mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(ui): harden Cyber panel layout and preset restoration
This commit is contained in:
1 parent
e7707d9a0f
commit
e03bf008f3
8 files changed
+695
-16
No files matched your search
+205
-4
@@ -9,6 +9,9 @@ import {
|
||||
normalizeHudLayout,
|
||||
} from '../hudLayouts.js';
|
||||
import { GEV_ACTION_SCHEMAS } from '../voice/actionSchemas.js';
|
||||
import { createGevActionRunner } from '../voice/gevActions.js';
|
||||
import { StyleManager } from './applicationShell.js';
|
||||
import { bindDisplayControls } from './displayControls.js';
|
||||
import { VisualSettings } from './visualSettings.js';
|
||||
import { STYLES } from './visualPresets.js';
|
||||
|
||||
@@ -137,6 +140,102 @@ test('non-Cyber style presets and explicit HUD choices remain authoritative', (t
|
||||
}
|
||||
});
|
||||
|
||||
test('leaving an explicitly selected Cyber layout restores the prior visual preset', (t) => {
|
||||
const { owner, hud } = styleOwner(t, 'tactical');
|
||||
owner.setStyle('noir');
|
||||
|
||||
owner._setHudVariant('cyber', { applyVisualDefaults: true });
|
||||
assert.equal(owner.activeStyle, 'thermal');
|
||||
assert.equal(owner._preCyberStyle, 'noir');
|
||||
|
||||
// A temporary choice made while Cyber is active does not replace the preset
|
||||
// that was active before the user entered Cyber.
|
||||
owner.setStyle('snow');
|
||||
owner._setHudVariant('cyber', { applyVisualDefaults: true });
|
||||
assert.equal(owner._preCyberStyle, 'noir');
|
||||
owner._setHudVariant('operator', { applyVisualDefaults: true });
|
||||
assert.equal(hud.getVariant(), 'operator');
|
||||
assert.equal(owner.activeStyle, 'noir');
|
||||
assert.equal(owner._preCyberStyle, null);
|
||||
});
|
||||
|
||||
test('Display and voice HUD routes preserve explicit Cyber entry and exit authority', async (t) => {
|
||||
const makeOwner = () => {
|
||||
const { owner, hud } = styleOwner(t, 'tactical');
|
||||
owner.setStyle('noir');
|
||||
owner.setHudLayout = (...args) =>
|
||||
StyleManager.prototype.setHudLayout.call(owner, ...args);
|
||||
owner.getControlState = () => ({
|
||||
hud: { layout: hud.getVariant(), visible: true },
|
||||
});
|
||||
return { owner, hud };
|
||||
};
|
||||
|
||||
const display = makeOwner();
|
||||
const hudLayout = new EventTarget();
|
||||
hudLayout.value = 'cyber';
|
||||
const controls = bindDisplayControls({
|
||||
elements: { hudLayout },
|
||||
actions: { setHudLayout: (value) => display.owner.setHudLayout(value) },
|
||||
});
|
||||
hudLayout.dispatchEvent(new Event('change'));
|
||||
assert.equal(display.hud.getVariant(), 'cyber');
|
||||
assert.equal(display.owner.activeStyle, 'thermal');
|
||||
hudLayout.value = 'operator';
|
||||
hudLayout.dispatchEvent(new Event('change'));
|
||||
assert.equal(display.hud.getVariant(), 'operator');
|
||||
assert.equal(display.owner.activeStyle, 'noir');
|
||||
controls.destroy();
|
||||
|
||||
const voice = makeOwner();
|
||||
const eventSource = { addEventListener() {}, removeEventListener() {} };
|
||||
const viewer = {
|
||||
camera: { moveEnd: { addEventListener() {} } },
|
||||
clock: { onTick: { addEventListener: () => () => {} } },
|
||||
scene: { canvas: eventSource },
|
||||
};
|
||||
const runVoiceAction = createGevActionRunner({
|
||||
viewer,
|
||||
styleManager: voice.owner,
|
||||
dataManager: {},
|
||||
});
|
||||
const entered = await runVoiceAction('set_hud', { layout: 'cyber' });
|
||||
assert.equal(entered.ok, true);
|
||||
assert.equal(voice.hud.getVariant(), 'cyber');
|
||||
assert.equal(voice.owner.activeStyle, 'thermal');
|
||||
const exited = await runVoiceAction('set_hud', { layout: 'operator' });
|
||||
assert.equal(exited.ok, true);
|
||||
assert.equal(voice.hud.getVariant(), 'operator');
|
||||
assert.equal(voice.owner.activeStyle, 'noir');
|
||||
});
|
||||
|
||||
test('scene and share HUD transitions do not restore a local pre-Cyber preset', async (t) => {
|
||||
const { owner, hud } = styleOwner(t, 'tactical');
|
||||
owner.setStyle('noir');
|
||||
owner._setHudVariant('cyber', { applyVisualDefaults: true });
|
||||
|
||||
await owner.applyVisualState({ style: 'retro', hud: { variant: 'cyber' } });
|
||||
assert.equal(hud.getVariant(), 'cyber');
|
||||
assert.equal(owner.activeStyle, 'retro');
|
||||
assert.equal(owner._preCyberStyle, null);
|
||||
owner._setHudVariant('operator', { applyVisualDefaults: true });
|
||||
assert.equal(owner.activeStyle, 'retro');
|
||||
|
||||
owner.setStyle('surveillance');
|
||||
owner._setHudVariant('cyber', { applyVisualDefaults: true });
|
||||
assert.equal(owner._preCyberStyle, 'surveillance');
|
||||
await owner.restoreShareState({
|
||||
style: 'thermal',
|
||||
hudVariant: 'cyber',
|
||||
});
|
||||
assert.equal(hud.getVariant(), 'cyber');
|
||||
assert.equal(owner.activeStyle, 'thermal');
|
||||
assert.equal(owner._preCyberStyle, null);
|
||||
owner._setHudVariant('tactical', { applyVisualDefaults: true });
|
||||
assert.equal(owner.activeStyle, 'thermal');
|
||||
assert.equal(owner._preCyberStyle, null);
|
||||
});
|
||||
|
||||
test('scene and share restoration override Cyber and preserve restored Cyber parameters', async (t) => {
|
||||
const { owner, hud } = styleOwner(t);
|
||||
await owner.applyVisualState({ style: 'retro', hud: { variant: 'minimal' } });
|
||||
@@ -179,6 +278,104 @@ test('Cyber setup uses a compact toolbar icon and highlights the north-up letter
|
||||
});
|
||||
const cyberStyles = read('./styles/cyber.css');
|
||||
|
||||
// Source guards supplement rendered popup/hit-target checks. Inspect one rule
|
||||
// at a time so a safe declaration on a child cannot excuse a clipped ancestor.
|
||||
function cssRuleBody(styles, selector) {
|
||||
const match = styles.match(new RegExp(`${selector.source}\\s*\\{([^}]+)\\}`));
|
||||
assert.ok(match, `Missing CSS rule: ${selector.source}`);
|
||||
return match[1];
|
||||
}
|
||||
|
||||
test('Cyber Voice clips its decoration without clipping descendant help and errors', () => {
|
||||
const owner = cssRuleBody(
|
||||
cyberStyles,
|
||||
/:root\[data-ui-theme='cyber'\] #command-dock > #gev-voice-control/,
|
||||
);
|
||||
assert.match(owner, /isolation:\s*isolate;/);
|
||||
assert.match(owner, /overflow:\s*visible;/);
|
||||
assert.match(owner, /clip-path:\s*none;/);
|
||||
|
||||
const decoration = cssRuleBody(
|
||||
cyberStyles,
|
||||
/:root\[data-ui-theme='cyber'\] #command-dock > #gev-voice-control::before/,
|
||||
);
|
||||
assert.match(decoration, /clip-path:\s*polygon\(/);
|
||||
assert.match(decoration, /pointer-events:\s*none;/);
|
||||
assert.match(decoration, /inset:\s*0;/);
|
||||
assert.match(decoration, /z-index:\s*-1;/);
|
||||
});
|
||||
|
||||
test('Cyber dock pins escape the tray mask and its decoration overrides the legacy arrow geometry', () => {
|
||||
const owner = cssRuleBody(
|
||||
cyberStyles,
|
||||
/:root\[data-ui-theme='cyber'\] #command-dock \.dock-popover-content/,
|
||||
);
|
||||
assert.match(owner, /clip-path:\s*none;/);
|
||||
assert.match(owner, /isolation:\s*isolate;/);
|
||||
|
||||
// Including the tray owners beats #control-panel's more specific old
|
||||
// ::after arrow positioning. Otherwise left:auto can collapse the frame.
|
||||
const decoration = cssRuleBody(
|
||||
cyberStyles,
|
||||
/:root\[data-ui-theme='cyber'\]\s+#command-dock\s+:is\(#location-bar, #control-panel\)\s+\.dock-popover-content::after/,
|
||||
);
|
||||
assert.match(decoration, /inset:\s*0;/);
|
||||
assert.match(decoration, /width:\s*auto;/);
|
||||
assert.match(decoration, /height:\s*auto;/);
|
||||
assert.match(decoration, /transform:\s*none;/);
|
||||
assert.match(decoration, /clip-path:\s*var\(--cyber-panel-frame\);/);
|
||||
assert.match(decoration, /pointer-events:\s*none;/);
|
||||
});
|
||||
|
||||
test('Cyber lower-left telemetry tracks the credit anchor with clearance for its logo row', () => {
|
||||
const telemetry = cssRuleBody(
|
||||
cyberStyles,
|
||||
/#intel-hud\[data-variant='cyber'\] \.hud-bottom-left/,
|
||||
);
|
||||
const offset = telemetry.match(/bottom:\s*calc\(2vh \+ ([\d.]+)rem\);/);
|
||||
assert.ok(offset, 'telemetry must retain the attribution viewport anchor');
|
||||
const credits = read('./styles/command-dock-trays.css');
|
||||
const creditOffsets = [
|
||||
...credits.matchAll(
|
||||
/#cesium-credits\s*\{[^}]*bottom:\s*calc\(2vh \+ ([\d.]+)rem\);/g,
|
||||
),
|
||||
];
|
||||
assert.ok(creditOffsets.length > 0);
|
||||
for (const credit of creditOffsets) {
|
||||
assert.ok(
|
||||
Number(offset[1]) - Number(credit[1]) >= 2.75,
|
||||
'reserve the 28px logo row, baseline descent and 12px clearance at the 16px root size',
|
||||
);
|
||||
}
|
||||
assert.doesNotMatch(cyberStyles, /\.hud-bottom-left\s*\{\s*bottom:\s*60px;/);
|
||||
});
|
||||
|
||||
test('shared panel hooks affect only opted-in rail surfaces without overriding disclosure and concealment', () => {
|
||||
const styles = read('./styles/panel-surfaces.css');
|
||||
const rail = cssRuleBody(
|
||||
styles,
|
||||
/:is\(#left-panel-stack, #right-context-rail\)\s+> \[data-panel-id\]:has\(> \[data-panel-surface\]\)/,
|
||||
);
|
||||
assert.match(rail, /pointer-events:\s*auto;/);
|
||||
assert.doesNotMatch(rail, /(?:display|visibility):/);
|
||||
assert.doesNotMatch(
|
||||
styles,
|
||||
/:is\(#left-panel-stack, #right-context-rail\)\s*> \[data-panel-id\]\s*\{/,
|
||||
);
|
||||
assert.match(
|
||||
styles,
|
||||
/\[data-panel-surface\]:not\(\[hidden\]\):not\(dialog:not\(\[open\]\)\)\s*\{\s*display: flex;/,
|
||||
);
|
||||
assert.match(
|
||||
styles,
|
||||
/dialog\[data-panel-surface\]:not\(\[open\]\)\s*\{\s*display: none !important;/,
|
||||
);
|
||||
assert.match(
|
||||
styles,
|
||||
/body:is\(\.ui-clean-view, \.recording-mode\)[^{]+\{[^}]*visibility: hidden !important;[^}]*pointer-events: none !important;/,
|
||||
);
|
||||
});
|
||||
|
||||
test('Cyber clipped expand controls retain an inset keyboard-focus outline in map and cockpit', () => {
|
||||
assert.match(
|
||||
cyberStyles,
|
||||
@@ -330,6 +527,7 @@ test('Cyber visual defaults update the style, thermal palette and render state',
|
||||
test('the Display selector, voice schema and final stylesheet expose Cyber', () => {
|
||||
const display = read('./templates/display-controls.html');
|
||||
const stylesheet = read('../../style.css');
|
||||
const rightRail = read('./rightPanelRail.js');
|
||||
const hud = read('../hud.js');
|
||||
const detection = read('../data/detection.js');
|
||||
const civilRendering = read('../layers/flights/rendering.js');
|
||||
@@ -415,21 +613,24 @@ test('the Display selector, voice schema and final stylesheet expose Cyber', ()
|
||||
assert.match(cyberStyles, /--cyber-hud-card-top: max\(100px, 10\.25vh\);/);
|
||||
assert.match(
|
||||
cyberStyles,
|
||||
/--cyber-left-rail-top: max\([\s\S]*?252px,[\s\S]*?calc\(var\(--cyber-hud-card-top\) \+ 146px\)/,
|
||||
/--cyber-left-rail-top: max\([\s\S]*?220px,[\s\S]*?calc\(var\(--cyber-hud-card-top\) \+ 120px\)/,
|
||||
);
|
||||
assert.match(
|
||||
cyberStyles,
|
||||
/\.hud-top-left \{[\s\S]*?top: var\(--cyber-hud-card-top\);/,
|
||||
);
|
||||
assert.match(cyberStyles, /\.hud-bottom-left \{\s*bottom: 60px;/);
|
||||
assert.match(
|
||||
cyberStyles,
|
||||
/#left-panel-stack \{[\s\S]*?top: var\(--cyber-left-rail-top\) !important;/,
|
||||
/#left-panel-stack \{[\s\S]*?top: var\(--cyber-left-rail-top\) !important;[\s\S]*?bottom: calc\(2vh \+ 13rem\);/,
|
||||
);
|
||||
assert.match(
|
||||
cyberStyles,
|
||||
/\.hud-left-edge \{\s*top: calc\(var\(--cyber-left-rail-top\) - 18px\);\s*left: 14px;/,
|
||||
);
|
||||
assert.match(
|
||||
rightRail,
|
||||
/const leftStackTop = leftStack\?\.getBoundingClientRect\(\)\.top;[\s\S]*?const alignedTop = Number\.isFinite\(leftStackTop\)[\s\S]*?baseTop: alignedTop,/,
|
||||
);
|
||||
assert.match(
|
||||
cyberStyles,
|
||||
/--right-display-collapsed-width: var\(--cyber-panel-collapsed-width\);/,
|
||||
@@ -453,7 +654,7 @@ test('the Display selector, voice schema and final stylesheet expose Cyber', ()
|
||||
assert.match(cyberStyles, /#left-panel-stack:has/);
|
||||
assert.match(cyberStyles, /left: 54px;/);
|
||||
assert.match(cyberStyles, /right: 62px;/);
|
||||
assert.match(cyberStyles, /bottom: calc\(2vh \+ 11rem\);/);
|
||||
assert.match(cyberStyles, /bottom: calc\(2vh \+ 13rem\);/);
|
||||
assert.doesNotMatch(cyberStyles, /NAV 01 \/\/ COMMAND BUS \/\/ VIS 03/);
|
||||
assert.match(cyberStyles, /data-layer-id='satellites'/);
|
||||
assert.match(cyberStyles, /#search-toggle::before/);
|
||||
|
||||
+63
-10
@@ -185,6 +185,11 @@
|
||||
0 8px 20px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
/* Compact Cyber rails must wrap multi-word actions within their own button. */
|
||||
:root[data-ui-theme='cyber'] #scene-panel .scene-controls .scene-btn {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* Carry the raised voice-module silhouette across the primary panel system.
|
||||
The six-pixel calibration bay is decorative and stays outside panel content. */
|
||||
:root[data-ui-theme='cyber']
|
||||
@@ -924,7 +929,24 @@
|
||||
--cyber-voice-wave: 105, 203, 209;
|
||||
--cyber-voice-peak: 226, 240, 240;
|
||||
--cyber-voice-alert: 196, 84, 84;
|
||||
border-color: rgba(196, 84, 84, 0.58);
|
||||
/* Popups extend above this root. Only its decorative layer may be clipped. */
|
||||
isolation: isolate;
|
||||
overflow: visible;
|
||||
clip-path: none;
|
||||
border-color: transparent;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber'] #command-dock > #gev-voice-control::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
border: 1px solid rgba(196, 84, 84, 0.58);
|
||||
border-radius: 0;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(196, 84, 84, 0.14), transparent 34%),
|
||||
@@ -1192,8 +1214,30 @@
|
||||
|
||||
:root[data-ui-theme='cyber'] #command-dock .dock-popover-content {
|
||||
padding-top: 1.65rem;
|
||||
border: 1px solid rgba(196, 84, 84, 0.62);
|
||||
/* The pin straddles the tray edge; keep it outside the decorative mask. */
|
||||
isolation: isolate;
|
||||
clip-path: none;
|
||||
border-color: transparent;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber']
|
||||
#command-dock
|
||||
:is(#location-bar, #control-panel)
|
||||
.dock-popover-content::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
width: auto;
|
||||
height: auto;
|
||||
transform: none;
|
||||
pointer-events: none;
|
||||
border: 1px solid rgba(196, 84, 84, 0.62);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(62, 86, 94, 0.22), transparent 46%),
|
||||
linear-gradient(rgba(188, 211, 218, 0.045) 1px, transparent 1px),
|
||||
@@ -1456,6 +1500,12 @@
|
||||
bottom: calc(2vh + 6.5rem);
|
||||
}
|
||||
|
||||
/* Attribution's logo row extends above its text baseline. Reserve the whole
|
||||
row plus a gap above the dock-relative 2vh + 5rem credit position. */
|
||||
#intel-hud[data-variant='cyber'] .hud-bottom-left {
|
||||
bottom: calc(2vh + 7.75rem);
|
||||
}
|
||||
|
||||
#intel-hud[data-variant='cyber'] .hud-corner {
|
||||
position: fixed;
|
||||
isolation: isolate;
|
||||
@@ -1493,12 +1543,21 @@
|
||||
|
||||
/* Cyber also owns a distinct desktop composition. Existing components keep
|
||||
their controls and semantics, but move onto asymmetric mission-deck rails. */
|
||||
@media (min-width: 961px) and (max-height: 649px) {
|
||||
/* The compact-height layout drops the desktop rail composition one pixel
|
||||
below its 650px threshold. Keep the left rail inside the same telemetry
|
||||
and attribution-safe floor while its panel bodies take over scrolling. */
|
||||
:root[data-ui-theme='cyber'] #left-panel-stack {
|
||||
bottom: calc(2vh + 13rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 961px) and (min-height: 650px) {
|
||||
:root[data-ui-theme='cyber'] {
|
||||
/* Compose the upper mission card and both panel rails as one compact lane.
|
||||
The shared rail anchor keeps expanded left/right panels aligned. */
|
||||
--cyber-hud-card-top: max(100px, 10.25vh);
|
||||
--cyber-left-rail-top: max(252px, calc(var(--cyber-hud-card-top) + 146px));
|
||||
--cyber-left-rail-top: max(220px, calc(var(--cyber-hud-card-top) + 120px));
|
||||
}
|
||||
|
||||
/* Keep the rotated COLL / ONA telemetry in the upper clearance lane. Its
|
||||
@@ -1510,7 +1569,7 @@
|
||||
|
||||
:root[data-ui-theme='cyber'] #left-panel-stack {
|
||||
top: var(--cyber-left-rail-top) !important;
|
||||
bottom: calc(2vh + 11rem);
|
||||
bottom: calc(2vh + 13rem);
|
||||
left: 54px;
|
||||
width: 300px;
|
||||
padding-left: 10px;
|
||||
@@ -1709,12 +1768,6 @@
|
||||
right: 28px;
|
||||
bottom: 28px;
|
||||
}
|
||||
|
||||
/* Match the lower telemetry cards by their top edge while leaving the
|
||||
shorter coordinate card clear of Cesium attribution. */
|
||||
#intel-hud[data-variant='cyber'] .hud-bottom-left {
|
||||
bottom: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
/* Shared structural hooks for panels admitted by the existing rail owners.
|
||||
Specific legacy component rules retain priority; no panel IDs are registered
|
||||
here. The rail itself remains pointer-transparent over the globe. */
|
||||
:is(#left-panel-stack, #right-context-rail)
|
||||
> [data-panel-id]:has(> [data-panel-surface]) {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
flex: 0 0 auto;
|
||||
min-height: 0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#left-panel-stack
|
||||
> [data-panel-id]:has(> [data-panel-surface]):not(.collapsed) {
|
||||
flex: 0 1 var(--left-panel-allocated-height, auto);
|
||||
height: var(--left-panel-allocated-height, auto);
|
||||
max-height: var(--left-panel-allocated-height, 100%);
|
||||
}
|
||||
|
||||
#left-panel-stack.layout-tail
|
||||
> [data-panel-id]:has(> [data-panel-surface]):not(.collapsed) {
|
||||
flex: 0 0 var(--left-panel-allocated-height);
|
||||
}
|
||||
|
||||
#right-context-rail.layout-focus
|
||||
> [data-panel-id]:has(> [data-panel-surface]):not(.collapsed) {
|
||||
flex: 0 1 var(--right-panel-allocated-height);
|
||||
height: var(--right-panel-allocated-height);
|
||||
max-height: var(--right-panel-allocated-height);
|
||||
}
|
||||
|
||||
/* Opt-in skin: the positioned surface can contain an outward popup. Only the
|
||||
non-interactive decoration is clipped; only data-panel-body scrolls. */
|
||||
:root {
|
||||
--panel-surface-font: var(--font-sans);
|
||||
--panel-surface-detail: none;
|
||||
--panel-surface-frame: none;
|
||||
--panel-surface-shadow: 0 8px 24px rgba(0, 0, 0, 0.38);
|
||||
--panel-surface-title: var(--text-secondary);
|
||||
--panel-surface-error: #ff9b8e;
|
||||
--panel-surface-width: 300px;
|
||||
--panel-surface-collapsed-width: var(--left-collapsed-width);
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber'] {
|
||||
--panel-surface-font: var(--font-mono);
|
||||
--panel-surface-detail: var(--cyber-panel-surface-detail);
|
||||
--panel-surface-frame: var(--cyber-panel-frame);
|
||||
--panel-surface-shadow: var(--cyber-panel-shadow);
|
||||
--panel-surface-title: var(--cyber-red-bright);
|
||||
--panel-surface-error: var(--cyber-red-bright);
|
||||
--panel-surface-width: var(--cyber-panel-width);
|
||||
--panel-surface-collapsed-width: var(--cyber-panel-collapsed-width);
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber']
|
||||
#right-context-rail
|
||||
> [data-panel-id]:has(> [data-panel-surface]) {
|
||||
--panel-surface-width: var(--cyber-right-panel-width);
|
||||
}
|
||||
|
||||
:is([data-panel-surface], [data-panel-popup]) {
|
||||
isolation: isolate;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--panel-radius);
|
||||
background: none;
|
||||
color: var(--text-primary);
|
||||
font: 12px/1.5 var(--panel-surface-font);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
[data-panel-surface]:not(dialog) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:is([data-panel-surface], [data-panel-popup])::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
z-index: -1;
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: inherit;
|
||||
background-color: var(--glass-bg);
|
||||
background-image: var(--panel-surface-detail);
|
||||
box-shadow: var(--panel-surface-shadow);
|
||||
clip-path: var(--panel-surface-frame);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* A closed native dialog must retain the browser's display:none. */
|
||||
[data-panel-surface]:not([hidden]):not(dialog:not([open])) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
:is(#left-panel-stack, #right-context-rail)
|
||||
> [data-panel-id]:has(> [data-panel-surface]) {
|
||||
width: min(100%, var(--panel-expanded-width, var(--panel-surface-width)));
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:is(#left-panel-stack, #right-context-rail)
|
||||
> [data-panel-id]
|
||||
> [data-panel-surface] {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
:is(#left-panel-stack, #right-context-rail)
|
||||
> [data-panel-id].collapsed:has(> [data-panel-surface]) {
|
||||
width: min(
|
||||
100%,
|
||||
var(--panel-collapsed-width, var(--panel-surface-collapsed-width))
|
||||
);
|
||||
}
|
||||
|
||||
#right-context-rail[data-rail-measuring]
|
||||
> [data-panel-id]
|
||||
> [data-panel-surface] {
|
||||
height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
[data-panel-header] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
gap: 8px;
|
||||
min-height: 30px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.panel-surface-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: var(--panel-surface-title);
|
||||
font: 600 11px/1.4 var(--font-mono);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
[data-panel-body] {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--glass-border-hover) transparent;
|
||||
/* Keep an inset focus ring inside the intentional scrolling boundary. */
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.panel-surface-control {
|
||||
min-width: 0;
|
||||
min-height: 30px;
|
||||
max-width: 100%;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--btn-radius);
|
||||
background: var(--bg-dark);
|
||||
color: var(--text-primary);
|
||||
font: inherit;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
button.panel-surface-control {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.panel-surface-control:hover:not(:disabled):not([aria-disabled='true']) {
|
||||
border-color: var(--glass-border-hover);
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
|
||||
.panel-surface-control:focus-visible {
|
||||
outline: 2px solid var(--text-primary);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
.panel-surface-control:is(:disabled, [aria-disabled='true']) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.panel-surface-error,
|
||||
.panel-surface-control[aria-invalid='true'] {
|
||||
color: var(--panel-surface-error);
|
||||
border-color: var(--panel-surface-error);
|
||||
}
|
||||
|
||||
[data-panel-popup] {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
/* The owner supplies placement, width, collision handling and disclosure. */
|
||||
}
|
||||
|
||||
[data-panel-surface].collapsed > :is([data-panel-body], [data-panel-popup]),
|
||||
[data-panel-id].collapsed
|
||||
> [data-panel-surface]
|
||||
> :is([data-panel-body], [data-panel-popup]),
|
||||
:is(#left-panel-stack, #right-context-rail)
|
||||
> [data-panel-id]:has(> [data-panel-surface])[hidden],
|
||||
:is(
|
||||
[data-panel-surface],
|
||||
[data-panel-header],
|
||||
[data-panel-body],
|
||||
[data-panel-popup]
|
||||
)[hidden],
|
||||
dialog[data-panel-surface]:not([open]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Conforming additions follow the same concealment policy as existing chrome.
|
||||
Cockpit continues to own rail/dock placement and exclusions in cockpit.css. */
|
||||
body:is(.ui-clean-view, .recording-mode)
|
||||
:is([data-panel-surface], [data-panel-popup]),
|
||||
body:is(.ui-clean-view, .recording-mode)
|
||||
:is(#left-panel-stack, #right-context-rail)
|
||||
> [data-panel-id]:has(> [data-panel-surface]) {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
@@ -196,6 +196,7 @@ export class VisualSettings {
|
||||
releaseRender: services.releaseContinuousRender,
|
||||
});
|
||||
this.activeStyle = 'normal';
|
||||
this._preCyberStyle = null;
|
||||
document.documentElement.dataset.gevStyle = this.activeStyle;
|
||||
this._detectionUserOverridden = false;
|
||||
this._cockpitVisionMode = 'optical';
|
||||
@@ -614,6 +615,16 @@ export class VisualSettings {
|
||||
const previousVariant = this.hud.getVariant();
|
||||
this.hud.setVariant(variantName);
|
||||
const nextVariant = this.hud.getVariant();
|
||||
const enteredCyber = previousVariant !== 'cyber' && nextVariant === 'cyber';
|
||||
const leftCyber = previousVariant === 'cyber' && nextVariant !== 'cyber';
|
||||
if (!applyVisualDefaults) {
|
||||
// Scene/share/programmatic state is authoritative even when it reapplies
|
||||
// the current Cyber variant. Do not let an older explicit entry restore
|
||||
// stale local preset memory on the next user-selected exit.
|
||||
this._preCyberStyle = null;
|
||||
} else if (enteredCyber) {
|
||||
this._preCyberStyle = this.activeStyle;
|
||||
}
|
||||
if (this._hudLayoutSelect && this._hudLayoutSelect.value !== nextVariant) {
|
||||
this._hudLayoutSelect.value = nextVariant;
|
||||
}
|
||||
@@ -623,6 +634,16 @@ export class VisualSettings {
|
||||
{ explicit: applyVisualDefaults },
|
||||
);
|
||||
if (visualDefaults) this._applyCyberVisualDefaults(visualDefaults);
|
||||
if (leftCyber) {
|
||||
const restoreStyle = applyVisualDefaults ? this._preCyberStyle : null;
|
||||
this._preCyberStyle = null;
|
||||
if (restoreStyle && restoreStyle !== this.activeStyle) {
|
||||
this.setStyle(restoreStyle, {
|
||||
applyPreset: false,
|
||||
revealParameters: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
this._syncCyberSonarControl();
|
||||
this._syncShareState();
|
||||
this._scheduleAdaptivePanelLayout({ settle: true });
|
||||
|
||||
Reference in new issue
Block a user