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,140 @@
|
||||
import { after, before, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { installVueDom } from '../testSupport/vueDom.mjs';
|
||||
|
||||
let PanelNavigation;
|
||||
let createUiRuntime;
|
||||
let nextTick;
|
||||
let uninstall;
|
||||
before(async () => {
|
||||
uninstall = installVueDom();
|
||||
({ nextTick } = await import('vue'));
|
||||
({ PanelNavigation } = await import('./panelNavigation.js'));
|
||||
({ createUiRuntime } = await import('./vueRuntime.js'));
|
||||
});
|
||||
after(() => uninstall());
|
||||
|
||||
function panel(id, { collapsed = true, hidden = false, title = '' } = {}) {
|
||||
const element = document.createElement('div');
|
||||
element.id = id;
|
||||
element.dataset.panelId = id;
|
||||
element.className = collapsed
|
||||
? 'panel-collapsible collapsed'
|
||||
: 'panel-collapsible';
|
||||
// Data Layers shows only while `active` (the `f` shortcut toggles it).
|
||||
if (id === 'data-panel') element.classList.add('active');
|
||||
element.hidden = hidden;
|
||||
if (title) element.innerHTML = `<span class="panel-title">${title}</span>`;
|
||||
return element;
|
||||
}
|
||||
|
||||
function setup() {
|
||||
document.body.innerHTML = '';
|
||||
const left = document.createElement('div');
|
||||
left.id = 'left-panel-stack';
|
||||
left.append(panel('data-panel', { collapsed: false }), panel('scene-panel'));
|
||||
const right = document.createElement('div');
|
||||
right.id = 'right-context-rail';
|
||||
right.append(
|
||||
panel('pp-toggles'),
|
||||
panel('weather-panel', { hidden: true }),
|
||||
panel('global-context-panel'),
|
||||
panel('host-panel', { title: 'HOST' }),
|
||||
);
|
||||
document.body.append(left, right);
|
||||
const calls = [];
|
||||
const ui = createUiRuntime();
|
||||
const navigation = new PanelNavigation({
|
||||
ui,
|
||||
stacks: { left, right },
|
||||
setPanelCollapsed: (...args) => calls.push(args),
|
||||
});
|
||||
navigation.init();
|
||||
return { left, right, calls, ui, navigation };
|
||||
}
|
||||
|
||||
const railItems = (stack) =>
|
||||
[...stack.querySelectorAll('.gev-panel-rail [data-rail-item]')].map(
|
||||
(button) => [button.dataset.railItem, button.getAttribute('aria-pressed')],
|
||||
);
|
||||
|
||||
test('each stack gets a rail of its offered panels, marking the open one', async () => {
|
||||
const { left, right, ui, navigation } = setup();
|
||||
await nextTick();
|
||||
assert.equal(
|
||||
left.firstElementChild.className,
|
||||
'gev-panel-rail gev-panel-rail--left',
|
||||
);
|
||||
assert.deepEqual(railItems(left), [
|
||||
['data-panel', 'true'],
|
||||
['scene-panel', 'false'],
|
||||
]);
|
||||
// Hidden panels are not offered; a host's panel gets a generic item.
|
||||
assert.deepEqual(railItems(right), [
|
||||
['pp-toggles', 'false'],
|
||||
['global-context-panel', 'false'],
|
||||
['host-panel', 'false'],
|
||||
]);
|
||||
assert.equal(
|
||||
right
|
||||
.querySelector('[data-rail-item="host-panel"]')
|
||||
.getAttribute('aria-label'),
|
||||
'HOST',
|
||||
);
|
||||
assert.equal(
|
||||
left.querySelector('nav').getAttribute('aria-label'),
|
||||
'Left panels',
|
||||
);
|
||||
navigation.destroy();
|
||||
ui.destroy();
|
||||
});
|
||||
|
||||
test('selecting an item toggles its panel explicitly', async () => {
|
||||
const { left, calls, ui, navigation } = setup();
|
||||
await nextTick();
|
||||
left.querySelector('[data-rail-item="scene-panel"]').click();
|
||||
left.querySelector('[data-rail-item="data-panel"]').click();
|
||||
assert.deepEqual(calls, [
|
||||
['scene-panel', false, { explicit: true }],
|
||||
['data-panel', true, { explicit: true }],
|
||||
]);
|
||||
navigation.destroy();
|
||||
ui.destroy();
|
||||
});
|
||||
|
||||
test('the rails follow disclosure and availability changes', async () => {
|
||||
const { left, right, ui, navigation } = setup();
|
||||
document.getElementById('data-panel').classList.add('collapsed');
|
||||
document.getElementById('scene-panel').classList.remove('collapsed');
|
||||
document.getElementById('weather-panel').hidden = false;
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await nextTick();
|
||||
assert.deepEqual(railItems(left), [
|
||||
['data-panel', 'false'],
|
||||
['scene-panel', 'true'],
|
||||
]);
|
||||
assert.deepEqual(railItems(right), [
|
||||
['pp-toggles', 'false'],
|
||||
['weather-panel', 'false'],
|
||||
['global-context-panel', 'false'],
|
||||
['host-panel', 'false'],
|
||||
]);
|
||||
// `f` hides Data Layers outright, so the rail stops offering it.
|
||||
document.getElementById('data-panel').classList.remove('active');
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await nextTick();
|
||||
assert.deepEqual(railItems(left), [['scene-panel', 'true']]);
|
||||
navigation.destroy();
|
||||
ui.destroy();
|
||||
});
|
||||
|
||||
test('destroy removes the rails and stops following the stacks', async () => {
|
||||
const { left, right, calls, ui, navigation } = setup();
|
||||
navigation.destroy();
|
||||
navigation.destroy();
|
||||
assert.equal(left.querySelector('.gev-panel-rail'), null);
|
||||
assert.equal(right.querySelector('.gev-panel-rail'), null);
|
||||
navigation.select('data-panel');
|
||||
assert.deepEqual(calls, []);
|
||||
ui.destroy();
|
||||
});
|
||||
Reference in new issue
Block a user