fix(agenda): make the desktop timetable readable - #338
Merged
Conversation
The desktop timetable is a proportional time × room grid, so every minute between the first and last session is drawn. The afterparty runs 18:30–00:00, which added five and a half hours of empty ruled sheet under the last talk — roughly half the page — and, because a band is a flex row, floated its "AFTERPARTY" label in the middle of that void, two hours below the hairline it belongs to. Three fixes: - A trailing band — one that starts after everything else on the day has finished — is capped at 30 minutes for layout (`gridPlacements`), so the day ends where the day ends. The strip still prints its real range. Only trailing bands are capped: shortening lunch would open a gap that reads as free time. - A session whose end crosses midnight lost its end entirely. `parseLocalMinutes` returns minutes-from-midnight, so the afterparty's 00:00 came back as 0, which is not after its 18:30 start and fell through to the 30-minute fallback. `placement` now carries the day difference, and `formatMinutes` wraps past- midnight values so the label reads 00:00, not 24:00. - Bands align to `flex-start` and 20-minute talks get the height they measure (`ROW_PX` 24 → 26); at 24 a lightning talk was 96px against 102px of content and clipped its speaker row. Verified against the live `/api/lineup` payload at 1025 / 1440 / 1920: the sheet now ends at 19:00 instead of 00:00 and no cell clips. `npm run a11y` passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ryzizub
force-pushed
the
claude/devfest-agenda-pc-display-4390df
branch
from
September 7, 2026 21:09
c61a4f7 to
524bebc
Compare
…id of lines The sheet was ruled on both axes: a vertical hairline per room column running the full height, an hour hairline across the full width, and a top AND bottom rule on every band. Four things came out of that: - the column hairline ran straight through every full-width band, slicing BREAKFAST, KICK OFF, KEYNOTE and LUNCH in two; - a band's bottom rule sat 1px above the next cell's top rule, so every boundary on the sheet was drawn twice; - an empty stretch — 17:05–17:35, or a room with nothing on — was ruled exactly as heavily as an hour of content, so free time read as scheduled; - the hour lines, solid at `--rule-soft`, were the loudest thing on a page whose content is mostly dark. So the ruling is now carried by three things instead of one grid of lines: - The room columns are parted by a 6px column gap; the vertical rules are gone. Rows keep `gap: 0`, so a talk still butts against the next hour and the sheet reads as one continuous field rather than tiles. - The hour hairline is dashed and fainter (0.07 vs 0.13) — a tick mark for the numeral beside it, not a line of content. - A band is a hatched strip with a single rule on its top edge: the day stopping, struck through, rather than another lit block with rules on both sides. Verified at 1025 / 1440 / 1920 against the live `/api/lineup` payload: no horizontal scroll, no clipped cells, zero column rules, all eleven bands intact. `npm run build` and `npm run a11y` pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The desktop sheet reads as a timetable now rather than as blocks floating on black: a talk is an opaque panel in its room column, every minute nobody is on is struck through, and the ruling sits underneath all of it. - **Dead time is hatched.** `idleSpans()` returns the stretches of the day that no session occupies in ANY room, and the grid draws each as a full-width hatched strip — the same fill a break already had, without the label. On a proportional sheet an empty half-hour was otherwise indistinguishable from a half-hour whose talks simply have not been announced. A room idle while another room runs a talk is not dead time and stays open. - **The column hairline is back, between the rooms only.** It used to run down the left edge of every column (including the first) and straight through every full-width band. Panels, bands and the new hatch strips are all opaque, so nothing cuts through content any more. - **A talk is a panel**: `--panel-2` fill, a `--rule` hairline box, `--radius`. The header row is filled too — it is the one row that must stay legible while the sheet scrolls under it. Two defects surfaced while doing it: - **A short talk rendered with no title.** The cell is a flex column and the title was the only thing in it that could give, so in a 20-minute cell (104px against 164px of content) it was shrinking to 3px: eight of the sixteen talks showed a time and a speaker and nothing else. Nothing in the cell shrinks now, and a talk under 30 minutes takes a compact stack — time and title on one line, speakers under it, no tags. - **The rows did not survive a text-only zoom.** They were sized in px, so at a 32px root eight cells overflowed their slot and one had no room for its title. The row unit is `rem` now (1.625rem = the same 26px at the default root), so the sheet grows with the text. Verified at 1025 / 1440 / 1920 against the live `/api/lineup` payload, and at a 32px root: no horizontal scroll, no overflowing cell, no titleless cell. `npm run build` and `npm run a11y` pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…very stop Three things about how the sheet spends its space: - **Every talk title is the same size.** A short talk was dropping to the UI step while a 40-minute one kept the card-title step, so the sheet had two typographic ranks for the same kind of thing. A short talk now takes the same step on one line and truncates. - **Every talk carries its topic.** The compact cell dropped the tags; it now carries them on its meta line, ahead of the speakers. Under 20 minutes the line has no room for a 26px avatar row, so those cells show the topic and drop the speakers (they are in the sheet the cell opens, and in its aria-label). Two talks show no topic because Sessionize has no category on them yet. - **Every non-talk strip is one line tall.** The sheet ran to scale everywhere, so the afterparty (18:30–00:00) was twenty times the height of the talk above it and lunch alone was 80 minutes of hatch — the day's content lived in the top third of a page of empty strips. `rowScale()` keeps the talks proportional and compresses every band and dead stretch to two rows, and `nonTalkSpans()` finds those stretches (a band that overlaps a talk is left to scale, since squeezing it would drag the talk off its own start time). The whole day is 2006px now, down from 3514. That also retires the trailing-band cap: the afterparty is short because every stop is short, not because of a rule about the last one. `gridPlacements` is gone and `dayRange` reads real placements again, so the day ends at 00:00 as it should and the now-line works during the party. An hour tick inside a compressed strip is skipped — the sheet is not to scale there, and the numeral would lie. Verified at 1025 / 1440 / 1920 and at a 32px root against the live `/api/lineup` payload: every band 52px, one title size, no overflowing or titleless cell, no horizontal scroll; the mobile list is untouched. `npm run build` and `npm run a11y` pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A short talk was rendered as a "compact" cell: the title on one line beside the time, truncated with an ellipsis, and the topic pushed onto the meta line in front of the speakers. Both are wrong. A timetable's whole job is telling you what a talk is, so cutting the title mid-phrase defeats it, and a topic whose position moves with the length of the speaker list beside it cannot be scanned down a column. The compact cell is gone. Every talk now takes the same stack — time, full title, topic, speakers — and the title has no line clamp at all. What made that possible: the grid rows are `minmax(1.625rem, auto)` instead of a fixed height, so a row grows to whatever the talk in it needs. Every column shares the same rows, so the tracks stay aligned while they grow, and the sheet is still to scale through the talks (the row minimum) and compressed across the stops. Cell heights now vary by content — 134 to 208px at 1440 — rather than by the clock alone. Verified at 1025 / 1440 / 1920 and at a 32px root against the live `/api/lineup` payload: no cell overflows its slot, no title is clamped, every band is one strip tall, no horizontal scroll; the mobile list is untouched. `npm run build` and `npm run a11y` pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dashed hour rule and the hatch stripe were raw `rgba(240, 237, 230, …)` at 0.07 and 0.05 — both a rounding error away from `--rule-soft` (0.06), which is the token for exactly this weight. DESIGN.md keeps decorative hairlines on the rule tokens, so they use it. No visual change worth the name: the hour rule stays dashed, which is what keeps it reading as a tick rather than as content. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ryzizub
added a commit
that referenced
this pull request
Sep 8, 2026
… a comment that now says the opposite of the code (#339) Two things a review of #338 turned up. **`.cell::before` animated under `prefers-reduced-motion: reduce`.** The block sets `transition: none` on `.cell` and on `.cell::after`, but a pseudo-element carries its own transition and is not reached by the rule on its host, so the light wash kept fading over 0.28s on hover and focus. DESIGN.md is binding here: every animation and transition has a reduced-motion counterpart. Verified in the browser — the cell's `::before` reports 0.28s at `no-preference` and 0s at `reduce` now; `::after` and the cell itself were already covered. **`.grid`'s comment claimed there is no vertical rule between the rooms.** The rule was removed in one commit of #338 and reinstated in the next, once the bands and dead-time strips became opaque and could close over it — but the comment explaining its removal stayed, so the file asserted three different things at once (its header says the sheet is ruled "down each room", `.colRule` says "one hairline BETWEEN the rooms", and `.grid` said there is no such rule and gave the reason it was rejected). The stale "hair-thin on purpose" clause went the same way: that gap is 10px now, not 6px. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
Summary
/agendaon a desktop was unreadable in several ways at once. The sheet ran to midnight because the afterparty does, so half the page was empty ruled grid; a session ending after midnight silently lost its end; short talks rendered with no title at all; and the ruling drew a line through every full-width band. This rebuilds the sheet.Why
Six things, each with its own commit:
parseLocalMinutesreturns minutes-from-midnight, so 00:00 came back as0— not after an 18:30 start — andplacementfell through to its 30-minute fallback. The last event of the day rendered as half an hour.Behavior
Time math.
placementcarries the day difference across midnight andformatMinuteswraps, so the afterparty reads18:30–00:00and spans its real length.dayRangefollows.Layout.
rowScale()maps minutes to rows: proportional through the talks, and every non-talk span — a break, a lunch, dead time nobody is on — compressed to two rows, so every stop is one strip tall whatever it runs.nonTalkSpans()finds those spans, keeps adjacent ones separate (a run of breaks is a strip each) and leaves alone any band a room is running a talk through. Rows areminmax(1.625rem, auto): the sheet is to scale at the minimum and a row grows to whatever its talk needs, with every column sharing the rows so the tracks stay aligned. An hour tick inside a compressed strip is skipped — the sheet is not to scale there.The sheet. A talk is an opaque
--panel-2panel with a--rulehairline box; the rooms are parted by a column gap and one hairline between them; dead time and the day's stops are hatched strips, opaque so nothing rules through them; the hour line is--rule-softdashed. The header row is filled so it stays legible while the sheet scrolls under it.The cell. One stack for every talk — time, full title, topic, speakers. The title has no line clamp: a truncated talk title is the one thing a timetable must not do. Every title takes the same step on the ramp.
Net: the whole day is 2006px instead of 3514, and nothing is cut.
Verification
/api/lineuppayload at 1025 / 1440 / 1920 px and at a 32px root (200% text-only zoom): no horizontal scroll, no cell overflowing its slot, no clamped or titleless cell, every band 52px, one title size.npm run buildandnpm run a11ypass (/agenda/desktop and mobile-list passes included).Files
src/lib/agenda.ts— midnight-crossingplacement, wrappingformatMinutes,placements,idleSpans,nonTalkSpans,rowScalesrc/components/Agenda.tsx— the grid drives off the row scale; one cell shape; rem rowssrc/components/Agenda.module.scss— panels, hatched strips, the sheet's ruling🤖 Generated with Claude Code