Files
2026-05-27 23:29:53 +01:00

5.5 KiB

Tasks: enterprise-react-map

Change ID: enterprise-react-map Package: @suaveplan/react-map Target path: react/platform/react-map Prerequisite packages on Verdaccio: @suaveplan/error, @suaveplan/events, @suaveplan/react-translate


0. Pre-flight (non-negotiable)

  • 0.1 Confirm clean working tree (git status reports nothing) and no in-flight worktrees with uncommitted work (git worktree list then git status in each)
  • 0.2 If any worktrees exist under .claude/worktrees/, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (git worktree remove) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
  • 0.3 Confirm the package set in MANIFEST.md matches find packages -maxdepth 3 -name package.json count. Drift here means a prior change archived without regenerating the manifest.

1. Scaffold

  • Create directory tree. Write package.json at 0.1.0, tsconfig.json, vitest.config.ts, scripts.
  • Dependencies: @suaveplan/error workspace:^, @suaveplan/events workspace:^, @suaveplan/react-translate workspace:^, @opentelemetry/api catalog:observability, zod catalog:forms, tslib catalog:build. Peer/optional: maplibre-gl catalog:maps, leaflet catalog:maps.

2. Error codes and types

  • Define MapError; codes: MAP_ADAPTER_INIT_FAILED, MAP_RENDER_FAILED.
  • Define all types: LatLng, MapBounds, MapConfig, MapProps, MapContainerProps, MarkerData, MarkerProps, MarkerClusterData, MarkerClusterProps, PolygonData, PolygonProps, ClusterConfig, MapEventMap, UseMapReturn, MapAdapter, MapAdapterOptions.

3. Utils

geo.ts

  • haversine(a: LatLng, b: LatLng): number — distance in metres.
  • boundsUnion(a: MapBounds, b: MapBounds): MapBounds — smallest bounding box covering both.
  • pointInPolygon(point: LatLng, polygon: LatLng[]): boolean — ray-cast algorithm.
  • Write geo.test.ts (≤500 lines): known-distance pairs, bounds union, polygon in/out. { timeout: 5000 }.
  • Write geo.md.

cluster.ts

  • clusterMarkers(markers: MarkerData[], config: ClusterConfig): MarkerClusterData[] — grid-based clustering with configurable cell size.
  • Write cluster.test.ts (≤500 lines): single cluster, no overlap, empty input, zoom-level variation. { timeout: 5000 }.
  • Write cluster.md.

4. Adapters

MapAdapter interface

  • adapter.types.ts — MapAdapter<TMap> interface: init(container, config): TMap, setCenter(map, center), setZoom(map, zoom), getBounds(map): MapBounds, on(map, event, handler), off(map, event, handler), destroy(map).

MapLibreAdapter

  • createMapLibreAdapter(): MapAdapter — wraps maplibre-gl; gate with if (maplibregl === undefined) throw MapError(MAP_ADAPTER_INIT_FAILED).
  • Write maplibre.test.ts (≤500 lines): init, center/zoom set, bounds read, destroy, missing-lib error. DI-injected maplibre mock. { timeout: 10000 }.
  • Write maplibre.md.

LeafletAdapter

  • createLeafletAdapter(): MapAdapter — wraps leaflet; gate similarly.
  • Write leaflet.test.ts (≤500 lines). { timeout: 10000 }.
  • Write leaflet.md.

5. Hooks

useMap

  • useMap(config) — manages center, zoom, bounds, markers, polygons, clusters. Accepts adapter prop. Returns UseMapReturn. OTel map.init span.
  • Write use-map.test.ts (≤500 lines): state init, center update, marker add/remove, bounds change. { timeout: 10000 }.
  • Write use-map.md.

useMapContext

  • React context + useMapContext() hook. Throws MapError if used outside MapContainer.
  • Write test + md.

6. Components

MapContainer

  • Provides MapContext; renders the vendor-specific map instance. Accepts adapter, config, className.
  • Write test, stories (maplibre, leaflet, playground), md.

MapView

  • Top-level composite: MapContainer + children overlay slot. Keyboard pan (arrow keys), zoom (+/-), role="application", aria-label via useTranslation.
  • Write test, stories (default, markers, polygons, clusters, playground), md.

Marker

  • Rendered via adapter; accepts position, onClick, label, icon. Screen-reader description generated from label.
  • Write test, stories (default, custom-icon, with-popup, playground), md.

MarkerCluster

  • Renders cluster circle with count using clusterMarkers. Animated expand on click.
  • Write test, stories (small-cluster, large-cluster, expanding, playground), md.

Polygon

  • Renders polygon overlay via adapter. Accepts positions[], fillColor, strokeColor, onClick.
  • Write test, stories (default, filled, multi-polygon, playground), md.

7. E2E

  • e2e/map.spec.ts — Playwright:
    • Render MapView with MapLibre adapter; verify map canvas present.
    • Click a Marker; verify popup/callback fires.
    • Render MarkerCluster; click cluster; verify it expands to individual markers.
    • Draw a Polygon; verify onClick fires with correct coordinates.
    • Use keyboard arrow keys to pan; verify center coordinates change.

8. Verify and publish

  • bunx turbo lint typecheck test --filter=@suaveplan/react-map — green.
  • bunx turbo test:coverage --filter=@suaveplan/react-map — 100%.
  • bun scripts/publish.ts --package @suaveplan/react-map.
  • Archive change folder.