feat(timeline): let the reader set the scale

One density cannot answer both questions the board is asked. A patch cycle is
six weeks, a login campaign runs for months, and 13px a day is a compromise
between "what am I in the middle of this week?" and "how do the next three
months line up?" that serves neither well.

A pair of controls steps through a ladder of day widths and the choice is
remembered, on the same argument as the view tabs: a reader who has said how
they want to read this should not say it again on the next load.

Two things it had to get right. Zooming holds the middle of the view still —
rescaling around the left edge of a three-month board throws away whatever
they had scrolled to, and re-opening at today would undo the scrolling that
got them there. And the dated ticks thin out as the scale shrinks, because a
week is 42px at the widest setting and the dates would sit on top of each
other; the gridlines stay weekly, since they carry the rhythm rather than the
reading.

The scale is stored as the measurement, not a step number, and read through
`snapDayWidth`. An export written against a different ladder then opens on
something close to what its reader chose, and a corrupt value opens on the
default rather than a board one pixel wide.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-18 04:13:31 +02:00
co-authored by Claude Opus 5
parent d5dbe72e17
commit ba39ad2bce
9 changed files with 302 additions and 27 deletions
+1
View File
@@ -125,6 +125,7 @@ src/
gameMeta.tsx lane id → name, label, hue; resolves custom lanes too
sort.ts deadline order, or what you're partway through
lens.ts who sees which rows — focus, outstanding, next-to-expire
zoom.ts the timeline's scale ladder; pure
useAppUpdate.ts is a newer build waiting, and taking it (F14)
serve.ts static server + /api/health ✓ built
scripts/
+6 -2
View File
@@ -201,8 +201,12 @@ Namespaced, versioned, and small. Nothing here ever goes to the server.
"gacha-tracker:v1:daily" // { [id]: { days: ["2026-08-15", ...], at } }
"gacha-tracker:v1:ignored" // { [eventId]: { at } } — "stop showing me this"
"gacha-tracker:v1:prefs" // { region, hiddenGames[], knownGames[]?, focusGame, sort, view,
// detectDaily, showCompleted, showIgnored, regionConfirmed,
// onboarded }
// timelineDayWidth, detectDaily, showCompleted, showIgnored,
// regionConfirmed, onboarded }
// timelineDayWidth is px per day on the board, stored as the
// measurement rather than a step number and read through
// snapDayWidth — so a value from an older ladder still opens
// on something renderable.
// knownGames is every lane the reader has been offered. Absent
// means unrecorded, not "offered nothing" — see PRD F8; a lane
// missing from it is new to them and arrives switched off.
+10
View File
@@ -80,6 +80,16 @@ calendar on screen, and nothing left saying which day, whose game, or which even
six-week bar starts weeks off-screen, so a name that rides off with its own start date leaves a
coloured rectangle behind.
**The reader sets the scale.** A patch cycle is six weeks and a login campaign can run for months,
so no single density answers both "what am I in the middle of this week?" and "how do the next three
months line up?". A pair of controls steps through a ladder of day widths, and the choice is
remembered (`prefs.timelineDayWidth`) — the same argument as the view tabs: a reader who has said how
they want to read this should not have to say it again on the next load. Two things it has to get
right. Zooming holds the middle of the view still, because rescaling around the left edge of a
three-month board throws away whatever the reader had scrolled to. And the dated ticks thin out as
the scale shrinks, since a week is 42px at the widest setting and the dates would sit on top of one
another; the gridlines stay weekly either way, because they carry the rhythm rather than the reading.
**The board draws at most two months of past.** A standing login campaign can have been running for
half a year, and drawing from the earliest start bought months of empty calendar that nobody scrolls
back through and that pushed every other bar off to the right. An event older than the board keeps