diff --git a/CLAUDE.md b/CLAUDE.md index cd65916f..7df53070 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -186,9 +186,9 @@ The web UI uses server-side rendering with Askama templates: ### Styling - Tailwind CSS for utility-first styling -- Custom components defined in `static/css/input.css` +- Custom components defined in `static/css/components.css` - Compiled CSS output in `static/css/output.css` -- Configuration in `tailwind.config.js` +- Tailwind is configured in `static/css/input.css` (`@source`, `@custom-variant`, `@theme`); components live in `static/css/components.css` ### Key Templates - `base.html` - Common layout with navigation and search @@ -206,14 +206,18 @@ The web UI uses server-side rendering with Askama templates: - **Responsive Design**: Mobile-friendly layout with Tailwind ### CSS Component Classes -Custom Tailwind components for consistent styling: -- `.recipe-card` - Recipe cards with gradient top border -- `.ingredient-badge` - Orange gradient badges for ingredients -- `.cookware-badge` - Green gradient badges for cookware -- `.timer-badge` - Red gradient badges for timers -- `.metadata-pill` - Clean outline badges for metadata -- `.nav-pill` - Navigation items with hover effects -- `.step-number` - Circular step numbers with gradient +Component classes live in `static/css/components.css` and resolve every colour through the tokens in `static/css/input.css`: +- `.card` / `.card-head` - Bordered surface with hairline and 6px radius +- `.btn`, `.btn-primary`, `.btn-danger`, `.btn-sm` - 40px controls (32px compact); one accent-filled primary per view +- `.icon-btn` - 36px icon-only control +- `.nav-card`, `.nav-pill`, `.menu-item` - Navigation card, pills, and small-screen menu rows +- `.recipe-card`, `.recipe-card-icon`, `.recipe-card-title` - Index cards with a flat icon disc +- `.ingredient-badge`, `.cookware-badge`, `.timer-badge` - Inline recipe entities (tint, dotted underline, chip) +- `.metadata-pill`, `.tag` - Neutral bordered pills +- `.step-box`, `.step-number`, `.step-body`, `.step-refs` - Boxed recipe steps +- `.ingredient-row`, `.row`, `.row-value`, `.row-note` - List rows +- `.pantry-item`, `.pantry-actions`, `.item-status-dot` - Pantry blocks and stock state +- `.stepper`, `.select`, `.search-input` - Form controls ## Testing Approach @@ -260,10 +264,10 @@ Output formatting is centralized in `src/util/` modules. Each format has its own 4. Add route in the UI router ### Modifying Styles -1. Edit component classes in `static/css/input.css` +1. Edit component classes in `static/css/components.css` 2. Run `make css` or `npm run build-css` to compile 3. For development, use `npm run watch-css` for auto-rebuild -4. Custom colors and utilities can be added to `tailwind.config.js` +4. Custom colors and utilities can be added to `static/css/input.css` (`@theme`) and component classes to `static/css/components.css` ### Frontend Development Workflow 1. Install dependencies: `npm install` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fdfc4c79..e0fb0e18 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -142,13 +142,13 @@ Custom component classes: │ └── recipe.html # Single recipe view ├── static/ # Static assets │ └── css/ -│ ├── input.css # Tailwind input with custom classes -│ └── output.css # Compiled CSS (generated) +│ ├── input.css # Tailwind config (@source, @custom-variant, @theme) and tokens +│ ├── components.css # Component classes (.card, .btn, .nav-pill, ...) +│ └── output.css # Compiled CSS (generated) ├── src/server/ │ ├── mod.rs # Server setup and routing │ ├── ui.rs # UI request handlers │ └── templates.rs # Template data structures -├── tailwind.config.js # Tailwind configuration └── package.json # NPM dependencies ``` diff --git a/docs/superpowers/plans/2026-09-04-web-ui-tokens.md b/docs/superpowers/plans/2026-09-04-web-ui-tokens.md new file mode 100644 index 00000000..b6f7162f --- /dev/null +++ b/docs/superpowers/plans/2026-09-04-web-ui-tokens.md @@ -0,0 +1,4265 @@ +# Web UI Token Foundation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rebuild the web UI's styling on a Tailwind v4 CSS-first token layer with the Cooklang palette and flat surfaces, while keeping every page's layout, spacing and dimensions exactly as they are on `main`. + +**Architecture:** `static/css/input.css` owns the tokens, the `@theme` registration and the print/CodeMirror rules; `static/css/components.css` owns the component vocabulary (`.card`, `.btn`, `.nav-pill`, `.recipe-card`, `.step-box`, …). Each Askama template keeps its current markup and swaps palette utilities and gradients for token utilities and component classes. The `.dark .*` override block in `base.html` is deleted last, once no page depends on it. + +**Tech Stack:** Rust (Axum + Askama templates), Tailwind CSS 4.3 via `@tailwindcss/cli`, Playwright E2E tests, `cargo test`. + +**Spec:** `docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md`. Read it first. Section 1.4 is the component table every template task refers to. + +**Branch:** `design/web-ui-tokens`, cut from `main`. Final step force-pushes it over `design/web-ui-refresh` (PR #456). + +**Rules that apply to every task:** + +- Never commit `static/css/output.css` (gitignored). Rebuild it with `npm run build-css` after any CSS change and before running E2E tests. +- No Tailwind palette utility (`gray-*`, `orange-*`, `purple-*`, `blue-*`, `green-*`, `red-*`, `pink-*`, `yellow-*`, `indigo-*`, `lime-*`, `amber-*`, `cyan-*`, `emerald-*`, `white`, `black`) may appear in a file you touch, except `bg-black/50` for modal backdrops. No gradients. The only `dark:` usage allowed is the theme-toggle icon swap (`hidden dark:block` / `block dark:hidden`). +- Never stack two utilities that set the same property where one is conditional (e.g. `border-line` plus a conditional `border-danger`): Tailwind emits them in alphabetical order, so whichever sorts last wins regardless of intent. Branch the class instead: `{% if err %}border-danger{% else %}border-line{% endif %}`. +- Keep every structural utility (grid columns, `p-6`, `gap-6`, `mb-8`, `h-48`, `w-16`, `sticky top-6`, responsive variants) exactly as `main` has it. Only colour, border, radius, shadow and typography classes change. +- Commit messages use Conventional Commits (`feat(ui):`, `fix(ui):`, `refactor(ui):`, `test(ui):`, `docs:`) and end with the trailer line `Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu`. +- E2E: `npm test -- --project=chromium ` runs one spec file in Chromium. The Playwright web server builds CSS, JS and the binary automatically; if a server is already on port 9080 it is reused, so restart it after Rust changes. The `shopping-list*.spec.ts` files race on the shared fixture; run them with `--workers=1`. + +--- + +## File map + +| File | Responsibility | Action | +|---|---|---| +| `static/css/input.css` | Tailwind entry: custom variant, sources, tokens, `@theme`, type scale, print token reset, CodeMirror | Rewrite | +| `static/css/components.css` | Component vocabulary in `@layer components` | Create | +| `static/css/cooking-mode.css` | Cook mode overlay, on tokens | Replace with PR #456's version | +| `static/css/custom-styles.css`, `static/css/styles.css`, `tailwind.config.js` | Dead / superseded | Delete | +| `templates/base.html` | Nav card, search, footer, inline style block | Edit (Tasks 2 and 12) | +| `templates/recipes.html` | Index cards + sorter | Edit | +| `templates/recipe.html` | Recipe page + scale scripts | Edit | +| `templates/shopping_list.html` | Shopping list markup + JS-rendered strings | Edit | +| `templates/pantry.html` | Pantry blocks + status JS | Edit | +| `templates/menu.html` | Menu page | Edit | +| `templates/preferences.html` | Preferences cards and toggles | Edit | +| `templates/edit.html`, `templates/new.html` | Editor and new-recipe form | Edit | +| `templates/api_docs.html`, `templates/error.html` | API docs and error page | Edit | +| `src/web/templates.rs` | `method_classes()` badge classes | Edit | +| `static/js/keyboard-shortcuts.js` | Modal classes, `adjustScale` export | Edit | +| `static/js/search.js` | Result row classes | Edit | +| `static/js/cooking-mode.js` | Step capture selectors | Edit | +| `tests/e2e/navigation.spec.ts`, `preferences.spec.ts`, `recipe-display.spec.ts` | Selector updates | Edit | +| `tests/e2e/recipes-sort.spec.ts` | Sorter coverage | Create | +| `tests/menu_api_test.rs` | Class-agnostic badge regex | Edit | + +--- + +### Task 1: CSS foundation + +**Files:** +- Rewrite: `static/css/input.css` +- Create: `static/css/components.css` +- Replace: `static/css/cooking-mode.css` +- Delete: `static/css/custom-styles.css`, `static/css/styles.css`, `tailwind.config.js` +- Modify: `templates/base.html:13` (remove the `custom-styles.css` link) + +- [ ] **Step 1: Confirm the build is green before touching anything** + +Run: `npm run build-css && cargo build 2>&1 | tail -1` +Expected: `output.css` written, `Finished` line from cargo. + +- [ ] **Step 2: Write `static/css/input.css`** + +Replace the whole file with: + +```css +@import "tailwindcss" source(none); + +/* Tailwind v4 is CSS-first. The two things tailwind.config.js used to own — + class-based dark mode and the list of files to scan for class names — are + declared here instead. `source(none)` disables Tailwind's automatic + whole-repo auto-detection, so only the paths named below are scanned. */ +@custom-variant dark (&:where(.dark, .dark *)); + +@source "../../templates"; +@source "../../static/js/*.js"; +@source "../../static/js/src"; +@source not "../../static/js/editor.bundle.js"; +/* The one Rust file that emits class names: method_classes() for the API + docs method badge. */ +@source "../../src/web/templates.rs"; + +/* ============================================================ + DESIGN TOKENS + Twenty-one semantic colour names plus radii and shadows, defined once per + theme. Every colour in the UI resolves through one of these. Do not + introduce raw hex values or Tailwind palette utilities in templates. + --accent-strong is the fill for controls that carry text: the DS orange + (--accent) only reaches 3.7:1 under white text, so filled buttons use + this slightly darker step, where white (--accent-ink) clears AA at 4.7:1. + --accent itself stays on icons, borders, focus rings and status dots. + ============================================================ */ +:root { + color-scheme: light; + + --bg: #fcfcfb; + --surface: #ffffff; + --surface-sunk: #f5f3f0; + --border: #e4e0da; + --border-strong: #c3bcb1; + --text: #16161d; /* DS Text/Primary */ + --text-muted: #5f5a51; /* DS Text/Secondary, darkened for AA */ + --text-faint: #6a645b; + --accent: #e15a29; /* DS Controls/Primary, Icons/Primary */ + --accent-strong: #c94a1c; /* --accent darkened until white text clears AA */ + --accent-text: #715329; /* DS Text/Tags */ + --accent-soft: #f5dacf; /* DS Background/UI One */ + --accent-ink: #ffffff; + --ok: #3d6849; + --ok-soft: #e2e8df; + --danger: #c4261c; /* DS Text/Warning, darkened for AA */ + --danger-soft: #f7dfdc; + --danger-ink: #ffffff; + /* Links. The design system is entirely warm, so this is the dark end of + the DS orange rather than a blue; every link also carries an underline + so the affordance never rests on hue alone. */ + --info: #8a3d14; + --disabled: #d3cdcb; /* DS Controls/Disabled */ + --inactive: #8a8075; /* DS Controls/Inactive, darkened for AA */ + + --radius-control: 6px; + --radius-card: 6px; + + /* Two elevations. In-flow surfaces get --shadow-card, which is barely a + shadow at all — the border does the work. --shadow-overlay is only for + things floating above the page: dropdowns, dialogs, search results. */ + --shadow-card: 0 1px 0 rgba(27, 31, 36, .04); + --shadow-overlay: 0 8px 24px rgba(27, 31, 36, .12); +} + +/* .cooking-overlay is listed here deliberately: cook mode is always dark + regardless of the site theme, so its inline entity badges must resolve the + dark token values or they render at ~2.5:1 on the dark card. */ +.dark, +.cooking-overlay { + color-scheme: dark; + + --bg: #16161d; + --surface: #1c1c24; + --surface-sunk: #23232c; + --border: #30303b; + --border-strong: #43434f; + --text: #efeae6; + --text-muted: #ada69b; + --text-faint: #948d83; + --accent: #e15a29; + --accent-strong: #c94a1c; + --accent-text: #f08050; + --accent-soft: #3a2820; + --accent-ink: #ffffff; + --ok: #6fb283; + --ok-soft: #1e2a22; + --danger: #ff6b60; + --danger-soft: #2e1b1a; + --danger-ink: #16161d; + --info: #e59a6d; + --disabled: #4a4a55; + --inactive: #8f8880; + + --shadow-card: none; + --shadow-overlay: 0 8px 24px rgba(0, 0, 0, .5); +} + +/* Register tokens with Tailwind so they generate real utilities + (bg-surface, text-muted, border-line, …). `inline` makes the generated + utilities reference the custom property, so they flip automatically under + .dark with no override rules. Border tokens are named `line` so the + utility reads `border-line` rather than `border-border`. */ +@theme inline { + --color-bg: var(--bg); + --color-surface: var(--surface); + --color-sunk: var(--surface-sunk); + --color-line: var(--border); + --color-line-strong: var(--border-strong); + --color-text: var(--text); + --color-muted: var(--text-muted); + --color-faint: var(--text-faint); + --color-accent: var(--accent); + --color-accent-strong: var(--accent-strong); + --color-accent-text: var(--accent-text); + --color-accent-soft: var(--accent-soft); + --color-accent-ink: var(--accent-ink); + --color-ok: var(--ok); + --color-ok-soft: var(--ok-soft); + --color-danger: var(--danger); + --color-danger-soft: var(--danger-soft); + --color-danger-ink: var(--danger-ink); + --color-info: var(--info); + --color-disabled: var(--disabled); + --color-inactive: var(--inactive); +} + +/* ============================================================ + TYPE SCALE + Seven steps, each with a role and a line-height chosen for that role. + `display` is 30px — the size main's page titles have always been — the + rest are the PR #456 scale. Tailwind's xs/sm/base/lg/2xl/3xl names are + aliased onto the steps so a stray `text-sm` cannot drift off the scale. + ============================================================ */ +@theme { + --text-display: 30px; + --text-display--line-height: 1.2; + --text-display--letter-spacing: -.01em; + + --text-title: 18px; + --text-title--line-height: 1.35; + --text-title--letter-spacing: -.005em; + + --text-read: 16px; + --text-read--line-height: 1.6; + + --text-body: 14px; + --text-body--line-height: 1.5; + + --text-ui: 13px; + --text-ui--line-height: 1.4; + + --text-meta: 12px; + --text-meta--line-height: 1.4; + + --text-label: 11px; + --text-label--line-height: 1.3; + --text-label--letter-spacing: .06em; + + --text-xs: var(--text-meta); + --text-xs--line-height: var(--text-meta--line-height); + --text-sm: var(--text-body); + --text-sm--line-height: var(--text-body--line-height); + --text-base: var(--text-read); + --text-base--line-height: var(--text-read--line-height); + --text-lg: var(--text-title); + --text-lg--line-height: var(--text-title--line-height); + --text-2xl: var(--text-display); + --text-2xl--line-height: var(--text-display--line-height); + --text-3xl: var(--text-display); + --text-3xl--line-height: var(--text-display--line-height); +} + +body { + font-size: var(--text-body); + line-height: 1.5; +} + +/* Component vocabulary. Kept in its own file so this one stays the small, + stable theme contract. */ +@import "./components.css"; + +/* Print: force the light token values regardless of the active theme. + Layout flattening lives in base.html's print block. */ +@media print { + :root, + .dark, + .cooking-overlay { + color-scheme: light; + + --bg: #ffffff; + --surface: #ffffff; + --surface-sunk: #f5f4f2; + --border: #dddad5; + --border-strong: #bbb6ae; + --text: #111111; + --text-muted: #444444; + --text-faint: #666666; + --accent: #a8380c; + --accent-strong: #a8380c; + --accent-text: #a8380c; + --accent-soft: #f7ece5; + --accent-ink: #ffffff; + --ok: #23624a; + --ok-soft: #eaf3ee; + --danger: #96201a; + --danger-soft: #f8eae9; + --danger-ink: #ffffff; + --info: #1f4f86; + --disabled: #cccccc; + --inactive: #767676; + + --shadow-card: none; + --shadow-overlay: none; + } + + .card, + .btn, + .stepper, + .nav-card, + .recipe-card, + .step-box, + .ingredient-row, + .pantry-item { + border: 1px solid var(--border) !important; + box-shadow: none !important; + } + + .card, + .step-box { + break-inside: avoid; + } +} + +/* ============================================================ + CODEMIRROR + Token-driven in BOTH themes. These rules live OUTSIDE @layer components on + purpose: CodeMirror injects its base theme as an unlayered ` block in ``) + +Every page is now on tokens, so nothing depends on the `.dark .*` overrides. The print block is rewritten to target the new markup only. + +- [ ] **Step 1: Replace the entire `` in `` with: + +```html + +``` + +- [ ] **Step 2: Confirm the override block is gone and no palette utility survives anywhere** + +Run: +```bash +grep -c '\.dark ' templates/base.html +grep -rnE '(^|[ "'"'"'])(hover:|focus:|group-hover:)?(bg|text|border|from|to|via|ring|placeholder|decoration)-(gray|orange|purple|pink|blue|indigo|green|emerald|red|yellow|lime|amber|cyan|slate|white)\b' templates static/js/*.js static/css/input.css static/css/components.css static/css/cooking-mode.css src/web/templates.rs +grep -rn 'gradient' templates static/js/*.js static/css/input.css static/css/components.css static/css/cooking-mode.css +grep -rn 'dark:' templates static/js/*.js | grep -v 'dark:block\|dark:hidden' +``` +Expected: `0` from the first command, nothing from the other three except comment lines in `components.css` that mention the word "gradient" (those are fine). Fix any class or property occurrence that prints before continuing. + +- [ ] **Step 3: Build everything and run the full suites** + +```bash +cargo fmt && cargo clippy 2>&1 | tail -3 && cargo test 2>&1 | grep -E 'test result|FAILED' +npm run build-css && npm run build-js +npm test -- --project=chromium --workers=1 +``` +Expected: clippy clean, every cargo test result line `ok`, Playwright reports 0 failed. The accessibility spec asserts one `h1` per page and AA contrast; the shopping list, pantry and error pages now have an `h1`, which is what it wants. + +- [ ] **Step 4: Print check** + +In dark mode, open `/recipe/Neapolitan%20Pizza`, `Cmd+P`, preview: dark text on white, one column, no nav. Cancel. + +Dark-mode values to verify once the override block is gone: +- Body background `#16161d` (the old `.dark body { background-color: #111827 }` used to win) +- Idle `.nav-pill` colour `#ada69b` +- Hover fill `--surface-sunk` +- Active pill `--accent-soft` / `--accent-text` + +- [ ] **Step 5: Commit** + +```bash +git add templates/base.html +git commit -q -F - <<'EOF' +refactor(ui): delete the dark-mode override block + +Every page resolves its colours through tokens now, so the ~450 lines +of `.dark .*` utility overrides are dead. The print block is rewritten +against the current markup. + +Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu +EOF +``` + +--- + +### Task 13: Visual pass, PR description, force-push + +**Files:** none new. This task verifies, documents and ships. + +- [ ] **Step 1: Side-by-side against main** + +Start a second server on main for comparison: + +```bash +git worktree add /tmp/cookcli-main origin/main +cd /tmp/cookcli-main && npm install --silent && npm run build-css && npm run build-js && cargo build +``` + +then, in a second terminal, `/tmp/cookcli-main/target/debug/cook server /tmp/cookcli-main/seed --port 9081`. + +For each of `/`, `/recipe/Neapolitan%20Pizza`, `/shopping-list` (with a recipe added), `/pantry`, `/preferences`, `/edit/Neapolitan%20Pizza`, `/api-docs`, and a menu page, compare `:9080` against `:9081` at 1440, 1024 and 820px in light and dark. What must match: nav height, card positions and heights, grid columns, button heights, step box padding, pantry block size. What must differ: colours, gradients gone, hairline borders, 6px radii, one accent. + +When done, stop that server and run `git worktree remove /tmp/cookcli-main`. + +- [ ] **Step 2: Cook mode in light theme** + +On `:9080` in light theme open a recipe, press Cook. Entity badges on the step card are readable; the header pill for the active section is accent-filled. + +- [ ] **Step 3: Static build smoke test** + +```bash +cargo run -q -- build ./seed --output /tmp/cook-static 2>&1 | tail -2 +ls /tmp/cook-static/static/css/ +open /tmp/cook-static/index.html +``` +Expected: `output.css` and `cooking-mode.css` present, no `custom-styles.css`; the index renders on tokens from `file://`, and search works (main's script-tag loader was kept). + +- [ ] **Step 4: Write the PR description** + +Save to `/tmp/pr-body.md`: + +```markdown +Rebuilds the web UI's styling on a Tailwind v4 CSS-first token layer and adopts the Cooklang design-system palette with flat, hairline-bordered surfaces. Every page keeps the layout, spacing and dimensions it has on `main`. + +This replaces the earlier version of this PR, which bundled the same foundation with a density pass (48px app bar, 60px index rows, sticky ingredient rail, compact lists). The density work is dropped; the foundation, palette, type scale and bug fixes are kept. + +## Foundation + +- `input.css` is fully CSS-first: `@custom-variant dark` and `@source` replace `tailwind.config.js`, which is deleted. +- Twenty semantic tokens (`--bg`, `--surface`, `--text`, `--accent`, …) registered with `@theme inline`, so `bg-surface` / `text-muted` / `border-line` are real utilities that flip under `.dark` with no override rules. +- `components.css` holds the component vocabulary. Every colour resolves through a token; no raw hex outside `@media print`. +- Deleted: `custom-styles.css` (shadowed `output.css`), `styles.css` (unreferenced), and the ~450-line `.dark .*` override block in `base.html`. + +## Look + +Cooklang DS palette, no gradients, one accent, 6px radii, hairline borders, two-value elevation scale. Inline entities are weight + tint (ingredients) and a dotted underline (cookware), so the distinction no longer rests on a red/green hue pair. + +## Type + +Seven-step scale with per-step line-heights; Tailwind's size names are aliased onto it. Page titles stay at 30px. Step text keeps its 2.0 leading. + +## Fixes carried over + +- Index sorter reads `data-name`, collates numerically, persists in `sessionStorage`; now covered by `recipes-sort.spec.ts`. +- Scale changes preserve scroll position; −/+ stepper shares `adjustScale` with the keyboard shortcuts. +- Every page has exactly one `h1` and no skipped heading level. +- Cook mode legible in light theme; its step capture no longer scrapes layout utilities. +- Print path works from dark theme. +- CodeMirror dark caret/gutters (rules moved out of `@layer`). +- `menu_api_test.rs` no longer pins CSS classes; `recipe-display.spec.ts` lost its vacuous guard. + +## Verification + +- `cargo fmt`, `cargo clippy`, `cargo test` clean. +- Playwright: full suite green in Chromium. +- Each page compared against `main` at 1440/1024/820px, light and dark. + +Spec: `docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md`. + +https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu +``` + +- [ ] **Step 5: Force-push over the PR branch and update the description** + +The user approved replacing PR #456's branch. Its old commits stay reachable from the PR's history. + +```bash +git push --force-with-lease=design/web-ui-refresh origin design/web-ui-tokens:design/web-ui-refresh +gh pr edit 456 --title "feat(ui): token foundation and Cooklang palette, existing layout kept" --body-file /tmp/pr-body.md +gh pr view 456 --json url,title -q '.url + " " + .title' +``` + +If `--force-with-lease` is refused because the remote moved, fetch, look at what changed on `design/web-ui-refresh`, and ask the user before retrying. + +- [ ] **Step 6: Report** + +Tell the user: the PR URL, the number of commits on the branch, which E2E projects were run, and anything skipped. + +--- + +## Self-review against the spec + +| Spec section | Task | +|---|---| +| 1.1 `input.css` structure, `@custom-variant`, `@source`, no `@config` | 1 | +| 1.2 tokens (light, dark, print) | 1 | +| 1.3 type scale, 30px display, `text-3xl` alias, 2.0 step leading | 1 (`.step-body` in components) | +| 1.4 component retuning and additions | 1 | +| 1.5 stylesheet deletions, cook-mode replacement | 1 | +| 1.6 `base.html` style block | 2 (search rules), 12 (dark block, print) | +| 3.1 base | 2 | +| 3.2 recipes | 3 | +| 3.3 recipe | 5 | +| 3.4 shopping list | 6 | +| 3.5 pantry | 7 | +| 3.6 menu | 8 | +| 3.7 preferences | 9 | +| 3.8 edit, new | 10 | +| 3.9 api docs, error, `method_classes` | 11 | +| 3.10 scripts | 4 | +| 4 heading semantics | 3, 5, 6, 7, 11 | +| 5 behaviour fixes | 3 (sorter), 5 (scale), 1 (cook-mode tokens, print, CodeMirror, no transitions), 4 (cook-mode JS), 8 (menu test) | +| 6 tests | 3, 5, 8, 9 | +| 7 verification | 12, 13 | +| 8 sequencing | task order | diff --git a/docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md b/docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md new file mode 100644 index 00000000..32fd1840 --- /dev/null +++ b/docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md @@ -0,0 +1,278 @@ +# Web UI: Token Foundation With the Existing Layout + +**Date:** 2026-09-04 +**Status:** Approved, ready for planning +**Supersedes:** PR #456 (`design/web-ui-refresh`). This spec reuses that PR's CSS foundation and visual identity and discards its density work. At the end, this branch is force-pushed over `design/web-ui-refresh` so PR #456 keeps its number and discussion; its description is rewritten to match this spec. + +## Goal + +Rebuild the web UI's styling on Tailwind v4's CSS-first model with a semantic token layer, adopt the Cooklang design-system palette and the flat, hairline-bordered surface language from PR #456, and make the pages visually consistent with one another. Keep every page's structure, spacing, and dimensions as they are on `main` today. + +## Non-goals + +- No compaction. The 48px app bar, the 60px index rows, the sticky ingredient rail, the unboxed hairline step list, the single-line pantry rows, and the compact shopping list from PR #456 are not adopted. +- No backend, routing, template-data, or API changes. +- No change to cook mode's layout. Only its stylesheet moves onto tokens. +- No new pages, no new features beyond the behaviour fixes listed in section 5. + +## Starting point + +`main` is already on Tailwind v4 (`tailwindcss` and `@tailwindcss/cli` 4.3.3). Its `input.css` still drives Tailwind through `@config "../../tailwind.config.js"`, defines its components with `@apply` and raw hex gradients, and relies on a ~450-line block of `.dark .*` overrides in `templates/base.html` plus two extra stylesheets (`custom-styles.css`, which shadows `output.css`, and `styles.css`, which nothing references). + +Work happens on a new branch from `main` (`design/web-ui-tokens`). Nothing is cherry-picked from PR #456; its final CSS files are used as the reference and copied where the spec says so. + +## 1. Foundation + +### 1.1 `static/css/input.css` + +Structure, in order: + +1. `@import "tailwindcss";` +2. `@custom-variant dark (&:where(.dark, .dark *));` — replaces `darkMode: 'class'`. +3. `@source "../../templates";`, `@source "../../static/js";` and `@source "../../src/web/templates.rs";` — replaces the content globs. The one Rust file that emits class names is `src/web/templates.rs` (`method_classes()` for the API docs method badge), so it is scanned explicitly; the rest of `src/` is not. +4. Token declarations: `:root` (light), `.dark, .cooking-overlay` (dark), each with `color-scheme`. +5. `@theme inline` registering every colour token as a Tailwind colour. +6. `@theme` type scale. +7. `body { font-size: var(--text-body); line-height: 1.5; }` +8. `@import "./components.css";` +9. `@media print` token reset and layout flattening. +10. CodeMirror rules, outside any layer. + +`@config` is removed and `tailwind.config.js` is deleted. The gradient keyframes it defined are unused once gradients are gone. + +### 1.2 Tokens + +Twenty-one semantic colour tokens plus two radii, two shadows. Values are the PR's final values, verbatim. + +| Token | Light | Dark | +|---|---|---| +| `--bg` | `#fcfcfb` | `#16161d` | +| `--surface` | `#ffffff` | `#1c1c24` | +| `--surface-sunk` | `#f5f3f0` | `#23232c` | +| `--border` | `#e4e0da` | `#30303b` | +| `--border-strong` | `#c3bcb1` | `#43434f` | +| `--text` | `#16161d` | `#efeae6` | +| `--text-muted` | `#5f5a51` | `#ada69b` | +| `--text-faint` | `#6a645b` | `#948d83` | +| `--accent` | `#e15a29` | `#e15a29` | +| `--accent-strong` | `#c94a1c` | `#c94a1c` | +| `--accent-text` | `#715329` | `#f08050` | +| `--accent-soft` | `#f5dacf` | `#3a2820` | +| `--accent-ink` | `#ffffff` | `#ffffff` | +| `--ok` | `#3d6849` | `#6fb283` | +| `--ok-soft` | `#e2e8df` | `#1e2a22` | +| `--danger` | `#c4261c` | `#ff6b60` | +| `--danger-soft` | `#f7dfdc` | `#2e1b1a` | +| `--danger-ink` | `#ffffff` | `#16161d` | +| `--info` | `#8a3d14` | `#e59a6d` | +| `--disabled` | `#d3cdcb` | `#4a4a55` | +| `--inactive` | `#8a8075` | `#8f8880` | +| `--radius-control` | `6px` | `6px` | +| `--radius-card` | `6px` | `6px` | +| `--shadow-card` | `0 1px 0 rgba(27,31,36,.04)` | `none` | +| `--shadow-overlay` | `0 8px 24px rgba(27,31,36,.12)` | `0 8px 24px rgba(0,0,0,.5)` | + +The print block resets all of these to the PR's print values (white surfaces, dark text, no shadows) under `:root, .dark, .cooking-overlay`. + +Tailwind names, registered under `@theme inline`: `bg`, `surface`, `sunk`, `line`, `line-strong`, `text`, `muted`, `faint`, `accent`, `accent-strong`, `accent-text`, `accent-soft`, `accent-ink`, `ok`, `ok-soft`, `danger`, `danger-soft`, `danger-ink`, `info`, `disabled`, `inactive`. Border tokens are named `line` so the utility reads `border-line`. + +No raw hex appears outside the token declarations and `@media print`. No Tailwind palette utility (`gray-*`, `orange-*`, `purple-*`, …) appears in any template, script, or stylesheet. No `dark:` variant appears in any template; every colour flips through its token. + +### 1.3 Type scale + +The PR's seven steps, with the page-title step raised to main's size. + +| Step | Size | Line-height | Role | +|---|---|---|---| +| `display` | **30px** | 1.2 | page title (`h1`) | +| `title` | 18px | 1.35 | section headings | +| `read` | 16px | 1.6 | recipe step text, notes | +| `body` | 14px | 1.5 | list rows, card titles, default | +| `ui` | 13px | 1.4 | controls, metadata | +| `meta` | 12px | 1.4 | captions, counts, tags | +| `label` | 11px | 1.3 | uppercase section labels | + +`display` and `title` carry the PR's negative letter-spacing. Tailwind's `text-xs`, `text-sm`, `text-base`, `text-lg`, `text-2xl` are aliased onto `meta`, `body`, `read`, `title`, `display` as in the PR; `text-3xl` is also aliased onto `display`. `text-4xl` is not used anywhere after this change. Four weights only: 400, 500, 600, 700. + +Exception: recipe step text keeps main's `leading-8` (2.0 line-height) on 16px text. This is the one place the reading step's line-height is overridden, and it is a deliberate density choice. + +### 1.4 `static/css/components.css` + +Copied from the PR, then adjusted. Everything lives in `@layer components` and resolves through tokens. + +**Kept as in the PR:** `:focus-visible` ring, `.card`, `.card-head` (including `.plain` and `.count`), `.btn-danger` colours, `.select` colours, `.row` family (used by the shopping list sidebar and JS-rendered lists), `.metaline` (available but unused), `.section-label`, `.item-status-dot` states, `.ingredient-badge`, `.cookware-badge`, `.timer-badge`, `.tag`, `.metadata-pill`, `.recipe-note`, `.step-refs`, `.image-step`, `.recipe-image-placeholder`, `.search-input` colours, `.nav-pill` colours, `a.text-info` underline, the coarse-pointer 44px target block, and the "no transitions on token colours" rule. + +**Retuned to main's dimensions:** + +| Class | Change | +|---|---| +| `.btn`, `.btn-primary`, `.btn-danger` | `.btn-primary` fills with `--accent-strong` under white `--accent-ink` (4.7:1); hover/active darken. Height 40px, padding `0 16px`, `text-body`, svg 20px. Matches main's `px-4 py-2` buttons. | +| `.select` | height 40px to sit level with `.btn`. | +| `.stepper` | height 40px; buttons 32px wide; input 56px wide. | +| `.nav-pill` | main's `px-5 py-2`, `border-radius: 9999px`, `text-body`. Flat fill states: hover `--surface-sunk`; active `--accent-soft` background, `--accent-text` colour, weight 600. | +| `.icon-btn` | 36px square (main's `p-2` + 20px icon), radius `--radius-control`, svg 20px. | +| `.search-input` | height 44px, padding `0 16px 0 40px`, `text-body`; keeps PR's border and focus rule. | +| `.step-number` | 32px circle (main), `--accent-soft` fill, `--accent-text` glyph, `text-body` weight 700, no border. | +| `.recipe-card` | block card: `display:flex; flex-direction:column; overflow:hidden`, `--surface`, hairline, `--radius-card`, `--shadow-card`; hover `border-color: var(--border-strong)`. No `::before` gradient stripe, no scale transform. Content padding is `p-6` in the template as today. | +| `.recipe-card-icon` | 64px circle, `--surface-sunk` fill, hairline, emoji at 24px. | +| `.recipe-card-title` | `h2` styled as main's `text-lg font-bold`, i.e. `text-title` 700, `--text`. | +| `.recipe-card-sub` | `text-meta`, `--text-faint`. | + +**Removed:** `.appbar`, `.appbar-brand`, `.appbar-nav`, `--appbar-h`, `.recipe-layout`, `.recipe-rail`, `.step-list` / `.step-list > li` hairline rules, `.step-body` max-width, `.ingredient-list` padding, `.pantry-item` single-line rules, `.pantry-dates`, `.pantry-date` separators, `.pantry-actions` opacity rules. + +**Added:** + +| Class | Definition | +|---|---| +| `.nav-card` | main's nav container: `--surface`, hairline, `--radius-card`, `--shadow-card`, `margin-bottom: 2rem`. Not sticky. | +| `.btn-sm` | compact `.btn` variant for inline forms that sit inside a card row: height 32px, padding `0 12px`, `text-ui`, svg 16px. Used by the pantry item edit form's Cancel/Save. | +| `.step-list` | `list-style:none; margin:0; padding:0;` only. Step boxes are `li.step-box`. | +| `.step-box` | main's per-step box: `--surface-sunk` fill, hairline, `--radius-card`, `padding: 1rem`. | +| `.step-body` | `font-size: var(--text-read); line-height: 2;` | +| `.ingredient-list` | `list-style:none; margin:0; padding:0;` | +| `.ingredient-row` | main's tinted row: `display:flex; justify-content:space-between; align-items:center; padding: .5rem .75rem; border-radius: var(--radius-control); background: var(--surface-sunk);` Quantity uses `.row-value`. Note uses `.row-note`. | +| `.pantry-item` | main's block: `--surface-sunk` fill, `--radius-control`, `padding: 1rem`, `border: 1px solid transparent`; hover `border-color: var(--border)`. Base `.quantity-display` is `--text-muted`, `.item-quantity` weight 500, so the template carries no colour utility on either element and the state rules below can win. `.out-of-stock` variant: `--danger-soft` fill, `--danger` border; its `.quantity-display` (and `.out-of-stock-icon`) recolour to `--danger`, `.item-quantity` weight 600. `.low-stock` variant: `--accent-soft` fill, `--accent` border; same shape with `--accent-text`. The JS toggles only these state classes; no utility classes are added at runtime. | +| `.pantry-actions` | opacity 0, revealed on `.pantry-item:hover`, `:focus-within`; always visible on coarse pointers. (Same behaviour as main's `group-hover:opacity-100`.) Its `.icon-btn`s are sized down to 28px (16px svg) so the row buttons don't crowd the text column at narrower breakpoints; the coarse-pointer 44px override still wins on touch. | + +### 1.5 Other stylesheets + +- `static/css/custom-styles.css` and `static/css/styles.css` are deleted; the `` to `custom-styles.css` in `base.html` is removed. +- `static/css/cooking-mode.css` is replaced with the PR's tokenised version verbatim (66 added / 58 removed lines against main). Its layout is unchanged. +- `static/css/output.css` stays gitignored and is never committed. + +### 1.6 `templates/base.html` ` - +
-