From 6f3bf0d0e3f15f0ca622b30a19ac6b9667542ae5 Mon Sep 17 00:00:00 2001 From: Sandesh Pandey Date: Tue, 16 Jun 2026 15:27:20 -0500 Subject: [PATCH 1/3] Initial implementation of floating panel --- configure/package-lock.json | 4 +- configure/src/metaconfigs/tab-ui-config.json | 129 ++ docs/MISSION_CONFIG_REFERENCE.md | 53 + package-lock.json | 1502 ++++++++++++++++- package.json | 8 +- .../Basics/PanelManager_/types/layout.ts | 19 + .../Basics/PanelManager_/types/panel.ts | 14 + .../UserInterface_/UserInterfaceModern_.css | 86 +- .../UserInterface_/UserInterfaceModern_.js | 100 +- .../Validators/DashboardConfigValidator.js | 95 +- src/essence/modern.js | 7 +- src/essence/types/dashboard.ts | 3 + 12 files changed, 1974 insertions(+), 46 deletions(-) diff --git a/configure/package-lock.json b/configure/package-lock.json index 481959f16..628b3dda3 100644 --- a/configure/package-lock.json +++ b/configure/package-lock.json @@ -1,12 +1,12 @@ { "name": "configure", - "version": "0.1.0", + "version": "4.2.9-20260211", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "configure", - "version": "0.1.0", + "version": "4.2.9-20260211", "dependencies": { "@codemirror/lang-json": "^6.0.1", "@emotion/react": "^11.11.1", diff --git a/configure/src/metaconfigs/tab-ui-config.json b/configure/src/metaconfigs/tab-ui-config.json index 50d9d4fa2..1a30ffc2d 100644 --- a/configure/src/metaconfigs/tab-ui-config.json +++ b/configure/src/metaconfigs/tab-ui-config.json @@ -235,6 +235,135 @@ } ] }, + { + "name": "Floating Panels (Center Area)", + "components": [ + { + "field": "panelSettings.floatingPanels", + "name": "Floating Panels", + "description": "Panels that float over the center map area. Each floating panel wraps a single tool and is positioned at one of six anchor points. Multiple panels at the same position stack vertically.", + "type": "objectarray", + "width": 24, + "object": [ + { + "field": "id", + "name": "ID", + "description": "Unique identifier for this floating panel (e.g., 'timeline-panel', 'legend-float').", + "type": "text", + "width": 6 + }, + { + "field": "position", + "name": "Position", + "description": "Where the panel floats within the map area.", + "type": "dropdown", + "options": [ + "float-top-left", + "float-top-center", + "float-top-right", + "float-bottom-left", + "float-bottom-center", + "float-bottom-right" + ], + "default": "float-bottom-center", + "width": 6 + }, + { + "field": "priority", + "name": "Priority", + "description": "Stacking order when multiple panels share the same position (lower number appears first/top).", + "type": "number", + "default": 0, + "width": 4, + "min": 0 + }, + { + "field": "hasHeader", + "name": "Has Header", + "description": "Whether the panel shows a title bar with control buttons.", + "type": "checkbox", + "width": 4, + "defaultChecked": false + }, + { + "field": "title", + "name": "Title", + "description": "Display title shown in the header (only relevant when Has Header is enabled).", + "type": "text", + "width": 4 + }, + { + "field": "stateConstraints.allowedStates", + "name": "Allowed States", + "description": "Permitted states for this panel. Floating panels support collapsed (hidden) and expanded (visible) only.", + "type": "multiselect", + "options": ["collapsed", "expanded"], + "default": ["collapsed", "expanded"], + "width": 12 + }, + { + "field": "stateConstraints.defaultState", + "name": "Default State", + "description": "Initial visibility state when the panel is created.", + "type": "dropdown", + "options": ["collapsed", "expanded"], + "default": "expanded", + "width": 12 + }, + { + "field": "dimensions.defaultWidth", + "name": "Default Width", + "description": "Initial width of each tool card. Accepts any CSS unit (e.g. '300px', '40%', '20vw'). Leave empty to size to content.", + "type": "text", + "width": 4 + }, + { + "field": "dimensions.defaultHeight", + "name": "Default Height", + "description": "Initial height of each tool card. Accepts any CSS unit (e.g. '200px', '30%', '25vh'). Leave empty to size to content.", + "type": "text", + "width": 4 + }, + { + "field": "dimensions.minWidth", + "name": "Min Width", + "description": "Minimum width of each tool card. Accepts any CSS unit (e.g. '100px', '10%').", + "type": "text", + "width": 4 + }, + { + "field": "dimensions.maxWidth", + "name": "Max Width", + "description": "Maximum width of each tool card. Accepts any CSS unit (e.g. '600px', '50%').", + "type": "text", + "width": 4 + }, + { + "field": "dimensions.minHeight", + "name": "Min Height", + "description": "Minimum height of each tool card. Accepts any CSS unit (e.g. '100px', '10vh').", + "type": "text", + "width": 4 + }, + { + "field": "dimensions.maxHeight", + "name": "Max Height", + "description": "Maximum height of each tool card. Accepts any CSS unit (e.g. '400px', '40%', '50vh'). Useful for preventing top/bottom float zones from overlapping.", + "type": "text", + "width": 4 + }, + { + "field": "panelTools", + "name": "Tools", + "description": "Tool names to display in this floating panel. Each tool renders as its own card with a gap between cards. Must match tool names in the mission's tools array (e.g., 'Timeline', 'Legend').", + "type": "textarray", + "default": [], + "width": 24 + } + ] + } + ] + }, { "name": "Map Panel", "components": [ diff --git a/docs/MISSION_CONFIG_REFERENCE.md b/docs/MISSION_CONFIG_REFERENCE.md index 18337a049..c18d85985 100644 --- a/docs/MISSION_CONFIG_REFERENCE.md +++ b/docs/MISSION_CONFIG_REFERENCE.md @@ -109,6 +109,37 @@ Determines where the panel is located: - **`"left"`**: Vertical panel on the left side - **`"right"`**: Vertical panel on the right side +**Floating positions** (render inside the center map area as overlays): + +- **`"float-top-left"`**: Floats in the top-left corner of the map +- **`"float-top-center"`**: Floats at the top-center of the map +- **`"float-top-right"`**: Floats in the top-right corner of the map +- **`"float-bottom-left"`**: Floats in the bottom-left corner of the map +- **`"float-bottom-center"`**: Floats at the bottom-center of the map +- **`"float-bottom-right"`**: Floats in the bottom-right corner of the map + +**Floating panel rules**: +- Multiple tools can be assigned to one floating panel — each tool renders as its own card with a gap between cards +- Multiple floating panels at the same position stack **vertically** in declaration order +- Supported states: `"collapsed"` and `"expanded"` only (`"iconified"` and `"focused"` are not supported) +- `layoutType` is ignored for floating panels +- `capabilities.resizable` is not supported for floating panels +- In `overlay` layout style the panel has rounded corners; in `compact` layout style it has sharp corners +- A gap between cards is applied automatically; no outer gap is added in overlay mode (the panel's own margin handles it) + +**Floating panel `dimensions` fields**: + +All values accept any CSS unit string (e.g. `"40%"`, `"50vh"`, `"300px"`) or a plain number (treated as `px`). Dimensions apply to each individual tool card. + +| Field | Description | +|---|---| +| `defaultWidth` | Initial width (omit to size to content) | +| `defaultHeight` | Initial height (omit to size to content) | +| `minWidth` | Minimum width | +| `maxWidth` | Maximum width | +| `minHeight` | Minimum height | +| `maxHeight` | Maximum height — useful for capping top/bottom float zones so they never overlap (e.g. top panel `maxHeight: "40%"`, bottom panel `maxHeight: "50%"` leaves a 10% gap) | + ### priority Controls the order in which panels claim viewport space: @@ -441,6 +472,28 @@ Full mission configuration with four panels: "expandedSize": 300 }, "tools": ["Draw", "RasterTile", "Identifier"] + }, + { + "id": "timeline-panel", + "position": "float-bottom-center", + "priority": 0, + "layoutType": "stacked", + "stateConstraints": { + "allowedStates": ["collapsed", "expanded"], + "defaultState": "expanded" + }, + "tools": ["Timeline"] + }, + { + "id": "legend-float-panel", + "position": "float-bottom-right", + "priority": 0, + "layoutType": "stacked", + "stateConstraints": { + "allowedStates": ["collapsed", "expanded"], + "defaultState": "expanded" + }, + "tools": ["Legend"] } ] }, diff --git a/package-lock.json b/package-lock.json index 0660886ca..689a7d00a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,6 @@ "@trussworks/react-uswds": "^11.0.1", "@turf/turf": "^6.5.0", "@uswds/uswds": "^3.13.0", - "@uswds/uswds": "^3.13.0", "bcryptjs": "^2.4.3", "bluebird": "^3.7.2", "busboy": "1.6", @@ -111,6 +110,7 @@ "sequelize": "^6.33.0", "sharp": "^0.31.2", "showdown": "^2.1.0", + "shpjs": "^6.2.0", "snap-bbox": "^0.2.0", "sortablejs": "^1.15.0", "swagger-ui-express": "^4.1.4", @@ -134,6 +134,7 @@ "@babel/plugin-transform-private-property-in-object": "^7.22.11", "@playwright/test": "^1.57.0", "@svgr/webpack": "^8.1.0", + "@types/jest": "^30.0.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^2.10.0", @@ -3308,6 +3309,53 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "node_modules/@jest/diff-sequences": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", + "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -6942,10 +6990,11 @@ } }, "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.1", @@ -6957,14 +7006,26 @@ } }, "node_modules/@types/istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-kv43F9eb3Lhj+lr/Hn6OcLCs/sSM8bt+fIaP11rCYngfV6NVjzWXJ17owQtDQTL9tQ8WSLUrGsSJ6rJz0F1w1A==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -7095,6 +7156,13 @@ "@types/node": "*" } }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/supercluster": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", @@ -7148,10 +7216,11 @@ } }, "node_modules/@types/yargs": { - "version": "17.0.28", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.28.tgz", - "integrity": "sha512-N3e3fkS86hNhtk6BEnc0rj3zcehaxx8QWhCROJkqpl5Zaoi7nAic3jH8q94jVD3zu5LGk+PUB6KAiDmimYOEQw==", + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } @@ -9061,6 +9130,12 @@ "node": ">=10.16.0" } }, + "node_modules/but-unzip": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/but-unzip/-/but-unzip-0.1.10.tgz", + "integrity": "sha512-hLfQ9WlUimmv/okzsRl6AYG3Ew5HNWhWgUslSR93FsDdeL0MAoQvmC/BJfs35lqEAO5t/QD7Y4vCFcPJtijt3A==", + "license": "Apache-2.0" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -13395,6 +13470,176 @@ "node": ">=6" } }, + "node_modules/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/expect/node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/expect/node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/expect/node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/expect/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/expect/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/expect/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/expect/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/expect/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/expect/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/expect/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/expect/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -18474,6 +18719,521 @@ "resolved": "https://registry.npmjs.org/jdataview/-/jdataview-2.5.0.tgz", "integrity": "sha512-ZJop3D5nyDcWPBPv4NPnhCvx3HgQNsCXMfw8gpNKY16BobgxmVF+kJ08aHuqk6bJQVeL2mkf6nDCcZPMompalw==" }, + "node_modules/jest-diff": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.4.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-mock/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-mock/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-mock/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-mock/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -21364,6 +22124,12 @@ "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" }, + "node_modules/parsedbf": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parsedbf/-/parsedbf-2.0.0.tgz", + "integrity": "sha512-WNjKn/cwgGBkXqQLif+2VMEahcRHkBRU0/RfBWZ7Vj7snRNNW63yW1mVuuHRDyXTRxuGCzAHHBcr/Fn+U/bXjQ==", + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -23796,6 +24562,55 @@ "renderkid": "^3.0.0" } }, + "node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -24570,6 +25385,22 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "dev": true }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "dev": true, + "license": "MIT" + }, "node_modules/react-pdf": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-9.2.1.tgz", @@ -26195,6 +27026,17 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/shpjs": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/shpjs/-/shpjs-6.2.0.tgz", + "integrity": "sha512-8cR/RKYHQepmVyBMtzZQ+1bnSbWrtLXS6aoEJmpUlOSHtSUddterebVxYmIWq2g9kOEX9jm2kjHiikyPX7cNQA==", + "license": "MIT", + "dependencies": { + "but-unzip": "^0.1.4", + "parsedbf": "^2.0.0", + "proj4": "^2.1.4" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -26808,6 +27650,29 @@ "node": "*" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -32873,6 +33738,37 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "@jest/diff-sequences": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "dev": true + }, + "@jest/expect-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", + "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", + "dev": true, + "requires": { + "@jest/get-type": "30.1.0" + } + }, + "@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true + }, + "@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-regex-util": "30.4.0" + } + }, "@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -35566,9 +36462,9 @@ } }, "@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, "@types/istanbul-lib-report": { @@ -35581,14 +36477,24 @@ } }, "@types/istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-kv43F9eb3Lhj+lr/Hn6OcLCs/sSM8bt+fIaP11rCYngfV6NVjzWXJ17owQtDQTL9tQ8WSLUrGsSJ6rJz0F1w1A==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, "requires": { "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "requires": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, "@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -35711,6 +36617,12 @@ "@types/node": "*" } }, + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, "@types/supercluster": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", @@ -35760,9 +36672,9 @@ } }, "@types/yargs": { - "version": "17.0.28", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.28.tgz", - "integrity": "sha512-N3e3fkS86hNhtk6BEnc0rj3zcehaxx8QWhCROJkqpl5Zaoi7nAic3jH8q94jVD3zu5LGk+PUB6KAiDmimYOEQw==", + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -37200,6 +38112,11 @@ "streamsearch": "^1.1.0" } }, + "but-unzip": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/but-unzip/-/but-unzip-0.1.10.tgz", + "integrity": "sha512-hLfQ9WlUimmv/okzsRl6AYG3Ew5HNWhWgUslSR93FsDdeL0MAoQvmC/BJfs35lqEAO5t/QD7Y4vCFcPJtijt3A==" + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -40341,6 +41258,121 @@ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" }, + "expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", + "dev": true, + "requires": { + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "dependencies": { + "@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "requires": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, + "@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "requires": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + } + }, + "picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -44234,6 +45266,357 @@ "resolved": "https://registry.npmjs.org/jdataview/-/jdataview-2.5.0.tgz", "integrity": "sha512-ZJop3D5nyDcWPBPv4NPnhCvx3HgQNsCXMfw8gpNKY16BobgxmVF+kJ08aHuqk6bJQVeL2mkf6nDCcZPMompalw==" }, + "jest-diff": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "dev": true, + "requires": { + "@jest/diff-sequences": "30.4.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.4.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-matcher-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "dev": true, + "requires": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "dependencies": { + "@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "requires": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, + "@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "requires": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + } + }, + "picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "dev": true, + "requires": { + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" + }, + "dependencies": { + "@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "requires": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, + "@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "requires": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + } + }, + "picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "dev": true + }, "jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -46533,6 +47916,11 @@ "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" }, + "parsedbf": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parsedbf/-/parsedbf-2.0.0.tgz", + "integrity": "sha512-WNjKn/cwgGBkXqQLif+2VMEahcRHkBRU0/RfBWZ7Vj7snRNNW63yW1mVuuHRDyXTRxuGCzAHHBcr/Fn+U/bXjQ==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -48259,6 +49647,41 @@ "renderkid": "^3.0.0" } }, + "pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "requires": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "dependencies": { + "@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -48896,6 +50319,18 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "dev": true }, + "react-is-18": { + "version": "npm:react-is@18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "react-is-19": { + "version": "npm:react-is@19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "dev": true + }, "react-pdf": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-9.2.1.tgz", @@ -50099,6 +51534,16 @@ } } }, + "shpjs": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/shpjs/-/shpjs-6.2.0.tgz", + "integrity": "sha512-8cR/RKYHQepmVyBMtzZQ+1bnSbWrtLXS6aoEJmpUlOSHtSUddterebVxYmIWq2g9kOEX9jm2kjHiikyPX7cNQA==", + "requires": { + "but-unzip": "^0.1.4", + "parsedbf": "^2.0.0", + "proj4": "^2.1.4" + } + }, "side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -50572,6 +52017,23 @@ "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", diff --git a/package.json b/package.json index 39608a1cb..7536c5e19 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,10 @@ "cesium" ], "scripts": { - "db:start": "docker-compose -f docker-compose.db.yml up -d", - "db:stop": "docker-compose -f docker-compose.db.yml down", - "db:logs": "docker-compose -f docker-compose.db.yml logs -f", - "prestart": "docker-compose -f docker-compose.db.yml up -d --wait", + "db:start": "docker compose -f docker-compose.db.yml up -d", + "db:stop": "docker compose -f docker-compose.db.yml down", + "db:logs": "docker compose -f docker-compose.db.yml logs -f", + "prestart": "docker compose -f docker-compose.db.yml up -d --wait", "start": "node scripts/init-db.js && node scripts/server.js", "start:no-docker": "node scripts/init-db.js && node scripts/server.js", "start:prod": "node scripts/init-db.js && cross-env NODE_ENV=production node scripts/server.js", diff --git a/src/essence/Basics/PanelManager_/types/layout.ts b/src/essence/Basics/PanelManager_/types/layout.ts index a30296881..1cf64747d 100644 --- a/src/essence/Basics/PanelManager_/types/layout.ts +++ b/src/essence/Basics/PanelManager_/types/layout.ts @@ -6,9 +6,28 @@ export const PANEL_POSITION = { LEFT: 'left', RIGHT: 'right', BOTTOM: 'bottom', + FLOAT_TOP_LEFT: 'float-top-left', + FLOAT_TOP_CENTER: 'float-top-center', + FLOAT_TOP_RIGHT: 'float-top-right', + FLOAT_BOTTOM_LEFT: 'float-bottom-left', + FLOAT_BOTTOM_CENTER: 'float-bottom-center', + FLOAT_BOTTOM_RIGHT: 'float-bottom-right', } as const export type PanelPosition = (typeof PANEL_POSITION)[keyof typeof PANEL_POSITION] +/** + * Set of all float positions for quick membership checks. + * Float panels render inside the center map area as overlays. + */ +export const FLOAT_POSITIONS = new Set([ + PANEL_POSITION.FLOAT_TOP_LEFT, + PANEL_POSITION.FLOAT_TOP_CENTER, + PANEL_POSITION.FLOAT_TOP_RIGHT, + PANEL_POSITION.FLOAT_BOTTOM_LEFT, + PANEL_POSITION.FLOAT_BOTTOM_CENTER, + PANEL_POSITION.FLOAT_BOTTOM_RIGHT, +] as const) + /** * Visual states of a panel: * - collapsed: Hidden completely, takes no space in the viewport diff --git a/src/essence/Basics/PanelManager_/types/panel.ts b/src/essence/Basics/PanelManager_/types/panel.ts index 24a950ac0..41e25e266 100644 --- a/src/essence/Basics/PanelManager_/types/panel.ts +++ b/src/essence/Basics/PanelManager_/types/panel.ts @@ -62,6 +62,20 @@ export interface PanelDimensions { * - For left/right: this is the width */ expandedSize?: PanelSize; + + /** + * CSS sizing for floating panels — applied directly as CSS properties on the panel element. + * Numbers are treated as px; strings are passed through as-is (e.g. "50%", "40vh", "300px"). + * + * Distinct from PanelCapabilities.minSize/maxSize, which constrain drag-resize handles + * (single-axis, pixels only). These apply to both axes and support all CSS units. + */ + defaultWidth?: number | string; + defaultHeight?: number | string; + minWidth?: number | string; + maxWidth?: number | string; + minHeight?: number | string; + maxHeight?: number | string; } /** diff --git a/src/essence/Basics/UserInterface_/UserInterfaceModern_.css b/src/essence/Basics/UserInterface_/UserInterfaceModern_.css index bd83f44d7..91a9777c3 100644 --- a/src/essence/Basics/UserInterface_/UserInterfaceModern_.css +++ b/src/essence/Basics/UserInterface_/UserInterfaceModern_.css @@ -41,8 +41,6 @@ /* Base Panel Styling */ .ui-panel { background: var(--theme-color-white, #ffffff); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); box-shadow: 0 4px 6px var(--theme-color-shadow, rgba(0, 0, 0, 0.15)); display: flex; flex-direction: column; @@ -465,4 +463,88 @@ body.ui-is-dragging .ui-modern-grid .ui-panel { border: none; } +/* ============================================ + Floating Panels (center area overlays) + ============================================ */ + +:root { + --ui-float-gap: var(--theme-spacing-1, 0.5rem); + --ui-float-outer-gap: var(--theme-spacing-2, 1rem); +} + +/* Invisible wrapper that holds all float zones */ +.ui-float-regions { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 5; +} + +/* Each zone is an absolutely-positioned column of tool cards */ +/* No padding in overlay mode — .ui-panel margin already provides the outer gap */ +.ui-float-zone { + position: absolute; + display: flex; + flex-direction: column; + gap: var(--ui-float-gap); + pointer-events: none; +} + +/* Compact mode has no panel margin, so add outer gap on the zone instead */ +.ui-layout-compact .ui-float-zone { + padding: var(--ui-float-outer-gap); +} + +/* 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: 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 { + pointer-events: auto; + position: relative; + background-color: transparent; +} + +/* Overlay layout: rounded corners (inherits from .ui-panel — kept explicit for clarity) */ +.ui-modern-grid:not(.ui-layout-compact) .ui-float-panel { + border-radius: var(--theme-radius-md, 8px); +} + +/* Compact layout: no rounded corners, no margin */ +.ui-modern-grid.ui-layout-compact .ui-float-panel { + border-radius: 0; + margin: 0; +} + +/* Collapsed float panels are hidden */ +.ui-float-panel[data-panel-state="collapsed"] { + display: none; +} + +/* Float panel body: stacks tools vertically with gaps; scrolls if content exceeds max-height */ +.ui-float-panel-body { + display: flex; + flex-direction: column; + gap: var(--ui-float-gap, 0.5rem); + overflow-y: auto; + flex: 1; + min-height: 0; +} + +/* Thin, subtle scrollbar inside float panels */ +.ui-float-panel-body::-webkit-scrollbar { width: 4px; } +.ui-float-panel-body::-webkit-scrollbar-track { background: transparent; } +.ui-float-panel-body::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); border-radius: 2px; } + +/* Float tool cards size to their content; panel body handles overflow */ +.ui-float-panel .ui-tool-card { + display: flex; + flex-shrink: 0; +} + /* Z-index management (no longer needed as variables, kept for reference) */ \ No newline at end of file diff --git a/src/essence/Basics/UserInterface_/UserInterfaceModern_.js b/src/essence/Basics/UserInterface_/UserInterfaceModern_.js index 21f9ef061..35312dc39 100644 --- a/src/essence/Basics/UserInterface_/UserInterfaceModern_.js +++ b/src/essence/Basics/UserInterface_/UserInterfaceModern_.js @@ -83,6 +83,87 @@ const _createPanelHeader = (panel) => { return header } +const FLOAT_POSITIONS = new Set([ + 'float-top-left', + 'float-top-center', + 'float-top-right', + 'float-bottom-left', + 'float-bottom-center', + 'float-bottom-right', +]) + +// Converts a dimension value to a CSS string. +// Numbers are treated as px; strings are passed through as-is (e.g. "40%", "50vh"). +const _toCssValue = (v) => (typeof v === 'number' ? v + 'px' : v) + +const _renderFloatRegions = (floatPanels) => { + if (!floatPanels || floatPanels.length === 0) return null + + const fragment = $('
') + + // Group panels by their float position (validator enforces at most one per position) + const byPosition = {} + floatPanels.forEach(panel => { + const pos = panel.config.position + if (!byPosition[pos]) byPosition[pos] = [] + byPosition[pos].push(panel) + }) + + Object.entries(byPosition).forEach(([position, positionPanels]) => { + const suffix = position.replace('float-', '') + const zone = $(`
`) + + positionPanels.forEach(panel => { + const toolsMetadata = PanelManager_.getToolsForPanel(panel.id) || [] + if (toolsMetadata.length === 0) return + + // One panel card per float zone — all tools stack inside it + const panelDiv = $('
') + .attr('id', panel.containerId) + .attr('data-panel-id', panel.containerId) + .attr('data-panel-state', panel.state) + + const dims = panel.config.dimensions || {} + + // Width constraints go on the panel container + const panelCss = {} + if (dims.defaultWidth) panelCss['width'] = _toCssValue(dims.defaultWidth) + if (dims.minWidth) panelCss['min-width'] = _toCssValue(dims.minWidth) + if (dims.maxWidth) panelCss['max-width'] = _toCssValue(dims.maxWidth) + if (Object.keys(panelCss).length) panelDiv.css(panelCss) + + if (panel.config.hasHeader) { + panelDiv.append(_createPanelHeader(panel)) + } + + // Body holds all tool cards stacked vertically with gaps + const body = $('
') + + // Height constraints go on the body so overflow-y: auto can trigger correctly. + // Applying max-height only to the panel won't constrain the flex body's actual height, + // so the body scroll never fires; setting it on the body directly fixes this. + const bodyCss = {} + if (dims.defaultHeight) bodyCss['height'] = _toCssValue(dims.defaultHeight) + if (dims.minHeight) bodyCss['min-height'] = _toCssValue(dims.minHeight) + if (dims.maxHeight) bodyCss['max-height'] = _toCssValue(dims.maxHeight) + if (Object.keys(bodyCss).length) body.css(bodyCss) + toolsMetadata.forEach(toolMetadata => { + const { toolCard, loadTool } = UserInterfaceModern_.createToolCard(toolMetadata, panel.containerId) + toolCard.addClass('active') + body.append(toolCard) + toolLoadQueue.push(loadTool) + }) + + panelDiv.append(body) + zone.append(panelDiv) + }) + + fragment.append(zone) + }) + + return fragment +} + const _renderRegion = (regionName, regionPanels) => { if (!regionPanels || regionPanels.length === 0) return null @@ -356,17 +437,20 @@ const UserInterfaceModern_ = { gridWrapper.addClass('ui-layout-compact') } - // Group panels by position + // Group panels by position — float positions go into a separate bucket const layoutRegions = { top: [], left: [], right: [], bottom: [] } + const floatPanels = [] panels.forEach(p => { const pos = p.config?.position - if (layoutRegions[pos]) { + if (FLOAT_POSITIONS.has(pos)) { + floatPanels.push(p) + } else if (layoutRegions[pos]) { layoutRegions[pos].push(p) } }) @@ -413,6 +497,11 @@ const UserInterfaceModern_ = { } } + const floatRegions = _renderFloatRegions(floatPanels) + if (floatRegions) { + centralArea.append(floatRegions) + } + gridWrapper.append(_renderRegion('left', layoutRegions.left)) gridWrapper.append(_renderRegion('right', layoutRegions.right)) gridWrapper.append(_renderRegion('bottom', layoutRegions.bottom)) @@ -452,7 +541,12 @@ const UserInterfaceModern_ = { } panels.forEach(panel => { - const $panel = $(document.getElementById(panel.containerId)) + const isFloat = FLOAT_POSITIONS.has(panel.config?.position) + + // Float panel tool cards use data-panel-id; edge panels use their containerId directly + const $panel = isFloat + ? $(`[data-panel-id="${panel.containerId}"]`) + : $(document.getElementById(panel.containerId)) if ($panel.length === 0) return $panel.attr('data-panel-state', panel.state) diff --git a/src/essence/Validators/DashboardConfigValidator.js b/src/essence/Validators/DashboardConfigValidator.js index f2ff9533a..e0fa79936 100644 --- a/src/essence/Validators/DashboardConfigValidator.js +++ b/src/essence/Validators/DashboardConfigValidator.js @@ -12,7 +12,7 @@ * @typedef {import('../types/dashboard').ValidationResult} ValidationResult */ -import { PANEL_POSITION, PANEL_STATE, PANEL_LAYOUT_TYPE } from '../Basics/PanelManager_/types/layout' +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' @@ -78,10 +78,9 @@ export function validateModernConfig(config) { // Validate each panel const panelIds = new Set() panelSettings.panels.forEach((panel, index) => { - const panelErrors = validatePanelConfig(panel, index) + const panelErrors = validatePanelConfig(panel, index, false) errors.push(...panelErrors) - // Check for duplicate panel IDs if (panel.id) { if (panelIds.has(panel.id)) { errors.push(`Duplicate panel ID: "${panel.id}"`) @@ -90,6 +89,33 @@ export function validateModernConfig(config) { } }) + // Validate optional floatingPanels array + if (panelSettings.floatingPanels !== undefined) { + if (!Array.isArray(panelSettings.floatingPanels)) { + errors.push('panelSettings.floatingPanels must be an array') + } else { + const floatPositions = new Set() + panelSettings.floatingPanels.forEach((panel, index) => { + const panelErrors = validatePanelConfig(panel, index, true) + errors.push(...panelErrors) + + if (panel.id) { + if (panelIds.has(panel.id)) { + errors.push(`Duplicate panel ID: "${panel.id}"`) + } + panelIds.add(panel.id) + } + + if (panel.position && FLOAT_POSITIONS.has(panel.position)) { + if (floatPositions.has(panel.position)) { + errors.push(`Duplicate floating panel position: "${panel.position}" — each float zone can only have one panel`) + } + floatPositions.add(panel.position) + } + }) + } + } + // Validate tools array (optional) if (config.tools && !Array.isArray(config.tools)) { errors.push('Config "tools" must be an array') @@ -125,11 +151,12 @@ export function validateModernConfig(config) { * * @param {PanelConfig} panel - The panel config to validate * @param {number} index - Index in the panels array (for error messages) + * @param {boolean} isFloat - Whether this is a floating panel (relaxes layoutType/priority requirements) * @returns {string[]} - Array of error messages */ -function validatePanelConfig(panel, index) { +function validatePanelConfig(panel, index, isFloat = false) { const errors = [] - const prefix = `Panel[${index}]` + const prefix = isFloat ? `FloatingPanel[${index}]` : `Panel[${index}]` if (!panel || typeof panel !== 'object') { errors.push(`${prefix}: Must be an object`) @@ -150,24 +177,40 @@ function validatePanelConfig(panel, index) { errors.push( `${prefix}: Must have a valid "position" (${VALID_POSITIONS.join(', ')})` ) + } else if (isFloat && !FLOAT_POSITIONS.has(panel.position)) { + errors.push( + `${prefix}: Floating panel position must be one of the float positions (float-top-left, float-top-center, etc.)` + ) } - // Validate priority - try to convert if string - if (panel.priority === undefined || panel.priority === null) { - errors.push(`${prefix}: Must have a "priority"`) - } else { + // Validate priority - optional for float panels + if (!isFloat) { + if (panel.priority === undefined || panel.priority === null) { + errors.push(`${prefix}: Must have a "priority"`) + } else { + const priorityNum = Number(panel.priority) + if (isNaN(priorityNum)) { + errors.push(`${prefix}: "priority" must be a valid number (got "${panel.priority}")`) + } else if (priorityNum < 0) { + errors.push(`${prefix}: "priority" must be non-negative`) + } + } + } else if (panel.priority !== undefined && panel.priority !== null) { const priorityNum = Number(panel.priority) - if (isNaN(priorityNum)) { - errors.push(`${prefix}: "priority" must be a valid number (got "${panel.priority}")`) - } else if (priorityNum < 0) { - errors.push(`${prefix}: "priority" must be non-negative`) + if (isNaN(priorityNum) || priorityNum < 0) { + errors.push(`${prefix}: "priority" must be a non-negative number when provided`) } } - if (!panel.layoutType || !VALID_LAYOUT_TYPES.includes(panel.layoutType)) { + // layoutType is required for edge panels, optional for float panels + if (!isFloat && (!panel.layoutType || !VALID_LAYOUT_TYPES.includes(panel.layoutType))) { errors.push( `${prefix}: Must have a valid "layoutType" (${VALID_LAYOUT_TYPES.join(', ')})` ) + } else if (isFloat && panel.layoutType && !VALID_LAYOUT_TYPES.includes(panel.layoutType)) { + errors.push( + `${prefix}: "layoutType" must be one of: ${VALID_LAYOUT_TYPES.join(', ')}` + ) } if (panel.hasHeader !== undefined && typeof panel.hasHeader !== 'boolean') { @@ -273,11 +316,35 @@ function validatePanelConfig(panel, index) { } } } + + // CSS dimension fields for floating panels (number or CSS string e.g. "50%", "40vh") + // Empty strings are treated as "not provided" and skipped. + const cssDimFields = ['defaultWidth', 'defaultHeight', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight'] + cssDimFields.forEach(field => { + const val = dim[field] + if (val !== undefined && val !== '' && !isValidCssDimension(val)) { + errors.push( + `${prefix}.dimensions: "${field}" must be a number (px) or a CSS string with a unit (px, %, vh, vw, rem, em) — got "${val}"` + ) + } + }) } return errors } +/** + * Returns true if v is a valid CSS dimension value: a number (treated as px) + * or a string with a number followed by a recognised CSS unit. + * @param {*} v + * @returns {boolean} + */ +function isValidCssDimension(v) { + if (typeof v === 'number') return true + if (typeof v === 'string') return /^\d+(\.\d+)?(px|%|vh|vw|svh|dvh|rem|em)$/.test(v) + return false +} + /** * Sanitizes a string for safe use in HTML/URLs. * Removes potentially dangerous characters. diff --git a/src/essence/modern.js b/src/essence/modern.js index 486fb4d58..7b5b13c8e 100644 --- a/src/essence/modern.js +++ b/src/essence/modern.js @@ -262,9 +262,14 @@ class ModernInterface { */ _registerPanels() { const panelsConfig = this.configData?.panelSettings?.panels || [] + const floatingPanelsConfig = (this.configData?.panelSettings?.floatingPanels || []).map(p => ({ + layoutType: 'stacked', + ...p, + })) + const allPanels = [...panelsConfig, ...floatingPanelsConfig] const failedPanels = [] - panelsConfig.forEach(panelConfig => { + allPanels.forEach(panelConfig => { try { // Register with the global PanelManager PanelManager_.registerPanel(panelConfig) diff --git a/src/essence/types/dashboard.ts b/src/essence/types/dashboard.ts index 9b6f6810d..d5ae8dc8e 100644 --- a/src/essence/types/dashboard.ts +++ b/src/essence/types/dashboard.ts @@ -62,6 +62,9 @@ export interface PanelSettings { /** Array of panel configurations */ panels: PanelConfig[]; + /** Floating panels rendered inside the center map area */ + floatingPanels?: PanelConfig[]; + /** Layout style ('overlay' | 'compact') */ layoutStyle?: LayoutStyle; } From 8cfb0927bebc79518f4a05986868adb8250d5345 Mon Sep 17 00:00:00 2001 From: Sandesh Pandey Date: Mon, 22 Jun 2026 11:06:58 -0500 Subject: [PATCH 2/3] Refine floating panel descriptions and constraints; update styles for compact layout --- configure/src/metaconfigs/tab-ui-config.json | 11 +--------- .../Basics/PanelManager_/PanelManager_.ts | 10 +++++++++- .../UserInterface_/UserInterfaceModern_.css | 20 +++++++++++-------- .../UserInterface_/UserInterfaceModern_.js | 10 +--------- 4 files changed, 23 insertions(+), 28 deletions(-) diff --git a/configure/src/metaconfigs/tab-ui-config.json b/configure/src/metaconfigs/tab-ui-config.json index 1a30ffc2d..0a95c60ba 100644 --- a/configure/src/metaconfigs/tab-ui-config.json +++ b/configure/src/metaconfigs/tab-ui-config.json @@ -241,7 +241,7 @@ { "field": "panelSettings.floatingPanels", "name": "Floating Panels", - "description": "Panels that float over the center map area. Each floating panel wraps a single tool and is positioned at one of six anchor points. Multiple panels at the same position stack vertically.", + "description": "Panels that float over the center map area. Each floating panel is anchored to one of six positions. At most one panel per position is allowed.", "type": "objectarray", "width": 24, "object": [ @@ -268,15 +268,6 @@ "default": "float-bottom-center", "width": 6 }, - { - "field": "priority", - "name": "Priority", - "description": "Stacking order when multiple panels share the same position (lower number appears first/top).", - "type": "number", - "default": 0, - "width": 4, - "min": 0 - }, { "field": "hasHeader", "name": "Has Header", diff --git a/src/essence/Basics/PanelManager_/PanelManager_.ts b/src/essence/Basics/PanelManager_/PanelManager_.ts index 3e2f8a63c..882c64109 100644 --- a/src/essence/Basics/PanelManager_/PanelManager_.ts +++ b/src/essence/Basics/PanelManager_/PanelManager_.ts @@ -1,5 +1,5 @@ import { ToolOrientation, ToolMetadata } from '../ToolController_/types/tool'; -import { PanelPosition, PanelState, PanelLayoutType, PANEL_STATE } from './types/layout'; +import { PanelPosition, PanelState, PanelLayoutType, PANEL_STATE, FLOAT_POSITIONS } from './types/layout'; import { PanelConfig, PanelStateObject, PanelManager as PanelManagerInterface } from './types/panel'; import { mmgisAPI } from '../../mmgisAPI/mmgisAPI'; @@ -157,6 +157,14 @@ class PanelManager implements PanelManagerInterface { throw new Error(`Panel with ID ${panelId} not found`); } + if ((FLOAT_POSITIONS as Set).has(panel.config.position) && + (newState === PANEL_STATE.ICONIFIED || newState === PANEL_STATE.FOCUSED)) { + throw new Error( + `Float panels do not support '${newState}' state. ` + + `Only 'collapsed' and 'expanded' are allowed for float panel ${panelId}.` + ); + } + if (!panel.config.stateConstraints.allowedStates.includes(newState)) { throw new Error(`State transition to ${newState} is not allowed for panel ${panelId}`); } diff --git a/src/essence/Basics/UserInterface_/UserInterfaceModern_.css b/src/essence/Basics/UserInterface_/UserInterfaceModern_.css index 91a9777c3..054bb9c4e 100644 --- a/src/essence/Basics/UserInterface_/UserInterfaceModern_.css +++ b/src/essence/Basics/UserInterface_/UserInterfaceModern_.css @@ -510,15 +510,19 @@ body.ui-is-dragging .ui-modern-grid .ui-panel { background-color: transparent; } -/* Overlay layout: rounded corners (inherits from .ui-panel — kept explicit for clarity) */ -.ui-modern-grid:not(.ui-layout-compact) .ui-float-panel { +/* Compact layout: no margin */ +.ui-modern-grid.ui-layout-compact .ui-float-panel { + margin: 0; +} + +/* Overlay mode: tool cards have rounded corners (panel container is transparent) */ +.ui-modern-grid:not(.ui-layout-compact) .ui-float-panel .ui-tool-card { border-radius: var(--theme-radius-md, 8px); } -/* Compact layout: no rounded corners, no margin */ -.ui-modern-grid.ui-layout-compact .ui-float-panel { +/* Compact mode: tool cards are square */ +.ui-modern-grid.ui-layout-compact .ui-float-panel .ui-tool-card { border-radius: 0; - margin: 0; } /* Collapsed float panels are hidden */ @@ -545,6 +549,6 @@ body.ui-is-dragging .ui-modern-grid .ui-panel { .ui-float-panel .ui-tool-card { display: flex; flex-shrink: 0; -} - -/* Z-index management (no longer needed as variables, kept for reference) */ \ No newline at end of file + padding: 2px; + overflow: hidden; +} \ No newline at end of file diff --git a/src/essence/Basics/UserInterface_/UserInterfaceModern_.js b/src/essence/Basics/UserInterface_/UserInterfaceModern_.js index 35312dc39..b683d713d 100644 --- a/src/essence/Basics/UserInterface_/UserInterfaceModern_.js +++ b/src/essence/Basics/UserInterface_/UserInterfaceModern_.js @@ -4,6 +4,7 @@ import { mmgisAPI } from '../../mmgisAPI/mmgisAPI' import ToolControllerModern_ from '../ToolController_/ToolControllerModern_' import { getValidIconClass } from '../ToolController_/ToolMetadataUtils' import { createLogger } from '../Logger_/Logger_' +import { FLOAT_POSITIONS } from '../PanelManager_/types/layout' import './UserInterfaceModern_.css' const logger = createLogger('UserInterfaceModern') @@ -83,15 +84,6 @@ const _createPanelHeader = (panel) => { return header } -const FLOAT_POSITIONS = new Set([ - 'float-top-left', - 'float-top-center', - 'float-top-right', - 'float-bottom-left', - 'float-bottom-center', - 'float-bottom-right', -]) - // Converts a dimension value to a CSS string. // Numbers are treated as px; strings are passed through as-is (e.g. "40%", "50vh"). const _toCssValue = (v) => (typeof v === 'number' ? v + 'px' : v) From 36fab799c7b8b098c6a882ac708cf74b2b0e692c Mon Sep 17 00:00:00 2001 From: Sandesh Pandey Date: Tue, 30 Jun 2026 15:26:14 -0500 Subject: [PATCH 3/3] Update floating panel validation and configuration rules; enforce one panel per float position --- docs/MISSION_CONFIG_REFERENCE.md | 6 ++-- .../UserInterface_/UserInterfaceModern_.js | 15 ++++---- .../Validators/DashboardConfigValidator.js | 35 +++++++++++++++++-- 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/docs/MISSION_CONFIG_REFERENCE.md b/docs/MISSION_CONFIG_REFERENCE.md index c18d85985..4e74297d7 100644 --- a/docs/MISSION_CONFIG_REFERENCE.md +++ b/docs/MISSION_CONFIG_REFERENCE.md @@ -120,7 +120,7 @@ Determines where the panel is located: **Floating panel rules**: - Multiple tools can be assigned to one floating panel — each tool renders as its own card with a gap between cards -- Multiple floating panels at the same position stack **vertically** in declaration order +- Only **one** floating panel is allowed per position — the validator rejects configs that assign more than one panel (in `panels` or `floatingPanels`) to the same float position - Supported states: `"collapsed"` and `"expanded"` only (`"iconified"` and `"focused"` are not supported) - `layoutType` is ignored for floating panels - `capabilities.resizable` is not supported for floating panels @@ -482,7 +482,7 @@ Full mission configuration with four panels: "allowedStates": ["collapsed", "expanded"], "defaultState": "expanded" }, - "tools": ["Timeline"] + "panelTools": ["Timeline"] }, { "id": "legend-float-panel", @@ -493,7 +493,7 @@ Full mission configuration with four panels: "allowedStates": ["collapsed", "expanded"], "defaultState": "expanded" }, - "tools": ["Legend"] + "panelTools": ["Legend"] } ] }, diff --git a/src/essence/Basics/UserInterface_/UserInterfaceModern_.js b/src/essence/Basics/UserInterface_/UserInterfaceModern_.js index b683d713d..b9aceb240 100644 --- a/src/essence/Basics/UserInterface_/UserInterfaceModern_.js +++ b/src/essence/Basics/UserInterface_/UserInterfaceModern_.js @@ -112,7 +112,6 @@ const _renderFloatRegions = (floatPanels) => { // One panel card per float zone — all tools stack inside it const panelDiv = $('
') .attr('id', panel.containerId) - .attr('data-panel-id', panel.containerId) .attr('data-panel-state', panel.state) const dims = panel.config.dimensions || {} @@ -533,12 +532,9 @@ const UserInterfaceModern_ = { } panels.forEach(panel => { - const isFloat = FLOAT_POSITIONS.has(panel.config?.position) + const isFloatingPanel = FLOAT_POSITIONS.has(panel.config?.position) - // Float panel tool cards use data-panel-id; edge panels use their containerId directly - const $panel = isFloat - ? $(`[data-panel-id="${panel.containerId}"]`) - : $(document.getElementById(panel.containerId)) + const $panel = $(document.getElementById(panel.containerId)) if ($panel.length === 0) return $panel.attr('data-panel-state', panel.state) @@ -561,7 +557,12 @@ const UserInterfaceModern_ = { $panel.find('.ui-panel-icon-btn').removeClass('active') } - if (panel.state === 'iconified') { + // Float panels are sized via dimensions.defaultWidth/defaultHeight applied + // once at render time (see _renderFloatRegions) — the edge-panel + // expandedSize/iconifiedSize logic below doesn't apply and would clobber them. + if (isFloatingPanel) { + // no-op + } else if (panel.state === 'iconified') { $panel.css({ width: '', height: '', flex: 'none' }) } else if (panel.state === 'expanded' || panel.state === 'focused') { let targetSize = panel.currentSize; diff --git a/src/essence/Validators/DashboardConfigValidator.js b/src/essence/Validators/DashboardConfigValidator.js index e0fa79936..c9a949eac 100644 --- a/src/essence/Validators/DashboardConfigValidator.js +++ b/src/essence/Validators/DashboardConfigValidator.js @@ -76,7 +76,11 @@ export function validateModernConfig(config) { } // Validate each panel + // Float positions are tracked across BOTH the regular "panels" array and the + // "floatingPanels" array so the one-panel-per-float-zone rule can't be bypassed + // by splitting panels with the same float position across the two arrays. const panelIds = new Set() + const floatPositions = new Set() panelSettings.panels.forEach((panel, index) => { const panelErrors = validatePanelConfig(panel, index, false) errors.push(...panelErrors) @@ -87,6 +91,16 @@ export function validateModernConfig(config) { } panelIds.add(panel.id) } + + // Float positions are honored by the renderer regardless of which array a + // panel was declared in (modern.js merges "panels" and "floatingPanels" + // before registration), so dedup must consider this array too. + if (panel.position && FLOAT_POSITIONS.has(panel.position)) { + if (floatPositions.has(panel.position)) { + errors.push(`Duplicate floating panel position: "${panel.position}" — each float zone can only have one panel`) + } + floatPositions.add(panel.position) + } }) // Validate optional floatingPanels array @@ -94,7 +108,6 @@ export function validateModernConfig(config) { if (!Array.isArray(panelSettings.floatingPanels)) { errors.push('panelSettings.floatingPanels must be an array') } else { - const floatPositions = new Set() panelSettings.floatingPanels.forEach((panel, index) => { const panelErrors = validatePanelConfig(panel, index, true) errors.push(...panelErrors) @@ -163,6 +176,12 @@ function validatePanelConfig(panel, index, isFloat = false) { return errors } + // What actually governs float rendering/state-machine behavior at runtime is the + // panel's position, not which config array it was declared in (modern.js merges + // "panels" and "floatingPanels" before registration). Use this for checks that + // must hold regardless of source array. + const isFloatPosition = !!(panel.position && FLOAT_POSITIONS.has(panel.position)) + // Required fields if (!panel.id || typeof panel.id !== 'string') { errors.push(`${prefix}: Must have a string "id"`) @@ -232,6 +251,13 @@ function validatePanelConfig(panel, index, isFloat = false) { errors.push( `${prefix}.stateConstraints: Invalid state "${state}" (must be one of ${VALID_STATES.join(', ')})` ) + } else if (isFloatPosition && state !== PANEL_STATE.COLLAPSED && state !== PANEL_STATE.EXPANDED) { + // Float panels only support collapsed/expanded — PanelManager_.setPanelState + // throws for float + iconified/focused transitions, so the validator must + // reject these here rather than let it become a reachable runtime throw. + errors.push( + `${prefix}.stateConstraints: Floating panels only support "collapsed" and "expanded" states (got "${state}")` + ) } }) } @@ -324,7 +350,7 @@ function validatePanelConfig(panel, index, isFloat = false) { const val = dim[field] if (val !== undefined && val !== '' && !isValidCssDimension(val)) { errors.push( - `${prefix}.dimensions: "${field}" must be a number (px) or a CSS string with a unit (px, %, vh, vw, rem, em) — got "${val}"` + `${prefix}.dimensions: "${field}" must be a number (px), unitless "0", or a CSS string with a unit (px, %, vh, vw, svh, dvh, vmin, vmax, ch, rem, em) — got "${val}"` ) } }) @@ -341,7 +367,10 @@ function validatePanelConfig(panel, index, isFloat = false) { */ function isValidCssDimension(v) { if (typeof v === 'number') return true - if (typeof v === 'string') return /^\d+(\.\d+)?(px|%|vh|vw|svh|dvh|rem|em)$/.test(v) + if (typeof v === 'string') { + if (v === '0') return true + return /^\d+(\.\d+)?(px|%|vh|vw|svh|dvh|vmin|vmax|ch|rem|em)$/.test(v) + } return false }