mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +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
@@ -155,6 +155,14 @@
|
||||
panel method.
|
||||
- On phones the title bar sits 16 px from the top so both Radio broadcast
|
||||
waves stay on-screen.
|
||||
- Keep Cyber Voice help/error popups and Location/Visual Presets pins clear of
|
||||
their decorative frames. Leave space above attribution for its full logo row.
|
||||
Add shared panel surface hooks for theme styling, rail input and bounded body
|
||||
scrolling without changing panel disclosure or visibility policies.
|
||||
- Restore the user's previous visual preset when they explicitly switch from
|
||||
Cyber to another HUD layout, without overriding scene or shared-link state.
|
||||
- Move the aligned Cyber side-panel rails upward on desktop so the left stack
|
||||
clears the lower coordinate card; keep Cockpit's independent visor layout.
|
||||
- Keep Cyber right-rail panels mutually exclusive and Display, CCTV and Context
|
||||
headers and frames fixed during content scrolling. Restore Radio's nested Context placement and compact
|
||||
player. Add Cyber Sonar voice controls with settings and effect-state readback.
|
||||
|
||||
+17
-2
@@ -1,10 +1,11 @@
|
||||
# God's Eye View Current State
|
||||
|
||||
## Cyber HUD — September 23, 2026
|
||||
## Cyber HUD — September 30, 2026
|
||||
|
||||
Display > HUD > Layout includes Cyber, also available through the HUD voice
|
||||
action and shared visual state. An explicit first transition into Cyber selects
|
||||
FLIR with Ironbow 0.42; restored links and subsequent visual tuning remain
|
||||
FLIR with Ironbow 0.42. An explicit switch back to another HUD layout restores
|
||||
the visual preset used before Cyber; restored links and scenes remain
|
||||
authoritative. The skin uses shared red/slate panel treatments in map and cockpit,
|
||||
with compact 200px collapsed controls and wider expanded panels. Other HUD
|
||||
layouts retain their existing presentation.
|
||||
@@ -17,6 +18,20 @@ headers and decorative frames. The narrow-screen rail remains scrollable to reac
|
||||
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
|
||||
pseudo-elements. The lower-left telemetry card leaves room for attribution's
|
||||
full logo row. New panels can use the [shared surface contract](panel-surfaces.md)
|
||||
for theme tokens, rail input and a fixed header with a bounded scroll body.
|
||||
Their existing owners still control disclosure, persistence and placement;
|
||||
small-screen rail popup clipping and other integration limits are documented
|
||||
in that contract.
|
||||
|
||||
On desktop, Cyber raises the left rail so it clears the lower coordinate card.
|
||||
`layoutRightPanelRail` reads that rendered top as its measured `baseTop`, which
|
||||
keeps the right rail aligned. Cockpit retains its separate rail placement for
|
||||
its visor layout.
|
||||
|
||||
Sonar has one contact-highlighting method. Native Cesium points, billboards and
|
||||
labels are treated in GPU draw commands without replacing their positions,
|
||||
identities or pick commands. Model and canvas/DOM overlays retain their own
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# Panel surface contract
|
||||
|
||||
`src/ui/styles/panel-surfaces.css` supplies an opt-in visual surface for new
|
||||
panels. It uses the application's default color, font, border and radius tokens;
|
||||
Cyber supplies its existing red/slate palette, monospace typography and shaped
|
||||
frame. This is CSS shared by the existing panel system, not a panel registry or
|
||||
a controller. A panel ID does not need a new theme selector.
|
||||
|
||||
## Markup
|
||||
|
||||
A rail panel uses an outer owner with its existing stable `id`, matching
|
||||
`data-panel-id`, and `panel-collapsible` class. Its first non-glow child is the
|
||||
surface. Keep the header and any outward popup outside the scrolling body:
|
||||
|
||||
```html
|
||||
<section
|
||||
id="example-panel"
|
||||
data-panel-id="example-panel"
|
||||
class="panel-collapsible"
|
||||
>
|
||||
<div data-panel-surface>
|
||||
<header data-panel-header>
|
||||
<h2 class="panel-title panel-surface-title">Example</h2>
|
||||
<button
|
||||
class="panel-collapse-btn panel-surface-control"
|
||||
data-collapse-target="example-panel"
|
||||
aria-expanded="true"
|
||||
aria-label="Collapse Example"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
</header>
|
||||
<div data-panel-body data-rail-scroller>
|
||||
<label>Filter <input class="panel-surface-control" type="text" /></label>
|
||||
<button class="panel-surface-control" type="button">Apply</button>
|
||||
<p class="panel-surface-error" role="status" hidden></p>
|
||||
</div>
|
||||
<aside data-panel-popup hidden aria-label="Example help">
|
||||
<!-- The panel owns this popup's placement and interactions. -->
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
Use native controls and accessible names. `.panel-surface-control` is an
|
||||
optional skin for buttons, text fields, selects and textareas; it provides
|
||||
hover, focus, disabled and invalid-state styling. Native `disabled` owns actual
|
||||
disablement; `aria-disabled` alone only describes a state and must be enforced
|
||||
by the controller. `.panel-surface-error` supplies error color, not announcement
|
||||
or validation behavior. Keep checkbox/radio semantics and labels intact.
|
||||
|
||||
`data-panel-header` stays outside `data-panel-body`, so it remains reachable
|
||||
when content scrolls. Give the body a bounded height through its containing
|
||||
surface. The rail's allocation supplies that bound; a dock or dialog owner must
|
||||
supply its own `max-height` or height. Avoid a flex body whose contents shrink to
|
||||
fit when those contents should instead scroll.
|
||||
|
||||
## Placement and ownership
|
||||
|
||||
| Mount | Shared contract | Owner responsibilities |
|
||||
| ------------------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Direct child of `#left-panel-stack` | An owner with a direct `[data-panel-surface]` child receives relative positioning and allocated height; inner surface/body skin | Add the wrapper shown above, use the existing layout/disclosure owners, and retain left-rail focus/auto-collapse rules. |
|
||||
| Direct child of `#right-context-rail` | The same hooks with right-rail allocation and intrinsic measurement | Keep `data-rail-scroller` on the body so measurement preserves its scroll position. Respect Cyber's one-expanded-panel policy. |
|
||||
| Surface inside `#command-dock` | Surface, controls, popup decoration and inherited dock concealment | Supply geometry, stacking, disclosure, pin behavior if wanted, safe viewport bounds and dismissal. This CSS does not extend the existing Location/Visual Presets tray stack manager to arbitrary panels. |
|
||||
| `dialog[data-panel-surface]` | The same surface/body skin; a closed dialog remains hidden | Set width/max-height, use `showModal()` or `show()` and `close()`, and own labels, initial/return focus and dismissal. Do not put `panel-collapsible` on a dialog. |
|
||||
|
||||
For rails, `--panel-expanded-width` and `--panel-collapsed-width` customize
|
||||
the desired widths within the rail; otherwise Cyber uses its existing left,
|
||||
right and collapsed panel widths. `--left-panel-allocated-height` and
|
||||
`--right-panel-allocated-height` belong
|
||||
to the existing layout owner, not to individual panels. The surface must be
|
||||
the wrapper's first non-glow child because left-rail intrinsic measurement
|
||||
inspects that child. Do not put an absolutely positioned popup before the
|
||||
surface or inside its body. Position a popup beside or above its surface using
|
||||
component CSS; the shared rules deliberately do not choose a direction that
|
||||
could run beyond the viewport.
|
||||
|
||||
Rail measurement still considers visible child extents. A popup extending
|
||||
below the surface can therefore increase its measured height. Prefer a popup
|
||||
beside or above the surface, or integrate an external overlay host with the
|
||||
feature's layout owner; this CSS does not change measurement algorithms.
|
||||
|
||||
At viewport widths of 720px or less, both rails are intentional scroll
|
||||
containers. Their overflow can clip outward popups even when the panel's own
|
||||
surface is unclipped. Use a popup contained within the rail's visible bounds,
|
||||
or an explicitly owned overlay host outside the rail; the surface contract
|
||||
does not provide automatic popup escape from these small-screen scroll regions.
|
||||
|
||||
For dock disclosure, use a semantic button with `.panel-surface-control` and
|
||||
the feature's own accessible name, `aria-expanded` state and event binding.
|
||||
Do not copy the rail example's `.panel-collapse-btn` class into the dock:
|
||||
existing dock rules intentionally hide that class. The generic surface skin
|
||||
does not replace the dock's disclosure policy.
|
||||
|
||||
The existing `PanelChrome` binds `.panel-collapse-btn[data-collapse-target]`
|
||||
at initialization and routes state through `setPanelCollapsed`. For additions
|
||||
present at startup, keep those hooks and the existing chrome lifecycle. A
|
||||
dynamically mounted panel must bind and dispose its own disclosure using
|
||||
`bindPanelDisclosure` / `collapsePanelOnEscape` from `panelDisclosure.js`, with
|
||||
the application owning state changes. Do not rerun the whole application chrome
|
||||
initializer just to add one panel. `layoutLeftPanelRail` and
|
||||
`layoutRightPanelRail` accept arbitrary direct `data-panel-id` children; their
|
||||
host owns layout scheduling, observations and collapse callbacks. Dynamic
|
||||
content that changes size may require an owner-scheduled layout pass or an
|
||||
owned `ResizeObserver`; adding CSS attributes does not register new observers.
|
||||
|
||||
Persisted/share-link state, pinning, provider loading, keyboard shortcuts and
|
||||
Cockpit-specific mounting still require deliberate integration with their
|
||||
current owners. This contract does not add entries to named persistence, dock,
|
||||
Cockpit or share-state policies. Dispose listeners, observers and timers,
|
||||
dismiss open popups/dialogs and restore focus before removing a dynamic panel.
|
||||
|
||||
## Decoration, popups and visibility
|
||||
|
||||
The shared surface reserves `::before` for its non-interactive background. The
|
||||
Cyber frame clips this pseudo-element only; the surface stays unclipped with
|
||||
visible overflow. This follows the existing Display and Global Context pattern.
|
||||
Putting the shape on an interactive ancestor clips all descendant popups even
|
||||
if their `z-index` is high. A scrolling body also clips descendants by design,
|
||||
so outward popups must be siblings of the body, or use an explicitly owned
|
||||
overlay host outside every clipping ancestor.
|
||||
|
||||
`data-panel-popup` gives a positioned popup the same decorative and control
|
||||
tokens. It is not a popover API, focus trap, collision solver or top-layer
|
||||
manager. The owner must keep it inside the viewport, above relevant siblings,
|
||||
and reachable by pointer and keyboard. A native modal dialog participates in
|
||||
the browser top layer; a surface in a rail does not. Transform, containment,
|
||||
mask, clipping or stacking rules on an additional ancestor can still obstruct
|
||||
either an ordinary panel or its popup. Arbitrary future markup is not covered.
|
||||
|
||||
`hidden`, a closed native dialog and collapsed bodies/popups remain hidden.
|
||||
Clean UI and recording conceal conforming surfaces, including outward popups;
|
||||
owners must close modal dialogs when entering these modes so an invisible
|
||||
modal cannot leave the application inert. Existing Cockpit rail and dock
|
||||
exclusions remain in force. New left-rail/Cockpit functionality needs an
|
||||
explicit product integration; the surface attribute grants no mode exception.
|
||||
Never use theme styling to alter stored preferences or force hidden content
|
||||
visible.
|
||||
|
||||
## Check an addition
|
||||
|
||||
Use the real application with the default and Cyber themes. Check open,
|
||||
collapsed, hidden, disabled/error, theme round trips, clean UI/recording and
|
||||
the intended Cockpit behavior. Exercise keyboard focus, Escape, pointer input,
|
||||
the last scrolling item, resize without reload and any pin/unpin controls.
|
||||
For an outward popup, test visible pixels and pointer hit-testing outside the
|
||||
surface; a positive bounding box alone does not prove it escaped clipping.
|
||||
Check each mount the addition actually supports. The CSS contract cannot
|
||||
establish provider correctness, disclosure lifecycle or responsive placement
|
||||
without those feature-specific checks.
|
||||
+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