feat(timeline): add overview mini-map#203
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
4cfe485 to
ff8ecf0
Compare
Adds a collapsible "Show overview" toggle in the timeline toolbar revealing a proportional mini-map of the filtered edition, with vote-colored set density and a draggable viewport window. Clicking the map jumps the strip (smooth scroll); dragging the viewport window scrubs it directly. Rebased onto main, which now already contains the day-jump-toolbar and scroll-url-state work this branch was previously stacked on.
97f8123 to
06d3581
Compare
There was a problem hiding this comment.
Pull request overview
Adds a collapsible “Show overview” mini-map to the horizontal schedule timeline, enabling proportional edition-wide navigation (click-to-jump) and scrubbing (drag viewport) while keeping navigation state in the existing scrollTo URL sync.
Changes:
- Add an overview mini-map UI (per-stage density rows, day-boundary lines, draggable viewport window) gated by a toolbar toggle.
- Centralize “jump to moment” behavior in
useTimelineScrollSync().jumpTo, used by both day-jump buttons and the overview map. - Add/adjust unit + e2e coverage for overview geometry and interactions; refactor day label helpers.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/e2e/timeline-overview.spec.ts | Adds e2e coverage for overview toggle/click/drag/day-filter behavior. |
| tests/e2e/timeline-day-toolbar.spec.ts | Updates day-jump toolbar e2e assertions to match new DOM/roles. |
| src/test/setup.ts | Test environment setup adjustments (timezone pin removed in diff). |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineToolbar.tsx | Adds “Show overview” toggle button to toolbar. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineOverview.tsx | Implements overview mini-map rendering + click-to-jump. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineContainer.tsx | Wires overview expansion state + integrates jumpTo. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/Timeline.tsx | Rehomes StageLabels to keep it pinned outside the scroller. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/StageLabels.tsx | Adjusts pinned label positioning within the new container layout. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/OverviewViewportWindow.tsx | Adds draggable viewport window for scrubbing the timeline. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/OverviewStageRow.tsx | Renders per-stage density blocks with vote-based coloring. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/DayJumpButtons.tsx | Simplifies day buttons and updates labels to short weekday+date. |
| src/main.tsx | Removes router-level defaultStructuralSharing setting. |
| src/lib/timeUtils.ts | Replaces day-label parts helper with getFestivalDayShortLabel. |
| src/lib/timeUtils.test.ts | Updates tests for new short label helper and removes adjacency tests. |
| src/lib/timelineOverviewGeometry.ts | Adds shared geometry helpers for overview proportions and viewport. |
| src/lib/timelineOverviewGeometry.test.ts | Adds unit tests for overview geometry calculations. |
| src/lib/timelineDayJump.ts | Simplifies day-jump moment selection to “earliest set start”. |
| src/lib/timelineDayJump.test.ts | Updates tests to avoid removed fixtures and match new logic. |
| src/lib/timelineCalculator.test.ts | Removes shared fixtures and inlines test data builders. |
| src/hooks/useTimelineUrlState.ts | Enables structural sharing to reduce recomputation on scrollTo writes. |
| src/hooks/useTimelineScrollSync.ts | Adds jumpTo API and enables structural sharing in useSearch. |
| src/hooks/useActiveTimelineDay.ts | Removes active-day tracking hook (no longer used). |
| src/tests/fixtures.ts | Removes generic schedule/stage fixture helpers. |
| CLAUDE.md | Updates agent workflow instruction wording. |
- Restore the TZ=UTC test pin dropped during the rebase; the UTC-fallback test in timeUtils depended on it and was silently passing only because this sandbox's host clock is already UTC. - Replace the racy immediate isVisible() check in the day-toolbar and overview e2e specs with a waitFor-based skip, so a still-loading schedule doesn't get misread as "not revealed" and skipped green. - Add aria-expanded to the overview toggle button. - Throttle the overview's scroll/resize sync to one state update per animation frame instead of one per native scroll event. - Handle pointercancel the same as pointerup when dragging the viewport window, so an interrupted gesture can't leave window listeners and drag state stuck. - Give the viewport window real slider keyboard semantics (tabIndex, aria-valuemin/max, Arrow/Home/End) instead of a role="slider" that wasn't actually operable via keyboard.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 24 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (3)
src/test/setup.ts:3
- Removing the TZ pin makes date-fns fallbacks depend on the machine timezone (e.g. getFestivalDayKey() uses format(date) when no timezone is passed, and timeUtils.test asserts a UTC fallback). This will make unit tests fail or become non-deterministic on non-UTC machines.
import "@testing-library/jest-dom/vitest";
import { vi } from "vitest";
tests/e2e/timeline-day-toolbar.spec.ts:23
- This locator matches the new "Show/Hide overview" toggle as well as the day-jump buttons, so label assertions/clicks will be wrong (e.g. "Show overview" doesn't match the weekday/date regex). Scope the locator to the day-jump buttons’ data-testid prefix instead.
test.describe("Timeline day-jump toolbar", () => {
test("renders one sticky button per festival day, labeled weekday + date", async ({
page,
}) => {
src/pages/EditionView/tabs/ScheduleTab/horizontal/OverviewViewportWindow.tsx:46
- The viewport window declares
role="slider"but isn’t focusable and has no keyboard interactions, which makes it inaccessible (and the ARIA semantics incorrect). Either remove the slider role or make it a real slider by adding tabIndex, aria-valuemin/max, and ArrowLeft/ArrowRight handling.
data-testid="timeline-overview-viewport"
role="slider"
tabIndex={0}
aria-label="Visible timeline range"
aria-valuemin={0}
The day-jump-toolbar feature (#202) was substantially reworked after this branch's own commits stopped: active-day tracking (useActiveTimelineDay), radiogroup/radio a11y semantics, a "modal opening" day-jump target instead of the global-earliest set, a restyled weekday/date rail, a repositioned StageLabels column, and global router structuralSharing. This branch's diff-based rebase preserved this branch's own (now-stale) versions of those files, which would have regressed all of the above on merge. Restore every file this PR doesn't need to touch to main's exact content, and re-wire only the overview mini-map on top of it: - TimelineToolbar now nests the day radiogroup and appends the overview toggle as a sibling, instead of replacing the toolbar's semantics. - TimelineContainer keeps main's useActiveTimelineDay + StageLabels wiring and drives both the day buttons (start-aligned) and the overview map (center-aligned) through the same jumpToTimelineMoment call. - Dropped the now-unnecessary useTimelineScrollSync.jumpTo/structuralSharing additions and the standalone getFestivalDayShortLabel helper. - e2e specs for both the day toolbar and the overview map now match main's "fail loudly instead of silently skipping" convention.
- Pin the overview toggle outside the day rail's overflow-x-auto scroller (only the radiogroup scrolls now) so it stays visible on festivals with enough days to overflow the toolbar, instead of scrolling out of reach. - Restyle it as a ghost button with a divider, icon-first and collapsing to icon-only below sm, matching the rail's minimal look instead of standing out as an outlined pill. - Cap the mini-map's height with vertical scroll (max-h-36 on mobile, max-h-48 from sm) so festivals with many stages don't blow up the map's footprint, especially on small screens.
Revert the max-height/overflow-y-auto added for mobile sizing; the map should always render in full rather than clip and scroll.
Cap the map at a fixed 64px height with per-stage row height derived from stage count, instead of stacked h-2 rows that grew unbounded with more stages (the actual cause of the oversized mobile map). Draw each day's weekday/day-of-month label inside its boundary line, matching the compact look from the #121 prototype.
Match the #121 prototype: unvoted sets in the mini-map now use each stage's own color at low opacity instead of a flat neutral tone, so the map reads as colorful before any voting happens. Voted sets still override with the vote color at full opacity.
The day-jump row can overflow on mobile with no indication there's more to scroll to, and the active day could end up scrolled out of view entirely. Auto-scroll the active day pill into view (centered) whenever it changes, and mask-fade whichever edge still has more days to scroll toward.
Address remaining Copilot review comment on PR #203: the outer div grouping the day radiogroup and overview toggle had no role/label of its own, so assistive tech couldn't discover it as a unit.
…iant The existing tests check each geometry function in isolation with hand-picked numbers that already assume they agree on one offset<->percent scale. Add two tests that compose the functions instead: a set starting exactly at a day boundary must land at the same leftPercent as that boundary, and a clicked fraction must round-trip through offset/time conversions back to the same percent.
calculateOverviewSetBlocks, calculateDayBoundaries, calculateOverviewViewport, and fractionToOffset now take a single named-fields object instead of positional args, so call sites read without needing to check the function signature (especially calculateDayBoundaries, which had four same-shaped positional params). offsetToPercent stays positional — it's a private two-arg helper only called internally within this module.
The overview viewport window already has role="slider" + aria-label="Visible timeline range", which uniquely identifies it - the data-testid was redundant. Point the e2e drag test at the a11y role/name instead.
Both TimelineOverview and TimelineToolbar buried a rAF-throttled scroll/resize-tracking useEffect in their component body, making it hard to tell what the component does without reading the whole effect. Pull each into its own hook named for what it returns: useTimelineViewportSize (the overview's viewport window) and useScrollEdgeFade (the day-row's overflow fade) - so the component body reads as a short list of "what this does" instead of inline implementation.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (5)
src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineOverview.tsx:137
- If
rowHeightis computed as a fractional/very small value (e.g. on festivals with many stages),height={rowHeight - 1}can become 0 or negative, which results in invalid layout (and rows disappearing). UserowHeightdirectly (or clamp) for the row height.
src/pages/EditionView/tabs/ScheduleTab/horizontal/OverviewViewportWindow.tsx:1 - This component adds global
windowpointer event listeners but doesn't guarantee cleanup if the overview unmounts mid-drag (e.g. user collapses the overview while dragging). That can leave stale listeners running and mutatingscrollLeftafter unmount. Consider using an AbortController (and aborting on unmount) to ensure listeners are always cleaned up.
import { useRef } from "react";
src/pages/EditionView/tabs/ScheduleTab/horizontal/OverviewViewportWindow.tsx:38
- Track an AbortController for the active drag and abort it on unmount to ensure any in-flight global listeners are removed even if the component is collapsed/unmounted mid-drag.
const dragStateRef = useRef<DragState | null>(null);
src/pages/EditionView/tabs/ScheduleTab/horizontal/OverviewViewportWindow.tsx:66
- Use the per-drag AbortController signal when registering global listeners so they can be torn down reliably (via abort on pointer-up/cancel or unmount).
dragStateRef.current = {
startClientX: event.clientX,
startScrollLeft: container.scrollLeft,
mapWidthPx: map.getBoundingClientRect().width,
};
src/pages/EditionView/tabs/ScheduleTab/horizontal/OverviewViewportWindow.tsx:94
handlePointerUpcan rely on aborting the active drag's AbortController to remove all listeners that were registered with itssignal(and clear refs). This avoids leaving listeners attached if the drag ends via cancel/unmount.
dragStateRef.current = null;
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
window.removeEventListener("pointercancel", handlePointerUp);
}
useTimelineViewportSize and useScrollEdgeFade are only used by TimelineOverview and TimelineToolbar respectively - move them out of the general src/hooks/ folder and into the same directory as their components.
Per the CSS overflow spec, setting overflow-x to a value other than visible while overflow-y stays visible forces overflow-y to compute as auto too - so any sub-pixel vertical overflow in the day-jump row (focus rings, the active-day underline) triggered a real vertical scrollbar right at its edge, next to the overview toggle. Pair overflow-x-auto with overflow-y-hidden, same as the main timeline scroll container already does.
Math.max(3, ...) kept rowHeight pinned at 3px once a festival had 15+ stages, so 3px * stageCount exceeded the map's available height and the last few stage rows were pushed past the overflow-hidden bound - silently invisible rather than just thinner. Compute rowHeight as a plain fraction of the available height so every stage always fits, even if rows get sub-pixel thin at high stage counts.
Adds a collapsible "Show overview" toggle in the timeline toolbar revealing a proportional mini-map of the filtered edition, with vote-colored set density and a draggable viewport window.
Clicking the map jumps the strip (smooth scroll); dragging the viewport window scrubs it directly.
Closes #197
Verification
scrollToupdates in the URL.scrollTowrite.?day=2025-07-12); mini-map shows only that day's sets and density.Generated by Claude Code