mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
feat(ui): join the open right rail tab to its panel
The open tab and its panel share one glass surface and one outline, like a folder tab: the panel card draws no border of its own, the rail's ::before draws the outline with a notch at the tab, the tab closes the shape with its own borders and two concave fillets round the inner corners. The strip publishes the open tab's extent and the panel's height on the rail; a short panel grows to reach its tab. Cyber joins square below its chamfer. Unselected tabs are plain labels. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
f2e23249eb
commit
65c1ae67f2
6 files changed
+345
-45
No files matched your search
@@ -1,7 +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 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).
|
||||
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. 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).
|
||||
@@ -1509,6 +1509,21 @@ the panels' own translated header titles. The strip marks the rail with
|
||||
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.
|
||||
|
||||
The open tab and its panel read as one shape, like a folder tab: they share
|
||||
the glass surface and one outline, and the other tabs are borderless labels.
|
||||
The panel card keeps its surface but draws no border; the rail's `::before`
|
||||
draws the outline around the card with a notch where the tab joins, the open
|
||||
tab closes the shape with its own top, right and bottom borders, and two
|
||||
concave fillets round the inner corners (Cyber's chamfered frames join
|
||||
square, and its strip starts below the frame's upper chamfer). The strip
|
||||
publishes the join on the rail as inline custom properties, re-measured
|
||||
through a ResizeObserver and on strip scroll: `--rail-tab-top` and
|
||||
`--rail-tab-bottom` place the notch and set the open panel's minimum height,
|
||||
so a short panel such as Context still reaches its tab, and
|
||||
`--rail-panel-height` sizes the outline, since the rail is taller than its
|
||||
panel in the focus layout. The first tab starts level with the panel's top
|
||||
edge, so the outline's top runs straight into it with no corner.
|
||||
|
||||
## Phone layout
|
||||
|
||||
At 720px and narrower, `src/ui/mobileShell.js` keeps the globe full screen and
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 383 KiB After Width: | Height: | Size: 362 KiB |
+42
-2
@@ -104,6 +104,7 @@ export function createRightRailTabs({
|
||||
const label = documentRef.createElement('span');
|
||||
label.className = 'right-rail-tab-label';
|
||||
tab.append(label);
|
||||
resizer?.observe(tab);
|
||||
entry = { tab, label };
|
||||
entries.set(panel.id, entry);
|
||||
return entry;
|
||||
@@ -113,15 +114,45 @@ 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 the rail's allocation, so the join is re-measured whenever
|
||||
// the strip, a tab or a panel changes size or the strip scrolls.
|
||||
const Resize = documentRef.defaultView?.ResizeObserver;
|
||||
const resizer =
|
||||
typeof Resize === 'function' ? new Resize(() => sync()) : null;
|
||||
resizer?.observe(strip);
|
||||
const observe = (panel) => {
|
||||
if (!observer || observed.has(panel)) return;
|
||||
if (observed.has(panel)) return;
|
||||
observed.add(panel);
|
||||
observer.observe(panel, {
|
||||
resizer?.observe(panel);
|
||||
observer?.observe(panel, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'hidden'],
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Publish the join on the rail for styles/right-rail-tabs.css: the open
|
||||
* tab's extent (--rail-tab-top, --rail-tab-bottom) and the open panel's
|
||||
* height (--rail-panel-height), in rail pixels. The panel outline is drawn
|
||||
* at that height with a notch at the tab, and the panel reaches down to it.
|
||||
* The rail itself can be taller than its panel in the focus layout.
|
||||
*/
|
||||
function placeJoin(tab, panel) {
|
||||
const style = rail.style;
|
||||
if (typeof style?.setProperty !== 'function') return;
|
||||
if (!tab || !Number.isFinite(tab.offsetTop)) {
|
||||
style.removeProperty('--rail-tab-top');
|
||||
style.removeProperty('--rail-tab-bottom');
|
||||
style.removeProperty('--rail-panel-height');
|
||||
return;
|
||||
}
|
||||
const top = tab.offsetTop - (strip.scrollTop || 0);
|
||||
style.setProperty('--rail-tab-top', `${top}px`);
|
||||
style.setProperty('--rail-tab-bottom', `${top + tab.offsetHeight}px`);
|
||||
style.setProperty('--rail-panel-height', `${panel.offsetHeight}px`);
|
||||
}
|
||||
|
||||
/** Reconcile tabs with the rail: order, visibility, selection, labels. */
|
||||
function sync() {
|
||||
if (destroyed) return;
|
||||
@@ -136,11 +167,13 @@ export function createRightRailTabs({
|
||||
// Move a tab only when it is out of place: reinserting the focused tab
|
||||
// would drop keyboard focus on every class change.
|
||||
let cursor = strip.firstElementChild;
|
||||
let open = null;
|
||||
for (const panel of panels) {
|
||||
const { tab, label } = 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-selected', String(!collapsed));
|
||||
@@ -153,9 +186,11 @@ export function createRightRailTabs({
|
||||
const live = new Set(panels.map((panel) => panel.id));
|
||||
for (const [id, entry] of entries) {
|
||||
if (live.has(id)) continue;
|
||||
resizer?.unobserve(entry.tab);
|
||||
entry.tab.remove();
|
||||
entries.delete(id);
|
||||
}
|
||||
placeJoin(open?.tab, open?.panel);
|
||||
}
|
||||
|
||||
const onClick = (event) => {
|
||||
@@ -177,8 +212,10 @@ export function createRightRailTabs({
|
||||
event.preventDefault();
|
||||
visible[next].focus();
|
||||
};
|
||||
const onScroll = () => sync();
|
||||
strip.addEventListener('click', onClick);
|
||||
strip.addEventListener('keydown', onKeydown);
|
||||
strip.addEventListener('scroll', onScroll, { passive: true });
|
||||
observer?.observe(rail, { childList: true });
|
||||
rail.prepend(strip);
|
||||
sync();
|
||||
@@ -190,10 +227,13 @@ export function createRightRailTabs({
|
||||
if (destroyed) return;
|
||||
destroyed = true;
|
||||
observer?.disconnect();
|
||||
resizer?.disconnect();
|
||||
strip.removeEventListener('click', onClick);
|
||||
strip.removeEventListener('keydown', onKeydown);
|
||||
strip.removeEventListener('scroll', onScroll);
|
||||
strip.remove();
|
||||
rail.removeAttribute(RIGHT_RAIL_TABS_ATTRIBUTE);
|
||||
placeJoin(null, null);
|
||||
entries.clear();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -322,6 +322,40 @@ test('a sync that changes nothing keeps the same tab elements in place', () => {
|
||||
assert.equal(f.doc.activeElement, f.tabFor('cctv-panel'));
|
||||
});
|
||||
|
||||
test('publishes the open tab and panel extent on the rail for the joined outline', () => {
|
||||
const f = fixture();
|
||||
const props = new Map();
|
||||
f.rail.style = {
|
||||
setProperty: (key, value) => props.set(key, value),
|
||||
removeProperty: (key) => props.delete(key),
|
||||
};
|
||||
const tab = f.tabFor('cctv-panel');
|
||||
tab.offsetTop = 86;
|
||||
tab.offsetHeight = 56;
|
||||
f.cctv.offsetHeight = 300;
|
||||
f.strip.scrollTop = 0;
|
||||
f.cctv.classList.remove('collapsed');
|
||||
f.tabs.sync();
|
||||
assert.deepEqual(
|
||||
[...props],
|
||||
[
|
||||
['--rail-tab-top', '86px'],
|
||||
['--rail-tab-bottom', '142px'],
|
||||
['--rail-panel-height', '300px'],
|
||||
],
|
||||
);
|
||||
f.strip.scrollTop = 20;
|
||||
f.tabs.sync();
|
||||
assert.equal(
|
||||
props.get('--rail-tab-top'),
|
||||
'66px',
|
||||
'strip scroll is subtracted',
|
||||
);
|
||||
f.cctv.classList.add('collapsed');
|
||||
f.tabs.sync();
|
||||
assert.deepEqual([...props], [], 'no open panel, no join');
|
||||
});
|
||||
|
||||
test('destroy removes the strip and stops listening', () => {
|
||||
const f = fixture();
|
||||
const tab = f.tabFor('cctv-panel');
|
||||
|
||||
@@ -4,36 +4,62 @@
|
||||
exclusive layout (rightPanelRail.js) hides the collapsed ones, so the strip
|
||||
is the only launcher. Phones keep the sheet shell (mobile-shell.css).
|
||||
|
||||
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. */
|
||||
The open tab and its panel read as one shape with one surface and one
|
||||
outline. The panel card keeps its glass surface but draws no border of its
|
||||
own; the rail's ::before draws the outline around the card with a notch
|
||||
where the tab joins, the open tab closes the shape with its own top, right
|
||||
and bottom borders, and two concave fillets (the tab's ::before/::after)
|
||||
round the inner corners. The strip publishes the join on the rail
|
||||
(rightRailTabs.js): --rail-tab-top/--rail-tab-bottom place the notch and
|
||||
set the panel's minimum height, so a short panel still reaches its tab,
|
||||
and --rail-panel-height sizes the outline, since the rail itself can be
|
||||
taller than its panel. The other tabs are borderless labels. `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-rail-panel-width: 330px;
|
||||
--right-rail-display-width: var(--display-panel-expanded-width);
|
||||
--right-rail-tabs-width: 34px;
|
||||
--right-rail-tabs-gap: 10px;
|
||||
/* The bridge the open tab reaches across to its panel. */
|
||||
--right-rail-tabs-gap: 8px;
|
||||
--right-rail-tabs-inset: calc(
|
||||
var(--right-rail-tabs-width) + var(--right-rail-tabs-gap)
|
||||
);
|
||||
--right-rail-tab-radius: 10px;
|
||||
--right-rail-fillet: 8px;
|
||||
--right-rail-corner: var(--panel-radius);
|
||||
--right-rail-notch-top: max(
|
||||
0px,
|
||||
calc(var(--rail-tab-top, 0px) - var(--right-rail-fillet))
|
||||
);
|
||||
--right-rail-notch-bottom: min(
|
||||
100%,
|
||||
calc(var(--rail-tab-bottom, 0px) + var(--right-rail-fillet))
|
||||
);
|
||||
/* Lower fillet, a short straight run, then the panel's corner. */
|
||||
--right-rail-panel-cover: calc(
|
||||
var(--rail-tab-bottom, 0px) + var(--right-rail-fillet) + 4px +
|
||||
var(--right-rail-corner)
|
||||
);
|
||||
width: calc(var(--right-rail-panel-width) + var(--right-rail-tabs-inset));
|
||||
padding-right: var(--right-rail-tabs-inset);
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber'] #right-context-rail[data-rail-tabs] {
|
||||
--right-rail-panel-width: 312px;
|
||||
width: calc(var(--right-rail-panel-width) + var(--right-rail-tabs-inset));
|
||||
}
|
||||
|
||||
#right-rail-tabs {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 120;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: var(--right-rail-tabs-width);
|
||||
box-sizing: border-box;
|
||||
width: var(--right-rail-tabs-inset);
|
||||
max-height: var(--right-stack-max-height, 70vh);
|
||||
/* Room for the open tab's bridge on the left and its lower fillet below. */
|
||||
padding: 0 0 calc(var(--right-rail-fillet) + 1px) var(--right-rail-tabs-gap);
|
||||
overflow: hidden auto;
|
||||
scrollbar-width: none;
|
||||
pointer-events: auto;
|
||||
@@ -49,28 +75,24 @@
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
width: var(--right-rail-tabs-width);
|
||||
min-height: 56px;
|
||||
padding: 12px 0;
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 9px;
|
||||
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);
|
||||
border: 1px solid transparent;
|
||||
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: 2.2px;
|
||||
text-transform: uppercase;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
border-color var(--transition-fast),
|
||||
box-shadow var(--transition-fast),
|
||||
color var(--transition-fast);
|
||||
}
|
||||
|
||||
@@ -81,41 +103,229 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.right-rail-tab:hover {
|
||||
border-color: var(--glass-border-hover);
|
||||
.right-rail-tab:not([aria-selected='true']):hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* The open tab: the panel's surface continued across the bridge, closed by
|
||||
its own top, right and bottom borders. The label stays in the strip's
|
||||
column. */
|
||||
.right-rail-tab[aria-selected='true'] {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
box-shadow: 0 0 12px rgba(0, 212, 255, 0.15);
|
||||
z-index: 1;
|
||||
width: calc(var(--right-rail-tabs-width) + var(--right-rail-tabs-gap));
|
||||
margin-left: calc(-1 * var(--right-rail-tabs-gap));
|
||||
padding-left: var(--right-rail-tabs-gap);
|
||||
border-color: var(--glass-border);
|
||||
border-left: 0;
|
||||
border-radius: 0 var(--right-rail-tab-radius) var(--right-rail-tab-radius) 0;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(24px) saturate(1.4);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(1.4);
|
||||
color: var(--accent);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
/* Concave fillets where the tab's top and bottom edges meet the panel's
|
||||
right edge. Each is a square outside the tab, overlapping the panel's
|
||||
border column by one pixel and the tab's border row by one pixel, painted
|
||||
with the surface beyond a quarter circle and the border along it. */
|
||||
.right-rail-tab[aria-selected='true']::before,
|
||||
.right-rail-tab[aria-selected='true']::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
width: calc(var(--right-rail-fillet) + 1px);
|
||||
height: calc(var(--right-rail-fillet) + 1px);
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at var(--right-rail-fillet-origin),
|
||||
transparent calc(var(--right-rail-fillet) - 0.25px),
|
||||
var(--glass-border) calc(var(--right-rail-fillet) + 0.25px),
|
||||
var(--glass-border) calc(var(--right-rail-fillet) + 0.75px),
|
||||
transparent calc(var(--right-rail-fillet) + 1.25px)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--right-rail-fillet-origin),
|
||||
transparent calc(var(--right-rail-fillet) - 0.25px),
|
||||
var(--glass-bg) calc(var(--right-rail-fillet) + 0.25px)
|
||||
);
|
||||
}
|
||||
|
||||
.right-rail-tab[aria-selected='true']::before {
|
||||
--right-rail-fillet-origin: 100% 0;
|
||||
top: calc(-1 * (var(--right-rail-fillet) + 1px));
|
||||
}
|
||||
|
||||
.right-rail-tab[aria-selected='true']::after {
|
||||
--right-rail-fillet-origin: 100% 100%;
|
||||
bottom: calc(-1 * (var(--right-rail-fillet) + 1px));
|
||||
}
|
||||
|
||||
/* The outline around the open panel, notched where the tab joins. */
|
||||
body:not(.mobile-shell)
|
||||
#right-context-rail[data-rail-tabs]:has(
|
||||
> [data-panel-id]:not(.collapsed):not([hidden])
|
||||
)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: var(--right-rail-tabs-inset);
|
||||
z-index: 130;
|
||||
box-sizing: border-box;
|
||||
width: var(--right-rail-panel-width);
|
||||
/* The rail can be taller than its panel in the focus layout. */
|
||||
height: var(--rail-panel-height, 100%);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--right-rail-corner);
|
||||
clip-path: polygon(
|
||||
0 0,
|
||||
100% 0,
|
||||
100% var(--right-rail-notch-top),
|
||||
calc(100% - 2px) var(--right-rail-notch-top),
|
||||
calc(100% - 2px) var(--right-rail-notch-bottom),
|
||||
100% var(--right-rail-notch-bottom),
|
||||
100% 100%,
|
||||
0 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Display is narrower than the rail and sits against the strip. */
|
||||
body:not(.mobile-shell)
|
||||
#right-context-rail[data-rail-tabs]:has(
|
||||
> #pp-toggles:not(.collapsed)
|
||||
)::before {
|
||||
width: min(var(--right-rail-panel-width), var(--right-rail-display-width));
|
||||
}
|
||||
|
||||
/* The first tab starts level with the panel's top edge: the outline's top
|
||||
runs straight into the tab's top border, so no corner and no upper fillet. */
|
||||
body:not(.mobile-shell)
|
||||
#right-context-rail[data-rail-tabs]:has(
|
||||
> #right-rail-tabs > .right-rail-tab:first-child[aria-selected='true']
|
||||
)::before {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
#right-rail-tabs > .right-rail-tab:first-child[aria-selected='true']::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
/* The open panel's card keeps its surface and leaves the outline to the rail.
|
||||
Cyber draws Display's and Context's frames on a ::before. */
|
||||
body:not(.mobile-shell)
|
||||
#right-context-rail[data-rail-tabs]
|
||||
> #pp-toggles:not(.collapsed),
|
||||
body:not(.mobile-shell)
|
||||
#right-context-rail[data-rail-tabs]
|
||||
> #pp-toggles:not(.collapsed)::before,
|
||||
body:not(.mobile-shell)
|
||||
#right-context-rail[data-rail-tabs]
|
||||
> [data-panel-id]:not(.collapsed)
|
||||
> :is(
|
||||
.cctv-panel-inner,
|
||||
.street-level-panel-inner,
|
||||
.weather-panel-inner,
|
||||
.recent-imagery-panel-inner,
|
||||
.global-context-panel-inner,
|
||||
[data-panel-surface]
|
||||
),
|
||||
body:not(.mobile-shell)
|
||||
#right-context-rail[data-rail-tabs]
|
||||
> [data-panel-id]:not(.collapsed)
|
||||
> :is(.global-context-panel-inner, [data-panel-surface])::before {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* The panel reaches at least to its tab's lower fillet and corner. The
|
||||
wrapper carries the floor for the rail's allocated (focus) heights and the
|
||||
card for its natural height. */
|
||||
body:not(.mobile-shell)
|
||||
#right-context-rail[data-rail-tabs]
|
||||
> [data-panel-id]:not(.collapsed),
|
||||
body:not(.mobile-shell)
|
||||
#right-context-rail[data-rail-tabs]
|
||||
> [data-panel-id]:not(.collapsed)
|
||||
> :is(
|
||||
.cctv-panel-inner,
|
||||
.street-level-panel-inner,
|
||||
.weather-panel-inner,
|
||||
.recent-imagery-panel-inner,
|
||||
.global-context-panel-inner,
|
||||
[data-panel-surface]
|
||||
) {
|
||||
min-height: var(--right-rail-panel-cover);
|
||||
}
|
||||
|
||||
/* ── Cyber skin ────────────────────────────────────────────────────────── */
|
||||
/* Cyber panels are chamfered frames (--cyber-panel-frame), not rounded
|
||||
cards: square joins, no fillets, and the strip starts below the frame's
|
||||
upper chamfer so the first tab meets a straight edge. */
|
||||
:root[data-ui-theme='cyber'] #right-context-rail[data-rail-tabs] {
|
||||
--right-rail-panel-width: 312px;
|
||||
--right-rail-display-width: var(--cyber-right-panel-width);
|
||||
--right-rail-tab-radius: var(--btn-radius);
|
||||
--right-rail-fillet: 0px;
|
||||
--right-rail-corner: 10px;
|
||||
--right-rail-notch-top: max(10px, var(--rail-tab-top, 10px));
|
||||
--right-rail-notch-bottom: min(
|
||||
calc(100% - 10px),
|
||||
var(--rail-tab-bottom, 10px)
|
||||
);
|
||||
width: calc(var(--right-rail-panel-width) + var(--right-rail-tabs-inset));
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber'] #right-rail-tabs {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber'] .right-rail-tab {
|
||||
border-color: var(--glass-border);
|
||||
border-radius: var(--btn-radius);
|
||||
background-color: var(--cyber-panel-surface);
|
||||
background-image: var(--cyber-panel-surface-detail);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 0, 0, 0.32) inset,
|
||||
0 8px 20px rgba(0, 0, 0, 0.28);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber'] .right-rail-tab:not([aria-selected='true']):hover {
|
||||
background: rgba(226, 235, 236, 0.08);
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber'] .right-rail-tab[aria-selected='true'] {
|
||||
border-color: var(--cyber-red);
|
||||
background-color: var(--cyber-panel-surface);
|
||||
box-shadow:
|
||||
3px 0 0 var(--cyber-red) inset,
|
||||
0 8px 20px rgba(0, 0, 0, 0.28);
|
||||
border-color: var(--glass-border);
|
||||
border-radius: 0 var(--btn-radius) var(--btn-radius) 0;
|
||||
background: var(--cyber-panel-surface);
|
||||
box-shadow: -3px 0 0 var(--cyber-red) inset;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
color: var(--cyber-red-bright);
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber']
|
||||
body:not(.mobile-shell)
|
||||
#right-context-rail[data-rail-tabs]:has(
|
||||
> [data-panel-id]:not(.collapsed):not([hidden])
|
||||
)::before {
|
||||
border-radius: 0;
|
||||
/* --cyber-panel-frame with the tab's notch on its right edge. */
|
||||
clip-path: polygon(
|
||||
0 10px,
|
||||
10px 0,
|
||||
calc(50% - 25px) 0,
|
||||
calc(50% - 18px) 6px,
|
||||
calc(50% + 18px) 6px,
|
||||
calc(50% + 25px) 0,
|
||||
calc(100% - 10px) 0,
|
||||
100% 10px,
|
||||
100% var(--right-rail-notch-top),
|
||||
calc(100% - 2px) var(--right-rail-notch-top),
|
||||
calc(100% - 2px) var(--right-rail-notch-bottom),
|
||||
100% var(--right-rail-notch-bottom),
|
||||
100% calc(100% - 10px),
|
||||
calc(100% - 10px) 100%,
|
||||
10px 100%,
|
||||
0 calc(100% - 10px)
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Phones keep the sheet shell ───────────────────────────────────────── */
|
||||
body.mobile-shell #right-context-rail {
|
||||
padding-right: 0;
|
||||
|
||||
Reference in new issue
Block a user