feat(hud): Cyber HUD theme with GPU contact sonar (#706)

Add the opt-in Cyber HUD layout with coordinated map and cockpit panel
styling, GPU-only contact sonar (rings, range, power, opacity, sector) with
native-rendering fallback, and set_cyber_sonar voice controls. Sonar visual
style inspired by kk376's tactical naval sonar (#214).

Co-authored-by: Sameh Khamis <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-09-23 20:31:56 -07:00
1 parent f913066cf2
commit 513dc87a0e
74 files changed
+7320 -90

No files matched your search

+27 -2
View File
@@ -7,6 +7,8 @@ import {
shouldHideCollapsedRightPanels,
} from './panelRailGeometry.js';
import { displayPanelScroller } from './displayPanelScroll.js';
const pendingCollapseRetries = new WeakSet();
/**
@@ -56,6 +58,28 @@ export function layoutRightPanelRail({
onCollapse(panel);
}
}
// Normalize restored state and theme entry without overwriting saved panel
// preferences. This marker is distinct from Tactical's space-based collapse.
if (hud.variant === 'cyber') {
const expanded = panels.filter(
(panel) => !panel.classList.contains('collapsed'),
);
const owner =
expanded.find((panel) => panel.id === preferredPanelId) ||
expanded.find((panel) => panel.contains(documentRef?.activeElement)) ||
expanded[0];
for (const panel of expanded) {
if (panel === owner) continue;
panel.classList.add('collapsed', 'cyber-accordion-collapsed');
onCollapse(panel);
}
} else {
for (const panel of panels) {
if (!panel.classList.contains('cyber-accordion-collapsed')) continue;
panel.classList.remove('collapsed', 'cyber-accordion-collapsed');
onCollapse(panel);
}
}
const isMobile = windowRef.matchMedia('(max-width: 720px)').matches;
const hasExpandedPanel = panels.some(
(panel) =>
@@ -276,10 +300,11 @@ export function layoutRightPanelRail({
stack.dataset.expandedCount = String(expandedPanels.length);
if (displayPanel && expandedPanels.includes(displayPanel)) {
const scroller = displayPanelScroller(displayPanel);
const maxScrollTop = Math.max(
0,
displayPanel.scrollHeight - displayPanel.clientHeight,
scroller.scrollHeight - scroller.clientHeight,
);
displayPanel.scrollTop = Math.min(displayScrollTop, maxScrollTop);
scroller.scrollTop = Math.min(displayScrollTop, maxScrollTop);
}
}