style: format the UI shell and component styles

This commit is contained in:
Sameh Khamis committed 2026-09-13 04:26:11 -07:00
1 parent f5ef9091f0
commit 03ad8142e6
32 files changed
+4126 -1546

No files matched your search

+34 -1
View File
@@ -213,5 +213,38 @@
"src/ui/sceneControls.js",
"src/ui/scenePresentation.js",
"src/ui/sceneControls.test.mjs",
"scripts/qa-scene-controls.mjs"
"scripts/qa-scene-controls.mjs",
"src/ui.js",
"src/standalone/ui.js",
"src/ui/applicationShell.js",
"src/ui/panelLayoutController.js",
"src/ui/panelPositionControls.js",
"src/ui/recordingControls.js",
"src/ui/shellElements.js",
"src/ui/shellFeedback.js",
"src/ui/uiLifetime.js",
"src/ui/styles.js",
"src/ui/shellLifecycle.test.mjs",
"src/testSupport/readStylesheet.mjs",
"src/splitFlap.js",
"style.css",
"src/ui/styles/overlays.css",
"src/ui/styles/command-dock-trays.css",
"src/ui/styles/location.css",
"src/ui/styles/recording.css",
"src/ui/styles/status.css",
"src/ui/styles/layers.css",
"src/ui/styles/controls.css",
"src/ui/styles/cctv.css",
"src/ui/styles/foundation.css",
"src/ui/styles/first-run.css",
"src/ui/styles/scenes.css",
"src/ui/styles/command-dock.css",
"src/ui/styles/command-dock-sliding.css",
"src/ui/styles/provider-settings.css",
"src/ui/styles/radio.css",
"src/ui/styles/responsive.css",
"src/ui/styles/voice-cost.css",
"src/ui/styles/cockpit.css",
"src/ui/styles/command-dock-compact.css"
]
+3 -9
View File
@@ -425,9 +425,7 @@
"external": []
},
"ui-shell": {
"exports": [
"./ui/shell"
],
"exports": ["./ui/shell"],
"modules": [
"src/bloom.js",
"src/cctvFocusPolicy.js",
@@ -522,14 +520,10 @@
"src/worldFocus.js",
"src/ui/uiLifetime.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"ui-styles": {
"exports": [
"./ui/styles"
],
"exports": ["./ui/styles"],
"modules": [
"src/ui/styles.js",
"style.css",
+5 -1
View File
@@ -210,7 +210,11 @@ try {
);
// Escape can also collapse the Scene accordion. Reopen it through its
// installed disclosure before testing the next visible user action.
if (await page.$eval('#scene-panel', (panel) => panel.classList.contains('collapsed')))
if (
await page.$eval('#scene-panel', (panel) =>
panel.classList.contains('collapsed'),
)
)
await page.click('[data-collapse-target="scene-panel"]');
await page.waitForSelector('#scene-download-btn', { visible: true });
await page.click('#scene-download-btn');
+31 -16
View File
@@ -107,7 +107,8 @@ function positiveNumber(value, fallback) {
}
function nowMs() {
return typeof performance !== 'undefined' && typeof performance.now === 'function'
return typeof performance !== 'undefined' &&
typeof performance.now === 'function'
? performance.now()
: Date.now();
}
@@ -235,7 +236,9 @@ export function visibleGlyphs(plan, elapsedMs, options = {}) {
const turnRatio = Number.isFinite(Number(options.turnRatio))
? Number(options.turnRatio)
: FLAP_TURN_RATIO;
const elapsed = Number.isFinite(Number(elapsedMs)) ? Math.max(0, Number(elapsedMs)) : 0;
const elapsed = Number.isFinite(Number(elapsedMs))
? Math.max(0, Number(elapsedMs))
: 0;
const turn = charMs * turnRatio;
let out = '';
for (const cell of plan?.cells || []) {
@@ -246,9 +249,11 @@ export function visibleGlyphs(plan, elapsedMs, options = {}) {
/** Whether the viewer asked for reduced motion. */
function prefersReducedMotion() {
return typeof window !== 'undefined'
&& typeof window.matchMedia === 'function'
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches;
return (
typeof window !== 'undefined' &&
typeof window.matchMedia === 'function' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches
);
}
/**
@@ -274,7 +279,11 @@ function isVisible(element) {
if (element.getClientRects().length === 0) return false;
const view = element.ownerDocument?.defaultView;
if (typeof view?.getComputedStyle !== 'function') return true;
for (let node = element; node && node.nodeType === 1; node = node.parentElement) {
for (
let node = element;
node && node.nodeType === 1;
node = node.parentElement
) {
const style = view.getComputedStyle(node);
if (style.visibility === 'hidden' || style.display === 'none') return false;
if (Number(style.opacity) === 0) return false;
@@ -301,10 +310,12 @@ function ensureHost(element) {
// nothing changed.
const text = element.firstElementChild;
const cells = text?.nextElementSibling;
if (text?.classList?.contains(TEXT_CLASS)
&& text.firstChild?.nodeType === 3
&& cells?.classList?.contains(CELLS_CLASS)
&& !cells.nextElementSibling) {
if (
text?.classList?.contains(TEXT_CLASS) &&
text.firstChild?.nodeType === 3 &&
cells?.classList?.contains(CELLS_CLASS) &&
!cells.nextElementSibling
) {
return { text, cells };
}
@@ -363,7 +374,8 @@ function easeWidth(element, fromWidth, toWidth, durationMs) {
element.style.width = `${toWidth}px`;
const finish = (event) => {
if (event && (event.target !== element || event.propertyName !== 'width')) return;
if (event && (event.target !== element || event.propertyName !== 'width'))
return;
cancelWidthEase(element);
clearSizing(element);
};
@@ -449,16 +461,19 @@ export function setSplitFlapText(element, text, options = {}) {
// previous label's glyphs, and those are what must flap away.
const state = flapStates.get(element);
const displayed = state
? visibleGlyphs(state.plan, nowMs() - state.startedAt, { charMs: state.charMs })
? visibleGlyphs(state.plan, nowMs() - state.startedAt, {
charMs: state.charMs,
})
: settled;
clearFlapTimer(element);
const beforeWidth = measureWidth(element);
const animate = SPLIT_FLAP_ENABLED
&& options.immediate !== true
&& !prefersReducedMotion()
&& isVisible(element);
const animate =
SPLIT_FLAP_ENABLED &&
options.immediate !== true &&
!prefersReducedMotion() &&
isVisible(element);
const plan = animate ? planSplitFlap(displayed, next, options) : null;
+88 -68
View File
@@ -1,7 +1,14 @@
/** Compose UI controls with the application's existing engines and layer instances. */
import { StyleManager as ApplicationShell } from '../ui/applicationShell.js';
import { LocationSearch } from '../ui/location.js';
import { CITY_POIS, GLOBE_VIEW, flyToGlobeView, flyToPresetLocation, flyToPOI, searchAndFlyTo } from '../locations.js';
import {
CITY_POIS,
GLOBE_VIEW,
flyToGlobeView,
flyToPresetLocation,
flyToPOI,
searchAndFlyTo,
} from '../locations.js';
import { interruptCameraMotion } from '../cameraVerbs.js';
import { IntelHUD } from '../hud.js';
import { ShareLinkManager } from '../sharelink.js';
@@ -12,8 +19,14 @@ import {
isCelestialRingStyleSupported,
setKeyholeFadeTuning,
} from '../celestialRing.js';
import { destroyTrackedReadout, initTrackedReadout } from '../data/trackedReadout.js';
import { destroyWorldOverlay, initWorldOverlay } from '../overlays/worldOverlay.js';
import {
destroyTrackedReadout,
initTrackedReadout,
} from '../data/trackedReadout.js';
import {
destroyWorldOverlay,
initWorldOverlay,
} from '../overlays/worldOverlay.js';
import {
destroyDetection,
initDetection,
@@ -47,7 +60,11 @@ import {
warmGroundFloor,
} from '../data/groundFloor.js';
import { sampleMeshFloorCells } from '../data/meshFloorSampler.js';
import { holdContinuousRender, releaseContinuousRender, governorRequestRender } from '../renderGovernor.js';
import {
holdContinuousRender,
releaseContinuousRender,
governorRequestRender,
} from '../renderGovernor.js';
import {
setScopeMaskEnabled,
isScopeMaskEnabled,
@@ -65,69 +82,72 @@ import {
export class StyleManager extends ApplicationShell {
constructor(viewer, options = {}) {
super(viewer, { ...options, services: {
CITY_POIS,
GLOBE_VIEW,
flyToGlobeView,
flyToPresetLocation,
flyToPOI,
searchAndFlyTo,
interruptCameraMotion,
IntelHUD,
ShareLinkManager,
OrbitController,
CelestialRing,
getKeyholeFadeTuning,
isCelestialRingStyleSupported,
setKeyholeFadeTuning,
destroyTrackedReadout,
initTrackedReadout,
destroyWorldOverlay,
initWorldOverlay,
destroyDetection,
initDetection,
cycleDetectionMode,
readDetectionDiagnostics,
getDetectionTuning,
getDetectionMode,
setDetectionModeByLabel,
suspendDetection,
resumeDetection,
setDetectionStyle,
setDetectionTuning,
trafficLayer,
flightsLayer,
militaryFlightsLayer,
isTr3b,
toggleTr3b,
satellitesLayer,
cctvLayer,
radioLayer,
bikeshareLayer,
aisLiveVesselsLayer,
militaryAwarenessLayer,
militaryInstallationsLayer,
rocketLaunchesLayer,
cachedGroundFloor,
cachedMeshFloor,
GROUND_FLOOR_LIFT_M,
meshFloorPreferred,
warmGroundFloor,
sampleMeshFloorCells,
holdContinuousRender,
releaseContinuousRender,
governorRequestRender,
setScopeMaskEnabled,
isScopeMaskEnabled,
setScopeMaskFeather,
getScopeMaskFeather,
setScopeTerminusOverride,
getScopeTerminusOverride,
clampScopeTerminusPct,
fetchRegionalBrief,
regionalDistanceM,
weatherCodeLabel,
LocationSearch
} });
super(viewer, {
...options,
services: {
CITY_POIS,
GLOBE_VIEW,
flyToGlobeView,
flyToPresetLocation,
flyToPOI,
searchAndFlyTo,
interruptCameraMotion,
IntelHUD,
ShareLinkManager,
OrbitController,
CelestialRing,
getKeyholeFadeTuning,
isCelestialRingStyleSupported,
setKeyholeFadeTuning,
destroyTrackedReadout,
initTrackedReadout,
destroyWorldOverlay,
initWorldOverlay,
destroyDetection,
initDetection,
cycleDetectionMode,
readDetectionDiagnostics,
getDetectionTuning,
getDetectionMode,
setDetectionModeByLabel,
suspendDetection,
resumeDetection,
setDetectionStyle,
setDetectionTuning,
trafficLayer,
flightsLayer,
militaryFlightsLayer,
isTr3b,
toggleTr3b,
satellitesLayer,
cctvLayer,
radioLayer,
bikeshareLayer,
aisLiveVesselsLayer,
militaryAwarenessLayer,
militaryInstallationsLayer,
rocketLaunchesLayer,
cachedGroundFloor,
cachedMeshFloor,
GROUND_FLOOR_LIFT_M,
meshFloorPreferred,
warmGroundFloor,
sampleMeshFloorCells,
holdContinuousRender,
releaseContinuousRender,
governorRequestRender,
setScopeMaskEnabled,
isScopeMaskEnabled,
setScopeMaskFeather,
getScopeMaskFeather,
setScopeTerminusOverride,
getScopeTerminusOverride,
clampScopeTerminusPct,
fetchRegionalBrief,
regionalDistanceM,
weatherCodeLabel,
LocationSearch,
},
});
}
}
+8 -3
View File
@@ -3,9 +3,14 @@ import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
export function readStylesheet(file, ancestors = new Set()) {
const resolved = path.resolve(file instanceof URL ? fileURLToPath(file) : file);
const resolved = path.resolve(
file instanceof URL ? fileURLToPath(file) : file,
);
if (ancestors.has(resolved)) throw new Error('Circular stylesheet import');
const next = new Set(ancestors).add(resolved);
return readFileSync(resolved, 'utf8').replace(/@import ['"]([^'"]+)['"];\s*/g,
(_, imported) => readStylesheet(path.resolve(path.dirname(resolved), imported), next));
return readFileSync(resolved, 'utf8').replace(
/@import ['"]([^'"]+)['"];\s*/g,
(_, imported) =>
readStylesheet(path.resolve(path.dirname(resolved), imported), next),
);
}
+1563 -615
View File
File diff suppressed because it is too large. Load diff
+130 -42
View File
@@ -50,7 +50,12 @@ const RIGHT_STACK_OBSTACLE_SELECTOR = [
'#gev-voice-control',
].join(', ');
export class PanelLayoutController {
constructor({ readHud, scheduleCockpitLayout, syncPanelCollapseButton, readDisplayScrollTop }) {
constructor({
readHud,
scheduleCockpitLayout,
syncPanelCollapseButton,
readDisplayScrollTop,
}) {
this.readHud = readHud;
this.scheduleCockpitLayout = scheduleCockpitLayout;
this._syncPanelCollapseButton = syncPanelCollapseButton;
@@ -103,7 +108,9 @@ export class PanelLayoutController {
if (!dock) return;
this._commandDockTrayObserver?.disconnect?.();
if (typeof ResizeObserver === 'function') {
this._commandDockTrayObserver = new ResizeObserver(() => this._updateCommandDockTrayStack());
this._commandDockTrayObserver = new ResizeObserver(() =>
this._updateCommandDockTrayStack(),
);
dock.querySelectorAll('.dock-popover-content').forEach((tray) => {
this._commandDockTrayObserver.observe(tray);
});
@@ -115,24 +122,48 @@ export class PanelLayoutController {
if (this.destroyed) return;
const dock = document.getElementById('command-dock');
if (!dock) return;
const locationPanel = dock.querySelector('#location-bar.dock-pinned:not(.collapsed)');
const presetsPanel = dock.querySelector('#control-panel.dock-pinned:not(.collapsed)');
const locationHeight = locationPanel?.querySelector('.dock-popover-content')?.getBoundingClientRect().height || 0;
const presetsHeight = presetsPanel?.querySelector('.dock-popover-content')?.getBoundingClientRect().height || 0;
const locationPanel = dock.querySelector(
'#location-bar.dock-pinned:not(.collapsed)',
);
const presetsPanel = dock.querySelector(
'#control-panel.dock-pinned:not(.collapsed)',
);
const locationHeight =
locationPanel
?.querySelector('.dock-popover-content')
?.getBoundingClientRect().height || 0;
const presetsHeight =
presetsPanel
?.querySelector('.dock-popover-content')
?.getBoundingClientRect().height || 0;
const pinnedCount = Number(locationHeight > 0) + Number(presetsHeight > 0);
const locationHeightPx = Math.ceil(locationHeight);
const presetsHeightPx = Math.ceil(presetsHeight);
const topPinnedPanel = dock.querySelector('.dock-pinned-top.dock-pinned:not(.collapsed)');
const lowerPinnedPanel = topPinnedPanel?.id === 'location-bar' ? presetsPanel : locationPanel;
const lowerPinnedHeight = lowerPinnedPanel
?.querySelector('.dock-popover-content')
?.getBoundingClientRect().height || 0;
const stackHeight = pinnedCount > 1
? `calc(${locationHeightPx}px + ${presetsHeightPx}px + 1.2rem)`
: `${locationHeightPx + presetsHeightPx}px`;
dock.style.setProperty('--dock-location-pinned-height', `${locationHeightPx}px`);
dock.style.setProperty('--dock-presets-pinned-height', `${presetsHeightPx}px`);
dock.style.setProperty('--dock-lower-pinned-height', `${Math.ceil(lowerPinnedHeight)}px`);
const topPinnedPanel = dock.querySelector(
'.dock-pinned-top.dock-pinned:not(.collapsed)',
);
const lowerPinnedPanel =
topPinnedPanel?.id === 'location-bar' ? presetsPanel : locationPanel;
const lowerPinnedHeight =
lowerPinnedPanel
?.querySelector('.dock-popover-content')
?.getBoundingClientRect().height || 0;
const stackHeight =
pinnedCount > 1
? `calc(${locationHeightPx}px + ${presetsHeightPx}px + 1.2rem)`
: `${locationHeightPx + presetsHeightPx}px`;
dock.style.setProperty(
'--dock-location-pinned-height',
`${locationHeightPx}px`,
);
dock.style.setProperty(
'--dock-presets-pinned-height',
`${presetsHeightPx}px`,
);
dock.style.setProperty(
'--dock-lower-pinned-height',
`${Math.ceil(lowerPinnedHeight)}px`,
);
dock.style.setProperty('--dock-pinned-stack-height', stackHeight);
dock.classList.toggle('dock-has-pinned-tray', pinnedCount > 0);
dock.classList.toggle('dock-has-two-pinned-trays', pinnedCount > 1);
@@ -177,12 +208,18 @@ export class PanelLayoutController {
this._scheduleRightPanelLayout();
});
this._rightStackResizeObserver.observe(stack);
for (const panel of [this._ppToggles, this._cctvPanel, globalContextPanel]) {
for (const panel of [
this._ppToggles,
this._cctvPanel,
globalContextPanel,
]) {
if (panel) this._rightStackResizeObserver.observe(panel);
}
document.querySelectorAll(RIGHT_STACK_OBSTACLE_SELECTOR).forEach((element) => {
this._rightStackResizeObserver.observe(element);
});
document
.querySelectorAll(RIGHT_STACK_OBSTACLE_SELECTOR)
.forEach((element) => {
this._rightStackResizeObserver.observe(element);
});
}
if (typeof MutationObserver !== 'undefined') {
@@ -209,11 +246,17 @@ export class PanelLayoutController {
const transitionHud = document.getElementById('intel-hud');
if (transitionHud) {
this._rightStackHudTransitionHandler = (event) => {
if (event.propertyName === 'opacity' || event.propertyName === 'visibility') {
if (
event.propertyName === 'opacity' ||
event.propertyName === 'visibility'
) {
this._scheduleRightPanelLayout({ reconsiderAutoCollapse: true });
}
};
transitionHud.addEventListener('transitionend', this._rightStackHudTransitionHandler);
transitionHud.addEventListener(
'transitionend',
this._rightStackHudTransitionHandler,
);
}
this._scheduleRightPanelLayout();
@@ -228,7 +271,9 @@ export class PanelLayoutController {
this._rightStackLayoutFrame = null;
if (this._rightStackReconsiderAutoCollapse) {
this._rightStackReconsiderAutoCollapse = false;
for (const panel of this._rightPanelStack.querySelectorAll('.layout-auto-collapsed')) {
for (const panel of this._rightPanelStack.querySelectorAll(
'.layout-auto-collapsed',
)) {
panel.classList.remove('collapsed', 'layout-auto-collapsed');
this._syncPanelCollapseButton(panel);
}
@@ -265,12 +310,16 @@ export class PanelLayoutController {
this._leftStackResizeObserver.observe(stack);
stack.querySelectorAll(':scope > [data-panel-id]').forEach((panel) => {
this._leftStackResizeObserver.observe(panel);
const inner = [...panel.children].find((child) => !child.classList.contains('panel-glow'));
const inner = [...panel.children].find(
(child) => !child.classList.contains('panel-glow'),
);
if (inner) this._leftStackResizeObserver.observe(inner);
});
document.querySelectorAll(LEFT_STACK_OBSTACLE_SELECTOR).forEach((element) => {
this._leftStackResizeObserver.observe(element);
});
document
.querySelectorAll(LEFT_STACK_OBSTACLE_SELECTOR)
.forEach((element) => {
this._leftStackResizeObserver.observe(element);
});
}
if (typeof MutationObserver !== 'undefined') {
@@ -303,7 +352,10 @@ export class PanelLayoutController {
const transitionHud = document.getElementById('intel-hud');
if (transitionHud) {
this._leftStackHudTransitionHandler = (event) => {
if (event.propertyName === 'opacity' || event.propertyName === 'visibility') {
if (
event.propertyName === 'opacity' ||
event.propertyName === 'visibility'
) {
this._scheduleLeftPanelLayout({ reconsiderAutoCollapse: true });
// The Cockpit strip hangs off the HUD's REC readout, so it has to
// remeasure on the same event: the readout keeps its rect through
@@ -311,7 +363,10 @@ export class PanelLayoutController {
this.scheduleCockpitLayout();
}
};
transitionHud.addEventListener('transitionend', this._leftStackHudTransitionHandler);
transitionHud.addEventListener(
'transitionend',
this._leftStackHudTransitionHandler,
);
}
this._leftStackCockpitModeHandler = () => {
@@ -321,12 +376,22 @@ export class PanelLayoutController {
// the accordion remains in the same obstacle-safe lane instead of
// jumping to a cockpit-specific top anchor.
this._scheduleLeftPanelLayout();
if (this._cockpitLayoutFrame !== null) cancelAnimationFrame(this._cockpitLayoutFrame);
this._cockpitLayoutFrame = requestAnimationFrame(() => { this._cockpitLayoutFrame = null; this._scheduleLeftPanelLayout(); });
if (this._cockpitLayoutFrame !== null)
cancelAnimationFrame(this._cockpitLayoutFrame);
this._cockpitLayoutFrame = requestAnimationFrame(() => {
this._cockpitLayoutFrame = null;
this._scheduleLeftPanelLayout();
});
clearTimeout(this._cockpitLayoutTimer);
this._cockpitLayoutTimer = setTimeout(() => { this._cockpitLayoutTimer = null; this._scheduleLeftPanelLayout(); }, 300);
this._cockpitLayoutTimer = setTimeout(() => {
this._cockpitLayoutTimer = null;
this._scheduleLeftPanelLayout();
}, 300);
};
window.addEventListener('gev:cockpit-mode-changed', this._leftStackCockpitModeHandler);
window.addEventListener(
'gev:cockpit-mode-changed',
this._leftStackCockpitModeHandler,
);
this._scheduleLeftPanelLayout();
}
@@ -340,7 +405,9 @@ export class PanelLayoutController {
this._leftStackLayoutFrame = null;
if (this._leftStackReconsiderAutoCollapse) {
this._leftStackReconsiderAutoCollapse = false;
for (const panel of this._leftPanelStack.querySelectorAll('.layout-auto-collapsed')) {
for (const panel of this._leftPanelStack.querySelectorAll(
'.layout-auto-collapsed',
)) {
panel.classList.remove('collapsed', 'layout-auto-collapsed');
this._syncPanelCollapseButton(panel);
}
@@ -366,21 +433,42 @@ export class PanelLayoutController {
destroy() {
if (this.destroyed) return;
this.destroyed = true;
for (const field of ['_leftStackLayoutFrame', '_rightStackLayoutFrame', '_cockpitLayoutFrame']) {
for (const field of [
'_leftStackLayoutFrame',
'_rightStackLayoutFrame',
'_cockpitLayoutFrame',
]) {
if (this[field] !== null) cancelAnimationFrame(this[field]);
this[field] = null;
}
clearTimeout(this._adaptivePanelSettleTimer);
clearTimeout(this._cockpitLayoutTimer);
this._adaptivePanelSettleTimer = this._cockpitLayoutTimer = null;
for (const field of ['_commandDockTrayObserver', '_leftStackResizeObserver', '_leftStackMutationObserver', '_rightStackResizeObserver', '_rightStackMutationObserver']) {
this[field]?.disconnect(); this[field] = null;
}
for (const field of ['_leftStackHudTransitionHandler', '_rightStackHudTransitionHandler']) {
if (this[field]) document.getElementById('intel-hud')?.removeEventListener('transitionend', this[field]);
for (const field of [
'_commandDockTrayObserver',
'_leftStackResizeObserver',
'_leftStackMutationObserver',
'_rightStackResizeObserver',
'_rightStackMutationObserver',
]) {
this[field]?.disconnect();
this[field] = null;
}
if (this._leftStackCockpitModeHandler) window.removeEventListener('gev:cockpit-mode-changed', this._leftStackCockpitModeHandler);
for (const field of [
'_leftStackHudTransitionHandler',
'_rightStackHudTransitionHandler',
]) {
if (this[field])
document
.getElementById('intel-hud')
?.removeEventListener('transitionend', this[field]);
this[field] = null;
}
if (this._leftStackCockpitModeHandler)
window.removeEventListener(
'gev:cockpit-mode-changed',
this._leftStackCockpitModeHandler,
);
this._leftStackCockpitModeHandler = null;
}
}
+42 -14
View File
@@ -4,7 +4,12 @@ const PANEL_POSITION_STORAGE_VERSION = 'v8';
const PANEL_Z_BASE = 100;
const PANEL_Z_MAX = 139;
export class PanelPositionControls {
constructor({ syncPanelCollapseButton, layoutRightPanels, syncCctvPanelViewport, showToast }) {
constructor({
syncPanelCollapseButton,
layoutRightPanels,
syncCctvPanelViewport,
showToast,
}) {
this._syncPanelCollapseButton = syncPanelCollapseButton;
this._layoutRightPanels = layoutRightPanels;
this._syncCctvPanelViewport = syncCctvPanelViewport;
@@ -17,7 +22,9 @@ export class PanelPositionControls {
this.destroyed = false;
}
listen(target, type, callback) {
const listener = (event) => { if (!this.destroyed) callback(event); };
const listener = (event) => {
if (!this.destroyed) callback(event);
};
target.addEventListener(type, listener);
this.removers.push(() => target.removeEventListener(type, listener));
}
@@ -35,10 +42,13 @@ export class PanelPositionControls {
const marker = `godsEyeView.${PANEL_POSITION_STORAGE_VERSION}.layoutResetNotified`;
if (localStorage.getItem(marker)) return;
localStorage.setItem(marker, '1');
const hadOldPositions = Object.keys(localStorage)
.some((key) => key.startsWith('godsEyeView.v6.panelPos.'));
const hadOldPositions = Object.keys(localStorage).some((key) =>
key.startsWith('godsEyeView.v6.panelPos.'),
);
if (hadOldPositions) {
this._showToast('Panel layout updated — positions reset to new defaults');
this._showToast(
'Panel layout updated — positions reset to new defaults',
);
}
} catch {
// storage unavailable
@@ -63,7 +73,9 @@ export class PanelPositionControls {
// full row set a frame or two later, so the restore-time clamp used a stale (shorter) height and
// the panel could still hang off the bottom (audit U2). Re-clamp on every size change.
if (this._ppToggles && typeof ResizeObserver !== 'undefined') {
this._draggableResizeObserver = new ResizeObserver(() => this._reclampDraggablePanels());
this._draggableResizeObserver = new ResizeObserver(() =>
this._reclampDraggablePanels(),
);
this._draggableResizeObserver.observe(this._ppToggles);
}
}
@@ -105,7 +117,10 @@ export class PanelPositionControls {
_savePanelCollapsedState(panelId, collapsed) {
try {
localStorage.setItem(this._panelCollapseStorageKey(panelId), collapsed ? '1' : '0');
localStorage.setItem(
this._panelCollapseStorageKey(panelId),
collapsed ? '1' : '0',
);
} catch {
// storage unavailable
}
@@ -124,10 +139,15 @@ export class PanelPositionControls {
const raw = localStorage.getItem(this._panelStorageKey(panelId));
if (!raw) return;
const pos = JSON.parse(raw);
if (!pos || typeof pos.left !== 'number' || typeof pos.top !== 'number') return;
if (!pos || typeof pos.left !== 'number' || typeof pos.top !== 'number')
return;
// Clamp to the viewport: a position saved at one window size would otherwise land off-screen at
// another (audit U2 — observed a panel at x:-192). The drag handler clamps; restore must too.
const { left, top } = this._clampToViewport(Math.round(pos.left), Math.round(pos.top), panelEl);
const { left, top } = this._clampToViewport(
Math.round(pos.left),
Math.round(pos.top),
panelEl,
);
panelEl.style.left = `${left}px`;
panelEl.style.top = `${top}px`;
panelEl.style.right = 'auto';
@@ -153,10 +173,13 @@ export class PanelPositionControls {
_savePanelPosition(panelId, panelEl) {
const rect = panelEl.getBoundingClientRect();
try {
localStorage.setItem(this._panelStorageKey(panelId), JSON.stringify({
left: Math.round(rect.left),
top: Math.round(rect.top),
}));
localStorage.setItem(
this._panelStorageKey(panelId),
JSON.stringify({
left: Math.round(rect.left),
top: Math.round(rect.top),
}),
);
} catch {
// storage unavailable
}
@@ -187,7 +210,12 @@ export class PanelPositionControls {
this.listen(handleEl, 'pointerdown', (event) => {
if (event.button !== 0) return;
if (event.target.closest('.panel-collapse-btn')) return;
if (event.target.closest('input, select, option, button:not(.panel-collapse-btn)')) return;
if (
event.target.closest(
'input, select, option, button:not(.panel-collapse-btn)',
)
)
return;
this._cancelDrag?.();
event.preventDefault();
+31 -8
View File
@@ -4,7 +4,11 @@ export class RecordingControls {
this._syncShareState = syncShareState;
this.destroyed = false;
this._recordingMode = false;
this._recordingConfig = { hidePanels: true, hudMode: 'minimal', safeFrame: '16:9' };
this._recordingConfig = {
hidePanels: true,
hudMode: 'minimal',
safeFrame: '16:9',
};
this._preRecordingHudState = null;
this._safeFrameOverlay = document.getElementById('safe-frame-overlay');
this._safeFrameBox = document.getElementById('safe-frame-box');
@@ -15,21 +19,34 @@ export class RecordingControls {
_initRecordingOverlay() {
if (this.destroyed) return;
if (!this._safeFrameOverlay || !this._safeFrameBox) return;
this._safeFrameOverlay.classList.remove('active', 'ratio-9-16', 'ratio-16-9');
this._safeFrameOverlay.classList.remove(
'active',
'ratio-9-16',
'ratio-16-9',
);
}
setRecordingMode(enabled, options = {}) {
if (this.destroyed) return;
const { hidePanels = true, hudMode = 'minimal', safeFrame = '16:9' } = options;
const {
hidePanels = true,
hudMode = 'minimal',
safeFrame = '16:9',
} = options;
this._recordingMode = !!enabled;
this._recordingConfig = { hidePanels, hudMode, safeFrame };
document.body.classList.toggle('recording-mode', this._recordingMode && hidePanels);
document.body.classList.toggle(
'recording-mode',
this._recordingMode && hidePanels,
);
if (this._safeFrameOverlay) {
this._safeFrameOverlay.classList.remove('ratio-9-16', 'ratio-16-9');
this._safeFrameOverlay.classList.toggle('active', this._recordingMode);
this._safeFrameOverlay.classList.add(safeFrame === '9:16' ? 'ratio-9-16' : 'ratio-16-9');
this._safeFrameOverlay.classList.add(
safeFrame === '9:16' ? 'ratio-9-16' : 'ratio-16-9',
);
}
if (this._recordingMode) {
@@ -46,7 +63,8 @@ export class RecordingControls {
} else if (hudMode === 'full' || hudMode === 'minimal') {
this.hud.setMode('on');
this.hud.setVariant(hudMode === 'minimal' ? 'minimal' : 'tactical');
if (this._hudLayoutSelect) this._hudLayoutSelect.value = this.hud.getVariant();
if (this._hudLayoutSelect)
this._hudLayoutSelect.value = this.hud.getVariant();
} else {
this.hud.setMode('auto');
}
@@ -55,11 +73,16 @@ export class RecordingControls {
this._preRecordingHudState = null;
if (saved) {
this.hud.setVariant(saved.variant);
if (this._hudLayoutSelect) this._hudLayoutSelect.value = this.hud.getVariant();
if (this._hudLayoutSelect)
this._hudLayoutSelect.value = this.hud.getVariant();
}
this.hud.setMode(saved ? saved.mode : 'auto');
if (this._safeFrameOverlay) {
this._safeFrameOverlay.classList.remove('active', 'ratio-9-16', 'ratio-16-9');
this._safeFrameOverlay.classList.remove(
'active',
'ratio-9-16',
'ratio-16-9',
);
}
}
this._hudBtn.classList.toggle('active', this.hud.visible);
+54 -18
View File
@@ -18,14 +18,20 @@ export function readShellElements(document = globalThis.document) {
_hudLayoutRow: document.getElementById('hud-layout-row'),
_hudLayoutSelect: document.getElementById('hud-layout-select'),
_detectionSliderRow: document.getElementById('detection-slider-row'),
_detectionDensitySlider: document.getElementById('detection-density-slider'),
_detectionDensitySlider: document.getElementById(
'detection-density-slider',
),
_detectionDensityValue: document.getElementById('detection-density-value'),
_detectionAllocationRow: document.getElementById('detection-allocation-row'),
_detectionAllocationRow: document.getElementById(
'detection-allocation-row',
),
_detectionFadeRow: document.getElementById('detection-fade-row'),
_detectionFadeSlider: document.getElementById('detection-fade-slider'),
_detectionFadeValue: document.getElementById('detection-fade-value'),
_detectionOpacityRow: document.getElementById('detection-opacity-row'),
_detectionOpacitySlider: document.getElementById('detection-opacity-slider'),
_detectionOpacitySlider: document.getElementById(
'detection-opacity-slider',
),
_detectionOpacityValue: document.getElementById('detection-opacity-value'),
_celestialBtn: document.getElementById('celestial-toggle'),
_scopeBtn: document.getElementById('scope-toggle'),
@@ -42,17 +48,39 @@ export function readShellElements(document = globalThis.document) {
_contextRadioDock: document.getElementById('context-radio-dock'),
_contextRadioToggleBtn: document.getElementById('context-radio-toggle-btn'),
_contextRadioMini: document.getElementById('context-radio-mini'),
_contextRadioMiniEnableBtn: document.getElementById('context-radio-mini-enable-btn'),
_contextRadioDetailsBtn: document.getElementById('context-radio-details-btn'),
_contextRadioMiniCloseBtn: document.getElementById('context-radio-mini-close-btn'),
_contextRadioMiniStation: document.getElementById('context-radio-mini-station'),
_contextRadioMiniPrevBtn: document.getElementById('context-radio-mini-prev-btn'),
_contextRadioMiniPlayBtn: document.getElementById('context-radio-mini-play-btn'),
_contextRadioMiniNextBtn: document.getElementById('context-radio-mini-next-btn'),
_contextRadioMiniVolume: document.getElementById('context-radio-mini-volume'),
_contextRadioMiniVolumeValue: document.getElementById('context-radio-mini-volume-value'),
_cockpitUtilityControls: document.getElementById('cockpit-utility-controls'),
_cockpitDisplayToggleBtn: document.getElementById('cockpit-display-toggle-btn'),
_contextRadioMiniEnableBtn: document.getElementById(
'context-radio-mini-enable-btn',
),
_contextRadioDetailsBtn: document.getElementById(
'context-radio-details-btn',
),
_contextRadioMiniCloseBtn: document.getElementById(
'context-radio-mini-close-btn',
),
_contextRadioMiniStation: document.getElementById(
'context-radio-mini-station',
),
_contextRadioMiniPrevBtn: document.getElementById(
'context-radio-mini-prev-btn',
),
_contextRadioMiniPlayBtn: document.getElementById(
'context-radio-mini-play-btn',
),
_contextRadioMiniNextBtn: document.getElementById(
'context-radio-mini-next-btn',
),
_contextRadioMiniVolume: document.getElementById(
'context-radio-mini-volume',
),
_contextRadioMiniVolumeValue: document.getElementById(
'context-radio-mini-volume-value',
),
_cockpitUtilityControls: document.getElementById(
'cockpit-utility-controls',
),
_cockpitDisplayToggleBtn: document.getElementById(
'cockpit-display-toggle-btn',
),
_cockpitDisplayPanel: document.getElementById('cockpit-display-panel'),
_cockpitRadioToggleBtn: document.getElementById('cockpit-radio-toggle-btn'),
_cockpitRadioPanel: document.getElementById('cockpit-radio-panel'),
@@ -62,7 +90,9 @@ export function readShellElements(document = globalThis.document) {
_cockpitRadioPlayBtn: document.getElementById('cockpit-radio-play-btn'),
_cockpitRadioNextBtn: document.getElementById('cockpit-radio-next-btn'),
_cockpitRadioVolume: document.getElementById('cockpit-radio-volume'),
_cockpitRadioVolumeValue: document.getElementById('cockpit-radio-volume-value'),
_cockpitRadioVolumeValue: document.getElementById(
'cockpit-radio-volume-value',
),
_radioLayerState: document.getElementById('radio-layer-state'),
_radioEnableBtn: document.getElementById('radio-enable-btn'),
_radioFilter: document.getElementById('radio-filter'),
@@ -83,12 +113,18 @@ export function readShellElements(document = globalThis.document) {
_radioVolumeValue: document.getElementById('radio-volume-value'),
_radioPlaybackState: document.getElementById('radio-playback-state'),
_radioStationHomepage: document.getElementById('radio-station-homepage'),
_globalContextFlightsBtn: document.getElementById('global-context-flights-btn'),
_globalContextMissionsBtn: document.getElementById('global-context-missions-btn'),
_globalContextFlightsBtn: document.getElementById(
'global-context-flights-btn',
),
_globalContextMissionsBtn: document.getElementById(
'global-context-missions-btn',
),
_contextModeStandby: document.getElementById('context-mode-standby'),
_contextFlightsView: document.getElementById('context-flights-view'),
_contextMissionsView: document.getElementById('context-missions-view'),
_installationsSearchBtn: document.getElementById('installations-search-btn'),
_installationsSearchBtn: document.getElementById(
'installations-search-btn',
),
_leftPanelStack: document.getElementById('left-panel-stack'),
_cctvEnableBtn: document.getElementById('cctv-enable-btn'),
_cctvNearestBtn: document.getElementById('cctv-nearest-btn'),
+55 -15
View File
@@ -1,5 +1,13 @@
/** Own loading notices, traffic status, toast timing and visibility cleanup. */
import { aggregateLayerLoading, createGlobalStatusNotice, createLoadingFeedbackState, createTrafficSyncFeedbackState, presentGlobalLoadingStatus, reduceLoadingFeedback, reduceTrafficSyncFeedback } from '../loadingFeedback.js';
import {
aggregateLayerLoading,
createGlobalStatusNotice,
createLoadingFeedbackState,
createTrafficSyncFeedbackState,
presentGlobalLoadingStatus,
reduceLoadingFeedback,
reduceTrafficSyncFeedback,
} from '../loadingFeedback.js';
import { setSplitFlapText, disposeSplitFlap } from '../splitFlap.js';
export class ShellFeedback {
constructor({ readLayers }) {
@@ -9,15 +17,24 @@ export class ShellFeedback {
this._trafficSyncFeedbackState = createTrafficSyncFeedbackState();
this._loadingFeedbackEvent = null;
this._globalStatusNotice = null;
this._loadingFeedbackTicker = this._trafficChipTicker = this._toastTimer = null;
this._loadingFeedbackTicker =
this._trafficChipTicker =
this._toastTimer =
null;
this._loadingVisibilityHandler = null;
this._lastLoadingFeedbackUpdateAt = 0;
this._globalLoadingStatus = document.getElementById('global-loading-status');
this._globalLoadingStatus = document.getElementById(
'global-loading-status',
);
this._globalLoadingLabel = document.getElementById('global-loading-label');
this._globalLoadingDetail = document.getElementById('global-loading-detail');
this._globalLoadingDetail = document.getElementById(
'global-loading-detail',
);
this._trafficSyncChip = document.getElementById('traffic-sync-chip');
this._trafficSyncLabel = document.getElementById('traffic-sync-label');
this._trafficSyncProgress = document.getElementById('traffic-sync-progress');
this._trafficSyncProgress = document.getElementById(
'traffic-sync-progress',
);
this._toast = document.getElementById('toast');
}
observeVisibility() {
@@ -26,23 +43,38 @@ export class ShellFeedback {
if (!document.hidden) this._updateGlobalLoadingFeedback();
else this._stopLoadingFeedbackTicker();
};
document.addEventListener('visibilitychange', this._loadingVisibilityHandler);
document.addEventListener(
'visibilitychange',
this._loadingVisibilityHandler,
);
}
_updateTrafficSyncChip(forceShow = false, now = performance.now()) {
if (this.destroyed) return;
if (!this._trafficSyncChip || !this._trafficSyncLabel || !this._trafficSyncProgress) return;
if (
!this._trafficSyncChip ||
!this._trafficSyncLabel ||
!this._trafficSyncProgress
)
return;
const layers = this.readLayers();
const traffic = Array.isArray(layers) ? layers.find((layer) => layer.id === 'traffic') : null;
const traffic = Array.isArray(layers)
? layers.find((layer) => layer.id === 'traffic')
: null;
this._trafficSyncFeedbackState = reduceTrafficSyncFeedback(
this._trafficSyncFeedbackState,
{ enabled: traffic?.enabled === true, stats: traffic?.stats || {}, forceShow },
{
enabled: traffic?.enabled === true,
stats: traffic?.stats || {},
forceShow,
},
now,
);
const presentation = this._trafficSyncFeedbackState;
// setSplitFlapText carries the same unchanged-text guard internally, and
// the flap keeps textContent equal to the settled label throughout, so
// this stays a no-op on the repeat ticks exactly as it did before.
if (presentation.label) setSplitFlapText(this._trafficSyncLabel, presentation.label);
if (presentation.label)
setSplitFlapText(this._trafficSyncLabel, presentation.label);
// Written on every change INCLUDING the empty settled value — the reducer
// clears the progress number once the sync lands, and a truthiness guard
// here would strand the last "..." beside the settled label.
@@ -69,9 +101,11 @@ export class ShellFeedback {
summary,
now,
);
if (this._globalStatusNotice?.persistent !== true
&& Number.isFinite(this._globalStatusNotice?.hideAt)
&& now >= this._globalStatusNotice.hideAt) {
if (
this._globalStatusNotice?.persistent !== true &&
Number.isFinite(this._globalStatusNotice?.hideAt) &&
now >= this._globalStatusNotice.hideAt
) {
this._globalStatusNotice = null;
}
// Loading phases and universal notices both have time-driven transitions.
@@ -129,7 +163,9 @@ export class ShellFeedback {
const now = performance.now();
this._lastLoadingFeedbackUpdateAt = now;
this._updateGlobalLoadingFeedback(now);
const noticeNeedsTicker = Number.isFinite(this._globalStatusNotice?.hideAt);
const noticeNeedsTicker = Number.isFinite(
this._globalStatusNotice?.hideAt,
);
if (this._loadingFeedbackState?.phase === 'idle' && !noticeNeedsTicker) {
this._stopLoadingFeedbackTicker();
}
@@ -158,7 +194,11 @@ export class ShellFeedback {
clearInterval(this._trafficChipTicker);
clearTimeout(this._toastTimer);
this._trafficChipTicker = this._toastTimer = null;
if (this._loadingVisibilityHandler) document.removeEventListener('visibilitychange', this._loadingVisibilityHandler);
if (this._loadingVisibilityHandler)
document.removeEventListener(
'visibilitychange',
this._loadingVisibilityHandler,
);
this._loadingVisibilityHandler = null;
disposeSplitFlap(this._globalLoadingLabel);
disposeSplitFlap(this._trafficSyncLabel);
+165 -40
View File
@@ -7,133 +7,258 @@ import { ShellFeedback } from './shellFeedback.js';
import { RecordingControls } from './recordingControls.js';
function fixture() {
const saved = Object.fromEntries(['document', 'window', 'localStorage', 'requestAnimationFrame', 'cancelAnimationFrame', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'].map((key) => [key, globalThis[key]]));
const frames = new Map(), timers = new Map();
const saved = Object.fromEntries(
[
'document',
'window',
'localStorage',
'requestAnimationFrame',
'cancelAnimationFrame',
'setTimeout',
'clearTimeout',
'setInterval',
'clearInterval',
].map((key) => [key, globalThis[key]]),
);
const frames = new Map(),
timers = new Map();
let next = 0;
const element = () => {
const classes = new Set();
return Object.assign(new EventTarget(), {
style: {}, dataset: {}, hidden: false, textContent: '',
style: {},
dataset: {},
hidden: false,
textContent: '',
classList: {
add: (...names) => names.forEach((name) => classes.add(name)),
remove: (...names) => names.forEach((name) => classes.delete(name)),
contains: (name) => classes.has(name),
toggle: (name, active) => active ? classes.add(name) : classes.delete(name),
toggle: (name, active) =>
active ? classes.add(name) : classes.delete(name),
},
querySelector: () => null, querySelectorAll: () => [], closest: () => null,
getBoundingClientRect: () => ({ left: 20, top: 30, width: 100, height: 80 }),
querySelector: () => null,
querySelectorAll: () => [],
closest: () => null,
getBoundingClientRect: () => ({
left: 20,
top: 30,
width: 100,
height: 80,
}),
});
};
const nodes = new Map(['left-panel-stack', 'right-context-rail', 'pp-toggles', 'toast', 'global-loading-status', 'global-loading-detail', 'safe-frame-overlay', 'safe-frame-box', 'hud-toggle', 'hud-layout-select'].map((id) => [id, element()]));
globalThis.document = Object.assign(new EventTarget(), { hidden: false, getElementById: (id) => nodes.get(id) || null, querySelectorAll: () => [], body: element() });
globalThis.window = Object.assign(new EventTarget(), { innerWidth: 600, innerHeight: 400 });
const nodes = new Map(
[
'left-panel-stack',
'right-context-rail',
'pp-toggles',
'toast',
'global-loading-status',
'global-loading-detail',
'safe-frame-overlay',
'safe-frame-box',
'hud-toggle',
'hud-layout-select',
].map((id) => [id, element()]),
);
globalThis.document = Object.assign(new EventTarget(), {
hidden: false,
getElementById: (id) => nodes.get(id) || null,
querySelectorAll: () => [],
body: element(),
});
globalThis.window = Object.assign(new EventTarget(), {
innerWidth: 600,
innerHeight: 400,
});
const writes = [];
globalThis.localStorage = { setItem: (...args) => writes.push(args), getItem: () => null };
globalThis.requestAnimationFrame = (callback) => { frames.set(++next, callback); return next; };
globalThis.localStorage = {
setItem: (...args) => writes.push(args),
getItem: () => null,
};
globalThis.requestAnimationFrame = (callback) => {
frames.set(++next, callback);
return next;
};
globalThis.cancelAnimationFrame = (id) => frames.delete(id);
globalThis.setTimeout = globalThis.setInterval = (callback) => { timers.set(++next, callback); return next; };
globalThis.clearTimeout = globalThis.clearInterval = (id) => timers.delete(id);
return { frames, timers, nodes, element, writes, restore() { Object.assign(globalThis, saved); } };
globalThis.setTimeout = globalThis.setInterval = (callback) => {
timers.set(++next, callback);
return next;
};
globalThis.clearTimeout = globalThis.clearInterval = (id) =>
timers.delete(id);
return {
frames,
timers,
nodes,
element,
writes,
restore() {
Object.assign(globalThis, saved);
},
};
}
test('UI lifetime revokes listeners and nested presentation work synchronously', () => {
const f = fixture();
try {
const owner = new UiLifetime(), target = new EventTarget();
const owner = new UiLifetime(),
target = new EventTarget();
let calls = 0;
owner.listen(target, 'change', () => calls++);
owner.frame(() => { calls++; owner.frame(() => calls++); });
owner.frame(() => {
calls++;
owner.frame(() => calls++);
});
owner.timeout(() => calls++, 100);
const retainedFrame = [...f.frames.values()][0];
owner.destroy(); owner.destroy();
owner.destroy();
owner.destroy();
target.dispatchEvent(new Event('change'));
retainedFrame();
owner.frame(() => calls++); owner.timeout(() => calls++, 1);
owner.frame(() => calls++);
owner.timeout(() => calls++, 1);
assert.equal(calls, 0);
assert.equal(f.frames.size + f.timers.size + owner.removers.size, 0);
} finally { f.restore(); }
} finally {
f.restore();
}
});
test('one-time UI listeners and cancelled notice timers release their registrations', () => {
const f = fixture();
try {
const owner = new UiLifetime(), target = new EventTarget();
const owner = new UiLifetime(),
target = new EventTarget();
let calls = 0;
owner.listen(target, 'change', () => calls++, { once: true });
target.dispatchEvent(new Event('change')); target.dispatchEvent(new Event('change'));
target.dispatchEvent(new Event('change'));
target.dispatchEvent(new Event('change'));
owner.cancelTimeout(owner.timeout(() => calls++, 100));
assert.equal(calls, 1);
assert.equal(owner.removers.size + owner.timers.size + f.timers.size, 0);
owner.destroy();
} finally { f.restore(); }
} finally {
f.restore();
}
});
test('panel layout coalesces each rail and cannot rearm after destruction', () => {
const f = fixture();
try {
let layouts = 0;
const owner = new PanelLayoutController({ readHud: () => ({}), scheduleCockpitLayout() {}, syncPanelCollapseButton() {}, readDisplayScrollTop: () => 0 });
const owner = new PanelLayoutController({
readHud: () => ({}),
scheduleCockpitLayout() {},
syncPanelCollapseButton() {},
readDisplayScrollTop: () => 0,
});
owner._syncLeftPanelAdaptiveLayout = () => layouts++;
owner._scheduleLeftPanelLayout(); owner._scheduleLeftPanelLayout();
owner._scheduleLeftPanelLayout();
owner._scheduleLeftPanelLayout();
assert.equal(f.frames.size, 1);
owner._scheduleAdaptivePanelLayout({ settle: true });
assert.equal(f.frames.size, 2);
assert.equal(f.timers.size, 1);
const late = [...f.frames.values(), ...f.timers.values()];
owner.destroy(); owner.destroy();
owner.destroy();
owner.destroy();
late.forEach((callback) => callback());
owner._scheduleAdaptivePanelLayout({ settle: true });
assert.equal(layouts, 0);
assert.equal(f.frames.size + f.timers.size, 0);
} finally { f.restore(); }
} finally {
f.restore();
}
});
test('panel disposal cancels a live drag without saving or accepting later pointer movement', () => {
const f = fixture();
try {
const owner = new PanelPositionControls({ syncPanelCollapseButton() {}, layoutRightPanels() {}, syncCctvPanelViewport() {}, showToast() {} });
const panel = f.element(), handle = f.element();
const owner = new PanelPositionControls({
syncPanelCollapseButton() {},
layoutRightPanels() {},
syncCctvPanelViewport() {},
showToast() {},
});
const panel = f.element(),
handle = f.element();
owner._makePanelDraggable('sample', panel, handle);
handle.dispatchEvent(Object.assign(new Event('pointerdown', { cancelable: true }), { button: 0, clientX: 30, clientY: 40 }));
handle.dispatchEvent(
Object.assign(new Event('pointerdown', { cancelable: true }), {
button: 0,
clientX: 30,
clientY: 40,
}),
);
assert.equal(panel.classList.contains('panel-dragging'), true);
owner.destroy();
const before = { ...panel.style };
window.dispatchEvent(Object.assign(new Event('pointermove'), { clientX: 500, clientY: 300 }));
window.dispatchEvent(
Object.assign(new Event('pointermove'), { clientX: 500, clientY: 300 }),
);
window.dispatchEvent(new Event('pointerup'));
assert.deepEqual(panel.style, before);
assert.equal(panel.classList.contains('panel-dragging'), false);
assert.equal(owner.removers.length, 0);
assert.deepEqual(f.writes, []);
} finally { f.restore(); }
} finally {
f.restore();
}
});
test('feedback disposal clears toast and polling work and rejects retained notices', () => {
const f = fixture();
try {
const owner = new ShellFeedback({ readLayers: () => [] });
owner.observeVisibility(); owner._startTrafficChipTicker(); owner._showToast('Saved');
owner.observeVisibility();
owner._startTrafficChipTicker();
owner._showToast('Saved');
assert.equal(f.timers.size, 2);
owner.destroy();
owner._showToast('Late'); owner._showGlobalStatusNotice('Late'); owner._startTrafficChipTicker();
owner._showToast('Late');
owner._showGlobalStatusNotice('Late');
owner._startTrafficChipTicker();
document.dispatchEvent(new Event('visibilitychange'));
assert.equal(f.timers.size, 0);
assert.equal(f.nodes.get('toast').textContent, 'Saved');
assert.equal(f.nodes.get('toast').classList.contains('visible'), false);
assert.equal(f.nodes.get('global-loading-status').hidden, true);
} finally { f.restore(); }
} finally {
f.restore();
}
});
test('recording restores the original HUD after repeated entry and revokes retained controls', () => {
const f = fixture();
try {
let mode = 'auto', variant = 'tactical';
let mode = 'auto',
variant = 'tactical';
const owner = new RecordingControls({ syncShareState() {} });
owner.hud = { getMode: () => mode, getVariant: () => variant, setMode: (value) => { mode = value; }, setVariant: (value) => { variant = value; }, visible: true };
owner.hud = {
getMode: () => mode,
getVariant: () => variant,
setMode: (value) => {
mode = value;
},
setVariant: (value) => {
variant = value;
},
visible: true,
};
owner.setRecordingMode(true, { hudMode: 'minimal', safeFrame: '9:16' });
owner.setRecordingMode(true, { hudMode: 'off' });
owner.destroy(); owner.setRecordingMode(true);
assert.equal(mode, 'auto'); assert.equal(variant, 'tactical');
owner.destroy();
owner.setRecordingMode(true);
assert.equal(mode, 'auto');
assert.equal(variant, 'tactical');
assert.equal(document.body.classList.contains('recording-mode'), false);
assert.equal(f.nodes.get('safe-frame-overlay').classList.contains('active'), false);
} finally { f.restore(); }
assert.equal(
f.nodes.get('safe-frame-overlay').classList.contains('active'),
false,
);
} finally {
f.restore();
}
});
+18 -5
View File
@@ -14,7 +14,11 @@
#cctv-panel .panel-glow {
position: absolute;
inset: -18px;
background: radial-gradient(ellipse at center, rgba(0, 212, 255, 0.16) 0%, transparent 65%);
background: radial-gradient(
ellipse at center,
rgba(0, 212, 255, 0.16) 0%,
transparent 65%
);
filter: blur(16px);
opacity: 0.25;
pointer-events: none;
@@ -51,7 +55,11 @@
}
.cctv-panel-inner::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(0, 212, 255, 0.6), rgba(0, 212, 255, 0.28));
background: linear-gradient(
180deg,
rgba(0, 212, 255, 0.6),
rgba(0, 212, 255, 0.28)
);
border: 1px solid rgba(0, 212, 255, 0.4);
border-radius: 999px;
}
@@ -99,7 +107,9 @@
border-radius: 7px;
padding: 4px 7px;
cursor: text;
transition: border-color 0.15s ease, background 0.15s ease;
transition:
border-color 0.15s ease,
background 0.15s ease;
}
.cctv-cal-value:hover:not(:disabled) {
@@ -212,7 +222,11 @@
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.12);
background:
radial-gradient(circle at 20% 20%, rgba(0, 244, 255, 0.09), transparent 44%),
radial-gradient(
circle at 20% 20%,
rgba(0, 244, 255, 0.09),
transparent 44%
),
linear-gradient(180deg, rgba(8, 14, 19, 0.88), rgba(4, 7, 11, 0.96));
min-height: 150px;
/* .cctv-panel-inner is a scrolling column flex container, so the default
@@ -335,4 +349,3 @@
color: #a8f7ff;
text-shadow: 0 0 10px rgba(0, 212, 255, 0.18);
}
+589 -222
View File
File diff suppressed because it is too large. Load diff
+42 -13
View File
@@ -21,11 +21,15 @@
}
#command-dock:has(#location-bar:not(.collapsed)) {
transform: translateX(calc(-50% + (var(--dock-location-open) - var(--dock-side-compact)) / 2));
transform: translateX(
calc(-50% + (var(--dock-location-open) - var(--dock-side-compact)) / 2)
);
}
#command-dock:has(#control-panel:not(.collapsed)) {
transform: translateX(calc(-50% - (var(--dock-presets-open) - var(--dock-side-compact)) / 2));
transform: translateX(
calc(-50% - (var(--dock-presets-open) - var(--dock-side-compact)) / 2)
);
}
#command-dock > #location-bar,
@@ -300,13 +304,27 @@
transform-origin: bottom;
}
#command-dock .gev-voice-visualizer span:nth-child(3n) { height: 1.35rem; }
#command-dock .gev-voice-visualizer span:nth-child(4n) { height: 0.95rem; }
#command-dock .gev-voice-visualizer span:nth-child(5n) { height: 1.7rem; }
#command-dock .gev-voice-visualizer span:nth-child(7n) { height: 0.75rem; }
#command-dock .gev-voice-visualizer span:nth-child(3n) {
height: 1.35rem;
}
#command-dock .gev-voice-visualizer span:nth-child(4n) {
height: 0.95rem;
}
#command-dock .gev-voice-visualizer span:nth-child(5n) {
height: 1.7rem;
}
#command-dock .gev-voice-visualizer span:nth-child(7n) {
height: 0.75rem;
}
#command-dock #gev-voice-control[data-status='listening'] .gev-voice-visualizer span,
#command-dock #gev-voice-control[data-status='executing'] .gev-voice-visualizer span {
#command-dock
#gev-voice-control[data-status='listening']
.gev-voice-visualizer
span,
#command-dock
#gev-voice-control[data-status='executing']
.gev-voice-visualizer
span {
animation: gev-voice-bar 680ms ease-in-out infinite alternate;
animation-delay: calc(var(--bar) * -45ms);
}
@@ -336,13 +354,25 @@
}
@keyframes command-dock-reveal {
from { opacity: 0; transform: translateY(0.2rem); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(0.2rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes gev-voice-bar {
from { transform: scaleY(0.35); opacity: 0.5; }
to { transform: scaleY(1); opacity: 1; }
from {
transform: scaleY(0.35);
opacity: 0.5;
}
to {
transform: scaleY(1);
opacity: 1;
}
}
/* Compact dock clearance keeps the reference-like low profile. */
@@ -401,4 +431,3 @@ body:not(.ui-clean-view):not(.recording-mode) #cesium-credits {
flex-basis: 8.5rem;
}
}
+103 -36
View File
@@ -170,7 +170,13 @@
}
#command-dock #location-bar .dock-popover-content {
left: calc((var(--dock-side-compact) + var(--dock-side-compact) + var(--dock-voice-width) - var(--dock-popover-mobile-width)) / 2);
left: calc(
(
var(--dock-side-compact) + var(--dock-side-compact) +
var(--dock-voice-width) - var(--dock-popover-mobile-width)
) /
2
);
}
#command-dock #control-panel .dock-popover-content {
@@ -182,7 +188,13 @@
left edge — pinned by the 480 px bounds assert in
`scripts/qa-map-source-tray.mjs`. */
width: var(--dock-popover-mobile-width);
right: calc((var(--dock-side-compact) + var(--dock-side-compact) + var(--dock-voice-width) - var(--dock-popover-mobile-width)) / 2);
right: calc(
(
var(--dock-side-compact) + var(--dock-side-compact) +
var(--dock-voice-width) - var(--dock-popover-mobile-width)
) /
2
);
}
/* ── Required-attribution clearance ─────────────────────────────────
@@ -213,8 +225,12 @@
bottom: calc(100% + 3rem);
}
#command-dock.dock-has-pinned-tray #location-bar:not(.collapsed):not(.dock-pinned) .dock-popover-content,
#command-dock.dock-has-pinned-tray #control-panel:not(.collapsed):not(.dock-pinned) .dock-popover-content {
#command-dock.dock-has-pinned-tray
#location-bar:not(.collapsed):not(.dock-pinned)
.dock-popover-content,
#command-dock.dock-has-pinned-tray
#control-panel:not(.collapsed):not(.dock-pinned)
.dock-popover-content {
bottom: calc(100% + 3rem + var(--dock-pinned-stack-height));
}
@@ -224,8 +240,12 @@
top tray drops var(--dock-lower-pinned-height), landing on top of its
pinned sibling. Naming the panel makes it (2,5,0), ahead of both the
ordinary (2,1,0) and single-pinned (2,4,0) rules. */
#command-dock.dock-has-two-pinned-trays #location-bar.dock-pinned-top.dock-pinned:not(.collapsed) .dock-popover-content,
#command-dock.dock-has-two-pinned-trays #control-panel.dock-pinned-top.dock-pinned:not(.collapsed) .dock-popover-content {
#command-dock.dock-has-two-pinned-trays
#location-bar.dock-pinned-top.dock-pinned:not(.collapsed)
.dock-popover-content,
#command-dock.dock-has-two-pinned-trays
#control-panel.dock-pinned-top.dock-pinned:not(.collapsed)
.dock-popover-content {
bottom: calc(100% + 3rem + var(--dock-lower-pinned-height));
}
}
@@ -242,14 +262,22 @@
bottom: calc(100% + 2vh + 2.5rem);
}
#command-dock.dock-has-pinned-tray #location-bar:not(.collapsed):not(.dock-pinned) .dock-popover-content,
#command-dock.dock-has-pinned-tray #control-panel:not(.collapsed):not(.dock-pinned) .dock-popover-content {
#command-dock.dock-has-pinned-tray
#location-bar:not(.collapsed):not(.dock-pinned)
.dock-popover-content,
#command-dock.dock-has-pinned-tray
#control-panel:not(.collapsed):not(.dock-pinned)
.dock-popover-content {
bottom: calc(100% + 2vh + 2.5rem + var(--dock-pinned-stack-height));
}
/* Panel IDs for the same specificity reason as the max-width:900px block. */
#command-dock.dock-has-two-pinned-trays #location-bar.dock-pinned-top.dock-pinned:not(.collapsed) .dock-popover-content,
#command-dock.dock-has-two-pinned-trays #control-panel.dock-pinned-top.dock-pinned:not(.collapsed) .dock-popover-content {
#command-dock.dock-has-two-pinned-trays
#location-bar.dock-pinned-top.dock-pinned:not(.collapsed)
.dock-popover-content,
#command-dock.dock-has-two-pinned-trays
#control-panel.dock-pinned-top.dock-pinned:not(.collapsed)
.dock-popover-content {
bottom: calc(100% + 2vh + 2.5rem + var(--dock-lower-pinned-height));
}
}
@@ -327,24 +355,39 @@
height: var(--audio-level, 5px);
opacity: var(--audio-opacity, 0.56);
animation: none;
transition: height 70ms linear, opacity 90ms linear;
transition:
height 70ms linear,
opacity 90ms linear;
}
#command-dock #gev-voice-control[data-status='listening'] .gev-voice-visualizer span,
#command-dock #gev-voice-control[data-status='executing'] .gev-voice-visualizer span {
#command-dock
#gev-voice-control[data-status='listening']
.gev-voice-visualizer
span,
#command-dock
#gev-voice-control[data-status='executing']
.gev-voice-visualizer
span {
animation: none;
}
#command-dock #gev-voice-control[data-speaker='user'] :is(#gev-voice-status, #gev-voice-detail) {
#command-dock
#gev-voice-control[data-speaker='user']
:is(#gev-voice-status, #gev-voice-detail) {
color: #ffbf69;
}
#command-dock #gev-voice-control[data-speaker='user'] .gev-voice-visualizer span {
#command-dock
#gev-voice-control[data-speaker='user']
.gev-voice-visualizer
span {
background: linear-gradient(to top, #d98a35, #ffcf8a);
box-shadow: 0 0 0.35rem rgba(255, 191, 105, 0.32);
}
#command-dock #gev-voice-control[data-speaker='ai'] :is(#gev-voice-status, #gev-voice-detail) {
#command-dock
#gev-voice-control[data-speaker='ai']
:is(#gev-voice-status, #gev-voice-detail) {
color: var(--accent);
}
@@ -364,11 +407,15 @@
@media (max-width: 900px) {
#command-dock #location-bar .dock-popover-content {
left: calc((var(--dock-unified-width) - var(--dock-popover-mobile-width)) / 2);
left: calc(
(var(--dock-unified-width) - var(--dock-popover-mobile-width)) / 2
);
}
#command-dock #control-panel .dock-popover-content {
right: calc((var(--dock-unified-width) - var(--dock-popover-mobile-width)) / 2);
right: calc(
(var(--dock-unified-width) - var(--dock-popover-mobile-width)) / 2
);
}
}
@@ -417,32 +464,44 @@
box-shadow:
0 0 0 0.18rem rgba(232, 234, 237, 0.04),
0 0 0.75rem rgba(232, 234, 237, 0.08);
transition: background var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast);
transition:
background var(--transition-fast),
border-color var(--transition-fast),
box-shadow var(--transition-fast);
}
#command-dock .gev-mic-orbit img {
filter: grayscale(1) saturate(0) brightness(1.65);
opacity: 0.68;
transition: filter var(--transition-fast), opacity var(--transition-fast);
transition:
filter var(--transition-fast),
opacity var(--transition-fast);
}
#command-dock #gev-voice-button:hover .gev-mic-orbit,
#command-dock #gev-voice-control:is(
[data-status='connecting'],
[data-status='listening']:not([data-microphone='muted']),
[data-status='executing']
) .gev-mic-orbit {
#command-dock
#gev-voice-control:is(
[data-status='connecting'],
[data-status='listening']:not([data-microphone='muted']),
[data-status='executing']
)
.gev-mic-orbit {
background: rgba(0, 212, 255, 0.06);
border-color: rgba(0, 212, 255, 0.48);
box-shadow: 0 0 0 0.18rem rgba(0, 212, 255, 0.06), 0 0 0.9rem rgba(0, 212, 255, 0.12);
box-shadow:
0 0 0 0.18rem rgba(0, 212, 255, 0.06),
0 0 0.9rem rgba(0, 212, 255, 0.12);
}
#command-dock #gev-voice-button:hover .gev-mic-orbit img,
#command-dock #gev-voice-control:is(
[data-status='connecting'],
[data-status='listening']:not([data-microphone='muted']),
[data-status='executing']
) .gev-mic-orbit img {
#command-dock
#gev-voice-control:is(
[data-status='connecting'],
[data-status='listening']:not([data-microphone='muted']),
[data-status='executing']
)
.gev-mic-orbit
img {
filter: none;
opacity: 1;
}
@@ -470,7 +529,6 @@
background: var(--glass-bg);
border-color: var(--glass-border-hover);
color: var(--accent);
}
#command-dock > :is(#location-bar, #control-panel).dock-pinned {
@@ -590,7 +648,9 @@
}
#gev-voice-control:not([data-status='error']):hover .gev-voice-help-tray,
#gev-voice-control:not([data-status='error']) #gev-voice-button:focus-visible ~ .gev-voice-help-tray {
#gev-voice-control:not([data-status='error'])
#gev-voice-button:focus-visible
~ .gev-voice-help-tray {
visibility: visible;
opacity: 1;
transform: translate(-50%, 0) scale(1);
@@ -622,10 +682,17 @@
}
#command-dock #location-bar .location-toolbar::after {
background: linear-gradient(90deg, rgba(232, 234, 237, 0.24), rgba(232, 234, 237, 0));
background: linear-gradient(
90deg,
rgba(232, 234, 237, 0.24),
rgba(232, 234, 237, 0)
);
}
#command-dock #control-panel .panel-header::after {
background: linear-gradient(90deg, rgba(232, 234, 237, 0), rgba(232, 234, 237, 0.24));
background: linear-gradient(
90deg,
rgba(232, 234, 237, 0),
rgba(232, 234, 237, 0.24)
);
}
+46 -18
View File
@@ -5,7 +5,9 @@
--dock-presets-pinned-height: 0px;
--dock-pinned-stack-height: 0px;
--dock-lower-pinned-height: 0px;
--dock-unified-width: calc(var(--dock-side-compact) * 2 + var(--dock-voice-width));
--dock-unified-width: calc(
var(--dock-side-compact) * 2 + var(--dock-voice-width)
);
--dock-location-open: max(34rem, calc(var(--dock-unified-width) + 0.75rem));
--dock-presets-open: max(34rem, calc(var(--dock-unified-width) + 0.75rem));
}
@@ -33,7 +35,9 @@
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
transition: background-color 160ms ease, filter 160ms ease;
transition:
background-color 160ms ease,
filter 160ms ease;
}
#command-dock .dock-label-icon-location {
@@ -53,12 +57,10 @@
}
#command-dock :is(#location-bar, #control-panel):hover .location-toolbar-label,
#command-dock :is(#location-bar, #control-panel):hover .panel-title{
#command-dock :is(#location-bar, #control-panel):hover .panel-title {
color: var(--accent);
}
#command-dock #control-panel .panel-header {
justify-content: center;
}
@@ -173,7 +175,9 @@
width: 0.9rem;
height: 0.9rem;
opacity: 0.58;
transition: opacity 160ms ease, transform 160ms ease;
transition:
opacity 160ms ease,
transform 160ms ease;
}
#command-dock #location-bar .dock-pin-btn {
@@ -188,12 +192,18 @@
transform: rotate(-12deg) scale(1.08);
}
#command-dock.dock-has-pinned-tray #location-bar:not(.collapsed):not(.dock-pinned) .dock-popover-content,
#command-dock.dock-has-pinned-tray #control-panel:not(.collapsed):not(.dock-pinned) .dock-popover-content {
#command-dock.dock-has-pinned-tray
#location-bar:not(.collapsed):not(.dock-pinned)
.dock-popover-content,
#command-dock.dock-has-pinned-tray
#control-panel:not(.collapsed):not(.dock-pinned)
.dock-popover-content {
bottom: calc(100% + 1.2rem + var(--dock-pinned-stack-height));
}
#command-dock.dock-has-two-pinned-trays .dock-pinned-top.dock-pinned:not(.collapsed) .dock-popover-content {
#command-dock.dock-has-two-pinned-trays
.dock-pinned-top.dock-pinned:not(.collapsed)
.dock-popover-content {
bottom: calc(100% + 1.2rem + var(--dock-lower-pinned-height));
}
@@ -264,7 +274,9 @@
background: rgba(0, 212, 255, 0.06);
border: 1px solid rgba(0, 212, 255, 0.48);
border-radius: 50%;
box-shadow: 0 0 0 0.18rem rgba(0, 212, 255, 0.06), 0 0 0.9rem rgba(0, 212, 255, 0.12);
box-shadow:
0 0 0 0.18rem rgba(0, 212, 255, 0.06),
0 0 0.9rem rgba(0, 212, 255, 0.12);
}
#command-dock .gev-mic-label {
@@ -288,9 +300,13 @@
transition: opacity 180ms ease;
}
#command-dock #gev-voice-control[data-status='connecting'] .gev-voice-visualizer,
#command-dock
#gev-voice-control[data-status='connecting']
.gev-voice-visualizer,
#command-dock #gev-voice-control[data-status='listening'] .gev-voice-visualizer,
#command-dock #gev-voice-control[data-status='executing'] .gev-voice-visualizer {
#command-dock
#gev-voice-control[data-status='executing']
.gev-voice-visualizer {
visibility: visible;
opacity: 1;
transform: none;
@@ -362,16 +378,23 @@
background: rgba(34, 10, 16, 0.96);
border: 1px solid rgba(255, 92, 110, 0.5);
border-radius: 0.42rem;
box-shadow: 0 0.9rem 2.2rem rgba(0, 0, 0, 0.62), 0 0 1.4rem rgba(255, 92, 110, 0.08);
box-shadow:
0 0.9rem 2.2rem rgba(0, 0, 0, 0.62),
0 0 1.4rem rgba(255, 92, 110, 0.08);
backdrop-filter: blur(28px) saturate(1.25);
-webkit-backdrop-filter: blur(28px) saturate(1.25);
pointer-events: none;
transform: translate(-50%, 0.55rem) scale(0.985);
transform-origin: bottom center;
transition: opacity 180ms ease, visibility 180ms ease, transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
transition:
opacity 180ms ease,
visibility 180ms ease,
transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
#command-dock #gev-voice-control[data-status='error']:not(.error-dismissed) .gev-voice-error-tray {
#command-dock
#gev-voice-control[data-status='error']:not(.error-dismissed)
.gev-voice-error-tray {
visibility: visible;
opacity: 1;
pointer-events: auto;
@@ -421,7 +444,10 @@
/* The compact dock leaves both lower corners free for required attribution and telemetry. */
body:not(.ui-clean-view):not(.recording-mode) #cesium-credits,
body:not(.ui-clean-view):not(.recording-mode):has(#intel-hud[data-variant='minimal'].active) #cesium-credits {
body:not(.ui-clean-view):not(.recording-mode):has(
#intel-hud[data-variant='minimal'].active
)
#cesium-credits {
right: auto;
bottom: 0.75rem;
left: 1.5rem;
@@ -452,7 +478,10 @@ body:not(.ui-clean-view):not(.recording-mode):has(#intel-hud[data-variant='minim
@media (max-width: 900px) {
body:not(.ui-clean-view):not(.recording-mode) #cesium-credits,
body:not(.ui-clean-view):not(.recording-mode):has(#intel-hud[data-variant='minimal'].active) #cesium-credits {
body:not(.ui-clean-view):not(.recording-mode):has(
#intel-hud[data-variant='minimal'].active
)
#cesium-credits {
bottom: calc(2vh + 5rem);
left: 0.75rem;
max-width: calc(100vw - 1.5rem);
@@ -480,4 +509,3 @@ body:not(.ui-clean-view):not(.recording-mode):has(#intel-hud[data-variant='minim
bottom: calc(50vh + 8px);
}
}
+11 -3
View File
@@ -7,7 +7,10 @@
z-index: 145;
display: grid;
grid-template-areas: 'locations voice presets';
grid-template-columns: minmax(300px, 1fr) minmax(230px, 270px) minmax(390px, 1.15fr);
grid-template-columns: minmax(300px, 1fr) minmax(230px, 270px) minmax(
390px,
1.15fr
);
align-items: stretch;
width: min(94vw, 1240px);
padding: 6px;
@@ -22,7 +25,9 @@
backdrop-filter: blur(28px) saturate(1.35);
-webkit-backdrop-filter: blur(28px) saturate(1.35);
transform: translateX(-50%);
transition: opacity var(--transition-smooth), visibility var(--transition-smooth);
transition:
opacity var(--transition-smooth),
visibility var(--transition-smooth);
}
#command-dock > #location-bar,
@@ -214,7 +219,10 @@ body:not(.ui-clean-view):not(.recording-mode) #cesium-credits {
bottom: calc(2vh + 7rem);
}
body:not(.ui-clean-view):not(.recording-mode):has(#intel-hud[data-variant='minimal'].active) #cesium-credits {
body:not(.ui-clean-view):not(.recording-mode):has(
#intel-hud[data-variant='minimal'].active
)
#cesium-credits {
right: 36px;
left: auto;
width: min(340px, calc(100vw - 72px)) !important;
+76 -22
View File
@@ -12,7 +12,11 @@
#control-panel .panel-glow {
position: absolute;
inset: -30px;
background: radial-gradient(ellipse at center bottom, var(--accent-dim) 0%, transparent 60%);
background: radial-gradient(
ellipse at center bottom,
var(--accent-dim) 0%,
transparent 60%
);
filter: blur(30px);
opacity: 0.4;
pointer-events: none;
@@ -185,7 +189,11 @@
content: '';
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, transparent 100%);
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.04) 0%,
transparent 100%
);
opacity: 0;
transition: opacity var(--transition-fast);
}
@@ -264,7 +272,9 @@
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.8s ease, visibility 0.8s ease;
transition:
opacity 0.8s ease,
visibility 0.8s ease;
}
#loading-screen.hidden {
@@ -287,8 +297,15 @@
}
@keyframes logo-pulse {
0%, 100% { opacity: 0.72; transform: scale(0.98); }
50% { opacity: 1; transform: scale(1.02); }
0%,
100% {
opacity: 0.72;
transform: scale(0.98);
}
50% {
opacity: 1;
transform: scale(1.02);
}
}
.loader-content h2 {
@@ -309,8 +326,13 @@
}
@keyframes pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
0%,
100% {
opacity: 0.4;
}
50% {
opacity: 1;
}
}
/* ── Post-Processing Toggles ─────────────────── */
@@ -332,7 +354,9 @@
border: 1px solid var(--glass-border);
border-radius: var(--panel-radius);
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);
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);
}
@@ -389,7 +413,9 @@
border: 1px solid var(--glass-border);
border-radius: var(--panel-radius);
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);
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);
cursor: default;
@@ -461,7 +487,11 @@
font-size: 14px;
font-weight: 400;
font-style: normal;
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 20;
}
.pp-label {
@@ -600,7 +630,7 @@
outline-offset: 2px;
}
.gev-quantitative-slider:disabled {
opacity: .42;
opacity: 0.42;
cursor: not-allowed;
}
.gev-slider-value {
@@ -608,7 +638,7 @@
color: var(--accent);
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: .5px;
letter-spacing: 0.5px;
text-align: right;
flex-shrink: 0;
}
@@ -793,7 +823,9 @@
}
/* Proximity / All segmented control under the 3D toggle */
#models3d-mode-row { min-width: 184px; }
#models3d-mode-row {
min-width: 184px;
}
.pp-mode-seg {
display: flex;
flex: 1;
@@ -812,11 +844,21 @@
letter-spacing: 0.6px;
text-transform: uppercase;
cursor: pointer;
transition: background-color var(--transition-fast), color var(--transition-fast);
transition:
background-color var(--transition-fast),
color var(--transition-fast);
}
.pp-mode-btn + .pp-mode-btn {
border-left: 1px solid rgba(255, 255, 255, 0.14);
}
.pp-mode-btn:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--text-primary);
}
.pp-mode-btn.active {
background: var(--accent);
color: #000;
}
.pp-mode-btn + .pp-mode-btn { border-left: 1px solid rgba(255, 255, 255, 0.14); }
.pp-mode-btn:hover { background: rgba(255, 255, 255, 0.08); color: var(--text-primary); }
.pp-mode-btn.active { background: var(--accent); color: #000; }
/* These controls also move into Cockpit Display; keep focus on the control
itself and inside clipped button groups, independent of selection color. */
@@ -913,8 +955,13 @@ body.ui-clean-view #scene-runtime.active {
}
@keyframes detection-pulse {
0%, 100% { box-shadow: 0 0 12px rgba(0, 255, 80, 0.15); }
50% { box-shadow: 0 0 22px rgba(0, 255, 80, 0.35); }
0%,
100% {
box-shadow: 0 0 12px rgba(0, 255, 80, 0.15);
}
50% {
box-shadow: 0 0 22px rgba(0, 255, 80, 0.35);
}
}
/* ── Parameter Slider Panel ──────────────────── */
@@ -942,7 +989,10 @@ body.ui-clean-view #scene-runtime.active {
opacity: 0;
visibility: hidden;
transform: translateY(6px);
transition: opacity var(--transition-smooth), visibility var(--transition-smooth), transform var(--transition-smooth);
transition:
opacity var(--transition-smooth),
visibility var(--transition-smooth),
transform var(--transition-smooth);
}
#param-slider-panel::-webkit-scrollbar {
@@ -988,7 +1038,12 @@ body.ui-clean-view #scene-runtime.active {
}
#param-slider-panel:not(.collapsed) .param-panel-divider {
background: linear-gradient(90deg, rgb(0 212 255 / 28%), rgba(0, 212, 255, 0.18) 58%, transparent);
background: linear-gradient(
90deg,
rgb(0 212 255 / 28%),
rgba(0, 212, 255, 0.18) 58%,
transparent
);
}
.param-slider-row {
@@ -1058,4 +1113,3 @@ body.ui-clean-view #scene-runtime.active {
text-align: right;
flex-shrink: 0;
}
+96 -28
View File
@@ -31,7 +31,9 @@
-webkit-backdrop-filter: blur(28px) saturate(1.25);
opacity: 0;
transform: translate(-50%, calc(-50% + 0.8rem)) scale(0.985);
transition: opacity 220ms ease, transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1);
transition:
opacity 220ms ease,
transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1);
/* Inert until revealed AND again while fading out, so a click during the
dismiss animation cannot launch a second mission behind the fade. */
pointer-events: none;
@@ -55,7 +57,12 @@
position: absolute;
inset: 0 0 auto;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(54, 220, 255, 0.8), transparent);
background: linear-gradient(
90deg,
transparent,
rgba(54, 220, 255, 0.8),
transparent
);
box-shadow: 0 0 1rem rgba(54, 220, 255, 0.45);
pointer-events: none;
}
@@ -73,7 +80,9 @@
text-transform: uppercase;
}
.first-run-kicker { color: rgba(54, 220, 255, 0.86); }
.first-run-kicker {
color: rgba(54, 220, 255, 0.86);
}
#first-run-launcher h2 {
margin-top: 0.6rem;
@@ -107,7 +116,9 @@
scrollbar-gutter: stable;
}
.first-run-choices::-webkit-scrollbar { width: 6px; }
.first-run-choices::-webkit-scrollbar {
width: 6px;
}
.first-run-choices::-webkit-scrollbar-thumb {
background: rgba(54, 220, 255, 0.45);
border-radius: 3px;
@@ -127,7 +138,10 @@
border: 1px solid rgba(100, 200, 220, 0.18);
border-radius: 0.42rem;
cursor: pointer;
transition: background var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast);
transition:
background var(--transition-fast),
border-color var(--transition-fast),
transform var(--transition-fast);
}
.first-run-choices button:hover,
@@ -173,7 +187,9 @@
.first-run-arrow {
color: rgba(54, 220, 255, 0.4);
font-size: 1rem;
transition: transform var(--transition-fast), color var(--transition-fast);
transition:
transform var(--transition-fast),
color var(--transition-fast);
}
.first-run-choices button:hover .first-run-arrow,
@@ -212,7 +228,9 @@
cursor: pointer;
}
.first-run-suppress:hover { color: rgba(225, 244, 248, 0.92); }
.first-run-suppress:hover {
color: rgba(225, 244, 248, 0.92);
}
/* The live region carries mission progress and failures, so it owns a full row
and reserves its height — otherwise the card resizes as it speaks. */
@@ -249,15 +267,31 @@ body.scene-playback-mode #first-run-launcher {
transform: translate(-50%, 0.8rem) scale(0.985);
}
#first-run-launcher.visible { transform: translate(-50%, 0) scale(1); }
#first-run-launcher.visible {
transform: translate(-50%, 0) scale(1);
}
/* Five tiles have to clear the dock on a 375 x 812 phone, so the rows tighten
and the redundant "ESC to dismiss" hint (no ESC key on a phone) drops. */
#first-run-launcher h2 { margin-top: 0.45rem; }
#first-run-description { margin-top: 0.25rem; font-size: 0.72rem; }
.first-run-choices { margin-top: 0.75rem; }
.first-run-choices button { min-height: 3rem; padding: 0.45rem 0.6rem; }
.first-run-choices small { font-size: 0.62rem; }
.first-run-footer > span:last-child { display: none; }
#first-run-launcher h2 {
margin-top: 0.45rem;
}
#first-run-description {
margin-top: 0.25rem;
font-size: 0.72rem;
}
.first-run-choices {
margin-top: 0.75rem;
}
.first-run-choices button {
min-height: 3rem;
padding: 0.45rem 0.6rem;
}
.first-run-choices small {
font-size: 0.62rem;
}
.first-run-footer > span:last-child {
display: none;
}
}
/* Short viewports — landscape phones, small laptop windows. The chrome tightens
@@ -266,15 +300,38 @@ body.scene-playback-mode #first-run-launcher {
fits. (It was dropped here while there were five; the tile that went is
exactly why it can come back.) */
@media (max-height: 620px) {
#first-run-launcher { padding: 0.75rem 0.85rem; }
#first-run-launcher h2 { margin-top: 0.3rem; font-size: 1.1rem; }
#first-run-description { margin-top: 0.2rem; font-size: 0.7rem; line-height: 1.4; }
.first-run-choices { margin-top: 0.55rem; }
.first-run-choices button { min-height: 2.7rem; padding: 0.36rem 0.6rem; }
.first-run-choices strong { font-size: 0.68rem; }
.first-run-choices small { font-size: 0.6rem; }
.first-run-footer { margin-top: 0.5rem; }
.first-run-note { margin-top: 0.3rem; min-height: 0; }
#first-run-launcher {
padding: 0.75rem 0.85rem;
}
#first-run-launcher h2 {
margin-top: 0.3rem;
font-size: 1.1rem;
}
#first-run-description {
margin-top: 0.2rem;
font-size: 0.7rem;
line-height: 1.4;
}
.first-run-choices {
margin-top: 0.55rem;
}
.first-run-choices button {
min-height: 2.7rem;
padding: 0.36rem 0.6rem;
}
.first-run-choices strong {
font-size: 0.68rem;
}
.first-run-choices small {
font-size: 0.6rem;
}
.first-run-footer {
margin-top: 0.5rem;
}
.first-run-note {
margin-top: 0.3rem;
min-height: 0;
}
}
/* A soft edge on the list, so a tile cut by the fold reads as "more below"
@@ -283,8 +340,16 @@ body.scene-playback-mode #first-run-launcher {
promise content that is not there, which is the opposite of the affordance
it is meant to be. Decorative only: the list scrolls under it. */
.first-run-choices[data-scrollable='true'] {
-webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 1.4rem), transparent 100%);
mask-image: linear-gradient(to bottom, #000 calc(100% - 1.4rem), transparent 100%);
-webkit-mask-image: linear-gradient(
to bottom,
#000 calc(100% - 1.4rem),
transparent 100%
);
mask-image: linear-gradient(
to bottom,
#000 calc(100% - 1.4rem),
transparent 100%
);
}
/* Scrolled to the end, there is nothing left to promise, so the fade lifts.
@@ -308,8 +373,11 @@ body.scene-playback-mode #first-run-launcher {
@media (prefers-reduced-motion: reduce) {
#first-run-launcher,
.first-run-choices button,
.first-run-arrow { transition: none; }
.first-run-arrow {
transition: none;
}
/* Scrolling a tile into view must not animate either. */
.first-run-choices { scroll-behavior: auto; }
.first-run-choices {
scroll-behavior: auto;
}
}
+25 -12
View File
@@ -69,7 +69,8 @@
outline-offset: 2px;
}
html, body {
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
@@ -185,7 +186,9 @@ body.cockpit-mode #cockpit-cloud-effects.active {
pointer-events: none;
opacity: 0;
visibility: hidden;
transition: opacity 300ms ease, visibility 300ms ease;
transition:
opacity 300ms ease,
visibility 300ms ease;
}
.celestial-ring-overlay.visible {
@@ -222,19 +225,27 @@ body.cockpit-mode #cockpit-cloud-effects.active {
font-weight: 400;
font-style: normal;
line-height: 1;
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 20;
transition: opacity 120ms linear;
user-select: none;
}
.celestial-sun {
color: rgba(231, 194, 72, 0.92);
text-shadow: 0 0 7px rgba(231, 194, 72, 0.72), 0 0 18px rgba(231, 194, 72, 0.3);
text-shadow:
0 0 7px rgba(231, 194, 72, 0.72),
0 0 18px rgba(231, 194, 72, 0.3);
}
.celestial-moon {
color: rgba(55, 211, 239, 0.84);
text-shadow: 0 0 7px rgba(55, 211, 239, 0.65), 0 0 17px rgba(55, 211, 239, 0.25);
text-shadow:
0 0 7px rgba(55, 211, 239, 0.65),
0 0 17px rgba(55, 211, 239, 0.25);
}
#celestial-toggle:disabled {
@@ -499,7 +510,7 @@ body.cockpit-mode #cockpit-cloud-effects.active {
#title-bar .title-logo::before,
#title-bar .title-logo::after {
--radio-broadcast-opacity: .28;
--radio-broadcast-opacity: 0.28;
content: '';
position: absolute;
z-index: 1;
@@ -517,7 +528,7 @@ body.cockpit-mode #cockpit-cloud-effects.active {
}
#title-bar .title-logo::after {
--radio-broadcast-opacity: .17;
--radio-broadcast-opacity: 0.17;
top: -15px;
width: 27px;
height: 13px;
@@ -529,12 +540,15 @@ body.cockpit-mode #cockpit-cloud-effects.active {
animation: title-radio-broadcast 1.8s ease-in-out infinite;
}
#title-bar.radio-broadcasting .title-logo::after { animation-delay: .28s; }
#title-bar.radio-broadcasting .title-logo::after {
animation-delay: 0.28s;
}
@keyframes title-radio-broadcast {
0%, 100% {
opacity: calc(var(--radio-broadcast-opacity) * .55);
transform: translateX(-50%) scale(.94);
0%,
100% {
opacity: calc(var(--radio-broadcast-opacity) * 0.55);
transform: translateX(-50%) scale(0.94);
}
50% {
opacity: var(--radio-broadcast-opacity);
@@ -608,4 +622,3 @@ body.cockpit-mode #cockpit-cloud-effects.active {
text-shadow: 0 0 20px var(--accent-glow);
transition: var(--transition-smooth);
}
+390 -182
View File
File diff suppressed because it is too large. Load diff
+15 -7
View File
@@ -12,7 +12,11 @@
#location-bar .panel-glow {
position: absolute;
inset: -20px;
background: radial-gradient(ellipse at center, var(--accent-dim) 0%, transparent 60%);
background: radial-gradient(
ellipse at center,
var(--accent-dim) 0%,
transparent 60%
);
filter: blur(20px);
opacity: 0.25;
pointer-events: none;
@@ -137,9 +141,10 @@
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 250ms cubic-bezier(0.4, 0, 0.2, 1),
opacity 200ms ease,
padding 250ms cubic-bezier(0.4, 0, 0.2, 1);
transition:
max-height 250ms cubic-bezier(0.4, 0, 0.2, 1),
opacity 200ms ease,
padding 250ms cubic-bezier(0.4, 0, 0.2, 1);
padding: 0 4px;
}
@@ -155,7 +160,9 @@
border-top: 1px solid var(--glass-border);
margin: 0 4px;
opacity: 0;
transition: opacity 200ms ease, margin 250ms ease;
transition:
opacity 200ms ease,
margin 250ms ease;
}
.location-bar-divider.visible {
@@ -272,7 +279,9 @@
font-family: var(--font-sans);
font-size: 13px;
letter-spacing: 0.3px;
transition: width var(--transition-smooth), padding var(--transition-smooth);
transition:
width var(--transition-smooth),
padding var(--transition-smooth);
caret-color: var(--accent);
}
@@ -289,4 +298,3 @@
#location-search.searching {
opacity: 0.5;
}
+15 -6
View File
@@ -40,7 +40,8 @@
#safe-frame-box::before {
background:
linear-gradient(to right,
linear-gradient(
to right,
transparent 33.333%,
rgba(255, 255, 255, 0.2) 33.333%,
rgba(255, 255, 255, 0.2) 33.7%,
@@ -50,7 +51,8 @@
rgba(255, 255, 255, 0.2) 66.7%,
transparent 66.7%
),
linear-gradient(to bottom,
linear-gradient(
to bottom,
transparent 33.333%,
rgba(255, 255, 255, 0.2) 33.333%,
rgba(255, 255, 255, 0.2) 33.7%,
@@ -77,7 +79,9 @@
user-select: none;
opacity: 0;
visibility: hidden;
transition: opacity var(--transition-smooth), visibility var(--transition-smooth);
transition:
opacity var(--transition-smooth),
visibility var(--transition-smooth);
}
#orbit-indicator.active {
@@ -99,7 +103,9 @@
pointer-events: none;
opacity: 0;
visibility: hidden;
transition: opacity 0.4s ease, visibility 0.4s ease;
transition:
opacity 0.4s ease,
visibility 0.4s ease;
--hud-color: rgba(0, 255, 255, 0.6);
--hud-glow: rgba(0, 255, 255, 0.4);
--hud-border: rgba(0, 255, 255, 0.15);
@@ -322,7 +328,11 @@
#intel-hud[data-variant='operator'] .hud-bottom-bar {
border-color: rgba(255, 255, 255, 0.2);
background: linear-gradient(90deg, rgba(0, 0, 0, 0.52), rgba(8, 22, 18, 0.52));
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0.52),
rgba(8, 22, 18, 0.52)
);
}
#intel-hud[data-variant='operator'] .hud-corner {
@@ -361,4 +371,3 @@
#intel-hud[data-variant='minimal'] .hud-summary-wrap {
max-width: 280px;
}
+56 -16
View File
@@ -17,7 +17,11 @@
font-family: var(--font-mono);
font-size: 0.6rem;
letter-spacing: 0.09rem;
background: linear-gradient(145deg, rgba(10, 27, 37, 0.92), rgba(7, 13, 22, 0.9));
background: linear-gradient(
145deg,
rgba(10, 27, 37, 0.92),
rgba(7, 13, 22, 0.9)
);
border: 1px solid rgba(54, 220, 255, 0.38);
border-radius: 0.42rem;
box-shadow:
@@ -26,12 +30,19 @@
backdrop-filter: blur(18px) saturate(1.2);
-webkit-backdrop-filter: blur(18px) saturate(1.2);
cursor: pointer;
transition: border-color var(--transition-fast), background var(--transition-fast), transform var(--transition-fast);
transition:
border-color var(--transition-fast),
background var(--transition-fast),
transform var(--transition-fast);
}
#key-setup-chip:hover,
#key-setup-chip:focus-visible {
background: linear-gradient(145deg, rgba(14, 38, 51, 0.95), rgba(9, 18, 28, 0.93));
background: linear-gradient(
145deg,
rgba(14, 38, 51, 0.95),
rgba(9, 18, 28, 0.93)
);
border-color: rgba(54, 220, 255, 0.7);
outline: none;
transform: translateY(-1px);
@@ -42,7 +53,9 @@
font-size: 0.95rem;
}
#key-setup-chip[hidden] { display: none; }
#key-setup-chip[hidden] {
display: none;
}
#key-setup {
position: fixed;
@@ -70,12 +83,16 @@
-webkit-backdrop-filter: blur(28px) saturate(1.25);
opacity: 0;
transform: translate(-50%, calc(-50% + 0.8rem)) scale(0.985);
transition: opacity 220ms ease, transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1);
transition:
opacity 220ms ease,
transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1);
pointer-events: none;
}
/* Same cascade note as the launcher: an author display beats [hidden]. */
#key-setup[hidden] { display: none; }
#key-setup[hidden] {
display: none;
}
#key-setup.visible {
opacity: 1;
@@ -87,7 +104,12 @@
position: absolute;
inset: 0 0 auto;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(54, 220, 255, 0.8), transparent);
background: linear-gradient(
90deg,
transparent,
rgba(54, 220, 255, 0.8),
transparent
);
box-shadow: 0 0 1rem rgba(54, 220, 255, 0.45);
pointer-events: none;
}
@@ -105,7 +127,9 @@
text-transform: uppercase;
}
.key-setup-kicker { color: rgba(54, 220, 255, 0.86); }
.key-setup-kicker {
color: rgba(54, 220, 255, 0.86);
}
.key-setup-close {
display: grid;
@@ -116,7 +140,9 @@
border: 1px solid transparent;
border-radius: 0.3rem;
cursor: pointer;
transition: color var(--transition-fast), border-color var(--transition-fast);
transition:
color var(--transition-fast),
border-color var(--transition-fast);
}
.key-setup-close:hover,
@@ -126,7 +152,9 @@
outline: none;
}
.key-setup-close .material-symbols-outlined { font-size: 1rem; }
.key-setup-close .material-symbols-outlined {
font-size: 1rem;
}
#key-setup h2 {
margin-top: 0.6rem;
@@ -163,7 +191,9 @@
scrollbar-gutter: stable;
}
.key-setup-rows::-webkit-scrollbar { width: 6px; }
.key-setup-rows::-webkit-scrollbar {
width: 6px;
}
.key-setup-rows::-webkit-scrollbar-thumb {
background: rgba(54, 220, 255, 0.45);
border-radius: 3px;
@@ -209,7 +239,9 @@
box-shadow: 0 0 0.5rem rgba(84, 227, 156, 0.66);
}
.key-setup-tier { font-size: 0.55rem; }
.key-setup-tier {
font-size: 0.55rem;
}
.key-setup-exposed {
padding: 0.05rem 0.3rem;
@@ -264,9 +296,13 @@
outline: none;
}
.key-setup-fields input::placeholder { color: rgba(148, 178, 188, 0.42); }
.key-setup-fields input::placeholder {
color: rgba(148, 178, 188, 0.42);
}
.key-setup-footer { margin-top: 0.85rem; }
.key-setup-footer {
margin-top: 0.85rem;
}
.key-setup-apply {
padding: 0.5rem 0.95rem;
@@ -318,7 +354,9 @@ body.scene-playback-mode #key-setup {
@media (max-width: 620px) {
/* Clear the dock. Desktop-first app; this is a courtesy, not a layout. */
#key-setup-chip { bottom: 5.8rem; }
#key-setup-chip {
bottom: 5.8rem;
}
}
/* Provider Settings additions: externally-managed badge + per-row remove. */
@@ -344,7 +382,9 @@ body.scene-playback-mode #key-setup {
border: 1px solid rgba(255, 170, 150, 0.3);
border-radius: 0.32rem;
cursor: pointer;
transition: color var(--transition-fast), border-color var(--transition-fast);
transition:
color var(--transition-fast),
border-color var(--transition-fast);
}
.key-setup-remove:hover,
+314 -88
View File
@@ -5,7 +5,7 @@
min-height: 0;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(65, 220, 231, .16);
border-top: 1px solid rgba(65, 220, 231, 0.16);
}
.radio-panel-inner {
position: relative;
@@ -14,20 +14,24 @@
flex-direction: column;
gap: 9px;
padding: 10px;
border: 1px solid rgba(65, 220, 231, .18);
border: 1px solid rgba(65, 220, 231, 0.18);
border-radius: 8px;
background: rgba(2, 15, 20, .42);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .035);
background: rgba(2, 15, 20, 0.42);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035);
scrollbar-width: thin;
scrollbar-color: rgba(0, 212, 255, .46) rgba(255, 255, 255, .06);
scrollbar-color: rgba(0, 212, 255, 0.46) rgba(255, 255, 255, 0.06);
}
#radio-panel .panel-header {
margin-bottom: 0;
}
#radio-panel .panel-header { margin-bottom: 0; }
.radio-layer-state {
color: var(--text-dim);
font: 8px/1 var(--font-mono);
letter-spacing: .12em;
letter-spacing: 0.12em;
}
.radio-layer-state.active {
color: #65f7ff;
}
.radio-layer-state.active { color: #65f7ff; }
.radio-directory-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
@@ -38,37 +42,42 @@
grid-column: 2;
color: var(--text-dim);
font: 7px/1 var(--font-mono);
letter-spacing: .12em;
letter-spacing: 0.12em;
}
#radio-enable-btn {
grid-row: 1 / span 2;
min-height: 36px;
}
#radio-enable-btn { grid-row: 1 / span 2; min-height: 36px; }
#radio-filter {
min-width: 0;
padding: 7px 24px 7px 8px;
border: 1px solid rgba(65, 220, 231, .28);
border: 1px solid rgba(65, 220, 231, 0.28);
border-radius: 5px;
color: #c5faff;
background: rgba(5, 24, 31, .88);
background: rgba(5, 24, 31, 0.88);
font: 9px/1.2 var(--font-mono);
}
.radio-station-card {
display: grid;
gap: 4px;
padding: 10px;
border: 1px solid rgba(65, 220, 231, .18);
border: 1px solid rgba(65, 220, 231, 0.18);
border-radius: 7px;
background: rgba(2, 15, 20, .48);
background: rgba(2, 15, 20, 0.48);
overflow-wrap: anywhere;
}
.radio-station-card strong {
color: #e8feff;
font: 11px/1.25 var(--font-mono);
letter-spacing: .05em;
letter-spacing: 0.05em;
}
.radio-station-card span {
color: var(--text-dim);
font: 8px/1.35 var(--font-mono);
}
#radio-station-tags { color: #72dce6; }
#radio-station-tags {
color: #72dce6;
}
.radio-tuner {
display: grid;
width: 100%;
@@ -77,12 +86,18 @@
gap: 5px;
padding: 8px;
overflow: hidden;
border: 1px solid rgba(65, 220, 231, .22);
border: 1px solid rgba(65, 220, 231, 0.22);
border-radius: 7px;
background: linear-gradient(180deg, rgba(1, 9, 13, .92), rgba(4, 24, 30, .72));
box-shadow: inset 0 0 18px rgba(0, 212, 255, .045);
background: linear-gradient(
180deg,
rgba(1, 9, 13, 0.92),
rgba(4, 24, 30, 0.72)
);
box-shadow: inset 0 0 18px rgba(0, 212, 255, 0.045);
}
.radio-tuner[hidden] {
display: none;
}
.radio-tuner[hidden] { display: none; }
.radio-tuner-header,
.radio-tuner-readout {
display: flex;
@@ -90,7 +105,7 @@
gap: 8px;
color: #6daeb5;
font: 7px/1.2 var(--font-mono);
letter-spacing: .1em;
letter-spacing: 0.1em;
}
#radio-tuner-value {
min-width: 0;
@@ -108,10 +123,22 @@
height: 51px;
overflow: hidden;
contain: paint;
border-block: 1px solid rgba(72, 225, 237, .3);
border-block: 1px solid rgba(72, 225, 237, 0.3);
background:
linear-gradient(180deg, transparent 23px, rgba(65, 220, 231, .8) 24px, transparent 25px, transparent 28px, rgba(65, 220, 231, .6) 29px, transparent 30px),
radial-gradient(ellipse at center, rgba(39, 177, 191, .12), transparent 70%);
linear-gradient(
180deg,
transparent 23px,
rgba(65, 220, 231, 0.8) 24px,
transparent 25px,
transparent 28px,
rgba(65, 220, 231, 0.6) 29px,
transparent 30px
),
radial-gradient(
ellipse at center,
rgba(39, 177, 191, 0.12),
transparent 70%
);
}
.radio-tuner-scale {
position: absolute;
@@ -119,7 +146,7 @@
overflow: hidden;
color: #58cbd6;
font: 9px/1 var(--font-mono);
text-shadow: 0 0 7px rgba(65, 220, 231, .5);
text-shadow: 0 0 7px rgba(65, 220, 231, 0.5);
pointer-events: none;
}
.radio-tuner-tick {
@@ -127,7 +154,7 @@
top: 6px;
bottom: 0;
min-width: 1px;
border-left: 1px solid rgba(85, 224, 235, .38);
border-left: 1px solid rgba(85, 224, 235, 0.38);
color: #58cbd6;
padding-left: 3px;
white-space: nowrap;
@@ -135,7 +162,7 @@
transition: left 80ms linear;
}
.radio-tuner-tick.is-current {
border-left-color: rgba(168, 247, 255, .9);
border-left-color: rgba(168, 247, 255, 0.9);
color: #a8f7ff;
}
.radio-tuner-needle {
@@ -147,13 +174,17 @@
width: 3px;
border-radius: 1px;
background: #ff6f4f;
box-shadow: 0 0 4px #ff6f4f, 0 0 10px rgba(255, 77, 50, .65);
box-shadow:
0 0 4px #ff6f4f,
0 0 10px rgba(255, 77, 50, 0.65);
pointer-events: none;
transform: translateX(-50%);
transition: left .18s ease-out;
transition: left 0.18s ease-out;
}
.radio-tuner.is-dragging .radio-tuner-needle,
.radio-tuner.is-dragging .radio-tuner-tick { transition: none; }
.radio-tuner.is-dragging .radio-tuner-tick {
transition: none;
}
#radio-tuner-slider {
position: absolute;
z-index: 2;
@@ -184,7 +215,10 @@
background: transparent;
box-shadow: none;
}
#radio-tuner-slider::-moz-range-track { height: 100%; background: transparent; }
#radio-tuner-slider::-moz-range-track {
height: 100%;
background: transparent;
}
#radio-tuner-slider::-moz-range-thumb {
width: 14px;
height: 47px;
@@ -194,9 +228,11 @@
box-shadow: none;
}
.radio-tuner.is-static .radio-tuner-dial {
box-shadow: inset 0 0 14px rgba(255, 111, 79, .1);
box-shadow: inset 0 0 14px rgba(255, 111, 79, 0.1);
}
.radio-tuner.is-static #radio-tuner-station {
color: #ff9b7e;
}
.radio-tuner.is-static #radio-tuner-station { color: #ff9b7e; }
.radio-tuner-readout span:first-child {
min-width: 0;
color: #7fe6ef;
@@ -216,12 +252,12 @@
.radio-transport button {
min-height: 30px;
padding: 6px 3px;
border: 1px solid rgba(65, 220, 231, .28);
border: 1px solid rgba(65, 220, 231, 0.28);
border-radius: 5px;
color: #8feaf2;
background: rgba(14, 69, 78, .14);
background: rgba(14, 69, 78, 0.14);
font: 8px/1 var(--font-mono);
letter-spacing: .06em;
letter-spacing: 0.06em;
cursor: pointer;
}
.radio-transport button:hover:not(:disabled),
@@ -231,12 +267,15 @@
outline: none;
}
.radio-transport button:disabled,
#radio-filter:disabled { opacity: .42; cursor: not-allowed; }
#radio-filter:disabled {
opacity: 0.42;
cursor: not-allowed;
}
#radio-play-btn.active {
color: #fff;
border-color: rgba(101, 247, 255, .8);
background: rgba(65, 220, 231, .18);
box-shadow: 0 0 12px rgba(65, 220, 231, .12);
border-color: rgba(101, 247, 255, 0.8);
background: rgba(65, 220, 231, 0.18);
box-shadow: 0 0 12px rgba(65, 220, 231, 0.12);
}
.radio-volume-row {
display: grid;
@@ -245,15 +284,19 @@
align-items: center;
color: var(--text-dim);
font: 7px/1 var(--font-mono);
letter-spacing: .1em;
letter-spacing: 0.1em;
}
.radio-volume-row input {
width: 100%;
}
.radio-volume-row input { width: 100%; }
.radio-playback-state {
min-height: 12px;
color: #79d7df;
font: 8px/1.35 var(--font-mono);
}
.radio-playback-state.error { color: #ff9d8f; }
.radio-playback-state.error {
color: #ff9d8f;
}
.radio-links {
display: flex;
flex-wrap: wrap;
@@ -263,13 +306,16 @@
.radio-links a {
color: #64cbd5;
font: 7px/1.3 var(--font-mono);
letter-spacing: .06em;
letter-spacing: 0.06em;
text-decoration: none;
}
.radio-links a:hover, .radio-links a:focus-visible { color: #fff; }
.radio-links a:hover,
.radio-links a:focus-visible {
color: #fff;
}
.radio-privacy {
margin: 0;
color: rgba(190, 230, 230, .48);
color: rgba(190, 230, 230, 0.48);
font: 7px/1.4 var(--font-mono);
}
#military-awareness-panel {
@@ -281,36 +327,129 @@
color: #dceeff;
background: transparent;
border: 0;
font: 11px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace;
font:
11px/1.35 ui-monospace,
SFMono-Regular,
Menlo,
monospace;
}
#military-awareness-panel[hidden] { display: none; }
.military-awareness-subject { margin: 0 0 7px; color: #fff; }
.military-awareness-controls { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 5px; margin: 0 0 8px; }
.military-awareness-controls button { border: 1px solid rgba(98,181,255,0.34); border-radius: 4px; padding: 4px 3px; color: #a9d9ff; background: rgba(42,113,171,0.12); font: 9px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.07em; cursor: pointer; }
.military-awareness-controls button:hover, .military-awareness-controls button:focus-visible { color: #fff; border-color: rgba(127,200,255,0.85); background: rgba(66,148,211,0.24); outline: none; }
.military-awareness-controls button:disabled { color: #627181; border-color: rgba(98,181,255,0.12); background: transparent; cursor: default; }
.military-awareness-row { border-top: 1px solid rgba(255,255,255,0.1); padding: 7px 0; }
.military-awareness-row > div { display: flex; justify-content: space-between; }
.military-awareness-row b { color: #7fc8ff; }
.military-awareness-row.unknown b { color: #8fcfff; }
.military-awareness-row small, .military-awareness-note { color: #a9b7c7; }
.military-awareness-row ul { margin: 4px 0 0; padding: 0; list-style: none; }
.military-awareness-row li { display: block; }
.military-awareness-target { display: flex; justify-content: space-between; gap: 10px; width: 100%; border: 0; padding: 1px 0; color: #dceeff; background: transparent; font: inherit; text-align: left; cursor: pointer; }
.military-awareness-target.unavailable { color: var(--text-dim); cursor: default; }
.military-awareness-target:hover, .military-awareness-target:focus-visible { color: #fff; outline: none; text-shadow: 0 0 8px rgba(114, 202, 255, 0.85); }
.military-awareness-target:focus-visible { box-shadow: inset 0 -1px 0 #7fc8ff; }
.military-awareness-target span { color: #7fc8ff; white-space: nowrap; }
.military-awareness-note { margin: 8px 0 0; font-size: 10px; }
#military-awareness-panel[hidden] {
display: none;
}
.military-awareness-subject {
margin: 0 0 7px;
color: #fff;
}
.military-awareness-controls {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 5px;
margin: 0 0 8px;
}
.military-awareness-controls button {
border: 1px solid rgba(98, 181, 255, 0.34);
border-radius: 4px;
padding: 4px 3px;
color: #a9d9ff;
background: rgba(42, 113, 171, 0.12);
font:
9px/1.2 ui-monospace,
SFMono-Regular,
Menlo,
monospace;
letter-spacing: 0.07em;
cursor: pointer;
}
.military-awareness-controls button:hover,
.military-awareness-controls button:focus-visible {
color: #fff;
border-color: rgba(127, 200, 255, 0.85);
background: rgba(66, 148, 211, 0.24);
outline: none;
}
.military-awareness-controls button:disabled {
color: #627181;
border-color: rgba(98, 181, 255, 0.12);
background: transparent;
cursor: default;
}
.military-awareness-row {
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding: 7px 0;
}
.military-awareness-row > div {
display: flex;
justify-content: space-between;
}
.military-awareness-row b {
color: #7fc8ff;
}
.military-awareness-row.unknown b {
color: #8fcfff;
}
.military-awareness-row small,
.military-awareness-note {
color: #a9b7c7;
}
.military-awareness-row ul {
margin: 4px 0 0;
padding: 0;
list-style: none;
}
.military-awareness-row li {
display: block;
}
.military-awareness-target {
display: flex;
justify-content: space-between;
gap: 10px;
width: 100%;
border: 0;
padding: 1px 0;
color: #dceeff;
background: transparent;
font: inherit;
text-align: left;
cursor: pointer;
}
.military-awareness-target.unavailable {
color: var(--text-dim);
cursor: default;
}
.military-awareness-target:hover,
.military-awareness-target:focus-visible {
color: #fff;
outline: none;
text-shadow: 0 0 8px rgba(114, 202, 255, 0.85);
}
.military-awareness-target:focus-visible {
box-shadow: inset 0 -1px 0 #7fc8ff;
}
.military-awareness-target span {
color: #7fc8ff;
white-space: nowrap;
}
.military-awareness-note {
margin: 8px 0 0;
font-size: 10px;
}
.military-awareness-standby {
display: grid;
gap: 7px;
padding: 12px 4px 4px;
border-top: 1px solid rgba(98, 181, 255, 0.16);
}
.military-awareness-standby strong { color: #79c2ff; font-size: 10px; letter-spacing: 0.1em; }
.military-awareness-standby span { color: #8192a6; font-size: 9px; letter-spacing: 0.05em; }
.military-awareness-standby strong {
color: #79c2ff;
font-size: 10px;
letter-spacing: 0.1em;
}
.military-awareness-standby span {
color: #8192a6;
font-size: 9px;
letter-spacing: 0.05em;
}
.military-awareness-standby button {
justify-self: start;
padding: 4px 12px;
@@ -318,27 +457,51 @@
border-radius: 4px;
color: #a9d9ff;
background: rgba(42, 113, 171, 0.12);
font: 9px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
font:
9px/1.2 ui-monospace,
SFMono-Regular,
Menlo,
monospace;
letter-spacing: 0.08em;
cursor: pointer;
}
#military-awareness-direction-overlay { position: absolute; inset: 0; z-index: 4; overflow: hidden; pointer-events: none; }
#military-awareness-direction-overlay[hidden] { display: none; }
#military-awareness-direction-overlay {
position: absolute;
inset: 0;
z-index: 4;
overflow: hidden;
pointer-events: none;
}
#military-awareness-direction-overlay[hidden] {
display: none;
}
.military-awareness-compass-ring {
--compass-rotation: 0deg;
position: absolute;
box-sizing: border-box;
border: 1px solid rgba(111,196,255,0.2);
outline: 1px dashed rgba(111,196,255,0.28);
border: 1px solid rgba(111, 196, 255, 0.2);
outline: 1px dashed rgba(111, 196, 255, 0.28);
outline-offset: -5px;
border-radius: 50%;
box-shadow:
inset 0 0 0 5px rgba(34,128,196,0.018),
inset 0 0 24px rgba(34,128,196,0.035),
0 0 14px rgba(34,128,196,0.05);
inset 0 0 0 5px rgba(34, 128, 196, 0.018),
inset 0 0 24px rgba(34, 128, 196, 0.035),
0 0 14px rgba(34, 128, 196, 0.05);
}
.military-awareness-compass-ring::before {
content: '';
position: absolute;
left: 50%;
top: -7px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 8px solid rgba(127, 200, 255, 0.88);
transform: translateX(-50%);
filter: drop-shadow(0 0 5px rgba(87, 184, 255, 0.7));
}
.military-awareness-compass-ring::before { content: ''; position: absolute; left: 50%; top: -7px; width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: 8px solid rgba(127,200,255,0.88); transform: translateX(-50%); filter: drop-shadow(0 0 5px rgba(87,184,255,0.7)); }
.military-awareness-compass-ring::after {
content: '';
position: absolute;
@@ -349,22 +512,86 @@
rgba(125, 205, 255, 0.38) 0deg 0.7deg,
transparent 0.7deg 15deg
);
-webkit-mask: radial-gradient(circle, transparent calc(100% - 8px), #000 calc(100% - 8px) calc(100% - 4px), transparent calc(100% - 4px));
mask: radial-gradient(circle, transparent calc(100% - 8px), #000 calc(100% - 8px) calc(100% - 4px), transparent calc(100% - 4px));
-webkit-mask: radial-gradient(
circle,
transparent calc(100% - 8px),
#000 calc(100% - 8px) calc(100% - 4px),
transparent calc(100% - 4px)
);
mask: radial-gradient(
circle,
transparent calc(100% - 8px),
#000 calc(100% - 8px) calc(100% - 4px),
transparent calc(100% - 4px)
);
transform: rotate(var(--compass-rotation));
transition: transform 80ms linear;
}
.military-awareness-compass-label, .military-awareness-compass-heading { position: absolute; transform: translate(-50%, -50%); color: rgba(156,213,255,0.7); font: 9px/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.08em; text-shadow: 0 0 7px rgba(53,161,235,0.55); }
.military-awareness-compass-label.cardinal-n { color: rgba(255,188,87,0.92); }
.military-awareness-compass-heading { color: rgba(190,225,249,0.76); font-size: 8px; }
.military-awareness-direction-marker { position: absolute; width: 1px; height: 1px; transform: translate(-50%, -50%); transition: opacity 100ms linear; }
.military-awareness-direction-marker[hidden] { display: none; }
.military-awareness-direction-arrow { position: absolute; left: 50%; top: 50%; color: #74dbff; font: 23px/1 sans-serif; transform-origin: 50% 50%; text-shadow: 0 0 5px rgba(87,223,255,0.9), 0 0 13px rgba(87,223,255,0.35); }
.military-awareness-direction-label { position: absolute; left: 50%; top: 50%; padding: 3px 6px; border: 1px solid rgba(116,219,255,0.24); border-radius: 3px; color: #bceeff; background: rgba(5,12,20,0.68); font: 8px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; text-align: center; white-space: pre; text-shadow: 0 0 5px rgba(116,219,255,0.38); }
.military-awareness-compass-label,
.military-awareness-compass-heading {
position: absolute;
transform: translate(-50%, -50%);
color: rgba(156, 213, 255, 0.7);
font:
9px/1 ui-monospace,
SFMono-Regular,
Menlo,
monospace;
letter-spacing: 0.08em;
text-shadow: 0 0 7px rgba(53, 161, 235, 0.55);
}
.military-awareness-compass-label.cardinal-n {
color: rgba(255, 188, 87, 0.92);
}
.military-awareness-compass-heading {
color: rgba(190, 225, 249, 0.76);
font-size: 8px;
}
.military-awareness-direction-marker {
position: absolute;
width: 1px;
height: 1px;
transform: translate(-50%, -50%);
transition: opacity 100ms linear;
}
.military-awareness-direction-marker[hidden] {
display: none;
}
.military-awareness-direction-arrow {
position: absolute;
left: 50%;
top: 50%;
color: #74dbff;
font: 23px/1 sans-serif;
transform-origin: 50% 50%;
text-shadow:
0 0 5px rgba(87, 223, 255, 0.9),
0 0 13px rgba(87, 223, 255, 0.35);
}
.military-awareness-direction-label {
position: absolute;
left: 50%;
top: 50%;
padding: 3px 6px;
border: 1px solid rgba(116, 219, 255, 0.24);
border-radius: 3px;
color: #bceeff;
background: rgba(5, 12, 20, 0.68);
font:
8px/1.3 ui-monospace,
SFMono-Regular,
Menlo,
monospace;
text-align: center;
white-space: pre;
text-shadow: 0 0 5px rgba(116, 219, 255, 0.38);
}
.data-toggle-row.gev-voice-focus {
background: rgba(92, 255, 198, 0.1);
box-shadow: inset 0 0 0 1px rgba(92, 255, 198, 0.72), 0 0 18px rgba(92, 255, 198, 0.18);
box-shadow:
inset 0 0 0 1px rgba(92, 255, 198, 0.72),
0 0 18px rgba(92, 255, 198, 0.18);
}
.data-toggle-top {
@@ -573,4 +800,3 @@
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.18);
flex: none;
}
-1
View File
@@ -30,4 +30,3 @@ body.recording-mode #scene-runtime.active {
body.scene-playback-mode :is(#clear-selected-layers, #reset-globe-view) {
display: none !important;
}
-1
View File
@@ -213,4 +213,3 @@
text-align: center;
}
}
+39 -11
View File
@@ -35,7 +35,9 @@
order: 2;
}
#left-panel-stack > #scene-panel { order: 4; }
#left-panel-stack > #scene-panel {
order: 4;
}
/*
* Runtime focus mode: the layout engine applies this only when the expanded
@@ -59,7 +61,8 @@
* When an expanded panel and its collapsed siblings fit together, preserve
* the canonical control order and center their combined stack at 50vh.
*/
#left-panel-stack.layout-tail > :is(#data-panel, #cctv-panel, #scene-panel):not(.collapsed) {
#left-panel-stack.layout-tail
> :is(#data-panel, #cctv-panel, #scene-panel):not(.collapsed) {
box-sizing: border-box;
flex: 0 0 var(--left-panel-allocated-height);
height: var(--left-panel-allocated-height);
@@ -91,19 +94,33 @@
}
#left-panel-stack > [data-panel-id]:not(.collapsed) .panel-divider {
background: linear-gradient(90deg, rgb(0 212 255 / 28%), rgba(0, 212, 255, 0.18) 58%, transparent);
background: linear-gradient(
90deg,
rgb(0 212 255 / 28%),
rgba(0, 212, 255, 0.18) 58%,
transparent
);
box-shadow: 0 0 7px rgba(0, 212, 255, 0.22);
}
#right-context-rail [data-panel-id]:not(.collapsed) .panel-divider {
background: linear-gradient(90deg, rgb(0 212 255 / 28%), rgba(0, 212, 255, 0.18) 58%, transparent);
background: linear-gradient(
90deg,
rgb(0 212 255 / 28%),
rgba(0, 212, 255, 0.18) 58%,
transparent
);
box-shadow: 0 0 7px rgba(0, 212, 255, 0.22);
}
#scene-panel .panel-glow {
position: absolute;
inset: -18px;
background: radial-gradient(ellipse at center, rgba(0, 212, 255, 0.16) 0%, transparent 65%);
background: radial-gradient(
ellipse at center,
rgba(0, 212, 255, 0.16) 0%,
transparent 65%
);
filter: blur(16px);
opacity: 0.25;
pointer-events: none;
@@ -209,7 +226,9 @@
border-radius: 9px;
padding: 7px 8px;
background: rgba(255, 255, 255, 0.03);
transition: border-color var(--transition-fast), background var(--transition-fast);
transition:
border-color var(--transition-fast),
background var(--transition-fast);
}
.scene-shot-row.active {
@@ -300,7 +319,11 @@
#scene-progress-fill {
width: 0%;
height: 100%;
background: linear-gradient(90deg, rgba(0, 212, 255, 0.2), rgba(0, 212, 255, 0.75));
background: linear-gradient(
90deg,
rgba(0, 212, 255, 0.2),
rgba(0, 212, 255, 0.75)
);
color: #d5f7ff;
font-family: var(--font-mono);
font-size: 8px;
@@ -329,7 +352,9 @@
z-index: 105;
opacity: 0;
visibility: hidden;
transition: opacity 180ms ease, visibility 180ms ease;
transition:
opacity 180ms ease,
visibility 180ms ease;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 2px;
@@ -354,7 +379,9 @@
#global-context-panel.collapsed .global-context-actions,
#global-context-panel.collapsed .global-context-modes,
#global-context-panel.collapsed .context-mode-view,
#global-context-panel.collapsed > .global-context-panel-inner > .context-mode-standby,
#global-context-panel.collapsed
> .global-context-panel-inner
> .context-mode-standby,
#radio-panel.collapsed .radio-panel-inner > :not(.panel-header),
#cctv-panel.collapsed .cctv-controls,
#cctv-panel.collapsed .cctv-calibration-block,
@@ -380,7 +407,9 @@
width: var(--left-collapsed-width);
}
#global-context-panel #radio-panel.collapsed { width: 100%; }
#global-context-panel #radio-panel.collapsed {
width: 100%;
}
#data-panel.collapsed .panel-title,
#cctv-panel.collapsed .panel-title,
@@ -409,4 +438,3 @@
#param-slider-panel.collapsed .param-panel-header {
margin-bottom: 2px;
}
+79 -23
View File
@@ -74,10 +74,14 @@
letter-spacing: 1.35px;
text-transform: uppercase;
transform: translateX(-50%);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.34), 0 0 14px rgba(0, 212, 255, 0.08);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.34),
0 0 14px rgba(0, 212, 255, 0.08);
pointer-events: none;
}
#global-loading-status[hidden] { display: none; }
#global-loading-status[hidden] {
display: none;
}
#global-loading-label {
color: rgba(184, 244, 255, 0.92);
}
@@ -92,25 +96,49 @@
text-overflow: ellipsis;
white-space: nowrap;
}
#global-loading-status:is([data-state='complete'], [data-state='cancelled'], [data-state='error']) {
#global-loading-status:is(
[data-state='complete'],
[data-state='cancelled'],
[data-state='error']
) {
justify-content: center;
text-align: center;
}
#global-loading-status:is([data-state='complete'], [data-state='cancelled'], [data-state='error']) #global-loading-detail {
#global-loading-status:is(
[data-state='complete'],
[data-state='cancelled'],
[data-state='error']
)
#global-loading-detail {
display: none;
}
#global-loading-status[data-state='error'] { border-color: rgba(255, 98, 82, 0.55); color: #ff9b8e; }
#global-loading-status[data-state='error'] {
border-color: rgba(255, 98, 82, 0.55);
color: #ff9b8e;
}
#global-loading-status[data-state='error'] #global-loading-label,
#global-loading-status[data-state='error'] #global-loading-detail { color: #ff9b8e; }
#global-loading-status[data-state='cancelled'] { color: #ffd488; }
#global-loading-status[data-state='retry'] { border-color: rgba(255, 212, 136, 0.55); }
#global-loading-status[data-state='error'] #global-loading-detail {
color: #ff9b8e;
}
#global-loading-status[data-state='cancelled'] {
color: #ffd488;
}
#global-loading-status[data-state='retry'] {
border-color: rgba(255, 212, 136, 0.55);
}
#global-loading-status[data-state='retry'] #global-loading-label,
#global-loading-status[data-state='retry'] #global-loading-detail { color: #ffd488; }
#global-loading-status[data-state='retry'] #global-loading-detail {
color: #ffd488;
}
#global-loading-status[data-state='cancelled'] #global-loading-label,
#global-loading-status[data-state='cancelled'] #global-loading-detail { color: #ffd488; }
#global-loading-status[data-state='cancelled'] #global-loading-detail {
color: #ffd488;
}
#global-loading-status[data-state='acquiring'] {
border-color: rgba(0, 212, 255, 0.42);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.34), 0 0 18px rgba(0, 212, 255, 0.12);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.34),
0 0 18px rgba(0, 212, 255, 0.12);
}
.share-icon {
@@ -141,8 +169,12 @@
text-transform: uppercase;
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.34), 0 0 14px rgba(0, 212, 255, 0.08);
transition:
opacity 0.22s ease,
transform 0.22s ease;
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.34),
0 0 14px rgba(0, 212, 255, 0.08);
}
/* Stacked below the traffic chip so both can show at once */
@@ -270,21 +302,43 @@
them. */
.gev-flap-sizing {
display: inline-block;
transition: width var(--gev-flap-total, 500ms) cubic-bezier(0.2, 0.75, 0.25, 1);
transition: width var(--gev-flap-total, 500ms)
cubic-bezier(0.2, 0.75, 0.25, 1);
}
/* The old leaf hinges down and away on the cell's midline. */
@keyframes gev-flap-out {
0% { transform: rotateX(0deg); opacity: 1; }
48% { transform: rotateX(-88deg); opacity: 0.5; }
100% { transform: rotateX(-90deg); opacity: 0; }
0% {
transform: rotateX(0deg);
opacity: 1;
}
48% {
transform: rotateX(-88deg);
opacity: 0.5;
}
100% {
transform: rotateX(-90deg);
opacity: 0;
}
}
/* The new leaf waits for the old one to clear, then drops in and settles
with a single small overshoot — the mechanical clack. */
@keyframes gev-flap-in {
0% { transform: rotateX(90deg); opacity: 0; }
46% { transform: rotateX(90deg); opacity: 0; }
78% { transform: rotateX(-7deg); opacity: 1; }
100% { transform: rotateX(0deg); opacity: 1; }
0% {
transform: rotateX(90deg);
opacity: 0;
}
46% {
transform: rotateX(90deg);
opacity: 0;
}
78% {
transform: rotateX(-7deg);
opacity: 1;
}
100% {
transform: rotateX(0deg);
opacity: 1;
}
}
/* Matches the .cockpit-roll-track treatment: collapse the duration rather
than dropping the animation, so the final frame still lands the glyph.
@@ -322,7 +376,10 @@
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s ease;
transition:
opacity 0.3s ease,
visibility 0.3s ease,
transform 0.3s ease;
box-shadow: 0 0 20px rgba(0, 212, 255, 0.15);
}
@@ -331,4 +388,3 @@
visibility: visible;
transform: translateX(-50%) translateY(0);
}
+3 -2
View File
@@ -28,7 +28,9 @@
letter-spacing: 0.045rem;
line-height: 1;
cursor: pointer;
transition: color var(--transition-fast), border-color var(--transition-fast),
transition:
color var(--transition-fast),
border-color var(--transition-fast),
background var(--transition-fast);
}
@@ -75,4 +77,3 @@
#command-dock.tray-open .gev-voice-cost {
display: none;
}