- pnpm add maplibre-google-maps (runtime; lazy-imported on Google use).
- src/map/core/styles.ts: BasemapDescriptor model with four entries
(Esri Satellite / OpenTopoMap / OSM / Google Satellite).
Google entry gated on cfg.googleMapsKey via available(cfg).
styleCustom() builds a single-raster-source MapLibre style with a
glyphs URL for future label rendering.
ensureGoogleProtocol() lazy-loads maplibre-google-maps + addProtocol
on first use; _googleRegistered guards re-registration.
- src/map/core/map-pref-store.ts: Zustand + persist middleware
(key: trm-map-prefs, version: 1). Holds basemapId + setBasemap.
- src/map/core/basemap-switcher.tsx: floating control top-right of
<MapView>. Filters by available(cfg), highlights active, applies via
getMap().setStyle(). Mount-time bootstrap (no UI state) + user-driven
onClick (with switching state) split to satisfy React 19's
react-hooks/set-state-in-effect rule.
- src/routes/_authed/monitor.tsx: renders <BasemapSwitcher /> as a
child of <MapView>.
- src/vite-env.d.ts: ambient module declaration for
maplibre-google-maps (no .d.ts ships in the package).
Deviations:
- BasemapDescriptor.style is buildStyle(cfg): StyleSpecification, not
the spec's StyleSpecification|string union. Google needs the cfg key
at build time; uniform function shape across all entries is cleaner.
- Bootstrap apply path doesn't use the switching UI state (no visual
feedback needed during the silent first-paint flip from OSM to user
preference). User-click path keeps setSwitching for the in-flight
indicator.
Bundle: MapLibre is now its own chunk (1MB / 273KB gz), shared across
maplibre consumers. Main bundle 371KB / 114KB gz.
- pnpm add maplibre-gl + -D @types/geojson.
- src/map/core/styles.ts: defaultStyle (OSM raster bootstrap; 2.2
replaces with the basemap-switcher descriptor table).
- src/map/core/map-view.tsx: module-level Map singleton lazily created
on first <MapView> mount, attached to a class="trm-map-host" detached
<div> that React refs append/remove on mount/unmount. Style-data
lifecycle flips mapReady false on every styledata event, polls
loaded() at 33ms intervals, flips ready true once the style is
loaded — the canonical MapLibre style-swap dance.
- Exports getMap()/getMapReady()/subscribeMapReady()/useMapReady (via
useSyncExternalStore for SSR-safe + concurrent-safe reads). getMap()
throws if called pre-mount; the explicit failure mode beats a
null-able top-level export.
- src/routes/_authed/monitor.tsx: new /monitor route, full-viewport
<MapView /> for 2.1 (no children — subsequent tasks plug in here).
- src/routes/_authed/index.tsx: home-page card now links to /monitor.
- eslint.config.js: override for src/map/** + src/live/** disables
react-refresh/only-export-components. Same pattern as the existing
overrides for shadcn primitives and route files.
Deviation: spec sketched a top-level `map` constant export; implemented
as `getMap(): MapLibreMap` (a function) so the singleton stays lazy
until <MapView> mounts. Top-level constant would either force eager
init (breaks SSR/tests) or be nullable (footgun). The function form
throws a clear error if called pre-mount.
Bundle: /monitor lazy chunk is 1MB raw / 274KB gzipped (MapLibre + CSS).
Other routes unaffected. Vite chunk-size warning is harmless.
Bug 1: hard-loading / while unauthenticated stayed stuck on "Loading..."
forever. Cause: the _authed layout short-circuits to the spinner when
status is anything other than 'authenticated', which means child routes
never mount. The redirect-on-anonymous useEffect lived only in the
home page (_authed/index.tsx), so it never fired — the layout's spinner
was the last thing rendered.
Fix: add a useEffect to the _authed layout component itself that
navigates to /login on the 'anonymous' transition. The layout's gate
is now: beforeLoad redirects on cold-known-anonymous, useEffect
redirects on post-mount transitions to anonymous (e.g. boot probe
resolves to anonymous after the route already rendered).
Bug 2: console warning that @tanstack/router-devtools moved to
@tanstack/react-router-devtools. Same package, renamed.
Fix: pnpm remove @tanstack/router-devtools && pnpm add -D
@tanstack/react-router-devtools; updated the lazy import in __root.tsx
to point at the new package name.
Plus: TRM_Design_System-handoff/ added to .prettierignore — those files
are immutable source material from claude.ai/design and shouldn't be
reformatted.
Two CI gaps surfaced on first push:
1. typecheck failed because tsc -b ran before vite build, but
src/routeTree.gen.ts is only generated by the Vite plugin during
build. tsc has nothing to typecheck against.
Fix: install @tanstack/router-cli and chain `tsr generate` before
tsc in the typecheck and build scripts. Now any environment that
runs typecheck cold (CI, fresh clone) generates the route tree
first.
Also added a top-level `route-tree` script so the same command is
reusable elsewhere if needed.
2. format:check would fail on Windows working trees because
git autocrlf (default on Windows) checks files out with CRLF, but
.prettierrc pins endOfLine: "lf". Locally the format:check
intermittently passed/failed depending on whether files had been
recently auto-formatted.
Fix: .gitattributes with `* text=auto eol=lf` enforces LF in every
working tree. Plus explicit overrides for binary blobs (images) and
the route tree file. `git add --renormalize .` brought the index in
line with the new policy; no actual file content changed.
CI on the next push should now see the same green gates the local
working tree shows.