@defra/interactive-map 0.0.14-alpha → 0.0.16-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 (199) 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/dist/css/index.css +1 -1
  5. package/dist/esm/im-core.js +1 -1
  6. package/dist/umd/im-core.js +1 -1
  7. package/dist/umd/index.js +1 -1
  8. package/docs/api/slot-map.svg +1 -0
  9. package/docs/api/slots.md +89 -6
  10. package/docs/api.md +1 -1
  11. package/docs/architecture.md +3 -1
  12. package/docs/{demo.mdx → examples.mdx} +1 -1
  13. package/docs/getting-started.md +1 -3
  14. package/docs/index.mdx +42 -0
  15. package/docs/plugins/interact.md +176 -55
  16. package/docs/plugins/map-styles.md +64 -7
  17. package/docs/plugins/search.md +207 -63
  18. package/docs/plugins.md +7 -15
  19. package/docusaurus.config.cjs +34 -34
  20. package/jest.setup.js +1 -1
  21. package/package.json +5 -4
  22. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  23. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  24. package/plugins/beta/datasets/src/DatasetsInit.jsx +1 -1
  25. package/plugins/beta/datasets/src/api/addDataset.js +1 -1
  26. package/plugins/beta/datasets/src/api/hideDataset.js +1 -1
  27. package/plugins/beta/datasets/src/api/hideFeatures.js +1 -1
  28. package/plugins/beta/datasets/src/api/removeDataset.js +1 -1
  29. package/plugins/beta/datasets/src/api/showDataset.js +1 -1
  30. package/plugins/beta/datasets/src/api/showFeatures.js +1 -1
  31. package/plugins/beta/datasets/src/datasets.js +4 -4
  32. package/plugins/beta/datasets/src/defaults.js +1 -1
  33. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +5 -5
  34. package/plugins/beta/datasets/src/handleSetMapStyle.js +1 -1
  35. package/plugins/beta/datasets/src/manifest.js +7 -7
  36. package/plugins/beta/datasets/src/mapLayers.js +2 -3
  37. package/plugins/beta/datasets/src/panels/Key.jsx +31 -29
  38. package/plugins/beta/datasets/src/panels/Layers.jsx +8 -9
  39. package/plugins/beta/datasets/src/utils/bbox.js +4 -4
  40. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  41. package/plugins/beta/draw-es/src/DrawInit.jsx +16 -16
  42. package/plugins/beta/draw-es/src/api/addFeature.js +3 -3
  43. package/plugins/beta/draw-es/src/api/deleteFeature.js +3 -3
  44. package/plugins/beta/draw-es/src/api/editFeature.js +3 -3
  45. package/plugins/beta/draw-es/src/api/newPolygon.js +3 -3
  46. package/plugins/beta/draw-es/src/events.js +52 -20
  47. package/plugins/beta/draw-es/src/events.test.js +301 -0
  48. package/plugins/beta/draw-es/src/graphic.js +1 -1
  49. package/plugins/beta/draw-es/src/manifest.js +4 -4
  50. package/plugins/beta/draw-es/src/reducer.js +1 -1
  51. package/plugins/beta/draw-es/src/sketchViewModel.js +1 -1
  52. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  53. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  54. package/plugins/beta/draw-ml/src/DrawInit.jsx +49 -52
  55. package/plugins/beta/draw-ml/src/api/deleteFeature.js +1 -1
  56. package/plugins/beta/draw-ml/src/api/editFeature.js +8 -5
  57. package/plugins/beta/draw-ml/src/api/newLine.js +0 -1
  58. package/plugins/beta/draw-ml/src/api/newPolygon.js +0 -1
  59. package/plugins/beta/draw-ml/src/api/split.js +4 -4
  60. package/plugins/beta/draw-ml/src/defaults.js +1 -1
  61. package/plugins/beta/draw-ml/src/events.js +8 -6
  62. package/plugins/beta/draw-ml/src/manifest.js +15 -15
  63. package/plugins/beta/draw-ml/src/mapboxDraw.js +1 -1
  64. package/plugins/beta/draw-ml/src/mapboxSnap.js +17 -18
  65. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +31 -31
  66. package/plugins/beta/draw-ml/src/modes/disabledMode.js +1 -1
  67. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +11 -11
  68. package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +7 -7
  69. package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +8 -8
  70. package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +7 -7
  71. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +32 -24
  72. package/plugins/beta/draw-ml/src/reducer.js +1 -1
  73. package/plugins/beta/draw-ml/src/undoStack.js +4 -4
  74. package/plugins/beta/draw-ml/src/utils/snapHelpers.js +12 -12
  75. package/plugins/beta/draw-ml/src/utils/spatial.js +11 -11
  76. package/plugins/beta/frame/src/Frame.jsx +4 -4
  77. package/plugins/beta/frame/src/FrameInit.jsx +4 -4
  78. package/plugins/beta/frame/src/api/addFrame.js +1 -1
  79. package/plugins/beta/frame/src/api/editFeature.js +1 -1
  80. package/plugins/beta/frame/src/config.js +1 -1
  81. package/plugins/beta/frame/src/manifest.js +3 -3
  82. package/plugins/beta/frame/src/reducer.js +1 -1
  83. package/plugins/beta/frame/src/utils.js +1 -1
  84. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  85. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  86. package/plugins/beta/map-styles/src/MapStyles.jsx +18 -18
  87. package/plugins/beta/map-styles/src/manifest.js +2 -2
  88. package/plugins/beta/scale-bar/src/ScaleBar.jsx +5 -5
  89. package/plugins/beta/use-location/src/UseLocation.jsx +1 -1
  90. package/plugins/beta/use-location/src/defaults.js +1 -1
  91. package/plugins/beta/use-location/src/events.js +3 -3
  92. package/plugins/interact/src/InteractInit.jsx +1 -2
  93. package/plugins/interact/src/api/enable.js +8 -5
  94. package/plugins/interact/src/api/enable.test.js +2 -2
  95. package/plugins/interact/src/api/selectFeature.js +4 -4
  96. package/plugins/interact/src/api/unselectFeature.js +5 -5
  97. package/plugins/interact/src/defaults.js +0 -1
  98. package/plugins/interact/src/events.test.js +15 -15
  99. package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
  100. package/plugins/interact/src/hooks/useInteractionHandlers.js +2 -2
  101. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +5 -5
  102. package/plugins/interact/src/manifest.js +2 -2
  103. package/plugins/interact/src/manifest.test.js +3 -4
  104. package/plugins/interact/src/reducer.js +3 -3
  105. package/plugins/interact/src/reducer.test.js +0 -1
  106. package/plugins/interact/src/utils/spatial.js +10 -10
  107. package/plugins/interact/src/utils/spatial.test.js +14 -14
  108. package/plugins/search/dist/css/index.css +1 -1
  109. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  110. package/plugins/search/dist/esm/index.js +1 -1
  111. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  112. package/plugins/search/dist/umd/index.js +1 -1
  113. package/plugins/search/src/Search.jsx +7 -6
  114. package/plugins/search/src/Search.test.jsx +23 -23
  115. package/plugins/search/src/components/CloseButton/CloseButton.jsx +15 -15
  116. package/plugins/search/src/components/CloseButton/CloseButton.test.jsx +2 -2
  117. package/plugins/search/src/components/Form/Form.jsx +14 -14
  118. package/plugins/search/src/components/Form/Form.test.jsx +11 -11
  119. package/plugins/search/src/components/OpenButton/OpenButton.jsx +16 -15
  120. package/plugins/search/src/components/OpenButton/OpenButton.test.jsx +6 -2
  121. package/plugins/search/src/components/SubmitButton/SubmitButton.jsx +15 -15
  122. package/plugins/search/src/components/Suggestions/Suggestions.jsx +6 -6
  123. package/plugins/search/src/components/Suggestions/Suggestions.test.jsx +4 -4
  124. package/plugins/search/src/datasets.js +12 -13
  125. package/plugins/search/src/datasets.test.js +1 -1
  126. package/plugins/search/src/defaults.js +1 -1
  127. package/plugins/search/src/events/fetchSuggestions.js +4 -4
  128. package/plugins/search/src/events/fetchSuggestions.test.js +5 -5
  129. package/plugins/search/src/events/formHandlers.js +3 -3
  130. package/plugins/search/src/events/formHandlers.test.js +1 -1
  131. package/plugins/search/src/events/index.js +2 -2
  132. package/plugins/search/src/events/index.test.js +2 -2
  133. package/plugins/search/src/events/inputHandlers.js +4 -4
  134. package/plugins/search/src/events/inputHandlers.test.js +1 -1
  135. package/plugins/search/src/events/suggestionHandlers.js +2 -2
  136. package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
  137. package/plugins/search/src/index.js +2 -1
  138. package/plugins/search/src/index.test.js +3 -3
  139. package/plugins/search/src/manifest.js +6 -4
  140. package/plugins/search/src/reducer.js +1 -2
  141. package/plugins/search/src/reducer.test.js +2 -2
  142. package/plugins/search/src/search.scss +18 -6
  143. package/plugins/search/src/utils/parseOsNamesResults.js +1 -2
  144. package/plugins/search/src/utils/parseOsNamesResults.test.js +2 -2
  145. package/plugins/search/src/utils/updateMap.js +1 -1
  146. package/plugins/search/src/utils/updateMap.test.js +5 -5
  147. package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
  148. package/providers/beta/esri/src/esriProvider.js +5 -5
  149. package/providers/beta/esri/src/utils/coords.js +1 -1
  150. package/providers/beta/esri/src/utils/esriFixes.js +1 -1
  151. package/providers/beta/esri/src/utils/query.js +4 -4
  152. package/providers/beta/esri/src/utils/spatial.js +1 -2
  153. package/providers/beta/esri/src/utils/spatial.test.js +4 -1
  154. package/providers/beta/open-names/src/utils/mapToLocationModel.test.js +1 -1
  155. package/providers/maplibre/src/appEvents.test.js +1 -1
  156. package/providers/maplibre/src/index.js +1 -1
  157. package/providers/maplibre/src/index.test.js +3 -5
  158. package/providers/maplibre/src/mapEvents.test.js +15 -5
  159. package/providers/maplibre/src/maplibreProvider.test.js +6 -2
  160. package/providers/maplibre/src/utils/calculateLinearTextSize.js +4 -4
  161. package/providers/maplibre/src/utils/calculateLinearTextSize.test.js +3 -3
  162. package/providers/maplibre/src/utils/detectWebgl.test.js +1 -1
  163. package/providers/maplibre/src/utils/highlightFeatures.js +2 -2
  164. package/providers/maplibre/src/utils/highlightFeatures.test.js +12 -6
  165. package/providers/maplibre/src/utils/labels.js +19 -20
  166. package/providers/maplibre/src/utils/labels.test.js +15 -13
  167. package/providers/maplibre/src/utils/maplibreFixes.test.js +1 -1
  168. package/providers/maplibre/src/utils/queryFeatures.js +6 -6
  169. package/providers/maplibre/src/utils/queryFeatures.test.js +13 -13
  170. package/providers/maplibre/src/utils/spatial.js +0 -1
  171. package/providers/maplibre/src/utils/spatial.test.js +26 -27
  172. package/src/App/components/Panel/Panel.module.scss +1 -0
  173. package/src/App/hooks/useLayoutMeasurements.js +1 -10
  174. package/src/App/hooks/useLayoutMeasurements.test.js +2 -5
  175. package/src/App/hooks/useVisibleGeometry.js +7 -13
  176. package/src/App/hooks/useVisibleGeometry.test.js +72 -47
  177. package/src/App/layout/Layout.jsx +0 -3
  178. package/src/App/layout/Layout.test.jsx +0 -1
  179. package/src/App/layout/layout.module.scss +11 -77
  180. package/src/App/registry/pluginRegistry.js +17 -0
  181. package/src/App/registry/pluginRegistry.test.js +33 -0
  182. package/src/App/renderer/HtmlElementHost.jsx +0 -1
  183. package/src/App/renderer/HtmlElementHost.test.jsx +20 -11
  184. package/src/App/renderer/mapButtons.js +3 -2
  185. package/src/App/renderer/mapPanels.test.js +3 -3
  186. package/src/App/renderer/slotHelpers.js +2 -2
  187. package/src/App/renderer/slotHelpers.test.js +3 -3
  188. package/src/App/renderer/slots.js +0 -3
  189. package/src/App/store/AppProvider.jsx +0 -1
  190. package/src/App/store/appDispatchMiddleware.js +33 -1
  191. package/src/App/store/appDispatchMiddleware.test.js +250 -222
  192. package/src/config/appConfig.js +4 -4
  193. package/src/utils/getSafeZoneInset.js +139 -42
  194. package/src/utils/getSafeZoneInset.test.js +298 -122
  195. package/src/utils/logger.js +6 -0
  196. package/src/utils/logger.test.js +32 -0
  197. package/webpack.dev.mjs +22 -18
  198. package/docs/govuk-prototype.md +0 -23
  199. package/docs/index.md +0 -19
