Files
ccs/ui/src/components/monitor-layout/kpi.tsx
T
Tam Nhu Tran 64e78f6e69 feat(ui): add design system foundations
Introduce two locked page archetypes -- Config (3-pane) and Monitor
(KPI row + 12-col grid) -- wrapped by shared PageShell + PageHeader.

Primitives:
- page-shell/: PageShell, PageHeader, EmptyState, ErrorState
- config-layout/: ConfigLayout, ListPane, SectionRail, FormPane,
  FormSection, JsonPane (read-only by default, opt-in editable)
- monitor-layout/: MonitorLayout, KpiRow, KpiCard, MonitorGrid,
  MonitorCard (incl. variant="terminal")

Single ConfigLayout component, prop-controlled left rail:
- ListPane for multi-entity (cliproxy, accounts, providers)
- SectionRail for single-entity (codex, copilot, cursor, droid)
  with IntersectionObserver scroll-spy
- omit for none

DEV-ONLY /_styleguide route gated by import.meta.env.DEV. Shows every
primitive plus composed Config (multi + single) and Monitor demos using
fully anonymized data (Provider A/B/C, fake metrics).

Phase 1 of dashboard design system unification. Existing pages untouched
this phase -- migrations land in Phase 2+.

Locked decisions in ui/docs/design-decisions.md:
1. In-app /_styleguide over Storybook
2. Archetype name: Monitor (not Dashboard)
3. JsonPane read-only by default
4. Health terminal aesthetic kept as variant
5. i18n per-page namespaces
6. SectionRail uses scroll-spy
2026-04-25 12:16:27 -04:00

57 lines
1.7 KiB
TypeScript

import type { ReactNode } from 'react';
import { cn } from '@/lib/utils';
interface KpiRowProps {
children: ReactNode;
className?: string;
}
/**
* KpiRow - Container row for hero stat tiles.
*
* Use only when there are ≤4 hero numbers. More than 4 → consider grouping into a card grid.
* Responsive: 2 cols on mobile, 4 on desktop.
*/
export function KpiRow({ children, className }: KpiRowProps) {
return (
<div className={cn('grid grid-cols-2 gap-3 sm:gap-4 lg:grid-cols-4', className)}>
{children}
</div>
);
}
interface KpiCardProps {
label: ReactNode;
value: ReactNode;
hint?: ReactNode;
/** Tone for the hint line (e.g. positive delta, warning, etc.). */
tone?: 'default' | 'positive' | 'warning' | 'negative';
icon?: ReactNode;
className?: string;
}
const TONE_CLASSES: Record<NonNullable<KpiCardProps['tone']>, string> = {
default: 'text-muted-foreground',
positive: 'text-emerald-600 dark:text-emerald-400',
warning: 'text-amber-600 dark:text-amber-400',
negative: 'text-destructive',
};
/**
* KpiCard - Single hero stat tile inside a KpiRow.
*/
export function KpiCard({ label, value, hint, tone = 'default', icon, className }: KpiCardProps) {
return (
<div className={cn('rounded-xl border bg-card p-4 shadow-sm', className)}>
<div className="flex items-center justify-between">
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
{label}
</p>
{icon && <span className="text-muted-foreground">{icon}</span>}
</div>
<p className="mt-2 text-2xl font-semibold tracking-tight tabular-nums">{value}</p>
{hint && <p className={cn('mt-1 text-xs', TONE_CLASSES[tone])}>{hint}</p>}
</div>
);
}