Skip to content

Quadrante: the whole app becomes one instrument for time - #299

Closed
JanWelker wants to merge 2 commits into
mainfrom
design/quadrante-dial
Closed

Quadrante: the whole app becomes one instrument for time#299
JanWelker wants to merge 2 commits into
mainfrom
design/quadrante-dial

Conversation

@JanWelker

@JanWelker JanWelker commented Sep 7, 2026

Copy link
Copy Markdown
Owner

The whole app becomes one instrument for time: a 24-hour dial with the bake plan drawn on it as a coil, one revolution per day.

Every version of this app has drawn time as a vertical list of rows. That is a spreadsheet's idea of time, and it is a large part of why the app reads as engineered. Dough does not happen in rows — it happens over a day and a half, mostly overnight, mostly while nothing is being done. That shape is a cycle.

So the centre of the page is now a dial. Noon at the top, hours clockwise, the schedule drawn as a coil that makes one revolution per day: a 32 h bake is a turn and a bit, a 72 h bake is three coils, and you see how long the dough takes before you read a single number. The bake sits on the outer rim and the dough unwinds inward from it, which is what "everything schedules backwards from readyBy" actually looks like when you draw it. The 22:00–08:00 night is a fixed wedge that every coil passes through — possible because a moment's angle is its time of day and nothing else.

The dial at 1440 px, light

What a user sees differently, region by region

The dial (new). Warm phases (counter) are amber, cold phases (fridge) are blue — hue means temperature on this face and nothing else. Baker-action steps are engraved radial ticks across the coil; waiting phases are the coil itself. Two pre-ferments maturing in parallel get a lane each, drawn dashed, and merge at prep. A clock hand shows now, with a bead where it crosses the dough. The centre plate carries the bake time in plain words, because a dial nobody can read a time off is a picture of a schedule.

a short room-temperature bake 72 h, biga + poolish, three coils
room mode 72h

It is a control, not a picture. The coil is a role="listbox" with roving tabindex — one tab stop, arrows walk the plan — and clicking or focusing an arc moves the readout. The coil's tail is a role="slider" that turns like a knob: one revolution of the face is one day of fermentation window, snapping to exactly the stops the rail in the form offers, with arrow keys and Home/End. Dragging past the bake deadline is refused out loud, in the same words the rail uses. Everything draggable keeps a keyboard path and a plain numeric field behind it.

The step detail moved beside the dial. A readout panel gives the selected step in full — date, start and end time (naming the day when a leg crosses midnight), duration, what goes on the scale, the method copy, the detail paragraph. It and the plan list render from the same stepCopy.ts functions, and share one selection, so they cannot disagree.

The form became a settings rail. Sticky on the left at lg+, with its own scroll region; below lg it sits under the answer rather than in front of it. Same fields, same order, same beginner/expert rule.

The plan list stays, as the secondary representation and the accessible equivalent of the drawing — same day headers, same rail, same per-step ingredients and detail copy. Its step titles are now buttons that move the dial.

The window card got tougher, not different. Its bake-limit caption was one unwrapped line positioned at a percentage, so its width was whatever the font made it — about 6 % wider on the CI runner than on my machine, which is enough to hang it off the rail. It wraps inside a max-width derived from the space on its side now, so it is inside by construction. And the tick labels became a doubling ladder — 8 / 18 / 36 / 72 — with no two adjacent stops labelled, which retires the phone-only label set that existed only because 48 h and 72 h are neighbours. Both are still snap points; they just carry a notch rather than text.

Everything else is quieter. .card lost its shadow, blur and 2xl radius: the instrument is the only thing on this page allowed to look like an object, and eight drop-shadowed boxes around it is what made the old layout read as a stack of widgets.

Dark
Phone, 390 px

What moved where

