docs(specs): design-token adoption across feature screens (#49)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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).
|
||||
Reference in New Issue
Block a user