@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,15 @@
1
+ import { cn } from '@djangocfg/ui-core/lib'
2
+
3
+ /**
4
+ * Controls overlaid on the map. The map tiles are always a light basemap,
5
+ * but per ui-core's style guide we use semantic tokens (never raw color
6
+ * scales). `popover` is the floating-chrome surface — the same token
7
+ * tooltips/menus use — so these chips read like maplibre's own controls
8
+ * and stay legible over the map in both light and dark themes.
9
+ */
10
+ export const MAP_CONTROL_CLASS = cn(
11
+ 'inline-flex items-center justify-center rounded-lg',
12
+ 'bg-popover/95 text-popover-foreground backdrop-blur-sm shadow-sm border border-border',
13
+ 'hover:bg-popover transition-colors',
14
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'
15
+ )
@@ -0,0 +1,51 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useRef } from 'react'
4
+
5
+ /**
6
+ * Auto-reset timer. After `autoResetDelay` ms of inactivity the map eases back
7
+ * to its initial viewport. Interaction (move-start) cancels any pending reset;
8
+ * move-end (re)arms the timer when the feature is enabled.
9
+ */
10
+ export function useAutoReset({
11
+ autoResetDelay,
12
+ resetToInitial,
13
+ }: {
14
+ autoResetDelay: number
15
+ resetToInitial: () => void
16
+ }) {
17
+ const resetTimerRef = useRef<NodeJS.Timeout | null>(null)
18
+ const isInteractingRef = useRef(false)
19
+
20
+ const handleMoveStart = useCallback(() => {
21
+ isInteractingRef.current = true
22
+ // Clear any pending reset timer
23
+ if (resetTimerRef.current) {
24
+ clearTimeout(resetTimerRef.current)
25
+ resetTimerRef.current = null
26
+ }
27
+ }, [])
28
+
29
+ const handleMoveEnd = useCallback(() => {
30
+ isInteractingRef.current = false
31
+ // Start auto-reset timer if enabled
32
+ if (autoResetDelay > 0) {
33
+ resetTimerRef.current = setTimeout(() => {
34
+ if (!isInteractingRef.current) {
35
+ resetToInitial()
36
+ }
37
+ }, autoResetDelay)
38
+ }
39
+ }, [autoResetDelay, resetToInitial])
40
+
41
+ // Cleanup timer on unmount
42
+ useEffect(() => {
43
+ return () => {
44
+ if (resetTimerRef.current) {
45
+ clearTimeout(resetTimerRef.current)
46
+ }
47
+ }
48
+ }, [])
49
+
50
+ return { handleMoveStart, handleMoveEnd }
51
+ }
@@ -0,0 +1,48 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from 'react'
4
+
5
+ import { MAP_STYLES, resolveBasemapOptions } from '../../../styles'
6
+ import type { MapStyleKey } from '../../../types'
7
+
8
+ /**
9
+ * Active basemap state. Seeded from `mapStyle`, then driven by the switcher.
10
+ * If `mapStyle` is a built-in KEY we track it as the active key; a raw URL is
11
+ * kept as-is (the switcher still works, just starts "unselected").
12
+ */
13
+ export function useBasemapState({
14
+ mapStyle,
15
+ basemaps,
16
+ onBasemapChange,
17
+ }: {
18
+ mapStyle: MapStyleKey | string
19
+ basemaps: MapStyleKey[] | undefined
20
+ onBasemapChange: ((key: MapStyleKey) => void) | undefined
21
+ }) {
22
+ const initialKey = (mapStyle in MAP_STYLES ? mapStyle : undefined) as MapStyleKey | undefined
23
+ const [activeKey, setActiveKey] = useState<MapStyleKey | undefined>(initialKey)
24
+ // Follow controlled `mapStyle` changes from the host.
25
+ useEffect(() => {
26
+ setActiveKey(mapStyle in MAP_STYLES ? (mapStyle as MapStyleKey) : undefined)
27
+ }, [mapStyle])
28
+ // The style actually rendered: the chosen key's URL, else the raw prop.
29
+ const effectiveStyle = activeKey ?? mapStyle
30
+ const basemapOptions = useMemo(
31
+ () => resolveBasemapOptions(basemaps, activeKey ?? (mapStyle as MapStyleKey | string)),
32
+ [basemaps, activeKey, mapStyle],
33
+ )
34
+ const handleBasemapPick = useCallback(
35
+ (key: string) => {
36
+ setActiveKey(key as MapStyleKey)
37
+ onBasemapChange?.(key as MapStyleKey)
38
+ },
39
+ [onBasemapChange],
40
+ )
41
+
42
+ // Resolve the EFFECTIVE style (switcher-aware) to a URL/object.
43
+ const resolvedStyle = effectiveStyle in MAP_STYLES
44
+ ? MAP_STYLES[effectiveStyle as MapStyleKey]
45
+ : effectiveStyle
46
+
47
+ return { activeKey, effectiveStyle, resolvedStyle, basemapOptions, handleBasemapPick }
48
+ }
@@ -0,0 +1,29 @@
1
+ 'use client'
2
+
3
+ import type { MapViewport } from '../../../types'
4
+
5
+ /**
6
+ * External-maps link. A host-supplied `openInMapsUrl` wins (Apple Maps, a
7
+ * custom deep link); otherwise deep-link to Google Maps at the current center
8
+ * so the button always works without configuration.
9
+ */
10
+ export function useExternalMapsUrl({
11
+ openInMapsUrl,
12
+ openInMapsLabel,
13
+ googleMapsButton,
14
+ viewport,
15
+ }: {
16
+ openInMapsUrl: string | undefined
17
+ openInMapsLabel: string
18
+ googleMapsButton: boolean
19
+ viewport: MapViewport
20
+ }) {
21
+ const externalMapsUrl =
22
+ openInMapsUrl ??
23
+ (googleMapsButton
24
+ ? `https://www.google.com/maps/search/?api=1&query=${viewport.latitude},${viewport.longitude}`
25
+ : undefined)
26
+ const externalMapsLabel = openInMapsUrl ? openInMapsLabel : 'Open in Google Maps'
27
+
28
+ return { externalMapsUrl, externalMapsLabel }
29
+ }
@@ -0,0 +1,29 @@
1
+ 'use client'
2
+
3
+ import { MapProvider } from '../../context'
4
+ import { MapInner } from './MapInner'
5
+ import type { MapContainerProps } from './types'
6
+
7
+ export type { MapContainerProps, GeolocateOptions, GeolocatePosition } from './types'
8
+
9
+ export function MapContainer({
10
+ children,
11
+ initialViewport,
12
+ className,
13
+ ...props
14
+ }: MapContainerProps) {
15
+ return (
16
+ <div className={className} style={{ width: '100%', height: '100%', position: 'relative' }}>
17
+ <MapProvider initialViewport={initialViewport}>
18
+ <MapInner {...props} MapContainerComponent={MapContainer}>{children}</MapInner>
19
+ </MapProvider>
20
+ </div>
21
+ )
22
+ }
23
+
24
+ /**
25
+ * Use this when you need the map inside an existing MapProvider
26
+ */
27
+ export function MapView(props: Omit<MapContainerProps, 'initialViewport'>) {
28
+ return <MapInner {...props} MapContainerComponent={MapContainer} />
29
+ }
@@ -0,0 +1,198 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ import type { MapViewport, MapStyleKey } from '../../types'
4
+ import type { GeocodeResult, ResolveGeocode } from '../../geocode'
5
+
6
+ /**
7
+ * Tunables forwarded to the maplibre `GeolocateControl` that backs the
8
+ * locate-me chip. All optional; sensible privacy-friendly defaults.
9
+ */
10
+ export interface GeolocateOptions {
11
+ /**
12
+ * Use the device's high-accuracy provider (GPS). More precise but slower
13
+ * and battery-hungry — off by default.
14
+ * @default false
15
+ */
16
+ highAccuracy?: boolean
17
+ /**
18
+ * Show a heading arrow on the location dot when a compass heading is
19
+ * available. (No-op on maplibre versions without heading support.)
20
+ * @default true
21
+ */
22
+ showHeading?: boolean
23
+ /**
24
+ * Draw the translucent accuracy circle (95% confidence) around the dot.
25
+ * @default true
26
+ */
27
+ showAccuracyCircle?: boolean
28
+ /**
29
+ * Cap how far the camera zooms in when easing to the user (very accurate
30
+ * fixes can otherwise zoom street-level).
31
+ */
32
+ fitBoundsMaxZoom?: number
33
+ }
34
+
35
+ /** A single successful position fix surfaced to `onGeolocate`. */
36
+ export interface GeolocatePosition {
37
+ lat: number
38
+ lng: number
39
+ /** Accuracy radius in meters. */
40
+ accuracy: number
41
+ /** Compass heading in degrees (0 = north), or null when unknown. */
42
+ heading: number | null
43
+ }
44
+
45
+ export interface MapContainerProps {
46
+ children?: ReactNode
47
+ initialViewport?: Partial<MapViewport>
48
+ mapStyle?: MapStyleKey | string
49
+ interactiveLayerIds?: string[]
50
+ className?: string
51
+ style?: React.CSSProperties
52
+ cursor?: string
53
+ /**
54
+ * Show the CARTO / OpenStreetMap attribution pill. Off by default for a
55
+ * clean embedded look. Note: CARTO & OSM licenses require attribution to
56
+ * appear somewhere — if you hide it here, surface it elsewhere in your UI.
57
+ * @default false
58
+ */
59
+ attributionControl?: boolean
60
+ /**
61
+ * Pool and reuse maplibre instances across mounts. Off by default — a
62
+ * reused instance can come back style-less (blank map) under Storybook
63
+ * HMR / story switches / chat lists. Enable only for a single, stable,
64
+ * long-lived full-page map.
65
+ * @default false
66
+ */
67
+ reuseMaps?: boolean
68
+ /**
69
+ * URL to open in an external maps app. When omitted, a Google Maps URL
70
+ * is generated from the current center (see `googleMapsButton`). Pass
71
+ * this to point at Apple Maps / a custom deep link instead.
72
+ */
73
+ openInMapsUrl?: string
74
+ /** Label for the open in maps button */
75
+ openInMapsLabel?: string
76
+ /**
77
+ * Show an "Open in Google Maps" button that deep-links to the current
78
+ * center. Suppressed when `openInMapsUrl` is provided (the host link
79
+ * takes over). Set `false` to hide it.
80
+ * @default true
81
+ */
82
+ googleMapsButton?: boolean
83
+ /** Auto-reset to initial viewport after N ms of inactivity (0 = disabled) */
84
+ autoResetDelay?: number
85
+ /** Show reset button */
86
+ showResetButton?: boolean
87
+ /**
88
+ * Block wheel/trackpad zoom until the user clicks the map (Google Maps
89
+ * style). Stops an inline map from hijacking the page scroll. Drag-pan,
90
+ * touch and double-click zoom stay live. Click outside or move the
91
+ * pointer off the map to re-lock.
92
+ * @default true
93
+ */
94
+ scrollProtection?: boolean
95
+ /** Hint shown on the lock overlay while scroll-zoom is blocked. */
96
+ scrollProtectionHint?: string
97
+ /**
98
+ * Show a button that opens the map full-screen in a large dialog
99
+ * (Google-Maps-style "expand"). Scroll protection is off inside the
100
+ * dialog since the map already fills the screen.
101
+ * @default true
102
+ */
103
+ fullscreenButton?: boolean
104
+ /** Accessible / tooltip label for the fullscreen button. */
105
+ fullscreenLabel?: string
106
+ /** Title for the fullscreen dialog (screen readers). */
107
+ fullscreenTitle?: string
108
+ /**
109
+ * Show a basemap switcher (a Layers button → dropdown) in the toolbar,
110
+ * Google-Maps style. Lets the user pick the map style at runtime.
111
+ * @default true
112
+ */
113
+ basemapSwitcher?: boolean
114
+ /**
115
+ * Explicit list of basemap keys to offer in the switcher. When omitted,
116
+ * the switcher shows all key-free built-ins (CARTO + OpenFreeMap) plus
117
+ * `satellite` only if it's actually usable (host-keyed). See
118
+ * `resolveBasemapOptions`.
119
+ */
120
+ basemaps?: MapStyleKey[]
121
+ /** Accessible / tooltip label for the basemap switcher button. */
122
+ basemapLabel?: string
123
+ /** Notified when the user picks a basemap from the switcher. */
124
+ onBasemapChange?: (key: MapStyleKey) => void
125
+ /**
126
+ * Render 3D terrain + hillshade from the free AWS Terrarium DEM (no API
127
+ * key). Eases the camera pitch in so it reads as 3D; toggles off cleanly.
128
+ * @default false
129
+ */
130
+ terrain?: boolean
131
+ /**
132
+ * Show a toolbar chip (a Mountain icon) that toggles 3D terrain at
133
+ * runtime, in the same top-right control column as the other chips.
134
+ * @default true
135
+ */
136
+ terrainButton?: boolean
137
+ /** Accessible / tooltip label for the terrain toggle button. */
138
+ terrainLabel?: string
139
+ /** Notified when the user toggles terrain from the chip. */
140
+ onTerrainChange?: (enabled: boolean) => void
141
+ /**
142
+ * Show a compass chip that appears only when the map is rotated or
143
+ * tilted (Google-style); its needle reflects the current bearing and a
144
+ * click eases the camera back to a north-up, top-down view. Rotate/tilt
145
+ * the map with right-drag, Ctrl+drag, or two fingers.
146
+ * @default true
147
+ */
148
+ compassButton?: boolean
149
+ /** Accessible / tooltip label for the compass (reset-north) button. */
150
+ compassLabel?: string
151
+ /**
152
+ * Show a Google-style "locate me" chip that drives maplibre's built-in
153
+ * `GeolocateControl` — centers + follows the user (track mode), with a
154
+ * location dot + accuracy circle. Privacy: **off by default**; nothing
155
+ * requests the OS permission prompt unless this is `true` AND the user
156
+ * presses the chip. Requires a secure context (HTTPS / localhost).
157
+ * @default false
158
+ */
159
+ geolocate?: boolean
160
+ /** Accessible / tooltip label for the locate-me chip. */
161
+ geolocateLabel?: string
162
+ /** Tunables forwarded to the underlying `GeolocateControl`. */
163
+ geolocateOptions?: GeolocateOptions
164
+ /** Notified on each successful position fix while geolocation is active. */
165
+ onGeolocate?: (pos: GeolocatePosition) => void
166
+ /** Notified when geolocation fails (permission denied / unavailable / timeout). */
167
+ onGeolocateError?: (err: GeolocationPositionError) => void
168
+ /**
169
+ * Show a Google-Maps-style search chip (a magnifier) in the toolbar that
170
+ * toggles an on-map address/place autocomplete. Collapsed by default:
171
+ * clicking the chip reveals an autofocused input (top-left), and clicking
172
+ * it again / pressing Esc / picking a result hides it. Picking a result
173
+ * flies the camera there and drops a pin. **Off by default** (opt-in).
174
+ * @default false
175
+ */
176
+ geocoder?: boolean
177
+ /**
178
+ * Injectable geocode resolver for the search chip. Defaults to the
179
+ * free, key-free public Photon endpoint. Pass a backend/route proxy in
180
+ * production to dodge CORS + throttling.
181
+ */
182
+ geocoderResolve?: ResolveGeocode
183
+ /** Placeholder for the search input. */
184
+ geocoderPlaceholder?: string
185
+ /** Notified with the chosen result after the camera flies to it. */
186
+ onGeocoderResult?: (result: GeocodeResult) => void
187
+ /**
188
+ * When set, a close button is rendered as the FIRST chip in the
189
+ * top-right control toolbar (used by the fullscreen dialog so the
190
+ * close / Google / reset controls share one tidy column instead of
191
+ * scattering the dialog's own X into a different corner).
192
+ */
193
+ onClose?: () => void
194
+ /** Accessible label for the map region (screen readers) */
195
+ ariaLabel?: string
196
+ }
197
+
198
+ export interface MapInnerProps extends Omit<MapContainerProps, 'initialViewport'> {}
@@ -7,13 +7,30 @@ import {
7
7
  ScaleControl,
8
8
  } from 'react-map-gl/maplibre'
