feat(ui): token foundation and Cooklang palette, existing layout kept - #456
Conversation
Review: feat(ui) design refresh with semantic tokensWent through the diff file by file (design tokens/ Strengths
Issues
Nits
Test coverageE2E additions look appropriately targeted ( No security concerns beyond item 2 above (which is a UX regression, not a vulnerability) — the escaping discipline in the rewritten JS is good. |
Review: feat(ui): design refresh — token system, Cooklang palette, Primer structureReviewed the full diff ( Strengths
FindingsModerate — theme-toggle transition race not fully eliminated. Moderate — scale stepper buttons behave inconsistently between Nit — planning artifacts committed to Nit — missing trailing newline. Not flagged, but worth confirming before mergePer No security issues found — XSS hygiene ( 🤖 Automated review generated with Claude Code |
Supersedes the PR #456 spec: same CSS foundation and palette, but the existing page layouts and density are kept. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
Tailwind moves fully CSS-first: @custom-variant dark and @source lines replace tailwind.config.js. custom-styles.css (which shadowed output.css) and the unreferenced styles.css are deleted. Component dimensions match what main rendered; only the colour system changes. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
- Disable Tailwind's whole-repo auto-detection (`source(none)`) so markdown utilities and the gitignored editor.bundle.js can't leak into output.css; scope @source to templates, static/js/*.js, static/js/src, and templates.rs, excluding editor.bundle.js explicitly. - Drop the grey hover border-color from .btn so .btn-primary/.btn-danger don't get a mismatched ring; add explicit :active states for both. - Give .stepper and .search-input a visible focus ring (border + soft accent glow) instead of relying on outline alone. - Remove the redundant border-radius from :focus-visible. - Correct the token count comment (seventeen -> twenty) in input.css and the design spec. - Force color-scheme: light in the print token block. - Alias the type-scale's xs/sm/base/lg/2xl/3xl line-heights onto their source steps instead of duplicating literals, and narrow the comment to name only the aliased steps. - Rename the coarse-pointer checkbox selector to .list-checkbox ahead of the shopping-list template migration. - Split .image-step's rounded corner into a token-driven border-radius. - Update CLAUDE.md/CONTRIBUTING.md references from tailwind.config.js to static/css/input.css and static/css/components.css. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
The sorter now uses Intl.Collator with numeric collation and persists the choice in sessionStorage. Card headings are h2 so the outline has no skipped level; the E2E suite selects .recipe-card-title. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
A fresh checkout gives every seed file the same mtime at second precision, which made "sorting by modified date reorders..." fail outside a dev environment with naturally-staggered edits. The test now reads data-modified before sorting and only asserts the order actually changed when at least two timestamps differ; it always asserts the post-sort order is descending and the item set is unchanged. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
- corrupt-state test now proves the sorter script kept running past the bad JSON, not just that the controls rendered - today's-menu "View" link no longer stretches full width on phones - directories only follow sortDir when sorting by name; date sorts keep them A→Z since they have no date of their own - an invalid saved field discards the saved direction too, so it can't leave a Z→A list behind - the sort-direction button keeps a title alongside its aria-label - the menu badge moved out of the h2 so the heading's accessible name is just the recipe name The plan's Task 3 Step 2 and Step 4 code blocks are updated to match byte-for-byte. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
The "corrupt saved state" test previously only checked that the controls rendered with default values, which stays green even if the try/catch around JSON.parse is removed (the click listeners are registered before restore() runs). It now records pageerror events and asserts none fired, alongside the existing click/reverse check that the sorter kept working. The plan's Task 3 Step 2 block is updated to match byte-for-byte. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
Cook mode captures steps from ol.step-list/.step-body instead of the layout utilities it used to scrape. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
Layout is unchanged from main. Scale changes keep the scroll position and the stepper shares adjustScale with the keyboard shortcuts. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
goToScale() built the recipe URL inside a <script> template literal from
{{ recipe_path }}, so Askama's HTML escaping was never decoded there the
way it is inside an onchange attribute on main — recipe names with & or
' navigated to a broken URL. The path and prefix now come from |json
constants instead. goToScale() also clamps/guards non-numeric input
(clearing the field and pressing Tab no longer navigates to ?scale=),
adjustScale() falls back to min instead of NaN, showRecipeError() sets
the message via textContent instead of interpolating into innerHTML,
and the error banner now anchors on #recipe-body instead of the first
.grid it finds.
Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
The page gains its missing h1; sidebar and aisle headings become h2. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
The quantity carried a text-muted utility that beat the component state rules, so out-of-stock quantities never turned red. Inline Save/Cancel use a compact .btn-sm so longer locales fit the stat block, and the row icons are 28px as main's were, keeping the text column wide enough. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
The menu API test stops pinning the scale badge's classes. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
…oggles The menu stepper now uses the same guarded goToScale as the recipe page (JSON-encoded path, clamped and encoded value, no navigation on an empty input). Preference toggles expose their state with aria-pressed instead of a test-only data attribute. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
…I chips border-line and border-danger were stacked on the same input, and Tailwind's alphabetical emission let border-line win, so the error state never showed. The classes now branch. The input gets the same 2px accent-soft focus ring as the search field, and the API docs contents chips use .btn-sm to match main's 34px height. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
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
Recipe action labels hide below lg again as on main, so the title row does not wrap on tablets. The menu tag sits inline with the card title. The shortcuts scrim uses bg-black/50 (bg-opacity-* no longer exists in Tailwind 4). Dead print selectors and hand-redeclared print: utilities are removed. CLAUDE.md lists the current component classes. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
c079c4a to
c5aa9c2
Compare
Review: Tailwind v4 token foundation + Cooklang paletteThis is a large but well-scoped refactor - the diff is dominated by mechanical class-name substitutions, and the actual logic changes are small, deliberate, and well commented. Overall this is high quality work. Notes below. Strengths
Issue found
Two consequences:
Since this is the exact page the PR calls out as fixed, it's worth a follow-up rather than filing as a new pre-existing item: either move Minor / non-blocking
Nice work overall - the token layer, dark-mode handling, and print styles all look correct, and the accompanying Playwright coverage gives good confidence the visual refactor didn't regress behavior. |
Near-black on the DS orange was hard to read. Filled controls now use --accent-strong (#c94a1c) under white --accent-ink, which clears AA at 4.7:1; hover and active darken so the label never drops below it. --accent itself stays on icons, borders, focus rings and status dots. Cook mode's active pill, step number and done button follow suit. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu
fix(web): close the UI follow-ups from #456
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.cssis fully CSS-first:@import "tailwindcss" source(none)with explicit@sourcelines and@custom-variant darkreplacetailwind.config.js, which is deleted. Only templates,static/js, and the one Rust file that emits class names are scanned, so nothing fromdocs/leaks into the built CSS.--bg,--surface,--text,--accent, …) registered with@theme inline, sobg-surface/text-muted/border-lineare real utilities that flip under.darkwith no override rules.components.cssholds the component vocabulary (.card,.btn,.btn-sm,.icon-btn,.nav-card,.nav-pill,.menu-item,.recipe-card,.step-box,.ingredient-row,.pantry-item,.stepper, …), every one sized to whatmainrendered. Every colour resolves through a token; no raw hex outside@media print.custom-styles.css(shadowedoutput.css),styles.css(unreferenced), and the ~450-line.dark .*override block inbase.html. The print block is rewritten against the current markup.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. Cook mode is on the same tokens.
Type
Seven-step scale with per-step line-heights; Tailwind's size names are aliased onto it. Page titles stay at 30px. Recipe step text keeps its 2.0 leading.
Behaviour fixes carried over or found in review
data-name, collates numerically, persists insessionStorage; covered by a newrecipes-sort.spec.ts.adjustScalewith the keyboard shortcuts. The scale URL is built from a JSON-encoded path, which fixes scaling for recipes whose name contains&or an apostrophe, and empty or out-of-range values no longer navigate to a 400 page. The menu page uses the same code.h1and no skipped heading level (shopping list and error page gained theirs).@layer).menu_api_test.rsno longer pins CSS classes;recipe-display.spec.tslost its vacuous guard; the pantry spec asserts the out-of-stock colour; preference toggles exposearia-pressed.Verification
cargo fmt,cargo clippy --all-targets,cargo testclean.mainat 1440/1024/820px in light and dark; print preview checked from dark theme; static build checked overfile://.Follow-ups (pre-existing on
main, not addressed here)src/web/builders.rs) while the client re-sorts with a natural collator, so the no-JS first paint of the static site can differ from the JS order.base.htmlinterpolates recipe names intoinnerHTMLunescaped;search.js(static mode) escapes. A recipe name containing<breaks the row.aria-labels ("Toggle theme", "Dismiss", "Increase scale", …) are hard-coded English rather than locale keys.clearList()on the shopping list addshiddento#shopping-list-resultsand nothing removes it, so the empty-state message is invisible until reload.static/js/src/editor.jsare raw hex and not theme-aware./edit/…fires an autosave immediately, showing "Saved" with no edits./api-docsskips fromh2toh4inside endpoint articles; sectioned recipes render section names ash3under the pageh1.static/tree, soinput.cssandcomponents.csssource ship alongsideoutput.css.Spec:
docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md. Plan:docs/superpowers/plans/2026-09-04-web-ui-tokens.md.https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu