@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.
Files changed (171) hide show
  1. package/package.json +28 -14
  2. package/src/common/CodePanelHeader/CodePanelHeader.tsx +121 -0
  3. package/src/common/CodePanelHeader/index.ts +1 -0
  4. package/src/common/blocks/BlockError.tsx +46 -0
  5. package/src/common/blocks/index.ts +34 -0
  6. package/src/common/blocks/schemas.ts +143 -0
  7. package/src/common/blocks/types.ts +76 -0
  8. package/src/common/chips/README.md +67 -0
  9. package/src/common/chips/UrlChip.tsx +105 -0
  10. package/src/common/chips/index.ts +20 -0
  11. package/src/common/link-preview/LinkPreviewCard.tsx +178 -0
  12. package/src/common/link-preview/index.ts +12 -0
  13. package/src/common/link-preview/types.ts +44 -0
  14. package/src/common/tiptap/index.ts +10 -0
  15. package/src/common/tiptap/useNodeAttrs.ts +128 -0
  16. package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +1 -1
  17. package/src/tools/chat/README.md +88 -2
  18. package/src/tools/chat/composer/Composer.tsx +45 -12
  19. package/src/tools/chat/composer/ComposerRichTextarea.tsx +16 -4
  20. package/src/tools/chat/constants.ts +9 -2
  21. package/src/tools/chat/core/extractFirstUrl.ts +45 -0
  22. package/src/tools/chat/core/reducer.ts +32 -8
  23. package/src/tools/chat/core/transport/mappers/pydantic-ai.ts +1 -1
  24. package/src/tools/chat/core/transport/mock.ts +4 -1
  25. package/src/tools/chat/hooks/useChat.ts +20 -20
  26. package/src/tools/chat/lazy.tsx +10 -0
  27. package/src/tools/chat/messages/Attachments.tsx +267 -42
  28. package/src/tools/chat/messages/BubbleContextMenu.tsx +203 -0
  29. package/src/tools/chat/messages/MessageBubble.tsx +175 -31
  30. package/src/tools/chat/messages/MessageList.tsx +107 -21
  31. package/src/tools/chat/messages/ToolCalls.tsx +28 -13
  32. package/src/tools/chat/messages/blocks/MessageBlocks.tsx +70 -17
  33. package/src/tools/chat/messages/blocks/builtin.tsx +6 -3
  34. package/src/tools/chat/messages/blocks/renderers/AudioBlock.tsx +6 -0
  35. package/src/tools/chat/messages/blocks/renderers/ChatImageAlbum.tsx +154 -0
  36. package/src/tools/chat/messages/blocks/renderers/CodeBlock.tsx +94 -6
  37. package/src/tools/chat/messages/blocks/renderers/DiffBlock.tsx +29 -0
  38. package/src/tools/chat/messages/blocks/renderers/GalleryBlock.tsx +17 -10
  39. package/src/tools/chat/messages/blocks/renderers/ImageBlock.tsx +37 -16
  40. package/src/tools/chat/messages/blocks/renderers/JsonBlock.tsx +11 -6
  41. package/src/tools/chat/messages/blocks/renderers/LinkBlock.tsx +32 -0
  42. package/src/tools/chat/messages/blocks/renderers/MapBlock.tsx +181 -19
  43. package/src/tools/chat/messages/blocks/renderers/VideoBlock.tsx +2 -5
  44. package/src/tools/chat/messages/index.ts +13 -0
  45. package/src/tools/chat/public.ts +5 -1
  46. package/src/tools/chat/shell/ChatRoot.tsx +29 -0
  47. package/src/tools/chat/styles/bubbleTokens.ts +18 -0
  48. package/src/tools/chat/styles/index.ts +1 -0
  49. package/src/tools/chat/types/attachment.ts +26 -2
  50. package/src/tools/chat/types/block.ts +56 -9
  51. package/src/tools/chat/types/blockSchemas.ts +197 -0
  52. package/src/tools/chat/types/config.ts +33 -0
  53. package/src/tools/chat/types/events.ts +14 -7
  54. package/src/tools/chat/types/index.ts +4 -2
  55. package/src/tools/chat/types/message.ts +7 -0
  56. package/src/tools/data/JsonTree/JsonViewer.tsx +7 -7
  57. package/src/tools/dev/Map/README.md +30 -345
  58. package/src/tools/dev/Map/cards/index.ts +6 -0
  59. package/src/tools/dev/Map/cards/types.ts +67 -0
  60. package/src/tools/dev/Map/components/CompassChip.tsx +67 -0
  61. package/src/tools/dev/Map/components/DraggableMarkers.tsx +79 -0
  62. package/src/tools/dev/Map/components/GeocoderControl.tsx +148 -63
  63. package/src/tools/dev/Map/components/GeocoderInput.tsx +181 -0
  64. package/src/tools/dev/Map/components/MapContainer/FullscreenDialog.tsx +140 -0
  65. package/src/tools/dev/Map/components/MapContainer/GeolocateHandle.tsx +70 -0
  66. package/src/tools/dev/Map/components/MapContainer/MapInner.tsx +326 -0
  67. package/src/tools/dev/Map/components/MapContainer/MapToolbar.tsx +286 -0
  68. package/src/tools/dev/Map/components/MapContainer/RotateHint.tsx +25 -0
  69. package/src/tools/dev/Map/components/MapContainer/ScrollLockOverlay.tsx +52 -0
  70. package/src/tools/dev/Map/components/MapContainer/constants.ts +15 -0
  71. package/src/tools/dev/Map/components/MapContainer/hooks/useAutoReset.ts +51 -0
  72. package/src/tools/dev/Map/components/MapContainer/hooks/useBasemapState.ts +48 -0
  73. package/src/tools/dev/Map/components/MapContainer/hooks/useExternalMapsUrl.ts +29 -0
  74. package/src/tools/dev/Map/components/MapContainer/index.tsx +29 -0
  75. package/src/tools/dev/Map/components/MapContainer/types.ts +198 -0
  76. package/src/tools/dev/Map/components/MapControls.tsx +27 -0
  77. package/src/tools/dev/Map/components/MapMarkers.tsx +94 -0
  78. package/src/tools/dev/Map/components/MarkerCard.tsx +102 -0
  79. package/src/tools/dev/Map/components/MeasureControl.tsx +219 -0
  80. package/src/tools/dev/Map/components/index.ts +35 -1
  81. package/src/tools/dev/Map/geocode/index.ts +2 -0
  82. package/src/tools/dev/Map/geocode/photon.ts +265 -0
  83. package/src/tools/dev/Map/hooks/index.ts +37 -0
  84. package/src/tools/dev/Map/hooks/useCompass.ts +44 -0
  85. package/src/tools/dev/Map/hooks/useDraggableMarkers.ts +112 -0
  86. package/src/tools/dev/Map/hooks/useGeocoder.ts +155 -0
  87. package/src/tools/dev/Map/hooks/useGeolocation.ts +174 -0
  88. package/src/tools/dev/Map/hooks/useMapControl.ts +5 -1
  89. package/src/tools/dev/Map/hooks/useMapImages.ts +112 -0
  90. package/src/tools/dev/Map/hooks/useMapScrollProtection.ts +155 -0
  91. package/src/tools/dev/Map/hooks/useMapTouchMode.ts +21 -0
  92. package/src/tools/dev/Map/hooks/useMarkerCard.ts +39 -0
  93. package/src/tools/dev/Map/hooks/useMarkers.ts +9 -3
  94. package/src/tools/dev/Map/hooks/useMeasure.ts +169 -0
  95. package/src/tools/dev/Map/hooks/useTerrain.ts +123 -0
  96. package/src/tools/dev/Map/index.ts +59 -2
  97. package/src/tools/dev/Map/layers/index.ts +1 -0
  98. package/src/tools/dev/Map/layers/symbol.ts +93 -0
  99. package/src/tools/dev/Map/lazy.tsx +120 -0
  100. package/src/tools/dev/Map/protocols/pmtiles.ts +73 -0
  101. package/src/tools/dev/Map/styles/index.ts +62 -0
  102. package/src/tools/dev/Map/styles/terrain.ts +29 -0
  103. package/src/tools/dev/Map/types.ts +31 -3
  104. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +1 -1
  105. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +5 -12
  106. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +1 -1
  107. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +1 -1
  108. package/src/tools/dev/api/OpenapiViewer/components/shared/SendButton.tsx +1 -1
  109. package/src/tools/dev/api/OpenapiViewer/components/shared/http-colors.ts +61 -0
  110. package/src/tools/dev/api/OpenapiViewer/components/shared/ui.tsx +4 -21
  111. package/src/tools/dev/code/MarkdownMessage/MarkdownMessage.tsx +150 -58
  112. package/src/tools/dev/code/MarkdownMessage/README.md +38 -0
  113. package/src/tools/dev/code/MarkdownMessage/builtinRules.tsx +92 -0
  114. package/src/tools/dev/code/MarkdownMessage/index.ts +3 -0
  115. package/src/tools/dev/code/MarkdownMessage/streamingBlocks.ts +164 -0
  116. package/src/tools/dev/code/MarkdownMessage/types.ts +20 -0
  117. package/src/tools/dev/code/PrettyCode/PrettyCode.client.tsx +27 -30
  118. package/src/tools/dev/code/PrettyCode/index.tsx +4 -1
  119. package/src/tools/dev/code/PrettyCode/lazy.tsx +3 -1
  120. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +168 -3
  121. package/src/tools/forms/MarkdownEditor/README.md +84 -0
  122. package/src/tools/forms/MarkdownEditor/chip/ChipNode.ts +454 -0
  123. package/src/tools/forms/MarkdownEditor/chip/syncChips.ts +80 -0
  124. package/src/tools/forms/MarkdownEditor/filePath/FilePathChip.tsx +68 -0
  125. package/src/tools/forms/MarkdownEditor/filePath/__tests__/detect.test.ts +161 -0
  126. package/src/tools/forms/MarkdownEditor/filePath/detect.ts +289 -0
  127. package/src/tools/forms/MarkdownEditor/filePath/index.ts +15 -0
  128. package/src/tools/forms/MarkdownEditor/index.ts +19 -0
  129. package/src/tools/forms/MarkdownEditor/lazy.tsx +10 -0
  130. package/src/tools/forms/MarkdownEditor/styles.css +134 -2
  131. package/src/tools/forms/MarkdownEditor/url/__tests__/detect.test.ts +121 -0
  132. package/src/tools/forms/MarkdownEditor/url/detect.ts +227 -0
  133. package/src/tools/forms/NotionEditor/BookmarkNode.ts +158 -0
  134. package/src/tools/forms/NotionEditor/BookmarkView.tsx +45 -0
  135. package/src/tools/forms/NotionEditor/MapEditLayer.tsx +199 -0
  136. package/src/tools/forms/NotionEditor/MapNode.ts +218 -0
  137. package/src/tools/forms/NotionEditor/MapView.tsx +103 -0
  138. package/src/tools/forms/NotionEditor/NotionEditor.tsx +7 -2
  139. package/src/tools/forms/NotionEditor/README.md +109 -188
  140. package/src/tools/forms/NotionEditor/extensions.ts +15 -0
  141. package/src/tools/forms/NotionEditor/slashItems.ts +19 -0
  142. package/src/tools/forms/NotionEditor/styles.css +25 -0
  143. package/src/tools/forms/NotionEditor/types.ts +9 -0
  144. package/src/tools/input/SpeechRecognition/core/engine/external.ts +5 -0
  145. package/src/tools/input/SpeechRecognition/types.ts +10 -0
  146. package/src/tools/media/Gallery/components/lightbox/GalleryLightbox.tsx +39 -22
  147. package/src/tools/media/ImageViewer/README.md +9 -0
  148. package/src/tools/media/ImageViewer/components/ImageViewer.tsx +165 -79
  149. package/src/tools/media/ImageViewer/components/LightboxChrome.tsx +137 -0
  150. package/src/tools/media/ImageViewer/components/MiddleEllipsis.tsx +46 -0
  151. package/src/tools/media/ImageViewer/components/index.ts +2 -0
  152. package/src/tools/media/ImageViewer/hooks/index.ts +3 -0
  153. package/src/tools/media/ImageViewer/hooks/useIdleChrome.ts +73 -0
  154. package/src/tools/media/ImageViewer/types.ts +10 -0
  155. package/src/tools/media/ImageViewer/utils/constants.ts +28 -0
  156. package/src/tools/media/ImageViewer/utils/index.ts +4 -0
  157. package/src/tools/media/VideoPlayer/VideoPlayer.tsx +34 -40
  158. package/src/tools/media/VideoPlayer/canvas/youtube-canvas.tsx +13 -13
  159. package/src/tools/media/VideoPlayer/parts/center-play.tsx +26 -0
  160. package/src/tools/media/VideoPlayer/parts/index.ts +2 -0
  161. package/src/tools/media/VideoPlayer/styles/video-player.css +102 -53
  162. package/src/tools/visual/charts/SmoothLine/README.md +97 -0
  163. package/src/tools/visual/charts/SmoothLine/SmoothLine.tsx +177 -0
  164. package/src/tools/visual/charts/SmoothLine/index.ts +21 -0
  165. package/src/tools/visual/charts/SmoothLine/lazy.tsx +23 -0
  166. package/src/tools/visual/charts/SmoothLine/smoothPath.ts +209 -0
  167. package/src/tools/visual/charts/SmoothLine/types.ts +98 -0
  168. package/src/tools/visual/charts/SmoothLine/useSmoothSeries.ts +429 -0
  169. package/src/tools/visual/indicators/Fps/types.ts +1 -1
  170. package/src/tools/chat/messages/blocks/renderers/LottieBlock.tsx +0 -11
  171. package/src/tools/dev/Map/components/MapContainer.tsx +0 -198
