mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Fix the format gate and the Cyber Context styling, and pin the redesign
CI failed at format:check: the format scope still listed the deleted panelChrome.radioOwnership test, and panelRails.test.mjs needed Prettier after its glow list shrank. The new Context title colour outranked Cyber's red panel titles, and the collapsed Context launcher came out brighter than its siblings; the rule now applies only to the expanded panel outside Cyber. Cyber's layout-focus standby card had relied on the old dashed border and centring, so it drew a 3px frame with its text off the radar art; it now names its own 1px frame, padding and centring. Cyber also drops the shared segmented track, which drew a third frame around its already framed mode buttons. Tests now pin the Context header to its title and collapse control, the segmented control, 10px type, the missing halo and the scoped title colour, plus the Cyber standby and track overrides. Each fails on the previous CSS or markup. qa-radio enables Radio and starts the first Play from the visible controls, expanding Context and Radio the way a person must, then puts the panels back. The Escape test and its mutation describe what they still protect. CURRENT-STATE no longer describes the removed Radio icon or compact player.
This commit is contained in:
1 parent
66e19f0c50
commit
97b7307b14
11 files changed
+115
-41
No files matched your search
+6
-18
@@ -246,8 +246,8 @@ red hover border in both map and cockpit, so clipping does not hide the highligh
|
||||
Cyber's map-side right rail opens one panel at a time while keeping collapsed
|
||||
launchers available. Display, CCTV and Context scroll their contents inside fixed
|
||||
headers and decorative frames. The narrow-screen rail remains scrollable to reach
|
||||
each panel. Radio retains the shared nested Context player and compact
|
||||
header disclosure, without relocating playback controls on theme changes.
|
||||
each panel. Radio retains the shared nested Context player without relocating
|
||||
playback controls on theme changes.
|
||||
|
||||
Voice help/error popups and Location/Visual Presets pins extend beyond their
|
||||
Cyber frames without being clipped: the shapes decorate non-interactive
|
||||
@@ -2986,15 +2986,10 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with
|
||||
> stacking. Narrow mobile Cockpit
|
||||
> viewports suppress the legacy layer stack, peripheral HUD, and secondary
|
||||
> Context/briefing panes so the flight instruments and primary controls remain
|
||||
> unobstructed. Outside Cockpit, Radio starts collapsed while off. When Context
|
||||
> is collapsed, its Radio header icon opens compact controls whose explicit
|
||||
> Enable/Disable action owns power; stable close and full-panel buttons own
|
||||
> compact dismissal and one-way detailed expansion independently. When Context
|
||||
> is already expanded, that same icon skips the floating compact card, expands
|
||||
> the embedded Radio section, scrolls it into view, and moves keyboard focus to
|
||||
> its disclosure control. The detailed panel's normal accordion control owns
|
||||
> its collapse. Compact/detailed state and playback continuity remain shared.
|
||||
> Radio volume in the full, compact, and Cockpit surfaces, plus Space Mission
|
||||
> unobstructed. Outside Cockpit, Radio starts collapsed while off and lives
|
||||
> only in its section inside Context; the detailed panel's normal accordion
|
||||
> control owns its disclosure and its Enable/Disable action owns power.
|
||||
> Radio volume in the full and Cockpit surfaces, plus Space Mission
|
||||
> replay speed, use Display / Sharpen's muted 3px rail, circular cyan thumb,
|
||||
> glow, and mono value treatment. Their larger transparent hit areas, keyboard
|
||||
> focus indication, ranges, disabled states, and mission speed scale remain
|
||||
@@ -4770,13 +4765,6 @@ synchronous playback-control observers run, so reentrant voice cleanup cannot
|
||||
overwrite a released stream with a stale paused state. Layer
|
||||
destruction likewise retains an enabled manager entry when module disable or
|
||||
destroy fails semantically, preventing an active orphan and allowing retry.
|
||||
When Context is collapsed, explicitly activating its Radio header icon reveals
|
||||
the compact transport; hover and focus alone do not open it. When Context is
|
||||
expanded, the icon instead expands and scrolls to the embedded Radio section.
|
||||
Its accessible label, controlled region, and expanded state keep describing the
|
||||
current route throughout enabling, enabled, disabling, and uncertain lifecycle
|
||||
renders. Both routes preserve Radio power, playback, station, filter, and volume
|
||||
state.
|
||||
|
||||
Bundled datasets live in `src/data/local_data/` with per-folder provenance READMEs; they are lazily loaded via `src/data/localGeojsonCore.js` (Vite `?url` assets) when toggled on.
|
||||
|
||||
|
||||
@@ -703,7 +703,6 @@
|
||||
"src/ui/shareRestoration.js",
|
||||
"src/ui/visualSettings.js",
|
||||
"src/ui/panelChrome.js",
|
||||
"src/ui/panelChrome.radioOwnership.test.mjs",
|
||||
"src/testSupport/readShellSource.mjs",
|
||||
"src/ui/stateOwners.test.mjs",
|
||||
"src/layers/flights/records.js",
|
||||
|
||||
@@ -177,7 +177,7 @@ const MUTATIONS = [
|
||||
{
|
||||
// Anchored past the call so it lands on the Cockpit utility disclosure's
|
||||
// Escape claim and nowhere else.
|
||||
defect: 'a Cockpit utility disclosure returns to stopPropagation, so one ESC does two things',
|
||||
defect: 'a Cockpit utility disclosure returns to stopPropagation, so later document listeners also act on its ESC',
|
||||
file: 'radio',
|
||||
from: " event.stopImmediatePropagation();\n const kind = displayOpen ? 'display' : 'radio';",
|
||||
to: " event.stopPropagation();\n const kind = displayOpen ? 'display' : 'radio';",
|
||||
|
||||
+36
-5
@@ -83,6 +83,32 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Native pick buffers and clustered overlay entries settle on rendered frames,
|
||||
// not on wall-clock sleeps, particularly with software rendering.
|
||||
// Radio lives only inside Context; a user reaches its controls by expanding
|
||||
// Context and then Radio. Returns the prior collapse state so a step can put
|
||||
// the panels back for the scenarios that follow.
|
||||
async function revealRadioControls(page, targetId) {
|
||||
const prior = await page.evaluate(async (id) => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
const collapsed = (panelId) => document.getElementById(panelId).classList.contains('collapsed');
|
||||
const state = { context: collapsed('global-context-panel'), radio: collapsed('radio-panel') };
|
||||
manager.setPanelCollapsed('global-context-panel', false, { explicit: true });
|
||||
manager.setPanelCollapsed('radio-panel', false, { explicit: true });
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
document.getElementById(id).scrollIntoView({ block: 'nearest' });
|
||||
return state;
|
||||
}, targetId);
|
||||
await page.waitForSelector(`#${targetId}`, { visible: true });
|
||||
return prior;
|
||||
}
|
||||
|
||||
async function restoreRadioPanels(page, prior) {
|
||||
await page.evaluate(({ context, radio }) => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
manager.setPanelCollapsed('radio-panel', radio);
|
||||
manager.setPanelCollapsed('global-context-panel', context);
|
||||
}, prior);
|
||||
}
|
||||
|
||||
async function settleRadioFrames(page) {
|
||||
await page.evaluate(() => new Promise((resolve, reject) => {
|
||||
const scene = window.__godsEyeView.viewer.scene;
|
||||
@@ -285,13 +311,16 @@ async function main() {
|
||||
window.__godsEyeView.styleManager.setPanelCollapsed('global-context-panel', true);
|
||||
});
|
||||
|
||||
const enablePanels = await revealRadioControls(page, 'radio-enable-btn');
|
||||
const enableStartedAt = Date.now();
|
||||
await page.$eval('#radio-enable-btn', (button) => button.click());
|
||||
await page.click('#radio-enable-btn');
|
||||
await page.waitForFunction(() => {
|
||||
const state = window.__godsEyeView.dataManager.layers.get('radio').module.getUIState();
|
||||
return state.enabled && !state.loading && state.stationCount === 750;
|
||||
});
|
||||
const enableElapsedMs = Date.now() - enableStartedAt;
|
||||
// Later scenarios start from the collapsed rail this step began with.
|
||||
await restoreRadioPanels(page, enablePanels);
|
||||
const initial = await page.evaluate(() => {
|
||||
const module = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const state = module.getUIState();
|
||||
@@ -320,7 +349,7 @@ async function main() {
|
||||
check('750-station catalog renders within the browser budget', initial.state.stationCount === 750 && enableElapsedMs < 5000, `${enableElapsedMs}ms`);
|
||||
check('enable does not autoplay', initial.plays === 0 && initial.state.audioState === 'stopped');
|
||||
check(
|
||||
'detailed Radio power control reflects the layer manager while the panel is collapsed',
|
||||
'Radio power control reflects the layer manager after the panels collapse again',
|
||||
initial.enablePressed === 'true',
|
||||
);
|
||||
check('dynamic canonical and genre categories reach the UI', initial.filterOptions >= 10, `${initial.filterOptions} options`);
|
||||
@@ -1537,7 +1566,7 @@ async function main() {
|
||||
JSON.stringify(directoryRefreshContinuity),
|
||||
);
|
||||
await page.select('#radio-filter', 'news');
|
||||
check('collapsed Radio power enables Radio without forcing Context open', !initial.panelExpanded && !initial.contextExpanded && initial.nestedInContext);
|
||||
check('enabled Radio stays nested in Context and leaves both panels collapsed', !initial.panelExpanded && !initial.contextExpanded && initial.nestedInContext);
|
||||
check('full Play is available before a station is selected', !initial.state.selected && initial.fullPlayEnabled && initial.fullPlayLabel === 'Play nearest radio station');
|
||||
|
||||
const multiExpandedPanelLayout = await page.evaluate(async () => {
|
||||
@@ -2326,13 +2355,15 @@ async function main() {
|
||||
window.__qaFirstPlayFlyToCalls = [];
|
||||
window.__qaFirstPlayOriginalFlyTo = camera.flyTo;
|
||||
camera.flyTo = (options) => window.__qaFirstPlayFlyToCalls.push(options);
|
||||
document.getElementById('radio-play-btn').focus();
|
||||
});
|
||||
await page.$eval('#radio-play-btn', (button) => button.click());
|
||||
const firstPlayPanels = await revealRadioControls(page, 'radio-play-btn');
|
||||
await page.focus('#radio-play-btn');
|
||||
await page.click('#radio-play-btn');
|
||||
await page.waitForFunction(() => {
|
||||
const state = window.__godsEyeView.dataManager.layers.get('radio').module.getUIState();
|
||||
return state.audioState === 'playing' || state.audioState === 'error';
|
||||
});
|
||||
await restoreRadioPanels(page, firstPlayPanels);
|
||||
const postMicroDragPlayback = await page.evaluate(async () => {
|
||||
const mod = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
let state = mod.getUIState();
|
||||
|
||||
@@ -439,7 +439,7 @@ test('Cockpit owns a focused shared Display portal and compact Radio controls',
|
||||
assert.ok(cockpitRadio, 'Cockpit compact Radio controls are missing');
|
||||
assert.doesNotMatch(
|
||||
cockpitRadio[0],
|
||||
/context-radio-details-btn|radio-filter|radio-tuner/,
|
||||
/radio-filter|radio-tuner/,
|
||||
);
|
||||
assert.match(
|
||||
css,
|
||||
|
||||
@@ -146,3 +146,36 @@ test('Context async action buttons remain focused while busy', () => {
|
||||
assert.match(control, /button\.setAttribute\('aria-busy', String\(busy\)\)/);
|
||||
assert.doesNotMatch(control, /button\.disabled\s*=/);
|
||||
});
|
||||
|
||||
test('Context header holds only its title and collapse control', () => {
|
||||
const panel = html.match(/<div id="global-context-panel"[\s\S]*?<div class="cyber-panel-body"/);
|
||||
assert.ok(panel, 'Global Context panel is missing');
|
||||
const header = panel[0].match(/<div class="panel-header">([\s\S]*?)<\/div>\s*<div class="cyber-panel-body"/);
|
||||
assert.ok(header, 'Global Context header is missing');
|
||||
const tags = [...header[1].matchAll(/<(\w+)\s+class="([^"]+)"/g)].map(([, tag, cls]) => `${tag}.${cls}`);
|
||||
assert.deepEqual(tags, ['span.panel-title', 'span.panel-divider', 'button.panel-collapse-btn']);
|
||||
});
|
||||
|
||||
test('Context reads as one segmented control with legible type and no halo', () => {
|
||||
const rules = [...css.matchAll(/([^{}]+)\{([^{}]*)\}/g)];
|
||||
const clean = (selector) => selector.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\s+/g, ' ').trim();
|
||||
const rule = (selector) => rules.find(([, s]) => clean(s) === selector)?.[2] || '';
|
||||
const px = (body, prop) => Number(body.match(new RegExp(`${prop}:[^;]*?(\\d+(?:\\.\\d+)?)px`))?.[1] || 0);
|
||||
|
||||
assert.match(rule('#global-context-panel .panel-glow'), /display:\s*none/);
|
||||
const modes = rule('.global-context-modes');
|
||||
assert.match(modes, /border:\s*1px solid/);
|
||||
assert.match(modes, /padding:\s*2px/);
|
||||
assert.ok(px(rule('.context-mode-button'), 'font') >= 10, 'mode labels must stay at least 10px');
|
||||
assert.doesNotMatch(rule('.context-mode-button.active'), /box-shadow/);
|
||||
const standby = rule('.context-mode-standby');
|
||||
assert.ok(px(standby, 'font') >= 10, 'standby guidance must stay at least 10px');
|
||||
assert.doesNotMatch(standby, /dashed/);
|
||||
assert.ok(px(rule('.global-context-actions .panel-layer-toggle'), 'font-size') >= 10);
|
||||
// Default theme only, expanded only: Cyber keeps its red titles and the
|
||||
// collapsed launcher matches its sibling launchers.
|
||||
assert.match(
|
||||
rule(":root:not([data-ui-theme='cyber']) #global-context-panel:not(.collapsed) .panel-title"),
|
||||
/color:\s*var\(--text-secondary\)/,
|
||||
);
|
||||
});
|
||||
@@ -374,16 +374,18 @@ test('an overlay with NO class to watch still disarms the launcher', () => {
|
||||
assert.match(covered, /\} catch \{\s*\n\s*return false;\s*\n\s*\}/);
|
||||
});
|
||||
|
||||
test('one ESC does one thing — a Cockpit utility disclosure stops the launcher outright', () => {
|
||||
test('one ESC does one thing — a Cockpit utility disclosure stops later document listeners', () => {
|
||||
const ui = fs.readFileSync(
|
||||
new URL('./ui/radioBindings.js', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
// stopPropagation() does NOT stop later listeners on the SAME document, so the
|
||||
// disclosure's earlier capture handler closed the disclosure and the launcher
|
||||
// dismissed itself off the same key. The earlier listener is the only one that
|
||||
// can stop the later one — and only the immediate form does it.
|
||||
// stopPropagation() does NOT stop later listeners on the SAME document. The
|
||||
// Context Radio disclosure that once raced the launcher this way is gone, and
|
||||
// Cockpit's utility disclosures never share the screen with the launcher, but
|
||||
// they own the same contract: their capture handler is the only one that can
|
||||
// keep a later document listener (Cockpit exit, the launcher) off the key, and
|
||||
// only the immediate form does it.
|
||||
const utilityEsc = ui.slice(ui.indexOf('if (!displayOpen && !radioOpen) return;'));
|
||||
const claim = utilityEsc.slice(0, utilityEsc.indexOf('setCockpitDisclosure(kind, false'));
|
||||
assert.match(claim, /event\.preventDefault\(\);/);
|
||||
@@ -391,7 +393,7 @@ test('one ESC does one thing — a Cockpit utility disclosure stops the launcher
|
||||
assert.doesNotMatch(
|
||||
claim,
|
||||
/event\.stopPropagation\(\);/,
|
||||
'the plain form leaves the launcher listening and is the defect itself',
|
||||
'the plain form leaves later document listeners acting on the same key',
|
||||
);
|
||||
|
||||
// Belt on the launcher side: a key another surface already marked is not ours,
|
||||
|
||||
@@ -762,6 +762,16 @@ test('the Display selector, voice schema and final stylesheet expose Cyber', ()
|
||||
assert.match(cyberStyles, /\.panel-collapsible\.collapsed::after/);
|
||||
assert.match(cyberStyles, /\.panel-collapsible:not\(\.collapsed\)::after/);
|
||||
assert.match(cyberStyles, /\.context-mode-standby:not\(\[hidden\]\)/);
|
||||
// The base standby is borderless plain text, so the focus card must name its
|
||||
// own frame width and centring rather than inherit them.
|
||||
assert.match(
|
||||
cyberStyles,
|
||||
/\.context-mode-standby:not\(\[hidden\]\) \{[^}]*place-content: center;[^}]*border: 1px solid[^}]*text-align: center;/,
|
||||
);
|
||||
assert.match(
|
||||
cyberStyles,
|
||||
/:root\[data-ui-theme='cyber'\] \.global-context-modes \{[^}]*border: 0;/,
|
||||
);
|
||||
assert.match(cyberStyles, /mask-image: linear-gradient\(/);
|
||||
assert.match(cyberStyles, /flex: 1 1 auto;/);
|
||||
assert.match(cyberStyles, /#left-panel-stack:has/);
|
||||
|
||||
@@ -654,11 +654,7 @@ function flatRules(block) {
|
||||
|
||||
test('narrow-screen rails pin every hosted panel glow inside its panel box', () => {
|
||||
const css = readStylesheet(new URL('../../style.css', import.meta.url));
|
||||
for (const panel of [
|
||||
'data-panel',
|
||||
'scene-panel',
|
||||
'cctv-panel',
|
||||
]) {
|
||||
for (const panel of ['data-panel', 'scene-panel', 'cctv-panel']) {
|
||||
assert.match(
|
||||
css,
|
||||
new RegExp(`#${panel} \\.panel-glow \\{[^}]*\\binset: -\\d+px;`),
|
||||
|
||||
+14
-2
@@ -1447,6 +1447,15 @@
|
||||
text-shadow: 0 0 6px var(--cyber-red);
|
||||
}
|
||||
|
||||
/* Cyber frames each mode button itself; the shared segmented track would
|
||||
draw a third, blue frame around them. */
|
||||
:root[data-ui-theme='cyber'] .global-context-modes {
|
||||
gap: 7px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber'] .dock-tray-toggle,
|
||||
:root[data-ui-theme='cyber'] .context-mode-button {
|
||||
background-color: rgba(38, 52, 58, 0.95);
|
||||
@@ -1798,8 +1807,11 @@
|
||||
isolation: isolate;
|
||||
min-height: 132px;
|
||||
flex: 1 1 auto;
|
||||
border-style: solid;
|
||||
border-color: rgba(206, 222, 226, 0.14);
|
||||
place-content: center;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(206, 222, 226, 0.14);
|
||||
border-radius: 7px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(
|
||||
|
||||
@@ -989,8 +989,11 @@
|
||||
#global-context-panel .panel-glow {
|
||||
display: none;
|
||||
}
|
||||
/* Its section titles (Context, Radio) stay legible over bright imagery. */
|
||||
#global-context-panel .panel-title {
|
||||
/* Expanded, its section titles (Context, Radio) stay legible over bright
|
||||
imagery. Cyber keeps its own red titles; collapsed launchers match siblings. */
|
||||
:root:not([data-ui-theme='cyber'])
|
||||
#global-context-panel:not(.collapsed)
|
||||
.panel-title {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.global-context-panel-inner {
|
||||
|
||||
Reference in new issue
Block a user