feat: design system v1.0.0 — reconcile tokens from cross-repo audit

Extracted real production usage from 8 KDC repos and consolidated the
consensus (kdcsurveyadd / kdcvault / kdc_void_planner) into the canonical
design.md + tokens.

Highlights:
- brand.ink #000000 -> #1A1530 (purple-tinted, matches real usage)
- brand.canvas #F5F4F0 -> #F7F4ED (warm)
- neutral ramp rebuilt around ink (purple-tinted, not cold slate)
- semantic palette retuned for warm canvas + soft tints
- elevation shadows retinted with brand-ink rgb
- new accent.gold (product) and accent.mint (marketing) tokens
- real themes/light, themes/dark, themes/high-contrast
- fleshed out every component + foundation README
- docs/consolidation-2026-05.md captures the full audit + drift inventory

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Roberts
2026-05-17 17:51:39 +00:00
parent 5c6d703774
commit 6593bdf689
28 changed files with 904 additions and 247 deletions
+28 -12
View File
@@ -1,25 +1,41 @@
# Badges
Specification for the **badges** primitive.
Small inline labels for status, counts, and metadata. Pill-shaped by default. Reference: `kdcvault/frontend/src/components/ui/badge.tsx`.
## Anatomy
TODO — drop `anatomy.svg` next to this file.
See `anatomy.svg`. Optional leading dot or icon · label · `rounded.full` shape · 22px default height.
## Variants
- default
- (list per-component variants)
## States
default · hover · focus-visible · active/pressed · disabled · loading
| Token | Use | Background | Text |
|-------|-----|------------|------|
| `badge.default` | Neutral tag | `brand.primarySoft` | `brand.primaryHover` |
| `badge.success` | Positive status | `semantic.successSoft` | `semantic.success` |
| `badge.warning` | Caution / attention | `semantic.warningSoft` | `semantic.warning` |
| `badge.danger` | Failure / blocked | `semantic.dangerSoft` | `semantic.danger` |
| `badge.outline` | Inline tag w/o fill | transparent | `text.muted` + `border.default` |
| `badge.solid` | High-emphasis count | `brand.primary` | `text.inverse` |
## Sizes
| Size | Height | Padding | Typography |
|------|--------|---------|------------|
| `sm` | 18px | `0 6px` | `caption` |
| `md` (default) | 22px | `2px 10px` | `caption` |
| `lg` | 26px | `4px 12px` | `bodyS` |
## Tokens used
See `design.md``components.badges.*`.
`components.badge.*`, `colors.semantic.*Soft`.
## Accessibility
- All interactive instances are keyboard-reachable.
- Focus state has ≥ 3:1 contrast against its background.
- Pair color with an icon or text to convey meaning.
- Decorative badges don't need ARIA, but status badges should set `role="status"` and include a text label (not colour alone).
- Counts greater than 99 should render as `99+`.
## Do / Don't
- ✅ Use the canonical token references.
- ❌ Don't hardcode colors, sizes, or radii.
- ✅ Pair colour with text or an icon to convey status.
- ❌ Don't stack more than two badges in a row — fold them into a single chip group instead.
- ❌ Don't use `badge.solid` for ambient counts; reserve it for active selections.
+44 -11
View File
@@ -1,25 +1,58 @@
# Buttons
Specification for the **buttons** primitive.
Specification for the **buttons** primitive, reconciled from production usage in kdcsurveyadd, kdcvault, and kdc_void_planner (CVA-driven shadcn variants).
## Anatomy
TODO — drop `anatomy.svg` next to this file.
See `anatomy.svg` — label · optional leading/trailing icon · 8px gap · 40px height (default size).
## Variants
- default
- (list per-component variants)
| Token | Use | Background | Text |
|-------|-----|------------|------|
| `button.primary` | Default CTA (one per surface) | `brand.primary` | `text.inverse` |
| `button.accent` | Emphasis / submit / commit action | `accent.gold` | `text.inverse` |
| `button.secondary` | Neutral default action | `surface.subtle` | `text.default` |
| `button.outline` | Lower-emphasis bordered | transparent | `text.default` + `border.strong` |
| `button.ghost` | Tertiary, inline within text | transparent | `brand.primary` |
| `button.destructive` | Irreversible negative action | `semantic.danger` | `text.inverse` |
| `button.link` | Visually a link, behaves as a button | transparent | `brand.primary` (underlined on hover) |
Reference implementation: `kdcvault/frontend/src/components/ui/button.tsx` (class-variance-authority).
## Sizes
| Size | Height | Padding | Typography |
|------|--------|---------|------------|
| `xs` | 28px | `4px 10px` | `caption` |
| `sm` | 32px | `6px 12px` | `bodyS` |
| `md` (default) | 40px | `10px 16px` | `bodyM` |
| `lg` | 48px | `12px 20px` | `bodyM` |
| `icon` | 40×40px | `0` | — |
## States
default · hover · focus-visible · active/pressed · disabled · loading
`default · hover · focus-visible · active/pressed · disabled · loading`
- **Hover** swaps `primary``primaryHover` (`#5C2D90`), darkens accent by ~12%.
- **Focus-visible** applies `elevation.focusRing` (`0 0 0 3px rgba(107,53,167,0.18)`).
- **Disabled** sets `opacity: 0.5` and `pointer-events: none`. Keep colour — don't desaturate.
- **Loading** swaps the label for a 14px spinner; preserve width to avoid layout shift.
## Tokens used
See `design.md``components.buttons.*`.
See `design.md``components.button.*` and `elevation.focusRing`.
## Accessibility
- All interactive instances are keyboard-reachable.
- Focus state has ≥ 3:1 contrast against its background.
- Pair color with an icon or text to convey meaning.
- All buttons are keyboard-reachable and have a visible focus ring.
- Icon-only buttons must carry `aria-label`.
- Focus and hover states meet ≥ 3:1 contrast against the button surface.
- Touch target ≥ 44×44px on mobile (`md` size satisfies this).
## Do / Don't
- ✅ Use the canonical token references.
- ❌ Don't hardcode colors, sizes, or radii.
- ✅ One `primary` *or* one `accent` per surface, never both side-by-side.
- ✅ Use `accent.gold` for the *forward-moving* action in a multi-step flow.
- ❌ Don't hardcode `#6B35A7` — use `colors.brand.primary`.
- ❌ Don't put two CTAs of equal emphasis next to each other.
+23 -11
View File
@@ -1,25 +1,37 @@
# Cards
Specification for the **cards** primitive.
A surface for grouping related content. Two canonical variants: `default` (flat, hairline border) and `elevated` (lifted shadow).
## Anatomy
TODO — drop `anatomy.svg` next to this file.
See `anatomy.svg`. Standard parts: `CardHeader` · `CardTitle` · `CardDescription` · `CardAction` (top-right slot) · `CardContent` · `CardFooter`. Reference: `kdcvault/frontend/src/components/ui/card.tsx`.
## Variants
- default
- (list per-component variants)
| Token | Background | Border | Shadow | Radius | Padding |
|-------|------------|--------|--------|--------|---------|
| `card.default` | `surface.background` | `border.default` (1px) | `elevation.1` | `rounded.lg` (12px) | 24px |
| `card.elevated` | `surface.background` | none | `elevation.3` | `rounded.xl` (16px) | 28px |
| `card.muted` | `surface.subtle` | `border.subtle` | none | `rounded.lg` | 24px |
| `card.outlined` | `surface.background` | `border.strong` (1px) | none | `rounded.lg` | 24px |
## States
default · hover · focus-visible · active/pressed · disabled · loading
- **Hover (interactive cards only)** — bump shadow one level (`1``2`) and shift `translateY(-2px)` over `motion.duration.fast`.
- **Focus-visible** — apply `elevation.focusRing` on the card root.
- **Selected** — 2px `brand.primary` left border or full `border.strong` ring.
## Tokens used
See `design.md``components.cards.*`.
`components.card.*`, `elevation.1``3`, `colors.surface.*`, `colors.border.*`.
## Accessibility
- All interactive instances are keyboard-reachable.
- Focus state has ≥ 3:1 contrast against its background.
- Pair color with an icon or text to convey meaning.
- If the card is clickable, the entire card is the hit target; expose it as a `<button>` or `<a>` (not a `<div onClick>`).
- Don't rely on hover state alone to indicate interactivity — pair with cursor + raised elevation.
## Do / Don't
- ✅ Use the canonical token references.
- ❌ Don't hardcode colors, sizes, or radii.
- ✅ Use `card.default` for list rows and grid tiles. Reserve `card.elevated` for modals, popovers, and feature highlights.
- ❌ Don't nest cards more than one level deep — collapse to sections instead.
- ❌ Don't mix `card.default` and `card.elevated` in the same grid.
+37 -13
View File
@@ -1,25 +1,49 @@
# Forms
Specification for the **forms** primitive.
Form composition: field stack, label / helper / error pairing, multi-step wizards. Reference: `kdcsurveyadd/components/surveys/SurveyWizard.tsx` (4-step wizard) and `kdcsurveyadd/components/surveys/StepIndicator.tsx`.
## Anatomy
TODO — drop `anatomy.svg` next to this file.
## Variants
- default
- (list per-component variants)
See `anatomy.svg`. Field row = label + optional helper hint + control + error slot. Sections separated by 32px; fields within a section by 16px.
## States
default · hover · focus-visible · active/pressed · disabled · loading
## Field structure
```
<label> field name (bodyM, 600, text.default)
<helper> optional hint (bodyS, 400, text.muted)
<input> (input.default)
<error> message when invalid (bodyS, 500, semantic.danger, role="alert")
```
## Layout
- **Single column** by default — multi-column only for ≥ 3 closely-related short fields (address, date range).
- **Field width** matches expected content: numeric → 120px, short text → 240px, free text → fill container.
- **Required indicator** — append `*` to label in `semantic.danger`; never use placeholder text or icon-only.
- **Optional fields** — append `(optional)` in `text.muted` `caption`.
## Wizard pattern (multi-step)
- Step indicator on top: `step n of N` + horizontal progress (`StepIndicator`).
- Context strip below — pills summarising decisions so far (jump-back affordance).
- Footer fixed: `Back` (ghost, left) · `Save & exit` (outline) · `Continue` / `Submit` (primary or accent, right).
- Validate per-step on `Continue`; never reveal step `n+1` errors prematurely.
## Tokens used
See `design.md``components.forms.*`.
`components.input.*`, `components.button.*`, `colors.semantic.danger`, `typography.bodyM`/`bodyS`/`caption`.
## Accessibility
- All interactive instances are keyboard-reachable.
- Focus state has ≥ 3:1 contrast against its background.
- Pair color with an icon or text to convey meaning.
- Use a `<form>` element with explicit `<button type="submit">`.
- Group related fields with `<fieldset>` + `<legend>`.
- Errors are announced via `role="alert"` and referenced by `aria-describedby`.
- On submit failure, focus moves to the first invalid field.
- Wizard step indicator uses `aria-current="step"` on the active step.
## Do / Don't
- ✅ Use the canonical token references.
- ❌ Don't hardcode colors, sizes, or radii.
- ✅ Validate on blur for individual fields; on submit for the whole form.
- ✅ Preserve user input across step changes — never silently discard.
- ❌ Don't show inline validation on first focus (only on blur or submit attempt).
- ❌ Don't disable the submit button to indicate invalid state — show the errors instead.
+35 -11
View File
@@ -1,25 +1,49 @@
# Inputs
Specification for the **inputs** primitive.
Text inputs, textareas, selects. Single visual treatment, semantic states. Reference: `kdcvault/frontend/src/components/ui/input.tsx` and `.kdc-input` / `.kdc-input-underline` utilities in `kdcsurveyadd/app/globals.css`.
## Anatomy
TODO — drop `anatomy.svg` next to this file.
See `anatomy.svg`. Label · input shell · helper / error text. Label sits above the input; helper text below in `text.muted` `bodyS`.
## Variants
- default
- (list per-component variants)
| Token | Use | Background | Border |
|-------|-----|------------|--------|
| `input.default` | Standard field | `surface.background` | `border.default` (1px) |
| `input.filled` | High-density forms | `surface.subtle` | none |
| `input.underline` | Marketing / minimal forms | transparent | bottom 1px `border.default` |
## Sizes
| Size | Height | Padding |
|------|--------|---------|
| `sm` | 32px | `6px 10px` |
| `md` (default) | 40px | `8px 12px` |
| `lg` | 48px | `12px 16px` |
## States
default · hover · focus-visible · active/pressed · disabled · loading
- **Hover** — border `border.default``border.strong`, `120ms` transition.
- **Focus-visible** — apply `elevation.focusRing`, border `brand.primary`.
- **Disabled** — `opacity: 0.5`, `cursor: not-allowed`, background `surface.subtle`.
- **Error** — border `semantic.danger`, helper text `semantic.danger`, focus ring `rgba(176,56,47,0.18)`.
- **Read-only** — background `surface.subtle`, remove border, keep text colour.
## Tokens used
See `design.md``components.inputs.*`.
`components.input.*`, `elevation.focusRing`, `colors.border.*`, `colors.semantic.danger`.
## Accessibility
- All interactive instances are keyboard-reachable.
- Focus state has ≥ 3:1 contrast against its background.
- Pair color with an icon or text to convey meaning.
- Every input has a programmatically associated `<label>` (visible or `sr-only` only when an adjacent icon supplies meaning).
- Error messages reference the input via `aria-describedby` and use `role="alert"` on first appearance.
- Placeholder text is decorative — never the only label.
- Maintain 4.5:1 contrast on placeholder text (currently `text.muted` `#6B647A`).
## Do / Don't
- ✅ Use the canonical token references.
- ❌ Don't hardcode colors, sizes, or radii.
- ✅ Show errors *under* the input, not as a tooltip.
- ✅ Use `tabular-nums` for numeric-only inputs (`.tnum` utility).
- ❌ Don't use placeholder text as a label.
- ❌ Don't shrink the height below 32px — it breaks touch targets.
+28 -13
View File
@@ -1,25 +1,40 @@
# Modals
Specification for the **modals** primitive.
Overlaid surfaces that interrupt the user — dialogs, confirmations, sheets, drawers. Built on `base-ui` / Radix primitives in the product family (`kdcvault/frontend/src/components/ui/dialog.tsx`, `sheet.tsx`).
## Anatomy
TODO — drop `anatomy.svg` next to this file.
See `anatomy.svg`. Backdrop · panel · header (title + close) · body · footer (actions, right-aligned).
## Variants
- default
- (list per-component variants)
## States
default · hover · focus-visible · active/pressed · disabled · loading
| Token | Use | Width | Position |
|-------|-----|-------|----------|
| `modal.default` | Confirmations, forms (≤ 5 fields) | 480px | centered |
| `modal.wide` | Multi-step wizards, content browsers | 720px | centered |
| `modal.sheet` | Slide-in side panel (editing, filters) | 420px | right edge |
| `modal.drawer` | Bottom sheet (mobile) | 100% | bottom edge |
| `modal.fullscreen` | Immersive flows | 100% | full viewport |
## Tokens used
See `design.md``components.modals.*`.
## Tokens & layout
- Radius `rounded.xl` (16px); sheet variants use `rounded.lg` on inner edge only.
- Background `surface.background`; backdrop `rgba(26,21,48,0.45)`.
- Shadow `elevation.4` (`0 24px 80px -24px rgba(26,21,48,0.22)`).
- Padding 24px; footer separated by `1px border.default` top edge.
- Open animation: `kdc-fade-up` (360ms cubic-bezier, `translateY(6px)` → 0).
## Accessibility
- All interactive instances are keyboard-reachable.
- Focus state has ≥ 3:1 contrast against its background.
- Pair color with an icon or text to convey meaning.
- `role="dialog"` (or `alertdialog` for destructive confirmations) with `aria-labelledby` / `aria-describedby`.
- Trap focus inside the modal; first focusable element receives focus on open.
- Close on `Esc`; restore focus to the trigger element on close.
- Backdrop click closes only non-destructive modals; confirmations require explicit action.
- Disable body scroll while open.
## Do / Don't
- ✅ Use the canonical token references.
- ❌ Don't hardcode colors, sizes, or radii.
- ✅ Right-align primary action in the footer (`primary` or `accent`); secondary / cancel on the left.
- ✅ Use a `sheet` for editing context that benefits from staying tethered to a list.
- ❌ Don't stack modals — open a `sheet` from inside a modal instead.
- ❌ Don't put more than one CTA in a single modal footer.
+37 -12
View File
@@ -1,25 +1,50 @@
# Navigation
Specification for the **navigation** primitive.
App shell, sidebar, top bar, tabs, breadcrumbs. Reference: `kdcvault/frontend/src/components/ui/sidebar.tsx`, `kdcsurveyadd/components/sidebar-nav.tsx`, `k-d-c-workspace/dashboard/components/AppShell.tsx`.
## Anatomy
TODO — drop `anatomy.svg` next to this file.
App shell = top bar (60px, `surface.background`, hairline bottom border) + sidebar (240px collapsed → 72px icon-only, dark `brand.ink` on workspace; canvas-toned in product apps) + main outlet.
## Variants
- default
- (list per-component variants)
## States
default · hover · focus-visible · active/pressed · disabled · loading
| Pattern | Use |
|---------|-----|
| **Top bar + sidebar** | Default product layout (vault, void planner, surveyadd) |
| **Top bar only** | Simple tools (mapper, tracker) |
| **Sidebar collapsible** | `SidebarProvider` controls expanded / icon-only / off-canvas (mobile) |
| **Command palette** | ⌘K trigger; `modal.default` shell over backdrop |
| **Tabs** | In-page section switching — underline (`border.bottom-2 brand.primary` active) |
| **Breadcrumbs** | Hierarchical path; `text.muted` non-active, `text.default` current, `` separator in `text.disabled` |
## Sidebar items
- 40px row height, 12px horizontal padding, 12px gap between icon and label.
- Icon 20×20px from the curated Phosphor set (see `assets/icons/`).
- Active state: `brand.primarySoft` background, `brand.primary` text, 2px left border in `brand.primary`.
- Hover: `surface.subtle` background.
- Section labels: `caption` uppercase, `text.muted`, 24px row.
## Top bar
- 60px tall, sticky.
- Left: brand mark · workspace switcher. Right: search · notifications · user menu.
- `backdrop-filter: saturate(180%) blur(14px)` on scroll (translucent over content).
## Tokens used
See `design.md``components.navigation.*`.
`components.button.*` (icon buttons), `colors.surface.*`, `colors.border.*`, `elevation.focusRing`.
## Accessibility
- All interactive instances are keyboard-reachable.
- Focus state has ≥ 3:1 contrast against its background.
- Pair color with an icon or text to convey meaning.
- Sidebar uses `<nav aria-label="primary">`; in-page tabs use `role="tablist"` / `role="tab"` / `role="tabpanel"`.
- Current page link: `aria-current="page"`.
- Skip-to-content link as the first focusable element, visible on focus.
- Command palette: trap focus, restore on close, announce via `aria-live`.
## Do / Don't
- ✅ Use the canonical token references.
- ❌ Don't hardcode colors, sizes, or radii.
- ✅ Always show the active route — left border + tinted background, not colour-shift alone.
- ✅ Collapse to icon-only below 1024px, off-canvas below 768px.
- ❌ Don't nest more than two sidebar levels — flatten into separate sections.
- ❌ Don't put primary actions in the sidebar — they belong in the top bar or page header.
+39 -13
View File
@@ -1,25 +1,51 @@
# Tables
Specification for the **tables** primitive.
Dense data display. Default to flat (no zebra striping) with hairline row borders. Reference: `kdctracker/admin.html` (`.card` data tables), `kdcdocs/public/styles.css` (`.kdc-table-head`, `.kdc-row`).
## Anatomy
TODO — drop `anatomy.svg` next to this file.
## Variants
- default
- (list per-component variants)
See `anatomy.svg`. Header row · grouped header rows (optional) · data rows · footer summary (optional).
## States
default · hover · focus-visible · active/pressed · disabled · loading
## Sizing
| Density | Row height | Cell padding |
|---------|------------|--------------|
| `compact` | 36px | `6px 12px` |
| `default` | 44px | `10px 16px` |
| `comfortable` | 56px | `14px 20px` |
Drive density with `[data-density="compact|default|comfortable"]` (per the kdcmapper convention).
## Styling
- Header: `text.muted` `caption` uppercase, 600 weight, `surface.subtle` background.
- Cell text: `bodyM`, `text.default`. Numerics use `tabular-nums` (`.tnum` utility).
- Row separator: 1px `border.subtle` bottom border. No vertical lines.
- Hover row: `surface.subtle`, `120ms` transition.
- Selected row: 2px `brand.primary` left border, `brand.primarySoft` background.
- Sticky header sits below any toolbar; sticky first column gets a 4px `elevation.2` shadow on right edge when scrolled.
## Cell types
- **Status pill** → `badge.*` variants.
- **Identifier / code** → `typography.mono` with subtle background `surface.subtle`.
- **Numeric** → right-aligned, `tabular-nums`.
- **Action cell** → trailing cell, ghost icon buttons; right-aligned, never wrap.
## Tokens used
See `design.md``components.tables.*`.
`colors.surface.*`, `colors.border.*`, `typography.bodyM`/`caption`/`mono`, `components.badge.*`.
## Accessibility
- All interactive instances are keyboard-reachable.
- Focus state has ≥ 3:1 contrast against its background.
- Pair color with an icon or text to convey meaning.
- Use proper `<thead>` / `<tbody>` / `<th scope="col">` / `<th scope="row">`.
- Sortable columns: button inside `<th>` with `aria-sort="ascending|descending|none"`.
- Empty state: `role="status"`, single line in `text.muted`.
- Long tables: pagination or `aria-rowcount` for virtualised lists.
## Do / Don't
- ✅ Use the canonical token references.
- ❌ Don't hardcode colors, sizes, or radii.
- ✅ Use `compact` density for ≥ 50 rows on screen at once.
- ✅ Right-align numerics and currencies; left-align text and identifiers.
- ❌ Don't zebra-stripe — it competes with hover and selection.
- ❌ Don't wrap action buttons; truncate text instead.
+27 -13
View File
@@ -1,25 +1,39 @@
# Tooltips
Specification for the **tooltips** primitive.
Brief, hover/focus-triggered hints. Reference: `kdcvault/frontend/src/components/ui/tooltip.tsx` (base-ui Tooltip).
## Anatomy
TODO — drop `anatomy.svg` next to this file.
## Variants
- default
- (list per-component variants)
See `anatomy.svg`. Trigger · floating panel · optional arrow. Max-width 240px; wraps to 3 lines maximum.
## States
default · hover · focus-visible · active/pressed · disabled · loading
## Styling
- Background `brand.ink` (`#1A1530`), text `text.inverse`, `caption` typography.
- Radius `rounded.sm` (4px), padding `6px 10px`.
- Shadow `elevation.2`.
- Arrow 6×6px, same fill as panel.
- Offset 8px from trigger; flip to opposite side at viewport edge.
## Timing
- Open delay 400ms (skip if another tooltip is already open within 500ms).
- Close delay 100ms (allow hover-into-tooltip for copy / link interactions).
- Open/close transition: `motion.duration.fast` `motion.easing.standard`.
## Tokens used
See `design.md``components.tooltips.*`.
`colors.brand.ink`, `colors.text.inverse`, `typography.caption`, `rounded.sm`, `elevation.2`.
## Accessibility
- All interactive instances are keyboard-reachable.
- Focus state has ≥ 3:1 contrast against its background.
- Pair color with an icon or text to convey meaning.
- Trigger is keyboard-focusable; tooltip opens on focus as well as hover.
- Use `aria-describedby` on the trigger pointing at the tooltip's `id`.
- Never put interactive content inside a tooltip — use a popover instead.
- Dismiss on `Esc` and on focus / hover leave.
## Do / Don't
- ✅ Use the canonical token references.
- ❌ Don't hardcode colors, sizes, or radii.
- ✅ Keep copy ≤ 80 chars; one sentence, no period.
- ✅ Use to surface a *non-essential* hint (full label, keyboard shortcut, brief explanation).
- ❌ Don't put a tooltip on a tappable mobile target — touch users never see it.
- ❌ Don't put interactive elements (links, buttons) inside a tooltip — escalate to a `popover`.