@elabs-ai/components-maps 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -0
  3. package/dist/index.css +11 -0
  4. package/dist/index.css.map +1 -0
  5. package/dist/index.d.ts +442 -0
  6. package/dist/index.js +1475 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +66 -0
  9. package/src/index.ts +37 -0
  10. package/src/lib/arc-math.test.ts +41 -0
  11. package/src/lib/arc-math.ts +45 -0
  12. package/src/lib/merge-hover-paint.test.ts +36 -0
  13. package/src/lib/merge-hover-paint.ts +21 -0
  14. package/src/lib/use-token-color.ts +23 -0
  15. package/src/map-arc/index.ts +9 -0
  16. package/src/map-arc/map-arc.stories.tsx +58 -0
  17. package/src/map-arc/map-arc.tsx +294 -0
  18. package/src/map-canvas/index.ts +8 -0
  19. package/src/map-canvas/map-canvas-webgl-fallback.test.tsx +30 -0
  20. package/src/map-canvas/map-canvas.stories.tsx +54 -0
  21. package/src/map-canvas/map-canvas.test.tsx +93 -0
  22. package/src/map-canvas/map-canvas.tsx +349 -0
  23. package/src/map-canvas/map-context.ts +32 -0
  24. package/src/map-canvas/maps.css +15 -0
  25. package/src/map-canvas/use-resolved-basemap-theme.ts +77 -0
  26. package/src/map-cluster-layer/index.ts +1 -0
  27. package/src/map-cluster-layer/map-cluster-layer.stories.tsx +56 -0
  28. package/src/map-cluster-layer/map-cluster-layer.tsx +292 -0
  29. package/src/map-controls/index.ts +1 -0
  30. package/src/map-controls/map-controls.stories.tsx +43 -0
  31. package/src/map-controls/map-controls.test.tsx +56 -0
  32. package/src/map-controls/map-controls.tsx +220 -0
  33. package/src/map-geojson/index.ts +9 -0
  34. package/src/map-geojson/map-geojson.stories.tsx +124 -0
  35. package/src/map-geojson/map-geojson.tsx +274 -0
  36. package/src/map-marker/index.ts +12 -0
  37. package/src/map-marker/map-marker.stories.tsx +71 -0
  38. package/src/map-marker/map-marker.test.tsx +81 -0
  39. package/src/map-marker/map-marker.tsx +373 -0
  40. package/src/map-popup/index.ts +1 -0
  41. package/src/map-popup/map-popup.tsx +113 -0
  42. package/src/map-route/index.ts +1 -0
  43. package/src/map-route/map-route.stories.tsx +56 -0
  44. package/src/map-route/map-route.tsx +143 -0
  45. package/src/test-utils/maplibre-mock.ts +249 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/map-canvas/map-canvas.tsx","../src/map-canvas/map-context.ts","../src/map-canvas/use-resolved-basemap-theme.ts","../src/map-marker/map-marker.tsx","../src/map-popup/map-popup.tsx","../src/map-controls/map-controls.tsx","../src/map-route/map-route.tsx","../src/lib/use-token-color.ts","../src/map-arc/map-arc.tsx","../src/lib/arc-math.ts","../src/lib/merge-hover-paint.ts","../src/map-geojson/map-geojson.tsx","../src/map-cluster-layer/map-cluster-layer.tsx"],"sourcesContent":["\"use client\";\n\nimport MapLibreGL from \"maplibre-gl\";\nimport \"maplibre-gl/dist/maplibre-gl.css\";\nimport \"./maps.css\";\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport { Spinner, StatePanel } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nimport { MapContext, type BasemapTheme } from \"./map-context\";\nimport { useResolvedBasemapTheme } from \"./use-resolved-basemap-theme\";\n\n/**\n * Default basemaps: Carto's free light/dark GL styles. These serve ODbL-licensed\n * OpenStreetMap data, which requires attribution on a public surface — see the\n * `attributionControl` note in the map constructor below.\n */\nconst defaultStyles = {\n dark: \"https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json\",\n light: \"https://basemaps.cartocdn.com/gl/positron-gl-style/style.json\",\n};\n\n// A tile-less, dependency-free style with a transparent background. Use it for\n// data visualizations (choropleths, arcs, dot maps) where you draw your own\n// layers and don't need a street basemap: `<MapCanvas blank>`. The transparent\n// background lets the themed container show through.\nconst blankMapStyle: MapLibreGL.StyleSpecification = {\n version: 8,\n sources: {},\n layers: [\n {\n id: \"background\",\n type: \"background\",\n paint: { \"background-color\": \"transparent\" },\n },\n ],\n};\n\n/** Map viewport state. */\nexport interface MapViewport {\n /** Center coordinates [longitude, latitude]. */\n center: [number, number];\n /** Zoom level. */\n zoom: number;\n /** Bearing (rotation) in degrees. */\n bearing: number;\n /** Pitch (tilt) in degrees. */\n pitch: number;\n}\n\nexport type MapStyleOption = string | MapLibreGL.StyleSpecification;\n\n/** The imperative handle exposed by `<MapCanvas ref>`: the MapLibre map itself. */\nexport type MapCanvasRef = MapLibreGL.Map;\n\nexport type MapCanvasProps = {\n children?: ReactNode;\n /** Additional CSS classes for the map container. */\n className?: string;\n /**\n * Basemap flavor. If not provided, it is derived from the active brand theme\n * (`data-theme` + that theme's own `color-scheme`), then a `dark`/`light` root class,\n * then the OS preference.\n */\n theme?: BasemapTheme;\n /** Custom map styles for light and dark themes. Overrides the default Carto styles. */\n styles?: {\n light?: MapStyleOption;\n dark?: MapStyleOption;\n };\n /**\n * Use a transparent, tile-less basemap instead of the default Carto street\n * basemap — a blank canvas. Used alone it renders nothing; add your own\n * layers on top (`<MapGeoJSON>`, `<MapArc>`, markers, …). Ideal for data\n * visualizations. Ignored when an explicit `styles` prop is provided.\n */\n blank?: boolean;\n /** Map projection type. Use `{ type: \"globe\" }` for a 3D globe view. */\n projection?: MapLibreGL.ProjectionSpecification;\n /**\n * Controlled viewport. When provided together with `onViewportChange`, the\n * map becomes controlled and the viewport is driven by this prop.\n */\n viewport?: Partial<MapViewport>;\n /**\n * Callback fired continuously as the viewport changes (pan, zoom, rotate,\n * pitch). Use standalone to observe changes, or with `viewport` for\n * controlled mode.\n */\n onViewportChange?: (viewport: MapViewport) => void;\n /** Show a loading overlay on the map (e.g. while the app fetches map data). */\n loading?: boolean;\n} & Omit<MapLibreGL.MapOptions, \"container\" | \"style\">;\n\nfunction MapLoadingOverlay() {\n return (\n <div className=\"absolute inset-0 z-10 flex items-center justify-center bg-background/50 backdrop-blur-xs\">\n <Spinner label=\"Loading map\" className=\"size-5\" />\n </div>\n );\n}\n\nfunction getViewport(map: MapLibreGL.Map): MapViewport {\n const center = map.getCenter();\n return {\n center: [center.lng, center.lat],\n zoom: map.getZoom(),\n bearing: map.getBearing(),\n pitch: map.getPitch(),\n };\n}\n\n/**\n * The root map surface — a token/theme-aware MapLibre GL canvas. Compose the\n * other `@elabs-ai/components-maps` components (markers, popups, controls, layers) as\n * children; they reach the map through context (`useMap`).\n *\n * The ref exposes the raw MapLibre `Map` instance for imperative work\n * (`flyTo`, `fitBounds`, …).\n */\nexport const MapCanvas = forwardRef<MapCanvasRef, MapCanvasProps>(function MapCanvas(\n {\n children,\n className,\n theme: themeProp,\n styles,\n blank = false,\n projection,\n viewport,\n onViewportChange,\n loading = false,\n ...props\n },\n ref,\n) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [mapInstance, setMapInstance] = useState<MapLibreGL.Map | null>(null);\n const [initFailed, setInitFailed] = useState(false);\n const [isLoaded, setIsLoaded] = useState(false);\n const [isStyleLoaded, setIsStyleLoaded] = useState(false);\n const currentStyleRef = useRef<MapStyleOption | null>(null);\n const styleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const internalUpdateRef = useRef(false);\n const { resolvedTheme, themeKey } = useResolvedBasemapTheme(themeProp);\n\n const isControlled = viewport !== undefined && onViewportChange !== undefined;\n\n const onViewportChangeRef = useRef(onViewportChange);\n onViewportChangeRef.current = onViewportChange;\n\n const mapStyles = useMemo(() => {\n // Explicit styles win. Otherwise `blank` opts into the transparent\n // tile-less basemap; with neither, fall back to the Carto defaults.\n if (styles) {\n return {\n dark: styles.dark ?? defaultStyles.dark,\n light: styles.light ?? defaultStyles.light,\n };\n }\n if (blank) {\n return { dark: blankMapStyle, light: blankMapStyle };\n }\n return defaultStyles;\n }, [styles, blank]);\n\n // Expose the map instance to the parent component.\n useImperativeHandle(ref, () => mapInstance as MapLibreGL.Map, [mapInstance]);\n\n const clearStyleTimeout = useCallback(() => {\n if (styleTimeoutRef.current) {\n clearTimeout(styleTimeoutRef.current);\n styleTimeoutRef.current = null;\n }\n }, []);\n\n // Initialize the map.\n useEffect(() => {\n if (!containerRef.current) return;\n\n const initialStyle = resolvedTheme === \"dark\" ? mapStyles.dark : mapStyles.light;\n currentStyleRef.current = initialStyle;\n\n let map: MapLibreGL.Map;\n try {\n map = new MapLibreGL.Map({\n container: containerRef.current,\n style: initialStyle,\n renderWorldCopies: false,\n // Attribution control OFF by default — a maintainer decision for internal\n // use, taken deliberately and recorded in CHANGELOG.md + the map-components\n // rule. NOTE THE CONSTRAINT: the default Carto basemap serves OpenStreetMap\n // data, which is ODbL-licensed and requires the credit, and Carto's terms\n // require it too — so a surface that ships PUBLICLY on these tiles must turn\n // it back on with `attributionControl={{ compact: true }}` (it wins through\n // `...props` below), or move to tiles licensed without the requirement via\n // `styles` / `blank`.\n attributionControl: false,\n ...props,\n ...viewport,\n });\n } catch {\n // MapLibre throws at construction when WebGL is unavailable (headless\n // browsers without GPU, remote desktops). Degrade to a quiet panel\n // instead of an unhandled render error.\n setInitFailed(true);\n return;\n }\n\n const styleDataHandler = () => {\n clearStyleTimeout();\n // Delay so the style is fully processed before layer operations — avoids\n // race conditions on setStyle without force-updating every layer.\n styleTimeoutRef.current = setTimeout(() => {\n setIsStyleLoaded(true);\n if (projection) {\n map.setProjection(projection);\n }\n }, 100);\n };\n // No-op under the default (`attributionControl: false` above). This exists for\n // the case a consumer turns the control back ON for a public surface: MapLibre\n // paints even a COMPACT control expanded on first render (`<details open>` +\n // `maplibregl-compact-show`), so the credits land as a text slab until someone\n // clicks the toggle. Collapse it to the labelled ⓘ button instead.\n const loadHandler = () => {\n setIsLoaded(true);\n map\n .getContainer()\n .querySelectorAll<HTMLDetailsElement>(\"details.maplibregl-ctrl-attrib[open]\")\n .forEach((el) => {\n el.open = false;\n });\n };\n\n // Viewport change handler — skip if triggered by an internal update.\n const handleMove = () => {\n if (internalUpdateRef.current) return;\n onViewportChangeRef.current?.(getViewport(map));\n };\n\n map.on(\"load\", loadHandler);\n map.on(\"styledata\", styleDataHandler);\n map.on(\"move\", handleMove);\n setMapInstance(map);\n\n return () => {\n clearStyleTimeout();\n map.off(\"load\", loadHandler);\n map.off(\"styledata\", styleDataHandler);\n map.off(\"move\", handleMove);\n map.remove();\n setIsLoaded(false);\n setIsStyleLoaded(false);\n setMapInstance(null);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only: map options are init-time; style/theme/projection changes are synced by the effects below\n }, []);\n\n // Sync controlled viewport to the map.\n useEffect(() => {\n if (!mapInstance || !isControlled || !viewport) return;\n if (mapInstance.isMoving()) return;\n\n const current = getViewport(mapInstance);\n const next = {\n center: viewport.center ?? current.center,\n zoom: viewport.zoom ?? current.zoom,\n bearing: viewport.bearing ?? current.bearing,\n pitch: viewport.pitch ?? current.pitch,\n };\n\n if (\n next.center[0] === current.center[0] &&\n next.center[1] === current.center[1] &&\n next.zoom === current.zoom &&\n next.bearing === current.bearing &&\n next.pitch === current.pitch\n ) {\n return;\n }\n\n internalUpdateRef.current = true;\n mapInstance.jumpTo(next);\n internalUpdateRef.current = false;\n }, [mapInstance, isControlled, viewport]);\n\n // Swap the basemap style when the theme flips.\n useEffect(() => {\n if (!mapInstance || !resolvedTheme) return;\n\n const newStyle = resolvedTheme === \"dark\" ? mapStyles.dark : mapStyles.light;\n\n if (currentStyleRef.current === newStyle) return;\n\n clearStyleTimeout();\n currentStyleRef.current = newStyle;\n setIsStyleLoaded(false);\n\n mapInstance.setStyle(newStyle, { diff: true });\n }, [mapInstance, resolvedTheme, mapStyles, clearStyleTimeout]);\n\n // Sync projection when the prop changes after mount.\n useEffect(() => {\n if (!mapInstance || !isStyleLoaded || !projection) return;\n mapInstance.setProjection(projection);\n }, [mapInstance, isStyleLoaded, projection]);\n\n const contextValue = useMemo(\n () => ({\n map: mapInstance,\n isLoaded: isLoaded && isStyleLoaded,\n resolvedTheme,\n themeKey,\n }),\n [mapInstance, isLoaded, isStyleLoaded, resolvedTheme, themeKey],\n );\n\n if (initFailed) {\n return (\n <div className={cn(\"relative h-full w-full\", className)}>\n <StatePanel\n kind=\"error\"\n title=\"Map unavailable\"\n description=\"This browser can’t render WebGL maps.\"\n className=\"h-full\"\n />\n </div>\n );\n }\n\n return (\n <MapContext.Provider value={contextValue}>\n <div ref={containerRef} className={cn(\"relative h-full w-full\", className)}>\n {(!isLoaded || loading) && <MapLoadingOverlay />}\n {/* SSR-safe: children render only when the map exists on the client. */}\n {mapInstance && children}\n </div>\n </MapContext.Provider>\n );\n});\n","\"use client\";\n\nimport type MapLibreGL from \"maplibre-gl\";\nimport { createContext, use } from \"react\";\n\n/** Light-or-dark flavor of the active basemap (derived from the brand theme). */\nexport type BasemapTheme = \"light\" | \"dark\";\n\nexport interface MapContextValue {\n /** The live MapLibre map instance (`null` until the map has mounted). */\n map: MapLibreGL.Map | null;\n /** True once the map AND its style are fully loaded — gate layer operations on this. */\n isLoaded: boolean;\n /** Which basemap flavor is active. */\n resolvedTheme: BasemapTheme;\n /**\n * Changes whenever the active brand theme changes. Layer components use it\n * as a dependency key to re-resolve semantic token colors for WebGL paint.\n */\n themeKey: string;\n}\n\nexport const MapContext = createContext<MapContextValue | null>(null);\n\n/** Access the map instance + load state from any descendant of `<MapCanvas>`. */\nexport function useMap(): MapContextValue {\n const context = use(MapContext);\n if (!context) {\n throw new Error(\"useMap must be used within a <MapCanvas>\");\n }\n return context;\n}\n","\"use client\";\n\nimport { resolveThemeIsDark } from \"@elabs-ai/components-tokens\";\nimport { useEffect, useState } from \"react\";\n\nimport type { BasemapTheme } from \"./map-context\";\n\n/**\n * Brand theme → basemap flavor. When a `data-theme` is set (the ThemeProvider\n * contract) the theme's own `color-scheme` is authoritative via\n * `resolveThemeIsDark` — which is why a CONSUMER-AUTHORED dark theme gets the\n * dark basemap without registering anything here (ADR 0029; the old\n * `THEME_META[theme].dark` lookup only knew the themes this repo ships). A\n * `dark`/`light` class on `<html>` is honored for non-brand hosts (next-themes\n * et al.); the OS preference is only the last-resort fallback.\n */\nfunction getBrandTheme(): BasemapTheme | null {\n if (typeof document === \"undefined\") return null;\n const root = document.documentElement;\n if (root.getAttribute(\"data-theme\")) return resolveThemeIsDark(root) ? \"dark\" : \"light\";\n if (root.classList.contains(\"dark\")) return \"dark\";\n if (root.classList.contains(\"light\")) return \"light\";\n return null;\n}\n\nfunction getSystemTheme(): BasemapTheme {\n if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return \"light\";\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\nfunction getThemeAttribute(): string {\n if (typeof document === \"undefined\") return \"\";\n return document.documentElement.getAttribute(\"data-theme\") ?? \"\";\n}\n\n/**\n * Resolve the basemap flavor and a change-key for the active brand theme.\n * The observer stays active even with an explicit `theme` prop: token colors\n * (routes, clusters, …) must still re-resolve when `data-theme` changes.\n */\nexport function useResolvedBasemapTheme(themeProp?: BasemapTheme): {\n resolvedTheme: BasemapTheme;\n themeKey: string;\n} {\n const [detected, setDetected] = useState<BasemapTheme>(() => getBrandTheme() ?? getSystemTheme());\n const [themeAttr, setThemeAttr] = useState(getThemeAttribute);\n\n useEffect(() => {\n const update = () => {\n setThemeAttr(getThemeAttribute());\n const brand = getBrandTheme();\n if (brand) setDetected(brand);\n };\n const observer = new MutationObserver(update);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"data-theme\", \"class\"],\n });\n\n const mediaQuery =\n typeof window.matchMedia === \"function\"\n ? window.matchMedia(\"(prefers-color-scheme: dark)\")\n : null;\n const handleSystemChange = (e: MediaQueryListEvent) => {\n if (!getBrandTheme()) setDetected(e.matches ? \"dark\" : \"light\");\n };\n mediaQuery?.addEventListener(\"change\", handleSystemChange);\n\n return () => {\n observer.disconnect();\n mediaQuery?.removeEventListener(\"change\", handleSystemChange);\n };\n }, []);\n\n const resolvedTheme = themeProp ?? detected;\n return { resolvedTheme, themeKey: `${themeAttr}:${resolvedTheme}` };\n}\n","\"use client\";\n\nimport MapLibreGL, { type MarkerOptions, type PopupOptions } from \"maplibre-gl\";\nimport { createContext, use, useEffect, useMemo, useRef, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nimport { useMap } from \"../map-canvas/map-context\";\n\ntype MarkerContextValue = {\n marker: MapLibreGL.Marker;\n map: MapLibreGL.Map | null;\n};\n\nconst MarkerContext = createContext<MarkerContextValue | null>(null);\n\nfunction useMarkerContext() {\n const context = use(MarkerContext);\n if (!context) {\n throw new Error(\"MapMarker sub-components must be used within <MapMarker>\");\n }\n return context;\n}\n\nexport type MapMarkerProps = {\n /** Longitude coordinate for the marker position. */\n longitude: number;\n /** Latitude coordinate for the marker position. */\n latitude: number;\n /** Marker sub-components (MapMarkerContent, MapMarkerPopup, MapMarkerTooltip, MapMarkerLabel). */\n children: ReactNode;\n /** Callback when the marker is clicked. */\n onClick?: (e: MouseEvent) => void;\n /** Callback when the mouse enters the marker. */\n onMouseEnter?: (e: MouseEvent) => void;\n /** Callback when the mouse leaves the marker. */\n onMouseLeave?: (e: MouseEvent) => void;\n /** Callback when a drag starts (requires `draggable`). */\n onDragStart?: (lngLat: { lng: number; lat: number }) => void;\n /** Callback during a drag (requires `draggable`). */\n onDrag?: (lngLat: { lng: number; lat: number }) => void;\n /** Callback when a drag ends (requires `draggable`). */\n onDragEnd?: (lngLat: { lng: number; lat: number }) => void;\n} & Omit<MarkerOptions, \"element\">;\n\n/**\n * A marker anchored at a lng/lat. Compose the pieces you need:\n * `MapMarkerContent` (the visual), `MapMarkerLabel`, `MapMarkerPopup` (opens\n * on click) and `MapMarkerTooltip` (shows on hover).\n */\nexport function MapMarker({\n longitude,\n latitude,\n children,\n onClick,\n onMouseEnter,\n onMouseLeave,\n onDragStart,\n onDrag,\n onDragEnd,\n draggable = false,\n ...markerOptions\n}: MapMarkerProps) {\n const { map } = useMap();\n\n const callbacksRef = useRef({\n onClick,\n onMouseEnter,\n onMouseLeave,\n onDragStart,\n onDrag,\n onDragEnd,\n });\n callbacksRef.current = {\n onClick,\n onMouseEnter,\n onMouseLeave,\n onDragStart,\n onDrag,\n onDragEnd,\n };\n\n const marker = useMemo(() => {\n const markerInstance = new MapLibreGL.Marker({\n ...markerOptions,\n element: document.createElement(\"div\"),\n draggable,\n }).setLngLat([longitude, latitude]);\n\n const handleClick = (e: MouseEvent) => callbacksRef.current.onClick?.(e);\n const handleMouseEnter = (e: MouseEvent) => callbacksRef.current.onMouseEnter?.(e);\n const handleMouseLeave = (e: MouseEvent) => callbacksRef.current.onMouseLeave?.(e);\n\n markerInstance.getElement()?.addEventListener(\"click\", handleClick);\n markerInstance.getElement()?.addEventListener(\"mouseenter\", handleMouseEnter);\n markerInstance.getElement()?.addEventListener(\"mouseleave\", handleMouseLeave);\n\n const handleDragStart = () => {\n const lngLat = markerInstance.getLngLat();\n callbacksRef.current.onDragStart?.({ lng: lngLat.lng, lat: lngLat.lat });\n };\n const handleDrag = () => {\n const lngLat = markerInstance.getLngLat();\n callbacksRef.current.onDrag?.({ lng: lngLat.lng, lat: lngLat.lat });\n };\n const handleDragEnd = () => {\n const lngLat = markerInstance.getLngLat();\n callbacksRef.current.onDragEnd?.({ lng: lngLat.lng, lat: lngLat.lat });\n };\n\n markerInstance.on(\"dragstart\", handleDragStart);\n markerInstance.on(\"drag\", handleDrag);\n markerInstance.on(\"dragend\", handleDragEnd);\n\n return markerInstance;\n // eslint-disable-next-line react-hooks/exhaustive-deps -- instance created once; position/options are synced by the effect below\n }, []);\n\n useEffect(() => {\n if (!map) return;\n\n marker.addTo(map);\n\n return () => {\n marker.remove();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- `marker` is stable (created once above)\n }, [map]);\n\n const { offset, rotation, rotationAlignment, pitchAlignment } = markerOptions;\n\n useEffect(() => {\n const current = marker.getLngLat();\n if (current.lng !== longitude || current.lat !== latitude) {\n marker.setLngLat([longitude, latitude]);\n }\n\n if (marker.isDraggable() !== draggable) {\n marker.setDraggable(draggable);\n }\n\n const currentOffset = marker.getOffset();\n const newOffset = offset ?? [0, 0];\n const [newOffsetX, newOffsetY] = Array.isArray(newOffset)\n ? newOffset\n : [newOffset.x, newOffset.y];\n if (currentOffset.x !== newOffsetX || currentOffset.y !== newOffsetY) {\n marker.setOffset(newOffset);\n }\n\n if (marker.getRotation() !== (rotation ?? 0)) {\n marker.setRotation(rotation ?? 0);\n }\n if (marker.getRotationAlignment() !== (rotationAlignment ?? \"auto\")) {\n marker.setRotationAlignment(rotationAlignment ?? \"auto\");\n }\n if (marker.getPitchAlignment() !== (pitchAlignment ?? \"auto\")) {\n marker.setPitchAlignment(pitchAlignment ?? \"auto\");\n }\n }, [marker, longitude, latitude, draggable, offset, rotation, rotationAlignment, pitchAlignment]);\n\n return <MarkerContext.Provider value={{ marker, map }}>{children}</MarkerContext.Provider>;\n}\n\nexport interface MapMarkerContentProps {\n /** Custom marker content. Defaults to a primary-colored dot. */\n children?: ReactNode;\n /** Additional CSS classes for the marker container. */\n className?: string;\n}\n\n/** The marker's visual, portaled into the MapLibre marker element. */\nexport function MapMarkerContent({ children, className }: MapMarkerContentProps) {\n const { marker } = useMarkerContext();\n\n return createPortal(\n <div className={cn(\"relative cursor-pointer\", className)}>\n {children || <DefaultMarkerIcon />}\n </div>,\n marker.getElement(),\n );\n}\n\nfunction DefaultMarkerIcon() {\n return (\n <div className=\"relative size-4 rounded-full border-2 border-background bg-primary shadow-sm\" />\n );\n}\n\nfunction PopupCloseButton({ onClick }: { onClick: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label=\"Close popup\"\n className=\"absolute top-1 right-1 z-10 inline-flex size-5 items-center justify-center rounded-sm text-foreground transition-colors duration-fast hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\"\n >\n <X className=\"size-3.5\" aria-hidden=\"true\" />\n </button>\n );\n}\n\nexport type MapMarkerPopupProps = {\n /** Popup content. */\n children: ReactNode;\n /** Additional CSS classes for the popup container. */\n className?: string;\n /** Show a close button in the popup (default: false). */\n closeButton?: boolean;\n} & Omit<PopupOptions, \"className\" | \"closeButton\">;\n\n/** A popup attached to the marker — MapLibre toggles it on marker click. */\nexport function MapMarkerPopup({\n children,\n className,\n closeButton = false,\n ...popupOptions\n}: MapMarkerPopupProps) {\n const { marker, map } = useMarkerContext();\n const container = useMemo(() => document.createElement(\"div\"), []);\n const { offset, maxWidth } = popupOptions;\n\n const popup = useMemo(() => {\n return new MapLibreGL.Popup({\n offset: 16,\n ...popupOptions,\n closeButton: false,\n })\n .setMaxWidth(\"none\")\n .setDOMContent(container);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- instance created once; options are synced by the effect below\n }, []);\n\n useEffect(() => {\n if (!map) return;\n\n popup.setDOMContent(container);\n marker.setPopup(popup);\n\n return () => {\n marker.setPopup(null);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- popup/marker/container are stable\n }, [map]);\n\n // Sync popup options when they change.\n useEffect(() => {\n popup.setOffset(offset ?? 16);\n if (maxWidth) {\n popup.setMaxWidth(maxWidth);\n }\n }, [popup, offset, maxWidth]);\n\n const handleClose = () => popup.remove();\n\n return createPortal(\n <div\n className={cn(\n \"relative max-w-62 rounded-md bg-popover p-3 text-popover-foreground shadow-ring-md\",\n \"animate-in fade-in-0 zoom-in-95 duration-fast ease-entrance\",\n className,\n )}\n >\n {closeButton && <PopupCloseButton onClick={handleClose} />}\n {children}\n </div>,\n container,\n );\n}\n\nexport type MapMarkerTooltipProps = {\n /** Tooltip content. */\n children: ReactNode;\n /** Additional CSS classes for the tooltip container. */\n className?: string;\n} & Omit<PopupOptions, \"className\" | \"closeButton\" | \"closeOnClick\">;\n\n/** A hover tooltip attached to the marker. */\nexport function MapMarkerTooltip({ children, className, ...popupOptions }: MapMarkerTooltipProps) {\n const { marker, map } = useMarkerContext();\n const container = useMemo(() => document.createElement(\"div\"), []);\n const { offset, maxWidth } = popupOptions;\n\n const tooltip = useMemo(() => {\n return new MapLibreGL.Popup({\n offset: 16,\n ...popupOptions,\n closeOnClick: true,\n closeButton: false,\n }).setMaxWidth(\"none\");\n // eslint-disable-next-line react-hooks/exhaustive-deps -- instance created once; options are synced by the effect below\n }, []);\n\n useEffect(() => {\n if (!map) return;\n\n tooltip.setDOMContent(container);\n\n const handleMouseEnter = () => {\n tooltip.setLngLat(marker.getLngLat()).addTo(map);\n };\n const handleMouseLeave = () => tooltip.remove();\n\n marker.getElement()?.addEventListener(\"mouseenter\", handleMouseEnter);\n marker.getElement()?.addEventListener(\"mouseleave\", handleMouseLeave);\n\n return () => {\n marker.getElement()?.removeEventListener(\"mouseenter\", handleMouseEnter);\n marker.getElement()?.removeEventListener(\"mouseleave\", handleMouseLeave);\n tooltip.remove();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- tooltip/marker/container are stable\n }, [map]);\n\n // Sync tooltip options when they change.\n useEffect(() => {\n tooltip.setOffset(offset ?? 16);\n if (maxWidth) {\n tooltip.setMaxWidth(maxWidth);\n }\n }, [tooltip, offset, maxWidth]);\n\n return createPortal(\n <div\n className={cn(\n \"pointer-events-none rounded-md bg-foreground px-2 py-1 text-meta text-balance text-background shadow-md\",\n \"animate-in fade-in-0 zoom-in-95 duration-fast ease-entrance\",\n className,\n )}\n >\n {children}\n </div>,\n container,\n );\n}\n\nexport interface MapMarkerLabelProps {\n /** Label text content. */\n children: ReactNode;\n /** Additional CSS classes for the label. */\n className?: string;\n /** Position of the label relative to the marker (default: \"top\"). */\n position?: \"top\" | \"bottom\";\n}\n\n/**\n * A small always-visible text label above or below the marker. Portaled into\n * the marker element so it anchors to the marker whether composed as a sibling\n * of `MapMarkerContent` or nested inside it (the three-theme sweep caught the\n * sibling composition rendering the label against the map container instead).\n */\nexport function MapMarkerLabel({ children, className, position = \"top\" }: MapMarkerLabelProps) {\n const { marker } = useMarkerContext();\n const positionClasses = {\n top: \"bottom-full mb-1\",\n bottom: \"top-full mt-1\",\n };\n\n return createPortal(\n <div\n className={cn(\n \"absolute left-1/2 -translate-x-1/2 whitespace-nowrap\",\n \"text-meta font-medium text-foreground\",\n positionClasses[position],\n className,\n )}\n >\n {children}\n </div>,\n marker.getElement(),\n );\n}\n","\"use client\";\n\nimport MapLibreGL, { type PopupOptions } from \"maplibre-gl\";\nimport { useEffect, useMemo, useRef, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nimport { useMap } from \"../map-canvas/map-context\";\n\nexport type MapPopupProps = {\n /** Longitude coordinate for the popup position. */\n longitude: number;\n /** Latitude coordinate for the popup position. */\n latitude: number;\n /** Callback when the popup is closed. */\n onClose?: () => void;\n /** Popup content. */\n children: ReactNode;\n /** Additional CSS classes for the popup container. */\n className?: string;\n /** Show a close button in the popup (default: false). */\n closeButton?: boolean;\n} & Omit<PopupOptions, \"className\" | \"closeButton\">;\n\n/**\n * A standalone popup anchored at a lng/lat (not attached to a marker) —\n * typically rendered conditionally from app state (e.g. after a layer click).\n */\nexport function MapPopup({\n longitude,\n latitude,\n onClose,\n children,\n className,\n closeButton = false,\n ...popupOptions\n}: MapPopupProps) {\n const { map } = useMap();\n const onCloseRef = useRef(onClose);\n onCloseRef.current = onClose;\n const container = useMemo(() => document.createElement(\"div\"), []);\n const { offset, maxWidth } = popupOptions;\n\n const popup = useMemo(() => {\n return new MapLibreGL.Popup({\n offset: 16,\n ...popupOptions,\n closeButton: false,\n })\n .setMaxWidth(\"none\")\n .setLngLat([longitude, latitude]);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- instance created once; position/options are synced by the effect below\n }, []);\n\n useEffect(() => {\n if (!map) return;\n\n const onCloseProp = () => onCloseRef.current?.();\n\n popup.on(\"close\", onCloseProp);\n\n popup.setDOMContent(container);\n popup.addTo(map);\n\n return () => {\n popup.off(\"close\", onCloseProp);\n if (popup.isOpen()) {\n popup.remove();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- popup/container are stable\n }, [map]);\n\n // Sync popup position and options when they change.\n useEffect(() => {\n const current = popup.getLngLat();\n if (!current || current.lng !== longitude || current.lat !== latitude) {\n popup.setLngLat([longitude, latitude]);\n }\n popup.setOffset(offset ?? 16);\n if (maxWidth) {\n popup.setMaxWidth(maxWidth);\n }\n }, [popup, longitude, latitude, offset, maxWidth]);\n\n const handleClose = () => {\n popup.remove();\n };\n\n return createPortal(\n <div\n className={cn(\n \"relative max-w-62 rounded-md bg-popover p-3 text-popover-foreground shadow-ring-md\",\n \"animate-in fade-in-0 zoom-in-95 duration-fast ease-entrance\",\n className,\n )}\n >\n {closeButton && (\n <button\n type=\"button\"\n onClick={handleClose}\n aria-label=\"Close popup\"\n className=\"absolute top-1 right-1 z-10 inline-flex size-5 items-center justify-center rounded-sm text-foreground transition-colors duration-fast hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\"\n >\n <X className=\"size-3.5\" aria-hidden=\"true\" />\n </button>\n )}\n {children}\n </div>,\n container,\n );\n}\n","\"use client\";\n\nimport { useCallback, useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { Locate, Maximize, Minus, Plus } from \"lucide-react\";\nimport { Spinner } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nimport { useMap } from \"../map-canvas/map-context\";\n\nexport interface MapControlsProps {\n /** Position of the controls on the map (default: \"bottom-right\"). */\n position?: \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\";\n /** Show zoom in/out buttons (default: true). */\n showZoom?: boolean;\n /** Show a compass button to reset bearing/pitch (default: false). */\n showCompass?: boolean;\n /** Show a locate button to fly to the user's location (default: false). */\n showLocate?: boolean;\n /** Show a fullscreen toggle button (default: false). */\n showFullscreen?: boolean;\n /** Additional CSS classes for the controls container. */\n className?: string;\n /** Callback with user coordinates when located. */\n onLocate?: (coords: { longitude: number; latitude: number }) => void;\n /** Callback when geolocation fails or is denied. */\n onLocateError?: (error: GeolocationPositionError) => void;\n}\n\n// bottom-right sits above MapLibre's attribution control — keep it visible.\nconst positionClasses = {\n \"top-left\": \"top-2 left-2\",\n \"top-right\": \"top-2 right-2\",\n \"bottom-left\": \"bottom-2 left-2\",\n \"bottom-right\": \"bottom-10 right-2\",\n};\n\nfunction ControlGroup({ children }: { children: ReactNode }) {\n return (\n <div className=\"flex flex-col divide-y overflow-hidden rounded-md bg-surface-elevated shadow-ring-sm\">\n {children}\n </div>\n );\n}\n\nfunction ControlButton({\n onClick,\n label,\n children,\n disabled = false,\n}: {\n onClick: () => void;\n label: string;\n children: ReactNode;\n disabled?: boolean;\n}) {\n return (\n <button\n onClick={onClick}\n aria-label={label}\n type=\"button\"\n className={cn(\n \"flex size-8 items-center justify-center text-foreground transition-colors duration-fast\",\n \"hover:bg-surface-muted\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n )}\n disabled={disabled}\n >\n {children}\n </button>\n );\n}\n\n/** Branded zoom / compass / locate / fullscreen controls. Render inside `<MapCanvas>`. */\nexport function MapControls({\n position = \"bottom-right\",\n showZoom = true,\n showCompass = false,\n showLocate = false,\n showFullscreen = false,\n className,\n onLocate,\n onLocateError,\n}: MapControlsProps) {\n const { map } = useMap();\n const [waitingForLocation, setWaitingForLocation] = useState(false);\n\n const handleZoomIn = useCallback(() => {\n map?.zoomTo(map.getZoom() + 1, { duration: 300 });\n }, [map]);\n\n const handleZoomOut = useCallback(() => {\n map?.zoomTo(map.getZoom() - 1, { duration: 300 });\n }, [map]);\n\n const handleResetBearing = useCallback(() => {\n map?.resetNorthPitch({ duration: 300 });\n }, [map]);\n\n const handleLocate = useCallback(() => {\n if (!(\"geolocation\" in navigator)) return;\n setWaitingForLocation(true);\n navigator.geolocation.getCurrentPosition(\n (pos) => {\n const coords = {\n longitude: pos.coords.longitude,\n latitude: pos.coords.latitude,\n };\n map?.flyTo({\n center: [coords.longitude, coords.latitude],\n zoom: 14,\n duration: 1500,\n });\n onLocate?.(coords);\n setWaitingForLocation(false);\n },\n (error) => {\n onLocateError?.(error);\n setWaitingForLocation(false);\n },\n );\n }, [map, onLocate, onLocateError]);\n\n const handleFullscreen = useCallback(() => {\n const container = map?.getContainer();\n if (!container) return;\n if (document.fullscreenElement) {\n void document.exitFullscreen();\n } else {\n void container.requestFullscreen();\n }\n }, [map]);\n\n return (\n <div\n className={cn(\"absolute z-10 flex flex-col gap-1.5\", positionClasses[position], className)}\n >\n {showZoom && (\n <ControlGroup>\n <ControlButton onClick={handleZoomIn} label=\"Zoom in\">\n <Plus className=\"size-4\" aria-hidden=\"true\" />\n </ControlButton>\n <ControlButton onClick={handleZoomOut} label=\"Zoom out\">\n <Minus className=\"size-4\" aria-hidden=\"true\" />\n </ControlButton>\n </ControlGroup>\n )}\n {showCompass && (\n <ControlGroup>\n <CompassButton onClick={handleResetBearing} />\n </ControlGroup>\n )}\n {showLocate && (\n <ControlGroup>\n <ControlButton\n onClick={handleLocate}\n label=\"Find my location\"\n disabled={waitingForLocation}\n >\n {waitingForLocation ? (\n <Spinner label=\"Locating\" className=\"size-4 text-foreground\" />\n ) : (\n <Locate className=\"size-4\" aria-hidden=\"true\" />\n )}\n </ControlButton>\n </ControlGroup>\n )}\n {showFullscreen && (\n <ControlGroup>\n <ControlButton onClick={handleFullscreen} label=\"Toggle fullscreen\">\n <Maximize className=\"size-4\" aria-hidden=\"true\" />\n </ControlButton>\n </ControlGroup>\n )}\n </div>\n );\n}\n\nfunction CompassButton({ onClick }: { onClick: () => void }) {\n const { map } = useMap();\n const compassRef = useRef<SVGSVGElement>(null);\n\n useEffect(() => {\n if (!map || !compassRef.current) return;\n\n const compass = compassRef.current;\n\n const updateRotation = () => {\n const bearing = map.getBearing();\n const pitch = map.getPitch();\n compass.style.transform = `rotateX(${pitch}deg) rotateZ(${-bearing}deg)`;\n };\n\n map.on(\"rotate\", updateRotation);\n map.on(\"pitch\", updateRotation);\n updateRotation();\n\n return () => {\n map.off(\"rotate\", updateRotation);\n map.off(\"pitch\", updateRotation);\n };\n }, [map]);\n\n return (\n <ControlButton onClick={onClick} label=\"Reset bearing to north\">\n <svg\n ref={compassRef}\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n className=\"size-5 transition-transform duration-fast\"\n style={{ transformStyle: \"preserve-3d\" }}\n >\n <path d=\"M12 2L16 12H12V2Z\" className=\"fill-destructive\" />\n <path d=\"M12 2L8 12H12V2Z\" className=\"fill-destructive/50\" />\n <path d=\"M12 22L16 12H12V22Z\" className=\"fill-muted-foreground/60\" />\n <path d=\"M12 22L8 12H12V22Z\" className=\"fill-muted-foreground/30\" />\n </svg>\n </ControlButton>\n );\n}\n","\"use client\";\n\nimport type MapLibreGL from \"maplibre-gl\";\nimport { useEffect, useId } from \"react\";\n\nimport { useMap } from \"../map-canvas/map-context\";\nimport { useTokenColor } from \"../lib/use-token-color\";\n\nexport interface MapRouteProps {\n /** Optional unique identifier for the route layer. */\n id?: string;\n /** Array of [longitude, latitude] coordinate pairs defining the route. */\n coordinates: [number, number][];\n /** Line color as a CSS color value. Defaults to the theme's `--primary` token. */\n color?: string;\n /** Line width in pixels (default: 3). */\n width?: number;\n /** Line opacity from 0 to 1 (default: 0.8). */\n opacity?: number;\n /** Dash pattern [dash length, gap length] for dashed lines. */\n dashArray?: [number, number];\n /** Callback when the route line is clicked. */\n onClick?: () => void;\n /** Callback when the mouse enters the route line. */\n onMouseEnter?: () => void;\n /** Callback when the mouse leaves the route line. */\n onMouseLeave?: () => void;\n /** Whether the route is interactive — shows a pointer cursor on hover (default: true). */\n interactive?: boolean;\n}\n\n/** A GeoJSON line layer for routes/paths. Renders nothing itself — it draws on the map. */\nexport function MapRoute({\n id: propId,\n coordinates,\n color,\n width = 3,\n opacity = 0.8,\n dashArray,\n onClick,\n onMouseEnter,\n onMouseLeave,\n interactive = true,\n}: MapRouteProps) {\n const { map, isLoaded } = useMap();\n const autoId = useId();\n const id = propId ?? autoId;\n const sourceId = `route-source-${id}`;\n const layerId = `route-layer-${id}`;\n\n const primary = useTokenColor(\"--primary\");\n const lineColor = color ?? primary;\n\n // Add source and layer on mount.\n useEffect(() => {\n if (!isLoaded || !map) return;\n\n map.addSource(sourceId, {\n type: \"geojson\",\n data: {\n type: \"Feature\",\n properties: {},\n geometry: { type: \"LineString\", coordinates: [] },\n },\n });\n\n map.addLayer({\n id: layerId,\n type: \"line\",\n source: sourceId,\n layout: { \"line-join\": \"round\", \"line-cap\": \"round\" },\n paint: {\n \"line-color\": lineColor,\n \"line-width\": width,\n \"line-opacity\": opacity,\n ...(dashArray && { \"line-dasharray\": dashArray }),\n },\n });\n\n return () => {\n try {\n if (map.getLayer(layerId)) map.removeLayer(layerId);\n if (map.getSource(sourceId)) map.removeSource(sourceId);\n } catch {\n // style may be mid-reload\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- source/layer created once per map; data + paint are synced by the effects below\n }, [isLoaded, map]);\n\n // When coordinates change, update the source data.\n useEffect(() => {\n if (!isLoaded || !map || coordinates.length < 2) return;\n\n const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource;\n if (source) {\n source.setData({\n type: \"Feature\",\n properties: {},\n geometry: { type: \"LineString\", coordinates },\n });\n }\n }, [isLoaded, map, coordinates, sourceId]);\n\n // Sync paint when styling (or the resolved theme color) changes.\n useEffect(() => {\n if (!isLoaded || !map || !map.getLayer(layerId)) return;\n\n map.setPaintProperty(layerId, \"line-color\", lineColor);\n map.setPaintProperty(layerId, \"line-width\", width);\n map.setPaintProperty(layerId, \"line-opacity\", opacity);\n map.setPaintProperty(layerId, \"line-dasharray\", dashArray);\n }, [isLoaded, map, layerId, lineColor, width, opacity, dashArray]);\n\n // Handle click and hover events.\n useEffect(() => {\n if (!isLoaded || !map || !interactive) return;\n\n const handleClick = () => {\n onClick?.();\n };\n const handleMouseEnter = () => {\n map.getCanvas().style.cursor = \"pointer\";\n onMouseEnter?.();\n };\n const handleMouseLeave = () => {\n map.getCanvas().style.cursor = \"\";\n onMouseLeave?.();\n };\n\n map.on(\"click\", layerId, handleClick);\n map.on(\"mouseenter\", layerId, handleMouseEnter);\n map.on(\"mouseleave\", layerId, handleMouseLeave);\n\n return () => {\n map.off(\"click\", layerId, handleClick);\n map.off(\"mouseenter\", layerId, handleMouseEnter);\n map.off(\"mouseleave\", layerId, handleMouseLeave);\n };\n }, [isLoaded, map, layerId, onClick, onMouseEnter, onMouseLeave, interactive]);\n\n return null;\n}\n","\"use client\";\n\nimport { resolveTokenColor } from \"@elabs-ai/components-tokens\";\nimport { useLayoutEffect, useState } from \"react\";\n\nimport { useMap } from \"../map-canvas/map-context\";\n\n/**\n * Resolve a semantic token (e.g. `\"--primary\"`) to a concrete color MapLibre's\n * WebGL paint can consume, re-resolving whenever the brand theme changes.\n * This is how default layer paints stay token-driven: WebGL can't read CSS\n * custom properties, so we read them at runtime off the map container.\n */\nexport function useTokenColor(name: string, fallback = \"#000000\"): string {\n const { map, themeKey } = useMap();\n const [color, setColor] = useState(fallback);\n\n useLayoutEffect(() => {\n setColor(resolveTokenColor(name, { el: map?.getContainer(), fallback }));\n }, [name, fallback, map, themeKey]);\n\n return color;\n}\n","\"use client\";\n\nimport type MapLibreGL from \"maplibre-gl\";\nimport { useEffect, useId, useMemo, useRef } from \"react\";\n\nimport { useMap } from \"../map-canvas/map-context\";\nimport { buildArcCoordinates } from \"../lib/arc-math\";\nimport { mergeHoverPaint } from \"../lib/merge-hover-paint\";\nimport { useTokenColor } from \"../lib/use-token-color\";\n\n/** A single arc to render inside `<MapArc data={...}>`. */\nexport type MapArcDatum = {\n /** Unique identifier for this arc. Required for hover state tracking and event payloads. */\n id: string | number;\n /** Start coordinate as [longitude, latitude]. */\n from: [number, number];\n /** End coordinate as [longitude, latitude]. */\n to: [number, number];\n};\n\n/** Event payload passed to MapArc interaction callbacks. */\nexport type MapArcEvent<T extends MapArcDatum = MapArcDatum> = {\n /** The arc datum that was hovered or clicked. */\n arc: T;\n /** Longitude of the cursor at the time of the event. */\n longitude: number;\n /** Latitude of the cursor at the time of the event. */\n latitude: number;\n /** The underlying MapLibre mouse event for advanced use cases. */\n originalEvent: MapLibreGL.MapMouseEvent;\n};\n\nexport type MapArcLinePaint = NonNullable<MapLibreGL.LineLayerSpecification[\"paint\"]>;\nexport type MapArcLineLayout = NonNullable<MapLibreGL.LineLayerSpecification[\"layout\"]>;\n\nexport type MapArcProps<T extends MapArcDatum = MapArcDatum> = {\n /** Array of arcs to render. Each arc must have a unique `id`. */\n data: T[];\n /** Optional unique identifier prefix for the arc source/layers. Auto-generated if not provided. */\n id?: string;\n /**\n * How far each arc bows away from a straight line. `0` renders straight\n * lines; higher values bend further; negative values bend to the opposite\n * side. Arcs cross the antimeridian via the shorter direction. (default: 0.2)\n */\n curvature?: number;\n /** Number of samples used to render each curve. Higher = smoother. (default: 64) */\n samples?: number;\n /**\n * MapLibre paint properties for the arc layer. Merged on top of theme-aware\n * defaults (`line-color` = the `--primary` token, `line-width: 2`,\n * `line-opacity: 0.85`). Any value can be a MapLibre expression for\n * per-feature styling; every field on each arc datum (besides `from`/`to`)\n * is exposed via `[\"get\", ...]`.\n */\n paint?: MapArcLinePaint;\n /** MapLibre layout properties for the arc layer. Defaults to rounded joins/caps. */\n layout?: MapArcLineLayout;\n /**\n * Paint properties applied to the arc currently under the cursor. Each key\n * is merged into `paint` as a `case` expression keyed on per-feature hover\n * state, so only the hovered arc changes appearance.\n */\n hoverPaint?: MapArcLinePaint;\n /** Callback when an arc is clicked. */\n onClick?: (e: MapArcEvent<T>) => void;\n /**\n * Callback fired when the hovered arc changes. Receives the cursor's\n * lng/lat at the moment of entry, and `null` when the cursor leaves the\n * last hovered arc.\n */\n onHover?: (e: MapArcEvent<T> | null) => void;\n /** Whether arcs respond to mouse events (default: true). */\n interactive?: boolean;\n /** Optional MapLibre layer id to insert the arc layers before (z-order control). */\n beforeId?: string;\n};\n\nconst DEFAULT_ARC_CURVATURE = 0.2;\nconst DEFAULT_ARC_SAMPLES = 64;\nconst ARC_HIT_MIN_WIDTH = 12;\nconst ARC_HIT_PADDING = 6;\n\nconst DEFAULT_ARC_LAYOUT: MapArcLineLayout = {\n \"line-join\": \"round\",\n \"line-cap\": \"round\",\n};\n\n/**\n * Curved great-circle-style arcs between coordinate pairs (flight paths,\n * network links). Pairs well with `<MapCanvas blank projection={{ type: \"globe\" }}>`.\n */\nexport function MapArc<T extends MapArcDatum = MapArcDatum>({\n data,\n id: propId,\n curvature = DEFAULT_ARC_CURVATURE,\n samples = DEFAULT_ARC_SAMPLES,\n paint,\n layout,\n hoverPaint,\n onClick,\n onHover,\n interactive = true,\n beforeId,\n}: MapArcProps<T>) {\n const { map, isLoaded } = useMap();\n const autoId = useId();\n const id = propId ?? autoId;\n const sourceId = `arc-source-${id}`;\n const layerId = `arc-layer-${id}`;\n const hitLayerId = `arc-hit-layer-${id}`;\n\n const primary = useTokenColor(\"--primary\");\n\n const mergedPaint = useMemo(\n () =>\n mergeHoverPaint(\n { \"line-color\": primary, \"line-width\": 2, \"line-opacity\": 0.85, ...paint },\n hoverPaint,\n ),\n [primary, paint, hoverPaint],\n );\n const mergedLayout = useMemo(() => ({ ...DEFAULT_ARC_LAYOUT, ...layout }), [layout]);\n\n const hitWidth = useMemo(() => {\n const w = paint?.[\"line-width\"] ?? 2;\n const base = typeof w === \"number\" ? w : ARC_HIT_MIN_WIDTH;\n return Math.max(base + ARC_HIT_PADDING, ARC_HIT_MIN_WIDTH);\n }, [paint]);\n\n const geoJSON = useMemo<GeoJSON.FeatureCollection<GeoJSON.LineString>>(\n () => ({\n type: \"FeatureCollection\",\n features: data.map((arc) => {\n const { from, to, ...properties } = arc;\n return {\n type: \"Feature\",\n properties,\n geometry: {\n type: \"LineString\",\n coordinates: buildArcCoordinates(from, to, curvature, samples),\n },\n };\n }),\n }),\n [data, curvature, samples],\n );\n\n const latestRef = useRef({ data, onClick, onHover });\n latestRef.current = { data, onClick, onHover };\n\n // Add source and layers on mount. The invisible hit layer widens the\n // pointer target so thin arcs stay hoverable/clickable.\n useEffect(() => {\n if (!isLoaded || !map) return;\n\n map.addSource(sourceId, {\n type: \"geojson\",\n data: geoJSON,\n promoteId: \"id\",\n });\n\n map.addLayer(\n {\n id: hitLayerId,\n type: \"line\",\n source: sourceId,\n layout: DEFAULT_ARC_LAYOUT,\n paint: {\n \"line-color\": \"transparent\",\n \"line-width\": hitWidth,\n \"line-opacity\": 1,\n },\n },\n beforeId,\n );\n\n map.addLayer(\n {\n id: layerId,\n type: \"line\",\n source: sourceId,\n layout: mergedLayout,\n paint: mergedPaint,\n },\n beforeId,\n );\n\n return () => {\n try {\n if (map.getLayer(layerId)) map.removeLayer(layerId);\n if (map.getLayer(hitLayerId)) map.removeLayer(hitLayerId);\n if (map.getSource(sourceId)) map.removeSource(sourceId);\n } catch {\n // style may be mid-reload\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- source/layers created once per map; data + paint are synced by the effects below\n }, [isLoaded, map]);\n\n // Sync features when data / curvature / samples change.\n useEffect(() => {\n if (!isLoaded || !map) return;\n const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource | undefined;\n source?.setData(geoJSON);\n }, [isLoaded, map, geoJSON, sourceId]);\n\n // Sync paint/layout when they change.\n useEffect(() => {\n if (!isLoaded || !map || !map.getLayer(layerId)) return;\n for (const [key, value] of Object.entries(mergedPaint)) {\n map.setPaintProperty(layerId, key as keyof MapArcLinePaint, value as never);\n }\n for (const [key, value] of Object.entries(mergedLayout)) {\n map.setLayoutProperty(layerId, key as keyof MapArcLineLayout, value as never);\n }\n if (map.getLayer(hitLayerId)) {\n map.setPaintProperty(hitLayerId, \"line-width\", hitWidth);\n }\n }, [isLoaded, map, layerId, hitLayerId, mergedPaint, mergedLayout, hitWidth]);\n\n // Interaction handlers (bound to the hit layer).\n useEffect(() => {\n if (!isLoaded || !map || !interactive) return;\n\n let hoveredId: string | number | null = null;\n\n const setHover = (next: string | number | null) => {\n if (next === hoveredId) return;\n const sourceExists = !!map.getSource(sourceId);\n if (hoveredId != null && sourceExists) {\n map.setFeatureState({ source: sourceId, id: hoveredId }, { hover: false });\n }\n hoveredId = next;\n if (next != null && sourceExists) {\n map.setFeatureState({ source: sourceId, id: next }, { hover: true });\n }\n };\n\n const findArc = (featureId: string | number | undefined) =>\n featureId == null\n ? undefined\n : latestRef.current.data.find((arc) => String(arc.id) === String(featureId));\n\n const handleMouseMove = (e: MapLibreGL.MapLayerMouseEvent) => {\n const featureId = e.features?.[0]?.id as string | number | undefined;\n if (featureId == null || featureId === hoveredId) return;\n\n setHover(featureId);\n map.getCanvas().style.cursor = \"pointer\";\n\n const arc = findArc(featureId);\n if (arc) {\n latestRef.current.onHover?.({\n arc: arc as T,\n longitude: e.lngLat.lng,\n latitude: e.lngLat.lat,\n originalEvent: e,\n });\n }\n };\n\n const handleMouseLeave = () => {\n setHover(null);\n map.getCanvas().style.cursor = \"\";\n latestRef.current.onHover?.(null);\n };\n\n const handleClick = (e: MapLibreGL.MapLayerMouseEvent) => {\n const arc = findArc(e.features?.[0]?.id as string | number | undefined);\n if (!arc) return;\n latestRef.current.onClick?.({\n arc: arc as T,\n longitude: e.lngLat.lng,\n latitude: e.lngLat.lat,\n originalEvent: e,\n });\n };\n\n map.on(\"mousemove\", hitLayerId, handleMouseMove);\n map.on(\"mouseleave\", hitLayerId, handleMouseLeave);\n map.on(\"click\", hitLayerId, handleClick);\n\n return () => {\n map.off(\"mousemove\", hitLayerId, handleMouseMove);\n map.off(\"mouseleave\", hitLayerId, handleMouseLeave);\n map.off(\"click\", hitLayerId, handleClick);\n setHover(null);\n map.getCanvas().style.cursor = \"\";\n };\n }, [isLoaded, map, hitLayerId, sourceId, interactive]);\n\n return null;\n}\n","/** Pure geometry for `<MapArc>` — kept engine-free so it can be unit-tested. */\n\n/**\n * Sample a quadratic Bézier between `from` and `to` in lng/lat space.\n * `curvature` is how far the arc bows away from a straight line (0 = straight;\n * negative bends to the opposite side). The destination longitude is unwrapped\n * relative to the origin so arcs cross the antimeridian via the shorter\n * great-circle direction — resulting longitudes may fall outside [-180, 180],\n * which MapLibre renders correctly on the globe projection.\n */\nexport function buildArcCoordinates(\n from: [number, number],\n to: [number, number],\n curvature: number,\n samples: number,\n): [number, number][] {\n const [x0, y0] = from;\n const [xTo, y2] = to;\n const rawDx = xTo - x0;\n const x2 = rawDx > 180 ? xTo - 360 : rawDx < -180 ? xTo + 360 : xTo;\n const dx = x2 - x0;\n const dy = y2 - y0;\n const distance = Math.hypot(dx, dy);\n\n if (distance === 0 || curvature === 0) return [from, [x2, y2]];\n\n const mx = (x0 + x2) / 2;\n const my = (y0 + y2) / 2;\n const nx = -dy / distance;\n const ny = dx / distance;\n const offset = distance * curvature;\n const cx = mx + nx * offset;\n const cy = my + ny * offset;\n\n const points: [number, number][] = [];\n const segments = Math.max(2, Math.floor(samples));\n for (let i = 0; i <= segments; i += 1) {\n const t = i / segments;\n const inv = 1 - t;\n const x = inv * inv * x0 + 2 * inv * t * cx + t * t * x2;\n const y = inv * inv * y0 + 2 * inv * t * cy + t * t * y2;\n points.push([x, y]);\n }\n return points;\n}\n","/**\n * Merge hover paint into base paint as MapLibre `case` expressions keyed on\n * the per-feature `hover` feature-state, so only the hovered feature changes\n * appearance.\n */\nexport function mergeHoverPaint<T extends Record<string, unknown>>(\n paint: T,\n hoverPaint: T | undefined,\n): T {\n if (!hoverPaint) return paint;\n const merged: Record<string, unknown> = { ...paint };\n for (const [key, hoverValue] of Object.entries(hoverPaint)) {\n if (hoverValue === undefined) continue;\n const baseValue = merged[key];\n merged[key] =\n baseValue === undefined\n ? hoverValue\n : [\"case\", [\"boolean\", [\"feature-state\", \"hover\"], false], hoverValue, baseValue];\n }\n return merged as T;\n}\n","\"use client\";\n\nimport type MapLibreGL from \"maplibre-gl\";\nimport { useEffect, useId, useMemo, useRef } from \"react\";\n\nimport { useMap } from \"../map-canvas/map-context\";\nimport { mergeHoverPaint } from \"../lib/merge-hover-paint\";\nimport { useTokenColor } from \"../lib/use-token-color\";\n\nexport type MapGeoJSONData<P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties> =\n | GeoJSON.FeatureCollection<GeoJSON.Geometry, P>\n | GeoJSON.Feature<GeoJSON.Geometry, P>\n | GeoJSON.Geometry\n | string;\n\nexport type MapFillPaint = NonNullable<MapLibreGL.FillLayerSpecification[\"paint\"]>;\nexport type MapLinePaint = NonNullable<MapLibreGL.LineLayerSpecification[\"paint\"]>;\n\n/** A rendered feature with strongly-typed `properties`. */\nexport type MapGeoJSONFeature<P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties> =\n Omit<MapLibreGL.MapGeoJSONFeature, \"properties\"> & { properties: P };\n\n/** Event payload passed to MapGeoJSON interaction callbacks. */\nexport type MapGeoJSONEvent<P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties> = {\n /** The feature under the cursor, with its typed GeoJSON properties. */\n feature: MapGeoJSONFeature<P>;\n /** Longitude of the cursor at the time of the event. */\n longitude: number;\n /** Latitude of the cursor at the time of the event. */\n latitude: number;\n /** The underlying MapLibre mouse event for advanced use cases. */\n originalEvent: MapLibreGL.MapLayerMouseEvent;\n};\n\nexport type MapGeoJSONProps<P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties> = {\n /** GeoJSON data (FeatureCollection, Feature, Geometry) or a URL to fetch it from. */\n data: MapGeoJSONData<P>;\n /** Optional unique identifier prefix for the source/layers. Auto-generated if not provided. */\n id?: string;\n /**\n * Feature property to promote to the feature `id`. Required for hover\n * feature-state (`fillHoverPaint`) and stable `onHover`/`onClick` payloads.\n */\n promoteId?: string;\n /**\n * Paint for the polygon fill layer. Merged on top of a theme-aware neutral\n * default (the `--border` token — a mid neutral that reads on the page\n * surface in every theme). Pass `false` to omit the fill layer entirely\n * (e.g. outlines only).\n */\n fillPaint?: MapFillPaint | false;\n /**\n * Paint for the outline layer. Merged on top of a hairline default\n * (`line-color` = the `--background` token, `line-width` = 0.5) for thin\n * separators. Override `line-color` if your container differs, or pass\n * `false` to omit the layer.\n */\n linePaint?: MapLinePaint | false;\n /**\n * Paint merged onto the fill layer for the feature under the cursor, applied\n * as a `case` expression keyed on hover feature-state. Requires `promoteId`.\n */\n fillHoverPaint?: MapFillPaint;\n /** Callback when a feature is clicked. */\n onClick?: (e: MapGeoJSONEvent<P>) => void;\n /** Callback fired when the hovered feature changes; `null` when the cursor leaves. */\n onHover?: (e: MapGeoJSONEvent<P> | null) => void;\n /** Whether features respond to mouse events (default: false). */\n interactive?: boolean;\n /** Optional MapLibre layer id to insert the layers before (z-order control). */\n beforeId?: string;\n};\n\n/**\n * Renders arbitrary GeoJSON as fill + outline layers on the map. Composes like\n * `MapRoute` / `MapArc` — drop it inside `<MapCanvas>` (typically with `blank`)\n * for choropleths and region/data maps. For full control over expressions and\n * multiple layers, manage layers directly via `useMap()` instead.\n */\nexport function MapGeoJSON<P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties>({\n data,\n id: propId,\n promoteId,\n fillPaint,\n linePaint,\n fillHoverPaint,\n onClick,\n onHover,\n interactive = false,\n beforeId,\n}: MapGeoJSONProps<P>) {\n const { map, isLoaded } = useMap();\n const autoId = useId();\n const id = propId ?? autoId;\n const sourceId = `geojson-source-${id}`;\n const fillLayerId = `geojson-fill-${id}`;\n const lineLayerId = `geojson-line-${id}`;\n\n // Theme-driven neutral defaults: landmass = the mid-neutral `--border` rung,\n // separators = the page surface. Both re-resolve on theme change.\n const defaultFill = useTokenColor(\"--border\");\n const defaultLine = useTokenColor(\"--background\");\n\n const showFill = fillPaint !== false;\n const showLine = linePaint !== false;\n\n const mergedFillPaint = useMemo(\n () => mergeHoverPaint({ \"fill-color\": defaultFill, ...(fillPaint || {}) }, fillHoverPaint),\n [defaultFill, fillPaint, fillHoverPaint],\n );\n const mergedLinePaint = useMemo(\n () => ({\n \"line-color\": defaultLine,\n \"line-width\": 0.5,\n ...(linePaint || {}),\n }),\n [defaultLine, linePaint],\n );\n const latestRef = useRef({ onClick, onHover });\n latestRef.current = { onClick, onHover };\n\n // Add source on mount.\n useEffect(() => {\n if (!isLoaded || !map) return;\n\n map.addSource(sourceId, {\n type: \"geojson\",\n data,\n ...(promoteId ? { promoteId } : {}),\n });\n\n return () => {\n try {\n if (map.getLayer(lineLayerId)) map.removeLayer(lineLayerId);\n if (map.getLayer(fillLayerId)) map.removeLayer(fillLayerId);\n if (map.getSource(sourceId)) map.removeSource(sourceId);\n } catch {\n // style may be mid-reload\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- source created once per map; data/layers are synced by the effects below\n }, [isLoaded, map]);\n\n // Sync data when it changes.\n useEffect(() => {\n if (!isLoaded || !map) return;\n const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource | undefined;\n source?.setData(data as never);\n }, [isLoaded, map, data, sourceId]);\n\n // Sync layers and paint when visibility or styling changes.\n useEffect(() => {\n if (!isLoaded || !map) return;\n\n const source = map.getSource(sourceId);\n if (!source) return;\n\n if (showFill && !map.getLayer(fillLayerId)) {\n map.addLayer(\n {\n id: fillLayerId,\n type: \"fill\",\n source: sourceId,\n paint: mergedFillPaint,\n },\n beforeId,\n );\n } else if (!showFill && map.getLayer(fillLayerId)) {\n map.removeLayer(fillLayerId);\n }\n\n if (showLine && !map.getLayer(lineLayerId)) {\n map.addLayer(\n {\n id: lineLayerId,\n type: \"line\",\n source: sourceId,\n paint: mergedLinePaint,\n },\n beforeId,\n );\n } else if (!showLine && map.getLayer(lineLayerId)) {\n map.removeLayer(lineLayerId);\n }\n\n if (showFill && map.getLayer(fillLayerId)) {\n for (const [key, value] of Object.entries(mergedFillPaint)) {\n map.setPaintProperty(fillLayerId, key as keyof MapFillPaint, value as never);\n }\n }\n if (showLine && map.getLayer(lineLayerId)) {\n for (const [key, value] of Object.entries(mergedLinePaint)) {\n map.setPaintProperty(lineLayerId, key as keyof MapLinePaint, value as never);\n }\n }\n }, [\n isLoaded,\n map,\n sourceId,\n fillLayerId,\n lineLayerId,\n showFill,\n showLine,\n mergedFillPaint,\n mergedLinePaint,\n beforeId,\n ]);\n\n // Interaction handlers (bound to the fill layer).\n useEffect(() => {\n if (!isLoaded || !map || !interactive || !showFill) return;\n\n let hoveredId: string | number | null = null;\n\n const setHover = (next: string | number | null) => {\n if (next === hoveredId) return;\n const sourceExists = !!map.getSource(sourceId);\n if (hoveredId != null && sourceExists) {\n map.setFeatureState({ source: sourceId, id: hoveredId }, { hover: false });\n }\n hoveredId = next;\n if (next != null && sourceExists) {\n map.setFeatureState({ source: sourceId, id: next }, { hover: true });\n }\n };\n\n const handleMouseMove = (e: MapLibreGL.MapLayerMouseEvent) => {\n const feature = e.features?.[0];\n if (!feature) return;\n map.getCanvas().style.cursor = \"pointer\";\n\n const featureId = feature.id;\n if (featureId === hoveredId) return;\n setHover(featureId ?? null);\n latestRef.current.onHover?.({\n feature: feature as unknown as MapGeoJSONFeature<P>,\n longitude: e.lngLat.lng,\n latitude: e.lngLat.lat,\n originalEvent: e,\n });\n };\n\n const handleMouseLeave = () => {\n setHover(null);\n map.getCanvas().style.cursor = \"\";\n latestRef.current.onHover?.(null);\n };\n\n const handleClick = (e: MapLibreGL.MapLayerMouseEvent) => {\n const feature = e.features?.[0];\n if (!feature) return;\n latestRef.current.onClick?.({\n feature: feature as unknown as MapGeoJSONFeature<P>,\n longitude: e.lngLat.lng,\n latitude: e.lngLat.lat,\n originalEvent: e,\n });\n };\n\n map.on(\"mousemove\", fillLayerId, handleMouseMove);\n map.on(\"mouseleave\", fillLayerId, handleMouseLeave);\n map.on(\"click\", fillLayerId, handleClick);\n\n return () => {\n map.off(\"mousemove\", fillLayerId, handleMouseMove);\n map.off(\"mouseleave\", fillLayerId, handleMouseLeave);\n map.off(\"click\", fillLayerId, handleClick);\n setHover(null);\n map.getCanvas().style.cursor = \"\";\n };\n }, [isLoaded, map, fillLayerId, sourceId, interactive, showFill]);\n\n return null;\n}\n","\"use client\";\n\nimport type MapLibreGL from \"maplibre-gl\";\nimport { useEffect, useId, useMemo } from \"react\";\n\nimport { useMap } from \"../map-canvas/map-context\";\nimport { useTokenColor } from \"../lib/use-token-color\";\n\nexport type MapClusterLayerProps<P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties> =\n {\n /** GeoJSON FeatureCollection data or a URL to fetch GeoJSON from. */\n data: string | GeoJSON.FeatureCollection<GeoJSON.Point, P>;\n /** Maximum zoom level to cluster points on (default: 14). */\n clusterMaxZoom?: number;\n /** Radius of each cluster when clustering points, in pixels (default: 50). */\n clusterRadius?: number;\n /**\n * Colors for cluster circles: [small, medium, large] based on point count.\n * Defaults to the theme's `--success`/`--warning`/`--destructive` tokens.\n */\n clusterColors?: [string, string, string];\n /** Point-count thresholds for the color/size steps: [medium, large] (default: [100, 750]). */\n clusterThresholds?: [number, number];\n /** Color for unclustered individual points. Defaults to the theme's `--primary` token. */\n pointColor?: string;\n /** Callback when an unclustered point is clicked. */\n onPointClick?: (\n feature: GeoJSON.Feature<GeoJSON.Point, P>,\n coordinates: [number, number],\n ) => void;\n /** Callback when a cluster is clicked. If not provided, zooms into the cluster. */\n onClusterClick?: (clusterId: number, coordinates: [number, number], pointCount: number) => void;\n };\n\nconst DEFAULT_CLUSTER_THRESHOLDS: [number, number] = [100, 750];\n\n/**\n * Clustered point rendering for large point datasets. Cluster circles step\n * through the status tokens (success → warning → destructive) as the point\n * count grows; strokes and count labels use the page surface for contrast.\n */\nexport function MapClusterLayer<P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties>({\n data,\n clusterMaxZoom = 14,\n clusterRadius = 50,\n clusterColors,\n clusterThresholds = DEFAULT_CLUSTER_THRESHOLDS,\n pointColor,\n onPointClick,\n onClusterClick,\n}: MapClusterLayerProps<P>) {\n const { map, isLoaded } = useMap();\n const id = useId();\n const sourceId = `cluster-source-${id}`;\n const clusterLayerId = `clusters-${id}`;\n const clusterCountLayerId = `cluster-count-${id}`;\n const unclusteredLayerId = `unclustered-point-${id}`;\n\n const success = useTokenColor(\"--success\");\n const warning = useTokenColor(\"--warning\");\n const destructive = useTokenColor(\"--destructive\");\n const primary = useTokenColor(\"--primary\");\n const surface = useTokenColor(\"--background\");\n\n const resolvedClusterColors = useMemo<[string, string, string]>(\n () => clusterColors ?? [success, warning, destructive],\n [clusterColors, success, warning, destructive],\n );\n const resolvedPointColor = pointColor ?? primary;\n\n // Add source and layers on mount.\n useEffect(() => {\n if (!isLoaded || !map) return;\n\n map.addSource(sourceId, {\n type: \"geojson\",\n data,\n cluster: true,\n clusterMaxZoom,\n clusterRadius,\n });\n\n map.addLayer({\n id: clusterLayerId,\n type: \"circle\",\n source: sourceId,\n filter: [\"has\", \"point_count\"],\n paint: {\n \"circle-color\": [\n \"step\",\n [\"get\", \"point_count\"],\n resolvedClusterColors[0],\n clusterThresholds[0],\n resolvedClusterColors[1],\n clusterThresholds[1],\n resolvedClusterColors[2],\n ],\n \"circle-radius\": [\n \"step\",\n [\"get\", \"point_count\"],\n 20,\n clusterThresholds[0],\n 30,\n clusterThresholds[1],\n 40,\n ],\n \"circle-stroke-width\": 1,\n \"circle-stroke-color\": surface,\n \"circle-opacity\": 0.85,\n },\n });\n\n map.addLayer({\n id: clusterCountLayerId,\n type: \"symbol\",\n source: sourceId,\n filter: [\"has\", \"point_count\"],\n layout: {\n \"text-field\": \"{point_count_abbreviated}\",\n \"text-font\": [\"Open Sans\"],\n \"text-size\": 12,\n },\n paint: {\n \"text-color\": surface,\n },\n });\n\n map.addLayer({\n id: unclusteredLayerId,\n type: \"circle\",\n source: sourceId,\n filter: [\"!\", [\"has\", \"point_count\"]],\n paint: {\n \"circle-color\": resolvedPointColor,\n \"circle-radius\": 5,\n \"circle-stroke-width\": 2,\n \"circle-stroke-color\": surface,\n },\n });\n\n return () => {\n try {\n if (map.getLayer(clusterCountLayerId)) map.removeLayer(clusterCountLayerId);\n if (map.getLayer(unclusteredLayerId)) map.removeLayer(unclusteredLayerId);\n if (map.getLayer(clusterLayerId)) map.removeLayer(clusterLayerId);\n if (map.getSource(sourceId)) map.removeSource(sourceId);\n } catch {\n // style may be mid-reload\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- source/layers created once per map; data + paint are synced by the effects below\n }, [isLoaded, map, sourceId]);\n\n // Update source data when the data prop changes (only for non-URL data).\n useEffect(() => {\n if (!isLoaded || !map || typeof data === \"string\") return;\n\n const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource;\n if (source) {\n source.setData(data);\n }\n }, [isLoaded, map, data, sourceId]);\n\n // Sync layer styles when props (or the resolved theme colors) change.\n useEffect(() => {\n if (!isLoaded || !map) return;\n\n if (map.getLayer(clusterLayerId)) {\n map.setPaintProperty(clusterLayerId, \"circle-color\", [\n \"step\",\n [\"get\", \"point_count\"],\n resolvedClusterColors[0],\n clusterThresholds[0],\n resolvedClusterColors[1],\n clusterThresholds[1],\n resolvedClusterColors[2],\n ]);\n map.setPaintProperty(clusterLayerId, \"circle-radius\", [\n \"step\",\n [\"get\", \"point_count\"],\n 20,\n clusterThresholds[0],\n 30,\n clusterThresholds[1],\n 40,\n ]);\n map.setPaintProperty(clusterLayerId, \"circle-stroke-color\", surface);\n }\n\n if (map.getLayer(clusterCountLayerId)) {\n map.setPaintProperty(clusterCountLayerId, \"text-color\", surface);\n }\n\n if (map.getLayer(unclusteredLayerId)) {\n map.setPaintProperty(unclusteredLayerId, \"circle-color\", resolvedPointColor);\n map.setPaintProperty(unclusteredLayerId, \"circle-stroke-color\", surface);\n }\n }, [\n isLoaded,\n map,\n clusterLayerId,\n clusterCountLayerId,\n unclusteredLayerId,\n resolvedClusterColors,\n clusterThresholds,\n resolvedPointColor,\n surface,\n ]);\n\n // Handle click events.\n useEffect(() => {\n if (!isLoaded || !map) return;\n\n // Cluster click handler — zoom into the cluster by default.\n const handleClusterClick = async (e: MapLibreGL.MapMouseEvent) => {\n const features = map.queryRenderedFeatures(e.point, {\n layers: [clusterLayerId],\n });\n const feature = features[0];\n if (!feature) return;\n const clusterId = feature.properties?.cluster_id as number;\n const pointCount = feature.properties?.point_count as number;\n const coordinates = (feature.geometry as GeoJSON.Point).coordinates as [number, number];\n\n if (onClusterClick) {\n onClusterClick(clusterId, coordinates, pointCount);\n } else {\n const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource;\n const zoom = await source.getClusterExpansionZoom(clusterId);\n map.easeTo({\n center: coordinates,\n zoom,\n });\n }\n };\n\n // Unclustered point click handler.\n const handlePointClick = (\n e: MapLibreGL.MapMouseEvent & {\n features?: MapLibreGL.MapGeoJSONFeature[];\n },\n ) => {\n const feature = e.features?.[0];\n if (!onPointClick || !feature) return;\n const coordinates = (feature.geometry as GeoJSON.Point).coordinates.slice() as [\n number,\n number,\n ];\n\n // Handle world copies.\n while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {\n coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;\n }\n\n onPointClick(feature as unknown as GeoJSON.Feature<GeoJSON.Point, P>, coordinates);\n };\n\n // Cursor style handlers.\n const handleMouseEnterCluster = () => {\n map.getCanvas().style.cursor = \"pointer\";\n };\n const handleMouseLeaveCluster = () => {\n map.getCanvas().style.cursor = \"\";\n };\n const handleMouseEnterPoint = () => {\n if (onPointClick) {\n map.getCanvas().style.cursor = \"pointer\";\n }\n };\n const handleMouseLeavePoint = () => {\n map.getCanvas().style.cursor = \"\";\n };\n\n map.on(\"click\", clusterLayerId, handleClusterClick);\n map.on(\"click\", unclusteredLayerId, handlePointClick);\n map.on(\"mouseenter\", clusterLayerId, handleMouseEnterCluster);\n map.on(\"mouseleave\", clusterLayerId, handleMouseLeaveCluster);\n map.on(\"mouseenter\", unclusteredLayerId, handleMouseEnterPoint);\n map.on(\"mouseleave\", unclusteredLayerId, handleMouseLeavePoint);\n\n return () => {\n map.off(\"click\", clusterLayerId, handleClusterClick);\n map.off(\"click\", unclusteredLayerId, handlePointClick);\n map.off(\"mouseenter\", clusterLayerId, handleMouseEnterCluster);\n map.off(\"mouseleave\", clusterLayerId, handleMouseLeaveCluster);\n map.off(\"mouseenter\", unclusteredLayerId, handleMouseEnterPoint);\n map.off(\"mouseleave\", unclusteredLayerId, handleMouseLeavePoint);\n };\n }, [isLoaded, map, clusterLayerId, unclusteredLayerId, sourceId, onClusterClick, onPointClick]);\n\n return null;\n}\n"],"mappings":"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","names":["useEffect","useState","MapCanvas","useState","useEffect","MapLibreGL","createContext","use","useEffect","useMemo","useRef","cn","jsx","jsxs","createContext","use","useRef","useMemo","MapLibreGL","useEffect","cn","positionClasses","MapLibreGL","useEffect","useMemo","useRef","createPortal","X","cn","jsx","jsxs","useRef","useMemo","MapLibreGL","useEffect","createPortal","cn","X","useCallback","useEffect","useRef","useState","Spinner","cn","jsx","jsxs","cn","useState","useCallback","Spinner","useRef","useEffect","useEffect","useState","useState","useEffect","useEffect","useId","useMemo","useRef","useId","useMemo","useRef","useEffect","useEffect","useId","useMemo","useRef","useId","useMemo","useRef","useEffect","useEffect","useId","useMemo","useId","useMemo","useEffect"]}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@elabs-ai/components-maps",
