Skip to content

feat(design): Impasto — the soft, tactile, answer-first redesign - #295

Closed
JanWelker wants to merge 3 commits into
mainfrom
design/impasto-tactile
Closed

feat(design): Impasto — the soft, tactile, answer-first redesign#295
JanWelker wants to merge 3 commits into
mainfrom
design/impasto-tactile

Conversation

@JanWelker

@JanWelker JanWelker commented Sep 7, 2026

Copy link
Copy Markdown
Owner

The answer comes first, and the fermentation window becomes the control the app is really about.

The feedback was that the site looks like it was made by an engineer, and it did: six identical cards, a form-first column that pushed the schedule below the fold on a phone, two type sizes doing the work of a scale, and an "Italian-warm" palette that resolved on screen to cream plus one red. "Impasto" is the phone-app answer — calm, tactile, warm, organised around a single hero decision instead of a wall of fields.

The math, the URL schema, the .ics export, the TRMNL payload and the print sheet are untouched. Every existing share link resolves to exactly the same recipe.

What a user sees differently

Above everything — a bake board. A new region opens the page on every width: the bake moment as a large figure, a "Now / Next" line naming the step the clock is standing in with the time still left on it, and three stat tiles (pizzas, total dough, window). It computes nothing of its own — it reads the schedule the form already produced. On a phone the answer is now wholly inside the first screen; before, you met eight input rows first.

The form. Quieter, and grouped: the two field sets are separated by a rule, every label goes through one class, and the flour/W pair reads as one step. Nothing was removed — expert view still exposes all twelve numbers in the same order, in the same two-up grid, one tap away as before.

The fermentation window. Was a 2 px hairline range input in a small white box. It is now a dough-tinted slab inside the form with a 20 px groove cut into it, the flour's tolerance zones painted inside the groove, a 28 px thumb sitting in the channel, and the duration set as the panel's headline figure rather than a caption. The bake-deadline flag and the ideal-stop marker keep their exact geometry — arrow on the thumb to the pixel, half-thumb inset discipline intact, just at the new radius.

The schedule. A day-grouped feed of step cards instead of a timeline of identical dots. Each rail node carries a drawing of what that step actually is — scales for the weigh, a hook in the bowl for the mix, a fridge for the cold leg, a sliced pizza for the bake — filled tomato when the baker has to do something, a hollow basil ring when time is just passing. The running step is the only card that lifts, and it now sits on a tomato wash rather than the same beige as the ingredient lists inside its neighbours.

The reference sections. My recipes, Community and 50 Top Pizza are flat and hairlined now instead of being the same lifted card as the schedule. Browsing other people's recipes is a detour; the page should say so.

Dark mode is a warm dusk rather than a cooler copy of the light theme: deep red-brown greys, the same soft-light logic, and a visible edge on every panel — see below for why that edge is load-bearing.

Design tokens and component classes

Semantic surfaces and ink are @theme utilities mapped onto --kt-* custom properties that :root and html.dark both define. Since the theme class sits on <html>, which is :root, one declaration serves both themes — so components write bg-surface text-ink-soft instead of a hand-picked grey plus a dark: twin. A hand-picked pair is exactly how the recipe tables once sat at 2.71:1 in dark without anyone noticing.

  • Surfaces: --kt-ground, --kt-surface, --kt-raised, --kt-sunk, --kt-tint, --kt-now, --kt-hairline, --kt-edge
  • Ink: --kt-ink, --kt-ink-soft, --kt-ink-faint, --kt-accent, --kt-leaf
  • Elevation: --kt-lift-1, --kt-lift-2, --kt-press
  • Type: --font-display is now a rounded geometric stack (ui-rounded → SF Pro Rounded → Nunito → …), not a serif; --text-figure and --text-stat clamp against the viewport
  • Components: .card, .card-quiet, .window-card, .well, .popover-panel, .figure, .stat-value / .stat-label, .chip + .chip-{action,time,now}, .field-label, .fieldset-legend, .section-title, .eyebrow, .disclosure-caret — alongside the existing .btn-*, .notice*, .pill*, .input, .link-*, .dialog-panel, .row-divider, .text-accent
  • Colour means something: tomato is ACTION (a step you perform, a button, a refusal), basil is TIME (fermentation, the tolerance band, the cold leg, every passive step), dough is the SUBSTRATE (surfaces, wells, hairlines). Basil had almost no job before.
  • New components: BakeBoard.svelte, StepGlyph.svelte

