# Design-Token Adoption Across Feature Screens — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax. **Goal:** Route every feature screen through the OKLCH design tokens — one indigo brand accent (`--primary`), token-based status colors (success/warning/highlight), the radius token, and a shared caption utility — and add a guard that keeps raw color utilities out of `src` (outside `components/ui/`). **Architecture:** Pure styling refactor. Phase 1 adds/changes tokens + `ui` Badge variants + the visibility badge / highlight / caption helpers. Phase 2 mechanically migrates ~120 raw utilities across 27 files to tokens + the radius token. Phase 3 adds the `check:colors` guard (which can only pass once the migration is complete) and runs the gate. No behavior, layout, routing, API, or data changes. **Tech Stack:** React 19 + TS + pnpm, Tailwind v4 (OKLCH tokens in `index.css`), Base UI, Vitest+RTL+MSW (incl. Storybook browser project). **Conventions:** pnpm; **no `any`/`eslint-disable`/`@ts-ignore`**; no codename; en/sv untouched (no strings); `check:size` budget 250 KB gz (no real change expected). Stories single-quote/no-semicolon; source double-quote/semicolon. **Do not change markup/layout/spacing** — only color/radius utilities + Badge variant selection. **Spec:** `docs/superpowers/specs/2026-06-07-design-token-adoption-design.md` **Migration surface (27 files with raw color utilities, outside `components/ui/`):** `app.tsx`, `auth/login-page.tsx`, `authorities/authorities-page.tsx`, `components/delete-confirm-dialog.tsx`, `fields/field-form.tsx`, `fields/field-list.tsx`, `objects/{delete-object-dialog,flexible-field-value,object-detail-drawer,object-detail,object-edit-form,object-form,objects-page,objects-table,options-combobox,publish-control,visibility-badge,visibility-badge.stories}.tsx`, `search/{highlight,search-panel,search-result-row,select-search-prompt}.tsx`, `shell/{lang-switch,sidebar}.tsx`, `vocab/{select-vocabulary-prompt,vocabulary-list,vocabulary-terms}.tsx`. --- # Task 1: Token + component foundation **Files:** `web/src/index.css`, `web/src/components/ui/badge.tsx` (+ `badge.stories.tsx` if present), `web/src/objects/visibility-badge.tsx`, `web/src/objects/visibility-badge.stories.tsx`, `web/src/search/highlight.tsx`. - [ ] **Step 1: Indigo primary + status tokens** in `web/src/index.css`. In `:root`: ```css --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); --success: oklch(0.627 0.194 149.214); /* green-600 — readable as text */ --success-foreground: oklch(0.985 0 0); --warning: oklch(0.666 0.179 58.318); /* amber-700-ish — readable as text */ --warning-foreground: oklch(0.985 0 0); --highlight: oklch(0.905 0.182 98.111); /* ~yellow-300 search highlight */ --highlight-foreground: oklch(0.205 0 0); ``` In `.dark` (keep coherent for #59): `--primary: oklch(0.673 0.182 276.935)` (indigo-400), `--primary-foreground: oklch(0.205 0 0)`, `--ring` to match; `--success`/`--warning` slightly lighter for dark; `--highlight` unchanged or darker-text. In `@theme inline` add the `--color-*` mappings: `--color-success: var(--success); --color-success-foreground: var(--success-foreground); --color-warning: var(--warning); --color-warning-foreground: var(--warning-foreground); --color-highlight: var(--highlight); --color-highlight-foreground: var(--highlight-foreground);`. Add a shared caption utility in `@layer components`: ```css @layer components { .label-caption { @apply text-xs font-medium uppercase tracking-wide text-muted-foreground; } } ``` (Implementer may fine-tune the oklch to match exact Tailwind shades; keep `*-foreground` contrast ≥ AA.) - [ ] **Step 2: Badge variants.** In `web/src/components/ui/badge.tsx`, add to the `cva` variants (mirror the `destructive` shape): ```ts success: "bg-success/10 text-success [a]:hover:bg-success/20", warning: "bg-warning/10 text-warning [a]:hover:bg-warning/20", ``` - [ ] **Step 3: VisibilityBadge → variants.** In `web/src/objects/visibility-badge.tsx`, replace the hardcoded `STYLES` (amber/green/neutral) with variant selection: ```tsx const VARIANT: Record = { draft: "secondary", internal: "warning", public: "success", }; export function VisibilityBadge({ visibility }: { visibility: Visibility }) { const { t } = useTranslation(); return {t(`visibility.${visibility}`)}; } ``` (Drop the `variant="outline" className={STYLES[...]}` patching.) - [ ] **Step 4: Highlight token.** In `web/src/search/highlight.tsx`, `bg-yellow-200` → `bg-highlight text-highlight-foreground`. - [ ] **Step 5: Update stories.** Add `Success`/`Warning` stories to the Badge story file (if `badge.stories.tsx` exists; else create alongside). **Update the `CssCheck` story** in `visibility-badge.stories.tsx`: it asserts the public badge background `oklch(0.962 0.044 156.743)` (old green-100). Public is now the `success` variant (`bg-success/10`). **Run the story, read the new `getComputedStyle(...).backgroundColor`, and pin that value** (keep the CssCheck — it proves Tailwind + tokens load). Update the comment. - [ ] **Step 6:** `cd web && pnpm test -- visibility-badge badge && pnpm typecheck && pnpm lint`. The visibility badge renders with token colors; CssCheck passes with the new value. **Commit** `feat(web): indigo brand token + status tokens + Badge success/warning variants (#49)`. --- # Task 2: Migrate feature screens to tokens + radius **Files:** the 27 migration-surface files listed above (excluding `visibility-badge.tsx`/`.stories.tsx` + `highlight.tsx` done in Task 1). Apply the migration map mechanically. **Use the guard regex (Task 3) as your completeness checker**: after migrating, `grep -rE "(text|bg|border|ring)-(neutral|gray|slate|red|amber|green|yellow|indigo|…)-[0-9]+" src --include="*.tsx" | grep -v "components/ui/"` must return **nothing**. | 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, sidebar) | `bg-accent` | | `bg-indigo-50` (selected row) | `bg-primary/10` | | `bg-indigo-600` / `text-indigo-600` | `bg-primary` / `text-primary` (+ `text-primary-foreground` where on `bg-primary`) | | `bg-neutral-800` (publish stepper / authority tabs active) | `bg-primary text-primary-foreground` | | `border-red-300` (combobox/drawer error) | `border-destructive` (or keep neutral `border` if it's not an error state) | | `border-green-300` | `border-success` (or neutral) | | bare `rounded` (×23) | `rounded-md` | - [ ] **Step 1: Migrate by area**, file-by-file, replacing per the map. Also collapse the uppercase-caption recipes (object-detail, object-form, publish-control, field-list, vocabulary-terms) to the shared `label-caption` class (`
…`). **Do not change any non-color/radius classes, markup, or layout.** For the few ambiguous one-offs, follow the map's intent (muted captions → `text-muted-foreground`; emphasized values → `text-foreground`; error text → `text-destructive`). Optionally adopt `ui/Card` for an obviously hand-rolled bordered panel (e.g. object-detail) — only if a clean swap; skip otherwise. - [ ] **Step 2: Completeness check** — run the grep above; iterate until **zero** raw color utilities remain outside `components/ui/`. Also confirm no bare `rounded` remains (→ `rounded-md`). - [ ] **Step 3: Verify no regressions** — `cd web && pnpm typecheck && pnpm lint && pnpm test` (all existing tests pass; the styling change shouldn't break behavioral tests — if a test asserts a specific old color/class, update it to the token equivalent). `pnpm build`. - [ ] **Step 4: Commit** `refactor(web): migrate feature screens to design tokens + radius token (#49)`. --- # Task 3: Enforcement guard + final verification **Files:** `web/scripts/check-no-raw-colors.mjs` (new), `web/package.json` (a `check:colors` script), wire into the gate. - [ ] **Step 1: Guard script** `web/scripts/check-no-raw-colors.mjs` (mirror `check-bundle-size.mjs` style): recursively scan `web/src/**/*.{ts,tsx}` **excluding `src/components/ui/`**; fail (exit 1, printing each `file:line`) on any match of: ``` /(?:text|bg|border|ring|fill|stroke|from|to|via|decoration|outline|divide|placeholder)-(?:neutral|gray|slate|zinc|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950)\b/ ``` Skip comments if practical; the goal is to catch real className usages. (It must NOT flag token utilities like `text-foreground`/`bg-primary` or numerics like `gap-2`.) - [ ] **Step 2: Wire it in** — add `"check:colors": "node scripts/check-no-raw-colors.mjs"` to `web/package.json`; include it in the project's check/CI flow (e.g. the `.gitea/workflows` web job, or alongside `check:size`). Run it → it must **pass** now (Task 2 cleared the surface). - [ ] **Step 3: Final verification:** ``` cd web && pnpm typecheck && pnpm lint && pnpm test && pnpm build && pnpm check:size && pnpm check:colors ``` All green. `pnpm test -- i18n` (parity unaffected). `git grep -in 'biggus\|dickus' -- web/src || echo CLEAN`. `git status --short` clean. - [ ] **Step 4: Manual smoke (recommended):** run the app — buttons/links/selected rows/active nav share the indigo accent; visibility badges (success/warning/neutral) + search highlight use the status tokens; nothing renders an unstyled/transparent element from a removed color. - [ ] **Step 5: Commit** `chore(web): add check:colors guard banning raw color utilities outside ui/ (#49)`. --- ## Self-Review (completed) **Spec coverage:** indigo `--primary`/`--ring` + status tokens + `@theme` + `.dark` (T1 S1); Badge success/warning + VisibilityBadge + highlight + label-caption (T1 S2–S4); ~120-utility migration + radius (T2); guard added last + gate (T3); CssCheck updated (T1 S5); dark-mode toggle out (#59), no behavior/layout change. ✓ **Placeholder scan:** concrete token values, badge variants, VisibilityBadge code, guard regex, and the explicit migration map + 27-file list. The CssCheck new value is "run to read" (the original story did the same — a genuine measurement step, not a placeholder). The few "ambiguous one-off" mappings are governed by the map's stated intent. **Type/consistency:** `success`/`warning` Badge variants (T1) consumed by `VisibilityBadge` `VARIANT` map; `--color-success/warning/highlight` tokens (T1) back `bg-success`/`bg-warning`/`bg-highlight`; the guard regex (T3) matches exactly the palette utilities the migration (T2) removes. ## Notes - No new dependency; CSS token churn only → `check:size` ≈ unchanged. - The guard is the durable win — it makes the consistency self-enforcing (closes the loop that caused #49). - If a behavioral test asserts an old raw class/color, update it to the token equivalent (don't weaken it).