@djangocfg/ui-tools 2.1.334 → 2.1.336
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.
- package/README.md +68 -2
- package/dist/ChatRoot-IIYQEWUU.mjs +5 -0
- package/dist/ChatRoot-IIYQEWUU.mjs.map +1 -0
- package/dist/ChatRoot-PNNGQCYF.css +7 -0
- package/dist/ChatRoot-PNNGQCYF.css.map +1 -0
- package/dist/ChatRoot-UUKTYM4N.cjs +14 -0
- package/dist/ChatRoot-UUKTYM4N.cjs.map +1 -0
- package/dist/{CronScheduler.client-3O3VU4CI.mjs → CronScheduler.client-DLMXCPAJ.mjs} +4 -4
- package/dist/{CronScheduler.client-3O3VU4CI.mjs.map → CronScheduler.client-DLMXCPAJ.mjs.map} +1 -1
- package/dist/{CronScheduler.client-A4GO6YBY.cjs → CronScheduler.client-WEJF4PWQ.cjs} +14 -14
- package/dist/{CronScheduler.client-A4GO6YBY.cjs.map → CronScheduler.client-WEJF4PWQ.cjs.map} +1 -1
- package/dist/{DocsLayout-XLDB6CJ2.cjs → DocsLayout-N5ZJZPBY.cjs} +200 -199
- package/dist/DocsLayout-N5ZJZPBY.cjs.map +1 -0
- package/dist/{DocsLayout-CTJINVBM.mjs → DocsLayout-VFPPNKSQ.mjs} +7 -6
- package/dist/DocsLayout-VFPPNKSQ.mjs.map +1 -0
- package/dist/JsonSchemaForm-DD7CLRIG.cjs +13 -0
- package/dist/{JsonSchemaForm-OSPUUUHM.cjs.map → JsonSchemaForm-DD7CLRIG.cjs.map} +1 -1
- package/dist/JsonSchemaForm-XKUIVELK.mjs +4 -0
- package/dist/{JsonSchemaForm-TSLX2GRO.mjs.map → JsonSchemaForm-XKUIVELK.mjs.map} +1 -1
- package/dist/JsonTree-55625VVH.mjs +5 -0
- package/dist/{JsonTree-F27RMYSI.cjs.map → JsonTree-55625VVH.mjs.map} +1 -1
- package/dist/JsonTree-DCM5QGWF.cjs +11 -0
- package/dist/{JsonTree-QTJYSHCV.mjs.map → JsonTree-DCM5QGWF.cjs.map} +1 -1
- package/dist/{LottiePlayer.client-6WVWDO75.cjs → LottiePlayer.client-2S7ISJ2S.cjs} +6 -6
- package/dist/{LottiePlayer.client-6WVWDO75.cjs.map → LottiePlayer.client-2S7ISJ2S.cjs.map} +1 -1
- package/dist/{LottiePlayer.client-B4I6WNZM.mjs → LottiePlayer.client-5LDSSJWS.mjs} +4 -4
- package/dist/{LottiePlayer.client-B4I6WNZM.mjs.map → LottiePlayer.client-5LDSSJWS.mjs.map} +1 -1
- package/dist/{MapContainer-RYG4HPH4.cjs → MapContainer-76YL2JXL.cjs} +8 -8
- package/dist/{MapContainer-RYG4HPH4.cjs.map → MapContainer-76YL2JXL.cjs.map} +1 -1
- package/dist/{MapContainer-GXQLP5WY.mjs → MapContainer-7HXBI3OH.mjs} +3 -3
- package/dist/{MapContainer-GXQLP5WY.mjs.map → MapContainer-7HXBI3OH.mjs.map} +1 -1
- package/dist/{Mermaid.client-SXRRI2YW.mjs → Mermaid.client-NL4SVR7F.mjs} +4 -4
- package/dist/{Mermaid.client-SXRRI2YW.mjs.map → Mermaid.client-NL4SVR7F.mjs.map} +1 -1
- package/dist/{Mermaid.client-W76R5AKJ.cjs → Mermaid.client-NNTI6DFX.cjs} +26 -26
- package/dist/{Mermaid.client-W76R5AKJ.cjs.map → Mermaid.client-NNTI6DFX.cjs.map} +1 -1
- package/dist/Player-BRV7XTWR.mjs +4 -0
- package/dist/{Player-M3GC3VPE.mjs.map → Player-BRV7XTWR.mjs.map} +1 -1
- package/dist/Player-PM7F7DD7.cjs +13 -0
- package/dist/{Player-ZL2X5LGG.cjs.map → Player-PM7F7DD7.cjs.map} +1 -1
- package/dist/{PrettyCode.client-RPDIE5CH.cjs → PrettyCode.client-KOHDVPPN.cjs} +13 -13
- package/dist/{PrettyCode.client-RPDIE5CH.cjs.map → PrettyCode.client-KOHDVPPN.cjs.map} +1 -1
- package/dist/{PrettyCode.client-SPMTQEG4.mjs → PrettyCode.client-ZGYGKE7G.mjs} +4 -4
- package/dist/{PrettyCode.client-SPMTQEG4.mjs.map → PrettyCode.client-ZGYGKE7G.mjs.map} +1 -1
- package/dist/TreeRoot-N72OYKXU.cjs +19 -0
- package/dist/{TreeRoot-A3J65L6F.mjs.map → TreeRoot-N72OYKXU.cjs.map} +1 -1
- package/dist/TreeRoot-VGAIXCUA.mjs +4 -0
- package/dist/{TreeRoot-DSK5JILT.cjs.map → TreeRoot-VGAIXCUA.mjs.map} +1 -1
- package/dist/chunk-2ZLKZ5VR.mjs +631 -0
- package/dist/chunk-2ZLKZ5VR.mjs.map +1 -0
- package/dist/{chunk-LFWQ36LJ.mjs → chunk-5G5YBFS6.mjs} +4 -4
- package/dist/{chunk-LFWQ36LJ.mjs.map → chunk-5G5YBFS6.mjs.map} +1 -1
- package/dist/{chunk-IHAY6FO6.cjs → chunk-5I5QNGUG.cjs} +17 -17
- package/dist/{chunk-IHAY6FO6.cjs.map → chunk-5I5QNGUG.cjs.map} +1 -1
- package/dist/{chunk-F2CMIIOH.cjs → chunk-76NNDZH6.cjs} +42 -42
- package/dist/{chunk-F2CMIIOH.cjs.map → chunk-76NNDZH6.cjs.map} +1 -1
- package/dist/chunk-B5AWZOHJ.cjs +649 -0
- package/dist/chunk-B5AWZOHJ.cjs.map +1 -0
- package/dist/{chunk-KR6B3LVY.mjs → chunk-B6IR5KSC.mjs} +3 -3
- package/dist/{chunk-KR6B3LVY.mjs.map → chunk-B6IR5KSC.mjs.map} +1 -1
- package/dist/{chunk-5LBDYFWH.mjs → chunk-C6GXVH5J.mjs} +3 -3
- package/dist/{chunk-5LBDYFWH.mjs.map → chunk-C6GXVH5J.mjs.map} +1 -1
- package/dist/{chunk-4IW7GZFQ.cjs → chunk-FEN5S772.cjs} +74 -48
- package/dist/chunk-FEN5S772.cjs.map +1 -0
- package/dist/{chunk-2SMCH62O.cjs → chunk-FP2RLYQZ.cjs} +11 -11
- package/dist/{chunk-2SMCH62O.cjs.map → chunk-FP2RLYQZ.cjs.map} +1 -1
- package/dist/{chunk-MOME6KYD.mjs → chunk-G5IEC7SR.mjs} +3 -3
- package/dist/{chunk-MOME6KYD.mjs.map → chunk-G5IEC7SR.mjs.map} +1 -1
- package/dist/{chunk-EXGXUK2N.mjs → chunk-GYIO7W7M.mjs} +41 -15
- package/dist/chunk-GYIO7W7M.mjs.map +1 -0
- package/dist/{chunk-3Z3A7FHA.cjs → chunk-IEEAENLX.cjs} +48 -48
- package/dist/{chunk-3Z3A7FHA.cjs.map → chunk-IEEAENLX.cjs.map} +1 -1
- package/dist/{chunk-DFTVB66S.cjs → chunk-KNDLV4PI.cjs} +85 -85
- package/dist/{chunk-DFTVB66S.cjs.map → chunk-KNDLV4PI.cjs.map} +1 -1
- package/dist/{chunk-SSUOENAZ.mjs → chunk-KNEQRUBA.mjs} +3 -3
- package/dist/{chunk-SSUOENAZ.mjs.map → chunk-KNEQRUBA.mjs.map} +1 -1
- package/dist/chunk-KRETIZU6.mjs +2218 -0
- package/dist/chunk-KRETIZU6.mjs.map +1 -0
- package/dist/{chunk-CGILA3WO.mjs → chunk-N2XQF2OL.mjs} +5 -3
- package/dist/{chunk-CGILA3WO.mjs.map → chunk-N2XQF2OL.mjs.map} +1 -1
- package/dist/{chunk-EUADAUBQ.mjs → chunk-N4MZYNR4.mjs} +4 -4
- package/dist/{chunk-EUADAUBQ.mjs.map → chunk-N4MZYNR4.mjs.map} +1 -1
- package/dist/chunk-NRXYYO5V.cjs +2257 -0
- package/dist/chunk-NRXYYO5V.cjs.map +1 -0
- package/dist/{chunk-GGKGH5PM.mjs → chunk-OBRSGM64.mjs} +4 -4
- package/dist/{chunk-GGKGH5PM.mjs.map → chunk-OBRSGM64.mjs.map} +1 -1
- package/dist/{chunk-6JTB2X72.mjs → chunk-ODO4GMW7.mjs} +3 -3
- package/dist/{chunk-6JTB2X72.mjs.map → chunk-ODO4GMW7.mjs.map} +1 -1
- package/dist/{chunk-WGEGR3DF.cjs → chunk-OLISEQHS.cjs} +5 -2
- package/dist/{chunk-WGEGR3DF.cjs.map → chunk-OLISEQHS.cjs.map} +1 -1
- package/dist/{chunk-PZKAH7WQ.mjs → chunk-PVAX67JG.mjs} +3 -3
- package/dist/{chunk-PZKAH7WQ.mjs.map → chunk-PVAX67JG.mjs.map} +1 -1
- package/dist/{chunk-PRPG2T2E.cjs → chunk-QJ6GTUCO.cjs} +6 -6
- package/dist/{chunk-PRPG2T2E.cjs.map → chunk-QJ6GTUCO.cjs.map} +1 -1
- package/dist/chunk-QW4RBGHN.cjs +961 -0
- package/dist/chunk-QW4RBGHN.cjs.map +1 -0
- package/dist/{chunk-33AMWFBZ.cjs → chunk-SGP7V2UW.cjs} +15 -15
- package/dist/{chunk-33AMWFBZ.cjs.map → chunk-SGP7V2UW.cjs.map} +1 -1
- package/dist/{chunk-FX2QFYWF.mjs → chunk-VWQ5WOIL.mjs} +3 -3
- package/dist/{chunk-FX2QFYWF.mjs.map → chunk-VWQ5WOIL.mjs.map} +1 -1
- package/dist/{chunk-ZLQHUZDU.cjs → chunk-YDPDTOSP.cjs} +139 -139
- package/dist/{chunk-ZLQHUZDU.cjs.map → chunk-YDPDTOSP.cjs.map} +1 -1
- package/dist/{chunk-77HQWEQ6.cjs → chunk-YW5IVWHQ.cjs} +33 -33
- package/dist/{chunk-77HQWEQ6.cjs.map → chunk-YW5IVWHQ.cjs.map} +1 -1
- package/dist/{chunk-YXBOAGIM.cjs → chunk-YXZ6GU7H.cjs} +7 -7
- package/dist/{chunk-YXBOAGIM.cjs.map → chunk-YXZ6GU7H.cjs.map} +1 -1
- package/dist/{chunk-62Y65TGK.mjs → chunk-ZUFTH5IR.mjs} +8 -631
- package/dist/chunk-ZUFTH5IR.mjs.map +1 -0
- package/dist/components-EHOGXATG.cjs +22 -0
- package/dist/{components-5UXYNAKR.cjs.map → components-EHOGXATG.cjs.map} +1 -1
- package/dist/components-MQ6DR7TX.cjs +26 -0
- package/dist/{components-CFXOEVPN.mjs.map → components-MQ6DR7TX.cjs.map} +1 -1
- package/dist/components-XRX7QGLB.mjs +5 -0
- package/dist/{components-WYEZL5TE.cjs.map → components-XRX7QGLB.mjs.map} +1 -1
- package/dist/components-YATKRWLH.mjs +5 -0
- package/dist/{components-ZAGG2PBO.mjs.map → components-YATKRWLH.mjs.map} +1 -1
- package/dist/file-icon/index.cjs +6 -6
- package/dist/file-icon/index.mjs +1 -1
- package/dist/index.cjs +735 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +972 -39
- package/dist/index.d.ts +972 -39
- package/dist/index.mjs +387 -31
- package/dist/index.mjs.map +1 -1
- package/dist/tree/index.cjs +38 -38
- package/dist/tree/index.d.cts +2 -2
- package/dist/tree/index.d.ts +2 -2
- package/dist/tree/index.mjs +3 -3
- package/package.json +6 -6
- package/src/index.ts +5 -0
- package/src/stories/index.ts +3 -1
- package/src/tools/Chat/Chat.story.tsx +1006 -0
- package/src/tools/Chat/README.md +528 -0
- package/src/tools/Chat/components/Attachments.tsx +192 -0
- package/src/tools/Chat/components/ChatRoot.tsx +201 -0
- package/src/tools/Chat/components/Composer.tsx +134 -0
- package/src/tools/Chat/components/EmptyState.tsx +47 -0
- package/src/tools/Chat/components/ErrorBanner.tsx +47 -0
- package/src/tools/Chat/components/JumpToLatest.tsx +30 -0
- package/src/tools/Chat/components/MessageActions.tsx +72 -0
- package/src/tools/Chat/components/MessageBubble.tsx +228 -0
- package/src/tools/Chat/components/MessageList.tsx +82 -0
- package/src/tools/Chat/components/Sources.tsx +55 -0
- package/src/tools/Chat/components/StreamingIndicator.tsx +29 -0
- package/src/tools/Chat/components/ToolCalls.tsx +172 -0
- package/src/tools/Chat/components/index.ts +24 -0
- package/src/tools/Chat/config.ts +55 -0
- package/src/tools/Chat/context/ChatProvider.tsx +122 -0
- package/src/tools/Chat/context/index.ts +9 -0
- package/src/tools/Chat/core/audio/audioBus.ts +172 -0
- package/src/tools/Chat/core/audio/index.ts +8 -0
- package/src/tools/Chat/core/audio/preferences.ts +68 -0
- package/src/tools/Chat/core/audio/types.ts +49 -0
- package/src/tools/Chat/core/ids.ts +16 -0
- package/src/tools/Chat/core/index.ts +5 -0
- package/src/tools/Chat/core/markdown.ts +56 -0
- package/src/tools/Chat/core/payload-dispatch.ts +54 -0
- package/src/tools/Chat/core/persona.ts +35 -0
- package/src/tools/Chat/core/reducer.ts +335 -0
- package/src/tools/Chat/core/transport/http.ts +167 -0
- package/src/tools/Chat/core/transport/index.ts +13 -0
- package/src/tools/Chat/core/transport/mock.ts +134 -0
- package/src/tools/Chat/core/transport/sse.ts +116 -0
- package/src/tools/Chat/core/transport/types.ts +24 -0
- package/src/tools/Chat/hooks/index.ts +26 -0
- package/src/tools/Chat/hooks/useChat.ts +440 -0
- package/src/tools/Chat/hooks/useChatAudio.ts +191 -0
- package/src/tools/Chat/hooks/useChatComposer.ts +227 -0
- package/src/tools/Chat/hooks/useChatHistory.ts +59 -0
- package/src/tools/Chat/hooks/useChatLayout.ts +111 -0
- package/src/tools/Chat/hooks/useChatLightbox.ts +34 -0
- package/src/tools/Chat/hooks/useChatScroll.ts +132 -0
- package/src/tools/Chat/index.ts +158 -0
- package/src/tools/Chat/lazy.tsx +14 -0
- package/src/tools/Chat/types.ts +237 -0
- package/src/tools/Chat/utils/collectImageAttachments.ts +13 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +32 -1
- package/src/tools/Map/README.md +384 -0
- package/dist/DocsLayout-CTJINVBM.mjs.map +0 -1
- package/dist/DocsLayout-XLDB6CJ2.cjs.map +0 -1
- package/dist/JsonSchemaForm-OSPUUUHM.cjs +0 -13
- package/dist/JsonSchemaForm-TSLX2GRO.mjs +0 -4
- package/dist/JsonTree-F27RMYSI.cjs +0 -11
- package/dist/JsonTree-QTJYSHCV.mjs +0 -5
- package/dist/Player-M3GC3VPE.mjs +0 -4
- package/dist/Player-ZL2X5LGG.cjs +0 -13
- package/dist/TreeRoot-A3J65L6F.mjs +0 -4
- package/dist/TreeRoot-DSK5JILT.cjs +0 -19
- package/dist/chunk-4IW7GZFQ.cjs.map +0 -1
- package/dist/chunk-62Y65TGK.mjs.map +0 -1
- package/dist/chunk-EXGXUK2N.mjs.map +0 -1
- package/dist/chunk-TKSFZHCG.cjs +0 -1597
- package/dist/chunk-TKSFZHCG.cjs.map +0 -1
- package/dist/components-5UXYNAKR.cjs +0 -22
- package/dist/components-CFXOEVPN.mjs +0 -5
- package/dist/components-WYEZL5TE.cjs +0 -26
- package/dist/components-ZAGG2PBO.mjs +0 -5
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# Map
|
|
2
|
+
|
|
3
|
+
Interactive maps for `@djangocfg/ui-tools`, built on [MapLibre GL](https://maplibre.org/) via `react-map-gl`. Ships markers, clustered GeoJSON, popups, drawing/geocoder hooks, layer switching, and helpers for overlapping points — all behind a small Provider/Context surface so nothing leaks the raw `Map` instance unless you ask for it.
|
|
4
|
+
|
|
5
|
+
> **Bundle warning.** MapLibre GL is heavy (~800 KB gzipped). Prefer `LazyMapContainer` from `./lazy` unless the map is above the fold. All components are `'use client'` — never render them on the Next.js server.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { MapContainer, MapMarker } from '@djangocfg/ui-tools/map';
|
|
11
|
+
|
|
12
|
+
<div className="h-96 rounded-xl overflow-hidden">
|
|
13
|
+
<MapContainer
|
|
14
|
+
initialViewport={{ latitude: 25.08, longitude: 55.14, zoom: 13 }}
|
|
15
|
+
mapStyle="light"
|
|
16
|
+
>
|
|
17
|
+
<MapMarker
|
|
18
|
+
marker={{ id: 'home', latitude: 25.08, longitude: 55.14 }}
|
|
19
|
+
color="#10b981"
|
|
20
|
+
size={32}
|
|
21
|
+
/>
|
|
22
|
+
</MapContainer>
|
|
23
|
+
</div>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The container provides its own `MapProvider`. If you need the map context outside `MapContainer` (for example to call `flyTo` on a sibling button), wrap your tree in `<MapProvider>` and use `<MapView>` inside it instead.
|
|
27
|
+
|
|
28
|
+
## Public exports
|
|
29
|
+
|
|
30
|
+
### Components
|
|
31
|
+
|
|
32
|
+
| Export | Notes |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `MapContainer` | Self-contained map: `MapProvider` + `MapView` + reset / open-in-maps overlay buttons |
|
|
35
|
+
| `MapView` | Inner map only — must live inside an existing `<MapProvider>` |
|
|
36
|
+
| `MapProvider` | Context provider (viewport, ref, markers, hover state, isLoaded) |
|
|
37
|
+
| `MapMarker` | DOM marker with default pin, custom children, drag |
|
|
38
|
+
| `MapPopup` | MapLibre popup wrapper |
|
|
39
|
+
| `MapCluster` | GeoJSON source + cluster/point layers + click-to-zoom + optional render-prop popup |
|
|
40
|
+
| `MapSource` / `MapLayer` | Thin GeoJSON `Source` + `Layer` re-exports |
|
|
41
|
+
| `MapControls` | NavigationControl / Fullscreen / Geolocate / Scale toggles |
|
|
42
|
+
| `CustomOverlay` | DOM overlay that re-renders on `move` (uses `useControl` + portal) |
|
|
43
|
+
| `MapLegend` | Floating legend with circle / line / fill / symbol icons |
|
|
44
|
+
| `LayerSwitcher` | Floating checkbox panel that toggles layer visibility |
|
|
45
|
+
|
|
46
|
+
### Hooks
|
|
47
|
+
|
|
48
|
+
| Hook | Returns |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `useMap()` | Full map context (alias for `useMapContext`) |
|
|
51
|
+
| `useMapContext()` | Same — throws if used outside `<MapProvider>` |
|
|
52
|
+
| `useMapControl()` | `flyTo` / `easeTo` / `fitBounds` / `zoomIn` / `zoomOut` / `resetView` / `getCenter` / `getZoom` / `getBounds` |
|
|
53
|
+
| `useMapViewport()` | `{ viewport, setViewport, animateTo, zoom, center, bounds }` |
|
|
54
|
+
| `useMarkers()` | `addMarker` / `removeMarker` / `updateMarker` / `clearMarkers` / `fitToMarkers` |
|
|
55
|
+
| `useMapEvents(handlers)` | Subscribe to `click` / `hover` / `move*` / `zoom*` / `load` with cleanup |
|
|
56
|
+
| `useMapLayers()` | `addLayer` / `removeLayer` / `setLayerVisibility` / `setLayerFilter` / `setLayerPaint` |
|
|
57
|
+
| `useSpiderfy(markers, opts?)` | Auto-offset overlapping markers; optional click-to-expand |
|
|
58
|
+
| `useControl(...)` | Re-exported from `react-map-gl/maplibre` for custom controls |
|
|
59
|
+
|
|
60
|
+
### Layer factories
|
|
61
|
+
|
|
62
|
+
`createClusterLayers`, `createPointLayer`, `createHeatmapLayer`, `createPolygonLayer`, `createPolygonOutlineLayer`, `createHighlightLayer`, `createLineLayer`, `createRouteLayers`, `createDashedLineLayer`, `createAnimatedLineLayer`.
|
|
63
|
+
|
|
64
|
+
### Geometry utils
|
|
65
|
+
|
|
66
|
+
`calculateBounds`, `calculateCenter`, `calculateDistance` (Haversine, km), `isPointInBounds`, `expandBounds`, `toGeoJSON`, `fromGeoJSON`, `createPoint`, `createPolygon`, `createLineString`, `createFeatureCollection`.
|
|
67
|
+
|
|
68
|
+
### Spiderfy utils
|
|
69
|
+
|
|
70
|
+
`offsetOverlappingMarkers`, `getSpiderfyPositions`, `groupOverlappingMarkers`, `hasOverlappingMarkers`, `getOverlapStats`.
|
|
71
|
+
|
|
72
|
+
### Styles
|
|
73
|
+
|
|
74
|
+
`MAP_STYLES`, `getMapStyle(key)`.
|
|
75
|
+
|
|
76
|
+
### Re-exports from `react-map-gl/maplibre`
|
|
77
|
+
|
|
78
|
+
`Source`, `Layer`, `Marker`, `Popup`, types `ViewState` and `MapRef`.
|
|
79
|
+
|
|
80
|
+
## `MapContainer` props
|
|
81
|
+
|
|
82
|
+
| Prop | Type | Default | Notes |
|
|
83
|
+
|---|---|---|---|
|
|
84
|
+
| `initialViewport` | `Partial<MapViewport>` | `{ longitude: 115.1889, latitude: -8.4095, zoom: 10 }` | Bali fallback. Provide your own. |
|
|
85
|
+
| `mapStyle` | `MapStyleKey \| string` | `'light'` | Built-in key or full style URL/object |
|
|
86
|
+
| `interactiveLayerIds` | `string[]` | — | Layer ids that emit `click` / `hover` events |
|
|
87
|
+
| `attributionControl` | `boolean` | `true` | |
|
|
88
|
+
| `reuseMaps` | `boolean` | `true` | Reuse the canvas across remounts (faster Storybook / route changes) |
|
|
89
|
+
| `cursor` | `string` | — | Cursor while not dragging |
|
|
90
|
+
| `style` | `CSSProperties` | — | Forwarded to inner `<Map>` (size, etc.) |
|
|
91
|
+
| `className` | `string` | — | Outer wrapper `<div>` |
|
|
92
|
+
| `openInMapsUrl` | `string` | — | Renders an "Open in Maps" pill in the bottom-right |
|
|
93
|
+
| `openInMapsLabel` | `string` | `'Open in Maps'` | |
|
|
94
|
+
| `showResetButton` | `boolean` | `false` | Shows a reset pill once the user has panned/zoomed away from `initialViewport` |
|
|
95
|
+
| `autoResetDelay` | `number` (ms) | `0` | After this many ms of inactivity, fly back to `initialViewport`. `0` disables. |
|
|
96
|
+
|
|
97
|
+
`MapView` accepts every prop above except `initialViewport` (which lives on the surrounding `MapProvider`).
|
|
98
|
+
|
|
99
|
+
## Markers
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
<MapMarker
|
|
103
|
+
marker={{ id: 'pin', longitude: 2.35, latitude: 48.85, data: { city: 'Paris' } }}
|
|
104
|
+
color="#3b82f6"
|
|
105
|
+
size={28}
|
|
106
|
+
anchor="bottom"
|
|
107
|
+
draggable
|
|
108
|
+
onClick={(m) => console.log(m.id)}
|
|
109
|
+
onDragEnd={(m, lngLat) => save(m.id, lngLat)}
|
|
110
|
+
/>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Pass `children` to replace the default SVG pin entirely:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
<MapMarker marker={pin}>
|
|
117
|
+
<div className="size-8 rounded-full bg-primary text-white grid place-items-center">
|
|
118
|
+
{pin.data?.count}
|
|
119
|
+
</div>
|
|
120
|
+
</MapMarker>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`MarkerData` is intentionally minimal: `{ id, longitude, latitude, data? }`. Carry your domain payload in `data` — markers are generic over what's there.
|
|
124
|
+
|
|
125
|
+
## Popups
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
<MapPopup
|
|
129
|
+
longitude={loc.lng}
|
|
130
|
+
latitude={loc.lat}
|
|
131
|
+
onClose={() => setSelected(null)}
|
|
132
|
+
anchor="bottom"
|
|
133
|
+
offset={20}
|
|
134
|
+
>
|
|
135
|
+
<div className="p-3">…</div>
|
|
136
|
+
</MapPopup>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
For "click marker → fly to it → open popup" flow, use `useMapControl().flyTo` from a component nested inside `<MapProvider>` (see `Map.story.tsx` → `WithPopup`).
|
|
140
|
+
|
|
141
|
+
## Clustering
|
|
142
|
+
|
|
143
|
+
`MapCluster` mounts a clustered GeoJSON source with three layers (cluster bubbles, count labels, unclustered points), wires click + hover handlers, and optionally renders a popup via render prop.
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
<MapCluster
|
|
147
|
+
sourceId="properties"
|
|
148
|
+
data={featureCollection}
|
|
149
|
+
clusterRadius={60}
|
|
150
|
+
clusterMaxZoom={14}
|
|
151
|
+
colors={['#3b82f6', '#8b5cf6', '#ec4899']}
|
|
152
|
+
onPointClick={(feature) => setSelected(feature)}
|
|
153
|
+
renderPopup={(feature, close) => (
|
|
154
|
+
<Card>
|
|
155
|
+
<h3>{feature.properties?.name}</h3>
|
|
156
|
+
<button onClick={close}>Close</button>
|
|
157
|
+
</Card>
|
|
158
|
+
)}
|
|
159
|
+
popupAnchor="bottom"
|
|
160
|
+
popupOffset={15}
|
|
161
|
+
/>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Click on a cluster zooms to its expansion zoom. Hover sets `feature-state.hover` so you can tint the bubble in your paint expressions.
|
|
165
|
+
|
|
166
|
+
### Overlapping points
|
|
167
|
+
|
|
168
|
+
When several features share a coordinate (e.g. multiple units in one building), pre-process the source so they're individually clickable when unclustered:
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import { offsetOverlappingMarkers } from '@djangocfg/ui-tools/map';
|
|
172
|
+
|
|
173
|
+
const data = useMemo(() => {
|
|
174
|
+
const markers = features.map((f, i) => ({
|
|
175
|
+
id: f.properties?.id ?? `p-${i}`,
|
|
176
|
+
longitude: f.geometry.coordinates[0],
|
|
177
|
+
latitude: f.geometry.coordinates[1],
|
|
178
|
+
data: f.properties,
|
|
179
|
+
}));
|
|
180
|
+
const offset = offsetOverlappingMarkers(markers, { spiralRadius: 0.0003 });
|
|
181
|
+
return {
|
|
182
|
+
type: 'FeatureCollection' as const,
|
|
183
|
+
features: offset.map((m) => ({
|
|
184
|
+
type: 'Feature' as const,
|
|
185
|
+
properties: m.data,
|
|
186
|
+
geometry: { type: 'Point' as const, coordinates: [m.longitude, m.latitude] },
|
|
187
|
+
})),
|
|
188
|
+
};
|
|
189
|
+
}, [features]);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`offsetOverlappingMarkers` uses a Fermat spiral by default; pass `useJitter: true` for randomised offsets keyed on marker `id`. The `useSpiderfy` hook wraps this with React state and an optional click-to-expand mode.
|
|
193
|
+
|
|
194
|
+
## Layers
|
|
195
|
+
|
|
196
|
+
Drop a `MapSource` + `MapLayer` for raw GeoJSON, or use the layer factories for typed defaults:
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
import {
|
|
200
|
+
MapSource, MapLayer,
|
|
201
|
+
createPolygonLayer, createPolygonOutlineLayer,
|
|
202
|
+
} from '@djangocfg/ui-tools/map';
|
|
203
|
+
|
|
204
|
+
<MapSource id="zones" data={polygonGeoJson}>
|
|
205
|
+
<MapLayer {...createPolygonLayer({ fillColor: '#10b981', fillOpacity: 0.2 })} />
|
|
206
|
+
<MapLayer {...createPolygonOutlineLayer({ strokeColor: '#10b981', strokeWidth: 2 })} />
|
|
207
|
+
</MapSource>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Available factories: `createClusterLayers`, `createPointLayer`, `createHeatmapLayer`, `createPolygonLayer`, `createPolygonOutlineLayer`, `createHighlightLayer`, `createLineLayer`, `createRouteLayers`, `createDashedLineLayer`, `createAnimatedLineLayer`.
|
|
211
|
+
|
|
212
|
+
## Map styles
|
|
213
|
+
|
|
214
|
+
Four built-in keys, all served from public CDNs (no API key needed except `satellite`, which uses MapTiler):
|
|
215
|
+
|
|
216
|
+
| Key | Source |
|
|
217
|
+
|---|---|
|
|
218
|
+
| `light` | CartoCDN Positron |
|
|
219
|
+
| `dark` | CartoCDN Dark Matter |
|
|
220
|
+
| `streets` | CartoCDN Voyager |
|
|
221
|
+
| `satellite` | MapTiler Satellite (replace with your own key in production) |
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
import { MAP_STYLES, getMapStyle } from '@djangocfg/ui-tools/map';
|
|
225
|
+
|
|
226
|
+
<MapContainer mapStyle="dark" />
|
|
227
|
+
<MapContainer mapStyle="https://your-cdn.example/style.json" />
|
|
228
|
+
<MapContainer mapStyle={getMapStyle(userPref)} />
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Hooks
|
|
232
|
+
|
|
233
|
+
All hooks must be called inside `<MapProvider>` (which `MapContainer` provides automatically).
|
|
234
|
+
|
|
235
|
+
### `useMapControl()`
|
|
236
|
+
|
|
237
|
+
Imperative camera control. All animations have sensible defaults (`flyTo` 2 s, `easeTo` 0.5 s, `fitBounds` 1 s, padding 50 px).
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
const { flyTo, fitBounds, zoomIn, getBounds } = useMapControl();
|
|
241
|
+
flyTo([lng, lat], 14, { duration: 1500 });
|
|
242
|
+
fitBounds([[w, s], [e, n]], { padding: 80 });
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### `useMapViewport()`
|
|
246
|
+
|
|
247
|
+
Reactive viewport state plus an `animateTo` shortcut.
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
const { viewport, animateTo, bounds } = useMapViewport();
|
|
251
|
+
animateTo({ latitude: 0, longitude: 0, zoom: 2 }, 800);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### `useMarkers()`
|
|
255
|
+
|
|
256
|
+
Manages markers held by `MapProvider` (separate from any `<MapMarker>` you render manually). Generates ids automatically.
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
const { addMarker, fitToMarkers, clearMarkers } = useMarkers();
|
|
260
|
+
const id = addMarker({ longitude, latitude, data: { kind: 'pickup' } });
|
|
261
|
+
fitToMarkers(80);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### `useMapEvents(handlers)`
|
|
265
|
+
|
|
266
|
+
Attaches handlers to the underlying MapLibre map once it's loaded; cleans up on unmount or when handlers change.
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
useMapEvents({
|
|
270
|
+
onClick: (e) => console.log(e.lngLat, e.features),
|
|
271
|
+
onMoveEnd: (vp) => persistViewport(vp),
|
|
272
|
+
onZoomEnd: (z) => setZoom(z),
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### `useMapLayers()`
|
|
277
|
+
|
|
278
|
+
Imperative layer mutation — for layers added at runtime via `addLayer`, or for tweaking declarative `<MapLayer>` instances by id.
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
const { setLayerVisibility, setLayerFilter, setLayerPaint } = useMapLayers();
|
|
282
|
+
setLayerVisibility('zones-fill', visible);
|
|
283
|
+
setLayerFilter('zones-fill', ['==', ['get', 'status'], 'active']);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### `useSpiderfy(markers, options?)`
|
|
287
|
+
|
|
288
|
+
Higher-level than `offsetOverlappingMarkers`: returns processed markers, expansion state, and `expandGroup` / `collapseGroup` callbacks.
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
const { markers, expandGroup, isExpanded } = useSpiderfy(rawMarkers, {
|
|
292
|
+
spiralRadius: 0.0002,
|
|
293
|
+
expandOnClick: true,
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Geometry utilities
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
import {
|
|
301
|
+
calculateBounds, calculateCenter, calculateDistance,
|
|
302
|
+
expandBounds, isPointInBounds,
|
|
303
|
+
toGeoJSON, fromGeoJSON,
|
|
304
|
+
createPoint, createPolygon, createLineString, createFeatureCollection,
|
|
305
|
+
} from '@djangocfg/ui-tools/map';
|
|
306
|
+
|
|
307
|
+
const bounds = calculateBounds(points.map((p) => [p.lng, p.lat]));
|
|
308
|
+
const km = calculateDistance([fromLng, fromLat], [toLng, toLat]); // Haversine
|
|
309
|
+
const fc = toGeoJSON(items, (i) => [i.lng, i.lat], (i) => ({ name: i.name }));
|
|
310
|
+
|
|
311
|
+
useMapControl().fitBounds(expandBounds(bounds, 10), { padding: 50 });
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Floating UI
|
|
315
|
+
|
|
316
|
+
`MapLegend` and `LayerSwitcher` are absolutely-positioned panels (`top-left` / `top-right` / `bottom-left` / `bottom-right`). They use inline styles for portability — wrap them in your own component if you want shadcn theming.
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
<MapLegend
|
|
320
|
+
title="Activity"
|
|
321
|
+
position="bottom-left"
|
|
322
|
+
collapsible
|
|
323
|
+
items={[
|
|
324
|
+
{ id: 'high', label: 'High', color: '#ef4444', type: 'circle' },
|
|
325
|
+
{ id: 'low', label: 'Low', color: '#3b82f6', type: 'circle' },
|
|
326
|
+
]}
|
|
327
|
+
/>
|
|
328
|
+
|
|
329
|
+
<LayerSwitcher
|
|
330
|
+
position="top-right"
|
|
331
|
+
showToggleAll
|
|
332
|
+
layers={[
|
|
333
|
+
{ id: 'zones-fill', label: 'Zones', defaultVisible: true },
|
|
334
|
+
{ id: 'routes-line', label: 'Routes', defaultVisible: false },
|
|
335
|
+
]}
|
|
336
|
+
onChange={(id, visible) => console.log(id, visible)}
|
|
337
|
+
/>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## DrawControl & GeocoderControl
|
|
341
|
+
|
|
342
|
+
These are documentation stubs — both rely on optional peer deps and are easy to wire by hand:
|
|
343
|
+
|
|
344
|
+
- `DrawControl` → `npm i @mapbox/mapbox-gl-draw`
|
|
345
|
+
- `GeocoderControl` → `npm i @maplibre/maplibre-gl-geocoder` + an HTTP geocoder (Nominatim, Photon, your backend)
|
|
346
|
+
|
|
347
|
+
See the JSDoc in `components/DrawControl.tsx` and `components/GeocoderControl.tsx` for ready-to-paste implementations using the re-exported `useControl` hook.
|
|
348
|
+
|
|
349
|
+
## Lazy loading
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
import { LazyMapContainer } from '@djangocfg/ui-tools/map/lazy';
|
|
353
|
+
|
|
354
|
+
<LazyMapContainer
|
|
355
|
+
initialViewport={{ latitude: 0, longitude: 0, zoom: 2 }}
|
|
356
|
+
mapStyle="light"
|
|
357
|
+
/>
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
`LazyMapContainer` and `LazyMapView` defer the MapLibre GL chunk until the component renders. They show a built-in `MapLoadingFallback` (skeleton, `minHeight={400}`) while the chunk is in-flight. Prefer the lazy variants on routes where the map is below the fold or behind a tab.
|
|
361
|
+
|
|
362
|
+
## SSR
|
|
363
|
+
|
|
364
|
+
Every component is `'use client'` and depends on `window`. In Next.js App Router this is fine — they will only render on the client. In the Pages Router or any custom SSR setup, gate them with `dynamic(() => …, { ssr: false })` or use the lazy variants from `./lazy`.
|
|
365
|
+
|
|
366
|
+
## Stories
|
|
367
|
+
|
|
368
|
+
Run `pnpm playground` from `packages/ui-tools/`. Map stories in `Map.story.tsx`:
|
|
369
|
+
|
|
370
|
+
| Story | Demonstrates |
|
|
371
|
+
|---|---|
|
|
372
|
+
| `Interactive` | Style switcher + zoom + reset button + auto-reset |
|
|
373
|
+
| `PropertyCard` | Embedded compact map inside a card |
|
|
374
|
+
| `DarkStyle` | `mapStyle="dark"` |
|
|
375
|
+
| `MultipleMarkers` | World view with several pins |
|
|
376
|
+
| `WithAutoReset` | `autoResetDelay={3000}` |
|
|
377
|
+
| `WithPopup` | `useMapControl().flyTo` + `<MapPopup>` |
|
|
378
|
+
| `SpiderfyCluster` | `MapCluster` + `offsetOverlappingMarkers` |
|
|
379
|
+
|
|
380
|
+
## Related
|
|
381
|
+
|
|
382
|
+
- Public surface: `src/tools/Map/index.ts`
|
|
383
|
+
- Lazy preset: `src/tools/Map/lazy.tsx`
|
|
384
|
+
- Underlying lib: [`react-map-gl` (MapLibre adapter)](https://visgl.github.io/react-map-gl/) on top of [MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/)
|