Skip to content

Bottega: the app as the pizzeria's own printed matter - #297

Closed
JanWelker wants to merge 1 commit into
mainfrom
design/bottega-print
Closed

Bottega: the app as the pizzeria's own printed matter#297
JanWelker wants to merge 1 commit into
mainfrom
design/bottega-print

Conversation

@JanWelker

Copy link
Copy Markdown
Owner

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.

before after
Desktop, light
Desktop, dark
Phone, light
Phone, dark

The ingredients, biga + poolish, before and after:

before 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 NOW mark. 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.dark hold the whole palette as --kt-* variables, and @theme inline maps them onto real utilities, so the page re-inks itself from two blocks and a component almost never needs a dark: 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-grain

New 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-caps is an @utility so the others can @apply it.

.text-accent is gone as a component class: registering --color-accent makes Tailwind generate text-accent as 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-ink reads, bg-accent fills. 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

  • A rail caption could hang off the card at 390 px. The markers pivoted to one end past fixed 12 %/88 % thresholds — numbers chosen against a desktop rail. On a phone the rail is ~240 px while the bake flag's caption is 188 px of it, so a deadline at 62 % counted as "centred" and hung off the right; pivoting it fully to the end would have hung the other side off instead. The caption's own box is now clamped into the rail's measured width (bind:clientWidth), and the arrow still never moves. Pinned at both widths.
  • The print-parity check could compare the paper against numbers nobody asked for. It read /print/en straight after goto, 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.
  • Two new geometry tests for failure modes this direction introduces: nothing may paint past the edge of the page at 390 px (offset blocks, full-bleed bands and perforation notches all draw outside their box), and the header band and day divider must reach the card's own rule — the negative margins that get them there are coupled to .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() is form .window-card (was form div.rounded-2xl), the rail is .window-rail, the rail captions .rail-caption, the nerdy panel's section titles .label-caps. cascade.spec and headings.spec were re-pinned to Anton / 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 / lines
  • npm run test:baseline ✅ 940 unit, 93 browser
  • E2E_PORT=4183 npm run test:e2e ✅ 93 passed — including axe on the real build in light and dark × beginner and expert

Verified 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.ts now reads E2E_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.
  • The version bump conflicts with the sibling redesign PRs on purpose. All four bump package.json to 6.11.0; whichever lands first wins and the rest rebase. No URL schema key changed, so CURRENT_VERSION stays 6.
  • Deliberately left undone: the print route and the TRMNL payload are separate surfaces and are untouched. No new user-facing copy, so messages.ts is unchanged in all five locales.

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.
@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-297/

Built to branch gh-pages at 2026-09-07 11:18 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 style. #300 merged as v7.0.0 and carries Bottega's design system essentially whole — the --kt-* token block, the ink rules and flat offset blocks, Anton + Archivo with the self-promoting preload, the tricolore as structure, the numbered job-ticket schedule and the deli-ticket ingredients with their punched perforation. What it does not carry is this branch's two-column page, which lost to #298's structure.

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.

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