mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +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
@@ -1088,7 +1088,7 @@ test('Cockpit Display portals shared HUD, Detection, Parameters, and 3D controls
|
||||
);
|
||||
assert.match(
|
||||
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(
|
||||
radioBindings,
|
||||
|
||||
@@ -6,6 +6,7 @@ import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createPinia } from 'pinia';
|
||||
import { msg, t } from './i18n/index.js';
|
||||
import { followPanelState, readPanelState } from './ui/rail/panelState.js';
|
||||
import { usePanelChromeStore } from './ui/stores/panelChrome.js';
|
||||
|
||||
// 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.performance = { now: () => now };
|
||||
document.defaultView = window;
|
||||
const methods = new Function('createHoverDisclosure', 'collapsePanelOnEscape', 'document', 'window', 'clearTimeout', 'performance', 'requestAnimationFrame', 't', 'msg',
|
||||
`return ({${shellMethod('_initAutoHoverPanel').toString()},\n${shellMethod('_collapsePanelOnEscape').toString()},\n${shellMethod('setPanelCollapsed').toString()},\n${shellMethod('_syncPanelCollapseButton').toString()}});`)(
|
||||
createHoverDisclosure, collapsePanelOnEscape, document, window, (id) => timers.delete(id), { now: () => now }, () => {}, 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('_presentPanel').toString()}});`)(
|
||||
createHoverDisclosure, collapsePanelOnEscape, document, window, (id) => timers.delete(id), { now: () => now }, () => {}, t, msg, readPanelState,
|
||||
);
|
||||
const saves = [];
|
||||
const claims = [];
|
||||
let shareSyncs = 0;
|
||||
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 = {
|
||||
_lifetime: { frame() {} },
|
||||
_chromeStore: chromeStore,
|
||||
_chromeToken: chromeStore.connect({}),
|
||||
_chromeToken: chromeToken,
|
||||
_panelLayout: { _leftStackPreferredPanelId: null, _rightStackPreferredPanelId: null },
|
||||
...methods,
|
||||
_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 css = readStylesheet(new URL('../style.css', import.meta.url));
|
||||
assert.doesNotMatch(ui, /_enforce(?:Left|Right)PanelAccordion/);
|
||||
assert.match(rails, /classList\.add\('collapsed', 'layout-auto-collapsed'\)/);
|
||||
assert.match(readFileSync(new URL('./ui/panelLayoutController.js', import.meta.url), 'utf8'), /classList\.remove\('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'), /_presentPanel\(panel, \{ collapsed: false, auto: null \}\)/);
|
||||
assert.match(ui, /reconsiderAutoCollapse/);
|
||||
assert.match(
|
||||
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*\)/);
|
||||
// Collapse buttons render aria-expanded from the state the chrome presents
|
||||
// (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(rightRail, /--right-panel-allocated-height/);
|
||||
assert.match(
|
||||
|
||||
@@ -359,7 +359,7 @@ test('panel collapse is presentation-only and Radio exposes explicit voice playb
|
||||
assert.doesNotMatch(enableMethod, /contextRadio/);
|
||||
assert.match(enableMethod, /toggleRadio\(this\._radioEnableBtn\)/);
|
||||
// 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\)/);
|
||||
});
|
||||
|
||||
|
||||
@@ -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.
|
||||
const { PanelPositionControls } =
|
||||
await import('../ui/panelPositionControls.js');
|
||||
const { presentPanelLocally } = await import('../testSupport/panelState.mjs');
|
||||
const html = await readFile(
|
||||
path.join(root, 'src/ui/views/panels/CctvPanel.vue'),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(
|
||||
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',
|
||||
);
|
||||
|
||||
@@ -110,7 +111,7 @@ test('the documented outcomes hold: default, stored open, stored shut, and a sha
|
||||
};
|
||||
try {
|
||||
const controls = new PanelPositionControls({
|
||||
syncPanelCollapseButton: () => {},
|
||||
presentPanel: presentPanelLocally,
|
||||
layoutRightPanels: () => {},
|
||||
syncCctvPanelViewport: () => {},
|
||||
showToast: () => {},
|
||||
|
||||
@@ -234,8 +234,7 @@ export class StyleManager extends ShellFacade {
|
||||
_setCockpitDisclosure: (...args) => this._setCockpitDisclosure(...args),
|
||||
_cockpitDisclosureOpen: (kind) => this._cockpitDisclosureOpen(kind),
|
||||
_setMapStack: (...args) => this._setMapStack(...args),
|
||||
_syncPanelCollapseButton: (...args) =>
|
||||
this._syncPanelCollapseButton(...args),
|
||||
_presentPanel: (...args) => this._presentPanel(...args),
|
||||
_syncShareState: (...args) => this._syncShareState(...args),
|
||||
setPanelCollapsed: (...args) => this.setPanelCollapsed(...args),
|
||||
},
|
||||
|
||||
@@ -17,7 +17,8 @@ import { settleSideAccordion } from './panelAccordion.js';
|
||||
* @param {Window} options.windowRef Viewport and style reader.
|
||||
* @param {{visible: boolean, variant: string}} options.hud Current HUD presentation.
|
||||
* @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.getComputedStyle] Optional DOM style reader override.
|
||||
* @param {Map<string, number>} options.collapsedHeights Caller-owned measurement cache.
|
||||
@@ -45,8 +46,7 @@ export function layoutLeftPanelRail({
|
||||
for (const panel of panels.filter((item) =>
|
||||
item.classList.contains('layout-auto-collapsed'),
|
||||
)) {
|
||||
panel.classList.remove('collapsed', 'layout-auto-collapsed');
|
||||
onCollapse(panel);
|
||||
onCollapse(panel, { collapsed: false, auto: null });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,8 +262,7 @@ export function layoutLeftPanelRail({
|
||||
if (autoCollapseIndices.length) {
|
||||
for (const index of autoCollapseIndices) {
|
||||
const panel = expandedPanels[index];
|
||||
panel.classList.add('collapsed', 'layout-auto-collapsed');
|
||||
onCollapse(panel);
|
||||
onCollapse(panel, { collapsed: true, auto: 'layout' });
|
||||
}
|
||||
onRetry();
|
||||
return;
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
* @param {boolean} options.enabled Whether the one-panel rule applies.
|
||||
* @param {string} [options.preferredPanelId] Most recently opened panel.
|
||||
* @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({
|
||||
panels,
|
||||
@@ -21,8 +22,7 @@ export function settleSideAccordion({
|
||||
if (!enabled) {
|
||||
for (const panel of panels) {
|
||||
if (!panel.classList.contains('accordion-collapsed')) continue;
|
||||
panel.classList.remove('collapsed', 'accordion-collapsed');
|
||||
onCollapse(panel);
|
||||
onCollapse(panel, { collapsed: false, auto: null });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -35,7 +35,6 @@ export function settleSideAccordion({
|
||||
expanded[0];
|
||||
for (const panel of expanded) {
|
||||
if (panel === owner) continue;
|
||||
panel.classList.add('collapsed', 'accordion-collapsed');
|
||||
onCollapse(panel);
|
||||
onCollapse(panel, { collapsed: true, auto: 'accordion' });
|
||||
}
|
||||
}
|
||||
+32
-23
@@ -1,4 +1,5 @@
|
||||
import { UiLifetime } from './uiLifetime.js';
|
||||
import { readPanelState } from './rail/panelState.js';
|
||||
import { usePanelChromeStore } from './stores/panelChrome.js';
|
||||
import { applicationPinia } from './vueRuntime.js';
|
||||
import { displayPanelScroller } from './displayPanelScroll.js';
|
||||
@@ -81,7 +82,7 @@ export class PanelChrome {
|
||||
this._cockpitPanelRestore = null;
|
||||
this._cockpitContextCollapsedForDataPanel = false;
|
||||
this._panelPosition = new PanelPositionControls({
|
||||
syncPanelCollapseButton: (panel) => this._syncPanelCollapseButton(panel),
|
||||
presentPanel: (panel, patch) => this._presentPanel(panel, patch),
|
||||
showToast: (message) => this._showToast(message),
|
||||
});
|
||||
this._panelLayout = new PanelLayoutController({
|
||||
@@ -90,7 +91,7 @@ export class PanelChrome {
|
||||
variant: this.hud.getVariant(),
|
||||
}),
|
||||
scheduleCockpitLayout: () => this.cockpitView?.scheduleContextLayout(),
|
||||
syncPanelCollapseButton: (panel) => this._syncPanelCollapseButton(panel),
|
||||
presentPanel: (panel, patch) => this._presentPanel(panel, patch),
|
||||
readDisplayScrollTop: () =>
|
||||
this._displayPortalScrollRestoreOwner === 'standard'
|
||||
? this._standardDisplayScrollTop
|
||||
@@ -221,18 +222,18 @@ export class PanelChrome {
|
||||
typeof pin === 'boolean'
|
||||
? pin
|
||||
: !panelEl.classList.contains('dock-pinned');
|
||||
panelEl.classList.toggle('dock-pinned', shouldPin);
|
||||
this._chromeStore.publish(this._chromeToken, panelId, {
|
||||
collapsed: panelEl.classList.contains('collapsed'),
|
||||
pinned: shouldPin,
|
||||
});
|
||||
document
|
||||
.querySelectorAll('#command-dock .dock-pinned-top')
|
||||
.forEach((pinnedPanel) => {
|
||||
pinnedPanel.classList.remove('dock-pinned-top');
|
||||
// The tray pinned last draws on top; unpinning hands the top to the
|
||||
// other tray if it is pinned.
|
||||
this._presentPanel(panelEl, { pinned: shouldPin, pinnedTop: shouldPin });
|
||||
for (const otherId of PINNABLE_PANEL_IDS) {
|
||||
if (otherId === panelId) continue;
|
||||
const other = document.getElementById(otherId);
|
||||
if (!other) continue;
|
||||
this._presentPanel(other, {
|
||||
pinnedTop: !shouldPin && other.classList.contains('dock-pinned'),
|
||||
});
|
||||
}
|
||||
if (shouldPin) {
|
||||
panelEl.classList.add('dock-pinned-top');
|
||||
this.setPanelCollapsed(panelId, false, {
|
||||
explicit: !restore,
|
||||
restore,
|
||||
@@ -240,10 +241,6 @@ export class PanelChrome {
|
||||
syncShare: false,
|
||||
});
|
||||
} else {
|
||||
const remainingPinnedPanel = document.querySelector(
|
||||
'#command-dock .dock-pinned',
|
||||
);
|
||||
remainingPinnedPanel?.classList.add('dock-pinned-top');
|
||||
if (!restore && !panelEl.matches(':hover')) {
|
||||
this.setPanelCollapsed(panelId, true, {
|
||||
explicit: true,
|
||||
@@ -323,11 +320,23 @@ export class PanelChrome {
|
||||
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;
|
||||
const shown =
|
||||
this._chromeStore.panels[panelEl.id] ?? readPanelState(panelEl);
|
||||
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 &&
|
||||
!wasAutoCollapsed
|
||||
) {
|
||||
this._syncPanelCollapseButton(panelEl);
|
||||
if (priorLeftOwner !== this._panelLayout._leftStackPreferredPanelId) {
|
||||
this._scheduleLeftPanelLayout({ reconsiderAutoCollapse: true });
|
||||
}
|
||||
@@ -487,7 +495,6 @@ export class PanelChrome {
|
||||
}
|
||||
return;
|
||||
}
|
||||
panelEl.classList.remove('layout-auto-collapsed', 'accordion-collapsed');
|
||||
if (
|
||||
!nextCollapsed &&
|
||||
this.cockpitView?.active &&
|
||||
@@ -518,7 +525,7 @@ export class PanelChrome {
|
||||
});
|
||||
}
|
||||
}
|
||||
panelEl.classList.toggle('collapsed', nextCollapsed);
|
||||
this._presentPanel(panelEl, { collapsed: nextCollapsed, auto: null });
|
||||
if (
|
||||
nextCollapsed &&
|
||||
this.cockpitView?.active &&
|
||||
@@ -528,7 +535,6 @@ export class PanelChrome {
|
||||
this._cockpitContextCollapsedForDataPanel = false;
|
||||
this.cockpitView.setContextCollapsed(false);
|
||||
}
|
||||
this._syncPanelCollapseButton(panelEl);
|
||||
if (persist !== false)
|
||||
this._savePanelCollapsedState(panelId, nextCollapsed);
|
||||
if (panelId === 'pp-toggles') {
|
||||
@@ -586,6 +592,9 @@ export class PanelChrome {
|
||||
}
|
||||
destroy() {
|
||||
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._lifetime.destroy();
|
||||
this._chromeStore.detach(this._chromeToken);
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
import '../testSupport/registerDom.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createApp, h, nextTick } from 'vue';
|
||||
import { createApp, h, nextTick, withDirectives } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { settleSideAccordion } from './panelAccordion.js';
|
||||
import { PanelChrome } from './panelChrome.js';
|
||||
import { followPanelState, vPanelState } from './rail/panelState.js';
|
||||
import { bindPanelDisclosure } from './panelDisclosure.js';
|
||||
import PanelCollapseButton from './rail/PanelCollapseButton.vue';
|
||||
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'], {
|
||||
collapsed: true,
|
||||
pinned: false,
|
||||
pinnedTop: false,
|
||||
auto: null,
|
||||
});
|
||||
store.publish(token, 'data-panel', { collapsed: true });
|
||||
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>`;
|
||||
const store = usePanelChromeStore(createPinia());
|
||||
const chrome = panelChrome(store);
|
||||
for (const id of ['data-panel', 'cctv-panel', 'control-panel'])
|
||||
followPanelState(store, id, document.getElementById(id));
|
||||
const calls = [];
|
||||
const setPanelCollapsed = chrome.setPanelCollapsed.bind(chrome);
|
||||
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'], {
|
||||
collapsed: false,
|
||||
pinned: true,
|
||||
pinnedTop: true,
|
||||
auto: null,
|
||||
});
|
||||
assert.equal(
|
||||
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;
|
||||
store.act('toggle', 'data-panel');
|
||||
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(
|
||||
document.getElementById('cctv-panel').classList.contains('collapsed'),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
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>';
|
||||
const second = panelChrome(store);
|
||||
assert.equal(store.panels['location-bar'], undefined);
|
||||
second._syncPanelCollapseButton(document.getElementById('location-bar'));
|
||||
second._presentPanel('location-bar');
|
||||
assert.deepEqual(store.panels['location-bar'], {
|
||||
collapsed: true,
|
||||
pinned: false,
|
||||
pinnedTop: false,
|
||||
auto: null,
|
||||
});
|
||||
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({
|
||||
readHud,
|
||||
scheduleCockpitLayout,
|
||||
syncPanelCollapseButton,
|
||||
presentPanel,
|
||||
readDisplayScrollTop,
|
||||
}) {
|
||||
this.readHud = readHud;
|
||||
this.scheduleCockpitLayout = scheduleCockpitLayout;
|
||||
this._syncPanelCollapseButton = syncPanelCollapseButton;
|
||||
this._presentPanel = presentPanel;
|
||||
this.readDisplayScrollTop = readDisplayScrollTop;
|
||||
this.destroyed = false;
|
||||
this._adaptivePanelSettleTimer = null;
|
||||
@@ -198,7 +198,6 @@ export class PanelLayoutController {
|
||||
]) {
|
||||
if (!panel) continue;
|
||||
stack.insertBefore(panel, globalContextPanel);
|
||||
this._syncPanelCollapseButton(panel);
|
||||
}
|
||||
if (this._sliderPanel) {
|
||||
this._sliderPanel.style.removeProperty('top');
|
||||
@@ -286,8 +285,7 @@ export class PanelLayoutController {
|
||||
for (const panel of this._rightPanelStack.querySelectorAll(
|
||||
'.layout-auto-collapsed',
|
||||
)) {
|
||||
panel.classList.remove('collapsed', 'layout-auto-collapsed');
|
||||
this._syncPanelCollapseButton(panel);
|
||||
this._presentPanel(panel, { collapsed: false, auto: null });
|
||||
}
|
||||
}
|
||||
this._syncRightPanelAdaptiveLayout();
|
||||
@@ -303,7 +301,7 @@ export class PanelLayoutController {
|
||||
windowRef: window,
|
||||
hud: this.readHud(),
|
||||
preferredPanelId: this._rightStackPreferredPanelId,
|
||||
onCollapse: (panel) => this._syncPanelCollapseButton(panel),
|
||||
onCollapse: (panel, patch) => this._presentPanel(panel, patch),
|
||||
onRetry: () => this._scheduleRightPanelLayout(),
|
||||
leftStack: this._leftPanelStack,
|
||||
displayPanel: this._ppToggles,
|
||||
@@ -422,8 +420,7 @@ export class PanelLayoutController {
|
||||
for (const panel of this._leftPanelStack.querySelectorAll(
|
||||
'.layout-auto-collapsed',
|
||||
)) {
|
||||
panel.classList.remove('collapsed', 'layout-auto-collapsed');
|
||||
this._syncPanelCollapseButton(panel);
|
||||
this._presentPanel(panel, { collapsed: false, auto: null });
|
||||
}
|
||||
}
|
||||
this._syncLeftPanelAdaptiveLayout();
|
||||
@@ -438,7 +435,7 @@ export class PanelLayoutController {
|
||||
windowRef: window,
|
||||
hud: this.readHud(),
|
||||
preferredPanelId: this._leftStackPreferredPanelId,
|
||||
onCollapse: (panel) => this._syncPanelCollapseButton(panel),
|
||||
onCollapse: (panel, patch) => this._presentPanel(panel, patch),
|
||||
onRetry: () => this._scheduleLeftPanelLayout(),
|
||||
collapsedHeights: this._leftStackCollapsedHeights,
|
||||
onAligned: () => this._scheduleRightPanelLayout(),
|
||||
|
||||
@@ -14,8 +14,8 @@ const RETIRED_PANEL_KEY_PREFIXES = [
|
||||
`godsEyeView.${PANEL_POSITION_STORAGE_VERSION}.panelFloatHintShown`,
|
||||
];
|
||||
export class PanelPositionControls {
|
||||
constructor({ syncPanelCollapseButton, showToast }) {
|
||||
this._syncPanelCollapseButton = syncPanelCollapseButton;
|
||||
constructor({ presentPanel, showToast }) {
|
||||
this._presentPanel = presentPanel;
|
||||
this._showToast = showToast;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export class PanelPositionControls {
|
||||
// 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.
|
||||
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
|
||||
// choice the user (or a share link) already made.
|
||||
if (panelEl.dataset)
|
||||
@@ -84,7 +84,6 @@ export class PanelPositionControls {
|
||||
: stored === null
|
||||
? 'default'
|
||||
: 'stored';
|
||||
this._syncPanelCollapseButton(panelEl);
|
||||
}
|
||||
|
||||
_savePanelCollapsedState(panelId, collapsed) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
layoutRightPanelRail,
|
||||
measurePanelNaturalHeight,
|
||||
} from './panelRails.js';
|
||||
import { applyPanelState, readPanelState } from './rail/panelState.js';
|
||||
|
||||
test('Display uses an inner scroll body only in Cyber', () => {
|
||||
const body = { scrollTop: 95 };
|
||||
@@ -169,7 +170,11 @@ function fixture(
|
||||
matchMedia: () => ({ matches: mobile }),
|
||||
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: () => {
|
||||
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 {{visible: boolean, variant: string}} options.hud Current HUD presentation.
|
||||
* @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.getComputedStyle] Optional DOM style reader override.
|
||||
* @param {HTMLElement} options.leftStack Rail supplying the shared top baseline.
|
||||
@@ -83,8 +84,7 @@ export function layoutRightPanelRail({
|
||||
for (const panel of panels.filter((item) =>
|
||||
item.classList.contains('layout-auto-collapsed'),
|
||||
)) {
|
||||
panel.classList.remove('collapsed', 'layout-auto-collapsed');
|
||||
onCollapse(panel);
|
||||
onCollapse(panel, { collapsed: false, auto: null });
|
||||
}
|
||||
}
|
||||
// One panel is open per desktop side; this marker is distinct from
|
||||
@@ -275,8 +275,7 @@ export function layoutRightPanelRail({
|
||||
if (!isCollapseRetry && autoCollapseIndices.length) {
|
||||
for (const index of autoCollapseIndices) {
|
||||
const panel = expandedPanels[index];
|
||||
panel.classList.add('collapsed', 'layout-auto-collapsed');
|
||||
onCollapse(panel);
|
||||
onCollapse(panel, { collapsed: true, auto: 'layout' });
|
||||
}
|
||||
pendingCollapseRetries.add(stack);
|
||||
onRetry();
|
||||
|
||||
@@ -1024,13 +1024,14 @@ export class ShellFacade {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates collapse button glyphs based on panel state. Right-rail panels
|
||||
* use directional arrows; left-stack panels use +/- symbols.
|
||||
* @param {HTMLElement} panelEl - The panel DOM element.
|
||||
* Presents a panel's state, merged over what it shows now; its root and
|
||||
* buttons draw it from the panel chrome store.
|
||||
* @param {HTMLElement|string} panelOrId - The panel or its id.
|
||||
* @param {object} [patch] - The state that changes.
|
||||
* @returns {void}
|
||||
*/
|
||||
_syncPanelCollapseButton(panelEl) {
|
||||
return this._panelChrome._syncPanelCollapseButton(...arguments);
|
||||
_presentPanel(panelOrId, patch) {
|
||||
return this._panelChrome._presentPanel(...arguments);
|
||||
}
|
||||
|
||||
_buildSharePanelState() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
||||
import { UiLifetime } from './uiLifetime.js';
|
||||
import { createPinia } from 'pinia';
|
||||
import { PanelChrome } from './panelChrome.js';
|
||||
import { followPanelState } from './rail/panelState.js';
|
||||
import { usePanelChromeStore } from './stores/panelChrome.js';
|
||||
import { PanelLayoutController } from './panelLayoutController.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({
|
||||
readHud: () => ({}),
|
||||
scheduleCockpitLayout() {},
|
||||
syncPanelCollapseButton() {},
|
||||
presentPanel() {},
|
||||
readDisplayScrollTop: () => 0,
|
||||
});
|
||||
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 observed = new Set();
|
||||
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');
|
||||
stack.children = [weather, f.nodes.get('global-context-panel')];
|
||||
const synced = [];
|
||||
const store = usePanelChromeStore(createPinia());
|
||||
const chrome = { _chromeStore: store, _chromeToken: store.connect({}) };
|
||||
const presented = [];
|
||||
const owner = new PanelLayoutController({
|
||||
readHud: () => ({}),
|
||||
scheduleCockpitLayout() {},
|
||||
readDisplayScrollTop: () => 0,
|
||||
syncPanelCollapseButton(panel) {
|
||||
synced.push(panel.id);
|
||||
PanelChrome.prototype._syncPanelCollapseButton.call(chrome, panel);
|
||||
presentPanel(panel, patch) {
|
||||
presented.push(panel.id);
|
||||
PanelChrome.prototype._presentPanel.call(chrome, panel, patch);
|
||||
},
|
||||
});
|
||||
owner._initRightPanelAdaptiveLayout();
|
||||
@@ -315,12 +316,15 @@ test('weather rail orders between CCTV and Context, observes and syncs chrome',
|
||||
stack.children.map((panel) => panel.id),
|
||||
['pp-toggles', 'cctv-panel', 'weather-panel', 'global-context-panel'],
|
||||
);
|
||||
assert.ok(synced.includes('weather-panel'));
|
||||
assert.ok(observed.has(weather));
|
||||
assert.equal(store.panels['weather-panel'].collapsed, true);
|
||||
weather.classList.remove('collapsed');
|
||||
PanelChrome.prototype._syncPanelCollapseButton.call(chrome, weather);
|
||||
// Moving a panel into the rail leaves its state as it was.
|
||||
assert.deepEqual(presented, []);
|
||||
followPanelState(store, 'weather-panel', weather);
|
||||
PanelChrome.prototype._presentPanel.call(chrome, weather, {
|
||||
collapsed: false,
|
||||
});
|
||||
assert.equal(store.panels['weather-panel'].collapsed, false);
|
||||
assert.equal(weather.classList.contains('collapsed'), false);
|
||||
owner.destroy();
|
||||
assert.equal(observed.size, 0);
|
||||
assert.equal(f.frames.size, 0);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { PanelChrome } from './panelChrome.js';
|
||||
import { presentPanelLocally } from '../testSupport/panelState.mjs';
|
||||
|
||||
function fixture(theme = 'cyber') {
|
||||
const nodes = new Map();
|
||||
@@ -35,7 +36,7 @@ function fixture(theme = 'cyber') {
|
||||
_rightPanelStack: rail,
|
||||
_panelLayout: {},
|
||||
_lifetime: { frame() {} },
|
||||
_syncPanelCollapseButton() {}, _scheduleRightPanelLayout() {},
|
||||
_presentPanel: presentPanelLocally, _scheduleRightPanelLayout() {},
|
||||
_scheduleLeftPanelLayout() {}, _layoutRightPanels() {}, _syncCctvPanelViewport() {},
|
||||
_savePanelCollapsedState: (...args) => saved.push(args),
|
||||
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
|
||||
* it. PanelChrome owns the panels: it publishes here whenever a panel opens,
|
||||
* closes or is pinned, and connects the buttons' clicks; the views only render
|
||||
* and forward clicks.
|
||||
* @typedef {object} PanelState
|
||||
* @property {boolean} collapsed Whether the panel is closed, by the person or
|
||||
* by its layout.
|
||||
* @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', () => {
|
||||
/**
|
||||
* `{[panelId]: {collapsed, pinned}}`, frozen; a panel is absent until its
|
||||
* owner first presents it, and its buttons keep their markup until then.
|
||||
* `{[panelId]: PanelState}`, frozen; a panel is absent until its owner
|
||||
* first presents it, and it keeps its markup's classes until then.
|
||||
*/
|
||||
const panels = shallowRef(Object.freeze({}));
|
||||
/** @type {{handlers: PanelChromeHandlers}|null} */
|
||||
@@ -44,17 +64,13 @@ export const usePanelChromeStore = defineStore('gev-panel-chrome', () => {
|
||||
* unchanged state is not republished.
|
||||
* @param {object} token
|
||||
* @param {string} panelId
|
||||
* @param {{collapsed?: boolean, pinned?: boolean}} patch
|
||||
* @param {Partial<PanelState>} patch
|
||||
*/
|
||||
function publish(token, panelId, patch) {
|
||||
if (!owns(token) || !panelId) return;
|
||||
const prior = panels.value[panelId];
|
||||
const next = { collapsed: false, pinned: false, ...prior, ...patch };
|
||||
if (
|
||||
prior &&
|
||||
prior.collapsed === next.collapsed &&
|
||||
prior.pinned === next.pinned
|
||||
)
|
||||
const next = { ...BLANK, ...prior, ...patch };
|
||||
if (prior && Object.keys(BLANK).every((key) => prior[key] === next[key]))
|
||||
return;
|
||||
panels.value = Object.freeze({
|
||||
...panels.value,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { msg, t } from '../../i18n/index.js';
|
||||
import { vPanelState } from '../rail/panelState.js';
|
||||
import UiButton from '../kit/UiButton.vue';
|
||||
import { vClassToggle } from '../rail/classToggle.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>
|
||||
<template v-for="slot in dockOrder" :key="slot">
|
||||
<!-- 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-inner">
|
||||
<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" />
|
||||
|
||||
<!-- 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="location-inner">
|
||||
<div class="location-toolbar">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { vPanelState } from '../rail/panelState.js';
|
||||
import PanelCollapseButton from '../rail/PanelCollapseButton.vue';
|
||||
import { vClassToggle } from '../rail/classToggle.js';
|
||||
import { useCockpitStore } from '../stores/cockpit.js';
|
||||
@@ -28,7 +29,7 @@ const sceneRuntime = computed(() => scenePanel.value?.runtime ?? '');
|
||||
<template>
|
||||
<!-- 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 }">
|
||||
<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="weather-panel-inner" data-rail-scroller>
|
||||
<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>
|
||||
</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="recent-imagery-panel-inner" data-rail-scroller>
|
||||
<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>
|
||||
</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="global-context-panel-inner ui-scroll" data-rail-scroller>
|
||||
<div class="panel-header">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { vPanelState } from '../rail/panelState.js';
|
||||
import PanelCollapseButton from '../rail/PanelCollapseButton.vue';
|
||||
import { useSceneSharingStore } from '../stores/sceneSharing.js';
|
||||
import { useSceneStore } from '../stores/scenes.js';
|
||||
@@ -37,7 +38,7 @@ async function importChosen(event) {
|
||||
<template>
|
||||
<div id="left-panel-stack" class="ui-scroll">
|
||||
<!-- 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="data-panel-inner">
|
||||
<div class="panel-header">
|
||||
@@ -56,7 +57,7 @@ async function importChosen(event) {
|
||||
<StreetLevelPanel />
|
||||
|
||||
<!-- 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="scene-panel-inner ui-scroll">
|
||||
<div class="panel-header">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
import { vPanelState } from '../../rail/panelState.js';
|
||||
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
||||
import CctvCalValue from '../../cctv/CctvCalValue.vue';
|
||||
import CctvPicker from '../../cctv/CctvPicker.vue';
|
||||
@@ -27,7 +28,7 @@ const frameData = computed(() =>
|
||||
</script>
|
||||
|
||||
<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="cctv-panel-inner ui-scroll" data-rail-scroller>
|
||||
<div class="panel-header">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
import { vPanelState } from '../../rail/panelState.js';
|
||||
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
||||
import { vRangeValue } from '../../rail/rangeValue.js';
|
||||
import { useDisplayStore } from '../../stores/display.js';
|
||||
@@ -43,7 +44,7 @@ const geoListed = computed(() => geoRows.value.length > 0);
|
||||
|
||||
<template>
|
||||
<!-- 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">
|
||||
<span data-i18n class="pp-header-label">DISPLAY</span>
|
||||
<span class="panel-divider" aria-hidden="true"></span>
|
||||
@@ -123,7 +124,7 @@ const geoListed = computed(() => geoRows.value.length > 0);
|
||||
</div>
|
||||
</div>
|
||||
<!-- 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">
|
||||
<span class="param-panel-title" data-i18n>PARAMETERS</span>
|
||||
<span class="param-panel-divider"></span>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
import { vPanelState } from '../../rail/panelState.js';
|
||||
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
||||
import { vClassToggle } from '../../rail/classToggle.js';
|
||||
import { vFieldValue } from '../../rail/fieldValue.js';
|
||||
@@ -29,7 +30,7 @@ const radioWords = {
|
||||
</script>
|
||||
|
||||
<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="panel-header">
|
||||
<span class="panel-title" data-i18n>RADIO</span>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, shallowRef, watch } from 'vue';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
import { vPanelState } from '../../rail/panelState.js';
|
||||
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
||||
import RailChips from '../../rail/RailChips.vue';
|
||||
import { useStreetLevelStore } from '../../stores/streetLevel.js';
|
||||
@@ -101,7 +102,7 @@ const vThumb = { mounted: placeThumb, updated: placeThumb };
|
||||
</script>
|
||||
|
||||
<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="street-level-panel-inner ui-scroll" data-rail-scroller>
|
||||
<div class="panel-header">
|
||||
|
||||
@@ -445,8 +445,7 @@ export class VisualSettings {
|
||||
return;
|
||||
}
|
||||
if (!this._cockpitDisclosureOpen?.('display')) return;
|
||||
this._sliderPanel.classList.remove('collapsed');
|
||||
this._syncPanelCollapseButton(this._sliderPanel);
|
||||
this._presentPanel(this._sliderPanel, { collapsed: false });
|
||||
this._lifetime.frame(() =>
|
||||
this._lifetime.frame(() => {
|
||||
this._sliderPanel?.scrollIntoView?.({ block: 'nearest' });
|
||||
@@ -1434,8 +1433,7 @@ export class VisualSettings {
|
||||
_revealStyleParameters() {
|
||||
if (!this._sliderPanel?.classList.contains('active')) return;
|
||||
if (this._cockpitDisplayPortalActive) return;
|
||||
this._sliderPanel.classList.remove('collapsed');
|
||||
this._syncPanelCollapseButton(this._sliderPanel);
|
||||
this._presentPanel(this._sliderPanel, { collapsed: false });
|
||||
this.setPanelCollapsed('pp-toggles', false, { explicit: true });
|
||||
this._lifetime.frame(() =>
|
||||
this._lifetime.frame(() => {
|
||||
|
||||
+18
-7
@@ -817,7 +817,17 @@ export function createGevActionRunner({
|
||||
...(!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 };
|
||||
}
|
||||
|
||||
@@ -3143,13 +3153,14 @@ function normalizeAircraftClassFilter(value) {
|
||||
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) {
|
||||
if (styleManager && typeof styleManager.setPanelCollapsed === 'function') {
|
||||
styleManager.setPanelCollapsed(panelId, !open, { explicit: true });
|
||||
} else {
|
||||
const panel = document.getElementById(panelId);
|
||||
if (panel) panel.classList.toggle('collapsed', !open);
|
||||
}
|
||||
if (typeof styleManager?.setPanelCollapsed !== 'function') return false;
|
||||
styleManager.setPanelCollapsed(panelId, !open, { explicit: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
function normalizeContextMode(value) {
|
||||
|
||||
Reference in new issue
Block a user