@@ -153,6 +153,11 @@
153
153
  left: var(--primary-gap);
154
154
  top: var(--left-offset-top);
155
155
  bottom: var(--left-offset-bottom);
156
+ gap: var(--divider-gap);
157
+
158
+ & > *:empty {
159
+ display: none;
160
+ }
156
161
 
157
162
  @media (prefers-reduced-motion: no-preference) {
158
163
  transition: bottom 0.15s ease;
@@ -162,34 +167,21 @@
162
167
  .im-o-app__left-top {
163
168
  display: flex;
164
169
  flex-direction: column;
165
- flex: 0 0 auto;
166
- align-items: flex-end;
170
+ align-items: flex-start;
171
+ min-height: 0;
167
172
  position: relative;
168
173
  gap: var(--divider-gap);
169
-
170
- .im-c-panel {
171
- position: absolute;
172
- top: 0;
173
- left: 0;
174
- max-height: var(--left-top-panel-max-height);
175
- }
176
174
  }
177
175
 
178
176
  .im-o-app__left-bottom {
179
177
  display: flex;
180
178
  flex-direction: column;
181
- flex: 0 0 auto;
179
+ align-items: flex-start;
180
+ justify-content: flex-end;
181
+ min-height: 0;
182
182
  margin-top: auto;
183
- align-items: flex-end;
184
183
  position: relative;
185
184
  gap: var(--divider-gap);
186
-
187
- .im-c-panel {
188
- position: absolute;
189
- bottom: 0;
190
- left: 0;
191
- max-height: var(--left-bottom-panel-max-height);
192
- }
193
185
  }
194
186
 
195
187
  // ---------------------------------------------------
@@ -219,6 +211,7 @@
219
211
  right: var(--primary-gap);
220
212
  top: var(--right-offset-top);
221
213
  bottom: var(--right-offset-bottom);
214
+ gap: var(--divider-gap);
222
215
 
223
216
  @media (prefers-reduced-motion: no-preference) {
224
217
  transition: bottom 0.15s ease;
@@ -232,13 +225,6 @@
232
225
  align-items: flex-end;
233
226
  position: relative;
234
227
  gap: var(--divider-gap);
235
-
236
- .im-c-panel {
237
- position: absolute;
238
- top: 0;
239
- right: 0;
240
- max-height: var(--right-top-panel-max-height);
241
- }
242
228
  }
243
229
 
244
230
  .im-o-app__right-bottom {
@@ -249,57 +235,6 @@
249
235
  align-items: flex-end;
250
236
  position: relative;
251
237
  gap: var(--divider-gap);
252
-
253
- .im-c-panel {
254
- position: absolute;
255
- bottom: 0;
256
- right: 0;
257
- max-height: var(--right-bottom-panel-max-height);
258
- }
259
- }
260
-
261
- // When both left sub-slots have panels, split the column proportionally
262
- // so panels don't overlap. flex: 1 1 auto gives each sub-slot a share of the
263
- // column; max-height: 100% caps the panel at its sub-slot height (not forced).
264
- .im-o-app__left:has(.im-o-app__left-top > .im-c-panel:not([style*="display: none"])):has(.im-o-app__left-bottom > .im-c-panel:not([style*="display: none"])) {
265
- gap: var(--divider-gap);
266
-
267
- .im-o-app__left-top,
268
- .im-o-app__left-bottom {
269
- flex: 1 1 auto;
270
- min-height: 0;
271
- }
272
-
273
- .im-o-app__left-bottom {
274
- margin-top: 0;
275
- justify-content: flex-end;
276
- }
277
-
278
- .im-o-app__left-top .im-c-panel,
279
- .im-o-app__left-bottom .im-c-panel {
280
- max-height: 100%;
281
- }
282
- }
283
-
284
- // Same for right column
285
- .im-o-app__right:has(.im-o-app__right-top > .im-c-panel:not([style*="display: none"])):has(.im-o-app__right-bottom > .im-c-panel:not([style*="display: none"])) {
286
- gap: var(--divider-gap);
287
-
288
- .im-o-app__right-top,
289
- .im-o-app__right-bottom {
290
- flex: 1 1 auto;
291
- min-height: 0;
292
- }
293
-
294
- .im-o-app__right-bottom {
295
- margin-top: 0;
296
- justify-content: flex-end;
297
- }
298
-
299
- .im-o-app__right-top .im-c-panel,
300
- .im-o-app__right-bottom .im-c-panel {
301
- max-height: 100%;
302
- }
303
238
  }
304
239
 
305
240
  // ---------------------------------------------------
@@ -517,7 +452,6 @@
517
452
  width: 100%;
518
453
  left: 0;
519
454
  bottom: calc(var(--primary-gap) * 2);
520
- padding-left: var(--offset-left);
521
455
 
522
456
  .im-c-panel {
523
457
  max-width: var(--action-bar-max-width);
@@ -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 '../../utils/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: {} }
@@ -14,7 +14,6 @@ export const getSlotRef = (slot, layoutRefs) => {
14
14
  banner: layoutRefs.bannerRef,
15
15
  'top-left': layoutRefs.topLeftColRef,
16
16
  'top-right': layoutRefs.topRightColRef,
17
- inset: layoutRefs.insetRef,
18
17
  'left-top': layoutRefs.leftTopRef,
19
18
  'left-bottom': layoutRefs.leftBottomRef,
20
19
  middle: layoutRefs.middleRef,
@@ -14,8 +14,8 @@ jest.mock('../components/Panel/Panel.jsx', () => ({
14
14
  }))
15
15
  jest.mock('./slots.js', () => ({
16
16
  allowedSlots: {
17
- panel: ['inset', 'side', 'modal', 'bottom'],
18
- control: ['inset', 'banner', 'bottom', 'actions']
17
+ panel: ['left-top', 'side', 'modal', 'bottom'],
18
+ control: ['left-top', 'banner', 'bottom', 'actions']
19
19
  }
20
20
  }))
21
21
 
@@ -25,7 +25,7 @@ jest.mock('./slots.js', () => ({
25
25
  */
26
26
  const SlotHarness = ({ layoutRefs, children }) => (
27
27
  <div>
28
- <div ref={layoutRefs.insetRef} data-slot='inset' />
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
31
  <div ref={layoutRefs.bottomRef} data-slot='bottom' />
@@ -45,7 +45,7 @@ describe('HtmlElementHost', () => {
45
45
  bannerRef: { current: null },
46
46
  topLeftColRef: { current: null },
47
47
  topRightColRef: { current: null },
48
- insetRef: { current: null },
48
+ leftTopRef: { current: null },
49
49
  middleRef: { current: null },
50
50
  bottomRef: { current: null },
51
51
  actionsRef: { current: null },
@@ -105,10 +105,10 @@ describe('HtmlElementHost', () => {
105
105
 
106
106
  it('projects open panel into correct slot', () => {
107
107
  const { container } = renderWithSlots({
108
- panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'inset' } } },
108
+ panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'left-top' } } },
109
109
  openPanels: { p1: { props: {} } }
110
110
  })
111
- expect(container.querySelector('[data-slot="inset"] [data-testid="panel-p1"]')).toBeTruthy()
111
+ expect(container.querySelector('[data-slot="left-top"] [data-testid="panel-p1"]')).toBeTruthy()
112
112
  })
113
113
 
114
114
  it('hides panel when closed and passes isOpen=false', () => {
@@ -130,19 +130,28 @@ describe('HtmlElementHost', () => {
130
130
 
131
131
  it('hides panel with inline:false when not fullscreen', () => {
132
132
  const { getByTestId } = renderWithSlots({
133
- panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'inset' }, inline: false } },
133
+ panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'left-top' }, inline: false } },
134
134
  openPanels: { p1: { props: {} } },
135
135
  isFullscreen: false
136
136
  })
137
137
  expect(getByTestId('panel-p1').style.display).toBe('none')
138
138
  })
139
139
 
140
- it('resolves bottom slot to inset on desktop', () => {
140
+ it('shows panel with inline:false when fullscreen', () => {
141
+ const { getByTestId } = renderWithSlots({
142
+ panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'left-top' }, inline: false } },
143
+ openPanels: { p1: { props: {} } },
144
+ isFullscreen: true
145
+ })
146
+ expect(getByTestId('panel-p1').dataset.open).toBe('true')
147
+ })
148
+
149
+ it('resolves bottom slot to left-top on desktop', () => {
141
150
  const { container } = renderWithSlots({
142
151
  panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'bottom' } } },
143
152
  openPanels: { p1: { props: {} } }
144
153
  })
145
- expect(container.querySelector('[data-slot="inset"] [data-testid="panel-p1"]')).toBeTruthy()
154
+ expect(container.querySelector('[data-slot="left-top"] [data-testid="panel-p1"]')).toBeTruthy()
146
155
  expect(container.querySelector('[data-slot="bottom"] [data-testid="panel-p1"]')).toBeNull()
147
156
  })
