New non-destructive route /site-finder/analysis/[cad]/ptica rendering the existing /analyze data in the «ПТИЦА» operator-terminal cockpit. The light analysis page, Section1-6, globals.css and package.json are untouched. PR#1 scope: app-shell (topbar + 4 tabs + left rail), hero (dark Leaflet map + parcel passport KV-grid + Buildability radial gauge + invest-score block), and the Development Scan card grid — wired to useParcelAnalyzeQuery / ParcelAnalysis. Data honesty: real fields render live (passport egrn.*, district, utilities.summary, median_price, pipeline_24mo); absent fields show muted placeholders with source captions (buildability/risk = derived proxy «предв.»; invest/buy-signal «после прогноза»; КСИТ/height/ОКС/economy «—») via typed ptica-adapt.ts — no fake numbers presented as live. Dark theme is a CSS-module scoped under .pticaRoot[data-theme=dark] (cannot leak to the light app); IBM Plex Mono added via next/font (no dep/lockfile change). Leaflet via dynamic(ssr:false). TS strict, no any. Deferred to follow-up PRs: Scenarios (forecast), Reports+Compare tabs, 16 detail drawers, Three.js 3D massing.
61 lines
1.7 KiB
TypeScript
61 lines
1.7 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* ScanCard — generic dark cockpit card: title (+ optional badge), metric rows,
|
||
* and a "Подробнее" button that is RENDERED but disabled in INCREMENT 1 (drill-
|
||
* down detail ships in a later release).
|
||
*
|
||
* Each row's value flows from a {value, isReal, caption} view-model: placeholder
|
||
* fields are rendered muted with their caption so they never read as live data.
|
||
*/
|
||
|
||
import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";
|
||
import type { PticaScanCard } from "@/components/site-finder/ptica/ptica-adapt";
|
||
|
||
interface Props {
|
||
card: PticaScanCard;
|
||
}
|
||
|
||
export function ScanCard({ card }: Props) {
|
||
const { title, badge, rows, emptyState } = card;
|
||
|
||
return (
|
||
<div className={`${styles.card} ${styles.scanCard}`}>
|
||
<h3 className={styles.cardTitle}>
|
||
{title}
|
||
{badge && <span className={styles.cardTitleSub}>{badge}</span>}
|
||
</h3>
|
||
|
||
{emptyState ? (
|
||
<div className={styles.emptyState}>{emptyState}</div>
|
||
) : (
|
||
rows.map((row) => {
|
||
const placeholder = !row.field.isReal;
|
||
return (
|
||
<div
|
||
key={row.key}
|
||
className={`${styles.kvrow} ${
|
||
placeholder ? styles.kvrowPlaceholder : ""
|
||
}`}
|
||
>
|
||
<span className={styles.k}>{row.key}</span>
|
||
<span className={styles.v} title={row.field.caption}>
|
||
{row.field.value}
|
||
</span>
|
||
</div>
|
||
);
|
||
})
|
||
)}
|
||
|
||
<button
|
||
type="button"
|
||
className={styles.detailBtn}
|
||
disabled
|
||
aria-disabled="true"
|
||
title="Детализация — в следующем релизе"
|
||
>
|
||
Подробнее
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|