@elabs-ai/components-maps 4.1.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 -1456
  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 +421 -1
  242. package/src/map-canvas/map-canvas.tsx +466 -20
  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 +383 -3
  262. package/src/map-marker/map-marker.tsx +414 -44
  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 +18 -11
  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 +267 -20
@@ -0,0 +1,247 @@
1
+ "use client";
2
+ import {
3
+ resolveMapResponsive,
4
+ useMapBreakpoint
5
+ } from "./chunk-W76NLFER.js";
6
+ import {
7
+ useMap
8
+ } from "./chunk-JYFERXZT.js";
9
+ import {
10
+ clampShift,
11
+ mapAnchorGeometry
12
+ } from "./chunk-4DANBD5B.js";
13
+
14
+ // src/map-annotation/map-annotation.tsx
15
+ import {
16
+ createContext,
17
+ forwardRef,
18
+ useCallback,
19
+ useContext,
20
+ useId,
21
+ useLayoutEffect,
22
+ useMemo,
23
+ useRef,
24
+ useState
25
+ } from "react";
26
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
27
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
28
+ var NO_ROWS = [];
29
+ var MapAnnotationRegistryContext = createContext(null);
30
+ function byDocumentOrder(a, b) {
31
+ if (a.el === b.el) return 0;
32
+ return a.el.compareDocumentPosition(b.el) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
33
+ }
34
+ function useMapAnnotationRegistry(breakpoint) {
35
+ const [entries, setEntries] = useState(() => /* @__PURE__ */ new Map());
36
+ const register = useCallback((entry) => {
37
+ setEntries((prev) => {
38
+ const current = prev.get(entry.id);
39
+ if (current && current.el === entry.el && current.text === entry.text && current.hidden === entry.hidden) {
40
+ return prev;
41
+ }
42
+ const next = new Map(prev);
43
+ next.set(entry.id, entry);
44
+ return next;
45
+ });
46
+ return () => {
47
+ setEntries((prev) => {
48
+ if (!prev.has(entry.id)) return prev;
49
+ const next = new Map(prev);
50
+ next.delete(entry.id);
51
+ return next;
52
+ });
53
+ };
54
+ }, []);
55
+ const rows = useMemo(() => {
56
+ if (breakpoint !== "narrow") return NO_ROWS;
57
+ return [...entries.values()].filter((entry) => !entry.hidden && entry.el.isConnected).sort(byDocumentOrder).map((entry, index) => ({ id: entry.id, number: index + 1, text: entry.text }));
58
+ }, [entries, breakpoint]);
59
+ const value = useMemo(() => {
60
+ const numbers = new Map(rows.map((row) => [row.id, row.number]));
61
+ return { register, numberOf: (id) => numbers.get(id) };
62
+ }, [register, rows]);
63
+ return { value, rows };
64
+ }
65
+ var MapAnnotationRegistryProvider = MapAnnotationRegistryContext.Provider;
66
+ var MapAnnotationKey = forwardRef(
67
+ function MapAnnotationKey2({ rows, className, ...props }, ref) {
68
+ return /* @__PURE__ */ jsx(
69
+ "ol",
70
+ {
71
+ ref,
72
+ "data-slot": "map-annotation-key",
73
+ "data-count": rows.length,
74
+ className: cn("m-0 flex list-none flex-col gap-1 p-0", className),
75
+ ...props,
76
+ children: rows.map((row) => /* @__PURE__ */ jsxs(
77
+ "li",
78
+ {
79
+ "data-slot": "map-annotation-key-item",
80
+ className: "flex items-start gap-1.5 text-caption text-foreground",
81
+ children: [
82
+ /* @__PURE__ */ jsx(NumberBadge, { number: row.number }),
83
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 break-words", children: row.text })
84
+ ]
85
+ },
86
+ row.id
87
+ ))
88
+ }
89
+ );
90
+ }
91
+ );
92
+ function NumberBadge({ number, className }) {
93
+ return /* @__PURE__ */ jsx(
94
+ "span",
95
+ {
96
+ "aria-hidden": "true",
97
+ "data-slot": "map-annotation-number",
98
+ className: cn(
99
+ "inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-foreground text-meta font-semibold text-background tabular-nums",
100
+ className
101
+ ),
102
+ children: number
103
+ }
104
+ );
105
+ }
106
+ var LEADER_DISTANCE = 28;
107
+ var PLAIN_DISTANCE = 6;
108
+ var MapAnnotation = forwardRef(function MapAnnotation2({
109
+ longitude,
110
+ latitude,
111
+ text,
112
+ anchor = "top-right",
113
+ connector = {},
114
+ showAt = true,
115
+ priority = 0,
116
+ className,
117
+ style,
118
+ ...props
119
+ }, forwardedRef) {
120
+ const { map } = useMap();
121
+ const registry = useContext(MapAnnotationRegistryContext);
122
+ const breakpoint = useMapBreakpoint();
123
+ const id = useId();
124
+ const [root, setRoot] = useState(null);
125
+ const boxRef = useRef(null);
126
+ const lineRef = useRef(null);
127
+ const hidden = !resolveMapResponsive(showAt, breakpoint);
128
+ const number = registry?.numberOf(id);
129
+ const keyed = number !== void 0;
130
+ const setRefs = useCallback(
131
+ (node) => {
132
+ setRoot(node);
133
+ if (typeof forwardedRef === "function") forwardedRef(node);
134
+ else if (forwardedRef) forwardedRef.current = node;
135
+ },
136
+ [forwardedRef]
137
+ );
138
+ const register = registry?.register;
139
+ useLayoutEffect(() => {
140
+ if (!register || !root) return void 0;
141
+ return register({ id, el: root, text, hidden });
142
+ }, [register, id, root, text, hidden]);
143
+ const distance = connector === false ? PLAIN_DISTANCE : LEADER_DISTANCE;
144
+ useLayoutEffect(() => {
145
+ if (!map || !root || hidden) return void 0;
146
+ const container = map.getContainer();
147
+ const { dx, dy, fx, fy } = mapAnchorGeometry(anchor, distance);
148
+ const update = () => {
149
+ const point = map.project([longitude, latitude]);
150
+ const width = container.clientWidth;
151
+ const height = container.clientHeight;
152
+ const inside = point.x >= 0 && point.y >= 0 && point.x <= width && point.y <= height;
153
+ root.style.visibility = inside ? "" : "hidden";
154
+ root.style.transform = `translate(${point.x}px, ${point.y}px)`;
155
+ const box = boxRef.current;
156
+ if (!box) return;
157
+ const boxWidth = box.offsetWidth;
158
+ const boxHeight = box.offsetHeight;
159
+ const left = point.x + dx + fx * boxWidth;
160
+ const top = point.y + dy + fy * boxHeight;
161
+ const { sx, sy } = clampShift(left, top, boxWidth, boxHeight, width, height);
162
+ box.style.transform = `translate(${dx + sx + fx * boxWidth}px, ${dy + sy + fy * boxHeight}px)`;
163
+ const line = lineRef.current;
164
+ if (line) {
165
+ line.setAttribute("x2", String(dx + sx));
166
+ line.setAttribute("y2", String(dy + sy));
167
+ }
168
+ };
169
+ update();
170
+ map.on("move", update);
171
+ map.on("resize", update);
172
+ return () => {
173
+ map.off("move", update);
174
+ map.off("resize", update);
175
+ };
176
+ }, [map, root, hidden, keyed, anchor, distance, longitude, latitude, text]);
177
+ const arrowId = `${id}-arrow`;
178
+ return /* @__PURE__ */ jsx(
179
+ "div",
180
+ {
181
+ ref: setRefs,
182
+ "data-slot": "map-annotation",
183
+ "data-display": hidden ? "hidden" : keyed ? "keyed" : "painted",
184
+ hidden,
185
+ className: cn("pointer-events-none absolute", className),
186
+ style: { left: 0, top: 0, zIndex: 1 + Math.max(0, Math.min(8, priority)), ...style },
187
+ ...props,
188
+ children: keyed ? /* @__PURE__ */ jsx(NumberBadge, { number, className: "absolute -translate-x-1/2 -translate-y-1/2" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
189
+ connector !== false && /* @__PURE__ */ jsxs(
190
+ "svg",
191
+ {
192
+ "aria-hidden": "true",
193
+ "data-slot": "map-annotation-connector",
194
+ className: "absolute overflow-visible text-foreground",
195
+ width: 1,
196
+ height: 1,
197
+ children: [
198
+ connector.arrow && /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx(
199
+ "marker",
200
+ {
201
+ id: arrowId,
202
+ viewBox: "0 0 8 8",
203
+ refX: 0,
204
+ refY: 4,
205
+ markerWidth: 8,
206
+ markerHeight: 8,
207
+ orient: "auto",
208
+ children: /* @__PURE__ */ jsx("path", { d: "M8 0 L0 4 L8 8 Z", fill: "currentColor" })
209
+ }
210
+ ) }),
211
+ /* @__PURE__ */ jsx(
212
+ "line",
213
+ {
214
+ ref: lineRef,
215
+ x1: 0,
216
+ y1: 0,
217
+ x2: 0,
218
+ y2: 0,
219
+ stroke: "currentColor",
220
+ strokeWidth: 1,
221
+ markerStart: connector.arrow ? `url(#${arrowId})` : void 0
222
+ }
223
+ )
224
+ ]
225
+ }
226
+ ),
227
+ /* @__PURE__ */ jsx(
228
+ "div",
229
+ {
230
+ ref: boxRef,
231
+ "data-slot": "map-annotation-text",
232
+ className: "absolute w-max max-w-48 rounded-sm bg-background/85 px-1 text-caption text-foreground",
233
+ children: text
234
+ }
235
+ )
236
+ ] })
237
+ }
238
+ );
239
+ });
240
+
241
+ export {
242
+ useMapAnnotationRegistry,
243
+ MapAnnotationRegistryProvider,
244
+ MapAnnotationKey,
245
+ MapAnnotation
246
+ };
247
+ //# sourceMappingURL=chunk-QDWWP3N7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/map-annotation/map-annotation.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n forwardRef,\n useCallback,\n useContext,\n useId,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nimport { useMap } from \"../map-canvas/map-context\";\nimport {\n type MapBreakpoint,\n type MapResponsive,\n useMapBreakpoint,\n resolveMapResponsive,\n} from \"../lib/use-map-breakpoint\";\nimport { type MapLabelAnchor, clampShift, mapAnchorGeometry } from \"./anchor\";\n\n// ── Registry ─────────────────────────────────────────────────────────────────\n//\n// Every `<MapAnnotation>` in a canvas registers here, so the canvas can number\n// them and render ONE key under the map at `narrow`. Numbers follow reading\n// order — the annotations' DOM order, which is their JSX order — and skip an\n// annotation hidden at the current tier (the RM-111 rule `charts` uses).\n\ninterface AnnotationEntry {\n id: string;\n el: HTMLElement;\n text: ReactNode;\n hidden: boolean;\n}\n\n/** One row of the narrow key. */\nexport interface MapAnnotationKeyRow {\n id: string;\n number: number;\n text: ReactNode;\n}\n\ninterface MapAnnotationRegistryValue {\n register: (entry: AnnotationEntry) => () => void;\n /** The number an annotation shows at this tier, or `undefined` when it paints its text. */\n numberOf: (id: string) => number | undefined;\n}\n\nconst NO_ROWS: MapAnnotationKeyRow[] = [];\n\nconst MapAnnotationRegistryContext = createContext<MapAnnotationRegistryValue | null>(null);\n\nfunction byDocumentOrder(a: AnnotationEntry, b: AnnotationEntry): number {\n if (a.el === b.el) return 0;\n return a.el.compareDocumentPosition(b.el) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;\n}\n\n/**\n * Internal: the annotation registry a `<MapCanvas>` owns. Returns the context\n * value to provide and the key rows to render under the map (empty unless the\n * tier is `narrow`).\n */\nexport function useMapAnnotationRegistry(breakpoint: MapBreakpoint): {\n value: MapAnnotationRegistryValue;\n rows: MapAnnotationKeyRow[];\n} {\n const [entries, setEntries] = useState<ReadonlyMap<string, AnnotationEntry>>(() => new Map());\n\n const register = useCallback((entry: AnnotationEntry) => {\n setEntries((prev) => {\n const current = prev.get(entry.id);\n if (\n current &&\n current.el === entry.el &&\n current.text === entry.text &&\n current.hidden === entry.hidden\n ) {\n return prev;\n }\n const next = new Map(prev);\n next.set(entry.id, entry);\n return next;\n });\n return () => {\n setEntries((prev) => {\n if (!prev.has(entry.id)) return prev;\n const next = new Map(prev);\n next.delete(entry.id);\n return next;\n });\n };\n }, []);\n\n const rows = useMemo<MapAnnotationKeyRow[]>(() => {\n if (breakpoint !== \"narrow\") return NO_ROWS;\n return [...entries.values()]\n .filter((entry) => !entry.hidden && entry.el.isConnected)\n .sort(byDocumentOrder)\n .map((entry, index) => ({ id: entry.id, number: index + 1, text: entry.text }));\n }, [entries, breakpoint]);\n\n const value = useMemo<MapAnnotationRegistryValue>(() => {\n const numbers = new Map(rows.map((row) => [row.id, row.number]));\n return { register, numberOf: (id) => numbers.get(id) };\n }, [register, rows]);\n\n return { value, rows };\n}\n\n/** Internal: provides the registry `useMapAnnotationRegistry` built. */\nexport const MapAnnotationRegistryProvider = MapAnnotationRegistryContext.Provider;\n\n// ── Key ──────────────────────────────────────────────────────────────────────\n\nexport interface MapAnnotationKeyProps extends HTMLAttributes<HTMLOListElement> {\n /** The rows, in reading order. */\n rows: readonly MapAnnotationKeyRow[];\n}\n\n/**\n * The numbered list `<MapCanvas>` renders under the map at `narrow`, where each\n * `<MapAnnotation>` shows only its number on the map: \"1 Lake Ontario, …\".\n * NOT `aria-hidden` — at `narrow` this list is the only place the text lives.\n */\nexport const MapAnnotationKey = forwardRef<HTMLOListElement, MapAnnotationKeyProps>(\n function MapAnnotationKey({ rows, className, ...props }, ref) {\n return (\n <ol\n ref={ref}\n data-slot=\"map-annotation-key\"\n data-count={rows.length}\n className={cn(\"m-0 flex list-none flex-col gap-1 p-0\", className)}\n {...props}\n >\n {rows.map((row) => (\n <li\n key={row.id}\n data-slot=\"map-annotation-key-item\"\n className=\"flex items-start gap-1.5 text-caption text-foreground\"\n >\n <NumberBadge number={row.number} />\n <span className=\"min-w-0 break-words\">{row.text}</span>\n </li>\n ))}\n </ol>\n );\n },\n);\n\nfunction NumberBadge({ number, className }: { number: number; className?: string }) {\n return (\n <span\n aria-hidden=\"true\"\n data-slot=\"map-annotation-number\"\n className={cn(\n \"inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-foreground text-meta font-semibold text-background tabular-nums\",\n className,\n )}\n >\n {number}\n </span>\n );\n}\n\n// ── Annotation ───────────────────────────────────────────────────────────────\n\n/** A leader line from the point to the text; `arrow` puts an arrowhead at the point. */\nexport interface MapAnnotationConnector {\n arrow?: boolean;\n}\n\nexport interface MapAnnotationProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Longitude of the point the note is about. */\n longitude: number;\n /** Latitude of the point the note is about. */\n latitude: number;\n /** The note. Plain text reads best; it moves into the key at `narrow`. */\n text: ReactNode;\n /** Where the text sits relative to the point (default `\"top-right\"`). */\n anchor?: MapLabelAnchor;\n /**\n * The leader line from the point to the text (default: a plain line). Pass\n * `false` to set the text right beside the point with no line.\n */\n connector?: MapAnnotationConnector | false;\n /**\n * Whether the note shows at a tier (default `true` everywhere). A hidden note\n * takes no number, so the key stays gap-free.\n */\n showAt?: MapResponsive<boolean>;\n /** Paint order among annotations: a higher one paints on top (0–8, default 0). */\n priority?: number;\n}\n\nconst LEADER_DISTANCE = 28;\nconst PLAIN_DISTANCE = 6;\n\n/**\n * A text note pinned to a lon / lat with a leader line — an HTML overlay that\n * follows the map. At `narrow` the text gives way to a numbered marker and\n * `<MapCanvas>` lists the notes, in reading order, in a key under the map.\n * The text box clamps inside the map on both axes. Render inside `<MapCanvas>`.\n */\nexport const MapAnnotation = forwardRef<HTMLDivElement, MapAnnotationProps>(function MapAnnotation(\n {\n longitude,\n latitude,\n text,\n anchor = \"top-right\",\n connector = {},\n showAt = true,\n priority = 0,\n className,\n style,\n ...props\n },\n forwardedRef,\n) {\n const { map } = useMap();\n const registry = useContext(MapAnnotationRegistryContext);\n const breakpoint = useMapBreakpoint();\n const id = useId();\n const [root, setRoot] = useState<HTMLDivElement | null>(null);\n const boxRef = useRef<HTMLDivElement>(null);\n const lineRef = useRef<SVGLineElement>(null);\n\n const hidden = !resolveMapResponsive(showAt, breakpoint);\n const number = registry?.numberOf(id);\n const keyed = number !== undefined;\n\n const setRefs = useCallback(\n (node: HTMLDivElement | null) => {\n setRoot(node);\n if (typeof forwardedRef === \"function\") forwardedRef(node);\n else if (forwardedRef) forwardedRef.current = node;\n },\n [forwardedRef],\n );\n\n // Depend on the stable `register`, never the registry value: that changes\n // with every registration and would re-register in a loop.\n const register = registry?.register;\n useLayoutEffect(() => {\n if (!register || !root) return undefined;\n return register({ id, el: root, text, hidden });\n }, [register, id, root, text, hidden]);\n\n const distance = connector === false ? PLAIN_DISTANCE : LEADER_DISTANCE;\n\n // Follow the map. Transforms are written straight to the DOM on every move\n // (no React render per frame); the text box is measured and clamped inside\n // the map, and the leader line's end moves with it.\n useLayoutEffect(() => {\n if (!map || !root || hidden) return undefined;\n const container = map.getContainer();\n const { dx, dy, fx, fy } = mapAnchorGeometry(anchor, distance);\n const update = () => {\n const point = map.project([longitude, latitude]);\n const width = container.clientWidth;\n const height = container.clientHeight;\n const inside = point.x >= 0 && point.y >= 0 && point.x <= width && point.y <= height;\n root.style.visibility = inside ? \"\" : \"hidden\";\n root.style.transform = `translate(${point.x}px, ${point.y}px)`;\n const box = boxRef.current;\n if (!box) return;\n const boxWidth = box.offsetWidth;\n const boxHeight = box.offsetHeight;\n const left = point.x + dx + fx * boxWidth;\n const top = point.y + dy + fy * boxHeight;\n const { sx, sy } = clampShift(left, top, boxWidth, boxHeight, width, height);\n box.style.transform = `translate(${dx + sx + fx * boxWidth}px, ${dy + sy + fy * boxHeight}px)`;\n const line = lineRef.current;\n if (line) {\n line.setAttribute(\"x2\", String(dx + sx));\n line.setAttribute(\"y2\", String(dy + sy));\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, root, hidden, keyed, anchor, distance, longitude, latitude, text]);\n\n const arrowId = `${id}-arrow`;\n\n return (\n <div\n ref={setRefs}\n data-slot=\"map-annotation\"\n data-display={hidden ? \"hidden\" : keyed ? \"keyed\" : \"painted\"}\n hidden={hidden}\n className={cn(\"pointer-events-none absolute\", className)}\n // Geographic overlay: a projected pixel offset from the map's top-left\n // corner, physical in every writing direction (like MapLibre's markers).\n style={{ left: 0, top: 0, zIndex: 1 + Math.max(0, Math.min(8, priority)), ...style }}\n {...props}\n >\n {keyed ? (\n <NumberBadge number={number} className=\"absolute -translate-x-1/2 -translate-y-1/2\" />\n ) : (\n <>\n {connector !== false && (\n <svg\n aria-hidden=\"true\"\n data-slot=\"map-annotation-connector\"\n className=\"absolute overflow-visible text-foreground\"\n width={1}\n height={1}\n >\n {connector.arrow && (\n <defs>\n <marker\n id={arrowId}\n viewBox=\"0 0 8 8\"\n refX={0}\n refY={4}\n markerWidth={8}\n markerHeight={8}\n orient=\"auto\"\n >\n <path d=\"M8 0 L0 4 L8 8 Z\" fill=\"currentColor\" />\n </marker>\n </defs>\n )}\n <line\n ref={lineRef}\n x1={0}\n y1={0}\n x2={0}\n y2={0}\n stroke=\"currentColor\"\n strokeWidth={1}\n markerStart={connector.arrow ? `url(#${arrowId})` : undefined}\n />\n </svg>\n )}\n <div\n ref={boxRef}\n data-slot=\"map-annotation-text\"\n className=\"absolute w-max max-w-48 rounded-sm bg-background/85 px-1 text-caption text-foreground\"\n >\n {text}\n </div>\n </>\n )}\n </div>\n );\n});\n"],"mappings":"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","names":["MapAnnotationKey","MapAnnotation"]}
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ //# sourceMappingURL=chunk-QU33KPMP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ //# sourceMappingURL=chunk-RK4BUMHS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ // src/lib/merge-hover-paint.ts
4
+ function mergeFeatureStatePaint(paint, states) {
5
+ if (!states.hover && !states.selected) return paint;
6
+ const merged = { ...paint };
7
+ for (const state of ["hover", "selected"]) {
8
+ const statePaint = states[state];
9
+ if (!statePaint) continue;
10
+ for (const [key, stateValue] of Object.entries(statePaint)) {
11
+ if (stateValue === void 0) continue;
12
+ const baseValue = merged[key];
13
+ merged[key] = baseValue === void 0 ? stateValue : ["case", ["boolean", ["feature-state", state], false], stateValue, baseValue];
14
+ }
15
+ }
16
+ return merged;
17
+ }
18
+ function mergeHoverPaint(paint, hoverPaint) {
19
+ return mergeFeatureStatePaint(paint, { hover: hoverPaint });
20
+ }
21
+
22
+ export {
23
+ mergeFeatureStatePaint,
24
+ mergeHoverPaint
25
+ };
26
+ //# sourceMappingURL=chunk-RNSLG64F.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/merge-hover-paint.ts"],"sourcesContent":["/** Per-feature paint states a layer can express through MapLibre feature-state. */\nexport interface FeatureStatePaint<T> {\n /** Applied to the feature under the cursor or keyboard focus. */\n hover?: T;\n /** Applied to the selected feature(s); wins over `hover`. */\n selected?: T;\n}\n\n/**\n * Merge hover/selected paint into base paint as MapLibre `case` expressions\n * keyed on per-feature feature-state, so only the affected features change\n * appearance. `selected` is tested first, so a selected feature keeps looking\n * selected while the cursor is over it.\n *\n * Only paint properties that accept feature-state belong here (`fill-color`,\n * `fill-opacity`, `line-color`, `line-width`, …). `fill-pattern` and\n * `line-dasharray` do NOT: they read feature PROPERTIES, which is why status\n * texture and selection are separate channels.\n */\nexport function mergeFeatureStatePaint<T extends Record<string, unknown>>(\n paint: T,\n states: FeatureStatePaint<T>,\n): T {\n // Identity preserved when there is nothing to merge: callers memoize on it.\n if (!states.hover && !states.selected) return paint;\n\n const merged: Record<string, unknown> = { ...paint };\n\n // Applied hover-first so that `selected` ends up as the OUTER case.\n for (const state of [\"hover\", \"selected\"] as const) {\n const statePaint = states[state];\n if (!statePaint) continue;\n for (const [key, stateValue] of Object.entries(statePaint)) {\n if (stateValue === undefined) continue;\n const baseValue = merged[key];\n merged[key] =\n baseValue === undefined\n ? stateValue\n : [\"case\", [\"boolean\", [\"feature-state\", state], false], stateValue, baseValue];\n }\n }\n\n return merged as T;\n}\n\n/** Hover-only shorthand for {@link mergeFeatureStatePaint}. */\nexport function mergeHoverPaint<T extends Record<string, unknown>>(\n paint: T,\n hoverPaint: T | undefined,\n): T {\n return mergeFeatureStatePaint(paint, { hover: hoverPaint });\n}\n"],"mappings":";;;AAmBO,SAAS,uBACd,OACA,QACG;AAEH,MAAI,CAAC,OAAO,SAAS,CAAC,OAAO,SAAU,QAAO;AAE9C,QAAM,SAAkC,EAAE,GAAG,MAAM;AAGnD,aAAW,SAAS,CAAC,SAAS,UAAU,GAAY;AAClD,UAAM,aAAa,OAAO,KAAK;AAC/B,QAAI,CAAC,WAAY;AACjB,eAAW,CAAC,KAAK,UAAU,KAAK,OAAO,QAAQ,UAAU,GAAG;AAC1D,UAAI,eAAe,OAAW;AAC9B,YAAM,YAAY,OAAO,GAAG;AAC5B,aAAO,GAAG,IACR,cAAc,SACV,aACA,CAAC,QAAQ,CAAC,WAAW,CAAC,iBAAiB,KAAK,GAAG,KAAK,GAAG,YAAY,SAAS;AAAA,IACpF;AAAA,EACF;AAEA,SAAO;AACT;AAGO,SAAS,gBACd,OACA,YACG;AACH,SAAO,uBAAuB,OAAO,EAAE,OAAO,WAAW,CAAC;AAC5D;","names":[]}
@@ -0,0 +1,68 @@
1
+ "use client";
2
+ import {
3
+ PLAN_STATUSES,
4
+ PLAN_STATUS_ENCODING
5
+ } from "./chunk-E65Z3MSV.js";
6
+
7
+ // src/map-plan/map-plan-legend.tsx
8
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
10
+ function MapPlanLegend({
11
+ labels,
12
+ statuses = PLAN_STATUSES,
13
+ counts,
14
+ label,
15
+ className
16
+ }) {
17
+ const entries = statuses.filter((status) => labels[status]);
18
+ if (entries.length === 0) return null;
19
+ return /* @__PURE__ */ jsx(
20
+ "ul",
21
+ {
22
+ "data-slot": "map-plan-legend",
23
+ "aria-label": label,
24
+ className: cn("flex flex-wrap items-center gap-x-5 gap-y-2", className),
25
+ children: entries.map((status) => {
26
+ const encoding = PLAN_STATUS_ENCODING[status];
27
+ const Icon = encoding.icon;
28
+ const count = counts?.[status];
29
+ return /* @__PURE__ */ jsxs(
30
+ "li",
31
+ {
32
+ "data-slot": "map-plan-legend-item",
33
+ "data-status": status,
34
+ className: "flex items-center gap-2 text-caption text-foreground",
35
+ children: [
36
+ /* @__PURE__ */ jsx(
37
+ "span",
38
+ {
39
+ "data-slot": "map-plan-legend-swatch",
40
+ "aria-hidden": "true",
41
+ className: cn(
42
+ "inline-block size-3 rounded-xs",
43
+ encoding.markClass,
44
+ // The outline style, as the swatch's own border: the same
45
+ // second channel the shape on the map carries.
46
+ encoding.dash === "solid" && "border border-foreground/40",
47
+ encoding.dash === "dotted" && "border border-dotted border-foreground/70",
48
+ encoding.dash === "dashed" && "border border-dashed border-foreground/70",
49
+ encoding.dash === "dot-dash" && "border-2 border-dashed border-foreground/70"
50
+ )
51
+ }
52
+ ),
53
+ Icon && /* @__PURE__ */ jsx(Icon, { className: cn("size-3.5", encoding.textClass), "aria-hidden": "true" }),
54
+ /* @__PURE__ */ jsx("span", { children: labels[status] }),
55
+ count !== void 0 && /* @__PURE__ */ jsx("span", { className: "tabular-nums text-muted-foreground", children: count })
56
+ ]
57
+ },
58
+ status
59
+ );
60
+ })
61
+ }
62
+ );
63
+ }
64
+
65
+ export {
66
+ MapPlanLegend
67
+ };
68
+ //# sourceMappingURL=chunk-RV673TB4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/map-plan/map-plan-legend.tsx"],"sourcesContent":["\"use client\";\n\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nimport { PLAN_STATUSES, PLAN_STATUS_ENCODING, type PlanStatus } from \"../lib/plan-status\";\n\n/** Words for each status, supplied by the consumer so they can be localized. */\nexport type PlanStatusLabels = Partial<Record<PlanStatus, string>>;\n\nexport interface MapPlanLegendProps {\n /** The word for each status. A status with no word is left out of the legend. */\n labels: PlanStatusLabels;\n /** Only show these statuses, in this order. Defaults to all four, in table order. */\n statuses?: readonly PlanStatus[];\n /** Optional count per status, e.g. how many rooms are free right now. */\n counts?: Partial<Record<PlanStatus, number>>;\n /** Accessible name for the legend as a whole. */\n label?: string;\n className?: string;\n}\n\n/**\n * The key to the plan's status channels — and the place a sighted user learns\n * that the dashes and textures mean something.\n *\n * Each entry repeats the status three ways: the tone as a swatch, the outline\n * style as a rule under it, and the word. The two states that must never be\n * confused with a working one also carry their glyph. In greyscale the outline\n * styles and the words still tell them apart.\n */\nexport function MapPlanLegend({\n labels,\n statuses = PLAN_STATUSES,\n counts,\n label,\n className,\n}: MapPlanLegendProps) {\n const entries = statuses.filter((status) => labels[status]);\n if (entries.length === 0) return null;\n\n return (\n <ul\n data-slot=\"map-plan-legend\"\n aria-label={label}\n className={cn(\"flex flex-wrap items-center gap-x-5 gap-y-2\", className)}\n >\n {entries.map((status) => {\n const encoding = PLAN_STATUS_ENCODING[status];\n const Icon = encoding.icon;\n const count = counts?.[status];\n\n return (\n <li\n key={status}\n data-slot=\"map-plan-legend-item\"\n data-status={status}\n className=\"flex items-center gap-2 text-caption text-foreground\"\n >\n <span\n data-slot=\"map-plan-legend-swatch\"\n aria-hidden=\"true\"\n className={cn(\n \"inline-block size-3 rounded-xs\",\n encoding.markClass,\n // The outline style, as the swatch's own border: the same\n // second channel the shape on the map carries.\n encoding.dash === \"solid\" && \"border border-foreground/40\",\n encoding.dash === \"dotted\" && \"border border-dotted border-foreground/70\",\n encoding.dash === \"dashed\" && \"border border-dashed border-foreground/70\",\n encoding.dash === \"dot-dash\" && \"border-2 border-dashed border-foreground/70\",\n )}\n />\n {Icon && <Icon className={cn(\"size-3.5\", encoding.textClass)} aria-hidden=\"true\" />}\n <span>{labels[status]}</span>\n {count !== undefined && (\n <span className=\"tabular-nums text-muted-foreground\">{count}</span>\n )}\n </li>\n );\n })}\n </ul>\n );\n}\n"],"mappings":";;;;;;;AAEA,SAAS,UAAU;AAkDT,SAME,KANF;AAtBH,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,UAAU,SAAS,OAAO,CAAC,WAAW,OAAO,MAAM,CAAC;AAC1D,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,cAAY;AAAA,MACZ,WAAW,GAAG,+CAA+C,SAAS;AAAA,MAErE,kBAAQ,IAAI,CAAC,WAAW;AACvB,cAAM,WAAW,qBAAqB,MAAM;AAC5C,cAAM,OAAO,SAAS;AACtB,cAAM,QAAQ,SAAS,MAAM;AAE7B,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,aAAU;AAAA,YACV,eAAa;AAAA,YACb,WAAU;AAAA,YAEV;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,aAAU;AAAA,kBACV,eAAY;AAAA,kBACZ,WAAW;AAAA,oBACT;AAAA,oBACA,SAAS;AAAA;AAAA;AAAA,oBAGT,SAAS,SAAS,WAAW;AAAA,oBAC7B,SAAS,SAAS,YAAY;AAAA,oBAC9B,SAAS,SAAS,YAAY;AAAA,oBAC9B,SAAS,SAAS,cAAc;AAAA,kBAClC;AAAA;AAAA,cACF;AAAA,cACC,QAAQ,oBAAC,QAAK,WAAW,GAAG,YAAY,SAAS,SAAS,GAAG,eAAY,QAAO;AAAA,cACjF,oBAAC,UAAM,iBAAO,MAAM,GAAE;AAAA,cACrB,UAAU,UACT,oBAAC,UAAK,WAAU,sCAAsC,iBAAM;AAAA;AAAA;AAAA,UAtBzD;AAAA,QAwBP;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEJ;","names":[]}
@@ -0,0 +1,180 @@
1
+ "use client";
2
+ import {
3
+ useMapFrame
4
+ } from "./chunk-W76NLFER.js";
5
+ import {
6
+ useMap
7
+ } from "./chunk-JYFERXZT.js";
8
+
9
+ // src/map-controls/map-controls.tsx
10
+ import { useCallback, useEffect, useRef, useState } from "react";
11
+ import { Locate, Maximize, Minus, Plus, Scan } from "lucide-react";
12
+ import { Spinner, useLocale } from "@elabs-ai/components-ui";
13
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ var positionClasses = {
16
+ "top-left": "top-2 left-2",
17
+ "top-right": "top-2 right-2",
18
+ "bottom-left": "bottom-2 left-2",
19
+ "bottom-right": "bottom-10 right-2"
20
+ };
21
+ function ControlGroup({ children }) {
22
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-col divide-y overflow-hidden rounded-md bg-surface-elevated shadow-ring-sm", children });
23
+ }
24
+ function ControlButton({
25
+ onClick,
26
+ label,
27
+ children,
28
+ disabled = false
29
+ }) {
30
+ return /* @__PURE__ */ jsx(
31
+ "button",
32
+ {
33
+ onClick,
34
+ "aria-label": label,
35
+ type: "button",
36
+ className: cn(
37
+ "flex size-8 items-center justify-center text-foreground transition-colors duration-fast",
38
+ "hover:bg-surface-muted",
39
+ "focus-ring-inset",
40
+ "disabled:pointer-events-none disabled:opacity-50"
41
+ ),
42
+ disabled,
43
+ children
44
+ }
45
+ );
46
+ }
47
+ function MapControls({
48
+ position = "bottom-right",
49
+ showZoom,
50
+ showCompass = false,
51
+ showLocate = false,
52
+ showFullscreen = false,
53
+ showFit,
54
+ fitBounds,
55
+ className,
56
+ onLocate,
57
+ onLocateError,
58
+ onFit
59
+ }) {
60
+ const { map, plan } = useMap();
61
+ const { t } = useLocale();
62
+ const { interactive } = useMapFrame();
63
+ const zoomVisible = showZoom ?? interactive;
64
+ const [waitingForLocation, setWaitingForLocation] = useState(false);
65
+ const fitVisible = showFit ?? plan != null;
66
+ const handleZoomIn = useCallback(() => {
67
+ map?.zoomTo(map.getZoom() + 1, { duration: 300 });
68
+ }, [map]);
69
+ const handleZoomOut = useCallback(() => {
70
+ map?.zoomTo(map.getZoom() - 1, { duration: 300 });
71
+ }, [map]);
72
+ const handleResetBearing = useCallback(() => {
73
+ map?.resetNorthPitch({ duration: 300 });
74
+ }, [map]);
75
+ const handleLocate = useCallback(() => {
76
+ if (!("geolocation" in navigator)) return;
77
+ setWaitingForLocation(true);
78
+ navigator.geolocation.getCurrentPosition(
79
+ (pos) => {
80
+ const coords = {
81
+ longitude: pos.coords.longitude,
82
+ latitude: pos.coords.latitude
83
+ };
84
+ map?.flyTo({
85
+ center: [coords.longitude, coords.latitude],
86
+ zoom: 14,
87
+ duration: 1500
88
+ });
89
+ onLocate?.(coords);
90
+ setWaitingForLocation(false);
91
+ },
92
+ (error) => {
93
+ onLocateError?.(error);
94
+ setWaitingForLocation(false);
95
+ }
96
+ );
97
+ }, [map, onLocate, onLocateError]);
98
+ const handleFit = useCallback(() => {
99
+ const bounds = fitBounds ?? plan?.bounds;
100
+ if (!map || !bounds) return;
101
+ map.fitBounds(bounds, { padding: 24, duration: 300 });
102
+ onFit?.();
103
+ }, [fitBounds, map, onFit, plan]);
104
+ const handleFullscreen = useCallback(() => {
105
+ const container = map?.getContainer();
106
+ if (!container) return;
107
+ if (document.fullscreenElement) {
108
+ void document.exitFullscreen();
109
+ } else {
110
+ void container.requestFullscreen();
111
+ }
112
+ }, [map]);
113
+ if (!zoomVisible && !fitVisible && !showCompass && !showLocate && !showFullscreen) return null;
114
+ return /* @__PURE__ */ jsxs(
115
+ "div",
116
+ {
117
+ className: cn("absolute z-10 flex flex-col gap-1.5", positionClasses[position], className),
118
+ children: [
119
+ zoomVisible && /* @__PURE__ */ jsxs(ControlGroup, { children: [
120
+ /* @__PURE__ */ jsx(ControlButton, { onClick: handleZoomIn, label: t("maps.controls.zoomIn"), children: /* @__PURE__ */ jsx(Plus, { className: "size-4", "aria-hidden": "true" }) }),
121
+ /* @__PURE__ */ jsx(ControlButton, { onClick: handleZoomOut, label: t("maps.controls.zoomOut"), children: /* @__PURE__ */ jsx(Minus, { className: "size-4", "aria-hidden": "true" }) })
122
+ ] }),
123
+ showCompass && /* @__PURE__ */ jsx(ControlGroup, { children: /* @__PURE__ */ jsx(CompassButton, { onClick: handleResetBearing }) }),
124
+ showLocate && /* @__PURE__ */ jsx(ControlGroup, { children: /* @__PURE__ */ jsx(
125
+ ControlButton,
126
+ {
127
+ onClick: handleLocate,
128
+ label: t("maps.controls.locate"),
129
+ disabled: waitingForLocation,
130
+ children: waitingForLocation ? /* @__PURE__ */ jsx(Spinner, { label: t("maps.controls.locating"), className: "size-4 text-foreground" }) : /* @__PURE__ */ jsx(Locate, { className: "size-4", "aria-hidden": "true" })
131
+ }
132
+ ) }),
133
+ fitVisible && /* @__PURE__ */ jsx(ControlGroup, { children: /* @__PURE__ */ jsx(ControlButton, { onClick: handleFit, label: t("maps.controls.fit"), children: /* @__PURE__ */ jsx(Scan, { className: "size-4", "aria-hidden": "true" }) }) }),
134
+ showFullscreen && /* @__PURE__ */ jsx(ControlGroup, { children: /* @__PURE__ */ jsx(ControlButton, { onClick: handleFullscreen, label: t("maps.controls.fullscreen"), children: /* @__PURE__ */ jsx(Maximize, { className: "size-4", "aria-hidden": "true" }) }) })
135
+ ]
136
+ }
137
+ );
138
+ }
139
+ function CompassButton({ onClick }) {
140
+ const { map } = useMap();
141
+ const { t } = useLocale();
142
+ const compassRef = useRef(null);
143
+ useEffect(() => {
144
+ if (!map || !compassRef.current) return;
145
+ const compass = compassRef.current;
146
+ const updateRotation = () => {
147
+ const bearing = map.getBearing();
148
+ const pitch = map.getPitch();
149
+ compass.style.transform = `rotateX(${pitch}deg) rotateZ(${-bearing}deg)`;
150
+ };
151
+ map.on("rotate", updateRotation);
152
+ map.on("pitch", updateRotation);
153
+ updateRotation();
154
+ return () => {
155
+ map.off("rotate", updateRotation);
156
+ map.off("pitch", updateRotation);
157
+ };
158
+ }, [map]);
159
+ return /* @__PURE__ */ jsx(ControlButton, { onClick, label: t("maps.controls.resetBearing"), children: /* @__PURE__ */ jsxs(
160
+ "svg",
161
+ {
162
+ ref: compassRef,
163
+ viewBox: "0 0 24 24",
164
+ "aria-hidden": "true",
165
+ className: "size-5 transition-transform duration-fast",
166
+ style: { transformStyle: "preserve-3d" },
167
+ children: [
168
+ /* @__PURE__ */ jsx("path", { d: "M12 2L16 12H12V2Z", className: "fill-destructive" }),
169
+ /* @__PURE__ */ jsx("path", { d: "M12 2L8 12H12V2Z", className: "fill-destructive/50" }),
170
+ /* @__PURE__ */ jsx("path", { d: "M12 22L16 12H12V22Z", className: "fill-muted-foreground/60" }),
171
+ /* @__PURE__ */ jsx("path", { d: "M12 22L8 12H12V22Z", className: "fill-muted-foreground/30" })
172
+ ]
173
+ }
174
+ ) });
175
+ }
176
+
177
+ export {
178
+ MapControls
179
+ };
180
+ //# sourceMappingURL=chunk-S7N2WMVT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/map-controls/map-controls.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { Locate, Maximize, Minus, Plus, Scan } from \"lucide-react\";\nimport { Spinner, useLocale } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nimport { useMapFrame } from \"../lib/use-map-breakpoint\";\nimport { useMap } from \"../map-canvas/map-context\";\n\nexport interface MapControlsProps {\n /** Position of the controls on the map (default: \"bottom-right\"). */\n position?: \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\";\n /**\n * Show zoom in/out buttons. Default: true on an interactive map, false on a\n * static one (`<MapCanvas interactive={false}>`), whose whole point is a\n * fixed view — set `showZoom` explicitly to override either way.\n */\n showZoom?: boolean;\n /** Show a compass button to reset bearing/pitch (default: false). */\n showCompass?: boolean;\n /** Show a locate button to fly to the user's location (default: false). */\n showLocate?: boolean;\n /** Show a fullscreen toggle button (default: false). */\n showFullscreen?: boolean;\n /**\n * Show a button that frames the whole plan again — a plan map has an edge to\n * come back to, so this defaults to ON there and off on a geographic map.\n */\n showFit?: boolean;\n /**\n * What the fit button frames. Defaults to the plan's own extent; pass bounds\n * to frame something else (one wing, the selected cluster).\n */\n fitBounds?: [[number, number], [number, number]];\n /** Called after the camera has been asked to frame the plan. */\n onFit?: () => void;\n /** Additional CSS classes for the controls container. */\n className?: string;\n /** Callback with user coordinates when located. */\n onLocate?: (coords: { longitude: number; latitude: number }) => void;\n /** Callback when geolocation fails or is denied. */\n onLocateError?: (error: GeolocationPositionError) => void;\n}\n\n// bottom-right sits above MapLibre's attribution control — keep it visible.\nconst positionClasses = {\n \"top-left\": \"top-2 left-2\",\n \"top-right\": \"top-2 right-2\",\n \"bottom-left\": \"bottom-2 left-2\",\n \"bottom-right\": \"bottom-10 right-2\",\n};\n\nfunction ControlGroup({ children }: { children: ReactNode }) {\n return (\n <div className=\"flex flex-col divide-y overflow-hidden rounded-md bg-surface-elevated shadow-ring-sm\">\n {children}\n </div>\n );\n}\n\nfunction ControlButton({\n onClick,\n label,\n children,\n disabled = false,\n}: {\n onClick: () => void;\n label: string;\n children: ReactNode;\n disabled?: boolean;\n}) {\n return (\n <button\n onClick={onClick}\n aria-label={label}\n type=\"button\"\n className={cn(\n \"flex size-8 items-center justify-center text-foreground transition-colors duration-fast\",\n \"hover:bg-surface-muted\",\n \"focus-ring-inset\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n )}\n disabled={disabled}\n >\n {children}\n </button>\n );\n}\n\n/** Branded zoom / compass / locate / fullscreen controls. Render inside `<MapCanvas>`. */\nexport function MapControls({\n position = \"bottom-right\",\n showZoom,\n showCompass = false,\n showLocate = false,\n showFullscreen = false,\n showFit,\n fitBounds,\n className,\n onLocate,\n onLocateError,\n onFit,\n}: MapControlsProps) {\n const { map, plan } = useMap();\n const { t } = useLocale();\n const { interactive } = useMapFrame();\n // A static map (`interactive={false}`) is a fixed editorial view: zoom chrome\n // invites a gesture the map will not answer, and a click leaves the reader on\n // a frame they cannot pan back from. The host may still ask for it.\n const zoomVisible = showZoom ?? interactive;\n const [waitingForLocation, setWaitingForLocation] = useState(false);\n const fitVisible = showFit ?? plan != null;\n\n const handleZoomIn = useCallback(() => {\n map?.zoomTo(map.getZoom() + 1, { duration: 300 });\n }, [map]);\n\n const handleZoomOut = useCallback(() => {\n map?.zoomTo(map.getZoom() - 1, { duration: 300 });\n }, [map]);\n\n const handleResetBearing = useCallback(() => {\n map?.resetNorthPitch({ duration: 300 });\n }, [map]);\n\n const handleLocate = useCallback(() => {\n if (!(\"geolocation\" in navigator)) return;\n setWaitingForLocation(true);\n navigator.geolocation.getCurrentPosition(\n (pos) => {\n const coords = {\n longitude: pos.coords.longitude,\n latitude: pos.coords.latitude,\n };\n map?.flyTo({\n center: [coords.longitude, coords.latitude],\n zoom: 14,\n duration: 1500,\n });\n onLocate?.(coords);\n setWaitingForLocation(false);\n },\n (error) => {\n onLocateError?.(error);\n setWaitingForLocation(false);\n },\n );\n }, [map, onLocate, onLocateError]);\n\n const handleFit = useCallback(() => {\n const bounds = fitBounds ?? plan?.bounds;\n if (!map || !bounds) return;\n map.fitBounds(bounds, { padding: 24, duration: 300 });\n onFit?.();\n }, [fitBounds, map, onFit, plan]);\n\n const handleFullscreen = useCallback(() => {\n const container = map?.getContainer();\n if (!container) return;\n if (document.fullscreenElement) {\n void document.exitFullscreen();\n } else {\n void container.requestFullscreen();\n }\n }, [map]);\n\n // Nothing to show (a static map with no explicit group) renders no box at all.\n if (!zoomVisible && !fitVisible && !showCompass && !showLocate && !showFullscreen) return null;\n\n return (\n <div\n className={cn(\"absolute z-10 flex flex-col gap-1.5\", positionClasses[position], className)}\n >\n {zoomVisible && (\n <ControlGroup>\n <ControlButton onClick={handleZoomIn} label={t(\"maps.controls.zoomIn\")}>\n <Plus className=\"size-4\" aria-hidden=\"true\" />\n </ControlButton>\n <ControlButton onClick={handleZoomOut} label={t(\"maps.controls.zoomOut\")}>\n <Minus className=\"size-4\" aria-hidden=\"true\" />\n </ControlButton>\n </ControlGroup>\n )}\n {showCompass && (\n <ControlGroup>\n <CompassButton onClick={handleResetBearing} />\n </ControlGroup>\n )}\n {showLocate && (\n <ControlGroup>\n <ControlButton\n onClick={handleLocate}\n label={t(\"maps.controls.locate\")}\n disabled={waitingForLocation}\n >\n {waitingForLocation ? (\n <Spinner label={t(\"maps.controls.locating\")} className=\"size-4 text-foreground\" />\n ) : (\n <Locate className=\"size-4\" aria-hidden=\"true\" />\n )}\n </ControlButton>\n </ControlGroup>\n )}\n {fitVisible && (\n <ControlGroup>\n <ControlButton onClick={handleFit} label={t(\"maps.controls.fit\")}>\n <Scan className=\"size-4\" aria-hidden=\"true\" />\n </ControlButton>\n </ControlGroup>\n )}\n {showFullscreen && (\n <ControlGroup>\n <ControlButton onClick={handleFullscreen} label={t(\"maps.controls.fullscreen\")}>\n <Maximize className=\"size-4\" aria-hidden=\"true\" />\n </ControlButton>\n </ControlGroup>\n )}\n </div>\n );\n}\n\nfunction CompassButton({ onClick }: { onClick: () => void }) {\n const { map } = useMap();\n const { t } = useLocale();\n const compassRef = useRef<SVGSVGElement>(null);\n\n useEffect(() => {\n if (!map || !compassRef.current) return;\n\n const compass = compassRef.current;\n\n const updateRotation = () => {\n const bearing = map.getBearing();\n const pitch = map.getPitch();\n compass.style.transform = `rotateX(${pitch}deg) rotateZ(${-bearing}deg)`;\n };\n\n map.on(\"rotate\", updateRotation);\n map.on(\"pitch\", updateRotation);\n updateRotation();\n\n return () => {\n map.off(\"rotate\", updateRotation);\n map.off(\"pitch\", updateRotation);\n };\n }, [map]);\n\n return (\n <ControlButton onClick={onClick} label={t(\"maps.controls.resetBearing\")}>\n <svg\n ref={compassRef}\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n className=\"size-5 transition-transform duration-fast\"\n style={{ transformStyle: \"preserve-3d\" }}\n >\n <path d=\"M12 2L16 12H12V2Z\" className=\"fill-destructive\" />\n <path d=\"M12 2L8 12H12V2Z\" className=\"fill-destructive/50\" />\n <path d=\"M12 22L16 12H12V22Z\" className=\"fill-muted-foreground/60\" />\n <path d=\"M12 22L8 12H12V22Z\" className=\"fill-muted-foreground/30\" />\n </svg>\n </ControlButton>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ //# sourceMappingURL=chunk-T3Z6TCDD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}