@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.
- package/package.json +28 -14
- package/src/common/CodePanelHeader/CodePanelHeader.tsx +121 -0
- package/src/common/CodePanelHeader/index.ts +1 -0
- package/src/common/blocks/BlockError.tsx +46 -0
- package/src/common/blocks/index.ts +34 -0
- package/src/common/blocks/schemas.ts +143 -0
- package/src/common/blocks/types.ts +76 -0
- package/src/common/chips/README.md +67 -0
- package/src/common/chips/UrlChip.tsx +105 -0
- package/src/common/chips/index.ts +20 -0
- package/src/common/link-preview/LinkPreviewCard.tsx +178 -0
- package/src/common/link-preview/index.ts +12 -0
- package/src/common/link-preview/types.ts +44 -0
- package/src/common/tiptap/index.ts +10 -0
- package/src/common/tiptap/useNodeAttrs.ts +128 -0
- package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +1 -1
- package/src/tools/chat/README.md +88 -2
- package/src/tools/chat/composer/Composer.tsx +45 -12
- package/src/tools/chat/composer/ComposerRichTextarea.tsx +16 -4
- package/src/tools/chat/constants.ts +9 -2
- package/src/tools/chat/core/extractFirstUrl.ts +45 -0
- package/src/tools/chat/core/reducer.ts +32 -8
- package/src/tools/chat/core/transport/mappers/pydantic-ai.ts +1 -1
- package/src/tools/chat/core/transport/mock.ts +4 -1
- package/src/tools/chat/hooks/useChat.ts +20 -20
- package/src/tools/chat/lazy.tsx +10 -0
- package/src/tools/chat/messages/Attachments.tsx +267 -42
- package/src/tools/chat/messages/BubbleContextMenu.tsx +203 -0
- package/src/tools/chat/messages/MessageBubble.tsx +175 -31
- package/src/tools/chat/messages/MessageList.tsx +107 -21
- package/src/tools/chat/messages/ToolCalls.tsx +28 -13
- package/src/tools/chat/messages/blocks/MessageBlocks.tsx +70 -17
- package/src/tools/chat/messages/blocks/builtin.tsx +6 -3
- package/src/tools/chat/messages/blocks/renderers/AudioBlock.tsx +6 -0
- package/src/tools/chat/messages/blocks/renderers/ChatImageAlbum.tsx +154 -0
- package/src/tools/chat/messages/blocks/renderers/CodeBlock.tsx +94 -6
- package/src/tools/chat/messages/blocks/renderers/DiffBlock.tsx +29 -0
- package/src/tools/chat/messages/blocks/renderers/GalleryBlock.tsx +17 -10
- package/src/tools/chat/messages/blocks/renderers/ImageBlock.tsx +37 -16
- package/src/tools/chat/messages/blocks/renderers/JsonBlock.tsx +11 -6
- package/src/tools/chat/messages/blocks/renderers/LinkBlock.tsx +32 -0
- package/src/tools/chat/messages/blocks/renderers/MapBlock.tsx +181 -19
- package/src/tools/chat/messages/blocks/renderers/VideoBlock.tsx +2 -5
- package/src/tools/chat/messages/index.ts +13 -0
- package/src/tools/chat/public.ts +5 -1
- package/src/tools/chat/shell/ChatRoot.tsx +29 -0
- package/src/tools/chat/styles/bubbleTokens.ts +18 -0
- package/src/tools/chat/styles/index.ts +1 -0
- package/src/tools/chat/types/attachment.ts +26 -2
- package/src/tools/chat/types/block.ts +56 -9
- package/src/tools/chat/types/blockSchemas.ts +197 -0
- package/src/tools/chat/types/config.ts +33 -0
- package/src/tools/chat/types/events.ts +14 -7
- package/src/tools/chat/types/index.ts +4 -2
- package/src/tools/chat/types/message.ts +7 -0
- package/src/tools/data/JsonTree/JsonViewer.tsx +7 -7
- package/src/tools/dev/Map/README.md +30 -345
- package/src/tools/dev/Map/cards/index.ts +6 -0
- package/src/tools/dev/Map/cards/types.ts +67 -0
- package/src/tools/dev/Map/components/CompassChip.tsx +67 -0
- package/src/tools/dev/Map/components/DraggableMarkers.tsx +79 -0
- package/src/tools/dev/Map/components/GeocoderControl.tsx +148 -63
- package/src/tools/dev/Map/components/GeocoderInput.tsx +181 -0
- package/src/tools/dev/Map/components/MapContainer/FullscreenDialog.tsx +140 -0
- package/src/tools/dev/Map/components/MapContainer/GeolocateHandle.tsx +70 -0
- package/src/tools/dev/Map/components/MapContainer/MapInner.tsx +326 -0
- package/src/tools/dev/Map/components/MapContainer/MapToolbar.tsx +286 -0
- package/src/tools/dev/Map/components/MapContainer/RotateHint.tsx +25 -0
- package/src/tools/dev/Map/components/MapContainer/ScrollLockOverlay.tsx +52 -0
- package/src/tools/dev/Map/components/MapContainer/constants.ts +15 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useAutoReset.ts +51 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useBasemapState.ts +48 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useExternalMapsUrl.ts +29 -0
- package/src/tools/dev/Map/components/MapContainer/index.tsx +29 -0
- package/src/tools/dev/Map/components/MapContainer/types.ts +198 -0
- package/src/tools/dev/Map/components/MapControls.tsx +27 -0
- package/src/tools/dev/Map/components/MapMarkers.tsx +94 -0
- package/src/tools/dev/Map/components/MarkerCard.tsx +102 -0
- package/src/tools/dev/Map/components/MeasureControl.tsx +219 -0
- package/src/tools/dev/Map/components/index.ts +35 -1
- package/src/tools/dev/Map/geocode/index.ts +2 -0
- package/src/tools/dev/Map/geocode/photon.ts +265 -0
- package/src/tools/dev/Map/hooks/index.ts +37 -0
- package/src/tools/dev/Map/hooks/useCompass.ts +44 -0
- package/src/tools/dev/Map/hooks/useDraggableMarkers.ts +112 -0
- package/src/tools/dev/Map/hooks/useGeocoder.ts +155 -0
- package/src/tools/dev/Map/hooks/useGeolocation.ts +174 -0
- package/src/tools/dev/Map/hooks/useMapControl.ts +5 -1
- package/src/tools/dev/Map/hooks/useMapImages.ts +112 -0
- package/src/tools/dev/Map/hooks/useMapScrollProtection.ts +155 -0
- package/src/tools/dev/Map/hooks/useMapTouchMode.ts +21 -0
- package/src/tools/dev/Map/hooks/useMarkerCard.ts +39 -0
- package/src/tools/dev/Map/hooks/useMarkers.ts +9 -3
- package/src/tools/dev/Map/hooks/useMeasure.ts +169 -0
- package/src/tools/dev/Map/hooks/useTerrain.ts +123 -0
- package/src/tools/dev/Map/index.ts +59 -2
- package/src/tools/dev/Map/layers/index.ts +1 -0
- package/src/tools/dev/Map/layers/symbol.ts +93 -0
- package/src/tools/dev/Map/lazy.tsx +120 -0
- package/src/tools/dev/Map/protocols/pmtiles.ts +73 -0
- package/src/tools/dev/Map/styles/index.ts +62 -0
- package/src/tools/dev/Map/styles/terrain.ts +29 -0
- package/src/tools/dev/Map/types.ts +31 -3
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +5 -12
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/shared/SendButton.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/shared/http-colors.ts +61 -0
- package/src/tools/dev/api/OpenapiViewer/components/shared/ui.tsx +4 -21
- package/src/tools/dev/code/MarkdownMessage/MarkdownMessage.tsx +150 -58
- package/src/tools/dev/code/MarkdownMessage/README.md +38 -0
- package/src/tools/dev/code/MarkdownMessage/builtinRules.tsx +92 -0
- package/src/tools/dev/code/MarkdownMessage/index.ts +3 -0
- package/src/tools/dev/code/MarkdownMessage/streamingBlocks.ts +164 -0
- package/src/tools/dev/code/MarkdownMessage/types.ts +20 -0
- package/src/tools/dev/code/PrettyCode/PrettyCode.client.tsx +27 -30
- package/src/tools/dev/code/PrettyCode/index.tsx +4 -1
- package/src/tools/dev/code/PrettyCode/lazy.tsx +3 -1
- package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +168 -3
- package/src/tools/forms/MarkdownEditor/README.md +84 -0
- package/src/tools/forms/MarkdownEditor/chip/ChipNode.ts +454 -0
- package/src/tools/forms/MarkdownEditor/chip/syncChips.ts +80 -0
- package/src/tools/forms/MarkdownEditor/filePath/FilePathChip.tsx +68 -0
- package/src/tools/forms/MarkdownEditor/filePath/__tests__/detect.test.ts +161 -0
- package/src/tools/forms/MarkdownEditor/filePath/detect.ts +289 -0
- package/src/tools/forms/MarkdownEditor/filePath/index.ts +15 -0
- package/src/tools/forms/MarkdownEditor/index.ts +19 -0
- package/src/tools/forms/MarkdownEditor/lazy.tsx +10 -0
- package/src/tools/forms/MarkdownEditor/styles.css +134 -2
- package/src/tools/forms/MarkdownEditor/url/__tests__/detect.test.ts +121 -0
- package/src/tools/forms/MarkdownEditor/url/detect.ts +227 -0
- package/src/tools/forms/NotionEditor/BookmarkNode.ts +158 -0
- package/src/tools/forms/NotionEditor/BookmarkView.tsx +45 -0
- package/src/tools/forms/NotionEditor/MapEditLayer.tsx +199 -0
- package/src/tools/forms/NotionEditor/MapNode.ts +218 -0
- package/src/tools/forms/NotionEditor/MapView.tsx +103 -0
- package/src/tools/forms/NotionEditor/NotionEditor.tsx +7 -2
- package/src/tools/forms/NotionEditor/README.md +109 -188
- package/src/tools/forms/NotionEditor/extensions.ts +15 -0
- package/src/tools/forms/NotionEditor/slashItems.ts +19 -0
- package/src/tools/forms/NotionEditor/styles.css +25 -0
- package/src/tools/forms/NotionEditor/types.ts +9 -0
- package/src/tools/input/SpeechRecognition/core/engine/external.ts +5 -0
- package/src/tools/input/SpeechRecognition/types.ts +10 -0
- package/src/tools/media/Gallery/components/lightbox/GalleryLightbox.tsx +39 -22
- package/src/tools/media/ImageViewer/README.md +9 -0
- package/src/tools/media/ImageViewer/components/ImageViewer.tsx +165 -79
- package/src/tools/media/ImageViewer/components/LightboxChrome.tsx +137 -0
- package/src/tools/media/ImageViewer/components/MiddleEllipsis.tsx +46 -0
- package/src/tools/media/ImageViewer/components/index.ts +2 -0
- package/src/tools/media/ImageViewer/hooks/index.ts +3 -0
- package/src/tools/media/ImageViewer/hooks/useIdleChrome.ts +73 -0
- package/src/tools/media/ImageViewer/types.ts +10 -0
- package/src/tools/media/ImageViewer/utils/constants.ts +28 -0
- package/src/tools/media/ImageViewer/utils/index.ts +4 -0
- package/src/tools/media/VideoPlayer/VideoPlayer.tsx +34 -40
- package/src/tools/media/VideoPlayer/canvas/youtube-canvas.tsx +13 -13
- package/src/tools/media/VideoPlayer/parts/center-play.tsx +26 -0
- package/src/tools/media/VideoPlayer/parts/index.ts +2 -0
- package/src/tools/media/VideoPlayer/styles/video-player.css +102 -53
- package/src/tools/visual/charts/SmoothLine/README.md +97 -0
- package/src/tools/visual/charts/SmoothLine/SmoothLine.tsx +177 -0
- package/src/tools/visual/charts/SmoothLine/index.ts +21 -0
- package/src/tools/visual/charts/SmoothLine/lazy.tsx +23 -0
- package/src/tools/visual/charts/SmoothLine/smoothPath.ts +209 -0
- package/src/tools/visual/charts/SmoothLine/types.ts +98 -0
- package/src/tools/visual/charts/SmoothLine/useSmoothSeries.ts +429 -0
- package/src/tools/visual/indicators/Fps/types.ts +1 -1
- package/src/tools/chat/messages/blocks/renderers/LottieBlock.tsx +0 -11
- 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
|
|
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).
|
|
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 {
|
|
10
|
+
import { LazyMapContainer, MapMarker } from '@djangocfg/ui-tools/map';
|
|
11
11
|
|
|
12
12
|
<div className="h-96 rounded-xl overflow-hidden">
|
|
13
|
-
<
|
|
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
|
-
</
|
|
22
|
+
</LazyMapContainer>
|
|
23
23
|
</div>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
The container provides its own `MapProvider`.
|
|
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
|
-
|
|
30
|
+
The package subpath resolves to the lazy entry. It surfaces:
|
|
31
31
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
+
**PMTiles** — `addPMTilesProtocol`, `pmtilesSourceUrl` (self-hosted / offline basemaps).
|
|
193
39
|
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
46
|
+
**Layer factories** — `createClusterLayers`, `createPointLayer`, `createHeatmapLayer`, `createPolygonLayer`, `createPolygonOutlineLayer`, `createHighlightLayer`, `createLineLayer`, `createRouteLayers`, `createDashedLineLayer`, `createAnimatedLineLayer`, `createSymbolLayer`.
|
|
363
47
|
|
|
364
|
-
|
|
48
|
+
**Geometry utils** — `calculateBounds`, `calculateCenter`, `calculateDistance`, `isPointInBounds`, `expandBounds`, `toGeoJSON`, `fromGeoJSON`, `createPoint`, `createPolygon`, `createLineString`, `createFeatureCollection`.
|
|
365
49
|
|
|
366
|
-
|
|
50
|
+
**Spiderfy utils** — `offsetOverlappingMarkers`, `getSpiderfyPositions`, `groupOverlappingMarkers`, `hasOverlappingMarkers`, `getOverlapStats`.
|
|
367
51
|
|
|
368
|
-
|
|
52
|
+
## Docs
|
|
369
53
|
|
|
370
|
-
|
|
|
54
|
+
| Doc | Covers |
|
|
371
55
|
|---|---|
|
|
372
|
-
| `
|
|
373
|
-
|
|
|
374
|
-
| `
|
|
375
|
-
| `
|
|
376
|
-
| `
|
|
377
|
-
|
|
|
378
|
-
|
|
|
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
|
|
383
|
-
-
|
|
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,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)
|