mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
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:
1 parent
71bd645d59
commit
3fb2105fb1
5 files changed
+54
-232
No files matched your search
+38
-13
@@ -225,11 +225,13 @@ layouts retain their existing presentation.
|
||||
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's map-side right rail opens one panel at a time while keeping collapsed
|
||||
launchers available. 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 changes.
|
||||
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
|
||||
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
|
||||
changes.
|
||||
|
||||
Voice help/error popups and Location/Visual Presets pins extend beyond their
|
||||
Cyber frames without being clipped: the shapes decorate non-interactive
|
||||
@@ -1472,17 +1474,40 @@ retains observers, frame scheduling, saved/share preferences and Cockpit portals
|
||||
|
||||
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.
|
||||
Automatic collapse remains presentation only, prioritizes the latest explicit
|
||||
panel, and honors keyboard focus on the right. Mobile layout still releases
|
||||
desktop allocation styles. Stable Display allocation avoids unnecessary style
|
||||
writes. This extraction does not change panel positions or layout defaults.
|
||||
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.
|
||||
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
|
||||
Radio-overlap path reported in #804 without changing z-index or layout policy.
|
||||
The right rail skips panels hidden by other CSS (computed `display: none`), but
|
||||
keeps the collapsed panels its own exclusive presentation hides, so automatic
|
||||
collapse can still be undone.
|
||||
keeps the collapsed panels its own exclusive presentation hides, so the owner
|
||||
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
|
||||
(`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.
|
||||
|
||||
## Phone layout
|
||||
|
||||
@@ -1519,8 +1544,8 @@ 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 nor applies Cyber's one-panel
|
||||
accordion at these widths, since the sheet chooses what shows. The Cesium and Google attribution stays visible above 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
|
||||
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.
|
||||
|
||||
+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,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,tune,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,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" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- gev:template scene-chrome -->
|
||||
|
||||
+8
-30
@@ -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();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
Reference in new issue
Block a user