@defra/interactive-map 0.0.14-alpha → 0.0.15-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 (35) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/umd/im-core.js +1 -1
  4. package/package.json +1 -1
  5. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  6. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  7. package/plugins/beta/datasets/src/manifest.js +4 -4
  8. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  9. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  10. package/plugins/beta/map-styles/src/manifest.js +2 -2
  11. package/plugins/search/dist/css/index.css +1 -1
  12. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  13. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  14. package/plugins/search/src/events/fetchSuggestions.js +1 -1
  15. package/plugins/search/src/events/fetchSuggestions.test.js +4 -4
  16. package/plugins/search/src/search.scss +8 -3
  17. package/src/App/components/Panel/Panel.module.scss +1 -0
  18. package/src/App/hooks/useLayoutMeasurements.js +1 -10
  19. package/src/App/hooks/useLayoutMeasurements.test.js +2 -5
  20. package/src/App/hooks/useVisibleGeometry.js +7 -13
  21. package/src/App/hooks/useVisibleGeometry.test.js +72 -47
  22. package/src/App/layout/Layout.jsx +0 -3
  23. package/src/App/layout/Layout.test.jsx +0 -1
  24. package/src/App/layout/layout.module.scss +11 -77
  25. package/src/App/renderer/HtmlElementHost.jsx +0 -1
  26. package/src/App/renderer/HtmlElementHost.test.jsx +20 -11
  27. package/src/App/renderer/mapPanels.test.js +3 -3
  28. package/src/App/renderer/slotHelpers.js +2 -2
  29. package/src/App/renderer/slotHelpers.test.js +3 -3
  30. package/src/App/renderer/slots.js +0 -3
  31. package/src/App/store/AppProvider.jsx +0 -1
  32. package/src/App/store/appDispatchMiddleware.test.js +2 -2
  33. package/src/config/appConfig.js +4 -4
  34. package/src/utils/getSafeZoneInset.js +139 -42
  35. package/src/utils/getSafeZoneInset.test.js +298 -122
@@ -10,8 +10,16 @@ const pointFeature = { type: 'Feature', geometry: { type: 'Point', coordinates:
10
10
  const multiPointFeature = { type: 'Feature', geometry: { type: 'MultiPoint', coordinates: [[1, 51], [2, 52]] }, properties: {} }
11
11
  const polygonFeature = { type: 'Feature', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]]] }, properties: {} }
12
12
 
13
- const insetPanelRect = { left: 600, top: 0, right: 1000, bottom: 800, width: 400, height: 800 }
14
- const bottomPanelRect = { left: 0, top: 500, right: 1000, bottom: 800, width: 1000, height: 300 }
13
+ const APP_ID = 'test'
14
+ const panelRect = { left: 600, top: 0, right: 1000, bottom: 800, width: 400, height: 800 }
15
+
16
+ // Creates a panel DOM element with id matching what useVisibleGeometry queries.
17
+ const makePanelEl = (panelId, rect = panelRect) => {
18
+ const el = document.createElement('div')
19
+ el.id = `${APP_ID}-panel-${panelId}`
20
+ el.getBoundingClientRect = jest.fn(() => rect)
21
+ return el
22
+ }
15
23
 
16
24
  const setup = (overrides = {}) => {
17
25
  const capturedHandlers = {}
@@ -27,19 +35,18 @@ const setup = (overrides = {}) => {
27
35
  ...overrides.eventBus
28
36
  }
29
37
 
30
- const insetEl = document.createElement('div')
31
- insetEl.getBoundingClientRect = jest.fn(() => insetPanelRect)
32
- const bottomEl = document.createElement('div')
33
- bottomEl.getBoundingClientRect = jest.fn(() => bottomPanelRect)
38
+ // appContainerRef holds panel elements that the hook queries by id
39
+ const appContainer = document.createElement('div')
40
+ const myPanelEl = makePanelEl('myPanel')
41
+ appContainer.appendChild(myPanelEl)
34
42
 
35
43
  const layoutRefs = {
36
44
  mainRef: { current: document.createElement('div') },
37
- insetRef: { current: insetEl },
38
- bottomRef: { current: bottomEl },
45
+ appContainerRef: { current: appContainer },
39
46
  ...overrides.layoutRefs
40
47
  }
41
48
  const panelConfig = {
42
- myPanel: { visibleGeometry: polygonFeature, desktop: { slot: 'inset' } },
49
+ myPanel: { visibleGeometry: polygonFeature, desktop: { slot: 'left-top' } },
43
50
  emptyPanel: {},
44
51
  ...overrides.panelConfig
45
52
  }
@@ -48,10 +55,10 @@ const setup = (overrides = {}) => {
48
55
  ...overrides.panelRegistry
49
56
  }
50
57
 
51
- useConfig.mockReturnValue({ mapProvider, eventBus, ...overrides.config })
58
+ useConfig.mockReturnValue({ id: APP_ID, mapProvider, eventBus, ...overrides.config })
52
59
  useApp.mockReturnValue({ layoutRefs, panelConfig, panelRegistry, breakpoint: 'desktop', ...overrides.app })
53
60
 
54
- return { mapProvider, eventBus, capturedHandlers, layoutRefs, panelConfig, insetEl, bottomEl }
61
+ return { mapProvider, eventBus, capturedHandlers, layoutRefs, panelConfig, myPanelEl, appContainer }
55
62
  }
