@djangocfg/ui-tools 2.1.431 → 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/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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { useMapContext } from '../context'
|
|
6
|
+
|
|
7
|
+
export type MapScrollProtectionMode = 'off' | 'desktop' | 'mobile'
|
|
8
|
+
|
|
9
|
+
export interface UseMapScrollProtectionResult {
|
|
10
|
+
/** Whether the lock overlay should be shown. */
|
|
11
|
+
locked: boolean
|
|
12
|
+
/**
|
|
13
|
+
* Click/tap handler for the overlay. On desktop it unlocks scroll-zoom
|
|
14
|
+
* in place; on mobile it stays locked (the caller opens fullscreen).
|
|
15
|
+
*/
|
|
16
|
+
unlock: () => void
|
|
17
|
+
/** True when running in mobile mode — the caller should expand to
|
|
18
|
+
* fullscreen on tap instead of unlocking in place. */
|
|
19
|
+
isMobileLock: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Gesture guard for an inline map.
|
|
24
|
+
*
|
|
25
|
+
* - `'desktop'` — Google-Maps-style scroll isolation. Only the wheel/
|
|
26
|
+
* trackpad **scroll-zoom** is blocked until the user clicks the map;
|
|
27
|
+
* drag-pan / double-click / pinch stay live. Click inside unlocks,
|
|
28
|
+
* click outside re-locks. So scrolling a page past the map never
|
|
29
|
+
* hijacks the zoom, but the map is never fully inert.
|
|
30
|
+
*
|
|
31
|
+
* - `'mobile'` — the map is made **fully static**: scroll-zoom, drag-pan,
|
|
32
|
+
* touch pinch/rotate, double-click and keyboard are all disabled, so a
|
|
33
|
+
* finger dragging over the map scrolls the PAGE. There is no in-place
|
|
34
|
+
* unlock; the caller turns a tap into "open fullscreen", where a fresh
|
|
35
|
+
* map runs with all gestures enabled. This is the native-app pattern
|
|
36
|
+
* (static map preview in a feed → tap to open the full map).
|
|
37
|
+
*
|
|
38
|
+
* - `'off'` — every gesture enabled, no overlay.
|
|
39
|
+
*/
|
|
40
|
+
export function useMapScrollProtection(
|
|
41
|
+
mode: MapScrollProtectionMode,
|
|
42
|
+
): UseMapScrollProtectionResult {
|
|
43
|
+
const { mapRef, isLoaded } = useMapContext()
|
|
44
|
+
const [locked, setLocked] = useState(true)
|
|
45
|
+
|
|
46
|
+
// Latest `locked` without forcing the document-listener effect to
|
|
47
|
+
// re-subscribe on every toggle.
|
|
48
|
+
const lockedRef = useRef(locked)
|
|
49
|
+
lockedRef.current = locked
|
|
50
|
+
|
|
51
|
+
// On mobile the lock is permanent (tap opens fullscreen); only desktop
|
|
52
|
+
// unlocks in place.
|
|
53
|
+
const unlock = useCallback(() => {
|
|
54
|
+
if (mode === 'desktop') setLocked(false)
|
|
55
|
+
}, [mode])
|
|
56
|
+
|
|
57
|
+
// All the maplibre gesture handlers we toggle in mobile mode. Listed
|
|
58
|
+
// defensively (some are absent depending on options) — each is guarded.
|
|
59
|
+
const setAllGestures = useCallback(
|
|
60
|
+
(map: maplibregl.Map, enable: boolean) => {
|
|
61
|
+
const handlers = [
|
|
62
|
+
map.scrollZoom,
|
|
63
|
+
map.boxZoom,
|
|
64
|
+
map.dragRotate,
|
|
65
|
+
map.dragPan,
|
|
66
|
+
map.keyboard,
|
|
67
|
+
map.doubleClickZoom,
|
|
68
|
+
map.touchZoomRotate,
|
|
69
|
+
(map as unknown as { touchPitch?: { enable(): void; disable(): void } }).touchPitch,
|
|
70
|
+
]
|
|
71
|
+
for (const h of handlers) {
|
|
72
|
+
if (h && typeof h.enable === 'function') {
|
|
73
|
+
enable ? h.enable() : h.disable()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
[],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
// Apply the lock to maplibre. Re-runs on load and whenever mode/lock change.
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const map = mapRef.current?.getMap()
|
|
83
|
+
if (!map) return
|
|
84
|
+
|
|
85
|
+
if (mode === 'off') {
|
|
86
|
+
// Make sure nothing we may have disabled stays off.
|
|
87
|
+
map.scrollZoom.enable()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (mode === 'mobile') {
|
|
92
|
+
// Fully static while locked; fully live once unlocked (never happens
|
|
93
|
+
// on mobile, but keep the branch symmetric).
|
|
94
|
+
setAllGestures(map, !locked)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// desktop: only the scroll-zoom is gated.
|
|
99
|
+
if (locked) {
|
|
100
|
+
map.scrollZoom.disable()
|
|
101
|
+
} else {
|
|
102
|
+
map.scrollZoom.enable()
|
|
103
|
+
}
|
|
104
|
+
}, [mapRef, isLoaded, mode, locked, setAllGestures])
|
|
105
|
+
|
|
106
|
+
// Re-lock (desktop only) when the user clicks anywhere OUTSIDE the map.
|
|
107
|
+
// We do NOT re-lock on mouse-leave: once you've clicked into the map you
|
|
108
|
+
// keep interacting even as the pointer wanders off and back — the Google
|
|
109
|
+
// Maps behavior. Capture phase so we see the click before inner handlers
|
|
110
|
+
// can stop it. Mobile never unlocks in place, so it needs no re-lock.
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (mode !== 'desktop') return
|
|
113
|
+
const container = mapRef.current?.getMap()?.getContainer()
|
|
114
|
+
if (!container) return
|
|
115
|
+
|
|
116
|
+
const handleDocPointerDown = (e: Event) => {
|
|
117
|
+
const target = e.target as Element | null
|
|
118
|
+
// A toolbar chip (or a menu/tooltip it opens) — never re-locks, and
|
|
119
|
+
// when the map is LOCKED, pressing a control unlocks it: the user is
|
|
120
|
+
// clearly working with the map, so the scroll guard should step aside.
|
|
121
|
+
const onControl = !!target?.closest?.(
|
|
122
|
+
'[data-map-control],[data-radix-popper-content-wrapper],[role="menu"],[role="tooltip"]',
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if (lockedRef.current) {
|
|
126
|
+
// Only desktop unlocks in place; `unlock()` is a no-op on mobile.
|
|
127
|
+
if (onControl) unlock()
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Unlocked: clicking a control must NOT re-lock (otherwise pressing a
|
|
132
|
+
// toolbar button would immediately re-lock the map).
|
|
133
|
+
if (onControl) return
|
|
134
|
+
if (!container.contains(e.target as Node)) {
|
|
135
|
+
setLocked(true)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
document.addEventListener('pointerdown', handleDocPointerDown, true)
|
|
140
|
+
return () => {
|
|
141
|
+
document.removeEventListener('pointerdown', handleDocPointerDown, true)
|
|
142
|
+
}
|
|
143
|
+
}, [mapRef, isLoaded, mode, unlock])
|
|
144
|
+
|
|
145
|
+
// Reset to locked whenever the feature is (re)enabled.
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (mode !== 'off') setLocked(true)
|
|
148
|
+
}, [mode])
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
locked: mode !== 'off' && locked,
|
|
152
|
+
unlock,
|
|
153
|
+
isMobileLock: mode === 'mobile' && locked,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useIsTouch, useMediaQuery, MEDIA_BREAKPOINTS } from '@djangocfg/ui-core/hooks'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Should the map behave as a "mobile / touch" surface?
|
|
7
|
+
*
|
|
8
|
+
* True when the primary pointer is coarse (a finger) OR the viewport is
|
|
9
|
+
* narrow. Both signals come from ui-core's `matchMedia`-based hooks, NOT
|
|
10
|
+
* the userAgent — so this reflects real touchscreens, DevTools device
|
|
11
|
+
* emulation, narrow windows and tablets, and updates live. (UA sniffing
|
|
12
|
+
* misses touch laptops, mis-detects iPadOS, and won't react to a resize.)
|
|
13
|
+
*
|
|
14
|
+
* @param breakpoint width (px) at/under which the map is treated as mobile
|
|
15
|
+
* even with a fine pointer. Default 768 (Tailwind `md`).
|
|
16
|
+
*/
|
|
17
|
+
export function useMapTouchMode(breakpoint = MEDIA_BREAKPOINTS.md): boolean {
|
|
18
|
+
const isTouch = useIsTouch()
|
|
19
|
+
const isNarrow = useMediaQuery(`(max-width: ${breakpoint - 1}px)`)
|
|
20
|
+
return isTouch || isNarrow
|
|
21
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
export interface UseMarkerCardResult {
|
|
6
|
+
/** The currently open marker id, or `null` when none is open. */
|
|
7
|
+
openId: string | null
|
|
8
|
+
/** Open the card for `id` (closes any other open card). */
|
|
9
|
+
open: (id: string) => void
|
|
10
|
+
/** Close the open card. */
|
|
11
|
+
close: () => void
|
|
12
|
+
/** Toggle `id` — closes it if it's already the open one. */
|
|
13
|
+
toggle: (id: string) => void
|
|
14
|
+
/** Whether `id` is the currently open card. */
|
|
15
|
+
isOpen: (id: string) => boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* useMarkerCard — selection state for marker info-cards. Holds a single
|
|
20
|
+
* `openId` so only ONE card is open at a time; opening a new id closes the
|
|
21
|
+
* previous, and `toggle` closes the same id. Pure React glue — no UI, no map
|
|
22
|
+
* dependency. Drives `MapMarkers`, or use it standalone for manual control.
|
|
23
|
+
*/
|
|
24
|
+
export function useMarkerCard(): UseMarkerCardResult {
|
|
25
|
+
const [openId, setOpenId] = useState<string | null>(null)
|
|
26
|
+
|
|
27
|
+
const open = useCallback((id: string) => setOpenId(id), [])
|
|
28
|
+
const close = useCallback(() => setOpenId(null), [])
|
|
29
|
+
const toggle = useCallback(
|
|
30
|
+
(id: string) => setOpenId((prev) => (prev === id ? null : id)),
|
|
31
|
+
[]
|
|
32
|
+
)
|
|
33
|
+
const isOpen = useCallback((id: string) => openId === id, [openId])
|
|
34
|
+
|
|
35
|
+
return useMemo(
|
|
36
|
+
() => ({ openId, open, close, toggle, isOpen }),
|
|
37
|
+
[openId, open, close, toggle, isOpen]
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -48,9 +48,15 @@ export function useMarkers(): MarkerActions {
|
|
|
48
48
|
}, [setMarkers])
|
|
49
49
|
|
|
50
50
|
const fitToMarkers = useCallback(
|
|
51
|
-
|
|
51
|
+
// Accepts a plain `padding` number (back-compat) or an options object.
|
|
52
|
+
// `maxZoom` (default 16) stops a single / tightly-clustered set from
|
|
53
|
+
// zooming all the way in and losing surrounding context.
|
|
54
|
+
(paddingOrOpts: number | { padding?: number; maxZoom?: number } = 50) => {
|
|
52
55
|
if (markers.length === 0) return
|
|
53
56
|
|
|
57
|
+
const { padding = 50, maxZoom = 16 } =
|
|
58
|
+
typeof paddingOrOpts === 'number' ? { padding: paddingOrOpts } : paddingOrOpts
|
|
59
|
+
|
|
54
60
|
if (markers.length === 1) {
|
|
55
61
|
const marker = markers[0]
|
|
56
62
|
fitBounds(
|
|
@@ -58,7 +64,7 @@ export function useMarkers(): MarkerActions {
|
|
|
58
64
|
[marker.longitude - 0.01, marker.latitude - 0.01],
|
|
59
65
|
[marker.longitude + 0.01, marker.latitude + 0.01],
|
|
60
66
|
],
|
|
61
|
-
{ padding }
|
|
67
|
+
{ padding, maxZoom }
|
|
62
68
|
)
|
|
63
69
|
return
|
|
64
70
|
}
|
|
@@ -71,7 +77,7 @@ export function useMarkers(): MarkerActions {
|
|
|
71
77
|
[Math.max(...lngs), Math.max(...lats)],
|
|
72
78
|
]
|
|
73
79
|
|
|
74
|
-
fitBounds(bounds, { padding })
|
|
80
|
+
fitBounds(bounds, { padding, maxZoom })
|
|
75
81
|
},
|
|
76
82
|
[markers, fitBounds]
|
|
77
83
|
)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
+
import { length as turfLength, area as turfArea } from '@turf/turf'
|
|
5
|
+
|
|
6
|
+
import { useMapContext } from '../context'
|
|
7
|
+
import { createLineString, createPolygon, createFeatureCollection } from '../utils'
|
|
8
|
+
|
|
9
|
+
/** Measure interaction mode. */
|
|
10
|
+
export type MeasureMode = 'distance' | 'area'
|
|
11
|
+
|
|
12
|
+
export interface UseMeasureResult {
|
|
13
|
+
/** Current measure mode (`'distance'` = path length, `'area'` = polygon area). */
|
|
14
|
+
mode: MeasureMode
|
|
15
|
+
/** Switch the measure mode. Clears the in-progress vertices. */
|
|
16
|
+
setMode: (mode: MeasureMode) => void
|
|
17
|
+
/** Vertices collected so far, as `[lng, lat]` pairs in click order. */
|
|
18
|
+
points: [number, number][]
|
|
19
|
+
/**
|
|
20
|
+
* The collected geometry as a FeatureCollection ready for `MapSource`.
|
|
21
|
+
* A LineString in `'distance'` mode, a Polygon in `'area'` mode (empty
|
|
22
|
+
* until enough vertices exist to form the geometry).
|
|
23
|
+
*/
|
|
24
|
+
geojson: GeoJSON.FeatureCollection
|
|
25
|
+
/**
|
|
26
|
+
* The running total — kilometres in `'distance'` mode, square metres in
|
|
27
|
+
* `'area'` mode. `0` until at least two (distance) / three (area) points.
|
|
28
|
+
*/
|
|
29
|
+
total: number
|
|
30
|
+
/** Human-readable total, e.g. `"1.24 km"`, `"3,420 m²"`, `"0.34 km²"`. */
|
|
31
|
+
totalLabel: string
|
|
32
|
+
/** Remove all collected vertices. */
|
|
33
|
+
clear: () => void
|
|
34
|
+
/** Whether the measure interaction is listening for map clicks. */
|
|
35
|
+
active: boolean
|
|
36
|
+
/** Start / stop listening for map clicks. Stopping clears the vertices. */
|
|
37
|
+
setActive: (active: boolean) => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const numberFormat = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 })
|
|
41
|
+
|
|
42
|
+
/** Format a distance in kilometres into a compact `m`/`km` label. */
|
|
43
|
+
function formatDistance(km: number): string {
|
|
44
|
+
if (km <= 0) return '0 m'
|
|
45
|
+
if (km < 1) return `${numberFormat.format(km * 1000)} m`
|
|
46
|
+
return `${km.toFixed(2)} km`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Format an area in square metres into a compact `m²`/`km²` label. */
|
|
50
|
+
function formatArea(m2: number): string {
|
|
51
|
+
if (m2 <= 0) return '0 m²'
|
|
52
|
+
if (m2 < 1_000_000) return `${numberFormat.format(m2)} m²`
|
|
53
|
+
return `${(m2 / 1_000_000).toFixed(2)} km²`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Click-to-measure interaction for the Map tool.
|
|
58
|
+
*
|
|
59
|
+
* Subscribes to map clicks while `active` and appends each `[lng, lat]` to
|
|
60
|
+
* the vertex list, recomputing a running total with turf:
|
|
61
|
+
* - `'distance'` → a LineString, total via `turf.length` (km).
|
|
62
|
+
* - `'area'` → a Polygon, total via `turf.area` (m²).
|
|
63
|
+
*
|
|
64
|
+
* Geometry is built with the tool's own `createLineString` / `createPolygon`
|
|
65
|
+
* helpers so it matches house style and renders through the existing
|
|
66
|
+
* `createLineLayer` / `createPolygonLayer` factories.
|
|
67
|
+
*
|
|
68
|
+
* Must be used inside a `MapProvider` (it reads `useMapContext`).
|
|
69
|
+
*/
|
|
70
|
+
export function useMeasure(initialMode: MeasureMode = 'distance'): UseMeasureResult {
|
|
71
|
+
const { mapRef, isLoaded } = useMapContext()
|
|
72
|
+
|
|
73
|
+
const [mode, setModeState] = useState<MeasureMode>(initialMode)
|
|
74
|
+
const [active, setActiveState] = useState(false)
|
|
75
|
+
const [points, setPoints] = useState<[number, number][]>([])
|
|
76
|
+
|
|
77
|
+
// Latest mode/active read inside the (re-subscribed only on isLoaded) click
|
|
78
|
+
// handler, so changing mode/active never re-binds the underlying listener.
|
|
79
|
+
const modeRef = useRef(mode)
|
|
80
|
+
const activeRef = useRef(active)
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
modeRef.current = mode
|
|
83
|
+
}, [mode])
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
activeRef.current = active
|
|
86
|
+
}, [active])
|
|
87
|
+
|
|
88
|
+
const clear = useCallback(() => {
|
|
89
|
+
setPoints([])
|
|
90
|
+
}, [])
|
|
91
|
+
|
|
92
|
+
const setMode = useCallback((next: MeasureMode) => {
|
|
93
|
+
setModeState(next)
|
|
94
|
+
setPoints([])
|
|
95
|
+
}, [])
|
|
96
|
+
|
|
97
|
+
const setActive = useCallback((next: boolean) => {
|
|
98
|
+
setActiveState(next)
|
|
99
|
+
// Stopping the interaction discards the in-progress measurement.
|
|
100
|
+
if (!next) setPoints([])
|
|
101
|
+
}, [])
|
|
102
|
+
|
|
103
|
+
// Subscribe to map clicks. Listener is attached once per `isLoaded`; the
|
|
104
|
+
// handler reads the latest mode/active via refs (same pattern as
|
|
105
|
+
// `useMapEvents`).
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const map = mapRef.current?.getMap()
|
|
108
|
+
if (!map || !isLoaded) return
|
|
109
|
+
|
|
110
|
+
const handleClick = (event: maplibregl.MapMouseEvent) => {
|
|
111
|
+
if (!activeRef.current) return
|
|
112
|
+
const { lng, lat } = event.lngLat
|
|
113
|
+
setPoints((prev) => [...prev, [lng, lat]])
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
map.on('click', handleClick)
|
|
117
|
+
return () => {
|
|
118
|
+
map.off('click', handleClick)
|
|
119
|
+
}
|
|
120
|
+
}, [mapRef, isLoaded])
|
|
121
|
+
|
|
122
|
+
// Reflect the active state as a crosshair cursor on the map canvas.
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const map = mapRef.current?.getMap()
|
|
125
|
+
if (!map || !isLoaded) return
|
|
126
|
+
const canvas = map.getCanvas()
|
|
127
|
+
if (!canvas) return
|
|
128
|
+
const previous = canvas.style.cursor
|
|
129
|
+
if (active) canvas.style.cursor = 'crosshair'
|
|
130
|
+
return () => {
|
|
131
|
+
canvas.style.cursor = previous
|
|
132
|
+
}
|
|
133
|
+
}, [mapRef, isLoaded, active])
|
|
134
|
+
|
|
135
|
+
const geojson = useMemo<GeoJSON.FeatureCollection>(() => {
|
|
136
|
+
if (mode === 'distance') {
|
|
137
|
+
if (points.length < 2) return createFeatureCollection()
|
|
138
|
+
return createFeatureCollection([createLineString(points)])
|
|
139
|
+
}
|
|
140
|
+
if (points.length < 3) return createFeatureCollection()
|
|
141
|
+
return createFeatureCollection([createPolygon(points)])
|
|
142
|
+
}, [mode, points])
|
|
143
|
+
|
|
144
|
+
const total = useMemo<number>(() => {
|
|
145
|
+
if (mode === 'distance') {
|
|
146
|
+
if (points.length < 2) return 0
|
|
147
|
+
return turfLength(createLineString(points), { units: 'kilometers' })
|
|
148
|
+
}
|
|
149
|
+
if (points.length < 3) return 0
|
|
150
|
+
return turfArea(createPolygon(points))
|
|
151
|
+
}, [mode, points])
|
|
152
|
+
|
|
153
|
+
const totalLabel = useMemo<string>(
|
|
154
|
+
() => (mode === 'distance' ? formatDistance(total) : formatArea(total)),
|
|
155
|
+
[mode, total]
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
mode,
|
|
160
|
+
setMode,
|
|
161
|
+
points,
|
|
162
|
+
geojson,
|
|
163
|
+
total,
|
|
164
|
+
totalLabel,
|
|
165
|
+
clear,
|
|
166
|
+
active,
|
|
167
|
+
setActive,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react'
|
|
4
|
+
|
|
5
|
+
import { useMapContext } from '../context'
|
|
6
|
+
import { TERRAIN_DEM } from '../styles'
|
|
7
|
+
|
|
8
|
+
import type { RasterDEMSourceSpecification } from 'maplibre-gl'
|
|
9
|
+
|
|
10
|
+
/** Stable source / layer ids the hook owns. */
|
|
11
|
+
const DEM_SOURCE_ID = 'terrain-dem'
|
|
12
|
+
const HILLSHADE_LAYER_ID = 'terrain-hillshade'
|
|
13
|
+
|
|
14
|
+
/** Default vertical exaggeration — subtle, reads as 3D without caricature. */
|
|
15
|
+
const DEFAULT_EXAGGERATION = 1.2
|
|
16
|
+
/** Pitch eased in when terrain is enabled so it actually reads as 3D. */
|
|
17
|
+
const TERRAIN_PITCH = 60
|
|
18
|
+
|
|
19
|
+
export interface UseTerrainOptions {
|
|
20
|
+
/** Turn 3D terrain on/off. */
|
|
21
|
+
enabled?: boolean
|
|
22
|
+
/**
|
|
23
|
+
* `raster-dem` source spec to use. Defaults to the free AWS Terrarium DEM
|
|
24
|
+
* (`TERRAIN_DEM`) — no API key required.
|
|
25
|
+
*/
|
|
26
|
+
source?: RasterDEMSourceSpecification
|
|
27
|
+
/** Vertical exaggeration passed to `setTerrain`. @default 1.2 */
|
|
28
|
+
exaggeration?: number
|
|
29
|
+
/** Also add a `hillshade` layer from the same DEM. @default true */
|
|
30
|
+
hillshade?: boolean
|
|
31
|
+
/**
|
|
32
|
+
* Ease the camera pitch in (to ~60°) when enabling and back to 0 when
|
|
33
|
+
* disabling, so a flat map visibly becomes 3D. Opt out for hosts that
|
|
34
|
+
* drive pitch themselves. @default true
|
|
35
|
+
*/
|
|
36
|
+
autoPitch?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 3D terrain + hillshade from a free `raster-dem` source.
|
|
41
|
+
*
|
|
42
|
+
* When `enabled`, adds the DEM source (if absent), calls `setTerrain`, and
|
|
43
|
+
* optionally a `hillshade` layer. On disable/unmount it clears terrain and
|
|
44
|
+
* removes the source + layer cleanly. Re-applies on `styledata` because a
|
|
45
|
+
* style reload drops terrain. Terrain only reads as 3D with `pitch > 0`, so
|
|
46
|
+
* `autoPitch` eases the camera in/out (default on).
|
|
47
|
+
*
|
|
48
|
+
* Mirrors the map-access + add/remove + cleanup pattern of `useMapLayers`.
|
|
49
|
+
*/
|
|
50
|
+
export function useTerrain({
|
|
51
|
+
enabled = false,
|
|
52
|
+
source = TERRAIN_DEM,
|
|
53
|
+
exaggeration = DEFAULT_EXAGGERATION,
|
|
54
|
+
hillshade = true,
|
|
55
|
+
autoPitch = true,
|
|
56
|
+
}: UseTerrainOptions) {
|
|
57
|
+
const { mapRef, isLoaded } = useMapContext()
|
|
58
|
+
|
|
59
|
+
// Keep latest opts in refs so the styledata listener always reads current
|
|
60
|
+
// values without re-subscribing.
|
|
61
|
+
const optsRef = useRef({ source, exaggeration, hillshade })
|
|
62
|
+
optsRef.current = { source, exaggeration, hillshade }
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const map = mapRef.current?.getMap()
|
|
66
|
+
if (!map || !isLoaded) return
|
|
67
|
+
|
|
68
|
+
const apply = () => {
|
|
69
|
+
const m = mapRef.current?.getMap()
|
|
70
|
+
if (!m || !m.isStyleLoaded()) return
|
|
71
|
+
const { source: src, exaggeration: ex, hillshade: shade } = optsRef.current
|
|
72
|
+
|
|
73
|
+
if (!m.getSource(DEM_SOURCE_ID)) {
|
|
74
|
+
m.addSource(DEM_SOURCE_ID, src)
|
|
75
|
+
}
|
|
76
|
+
m.setTerrain({ source: DEM_SOURCE_ID, exaggeration: ex })
|
|
77
|
+
|
|
78
|
+
if (shade) {
|
|
79
|
+
if (!m.getLayer(HILLSHADE_LAYER_ID)) {
|
|
80
|
+
m.addLayer({
|
|
81
|
+
id: HILLSHADE_LAYER_ID,
|
|
82
|
+
type: 'hillshade',
|
|
83
|
+
source: DEM_SOURCE_ID,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
} else if (m.getLayer(HILLSHADE_LAYER_ID)) {
|
|
87
|
+
m.removeLayer(HILLSHADE_LAYER_ID)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const teardown = () => {
|
|
92
|
+
const m = mapRef.current?.getMap()
|
|
93
|
+
if (!m) return
|
|
94
|
+
try {
|
|
95
|
+
if (m.getTerrain()) m.setTerrain(null)
|
|
96
|
+
if (m.getLayer(HILLSHADE_LAYER_ID)) m.removeLayer(HILLSHADE_LAYER_ID)
|
|
97
|
+
if (m.getSource(DEM_SOURCE_ID)) m.removeSource(DEM_SOURCE_ID)
|
|
98
|
+
} catch {
|
|
99
|
+
// style may have been swapped out mid-teardown — ignore.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!enabled) {
|
|
104
|
+
teardown()
|
|
105
|
+
if (autoPitch && map.getPitch() > 0) {
|
|
106
|
+
map.easeTo({ pitch: 0, duration: 600 })
|
|
107
|
+
}
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
apply()
|
|
112
|
+
if (autoPitch && map.getPitch() === 0) {
|
|
113
|
+
map.easeTo({ pitch: TERRAIN_PITCH, duration: 800 })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// A style reload drops terrain/sources — re-apply when the style settles.
|
|
117
|
+
map.on('styledata', apply)
|
|
118
|
+
return () => {
|
|
119
|
+
map.off('styledata', apply)
|
|
120
|
+
teardown()
|
|
121
|
+
}
|
|
122
|
+
}, [mapRef, isLoaded, enabled, autoPitch])
|
|
123
|
+
}
|
|
@@ -28,9 +28,9 @@ export type {
|
|
|
28
28
|
PolygonLayerOptions,
|
|
29
29
|
LineLayerOptions,
|
|
30
30
|
RouteLayerOptions,
|
|
31
|
+
SymbolLayerOptions,
|
|
31
32
|
// Control props
|
|
32
33
|
DrawControlProps,
|
|
33
|
-
GeocoderControlProps,
|
|
34
34
|
CustomOverlayProps,
|
|
35
35
|
// Legend
|
|
36
36
|
LegendItem,
|
|
@@ -52,7 +52,32 @@ export {
|
|
|
52
52
|
useMapEvents,
|
|
53
53
|
useMapViewport,
|
|
54
54
|
useMapLayers,
|
|
55
|
+
useMapImages,
|
|
56
|
+
useMeasure,
|
|
57
|
+
useTerrain,
|
|
58
|
+
useCompass,
|
|
59
|
+
useGeolocation,
|
|
55
60
|
useControl,
|
|
61
|
+
useDraggableMarkers,
|
|
62
|
+
useGeocoder,
|
|
63
|
+
useMarkerCard,
|
|
64
|
+
type UseMarkerCardResult,
|
|
65
|
+
type UseCompassResult,
|
|
66
|
+
type UseGeocoderOptions,
|
|
67
|
+
type UseGeocoderResult,
|
|
68
|
+
type MapImage,
|
|
69
|
+
type UseMapImagesResult,
|
|
70
|
+
type UseMeasureResult,
|
|
71
|
+
type MeasureMode,
|
|
72
|
+
type UseTerrainOptions,
|
|
73
|
+
type GeolocationFix,
|
|
74
|
+
type GeolocationStatus,
|
|
75
|
+
type UseGeolocationOptions,
|
|
76
|
+
type UseGeolocationResult,
|
|
77
|
+
type DraggablePoint,
|
|
78
|
+
type UseDraggableMarkersOptions,
|
|
79
|
+
type UseDraggableMarkersResult,
|
|
80
|
+
type DraggableMarkerHandlers,
|
|
56
81
|
} from './hooks'
|
|
57
82
|
|
|
58
83
|
// Components
|
|
@@ -68,17 +93,45 @@ export {
|
|
|
68
93
|
CustomOverlay,
|
|
69
94
|
MapLegend,
|
|
70
95
|
LayerSwitcher,
|
|
96
|
+
MeasureControl,
|
|
97
|
+
DraggableMarkers,
|
|
98
|
+
GeocoderInput,
|
|
99
|
+
GeocoderControl,
|
|
100
|
+
CompassChip,
|
|
101
|
+
MarkerCard,
|
|
102
|
+
MapMarkers,
|
|
71
103
|
} from './components'
|
|
72
104
|
export type {
|
|
73
105
|
MapContainerProps,
|
|
106
|
+
GeolocateOptions,
|
|
107
|
+
GeolocatePosition,
|
|
74
108
|
MapControlsProps,
|
|
75
109
|
MapMarkerProps,
|
|
76
110
|
MapPopupProps,
|
|
77
111
|
MapClusterProps,
|
|
78
112
|
MapSourceProps,
|
|
79
113
|
MapLayerProps,
|
|
114
|
+
MeasureControlProps,
|
|
115
|
+
DraggableMarkersProps,
|
|
116
|
+
GeocoderInputProps,
|
|
117
|
+
GeocoderControlProps,
|
|
118
|
+
CompassChipProps,
|
|
119
|
+
MarkerCardProps,
|
|
120
|
+
MapMarkersProps,
|
|
121
|
+
MarkerWithCard,
|
|
122
|
+
MarkerCardAction,
|
|
123
|
+
MarkerCardData,
|
|
124
|
+
MarkerCardActionData,
|
|
80
125
|
} from './components'
|
|
81
126
|
|
|
127
|
+
// The card-data shape. `MarkerCard` (value) is the component above; the data
|
|
128
|
+
// type ships as `MarkerCardInfo` to avoid colliding with that value export.
|
|
129
|
+
export type { MarkerCard as MarkerCardInfo } from './cards'
|
|
130
|
+
|
|
131
|
+
// Geocoder resolver — pure, key-free (light; no maplibre runtime).
|
|
132
|
+
export { photonResolve, cameraForResult } from './geocode'
|
|
133
|
+
export type { GeocodeResult, GeocodeKind, ResolveGeocode } from './geocode'
|
|
134
|
+
|
|
82
135
|
// Layers
|
|
83
136
|
export {
|
|
84
137
|
createClusterLayers,
|
|
@@ -91,10 +144,14 @@ export {
|
|
|
91
144
|
createRouteLayers,
|
|
92
145
|
createDashedLineLayer,
|
|
93
146
|
createAnimatedLineLayer,
|
|
147
|
+
createSymbolLayer,
|
|
94
148
|
} from './layers'
|
|
95
149
|
|
|
96
150
|
// Styles
|
|
97
|
-
export { MAP_STYLES, getMapStyle } from './styles'
|
|
151
|
+
export { MAP_STYLES, getMapStyle, TERRAIN_DEM, TERRAIN_DEM_URL } from './styles'
|
|
152
|
+
|
|
153
|
+
// PMTiles protocol — opt-in self-hosted / offline basemaps (no tile server)
|
|
154
|
+
export { addPMTilesProtocol, pmtilesSourceUrl } from './protocols/pmtiles'
|
|
98
155
|
|
|
99
156
|
// Utils
|
|
100
157
|
export {
|