# Design-Token Adoption Across Feature Screens — Design **Date:** 2026-06-07 **Status:** Approved (brainstorming) — ready for implementation planning. **Issue:** #49. ## Context `web/src/index.css` defines a full semantic OKLCH token set (the shadcn defaults: `--foreground`, `--muted-foreground`, `--primary`, `--destructive`, `--accent`, `--border`, `--radius`, light + `.dark`) and `ui/*` components use it. But the **feature screens bypass it**: ~120 hardcoded Tailwind color utilities outside `components/ui/` — `text-red-600` ×27 (not `--destructive`, a *different* red), `text-neutral-400/500/600` ×47 (three shades for "muted text"), `bg-neutral-50/100`, plus **two competing accents** (`bg-indigo-50`/`text-indigo-600`/ `bg-indigo-600` for selection/links/chips vs the near-black `--primary` for buttons and `bg-neutral-800` for the publish stepper). Status colors (visibility badge `amber`/`green`, search highlight `yellow`) aren't tokens; `rounded` (0.25rem, ×23) ignores the `--radius` token; `ui/Card` has zero usages; the uppercase caption label appears with 4 different recipes. (Recent table/detail/toast work *did* introduce token usage — `bg-primary` ×6, `bg-destructive`, `text-foreground` — so the codebase is now mixed; this finishes the job.) ### Decisions (from brainstorming) 1. **One brand accent: indigo `--primary`.** Set `--primary`/`--ring` to an indigo so primary buttons, selected rows, links, and chips share one recognizable accent (the existing indigo usages map straight onto `bg-primary`/`text-primary`). 2. **Add status tokens** (`--success`/`--warning`/`--highlight`) and route the visibility badge + search highlight through them (via Badge variants). 3. **Enforce** with a CI/lint guard banning raw color utilities outside `components/ui/`. 4. **Dark-mode toggle stays #59.** This migration makes the `.dark` token set *work* (semantic tokens adapt), unblocking #59; the toggle/persistence is not in scope. 5. **One milestone** (the guard can only pass once the migration is complete, so it lands last). ## Token changes (`web/src/index.css`) - **Indigo primary** in `:root`: `--primary: oklch(0.511 0.262 276.966)` (≈ indigo-600), `--primary-foreground: oklch(0.985 0 0)`, `--ring: oklch(0.511 0.262 276.966)`. In `.dark`: a lighter indigo that reads on dark (≈ indigo-400, `oklch(0.673 0.182 276.966)`) + `--primary-foreground: oklch(0.205 0 0)`, matching `--ring`. (Implementer may fine-tune to a Tailwind indigo shade; keep `--primary-foreground` contrast AA.) - **Status tokens** in `:root` and `.dark`, exposed in `@theme inline` as `--color-success`, `--color-success-foreground`, `--color-warning`, `--color-warning-foreground`, `--color-highlight`, `--color-highlight-foreground`. Light values ≈ success `oklch(0.6 0.13 160)` / warning `oklch(0.72 0.15 75)` / highlight `oklch(0.905 0.16 100)` with readable foregrounds; dark variants adjusted for contrast. `--destructive` unchanged. - `--accent`/`--muted` stay the neutral shadcn grays (hover/subtle surfaces). Selected/active states use `bg-primary/10` (a light indigo tint) rather than repurposing `--accent`. ## Component updates - **`ui/badge.tsx`:** add `success` and `warning` variants (token-based, with dark variants); **`VisibilityBadge`** (`web/src/objects/visibility-badge.tsx`) selects a variant — `public` → `success`, `internal` → `warning`, `draft` → the default/secondary — instead of patching `bg-amber-100`/`bg-green-100`. - **`search/highlight.tsx`:** `bg-yellow-200` → `bg-highlight` (token). - **Shared caption:** add a single `.label-caption` utility (in `index.css` `@layer components`, or a tiny `ui` helper) = `text-xs font-medium uppercase tracking-wide text-muted-foreground`; replace the 4 ad-hoc recipes (object-detail, object-form, publish-control, field-list, vocabulary-terms). - **`ui/Card`:** adopt for clearly hand-rolled bordered panels where it's a clean swap (e.g. the object-detail container). Not forced onto every bordered div — low priority within scope. ## Migration map (mechanical, ~120 sites, outside `components/ui/`) | From | To | |---|---| | `text-red-600` | `text-destructive` | | `text-neutral-400` / `-500` / `-600` | `text-muted-foreground` | | `text-neutral-700` / `-900` | `text-foreground` | | `bg-neutral-50` / `-100` | `bg-muted` | | `bg-neutral-200` (active nav) | `bg-accent` | | `bg-indigo-50` (selected row) | `bg-primary/10` | | `bg-indigo-600` / `text-indigo-600` | `bg-primary` / `text-primary` | | `bg-neutral-800` (publish stepper / tabs) | `bg-primary` (with `text-primary-foreground`) | | bare `rounded` (×23) | `rounded-md` | | `bg-amber-*`/`bg-green-*`/`bg-yellow-*` (badge/highlight) | via Badge variants / `--highlight` | Apply judgment on a few one-offs (e.g. `border-red-300`/`border-green-300` on the combobox/drawer → `border-destructive`/`border-success` or keep neutral `border`). The goal: **zero raw color utilities outside `components/ui/`** after the migration. ## Enforcement (`web/scripts/check-no-raw-colors.mjs`) A grep-style Node script (mirroring `check-bundle-size.mjs`) that scans `src/**/*.{ts,tsx}`, **excluding `src/components/ui/`**, and fails (exit 1) if it finds a raw palette utility matching `(text|bg|border|ring|fill|stroke|from|to|via)-(neutral|gray|slate|zinc|stone|red|orange|amber| yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-[0-9]{2,3}`. Print each offending file:line. Add a `check:colors` script and wire it into the test gate (e.g. the `check` flow / CI). A short inline allowlist (path or `eslint-disable`-style comment) only if a genuine exception arises. **Added last**, after the migration, so the gate is green. ## Data flow / architecture Pure styling refactor — no behavior, routing, API, or data changes. Tokens in `index.css` → Tailwind utility classes (`bg-primary`, `text-muted-foreground`, …) in feature components → resolved at build. `ui/*` components already consume tokens and gain the new Badge variants. ## Error handling / edges - The migration must not change layout/spacing — only color/radius utilities (and Badge variant selection). Don't restructure markup. - `bg-primary/10` opacity modifier on a token works in Tailwind v4 — verify it resolves. - A few utilities are genuinely semantic-ambiguous (e.g. `text-neutral-900` for an emphasized value vs body) — map to `text-foreground`; muted captions → `text-muted-foreground`. - The enforcement regex must not flag token utilities (`text-foreground`, `bg-primary`) or non-color numerics (`gap-2`, `w-44`) — scope it to the palette names above. ## Testing - **Existing `CssCheck` story** (`visibility-badge.stories.tsx`) asserts the public badge's resolved `bg-green-100` oklch — it **must be updated** to the new `--success` token's resolved value (run the story to read the actual computed color, as the original did). Add Badge `success`/`warning` stories. - `pnpm typecheck` / `lint` / `test` / `build` green; the **new `check:colors` passes** (proves the migration is complete — zero raw utilities outside `ui/`); `check:size` ≈ unchanged (CSS token churn only); no codename; en/sv untouched (no strings). - Manual smoke: the app still renders (now indigo-accented); selected rows/links/buttons share the indigo; visibility badges + search highlight use the status tokens; nothing relies on a removed color. (No automated visual-regression in the repo — the guard + updated stories + smoke are the safety net.) ## Acceptance criteria 1. `--primary`/`--ring` are indigo; primary buttons, selected rows, links, chips use it (one accent). Status tokens (`--success`/`--warning`/`--highlight`) exist (+ `@theme` + `.dark`). 2. `VisibilityBadge` and the search highlight use token-based Badge variants / `--highlight` (no hardcoded amber/green/yellow); one shared caption utility. 3. **No raw color utilities outside `components/ui/`**; bare `rounded` standardized to the radius token; `check:colors` guard added to the gate and passing. 4. No behavior/layout change; `CssCheck` story updated; `typecheck`/`lint`/`test`/`build`/ `check:size` green; no codename. 5. Dark-mode tokens are coherent (light + `.dark`) so #59 (toggle) is unblocked — but the toggle is not added here. ## Out of scope → follow-ups - The **dark-mode toggle** + persistence (#59) — this only makes the tokens dark-ready. - Per-screen **layout/spacing** redesign, density changes, typography scale (#57). - Forcing `ui/Card` onto every panel (adopt only the clean swaps).