wave3: categorized collapsible HUD (Space/Air/Sea/Earth/Context/Regional)

12+ layers in a flat list was unusable. ui.js now groups rows into ordered,
collapsible category sections (scheme owned in ui.js so order is stable
regardless of registration order); Regional — Australia collapsed by default;
empty categories auto-hidden via :has(:empty). URL-state get/set layer APIs
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jing 2026-07-14 00:29:49 +10:00
parent 941f5032e0
commit 3dde813d7d
2 changed files with 78 additions and 4 deletions

View File

@ -115,9 +115,32 @@ html, body {
#layers {
display: flex;
flex-direction: column;
gap: 2px;
gap: 1px;
overflow-y: auto;
}
/* ---- collapsible category sections ---- */
.cat:has(.cat-body:empty) { display: none; } /* hide unused categories */
.cat-header {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 4px 3px;
margin-top: 4px;
cursor: pointer;
user-select: none;
color: var(--dim);
font-size: 9.5px;
letter-spacing: 0.6px;
text-transform: uppercase;
border-top: 1px solid var(--border);
}
.cat-header:hover { color: var(--text); }
.cat-chevron { font-size: 8px; width: 8px; transition: transform 0.15s; }
.cat.collapsed .cat-chevron { transform: rotate(-90deg); }
.cat.collapsed .cat-body { display: none; }
.cat-body { display: flex; flex-direction: column; gap: 1px; }
.layer-row {
padding: 5px 4px;
border-radius: 6px;

View File

@ -1,9 +1,60 @@
// HUD controller: layer registry, per-feed status lines, LIVE/SCRUBBED chip.
// HUD controller: categorized collapsible layer list, per-feed status lines,
// LIVE/SCRUBBED chip.
const rows = new Map();
// Category scheme is owned here so HUD order is stable regardless of layer
// registration order. Categories collapsed-by-default are marked below.
const CATEGORY_ORDER = ['Space', 'Air', 'Sea', 'Earth & hazards', 'Context', 'Regional — Australia'];
const COLLAPSED_BY_DEFAULT = new Set(['Regional — Australia']);
const CATEGORY_BY_ID = {
satellites: 'Space', 'sat-paths': 'Space', launches: 'Space',
aircraft: 'Air', military: 'Air', 'aircraft-mil': 'Air',
ships: 'Sea',
quakes: 'Earth & hazards', fires: 'Earth & hazards', gdacs: 'Earth & hazards', nws: 'Earth & hazards',
infra: 'Context', events: 'Context',
rfs: 'Regional — Australia',
};
const sections = new Map(); // category → { body }
// Lazily create a category section, inserted in CATEGORY_ORDER position.
function sectionFor(category) {
if (sections.has(category)) return sections.get(category);
const wrap = document.createElement('div');
wrap.className = 'cat';
if (COLLAPSED_BY_DEFAULT.has(category)) wrap.classList.add('collapsed');
const header = document.createElement('div');
header.className = 'cat-header';
header.innerHTML = `<span class="cat-chevron">▾</span><span class="cat-name"></span>`;
header.querySelector('.cat-name').textContent = category;
header.addEventListener('click', () => wrap.classList.toggle('collapsed'));
const body = document.createElement('div');
body.className = 'cat-body';
wrap.appendChild(header);
wrap.appendChild(body);
// Insert before the existing section that is this one's immediate successor in
// CATEGORY_ORDER (smallest index greater than ours), so DOM order stays stable
// regardless of registration order. Unknown categories sort last.
const container = document.getElementById('layers');
const ord = (c) => { const i = CATEGORY_ORDER.indexOf(c); return i < 0 ? Infinity : i; };
const myOrder = ord(category);
let before = null, bestOrder = Infinity;
for (const [cat, s] of sections) {
const idx = ord(cat);
if (idx > myOrder && idx < bestOrder) { bestOrder = idx; before = s.wrap; }
}
container.insertBefore(wrap, before);
const rec = { wrap, body };
sections.set(category, rec);
return rec;
}
// Register a layer row. Returns a handle so callers can flip the checkbox.
export function addLayer(id, name, defaultOn, onToggle) {
// `category` is optional; a known id maps to a fixed category regardless.
export function addLayer(id, name, defaultOn, onToggle, category) {
const cat = CATEGORY_BY_ID[id] || category || 'Other';
const row = document.createElement('div');
row.className = 'layer-row';
row.innerHTML = `
@ -16,7 +67,7 @@ export function addLayer(id, name, defaultOn, onToggle) {
row.querySelector('.lname').textContent = name;
const cb = row.querySelector('input');
cb.addEventListener('change', () => onToggle(cb.checked));
document.getElementById('layers').appendChild(row);
sectionFor(cat).body.appendChild(row);
rows.set(id, row);
return { setChecked: (v) => { cb.checked = v; } };
}