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:
Said Turksever committed 2026-10-29 12:55:47 +00:00
1 parent 66e19f0c50
commit 97b7307b14
11 files changed
+115 -41

No files matched your search

+6 -18
View File
@@ -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.
-1
View File
@@ -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",
+1 -1
View File
@@ -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
View File
@@ -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();
+1 -1
View File
@@ -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,
+33
View File
@@ -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\)/,
);
});
+8 -6
View File
@@ -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,
+10
View File
@@ -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/);
+1 -5
View File
@@ -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
View File
@@ -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(
+5 -2
View File
@@ -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 {