@page-speed/maps 0.1.3 → 0.1.5

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 (54) hide show
  1. package/README.md +120 -0
  2. package/dist/components/geo-map.cjs +1228 -0
  3. package/dist/components/geo-map.cjs.map +1 -0
  4. package/dist/components/geo-map.d.cts +138 -0
  5. package/dist/components/geo-map.d.ts +138 -0
  6. package/dist/components/geo-map.js +1207 -0
  7. package/dist/components/geo-map.js.map +1 -0
  8. package/dist/components/index.cjs +1350 -0
  9. package/dist/components/index.cjs.map +1 -0
  10. package/dist/components/index.d.cts +5 -0
  11. package/dist/components/index.d.ts +5 -0
  12. package/dist/components/index.js +1326 -0
  13. package/dist/components/index.js.map +1 -0
  14. package/dist/components/map-marker.cjs +137 -0
  15. package/dist/components/map-marker.cjs.map +1 -0
  16. package/dist/components/map-marker.d.cts +76 -0
  17. package/dist/components/map-marker.d.ts +76 -0
  18. package/dist/components/map-marker.js +130 -0
  19. package/dist/components/map-marker.js.map +1 -0
  20. package/dist/core/MapLibre.cjs +46 -20
  21. package/dist/core/MapLibre.cjs.map +1 -1
  22. package/dist/core/MapLibre.js +46 -20
  23. package/dist/core/MapLibre.js.map +1 -1
  24. package/dist/core/index.cjs +46 -20
  25. package/dist/core/index.cjs.map +1 -1
  26. package/dist/core/index.js +46 -20
  27. package/dist/core/index.js.map +1 -1
  28. package/dist/index.cjs +964 -39
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +2 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +945 -39
  33. package/dist/index.js.map +1 -1
  34. package/dist/types/index.d.cts +5 -5
  35. package/dist/types/index.d.ts +5 -5
  36. package/dist/utils/cn.cjs +13 -0
  37. package/dist/utils/cn.cjs.map +1 -0
  38. package/dist/utils/cn.d.cts +16 -0
  39. package/dist/utils/cn.d.ts +16 -0
  40. package/dist/utils/cn.js +11 -0
  41. package/dist/utils/cn.js.map +1 -0
  42. package/dist/utils/index.cjs +63 -0
  43. package/dist/utils/index.cjs.map +1 -1
  44. package/dist/utils/index.d.cts +4 -0
  45. package/dist/utils/index.d.ts +4 -0
  46. package/dist/utils/index.js +42 -1
  47. package/dist/utils/index.js.map +1 -1
  48. package/dist/utils/simple-pressable.cjs +63 -0
  49. package/dist/utils/simple-pressable.cjs.map +1 -0
  50. package/dist/utils/simple-pressable.d.cts +20 -0
  51. package/dist/utils/simple-pressable.d.ts +20 -0
  52. package/dist/utils/simple-pressable.js +41 -0
  53. package/dist/utils/simple-pressable.js.map +1 -0
  54. package/package.json +29 -2
