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 <[email protected]>
This commit is contained in:
Manjunath PaparajuandSameh Khamis authored and Sameh Khamis committed 2026-11-14 13:41:07 -08:00
1 parent 59ffa87535
commit b090937e5f
5 files changed
+30 -72

No files matched your search

+5 -2
View File
@@ -283,13 +283,16 @@ export function layoutLeftPanelRail({
getComputedStyle(stack) getComputedStyle(stack)
.getPropertyValue?.('--panel-stack-top-pinned') .getPropertyValue?.('--panel-stack-top-pinned')
?.trim() === '1'; ?.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) { if (stack.style.getPropertyValue('--panel-rail-offset') !== railOffset) {
stack.style.setProperty('--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 // 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. // 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) { if (stack.style.getPropertyValue('--panel-rail-lift') !== railLift) {
stack.style.setProperty('--panel-rail-lift', railLift); stack.style.setProperty('--panel-rail-lift', railLift);
} }
+13 -4
View File
@@ -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'); 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'); const f = fixture('left');
f.expand(f.first, 900); f.expand(f.first, 900);
f.run(); f.run();
assert.equal(f.stack.dataset.layoutMode, 'focus'); assert.equal(f.stack.dataset.layoutMode, 'focus');
assert.ok( assert.notEqual(short.stack.dataset.layoutMode, 'focus');
parseFloat(f.stack.style.getPropertyValue('--panel-rail-offset')) > 0, 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. // A theme that pins the stack's top (Cyber) leaves nothing to offset.
f.stack.computed.getPropertyValue = (name) => f.stack.computed.getPropertyValue = (name) =>
-16
View File
@@ -62,24 +62,9 @@ export function installRailEdgeScroll(doc = globalThis.document) {
// highlight is showing is kept here, and the classes are put back each time. // highlight is showing is kept here, and the classes are put back each time.
const gliding = new WeakSet(); 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) { function glide(rail) {
rail.classList.toggle('ui-rail--edge-hot', rail === hot); rail.classList.toggle('ui-rail--edge-hot', rail === hot);
const on = rail.querySelector('.ui-rail__item[aria-pressed="true"]'); const on = rail.querySelector('.ui-rail__item[aria-pressed="true"]');
reorder(rail, on);
if (!on?.offsetWidth) { if (!on?.offsetWidth) {
gliding.delete(rail); gliding.delete(rail);
rail.classList.remove('ui-rail--glide'); rail.classList.remove('ui-rail--glide');
@@ -263,7 +248,6 @@ export function installRailEdgeScroll(doc = globalThis.document) {
win.removeEventListener('resize', onResize); win.removeEventListener('resize', onResize);
doc.querySelectorAll('.ui-rail').forEach((rail) => { doc.querySelectorAll('.ui-rail').forEach((rail) => {
rail.classList.remove('ui-rail--glide', 'ui-rail--glide-jump', 'ui-rail--edge-hot'); rail.classList.remove('ui-rail--glide', 'ui-rail--glide-jump', 'ui-rail--edge-hot');
reorder(rail, null);
}); });
}; };
} }
-10
View File
@@ -71,16 +71,6 @@ body.panels-edge-right:not(.context-docked) .gev-panel-rail--right .ui-rail--col
gap: 4px; 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) { 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)); transform: translate(calc(-1 * var(--panel-rail-clearance, 48px)), var(--edge-rail-row));
max-height: calc(100% - var(--edge-rail-row)) !important; max-height: calc(100% - var(--edge-rail-row)) !important;
+12 -40
View File
@@ -1,37 +1,5 @@
/* Experiment: with a left panel open, its rail icon leaves the rail and sits /* The left rail's icons keep their places whether or not a panel is open;
level with the panel's title, as the panel's own tab. The other icons stay only the highlight moves to the open one. */
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;
}
/* With nothing open, the icons and their names split into two glass panels /* 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 side by side: the plain icon rail, and a name panel beside it with one row
@@ -179,14 +147,18 @@
color: #fff; color: #fff;
} }
/* The open item's highlight sits on the button itself, so it shows wherever /* The open item takes the accent colour; its highlight is the rail's shared
the button is (the lifted chip, the edge row). */ 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 { :is(.gev-panel-rail--left, .gev-panel-rail--right) .ui-rail__item.ui-rail__item--on {
color: #00d4ff !important; 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 { :is(.gev-panel-rail--left, .gev-panel-rail--right) .ui-rail:not(.ui-rail--glide) .ui-rail__item.ui-rail__item--on {
display: none !important; 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);
} }