From d408464e91193a167de6ea90d60090d3b5d90ebc Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Sun, 7 Jun 2026 14:00:42 +0200 Subject: [PATCH] docs(specs): design-token adoption across feature screens (#49) Co-Authored-By: Claude Opus 4.8 (1M context) --- ...2026-06-07-design-token-adoption-design.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-07-design-token-adoption-design.md diff --git a/docs/superpowers/specs/2026-06-07-design-token-adoption-design.md b/docs/superpowers/specs/2026-06-07-design-token-adoption-design.md new file mode 100644 index 0000000..9b1f213 --- /dev/null +++ b/docs/superpowers/specs/2026-06-07-design-token-adoption-design.md @@ -0,0 +1,128 @@ +# 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).