Three things a future contributor must not tidy away

A soft shadow does nothing on a dark ground. Depth here comes from light — a panel is lighter than the ground and casts a soft warm shadow — but that trick only works in one theme, so every raised surface also carries --kt-edge: transparent in light, a warm hairline in dark. Drop the dark half and the panels dissolve into the page, visible only to whoever looks at the theme nobody checks. e2e/bake-board.spec.ts pins it in both themes.

A chip is a translucent wash, never a fixed fill. .chip carries a step's duration in the schedule and a ranking in the 50 Top Pizza table, and it lands on three different surfaces: the lifted panel, the flat card the collections sit in, and the tomato wash under the running step. A fixed colour can only be tuned to one of them — tuned to the panel, it matched the quiet card closely enough that the ranking pills read as plain text in dark. An alpha wash is relative to whatever it sits on, so it cannot collide with any of the three.

No component transition may name outline-color. transition-colors includes it, so any control carrying that utility fades its own focus ring into existence — 150 ms of no visible focus after a Tab. I hit this while restyling .input; e2e/cascade.spec.ts caught it because it reads the outline immediately after focus, which is exactly when a transitioned ring is still the colour it was before. Every transition in the components layer now lists its properties, and that test carries a note explaining the second failure mode it guards.

Accessibility

--kt-ink-faint started at 4.29:1 against --kt-sunk — the wells are the darkest ground it ever lands on, and axe caught it on the bake board's stat labels. Both ink tokens are now held above 4.5:1 against the sunk surface, not merely against the panel. The full axe sweep is green in both themes and both view modes, every control keeps its accessible name and focus ring, the heading outline is unchanged, and there is no tracked-out capital anywhere in the app any more.

Tests

e2e/helpers.ts addresses the app through class names, so the ones I moved are updated rather than worked around:

  • windowCard() was "the rounded div inside the form"; the form has several rounded surfaces now, so it finds form .window-card — a real component class, which is closer to an API than a style.
  • card() matches .card, .card-quiet, because the regions are deliberately no longer all the same box.
  • thumbCentreX() hard-codes half the thumb as its radius; the thumb went 1.25 rem → 1.75 rem, so it went 10 → 14 and the inset utilities went 2.53.5. All three have to agree, and CLAUDE.md now says so.
  • cascade.spec.ts and headings.spec.ts pinned ui-serif; the display face changed, so they pin ui-rounded. The assertions themselves — a utility must be able to beat the base heading rule, and heading level must not silently change typeface — are untouched.

Seven new browser tests in e2e/bake-board.spec.ts, baseline raised 90 → 97:

  • the answer is on screen before any input (phone) — the layout rule this redesign exists for
  • the board names the step the schedule marks as now — the board and the table decide "what is happening" separately, which is the sort of thing that drifts silently
  • with nothing running the board reports the next step, not a blank
  • a panel is bounded in the light / dark theme — the soft-shadow failure mode above
  • a chip is visible against what it sits on in the light / dark theme — the fixed-fill failure mode above. It polls rather than reading once, because the running step's card transitions its own background: switching theme and measuring in the same tick catches the colour it is on its way from rather than the one it lands on. Verified to fail when the fixed fill is put back.

Checks

Check Result
npm run lint pass
npm run check pass, 0 errors / 0 warnings
npm run test:coverage pass, 940 tests, 100 % statements/branches/functions/lines
npm run test:baseline pass, 940 unit / 97 browser
E2E_PORT=4184 npm run test:e2e pass, 97/97

