Skip to content

Travel: year scrubber, touch/keyboard globe rotation, theme-aware colors - #6

Merged
dommango merged 2 commits into
mainfrom
feat/travel-scrubber-touch
Sep 1, 2026
Merged

Travel: year scrubber, touch/keyboard globe rotation, theme-aware colors#6
dommango merged 2 commits into
mainfrom
feat/travel-scrubber-touch

Conversation

@dommango

@dommango dommango commented Sep 1, 2026

Copy link
Copy Markdown
Owner

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.

  • Year scrubber: a range slider (1986–2024) drives the continent bars, heading, and globe together, plus a Play button that auto-advances through the years.
  • Touch + keyboard rotation: the globe now drags via pointer events (mouse, touch, pen) instead of mouse-only handlers, and rotates with arrow keys when focused.
  • Theme-aware globe: map colors and the tooltip now come from CSS custom properties instead of hardcoded hex/Tailwind, so Oxblood and High Contrast no longer show a gold globe underneath.
  • Local atlas: the world topojson is now served from public/data/world-110m.json (with a LICENSE file) instead of fetched from a CDN at runtime.
  • buildContinentBars/countriesUpTo/yearBounds/nextPlayState move into lib/content/travel.ts as 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:

  • Countries flashed solid black on every drag start (the pressed style state lost its fill).
  • Clicking Play then Stop could strand the section at "0 countries" with the year label desynced from the slider (an out-of-range setYear value the browser silently clamped).

Also fixed: inverted arrow-key vertical rotation, a touch-action: none scroll trap on phones, a touch-tap tooltip that never dismissed, a 177-stop keyboard tab-trap through every country path, aria-live announcement spam during Play, and a caption that contradicted the scrubbed heading.

Test plan

  • npx tsc --noEmit clean
  • npm test -- --run — 38 unit tests pass (added travel-transforms.test.ts: buildContinentBars, countriesUpTo, yearBounds, nextPlayState)
  • npm run lint clean (no new errors; two pre-existing lint errors in TravelMap.tsx fixed as a side effect)
  • npx playwright test — 27 e2e tests pass, including 8 in e2e/travel.spec.ts (scrubber, keyboard rotation, flight toggle, visited-country shading)
  • npm run build succeeds
  • Manually verified all three themes (Gold/Oxblood/High Contrast) render correct globe colors
  • Manually reproduced and confirmed the fix for both CRITICAL bugs and all HIGH issues via direct pointer/touch/keyboard interaction (not just code review)

🤖 Generated with Claude Code

https://claude.ai/code/session_01TUH1L7Q2FfGYa8dS7Lqvtz

…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.
@dommango
dommango merged commit 675886f into main Sep 1, 2026
1 check passed
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