feat(ui): keep one tab presentation and document the right rail tabs

Drop the four alternative strip presentations (cards-up, spine, icons,
minimal) and the icon glyphs they needed; the strip marks the rail with
data-rail-tabs so a rail without it keeps its legacy geometry. Describe
the tab strip and the desktop one-owner rule in docs/CURRENT-STATE.md.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Fable 5.1 committed 2026-10-27 11:08:56 +04:00
1 parent 71bd645d59
commit 3fb2105fb1
5 files changed
+54 -232

No files matched your search

+8 -30
View File
@@ -12,25 +12,12 @@ import { msg, t } from '../i18n/index.js';
export const RIGHT_RAIL_TABS_ID = 'right-rail-tabs';
/** Presentation variants, selected through `data-rail-tabs` on the rail. */
export const RIGHT_RAIL_TAB_VARIANTS = Object.freeze([
'cards-down',
'cards-up',
'spine',
'icons',
'minimal',
]);
export const DEFAULT_RIGHT_RAIL_TAB_VARIANT = 'cards-down';
/** Glyphs for the icon variant; every name is in index.html's icon_names. */
const PANEL_ICONS = Object.freeze({
'pp-toggles': 'tune',
'cctv-panel': 'videocam',
'street-level-panel': 'streetview',
'global-context-panel': 'radar',
'weather-panel': 'cloud',
'recent-imagery-panel': 'satellite_alt',
});
/**
* 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.
*/
export const RIGHT_RAIL_TABS_ATTRIBUTE = 'data-rail-tabs';
/**
* The tab keyboard navigation moves to in a vertical tablist: Down and Up
@@ -80,14 +67,12 @@ function panelTitle(panel) {
* @param {(panelId: string, collapsed: boolean, options: object) => void} input.setPanelCollapsed
* Owner callback; the strip never touches panel classes itself.
* @param {Document} [input.documentRef]
* @param {string} [input.variant] Initial presentation variant.
* @returns {{ element: HTMLElement, sync: () => void, destroy: () => void }}
*/
export function createRightRailTabs({
rail,
setPanelCollapsed,
documentRef = rail?.ownerDocument,
variant = DEFAULT_RIGHT_RAIL_TAB_VARIANT,
}) {
if (!rail || !documentRef || typeof setPanelCollapsed !== 'function') {
throw new TypeError(
@@ -100,7 +85,7 @@ export function createRightRailTabs({
strip.setAttribute('role', 'tablist');
strip.setAttribute('aria-orientation', 'vertical');
strip.setAttribute('aria-label', t('Panels'));
if (!rail.dataset.railTabs) rail.dataset.railTabs = variant;
rail.setAttribute(RIGHT_RAIL_TABS_ATTRIBUTE, 'vertical');
/** @type {Map<string, {tab: HTMLElement, label: HTMLElement}>} */
const entries = new Map();
@@ -116,14 +101,6 @@ export function createRightRailTabs({
tab.dataset.railTab = panel.id;
tab.setAttribute('role', 'tab');
tab.setAttribute('aria-controls', panel.id);
const icon = PANEL_ICONS[panel.id];
if (icon) {
const glyph = documentRef.createElement('span');
glyph.className = 'material-symbols-outlined right-rail-tab-icon';
glyph.setAttribute('aria-hidden', 'true');
glyph.textContent = icon;
tab.append(glyph);
}
const label = documentRef.createElement('span');
label.className = 'right-rail-tab-label';
tab.append(label);
@@ -216,6 +193,7 @@ export function createRightRailTabs({
strip.removeEventListener('click', onClick);
strip.removeEventListener('keydown', onKeydown);
strip.remove();
rail.removeAttribute(RIGHT_RAIL_TABS_ATTRIBUTE);
entries.clear();
},
};
+3 -2
View File
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
DEFAULT_RIGHT_RAIL_TAB_VARIANT,
RIGHT_RAIL_TABS_ATTRIBUTE,
RIGHT_RAIL_TABS_ID,
createRightRailTabs,
nextRailTabIndex,
@@ -196,7 +196,7 @@ test('builds one tab per visible rail panel, in rail order, labelled from the pa
assert.equal(f.strip.id, RIGHT_RAIL_TABS_ID);
assert.equal(f.strip.getAttribute('role'), 'tablist');
assert.equal(f.strip.getAttribute('aria-orientation'), 'vertical');
assert.equal(f.rail.dataset.railTabs, DEFAULT_RIGHT_RAIL_TAB_VARIANT);
assert.equal(f.rail.getAttribute(RIGHT_RAIL_TABS_ATTRIBUTE), 'vertical');
assert.deepEqual(
f.strip.children.map((tab) => [
tab.dataset.railTab,
@@ -328,6 +328,7 @@ test('destroy removes the strip and stops listening', () => {
f.tabs.destroy();
assert.equal(f.strip.parentElement, null);
assert.equal(f.rail.children.includes(f.strip), false);
assert.equal(f.rail.getAttribute(RIGHT_RAIL_TABS_ATTRIBUTE), null);
fire(tab, 'click');
assert.deepEqual(f.calls, []);
f.tabs.destroy();
+4 -186
View File
@@ -4,16 +4,10 @@
exclusive layout (rightPanelRail.js) hides the collapsed ones, so the strip
is the only launcher. Phones keep the sheet shell (mobile-shell.css).
`data-rail-tabs` on the rail picks a presentation:
cards-down one glass card per tab, labels read top to bottom (default)
cards-up the same cards, labels read bottom to top
spine one continuous bar with an accent edge on the open tab
icons upright icon above a sideways label
minimal bare labels with an accent bar, like the HUD edge text
The attribute arrives with the strip (rightRailTabs.js), so a rail without
it keeps its legacy geometry; the selectors carry it so they outrank the
Cyber rail rules that follow this sheet. */
One glass card per tab, labels read top to bottom like the HUD edge text.
`data-rail-tabs` arrives on the rail with the strip (rightRailTabs.js), 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-rail-panel-width: 330px;
@@ -87,11 +81,6 @@
white-space: nowrap;
}
/* The icon font's own display rule must not reveal the glyph elsewhere. */
.right-rail-tab > .right-rail-tab-icon {
display: none;
}
.right-rail-tab:hover {
border-color: var(--glass-border-hover);
background: rgba(255, 255, 255, 0.06);
@@ -105,135 +94,6 @@
color: var(--accent);
}
/* ── cards-up: labels read bottom to top ───────────────────────────────── */
#right-context-rail[data-rail-tabs='cards-up'] .right-rail-tab-label {
transform: rotate(180deg);
}
@supports (writing-mode: sideways-lr) {
#right-context-rail[data-rail-tabs='cards-up'] .right-rail-tab-label {
writing-mode: sideways-lr;
transform: none;
}
}
/* ── spine: one bar, accent edge toward the open panel ─────────────────── */
#right-context-rail[data-rail-tabs='spine'] {
--right-rail-tabs-gap: 8px;
}
#right-context-rail[data-rail-tabs='spine'] #right-rail-tabs {
gap: 0;
border: 1px solid var(--glass-border);
border-radius: 10px;
background: var(--glass-bg);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.42),
inset 0 1px 0 rgba(255, 255, 255, 0.04);
backdrop-filter: blur(24px) saturate(1.4);
-webkit-backdrop-filter: blur(24px) saturate(1.4);
}
#right-context-rail[data-rail-tabs='spine'] .right-rail-tab {
min-height: 64px;
padding: 16px 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
#right-context-rail[data-rail-tabs='spine'] .right-rail-tab + .right-rail-tab {
border-top: 1px solid var(--glass-border);
}
#right-context-rail[data-rail-tabs='spine'] .right-rail-tab:hover {
background: rgba(255, 255, 255, 0.05);
}
#right-context-rail[data-rail-tabs='spine']
.right-rail-tab[aria-selected='true'] {
background: var(--accent-dim);
box-shadow: inset 2px 0 0 var(--accent);
}
/* ── icons: upright glyph over the sideways label ──────────────────────── */
#right-context-rail[data-rail-tabs='icons'] {
--right-rail-tabs-width: 40px;
}
#right-context-rail[data-rail-tabs='icons'] .right-rail-tab {
flex-direction: column;
gap: 6px;
min-height: 64px;
padding: 10px 0 12px;
letter-spacing: 2px;
}
#right-context-rail[data-rail-tabs='icons']
.right-rail-tab
> .right-rail-tab-icon {
display: block;
font-size: 16px;
line-height: 1;
}
/* ── minimal: bare labels with an accent bar, like the HUD edge text ───── */
#right-context-rail[data-rail-tabs='minimal'] {
--right-rail-tabs-width: 26px;
--right-rail-tabs-gap: 14px;
}
#right-context-rail[data-rail-tabs='minimal'] #right-rail-tabs {
gap: 18px;
}
#right-context-rail[data-rail-tabs='minimal'] .right-rail-tab {
min-height: 0;
padding: 4px 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
color: var(--text-secondary);
letter-spacing: 3px;
}
#right-context-rail[data-rail-tabs='minimal'] .right-rail-tab::before {
content: '';
position: absolute;
top: 2px;
bottom: 2px;
left: 2px;
width: 2px;
border-radius: 1px;
background: transparent;
transition: background-color var(--transition-fast);
}
#right-context-rail[data-rail-tabs='minimal'] .right-rail-tab:hover {
background: transparent;
color: var(--text-primary);
}
#right-context-rail[data-rail-tabs='minimal']
.right-rail-tab[aria-selected='true'] {
background: transparent;
box-shadow: none;
color: var(--accent);
text-shadow: 0 0 8px var(--accent-glow);
}
#right-context-rail[data-rail-tabs='minimal']
.right-rail-tab[aria-selected='true']::before {
background: var(--accent);
box-shadow: 0 0 8px var(--accent-glow);
}
/* ── Cyber skin ────────────────────────────────────────────────────────── */
:root[data-ui-theme='cyber'] .right-rail-tab {
border-color: var(--glass-border);
@@ -256,48 +116,6 @@
color: var(--cyber-red-bright);
}
:root[data-ui-theme='cyber']
#right-context-rail[data-rail-tabs='spine']
#right-rail-tabs {
border-radius: var(--btn-radius);
background-color: var(--cyber-panel-surface);
background-image: var(--cyber-panel-surface-detail);
box-shadow: var(--cyber-panel-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
:root[data-ui-theme='cyber']
#right-context-rail[data-rail-tabs='spine']
.right-rail-tab,
:root[data-ui-theme='cyber']
#right-context-rail[data-rail-tabs='minimal']
.right-rail-tab {
background: transparent;
box-shadow: none;
}
:root[data-ui-theme='cyber']
#right-context-rail[data-rail-tabs='spine']
.right-rail-tab[aria-selected='true'] {
background: var(--accent-dim);
box-shadow: inset 3px 0 0 var(--cyber-red);
}
:root[data-ui-theme='cyber']
#right-context-rail[data-rail-tabs='minimal']
.right-rail-tab[aria-selected='true'] {
color: var(--cyber-red-bright);
text-shadow: 0 0 8px var(--accent-glow);
}
:root[data-ui-theme='cyber']
#right-context-rail[data-rail-tabs='minimal']
.right-rail-tab[aria-selected='true']::before {
background: var(--cyber-red);
box-shadow: 0 0 8px var(--accent-glow);
}
/* ── Phones keep the sheet shell ───────────────────────────────────────── */
body.mobile-shell #right-context-rail {
padding-right: 0;