@defra/interactive-map 0.0.16-alpha → 0.0.18-alpha

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 (224) hide show
  1. package/assets/images/slot-map.svg +264 -0
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/im-core.js +1 -1
  4. package/dist/esm/im-shell.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api/context.md +53 -7
  8. package/docs/api/map-style-config.md +41 -2
  9. package/docs/api/marker-config.md +53 -11
  10. package/docs/api/slots.md +16 -15
  11. package/docs/api/symbol-config.md +160 -0
  12. package/docs/api/symbol-registry.md +115 -0
  13. package/docs/api.md +25 -22
  14. package/docs/getting-started.md +4 -1
  15. package/docs/plugins/datasets.md +657 -0
  16. package/docs/plugins/interact.md +68 -43
  17. package/docs/plugins/search.md +15 -3
  18. package/docs/plugins.md +1 -1
  19. package/package.json +2 -2
  20. package/plugins/beta/datasets/dist/css/index.css +103 -15
  21. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  22. package/plugins/beta/datasets/dist/esm/index.js +1 -1
  23. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  24. package/plugins/beta/datasets/dist/umd/index.js +1 -1
  25. package/plugins/beta/datasets/src/DatasetsInit.jsx +29 -9
  26. package/plugins/beta/datasets/src/adapters/maplibre/index.js +18 -0
  27. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +159 -0
  28. package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +75 -0
  29. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +440 -0
  30. package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +27 -0
  31. package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +31 -0
  32. package/plugins/beta/datasets/src/api/addDataset.js +2 -8
  33. package/plugins/beta/datasets/src/api/getOpacity.js +17 -0
  34. package/plugins/beta/datasets/src/api/getStyle.js +13 -0
  35. package/plugins/beta/datasets/src/api/removeDataset.js +2 -44
  36. package/plugins/beta/datasets/src/api/setData.js +10 -0
  37. package/plugins/beta/datasets/src/api/setDatasetVisibility.js +37 -0
  38. package/plugins/beta/datasets/src/api/setFeatureVisibility.js +22 -0
  39. package/plugins/beta/datasets/src/api/setOpacity.js +29 -0
  40. package/plugins/beta/datasets/src/api/setStyle.js +22 -0
  41. package/plugins/beta/datasets/src/components/EmptyKey.jsx +7 -0
  42. package/plugins/beta/datasets/src/components/EmptyKey.test.jsx +21 -0
  43. package/plugins/beta/datasets/src/components/KeySvg.jsx +24 -0
  44. package/plugins/beta/datasets/src/components/KeySvgLine.jsx +19 -0
  45. package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +15 -0
  46. package/plugins/beta/datasets/src/components/KeySvgRect.jsx +22 -0
  47. package/plugins/beta/datasets/src/components/KeySvgSymbol.jsx +16 -0
  48. package/plugins/beta/datasets/src/components/svgProperties.js +20 -0
  49. package/plugins/beta/datasets/src/datasets.js +39 -56
  50. package/plugins/beta/datasets/src/defaults.js +44 -8
  51. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +34 -25
  52. package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +2 -2
  53. package/plugins/beta/datasets/src/index.js +2 -1
  54. package/plugins/beta/datasets/src/manifest.js +25 -17
  55. package/plugins/beta/datasets/src/panels/Key.jsx +51 -51
  56. package/plugins/beta/datasets/src/panels/Key.module.scss +59 -9
  57. package/plugins/beta/datasets/src/panels/Layers.jsx +132 -29
  58. package/plugins/beta/datasets/src/panels/Layers.module.scss +56 -8
  59. package/plugins/beta/datasets/src/reducer.js +134 -9
  60. package/plugins/beta/datasets/src/reducers/keyReducer.js +34 -0
  61. package/plugins/beta/datasets/src/utils/bbox.js +7 -5
  62. package/plugins/beta/datasets/src/utils/filters.js +5 -2
  63. package/plugins/beta/datasets/src/utils/mergeSublayer.js +86 -0
  64. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  65. package/plugins/beta/draw-es/src/DrawInit.jsx +3 -2
  66. package/plugins/beta/draw-ml/dist/css/index.css +21 -1
  67. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  68. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  69. package/plugins/beta/draw-ml/dist/umd/index.js +1 -1
  70. package/plugins/beta/draw-ml/src/DrawInit.jsx +4 -3
  71. package/plugins/beta/draw-ml/src/draw.scss +0 -7
  72. package/plugins/beta/draw-ml/src/manifest.js +16 -16
  73. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
  74. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
  75. package/plugins/beta/frame/src/Frame.jsx +5 -5
  76. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  77. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  78. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  79. package/plugins/beta/map-styles/src/MapStyles.jsx +5 -4
  80. package/plugins/beta/map-styles/src/MapStylesInit.jsx +5 -4
  81. package/plugins/beta/map-styles/src/manifest.js +1 -1
  82. package/plugins/beta/scale-bar/dist/css/index.css +1 -1
  83. package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -1
  84. package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -1
  85. package/plugins/beta/scale-bar/src/index.test.js +3 -3
  86. package/plugins/beta/scale-bar/src/manifest.js +3 -3
  87. package/plugins/beta/scale-bar/src/scaleBar.scss +2 -1
  88. package/plugins/interact/dist/css/index.css +1 -1
  89. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  90. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  91. package/plugins/interact/dist/umd/index.js +1 -1
  92. package/plugins/interact/src/InteractInit.jsx +14 -5
  93. package/plugins/interact/src/InteractInit.test.js +26 -6
  94. package/plugins/interact/src/api/enable.test.js +7 -7
  95. package/plugins/interact/src/defaults.js +4 -6
  96. package/plugins/interact/src/events.js +9 -6
  97. package/plugins/interact/src/events.test.js +28 -4
  98. package/plugins/interact/src/hooks/useHighlightSync.js +3 -3
  99. package/plugins/interact/src/hooks/useHighlightSync.test.js +6 -6
  100. package/plugins/interact/src/hooks/useHoverCursor.js +10 -0
  101. package/plugins/interact/src/hooks/useHoverCursor.test.js +44 -0
  102. package/plugins/interact/src/hooks/useInteractionHandlers.js +111 -69
  103. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +147 -32
  104. package/plugins/interact/src/interact.scss +0 -7
  105. package/plugins/interact/src/manifest.js +14 -18
  106. package/plugins/interact/src/manifest.test.js +3 -1
  107. package/plugins/interact/src/reducer.js +23 -4
  108. package/plugins/interact/src/reducer.test.js +60 -11
  109. package/plugins/interact/src/utils/buildStylesMap.js +17 -4
  110. package/plugins/interact/src/utils/buildStylesMap.test.js +16 -2
  111. package/plugins/interact/src/utils/featureQueries.js +11 -6
  112. package/plugins/interact/src/utils/featureQueries.test.js +8 -1
  113. package/plugins/search/dist/css/index.css +1 -1
  114. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  115. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  116. package/plugins/search/src/Search.jsx +3 -1
  117. package/plugins/search/src/components/Form/Form.module.scss +2 -1
  118. package/plugins/search/src/events/fetchSuggestions.js +6 -4
  119. package/plugins/search/src/events/fetchSuggestions.test.js +26 -4
  120. package/plugins/search/src/events/formHandlers.js +3 -3
  121. package/plugins/search/src/events/formHandlers.test.js +1 -1
  122. package/plugins/search/src/events/suggestionHandlers.js +2 -2
  123. package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
  124. package/plugins/search/src/utils/updateMap.js +3 -3
  125. package/plugins/search/src/utils/updateMap.test.js +3 -3
  126. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  127. package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
  128. package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
  129. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  130. package/providers/maplibre/dist/umd/index.js +1 -1
  131. package/providers/maplibre/src/appEvents.js +7 -0
  132. package/providers/maplibre/src/appEvents.test.js +18 -4
  133. package/providers/maplibre/src/maplibreProvider.js +52 -0
  134. package/providers/maplibre/src/maplibreProvider.test.js +105 -1
  135. package/providers/maplibre/src/utils/highlightFeatures.js +37 -7
  136. package/providers/maplibre/src/utils/highlightFeatures.test.js +153 -95
  137. package/providers/maplibre/src/utils/hoverCursor.js +61 -0
  138. package/providers/maplibre/src/utils/hoverCursor.test.js +130 -0
  139. package/providers/maplibre/src/utils/patternImages.js +70 -0
  140. package/providers/maplibre/src/utils/patternImages.test.js +180 -0
  141. package/providers/maplibre/src/utils/queryFeatures.js +38 -16
  142. package/providers/maplibre/src/utils/queryFeatures.test.js +20 -3
  143. package/providers/maplibre/src/utils/rasteriseToImageData.js +30 -0
  144. package/providers/maplibre/src/utils/rasteriseToImageData.test.js +69 -0
  145. package/providers/maplibre/src/utils/symbolImages.js +147 -0
  146. package/providers/maplibre/src/utils/symbolImages.test.js +248 -0
  147. package/src/App/components/Actions/Actions.jsx +2 -2
  148. package/src/App/components/Actions/Actions.module.scss +0 -7
  149. package/src/App/components/Actions/Actions.test.jsx +1 -1
  150. package/src/App/components/Icon/Icon.jsx +3 -2
  151. package/src/App/components/Icon/Icon.module.scss +4 -0
  152. package/src/App/components/Icon/Icon.test.jsx +43 -4
  153. package/src/App/components/MapButton/MapButton.jsx +42 -17
  154. package/src/App/components/MapButton/MapButton.module.scss +4 -13
  155. package/src/App/components/MapButton/MapButton.test.jsx +27 -3
  156. package/src/App/components/Markers/Markers.jsx +122 -27
  157. package/src/App/components/Markers/Markers.module.scss +0 -10
  158. package/src/App/components/Markers/Markers.test.jsx +246 -0
  159. package/src/App/components/PopupMenu/PopupMenu.jsx +51 -274
  160. package/src/App/components/PopupMenu/PopupMenu.module.scss +14 -7
  161. package/src/App/components/PopupMenu/PopupMenu.test.jsx +70 -1
  162. package/src/App/components/PopupMenu/usePopupMenu.js +258 -0
  163. package/src/App/hooks/useButtonStateEvaluator.js +12 -2
  164. package/src/App/hooks/useButtonStateEvaluator.test.js +38 -4
  165. package/src/App/hooks/useInterfaceAPI.js +6 -0
  166. package/src/App/hooks/useInterfaceAPI.test.js +156 -0
  167. package/src/App/hooks/useLayoutMeasurements.js +84 -18
  168. package/src/App/hooks/useLayoutMeasurements.test.js +124 -17
  169. package/src/App/hooks/useMarkersAPI.js +2 -5
  170. package/src/App/hooks/useMarkersAPI.test.js +4 -4
  171. package/src/App/layout/Layout.jsx +14 -9
  172. package/src/App/layout/Layout.test.jsx +6 -4
  173. package/src/App/layout/layout.module.scss +67 -29
  174. package/src/App/registry/pluginRegistry.js +1 -1
  175. package/src/App/renderer/HtmlElementHost.jsx +2 -1
  176. package/src/App/renderer/HtmlElementHost.test.jsx +7 -7
  177. package/src/App/renderer/mapButtons.js +1 -1
  178. package/src/App/renderer/mapPanels.test.js +2 -2
  179. package/src/App/renderer/slotHelpers.js +2 -2
  180. package/src/App/renderer/slotHelpers.test.js +5 -5
  181. package/src/App/renderer/slots.js +9 -5
  182. package/src/App/store/AppProvider.jsx +3 -1
  183. package/src/App/store/AppProvider.test.jsx +1 -1
  184. package/src/App/store/ServiceProvider.jsx +8 -4
  185. package/src/App/store/appActionsMap.js +16 -0
  186. package/src/App/store/appActionsMap.test.js +27 -0
  187. package/src/App/store/appDispatchMiddleware.js +1 -1
  188. package/src/App/store/appDispatchMiddleware.test.js +2 -2
  189. package/src/App/store/appReducer.js +2 -0
  190. package/src/App/store/mapActionsMap.js +4 -6
  191. package/src/App/store/mapActionsMap.test.js +3 -2
  192. package/src/App/store/mapReducer.js +2 -1
  193. package/src/InteractiveMap/InteractiveMap.js +4 -0
  194. package/src/config/appConfig.js +5 -8
  195. package/src/config/appConfig.test.js +1 -2
  196. package/src/config/defaults.js +0 -2
  197. package/src/config/events.js +28 -0
  198. package/src/config/mapTheme.js +56 -0
  199. package/src/config/patternConfig.js +16 -0
  200. package/src/config/symbolConfig.js +80 -0
  201. package/src/scss/main.scss +1 -0
  202. package/src/scss/settings/_colors.scss +0 -9
  203. package/src/scss/settings/_dimensions.scss +0 -1
  204. package/src/services/patternRegistry.js +40 -0
  205. package/src/services/patternRegistry.test.js +48 -0
  206. package/src/services/symbolRegistry.js +113 -0
  207. package/src/services/symbolRegistry.test.js +262 -0
  208. package/src/types.js +93 -11
  209. package/src/utils/getSafeZoneInset.js +9 -7
  210. package/src/utils/getSafeZoneInset.test.js +10 -10
  211. package/src/utils/patternUtils.js +94 -0
  212. package/src/utils/patternUtils.test.js +160 -0
  213. package/src/utils/symbolUtils.js +85 -0
  214. package/src/utils/symbolUtils.test.js +156 -0
  215. package/webpack.dev.mjs +1 -1
  216. package/docs/api/slot-map.svg +0 -1
  217. package/plugins/beta/datasets/src/api/hideDataset.js +0 -14
  218. package/plugins/beta/datasets/src/api/hideFeatures.js +0 -41
  219. package/plugins/beta/datasets/src/api/showDataset.js +0 -14
  220. package/plugins/beta/datasets/src/api/showFeatures.js +0 -44
  221. package/plugins/beta/datasets/src/handleSetMapStyle.js +0 -54
  222. package/plugins/beta/datasets/src/mapLayers.js +0 -164
  223. /package/src/{utils → services}/logger.js +0 -0
  224. /package/src/{utils → services}/logger.test.js +0 -0
