solargod/js/ui.js
monsterrobotparty 26a5751394 🌞 genesis: scaffold — serve.py proxy+cache, importmap renderer, sky, sun, HUD, time bar, floating-origin bootstrap (opus)
Stage 0. Zero-dependency serve.py (static + allowlisted horizons/sbdb/cad proxy,
sha256 disk cache with past-span immutability). Three.js via pinned importmap,
logarithmic depth, procedural starfield sky (+ Milky Way panorama swap-in), Sun
with additive glow, OrbitControls, brass-and-void HUD, scrubbable time bar,
central bodyWorld position pass, focus/camera system, hash state. Boots clean,
no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 00:04:41 +10:00

102 lines
3.3 KiB
JavaScript

// SOLARGOD HUD controller — layer rows with per-layer status lines, LIVE/SCRUBBED
// chip, breadcrumb, date readout. Ports GODSIGH's ui.js contract so layer modules
// use the identical addLayer/setStatus/setLiveChip API.
const rows = new Map();
// Register a layer row. Returns a handle so callers can flip the checkbox.
export function addLayer(id, name, defaultOn, onToggle) {
const row = document.createElement('div');
row.className = 'layer-row';
row.innerHTML = `
<label>
<input type="checkbox" ${defaultOn ? 'checked' : ''} />
<span class="dot" data-state="${defaultOn ? 'ok' : 'off'}"></span>
<span class="lname"></span>
</label>
<div class="lstatus"></div>`;
row.querySelector('.lname').textContent = name;
const cb = row.querySelector('input');
cb.addEventListener('change', () => onToggle(cb.checked));
document.getElementById('layers').appendChild(row);
rows.set(id, row);
return { setChecked: (v) => { cb.checked = v; } };
}
export function getLayerIds() { return [...rows.keys()]; }
export function getLayerChecked(id) {
const row = rows.get(id);
return row ? row.querySelector('input').checked : false;
}
// Set checkbox AND fire onToggle (replays a real user toggle) — used to restore
// hash state. No-op if unchanged.
export function setLayerChecked(id, on) {
const row = rows.get(id);
if (!row) return;
const cb = row.querySelector('input');
if (cb.checked !== on) {
cb.checked = on;
cb.dispatchEvent(new Event('change'));
}
}
// state ∈ 'ok' | 'warn' | 'err' | 'off'
export function setStatus(id, text, state = 'ok') {
const row = rows.get(id);
if (!row) return;
row.querySelector('.lstatus').textContent = text;
row.querySelector('.dot').dataset.state = state;
}
export function setLiveChip(isLive) {
const chip = document.getElementById('live-chip');
if (!chip) return;
chip.textContent = isLive ? 'LIVE' : 'SCRUBBED';
chip.title = isLive ? 'Clock is at wall-clock now' : 'Clock scrubbed off wall-clock now';
chip.dataset.state = isLive ? 'live' : 'scrub';
}
// Breadcrumb: array of {id, name} from Sun down to the focused body.
export function setBreadcrumb(trail, onClick) {
const el = document.getElementById('breadcrumb');
if (!el) return;
el.innerHTML = '';
trail.forEach((node, i) => {
if (i > 0) {
const sep = document.createElement('span');
sep.className = 'crumb-sep';
sep.textContent = '▸';
el.appendChild(sep);
}
const c = document.createElement('span');
c.className = 'crumb' + (i === trail.length - 1 ? ' current' : '');
c.textContent = node.name.toUpperCase();
if (i !== trail.length - 1 && onClick) {
c.style.cursor = 'pointer';
c.addEventListener('click', () => onClick(node.id));
}
el.appendChild(c);
});
}
export function setDateReadout(text, isLive) {
const el = document.getElementById('date-readout');
if (el) {
el.textContent = text;
el.classList.toggle('scrubbed', !isLive);
}
}
// Small transient toast (bottom-center) for one-shot notices.
let toastTimer = null;
export function toast(text, ms = 2200) {
let el = document.getElementById('toast');
if (!el) return;
el.textContent = text;
el.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove('show'), ms);
}