fix(ui): floating windows stack above the docked rail panels

A floating window restored after a reload had no z-index, and the z ladder
renumbered from 101, so the docked DISPLAY panel (#pp-toggles, z 110) painted
over it. The ladder now starts at 111 (PANEL_Z_FLOATING_FLOOR), a restored
window is promoted like a lifted one, docking drops the promotion, and
floating panels get a CSS floor of 111. Tests fail on the old code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-09-26 00:58:43 +01:00
1 parent e6c2c812f9
commit f7456f2436
3 files changed
+70 -1

No files matched your search

+54
View File
@@ -15,7 +15,9 @@ function fixture() {
const classes = new Set();
const style = {
removeProperty(name) {
// Like CSSStyleDeclaration: 'z-index' also clears style.zIndex.
delete this[name];
delete this[name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())];
},
getPropertyValue: () => '',
};
@@ -191,3 +193,55 @@ test('two quick header presses snap a floating panel back; slow or distant ones
f.restore();
}
});
test('a restored floating window stacks above the docked DISPLAY panel (z 110)', () => {
const f = fixture();
try {
const panel = f.element();
panel.id = 'street-level-panel';
f.owner._portablePanels.set('street-level-panel', {
panel,
min: { width: 320, height: 280 },
dockOnCollapse: true,
});
f.store.set(
'godsEyeView.v8.panelPos.street-level-panel',
JSON.stringify({
left: 900,
top: 200,
width: 420,
height: 600,
floating: true,
}),
);
f.owner._restorePanelPosition('street-level-panel', panel);
assert.equal(panel.classList.contains('panel-floating'), true);
assert.ok(Number(panel.style.zIndex) > 110, `z ${panel.style.zIndex}`);
f.owner.dockPanel('street-level-panel');
assert.equal(panel.style.zIndex, undefined, 'docking drops the promotion');
} finally {
f.restore();
}
});
test('renumbering the z ladder never drops a window below the docked panels', () => {
const f = fixture();
try {
const windows = [f.element(), f.element(), f.element()];
windows.forEach((node, index) => {
node.classList.add('panel-draggable');
node.style.zIndex = String(130 + index);
});
globalThis.document.querySelectorAll = (selector) =>
selector === '.panel-draggable' ? windows : [];
f.owner._panelZCounter = 139;
const top = f.element();
top.classList.add('panel-draggable');
f.owner._promotePanelZ(top);
for (const node of [...windows, top])
assert.ok(Number(node.style.zIndex) > 110, `z ${node.style.zIndex}`);
assert.ok(Number(top.style.zIndex) > Number(windows[2].style.zIndex));
} finally {
f.restore();
}
});
+13 -1
View File
@@ -12,6 +12,13 @@ const PANEL_POSITION_STORAGE_VERSION = 'v8';
/** Z ladder: panels promote within [100, 139]; voice pill 150, toast 200, clean-view-exit 300. */
const PANEL_Z_BASE = 100;
const PANEL_Z_MAX = 139;
/**
* Lowest z a promoted (dragged or floating) panel may take: above every panel
* docked in a rail, the highest of which is #pp-toggles at 110 (controls.css).
* Renumbering restarts here, so a floating window never slips under DISPLAY.
* Keep in step with `#right-context-rail > .panel-floating` in controls.css.
*/
const PANEL_Z_FLOATING_FLOOR = PANEL_Z_BASE + 11;
/** Pointer travel before a header press becomes a drag that lifts a portable panel out. */
const DRAG_THRESHOLD_PX = 4;
/**
@@ -37,6 +44,8 @@ const FLOATING_STYLE_PROPERTIES = [
'bottom',
'width',
'height',
// A docked panel takes the rail's stacking, not a floating window's.
'z-index',
];
function clamp(value, min, max) {
@@ -264,6 +273,9 @@ export class PanelPositionControls {
if (Boolean(pos.floating) !== Boolean(portable)) return;
if (portable) {
panelEl.classList.add('panel-floating', 'panel-draggable');
// A restored window must stack above the docked rail panels too, not
// only one that was lifted or clicked this session.
this._promotePanelZ(panelEl);
if (Number.isFinite(pos.width)) {
panelEl.style.width = `${clamp(
Math.round(pos.width),
@@ -337,7 +349,7 @@ export class PanelPositionControls {
const promoted = [...document.querySelectorAll('.panel-draggable')]
.filter((el) => el.style.zIndex)
.sort((a, b) => Number(a.style.zIndex) - Number(b.style.zIndex));
let z = PANEL_Z_BASE + 1;
let z = PANEL_Z_FLOATING_FLOOR;
for (const el of promoted) {
el.style.zIndex = String(z);
z += 1;
+3
View File
@@ -117,6 +117,9 @@
#right-context-rail > .panel-floating {
position: fixed;
inset: auto;
/* Above every docked rail panel (#pp-toggles is 110). PanelPositionControls
promotes floating windows from this floor (PANEL_Z_FLOATING_FLOOR). */
z-index: 111;
max-height: calc(100vh - 12px);
margin: 0;
flex: none;