@djangocfg/ui-tools 2.1.335 → 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.
Files changed (193) hide show
  1. package/README.md +68 -2
  2. package/dist/ChatRoot-IIYQEWUU.mjs +5 -0
  3. package/dist/ChatRoot-IIYQEWUU.mjs.map +1 -0
  4. package/dist/ChatRoot-PNNGQCYF.css +7 -0
  5. package/dist/ChatRoot-PNNGQCYF.css.map +1 -0
  6. package/dist/ChatRoot-UUKTYM4N.cjs +14 -0
  7. package/dist/ChatRoot-UUKTYM4N.cjs.map +1 -0
  8. package/dist/{CronScheduler.client-3O3VU4CI.mjs → CronScheduler.client-DLMXCPAJ.mjs} +4 -4
  9. package/dist/{CronScheduler.client-3O3VU4CI.mjs.map → CronScheduler.client-DLMXCPAJ.mjs.map} +1 -1
  10. package/dist/{CronScheduler.client-A4GO6YBY.cjs → CronScheduler.client-WEJF4PWQ.cjs} +14 -14
  11. package/dist/{CronScheduler.client-A4GO6YBY.cjs.map → CronScheduler.client-WEJF4PWQ.cjs.map} +1 -1
  12. package/dist/{DocsLayout-XLDB6CJ2.cjs → DocsLayout-N5ZJZPBY.cjs} +200 -199
  13. package/dist/DocsLayout-N5ZJZPBY.cjs.map +1 -0
  14. package/dist/{DocsLayout-CTJINVBM.mjs → DocsLayout-VFPPNKSQ.mjs} +7 -6
  15. package/dist/DocsLayout-VFPPNKSQ.mjs.map +1 -0
  16. package/dist/JsonSchemaForm-DD7CLRIG.cjs +13 -0
  17. package/dist/{JsonSchemaForm-6WMS4CIY.cjs.map → JsonSchemaForm-DD7CLRIG.cjs.map} +1 -1
  18. package/dist/JsonSchemaForm-XKUIVELK.mjs +4 -0
  19. package/dist/{JsonSchemaForm-KX4JT3M4.mjs.map → JsonSchemaForm-XKUIVELK.mjs.map} +1 -1
  20. package/dist/JsonTree-55625VVH.mjs +5 -0
  21. package/dist/{JsonTree-F27RMYSI.cjs.map → JsonTree-55625VVH.mjs.map} +1 -1
  22. package/dist/JsonTree-DCM5QGWF.cjs +11 -0
  23. package/dist/{JsonTree-QTJYSHCV.mjs.map → JsonTree-DCM5QGWF.cjs.map} +1 -1
  24. package/dist/{LottiePlayer.client-6WVWDO75.cjs → LottiePlayer.client-2S7ISJ2S.cjs} +6 -6
  25. package/dist/{LottiePlayer.client-6WVWDO75.cjs.map → LottiePlayer.client-2S7ISJ2S.cjs.map} +1 -1
  26. package/dist/{LottiePlayer.client-B4I6WNZM.mjs → LottiePlayer.client-5LDSSJWS.mjs} +4 -4
  27. package/dist/{LottiePlayer.client-B4I6WNZM.mjs.map → LottiePlayer.client-5LDSSJWS.mjs.map} +1 -1
  28. package/dist/{MapContainer-RYG4HPH4.cjs → MapContainer-76YL2JXL.cjs} +8 -8
  29. package/dist/{MapContainer-RYG4HPH4.cjs.map → MapContainer-76YL2JXL.cjs.map} +1 -1
  30. package/dist/{MapContainer-GXQLP5WY.mjs → MapContainer-7HXBI3OH.mjs} +3 -3
  31. package/dist/{MapContainer-GXQLP5WY.mjs.map → MapContainer-7HXBI3OH.mjs.map} +1 -1
  32. package/dist/{Mermaid.client-SXRRI2YW.mjs → Mermaid.client-NL4SVR7F.mjs} +4 -4
  33. package/dist/{Mermaid.client-SXRRI2YW.mjs.map → Mermaid.client-NL4SVR7F.mjs.map} +1 -1
  34. package/dist/{Mermaid.client-W76R5AKJ.cjs → Mermaid.client-NNTI6DFX.cjs} +26 -26
  35. package/dist/{Mermaid.client-W76R5AKJ.cjs.map → Mermaid.client-NNTI6DFX.cjs.map} +1 -1
  36. package/dist/Player-BRV7XTWR.mjs +4 -0
  37. package/dist/{Player-M3GC3VPE.mjs.map → Player-BRV7XTWR.mjs.map} +1 -1
  38. package/dist/Player-PM7F7DD7.cjs +13 -0
  39. package/dist/{Player-ZL2X5LGG.cjs.map → Player-PM7F7DD7.cjs.map} +1 -1
  40. package/dist/{PrettyCode.client-RPDIE5CH.cjs → PrettyCode.client-KOHDVPPN.cjs} +13 -13
  41. package/dist/{PrettyCode.client-RPDIE5CH.cjs.map → PrettyCode.client-KOHDVPPN.cjs.map} +1 -1
  42. package/dist/{PrettyCode.client-SPMTQEG4.mjs → PrettyCode.client-ZGYGKE7G.mjs} +4 -4
  43. package/dist/{PrettyCode.client-SPMTQEG4.mjs.map → PrettyCode.client-ZGYGKE7G.mjs.map} +1 -1
  44. package/dist/TreeRoot-N72OYKXU.cjs +19 -0
  45. package/dist/{TreeRoot-A3J65L6F.mjs.map → TreeRoot-N72OYKXU.cjs.map} +1 -1
  46. package/dist/TreeRoot-VGAIXCUA.mjs +4 -0
  47. package/dist/{TreeRoot-DSK5JILT.cjs.map → TreeRoot-VGAIXCUA.mjs.map} +1 -1
  48. package/dist/chunk-2ZLKZ5VR.mjs +631 -0
  49. package/dist/chunk-2ZLKZ5VR.mjs.map +1 -0
  50. package/dist/{chunk-LFWQ36LJ.mjs → chunk-5G5YBFS6.mjs} +4 -4
  51. package/dist/{chunk-LFWQ36LJ.mjs.map → chunk-5G5YBFS6.mjs.map} +1 -1
  52. package/dist/{chunk-IHAY6FO6.cjs → chunk-5I5QNGUG.cjs} +17 -17
  53. package/dist/{chunk-IHAY6FO6.cjs.map → chunk-5I5QNGUG.cjs.map} +1 -1
  54. package/dist/{chunk-F2CMIIOH.cjs → chunk-76NNDZH6.cjs} +42 -42
  55. package/dist/{chunk-F2CMIIOH.cjs.map → chunk-76NNDZH6.cjs.map} +1 -1
  56. package/dist/chunk-B5AWZOHJ.cjs +649 -0
  57. package/dist/chunk-B5AWZOHJ.cjs.map +1 -0
  58. package/dist/{chunk-KR6B3LVY.mjs → chunk-B6IR5KSC.mjs} +3 -3
  59. package/dist/{chunk-KR6B3LVY.mjs.map → chunk-B6IR5KSC.mjs.map} +1 -1
  60. package/dist/{chunk-5LBDYFWH.mjs → chunk-C6GXVH5J.mjs} +3 -3
  61. package/dist/{chunk-5LBDYFWH.mjs.map → chunk-C6GXVH5J.mjs.map} +1 -1
  62. package/dist/{chunk-NRKD4F5X.cjs → chunk-FEN5S772.cjs} +36 -36
  63. package/dist/{chunk-NRKD4F5X.cjs.map → chunk-FEN5S772.cjs.map} +1 -1
  64. package/dist/{chunk-2SMCH62O.cjs → chunk-FP2RLYQZ.cjs} +11 -11
  65. package/dist/{chunk-2SMCH62O.cjs.map → chunk-FP2RLYQZ.cjs.map} +1 -1
  66. package/dist/{chunk-MOME6KYD.mjs → chunk-G5IEC7SR.mjs} +3 -3
  67. package/dist/{chunk-MOME6KYD.mjs.map → chunk-G5IEC7SR.mjs.map} +1 -1
  68. package/dist/{chunk-SE5IERVH.mjs → chunk-GYIO7W7M.mjs} +3 -3
  69. package/dist/{chunk-SE5IERVH.mjs.map → chunk-GYIO7W7M.mjs.map} +1 -1
  70. package/dist/{chunk-3Z3A7FHA.cjs → chunk-IEEAENLX.cjs} +48 -48
  71. package/dist/{chunk-3Z3A7FHA.cjs.map → chunk-IEEAENLX.cjs.map} +1 -1
  72. package/dist/{chunk-DFTVB66S.cjs → chunk-KNDLV4PI.cjs} +85 -85
  73. package/dist/{chunk-DFTVB66S.cjs.map → chunk-KNDLV4PI.cjs.map} +1 -1
  74. package/dist/{chunk-SSUOENAZ.mjs → chunk-KNEQRUBA.mjs} +3 -3
  75. package/dist/{chunk-SSUOENAZ.mjs.map → chunk-KNEQRUBA.mjs.map} +1 -1
  76. package/dist/chunk-KRETIZU6.mjs +2218 -0
  77. package/dist/chunk-KRETIZU6.mjs.map +1 -0
  78. package/dist/{chunk-CGILA3WO.mjs → chunk-N2XQF2OL.mjs} +5 -3
  79. package/dist/{chunk-CGILA3WO.mjs.map → chunk-N2XQF2OL.mjs.map} +1 -1
  80. package/dist/{chunk-EUADAUBQ.mjs → chunk-N4MZYNR4.mjs} +4 -4
  81. package/dist/{chunk-EUADAUBQ.mjs.map → chunk-N4MZYNR4.mjs.map} +1 -1
  82. package/dist/chunk-NRXYYO5V.cjs +2257 -0
  83. package/dist/chunk-NRXYYO5V.cjs.map +1 -0
  84. package/dist/{chunk-GGKGH5PM.mjs → chunk-OBRSGM64.mjs} +4 -4
  85. package/dist/{chunk-GGKGH5PM.mjs.map → chunk-OBRSGM64.mjs.map} +1 -1
  86. package/dist/{chunk-6JTB2X72.mjs → chunk-ODO4GMW7.mjs} +3 -3
  87. package/dist/{chunk-6JTB2X72.mjs.map → chunk-ODO4GMW7.mjs.map} +1 -1
  88. package/dist/{chunk-WGEGR3DF.cjs → chunk-OLISEQHS.cjs} +5 -2
  89. package/dist/{chunk-WGEGR3DF.cjs.map → chunk-OLISEQHS.cjs.map} +1 -1
  90. package/dist/{chunk-PZKAH7WQ.mjs → chunk-PVAX67JG.mjs} +3 -3
  91. package/dist/{chunk-PZKAH7WQ.mjs.map → chunk-PVAX67JG.mjs.map} +1 -1
  92. package/dist/{chunk-PRPG2T2E.cjs → chunk-QJ6GTUCO.cjs} +6 -6
  93. package/dist/{chunk-PRPG2T2E.cjs.map → chunk-QJ6GTUCO.cjs.map} +1 -1
  94. package/dist/chunk-QW4RBGHN.cjs +961 -0
  95. package/dist/chunk-QW4RBGHN.cjs.map +1 -0
  96. package/dist/{chunk-33AMWFBZ.cjs → chunk-SGP7V2UW.cjs} +15 -15
  97. package/dist/{chunk-33AMWFBZ.cjs.map → chunk-SGP7V2UW.cjs.map} +1 -1
  98. package/dist/{chunk-FX2QFYWF.mjs → chunk-VWQ5WOIL.mjs} +3 -3
  99. package/dist/{chunk-FX2QFYWF.mjs.map → chunk-VWQ5WOIL.mjs.map} +1 -1
  100. package/dist/{chunk-ZLQHUZDU.cjs → chunk-YDPDTOSP.cjs} +139 -139
  101. package/dist/{chunk-ZLQHUZDU.cjs.map → chunk-YDPDTOSP.cjs.map} +1 -1
  102. package/dist/{chunk-77HQWEQ6.cjs → chunk-YW5IVWHQ.cjs} +33 -33
  103. package/dist/{chunk-77HQWEQ6.cjs.map → chunk-YW5IVWHQ.cjs.map} +1 -1
  104. package/dist/{chunk-YXBOAGIM.cjs → chunk-YXZ6GU7H.cjs} +7 -7
  105. package/dist/{chunk-YXBOAGIM.cjs.map → chunk-YXZ6GU7H.cjs.map} +1 -1
  106. package/dist/{chunk-62Y65TGK.mjs → chunk-ZUFTH5IR.mjs} +8 -631
  107. package/dist/chunk-ZUFTH5IR.mjs.map +1 -0
  108. package/dist/components-EHOGXATG.cjs +22 -0
  109. package/dist/{components-5UXYNAKR.cjs.map → components-EHOGXATG.cjs.map} +1 -1
  110. package/dist/components-MQ6DR7TX.cjs +26 -0
  111. package/dist/{components-CFXOEVPN.mjs.map → components-MQ6DR7TX.cjs.map} +1 -1
  112. package/dist/components-XRX7QGLB.mjs +5 -0
  113. package/dist/{components-WYEZL5TE.cjs.map → components-XRX7QGLB.mjs.map} +1 -1
  114. package/dist/components-YATKRWLH.mjs +5 -0
  115. package/dist/{components-ZAGG2PBO.mjs.map → components-YATKRWLH.mjs.map} +1 -1
  116. package/dist/file-icon/index.cjs +6 -6
  117. package/dist/file-icon/index.mjs +1 -1
  118. package/dist/index.cjs +735 -215
  119. package/dist/index.cjs.map +1 -1
  120. package/dist/index.d.cts +972 -39
  121. package/dist/index.d.ts +972 -39
  122. package/dist/index.mjs +387 -31
  123. package/dist/index.mjs.map +1 -1
  124. package/dist/tree/index.cjs +38 -38
  125. package/dist/tree/index.d.cts +2 -2
  126. package/dist/tree/index.d.ts +2 -2
  127. package/dist/tree/index.mjs +3 -3
  128. package/package.json +6 -6
  129. package/src/index.ts +5 -0
  130. package/src/stories/index.ts +3 -1
  131. package/src/tools/Chat/Chat.story.tsx +1006 -0
  132. package/src/tools/Chat/README.md +528 -0
  133. package/src/tools/Chat/components/Attachments.tsx +192 -0
  134. package/src/tools/Chat/components/ChatRoot.tsx +201 -0
  135. package/src/tools/Chat/components/Composer.tsx +134 -0
  136. package/src/tools/Chat/components/EmptyState.tsx +47 -0
  137. package/src/tools/Chat/components/ErrorBanner.tsx +47 -0
  138. package/src/tools/Chat/components/JumpToLatest.tsx +30 -0
  139. package/src/tools/Chat/components/MessageActions.tsx +72 -0
  140. package/src/tools/Chat/components/MessageBubble.tsx +228 -0
  141. package/src/tools/Chat/components/MessageList.tsx +82 -0
  142. package/src/tools/Chat/components/Sources.tsx +55 -0
  143. package/src/tools/Chat/components/StreamingIndicator.tsx +29 -0
  144. package/src/tools/Chat/components/ToolCalls.tsx +172 -0
  145. package/src/tools/Chat/components/index.ts +24 -0
  146. package/src/tools/Chat/config.ts +55 -0
  147. package/src/tools/Chat/context/ChatProvider.tsx +122 -0
  148. package/src/tools/Chat/context/index.ts +9 -0
  149. package/src/tools/Chat/core/audio/audioBus.ts +172 -0
  150. package/src/tools/Chat/core/audio/index.ts +8 -0
  151. package/src/tools/Chat/core/audio/preferences.ts +68 -0
  152. package/src/tools/Chat/core/audio/types.ts +49 -0
  153. package/src/tools/Chat/core/ids.ts +16 -0
  154. package/src/tools/Chat/core/index.ts +5 -0
  155. package/src/tools/Chat/core/markdown.ts +56 -0
  156. package/src/tools/Chat/core/payload-dispatch.ts +54 -0
  157. package/src/tools/Chat/core/persona.ts +35 -0
  158. package/src/tools/Chat/core/reducer.ts +335 -0
  159. package/src/tools/Chat/core/transport/http.ts +167 -0
  160. package/src/tools/Chat/core/transport/index.ts +13 -0
  161. package/src/tools/Chat/core/transport/mock.ts +134 -0
  162. package/src/tools/Chat/core/transport/sse.ts +116 -0
  163. package/src/tools/Chat/core/transport/types.ts +24 -0
  164. package/src/tools/Chat/hooks/index.ts +26 -0
  165. package/src/tools/Chat/hooks/useChat.ts +440 -0
  166. package/src/tools/Chat/hooks/useChatAudio.ts +191 -0
  167. package/src/tools/Chat/hooks/useChatComposer.ts +227 -0
  168. package/src/tools/Chat/hooks/useChatHistory.ts +59 -0
  169. package/src/tools/Chat/hooks/useChatLayout.ts +111 -0
  170. package/src/tools/Chat/hooks/useChatLightbox.ts +34 -0
  171. package/src/tools/Chat/hooks/useChatScroll.ts +132 -0
  172. package/src/tools/Chat/index.ts +158 -0
  173. package/src/tools/Chat/lazy.tsx +14 -0
  174. package/src/tools/Chat/types.ts +237 -0
  175. package/src/tools/Chat/utils/collectImageAttachments.ts +13 -0
  176. package/src/tools/Map/README.md +384 -0
  177. package/dist/DocsLayout-CTJINVBM.mjs.map +0 -1
  178. package/dist/DocsLayout-XLDB6CJ2.cjs.map +0 -1
  179. package/dist/JsonSchemaForm-6WMS4CIY.cjs +0 -13
  180. package/dist/JsonSchemaForm-KX4JT3M4.mjs +0 -4
  181. package/dist/JsonTree-F27RMYSI.cjs +0 -11
  182. package/dist/JsonTree-QTJYSHCV.mjs +0 -5
  183. package/dist/Player-M3GC3VPE.mjs +0 -4
  184. package/dist/Player-ZL2X5LGG.cjs +0 -13
  185. package/dist/TreeRoot-A3J65L6F.mjs +0 -4
  186. package/dist/TreeRoot-DSK5JILT.cjs +0 -19
  187. package/dist/chunk-62Y65TGK.mjs.map +0 -1
  188. package/dist/chunk-TKSFZHCG.cjs +0 -1597
  189. package/dist/chunk-TKSFZHCG.cjs.map +0 -1
  190. package/dist/components-5UXYNAKR.cjs +0 -22
  191. package/dist/components-CFXOEVPN.mjs +0 -5
  192. package/dist/components-WYEZL5TE.cjs +0 -26
  193. 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/)