@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
@@ -5,6 +5,7 @@ import { useEffect, useId, useMemo, useRef } from "react";
5
5
 
6
6
  import { useMap } from "../map-canvas/map-context";
7
7
  import { buildArcCoordinates } from "../lib/arc-math";
8
+ import type { PlanPoint } from "../lib/plan-crs";
8
9
  import { mergeHoverPaint } from "../lib/merge-hover-paint";
9
10
  import { useTokenColor } from "../lib/use-token-color";
10
11
 
@@ -12,9 +13,12 @@ import { useTokenColor } from "../lib/use-token-color";
12
13
  export type MapArcDatum = {
13
14
  /** Unique identifier for this arc. Required for hover state tracking and event payloads. */
14
15
  id: string | number;
15
- /** Start coordinate as [longitude, latitude]. */
16
+ /**
17
+ * Start coordinate as [longitude, latitude] — or, inside a `<MapCanvas plan>`,
18
+ * as [x, y] in plan units.
19
+ */
16
20
  from: [number, number];
17
- /** End coordinate as [longitude, latitude]. */
21
+ /** End coordinate, in the same units as `from`. */
18
22
  to: [number, number];
19
23
  };
20
24
 
@@ -26,6 +30,8 @@ export type MapArcEvent<T extends MapArcDatum = MapArcDatum> = {
26
30
  longitude: number;
27
31
  /** Latitude of the cursor at the time of the event. */
28
32
  latitude: number;
33
+ /** The cursor in plan units, on a plan map; `null` on a geographic one. */
34
+ plan: PlanPoint | null;
29
35
  /** The underlying MapLibre mouse event for advanced use cases. */
30
36
  originalEvent: MapLibreGL.MapMouseEvent;
31
37
  };
