From bfe9549d02ad7ca21202025d593e5c988c14f437 Mon Sep 17 00:00:00 2001 From: lorentz Date: Sun, 3 May 2026 16:50:35 -0400 Subject: [PATCH] feat(03-01): add KpiCardMobile, NeedsAttentionStrip, WorkerStatusRow components - KpiCardMobile: phone-sized KPI card with label/value/caption; tone='attention' adds destructive left border - NeedsAttentionStrip: horizontal-scroll strip of compact attention cards; renders null when items=[] - WorkerStatusRow: 3-cell status row with emerald/amber/destructive status dots, each linking to desktop admin - All three are pure presentational client components; no fetch, no recharts, no new state libraries --- components/mobile/KpiCardMobile.tsx | 39 ++++++++++++++++ components/mobile/NeedsAttentionStrip.tsx | 51 +++++++++++++++++++++ components/mobile/WorkerStatusRow.tsx | 55 +++++++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 components/mobile/KpiCardMobile.tsx create mode 100644 components/mobile/NeedsAttentionStrip.tsx create mode 100644 components/mobile/WorkerStatusRow.tsx diff --git a/components/mobile/KpiCardMobile.tsx b/components/mobile/KpiCardMobile.tsx new file mode 100644 index 0000000..342a84e --- /dev/null +++ b/components/mobile/KpiCardMobile.tsx @@ -0,0 +1,39 @@ +'use client'; + +/* KpiCardMobile — phase 03 (DASH-01). + * + * Phone-sized KPI card for the 2×2 dashboard grid. Renders a label, + * a large numeric value, and an optional caption. tone="attention" + * adds a left-edge destructive border for SLA breaches > 0. + * + * Pure presentational — no fetch, no state. Parent provides values. */ + +import { Card, CardContent } from '@/components/ui/card'; +import { cn } from '@/lib/utils'; + +export type KpiTone = 'default' | 'attention'; + +interface KpiCardMobileProps { + label: string; + value: number | string; + caption?: string; + tone?: KpiTone; +} + +const TONE_BORDER: Record = { + default: 'border-l-transparent', + attention: 'border-l-destructive', +}; + +export function KpiCardMobile({ label, value, caption, tone = 'default' }: KpiCardMobileProps) { + const display = typeof value === 'number' ? value.toLocaleString() : value; + return ( + + +

{label}

+

{display}

+ {caption &&

{caption}

} +
+
+ ); +} diff --git a/components/mobile/NeedsAttentionStrip.tsx b/components/mobile/NeedsAttentionStrip.tsx new file mode 100644 index 0000000..d754bf4 --- /dev/null +++ b/components/mobile/NeedsAttentionStrip.tsx @@ -0,0 +1,51 @@ +'use client'; + +/* NeedsAttentionStrip — phase 03 (DASH-02). + * + * Horizontal-scroll strip of compact attention cards. Each card shows a + * count + label and is a next/link to the destination view. The strip + * uses native horizontal overflow with snap-x for momentum scroll on + * iOS/Android. Renders nothing when items=[]. */ + +import Link from 'next/link'; +import { AlertTriangle, ChevronRight } from 'lucide-react'; + +export interface NeedsAttentionItem { + id: string; + label: string; + count: number; + href: string; +} + +interface NeedsAttentionStripProps { + items: NeedsAttentionItem[]; +} + +export function NeedsAttentionStrip({ items }: NeedsAttentionStripProps) { + if (items.length === 0) return null; + return ( +
+

+ Needs attention +

+
+ {items.map(item => ( + +
+ 0 ? 'text-destructive' : 'text-muted-foreground'}`} /> + +
+

0 ? 'text-destructive' : ''}`}> + {item.count} +

+

{item.label}

+ + ))} +
+
+ ); +} diff --git a/components/mobile/WorkerStatusRow.tsx b/components/mobile/WorkerStatusRow.tsx new file mode 100644 index 0000000..78a1887 --- /dev/null +++ b/components/mobile/WorkerStatusRow.tsx @@ -0,0 +1,55 @@ +'use client'; + +/* WorkerStatusRow — phase 03 (DASH-03). + * + * Compact 3-cell read-only status row showing analyzer worker, RMM worker, + * and backup success rate. Each cell is a next/link to the corresponding + * desktop admin page. status='ok' = emerald dot, 'warn' = amber, 'down' = + * destructive. */ + +import Link from 'next/link'; +import { ExternalLink } from 'lucide-react'; + +export type WorkerStatus = 'ok' | 'warn' | 'down'; + +export interface WorkerStatusEntry { + id: string; + label: string; + value: string; + status: WorkerStatus; + href: string; +} + +interface WorkerStatusRowProps { + entries: WorkerStatusEntry[]; +} + +const DOT_COLOR: Record = { + ok: 'bg-emerald-500', + warn: 'bg-amber-500', + down: 'bg-destructive', +}; + +export function WorkerStatusRow({ entries }: WorkerStatusRowProps) { + return ( +
+

+ Workers & backups +

+
+ {entries.map(e => ( + +
+
+ ); +}