mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
feat(ui): add a desktop panel rail to each sidebar
Each side stack hosts a Vue rail listing its panels; selecting one opens it through PanelChrome. The shell owns one Vue runtime and Pinia instance, and a panel store mirrors each side's panels from the DOM. The rail hides with its stack, in Cockpit, clean view and recording, and on phones. Panels without a catalog entry get a generic item, and Data Layers leaves the rail while the f shortcut hides it.
This commit is contained in:
1 parent
eed7657516
commit
3d5d3ef014
29 files changed
+627
-32
No files matched your search
@@ -0,0 +1,123 @@
|
||||
import PanelRailView from './PanelRailView.vue';
|
||||
import { PANEL_RAIL_HOST_CLASS, PANEL_RAIL_ITEMS } from './panelRailCatalog.js';
|
||||
import { usePanelStore } from './stores/panels.js';
|
||||
|
||||
/**
|
||||
* Own the desktop panel rails: publish each side's panels to the panel store
|
||||
* and open or close a panel when its rail item is selected. Disclosure itself
|
||||
* stays with PanelChrome, which enforces one open panel per side.
|
||||
*/
|
||||
export class PanelNavigation {
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {{pinia: object, mount: Function}} options.ui Shell Vue runtime.
|
||||
* @param {{left: HTMLElement|null, right: HTMLElement|null}} options.stacks Side panel stacks.
|
||||
* @param {Function} options.setPanelCollapsed PanelChrome's disclosure operation.
|
||||
*/
|
||||
constructor({ ui, stacks, setPanelCollapsed }) {
|
||||
this._ui = ui;
|
||||
this._stacks = stacks;
|
||||
this._setPanelCollapsed = setPanelCollapsed;
|
||||
this._store = usePanelStore(ui.pinia);
|
||||
this._unmounts = [];
|
||||
this._hosts = [];
|
||||
this._observer = null;
|
||||
this._destroyed = false;
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this._destroyed) return;
|
||||
for (const side of ['left', 'right']) {
|
||||
const stack = this._stacks[side];
|
||||
if (!stack) continue;
|
||||
// Inside the stack, the rail shows and hides with its panels in every
|
||||
// presentation mode (Cockpit, clean view, recording, phone sheets).
|
||||
const host = stack.ownerDocument.createElement('div');
|
||||
host.className = `${PANEL_RAIL_HOST_CLASS} ${PANEL_RAIL_HOST_CLASS}--${side}`;
|
||||
stack.prepend(host);
|
||||
this._hosts.push(host);
|
||||
this._unmounts.push(
|
||||
this._ui.mount(PanelRailView, host, {
|
||||
side,
|
||||
onSelect: (panelId) => this.select(panelId),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
// Only the panels themselves matter: their order, disclosure and
|
||||
// availability, not changes inside them or inside the rails.
|
||||
this._observer = new MutationObserver((records) => {
|
||||
if (records.some((record) => this._isPanelRecord(record))) this.sync();
|
||||
});
|
||||
for (const stack of Object.values(this._stacks)) {
|
||||
if (!stack) continue;
|
||||
this._observer.observe(stack, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'hidden'],
|
||||
});
|
||||
}
|
||||
}
|
||||
this.sync();
|
||||
}
|
||||
|
||||
/** Publish both sides' panels in display order. */
|
||||
sync() {
|
||||
if (this._destroyed) return;
|
||||
for (const side of ['left', 'right']) {
|
||||
const stack = this._stacks[side];
|
||||
const panels = stack
|
||||
? [...stack.children]
|
||||
.filter((panel) => panel.matches('[data-panel-id]'))
|
||||
.map((panel) => {
|
||||
const shownBy = PANEL_RAIL_ITEMS[panel.id]?.shownBy;
|
||||
return {
|
||||
id: panel.id,
|
||||
title:
|
||||
panel
|
||||
.querySelector('.panel-title, .pp-header-label')
|
||||
?.textContent.trim() || panel.id,
|
||||
open: !panel.classList.contains('collapsed'),
|
||||
available:
|
||||
!panel.hidden &&
|
||||
(!shownBy || panel.classList.contains(shownBy)),
|
||||
};
|
||||
})
|
||||
: [];
|
||||
this._store.setSide(side, panels);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle a panel from its rail item.
|
||||
* @param {string} panelId Panel element id.
|
||||
*/
|
||||
select(panelId) {
|
||||
if (this._destroyed) return;
|
||||
this._setPanelCollapsed(panelId, this._store.isOpen(panelId), {
|
||||
explicit: true,
|
||||
});
|
||||
}
|
||||
|
||||
_isPanelRecord(record) {
|
||||
const stacks = Object.values(this._stacks);
|
||||
return record.type === 'childList'
|
||||
? stacks.includes(record.target) &&
|
||||
[...record.addedNodes, ...record.removedNodes].some(
|
||||
(node) => !this._hosts.includes(node),
|
||||
)
|
||||
: stacks.includes(record.target.parentElement);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._destroyed) return;
|
||||
this._destroyed = true;
|
||||
this._observer?.disconnect();
|
||||
this._observer = null;
|
||||
for (const unmount of this._unmounts) unmount();
|
||||
this._unmounts = [];
|
||||
for (const host of this._hosts) host.remove();
|
||||
this._hosts = [];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user