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 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Fable 5.1 committed 2026-10-27 18:37:15 +04:00
1 parent f383174f3e
commit cf37e80b7f
7 files changed
+101 -118

No files matched your search

+6 -8
View File
@@ -1,10 +1,8 @@
- The right rail (Display, CCTV, Street Level, Context, plus Weather and Recent - 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 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 an accordion of collapsed launcher cards: at most one panel is open in every
strip and at most one panel is open in every desktop HUD layout. The open desktop HUD layout, and the open tab and its panel share one surface and
tab and its panel share one surface and one outline, like a folder tab; the outline, like a folder tab. A first-time visitor on a desktop finds Display
other tabs are plain labels. A first-time visitor on a desktop finds Display open; a stored choice still wins. The strip is a keyboard-navigable tablist
open, so the strip reads as launchers; a stored choice still wins. The tabs and Escape inside a panel returns focus to its tab. Saved preferences, share
are a keyboard-navigable tablist, Escape inside a panel returns focus to its links and the phone sheet shell are unchanged (gekh, #1425).
tab, and saved preferences and share links are unchanged. Phones keep their
sheet shell (gekh, #1425).
+46 -48
View File
@@ -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. 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 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 treatment; the one-panel rule applies in every desktop layout (see "Right rail
(see "Right rail tabs" below). Display, CCTV and Context scroll their contents tabs" below). Display, CCTV and Context scroll their contents
inside fixed headers and decorative frames. The narrow-screen rail remains inside fixed headers and decorative frames. The narrow-screen rail remains
scrollable to reach each panel. Radio retains the shared nested Context player scrollable to reach each panel. Radio retains the shared nested Context player
and compact header disclosure, without relocating playback controls on theme 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 right rail follows its top baseline and preserves Display's scroll owner.
The left rail's automatic collapse remains presentation only and prioritizes The left rail's automatic collapse remains presentation only and prioritizes
the latest explicit panel. The right rail keeps one expanded owner (the latest 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 explicit panel, else the focused one, else one with a stored or shared choice
presentation-collapses its peers with `rail-owner-collapsed`, which share links over one open only by default, else the first) and presentation-collapses its
and the phone shell read as "expanded"; it needs no second pass. Mobile layout peers with `rail-owner-collapsed`, which share links and the phone shell read
still releases desktop allocation styles and gives every owner-collapsed panel as "expanded". Mobile layout still releases desktop allocation styles and gives
back. Stable Display allocation avoids unnecessary style writes. every owner-collapsed panel back. Stable Display allocation avoids unnecessary
style writes.
Context expansion through the panel owner or adaptive right-rail restoration Context expansion through the panel owner or adaptive right-rail restoration
closes an open compact Radio disclosure before Context/Space Missions becomes closes an open compact Radio disclosure before Context/Space Missions becomes
visible. The mini remains available while Context is collapsed. This fixes the 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 ### Right rail tabs
On desktop the right rail (Display, CCTV, Street Level, Context, plus Weather 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 (`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 tab per panel at the rail's right edge, the open panel beside it, collapsed
collapsed panels are hidden in every HUD layout, so the strip is the only panels hidden in every HUD layout. A tab toggles its panel through
launcher. A tab toggles its panel through `setPanelCollapsed` with the explicit `setPanelCollapsed` with the explicit flag, so opening one collapses the others
flag, so opening one collapses the others and the saved preference, share link and saved preferences, share links and the Context/Radio ownership rules are
and Context/Radio ownership rules are unchanged; the panels' `collapsed` unchanged; the panels' `collapsed` classes remain the only state, mirrored
classes remain the only state and the strip mirrors them through a through a MutationObserver. The strip is a vertical `role="tablist"`: Up/Down
MutationObserver. The strip is a `role="tablist"` with vertical orientation: wrap, Home/End jump, the open tab (else the first) is the tab stop, Escape
Up/Down wrap, Home/End jump, the open tab (else the first) is the tab stop, and inside a panel returns focus to its tab, and the strip precedes the panels in
Escape inside a panel returns focus to that panel's tab. The strip precedes the the DOM so Tab from the open tab enters the panel. Labels are the panels' own
panels in the DOM so Tab from the open tab enters the open panel. Labels are translated header titles. The strip marks the rail with `data-rail-tabs`, which
the panels' own translated header titles. The strip marks the rail with the stylesheet keys the strip's column on; a rail without the strip keeps its
`data-rail-tabs`, which the stylesheet uses to reserve the strip's column; a plain geometry. Phones hide the strip and keep the sheet shell; Cockpit hides
rail without the strip keeps its legacy geometry. Phones hide the strip and the rail and the strip with it.
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 The open tab and its panel are one surface, like a folder tab; the other tabs
tabs are borderless labels. The strip measures the open panel and tab, are borderless labels. The strip measures the open panel and tab, traces their
traces their joined silhouette as an SVG path (`railJoinPath`: rounded panel joined silhouette as an SVG path (`railJoinPath`: rounded corners, concave
corners, the tab's outer corners, concave fillets where the tab meets the fillets at the junction; Cyber's chamfered frame with its calibration bay and a
panel's edge; Cyber's chamfered frame with its calibration bay and a square square tab) and keeps a join element under the panels: one layer blurs and
tab instead) 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
tints the backdrop inside the path, an SVG strokes the outline along it and casts the drop shadow, so the junction has no seam or second blur region. The
another SVG casts the drop shadow, so the junction has no seam, doubled edge open panel's wrapper and card and the tab draw no surface of their own. The
or second blur region. The open panel's wrapper and card and the tab draw no join is re-measured through a ResizeObserver and on strip scroll;
surface, border, shadow or blur of their own (Cyber dresses the wrapper too). The join is re-measured through a `--rail-tab-bottom` on the rail sets the open panel's minimum height, so a
ResizeObserver and on strip scroll; `--rail-tab-bottom` on the rail sets the short panel such as Context still reaches its tab. A first tab level with the
open panel's minimum height, so a short panel such as Context still reaches panel's top shares its top edge; Cyber's strip starts below the frame's upper
its tab. A first tab level with the panel's top shares its top edge, with no chamfer.
corner between them; Cyber's strip starts below the frame's upper chamfer.
A first-time visitor on a desktop viewport finds Display open A first-time visitor on a desktop viewport finds Display open
(`src/ui/panelPositionControls.js`): with every panel closed nothing shows (`src/ui/panelPositionControls.js`): with every panel closed nothing shows that
that the sideways labels are launchers, so the first panel opens to the labels are launchers. Phones keep it closed, a share link decides for
demonstrate the strip. Phones keep it closed (their sheets present the itself, and a stored choice wins either way. When the rail picks its owner, a
rail), a share link decides for itself, and a stored choice wins either way. panel the visitor or a share link opened outranks one open only by default, so
When the rail picks its one owner, a panel the visitor or a share link opened a stored CCTV choice is not displaced by the Display default.
outranks one open only by default, so a stored CCTV choice is not displaced
by the Display default.
## Phone layout ## 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. not apply inside a sheet.
Cockpit, recording and clean view close the sheet and hide the tab bar; scene 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 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 right rail hides no collapsed siblings, shows no tab strip and applies no
the desktop one-panel rule at these widths, since the sheet chooses what shows. The Cesium and Google attribution stays visible above the 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 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. 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. 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 > Missions · Environmental · Explore manually**. No layer and no optional API
> call happens until a tile is clicked. The right-hand DISPLAY rail > call happens until a tile is clicked. The right-hand DISPLAY rail
> (`pp-toggles`) now starts **collapsed** on a first run rather than expanded — > (`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 > a stored collapse state still wins, as before. (Superseded 2026-10-27: with
> right rail tabs: on a desktop viewport DISPLAY starts open again on a first > the right rail tabs DISPLAY starts open on a first desktop run; see "Right
> run; see "Right rail tabs".) > rail tabs".)
> >
> **The ENVIRONMENTAL tile is quakes AND fires** — live USGS earthquakes plus > **The ENVIRONMENTAL tile is quakes AND fires** — live USGS earthquakes plus
> NASA FIRMS active fires (`layerIds: ['earthquakes', 'local-firms']`), with the > NASA FIRMS active fires (`layerIds: ['earthquakes', 'local-firms']`), with the
+5 -7
View File
@@ -70,13 +70,11 @@ export class PanelPositionControls {
// storage unavailable // storage unavailable
} }
} }
// DISPLAY starts EXPANDED for a first-time visitor on a desktop viewport, // 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 // the right rail is a strip of label tabs (rightRailTabs.js), and with
// every panel closed nothing shows that the labels are launchers, so the // every panel closed nothing shows that the labels are launchers. Phones
// first panel opens to demonstrate the strip. Phones keep it collapsed // keep it collapsed (their sheets present the rail); a share link decides
// (their sheets present the rail), and a share link decides for itself. // for itself. A stored choice wins in both directions.
// A stored choice still wins in both directions, so anyone who closes it
// keeps it closed.
if (panelId === 'pp-toggles' && stored === null) if (panelId === 'pp-toggles' && stored === null)
collapsed = !allowStored || isPhoneViewport(); collapsed = !allowStored || isPhoneViewport();
panelEl.classList.toggle('collapsed', collapsed); panelEl.classList.toggle('collapsed', collapsed);
+5 -8
View File
@@ -62,14 +62,11 @@ export function resolveHudRailLayout({
} }
/** /**
* The right rail's tab strip (rightRailTabs.js) is the launcher in every HUD * The tab strip (rightRailTabs.js) is the launcher in every HUD layout, so
* layout, so a collapsed panel never stays visible as a card. The rail state * collapsed right-rail panels always hide. Callers still pass the rail state
* argument is accepted for callers that describe what they lay out. * they lay out.
* * @param {{hudVariant?: string, hasExpandedPanel?: boolean}} [input]
* @param {object} [input] Current rail state. * @returns {boolean}
* @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.
*/ */
export function shouldHideCollapsedRightPanels(input = {}) { export function shouldHideCollapsedRightPanels(input = {}) {
void input; void input;
+6 -7
View File
@@ -74,13 +74,12 @@ export function layoutRightPanelRail({
!panel.hidden && !panel.hidden &&
(hiddenByRail(panel) || getComputedStyle(panel).display !== 'none'), (hiddenByRail(panel) || getComputedStyle(panel).display !== 'none'),
); );
// One expanded owner: the latest explicit disclosure, else the panel that // One expanded owner: the latest explicit disclosure, else the focused
// holds keyboard focus, else one the visitor or a share link chose over one // panel, else one the visitor or a share link chose over one open only by
// open only by default (panelPositionControls.js marks each), else the // default (panelPositionControls.js marks each), else the first. Peers are
// first. Peers are presentation-collapsed without overwriting saved panel // presentation-collapsed; the marker is distinct from the left rail's
// preferences; the marker is distinct from the left rail's space-based // space-based collapse. Phone sheets show several panels at once, so phones
// collapse. Phones present the rail as tab sheets that show several panels // get every panel back.
// at once, so they get every panel back.
const isMobile = windowRef.matchMedia('(max-width: 720px)').matches; const isMobile = windowRef.matchMedia('(max-width: 720px)').matches;
if (!isMobile) { if (!isMobile) {
const expanded = panels.filter( const expanded = panels.filter(
+18 -21
View File
@@ -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 * The open tab and its panel are one surface: the strip measures both, traces
* rail panel, the open panel beside the strip, at most one panel open. The * their joined silhouette as an SVG path (railJoinPath) and keeps a join
* panels' `collapsed` classes remain the only state. The strip mirrors them and * element under the panels whose layers blur, tint, outline and shadow that
* asks the owner (setPanelCollapsed) for every change, so persistence, share * shape (styles/right-rail-tabs.css).
* 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).
*/ */
import { msg, t } from '../i18n/index.js'; 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 * 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 * the tab geometry on it, so a rail without a strip keeps its plain layout.
* layout.
*/ */
export const RIGHT_RAIL_TABS_ATTRIBUTE = 'data-rail-tabs'; export const RIGHT_RAIL_TABS_ATTRIBUTE = 'data-rail-tabs';
@@ -242,8 +239,8 @@ export function createRightRailTabs({
const observer = const observer =
typeof Observer === 'function' ? new Observer(() => sync()) : null; typeof Observer === 'function' ? new Observer(() => sync()) : null;
// Tab heights follow their labels (fonts, locale) and panel heights their // Tab heights follow their labels (fonts, locale) and panel heights their
// content and the rail's allocation, so the join is re-measured whenever // content and allocation: re-measure the join on any size change and on
// the strip, a tab or a panel changes size or the strip scrolls. // strip scroll.
const Resize = documentRef.defaultView?.ResizeObserver; const Resize = documentRef.defaultView?.ResizeObserver;
const resizer = const resizer =
typeof Resize === 'function' ? new Resize(() => sync()) : null; 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, * Publish the join: the open tab's extent on the rail (--rail-tab-top and
* --rail-tab-bottom, rail pixels, for the panel's minimum height) and the * --rail-tab-bottom, rail pixels; the stylesheet sets the panel's minimum
* joined silhouette on the join element's layers. The rail itself can be * height from them) and the joined silhouette on the join's layers. The
* taller than its panel in the focus layout, so the join is sized from * rail can be taller than its panel in the focus layout, so the join is
* the panel, extended down to cover the tab if the panel is shorter. * sized from the panel, extended down to cover the tab if that is shorter.
*/ */
function placeJoin(tab, panel) { function placeJoin(tab, panel) {
const style = rail.style; const style = rail.style;
+15 -19
View File
@@ -4,19 +4,15 @@
exclusive layout (rightPanelRail.js) hides the collapsed ones, so the strip exclusive layout (rightPanelRail.js) hides the collapsed ones, so the strip
is the only launcher. Phones keep the sheet shell (mobile-shell.css). 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 The open tab and its panel are one surface. The strip builds a join element
element (.right-rail-join) under the panels from the measured geometry: (.right-rail-join) under the panels: one path traces the panel with the tab
one path traces the silhouette of the panel with the tab attached to its attached (rounded corners and concave fillets; Cyber's chamfered frame).
right edge (rounded corners, concave fillets at the junction; Cyber's The glass layer blurs and tints the backdrop inside the path, one SVG
chamfered frame instead). The glass layer blurs and tints the backdrop strokes the outline and another casts the shadow, so the junction has no
inside that path, an SVG strokes the outline along it and another SVG seam and no second blur region. The open panel and the tab draw no surface
casts the drop shadow, so there is no seam, no doubled edge and no second of their own; the other tabs are plain labels. `data-rail-tabs` arrives on
blur region where the tab meets the panel. The open panel's card and the the rail with the strip, so a rail without it keeps its plain geometry; the
tab draw no surface of their own; the other tabs are borderless labels. selectors carry it to outrank the Cyber rail rules that follow this sheet. */
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. */
#right-context-rail[data-rail-tabs] { #right-context-rail[data-rail-tabs] {
--right-rail-panel-width: 330px; --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. /* 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 Cyber's Display and Context frames and the surface contract's card live on
card, on a ::before. */ a ::before. */
body:not(.mobile-shell) body:not(.mobile-shell)
#right-context-rail[data-rail-tabs] #right-context-rail[data-rail-tabs]
> #pp-toggles:not(.collapsed), > #pp-toggles:not(.collapsed),
@@ -190,7 +186,8 @@ body:not(.mobile-shell)
-webkit-backdrop-filter: none; -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) body:not(.mobile-shell)
#right-context-rail[data-rail-tabs] #right-context-rail[data-rail-tabs]
> [data-panel-id]:not(.collapsed) > [data-panel-id]:not(.collapsed)
@@ -256,9 +253,8 @@ body:not(.mobile-shell)
color: var(--cyber-red-bright); color: var(--cyber-red-bright);
} }
/* Cyber also dresses every .panel-collapsible wrapper as a surface with an /* Cyber gives every .panel-collapsible wrapper a surface with an inset ring
inset ring (cyber.css); under the tabs the join is the only surface, or (cyber.css); beside the open tab that ring would show as a line. */
the ring shows as a line along the panel's edge beside the open tab. */
:root[data-ui-theme='cyber'] :root[data-ui-theme='cyber']
body:not(.mobile-shell) body:not(.mobile-shell)
#right-context-rail[data-rail-tabs] #right-context-rail[data-rail-tabs]