Travel: year scrubber, touch/keyboard globe rotation, theme-aware colors - #6
Merged
Conversation
…globe Turns 52 static countries into a story: a year scrubber drives the continent bars, heading, and globe together, with a Play button that animates through 1986-2024. The globe now rotates via pointer events (mouse, touch, and pen) instead of mouse-only handlers, and arrow keys rotate it when focused, so phones and keyboard users can use it at all. Map colors and the tooltip now come from theme CSS tokens instead of hardcoded hex/Tailwind classes, so Oxblood and High Contrast no longer show a gold globe. The world atlas is now served from public/data/world-110m.json instead of fetched from a CDN at runtime, so a CDN hiccup can no longer blank the globe. buildContinentBars moves into lib/content/travel.ts (with countriesUpTo and yearBounds alongside it) so app/page.tsx stops precomputing it and Travel.tsx can recompute it per scrubbed year.
Two CRITICAL bugs, both reproduced live and verified fixed with
Playwright before this commit:
- Countries flashed solid black on every drag start. Moving fill out of
the plain SVG attribute into react-simple-maps' style map covered
default/hover but not pressed, which has no fill of its own and falls
back to the SVG initial value.
- Clicking Play then Stop left the section frozen at "0 countries" with
the year label desynced from the slider thumb. Restarting via
`setYear(min - 1)` sat outside the range input's own bounds, which the
browser silently clamps to `min` — and Stop could also trigger that
same restart branch if clicked in the 260ms window right as autoplay
finished on its own. Replaced with a pure nextPlayState(state, min,
max) helper (unit tested) so Stop only ever stops and restarting
always lands exactly on `min`.
HIGH, also verified live:
- Arrow-key vertical rotation was inverted relative to drag — ArrowUp
tilted opposite to dragging up.
- touch-action: none made the globe an un-scrollable dead zone on
phones; switched to pan-y so vertical swipes still scroll the page
and only non-vertical drags rotate.
- A touch tap left the tooltip stuck in the screen corner with nothing
to dismiss it (no hover on touch). Now seeded from the pointerdown
position and dismissed on pointerup for any non-mouse pointer type.
MEDIUM, fixed where practical:
- react-simple-maps hardcodes tabIndex=0 on every one of 177+ country
paths, turning Tab into a per-country trap after focusing the globe.
Set tabIndex={-1} on each Geography; the wrapping group is the one
keyboard target (role changed from img to group to match).
- aria-live="polite" on the year readout announced all 39 years during
a Play run; silenced while playing.
- The caption below the map still showed all-time totals while the
heading above tracks the scrubbed year — relabeled "All-time:" so the
two numbers don't read as contradicting each other.
- Play button's glyph is now aria-hidden with aria-pressed on the
button itself, instead of announcing as e.g. "black right-pointing
triangle Play".
- Added a LICENSE file for the vendored world-atlas topojson (ISC,
Mike Bostock) noting its source and how it was fetched.
Two of the review's five failure repros were only reproducible because
my own verification script's Playwright coordinates landed off-screen
(scrollIntoViewIfNeeded on the huge #travel section doesn't bring the
1150px-tall map itself into the viewport) — confirmed via
elementFromPoint returning null at those coordinates, not an app bug.
Fixed the test methodology and re-verified all five repros directly
against real pointer/touch/keyboard interaction.
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
Implements Plan 09 (
docs/plans/09-travel-scrubber-and-touch.md): turns the 52 static countries into a story you can scrub through, and makes the globe actually usable on a phone or via keyboard.public/data/world-110m.json(with a LICENSE file) instead of fetched from a CDN at runtime.buildContinentBars/countriesUpTo/yearBounds/nextPlayStatemove intolib/content/travel.tsas pure, unit-tested transforms.Code review fixes (second commit)
An automated review caught two CRITICAL bugs before this was pushed, both reproduced live with Playwright and confirmed fixed:
pressedstyle state lost its fill).setYearvalue the browser silently clamped).Also fixed: inverted arrow-key vertical rotation, a
touch-action: nonescroll trap on phones, a touch-tap tooltip that never dismissed, a 177-stop keyboard tab-trap through every country path,aria-liveannouncement spam during Play, and a caption that contradicted the scrubbed heading.Test plan
npx tsc --noEmitcleannpm test -- --run— 38 unit tests pass (addedtravel-transforms.test.ts:buildContinentBars,countriesUpTo,yearBounds,nextPlayState)npm run lintclean (no new errors; two pre-existing lint errors inTravelMap.tsxfixed as a side effect)npx playwright test— 27 e2e tests pass, including 8 ine2e/travel.spec.ts(scrubber, keyboard rotation, flight toggle, visited-country shading)npm run buildsucceeds🤖 Generated with Claude Code
https://claude.ai/code/session_01TUH1L7Q2FfGYa8dS7Lqvtz