@elabs-ai/components-maps 4.2.0 → 5.0.0

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 (291) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-2FPB53NG.js +36 -0
  3. package/dist/chunk-2FPB53NG.js.map +1 -0
  4. package/dist/chunk-2VVJRT66.js +37 -0
  5. package/dist/chunk-2VVJRT66.js.map +1 -0
  6. package/dist/chunk-3HXFONRP.js +2 -0
  7. package/dist/chunk-3HXFONRP.js.map +1 -0
  8. package/dist/chunk-3SJN3IYT.js +2 -0
  9. package/dist/chunk-3SJN3IYT.js.map +1 -0
  10. package/dist/chunk-4DANBD5B.js +55 -0
  11. package/dist/chunk-4DANBD5B.js.map +1 -0
  12. package/dist/chunk-5PDVG53B.js +2 -0
  13. package/dist/chunk-5PDVG53B.js.map +1 -0
  14. package/dist/chunk-643R7IJC.js +2 -0
  15. package/dist/chunk-643R7IJC.js.map +1 -0
  16. package/dist/chunk-B3ZDBQ6I.js +164 -0
  17. package/dist/chunk-B3ZDBQ6I.js.map +1 -0
  18. package/dist/chunk-BGCE3RR5.js +35 -0
  19. package/dist/chunk-BGCE3RR5.js.map +1 -0
  20. package/dist/chunk-C4VI37TD.js +485 -0
  21. package/dist/chunk-C4VI37TD.js.map +1 -0
  22. package/dist/chunk-CJJT6AEI.js +179 -0
  23. package/dist/chunk-CJJT6AEI.js.map +1 -0
  24. package/dist/chunk-D2RCPTRO.js +2 -0
  25. package/dist/chunk-D2RCPTRO.js.map +1 -0
  26. package/dist/chunk-DTV7JZVN.js +2 -0
  27. package/dist/chunk-DTV7JZVN.js.map +1 -0
  28. package/dist/chunk-E65Z3MSV.js +80 -0
  29. package/dist/chunk-E65Z3MSV.js.map +1 -0
  30. package/dist/chunk-E7VPNINZ.js +70 -0
  31. package/dist/chunk-E7VPNINZ.js.map +1 -0
  32. package/dist/chunk-FHV5IA6N.js +100 -0
  33. package/dist/chunk-FHV5IA6N.js.map +1 -0
  34. package/dist/chunk-FQ6V7BFZ.js +347 -0
  35. package/dist/chunk-FQ6V7BFZ.js.map +1 -0
  36. package/dist/chunk-G7JDXDJN.js +19 -0
  37. package/dist/chunk-G7JDXDJN.js.map +1 -0
  38. package/dist/chunk-GWULZOMW.js +21 -0
  39. package/dist/chunk-GWULZOMW.js.map +1 -0
  40. package/dist/chunk-GYDTTAVD.js +120 -0
  41. package/dist/chunk-GYDTTAVD.js.map +1 -0
  42. package/dist/chunk-HCOBWIM4.js +48 -0
  43. package/dist/chunk-HCOBWIM4.js.map +1 -0
  44. package/dist/chunk-IAHK5MVT.js +2 -0
  45. package/dist/chunk-IAHK5MVT.js.map +1 -0
  46. package/dist/chunk-JGJ6442I.js +235 -0
  47. package/dist/chunk-JGJ6442I.js.map +1 -0
  48. package/dist/chunk-JYFERXZT.js +18 -0
  49. package/dist/chunk-JYFERXZT.js.map +1 -0
  50. package/dist/chunk-LU2UEKFM.js +53 -0
  51. package/dist/chunk-LU2UEKFM.js.map +1 -0
  52. package/dist/chunk-LVYM6C5C.js +2 -0
  53. package/dist/chunk-LVYM6C5C.js.map +1 -0
  54. package/dist/chunk-MIMDP3EU.js +104 -0
  55. package/dist/chunk-MIMDP3EU.js.map +1 -0
  56. package/dist/chunk-NNBURJHY.js +2 -0
  57. package/dist/chunk-NNBURJHY.js.map +1 -0
  58. package/dist/chunk-NPUC3V37.js +2 -0
  59. package/dist/chunk-NPUC3V37.js.map +1 -0
  60. package/dist/chunk-OEVVNPN3.js +201 -0
  61. package/dist/chunk-OEVVNPN3.js.map +1 -0
  62. package/dist/chunk-PF3R2BXM.js +200 -0
  63. package/dist/chunk-PF3R2BXM.js.map +1 -0
  64. package/dist/chunk-Q2I5YODW.js +2 -0
  65. package/dist/chunk-Q2I5YODW.js.map +1 -0
  66. package/dist/chunk-QDWWP3N7.js +247 -0
  67. package/dist/chunk-QDWWP3N7.js.map +1 -0
  68. package/dist/chunk-QU33KPMP.js +2 -0
  69. package/dist/chunk-QU33KPMP.js.map +1 -0
  70. package/dist/chunk-RK4BUMHS.js +2 -0
  71. package/dist/chunk-RK4BUMHS.js.map +1 -0
  72. package/dist/chunk-RNSLG64F.js +26 -0
  73. package/dist/chunk-RNSLG64F.js.map +1 -0
  74. package/dist/chunk-RV673TB4.js +68 -0
  75. package/dist/chunk-RV673TB4.js.map +1 -0
  76. package/dist/chunk-S7N2WMVT.js +180 -0
  77. package/dist/chunk-S7N2WMVT.js.map +1 -0
  78. package/dist/chunk-T3Z6TCDD.js +2 -0
  79. package/dist/chunk-T3Z6TCDD.js.map +1 -0
  80. package/dist/chunk-T4TJTVVA.js +105 -0
  81. package/dist/chunk-T4TJTVVA.js.map +1 -0
  82. package/dist/chunk-UMOYRCDX.js +103 -0
  83. package/dist/chunk-UMOYRCDX.js.map +1 -0
  84. package/dist/chunk-V2REM6UT.js +185 -0
  85. package/dist/chunk-V2REM6UT.js.map +1 -0
  86. package/dist/chunk-VFBRFGID.js +89 -0
  87. package/dist/chunk-VFBRFGID.js.map +1 -0
  88. package/dist/chunk-W76NLFER.js +103 -0
  89. package/dist/chunk-W76NLFER.js.map +1 -0
  90. package/dist/chunk-WNU4IOGL.js +116 -0
  91. package/dist/chunk-WNU4IOGL.js.map +1 -0
  92. package/dist/chunk-WS4Z7ENS.js +494 -0
  93. package/dist/chunk-WS4Z7ENS.js.map +1 -0
  94. package/dist/chunk-XQT7YLJY.js +497 -0
  95. package/dist/chunk-XQT7YLJY.js.map +1 -0
  96. package/dist/chunk-Y44MEKJV.js +21 -0
  97. package/dist/chunk-Y44MEKJV.js.map +1 -0
  98. package/dist/chunk-Z4EPSOB4.js +53 -0
  99. package/dist/chunk-Z4EPSOB4.js.map +1 -0
  100. package/dist/chunk-Z5OPDBDC.js +2 -0
  101. package/dist/chunk-Z5OPDBDC.js.map +1 -0
  102. package/dist/index.d.ts +1241 -46
  103. package/dist/index.js +189 -1471
  104. package/dist/index.js.map +1 -1
  105. package/dist/lib/arc-math.js +8 -0
  106. package/dist/lib/arc-math.js.map +1 -0
  107. package/dist/lib/map-position.js +11 -0
  108. package/dist/lib/map-position.js.map +1 -0
  109. package/dist/lib/merge-hover-paint.js +10 -0
  110. package/dist/lib/merge-hover-paint.js.map +1 -0
  111. package/dist/lib/plan-crs.js +14 -0
  112. package/dist/lib/plan-crs.js.map +1 -0
  113. package/dist/lib/plan-patterns.js +14 -0
  114. package/dist/lib/plan-patterns.js.map +1 -0
  115. package/dist/lib/plan-regions.js +18 -0
  116. package/dist/lib/plan-regions.js.map +1 -0
  117. package/dist/lib/plan-status.js +14 -0
  118. package/dist/lib/plan-status.js.map +1 -0
  119. package/dist/lib/route-arrow.js +12 -0
  120. package/dist/lib/route-arrow.js.map +1 -0
  121. package/dist/lib/use-map-breakpoint.js +33 -0
  122. package/dist/lib/use-map-breakpoint.js.map +1 -0
  123. package/dist/lib/use-plan-projection.js +9 -0
  124. package/dist/lib/use-plan-projection.js.map +1 -0
  125. package/dist/lib/use-token-color.js +10 -0
  126. package/dist/lib/use-token-color.js.map +1 -0
  127. package/dist/lib/warn-once.js +10 -0
  128. package/dist/lib/warn-once.js.map +1 -0
  129. package/dist/map-annotation/anchor.js +14 -0
  130. package/dist/map-annotation/anchor.js.map +1 -0
  131. package/dist/map-annotation/index.js +21 -0
  132. package/dist/map-annotation/index.js.map +1 -0
  133. package/dist/map-annotation/map-annotation.js +18 -0
  134. package/dist/map-annotation/map-annotation.js.map +1 -0
  135. package/dist/map-arc/index.js +16 -0
  136. package/dist/map-arc/index.js.map +1 -0
  137. package/dist/map-arc/map-arc.js +13 -0
  138. package/dist/map-arc/map-arc.js.map +1 -0
  139. package/dist/map-canvas/index.css +11 -0
  140. package/dist/map-canvas/index.css.map +1 -0
  141. package/dist/map-canvas/index.js +20 -0
  142. package/dist/map-canvas/index.js.map +1 -0
  143. package/dist/map-canvas/map-canvas.css +11 -0
  144. package/dist/map-canvas/map-canvas.css.map +1 -0
  145. package/dist/map-canvas/map-canvas.js +17 -0
  146. package/dist/map-canvas/map-canvas.js.map +1 -0
  147. package/dist/map-canvas/map-context.js +11 -0
  148. package/dist/map-canvas/map-context.js.map +1 -0
  149. package/dist/map-canvas/use-resolved-basemap-theme.js +9 -0
  150. package/dist/map-canvas/use-resolved-basemap-theme.js.map +1 -0
  151. package/dist/map-cluster-layer/index.js +12 -0
  152. package/dist/map-cluster-layer/index.js.map +1 -0
  153. package/dist/map-cluster-layer/map-cluster-layer.js +12 -0
  154. package/dist/map-cluster-layer/map-cluster-layer.js.map +1 -0
  155. package/dist/map-controls/index.js +11 -0
  156. package/dist/map-controls/index.js.map +1 -0
  157. package/dist/map-controls/map-controls.js +11 -0
  158. package/dist/map-controls/map-controls.js.map +1 -0
  159. package/dist/map-geojson/index.js +13 -0
  160. package/dist/map-geojson/index.js.map +1 -0
  161. package/dist/map-geojson/map-geojson.js +13 -0
  162. package/dist/map-geojson/map-geojson.js.map +1 -0
  163. package/dist/map-inset/index.css +11 -0
  164. package/dist/map-inset/index.css.map +1 -0
  165. package/dist/map-inset/index.js +20 -0
  166. package/dist/map-inset/index.js.map +1 -0
  167. package/dist/map-inset/map-inset.css +11 -0
  168. package/dist/map-inset/map-inset.css.map +1 -0
  169. package/dist/map-inset/map-inset.js +20 -0
  170. package/dist/map-inset/map-inset.js.map +1 -0
  171. package/dist/map-legend/index.js +14 -0
  172. package/dist/map-legend/index.js.map +1 -0
  173. package/dist/map-legend/map-corner.js +10 -0
  174. package/dist/map-legend/map-corner.js.map +1 -0
  175. package/dist/map-legend/map-legend.js +11 -0
  176. package/dist/map-legend/map-legend.js.map +1 -0
  177. package/dist/map-marker/index.js +22 -0
  178. package/dist/map-marker/index.js.map +1 -0
  179. package/dist/map-marker/map-marker.js +22 -0
  180. package/dist/map-marker/map-marker.js.map +1 -0
  181. package/dist/map-north-arrow/index.js +11 -0
  182. package/dist/map-north-arrow/index.js.map +1 -0
  183. package/dist/map-north-arrow/map-north-arrow.js +11 -0
  184. package/dist/map-north-arrow/map-north-arrow.js.map +1 -0
  185. package/dist/map-plan/index.js +30 -0
  186. package/dist/map-plan/index.js.map +1 -0
  187. package/dist/map-plan/map-plan-legend.js +10 -0
  188. package/dist/map-plan/map-plan-legend.js.map +1 -0
  189. package/dist/map-plan/map-plan-overlay.js +13 -0
  190. package/dist/map-plan/map-plan-overlay.js.map +1 -0
  191. package/dist/map-plan/map-plan-status.js +9 -0
  192. package/dist/map-plan/map-plan-status.js.map +1 -0
  193. package/dist/map-plan/map-plan-table.js +10 -0
  194. package/dist/map-plan/map-plan-table.js.map +1 -0
  195. package/dist/map-plan/use-plan-selection.js +9 -0
  196. package/dist/map-plan/use-plan-selection.js.map +1 -0
  197. package/dist/map-plan-image/index.js +11 -0
  198. package/dist/map-plan-image/index.js.map +1 -0
  199. package/dist/map-plan-image/map-plan-image.js +11 -0
  200. package/dist/map-plan-image/map-plan-image.js.map +1 -0
  201. package/dist/map-popup/index.js +12 -0
  202. package/dist/map-popup/index.js.map +1 -0
  203. package/dist/map-popup/map-popup.js +12 -0
  204. package/dist/map-popup/map-popup.js.map +1 -0
  205. package/dist/map-route/index.js +12 -0
  206. package/dist/map-route/index.js.map +1 -0
  207. package/dist/map-route/map-route.js +12 -0
  208. package/dist/map-route/map-route.js.map +1 -0
  209. package/dist/map-scale-bar/index.js +18 -0
  210. package/dist/map-scale-bar/index.js.map +1 -0
  211. package/dist/map-scale-bar/map-scale-bar.js +18 -0
  212. package/dist/map-scale-bar/map-scale-bar.js.map +1 -0
  213. package/package.json +5 -5
  214. package/src/index.ts +83 -0
  215. package/src/lib/arc-math.test.ts +19 -0
  216. package/src/lib/arc-math.ts +6 -1
  217. package/src/lib/map-position.ts +77 -0
  218. package/src/lib/merge-hover-paint.ts +44 -13
  219. package/src/lib/plan-crs.parity.test.ts +39 -0
  220. package/src/lib/plan-crs.test.ts +248 -0
  221. package/src/lib/plan-crs.ts +252 -0
  222. package/src/lib/plan-patterns.test.ts +255 -0
  223. package/src/lib/plan-patterns.ts +160 -0
  224. package/src/lib/plan-regions.test.ts +186 -0
  225. package/src/lib/plan-regions.ts +209 -0
  226. package/src/lib/plan-status.test.ts +87 -0
  227. package/src/lib/plan-status.ts +147 -0
  228. package/src/lib/route-arrow.test.ts +88 -0
  229. package/src/lib/route-arrow.ts +59 -0
  230. package/src/lib/use-map-breakpoint.test.ts +83 -0
  231. package/src/lib/use-map-breakpoint.ts +238 -0
  232. package/src/lib/use-plan-projection.ts +157 -0
  233. package/src/lib/warn-once.ts +21 -0
  234. package/src/map-annotation/anchor.ts +92 -0
  235. package/src/map-annotation/index.ts +15 -0
  236. package/src/map-annotation/map-annotation.stories.tsx +113 -0
  237. package/src/map-annotation/map-annotation.test.tsx +106 -0
  238. package/src/map-annotation/map-annotation.tsx +356 -0
  239. package/src/map-arc/map-arc.tsx +26 -5
  240. package/src/map-canvas/map-canvas.stories.tsx +211 -0
  241. package/src/map-canvas/map-canvas.test.tsx +389 -0
  242. package/src/map-canvas/map-canvas.tsx +457 -17
  243. package/src/map-canvas/map-context.ts +11 -0
  244. package/src/map-cluster-layer/map-cluster-layer.tsx +14 -1
  245. package/src/map-controls/map-controls.stories.tsx +16 -0
  246. package/src/map-controls/map-controls.test.tsx +145 -1
  247. package/src/map-controls/map-controls.tsx +57 -12
  248. package/src/map-geojson/map-geojson.stories.tsx +153 -3
  249. package/src/map-geojson/map-geojson.test.tsx +453 -0
  250. package/src/map-geojson/map-geojson.tsx +591 -51
  251. package/src/map-inset/index.ts +1 -0
  252. package/src/map-inset/map-inset.stories.tsx +61 -0
  253. package/src/map-inset/map-inset.test.tsx +117 -0
  254. package/src/map-inset/map-inset.tsx +264 -0
  255. package/src/map-legend/index.ts +10 -0
  256. package/src/map-legend/map-corner.ts +23 -0
  257. package/src/map-legend/map-legend.stories.tsx +171 -0
  258. package/src/map-legend/map-legend.test.tsx +136 -0
  259. package/src/map-legend/map-legend.tsx +264 -0
  260. package/src/map-marker/map-marker.stories.tsx +136 -4
  261. package/src/map-marker/map-marker.test.tsx +328 -3
  262. package/src/map-marker/map-marker.tsx +370 -33
  263. package/src/map-north-arrow/index.ts +1 -0
  264. package/src/map-north-arrow/map-north-arrow.stories.tsx +65 -0
  265. package/src/map-north-arrow/map-north-arrow.test.tsx +39 -0
  266. package/src/map-north-arrow/map-north-arrow.tsx +82 -0
  267. package/src/map-plan/index.ts +13 -0
  268. package/src/map-plan/map-plan-legend.test.tsx +61 -0
  269. package/src/map-plan/map-plan-legend.tsx +83 -0
  270. package/src/map-plan/map-plan-overlay.stories.tsx +260 -0
  271. package/src/map-plan/map-plan-overlay.test.tsx +302 -0
  272. package/src/map-plan/map-plan-overlay.tsx +531 -0
  273. package/src/map-plan/map-plan-status.test.tsx +71 -0
  274. package/src/map-plan/map-plan-status.tsx +77 -0
  275. package/src/map-plan/map-plan-table.test.tsx +86 -0
  276. package/src/map-plan/map-plan-table.tsx +153 -0
  277. package/src/map-plan/use-plan-selection.ts +78 -0
  278. package/src/map-plan-image/index.ts +1 -0
  279. package/src/map-plan-image/map-plan-image.stories.tsx +301 -0
  280. package/src/map-plan-image/map-plan-image.test.tsx +176 -0
  281. package/src/map-plan-image/map-plan-image.tsx +279 -0
  282. package/src/map-popup/map-popup.tsx +15 -10
  283. package/src/map-route/index.ts +1 -1
  284. package/src/map-route/map-route.test.tsx +216 -0
  285. package/src/map-route/map-route.tsx +105 -6
  286. package/src/map-scale-bar/index.ts +9 -0
  287. package/src/map-scale-bar/map-scale-bar.stories.tsx +63 -0
  288. package/src/map-scale-bar/map-scale-bar.test.tsx +65 -0
  289. package/src/map-scale-bar/map-scale-bar.tsx +145 -0
  290. package/src/test-utils/locator-fixture.ts +75 -0
  291. package/src/test-utils/maplibre-mock.ts +256 -19
