Skip to content

feat(design): Servizio — the app asks, you answer, it hands you a plan - #298

Closed
JanWelker wants to merge 1 commit into
mainfrom
design/servizio-flow
Closed

feat(design): Servizio — the app asks, you answer, it hands you a plan#298
JanWelker wants to merge 1 commit into
mainfrom
design/servizio-flow

Conversation

@JanWelker

Copy link
Copy Markdown
Owner

The app stops being a form and becomes a service: it asks a short, confident sequence of questions and hands back a plan you live inside for two days.

This is one of the two "rethink it completely" directions, so the layout, the navigation and the order in which the app asks and answers are all redrawn. There is no left form column, no right schedule column and no stack of collapsed sections at the foot of the page.

The plan, 1440 px


What a user sees differently

The landing is one question, not twelve inputs

The first screen asks when are you eating? at 72 px with the answer already sensibly filled in. Everything in this app schedules backwards from that moment and it had never once been staged that way.

The question The window question
Question one The window question

Then four more real decisions, one per screen, full viewport: how many pizzas, which flour is in your cupboard, how long it ferments (the slider gets a whole screen — it is the decision that changes everything), and how you knead it. Each is answerable in one gesture. Each is skippable: "Skip to the plan" is on question one, and the progress rail jumps to any of the five. A live glance beside the question shows the plan forming — start moment, window, flour, mode, step count — so it never reads as a survey.

The plan is a place you return to

The destination is the schedule as a standalone view: the thing you open at 07:00 the next morning with flour on your hands, so it is the calm one. The bake moment is the largest thing on the page. Nothing on it is an input — every editable value is an underlined chip.

Phone, light Phone, dark Biga + poolish, cold
Phone Phone, dark Biga + poolish

Editing happens against the live plan

Tap the bake time, the pizza count, the hydration — the Adjust sheet opens with that field focused. It is a right-hand drawer above lg (the schedule stays visible and live behind it) and a bottom sheet below, and it holds every field in DoughInputs on one dense surface. That is also the expert's door: one press to twenty numbers, no wizard in the way.

Drawer, 1440 px Sheet, 390 px
Adjust drawer Adjust sheet

The collections became entry points

My recipes, Community and 50 Top Pizza are a Recipes view of their own, one press from the first question and one from the plan, instead of three disclosures at the bottom of a 5600 px column.

The library


What moved where

Nothing was dropped. For each region the brief lists:

