Commit Graph

6 Commits

Author SHA1 Message Date
julian bc54f1e811 feat: task 2.2 tile-source switcher
- 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.
2026-05-03 09:29:14 +02:00
julian 87a738313e feat: task 2.1 MapView singleton + mapReady gate
- 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.
2026-05-03 09:28:38 +02:00
julian c833d6f3dd fix(routing): redirect anonymous users from protected routes + devtools rename
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.
2026-05-02 20:00:41 +02:00
julian 79e50289fe fix(ci): generate route tree before typecheck + enforce LF line endings
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.
2026-05-02 18:54:59 +02:00
julian 26e059fc20 feat: planning structure + task 1.2 stack rounding-out
Add .planning/ scaffolding:
- ROADMAP.md (4 phases, 8 non-negotiable design rules)
- phase-1-foundation/ README + 9 task files (1.2-1.10)
- phase-2-live-map / phase-3-dogfood-readiness / phase-4-future README placeholders

Task 1.2 — stack rounding-out:
- Tailwind 4 via @tailwindcss/vite + src/styles/globals.css
- shadcn/ui (slate, new-york) primitives in src/ui/primitives/:
  button, input, label, form, card, alert
- TanStack Router 1.169 + Query 5.100 (devtools + plugin in devDeps)
- Zustand 5, @directus/sdk 21, zod 4, react-hook-form 7 + resolvers
- Prettier 3 + eslint-config-prettier + eslint-plugin-prettier
- ESLint override disabling react-refresh/only-export-components for
  src/ui/primitives/** (intentional dual-exports in shadcn primitives)
- Path alias @/* -> ./src/* in tsconfig.json + tsconfig.app.json
  (TS 6 deprecates baseUrl; paths now resolve relative to config file).
  Pulled forward from 1.3 because shadcn add CLI needs it resolvable.
- Scripts: dev, build, preview, lint, typecheck, format, format:check,
  test (placeholder)
- App.tsx Tailwind smoke test (centred card + shadcn Button)
- README.md rewritten with stack/scripts/shadcn-add docs

All four gates green: typecheck, lint, format:check, build (222KB / 70KB gz).
2026-05-02 18:41:54 +02:00
julian e76c70bfe4 Init 2026-05-02 16:53:12 +02:00