Port the prototype's missing sections — the React cockpit had only hero +
Development Scan, so the deployed page was incomplete («поплывшее»). Add:
- lower-grid: ОКС · Development Potential · Recommended Product (квартирография)
· Инсоляционная матрица (opens 3D);
- bottom-grid: Investment Clearance · Buy Signal · Legal Status · Site Verdict
(«Приобретать/Нельзя/Нужна проверка» plaque + checklist).
Move ОКС + Инсоляция out of DevelopmentScan → scan = 7 cards (prototype IA).
Data honesty: Development Potential (real КСИТ/площадь/плотность), Recommended
Product (forecast product_tz), Buy Signal + Site Verdict (forecast deficit +
gate_verdict, real, three-state), Legal (zouit/red_lines/gate). Investment
Clearance all «—» «после финмодели (§23)» — no fabricated numbers. Typed
{value,isReal,caption} adapters; scoped dark CSS; forecast-pending safe.
Cleanup: drop orphaned adaptOksCard/adaptInsolationCard; add .metric/.bignum.
80 lines
2.3 KiB
TypeScript
80 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}>
|
|
{title && (
|
|
<div className={`${styles.cardTitle} ${styles.cardTitleGauge}`}>
|
|
{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>
|
|
);
|
|
}
|