148
157
 
@@ -169,9 +178,9 @@ describe('HtmlElementHost', () => {
169
178
 
170
179
  it('projects visible control into correct slot', () => {
171
180
  const { container } = renderWithSlots({
172
- controlConfig: { c1: { id: 'c1', html: '<input type="checkbox">', desktop: { slot: 'inset' } } }
181
+ controlConfig: { c1: { id: 'c1', html: '<input type="checkbox">', desktop: { slot: 'left-top' } } }
173
182
  })
174
- const control = container.querySelector('[data-slot="inset"] .im-c-control')
183
+ const control = container.querySelector('[data-slot="left-top"] .im-c-control')
175
184
  expect(control).toBeTruthy()
176
185
  expect(control.innerHTML).toBe('<input type="checkbox">')
177
186
  })
@@ -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 '../../utils/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 {
@@ -7,7 +7,7 @@ jest.mock('../registry/panelRegistry.js')
7
7
  jest.mock('../registry/pluginRegistry.js', () => ({ registeredPlugins: [] }))
8
8
  jest.mock('./pluginWrapper.js', () => ({ withPluginContexts: jest.fn((c) => c) }))
9
9
  jest.mock('../components/Panel/Panel.jsx', () => ({ Panel: (props) => <div data-testid='panel' {...props} /> }))
10
- jest.mock('./slots.js', () => ({ allowedSlots: { panel: ['header', 'modal', 'inset'] } }))
10
+ jest.mock('./slots.js', () => ({ allowedSlots: { panel: ['header', 'modal', 'left-top'] } }))
11
11
 
12
12
  describe('mapPanels', () => {
13
13
  const baseConfig = {
@@ -145,7 +145,7 @@ describe('mapPanels', () => {
145
145
  expect(map()).toHaveLength(1)
146
146
  })
147
147
 
148
- it('replaces bottom slot with inset on non-mobile breakpoints', () => {
148
+ it('replaces bottom slot with left-top on non-mobile breakpoints', () => {
149
149
  defaultAppState.panelConfig = ({
150
150
  p1: {
151
151
  desktop: { slot: 'bottom' },
@@ -153,7 +153,7 @@ describe('mapPanels', () => {
153
153
  }
154
154
  })
155
155
 
156
- const result = map(defaultAppState, 'inset')
156
+ const result = map(defaultAppState, 'left-top')
157
157
  expect(result).toHaveLength(1)
158
158
  expect(result[0].id).toBe('p1')
159
159
  })
@@ -4,14 +4,14 @@ import { allowedSlots } from './slots.js'
4
4
  /**
5
5
  * Resolves the target slot for a panel based on its breakpoint config.
6
6
  * Modal panels always render in the 'modal' slot, and the bottom slot
7
- * is only available on mobile — tablet and desktop fall back to 'inset'.
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
13
  if (bpConfig.slot === 'bottom' && ['tablet', 'desktop'].includes(breakpoint)) {
14
- return 'inset'
14
+ return 'left-top'
15
15
  }
16
16
  return bpConfig.slot
17
17
  }
@@ -7,9 +7,9 @@ describe('resolveTargetSlot', () => {
7
7
  expect(resolveTargetSlot({ modal: true, slot: 'side' }, 'desktop')).toBe('modal')
8
8
  })
9
9
 
10
- it('replaces bottom with inset on tablet and desktop', () => {
11
- expect(resolveTargetSlot({ slot: 'bottom' }, 'tablet')).toBe('inset')
12
- expect(resolveTargetSlot({ slot: 'bottom' }, 'desktop')).toBe('inset')
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')
13
13
  })
14
14
 
15
15
  it('keeps bottom on mobile', () => {
@@ -5,7 +5,6 @@ export const layoutSlots = Object.freeze({
5
5
  TOP_LEFT: 'top-left',
6
6
  TOP_MIDDLE: 'top-middle',
7
7
  TOP_RIGHT: 'top-right',
8
- INSET: 'inset',
9
8
  LEFT_TOP: 'left-top',
10
9
  LEFT_BOTTOM: 'left-bottom',
11
10
  MIDDLE: 'middle',
@@ -22,7 +21,6 @@ export const allowedSlots = Object.freeze({
22
21
  layoutSlots.BANNER,
23
22
  layoutSlots.TOP_LEFT,
24
23
  layoutSlots.TOP_RIGHT,
25
- layoutSlots.INSET,
26
24
  layoutSlots.MIDDLE,
27
25
  layoutSlots.FOOTER_RIGHT,
28
26
  layoutSlots.BOTTOM,
@@ -31,7 +29,6 @@ export const allowedSlots = Object.freeze({
31
29
  panel: [
32
30
  layoutSlots.SIDE,
33
31
  layoutSlots.BANNER,
34
- layoutSlots.INSET, // Deprecate
35
32
  layoutSlots.LEFT_TOP,
36
33
  layoutSlots.LEFT_BOTTOM,
37
34
  layoutSlots.MIDDLE,
@@ -19,7 +19,6 @@ export const AppProvider = ({ options, children }) => {
19
19
  topRef: useRef(null),
20
20
  topLeftColRef: useRef(null),
21
21
  topRightColRef: useRef(null),
22
- insetRef: useRef(null),
23
22
  leftRef: useRef(null),
24
23
  leftTopRef: useRef(null),
25
24
  leftBottomRef: useRef(null),
@@ -1,7 +1,11 @@
1
1
  // src/App/store/dispatchMiddleware.js
2
2
  import { EVENTS as events } from '../../config/events.js'
3
- import { defaultPanelConfig } from '../../config/appConfig.js'
3
+ import { defaultPanelConfig, defaultButtonConfig, defaultControlConfig } from '../../config/appConfig.js'
4
4
  import { deepMerge } from '../../utils/deepMerge.js'
5
+ import { allowedSlots } from '../renderer/slots.js'
6
+ import { logger } from '../../utils/logger.js'
7
+
8
+ const BREAKPOINTS = ['mobile', 'tablet', 'desktop']
5
9
 
6
10
  /**
7
11
  * Determines which panels were implicitly closed when opening a new panel
@@ -81,9 +85,37 @@ export function handleActionSideEffects (action, previousState, panelConfig, eve
81
85
  })
82
86
  }
83
87
 
88
+ if (type === 'ADD_BUTTON') {
89
+ const { id, config } = payload
90
+ const mergedConfig = deepMerge(defaultButtonConfig, config)
91
+ BREAKPOINTS.forEach(bp => {
92
+ const slot = mergedConfig[bp]?.slot
93
+ if (slot && !allowedSlots.button.includes(slot)) {
94
+ logger.warn(`button "${id}" has invalid slot "${slot}" at breakpoint "${bp}". Allowed slots: ${allowedSlots.button.join(', ')}.`)
95
+ }
96
+ })
97
+ }
98
+
99
+ if (type === 'ADD_CONTROL') {
100
+ const { id, config } = payload
101
+ const mergedConfig = deepMerge(defaultControlConfig, config)
102
+ BREAKPOINTS.forEach(bp => {
103
+ const slot = mergedConfig[bp]?.slot
104
+ if (slot && !allowedSlots.control.includes(slot)) {
105
+ logger.warn(`control "${id}" has invalid slot "${slot}" at breakpoint "${bp}". Allowed slots: ${allowedSlots.control.join(', ')}.`)
106
+ }
107
+ })
108
+ }
109
+
84
110
  if (type === 'ADD_PANEL') {
85
111
  const { id, config } = payload
86
112
  const mergedConfig = deepMerge(defaultPanelConfig, config)
113
+ BREAKPOINTS.forEach(bp => {
114
+ const slot = mergedConfig[bp]?.slot
115
+ if (slot && !allowedSlots.panel.includes(slot) && !slot.endsWith('-button')) {
116
+ logger.warn(`panel "${id}" has invalid slot "${slot}" at breakpoint "${bp}". Allowed slots: ${allowedSlots.panel.join(', ')}.`)
117
+ }
118
+ })
87
119
  const bpConfig = mergedConfig[previousState.breakpoint]
88
120
  if (bpConfig?.open) {
89
121
  queueMicrotask(() => {