gendesign/frontend/src/components/site-finder/ptica/BuildabilityGauge.tsx
Light1YT 05eb70eb62
All checks were successful
CI / changes (pull_request) Successful in 5s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 55s
CI / openapi-codegen-check (pull_request) Successful in 1m57s
feat(site-finder): ПТИЦА dark cockpit — increment 1 (shell + hero + Development Scan)
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.
2026-06-20 15:29:53 +05:00

81 lines
2.3 KiB
TypeScript

"use client";
/**
* BuildabilityGauge — reusable SVG radial gauge for the cockpit.
*
* Renders a 0..100 value (or a muted track when value is null) with a color
* keyed to the `tone` bucket via CSS-module classes (which resolve to dark
* tokens). Optional footnote (e.g. "предв.") flags soft/preliminary proxies.
*/
import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";
import type { PticaGauge } from "@/components/site-finder/ptica/ptica-adapt";
const RADIUS = 52;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
const PROG_CLASS: Record<PticaGauge["tone"], string> = {
good: styles.gaugeGood,
warn: styles.gaugeWarn,
bad: styles.gaugeBad,
neutral: styles.gaugeNeutral,
};
const NUM_CLASS: Record<PticaGauge["tone"], string> = {
good: styles.gaugeNumGood,
warn: styles.gaugeNumWarn,
bad: styles.gaugeNumBad,
neutral: "",
};
interface Props {
gauge: PticaGauge;
/** Title shown above the gauge (e.g. "Buildability Index"). */
title: string;
}
export function BuildabilityGauge({ gauge, title }: Props) {
const { value, label, tone, footnote } = gauge;
const pct = value ?? 0;
const dashOffset = CIRCUMFERENCE * (1 - pct / 100);
return (
<div className={styles.gaugePanel}>
<div
className={styles.cardTitle}
style={{ fontSize: "9.5px", textAlign: "center" }}
>
{title}
</div>
<div className={styles.gauge}>
<svg viewBox="0 0 120 120" aria-hidden="true">
<circle
className={styles.gaugeTrack}
cx="60"
cy="60"
r={RADIUS}
strokeWidth="9"
/>
{value != null && (
<circle
className={`${styles.gaugeProg} ${PROG_CLASS[tone]}`}
cx="60"
cy="60"
r={RADIUS}
strokeWidth="9"
strokeDasharray={CIRCUMFERENCE}
strokeDashoffset={dashOffset}
/>
)}
</svg>
<div className={styles.gaugeCenter}>
<div className={`${styles.gaugeNum} ${NUM_CLASS[tone]}`}>
{value != null ? `${value}%` : "—"}
</div>
<div className={styles.gaugeLab}>{label}</div>
</div>
</div>
{footnote && <div className={styles.gaugeFootnote}>{footnote}</div>}
</div>
);
}