From b090937e5fda51e91461a4d6d0a0228c7cd162df Mon Sep 17 00:00:00 2001 From: Manjunath Paparaju Date: Fri, 13 Nov 2026 10:51:01 +0530 Subject: [PATCH] Keep rail icons in place and move only the highlight The left rail no longer lifts its open icon out beside the panel title or reorders the rest behind it. Every icon keeps its place, open or not, and the rail's shared highlight slides to the open one, on both rails. The rail also holds one height whichever panel is open: its offset now allows for the stack rising by a different amount for a tall panel than at rest. Co-authored-by: Sameh Khamis --- src/ui/leftPanelRail.js | 7 +++-- src/ui/panelRails.test.mjs | 17 +++++++++--- src/ui/railEdgeScroll.js | 16 ----------- src/ui/styles/panel-edge.css | 10 ------- src/ui/styles/rail-split.css | 52 +++++++++--------------------------- 5 files changed, 30 insertions(+), 72 deletions(-) diff --git a/src/ui/leftPanelRail.js b/src/ui/leftPanelRail.js index 0b3e7ee2f..c5c65378b 100644 --- a/src/ui/leftPanelRail.js +++ b/src/ui/leftPanelRail.js @@ -283,13 +283,16 @@ export function layoutLeftPanelRail({ getComputedStyle(stack) .getPropertyValue?.('--panel-stack-top-pinned') ?.trim() === '1'; - const railOffset = `${(topPinned ? 0 : safeTop - layoutTop).toFixed(1)}px`; + // The stack itself rises by the lift below, by a different amount for a + // tall panel than at rest; the rail takes up the difference. + const lift = (top) => Math.min(0, obstacleSafeTop - top); + const railOffset = `${(topPinned ? 0 : safeTop + lift(safeTop) - layoutTop - lift(layoutTop)).toFixed(1)}px`; if (stack.style.getPropertyValue('--panel-rail-offset') !== railOffset) { stack.style.setProperty('--panel-rail-offset', railOffset); } // How far the column can rise before it meets the HUD readout, so the rail // and any open panel sit just under it, whether or not a panel is open. - const railLift = `${(topPinned ? 0 : Math.min(0, obstacleSafeTop - layoutTop)).toFixed(1)}px`; + const railLift = `${(topPinned ? 0 : lift(layoutTop)).toFixed(1)}px`; if (stack.style.getPropertyValue('--panel-rail-lift') !== railLift) { stack.style.setProperty('--panel-rail-lift', railLift); } diff --git a/src/ui/panelRails.test.mjs b/src/ui/panelRails.test.mjs index 40c2be20b..7edbc854a 100644 --- a/src/ui/panelRails.test.mjs +++ b/src/ui/panelRails.test.mjs @@ -359,14 +359,23 @@ test('right rail aligns to the left rail buttons while the left stack is offset' assert.equal(f.stack.dataset.safeTop, '286.7'); }); -test('left rail buttons hold still while a tall panel moves the stack', () => { +test('left rail buttons hold still whether the open panel is short or tall', () => { + // Where the rail sits: the stack's top, the stack's rise, and the rail's + // own offset inside the stack. + const railTop = (stack) => + (parseFloat(stack.style.getPropertyValue('--left-stack-safe-top')) * 900) / + 100 + + parseFloat(stack.style.getPropertyValue('--panel-rail-lift')) + + parseFloat(stack.style.getPropertyValue('--panel-rail-offset')); + const short = fixture('left'); + short.expand(short.first, 250); + short.run(); 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, - ); + assert.notEqual(short.stack.dataset.layoutMode, 'focus'); + assert.ok(Math.abs(railTop(f.stack) - railTop(short.stack)) < 0.2); // A theme that pins the stack's top (Cyber) leaves nothing to offset. f.stack.computed.getPropertyValue = (name) => diff --git a/src/ui/railEdgeScroll.js b/src/ui/railEdgeScroll.js index 4dfd56df6..cf46ef02f 100644 --- a/src/ui/railEdgeScroll.js +++ b/src/ui/railEdgeScroll.js @@ -62,24 +62,9 @@ export function installRailEdgeScroll(doc = globalThis.document) { // highlight is showing is kept here, and the classes are put back each time. const gliding = new WeakSet(); - // Where a rail lifts its open item out to sit beside the panel's title, the - // items left in the rail follow on from it in turn, so the next one in line - // is the one directly under it and a step brings that one up. A rail that - // keeps its open item in place keeps its own order. - function reorder(rail, on) { - const slots = [...rail.children].filter((slot) => slot.querySelector?.('.ui-rail__item')); - const at = on ? slots.findIndex((slot) => slot.contains(on)) : -1; - const lifted = at >= 0 && win.getComputedStyle(slots[at]).position === 'absolute'; - slots.forEach((slot, index) => { - const order = lifted ? String((index - at + slots.length) % slots.length) : ''; - if (slot.style.order !== order) slot.style.order = order; - }); - } - function glide(rail) { rail.classList.toggle('ui-rail--edge-hot', rail === hot); const on = rail.querySelector('.ui-rail__item[aria-pressed="true"]'); - reorder(rail, on); if (!on?.offsetWidth) { gliding.delete(rail); rail.classList.remove('ui-rail--glide'); @@ -263,7 +248,6 @@ export function installRailEdgeScroll(doc = globalThis.document) { win.removeEventListener('resize', onResize); doc.querySelectorAll('.ui-rail').forEach((rail) => { rail.classList.remove('ui-rail--glide', 'ui-rail--glide-jump', 'ui-rail--edge-hot'); - reorder(rail, null); }); }; } diff --git a/src/ui/styles/panel-edge.css b/src/ui/styles/panel-edge.css index c9cfa4faf..79d19b6d2 100644 --- a/src/ui/styles/panel-edge.css +++ b/src/ui/styles/panel-edge.css @@ -71,16 +71,6 @@ body.panels-edge-right:not(.context-docked) .gev-panel-rail--right .ui-rail--col gap: 4px; } -/* The open panel's icon joins the row instead of floating above it. */ -body.panels-edge-left .gev-panel-rail--left .ui-rail--collapsible:not(.ui-rail--expanded) .ui-tooltip { - position: static !important; - padding: 0 !important; - border: 0 !important; - background: none !important; - backdrop-filter: none !important; - -webkit-backdrop-filter: none !important; -} - body.panels-edge-left #left-panel-stack > :not(.gev-panel-rail):not(.collapsed) { transform: translate(calc(-1 * var(--panel-rail-clearance, 48px)), var(--edge-rail-row)); max-height: calc(100% - var(--edge-rail-row)) !important; diff --git a/src/ui/styles/rail-split.css b/src/ui/styles/rail-split.css index 1f8400dd1..d84940233 100644 --- a/src/ui/styles/rail-split.css +++ b/src/ui/styles/rail-split.css @@ -1,37 +1,5 @@ -/* Experiment: with a left panel open, its rail icon leaves the rail and sits - level with the panel's title, as the panel's own tab. The other icons stay - in the rail underneath it and close the gap. With nothing open the rail keeps its - labelled pills. */ -.gev-panel-rail--left:has(.ui-rail--collapsible:not(.ui-rail--expanded) .ui-rail__item--on) { - /* The lifted chip (42px) centres on the panel title (26px into the - stack); the rail starts 6px below the chip. */ - top: 53px; -} - -.gev-panel-rail--left .ui-rail--collapsible:not(.ui-rail--expanded) { - position: relative; -} - -.gev-panel-rail--left - .ui-rail--collapsible:not(.ui-rail--expanded) - .ui-tooltip:has(> .ui-rail__item--on) { - position: absolute; - bottom: calc(100% + 6px); - left: -1px; - padding: 4px; - border: 1px solid var(--glass-border); - border-radius: calc(var(--btn-radius) + 4px); - background: var(--glass-bg); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); -} - -.gev-panel-rail--left - .ui-rail--collapsible:not(.ui-rail--expanded) - .ui-tooltip:has(> .ui-rail__item--on) - .ui-rail__item { - width: 32px; -} +/* The left rail's icons keep their places whether or not a panel is open; + only the highlight moves to the open one. */ /* With nothing open, the icons and their names split into two glass panels side by side: the plain icon rail, and a name panel beside it with one row @@ -179,14 +147,18 @@ color: #fff; } -/* The open item's highlight sits on the button itself, so it shows wherever - the button is (the lifted chip, the edge row). */ +/* The open item takes the accent colour; its highlight is the rail's shared + one, which slides to it. Without that highlight the button carries its own. */ :is(.gev-panel-rail--left, .gev-panel-rail--right) .ui-rail__item.ui-rail__item--on { color: #00d4ff !important; - background: rgba(0, 212, 255, 0.15) !important; - border-color: rgba(0, 212, 255, 0.15) !important; } -:is(.gev-panel-rail--left, .gev-panel-rail--right) .ui-rail.ui-rail--glide::before { - display: none !important; +:is(.gev-panel-rail--left, .gev-panel-rail--right) .ui-rail:not(.ui-rail--glide) .ui-rail__item.ui-rail__item--on { + background: rgba(0, 212, 255, 0.15); + border-color: rgba(0, 212, 255, 0.15); +} + +:is(.gev-panel-rail--left, .gev-panel-rail--right) .ui-rail::before { + background: rgba(0, 212, 255, 0.15); + border-color: rgba(0, 212, 255, 0.28); }