feat: task 2.8 camera control trio + follow toggle

- src/map/core/map-pref-store.ts: mapFollow boolean (default true) +
  setter. Persisted via trm-map-prefs.
- src/map/core/camera/default-camera.tsx: <MapDefaultCamera /> fits
  the snapshot's bounds once per activeEventId change. Uses a
  fittedFor ref; resets to null when activeEventId becomes null.
  Single-point events centre+zoom 12; multi-point fitBounds with
  padding capped at min(canvas*0.1, 80).
- src/map/core/camera/selected-device.tsx: <MapSelectedDevice />
  pans+zooms on selection change, smooth pans on subsequent position
  updates with mapFollow on. Separate effect listens for user-gesture
  movestart (originalEvent truthy) and flips mapFollow off.
- src/map/core/camera/map-camera.tsx: <MapCamera coordinates> imperative
  one-shot. Phase 2 doesn't use it; primitive for replay (Phase 4) and
  future "fit to all" UI.
- src/map/core/follow-toggle.tsx: <FollowToggle /> icon-button using
  Lucide Locate/LocateOff. Sits below the trails toggle.
- src/routes/_authed/monitor.tsx: renders FollowToggle, MapDefaultCamera,
  MapSelectedDevice.

Deviations:
- Manual-pan listener uses an inline { originalEvent?: unknown } type
  instead of MapLibre's MapMouseEvent|MapTouchEvent union (typing
  friction across version bumps).
- MapDefaultCamera clears fittedFor on activeEventId === null so
  re-selecting the same event later re-fits.

Bundle: main 395KB / 120KB gz, no measurable change.
This commit is contained in:
2026-05-02 23:41:02 +02:00
parent cc4cd8ddbf
commit 17439de34f
10 changed files with 252 additions and 4 deletions
+10
View File
@@ -8,6 +8,8 @@ import {
usePositionStore,
} from '@/live';
import { BasemapSwitcher } from '@/map/core/basemap-switcher';
import { MapDefaultCamera, MapSelectedDevice } from '@/map/core/camera';
import { FollowToggle } from '@/map/core/follow-toggle';
import { MapView } from '@/map/core/map-view';
import { TrailsToggle } from '@/map/core/trails-toggle';
import { MapPositions } from '@/map/layers/map-positions';
@@ -58,6 +60,7 @@ function MonitorPage() {
/>
<BasemapSwitcher />
<TrailsToggle />
<FollowToggle />
{/*
Layer order matters — MapLibre paints in addLayer() call order.
<MapTrails /> mounts first so its line layer renders below the
@@ -65,6 +68,13 @@ function MonitorPage() {
*/}
<MapTrails />
<MapPositions />
{/*
Camera components are side-effect-only (return null). They sit
at the end so they react to whatever <MapPositions /> just
surfaced (selected device, latest positions).
*/}
<MapDefaultCamera />
<MapSelectedDevice />
</MapView>
</div>
);