// Mobile ( void; }) { return ; } function StripCell({ kicker, value, valueClass, last }: { kicker: string; value: string; valueClass: string; last?: boolean; }) { return (
{kicker}
{value}
); } /** CRITICAL / HIGH / LAST SCAN strip that the desktop masthead carries inline; * on mobile the masthead hides its stat cluster and this sits below the tabs. */ export function SecuritySevStrip({ overview }: { overview: SecurityOverview }) { return (
0 ? 'text-destructive' : 'text-stat-value'} /> 0 ? 'text-warning' : 'text-stat-value'} />
); } /** The six desktop totals as a 3-col x 2-row hairline-divided card (replaces the * min-w-[640px] SignalRail, which forces a horizontal scroll on a phone). */ export function SecurityTotalsGrid({ overview }: { overview: SecurityOverview }) { const cells: Array<{ kicker: string; value: number; valueClass: string }> = [ { kicker: 'scanned', value: overview.scannedImages, valueClass: 'text-stat-value' }, { kicker: 'fixable', value: overview.fixable, valueClass: overview.fixable > 0 ? 'text-warning' : 'text-stat-value' }, { kicker: 'secrets', value: overview.secrets, valueClass: overview.secrets > 0 ? 'text-destructive' : 'text-stat-value' }, { kicker: 'misconfigs', value: overview.misconfigs, valueClass: overview.misconfigs > 0 ? 'text-warning' : 'text-stat-value' }, { kicker: 'stale', value: overview.staleScans, valueClass: overview.staleScans > 0 ? 'text-warning' : 'text-stat-value' }, { kicker: 'failed', value: overview.failedScans, valueClass: overview.failedScans > 0 ? 'text-destructive' : 'text-stat-value' }, ]; return (
{cells.map((cell, i) => (
= 3 && 'border-t border-hairline')} > {cell.kicker}
{cell.value}
))}
); } /** Footer freshness band (§9.11): context, not chrome. States the truthful scan * freshness and scanner version for the active node. */ export function SecurityFooterBand({ overview }: { overview: SecurityOverview }) { const parts: string[] = [ overview.lastSuccessfulScanAt ? `last scan ${formatTimeAgo(overview.lastSuccessfulScanAt)}` : 'no successful scan yet', ]; if (overview.scanner.available) { parts.push(`${overview.scanner.source}${overview.scanner.version ? ` v${overview.scanner.version}` : ''}`); } else { parts.push('scanner not installed'); } return (
{parts.join(' · ')}
); } const COUNT_TAG_TONE = { destructive: 'border-destructive/30 bg-destructive/[0.14] text-destructive', warning: 'border-warning/30 bg-warning/[0.14] text-warning', success: 'border-success/30 bg-success/[0.14] text-success', } as const; function CountTag({ tone, children }: { tone: keyof typeof COUNT_TAG_TONE; children: ReactNode }) { return ( {children} ); } /** One image row in the mobile Images list: severity dot, truncated mono ref over * a freshness meta line, trailing C/H count tags (or CLEAN), and a chevron. */ export function ImageScanRow({ summary, onInspect }: { summary: ScanSummary; onInspect: (scanId: number, initialTab?: ScanDetailTab) => void; }) { // Use the shared classifier so the count tags agree with the leading dot: a // medium/low-only image is not "clean", it is its highest severity. const severityKey = getSeverityKey(summary); const clean = severityKey === 'CLEAN'; return ( ); } export interface ImageFilterChip { value: ImageFilterValue; label: string; } /** The Images severity filter is the shared mobile chip row. */ export function ImageFilterChips({ chips, active, onSelect }: { chips: ImageFilterChip[]; active: ImageFilterValue; onSelect: (value: ImageFilterValue) => void; }) { return ; }