diff options
Diffstat (limited to 'internal/admin/static')
| -rw-r--r-- | internal/admin/static/admin.css | 2 | ||||
| -rw-r--r-- | internal/admin/static/coloreditor.js | 173 |
2 files changed, 123 insertions, 52 deletions
diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index 9f62897..0d2e8c6 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -504,6 +504,8 @@ input:focus-visible,select:focus-visible,textarea:focus-visible,button:focus-vis .color-list .search-bar{flex-shrink:0} #colorRows{flex:1;overflow-y:auto;padding-right:4px} .color-group{font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:#777;margin:10px 0 4px 0;padding-bottom:2px;border-bottom:1px solid var(--border)} +.color-section-header{font-size:13px;font-weight:bold;letter-spacing:.5px;color:var(--cond-light);margin:14px 0 6px 0;padding:4px 6px;border-top:1px solid var(--border);border-bottom:1px solid var(--border);background:var(--cond-bg);border-radius:3px} +.color-section-header:first-child{margin-top:4px;border-top:none} .color-row{display:flex;align-items:center;gap:6px;padding:4px 4px;border-radius:3px;margin-bottom:1px} .color-row:hover{background:rgba(15,52,96,.3)} .color-row.dirty{background:rgba(158,99,7,.12)} diff --git a/internal/admin/static/coloreditor.js b/internal/admin/static/coloreditor.js index fe88945..7ae305e 100644 --- a/internal/admin/static/coloreditor.js +++ b/internal/admin/static/coloreditor.js @@ -6,13 +6,23 @@ 'use strict'; var STATE = { - categories: [], // [{name,label,group}] - categoriesById: {}, // name -> cat - saved: {}, // name -> spec string (last persisted) - staged: {}, // name -> spec string (unsaved edits) - defaults: {}, // name -> spec string (builtins, for reset) - preview: [], // [{title, lines:[{segments:[{t,c}]}]}] - dirty: {} // name -> true + categories: [], // [{name,label,group}] — player-overridable + categoriesById: {}, // name -> cat (player) + saved: {}, // name -> spec (last persisted) — player + staged: {}, // name -> spec (unsaved) — player + defaults: {}, // name -> spec (builtins) — player + dirty: {}, // name -> true — player + + // Constant (admin-only) colors. Kept in parallel maps so the page can + // render two top-level sections but share one save/reset pipeline. + constantCategories: [], + constantCategoriesById: {}, + constantSaved: {}, + constantStaged: {}, + constantDefaults: {}, + constantDirty: {}, + + preview: [], }; function $i(id) { return document.getElementById(id); } @@ -73,10 +83,39 @@ return parts.join(' '); } + // Returns 'constant' if the category name is a constant (admin-only) + // color, otherwise 'player'. + function sectionOf(name) { + if (STATE.constantCategoriesById[name]) return 'constant'; + return 'player'; + } + + // Current effective spec for a category (staged overrides saved), works for + // both player and constant sections. + function effSaved(name) { return sectionOf(name) === 'constant' ? (STATE.constantSaved[name] || STATE.constantDefaults[name] || '') : (STATE.saved[name] || STATE.defaults[name] || ''); } + function effStaged(name) { return sectionOf(name) === 'constant' ? STATE.constantStaged[name] : STATE.staged[name]; } + function effDirty(name) { return sectionOf(name) === 'constant' ? STATE.constantDirty[name] : STATE.dirty[name]; } + function setStaged(name, val) { + if (sectionOf(name) === 'constant') STATE.constantStaged[name] = val; + else STATE.staged[name] = val; + } + function setDirty(name, val) { + if (sectionOf(name) === 'constant') STATE.constantDirty[name] = val; + else STATE.dirty[name] = val; + } + function clearDirty(name) { + if (sectionOf(name) === 'constant') delete STATE.constantDirty[name]; + else delete STATE.dirty[name]; + } + function clearStaged(name) { + if (sectionOf(name) === 'constant') delete STATE.constantStaged[name]; + else delete STATE.staged[name]; + } + // Current effective spec for a category (staged overrides saved). function effectiveSpec(name) { - if (STATE.dirty[name]) return STATE.staged[name]; - return STATE.saved[name] || STATE.defaults[name] || ''; + if (effDirty(name)) return effStaged(name); + return effSaved(name); } // ── 16-color ANSI projection (mirrors nearestANSI in color.go) ────────── @@ -114,16 +153,12 @@ function buildRows() { var host = $i('colorRows'); host.innerHTML = ''; - var byGroup = {}; - var order = []; - STATE.categories.forEach(function (c) { - if (!byGroup[c.group]) { byGroup[c.group] = []; order.push(c.group); } - byGroup[c.group].push(c); - }); - order.forEach(function (g) { - host.appendChild(el('div', 'color-group', g)); - byGroup[g].forEach(function (c) { host.appendChild(buildRow(c)); }); - }); + // Section header, then flat ordered rows. No per-group dividers: the + // order mirrors the in-game `colors` command exactly. + host.appendChild(el('div', 'color-section-header', 'Player Colors')); + STATE.categories.forEach(function (c) { host.appendChild(buildRow(c)); }); + host.appendChild(el('div', 'color-section-header', 'Constants')); + STATE.constantCategories.forEach(function (c) { host.appendChild(buildRow(c)); }); } function rowSwatchColor(spec) { @@ -220,8 +255,8 @@ } } var specStr = serializeSpec(s); - STATE.staged[cat.name] = specStr; - STATE.dirty[cat.name] = (specStr !== (STATE.saved[cat.name] || '')); + setStaged(cat.name, specStr); + setDirty(cat.name, (specStr !== effSaved(cat.name))); refreshRow(row); refreshRevertButton(row, cat); renderColorPreview(); @@ -238,7 +273,7 @@ } function refreshRow(row) { - row.classList.toggle('dirty', !!STATE.dirty[row.dataset.name]); + row.classList.toggle('dirty', !!effDirty(row.dataset.name)); var spec = parseSpec(effectiveSpec(row.dataset.name)); var sw = row.querySelector('.color-swatch'); var sc = rowSwatchColor(spec); @@ -248,13 +283,13 @@ function refreshRevertButton(row, cat) { var btn = row.querySelector('.color-revert'); - btn.disabled = !STATE.dirty[cat.name]; + btn.disabled = !effDirty(cat.name); } function revertRow(row, cat) { - delete STATE.staged[cat.name]; - delete STATE.dirty[cat.name]; - var spec = parseSpec(STATE.saved[cat.name] || STATE.defaults[cat.name] || ''); + clearStaged(cat.name); + clearDirty(cat.name); + var spec = parseSpec(effSaved(cat.name)); row.dataset.fg = spec.fg >= 0 ? spec.fg : '-1'; row.querySelectorAll('.color-flags input').forEach(function (cb) { var f = cb.dataset.flag; cb.checked = !!spec[f]; @@ -468,18 +503,23 @@ function colorSave() { var status = $i('colorSaveStatus'); status.textContent = 'Saving...'; status.className = 'color-save-status'; - var payload = {}; + var playerPayload = {}, constPayload = {}; Object.keys(STATE.dirty).forEach(function (k) { - if (STATE.dirty[k]) payload[k] = effectiveSpec(k); + if (STATE.dirty[k]) playerPayload[k] = effectiveSpec(k); + }); + Object.keys(STATE.constantDirty).forEach(function (k) { + if (STATE.constantDirty[k]) constPayload[k] = effectiveSpec(k); }); fetch('/api/colors', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ colors: payload }) + body: JSON.stringify({ colors: playerPayload, constant_colors: constPayload }) }).then(function (r) { return r.json(); }).then(function (j) { if (j.ok) { // committed values become the new saved baseline Object.keys(STATE.dirty).forEach(function (k) { if (STATE.dirty[k]) STATE.saved[k] = effectiveSpec(k); }); + Object.keys(STATE.constantDirty).forEach(function (k) { if (STATE.constantDirty[k]) STATE.constantSaved[k] = effectiveSpec(k); }); STATE.dirty = {}; + STATE.constantDirty = {}; document.querySelectorAll('.color-row.dirty').forEach(function (row) { row.classList.remove('dirty'); var btn = row.querySelector('.color-revert'); if (btn) btn.disabled = true; @@ -494,7 +534,7 @@ } function colorResetAll() { - if (!confirm('Reset ALL colors to the built-in defaults? This saves immediately.')) return; + if (!confirm('Reset ALL colors (player + constants) to the built-in defaults? This saves immediately.')) return; fetch('/api/colors/reset', { method: 'POST' }).then(function (r) { return r.json(); }).then(function (j) { if (!j.ok) { alert('Reset failed: ' + (j.error || 'unknown')); return; } Object.keys(j.colors || {}).forEach(function (k) { @@ -502,14 +542,20 @@ STATE.staged[k] = j.colors[k]; delete STATE.dirty[k]; }); + Object.keys(j.constant_colors || {}).forEach(function (k) { + STATE.constantSaved[k] = j.constant_colors[k]; + STATE.constantStaged[k] = j.constant_colors[k]; + delete STATE.constantDirty[k]; + }); buildRows(); renderColorPreview(); }).catch(function (e) { alert('Reset failed: ' + e); }); } function colorExport() { - var out = {}; - STATE.categories.forEach(function (c) { out[c.name] = effectiveSpec(c.name); }); + var out = { colors: {}, constant_colors: {} }; + STATE.categories.forEach(function (c) { out.colors[c.name] = effectiveSpec(c.name); }); + STATE.constantCategories.forEach(function (c) { out.constant_colors[c.name] = effectiveSpec(c.name); }); var json = JSON.stringify(out, null, 2); var ta = el('textarea'); ta.value = json; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); @@ -521,18 +567,28 @@ } function colorImportPrompt() { - var txt = prompt('Paste exported theme JSON (a {category: spec} object):'); + var txt = prompt('Paste exported theme JSON ({colors:{...},constant_colors:{...}}):'); if (!txt) return; var obj; try { obj = JSON.parse(txt); } catch (e) { alert('Invalid JSON: ' + e); return; } + // Accept either the layered {colors, constant_colors} shape or a flat + // {category: spec} legacy object. + var layerColors = obj.colors || obj; + var layerConst = obj.constant_colors || {}; var applied = 0; - Object.keys(obj).forEach(function (k) { - if (!STATE.categoriesById[k]) return; - var spec = parseSpec(obj[k]); - STATE.staged[k] = serializeSpec(spec); - STATE.dirty[k] = (STATE.staged[k] !== (STATE.saved[k] || '')); - applied++; - }); + function applyInto(map, idMap, stagedMap, dirtyMap) { + Object.keys(map).forEach(function (k) { + var cat = idMap[k]; + if (!cat) return; + var spec = parseSpec(map[k]); + stagedMap[k] = serializeSpec(spec); + var saved = sectionOf(k) === 'constant' ? STATE.constantSaved[k] : STATE.saved[k]; + dirtyMap[k] = (stagedMap[k] !== (saved || '')); + applied++; + }); + } + applyInto(layerColors, STATE.categoriesById, STATE.staged, STATE.dirty); + applyInto(layerConst, STATE.constantCategoriesById, STATE.constantStaged, STATE.constantDirty); if (applied === 0) { alert('No known categories found in the JSON.'); return; } buildRows(); renderColorPreview(); @@ -543,19 +599,23 @@ function filterColorRows(q) { q = (q || '').toLowerCase(); - document.querySelectorAll('#colorRows .color-row').forEach(function (row) { - var name = row.dataset.name; - var label = (STATE.categoriesById[name] || {}).label || ''; - row.style.display = (!q || name.indexOf(q) >= 0 || label.toLowerCase().indexOf(q) >= 0) ? '' : 'none'; - }); - document.querySelectorAll('#colorRows .color-group').forEach(function (g) { - // hide group header if all its rows are hidden — naive: check sibling visibility - var any = false, n = g.nextElementSibling; - while (n && !n.classList.contains('color-group')) { - if (n.classList.contains('color-row') && n.style.display !== 'none') { any = true; break; } + // Group rows under their preceding section header so the header can be + // hidden when its section has zero visible rows under a query. + var headers = document.querySelectorAll('#colorRows .color-section-header'); + headers.forEach(function (h) { + var any = false, n = h.nextElementSibling; + while (n && !n.classList.contains('color-section-header')) { + if (n.classList.contains('color-row')) { + var name = n.dataset.name; + var idMap = sectionOf(name) === 'constant' ? STATE.constantCategoriesById : STATE.categoriesById; + var label = (idMap[name] || {}).label || ''; + var match = (!q || name.indexOf(q) >= 0 || label.toLowerCase().indexOf(q) >= 0); + n.style.display = match ? '' : 'none'; + if (match) any = true; + } n = n.nextElementSibling; } - g.style.display = any || !q ? '' : 'none'; + h.style.display = (!q || any) ? '' : 'none'; }); } @@ -567,9 +627,18 @@ STATE.categories.forEach(function (c) { STATE.categoriesById[c.name] = c; }); STATE.saved = data.colors || {}; STATE.defaults = data.default_colors || {}; - STATE.preview = data.preview || []; STATE.staged = {}; STATE.dirty = {}; Object.keys(STATE.saved).forEach(function (k) { STATE.staged[k] = STATE.saved[k]; }); + + STATE.constantCategories = data.constant_categories || []; + STATE.constantCategoriesById = {}; + STATE.constantCategories.forEach(function (c) { STATE.constantCategoriesById[c.name] = c; }); + STATE.constantSaved = data.constant_colors || {}; + STATE.constantDefaults = data.default_constant_colors || {}; + STATE.constantStaged = {}; STATE.constantDirty = {}; + Object.keys(STATE.constantSaved).forEach(function (k) { STATE.constantStaged[k] = STATE.constantSaved[k]; }); + + STATE.preview = data.preview || []; buildRows(); renderColorPreview(); }).catch(function (e) { |
