Multi-region audit vs the prototype styles.css/dashboard.html → 22 CSS fixes +
markup. Headline: .lowerGrid/.bottomGrid align-items:start so the sparse ОКС
«нет данных» card no longer stretches into a huge void. Plus restore missing
prototype bits: topbar icon-buttons (Экспорт→Отчёты, Полный экран→fullscreen,
Настройки aria-disabled «скоро»), rail help-dot, app-footer (honest disclaimer,
no fabricated model/timestamp), invest-score sparkline (decorative, aria-hidden),
green/amber value tones, engineering status-row dots (from real nearest_m),
verdict-card gradient, scan-card status-rows, .canvas/.hbar/.mixHead spacing.
All new CSS scoped under .pticaRoot[data-theme=dark] (status-row classes
descendant-scoped under .scanCard — no collision with the drawer's). Environment
card stays honest kvrow placeholders (no fake dots). a11y: aria-labels on icon
buttons. TS strict, no any. tsc/lint/prettier/build green. code-reviewer ✅.
96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* ScanCard — generic dark cockpit card: title (+ optional badge), metric rows,
|
||
* and a "Подробнее" button that opens the card's detail drawer.
|
||
*
|
||
* When `drawerKey` + `onOpen` are provided the button is ENABLED and opens the
|
||
* matching drawer; without them it stays disabled (no drill-down wired). 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";
|
||
import type { DrawerKey } from "@/components/site-finder/ptica/drawers/drawer-keys";
|
||
|
||
interface Props {
|
||
card: PticaScanCard;
|
||
/** When set (with onOpen) the "Подробнее" button opens this drawer. */
|
||
drawerKey?: DrawerKey;
|
||
onOpen?: (key: DrawerKey) => void;
|
||
}
|
||
|
||
export function ScanCard({ card, drawerKey, onOpen }: Props) {
|
||
const { title, badge, rows, emptyState } = card;
|
||
const canOpen = Boolean(drawerKey && onOpen);
|
||
|
||
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) => {
|
||
// 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 (
|
||
<div key={row.key} className={styles.statusRow}>
|
||
<span className={`${styles.dot} ${dotClass}`} />
|
||
<span className={styles.label}>{row.key}</span>
|
||
<span
|
||
className={`${styles.state} ${stateClass}`}
|
||
title={row.field.caption}
|
||
>
|
||
{row.field.value}
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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={!canOpen}
|
||
aria-disabled={!canOpen}
|
||
title={
|
||
canOpen ? "Открыть детализацию" : "Детализация — в следующем релизе"
|
||
}
|
||
onClick={
|
||
canOpen && drawerKey && onOpen ? () => onOpen(drawerKey) : undefined
|
||
}
|
||
>
|
||
Подробнее
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|