@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,326 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState, type ComponentType } from 'react'
|
|
4
|
+
import Map, {
|
|
5
|
+
type ViewStateChangeEvent,
|
|
6
|
+
type GeolocateControlInstance,
|
|
7
|
+
} from 'react-map-gl/maplibre'
|
|
8
|
+
import 'maplibre-gl/dist/maplibre-gl.css'
|
|
9
|
+
|
|
10
|
+
import { useMapContext } from '../../context'
|
|
11
|
+
import { useCompass, useMapScrollProtection, useMapTouchMode, useTerrain } from '../../hooks'
|
|
12
|
+
import { MapToolbar } from './MapToolbar'
|
|
13
|
+
import { ScrollLockOverlay } from './ScrollLockOverlay'
|
|
14
|
+
import { RotateHint } from './RotateHint'
|
|
15
|
+
import { FullscreenDialog } from './FullscreenDialog'
|
|
16
|
+
import { GeolocateHandle, type GeolocateState } from './GeolocateHandle'
|
|
17
|
+
import { GeocoderControl } from '../GeocoderControl'
|
|
18
|
+
import { useBasemapState } from './hooks/useBasemapState'
|
|
19
|
+
import { useExternalMapsUrl } from './hooks/useExternalMapsUrl'
|
|
20
|
+
import { useAutoReset } from './hooks/useAutoReset'
|
|
21
|
+
import type { MapContainerProps, MapInnerProps } from './types'
|
|
22
|
+
|
|
23
|
+
export function MapInner({
|
|
24
|
+
children,
|
|
25
|
+
mapStyle = 'light',
|
|
26
|
+
interactiveLayerIds,
|
|
27
|
+
style,
|
|
28
|
+
cursor,
|
|
29
|
+
attributionControl = false,
|
|
30
|
+
// `reuseMaps` pools maplibre instances across mounts. It saves init cost
|
|
31
|
+
// but in practice (Storybook HMR, switching stories, chat lists mounting/
|
|
32
|
+
// unmounting map blocks) a reused instance comes back with no style —
|
|
33
|
+
// a blank white map that never requests its style.json. Default OFF for
|
|
34
|
+
// correctness; opt back in for a single long-lived full-page map.
|
|
35
|
+
reuseMaps = false,
|
|
36
|
+
openInMapsUrl,
|
|
37
|
+
openInMapsLabel = 'Open in Maps',
|
|
38
|
+
googleMapsButton = true,
|
|
39
|
+
autoResetDelay = 0,
|
|
40
|
+
showResetButton = false,
|
|
41
|
+
scrollProtection = true,
|
|
42
|
+
scrollProtectionHint = 'Click to interact',
|
|
43
|
+
fullscreenButton = true,
|
|
44
|
+
fullscreenLabel = 'Expand map',
|
|
45
|
+
fullscreenTitle = 'Map',
|
|
46
|
+
basemapSwitcher = true,
|
|
47
|
+
basemaps,
|
|
48
|
+
basemapLabel = 'Map style',
|
|
49
|
+
onBasemapChange,
|
|
50
|
+
terrain = false,
|
|
51
|
+
terrainButton = true,
|
|
52
|
+
terrainLabel = '3D terrain',
|
|
53
|
+
onTerrainChange,
|
|
54
|
+
compassButton = true,
|
|
55
|
+
compassLabel = 'Reset bearing to north',
|
|
56
|
+
geolocate = false,
|
|
57
|
+
geolocateLabel = 'Show your location',
|
|
58
|
+
geolocateOptions,
|
|
59
|
+
onGeolocate,
|
|
60
|
+
onGeolocateError,
|
|
61
|
+
geocoder = false,
|
|
62
|
+
geocoderResolve,
|
|
63
|
+
geocoderPlaceholder,
|
|
64
|
+
onGeocoderResult,
|
|
65
|
+
onClose,
|
|
66
|
+
ariaLabel = 'Interactive map',
|
|
67
|
+
MapContainerComponent,
|
|
68
|
+
}: MapInnerProps & { MapContainerComponent: ComponentType<MapContainerProps> }) {
|
|
69
|
+
const { mapRef, viewport, setViewport, setIsLoaded, resetToInitial, initialViewport } = useMapContext()
|
|
70
|
+
|
|
71
|
+
// Active basemap — seeded from `mapStyle`, then driven by the switcher.
|
|
72
|
+
const { activeKey, effectiveStyle, resolvedStyle, basemapOptions, handleBasemapPick } =
|
|
73
|
+
useBasemapState({ mapStyle, basemaps, onBasemapChange })
|
|
74
|
+
|
|
75
|
+
// 3D terrain — seeded from the `terrain` prop, then driven by the chip.
|
|
76
|
+
const [terrainOn, setTerrainOn] = useState(terrain)
|
|
77
|
+
useEffect(() => setTerrainOn(terrain), [terrain])
|
|
78
|
+
useTerrain({ enabled: terrainOn })
|
|
79
|
+
|
|
80
|
+
// Brief gesture hint when 3D turns on — rotation/tilt isn't discoverable
|
|
81
|
+
// (it's right-drag / Ctrl+drag), so surface it, then auto-hide.
|
|
82
|
+
const [showRotateHint, setShowRotateHint] = useState(false)
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!terrainOn) {
|
|
85
|
+
setShowRotateHint(false)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
setShowRotateHint(true)
|
|
89
|
+
const t = setTimeout(() => setShowRotateHint(false), 4500)
|
|
90
|
+
return () => clearTimeout(t)
|
|
91
|
+
}, [terrainOn])
|
|
92
|
+
const handleTerrainToggle = useCallback(() => {
|
|
93
|
+
setTerrainOn((prev) => {
|
|
94
|
+
const next = !prev
|
|
95
|
+
onTerrainChange?.(next)
|
|
96
|
+
return next
|
|
97
|
+
})
|
|
98
|
+
}, [onTerrainChange])
|
|
99
|
+
|
|
100
|
+
// Compass — reset the camera to a north-up, top-down view. Google-style:
|
|
101
|
+
// the chip only shows when the view is rotated or tilted, and its needle
|
|
102
|
+
// rotates to reflect the current bearing. One click eases back to north.
|
|
103
|
+
// Math/reset live in `useCompass`; the toolbar gates the chip on it.
|
|
104
|
+
const { bearing, isOriented, resetNorth } = useCompass()
|
|
105
|
+
|
|
106
|
+
// Locate-me (GPS). Drives maplibre's built-in GeolocateControl via its
|
|
107
|
+
// instance ref; the chip calls `trigger()` and reflects active/error from
|
|
108
|
+
// the control's events. Privacy: the control is only mounted when
|
|
109
|
+
// `geolocate` is true, and nothing prompts permission until the chip is
|
|
110
|
+
// pressed (`trigger()`) — never on mount.
|
|
111
|
+
const geolocateRef = useRef<GeolocateControlInstance | null>(null)
|
|
112
|
+
const [geolocateState, setGeolocateState] = useState<GeolocateState>('idle')
|
|
113
|
+
const handleGeolocateTrigger = useCallback(() => {
|
|
114
|
+
// A new trigger after an error is a retry — clear the disabled look so
|
|
115
|
+
// the events can re-drive state.
|
|
116
|
+
setGeolocateState((s) => (s === 'error' ? 'idle' : s))
|
|
117
|
+
geolocateRef.current?.trigger()
|
|
118
|
+
}, [])
|
|
119
|
+
|
|
120
|
+
// Geocoder search — a toolbar magnifier chip toggles an on-map autocomplete
|
|
121
|
+
// (Google-Maps style). Collapsed by default; the chip flips `geocoderOpen`,
|
|
122
|
+
// which mounts the controlled `GeocoderControl` (autofocused). Picking a
|
|
123
|
+
// result / Esc / re-clicking the chip closes it.
|
|
124
|
+
const [geocoderOpen, setGeocoderOpen] = useState(false)
|
|
125
|
+
const handleGeocoderToggle = useCallback(() => setGeocoderOpen((o) => !o), [])
|
|
126
|
+
const closeGeocoder = useCallback(() => setGeocoderOpen(false), [])
|
|
127
|
+
|
|
128
|
+
// External-maps link (host URL wins, else Google Maps at current center).
|
|
129
|
+
const { externalMapsUrl, externalMapsLabel } = useExternalMapsUrl({
|
|
130
|
+
openInMapsUrl,
|
|
131
|
+
openInMapsLabel,
|
|
132
|
+
googleMapsButton,
|
|
133
|
+
viewport,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Fullscreen — opens a fresh map in a large dialog, seeded with the
|
|
137
|
+
// viewport the user is currently looking at.
|
|
138
|
+
const [fullscreenOpen, setFullscreenOpen] = useState(false)
|
|
139
|
+
|
|
140
|
+
// `onClose` is only ever passed to the dialog's own (fullscreen) map, so
|
|
141
|
+
// it doubles as "this instance is the expanded one" — there gestures are
|
|
142
|
+
// always live.
|
|
143
|
+
const isFullscreenInstance = !!onClose
|
|
144
|
+
|
|
145
|
+
// Gesture mode. Touch / narrow inline maps are made fully static (a
|
|
146
|
+
// finger drags the PAGE, never the map); the user taps to expand into
|
|
147
|
+
// the fullscreen dialog where the map is fully interactive. Desktop
|
|
148
|
+
// keeps the Google-Maps scroll-isolation. The expanded map is always
|
|
149
|
+
// live. Detection is `matchMedia`-based so it reacts to DevTools device
|
|
150
|
+
// emulation, real touchscreens and window resizes (see useMapTouchMode).
|
|
151
|
+
const isTouch = useMapTouchMode()
|
|
152
|
+
const protectionMode = !scrollProtection || isFullscreenInstance
|
|
153
|
+
? 'off'
|
|
154
|
+
: isTouch
|
|
155
|
+
? 'mobile'
|
|
156
|
+
: 'desktop'
|
|
157
|
+
|
|
158
|
+
const { locked, unlock, isMobileLock } = useMapScrollProtection(protectionMode)
|
|
159
|
+
|
|
160
|
+
// Overlay tap: desktop unlocks in place; mobile opens fullscreen.
|
|
161
|
+
const handleOverlayActivate = useCallback(() => {
|
|
162
|
+
if (isMobileLock) {
|
|
163
|
+
setFullscreenOpen(true)
|
|
164
|
+
} else {
|
|
165
|
+
unlock()
|
|
166
|
+
}
|
|
167
|
+
}, [isMobileLock, unlock])
|
|
168
|
+
|
|
169
|
+
// Hint adapts to the gesture model.
|
|
170
|
+
const resolvedHint = isMobileLock ? 'Tap to expand' : scrollProtectionHint
|
|
171
|
+
|
|
172
|
+
// Check if viewport has changed from initial
|
|
173
|
+
const hasViewportChanged =
|
|
174
|
+
Math.abs(viewport.longitude - initialViewport.longitude) > 0.0001 ||
|
|
175
|
+
Math.abs(viewport.latitude - initialViewport.latitude) > 0.0001 ||
|
|
176
|
+
Math.abs(viewport.zoom - initialViewport.zoom) > 0.1
|
|
177
|
+
|
|
178
|
+
const handleMove = useCallback(
|
|
179
|
+
(evt: ViewStateChangeEvent) => {
|
|
180
|
+
setViewport({
|
|
181
|
+
longitude: evt.viewState.longitude,
|
|
182
|
+
latitude: evt.viewState.latitude,
|
|
183
|
+
zoom: evt.viewState.zoom,
|
|
184
|
+
bearing: evt.viewState.bearing,
|
|
185
|
+
pitch: evt.viewState.pitch,
|
|
186
|
+
})
|
|
187
|
+
},
|
|
188
|
+
[setViewport]
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const { handleMoveStart, handleMoveEnd } = useAutoReset({ autoResetDelay, resetToInitial })
|
|
192
|
+
|
|
193
|
+
const handleLoad = useCallback(() => {
|
|
194
|
+
setIsLoaded(true)
|
|
195
|
+
}, [setIsLoaded])
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<>
|
|
199
|
+
<Map
|
|
200
|
+
ref={mapRef}
|
|
201
|
+
{...viewport}
|
|
202
|
+
onMove={handleMove}
|
|
203
|
+
onMoveStart={handleMoveStart}
|
|
204
|
+
onMoveEnd={handleMoveEnd}
|
|
205
|
+
onLoad={handleLoad}
|
|
206
|
+
mapStyle={resolvedStyle}
|
|
207
|
+
interactiveLayerIds={interactiveLayerIds}
|
|
208
|
+
attributionControl={attributionControl ? {} : false}
|
|
209
|
+
reuseMaps={reuseMaps}
|
|
210
|
+
style={{
|
|
211
|
+
width: '100%',
|
|
212
|
+
height: '100%',
|
|
213
|
+
...style,
|
|
214
|
+
}}
|
|
215
|
+
cursor={cursor}
|
|
216
|
+
aria-label={ariaLabel}
|
|
217
|
+
>
|
|
218
|
+
{geolocate && (
|
|
219
|
+
<GeolocateHandle
|
|
220
|
+
ref={geolocateRef}
|
|
221
|
+
options={geolocateOptions ?? {}}
|
|
222
|
+
onGeolocate={onGeolocate}
|
|
223
|
+
onGeolocateError={onGeolocateError}
|
|
224
|
+
onStateChange={setGeolocateState}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
{/* Toolbar-toggled geocoder. Controlled (open driven by the chip),
|
|
228
|
+
autofocused on reveal. Rendered inside <Map> so its dropped pin
|
|
229
|
+
(a react-map-gl Marker) has the map context; the input overlay is
|
|
230
|
+
absolute-positioned top-left, clear of the top-right chip column. */}
|
|
231
|
+
{geocoder && (
|
|
232
|
+
<GeocoderControl
|
|
233
|
+
open={geocoderOpen}
|
|
234
|
+
onClose={closeGeocoder}
|
|
235
|
+
autoFocus
|
|
236
|
+
resolve={geocoderResolve}
|
|
237
|
+
placeholder={geocoderPlaceholder}
|
|
238
|
+
onResult={onGeocoderResult}
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
241
|
+
{children}
|
|
242
|
+
</Map>
|
|
243
|
+
|
|
244
|
+
{locked && (
|
|
245
|
+
<ScrollLockOverlay
|
|
246
|
+
isMobileLock={isMobileLock}
|
|
247
|
+
resolvedHint={resolvedHint}
|
|
248
|
+
onActivate={handleOverlayActivate}
|
|
249
|
+
/>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
<MapToolbar
|
|
253
|
+
onClose={onClose}
|
|
254
|
+
fullscreenButton={fullscreenButton}
|
|
255
|
+
fullscreenLabel={fullscreenLabel}
|
|
256
|
+
onFullscreenOpen={() => setFullscreenOpen(true)}
|
|
257
|
+
basemapSwitcher={basemapSwitcher}
|
|
258
|
+
basemapOptions={basemapOptions}
|
|
259
|
+
basemapLabel={basemapLabel}
|
|
260
|
+
activeKey={activeKey}
|
|
261
|
+
onBasemapPick={handleBasemapPick}
|
|
262
|
+
terrainButton={terrainButton}
|
|
263
|
+
terrainLabel={terrainLabel}
|
|
264
|
+
terrainOn={terrainOn}
|
|
265
|
+
onTerrainToggle={handleTerrainToggle}
|
|
266
|
+
compassButton={compassButton}
|
|
267
|
+
isOriented={isOriented}
|
|
268
|
+
compassLabel={compassLabel}
|
|
269
|
+
bearing={bearing}
|
|
270
|
+
onResetNorth={resetNorth}
|
|
271
|
+
geolocate={geolocate}
|
|
272
|
+
geolocateLabel={geolocateLabel}
|
|
273
|
+
geolocateState={geolocateState}
|
|
274
|
+
onGeolocateTrigger={handleGeolocateTrigger}
|
|
275
|
+
geocoder={geocoder}
|
|
276
|
+
geocoderOpen={geocoderOpen}
|
|
277
|
+
onGeocoderToggle={handleGeocoderToggle}
|
|
278
|
+
externalMapsUrl={externalMapsUrl}
|
|
279
|
+
externalMapsLabel={externalMapsLabel}
|
|
280
|
+
showResetButton={showResetButton}
|
|
281
|
+
hasViewportChanged={hasViewportChanged}
|
|
282
|
+
onResetToInitial={resetToInitial}
|
|
283
|
+
/>
|
|
284
|
+
|
|
285
|
+
<RotateHint show={showRotateHint} />
|
|
286
|
+
|
|
287
|
+
{fullscreenButton && (
|
|
288
|
+
<FullscreenDialog
|
|
289
|
+
MapContainer={MapContainerComponent}
|
|
290
|
+
open={fullscreenOpen}
|
|
291
|
+
onOpenChange={setFullscreenOpen}
|
|
292
|
+
fullscreenTitle={fullscreenTitle}
|
|
293
|
+
viewport={viewport}
|
|
294
|
+
effectiveStyle={effectiveStyle}
|
|
295
|
+
interactiveLayerIds={interactiveLayerIds}
|
|
296
|
+
openInMapsUrl={openInMapsUrl}
|
|
297
|
+
openInMapsLabel={openInMapsLabel}
|
|
298
|
+
googleMapsButton={googleMapsButton}
|
|
299
|
+
basemapSwitcher={basemapSwitcher}
|
|
300
|
+
basemaps={basemaps}
|
|
301
|
+
onBasemapChange={onBasemapChange}
|
|
302
|
+
terrainOn={terrainOn}
|
|
303
|
+
terrainButton={terrainButton}
|
|
304
|
+
terrainLabel={terrainLabel}
|
|
305
|
+
onTerrainChange={onTerrainChange}
|
|
306
|
+
compassButton={compassButton}
|
|
307
|
+
compassLabel={compassLabel}
|
|
308
|
+
geolocate={geolocate}
|
|
309
|
+
geolocateLabel={geolocateLabel}
|
|
310
|
+
geolocateOptions={geolocateOptions}
|
|
311
|
+
onGeolocate={onGeolocate}
|
|
312
|
+
onGeolocateError={onGeolocateError}
|
|
313
|
+
geocoder={geocoder}
|
|
314
|
+
geocoderResolve={geocoderResolve}
|
|
315
|
+
geocoderPlaceholder={geocoderPlaceholder}
|
|
316
|
+
onGeocoderResult={onGeocoderResult}
|
|
317
|
+
showResetButton={showResetButton}
|
|
318
|
+
ariaLabel={ariaLabel}
|
|
319
|
+
onClose={() => setFullscreenOpen(false)}
|
|
320
|
+
>
|
|
321
|
+
{children}
|
|
322
|
+
</FullscreenDialog>
|
|
323
|
+
)}
|
|
324
|
+
</>
|
|
325
|
+
)
|
|
326
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Layers, LocateFixed, MapPin, Maximize2, Mountain, RotateCcw, Search, X } from 'lucide-react'
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib'
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuRadioGroup,
|
|
9
|
+
DropdownMenuRadioItem,
|
|
10
|
+
DropdownMenuTrigger,
|
|
11
|
+
Tooltip,
|
|
12
|
+
TooltipContent,
|
|
13
|
+
TooltipTrigger,
|
|
14
|
+
} from '@djangocfg/ui-core/components'
|
|
15
|
+
|
|
16
|
+
import { MAP_STYLE_LABELS } from '../../styles'
|
|
17
|
+
import type { MapStyleKey } from '../../types'
|
|
18
|
+
import { CompassChip } from '../CompassChip'
|
|
19
|
+
import { MAP_CONTROL_CLASS } from './constants'
|
|
20
|
+
import type { GeolocateState } from './GeolocateHandle'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Map controls — one icon-only toolbar, top-right (Google-Maps style). All
|
|
24
|
+
* chips are a uniform 36×36 square; labels live in tooltips (shown on the LEFT
|
|
25
|
+
* so they never spill off the map's right edge). Sits above the scroll-lock
|
|
26
|
+
* overlay so it stays clickable.
|
|
27
|
+
*
|
|
28
|
+
* The `data-map-control` attribute is load-bearing: the scroll-protection
|
|
29
|
+
* re-lock skips clicks inside it.
|
|
30
|
+
*/
|
|
31
|
+
export function MapToolbar({
|
|
32
|
+
onClose,
|
|
33
|
+
fullscreenButton,
|
|
34
|
+
fullscreenLabel,
|
|
35
|
+
onFullscreenOpen,
|
|
36
|
+
basemapSwitcher,
|
|
37
|
+
basemapOptions,
|
|
38
|
+
basemapLabel,
|
|
39
|
+
activeKey,
|
|
40
|
+
onBasemapPick,
|
|
41
|
+
terrainButton,
|
|
42
|
+
terrainLabel,
|
|
43
|
+
terrainOn,
|
|
44
|
+
onTerrainToggle,
|
|
45
|
+
compassButton,
|
|
46
|
+
isOriented,
|
|
47
|
+
compassLabel,
|
|
48
|
+
bearing,
|
|
49
|
+
onResetNorth,
|
|
50
|
+
geolocate,
|
|
51
|
+
geolocateLabel,
|
|
52
|
+
geolocateState,
|
|
53
|
+
onGeolocateTrigger,
|
|
54
|
+
geocoder,
|
|
55
|
+
geocoderOpen,
|
|
56
|
+
onGeocoderToggle,
|
|
57
|
+
externalMapsUrl,
|
|
58
|
+
externalMapsLabel,
|
|
59
|
+
showResetButton,
|
|
60
|
+
hasViewportChanged,
|
|
61
|
+
onResetToInitial,
|
|
62
|
+
}: {
|
|
63
|
+
onClose: (() => void) | undefined
|
|
64
|
+
fullscreenButton: boolean
|
|
65
|
+
fullscreenLabel: string
|
|
66
|
+
onFullscreenOpen: () => void
|
|
67
|
+
basemapSwitcher: boolean
|
|
68
|
+
basemapOptions: MapStyleKey[]
|
|
69
|
+
basemapLabel: string
|
|
70
|
+
activeKey: MapStyleKey | undefined
|
|
71
|
+
onBasemapPick: (key: string) => void
|
|
72
|
+
terrainButton: boolean
|
|
73
|
+
terrainLabel: string
|
|
74
|
+
terrainOn: boolean
|
|
75
|
+
onTerrainToggle: () => void
|
|
76
|
+
compassButton: boolean
|
|
77
|
+
isOriented: boolean
|
|
78
|
+
compassLabel: string
|
|
79
|
+
bearing: number
|
|
80
|
+
onResetNorth: () => void
|
|
81
|
+
geolocate: boolean
|
|
82
|
+
geolocateLabel: string
|
|
83
|
+
geolocateState: GeolocateState
|
|
84
|
+
onGeolocateTrigger: () => void
|
|
85
|
+
geocoder: boolean
|
|
86
|
+
geocoderOpen: boolean
|
|
87
|
+
onGeocoderToggle: () => void
|
|
88
|
+
externalMapsUrl: string | undefined
|
|
89
|
+
externalMapsLabel: string
|
|
90
|
+
showResetButton: boolean
|
|
91
|
+
hasViewportChanged: boolean
|
|
92
|
+
onResetToInitial: () => void
|
|
93
|
+
}) {
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
data-map-control
|
|
97
|
+
className="absolute top-3 right-3 z-20 flex flex-col items-end gap-2"
|
|
98
|
+
>
|
|
99
|
+
{onClose && (
|
|
100
|
+
<Tooltip>
|
|
101
|
+
<TooltipTrigger asChild>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={onClose}
|
|
105
|
+
aria-label="Close map"
|
|
106
|
+
className={cn(MAP_CONTROL_CLASS, 'w-9 h-9')}
|
|
107
|
+
>
|
|
108
|
+
<X className="w-4 h-4" aria-hidden="true" />
|
|
109
|
+
</button>
|
|
110
|
+
</TooltipTrigger>
|
|
111
|
+
<TooltipContent side="left">Close</TooltipContent>
|
|
112
|
+
</Tooltip>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{fullscreenButton && (
|
|
116
|
+
<Tooltip>
|
|
117
|
+
<TooltipTrigger asChild>
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
onClick={onFullscreenOpen}
|
|
121
|
+
aria-label={fullscreenLabel}
|
|
122
|
+
className={cn(MAP_CONTROL_CLASS, 'w-9 h-9')}
|
|
123
|
+
>
|
|
124
|
+
<Maximize2 className="w-4 h-4" aria-hidden="true" />
|
|
125
|
+
</button>
|
|
126
|
+
</TooltipTrigger>
|
|
127
|
+
<TooltipContent side="left">{fullscreenLabel}</TooltipContent>
|
|
128
|
+
</Tooltip>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* Search (geocoder) — Google-Maps-style magnifier that toggles an
|
|
132
|
+
on-map autocomplete (rendered top-left by MapInner). Tinted
|
|
133
|
+
text-primary while open; aria-pressed mirrors the open state. */}
|
|
134
|
+
{geocoder && (
|
|
135
|
+
<Tooltip>
|
|
136
|
+
<TooltipTrigger asChild>
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
onClick={onGeocoderToggle}
|
|
140
|
+
aria-label="Search the map"
|
|
141
|
+
aria-pressed={geocoderOpen}
|
|
142
|
+
className={cn(
|
|
143
|
+
MAP_CONTROL_CLASS,
|
|
144
|
+
'w-9 h-9',
|
|
145
|
+
geocoderOpen && 'bg-popover text-primary',
|
|
146
|
+
)}
|
|
147
|
+
>
|
|
148
|
+
<Search className="w-4 h-4" aria-hidden="true" />
|
|
149
|
+
</button>
|
|
150
|
+
</TooltipTrigger>
|
|
151
|
+
<TooltipContent side="left">
|
|
152
|
+
{geocoderOpen ? 'Close search' : 'Search'}
|
|
153
|
+
</TooltipContent>
|
|
154
|
+
</Tooltip>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{/* Basemap switcher (Google-Maps "Layers") — pick the map style at
|
|
158
|
+
runtime. Hidden if disabled or there's nothing to switch to. */}
|
|
159
|
+
{basemapSwitcher && basemapOptions.length > 1 && (
|
|
160
|
+
<DropdownMenu>
|
|
161
|
+
<Tooltip>
|
|
162
|
+
<TooltipTrigger asChild>
|
|
163
|
+
<DropdownMenuTrigger
|
|
164
|
+
aria-label={basemapLabel}
|
|
165
|
+
className={cn(MAP_CONTROL_CLASS, 'w-9 h-9')}
|
|
166
|
+
>
|
|
167
|
+
<Layers className="w-4 h-4" aria-hidden="true" />
|
|
168
|
+
</DropdownMenuTrigger>
|
|
169
|
+
</TooltipTrigger>
|
|
170
|
+
<TooltipContent side="left">{basemapLabel}</TooltipContent>
|
|
171
|
+
</Tooltip>
|
|
172
|
+
<DropdownMenuContent align="end" side="left" className="min-w-36">
|
|
173
|
+
<DropdownMenuRadioGroup
|
|
174
|
+
value={activeKey ?? ''}
|
|
175
|
+
onValueChange={onBasemapPick}
|
|
176
|
+
>
|
|
177
|
+
{basemapOptions.map((key) => (
|
|
178
|
+
<DropdownMenuRadioItem key={key} value={key}>
|
|
179
|
+
{MAP_STYLE_LABELS[key]}
|
|
180
|
+
</DropdownMenuRadioItem>
|
|
181
|
+
))}
|
|
182
|
+
</DropdownMenuRadioGroup>
|
|
183
|
+
</DropdownMenuContent>
|
|
184
|
+
</DropdownMenu>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{/* 3D terrain toggle (Mountain) — adds the free DEM terrain +
|
|
188
|
+
hillshade and pitches the camera in so it reads as 3D. */}
|
|
189
|
+
{terrainButton && (
|
|
190
|
+
<Tooltip>
|
|
191
|
+
<TooltipTrigger asChild>
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={onTerrainToggle}
|
|
195
|
+
aria-label={terrainLabel}
|
|
196
|
+
aria-pressed={terrainOn}
|
|
197
|
+
className={cn(
|
|
198
|
+
MAP_CONTROL_CLASS,
|
|
199
|
+
'w-9 h-9',
|
|
200
|
+
terrainOn && 'bg-popover text-primary',
|
|
201
|
+
)}
|
|
202
|
+
>
|
|
203
|
+
<Mountain className="w-4 h-4" aria-hidden="true" />
|
|
204
|
+
</button>
|
|
205
|
+
</TooltipTrigger>
|
|
206
|
+
<TooltipContent side="left">{terrainLabel}</TooltipContent>
|
|
207
|
+
</Tooltip>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{/* Compass — shows only when rotated/tilted (gated here); the chip's
|
|
211
|
+
needle reflects the bearing and tints accent as an off-north cue,
|
|
212
|
+
click eases back to north-up + top-down (Google style). */}
|
|
213
|
+
{compassButton && isOriented && (
|
|
214
|
+
<CompassChip
|
|
215
|
+
bearing={bearing}
|
|
216
|
+
oriented={isOriented}
|
|
217
|
+
onReset={onResetNorth}
|
|
218
|
+
label={compassLabel}
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{/* Locate me (GPS) — drives maplibre's hidden GeolocateControl via
|
|
223
|
+
trigger(). Tinted text-primary while actively following (like the
|
|
224
|
+
terrain chip); muted/disabled look on permission-denied or
|
|
225
|
+
unavailable, with a tooltip explaining it. Opt-in only. */}
|
|
226
|
+
{geolocate && (
|
|
227
|
+
<Tooltip>
|
|
228
|
+
<TooltipTrigger asChild>
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
onClick={onGeolocateTrigger}
|
|
232
|
+
aria-label={geolocateLabel}
|
|
233
|
+
aria-pressed={geolocateState === 'active'}
|
|
234
|
+
className={cn(
|
|
235
|
+
MAP_CONTROL_CLASS,
|
|
236
|
+
'w-9 h-9',
|
|
237
|
+
geolocateState === 'active' && 'bg-popover text-primary',
|
|
238
|
+
geolocateState === 'error' && 'text-muted-foreground',
|
|
239
|
+
)}
|
|
240
|
+
>
|
|
241
|
+
<LocateFixed className="w-4 h-4" aria-hidden="true" />
|
|
242
|
+
</button>
|
|
243
|
+
</TooltipTrigger>
|
|
244
|
+
<TooltipContent side="left">
|
|
245
|
+
{geolocateState === 'error'
|
|
246
|
+
? 'Location unavailable — check permission'
|
|
247
|
+
: geolocateLabel}
|
|
248
|
+
</TooltipContent>
|
|
249
|
+
</Tooltip>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
{externalMapsUrl && (
|
|
253
|
+
<Tooltip>
|
|
254
|
+
<TooltipTrigger asChild>
|
|
255
|
+
<a
|
|
256
|
+
href={externalMapsUrl}
|
|
257
|
+
target="_blank"
|
|
258
|
+
rel="noopener noreferrer"
|
|
259
|
+
aria-label={externalMapsLabel}
|
|
260
|
+
className={cn(MAP_CONTROL_CLASS, 'w-9 h-9')}
|
|
261
|
+
>
|
|
262
|
+
<MapPin className="w-4 h-4" aria-hidden="true" />
|
|
263
|
+
</a>
|
|
264
|
+
</TooltipTrigger>
|
|
265
|
+
<TooltipContent side="left">{externalMapsLabel}</TooltipContent>
|
|
266
|
+
</Tooltip>
|
|
267
|
+
)}
|
|
268
|
+
|
|
269
|
+
{showResetButton && hasViewportChanged && (
|
|
270
|
+
<Tooltip>
|
|
271
|
+
<TooltipTrigger asChild>
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
onClick={onResetToInitial}
|
|
275
|
+
aria-label="Reset map view"
|
|
276
|
+
className={cn(MAP_CONTROL_CLASS, 'w-9 h-9')}
|
|
277
|
+
>
|
|
278
|
+
<RotateCcw className="w-4 h-4" aria-hidden="true" />
|
|
279
|
+
</button>
|
|
280
|
+
</TooltipTrigger>
|
|
281
|
+
<TooltipContent side="left">Reset to the initial view</TooltipContent>
|
|
282
|
+
</Tooltip>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '@djangocfg/ui-core/lib'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gesture hint — appears briefly when 3D turns on, since rotate/tilt
|
|
7
|
+
* (right-drag / Ctrl+drag / two fingers) isn't otherwise discoverable. The
|
|
8
|
+
* compass chip then offers a one-click reset back to north.
|
|
9
|
+
*/
|
|
10
|
+
export function RotateHint({ show }: { show: boolean }) {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
aria-hidden={!show}
|
|
14
|
+
className={cn(
|
|
15
|
+
'pointer-events-none absolute bottom-3 left-1/2 z-20 -translate-x-1/2',
|
|
16
|
+
'rounded-full px-3 py-1.5 text-xs font-medium',
|
|
17
|
+
'bg-popover/95 text-popover-foreground border border-border shadow-sm',
|
|
18
|
+
'transition-opacity duration-300',
|
|
19
|
+
show ? 'opacity-100' : 'opacity-0',
|
|
20
|
+
)}
|
|
21
|
+
>
|
|
22
|
+
Right-click or Ctrl+drag to rotate & tilt
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Gesture overlay. Desktop: tap unlocks scroll-zoom in place. Mobile: the map
|
|
8
|
+
* is fully static, tap expands to fullscreen. `touch-action: pan-y` lets a
|
|
9
|
+
* vertical swipe scroll the PAGE straight through the overlay so the static
|
|
10
|
+
* map never traps the scroll.
|
|
11
|
+
*/
|
|
12
|
+
export function ScrollLockOverlay({
|
|
13
|
+
isMobileLock,
|
|
14
|
+
resolvedHint,
|
|
15
|
+
onActivate,
|
|
16
|
+
}: {
|
|
17
|
+
isMobileLock: boolean
|
|
18
|
+
resolvedHint: string
|
|
19
|
+
onActivate: () => void
|
|
20
|
+
}) {
|
|
21
|
+
const [overlayHovered, setOverlayHovered] = useState(false)
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
onClick={onActivate}
|
|
27
|
+
onMouseEnter={() => setOverlayHovered(true)}
|
|
28
|
+
onMouseLeave={() => setOverlayHovered(false)}
|
|
29
|
+
aria-label={`${resolvedHint} (map interaction is locked)`}
|
|
30
|
+
style={{ touchAction: 'pan-y' }}
|
|
31
|
+
className={cn(
|
|
32
|
+
'absolute inset-0 z-10 flex items-center justify-center',
|
|
33
|
+
'cursor-pointer border-0 bg-transparent p-0 transition-colors',
|
|
34
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring',
|
|
35
|
+
overlayHovered && 'bg-foreground/5'
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
<span
|
|
39
|
+
className={cn(
|
|
40
|
+
// Same popover surface as the control chips — floating hint.
|
|
41
|
+
// Mobile has no hover, so the "Tap to expand" hint is shown
|
|
42
|
+
// persistently; desktop reveals it on hover.
|
|
43
|
+
'pointer-events-none select-none rounded-full px-3 py-1.5 text-xs font-medium',
|
|
44
|
+
'bg-popover/95 text-popover-foreground border border-border shadow-sm transition-opacity',
|
|
45
|
+
isMobileLock || overlayHovered ? 'opacity-100' : 'opacity-0'
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
{resolvedHint}
|
|
49
|
+
</span>
|
|
50
|
+
</button>
|
|
51
|
+
)
|
|
52
|
+
}
|