mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
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
57 lines
1.7 KiB
TypeScript
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>
|
|
);
|
|
}
|