Bottega: the app as the pizzeria's own printed matter - #297
Closed
JanWelker wants to merge 1 commit into
Closed
Conversation
The polite cream web-app is gone. Every surface is now a sheet of shop printing: an enamel sign at the top, a numbered job ticket for the schedule, a deli ticket with dotted leaders and a tear-off stub for the ingredients, and a ruled order pad for the form. Ink does the work shadows used to — hard 2 px rules, flat paper fills, square-ish corners, and a solid offset block where something has to lift off the page. No gradients, no blur, no soft elevation. The palette lives once, as --kt-* variables in :root and html.dark, mapped onto utilities through @theme inline — so the whole page re-inks itself from two blocks and a component almost never needs a dark: variant. Ink carries the structure, tomato is the app's own voice, and basil is spent only on fermentation quality. Light and dark are two authored palettes: dark is the night shift on the same press. Type is Anton for the sign and Archivo for everything else, the latter's width axis supplying the condensed caps every label is set in, so there is no third family for micro-type. Both load as a preload that promotes itself to a stylesheet: a render-blocking request to another origin would hold up first paint, and hydration behind it, on an app that is otherwise entirely local. Nothing in src/lib/dough changed. Every share link reproduces the same recipe, and the print route keeps its own inline styles. Two browser-only bugs found on the way, fixed with their tests: - The rail marker captions pivoted to one end past fixed 12 %/88 % thresholds, numbers chosen against a desktop rail. On a phone the rail is ~240 px and the bake flag's caption is 188 px of it, so a deadline at 62 % counted as centred and hung off the card — and pivoting it fully would have hung the other side off instead. The caption's own box is now clamped into the rail's measured width; the arrow still never moves. - The print-parity check read the print sheet straight after goto, before hydration had swapped the URL's recipe in for the prerendered defaults. It compared the paper against numbers nobody asked for at least once under parallel load; it now waits for the decoded recipe like every other spec. e2e selectors moved with the design: .card is the sheet again (no padding of its own), the window card is .window-card, its rail .window-rail, the rail captions .rail-caption, and the nerdy panel's titles .label-caps. The three specs that pin typefaces and the focus ring were re-pinned to the new faces and the new 3 px ring rather than relaxed. Version bumped to 6.11.0: user-facing, backwards compatible, no URL schema key touched, so CURRENT_VERSION stays 6.
|
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.
Owner
Author
|
Closing: this one won the style. #300 merged as v7.0.0 and carries Bottega's design system essentially whole — the Both of your browser-only fixes came across with their tests: the rail-caption clamping at 390 px (measured width rather than fixed pivot thresholds) and the print-parity spec's hydration wait. Superseded by #300 — not abandoned, merged. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
One idea: Knead Time stops looking like a web app and starts looking like the pizzeria's own printed matter — an enamel sign, a job ticket, a deli ticket, a ruled order pad, printed in two inks on off-white stock.
Ink does the work shadows used to: hard 2 px rules, flat paper fills, square-ish corners, and a solid offset block of ink where something has to lift off the page. No gradients, no backdrop blur, no soft elevation anywhere. Nothing in
src/lib/dough/**changed, every share link reproduces the same recipe, and/print/[[locale]]keeps its own inline styles.The ingredients, biga + poolish, before and after:
What a user sees differently
Masthead. A painted sign: the title set in a heavy condensed display face, reversed out of a tomato plaque with a 3 px ink rule and an offset block behind it, the tricolore hairline running under it the width of the page, and the tagline as the sign-painter's subtitle.
Schedule. The one loud surface in the design — it is what the app is for, so it alone carries the offset block. Its heading is reversed out of a band of ink across the top of the sheet, with the actions trigger reversed into the same band. Each step is a numbered ticket row: the line number is the rail node (a filled ink square for something the baker does, hollow for time passing, dashed rule between them while nothing happens), the time sits in a hard-ruled column in the display face big enough to read across a kitchen, the duration is a stamped chip, and the current step gets a rubber-stamp
NOWmark. Day dividers are full-bleed bands with the date reversed out. Numbers run across the whole plan rather than restarting per day — a schedule is one sequence that happens to cross midnight.Ingredients. A deli ticket. Every name walks to its weight on a dotted leader, the stub tears off above the total on a perforation whose half-round bites are punched into the card's own rule, and the total is struck under a double rule with the figure in the display face. Same
<table>per section as before, so the print sheet still matches it row for row.Form. A quiet ruled order pad: section legends are small ink bands, every field label is condensed caps, and the boxes are hard-ruled and roomy — a dozen numbers get typed in here at a time, so they read crisp rather than cramped. The fermentation window becomes a gauge printed on the sheet: a bordered rail, basil tolerance zones, diagonal ink hatching over everything past the bake time (a printed schedule says "not available" with hatching, not with a grey block), and a square cursor for a thumb.
Marks. The ferment mode is a rubber stamp pressed a degree and a half off square; the fit score is a graded seal — the same hard-ruled box, square to the page, because it is a control and a rotated tap target is a worse tap target.
Collections and menus. My recipes, Community and 50 Top Pizza are drawers with the same reversed band as their label; their tables get an ink header band, dotted row rules and boxed ranking chips. The actions menu drops out of the schedule's band as a bordered sticker with an offset block.
Dark is the night shift on the same press: warm black stock, cream ink, the same hard edges — not a soft dark theme.
Tokens and component classes
:root/html.darkhold the whole palette as--kt-*variables, and@theme inlinemaps them onto real utilities, so the page re-inks itself from two blocks and a component almost never needs adark:variant:--kt-paper--kt-sheet--kt-field--kt-ink--kt-rule--kt-ink-soft--kt-ink-faint--kt-accent--kt-accent-ink--kt-accent-wash--kt-on-accent--kt-herb--kt-herb-ink--kt-herb-wash--kt-grainNew or reshaped component classes in
@layer components:.card(now the sheet — border and fill, no padding) +.card-body,.card-loud,.card-header/.card-header-title,.banner,.window-card/.window-rail/.window-closed,.rail-caption,.stamp/.stamp-herb,.seal,.chip,.leader,.perforation,.rule-double,.tricolore,.field-label,.btn-ghost,.btn-band,.block-shadow{,-sm}, plus rebuilt.input,.btn-tomato{,-sm},.btn-quiet,.menu-item,.pill-*,.notice*,.row-divider,.link-*,.dialog-panel.label-capsis an@utilityso the others can@applyit..text-accentis gone as a component class: registering--color-accentmakes Tailwind generatetext-accentas a utility, which silently beat the component class of the same name and switched ten call sites from the text-safe red to the fill red — 4.05:1, caught by axe. Two reds, two names:text-accent-inkreads,bg-accentfills. Noted in CLAUDE.md so the next token gets checked for the same collision.Type is Anton (the sign painter — masthead, bands, day dates, step titles, the window readout, the ticket total) and Archivo, a variable grotesque whose width axis supplies the condensed caps every label is set in, so there is no third family for micro-type. Both load as a
rel="preload"that promotes itself to a stylesheet, because a render-blocking request to another origin would hold up first paint — and hydration behind it — on an app that is otherwise entirely local.Bugs found on the way, fixed with their tests
bind:clientWidth), and the arrow still never moves. Pinned at both widths./print/enstraight aftergoto, before hydration swapped the URL's recipe in for the prerendered defaults; one parallel run reported the defaults as "the paper". It now waits for the decoded recipe, like every other spec..card-body's padding with nothing in the markup to say so.Browser test count 90 → 93, written into
.github/test-baseline.json.e2e selectors
The specs address the app through these class names, so they moved with the design rather than being relaxed:
windowCard()isform .window-card(wasform div.rounded-2xl), the rail is.window-rail, the rail captions.rail-caption, the nerdy panel's section titles.label-caps.cascade.specandheadings.specwere re-pinned toAnton/Archivo, and the focus ring to 3 px — every border here is already 2 px of ink, so a 2 px ring read as one more rule rather than as the thing you are on.Checks
Run locally on this branch, all green:
npm run lint✅npm run check✅ (0 errors, 0 warnings)npm run test:coverage✅ 100 % statements / branches / functions / linesnpm run test:baseline✅ 940 unit, 93 browserE2E_PORT=4183 npm run test:e2e✅ 93 passed — including axe on the real build in light and dark × beginner and expertVerified by eye at 390 px and 1440 px, light and dark, beginner and expert, on a cold-ferment schedule and a biga + poolish one, plus the print route.
Notes for review
playwright.config.tsnow readsE2E_PORT(const PORT = Number(process.env.E2E_PORT ?? 4173)) so four of these branches can run their suites on one machine. All four sibling PRs make the identical one-line change.package.jsonto6.11.0; whichever lands first wins and the rest rebase. No URL schema key changed, soCURRENT_VERSIONstays 6.messages.tsis unchanged in all five locales.