From 3dde813d7d414389febefe735059ea36256543d1 Mon Sep 17 00:00:00 2001 From: jing Date: Tue, 14 Jul 2026 00:29:49 +1000 Subject: [PATCH] wave3: categorized collapsible HUD (Space/Air/Sea/Earth/Context/Regional) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- css/style.css | 25 +++++++++++++++++++++- js/ui.js | 57 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/css/style.css b/css/style.css index 0809380..b0cc4f4 100644 --- a/css/style.css +++ b/css/style.css @@ -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; diff --git a/js/ui.js b/js/ui.js index 731fabc..0bf14f8 100644 --- a/js/ui.js +++ b/js/ui.js @@ -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 = ``; + 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; } }; }