mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Merge pull request #1471 from asturksever/ux/context-panel-cleanup
Drop the Radio icon from Context and make the panel easier to read
This commit is contained in:
45 files changed
+235
-1442
No files matched your search
@@ -0,0 +1,7 @@
|
||||
- Remove the Radio icon from the Context panel header, along with the compact
|
||||
Radio player it opened; Radio stays in Context as its own section, and
|
||||
Cockpit's compact Radio is unchanged. Context is also easier to read: the
|
||||
Contacts / Space Missions modes are one segmented control, labels and
|
||||
guidance are larger and higher contrast, the standby hint is plain text
|
||||
instead of a dashed box, and the panel draws no glow halo. Cyber keeps its
|
||||
own framed buttons and red titles (asturksever, #1471).
|
||||
+13
-26
@@ -255,8 +255,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
|
||||
@@ -1519,10 +1519,6 @@ Automatic collapse remains presentation only, prioritizes the latest explicit
|
||||
panel, and honors keyboard focus on the right. Mobile layout still releases
|
||||
desktop allocation styles. Stable Display allocation avoids unnecessary style
|
||||
writes. This extraction does not change panel positions or layout defaults.
|
||||
Context expansion through the panel owner or adaptive right-rail restoration
|
||||
closes an open compact Radio disclosure before Context/Space Missions becomes
|
||||
visible. The mini remains available while Context is collapsed. This fixes the
|
||||
Radio-overlap path reported in #804 without changing z-index or layout policy.
|
||||
The right rail skips panels hidden by other CSS (computed `display: none`), but
|
||||
keeps the collapsed panels its own exclusive presentation hides, so automatic
|
||||
collapse can still be undone.
|
||||
@@ -2011,7 +2007,7 @@ they do not reduce the materialized entity count or establish an FPS gain.
|
||||
rotation and mission-marker highlight as pointer hover. Focus alone does not
|
||||
select the mission; Enter or Space performs selection. Keyboard and pointer
|
||||
preview ownership remain independent when the pointer is parked over the list.
|
||||
- Radio power controls in the full, compact, and Cockpit surfaces, Search Nearby
|
||||
- Radio power controls in the full and Cockpit surfaces, Search Nearby
|
||||
Sites, and Clear Selected Layers remain focusable while lifecycle work is in
|
||||
progress. They announce busy/disabled semantics and ignore repeat activation
|
||||
until the operation settles, so async work cannot drop the keyboard ring.
|
||||
@@ -2257,8 +2253,9 @@ fetching, trailing-24-hour filtering and partial-success caching are unchanged.
|
||||
> to yield to: whoever handles ESC first calls `preventDefault()` **and**
|
||||
> `stopImmediatePropagation()`, and the launcher skips `defaultPrevented` events.
|
||||
> `stopPropagation()` alone does **not** stop later listeners on the same
|
||||
> `document` — that is exactly how the compact Radio disclosure made one key
|
||||
> close the disclosure _and_ dismiss the launcher.
|
||||
> `document` — that is exactly how a disclosure that called it alone made one
|
||||
> key close the disclosure _and_ dismiss the launcher. Cockpit's utility
|
||||
> disclosures (Display, Radio) claim the key the immediate way.
|
||||
>
|
||||
> **Accepted:** a surface class that never clears means no launcher for that page
|
||||
> load, with no timeout. None of the four classes is restored at startup, so an
|
||||
@@ -3000,15 +2997,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
|
||||
@@ -4784,13 +4776,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.
|
||||
|
||||
@@ -4811,6 +4796,8 @@ silently demoting every later lookup for the session.
|
||||
- The expanded `CONTEXT` view offers mutually exclusive `CONTACTS` and `SPACE MISSIONS` modes. Selecting `CONTACTS` enables the context-owned Flights, Military Flights, AIS Vessels, and Mapped Installations dependencies only when they are not already user-enabled; selecting `SPACE MISSIONS` enables the recent-launch layer and its Satellite dependency. `CONTACTS` cycles the nearest supported contact of whatever type is selected. Satellites are deliberately excluded from those Awareness cohorts and keep their own tracking UX. Selecting the active mode again returns to the neutral chooser and releases only mode-owned dependencies.
|
||||
- If a civilian or military aircraft is already tracked when `CONTACTS` becomes operational, that source-owned track is adopted as the Context subject before nearest-contact autofocus. Context rechecks the tracker after its dependencies settle, so a newer selection wins, while an explicit clear during activation prevents fallback from silently selecting a replacement. Cockpit entry remains unavailable until that Context transaction has settled, so its camera takeover cannot clear Cesium tracking before adoption. Adoption does not recreate tracking or transfer camera ownership; it initializes the normal 250 km ring, history, proximity results, and Cockpit Previous/Next state for the original aircraft.
|
||||
- `SEARCH NEARBY SITES` retains the bounded OSM results and makes one user-initiated, view-biased Google Maps Places text search for “military installation.” Google results are source-stamped, deduplicated against OSM by rounded location/name, and remain mapped context rather than operational claims. If Places is unavailable or the API is not enabled for the supplied key, OSM context remains available. A view across the antimeridian is searched around its own centre, with a radius from its real width.
|
||||
- The Context header holds only its title and collapse control. It no longer carries a Radio shortcut or a compact Radio player: Radio is the `RADIO` section inside the panel, with its own disclosure and the single `ENABLE` power control. Cockpit keeps its own compact Radio utility.
|
||||
- Context is set for reading in every theme: mode, standby and action labels are 10 px, and the panel draws no glow halo. Outside Cyber, the two modes form one segmented control, the standby guidance is plain left-aligned text rather than a dashed placeholder, and the expanded panel's section titles use `--text-secondary`. Cyber keeps its separately framed mode buttons, its red panel titles, and in layout-focus a framed, centred standby card over its radar art.
|
||||
- The expanded desktop header omits the redundant `ON` label; the active mode button carries state. Expanded Contact results also omit the duplicate `GLOBAL CONTEXT` / `CONTEXT ONLY` status row and begin with the selected subject and its 250 km scope. Global Context does not fabricate a selected-entity model preview: the provisional hand-authored aircraft wireframe was removed because it was not geometry extracted from the selected entity's actual asset.
|
||||
- Dependency ownership is reversible: disabling Global Context releases only dependencies it enabled, while user-enabled layers remain on. This also removes the Military-layer suppression handoff when Global Context owned Military, allowing an already-enabled civilian Flights layer to resume its normal mixed rendering. If OpenSky is unavailable and has no last-good cache, Flights requests a capped 250 nm adsb.lol point snapshot around the current view anchor and labels that provenance explicitly; it never relabels military-feed rows as civilian data. If both inputs fail, Flights remains `UNKNOWN`.
|
||||
- Space Missions is replay-isolated: Rocket Launches and Satellites are the only Data Layers permitted while the mode is active. Direct UI and voice entry capture the same pre-entry snapshot; internal dependency and restoration enables do not create a user-owned Context session. Entry waits for incompatible layers to shut down, direct incompatible enables are blocked before lifecycle work, and the entry gate remains active through the complete Rocket Launches enable. A newer same-target ON request takes ownership of the pending entry without releasing its isolation snapshot, including when it arrives while the prior request is awaiting the adoption guard. A caller abort, resource cancellation, newer OFF, or layer teardown waits for exact manager settlement and restores that snapshot without resurrecting Rocket Launches. If an abort lands after only part of a restore settles, Context completes the same exact target without the stale caller signal and then replays newer explicit layer intent. Dedicated Voice Context cancellation reports a stable cancelled result plus the current Context state; generic layer visibility additionally exposes manager phase, reason, successor, and lifecycle details. Once an exact voice visibility intent commits, a newer voice turn cannot relabel it as cancelled while Context settlement completes; pre-commit aborts remain cancellable and final lifecycle mismatches remain failures. Clear Selected Layers reserves its complete captured OFF set before sequential teardown; a newer absolute request of any origin supersedes only its layer reservation and remains authoritative. A rejected layer teardown retains the truthful enabled state, rolls already-stopped siblings back to the captured pre-entry set, and aborts replay; rapid exit/re-entry serializes the full Satellite enabled-state and parameter restore before a new snapshot is taken. Contacts remains additive and restores user-enabled layers normally.
|
||||
@@ -5601,7 +5588,7 @@ apply to `public/` assets or built-preview serving.
|
||||
- The desktop right rail (`#right-context-rail`) owns `DISPLAY`, `CCTV`, its active parameter controls, and `GLOBAL CONTEXT` as one fixed responsive stack in that order. Its compact buttons use the same 176 px width as the left accordion and one consistent 50 px height, share the left stack's 52 px edge inset and measured top baseline across HUD variants, then constrain themselves against visible HUD/chrome rectangles and the remaining vertical corridor. `DISPLAY` is no longer draggable and legacy saved coordinates are ignored.
|
||||
- The right rail is labeled **DISPLAY** (formerly "MOVE") and groups, in order, HUD, DETECT, Bloom, Sharpen, 3D, Clean-UI (HUD + DETECT promoted to the top). Its expanded controls retain the same compact 176 px width as the right-side tabs instead of growing to the wider Context detail-card width. It starts expanded on first run and respects the user's later `v6` collapse choice. Collapses/expands with directional chevrons (`◀` collapsed, `▶` expanded).
|
||||
- Display and Context use matching 330 px expanded widths and matching compact tab dimensions. The parameter panel is part of Display's expanded content. DISPLAY may remain open beside one contextual panel; CCTV and Context are mutually exclusive. In Tactical HUD, expanding CCTV or Context hides the other contextual launcher while DISPLAY remains independently available. The most recently opened right-rail panel owns the constrained lane even when it appears later in DOM order; passive restoration and automatic disclosure do not replace that explicit owner. Minimal and other HUD layouts retain the collapsed launchers; when their active panel exceeds the measured corridor, the rail reserves sibling heights and gaps and scrolls the active panel internally.
|
||||
- At 720px and below both panel stacks (`#left-panel-stack`, `#right-context-rail`) go edge-to-edge and scroll vertically. Every hosted panel's decorative `.panel-glow` is pinned to its panel box there (`inset: 0` in `src/ui/styles/responsive.css`): on desktop the glow overhangs the panel by 18–20 px as harmless paint, but inside a scroll container that overhang is scrollable overflow on both axes and drew a horizontal scrollbar band plus a vertical scrollbar that scrolled nothing but glow whenever CCTV, Context, Data Layers or Scenes was expanded. The glow's blur is ink overflow and never scrolls, so the look is unchanged. Do not fix this by clipping the stacks: `overflow-x: clip` would also clip the Context radio popover and would not remove the vertical scrollbar. Pinned by `src/ui/panelRails.test.mjs` against every `(max-width: 720px)` block in the flattened stylesheet.
|
||||
- At 720px and below both panel stacks (`#left-panel-stack`, `#right-context-rail`) go edge-to-edge and scroll vertically. Every hosted panel's decorative `.panel-glow` is pinned to its panel box there (`inset: 0` in `src/ui/styles/responsive.css`): on desktop the glow overhangs the panel by 18–20 px as harmless paint, but inside a scroll container that overhang is scrollable overflow on both axes and drew a horizontal scrollbar band plus a vertical scrollbar that scrolled nothing but glow whenever CCTV, Context, Data Layers or Scenes was expanded. The glow's blur is ink overflow and never scrolls, so the look is unchanged. Do not fix this by clipping the stacks: `overflow-x: clip` would not remove the vertical scrollbar. Context draws no glow at all (`display: none`), so it is not part of that pin. Pinned by `src/ui/panelRails.test.mjs` against every `(max-width: 720px)` block in the flattened stylesheet.
|
||||
- `STYLE PRESETS` and `LOCATIONS` start collapsed, expand on intentional hover/click, and auto-collapse after hover leave delay.
|
||||
- Collapsed mini-status indicators show active style and active location/landmark.
|
||||
- Detection mode is user-controlled and should persist when switching styles. Since 2026-08-22 it also STARTS on — Dense @ 75% for every style on a first run, Normal included — as a `GLOBAL_POST_DEFAULTS` baseline that does NOT set `_detectionUserOverridden`. Exception (unchanged): selecting a military style (CRT/NVG/FLIR) auto-enables the same Dense preset, but only until the user manually changes detection this session (`_detectionUserOverridden` gate), after which style switches never touch it.
|
||||
|
||||
@@ -702,7 +702,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",
|
||||
|
||||
@@ -175,12 +175,12 @@ const MUTATIONS = [
|
||||
to: ' /* belt removed */',
|
||||
},
|
||||
{
|
||||
// Anchored past the call so it cannot land on the cockpit disclosure's own
|
||||
// stopImmediatePropagation() a few lines below and prove nothing about this.
|
||||
defect: 'the radio disclosure returns to stopPropagation, so one ESC does two things',
|
||||
// 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 later document listeners also act on its ESC',
|
||||
file: 'radio',
|
||||
from: ' event.stopImmediatePropagation();\n const escapedFromDisclosure =\n event.target === this._contextRadioToggleBtn ||',
|
||||
to: ' event.stopPropagation();\n const escapedFromDisclosure =\n event.target === this._contextRadioToggleBtn ||',
|
||||
from: " event.stopImmediatePropagation();\n const kind = displayOpen ? 'display' : 'radio';",
|
||||
to: " event.stopPropagation();\n const kind = displayOpen ? 'display' : 'radio';",
|
||||
},
|
||||
{
|
||||
defect: 'the "no timer" decision is deleted, so the next editor re-litigates it blind',
|
||||
|
||||
@@ -357,53 +357,6 @@ async function runArbitrationSection(page, { shots, consoleErrors }) {
|
||||
`classVisible=${afterUncoveredEsc.classVisible} session=${afterUncoveredEsc.session}`,
|
||||
);
|
||||
|
||||
// ── Blocker repro: a control that claims only the KEY ───────────────────
|
||||
// The compact Radio disclosure closes on ESC from a capture listener bound
|
||||
// long before this module exists. It called stopPropagation(), which does
|
||||
// NOT stop later listeners on the same document — so one key closed the
|
||||
// disclosure AND dismissed the launcher. The card is not hiding behind this
|
||||
// one and must not yield to it: the key is simply already spoken for.
|
||||
await open(page, { query: "?welcome=1", errorSink: consoleErrors });
|
||||
const beforeRadio = await launcherState();
|
||||
record('the launcher is up before the radio disclosure opens', beforeRadio.onScreen);
|
||||
const radioOpened = await page.evaluate(() => {
|
||||
document.getElementById('context-radio-toggle-btn')?.click();
|
||||
return !!document.getElementById('context-radio-dock')?.classList.contains('disclosure-open');
|
||||
});
|
||||
await sleep(250);
|
||||
record('the compact Radio disclosure opens', radioOpened);
|
||||
const withRadio = await launcherState();
|
||||
record(
|
||||
'a small disclosure is a key contest, not a screen takeover — no yield',
|
||||
withRadio.classVisible && withRadio.topmost,
|
||||
`classVisible=${withRadio.classVisible} topmost=${withRadio.topmost}`,
|
||||
);
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
await sleep(400);
|
||||
const radioAfterEsc = await page.evaluate(() => (
|
||||
!!document.getElementById('context-radio-dock')?.classList.contains('disclosure-open')
|
||||
));
|
||||
const launcherAfterRadioEsc = await launcherState();
|
||||
record('ESC closes the disclosure', radioOpened && !radioAfterEsc,
|
||||
`disclosure still open=${radioAfterEsc}`);
|
||||
record(
|
||||
'that SAME ESC does not also dismiss the launcher',
|
||||
beforeRadio.onScreen && launcherAfterRadioEsc.classVisible
|
||||
&& launcherAfterRadioEsc.session !== 'dismissed',
|
||||
`classVisible=${launcherAfterRadioEsc.classVisible} session=${launcherAfterRadioEsc.session} — one key, one action`,
|
||||
);
|
||||
// ...and the NEXT press is the launcher's, so nothing was permanently taken.
|
||||
await page.keyboard.press('Escape');
|
||||
await sleep(600);
|
||||
const escAfterRadioClosed = await launcherState();
|
||||
record(
|
||||
'the next ESC belongs to the launcher again',
|
||||
beforeRadio.onScreen && !escAfterRadioClosed.classVisible
|
||||
&& escAfterRadioClosed.session === 'dismissed',
|
||||
`classVisible=${escAfterRadioClosed.classVisible} session=${escAfterRadioClosed.session}`,
|
||||
);
|
||||
|
||||
// ── Deferred reveal: a surface already up when the launcher would show ──
|
||||
// The class goes on as soon as the app exists, which is well before the
|
||||
// launcher's reveal (~T+1.9s), so init genuinely sees a screen it does not
|
||||
|
||||
+69
-325
@@ -81,6 +81,32 @@ function check(name, ok, detail = '') {
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Native pick buffers and clustered overlay entries settle on rendered frames,
|
||||
// not on wall-clock sleeps, particularly with software rendering.
|
||||
async function settleRadioFrames(page) {
|
||||
@@ -285,31 +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();
|
||||
const initialDisclosure = await page.evaluate(() => {
|
||||
const button = document.getElementById('context-radio-toggle-btn');
|
||||
const mini = document.getElementById('context-radio-mini');
|
||||
const enabledBefore = window.__godsEyeView.dataManager.isEnabled('radio');
|
||||
button.click();
|
||||
return {
|
||||
enabledBefore,
|
||||
enabledAfterDisclosure: window.__godsEyeView.dataManager.isEnabled('radio'),
|
||||
expanded: button.getAttribute('aria-expanded'),
|
||||
miniHidden: mini.hidden,
|
||||
};
|
||||
});
|
||||
check(
|
||||
'Radio launcher discloses compact controls without changing power',
|
||||
!initialDisclosure.enabledBefore && !initialDisclosure.enabledAfterDisclosure
|
||||
&& initialDisclosure.expanded === 'true' && !initialDisclosure.miniHidden,
|
||||
JSON.stringify(initialDisclosure),
|
||||
);
|
||||
await page.$eval('#context-radio-mini-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();
|
||||
@@ -328,9 +339,6 @@ async function main() {
|
||||
color: entry.dataset.radioColor,
|
||||
})),
|
||||
enablePressed: document.getElementById('radio-enable-btn').getAttribute('aria-pressed'),
|
||||
launcherExpanded: document.getElementById('context-radio-toggle-btn').getAttribute('aria-expanded'),
|
||||
launcherActive: document.getElementById('context-radio-toggle-btn').classList.contains('active'),
|
||||
compactEnablePressed: document.getElementById('context-radio-mini-enable-btn').getAttribute('aria-pressed'),
|
||||
fullPlayEnabled: !document.getElementById('radio-play-btn').disabled,
|
||||
fullPlayLabel: document.getElementById('radio-play-btn').getAttribute('aria-label'),
|
||||
tunerVisible: !document.getElementById('radio-tuner').hidden,
|
||||
@@ -341,9 +349,8 @@ 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(
|
||||
'compact and detailed Radio power controls reflect the layer manager independently of disclosure',
|
||||
initial.enablePressed === 'true' && initial.compactEnablePressed === 'true'
|
||||
&& initial.launcherExpanded === 'true' && initial.launcherActive,
|
||||
'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`);
|
||||
const musicOption = initial.filterPalette.find((entry) => entry.id === 'music');
|
||||
@@ -419,14 +426,10 @@ async function main() {
|
||||
prevDisabled: document.getElementById('radio-prev-btn').disabled,
|
||||
playDisabled: document.getElementById('radio-play-btn').disabled,
|
||||
volumeDisabled: document.getElementById('radio-volume').disabled,
|
||||
disclosureLabel: document.getElementById('context-radio-toggle-btn').getAttribute('aria-label'),
|
||||
compactPowerLabel: document.getElementById('context-radio-mini-enable-btn').getAttribute('aria-label'),
|
||||
compactPowerText: document.getElementById('context-radio-mini-enable-btn').textContent,
|
||||
fullLabel: document.getElementById('radio-enable-btn').getAttribute('aria-label'),
|
||||
fullText: document.getElementById('radio-enable-btn').textContent,
|
||||
layerState: document.getElementById('radio-layer-state').textContent,
|
||||
playbackMessage: document.getElementById('radio-playback-state').textContent,
|
||||
miniStation: document.getElementById('context-radio-mini-station').textContent,
|
||||
dataRailText: document.querySelector('[data-layer-id="radio"] .data-toggle-btn')?.textContent,
|
||||
dataRailState: document.querySelector('[data-layer-id="radio"] .data-toggle-btn')?.dataset.feedState,
|
||||
dataRailLabel: document.querySelector('[data-layer-id="radio"] .data-toggle-btn')?.getAttribute('aria-label'),
|
||||
@@ -468,14 +471,10 @@ async function main() {
|
||||
&& uncertainPresentation.prevDisabled
|
||||
&& uncertainPresentation.playDisabled
|
||||
&& uncertainPresentation.volumeDisabled
|
||||
&& /compact Radio controls/i.test(uncertainPresentation.disclosureLabel)
|
||||
&& /uncertain/i.test(uncertainPresentation.compactPowerLabel)
|
||||
&& uncertainPresentation.compactPowerText === 'RECONCILE'
|
||||
&& /uncertain/i.test(uncertainPresentation.fullLabel)
|
||||
&& uncertainPresentation.fullText === 'RECONCILE'
|
||||
&& uncertainPresentation.layerState === 'UNCERTAIN'
|
||||
&& /uncertain/i.test(uncertainPresentation.playbackMessage)
|
||||
&& uncertainPresentation.miniStation === 'RADIO STATE UNCERTAIN'
|
||||
&& uncertainPresentation.dataRailText === 'UNCERTAIN'
|
||||
&& uncertainPresentation.dataRailState === 'uncertain'
|
||||
&& /uncertain/i.test(uncertainPresentation.dataRailLabel)
|
||||
@@ -517,10 +516,6 @@ async function main() {
|
||||
presentationActive: window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().presentationActive,
|
||||
markerSourceVisible: window.__godsEyeView.viewer.dataSources.getByName('Radio stations')[0]?.show,
|
||||
full: document.getElementById('radio-enable-btn').textContent,
|
||||
disclosure: document.getElementById('context-radio-toggle-btn').getAttribute('aria-label'),
|
||||
disclosureControls: document.getElementById('context-radio-toggle-btn').getAttribute('aria-controls'),
|
||||
disclosureExpanded: document.getElementById('context-radio-toggle-btn').getAttribute('aria-expanded'),
|
||||
compactPower: document.getElementById('context-radio-mini-enable-btn').textContent,
|
||||
layerState: document.getElementById('radio-layer-state').textContent,
|
||||
message: document.getElementById('radio-playback-state').textContent,
|
||||
stopDisabled: document.getElementById('radio-stop-btn').disabled,
|
||||
@@ -534,8 +529,6 @@ async function main() {
|
||||
&& disablingPresentation.presentationActive === false
|
||||
&& disablingPresentation.markerSourceVisible === false
|
||||
&& disablingPresentation.full === 'DISABLING'
|
||||
&& /compact Radio controls/i.test(disablingPresentation.disclosure)
|
||||
&& disablingPresentation.compactPower === 'DISABLING'
|
||||
&& disablingPresentation.layerState === 'DISABLING'
|
||||
&& /disabling/i.test(disablingPresentation.message)
|
||||
&& disablingPresentation.stopDisabled
|
||||
@@ -592,10 +585,6 @@ async function main() {
|
||||
presentationActive: window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().presentationActive,
|
||||
markerSourceVisible: window.__godsEyeView.viewer.dataSources.getByName('Radio stations')[0]?.show,
|
||||
full: document.getElementById('radio-enable-btn').textContent,
|
||||
disclosure: document.getElementById('context-radio-toggle-btn').getAttribute('aria-label'),
|
||||
disclosureControls: document.getElementById('context-radio-toggle-btn').getAttribute('aria-controls'),
|
||||
disclosureExpanded: document.getElementById('context-radio-toggle-btn').getAttribute('aria-expanded'),
|
||||
compactPower: document.getElementById('context-radio-mini-enable-btn').textContent,
|
||||
layerState: document.getElementById('radio-layer-state').textContent,
|
||||
message: document.getElementById('radio-playback-state').textContent,
|
||||
tunerHidden: document.getElementById('radio-tuner').hidden,
|
||||
@@ -610,10 +599,6 @@ async function main() {
|
||||
&& enablingPresentation.presentationActive === false
|
||||
&& enablingPresentation.markerSourceVisible === false
|
||||
&& enablingPresentation.full === 'ENABLING'
|
||||
&& enablingPresentation.disclosure === 'Go to expanded Radio section'
|
||||
&& enablingPresentation.disclosureControls === 'radio-panel'
|
||||
&& enablingPresentation.disclosureExpanded === 'true'
|
||||
&& enablingPresentation.compactPower === 'ENABLING'
|
||||
&& enablingPresentation.layerState === 'ENABLING'
|
||||
&& /enabling/i.test(enablingPresentation.message)
|
||||
&& enablingPresentation.tunerHidden
|
||||
@@ -1581,112 +1566,9 @@ async function main() {
|
||||
JSON.stringify(directoryRefreshContinuity),
|
||||
);
|
||||
await page.select('#radio-filter', 'news');
|
||||
check('launcher 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 hoverOnlyDisclosure = await page.evaluate(() => {
|
||||
const button = document.getElementById('context-radio-toggle-btn');
|
||||
if (button.getAttribute('aria-expanded') === 'true') button.click();
|
||||
return {
|
||||
expanded: button.getAttribute('aria-expanded'),
|
||||
hidden: document.getElementById('context-radio-mini').hidden,
|
||||
};
|
||||
});
|
||||
await page.hover('#context-radio-toggle-btn');
|
||||
await sleep(100);
|
||||
const hoverOnlyVisible = await page.$eval('#context-radio-mini', (mini) => (
|
||||
!mini.hidden && getComputedStyle(mini).display !== 'none'
|
||||
));
|
||||
check(
|
||||
'hover alone does not disclose compact Radio controls',
|
||||
hoverOnlyDisclosure.expanded === 'false' && hoverOnlyDisclosure.hidden && !hoverOnlyVisible,
|
||||
JSON.stringify({ hoverOnlyDisclosure, hoverOnlyVisible }),
|
||||
);
|
||||
await page.click('#context-radio-toggle-btn');
|
||||
await page.waitForFunction(() => {
|
||||
const button = document.getElementById('context-radio-toggle-btn');
|
||||
const mini = document.getElementById('context-radio-mini');
|
||||
return button.getAttribute('aria-expanded') === 'true'
|
||||
&& !mini.hidden && getComputedStyle(mini).display !== 'none';
|
||||
});
|
||||
const compact = await page.evaluate(() => {
|
||||
const mini = document.getElementById('context-radio-mini');
|
||||
const toggle = document.getElementById('context-radio-toggle-btn');
|
||||
const dock = document.getElementById('context-radio-dock');
|
||||
const miniRect = mini.getBoundingClientRect();
|
||||
const toggleRect = toggle.getBoundingClientRect();
|
||||
const miniStyle = getComputedStyle(mini);
|
||||
return {
|
||||
visible: miniStyle.display !== 'none' && miniRect.width > 0 && miniRect.height > 0,
|
||||
display: miniStyle.display,
|
||||
visibility: miniStyle.visibility,
|
||||
opacity: miniStyle.opacity,
|
||||
miniRect: { top: miniRect.top, left: miniRect.left, width: miniRect.width, height: miniRect.height },
|
||||
toggleRect: { top: toggleRect.top, left: toggleRect.left, width: toggleRect.width, height: toggleRect.height },
|
||||
dockRect: (() => {
|
||||
const rect = dock.getBoundingClientRect();
|
||||
return { top: rect.top, left: rect.left, width: rect.width, height: rect.height };
|
||||
})(),
|
||||
offsetParent: mini.offsetParent?.id || mini.offsetParent?.className || null,
|
||||
prevEnabled: !document.getElementById('context-radio-mini-prev-btn').disabled,
|
||||
playEnabled: !document.getElementById('context-radio-mini-play-btn').disabled,
|
||||
playLabel: document.getElementById('context-radio-mini-play-btn').getAttribute('aria-label'),
|
||||
nextEnabled: !document.getElementById('context-radio-mini-next-btn').disabled,
|
||||
volumeEnabled: !document.getElementById('context-radio-mini-volume').disabled,
|
||||
offsetBelowToggle: Math.round(miniRect.top - toggleRect.bottom),
|
||||
};
|
||||
});
|
||||
check(
|
||||
'explicit launcher disclosure reveals enabled compact Radio controls',
|
||||
compact.visible && compact.prevEnabled && compact.playEnabled && compact.nextEnabled && compact.volumeEnabled,
|
||||
JSON.stringify(compact),
|
||||
);
|
||||
check('compact Play is available before a station is selected', compact.playLabel === 'Play nearest radio station');
|
||||
check('compact Radio controls sit 20px below the launcher', compact.offsetBelowToggle === 20, `${compact.offsetBelowToggle}px`);
|
||||
const compactCloseDisclosure = await page.evaluate(() => {
|
||||
const launcher = document.getElementById('context-radio-toggle-btn');
|
||||
const close = document.getElementById('context-radio-mini-close-btn');
|
||||
close.focus();
|
||||
close.click();
|
||||
const result = {
|
||||
launcherExpanded: launcher.getAttribute('aria-expanded'),
|
||||
compactHidden: document.getElementById('context-radio-mini').hidden,
|
||||
focusRestored: document.activeElement === launcher,
|
||||
radioCollapsed: document.getElementById('radio-panel').classList.contains('collapsed'),
|
||||
};
|
||||
launcher.click();
|
||||
return result;
|
||||
});
|
||||
check(
|
||||
'dedicated compact close hides only the compact Radio disclosure and restores launcher focus',
|
||||
compactCloseDisclosure.launcherExpanded === 'false'
|
||||
&& compactCloseDisclosure.compactHidden
|
||||
&& compactCloseDisclosure.focusRestored
|
||||
&& compactCloseDisclosure.radioCollapsed,
|
||||
JSON.stringify(compactCloseDisclosure),
|
||||
);
|
||||
const compactTouchDisclosure = await page.evaluate(() => {
|
||||
const button = document.getElementById('context-radio-toggle-btn');
|
||||
button.click();
|
||||
const closed = button.getAttribute('aria-expanded') === 'false'
|
||||
&& document.getElementById('context-radio-mini').hidden;
|
||||
button.dispatchEvent(new PointerEvent('pointerdown', {
|
||||
bubbles: true,
|
||||
pointerType: 'touch',
|
||||
isPrimary: true,
|
||||
}));
|
||||
button.click();
|
||||
return {
|
||||
closed,
|
||||
reopened: button.getAttribute('aria-expanded') === 'true'
|
||||
&& !document.getElementById('context-radio-mini').hidden,
|
||||
};
|
||||
});
|
||||
check(
|
||||
'touch/coarse-pointer activation can close and reopen compact Radio controls',
|
||||
compactTouchDisclosure.closed && compactTouchDisclosure.reopened,
|
||||
JSON.stringify(compactTouchDisclosure),
|
||||
);
|
||||
const multiExpandedPanelLayout = await page.evaluate(async () => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
const ids = ['data-panel', 'scene-panel', 'pp-toggles', 'cctv-panel', 'global-context-panel'];
|
||||
@@ -2062,7 +1944,6 @@ async function main() {
|
||||
hudVariant: manager.hud.getVariant(),
|
||||
displayExpanded: display.getAttribute('aria-expanded') === 'true',
|
||||
radioExpanded: radio.getAttribute('aria-expanded') === 'true',
|
||||
contextRadioExpanded: document.getElementById('context-radio-toggle-btn')?.getAttribute('aria-expanded') === 'true',
|
||||
focusId: document.activeElement?.id || null,
|
||||
detectionMode: manager.getDetectionState().detectionMode,
|
||||
models3dEnabled: manager._models3dEnabled,
|
||||
@@ -2289,8 +2170,7 @@ async function main() {
|
||||
document.getElementById('cockpit-radio-next-btn'),
|
||||
].every((button) => button?.isConnected && !button.disabled);
|
||||
const detailsExcluded = !document.querySelector(
|
||||
'#cockpit-radio-panel #context-radio-details-btn, '
|
||||
+ '#cockpit-radio-panel #radio-filter, #cockpit-radio-panel #radio-tuner',
|
||||
'#cockpit-radio-panel #radio-filter, #cockpit-radio-panel #radio-tuner',
|
||||
);
|
||||
const scenesHidden = getComputedStyle(scene).display === 'none';
|
||||
radio.click();
|
||||
@@ -2383,7 +2263,6 @@ async function main() {
|
||||
manager.setPanelCollapsed('global-context-panel', prior.contextCollapsed);
|
||||
if (display.getAttribute('aria-expanded') !== String(prior.displayExpanded)) display.click();
|
||||
if (radio.getAttribute('aria-expanded') !== String(prior.radioExpanded)) radio.click();
|
||||
manager._setRadioDisclosure(prior.contextRadioExpanded);
|
||||
hud.hidden = prior.hudHidden;
|
||||
signal.hidden = prior.signalHidden;
|
||||
if (prior.utilityTop) hud.style.setProperty('--cockpit-utility-top', prior.utilityTop);
|
||||
@@ -2418,7 +2297,6 @@ async function main() {
|
||||
&& document.getElementById('global-context-panel').classList.contains('collapsed') === prior.contextCollapsed
|
||||
&& display.getAttribute('aria-expanded') === String(prior.displayExpanded)
|
||||
&& radio.getAttribute('aria-expanded') === String(prior.radioExpanded)
|
||||
&& document.getElementById('context-radio-toggle-btn')?.getAttribute('aria-expanded') === String(prior.contextRadioExpanded)
|
||||
&& hud.hidden === prior.hudHidden
|
||||
&& signal.hidden === prior.signalHidden
|
||||
&& intelHud.style.getPropertyValue('transition') === priorHudTransition
|
||||
@@ -2474,21 +2352,20 @@ async function main() {
|
||||
check('first Play scenario starts without a leaked Radio selection', firstPlayPrecondition === null,
|
||||
`selected=${firstPlayPrecondition}`);
|
||||
await page.select('#radio-filter', 'all');
|
||||
await page.$eval('#context-radio-toggle-btn', (button) => {
|
||||
if (button.getAttribute('aria-expanded') !== 'true') button.click();
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
const camera = window.__godsEyeView.viewer.camera;
|
||||
window.__qaFirstPlayFlyToCalls = [];
|
||||
window.__qaFirstPlayOriginalFlyTo = camera.flyTo;
|
||||
camera.flyTo = (options) => window.__qaFirstPlayFlyToCalls.push(options);
|
||||
document.getElementById('context-radio-toggle-btn').focus();
|
||||
});
|
||||
await page.$eval('#context-radio-mini-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();
|
||||
@@ -2595,59 +2472,19 @@ async function main() {
|
||||
);
|
||||
await page.select('#radio-filter', 'news');
|
||||
|
||||
await page.$eval('#context-radio-mini-volume', (input) => {
|
||||
await page.$eval('#radio-volume', (input) => {
|
||||
input.value = '35';
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
});
|
||||
const compactVolume = await page.evaluate(() => ({
|
||||
const radioVolume = await page.evaluate(() => ({
|
||||
volume: window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().volume,
|
||||
full: document.getElementById('radio-volume').value,
|
||||
mini: document.getElementById('context-radio-mini-volume').value,
|
||||
label: document.getElementById('radio-volume-value').textContent,
|
||||
}));
|
||||
check('compact volume shares the full Radio player state', Math.abs(compactVolume.volume - 0.35) < 0.001 && compactVolume.full === '35' && compactVolume.mini === '35');
|
||||
|
||||
const compactDisclosureContinuity = await page.evaluate(async () => {
|
||||
const radio = window.__godsEyeView.dataManager.layers.get('radio').module;
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
manager.setPanelCollapsed('global-context-panel', true);
|
||||
manager._setRadioDisclosure(true);
|
||||
await radio.play({ origin: 'user' });
|
||||
const before = radio.getUIState();
|
||||
const disclosure = document.getElementById('context-radio-toggle-btn');
|
||||
if (disclosure.getAttribute('aria-expanded') !== 'true') disclosure.click();
|
||||
disclosure.click();
|
||||
const closed = {
|
||||
expanded: disclosure.getAttribute('aria-expanded'),
|
||||
hidden: document.getElementById('context-radio-mini').hidden,
|
||||
state: radio.getUIState(),
|
||||
};
|
||||
disclosure.click();
|
||||
return {
|
||||
before,
|
||||
closed,
|
||||
reopened: disclosure.getAttribute('aria-expanded'),
|
||||
miniHidden: document.getElementById('context-radio-mini').hidden,
|
||||
after: radio.getUIState(),
|
||||
};
|
||||
});
|
||||
check(
|
||||
'compact disclosure preserves playback, station, filter, and volume continuity',
|
||||
compactDisclosureContinuity.closed.expanded === 'false'
|
||||
&& compactDisclosureContinuity.closed.hidden
|
||||
&& compactDisclosureContinuity.reopened === 'true'
|
||||
&& !compactDisclosureContinuity.miniHidden
|
||||
&& compactDisclosureContinuity.before.audioState === 'playing'
|
||||
&& compactDisclosureContinuity.closed.state.audioState === 'playing'
|
||||
&& compactDisclosureContinuity.after.audioState === 'playing'
|
||||
&& compactDisclosureContinuity.after.selected?.id === compactDisclosureContinuity.before.selected?.id
|
||||
&& compactDisclosureContinuity.after.filter === compactDisclosureContinuity.before.filter
|
||||
&& compactDisclosureContinuity.after.volume === compactDisclosureContinuity.before.volume,
|
||||
JSON.stringify({
|
||||
before: compactDisclosureContinuity.before,
|
||||
closed: compactDisclosureContinuity.closed,
|
||||
reopened: compactDisclosureContinuity.reopened,
|
||||
after: compactDisclosureContinuity.after,
|
||||
}),
|
||||
'full Radio volume drives the Radio player state',
|
||||
Math.abs(radioVolume.volume - 0.35) < 0.001 && radioVolume.full === '35' && radioVolume.label === '35%',
|
||||
JSON.stringify(radioVolume),
|
||||
);
|
||||
|
||||
const voicePause = await page.evaluate(async () => {
|
||||
@@ -2711,11 +2548,13 @@ async function main() {
|
||||
idleVoice.audioState === 'paused',
|
||||
idleVoice.audioState,
|
||||
);
|
||||
const manualPlayTarget = await page.evaluate(() => {
|
||||
const manualPlayTarget = await page.evaluate(async () => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
manager.setPanelCollapsed('global-context-panel', true);
|
||||
manager._setRadioDisclosure(true);
|
||||
const button = document.getElementById('context-radio-mini-play-btn');
|
||||
manager.setPanelCollapsed('global-context-panel', false);
|
||||
manager.setPanelCollapsed('radio-panel', false);
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
const button = document.getElementById('radio-play-btn');
|
||||
button.scrollIntoView({ block: 'nearest' });
|
||||
const rect = button.getBoundingClientRect();
|
||||
const style = getComputedStyle(button);
|
||||
return {
|
||||
@@ -2730,12 +2569,12 @@ async function main() {
|
||||
};
|
||||
});
|
||||
check(
|
||||
'manual voice takeover targets the visible compact Radio Play control',
|
||||
'manual voice takeover targets the visible full Radio Play control',
|
||||
manualPlayTarget.visible && manualPlayTarget.enabled,
|
||||
JSON.stringify(manualPlayTarget),
|
||||
);
|
||||
if (manualPlayTarget.visible && manualPlayTarget.enabled) {
|
||||
await page.click('#context-radio-mini-play-btn');
|
||||
await page.click('#radio-play-btn');
|
||||
await page.waitForFunction(() => (
|
||||
window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().audioState === 'playing'
|
||||
&& window.__godsEyeView.voiceCommands.status === 'idle'
|
||||
@@ -2783,7 +2622,6 @@ async function main() {
|
||||
await page.evaluate(() => window.__godsEyeView.voiceCommands.setStatus('idle', 'QA complete'));
|
||||
await page.evaluate(async () => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
manager._setRadioDisclosure(false);
|
||||
manager.setPanelCollapsed('global-context-panel', false);
|
||||
manager.setPanelCollapsed('radio-panel', false);
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
@@ -3837,22 +3675,22 @@ async function main() {
|
||||
gev.dataManager.layers.get('flights').enabled = true;
|
||||
});
|
||||
await sleep(500);
|
||||
await page.evaluate(() => window.__godsEyeView.styleManager.setPanelCollapsed('global-context-panel', true));
|
||||
await page.evaluate(() => {
|
||||
const button = document.getElementById('context-radio-toggle-btn');
|
||||
if (button.getAttribute('aria-expanded') !== 'true') button.click();
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
manager.setPanelCollapsed('global-context-panel', false);
|
||||
manager.setPanelCollapsed('radio-panel', false);
|
||||
});
|
||||
await page.waitForSelector('#context-radio-mini', { visible: true });
|
||||
await page.waitForSelector('#radio-next-btn', { visible: true });
|
||||
await page.evaluate(() => {
|
||||
window.__qaRadioCameraHeight = window.__godsEyeView.viewer.camera.positionCartographic.height;
|
||||
});
|
||||
const radioOnlyNextDispatched = await page.evaluate(() => {
|
||||
const button = document.getElementById('context-radio-mini-next-btn');
|
||||
const button = document.getElementById('radio-next-btn');
|
||||
if (!button) return false;
|
||||
button.click();
|
||||
return true;
|
||||
});
|
||||
if (!radioOnlyNextDispatched) throw new Error('Radio compact next button unavailable');
|
||||
if (!radioOnlyNextDispatched) throw new Error('Radio next button unavailable');
|
||||
await page.waitForFunction(() => typeof window.__qaRejectRadioPlay === 'function');
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
@@ -3887,7 +3725,7 @@ async function main() {
|
||||
flyToCalls: window.__qaRadioFlyToCalls.length,
|
||||
flightTargets,
|
||||
cameraHeight: window.__qaRadioCameraHeight,
|
||||
compactPlayLabel: document.getElementById('context-radio-mini-play-btn').getAttribute('aria-label'),
|
||||
playLabel: document.getElementById('radio-play-btn').getAttribute('aria-label'),
|
||||
regularMarkerSize: Array.from({ length: window.__godsEyeView.viewer.dataSources.length }, (_, index) => window.__godsEyeView.viewer.dataSources.get(index))
|
||||
.find((item) => item.name === 'Radio stations')?.entities.values[0]?.point?.pixelSize?.getValue(),
|
||||
selectedMarkerSize: selectedEntity?.point?.pixelSize?.getValue(),
|
||||
@@ -3900,7 +3738,7 @@ async function main() {
|
||||
};
|
||||
});
|
||||
check(
|
||||
'compact Next follows stable directory order and retries its next available station',
|
||||
'Radio Next follows stable directory order and retries its next available station',
|
||||
playing.state.selected?.id === playing.expectedFallback?.id
|
||||
&& playing.calls[0] === playing.expectedPrimary?.streamUrl
|
||||
&& playing.calls[1] === playing.expectedFallback?.streamUrl,
|
||||
@@ -3911,9 +3749,9 @@ async function main() {
|
||||
fallback: playing.expectedFallback?.id,
|
||||
}),
|
||||
);
|
||||
check('compact next is explicit playback and updates play/pause state', playing.calls.length === 2 && playing.compactPlayLabel.startsWith('Pause'));
|
||||
check('Radio next is explicit playback and updates play/pause state', playing.calls.length === 2 && playing.playLabel.startsWith('Pause'));
|
||||
check(
|
||||
'compact next rotates toward its station without changing zoom, including fallback',
|
||||
'Radio next rotates toward its station without changing zoom, including fallback',
|
||||
playing.flyToCalls === 2
|
||||
&& Math.abs(playing.flightTargets[0]?.lat - playing.expectedPrimary?.lat) < 0.001
|
||||
&& Math.abs(playing.flightTargets[0]?.lon - playing.expectedPrimary?.lon) < 0.001
|
||||
@@ -3952,11 +3790,11 @@ async function main() {
|
||||
};
|
||||
return [
|
||||
await run('flights', 'radio-next-btn'),
|
||||
await run('military', 'context-radio-mini-prev-btn'),
|
||||
await run('military', 'radio-prev-btn'),
|
||||
];
|
||||
});
|
||||
check(
|
||||
'full and compact Radio navigation preserve Flights/Military camera ownership',
|
||||
'full Radio Next and Prev navigation preserve Flights/Military camera ownership',
|
||||
trackedRadioControls.every((sample) => sample.afterStationId
|
||||
&& sample.afterStationId !== sample.beforeStationId
|
||||
&& sample.flyToDelta === 0
|
||||
@@ -3970,7 +3808,7 @@ async function main() {
|
||||
viewer.trackedEntity = undefined;
|
||||
window.__qaRadioDelayNextFailure = true;
|
||||
const beforeFlyToCalls = window.__qaRadioFlyToCalls.length;
|
||||
document.getElementById('context-radio-mini-next-btn')?.click();
|
||||
document.getElementById('radio-next-btn')?.click();
|
||||
return { beforeFlyToCalls };
|
||||
});
|
||||
await page.waitForFunction(() => typeof window.__qaRejectRadioPlay === 'function');
|
||||
@@ -4246,7 +4084,7 @@ async function main() {
|
||||
window.__godsEyeView.dataManager.layers.get('flights').enabled = false;
|
||||
});
|
||||
const radioOnlyNextClicked = await page.evaluate(() => {
|
||||
const button = document.getElementById('context-radio-mini-next-btn');
|
||||
const button = document.getElementById('radio-next-btn');
|
||||
if (!(button instanceof HTMLButtonElement)) return false;
|
||||
button.click();
|
||||
return true;
|
||||
@@ -4510,106 +4348,12 @@ async function main() {
|
||||
&& companion.radioCollapsed && !companion.contextCollapsed && companion.nested,
|
||||
);
|
||||
|
||||
const expandedContextRadioBefore = await page.evaluate(() => {
|
||||
const gev = window.__godsEyeView;
|
||||
const scroller = document.querySelector('#global-context-panel .global-context-panel-inner');
|
||||
scroller.scrollTop = 0;
|
||||
const camera = gev.viewer.camera.positionWC;
|
||||
return {
|
||||
scrollTop: scroller.scrollTop,
|
||||
pageY: window.scrollY,
|
||||
camera: { x: camera.x, y: camera.y, z: camera.z },
|
||||
plays: window.__qaRadioPlayCalls.length,
|
||||
};
|
||||
});
|
||||
await page.click('#context-radio-toggle-btn');
|
||||
await page.waitForFunction(() => !document.getElementById('radio-panel').classList.contains('collapsed'));
|
||||
await page.waitForFunction((priorScroll) => (
|
||||
document.activeElement?.getAttribute('data-collapse-target') === 'radio-panel'
|
||||
&& document.querySelector('#global-context-panel .global-context-panel-inner').scrollTop > priorScroll
|
||||
), { timeout: 10_000 }, expandedContextRadioBefore.scrollTop);
|
||||
const expandedContextRadioAfter = await page.evaluate(() => {
|
||||
const gev = window.__godsEyeView;
|
||||
gev.styleManager._renderRadioState(gev.dataManager.layers.get('radio').module.getUIState());
|
||||
const launcher = document.getElementById('context-radio-toggle-btn');
|
||||
const radio = document.getElementById('radio-panel');
|
||||
const scroller = document.querySelector('#global-context-panel .global-context-panel-inner');
|
||||
const camera = gev.viewer.camera.positionWC;
|
||||
return {
|
||||
ariaControls: launcher.getAttribute('aria-controls'),
|
||||
expanded: launcher.getAttribute('aria-expanded'),
|
||||
label: launcher.getAttribute('aria-label'),
|
||||
radioCollapsed: radio.classList.contains('collapsed'),
|
||||
compactHidden: document.getElementById('context-radio-mini').hidden,
|
||||
scrollTop: scroller.scrollTop,
|
||||
focusTarget: document.activeElement?.getAttribute('data-collapse-target'),
|
||||
pageY: window.scrollY,
|
||||
camera: { x: camera.x, y: camera.y, z: camera.z },
|
||||
plays: window.__qaRadioPlayCalls.length,
|
||||
state: gev.dataManager.layers.get('radio').module.getUIState().audioState,
|
||||
};
|
||||
});
|
||||
const expandedContextRadioCameraDelta = Math.hypot(
|
||||
expandedContextRadioAfter.camera.x - expandedContextRadioBefore.camera.x,
|
||||
expandedContextRadioAfter.camera.y - expandedContextRadioBefore.camera.y,
|
||||
expandedContextRadioAfter.camera.z - expandedContextRadioBefore.camera.z,
|
||||
);
|
||||
check(
|
||||
'expanded Context Radio icon reveals and scrolls to embedded Radio without an overlay or playback/camera theft',
|
||||
expandedContextRadioAfter.ariaControls === 'radio-panel'
|
||||
&& expandedContextRadioAfter.expanded === 'true'
|
||||
&& expandedContextRadioAfter.label === 'Go to expanded Radio section'
|
||||
&& !expandedContextRadioAfter.radioCollapsed
|
||||
&& expandedContextRadioAfter.compactHidden
|
||||
&& expandedContextRadioAfter.scrollTop > expandedContextRadioBefore.scrollTop
|
||||
&& expandedContextRadioAfter.focusTarget === 'radio-panel'
|
||||
&& expandedContextRadioAfter.pageY === expandedContextRadioBefore.pageY
|
||||
&& expandedContextRadioCameraDelta < 0.01
|
||||
&& expandedContextRadioAfter.plays === expandedContextRadioBefore.plays
|
||||
&& expandedContextRadioAfter.state === 'playing',
|
||||
JSON.stringify({
|
||||
before: expandedContextRadioBefore,
|
||||
after: expandedContextRadioAfter,
|
||||
cameraDelta: expandedContextRadioCameraDelta,
|
||||
}),
|
||||
);
|
||||
|
||||
await page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
manager.setPanelCollapsed('global-context-panel', true);
|
||||
manager._setRadioDisclosure(true);
|
||||
manager.setPanelCollapsed('global-context-panel', false);
|
||||
manager.setPanelCollapsed('radio-panel', false);
|
||||
});
|
||||
await page.waitForFunction(() => {
|
||||
const close = document.getElementById('context-radio-mini-close-btn');
|
||||
const rect = close.getBoundingClientRect();
|
||||
return !document.getElementById('context-radio-mini').hidden && rect.width > 0 && rect.height > 0;
|
||||
});
|
||||
await page.click('#context-radio-mini-close-btn');
|
||||
const closedCompact = await page.evaluate(() => {
|
||||
const launcher = document.getElementById('context-radio-toggle-btn');
|
||||
return {
|
||||
radioCollapsed: document.getElementById('radio-panel').classList.contains('collapsed'),
|
||||
compactHidden: document.getElementById('context-radio-mini').hidden,
|
||||
focusRestored: document.activeElement === launcher,
|
||||
};
|
||||
});
|
||||
await page.click('#context-radio-toggle-btn');
|
||||
await page.waitForFunction(() => !document.getElementById('context-radio-mini').hidden);
|
||||
await page.click('#context-radio-details-btn');
|
||||
const repeatedOpen = await page.evaluate(() => ({
|
||||
radioCollapsed: document.getElementById('radio-panel').classList.contains('collapsed'),
|
||||
compactHidden: document.getElementById('context-radio-mini').hidden,
|
||||
}));
|
||||
const splitDetailActions = { closedCompact, repeatedOpen };
|
||||
check(
|
||||
'collapsed Context keeps compact close and full-panel open as separate stable actions',
|
||||
!splitDetailActions.closedCompact.radioCollapsed
|
||||
&& splitDetailActions.closedCompact.compactHidden
|
||||
&& splitDetailActions.closedCompact.focusRestored
|
||||
&& !splitDetailActions.repeatedOpen.radioCollapsed
|
||||
&& splitDetailActions.repeatedOpen.compactHidden,
|
||||
JSON.stringify(splitDetailActions),
|
||||
);
|
||||
await page.waitForFunction(() => !document.getElementById('radio-panel').classList.contains('collapsed'));
|
||||
|
||||
await page.click('#global-context-flights-btn');
|
||||
await page.waitForFunction(() => !document.getElementById('context-flights-view').hidden);
|
||||
|
||||
@@ -227,13 +227,9 @@ test('Cockpit Escape handling precedes form-control shortcut suppression and foc
|
||||
);
|
||||
});
|
||||
|
||||
test('Cockpit shortcut failures do not leak and open Radio owns the first Escape', () => {
|
||||
test('Cockpit shortcut failures do not leak and open utilities own the first Escape', () => {
|
||||
const keydown = cockpitKeyDown.toString();
|
||||
assert.ok(keydown, 'Cockpit keyboard handler is missing');
|
||||
assert.match(
|
||||
keydown,
|
||||
/document\s*\.getElementById\('context-radio-dock'\)\s*\?\.classList\.contains\('disclosure-open'\)/,
|
||||
);
|
||||
assert.match(keydown, /#cockpit-utility-controls \[aria-expanded="true"\]/);
|
||||
assert.match(
|
||||
keydown,
|
||||
@@ -443,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,
|
||||
@@ -898,7 +894,7 @@ test('Cockpit Radio station changes preserve first-person camera ownership', ()
|
||||
);
|
||||
assert.match(
|
||||
radioBindings,
|
||||
/listen\(this\._contextRadioMiniNextBtn, 'click', \(\) => cycleRadio\(1\)\)/,
|
||||
/listen\(this\._radioNextBtn, 'click', \(\) => cycleRadio\(1\)\)/,
|
||||
);
|
||||
assert.match(
|
||||
radioBindings,
|
||||
|
||||
@@ -129,7 +129,7 @@ test('Context tabs draw a visible keyboard-focus outline including active tabs',
|
||||
|
||||
test('Context async action buttons remain focused while busy', () => {
|
||||
const init = _initGlobalContextPanel.toString();
|
||||
const radio = radioBindings.slice(radioBindings.indexOf('const toggleRadio = async (trigger) => {'), radioBindings.indexOf('this.listen(this._contextRadioToggleBtn,'));
|
||||
const radio = radioBindings.slice(radioBindings.indexOf('const toggleRadio = async (trigger) => {'), radioBindings.indexOf('this.listen(this._cockpitRadioToggleBtn,'));
|
||||
const radioSync = radioPresentation.slice(0, radioPresentation.indexOf('if (this._radioFilter)'));
|
||||
const clear = clearSelectedLayers.toString();
|
||||
|
||||
@@ -137,7 +137,7 @@ test('Context async action buttons remain focused while busy', () => {
|
||||
assert.doesNotMatch(init, /button\.disabled\s*=\s*true/);
|
||||
assert.match(radio, /trigger\.getAttribute\('aria-busy'\) === 'true'/);
|
||||
assert.doesNotMatch(radio, /trigger\.disabled\s*=\s*true/);
|
||||
for (const name of ['_radioEnableBtn', '_contextRadioMiniEnableBtn', '_cockpitRadioEnableBtn']) {
|
||||
for (const name of ['_radioEnableBtn', '_cockpitRadioEnableBtn']) {
|
||||
assert.match(radioSync, new RegExp(`${name}\\.disabled = false`));
|
||||
}
|
||||
assert.doesNotMatch(clear, /_clearSelectedLayersBtn\.disabled\s*=\s*true/);
|
||||
@@ -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,24 +374,26 @@ 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 — the radio 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.
|
||||
const radioEsc = ui.slice(ui.indexOf("event.key !== 'Escape' ||"));
|
||||
const claim = radioEsc.slice(0, radioEsc.indexOf('setRadioDisclosure(false'));
|
||||
// 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\(\);/);
|
||||
assert.match(claim, /event\.stopImmediatePropagation\(\);/);
|
||||
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,
|
||||
|
||||
@@ -561,8 +561,6 @@
|
||||
"OFF AIR": "NICHT AUF SENDUNG",
|
||||
"STATION UNAVAILABLE": "SENDER NICHT VERFÜGBAR",
|
||||
"Radio could not {action} cleanly": "Radio konnte nicht sauber {action}",
|
||||
"Close compact Radio controls": "Kompakte Radio-Steuerung schließen",
|
||||
"Open compact Radio controls": "Kompakte Radio-Steuerung öffnen",
|
||||
"SYNC": "SYNC",
|
||||
"RECONCILE": "ABGLEICHEN",
|
||||
"DISABLE": "AUS",
|
||||
@@ -582,9 +580,6 @@
|
||||
"selected": "gewählter",
|
||||
"nearest": "nächstgelegener",
|
||||
"{action} {target} radio station": "{action}: {target} Radiosender",
|
||||
"RADIO STATE UNCERTAIN": "RADIOSTATUS UNKLAR",
|
||||
"SYNCING DIRECTORY": "VERZEICHNIS WIRD SYNCHRONISIERT",
|
||||
"RADIO READY": "RADIO BEREIT",
|
||||
"SYNCING": "SYNC",
|
||||
"READY": "BEREIT",
|
||||
"station": "Sender",
|
||||
@@ -749,11 +744,6 @@
|
||||
"Search any location...": "Ort suchen...",
|
||||
"WEATHER": "WETTER",
|
||||
"CONTEXT": "KONTEXT",
|
||||
"Compact Radio controls": "Kompakte Radio-Steuerung",
|
||||
"Open detailed Radio controls": "Detaillierte Radio-Steuerung öffnen",
|
||||
"Previous station": "Vorheriger Sender",
|
||||
"Next station": "Nächster Sender",
|
||||
"Compact Radio volume": "Lautstärke des kompakten Radios",
|
||||
"Expand panel": "Bereich ausklappen",
|
||||
"Context mode": "Kontextmodus",
|
||||
"CONTACTS": "KONTAKTE",
|
||||
@@ -2237,8 +2227,6 @@
|
||||
"Los Angeles": "Los Angeles",
|
||||
"CH {index} / {total}": "KAN {index} / {total}",
|
||||
"Station unavailable after directory refresh": "Sender nach Verzeichnisaktualisierung nicht verfügbar",
|
||||
"Go to expanded Radio section": "Zum ausgeklappten Radio-Bereich",
|
||||
"Expand Radio section in Context": "Radio-Bereich in „Kontext“ ausklappen",
|
||||
"{category} ({count})": "{category} ({count})",
|
||||
"PAUSE": "PAUSE",
|
||||
"RESUME": "FORTSETZEN",
|
||||
|
||||
@@ -195,21 +195,13 @@
|
||||
"AVAILABLE MISSIONS": "MISIONES DISPONIBLES",
|
||||
"SELECT A MISSION TO INSPECT": "SELECCIONA UNA MISIÓN PARA INSPECCIONAR",
|
||||
"LOADING 30-DAY MISSION INDEX": "CARGANDO ÍNDICE DE MISIONES DE 30 DÍAS",
|
||||
"Open compact Radio controls": "Abrir los controles compactos de Radio",
|
||||
"Compact Radio controls": "Controles compactos de Radio",
|
||||
"RADIO": "RADIO",
|
||||
"RADIO READY": "RADIO LISTA",
|
||||
"Open detailed Radio controls": "Abrir los controles detallados de Radio",
|
||||
"Close compact Radio controls": "Cerrar los controles compactos de Radio",
|
||||
"ENABLE": "ACTIVAR",
|
||||
"Previous filtered radio station": "Emisora de radio filtrada anterior",
|
||||
"Previous station": "Emisora anterior",
|
||||
"Play selected radio station": "Reproducir la emisora de radio seleccionada",
|
||||
"Play": "Reproducir",
|
||||
"Next filtered radio station": "Siguiente emisora de radio filtrada",
|
||||
"Next station": "Siguiente emisora",
|
||||
"VOLUME": "VOLUMEN",
|
||||
"Compact Radio volume": "Volumen de la Radio compacta",
|
||||
"Cockpit display and Radio controls": "Controles de visualización y Radio de la cabina",
|
||||
"Expand Cockpit display options": "Expandir las opciones de visualización de la cabina",
|
||||
"Cockpit display options": "Opciones de visualización de la cabina",
|
||||
@@ -354,8 +346,6 @@
|
||||
"selected": "seleccionada",
|
||||
"nearest": "más cercana",
|
||||
"{action} {target} radio station": "{action} la emisora de radio {target}",
|
||||
"RADIO STATE UNCERTAIN": "ESTADO DE RADIO SIN CONFIRMAR",
|
||||
"SYNCING DIRECTORY": "SINCRONIZANDO DIRECTORIO",
|
||||
"SYNCING": "SINCRONIZANDO",
|
||||
"station": "emisora",
|
||||
"Ready": "Listo",
|
||||
@@ -2441,8 +2431,6 @@
|
||||
"Exit cockpit to fly to a route": "Sal de la cabina para volar a una ruta",
|
||||
"Exit cockpit to fly to a file": "Sal de la cabina para volar a un archivo",
|
||||
"Exit cockpit to fly to a scene": "Sal de la cabina para volar a una escena",
|
||||
"Go to expanded Radio section": "Ir a la sección de Radio expandida",
|
||||
"Expand Radio section in Context": "Expandir la sección de Radio en Contexto",
|
||||
"New scene name": "Nombre de la nueva escena",
|
||||
"Delete scene \"{scene}\" and all shots?": "¿Eliminar la escena \"{scene}\" y todos sus planos?",
|
||||
"Detailed outline unavailable": "Contorno detallado no disponible",
|
||||
|
||||
@@ -1957,10 +1957,6 @@
|
||||
"STATION UNAVAILABLE": "STATION INDISPONIBLE",
|
||||
"Station unavailable after directory refresh": "Station indisponible après l'actualisation de l'annuaire",
|
||||
"Radio could not {action} cleanly": "La Radio n'a pas pu {action} proprement",
|
||||
"Go to expanded Radio section": "Aller à la section Radio développée",
|
||||
"Expand Radio section in Context": "Développer la section Radio dans Contexte",
|
||||
"Close compact Radio controls": "Fermer les commandes Radio compactes",
|
||||
"Open compact Radio controls": "Ouvrir les commandes Radio compactes",
|
||||
"SYNC": "SYNCHRO",
|
||||
"RECONCILE": "RÉTABLIR",
|
||||
"DISABLE": "DÉSACTIVER",
|
||||
@@ -1983,9 +1979,6 @@
|
||||
"selected": "sélectionnée",
|
||||
"nearest": "la plus proche",
|
||||
"{action} {target} radio station": "{action} : station radio {target}",
|
||||
"RADIO STATE UNCERTAIN": "ÉTAT RADIO INCERTAIN",
|
||||
"SYNCING DIRECTORY": "SYNCHRO DE L'ANNUAIRE",
|
||||
"RADIO READY": "RADIO PRÊTE",
|
||||
"SYNCING": "SYNCHRO",
|
||||
"READY": "PRÊT",
|
||||
"station": "station",
|
||||
@@ -2795,11 +2788,6 @@
|
||||
"RECENT IMAGERY": "IMAGERIE RÉCENTE",
|
||||
"Imagery days in the box": "Jours d'imagerie dans la zone",
|
||||
"CONTEXT": "CONTEXTE",
|
||||
"Compact Radio controls": "Commandes Radio compactes",
|
||||
"Open detailed Radio controls": "Ouvrir les commandes Radio détaillées",
|
||||
"Previous station": "Station précédente",
|
||||
"Next station": "Station suivante",
|
||||
"Compact Radio volume": "Volume de la Radio compacte",
|
||||
"Expand panel": "Développer le panneau",
|
||||
"Context mode": "Mode de contexte",
|
||||
"CONTACTS": "CONTACTS",
|
||||
|
||||
@@ -1957,10 +1957,6 @@
|
||||
"STATION UNAVAILABLE": "STAZIONE NON DISPONIBILE",
|
||||
"Station unavailable after directory refresh": "Stazione non disponibile dopo l'aggiornamento dell'elenco",
|
||||
"Radio could not {action} cleanly": "La Radio non ha potuto {action} correttamente",
|
||||
"Go to expanded Radio section": "Vai alla sezione Radio espansa",
|
||||
"Expand Radio section in Context": "Espandi la sezione Radio in Contesto",
|
||||
"Close compact Radio controls": "Chiudi i controlli Radio compatti",
|
||||
"Open compact Radio controls": "Apri i controlli Radio compatti",
|
||||
"SYNC": "SYNC",
|
||||
"RECONCILE": "RICONCILIA",
|
||||
"DISABLE": "DISATTIVA",
|
||||
@@ -1983,9 +1979,6 @@
|
||||
"selected": "selezionata",
|
||||
"nearest": "più vicina",
|
||||
"{action} {target} radio station": "{action}: stazione radio {target}",
|
||||
"RADIO STATE UNCERTAIN": "STATO RADIO INCERTO",
|
||||
"SYNCING DIRECTORY": "SINCRONIZZAZIONE ELENCO",
|
||||
"RADIO READY": "RADIO PRONTA",
|
||||
"SYNCING": "SYNC IN CORSO",
|
||||
"READY": "PRONTA",
|
||||
"station": "stazione",
|
||||
@@ -2795,11 +2788,6 @@
|
||||
"RECENT IMAGERY": "IMMAGINI RECENTI",
|
||||
"Imagery days in the box": "Giorni con immagini nel riquadro",
|
||||
"CONTEXT": "CONTESTO",
|
||||
"Compact Radio controls": "Controlli Radio compatti",
|
||||
"Open detailed Radio controls": "Apri i controlli Radio dettagliati",
|
||||
"Previous station": "Stazione precedente",
|
||||
"Next station": "Stazione successiva",
|
||||
"Compact Radio volume": "Volume Radio (controlli compatti)",
|
||||
"Expand panel": "Espandi il pannello",
|
||||
"Context mode": "Modalità contesto",
|
||||
"CONTACTS": "CONTATTI",
|
||||
|
||||
@@ -1891,10 +1891,6 @@
|
||||
"STATION UNAVAILABLE": "局を利用できません",
|
||||
"Station unavailable after directory refresh": "ディレクトリ更新後に局を利用できません",
|
||||
"Radio could not {action} cleanly": "ラジオを正常に{action}できませんでした",
|
||||
"Go to expanded Radio section": "展開したラジオセクションへ移動",
|
||||
"Expand Radio section in Context": "コンテキストでラジオセクションを展開",
|
||||
"Close compact Radio controls": "コンパクトラジオ操作を閉じる",
|
||||
"Open compact Radio controls": "コンパクトラジオ操作を開く",
|
||||
"SYNC": "同期",
|
||||
"RECONCILE": "再調整",
|
||||
"DISABLE": "無効化",
|
||||
@@ -1917,9 +1913,6 @@
|
||||
"selected": "選択中の",
|
||||
"nearest": "最寄りの",
|
||||
"{action} {target} radio station": "{target}ラジオ局を{action}",
|
||||
"RADIO STATE UNCERTAIN": "ラジオ状態不明",
|
||||
"SYNCING DIRECTORY": "ディレクトリ同期中",
|
||||
"RADIO READY": "ラジオ準備完了",
|
||||
"SYNCING": "同期中",
|
||||
"READY": "準備完了",
|
||||
"station": "局",
|
||||
@@ -2655,11 +2648,6 @@
|
||||
"RECENT IMAGERY": "最近の衛星画像",
|
||||
"Imagery days in the box": "範囲内の画像のある日",
|
||||
"CONTEXT": "コンテキスト",
|
||||
"Compact Radio controls": "コンパクトラジオ操作",
|
||||
"Open detailed Radio controls": "詳細ラジオ操作を開く",
|
||||
"Previous station": "前の局",
|
||||
"Next station": "次の局",
|
||||
"Compact Radio volume": "コンパクトラジオの音量",
|
||||
"Expand panel": "パネルを展開",
|
||||
"Context mode": "コンテキストモード",
|
||||
"CONTACTS": "コンタクト",
|
||||
|
||||
@@ -1957,10 +1957,6 @@
|
||||
"STATION UNAVAILABLE": "ESTAÇÃO INDISPONÍVEL",
|
||||
"Station unavailable after directory refresh": "Estação indisponível após atualizar o diretório",
|
||||
"Radio could not {action} cleanly": "Não foi possível {action} o rádio corretamente",
|
||||
"Go to expanded Radio section": "Ir para a seção de rádio expandida",
|
||||
"Expand Radio section in Context": "Expandir a seção de rádio em \"Contexto\"",
|
||||
"Close compact Radio controls": "Fechar controles compactos do rádio",
|
||||
"Open compact Radio controls": "Abrir controles compactos do rádio",
|
||||
"SYNC": "SINC",
|
||||
"RECONCILE": "RECONCILIAR",
|
||||
"DISABLE": "DESATIVAR",
|
||||
@@ -1983,9 +1979,6 @@
|
||||
"selected": "selecionada",
|
||||
"nearest": "mais próxima",
|
||||
"{action} {target} radio station": "{action} estação de rádio {target}",
|
||||
"RADIO STATE UNCERTAIN": "ESTADO DO RÁDIO INCERTO",
|
||||
"SYNCING DIRECTORY": "SINCRONIZANDO DIRETÓRIO",
|
||||
"RADIO READY": "RÁDIO PRONTO",
|
||||
"SYNCING": "SINCRONIZANDO",
|
||||
"READY": "PRONTO",
|
||||
"station": "estação",
|
||||
@@ -2797,11 +2790,6 @@
|
||||
"RECENT IMAGERY": "IMAGENS RECENTES",
|
||||
"Imagery days in the box": "Dias com imagens na área",
|
||||
"CONTEXT": "CONTEXTO",
|
||||
"Compact Radio controls": "Controles compactos do rádio",
|
||||
"Open detailed Radio controls": "Abrir controles detalhados do rádio",
|
||||
"Previous station": "Estação anterior",
|
||||
"Next station": "Próxima estação",
|
||||
"Compact Radio volume": "Volume do rádio compacto",
|
||||
"Expand panel": "Expandir painel",
|
||||
"Context mode": "Modo de contexto",
|
||||
"CONTACTS": "CONTATOS",
|
||||
|
||||
@@ -552,8 +552,6 @@
|
||||
"Expand Cockpit display options": "Развернуть параметры экрана кабины",
|
||||
"Collapse Cockpit Radio controls": "Свернуть управление радио кабины",
|
||||
"Expand Cockpit Radio controls": "Развернуть управление радио кабины",
|
||||
"Close compact Radio controls": "Закрыть компактное управление радио",
|
||||
"Open compact Radio controls": "Открыть компактное управление радио",
|
||||
"DIRECTORY BAND": "ДИАПАЗОН КАТАЛОГА",
|
||||
"{category} BAND": "ДИАПАЗОН {category}",
|
||||
"NO STATION SELECTED": "СТАНЦИЯ НЕ ВЫБРАНА",
|
||||
@@ -567,9 +565,6 @@
|
||||
"selected": "выбранную",
|
||||
"nearest": "ближайшую",
|
||||
"{action} {target} radio station": "{action} {target} радиостанцию",
|
||||
"RADIO STATE UNCERTAIN": "СОСТОЯНИЕ РАДИО НЕОПРЕДЕЛЁННО",
|
||||
"SYNCING DIRECTORY": "СИНХРОНИЗАЦИЯ КАТАЛОГА",
|
||||
"RADIO READY": "РАДИО ГОТОВО",
|
||||
"SYNCING": "СИНХРОНИЗАЦИЯ",
|
||||
"READY": "ГОТОВО",
|
||||
"station": "станция",
|
||||
@@ -708,9 +703,6 @@
|
||||
"FLIR": "FLIR",
|
||||
"Style": "Стиль",
|
||||
"STATION SITE": "САЙТ СТАНЦИИ",
|
||||
"Open detailed Radio controls": "Открыть подробное управление радио",
|
||||
"Previous station": "Предыдущая станция",
|
||||
"Next station": "Следующая станция",
|
||||
"Expand panel": "Развернуть панель",
|
||||
"CONTACTS": "КОНТАКТЫ",
|
||||
"Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "Перебирает ближайшие контакты выбранного типа — самолёты, суда, объекты. Спутники отслеживаются отдельно.",
|
||||
@@ -722,13 +714,11 @@
|
||||
"Next filtered station": "Следующая отфильтрованная станция",
|
||||
"STOP": "СТОП",
|
||||
"Stop radio playback": "Остановить воспроизведение радио",
|
||||
"Compact Radio controls": "Компактное управление радио",
|
||||
"Context mode": "Режим контекста",
|
||||
"Contact Context actions": "Действия контекста контакта",
|
||||
"Available Space Missions": "Доступные космические миссии",
|
||||
"LOADING 30-DAY MISSION INDEX": "ЗАГРУЗКА ИНДЕКСА МИССИЙ ЗА 30 ДНЕЙ",
|
||||
"Radio playback": "Воспроизведение радио",
|
||||
"Compact Radio volume": "Громкость компактного радио",
|
||||
"Radio volume": "Громкость радио",
|
||||
"STATION TAG": "ТЕГ СТАНЦИИ",
|
||||
"DRAG TO TUNE": "ТЯНИТЕ ДЛЯ НАСТРОЙКИ",
|
||||
@@ -2482,8 +2472,6 @@
|
||||
"Exit cockpit to fly to a route": "Выйдите из кабины, чтобы перелететь к маршруту",
|
||||
"Exit cockpit to fly to a file": "Выйдите из кабины, чтобы перелететь к файлу",
|
||||
"Exit cockpit to fly to a scene": "Выйдите из кабины, чтобы перелететь к сцене",
|
||||
"Go to expanded Radio section": "Перейти к развёрнутому разделу радио",
|
||||
"Expand Radio section in Context": "Развернуть раздел «Радио» в панели «Контекст»",
|
||||
"New scene name": "Название новой сцены",
|
||||
"Delete scene \"{scene}\" and all shots?": "Удалить сцену «{scene}» и все кадры?",
|
||||
"Detailed outline unavailable": "Подробный контур недоступен",
|
||||
|
||||
@@ -1989,10 +1989,6 @@
|
||||
"STATION UNAVAILABLE": "СТАНЦІЯ НЕДОСТУПНА",
|
||||
"Station unavailable after directory refresh": "Станція недоступна після оновлення каталогу",
|
||||
"Radio could not {action} cleanly": "Радіо: не вдалося коректно {action}",
|
||||
"Go to expanded Radio section": "Перейти до розгорнутого розділу радіо",
|
||||
"Expand Radio section in Context": "Розгорнути розділ «Радіо» на панелі «Контекст»",
|
||||
"Close compact Radio controls": "Закрити компактне керування радіо",
|
||||
"Open compact Radio controls": "Відкрити компактне керування радіо",
|
||||
"SYNC": "СИНХР.",
|
||||
"RECONCILE": "ЗВІРИТИ",
|
||||
"DISABLE": "ВИМКНУТИ",
|
||||
@@ -2015,9 +2011,6 @@
|
||||
"selected": "вибрана",
|
||||
"nearest": "найближча",
|
||||
"{action} {target} radio station": "{action}: {target} радіостанція",
|
||||
"RADIO STATE UNCERTAIN": "СТАН РАДІО НЕВИЗНАЧЕНИЙ",
|
||||
"SYNCING DIRECTORY": "СИНХРОНІЗАЦІЯ КАТАЛОГУ",
|
||||
"RADIO READY": "РАДІО ГОТОВЕ",
|
||||
"SYNCING": "СИНХРОНІЗАЦІЯ",
|
||||
"READY": "ГОТОВО",
|
||||
"station": "станція",
|
||||
@@ -2864,11 +2857,6 @@
|
||||
"RECENT IMAGERY": "СВІЖІ ЗНІМКИ",
|
||||
"Imagery days in the box": "Дні зі знімками в області",
|
||||
"CONTEXT": "КОНТЕКСТ",
|
||||
"Compact Radio controls": "Компактне керування радіо",
|
||||
"Open detailed Radio controls": "Відкрити докладне керування радіо",
|
||||
"Previous station": "Попередня станція",
|
||||
"Next station": "Наступна станція",
|
||||
"Compact Radio volume": "Гучність компактного радіо",
|
||||
"Expand panel": "Розгорнути панель",
|
||||
"Context mode": "Режим контексту",
|
||||
"CONTACTS": "КОНТАКТИ",
|
||||
|
||||
@@ -1861,10 +1861,6 @@
|
||||
"STATION UNAVAILABLE": "电台不可用",
|
||||
"Station unavailable after directory refresh": "目录刷新后该电台不可用",
|
||||
"Radio could not {action} cleanly": "电台未能正常{action}",
|
||||
"Go to expanded Radio section": "前往展开的电台区域",
|
||||
"Expand Radio section in Context": "在态势面板中展开电台区域",
|
||||
"Close compact Radio controls": "关闭精简电台控件",
|
||||
"Open compact Radio controls": "打开精简电台控件",
|
||||
"SYNC": "同步",
|
||||
"RECONCILE": "核对",
|
||||
"DISABLE": "停用",
|
||||
@@ -1886,9 +1882,6 @@
|
||||
"selected": "所选",
|
||||
"nearest": "最近",
|
||||
"{action} {target} radio station": "{action}{target}电台",
|
||||
"RADIO STATE UNCERTAIN": "电台状态不确定",
|
||||
"SYNCING DIRECTORY": "正在同步目录",
|
||||
"RADIO READY": "电台就绪",
|
||||
"SYNCING": "同步中",
|
||||
"READY": "就绪",
|
||||
"station": "电台",
|
||||
@@ -2551,11 +2544,6 @@
|
||||
"RECENT IMAGERY": "近期影像",
|
||||
"Imagery days in the box": "视野内的影像天数",
|
||||
"CONTEXT": "态势",
|
||||
"Compact Radio controls": "精简电台控件",
|
||||
"Open detailed Radio controls": "打开详细电台控件",
|
||||
"Previous station": "上一个电台",
|
||||
"Next station": "下一个电台",
|
||||
"Compact Radio volume": "精简电台音量",
|
||||
"Expand panel": "展开面板",
|
||||
"Context mode": "态势模式",
|
||||
"CONTACTS": "目标",
|
||||
|
||||
@@ -1849,10 +1849,6 @@
|
||||
"STATION UNAVAILABLE": "電臺不可用",
|
||||
"Station unavailable after directory refresh": "目錄刷新後該電臺不可用",
|
||||
"Radio could not {action} cleanly": "電臺未能正常{action}",
|
||||
"Go to expanded Radio section": "前往展開的電臺區域",
|
||||
"Expand Radio section in Context": "在態勢面板中展開電臺區域",
|
||||
"Close compact Radio controls": "關閉精簡電臺控件",
|
||||
"Open compact Radio controls": "打開精簡電臺控件",
|
||||
"SYNC": "同步",
|
||||
"RECONCILE": "核對",
|
||||
"DISABLE": "停用",
|
||||
@@ -1874,9 +1870,6 @@
|
||||
"selected": "所選",
|
||||
"nearest": "最近",
|
||||
"{action} {target} radio station": "{action}{target}電臺",
|
||||
"RADIO STATE UNCERTAIN": "電臺狀態不確定",
|
||||
"SYNCING DIRECTORY": "正在同步目錄",
|
||||
"RADIO READY": "電臺就緒",
|
||||
"SYNCING": "同步中",
|
||||
"READY": "就緒",
|
||||
"station": "電臺",
|
||||
@@ -2539,11 +2532,6 @@
|
||||
"RECENT IMAGERY": "近期影像",
|
||||
"Imagery days in the box": "視野內的影像天數",
|
||||
"CONTEXT": "態勢",
|
||||
"Compact Radio controls": "精簡電臺控件",
|
||||
"Open detailed Radio controls": "打開詳細電臺控件",
|
||||
"Previous station": "上一個電臺",
|
||||
"Next station": "下一個電臺",
|
||||
"Compact Radio volume": "精簡電臺音量",
|
||||
"Expand panel": "展開面板",
|
||||
"Context mode": "態勢模式",
|
||||
"CONTACTS": "目標",
|
||||
|
||||
@@ -91,7 +91,6 @@ test('radio option text and accessible name use the same localized parentheses',
|
||||
setLocale(locale);
|
||||
const host = {
|
||||
_radioPanel: element(), _radioFilter: element(),
|
||||
_syncContextRadioLauncherState() {},
|
||||
actions: { getLifecycle: () => null, scheduleLayout() {} },
|
||||
};
|
||||
renderRadioState.call(host, {
|
||||
|
||||
@@ -166,11 +166,6 @@ test('Cockpit utility Escape leaves an expanded nested Parameters panel to the s
|
||||
/const kind = displayOpen \? 'display' : 'radio';[\s\S]*?escapedFromDisclosure[\s\S]*?returnFocus: !escapedFromDisclosure[\s\S]*?disclosure\?\.blur/,
|
||||
'Cockpit utility disclosures clear their own focus when Escape closes them',
|
||||
);
|
||||
assert.match(
|
||||
radioBindings,
|
||||
/_contextRadioDock\?\.classList\.contains\('disclosure-open'\)[\s\S]*?escapedFromDisclosure[\s\S]*?setRadioDisclosure\(false, \{ returnFocus: !escapedFromDisclosure \}\)[\s\S]*?_contextRadioToggleBtn\?\.blur/,
|
||||
'compact Radio disclosure clears its own focus when Escape closes it',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
|
||||
+10
-57
@@ -263,7 +263,7 @@ test('no unchanged Realtime tool definition drifts silently', () => {
|
||||
});
|
||||
|
||||
test('Radio volume and mission speed share the Sharpen slider visual language', () => {
|
||||
for (const id of ['cockpit-radio-volume', 'context-radio-mini-volume', 'radio-volume', 'sdr-volume']) {
|
||||
for (const id of ['cockpit-radio-volume', 'radio-volume', 'sdr-volume']) {
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(`id="${id}"[^>]*class="gev-quantitative-slider"[^>]*type="range"`),
|
||||
@@ -283,22 +283,17 @@ test('Radio volume and mission speed share the Sharpen slider visual language',
|
||||
assert.doesNotMatch(css, /#space-mission-panel \[data-mission-replay-speed\]::-webkit-slider-thumb/);
|
||||
});
|
||||
|
||||
test('Radio is nested inside Context with separate disclosure and power controls', () => {
|
||||
test('Radio is nested inside Context with its own disclosure and power controls', () => {
|
||||
const contextStart = html.indexOf('id="global-context-panel"');
|
||||
const radioStart = html.indexOf('id="radio-panel"');
|
||||
const contextEnd = html.indexOf('\n </aside>', contextStart);
|
||||
assert.ok(contextStart >= 0 && radioStart > contextStart && radioStart < contextEnd);
|
||||
assert.match(html, /id="radio-panel"[^>]*data-panel-id="radio-panel"/);
|
||||
assert.match(html, /aria-label="Radio playback"/);
|
||||
assert.match(html, /id="context-radio-toggle-btn"[^>]*aria-expanded="false"[^>]*aria-controls="context-radio-mini"/);
|
||||
assert.doesNotMatch(html, /id="context-radio-toggle-btn"[^>]*aria-pressed=/);
|
||||
assert.match(html, /id="context-radio-mini"[^>]*aria-label="Compact Radio controls"[^>]*hidden/);
|
||||
assert.match(html, /id="context-radio-mini-enable-btn"[^>]*aria-pressed="false"/);
|
||||
assert.match(html, /id="context-radio-details-btn"[^>]*aria-expanded="false"[^>]*aria-controls="radio-panel"/);
|
||||
assert.match(html, /id="context-radio-details-btn"[\s\S]*?<span class="material-symbols-outlined" aria-hidden="true">open_in_full<\/span>/);
|
||||
assert.match(html, /id="context-radio-mini-close-btn"[^>]*aria-label="Close compact Radio controls"/);
|
||||
assert.match(html, /id="context-radio-mini-(?:prev|play|next)-btn"/);
|
||||
assert.match(html, /id="context-radio-mini-volume"/);
|
||||
// The Context header carries no Radio shortcut; Radio lives in its own
|
||||
// section below the modes, with one power control.
|
||||
assert.doesNotMatch(html, /context-radio-/);
|
||||
assert.match(html, /id="radio-enable-btn"[^>]*aria-pressed="false"/);
|
||||
assert.match(html, /id="cockpit-radio-panel"[^>]*aria-label="Cockpit compact Radio controls"[^>]*hidden/);
|
||||
assert.match(html, /id="cockpit-radio-enable-btn"[^>]*aria-pressed="false"/);
|
||||
assert.match(html, /id="cockpit-radio-(?:prev|play|next)-btn"/);
|
||||
@@ -343,8 +338,7 @@ test('Radio is nested inside Context with separate disclosure and power controls
|
||||
const baseCss = css.replace(readFileSync(new URL('./ui/styles/cyber.css', import.meta.url), 'utf8'), '');
|
||||
assert.doesNotMatch(baseCss, /#right-context-rail\s*>\s*#radio-panel/);
|
||||
assert.match(css, /#global-context-panel #radio-panel\.collapsed/);
|
||||
assert.doesNotMatch(css, /\.context-radio-dock\.active:hover \.context-radio-mini/);
|
||||
assert.doesNotMatch(css, /\.context-radio-dock\.active:focus-within \.context-radio-mini/);
|
||||
assert.doesNotMatch(css, /\.context-radio-/);
|
||||
assert.match(css, /#right-context-rail #global-context-panel:not\(\.collapsed\) \.context-mode-view,[\s\S]*?#right-context-rail #global-context-panel:not\(\.collapsed\) #radio-panel\s*\{[\s\S]*?flex: 0 0 auto;/);
|
||||
});
|
||||
|
||||
@@ -362,59 +356,18 @@ test('panel collapse is presentation-only and Radio exposes explicit voice playb
|
||||
assert.deepEqual(realtimeTools().find(tool => tool.name === 'control_radio').parameters.properties.action.enum, ['enable', 'disable', 'play', 'resume', 'pause', 'stop', 'next', 'previous', 'volume', 'select', 'status']);
|
||||
const enableMethod = radioBindings;
|
||||
assert.doesNotMatch(enableMethod, /playSelectedRadio|togglePlayback\(\).*radio-enable/i);
|
||||
assert.match(enableMethod, /contextRadioToggleBtn/);
|
||||
assert.match(enableMethod, /contextRadioMiniEnableBtn/);
|
||||
assert.match(enableMethod, /contextRadioDetailsBtn/);
|
||||
assert.match(enableMethod, /contextRadioMiniCloseBtn/);
|
||||
assert.match(enableMethod, /contextRadioMiniPlayBtn/);
|
||||
const disclosureStart = enableMethod.indexOf('this.listen(this._contextRadioToggleBtn,');
|
||||
const disclosureEnd = enableMethod.indexOf("this.listen(this._radioFilter,", disclosureStart);
|
||||
const disclosureBindings = enableMethod.slice(disclosureStart, disclosureEnd);
|
||||
assert.ok(disclosureStart >= 0 && disclosureEnd > disclosureStart, 'compact Radio disclosure bindings are missing');
|
||||
assert.doesNotMatch(disclosureBindings, /toggleRadio\(this\._contextRadioToggleBtn\)/);
|
||||
assert.match(enableMethod, /toggleRadio\(this\._contextRadioMiniEnableBtn\)/);
|
||||
assert.match(disclosureBindings, /contextRadioMiniCloseBtn[\s\S]*?setRadioDisclosure\(false, \{ returnFocus: true \}\)/);
|
||||
assert.match(disclosureBindings, /contextRadioDetailsBtn[\s\S]*?setPanelCollapsed\('radio-panel', false, \{ explicit: true \}\)/);
|
||||
assert.match(
|
||||
disclosureBindings,
|
||||
/contextRadioToggleBtn[\s\S]*?!contextPanel\.classList\.contains\('collapsed'\)[\s\S]*?setRadioDisclosure\(false\)[\s\S]*?setPanelCollapsed\('radio-panel', false, \{ explicit: true \}\)[\s\S]*?_revealRadioPanelInsideContext/,
|
||||
);
|
||||
assert.doesNotMatch(disclosureBindings, /setPanelCollapsed\('radio-panel', !/);
|
||||
assert.doesNotMatch(enableMethod, /contextRadio/);
|
||||
assert.match(enableMethod, /toggleRadio\(this\._radioEnableBtn\)/);
|
||||
assert.match(ui, /setAttribute\('aria-expanded', String\(/);
|
||||
assert.match(readFileSync(new URL('./ui/shellFeedback.js', import.meta.url), 'utf8'), /\.hidden = !/);
|
||||
assert.doesNotMatch(method, /panelId === 'global-context-panel'[\s\S]*?this\._radioState\?\.enabled[\s\S]*?setPanelCollapsed\('radio-panel', false\)/);
|
||||
});
|
||||
|
||||
test('expanded Context routes its Radio icon to the embedded section and keeps collapsed Context compact', () => {
|
||||
const revealStart = radioControlsSource.indexOf('\n async _revealRadioPanelInsideContext');
|
||||
const revealEnd = radioControlsSource.indexOf('\n /**', revealStart + 10);
|
||||
const revealMethod = radioControlsSource.slice(revealStart, revealEnd);
|
||||
assert.ok(revealStart >= 0 && revealEnd > revealStart, 'embedded Radio reveal helper is missing');
|
||||
assert.match(revealMethod, /requestAnimationFrame\(\(\) => requestAnimationFrame/);
|
||||
assert.match(revealMethod, /scroller\.scrollTo\(\{\s*top: next,\s*behavior: reducedMotion \? 'auto' : 'smooth',?\s*\}\)/);
|
||||
assert.match(revealMethod, /focus\?\.\(\{ preventScroll: true \}\)/);
|
||||
assert.doesNotMatch(revealMethod, /setEnabled|togglePlayback|selectStation|setContextMode/);
|
||||
|
||||
const syncStart = radioControlsSource.indexOf('\n _syncContextRadioLauncherState()');
|
||||
const syncEnd = radioControlsSource.indexOf('\n destroy()', syncStart + 10);
|
||||
const syncMethod = radioControlsSource.slice(syncStart, syncEnd);
|
||||
assert.ok(syncStart >= 0 && syncEnd > syncStart, 'Context Radio launcher state sync is missing');
|
||||
assert.match(syncMethod, /contextExpanded[\s\S]*?aria-controls', 'radio-panel'[\s\S]*?aria-expanded',\s*String\(radioExpanded\)/);
|
||||
assert.match(syncMethod, /aria-controls',\s*'context-radio-mini'[\s\S]*?aria-expanded',\s*String\(compactOpen\)/);
|
||||
const renderMethod = radioPresentation;
|
||||
assert.match(renderMethod, /this\._syncContextRadioLauncherState\(\)/);
|
||||
assert.doesNotMatch(renderMethod, /compact Radio controls/);
|
||||
assert.doesNotMatch(renderMethod, /_contextRadioToggleBtn\.setAttribute\('aria-(?:controls|expanded|label)'/);
|
||||
});
|
||||
|
||||
test('Radio disclosure is explicit, starts closed while off, and preserves playback state', () => {
|
||||
const renderMethod = radioPresentation;
|
||||
assert.doesNotMatch(renderMethod, /setPanelCollapsed\('radio-panel', true\).*stopPlayback/s);
|
||||
assert.doesNotMatch(renderMethod, /_radioMiniExpanded\s*=\s*false.*audioState === 'playing'/s);
|
||||
assert.match(ui, /contextRadioDetailsBtn/);
|
||||
const syncStart = ui.indexOf('\n _syncPanelCollapseButton(panelEl)');
|
||||
const syncMethod = ui.slice(syncStart, ui.indexOf('\n }', syncStart + 10));
|
||||
assert.doesNotMatch(syncMethod, /contextRadioDetailsBtn[\s\S]*?(?:aria-label|textContent|\.title)/);
|
||||
assert.doesNotMatch(renderMethod, /_syncContextRadioLauncherState/);
|
||||
});
|
||||
|
||||
test('successful explicit user playback hands the speaker from voice to Radio', () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]')
|
||||
)
|
||||
|
||||
@@ -768,6 +768,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
-40
@@ -71,8 +71,7 @@ export class PanelChrome {
|
||||
variant: this.hud.getVariant(),
|
||||
}),
|
||||
scheduleCockpitLayout: () => this.cockpitView?.scheduleContextLayout(),
|
||||
syncPanelCollapseButton: (panel) =>
|
||||
this._syncRailPanelCollapseButton(panel),
|
||||
syncPanelCollapseButton: (panel) => this._syncPanelCollapseButton(panel),
|
||||
readDisplayScrollTop: () =>
|
||||
this._displayPortalScrollRestoreOwner === 'standard'
|
||||
? this._standardDisplayScrollTop
|
||||
@@ -292,19 +291,6 @@ export class PanelChrome {
|
||||
return this._panelLayout._scheduleLeftPanelLayout(options0);
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
|
||||
_syncPanelCollapseButton(panelEl) {
|
||||
const isRightRail = [
|
||||
'pp-toggles',
|
||||
@@ -365,15 +351,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 +494,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 +522,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' &&
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
@@ -654,12 +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',
|
||||
'global-context-panel',
|
||||
]) {
|
||||
for (const panel of ['data-panel', 'scene-panel', 'cctv-panel']) {
|
||||
assert.match(
|
||||
css,
|
||||
new RegExp(`#${panel} \\.panel-glow \\{[^}]*\\binset: -\\d+px;`),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -22,7 +22,6 @@ function render(audioError) {
|
||||
destroyed: false,
|
||||
_radioPanel: el(),
|
||||
_radioPlaybackState: el(),
|
||||
_syncContextRadioLauncherState() {},
|
||||
actions: {
|
||||
getLifecycle: () => null,
|
||||
preservePanelStateDuringClear: () => false,
|
||||
|
||||
@@ -27,7 +27,6 @@ for (const lifecycleState of ['enabling', 'disabling']) {
|
||||
destroyed: false,
|
||||
_radioPanel: el(),
|
||||
_radioEnableBtn: enable,
|
||||
_syncContextRadioLauncherState() {},
|
||||
actions: {
|
||||
getLifecycle: () => ({
|
||||
lifecycleState,
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ test('weather rail orders between CCTV and Context, observes and syncs chrome',
|
||||
const weather = f.nodes.get('weather-panel');
|
||||
stack.children = [weather, f.nodes.get('global-context-panel')];
|
||||
const synced = [];
|
||||
const chrome = { _syncContextRadioLauncherState() {} };
|
||||
const chrome = {};
|
||||
const owner = new PanelLayoutController({
|
||||
readHud: () => ({}),
|
||||
scheduleCockpitLayout() {},
|
||||
|
||||
@@ -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',
|
||||
|
||||
+14
-2
@@ -1441,6 +1441,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);
|
||||
@@ -1788,8 +1797,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(
|
||||
|
||||
+43
-197
@@ -985,17 +985,16 @@
|
||||
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;
|
||||
}
|
||||
/* 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 {
|
||||
position: relative;
|
||||
@@ -1012,158 +1011,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 +1076,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 +1123,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 +1135,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 +1189,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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
under the expanded panel plus a vertical scrollbar that scrolls nothing
|
||||
but glow. Pin the glow to the panel box here — its blur is ink overflow,
|
||||
which never scrolls — instead of clipping the rail, which would also clip
|
||||
legitimate content such as the Context radio popover. */
|
||||
legitimate content that extends past a panel's edge. */
|
||||
#left-panel-stack .panel-glow,
|
||||
#right-context-rail .panel-glow {
|
||||
inset: 0;
|
||||
@@ -215,10 +215,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,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>
|
||||
|
||||
Reference in new issue
Block a user