@defra/interactive-map 0.0.15-alpha → 0.0.17-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 (263) hide show
  1. package/assets/css/docusaurus.css +104 -0
  2. package/assets/images/favicon.svg +1 -0
  3. package/assets/images/hero.png +0 -0
  4. package/assets/images/slot-map.svg +264 -0
  5. package/dist/css/index.css +1 -1
  6. package/dist/esm/im-core.js +1 -1
  7. package/dist/esm/im-shell.js +1 -1
  8. package/dist/umd/im-core.js +1 -1
  9. package/dist/umd/index.js +1 -1
  10. package/docs/api/slots.md +90 -6
  11. package/docs/api.md +4 -4
  12. package/docs/architecture.md +3 -1
  13. package/docs/{demo.mdx → examples.mdx} +1 -1
  14. package/docs/getting-started.md +5 -4
  15. package/docs/index.mdx +42 -0
  16. package/docs/plugins/datasets.md +561 -0
  17. package/docs/plugins/interact.md +176 -55
  18. package/docs/plugins/map-styles.md +64 -7
  19. package/docs/plugins/search.md +207 -63
  20. package/docs/plugins.md +8 -16
  21. package/docusaurus.config.cjs +34 -34
  22. package/jest.setup.js +1 -1
  23. package/package.json +6 -5
  24. package/plugins/beta/datasets/dist/css/index.css +85 -15
  25. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  26. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  27. package/plugins/beta/datasets/dist/umd/index.js +1 -1
  28. package/plugins/beta/datasets/src/DatasetsInit.jsx +24 -9
  29. package/plugins/beta/datasets/src/adapters/maplibre/index.js +18 -0
  30. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +113 -0
  31. package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +69 -0
  32. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +338 -0
  33. package/plugins/beta/datasets/src/adapters/maplibre/patternRegistry.js +48 -0
  34. package/plugins/beta/datasets/src/api/addDataset.js +3 -9
  35. package/plugins/beta/datasets/src/api/getOpacity.js +17 -0
  36. package/plugins/beta/datasets/src/api/getStyle.js +13 -0
  37. package/plugins/beta/datasets/src/api/removeDataset.js +3 -45
  38. package/plugins/beta/datasets/src/api/setData.js +8 -0
  39. package/plugins/beta/datasets/src/api/setDatasetVisibility.js +37 -0
  40. package/plugins/beta/datasets/src/api/setFeatureVisibility.js +22 -0
  41. package/plugins/beta/datasets/src/api/setOpacity.js +29 -0
  42. package/plugins/beta/datasets/src/api/setStyle.js +22 -0
  43. package/plugins/beta/datasets/src/datasets.js +33 -59
  44. package/plugins/beta/datasets/src/defaults.js +43 -9
  45. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +39 -30
  46. package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +2 -2
  47. package/plugins/beta/datasets/src/manifest.js +27 -19
  48. package/plugins/beta/datasets/src/panels/Key.jsx +129 -49
  49. package/plugins/beta/datasets/src/panels/Key.module.scss +48 -9
  50. package/plugins/beta/datasets/src/panels/Layers.jsx +131 -29
  51. package/plugins/beta/datasets/src/panels/Layers.module.scss +50 -8
  52. package/plugins/beta/datasets/src/reducer.js +128 -9
  53. package/plugins/beta/datasets/src/styles/patterns.js +157 -0
  54. package/plugins/beta/datasets/src/utils/bbox.js +8 -6
  55. package/plugins/beta/datasets/src/utils/filters.js +5 -2
  56. package/plugins/beta/datasets/src/utils/mergeSublayer.js +78 -0
  57. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  58. package/plugins/beta/draw-es/src/DrawInit.jsx +16 -16
  59. package/plugins/beta/draw-es/src/api/addFeature.js +3 -3
  60. package/plugins/beta/draw-es/src/api/deleteFeature.js +3 -3
  61. package/plugins/beta/draw-es/src/api/editFeature.js +3 -3
  62. package/plugins/beta/draw-es/src/api/newPolygon.js +3 -3
  63. package/plugins/beta/draw-es/src/events.js +52 -20
  64. package/plugins/beta/draw-es/src/events.test.js +301 -0
  65. package/plugins/beta/draw-es/src/graphic.js +1 -1
  66. package/plugins/beta/draw-es/src/manifest.js +4 -4
  67. package/plugins/beta/draw-es/src/reducer.js +1 -1
  68. package/plugins/beta/draw-es/src/sketchViewModel.js +1 -1
  69. package/plugins/beta/draw-ml/dist/css/index.css +1 -1
  70. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  71. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  72. package/plugins/beta/draw-ml/src/DrawInit.jsx +49 -52
  73. package/plugins/beta/draw-ml/src/api/deleteFeature.js +1 -1
  74. package/plugins/beta/draw-ml/src/api/editFeature.js +8 -5
  75. package/plugins/beta/draw-ml/src/api/newLine.js +0 -1
  76. package/plugins/beta/draw-ml/src/api/newPolygon.js +0 -1
  77. package/plugins/beta/draw-ml/src/api/split.js +4 -4
  78. package/plugins/beta/draw-ml/src/defaults.js +1 -1
  79. package/plugins/beta/draw-ml/src/draw.scss +0 -7
  80. package/plugins/beta/draw-ml/src/events.js +8 -6
  81. package/plugins/beta/draw-ml/src/manifest.js +29 -29
  82. package/plugins/beta/draw-ml/src/mapboxDraw.js +1 -1
  83. package/plugins/beta/draw-ml/src/mapboxSnap.js +17 -18
  84. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +31 -31
  85. package/plugins/beta/draw-ml/src/modes/disabledMode.js +1 -1
  86. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +11 -11
  87. package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +7 -7
  88. package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +8 -8
  89. package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +7 -7
  90. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +32 -24
  91. package/plugins/beta/draw-ml/src/reducer.js +1 -1
  92. package/plugins/beta/draw-ml/src/undoStack.js +4 -4
  93. package/plugins/beta/draw-ml/src/utils/snapHelpers.js +12 -12
  94. package/plugins/beta/draw-ml/src/utils/spatial.js +11 -11
  95. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
  96. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
  97. package/plugins/beta/frame/src/Frame.jsx +9 -9
  98. package/plugins/beta/frame/src/FrameInit.jsx +4 -4
  99. package/plugins/beta/frame/src/api/addFrame.js +1 -1
  100. package/plugins/beta/frame/src/api/editFeature.js +1 -1
  101. package/plugins/beta/frame/src/config.js +1 -1
  102. package/plugins/beta/frame/src/manifest.js +3 -3
  103. package/plugins/beta/frame/src/reducer.js +1 -1
  104. package/plugins/beta/frame/src/utils.js +1 -1
  105. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  106. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  107. package/plugins/beta/map-styles/src/MapStyles.jsx +18 -18
  108. package/plugins/beta/map-styles/src/manifest.js +1 -1
  109. package/plugins/beta/scale-bar/dist/css/index.css +1 -1
  110. package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -1
  111. package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -1
  112. package/plugins/beta/scale-bar/src/ScaleBar.jsx +5 -5
  113. package/plugins/beta/scale-bar/src/index.test.js +3 -3
  114. package/plugins/beta/scale-bar/src/manifest.js +3 -3
  115. package/plugins/beta/scale-bar/src/scaleBar.scss +2 -1
  116. package/plugins/beta/use-location/src/UseLocation.jsx +1 -1
  117. package/plugins/beta/use-location/src/defaults.js +1 -1
  118. package/plugins/beta/use-location/src/events.js +3 -3
  119. package/plugins/interact/dist/css/index.css +1 -1
  120. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  121. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  122. package/plugins/interact/src/InteractInit.jsx +1 -2
  123. package/plugins/interact/src/api/enable.js +8 -5
  124. package/plugins/interact/src/api/enable.test.js +2 -2
  125. package/plugins/interact/src/api/selectFeature.js +4 -4
  126. package/plugins/interact/src/api/unselectFeature.js +5 -5
  127. package/plugins/interact/src/defaults.js +0 -1
  128. package/plugins/interact/src/events.test.js +15 -15
  129. package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
  130. package/plugins/interact/src/hooks/useInteractionHandlers.js +2 -2
  131. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +5 -5
  132. package/plugins/interact/src/interact.scss +0 -7
  133. package/plugins/interact/src/manifest.js +15 -19
  134. package/plugins/interact/src/manifest.test.js +6 -5
  135. package/plugins/interact/src/reducer.js +3 -3
  136. package/plugins/interact/src/reducer.test.js +0 -1
  137. package/plugins/interact/src/utils/spatial.js +10 -10
  138. package/plugins/interact/src/utils/spatial.test.js +14 -14
  139. package/plugins/search/dist/css/index.css +1 -1
  140. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  141. package/plugins/search/dist/esm/index.js +1 -1
  142. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  143. package/plugins/search/dist/umd/index.js +1 -1
  144. package/plugins/search/src/Search.jsx +7 -6
  145. package/plugins/search/src/Search.test.jsx +23 -23
  146. package/plugins/search/src/components/CloseButton/CloseButton.jsx +15 -15
  147. package/plugins/search/src/components/CloseButton/CloseButton.test.jsx +2 -2
  148. package/plugins/search/src/components/Form/Form.jsx +14 -14
  149. package/plugins/search/src/components/Form/Form.module.scss +2 -1
  150. package/plugins/search/src/components/Form/Form.test.jsx +11 -11
  151. package/plugins/search/src/components/OpenButton/OpenButton.jsx +16 -15
  152. package/plugins/search/src/components/OpenButton/OpenButton.test.jsx +6 -2
  153. package/plugins/search/src/components/SubmitButton/SubmitButton.jsx +15 -15
  154. package/plugins/search/src/components/Suggestions/Suggestions.jsx +6 -6
  155. package/plugins/search/src/components/Suggestions/Suggestions.test.jsx +4 -4
  156. package/plugins/search/src/datasets.js +12 -13
  157. package/plugins/search/src/datasets.test.js +1 -1
  158. package/plugins/search/src/defaults.js +1 -1
  159. package/plugins/search/src/events/fetchSuggestions.js +3 -3
  160. package/plugins/search/src/events/fetchSuggestions.test.js +1 -1
  161. package/plugins/search/src/events/formHandlers.js +3 -3
  162. package/plugins/search/src/events/formHandlers.test.js +1 -1
  163. package/plugins/search/src/events/index.js +2 -2
  164. package/plugins/search/src/events/index.test.js +2 -2
  165. package/plugins/search/src/events/inputHandlers.js +4 -4
  166. package/plugins/search/src/events/inputHandlers.test.js +1 -1
  167. package/plugins/search/src/events/suggestionHandlers.js +2 -2
  168. package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
  169. package/plugins/search/src/index.js +2 -1
  170. package/plugins/search/src/index.test.js +3 -3
  171. package/plugins/search/src/manifest.js +6 -4
  172. package/plugins/search/src/reducer.js +1 -2
  173. package/plugins/search/src/reducer.test.js +2 -2
  174. package/plugins/search/src/search.scss +10 -3
  175. package/plugins/search/src/utils/parseOsNamesResults.js +1 -2
  176. package/plugins/search/src/utils/parseOsNamesResults.test.js +2 -2
  177. package/plugins/search/src/utils/updateMap.js +1 -1
  178. package/plugins/search/src/utils/updateMap.test.js +5 -5
  179. package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
  180. package/providers/beta/esri/src/esriProvider.js +5 -5
  181. package/providers/beta/esri/src/utils/coords.js +1 -1
  182. package/providers/beta/esri/src/utils/esriFixes.js +1 -1
  183. package/providers/beta/esri/src/utils/query.js +4 -4
  184. package/providers/beta/esri/src/utils/spatial.js +1 -2
  185. package/providers/beta/esri/src/utils/spatial.test.js +4 -1
  186. package/providers/beta/open-names/src/utils/mapToLocationModel.test.js +1 -1
  187. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  188. package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
  189. package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
  190. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  191. package/providers/maplibre/src/appEvents.test.js +1 -1
  192. package/providers/maplibre/src/index.js +1 -1
  193. package/providers/maplibre/src/index.test.js +3 -5
  194. package/providers/maplibre/src/mapEvents.test.js +15 -5
  195. package/providers/maplibre/src/maplibreProvider.test.js +6 -2
  196. package/providers/maplibre/src/utils/calculateLinearTextSize.js +4 -4
  197. package/providers/maplibre/src/utils/calculateLinearTextSize.test.js +3 -3
  198. package/providers/maplibre/src/utils/detectWebgl.test.js +1 -1
  199. package/providers/maplibre/src/utils/highlightFeatures.js +3 -2
  200. package/providers/maplibre/src/utils/highlightFeatures.test.js +13 -6
  201. package/providers/maplibre/src/utils/labels.js +19 -20
  202. package/providers/maplibre/src/utils/labels.test.js +15 -13
  203. package/providers/maplibre/src/utils/maplibreFixes.test.js +1 -1
  204. package/providers/maplibre/src/utils/queryFeatures.js +6 -6
  205. package/providers/maplibre/src/utils/queryFeatures.test.js +13 -13
  206. package/providers/maplibre/src/utils/spatial.js +0 -1
  207. package/providers/maplibre/src/utils/spatial.test.js +26 -27
  208. package/src/App/components/Actions/Actions.jsx +2 -2
  209. package/src/App/components/Actions/Actions.module.scss +0 -7
  210. package/src/App/components/Actions/Actions.test.jsx +1 -1
  211. package/src/App/components/Icon/Icon.jsx +3 -2
  212. package/src/App/components/Icon/Icon.module.scss +4 -0
  213. package/src/App/components/Icon/Icon.test.jsx +43 -4
  214. package/src/App/components/MapButton/MapButton.jsx +42 -17
  215. package/src/App/components/MapButton/MapButton.module.scss +4 -13
  216. package/src/App/components/MapButton/MapButton.test.jsx +27 -3
  217. package/src/App/components/PopupMenu/PopupMenu.jsx +51 -274
  218. package/src/App/components/PopupMenu/PopupMenu.module.scss +14 -7
  219. package/src/App/components/PopupMenu/PopupMenu.test.jsx +70 -1
  220. package/src/App/components/PopupMenu/usePopupMenu.js +258 -0
  221. package/src/App/hooks/useButtonStateEvaluator.js +12 -2
  222. package/src/App/hooks/useButtonStateEvaluator.test.js +38 -4
  223. package/src/App/hooks/useInterfaceAPI.js +6 -0
  224. package/src/App/hooks/useLayoutMeasurements.js +84 -18
  225. package/src/App/hooks/useLayoutMeasurements.test.js +124 -17
  226. package/src/App/layout/Layout.jsx +12 -7
  227. package/src/App/layout/Layout.test.jsx +2 -2
  228. package/src/App/layout/layout.module.scss +67 -29
  229. package/src/App/registry/pluginRegistry.js +17 -0
  230. package/src/App/registry/pluginRegistry.test.js +33 -0
  231. package/src/App/renderer/HtmlElementHost.jsx +2 -1
  232. package/src/App/renderer/HtmlElementHost.test.jsx +7 -7
  233. package/src/App/renderer/mapButtons.js +3 -2
  234. package/src/App/renderer/mapPanels.test.js +2 -2
  235. package/src/App/renderer/slotHelpers.js +2 -2
  236. package/src/App/renderer/slotHelpers.test.js +5 -5
  237. package/src/App/renderer/slots.js +9 -5
  238. package/src/App/store/AppProvider.jsx +3 -1
  239. package/src/App/store/AppProvider.test.jsx +1 -1
  240. package/src/App/store/ServiceProvider.jsx +3 -1
  241. package/src/App/store/appActionsMap.js +16 -0
  242. package/src/App/store/appActionsMap.test.js +27 -0
  243. package/src/App/store/appDispatchMiddleware.js +33 -1
  244. package/src/App/store/appDispatchMiddleware.test.js +250 -222
  245. package/src/App/store/appReducer.js +2 -0
  246. package/src/InteractiveMap/InteractiveMap.js +4 -0
  247. package/src/config/appConfig.js +7 -4
  248. package/src/config/events.js +28 -0
  249. package/src/scss/main.scss +1 -0
  250. package/src/scss/settings/_dimensions.scss +0 -1
  251. package/src/services/logger.js +6 -0
  252. package/src/services/logger.test.js +32 -0
  253. package/src/utils/getSafeZoneInset.js +9 -7
  254. package/src/utils/getSafeZoneInset.test.js +10 -10
  255. package/webpack.dev.mjs +23 -19
  256. package/docs/govuk-prototype.md +0 -23
  257. package/docs/index.md +0 -19
  258. package/plugins/beta/datasets/src/api/hideDataset.js +0 -14
  259. package/plugins/beta/datasets/src/api/hideFeatures.js +0 -41
  260. package/plugins/beta/datasets/src/api/showDataset.js +0 -14
  261. package/plugins/beta/datasets/src/api/showFeatures.js +0 -44
  262. package/plugins/beta/datasets/src/handleSetMapStyle.js +0 -54
  263. package/plugins/beta/datasets/src/mapLayers.js +0 -165
