From 17dbf1eadedc8fd42ca89354d66fbb70ebd0a09b Mon Sep 17 00:00:00 2001 From: Sandesh Pandey Date: Thu, 15 Oct 2026 15:29:25 -0500 Subject: [PATCH 1/4] Layout: configurable bottom offset for floating panels panelSettings.floatingBottomOffset raises the bottom-left, bottom-center and bottom-right float zones that many pixels above the map's bottom edge, so an admin can keep them clear of the basemap's logo and attribution. It is set in the configure page's Floating Panels section and defaults to 0. --- configure/src/metaconfigs/tab-ui-config.json | 9 +++++ docs/MISSION_CONFIG_REFERENCE.md | 1 + .../UserInterface_/UserInterfaceModern_.css | 9 ++--- .../UserInterface_/UserInterfaceModern_.js | 14 +++++--- .../Validators/DashboardConfigValidator.js | 6 ++++ src/essence/modern.js | 3 +- src/essence/types/dashboard.ts | 3 ++ tests/unit/DashboardConfigValidator.spec.js | 26 ++++++++++++++ ...rInterfaceModern.floatBottomOffset.spec.js | 34 +++++++++++++++++++ 9 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 tests/unit/userInterfaceModern.floatBottomOffset.spec.js diff --git a/configure/src/metaconfigs/tab-ui-config.json b/configure/src/metaconfigs/tab-ui-config.json index 55e3b8451..d1a85b033 100644 --- a/configure/src/metaconfigs/tab-ui-config.json +++ b/configure/src/metaconfigs/tab-ui-config.json @@ -245,6 +245,15 @@ { "name": "Floating Panels (Center Area)", "components": [ + { + "field": "panelSettings.floatingBottomOffset", + "name": "Bottom Offset (px)", + "description": "Raises the bottom-left, bottom-center and bottom-right floating panels this many pixels above the map's bottom edge, e.g. to keep them clear of the basemap's logo and attribution.", + "type": "number", + "default": 0, + "width": 6, + "min": 0 + }, { "field": "panelSettings.floatingPanels", "name": "Floating Panels", diff --git a/docs/MISSION_CONFIG_REFERENCE.md b/docs/MISSION_CONFIG_REFERENCE.md index 3d9da9893..5dfb19ad8 100644 --- a/docs/MISSION_CONFIG_REFERENCE.md +++ b/docs/MISSION_CONFIG_REFERENCE.md @@ -65,6 +65,7 @@ The mission configuration controls the layout and behavior of panels in modern i | Field | Type | Required | Description | |-------|------|----------|-------------| | `layoutStyle` | String | No | `"overlay"` (default) or `"compact"` layout style | +| `floatingBottomOffset` | Number | No | Pixels the bottom-left, bottom-center and bottom-right floating panels sit above the map's bottom edge, e.g. to clear the basemap's logo and attribution. Default `0` | | `panels` | Array | Yes | Array of panel configuration objects | ## Panel Configuration diff --git a/src/essence/Basics/UserInterface_/UserInterfaceModern_.css b/src/essence/Basics/UserInterface_/UserInterfaceModern_.css index 3806531cf..91cb19986 100644 --- a/src/essence/Basics/UserInterface_/UserInterfaceModern_.css +++ b/src/essence/Basics/UserInterface_/UserInterfaceModern_.css @@ -555,13 +555,14 @@ body.ui-is-dragging .ui-modern-grid .ui-panel { padding: var(--ui-float-outer-gap); } -/* Zone positions */ +/* Zone positions. The bottom zones sit --ui-float-bottom-offset above the + map's bottom edge (panelSettings.floatingBottomOffset). */ .ui-float-zone-top-left { top: 0; left: 0; } .ui-float-zone-top-center { top: 0; left: 50%; transform: translateX(-50%); } .ui-float-zone-top-right { top: 0; right: 0; } -.ui-float-zone-bottom-left { bottom: 0; left: 0; } -.ui-float-zone-bottom-center { bottom: 0; left: 50%; transform: translateX(-50%); } -.ui-float-zone-bottom-right { bottom: 0; right: 0; } +.ui-float-zone-bottom-left { bottom: var(--ui-float-bottom-offset, 0px); left: 0; } +.ui-float-zone-bottom-center { bottom: var(--ui-float-bottom-offset, 0px); left: 50%; transform: translateX(-50%); } +.ui-float-zone-bottom-right { bottom: var(--ui-float-bottom-offset, 0px); right: 0; } /* Float panels re-enable pointer events */ .ui-float-panel { diff --git a/src/essence/Basics/UserInterface_/UserInterfaceModern_.js b/src/essence/Basics/UserInterface_/UserInterfaceModern_.js index cd8595a56..47f4343a5 100644 --- a/src/essence/Basics/UserInterface_/UserInterfaceModern_.js +++ b/src/essence/Basics/UserInterface_/UserInterfaceModern_.js @@ -16,6 +16,7 @@ const DEFAULT_MAX_PANEL_SIZE = 9999; // --- Module-Level State --- let panels = [] let layoutStyle = '' +let floatBottomOffset = 0 let toolLoadQueue = [] let cleanupLayoutListener = null let _resizeObserver = null @@ -111,10 +112,13 @@ const _createPanelHeader = (panel) => { return header } -const _renderFloatRegions = (floatPanels) => { +const _renderFloatRegions = (floatPanels, bottomOffset = 0) => { if (!floatPanels || floatPanels.length === 0) return null const fragment = $('
') + if (bottomOffset > 0) { + fragment[0].style.setProperty('--ui-float-bottom-offset', `${bottomOffset}px`) + } // Group panels by their float position (validator enforces at most one per position) const byPosition = {} @@ -276,10 +280,12 @@ const UserInterfaceModern_ = { * @param {Array} panelsArr - Array of panel objects (parsed from config & PanelManager) * @param {string} style - The layout style ('overlay' | 'compact') * @param {string} theme - The theme identifier (e.g. 'default', 'disasters') + * @param {number} bottomOffset - Pixels the bottom float zones sit above the map's bottom edge */ - init: function (panelsArr, style = 'overlay', theme = 'default') { + init: function (panelsArr, style = 'overlay', theme = 'default', bottomOffset = 0) { panels = panelsArr layoutStyle = style + floatBottomOffset = Number(bottomOffset) > 0 ? Number(bottomOffset) : 0 // Dynamic import() so webpack emits one lazy CSS chunk per theme and // only the configured one is fetched (a sync require() here bundled @@ -613,7 +619,7 @@ const UserInterfaceModern_ = { } } - const floatRegions = _renderFloatRegions(floatPanels) + const floatRegions = _renderFloatRegions(floatPanels, floatBottomOffset) if (floatRegions) { centralArea.append(floatRegions) } @@ -870,4 +876,4 @@ const UserInterfaceModern_ = { } export default UserInterfaceModern_ -export { _createPanelHeader, _createPanelIconTray, _renderRegion } +export { _createPanelHeader, _createPanelIconTray, _renderRegion, _renderFloatRegions } diff --git a/src/essence/Validators/DashboardConfigValidator.js b/src/essence/Validators/DashboardConfigValidator.js index 69e540414..62d32f0ef 100644 --- a/src/essence/Validators/DashboardConfigValidator.js +++ b/src/essence/Validators/DashboardConfigValidator.js @@ -76,6 +76,12 @@ export function validateModernConfig(config) { ) } + // Validate floatingBottomOffset (optional, defaults to 0) + const offset = panelSettings.floatingBottomOffset + if (offset != null && offset !== '' && !(typeof offset === 'number' && Number.isFinite(offset) && offset >= 0)) { + warnings.push('panelSettings.floatingBottomOffset must be a non-negative number of pixels; using 0') + } + // Validate theme (optional, defaults to 'default') if (config.msv && config.msv.theme && typeof config.msv.theme !== 'string') { errors.push('msv.theme must be a string') diff --git a/src/essence/modern.js b/src/essence/modern.js index 9cbac7c9a..e137f60d1 100644 --- a/src/essence/modern.js +++ b/src/essence/modern.js @@ -235,9 +235,10 @@ class ModernInterface { const layoutStyle = this.configData?.panelSettings?.layoutStyle || 'overlay' const theme = this.configData?.msv?.theme || 'default' + const floatBottomOffset = this.configData?.panelSettings?.floatingBottomOffset || 0 // Initialize the User Interface with the sorted panels from PanelManager - UserInterfaceModern_.init(activePanels, layoutStyle, theme) + UserInterfaceModern_.init(activePanels, layoutStyle, theme, floatBottomOffset) // TimeControl must initialize ahead of Map_ because it seeds each // time-enabled layer's time window, which Map_.init reads when it diff --git a/src/essence/types/dashboard.ts b/src/essence/types/dashboard.ts index fd619805e..3e285e461 100644 --- a/src/essence/types/dashboard.ts +++ b/src/essence/types/dashboard.ts @@ -71,6 +71,9 @@ export interface PanelSettings { /** Layout style ('overlay' | 'compact') */ layoutStyle?: LayoutStyle; + + /** Pixels the bottom floating panels sit above the map's bottom edge */ + floatingBottomOffset?: number; } /** diff --git a/tests/unit/DashboardConfigValidator.spec.js b/tests/unit/DashboardConfigValidator.spec.js index 98b94ea45..50e023e1b 100644 --- a/tests/unit/DashboardConfigValidator.spec.js +++ b/tests/unit/DashboardConfigValidator.spec.js @@ -91,6 +91,32 @@ test.describe('DashboardConfigValidator', () => { expect(result.errors[0]).toMatch(/panelSettings.layoutStyle must be one of/); }); + test('warns on an unusable floatingBottomOffset and accepts a pixel count', () => { + const config = (floatingBottomOffset) => ({ + panelSettings: { + floatingBottomOffset, + panels: [ + { + id: 'panel-1', + position: 'left', + priority: 1, + layoutType: 'stacked', + stateConstraints: { + allowedStates: ['expanded'], + defaultState: 'expanded' + } + } + ] + } + }); + + expect(validateModernConfig(config(24)).warnings).toHaveLength(0); + expect(validateModernConfig(config('')).warnings).toHaveLength(0); + const result = validateModernConfig(config(-5)); + expect(result.valid).toBe(true); + expect(result.warnings[0]).toMatch(/floatingBottomOffset must be a non-negative number/); + }); + test('catches defaultState not in allowedStates', () => { const config = { panelSettings: { diff --git a/tests/unit/userInterfaceModern.floatBottomOffset.spec.js b/tests/unit/userInterfaceModern.floatBottomOffset.spec.js new file mode 100644 index 000000000..96b1f3123 --- /dev/null +++ b/tests/unit/userInterfaceModern.floatBottomOffset.spec.js @@ -0,0 +1,34 @@ +import { test, expect, vi } from 'vitest' +vi.mock('../../src/essence/Basics/Viewer_/Viewer_', () => ({ default: {} })) + +import PanelManager_ from '../../src/essence/Basics/PanelManager_/PanelManager_.ts' + +// The bottom float zones read --ui-float-bottom-offset, which the float +// regions wrapper carries from panelSettings.floatingBottomOffset. + +const { _renderFloatRegions } = await import( + '../../src/essence/Basics/UserInterface_/UserInterfaceModern_.js' +) + +const floatPanels = () => { + PanelManager_.clear() + PanelManager_.registerPanel({ + id: 'scale', + position: 'float-bottom-left', + layoutType: 'stacked', + priority: 0, + stateConstraints: { allowedStates: ['expanded'], defaultState: 'expanded' }, + }) + return [PanelManager_.getPanelState('scale')] +} + +const offsetOf = (regions) => regions[0].style.getPropertyValue('--ui-float-bottom-offset') + +test('a configured offset raises the bottom float zones', () => { + expect(offsetOf(_renderFloatRegions(floatPanels(), 24))).toBe('24px') +}) + +test('without an offset the zones stay on the edge', () => { + expect(offsetOf(_renderFloatRegions(floatPanels()))).toBe('') + expect(offsetOf(_renderFloatRegions(floatPanels(), 0))).toBe('') +}) From bba0ed45eb00ded67f49a1daf1a0ca6b0cf56cf1 Mon Sep 17 00:00:00 2001 From: Sandesh Pandey Date: Thu, 15 Oct 2026 16:11:45 -0500 Subject: [PATCH 2/4] Layout: read the float bottom offset through toPixelNumber and cap it The validator and the UI now read panelSettings.floatingBottomOffset the same way, so numeric strings are accepted and Infinity or other unusable values fall back to 0 instead of reaching the CSS. The bottom zones never sit higher than half the map's height, and the Configure field is capped at 200px. --- configure/src/metaconfigs/tab-ui-config.json | 4 ++-- .../Basics/UserInterface_/UserInterfaceModern_.css | 10 ++++++---- .../Basics/UserInterface_/UserInterfaceModern_.js | 4 ++-- src/essence/Validators/DashboardConfigValidator.js | 2 +- src/essence/modern.js | 2 +- tests/unit/DashboardConfigValidator.spec.js | 3 +++ 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/configure/src/metaconfigs/tab-ui-config.json b/configure/src/metaconfigs/tab-ui-config.json index d1a85b033..ff55d7b83 100644 --- a/configure/src/metaconfigs/tab-ui-config.json +++ b/configure/src/metaconfigs/tab-ui-config.json @@ -250,9 +250,9 @@ "name": "Bottom Offset (px)", "description": "Raises the bottom-left, bottom-center and bottom-right floating panels this many pixels above the map's bottom edge, e.g. to keep them clear of the basemap's logo and attribution.", "type": "number", - "default": 0, "width": 6, - "min": 0 + "min": 0, + "max": 200 }, { "field": "panelSettings.floatingPanels", diff --git a/src/essence/Basics/UserInterface_/UserInterfaceModern_.css b/src/essence/Basics/UserInterface_/UserInterfaceModern_.css index 91cb19986..561216c81 100644 --- a/src/essence/Basics/UserInterface_/UserInterfaceModern_.css +++ b/src/essence/Basics/UserInterface_/UserInterfaceModern_.css @@ -556,13 +556,15 @@ body.ui-is-dragging .ui-modern-grid .ui-panel { } /* Zone positions. The bottom zones sit --ui-float-bottom-offset above the - map's bottom edge (panelSettings.floatingBottomOffset). */ + map's bottom edge (panelSettings.floatingBottomOffset), capped at half the + map's height so a large offset on a short viewport cannot push them into + the top zones or off the map. */ .ui-float-zone-top-left { top: 0; left: 0; } .ui-float-zone-top-center { top: 0; left: 50%; transform: translateX(-50%); } .ui-float-zone-top-right { top: 0; right: 0; } -.ui-float-zone-bottom-left { bottom: var(--ui-float-bottom-offset, 0px); left: 0; } -.ui-float-zone-bottom-center { bottom: var(--ui-float-bottom-offset, 0px); left: 50%; transform: translateX(-50%); } -.ui-float-zone-bottom-right { bottom: var(--ui-float-bottom-offset, 0px); right: 0; } +.ui-float-zone-bottom-left { bottom: min(var(--ui-float-bottom-offset, 0px), 50%); left: 0; } +.ui-float-zone-bottom-center { bottom: min(var(--ui-float-bottom-offset, 0px), 50%); left: 50%; transform: translateX(-50%); } +.ui-float-zone-bottom-right { bottom: min(var(--ui-float-bottom-offset, 0px), 50%); right: 0; } /* Float panels re-enable pointer events */ .ui-float-panel { diff --git a/src/essence/Basics/UserInterface_/UserInterfaceModern_.js b/src/essence/Basics/UserInterface_/UserInterfaceModern_.js index 47f4343a5..c2c1f64a5 100644 --- a/src/essence/Basics/UserInterface_/UserInterfaceModern_.js +++ b/src/essence/Basics/UserInterface_/UserInterfaceModern_.js @@ -280,12 +280,12 @@ const UserInterfaceModern_ = { * @param {Array} panelsArr - Array of panel objects (parsed from config & PanelManager) * @param {string} style - The layout style ('overlay' | 'compact') * @param {string} theme - The theme identifier (e.g. 'default', 'disasters') - * @param {number} bottomOffset - Pixels the bottom float zones sit above the map's bottom edge + * @param {number|string} bottomOffset - Pixels the bottom float zones sit above the map's bottom edge; an unusable value means 0 */ init: function (panelsArr, style = 'overlay', theme = 'default', bottomOffset = 0) { panels = panelsArr layoutStyle = style - floatBottomOffset = Number(bottomOffset) > 0 ? Number(bottomOffset) : 0 + floatBottomOffset = toPixelNumber(bottomOffset) ?? 0 // Dynamic import() so webpack emits one lazy CSS chunk per theme and // only the configured one is fetched (a sync require() here bundled diff --git a/src/essence/Validators/DashboardConfigValidator.js b/src/essence/Validators/DashboardConfigValidator.js index 62d32f0ef..f4452b98a 100644 --- a/src/essence/Validators/DashboardConfigValidator.js +++ b/src/essence/Validators/DashboardConfigValidator.js @@ -78,7 +78,7 @@ export function validateModernConfig(config) { // Validate floatingBottomOffset (optional, defaults to 0) const offset = panelSettings.floatingBottomOffset - if (offset != null && offset !== '' && !(typeof offset === 'number' && Number.isFinite(offset) && offset >= 0)) { + if (isDimensionSet(offset) && toPixelNumber(offset, { allowZero: true }) === null) { warnings.push('panelSettings.floatingBottomOffset must be a non-negative number of pixels; using 0') } diff --git a/src/essence/modern.js b/src/essence/modern.js index e137f60d1..10a6d399a 100644 --- a/src/essence/modern.js +++ b/src/essence/modern.js @@ -235,7 +235,7 @@ class ModernInterface { const layoutStyle = this.configData?.panelSettings?.layoutStyle || 'overlay' const theme = this.configData?.msv?.theme || 'default' - const floatBottomOffset = this.configData?.panelSettings?.floatingBottomOffset || 0 + const floatBottomOffset = this.configData?.panelSettings?.floatingBottomOffset // Initialize the User Interface with the sorted panels from PanelManager UserInterfaceModern_.init(activePanels, layoutStyle, theme, floatBottomOffset) diff --git a/tests/unit/DashboardConfigValidator.spec.js b/tests/unit/DashboardConfigValidator.spec.js index 50e023e1b..0a0be929c 100644 --- a/tests/unit/DashboardConfigValidator.spec.js +++ b/tests/unit/DashboardConfigValidator.spec.js @@ -112,6 +112,9 @@ test.describe('DashboardConfigValidator', () => { expect(validateModernConfig(config(24)).warnings).toHaveLength(0); expect(validateModernConfig(config('')).warnings).toHaveLength(0); + expect(validateModernConfig(config('24')).warnings).toHaveLength(0); + expect(validateModernConfig(config(Infinity)).warnings).toHaveLength(1); + expect(validateModernConfig(config('1e400')).warnings).toHaveLength(1); const result = validateModernConfig(config(-5)); expect(result.valid).toBe(true); expect(result.warnings[0]).toMatch(/floatingBottomOffset must be a non-negative number/); From f149d8e0f77578d07375a575bacfa2ad3fcd0cd5 Mon Sep 17 00:00:00 2001 From: Sandesh Pandey Date: Thu, 15 Oct 2026 16:32:22 -0500 Subject: [PATCH 3/4] Layout: cap the float bottom offset at 200px in the validator and runtime The validator warns above the Configure field's 200px maximum and init clamps to it, so hand-edited configs behave as Configure shows. The PanelSettings type accepts the numeric and 'px' strings the runtime parses. --- docs/MISSION_CONFIG_REFERENCE.md | 2 +- .../Basics/UserInterface_/UserInterfaceModern_.js | 5 +++-- src/essence/Validators/DashboardConfigValidator.js | 13 ++++++++++--- src/essence/types/dashboard.ts | 14 ++++++++++++-- tests/unit/DashboardConfigValidator.spec.js | 4 ++++ 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/docs/MISSION_CONFIG_REFERENCE.md b/docs/MISSION_CONFIG_REFERENCE.md index 5dfb19ad8..7cc9c9352 100644 --- a/docs/MISSION_CONFIG_REFERENCE.md +++ b/docs/MISSION_CONFIG_REFERENCE.md @@ -65,7 +65,7 @@ The mission configuration controls the layout and behavior of panels in modern i | Field | Type | Required | Description | |-------|------|----------|-------------| | `layoutStyle` | String | No | `"overlay"` (default) or `"compact"` layout style | -| `floatingBottomOffset` | Number | No | Pixels the bottom-left, bottom-center and bottom-right floating panels sit above the map's bottom edge, e.g. to clear the basemap's logo and attribution. Default `0` | +| `floatingBottomOffset` | Number | No | Pixels the bottom-left, bottom-center and bottom-right floating panels sit above the map's bottom edge, e.g. to clear the basemap's logo and attribution. Default `0`, max `200` | | `panels` | Array | Yes | Array of panel configuration objects | ## Panel Configuration diff --git a/src/essence/Basics/UserInterface_/UserInterfaceModern_.js b/src/essence/Basics/UserInterface_/UserInterfaceModern_.js index c2c1f64a5..1bb8c41f2 100644 --- a/src/essence/Basics/UserInterface_/UserInterfaceModern_.js +++ b/src/essence/Basics/UserInterface_/UserInterfaceModern_.js @@ -6,6 +6,7 @@ import { getValidIconClass } from '../ToolController_/ToolMetadataUtils' import { createLogger } from '../Logger_/Logger_' import { PANEL_STATE, FLOAT_POSITIONS } from '../PanelManager_/types/layout' import { toCssDimension, toPixelNumber } from '../PanelManager_/dimensions' +import { MAX_FLOAT_BOTTOM_OFFSET } from '../../types/dashboard' import './UserInterfaceModern_.css' const logger = createLogger('UserInterfaceModern') @@ -280,12 +281,12 @@ const UserInterfaceModern_ = { * @param {Array} panelsArr - Array of panel objects (parsed from config & PanelManager) * @param {string} style - The layout style ('overlay' | 'compact') * @param {string} theme - The theme identifier (e.g. 'default', 'disasters') - * @param {number|string} bottomOffset - Pixels the bottom float zones sit above the map's bottom edge; an unusable value means 0 + * @param {number|string} bottomOffset - Pixels the bottom float zones sit above the map's bottom edge; an unusable value means 0, and it is capped at MAX_FLOAT_BOTTOM_OFFSET */ init: function (panelsArr, style = 'overlay', theme = 'default', bottomOffset = 0) { panels = panelsArr layoutStyle = style - floatBottomOffset = toPixelNumber(bottomOffset) ?? 0 + floatBottomOffset = Math.min(toPixelNumber(bottomOffset) ?? 0, MAX_FLOAT_BOTTOM_OFFSET) // Dynamic import() so webpack emits one lazy CSS chunk per theme and // only the configured one is fetched (a sync require() here bundled diff --git a/src/essence/Validators/DashboardConfigValidator.js b/src/essence/Validators/DashboardConfigValidator.js index f4452b98a..c55bd83ca 100644 --- a/src/essence/Validators/DashboardConfigValidator.js +++ b/src/essence/Validators/DashboardConfigValidator.js @@ -14,7 +14,7 @@ import { PANEL_POSITION, PANEL_STATE, PANEL_LAYOUT_TYPE, FLOAT_POSITIONS } from '../Basics/PanelManager_/types/layout' import { TOOL_ORIENTATION } from '../Basics/ToolController_/types/tool' -import { VALID_MODES, VALID_LAYOUT_STYLES } from '../types/dashboard' +import { VALID_MODES, VALID_LAYOUT_STYLES, MAX_FLOAT_BOTTOM_OFFSET } from '../types/dashboard' import { isDimensionSet, toCssDimension, toPixelNumber, DIMENSION_HINT } from '../Basics/PanelManager_/dimensions' // Extract valid values from TypeScript constants @@ -78,8 +78,15 @@ export function validateModernConfig(config) { // Validate floatingBottomOffset (optional, defaults to 0) const offset = panelSettings.floatingBottomOffset - if (isDimensionSet(offset) && toPixelNumber(offset, { allowZero: true }) === null) { - warnings.push('panelSettings.floatingBottomOffset must be a non-negative number of pixels; using 0') + if (isDimensionSet(offset)) { + const offsetPx = toPixelNumber(offset, { allowZero: true }) + if (offsetPx === null) { + warnings.push('panelSettings.floatingBottomOffset must be a non-negative number of pixels; using 0') + } else if (offsetPx > MAX_FLOAT_BOTTOM_OFFSET) { + warnings.push( + `panelSettings.floatingBottomOffset must be at most ${MAX_FLOAT_BOTTOM_OFFSET} pixels; using ${MAX_FLOAT_BOTTOM_OFFSET}` + ) + } } // Validate theme (optional, defaults to 'default') diff --git a/src/essence/types/dashboard.ts b/src/essence/types/dashboard.ts index 3e285e461..7e2e49bb6 100644 --- a/src/essence/types/dashboard.ts +++ b/src/essence/types/dashboard.ts @@ -39,6 +39,12 @@ export type InterfaceMode = typeof VALID_MODES[number]; export const VALID_LAYOUT_STYLES = ['overlay', 'compact'] as const; export type LayoutStyle = typeof VALID_LAYOUT_STYLES[number]; +/** + * Largest panelSettings.floatingBottomOffset, in pixels. The Configure field + * (tab-ui-config.json) carries the same max. + */ +export const MAX_FLOAT_BOTTOM_OFFSET = 200; + /** * Mission, Site, Venue configuration object */ @@ -72,8 +78,12 @@ export interface PanelSettings { /** Layout style ('overlay' | 'compact') */ layoutStyle?: LayoutStyle; - /** Pixels the bottom floating panels sit above the map's bottom edge */ - floatingBottomOffset?: number; + /** + * Pixels the bottom floating panels sit above the map's bottom edge, as a + * number or a numeric/'px' string ('24', '24px'); '' (a cleared Configure + * field) means unset. Capped at MAX_FLOAT_BOTTOM_OFFSET. + */ + floatingBottomOffset?: number | string; } /** diff --git a/tests/unit/DashboardConfigValidator.spec.js b/tests/unit/DashboardConfigValidator.spec.js index 0a0be929c..a91d239f6 100644 --- a/tests/unit/DashboardConfigValidator.spec.js +++ b/tests/unit/DashboardConfigValidator.spec.js @@ -115,6 +115,10 @@ test.describe('DashboardConfigValidator', () => { expect(validateModernConfig(config('24')).warnings).toHaveLength(0); expect(validateModernConfig(config(Infinity)).warnings).toHaveLength(1); expect(validateModernConfig(config('1e400')).warnings).toHaveLength(1); + expect(validateModernConfig(config(200)).warnings).toHaveLength(0); + expect(validateModernConfig(config('201px')).warnings[0]).toMatch( + /floatingBottomOffset must be at most 200 pixels/ + ); const result = validateModernConfig(config(-5)); expect(result.valid).toBe(true); expect(result.warnings[0]).toMatch(/floatingBottomOffset must be a non-negative number/); From a24c79562075a18366346565cdf8aa3adf610a8c Mon Sep 17 00:00:00 2001 From: Sandesh Pandey Date: Fri, 16 Oct 2026 08:25:31 -0500 Subject: [PATCH 4/4] Layout: per-side floating panel offsets panelSettings.floatingOffset takes [top, right, bottom, left] pixels and insets the float regions wrapper by them, so every float zone moves with it and the center zones stay centered between the side offsets. Missing entries are 0, each is capped at 200px, and each side is limited to a quarter of the map. The Configure page has one field per side and the validator warns per entry. --- configure/src/metaconfigs/tab-ui-config.json | 33 ++++++++++-- docs/MISSION_CONFIG_REFERENCE.md | 2 +- .../UserInterface_/UserInterfaceModern_.css | 23 ++++---- .../UserInterface_/UserInterfaceModern_.js | 36 +++++++++---- .../Validators/DashboardConfigValidator.js | 35 +++++++++---- src/essence/modern.js | 4 +- src/essence/types/dashboard.ts | 21 +++++--- tests/unit/DashboardConfigValidator.spec.js | 34 ++++++------ ...rInterfaceModern.floatBottomOffset.spec.js | 34 ------------ .../userInterfaceModern.floatOffset.spec.js | 52 +++++++++++++++++++ 10 files changed, 183 insertions(+), 91 deletions(-) delete mode 100644 tests/unit/userInterfaceModern.floatBottomOffset.spec.js create mode 100644 tests/unit/userInterfaceModern.floatOffset.spec.js diff --git a/configure/src/metaconfigs/tab-ui-config.json b/configure/src/metaconfigs/tab-ui-config.json index ff55d7b83..4ec80f478 100644 --- a/configure/src/metaconfigs/tab-ui-config.json +++ b/configure/src/metaconfigs/tab-ui-config.json @@ -246,11 +246,38 @@ "name": "Floating Panels (Center Area)", "components": [ { - "field": "panelSettings.floatingBottomOffset", + "field": "panelSettings.floatingOffset.0", + "name": "Top Offset (px)", + "description": "Moves the top-left, top-center and top-right floating panels this many pixels below the map's top edge.", + "type": "number", + "width": 3, + "min": 0, + "max": 200 + }, + { + "field": "panelSettings.floatingOffset.1", + "name": "Right Offset (px)", + "description": "Moves the top-right and bottom-right floating panels this many pixels in from the map's right edge.", + "type": "number", + "width": 3, + "min": 0, + "max": 200 + }, + { + "field": "panelSettings.floatingOffset.2", "name": "Bottom Offset (px)", - "description": "Raises the bottom-left, bottom-center and bottom-right floating panels this many pixels above the map's bottom edge, e.g. to keep them clear of the basemap's logo and attribution.", + "description": "Moves the bottom-left, bottom-center and bottom-right floating panels this many pixels above the map's bottom edge, e.g. to keep them clear of the basemap's logo and attribution.", "type": "number", - "width": 6, + "width": 3, + "min": 0, + "max": 200 + }, + { + "field": "panelSettings.floatingOffset.3", + "name": "Left Offset (px)", + "description": "Moves the top-left and bottom-left floating panels this many pixels in from the map's left edge.", + "type": "number", + "width": 3, "min": 0, "max": 200 }, diff --git a/docs/MISSION_CONFIG_REFERENCE.md b/docs/MISSION_CONFIG_REFERENCE.md index 7cc9c9352..0db61a307 100644 --- a/docs/MISSION_CONFIG_REFERENCE.md +++ b/docs/MISSION_CONFIG_REFERENCE.md @@ -65,7 +65,7 @@ The mission configuration controls the layout and behavior of panels in modern i | Field | Type | Required | Description | |-------|------|----------|-------------| | `layoutStyle` | String | No | `"overlay"` (default) or `"compact"` layout style | -| `floatingBottomOffset` | Number | No | Pixels the bottom-left, bottom-center and bottom-right floating panels sit above the map's bottom edge, e.g. to clear the basemap's logo and attribution. Default `0`, max `200` | +| `floatingOffset` | Array | No | Pixels the floating panels sit in from each edge of the map, as `[top, right, bottom, left]`, e.g. `[0, 0, 32, 0]` to clear the basemap's logo and attribution. Missing entries are `0`; each entry is at most `200`. The center zones stay centered between the left and right offsets | | `panels` | Array | Yes | Array of panel configuration objects | ## Panel Configuration diff --git a/src/essence/Basics/UserInterface_/UserInterfaceModern_.css b/src/essence/Basics/UserInterface_/UserInterfaceModern_.css index 561216c81..b5e7ab5be 100644 --- a/src/essence/Basics/UserInterface_/UserInterfaceModern_.css +++ b/src/essence/Basics/UserInterface_/UserInterfaceModern_.css @@ -532,10 +532,18 @@ body.ui-is-dragging .ui-modern-grid .ui-panel { --ui-float-outer-gap: var(--theme-spacing-2, 1rem); } -/* Invisible wrapper that holds all float zones */ +/* Invisible wrapper that holds all float zones. It sits + --ui-float-offset-{top,right,bottom,left} in from the map's edges + (panelSettings.floatingOffset), so every zone moves with it and the center + zones stay centered between the side offsets. Each side is capped at a + quarter of the map's size so large offsets on a small viewport still leave + the zones room. */ .ui-float-regions { position: absolute; - inset: 0; + top: min(var(--ui-float-offset-top, 0px), 25%); + right: min(var(--ui-float-offset-right, 0px), 25%); + bottom: min(var(--ui-float-offset-bottom, 0px), 25%); + left: min(var(--ui-float-offset-left, 0px), 25%); pointer-events: none; z-index: 5; } @@ -555,16 +563,13 @@ body.ui-is-dragging .ui-modern-grid .ui-panel { padding: var(--ui-float-outer-gap); } -/* Zone positions. The bottom zones sit --ui-float-bottom-offset above the - map's bottom edge (panelSettings.floatingBottomOffset), capped at half the - map's height so a large offset on a short viewport cannot push them into - the top zones or off the map. */ +/* Zone positions */ .ui-float-zone-top-left { top: 0; left: 0; } .ui-float-zone-top-center { top: 0; left: 50%; transform: translateX(-50%); } .ui-float-zone-top-right { top: 0; right: 0; } -.ui-float-zone-bottom-left { bottom: min(var(--ui-float-bottom-offset, 0px), 50%); left: 0; } -.ui-float-zone-bottom-center { bottom: min(var(--ui-float-bottom-offset, 0px), 50%); left: 50%; transform: translateX(-50%); } -.ui-float-zone-bottom-right { bottom: min(var(--ui-float-bottom-offset, 0px), 50%); right: 0; } +.ui-float-zone-bottom-left { bottom: 0; left: 0; } +.ui-float-zone-bottom-center { bottom: 0; left: 50%; transform: translateX(-50%); } +.ui-float-zone-bottom-right { bottom: 0; right: 0; } /* Float panels re-enable pointer events */ .ui-float-panel { diff --git a/src/essence/Basics/UserInterface_/UserInterfaceModern_.js b/src/essence/Basics/UserInterface_/UserInterfaceModern_.js index 1bb8c41f2..d495450bf 100644 --- a/src/essence/Basics/UserInterface_/UserInterfaceModern_.js +++ b/src/essence/Basics/UserInterface_/UserInterfaceModern_.js @@ -6,7 +6,7 @@ import { getValidIconClass } from '../ToolController_/ToolMetadataUtils' import { createLogger } from '../Logger_/Logger_' import { PANEL_STATE, FLOAT_POSITIONS } from '../PanelManager_/types/layout' import { toCssDimension, toPixelNumber } from '../PanelManager_/dimensions' -import { MAX_FLOAT_BOTTOM_OFFSET } from '../../types/dashboard' +import { FLOAT_OFFSET_SIDES, MAX_FLOAT_OFFSET } from '../../types/dashboard' import './UserInterfaceModern_.css' const logger = createLogger('UserInterfaceModern') @@ -17,7 +17,7 @@ const DEFAULT_MAX_PANEL_SIZE = 9999; // --- Module-Level State --- let panels = [] let layoutStyle = '' -let floatBottomOffset = 0 +let floatOffsets = [0, 0, 0, 0] let toolLoadQueue = [] let cleanupLayoutListener = null let _resizeObserver = null @@ -113,13 +113,27 @@ const _createPanelHeader = (panel) => { return header } -const _renderFloatRegions = (floatPanels, bottomOffset = 0) => { +/** + * panelSettings.floatingOffset as four pixel counts in [top, right, bottom, + * left] order. A missing or unusable entry is 0; each is capped at + * MAX_FLOAT_OFFSET. + */ +const _toFloatOffsets = (value) => { + const entries = Array.isArray(value) ? value : [] + return FLOAT_OFFSET_SIDES.map((_, i) => + Math.min(toPixelNumber(entries[i], { allowZero: true }) ?? 0, MAX_FLOAT_OFFSET) + ) +} + +const _renderFloatRegions = (floatPanels, offsets = []) => { if (!floatPanels || floatPanels.length === 0) return null const fragment = $('
') - if (bottomOffset > 0) { - fragment[0].style.setProperty('--ui-float-bottom-offset', `${bottomOffset}px`) - } + FLOAT_OFFSET_SIDES.forEach((side, i) => { + if (offsets[i] > 0) { + fragment[0].style.setProperty(`--ui-float-offset-${side}`, `${offsets[i]}px`) + } + }) // Group panels by their float position (validator enforces at most one per position) const byPosition = {} @@ -281,12 +295,12 @@ const UserInterfaceModern_ = { * @param {Array} panelsArr - Array of panel objects (parsed from config & PanelManager) * @param {string} style - The layout style ('overlay' | 'compact') * @param {string} theme - The theme identifier (e.g. 'default', 'disasters') - * @param {number|string} bottomOffset - Pixels the bottom float zones sit above the map's bottom edge; an unusable value means 0, and it is capped at MAX_FLOAT_BOTTOM_OFFSET + * @param {Array} floatOffset - Pixels the float zones sit in from each edge of the map, as [top, right, bottom, left]; a missing or unusable entry means 0, and each is capped at MAX_FLOAT_OFFSET */ - init: function (panelsArr, style = 'overlay', theme = 'default', bottomOffset = 0) { + init: function (panelsArr, style = 'overlay', theme = 'default', floatOffset = []) { panels = panelsArr layoutStyle = style - floatBottomOffset = Math.min(toPixelNumber(bottomOffset) ?? 0, MAX_FLOAT_BOTTOM_OFFSET) + floatOffsets = _toFloatOffsets(floatOffset) // Dynamic import() so webpack emits one lazy CSS chunk per theme and // only the configured one is fetched (a sync require() here bundled @@ -620,7 +634,7 @@ const UserInterfaceModern_ = { } } - const floatRegions = _renderFloatRegions(floatPanels, floatBottomOffset) + const floatRegions = _renderFloatRegions(floatPanels, floatOffsets) if (floatRegions) { centralArea.append(floatRegions) } @@ -877,4 +891,4 @@ const UserInterfaceModern_ = { } export default UserInterfaceModern_ -export { _createPanelHeader, _createPanelIconTray, _renderRegion, _renderFloatRegions } +export { _createPanelHeader, _createPanelIconTray, _renderRegion, _renderFloatRegions, _toFloatOffsets } diff --git a/src/essence/Validators/DashboardConfigValidator.js b/src/essence/Validators/DashboardConfigValidator.js index c55bd83ca..48f1ec51f 100644 --- a/src/essence/Validators/DashboardConfigValidator.js +++ b/src/essence/Validators/DashboardConfigValidator.js @@ -14,7 +14,7 @@ import { PANEL_POSITION, PANEL_STATE, PANEL_LAYOUT_TYPE, FLOAT_POSITIONS } from '../Basics/PanelManager_/types/layout' import { TOOL_ORIENTATION } from '../Basics/ToolController_/types/tool' -import { VALID_MODES, VALID_LAYOUT_STYLES, MAX_FLOAT_BOTTOM_OFFSET } from '../types/dashboard' +import { VALID_MODES, VALID_LAYOUT_STYLES, FLOAT_OFFSET_SIDES, MAX_FLOAT_OFFSET } from '../types/dashboard' import { isDimensionSet, toCssDimension, toPixelNumber, DIMENSION_HINT } from '../Basics/PanelManager_/dimensions' // Extract valid values from TypeScript constants @@ -76,16 +76,33 @@ export function validateModernConfig(config) { ) } - // Validate floatingBottomOffset (optional, defaults to 0) - const offset = panelSettings.floatingBottomOffset - if (isDimensionSet(offset)) { - const offsetPx = toPixelNumber(offset, { allowZero: true }) - if (offsetPx === null) { - warnings.push('panelSettings.floatingBottomOffset must be a non-negative number of pixels; using 0') - } else if (offsetPx > MAX_FLOAT_BOTTOM_OFFSET) { + // Validate floatingOffset (optional [top, right, bottom, left], each defaults to 0) + const offsets = panelSettings.floatingOffset + if (offsets !== undefined && offsets !== null) { + if (!Array.isArray(offsets)) { warnings.push( - `panelSettings.floatingBottomOffset must be at most ${MAX_FLOAT_BOTTOM_OFFSET} pixels; using ${MAX_FLOAT_BOTTOM_OFFSET}` + 'panelSettings.floatingOffset must be an array of [top, right, bottom, left] pixels; using 0 on every side' ) + } else { + if (offsets.length > FLOAT_OFFSET_SIDES.length) { + warnings.push( + `panelSettings.floatingOffset has more than ${FLOAT_OFFSET_SIDES.length} entries; extra entries are ignored` + ) + } + FLOAT_OFFSET_SIDES.forEach((side, i) => { + const offset = offsets[i] + if (!isDimensionSet(offset)) return + const offsetPx = toPixelNumber(offset, { allowZero: true }) + if (offsetPx === null) { + warnings.push( + `panelSettings.floatingOffset[${i}] (${side}) must be a non-negative number of pixels; using 0` + ) + } else if (offsetPx > MAX_FLOAT_OFFSET) { + warnings.push( + `panelSettings.floatingOffset[${i}] (${side}) must be at most ${MAX_FLOAT_OFFSET} pixels; using ${MAX_FLOAT_OFFSET}` + ) + } + }) } } diff --git a/src/essence/modern.js b/src/essence/modern.js index 10a6d399a..1f7a259f1 100644 --- a/src/essence/modern.js +++ b/src/essence/modern.js @@ -235,10 +235,10 @@ class ModernInterface { const layoutStyle = this.configData?.panelSettings?.layoutStyle || 'overlay' const theme = this.configData?.msv?.theme || 'default' - const floatBottomOffset = this.configData?.panelSettings?.floatingBottomOffset + const floatOffset = this.configData?.panelSettings?.floatingOffset // Initialize the User Interface with the sorted panels from PanelManager - UserInterfaceModern_.init(activePanels, layoutStyle, theme, floatBottomOffset) + UserInterfaceModern_.init(activePanels, layoutStyle, theme, floatOffset) // TimeControl must initialize ahead of Map_ because it seeds each // time-enabled layer's time window, which Map_.init reads when it diff --git a/src/essence/types/dashboard.ts b/src/essence/types/dashboard.ts index 7e2e49bb6..560c03ec3 100644 --- a/src/essence/types/dashboard.ts +++ b/src/essence/types/dashboard.ts @@ -40,10 +40,16 @@ export const VALID_LAYOUT_STYLES = ['overlay', 'compact'] as const; export type LayoutStyle = typeof VALID_LAYOUT_STYLES[number]; /** - * Largest panelSettings.floatingBottomOffset, in pixels. The Configure field - * (tab-ui-config.json) carries the same max. + * The sides panelSettings.floatingOffset lists, in CSS shorthand order. */ -export const MAX_FLOAT_BOTTOM_OFFSET = 200; +export const FLOAT_OFFSET_SIDES = ['top', 'right', 'bottom', 'left'] as const; +export type FloatOffsetSide = typeof FLOAT_OFFSET_SIDES[number]; + +/** + * Largest value of each panelSettings.floatingOffset entry, in pixels. The + * Configure fields (tab-ui-config.json) carry the same max. + */ +export const MAX_FLOAT_OFFSET = 200; /** * Mission, Site, Venue configuration object @@ -79,11 +85,12 @@ export interface PanelSettings { layoutStyle?: LayoutStyle; /** - * Pixels the bottom floating panels sit above the map's bottom edge, as a - * number or a numeric/'px' string ('24', '24px'); '' (a cleared Configure - * field) means unset. Capped at MAX_FLOAT_BOTTOM_OFFSET. + * Pixels the floating panels sit in from each edge of the map, as + * [top, right, bottom, left]. Each entry is a number or a numeric/'px' + * string ('24', '24px'); a missing, null or '' entry (a cleared Configure + * field) means 0. Each entry is capped at MAX_FLOAT_OFFSET. */ - floatingBottomOffset?: number | string; + floatingOffset?: Array; } /** diff --git a/tests/unit/DashboardConfigValidator.spec.js b/tests/unit/DashboardConfigValidator.spec.js index a91d239f6..c34149d1c 100644 --- a/tests/unit/DashboardConfigValidator.spec.js +++ b/tests/unit/DashboardConfigValidator.spec.js @@ -91,10 +91,10 @@ test.describe('DashboardConfigValidator', () => { expect(result.errors[0]).toMatch(/panelSettings.layoutStyle must be one of/); }); - test('warns on an unusable floatingBottomOffset and accepts a pixel count', () => { - const config = (floatingBottomOffset) => ({ + test('warns on an unusable floatingOffset and accepts pixel counts per side', () => { + const config = (floatingOffset) => ({ panelSettings: { - floatingBottomOffset, + floatingOffset, panels: [ { id: 'panel-1', @@ -109,19 +109,23 @@ test.describe('DashboardConfigValidator', () => { ] } }); - - expect(validateModernConfig(config(24)).warnings).toHaveLength(0); - expect(validateModernConfig(config('')).warnings).toHaveLength(0); - expect(validateModernConfig(config('24')).warnings).toHaveLength(0); - expect(validateModernConfig(config(Infinity)).warnings).toHaveLength(1); - expect(validateModernConfig(config('1e400')).warnings).toHaveLength(1); - expect(validateModernConfig(config(200)).warnings).toHaveLength(0); - expect(validateModernConfig(config('201px')).warnings[0]).toMatch( - /floatingBottomOffset must be at most 200 pixels/ + const warningsFor = (offset) => validateModernConfig(config(offset)).warnings; + + expect(warningsFor(undefined)).toHaveLength(0); + expect(warningsFor([0, 0, 24, 0])).toHaveLength(0); + expect(warningsFor(['8', '16px', 200, 0])).toHaveLength(0); + expect(warningsFor([null, null, 32])).toHaveLength(0); + expect(warningsFor(['', '', '', ''])).toHaveLength(0); + expect(warningsFor([Infinity, 0, '1e400', 0])).toHaveLength(2); + expect(warningsFor([0, 0, '201px', 0])[0]).toMatch( + /floatingOffset\[2\] \(bottom\) must be at most 200 pixels/ ); - const result = validateModernConfig(config(-5)); - expect(result.valid).toBe(true); - expect(result.warnings[0]).toMatch(/floatingBottomOffset must be a non-negative number/); + expect(warningsFor([0, 0, 0, -5])[0]).toMatch( + /floatingOffset\[3\] \(left\) must be a non-negative number/ + ); + expect(warningsFor(24)[0]).toMatch(/floatingOffset must be an array/); + expect(warningsFor([1, 2, 3, 4, 5])[0]).toMatch(/more than 4 entries/); + expect(validateModernConfig(config([0, 0, 0, -5])).valid).toBe(true); }); test('catches defaultState not in allowedStates', () => { diff --git a/tests/unit/userInterfaceModern.floatBottomOffset.spec.js b/tests/unit/userInterfaceModern.floatBottomOffset.spec.js deleted file mode 100644 index 96b1f3123..000000000 --- a/tests/unit/userInterfaceModern.floatBottomOffset.spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import { test, expect, vi } from 'vitest' -vi.mock('../../src/essence/Basics/Viewer_/Viewer_', () => ({ default: {} })) - -import PanelManager_ from '../../src/essence/Basics/PanelManager_/PanelManager_.ts' - -// The bottom float zones read --ui-float-bottom-offset, which the float -// regions wrapper carries from panelSettings.floatingBottomOffset. - -const { _renderFloatRegions } = await import( - '../../src/essence/Basics/UserInterface_/UserInterfaceModern_.js' -) - -const floatPanels = () => { - PanelManager_.clear() - PanelManager_.registerPanel({ - id: 'scale', - position: 'float-bottom-left', - layoutType: 'stacked', - priority: 0, - stateConstraints: { allowedStates: ['expanded'], defaultState: 'expanded' }, - }) - return [PanelManager_.getPanelState('scale')] -} - -const offsetOf = (regions) => regions[0].style.getPropertyValue('--ui-float-bottom-offset') - -test('a configured offset raises the bottom float zones', () => { - expect(offsetOf(_renderFloatRegions(floatPanels(), 24))).toBe('24px') -}) - -test('without an offset the zones stay on the edge', () => { - expect(offsetOf(_renderFloatRegions(floatPanels()))).toBe('') - expect(offsetOf(_renderFloatRegions(floatPanels(), 0))).toBe('') -}) diff --git a/tests/unit/userInterfaceModern.floatOffset.spec.js b/tests/unit/userInterfaceModern.floatOffset.spec.js new file mode 100644 index 000000000..c7a19bcf0 --- /dev/null +++ b/tests/unit/userInterfaceModern.floatOffset.spec.js @@ -0,0 +1,52 @@ +import { test, expect, vi } from 'vitest' +vi.mock('../../src/essence/Basics/Viewer_/Viewer_', () => ({ default: {} })) + +import PanelManager_ from '../../src/essence/Basics/PanelManager_/PanelManager_.ts' + +// The float regions wrapper carries panelSettings.floatingOffset +// ([top, right, bottom, left]) as --ui-float-offset-{side}, and the CSS insets +// the wrapper by those values. + +const { _renderFloatRegions, _toFloatOffsets } = await import( + '../../src/essence/Basics/UserInterface_/UserInterfaceModern_.js' +) + +const floatPanels = () => { + PanelManager_.clear() + PanelManager_.registerPanel({ + id: 'scale', + position: 'float-bottom-left', + layoutType: 'stacked', + priority: 0, + stateConstraints: { allowedStates: ['expanded'], defaultState: 'expanded' }, + }) + return [PanelManager_.getPanelState('scale')] +} + +const offsetsOf = (regions) => + ['top', 'right', 'bottom', 'left'].map((side) => + regions[0].style.getPropertyValue(`--ui-float-offset-${side}`) + ) + +test('each configured side insets the float regions', () => { + expect(offsetsOf(_renderFloatRegions(floatPanels(), [8, 16, 32, 4]))).toEqual([ + '8px', + '16px', + '32px', + '4px', + ]) +}) + +test('without offsets the zones stay on the edges', () => { + expect(offsetsOf(_renderFloatRegions(floatPanels()))).toEqual(['', '', '', '']) + expect(offsetsOf(_renderFloatRegions(floatPanels(), [0, 0, 0, 0]))).toEqual(['', '', '', '']) +}) + +test('the configured array is read as four capped pixel counts', () => { + expect(_toFloatOffsets([0, '12', '24px', 4])).toEqual([0, 12, 24, 4]) + expect(_toFloatOffsets([null, null, 32])).toEqual([0, 0, 32, 0]) + expect(_toFloatOffsets(['', -5, 'abc', 500])).toEqual([0, 0, 0, 200]) + expect(_toFloatOffsets([1, 2, 3, 4, 5])).toEqual([1, 2, 3, 4]) + expect(_toFloatOffsets(undefined)).toEqual([0, 0, 0, 0]) + expect(_toFloatOffsets(24)).toEqual([0, 0, 0, 0]) +})