diff --git a/changelog.d/1425-right-rail-vertical-tabs.md b/changelog.d/1425-right-rail-vertical-tabs.md index 459b8546e..a1231690a 100644 --- a/changelog.d/1425-right-rail-vertical-tabs.md +++ b/changelog.d/1425-right-rail-vertical-tabs.md @@ -2,12 +2,13 @@ Imagery while shown) is a vertical tab strip with sideways labels instead of an accordion of collapsed launcher cards: at most one panel is open in every desktop HUD layout, and the open tab and its panel share one surface and - outline, like a folder tab. On a short window the rail rises toward the HUD corner above it and the tabs share the room equally, clipping the longer labels, rather than scroll. A first-time visitor on a - desktop finds Display open; a stored choice still wins. The panels' own - collapse arrows are gone on desktop: the tab opens and closes a panel, and - Escape inside one closes it and returns focus to its tab; the strip is a - keyboard-navigable tablist. The compact Radio launcher moves from Context's - header to the bottom of the strip on desktop and always opens the compact - player, also while Context is open; the player's Details button leads to the - Radio section. Saved preferences, share links and the phone sheet shell are - unchanged (gekh, #1425). + outline, like a folder tab. On a short window the rail rises toward the HUD + corner above it, and the tabs then share the room equally rather than + scroll. A first-time visitor on a desktop finds Display open; a stored + choice still wins. The panels' own collapse arrows are gone on desktop: the + tab opens and closes a panel, Escape inside one closes it and returns focus + to its tab, and the strip is a keyboard-navigable tablist. The compact Radio + launcher moves from Context's header to the bottom of the strip on desktop + and always opens the compact player, also while Context is open; the + player's Details button leads to the Radio section. Saved preferences, share + links and the phone sheet shell are unchanged (gekh, #1425). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index da8d8b768..d836efd75 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1488,8 +1488,8 @@ unnecessary style writes. The compact Radio player is independent of Context: its launcher toggles it whether Context is collapsed or open, and only an outside pointer, Escape, its close button or its Details button closes it, never a layout pass. The -expanded Enable's reveal of the directory and Play lands its smooth scroll -even when a rail layout pass, measuring the panels, cuts it short. +reveal after the expanded Enable lands its smooth scroll even when a rail +layout pass cuts it short. The right rail skips panels hidden by other CSS (computed `display: none`), but keeps the collapsed panels its own exclusive presentation hides, so the owner rule can still give them back. @@ -1510,12 +1510,11 @@ The strip is a vertical `role="tablist"`: Up/Down wrap, Home/End jump, the open tab (else the first) is the tab stop, Escape inside a panel returns focus to its tab, and the strip precedes the panels in the DOM so Tab from the open tab reaches the tool slot and then the panel. -Labels are the panels' own translated header titles. The rail's lane shares -the left rail's top; when it is too short for the strip, the rail climbs toward -the HUD corner above as far as the strip needs (`src/ui/rightPanelRail.js`), -and a lane still shorter is shared equally between the tabs: a label short -enough keeps its height, the long ones end up the same height and clip at the -end; the strip never scrolls. Under the tabs the strip's +Labels are the panels' own translated header titles. The lane's top follows the +left rail's; a strip taller than the lane lifts the rail toward the HUD corner +above (`src/ui/rightPanelRail.js`), and a lane still shorter is shared equally +between the tabs, so the long labels end up one height and clip; the strip +never scrolls. Under the tabs the strip's column keeps a tool slot (`#right-rail-tools`) that holds the compact Radio launcher on desktop: `src/ui/radioPanelPlacement.js` moves it there from Context's header, and back for the phone sheet, so the compact player stays one diff --git a/src/ui/panelDisclosure.js b/src/ui/panelDisclosure.js index 69b9d25d1..aa8418102 100644 --- a/src/ui/panelDisclosure.js +++ b/src/ui/panelDisclosure.js @@ -13,13 +13,6 @@ function listenerScope() { }; } -/** The strip tab that controls a right-rail panel, if the panel has one. */ -function railTabFor(panel) { - return panel.ownerDocument?.querySelector?.( - `[role="tab"][aria-controls="${panel.id}"]`, - ); -} - /** Bind collapse buttons and bubbling Escape to an existing panel. */ export function bindPanelDisclosure({ panel, @@ -70,7 +63,9 @@ export function collapsePanelOnEscape( panel.querySelector(`[data-dock-toggle-target="${panel.id}"]`) || panel.querySelector(`[data-collapse-target="${panel.id}"]`); // A right-rail panel collapses out of view; its tab stays and takes focus. - const tab = railTabFor(panel); + const tab = panel.ownerDocument?.querySelector?.( + `[role="tab"][aria-controls="${panel.id}"]`, + ); const escapedFromDisclosure = event.target === disclosure || disclosure?.contains?.(event.target); if (tab) tab.focus({ preventScroll: true }); diff --git a/src/ui/rightPanelRail.js b/src/ui/rightPanelRail.js index cdb165399..71149773e 100644 --- a/src/ui/rightPanelRail.js +++ b/src/ui/rightPanelRail.js @@ -174,8 +174,8 @@ export function layoutRightPanelRail({ .map((node) => [node, node.scrollTop || 0]) .filter(([, top]) => top > 0); const naturalHeights = new Map(); - // The tab strip's column stands beside the panels at its own height; the - // measuring override lifts its lane clamp too. + // The tab strip's column stands beside the panels; the measuring override + // gives it its natural height. const column = stack.querySelector?.(':scope > #right-rail-column'); let columnHeight = 0; // Remove live height allocations through a synchronous CSS override. Keeping @@ -222,7 +222,7 @@ export function layoutRightPanelRail({ if (!layout) return; // A lane too short for the strip's column: the rail climbs toward the HUD // above, as far as the strip needs and the obstacles allow. A lane still - // shorter scrolls the strip (rightRailTabs.js). + // shorter shrinks the tabs (styles/right-rail-tabs.css). const shortfall = columnHeight - (layout.safeBottom - layout.safeTop); if (shortfall > 0) layout = resolve(alignedTop - shortfall) || layout; const { safeTop, safeBottom, maxHeight: availableHeight } = layout; diff --git a/src/ui/styles/right-rail-tabs.css b/src/ui/styles/right-rail-tabs.css index 534ecf06b..0bace3e13 100644 --- a/src/ui/styles/right-rail-tabs.css +++ b/src/ui/styles/right-rail-tabs.css @@ -57,10 +57,10 @@ } } -/* The strip's column: the tablist, then a tool slot that holds the compact - Radio launcher on desktop (radioPanelPlacement.js). It stands the lane's - full height, which makes the strip's size definite so its tabs can share - it (see .right-rail-tab); only the strip and the slot take the pointer. */ +/* The strip's column: the tablist, then a tool slot for the compact Radio + launcher (radioPanelPlacement.js). It stands the lane's full height, which + makes the strip's size definite for the tabs' share (.right-rail-tab); only + the strip and the slot take the pointer. */ #right-rail-column { position: absolute; top: 0; @@ -90,8 +90,7 @@ min-height: 0; /* Room for the open tab's bridge on the left. */ padding-left: var(--right-rail-tabs-gap); - /* The rail climbs above its baseline to fit the column (rightPanelRail.js); - a lane still shorter shrinks the tabs. The strip never scrolls. */ + /* Never a scrollbar: a lane too short shrinks the tabs (.right-rail-tab). */ overflow: hidden; pointer-events: auto; } @@ -168,17 +167,16 @@ body:not(.mobile-shell) display: none; } -/* The strip clips overflow, so the focus ring draws inside the tab. */ +/* The tab's mask covers its own box only: the focus ring draws inside it. */ #right-rail-tabs > .right-rail-tab:focus-visible { outline-offset: -3px; } -/* A short strip is shared between the tabs: a zero basis hands the strip's - height out in equal parts, a tab's own length caps its part, and what a - short label leaves goes to the longer ones, which end up the same height. A - strip with room gives every tab its own length. The basis is 0%, not 0: - the strip's content size, which sizes the column, comes from the labels. A - tab clips its label at the end, faded over the bottom padding. */ +/* A short strip is shared out equally: the zero basis hands every tab the + same part, a tab's own length caps it, and a short label's leftover goes + to the longer ones; with room each tab is its own length. The basis is + 0%, not 0, so the strip still sizes to its labels. The label clips at the + end, faded over the bottom padding. */ .right-rail-tab { position: relative; display: flex; @@ -231,7 +229,7 @@ body:not(.mobile-shell) text-shadow: none; } -/* Short windows: tighter tabs before the strip has to scroll. */ +/* Short windows: tighter tabs, so fewer labels clip. */ @media (max-height: 720px) { #right-rail-tabs { gap: 3px;