@elabs-ai/components-maps 4.2.0 → 5.1.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
@@ -9,6 +9,7 @@ vi.mock("maplibre-gl", async () => {
9
9
  });
10
10
 
11
11
  import { MockMap, resetMaplibreMock } from "../test-utils/maplibre-mock";
12
+ import { createPlanCrs } from "../lib/plan-crs";
12
13
  import { MapCanvas } from "./map-canvas";
13
14
  import { useMap } from "./map-context";
14
15
 
@@ -121,4 +122,392 @@ describe("MapCanvas", () => {
121
122
  expect(setProjectionSpy).toHaveBeenCalledWith(mercator);
122
123
  expect(setProjectionSpy).not.toHaveBeenCalledWith(globe);
123
124
  });
125
+
126
+ describe("static mode (interactive={false})", () => {
127
+ it("switches every gesture handler off but keeps MapLibre's own listeners", () => {
128
+ render(<MapCanvas interactive={false} />);
129
+ const options = MockMap.instances[0]!.options;
130
+ for (const key of [
131
+ "scrollZoom",
132
+ "boxZoom",
133
+ "dragRotate",
134
+ "dragPan",
135
+ "keyboard",
136
+ "doubleClickZoom",
137
+ "touchZoomRotate",
138
+ "touchPitch",
139
+ ]) {
140
+ expect(options[key]).toBe(false);
141
+ }
142
+ // MapLibre's `interactive: false` would detach hover / click too.
143
+ expect(options.interactive).toBeUndefined();
144
+ });
145
+
146
+ it("drops the grab cursor and the tab stop, and marks the container", async () => {
147
+ const { container } = render(<MapCanvas interactive={false} />);
148
+ const map = MockMap.instances[0]!;
149
+ await waitFor(() => {
150
+ expect(map.getCanvas().tabIndex).toBe(-1);
151
+ });
152
+ expect(map.canvasContainer).not.toHaveClass("maplibregl-interactive");
153
+ expect(container.querySelector('[data-slot="map-canvas"]')).toHaveAttribute(
154
+ "data-interactive",
155
+ "false",
156
+ );
157
+ });
158
+
159
+ it("turns the handlers back on when the map becomes interactive again", async () => {
160
+ const { rerender } = render(<MapCanvas interactive={false} />);
161
+ const map = MockMap.instances[0]!;
162
+ await waitFor(() => expect(map.dragPan.enabled).toBe(false));
163
+ rerender(<MapCanvas interactive scrollZoom={false} />);
164
+ await waitFor(() => expect(map.dragPan.enabled).toBe(true));
165
+ // An explicit `scrollZoom={false}` stays off.
166
+ expect(map.scrollZoom.enabled).toBe(false);
167
+ expect(map.getCanvas().tabIndex).toBe(0);
168
+ });
169
+
170
+ it("leaves an interactive map's handlers alone (no default change)", async () => {
171
+ render(<MapCanvas />);
172
+ const map = MockMap.instances[0]!;
173
+ const disable = vi.spyOn(map.dragPan, "disable");
174
+ const enable = vi.spyOn(map.dragPan, "enable");
175
+ await act(async () => {
176
+ await new Promise((resolve) => setTimeout(resolve, 0));
177
+ });
178
+ expect(disable).not.toHaveBeenCalled();
179
+ expect(enable).not.toHaveBeenCalled();
180
+ expect(map.options.dragPan).toBeUndefined();
181
+ });
182
+ });
183
+
184
+ it('accepts the "globe" shorthand and applies it as a projection spec', async () => {
185
+ render(<MapCanvas projection="globe" />);
186
+ const map = MockMap.instances[0]!;
187
+ const setProjection = vi.spyOn(map, "setProjection");
188
+ await act(async () => {
189
+ await new Promise((resolve) => setTimeout(resolve, 150));
190
+ });
191
+ expect(setProjection).toHaveBeenCalledWith({ type: "globe" });
192
+ });
193
+
194
+ it("ignores a projection when the MapLibre build cannot switch one", async () => {
195
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
196
+ render(<MapCanvas projection="globe" />);
197
+ const map = MockMap.instances[0]!;
198
+ (map as unknown as { setProjection: unknown }).setProjection = undefined;
199
+ await act(async () => {
200
+ map.emit("styledata");
201
+ await new Promise((resolve) => setTimeout(resolve, 150));
202
+ });
203
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("cannot switch projections"));
204
+ warn.mockRestore();
205
+ });
206
+
207
+ describe("height and tiers", () => {
208
+ function atWidth(width: number) {
209
+ return vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockReturnValue({
210
+ width,
211
+ height: 0,
212
+ top: 0,
213
+ left: 0,
214
+ right: width,
215
+ bottom: 0,
216
+ x: 0,
217
+ y: 0,
218
+ toJSON: () => ({}),
219
+ });
220
+ }
221
+
222
+ it("publishes its tier and gives an unsized map the default aspect", () => {
223
+ const spy = atWidth(348);
224
+ const { container } = render(<MapCanvas />);
225
+ const root = container.querySelector<HTMLElement>('[data-slot="map-canvas"]')!;
226
+ expect(root).toHaveAttribute("data-map-breakpoint", "narrow");
227
+ expect(root.style.aspectRatio).toBe("1 / 1");
228
+ // `h-full` still sizes a map whose parent has a height of its own.
229
+ expect(root).toHaveClass("h-full");
230
+ spy.mockRestore();
231
+ });
232
+
233
+ it("sizes the box for its tier BEFORE MapLibre measures it", () => {
234
+ const spy = atWidth(348);
235
+ render(<MapCanvas />);
236
+ const map = MockMap.instances[0]!;
237
+ // The narrow square was already on the box when the map was built, so
238
+ // the canvas and any `bounds` fit use the final size.
239
+ expect(map.aspectAtConstruction).toBe("1 / 1");
240
+ spy.mockRestore();
241
+ });
242
+
243
+ // A height / aspect change is layout the ENGINE measures. `transition-property`
244
+ // defaults to `all` and the house reduced-motion clamp forces a 0.01 ms
245
+ // duration on every element, so without this the new rule only lands on the
246
+ // next animation frame — ~0.7 s away under a busy WebGL first paint, long
247
+ // after MapLibre measured the box.
248
+ it("never lets the box's geometry animate", () => {
249
+ const spy = atWidth(348);
250
+ const { container } = render(<MapCanvas />);
251
+ const root = container.querySelector<HTMLElement>('[data-slot="map-canvas"]')!;
252
+ expect(root.style.transitionProperty).toBe("none");
253
+ expect(MockMap.instances[0]!.transitionAtConstruction).toBe("none");
254
+ spy.mockRestore();
255
+ });
256
+
257
+ it("resolves a responsive height at the measured tier", () => {
258
+ const spy = atWidth(868);
259
+ const { container } = render(<MapCanvas height={{ base: 420, narrow: { aspect: 1 } }} />);
260
+ const root = container.querySelector<HTMLElement>('[data-slot="map-canvas"]')!;
261
+ expect(root).toHaveAttribute("data-map-breakpoint", "wide");
262
+ expect(root.style.height).toBe("420px");
263
+ spy.mockRestore();
264
+ });
265
+
266
+ // MapLibre fits `bounds` ONCE, against the box it measures at construction.
267
+ // A box that settles later (a responsive height landing after the first
268
+ // paint, a panel opening) used to keep that stale viewport: measured on the
269
+ // narrow locator, a 348×218 fit left on a 348×348 box drew North America
270
+ // instead of Lake Ontario ("500 km" on the scale bar, not "50 km").
271
+ describe("re-fitting bounds when the box settles", () => {
272
+ const BOUNDS: [[number, number], [number, number]] = [
273
+ [-80.05, 43.2],
274
+ [-76.2, 44.28],
275
+ ];
276
+
277
+ /** A resizable box: mutate `box`, then `fireResize()`. */
278
+ function sizedBox(box: { width: number; height: number }) {
279
+ const rectSpy = vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
280
+ () =>
281
+ ({
282
+ width: box.width,
283
+ height: box.height,
284
+ top: 0,
285
+ left: 0,
286
+ right: box.width,
287
+ bottom: box.height,
288
+ x: 0,
289
+ y: 0,
290
+ toJSON: () => ({}),
291
+ }) as DOMRect,
292
+ );
293
+ const callbacks: ResizeObserverCallback[] = [];
294
+ class TestResizeObserver {
295
+ constructor(callback: ResizeObserverCallback) {
296
+ callbacks.push(callback);
297
+ }
298
+ observe() {}
299
+ unobserve() {}
300
+ disconnect() {}
301
+ }
302
+ const previous = globalThis.ResizeObserver;
303
+ globalThis.ResizeObserver = TestResizeObserver as unknown as typeof ResizeObserver;
304
+ return {
305
+ fireResize: () =>
306
+ act(() => {
307
+ callbacks.forEach((callback) =>
308
+ callback([] as unknown as ResizeObserverEntry[], {} as ResizeObserver),
309
+ );
310
+ }),
311
+ restore: () => {
312
+ rectSpy.mockRestore();
313
+ globalThis.ResizeObserver = previous;
314
+ },
315
+ };
316
+ }
317
+
318
+ it("re-fits bounds after the container box changes size", () => {
319
+ const box = { width: 348, height: 218 };
320
+ const { fireResize, restore } = sizedBox(box);
321
+ render(<MapCanvas bounds={BOUNDS} fitBoundsOptions={{ padding: 24 }} />);
322
+ const map = MockMap.instances[0]!;
323
+ // Nothing to re-fit while the box is the one MapLibre measured.
324
+ expect(map.fitBoundsCalls).toEqual([]);
325
+
326
+ box.height = 348;
327
+ fireResize();
328
+
329
+ expect(map.fitBoundsCalls).toEqual([
330
+ { bounds: BOUNDS, options: { padding: 24, duration: 0 } },
331
+ ]);
332
+ expect(map.resizeCount).toBeGreaterThan(0);
333
+ restore();
334
+ });
335
+
336
+ it("stops re-fitting once a gesture has moved the map", () => {
337
+ const box = { width: 868, height: 543 };
338
+ const { fireResize, restore } = sizedBox(box);
339
+ render(<MapCanvas bounds={BOUNDS} />);
340
+ const map = MockMap.instances[0]!;
341
+ act(() => {
342
+ map.emit("movestart", undefined, { originalEvent: new MouseEvent("mousedown") });
343
+ });
344
+
345
+ box.height = 300;
346
+ fireResize();
347
+
348
+ expect(map.fitBoundsCalls).toEqual([]);
349
+ // The canvas still follows the box — only the framing is left alone.
350
+ expect(map.resizeCount).toBeGreaterThan(0);
351
+ restore();
352
+ });
353
+
354
+ it("resizes but never fits a map that was not given bounds", () => {
355
+ const box = { width: 868, height: 543 };
356
+ const { fireResize, restore } = sizedBox(box);
357
+ render(<MapCanvas center={[-79.38, 43.65]} zoom={9} />);
358
+ const map = MockMap.instances[0]!;
359
+ const before = map.resizeCount;
360
+
361
+ box.height = 300;
362
+ fireResize();
363
+
364
+ expect(map.fitBoundsCalls).toEqual([]);
365
+ expect(map.resizeCount).toBeGreaterThan(before);
366
+ restore();
367
+ });
368
+ });
369
+
370
+ it("renders no strips above or below the map until furniture asks", () => {
371
+ const { container } = render(<MapCanvas className="my-map" />);
372
+ expect(container.firstChild).toHaveClass("my-map");
373
+ expect(container.querySelector('[data-slot="map-canvas-above"]')).toBeNull();
374
+ expect(container.querySelector('[data-slot="map-canvas-below"]')).toBeNull();
375
+ });
376
+ });
377
+ });
378
+
379
+ describe("MapCanvas basemap labels (c-6, c-11)", () => {
380
+ /** A basemap's own layers: two label layers and one that only draws ground. */
381
+ function seedBasemapLayers(map: MockMap) {
382
+ map.addLayer({ id: "place-city", type: "symbol", layout: { "text-field": ["get", "name"] } });
383
+ map.addLayer({ id: "water-name", type: "symbol", layout: { "text-field": ["get", "name"] } });
384
+ map.addLayer({ id: "landcover", type: "fill", paint: { "fill-color": "#eee" } });
385
+ }
386
+
387
+ it("keeps the basemap's own labels by default", async () => {
388
+ const { rerender } = render(<MapCanvas />);
389
+ const map = MockMap.instances[0]!;
390
+ seedBasemapLayers(map);
391
+ rerender(<MapCanvas basemapLabels />);
392
+ await waitFor(() => expect(map.layoutProperties.size).toBe(0));
393
+ });
394
+
395
+ it("hides every text label layer when basemapLabels is false, and only those", async () => {
396
+ const { rerender } = render(<MapCanvas />);
397
+ const map = MockMap.instances[0]!;
398
+ seedBasemapLayers(map);
399
+ rerender(<MapCanvas basemapLabels={false} />);
400
+ await waitFor(() => {
401
+ expect(map.getLayoutProperty("place-city", "visibility")).toBe("none");
402
+ expect(map.getLayoutProperty("water-name", "visibility")).toBe("none");
403
+ });
404
+ expect(map.getLayoutProperty("landcover", "visibility")).toBeUndefined();
405
+ });
406
+
407
+ it("restores only the label layers it hid when basemapLabels goes back on", async () => {
408
+ const { rerender } = render(<MapCanvas />);
409
+ const map = MockMap.instances[0]!;
410
+ seedBasemapLayers(map);
411
+ // A layer the STYLE itself ships hidden must stay hidden.
412
+ map.addLayer({ id: "poi-name", type: "symbol", layout: { "text-field": ["get", "name"] } });
413
+ map.setLayoutProperty("poi-name", "visibility", "none");
414
+
415
+ rerender(<MapCanvas basemapLabels={false} />);
416
+ await waitFor(() => expect(map.getLayoutProperty("place-city", "visibility")).toBe("none"));
417
+
418
+ rerender(<MapCanvas basemapLabels />);
419
+ await waitFor(() => expect(map.getLayoutProperty("place-city", "visibility")).toBe("visible"));
420
+ expect(map.getLayoutProperty("water-name", "visibility")).toBe("visible");
421
+ expect(map.getLayoutProperty("poi-name", "visibility")).toBe("none");
422
+ });
423
+
424
+ // c-10: MapLibre owns the canvas element, so the house focus indicator has
425
+ // to be put on it — a bare canvas paints the browser's own blue outline,
426
+ // which does not follow the theme.
427
+ it("gives the MapLibre canvas the house focus ring", async () => {
428
+ render(<MapCanvas />);
429
+ const map = MockMap.instances[0]!;
430
+ await waitFor(() => expect(map.getCanvas().classList.contains("focus-ring")).toBe(true));
431
+ });
432
+ });
433
+
434
+ describe("MapCanvas with a plan", () => {
435
+ const PLAN = { width: 1600, height: 900 };
436
+
437
+ it("keeps a geographic canvas free of every plan option", () => {
438
+ render(<MapCanvas />);
439
+ const { options } = MockMap.instances[0]!;
440
+
441
+ expect(options.maxBounds).toBeUndefined();
442
+ expect(options.minZoom).toBeUndefined();
443
+ expect(options.maxZoom).toBeUndefined();
444
+ expect(options.dragRotate).toBeUndefined();
445
+ expect(options.cooperativeGestures).toBeUndefined();
446
+ expect(options.bounds).toBeUndefined();
447
+ });
448
+
449
+ it("frames the plan and clamps the camera to it", () => {
450
+ render(<MapCanvas plan={PLAN} />);
451
+ const { options } = MockMap.instances[0]!;
452
+ const crs = createPlanCrs(PLAN);
453
+
454
+ expect(options.bounds).toEqual(crs.bounds);
455
+ expect(options.maxBounds).toEqual(crs.maxBounds());
456
+ expect(options.minZoom).toBe(crs.minZoom);
457
+ expect(options.maxZoom).toBe(crs.maxZoom);
458
+ // A plan has no north and no horizon: rotation and pitch stay off.
459
+ expect(options.dragRotate).toBe(false);
460
+ expect(options.pitchWithRotate).toBe(false);
461
+ expect(options.touchPitch).toBe(false);
462
+ expect(options.bearing).toBe(0);
463
+ expect(options.pitch).toBe(0);
464
+ });
465
+
466
+ it("lets a caller override a plan default", () => {
467
+ render(<MapCanvas plan={PLAN} maxZoom={4} cooperativeGestures={false} />);
468
+ const { options } = MockMap.instances[0]!;
469
+
470
+ expect(options.maxZoom).toBe(4);
471
+ expect(options.cooperativeGestures).toBe(false);
472
+ });
473
+
474
+ it("does not wrap a plan around a globe", () => {
475
+ render(<MapCanvas plan={PLAN} projection={{ type: "globe" }} />);
476
+
477
+ expect(MockMap.instances[0]!.options.projection).toBeUndefined();
478
+ });
479
+
480
+ it("publishes the coordinate system on the context", async () => {
481
+ let seen: ReturnType<typeof useMap> | null = null;
482
+ function Probe() {
483
+ seen = useMap();
484
+ return null;
485
+ }
486
+
487
+ render(
488
+ <MapCanvas plan={PLAN}>
489
+ <Probe />
490
+ </MapCanvas>,
491
+ );
492
+
493
+ await waitFor(() => expect(seen?.plan).not.toBeNull());
494
+ expect(seen!.plan!.extent).toEqual({
495
+ width: 1600,
496
+ height: 900,
497
+ origin: "top-left",
498
+ unit: "px",
499
+ });
500
+ });
501
+
502
+ it("re-frames and re-clamps when the plan changes size", async () => {
503
+ const { rerender } = render(<MapCanvas plan={PLAN} />);
504
+ const map = MockMap.instances[0]!;
505
+
506
+ rerender(<MapCanvas plan={{ width: 400, height: 400 }} />);
507
+
508
+ const square = createPlanCrs({ width: 400, height: 400 });
509
+ await waitFor(() => expect(map.fitBoundsCalls.length).toBeGreaterThan(0));
510
+ expect(map.fitBoundsCalls.at(-1)!.bounds).toEqual(square.bounds);
511
+ expect(map.maxBounds).toEqual(square.maxBounds());
512
+ });
124
513
  });