From a4ce7381d88f7da849d80010fe1d8979457b754c Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:32:50 -0500 Subject: [PATCH 1/5] fix(ui): enforce Context ownership before disclosure no-op --- src/ui/panelChrome.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/ui/panelChrome.js b/src/ui/panelChrome.js index 30266312a..6464f1588 100644 --- a/src/ui/panelChrome.js +++ b/src/ui/panelChrome.js @@ -474,6 +474,17 @@ export class PanelChrome { syncShare, }); } + // Parent ownership is an invariant even on a no-op disclosure request. + // A stale compact Radio disclosure is absolutely positioned outside the + // measured Context rail; clear it before returning from an already-open + // Context request so it cannot paint over the active Context body. + if ( + !nextCollapsed && + panelId === 'global-context-panel' && + this._contextRadioDock?.classList.contains('disclosure-open') + ) { + this._setRadioDisclosure?.(false); + } if ( panelEl.classList.contains('collapsed') === nextCollapsed && !wasAutoCollapsed @@ -502,13 +513,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' && From b79ab84a1a5efaffea6eb2540142b22c34a67ca2 Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:33:09 -0500 Subject: [PATCH 2/5] test(ui): pin Context and compact Radio ownership --- src/ui/panelChrome.radioOwnership.test.mjs | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/ui/panelChrome.radioOwnership.test.mjs diff --git a/src/ui/panelChrome.radioOwnership.test.mjs b/src/ui/panelChrome.radioOwnership.test.mjs new file mode 100644 index 000000000..c52a4888e --- /dev/null +++ b/src/ui/panelChrome.radioOwnership.test.mjs @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { PanelChrome } from './panelChrome.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); +}); From b22e6d519a0387b846a71a3069dc8c66797bf52d Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:33:11 -0500 Subject: [PATCH 3/5] chore(format): adopt panel ownership test --- scripts/format-scope.json | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/format-scope.json b/scripts/format-scope.json index ef44df03f..507ee3126 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -651,6 +651,7 @@ "src/ui/visualSettings.js", "src/ui/panelChrome.js", "src/testSupport/readShellSource.mjs", + "src/ui/panelChrome.radioOwnership.test.mjs", "src/ui/stateOwners.test.mjs", "src/layers/flights/records.js", "src/layers/flights/recordPolicy.js", From d9e2c8a7e744c3fff55f8e28b097a4c922fd4fac Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:38:19 -0500 Subject: [PATCH 4/5] fix(ui): enforce compact Radio ownership on right-rail restores Close the overlay when adaptive layout restores an auto-collapsed Context directly. Regression covers the reachable rail reflow path; changelog credits the original report by raysung-2026 (#804). --- CHANGELOG.md | 5 ++ src/ui/panelChrome.js | 16 ++++- src/ui/panelChrome.radioOwnership.test.mjs | 69 ++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0c5f754..802407a41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +- Prevent a compact Radio disclosure from covering Space Missions when the + adaptive right rail restores Context without calling the panel owner. + Repeated Context-open calls and rail-driven expansion both close the mini, + with a regression for auto-collapse restoration (raysung-2026, #804). + - CCTV cameras whose bearing is a guess now say so. Packs mark bearings derived from a hash of the camera id as `headingConfidence: 'low'`, but nothing read the flag, so roughly 70% of a default catalog rendered like surveyed facings. The HUD diff --git a/src/ui/panelChrome.js b/src/ui/panelChrome.js index 6464f1588..0717a1313 100644 --- a/src/ui/panelChrome.js +++ b/src/ui/panelChrome.js @@ -68,7 +68,8 @@ export class PanelChrome { variant: this.hud.getVariant(), }), scheduleCockpitLayout: () => this.cockpitView?.scheduleContextLayout(), - syncPanelCollapseButton: (panel) => this._syncPanelCollapseButton(panel), + syncPanelCollapseButton: (panel) => + this._syncRailPanelCollapseButton(panel), readDisplayScrollTop: () => this._displayPortalScrollRestoreOwner === 'standard' ? this._standardDisplayScrollTop @@ -282,6 +283,19 @@ 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', diff --git a/src/ui/panelChrome.radioOwnership.test.mjs b/src/ui/panelChrome.radioOwnership.test.mjs index c52a4888e..1ecc1c7b9 100644 --- a/src/ui/panelChrome.radioOwnership.test.mjs +++ b/src/ui/panelChrome.radioOwnership.test.mjs @@ -2,6 +2,7 @@ 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); @@ -65,3 +66,71 @@ test('opening an already-open Context still closes a stale compact Radio disclos 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', + ]); +}); From a29d5a687445896411d95f3942bd912cbd4a02c1 Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Mon, 19 Oct 2026 14:46:30 +0100 Subject: [PATCH 5/5] docs(changelog): credit kvnloo in the #816 fragment Co-Authored-By: Claude Opus 5.5 --- changelog.d/816-radio-owner-restoration.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.d/816-radio-owner-restoration.md b/changelog.d/816-radio-owner-restoration.md index 3e441f31a..df38617ee 100644 --- a/changelog.d/816-radio-owner-restoration.md +++ b/changelog.d/816-radio-owner-restoration.md @@ -2,5 +2,6 @@ restore directly expands Context, before Space Missions can be covered. A repeated Context-open request also clears stale disclosure state. Real PanelChrome constructor wiring and the rail reflow path are covered - by focused regressions. Credits: raysung-2026's original #804 report, - asturksever's review, and quanticsoul4772's independent Windows diagnosis. + by focused regressions. Credits: kvnloo (#816), raysung-2026's original + #804 report, asturksever's review, and quanticsoul4772's independent + Windows diagnosis.