Region Where it lives now
The twelve recipe inputs AdjustPanel — a modal <dialog> opened from the plan's Adjust button or from any chip. InputForm.svelte still renders them, grouped When / Batch / Dough / what leavens it / where it proofs. Beginner/expert still decide which appear.
Bake time, pizza count, flour, ferment window, mixing method Also asked one per screen on the ask flow (AskFlow.svelte), and shown as editable chips on the plan.
The full schedule (per-step ingredients, detail copy, source-timing badge) The plan's main column (ScheduleTable.svelte), rebuilt as a three-column timeline — time in a hanging gutter, rail, step.
The ingredients table The plan's right-hand rail at xl, under the schedule below it. "Round numbers" sits with it.
Warnings Still in slots, still next to their cause. window + temperature under the plan's summary chips (above the schedule); ingredients beside the weights; window again inside the slider on the ask flow (opt-in via a new warnings prop, so it is never rendered twice). warningSlots.ts is unchanged.
Fit score (stars + panel) The plan's status row, beside the mode badge.
Mode badge Same row, with its blurb.
Schedule verbosity (short/detailed) Same row.
"Get nerdy" info panel Foot of the Adjust sheet, expert-only, in full — infoSections.ts untouched.
My recipes / Community / 50 Top Pizza LibraryView (#library), still collapsed disclosures, still one .md each.
Actions (share, .ics, print, save, TRMNL) The plan's masthead, unchanged menu.
Language + theme switchers The shared footer, under every view.
Footer (licence, version, source/docs/support) SiteFooter.svelte, under every view.
/print/[[locale]] Untouched. Still its own SSR route, still one A4 page in B&W.
TRMNL payload Untouched.

Navigation

src/lib/view.ts (pure, 100 % covered) is the whole of it: AppView = 'ask' | 'plan' | 'library', five ask steps, and initialLocation().

  • The place lives in the URL fragment#ask/<step>, #plan, #library — and adds no query key. The recipe query is byte-for-byte what it always was, hasRecipeParams needs no exception list, and the fragment is linkable, survives a reload and walks with the back button.
  • initialLocation() never walks a returning baker through the questions. An explicit fragment wins; then anyone arriving with a recipe — a share link, a saved recipe, or the one this device last worked on — goes straight to the plan. A truly fresh visit gets question one.

Design tokens and component classes

Palette. Five surface roles as --kt-* on :root / .dark, re-exported through @theme as --color-{ground,plane,ink,ink-soft,line,line-soft}. The ground is limewashed plaster with a green cast (#e9ece1 / #141711), not cookbook cream. Dough is spent only where something is fermenting; tomato means acting or danger and nothing else — it came off section headings, totals and the room-ferment badge. The three hue ramps are unchanged and --color-tomato-500 is still #c8401a (three specs pin the focus ring to it).

Type. One grotesque, three roles separated by size, weight and tracking rather than a serif/sans pair. No webfont — this is a static build that makes no third-party requests and a Google Fonts link would be its only one.

Structure. .card is gone. Regions are separated by space and one .rule hairline; .plane (the only radius + shadow) is for things that genuinely float, so radius reads as elevation.

New in @layer components: .view, .view-pad, .rule, .plane, .question, .lede, .data, .field-label, .section-head, .text-time, .chip, .btn-ghost, .tile, .stepper, .dot / .dot-on, .wordmark, .input-lg, .window-card. Removed: .card.

Motion. One orchestrated moment: moving between questions slides the question block as a single surface (.kt-enter, direction-aware). It is the only non-user-triggered animation besides the current step's node pulse, both silent under prefers-reduced-motion — pinned by a test.


Testing

e2e/ encodes rules, not layout, but it reached them through the old layout, so the specs were rewritten to test the same rules through the new structure. Nothing was weakened, deleted or lowered:

  • helpers.ts now addresses the app through main [data-view], region() (a section addressed by heading), sheet() / openAdjust(), .window-card, #field-window. card() and formCard() went with .card.
  • Form, slider, help-text, info-panel and persistence specs open the Adjust sheet first; the collections specs go via the library; save-recipe checks the book in the library.
  • warnings.spec.ts asserts the same slot mapping against the new mount points, and gained a check that the window family follows the slider onto the ask flow.
  • Baseline raised, not lowered: 940 → 949 unit (all of view.ts), 90 → 114 browser.
  • New: e2e/views.spec.ts (8 tests — landing rules, reload, back/forward, the recipe query surviving every move, chip-to-focus, sheet dismissal) and e2e/ask-flow.spec.ts (5 — one gesture per question, skippability, the stepper, the shared slider, reduced motion). axe.spec.ts grew from 4 runs to 12: plan, sheet, questions and library, each in both themes and (where it matters) both view modes.

Checks run, honestly:

Check Result
npm run lint pass
npm run check pass — 396 files, 0 errors, 0 warnings
npm run test:coverage pass — 949 tests, 100 % statements / branches / functions / lines
npm run test:baseline pass — 949 unit, 114 browser, coverage gate at 100
E2E_PORT=4185 npm run test:e2e pass — 114/114
Looked at with my own eyes 390 px and 1440 px, light and dark, beginner and expert, a cold plan and a biga+poolish cold plan, the ask flow, the library, the Adjust sheet, and the print route

playwright.config.ts takes E2E_PORT (Number(process.env.E2E_PORT ?? 4173)) so four agents can share this machine — all four sibling PRs make this identical one-line change.


Versioning — npm version minor → 6.11.0, CURRENT_VERSION stays 6

Judged against the addendum's test: the contract did not move. No URL key was added, renamed or repurposed; the fragment carries the view precisely so the query would not have to. Every published v=1 … v=6 link decodes identically and reproduces its recipe gram for gram and minute for minute — urlState.test.ts and the legacy-link e2e specs are unchanged and green. Every DoughInputs field is still reachable and editable by mouse, touch and keyboard; beginner and expert both still exist and still mean what they mean.

And a returning bookmark-holder is not made to answer anything: they land on their plan. What changed is the arrangement and the aesthetics, and the addendum is explicit that those alone are not a major bump.

This bumps to the same 6.11.0 as the sibling redesign PRs and will conflict with them on package.json / package-lock.json at merge. That is expected — resolve by keeping one 6.11.0. .github/test-baseline.json will also conflict with any sibling that added tests; take the higher number of each.


Deliberately left undone

  • The library's three collections still ship collapsed. The rule they were given ("browsing other people's recipes is a detour") is weaker now that they are a destination, but three long tables stacked open is a wall, and closed they read as an index of three. Opening them by default is a one-line change if you disagree.
  • The ask flow asks the beginner subset. An expert who wants oil, sugar or a pre-ferment goes to the Adjust sheet — which is the point of having two doors — rather than getting a longer flow.
  • No webfont, so the display face is a platform grotesque. A licensed face would sharpen the question screens further, at the cost of the app's first third-party request.

Knead Time stops being a form. It opens with one question set large —
when are you eating? — and walks through four more, each answerable in a
single gesture, with the plan forming beside it. The answer is a place
you return to: a full-screen schedule where every value is underlined
and opens for editing where it stands.

The layout is redrawn rather than restyled. There is no left form
column, no right schedule column and no stack of collapsed sections at
the foot of the page; there are three views, exactly one of which is
mounted at a time, and src/lib/view.ts owns which. The place lives in
the URL fragment so it is linkable, survives a reload and walks with the
back button — the recipe query is untouched, no key was added and
CURRENT_VERSION stays 6, so every published share-link still resolves to
the same recipe.

Nobody is walked through the questions twice: anyone arriving with a
share link or the recipe this device last worked on lands straight on
the plan. Anyone who already knows all twelve numbers opens the Adjust
sheet — a native modal dialog holding every DoughInputs field on one
dense surface — and fills them in without stepping through anything.

Visually: no cards, one grotesque separated by size and weight instead
of a serif/sans pair, a limewashed-plaster ground in place of cream, and
the dough colour spent only where something is actually fermenting.
Tomato now means acting or danger and nothing else.

The browser suite reaches the same rules through the new structure and
gains eight views/flow specs plus axe coverage of the sheet, the
questions and the library; the unit suite gains view.ts at 100 %.
@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-298/

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

JanWelker added a commit that referenced this pull request Sep 7, 2026
Direct feedback on the old design was that it looked like it was made by an
engineer, and it did: every region was the same cream card, the page opened
with twelve near-identical label-over-input rows, and the schedule — the thing
the app is for — sat below the fold on a phone and no louder than the fridge
temperature field. This is the redesign that answers it, combining the
structure of PR #298 with the visual language of PR #297.

THE STRUCTURE. The app is three places, and which one is on screen lives in the
URL fragment (`#ask/<step>`, `#plan`, `#library`) so it is linkable, survives a
reload and walks with the back button — and adds no query key, so the recipe
schema is untouched and `hasRecipeParams` needs no exception. It opens with one
question at 72 px, then four more, each answerable in a gesture and each
skippable. Anyone arriving with a share link or a remembered recipe lands
straight on the plan and is never asked anything. The plan carries no inputs:
every value is a blank on a printed form that opens the adjust sheet focused on
that field, and the sheet is the expert's door — one press to twenty numbers.
The collections became a view of their own, since they are entry points to a
recipe rather than an appendix to one.

THE PRESS. Ink does the work shadows used to: flat paper stock, 2 px rules,
square corners, offset blocks, nothing blurred. Anton for the sign, Archivo's
width axis for the condensed caps, loaded as a preload that promotes itself so
a slow font server can never block first paint. The schedule is a numbered job
ticket where the line number IS the rail node; the ingredients are a deli
ticket with dotted leaders and a real punched perforation; the masthead is a
painted enamel plaque over a tricolore hairline that closes the colophon too —
that flag is what holds three views together as one publication.

Every control sits with the thing it acts on: the buttons under the flag with
the values they change, the mode and fit seals on the schedule's lede line,
each opening what its mark means. Language, theme and step detail are choices
in one menu, which is why SegmentedControl and the pill strip are gone. The
mode and the fit are drawn guild seals rather than outlined boxes carrying
ASCII stars.

The maths is untouched. `src/lib/dough/**`, the .ics builder, the TRMNL payload
and the print route are unchanged; every v=1…v=6 link reproduces its recipe
exactly, and `CURRENT_VERSION` stays 6, so this is a minor bump to 6.11.0.

Testing: 951 unit at 100 % coverage, 122 browser tests (from 940/90). Nothing
was weakened to fit the new structure — specs were rewritten to reach the same
rules through it. Bugs found and fixed along the way, each with its own test: a
question's slide-in painting past the sheet and putting a scrollbar under every
move at 390 px; the tricolore's middle panel vanishing on the dark press,
because the flag's white is the stock; a dotted leader stranded as a stub
beside a flour name that wrapped; a fit panel that lost its position, border,
fill and shadow at once because `@apply` reached for another components class;
and a menu that painted behind the schedule card after a transform trapped its
stacking context.
JanWelker added a commit that referenced this pull request Sep 7, 2026
BREAKING CHANGE: the default experience is a different app. Anyone returning to
a bookmark meets a question flow and a job ticket where a form and a table used
to be, which is what CLAUDE.md calls a user contract break, so this is 7.0.0.

Direct feedback on the old design was that it looked like it was made by an
engineer, and it did: every region was the same cream card, the page opened
with twelve near-identical label-over-input rows, and the schedule — the thing
the app is for — sat below the fold on a phone and no louder than the fridge
temperature field. This is the redesign that answers it, combining the
structure of PR #298 with the visual language of PR #297.

THE STRUCTURE. The app is three places, and which one is on screen lives in the
URL fragment (`#ask/<step>`, `#plan`, `#library`) so it is linkable, survives a
reload and walks with the back button — and adds no query key, so the recipe
schema is untouched and `hasRecipeParams` needs no exception. It opens with one
question at 72 px, then four more, each answerable in a gesture and each
skippable. Anyone arriving with a share link or a remembered recipe lands
straight on the plan and is never asked anything. The plan carries no inputs:
every value is a blank on a printed form that opens the adjust sheet focused on
that field, and the sheet is the expert's door — one press to twenty numbers.
The collections became a view of their own, since they are entry points to a
recipe rather than an appendix to one.

THE PRESS. Ink does the work shadows used to: flat paper stock, 2 px rules,
square corners, offset blocks, nothing blurred. Anton for the sign, Archivo's
width axis for the condensed caps, loaded as a preload that promotes itself so
a slow font server can never block first paint. The schedule is a numbered job
ticket where the line number IS the rail node; the ingredients are a deli
ticket with dotted leaders and a real punched perforation; the masthead is a
painted enamel plaque over a tricolore hairline that closes the colophon too —
that flag is what holds three views together as one publication.

Every control sits with the thing it acts on: the buttons under the flag with
the values they change, the mode and fit seals on the schedule's lede line,
each opening what its mark means. Language, theme and step detail are choices
in one menu, which is why SegmentedControl and the pill strip are gone. The
mode and the fit are drawn guild seals rather than outlined boxes carrying
ASCII stars.

THE SCHEMA IS NOT BROKEN. `src/lib/dough/**`, the .ics builder, the TRMNL
payload and the print route are unchanged, and every v=1…v=6 link reproduces
its recipe exactly. CURRENT_VERSION goes to 7 only because the major version is
pinned to it: v=7 adds no key and no gate, both existing gates sit below it, and
two tests hold that — the encoded key set is unchanged, and the same query
decodes identically at v=6 and v=7.

Testing: 953 unit at 100 % coverage, 122 browser (from 940/90). Nothing was
weakened to fit the new structure — specs were rewritten to reach the same
rules through it. Bugs found and fixed along the way, each with its own test: a
question's slide-in painting past the sheet and putting a scrollbar under every
move at 390 px; the tricolore's middle panel vanishing on the dark press,
because the flag's white is the stock; a dotted leader stranded as a stub
beside a flour name that wrapped; a fit panel that lost its position, border,
fill and shadow at once because `@apply` reached for another components class;
and a menu that painted behind the schedule card after a transform trapped its
stacking context.
@JanWelker

Copy link
Copy Markdown
Owner Author

Closing: this one won the structure. #300 merged as v7.0.0 and carries this branch's architecture intact — the three views with the place in the URL fragment (#ask/<step>, #plan, #library), initialLocation() sending a share link or a remembered recipe straight to the plan, the plan as a surface with no inputs where every value is a blank that opens the adjust sheet on that field, and the collections promoted to a view of their own.

Reading the view state out of the fragment rather than the query is the decision that made the whole thing possible: no new query key, so the recipe schema was untouched and hasRecipeParams needed no exception. What it does not carry is this branch's limewashed-plaster palette and system grotesque, which lost to #297's press.

Superseded by #300 — not abandoned, merged.

@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