@@ -158,10 +158,6 @@
158
158
  & > *:empty {
159
159
  display: none;
160
160
  }
161
-
162
- @media (prefers-reduced-motion: no-preference) {
163
- transition: bottom 0.15s ease;
164
- }
165
161
  }
166
162
 
167
163
  .im-o-app__left-top {
@@ -212,10 +208,6 @@
212
208
  top: var(--right-offset-top);
213
209
  bottom: var(--right-offset-bottom);
214
210
  gap: var(--divider-gap);
215
-
216
- @media (prefers-reduced-motion: no-preference) {
217
- transition: bottom 0.15s ease;
218
- }
219
211
  }
220
212
 
221
213
  .im-o-app__right-top {
@@ -238,55 +230,74 @@
238
230
  }
239
231
 
240
232
  // ---------------------------------------------------
241
- // Footer: Logo, scalebar, copyright etc
233
+ // Bottom: Logo, scalebar, copyright etc
242
234
  // ---------------------------------------------------
243
235
 
244
- .im-o-app__footer {
236
+ .im-o-app__bottom {
245
237
  display: flex;
246
238
  justify-content: space-between;
247
239
  align-items: flex-end;
248
240
  z-index: -2; // Support masking the viewport
249
241
  }
250
242
 
251
- .im-o-app__footer-col {
243
+ .im-o-app__bottom-col {
252
244
  display: flex;
253
- flex-direction: column;
254
245
  min-width: auto;
255
246
  }
256
247
 
257
- .im-o-app__footer-col:first-child {
258
- justify-content: flex-start;
248
+ .im-o-app__bottom-col:first-child {
249
+ flex-direction: row;
250
+ align-items: flex-end;
259
251
  flex-shrink: 0;
252
+ gap: var(--divider-gap);
260
253
  }
261
254
 
262
- .im-o-app__footer-col:last-child {
255
+ .im-o-app__bottom-col:last-child {
256
+ position: relative; // anchor for absolutely-positioned attributions
263
257
  flex: 1 1 auto;
264
- justify-content: flex-end;
265
- text-align: right;
258
+ flex-direction: column;
259
+ align-items: flex-end;
266
260
  min-width: 0;
267
261
  }
268
262
 
263
+ // Horizontal button rows within the bottom area
264
+ .im-o-app__bottom-left,
265
+ .im-o-app__bottom-right {
266
+ display: flex;
267
+ flex-direction: row;
268
+ gap: var(--divider-gap);
269
+ }
270
+
271
+ .im-o-app__bottom-right {
272
+ justify-content: flex-end;
273
+ }
274
+
269
275
  // ---------------------------------------------------
270
276
  // Attributions:
271
277
  // ---------------------------------------------------
272
278
 
273
279
  .im-o-app__attributions:not(:empty) {
280
+ position: absolute;
281
+ bottom: calc(var(--primary-gap) * -1);
282
+ right: calc(var(--primary-gap) * -1);
274
283
  display: flex;
275
284
  justify-content: flex-end;
276
- position: relative;
277
- padding-top: var(--divider-gap);
278
- margin-bottom: calc(var(--primary-gap) * -1);
279
- margin-right: calc(var(--primary-gap) * -1);
280
285
  }
281
286
 
282
287
  // ---------------------------------------------------
283
- // Bottom:
288
+ // Drawer:
284
289
  // ---------------------------------------------------
285
290
 
286
- .im-o-app__bottom {
291
+ .im-o-app__drawer {
287
292
  z-index: 1;
288
293
  }
289
294
 
295
+ .im-o-app__drawer .im-c-panel {
296
+ @include tools.border-focus-corner-override(
297
+ $corners: 'top'
298
+ );
299
+ }
300
+
290
301
  // ---------------------------------------------------
291
302
  // Actions:
292
303
  // ---------------------------------------------------
@@ -387,10 +398,14 @@
387
398
  max-height: calc(100% - (var(--primary-gap) * 2));
388
399
  }
389
400
 
390
- .im-c-panel--bottom {
401
+ .im-c-panel--drawer {
391
402
  top: auto;
392
403
  bottom: 0;
393
404
  max-height: 85%;
405
+
406
+ @include tools.border-focus-corner-override(
407
+ $corners: 'top'
408
+ );
394
409
  }
395
410
 
396
411
  [class*="im-c-panel--"][class*="-button"] { // Adjacent to button
@@ -452,15 +467,27 @@
452
467
  width: 100%;
453
468
  left: 0;
454
469
  bottom: calc(var(--primary-gap) * 2);
455
-
470
+
456
471
  .im-c-panel {
457
472
  max-width: var(--action-bar-max-width);
458
473
  }
474
+
475
+ // max-height animation on a bottom-anchored element grows upward, looking like
476
+ // a slide-up. Override to opacity-only so the bar fades in instead.
477
+ @media (prefers-reduced-motion: no-preference) {
478
+ .im-c-actions {
479
+ transition: opacity var(--duration) ease;
480
+ }
481
+ }
482
+
483
+ .im-c-actions--hidden {
484
+ opacity: 0;
485
+ }
459
486
  }
460
487
  }
461
488
 
462
- // Bottom panels corners removed on mobile
463
- .im-o-app--mobile .im-o-app__bottom,
489
+ // Drawer panels corners removed on mobile
490
+ .im-o-app--mobile .im-o-app__drawer,
464
491
  .im-o-app--mobile .im-o-app__actions {
465
492
  margin: var(--primary-gap) calc(var(--primary-gap) * -1) calc(var(--primary-gap) * -1) calc(var(--primary-gap) * -1);
466
493
 
@@ -473,10 +500,21 @@
473
500
  }
474
501
  }
475
502
 
476
- .im-o-app--mobile .im-o-app__bottom .im-c-panel {
503
+ .im-o-app--mobile .im-o-app__drawer .im-c-panel {
477
504
  clip-path: inset(-20px 0 0 0);
478
505
  }
479
506
 
507
+ // Inset focus ring on panels that are flush with the container edge,
508
+ // so the ::after ring isn't clipped by the parent overflow:hidden
509
+ .im-o-app--mobile .im-o-app__drawer .im-c-panel::after,
510
+ .im-o-app--mobile .im-o-app__actions .im-c-panel::after,
511
+ .im-o-app__modal .im-c-panel--drawer::after {
512
+ top: var(--focus-border-width);
513
+ right: var(--focus-border-width);
514
+ bottom: var(--focus-border-width);
515
+ left: var(--focus-border-width);
516
+ }
517
+
480
518
  // 4. State styles
481
519
 
482
520
  // 5. Responsive tweaks
@@ -488,4 +526,4 @@
488
526
  width: 100%;
489
527
  max-width: 80%;
490
528
  pointer-events: none;
491
- }
529
+ }
@@ -2,7 +2,7 @@
2
2
  import { registerIcon } from './iconRegistry.js'
3
3
  import { registerKeyboardShortcut } from './keyboardShortcutRegistry.js'
4
4
  import { allowedSlots } from '../renderer/slots.js'
5
- import { logger } from '../../utils/logger.js'
5
+ import { logger } from '../../services/logger.js'
6
6
 
7
7
  const asArray = (value) => Array.isArray(value) ? value : [value]
8
8
 
@@ -19,7 +19,8 @@ export const getSlotRef = (slot, layoutRefs) => {
19
19
  middle: layoutRefs.middleRef,
20
20
  'right-top': layoutRefs.rightTopRef,
21
21
  'right-bottom': layoutRefs.rightBottomRef,
22
- bottom: layoutRefs.bottomRef,
22
+ 'bottom-right': layoutRefs.bottomRightRef,
23
+ drawer: layoutRefs.drawerRef,
23
24
  actions: layoutRefs.actionsRef,
24
25
  modal: layoutRefs.modalRef
25
26
  }
@@ -14,8 +14,8 @@ jest.mock('../components/Panel/Panel.jsx', () => ({
14
14
  }))
15
15
  jest.mock('./slots.js', () => ({
16
16
  allowedSlots: {
17
- panel: ['left-top', 'side', 'modal', 'bottom'],
18
- control: ['left-top', 'banner', 'bottom', 'actions']
17
+ panel: ['left-top', 'side', 'modal', 'drawer'],
18
+ control: ['left-top', 'banner', 'drawer', 'actions']
19
19
  }
20
20
  }))
21
21
 
@@ -28,7 +28,7 @@ const SlotHarness = ({ layoutRefs, children }) => (
28
28
  <div ref={layoutRefs.leftTopRef} data-slot='left-top' />
29
29
  <div ref={layoutRefs.sideRef} data-slot='side' />
30
30
  <div ref={layoutRefs.modalRef} data-slot='modal' />
31
- <div ref={layoutRefs.bottomRef} data-slot='bottom' />
31
+ <div ref={layoutRefs.drawerRef} data-slot='drawer' />
32
32
  <div ref={layoutRefs.bannerRef} data-slot='banner' />
33
33
  <div ref={layoutRefs.actionsRef} data-slot='actions' />
34
34
  {children}
@@ -47,7 +47,7 @@ describe('HtmlElementHost', () => {
47
47
  topRightColRef: { current: null },
48
48
  leftTopRef: { current: null },
49
49
  middleRef: { current: null },
50
- bottomRef: { current: null },
50
+ drawerRef: { current: null },
51
51
  actionsRef: { current: null },
52
52
  modalRef: { current: null },
53
53
  viewportRef: { current: null },
@@ -146,13 +146,13 @@ describe('HtmlElementHost', () => {
146
146
  expect(getByTestId('panel-p1').dataset.open).toBe('true')
147
147
  })
148
148
 
149
- it('resolves bottom slot to left-top on desktop', () => {
149
+ it('resolves drawer slot to left-top on desktop', () => {
150
150
  const { container } = renderWithSlots({
151
- panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'bottom' } } },
151
+ panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'drawer' } } },
152
152
  openPanels: { p1: { props: {} } }
153
153
  })
154
154
  expect(container.querySelector('[data-slot="left-top"] [data-testid="panel-p1"]')).toBeTruthy()
155
- expect(container.querySelector('[data-slot="bottom"] [data-testid="panel-p1"]')).toBeNull()
155
+ expect(container.querySelector('[data-slot="drawer"] [data-testid="panel-p1"]')).toBeNull()
156
156
  })
157
157
 
158
158
  it('only shows topmost modal panel', () => {
@@ -1,7 +1,7 @@
1
1
  // src/core/renderers/mapButtons.js
2
2
  import { MapButton } from '../components/MapButton/MapButton.jsx'
3
3
  import { allowedSlots } from './slots.js'
4
- import { logger } from '../../utils/logger.js'
4
+ import { logger } from '../../services/logger.js'
5
5
 
6
6
  function getMatchingButtons ({ appState, buttonConfig, slot, evaluateProp }) {
7
7
  const { breakpoint, mode } = appState
@@ -145,10 +145,10 @@ describe('mapPanels', () => {
145
145
  expect(map()).toHaveLength(1)
146
146
  })
147
147
 
148
- it('replaces bottom slot with left-top on non-mobile breakpoints', () => {
148
+ it('replaces drawer slot with left-top on non-mobile breakpoints', () => {
149
149
  defaultAppState.panelConfig = ({
150
150
  p1: {
151
- desktop: { slot: 'bottom' },
151
+ desktop: { slot: 'drawer' },
152
152
  includeModes: ['view']
153
153
  }
154
154
  })
@@ -3,14 +3,14 @@ import { allowedSlots } from './slots.js'
3
3
 
4
4
  /**
5
5
  * Resolves the target slot for a panel based on its breakpoint config.
6
- * Modal panels always render in the 'modal' slot, and the bottom slot
6
+ * Modal panels always render in the 'modal' slot, and the drawer slot
7
7
  * is only available on mobile — tablet and desktop fall back to 'left-top'.
8
8
  */
9
9
  export const resolveTargetSlot = (bpConfig, breakpoint) => {
10
10
  if (bpConfig.modal) {
11
11
  return 'modal'
12
12
  }
13
- if (bpConfig.slot === 'bottom' && ['tablet', 'desktop'].includes(breakpoint)) {
13
+ if (bpConfig.slot === 'drawer' && ['tablet', 'desktop'].includes(breakpoint)) {
14
14
  return 'left-top'
15
15
  }
16
16
  return bpConfig.slot
@@ -7,13 +7,13 @@ describe('resolveTargetSlot', () => {
7
7
  expect(resolveTargetSlot({ modal: true, slot: 'side' }, 'desktop')).toBe('modal')
8
8
  })
9
9
 
10
- it('replaces bottom with left-top on tablet and desktop', () => {
11
- expect(resolveTargetSlot({ slot: 'bottom' }, 'tablet')).toBe('left-top')
12
- expect(resolveTargetSlot({ slot: 'bottom' }, 'desktop')).toBe('left-top')
10
+ it('replaces drawer with left-top on tablet and desktop', () => {
11
+ expect(resolveTargetSlot({ slot: 'drawer' }, 'tablet')).toBe('left-top')
12
+ expect(resolveTargetSlot({ slot: 'drawer' }, 'desktop')).toBe('left-top')
13
13
  })
14
14
 
15
- it('keeps bottom on mobile', () => {
16
- expect(resolveTargetSlot({ slot: 'bottom' }, 'mobile')).toBe('bottom')
15
+ it('keeps drawer on mobile', () => {
16
+ expect(resolveTargetSlot({ slot: 'drawer' }, 'mobile')).toBe('drawer')
17
17
  })
18
18
 
19
19
  it('returns slot as-is otherwise', () => {
@@ -10,8 +10,9 @@ export const layoutSlots = Object.freeze({
10
10
  MIDDLE: 'middle',
11
11
  RIGHT_TOP: 'right-top',
12
12
  RIGHT_BOTTOM: 'right-bottom',
13
- FOOTER_RIGHT: 'footer-right',
14
- BOTTOM: 'bottom',
13
+ BOTTOM_LEFT: 'bottom-left',
14
+ BOTTOM_RIGHT: 'bottom-right',
15
+ DRAWER: 'drawer',
15
16
  ACTIONS: 'actions',
16
17
  MODAL: 'modal' // internal only
17
18
  })
@@ -22,8 +23,9 @@ export const allowedSlots = Object.freeze({
22
23
  layoutSlots.TOP_LEFT,
23
24
  layoutSlots.TOP_RIGHT,
24
25
  layoutSlots.MIDDLE,
25
- layoutSlots.FOOTER_RIGHT,
26
- layoutSlots.BOTTOM,
26
+ layoutSlots.RIGHT_BOTTOM,
27
+ layoutSlots.BOTTOM_RIGHT,
28
+ layoutSlots.DRAWER,
27
29
  layoutSlots.ACTIONS
28
30
  ],
29
31
  panel: [
@@ -34,7 +36,7 @@ export const allowedSlots = Object.freeze({
34
36
  layoutSlots.MIDDLE,
35
37
  layoutSlots.RIGHT_TOP,
36
38
  layoutSlots.RIGHT_BOTTOM,
37
- layoutSlots.BOTTOM, // Typicaly on mobile
39
+ layoutSlots.DRAWER, // Typically on mobile
38
40
  layoutSlots.MODAL // Internal only
39
41
  ],
40
42
  button: [
@@ -45,6 +47,8 @@ export const allowedSlots = Object.freeze({
45
47
  layoutSlots.LEFT_BOTTOM,
46
48
  layoutSlots.RIGHT_TOP,
47
49
  layoutSlots.RIGHT_BOTTOM,
50
+ layoutSlots.BOTTOM_LEFT,
51
+ layoutSlots.BOTTOM_RIGHT,
48
52
  layoutSlots.ACTIONS
49
53
  ]
50
54
  })
@@ -26,8 +26,10 @@ export const AppProvider = ({ options, children }) => {
26
26
  rightRef: useRef(null),
27
27
  rightTopRef: useRef(null),
28
28
  rightBottomRef: useRef(null),
29
+ drawerRef: useRef(null),
29
30
  bottomRef: useRef(null),
30
- footerRef: useRef(null),
31
+ bottomRightRef: useRef(null),
32
+ attributionsRef: useRef(null),
31
33
  actionsRef: useRef(null),
32
34
  bannerRef: useRef(null),
33
35
  modalRef: useRef(null),
@@ -110,7 +110,7 @@ describe('AppProvider', () => {
110
110
  expect(contextValue).toHaveProperty('mode')
111
111
  expect(contextValue).toHaveProperty('openPanels')
112
112
  expect(contextValue.layoutRefs).toHaveProperty('mainRef')
113
- expect(contextValue.layoutRefs).toHaveProperty('footerRef')
113
+ expect(contextValue.layoutRefs).toHaveProperty('bottomRef')
114
114
  })
115
115
 
116
116
  test('dispatch fallback uses options.panelRegistry.getPanelConfig() when state.panelConfig missing', () => {
@@ -1,25 +1,29 @@
1
1
  // src/App/store/ServiceProvider.jsx
2
2
  import React, { createContext, useMemo, useRef } from 'react'
3
- import { EVENTS } from '../../config/events.js'
4
3
  import { createAnnouncer } from '../../services/announcer.js'
5
4
  import { reverseGeocode } from '../../services/reverseGeocode.js'
6
5
  import { useConfig } from '../store/configContext.js'
7
6
  import { closeApp } from '../../services/closeApp.js'
7
+ import { symbolRegistry } from '../../services/symbolRegistry.js'
8
+ import { patternRegistry } from '../../services/patternRegistry.js'
8
9
 
9
10
  export const ServiceContext = createContext(null)
10
11
 
11
12
  export const ServiceProvider = ({ eventBus, children }) => {
12
- const { id, handleExitClick } = useConfig()
13
+ const { id, handleExitClick, symbolDefaults: constructorSymbolDefaults } = useConfig()
13
14
  const mapStatusRef = useRef(null)
14
15
  const announce = useMemo(() => createAnnouncer(mapStatusRef), [])
15
16
 
17
+ symbolRegistry.setDefaults(constructorSymbolDefaults || {})
18
+
16
19
  const services = useMemo(() => ({
17
20
  announce,
18
21
  reverseGeocode: (zoom, center) => reverseGeocode(zoom, center),
19
- events: EVENTS,
20
22
  eventBus,
21
23
  mapStatusRef,
22
- closeApp: () => closeApp(id, handleExitClick, eventBus)
24
+ closeApp: () => closeApp(id, handleExitClick, eventBus),
25
+ symbolRegistry,
26
+ patternRegistry
23
27
  }), [announce])
24
28
 
25
29
  return (
@@ -152,6 +152,12 @@ const toggleHasExclusiveControl = (state, payload) => {
152
152
  }
153
153
  }
154
154
 
155
+ const setPluginsEvaluated = (state) =>
156
+ state.arePluginsEvaluated ? state : { ...state, arePluginsEvaluated: true }
157
+
158
+ const clearPluginsEvaluated = (state) =>
159
+ state.arePluginsEvaluated ? { ...state, arePluginsEvaluated: false } : state
160
+
155
161
  const setSafeZoneInset = (state, { safeZoneInset, syncMapPadding = true }) => {
156
162
  return shallowEqual(state.safeZoneInset, safeZoneInset)
157
163
  ? state
@@ -163,6 +169,13 @@ const setSafeZoneInset = (state, { safeZoneInset, syncMapPadding = true }) => {
163
169
  }
164
170
  }
165
171
 
172
+ const toggleAppVisible = (state, payload) => {
173
+ return {
174
+ ...state,
175
+ appVisible: payload
176
+ }
177
+ }
178
+
166
179
  const toggleButtonDisabled = (state, payload) => {
167
180
  const { id, isDisabled } = payload
168
181
  const updated = new Set(state.disabledButtons)
@@ -358,12 +371,15 @@ export const actionsMap = {
358
371
  SET_HYBRID_FULLSCREEN: setHybridFullscreen,
359
372
  SET_INTERFACE_TYPE: setInterfaceType,
360
373
  SET_MODE: setMode,
374
+ PLUGINS_EVALUATED: setPluginsEvaluated,
375
+ CLEAR_PLUGINS_EVALUATED: clearPluginsEvaluated,
361
376
  SET_SAFE_ZONE_INSET: setSafeZoneInset,
362
377
  REVERT_MODE: revertMode,
363
378
  OPEN_PANEL: openPanel,
364
379
  CLOSE_PANEL: closePanel,
365
380
  CLOSE_ALL_PANELS: closeAllPanels,
366
381
  RESTORE_PREVIOUS_PANELS: restorePreviousPanels,
382
+ TOGGLE_APP_VISIBLE: toggleAppVisible,
367
383
  TOGGLE_HAS_EXCLUSIVE_CONTROL: toggleHasExclusiveControl,
368
384
  TOGGLE_BUTTON_DISABLED: toggleButtonDisabled,
369
385
  TOGGLE_BUTTON_HIDDEN: toggleButtonHidden,
@@ -128,6 +128,26 @@ describe('actionsMap full coverage', () => {
128
128
  expect(result.hasExclusiveControl).toBe(true)
129
129
  })
130
130
 
131
+ test('PLUGINS_EVALUATED is no-op when arePluginsEvaluated already true', () => {
132
+ const s = { ...state, arePluginsEvaluated: true }
133
+ expect(actionsMap.PLUGINS_EVALUATED(s)).toBe(s)
134
+ })
135
+
136
+ test('PLUGINS_EVALUATED sets arePluginsEvaluated when false', () => {
137
+ const s = { ...state, arePluginsEvaluated: false }
138
+ expect(actionsMap.PLUGINS_EVALUATED(s).arePluginsEvaluated).toBe(true)
139
+ })
140
+
141
+ test('CLEAR_PLUGINS_EVALUATED clears arePluginsEvaluated when true', () => {
142
+ const s = { ...state, arePluginsEvaluated: true }
143
+ expect(actionsMap.CLEAR_PLUGINS_EVALUATED(s).arePluginsEvaluated).toBe(false)
144
+ })
145
+
146
+ test('CLEAR_PLUGINS_EVALUATED is no-op when arePluginsEvaluated already false', () => {
147
+ const s = { ...state, arePluginsEvaluated: false }
148
+ expect(actionsMap.CLEAR_PLUGINS_EVALUATED(s)).toBe(s)
149
+ })
150
+
131
151
  test('SET_SAFE_ZONE_INSET branch true/false', () => {
132
152
  shallowEqualModule.shallowEqual.mockReturnValueOnce(false)
133
153
  const res1 = actionsMap.SET_SAFE_ZONE_INSET(state, { safeZoneInset: { top: 10, bottom: 10 } })
@@ -152,6 +172,13 @@ describe('actionsMap full coverage', () => {
152
172
  expect(r2.hiddenButtons.has('btn3')).toBe(false)
153
173
  })
154
174
 
175
+ test('TOGGLE_APP_VISIBLE sets appVisible to payload', () => {
176
+ const r1 = actionsMap.TOGGLE_APP_VISIBLE(state, true)
177
+ expect(r1.appVisible).toBe(true)
178
+ const r2 = actionsMap.TOGGLE_APP_VISIBLE(state, false)
179
+ expect(r2.appVisible).toBe(false)
180
+ })
181
+
155
182
  test('TOGGLE_BUTTON_PRESSED adds/removes button', () => {
156
183
  const r1 = actionsMap.TOGGLE_BUTTON_PRESSED(state, { id: 'btn6', isPressed: true })
157
184
  expect(r1.pressedButtons.has('btn6')).toBe(true)
@@ -3,7 +3,7 @@ import { EVENTS as events } from '../../config/events.js'
3
3
  import { defaultPanelConfig, defaultButtonConfig, defaultControlConfig } from '../../config/appConfig.js'
4
4
  import { deepMerge } from '../../utils/deepMerge.js'
5
5
  import { allowedSlots } from '../renderer/slots.js'
6
- import { logger } from '../../utils/logger.js'
6
+ import { logger } from '../../services/logger.js'
7
7
 
8
8
  const BREAKPOINTS = ['mobile', 'tablet', 'desktop']
9
9
 
@@ -180,7 +180,7 @@ describe('ADD_CONTROL', () => {
180
180
 
181
181
  it('does not warn when control has a valid slot', () => {
182
182
  run(
183
- { type: 'ADD_CONTROL', payload: { id: 'myCtrl', config: { mobile: { slot: 'bottom' }, tablet: { slot: 'top-left' }, desktop: { slot: 'top-left' } } } },
183
+ { type: 'ADD_CONTROL', payload: { id: 'myCtrl', config: { mobile: { slot: 'drawer' }, tablet: { slot: 'top-left' }, desktop: { slot: 'top-left' } } } },
184
184
  { breakpoint: 'desktop' }
185
185
  )
186
186
  expect(console.warn).not.toHaveBeenCalled()
@@ -238,7 +238,7 @@ describe('ADD_PANEL', () => {
238
238
 
239
239
  expect(eventBus.emit).toHaveBeenCalledWith(
240
240
  events.APP_PANEL_OPENED,
241
- { panelId: 'mobilePanel', slot: 'bottom' }
241
+ { panelId: 'mobilePanel', slot: 'drawer' }
242
242
  )
243
243
  })
244
244
 
@@ -29,7 +29,9 @@ export const initialState = (config) => {
29
29
  const openPanels = getInitialOpenPanels(panelConfig, initialBreakpoint)
30
30
 
31
31
  return {
32
+ appVisible: null,
32
33
  isLayoutReady: false,
34
+ arePluginsEvaluated: false,
33
35
  breakpoint: initialBreakpoint,
34
36
  interfaceType: initialInterfaceType,
35
37
  preferredColorScheme: autoColorScheme ? preferredColorScheme : appColorScheme,
@@ -12,12 +12,10 @@ const setMapReady = (state) => ({
12
12
  isMapReady: true
13
13
  })
14
14
 
15
- const setMapStyle = (state, payload) => {
16
- return {
17
- ...state,
18
- mapStyle: payload
19
- }
20
- }
15
+ const setMapStyle = (state, payload) => ({
16
+ ...state,
17
+ mapStyle: payload
18
+ })
21
19
 
22
20
  const setMapSize = (state, payload) => {
23
21
  return {
@@ -46,8 +46,9 @@ describe('actionsMap', () => {
46
46
  })
47
47
 
48
48
  test('SET_MAP_STYLE sets mapStyle', () => {
49
- const result = actionsMap.SET_MAP_STYLE(state, 'satellite')
50
- expect(result.mapStyle).toBe('satellite')
49
+ const mapStyle = { id: 'satellite' }
50
+ const result = actionsMap.SET_MAP_STYLE(state, mapStyle)
51
+ expect(result.mapStyle).toBe(mapStyle)
51
52
  expect(result.otherProp).toBe(state.otherProp)
52
53
  })
53
54
 
@@ -17,10 +17,11 @@ export const initialState = (config) => {
17
17
  // Does a plugin handle map styles
18
18
  const pluginHandlesMapStyles = !!registeredPlugins?.find(plugin => plugin.config?.handlesMapStyle)
19
19
 
20
+ const initialMapStyle = pluginHandlesMapStyles ? null : mapStyle
20
21
  return {
21
22
  isMapReady: false,
22
23
  mapProvider: null,
23
- mapStyle: pluginHandlesMapStyles ? null : mapStyle,
24
+ mapStyle: initialMapStyle,
24
25
  mapSize,
25
26
  center,
26
27
  zoom,
@@ -245,6 +245,8 @@ export default class InteractiveMap {
245
245
  if (parts.length > 1) {
246
246
  document.title = parts[parts.length - 1]
247
247
  }
248
+
249
+ this.eventBus.emit(events.APP_HIDDEN)
248
250
  }
249
251
 
250
252
  /**
@@ -262,6 +264,8 @@ export default class InteractiveMap {
262
264
  }
263
265
 
264
266
  updateDOMState(this)
267
+
268
+ this.eventBus.emit(events.APP_VISIBLE)
265
269
  }
266
270
 
267
271
  /**
@@ -93,6 +93,9 @@ export const defaultAppConfig = {
93
93
  }, {
94
94
  id: 'minus',
95
95
  svgContent: '<path d="M5 12h14"/>'
96
+ }, {
97
+ id: 'chevron',
98
+ svgContent: '<path d="m6 9 6 6 6-6"/>'
96
99
  }]
97
100
  }
98
101
 
@@ -113,7 +116,7 @@ export const defaultButtonConfig = {
113
116
  export const defaultPanelConfig = {
114
117
  label: 'Panel',
115
118
  mobile: {
116
- slot: 'bottom',
119
+ slot: 'drawer',
117
120
  open: true,
118
121
  dismissible: true,
119
122
  modal: false,
@@ -141,7 +144,7 @@ export const defaultPanelConfig = {
141
144
  export const defaultControlConfig = {
142
145
  label: 'Control',
143
146
  mobile: {
144
- slot: 'bottom'
147
+ slot: 'drawer'
145
148
  },
146
149
  tablet: {
147
150
  slot: 'top-left'
@@ -156,9 +159,3 @@ export const scaleFactor = {
156
159
  medium: 1.5,
157
160
  large: 2
158
161
  }
159
-
160
- export const markerSvgPaths = [{
161
- shape: 'pin',
162
- backgroundPath: 'M31 16.001c0 7.489-8.308 15.289-11.098 17.698-.533.4-1.271.4-1.803 0C15.309 31.29 7 23.49 7 16.001c0-6.583 5.417-12 12-12s12 5.417 12 12z',
163
- graphicPath: 'M19 11.001c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.241 5-5-2.24-5-5-5z'
164
- }]
@@ -1,5 +1,5 @@
1
1
  import { render } from '@testing-library/react'
2
- import { defaultAppConfig, defaultButtonConfig, scaleFactor, markerSvgPaths } from './appConfig'
2
+ import { defaultAppConfig, defaultButtonConfig, scaleFactor } from './appConfig'
3
3
 
4
4
  describe('defaultAppConfig', () => {
5
5
  const appState = {
@@ -135,6 +135,5 @@ describe('defaultAppConfig', () => {
135
135
  it('exports supplementary configs and constants', () => {
136
136
  expect(defaultButtonConfig.label).toBe('Button')
137
137
  expect(scaleFactor.large).toBe(2)
138
- expect(markerSvgPaths[0].shape).toBe('pin')
139
138
  })
140
139
  })
@@ -27,8 +27,6 @@ const defaults = {
27
27
  mapViewParamKey: 'mv',
28
28
  maxMobileWidth: 640,
29
29
  minDesktopWidth: 835,
30
- markerColor: '#ff0000',
31
- markerShape: 'pin',
32
30
  nudgePanDelta: 5,
33
31
  nudgeZoomDelta: 0.1,
34
32
  panDelta: 100,