@@ -24,18 +24,21 @@ const refs = (o = {}) => ({
24
24
  topRef: { current: o.top === null ? null : el({ offsetTop: 10, ...o.top }) },
25
25
  topLeftColRef: { current: el({ offsetHeight: 50, offsetWidth: 200, ...o.topLeftCol }) },
26
26
  topRightColRef: { current: el({ offsetHeight: 40, offsetWidth: 180, ...o.topRightCol }) },
27
- footerRef: { current: o.footer === null ? null : el({ offsetTop: 400, ...o.footer }) },
28
- actionsRef: { current: el({ offsetTop: 450, ...o.actions }) },
27
+ bottomRef: { current: o.bottom === null ? null : el({ offsetTop: 400, ...o.bottom }) },
28
+ bottomRightRef: { current: el({ offsetTop: 400, ...o.bottomRight }) },
29
29
  leftTopRef: { current: el({ offsetHeight: 0, ...o.leftTop }) },
30
30
  leftBottomRef: { current: el({ offsetHeight: 0, ...o.leftBottom }) },
31
31
  rightTopRef: { current: el({ offsetHeight: 0, ...o.rightTop }) },
32
- rightBottomRef: { current: el({ offsetHeight: 0, ...o.rightBottom }) }
32
+ rightBottomRef: { current: el({ offsetHeight: 0, ...o.rightBottom }) },
33
+ attributionsRef: { current: el({ offsetHeight: 16, ...o.attributions }) },
34
+ drawerRef: { current: el(o.drawer) },
35
+ actionsRef: { current: el({ offsetTop: 450, ...o.actions }) }
33
36
  })