@@ -1,70 +1,155 @@
1
1
  'use client'
2
2
 
3
3
  /**
4
- * GeocoderControl - Address search with autocomplete
4
+ * GeocoderControl — the thin map-bound wrapper around `GeocoderInput`.
5
5
  *
6
- * Requires: @maplibre/maplibre-gl-geocoder
7
- * CSS: @import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css'
6
+ * Renders the autocomplete as an on-map control (a positioned overlay, top-
7
+ * left by default). On select it flies the camera to the picked coordinate
8
+ * via `useMapControl().flyTo`, optionally drops a marker, and calls an
9
+ * optional `onResult`. Biases search to the current map center (`near`).
8
10
  *
9
- * @example
10
- * ```tsx
11
- * import { useState } from 'react'
12
- * import { useControl, Marker } from '@carapis/map'
13
- * import MaplibreGeocoder from '@maplibre/maplibre-gl-geocoder'
14
- *
15
- * const geocoderApi = {
16
- * forwardGeocode: async (config) => {
17
- * const features = []
18
- * const response = await fetch(
19
- * `https://nominatim.openstreetmap.org/search?q=${config.query}&format=geojson`
20
- * )
21
- * const geojson = await response.json()
22
- * for (const feature of geojson.features) {
23
- * const center = [
24
- * feature.bbox[0] + (feature.bbox[2] - feature.bbox[0]) / 2,
25
- * feature.bbox[1] + (feature.bbox[3] - feature.bbox[1]) / 2,
26
- * ]
27
- * features.push({
28
- * type: 'Feature',
29
- * geometry: { type: 'Point', coordinates: center },
30
- * place_name: feature.properties.display_name,
31
- * properties: feature.properties,
32
- * text: feature.properties.display_name,
33
- * center,
34
- * })
35
- * }
36
- * return { features }
37
- * },
38
- * }
39
- *
40
- * function GeocoderControl({ position, onResult }) {
41
- * const [marker, setMarker] = useState(null)
42
- *
43
- * useControl(
44
- * ({ mapLib }) => {
45
- * const ctrl = new MaplibreGeocoder(geocoderApi, {
46
- * maplibregl: mapLib,
47
- * marker: false,
48
- * })
49
- * ctrl.on('result', (evt) => {
50
- * onResult?.(evt)
51
- * const { result } = evt
52
- * if (result?.center) {
53
- * setMarker(
54
- * <Marker longitude={result.center[0]} latitude={result.center[1]} />
55
- * )
56
- * }
57
- * })
58
- * return ctrl
59
- * },
60
- * { position }
61
- * )
62
- *
63
- * return marker
64
- * }
65
- * ```
11
+ * Must live inside a `<MapProvider>` (i.e. rendered as a child of
12
+ * `MapContainer`). The `data-map-control` attribute is load-bearing: the
13
+ * scroll-protection re-lock skips clicks inside it so the input stays usable.
66
14
  */
