Repository navigation
update route selection screen #505
Description
Update the route selection screen
The route selection screen on the next/svelte-webgpu rebuild branch (epic #542) is the SvelteKit Routes route at src/routes/search/+page.svelte (App.tsx's old currentView === 'route-search' view). Its catalogue state lives in the shared RoutesStore (src/lib/ui/routesStore.svelte.ts, reached through src/lib/ui/routesContext.ts); services are provided via Svelte context (getServices() / getRoutesStore() / getAuth()), never via a concrete service class. The React App.tsx + three.js app this screen once lived in is being deleted (PR #587), so there is no React path left to keep behind a flag.
Status on the branch: the map-primary redesign described below is already substantially built in Svelte. src/routes/search/+page.svelte already renders a RouteOverviewMap (src/lib/ui/RouteOverviewMap.svelte) above a slide-in drawer (data-drawer-open, routes-drawer-toggle), a RouteDetailOverlay (src/lib/ui/RouteDetailOverlay.svelte), the difficulty filter, the rownative import (RownativeRouteImport.svelte), the file import, per-card RouteThumbnail previews (src/lib/ui/RouteThumbnail.svelte, with showBackdrop), and the default-route badge/toggle, with dimmedRouteIds wired from the store. This issue is therefore a verification + refinement pass over that screen rather than a build-from-scratch: confirm every FR/AC holds on the Svelte screen, close any gaps, and lock the DOM contracts the Routes Playwright specs rely on.
The attached Biketerra screenshots show the pattern this issue targets: a world map with route pins at approximate geographic locations, a card list that cross-references the pins, and a detail view that shows each route's outline against a basemap (and labels its region in context).
This issue upgrades the Routes screen (src/routes/search/+page.svelte + its src/lib/ui/Route* components) to that shape, reusing #493's OSM enrichment data where available (water/island/coastline geometry from src/lib/services/routeEnrichmentService.ts) and leaving the Row screen (src/routes/row/+page.svelte) alone.
Functional requirements
- FR1. The Routes screen renders a MapLibre GL overview map (map-primary layout) via
RouteOverviewMap.svelte, with the route list in a slide-in drawer (.route-search-drawer, toggled byroutes.routesDrawerOpen). The map shows one pin per route in the current catalogue (routes.routes), positioned at the route's approximate geographic location. - FR2. Hovering a pin highlights the matching list row; clicking a pin opens the route's detail panel (
RouteDetailOverlay.svelte, driven byroutes.routeDetailOpen) — route selection happens here via itsonStartRow. - FR3. Hovering a list row highlights the matching pin on the map.
- FR4. The map clusters co-located pins; clicking a cluster zooms in until its children separate (world-view clustering, D4), inside
RouteOverviewMap.svelte. - FR5. Each route card in the drawer shows a route-outline preview with geographic context —
RouteThumbnail.sveltewithshowBackdropdraws the polyline over OSM raster tiles per card (D3), falling back to the bare-polyline SVG when offline or tiles fail to load. - FR6. The map and the list respond to the existing difficulty filter (
routes.difficultyFilter): filtered-out rows are hidden (routes.filteredRoutes); filtered-out pins are dimmed on the map viaroutes.dimmedRouteIds(D7). - FR7. Clicking a pin or a drawer row opens the detail overlay (
RouteDetailOverlay.svelte, D8) with: name, location, distance, estimated time, difficulty, the geometry-provenance badge (geometryProvenanceBadgefromsrc/lib/utils/geometryProvenance.ts), a large route outline drawn overroute.enrichmentOSM water/land geometry (D9), and a "Row this route" action that selects the route and navigates to the Row screen (routes.selectRoute(route)thengoto(base + '/')). - FR8. The user's default route (
routes.defaultRouteId) is marked on both the map pin and the drawer row (.route-default-badge), and the map auto-centres on it when the screen first opens (D10). - FR9. OpenStreetMap attribution ("© OpenStreetMap contributors") is visible on the overview map and on any card/detail outline that shows OSM-sourced tiles or geometry (D13).
- FR10. The rownative-import (
RownativeRouteImport.svelte) and file-import controls stay on the screen, inside the drawer, unchanged in behaviour (routes.importFile,routes.routeImported). - FR11. On viewports < 600 px, the map stays above the drawer at a reduced fixed height; the drawer is always open and inline, no slide-out (D11).
Non-functional requirements
- NFR1. OSM raster tiles are fetched from a configurable tile URL (default: standard OSM tile server) and cached in the browser's HTTP cache + a MapLibre tile source cache. First open fetches tiles; subsequent opens within the same session reuse them. An offline / tile-fetch-failure path renders the bare SVG polyline fallback on both the overview (pins over a neutral background) and the cards.
- NFR2. Visual-baseline coverage: the map region of the Routes screen is excluded from
test:visual(MapLibre + WebGL is not bit-deterministic under SwiftShader; the WebGPU ride canvas is excluded for the same Cascaded Shadow Maps for the sun #495 reason). The surrounding chrome (drawer, filters, attribution, modal) is visual-baselined viaassertCanvasNotBlank.ts-style DOM checks where a frozen frame is used. DOM-level Playwright assertions cover the map region (pin count, dimmed state, cluster count). - NFR3. New screen bundle weight ≤ 120 KB gzipped over the pre-redesign Routes screen, with MapLibre loaded via a dynamic
import()insideRouteOverviewMap.svelteso the Row screen and signed-out catalogue are unaffected. Measured byscripts/measure-bundle.mjs. - NFR4. Interaction latency ≤ 100 ms from pin click to detail-overlay open on a mid-tier device, measured in a
routes-screenPlaywright perf spec. - NFR5.
npm run verifystays green: lint,tsc, Vitest unit tests, Playwright E2E. - NFR6. Coverage ratchets up for the new/changed files (per CLAUDE.md), with per-file thresholds added to
vitest.config.ts. - NFR7. Because the React route-search markup has already been replaced by the Svelte screen on the branch, no feature flag is needed (D14). Work ships directly against
src/routes/search/+page.svelte;test:visualbaselines for the chrome are recorded via thevisual-baselinelabel flow (Linux/SwiftShader, never a local GPU). - NFR8. Clustering uses
supercluster(~5 KB gzipped) or an equivalent small library insideRouteOverviewMap.svelte; it must be covered by unit tests over cluster/decluster transitions as pins are filtered.
Acceptance criteria
- AC1. On the Routes screen, the overview map renders map-primary with a slide-in drawer; one
data-testid="route-pin"is rendered per visible route (ordata-testid="route-cluster"for a cluster). (route-overview-sectionis already present.) - AC2. Clicking a pin opens the detail overlay and dispatches the same selection path as the matching drawer row (both call
routes.selectRoute); asserted by a unit test on the store + a component test. - AC3. Setting the difficulty filter to "hard" hides every non-hard drawer row (
routes.filteredRoutes) and dims every non-hard pin (viaroutes.dimmedRouteIds→ adata-dimmed="true"attribute) in the same render. - AC4. Opening the Routes screen with a
defaultRouteIdset centres the map on that route's centroid, zooms to a sensible level, and marks its pin + its drawer row withdata-default="true"(the.route-default-badgeis already rendered). - AC5. When tile fetch fails (Playwright blocks the tile host), each card falls back to the
RouteThumbnailSVG polyline and the overview map renders pins over a neutral background with no uncaught errors (regression test). - AC6. With
route.enrichmentpresent, theRouteDetailOverlayoutline renders at least one OSM water polygon under the route polyline (component test onRouteDetailOverlay.svelte). - AC7. The "© OpenStreetMap contributors" attribution is present in the DOM on the overview map and in the detail overlay whenever OSM-sourced geometry or tiles are shown (unit/DOM grep).
- AC8.
npm run test:visualrecords baselines for the drawer, filters, attribution and detail chrome via thevisual-baselinelabel flow; the map tiles region is explicitly masked out in the baseline comparator. - AC9. (Replaces the former "flag off renders exactly as today's React path".) With the React path removed post-cutover, the SvelteKit Routes route renders the map-primary screen as its only state; a Playwright snapshot of
src/routes/search/+page.svelte's chrome stays green and the Row screen (src/routes/row/+page.svelte) is unaffected. There is no legacy state to fall back to. - AC10. A Playwright E2E spec clusters two routes at identical centroids and verifies clicking the cluster zooms in until the children separate.
- AC11.
npm run verifypasses end-to-end.
Open decisions — LOCKED
| # | Decision | Choice |
|---|---|---|
| D1 | Basemap source | b — OSM raster tiles via MapLibre |
| D2 | Pin location | b — centroid of all coordinates |
| D3 | Card thumbnail | b — OSM raster tiles behind polyline per card (RouteThumbnail showBackdrop) |
| D4 | Map viewport | b — clustered world view, zoom-on-click |
| D5 | Pin interaction | c — click opens detail overlay |
| D6 | Layout | c — map primary, list in a slide-in drawer |
| D7 | Filter coupling | c — list reduced, pins dimmed (dimmedRouteIds) |
| D8 | Detail view | c — overlay with "Row" button (RouteDetailOverlay.svelte) |
| D9 | Detail backdrop | b — polyline over #493 OSM geometry |
| D10 | Default route | c — badge + auto-centre on mount |
| D11 | Narrow viewport | a — map above drawer, shorter fixed height |
| D12 | Map testability | b — MapLibre + WebGL; map region excluded from visual baselines (DOM tests only) |
| D13 | OSM attribution | a — on overview map and detail backdrop |
| D14 | Rollout | Superseded — the React route-search markup is already replaced by the Svelte screen on next/svelte-webgpu; no flag, no follow-up removal step. Work ships directly against src/routes/search/+page.svelte. |
Activity
#505 Phase 1 — map-primary Routes overview behind __VIRTUALROW_ROUTE_…
#505 Phase 2 — filter dimming, modal detail overlay, cluster utility (#…
#505 Phase 2b — render clusters on the overview map (#510)
#505 Phase 2c — live MapLibre zoom tracking for clusters (#513)
#505 Phase 2d — per-card OSM tile backdrop (D3-b) (#524)
#505 Phase 2e — map-primary Routes screen with slide-in catalogue dra…
#505 Phase 3 — default the Routes drawer open on laptop and up (fix r…
#505 Phase 3 — free the Back to Row button from the pinned drawer on …
#505 Phase 3 — side-by-side drawer at laptop width, teach responsive …
#505 Phase 3 — keep the drawer toggle clickable while the overlay dra…
#505 Phase 3 — float the drawer toggle to bottom-left while the overl…
#505 Phase 3 — filter MapLibre worker/tile console noise in rownative…
#505 Phase 3 — open the Routes drawer before clicking Import a file i…
#505 Phase 3: flip __VIRTUALROW_ROUTE_MAP_V2 on by default (D14-c) (#533
#505 Phase 3 cleanup — remove the __VIRTUALROW_ROUTE_MAP_V2 opt-out a…
#549 P1: port the Routes screen to Svelte (#553)
[-]update route selection screen - placeholder[/-][+]update route selection screen[/+]