aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/static
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-18 04:44:50 -0400
committerhistoria <[not public]>2026-07-18 04:44:50 -0400
commit2b5d8f595f96a2864899f4c7e28bc06fa52d2603 (patch)
treef1539db865d9968414e59d9b250861c1b356d627 /internal/admin/static
parentdb8ca1fe26a36b78074a424ae657b2cbb9b3af19 (diff)
downloadthehouseoficarus-2b5d8f595f96a2864899f4c7e28bc06fa52d2603.tar.gz
feat: expose all colors in config.yaml for easier theming
Diffstat (limited to 'internal/admin/static')
-rw-r--r--internal/admin/static/admin.css2
-rw-r--r--internal/admin/static/coloreditor.js173
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) {