34
37
 
35
38
  const setup = (o = {}) => {
36
39
  const dispatch = jest.fn()
37
40
  const layoutRefs = refs(o.refs)
38
- useApp.mockReturnValue({ dispatch, breakpoint: 'desktop', layoutRefs, ...o.app })
41
+ useApp.mockReturnValue({ dispatch, breakpoint: 'desktop', layoutRefs, arePluginsEvaluated: true, ...o.app })
39
42
  useMap.mockReturnValue({ mapSize: { width: 800, height: 600 }, isMapReady: true, ...o.map })
40
43
  getSafeZoneInset.mockReturnValue({ top: 0, right: 0, bottom: 0, left: 0 })
41
44
  return { dispatch, layoutRefs }
@@ -56,7 +59,7 @@ describe('useLayoutMeasurements', () => {
56
59
  })
57
60
 
58
61
  test('early return when required refs are null', () => {
59
- const { layoutRefs } = setup({ refs: { main: null, top: null, footer: null } })
62
+ const { layoutRefs } = setup({ refs: { main: null, top: null, bottom: null } })
60
63
  renderHook(() => useLayoutMeasurements())
61
64
  expect(layoutRefs.appContainerRef.current.style.setProperty).not.toHaveBeenCalled()
62
65
  })
@@ -71,7 +74,7 @@ describe('useLayoutMeasurements', () => {
71
74
 
72
75
  test.each([
73
76
  ['right-offset-top', { topRightCol: { offsetHeight: 80 }, top: { offsetTop: 15 } }, '95px'],
74
- ['right-offset-bottom', { main: { offsetHeight: 600 }, footer: { offsetTop: 500 } }, '108px'],
77
+ ['right-offset-bottom', { main: { offsetHeight: 600 }, bottom: { offsetTop: 500 } }, '116px'],
75
78
  // leftColumnHeight = 400 - (50+10) - 8 = 332; rightColumnHeight = 400 - (40+10) - 8 = 342
76
79
  ['left-top-max-height', {}, '332px'],
77
80
  ['right-top-max-height', {}, '342px']
@@ -107,6 +110,26 @@ describe('useLayoutMeasurements', () => {
107
110
  expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--top-col-width', expected)
108
111
  })
109
112
 
113
+ test('uses 0 when bottomRightRef current is null', () => {
114
+ const { layoutRefs } = setup()
115
+ layoutRefs.bottomRightRef.current = null
116
+ renderHook(() => useLayoutMeasurements())
117
+ expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--right-offset-bottom', '116px')
118
+ })
119
+
120
+ test('uses bottomRight height when bottomRightHeight > 0', () => {
121
+ const { layoutRefs } = setup({
122
+ refs: {
123
+ bottomRight: { offsetHeight: 20 } // 👈 triggers TRUE branch
124
+ }
125
+ })
126
+ renderHook(() => useLayoutMeasurements())
127
+ // bottomContainerPad = 500 - 400 - 0 = 100
128
+ // expected = 100 + (20 + 8) = 128
129
+ expect(layoutRefs.appContainerRef.current.style.setProperty)
130
+ .toHaveBeenCalledWith('--right-offset-bottom', '128px')
131
+ })
132
+
110
133
  test('uses 0 when sub-slot refs have null current', () => {
111
134
  const { layoutRefs } = setup()
112
135
  layoutRefs.leftTopRef.current = null
@@ -119,31 +142,100 @@ describe('useLayoutMeasurements', () => {
119
142
  expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--right-bottom-panel-max-height', '342px')
120
143
  })
121
144
 
122
- test('dispatches safe zone inset', () => {
123
- const { dispatch, layoutRefs } = setup()
145
+ test('dispatches safe zone inset on desktop (post-batch RAF read only)', () => {
146
+ const { dispatch, layoutRefs } = setup({ app: { breakpoint: 'desktop' } })
124
147
  getSafeZoneInset.mockReturnValue({ top: 10, right: 5, bottom: 15, left: 5 })
125
148
  renderHook(() => useLayoutMeasurements())
126
149
  expect(getSafeZoneInset).toHaveBeenCalledWith(layoutRefs)
127
150
  expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SAFE_ZONE_INSET', payload: { safeZoneInset: { top: 10, right: 5, bottom: 15, left: 5 } } })
128
151
  })
129
152
 
130
- test('recalculates on dependency changes', () => {
153
+ test('dispatches safe zone inset on mobile', () => {
154
+ const { dispatch } = setup({ app: { breakpoint: 'mobile' } })
155
+ getSafeZoneInset.mockReturnValue({ top: 10, right: 5, bottom: 40, left: 5 })
156
+ renderHook(() => useLayoutMeasurements())
157
+ expect(dispatch).toHaveBeenCalledWith({
158
+ type: 'SET_SAFE_ZONE_INSET',
159
+ payload: { safeZoneInset: { top: 10, right: 5, bottom: 40, left: 5 } }
160
+ })
161
+ })
162
+
163
+ test('does not dispatch SET_SAFE_ZONE_INSET when getSafeZoneInset returns undefined', () => {
164
+ const { dispatch } = setup()
165
+ getSafeZoneInset.mockReturnValue(undefined)
166
+ renderHook(() => useLayoutMeasurements())
167
+ expect(dispatch).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'SET_SAFE_ZONE_INSET' }))
168
+ })
169
+
170
+ test('does not dispatch safe zone when arePluginsEvaluated is false', () => {
171
+ const { dispatch } = setup({ app: { arePluginsEvaluated: false } })
172
+ renderHook(() => useLayoutMeasurements())
173
+ expect(dispatch).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'SET_SAFE_ZONE_INSET' }))
174
+ expect(layoutRefs => layoutRefs).toBeDefined() // no layout calculation
175
+ })
176
+
177
+ test('re-dispatches safe zone when arePluginsEvaluated becomes true', () => {
178
+ setup({ app: { arePluginsEvaluated: false } })
179
+ const { rerender } = renderHook(() => useLayoutMeasurements())
180
+ const { dispatch } = setup({ app: { arePluginsEvaluated: true } })
181
+ getSafeZoneInset.mockReturnValue({ top: 5, right: 5, bottom: 60, left: 5 })
182
+ rerender()
183
+ expect(dispatch).toHaveBeenCalledWith({
184
+ type: 'SET_SAFE_ZONE_INSET',
185
+ payload: { safeZoneInset: { top: 5, right: 5, bottom: 60, left: 5 } }
186
+ })
187
+ })
188
+
189
+ test('dispatches CLEAR_PLUGINS_EVALUATED when breakpoint changes', () => {
190
+ setup()
191
+ const { rerender } = renderHook(() => useLayoutMeasurements())
192
+ const { dispatch } = setup({ app: { breakpoint: 'mobile' } })
193
+ dispatch.mockClear()
194
+ rerender()
195
+ expect(dispatch).toHaveBeenCalledWith({ type: 'CLEAR_PLUGINS_EVALUATED' })
196
+ })
197
+
198
+ test('dispatches CLEAR_PLUGINS_EVALUATED when isMapReady changes', () => {
131
199
  setup()
132
200
  const { rerender } = renderHook(() => useLayoutMeasurements())
133
- ;[{ app: { breakpoint: 'mobile' } }, { map: { mapSize: { width: 1000, height: 800 } } }, { map: { isMapReady: false } }]
134
- .forEach(change => {
135
- const { layoutRefs } = setup(change)
136
- layoutRefs.appContainerRef.current.style.setProperty.mockClear()
137
- rerender()
138
- expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalled()
139
- })
201
+ const { dispatch } = setup({ map: { isMapReady: false } })
202
+ dispatch.mockClear()
203
+ rerender()
204
+ expect(dispatch).toHaveBeenCalledWith({ type: 'CLEAR_PLUGINS_EVALUATED' })
205
+ })
206
+
207
+ test('dispatches CLEAR_PLUGINS_EVALUATED when isFullscreen changes', () => {
208
+ setup({ app: { isFullscreen: false } })
209
+ const { rerender } = renderHook(() => useLayoutMeasurements())
210
+ const { dispatch } = setup({ app: { isFullscreen: true } })
211
+ dispatch.mockClear()
212
+ rerender()
213
+ expect(dispatch).toHaveBeenCalledWith({ type: 'CLEAR_PLUGINS_EVALUATED' })
214
+ })
215
+
216
+ test('dispatches CLEAR_PLUGINS_EVALUATED when appVisible changes', () => {
217
+ setup({ app: { appVisible: false } })
218
+ const { rerender } = renderHook(() => useLayoutMeasurements())
219
+ const { dispatch } = setup({ app: { appVisible: true } })
220
+ dispatch.mockClear()
221
+ rerender()
222
+ expect(dispatch).toHaveBeenCalledWith({ type: 'CLEAR_PLUGINS_EVALUATED' })
223
+ })
224
+
225
+ test('recalculates layout when arePluginsEvaluated becomes true', () => {
226
+ setup({ app: { arePluginsEvaluated: false } })
227
+ const { rerender } = renderHook(() => useLayoutMeasurements())
228
+ const { layoutRefs } = setup({ app: { arePluginsEvaluated: true } })
229
+ layoutRefs.appContainerRef.current.style.setProperty.mockClear()
230
+ rerender()
231
+ expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalled()
140
232
  })
141
233
 
142
234
  test('sets up resize observer', () => {
143
235
  const { layoutRefs } = setup()
144
236
  renderHook(() => useLayoutMeasurements())
145
237
  expect(useResizeObserver).toHaveBeenCalledWith(
146
- [layoutRefs.bannerRef, layoutRefs.mainRef, layoutRefs.topRef, layoutRefs.topLeftColRef, layoutRefs.topRightColRef, layoutRefs.actionsRef, layoutRefs.footerRef, layoutRefs.leftTopRef, layoutRefs.leftBottomRef, layoutRefs.rightTopRef, layoutRefs.rightBottomRef],
238
+ [layoutRefs.bannerRef, layoutRefs.mainRef, layoutRefs.topRef, layoutRefs.topLeftColRef, layoutRefs.topRightColRef, layoutRefs.actionsRef, layoutRefs.bottomRef, layoutRefs.bottomRightRef, layoutRefs.leftTopRef, layoutRefs.leftBottomRef, layoutRefs.rightTopRef, layoutRefs.rightBottomRef, layoutRefs.drawerRef],
147
239
  expect.any(Function)
148
240
  )
149
241
  layoutRefs.appContainerRef.current.style.setProperty.mockClear()
@@ -151,4 +243,19 @@ describe('useLayoutMeasurements', () => {
151
243
  expect(rafSpy).toHaveBeenCalled()
152
244
  expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalled()
153
245
  })
246
+
247
+ test('resize observer does not dispatch safe zone (safe zone is Effect 3 only)', () => {
248
+ const { dispatch } = setup()
249
+ renderHook(() => useLayoutMeasurements())
250
+ dispatch.mockClear()
251
+ useResizeObserver.mock.calls[0][1]()
252
+ expect(dispatch).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'SET_SAFE_ZONE_INSET' }))
253
+ })
254
+
255
+ test('resize observer handles null mainRef without throwing', () => {
256
+ const { layoutRefs } = setup()
257
+ renderHook(() => useLayoutMeasurements())
258
+ layoutRefs.mainRef.current = null
259
+ expect(() => useResizeObserver.mock.calls[0][1]()).not.toThrow()
260
+ })
154
261
  })
@@ -82,19 +82,24 @@ export const Layout = () => {
82
82
  <SlotRenderer slot={layoutSlots.RIGHT_BOTTOM} />
83
83
  </div>
84
84
  </div>
85
- <div className='im-o-app__footer' ref={layoutRefs.footerRef}>
86
- <div className='im-o-app__footer-col'>
85
+ <div className='im-o-app__bottom' ref={layoutRefs.bottomRef}>
86
+ <div className='im-o-app__bottom-col'>
87
87
  <Logo />
88
+ <div className='im-o-app__bottom-left'>
89
+ <SlotRenderer slot={layoutSlots.BOTTOM_LEFT} />
90
+ </div>
88
91
  </div>
89
- <div className='im-o-app__footer-col'>
90
- <SlotRenderer slot={layoutSlots.FOOTER_RIGHT} />
91
- <div className='im-o-app__attributions'>
92
+ <div className='im-o-app__bottom-col'>
93
+ <div className='im-o-app__bottom-right' ref={layoutRefs.bottomRightRef}>
94
+ <SlotRenderer slot={layoutSlots.BOTTOM_RIGHT} />
95
+ </div>
96
+ <div className='im-o-app__attributions' ref={layoutRefs.attributionsRef}>
92
97
  <Attributions />
93
98
  </div>
94
99
  </div>
95
100
  </div>
96
- <div className='im-o-app__bottom' ref={layoutRefs.bottomRef}>
97
- <SlotRenderer slot={layoutSlots.BOTTOM} />
101
+ <div className='im-o-app__drawer' ref={layoutRefs.drawerRef}>
102
+ <SlotRenderer slot={layoutSlots.DRAWER} />
98
103
  </div>
99
104
  <div className='im-o-app__actions' ref={layoutRefs.actionsRef}>
100
105
  <SlotRenderer slot={layoutSlots.ACTIONS} />
@@ -38,7 +38,7 @@ describe('Layout', () => {
38
38
  topLeftColRef: React.createRef(),
39
39
  topRightColRef: React.createRef(),
40
40
  rightRef: React.createRef(),
41
- footerRef: React.createRef(),
41
+ bottomRef: React.createRef(),
42
42
  actionsRef: React.createRef()
43
43
  }
44
44
 
@@ -86,7 +86,7 @@ describe('Layout', () => {
86
86
  expect(screen.getByTestId('slot-side')).toBeInTheDocument()
87
87
  expect(screen.getByTestId('slot-banner')).toBeInTheDocument()
88
88
  expect(screen.getByTestId('slot-top-left')).toBeInTheDocument()
89
- expect(screen.getByTestId('slot-footer-right')).toBeInTheDocument()
89
+ expect(screen.getByTestId('slot-bottom-right')).toBeInTheDocument()
90
90
  expect(screen.getByTestId('slot-modal')).toBeInTheDocument()
91
91
  })
92
92
 
@@ -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
+ }
@@ -1,9 +1,23 @@
1
1
  // src/core/registry/pluginRegistry.js
2
2
  import { registerIcon } from './iconRegistry.js'
3
3
  import { registerKeyboardShortcut } from './keyboardShortcutRegistry.js'
4
+ import { allowedSlots } from '../renderer/slots.js'
5
+ import { logger } from '../../services/logger.js'
4
6
 
5
7
  const asArray = (value) => Array.isArray(value) ? value : [value]
6
8
 
9
+ const BREAKPOINTS = ['mobile', 'tablet', 'desktop']
10
+
11
+ function validateSlots (item, type) {
12
+ const allowed = allowedSlots[type]
13
+ BREAKPOINTS.forEach(bp => {
14
+ const slot = item[bp]?.slot
15
+ if (slot && !allowed.includes(slot) && !(type === 'panel' && slot.endsWith('-button'))) {
16
+ logger.warn(`${type} "${item.id}" has invalid slot "${slot}" at breakpoint "${bp}". Allowed slots: ${allowed.join(', ')}.`)
17
+ }
18
+ })
19
+ }
20
+
7
21
  export function createPluginRegistry ({ registerButton, registerPanel, registerControl }) {
8
22
  const registeredPlugins = []
9
23
 
@@ -18,6 +32,7 @@ export function createPluginRegistry ({ registerButton, registerPanel, registerC
18
32
 
19
33
  if (manifest.buttons) {
20
34
  asArray(manifest.buttons).forEach(button => {
35
+ validateSlots(button, 'button')
21
36
  registerButton({ [button.id]: { ...pluginConfig, ...button } })
22
37
  // Flat button registry including any menu items (isMenuItem prevents slot rendering)
23
38
  button?.menuItems?.forEach(menuItem => {
@@ -28,12 +43,14 @@ export function createPluginRegistry ({ registerButton, registerPanel, registerC
28
43
 
29
44
  if (manifest.panels) {
30
45
  asArray(manifest.panels).forEach(panel => {
46
+ validateSlots(panel, 'panel')
31
47
  registerPanel({ [panel.id]: { ...pluginConfig, ...panel } })
32
48
  })
33
49
  }
34
50
 
35
51
  if (manifest.controls) {
36
52
  asArray(manifest.controls).forEach(control => {
53
+ validateSlots(control, 'control')
37
54
  registerControl({ [control.id]: { ...pluginConfig, ...control } })
38
55
  })
39
56
  }
@@ -139,6 +139,39 @@ describe('pluginRegistry', () => {
139
139
  expect(pluginRegistry.registeredPlugins).toEqual([pluginA, pluginB])
140
140
  })
141
141
 
142
+ describe('slot validation', () => {
143
+ const INVALID_SLOT = 'invalid-slot'
144
+
145
+ beforeEach(() => jest.spyOn(console, 'warn').mockImplementation(() => {}))
146
+ afterEach(() => jest.restoreAllMocks())
147
+
148
+ it('warns when a manifest item has an invalid slot', () => {
149
+ const plugin = {
150
+ id: 'bad-plugin',
151
+ config: {},
152
+ manifest: {
153
+ buttons: [{ id: 'btn1', desktop: { slot: INVALID_SLOT } }],
154
+ panels: [{ id: 'panel1', desktop: { slot: INVALID_SLOT } }],
155
+ controls: [{ id: 'ctrl1', desktop: { slot: INVALID_SLOT } }]
156
+ }
157
+ }
158
+ pluginRegistry.registerPlugin(plugin)
159
+ expect(console.warn).toHaveBeenCalledWith('[interactive-map]', expect.stringContaining(INVALID_SLOT))
160
+ })
161
+
162
+ it('does not warn for a panel with a button-adjacent slot', () => {
163
+ const plugin = {
164
+ id: 'adj-plugin',
165
+ config: {},
166
+ manifest: {
167
+ panels: [{ id: 'panel1', desktop: { slot: 'left-top-button' } }]
168
+ }
169
+ }
170
+ pluginRegistry.registerPlugin(plugin)
171
+ expect(console.warn).not.toHaveBeenCalled()
172
+ })
173
+ })
174
+
142
175
  it('clears all registered plugins', () => {
143
176
  const pluginA = { id: 'A', config: {}, manifest: {} }
144
177
  const pluginB = { id: 'B', config: {}, manifest: {} }
@@ -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,6 +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 '../../services/logger.js'
4
5
 
5
6
  function getMatchingButtons ({ appState, buttonConfig, slot, evaluateProp }) {
6
7
  const { breakpoint, mode } = appState
@@ -163,7 +164,7 @@ function mapButtons ({ slot, appState, appConfig, evaluateProp }) {
163
164
 
164
165
  /* istanbul ignore next */
165
166
  if (process.env.NODE_ENV !== 'production' && typeof group === 'string') {
166
- console.warn(`[interactive-map] Button "${buttonId}": group should be an object { name, label?, order? } — string groups are deprecated.`)
167
+ logger.warn(`Button "${buttonId}": group should be an object { name, label?, order? } — string groups are deprecated.`)
167
168
  }
168
169
 
169
170
  const name = resolveGroupName(group)
@@ -174,7 +175,7 @@ function mapButtons ({ slot, appState, appConfig, evaluateProp }) {
174
175
  const existing = groupMap.get(name)
175
176
  /* istanbul ignore next */
176
177
  if (process.env.NODE_ENV !== 'production' && existing.order !== order) {
177
- console.warn(`[interactive-map] Group "${name}" has inconsistent order values (${existing.order} vs ${order}). Using the lower value.`)
178
+ logger.warn(`Group "${name}" has inconsistent order values (${existing.order} vs ${order}). Using the lower value.`)
178
179
  existing.order = Math.min(existing.order, order)
179
180
  }
180
181
  } else {
@@ -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', () => {