Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions .planning/design/system/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Carmen Platform — Component Inventory

> **Documented from the live codebase** (`src/components/`), not proposed. This is a
> frontend-only React + TypeScript **admin dashboard** (clusters, business units, users,
> report/print templates, platform RBAC). Built on **shadcn/ui (Radix + CVA)** + Tailwind.
>
> Every component consumes semantic tokens from **[`tokens.md`](./tokens.md)** — no
> hardcoded colors. Test files (`*.test.tsx`) co-locate beside each component.

---

## 1. Primitives — `src/components/ui/` (shadcn/ui)

**⚠️ Do NOT modify these without an explicit reason** (repo rule 2). Radix + CVA under the hood.

| Component | File | Notes |
|-----------|------|-------|
| Avatar | `avatar.tsx` | User/entity image with fallback initials |
| Badge | `badge.tsx` | Status pill — variants `success` / `warning` / `info` / `secondary` / `destructive` / `default`. **The only sanctioned status indicator** (rule 5) |
| Button | `button.tsx` | CVA variants (default/secondary/ghost/destructive/outline) + `size="icon"` |
| Card | `card.tsx` | Flat surface `bg-card` + 1px border — the primary layout container |
| ChipInput | `chip-input.tsx` | Tag/chip editor over a comma-joined string (in ↔ out) |
| ConfirmDialog | `confirm-dialog.tsx` | Destructive-action confirm. **Replaces `window.confirm()`** (rule 3). Self-manages async spinner |
| DataTable | `data-table.tsx` | TanStack Table v8 wrapper. Server-side pagination/sort. **Auto-prepends `#` index column — never add your own** (rule 4) |
| DevDebugSheet | `dev-debug-sheet.tsx` | Dev-only raw-response inspector (wrap in `NODE_ENV === 'development'`) |
| Dialog | `dialog.tsx` | Modal (Radix Dialog) |
| DropdownMenu | `dropdown-menu.tsx` | Row actions, context menus |
| Input | `input.tsx` | Text field; `border-destructive` on validation error |
| JsonViewer | `json-viewer.tsx` | Pretty JSON render for debug sheets |
| Label | `label.tsx` | Form field label |
| Select | `select.tsx` | Dropdown select (Radix) |
| Separator | `separator.tsx` | Horizontal/vertical divider |
| Sheet | `sheet.tsx` | Slide-in panel — filters, mobile nav, debug |
| Skeleton | `skeleton.tsx` | Loading placeholder primitive |
| Table | `table.tsx` | Raw table primitive (DataTable builds on it) |
| Tabs | `tabs.tsx` | Tabbed panels (e.g. ReportTemplate XML editor) |
| Textarea | `textarea.tsx` | Multi-line input |
| Tooltip | `tooltip.tsx` | Hover hints (collapsed sidebar labels, `delayDuration={200}`) |

---

## 2. Layout & navigation

| Component | File | Role |
|-----------|------|------|
| Layout | `Layout.tsx` | Shell orchestrator — owns sidebar collapse state (`localStorage('sidebar-collapsed')`), desktop breadcrumb bar, `allNavItems` registry. Main content margin `md:ml-16` ↔ `md:ml-60` |
| Sidebar | `Sidebar.tsx` | Desktop fixed sidebar (`w-60` / `w-16`) + mobile Sheet drawer. Nav items grouped `Organization` / `Content` / `Platform`; collapsed shows icons + right-side tooltips |
| Breadcrumbs | `Breadcrumbs.tsx` | Route breadcrumb trail (desktop top bar) |
| PageHeader | `PageHeader.tsx` | Standard page title + subtitle + action slot |

---

## 3. Access control

| Component | File | Role |
|-----------|------|------|
| PrivateRoute | `PrivateRoute.tsx` | Route guard — `requiredPermission` / `requireSuperAdmin` props |
| Can | `Can.tsx` | Inline permission-gate wrapper (conditionally render children) |

Both read from `AuthContext` (`hasPermission`, `isSuperAdmin`).

---

## 4. Data display & state

| Component | File | Role |
|-----------|------|------|
| TableSkeleton | `TableSkeleton.tsx` | Initial table load — use only when `loading && items.length === 0` |
| EmptyState | `EmptyState.tsx` | No-data state. Required `icon` + `title`; include `description` + action button |
| ReadOnlyField | `ReadOnlyField.tsx` | Styled read-only value (edit-page read mode) — `bg-muted/50` div |
| VersionBadge | `VersionBadge.tsx` | App version chip (changelog) |
| DialogPreview | `DialogPreview.tsx` | Renders XML `<Label>`+`<Date>`/`<Lookup>` pairs as preview |

