Skip to content

feat: redesign home hero with spotlight map - #95

Merged
ronload merged 3 commits into
mainfrom
feat/hero-spotlight-map
Oct 25, 2026
Merged

ronload merged 3 commits into
mainfrom
feat/hero-spotlight-map

Conversation

@ronload

@ronload ronload commented Oct 25, 2026

Copy link
Copy Markdown
Owner

Why

The home page was a tool with a map beside it. On desktop the brand, GitHub link and theme toggle sat in a card above the form (mobile had a separate header), and the map stayed a static overview of Taiwan until a city was picked. It worked, but it did not read as a landing page, and the map had nothing to show before the first input.

This turns the page into a split hero in the style of ui.spectrumhq.in: headline and tool on the left, an animated map on the right, faded where the two meet. The form still comes right after the headline, so a result is as close as before.

Changes

All changes are in apps/web; packages/core and the vendored components/ui/* are untouched.

Hero layout and header (25f470f)

  • Left column: a corner-bracket badge linking the Chunghwa Post data source, the headline, then the form and result. They enter with a short staggered fade-up (enter-fx), reduced to a plain fade under prefers-reduced-motion.
  • The map panel (still lg and up) is no longer a grid cell. It spans from 42% of the container to the viewport's right edge and sticks under the header, sized to the viewport so the camera only frames what is on screen.
  • The canvas fades into the page through a CSS mask (map-fade) rather than background-colored overlays, which hid the page glow behind the map and left a visible seam. Only the canvas is masked; the focus label and zoom controls stay crisp.
  • One header at every breakpoint (logo and wordmark linking home, GitHub, theme toggle) replaces the desktop-only card.
  • New BracketBadge and SectionLabel components, and a --mark accent color for the corner marks.

Spotlight map (5d08445)

  • Everything outside the focus is dimmed. With nothing selected, the map tours the counties on its own. The tour pauses while the pointer or keyboard focus is on the map, or while the page is hidden, and resumes about 2s after that ends.
  • Selecting a city or district moves the spotlight there, flies the camera to it (max zoom 11 for counties, 13 for districts) and labels it with a corner mark.
  • The dimming is feature state on a single GeoJSON source: a world polygon with Taiwan cut out, plus every county and district. Each feature tweens between fully clear and fully opaque (FeatureStateTweener) under one layer opacity, so overlapping county and district masks never stack and shared borders leave no seams.
  • Framing ignores islets under 1% of a region's largest landmass, so the Wuqiu islets no longer pull the camera from Kinmen out to sea.
  • The canvas stays hidden until the spotlight is drawn, on first load and while a theme switch restyles the basemap, so neither the previous basemap nor an unmasked map flashes. A 2s timeout reveals the map anyway if the spotlight never draws.
  • Under reduced motion there is no tour, and the camera jumps instead of flying.
  • The topology loaders share a promise cache, reject non-OK responses and evict failed requests so a later call can retry. GeoJSON conversion moved into src/lib/map-spotlight.ts.

Basemap paint transitions (86d2241)

  • scripts/process-map-styles.ts sets transition: { duration: 0, delay: 0 } on the neutralized styles, and both generated styles in public/data/styles/ are updated to match. MapLibre's default 300ms paint transition was still running when the canvas faded back in after a theme switch, which showed as a gray haze over the new theme.

Verification

  • pnpm check (core build, workspace typecheck, ESLint, Prettier) passes; 29 unit tests pass.
  • next build passes, 379 static pages generated.
  • Each commit typechecks and lints on its own.
  • Checked by hand at 1440x900 in light and dark, and at 390x844:
    • the tour pauses on hover and resumes about 2s after the pointer leaves; keyboard focus pauses it and keeps it paused while focus moves to the zoom buttons; focusing the map with a click does not leave it paused;
    • reduced motion turns the tour off and makes camera moves instant;
    • theme toggles, rapid repeats included, probed frame by frame: once the map has loaded, no visible frame shows the previous basemap or a map without its spotlight;
    • Kinmen is framed without Wuqiu; 大安區 and 鹽埕區 (one of the smallest districts) stay readable at the district zoom cap.
  • apps/web has no browser tests, so none of the map behavior above is covered in CI.

Not done

  • Label placement. The focus label anchors at the north-west corner of the region's bounds. For regions that run diagonally, such as 高雄市, that corner sits far from the land. Anchoring to a point inside the polygon would fix it.
  • Theme toggle during the first style load. Toggling the theme before the basemap has loaded for the first time can, rarely, flash the map before its spotlight is drawn.
  • Background glow banding. Faint banding in the page's background glow comes from the existing body::after radial gradient, which this PR does not change.

Put the text block and the tool on the left and let the map fill the
right, so the page reads as a landing hero while the form stays one
glance away.

The header now holds the brand, GitHub link and theme toggle at every
breakpoint, replacing the card that only showed on desktop. The map
panel bleeds to the viewport's right edge and sticks under the header,
so the camera only frames what is on screen. Its canvas fades out
through a CSS mask rather than background-colored overlays, which cut
off the page glow behind it and left a visible seam.

Corner marks in an accent color, mono section labels and a staggered
entrance animation carry the new style; under reduced motion the
entrance becomes a short fade.
Give the hero an animated subject: the map dims everything but its
focus. With no city selected it tours the counties on its own, pausing
while the pointer or keyboard focus is on the map and while the page is
hidden. Selecting a city or district moves the spotlight there, flies
the camera to it and labels it with a corner mark.

Dimming runs on feature state in a single source, with every feature
settling fully clear or fully opaque under one layer opacity, so
overlapping county and town masks never stack and shared borders leave
no seams. Islets under 1% of a region's largest landmass are left out
of its framing, so Kinmen's far-off Wuqiu islets do not pull the camera
out to sea.

The canvas stays hidden until the spotlight is drawn, on first load and
while a theme switch restyles the basemap, so neither the old basemap
nor an unmasked map flashes. Under reduced motion the tour is off and
the camera jumps instead of flying.

The topology loaders now share a promise cache, reject non-OK responses
and evict failed requests so a later call can retry.
Theme switches swap the basemap style behind a hidden canvas. The
default 300ms paint transition was still running when the canvas faded
back in, which showed as a gray haze over the new theme.

The generated styles are updated to match the script.
@ronload ronload added the enhancement New feature or request label Oct 25, 2026
@vercel

vercel Bot commented Oct 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
zipkit Ready Ready Preview Oct 25, 2026 12:27pm UTC

@ronload ronload self-assigned this Oct 25, 2026
@ronload
ronload merged commit 45984a3 into main Oct 25, 2026
4 checks passed
@ronload
ronload deleted the feat/hero-spotlight-map branch October 25, 2026 14:28

This branch was successfully deployed

1 active deployment
Preview — 86d2241c Deployed Oct 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant