Skip to content

fix(agenda): make the desktop timetable readable - #338

Merged
ryzizub merged 6 commits into
2026from
claude/devfest-agenda-pc-display-4390df
Sep 7, 2026
Merged

fix(agenda): make the desktop timetable readable#338
ryzizub merged 6 commits into
2026from
claude/devfest-agenda-pc-display-4390df

Conversation

@ryzizub

@ryzizub ryzizub commented Sep 7, 2026

Copy link
Copy Markdown
Member

Summary

/agenda on 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:

  1. The sheet ran to 00:00. The grid is proportional and the afterparty is 18:30–00:00, so five and a half hours of empty hatch sat under the last talk, with the band's label floating two hours below the hairline it belongs to.
  2. A session ending after midnight lost its end. parseLocalMinutes returns minutes-from-midnight, so 00:00 came back as 0 — not after an 18:30 start — and placement fell through to its 30-minute fallback. The last event of the day rendered as half an hour.
  3. The ruling fought the content. A vertical hairline per room ran the full height, cutting through every band; a band's bottom rule sat 1px above the next cell's top rule, doubling every boundary; an empty stretch was ruled as heavily as an hour of content, so free time read as scheduled.
  4. Short talks had no title. The cell is a flex column and the title was the only thing that could give, so in a 20-minute cell (104px against 164px of content) it shrank to 3px: eight of sixteen talks showed a time and a speaker and nothing else.
  5. Rows did not survive a text-only zoom. Sized in px, at a 32px root eight cells overflowed their slot.
  6. Every stop was drawn to scale, so the afterparty 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.

Behavior

Time math. placement carries the day difference across midnight and formatMinutes wraps, so the afterparty reads 18:30–00:00 and spans its real length. dayRange follows.

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 are minmax(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-2 panel with a --rule hairline 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-soft dashed. 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

  • Rendered against the live /api/lineup payload 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.
  • Mobile list checked at 320 / 375 / 390 / 430 / 768 / 1024 px and at a 32px root: list layout at every width, 27 rows, no page scroll, tap targets ≥44px, full titles, the detail sheet opens.
  • npm run build and npm run a11y pass (/agenda/ desktop and mobile-list passes included).

Files

  • src/lib/agenda.ts — midnight-crossing placement, wrapping formatMinutes, placements, idleSpans, nonTalkSpans, rowScale
  • src/components/Agenda.tsx — the grid drives off the row scale; one cell shape; rem rows
  • src/components/Agenda.module.scss — panels, hatched strips, the sheet's ruling

🤖 Generated with Claude Code

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
ryzizub force-pushed the claude/devfest-agenda-pc-display-4390df branch from c61a4f7 to 524bebc Compare September 7, 2026 21:09
ryzizub and others added 5 commits September 7, 2026 23:18
…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 ryzizub changed the title fix(agenda): stop the desktop sheet at the end of the day fix(agenda): make the desktop timetable readable Sep 7, 2026
@ryzizub
ryzizub merged commit 9ec9f00 into 2026 Sep 7, 2026
2 checks passed
@ryzizub
ryzizub deleted the claude/devfest-agenda-pc-display-4390df branch September 7, 2026 22:20
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>
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