67
15
 
68
- // Re-export for documentation purposes
69
- // Actual implementation requires user to install @maplibre/maplibre-gl-geocoder
70
- export { }
16
+ import { useCallback, useState } from 'react'
17
+
18
+ import { cn } from '@djangocfg/ui-core/lib'
19
+
20
+ import { useMapControl } from '../hooks/useMapControl'
21
+ import { useMapViewport } from '../hooks/useMapViewport'
22
+ import { GeocoderInput } from './GeocoderInput'
23
+ import { MapMarker } from './MapMarker'
24
+ import { cameraForResult } from '../geocode'
25
+ import type { GeocodeResult, ResolveGeocode } from '../geocode'
26
+ import type { MarkerData } from '../types'
27
+
28
+ type ControlCorner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
29
+
30
+ const cornerClass: Record<ControlCorner, string> = {
31
+ 'top-left': 'top-3 left-3',
32
+ 'top-right': 'top-3 right-3',
33
+ 'bottom-left': 'bottom-3 left-3',
34
+ 'bottom-right': 'bottom-3 right-3',
35
+ }
36
+
37
+ export interface GeocoderControlProps {
38
+ /** Injectable resolver. Defaults to public Photon (`photonResolve`). */
39
+ resolve?: ResolveGeocode
40
+ /** On-map corner for the search box. Default `top-left`. */
41
+ position?: ControlCorner
42
+ /** Input placeholder. */
43
+ placeholder?: string
44
+ /**
45
+ * Override the smart zoom. By default the camera fits the result's extent
46
+ * (`bbox`) when available, else flies to a zoom chosen by the result kind
47
+ * (country/city/street/house…). Set this to force a fixed fly-to zoom for
48
+ * every pick instead.
49
+ */
50
+ zoom?: number
51
+ /** Padding (px) around a fitted bounding box. Default 48. */
52
+ fitPadding?: number
53
+ /** Cap the zoom when fitting tiny extents (avoid over-zoom). Default 16. */
54
+ fitMaxZoom?: number
55
+ /** Drop a pin at the picked location. Default `true`. */
56
+ marker?: boolean
57
+ /** Called with the chosen result after the camera flies. */
58
+ onResult?: (result: GeocodeResult) => void
59
+ /** Max results. Default 5. */
60
+ limit?: number
61
+ /**
62
+ * Controlled visibility of the search input. When omitted the control is
63
+ * uncontrolled and the input is always visible (the standalone, always-on
64
+ * usage). Pass `open` (with `onClose`) to drive it from a toolbar toggle.
65
+ */
66
+ open?: boolean
67
+ /** Called when the input requests to close (Esc / select). Controlled mode. */
68
+ onClose?: () => void
69
+ /** Focus the input when it appears. Default `false`. */
70
+ autoFocus?: boolean
71
+ className?: string
72
+ }
73
+
74
+ export function GeocoderControl({
75
+ resolve,
76
+ position = 'top-left',
77
+ placeholder,
78
+ zoom,
79
+ fitPadding = 48,
80
+ fitMaxZoom = 16,
81
+ marker = true,
82
+ onResult,
83
+ limit,
84
+ open,
85
+ onClose,
86
+ autoFocus = false,
87
+ className,
88
+ }: GeocoderControlProps) {
89
+ const { flyTo, fitBounds } = useMapControl()
90
+ const { center } = useMapViewport()
91
+ const [pin, setPin] = useState<MarkerData | null>(null)
92
+
93
+ // Uncontrolled (no `open` prop) → always visible, like the original
94
+ // always-on control. Controlled → the host (toolbar chip) owns visibility.
95
+ const isControlled = open !== undefined
96
+ const inputVisible = isControlled ? open : true
97
+
98
+ const handleSelect = useCallback(
99
+ (result: GeocodeResult) => {
100
+ // Smart zoom: a fixed `zoom` override wins; otherwise fit the result's
101
+ // extent (island, street, district frame themselves) and fall back to a
102
+ // kind-based zoom for point results with no bbox.
103
+ const cam = cameraForResult(result)
104
+ if (zoom !== undefined) {
105
+ flyTo([result.lng, result.lat], zoom)
106
+ } else if (cam.type === 'fit') {
107
+ fitBounds(cam.bbox, { padding: fitPadding, maxZoom: fitMaxZoom })
108
+ } else {
109
+ flyTo(cam.center, cam.zoom)
110
+ }
111
+ if (marker) {
112
+ setPin({
113
+ id: `geocoder-${result.id}`,
114
+ longitude: result.lng,
115
+ latitude: result.lat,
116
+ data: { label: result.label },
117
+ })
118
+ }
119
+ onResult?.(result)
120
+ // Picking a result dismisses the input in controlled (toolbar) mode.
121
+ onClose?.()
122
+ },
123
+ [flyTo, fitBounds, zoom, fitPadding, fitMaxZoom, marker, onResult, onClose],
124
+ )
125
+
126
+ return (
127
+ <>
128
+ {inputVisible ? (
129
+ <div
130
+ data-map-control
131
+ className={cn('absolute z-20 w-72 max-w-[calc(100%-1.5rem)]', cornerClass[position], className)}
132
+ // Esc closes in controlled mode (the input also stops propagation
133
+ // of its own Escape, but the wrapper catch covers the case where
134
+ // focus has left the cmdk input).
135
+ onKeyDown={(e) => {
136
+ if (e.key === 'Escape' && isControlled) {
137
+ e.stopPropagation()
138
+ onClose?.()
139
+ }
140
+ }}
141
+ >
142
+ <GeocoderInput
143
+ resolve={resolve}
144
+ onSelect={handleSelect}
145
+ placeholder={placeholder}
146
+ near={{ lng: center[0], lat: center[1] }}
147
+ limit={limit}
148
+ autoFocus={autoFocus}
149
+ />
150
+ </div>
151
+ ) : null}
152
+ {pin ? <MapMarker marker={pin} ariaLabel={String(pin.data?.label ?? 'Result')} /> : null}
153
+ </>
154
+ )
155
+ }
@@ -0,0 +1,181 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * GeocoderInput — a self-contained, map-agnostic address/place autocomplete.
5
+ *
6
+ * A text field + a results dropdown (primary `label` + muted `secondary`),
7
+ * with loading / empty states and keyboard nav (↑/↓ move, Enter select, Esc
8
+ * close). Resolution is INJECTABLE: pass a `resolve` (default = public
9
+ * Photon) — the component never hard-couples to a backend.
10
+ *
11
+ * Built on ui-core's `Command` primitives (cmdk). cmdk's built-in fuzzy
12
+ * filter is DISABLED (`shouldFilter={false}`) because results already come
13
+ * pre-ranked from the resolver — we drive selection state manually so it
14
+ * works as a live remote-search box.
15
+ *
16
+ * Usable WITHOUT a map (plain forms). `GeocoderControl` is the thin map-bound
17
+ * wrapper that flies the camera on select.
18
+ */
19
+
20
+ import { useCallback, useEffect, useRef, useState } from 'react'
21
+
22
+ import { cn } from '@djangocfg/ui-core/lib'
23
+ import {
24
+ Command,
25
+ CommandEmpty,
26
+ CommandInput,
27
+ CommandItem,
28
+ CommandList,
29
+ } from '@djangocfg/ui-core/components'
30
+
31
+ import { useGeocoder } from '../hooks/useGeocoder'
32
+ import type { GeocodeResult, ResolveGeocode } from '../geocode'
33
+
34
+ export interface GeocoderInputProps {
35
+ /** Injectable resolver. Defaults to public Photon (`photonResolve`). */
36
+ resolve?: ResolveGeocode
37
+ /** Called with the chosen result when the user picks one. */
38
+ onSelect?: (result: GeocodeResult) => void
39
+ /** Input placeholder. */
40
+ placeholder?: string
41
+ /** Geo-bias — rank results near this point (e.g. the map center). */
42
+ near?: { lng: number; lat: number }
43
+ /** Minimum query length before searching. Default 3. */
44
+ minLength?: number
45
+ /** Max results. Default 5. */
46
+ limit?: number
47
+ /**
48
+ * Focus the text field on mount (e.g. when revealed by a toolbar toggle).
49
+ * @default false
50
+ */
51
+ autoFocus?: boolean
52
+ className?: string
53
+ }
54
+
55
+ export function GeocoderInput({
56
+ resolve,
57
+ onSelect,
58
+ placeholder = 'Search address or place…',
59
+ near,
60
+ minLength = 3,
61
+ limit,
62
+ autoFocus = false,
63
+ className,
64
+ }: GeocoderInputProps) {
65
+ const { query, setQuery, results, loading, error, select } = useGeocoder({
66
+ resolve,
67
+ near,
68
+ minLength,
69
+ limit,
70
+ })
71
+
72
+ // Show the dropdown only while focused with a meaningful query.
73
+ const [open, setOpen] = useState(false)
74
+
75
+ // Optional autofocus when revealed (e.g. by the toolbar search chip).
76
+ // cmdk's `CommandInput` doesn't forward a ref reliably across versions, so
77
+ // query the rendered `[cmdk-input]` element from the Command root instead.
78
+ const rootRef = useRef<HTMLDivElement>(null)
79
+ useEffect(() => {
80
+ if (!autoFocus) return
81
+ const input = rootRef.current?.querySelector<HTMLInputElement>('[cmdk-input]')
82
+ input?.focus()
83
+ }, [autoFocus])
84
+
85
+ const handleSelect = useCallback(
86
+ (result: GeocodeResult) => {
87
+ onSelect?.(select(result))
88
+ setOpen(false)
89
+ },
90
+ [onSelect, select],
91
+ )
92
+
93
+ const showList = open && query.trim().length >= minLength
94
+
95
+ return (
96
+ // The Command wrapper is a TRANSPARENT positioning context only — the
97
+ // popover chrome (bg/border/shadow) lives on the input and on the
98
+ // floating list separately, so nothing peeks out beneath the input when
99
+ // the list is closed.
100
+ <Command
101
+ ref={rootRef}
102
+ shouldFilter={false}
103
+ // Esc closes the dropdown without clearing; cmdk handles ↑/↓/Enter.
104
+ onKeyDown={(e) => {
105
+ if (e.key === 'Escape') {
106
+ e.preventDefault()
107
+ setOpen(false)
108
+ }
109
+ }}
110
+ className={cn(
111
+ 'relative overflow-visible bg-transparent',
112
+ // Turn cmdk's input WRAPPER into the search field itself: a rounded
113
+ // popover surface with its own border (drop cmdk's default bottom
114
+ // border so no line/stub shows beneath the input when closed).
115
+ '[&_[cmdk-input-wrapper]]:h-10 [&_[cmdk-input-wrapper]]:rounded-lg',
116
+ '[&_[cmdk-input-wrapper]]:border [&_[cmdk-input-wrapper]]:border-border',
117
+ '[&_[cmdk-input-wrapper]]:bg-popover [&_[cmdk-input-wrapper]]:text-popover-foreground',
118
+ '[&_[cmdk-input-wrapper]]:shadow-sm',
119
+ className,
120
+ )}
121
+ >
122
+ <CommandInput
123
+ value={query}
124
+ onValueChange={(v) => {
125
+ setQuery(v)
126
+ setOpen(true)
127
+ }}
128
+ onFocus={() => setOpen(true)}
129
+ onBlur={() => {
130
+ // Delay so an item click registers before the list unmounts.
131
+ window.setTimeout(() => setOpen(false), 120)
132
+ }}
133
+ placeholder={placeholder}
134
+ aria-label={placeholder}
135
+ />
136
+
137
+ {showList ? (
138
+ // Floating result panel — its OWN popover surface, anchored under the
139
+ // input, shown only when there's a query (never an empty stub).
140
+ <CommandList className="absolute inset-x-0 top-full z-10 mt-1.5 max-h-72 overflow-y-auto rounded-lg border border-border bg-popover text-popover-foreground shadow-md">
141
+ {loading ? (
142
+ <div
143
+ className="px-3 py-6 text-center text-sm text-muted-foreground"
144
+ aria-busy="true"
145
+ >
146
+ Searching…
147
+ </div>
148
+ ) : error ? (
149
+ <div className="px-3 py-6 text-center text-sm text-destructive">
150
+ Search failed. Try again.
151
+ </div>
152
+ ) : (
153
+ <>
154
+ <CommandEmpty className="py-6 text-center text-sm text-muted-foreground">
155
+ No results.
156
+ </CommandEmpty>
157
+ {results.map((result) => (
158
+ <CommandItem
159
+ key={result.id}
160
+ // cmdk matches `value`; keep it unique + searchable-ish.
161
+ value={`${result.id} ${result.label}`}
162
+ onSelect={() => handleSelect(result)}
163
+ className="flex-col items-start gap-0.5"
164
+ >
165
+ <span className="truncate font-medium text-foreground">
166
+ {result.label}
167
+ </span>
168
+ {result.secondary ? (
169
+ <span className="truncate text-xs text-muted-foreground">
170
+ {result.secondary}
171
+ </span>
172
+ ) : null}
173
+ </CommandItem>
174
+ ))}
175
+ </>
176
+ )}
177
+ </CommandList>
178
+ ) : null}
179
+ </Command>
180
+ )
181
+ }
@@ -0,0 +1,140 @@
1
+ 'use client'
2
+
3
+ import type { ComponentType, ReactNode } from 'react'
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogTitle,
8
+ } from '@djangocfg/ui-core/components'
9
+
10
+ import type { MapViewport, MapStyleKey } from '../../types'
11
+ import type { GeocodeResult, ResolveGeocode } from '../../geocode'
12
+ import type { MapContainerProps, GeolocateOptions, GeolocatePosition } from './types'
13
+
14
+ /**
15
+ * Fullscreen dialog — a fresh map seeded with the current viewport.
16
+ *
17
+ * The nested map is the public `MapContainer`, passed in as a prop so this
18
+ * module never imports the folder index (which would form a cycle:
19
+ * index → MapInner → FullscreenDialog → index).
20
+ */
21
+ export function FullscreenDialog({
22
+ MapContainer,
23
+ open,
24
+ onOpenChange,
25
+ fullscreenTitle,
26
+ viewport,
27
+ effectiveStyle,
28
+ interactiveLayerIds,
29
+ openInMapsUrl,
30
+ openInMapsLabel,
31
+ googleMapsButton,
32
+ basemapSwitcher,
33
+ basemaps,
34
+ onBasemapChange,
35
+ terrainOn,
36
+ terrainButton,
37
+ terrainLabel,
38
+ onTerrainChange,
39
+ compassButton,
40
+ compassLabel,
41
+ geolocate,
42
+ geolocateLabel,
43
+ geolocateOptions,
44
+ onGeolocate,
45
+ onGeolocateError,
46
+ geocoder,
47
+ geocoderResolve,
48
+ geocoderPlaceholder,
49
+ onGeocoderResult,
50
+ showResetButton,
51
+ ariaLabel,
52
+ onClose,
53
+ children,
54
+ }: {
55
+ MapContainer: ComponentType<MapContainerProps>
56
+ open: boolean
57
+ onOpenChange: (open: boolean) => void
58
+ fullscreenTitle: string
59
+ viewport: MapViewport
60
+ effectiveStyle: MapStyleKey | string
61
+ interactiveLayerIds: string[] | undefined
62
+ openInMapsUrl: string | undefined
63
+ openInMapsLabel: string
64
+ googleMapsButton: boolean
65
+ basemapSwitcher: boolean
66
+ basemaps: MapStyleKey[] | undefined
67
+ onBasemapChange: ((key: MapStyleKey) => void) | undefined
68
+ terrainOn: boolean
69
+ terrainButton: boolean
70
+ terrainLabel: string
71
+ onTerrainChange: ((enabled: boolean) => void) | undefined
72
+ compassButton: boolean
73
+ compassLabel: string
74
+ geolocate: boolean
75
+ geolocateLabel: string
76
+ geolocateOptions: GeolocateOptions | undefined
77
+ onGeolocate: ((pos: GeolocatePosition) => void) | undefined
78
+ onGeolocateError: ((err: GeolocationPositionError) => void) | undefined
79
+ geocoder: boolean
80
+ geocoderResolve: ResolveGeocode | undefined
81
+ geocoderPlaceholder: string | undefined
82
+ onGeocoderResult: ((result: GeocodeResult) => void) | undefined
83
+ showResetButton: boolean
84
+ ariaLabel: string
85
+ onClose: () => void
86
+ children?: ReactNode
87
+ }) {
88
+ return (
89
+ <Dialog open={open} onOpenChange={onOpenChange}>
90
+ <DialogContent
91
+ className="h-[90vh] w-[90vw] max-w-[1400px] overflow-hidden p-0 sm:rounded-xl"
92
+ // The dialog's own X is suppressed — the map renders close as the
93
+ // first chip in its top-right toolbar (via `onClose`), so close /
94
+ // Google / reset share one tidy column like the inline map.
95
+ closeButton={false}
96
+ >
97
+ <DialogTitle className="sr-only">{fullscreenTitle}</DialogTitle>
98
+ {open && (
99
+ <MapContainer
100
+ initialViewport={viewport}
101
+ // Carry the user's current basemap choice into fullscreen.
102
+ mapStyle={effectiveStyle}
103
+ interactiveLayerIds={interactiveLayerIds}
104
+ attributionControl={false}
105
+ reuseMaps={false}
106
+ openInMapsUrl={openInMapsUrl}
107
+ openInMapsLabel={openInMapsLabel}
108
+ googleMapsButton={googleMapsButton}
109
+ basemapSwitcher={basemapSwitcher}
110
+ basemaps={basemaps}
111
+ onBasemapChange={onBasemapChange}
112
+ terrain={terrainOn}
113
+ terrainButton={terrainButton}
114
+ terrainLabel={terrainLabel}
115
+ onTerrainChange={onTerrainChange}
116
+ compassButton={compassButton}
117
+ compassLabel={compassLabel}
118
+ geolocate={geolocate}
119
+ geolocateLabel={geolocateLabel}
120
+ geolocateOptions={geolocateOptions}
121
+ onGeolocate={onGeolocate}
122
+ onGeolocateError={onGeolocateError}
123
+ geocoder={geocoder}
124
+ geocoderResolve={geocoderResolve}
125
+ geocoderPlaceholder={geocoderPlaceholder}
126
+ onGeocoderResult={onGeocoderResult}
127
+ showResetButton={showResetButton}
128
+ ariaLabel={ariaLabel}
129
+ scrollProtection={false}
130
+ fullscreenButton={false}
131
+ onClose={onClose}
132
+ className="h-full w-full"
133
+ >
134
+ {children}
135
+ </MapContainer>
136
+ )}
137
+ </DialogContent>
138
+ </Dialog>
139
+ )
140
+ }
@@ -0,0 +1,70 @@
1
+ 'use client'
2
+
3
+ import { forwardRef } from 'react'
4
+ import {
5
+ GeolocateControl,
6
+ type GeolocateControlInstance,
7
+ type GeolocateResultEvent,
8
+ type GeolocateErrorEvent,
9
+ } from 'react-map-gl/maplibre'
10
+
11
+ import type { GeolocateOptions, GeolocatePosition } from './types'
12
+
13
+ /** The follow-state our chip reflects, derived from the control's events. */
14
+ export type GeolocateState = 'idle' | 'active' | 'error'
15
+
16
+ interface GeolocateHandleProps {
17
+ options: GeolocateOptions
18
+ onGeolocate?: (pos: GeolocatePosition) => void
19
+ onGeolocateError?: (err: GeolocationPositionError) => void
20
+ onStateChange: (state: GeolocateState) => void
21
+ }
22
+
23
+ /**
24
+ * The maplibre `GeolocateControl`, rendered as a child of `<Map>` so its
25
+ * location dot + accuracy circle draw on the map, but with the default
26
+ * control button **hidden** (`display:none`) — our `MapToolbar` chip is the
27
+ * UI and drives it via the forwarded instance ref (`control.trigger()`).
28
+ *
29
+ * `trackUserLocation` gives Google-style follow: first activation centers +
30
+ * follows; panning away drops to passive; re-trigger re-follows. We reflect
31
+ * active/error back to the chip through `onStateChange`.
32
+ *
33
+ * Note: maplibre-gl 4.7.1's `GeolocateControlOptions` has no `showUserHeading`
34
+ * field, so heading is surfaced via `onGeolocate` (and `showHeading` is a
35
+ * forward-compatible no-op on this version).
36
+ */
37
+ export const GeolocateHandle = forwardRef<GeolocateControlInstance, GeolocateHandleProps>(
38
+ function GeolocateHandle({ options, onGeolocate, onGeolocateError, onStateChange }, ref) {
39
+ return (
40
+ <GeolocateControl
41
+ ref={ref}
42
+ // Hidden: our chip is the UI. The dot + accuracy circle are map
43
+ // layers (not this button), so they still render.
44
+ style={{ display: 'none' }}
45
+ trackUserLocation
46
+ showUserLocation
47
+ showAccuracyCircle={options.showAccuracyCircle ?? true}
48
+ positionOptions={{ enableHighAccuracy: options.highAccuracy ?? false }}
49
+ fitBoundsOptions={
50
+ options.fitBoundsMaxZoom != null ? { maxZoom: options.fitBoundsMaxZoom } : undefined
51
+ }
52
+ onGeolocate={(e: GeolocateResultEvent) => {
53
+ const c = e.coords
54
+ onGeolocate?.({
55
+ lat: c.latitude,
56
+ lng: c.longitude,
57
+ accuracy: c.accuracy,
58
+ heading: c.heading != null && !Number.isNaN(c.heading) ? c.heading : null,
59
+ })
60
+ }}
61
+ onError={(e: GeolocateErrorEvent) => {
62
+ onStateChange('error')
63
+ onGeolocateError?.(e)
64
+ }}
65
+ onTrackUserLocationStart={() => onStateChange('active')}
66
+ onTrackUserLocationEnd={() => onStateChange('idle')}
67
+ />
68
+ )
69
+ },
70
+ )