mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
feat(ui): upright icon tabs on the right rail, never shrunk or clipped
The rail tabs show one Material Symbols glyph each (Display tune, CCTV videocam, Street Level streetview, Context travel_explore, Weather cloud, Recent Imagery satellite_alt) in a fixed 34px square, the size of the Radio launcher under them. The panel's translated title is the tab's aria-label and tooltip. The sideways labels, the equal share of a short strip, the end fade and the column's lane cap are gone: every tab stays whole, and on the shortest windows the strip runs past the lane instead of clipping. The glyphs join the icon font subset, with a test that keeps them there. The screenshot is refreshed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
33ccdad744
commit
8396fd3dc7
8 files changed
+109
-130
No files matched your search
@@ -1,14 +1,13 @@
|
||||
- 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: 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 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
|
||||
Imagery while shown) is a vertical strip of icon tabs 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
|
||||
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).
|
||||
+10
-9
@@ -1498,8 +1498,8 @@ rule can still give them back.
|
||||
|
||||
On desktop the right rail (Display, CCTV, Street Level, Context, plus Weather
|
||||
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 at the rail's right edge, the open panel beside it. A tab
|
||||
(`src/ui/rightRailTabs.js`, `src/ui/styles/right-rail-tabs.css`): one upright
|
||||
icon tab per panel at the rail's right edge, the open panel beside it. A tab
|
||||
toggles its panel through `setPanelCollapsed` with the explicit flag; the
|
||||
panels' `collapsed` classes remain the only state, mirrored through a
|
||||
MutationObserver, and the owner rule above hides the rest. On desktop the
|
||||
@@ -1509,11 +1509,12 @@ 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 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
|
||||
Glyphs come from `RAIL_TAB_ICONS` (Material Symbols, in the `index.html`
|
||||
subset); a tab's accessible name and tooltip are the panel's translated header
|
||||
title. The tabs keep their size. 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 on a lane still shorter the strip runs past
|
||||
its end; the strip never scrolls or clips. 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
|
||||
@@ -1524,7 +1525,7 @@ 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; the other tabs
|
||||
are borderless labels. The strip measures the open panel and tab, traces their
|
||||
are borderless icons. The strip measures the open panel and tab, traces their
|
||||
joined silhouette as an SVG path (`railJoinPath`; Cyber gets its chamfered
|
||||
frame) and shapes a join element under the panels with it: glass, outline and
|
||||
shadow in one piece, so the junction has no seam. The panel and the tab draw
|
||||
@@ -1533,7 +1534,7 @@ minimum height, so a short panel such as Context still reaches its tab.
|
||||
|
||||
A first-time visitor on a desktop viewport finds Display open
|
||||
(`src/ui/panelPositionControls.js`): with every panel closed nothing shows that
|
||||
the labels are launchers. Phones keep it closed, a share link decides for
|
||||
the icons 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.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 387 KiB After Width: | Height: | Size: 404 KiB |
+1
-1
@@ -26,7 +26,7 @@
|
||||
with the sources: src/materialSymbolsSubset.test.mjs fails when a source
|
||||
names a glyph this list is missing, because a missing glyph does not draw
|
||||
a box — the ligature never forms and the element renders the literal word. -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,400,0,0&icon_names=adjust,arrow_drop_down,arrow_forward,arrow_left,arrow_right,bolt,chevron_left,chevron_right,close,close_fullscreen,dark_mode,draw,east,flare,flight,layers,layers_clear,light_mode,local_fire_department,more_horiz,my_location,navigation,normal,on,open_in_full,public,radar,radio,right_panel_close,right_panel_open,rocket_launch,search,settings,skip_next,skip_previous,upload_file,videocam,view_in_ar,visibility,visibility_off" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,400,0,0&icon_names=adjust,arrow_drop_down,arrow_forward,arrow_left,arrow_right,bolt,chevron_left,chevron_right,close,close_fullscreen,cloud,dark_mode,draw,east,flare,flight,layers,layers_clear,light_mode,local_fire_department,more_horiz,my_location,navigation,normal,on,open_in_full,public,radar,radio,right_panel_close,right_panel_open,rocket_launch,satellite_alt,search,settings,skip_next,skip_previous,streetview,travel_explore,tune,upload_file,videocam,view_in_ar,visibility,visibility_off" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- gev:template scene-chrome -->
|
||||
|
||||
@@ -175,8 +175,7 @@ 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; the measuring override
|
||||
// gives it its natural height.
|
||||
// The tab strip's column stands beside the panels at its natural height.
|
||||
const column = stack.querySelector?.(':scope > #right-rail-column');
|
||||
let columnHeight = 0;
|
||||
// Remove live height allocations through a synchronous CSS override. Keeping
|
||||
@@ -223,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 shrinks the tabs (styles/right-rail-tabs.css).
|
||||
// shorter leaves the strip running past the lane's end.
|
||||
const shortfall = columnHeight - (layout.safeBottom - layout.safeTop);
|
||||
if (shortfall > 0) layout = resolve(alignedTop - shortfall) || layout;
|
||||
const { safeTop, safeBottom, maxHeight: availableHeight } = layout;
|
||||
|
||||
+28
-15
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Vertical tab strip for the right context rail: one sideways tab per rail
|
||||
* Vertical tab strip for the right context rail: one icon 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
|
||||
@@ -21,6 +21,19 @@ const RAIL_TABS_ATTRIBUTE = 'data-rail-tabs';
|
||||
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
|
||||
/**
|
||||
* Each rail panel's tab glyph (Material Symbols). index.html subsets the icon
|
||||
* font, so its `icon_names` must carry every one.
|
||||
*/
|
||||
export const RAIL_TAB_ICONS = Object.freeze({
|
||||
'pp-toggles': 'tune',
|
||||
'cctv-panel': 'videocam',
|
||||
'street-level-panel': 'streetview',
|
||||
'global-context-panel': 'travel_explore',
|
||||
'weather-panel': 'cloud',
|
||||
'recent-imagery-panel': 'satellite_alt',
|
||||
});
|
||||
|
||||
/**
|
||||
* The tab keyboard navigation moves to in a vertical tablist: Down and Up
|
||||
* wrap, Home and End jump. Any other key keeps the current index.
|
||||
@@ -177,7 +190,7 @@ export function railJoinPath({
|
||||
return d.join(' ');
|
||||
}
|
||||
|
||||
/** The tab label: the panel's own header title, already translated. */
|
||||
/** The tab's name: the panel's own header title, already translated. */
|
||||
function panelTitle(panel) {
|
||||
return (
|
||||
panel
|
||||
@@ -230,7 +243,7 @@ export function createRightRailTabs({
|
||||
outline.append(outlinePath);
|
||||
join.append(shadow, glass, outline);
|
||||
|
||||
/** @type {Map<string, {tab: HTMLElement, label: HTMLElement}>} */
|
||||
/** @type {Map<string, {tab: HTMLElement, icon: HTMLElement}>} */
|
||||
const entries = new Map();
|
||||
const observed = new WeakSet();
|
||||
let destroyed = false;
|
||||
@@ -244,10 +257,12 @@ export function createRightRailTabs({
|
||||
tab.dataset.railTab = panel.id;
|
||||
tab.setAttribute('role', 'tab');
|
||||
tab.setAttribute('aria-controls', panel.id);
|
||||
const label = documentRef.createElement('span');
|
||||
label.className = 'right-rail-tab-label';
|
||||
tab.append(label);
|
||||
entry = { tab, label };
|
||||
const icon = documentRef.createElement('span');
|
||||
icon.className = 'right-rail-tab-icon material-symbols-outlined';
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
icon.textContent = RAIL_TAB_ICONS[panel.id] || 'more_horiz';
|
||||
tab.append(icon);
|
||||
entry = { tab, icon };
|
||||
entries.set(panel.id, entry);
|
||||
return entry;
|
||||
};
|
||||
@@ -256,9 +271,8 @@ export function createRightRailTabs({
|
||||
documentRef.defaultView?.MutationObserver ?? globalThis.MutationObserver;
|
||||
const observer =
|
||||
typeof Observer === 'function' ? new Observer(() => sync()) : null;
|
||||
// Tab heights follow their labels (fonts, locale) and panel heights their
|
||||
// content and allocation: re-measure the join when the strip or a panel
|
||||
// changes size.
|
||||
// Panel heights follow their content and allocation: re-measure the join
|
||||
// when the strip or a panel changes size.
|
||||
const Resize = documentRef.defaultView?.ResizeObserver;
|
||||
const resizer =
|
||||
typeof Resize === 'function' ? new Resize(() => sync()) : null;
|
||||
@@ -325,8 +339,7 @@ export function createRightRailTabs({
|
||||
right: stripLeft + tab.offsetLeft + tab.offsetWidth,
|
||||
},
|
||||
corner,
|
||||
// A tab shrunk by a short strip keeps corners no larger than half of it.
|
||||
tabRadius: Math.min(tabRadius, (tabBottom - tabTop) / 2),
|
||||
tabRadius,
|
||||
fillet,
|
||||
style: chamfer ? 'chamfer' : 'round',
|
||||
};
|
||||
@@ -342,7 +355,7 @@ export function createRightRailTabs({
|
||||
join.hidden = false;
|
||||
}
|
||||
|
||||
/** Reconcile tabs with the rail: order, visibility, selection, labels. */
|
||||
/** Reconcile tabs with the rail: order, visibility, selection, names. */
|
||||
function sync() {
|
||||
if (destroyed) return;
|
||||
// The column precedes its panels in the tab order, so Tab from the open
|
||||
@@ -365,13 +378,13 @@ export function createRightRailTabs({
|
||||
let cursor = strip.firstElementChild;
|
||||
let open = null;
|
||||
for (const panel of panels) {
|
||||
const { tab, label } = entryFor(panel);
|
||||
const { tab } = entryFor(panel);
|
||||
if (tab !== cursor) strip.insertBefore(tab, cursor);
|
||||
else cursor = cursor.nextElementSibling;
|
||||
const collapsed = panel.classList.contains('collapsed');
|
||||
if (!collapsed && !open) open = { tab, panel };
|
||||
const name = panelTitle(panel);
|
||||
if (label.textContent !== name) label.textContent = name;
|
||||
tab.setAttribute('aria-label', name);
|
||||
tab.setAttribute('aria-selected', String(!collapsed));
|
||||
tab.tabIndex = panel === focusOwner ? 0 : -1;
|
||||
tab.title = t(collapsed ? msg('Expand {name}') : msg('Collapse {name}'), {
|
||||
|
||||
@@ -2,7 +2,9 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { fire, makeDocument } from '../testSupport/fakeDom.mjs';
|
||||
import { readStylesheet } from '../testSupport/readStylesheet.mjs';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import {
|
||||
RAIL_TAB_ICONS,
|
||||
createRightRailTabs,
|
||||
nextRailTabIndex,
|
||||
railJoinPath,
|
||||
@@ -57,10 +59,10 @@ function fixture() {
|
||||
const join = rail.querySelector('.right-rail-join');
|
||||
const tabFor = (id) =>
|
||||
strip.children.find((tab) => tab.dataset.railTab === id);
|
||||
const labelOf = (tab) =>
|
||||
const iconOf = (tab) =>
|
||||
tab.children.find((child) =>
|
||||
child.classList.contains('right-rail-tab-label'),
|
||||
).textContent;
|
||||
child.classList.contains('right-rail-tab-icon'),
|
||||
);
|
||||
return {
|
||||
doc,
|
||||
rail,
|
||||
@@ -75,11 +77,11 @@ function fixture() {
|
||||
join,
|
||||
calls,
|
||||
tabFor,
|
||||
labelOf,
|
||||
iconOf,
|
||||
};
|
||||
}
|
||||
|
||||
test('builds one tab per visible rail panel, in rail order, labelled from the panel header', () => {
|
||||
test('builds one icon tab per visible rail panel, in rail order, named from the panel header', () => {
|
||||
const f = fixture();
|
||||
assert.equal(
|
||||
f.rail.children[0],
|
||||
@@ -98,22 +100,39 @@ test('builds one tab per visible rail panel, in rail order, labelled from the pa
|
||||
assert.deepEqual(
|
||||
f.strip.children.map((tab) => [
|
||||
tab.dataset.railTab,
|
||||
f.labelOf(tab),
|
||||
f.iconOf(tab).textContent,
|
||||
tab.getAttribute('aria-label'),
|
||||
tab.getAttribute('aria-controls'),
|
||||
]),
|
||||
[
|
||||
['pp-toggles', 'DISPLAY', 'pp-toggles'],
|
||||
['cctv-panel', 'CCTV', 'cctv-panel'],
|
||||
['global-context-panel', 'CONTEXT', 'global-context-panel'],
|
||||
['pp-toggles', 'tune', 'DISPLAY', 'pp-toggles'],
|
||||
['cctv-panel', 'videocam', 'CCTV', 'cctv-panel'],
|
||||
[
|
||||
'global-context-panel',
|
||||
'travel_explore',
|
||||
'CONTEXT',
|
||||
'global-context-panel',
|
||||
],
|
||||
],
|
||||
'a hidden panel (Weather off) has no tab',
|
||||
);
|
||||
for (const tab of f.strip.children) {
|
||||
assert.equal(tab.getAttribute('role'), 'tab');
|
||||
assert.equal(tab.type, 'button');
|
||||
assert.equal(f.iconOf(tab).getAttribute('aria-hidden'), 'true');
|
||||
}
|
||||
});
|
||||
|
||||
test('the icon font subset carries every rail tab glyph', () => {
|
||||
const html = readFileSync(
|
||||
new URL('../../index.html', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const subset = /icon_names=([a-z0-9_,]+)/.exec(html)[1].split(',');
|
||||
for (const glyph of Object.values(RAIL_TAB_ICONS))
|
||||
assert.ok(subset.includes(glyph), glyph);
|
||||
});
|
||||
|
||||
test('mirrors collapsed state into aria-selected, a roving tabindex and titles', () => {
|
||||
const f = fixture();
|
||||
assert.deepEqual(
|
||||
@@ -255,15 +274,6 @@ test('publishes the open tab extent on the rail and keeps a join element under t
|
||||
glass.style.clipPath,
|
||||
/^path\('M 16 0 L 314 0 A 16 16 0 0 1 330 16 L 330 78 /,
|
||||
);
|
||||
// A short strip shrinks the tab: its corners stay within half its height.
|
||||
tab.offsetHeight = 12;
|
||||
f.tabs.sync();
|
||||
assert.equal(props.get('--rail-tab-bottom'), '98px');
|
||||
assert.match(
|
||||
glass.style.clipPath,
|
||||
/L 366 86 A 6 6 0 0 1 372 92 A 6 6 0 0 1 366 98 L 338 98 /,
|
||||
'a 12px tab gets 6px corners',
|
||||
);
|
||||
f.cctv.classList.add('collapsed');
|
||||
f.tabs.sync();
|
||||
assert.deepEqual([...props], [], 'no open panel, no join');
|
||||
@@ -431,7 +441,7 @@ test('railJoinPath shrinks the fillet and corner to the room left, down to a sha
|
||||
);
|
||||
});
|
||||
|
||||
test('the stylesheet keeps phones on border-box, folds the strip into narrow desktops, shrinks tabs instead of scrolling and draws the focus ring inside', () => {
|
||||
test('the stylesheet keeps phones on border-box, folds the strip into narrow desktops, keeps icon tabs square and whole without scrolling', () => {
|
||||
const css = readStylesheet(new URL('../../style.css', import.meta.url));
|
||||
assert.match(
|
||||
css,
|
||||
@@ -441,38 +451,24 @@ test('the stylesheet keeps phones on border-box, folds the strip into narrow des
|
||||
css,
|
||||
/@media \(min-width: 721px\) and \(max-width: 760px\) \{\s*#right-context-rail\[data-rail-tabs\] \{\s*width: calc\(330px - var\(--right-rail-tabs-inset\)\);/,
|
||||
);
|
||||
assert.match(css, /#right-rail-tabs \{[^}]*overflow: hidden;/);
|
||||
// The tabs keep their size: a square icon, never shrunk or rotated.
|
||||
assert.match(
|
||||
css,
|
||||
/\.right-rail-tab \{[^}]*flex: 0 0 auto;[^}]*width: var\(--right-rail-tabs-width\);\s*height: var\(--right-rail-tabs-width\);/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
css,
|
||||
/#right-rail-tabs \{[^}]*(overflow: [^;]*auto|scrollbar-width)/,
|
||||
/\.right-rail-tab[^{]*\{[^}]*(writing-mode|mask-image)/,
|
||||
);
|
||||
// The tabs give way to a short strip and clip their labels at the end.
|
||||
assert.match(
|
||||
// The column keeps its natural height: every tab stays whole, nothing
|
||||
// scrolls or clips; only the strip and the slot take the pointer.
|
||||
assert.doesNotMatch(
|
||||
css,
|
||||
/\.right-rail-tab \{[^}]*flex: 1 1 0%;[^}]*align-items: flex-start;[^}]*min-height: 0;[^}]*max-height: max-content;[^}]*overflow: hidden;[^}]*mask-image: linear-gradient\(to bottom, #000 calc\(100% - 6px\), transparent\);/,
|
||||
);
|
||||
// The column stands the lane's full height so the strip's size is definite
|
||||
// and a zero basis shares it; only the strip and the slot take the pointer.
|
||||
assert.match(
|
||||
css,
|
||||
/#right-rail-column \{[^}]*height: var\(--right-stack-max-height, 70vh\);[^}]*pointer-events: none;/,
|
||||
/#right-rail-(column|tabs) \{[^}]*(max-height|overflow|scrollbar-width)/,
|
||||
);
|
||||
assert.match(css, /#right-rail-column \{[^}]*pointer-events: none;/);
|
||||
assert.match(css, /#right-rail-tabs \{[^}]*pointer-events: auto;/);
|
||||
assert.match(css, /#right-rail-tools \{[^}]*pointer-events: auto;/);
|
||||
assert.doesNotMatch(
|
||||
css,
|
||||
/#right-rail-column \{[^}]*max-height: var\(--right-stack-max-height/,
|
||||
);
|
||||
// The rail layout pass measures the column's natural height to climb above
|
||||
// its baseline before the strip has to scroll.
|
||||
assert.match(
|
||||
css,
|
||||
/#right-context-rail\[data-rail-measuring\] > #right-rail-column \{\s*height: auto !important;/,
|
||||
);
|
||||
assert.match(
|
||||
css,
|
||||
/#right-rail-tabs > \.right-rail-tab:focus-visible \{\s*outline-offset: -3px;/,
|
||||
);
|
||||
assert.match(css, /--right-rail-panel-cover: min\(/);
|
||||
// The column hides on phones, where the Radio launcher is back in Context's
|
||||
// header; in the slot the launcher's popover opens upward and to the left.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* ── Right rail as vertical tabs ───────────────────────────────────────────
|
||||
A sideways tab strip (rightRailTabs.js) hugs the rail's right edge and the
|
||||
A strip of icon tabs (rightRailTabs.js) hugs the rail's right edge and the
|
||||
open panel sits beside it. Panels keep their `collapsed` state; the rail's
|
||||
exclusive layout (rightPanelRail.js) hides the collapsed ones, so the strip
|
||||
is the only launcher. The compact Radio launcher, which rides in Context's
|
||||
@@ -12,7 +12,7 @@
|
||||
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
|
||||
of their own; the other tabs are plain icons. `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. */
|
||||
|
||||
@@ -58,9 +58,9 @@
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
launcher (radioPanelPlacement.js). It keeps its natural height, so every
|
||||
tab stays whole even past a short lane; only the strip and the slot take
|
||||
the pointer. */
|
||||
#right-rail-column {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -70,28 +70,17 @@
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
width: var(--right-rail-tabs-inset);
|
||||
height: var(--right-stack-max-height, 70vh);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* The layout pass reads the column's natural height, to climb when the lane
|
||||
is too short for it (rightPanelRail.js). */
|
||||
#right-context-rail[data-rail-measuring] > #right-rail-column {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
#right-rail-tabs {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
box-sizing: border-box;
|
||||
min-height: 0;
|
||||
/* Room for the open tab's bridge on the left. */
|
||||
padding-left: var(--right-rail-tabs-gap);
|
||||
/* Never a scrollbar: a lane too short shrinks the tabs (.right-rail-tab). */
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@@ -110,7 +99,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* In the slot the launcher is one more label in the column: no card, the
|
||||
/* In the slot the launcher is one more icon in the column: no card, the
|
||||
tab's corner, the accent while the radio plays. Its popover opens to the
|
||||
left over the panel, bottom-aligned so it stays inside the lane. */
|
||||
#right-rail-tools .context-radio-dock {
|
||||
@@ -163,38 +152,22 @@ body:not(.mobile-shell)
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 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 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. */
|
||||
/* A square icon the strip's width; the panel's name is the tab's accessible
|
||||
name and tooltip. */
|
||||
.right-rail-tab {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
align-items: flex-start;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
width: var(--right-rail-tabs-width);
|
||||
min-height: 0;
|
||||
max-height: max-content;
|
||||
padding: 6px 0;
|
||||
overflow: hidden;
|
||||
mask-image: linear-gradient(to bottom, #000 calc(100% - 6px), transparent);
|
||||
height: var(--right-rail-tabs-width);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: var(--right-rail-tab-radius);
|
||||
background: transparent;
|
||||
color: var(--text-dim);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
letter-spacing: 1.6px;
|
||||
text-transform: uppercase;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
@@ -202,11 +175,9 @@ body:not(.mobile-shell)
|
||||
color var(--transition-fast);
|
||||
}
|
||||
|
||||
.right-rail-tab-label {
|
||||
display: block;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
white-space: nowrap;
|
||||
.right-rail-tab-icon {
|
||||
color: inherit;
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.right-rail-tab:not([aria-selected='true']):hover {
|
||||
@@ -214,8 +185,8 @@ body:not(.mobile-shell)
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* The open tab is the label and hit area over the join's surface; it spans
|
||||
the bridge too, with the label kept in the strip's column. */
|
||||
/* The open tab is the icon and hit area over the join's surface; it spans
|
||||
the bridge too, with the icon kept in the strip's column. */
|
||||
.right-rail-tab[aria-selected='true'] {
|
||||
width: calc(var(--right-rail-tabs-width) + var(--right-rail-tabs-gap));
|
||||
margin-left: calc(-1 * var(--right-rail-tabs-gap));
|
||||
@@ -331,7 +302,7 @@ body:not(.mobile-shell)
|
||||
}
|
||||
|
||||
/* Cyber's generic hover/selected button treatment (cyber.css) would paint
|
||||
the tabs; the open tab stays a label on the shared surface. */
|
||||
the tabs; the open tab stays an icon on the shared surface. */
|
||||
:root[data-ui-theme='cyber'] .right-rail-tab:not([aria-selected='true']):hover {
|
||||
background-color: rgba(226, 235, 236, 0.08);
|
||||
box-shadow: none;
|
||||
|
||||
Reference in new issue
Block a user