@page-speed/maps 0.1.4 → 0.1.6

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 (46) hide show
  1. package/README.md +120 -0
  2. package/dist/components/geo-map.cjs +1237 -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 +1216 -0
  7. package/dist/components/geo-map.js.map +1 -0
  8. package/dist/components/index.cjs +1359 -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 +1335 -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/index.cjs +929 -21
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +2 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +910 -21
  25. package/dist/index.js.map +1 -1
  26. package/dist/types/index.d.cts +5 -5
  27. package/dist/types/index.d.ts +5 -5
  28. package/dist/utils/cn.cjs +13 -0
  29. package/dist/utils/cn.cjs.map +1 -0
  30. package/dist/utils/cn.d.cts +16 -0
  31. package/dist/utils/cn.d.ts +16 -0
  32. package/dist/utils/cn.js +11 -0
  33. package/dist/utils/cn.js.map +1 -0
  34. package/dist/utils/index.cjs +63 -0
  35. package/dist/utils/index.cjs.map +1 -1
  36. package/dist/utils/index.d.cts +4 -0
  37. package/dist/utils/index.d.ts +4 -0
  38. package/dist/utils/index.js +42 -1
  39. package/dist/utils/index.js.map +1 -1
  40. package/dist/utils/simple-pressable.cjs +63 -0
  41. package/dist/utils/simple-pressable.cjs.map +1 -0
  42. package/dist/utils/simple-pressable.d.cts +20 -0
  43. package/dist/utils/simple-pressable.d.ts +20 -0
  44. package/dist/utils/simple-pressable.js +41 -0
  45. package/dist/utils/simple-pressable.js.map +1 -0
  46. package/package.json +29 -2
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
- import React, { useMemo } from 'react';
2
- import { Marker, Map, GeolocateControl, NavigationControl } from 'react-map-gl/maplibre';
3
- import { jsx, jsxs } from 'react/jsx-runtime';
1
+ import * as React3 from 'react';
2
+ import React3__default, { useMemo } from 'react';
3
+ import { Marker, Map as Map$1, GeolocateControl, NavigationControl } from 'react-map-gl/maplibre';
4
+ import { clsx } from 'clsx';
5
+ import { twMerge } from 'tailwind-merge';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
7
 
5
8
  // src/core/MapLibre.tsx
6
9
 
