Add theme switchers: Admin UI (A/B/C) + Display (5 themes)

Admin UI: Dark Minimal / Retro Terminal / Warm Editorial via header buttons
Display: Classic White / Dark / Sepia / Nord / Terminal via Settings tab
Both switchers persist via localStorage / config.json
This commit is contained in:
ki
2026-08-26 14:41:34 +04:00
parent 99fb5692ef
commit a2c4158ba8
3 changed files with 228 additions and 17 deletions
+132 -17
View File
@@ -6,24 +6,36 @@
<title>epaper-dashboard</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%230a0a0a'/%3E%3Crect x='10' y='10' width='80' height='80' fill='%23fafafa'/%3E%3Crect x='15' y='15' width='35' height='35' fill='%230080ff'/%3E%3Crect x='50' y='15' width='35' height='35' fill='%23ff0080'/%3E%3Crect x='15' y='50' width='35' height='35' fill='%2300cc00'/%3E%3Crect x='50' y='50' width='35' height='35' fill='%23ff8000'/%3E%3C/svg%3E">
<style>
:root {
--bg: #0a0a0f;
--surface: #15151c;
--surface-2: #1f1f28;
--surface-3: #2a2a35;
--border: #2e2e3a;
--border-light: #3a3a48;
--fg: #f0f0f5;
--fg-muted: #9090a0;
--fg-dim: #6a6a78;
--accent: #6d8eff;
--accent-glow: rgba(109, 142, 255, 0.15);
--ok: #4ade80;
--warn: #fbbf24;
--alert: #f87171;
--info: #60a5fa;
--success: #22c55e;
/* ================================================================
ADMIN UI THEME VARIABLES — switch via data-theme="A|B|C" on <body>
================================================================ */
:root,
:root[data-theme="A"] {
/* A: Dark Minimal (original/default) */
--bg: #0a0a0f; --surface: #15151c; --surface-2: #1f1f28; --surface-3: #2a2a35;
--border: #2e2e3a; --border-light: #3a3a48;
--fg: #f0f0f5; --fg-muted: #9090a0; --fg-dim: #6a6a78;
--accent: #6d8eff; --accent-glow: rgba(109,142,255,0.15);
--ok: #4ade80; --warn: #fbbf24; --alert: #f87171; --info: #60a5fa; --success: #22c55e;
}
:root[data-theme="B"] {
/* B: Retro Terminal — CRT green phosphor */
--bg: #0d0d00; --surface: #141400; --surface-2: #1a1a00; --surface-3: #222200;
--border: #2a2a00; --border-light: #3a3a00;
--fg: #e8c840; --fg-muted: #a09020; --fg-dim: #605800;
--accent: #ffcc00; --accent-glow: rgba(255,204,0,0.12);
--ok: #88ff44; --warn: #ffaa00; --alert: #ff4444; --info: #88ccff; --success: #44ff88;
}
:root[data-theme="C"] {
/* C: Warm Editorial — newspaper / cream paper */
--bg: #f4f1eb; --surface: #faf8f4; --surface-2: #f0ede6; --surface-3: #e8e4dc;
--border: #d8d3c8; --border-light: #e6e1d8;
--fg: #1a1816; --fg-muted: #6b6560; --fg-dim: #9a9490;
--accent: #c0392b; --accent-glow: rgba(192,57,43,0.10);
--ok: #27ae60; --warn: #e67e22; --alert: #c0392b; --info: #2980b9; --success: #27ae60;
}
:root[data-theme="B"] body { background-image: repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.06) 2px,rgba(0,0,0,0.06) 4px); }
* { box-sizing: border-box; }
@@ -464,6 +476,29 @@
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
/* ============ Admin Theme Switcher ============ */
.admin-theme-switcher {
display: flex;
gap: 2px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
padding: 3px;
}
.theme-btn {
width: 28px; height: 28px;
border: none; border-radius: 4px;
background: transparent; color: var(--fg-muted);
font-size: 0.8em; font-weight: 700; font-family: monospace;
cursor: pointer; transition: all 0.15s;
}
.theme-btn:hover { background: var(--surface-2); color: var(--fg); }
.theme-btn.active {
background: var(--accent); color: #fff;
}
[data-theme="B"] .theme-btn.active { background: var(--accent); color: #000; }
[data-theme="C"] .theme-btn.active { background: var(--accent); color: #fff; }
</style>
</head>
<body>
@@ -494,6 +529,11 @@
<div class="mode-error" id="netError"></div>
</div>
</div>
<div class="admin-theme-switcher" title="Admin UI Theme">
<button class="theme-btn active" data-admin-theme="A" onclick="setAdminTheme('A')" title="Dark Minimal">A</button>
<button class="theme-btn" data-admin-theme="B" onclick="setAdminTheme('B')" title="Retro Terminal">B</button>
<button class="theme-btn" data-admin-theme="C" onclick="setAdminTheme('C')" title="Warm Editorial">C</button>
</div>
<button class="refresh" id="refreshBtn" onclick="triggerRefresh()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 1 1-3-6.7L21 8"/>
@@ -586,6 +626,36 @@
</div>
</div>
<div class="card">
<div class="card-header">
<h2>Display Theme</h2>
</div>
<div style="display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap;">
<div style="flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: 8px; font-size: 0.82em; color: var(--fg-muted);">
Theme für das e-Paper-Display
</label>
<select id="displayThemeSelect" onchange="setDisplayTheme(this.value)"
style="width: 100%; padding: 8px 12px; background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px; color: var(--fg); font-size: 0.9em;">
<option value="default" id="themeOpt_default">Classic White</option>
<option value="dark" id="themeOpt_dark">Dark Mode</option>
<option value="sepia" id="themeOpt_sepia">Sepia</option>
<option value="nord" id="themeOpt_nord">Nord</option>
<option value="terminal" id="themeOpt_terminal">Terminal (Green)</option>
</select>
</div>
<div id="themePreviewBox" style="display: flex; gap: 10px; align-items: center; padding-top: 22px;">
<div style="text-align: center;">
<div id="themePreview" style="width: 80px; height: 48px; border-radius: 4px; border: 1px solid var(--border); background: #fff; display: flex; align-items: center; justify-content: center; font-size: 0.65em; color: #000;">Preview</div>
<div style="font-size: 0.65em; color: var(--fg-dim); margin-top: 4px;" id="themePreviewLabel">Classic</div>
</div>
</div>
</div>
<div class="meta" style="margin-top: 12px;">
Theme wird beim nächsten Refresh auf dem Display angezeigt. Nur für Designs mit weißem Hintergrund geeignet (Widget-Farben bleiben gleich).
</div>
</div>
<div class="card">
<div class="card-header">
<h2>System</h2>
@@ -986,11 +1056,56 @@
async function netStopAp() { await netFetch('/api/network/ap/stop', { method: 'POST' }); setTimeout(refreshNetStatus, 5000); }
async function netForget(name) { if (!confirm(`"${name}" löschen?`)) return; await netFetch('/api/network/forget', { method: 'POST', body: new URLSearchParams({ name }) }); refreshSaved(); }
// ============ Admin UI Theme Switcher ============
function setAdminTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('adminTheme', theme);
document.querySelectorAll('.theme-btn').forEach(b => {
b.classList.toggle('active', b.dataset.adminTheme === theme);
});
}
// Restore saved admin theme on load
const savedTheme = localStorage.getItem('adminTheme');
if (savedTheme) setAdminTheme(savedTheme);
// ============ Display Theme Switcher ============
const THEME_PREVIEWS = {
default: { bg: '#ffffff', fg: '#000000', label: 'Classic' },
dark: { bg: '#111111', fg: '#f0f0f0', label: 'Dark' },
sepia: { bg: '#f4ecd8', fg: '#5b4636', label: 'Sepia' },
nord: { bg: '#eceff4', fg: '#2e3440', label: 'Nord' },
terminal: { bg: '#0d0d00', fg: '#e8c840', label: 'Terminal' },
};
function updateThemePreview(theme) {
const p = THEME_PREVIEWS[theme] || THEME_PREVIEWS.default;
const el = document.getElementById('themePreview');
const lbl = document.getElementById('themePreviewLabel');
if (el) { el.style.background = p.bg; el.style.color = p.fg; }
if (lbl) lbl.textContent = p.label;
}
async function loadDisplayTheme() {
const r = await fetch('/api/display_theme');
if (!r.ok) return;
const d = await r.json();
document.getElementById('displayThemeSelect').value = d.current;
updateThemePreview(d.current);
}
async function setDisplayTheme(theme) {
updateThemePreview(theme);
const r = await fetch('/api/display_theme', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({theme})
});
if (r.ok) triggerRefresh();
}
// ============ Init ============
renderGrid();
refreshNetStatus();
refreshScan();
refreshSaved();
loadDisplayTheme();
setInterval(refreshNetStatus, 10000);
setInterval(refreshScan, 60000);
setInterval(refreshPreview, 10000);