diff --git a/frontend/src/app/site-finder/analysis/[cad]/ptica/ptica.module.css b/frontend/src/app/site-finder/analysis/[cad]/ptica/ptica.module.css index c3672e2c..9e9a02f1 100644 --- a/frontend/src/app/site-finder/analysis/[cad]/ptica/ptica.module.css +++ b/frontend/src/app/site-finder/analysis/[cad]/ptica/ptica.module.css @@ -200,6 +200,19 @@ text-align: center; line-height: 1.5; } +/* Rail nav-foot help-dot (?) button (prototype .help-dot). */ +.helpDot { + width: 24px; + height: 24px; + border-radius: 50%; + border: var(--line) solid var(--border-strong); + display: grid; + place-items: center; + font-size: 11px; + background: transparent; + color: var(--text-soft); + cursor: pointer; +} /* main column */ .main { @@ -286,15 +299,52 @@ color: var(--text); font-weight: 600; } +/* Topbar sysbar icon-buttons (prototype .icon-btn). */ +.iconBtn { + width: 30px; + height: 30px; + border: var(--line) solid transparent; + border-radius: var(--radius-xs); + background: transparent; + display: grid; + place-items: center; + color: var(--text-muted); +} +.iconBtn:hover { + color: var(--text); + background: var(--surface); + border-color: var(--border); +} +.sysbar .iconBtn + .iconBtn { + margin-left: -2px; +} +.iconBtn svg { + width: 16px; + height: 16px; +} /* canvas (scroll area) */ .canvas { - padding: 14px 18px 24px; + padding: 14px 18px 8px; display: flex; flex-direction: column; gap: 12px; } +/* Bottom disclaimer footer bar (prototype .app-footer). */ +.appFooter { + display: flex; + justify-content: space-between; + gap: 16px; + padding: 10px 20px 14px; + border-top: var(--line) solid var(--border); + color: var(--text-soft); + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.06em; + font-family: var(--font-mono); +} + /* ===================== PANEL / CARD primitives ===================== */ .panel, .card, @@ -563,6 +613,27 @@ .investPlaceholder { color: var(--text-soft); } +/* Hero stat-line semantic value colors (prototype .v.good / .v.warn). */ +.vGood { + color: var(--accent-green); +} +.vWarn { + color: var(--accent-yellow); +} +/* Hero invest-score sparkline (prototype .sparkline). */ +.sparkline { + display: flex; + align-items: flex-end; + gap: 3px; + height: 26px; +} +.sparkline span { + flex: 1; + background: linear-gradient(var(--accent-cyan), var(--accent-blue)); + border-radius: 1px; + min-height: 3px; + opacity: 0.85; +} .badge { display: inline-block; padding: 2px 9px; @@ -712,6 +783,46 @@ margin-top: auto; align-self: flex-start; } +/* Scan status-rows (prototype Инженерия / Экология .status-row): dot + label + + state. Scoped under .scanCard so they never collide with the unrelated + DRAWER .statusRow variant (drDot / drLabel / drState). */ +.scanCard .statusRow { + display: flex; + align-items: center; + gap: 7px; + font-size: 11px; + padding: 3px 0; +} +.scanCard .dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--accent-green); + box-shadow: 0 0 6px currentColor; + color: var(--accent-green); + flex: none; +} +.scanCard .dotWarn { + background: var(--accent-yellow); + color: var(--accent-yellow); +} +.scanCard .dotBad { + background: var(--accent-red); + color: var(--accent-red); +} +.scanCard .label { + flex: 1; + color: var(--text-muted); +} +.scanCard .state { + font-size: 10px; +} +.scanCard .stateOk { + color: var(--accent-green); +} +.scanCard .stateWarn { + color: var(--accent-yellow); +} /* Risks card centres its gauge; title stays left-aligned like the other cards. */ .scanCardCentered { align-items: center; @@ -2200,6 +2311,27 @@ display: grid; grid-template-columns: 1.05fr 1.05fr 1.15fr 0.95fr; gap: 10px; + /* HEADLINE BUG FIX: default align-items:stretch made the sparse/empty ОКС + card (isoBox 84px + emptyState flex:1 + button) stretch to the tallest + sibling, and .emptyState's flex:1 then filled the void → huge empty box. + align-items:start lets each card size to its own content. */ + align-items: start; +} + +/* Lower-grid квартирография bars match the prototype .bar-row (narrow label / + value columns, 2px track radius) — SCOPED so the shared base .hbar used by + the drawer (110px 1fr 46px, 3px radius) is NOT crushed. */ +.lowerGrid .hbars { + gap: 8px; +} +.lowerGrid .hbar { + grid-template-columns: 46px 1fr 38px; +} +.lowerGrid .hbarTrack { + border-radius: 2px; +} +.lowerGrid .hbarFill { + border-radius: 2px; } /* shared detail-btn that needs top separation inside flow cards */ @@ -2280,7 +2412,7 @@ text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-soft); - margin-bottom: 8px; + margin-bottom: 1px; } /* Инсоляционная матрица — faint preview massing + ПРЕВЬЮ placeholder */ @@ -2341,6 +2473,9 @@ display: grid; grid-template-columns: 1.5fr 0.62fr 0.86fr 1.2fr; gap: 10px; + /* Same grid-stretch defect as .lowerGrid — sparse Legal Status / cards must + not stretch to the tallest sibling. Pin each card to the top. */ + align-items: start; } /* Investment Clearance — bignum row */ @@ -2434,6 +2569,9 @@ align-items: center; gap: 10px; text-align: center; + /* Prototype .verdict-card plaque gradient — scoped dark cockpit (authoritative + here over the global flat-token rule). */ + background: linear-gradient(135deg, var(--surface), var(--surface-2)); } .verdictSplit { display: grid; @@ -2494,6 +2632,8 @@ } .checkItemWarn .checkIc { color: var(--accent-yellow); + /* Prototype removes the icon border ring on warn checklist items. */ + border: none; } .checkItemBad .checkIc { color: var(--accent-red); diff --git a/frontend/src/components/site-finder/ptica/InvestScoreBlock.tsx b/frontend/src/components/site-finder/ptica/InvestScoreBlock.tsx index 519f87ab..5824e9ff 100644 --- a/frontend/src/components/site-finder/ptica/InvestScoreBlock.tsx +++ b/frontend/src/components/site-finder/ptica/InvestScoreBlock.tsx @@ -11,8 +11,19 @@ import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css"; import type { ParcelAnalysis } from "@/types/site-finder"; import type { ForecastReport } from "@/types/forecast"; import { adaptInvestScore } from "@/components/site-finder/ptica/ptica-adapt"; +import type { PticaValueTone } from "@/components/site-finder/ptica/ptica-adapt"; import type { DrawerKey } from "@/components/site-finder/ptica/drawers/drawer-keys"; +/** Decorative hero sparkline shape (prototype data-spark="4,5,6,5,7,6,8,7,9,8"). */ +const SPARK = [4, 5, 6, 5, 7, 6, 8, 7, 9, 8]; +const SPARK_MAX = Math.max(...SPARK); + +function toneClass(tone: PticaValueTone): string { + if (tone === "good") return styles.vGood; + if (tone === "warn") return styles.vWarn; + return ""; +} + interface Props { analysis: ParcelAnalysis; /** §22 forecast report — when ready, surfaces the REAL invest-score / buy-signal. */ @@ -48,17 +59,31 @@ export function InvestScoreBlock({ {inv.score.caption && ( {inv.score.caption} )} +
Потенциал - + {inv.potential.value}
Риск - + {inv.risk.value}
diff --git a/frontend/src/components/site-finder/ptica/PticaShell.tsx b/frontend/src/components/site-finder/ptica/PticaShell.tsx index d4ea0b78..a8c548f8 100644 --- a/frontend/src/components/site-finder/ptica/PticaShell.tsx +++ b/frontend/src/components/site-finder/ptica/PticaShell.tsx @@ -215,6 +215,16 @@ const BirdMark = ({ className }: { className: string }) => ( ); +/** Toggle native fullscreen for the whole document (topbar fullscreen icon). */ +function toggleFullscreen() { + if (typeof document === "undefined") return; + if (document.fullscreenElement) { + void document.exitFullscreen?.(); + } else { + void document.documentElement.requestFullscreen?.(); + } +} + interface Props { activeTab: PticaTab; onTabChange: (tab: PticaTab) => void; @@ -294,6 +304,15 @@ export function PticaShell({ activeTab, onTabChange, children }: Props) {
{clock ?? "—"} + @@ -330,10 +349,76 @@ export function PticaShell({ activeTab, onTabChange, children }: Props) {
{clock ?? "—"}
+ + +
{children}
+ + ); diff --git a/frontend/src/components/site-finder/ptica/ScanCard.tsx b/frontend/src/components/site-finder/ptica/ScanCard.tsx index 83e348ca..0f7f6749 100644 --- a/frontend/src/components/site-finder/ptica/ScanCard.tsx +++ b/frontend/src/components/site-finder/ptica/ScanCard.tsx @@ -36,6 +36,30 @@ export function ScanCard({ card, drawerKey, onOpen }: Props) {
{emptyState}
) : ( rows.map((row) => { + // Status-row variant (prototype .status-row): dot + label + state. + if (row.status) { + const dotClass = + row.status === "warn" + ? styles.dotWarn + : row.status === "bad" + ? styles.dotBad + : ""; + const stateClass = + row.status === "ok" ? styles.stateOk : styles.stateWarn; + return ( +
+ + {row.key} + + {row.field.value} + +
+ ); + } + const placeholder = !row.field.isReal; return (
({ key: utilityLabel(u.subtype), field: real(`${formatInt(u.nearest_m)} м`), + status: (u.nearest_m <= 1000 ? "ok" : "warn") as PticaStatusTone, })), }; }