feat(ui): design refresh — token system, Cooklang palette, Primer structure - #456
feat(ui): design refresh — token system, Cooklang palette, Primer structure#456dubadub wants to merge 42 commits into
Conversation
text-orange-600 is 3.60:1 on white, below AA. The old .recipe-card gradient bar caused axe to report this as incomplete rather than a violation; removing it in Task 4 unmasked the pre-existing failure.
Badges lost their own background in Task 4, so they resolved the site theme's tokens. Cook mode's card is always dark navy, so in light mode ingredient/cookware text fell to ~2.5:1. Adding .cooking-overlay to the dark token block restores 6.6:1 and 6.4:1.
- brand link had no accessible name below sm (span is display:none there) - search-selected was ~1.1:1 in dark mode; accent bar now carries the signal - guard app bar overflow across the 768-1280 band (zero slack at 768) - record search-result markup migration as a Task 13 prerequisite
Dropped during the Task 6 rewrite. No test covers this span, so nothing failed, but long preparation notes could overflow .step-refs without it.
The 20px checkbox failed the 24x24 minimum the finding itself cited, and .stepper's border-box meant its buttons resolved to 42px, not 44.
Cards drop from ~190px hero tiles to ~59px rows. The client-side sorter is removed at the user's request: it reparented every grid child, which constrained the markup (title had to stay an h3) and blocked the grouped layout the design calls for. Locale keys sort-by/sort-name/sort-modified/sort-created/ sort-direction-toggle are now unused but left in place, in case sorting returns server-side.
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. |
Repoints the token contract at the Cooklang design system colours and rebuilds the component layer around soft dual-shadow elevation. Palette - Text/Primary #16161D, Text/Tags #715329, Controls+Icons/Stroke #E15A29 and Background/UI One #F5DACF are used exactly as specified. - Text/Secondary #817B6D (3.7:1) and Text/Warning #FF3B30 (3.0:1) are darkened; both fail AA as body text on a light ground, and both are used as body text today. Hue is preserved. - The DS supplies Light only. Dark is derived in the same warm family. Elevation - --neu-light / --neu-dark drive four composites (raised, raised-sm, inset, inset-sm). Cards, buttons and nav rise; fields, chips and the active nav item sink. Pressable surfaces invert to inset on :active. - Print flattens every composite to none — soft shadows print as smudge. Palette coherence - Recipe links were blue and cookware was green; the DS contains neither. Links move to the dark end of the DS orange, cookware drops colour entirely for a dotted underline. Both now carry a non-hue cue, which also fixes brown-vs-green being the confusable pair for red-green deficiency. - cooking-mode.css was never migrated in the previous refresh and was still hardcoded slate-navy with orange/amber gradients. It is now on tokens. Fixes a latent custom-property bug: --shadow-* were declared on :root in terms of var(--neu-*), and a custom property containing var() resolves at its declaring element, not its user. .dark got away with it by sharing <html> with :root; .cooking-overlay is a descendant and did not, so cook mode drew a 95%-opacity white halo whenever the site was in light theme. The composites are now redeclared in the dark block. Verified: 140 E2E passed / 0 failed / 5 skipped; cargo test, fmt, clippy clean; 846 text-contrast probes across 8 pages x 2 themes, zero failures.
Replaces the soft dual-shadow elevation with GitHub Primer's structural convention, keeping the Cooklang design system colours from the previous commit. Borders define surfaces; elevation is almost absent. - Canvas lightens to #fcfcfb with white surfaces and a #e4e0da hairline on every box. Radii 16/10px -> 6px. - The elevation scale is now two values: --shadow-card, a 4%-alpha single pixel in light and `none` in dark, and --shadow-overlay for things that genuinely float (dropdowns, dialogs). Nothing in flow uses the latter. - Controls signal state with fill, not depth. Chips are flat and bordered. - Dark canvas is the design system's own #16161D. - cooking-mode.css follows the same structure. Contrast improves across the board on the lighter canvas: --accent as a graphical colour goes from 2.95:1 to 3.69:1, clearing the 3:1 it previously missed, and every text token gains roughly a point. Fixes a theme-toggle bug introduced while building this: transitioning a property whose value is a theme token races Chrome's custom-property invalidation when .dark flips, leaving the element on the outgoing theme's colour until an unrelated recalc corrects it. It reproduced as recipe cards staying white on a dark page, and it was intermittent rather than tied to the background shorthand specifically. All such transitions are removed; hover feedback is instant, which suits a flat design anyway. Verified with 8 synchronous toggles read with no settle time, zero mismatches. Verified: 140 E2E passed / 0 failed / 5 skipped; cargo test, fmt, clippy clean; 840 text-contrast probes across 8 pages x 2 themes, zero failures.
Brings back the sort controls removed in 12a5cc7, restyled onto the token system and with the defects the original carried fixed. Kept client-side: recipes.html is rendered by the static site builder as well as the server, and a ?sort= query has nothing to answer it in a static export. Fixed while restoring: - Name sort read el.querySelector('h3').textContent, so any change to the card markup silently broke sorting. It now reads a data-name attribute. components.css carried a comment warning future editors about exactly this coupling; that warning is no longer needed. - Sorting used localeCompare with no options, so "Recipe 10" sorted before "Recipe 9". Now Intl.Collator with numeric and sensitivity 'base'. - The choice was lost on every navigation. Now persisted in sessionStorage, with the read guarded — a corrupt value falls back to the defaults rather than throwing and leaving the page unsorted. - A persisted field that does not exist on the current page (the "created" option is dropped where not every recipe has a date) is now validated against the live options instead of being applied blindly. - Directories kept source order; they now sort among themselves. - The direction button had a title but no accessible name; it has an aria-label now. - Controls were styled with raw gray-/purple- palette utilities that did not flip with the theme. New .select component, tokens throughout. Adds tests/e2e/recipes-sort.spec.ts — 8 tests, the feature previously had none, which is how it was removed and restored without any test changing colour. Verified non-vacuous by mutation: neutering the comparator fails the reverse-order and modified-date tests, and disabling persistence fails the survives-a-reload test. Verified: 148 E2E passed / 0 failed / 5 skipped; cargo test, fmt, clippy clean; no horizontal overflow at 768/800/820/1024/1280px.
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 |
Audit of five pages found nine distinct font sizes and six weights, with no scale behind either. Sizes were 11.5, 12, 12.5, 13, 13.5, 14, 15, 16 and 24px — six of them inside a 2.5px band. Half-pixel steps sit below the threshold at which a reader can tell two sizes apart, so they could not express hierarchy; they only made each new declaration a guess. Replaced with seven steps, each at least 1px from its neighbours and each mapped to one job: 24 display page title 18 title section headings 16 read recipe step text 14 body list rows, card titles, default 13 ui buttons, inputs, metadata line 12 meta captions, counts, tags, notes 11 label uppercase section labels 18px is new: section headings had been set at 15, 16, 17, 19 and 20px in different templates, five sizes doing one job. Every step carries its own line-height, falling as size rises — long-form text needs the leading to carry the eye back to the next line, headings do not. Line-height had been set in exactly two places in the whole component layer, so most text was on browser or Tailwind defaults. Weights 550 and 650 are gone. They do render on macOS, where the system font resolves to variable SF Pro, but 600 to 650 is a ~1% change in advance width and snaps to a neighbouring weight wherever the system font is not variable — hierarchy that is invisible on one platform and absent on another. Now 400/500/600/700. Tailwind's own text-xs/sm/base/lg/2xl are aliased onto the scale rather than left alongside it. Their sizes already coincided but their line-heights did not, so the same visual size carried two different leadings depending on which name an author reached for. Aliasing fixes ~80 call sites without touching markup, and keeps a future `text-sm` on-scale. Measure: step text ran to 84 characters per line on a wide viewport, past the point where the eye reliably finds the next line. Capped at 68ch, which measures 74 characters and tracks font size rather than freezing a width. Heading semantics, which a visual audit does not surface: - The recipes index went h1 -> h3, skipping a level. Card titles are h2 now. The comment requiring an <h3> was stale from the sorter restore, and the E2E suite now selects .recipe-card-title rather than the tag name. - The shopping list page had no h1; its title was an h2. Promoted. All pages now have exactly one h1 and no skipped levels. Step text moves 15px -> 16px. At 820x1037 the same five steps stay above the fold, but the page grows 69px; that is the cost of a readable size at arm's length, which is the stated use case. Cook mode keeps its own raw-px sizes deliberately. It is read from across a kitchen rather than at desk distance, and retuning it needs a real tablet to validate, not a guess. Noted as a follow-up. Verified: 148 E2E passed / 0 failed / 5 skipped; cargo test, fmt, clippy clean; 802 text-contrast probes across 8 pages x 2 themes, zero failures; re-audit finds six sizes and four weights in use, nothing off-scale.
Rebuilds the web UI on a token-based design system, adopts the Cooklang design system palette, and adopts GitHub Primer's structural conventions: flat surfaces, hairline borders, almost no elevation.
The original goal was making the recipe page usable on a tablet propped in a kitchen. That drove the density work; the design-system and palette work followed from it.
Measured, at 820px
Foundation
A seventeen-token contract in
input.css—--bg,--surface,--text,--accent, and so on — registered with Tailwind via@theme inlineso the generated utilities reference the custom properties and flip under.darkwith no override rules.components.cssholds the component vocabulary; every colour in the UI resolves through a token. No raw hex outside@media print.This replaced a ~330-line block of
.dark .*overrides inbase.htmlthat had to be hand-maintained in parallel with every component.Deleted:
custom-styles.css(146 lines, silently overrodeoutput.csswith duplicate class definitions — including a.btn-primarythat produced a gradient at rest and a flat fill on hover) andstyles.css(444 lines, referenced by nothing).Colour
Uses the Cooklang design system palette.
#16161Dtext,#715329tag text,#E15A29controls and icons,#F5DACFUI One are used exactly as specified.Two values are darkened, hue preserved, because both are used as body text and both fail AA on a light ground as given: Text/Secondary
#817B6D(3.7:1) and Text/Warning#FF3B30(3.0:1).The design system supplies Light only; Dark is derived in the same warm family, using the system's own
#16161Das the canvas.Two things the palette forced that are worth having independently:
cooking-mode.csswas never migrated and was still hardcoded slate-navy with orange/amber gradients, including a heading whose colour came from a gradient clipped to text — invisible wherever the clip is unsupported. Now on tokens.Typography
An audit across five pages found nine font sizes and six weights, with no scale behind either — six of the sizes sat inside a 2.5px band, below the threshold at which a reader can tell two sizes apart. Replaced with seven steps, each mapped to one job and each carrying its own line-height:
displaytitlereadbodyuimetalabeltitleis new: section headings had been set at 15, 16, 17, 19 and 20px in different templates — five sizes doing one job.Line-height had been set in exactly two places in the entire component layer, so most text ran on browser or Tailwind defaults. Weights 550 and 650 are gone: they do render on macOS's variable SF Pro, but 600→650 is a ~1% change in advance width and snaps to a neighbour wherever the system font is not variable.
Tailwind's
text-xs/sm/base/lg/2xlare aliased onto the scale rather than left beside it — their sizes already coincided but their line-heights did not, so the same visual size carried two different leadings depending on which name an author reached for. Aliasing fixes ~80 call sites without markup churn.Measure: step text ran to 84 characters per line on a wide viewport. Capped at
68ch, which measures 74 and tracks font size rather than freezing a pixel width.Heading semantics, which a visual audit does not surface: the recipes index went
h1 → h3, skipping a level, and the shopping list page had noh1at all — its title was anh2. Both fixed; every page now has exactly oneh1and no skipped levels.Step text moved 15px → 16px. At 820×1037 the same five steps stay above the fold, but the page grows 69px — the cost of a readable size at arm's length, which is the stated use case.
Structure
Primer's conventions: borders define surfaces, elevation is almost absent.
The elevation scale is two values.
--shadow-cardis a 4%-alpha single pixel in light andnonein dark.--shadow-overlayis for things that genuinely float — dropdowns, dialogs. Nothing in the document flow uses the second one.Canvas
#fcfcfb, white surfaces,#e4e0dahairlines, 6px radii. Controls signal state with fill, not depth.Behaviour changes
h3text (so any markup change broke it silently),localeComparehad no options (so "Recipe 10" sorted before "Recipe 9"), and the choice was lost on every navigation. It now readsdata-name, usesIntl.Collatorwith numeric collation, and persists insessionStoragewith a guarded read.adjustScale()rather than a duplicate implementation, and preserves scroll position viasessionStorage. Previously a scale tap jumped you back to the top of the recipe.Bugs found and fixed along the way
.cooking-overlayto the dark token selector — 6.6:1.cooking-mode.jsscraped.md\:col-span-2 oland.leading-8, classes the layout work had deleted..bg-white,.grid,.md\:col-span-*— none of which still matched. A dark-theme recipe printed near-black on near-black and never collapsed to one column. Fixed by resetting tokens under@media print.@layer componentsloses to CodeMirror's unlayered runtime-injected styles regardless of specificity. The rules had to move out of the layer.--shadow-*were declared on:rootin terms ofvar(--neu-*). A custom property containingvar()resolves at its declaring element, not its user, so the composite froze with light values and inherited down..darkgot away with it by sharing<html>with:root;.cooking-overlayis a descendant and did not. (Introduced and fixed within this PR.).darkflips — the element keeps the outgoing theme's colour until an unrelated recalc corrects it. Reproduced as recipe cards staying white on a dark page; intermittent rather than tied to thebackgroundshorthand. All token-colour transitions removed. (Introduced and fixed within this PR.)recipe-display.spec.tsguarded its assertions behindif (metadataPills.count() > 0), and the count had become 0 — it could no longer fail. Retargeted with the guard removed.menu_api_test.rsmatched<span class="text-sm text-gray-500">; the regex is now class-agnostic.Items 1–4 and 7 all passed CI throughout. They were found by driving a browser and measuring computed styles, not by the suite.
The index sorter is the same story from the other direction: it had no tests at all, so it was removed and later restored without a single test changing colour.
tests/e2e/recipes-sort.spec.tsnow covers it, and was checked for vacuousness by mutation — neutering the comparator fails the ordering tests, disabling persistence fails the reload test.Verification
shopping-list*.spec.tsraces on the sharedseed/.shopping-listfixture under parallel workers — pre-existing).cargo test,cargo fmt,cargo clippyclean.Known gaps
--accent#E15A29is 3.69:1 as a graphical colour, which passes, but it cannot carry white text (3.7:1), so filled accent buttons use the DS near-black as ink.Follow-ups, deliberately not in this PR
recipe-methodto the seven locales; replace the hardcodedMethodliteral and two stepperaria-labels.--okfrom its dual "in stock" / "cookware" roles.250%g→250 g).42 commits · 29 files · see diff