mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(panels): render each panel's state classes from the store
A panel's collapsed, dock-pinned, dock-pinned-top, layout-auto-collapsed
and accordion-collapsed classes are drawn by its root's v-panel-state from
the gev-panel-chrome store. PanelChrome, its restore, the layout
controller, both rails and the side accordion present each panel's state
there instead of writing its classes; the rails call onCollapse(panel,
{collapsed, auto}). The directive applies a change in a synchronous
watcher, since the rails measure a panel in the task that opens or closes
it.
Destroying the chrome inside Cockpit now puts back the panels Cockpit's
entry closed while it can still present them. Without a shell, the voice
tools' set_panel_open reports that there is no panel to open. QA scripts
present the states they set instead of writing the classes.
This commit is contained in:
1 parent
dd3a2c87f9
commit
95523e09cd
37 files changed
+404
-135
No files matched your search
@@ -108,6 +108,16 @@
|
|||||||
or `onChange`. `UiButton` joins the component kit with ui-vue's props
|
or `onChange`. `UiButton` joins the component kit with ui-vue's props
|
||||||
(samehkhamis).
|
(samehkhamis).
|
||||||
|
|
||||||
|
- The panels' own state classes (`collapsed`, `dock-pinned`,
|
||||||
|
`dock-pinned-top`, `layout-auto-collapsed`, `accordion-collapsed`) render
|
||||||
|
from the `gev-panel-chrome` store: the shell, the rails and the accordion
|
||||||
|
present each panel's state there instead of writing its classes.
|
||||||
|
`layoutLeftPanelRail` and `layoutRightPanelRail` (`./ui/layout`) now call
|
||||||
|
`onCollapse(panel, {collapsed, auto})` and leave drawing it to the host. Without a
|
||||||
|
shell to own the panels, the voice tools' `set_panel_open` now reports
|
||||||
|
that there is no panel to open instead of writing its classes
|
||||||
|
(samehkhamis).
|
||||||
|
|
||||||
## [0.3.0] — 2026-10-27 — Street Level, On Your Phone, in Your Language
|
## [0.3.0] — 2026-10-27 — Street Level, On Your Phone, in Your Language
|
||||||
|
|
||||||
- Show a Street Level view facing 359.5° or more as 0°. The viewer caption
|
- Show a Street Level view facing 359.5° or more as 0°. The viewer caption
|
||||||
|
|||||||
@@ -67,7 +67,13 @@ a feature supplying only its accent.
|
|||||||
mirrors each side's panels and the rail renders from it. Each panel's
|
mirrors each side's panels and the rail renders from it. Each panel's
|
||||||
collapse button (`PanelCollapseButton`), the dock tray toggles and the
|
collapse button (`PanelCollapseButton`), the dock tray toggles and the
|
||||||
dock pins render the open and pinned state PanelChrome presents to the
|
dock pins render the open and pinned state PanelChrome presents to the
|
||||||
`gev-panel-chrome` store, and their clicks call it there. Done.
|
`gev-panel-chrome` store, and their clicks call it there. Each panel's
|
||||||
|
root draws its own state from there too (`v-panel-state`: `collapsed`,
|
||||||
|
`dock-pinned`, `dock-pinned-top`, `layout-auto-collapsed`,
|
||||||
|
`accordion-collapsed`): PanelChrome, the rails and the accordion present
|
||||||
|
it rather than writing classes. The directive applies it in a synchronous
|
||||||
|
watcher, not on Vue's next render, because the rails measure a panel in
|
||||||
|
the same task that opens or closes it. Done.
|
||||||
3. **Panels.** Each template and its wiring become a component, keeping ids,
|
3. **Panels.** Each template and its wiring become a component, keeping ids,
|
||||||
classes, `data-i18n` text and CSS. The page places each view with a
|
classes, `data-i18n` text and CSS. The page places each view with a
|
||||||
`.gev-view` host (`display: contents`), and `mountApplicationViews()`
|
`.gev-view` host (`display: contents`), and `mountApplicationViews()`
|
||||||
|
|||||||
@@ -101,7 +101,11 @@ Escape, bound and disposed with `bindPanelDisclosure` /
|
|||||||
`collapsePanelOnEscape` from `panelDisclosure.js` (which bind Escape only). Do not rerun the whole application chrome
|
`collapsePanelOnEscape` from `panelDisclosure.js` (which bind Escape only). Do not rerun the whole application chrome
|
||||||
initializer just to add one panel. `layoutLeftPanelRail` and
|
initializer just to add one panel. `layoutLeftPanelRail` and
|
||||||
`layoutRightPanelRail` accept arbitrary direct `data-panel-id` children; their
|
`layoutRightPanelRail` accept arbitrary direct `data-panel-id` children; their
|
||||||
host owns layout scheduling, observations and collapse callbacks. Dynamic
|
host owns layout scheduling, observations and collapse callbacks. A rail
|
||||||
|
never writes a panel's classes: it calls `onCollapse(panel, {collapsed,
|
||||||
|
auto})`, and the host draws that state (the application presents it to the
|
||||||
|
panel chrome store, which each root's `v-panel-state` draws at once, so the
|
||||||
|
rail can measure the panel in the same task). Dynamic
|
||||||
content that changes size may require an owner-scheduled layout pass or an
|
content that changes size may require an owner-scheduled layout pass or an
|
||||||
owned `ResizeObserver`; adding CSS attributes does not register new observers.
|
owned `ResizeObserver`; adding CSS attributes does not register new observers.
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,8 @@
|
|||||||
"src/firstRunCopy.js",
|
"src/firstRunCopy.js",
|
||||||
"src/ui/kit/UiButton.vue",
|
"src/ui/kit/UiButton.vue",
|
||||||
"src/ui/stores/panelChrome.js",
|
"src/ui/stores/panelChrome.js",
|
||||||
"src/ui/rail/PanelCollapseButton.vue"
|
"src/ui/rail/PanelCollapseButton.vue",
|
||||||
|
"src/ui/rail/panelState.js"
|
||||||
],
|
],
|
||||||
"external": ["vue", "pinia"]
|
"external": ["vue", "pinia"]
|
||||||
},
|
},
|
||||||
@@ -1048,7 +1049,8 @@
|
|||||||
"src/ui/stores/worldOverlayActions.js",
|
"src/ui/stores/worldOverlayActions.js",
|
||||||
"src/ui/stores/awarenessDirection.js",
|
"src/ui/stores/awarenessDirection.js",
|
||||||
"src/ui/stores/hud.js",
|
"src/ui/stores/hud.js",
|
||||||
"src/ui/stores/panelChrome.js"
|
"src/ui/stores/panelChrome.js",
|
||||||
|
"src/ui/rail/panelState.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "vue", "pinia"]
|
"external": ["cesium", "vue", "pinia"]
|
||||||
},
|
},
|
||||||
@@ -3054,7 +3056,8 @@
|
|||||||
"src/ui/stores/hud.js",
|
"src/ui/stores/hud.js",
|
||||||
"src/ui/stores/firstRun.js",
|
"src/ui/stores/firstRun.js",
|
||||||
"src/firstRunCopy.js",
|
"src/firstRunCopy.js",
|
||||||
"src/ui/stores/panelChrome.js"
|
"src/ui/stores/panelChrome.js",
|
||||||
|
"src/ui/rail/panelState.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"hls.js",
|
"hls.js",
|
||||||
@@ -3863,7 +3866,8 @@
|
|||||||
"src/ui/panelRailCatalog.js",
|
"src/ui/panelRailCatalog.js",
|
||||||
"src/ui/panelAccordion.js",
|
"src/ui/panelAccordion.js",
|
||||||
"src/ui/vueRuntime.js",
|
"src/ui/vueRuntime.js",
|
||||||
"src/ui/stores/panelChrome.js"
|
"src/ui/stores/panelChrome.js",
|
||||||
|
"src/ui/rail/panelState.js"
|
||||||
],
|
],
|
||||||
"external": ["vue", "pinia"]
|
"external": ["vue", "pinia"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1799,7 +1799,7 @@ try {
|
|||||||
manager._setCockpitDisplayPortalActive(false);
|
manager._setCockpitDisplayPortalActive(false);
|
||||||
document.body.classList.remove('cockpit-mode');
|
document.body.classList.remove('cockpit-mode');
|
||||||
await waitFrames();
|
await waitFrames();
|
||||||
standard.classList.remove('collapsed');
|
manager._presentPanel(standard, { collapsed: false });
|
||||||
standard.style.height = '120px';
|
standard.style.height = '120px';
|
||||||
standard.style.maxHeight = '120px';
|
standard.style.maxHeight = '120px';
|
||||||
standard.style.overflowY = 'auto';
|
standard.style.overflowY = 'auto';
|
||||||
@@ -1834,7 +1834,7 @@ try {
|
|||||||
cockpit.style.height = priorCockpitHeight;
|
cockpit.style.height = priorCockpitHeight;
|
||||||
cockpit.style.maxHeight = priorCockpitMaxHeight;
|
cockpit.style.maxHeight = priorCockpitMaxHeight;
|
||||||
cockpit.style.overflowY = priorCockpitOverflow;
|
cockpit.style.overflowY = priorCockpitOverflow;
|
||||||
standard.classList.toggle('collapsed', standardWasCollapsed);
|
manager._presentPanel(standard, { collapsed: standardWasCollapsed });
|
||||||
return {
|
return {
|
||||||
standardBefore,
|
standardBefore,
|
||||||
standardAfter,
|
standardAfter,
|
||||||
|
|||||||
+18
-5
@@ -1716,7 +1716,14 @@ async function main() {
|
|||||||
else panel.setAttribute('style', state.style);
|
else panel.setAttribute('style', state.style);
|
||||||
if (state.ariaHidden === null) panel.removeAttribute('aria-hidden');
|
if (state.ariaHidden === null) panel.removeAttribute('aria-hidden');
|
||||||
else panel.setAttribute('aria-hidden', state.ariaHidden);
|
else panel.setAttribute('aria-hidden', state.ariaHidden);
|
||||||
manager._syncPanelCollapseButton(panel);
|
// The restored classes are the panel's state again.
|
||||||
|
const has = (name) => panel.classList.contains(name);
|
||||||
|
manager._presentPanel(panel, {
|
||||||
|
collapsed: has('collapsed'),
|
||||||
|
pinned: has('dock-pinned'),
|
||||||
|
pinnedTop: has('dock-pinned-top'),
|
||||||
|
auto: has('layout-auto-collapsed') ? 'layout' : has('accordion-collapsed') ? 'accordion' : null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
for (const [id, state] of Object.entries(prior.stacks)) {
|
for (const [id, state] of Object.entries(prior.stacks)) {
|
||||||
const stack = document.getElementById(id);
|
const stack = document.getElementById(id);
|
||||||
@@ -1974,7 +1981,7 @@ async function main() {
|
|||||||
manager._setCockpitDisplayPortalActive(true);
|
manager._setCockpitDisplayPortalActive(true);
|
||||||
hud.hidden = false;
|
hud.hidden = false;
|
||||||
signal.hidden = false;
|
signal.hidden = false;
|
||||||
dataPanel.classList.remove('collapsed');
|
manager._presentPanel(dataPanel, { collapsed: false });
|
||||||
window.__godsEyeView.styleManager._syncLeftPanelAdaptiveLayout();
|
window.__godsEyeView.styleManager._syncLeftPanelAdaptiveLayout();
|
||||||
window.__godsEyeView.styleManager.cockpitView.syncSignalLayout();
|
window.__godsEyeView.styleManager.cockpitView.syncSignalLayout();
|
||||||
if (display.getAttribute('aria-expanded') === 'true') display.click();
|
if (display.getAttribute('aria-expanded') === 'true') display.click();
|
||||||
@@ -2059,8 +2066,7 @@ async function main() {
|
|||||||
contextCard.hidden = false;
|
contextCard.hidden = false;
|
||||||
// Expand the panel the same way this block already does at entry, so the
|
// Expand the panel the same way this block already does at entry, so the
|
||||||
// portaled Display groups measured further down keep their DOM home.
|
// portaled Display groups measured further down keep their DOM home.
|
||||||
dataPanel.classList.remove('collapsed', 'layout-auto-collapsed');
|
manager._presentPanel(dataPanel, { collapsed: false, auto: null });
|
||||||
manager._syncPanelCollapseButton(dataPanel);
|
|
||||||
manager._syncLeftPanelAdaptiveLayout();
|
manager._syncLeftPanelAdaptiveLayout();
|
||||||
await waitForLayout();
|
await waitForLayout();
|
||||||
const panelRect = dataPanel.getBoundingClientRect();
|
const panelRect = dataPanel.getBoundingClientRect();
|
||||||
@@ -2287,7 +2293,14 @@ async function main() {
|
|||||||
else panel.setAttribute('style', state.style);
|
else panel.setAttribute('style', state.style);
|
||||||
if (state.ariaHidden === null) panel.removeAttribute('aria-hidden');
|
if (state.ariaHidden === null) panel.removeAttribute('aria-hidden');
|
||||||
else panel.setAttribute('aria-hidden', state.ariaHidden);
|
else panel.setAttribute('aria-hidden', state.ariaHidden);
|
||||||
manager._syncPanelCollapseButton(panel);
|
// The restored classes are the panel's state again.
|
||||||
|
const has = (name) => panel.classList.contains(name);
|
||||||
|
manager._presentPanel(panel, {
|
||||||
|
collapsed: has('collapsed'),
|
||||||
|
pinned: has('dock-pinned'),
|
||||||
|
pinnedTop: has('dock-pinned-top'),
|
||||||
|
auto: has('layout-auto-collapsed') ? 'layout' : has('accordion-collapsed') ? 'accordion' : null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
manager._panelLayout._leftStackPreferredPanelId = prior.preferredLeftPanelId;
|
manager._panelLayout._leftStackPreferredPanelId = prior.preferredLeftPanelId;
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ try {
|
|||||||
persist: false,
|
persist: false,
|
||||||
syncShare: false,
|
syncShare: false,
|
||||||
});
|
});
|
||||||
manager._sliderPanel.classList.remove('collapsed');
|
manager._presentPanel(manager._sliderPanel, { collapsed: false });
|
||||||
const slider = manager._sliderContainer.querySelector('input');
|
const slider = manager._sliderContainer.querySelector('input');
|
||||||
window.__qaOldSlider = slider;
|
window.__qaOldSlider = slider;
|
||||||
const name = 'pixelation';
|
const name = 'pixelation';
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ try {
|
|||||||
for (const [id, entry] of gev.dataManager.layers)
|
for (const [id, entry] of gev.dataManager.layers)
|
||||||
if (entry.enabled)
|
if (entry.enabled)
|
||||||
await gev.dataManager.setEnabled(id, false, { origin: 'user' });
|
await gev.dataManager.setEnabled(id, false, { origin: 'user' });
|
||||||
document.querySelector('#data-panel')?.classList.remove('collapsed');
|
gev.styleManager._presentPanel('data-panel', { collapsed: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1. Earth view, clean.
|
// 1. Earth view, clean.
|
||||||
|
|||||||
@@ -1088,7 +1088,7 @@ test('Cockpit Display portals shared HUD, Detection, Parameters, and 3D controls
|
|||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
ui,
|
ui,
|
||||||
/_revealCockpitStyleParameters\(\{ openDisplay = false \} = \{\}\) \{[\s\S]*?openDisplay[\s\S]*?_setCockpitDisclosure\?\.\('display', true\)[\s\S]*?!this\._cockpitDisclosureOpen\?\.\('display'\)[\s\S]*?classList\.remove\('collapsed'\)/,
|
/_revealCockpitStyleParameters\(\{ openDisplay = false \} = \{\}\) \{[\s\S]*?openDisplay[\s\S]*?_setCockpitDisclosure\?\.\('display', true\)[\s\S]*?!this\._cockpitDisclosureOpen\?\.\('display'\)[\s\S]*?_presentPanel\(this\._sliderPanel, \{ collapsed: false \}\)/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
radioBindings,
|
radioBindings,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import assert from 'node:assert/strict';
|
|||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
import { msg, t } from './i18n/index.js';
|
import { msg, t } from './i18n/index.js';
|
||||||
|
import { followPanelState, readPanelState } from './ui/rail/panelState.js';
|
||||||
import { usePanelChromeStore } from './ui/stores/panelChrome.js';
|
import { usePanelChromeStore } from './ui/stores/panelChrome.js';
|
||||||
|
|
||||||
// Exercise the installed event routes and central close method, without WebGL.
|
// Exercise the installed event routes and central close method, without WebGL.
|
||||||
@@ -108,18 +109,22 @@ function harness({ hidden = false, selected = true, noChips = false } = {}) {
|
|||||||
window.clearTimeout = (id) => timers.delete(id);
|
window.clearTimeout = (id) => timers.delete(id);
|
||||||
window.performance = { now: () => now };
|
window.performance = { now: () => now };
|
||||||
document.defaultView = window;
|
document.defaultView = window;
|
||||||
const methods = new Function('createHoverDisclosure', 'collapsePanelOnEscape', 'document', 'window', 'clearTimeout', 'performance', 'requestAnimationFrame', 't', 'msg',
|
const methods = new Function('createHoverDisclosure', 'collapsePanelOnEscape', 'document', 'window', 'clearTimeout', 'performance', 'requestAnimationFrame', 't', 'msg', 'readPanelState',
|
||||||
`return ({${shellMethod('_initAutoHoverPanel').toString()},\n${shellMethod('_collapsePanelOnEscape').toString()},\n${shellMethod('setPanelCollapsed').toString()},\n${shellMethod('_syncPanelCollapseButton').toString()}});`)(
|
`return ({${shellMethod('_initAutoHoverPanel').toString()},\n${shellMethod('_collapsePanelOnEscape').toString()},\n${shellMethod('setPanelCollapsed').toString()},\n${shellMethod('_presentPanel').toString()}});`)(
|
||||||
createHoverDisclosure, collapsePanelOnEscape, document, window, (id) => timers.delete(id), { now: () => now }, () => {}, t, msg,
|
createHoverDisclosure, collapsePanelOnEscape, document, window, (id) => timers.delete(id), { now: () => now }, () => {}, t, msg, readPanelState,
|
||||||
);
|
);
|
||||||
const saves = [];
|
const saves = [];
|
||||||
const claims = [];
|
const claims = [];
|
||||||
let shareSyncs = 0;
|
let shareSyncs = 0;
|
||||||
const chromeStore = usePanelChromeStore(createPinia());
|
const chromeStore = usePanelChromeStore(createPinia());
|
||||||
|
const chromeToken = chromeStore.connect({});
|
||||||
|
// The trays' roots draw the store's state, as v-panel-state does.
|
||||||
|
followPanelState(chromeStore, 'control-panel', panel);
|
||||||
|
followPanelState(chromeStore, 'location-bar', location);
|
||||||
const manager = {
|
const manager = {
|
||||||
_lifetime: { frame() {} },
|
_lifetime: { frame() {} },
|
||||||
_chromeStore: chromeStore,
|
_chromeStore: chromeStore,
|
||||||
_chromeToken: chromeStore.connect({}),
|
_chromeToken: chromeToken,
|
||||||
_panelLayout: { _leftStackPreferredPanelId: null, _rightStackPreferredPanelId: null },
|
_panelLayout: { _leftStackPreferredPanelId: null, _rightStackPreferredPanelId: null },
|
||||||
...methods,
|
...methods,
|
||||||
_savePanelCollapsedState(...args) { saves.push(args); },
|
_savePanelCollapsedState(...args) { saves.push(args); },
|
||||||
|
|||||||
@@ -141,8 +141,8 @@ test('desktop panel lanes use per-panel allocations and presentation-only auto-c
|
|||||||
const ui = readShellSource();
|
const ui = readShellSource();
|
||||||
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
||||||
assert.doesNotMatch(ui, /_enforce(?:Left|Right)PanelAccordion/);
|
assert.doesNotMatch(ui, /_enforce(?:Left|Right)PanelAccordion/);
|
||||||
assert.match(rails, /classList\.add\('collapsed', 'layout-auto-collapsed'\)/);
|
assert.match(rails, /onCollapse\(panel, \{ collapsed: true, auto: 'layout' \}\)/);
|
||||||
assert.match(readFileSync(new URL('./ui/panelLayoutController.js', import.meta.url), 'utf8'), /classList\.remove\('collapsed', 'layout-auto-collapsed'\)/);
|
assert.match(readFileSync(new URL('./ui/panelLayoutController.js', import.meta.url), 'utf8'), /_presentPanel\(panel, \{ collapsed: false, auto: null \}\)/);
|
||||||
assert.match(ui, /reconsiderAutoCollapse/);
|
assert.match(ui, /reconsiderAutoCollapse/);
|
||||||
assert.match(
|
assert.match(
|
||||||
ui,
|
ui,
|
||||||
@@ -169,7 +169,7 @@ test('desktop panel lanes use per-panel allocations and presentation-only auto-c
|
|||||||
assert.match(rightRail, /focusedExpandedPanel = expandedPanelsInDomOrder\.find\(\s*\(panel\) =>\s*panel\.contains\(documentRef\.activeElement\),?\s*\)/);
|
assert.match(rightRail, /focusedExpandedPanel = expandedPanelsInDomOrder\.find\(\s*\(panel\) =>\s*panel\.contains\(documentRef\.activeElement\),?\s*\)/);
|
||||||
// Collapse buttons render aria-expanded from the state the chrome presents
|
// Collapse buttons render aria-expanded from the state the chrome presents
|
||||||
// (panelChromeButtons.test.mjs covers the views).
|
// (panelChromeButtons.test.mjs covers the views).
|
||||||
assert.match(ui, /_chromeStore\.publish\(this\._chromeToken, panelEl\.id, \{\s*collapsed: panelEl\.classList\.contains\('collapsed'\),/);
|
assert.match(ui, /this\._presentPanel\(panelEl, \{ collapsed: nextCollapsed, auto: null \}\)/);
|
||||||
assert.match(leftRail, /--left-panel-allocated-height/);
|
assert.match(leftRail, /--left-panel-allocated-height/);
|
||||||
assert.match(rightRail, /--right-panel-allocated-height/);
|
assert.match(rightRail, /--right-panel-allocated-height/);
|
||||||
assert.match(
|
assert.match(
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ test('panel collapse is presentation-only and Radio exposes explicit voice playb
|
|||||||
assert.doesNotMatch(enableMethod, /contextRadio/);
|
assert.doesNotMatch(enableMethod, /contextRadio/);
|
||||||
assert.match(enableMethod, /toggleRadio\(this\._radioEnableBtn\)/);
|
assert.match(enableMethod, /toggleRadio\(this\._radioEnableBtn\)/);
|
||||||
// The collapse buttons render aria-expanded from the state the chrome presents.
|
// The collapse buttons render aria-expanded from the state the chrome presents.
|
||||||
assert.match(ui, /_chromeStore\.publish\(this\._chromeToken, panelEl\.id, \{/);
|
assert.match(ui, /this\._presentPanel\(panelEl, \{ collapsed: nextCollapsed, auto: null \}\)/);
|
||||||
assert.doesNotMatch(method, /panelId === 'global-context-panel'[\s\S]*?this\._radioState\?\.enabled[\s\S]*?setPanelCollapsed\('radio-panel', false\)/);
|
assert.doesNotMatch(method, /panelId === 'global-context-panel'[\s\S]*?this\._radioState\?\.enabled[\s\S]*?setPanelCollapsed\('radio-panel', false\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// src/testSupport/panelState.mjs — present a panel's state the way the shell
|
||||||
|
// does (PanelChrome._presentPanel), drawn at once as its root's v-panel-state
|
||||||
|
// would, for fixtures whose panels are plain objects with a classList.
|
||||||
|
import { applyPanelState, readPanelState } from '../ui/rail/panelState.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object|string} panelOrId A panel, or its id in `documentRef`.
|
||||||
|
* @param {object} [patch] The state that changes.
|
||||||
|
* @param {Document} [documentRef]
|
||||||
|
*/
|
||||||
|
export function presentPanelLocally(
|
||||||
|
panelOrId,
|
||||||
|
patch = {},
|
||||||
|
documentRef = globalThis.document,
|
||||||
|
) {
|
||||||
|
const panel =
|
||||||
|
typeof panelOrId === 'string'
|
||||||
|
? documentRef?.getElementById(panelOrId)
|
||||||
|
: panelOrId;
|
||||||
|
if (panel) applyPanelState(panel, { ...readPanelState(panel), ...patch });
|
||||||
|
}
|
||||||
@@ -77,13 +77,14 @@ test('the documented outcomes hold: default, stored open, stored shut, and a sha
|
|||||||
// against the code that decides them.
|
// against the code that decides them.
|
||||||
const { PanelPositionControls } =
|
const { PanelPositionControls } =
|
||||||
await import('../ui/panelPositionControls.js');
|
await import('../ui/panelPositionControls.js');
|
||||||
|
const { presentPanelLocally } = await import('../testSupport/panelState.mjs');
|
||||||
const html = await readFile(
|
const html = await readFile(
|
||||||
path.join(root, 'src/ui/views/panels/CctvPanel.vue'),
|
path.join(root, 'src/ui/views/panels/CctvPanel.vue'),
|
||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
html,
|
html,
|
||||||
/<div id="cctv-panel" class="panel-collapsible collapsed"/,
|
/<div id="cctv-panel"[^>]*class="panel-collapsible collapsed"/,
|
||||||
'the CCTV panel starts collapsed in the markup',
|
'the CCTV panel starts collapsed in the markup',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ test('the documented outcomes hold: default, stored open, stored shut, and a sha
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const controls = new PanelPositionControls({
|
const controls = new PanelPositionControls({
|
||||||
syncPanelCollapseButton: () => {},
|
presentPanel: presentPanelLocally,
|
||||||
layoutRightPanels: () => {},
|
layoutRightPanels: () => {},
|
||||||
syncCctvPanelViewport: () => {},
|
syncCctvPanelViewport: () => {},
|
||||||
showToast: () => {},
|
showToast: () => {},
|
||||||
|
|||||||
@@ -234,8 +234,7 @@ export class StyleManager extends ShellFacade {
|
|||||||
_setCockpitDisclosure: (...args) => this._setCockpitDisclosure(...args),
|
_setCockpitDisclosure: (...args) => this._setCockpitDisclosure(...args),
|
||||||
_cockpitDisclosureOpen: (kind) => this._cockpitDisclosureOpen(kind),
|
_cockpitDisclosureOpen: (kind) => this._cockpitDisclosureOpen(kind),
|
||||||
_setMapStack: (...args) => this._setMapStack(...args),
|
_setMapStack: (...args) => this._setMapStack(...args),
|
||||||
_syncPanelCollapseButton: (...args) =>
|
_presentPanel: (...args) => this._presentPanel(...args),
|
||||||
this._syncPanelCollapseButton(...args),
|
|
||||||
_syncShareState: (...args) => this._syncShareState(...args),
|
_syncShareState: (...args) => this._syncShareState(...args),
|
||||||
setPanelCollapsed: (...args) => this.setPanelCollapsed(...args),
|
setPanelCollapsed: (...args) => this.setPanelCollapsed(...args),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import { settleSideAccordion } from './panelAccordion.js';
|
|||||||
* @param {Window} options.windowRef Viewport and style reader.
|
* @param {Window} options.windowRef Viewport and style reader.
|
||||||
* @param {{visible: boolean, variant: string}} options.hud Current HUD presentation.
|
* @param {{visible: boolean, variant: string}} options.hud Current HUD presentation.
|
||||||
* @param {string} options.preferredPanelId Most recently opened panel.
|
* @param {string} options.preferredPanelId Most recently opened panel.
|
||||||
* @param {Function} options.onCollapse Update a panel's disclosure chrome.
|
* @param {Function} options.onCollapse Present a panel's collapse:
|
||||||
|
* `(panel, {collapsed, auto})`.
|
||||||
* @param {Function} options.onRetry Request another pass after automatic collapse.
|
* @param {Function} options.onRetry Request another pass after automatic collapse.
|
||||||
* @param {Function} [options.getComputedStyle] Optional DOM style reader override.
|
* @param {Function} [options.getComputedStyle] Optional DOM style reader override.
|
||||||
* @param {Map<string, number>} options.collapsedHeights Caller-owned measurement cache.
|
* @param {Map<string, number>} options.collapsedHeights Caller-owned measurement cache.
|
||||||
@@ -45,8 +46,7 @@ export function layoutLeftPanelRail({
|
|||||||
for (const panel of panels.filter((item) =>
|
for (const panel of panels.filter((item) =>
|
||||||
item.classList.contains('layout-auto-collapsed'),
|
item.classList.contains('layout-auto-collapsed'),
|
||||||
)) {
|
)) {
|
||||||
panel.classList.remove('collapsed', 'layout-auto-collapsed');
|
onCollapse(panel, { collapsed: false, auto: null });
|
||||||
onCollapse(panel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,8 +262,7 @@ export function layoutLeftPanelRail({
|
|||||||
if (autoCollapseIndices.length) {
|
if (autoCollapseIndices.length) {
|
||||||
for (const index of autoCollapseIndices) {
|
for (const index of autoCollapseIndices) {
|
||||||
const panel = expandedPanels[index];
|
const panel = expandedPanels[index];
|
||||||
panel.classList.add('collapsed', 'layout-auto-collapsed');
|
onCollapse(panel, { collapsed: true, auto: 'layout' });
|
||||||
onCollapse(panel);
|
|
||||||
}
|
}
|
||||||
onRetry();
|
onRetry();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
* @param {boolean} options.enabled Whether the one-panel rule applies.
|
* @param {boolean} options.enabled Whether the one-panel rule applies.
|
||||||
* @param {string} [options.preferredPanelId] Most recently opened panel.
|
* @param {string} [options.preferredPanelId] Most recently opened panel.
|
||||||
* @param {Document} [options.documentRef] Document supplying keyboard focus.
|
* @param {Document} [options.documentRef] Document supplying keyboard focus.
|
||||||
* @param {Function} options.onCollapse Update a panel's disclosure chrome.
|
* @param {Function} options.onCollapse Present a panel's collapse:
|
||||||
|
* `(panel, {collapsed, auto})`.
|
||||||
*/
|
*/
|
||||||
export function settleSideAccordion({
|
export function settleSideAccordion({
|
||||||
panels,
|
panels,
|
||||||
@@ -21,8 +22,7 @@ export function settleSideAccordion({
|
|||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
for (const panel of panels) {
|
for (const panel of panels) {
|
||||||
if (!panel.classList.contains('accordion-collapsed')) continue;
|
if (!panel.classList.contains('accordion-collapsed')) continue;
|
||||||
panel.classList.remove('collapsed', 'accordion-collapsed');
|
onCollapse(panel, { collapsed: false, auto: null });
|
||||||
onCollapse(panel);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,6 @@ export function settleSideAccordion({
|
|||||||
expanded[0];
|
expanded[0];
|
||||||
for (const panel of expanded) {
|
for (const panel of expanded) {
|
||||||
if (panel === owner) continue;
|
if (panel === owner) continue;
|
||||||
panel.classList.add('collapsed', 'accordion-collapsed');
|
onCollapse(panel, { collapsed: true, auto: 'accordion' });
|
||||||
onCollapse(panel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+32
-23
@@ -1,4 +1,5 @@
|
|||||||
import { UiLifetime } from './uiLifetime.js';
|
import { UiLifetime } from './uiLifetime.js';
|
||||||
|
import { readPanelState } from './rail/panelState.js';
|
||||||
import { usePanelChromeStore } from './stores/panelChrome.js';
|
import { usePanelChromeStore } from './stores/panelChrome.js';
|
||||||
import { applicationPinia } from './vueRuntime.js';
|
import { applicationPinia } from './vueRuntime.js';
|
||||||
import { displayPanelScroller } from './displayPanelScroll.js';
|
import { displayPanelScroller } from './displayPanelScroll.js';
|
||||||
@@ -81,7 +82,7 @@ export class PanelChrome {
|
|||||||
this._cockpitPanelRestore = null;
|
this._cockpitPanelRestore = null;
|
||||||
this._cockpitContextCollapsedForDataPanel = false;
|
this._cockpitContextCollapsedForDataPanel = false;
|
||||||
this._panelPosition = new PanelPositionControls({
|
this._panelPosition = new PanelPositionControls({
|
||||||
syncPanelCollapseButton: (panel) => this._syncPanelCollapseButton(panel),
|
presentPanel: (panel, patch) => this._presentPanel(panel, patch),
|
||||||
showToast: (message) => this._showToast(message),
|
showToast: (message) => this._showToast(message),
|
||||||
});
|
});
|
||||||
this._panelLayout = new PanelLayoutController({
|
this._panelLayout = new PanelLayoutController({
|
||||||
@@ -90,7 +91,7 @@ export class PanelChrome {
|
|||||||
variant: this.hud.getVariant(),
|
variant: this.hud.getVariant(),
|
||||||
}),
|
}),
|
||||||
scheduleCockpitLayout: () => this.cockpitView?.scheduleContextLayout(),
|
scheduleCockpitLayout: () => this.cockpitView?.scheduleContextLayout(),
|
||||||
syncPanelCollapseButton: (panel) => this._syncPanelCollapseButton(panel),
|
presentPanel: (panel, patch) => this._presentPanel(panel, patch),
|
||||||
readDisplayScrollTop: () =>
|
readDisplayScrollTop: () =>
|
||||||
this._displayPortalScrollRestoreOwner === 'standard'
|
this._displayPortalScrollRestoreOwner === 'standard'
|
||||||
? this._standardDisplayScrollTop
|
? this._standardDisplayScrollTop
|
||||||
@@ -221,18 +222,18 @@ export class PanelChrome {
|
|||||||
typeof pin === 'boolean'
|
typeof pin === 'boolean'
|
||||||
? pin
|
? pin
|
||||||
: !panelEl.classList.contains('dock-pinned');
|
: !panelEl.classList.contains('dock-pinned');
|
||||||
panelEl.classList.toggle('dock-pinned', shouldPin);
|
// The tray pinned last draws on top; unpinning hands the top to the
|
||||||
this._chromeStore.publish(this._chromeToken, panelId, {
|
// other tray if it is pinned.
|
||||||
collapsed: panelEl.classList.contains('collapsed'),
|
this._presentPanel(panelEl, { pinned: shouldPin, pinnedTop: shouldPin });
|
||||||
pinned: shouldPin,
|
for (const otherId of PINNABLE_PANEL_IDS) {
|
||||||
});
|
if (otherId === panelId) continue;
|
||||||
document
|
const other = document.getElementById(otherId);
|
||||||
.querySelectorAll('#command-dock .dock-pinned-top')
|
if (!other) continue;
|
||||||
.forEach((pinnedPanel) => {
|
this._presentPanel(other, {
|
||||||
pinnedPanel.classList.remove('dock-pinned-top');
|
pinnedTop: !shouldPin && other.classList.contains('dock-pinned'),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
if (shouldPin) {
|
if (shouldPin) {
|
||||||
panelEl.classList.add('dock-pinned-top');
|
|
||||||
this.setPanelCollapsed(panelId, false, {
|
this.setPanelCollapsed(panelId, false, {
|
||||||
explicit: !restore,
|
explicit: !restore,
|
||||||
restore,
|
restore,
|
||||||
@@ -240,10 +241,6 @@ export class PanelChrome {
|
|||||||
syncShare: false,
|
syncShare: false,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const remainingPinnedPanel = document.querySelector(
|
|
||||||
'#command-dock .dock-pinned',
|
|
||||||
);
|
|
||||||
remainingPinnedPanel?.classList.add('dock-pinned-top');
|
|
||||||
if (!restore && !panelEl.matches(':hover')) {
|
if (!restore && !panelEl.matches(':hover')) {
|
||||||
this.setPanelCollapsed(panelId, true, {
|
this.setPanelCollapsed(panelId, true, {
|
||||||
explicit: true,
|
explicit: true,
|
||||||
@@ -323,11 +320,23 @@ export class PanelChrome {
|
|||||||
return this._panelLayout._scheduleLeftPanelLayout(options0);
|
return this._panelLayout._scheduleLeftPanelLayout(options0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Present a panel's open state to its collapse and dock buttons. */
|
/**
|
||||||
_syncPanelCollapseButton(panelEl) {
|
* Present a panel's state, merged over what it shows now, to the panel
|
||||||
|
* chrome store; its root and buttons draw it from there.
|
||||||
|
* @param {Element|string} panelOrId
|
||||||
|
* @param {Partial<import('./stores/panelChrome.js').PanelState>} [patch]
|
||||||
|
*/
|
||||||
|
_presentPanel(panelOrId, patch = {}) {
|
||||||
|
const panelEl =
|
||||||
|
typeof panelOrId === 'string'
|
||||||
|
? document.getElementById(panelOrId)
|
||||||
|
: panelOrId;
|
||||||
if (!panelEl?.id) return;
|
if (!panelEl?.id) return;
|
||||||
|
const shown =
|
||||||
|
this._chromeStore.panels[panelEl.id] ?? readPanelState(panelEl);
|
||||||
this._chromeStore.publish(this._chromeToken, panelEl.id, {
|
this._chromeStore.publish(this._chromeToken, panelEl.id, {
|
||||||
collapsed: panelEl.classList.contains('collapsed'),
|
...shown,
|
||||||
|
...patch,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +487,6 @@ export class PanelChrome {
|
|||||||
panelEl.classList.contains('collapsed') === nextCollapsed &&
|
panelEl.classList.contains('collapsed') === nextCollapsed &&
|
||||||
!wasAutoCollapsed
|
!wasAutoCollapsed
|
||||||
) {
|
) {
|
||||||
this._syncPanelCollapseButton(panelEl);
|
|
||||||
if (priorLeftOwner !== this._panelLayout._leftStackPreferredPanelId) {
|
if (priorLeftOwner !== this._panelLayout._leftStackPreferredPanelId) {
|
||||||
this._scheduleLeftPanelLayout({ reconsiderAutoCollapse: true });
|
this._scheduleLeftPanelLayout({ reconsiderAutoCollapse: true });
|
||||||
}
|
}
|
||||||
@@ -487,7 +495,6 @@ export class PanelChrome {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
panelEl.classList.remove('layout-auto-collapsed', 'accordion-collapsed');
|
|
||||||
if (
|
if (
|
||||||
!nextCollapsed &&
|
!nextCollapsed &&
|
||||||
this.cockpitView?.active &&
|
this.cockpitView?.active &&
|
||||||
@@ -518,7 +525,7 @@ export class PanelChrome {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
panelEl.classList.toggle('collapsed', nextCollapsed);
|
this._presentPanel(panelEl, { collapsed: nextCollapsed, auto: null });
|
||||||
if (
|
if (
|
||||||
nextCollapsed &&
|
nextCollapsed &&
|
||||||
this.cockpitView?.active &&
|
this.cockpitView?.active &&
|
||||||
@@ -528,7 +535,6 @@ export class PanelChrome {
|
|||||||
this._cockpitContextCollapsedForDataPanel = false;
|
this._cockpitContextCollapsedForDataPanel = false;
|
||||||
this.cockpitView.setContextCollapsed(false);
|
this.cockpitView.setContextCollapsed(false);
|
||||||
}
|
}
|
||||||
this._syncPanelCollapseButton(panelEl);
|
|
||||||
if (persist !== false)
|
if (persist !== false)
|
||||||
this._savePanelCollapsedState(panelId, nextCollapsed);
|
this._savePanelCollapsedState(panelId, nextCollapsed);
|
||||||
if (panelId === 'pp-toggles') {
|
if (panelId === 'pp-toggles') {
|
||||||
@@ -586,6 +592,9 @@ export class PanelChrome {
|
|||||||
}
|
}
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this._disposed) return;
|
if (this._disposed) return;
|
||||||
|
// Cockpit's own teardown follows the chrome's: put back the panels its
|
||||||
|
// entry closed while this owner can still present them.
|
||||||
|
this.exitCockpit();
|
||||||
this._disposed = true;
|
this._disposed = true;
|
||||||
this._lifetime.destroy();
|
this._lifetime.destroy();
|
||||||
this._chromeStore.detach(this._chromeToken);
|
this._chromeStore.detach(this._chromeToken);
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
import '../testSupport/registerDom.mjs';
|
import '../testSupport/registerDom.mjs';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { createApp, h, nextTick } from 'vue';
|
import { createApp, h, nextTick, withDirectives } from 'vue';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
|
import { settleSideAccordion } from './panelAccordion.js';
|
||||||
import { PanelChrome } from './panelChrome.js';
|
import { PanelChrome } from './panelChrome.js';
|
||||||
|
import { followPanelState, vPanelState } from './rail/panelState.js';
|
||||||
import { bindPanelDisclosure } from './panelDisclosure.js';
|
import { bindPanelDisclosure } from './panelDisclosure.js';
|
||||||
import PanelCollapseButton from './rail/PanelCollapseButton.vue';
|
import PanelCollapseButton from './rail/PanelCollapseButton.vue';
|
||||||
import { usePanelChromeStore } from './stores/panelChrome.js';
|
import { usePanelChromeStore } from './stores/panelChrome.js';
|
||||||
@@ -30,6 +32,8 @@ test('the store republishes a panel only when its state changes', () => {
|
|||||||
assert.deepEqual(store.panels['data-panel'], {
|
assert.deepEqual(store.panels['data-panel'], {
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
|
pinnedTop: false,
|
||||||
|
auto: null,
|
||||||
});
|
});
|
||||||
store.publish(token, 'data-panel', { collapsed: true });
|
store.publish(token, 'data-panel', { collapsed: true });
|
||||||
assert.ok(store.panels === first, 'an unchanged state is not republished');
|
assert.ok(store.panels === first, 'an unchanged state is not republished');
|
||||||
@@ -175,6 +179,8 @@ test('PanelChrome takes clicks only once initialized, pins only dock trays, and
|
|||||||
<div id="control-panel" class="collapsed"></div>`;
|
<div id="control-panel" class="collapsed"></div>`;
|
||||||
const store = usePanelChromeStore(createPinia());
|
const store = usePanelChromeStore(createPinia());
|
||||||
const chrome = panelChrome(store);
|
const chrome = panelChrome(store);
|
||||||
|
for (const id of ['data-panel', 'cctv-panel', 'control-panel'])
|
||||||
|
followPanelState(store, id, document.getElementById(id));
|
||||||
const calls = [];
|
const calls = [];
|
||||||
const setPanelCollapsed = chrome.setPanelCollapsed.bind(chrome);
|
const setPanelCollapsed = chrome.setPanelCollapsed.bind(chrome);
|
||||||
chrome.setPanelCollapsed = (id, collapsed, options) => {
|
chrome.setPanelCollapsed = (id, collapsed, options) => {
|
||||||
@@ -196,6 +202,8 @@ test('PanelChrome takes clicks only once initialized, pins only dock trays, and
|
|||||||
assert.deepEqual(store.panels['control-panel'], {
|
assert.deepEqual(store.panels['control-panel'], {
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
|
pinnedTop: true,
|
||||||
|
auto: null,
|
||||||
});
|
});
|
||||||
assert.equal(
|
assert.equal(
|
||||||
document.getElementById('control-panel').classList.contains('dock-pinned'),
|
document.getElementById('control-panel').classList.contains('dock-pinned'),
|
||||||
@@ -206,8 +214,12 @@ test('PanelChrome takes clicks only once initialized, pins only dock trays, and
|
|||||||
const before = calls.length;
|
const before = calls.length;
|
||||||
store.act('toggle', 'data-panel');
|
store.act('toggle', 'data-panel');
|
||||||
assert.equal(calls.length, before, 'a destroyed owner gets no clicks');
|
assert.equal(calls.length, before, 'a destroyed owner gets no clicks');
|
||||||
chrome._syncPanelCollapseButton(document.getElementById('cctv-panel'));
|
chrome._presentPanel('cctv-panel', { collapsed: true });
|
||||||
assert.equal(store.panels['cctv-panel'], undefined, 'nor publishes');
|
assert.equal(store.panels['cctv-panel'], undefined, 'nor publishes');
|
||||||
|
assert.equal(
|
||||||
|
document.getElementById('cctv-panel').classList.contains('collapsed'),
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a remounted chrome starts from its own states, not a pin left behind', () => {
|
test('a remounted chrome starts from its own states, not a pin left behind', () => {
|
||||||
@@ -221,10 +233,12 @@ test('a remounted chrome starts from its own states, not a pin left behind', ()
|
|||||||
document.body.innerHTML = '<div id="location-bar" class="collapsed"></div>';
|
document.body.innerHTML = '<div id="location-bar" class="collapsed"></div>';
|
||||||
const second = panelChrome(store);
|
const second = panelChrome(store);
|
||||||
assert.equal(store.panels['location-bar'], undefined);
|
assert.equal(store.panels['location-bar'], undefined);
|
||||||
second._syncPanelCollapseButton(document.getElementById('location-bar'));
|
second._presentPanel('location-bar');
|
||||||
assert.deepEqual(store.panels['location-bar'], {
|
assert.deepEqual(store.panels['location-bar'], {
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
|
pinnedTop: false,
|
||||||
|
auto: null,
|
||||||
});
|
});
|
||||||
second.destroy();
|
second.destroy();
|
||||||
});
|
});
|
||||||
@@ -238,3 +252,80 @@ test('bindPanelDisclosure refuses the retired button wiring', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a panel root draws its state at once, keeping classes others own', async (t) => {
|
||||||
|
const store = mount(t, {
|
||||||
|
render: () =>
|
||||||
|
withDirectives(
|
||||||
|
h('div', { id: 'scene-panel', class: 'panel-collapsible collapsed' }),
|
||||||
|
[[vPanelState, 'scene-panel']],
|
||||||
|
),
|
||||||
|
});
|
||||||
|
const panel = document.getElementById('scene-panel');
|
||||||
|
panel.classList.add('running');
|
||||||
|
assert.equal(panel.classList.contains('collapsed'), true, 'markup stands');
|
||||||
|
const token = store.connect({});
|
||||||
|
store.publish(token, 'scene-panel', { collapsed: true, auto: 'accordion' });
|
||||||
|
// No tick: a layout measures in the same task.
|
||||||
|
assert.equal(panel.classList.contains('accordion-collapsed'), true);
|
||||||
|
store.publish(token, 'scene-panel', { collapsed: false, auto: null });
|
||||||
|
assert.equal(panel.classList.contains('collapsed'), false);
|
||||||
|
assert.equal(panel.classList.contains('accordion-collapsed'), false);
|
||||||
|
assert.equal(panel.classList.contains('running'), true);
|
||||||
|
assert.equal(panel.classList.contains('panel-collapsible'), true);
|
||||||
|
store.publish(token, 'scene-panel', { pinned: true, pinnedTop: true });
|
||||||
|
assert.ok(panel.classList.contains('dock-pinned'));
|
||||||
|
assert.ok(panel.classList.contains('dock-pinned-top'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroying the chrome inside Cockpit puts back the panels its entry closed', () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="data-panel"></div>
|
||||||
|
<div id="cctv-panel" class="collapsed"></div>`;
|
||||||
|
const store = usePanelChromeStore(createPinia());
|
||||||
|
const chrome = panelChrome(store);
|
||||||
|
const data = document.getElementById('data-panel');
|
||||||
|
followPanelState(store, 'data-panel', data);
|
||||||
|
chrome.enterCockpit();
|
||||||
|
assert.equal(data.classList.contains('collapsed'), true);
|
||||||
|
chrome.destroy();
|
||||||
|
assert.equal(data.classList.contains('collapsed'), false);
|
||||||
|
chrome.exitCockpit();
|
||||||
|
assert.equal(data.classList.contains('collapsed'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the accordion closes peers through the real store, and opens them again', () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="data-panel" data-panel-id="data-panel"></div>
|
||||||
|
<div id="scene-panel" data-panel-id="scene-panel"></div>`;
|
||||||
|
const store = usePanelChromeStore(createPinia());
|
||||||
|
const chrome = panelChrome(store);
|
||||||
|
const panels = ['data-panel', 'scene-panel'].map((id) => {
|
||||||
|
const panel = document.getElementById(id);
|
||||||
|
followPanelState(store, id, panel);
|
||||||
|
return panel;
|
||||||
|
});
|
||||||
|
const onCollapse = (panel, patch) => chrome._presentPanel(panel, patch);
|
||||||
|
settleSideAccordion({
|
||||||
|
panels,
|
||||||
|
enabled: true,
|
||||||
|
preferredPanelId: 'scene-panel',
|
||||||
|
documentRef: document,
|
||||||
|
onCollapse,
|
||||||
|
});
|
||||||
|
assert.deepEqual(store.panels['data-panel'], {
|
||||||
|
collapsed: true,
|
||||||
|
pinned: false,
|
||||||
|
pinnedTop: false,
|
||||||
|
auto: 'accordion',
|
||||||
|
});
|
||||||
|
assert.equal(panels[0].className, 'collapsed accordion-collapsed');
|
||||||
|
assert.equal(
|
||||||
|
store.panels['scene-panel'],
|
||||||
|
undefined,
|
||||||
|
'the owner is untouched',
|
||||||
|
);
|
||||||
|
settleSideAccordion({ panels, enabled: false, onCollapse });
|
||||||
|
assert.equal(panels[0].className, '');
|
||||||
|
chrome.destroy();
|
||||||
|
});
|
||||||
@@ -64,12 +64,12 @@ export class PanelLayoutController {
|
|||||||
constructor({
|
constructor({
|
||||||
readHud,
|
readHud,
|
||||||
scheduleCockpitLayout,
|
scheduleCockpitLayout,
|
||||||
syncPanelCollapseButton,
|
presentPanel,
|
||||||
readDisplayScrollTop,
|
readDisplayScrollTop,
|
||||||
}) {
|
}) {
|
||||||
this.readHud = readHud;
|
this.readHud = readHud;
|
||||||
this.scheduleCockpitLayout = scheduleCockpitLayout;
|
this.scheduleCockpitLayout = scheduleCockpitLayout;
|
||||||
this._syncPanelCollapseButton = syncPanelCollapseButton;
|
this._presentPanel = presentPanel;
|
||||||
this.readDisplayScrollTop = readDisplayScrollTop;
|
this.readDisplayScrollTop = readDisplayScrollTop;
|
||||||
this.destroyed = false;
|
this.destroyed = false;
|
||||||
this._adaptivePanelSettleTimer = null;
|
this._adaptivePanelSettleTimer = null;
|
||||||
@@ -198,7 +198,6 @@ export class PanelLayoutController {
|
|||||||
]) {
|
]) {
|
||||||
if (!panel) continue;
|
if (!panel) continue;
|
||||||
stack.insertBefore(panel, globalContextPanel);
|
stack.insertBefore(panel, globalContextPanel);
|
||||||
this._syncPanelCollapseButton(panel);
|
|
||||||
}
|
}
|
||||||
if (this._sliderPanel) {
|
if (this._sliderPanel) {
|
||||||
this._sliderPanel.style.removeProperty('top');
|
this._sliderPanel.style.removeProperty('top');
|
||||||
@@ -286,8 +285,7 @@ export class PanelLayoutController {
|
|||||||
for (const panel of this._rightPanelStack.querySelectorAll(
|
for (const panel of this._rightPanelStack.querySelectorAll(
|
||||||
'.layout-auto-collapsed',
|
'.layout-auto-collapsed',
|
||||||
)) {
|
)) {
|
||||||
panel.classList.remove('collapsed', 'layout-auto-collapsed');
|
this._presentPanel(panel, { collapsed: false, auto: null });
|
||||||
this._syncPanelCollapseButton(panel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._syncRightPanelAdaptiveLayout();
|
this._syncRightPanelAdaptiveLayout();
|
||||||
@@ -303,7 +301,7 @@ export class PanelLayoutController {
|
|||||||
windowRef: window,
|
windowRef: window,
|
||||||
hud: this.readHud(),
|
hud: this.readHud(),
|
||||||
preferredPanelId: this._rightStackPreferredPanelId,
|
preferredPanelId: this._rightStackPreferredPanelId,
|
||||||
onCollapse: (panel) => this._syncPanelCollapseButton(panel),
|
onCollapse: (panel, patch) => this._presentPanel(panel, patch),
|
||||||
onRetry: () => this._scheduleRightPanelLayout(),
|
onRetry: () => this._scheduleRightPanelLayout(),
|
||||||
leftStack: this._leftPanelStack,
|
leftStack: this._leftPanelStack,
|
||||||
displayPanel: this._ppToggles,
|
displayPanel: this._ppToggles,
|
||||||
@@ -422,8 +420,7 @@ export class PanelLayoutController {
|
|||||||
for (const panel of this._leftPanelStack.querySelectorAll(
|
for (const panel of this._leftPanelStack.querySelectorAll(
|
||||||
'.layout-auto-collapsed',
|
'.layout-auto-collapsed',
|
||||||
)) {
|
)) {
|
||||||
panel.classList.remove('collapsed', 'layout-auto-collapsed');
|
this._presentPanel(panel, { collapsed: false, auto: null });
|
||||||
this._syncPanelCollapseButton(panel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._syncLeftPanelAdaptiveLayout();
|
this._syncLeftPanelAdaptiveLayout();
|
||||||
@@ -438,7 +435,7 @@ export class PanelLayoutController {
|
|||||||
windowRef: window,
|
windowRef: window,
|
||||||
hud: this.readHud(),
|
hud: this.readHud(),
|
||||||
preferredPanelId: this._leftStackPreferredPanelId,
|
preferredPanelId: this._leftStackPreferredPanelId,
|
||||||
onCollapse: (panel) => this._syncPanelCollapseButton(panel),
|
onCollapse: (panel, patch) => this._presentPanel(panel, patch),
|
||||||
onRetry: () => this._scheduleLeftPanelLayout(),
|
onRetry: () => this._scheduleLeftPanelLayout(),
|
||||||
collapsedHeights: this._leftStackCollapsedHeights,
|
collapsedHeights: this._leftStackCollapsedHeights,
|
||||||
onAligned: () => this._scheduleRightPanelLayout(),
|
onAligned: () => this._scheduleRightPanelLayout(),
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ const RETIRED_PANEL_KEY_PREFIXES = [
|
|||||||
`godsEyeView.${PANEL_POSITION_STORAGE_VERSION}.panelFloatHintShown`,
|
`godsEyeView.${PANEL_POSITION_STORAGE_VERSION}.panelFloatHintShown`,
|
||||||
];
|
];
|
||||||
export class PanelPositionControls {
|
export class PanelPositionControls {
|
||||||
constructor({ syncPanelCollapseButton, showToast }) {
|
constructor({ presentPanel, showToast }) {
|
||||||
this._syncPanelCollapseButton = syncPanelCollapseButton;
|
this._presentPanel = presentPanel;
|
||||||
this._showToast = showToast;
|
this._showToast = showToast;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ export class PanelPositionControls {
|
|||||||
// first-run mission card for the one first impression there is. A stored
|
// first-run mission card for the one first impression there is. A stored
|
||||||
// choice still wins in both directions, so anyone who opens it keeps it.
|
// choice still wins in both directions, so anyone who opens it keeps it.
|
||||||
if (panelId === 'pp-toggles' && stored === null) collapsed = true;
|
if (panelId === 'pp-toggles' && stored === null) collapsed = true;
|
||||||
panelEl.classList.toggle('collapsed', collapsed);
|
this._presentPanel(panelEl, { collapsed });
|
||||||
// Bodies that expand themselves on first appearance must not override a
|
// Bodies that expand themselves on first appearance must not override a
|
||||||
// choice the user (or a share link) already made.
|
// choice the user (or a share link) already made.
|
||||||
if (panelEl.dataset)
|
if (panelEl.dataset)
|
||||||
@@ -84,7 +84,6 @@ export class PanelPositionControls {
|
|||||||
: stored === null
|
: stored === null
|
||||||
? 'default'
|
? 'default'
|
||||||
: 'stored';
|
: 'stored';
|
||||||
this._syncPanelCollapseButton(panelEl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_savePanelCollapsedState(panelId, collapsed) {
|
_savePanelCollapsedState(panelId, collapsed) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
layoutRightPanelRail,
|
layoutRightPanelRail,
|
||||||
measurePanelNaturalHeight,
|
measurePanelNaturalHeight,
|
||||||
} from './panelRails.js';
|
} from './panelRails.js';
|
||||||
|
import { applyPanelState, readPanelState } from './rail/panelState.js';
|
||||||
|
|
||||||
test('Display uses an inner scroll body only in Cyber', () => {
|
test('Display uses an inner scroll body only in Cyber', () => {
|
||||||
const body = { scrollTop: 95 };
|
const body = { scrollTop: 95 };
|
||||||
@@ -169,7 +170,11 @@ function fixture(
|
|||||||
matchMedia: () => ({ matches: mobile }),
|
matchMedia: () => ({ matches: mobile }),
|
||||||
getComputedStyle: (node) => node.computed,
|
getComputedStyle: (node) => node.computed,
|
||||||
},
|
},
|
||||||
onCollapse: (panel) => collapsed.push(panel.id),
|
// The owner presents the patch; the panel's root draws it at once.
|
||||||
|
onCollapse: (panel, patch) => {
|
||||||
|
collapsed.push(panel.id);
|
||||||
|
applyPanelState(panel, { ...readPanelState(panel), ...patch });
|
||||||
|
},
|
||||||
onRetry: () => {
|
onRetry: () => {
|
||||||
retries += 1;
|
retries += 1;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { watch } from 'vue';
|
||||||
|
import { usePanelChromeStore } from '../stores/panelChrome.js';
|
||||||
|
|
||||||
|
/** The classes a panel's state draws on its root. */
|
||||||
|
const CLASSES = Object.freeze({
|
||||||
|
collapsed: (state) => state.collapsed,
|
||||||
|
'dock-pinned': (state) => state.pinned,
|
||||||
|
'dock-pinned-top': (state) => state.pinnedTop,
|
||||||
|
'layout-auto-collapsed': (state) => state.auto === 'layout',
|
||||||
|
'accordion-collapsed': (state) => state.auto === 'accordion',
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw `state` on a panel's root with `classList`, leaving other classes.
|
||||||
|
* @param {Element} el
|
||||||
|
* @param {import('../stores/panelChrome.js').PanelState} state
|
||||||
|
*/
|
||||||
|
export function applyPanelState(el, state) {
|
||||||
|
for (const [name, on] of Object.entries(CLASSES))
|
||||||
|
if (el.classList.contains(name) !== on(state))
|
||||||
|
el.classList.toggle(name, on(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep `el` drawing the state `store` holds for `panelId`, synchronously:
|
||||||
|
* the panel layouts measure a panel in the same task that opens or closes
|
||||||
|
* it. Until the panel is presented its markup's classes stand.
|
||||||
|
* @returns {() => void} Stops following the store.
|
||||||
|
*/
|
||||||
|
export function followPanelState(store, panelId, el) {
|
||||||
|
return watch(
|
||||||
|
() => store.panels[panelId],
|
||||||
|
(state) => state && applyPanelState(el, state),
|
||||||
|
{ flush: 'sync', immediate: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `v-panel-state="'data-panel'"`: the panel root draws its open, pinned and
|
||||||
|
* layout-collapsed state from the panel chrome store.
|
||||||
|
*/
|
||||||
|
export const vPanelState = {
|
||||||
|
mounted(el, binding) {
|
||||||
|
const store = usePanelChromeStore(binding.instance.$pinia);
|
||||||
|
el._stopPanelState = followPanelState(store, binding.value, el);
|
||||||
|
},
|
||||||
|
beforeUnmount(el) {
|
||||||
|
el._stopPanelState?.();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A panel's state as its classes draw it: the markup's state, before its
|
||||||
|
* owner first presents the panel.
|
||||||
|
* @param {Element} el
|
||||||
|
* @returns {import('../stores/panelChrome.js').PanelState}
|
||||||
|
*/
|
||||||
|
export function readPanelState(el) {
|
||||||
|
const has = (name) => el.classList.contains(name);
|
||||||
|
return {
|
||||||
|
collapsed: has('collapsed'),
|
||||||
|
pinned: has('dock-pinned'),
|
||||||
|
pinnedTop: has('dock-pinned-top'),
|
||||||
|
auto: has('layout-auto-collapsed')
|
||||||
|
? 'layout'
|
||||||
|
: has('accordion-collapsed')
|
||||||
|
? 'accordion'
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -41,7 +41,8 @@ function settledLeftRailTop(leftStack, viewportHeight, getComputedStyle) {
|
|||||||
* @param {Window} options.windowRef Viewport and style reader.
|
* @param {Window} options.windowRef Viewport and style reader.
|
||||||
* @param {{visible: boolean, variant: string}} options.hud Current HUD presentation.
|
* @param {{visible: boolean, variant: string}} options.hud Current HUD presentation.
|
||||||
* @param {string} options.preferredPanelId Most recently opened panel.
|
* @param {string} options.preferredPanelId Most recently opened panel.
|
||||||
* @param {Function} options.onCollapse Update a panel's disclosure chrome.
|
* @param {Function} options.onCollapse Present a panel's collapse:
|
||||||
|
* `(panel, {collapsed, auto})`.
|
||||||
* @param {Function} options.onRetry Request another pass after automatic collapse.
|
* @param {Function} options.onRetry Request another pass after automatic collapse.
|
||||||
* @param {Function} [options.getComputedStyle] Optional DOM style reader override.
|
* @param {Function} [options.getComputedStyle] Optional DOM style reader override.
|
||||||
* @param {HTMLElement} options.leftStack Rail supplying the shared top baseline.
|
* @param {HTMLElement} options.leftStack Rail supplying the shared top baseline.
|
||||||
@@ -83,8 +84,7 @@ export function layoutRightPanelRail({
|
|||||||
for (const panel of panels.filter((item) =>
|
for (const panel of panels.filter((item) =>
|
||||||
item.classList.contains('layout-auto-collapsed'),
|
item.classList.contains('layout-auto-collapsed'),
|
||||||
)) {
|
)) {
|
||||||
panel.classList.remove('collapsed', 'layout-auto-collapsed');
|
onCollapse(panel, { collapsed: false, auto: null });
|
||||||
onCollapse(panel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// One panel is open per desktop side; this marker is distinct from
|
// One panel is open per desktop side; this marker is distinct from
|
||||||
@@ -275,8 +275,7 @@ export function layoutRightPanelRail({
|
|||||||
if (!isCollapseRetry && autoCollapseIndices.length) {
|
if (!isCollapseRetry && autoCollapseIndices.length) {
|
||||||
for (const index of autoCollapseIndices) {
|
for (const index of autoCollapseIndices) {
|
||||||
const panel = expandedPanels[index];
|
const panel = expandedPanels[index];
|
||||||
panel.classList.add('collapsed', 'layout-auto-collapsed');
|
onCollapse(panel, { collapsed: true, auto: 'layout' });
|
||||||
onCollapse(panel);
|
|
||||||
}
|
}
|
||||||
pendingCollapseRetries.add(stack);
|
pendingCollapseRetries.add(stack);
|
||||||
onRetry();
|
onRetry();
|
||||||
|
|||||||
@@ -1024,13 +1024,14 @@ export class ShellFacade {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates collapse button glyphs based on panel state. Right-rail panels
|
* Presents a panel's state, merged over what it shows now; its root and
|
||||||
* use directional arrows; left-stack panels use +/- symbols.
|
* buttons draw it from the panel chrome store.
|
||||||
* @param {HTMLElement} panelEl - The panel DOM element.
|
* @param {HTMLElement|string} panelOrId - The panel or its id.
|
||||||
|
* @param {object} [patch] - The state that changes.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_syncPanelCollapseButton(panelEl) {
|
_presentPanel(panelOrId, patch) {
|
||||||
return this._panelChrome._syncPanelCollapseButton(...arguments);
|
return this._panelChrome._presentPanel(...arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildSharePanelState() {
|
_buildSharePanelState() {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import { UiLifetime } from './uiLifetime.js';
|
import { UiLifetime } from './uiLifetime.js';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
import { PanelChrome } from './panelChrome.js';
|
import { PanelChrome } from './panelChrome.js';
|
||||||
|
import { followPanelState } from './rail/panelState.js';
|
||||||
import { usePanelChromeStore } from './stores/panelChrome.js';
|
import { usePanelChromeStore } from './stores/panelChrome.js';
|
||||||
import { PanelLayoutController } from './panelLayoutController.js';
|
import { PanelLayoutController } from './panelLayoutController.js';
|
||||||
import { ShellFeedback } from './shellFeedback.js';
|
import { ShellFeedback } from './shellFeedback.js';
|
||||||
@@ -150,7 +151,7 @@ test('panel layout coalesces each rail and cannot rearm after destruction', () =
|
|||||||
const owner = new PanelLayoutController({
|
const owner = new PanelLayoutController({
|
||||||
readHud: () => ({}),
|
readHud: () => ({}),
|
||||||
scheduleCockpitLayout() {},
|
scheduleCockpitLayout() {},
|
||||||
syncPanelCollapseButton() {},
|
presentPanel() {},
|
||||||
readDisplayScrollTop: () => 0,
|
readDisplayScrollTop: () => 0,
|
||||||
});
|
});
|
||||||
owner._syncLeftPanelAdaptiveLayout = () => layouts++;
|
owner._syncLeftPanelAdaptiveLayout = () => layouts++;
|
||||||
@@ -257,7 +258,7 @@ test('recording restores the original HUD after repeated entry and revokes retai
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('weather rail orders between CCTV and Context, observes and syncs chrome', (t) => {
|
test('weather rail orders between CCTV and Context, observes and presents chrome', (t) => {
|
||||||
const f = fixture();
|
const f = fixture();
|
||||||
const observed = new Set();
|
const observed = new Set();
|
||||||
const previousResizeObserver = globalThis.ResizeObserver;
|
const previousResizeObserver = globalThis.ResizeObserver;
|
||||||
@@ -298,16 +299,16 @@ test('weather rail orders between CCTV and Context, observes and syncs chrome',
|
|||||||
}
|
}
|
||||||
const weather = f.nodes.get('weather-panel');
|
const weather = f.nodes.get('weather-panel');
|
||||||
stack.children = [weather, f.nodes.get('global-context-panel')];
|
stack.children = [weather, f.nodes.get('global-context-panel')];
|
||||||
const synced = [];
|
|
||||||
const store = usePanelChromeStore(createPinia());
|
const store = usePanelChromeStore(createPinia());
|
||||||
const chrome = { _chromeStore: store, _chromeToken: store.connect({}) };
|
const chrome = { _chromeStore: store, _chromeToken: store.connect({}) };
|
||||||
|
const presented = [];
|
||||||
const owner = new PanelLayoutController({
|
const owner = new PanelLayoutController({
|
||||||
readHud: () => ({}),
|
readHud: () => ({}),
|
||||||
scheduleCockpitLayout() {},
|
scheduleCockpitLayout() {},
|
||||||
readDisplayScrollTop: () => 0,
|
readDisplayScrollTop: () => 0,
|
||||||
syncPanelCollapseButton(panel) {
|
presentPanel(panel, patch) {
|
||||||
synced.push(panel.id);
|
presented.push(panel.id);
|
||||||
PanelChrome.prototype._syncPanelCollapseButton.call(chrome, panel);
|
PanelChrome.prototype._presentPanel.call(chrome, panel, patch);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
owner._initRightPanelAdaptiveLayout();
|
owner._initRightPanelAdaptiveLayout();
|
||||||
@@ -315,12 +316,15 @@ test('weather rail orders between CCTV and Context, observes and syncs chrome',
|
|||||||
stack.children.map((panel) => panel.id),
|
stack.children.map((panel) => panel.id),
|
||||||
['pp-toggles', 'cctv-panel', 'weather-panel', 'global-context-panel'],
|
['pp-toggles', 'cctv-panel', 'weather-panel', 'global-context-panel'],
|
||||||
);
|
);
|
||||||
assert.ok(synced.includes('weather-panel'));
|
|
||||||
assert.ok(observed.has(weather));
|
assert.ok(observed.has(weather));
|
||||||
assert.equal(store.panels['weather-panel'].collapsed, true);
|
// Moving a panel into the rail leaves its state as it was.
|
||||||
weather.classList.remove('collapsed');
|
assert.deepEqual(presented, []);
|
||||||
PanelChrome.prototype._syncPanelCollapseButton.call(chrome, weather);
|
followPanelState(store, 'weather-panel', weather);
|
||||||
|
PanelChrome.prototype._presentPanel.call(chrome, weather, {
|
||||||
|
collapsed: false,
|
||||||
|
});
|
||||||
assert.equal(store.panels['weather-panel'].collapsed, false);
|
assert.equal(store.panels['weather-panel'].collapsed, false);
|
||||||
|
assert.equal(weather.classList.contains('collapsed'), false);
|
||||||
owner.destroy();
|
owner.destroy();
|
||||||
assert.equal(observed.size, 0);
|
assert.equal(observed.size, 0);
|
||||||
assert.equal(f.frames.size, 0);
|
assert.equal(f.frames.size, 0);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { PanelChrome } from './panelChrome.js';
|
import { PanelChrome } from './panelChrome.js';
|
||||||
|
import { presentPanelLocally } from '../testSupport/panelState.mjs';
|
||||||
|
|
||||||
function fixture(theme = 'cyber') {
|
function fixture(theme = 'cyber') {
|
||||||
const nodes = new Map();
|
const nodes = new Map();
|
||||||
@@ -35,7 +36,7 @@ function fixture(theme = 'cyber') {
|
|||||||
_rightPanelStack: rail,
|
_rightPanelStack: rail,
|
||||||
_panelLayout: {},
|
_panelLayout: {},
|
||||||
_lifetime: { frame() {} },
|
_lifetime: { frame() {} },
|
||||||
_syncPanelCollapseButton() {}, _scheduleRightPanelLayout() {},
|
_presentPanel: presentPanelLocally, _scheduleRightPanelLayout() {},
|
||||||
_scheduleLeftPanelLayout() {}, _layoutRightPanels() {}, _syncCctvPanelViewport() {},
|
_scheduleLeftPanelLayout() {}, _layoutRightPanels() {}, _syncCctvPanelViewport() {},
|
||||||
_savePanelCollapsedState: (...args) => saved.push(args),
|
_savePanelCollapsedState: (...args) => saved.push(args),
|
||||||
shareLinkManager: { claimRestoreLane: (...args) => claimed.push(args), onPanelStateChange() {} },
|
shareLinkManager: { claimRestoreLane: (...args) => claimed.push(args), onPanelStateChange() {} },
|
||||||
|
|||||||
@@ -8,15 +8,35 @@ import { shallowRef } from 'vue';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Each panel's disclosure state as its collapse, dock and pin buttons render
|
* @typedef {object} PanelState
|
||||||
* it. PanelChrome owns the panels: it publishes here whenever a panel opens,
|
* @property {boolean} collapsed Whether the panel is closed, by the person or
|
||||||
* closes or is pinned, and connects the buttons' clicks; the views only render
|
* by its layout.
|
||||||
* and forward clicks.
|
* @property {boolean} pinned A dock tray kept open (`dock-pinned`).
|
||||||
|
* @property {boolean} pinnedTop The pinned tray drawn on top.
|
||||||
|
* @property {?('layout'|'accordion')} auto Why a layout closed the panel
|
||||||
|
* (`layout-auto-collapsed`, `accordion-collapsed`), or null when the
|
||||||
|
* person's own choice stands.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** A panel's state before its owner has said otherwise. */
|
||||||
|
const BLANK = Object.freeze({
|
||||||
|
collapsed: false,
|
||||||
|
pinned: false,
|
||||||
|
pinnedTop: false,
|
||||||
|
auto: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each panel's disclosure state, which its root (`vPanelState`) and its
|
||||||
|
* collapse, dock and pin buttons render. PanelChrome owns the panels: it and
|
||||||
|
* the layouts it runs publish here whenever a panel opens, closes or is
|
||||||
|
* pinned, and it connects the buttons' clicks; the views only render and
|
||||||
|
* forward clicks.
|
||||||
*/
|
*/
|
||||||
export const usePanelChromeStore = defineStore('gev-panel-chrome', () => {
|
export const usePanelChromeStore = defineStore('gev-panel-chrome', () => {
|
||||||
/**
|
/**
|
||||||
* `{[panelId]: {collapsed, pinned}}`, frozen; a panel is absent until its
|
* `{[panelId]: PanelState}`, frozen; a panel is absent until its owner
|
||||||
* owner first presents it, and its buttons keep their markup until then.
|
* first presents it, and it keeps its markup's classes until then.
|
||||||
*/
|
*/
|
||||||
const panels = shallowRef(Object.freeze({}));
|
const panels = shallowRef(Object.freeze({}));
|
||||||
/** @type {{handlers: PanelChromeHandlers}|null} */
|
/** @type {{handlers: PanelChromeHandlers}|null} */
|
||||||
@@ -44,17 +64,13 @@ export const usePanelChromeStore = defineStore('gev-panel-chrome', () => {
|
|||||||
* unchanged state is not republished.
|
* unchanged state is not republished.
|
||||||
* @param {object} token
|
* @param {object} token
|
||||||
* @param {string} panelId
|
* @param {string} panelId
|
||||||
* @param {{collapsed?: boolean, pinned?: boolean}} patch
|
* @param {Partial<PanelState>} patch
|
||||||
*/
|
*/
|
||||||
function publish(token, panelId, patch) {
|
function publish(token, panelId, patch) {
|
||||||
if (!owns(token) || !panelId) return;
|
if (!owns(token) || !panelId) return;
|
||||||
const prior = panels.value[panelId];
|
const prior = panels.value[panelId];
|
||||||
const next = { collapsed: false, pinned: false, ...prior, ...patch };
|
const next = { ...BLANK, ...prior, ...patch };
|
||||||
if (
|
if (prior && Object.keys(BLANK).every((key) => prior[key] === next[key]))
|
||||||
prior &&
|
|
||||||
prior.collapsed === next.collapsed &&
|
|
||||||
prior.pinned === next.pinned
|
|
||||||
)
|
|
||||||
return;
|
return;
|
||||||
panels.value = Object.freeze({
|
panels.value = Object.freeze({
|
||||||
...panels.value,
|
...panels.value,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { msg, t } from '../../i18n/index.js';
|
import { msg, t } from '../../i18n/index.js';
|
||||||
|
import { vPanelState } from '../rail/panelState.js';
|
||||||
import UiButton from '../kit/UiButton.vue';
|
import UiButton from '../kit/UiButton.vue';
|
||||||
import { vClassToggle } from '../rail/classToggle.js';
|
import { vClassToggle } from '../rail/classToggle.js';
|
||||||
import { useDisplayStore } from '../stores/display.js';
|
import { useDisplayStore } from '../stores/display.js';
|
||||||
@@ -67,7 +68,7 @@ function selectMapSource(chip) {
|
|||||||
<div id="command-dock" class="ui-scroll" aria-label="Navigation, voice, and visual preset controls" data-i18n-aria-label>
|
<div id="command-dock" class="ui-scroll" aria-label="Navigation, voice, and visual preset controls" data-i18n-aria-label>
|
||||||
<template v-for="slot in dockOrder" :key="slot">
|
<template v-for="slot in dockOrder" :key="slot">
|
||||||
<!-- Control panel -->
|
<!-- Control panel -->
|
||||||
<div v-if="slot === 'control'" id="control-panel" class="panel-collapsible collapsed">
|
<div v-if="slot === 'control'" id="control-panel" v-panel-state="'control-panel'" class="panel-collapsible collapsed">
|
||||||
<div class="panel-glow"></div>
|
<div class="panel-glow"></div>
|
||||||
<div class="panel-inner">
|
<div class="panel-inner">
|
||||||
<button id="control-panel-toggle" class="panel-header dock-tray-toggle" type="button"
|
<button id="control-panel-toggle" class="panel-header dock-tray-toggle" type="button"
|
||||||
@@ -128,7 +129,7 @@ function selectMapSource(chip) {
|
|||||||
<VoiceControl v-else-if="slot === 'voice'" :key="voice.id" :view="voice" />
|
<VoiceControl v-else-if="slot === 'voice'" :key="voice.id" :view="voice" />
|
||||||
|
|
||||||
<!-- Location bar -->
|
<!-- Location bar -->
|
||||||
<div v-else-if="slot === 'location'" id="location-bar" class="panel-collapsible collapsed">
|
<div v-else-if="slot === 'location'" id="location-bar" v-panel-state="'location-bar'" class="panel-collapsible collapsed">
|
||||||
<div class="panel-glow"></div>
|
<div class="panel-glow"></div>
|
||||||
<div class="location-inner">
|
<div class="location-inner">
|
||||||
<div class="location-toolbar">
|
<div class="location-toolbar">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { t } from '../../i18n/index.js';
|
import { t } from '../../i18n/index.js';
|
||||||
|
import { vPanelState } from '../rail/panelState.js';
|
||||||
import PanelCollapseButton from '../rail/PanelCollapseButton.vue';
|
import PanelCollapseButton from '../rail/PanelCollapseButton.vue';
|
||||||
import { vClassToggle } from '../rail/classToggle.js';
|
import { vClassToggle } from '../rail/classToggle.js';
|
||||||
import { useCockpitStore } from '../stores/cockpit.js';
|
import { useCockpitStore } from '../stores/cockpit.js';
|
||||||
@@ -28,7 +29,7 @@ const sceneRuntime = computed(() => scenePanel.value?.runtime ?? '');
|
|||||||
<template>
|
<template>
|
||||||
<!-- Global Context is deliberately independent from the left layer accordion. -->
|
<!-- Global Context is deliberately independent from the left layer accordion. -->
|
||||||
<aside id="right-context-rail" class="ui-scroll" v-class-toggle="{ 'bhote-event-active': !!bhotePanel }">
|
<aside id="right-context-rail" class="ui-scroll" v-class-toggle="{ 'bhote-event-active': !!bhotePanel }">
|
||||||
<div id="weather-panel" class="panel-collapsible" data-panel-id="weather-panel" hidden>
|
<div id="weather-panel" v-panel-state="'weather-panel'" class="panel-collapsible" data-panel-id="weather-panel" hidden>
|
||||||
<div class="panel-glow"></div>
|
<div class="panel-glow"></div>
|
||||||
<div class="weather-panel-inner" data-rail-scroller>
|
<div class="weather-panel-inner" data-rail-scroller>
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
@@ -40,7 +41,7 @@ const sceneRuntime = computed(() => scenePanel.value?.runtime ?? '');
|
|||||||
<div id="weather-panel-body" class="ui-scroll" data-rail-scroller></div>
|
<div id="weather-panel-body" class="ui-scroll" data-rail-scroller></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="recent-imagery-panel" class="panel-collapsible" data-panel-id="recent-imagery-panel" hidden>
|
<div id="recent-imagery-panel" v-panel-state="'recent-imagery-panel'" class="panel-collapsible" data-panel-id="recent-imagery-panel" hidden>
|
||||||
<div class="panel-glow"></div>
|
<div class="panel-glow"></div>
|
||||||
<div class="recent-imagery-panel-inner" data-rail-scroller>
|
<div class="recent-imagery-panel-inner" data-rail-scroller>
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
@@ -52,7 +53,7 @@ const sceneRuntime = computed(() => scenePanel.value?.runtime ?? '');
|
|||||||
<div id="recent-imagery-panel-body" class="ui-scroll" data-rail-scroller></div>
|
<div id="recent-imagery-panel-body" class="ui-scroll" data-rail-scroller></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="global-context-panel" class="panel-collapsible collapsed" v-class-toggle="{ ...(ctx && { 'context-enabled': ctx.enabled }) }" :data-context-mode="ctx?.mode" data-panel-id="global-context-panel">
|
<div id="global-context-panel" v-panel-state="'global-context-panel'" class="panel-collapsible collapsed" v-class-toggle="{ ...(ctx && { 'context-enabled': ctx.enabled }) }" :data-context-mode="ctx?.mode" data-panel-id="global-context-panel">
|
||||||
<div class="panel-glow"></div>
|
<div class="panel-glow"></div>
|
||||||
<div class="global-context-panel-inner ui-scroll" data-rail-scroller>
|
<div class="global-context-panel-inner ui-scroll" data-rail-scroller>
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { t } from '../../i18n/index.js';
|
import { t } from '../../i18n/index.js';
|
||||||
|
import { vPanelState } from '../rail/panelState.js';
|
||||||
import PanelCollapseButton from '../rail/PanelCollapseButton.vue';
|
import PanelCollapseButton from '../rail/PanelCollapseButton.vue';
|
||||||
import { useSceneSharingStore } from '../stores/sceneSharing.js';
|
import { useSceneSharingStore } from '../stores/sceneSharing.js';
|
||||||
import { useSceneStore } from '../stores/scenes.js';
|
import { useSceneStore } from '../stores/scenes.js';
|
||||||
@@ -37,7 +38,7 @@ async function importChosen(event) {
|
|||||||
<template>
|
<template>
|
||||||
<div id="left-panel-stack" class="ui-scroll">
|
<div id="left-panel-stack" class="ui-scroll">
|
||||||
<!-- Data Layer Toggle Panel -->
|
<!-- Data Layer Toggle Panel -->
|
||||||
<div id="data-panel" class="panel-collapsible collapsed active" data-panel-id="data-panel">
|
<div id="data-panel" v-panel-state="'data-panel'" class="panel-collapsible collapsed active" data-panel-id="data-panel">
|
||||||
<div class="panel-glow"></div>
|
<div class="panel-glow"></div>
|
||||||
<div class="data-panel-inner">
|
<div class="data-panel-inner">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
@@ -56,7 +57,7 @@ async function importChosen(event) {
|
|||||||
<StreetLevelPanel />
|
<StreetLevelPanel />
|
||||||
|
|
||||||
<!-- Scene Director Panel -->
|
<!-- Scene Director Panel -->
|
||||||
<div id="scene-panel" v-class-toggle="{ running: scene?.running, 'has-shots': scene?.hasShots }" class="panel-collapsible collapsed" data-panel-id="scene-panel">
|
<div id="scene-panel" v-panel-state="'scene-panel'" v-class-toggle="{ running: scene?.running, 'has-shots': scene?.hasShots }" class="panel-collapsible collapsed" data-panel-id="scene-panel">
|
||||||
<div class="panel-glow"></div>
|
<div class="panel-glow"></div>
|
||||||
<div class="scene-panel-inner ui-scroll">
|
<div class="scene-panel-inner ui-scroll">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { t } from '../../../i18n/index.js';
|
import { t } from '../../../i18n/index.js';
|
||||||
|
import { vPanelState } from '../../rail/panelState.js';
|
||||||
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
||||||
import CctvCalValue from '../../cctv/CctvCalValue.vue';
|
import CctvCalValue from '../../cctv/CctvCalValue.vue';
|
||||||
import CctvPicker from '../../cctv/CctvPicker.vue';
|
import CctvPicker from '../../cctv/CctvPicker.vue';
|
||||||
@@ -27,7 +28,7 @@ const frameData = computed(() =>
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="cctv-panel" class="panel-collapsible collapsed" data-panel-id="cctv-panel">
|
<div id="cctv-panel" v-panel-state="'cctv-panel'" class="panel-collapsible collapsed" data-panel-id="cctv-panel">
|
||||||
<div class="panel-glow"></div>
|
<div class="panel-glow"></div>
|
||||||
<div class="cctv-panel-inner ui-scroll" data-rail-scroller>
|
<div class="cctv-panel-inner ui-scroll" data-rail-scroller>
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { t } from '../../../i18n/index.js';
|
import { t } from '../../../i18n/index.js';
|
||||||
|
import { vPanelState } from '../../rail/panelState.js';
|
||||||
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
||||||
import { vRangeValue } from '../../rail/rangeValue.js';
|
import { vRangeValue } from '../../rail/rangeValue.js';
|
||||||
import { useDisplayStore } from '../../stores/display.js';
|
import { useDisplayStore } from '../../stores/display.js';
|
||||||
@@ -43,7 +44,7 @@ const geoListed = computed(() => geoRows.value.length > 0);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Post-processing toggles (top-right) -->
|
<!-- Post-processing toggles (top-right) -->
|
||||||
<div id="pp-toggles" class="panel-collapsible collapsed ui-scroll" data-panel-id="pp-toggles">
|
<div id="pp-toggles" v-panel-state="'pp-toggles'" class="panel-collapsible collapsed ui-scroll" data-panel-id="pp-toggles">
|
||||||
<div class="compact pp-header-row">
|
<div class="compact pp-header-row">
|
||||||
<span data-i18n class="pp-header-label">DISPLAY</span>
|
<span data-i18n class="pp-header-label">DISPLAY</span>
|
||||||
<span class="panel-divider" aria-hidden="true"></span>
|
<span class="panel-divider" aria-hidden="true"></span>
|
||||||
@@ -123,7 +124,7 @@ const geoListed = computed(() => geoRows.value.length > 0);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- The one shared preset-parameter surface is inserted here at runtime. -->
|
<!-- The one shared preset-parameter surface is inserted here at runtime. -->
|
||||||
<div id="param-slider-panel">
|
<div id="param-slider-panel" v-panel-state="'param-slider-panel'">
|
||||||
<div class="param-panel-header">
|
<div class="param-panel-header">
|
||||||
<span class="param-panel-title" data-i18n>PARAMETERS</span>
|
<span class="param-panel-title" data-i18n>PARAMETERS</span>
|
||||||
<span class="param-panel-divider"></span>
|
<span class="param-panel-divider"></span>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { t } from '../../../i18n/index.js';
|
import { t } from '../../../i18n/index.js';
|
||||||
|
import { vPanelState } from '../../rail/panelState.js';
|
||||||
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
||||||
import { vClassToggle } from '../../rail/classToggle.js';
|
import { vClassToggle } from '../../rail/classToggle.js';
|
||||||
import { vFieldValue } from '../../rail/fieldValue.js';
|
import { vFieldValue } from '../../rail/fieldValue.js';
|
||||||
@@ -29,7 +30,7 @@ const radioWords = {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section id="radio-panel" class="panel-collapsible collapsed" v-class-toggle="{ ...(radio && { 'radio-enabled': radio.enabled, 'lifecycle-uncertain': radio.uncertain }), ...(sdrConnected !== null && { 'sdr-connected': sdrConnected }) }" data-panel-id="radio-panel" aria-label="Internet radio and local SDR" data-i18n-aria-label>
|
<section id="radio-panel" v-panel-state="'radio-panel'" class="panel-collapsible collapsed" v-class-toggle="{ ...(radio && { 'radio-enabled': radio.enabled, 'lifecycle-uncertain': radio.uncertain }), ...(sdrConnected !== null && { 'sdr-connected': sdrConnected }) }" data-panel-id="radio-panel" aria-label="Internet radio and local SDR" data-i18n-aria-label>
|
||||||
<div class="radio-panel-inner ui-scroll">
|
<div class="radio-panel-inner ui-scroll">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="panel-title" data-i18n>RADIO</span>
|
<span class="panel-title" data-i18n>RADIO</span>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed, shallowRef, watch } from 'vue';
|
import { computed, shallowRef, watch } from 'vue';
|
||||||
import { t } from '../../../i18n/index.js';
|
import { t } from '../../../i18n/index.js';
|
||||||
|
import { vPanelState } from '../../rail/panelState.js';
|
||||||
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
||||||
import RailChips from '../../rail/RailChips.vue';
|
import RailChips from '../../rail/RailChips.vue';
|
||||||
import { useStreetLevelStore } from '../../stores/streetLevel.js';
|
import { useStreetLevelStore } from '../../stores/streetLevel.js';
|
||||||
@@ -101,7 +102,7 @@ const vThumb = { mounted: placeThumb, updated: placeThumb };
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="street-level-panel" class="panel-collapsible collapsed" data-panel-id="street-level-panel" :data-sl-enabled="view ? String(view.enabled) : undefined">
|
<div id="street-level-panel" v-panel-state="'street-level-panel'" class="panel-collapsible collapsed" data-panel-id="street-level-panel" :data-sl-enabled="view ? String(view.enabled) : undefined">
|
||||||
<div class="panel-glow"></div>
|
<div class="panel-glow"></div>
|
||||||
<div class="street-level-panel-inner ui-scroll" data-rail-scroller>
|
<div class="street-level-panel-inner ui-scroll" data-rail-scroller>
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
|
|||||||
@@ -445,8 +445,7 @@ export class VisualSettings {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this._cockpitDisclosureOpen?.('display')) return;
|
if (!this._cockpitDisclosureOpen?.('display')) return;
|
||||||
this._sliderPanel.classList.remove('collapsed');
|
this._presentPanel(this._sliderPanel, { collapsed: false });
|
||||||
this._syncPanelCollapseButton(this._sliderPanel);
|
|
||||||
this._lifetime.frame(() =>
|
this._lifetime.frame(() =>
|
||||||
this._lifetime.frame(() => {
|
this._lifetime.frame(() => {
|
||||||
this._sliderPanel?.scrollIntoView?.({ block: 'nearest' });
|
this._sliderPanel?.scrollIntoView?.({ block: 'nearest' });
|
||||||
@@ -1434,8 +1433,7 @@ export class VisualSettings {
|
|||||||
_revealStyleParameters() {
|
_revealStyleParameters() {
|
||||||
if (!this._sliderPanel?.classList.contains('active')) return;
|
if (!this._sliderPanel?.classList.contains('active')) return;
|
||||||
if (this._cockpitDisplayPortalActive) return;
|
if (this._cockpitDisplayPortalActive) return;
|
||||||
this._sliderPanel.classList.remove('collapsed');
|
this._presentPanel(this._sliderPanel, { collapsed: false });
|
||||||
this._syncPanelCollapseButton(this._sliderPanel);
|
|
||||||
this.setPanelCollapsed('pp-toggles', false, { explicit: true });
|
this.setPanelCollapsed('pp-toggles', false, { explicit: true });
|
||||||
this._lifetime.frame(() =>
|
this._lifetime.frame(() =>
|
||||||
this._lifetime.frame(() => {
|
this._lifetime.frame(() => {
|
||||||
|
|||||||
+18
-7
@@ -817,7 +817,17 @@ export function createGevActionRunner({
|
|||||||
...(!applied ? { error: say } : {}),
|
...(!applied ? { error: say } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
setPanelOpen(styleManager, panelId, open);
|
if (!setPanelOpen(styleManager, panelId, open)) {
|
||||||
|
const say = 'There is no panel to open here.';
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
action: 'set_panel_open',
|
||||||
|
panelId,
|
||||||
|
open,
|
||||||
|
say,
|
||||||
|
error: say,
|
||||||
|
};
|
||||||
|
}
|
||||||
return { ok: true, action: 'set_panel_open', panelId, open };
|
return { ok: true, action: 'set_panel_open', panelId, open };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3143,13 +3153,14 @@ function normalizeAircraftClassFilter(value) {
|
|||||||
return raw.replace(/[\s-]+/g, '') === TR3B_CLASS ? TR3B_CLASS : raw;
|
return raw.replace(/[\s-]+/g, '') === TR3B_CLASS ? TR3B_CLASS : raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean} Whether a shell took the change: its panel chrome owns
|
||||||
|
* panel state, so without one there is no panel to open.
|
||||||
|
*/
|
||||||
function setPanelOpen(styleManager, panelId, open) {
|
function setPanelOpen(styleManager, panelId, open) {
|
||||||
if (styleManager && typeof styleManager.setPanelCollapsed === 'function') {
|
if (typeof styleManager?.setPanelCollapsed !== 'function') return false;
|
||||||
styleManager.setPanelCollapsed(panelId, !open, { explicit: true });
|
styleManager.setPanelCollapsed(panelId, !open, { explicit: true });
|
||||||
} else {
|
return true;
|
||||||
const panel = document.getElementById(panelId);
|
|
||||||
if (panel) panel.classList.toggle('collapsed', !open);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeContextMode(value) {
|
function normalizeContextMode(value) {
|
||||||
|
|||||||
Reference in new issue
Block a user