@@ -0,0 +1,1350 @@
1
+ 'use strict';
2
+
3
+ var clsx = require('clsx');
4
+ var tailwindMerge = require('tailwind-merge');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var React3 = require('react');
7
+ var maplibre = require('react-map-gl/maplibre');
8
+
9
+ function _interopNamespace(e) {
10
+ if (e && e.__esModule) return e;
11
+ var n = Object.create(null);
12
+ if (e) {
13
+ Object.keys(e).forEach(function (k) {
14
+ if (k !== 'default') {
15
+ var d = Object.getOwnPropertyDescriptor(e, k);
16
+ Object.defineProperty(n, k, d.get ? d : {
17
+ enumerable: true,
18
+ get: function () { return e[k]; }
19
+ });
20
+ }
21
+ });
22
+ }
23
+ n.default = e;
24
+ return Object.freeze(n);
25
+ }
26
+
27
+ var React3__namespace = /*#__PURE__*/_interopNamespace(React3);
28
+
29
+ // src/utils/cn.ts
30
+ function cn(...inputs) {
31
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
32
+ }
33
+ var SIZE_CONFIG = {
34
+ sm: {
35
+ outer: "size-10",
36
+ middle: "size-7",
37
+ inner: "size-5",
38
+ dot: "size-2"
39
+ },
40
+ md: {
41
+ outer: "size-14",
42
+ middle: "size-10",
43
+ inner: "size-7",
44
+ dot: "size-2.5"
45
+ },
46
+ lg: {
47
+ outer: "size-20",
48
+ middle: "size-14",
49
+ inner: "size-10",
50
+ dot: "size-3.5"
51
+ }
52
+ };
53
+ function MapMarker({
54
+ size = "md",
55
+ isSelected = false,
56
+ dotColor,
57
+ innerRingColor,
58
+ middleRingColor,
59
+ outerRingColor,
60
+ className,
61
+ onClick,
62
+ interactive = true,
63
+ "aria-label": ariaLabel = "Map location marker"
64
+ }) {
65
+ const sizeConfig = SIZE_CONFIG[size];
66
+ const content = /* @__PURE__ */ jsxRuntime.jsxs(
67
+ "div",
68
+ {
69
+ className: cn(
70
+ "relative flex items-center justify-center rounded-full transition-transform duration-200",
71
+ sizeConfig.outer,
72
+ isSelected && "scale-110",
73
+ className
74
+ ),
75
+ style: { backgroundColor: outerRingColor },
76
+ children: [
77
+ /* @__PURE__ */ jsxRuntime.jsx(
78
+ "div",
79
+ {
80
+ className: cn(
81
+ "absolute rounded-full transition-all duration-200",
82
+ sizeConfig.middle
83
+ ),
84
+ style: { backgroundColor: middleRingColor }
85
+ }
86
+ ),
87
+ /* @__PURE__ */ jsxRuntime.jsx(
88
+ "div",
89
+ {
90
+ className: cn(
91
+ "absolute rounded-full transition-all duration-200",
92
+ sizeConfig.inner
93
+ ),
94
+ style: { backgroundColor: innerRingColor }
95
+ }
96
+ ),
97
+ /* @__PURE__ */ jsxRuntime.jsx(
98
+ "div",
99
+ {
100
+ className: cn(
101
+ "absolute rounded-full transition-all duration-200",
102
+ sizeConfig.dot
103
+ ),
104
+ style: { backgroundColor: dotColor }
105
+ }
106
+ )
107
+ ]
108
+ }
109
+ );
110
+ if (!interactive) {
111
+ return content;
112
+ }
113
+ return /* @__PURE__ */ jsxRuntime.jsx(
114
+ "button",
115
+ {
116
+ type: "button",
117
+ className: "group cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-full",
118
+ onClick,
119
+ "aria-label": ariaLabel,
120
+ children: /* @__PURE__ */ jsxRuntime.jsx(
121
+ "div",
122
+ {
123
+ className: cn(
124
+ "transition-transform duration-200 group-hover:scale-110",
125
+ isSelected && "scale-110"
126
+ ),
127
+ children: content
128
+ }
129
+ )
130
+ }
131
+ );
132
+ }
133
+ function NeutralMapMarker(props) {
134
+ return /* @__PURE__ */ jsxRuntime.jsx(
135
+ MapMarker,
136
+ {
137
+ ...props,
138
+ dotColor: "hsl(var(--neutral-900, 0 0% 9%))",
139
+ innerRingColor: "hsl(var(--neutral-400, 0 0% 64%))",
140
+ middleRingColor: "hsl(var(--neutral-300, 0 0% 78%))",
141
+ outerRingColor: "hsl(var(--neutral-200, 0 0% 88%))"
142
+ }
143
+ );
144
+ }
145
+ function createMapMarkerElement(config) {
146
+ return function MarkerElement({ isSelected }) {
147
+ return /* @__PURE__ */ jsxRuntime.jsx(MapMarker, { ...config, isSelected, interactive: false });
148
+ };
149
+ }
150
+
151
+ // src/utils/getMapLibreStyleUrl.ts
152
+ var MAPLIBRE_DEFAULT_STYLE_URL = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json";
153
+ var DEFAULT_STADIA_STYLE_URL = "https://tiles.stadiamaps.com/styles/osm_bright.json";
154
+ var STYLE_MAP = {
155
+ default: DEFAULT_STADIA_STYLE_URL,
156
+ "alidade-smooth": "https://tiles.stadiamaps.com/styles/alidade_smooth.json",
157
+ "alidade-smooth-dark": "https://tiles.stadiamaps.com/styles/alidade_smooth_dark.json",
158
+ "maplibre-default": MAPLIBRE_DEFAULT_STYLE_URL,
159
+ "osm-bright": "https://tiles.stadiamaps.com/styles/osm_bright.json",
160
+ "stadia-outdoors": "https://tiles.stadiamaps.com/styles/outdoors.json",
161
+ "stamen-toner": "https://tiles.stadiamaps.com/styles/stamen_toner.json",
162
+ "stamen-terrain": "https://tiles.stadiamaps.com/styles/stamen_terrain.json",
163
+ "stamen-watercolor": "https://tiles.stadiamaps.com/styles/stamen_watercolor.json"
164
+ };
165
+ var HTTP_URL_REGEX = /^https?:\/\//i;
166
+ function isStadiaMapsUrl(url) {
167
+ try {
168
+ const parsed = new URL(url);
169
+ return parsed.hostname === "tiles.stadiamaps.com";
170
+ } catch {
171
+ return false;
172
+ }
173
+ }
174
+ function assertStadiaApiKey(stadiaApiKey) {
175
+ if (!stadiaApiKey.trim()) {
176
+ throw new Error(
177
+ "A non-empty stadiaApiKey is required for Stadia Maps style URLs."
178
+ );
179
+ }
180
+ }
181
+ function appendStadiaApiKey(styleUrl, stadiaApiKey) {
182
+ if (!isStadiaMapsUrl(styleUrl)) {
183
+ return styleUrl;
184
+ }
185
+ assertStadiaApiKey(stadiaApiKey);
186
+ const parsed = new URL(styleUrl);
187
+ if (!parsed.searchParams.has("api_key")) {
188
+ parsed.searchParams.set("api_key", stadiaApiKey);
189
+ }
190
+ return parsed.toString();
191
+ }
192
+ function getMapLibreStyleUrl(value, stadiaApiKey) {
193
+ const normalizedApiKey = stadiaApiKey.trim();
194
+ if (!value || typeof value !== "string") {
195
+ if (!normalizedApiKey) {
196
+ return MAPLIBRE_DEFAULT_STYLE_URL;
197
+ }
198
+ return appendStadiaApiKey(DEFAULT_STADIA_STYLE_URL, normalizedApiKey);
199
+ }
200
+ if (STYLE_MAP[value]) {
201
+ const mappedStyleUrl = STYLE_MAP[value];
202
+ if (isStadiaMapsUrl(mappedStyleUrl) && !normalizedApiKey) {
203
+ return MAPLIBRE_DEFAULT_STYLE_URL;
204
+ }
205
+ return appendStadiaApiKey(mappedStyleUrl, normalizedApiKey);
206
+ }
207
+ if (HTTP_URL_REGEX.test(value)) {
208
+ if (isStadiaMapsUrl(value) && !normalizedApiKey) {
209
+ return MAPLIBRE_DEFAULT_STYLE_URL;
210
+ }
211
+ return appendStadiaApiKey(value, normalizedApiKey);
212
+ }
213
+ if (!normalizedApiKey) {
214
+ return MAPLIBRE_DEFAULT_STYLE_URL;
215
+ }
216
+ return appendStadiaApiKey(DEFAULT_STADIA_STYLE_URL, normalizedApiKey);
217
+ }
218
+ var SimplePressable = React3__namespace.forwardRef(({ children, className, href, onClick, ...props }, ref) => {
219
+ if (href) {
220
+ const isExternal = href.startsWith("http://") || href.startsWith("https://");
221
+ return /* @__PURE__ */ jsxRuntime.jsx(
222
+ "a",
223
+ {
224
+ ref,
225
+ href,
226
+ className,
227
+ target: isExternal ? "_blank" : props.target,
228
+ rel: isExternal ? "noopener noreferrer" : props.rel,
229
+ onClick,
230
+ ...props,
231
+ children
232
+ }
233
+ );
234
+ }
235
+ if (onClick) {
236
+ return /* @__PURE__ */ jsxRuntime.jsx(
237
+ "button",
238
+ {
239
+ ref,
240
+ type: "button",
241
+ className,
242
+ onClick,
243
+ ...props,
244
+ children
245
+ }
246
+ );
247
+ }
248
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className, children });
249
+ });
250
+ SimplePressable.displayName = "SimplePressable";
251
+ var DEFAULT_MAPLIBRE_CSS_HREF = "https://cdn.jsdelivr.net/npm/maplibre-gl@5.18.0/dist/maplibre-gl.css";
252
+ var MAPLIBRE_STYLESHEET_ID = "page-speed-maplibre-gl-css";
253
+ var DEFAULT_FLY_TO_OPTIONS = Object.freeze({});
254
+ var VIEW_STATE_COORDINATE_EPSILON = 1e-6;
255
+ var VIEW_STATE_ZOOM_EPSILON = 0.01;
256
+ var DEFAULT_FLY_TO_EASING = (t) => 1 - Math.pow(1 - t, 3);
257
+ function joinClassNames(...classNames) {
258
+ return classNames.filter(Boolean).join(" ");
259
+ }
260
+ function hasMeaningfulViewStateDelta(previous, next) {
261
+ return Math.abs(previous.latitude - next.latitude) > VIEW_STATE_COORDINATE_EPSILON || Math.abs(previous.longitude - next.longitude) > VIEW_STATE_COORDINATE_EPSILON || Math.abs(previous.zoom - next.zoom) > VIEW_STATE_ZOOM_EPSILON;
262
+ }
263
+ function ensureMapLibreStylesheet(href) {
264
+ if (typeof document === "undefined") {
265
+ return;
266
+ }
267
+ const existingLink = document.getElementById(MAPLIBRE_STYLESHEET_ID);
268
+ if (existingLink instanceof HTMLLinkElement) {
269
+ if (existingLink.getAttribute("href") !== href) {
270
+ existingLink.setAttribute("href", href);
271
+ }
272
+ return;
273
+ }
274
+ const matchingLink = Array.from(
275
+ document.querySelectorAll("link[rel='stylesheet']")
276
+ ).find((link) => link.getAttribute("href") === href);
277
+ if (matchingLink instanceof HTMLLinkElement) {
278
+ matchingLink.id = MAPLIBRE_STYLESHEET_ID;
279
+ return;
280
+ }
281
+ const stylesheet = document.createElement("link");
282
+ stylesheet.id = MAPLIBRE_STYLESHEET_ID;
283
+ stylesheet.rel = "stylesheet";
284
+ stylesheet.href = href;
285
+ stylesheet.dataset.pageSpeedMaps = "maplibre-css";
286
+ document.head.appendChild(stylesheet);
287
+ }
288
+ function DefaultMarker({ marker }) {
289
+ return /* @__PURE__ */ jsxRuntime.jsxs(
290
+ "div",
291
+ {
292
+ style: {
293
+ cursor: marker.draggable ? "grab" : "pointer",
294
+ transform: "translate(-50%, -100%)",
295
+ display: "inline-flex",
296
+ alignItems: "center",
297
+ justifyContent: "center",
298
+ position: "relative"
299
+ },
300
+ onClick: marker.onClick,
301
+ children: [
302
+ /* @__PURE__ */ jsxRuntime.jsx(
303
+ "svg",
304
+ {
305
+ "aria-hidden": "true",
306
+ width: "32",
307
+ height: "32",
308
+ viewBox: "0 0 24 24",
309
+ fill: marker.color || "#3B82F6",
310
+ style: { filter: "drop-shadow(0 2px 8px rgba(0,0,0,0.35))" },
311
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 2C8.13 2 5 5.13 5 9c0 4.85 6.13 12.24 6.39 12.55a.75.75 0 0 0 1.16 0C12.87 21.24 19 13.85 19 9c0-3.87-3.13-7-7-7Zm0 9.75A2.75 2.75 0 1 1 12 6.25a2.75 2.75 0 0 1 0 5.5Z" })
312
+ }
313
+ ),
314
+ marker.label ? /* @__PURE__ */ jsxRuntime.jsx(
315
+ "div",
316
+ {
317
+ style: {
318
+ position: "absolute",
319
+ bottom: -28,
320
+ left: "50%",
321
+ transform: "translateX(-50%)",
322
+ background: "#FFFFFF",
323
+ borderRadius: 6,
324
+ padding: "2px 8px",
325
+ fontSize: 12,
326
+ fontWeight: 500,
327
+ whiteSpace: "nowrap",
328
+ boxShadow: "0 3px 10px rgba(0, 0, 0, 0.2)"
329
+ },
330
+ children: marker.label
331
+ }
332
+ ) : null
333
+ ]
334
+ }
335
+ );
336
+ }
337
+ function normalizeMarkers(markers) {
338
+ return markers.map((marker, index) => {
339
+ if (marker.lat !== void 0 && marker.lng !== void 0) {
340
+ return marker;
341
+ }
342
+ const basicMarker = marker;
343
+ return {
344
+ id: basicMarker.id ?? index,
345
+ lat: basicMarker.latitude,
346
+ lng: basicMarker.longitude,
347
+ color: basicMarker.color,
348
+ draggable: basicMarker.draggable,
349
+ label: basicMarker.label,
350
+ element: basicMarker.element,
351
+ onClick: basicMarker.onClick
352
+ };
353
+ });
354
+ }
355
+ function MapLibre({
356
+ stadiaApiKey,
357
+ mapLibreCssHref,
358
+ viewState,
359
+ onViewStateChange,
360
+ mapStyle,
361
+ center = viewState ? { lat: viewState.latitude ?? 0, lng: viewState.longitude ?? 0 } : { lat: 0, lng: 0 },
362
+ zoom = viewState?.zoom ?? 14,
363
+ styleUrl,
364
+ markers = [],
365
+ onMoveEnd,
366
+ onClick,
367
+ onMarkerDrag,
368
+ className,
369
+ style,
370
+ children,
371
+ showNavigationControl = true,
372
+ showGeolocateControl = false,
373
+ navigationControlPosition = "bottom-right",
374
+ geolocateControlPosition = "top-left",
375
+ flyToOptions = DEFAULT_FLY_TO_OPTIONS
376
+ }) {
377
+ const mapRef = React3__namespace.default.useRef(null);
378
+ const resolvedMapLibreCssHref = mapLibreCssHref && mapLibreCssHref.trim().length > 0 ? mapLibreCssHref : DEFAULT_MAPLIBRE_CSS_HREF;
379
+ const [internalViewState, setInternalViewState] = React3__namespace.default.useState({
380
+ latitude: viewState?.latitude ?? center.lat,
381
+ longitude: viewState?.longitude ?? center.lng,
382
+ zoom: viewState?.zoom ?? zoom
383
+ });
384
+ const isUserInteracting = React3__namespace.default.useRef(false);
385
+ const isMarkerDragging = React3__namespace.default.useRef(false);
386
+ const dragAnimationFrame = React3__namespace.default.useRef(null);
387
+ const lastReportedViewState = React3__namespace.default.useRef(null);
388
+ const resolvedFlyToOptions = React3__namespace.default.useMemo(
389
+ () => ({
390
+ speed: flyToOptions.speed ?? 0.8,
391
+ curve: flyToOptions.curve ?? 1.2,
392
+ bearing: flyToOptions.bearing ?? 0,
393
+ easing: flyToOptions.easing ?? DEFAULT_FLY_TO_EASING
394
+ }),
395
+ [
396
+ flyToOptions.bearing,
397
+ flyToOptions.curve,
398
+ flyToOptions.easing,
399
+ flyToOptions.speed
400
+ ]
401
+ );
402
+ React3__namespace.default.useEffect(() => {
403
+ ensureMapLibreStylesheet(resolvedMapLibreCssHref);
404
+ }, [resolvedMapLibreCssHref]);
405
+ React3__namespace.default.useEffect(() => {
406
+ if (!mapRef.current || !viewState || isUserInteracting.current || isMarkerDragging.current) {
407
+ return;
408
+ }
409
+ setInternalViewState((previous) => {
410
+ const next = {
411
+ latitude: viewState.latitude ?? previous.latitude,
412
+ longitude: viewState.longitude ?? previous.longitude,
413
+ zoom: viewState.zoom ?? previous.zoom
414
+ };
415
+ const hasChanged = hasMeaningfulViewStateDelta(previous, next);
416
+ if (!hasChanged) {
417
+ return previous;
418
+ }
419
+ const isEchoedMoveState = !!lastReportedViewState.current && !hasMeaningfulViewStateDelta(lastReportedViewState.current, next);
420
+ if (!isEchoedMoveState) {
421
+ mapRef.current?.flyTo({
422
+ center: [next.longitude, next.latitude],
423
+ zoom: next.zoom,
424
+ speed: resolvedFlyToOptions.speed,
425
+ curve: resolvedFlyToOptions.curve,
426
+ bearing: resolvedFlyToOptions.bearing,
427
+ easing: resolvedFlyToOptions.easing,
428
+ essential: true
429
+ });
430
+ }
431
+ return next;
432
+ });
433
+ }, [
434
+ resolvedFlyToOptions,
435
+ viewState?.latitude,
436
+ viewState?.longitude,
437
+ viewState?.zoom
438
+ ]);
439
+ const handleMoveStart = React3__namespace.default.useCallback(() => {
440
+ isUserInteracting.current = true;
441
+ }, []);
442
+ const handleMove = React3__namespace.default.useCallback(
443
+ (event) => {
444
+ const nextViewState = event.viewState;
445
+ setInternalViewState({
446
+ latitude: nextViewState.latitude,
447
+ longitude: nextViewState.longitude,
448
+ zoom: nextViewState.zoom
449
+ });
450
+ const roundedViewState = {
451
+ latitude: Number(nextViewState.latitude.toFixed(6)),
452
+ longitude: Number(nextViewState.longitude.toFixed(6)),
453
+ zoom: Number(nextViewState.zoom.toFixed(2))
454
+ };
455
+ lastReportedViewState.current = roundedViewState;
456
+ onViewStateChange?.(roundedViewState);
457
+ },
458
+ [onViewStateChange]
459
+ );
460
+ const handleMoveEnd = React3__namespace.default.useCallback(
461
+ (event) => {
462
+ isUserInteracting.current = false;
463
+ if (!onMoveEnd) {
464
+ return;
465
+ }
466
+ const map = event.target;
467
+ const nextCenter = map.getCenter();
468
+ const nextZoom = map.getZoom();
469
+ const bounds = map.getBounds();
470
+ onMoveEnd(
471
+ {
472
+ lat: Number(nextCenter.lat.toFixed(6)),
473
+ lng: Number(nextCenter.lng.toFixed(6))
474
+ },
475
+ Number(nextZoom.toFixed(2)),
476
+ bounds
477
+ );
478
+ },
479
+ [onMoveEnd]
480
+ );
481
+ const handleMapClick = React3__namespace.default.useCallback(
482
+ (event) => {
483
+ if (!onClick) {
484
+ return;
485
+ }
486
+ onClick({ latitude: event.lngLat.lat, longitude: event.lngLat.lng });
487
+ },
488
+ [onClick]
489
+ );
490
+ const normalizedMarkers = React3__namespace.default.useMemo(
491
+ () => normalizeMarkers(markers),
492
+ [markers]
493
+ );
494
+ const markerElements = React3__namespace.default.useMemo(
495
+ () => normalizedMarkers.map((marker) => /* @__PURE__ */ jsxRuntime.jsx(
496
+ maplibre.Marker,
497
+ {
498
+ longitude: marker.lng,
499
+ latitude: marker.lat,
500
+ draggable: marker.draggable,
501
+ onDragStart: () => {
502
+ isMarkerDragging.current = true;
503
+ },
504
+ onDrag: (event) => {
505
+ if (!mapRef.current) {
506
+ return;
507
+ }
508
+ const nextLngLat = event.lngLat;
509
+ if (!nextLngLat || nextLngLat.lng === void 0 || nextLngLat.lat === void 0) {
510
+ return;
511
+ }
512
+ const draggedLng = nextLngLat.lng;
513
+ const draggedLat = nextLngLat.lat;
514
+ if (dragAnimationFrame.current) {
515
+ cancelAnimationFrame(dragAnimationFrame.current);
516
+ }
517
+ dragAnimationFrame.current = requestAnimationFrame(() => {
518
+ if (!mapRef.current) {
519
+ return;
520
+ }
521
+ const bounds = mapRef.current.getBounds();
522
+ const viewportWidth = bounds.getEast() - bounds.getWest();
523
+ const viewportHeight = bounds.getNorth() - bounds.getSouth();
524
+ const edgePadding = 0.1;
525
+ const westThreshold = bounds.getWest() + viewportWidth * edgePadding;
526
+ const eastThreshold = bounds.getEast() - viewportWidth * edgePadding;
527
+ const southThreshold = bounds.getSouth() + viewportHeight * edgePadding;
528
+ const northThreshold = bounds.getNorth() - viewportHeight * edgePadding;
529
+ const nearWestEdge = draggedLng < westThreshold;
530
+ const nearEastEdge = draggedLng > eastThreshold;
531
+ const nearSouthEdge = draggedLat < southThreshold;
532
+ const nearNorthEdge = draggedLat > northThreshold;
533
+ if (!nearWestEdge && !nearEastEdge && !nearSouthEdge && !nearNorthEdge) {
534
+ return;
535
+ }
536
+ let panLng = draggedLng;
537
+ let panLat = draggedLat;
538
+ const offsetAmount = 0.2;
539
+ if (nearWestEdge) {
540
+ panLng = draggedLng - viewportWidth * offsetAmount;
541
+ }
542
+ if (nearEastEdge) {
543
+ panLng = draggedLng + viewportWidth * offsetAmount;
544
+ }
545
+ if (nearSouthEdge) {
546
+ panLat = draggedLat - viewportHeight * offsetAmount;
547
+ }
548
+ if (nearNorthEdge) {
549
+ panLat = draggedLat + viewportHeight * offsetAmount;
550
+ }
551
+ mapRef.current?.easeTo({
552
+ center: [panLng, panLat],
553
+ duration: 200
554
+ });
555
+ });
556
+ },
557
+ onDragEnd: (event) => {
558
+ isMarkerDragging.current = false;
559
+ if (dragAnimationFrame.current) {
560
+ cancelAnimationFrame(dragAnimationFrame.current);
561
+ dragAnimationFrame.current = null;
562
+ }
563
+ if (!onMarkerDrag) {
564
+ return;
565
+ }
566
+ const nextLngLat = event.lngLat;
567
+ if (!nextLngLat || nextLngLat.lng === void 0 || nextLngLat.lat === void 0) {
568
+ return;
569
+ }
570
+ onMarkerDrag(marker.id ?? null, {
571
+ latitude: nextLngLat.lat,
572
+ longitude: nextLngLat.lng
573
+ });
574
+ },
575
+ children: marker.element ? typeof marker.element === "function" ? marker.element() : marker.element : /* @__PURE__ */ jsxRuntime.jsx(DefaultMarker, { marker })
576
+ },
577
+ marker.id
578
+ )),
579
+ [normalizedMarkers, onMarkerDrag]
580
+ );
581
+ const resolvedMapStyleUrl = React3__namespace.default.useMemo(() => {
582
+ if (styleUrl) {
583
+ return appendStadiaApiKey(styleUrl, stadiaApiKey);
584
+ }
585
+ if (mapStyle) {
586
+ return getMapLibreStyleUrl(mapStyle, stadiaApiKey);
587
+ }
588
+ return getMapLibreStyleUrl("osm-bright", stadiaApiKey);
589
+ }, [mapStyle, stadiaApiKey, styleUrl]);
590
+ return /* @__PURE__ */ jsxRuntime.jsx(
591
+ "div",
592
+ {
593
+ className: joinClassNames("relative w-full h-full", className),
594
+ style: { width: "100%", height: "100%", ...style },
595
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
596
+ maplibre.Map,
597
+ {
598
+ ref: mapRef,
599
+ ...internalViewState,
600
+ mapStyle: resolvedMapStyleUrl,
601
+ onMoveStart: handleMoveStart,
602
+ onMove: handleMove,
603
+ onMoveEnd: handleMoveEnd,
604
+ onClick: handleMapClick,
605
+ attributionControl: false,
606
+ trackResize: true,
607
+ dragRotate: false,
608
+ touchZoomRotate: false,
609
+ children: [
610
+ showGeolocateControl ? /* @__PURE__ */ jsxRuntime.jsx(maplibre.GeolocateControl, { position: geolocateControlPosition }) : null,
611
+ showNavigationControl ? /* @__PURE__ */ jsxRuntime.jsx(maplibre.NavigationControl, { position: navigationControlPosition }) : null,
612
+ markerElements,
613
+ children
614
+ ]
615
+ }
616
+ )
617
+ }
618
+ );
619
+ }
620
+ var PANEL_POSITION_CLASS = {
621
+ "top-left": "left-4 top-4",
622
+ "top-right": "right-4 top-4",
623
+ "bottom-left": "bottom-4 left-4",
624
+ "bottom-right": "bottom-4 right-4"
625
+ };
626
+ var DEFAULT_VIEW_STATE = {
627
+ latitude: 39.5,
628
+ longitude: -98.35,
629
+ zoom: 3
630
+ };
631
+ var VIDEO_FILE_EXTENSION_REGEX = /\.(mp4|webm|ogg|mov|m4v|m3u8)(\?.*)?$/i;
632
+ function resolveMediaType(item) {
633
+ if (item.type) {
634
+ return item.type;
635
+ }
636
+ return VIDEO_FILE_EXTENSION_REGEX.test(item.src) ? "video" : "image";
637
+ }
638
+ function normalizeId(value, fallback) {
639
+ if (value === null || value === void 0 || value === "") {
640
+ return fallback;
641
+ }
642
+ return String(value);
643
+ }
644
+ function buildClusterCenter(markers) {
645
+ if (!markers.length) {
646
+ return null;
647
+ }
648
+ const total = markers.reduce(
649
+ (accumulator, marker) => ({
650
+ latitude: accumulator.latitude + marker.latitude,
651
+ longitude: accumulator.longitude + marker.longitude
652
+ }),
653
+ { latitude: 0, longitude: 0 }
654
+ );
655
+ return {
656
+ latitude: total.latitude / markers.length,
657
+ longitude: total.longitude / markers.length
658
+ };
659
+ }
660
+ function resolveActionKey(action, index) {
661
+ if (typeof action.label === "string" && action.label.trim().length > 0) {
662
+ return `label:${action.label}:${index}`;
663
+ }
664
+ if (action.href) {
665
+ return `href:${action.href}:${index}`;
666
+ }
667
+ return `action:${index}`;
668
+ }
669
+ var FallbackIcon = ({
670
+ size = 20,
671
+ className
672
+ }) => /* @__PURE__ */ jsxRuntime.jsx(
673
+ "svg",
674
+ {
675
+ width: size,
676
+ height: size,
677
+ viewBox: "0 0 24 24",
678
+ fill: "none",
679
+ stroke: "currentColor",
680
+ strokeWidth: "2",
681
+ strokeLinecap: "round",
682
+ strokeLinejoin: "round",
683
+ className,
684
+ children: /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10" })
685
+ }
686
+ );
687
+ var FallbackImg = ({ src, alt, className, loading }) => /* @__PURE__ */ jsxRuntime.jsx("img", { src, alt, className, loading });
688
+ function MarkerActions({ actions }) {
689
+ if (!actions || actions.length === 0) {
690
+ return null;
691
+ }
692
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 flex flex-wrap gap-2", children: actions.map((action, index) => {
693
+ const {
694
+ label,
695
+ icon,
696
+ iconAfter,
697
+ children,
698
+ href,
699
+ onClick,
700
+ className: actionClassName,
701
+ variant,
702
+ size,
703
+ asButton,
704
+ ...rest
705
+ } = action;
706
+ const buttonStyles = cn(
707
+ "inline-flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-colors",
708
+ variant === "outline" ? "border border-border bg-background hover:bg-muted" : "bg-primary text-primary-foreground hover:bg-primary/90",
709
+ size === "sm" && "text-sm px-3 py-1.5",
710
+ size === "icon" && "p-2",
711
+ actionClassName
712
+ );
713
+ return /* @__PURE__ */ jsxRuntime.jsx(
714
+ SimplePressable,
715
+ {
716
+ href,
717
+ onClick,
718
+ className: buttonStyles,
719
+ ...rest,
720
+ children: children ?? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
721
+ icon,
722
+ label,
723
+ iconAfter
724
+ ] })
725
+ },
726
+ resolveActionKey(action, index)
727
+ );
728
+ }) });
729
+ }
730
+ function MarkerMediaCarousel({
731
+ mediaItems,
732
+ optixFlowConfig,
733
+ IconComponent = FallbackIcon,
734
+ ImgComponent = FallbackImg
735
+ }) {
736
+ const [activeIndex, setActiveIndex] = React3__namespace.useState(0);
737
+ const totalItems = mediaItems.length;
738
+ const mediaResetKey = React3__namespace.useMemo(
739
+ () => mediaItems.map((item, index) => {
740
+ const itemId = normalizeId(item.id, `media-${index}`);
741
+ return `${itemId}:${item.src}:${item.type ?? ""}:${item.poster ?? ""}`;
742
+ }).join("|"),
743
+ [mediaItems]
744
+ );
745
+ const activeItemIndex = Math.min(activeIndex, Math.max(0, totalItems - 1));
746
+ React3__namespace.useEffect(() => {
747
+ setActiveIndex(0);
748
+ }, [mediaResetKey]);
749
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative border-b border-border/60 bg-muted/40", children: [
750
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative aspect-video w-full overflow-hidden", children: mediaItems.map((item, index) => {
751
+ const isActive = index === activeItemIndex;
752
+ const mediaType = resolveMediaType(item);
753
+ return /* @__PURE__ */ jsxRuntime.jsx(
754
+ "div",
755
+ {
756
+ "aria-hidden": !isActive,
757
+ className: cn(
758
+ "absolute inset-0 transition-opacity duration-500 ease-in-out",
759
+ isActive ? "opacity-100 z-1" : "opacity-0 z-0 pointer-events-none"
760
+ ),
761
+ children: mediaType === "video" ? /* @__PURE__ */ jsxRuntime.jsx(
762
+ "video",
763
+ {
764
+ className: "h-full w-full object-cover",
765
+ controls: isActive,
766
+ preload: "metadata",
767
+ poster: item.poster,
768
+ tabIndex: isActive ? 0 : -1,
769
+ children: /* @__PURE__ */ jsxRuntime.jsx("source", { src: item.src })
770
+ }
771
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
772
+ ImgComponent,
773
+ {
774
+ src: item.src,
775
+ alt: item.alt ?? "Map marker media",
776
+ className: "h-full w-full object-cover",
777
+ loading: "eager",
778
+ optixFlowConfig
779
+ }
780
+ )
781
+ },
782
+ normalizeId(item.id, `media-slide-${index}`)
783
+ );
784
+ }) }),
785
+ totalItems > 1 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
786
+ /* @__PURE__ */ jsxRuntime.jsx(
787
+ "button",
788
+ {
789
+ type: "button",
790
+ "aria-label": "Show previous media",
791
+ className: "absolute left-4 top-1/2 inline-flex size-10 -translate-y-1/2 items-center justify-center rounded-2xl bg-card text-card-foreground shadow-lg border-4 border-black hover:border-white hover:bg-black hover:text-white transition-all duration-500 z-[2]",
792
+ onClick: () => {
793
+ setActiveIndex(
794
+ (current) => (current - 1 + totalItems) % totalItems
795
+ );
796
+ },
797
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconComponent, { name: "lucide/arrow-left", size: 18 })
798
+ }
799
+ ),
800
+ /* @__PURE__ */ jsxRuntime.jsx(
801
+ "button",
802
+ {
803
+ type: "button",
804
+ "aria-label": "Show next media",
805
+ className: "absolute right-4 top-1/2 inline-flex size-10 -translate-y-1/2 items-center justify-center rounded-2xl bg-card text-card-foreground shadow-lg border-4 border-black hover:border-white hover:bg-black hover:text-white transition-all duration-500 z-2",
806
+ onClick: () => {
807
+ setActiveIndex((current) => (current + 1) % totalItems);
808
+ },
809
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconComponent, { name: "lucide/arrow-right", size: 18 })
810
+ }
811
+ ),
812
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute bottom-2 left-1/2 flex -translate-x-1/2 items-center gap-1.5 z-[2]", children: mediaItems.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
813
+ "button",
814
+ {
815
+ type: "button",
816
+ "aria-label": `Show media item ${index + 1}`,
817
+ className: cn(
818
+ "h-2 rounded-full transition-all duration-300",
819
+ index === activeItemIndex ? "w-6 bg-card" : "w-2 bg-card opacity-50 hover:opacity-100"
820
+ ),
821
+ onClick: () => setActiveIndex(index)
822
+ },
823
+ normalizeId(item.id, `media-dot-${index}`)
824
+ )) })
825
+ ] }) : null
826
+ ] });
827
+ }
828
+ function getMarkerTitle(marker, markerIndex) {
829
+ if (marker.title !== void 0 && marker.title !== null) {
830
+ return marker.title;
831
+ }
832
+ if (marker.label !== void 0 && marker.label !== null) {
833
+ return marker.label;
834
+ }
835
+ return `Location ${markerIndex + 1}`;
836
+ }
837
+ function GeoMap({
838
+ className,
839
+ mapWrapperClassName,
840
+ mapClassName,
841
+ panelClassName,
842
+ panelPosition = "top-left",
843
+ stadiaApiKey = "",
844
+ mapStyle = "osm-bright",
845
+ styleUrl,
846
+ mapLibreCssHref,
847
+ markers = [],
848
+ clusters = [],
849
+ viewState,
850
+ defaultViewState,
851
+ onViewStateChange,
852
+ onMapClick,
853
+ onMarkerDrag,
854
+ showNavigationControl = true,
855
+ showGeolocateControl = false,
856
+ navigationControlPosition = "top-right",
857
+ geolocateControlPosition = "top-left",
858
+ flyToOptions,
859
+ markerFocusZoom = 14,
860
+ clusterFocusZoom = 5,
861
+ selectedMarkerId,
862
+ initialSelectedMarkerId,
863
+ onSelectionChange,
864
+ clearSelectionOnMapClick = true,
865
+ mapChildren,
866
+ optixFlowConfig,
867
+ IconComponent = FallbackIcon,
868
+ ImgComponent = FallbackImg
869
+ }) {
870
+ const normalizedStandaloneMarkers = React3__namespace.useMemo(
871
+ () => markers.map((marker, index) => ({
872
+ ...marker,
873
+ id: normalizeId(marker.id, `marker-${index}`)
874
+ })),
875
+ [markers]
876
+ );
877
+ const normalizedClusters = React3__namespace.useMemo(() => {
878
+ const results = [];
879
+ clusters.forEach((cluster, clusterIndex) => {
880
+ const clusterId = normalizeId(cluster.id, `cluster-${clusterIndex}`);
881
+ const normalizedClusterMarkers = cluster.markers.map(
882
+ (marker, markerIndex) => ({
883
+ ...marker,
884
+ id: normalizeId(marker.id, `${clusterId}-marker-${markerIndex}`),
885
+ clusterId
886
+ })
887
+ );
888
+ const clusterCenter = cluster.latitude !== void 0 && cluster.longitude !== void 0 ? { latitude: cluster.latitude, longitude: cluster.longitude } : buildClusterCenter(normalizedClusterMarkers);
889
+ if (!clusterCenter) {
890
+ return;
891
+ }
892
+ results.push({
893
+ ...cluster,
894
+ id: clusterId,
895
+ latitude: clusterCenter.latitude,
896
+ longitude: clusterCenter.longitude,
897
+ markers: normalizedClusterMarkers
898
+ });
899
+ });
900
+ return results;
901
+ }, [clusters]);
902
+ const markerLookup = React3__namespace.useMemo(() => {
903
+ const lookup = /* @__PURE__ */ new Map();
904
+ normalizedStandaloneMarkers.forEach((marker) => {
905
+ lookup.set(marker.id, marker);
906
+ });
907
+ normalizedClusters.forEach((cluster) => {
908
+ cluster.markers.forEach((marker) => {
909
+ lookup.set(marker.id, marker);
910
+ });
911
+ });
912
+ return lookup;
913
+ }, [normalizedClusters, normalizedStandaloneMarkers]);
914
+ const clusterLookup = React3__namespace.useMemo(() => {
915
+ const lookup = /* @__PURE__ */ new Map();
916
+ normalizedClusters.forEach((cluster) => {
917
+ lookup.set(cluster.id, cluster);
918
+ });
919
+ return lookup;
920
+ }, [normalizedClusters]);
921
+ const firstCoordinate = React3__namespace.useMemo(() => {
922
+ const allCoords = [];
923
+ normalizedStandaloneMarkers.forEach((marker) => {
924
+ allCoords.push({ latitude: marker.latitude, longitude: marker.longitude });
925
+ });
926
+ normalizedClusters.forEach((cluster) => {
927
+ allCoords.push({ latitude: cluster.latitude, longitude: cluster.longitude });
928
+ });
929
+ if (allCoords.length > 0) {
930
+ const sum = allCoords.reduce(
931
+ (acc, coord) => ({
932
+ latitude: acc.latitude + coord.latitude,
933
+ longitude: acc.longitude + coord.longitude
934
+ }),
935
+ { latitude: 0, longitude: 0 }
936
+ );
937
+ return {
938
+ latitude: sum.latitude / allCoords.length,
939
+ longitude: sum.longitude / allCoords.length
940
+ };
941
+ }
942
+ return {
943
+ latitude: DEFAULT_VIEW_STATE.latitude,
944
+ longitude: DEFAULT_VIEW_STATE.longitude
945
+ };
946
+ }, [normalizedClusters, normalizedStandaloneMarkers]);
947
+ const calculatedZoom = React3__namespace.useMemo(() => {
948
+ if (normalizedStandaloneMarkers.length + normalizedClusters.length <= 1) {
949
+ return markerFocusZoom;
950
+ }
951
+ const allCoords = [];
952
+ normalizedStandaloneMarkers.forEach((marker) => {
953
+ allCoords.push({ latitude: marker.latitude, longitude: marker.longitude });
954
+ });
955
+ normalizedClusters.forEach((cluster) => {
956
+ allCoords.push({ latitude: cluster.latitude, longitude: cluster.longitude });
957
+ });
958
+ if (allCoords.length === 0) {
959
+ return DEFAULT_VIEW_STATE.zoom;
960
+ }
961
+ const lats = allCoords.map((c) => c.latitude);
962
+ const lngs = allCoords.map((c) => c.longitude);
963
+ const latDiff = Math.max(...lats) - Math.min(...lats);
964
+ const lngDiff = Math.max(...lngs) - Math.min(...lngs);
965
+ const maxDiff = Math.max(latDiff, lngDiff);
966
+ if (maxDiff > 10) return 3;
967
+ if (maxDiff > 5) return 5;
968
+ if (maxDiff > 2) return 7;
969
+ if (maxDiff > 1) return 9;
970
+ if (maxDiff > 0.5) return 10;
971
+ if (maxDiff > 0.1) return 12;
972
+ return 13;
973
+ }, [normalizedClusters, normalizedStandaloneMarkers, markerFocusZoom]);
974
+ const [uncontrolledViewState, setUncontrolledViewState] = React3__namespace.useState({
975
+ latitude: defaultViewState?.latitude ?? firstCoordinate.latitude,
976
+ longitude: defaultViewState?.longitude ?? firstCoordinate.longitude,
977
+ zoom: defaultViewState?.zoom ?? calculatedZoom
978
+ });
979
+ React3__namespace.useEffect(() => {
980
+ if (!viewState && !defaultViewState) {
981
+ setUncontrolledViewState({
982
+ latitude: firstCoordinate.latitude,
983
+ longitude: firstCoordinate.longitude,
984
+ zoom: calculatedZoom
985
+ });
986
+ }
987
+ }, [firstCoordinate, calculatedZoom, viewState, defaultViewState]);
988
+ const isControlledViewState = viewState !== void 0;
989
+ const resolvedViewState = isControlledViewState ? viewState : uncontrolledViewState;
990
+ const applyViewState = React3__namespace.useCallback(
991
+ (nextState) => {
992
+ if (!isControlledViewState) {
993
+ setUncontrolledViewState((current) => {
994
+ const next = { ...current, ...nextState };
995
+ const hasChanged = current.latitude !== next.latitude || current.longitude !== next.longitude || current.zoom !== next.zoom;
996
+ return hasChanged ? next : current;
997
+ });
998
+ }
999
+ onViewStateChange?.(nextState);
1000
+ },
1001
+ [isControlledViewState, onViewStateChange]
1002
+ );
1003
+ const [selection, setSelection] = React3__namespace.useState(() => {
1004
+ if (initialSelectedMarkerId !== void 0 && initialSelectedMarkerId !== null) {
1005
+ return {
1006
+ type: "marker",
1007
+ markerId: String(initialSelectedMarkerId)
1008
+ };
1009
+ }
1010
+ return { type: "none" };
1011
+ });
1012
+ React3__namespace.useEffect(() => {
1013
+ if (selectedMarkerId === void 0 || selectedMarkerId === null) {
1014
+ return;
1015
+ }
1016
+ setSelection({
1017
+ type: "marker",
1018
+ markerId: String(selectedMarkerId)
1019
+ });
1020
+ }, [selectedMarkerId]);
1021
+ const selectedMarker = selection.markerId ? markerLookup.get(selection.markerId) : void 0;
1022
+ const selectedCluster = selection.clusterId ? clusterLookup.get(selection.clusterId) : void 0;
1023
+ React3__namespace.useEffect(() => {
1024
+ if (selection.type === "marker" && selection.markerId && !selectedMarker) {
1025
+ setSelection({ type: "none" });
1026
+ onSelectionChange?.({ type: "none" });
1027
+ }
1028
+ }, [onSelectionChange, selectedMarker, selection]);
1029
+ const emitSelectionChange = React3__namespace.useCallback(
1030
+ (nextSelection) => {
1031
+ if (nextSelection.type === "none") {
1032
+ onSelectionChange?.({ type: "none" });
1033
+ return;
1034
+ }
1035
+ if (nextSelection.type === "marker") {
1036
+ const parentCluster = nextSelection.marker.clusterId ? clusterLookup.get(nextSelection.marker.clusterId) : void 0;
1037
+ onSelectionChange?.({
1038
+ type: "marker",
1039
+ marker: nextSelection.marker,
1040
+ cluster: parentCluster
1041
+ });
1042
+ return;
1043
+ }
1044
+ onSelectionChange?.({
1045
+ type: "cluster",
1046
+ cluster: nextSelection.cluster
1047
+ });
1048
+ },
1049
+ [clusterLookup, onSelectionChange]
1050
+ );
1051
+ const selectMarker = React3__namespace.useCallback(
1052
+ (marker) => {
1053
+ setSelection({
1054
+ type: "marker",
1055
+ markerId: marker.id,
1056
+ clusterId: marker.clusterId
1057
+ });
1058
+ applyViewState({
1059
+ latitude: marker.latitude,
1060
+ longitude: marker.longitude,
1061
+ zoom: markerFocusZoom
1062
+ });
1063
+ emitSelectionChange({ type: "marker", marker });
1064
+ },
1065
+ [applyViewState, emitSelectionChange, markerFocusZoom]
1066
+ );
1067
+ const selectCluster = React3__namespace.useCallback(
1068
+ (cluster) => {
1069
+ setSelection({
1070
+ type: "cluster",
1071
+ clusterId: cluster.id
1072
+ });
1073
+ applyViewState({
1074
+ latitude: cluster.latitude,
1075
+ longitude: cluster.longitude,
1076
+ zoom: clusterFocusZoom
1077
+ });
1078
+ emitSelectionChange({ type: "cluster", cluster });
1079
+ },
1080
+ [applyViewState, clusterFocusZoom, emitSelectionChange]
1081
+ );
1082
+ const clearSelection = React3__namespace.useCallback(() => {
1083
+ setSelection({ type: "none" });
1084
+ emitSelectionChange({ type: "none" });
1085
+ }, [emitSelectionChange]);
1086
+ const mapMarkers = React3__namespace.useMemo(() => {
1087
+ const resolvedMarkers = [];
1088
+ normalizedClusters.forEach((cluster) => {
1089
+ const isSelected = selection.type === "cluster" && selection.clusterId === cluster.id;
1090
+ resolvedMarkers.push({
1091
+ id: `cluster-pin:${cluster.id}`,
1092
+ latitude: cluster.latitude,
1093
+ longitude: cluster.longitude,
1094
+ element: () => {
1095
+ const customMarkerElement = cluster.markerElement;
1096
+ const markerBody = typeof customMarkerElement === "function" ? customMarkerElement({
1097
+ isSelected,
1098
+ count: cluster.markers.length
1099
+ }) : customMarkerElement;
1100
+ return /* @__PURE__ */ jsxRuntime.jsx(
1101
+ "button",
1102
+ {
1103
+ type: "button",
1104
+ className: "group cursor-pointer",
1105
+ onClick: (event) => {
1106
+ event.preventDefault();
1107
+ event.stopPropagation();
1108
+ selectCluster(cluster);
1109
+ },
1110
+ "aria-label": `View ${cluster.markers.length} clustered locations`,
1111
+ children: markerBody ?? /* @__PURE__ */ jsxRuntime.jsx(
1112
+ "span",
1113
+ {
1114
+ className: cn(
1115
+ "inline-flex min-h-10 min-w-10 items-center justify-center rounded-full border-2 border-white px-2 text-xs font-semibold text-white shadow-lg transition-transform duration-200 group-hover:scale-105",
1116
+ isSelected && "ring-4 ring-primary/30",
1117
+ cluster.pinClassName
1118
+ ),
1119
+ style: {
1120
+ backgroundColor: cluster.pinColor ?? "var(--foreground)"
1121
+ },
1122
+ children: cluster.markers.length
1123
+ }
1124
+ )
1125
+ }
1126
+ );
1127
+ }
1128
+ });
1129
+ });
1130
+ normalizedStandaloneMarkers.forEach((marker) => {
1131
+ const isSelected = selection.type === "marker" && selection.markerId === marker.id;
1132
+ const customMarkerElement = marker.markerElement;
1133
+ resolvedMarkers.push({
1134
+ id: marker.id,
1135
+ latitude: marker.latitude,
1136
+ longitude: marker.longitude,
1137
+ draggable: marker.draggable,
1138
+ element: () => {
1139
+ const markerBody = typeof customMarkerElement === "function" ? customMarkerElement({ isSelected }) : customMarkerElement;
1140
+ return /* @__PURE__ */ jsxRuntime.jsx(
1141
+ "button",
1142
+ {
1143
+ type: "button",
1144
+ className: "group cursor-pointer",
1145
+ onClick: (event) => {
1146
+ event.preventDefault();
1147
+ event.stopPropagation();
1148
+ selectMarker(marker);
1149
+ },
1150
+ "aria-label": typeof marker.title === "string" ? `View ${marker.title}` : "View location details",
1151
+ children: markerBody ?? /* @__PURE__ */ jsxRuntime.jsx(
1152
+ "span",
1153
+ {
1154
+ className: cn(
1155
+ "inline-flex h-4 w-4 rounded-full border-2 border-white shadow-md transition-transform duration-200 group-hover:scale-110",
1156
+ isSelected && "h-5 w-5 ring-4 ring-primary/30",
1157
+ marker.pinClassName
1158
+ ),
1159
+ style: {
1160
+ backgroundColor: marker.pinColor ?? "#f43f5e"
1161
+ }
1162
+ }
1163
+ )
1164
+ }
1165
+ );
1166
+ }
1167
+ });
1168
+ });
1169
+ return resolvedMarkers;
1170
+ }, [
1171
+ normalizedClusters,
1172
+ normalizedStandaloneMarkers,
1173
+ selectCluster,
1174
+ selectMarker,
1175
+ selection
1176
+ ]);
1177
+ const renderMarkerPanel = () => {
1178
+ if (selectedMarker) {
1179
+ const markerMediaItems = selectedMarker.mediaItems ?? [];
1180
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1181
+ "div",
1182
+ {
1183
+ className: cn(
1184
+ "relative w-[min(24rem,calc(100vw-2rem))] overflow-hidden rounded-xl border border-border bg-card text-card-foreground shadow-2xl",
1185
+ panelClassName
1186
+ ),
1187
+ children: [
1188
+ /* @__PURE__ */ jsxRuntime.jsx(
1189
+ "button",
1190
+ {
1191
+ type: "button",
1192
+ "aria-label": "Close marker details",
1193
+ className: "flex size-12 items-center justify-center rounded-bl-lg rounded-br-0 rounded-t-0 bg-black text-white transition-all duration-500 absolute top-0 right-0 z-10 cursor-pointer ring-4 ring-white",
1194
+ onClick: clearSelection,
1195
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconComponent, { name: "lucide/x", size: 20 })
1196
+ }
1197
+ ),
1198
+ markerMediaItems.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1199
+ MarkerMediaCarousel,
1200
+ {
1201
+ mediaItems: markerMediaItems,
1202
+ optixFlowConfig,
1203
+ IconComponent,
1204
+ ImgComponent
1205
+ }
1206
+ ) : null,
1207
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 p-4", children: [
1208
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-start justify-between gap-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 space-y-1", children: [
1209
+ selectedMarker.eyebrow ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-semibold uppercase tracking-wide", children: selectedMarker.eyebrow }) : null,
1210
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-base font-semibold leading-tight", children: selectedMarker.title ?? selectedMarker.label ?? "Location" })
1211
+ ] }) }),
1212
+ selectedMarker.summary ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm leading-relaxed", children: selectedMarker.summary }) : null,
1213
+ selectedMarker.locationLine ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-row items-center justify-start text-sm gap-2", children: [
1214
+ /* @__PURE__ */ jsxRuntime.jsx(
1215
+ IconComponent,
1216
+ {
1217
+ name: "lucide:map-pin",
1218
+ className: "opacity-50",
1219
+ size: 18
1220
+ }
1221
+ ),
1222
+ typeof selectedMarker.locationLine === "string" ? /* @__PURE__ */ jsxRuntime.jsx(
1223
+ SimplePressable,
1224
+ {
1225
+ href: selectedMarker.locationUrl,
1226
+ className: cn(
1227
+ "transition-all duration-500",
1228
+ "font-medium opacity-75 hover:opacity-100",
1229
+ selectedMarker.locationUrl ? "underline underline-offset-4" : ""
1230
+ ),
1231
+ children: selectedMarker.locationLine
1232
+ }
1233
+ ) : selectedMarker.locationLine
1234
+ ] }) : null,
1235
+ selectedMarker.hoursLine ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-row items-center justify-start text-sm gap-2", children: [
1236
+ /* @__PURE__ */ jsxRuntime.jsx(
1237
+ IconComponent,
1238
+ {
1239
+ name: "lucide:clock",
1240
+ className: "opacity-50",
1241
+ size: 18
1242
+ }
1243
+ ),
1244
+ typeof selectedMarker.hoursLine === "string" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium", children: selectedMarker.hoursLine }) : selectedMarker.hoursLine
1245
+ ] }) : null,
1246
+ selectedMarker.markerContentComponent ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative", children: selectedMarker.markerContentComponent }) : null,
1247
+ /* @__PURE__ */ jsxRuntime.jsx(MarkerActions, { actions: selectedMarker.actions })
1248
+ ] })
1249
+ ]
1250
+ }
1251
+ );
1252
+ }
1253
+ if (selectedCluster) {
1254
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1255
+ "div",
1256
+ {
1257
+ className: cn(
1258
+ "relative w-[min(24rem,calc(100vw-2rem))] overflow-hidden rounded-xl border border-border bg-card text-card-foreground p-4 shadow-2xl",
1259
+ panelClassName
1260
+ ),
1261
+ children: [
1262
+ /* @__PURE__ */ jsxRuntime.jsx(
1263
+ "button",
1264
+ {
1265
+ type: "button",
1266
+ "aria-label": "Close cluster details",
1267
+ className: "flex size-8 items-center justify-center rounded-full border border-border bg-card text-card-foreground transition hover:bg-muted hover:text-foreground absolute top-2 right-2 z-10",
1268
+ onClick: clearSelection,
1269
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconComponent, { name: "lucide/x", size: 20 })
1270
+ }
1271
+ ),
1272
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-3 flex items-start justify-between gap-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
1273
+ selectedCluster.label ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: selectedCluster.label }) : null,
1274
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-base font-semibold leading-tight text-foreground", children: selectedCluster.title ?? "Clustered Locations" }),
1275
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: selectedCluster.summary ?? `${selectedCluster.markers.length} location${selectedCluster.markers.length === 1 ? "" : "s"} in this cluster.` })
1276
+ ] }) }),
1277
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-h-56 space-y-2 overflow-y-auto pr-1", children: selectedCluster.markers.map((marker, markerIndex) => /* @__PURE__ */ jsxRuntime.jsxs(
1278
+ "button",
1279
+ {
1280
+ type: "button",
1281
+ className: "w-full rounded-lg border border-border/60 p-3 text-left transition hover:border-border hover:bg-muted/50",
1282
+ onClick: () => selectMarker(marker),
1283
+ children: [
1284
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "line-clamp-1 text-sm font-semibold text-foreground", children: getMarkerTitle(marker, markerIndex) }),
1285
+ marker.summary ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 line-clamp-2 text-xs text-muted-foreground", children: marker.summary }) : null
1286
+ ]
1287
+ },
1288
+ marker.id
1289
+ )) })
1290
+ ]
1291
+ }
1292
+ );
1293
+ }
1294
+ return null;
1295
+ };
1296
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1297
+ "div",
1298
+ {
1299
+ className: cn(
1300
+ "relative overflow-hidden rounded-2xl border border-border bg-background",
1301
+ className
1302
+ ),
1303
+ children: [
1304
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("h-[520px] w-full", mapWrapperClassName), children: /* @__PURE__ */ jsxRuntime.jsx(
1305
+ MapLibre,
1306
+ {
1307
+ stadiaApiKey,
1308
+ mapStyle,
1309
+ styleUrl,
1310
+ mapLibreCssHref,
1311
+ viewState: resolvedViewState,
1312
+ onViewStateChange: applyViewState,
1313
+ markers: mapMarkers,
1314
+ onClick: (coord) => {
1315
+ onMapClick?.(coord);
1316
+ if (clearSelectionOnMapClick) {
1317
+ clearSelection();
1318
+ }
1319
+ },
1320
+ onMarkerDrag,
1321
+ showNavigationControl,
1322
+ showGeolocateControl,
1323
+ navigationControlPosition,
1324
+ geolocateControlPosition,
1325
+ flyToOptions,
1326
+ className: cn("h-full w-full", mapClassName),
1327
+ children: mapChildren
1328
+ }
1329
+ ) }),
1330
+ selection.type !== "none" ? /* @__PURE__ */ jsxRuntime.jsx(
1331
+ "div",
1332
+ {
1333
+ className: cn(
1334
+ "pointer-events-none absolute z-20",
1335
+ PANEL_POSITION_CLASS[panelPosition]
1336
+ ),
1337
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-auto", children: renderMarkerPanel() })
1338
+ }
1339
+ ) : null
1340
+ ]
1341
+ }
1342
+ );
1343
+ }
1344
+
1345
+ exports.GeoMap = GeoMap;
1346
+ exports.MapMarker = MapMarker;
1347
+ exports.NeutralMapMarker = NeutralMapMarker;
1348
+ exports.createMapMarkerElement = createMapMarkerElement;
1349
+ //# sourceMappingURL=index.cjs.map
1350
+ //# sourceMappingURL=index.cjs.map