@@ -0,0 +1,105 @@
1
+ "use client";
2
+ import {
3
+ MAP_CORNER_CLASSES
4
+ } from "./chunk-GWULZOMW.js";
5
+ import {
6
+ useMapBreakpoint
7
+ } from "./chunk-W76NLFER.js";
8
+ import {
9
+ useMap
10
+ } from "./chunk-JYFERXZT.js";
11
+
12
+ // src/map-scale-bar/map-scale-bar.tsx
13
+ import { forwardRef, useEffect, useState } from "react";
14
+ import { useLocale } from "@elabs-ai/components-ui";
15
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
16
+ import { jsx, jsxs } from "react/jsx-runtime";
17
+ var EARTH_CIRCUMFERENCE_M = 40075016686e-3;
18
+ var TILE_SIZE = 512;
19
+ var METRES_PER_MILE = 1609.344;
20
+ var METRES_PER_FOOT = 0.3048;
21
+ var MAX_BAR_PX = { wide: 112, medium: 96, narrow: 80 };
22
+ function metresPerPixel(latitude, zoom) {
23
+ return EARTH_CIRCUMFERENCE_M * Math.cos(latitude * Math.PI / 180) / (TILE_SIZE * 2 ** zoom);
24
+ }
25
+ function niceDistance(max) {
26
+ if (!(max > 0) || !Number.isFinite(max)) return 0;
27
+ const magnitude = 10 ** Math.floor(Math.log10(max));
28
+ for (const step of [5, 2, 1]) {
29
+ if (step * magnitude <= max) return step * magnitude;
30
+ }
31
+ return magnitude;
32
+ }
33
+ function computeMapScale(latitude, zoom, unit, maxPx) {
34
+ const perPx = metresPerPixel(latitude, zoom);
35
+ const maxMetres = perPx * maxPx;
36
+ const [big, small, bigM, smallM] = unit === "mi" ? ["mile", "foot", METRES_PER_MILE, METRES_PER_FOOT] : ["kilometer", "meter", 1e3, 1];
37
+ const useBig = maxMetres >= bigM;
38
+ const perUnit = useBig ? bigM : smallM;
39
+ const value = niceDistance(maxMetres / perUnit);
40
+ const px = perPx > 0 ? value * perUnit / perPx : 0;
41
+ return { px: Math.max(0, px), value, unit: useBig ? big : small };
42
+ }
43
+ var MapScaleBar = forwardRef(function MapScaleBar2({ unit = "km", position = "bottom-left", className, ...props }, ref) {
44
+ const { map } = useMap();
45
+ const { formatNumber } = useLocale();
46
+ const maxPx = MAX_BAR_PX[useMapBreakpoint()];
47
+ const [scale, setScale] = useState(null);
48
+ useEffect(() => {
49
+ if (!map) return void 0;
50
+ const update = () => {
51
+ const next = computeMapScale(map.getCenter().lat, map.getZoom(), unit, maxPx);
52
+ setScale(
53
+ (prev) => prev && prev.value === next.value && prev.unit === next.unit && Math.abs(prev.px - next.px) < 0.5 ? prev : next
54
+ );
55
+ };
56
+ update();
57
+ map.on("move", update);
58
+ map.on("resize", update);
59
+ return () => {
60
+ map.off("move", update);
61
+ map.off("resize", update);
62
+ };
63
+ }, [map, unit, maxPx]);
64
+ if (!scale || scale.value <= 0) return null;
65
+ const label = formatNumber(scale.value, {
66
+ style: "unit",
67
+ unit: scale.unit,
68
+ unitDisplay: "short"
69
+ });
70
+ return /* @__PURE__ */ jsxs(
71
+ "div",
72
+ {
73
+ ref,
74
+ "data-slot": "map-scale-bar",
75
+ "data-unit": scale.unit,
76
+ "data-distance": scale.value,
77
+ className: cn(
78
+ "pointer-events-none absolute z-10 flex flex-col items-start gap-0.5 rounded-sm bg-background/80 px-1.5 py-1",
79
+ MAP_CORNER_CLASSES[position],
80
+ className
81
+ ),
82
+ ...props,
83
+ children: [
84
+ /* @__PURE__ */ jsx("span", { "data-slot": "map-scale-bar-label", className: "text-meta text-foreground tabular-nums", children: label }),
85
+ /* @__PURE__ */ jsx(
86
+ "span",
87
+ {
88
+ "aria-hidden": "true",
89
+ "data-slot": "map-scale-bar-bar",
90
+ className: "block h-1.5 border-x-2 border-b-2 border-foreground",
91
+ style: { width: Math.round(scale.px) }
92
+ }
93
+ )
94
+ ]
95
+ }
96
+ );
97
+ });
98
+
99
+ export {
100
+ metresPerPixel,
101
+ niceDistance,
102
+ computeMapScale,
103
+ MapScaleBar
104
+ };
105
+ //# sourceMappingURL=chunk-T4TJTVVA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/map-scale-bar/map-scale-bar.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useEffect, useState, type HTMLAttributes } from \"react\";\nimport { useLocale } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nimport { useMap } from \"../map-canvas/map-context\";\nimport { useMapBreakpoint } from \"../lib/use-map-breakpoint\";\nimport { MAP_CORNER_CLASSES, type MapCorner } from \"../map-legend/map-corner\";\n\n/** Metric (`km`, falling back to metres) or imperial (`mi`, falling back to feet). */\nexport type MapScaleUnit = \"km\" | \"mi\";\n\n/** The Earth's equatorial circumference in metres (WGS 84), as MapLibre uses it. */\nconst EARTH_CIRCUMFERENCE_M = 40_075_016.686;\n/** MapLibre's world size at zoom 0, in CSS px. */\nconst TILE_SIZE = 512;\nconst METRES_PER_MILE = 1609.344;\nconst METRES_PER_FOOT = 0.3048;\n\n/** The longest the bar may draw, per tier (CSS px). */\nconst MAX_BAR_PX = { wide: 112, medium: 96, narrow: 80 } as const;\n\n/** Metres one CSS px covers at `latitude` and `zoom` (Web Mercator). */\nexport function metresPerPixel(latitude: number, zoom: number): number {\n return (EARTH_CIRCUMFERENCE_M * Math.cos((latitude * Math.PI) / 180)) / (TILE_SIZE * 2 ** zoom);\n}\n\n/** The largest 1 / 2 / 5 × 10ⁿ that is ≤ `max`. */\nexport function niceDistance(max: number): number {\n if (!(max > 0) || !Number.isFinite(max)) return 0;\n const magnitude = 10 ** Math.floor(Math.log10(max));\n for (const step of [5, 2, 1]) {\n if (step * magnitude <= max) return step * magnitude;\n }\n return magnitude;\n}\n\nexport interface MapScale {\n /** The bar's length in CSS px. */\n px: number;\n /** The distance the bar stands for, in `unit`. */\n value: number;\n /** An `Intl.NumberFormat` unit identifier. */\n unit: \"kilometer\" | \"meter\" | \"mile\" | \"foot\";\n}\n\n/**\n * A round distance that fits in `maxPx` at this latitude and zoom, in `unit`\n * — kilometres (metres below 1 km) or miles (feet below 1 mi).\n */\nexport function computeMapScale(\n latitude: number,\n zoom: number,\n unit: MapScaleUnit,\n maxPx: number,\n): MapScale {\n const perPx = metresPerPixel(latitude, zoom);\n const maxMetres = perPx * maxPx;\n const [big, small, bigM, smallM] =\n unit === \"mi\"\n ? ([\"mile\", \"foot\", METRES_PER_MILE, METRES_PER_FOOT] as const)\n : ([\"kilometer\", \"meter\", 1000, 1] as const);\n const useBig = maxMetres >= bigM;\n const perUnit = useBig ? bigM : smallM;\n const value = niceDistance(maxMetres / perUnit);\n const px = perPx > 0 ? (value * perUnit) / perPx : 0;\n return { px: Math.max(0, px), value, unit: useBig ? big : small };\n}\n\nexport interface MapScaleBarProps extends HTMLAttributes<HTMLDivElement> {\n /** `km` (default) or `mi`. */\n unit?: MapScaleUnit;\n /** Corner of the map (default `\"bottom-left\"`). */\n position?: MapCorner;\n}\n\n/**\n * A scale bar for the map's centre latitude — a round distance (\"50 km\") and\n * a bar of that length, re-measured as the map moves or resizes. The bar gets\n * shorter at `narrow`. Render inside `<MapCanvas>`.\n */\nexport const MapScaleBar = forwardRef<HTMLDivElement, MapScaleBarProps>(function MapScaleBar(\n { unit = \"km\", position = \"bottom-left\", className, ...props },\n ref,\n) {\n const { map } = useMap();\n const { formatNumber } = useLocale();\n const maxPx = MAX_BAR_PX[useMapBreakpoint()];\n const [scale, setScale] = useState<MapScale | null>(null);\n\n useEffect(() => {\n if (!map) return undefined;\n const update = () => {\n const next = computeMapScale(map.getCenter().lat, map.getZoom(), unit, maxPx);\n setScale((prev) =>\n prev &&\n prev.value === next.value &&\n prev.unit === next.unit &&\n Math.abs(prev.px - next.px) < 0.5\n ? prev\n : next,\n );\n };\n update();\n map.on(\"move\", update);\n map.on(\"resize\", update);\n return () => {\n map.off(\"move\", update);\n map.off(\"resize\", update);\n };\n }, [map, unit, maxPx]);\n\n if (!scale || scale.value <= 0) return null;\n const label = formatNumber(scale.value, {\n style: \"unit\",\n unit: scale.unit,\n unitDisplay: \"short\",\n });\n\n return (\n <div\n ref={ref}\n data-slot=\"map-scale-bar\"\n data-unit={scale.unit}\n data-distance={scale.value}\n className={cn(\n \"pointer-events-none absolute z-10 flex flex-col items-start gap-0.5 rounded-sm bg-background/80 px-1.5 py-1\",\n MAP_CORNER_CLASSES[position],\n className,\n )}\n {...props}\n >\n <span data-slot=\"map-scale-bar-label\" className=\"text-meta text-foreground tabular-nums\">\n {label}\n </span>\n <span\n aria-hidden=\"true\"\n data-slot=\"map-scale-bar-bar\"\n className=\"block h-1.5 border-x-2 border-b-2 border-foreground\"\n style={{ width: Math.round(scale.px) }}\n />\n </div>\n );\n});\n"],"mappings":"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","names":["MapScaleBar"]}
@@ -0,0 +1,103 @@
1
+ "use client";
2
+ import {
3
+ useMap
4
+ } from "./chunk-JYFERXZT.js";
5
+ import {
6
+ resolveMapPosition
7
+ } from "./chunk-2FPB53NG.js";
8
+
9
+ // src/map-popup/map-popup.tsx
10
+ import MapLibreGL from "maplibre-gl";
11
+ import { useEffect, useMemo, useRef } from "react";
12
+ import { createPortal } from "react-dom";
13
+ import { X } from "lucide-react";
14
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
15
+ import { useLocale } from "@elabs-ai/components-ui";
16
+ import { jsx, jsxs } from "react/jsx-runtime";
17
+ function MapPopup({
18
+ longitude: longitudeProp,
19
+ latitude: latitudeProp,
20
+ x,
21
+ y,
22
+ onClose,
23
+ children,
24
+ className,
25
+ closeButton = false,
26
+ ...popupOptions
27
+ }) {
28
+ const { map, plan } = useMap();
29
+ const { t } = useLocale();
30
+ const [longitude, latitude] = resolveMapPosition(
31
+ { longitude: longitudeProp, latitude: latitudeProp, x, y },
32
+ plan,
33
+ "MapPopup"
34
+ );
35
+ const onCloseRef = useRef(onClose);
36
+ onCloseRef.current = onClose;
37
+ const container = useMemo(() => document.createElement("div"), []);
38
+ const { offset, maxWidth } = popupOptions;
39
+ const popup = useMemo(() => {
40
+ return new MapLibreGL.Popup({
41
+ offset: 16,
42
+ ...popupOptions,
43
+ closeButton: false
44
+ }).setMaxWidth("none").setLngLat([longitude, latitude]);
45
+ }, []);
46
+ useEffect(() => {
47
+ if (!map) return;
48
+ const onCloseProp = () => onCloseRef.current?.();
49
+ popup.on("close", onCloseProp);
50
+ popup.setDOMContent(container);
51
+ popup.addTo(map);
52
+ return () => {
53
+ popup.off("close", onCloseProp);
54
+ if (popup.isOpen()) {
55
+ popup.remove();
56
+ }
57
+ };
58
+ }, [map]);
59
+ useEffect(() => {
60
+ const current = popup.getLngLat();
61
+ if (!current || current.lng !== longitude || current.lat !== latitude) {
62
+ popup.setLngLat([longitude, latitude]);
63
+ }
64
+ popup.setOffset(offset ?? 16);
65
+ if (maxWidth) {
66
+ popup.setMaxWidth(maxWidth);
67
+ }
68
+ }, [popup, longitude, latitude, offset, maxWidth]);
69
+ const handleClose = () => {
70
+ popup.remove();
71
+ };
72
+ return createPortal(
73
+ /* @__PURE__ */ jsxs(
74
+ "div",
75
+ {
76
+ className: cn(
77
+ "relative max-w-62 rounded-md bg-popover p-3 text-popover-foreground shadow-ring-md",
78
+ "animate-in fade-in-0 zoom-in-95 duration-fast ease-entrance",
79
+ className
80
+ ),
81
+ children: [
82
+ closeButton && /* @__PURE__ */ jsx(
83
+ "button",
84
+ {
85
+ type: "button",
86
+ onClick: handleClose,
87
+ "aria-label": t("maps.popup.close"),
88
+ className: "absolute top-1 right-1 z-10 inline-flex size-5 items-center justify-center rounded-sm text-foreground transition-colors duration-fast hover:bg-muted focus-ring-inset",
89
+ children: /* @__PURE__ */ jsx(X, { className: "size-3.5", "aria-hidden": "true" })
90
+ }
91
+ ),
92
+ children
93
+ ]
94
+ }
95
+ ),
96
+ container
97
+ );
98
+ }
99
+
100
+ export {
101
+ MapPopup
102
+ };
103
+ //# sourceMappingURL=chunk-UMOYRCDX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/map-popup/map-popup.tsx"],"sourcesContent":["\"use client\";\n\nimport MapLibreGL, { type PopupOptions } from \"maplibre-gl\";\nimport { useEffect, useMemo, useRef, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport { useLocale } from \"@elabs-ai/components-ui\";\n\nimport { useMap } from \"../map-canvas/map-context\";\nimport { resolveMapPosition, type MapPosition } from \"../lib/map-position\";\n\nexport type MapPopupProps = MapPosition & {\n /** Callback when the popup is closed. */\n onClose?: () => void;\n /** Popup content. */\n children: ReactNode;\n /** Additional CSS classes for the popup container. */\n className?: string;\n /** Show a close button in the popup (default: false). */\n closeButton?: boolean;\n} & Omit<PopupOptions, \"className\" | \"closeButton\">;\n\n/**\n * A standalone popup anchored at a lng/lat — or, on a canvas with a `plan`\n * extent, at a plan `x`/`y`. Not attached to a marker; typically rendered\n * conditionally from app state (e.g. after a layer click).\n */\nexport function MapPopup({\n longitude: longitudeProp,\n latitude: latitudeProp,\n x,\n y,\n onClose,\n children,\n className,\n closeButton = false,\n ...popupOptions\n}: MapPopupProps) {\n const { map, plan } = useMap();\n const { t } = useLocale();\n const [longitude, latitude] = resolveMapPosition(\n { longitude: longitudeProp, latitude: latitudeProp, x, y },\n plan,\n \"MapPopup\",\n );\n const onCloseRef = useRef(onClose);\n onCloseRef.current = onClose;\n const container = useMemo(() => document.createElement(\"div\"), []);\n const { offset, maxWidth } = popupOptions;\n\n const popup = useMemo(() => {\n return new MapLibreGL.Popup({\n offset: 16,\n ...popupOptions,\n closeButton: false,\n })\n .setMaxWidth(\"none\")\n .setLngLat([longitude, latitude]);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- instance created once; position/options are synced by the effect below\n }, []);\n\n useEffect(() => {\n if (!map) return;\n\n const onCloseProp = () => onCloseRef.current?.();\n\n popup.on(\"close\", onCloseProp);\n\n popup.setDOMContent(container);\n popup.addTo(map);\n\n return () => {\n popup.off(\"close\", onCloseProp);\n if (popup.isOpen()) {\n popup.remove();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- popup/container are stable\n }, [map]);\n\n // Sync popup position and options when they change.\n useEffect(() => {\n const current = popup.getLngLat();\n if (!current || current.lng !== longitude || current.lat !== latitude) {\n popup.setLngLat([longitude, latitude]);\n }\n popup.setOffset(offset ?? 16);\n if (maxWidth) {\n popup.setMaxWidth(maxWidth);\n }\n }, [popup, longitude, latitude, offset, maxWidth]);\n\n const handleClose = () => {\n popup.remove();\n };\n\n return createPortal(\n <div\n className={cn(\n \"relative max-w-62 rounded-md bg-popover p-3 text-popover-foreground shadow-ring-md\",\n \"animate-in fade-in-0 zoom-in-95 duration-fast ease-entrance\",\n className,\n )}\n >\n {closeButton && (\n <button\n type=\"button\"\n onClick={handleClose}\n aria-label={t(\"maps.popup.close\")}\n className=\"absolute top-1 right-1 z-10 inline-flex size-5 items-center justify-center rounded-sm text-foreground transition-colors duration-fast hover:bg-muted focus-ring-inset\"\n >\n <X className=\"size-3.5\" aria-hidden=\"true\" />\n </button>\n )}\n {children}\n </div>,\n container,\n );\n}\n"],"mappings":";;;;;;;;;AAEA,OAAO,gBAAuC;AAC9C,SAAS,WAAW,SAAS,cAA8B;AAC3D,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,UAAU;AACnB,SAAS,iBAAiB;AA2FtB,SAcM,KAdN;AAtEG,SAAS,SAAS;AAAA,EACvB,WAAW;AAAA,EACX,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAAkB;AAChB,QAAM,EAAE,KAAK,KAAK,IAAI,OAAO;AAC7B,QAAM,EAAE,EAAE,IAAI,UAAU;AACxB,QAAM,CAAC,WAAW,QAAQ,IAAI;AAAA,IAC5B,EAAE,WAAW,eAAe,UAAU,cAAc,GAAG,EAAE;AAAA,IACzD;AAAA,IACA;AAAA,EACF;AACA,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AACrB,QAAM,YAAY,QAAQ,MAAM,SAAS,cAAc,KAAK,GAAG,CAAC,CAAC;AACjE,QAAM,EAAE,QAAQ,SAAS,IAAI;AAE7B,QAAM,QAAQ,QAAQ,MAAM;AAC1B,WAAO,IAAI,WAAW,MAAM;AAAA,MAC1B,QAAQ;AAAA,MACR,GAAG;AAAA,MACH,aAAa;AAAA,IACf,CAAC,EACE,YAAY,MAAM,EAClB,UAAU,CAAC,WAAW,QAAQ,CAAC;AAAA,EAEpC,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI,CAAC,IAAK;AAEV,UAAM,cAAc,MAAM,WAAW,UAAU;AAE/C,UAAM,GAAG,SAAS,WAAW;AAE7B,UAAM,cAAc,SAAS;AAC7B,UAAM,MAAM,GAAG;AAEf,WAAO,MAAM;AACX,YAAM,IAAI,SAAS,WAAW;AAC9B,UAAI,MAAM,OAAO,GAAG;AAClB,cAAM,OAAO;AAAA,MACf;AAAA,IACF;AAAA,EAEF,GAAG,CAAC,GAAG,CAAC;AAGR,YAAU,MAAM;AACd,UAAM,UAAU,MAAM,UAAU;AAChC,QAAI,CAAC,WAAW,QAAQ,QAAQ,aAAa,QAAQ,QAAQ,UAAU;AACrE,YAAM,UAAU,CAAC,WAAW,QAAQ,CAAC;AAAA,IACvC;AACA,UAAM,UAAU,UAAU,EAAE;AAC5B,QAAI,UAAU;AACZ,YAAM,YAAY,QAAQ;AAAA,IAC5B;AAAA,EACF,GAAG,CAAC,OAAO,WAAW,UAAU,QAAQ,QAAQ,CAAC;AAEjD,QAAM,cAAc,MAAM;AACxB,UAAM,OAAO;AAAA,EACf;AAEA,SAAO;AAAA,IACL;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QAEC;AAAA,yBACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS;AAAA,cACT,cAAY,EAAE,kBAAkB;AAAA,cAChC,WAAU;AAAA,cAEV,8BAAC,KAAE,WAAU,YAAW,eAAY,QAAO;AAAA;AAAA,UAC7C;AAAA,UAED;AAAA;AAAA;AAAA,IACH;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,185 @@
1
+ "use client";
2
+ import {
3
+ MAP_CORNER_CLASSES
4
+ } from "./chunk-GWULZOMW.js";
5
+ import {
6
+ useMapFrameSlot,
7
+ useMapResponsive
8
+ } from "./chunk-W76NLFER.js";
9
+
10
+ // src/map-legend/map-legend.tsx
11
+ import { forwardRef, useMemo } from "react";
12
+ import { createPortal } from "react-dom";
13
+ import {
14
+ colorScaleFor,
15
+ useLocale
16
+ } from "@elabs-ai/components-ui";
17
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
18
+ import { jsx, jsxs } from "react/jsx-runtime";
19
+ var SHAPE_CLASSES = {
20
+ circle: "size-2.5 rounded-full",
21
+ square: "size-2.5 rounded-xs",
22
+ line: "h-0.5 w-3.5 rounded-full"
23
+ };
24
+ var LAYOUT_CLASSES = {
25
+ list: "flex flex-col gap-1",
26
+ // `auto-fit`, never `auto-fill` (c-5): `auto-fill` keeps the tracks a wide
27
+ // strip could hold even when nothing occupies them, so four keys on an
28
+ // 868 px strip got 113.7 px each and "Greater Toronto Area" truncated with
29
+ // three empty tracks beside it. `auto-fit` collapses the empty tracks and
30
+ // the real keys share the full width; a key with more items than fit wraps
31
+ // exactly as before.
32
+ grid: "grid grid-cols-[repeat(auto-fit,minmax(7rem,1fr))] gap-x-3 gap-y-1"
33
+ };
34
+ function isColorScale(scale) {
35
+ return "colorOf" in scale;
36
+ }
37
+ function Swatch({ color, shape = "circle" }) {
38
+ return /* @__PURE__ */ jsx(
39
+ "span",
40
+ {
41
+ "aria-hidden": "true",
42
+ "data-slot": "map-legend-swatch",
43
+ "data-shape": shape,
44
+ className: cn("inline-block shrink-0", SHAPE_CLASSES[shape]),
45
+ style: { background: color }
46
+ }
47
+ );
48
+ }
49
+ function Ramp({ scale, format }) {
50
+ const ticks = [];
51
+ let strip = null;
52
+ if (scale.type === "stepped" && scale.steps.length > 0) {
53
+ const n = scale.steps.length;
54
+ strip = /* @__PURE__ */ jsx("div", { className: "flex h-2.5 w-full overflow-hidden rounded-xs", children: scale.steps.map((step) => /* @__PURE__ */ jsx(
55
+ "span",
56
+ {
57
+ "data-slot": "map-legend-step",
58
+ className: "min-w-0 flex-1",
59
+ style: { background: step.color }
60
+ },
61
+ `${step.from}-${step.to}`
62
+ )) });
63
+ const first = scale.steps[0];
64
+ if (first) ticks.push({ key: "start", at: 0, label: format(first.from) });
65
+ scale.steps.forEach((step, index) => {
66
+ ticks.push({ key: `to-${step.to}`, at: (index + 1) / n, label: format(step.to) });
67
+ });
68
+ } else if (scale.stops.length > 0) {
69
+ const last = Math.max(1, scale.stops.length - 1);
70
+ const gradient = scale.stops.map((stop, index) => `${stop.color} ${index / last * 100}%`).join(", ");
71
+ strip = /* @__PURE__ */ jsx(
72
+ "div",
73
+ {
74
+ "data-slot": "map-legend-gradient",
75
+ className: "h-2.5 w-full rounded-xs",
76
+ style: { background: `linear-gradient(to right, ${gradient})` }
77
+ }
78
+ );
79
+ const marks = [scale.domain?.[0], scale.center ?? void 0, scale.domain?.[1]];
80
+ for (const value of marks) {
81
+ if (value === void 0) continue;
82
+ const at = scale.positionOf(value);
83
+ if (at === null) continue;
84
+ ticks.push({ key: `v-${value}`, at, label: format(value) });
85
+ }
86
+ }
87
+ if (!strip) return null;
88
+ return (
89
+ // A ramp reads low → high left to right in every writing direction.
90
+ /* @__PURE__ */ jsxs("div", { dir: "ltr", "data-slot": "map-legend-ramp", className: "flex w-48 max-w-full flex-col gap-1", children: [
91
+ strip,
92
+ /* @__PURE__ */ jsx("div", { className: "relative h-4", children: ticks.map((tick) => /* @__PURE__ */ jsx(
93
+ "span",
94
+ {
95
+ "data-slot": "map-legend-tick",
96
+ className: cn(
97
+ "absolute top-0 text-meta whitespace-nowrap text-muted-foreground tabular-nums",
98
+ tick.at <= 0 ? "" : tick.at >= 1 ? "-translate-x-full" : "-translate-x-1/2"
99
+ ),
100
+ style: { left: `${Math.min(1, Math.max(0, tick.at)) * 100}%` },
101
+ children: tick.label
102
+ },
103
+ tick.key
104
+ )) })
105
+ ] })
106
+ );
107
+ }
108
+ var MapLegend = forwardRef(function MapLegend2({
109
+ title,
110
+ items,
111
+ scale: scaleInput,
112
+ layout = "list",
113
+ position: positionProp = "above",
114
+ formatValue,
115
+ className,
116
+ ...props
117
+ }, ref) {
118
+ const { formatNumber } = useLocale();
119
+ const position = useMapResponsive(positionProp);
120
+ const outside = position === "above" || position === "below" ? position : null;
121
+ const slot = useMapFrameSlot(outside);
122
+ const scale = useMemo(() => {
123
+ if (!scaleInput) return null;
124
+ return isColorScale(scaleInput) ? scaleInput : colorScaleFor(scaleInput.values, scaleInput.spec);
125
+ }, [scaleInput]);
126
+ const format = formatValue ?? ((value) => formatNumber(value));
127
+ const rows = [
128
+ ...items ?? [],
129
+ ...scale?.palette === "categorical" ? scale.categories.map((category) => ({
130
+ id: `category-${category.value}`,
131
+ label: String(category.value),
132
+ color: category.color,
133
+ shape: "square"
134
+ })) : []
135
+ ];
136
+ const showRamp = scale !== null && scale.palette !== "categorical";
137
+ const body = /* @__PURE__ */ jsxs(
138
+ "div",
139
+ {
140
+ ref,
141
+ "data-slot": "map-legend",
142
+ "data-position": position,
143
+ "data-layout": layout,
144
+ className: cn(
145
+ "flex flex-col gap-1.5 text-meta text-foreground",
146
+ outside ? "w-full" : cn(
147
+ "absolute z-10 max-w-[calc(100%-1rem)] rounded-md bg-background/90 px-2 py-1.5 shadow-ring-sm",
148
+ MAP_CORNER_CLASSES[position]
149
+ ),
150
+ className
151
+ ),
152
+ ...props,
153
+ children: [
154
+ title && /* @__PURE__ */ jsx("div", { "data-slot": "map-legend-title", className: "font-medium", children: title }),
155
+ rows.length > 0 && /* @__PURE__ */ jsx(
156
+ "ul",
157
+ {
158
+ "data-slot": "map-legend-items",
159
+ className: cn("m-0 list-none p-0", LAYOUT_CLASSES[layout]),
160
+ children: rows.map((item) => /* @__PURE__ */ jsxs(
161
+ "li",
162
+ {
163
+ "data-slot": "map-legend-item",
164
+ className: "flex min-w-0 items-center gap-1.5",
165
+ children: [
166
+ /* @__PURE__ */ jsx(Swatch, { color: item.color, shape: item.shape }),
167
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: item.label })
168
+ ]
169
+ },
170
+ item.id ?? item.label
171
+ ))
172
+ }
173
+ ),
174
+ showRamp && scale && /* @__PURE__ */ jsx(Ramp, { scale, format })
175
+ ]
176
+ }
177
+ );
178
+ if (outside) return slot ? createPortal(body, slot) : null;
179
+ return body;
180
+ });
181
+
182
+ export {
183
+ MapLegend
184
+ };
185
+ //# sourceMappingURL=chunk-V2REM6UT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/map-legend/map-legend.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useMemo, type HTMLAttributes, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n colorScaleFor,\n useLocale,\n type ColorScale,\n type ColorScaleSpec,\n type ColorScaleValue,\n} from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nimport { type MapResponsive, useMapFrameSlot, useMapResponsive } from \"../lib/use-map-breakpoint\";\nimport { MAP_CORNER_CLASSES, type MapCorner } from \"./map-corner\";\n\n/** The mark a key row shows: a point, an area or a line marker. */\nexport type MapLegendShape = \"circle\" | \"square\" | \"line\";\n\n/** One row of a marker key. */\nexport interface MapLegendItem {\n /** Stable identity; defaults to `label`. */\n id?: string;\n label: string;\n /** Any CSS colour — use a token reference (`var(--chart-1)`), never a literal. */\n color: string;\n /** Default `\"circle\"`. */\n shape?: MapLegendShape;\n}\n\n/**\n * A colour scale to key: one `colorScaleFor` already built (share it with the\n * layer it colours), or the values + spec to build one here.\n */\nexport type MapLegendScale =\n | ColorScale\n | { values: readonly ColorScaleValue[]; spec: ColorScaleSpec };\n\n/** Outside the map box (`above` / `below`) or over one of its corners. */\nexport type MapLegendPosition = \"above\" | \"below\" | MapCorner;\n\n/** `list`: one row per item. `grid`: items flow into as many columns as fit. */\nexport type MapLegendLayout = \"list\" | \"grid\";\n\nexport interface MapLegendProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n /** A heading for the key. */\n title?: ReactNode;\n /** Marker key rows. */\n items?: readonly MapLegendItem[];\n /** A colour ramp (continuous / stepped) or category key, from `colorScaleFor`. */\n scale?: MapLegendScale;\n /** Default `\"list\"`. */\n layout?: MapLegendLayout;\n /**\n * Where the key sits, optionally per tier — for example\n * `{ base: \"top-left\", narrow: \"below\" }` so it stops covering a phone-width\n * map. Default `\"above\"`.\n */\n position?: MapResponsive<MapLegendPosition>;\n /** Formats a ramp's tick values (default: the locale's number format). */\n formatValue?: (value: number) => string;\n}\n\nconst SHAPE_CLASSES: Record<MapLegendShape, string> = {\n circle: \"size-2.5 rounded-full\",\n square: \"size-2.5 rounded-xs\",\n line: \"h-0.5 w-3.5 rounded-full\",\n};\n\nconst LAYOUT_CLASSES: Record<MapLegendLayout, string> = {\n list: \"flex flex-col gap-1\",\n // `auto-fit`, never `auto-fill` (c-5): `auto-fill` keeps the tracks a wide\n // strip could hold even when nothing occupies them, so four keys on an\n // 868 px strip got 113.7 px each and \"Greater Toronto Area\" truncated with\n // three empty tracks beside it. `auto-fit` collapses the empty tracks and\n // the real keys share the full width; a key with more items than fit wraps\n // exactly as before.\n grid: \"grid grid-cols-[repeat(auto-fit,minmax(7rem,1fr))] gap-x-3 gap-y-1\",\n};\n\nfunction isColorScale(scale: MapLegendScale): scale is ColorScale {\n return \"colorOf\" in scale;\n}\n\nfunction Swatch({ color, shape = \"circle\" }: { color: string; shape?: MapLegendShape }) {\n return (\n <span\n aria-hidden=\"true\"\n data-slot=\"map-legend-swatch\"\n data-shape={shape}\n className={cn(\"inline-block shrink-0\", SHAPE_CLASSES[shape])}\n style={{ background: color }}\n />\n );\n}\n\ninterface Tick {\n key: string;\n at: number;\n label: string;\n}\n\n/** A ramp: stepped swatches or a continuous gradient, with value ticks under it. */\nfunction Ramp({ scale, format }: { scale: ColorScale; format: (value: number) => string }) {\n const ticks: Tick[] = [];\n let strip: ReactNode = null;\n\n if (scale.type === \"stepped\" && scale.steps.length > 0) {\n const n = scale.steps.length;\n strip = (\n <div className=\"flex h-2.5 w-full overflow-hidden rounded-xs\">\n {scale.steps.map((step) => (\n <span\n key={`${step.from}-${step.to}`}\n data-slot=\"map-legend-step\"\n className=\"min-w-0 flex-1\"\n style={{ background: step.color }}\n />\n ))}\n </div>\n );\n const first = scale.steps[0];\n if (first) ticks.push({ key: \"start\", at: 0, label: format(first.from) });\n scale.steps.forEach((step, index) => {\n ticks.push({ key: `to-${step.to}`, at: (index + 1) / n, label: format(step.to) });\n });\n } else if (scale.stops.length > 0) {\n const last = Math.max(1, scale.stops.length - 1);\n const gradient = scale.stops\n .map((stop, index) => `${stop.color} ${(index / last) * 100}%`)\n .join(\", \");\n strip = (\n <div\n data-slot=\"map-legend-gradient\"\n className=\"h-2.5 w-full rounded-xs\"\n style={{ background: `linear-gradient(to right, ${gradient})` }}\n />\n );\n const marks = [scale.domain?.[0], scale.center ?? undefined, scale.domain?.[1]];\n for (const value of marks) {\n if (value === undefined) continue;\n const at = scale.positionOf(value);\n if (at === null) continue;\n ticks.push({ key: `v-${value}`, at, label: format(value) });\n }\n }\n\n if (!strip) return null;\n return (\n // A ramp reads low → high left to right in every writing direction.\n <div dir=\"ltr\" data-slot=\"map-legend-ramp\" className=\"flex w-48 max-w-full flex-col gap-1\">\n {strip}\n <div className=\"relative h-4\">\n {ticks.map((tick) => (\n <span\n key={tick.key}\n data-slot=\"map-legend-tick\"\n className={cn(\n \"absolute top-0 text-meta whitespace-nowrap text-muted-foreground tabular-nums\",\n tick.at <= 0 ? \"\" : tick.at >= 1 ? \"-translate-x-full\" : \"-translate-x-1/2\",\n )}\n style={{ left: `${Math.min(1, Math.max(0, tick.at)) * 100}%` }}\n >\n {tick.label}\n </span>\n ))}\n </div>\n </div>\n );\n}\n\n/**\n * MapLegend — the map's key: marker rows (`items`) and / or a colour ramp\n * (`scale`, built with `colorScaleFor` from `@elabs-ai/components-ui`, so its\n * colours are token references in every theme). It sits above or below the\n * map box, or over a corner, per tier; the house legend look (`text-meta`,\n * dot markers) matches `charts`' `ChartLegend`. Render inside `<MapCanvas>`.\n */\nexport const MapLegend = forwardRef<HTMLDivElement, MapLegendProps>(function MapLegend(\n {\n title,\n items,\n scale: scaleInput,\n layout = \"list\",\n position: positionProp = \"above\",\n formatValue,\n className,\n ...props\n },\n ref,\n) {\n const { formatNumber } = useLocale();\n const position = useMapResponsive(positionProp);\n const outside = position === \"above\" || position === \"below\" ? position : null;\n const slot = useMapFrameSlot(outside);\n\n const scale = useMemo<ColorScale | null>(() => {\n if (!scaleInput) return null;\n return isColorScale(scaleInput)\n ? scaleInput\n : colorScaleFor(scaleInput.values, scaleInput.spec);\n }, [scaleInput]);\n const format = formatValue ?? ((value: number) => formatNumber(value));\n\n const rows: MapLegendItem[] = [\n ...(items ?? []),\n ...(scale?.palette === \"categorical\"\n ? scale.categories.map((category) => ({\n id: `category-${category.value}`,\n label: String(category.value),\n color: category.color,\n shape: \"square\" as const,\n }))\n : []),\n ];\n const showRamp = scale !== null && scale.palette !== \"categorical\";\n\n const body = (\n <div\n ref={ref}\n data-slot=\"map-legend\"\n data-position={position}\n data-layout={layout}\n className={cn(\n \"flex flex-col gap-1.5 text-meta text-foreground\",\n outside\n ? \"w-full\"\n : cn(\n \"absolute z-10 max-w-[calc(100%-1rem)] rounded-md bg-background/90 px-2 py-1.5 shadow-ring-sm\",\n MAP_CORNER_CLASSES[position as MapCorner],\n ),\n className,\n )}\n {...props}\n >\n {title && (\n <div data-slot=\"map-legend-title\" className=\"font-medium\">\n {title}\n </div>\n )}\n {rows.length > 0 && (\n <ul\n data-slot=\"map-legend-items\"\n className={cn(\"m-0 list-none p-0\", LAYOUT_CLASSES[layout])}\n >\n {rows.map((item) => (\n <li\n key={item.id ?? item.label}\n data-slot=\"map-legend-item\"\n className=\"flex min-w-0 items-center gap-1.5\"\n >\n <Swatch color={item.color} shape={item.shape} />\n <span className=\"min-w-0 truncate\">{item.label}</span>\n </li>\n ))}\n </ul>\n )}\n {showRamp && scale && <Ramp scale={scale} format={format} />}\n </div>\n );\n\n if (outside) return slot ? createPortal(body, slot) : null;\n return body;\n});\n"],"mappings":"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","names":["MapLegend"]}
@@ -0,0 +1,89 @@
1
+ "use client";
2
+ import {
3
+ PLAN_STATUS_ENCODING
4
+ } from "./chunk-E65Z3MSV.js";
5
+
6
+ // src/map-plan/map-plan-table.tsx
7
+ import {
8
+ Table,
9
+ TableBody,
10
+ TableCaption,
11
+ TableCell,
12
+ TableHead,
13
+ TableHeader,
14
+ TableRow
15
+ } from "@elabs-ai/components-ui";
16
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
17
+ import { jsx, jsxs } from "react/jsx-runtime";
18
+ function MapPlanTable({
19
+ regions,
20
+ caption,
21
+ columns,
22
+ status,
23
+ statusLabels,
24
+ statusHeader,
25
+ nameHeader = "Name",
26
+ selectedId,
27
+ onSelect,
28
+ printOnly = false,
29
+ className
30
+ }) {
31
+ const selected = selectedId == null ? [] : Array.isArray(selectedId) ? selectedId : [selectedId];
32
+ return /* @__PURE__ */ jsx("div", { "data-slot": "map-plan-table", className: cn(printOnly && "hidden print:block", className), children: /* @__PURE__ */ jsxs(Table, { children: [
33
+ /* @__PURE__ */ jsx(TableCaption, { children: caption }),
34
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
35
+ /* @__PURE__ */ jsx(TableHead, { scope: "col", children: nameHeader }),
36
+ status && /* @__PURE__ */ jsx(TableHead, { scope: "col", children: statusHeader }),
37
+ columns?.map((column) => /* @__PURE__ */ jsx(TableHead, { scope: "col", className: column.numeric ? "text-end" : "", children: column.header }, column.key))
38
+ ] }) }),
39
+ /* @__PURE__ */ jsx(TableBody, { children: regions.map((region) => {
40
+ const regionStatus = status?.(region);
41
+ const encoding = regionStatus ? PLAN_STATUS_ENCODING[regionStatus] : void 0;
42
+ const isSelected = selected.includes(region.id);
43
+ return /* @__PURE__ */ jsxs(
44
+ TableRow,
45
+ {
46
+ "data-slot": "map-plan-table-row",
47
+ "data-selected": isSelected || void 0,
48
+ "data-status": regionStatus,
49
+ "aria-selected": onSelect ? isSelected : void 0,
50
+ children: [
51
+ /* @__PURE__ */ jsx(TableCell, { children: onSelect ? /* @__PURE__ */ jsx(
52
+ "button",
53
+ {
54
+ type: "button",
55
+ onClick: () => onSelect(region.id, region),
56
+ className: "rounded-sm text-start focus-ring",
57
+ children: region.label
58
+ }
59
+ ) : region.label }),
60
+ status && /* @__PURE__ */ jsx(TableCell, { children: regionStatus && encoding ? /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
61
+ encoding.icon && /* @__PURE__ */ jsx(
62
+ encoding.icon,
63
+ {
64
+ className: cn("size-3.5", encoding.textClass),
65
+ "aria-hidden": "true"
66
+ }
67
+ ),
68
+ /* @__PURE__ */ jsx("span", { className: encoding.textClass, children: statusLabels?.[regionStatus] ?? regionStatus })
69
+ ] }) : null }),
70
+ columns?.map((column) => /* @__PURE__ */ jsx(
71
+ TableCell,
72
+ {
73
+ className: column.numeric ? "text-end tabular-nums" : "",
74
+ children: column.cell(region)
75
+ },
76
+ column.key
77
+ ))
78
+ ]
79
+ },
80
+ region.id
81
+ );
82
+ }) })
83
+ ] }) });
84
+ }
85
+
86
+ export {
87
+ MapPlanTable
88
+ };
89
+ //# sourceMappingURL=chunk-VFBRFGID.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/map-plan/map-plan-table.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport {\n Table,\n TableBody,\n TableCaption,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nimport type { MapPlanRegion } from \"../lib/plan-regions\";\nimport { PLAN_STATUS_ENCODING, type PlanStatus } from \"../lib/plan-status\";\n\nexport interface MapPlanTableColumn {\n /** Column key, used for the cell lookup and the React key. */\n key: string;\n /** Column header text. */\n header: string;\n /** Right-align the column — numbers belong on the end edge. */\n numeric?: boolean;\n /** The cell's content for one region. */\n cell: (region: MapPlanRegion) => ReactNode;\n}\n\nexport interface MapPlanTableProps {\n /** The same regions the plan draws. */\n regions: readonly MapPlanRegion[];\n /** What the table is a table of (\"Every room on level 3, with its state\"). */\n caption: string;\n /** Extra columns after the name. The status column is built in when `status` is given. */\n columns?: readonly MapPlanTableColumn[];\n /** A region's status, if the plan has one. Adds a status column with the word. */\n status?: (region: MapPlanRegion) => PlanStatus | undefined;\n /** The word for each status — supplied by the consumer, so it can be localized. */\n statusLabels?: Partial<Record<PlanStatus, string>>;\n /** Header for the built-in status column. */\n statusHeader?: string;\n /** Header for the name column. */\n nameHeader?: string;\n /** The selected region id(s), mirrored as a row state. */\n selectedId?: string | readonly string[] | null;\n /** Called when a row is chosen — the table is a second way to reach a region. */\n onSelect?: (id: string, region: MapPlanRegion) => void;\n /**\n * Hide the table on screen and keep it for print (default false). A WebGL\n * canvas prints blank, so a plan that is meant to be printed needs this.\n */\n printOnly?: boolean;\n className?: string;\n}\n\n/**\n * The plan as words — the parallel channel for anyone who cannot see the canvas,\n * for anyone who needs to sort or search it, and for the printer, which gets a\n * blank rectangle where the WebGL canvas was.\n *\n * Visible by default rather than `sr-only`: the information is useful to\n * everyone, and a hidden twin quietly rots.\n */\nexport function MapPlanTable({\n regions,\n caption,\n columns,\n status,\n statusLabels,\n statusHeader,\n nameHeader = \"Name\",\n selectedId,\n onSelect,\n printOnly = false,\n className,\n}: MapPlanTableProps) {\n const selected = selectedId == null ? [] : Array.isArray(selectedId) ? selectedId : [selectedId];\n\n return (\n <div data-slot=\"map-plan-table\" className={cn(printOnly && \"hidden print:block\", className)}>\n <Table>\n <TableCaption>{caption}</TableCaption>\n <TableHeader>\n <TableRow>\n <TableHead scope=\"col\">{nameHeader}</TableHead>\n {status && <TableHead scope=\"col\">{statusHeader}</TableHead>}\n {columns?.map((column) => (\n <TableHead key={column.key} scope=\"col\" className={column.numeric ? \"text-end\" : \"\"}>\n {column.header}\n </TableHead>\n ))}\n </TableRow>\n </TableHeader>\n <TableBody>\n {regions.map((region) => {\n const regionStatus = status?.(region);\n const encoding = regionStatus ? PLAN_STATUS_ENCODING[regionStatus] : undefined;\n const isSelected = selected.includes(region.id);\n\n return (\n <TableRow\n key={region.id}\n data-slot=\"map-plan-table-row\"\n data-selected={isSelected || undefined}\n data-status={regionStatus}\n aria-selected={onSelect ? isSelected : undefined}\n >\n <TableCell>\n {onSelect ? (\n <button\n type=\"button\"\n onClick={() => onSelect(region.id, region)}\n className=\"rounded-sm text-start focus-ring\"\n >\n {region.label}\n </button>\n ) : (\n region.label\n )}\n </TableCell>\n {status && (\n <TableCell>\n {regionStatus && encoding ? (\n <span className=\"inline-flex items-center gap-1.5\">\n {encoding.icon && (\n <encoding.icon\n className={cn(\"size-3.5\", encoding.textClass)}\n aria-hidden=\"true\"\n />\n )}\n <span className={encoding.textClass}>\n {statusLabels?.[regionStatus] ?? regionStatus}\n </span>\n </span>\n ) : null}\n </TableCell>\n )}\n {columns?.map((column) => (\n <TableCell\n key={column.key}\n className={column.numeric ? \"text-end tabular-nums\" : \"\"}\n >\n {column.cell(region)}\n </TableCell>\n ))}\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n );\n}\n"],"mappings":";;;;;;AAGA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,UAAU;AAqEX,cAEE,YAFF;AAlBD,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AACF,GAAsB;AACpB,QAAM,WAAW,cAAc,OAAO,CAAC,IAAI,MAAM,QAAQ,UAAU,IAAI,aAAa,CAAC,UAAU;AAE/F,SACE,oBAAC,SAAI,aAAU,kBAAiB,WAAW,GAAG,aAAa,sBAAsB,SAAS,GACxF,+BAAC,SACC;AAAA,wBAAC,gBAAc,mBAAQ;AAAA,IACvB,oBAAC,eACC,+BAAC,YACC;AAAA,0BAAC,aAAU,OAAM,OAAO,sBAAW;AAAA,MAClC,UAAU,oBAAC,aAAU,OAAM,OAAO,wBAAa;AAAA,MAC/C,SAAS,IAAI,CAAC,WACb,oBAAC,aAA2B,OAAM,OAAM,WAAW,OAAO,UAAU,aAAa,IAC9E,iBAAO,UADM,OAAO,GAEvB,CACD;AAAA,OACH,GACF;AAAA,IACA,oBAAC,aACE,kBAAQ,IAAI,CAAC,WAAW;AACvB,YAAM,eAAe,SAAS,MAAM;AACpC,YAAM,WAAW,eAAe,qBAAqB,YAAY,IAAI;AACrE,YAAM,aAAa,SAAS,SAAS,OAAO,EAAE;AAE9C,aACE;AAAA,QAAC;AAAA;AAAA,UAEC,aAAU;AAAA,UACV,iBAAe,cAAc;AAAA,UAC7B,eAAa;AAAA,UACb,iBAAe,WAAW,aAAa;AAAA,UAEvC;AAAA,gCAAC,aACE,qBACC;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS,MAAM,SAAS,OAAO,IAAI,MAAM;AAAA,gBACzC,WAAU;AAAA,gBAET,iBAAO;AAAA;AAAA,YACV,IAEA,OAAO,OAEX;AAAA,YACC,UACC,oBAAC,aACE,0BAAgB,WACf,qBAAC,UAAK,WAAU,oCACb;AAAA,uBAAS,QACR;AAAA,gBAAC,SAAS;AAAA,gBAAT;AAAA,kBACC,WAAW,GAAG,YAAY,SAAS,SAAS;AAAA,kBAC5C,eAAY;AAAA;AAAA,cACd;AAAA,cAEF,oBAAC,UAAK,WAAW,SAAS,WACvB,yBAAe,YAAY,KAAK,cACnC;AAAA,eACF,IACE,MACN;AAAA,YAED,SAAS,IAAI,CAAC,WACb;AAAA,cAAC;AAAA;AAAA,gBAEC,WAAW,OAAO,UAAU,0BAA0B;AAAA,gBAErD,iBAAO,KAAK,MAAM;AAAA;AAAA,cAHd,OAAO;AAAA,YAId,CACD;AAAA;AAAA;AAAA,QA3CI,OAAO;AAAA,MA4Cd;AAAA,IAEJ,CAAC,GACH;AAAA,KACF,GACF;AAEJ;","names":[]}
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ // src/lib/use-map-breakpoint.ts
4
+ import {
5
+ createContext,
6
+ useContext,
7
+ useEffect,
8
+ useLayoutEffect,
9
+ useState
10
+ } from "react";
11
+ var MAP_BREAKPOINTS = ["narrow", "medium", "wide"];
12
+ var MAP_BREAKPOINT_THRESHOLDS = { narrow: 480, medium: 768 };
13
+ function mapBreakpointForWidth(width) {
14
+ if (!Number.isFinite(width) || width <= 0) return "wide";
15
+ if (width < MAP_BREAKPOINT_THRESHOLDS.narrow) return "narrow";
16
+ if (width < MAP_BREAKPOINT_THRESHOLDS.medium) return "medium";
17
+ return "wide";
18
+ }
19
+ function isMapResponsiveByBreakpoint(value) {
20
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.hasOwnProperty.call(value, "base");
21
+ }
22
+ function resolveMapResponsive(value, breakpoint) {
23
+ if (!isMapResponsiveByBreakpoint(value)) return value;
24
+ if (breakpoint === "wide") return value.base;
25
+ if (breakpoint === "narrow" && value.narrow !== void 0) return value.narrow;
26
+ return value.medium !== void 0 ? value.medium : value.base;
27
+ }
28
+ var DEFAULT_MAP_HEIGHT = {
29
+ base: { aspect: 1.6 },
30
+ narrow: { aspect: 1 }
31
+ };
32
+ function validHeight(value) {
33
+ if (typeof value === "number") return Number.isFinite(value) && value > 0 ? value : void 0;
34
+ if (value && Number.isFinite(value.aspect) && value.aspect > 0) return value;
35
+ return void 0;
36
+ }
37
+ var NO_GEOMETRY_TRANSITION = { transitionProperty: "none" };
38
+ function resolveMapHeightStyle(height, breakpoint) {
39
+ const own = height === void 0 ? void 0 : validHeight(resolveMapResponsive(height, breakpoint));
40
+ if (typeof own === "number") return { ...NO_GEOMETRY_TRANSITION, height: own };
41
+ if (own) return { ...NO_GEOMETRY_TRANSITION, height: "auto", aspectRatio: `${own.aspect} / 1` };
42
+ const fallback = resolveMapResponsive(DEFAULT_MAP_HEIGHT, breakpoint);
43
+ return typeof fallback === "number" ? { ...NO_GEOMETRY_TRANSITION } : { ...NO_GEOMETRY_TRANSITION, aspectRatio: `${fallback.aspect} / 1` };
44
+ }
45
+ function useMeasuredMapBreakpoint(node) {
46
+ const [breakpoint, setBreakpoint] = useState("wide");
47
+ useLayoutEffect(() => {
48
+ if (!node) return void 0;
49
+ const update = (width) => {
50
+ const next = mapBreakpointForWidth(width);
51
+ setBreakpoint((prev) => prev === next ? prev : next);
52
+ };
53
+ update(node.getBoundingClientRect().width);
54
+ if (typeof ResizeObserver === "undefined") return void 0;
55
+ const observer = new ResizeObserver((entries) => {
56
+ const entry = entries[entries.length - 1];
57
+ if (entry) update(entry.contentRect.width);
58
+ });
59
+ observer.observe(node);
60
+ return () => observer.disconnect();
61
+ }, [node]);
62
+ return breakpoint;
63
+ }
64
+ var noopRelease = () => {
65
+ };
66
+ var DEFAULT_FRAME = {
67
+ breakpoint: "wide",
68
+ interactive: true,
69
+ slots: { above: null, below: null },
70
+ requestSlot: () => noopRelease
71
+ };
72
+ var MapFrameContext = createContext(DEFAULT_FRAME);
73
+ function useMapFrame() {
74
+ return useContext(MapFrameContext);
75
+ }
76
+ function useMapBreakpoint() {
77
+ return useContext(MapFrameContext).breakpoint;
78
+ }
79
+ function useMapResponsive(value) {
80
+ return resolveMapResponsive(value, useMapBreakpoint());
81
+ }
82
+ function useMapFrameSlot(side) {
83
+ const { slots, requestSlot } = useContext(MapFrameContext);
84
+ useEffect(() => side ? requestSlot(side) : void 0, [side, requestSlot]);
85
+ return side ? slots[side] : null;
86
+ }
87
+
88
+ export {
89
+ MAP_BREAKPOINTS,
90
+ MAP_BREAKPOINT_THRESHOLDS,
91
+ mapBreakpointForWidth,
92
+ isMapResponsiveByBreakpoint,
93
+ resolveMapResponsive,
94
+ DEFAULT_MAP_HEIGHT,
95
+ resolveMapHeightStyle,
96
+ useMeasuredMapBreakpoint,
97
+ MapFrameContext,
98
+ useMapFrame,
99
+ useMapBreakpoint,
100
+ useMapResponsive,
101
+ useMapFrameSlot
102
+ };
103
+ //# sourceMappingURL=chunk-W76NLFER.js.map