56
63
 
57
64
  describe('useVisibleGeometry', () => {
@@ -102,12 +109,14 @@ describe('useVisibleGeometry', () => {
102
109
  expect(mapProvider.isGeometryObscured).not.toHaveBeenCalled()
103
110
  })
104
111
 
105
- test('does nothing when panel has visibleGeometry but no slot config', () => {
112
+ test('does nothing when panel element is not in the DOM', () => {
113
+ // Panel has visibleGeometry and slot config but its DOM element is not mounted yet
106
114
  const { mapProvider, capturedHandlers } = setup({
107
- panelConfig: { noSlotPanel: { visibleGeometry: polygonFeature } }
115
+ panelConfig: { noElPanel: { visibleGeometry: polygonFeature, desktop: { slot: 'left-top' } } }
108
116
  })
109
117
  renderHook(() => useVisibleGeometry())
110
- capturedHandlers['app:panelopened']({ panelId: 'noSlotPanel' })
118
+ capturedHandlers['app:panelopened']({ panelId: 'noElPanel' })
119
+ jest.runAllTimers()
111
120
  expect(mapProvider.isGeometryObscured).not.toHaveBeenCalled()
112
121
  })
113
122
 
@@ -118,19 +127,20 @@ describe('useVisibleGeometry', () => {
118
127
  expect(mapProvider.fitToBounds).not.toHaveBeenCalled()
119
128
  })
120
129
 
121
- test('does nothing when slot ref has zero dimensions (panel not visible)', () => {
122
- const zeroEl = document.createElement('div')
123
- zeroEl.getBoundingClientRect = jest.fn(() => ({ left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }))
130
+ test('does nothing when panel element has zero dimensions (panel not yet visible)', () => {
131
+ const zeroRect = { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
132
+ const appContainer = document.createElement('div')
133
+ appContainer.appendChild(makePanelEl('myPanel', zeroRect))
124
134
  const { mapProvider, capturedHandlers } = setup({
125
135
  layoutRefs: {
126
136
  mainRef: { current: document.createElement('div') },
127
- insetRef: { current: zeroEl }
137
+ appContainerRef: { current: appContainer }
128
138
  }
129
139
  })
130
140
  renderHook(() => useVisibleGeometry())
131
141
  capturedHandlers['app:panelopened']({ panelId: 'myPanel' })
132
142
 
133
- // Run the current pending animation frame
143
+ // Run only the first pending animation frame — panel has zero size so it reschedules
134
144
  jest.runOnlyPendingTimers()
135
145
 
136
146
  expect(mapProvider.isGeometryObscured).not.toHaveBeenCalled()
@@ -151,14 +161,17 @@ describe('useVisibleGeometry', () => {
151
161
  capturedHandlers['app:panelopened']({ panelId: 'myPanel' })
152
162
  jest.runAllTimers()
153
163
 
154
- expect(mapProvider.isGeometryObscured).toHaveBeenCalledWith(polygonFeature, insetPanelRect)
164
+ expect(mapProvider.isGeometryObscured).toHaveBeenCalledWith(polygonFeature, panelRect)
155
165
  expect(mapProvider.fitToBounds).toHaveBeenCalledWith(polygonFeature)
156
166
  expect(mapProvider.setView).not.toHaveBeenCalled()
157
167
  })
158
168
 
159
169
  test('calls setView with center for Point geometry when obscured', () => {
170
+ const appContainer = document.createElement('div')
171
+ appContainer.appendChild(makePanelEl('pointPanel'))
160
172
  const { mapProvider, capturedHandlers } = setup({
161
- panelConfig: { pointPanel: { visibleGeometry: pointFeature, desktop: { slot: 'inset' } } }
173
+ panelConfig: { pointPanel: { visibleGeometry: pointFeature, desktop: { slot: 'left-top' } } },
174
+ layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
162
175
  })
163
176
  renderHook(() => useVisibleGeometry())
164
177
  capturedHandlers['app:panelopened']({ panelId: 'pointPanel' })
@@ -169,8 +182,11 @@ describe('useVisibleGeometry', () => {
169
182
  })
170
183
 
171
184
  test('calls setView with first coordinate for MultiPoint geometry when obscured', () => {
185
+ const appContainer = document.createElement('div')
186
+ appContainer.appendChild(makePanelEl('mpPanel'))
172
187
  const { mapProvider, capturedHandlers } = setup({
173
- panelConfig: { mpPanel: { visibleGeometry: multiPointFeature, desktop: { slot: 'inset' } } }
188
+ panelConfig: { mpPanel: { visibleGeometry: multiPointFeature, desktop: { slot: 'left-top' } } },
189
+ layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
174
190
  })
175
191
  renderHook(() => useVisibleGeometry())
176
192
  capturedHandlers['app:panelopened']({ panelId: 'mpPanel' })
@@ -182,8 +198,11 @@ describe('useVisibleGeometry', () => {
182
198
 
183
199
  test('calls fitToBounds for a raw non-Feature geometry (e.g. Polygon) when obscured', () => {
184
200
  const rawPolygon = { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]]] }
201
+ const appContainer = document.createElement('div')
202
+ appContainer.appendChild(makePanelEl('geoPanel'))
185
203
  const { mapProvider, capturedHandlers } = setup({
186
- panelConfig: { geoPanel: { visibleGeometry: rawPolygon, desktop: { slot: 'inset' } } }
204
+ panelConfig: { geoPanel: { visibleGeometry: rawPolygon, desktop: { slot: 'left-top' } } },
205
+ layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
187
206
  })
188
207
  renderHook(() => useVisibleGeometry())
189
208
  capturedHandlers['app:panelopened']({ panelId: 'geoPanel' })
@@ -194,8 +213,11 @@ describe('useVisibleGeometry', () => {
194
213
 
195
214
  test('calls setView for a raw Point geometry (not Feature-wrapped) when obscured', () => {
196
215
  const rawPoint = { type: 'Point', coordinates: [1, 51] }
216
+ const appContainer = document.createElement('div')
217
+ appContainer.appendChild(makePanelEl('rawPointPanel'))
197
218
  const { mapProvider, capturedHandlers } = setup({
198
- panelConfig: { rawPointPanel: { visibleGeometry: rawPoint, desktop: { slot: 'inset' } } }
219
+ panelConfig: { rawPointPanel: { visibleGeometry: rawPoint, desktop: { slot: 'left-top' } } },
220
+ layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
199
221
  })
200
222
  renderHook(() => useVisibleGeometry())
201
223
  capturedHandlers['app:panelopened']({ panelId: 'rawPointPanel' })
@@ -206,8 +228,11 @@ describe('useVisibleGeometry', () => {
206
228
 
207
229
  test('does not call setView when Point feature has null coordinates', () => {
208
230
  const nullCoordsFeature = { type: 'Feature', geometry: { type: 'Point', coordinates: null }, properties: {} }
231
+ const appContainer = document.createElement('div')
232
+ appContainer.appendChild(makePanelEl('nullPanel'))
209
233
  const { mapProvider, capturedHandlers } = setup({
210
- panelConfig: { nullPanel: { visibleGeometry: nullCoordsFeature, desktop: { slot: 'inset' } } }
234
+ panelConfig: { nullPanel: { visibleGeometry: nullCoordsFeature, desktop: { slot: 'left-top' } } },
235
+ layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
211
236
  })
212
237
  renderHook(() => useVisibleGeometry())
213
238
  capturedHandlers['app:panelopened']({ panelId: 'nullPanel' })
@@ -216,26 +241,14 @@ describe('useVisibleGeometry', () => {
216
241
  expect(mapProvider.fitToBounds).not.toHaveBeenCalled()
217
242
  })
218
243
 
219
- test('uses bottom slot ref when panel is in bottom slot', () => {
220
- const { mapProvider, capturedHandlers } = setup({
221
- panelConfig: { bottomPanel: { visibleGeometry: polygonFeature, desktop: { slot: 'bottom' } } }
222
- })
223
- renderHook(() => useVisibleGeometry())
224
- capturedHandlers['app:panelopened']({ panelId: 'bottomPanel' })
225
- jest.runAllTimers()
226
-
227
- expect(mapProvider.isGeometryObscured).toHaveBeenCalledWith(polygonFeature, bottomPanelRect)
228
- expect(mapProvider.fitToBounds).toHaveBeenCalledWith(polygonFeature)
229
- })
230
-
231
244
  test('uses latest panelConfig via ref when it changes between renders', () => {
232
- const { mapProvider, capturedHandlers, insetEl } = setup()
245
+ const { mapProvider, capturedHandlers, appContainer } = setup()
233
246
  const { rerender } = renderHook(() => useVisibleGeometry())
234
247
 
235
248
  const updatedGeometry = { type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} }
236
- const updatedPanelConfig = { myPanel: { visibleGeometry: updatedGeometry, desktop: { slot: 'inset' } } }
249
+ const updatedPanelConfig = { myPanel: { visibleGeometry: updatedGeometry, desktop: { slot: 'left-top' } } }
237
250
  useApp.mockReturnValue({
238
- layoutRefs: { mainRef: { current: document.createElement('div') }, insetRef: { current: insetEl } },
251
+ layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } },
239
252
  panelConfig: updatedPanelConfig,
240
253
  panelRegistry: { getPanelConfig: jest.fn(() => updatedPanelConfig) },
241
254
  breakpoint: 'desktop'
@@ -249,20 +262,26 @@ describe('useVisibleGeometry', () => {
249
262
 
250
263
  test('uses slot from event payload when registry config lacks slot info', () => {
251
264
  const freshGeometry = { type: 'Feature', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]]] }, properties: {} }
265
+ const appContainer = document.createElement('div')
266
+ appContainer.appendChild(makePanelEl('freshPanel'))
252
267
  const { mapProvider, capturedHandlers } = setup({
253
- panelRegistry: { getPanelConfig: jest.fn(() => ({ freshPanel: { visibleGeometry: freshGeometry } })) }
268
+ panelRegistry: { getPanelConfig: jest.fn(() => ({ freshPanel: { visibleGeometry: freshGeometry } })) },
269
+ layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
254
270
  })
255
271
  renderHook(() => useVisibleGeometry())
256
272
  // Event includes slot (as middleware provides for ADD_PANEL); registry config has no slot info
257
- capturedHandlers['app:panelopened']({ panelId: 'freshPanel', slot: 'inset' })
273
+ capturedHandlers['app:panelopened']({ panelId: 'freshPanel', slot: 'left-top' })
258
274
  jest.runAllTimers()
259
275
  expect(mapProvider.fitToBounds).toHaveBeenCalledWith(freshGeometry)
260
276
  })
261
277
 
262
278
  test('falls back to panelRegistry for panels not yet in stale panelConfig', () => {
263
279
  const freshGeometry = { type: 'Feature', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]]] }, properties: {} }
280
+ const appContainer = document.createElement('div')
281
+ appContainer.appendChild(makePanelEl('freshPanel'))
264
282
  const { mapProvider, capturedHandlers } = setup({
265
- panelRegistry: { getPanelConfig: jest.fn(() => ({ freshPanel: { visibleGeometry: freshGeometry, desktop: { slot: 'inset' } } })) }
283
+ panelRegistry: { getPanelConfig: jest.fn(() => ({ freshPanel: { visibleGeometry: freshGeometry, desktop: { slot: 'left-top' } } })) },
284
+ layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
266
285
  })
267
286
  renderHook(() => useVisibleGeometry())
268
287
  capturedHandlers['app:panelopened']({ panelId: 'freshPanel' })
@@ -271,13 +290,16 @@ describe('useVisibleGeometry', () => {
271
290
  })
272
291
 
273
292
  test('falls back to config when panel not in panelConfig and registry returns null', () => {
293
+ const appContainer = document.createElement('div')
294
+ appContainer.appendChild(makePanelEl('missingPanel'))
274
295
  const { mapProvider, capturedHandlers } = setup({
275
296
  panelConfig: {}, // panel not present
276
- panelRegistry: { getPanelConfig: jest.fn(() => null) } // registry returns null
297
+ panelRegistry: { getPanelConfig: jest.fn(() => null) }, // registry returns null
298
+ layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
277
299
  })
278
300
 
279
301
  renderHook(() => useVisibleGeometry())
280
- capturedHandlers['app:panelopened']({ panelId: 'missingPanel', visibleGeometry: polygonFeature, slot: 'inset' })
302
+ capturedHandlers['app:panelopened']({ panelId: 'missingPanel', visibleGeometry: polygonFeature, slot: 'left-top' })
281
303
  jest.runAllTimers()
282
304
  // Should still call fitToBounds using visibleGeometry from event payload
283
305
  expect(mapProvider.fitToBounds).toHaveBeenCalledWith(polygonFeature)
@@ -285,11 +307,14 @@ describe('useVisibleGeometry', () => {
285
307
 
286
308
  test('uses visibleGeometry from event payload directly, bypassing registry (ADD_PANEL first-click case)', () => {
287
309
  // Registry is empty — simulates first ADD_PANEL before React has processed the reducer
310
+ const appContainer = document.createElement('div')
311
+ appContainer.appendChild(makePanelEl('newPanel'))
288
312
  const { mapProvider, capturedHandlers } = setup({
289
- panelRegistry: { getPanelConfig: jest.fn(() => ({})) }
313
+ panelRegistry: { getPanelConfig: jest.fn(() => ({})) },
314
+ layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
290
315
  })
291
316
  renderHook(() => useVisibleGeometry())
292
- capturedHandlers['app:panelopened']({ panelId: 'newPanel', slot: 'inset', visibleGeometry: polygonFeature })
317
+ capturedHandlers['app:panelopened']({ panelId: 'newPanel', slot: 'left-top', visibleGeometry: polygonFeature })
293
318
  jest.runAllTimers()
294
319
  expect(mapProvider.fitToBounds).toHaveBeenCalledWith(polygonFeature)
295
320
  })
@@ -63,9 +63,6 @@ export const Layout = () => {
63
63
  <SlotRenderer slot={layoutSlots.TOP_RIGHT} />
64
64
  </div>
65
65
  </div>
66
- <div className='im-o-app__inset' ref={layoutRefs.insetRef}>
67
- <SlotRenderer slot={layoutSlots.INSET} />
68
- </div>
69
66
  <div className='im-o-app__left' ref={layoutRefs.leftRef}>
70
67
  <div className='im-o-app__left-top' ref={layoutRefs.leftTopRef}>
71
68
  <SlotRenderer slot={layoutSlots.LEFT_TOP} />
@@ -37,7 +37,6 @@ describe('Layout', () => {
37
37
  topRef: React.createRef(),
38
38
  topLeftColRef: React.createRef(),
39
39
  topRightColRef: React.createRef(),
40
- insetRef: React.createRef(),
41
40
  rightRef: React.createRef(),
42
41
  footerRef: React.createRef(),
43
42
  actionsRef: React.createRef()
@@ -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);
@@ -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
  })
@@ -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),
@@ -189,7 +189,7 @@ describe('appDispatchMiddleware', () => {
189
189
 
190
190
  expect(eventBus.emit).toHaveBeenCalledWith(
191
191
  events.APP_PANEL_OPENED,
192
- { panelId: 'newPanel', slot: 'inset' }
192
+ { panelId: 'newPanel', slot: 'left-top' }
193
193
  )
194
194
  })
195
195
 
@@ -204,7 +204,7 @@ describe('appDispatchMiddleware', () => {
204
204
 
205
205
  expect(eventBus.emit).toHaveBeenCalledWith(
206
206
  events.APP_PANEL_OPENED,
207
- { panelId: 'geoPanel', slot: 'inset', visibleGeometry }
207
+ { panelId: 'geoPanel', slot: 'left-top', visibleGeometry }
208
208
  )
209
209
  })
210
210
 
@@ -120,14 +120,14 @@ export const defaultPanelConfig = {
120
120
  showLabel: true
121
121
  },
122
122
  tablet: {
123
- slot: 'inset',
123
+ slot: 'left-top',
124
124
  open: true,
125
125
  dismissible: true,
126
126
  modal: false,
127
127
  showLabel: true
128
128
  },
129
129
  desktop: {
130
- slot: 'inset',
130
+ slot: 'left-top',
131
131
  open: true,
132
132
  dismissible: true,
133
133
  modal: false,
@@ -144,10 +144,10 @@ export const defaultControlConfig = {
144
144
  slot: 'bottom'
145
145
  },
146
146
  tablet: {
147
- slot: 'inset'
147
+ slot: 'left-top'
148
148
  },
149
149
  desktop: {
150
- slot: 'inset'
150
+ slot: 'left-top'
151
151
  }
152
152
  }
153
153