**Loading decision table** (rule 16): skeleton (empty+loading) · overlay (has-data+loading) · EmptyState (empty+idle) · DataTable (otherwise).

---

## 5. Forms & inputs (app-level)

| Component | File | Role |
|-----------|------|------|
| SearchInput | `SearchInput.tsx` | Debounced (400ms) search box for Management pages |
| BusinessUnitMultiSelect | `BusinessUnitMultiSelect.tsx` | Multi-select BU picker |
| UserMultiSelect | `UserMultiSelect.tsx` | Multi-select user picker |
| PermissionPicker | `PermissionPicker.tsx` | Permission assignment UI (RBAC) |
| MarkdownEditor | `MarkdownEditor.tsx` | Markdown authoring (news/content) |
| XmlEditor | `XmlEditor.tsx` | CodeMirror 6 XML editor; falls back to read-only Copy+Download when `readOnly` |
| ImageUpload | `ImageUpload.tsx` | Generic image upload |
| BrandingImageUpload | `BrandingImageUpload.tsx` | BU/cluster branding image upload |

**Form field pattern:** every field renders two modes — edit (Input/Select/checkbox) and
read-only (`ReadOnlyField`). Validation via `validateField` on blur → `border-destructive`
+ `text-xs text-destructive` message.

---

## 6. Feedback & overlays

| Element | Source | Role |
|---------|--------|------|
| Toast | `sonner` (external, wired in `App.tsx`) | `toast.success/error/info/warning`. **Replaces `alert()`** (rule 3) |
| ConfirmDialog | `ui/confirm-dialog.tsx` | Destructive confirms |
| Dialog / Sheet | `ui/` | Modals and slide-in panels |
| Tooltip | `ui/tooltip.tsx` | Hover hints |
| KeyboardShortcuts | `KeyboardShortcuts.tsx` | Global `Ctrl/⌘+K` search, `Ctrl/⌘+S` save, `Esc` cancel, `?` help dialog (auto-wired in Layout) |

---

## 7. Domain-specific cards

Composite feature cards (not primitives) — each encapsulates a platform sub-flow.

| Component | File | Feature |
|-----------|------|---------|
| InterfaceEntitlementCard | `InterfaceEntitlementCard.tsx` | Business-unit interface entitlement control (Phase 1 platform) |
| TenantMigrationCard | `TenantMigrationCard.tsx` | Tenant schema migration (FleetSync + DeployConsole, streaming) |
| TenantSeedCard | `TenantSeedCard.tsx` | Per-BU tenant data seeding (streaming, extensible types) |
| DbConnectionView | `DbConnectionView.tsx` | Database connection detail view |

---

## Component → page-pattern map

The app has **two canonical page patterns** — always copy the closest existing example
(rule 1), don't invent layouts:

| Pattern | Reference | Key components |
|---------|-----------|----------------|
| **Management** (list) | `ClusterManagement.tsx` | PageHeader · SearchInput · filter Sheet · DataTable · TableSkeleton · EmptyState · DevDebugSheet · CSV export |
| **Edit** (CRUD) | `ClusterEdit.tsx` (simple), `ReportTemplateEdit.tsx` (tabbed) | Card sections · ReadOnlyField · edit/read toggle · ConfirmDialog · Tabs · `useUnsavedChanges` · DevDebugSheet (tabbed) |

**Deliberate deviations:** `PrintTemplateMappingManagement.tsx` (card-grouped config, no
DataTable) and `Application*` pages (asymmetric read/write model) — documented in CLAUDE.md.

---

## Token compliance (enforced across all components)

1. Colors via semantic classes only — `bg-primary`, `text-muted-foreground`, `border-border`,
`bg-card`. **Never** `bg-blue-500` / raw hex / raw green for status.
2. Status → `<Badge variant="…">`, never colored text/backgrounds by hand.
3. Radius via `rounded-lg` (cards) / `rounded-md` (buttons, inputs) / `rounded-sm` (badges).
4. Spacing on the 4px grid — `space-y-4`, `gap-3` (see tokens.md).
5. Dark theme is automatic — semantic tokens flip with `.dark`; no per-component overrides.
230 changes: 230 additions & 0 deletions .planning/design/system/tokens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# Carmen Platform — Design Tokens

