From cf37e80b7f0a8c716e94c3063ce448a8e9a0c736 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Tue, 27 Oct 2026 18:37:15 +0400 Subject: [PATCH] docs: tighten the right rail tabs comments, docs and changelog Drop change-history wording, shorten the module, stylesheet and rail comments, and document the owner rule's stored-over-default step in CURRENT-STATE. No runtime change. Co-Authored-By: Claude Fable 5.1 --- changelog.d/1425-right-rail-vertical-tabs.md | 14 ++- docs/CURRENT-STATE.md | 94 ++++++++++---------- src/ui/panelPositionControls.js | 12 ++- src/ui/panelRailGeometry.js | 13 ++- src/ui/rightPanelRail.js | 13 ++- src/ui/rightRailTabs.js | 39 ++++---- src/ui/styles/right-rail-tabs.css | 34 ++++--- 7 files changed, 101 insertions(+), 118 deletions(-) diff --git a/changelog.d/1425-right-rail-vertical-tabs.md b/changelog.d/1425-right-rail-vertical-tabs.md index 27d5363d7..ac081f6e9 100644 --- a/changelog.d/1425-right-rail-vertical-tabs.md +++ b/changelog.d/1425-right-rail-vertical-tabs.md @@ -1,10 +1,8 @@ - The right rail (Display, CCTV, Street Level, Context, plus Weather and Recent Imagery while shown) is a vertical tab strip with sideways labels instead of - an accordion of collapsed launcher cards: the open panel sits beside the - strip and at most one panel is open in every desktop HUD layout. The open - tab and its panel share one surface and one outline, like a folder tab; the - other tabs are plain labels. A first-time visitor on a desktop finds Display - open, so the strip reads as launchers; a stored choice still wins. The tabs - are a keyboard-navigable tablist, Escape inside a panel returns focus to its - tab, and saved preferences and share links are unchanged. Phones keep their - sheet shell (gekh, #1425). + 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. A first-time visitor on a desktop finds Display + open; a stored choice still wins. The strip is a keyboard-navigable tablist + and Escape inside a panel returns focus to its tab. 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 a55354457..1553a0fd9 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -226,8 +226,8 @@ Chamfered panel expand/collapse controls use an inset keyboard-focus outline and red hover border in both map and cockpit, so clipping does not hide the highlight. Cyber skins the map-side right rail's tab strip in its red and slate panel -treatment; the one-panel rule it introduced now belongs to every desktop layout -(see "Right rail tabs" below). Display, CCTV and Context scroll their contents +treatment; the one-panel rule applies in every desktop layout (see "Right rail +tabs" below). Display, CCTV and Context scroll their contents inside fixed headers and decorative frames. The narrow-screen rail remains scrollable to reach each panel. Radio retains the shared nested Context player and compact header disclosure, without relocating playback controls on theme @@ -1476,11 +1476,12 @@ The left rail keeps its measured collapsed heights and obstacle-safe corridor; the right rail follows its top baseline and preserves Display's scroll owner. The left rail's automatic collapse remains presentation only and prioritizes the latest explicit panel. The right rail keeps one expanded owner (the latest -explicit panel, else the panel holding keyboard focus, else the first) and -presentation-collapses its peers with `rail-owner-collapsed`, which share links -and the phone shell read as "expanded"; it needs no second pass. Mobile layout -still releases desktop allocation styles and gives every owner-collapsed panel -back. Stable Display allocation avoids unnecessary style writes. +explicit panel, else the focused one, else one with a stored or shared choice +over one open only by default, else the first) and presentation-collapses its +peers with `rail-owner-collapsed`, which share links and the phone shell read +as "expanded". Mobile layout still releases desktop allocation styles and gives +every owner-collapsed panel back. Stable Display allocation avoids unnecessary +style writes. Context expansion through the panel owner or adaptive right-rail restoration closes an open compact Radio disclosure before Context/Space Missions becomes visible. The mini remains available while Context is collapsed. This fixes the @@ -1492,46 +1493,42 @@ rule can still give them back. ### Right rail tabs On desktop the right rail (Display, CCTV, Street Level, Context, plus Weather -and Recent Imagery while they are shown) is a vertical tab strip +and Recent Imagery while shown) is a vertical tab strip (`src/ui/rightRailTabs.js`, `src/ui/styles/right-rail-tabs.css`): one sideways -tab per panel hugs the rail's right edge, the open panel sits beside it, and -collapsed panels are hidden in every HUD layout, so the strip is the only -launcher. A tab toggles its panel through `setPanelCollapsed` with the explicit -flag, so opening one collapses the others and the saved preference, share link -and Context/Radio ownership rules are unchanged; the panels' `collapsed` -classes remain the only state and the strip mirrors them through a -MutationObserver. The strip is a `role="tablist"` with vertical orientation: -Up/Down wrap, Home/End jump, the open tab (else the first) is the tab stop, and -Escape inside a panel returns focus to that panel's tab. The strip precedes the -panels in the DOM so Tab from the open tab enters the open panel. Labels are -the panels' own translated header titles. The strip marks the rail with -`data-rail-tabs`, which the stylesheet uses to reserve the strip's column; a -rail without the strip keeps its legacy geometry. Phones hide the strip and -keep the sheet shell; Cockpit hides the rail and the strip with it. +tab per panel at the rail's right edge, the open panel beside it, collapsed +panels hidden in every HUD layout. A tab toggles its panel through +`setPanelCollapsed` with the explicit flag, so opening one collapses the others +and saved preferences, share links and the Context/Radio ownership rules are +unchanged; the panels' `collapsed` classes remain the only state, mirrored +through a MutationObserver. 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 enters the panel. Labels are the panels' own +translated header titles. The strip marks the rail with `data-rail-tabs`, which +the stylesheet keys the strip's column on; a rail without the strip keeps its +plain geometry. Phones hide the strip and keep the sheet shell; Cockpit hides +the rail and the strip with it. -The open tab and its panel are one surface, like a folder tab, and the other -tabs are borderless labels. The strip measures the open panel and tab, -traces their joined silhouette as an SVG path (`railJoinPath`: rounded panel -corners, the tab's outer corners, concave fillets where the tab meets the -panel's edge; Cyber's chamfered frame with its calibration bay and a square -tab instead) and keeps a join element under the panels: one layer blurs and -tints the backdrop inside the path, an SVG strokes the outline along it and -another SVG casts the drop shadow, so the junction has no seam, doubled edge -or second blur region. The open panel's wrapper and card and the tab draw no -surface, border, shadow or blur of their own (Cyber dresses the wrapper too). The join is re-measured through a -ResizeObserver and on strip scroll; `--rail-tab-bottom` on the rail sets the -open panel's minimum height, so a short panel such as Context still reaches -its tab. A first tab level with the panel's top shares its top edge, with no -corner between them; Cyber's strip starts below the frame's upper chamfer. +The open tab and its panel are one surface, like a folder tab; the other tabs +are borderless labels. The strip measures the open panel and tab, traces their +joined silhouette as an SVG path (`railJoinPath`: rounded corners, concave +fillets at the junction; Cyber's chamfered frame with its calibration bay and a +square tab) and keeps a join element under the panels: one layer blurs and +tints the backdrop inside the path, one SVG strokes the outline and another +casts the drop shadow, so the junction has no seam or second blur region. The +open panel's wrapper and card and the tab draw no surface of their own. The +join is re-measured through a ResizeObserver and on strip scroll; +`--rail-tab-bottom` on the rail sets the open panel's minimum height, so a +short panel such as Context still reaches its tab. A first tab level with the +panel's top shares its top edge; Cyber's strip starts below the frame's upper +chamfer. A first-time visitor on a desktop viewport finds Display open -(`src/ui/panelPositionControls.js`): with every panel closed nothing shows -that the sideways labels are launchers, so the first panel opens to -demonstrate the strip. Phones keep it closed (their sheets present the -rail), a share link decides for itself, and a stored choice wins either way. -When the rail picks its one owner, a panel the visitor or a share link opened -outranks one open only by default, so a stored CCTV choice is not displaced -by the Display default. +(`src/ui/panelPositionControls.js`): with every panel closed nothing shows that +the labels are launchers. Phones keep it closed, a share link decides for +itself, and a stored choice wins either way. When the rail picks its owner, a +panel the visitor or a share link opened outranks one open only by default, so +a stored CCTV choice is not displaced by the Display default. ## Phone layout @@ -1568,8 +1565,9 @@ sheet before a panel's own Escape handling, and the dock's hover auto-close does not apply inside a sheet. Cockpit, recording and clean view close the sheet and hide the tab bar; scene playback keeps both so its STOP control in the Scenes panel stays reachable. The -right rail neither hides collapsed siblings, shows its tab strip, nor applies -the desktop one-panel rule at these widths, since the sheet chooses what shows. The Cesium and Google attribution stays visible above the +right rail hides no collapsed siblings, shows no tab strip and applies no +one-panel rule at these widths, since the sheet chooses what shows. The Cesium +and Google attribution stays visible above the voice control, and above an open sheet. The page draws under the notch and system bars (`viewport-fit=cover`) and the shell's chrome uses safe-area insets. Wider viewports are unchanged, and crossing to them closes any open sheet. @@ -2166,9 +2164,9 @@ fetching, trailing-24-hour filtering and partial-success caching are unchanged. > Missions · Environmental · Explore manually**. No layer and no optional API > call happens until a tile is clicked. The right-hand DISPLAY rail > (`pp-toggles`) now starts **collapsed** on a first run rather than expanded — -> a stored collapse state still wins, as before. (Superseded 2026-10-27 by the -> right rail tabs: on a desktop viewport DISPLAY starts open again on a first -> run; see "Right rail tabs".) +> a stored collapse state still wins, as before. (Superseded 2026-10-27: with +> the right rail tabs DISPLAY starts open on a first desktop run; see "Right +> rail tabs".) > > **The ENVIRONMENTAL tile is quakes AND fires** — live USGS earthquakes plus > NASA FIRMS active fires (`layerIds: ['earthquakes', 'local-firms']`), with the diff --git a/src/ui/panelPositionControls.js b/src/ui/panelPositionControls.js index 214585a84..52cf20526 100644 --- a/src/ui/panelPositionControls.js +++ b/src/ui/panelPositionControls.js @@ -70,13 +70,11 @@ export class PanelPositionControls { // storage unavailable } } - // DISPLAY starts EXPANDED for a first-time visitor on a desktop viewport, - // where the right rail is a strip of label tabs (rightRailTabs.js): with - // every panel closed nothing shows that the labels are launchers, so the - // first panel opens to demonstrate the strip. Phones keep it collapsed - // (their sheets present the rail), and a share link decides for itself. - // A stored choice still wins in both directions, so anyone who closes it - // keeps it closed. + // DISPLAY starts EXPANDED for a first-time visitor on a desktop viewport: + // the right rail is a strip of label tabs (rightRailTabs.js), and with + // every panel closed nothing shows that the labels are launchers. Phones + // keep it collapsed (their sheets present the rail); a share link decides + // for itself. A stored choice wins in both directions. if (panelId === 'pp-toggles' && stored === null) collapsed = !allowStored || isPhoneViewport(); panelEl.classList.toggle('collapsed', collapsed); diff --git a/src/ui/panelRailGeometry.js b/src/ui/panelRailGeometry.js index 99e5767e0..89057c35b 100644 --- a/src/ui/panelRailGeometry.js +++ b/src/ui/panelRailGeometry.js @@ -62,14 +62,11 @@ export function resolveHudRailLayout({ } /** - * The right rail's tab strip (rightRailTabs.js) is the launcher in every HUD - * layout, so a collapsed panel never stays visible as a card. The rail state - * argument is accepted for callers that describe what they lay out. - * - * @param {object} [input] Current rail state. - * @param {string} [input.hudVariant] Active HUD layout variant. - * @param {boolean} [input.hasExpandedPanel] Whether any rail panel is expanded. - * @returns {boolean} Whether collapsed sibling panels should be hidden. + * The tab strip (rightRailTabs.js) is the launcher in every HUD layout, so + * collapsed right-rail panels always hide. Callers still pass the rail state + * they lay out. + * @param {{hudVariant?: string, hasExpandedPanel?: boolean}} [input] + * @returns {boolean} */ export function shouldHideCollapsedRightPanels(input = {}) { void input; diff --git a/src/ui/rightPanelRail.js b/src/ui/rightPanelRail.js index 70164ea2e..24ac479dc 100644 --- a/src/ui/rightPanelRail.js +++ b/src/ui/rightPanelRail.js @@ -74,13 +74,12 @@ export function layoutRightPanelRail({ !panel.hidden && (hiddenByRail(panel) || getComputedStyle(panel).display !== 'none'), ); - // One expanded owner: the latest explicit disclosure, else the panel that - // holds keyboard focus, else one the visitor or a share link chose over one - // open only by default (panelPositionControls.js marks each), else the - // first. Peers are presentation-collapsed without overwriting saved panel - // preferences; the marker is distinct from the left rail's space-based - // collapse. Phones present the rail as tab sheets that show several panels - // at once, so they get every panel back. + // One expanded owner: the latest explicit disclosure, else the focused + // panel, else one the visitor or a share link chose over one open only by + // default (panelPositionControls.js marks each), else the first. Peers are + // presentation-collapsed; the marker is distinct from the left rail's + // space-based collapse. Phone sheets show several panels at once, so phones + // get every panel back. const isMobile = windowRef.matchMedia('(max-width: 720px)').matches; if (!isMobile) { const expanded = panels.filter( diff --git a/src/ui/rightRailTabs.js b/src/ui/rightRailTabs.js index caebb0aec..32977c258 100644 --- a/src/ui/rightRailTabs.js +++ b/src/ui/rightRailTabs.js @@ -1,17 +1,15 @@ /** - * Vertical tab strip for the right context rail. + * Vertical tab strip for the right context rail: one sideways tab per rail + * panel, the open panel beside the strip, at most one panel open. The panels' + * `collapsed` classes remain the only state; the strip mirrors them and asks + * the owner (setPanelCollapsed) for every change, so persistence, share links + * and the rail layout pass are untouched. Phones hide the strip and keep their + * sheet shell (mobileShell.js). * - * The strip replaces the rail's collapsed launcher cards: one sideways tab per - * rail panel, the open panel beside the strip, at most one panel open. The - * panels' `collapsed` classes remain the only state. The strip mirrors them and - * asks the owner (setPanelCollapsed) for every change, so persistence, share - * links and the rail layout pass are untouched. Phones hide the strip and keep - * their sheet shell (mobileShell.js). - * - * The open tab and its panel are drawn as one surface: the strip measures - * both, traces their joined silhouette as an SVG path (railJoinPath) and keeps - * a join element under the panels whose layers blur, tint, outline and shadow - * that one shape (styles/right-rail-tabs.css). + * The open tab and its panel are one surface: the strip measures both, traces + * their joined silhouette as an SVG path (railJoinPath) and keeps a join + * element under the panels whose layers blur, tint, outline and shadow that + * shape (styles/right-rail-tabs.css). */ import { msg, t } from '../i18n/index.js'; @@ -19,8 +17,7 @@ export const RIGHT_RAIL_TABS_ID = 'right-rail-tabs'; /** * Marks the rail while its strip is mounted; styles/right-rail-tabs.css keys - * the rail's tab geometry on it so a rail without a strip keeps its legacy - * layout. + * the tab geometry on it, so a rail without a strip keeps its plain layout. */ export const RIGHT_RAIL_TABS_ATTRIBUTE = 'data-rail-tabs'; @@ -242,8 +239,8 @@ export function createRightRailTabs({ const observer = typeof Observer === 'function' ? new Observer(() => sync()) : null; // Tab heights follow their labels (fonts, locale) and panel heights their - // content and the rail's allocation, so the join is re-measured whenever - // the strip, a tab or a panel changes size or the strip scrolls. + // content and allocation: re-measure the join on any size change and on + // strip scroll. const Resize = documentRef.defaultView?.ResizeObserver; const resizer = typeof Resize === 'function' ? new Resize(() => sync()) : null; @@ -265,11 +262,11 @@ export function createRightRailTabs({ }; /** - * Publish the join: the open tab's extent on the rail (--rail-tab-top, - * --rail-tab-bottom, rail pixels, for the panel's minimum height) and the - * joined silhouette on the join element's layers. The rail itself can be - * taller than its panel in the focus layout, so the join is sized from - * the panel, extended down to cover the tab if the panel is shorter. + * Publish the join: the open tab's extent on the rail (--rail-tab-top and + * --rail-tab-bottom, rail pixels; the stylesheet sets the panel's minimum + * height from them) and the joined silhouette on the join's layers. The + * rail can be taller than its panel in the focus layout, so the join is + * sized from the panel, extended down to cover the tab if that is shorter. */ function placeJoin(tab, panel) { const style = rail.style; diff --git a/src/ui/styles/right-rail-tabs.css b/src/ui/styles/right-rail-tabs.css index d2976e13e..e18e3ed3c 100644 --- a/src/ui/styles/right-rail-tabs.css +++ b/src/ui/styles/right-rail-tabs.css @@ -4,19 +4,15 @@ exclusive layout (rightPanelRail.js) hides the collapsed ones, so the strip is the only launcher. Phones keep the sheet shell (mobile-shell.css). - The open tab and its panel are one surface. The strip builds a join - element (.right-rail-join) under the panels from the measured geometry: - one path traces the silhouette of the panel with the tab attached to its - right edge (rounded corners, concave fillets at the junction; Cyber's - chamfered frame instead). The glass layer blurs and tints the backdrop - inside that path, an SVG strokes the outline along it and another SVG - casts the drop shadow, so there is no seam, no doubled edge and no second - blur region where the tab meets the panel. The open panel's card and the - tab draw no surface of their own; the other tabs are borderless labels. - The strip also publishes --rail-tab-bottom on the rail so a short panel - grows down to its tab. `data-rail-tabs` arrives on the rail with the - strip, so a rail without it keeps its legacy geometry; the selectors carry - it so they outrank the Cyber rail rules that follow this sheet. */ + The open tab and its panel are one surface. The strip builds a join element + (.right-rail-join) under the panels: one path traces the panel with the tab + attached (rounded corners and concave fillets; Cyber's chamfered frame). + The glass layer blurs and tints the backdrop inside the path, one SVG + strokes the outline and another casts the shadow, so the junction has no + seam and no second blur region. The open panel and the tab draw no surface + of their own; the other tabs are plain labels. `data-rail-tabs` arrives on + the rail with the strip, so a rail without it keeps its plain geometry; the + selectors carry it to outrank the Cyber rail rules that follow this sheet. */ #right-context-rail[data-rail-tabs] { --right-rail-panel-width: 330px; @@ -160,8 +156,8 @@ body.mobile-shell #right-context-rail > .right-rail-join { } /* The open panel's card leaves surface, outline and shadow to the join. - Cyber draws Display's and Context's frames, and the surface contract its - card, on a ::before. */ + Cyber's Display and Context frames and the surface contract's card live on + a ::before. */ body:not(.mobile-shell) #right-context-rail[data-rail-tabs] > #pp-toggles:not(.collapsed), @@ -190,7 +186,8 @@ body:not(.mobile-shell) -webkit-backdrop-filter: none; } -/* The card's glow used to sit under its own surface; keep it as faint. */ +/* The glow's 0.25 is tuned to sit under the card's surface; without that + surface, 0.08 reads the same. */ body:not(.mobile-shell) #right-context-rail[data-rail-tabs] > [data-panel-id]:not(.collapsed) @@ -256,9 +253,8 @@ body:not(.mobile-shell) color: var(--cyber-red-bright); } -/* Cyber also dresses every .panel-collapsible wrapper as a surface with an - inset ring (cyber.css); under the tabs the join is the only surface, or - the ring shows as a line along the panel's edge beside the open tab. */ +/* Cyber gives every .panel-collapsible wrapper a surface with an inset ring + (cyber.css); beside the open tab that ring would show as a line. */ :root[data-ui-theme='cyber'] body:not(.mobile-shell) #right-context-rail[data-rail-tabs]