Region Where it lives now
Schedule Drawn twice: as the coil on the dial (centre), and as the day-grouped list under it — card(page, 'Schedule'), unchanged markup
Per-step ingredients + detail copy Unchanged in the list; also in the new readout beside the dial
Ingredients table A card beside the plan (xl), under it otherwise. Round-numbers button unchanged
Warnings Same three slots, still decided by warningSlots.ts: window → slider card, temperature → under the temperature fields, ingredients → under the weights
Fit score (stars + panel) The instrument's readout column, next to the mode badge
Mode badge Same column, above the readout
Short/detailed switch Same column; still drives both the list and the readout
The form (all of DoughInputs) The settings rail — sticky left at lg+, below the instrument on a phone. Nothing removed, nothing re-ordered
Fermentation-window slider Still at the end of the When fieldset. The dial's tail is a second control on the same value
"Get nerdy" info panel Still in the form, expert-only, all sections rendered (formulas now wrap instead of scrolling sideways)
Actions (share, .ics, print, save, TRMNL) Masthead, top right
Language + theme switchers Masthead, top right
My recipes / Community / 50 Top Pizza Collapsed, in one quiet stack below the instrument
Footer (licence, version, links) Unchanged, at the foot
/print/[[locale]], TRMNL payload Untouched

Tokens and component classes introduced

  • --kt-* theme roles, aliased into @theme as --color-* so every Tailwind utility resolves through them and html.dark swaps all thirteen at once: ink, ink-soft, ink-faint, face, face-deep, rule, rule-soft, warm, warm-soft, cold, cold-soft, night. This replaces the text-stone-* dark:text-stone-* pairs across every component — two palettes with a hand-written dark half each.
  • A frost colour scale (50–900). The app had no cold colour at all, so "in the fridge" was told in the same warm palette as "on the counter".
  • New component classes: .instrument, .readout, .step-row / .step-row-on, .field-label, .legend-dot. .card restyled to a hairline surface.
  • src/lib/dial.ts: DIAL_VIEWBOX, R_OUTER/R_INNER, STRAND_GAP, MIN_ARC_LENGTH, MIN_HIT_DEGREES, SAMPLE_DEGREES, STEP_TEMPERATURE, plus buildDial, stepKey, nightWedgePath, pointerAngle, unwrapDelta.

The math is untouched