> **Documented from the live system**, not generated. This file mirrors the actual
> tokens shipped in the "calm-corporate reskin" (PR #38, merged to `main`).
>
> **Source of truth:** `src/index.css` (CSS custom properties) + `tailwind.config.js`
> (Tailwind → variable bindings). If those files change, update this doc — never the
> other way around. HSL triplets are canonical; hex values are computed references.

**Personality:** calm / corporate / restrained-enterprise · warm-neutral ground with a
single trustworthy blue accent · flat surfaces (glassmorphism removed) · both light and
dark themes.

**Format note:** carmen-platform follows the shadcn/ui convention — CSS variables hold
**space-separated HSL channels without the `hsl()` wrapper** (e.g. `221 61% 48%`), and
Tailwind consumes them as `hsl(var(--token))`. This lets any color take an opacity
modifier, e.g. `bg-primary/50` → `hsl(var(--primary) / 0.5)`.

---

## Colors

Two themes. Dark theme is toggled by the `.dark` class on a root element
(`darkMode: ["class"]`). Every role below exists in both themes.

### Light theme (`:root`)

| Role | Variable | HSL (canonical) | Hex (approx) | Usage |
|------|----------|-----------------|--------------|-------|
| Background | `--background` | `40 9% 97.5%` | `#F9F9F8` | App canvas — warm off-white |
| Foreground | `--foreground` | `30 7% 12%` | `#211F1C` | Primary text (warm near-black) |
| Card | `--card` | `0 0% 100%` | `#FFFFFF` | Card / panel surface |
| Card foreground | `--card-foreground` | `30 7% 12%` | `#211F1C` | Text on cards |
| Popover | `--popover` | `0 0% 100%` | `#FFFFFF` | Dropdowns, tooltips, sheets |
| Popover foreground | `--popover-foreground` | `30 7% 12%` | `#211F1C` | Text in popovers |
| **Primary** | `--primary` | `221 61% 48%` | `#305FC5` | Buttons, links, active states, focus ring |
| Primary foreground | `--primary-foreground` | `0 0% 100%` | `#FFFFFF` | Text on primary |
| Secondary | `--secondary` | `40 8% 94%` | `#F1F0EE` | Secondary buttons, subtle fills |
| Secondary foreground | `--secondary-foreground` | `30 10% 20%` | `#38332E` | Text on secondary |
| Muted | `--muted` | `40 8% 95.5%` | `#F4F4F3` | Muted surfaces, read-only field bg |
| Muted foreground | `--muted-foreground` | `33 5% 43%` | `#736E68` | Subtitles, meta, placeholder text |
| Accent | `--accent` | `40 8% 94%` | `#F1F0EE` | Hover fills, subtle emphasis (**neutral, not a brand hue**) |
| Accent foreground | `--accent-foreground` | `30 10% 20%` | `#38332E` | Text on accent |
| Destructive | `--destructive` | `0 72% 51%` | `#DC2828` | Delete/danger actions, errors |
| Destructive foreground | `--destructive-foreground` | `0 0% 100%` | `#FFFFFF` | Text on destructive |
| Border | `--border` | `40 8% 90%` | `#E8E6E3` | 1px borders on all surfaces |
| Input | `--input` | `40 8% 87%` | `#E1DFDB` | Form control borders |
| Ring | `--ring` | `221 61% 48%` | `#305FC5` | Focus-visible outline |

### Light theme — status accents

Status uses **dedicated tokens**, never raw Tailwind greens/ambers (repo rule 5).

| Role | Variable | HSL | Hex (approx) | Usage |
|------|----------|-----|--------------|-------|
| Success | `--success` | `152 40% 36%` | `#37815E` | Active/healthy badges, positive state |
| Success foreground | `--success-foreground` | `0 0% 100%` | `#FFFFFF` | Text on success |
| Warning | `--warning` | `32 90% 44%` | `#D5770B` | Warnings, at-risk state |
| Warning foreground | `--warning-foreground` | `0 0% 100%` | `#FFFFFF` | Text on warning |
| Info | `--info` | `221 61% 48%` | `#305FC5` | Informational (mirrors primary) |
| Info foreground | `--info-foreground` | `0 0% 100%` | `#FFFFFF` | Text on info |

### Dark theme (`.dark`)

| Role | Variable | HSL (canonical) | Hex (approx) | Usage |
|------|----------|-----------------|--------------|-------|
| Background | `--background` | `30 6% 9%` | `#181716` | App canvas — warm near-black |
| Foreground | `--foreground` | `40 8% 90%` | `#E8E6E3` | Primary text |
| Card | `--card` | `30 6% 12%` | `#201F1D` | Card / panel surface |
| Card foreground | `--card-foreground` | `40 8% 90%` | `#E8E6E3` | Text on cards |
| Popover | `--popover` | `30 6% 12%` | `#201F1D` | Dropdowns, tooltips, sheets |
| Popover foreground | `--popover-foreground` | `40 8% 90%` | `#E8E6E3` | Text in popovers |
| **Primary** | `--primary` | `217 70% 60%` | `#5288E0` | Lighter blue — reads on dark ground |
| Primary foreground | `--primary-foreground` | `0 0% 100%` | `#FFFFFF` | Text on primary |
| Secondary | `--secondary` | `30 5% 17%` | `#2E2B29` | Secondary buttons, subtle fills |
| Secondary foreground | `--secondary-foreground` | `40 8% 95%` | `#F4F3F1` | Text on secondary |
| Muted | `--muted` | `30 5% 16%` | `#2B2927` | Muted surfaces |
| Muted foreground | `--muted-foreground` | `35 6% 62%` | `#A49F98` | Subtitles, meta, placeholders |
| Accent | `--accent` | `30 5% 18%` | `#302E2C` | Hover fills, subtle emphasis |
| Accent foreground | `--accent-foreground` | `40 8% 95%` | `#F4F3F1` | Text on accent |
| Destructive | `--destructive` | `0 62% 45%` | `#BA2C2C` | Delete/danger, errors |
| Destructive foreground | `--destructive-foreground` | `0 0% 100%` | `#FFFFFF` | Text on destructive |
| Border | `--border` | `30 5% 20%` | `#363330` | 1px borders |
| Input | `--input` | `30 5% 22%` | `#3B3835` | Form control borders |
| Ring | `--ring` | `217 70% 60%` | `#5288E0` | Focus-visible outline |
| Success | `--success` | `152 45% 45%` | `#3FA676` | Positive state |
| Warning | `--warning` | `38 92% 55%` | `#F6A823` | Warning state |
| Warning foreground | `--warning-foreground` | `20 14% 8%` | `#171310` | Dark text on bright warning |
| Info | `--info` | `217 70% 60%` | `#5288E0` | Informational |

### Special-purpose tokens

Not part of the standard shadcn set — carmen-specific.

| Variable | Light | Dark | Usage |
|----------|-------|------|-------|
| `--zebra-even` | `rgba(24,20,16,0.02)` | `rgba(255,255,255,0.015)` | Even-row zebra striping in tables (`.zebra-row`) |
| `--zebra-hover` | `hsl(221 61% 48% / 0.045)` | `hsl(217 70% 60% / 0.08)` | Row hover fill |
| `--zebra-hover-accent` | `hsl(221 61% 48% / 0.5)` | `hsl(217 70% 60% / 0.4)` | 3px inset left bar on hover |
| `--bu-chip-s` / `--bu-chip-l` | `62%` / `46%` | `58%` / `58%` | Per-Business-Unit color identity (SQL Workbench). Hue derives from the BU code; S/L are theme-tuned |

---

## Typography

Single family, loaded from Google Fonts in `index.html`.

- **Font family (UI):** `Inter, system-ui, -apple-system, sans-serif`
- **Font family (code/mono):** `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace`
- **Loaded weights:** 400, 500, 600, 700
- **Base:** `font-feature-settings: "rlig" 1, "calt" 1`; body `line-height: 1.5`
- **Tabular numbers:** `.tabular-nums` utility (`font-variant-numeric: tabular-nums`) for aligned figures in tables

### Type scale (Tailwind utility conventions)

There is no numeric scale in CSS — the system uses Tailwind utility classes consistently.
These are the canonical patterns from the codebase:

| Role | Classes | Renders (base) |
|------|---------|----------------|
| Page title | `text-2xl sm:text-3xl font-bold tracking-tight` | 24 → 30px / 700 |
| Subtitle | `text-sm sm:text-base text-muted-foreground` | 14 → 16px / 400 |
| Section / card heading | `text-lg font-semibold` | 18px / 600 |
| Body | `text-sm` | 14px / 400 |
| Meta / caption | `text-xs` or `text-[11px]` | 12 / 11px |
| Code / mono | `text-[10px] sm:text-xs font-mono` | 10 → 12px |

---

## Spacing

Tailwind's default 4px-base scale. Canonical layout rhythms from the codebase:

| Context | Classes | Value |
|---------|---------|-------|
| Page wrapper (vertical) | `space-y-4 sm:space-y-6` | 16 → 24px |
| Card content (vertical) | `space-y-4` | 16px |
| Form field (label→input) | `space-y-2` | 8px |
| Button / inline gaps | `gap-3` | 12px |
| Two-column form grid | `lg:grid-cols-2` | at ≥1024px |
| Container padding | `1rem` | 16px (max-width `2xl` = 1400px, centered) |

Reference scale (Tailwind): `1`=4px · `2`=8px · `3`=12px · `4`=16px · `6`=24px · `8`=32px · `12`=48px.

---

## Radius

Base `--radius: 0.5rem` (8px). Tailwind derives three steps:

| Token | Formula | Value | Typical use |
|-------|---------|-------|-------------|
| `rounded-lg` | `var(--radius)` | 8px | Cards, panels, sheets |
| `rounded-md` | `calc(var(--radius) - 2px)` | 6px | Buttons, inputs, selects |
| `rounded-sm` | `calc(var(--radius) - 4px)` | 4px | Badges, small chips |
| `rounded-full` | — | 9999px | Avatars, icon buttons, debug FAB |

> ⚠️ The stale `--radius` value `0.375rem` in `CLAUDE.md`'s Styling Reference predates the
> reskin. The live value is **`0.5rem`**.

---

## Shadows

Deliberately soft and low — flat enterprise surfaces lean on the 1px `--border`, not
elevation. Defined as CSS variables (not Tailwind's default shadow scale).

| Variable | Light | Dark |
|----------|-------|------|
| `--shadow-xs` | `0 1px 2px rgba(24,20,16,0.04)` | `0 1px 2px rgba(0,0,0,0.3)` |
| `--shadow-sm` | `0 1px 2px rgba(24,20,16,0.05)` | `0 1px 2px rgba(0,0,0,0.35)` |
| `--shadow-md` | `0 4px 12px rgba(24,20,16,0.06)` | `0 4px 12px rgba(0,0,0,0.45)` |

---

## Breakpoints

Mobile-first. The `md` breakpoint is the desktop/sidebar pivot.

| Breakpoint | Min width | Significance |
|-----------|-----------|--------------|
| `sm` | 640px | Larger phones — titles scale up |
| `md` | 768px | **Sidebar appears** (fixed desktop nav vs. mobile Sheet drawer) |
| `lg` | 1024px | Two-column form grids |
| `2xl` (container) | 1400px | Max container width |

---

## Interaction & motion

| Concern | Value |
|---------|-------|
| Focus ring | `outline: 2px solid hsl(var(--ring)); outline-offset: 2px` on `:focus-visible` |
| Sidebar expand/collapse | `width/margin 300ms cubic-bezier(0.4, 0, 0.2, 1)` (`.sidebar-transition`) |
| Sidebar item | `all 200ms cubic-bezier(0.4, 0, 0.2, 1)` |
| Zebra row hover | `background/box-shadow 150ms ease` |
| Accordion | `0.2s ease-out` (Radix content-height keyframes) |
| Scrollbar | 6px, thumb `hsl(var(--muted-foreground) / 0.3)`, radius 3px |
| Scroll behavior | `scroll-behavior: smooth` on `html` |

---

## How tokens are consumed

1. **Never hardcode hex or raw Tailwind color scales** (`bg-blue-500`, `text-green-600`).
Use semantic classes bound to variables: `bg-primary`, `text-muted-foreground`,
`border-border`, `bg-card`.
2. **Status is always a `<Badge variant="…">`** (`success` | `warning` | `info` |
`secondary` | `destructive`) — never raw green/amber Tailwind (repo rule 5).
3. **Opacity modifiers work on any color** because channels are wrapper-free:
`bg-primary/10`, `text-foreground/70`.
4. **Dark theme is automatic** — every component that uses semantic tokens flips with the
`.dark` class; no per-component dark overrides needed.

---

## Contrast notes

Key text pairs against WCAG AA (4.5:1 body / 3:1 large):

| Pair | Ratio (approx) | Verdict |
|------|----------------|---------|
| `foreground` on `background` (light) | ~15:1 | ✅ AAA |
| `foreground` on `background` (dark) | ~13:1 | ✅ AAA |
| `muted-foreground` on `background` (light) | ~4.9:1 | ✅ AA (body) |
| `primary-foreground` (#FFF) on `primary` (light `#305FC5`) | ~5.3:1 | ✅ AA |
| `muted-foreground` on `card` (dark) | ~5.6:1 | ✅ AA |

> `primary` at `221 61% 48%` is intentionally dark enough that white text on it clears AA —
> keep that in mind if the primary lightness is ever adjusted.
Loading
Loading