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
This commit is contained in:
Tam Nhu Tran committed 2026-04-25 12:16:27 -04:00
1 parent eba53715e7
commit 64e78f6e69
20 files changed
+1513

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
# Design System — Decisions Log
The 6 open questions from the brainstorm phase, resolved before implementation.
| # | Question | Decision | Rationale |
|---|----------|----------|-----------|
| 1 | Storybook vs in-app `/_styleguide` | **In-app `/_styleguide`** | Zero-config, lives in the repo, gated by `import.meta.env.DEV`. Storybook is a heavy second build pipeline for marginal benefit at this scale. |
| 2 | Archetype B name (Monitor vs Dashboard) | **Monitor** | "Dashboard" is the whole product. "Monitor" matches existing health/analytics framing and avoids overloading the term. |
| 3 | JsonPane editability scope | **Read-only by default**, opt-in `editable` prop | Cliproxy is the only page that genuinely needs in-pane editing today. Opt-in keeps the API safe for codex/copilot/cursor (read-only). |
| 4 | Health terminal aesthetic | **Keep** as `MonitorCard variant="terminal"` | Preserves the `ccs health --watch` feel. Implemented as opt-in variant, not a separate primitive. |
| 5 | i18n key namespace convention | **Per-page namespaces** (`pages.<name>.*`) + shared (`common.*`) | Current i18n already uses ad-hoc per-page keys; this just formalizes it. Primitives use `common.*` so they're translation-stable across pages. |
| 6 | SectionRail activation | **Scroll-spy** via `IntersectionObserver` | Preserves long-form config feel + shows all validation errors at once. Click-to-switch hides errors in inactive sections, which is worse UX for forms. |
---
## How to revisit
If a decision turns out wrong in practice, update this doc and bump the affected primitive — don't silently drift. Each row above should be appended with a "Revised: <date> · <reason>" line if changed.
+179
View File
@@ -0,0 +1,179 @@
# CCS Dashboard Design System
The dashboard ships **two page archetypes**. Every page picks exactly one. All pages share the same outer chrome via `PageShell` + `PageHeader`.
> Live preview in dev: `bun run dev` then visit `/_styleguide`.
---
## 1. The shell — every page
```
PageShell
├─ PageHeader (title · description · status · actions)
└─ <archetype body>
```
```tsx
<PageShell>
<PageHeader title="Cliproxy" status={…} actions={…} />
<ConfigLayout … /> {/* OR <MonitorLayout> */}
</PageShell>
```
Header rules:
- `title` is mandatory; `description` optional 1-line subtitle.
- `status` slot: small badges/chips (e.g. "Running · :8317").
- `actions` slot: button group; **primary action belongs in FormPane footer**, not here, for Config pages.
---
## 2. Config archetype — 3-pane
Used by every page that configures one or more entities (cliproxy, accounts, codex, copilot, cursor, droid, claude-extension, api, shared, profiles).
```
┌──────────┬──────────────────┬──────────┐
│ left │ form (FormPane) │ json │
│ rail │ │ (right) │
└──────────┴──────────────────┴──────────┘
```
One component, prop-controlled left rail:
```tsx
// Multi-entity (cliproxy, accounts, providers)
<ConfigLayout
left={<ListPane items={…} selectedId={id} onSelect={…} />}
form={<FormPane>…</FormPane>}
json={<JsonPane data={…} />}
/>
// Single-entity (codex, copilot, cursor, droid, claude-extension)
<ConfigLayout
left={<SectionRail sections={…} />}
form={<FormPane>…</FormPane>}
json={<JsonPane data={…} />}
/>
// No rail (rare — only if page has neither entities to pick nor sections to navigate)
<ConfigLayout form={<FormPane>…</FormPane>} json={<JsonPane data={…} />} />
```
### Rules
- **Layout**: left 260px / form flex / json 360px on `>=1024px`.
- **<1024px**: collapses to tabs (`Browse | Configure | JSON`).
- **Save action**: lives **only** in `FormPane footer`. Never duplicate in PageHeader.
- **Per-entity actions** (delete, duplicate, sync): ListPane row trailing actions or FormPane header secondary actions — never both.
- **JsonPane is read-only by default**. Pass `editable` prop only on pages that need inline editing (cliproxy is the canonical example).
### SectionRail
For single-entity pages. Provides anchor nav with `IntersectionObserver` scroll-spy. Each item must match the `id` of a `<FormSection id="…">` in the FormPane.
```tsx
<SectionRail
sections={[
{ id: 'general', label: 'General' },
{ id: 'auth', label: 'Authentication' },
{ id: 'routing', label: 'Routing' },
]}
/>
// FormPane:
<FormSection id="general" title="General">…</FormSection>
<FormSection id="auth" title="Authentication">…</FormSection>
```
---
## 3. Monitor archetype — KPI row + 12-col grid
Used by every page that shows live state (home, analytics, health, logs, future status pages).
```
┌────────────────────────────────────────────────┐
│ KpiRow: [KPI] [KPI] [KPI] [KPI] │ (optional, ≤4 hero numbers)
├────────────────────────────────────────────────┤
│ MonitorGrid (12-col): │
│ <MonitorCard span={8}> primary viz │
│ <MonitorCard span={4}> side widget │
│ <MonitorCard span={6}> ... │
│ <MonitorCard span={6}> ... │
└────────────────────────────────────────────────┘
```
```tsx
<MonitorLayout
kpis={
<KpiRow>
<KpiCard label="Active accounts" value="87" hint="▲ 3" tone="positive" />
…
</KpiRow>
}
>
<MonitorGrid>
<MonitorCard span={8} title="Live monitor">…</MonitorCard>
<MonitorCard span={4} title="Top providers">…</MonitorCard>
<MonitorCard span={6} title="Requests" meta="last 24h">…</MonitorCard>
<MonitorCard span={6} variant="terminal" title="$ ccs health --watch">…</MonitorCard>
</MonitorGrid>
</MonitorLayout>
```
### Rules
- **KpiRow** only when there are ≤4 hero numbers. More than 4 → use `MonitorCard`s inside the grid instead.
- **One primary viz per page** — span ≥8 cols. Preserves the "punch" of home and analytics.
- **`variant="terminal"`** = dark monospace card, for live-log / `health --watch` aesthetics. Opt-in only.
- **Spans clamp at 6 cols on tablet**, single column on mobile.
---
## 4. When NOT to use either archetype
These remain bespoke:
- `/login` — minimal centered shell, no header/grid.
- Setup wizard — modal overlay, not a page.
- Dialogs — radix `Dialog`, not a layout.
If you find yourself fighting the shell, the answer is usually "this isn't a Config or Monitor page" — talk to the maintainers before inventing a third archetype.
---
## 5. Composing a new page
The recipe for any new page is:
```tsx
import { PageShell, PageHeader } from '@/components/page-shell';
import { ConfigLayout, /*…*/ } from '@/components/config-layout';
export function MyPage() {
return (
<PageShell>
<PageHeader title="My Page" actions={<Actions/>} />
<ConfigLayout
left={…}
form={<FormPane>…</FormPane>}
json={<JsonPane data={…}/>}
/>
</PageShell>
);
}
```
Target LOC for a new page: **~80** for typical config, **~120** for monitor. If your page exceeds 400 LOC it should be split into `pages/<name>/` with section files.
---
## 6. Lint / enforcement
Phase 4 adds a lint rule: every file under `src/pages/*.tsx` (or `src/pages/*/index.tsx`) must import `PageShell`. Until then, code review enforces the contract.
---
## 7. Decisions
See [`design-decisions.md`](./design-decisions.md) for the resolutions of the 6 open questions from the brainstorm phase.