@djangocfg/ui-tools 2.1.431 → 2.1.433

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 (171) hide show
  1. package/package.json +28 -14
  2. package/src/common/CodePanelHeader/CodePanelHeader.tsx +121 -0
  3. package/src/common/CodePanelHeader/index.ts +1 -0
  4. package/src/common/blocks/BlockError.tsx +46 -0
  5. package/src/common/blocks/index.ts +34 -0
  6. package/src/common/blocks/schemas.ts +143 -0
  7. package/src/common/blocks/types.ts +76 -0
  8. package/src/common/chips/README.md +67 -0
  9. package/src/common/chips/UrlChip.tsx +105 -0
  10. package/src/common/chips/index.ts +20 -0
  11. package/src/common/link-preview/LinkPreviewCard.tsx +178 -0
  12. package/src/common/link-preview/index.ts +12 -0
  13. package/src/common/link-preview/types.ts +44 -0
  14. package/src/common/tiptap/index.ts +10 -0
  15. package/src/common/tiptap/useNodeAttrs.ts +128 -0
  16. package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +1 -1
  17. package/src/tools/chat/README.md +88 -2
  18. package/src/tools/chat/composer/Composer.tsx +45 -12
  19. package/src/tools/chat/composer/ComposerRichTextarea.tsx +16 -4
  20. package/src/tools/chat/constants.ts +9 -2
  21. package/src/tools/chat/core/extractFirstUrl.ts +45 -0
  22. package/src/tools/chat/core/reducer.ts +32 -8
  23. package/src/tools/chat/core/transport/mappers/pydantic-ai.ts +1 -1
  24. package/src/tools/chat/core/transport/mock.ts +4 -1
  25. package/src/tools/chat/hooks/useChat.ts +20 -20
  26. package/src/tools/chat/lazy.tsx +10 -0
  27. package/src/tools/chat/messages/Attachments.tsx +267 -42
  28. package/src/tools/chat/messages/BubbleContextMenu.tsx +203 -0
  29. package/src/tools/chat/messages/MessageBubble.tsx +175 -31
  30. package/src/tools/chat/messages/MessageList.tsx +107 -21
  31. package/src/tools/chat/messages/ToolCalls.tsx +28 -13
  32. package/src/tools/chat/messages/blocks/MessageBlocks.tsx +70 -17
  33. package/src/tools/chat/messages/blocks/builtin.tsx +6 -3
  34. package/src/tools/chat/messages/blocks/renderers/AudioBlock.tsx +6 -0
  35. package/src/tools/chat/messages/blocks/renderers/ChatImageAlbum.tsx +154 -0
  36. package/src/tools/chat/messages/blocks/renderers/CodeBlock.tsx +94 -6
  37. package/src/tools/chat/messages/blocks/renderers/DiffBlock.tsx +29 -0
  38. package/src/tools/chat/messages/blocks/renderers/GalleryBlock.tsx +17 -10
  39. package/src/tools/chat/messages/blocks/renderers/ImageBlock.tsx +37 -16
  40. package/src/tools/chat/messages/blocks/renderers/JsonBlock.tsx +11 -6
  41. package/src/tools/chat/messages/blocks/renderers/LinkBlock.tsx +32 -0
  42. package/src/tools/chat/messages/blocks/renderers/MapBlock.tsx +181 -19
  43. package/src/tools/chat/messages/blocks/renderers/VideoBlock.tsx +2 -5
  44. package/src/tools/chat/messages/index.ts +13 -0
  45. package/src/tools/chat/public.ts +5 -1
  46. package/src/tools/chat/shell/ChatRoot.tsx +29 -0
  47. package/src/tools/chat/styles/bubbleTokens.ts +18 -0
  48. package/src/tools/chat/styles/index.ts +1 -0
  49. package/src/tools/chat/types/attachment.ts +26 -2
  50. package/src/tools/chat/types/block.ts +56 -9
  51. package/src/tools/chat/types/blockSchemas.ts +197 -0
  52. package/src/tools/chat/types/config.ts +33 -0
  53. package/src/tools/chat/types/events.ts +14 -7
  54. package/src/tools/chat/types/index.ts +4 -2
  55. package/src/tools/chat/types/message.ts +7 -0
  56. package/src/tools/data/JsonTree/JsonViewer.tsx +7 -7
  57. package/src/tools/dev/Map/README.md +30 -345
  58. package/src/tools/dev/Map/cards/index.ts +6 -0
  59. package/src/tools/dev/Map/cards/types.ts +67 -0
  60. package/src/tools/dev/Map/components/CompassChip.tsx +67 -0
  61. package/src/tools/dev/Map/components/DraggableMarkers.tsx +79 -0
  62. package/src/tools/dev/Map/components/GeocoderControl.tsx +148 -63
  63. package/src/tools/dev/Map/components/GeocoderInput.tsx +181 -0
  64. package/src/tools/dev/Map/components/MapContainer/FullscreenDialog.tsx +140 -0
  65. package/src/tools/dev/Map/components/MapContainer/GeolocateHandle.tsx +70 -0
  66. package/src/tools/dev/Map/components/MapContainer/MapInner.tsx +326 -0
  67. package/src/tools/dev/Map/components/MapContainer/MapToolbar.tsx +286 -0
  68. package/src/tools/dev/Map/components/MapContainer/RotateHint.tsx +25 -0
  69. package/src/tools/dev/Map/components/MapContainer/ScrollLockOverlay.tsx +52 -0
  70. package/src/tools/dev/Map/components/MapContainer/constants.ts +15 -0
  71. package/src/tools/dev/Map/components/MapContainer/hooks/useAutoReset.ts +51 -0
  72. package/src/tools/dev/Map/components/MapContainer/hooks/useBasemapState.ts +48 -0
  73. package/src/tools/dev/Map/components/MapContainer/hooks/useExternalMapsUrl.ts +29 -0
  74. package/src/tools/dev/Map/components/MapContainer/index.tsx +29 -0
  75. package/src/tools/dev/Map/components/MapContainer/types.ts +198 -0
  76. package/src/tools/dev/Map/components/MapControls.tsx +27 -0
  77. package/src/tools/dev/Map/components/MapMarkers.tsx +94 -0
  78. package/src/tools/dev/Map/components/MarkerCard.tsx +102 -0
  79. package/src/tools/dev/Map/components/MeasureControl.tsx +219 -0
  80. package/src/tools/dev/Map/components/index.ts +35 -1
  81. package/src/tools/dev/Map/geocode/index.ts +2 -0
  82. package/src/tools/dev/Map/geocode/photon.ts +265 -0
  83. package/src/tools/dev/Map/hooks/index.ts +37 -0
  84. package/src/tools/dev/Map/hooks/useCompass.ts +44 -0
  85. package/src/tools/dev/Map/hooks/useDraggableMarkers.ts +112 -0
  86. package/src/tools/dev/Map/hooks/useGeocoder.ts +155 -0
  87. package/src/tools/dev/Map/hooks/useGeolocation.ts +174 -0
  88. package/src/tools/dev/Map/hooks/useMapControl.ts +5 -1
  89. package/src/tools/dev/Map/hooks/useMapImages.ts +112 -0
  90. package/src/tools/dev/Map/hooks/useMapScrollProtection.ts +155 -0
  91. package/src/tools/dev/Map/hooks/useMapTouchMode.ts +21 -0
  92. package/src/tools/dev/Map/hooks/useMarkerCard.ts +39 -0
  93. package/src/tools/dev/Map/hooks/useMarkers.ts +9 -3
  94. package/src/tools/dev/Map/hooks/useMeasure.ts +169 -0
  95. package/src/tools/dev/Map/hooks/useTerrain.ts +123 -0
  96. package/src/tools/dev/Map/index.ts +59 -2
  97. package/src/tools/dev/Map/layers/index.ts +1 -0
  98. package/src/tools/dev/Map/layers/symbol.ts +93 -0
  99. package/src/tools/dev/Map/lazy.tsx +120 -0
  100. package/src/tools/dev/Map/protocols/pmtiles.ts +73 -0
  101. package/src/tools/dev/Map/styles/index.ts +62 -0
  102. package/src/tools/dev/Map/styles/terrain.ts +29 -0
  103. package/src/tools/dev/Map/types.ts +31 -3
  104. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +1 -1
  105. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +5 -12
  106. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +1 -1
  107. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +1 -1
  108. package/src/tools/dev/api/OpenapiViewer/components/shared/SendButton.tsx +1 -1
  109. package/src/tools/dev/api/OpenapiViewer/components/shared/http-colors.ts +61 -0
  110. package/src/tools/dev/api/OpenapiViewer/components/shared/ui.tsx +4 -21
  111. package/src/tools/dev/code/MarkdownMessage/MarkdownMessage.tsx +150 -58
  112. package/src/tools/dev/code/MarkdownMessage/README.md +38 -0
  113. package/src/tools/dev/code/MarkdownMessage/builtinRules.tsx +92 -0
  114. package/src/tools/dev/code/MarkdownMessage/index.ts +3 -0
  115. package/src/tools/dev/code/MarkdownMessage/streamingBlocks.ts +164 -0
  116. package/src/tools/dev/code/MarkdownMessage/types.ts +20 -0
  117. package/src/tools/dev/code/PrettyCode/PrettyCode.client.tsx +27 -30
  118. package/src/tools/dev/code/PrettyCode/index.tsx +4 -1
  119. package/src/tools/dev/code/PrettyCode/lazy.tsx +3 -1
  120. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +168 -3
  121. package/src/tools/forms/MarkdownEditor/README.md +84 -0
  122. package/src/tools/forms/MarkdownEditor/chip/ChipNode.ts +454 -0
  123. package/src/tools/forms/MarkdownEditor/chip/syncChips.ts +80 -0
  124. package/src/tools/forms/MarkdownEditor/filePath/FilePathChip.tsx +68 -0
  125. package/src/tools/forms/MarkdownEditor/filePath/__tests__/detect.test.ts +161 -0
  126. package/src/tools/forms/MarkdownEditor/filePath/detect.ts +289 -0
  127. package/src/tools/forms/MarkdownEditor/filePath/index.ts +15 -0
  128. package/src/tools/forms/MarkdownEditor/index.ts +19 -0
  129. package/src/tools/forms/MarkdownEditor/lazy.tsx +10 -0
  130. package/src/tools/forms/MarkdownEditor/styles.css +134 -2
  131. package/src/tools/forms/MarkdownEditor/url/__tests__/detect.test.ts +121 -0
  132. package/src/tools/forms/MarkdownEditor/url/detect.ts +227 -0
  133. package/src/tools/forms/NotionEditor/BookmarkNode.ts +158 -0
  134. package/src/tools/forms/NotionEditor/BookmarkView.tsx +45 -0
  135. package/src/tools/forms/NotionEditor/MapEditLayer.tsx +199 -0
  136. package/src/tools/forms/NotionEditor/MapNode.ts +218 -0
  137. package/src/tools/forms/NotionEditor/MapView.tsx +103 -0
  138. package/src/tools/forms/NotionEditor/NotionEditor.tsx +7 -2
  139. package/src/tools/forms/NotionEditor/README.md +109 -188
  140. package/src/tools/forms/NotionEditor/extensions.ts +15 -0
  141. package/src/tools/forms/NotionEditor/slashItems.ts +19 -0
  142. package/src/tools/forms/NotionEditor/styles.css +25 -0
  143. package/src/tools/forms/NotionEditor/types.ts +9 -0
  144. package/src/tools/input/SpeechRecognition/core/engine/external.ts +5 -0
  145. package/src/tools/input/SpeechRecognition/types.ts +10 -0
  146. package/src/tools/media/Gallery/components/lightbox/GalleryLightbox.tsx +39 -22
  147. package/src/tools/media/ImageViewer/README.md +9 -0
  148. package/src/tools/media/ImageViewer/components/ImageViewer.tsx +165 -79
  149. package/src/tools/media/ImageViewer/components/LightboxChrome.tsx +137 -0
  150. package/src/tools/media/ImageViewer/components/MiddleEllipsis.tsx +46 -0
  151. package/src/tools/media/ImageViewer/components/index.ts +2 -0
  152. package/src/tools/media/ImageViewer/hooks/index.ts +3 -0
  153. package/src/tools/media/ImageViewer/hooks/useIdleChrome.ts +73 -0
  154. package/src/tools/media/ImageViewer/types.ts +10 -0
  155. package/src/tools/media/ImageViewer/utils/constants.ts +28 -0
  156. package/src/tools/media/ImageViewer/utils/index.ts +4 -0
  157. package/src/tools/media/VideoPlayer/VideoPlayer.tsx +34 -40
  158. package/src/tools/media/VideoPlayer/canvas/youtube-canvas.tsx +13 -13
  159. package/src/tools/media/VideoPlayer/parts/center-play.tsx +26 -0
  160. package/src/tools/media/VideoPlayer/parts/index.ts +2 -0
  161. package/src/tools/media/VideoPlayer/styles/video-player.css +102 -53
  162. package/src/tools/visual/charts/SmoothLine/README.md +97 -0
  163. package/src/tools/visual/charts/SmoothLine/SmoothLine.tsx +177 -0
  164. package/src/tools/visual/charts/SmoothLine/index.ts +21 -0
  165. package/src/tools/visual/charts/SmoothLine/lazy.tsx +23 -0
  166. package/src/tools/visual/charts/SmoothLine/smoothPath.ts +209 -0
  167. package/src/tools/visual/charts/SmoothLine/types.ts +98 -0
  168. package/src/tools/visual/charts/SmoothLine/useSmoothSeries.ts +429 -0
  169. package/src/tools/visual/indicators/Fps/types.ts +1 -1
  170. package/src/tools/chat/messages/blocks/renderers/LottieBlock.tsx +0 -11
  171. package/src/tools/dev/Map/components/MapContainer.tsx +0 -198
