feat(theming): in-UI theme editor (2.10.0)
Recolour the whole UI from Settings — 12 palette colour pickers with live preview, presets (Ember/Frost/Verdant/Amethyst), and reset to the default Blackflame. Overrides persist in app_settings (key 'theme') via a hex-validated /api/theme route and apply to :root on boot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,57 @@
|
||||
import { el, mount } from '../dom.js';
|
||||
import { api } from '../api.js';
|
||||
import { iconSetsPanel } from './icon_sets_panel.js';
|
||||
import { THEME_VARS, PRESETS, applyTheme, clearTheme, saveTheme, currentTheme, effectiveHex, toHex6 } from '../theme.js';
|
||||
|
||||
// Theming — colour pickers for the palette, live-preview on input, presets +
|
||||
// reset. Persists to /api/theme (app_settings); applied app-wide on next boot.
|
||||
function themingBody() {
|
||||
const cur = currentTheme(); // saved overrides (subset of vars)
|
||||
const grid = el('div', { class: 'theme-grid' });
|
||||
const out = el('span', { class: 'muted', style: { marginLeft: '8px' } });
|
||||
|
||||
function rebuild() {
|
||||
mount(grid, THEME_VARS.map(v => {
|
||||
const inp = el('input', { type: 'color', value: cur[v.key] ? toHex6(cur[v.key]) : effectiveHex(v.key) });
|
||||
inp.addEventListener('input', () => {
|
||||
cur[v.key] = inp.value;
|
||||
document.documentElement.style.setProperty(v.css, inp.value); // live preview
|
||||
});
|
||||
return el('label', { class: 'theme-row' }, el('span', {}, v.label), inp);
|
||||
}));
|
||||
}
|
||||
rebuild();
|
||||
|
||||
const preset = el('select', { class: 'pm-input', style: { maxWidth: '160px' } },
|
||||
el('option', { value: '' }, 'Apply preset…'),
|
||||
...Object.keys(PRESETS).map(n => el('option', { value: n }, n)));
|
||||
preset.addEventListener('change', () => {
|
||||
if (!preset.value) return;
|
||||
clearTheme();
|
||||
for (const k of Object.keys(cur)) delete cur[k];
|
||||
Object.assign(cur, PRESETS[preset.value]);
|
||||
applyTheme(cur);
|
||||
rebuild();
|
||||
preset.value = '';
|
||||
});
|
||||
|
||||
const save = el('button', { class: 'primary' }, 'Save theme');
|
||||
save.onclick = async () => {
|
||||
try { await saveTheme(cur); out.textContent = 'Saved — applies everywhere.'; }
|
||||
catch { out.textContent = 'Save failed'; }
|
||||
};
|
||||
const reset = el('button', { class: 'ghost' }, 'Reset to Blackflame');
|
||||
reset.onclick = async () => {
|
||||
for (const k of Object.keys(cur)) delete cur[k];
|
||||
clearTheme();
|
||||
try { await saveTheme({}); rebuild(); out.textContent = 'Reset to default.'; }
|
||||
catch { out.textContent = 'Reset failed'; }
|
||||
};
|
||||
|
||||
return el('div', { class: 'settings-body' },
|
||||
grid,
|
||||
el('div', { class: 'theme-actions' }, preset, save, reset, out));
|
||||
}
|
||||
|
||||
function section(title, sub, bodyEl) {
|
||||
return el('div', { class: 'card settings-card' },
|
||||
@@ -120,6 +171,7 @@ export async function render(main) {
|
||||
|
||||
mount(main,
|
||||
el('h1', { class: 'view-h1' }, '◆ Settings'),
|
||||
section('Theming', 'Recolour the interface. Pick a colour to preview it live, choose a preset, then Save to persist. Reset returns to the default Blackflame palette.', themingBody()),
|
||||
section('API Tokens', 'Bearer tokens for agents (e.g. the MCP external-research agent). The secret is shown once at creation.', tokensBody),
|
||||
section('Agents', 'Seeded Cradle agents and what each is allowed to do.', agentsBody),
|
||||
section('Icon Sets', 'Upload or delete custom icon packs for device and service icons.', iconSetsWrap),
|
||||
|
||||
Reference in New Issue
Block a user