3
+ "version": "4.0.0",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/mreimitz/elabs-components.git",
8
+ "directory": "packages/maps"
9
+ },
10
+ "type": "module",
11
+ "files": [
12
+ "dist",
13
+ "src"
14
+ ],
15
+ "sideEffects": [
16
+ "**/*.css"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "publishConfig": {
25
+ "access": "public",
26
+ "registry": "https://registry.npmjs.org/"
27
+ },
28
+ "dependencies": {
29
+ "lucide-react": "^0.577.0"
30
+ },
31
+ "peerDependencies": {
32
+ "maplibre-gl": "^5.15.0",
33
+ "react": "^18.2.0 || ^19.0.0",
34
+ "react-dom": "^18.2.0 || ^19.0.0",
35
+ "@elabs-ai/components-ui": "4.0.0",
36
+ "@elabs-ai/components-tokens": "4.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@testing-library/jest-dom": "^6.6.3",
40
+ "@testing-library/react": "^16.1.0",
41
+ "@types/geojson": "^7946.0.14",
42
+ "@types/react": "^19.0.2",
43
+ "@types/react-dom": "^19.0.2",
44
+ "@vitejs/plugin-react": "^4.3.4",
45
+ "eslint": "^9.17.0",
46
+ "jsdom": "^25.0.1",
47
+ "maplibre-gl": "^5.15.0",
48
+ "react": "^19.0.0",
49
+ "react-dom": "^19.0.0",
50
+ "tsup": "^8.3.5",
51
+ "typescript": "^5.7.3",
52
+ "vitest": "^3.0.2",
53
+ "@elabs-ai/components-typescript-config": "0.1.0",
54
+ "@elabs-ai/components-eslint-config": "0.1.0",
55
+ "@elabs-ai/components-ui": "4.0.0",
56
+ "@elabs-ai/components-tokens": "4.0.0"
57
+ },
58
+ "scripts": {
59
+ "build": "tsup && node ../../scripts/link-dist-css.mjs",
60
+ "typecheck": "tsc --noEmit",
61
+ "test": "vitest run",
62
+ "test:watch": "vitest",
63
+ "lint": "eslint .",
64
+ "clean": "rm -rf dist .turbo coverage"
65
+ }
66
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @elabs-ai/components-maps — token-driven MapLibre GL map components.
3
+ *
4
+ * Adapted from mapcn (https://github.com/AnmolSaini16/mapcn, MIT License,
5
+ * © 2025 Anmoldeep Singh), re-tokenized and renamed for brand-ui: semantic
6
+ * tokens drive default layer paints, the basemap follows the active brand
7
+ * theme (`data-theme` + its own `color-scheme`), and popups/controls use brand chrome.
8
+ *
9
+ * MapLibre's stylesheet and the brand popup overrides are imported by
10
+ * `<MapCanvas>` itself — no extra CSS import is needed.
11
+ *
12
+ * `<MapCanvas>` disables MapLibre's attribution control by default (a maintainer
13
+ * decision for internal use). The default Carto basemap serves ODbL-licensed
14
+ * OpenStreetMap data that requires the credit, so a PUBLIC surface on these tiles
15
+ * must re-enable it — `attributionControl={{ compact: true }}` — or move to tiles
16
+ * licensed without the requirement via `styles` / `blank`.
17
+ */
18
+ export * from "./map-canvas";
19
+ export * from "./map-marker";
20
+ export * from "./map-popup";
21
+ export * from "./map-controls";
22
+ export * from "./map-route";
23
+ export * from "./map-arc";
24
+ export * from "./map-geojson";
25
+ export * from "./map-cluster-layer";
26
+
27
+ // Convenience re-exports so consumers can type map work without a direct
28
+ // maplibre-gl dependency.
29
+ export type {
30
+ LngLatLike,
31
+ LngLatBoundsLike,
32
+ StyleSpecification,
33
+ ProjectionSpecification,
34
+ MapOptions,
35
+ MapMouseEvent,
36
+ MapLayerMouseEvent,
37
+ } from "maplibre-gl";
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { buildArcCoordinates } from "./arc-math";
4
+
5
+ describe("buildArcCoordinates", () => {
6
+ it("returns a straight two-point line when curvature is 0", () => {
7
+ expect(buildArcCoordinates([0, 0], [10, 10], 0, 64)).toEqual([
8
+ [0, 0],
9
+ [10, 10],
10
+ ]);
11
+ });
12
+
13
+ it("returns a straight line for identical endpoints", () => {
14
+ expect(buildArcCoordinates([5, 5], [5, 5], 0.3, 64)).toEqual([
15
+ [5, 5],
16
+ [5, 5],
17
+ ]);
18
+ });
19
+
20
+ it("samples `samples + 1` points for a curved arc, anchored at both ends", () => {
21
+ const points = buildArcCoordinates([0, 0], [20, 0], 0.2, 16);
22
+ expect(points).toHaveLength(17);
23
+ expect(points[0]).toEqual([0, 0]);
24
+ expect(points.at(-1)).toEqual([20, 0]);
25
+ // The midpoint bows away from the straight line.
26
+ const mid = points[8]!;
27
+ expect(Math.abs(mid[1])).toBeGreaterThan(0);
28
+ });
29
+
30
+ it("unwraps the destination longitude across the antimeridian (Tokyo → SF)", () => {
31
+ const points = buildArcCoordinates([139.7, 35.7], [-122.4, 37.8], 0, 64);
32
+ // -122.4 unwraps to 237.6 so the arc crosses the Pacific, not the Atlantic.
33
+ expect(points.at(-1)![0]).toBeCloseTo(237.6, 5);
34
+ });
35
+
36
+ it("bends to the opposite side with negative curvature", () => {
37
+ const up = buildArcCoordinates([0, 0], [20, 0], 0.2, 8);
38
+ const down = buildArcCoordinates([0, 0], [20, 0], -0.2, 8);
39
+ expect(Math.sign(up[4]![1])).toBe(-Math.sign(down[4]![1]));
40
+ });
41
+ });
@@ -0,0 +1,45 @@
1
+ /** Pure geometry for `<MapArc>` — kept engine-free so it can be unit-tested. */
2
+
3
+ /**
4
+ * Sample a quadratic Bézier between `from` and `to` in lng/lat space.
5
+ * `curvature` is how far the arc bows away from a straight line (0 = straight;
6
+ * negative bends to the opposite side). The destination longitude is unwrapped
7
+ * relative to the origin so arcs cross the antimeridian via the shorter
8
+ * great-circle direction — resulting longitudes may fall outside [-180, 180],
9
+ * which MapLibre renders correctly on the globe projection.
10
+ */
11
+ export function buildArcCoordinates(
12
+ from: [number, number],
13
+ to: [number, number],
14
+ curvature: number,
15
+ samples: number,
16
+ ): [number, number][] {
17
+ const [x0, y0] = from;
18
+ const [xTo, y2] = to;
19
+ const rawDx = xTo - x0;
20
+ const x2 = rawDx > 180 ? xTo - 360 : rawDx < -180 ? xTo + 360 : xTo;
21
+ const dx = x2 - x0;
22
+ const dy = y2 - y0;
23
+ const distance = Math.hypot(dx, dy);
24
+
25
+ if (distance === 0 || curvature === 0) return [from, [x2, y2]];
26
+
27
+ const mx = (x0 + x2) / 2;
28
+ const my = (y0 + y2) / 2;
29
+ const nx = -dy / distance;
30
+ const ny = dx / distance;
31
+ const offset = distance * curvature;
32
+ const cx = mx + nx * offset;
33
+ const cy = my + ny * offset;
34
+
35
+ const points: [number, number][] = [];
36
+ const segments = Math.max(2, Math.floor(samples));
37
+ for (let i = 0; i <= segments; i += 1) {
38
+ const t = i / segments;
39
+ const inv = 1 - t;
40
+ const x = inv * inv * x0 + 2 * inv * t * cx + t * t * x2;
41
+ const y = inv * inv * y0 + 2 * inv * t * cy + t * t * y2;
42
+ points.push([x, y]);
43
+ }
44
+ return points;
45
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { mergeHoverPaint } from "./merge-hover-paint";
4
+
5
+ describe("mergeHoverPaint", () => {
6
+ it("returns the base paint untouched without hover paint", () => {
7
+ const paint = { "line-color": "#111111", "line-width": 2 };
8
+ expect(mergeHoverPaint(paint, undefined)).toBe(paint);
9
+ });
10
+
11
+ it("wraps overlapping keys in a hover feature-state case expression", () => {
12
+ const merged = mergeHoverPaint({ "line-color": "#111111" }, { "line-color": "#222222" });
13
+ expect(merged["line-color"]).toEqual([
14
+ "case",
15
+ ["boolean", ["feature-state", "hover"], false],
16
+ "#222222",
17
+ "#111111",
18
+ ]);
19
+ });
20
+
21
+ it("passes through hover-only keys directly", () => {
22
+ const merged = mergeHoverPaint<Record<string, unknown>>(
23
+ { "line-color": "#111111" },
24
+ { "line-width": 4 },
25
+ );
26
+ expect(merged["line-width"]).toBe(4);
27
+ expect(merged["line-color"]).toBe("#111111");
28
+ });
29
+
30
+ it("skips undefined hover values", () => {
31
+ const merged = mergeHoverPaint({ "line-color": "#111111" }, {
32
+ "line-color": undefined,
33
+ } as Record<string, unknown>);
34
+ expect(merged["line-color"]).toBe("#111111");
35
+ });
36
+ });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Merge hover paint into base paint as MapLibre `case` expressions keyed on
3
+ * the per-feature `hover` feature-state, so only the hovered feature changes
4
+ * appearance.
5
+ */
6
+ export function mergeHoverPaint<T extends Record<string, unknown>>(
7
+ paint: T,
8
+ hoverPaint: T | undefined,
9
+ ): T {
10
+ if (!hoverPaint) return paint;
11
+ const merged: Record<string, unknown> = { ...paint };
12
+ for (const [key, hoverValue] of Object.entries(hoverPaint)) {
13
+ if (hoverValue === undefined) continue;
14
+ const baseValue = merged[key];
15
+ merged[key] =
16
+ baseValue === undefined
17
+ ? hoverValue
18
+ : ["case", ["boolean", ["feature-state", "hover"], false], hoverValue, baseValue];
19
+ }
20
+ return merged as T;
21
+ }
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import { resolveTokenColor } from "@elabs-ai/components-tokens";
4
+ import { useLayoutEffect, useState } from "react";
5
+
6
+ import { useMap } from "../map-canvas/map-context";
7
+
8
+ /**
9
+ * Resolve a semantic token (e.g. `"--primary"`) to a concrete color MapLibre's
10
+ * WebGL paint can consume, re-resolving whenever the brand theme changes.
11
+ * This is how default layer paints stay token-driven: WebGL can't read CSS
12
+ * custom properties, so we read them at runtime off the map container.
13
+ */
14
+ export function useTokenColor(name: string, fallback = "#000000"): string {
15
+ const { map, themeKey } = useMap();
16
+ const [color, setColor] = useState(fallback);
17
+
18
+ useLayoutEffect(() => {
19
+ setColor(resolveTokenColor(name, { el: map?.getContainer(), fallback }));
20
+ }, [name, fallback, map, themeKey]);
21
+
22
+ return color;
23
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ MapArc,
3
+ type MapArcProps,
4
+ type MapArcDatum,
5
+ type MapArcEvent,
6
+ type MapArcLinePaint,
7
+ type MapArcLineLayout,
8
+ } from "./map-arc";
9
+ export { buildArcCoordinates } from "../lib/arc-math";
@@ -0,0 +1,58 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
+
4
+ import { MapCanvas } from "../map-canvas";
5
+ import { MapArc, type MapArcDatum } from "./map-arc";
6
+
7
+ type CityArc = MapArcDatum & { label: string };
8
+
9
+ // Connections out of Frankfurt.
10
+ const arcs: CityArc[] = [
11
+ { id: "fra-jfk", from: [8.6821, 50.1109], to: [-73.7781, 40.6413], label: "FRA → JFK" },
12
+ { id: "fra-nrt", from: [8.6821, 50.1109], to: [140.3929, 35.7719], label: "FRA → NRT" },
13
+ { id: "fra-gru", from: [8.6821, 50.1109], to: [-46.4731, -23.4356], label: "FRA → GRU" },
14
+ { id: "fra-sin", from: [8.6821, 50.1109], to: [103.9915, 1.3644], label: "FRA → SIN" },
15
+ { id: "fra-jnb", from: [8.6821, 50.1109], to: [28.246, -26.1392], label: "FRA → JNB" },
16
+ ];
17
+
18
+ const meta = {
19
+ title: "Maps/MapArc",
20
+ component: MapArc,
21
+ tags: ["autodocs"],
22
+ parameters: { layout: "fullscreen" },
23
+ } satisfies Meta<typeof MapArc>;
24
+ export default meta;
25
+ type Story = StoryObj<typeof meta>;
26
+
27
+ /** Default paint follows the theme's `--primary` token. */
28
+ export const Default: Story = {
29
+ render: () => (
30
+ <div className="h-[480px]">
31
+ <MapCanvas center={[8.68, 50.11]} zoom={1.6}>
32
+ <MapArc data={arcs} />
33
+ </MapCanvas>
34
+ </div>
35
+ ),
36
+ };
37
+
38
+ function HoverableArcs() {
39
+ const [hovered, setHovered] = useState<CityArc | null>(null);
40
+ return (
41
+ <div className="relative h-[480px]">
42
+ <MapCanvas center={[8.68, 50.11]} zoom={1.6} projection={{ type: "globe" }}>
43
+ <MapArc
44
+ data={arcs}
45
+ hoverPaint={{ "line-width": 4 }}
46
+ onHover={(e) => setHovered(e?.arc ?? null)}
47
+ />
48
+ </MapCanvas>
49
+ <div className="absolute top-2 left-2 rounded-md border bg-card px-3 py-2 text-caption text-card-foreground shadow-sm">
50
+ {hovered ? hovered.label : "Hover an arc…"}
51
+ </div>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ export const GlobeWithHover: Story = {
57
+ render: () => <HoverableArcs />,
58
+ };