@@ -89,6 +95,11 @@ const DEFAULT_ARC_LAYOUT: MapArcLineLayout = {
89
95
  /**
90
96
  * Curved great-circle-style arcs between coordinate pairs (flight paths,
91
97
  * network links). Pairs well with `<MapCanvas blank projection={{ type: "globe" }}>`.
98
+ *
99
+ * Inside a `<MapCanvas plan>` the same component draws plan-space curves — a
100
+ * material flow between two machine cells, a walking route across a floor. The
101
+ * curve is built in PLAN units and each sample is then converted, because a
102
+ * straight lng/lat line bows once Mercator has had its say.
92
103
  */
93
104
  export function MapArc<T extends MapArcDatum = MapArcDatum>({
94
105
  data,
@@ -103,7 +114,7 @@ export function MapArc<T extends MapArcDatum = MapArcDatum>({
103
114
  interactive = true,
104
115
  beforeId,
105
116
  }: MapArcProps<T>) {
106
- const { map, isLoaded } = useMap();
117
+ const { map, isLoaded, plan } = useMap();
107
118
  const autoId = useId();
108
119
  const id = propId ?? autoId;
109
120
  const sourceId = `arc-source-${id}`;
@@ -133,21 +144,29 @@ export function MapArc<T extends MapArcDatum = MapArcDatum>({
133
144
  type: "FeatureCollection",
134
145
  features: data.map((arc) => {
135
146
  const { from, to, ...properties } = arc;
147
+ // In plan mode the curve is sampled in plan units with the antimeridian
148
+ // unwrap OFF — x is a position on a floor, not a longitude — and each
149
+ // sample is converted afterwards.
150
+ const sampled = buildArcCoordinates(from, to, curvature, samples, plan == null);
136
151
  return {
137
152
  type: "Feature",
138
153
  properties,
139
154
  geometry: {
140
155
  type: "LineString",
141
- coordinates: buildArcCoordinates(from, to, curvature, samples),
156
+ coordinates: plan ? sampled.map((point) => plan.toLngLat(point)) : sampled,
142
157
  },
143
158
  };
144
159
  }),
145
160
  }),
146
- [data, curvature, samples],
161
+ [data, curvature, samples, plan],
147
162
  );
148
163
 
149
164
  const latestRef = useRef({ data, onClick, onHover });
150
165
  latestRef.current = { data, onClick, onHover };
166
+ // The interaction effect is bound once per map; the plan is read through a ref
167
+ // so a plan arriving later does not rebind every listener.
168
+ const planRef = useRef(plan);
169
+ planRef.current = plan;
151
170
 
152
171
  // Add source and layers on mount. The invisible hit layer widens the
153
172
  // pointer target so thin arcs stay hoverable/clickable.
@@ -255,6 +274,7 @@ export function MapArc<T extends MapArcDatum = MapArcDatum>({
255
274
  arc: arc as T,
256
275
  longitude: e.lngLat.lng,
257
276
  latitude: e.lngLat.lat,
277
+ plan: planRef.current ? planRef.current.toPlan([e.lngLat.lng, e.lngLat.lat]) : null,
258
278
  originalEvent: e,
259
279
  });
260
280
  }
@@ -273,6 +293,7 @@ export function MapArc<T extends MapArcDatum = MapArcDatum>({
273
293
  arc: arc as T,
274
294
  longitude: e.lngLat.lng,
275
295
  latitude: e.lngLat.lat,
296
+ plan: planRef.current ? planRef.current.toPlan([e.lngLat.lng, e.lngLat.lat]) : null,
276
297
  originalEvent: e,
277
298
  });
278
299
  };
@@ -1,7 +1,21 @@
1
+ import { useEffect, useState } from "react";
1
2
  import type { Meta, StoryObj } from "@storybook/react-vite";
3
+ import { expect, waitFor } from "storybook/test";
4
+ import { resolveTokenColor } from "@elabs-ai/components-tokens";
2
5
 
3
6
  import { MapCanvas } from "./map-canvas";
4
7
  import { MapControls } from "../map-controls";
8
+ import { MapGeoJSON } from "../map-geojson";
9
+ import { MapInset } from "../map-inset";
10
+ import { MapLegend } from "../map-legend";
11
+ import { MapMarker, MapMarkerContent, MapMarkerTooltip } from "../map-marker";
12
+ import { MapScaleBar } from "../map-scale-bar";
13
+ import {
14
+ GREATER_TORONTO,
15
+ LAKE_ONTARIO,
16
+ LOCATOR_BOUNDS,
17
+ LOCATOR_PLACES,
18
+ } from "../test-utils/locator-fixture";
5
19
 
6
20
  const meta = {
7
21
  title: "Maps/MapCanvas",
@@ -52,3 +66,200 @@ export const Globe: Story = {
52
66
  </div>
53
67
  ),
54
68
  };
69
+
70
+ /**
71
+ * Consumer-side paint needs concrete colours (WebGL cannot read CSS
72
+ * variables): resolve a token and re-resolve when the theme flips.
73
+ */
74
+ function useThemedTokenColor(name: string): string {
75
+ const [color, setColor] = useState("#000000");
76
+ useEffect(() => {
77
+ const update = () => setColor(resolveTokenColor(name));
78
+ update();
79
+ const observer = new MutationObserver(update);
80
+ observer.observe(document.documentElement, {
81
+ attributes: true,
82
+ attributeFilter: ["data-theme", "class"],
83
+ });
84
+ return () => observer.disconnect();
85
+ }, [name]);
86
+ return color;
87
+ }
88
+
89
+ function LakeOntarioLocator() {
90
+ const lake = useThemedTokenColor("--chart-1");
91
+ const region = useThemedTokenColor("--chart-2");
92
+ return (
93
+ <div className="w-full p-4">
94
+ <MapCanvas
95
+ interactive={false}
96
+ // c-11: the basemap printed "Toronto" 16 px from the marker that
97
+ // names it, with the marker dot over the basemap word's last letters.
98
+ // A locator's editorial labels are the only labels on it.
99
+ basemapLabels={false}
100
+ bounds={LOCATOR_BOUNDS}
101
+ // Room at the top for the inset, at the bottom for the scale bar.
102
+ fitBoundsOptions={{ padding: { top: 152, right: 24, bottom: 56, left: 24 } }}
103
+ >
104
+ <MapGeoJSON
105
+ id="gta"
106
+ data={GREATER_TORONTO}
107
+ fillPaint={{ "fill-color": region }}
108
+ fillOpacity={0.3}
109
+ vignette={{ width: 10, opacity: 0.4 }}
110
+ linePaint={false}
111
+ />
112
+ <MapGeoJSON
113
+ id="lake"
114
+ data={LAKE_ONTARIO}
115
+ fillPaint={{ "fill-color": lake }}
116
+ pattern={{ kind: "stripes", width: 2, gap: 4 }}
117
+ vignette={{ width: 14, opacity: 0.35 }}
118
+ linePaint={{ "line-color": lake, "line-width": 1 }}
119
+ />
120
+ <MapMarker {...LOCATOR_PLACES.lakeLabel} label={{ text: "Lake Ontario", box: true }} />
121
+ <MapMarker {...LOCATOR_PLACES.gtaLabel} label={{ text: "Greater Toronto Area" }} />
122
+ <MapMarker {...LOCATOR_PLACES.toronto} label={{ text: "Toronto", position: "bottom-left" }}>
123
+ <MapMarkerContent />
124
+ <MapMarkerTooltip>Toronto — Ontario’s capital</MapMarkerTooltip>
125
+ </MapMarker>
126
+ <MapMarker
127
+ {...LOCATOR_PLACES.rochester}
128
+ showAt={{ base: true, narrow: false }}
129
+ label={{ text: "Rochester, NY", position: "right" }}
130
+ >
131
+ <MapMarkerContent />
132
+ </MapMarker>
133
+ <MapInset kind="globe" position="top-right" />
134
+ <MapScaleBar unit="km" position="bottom-left" />
135
+ <MapLegend
136
+ position={{ base: "top-left", narrow: "below" }}
137
+ items={[
138
+ { label: "Lake Ontario", color: "var(--chart-1)", shape: "square" },
139
+ { label: "Greater Toronto Area", color: "var(--chart-2)", shape: "square" },
140
+ ]}
141
+ />
142
+ </MapCanvas>
143
+ </div>
144
+ );
145
+ }
146
+
147
+ /**
148
+ * An editorial locator: static (no zoom or pan; tooltips still open),
149
+ * two area markers with a stripe pattern and a vignette glow, inline labels,
150
+ * a globe inset, a scale bar and a key. The map sets no height of its own:
151
+ * it takes 1.6 : 1, and turns square at `narrow` so a phone still gets a map
152
+ * worth reading; there the key moves under the map and Rochester's label
153
+ * (`showAt: { narrow: false }`) steps aside.
154
+ */
155
+ export const StaticLocator: Story = {
156
+ parameters: { layout: "fullscreen" },
157
+ render: () => <LakeOntarioLocator />,
158
+ play: async ({ canvasElement }) => {
159
+ // Without WebGL the canvas degrades to an error panel and no furniture mounts.
160
+ const map = await waitFor(
161
+ () => {
162
+ const el = canvasElement.querySelector<HTMLElement>('[data-slot="map-canvas"]');
163
+ expect(el ?? canvasElement.querySelector('[role="alert"]')).not.toBeNull();
164
+ return el;
165
+ },
166
+ { timeout: 5000 },
167
+ );
168
+ if (!map) return;
169
+ await expect(map).toHaveAttribute("data-interactive", "false");
170
+ const tier = map.getAttribute("data-map-breakpoint");
171
+ await waitFor(() =>
172
+ expect(canvasElement.querySelector('[data-slot="map-scale-bar"]')).not.toBeNull(),
173
+ );
174
+ const legend = canvasElement.querySelector('[data-slot="map-legend"]');
175
+ if (tier === "narrow") {
176
+ // Square at narrow; the key sits under the map. The tier is measured
177
+ // after mount, so wait for the box rather than reading it once.
178
+ await waitFor(() => {
179
+ const box = map.getBoundingClientRect();
180
+ expect(box.height).toBeGreaterThanOrEqual(box.width - 1);
181
+ });
182
+ await expect(canvasElement.querySelector('[data-slot="map-canvas-below"]')).not.toBeNull();
183
+ await expect(map.contains(legend)).toBe(false);
184
+ } else {
185
+ await expect(map.contains(legend)).toBe(true);
186
+ }
187
+ },
188
+ };
189
+
190
+ function ViewportReadout() {
191
+ const [viewport, setViewport] = useState("not moved");
192
+ return (
193
+ <div className="flex w-full flex-col gap-2 p-4">
194
+ <div className="h-[360px]">
195
+ <MapCanvas
196
+ interactive={false}
197
+ center={[-79.38, 43.65]}
198
+ zoom={9}
199
+ onViewportChange={(v) =>
200
+ setViewport(
201
+ `${v.center[0].toFixed(3)}, ${v.center[1].toFixed(3)} @ ${v.zoom.toFixed(2)}`,
202
+ )
203
+ }
204
+ >
205
+ <MapMarker {...LOCATOR_PLACES.toronto}>
206
+ <MapMarkerContent />
207
+ <MapMarkerTooltip>Toronto</MapMarkerTooltip>
208
+ </MapMarker>
209
+ </MapCanvas>
210
+ </div>
211
+ <p className="text-caption text-muted-foreground">
212
+ Viewport: <output data-testid="viewport">{viewport}</output>
213
+ </p>
214
+ </div>
215
+ );
216
+ }
217
+
218
+ /**
219
+ * `interactive={false}`: wheel, drag, double-click and keys leave the viewport
220
+ * alone and the cursor stays an arrow, but hovering the marker still opens
221
+ * its tooltip. The readout only changes when the viewport does.
222
+ */
223
+ export const StaticMode: Story = {
224
+ parameters: { layout: "fullscreen" },
225
+ render: () => <ViewportReadout />,
226
+ play: async ({ canvasElement }) => {
227
+ const canvas = await waitFor(
228
+ () => {
229
+ const el = canvasElement.querySelector<HTMLCanvasElement>(".maplibregl-canvas");
230
+ const fallback = canvasElement.querySelector('[role="alert"]');
231
+ expect(el ?? fallback).not.toBeNull();
232
+ return el;
233
+ },
234
+ { timeout: 5000 },
235
+ );
236
+ if (!canvas) return;
237
+ const readout = canvasElement.querySelector('[data-testid="viewport"]')!;
238
+ const before = readout.textContent;
239
+ canvas.dispatchEvent(
240
+ new WheelEvent("wheel", {
241
+ deltaY: -600,
242
+ bubbles: true,
243
+ cancelable: true,
244
+ clientX: 100,
245
+ clientY: 100,
246
+ }),
247
+ );
248
+ await new Promise((resolve) => setTimeout(resolve, 400));
249
+ await expect(readout.textContent).toBe(before);
250
+ await expect(canvas.tabIndex).toBe(-1);
251
+ },
252
+ };
253
+
254
+ /**
255
+ * A map in a parent with no height of its own: `height` resolves per tier —
256
+ * here 320 px, and a square at `narrow`.
257
+ */
258
+ export const ResponsiveHeight: Story = {
259
+ parameters: { layout: "fullscreen" },
260
+ render: () => (
261
+ <div className="w-full p-4">
262
+ <MapCanvas center={[-79.38, 43.65]} zoom={8} height={{ base: 320, narrow: { aspect: 1 } }} />
263
+ </div>
264
+ ),
265
+ };