9
9
 
10
+ import type { FitBoundsOptions } from 'maplibre-gl'
11
+
10
12
  type ControlPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
11
13
 
12
14
  export interface MapControlsProps {
13
15
  position?: ControlPosition
14
16
  showNavigation?: boolean
15
17
  showFullscreen?: boolean
18
+ /** Show maplibre's raw `GeolocateControl` (track mode). @default false */
16
19
  showGeolocate?: boolean
20
+ /**
21
+ * Draw a heading arrow on the location dot. Forwarded to the control; a
22
+ * no-op on maplibre versions without heading support.
23
+ */
24
+ showUserHeading?: boolean
25
+ /**
26
+ * Draw the translucent accuracy circle around the dot.
27
+ * @default true (maplibre default)
28
+ */
29
+ showAccuracyCircle?: boolean
30
+ /** Browser Geolocation API `PositionOptions` (enableHighAccuracy, timeout…). */
31
+ positionOptions?: PositionOptions
32
+ /** Options used when the control eases the camera to the user's location. */
33
+ fitBoundsOptions?: FitBoundsOptions
17
34
  showScale?: boolean
18
35
  scaleUnit?: 'imperial' | 'metric' | 'nautical'
19
36
  }
@@ -23,6 +40,10 @@ export function MapControls({
23
40
  showNavigation = true,
24
41
  showFullscreen = false,
25
42
  showGeolocate = false,
43
+ showUserHeading,
44
+ showAccuracyCircle,
45
+ positionOptions,
46
+ fitBoundsOptions,
26
47
  showScale = false,
27
48
  scaleUnit = 'metric',
28
49
  }: MapControlsProps) {
@@ -36,6 +57,12 @@ export function MapControls({
36
57
  <GeolocateControl
37
58
  position={position}
38
59
  trackUserLocation
60
+ // All optional — undefined falls back to maplibre's own defaults,
61
+ // so existing `showGeolocate` callers are unaffected.
62
+ {...(showUserHeading !== undefined ? { showUserHeading } : {})}
63
+ {...(showAccuracyCircle !== undefined ? { showAccuracyCircle } : {})}
64
+ {...(positionOptions !== undefined ? { positionOptions } : {})}
65
+ {...(fitBoundsOptions !== undefined ? { fitBoundsOptions } : {})}
39
66
  />
40
67
  )}
41
68
  {showScale && <ScaleControl unit={scaleUnit} />}
@@ -0,0 +1,94 @@
1
+ 'use client'
2
+
3
+ import { memo, useCallback, type ReactNode } from 'react'
4
+ import { MapMarker } from './MapMarker'
5
+ import { MapPopup, type MapPopupProps } from './MapPopup'
6
+ import { MarkerCard } from './MarkerCard'
7
+ import { useMarkerCard } from '../hooks/useMarkerCard'
8
+ import type { MarkerData } from '../types'
9
+ import type { MarkerCard as MarkerCardType } from '../cards/types'
10
+
11
+ /** A marker plus its optional info-card data. */
12
+ export type MarkerWithCard = MarkerData & { card?: MarkerCardType }
13
+
14
+ export interface MapMarkersProps {
15
+ /** Markers to render; any with a `card` (or when `renderCard` is set) opens a popup on click. */
16
+ markers: MarkerWithCard[]
17
+ /**
18
+ * Custom card content. When provided it overrides the default `MarkerCard`
19
+ * for the open marker. `close` dismisses the popup.
20
+ */
21
+ renderCard?: (marker: MarkerWithCard, close: () => void) => ReactNode
22
+ /** Popup anchor — default `'bottom'` so the card sits ABOVE the pin. */
23
+ cardAnchor?: MapPopupProps['anchor']
24
+ /** Notified on every marker click (in addition to opening its card). */
25
+ onMarkerClick?: (marker: MarkerWithCard) => void
26
+ /** Default-pin color (ignored when a marker supplies `children`-style icon — N/A here). */
27
+ color?: string
28
+ /** Default-pin size in px. */
29
+ size?: number
30
+ }
31
+
32
+ /**
33
+ * MapMarkers — the "just works" marker layer. Renders one `MapMarker` per
34
+ * entry and, on click, opens a `MapPopup` ANCHORED ABOVE the pin showing the
35
+ * marker's `card` (default `MarkerCard`) or `renderCard` output. Selection is
36
+ * driven by `useMarkerCard`, so only ONE card is open at a time and clicking
37
+ * the same pin toggles it. Outside-click / Esc / the popup close button all
38
+ * dismiss. Must live inside a `<MapProvider>` (a child of MapContainer/MapView,
39
+ * like `MapLegend`).
40
+ */
41
+ export const MapMarkers = memo(function MapMarkers({
42
+ markers,
43
+ renderCard,
44
+ cardAnchor = 'bottom',
45
+ onMarkerClick,
46
+ color,
47
+ size,
48
+ }: MapMarkersProps) {
49
+ const { openId, toggle, close } = useMarkerCard()
50
+
51
+ const handleClick = useCallback(
52
+ (marker: MarkerData) => {
53
+ toggle(marker.id)
54
+ onMarkerClick?.(marker as MarkerWithCard)
55
+ },
56
+ [toggle, onMarkerClick]
57
+ )
58
+
59
+ const openMarker = openId ? markers.find((m) => m.id === openId) : undefined
60
+ const hasCard = openMarker && (Boolean(openMarker.card) || Boolean(renderCard))
61
+
62
+ return (
63
+ <>
64
+ {markers.map((marker) => (
65
+ <MapMarker
66
+ key={marker.id}
67
+ marker={marker}
68
+ onClick={handleClick}
69
+ color={color}
70
+ size={size}
71
+ ariaLabel={
72
+ (marker.card?.title as string | undefined) ??
73
+ (marker.data?.label as string | undefined) ??
74
+ `Map marker ${marker.id}`
75
+ }
76
+ />
77
+ ))}
78
+
79
+ {openMarker && hasCard && (
80
+ <MapPopup
81
+ longitude={openMarker.longitude}
82
+ latitude={openMarker.latitude}
83
+ anchor={cardAnchor}
84
+ onClose={close}
85
+ maxWidth="none"
86
+ >
87
+ {renderCard
88
+ ? renderCard(openMarker, close)
89
+ : openMarker.card && <MarkerCard {...openMarker.card} />}
90
+ </MapPopup>
91
+ )}
92
+ </>
93
+ )
94
+ })
@@ -0,0 +1,102 @@
1
+ 'use client'
2
+
3
+ import { memo } from 'react'
4
+ import { Button, ButtonLink } from '@djangocfg/ui-core/components'
5
+ import { cn } from '@djangocfg/ui-core/lib'
6
+ import type { MarkerCard as MarkerCardData, MarkerCardAction } from '../cards/types'
7
+
8
+ export interface MarkerCardProps extends MarkerCardData {
9
+ /** Extra classes on the card root. */
10
+ className?: string
11
+ }
12
+
13
+ const VARIANT_MAP = {
14
+ default: 'default',
15
+ outline: 'outline',
16
+ ghost: 'ghost',
17
+ } as const
18
+
19
+ function CardAction({ action }: { action: MarkerCardAction }) {
20
+ const variant = VARIANT_MAP[action.variant ?? 'outline']
21
+
22
+ if (action.href) {
23
+ return (
24
+ <ButtonLink
25
+ href={action.href}
26
+ size="sm"
27
+ variant={variant}
28
+ elevated={false}
29
+ target="_blank"
30
+ rel="noopener noreferrer"
31
+ >
32
+ {action.icon}
33
+ {action.label}
34
+ </ButtonLink>
35
+ )
36
+ }
37
+
38
+ return (
39
+ <Button size="sm" variant={variant} elevated={false} onClick={action.onClick}>
40
+ {action.icon}
41
+ {action.label}
42
+ </Button>
43
+ )
44
+ }
45
+
46
+ /**
47
+ * MarkerCard — the DEFAULT presentational info-card for a map marker
48
+ * (Google-Maps-desktop style). Renders from plain data: an optional image
49
+ * header, a badge pill, a title, a clamped description, and an action row of
50
+ * links/buttons. Standalone-usable (no map dependency) and built on semantic
51
+ * tokens only. `MapMarkers` renders this inside a `MapPopup` above the pin.
52
+ */
53
+ export const MarkerCard = memo(function MarkerCard({
54
+ title,
55
+ description,
56
+ image,
57
+ badge,
58
+ actions,
59
+ className,
60
+ }: MarkerCardProps) {
61
+ return (
62
+ <div
63
+ className={cn(
64
+ 'flex max-w-[260px] flex-col overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-md',
65
+ className
66
+ )}
67
+ >
68
+ {image && (
69
+ <img
70
+ src={image}
71
+ alt={title}
72
+ className="h-28 w-full object-cover"
73
+ loading="lazy"
74
+ />
75
+ )}
76
+
77
+ <div className="flex flex-col gap-1.5 p-3">
78
+ {badge && (
79
+ <span className="inline-flex w-fit items-center rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
80
+ {badge}
81
+ </span>
82
+ )}
83
+
84
+ <h3 className="text-sm font-medium leading-snug text-foreground">{title}</h3>
85
+
86
+ {description && (
87
+ <p className="line-clamp-3 text-xs leading-relaxed text-muted-foreground">
88
+ {description}
89
+ </p>
90
+ )}
91
+
92
+ {actions && actions.length > 0 && (
93
+ <div className="mt-1.5 flex flex-wrap gap-1.5">
94
+ {actions.map((action, i) => (
95
+ <CardAction key={`${action.label}-${i}`} action={action} />
96
+ ))}
97
+ </div>
98
+ )}
99
+ </div>
100
+ </div>
101
+ )
102
+ })