Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions configure/src/metaconfigs/tab-ui-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,42 @@
{
"name": "Floating Panels (Center Area)",
"components": [
{
"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": "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": 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
},
{
"field": "panelSettings.floatingPanels",
"name": "Floating Panels",
Expand Down
1 change: 1 addition & 0 deletions docs/MISSION_CONFIG_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| `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
Expand Down
12 changes: 10 additions & 2 deletions src/essence/Basics/UserInterface_/UserInterfaceModern_.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
29 changes: 25 additions & 4 deletions src/essence/Basics/UserInterface_/UserInterfaceModern_.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 { FLOAT_OFFSET_SIDES, MAX_FLOAT_OFFSET } from '../../types/dashboard'
import './UserInterfaceModern_.css'

const logger = createLogger('UserInterfaceModern')
Expand All @@ -16,6 +17,7 @@ const DEFAULT_MAX_PANEL_SIZE = 9999;
// --- Module-Level State ---
let panels = []
let layoutStyle = ''
let floatOffsets = [0, 0, 0, 0]
let toolLoadQueue = []
let cleanupLayoutListener = null
let _resizeObserver = null
Expand Down Expand Up @@ -111,10 +113,27 @@ const _createPanelHeader = (panel) => {
return header
}

const _renderFloatRegions = (floatPanels) => {
/**
* 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 = $('<div class="ui-float-regions"></div>')
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 = {}
Expand Down Expand Up @@ -276,10 +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 {Array<number|string>} 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') {
init: function (panelsArr, style = 'overlay', theme = 'default', floatOffset = []) {
panels = panelsArr
layoutStyle = style
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
Expand Down Expand Up @@ -613,7 +634,7 @@ const UserInterfaceModern_ = {
}
}

const floatRegions = _renderFloatRegions(floatPanels)
const floatRegions = _renderFloatRegions(floatPanels, floatOffsets)
if (floatRegions) {
centralArea.append(floatRegions)
}
Expand Down Expand Up @@ -870,4 +891,4 @@ const UserInterfaceModern_ = {
}

export default UserInterfaceModern_
export { _createPanelHeader, _createPanelIconTray, _renderRegion }
export { _createPanelHeader, _createPanelIconTray, _renderRegion, _renderFloatRegions, _toFloatOffsets }
32 changes: 31 additions & 1 deletion src/essence/Validators/DashboardConfigValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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
Expand Down Expand Up @@ -76,6 +76,36 @@ export function validateModernConfig(config) {
)
}

// 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.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}`
)
}
})
}
}

// 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')
Expand Down
3 changes: 2 additions & 1 deletion src/essence/modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,10 @@ class ModernInterface {

const layoutStyle = this.configData?.panelSettings?.layoutStyle || 'overlay'
const theme = this.configData?.msv?.theme || 'default'
const floatOffset = this.configData?.panelSettings?.floatingOffset

// Initialize the User Interface with the sorted panels from PanelManager
UserInterfaceModern_.init(activePanels, layoutStyle, theme)
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
Expand Down
20 changes: 20 additions & 0 deletions src/essence/types/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ export type InterfaceMode = typeof VALID_MODES[number];
export const VALID_LAYOUT_STYLES = ['overlay', 'compact'] as const;
export type LayoutStyle = typeof VALID_LAYOUT_STYLES[number];

/**
* The sides panelSettings.floatingOffset lists, in CSS shorthand order.
*/
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
*/
Expand Down Expand Up @@ -71,6 +83,14 @@ export interface PanelSettings {

/** Layout style ('overlay' | 'compact') */
layoutStyle?: LayoutStyle;

/**
* 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.
*/
floatingOffset?: Array<number | string | null>;
}

/**
Expand Down
37 changes: 37 additions & 0 deletions tests/unit/DashboardConfigValidator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,43 @@ test.describe('DashboardConfigValidator', () => {
expect(result.errors[0]).toMatch(/panelSettings.layoutStyle must be one of/);
});

test('warns on an unusable floatingOffset and accepts pixel counts per side', () => {
const config = (floatingOffset) => ({
panelSettings: {
floatingOffset,
panels: [
{
id: 'panel-1',
position: 'left',
priority: 1,
layoutType: 'stacked',
stateConstraints: {
allowedStates: ['expanded'],
defaultState: 'expanded'
}
}
]
}
});
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/
);
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', () => {
const config = {
panelSettings: {
Expand Down
52 changes: 52 additions & 0 deletions tests/unit/userInterfaceModern.floatOffset.spec.js
Original file line number Diff line number Diff line change
@@ -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])
})
Loading