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