Drop the Radio icon from Context and make the panel easier to read

The Context header carried a Radio icon that opened a second, compact
Radio player on top of the panel, duplicating the Radio section that
already lives inside Context. Remove the icon, the compact player and
everything that kept the two in step. Radio stays in Context as its own
section with one Enable control; Cockpit's compact Radio is unchanged.

Context itself was hard to read: 7-8 px labels at low contrast, a dashed
placeholder box, and a glow halo. The two modes are now one segmented
control, labels and guidance are 10 px at higher contrast, the standby
hint is plain text, section titles use --text-secondary, and the panel
draws no glow.

Unused translations are pruned, tests and QA harnesses that pinned the
compact player are updated or retired, and CURRENT-STATE and the
changelog describe the new header.
This commit is contained in:
Said Turksever committed 2026-10-29 11:28:56 +00:00
1 parent aaa3ff46cf
commit 66e19f0c50
32 files changed
+127 -1326

No files matched your search

-19
View File
@@ -114,19 +114,14 @@ export class StyleManager extends ShellFacade {
Object.assign(this, readShellElements());
this._panelChrome = new PanelChrome({
elements: {
_contextRadioDetailsBtn: this._contextRadioDetailsBtn,
_contextRadioDock: this._contextRadioDock,
_leftPanelStack: this._leftPanelStack,
_locationSearch: this._locationSearch,
_ppToggles: this._ppToggles,
_rightPanelStack: this._rightPanelStack,
},
operations: {
_setRadioDisclosure: (...args) => this._setRadioDisclosure(...args),
_syncCctvPanelViewport: (...args) =>
this._syncCctvPanelViewport(...args),
_syncContextRadioLauncherState: (...args) =>
this._syncContextRadioLauncherState(...args),
_showToast: (...args) => this._showToast(...args),
},
readHud: () => this.hud,
@@ -414,8 +409,6 @@ export class StyleManager extends ShellFacade {
this.getAircraftTrackingTarget(...args),
_scheduleRightPanelLayout: (...args) =>
this._scheduleRightPanelLayout(...args),
_syncContextRadioLauncherState: (...args) =>
this._syncContextRadioLauncherState(...args),
},
readDataManager: () => this._dataManager,
readContext: () => this.getContextModeState(),
@@ -1052,18 +1045,6 @@ export class StyleManager extends ShellFacade {
_cockpitRadioVolume: this._cockpitRadioVolume,
_cockpitRadioVolumeValue: this._cockpitRadioVolumeValue,
_cockpitUtilityControls: this._cockpitUtilityControls,
_contextRadioDetailsBtn: this._contextRadioDetailsBtn,
_contextRadioDock: this._contextRadioDock,
_contextRadioMini: this._contextRadioMini,
_contextRadioMiniCloseBtn: this._contextRadioMiniCloseBtn,
_contextRadioMiniEnableBtn: this._contextRadioMiniEnableBtn,
_contextRadioMiniNextBtn: this._contextRadioMiniNextBtn,
_contextRadioMiniPlayBtn: this._contextRadioMiniPlayBtn,
_contextRadioMiniPrevBtn: this._contextRadioMiniPrevBtn,
_contextRadioMiniStation: this._contextRadioMiniStation,
_contextRadioMiniVolume: this._contextRadioMiniVolume,
_contextRadioMiniVolumeValue: this._contextRadioMiniVolumeValue,
_contextRadioToggleBtn: this._contextRadioToggleBtn,
_radioEnableBtn: this._radioEnableBtn,
_radioFilter: this._radioFilter,
_radioLayerState: this._radioLayerState,
-6
View File
@@ -8,12 +8,6 @@ export function onKeyDown(event) {
// focus. Its target handler closes the overlay and restores attribution
// focus; Cockpit must stay active behind it.
if (event.target?.closest?.('.cesium-credit-lightbox')) return;
if (
document
.getElementById('context-radio-dock')
?.classList.contains('disclosure-open')
)
return;
if (
document.querySelector('#cockpit-utility-controls [aria-expanded="true"]')
)
-34
View File
@@ -293,15 +293,6 @@ export class PanelChrome {
}
_syncRailPanelCollapseButton(panelEl) {
// Adaptive rail layout may directly restore an auto-collapsed Context
// without invoking setPanelCollapsed(). Enforce Radio ownership there too.
if (
panelEl?.id === 'global-context-panel' &&
!panelEl.classList.contains('collapsed') &&
this._contextRadioDock?.classList.contains('disclosure-open')
) {
this._setRadioDisclosure?.(false);
}
this._syncPanelCollapseButton(panelEl);
}
@@ -365,15 +356,6 @@ export class PanelChrome {
dockToggle.setAttribute('aria-label', t(titleKey, { name: panelName }));
dockToggle.title = t(titleKey, { name: panelName });
}
if (panelEl.id === 'radio-panel' && this._contextRadioDetailsBtn) {
this._contextRadioDetailsBtn.setAttribute(
'aria-expanded',
String(!collapsed),
);
}
if (panelEl.id === 'radio-panel' || panelEl.id === 'global-context-panel') {
this._syncContextRadioLauncherState();
}
}
_buildSharePanelState() {
@@ -517,15 +499,6 @@ export class PanelChrome {
syncShare,
});
}
// A repeated Context-open request must clear a stale compact Radio mini
// even when no collapsed class changes.
if (
!nextCollapsed &&
panelId === 'global-context-panel' &&
this._contextRadioDock?.classList.contains('disclosure-open')
) {
this._setRadioDisclosure?.(false);
}
if (
panelEl.classList.contains('collapsed') === nextCollapsed &&
!wasAutoCollapsed
@@ -554,13 +527,6 @@ export class PanelChrome {
this.cockpitView.setContextCollapsed(true);
}
}
if (
!nextCollapsed &&
panelId === 'global-context-panel' &&
this._contextRadioDock?.classList.contains('disclosure-open')
) {
this._setRadioDisclosure?.(false);
}
if (
!nextCollapsed &&
panelId === 'radio-panel' &&
-200
View File
@@ -1,200 +0,0 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { PanelChrome } from './panelChrome.js';
import { PanelLayoutController } from './panelLayoutController.js';
function classList(initial = []) {
const values = new Set(initial);
return {
contains: (value) => values.has(value),
add: (...items) => items.forEach((item) => values.add(item)),
remove: (...items) => items.forEach((item) => values.delete(item)),
toggle(value, force) {
if (force === true) values.add(value);
else if (force === false) values.delete(value);
else if (values.has(value)) values.delete(value);
else values.add(value);
return values.has(value);
},
};
}
test('opening an already-open Context still closes a stale compact Radio disclosure', (t) => {
const priorDocument = globalThis.document;
const contextPanel = {
id: 'global-context-panel',
classList: classList(),
};
globalThis.document = {
documentElement: { dataset: { uiTheme: 'tactical' } },
getElementById(id) {
return id === 'global-context-panel' ? contextPanel : null;
},
};
t.after(() => {
globalThis.document = priorDocument;
});
const radioDock = { classList: classList(['disclosure-open']) };
const calls = [];
const owner = {
_contextRadioDock: radioDock,
_setRadioDisclosure(open) {
calls.push(open);
radioDock.classList.toggle('disclosure-open', open);
},
_leftPanelStack: null,
_rightPanelStack: null,
_panelLayout: {
_leftStackPreferredPanelId: null,
_rightStackPreferredPanelId: null,
},
_syncPanelCollapseButton() {},
_scheduleLeftPanelLayout() {},
_scheduleRightPanelLayout() {},
shareLinkManager: null,
cockpitView: null,
};
PanelChrome.prototype.setPanelCollapsed.call(
owner,
'global-context-panel',
false,
);
assert.deepEqual(calls, [false]);
assert.equal(radioDock.classList.contains('disclosure-open'), false);
});
test('adaptive rail restore hides compact Radio before revealing Context', (t) => {
const priorRaf = globalThis.requestAnimationFrame;
let nextFrame;
globalThis.requestAnimationFrame = (callback) => {
nextFrame = callback;
return 1;
};
t.after(() => {
globalThis.requestAnimationFrame = priorRaf;
});
// Valid starting state: the tactical rail automatically collapsed Context,
// and the user opened the compact Radio controls from its header.
const contextPanel = {
id: 'global-context-panel',
classList: classList(['collapsed', 'layout-auto-collapsed']),
};
const radioDock = { classList: classList(['disclosure-open']) };
const radioMini = { hidden: false };
const calls = [];
const chrome = {
_contextRadioDock: radioDock,
_setRadioDisclosure(open) {
calls.push(`disclosure:${open}`);
radioDock.classList.toggle('disclosure-open', open);
radioMini.hidden = !open;
},
_syncPanelCollapseButton(panel) {
calls.push(`button:${panel.id}`);
},
};
const layout = {
destroyed: false,
_rightStackLayoutFrame: null,
_rightStackReconsiderAutoCollapse: false,
_rightPanelStack: {
querySelectorAll: (selector) =>
selector === '.layout-auto-collapsed' ? [contextPanel] : [],
},
_syncPanelCollapseButton(panel) {
PanelChrome.prototype._syncRailPanelCollapseButton.call(chrome, panel);
},
_syncRightPanelAdaptiveLayout() {
calls.push('layout');
},
};
PanelLayoutController.prototype._scheduleRightPanelLayout.call(layout, {
reconsiderAutoCollapse: true,
});
assert.equal(contextPanel.classList.contains('collapsed'), true);
assert.equal(radioMini.hidden, false);
nextFrame();
assert.equal(contextPanel.classList.contains('collapsed'), false);
assert.equal(contextPanel.classList.contains('layout-auto-collapsed'), false);
assert.equal(radioDock.classList.contains('disclosure-open'), false);
assert.equal(radioMini.hidden, true);
assert.deepEqual(calls, [
'disclosure:false',
'button:global-context-panel',
'layout',
]);
});
test('real PanelChrome constructor wires rail restore to compact Radio owner', (t) => {
const priorDocument = globalThis.document;
const priorRaf = globalThis.requestAnimationFrame;
let nextFrame;
const contextPanel = {
id: 'global-context-panel',
classList: classList(['collapsed', 'layout-auto-collapsed']),
};
const rightRail = {
querySelectorAll(selector) {
return selector === '.layout-auto-collapsed' ? [contextPanel] : [];
},
};
globalThis.document = {
getElementById(id) {
return id === 'right-context-rail' ? rightRail : null;
},
};
globalThis.requestAnimationFrame = (callback) => {
nextFrame = callback;
return 1;
};
t.after(() => {
globalThis.document = priorDocument;
globalThis.requestAnimationFrame = priorRaf;
});
const dock = { classList: classList(['disclosure-open']) };
const calls = [];
const chrome = new PanelChrome({
elements: { _contextRadioDock: dock },
operations: {
_setRadioDisclosure(open) {
calls.push(`disclosure:${open}`);
dock.classList.toggle('disclosure-open', open);
},
_syncPanelCollapseButton(panel) {
calls.push(`button:${panel.id}`);
},
},
readHud: () => ({ visible: true, getVariant: () => 'tactical' }),
readCockpit: () => null,
readShareLinks: () => null,
readInitialShare: () => null,
readScrollRestoreOwner: () => null,
readDisplayScrollTop: () => 0,
});
chrome._panelLayout._syncRightPanelAdaptiveLayout = () => {
calls.push('layout');
};
chrome._panelLayout._scheduleRightPanelLayout({
reconsiderAutoCollapse: true,
});
assert.equal(typeof nextFrame, 'function');
assert.equal(dock.classList.contains('disclosure-open'), true);
nextFrame();
assert.equal(contextPanel.classList.contains('collapsed'), false);
assert.equal(dock.classList.contains('disclosure-open'), false);
assert.deepEqual(calls, [
'disclosure:false',
'button:global-context-panel',
'layout',
]);
});
-1
View File
@@ -658,7 +658,6 @@ test('narrow-screen rails pin every hosted panel glow inside its panel box', ()
'data-panel',
'scene-panel',
'cctv-panel',
'global-context-panel',
]) {
assert.match(
css,
-90
View File
@@ -10,16 +10,6 @@ import {
export function bindRadioControls() {
if (!this._radioPanel) return;
const tunerListenerOptions = {};
const setRadioDisclosure = (expanded, { returnFocus = false } = {}) => {
if (this.destroyed) return;
const open = Boolean(expanded);
this._contextRadioDock?.classList.toggle('disclosure-open', open);
if (this._contextRadioMini) this._contextRadioMini.hidden = !open;
this._syncContextRadioLauncherState();
if (!open && returnFocus)
this._contextRadioToggleBtn?.focus({ preventScroll: true });
};
this._setRadioDisclosure = setRadioDisclosure;
const setCockpitDisclosure = (
kind,
expanded,
@@ -399,60 +389,16 @@ export function bindRadioControls() {
'click',
() => void toggleRadio(this._radioEnableBtn),
);
this.listen(
this._contextRadioMiniEnableBtn,
'click',
() => void toggleRadio(this._contextRadioMiniEnableBtn),
);
this.listen(
this._cockpitRadioEnableBtn,
'click',
() => void toggleRadio(this._cockpitRadioEnableBtn),
);
this.listen(this._contextRadioToggleBtn, 'click', () => {
const contextPanel = document.getElementById('global-context-panel');
if (contextPanel && !contextPanel.classList.contains('collapsed')) {
setRadioDisclosure(false);
this.actions.setPanelCollapsed('radio-panel', false, { explicit: true });
void this._revealRadioPanelInsideContext({
focusTarget: this._radioPanel?.querySelector(
'[data-collapse-target="radio-panel"]',
),
});
return;
}
setRadioDisclosure(
!this._contextRadioDock?.classList.contains('disclosure-open'),
);
});
this.listen(this._contextRadioMiniCloseBtn, 'click', () => {
setRadioDisclosure(false, { returnFocus: true });
});
this.listen(this._contextRadioDetailsBtn, 'click', () => {
if (!this.actions.isCockpitActive())
this.actions.setPanelCollapsed('global-context-panel', false, {
explicit: true,
});
this.actions.setPanelCollapsed('radio-panel', false, { explicit: true });
setRadioDisclosure(false);
this._radioEnableBtn?.focus({ preventScroll: true });
});
this.listen(this._cockpitRadioToggleBtn, 'click', () => {
const open =
this._cockpitRadioToggleBtn.getAttribute('aria-expanded') === 'true';
setCockpitDisclosure('radio', !open);
});
this.listen(
document,
'pointerdown',
(event) => {
if (!this._contextRadioDock?.classList.contains('disclosure-open'))
return;
if (event.target?.closest?.('#context-radio-dock')) return;
setRadioDisclosure(false);
},
tunerListenerOptions,
);
this.listen(
document,
'pointerdown',
@@ -470,29 +416,6 @@ export function bindRadioControls() {
},
tunerListenerOptions,
);
this.listen(
document,
'keydown',
(event) => {
if (
event.key !== 'Escape' ||
!this._contextRadioDock?.classList.contains('disclosure-open')
)
return;
event.preventDefault();
// Immediate: a plain stopPropagation() still lets every LATER listener on
// this same document run, so closing the disclosure ALSO dismissed the
// first-run launcher — one key, two actions. Matches the cockpit
// disclosure handler directly below.
event.stopImmediatePropagation();
const escapedFromDisclosure =
event.target === this._contextRadioToggleBtn ||
this._contextRadioToggleBtn?.contains?.(event.target);
setRadioDisclosure(false, { returnFocus: !escapedFromDisclosure });
if (escapedFromDisclosure) this._contextRadioToggleBtn?.blur?.();
},
{ capture: true },
);
this.listen(
document,
'keydown',
@@ -586,13 +509,6 @@ export function bindRadioControls() {
this._radioVolumeValue.textContent = `${value}%`;
this.actions.setParams({ volume: value / 100 }, { origin: 'user' });
});
this.listen(this._contextRadioMiniPrevBtn, 'click', () => cycleRadio(-1));
this.listen(this._contextRadioMiniNextBtn, 'click', () => cycleRadio(1));
this.listen(
this._contextRadioMiniPlayBtn,
'click',
() => void this.radio.togglePlayback({ origin: 'user' }),
);
// Cockpit owns the Cesium camera even though it intentionally clears
// viewer.trackedEntity. Station changes must never start the map-view
// rotation/fallback flights that would compete with its preUpdate pose.
@@ -607,12 +523,6 @@ export function bindRadioControls() {
'click',
() => void this.radio.togglePlayback({ origin: 'user' }),
);
this.listen(this._contextRadioMiniVolume, 'input', () => {
const value = Number(this._contextRadioMiniVolume.value);
if (this._contextRadioMiniVolumeValue)
this._contextRadioMiniVolumeValue.textContent = `${value}%`;
this.actions.setParams({ volume: value / 100 }, { origin: 'user' });
});
this.listen(this._cockpitRadioVolume, 'input', () => {
const value = Number(this._cockpitRadioVolume.value);
if (this._cockpitRadioVolumeValue)
-102
View File
@@ -1,4 +1,3 @@
import { t, msg } from '../i18n/index.js';
import { bindRadioControls } from './radioBindings.js';
import { renderRadioState } from './radioPresentation.js';
@@ -109,107 +108,6 @@ export class RadioControls {
return true;
}
/**
* Bring the embedded Radio section into the expanded Context scroller.
* This never changes Radio power, playback, selection, or Context mode.
* @param {{focusTarget?: HTMLElement|null}} [options]
* @returns {Promise<boolean>} Whether the internal scroller moved.
*/
async _revealRadioPanelInsideContext({ focusTarget = null } = {}) {
const contextPanel = document.getElementById('global-context-panel');
const scroller =
(document.documentElement?.dataset.uiTheme === 'cyber' &&
contextPanel?.querySelector('.cyber-panel-body')) ||
contextPanel?.querySelector('.global-context-panel-inner');
if (
!contextPanel ||
contextPanel.classList.contains('collapsed') ||
!scroller ||
!this._radioPanel ||
this._radioPanel.classList.contains('collapsed')
)
return false;
await new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve)),
);
if (
this.destroyed ||
contextPanel.classList.contains('collapsed') ||
this._radioPanel.classList.contains('collapsed')
)
return false;
const viewport = scroller.getBoundingClientRect();
const radioRect = this._radioPanel.getBoundingClientRect();
const desired = scroller.scrollTop + radioRect.top - viewport.top - 10;
const next = Math.min(
Math.max(0, scroller.scrollHeight - scroller.clientHeight),
Math.max(0, desired),
);
const moved = Math.abs(next - scroller.scrollTop) >= 1;
if (moved) {
const reducedMotion = window.matchMedia?.(
'(prefers-reduced-motion: reduce)',
)?.matches;
scroller.scrollTo({
top: next,
behavior: reducedMotion ? 'auto' : 'smooth',
});
}
focusTarget?.focus?.({ preventScroll: true });
return moved;
}
/** Keep the launcher truthful for the active theme and Context route. */
_syncContextRadioLauncherState() {
if (this.destroyed || !this._contextRadioToggleBtn) return;
const contextPanel = globalThis.document?.getElementById(
'global-context-panel',
);
const contextExpanded = Boolean(
contextPanel && !contextPanel.classList.contains('collapsed'),
);
if (contextExpanded) {
const radioExpanded = Boolean(
this._radioPanel && !this._radioPanel.classList.contains('collapsed'),
);
this._contextRadioToggleBtn.setAttribute('aria-controls', 'radio-panel');
this._contextRadioToggleBtn.setAttribute(
'aria-expanded',
String(radioExpanded),
);
const label = t(
radioExpanded
? msg('Go to expanded Radio section')
: msg('Expand Radio section in Context'),
);
this._contextRadioToggleBtn.setAttribute('aria-label', label);
this._contextRadioToggleBtn.title = label;
return;
}
// This launcher toggles the compact player even when the detailed panel
// is open elsewhere. Its ARIA state must describe that same disclosure.
const compactOpen = Boolean(
this._contextRadioDock?.classList.contains('disclosure-open'),
);
this._contextRadioToggleBtn.setAttribute(
'aria-controls',
'context-radio-mini',
);
this._contextRadioToggleBtn.setAttribute(
'aria-expanded',
String(compactOpen),
);
const launcherLabel = t(
compactOpen
? msg('Close compact Radio controls')
: msg('Open compact Radio controls'),
);
this._contextRadioToggleBtn.setAttribute('aria-label', launcherLabel);
this._contextRadioToggleBtn.title = launcherLabel;
}
destroy() {
if (this.destroyed) return;
this.destroyed = true;
-1
View File
@@ -102,7 +102,6 @@ test('Radio listeners and subscriptions are revoked once before tuning teardown'
f.controls.destroy();
f.controls.destroy();
const snapshot = structuredClone(f.calls);
f.controls._setRadioDisclosure(true);
f.controls._setCockpitDisclosure('display', true);
f.next.dispatchEvent(new Event('click'));
f.document.dispatchEvent(new Event('gev:radio-selected'));
+1 -7
View File
@@ -3,15 +3,9 @@ export function syncRadioPanelPlacement(doc) {
if (!doc?.getElementById) return;
const panel = doc.getElementById('radio-panel');
const context = doc.getElementById('global-context-panel');
const dock = doc.getElementById('context-radio-dock');
const mini = doc.getElementById('context-radio-mini');
const parent =
context?.querySelector('.cyber-panel-body') ||
context?.querySelector('.global-context-panel-inner');
const header = context?.querySelector('.panel-header');
if (!panel || !parent || !header || !dock || !mini) return;
if (!panel || !parent) return;
if (panel.parentElement !== parent) parent.append(panel);
if (dock.parentElement !== header)
header.insertBefore(dock, header.querySelector('.panel-collapse-btn'));
if (mini.parentElement !== dock) dock.append(mini);
}
+4 -32
View File
@@ -33,31 +33,15 @@ test('all themes keep the same Radio controls nested in Context', () => {
const panel = node(),
context = node(),
rail = node(),
dock = node(),
mini = node();
const inner = node(),
contextHeader = node(),
radioHeader = node();
const contextCollapse = node(),
radioCollapse = node();
inner = node(),
body = node();
context.selectors['.global-context-panel-inner'] = inner;
const body = node();
context.selectors['.cyber-panel-body'] = body;
context.selectors['.panel-header'] = contextHeader;
panel.selectors['.panel-header'] = radioHeader;
contextHeader.selectors['.panel-collapse-btn'] = contextCollapse;
radioHeader.selectors['.panel-collapse-btn'] = radioCollapse;
inner.append(panel);
contextHeader.append(dock);
contextHeader.append(contextCollapse);
radioHeader.append(radioCollapse);
dock.append(mini);
const nodes = {
'radio-panel': panel,
'global-context-panel': context,
'right-context-rail': rail,
'context-radio-dock': dock,
'context-radio-mini': mini,
};
const doc = {
documentElement: { dataset: {} },
@@ -67,31 +51,19 @@ test('all themes keep the same Radio controls nested in Context', () => {
doc.documentElement.dataset.uiTheme = 'cyber';
syncRadioPanelPlacement(doc);
assert.equal(panel.parentElement, body);
assert.deepEqual(contextHeader.children, [dock, contextCollapse]);
assert.equal(mini.parentElement, dock);
const moves = [panel.moves, dock.moves, mini.moves];
const moves = panel.moves;
syncRadioPanelPlacement(doc);
assert.deepEqual(
[panel.moves, dock.moves, mini.moves],
moves,
'no mutation feedback loop',
);
assert.equal(panel.moves, moves, 'no mutation feedback loop');
delete doc.documentElement.dataset.uiTheme;
syncRadioPanelPlacement(doc);
assert.equal(panel.parentElement, body);
assert.deepEqual(contextHeader.children, [dock, contextCollapse]);
assert.equal(mini.parentElement, dock);
}
// Recover a hierarchy left by the previous Cyber implementation without
// recreating the player, changing playback, or mutating collapse state.
rail.append(panel);
radioHeader.insertBefore(dock, radioCollapse);
panel.append(mini);
doc.documentElement.dataset.uiTheme = 'cyber';
syncRadioPanelPlacement(doc);
assert.equal(panel.parentElement, body);
assert.deepEqual(contextHeader.children, [dock, contextCollapse]);
assert.equal(mini.parentElement, dock);
});
test('placement is inert before the complete shell exists', () => {
-53
View File
@@ -26,11 +26,6 @@ export function renderRadioState(state) {
?.classList.toggle('radio-broadcasting', state.audioState === 'playing');
this._radioPanel.classList.toggle('radio-enabled', enabled);
this._radioPanel.classList.toggle('lifecycle-uncertain', uncertain);
this._contextRadioDock?.classList.toggle('active', enabled);
if (this._contextRadioToggleBtn) {
this._contextRadioToggleBtn.classList.toggle('active', enabled);
}
this._syncContextRadioLauncherState();
this._radioLayerState?.classList.toggle('active', enabled);
const lifecycleLabel =
lifecycleState === 'enabling' ? t('ENABLING') : t('DISABLING');
@@ -66,26 +61,6 @@ export function renderRadioState(state) {
this._radioEnableBtn.setAttribute('aria-disabled', String(transitioning));
this._radioEnableBtn.setAttribute('aria-busy', String(transitioning));
}
if (this._contextRadioMiniEnableBtn) {
this._contextRadioMiniEnableBtn.classList.toggle('active', enabled);
this._contextRadioMiniEnableBtn.setAttribute(
'aria-pressed',
String(enabled),
);
this._contextRadioMiniEnableBtn.textContent = transitioning
? lifecycleLabel
: enableLabel;
this._contextRadioMiniEnableBtn.setAttribute('aria-label', enableAria);
this._contextRadioMiniEnableBtn.disabled = false;
this._contextRadioMiniEnableBtn.setAttribute(
'aria-disabled',
String(transitioning),
);
this._contextRadioMiniEnableBtn.setAttribute(
'aria-busy',
String(transitioning),
);
}
if (this._cockpitRadioEnableBtn) {
this._cockpitRadioEnableBtn.classList.toggle('active', enabled);
this._cockpitRadioEnableBtn.setAttribute('aria-pressed', String(enabled));
@@ -202,10 +177,6 @@ export function renderRadioState(state) {
this._radioPrevBtn.disabled = !interactive || !hasStations;
if (this._radioNextBtn)
this._radioNextBtn.disabled = !interactive || !hasStations;
if (this._contextRadioMiniPrevBtn)
this._contextRadioMiniPrevBtn.disabled = !interactive || !hasStations;
if (this._contextRadioMiniNextBtn)
this._contextRadioMiniNextBtn.disabled = !interactive || !hasStations;
if (this._cockpitRadioPrevBtn)
this._cockpitRadioPrevBtn.disabled = !interactive || !hasStations;
if (this._cockpitRadioNextBtn)
@@ -232,13 +203,6 @@ export function renderRadioState(state) {
this._radioPlayBtn.textContent = playbackButtonLabel;
this._radioPlayBtn.setAttribute('aria-label', playAria);
}
if (this._contextRadioMiniPlayBtn) {
this._contextRadioMiniPlayBtn.disabled = !interactive || !hasStations;
this._contextRadioMiniPlayBtn.classList.toggle('active', activePlayback);
this._contextRadioMiniPlayBtn.textContent = activePlayback ? 'Ⅱ' : '▶';
this._contextRadioMiniPlayBtn.setAttribute('aria-label', playAria);
this._contextRadioMiniPlayBtn.title = playbackAction;
}
if (this._cockpitRadioPlayBtn) {
this._cockpitRadioPlayBtn.disabled = !interactive || !hasStations;
this._cockpitRadioPlayBtn.classList.toggle('active', activePlayback);
@@ -255,17 +219,6 @@ export function renderRadioState(state) {
if (this._radioVolumeValue)
this._radioVolumeValue.textContent = `${Math.round(state.volume * 100)}%`;
}
if (
this._contextRadioMiniVolume &&
document.activeElement !== this._contextRadioMiniVolume
) {
this._contextRadioMiniVolume.value = String(Math.round(state.volume * 100));
}
if (this._contextRadioMiniVolume)
this._contextRadioMiniVolume.disabled = !interactive;
if (this._contextRadioMiniVolumeValue) {
this._contextRadioMiniVolumeValue.textContent = `${Math.round(state.volume * 100)}%`;
}
if (
this._cockpitRadioVolume &&
document.activeElement !== this._cockpitRadioVolume
@@ -277,12 +230,6 @@ export function renderRadioState(state) {
if (this._cockpitRadioVolumeValue) {
this._cockpitRadioVolumeValue.textContent = `${Math.round(state.volume * 100)}%`;
}
if (this._contextRadioMiniStation) {
this._contextRadioMiniStation.textContent = uncertain
? t('RADIO STATE UNCERTAIN')
: selected?.name ||
(state.loading ? t('SYNCING DIRECTORY') : t('RADIO READY'));
}
if (this._cockpitRadioStation) {
this._cockpitRadioStation.textContent = uncertain
? t('UNCERTAIN')
-30
View File
@@ -61,36 +61,6 @@ export function readShellElements(document = globalThis.document) {
_cctvPanel: document.getElementById('cctv-panel'),
_streetLevelPanel: document.getElementById('street-level-panel'),
_radioPanel: document.getElementById('radio-panel'),
_contextRadioDock: document.getElementById('context-radio-dock'),
_contextRadioToggleBtn: document.getElementById('context-radio-toggle-btn'),
_contextRadioMini: document.getElementById('context-radio-mini'),
_contextRadioMiniEnableBtn: document.getElementById(
'context-radio-mini-enable-btn',
),
_contextRadioDetailsBtn: document.getElementById(
'context-radio-details-btn',
),
_contextRadioMiniCloseBtn: document.getElementById(
'context-radio-mini-close-btn',
),
_contextRadioMiniStation: document.getElementById(
'context-radio-mini-station',
),
_contextRadioMiniPrevBtn: document.getElementById(
'context-radio-mini-prev-btn',
),
_contextRadioMiniPlayBtn: document.getElementById(
'context-radio-mini-play-btn',
),
_contextRadioMiniNextBtn: document.getElementById(
'context-radio-mini-next-btn',
),
_contextRadioMiniVolume: document.getElementById(
'context-radio-mini-volume',
),
_contextRadioMiniVolumeValue: document.getElementById(
'context-radio-mini-volume-value',
),
_cockpitUtilityControls: document.getElementById(
'cockpit-utility-controls',
),
-8
View File
@@ -949,14 +949,6 @@ export class ShellFacade {
return this._radioControls?._setCockpitDisclosure?.(...args);
}
_setRadioDisclosure(...args) {
return this._radioControls?._setRadioDisclosure?.(...args);
}
_syncContextRadioLauncherState(...args) {
return this._radioControls?._syncContextRadioLauncherState?.(...args);
}
_renderRadioState(...args) {
return this._radioControls?._renderRadioState?.(...args);
}
-2
View File
@@ -26,8 +26,6 @@ const KEYS = [
'Exit cockpit to fly to a location',
'Exit cockpit to fly to a photo',
'Exit cockpit to fly to a shared view',
'Go to expanded Radio section',
'Expand Radio section in Context',
'New scene name',
'Delete scene "{scene}" and all shots?',
'Detailed outline unavailable',
+40 -197
View File
@@ -985,17 +985,13 @@
animation: none;
}
}
/* Context is read for longer than other panels; it carries no halo. */
#global-context-panel .panel-glow {
position: absolute;
inset: -20px;
background: radial-gradient(
ellipse at center,
rgba(98, 181, 255, 0.2) 0%,
transparent 62%
);
filter: blur(20px);
opacity: 0.24;
pointer-events: none;
display: none;
}
/* Its section titles (Context, Radio) stay legible over bright imagery. */
#global-context-panel .panel-title {
color: var(--text-secondary);
}
.global-context-panel-inner {
position: relative;
@@ -1012,158 +1008,6 @@
backdrop-filter: blur(24px) saturate(1.4);
-webkit-backdrop-filter: blur(24px) saturate(1.4);
}
.context-radio-dock {
position: relative;
display: flex;
align-items: center;
}
.context-radio-toggle {
display: grid;
width: 28px;
height: 28px;
place-items: center;
padding: 0;
border: 1px solid rgba(98, 181, 255, 0.22);
border-radius: 6px;
color: rgba(143, 207, 255, 0.58);
background: rgba(42, 113, 171, 0.08);
cursor: pointer;
transition:
border-color 0.15s ease,
color 0.15s ease,
background 0.15s ease,
box-shadow 0.15s ease;
}
.context-radio-toggle .material-symbols-outlined {
font-size: 17px;
}
.context-radio-toggle:hover,
.context-radio-toggle:focus-visible {
border-color: #79c2ff;
color: #fff;
outline: none;
}
.context-radio-toggle.active {
border-color: rgba(101, 247, 255, 0.82);
color: #eaffff;
background: rgba(0, 212, 255, 0.16);
box-shadow:
inset 0 0 12px rgba(0, 212, 255, 0.1),
0 0 12px rgba(0, 212, 255, 0.2);
}
.context-radio-toggle:disabled {
opacity: 0.5;
cursor: wait;
}
.context-radio-mini {
position: absolute;
z-index: 5;
top: calc(100% + 20px);
right: -38px;
display: none;
width: 244px;
box-sizing: border-box;
gap: 8px;
padding: 10px;
border: 1px solid rgba(65, 220, 231, 0.28);
border-radius: 8px;
background: rgba(2, 15, 20, 0.96);
box-shadow:
0 12px 32px rgba(0, 0, 0, 0.62),
inset 0 1px 0 rgba(255, 255, 255, 0.04);
backdrop-filter: blur(22px) saturate(1.35);
-webkit-backdrop-filter: blur(22px) saturate(1.35);
}
.context-radio-mini[hidden] {
display: none !important;
}
.context-radio-dock.disclosure-open .context-radio-mini {
display: grid;
}
.context-radio-mini-heading {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
color: #65f7ff;
font: 7px/1.2 var(--font-mono);
letter-spacing: 0.12em;
}
.context-radio-mini-heading button {
display: grid;
width: 26px;
height: 26px;
place-items: center;
padding: 0;
border: 1px solid rgba(65, 220, 231, 0.28);
border-radius: 5px;
color: #8feaf2;
background: rgba(14, 69, 78, 0.16);
cursor: pointer;
}
.context-radio-mini-heading button:hover,
.context-radio-mini-heading button:focus-visible {
border-color: #65f7ff;
color: #fff;
outline: none;
}
.context-radio-mini-actions {
display: flex;
gap: 5px;
}
.context-radio-mini-heading .material-symbols-outlined {
font-size: 16px;
}
.context-radio-mini > .panel-layer-toggle {
width: 100%;
}
.context-radio-mini-heading strong {
overflow: hidden;
color: #e8feff;
font-size: 8px;
font-weight: 500;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
.context-radio-mini-transport {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 5px;
}
.context-radio-mini-transport button {
min-height: 28px;
padding: 5px;
border: 1px solid rgba(65, 220, 231, 0.28);
border-radius: 5px;
color: #8feaf2;
background: rgba(14, 69, 78, 0.16);
font: 8px/1 var(--font-mono);
cursor: pointer;
}
.context-radio-mini-transport button:hover:not(:disabled),
.context-radio-mini-transport button:focus-visible:not(:disabled),
.context-radio-mini-transport button.active {
border-color: #65f7ff;
color: #fff;
outline: none;
}
.context-radio-mini-transport button:disabled {
opacity: 0.38;
cursor: not-allowed;
}
.context-radio-mini-volume {
display: grid;
grid-template-columns: auto minmax(0, 1fr) 30px;
gap: 7px;
align-items: center;
color: var(--text-dim);
font: 7px/1 var(--font-mono);
letter-spacing: 0.08em;
}
.context-radio-mini-volume input {
width: 100%;
}
.global-context-actions {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
@@ -1229,40 +1073,44 @@
rather than relying on the chrome alone. */
filter: drop-shadow(0 0 5px rgba(150, 210, 255, 0.9));
}
/* One segmented control: the two modes read as a single choice. */
.global-context-modes {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 7px;
margin: 0 0 10px;
gap: 2px;
margin: 0 0 12px;
padding: 2px;
border: 1px solid rgba(98, 181, 255, 0.22);
border-radius: 7px;
background: rgba(0, 0, 0, 0.22);
}
.context-mode-button {
display: flex;
min-width: 0;
min-height: 36px;
min-height: 34px;
align-items: center;
justify-content: center;
gap: 6px;
padding: 7px 8px;
border: 1px solid rgba(98, 181, 255, 0.28);
border-radius: 6px;
color: #8fcfff;
background: rgba(42, 113, 171, 0.09);
font: 8px/1.15 var(--font-mono);
letter-spacing: 0.09em;
padding: 6px 8px;
border: 1px solid transparent;
border-radius: 5px;
color: rgba(214, 236, 255, 0.72);
background: transparent;
font: 600 10px/1.2 var(--font-mono);
letter-spacing: 0.06em;
cursor: pointer;
transition:
border-color 0.15s ease,
color 0.15s ease,
background 0.15s ease,
box-shadow 0.15s ease;
background 0.15s ease;
}
.context-mode-button .material-symbols-outlined {
font-size: 16px;
}
.context-mode-button:hover,
.context-mode-button:focus-visible {
border-color: #79c2ff;
color: #fff;
background: rgba(98, 181, 255, 0.08);
outline: none;
}
/* Keep focus distinct when the selected tab owns the accent colors. */
@@ -1272,12 +1120,9 @@
}
.context-mode-button.active {
border-color: rgba(0, 212, 255, 0.75);
color: #eaffff;
background: rgba(0, 212, 255, 0.14);
box-shadow:
inset 0 0 16px rgba(0, 212, 255, 0.08),
0 0 12px rgba(0, 212, 255, 0.12);
border-color: rgba(0, 212, 255, 0.55);
color: #fff;
background: rgba(0, 212, 255, 0.16);
}
.context-mode-button:disabled {
opacity: 0.55;
@@ -1287,23 +1132,20 @@
.context-mode-standby[hidden] {
display: none !important;
}
/* Plain guidance under the tabs, not a placeholder box. */
.context-mode-standby {
display: grid;
min-height: 72px;
place-content: center;
gap: 6px;
padding: 12px;
border: 1px dashed rgba(98, 181, 255, 0.2);
border-radius: 7px;
color: rgba(190, 230, 230, 0.5);
text-align: center;
font: 8px/1.45 var(--font-mono);
letter-spacing: 0.1em;
padding: 2px 2px 4px;
color: rgba(214, 236, 255, 0.72);
font: 10px/1.6 var(--font-mono);
letter-spacing: 0.02em;
}
.context-mode-standby strong {
color: #8fcfff;
font-size: 9px;
letter-spacing: 0.16em;
color: var(--text-primary);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
}
.context-mode-view {
min-height: 0;
@@ -1344,19 +1186,20 @@
}
.global-context-actions .panel-layer-toggle {
width: 100%;
min-height: 30px;
min-height: 32px;
padding: 6px 8px;
font-size: 8px;
font-size: 10px;
letter-spacing: 0.06em;
}
#global-context-panel #cockpit-entry {
position: static;
z-index: auto;
width: 100%;
min-height: 30px;
min-height: 32px;
padding: 5px 8px;
border-radius: 5px;
justify-content: center;
font-size: 8px;
font-size: 10px;
box-shadow: none;
}
#global-context-panel #cockpit-entry .material-symbols-outlined {
+7 -5
View File
@@ -461,14 +461,16 @@ html:not([lang='en']) .radio-transport button {
border-top: 1px solid rgba(98, 181, 255, 0.16);
}
.military-awareness-standby strong {
color: #79c2ff;
color: var(--text-primary);
font-size: 10px;
letter-spacing: 0.1em;
font-weight: 600;
letter-spacing: 0.08em;
}
.military-awareness-standby span {
color: #8192a6;
font-size: 9px;
letter-spacing: 0.05em;
color: rgba(214, 236, 255, 0.72);
font-size: 10px;
line-height: 1.5;
letter-spacing: 0.02em;
}
.military-awareness-standby button {
justify-self: start;
-4
View File
@@ -228,10 +228,6 @@
#right-context-rail #global-context-panel.collapsed {
width: 100%;
}
.context-radio-mini {
right: -38px;
width: min(244px, calc(100vw - 32px));
}
#left-panel-stack > #data-panel,
#left-panel-stack > #cctv-panel,
-30
View File
@@ -30,36 +30,6 @@
<div class="panel-header">
<span class="panel-title" data-i18n>CONTEXT</span>
<span class="panel-divider"></span>
<div id="context-radio-dock" class="context-radio-dock">
<button id="context-radio-toggle-btn" class="context-radio-toggle" type="button" aria-expanded="false" aria-controls="context-radio-mini" aria-label="Open compact Radio controls" title="Open compact Radio controls" data-i18n-aria-label data-i18n-title>
<span class="material-symbols-outlined" aria-hidden="true">radio</span>
</button>
<div id="context-radio-mini" class="context-radio-mini" role="group" aria-label="Compact Radio controls" hidden data-i18n-aria-label>
<div class="context-radio-mini-heading">
<span data-i18n>RADIO</span>
<strong id="context-radio-mini-station" data-i18n>RADIO READY</strong>
<span class="context-radio-mini-actions">
<button id="context-radio-details-btn" type="button" aria-label="Open detailed Radio controls" aria-expanded="false" aria-controls="radio-panel" title="Open detailed Radio controls" data-i18n-aria-label data-i18n-title>
<span class="material-symbols-outlined" aria-hidden="true">open_in_full</span>
</button>
<button id="context-radio-mini-close-btn" type="button" aria-label="Close compact Radio controls" title="Close compact Radio controls" data-i18n-aria-label data-i18n-title>
<span class="material-symbols-outlined" aria-hidden="true">close</span>
</button>
</span>
</div>
<button id="context-radio-mini-enable-btn" class="panel-layer-toggle" type="button" aria-pressed="false" data-i18n>ENABLE</button>
<div class="context-radio-mini-transport">
<button id="context-radio-mini-prev-btn" type="button" aria-label="Previous filtered radio station" title="Previous station" disabled data-i18n-aria-label data-i18n-title>◀◀</button>
<button id="context-radio-mini-play-btn" type="button" aria-label="Play selected radio station" title="Play" disabled data-i18n-aria-label data-i18n-title>▶</button>
<button id="context-radio-mini-next-btn" type="button" aria-label="Next filtered radio station" title="Next station" disabled data-i18n-aria-label data-i18n-title>▶▶</button>
</div>
<label class="context-radio-mini-volume" for="context-radio-mini-volume">
<span data-i18n>VOLUME</span>
<input id="context-radio-mini-volume" class="gev-quantitative-slider" type="range" min="0" max="100" value="80" aria-label="Compact Radio volume" disabled data-i18n-aria-label />
<output id="context-radio-mini-volume-value" class="gev-slider-value" for="context-radio-mini-volume">80%</output>
</label>
</div>
</div>
<button class="panel-collapse-btn" data-collapse-target="global-context-panel" title="Expand panel" data-i18n-title>▶</button>
</div>
<div class="cyber-panel-body" data-rail-scroller>