Nothing in src/lib/dough/*.ts changed behaviour, and no field was added to ComputedSchedule — the dial reads ScheduleStep[] the same way the table does. Every share-URL reproduces the same recipe, gram for gram and minute for minute; the v=1 … v=6 decode tests and the whole yeast-solve/mass-balance matrix are unchanged and green.

Version

npm version minor → 6.11.0, deliberately not a major. The addendum's test is whether the contract moved: no URL key changed, CURRENT_VERSION stays 6, and a returning bookmark-holder gets exactly their recipe back. The app is the same tool arranged differently, so the schema version has nothing to say about it. This collides with the four sibling redesign PRs, which all bump to the same number — expected, and trivial to resolve.

Tests

The browser suite reaches its rules through the layout, so a real restructure moves it. Nothing was weakened or deleted:

  • New: e2e/dial.spec.ts (11 tests) — the drawing and the list agreeing on one selection; the coil being walkable from the keyboard; the tail and the rail landing on the same stop; a refused drag saying so; a leg that ends on another day naming the day; the readout opening on the step that is running; the face fitting 390 px with no sideways scroll; the phone order (plan before weights, settings past both); the desktop rail beside the instrument; the rail being reachable to its bottom.
  • New: src/lib/dial.test.ts (21 tests) — the geometry, including every edge case the direction called out.
  • e2e/layout.spec.ts: its two placement tests were about the form/schedule/ingredients grid the dial replaced. They are not gone — a comment where they used to be names the two tests in dial.spec.ts that ask the same questions of the new structure.
  • e2e/headings.spec.ts: the h2 list gains the dial's stage; the outline rules are unchanged.
  • e2e/focus-dismissal.spec.ts: the fit-score locator follows the panel to its new home.
  • e2e/window-slider.spec.ts: the marker test now reads every line of both captions rather than only the ones carrying whitespace-nowrap — the class that let them overflow. Its two tick tests became one rule asked at both widths (same count).
  • Baseline raised: unit 940 → 961, e2e 90 → 99.

Two browser-only bugs found and fixed with their tests, per the house rule: a 25 h fridge leg printed 01:00 PM – 02:45 PM with no day, and a sticky rail taller than the viewport pinned its own lower half out of reach.

Checks run

Check Result
npm run lint pass
npm run check pass — 390 files, 0 errors, 0 warnings
npm run test:coverage pass — 961 tests, 100 % statements/branches/functions/lines
npm run test:baseline pass — 961 unit, 99 browser, gate at 100 %
E2E_PORT=4186 npm run test:e2e pass — 99/99, axe clean in both themes and both view modes

playwright.config.ts takes the port from E2E_PORT (the same one-line change all five redesign branches make, so it merges trivially).

Looked at with my own eyes at 390 px and 1440 px, in both themes, for a short room-mode bake, a cold two-day bake and a 72 h biga + poolish.

Left undone, deliberately

  • The masthead takes three rows at 390 px (title, switchers, actions). The dial still clears the fold on an 844 px screen, so I left it rather than hiding the tagline.
  • The dial does not label which coil is which day. The readout and the plan list both carry dates, and a ring of date labels would crowd a three-coil face.
  • Print and TRMNL are untouched: they are separate surfaces with their own constraints, and neither gains anything from a dial.

The schedule was a vertical list of rows: a spreadsheet's idea of time, for a
subject that is a cycle. It is now a dial. A 24-hour face with noon at the top,
the plan drawn on it as a coil — one revolution per day, so a 32 h bake is a
turn and a bit and a 72 h bake is three coils, and the length of a plan is
something you see before you read a number. The bake sits on the outer rim and
the dough unwinds inward from it, which is what "everything schedules backwards
from readyBy" looks like when you draw it. The 22:00–08:00 night is a fixed
wedge every coil passes through, because a moment's angle is its time of day
and nothing else.

The dial is a control, not a picture: the coil is a listbox of steps with
roving tabindex, and its tail is a slider that turns like a knob, one
revolution to the day, snapping to exactly the stops the rail in the form
offers. Selecting an arc moves the readout beside the dial, which reads that
step out in full from the same stepCopy functions the plan list uses.

Everything else orbits it. The form is no longer a column standing in front of
the answer but a settings rail, sticky and scrollable in its own right; the
step list is the plan below the dial; the weights sit beside it; the three
collections stay collapsed at the foot.

The geometry is a pure module (src/lib/dial.ts) over ScheduleStep[] — angles,
coil radii, sampled spiral paths, the night wedge, hit areas — unit-tested to
100 % including midnight wrap-around, a bake under one revolution, a 72 h
three-coil plan, a phase too brief to draw and two pre-ferments sharing the
same minutes. Nothing in src/lib/dough/ changed and no field was added to
ComputedSchedule.

Also folds two palettes into one: --kt-* theme roles (ink/face/rule and a new
warm/cold pair, where hue means temperature) replace the hand-written
text-stone-* dark:text-stone-* pairs throughout, and the quiet ink tier that
had been sitting at 3.6:1 on 46 nodes is now AA in both themes.

Fixed along the way, each with a test: a 25 h leg printed "01:00 PM – 02:45 PM"
with no day, and a sticky rail taller than the viewport took its own lower half
out of reach.
@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-299/

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

…trics

The window card's bake-limit caption was one unwrapped line positioned at a
percentage, so its width was whatever the font made it: the same string renders
about 6 % wider on the CI runner than on the machine it was designed against,
which was enough to hang it off the end of the rail. It now wraps inside a
max-width derived from how much of the rail is available on its side, so it is
inside by construction whatever the metrics do — with the pivot moved out to a
quarter of the rail, since a centred caption only gets twice the shorter side
to spread into. Both markers share one .rail-caption class.

The tick labels had the same fragility from the other end: the rail is linear
in stop index, so 48 h and 72 h sat on adjacent stops about one label-width
apart, which needed a phone-only label set to paper over and still came within
two pixels of colliding. The set is a doubling ladder now — 8 / 18 / 36 / 72,
never two adjacent stops — the same at every width, and the special case is
gone. 24 h and 48 h are still snap points; they just carry a notch, not text.

The two tick tests become one rule asked at both widths, and the marker test
now reads every line of both captions rather than only the ones that carried
whitespace-nowrap — the class that let them overflow in the first place.
@JanWelker

Copy link
Copy Markdown
Owner Author

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

Quadrante made the sharpest argument of the seven — that a vertical list is a spreadsheet's idea of time, and dough is a cycle — and the coil genuinely reads: 72 h is three revolutions before you read a number. It lost on risk, not on merit. src/lib/dial.ts is pure, unit-tested geometry over ComputedSchedule and would port to v7 largely as-is if the dial is ever revisited.

It also found a real content bug the other six shipped: a 25 h fridge leg printing 01:00 PM – 02:45 PM with no day, so a schedule spanning midnight lied about which afternoon it meant. That one is still open on main and worth fixing independently.

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