Verified by eye at 390 px and 1440 px in both themes, on a cold schedule, a room schedule, a biga + poolish schedule and the print route.

Screenshots

Phone, first screen — the whole point of the change

Before After

Phone, dark

Before After

Desktop, above the fold

Before:

After:

Desktop, dark

Before:

After:

The whole page

The densest thing the feed has to render — biga + poolish, cold

Notes for review

  • Version bumped to 6.11.0 (minor: user-facing, backwards-compatible, no URL schema key changed, so CURRENT_VERSION stays 6). The three sibling redesign PRs bump to the same number and will conflict with each other on merge — that is expected; whichever lands second re-bumps.
  • playwright.config.ts reads E2E_PORT with the old 4173 as its default, so four agents can run the suite on one machine. All four PRs make this identical one-line change.
  • Deliberately left alone: src/lib/dough/** (no behaviour change anywhere), the .ics builder, the TRMNL payload and its 2 KB budget, and /print/[[locale]], which owns its own inline styles and still has to fit one B&W page.
  • Deliberately not done: the form does not become a bottom sheet on a phone. The direction floated it, but every field would have moved behind a tap, expert view would have become twelve taps instead of twelve fields, and a large chunk of the browser suite addresses those inputs directly. The bake board delivers the answer-first result without hiding anything.

The app read as assembled rather than designed: six identical cards, a
form-first column that pushed the schedule below the fold on a phone, two
type sizes doing the work of a scale, and a palette that resolved on screen
to cream plus one red.

"Impasto" gives it a point of view. A bake board opens the page with what
you are baking, when, and what to do next, so the inputs sit behind the
answer instead of in front of it. The fermentation window - the one
decision that changes everything - stops being a hairline range input in a
small box and becomes a tinted slab with a groove cut into it, a 1.75 rem
thumb and a large duration readout. The schedule becomes a feed of step
cards, each node carrying a drawing of what that step is. Tomato now means
action, basil means time, and dough is the substrate every surface is made
of, so the palette says something rather than decorating.

Depth comes from light: panels are lighter than the ground and cast a soft
warm shadow. That does nothing on a dark ground, so every raised surface
also carries a hairline that is transparent in light and warm in dark -
both halves designed, neither derived from the other. Semantic surface and
ink tokens replace hand-picked greys with dark: twins, which is how the
recipe tables once sat at 2.71:1 without anyone seeing it.

The math, the URL schema and the print sheet are untouched: no share link
resolves to a different recipe.
Referenced from the pull request body. Both themes at 390 px and 1440 px,
plus one full page and the nine-step biga + poolish schedule, which is the
densest thing the feed has to render.
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://JanWelker.github.io/knead-time/pr-preview/pr-295/

Built to branch gh-pages at 2026-09-07 10:59 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

The neutral chip carries a step's duration in the schedule and a ranking in
the 50 Top Pizza table, and it lands on three different surfaces: the lifted
panel, the flat card the collections sit in, and the tomato wash under the
running step. A fixed fill can only be tuned to one of them - tuned to the
panel it matched the quiet card closely enough that the ranking pills read
as plain text in the dark theme.

An alpha wash of the ink colour is relative to whatever it sits on, so it
cannot collide with any of the three. The regression test measures the
composited difference in both themes, and polls rather than reading once:
the running step's card transitions its own background, so switching theme
and measuring in the same tick catches the colour it is on its way from
rather than the one it lands on.
@JanWelker

Copy link
Copy Markdown
Owner Author

Closing: design variant not chosen. #300 (Servizio × Bottega) merged as v7.0.0.

Impasto was right that the fermentation window deserves to be a hero control rather than a hairline range input, and that idea is still unbuilt — worth revisiting on top of v7. Its focus-ring fix is the one to remember: transition-colors animates outline-color, so a restyled input gets 150 ms of invisible focus after a Tab unless every transition names its properties.

Kept for reference rather than deleted.

@JanWelker JanWelker closed this Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant