@djangocfg/ui-tools 2.1.430 → 2.1.432
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/dist/tree/index.cjs +43 -8
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +31 -1
- package/dist/tree/index.d.ts +31 -1
- package/dist/tree/index.mjs +43 -8
- package/dist/tree/index.mjs.map +1 -1
- 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 +109 -2
- package/src/tools/chat/composer/Composer.tsx +45 -12
- package/src/tools/chat/composer/ComposerRichTextarea.tsx +16 -4
- package/src/tools/chat/composer/PastedTextDialog.tsx +88 -0
- package/src/tools/chat/composer/index.ts +9 -0
- package/src/tools/chat/composer/textToAttachment.ts +69 -0
- package/src/tools/chat/composer/types.ts +13 -0
- package/src/tools/chat/composer/useComposerAttach.tsx +47 -0
- 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 +335 -46
- 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 +38 -3
- 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/data/Tree/README.md +30 -0
- package/src/tools/data/Tree/components/TreeRow.tsx +11 -2
- package/src/tools/data/Tree/context/menu/render.tsx +56 -27
- package/src/tools/data/Tree/data/appearance.ts +65 -3
- package/src/tools/data/Tree/data/index.ts +2 -0
- package/src/tools/data/Tree/index.tsx +2 -0
- 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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { memo } from 'react'
|
|
4
|
+
import { Pentagon, Ruler, Spline, Trash2 } from 'lucide-react'
|
|
5
|
+
import { cn } from '@djangocfg/ui-core/lib'
|
|
6
|
+
import {
|
|
7
|
+
Tooltip,
|
|
8
|
+
TooltipContent,
|
|
9
|
+
TooltipTrigger,
|
|
10
|
+
} from '@djangocfg/ui-core/components'
|
|
11
|
+
|
|
12
|
+
import { MapSource, MapLayer } from './MapSource'
|
|
13
|
+
import { useMeasure, type MeasureMode } from '../hooks/useMeasure'
|
|
14
|
+
import { createLineLayer } from '../layers/line'
|
|
15
|
+
import { createPolygonLayer, createPolygonOutlineLayer } from '../layers/polygon'
|
|
16
|
+
|
|
17
|
+
export interface MeasureControlProps {
|
|
18
|
+
/** Corner the control toolbar is pinned to. @default 'top-left' */
|
|
19
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
|
|
20
|
+
/** Mode the tool starts in. @default 'distance' */
|
|
21
|
+
defaultMode?: MeasureMode
|
|
22
|
+
/** Accent color for the measure overlay (line stroke / polygon fill). */
|
|
23
|
+
accentColor?: string
|
|
24
|
+
className?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const positionStyles: Record<
|
|
28
|
+
NonNullable<MeasureControlProps['position']>,
|
|
29
|
+
React.CSSProperties
|
|
30
|
+
> = {
|
|
31
|
+
'top-left': { top: 12, left: 12 },
|
|
32
|
+
'top-right': { top: 12, right: 12 },
|
|
33
|
+
'bottom-left': { bottom: 12, left: 12 },
|
|
34
|
+
'bottom-right': { bottom: 12, right: 12 },
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Same floating-chrome chip style MapContainer uses for its map controls
|
|
38
|
+
// (semantic `popover` tokens — reads like maplibre's own controls in light
|
|
39
|
+
// and dark themes). Kept in sync with MAP_CONTROL_CLASS there.
|
|
40
|
+
const MAP_CONTROL_CLASS = cn(
|
|
41
|
+
'inline-flex items-center justify-center rounded-lg',
|
|
42
|
+
'bg-popover/95 text-popover-foreground backdrop-blur-sm shadow-sm border border-border',
|
|
43
|
+
'hover:bg-popover transition-colors',
|
|
44
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const SOURCE_ID = 'measure-source'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* On-map measure control — click to measure a path's length (km) or a
|
|
51
|
+
* polygon's area (m²/km²), with a live total readout and a clear button.
|
|
52
|
+
*
|
|
53
|
+
* Drop it as a child of `MapContainer` / `MapView` (inside the
|
|
54
|
+
* `MapProvider`), alongside `MapLegend` / `LayerSwitcher`:
|
|
55
|
+
*
|
|
56
|
+
* ```tsx
|
|
57
|
+
* <MapContainer initialViewport={...}>
|
|
58
|
+
* <MeasureControl />
|
|
59
|
+
* </MapContainer>
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* Renders the measure geometry through the existing `createLineLayer` /
|
|
63
|
+
* `createPolygonLayer` factories — no bespoke layer code.
|
|
64
|
+
*/
|
|
65
|
+
function MeasureControlComponent({
|
|
66
|
+
position = 'top-left',
|
|
67
|
+
defaultMode = 'distance',
|
|
68
|
+
accentColor = '#f97316',
|
|
69
|
+
className = '',
|
|
70
|
+
}: MeasureControlProps) {
|
|
71
|
+
const { mode, setMode, geojson, totalLabel, points, clear, active, setActive } =
|
|
72
|
+
useMeasure(defaultMode)
|
|
73
|
+
|
|
74
|
+
const hasMeasurement = points.length > 0
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<>
|
|
78
|
+
{/* Overlay geometry — reuses the existing line / polygon factories. */}
|
|
79
|
+
<MapSource id={SOURCE_ID} data={geojson}>
|
|
80
|
+
{mode === 'area' ? (
|
|
81
|
+
<>
|
|
82
|
+
<MapLayer
|
|
83
|
+
{...createPolygonLayer(`${SOURCE_ID}-fill`, SOURCE_ID, {
|
|
84
|
+
fillColor: accentColor,
|
|
85
|
+
fillOpacity: 0.2,
|
|
86
|
+
})}
|
|
87
|
+
/>
|
|
88
|
+
<MapLayer
|
|
89
|
+
{...createPolygonOutlineLayer(`${SOURCE_ID}-outline`, SOURCE_ID, {
|
|
90
|
+
color: accentColor,
|
|
91
|
+
width: 2,
|
|
92
|
+
})}
|
|
93
|
+
/>
|
|
94
|
+
</>
|
|
95
|
+
) : (
|
|
96
|
+
<MapLayer
|
|
97
|
+
{...createLineLayer({
|
|
98
|
+
id: `${SOURCE_ID}-line`,
|
|
99
|
+
sourceId: SOURCE_ID,
|
|
100
|
+
color: accentColor,
|
|
101
|
+
width: 3,
|
|
102
|
+
})}
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
105
|
+
</MapSource>
|
|
106
|
+
|
|
107
|
+
{/* Toolbar — a single popover chip, same chrome as the map controls. */}
|
|
108
|
+
<div
|
|
109
|
+
className={cn(
|
|
110
|
+
'absolute z-20 flex items-center gap-1 p-1',
|
|
111
|
+
MAP_CONTROL_CLASS,
|
|
112
|
+
'rounded-lg',
|
|
113
|
+
className,
|
|
114
|
+
)}
|
|
115
|
+
style={positionStyles[position]}
|
|
116
|
+
role="group"
|
|
117
|
+
aria-label="Measure tool"
|
|
118
|
+
>
|
|
119
|
+
<Tooltip>
|
|
120
|
+
<TooltipTrigger asChild>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
onClick={() => setActive(!active)}
|
|
124
|
+
aria-pressed={active}
|
|
125
|
+
aria-label={active ? 'Stop measuring' : 'Start measuring'}
|
|
126
|
+
className={cn(
|
|
127
|
+
'inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors',
|
|
128
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
129
|
+
active
|
|
130
|
+
? 'bg-primary text-primary-foreground'
|
|
131
|
+
: 'hover:bg-accent hover:text-accent-foreground',
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
<Ruler className="h-4 w-4" aria-hidden="true" />
|
|
135
|
+
</button>
|
|
136
|
+
</TooltipTrigger>
|
|
137
|
+
<TooltipContent side="bottom">
|
|
138
|
+
{active ? 'Stop measuring' : 'Measure'}
|
|
139
|
+
</TooltipContent>
|
|
140
|
+
</Tooltip>
|
|
141
|
+
|
|
142
|
+
{/* Mode switch — distance / area. */}
|
|
143
|
+
<Tooltip>
|
|
144
|
+
<TooltipTrigger asChild>
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onClick={() => setMode('distance')}
|
|
148
|
+
aria-pressed={mode === 'distance'}
|
|
149
|
+
aria-label="Measure distance"
|
|
150
|
+
className={cn(
|
|
151
|
+
'inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors',
|
|
152
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
153
|
+
mode === 'distance'
|
|
154
|
+
? 'bg-accent text-accent-foreground'
|
|
155
|
+
: 'hover:bg-accent hover:text-accent-foreground',
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
<Spline className="h-4 w-4" aria-hidden="true" />
|
|
159
|
+
</button>
|
|
160
|
+
</TooltipTrigger>
|
|
161
|
+
<TooltipContent side="bottom">Distance</TooltipContent>
|
|
162
|
+
</Tooltip>
|
|
163
|
+
|
|
164
|
+
<Tooltip>
|
|
165
|
+
<TooltipTrigger asChild>
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
onClick={() => setMode('area')}
|
|
169
|
+
aria-pressed={mode === 'area'}
|
|
170
|
+
aria-label="Measure area"
|
|
171
|
+
className={cn(
|
|
172
|
+
'inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors',
|
|
173
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
174
|
+
mode === 'area'
|
|
175
|
+
? 'bg-accent text-accent-foreground'
|
|
176
|
+
: 'hover:bg-accent hover:text-accent-foreground',
|
|
177
|
+
)}
|
|
178
|
+
>
|
|
179
|
+
<Pentagon className="h-4 w-4" aria-hidden="true" />
|
|
180
|
+
</button>
|
|
181
|
+
</TooltipTrigger>
|
|
182
|
+
<TooltipContent side="bottom">Area</TooltipContent>
|
|
183
|
+
</Tooltip>
|
|
184
|
+
|
|
185
|
+
{/* Live total readout. */}
|
|
186
|
+
<span
|
|
187
|
+
className="px-2 text-xs font-medium tabular-nums"
|
|
188
|
+
aria-live="polite"
|
|
189
|
+
aria-label={`Measured ${mode}: ${totalLabel}`}
|
|
190
|
+
>
|
|
191
|
+
{totalLabel}
|
|
192
|
+
</span>
|
|
193
|
+
|
|
194
|
+
{/* Clear. */}
|
|
195
|
+
<Tooltip>
|
|
196
|
+
<TooltipTrigger asChild>
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
199
|
+
onClick={clear}
|
|
200
|
+
disabled={!hasMeasurement}
|
|
201
|
+
aria-label="Clear measurement"
|
|
202
|
+
className={cn(
|
|
203
|
+
'inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors',
|
|
204
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
205
|
+
'hover:bg-accent hover:text-accent-foreground',
|
|
206
|
+
'disabled:pointer-events-none disabled:opacity-40',
|
|
207
|
+
)}
|
|
208
|
+
>
|
|
209
|
+
<Trash2 className="h-4 w-4" aria-hidden="true" />
|
|
210
|
+
</button>
|
|
211
|
+
</TooltipTrigger>
|
|
212
|
+
<TooltipContent side="bottom">Clear</TooltipContent>
|
|
213
|
+
</Tooltip>
|
|
214
|
+
</div>
|
|
215
|
+
</>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const MeasureControl = memo(MeasureControlComponent)
|
|
@@ -7,14 +7,48 @@ export { MapSource, MapLayer } from './MapSource'
|
|
|
7
7
|
export { CustomOverlay } from './CustomOverlay'
|
|
8
8
|
export { MapLegend } from './MapLegend'
|
|
9
9
|
export { LayerSwitcher } from './LayerSwitcher'
|
|
10
|
+
export { MeasureControl } from './MeasureControl'
|
|
10
11
|
|
|
11
12
|
// DrawControl and GeocoderControl require optional dependencies
|
|
12
13
|
// See DrawControl.tsx and GeocoderControl.tsx for implementation examples
|
|
13
14
|
|
|
14
15
|
// Types are exported from ../types.ts
|
|
15
|
-
export type { MapContainerProps } from './MapContainer'
|
|
16
|
+
export type { MapContainerProps, GeolocateOptions, GeolocatePosition } from './MapContainer'
|
|
16
17
|
export type { MapControlsProps } from './MapControls'
|
|
17
18
|
export type { MapMarkerProps } from './MapMarker'
|
|
18
19
|
export type { MapPopupProps } from './MapPopup'
|
|
19
20
|
export type { MapClusterProps } from './MapCluster'
|
|
20
21
|
export type { MapSourceProps, MapLayerProps } from './MapSource'
|
|
22
|
+
export type { MeasureControlProps } from './MeasureControl'
|
|
23
|
+
|
|
24
|
+
export { DraggableMarkers } from './DraggableMarkers'
|
|
25
|
+
export type { DraggableMarkersProps, DraggablePoint } from './DraggableMarkers'
|
|
26
|
+
|
|
27
|
+
// Geocoder — free, key-free Photon autocomplete (presentational input +
|
|
28
|
+
// thin map-bound control). Injectable `resolve`; no optional deps.
|
|
29
|
+
export { GeocoderInput } from './GeocoderInput'
|
|
30
|
+
export { GeocoderControl } from './GeocoderControl'
|
|
31
|
+
export type { GeocoderInputProps } from './GeocoderInput'
|
|
32
|
+
export type { GeocoderControlProps } from './GeocoderControl'
|
|
33
|
+
|
|
34
|
+
// Compass chip — presentational reset-to-north control with a Google-style
|
|
35
|
+
// color cue (accent needle when the view is rotated/tilted). Use inside a
|
|
36
|
+
// MapProvider, fed by `useCompass`.
|
|
37
|
+
export { CompassChip } from './CompassChip'
|
|
38
|
+
export type { CompassChipProps } from './CompassChip'
|
|
39
|
+
|
|
40
|
+
// Marker info-cards — a Google-Maps-style card popup above a pin. `MarkerCard`
|
|
41
|
+
// is the default presentational card (data-driven, standalone); `MapMarkers`
|
|
42
|
+
// wires click → card-above-pin (one open at a time). Both inside a MapProvider.
|
|
43
|
+
export { MarkerCard } from './MarkerCard'
|
|
44
|
+
export type { MarkerCardProps } from './MarkerCard'
|
|
45
|
+
export { MapMarkers } from './MapMarkers'
|
|
46
|
+
export type { MapMarkersProps, MarkerWithCard } from './MapMarkers'
|
|
47
|
+
// Card data types. NOTE: the `MarkerCard` *type* (card data shape) is exported
|
|
48
|
+
// from the top-level Map barrel / `cards` — here the `MarkerCard` name is the
|
|
49
|
+
// component value, so we only surface the non-colliding card types.
|
|
50
|
+
export type {
|
|
51
|
+
MarkerCardAction,
|
|
52
|
+
MarkerCardData,
|
|
53
|
+
MarkerCardActionData,
|
|
54
|
+
} from '../cards/types'
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Photon geocoder resolver — the data contract + default `fetch` resolver
|
|
3
|
+
* for the address/place autocomplete.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the link-preview seam (`ResolveLinkPreview`): the UI is purely
|
|
6
|
+
* presentational and resolution is an INJECTABLE async function. The default
|
|
7
|
+
* `photonResolve` hits the public, key-free Photon (Komoot) endpoint — good
|
|
8
|
+
* for dev/internal use. A host can supply its own `resolve` (a Wails/Go
|
|
9
|
+
* method or a Next.js API route proxying Photon / a self-hosted instance) for
|
|
10
|
+
* production, dodging CORS + fair-use throttling.
|
|
11
|
+
*
|
|
12
|
+
* Pure — no React, no map dependency. Safe on the light (maplibre-free)
|
|
13
|
+
* surface.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Public Photon search endpoint (OSM data, no API key, fair-use only). */
|
|
17
|
+
const PHOTON_ENDPOINT = 'https://photon.komoot.io/api/';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One normalized geocoding hit. Plain JSON so it survives transport /
|
|
21
|
+
* persistence. `raw` carries the original GeoJSON feature for hosts that
|
|
22
|
+
* want more than the friendly fields.
|
|
23
|
+
*/
|
|
24
|
+
export interface GeocodeResult {
|
|
25
|
+
/** `osm_type+osm_id`, else a synthesized stable key. */
|
|
26
|
+
id: string;
|
|
27
|
+
/** Primary line — place `name`, else `street housenumber`. */
|
|
28
|
+
label: string;
|
|
29
|
+
/** Secondary line — `"city, state, country"` (filtered, joined). */
|
|
30
|
+
secondary?: string;
|
|
31
|
+
/** Longitude (GeoJSON x). */
|
|
32
|
+
lng: number;
|
|
33
|
+
/** Latitude (GeoJSON y). */
|
|
34
|
+
lat: number;
|
|
35
|
+
/** `[minLng, minLat, maxLng, maxLat]` when Photon returns an extent. */
|
|
36
|
+
bbox?: [number, number, number, number];
|
|
37
|
+
/**
|
|
38
|
+
* Coarse location kind, normalized across providers. Drives a sensible
|
|
39
|
+
* fallback zoom when there's no `bbox` to fit. `'unknown'` when the
|
|
40
|
+
* provider didn't say.
|
|
41
|
+
*/
|
|
42
|
+
kind: GeocodeKind;
|
|
43
|
+
/** The original Photon GeoJSON feature. */
|
|
44
|
+
raw?: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Provider-agnostic location granularity (coarse → fine). */
|
|
48
|
+
export type GeocodeKind =
|
|
49
|
+
| 'country'
|
|
50
|
+
| 'region' // state / province / county
|
|
51
|
+
| 'city' // city / town / village / locality
|
|
52
|
+
| 'district' // suburb / neighbourhood / borough
|
|
53
|
+
| 'postcode'
|
|
54
|
+
| 'street'
|
|
55
|
+
| 'house' // house / building / address point
|
|
56
|
+
| 'poi' // a named point of interest
|
|
57
|
+
| 'unknown';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Fallback zoom per kind — used ONLY when a result has no `bbox` to fit
|
|
61
|
+
* (point results like a house or POI). Bigger area → smaller zoom. These are
|
|
62
|
+
* sensible defaults, not a hardcode of any specific place; the bbox path
|
|
63
|
+
* (below) handles extents universally.
|
|
64
|
+
*/
|
|
65
|
+
const KIND_ZOOM: Record<GeocodeKind, number> = {
|
|
66
|
+
country: 4,
|
|
67
|
+
region: 7,
|
|
68
|
+
city: 11,
|
|
69
|
+
district: 13,
|
|
70
|
+
postcode: 13,
|
|
71
|
+
street: 15,
|
|
72
|
+
house: 17,
|
|
73
|
+
poi: 16,
|
|
74
|
+
unknown: 14,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The camera move for a chosen result — the SMART-ZOOM core.
|
|
79
|
+
*
|
|
80
|
+
* Prefer the location's own extent: if `bbox` is present, return a
|
|
81
|
+
* `fitBounds` move so the whole feature frames itself (an island fills the
|
|
82
|
+
* view, a street reads as a street) with no per-place tuning. Otherwise fall
|
|
83
|
+
* back to a `flyTo` at a zoom chosen by the result's `kind`.
|
|
84
|
+
*/
|
|
85
|
+
export function cameraForResult(
|
|
86
|
+
result: GeocodeResult,
|
|
87
|
+
):
|
|
88
|
+
| { type: 'fit'; bbox: [number, number, number, number] }
|
|
89
|
+
| { type: 'fly'; center: [number, number]; zoom: number } {
|
|
90
|
+
if (result.bbox) {
|
|
91
|
+
// Guard against a degenerate (zero-area) bbox — a point disguised as an
|
|
92
|
+
// extent — which would over-zoom; fall through to kind zoom then.
|
|
93
|
+
const [minLng, minLat, maxLng, maxLat] = result.bbox;
|
|
94
|
+
if (maxLng - minLng > 1e-6 && maxLat - minLat > 1e-6) {
|
|
95
|
+
return { type: 'fit', bbox: result.bbox };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
type: 'fly',
|
|
100
|
+
center: [result.lng, result.lat],
|
|
101
|
+
zoom: KIND_ZOOM[result.kind] ?? KIND_ZOOM.unknown,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Injectable resolver. Given a query string, returns ranked results. Honors
|
|
107
|
+
* an optional `near` geo-bias (e.g. the current map center), a `limit`, and
|
|
108
|
+
* an `AbortSignal` (rethrows `AbortError` when cancelled; returns `[]` for an
|
|
109
|
+
* empty/blank query).
|
|
110
|
+
*/
|
|
111
|
+
export type ResolveGeocode = (
|
|
112
|
+
query: string,
|
|
113
|
+
opts?: {
|
|
114
|
+
near?: { lng: number; lat: number };
|
|
115
|
+
limit?: number;
|
|
116
|
+
signal?: AbortSignal;
|
|
117
|
+
},
|
|
118
|
+
) => Promise<GeocodeResult[]>;
|
|
119
|
+
|
|
120
|
+
/** Photon feature `properties` (the subset we read). */
|
|
121
|
+
interface PhotonProperties {
|
|
122
|
+
osm_type?: string;
|
|
123
|
+
osm_id?: number | string;
|
|
124
|
+
name?: string;
|
|
125
|
+
street?: string;
|
|
126
|
+
housenumber?: string;
|
|
127
|
+
city?: string;
|
|
128
|
+
state?: string;
|
|
129
|
+
country?: string;
|
|
130
|
+
postcode?: string;
|
|
131
|
+
/** Photon's coarse type, e.g. `city` / `street` / `house` / `country`. */
|
|
132
|
+
type?: string;
|
|
133
|
+
/** OSM tag key/value, e.g. `place`/`city`, `highway`/`residential`. */
|
|
134
|
+
osm_key?: string;
|
|
135
|
+
osm_value?: string;
|
|
136
|
+
/** `[minLng, maxLat, maxLng, minLat]` per Photon's extent ordering. */
|
|
137
|
+
extent?: [number, number, number, number];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface PhotonFeature {
|
|
141
|
+
geometry?: { coordinates?: [number, number] };
|
|
142
|
+
properties?: PhotonProperties;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface PhotonResponse {
|
|
146
|
+
features?: PhotonFeature[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Build a `[minLng, minLat, maxLng, maxLat]` bbox from Photon's extent. */
|
|
150
|
+
function normalizeExtent(
|
|
151
|
+
extent: PhotonProperties['extent'],
|
|
152
|
+
): GeocodeResult['bbox'] {
|
|
153
|
+
if (!extent || extent.length !== 4) return undefined;
|
|
154
|
+
const [minLng, maxLat, maxLng, minLat] = extent;
|
|
155
|
+
return [minLng, minLat, maxLng, maxLat];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Map Photon's `type` (plus OSM `osm_key`/`osm_value` as a fallback) onto our
|
|
160
|
+
* coarse, provider-agnostic `GeocodeKind`. Pure lookup — no magic numbers.
|
|
161
|
+
*/
|
|
162
|
+
function normalizeKind(p: PhotonProperties): GeocodeKind {
|
|
163
|
+
switch ((p.type ?? '').toLowerCase()) {
|
|
164
|
+
case 'country':
|
|
165
|
+
return 'country';
|
|
166
|
+
case 'state':
|
|
167
|
+
case 'region':
|
|
168
|
+
case 'province':
|
|
169
|
+
case 'county':
|
|
170
|
+
return 'region';
|
|
171
|
+
case 'city':
|
|
172
|
+
case 'town':
|
|
173
|
+
case 'village':
|
|
174
|
+
case 'locality':
|
|
175
|
+
return 'city';
|
|
176
|
+
case 'district':
|
|
177
|
+
case 'suburb':
|
|
178
|
+
case 'neighbourhood':
|
|
179
|
+
return 'district';
|
|
180
|
+
case 'postcode':
|
|
181
|
+
return 'postcode';
|
|
182
|
+
case 'street':
|
|
183
|
+
return 'street';
|
|
184
|
+
case 'house':
|
|
185
|
+
case 'building':
|
|
186
|
+
return 'house';
|
|
187
|
+
}
|
|
188
|
+
// Fallbacks from the OSM tag when `type` is missing/unknown.
|
|
189
|
+
const key = (p.osm_key ?? '').toLowerCase();
|
|
190
|
+
if (key === 'highway') return 'street';
|
|
191
|
+
if (key === 'place') return 'city';
|
|
192
|
+
if (key === 'boundary') return 'region';
|
|
193
|
+
if (key === 'building') return 'house';
|
|
194
|
+
if (key === 'amenity' || key === 'shop' || key === 'tourism' || key === 'leisure') {
|
|
195
|
+
return 'poi';
|
|
196
|
+
}
|
|
197
|
+
return 'unknown';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Normalize one Photon GeoJSON feature → `GeocodeResult` (or `null`). */
|
|
201
|
+
function toResult(feature: PhotonFeature, index: number): GeocodeResult | null {
|
|
202
|
+
const coords = feature.geometry?.coordinates;
|
|
203
|
+
if (!coords || coords.length !== 2) return null;
|
|
204
|
+
const [lng, lat] = coords;
|
|
205
|
+
if (typeof lng !== 'number' || typeof lat !== 'number') return null;
|
|
206
|
+
|
|
207
|
+
const p = feature.properties ?? {};
|
|
208
|
+
|
|
209
|
+
const label =
|
|
210
|
+
p.name ||
|
|
211
|
+
[p.street, p.housenumber].filter(Boolean).join(' ') ||
|
|
212
|
+
[p.city, p.state, p.country].filter(Boolean).join(', ') ||
|
|
213
|
+
'Unknown location';
|
|
214
|
+
|
|
215
|
+
const secondary = [p.city, p.state, p.country].filter(Boolean).join(', ');
|
|
216
|
+
|
|
217
|
+
const id =
|
|
218
|
+
p.osm_type && p.osm_id != null
|
|
219
|
+
? `${p.osm_type}${p.osm_id}`
|
|
220
|
+
: `${lng.toFixed(6)},${lat.toFixed(6)}#${index}`;
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
id,
|
|
224
|
+
label,
|
|
225
|
+
secondary: secondary || undefined,
|
|
226
|
+
lng,
|
|
227
|
+
lat,
|
|
228
|
+
bbox: normalizeExtent(p.extent),
|
|
229
|
+
kind: normalizeKind(p),
|
|
230
|
+
raw: feature,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Default resolver: a built-in `fetch` to the public, key-free Photon
|
|
236
|
+
* endpoint. Honors `near` (geo-bias), `limit`, and `signal`. Returns `[]`
|
|
237
|
+
* for a blank query; rethrows `AbortError` when the request is cancelled.
|
|
238
|
+
*/
|
|
239
|
+
export const photonResolve: ResolveGeocode = async (query, opts) => {
|
|
240
|
+
const q = query.trim();
|
|
241
|
+
if (!q) return [];
|
|
242
|
+
|
|
243
|
+
const limit = opts?.limit ?? 5;
|
|
244
|
+
const params = new URLSearchParams({ q, limit: String(limit) });
|
|
245
|
+
if (opts?.near) {
|
|
246
|
+
params.set('lat', String(opts.near.lat));
|
|
247
|
+
params.set('lon', String(opts.near.lng));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const response = await fetch(`${PHOTON_ENDPOINT}?${params.toString()}`, {
|
|
251
|
+
signal: opts?.signal,
|
|
252
|
+
headers: { Accept: 'application/json' },
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
throw new Error(`Photon request failed: ${response.status}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const json = (await response.json()) as PhotonResponse;
|
|
260
|
+
const features = json.features ?? [];
|
|
261
|
+
|
|
262
|
+
return features
|
|
263
|
+
.map((feature, index) => toResult(feature, index))
|
|
264
|
+
.filter((r): r is GeocodeResult => r !== null);
|
|
265
|
+
};
|
|
@@ -5,6 +5,43 @@ export { useMapEvents } from './useMapEvents'
|
|
|
5
5
|
export { useMapViewport } from './useMapViewport'
|
|
6
6
|
export { useMapLayers } from './useMapLayers'
|
|
7
7
|
export { useSpiderfy, type UseSpiderfyOptions, type UseSpiderfyResult } from './useSpiderfy'
|
|
8
|
+
export {
|
|
9
|
+
useMapScrollProtection,
|
|
10
|
+
type UseMapScrollProtectionResult,
|
|
11
|
+
type MapScrollProtectionMode,
|
|
12
|
+
} from './useMapScrollProtection'
|
|
13
|
+
export { useMapTouchMode } from './useMapTouchMode'
|
|
14
|
+
export {
|
|
15
|
+
useMapImages,
|
|
16
|
+
type MapImage,
|
|
17
|
+
type UseMapImagesResult,
|
|
18
|
+
} from './useMapImages'
|
|
19
|
+
export { useTerrain, type UseTerrainOptions } from './useTerrain'
|
|
20
|
+
export { useCompass, type UseCompassResult } from './useCompass'
|
|
21
|
+
export { useMeasure, type UseMeasureResult, type MeasureMode } from './useMeasure'
|
|
22
|
+
export {
|
|
23
|
+
useGeolocation,
|
|
24
|
+
type GeolocationFix,
|
|
25
|
+
type GeolocationStatus,
|
|
26
|
+
type UseGeolocationOptions,
|
|
27
|
+
type UseGeolocationResult,
|
|
28
|
+
} from './useGeolocation'
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
useDraggableMarkers,
|
|
32
|
+
type DraggablePoint,
|
|
33
|
+
type UseDraggableMarkersOptions,
|
|
34
|
+
type UseDraggableMarkersResult,
|
|
35
|
+
type DraggableMarkerHandlers,
|
|
36
|
+
} from './useDraggableMarkers'
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
useGeocoder,
|
|
40
|
+
type UseGeocoderOptions,
|
|
41
|
+
type UseGeocoderResult,
|
|
42
|
+
} from './useGeocoder'
|
|
43
|
+
|
|
44
|
+
export { useMarkerCard, type UseMarkerCardResult } from './useMarkerCard'
|
|
8
45
|
|
|
9
46
|
// Re-export useControl from react-map-gl for custom control creation
|
|
10
47
|
export { useControl } from 'react-map-gl/maplibre'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback } from 'react'
|
|
4
|
+
|
|
5
|
+
import { useMapContext } from '../context'
|
|
6
|
+
|
|
7
|
+
/** What `useCompass` exposes for a compass chip / camera-orientation UI. */
|
|
8
|
+
export interface UseCompassResult {
|
|
9
|
+
/** Current camera bearing in degrees (0 = north-up). */
|
|
10
|
+
bearing: number
|
|
11
|
+
/** Current camera pitch in degrees (0 = top-down). */
|
|
12
|
+
pitch: number
|
|
13
|
+
/**
|
|
14
|
+
* Whether the view is rotated or tilted off true north / top-down. Google-
|
|
15
|
+
* style compasses use this to decide whether to show the chip at all and to
|
|
16
|
+
* tint the needle as an "off-north" cue. Threshold is a half-degree so
|
|
17
|
+
* floating-point jitter around 0 doesn't flicker it.
|
|
18
|
+
*/
|
|
19
|
+
isOriented: boolean
|
|
20
|
+
/** Ease the camera back to a north-up, top-down view (bearing + pitch → 0). */
|
|
21
|
+
resetNorth: () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compass state for a map — reads the live `bearing`/`pitch` from the viewport
|
|
26
|
+
* and exposes a one-call reset to north-up + top-down.
|
|
27
|
+
*
|
|
28
|
+
* Pure glue around `useMapContext` (no maplibre runtime import); reusable by
|
|
29
|
+
* any chip inside a `MapProvider`. Mirrors `useTerrain`'s map-access pattern:
|
|
30
|
+
* the camera move goes through the map ref's `easeTo`.
|
|
31
|
+
*/
|
|
32
|
+
export function useCompass(): UseCompassResult {
|
|
33
|
+
const { mapRef, viewport } = useMapContext()
|
|
34
|
+
|
|
35
|
+
const bearing = viewport.bearing ?? 0
|
|
36
|
+
const pitch = viewport.pitch ?? 0
|
|
37
|
+
const isOriented = Math.abs(bearing) > 0.5 || pitch > 0.5
|
|
38
|
+
|
|
39
|
+
const resetNorth = useCallback(() => {
|
|
40
|
+
mapRef.current?.getMap().easeTo({ bearing: 0, pitch: 0, duration: 500 })
|
|
41
|
+
}, [mapRef])
|
|
42
|
+
|
|
43
|
+
return { bearing, pitch, isOriented, resetNorth }
|
|
44
|
+
}
|