@@ -79,6 +82,42 @@ function generateGoogleMapLink(latitude, longitude, zoom = 15) {
79
82
  function generateGoogleDirectionsLink(latitude, longitude) {
80
83
  return `https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`;
81
84
  }
85
+ function cn(...inputs) {
86
+ return twMerge(clsx(inputs));
87
+ }
88
+ var SimplePressable = React3.forwardRef(({ children, className, href, onClick, ...props }, ref) => {
89
+ if (href) {
90
+ const isExternal = href.startsWith("http://") || href.startsWith("https://");
91
+ return /* @__PURE__ */ jsx(
92
+ "a",
93
+ {
94
+ ref,
95
+ href,
96
+ className,
97
+ target: isExternal ? "_blank" : props.target,
98
+ rel: isExternal ? "noopener noreferrer" : props.rel,
99
+ onClick,
100
+ ...props,
101
+ children
102
+ }
103
+ );
104
+ }
105
+ if (onClick) {
106
+ return /* @__PURE__ */ jsx(
107
+ "button",
108
+ {
109
+ ref,
110
+ type: "button",
111
+ className,
112
+ onClick,
113
+ ...props,
114
+ children
115
+ }
116
+ );
117
+ }
118
+ return /* @__PURE__ */ jsx("span", { className, children });
119
+ });
120
+ SimplePressable.displayName = "SimplePressable";
82
121
  var DEFAULT_MAPLIBRE_CSS_HREF = "https://cdn.jsdelivr.net/npm/maplibre-gl@5.18.0/dist/maplibre-gl.css";
83
122
  var MAPLIBRE_STYLESHEET_ID = "page-speed-maplibre-gl-css";
84
123
  var DEFAULT_FLY_TO_OPTIONS = Object.freeze({});
@@ -205,18 +244,18 @@ function MapLibre({
205
244
  geolocateControlPosition = "top-left",
206
245
  flyToOptions = DEFAULT_FLY_TO_OPTIONS
207
246
  }) {
208
- const mapRef = React.useRef(null);
247
+ const mapRef = React3__default.useRef(null);
209
248
  const resolvedMapLibreCssHref = mapLibreCssHref && mapLibreCssHref.trim().length > 0 ? mapLibreCssHref : DEFAULT_MAPLIBRE_CSS_HREF;
210
- const [internalViewState, setInternalViewState] = React.useState({
249
+ const [internalViewState, setInternalViewState] = React3__default.useState({
211
250
  latitude: viewState?.latitude ?? center.lat,
212
251
  longitude: viewState?.longitude ?? center.lng,
213
252
  zoom: viewState?.zoom ?? zoom
214
253
  });
215
- const isUserInteracting = React.useRef(false);
216
- const isMarkerDragging = React.useRef(false);
217
- const dragAnimationFrame = React.useRef(null);
218
- const lastReportedViewState = React.useRef(null);
219
- const resolvedFlyToOptions = React.useMemo(
254
+ const isUserInteracting = React3__default.useRef(false);
255
+ const isMarkerDragging = React3__default.useRef(false);
256
+ const dragAnimationFrame = React3__default.useRef(null);
257
+ const lastReportedViewState = React3__default.useRef(null);
258
+ const resolvedFlyToOptions = React3__default.useMemo(
220
259
  () => ({
221
260
  speed: flyToOptions.speed ?? 0.8,
222
261
  curve: flyToOptions.curve ?? 1.2,
@@ -230,10 +269,10 @@ function MapLibre({
230
269
  flyToOptions.speed
231
270
  ]
232
271
  );
233
- React.useEffect(() => {
272
+ React3__default.useEffect(() => {
234
273
  ensureMapLibreStylesheet(resolvedMapLibreCssHref);
235
274
  }, [resolvedMapLibreCssHref]);
236
- React.useEffect(() => {
275
+ React3__default.useEffect(() => {
237
276
  if (!mapRef.current || !viewState || isUserInteracting.current || isMarkerDragging.current) {
238
277
  return;
239
278
  }
@@ -267,10 +306,10 @@ function MapLibre({
267
306
  viewState?.longitude,
268
307
  viewState?.zoom
269
308
  ]);
270
- const handleMoveStart = React.useCallback(() => {
309
+ const handleMoveStart = React3__default.useCallback(() => {
271
310
  isUserInteracting.current = true;
272
311
  }, []);
273
- const handleMove = React.useCallback(
312
+ const handleMove = React3__default.useCallback(
274
313
  (event) => {
275
314
  const nextViewState = event.viewState;
276
315
  setInternalViewState({
@@ -288,7 +327,7 @@ function MapLibre({
288
327
  },
289
328
  [onViewStateChange]
290
329
  );
291
- const handleMoveEnd = React.useCallback(
330
+ const handleMoveEnd = React3__default.useCallback(
292
331
  (event) => {
293
332
  isUserInteracting.current = false;
294
333
  if (!onMoveEnd) {
@@ -309,7 +348,7 @@ function MapLibre({
309
348
  },
310
349
  [onMoveEnd]
311
350
  );
312
- const handleMapClick = React.useCallback(
351
+ const handleMapClick = React3__default.useCallback(
313
352
  (event) => {
314
353
  if (!onClick) {
315
354
  return;
@@ -318,11 +357,11 @@ function MapLibre({
318
357
  },
319
358
  [onClick]
320
359
  );
321
- const normalizedMarkers = React.useMemo(
360
+ const normalizedMarkers = React3__default.useMemo(
322
361
  () => normalizeMarkers(markers),
323
362
  [markers]
324
363
  );
325
- const markerElements = React.useMemo(
364
+ const markerElements = React3__default.useMemo(
326
365
  () => normalizedMarkers.map((marker) => /* @__PURE__ */ jsx(
327
366
  Marker,
328
367
  {
@@ -409,7 +448,7 @@ function MapLibre({
409
448
  )),
410
449
  [normalizedMarkers, onMarkerDrag]
411
450
  );
412
- const resolvedMapStyleUrl = React.useMemo(() => {
451
+ const resolvedMapStyleUrl = React3__default.useMemo(() => {
413
452
  if (styleUrl) {
414
453
  return appendStadiaApiKey(styleUrl, stadiaApiKey);
415
454
  }
@@ -424,7 +463,7 @@ function MapLibre({
424
463
  className: joinClassNames("relative w-full h-full", className),
425
464
  style: { width: "100%", height: "100%", ...style },
426
465
  children: /* @__PURE__ */ jsxs(
427
- Map,
466
+ Map$1,
428
467
  {
429
468
  ref: mapRef,
430
469
  ...internalViewState,
@@ -546,7 +585,857 @@ function useDefaultZoom(options) {
546
585
  [coordinates, mapWidth, mapHeight, padding, maxZoom, minZoom]
547
586
  );
548
587
  }
588
+ var SIZE_CONFIG = {
589
+ sm: {
590
+ outer: "size-10",
591
+ middle: "size-7",
592
+ inner: "size-5",
593
+ dot: "size-2"
594
+ },
595
+ md: {
596
+ outer: "size-14",
597
+ middle: "size-10",
598
+ inner: "size-7",
599
+ dot: "size-2.5"
600
+ },
601
+ lg: {
602
+ outer: "size-20",
603
+ middle: "size-14",
604
+ inner: "size-10",
605
+ dot: "size-3.5"
606
+ }
607
+ };
608
+ function MapMarker({
609
+ size = "md",
610
+ isSelected = false,
611
+ dotColor,
612
+ innerRingColor,
613
+ middleRingColor,
614
+ outerRingColor,
615
+ className,
616
+ onClick,
617
+ interactive = true,
618
+ "aria-label": ariaLabel = "Map location marker"
619
+ }) {
620
+ const sizeConfig = SIZE_CONFIG[size];
621
+ const content = /* @__PURE__ */ jsxs(
622
+ "div",
623
+ {
624
+ className: cn(
625
+ "relative flex items-center justify-center rounded-full transition-transform duration-200",
626
+ sizeConfig.outer,
627
+ isSelected && "scale-110",
628
+ className
629
+ ),
630
+ style: { backgroundColor: outerRingColor },
631
+ children: [
632
+ /* @__PURE__ */ jsx(
633
+ "div",
634
+ {
635
+ className: cn(
636
+ "absolute rounded-full transition-all duration-200",
637
+ sizeConfig.middle
638
+ ),
639
+ style: { backgroundColor: middleRingColor }
640
+ }
641
+ ),
642
+ /* @__PURE__ */ jsx(
643
+ "div",
644
+ {
645
+ className: cn(
646
+ "absolute rounded-full transition-all duration-200",
647
+ sizeConfig.inner
648
+ ),
649
+ style: { backgroundColor: innerRingColor }
650
+ }
651
+ ),
652
+ /* @__PURE__ */ jsx(
653
+ "div",
654
+ {
655
+ className: cn(
656
+ "absolute rounded-full transition-all duration-200",
657
+ sizeConfig.dot
658
+ ),
659
+ style: { backgroundColor: dotColor }
660
+ }
661
+ )
662
+ ]
663
+ }
664
+ );
665
+ if (!interactive) {
666
+ return content;
667
+ }
668
+ return /* @__PURE__ */ jsx(
669
+ "button",
670
+ {
671
+ type: "button",
672
+ className: "group cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-full",
673
+ onClick,
674
+ "aria-label": ariaLabel,
675
+ children: /* @__PURE__ */ jsx(
676
+ "div",
677
+ {
678
+ className: cn(
679
+ "transition-transform duration-200 group-hover:scale-110",
680
+ isSelected && "scale-110"
681
+ ),
682
+ children: content
683
+ }
684
+ )
685
+ }
686
+ );
687
+ }
688
+ function NeutralMapMarker(props) {
689
+ return /* @__PURE__ */ jsx(
690
+ MapMarker,
691
+ {
692
+ ...props,
693
+ dotColor: "hsl(var(--neutral-900, 0 0% 9%))",
694
+ innerRingColor: "hsl(var(--neutral-400, 0 0% 64%))",
695
+ middleRingColor: "hsl(var(--neutral-300, 0 0% 78%))",
696
+ outerRingColor: "hsl(var(--neutral-200, 0 0% 88%))"
697
+ }
698
+ );
699
+ }
700
+ function createMapMarkerElement(config) {
701
+ return function MarkerElement({ isSelected }) {
702
+ return /* @__PURE__ */ jsx(MapMarker, { ...config, isSelected, interactive: false });
703
+ };
704
+ }
705
+ var PANEL_POSITION_CLASS = {
706
+ "top-left": "left-4 top-4",
707
+ "top-right": "right-4 top-4",
708
+ "bottom-left": "bottom-4 left-4",
709
+ "bottom-right": "bottom-4 right-4"
710
+ };
711
+ var DEFAULT_VIEW_STATE = {
712
+ latitude: 39.5,
713
+ longitude: -98.35,
714
+ zoom: 3
715
+ };
716
+ var VIDEO_FILE_EXTENSION_REGEX = /\.(mp4|webm|ogg|mov|m4v|m3u8)(\?.*)?$/i;
717
+ function resolveMediaType(item) {
718
+ if (item.type) {
719
+ return item.type;
720
+ }
721
+ return VIDEO_FILE_EXTENSION_REGEX.test(item.src) ? "video" : "image";
722
+ }
723
+ function normalizeId(value, fallback) {
724
+ if (value === null || value === void 0 || value === "") {
725
+ return fallback;
726
+ }
727
+ return String(value);
728
+ }
729
+ function buildClusterCenter(markers) {
730
+ if (!markers.length) {
731
+ return null;
732
+ }
733
+ const total = markers.reduce(
734
+ (accumulator, marker) => ({
735
+ latitude: accumulator.latitude + marker.latitude,
736
+ longitude: accumulator.longitude + marker.longitude
737
+ }),
738
+ { latitude: 0, longitude: 0 }
739
+ );
740
+ return {
741
+ latitude: total.latitude / markers.length,
742
+ longitude: total.longitude / markers.length
743
+ };
744
+ }
745
+ function resolveActionKey(action, index) {
746
+ if (typeof action.label === "string" && action.label.trim().length > 0) {
747
+ return `label:${action.label}:${index}`;
748
+ }
749
+ if (action.href) {
750
+ return `href:${action.href}:${index}`;
751
+ }
752
+ return `action:${index}`;
753
+ }
754
+ var FallbackIcon = ({ size = 20, className }) => /* @__PURE__ */ jsx(
755
+ "svg",
756
+ {
757
+ width: size,
758
+ height: size,
759
+ viewBox: "0 0 24 24",
760
+ fill: "none",
761
+ stroke: "currentColor",
762
+ strokeWidth: "2",
763
+ strokeLinecap: "round",
764
+ strokeLinejoin: "round",
765
+ className,
766
+ children: /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" })
767
+ }
768
+ );
769
+ var FallbackImg = ({ src, alt, className, loading }) => /* @__PURE__ */ jsx("img", { src, alt, className, loading });
770
+ function MarkerActions({ actions }) {
771
+ if (!actions || actions.length === 0) {
772
+ return null;
773
+ }
774
+ return /* @__PURE__ */ jsx("div", { className: "mt-4 flex flex-wrap gap-2", children: actions.map((action, index) => {
775
+ const {
776
+ label,
777
+ icon,
778
+ iconAfter,
779
+ children,
780
+ href,
781
+ onClick,
782
+ className: actionClassName,
783
+ variant,
784
+ size,
785
+ asButton,
786
+ ...rest
787
+ } = action;
788
+ const buttonStyles = cn(
789
+ "inline-flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-colors",
790
+ variant === "outline" ? "border border-border bg-background hover:bg-muted" : "bg-primary text-primary-foreground hover:bg-primary/90",
791
+ size === "sm" && "text-sm px-3 py-1.5",
792
+ size === "icon" && "p-2",
793
+ actionClassName
794
+ );
795
+ return /* @__PURE__ */ jsx(
796
+ SimplePressable,
797
+ {
798
+ href,
799
+ onClick,
800
+ className: buttonStyles,
801
+ ...rest,
802
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
803
+ icon,
804
+ label,
805
+ iconAfter
806
+ ] })
807
+ },
808
+ resolveActionKey(action, index)
809
+ );
810
+ }) });
811
+ }
812
+ function MarkerMediaCarousel({
813
+ mediaItems,
814
+ optixFlowConfig,
815
+ IconComponent = FallbackIcon,
816
+ ImgComponent = FallbackImg
817
+ }) {
818
+ const [activeIndex, setActiveIndex] = React3.useState(0);
819
+ const totalItems = mediaItems.length;
820
+ const mediaResetKey = React3.useMemo(
821
+ () => mediaItems.map((item, index) => {
822
+ const itemId = normalizeId(item.id, `media-${index}`);
823
+ return `${itemId}:${item.src}:${item.type ?? ""}:${item.poster ?? ""}`;
824
+ }).join("|"),
825
+ [mediaItems]
826
+ );
827
+ const activeItemIndex = Math.min(activeIndex, Math.max(0, totalItems - 1));
828
+ React3.useEffect(() => {
829
+ setActiveIndex(0);
830
+ }, [mediaResetKey]);
831
+ return /* @__PURE__ */ jsxs("div", { className: "relative border-b border-border/60 bg-muted/40", children: [
832
+ /* @__PURE__ */ jsx("div", { className: "relative aspect-video w-full overflow-hidden", children: mediaItems.map((item, index) => {
833
+ const isActive = index === activeItemIndex;
834
+ const mediaType = resolveMediaType(item);
835
+ return /* @__PURE__ */ jsx(
836
+ "div",
837
+ {
838
+ "aria-hidden": !isActive,
839
+ className: cn(
840
+ "absolute inset-0 transition-opacity duration-500 ease-in-out",
841
+ isActive ? "opacity-100 z-1" : "opacity-0 z-0 pointer-events-none"
842
+ ),
843
+ children: mediaType === "video" ? /* @__PURE__ */ jsx(
844
+ "video",
845
+ {
846
+ className: "h-full w-full object-cover",
847
+ controls: isActive,
848
+ preload: "metadata",
849
+ poster: item.poster,
850
+ tabIndex: isActive ? 0 : -1,
851
+ children: /* @__PURE__ */ jsx("source", { src: item.src })
852
+ }
853
+ ) : /* @__PURE__ */ jsx(
854
+ ImgComponent,
855
+ {
856
+ src: item.src,
857
+ alt: item.alt ?? "Map marker media",
858
+ className: "h-full w-full object-cover",
859
+ loading: "eager",
860
+ optixFlowConfig
861
+ }
862
+ )
863
+ },
864
+ normalizeId(item.id, `media-slide-${index}`)
865
+ );
866
+ }) }),
867
+ totalItems > 1 ? /* @__PURE__ */ jsxs(Fragment, { children: [
868
+ /* @__PURE__ */ jsx(
869
+ "button",
870
+ {
871
+ type: "button",
872
+ "aria-label": "Show previous media",
873
+ 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",
874
+ onClick: () => {
875
+ setActiveIndex(
876
+ (current) => (current - 1 + totalItems) % totalItems
877
+ );
878
+ },
879
+ children: /* @__PURE__ */ jsx(IconComponent, { name: "lucide/arrow-left", size: 18 })
880
+ }
881
+ ),
882
+ /* @__PURE__ */ jsx(
883
+ "button",
884
+ {
885
+ type: "button",
886
+ "aria-label": "Show next media",
887
+ 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",
888
+ onClick: () => {
889
+ setActiveIndex((current) => (current + 1) % totalItems);
890
+ },
891
+ children: /* @__PURE__ */ jsx(IconComponent, { name: "lucide/arrow-right", size: 18 })
892
+ }
893
+ ),
894
+ /* @__PURE__ */ 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__ */ jsx(
895
+ "button",
896
+ {
897
+ type: "button",
898
+ "aria-label": `Show media item ${index + 1}`,
899
+ className: cn(
900
+ "h-2 rounded-full transition-all duration-300",
901
+ index === activeItemIndex ? "w-6 bg-card" : "w-2 bg-card opacity-50 hover:opacity-100"
902
+ ),
903
+ onClick: () => setActiveIndex(index)
904
+ },
905
+ normalizeId(item.id, `media-dot-${index}`)
906
+ )) })
907
+ ] }) : null
908
+ ] });
909
+ }
910
+ function getMarkerTitle(marker, markerIndex) {
911
+ if (marker.title !== void 0 && marker.title !== null) {
912
+ return marker.title;
913
+ }
914
+ if (marker.label !== void 0 && marker.label !== null) {
915
+ return marker.label;
916
+ }
917
+ return `Location ${markerIndex + 1}`;
918
+ }
919
+ function GeoMap({
920
+ className,
921
+ mapWrapperClassName,
922
+ mapClassName,
923
+ panelClassName,
924
+ panelPosition = "top-left",
925
+ stadiaApiKey = "",
926
+ mapStyle = "osm-bright",
927
+ styleUrl,
928
+ mapLibreCssHref,
929
+ markers = [],
930
+ clusters = [],
931
+ viewState,
932
+ defaultViewState,
933
+ onViewStateChange,
934
+ onMapClick,
935
+ onMarkerDrag,
936
+ showNavigationControl = true,
937
+ showGeolocateControl = false,
938
+ navigationControlPosition = "top-right",
939
+ geolocateControlPosition = "top-left",
940
+ flyToOptions,
941
+ markerFocusZoom = 14,
942
+ clusterFocusZoom = 5,
943
+ selectedMarkerId,
944
+ initialSelectedMarkerId,
945
+ onSelectionChange,
946
+ clearSelectionOnMapClick = true,
947
+ mapChildren,
948
+ optixFlowConfig,
949
+ IconComponent = FallbackIcon,
950
+ ImgComponent = FallbackImg
951
+ }) {
952
+ const normalizedStandaloneMarkers = React3.useMemo(
953
+ () => markers.map((marker, index) => ({
954
+ ...marker,
955
+ id: normalizeId(marker.id, `marker-${index}`)
956
+ })),
957
+ [markers]
958
+ );
959
+ const normalizedClusters = React3.useMemo(() => {
960
+ const results = [];
961
+ clusters.forEach((cluster, clusterIndex) => {
962
+ const clusterId = normalizeId(cluster.id, `cluster-${clusterIndex}`);
963
+ const normalizedClusterMarkers = cluster.markers.map(
964
+ (marker, markerIndex) => ({
965
+ ...marker,
966
+ id: normalizeId(marker.id, `${clusterId}-marker-${markerIndex}`),
967
+ clusterId
968
+ })
969
+ );
970
+ const clusterCenter = cluster.latitude !== void 0 && cluster.longitude !== void 0 ? { latitude: cluster.latitude, longitude: cluster.longitude } : buildClusterCenter(normalizedClusterMarkers);
971
+ if (!clusterCenter) {
972
+ return;
973
+ }
974
+ results.push({
975
+ ...cluster,
976
+ id: clusterId,
977
+ latitude: clusterCenter.latitude,
978
+ longitude: clusterCenter.longitude,
979
+ markers: normalizedClusterMarkers
980
+ });
981
+ });
982
+ return results;
983
+ }, [clusters]);
984
+ const markerLookup = React3.useMemo(() => {
985
+ const lookup = /* @__PURE__ */ new Map();
986
+ normalizedStandaloneMarkers.forEach((marker) => {
987
+ lookup.set(marker.id, marker);
988
+ });
989
+ normalizedClusters.forEach((cluster) => {
990
+ cluster.markers.forEach((marker) => {
991
+ lookup.set(marker.id, marker);
992
+ });
993
+ });
994
+ return lookup;
995
+ }, [normalizedClusters, normalizedStandaloneMarkers]);
996
+ const clusterLookup = React3.useMemo(() => {
997
+ const lookup = /* @__PURE__ */ new Map();
998
+ normalizedClusters.forEach((cluster) => {
999
+ lookup.set(cluster.id, cluster);
1000
+ });
1001
+ return lookup;
1002
+ }, [normalizedClusters]);
1003
+ const firstCoordinate = React3.useMemo(() => {
1004
+ const allCoords = [];
1005
+ normalizedStandaloneMarkers.forEach((marker) => {
1006
+ allCoords.push({
1007
+ latitude: marker.latitude,
1008
+ longitude: marker.longitude
1009
+ });
1010
+ });
1011
+ normalizedClusters.forEach((cluster) => {
1012
+ allCoords.push({
1013
+ latitude: cluster.latitude,
1014
+ longitude: cluster.longitude
1015
+ });
1016
+ });
1017
+ if (allCoords.length > 0) {
1018
+ const sum = allCoords.reduce(
1019
+ (acc, coord) => ({
1020
+ latitude: acc.latitude + coord.latitude,
1021
+ longitude: acc.longitude + coord.longitude
1022
+ }),
1023
+ { latitude: 0, longitude: 0 }
1024
+ );
1025
+ return {
1026
+ latitude: sum.latitude / allCoords.length,
1027
+ longitude: sum.longitude / allCoords.length
1028
+ };
1029
+ }
1030
+ return {
1031
+ latitude: DEFAULT_VIEW_STATE.latitude,
1032
+ longitude: DEFAULT_VIEW_STATE.longitude
1033
+ };
1034
+ }, [normalizedClusters, normalizedStandaloneMarkers]);
1035
+ const calculatedZoom = React3.useMemo(() => {
1036
+ if (normalizedStandaloneMarkers.length + normalizedClusters.length <= 1) {
1037
+ return markerFocusZoom;
1038
+ }
1039
+ const allCoords = [];
1040
+ normalizedStandaloneMarkers.forEach((marker) => {
1041
+ allCoords.push({
1042
+ latitude: marker.latitude,
1043
+ longitude: marker.longitude
1044
+ });
1045
+ });
1046
+ normalizedClusters.forEach((cluster) => {
1047
+ allCoords.push({
1048
+ latitude: cluster.latitude,
1049
+ longitude: cluster.longitude
1050
+ });
1051
+ });
1052
+ if (allCoords.length === 0) {
1053
+ return DEFAULT_VIEW_STATE.zoom;
1054
+ }
1055
+ const lats = allCoords.map((c) => c.latitude);
1056
+ const lngs = allCoords.map((c) => c.longitude);
1057
+ const latDiff = Math.max(...lats) - Math.min(...lats);
1058
+ const lngDiff = Math.max(...lngs) - Math.min(...lngs);
1059
+ const maxDiff = Math.max(latDiff, lngDiff);
1060
+ if (maxDiff > 10) return 3;
1061
+ if (maxDiff > 5) return 5;
1062
+ if (maxDiff > 2) return 7;
1063
+ if (maxDiff > 1) return 9;
1064
+ if (maxDiff > 0.5) return 10;
1065
+ if (maxDiff > 0.1) return 12;
1066
+ return 13;
1067
+ }, [normalizedClusters, normalizedStandaloneMarkers, markerFocusZoom]);
1068
+ const [uncontrolledViewState, setUncontrolledViewState] = React3.useState({
1069
+ latitude: defaultViewState?.latitude ?? firstCoordinate.latitude,
1070
+ longitude: defaultViewState?.longitude ?? firstCoordinate.longitude,
1071
+ zoom: defaultViewState?.zoom ?? calculatedZoom
1072
+ });
1073
+ React3.useEffect(() => {
1074
+ if (!viewState && !defaultViewState) {
1075
+ setUncontrolledViewState({
1076
+ latitude: firstCoordinate.latitude,
1077
+ longitude: firstCoordinate.longitude,
1078
+ zoom: calculatedZoom
1079
+ });
1080
+ }
1081
+ }, [firstCoordinate, calculatedZoom, viewState, defaultViewState]);
1082
+ const isControlledViewState = viewState !== void 0;
1083
+ const resolvedViewState = isControlledViewState ? viewState : uncontrolledViewState;
1084
+ const applyViewState = React3.useCallback(
1085
+ (nextState) => {
1086
+ if (!isControlledViewState) {
1087
+ setUncontrolledViewState((current) => {
1088
+ const next = { ...current, ...nextState };
1089
+ const hasChanged = current.latitude !== next.latitude || current.longitude !== next.longitude || current.zoom !== next.zoom;
1090
+ return hasChanged ? next : current;
1091
+ });
1092
+ }
1093
+ onViewStateChange?.(nextState);
1094
+ },
1095
+ [isControlledViewState, onViewStateChange]
1096
+ );
1097
+ const [selection, setSelection] = React3.useState(() => {
1098
+ if (initialSelectedMarkerId !== void 0 && initialSelectedMarkerId !== null) {
1099
+ return {
1100
+ type: "marker",
1101
+ markerId: String(initialSelectedMarkerId)
1102
+ };
1103
+ }
1104
+ return { type: "none" };
1105
+ });
1106
+ React3.useEffect(() => {
1107
+ if (selectedMarkerId === void 0 || selectedMarkerId === null) {
1108
+ return;
1109
+ }
1110
+ setSelection({
1111
+ type: "marker",
1112
+ markerId: String(selectedMarkerId)
1113
+ });
1114
+ }, [selectedMarkerId]);
1115
+ const selectedMarker = selection.markerId ? markerLookup.get(selection.markerId) : void 0;
1116
+ const selectedCluster = selection.clusterId ? clusterLookup.get(selection.clusterId) : void 0;
1117
+ React3.useEffect(() => {
1118
+ if (selection.type === "marker" && selection.markerId && !selectedMarker) {
1119
+ setSelection({ type: "none" });
1120
+ onSelectionChange?.({ type: "none" });
1121
+ }
1122
+ }, [onSelectionChange, selectedMarker, selection]);
1123
+ const emitSelectionChange = React3.useCallback(
1124
+ (nextSelection) => {
1125
+ if (nextSelection.type === "none") {
1126
+ onSelectionChange?.({ type: "none" });
1127
+ return;
1128
+ }
1129
+ if (nextSelection.type === "marker") {
1130
+ const parentCluster = nextSelection.marker.clusterId ? clusterLookup.get(nextSelection.marker.clusterId) : void 0;
1131
+ onSelectionChange?.({
1132
+ type: "marker",
1133
+ marker: nextSelection.marker,
1134
+ cluster: parentCluster
1135
+ });
1136
+ return;
1137
+ }
1138
+ onSelectionChange?.({
1139
+ type: "cluster",
1140
+ cluster: nextSelection.cluster
1141
+ });
1142
+ },
1143
+ [clusterLookup, onSelectionChange]
1144
+ );
1145
+ const selectMarker = React3.useCallback(
1146
+ (marker) => {
1147
+ setSelection({
1148
+ type: "marker",
1149
+ markerId: marker.id,
1150
+ clusterId: marker.clusterId
1151
+ });
1152
+ applyViewState({
1153
+ latitude: marker.latitude,
1154
+ longitude: marker.longitude,
1155
+ zoom: markerFocusZoom
1156
+ });
1157
+ emitSelectionChange({ type: "marker", marker });
1158
+ },
1159
+ [applyViewState, emitSelectionChange, markerFocusZoom]
1160
+ );
1161
+ const selectCluster = React3.useCallback(
1162
+ (cluster) => {
1163
+ setSelection({
1164
+ type: "cluster",
1165
+ clusterId: cluster.id
1166
+ });
1167
+ applyViewState({
1168
+ latitude: cluster.latitude,
1169
+ longitude: cluster.longitude,
1170
+ zoom: clusterFocusZoom
1171
+ });
1172
+ emitSelectionChange({ type: "cluster", cluster });
1173
+ },
1174
+ [applyViewState, clusterFocusZoom, emitSelectionChange]
1175
+ );
1176
+ const clearSelection = React3.useCallback(() => {
1177
+ setSelection({ type: "none" });
1178
+ emitSelectionChange({ type: "none" });
1179
+ }, [emitSelectionChange]);
1180
+ const mapMarkers = React3.useMemo(() => {
1181
+ const resolvedMarkers = [];
1182
+ normalizedClusters.forEach((cluster) => {
1183
+ const isSelected = selection.type === "cluster" && selection.clusterId === cluster.id;
1184
+ resolvedMarkers.push({
1185
+ id: `cluster-pin:${cluster.id}`,
1186
+ latitude: cluster.latitude,
1187
+ longitude: cluster.longitude,
1188
+ element: () => {
1189
+ const customMarkerElement = cluster.markerElement;
1190
+ const markerBody = typeof customMarkerElement === "function" ? customMarkerElement({
1191
+ isSelected,
1192
+ count: cluster.markers.length
1193
+ }) : customMarkerElement;
1194
+ return /* @__PURE__ */ jsx(
1195
+ "button",
1196
+ {
1197
+ type: "button",
1198
+ className: "group cursor-pointer",
1199
+ onClick: (event) => {
1200
+ event.preventDefault();
1201
+ event.stopPropagation();
1202
+ selectCluster(cluster);
1203
+ },
1204
+ "aria-label": `View ${cluster.markers.length} clustered locations`,
1205
+ children: markerBody ?? /* @__PURE__ */ jsx(
1206
+ "span",
1207
+ {
1208
+ className: cn(
1209
+ "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",
1210
+ isSelected && "ring-4 ring-primary/30",
1211
+ cluster.pinClassName
1212
+ ),
1213
+ style: {
1214
+ backgroundColor: cluster.pinColor ?? "var(--foreground)"
1215
+ },
1216
+ children: cluster.markers.length
1217
+ }
1218
+ )
1219
+ }
1220
+ );
1221
+ }
1222
+ });
1223
+ });
1224
+ normalizedStandaloneMarkers.forEach((marker) => {
1225
+ const isSelected = selection.type === "marker" && selection.markerId === marker.id;
1226
+ const customMarkerElement = marker.markerElement;
1227
+ resolvedMarkers.push({
1228
+ id: marker.id,
1229
+ latitude: marker.latitude,
1230
+ longitude: marker.longitude,
1231
+ draggable: marker.draggable,
1232
+ element: () => {
1233
+ const markerBody = typeof customMarkerElement === "function" ? customMarkerElement({ isSelected }) : customMarkerElement;
1234
+ return /* @__PURE__ */ jsx(
1235
+ "button",
1236
+ {
1237
+ type: "button",
1238
+ className: "group cursor-pointer",
1239
+ onClick: (event) => {
1240
+ event.preventDefault();
1241
+ event.stopPropagation();
1242
+ selectMarker(marker);
1243
+ },
1244
+ "aria-label": typeof marker.title === "string" ? `View ${marker.title}` : "View location details",
1245
+ children: markerBody ?? /* @__PURE__ */ jsx(
1246
+ "span",
1247
+ {
1248
+ className: cn(
1249
+ "inline-flex h-4 w-4 rounded-full border-2 border-white shadow-md transition-transform duration-200 group-hover:scale-110",
1250
+ isSelected && "h-5 w-5 ring-4 ring-primary/30",
1251
+ marker.pinClassName
1252
+ ),
1253
+ style: {
1254
+ backgroundColor: marker.pinColor ?? "#f43f5e"
1255
+ }
1256
+ }
1257
+ )
1258
+ }
1259
+ );
1260
+ }
1261
+ });
1262
+ });
1263
+ return resolvedMarkers;
1264
+ }, [
1265
+ normalizedClusters,
1266
+ normalizedStandaloneMarkers,
1267
+ selectCluster,
1268
+ selectMarker,
1269
+ selection
1270
+ ]);
1271
+ const renderMarkerPanel = () => {
1272
+ if (selectedMarker) {
1273
+ const markerMediaItems = selectedMarker.mediaItems ?? [];
1274
+ return /* @__PURE__ */ jsxs(
1275
+ "div",
1276
+ {
1277
+ className: cn(
1278
+ "relative w-[320px] overflow-hidden rounded-xl border border-border bg-card text-card-foreground shadow-2xl",
1279
+ panelClassName
1280
+ ),
1281
+ children: [
1282
+ /* @__PURE__ */ jsx(
1283
+ "button",
1284
+ {
1285
+ type: "button",
1286
+ "aria-label": "Close marker details",
1287
+ 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",
1288
+ onClick: clearSelection,
1289
+ children: /* @__PURE__ */ jsx(IconComponent, { name: "lucide/x", size: 20 })
1290
+ }
1291
+ ),
1292
+ markerMediaItems.length > 0 ? /* @__PURE__ */ jsx(
1293
+ MarkerMediaCarousel,
1294
+ {
1295
+ mediaItems: markerMediaItems,
1296
+ optixFlowConfig,
1297
+ IconComponent,
1298
+ ImgComponent
1299
+ }
1300
+ ) : null,
1301
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2 p-4", children: [
1302
+ /* @__PURE__ */ jsx("div", { className: "flex items-start justify-between gap-3", children: /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1", children: [
1303
+ selectedMarker.eyebrow ? /* @__PURE__ */ jsx("p", { className: "text-xs font-semibold uppercase tracking-wide", children: selectedMarker.eyebrow }) : null,
1304
+ /* @__PURE__ */ jsx("div", { className: "text-base font-semibold leading-tight", children: selectedMarker.title ?? selectedMarker.label ?? "Location" })
1305
+ ] }) }),
1306
+ selectedMarker.summary ? /* @__PURE__ */ jsx("div", { className: "text-sm leading-relaxed", children: selectedMarker.summary }) : null,
1307
+ selectedMarker.locationLine ? /* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center justify-start text-sm gap-2", children: [
1308
+ /* @__PURE__ */ jsx(
1309
+ IconComponent,
1310
+ {
1311
+ name: "lucide:map-pin",
1312
+ className: "opacity-50",
1313
+ size: 18
1314
+ }
1315
+ ),
1316
+ typeof selectedMarker.locationLine === "string" ? /* @__PURE__ */ jsx(
1317
+ SimplePressable,
1318
+ {
1319
+ href: selectedMarker.locationUrl,
1320
+ className: cn(
1321
+ "transition-all duration-500",
1322
+ "font-medium opacity-75 hover:opacity-100",
1323
+ selectedMarker.locationUrl ? "underline underline-offset-4" : ""
1324
+ ),
1325
+ children: selectedMarker.locationLine
1326
+ }
1327
+ ) : selectedMarker.locationLine
1328
+ ] }) : null,
1329
+ selectedMarker.hoursLine ? /* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center justify-start text-sm gap-2", children: [
1330
+ /* @__PURE__ */ jsx(
1331
+ IconComponent,
1332
+ {
1333
+ name: "lucide:clock",
1334
+ className: "opacity-50",
1335
+ size: 18
1336
+ }
1337
+ ),
1338
+ typeof selectedMarker.hoursLine === "string" ? /* @__PURE__ */ jsx("div", { className: "font-medium", children: selectedMarker.hoursLine }) : selectedMarker.hoursLine
1339
+ ] }) : null,
1340
+ selectedMarker.markerContentComponent ? /* @__PURE__ */ jsx("div", { className: "relative", children: selectedMarker.markerContentComponent }) : null,
1341
+ /* @__PURE__ */ jsx(MarkerActions, { actions: selectedMarker.actions })
1342
+ ] })
1343
+ ]
1344
+ }
1345
+ );
1346
+ }
1347
+ if (selectedCluster) {
1348
+ return /* @__PURE__ */ jsxs(
1349
+ "div",
1350
+ {
1351
+ className: cn(
1352
+ "relative w-[320px] overflow-hidden rounded-xl border border-border bg-card text-card-foreground p-4 shadow-2xl",
1353
+ panelClassName
1354
+ ),
1355
+ children: [
1356
+ /* @__PURE__ */ jsx(
1357
+ "button",
1358
+ {
1359
+ type: "button",
1360
+ "aria-label": "Close cluster details",
1361
+ 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",
1362
+ onClick: clearSelection,
1363
+ children: /* @__PURE__ */ jsx(IconComponent, { name: "lucide/x", size: 20 })
1364
+ }
1365
+ ),
1366
+ /* @__PURE__ */ jsx("div", { className: "mb-3 flex items-start justify-between gap-3", children: /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
1367
+ selectedCluster.label ? /* @__PURE__ */ jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: selectedCluster.label }) : null,
1368
+ /* @__PURE__ */ jsx("div", { className: "text-base font-semibold leading-tight text-foreground", children: selectedCluster.title ?? "Clustered Locations" }),
1369
+ /* @__PURE__ */ 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.` })
1370
+ ] }) }),
1371
+ /* @__PURE__ */ jsx("div", { className: "max-h-56 space-y-2 overflow-y-auto pr-1", children: selectedCluster.markers.map((marker, markerIndex) => /* @__PURE__ */ jsxs(
1372
+ "button",
1373
+ {
1374
+ type: "button",
1375
+ className: "w-full rounded-lg border border-border/60 p-3 text-left transition hover:border-border hover:bg-muted/50",
1376
+ onClick: () => selectMarker(marker),
1377
+ children: [
1378
+ /* @__PURE__ */ jsx("div", { className: "line-clamp-1 text-sm font-semibold text-foreground", children: getMarkerTitle(marker, markerIndex) }),
1379
+ marker.summary ? /* @__PURE__ */ jsx("div", { className: "mt-1 line-clamp-2 text-xs text-muted-foreground", children: marker.summary }) : null
1380
+ ]
1381
+ },
1382
+ marker.id
1383
+ )) })
1384
+ ]
1385
+ }
1386
+ );
1387
+ }
1388
+ return null;
1389
+ };
1390
+ return /* @__PURE__ */ jsxs(
1391
+ "div",
1392
+ {
1393
+ className: cn(
1394
+ "relative overflow-hidden rounded-2xl border border-border bg-background",
1395
+ className
1396
+ ),
1397
+ children: [
1398
+ /* @__PURE__ */ jsx("div", { className: cn("h-[520px] w-full", mapWrapperClassName), children: /* @__PURE__ */ jsx(
1399
+ MapLibre,
1400
+ {
1401
+ stadiaApiKey,
1402
+ mapStyle,
1403
+ styleUrl,
1404
+ mapLibreCssHref,
1405
+ viewState: resolvedViewState,
1406
+ onViewStateChange: applyViewState,
1407
+ markers: mapMarkers,
1408
+ onClick: (coord) => {
1409
+ onMapClick?.(coord);
1410
+ if (clearSelectionOnMapClick) {
1411
+ clearSelection();
1412
+ }
1413
+ },
1414
+ onMarkerDrag,
1415
+ showNavigationControl,
1416
+ showGeolocateControl,
1417
+ navigationControlPosition,
1418
+ geolocateControlPosition,
1419
+ flyToOptions,
1420
+ className: cn("h-full w-full", mapClassName),
1421
+ children: mapChildren
1422
+ }
1423
+ ) }),
1424
+ selection.type !== "none" ? /* @__PURE__ */ jsx(
1425
+ "div",
1426
+ {
1427
+ className: cn(
1428
+ "pointer-events-none absolute z-20",
1429
+ PANEL_POSITION_CLASS[panelPosition]
1430
+ ),
1431
+ children: /* @__PURE__ */ jsx("div", { className: "pointer-events-auto", children: renderMarkerPanel() })
1432
+ }
1433
+ ) : null
1434
+ ]
1435
+ }
1436
+ );
1437
+ }
549
1438
 
550
- export { DTMapLibreMap, MapLibre, appendStadiaApiKey, computeDefaultZoom, computeGeoCenter, generateGoogleDirectionsLink, generateGoogleMapLink, getMapLibreStyleUrl, useDefaultZoom, useGeoCenter };
1439
+ export { DTMapLibreMap, GeoMap, MapLibre, MapMarker, NeutralMapMarker, appendStadiaApiKey, computeDefaultZoom, computeGeoCenter, createMapMarkerElement, generateGoogleDirectionsLink, generateGoogleMapLink, getMapLibreStyleUrl, useDefaultZoom, useGeoCenter };
551
1440
  //# sourceMappingURL=index.js.map
552
1441
  //# sourceMappingURL=index.js.map