From 06994e0d0e0596734ae99feb16ad8bc6891faf72 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 1 Nov 2026 16:58:37 -0800 Subject: [PATCH] Keep the panel rails still when a tall panel opens A tall panel moves its stack to the focus or tail layout, and the rail inside the stack went with it, so the Data Layers and Authoring buttons jumped up and the right rail followed. Each layout pass now offsets the rail back to the resting top, with the same transition as the stack, and the right rail lines up with the left rail's buttons. Cyber pins the left stack's top, so it flags that and the rail takes no offset. --- src/ui/leftPanelRail.js | 11 +++++++++++ src/ui/panelRails.test.mjs | 26 ++++++++++++++++++++++++++ src/ui/rightPanelRail.js | 5 ++++- src/ui/styles/cyber.css | 2 ++ src/ui/styles/layers.css | 7 +++++-- 5 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/ui/leftPanelRail.js b/src/ui/leftPanelRail.js index 3df30677d..ca11a8149 100644 --- a/src/ui/leftPanelRail.js +++ b/src/ui/leftPanelRail.js @@ -59,6 +59,7 @@ export function layoutLeftPanelRail({ stack.style.removeProperty('--left-stack-safe-top'); stack.style.removeProperty('--left-stack-safe-bottom'); stack.style.removeProperty('--left-stack-centered-height'); + stack.style.removeProperty('--panel-rail-offset'); stack.classList.remove('layout-exclusive'); stack.dataset.layoutMode = 'mobile'; settleSideAccordion({ panels, enabled: false, onCollapse }); @@ -275,6 +276,16 @@ export function layoutLeftPanelRail({ ) { stack.style.setProperty('--left-stack-safe-bottom', bottomValue); } + // The rail stays where the resting stack puts it; only the panels move. + // A theme that pins the stack's top (Cyber) leaves nothing to offset. + const topPinned = + getComputedStyle(stack) + .getPropertyValue?.('--panel-stack-top-pinned') + ?.trim() === '1'; + const railOffset = `${(topPinned ? 0 : safeTop - layoutTop).toFixed(1)}px`; + if (stack.style.getPropertyValue('--panel-rail-offset') !== railOffset) { + stack.style.setProperty('--panel-rail-offset', railOffset); + } stack.style.removeProperty('--left-stack-centered-height'); for (const panel of panels) panel.style.removeProperty('--left-panel-allocated-height'); diff --git a/src/ui/panelRails.test.mjs b/src/ui/panelRails.test.mjs index 5650feb44..801fd02f4 100644 --- a/src/ui/panelRails.test.mjs +++ b/src/ui/panelRails.test.mjs @@ -349,6 +349,32 @@ test('right rail aligns to the current left rail and excludes hidden obstacles', assert.equal(f.stack.dataset.safeTop, '200.0'); }); +// An opened panel moves the left stack up, but its rail buttons hold still: +// the right rail lines up with those buttons, not with the moved stack. +test('right rail aligns to the left rail buttons while the left stack is offset', () => { + const f = fixture('right'); + f.options.leftStack.rect.top = 100; + f.options.leftStack.style.setProperty('--panel-rail-offset', '186.7px'); + f.run(); + assert.equal(f.stack.dataset.safeTop, '286.7'); +}); + +test('left rail buttons hold still while a tall panel moves the stack', () => { + const f = fixture('left'); + f.expand(f.first, 900); + f.run(); + assert.equal(f.stack.dataset.layoutMode, 'focus'); + assert.ok( + parseFloat(f.stack.style.getPropertyValue('--panel-rail-offset')) > 0, + ); + + // A theme that pins the stack's top (Cyber) leaves nothing to offset. + f.stack.computed.getPropertyValue = (name) => + name === '--panel-stack-top-pinned' ? ' 1' : ''; + f.run(); + assert.equal(f.stack.style.getPropertyValue('--panel-rail-offset'), '0.0px'); +}); + // The left rail's `top` animates after the right pass runs, and nothing // re-runs it when the transition lands: the right rail must read the end value. function animatingLeftRail({ keyframes = true } = {}) { diff --git a/src/ui/rightPanelRail.js b/src/ui/rightPanelRail.js index 40a80f3ba..44043fb01 100644 --- a/src/ui/rightPanelRail.js +++ b/src/ui/rightPanelRail.js @@ -126,8 +126,11 @@ export function layoutRightPanelRail({ viewportHeight, getComputedStyle, ); + // Align with the left rail's buttons, which hold still while its stack moves. + const leftRailOffset = + parseFloat(leftStack?.style.getPropertyValue('--panel-rail-offset')) || 0; const alignedTop = Number.isFinite(leftStackTop) - ? leftStackTop + ? leftStackTop + leftRailOffset : viewportHeight * 0.26; const obstacleRects = []; diff --git a/src/ui/styles/cyber.css b/src/ui/styles/cyber.css index 122357e97..4f5b4f9f9 100644 --- a/src/ui/styles/cyber.css +++ b/src/ui/styles/cyber.css @@ -1654,6 +1654,8 @@ } :root[data-ui-theme='cyber'] #left-panel-stack { + /* The stack holds still, so its rail needs no offset (leftPanelRail.js). */ + --panel-stack-top-pinned: 1; top: var(--cyber-left-rail-top) !important; bottom: calc(2vh + 13rem); left: calc(54px + var(--panel-rail-clearance)); diff --git a/src/ui/styles/layers.css b/src/ui/styles/layers.css index 90915c6b3..3074429ce 100644 --- a/src/ui/styles/layers.css +++ b/src/ui/styles/layers.css @@ -245,12 +245,15 @@ } /* Panel rails (src/ui/panelNavigation.js). Each rail lives inside its stack, - so it shows and hides with the stack, and floats in the screen-edge gutter. */ + so it shows and hides with the stack, and floats in the screen-edge gutter. + The stack's layout offsets it back to its resting top, so it holds still + when an opened panel moves the stack. */ .gev-panel-rail { position: absolute; - top: 0; + top: var(--panel-rail-offset, 0px); z-index: 1; pointer-events: auto; + transition: top var(--transition-fast); } .gev-panel-rail--left { right: calc(100% + 6px);