@@ -1,16 +1,16 @@
1
1
  # Map
2
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.
3
+ Interactive maps for `@djangocfg/ui-tools`, built on [MapLibre GL](https://maplibre.org/) via `react-map-gl` markers, clustered GeoJSON, popups, layer factories, free OpenFreeMap / PMTiles basemaps, a runtime basemap switcher, 3D terrain, address/place autocomplete (geocoder), a compass, click-to-measure, draggable markers, custom marker icons, and an opt-in locate-me (GPS) chip, all behind a small Provider/Context surface.
4
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.
5
+ > **Bundle warning.** MapLibre GL is heavy (~800 KB gzipped). The `@djangocfg/ui-tools/map` entry is already the **lazy** one — `LazyMapContainer` defers the MapLibre chunk until it mounts. All components are `'use client'`; never render them on the Next.js server.
6
6
 
7
7
  ## Quick start
8
8
 
9
9
  ```tsx
10
- import { MapContainer, MapMarker } from '@djangocfg/ui-tools/map';
10
+ import { LazyMapContainer, MapMarker } from '@djangocfg/ui-tools/map';
11
11
 
12
12
  <div className="h-96 rounded-xl overflow-hidden">
13
- <MapContainer
13
+ <LazyMapContainer
14
14
  initialViewport={{ latitude: 25.08, longitude: 55.14, zoom: 13 }}
15
15
  mapStyle="light"
16
16
  >
@@ -19,366 +19,51 @@ import { MapContainer, MapMarker } from '@djangocfg/ui-tools/map';
19
19
  color="#10b981"
20
20
  size={32}
21
21
  />
22
- </MapContainer>
22
+ </LazyMapContainer>
23
23
  </div>
24
24
  ```
25
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.
26
+ The container provides its own `MapProvider`. To use the map context from a sibling (e.g. a button calling `flyTo`), wrap your tree in `<MapProvider>` and render `<MapView>` inside it. See [container](./@docs/container.md).
27
27
 
28
- ## Public exports
28
+ ## Public exports — `@djangocfg/ui-tools/map`
29
29
 
30
- ### Components
30
+ The package subpath resolves to the lazy entry. It surfaces:
31
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
32
+ **Components** `LazyMapContainer`, `LazyMapView` (code-split), `MapView`-via-`MapContainer` types, plus the light primitives: `MapMarker`, `MapPopup`, `MapCluster`, `MapSource`, `MapLayer`, `MapControls`, `CustomOverlay`, `MapLegend`, `LayerSwitcher`, `MeasureControl`, `DraggableMarkers`, `GeocoderInput`, `GeocoderControl`, `CompassChip`, `MarkerCard`, `MapMarkers`.
126
33
 
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:
34
+ **Context** — `MapProvider`, `useMapContext`, `MapContext`.
169
35
 
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
- ```
36
+ **Styles** — `MAP_STYLES`, `getMapStyle`, `MAP_STYLE_LABELS`, `KEY_FREE_STYLES`, `resolveBasemapOptions`, and the free terrain DEM source `TERRAIN_DEM` / `TERRAIN_DEM_URL`.
191
37
 
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.
38
+ **PMTiles** `addPMTilesProtocol`, `pmtilesSourceUrl` (self-hosted / offline basemaps).
193
39
 
194
- ## Layers
40
+ **Geocoder** — `photonResolve` (default key-free Photon resolver), `cameraForResult` (smart-zoom helper), and types `GeocodeResult`, `GeocodeKind`, `ResolveGeocode`. See [geocoder](./@docs/geocoder.md).
195
41
 
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
- ```
42
+ **Types** `MapContainerProps`, `GeolocateOptions`, `GeolocatePosition`, `MapMarkerProps`, `MapPopupProps`, `MapClusterProps`, `MapSourceProps`, `MapLayerProps`, `MapControlsProps`, `MeasureControlProps`, `DraggableMarkersProps`, `DraggablePoint`, `GeocoderInputProps`, `GeocoderControlProps`, `CompassChipProps`, `MapProviderProps`, `MapViewport`, `MapStyleKey`, `MarkerData`, plus hook types `GeolocationFix`, `GeolocationStatus`, `UseGeolocationOptions`, `UseGeolocationResult`, `UseGeocoderOptions`, `UseGeocoderResult`, `UseCompassResult`, `UseTerrainOptions`, `UseMeasureResult`, `MeasureMode`, `MapImage`, `UseMapImagesResult`, `UseDraggableMarkersOptions`, `UseDraggableMarkersResult`, `DraggableMarkerHandlers`, plus the marker-card types `MarkerCardProps`, `MapMarkersProps`, `MarkerWithCard`, `MarkerCardInfo` (the card data shape), `MarkerCardAction`, `MarkerCardData`, `MarkerCardActionData`, `UseMarkerCardResult`.
359
43
 
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.
44
+ **Hooks** — `useMap`, `useMapControl`, `useMarkers`, `useMapEvents`, `useMapViewport`, `useMapLayers`, `useControl`, `useSpiderfy` (+ `UseSpiderfyOptions` / `UseSpiderfyResult` types), `useMarkerCard` (single-open marker-card selection state), the feature hooks `useCompass`, `useTerrain`, `useMeasure`, `useMapImages`, `useDraggableMarkers`, `useGeocoder`, and the map-independent `useGeolocation` (live browser position for custom "where am I" UI; see [hooks](./@docs/hooks.md#usegeolocationoptions)). `useMapScrollProtection` / `useMapTouchMode` are also exported — they power `scrollProtection`, which wires them for you.
361
45
 
362
- ## SSR
46
+ **Layer factories** — `createClusterLayers`, `createPointLayer`, `createHeatmapLayer`, `createPolygonLayer`, `createPolygonOutlineLayer`, `createHighlightLayer`, `createLineLayer`, `createRouteLayers`, `createDashedLineLayer`, `createAnimatedLineLayer`, `createSymbolLayer`.
363
47
 
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`.
48
+ **Geometry utils** `calculateBounds`, `calculateCenter`, `calculateDistance`, `isPointInBounds`, `expandBounds`, `toGeoJSON`, `fromGeoJSON`, `createPoint`, `createPolygon`, `createLineString`, `createFeatureCollection`.
365
49
 
366
- ## Stories
50
+ **Spiderfy utils** — `offsetOverlappingMarkers`, `getSpiderfyPositions`, `groupOverlappingMarkers`, `hasOverlappingMarkers`, `getOverlapStats`.
367
51
 
368
- Run `pnpm playground` from `packages/ui-tools/`. Map stories in `Map.story.tsx`:
52
+ ## Docs
369
53
 
370
- | Story | Demonstrates |
54
+ | Doc | Covers |
371
55
  |---|---|
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` |
56
+ | [container.md](./@docs/container.md) | `MapContainer` / `MapView` full props, control toolbar (search · terrain · compass · locate-me / GPS chips), scroll protection & mobile, fullscreen dialog. |
57
+ | [basemaps.md](./@docs/basemaps.md) | Map styles, the runtime basemap switcher, OpenFreeMap, satellite/keys, PMTiles, 3D terrain (free DEM). |
58
+ | [markers-popups.md](./@docs/markers-popups.md) | `MapMarker`, `MapPopup`, `MapCluster`, marker info-cards (`MarkerCard` / `MapMarkers` / `useMarkerCard`), draggable markers, overlapping points / spiderfy. |
59
+ | [layers.md](./@docs/layers.md) | `MapSource` / `MapLayer`, the layer factories, custom marker icons (symbol layer), geometry utilities. |
60
+ | [geocoder.md](./@docs/geocoder.md) | Address/place autocomplete — `GeocoderInput` / `GeocoderControl`, the toolbar search chip, `photonResolve`, smart zoom (`cameraForResult`), `useGeocoder`. |
61
+ | [hooks.md](./@docs/hooks.md) | Every hook, with an example. |
62
+ | [advanced.md](./@docs/advanced.md) | `CustomOverlay`, `MapLegend`, `LayerSwitcher`, `MeasureControl`, the DrawControl recipe, lazy loading, SSR. |
63
+ | [stories.md](./@docs/stories.md) | The Storybook stories list. |
379
64
 
380
65
  ## Related
381
66
 
382
- - Public surface: `src/tools/Map/index.ts`
383
- - Lazy preset: `src/tools/Map/lazy.tsx`
67
+ - Public entry: `lazy.tsx` (what `@djangocfg/ui-tools/map` resolves to)
68
+ - Full barrel: `index.ts` — the subpath re-exports the same light surface (hooks, layer factories, geometry + spiderfy utils, styles, PMTiles helpers).
384
69
  - 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/)
@@ -0,0 +1,6 @@
1
+ export type {
2
+ MarkerCard,
3
+ MarkerCardAction,
4
+ MarkerCardData,
5
+ MarkerCardActionData,
6
+ } from './types'
@@ -0,0 +1,67 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ /**
4
+ * Marker info-card data — the Google-Maps-style card that opens above a pin.
5
+ *
6
+ * This is the FULL, in-app shape. The SERIALIZABLE subset (everything except
7
+ * the function/React parts of `actions`) is what the chat `MapBlock` wire
8
+ * format reuses — see `MarkerCardData` below and `MapBlock.markers[].card`.
9
+ */
10
+ export interface MarkerCard {
11
+ /** Primary heading (required). */
12
+ title: string
13
+ /** Supporting copy, clamped to ~3 lines in the default card. */
14
+ description?: string
15
+ /** Image URL rendered as a rounded header (object-cover). */
16
+ image?: string
17
+ /** Small pill above the title (e.g. a category). */
18
+ badge?: string
19
+ /** Action row — buttons/links rendered below the body. */
20
+ actions?: MarkerCardAction[]
21
+ }
22
+
23
+ /**
24
+ * A single action in a card's action row.
25
+ *
26
+ * Provide `href` for a link (renders an `<a>`) OR `onClick` for an in-app
27
+ * callback (renders a `<button>`). `icon`/`onClick` are function/React parts
28
+ * that do NOT cross the chat wire — the serializable subset is href-only.
29
+ */
30
+ export interface MarkerCardAction {
31
+ /** Visible label. */
32
+ label: string
33
+ /** Link target → renders an anchor. */
34
+ href?: string
35
+ /** In-app callback → renders a button. Ignored when `href` is set. */
36
+ onClick?: () => void
37
+ /** Optional leading icon. */
38
+ icon?: ReactNode
39
+ /** Button style — maps to ui-core Button variants. */
40
+ variant?: 'default' | 'outline' | 'ghost'
41
+ }
42
+
43
+ // ============================================================================
44
+ // Serializable subset — the chat wire shape
45
+ // ============================================================================
46
+
47
+ /**
48
+ * Plain-JSON action — the wire subset of {@link MarkerCardAction}. No
49
+ * `onClick`/`icon` (functions/React don't serialize); links only.
50
+ */
51
+ export interface MarkerCardActionData {
52
+ label: string
53
+ href?: string
54
+ }
55
+
56
+ /**
57
+ * Plain-JSON card — the wire subset of {@link MarkerCard}. Strings/URLs only,
58
+ * actions are href-only. Carried by the chat `MapBlock` so a marker can open
59
+ * a card with no host code. Assignable to {@link MarkerCard}.
60
+ */
61
+ export interface MarkerCardData {
62
+ title: string
63
+ description?: string
64
+ image?: string
65
+ badge?: string
66
+ actions?: MarkerCardActionData[]
67
+ }
@@ -0,0 +1,67 @@
1
+ 'use client'
2
+
3
+ import { Compass } from 'lucide-react'
4
+ import { cn } from '@djangocfg/ui-core/lib'
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@djangocfg/ui-core/components'
6
+
7
+ import { MAP_CONTROL_CLASS } from './MapContainer/constants'
8
+
9
+ export interface CompassChipProps {
10
+ /** Current camera bearing in degrees; the needle counter-rotates by this. */
11
+ bearing: number
12
+ /**
13
+ * Whether the view is rotated/tilted off true north. Drives the color cue:
14
+ * an accent needle (Google's gray→red "you're off-north") when oriented,
15
+ * the neutral chip foreground otherwise. The chip is typically only rendered
16
+ * while oriented (the caller gates it), so the visible needle reads accent.
17
+ */
18
+ oriented: boolean
19
+ /** Click handler — ease the camera back to a north-up, top-down view. */
20
+ onReset: () => void
21
+ /** Accessible / tooltip label. @default 'Reset bearing to north' */
22
+ label?: string
23
+ /** Extra classes merged onto the chip button. */
24
+ className?: string
25
+ }
26
+
27
+ /**
28
+ * Compass chip — a Google-Maps-style reset-to-north control.
29
+ *
30
+ * Presentational: the needle counter-rotates by `bearing` so it keeps
31
+ * pointing at true north as the map turns underneath it, and it tints to an
32
+ * ACCENT (`text-primary`) when `oriented` to signal "you're off true north"
33
+ * (our semantic stand-in for Google's red needle). Click eases the camera
34
+ * back to north-up + top-down via `onReset`. Keyboard-reachable; the chip
35
+ * lives inside the toolbar's `data-map-control` region so it never re-locks
36
+ * scroll. Mirrors the terrain/geolocate chip pattern (`MAP_CONTROL_CLASS`,
37
+ * `Tooltip`, semantic tokens only).
38
+ */
39
+ export function CompassChip({
40
+ bearing,
41
+ oriented,
42
+ onReset,
43
+ label = 'Reset bearing to north',
44
+ className,
45
+ }: CompassChipProps) {
46
+ return (
47
+ <Tooltip>
48
+ <TooltipTrigger asChild>
49
+ <button
50
+ type="button"
51
+ onClick={onReset}
52
+ aria-label={label}
53
+ className={cn(MAP_CONTROL_CLASS, 'w-9 h-9', oriented && 'text-primary', className)}
54
+ >
55
+ <Compass
56
+ className="w-4 h-4"
57
+ aria-hidden="true"
58
+ // Counter-rotate the needle so it points to true north as the
59
+ // map rotates underneath it.
60
+ style={{ transform: `rotate(${-bearing}deg)` }}
61
+ />
62
+ </button>
63
+ </TooltipTrigger>
64
+ <TooltipContent side="left">{label}</TooltipContent>
65
+ </Tooltip>
66
+ )
67
+ }
@@ -0,0 +1,79 @@
1
+ 'use client'
2
+
3
+ import { memo, type ReactNode } from 'react'
4
+
5
+ import { MapMarker } from './MapMarker'
6
+ import {
7
+ useDraggableMarkers,
8
+ type DraggablePoint,
9
+ } from '../hooks/useDraggableMarkers'
10
+ import type { MarkerData } from '../types'
11
+
12
+ export type { DraggablePoint }
13
+
14
+ export interface DraggableMarkersProps {
15
+ /** Controlled list of draggable positions. */
16
+ positions: DraggablePoint[]
17
+ /**
18
+ * Fires with the full, updated array when a marker is dropped (drag-end).
19
+ * The host persists `next` and feeds it back via `positions`.
20
+ */
21
+ onChange: (next: DraggablePoint[]) => void
22
+ /** Optional live callback fired on every drag frame (not committed). */
23
+ onDragMove?: (id: string, lng: number, lat: number) => void
24
+ /** Pin color (passed through to `MapMarker`). */
25
+ color?: string
26
+ /** Pin size in px (passed through to `MapMarker`). */
27
+ size?: number
28
+ /** Custom marker renderer; replaces the default pin. */
29
+ renderMarker?: (marker: MarkerData) => ReactNode
30
+ }
31
+
32
+ /**
33
+ * Thin controlled component that renders one draggable `MapMarker` per
34
+ * position. Must be mounted inside `<MapProvider>` (i.e. as a child of
35
+ * `MapContainer` / `MapView`), like `MapLegend`.
36
+ *
37
+ * Dragging a pin commits its new `{ lng, lat }` through `onChange`. The
38
+ * maplibre `LngLat` object delivered by the drag callbacks is normalized to
39
+ * plain numbers inside `useDraggableMarkers`.
40
+ */
41
+ function DraggableMarkersComponent({
42
+ positions,
43
+ onChange,
44
+ onDragMove,
45
+ color,
46
+ size,
47
+ renderMarker,
48
+ }: DraggableMarkersProps) {
49
+ const { markers, getHandlers } = useDraggableMarkers({
50
+ positions,
51
+ onChange,
52
+ onDragMove,
53
+ })
54
+
55
+ return (
56
+ <>
57
+ {markers.map((marker) => {
58
+ const handlers = getHandlers(marker.id)
59
+ return (
60
+ <MapMarker
61
+ key={marker.id}
62
+ marker={marker}
63
+ draggable
64
+ color={color}
65
+ size={size}
66
+ ariaLabel={`Draggable marker ${marker.id}`}
67
+ onDragStart={handlers.onDragStart}
68
+ onDrag={handlers.onDrag}
69
+ onDragEnd={handlers.onDragEnd}
70
+ >
71
+ {renderMarker ? renderMarker(marker) : undefined}
72
+ </MapMarker>
73
+ )
74
+ })}
75
+ </>
76
+ )
77
+ }
78
+
79
+ export const DraggableMarkers = memo(DraggableMarkersComponent)