Files
biggus-dickus/docs/superpowers/specs/2026-06-07-design-token-adoption-design.md
T
2026-06-07 14:00:42 +02:00

8.4 KiB
Raw Blame History

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 — publicsuccess, internalwarning, draft → the default/secondary — instead of patching bg-amber-100/bg-green-100.
  • search/highlight.tsx: bg-yellow-200bg-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).