Linear design system: new admin UI + display design switcher (8 designs)

Admin UI: Linear.app dark theme — near-black canvas, indigo-violet accent, Inter font
Display: Minimal, Minimal Color, Bold, Analog Clock, Magazine, Cards, Kiosk, Classic
This commit is contained in:
ki
2026-08-26 15:08:17 +04:00
parent a2c4158ba8
commit 49de61bb26
6 changed files with 1595 additions and 119 deletions
+600 -74
View File
@@ -4,40 +4,552 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>epaper-dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;510;590&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<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>
/* ================================================================
ADMIN UI THEME VARIABLES — switch via data-theme="A|B|C" on <body>
LINEAR DESIGN SYSTEM — epaper-dashboard Admin UI
Based on Linear.app design system
================================================================ */
: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 {
/* Background Surfaces — near-black dark mode */
--bg: #08090a; /* marketing black */
--surface: #0f1011; /* panel dark */
--surface-2: #191a1b; /* elevated surface */
--surface-3: #28282c; /* hover/lighter surface */
/* Text */
--fg: #f7f8f8; /* primary white (not pure) */
--fg-muted: #d0d6e0; /* silver-gray body */
--fg-dim: #8a8f98; /* tertiary/muted */
--fg-subtle: #62666d; /* quaternary/timestamps */
/* Brand Accent — Linear indigo-violet */
--accent: #5e6ad2; /* brand indigo */
--accent-bright: #7170ff; /* interactive accent */
--accent-hover: #828fff; /* hover state */
/* Status */
--ok: #27a644; /* success green */
--success: #10b981; /* emerald */
--warn: #fbbf24; /* amber */
--alert: #f87171; /* red */
--info: #60a5fa; /* blue */
/* Borders — semi-transparent white */
--border: rgba(255,255,255,0.08); /* standard */
--border-subtle: rgba(255,255,255,0.05); /* subtle/default */
--border-light: rgba(255,255,255,0.12); /* lighter */
/* Glow */
--accent-glow: rgba(113,112,255,0.15);
--ok-glow: rgba(39,166,68,0.25);
--warn-glow: rgba(251,191,36,0.25);
}
: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; margin: 0; padding: 0; }
* { box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-feature-settings: "cv01", "ss03";
background: var(--bg);
color: var(--fg);
-webkit-font-smoothing: antialiased;
line-height: 1.5;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 24px 32px 80px;
}
/* ============ Header ============ */
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
margin-bottom: 32px;
border-bottom: 1px solid var(--border-subtle);
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.brand-logo {
width: 34px; height: 34px;
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-bright) 100%);
border-radius: 8px;
display: grid;
place-items: center;
}
.brand-logo svg { color: white; }
.brand-text h1 {
font-size: 1.1em; font-weight: 510;
letter-spacing: -0.02em;
color: var(--fg);
}
.brand-text .sub {
font-size: 0.78em; color: var(--fg-dim);
font-family: ui-monospace, 'SF Mono', monospace;
}
.header-actions {
display: flex; gap: 10px; align-items: center;
}
/* ============ Net-Status ============ */
.net-status {
display: flex; align-items: center; gap: 12px;
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 8px; padding: 8px 14px;
font-size: 0.85em;
}
.net-status .mode-dot {
width: 7px; height: 7px; border-radius: 50%;
background: var(--fg-subtle);
}
.net-status.online .mode-dot {
background: var(--ok);
box-shadow: 0 0 6px var(--ok-glow);
}
.net-status.ap .mode-dot {
background: var(--warn);
animation: pulse 2s ease-in-out infinite;
}
.net-status.offline .mode-dot { background: var(--alert); }
.net-status.connecting .mode-dot {
background: var(--accent-bright);
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.net-status .info-block { display: flex; flex-direction: column; line-height: 1.3; }
.net-status .mode-label { font-weight: 510; font-size: 0.88em; }
.net-status .mode-meta { color: var(--fg-dim); font-size: 0.85em; }
.net-status .mode-error { color: var(--alert); font-size: 0.82em; margin-top: 2px; }
/* ============ Buttons — Linear ghost style ============ */
button, .btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 7px 14px;
background: rgba(255,255,255,0.02);
border: 1px solid rgba(36,40,44,1);
color: var(--fg-muted);
border-radius: 6px; cursor: pointer; font-size: 0.88em;
font-family: inherit; font-weight: 400;
transition: all 0.12s ease;
}
button:hover, .btn:hover {
background: rgba(255,255,255,0.04);
color: var(--fg);
border-color: rgba(255,255,255,0.08);
}
button:active { transform: scale(0.98); }
button:disabled { opacity: 0.4; cursor: not-allowed; }
button.primary {
background: var(--accent); color: #fff; border-color: var(--accent);
}
button.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
button.danger { color: var(--alert); border-color: rgba(248,113,113,0.3); }
button.danger:hover { background: rgba(248,113,113,0.08); }
button.warn { color: var(--warn); border-color: rgba(251,191,36,0.3); }
button.ghost { background: transparent; border-color: transparent; }
button.ghost:hover { background: rgba(255,255,255,0.04); border-color: rgba(255,255,255,0.08); }
button.icon { padding: 6px 8px; }
button.refresh {
background: var(--accent); color: #fff; border-color: var(--accent);
}
button.refresh:hover {
background: var(--accent-hover);
box-shadow: 0 2px 8px var(--accent-glow);
}
/* ============ Tabs ============ */
.tabs {
display: flex; gap: 2px;
border-bottom: 1px solid var(--border-subtle);
margin-bottom: 28px;
}
.tab {
padding: 10px 16px;
background: transparent; border: none;
border-bottom: 2px solid transparent;
color: var(--fg-dim); cursor: pointer;
font-size: 0.9em; font-weight: 400;
font-family: inherit;
transition: all 0.12s ease;
margin-bottom: -1px;
}
.tab:hover { color: var(--fg-muted); }
.tab.active {
color: var(--fg); border-bottom-color: var(--accent-bright);
font-weight: 510;
}
.tab-content { display: none; }
.tab-content.active { display: block; }
/* ============ Cards — Linear elevated surface ============ */
.card {
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 10px;
padding: 20px 22px;
margin-bottom: 16px;
}
.card-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 16px; padding-bottom: 12px;
border-bottom: 1px solid var(--border-subtle);
}
.card-header h2, .card-header h3 {
font-size: 0.9em; font-weight: 510;
letter-spacing: -0.01em;
display: flex; align-items: center; gap: 8px;
}
.card-header .badge {
font-size: 0.7em; padding: 2px 8px;
background: rgba(255,255,255,0.05);
color: var(--fg-dim);
border-radius: 4px;
font-family: ui-monospace, 'SF Mono', monospace;
font-weight: 400;
}
/* ============ Layout Editor ============ */
.layout-toolbar {
display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
margin-bottom: 16px;
}
.add-form {
display: flex; gap: 8px; align-items: center;
background: var(--surface-2);
border: 1px solid var(--border-subtle);
border-radius: 8px;
padding: 4px 4px 4px 14px;
}
.add-form select, .add-form input {
border: none; background: transparent; color: var(--fg);
padding: 6px 8px; font-family: inherit; font-size: 0.88em;
}
.add-form select { padding-right: 20px; }
.add-form select:focus, .add-form input:focus { outline: none; }
.add-form button { padding: 6px 12px; font-size: 0.85em; }
.grid-preview {
display: grid;
grid-template-columns: repeat({{ grid.cols }}, 1fr);
grid-template-rows: repeat({{ grid.rows }}, 120px);
gap: 6px;
background: var(--border-subtle);
padding: 6px;
border-radius: 10px;
margin-bottom: 12px;
}
.grid-cell {
background: var(--surface-2);
border-radius: 5px;
}
.grid-item {
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 8px;
padding: 10px 12px;
display: flex; flex-direction: column;
justify-content: space-between;
overflow: hidden;
position: relative;
transition: all 0.12s ease;
cursor: grab;
}
.grid-item:hover {
border-color: var(--accent-bright);
background: var(--surface-2);
}
.grid-item.dragging { opacity: 0.4; }
.grid-item.oob {
border-color: rgba(248,113,113,0.5);
background: rgba(248,113,113,0.04);
}
.grid-item .item-label {
font-weight: 510; font-size: 0.85em; line-height: 1.2;
color: var(--fg);
}
.grid-item .item-meta {
font-size: 0.7em; color: var(--fg-dim);
font-family: ui-monospace, 'SF Mono', monospace;
}
.grid-item .item-category {
display: inline-block;
font-size: 0.62em; padding: 2px 6px;
background: rgba(113,112,255,0.1);
color: var(--accent-bright);
border-radius: 3px;
text-transform: uppercase; letter-spacing: 0.06em;
font-weight: 510;
margin-top: 4px;
}
.grid-item .item-controls {
position: absolute; top: 6px; right: 6px;
display: flex; gap: 2px; opacity: 0;
transition: opacity 0.12s;
}
.grid-item:hover .item-controls { opacity: 1; }
.grid-item .item-controls button {
padding: 2px 5px; font-size: 0.68em; line-height: 1.3;
background: var(--surface-2);
border: 1px solid var(--border-subtle);
}
.layout-status {
font-size: 0.82em; color: var(--fg-dim);
display: flex; align-items: center; gap: 6px;
}
.layout-status.has-warnings { color: var(--warn); }
.layout-status::before {
content: ''; width: 6px; height: 6px; border-radius: 50%;
background: currentColor;
}
/* ============ Plugin-Config Cards ============ */
.plugin-config-card {
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 10px;
padding: 16px 20px;
margin-bottom: 12px;
}
.plugin-config-card h3 {
font-size: 0.9em; font-weight: 510;
letter-spacing: -0.01em;
display: flex; align-items: center; gap: 8px;
margin-bottom: 4px;
}
.plugin-config-card h3 .badge {
font-size: 0.7em; padding: 2px 8px;
background: rgba(255,255,255,0.05);
color: var(--fg-dim);
border-radius: 4px;
font-family: ui-monospace, 'SF Mono', monospace;
font-weight: 400;
}
.plugin-config-card .desc {
font-size: 0.82em; color: var(--fg-dim); margin-bottom: 14px;
}
.plugin-config-card label {
display: block; margin: 12px 0 4px;
font-size: 0.76em; color: var(--fg-dim);
text-transform: uppercase; letter-spacing: 0.06em;
font-weight: 500;
}
.plugin-config-card input[type=text],
.plugin-config-card input[type=number],
.plugin-config-card input[type=password],
.plugin-config-card select {
width: 100%; padding: 9px 12px;
background: rgba(255,255,255,0.02);
border: 1px solid var(--border-subtle);
border-radius: 6px;
color: var(--fg);
font-size: 0.88em; font-family: inherit;
transition: all 0.12s;
}
.plugin-config-card input:focus, .plugin-config-card select:focus {
outline: none;
border-color: var(--accent-bright);
box-shadow: 0 0 0 3px rgba(113,112,255,0.1);
}
.plugin-config-card input[type=checkbox] {
width: 16px; height: 16px;
accent-color: var(--accent-bright);
}
.plugin-config-card .checkbox-row {
display: flex; align-items: center; gap: 8px; margin: 12px 0;
}
.plugin-config-card .checkbox-row label {
margin: 0; text-transform: none; letter-spacing: 0; font-size: 0.88em;
color: var(--fg-muted);
}
.plugin-config-card .help {
font-size: 0.76em; color: var(--fg-subtle);
margin-top: 4px; font-style: italic;
}
.plugin-config-card .secret-row {
display: flex; gap: 6px; align-items: stretch;
}
.plugin-config-card .secret-row input {
flex: 1;
font-family: ui-monospace, 'SF Mono', monospace;
}
.plugin-config-card .secret-row button {
padding: 0 12px;
color: var(--alert);
border-color: rgba(248,113,113,0.3);
}
.plugin-config-card .actions {
display: flex; gap: 8px; margin-top: 16px;
padding-top: 12px; border-top: 1px solid var(--border-subtle);
}
.plugin-config-card .save-status {
font-size: 0.78em; color: var(--success);
display: none; align-items: center; gap: 4px;
}
.plugin-config-card .save-status.visible { display: inline-flex; }
/* ============ Preview ============ */
.preview-card {
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 10px;
padding: 20px 22px;
margin-bottom: 24px;
}
.preview-card .preview-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 12px;
}
.preview-card .preview-header h2 {
font-size: 0.9em; font-weight: 510;
letter-spacing: -0.01em;
}
.preview-card .meta { color: var(--fg-dim); font-size: 0.82em; }
.preview-card img {
max-width: 100%; height: auto; display: block;
border-radius: 6px;
border: 1px solid var(--border-subtle);
image-rendering: pixelated;
}
/* ============ Network panel ============ */
.network-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
}
.network-grid > .card { margin-bottom: 0; }
.network-grid .full-width { grid-column: 1 / -1; }
.network-grid h3 {
font-size: 0.9em; font-weight: 510;
letter-spacing: -0.01em;
margin-bottom: 12px;
}
.network-grid label {
display: block; margin: 12px 0 4px;
font-size: 0.76em; color: var(--fg-dim);
text-transform: uppercase; letter-spacing: 0.06em;
}
.network-grid input, .network-grid select {
width: 100%; padding: 9px 12px;
background: rgba(255,255,255,0.02);
border: 1px solid var(--border-subtle);
border-radius: 6px; color: var(--fg);
font-family: inherit; font-size: 0.88em;
}
.network-grid input:focus, .network-grid select:focus {
outline: none; border-color: var(--accent-bright);
}
.network-grid .checkbox-row {
display: flex; align-items: center; gap: 8px; margin: 12px 0;
}
.network-grid .checkbox-row label { margin: 0; text-transform: none; letter-spacing: 0; }
.network-grid ul#savedList { list-style: none; padding: 0; margin: 0; }
.network-grid ul#savedList li {
display: flex; justify-content: space-between; align-items: center;
padding: 8px 0; border-bottom: 1px solid var(--border-subtle);
}
.network-grid ul#savedList li:last-child { border-bottom: 0; }
.network-grid ul#savedList code {
font-family: ui-monospace, 'SF Mono', monospace; font-size: 0.85em;
}
.network-grid details {
background: var(--surface-2);
border: 1px solid var(--border-subtle);
border-radius: 6px; padding: 12px 16px;
}
.network-grid details summary {
cursor: pointer; font-weight: 510; color: var(--fg-dim); font-size: 0.88em;
}
.network-grid details code {
background: var(--bg); padding: 2px 6px; border-radius: 3px;
font-size: 0.88em;
}
/* ============ Empty state ============ */
.empty-state {
text-align: center; padding: 48px 20px; color: var(--fg-dim);
}
.empty-state .icon { font-size: 2em; margin-bottom: 10px; opacity: 0.4; }
.empty-state .hint { font-size: 0.85em; margin-top: 4px; color: var(--fg-subtle); }
/* ============ Admin Theme Switcher ============ */
.admin-theme-switcher {
display: flex; gap: 2px;
background: rgba(255,255,255,0.02);
border: 1px solid var(--border-subtle);
border-radius: 6px; padding: 3px;
}
.theme-btn {
width: 26px; height: 26px;
border: none; border-radius: 4px;
background: transparent; color: var(--fg-dim);
font-size: 0.75em; font-weight: 510; font-family: inherit;
cursor: pointer; transition: all 0.12s;
}
.theme-btn:hover { background: rgba(255,255,255,0.05); color: var(--fg); }
.theme-btn.active {
background: var(--accent-bright); color: #fff;
}
/* ============ Display Design Picker ============ */
.design-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
margin-top: 16px;
}
.design-option {
background: var(--surface-2);
border: 1px solid var(--border-subtle);
border-radius: 8px;
padding: 12px;
cursor: pointer;
transition: all 0.12s;
text-align: center;
}
.design-option:hover {
border-color: var(--accent-bright);
background: var(--surface-3);
}
.design-option.selected {
border-color: var(--accent-bright);
background: rgba(113,112,255,0.06);
box-shadow: 0 0 0 1px var(--accent-bright);
}
.design-option .design-name {
font-size: 0.82em; font-weight: 510; margin-bottom: 4px;
}
.design-option .design-desc {
font-size: 0.72em; color: var(--fg-dim); line-height: 1.4;
}
/* ============ Responsive ============ */
@media (max-width: 900px) {
.container { padding: 16px; }
.network-grid { grid-template-columns: 1fr; }
.app-header { flex-direction: column; align-items: flex-start; gap: 16px; }
.header-actions { width: 100%; flex-wrap: wrap; }
}
/* ============ Scrollbar ============ */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.12); }
</style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif;
@@ -628,31 +1140,45 @@
<div class="card">
<div class="card-header">
<h2>Display Theme</h2>
<h2>Display Design</h2>
<span class="badge" id="currentDesignBadge"></span>
</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>
<p style="font-size: 0.82em; color: var(--fg-dim); margin-bottom: 16px;">
bestimmt das Layout auf dem e-Paper-Display.
</p>
<div class="design-grid" id="designGrid">
<div class="design-option" data-design="minimal" onclick="setDisplayDesign('minimal')">
<div class="design-name">Minimal</div>
<div class="design-desc">Nur Content, kein UI-Chrome, maximal clean</div>
</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 class="design-option" data-design="minimal_color" onclick="setDisplayDesign('minimal_color')">
<div class="design-name">Minimal Color</div>
<div class="design-desc">WinZiger Akzent-Strich pro Widget</div>
</div>
<div class="design-option" data-design="bold" onclick="setDisplayDesign('bold')">
<div class="design-name">Bold</div>
<div class="design-desc">Riesen-Typography, 1px-Rahmen, vollflächig</div>
</div>
<div class="design-option" data-design="analog" onclick="setDisplayDesign('analog')">
<div class="design-name">Analog Clock</div>
<div class="design-desc">2×2-Uhr als Ziffernblatt, Rest clean Panels</div>
</div>
<div class="design-option" data-design="magazine" onclick="setDisplayDesign('magazine')">
<div class="design-name">Magazine</div>
<div class="design-desc">Bold Typography, farbige Akzentstreifen links</div>
</div>
<div class="design-option" data-design="cards" onclick="setDisplayDesign('cards')">
<div class="design-name">Cards</div>
<div class="design-desc">iOS-Widget-Style, Emoji-Icons, warme Paneele</div>
</div>
<div class="design-option" data-design="kiosk" onclick="setDisplayDesign('kiosk')">
<div class="design-name">Kiosk</div>
<div class="design-desc">Dunkel, Daten-first, kompakte Info-Zellen</div>
</div>
<div class="design-option" data-design="classic" onclick="setDisplayDesign('classic')">
<div class="design-name">Classic</div>
<div class="design-desc">Original flaches Design, weißer Hintergrund</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>
@@ -1068,36 +1594,36 @@
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' },
// ============ Display Design Switcher ============
const DESIGN_LABELS = {
minimal: "Minimal", minimal_color: "Minimal Color",
bold: "Bold", analog: "Analog Clock", magazine: "Magazine",
cards: "Cards", kiosk: "Kiosk", classic: "Classic",
};
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');
async function loadDisplayDesign() {
const r = await fetch('/api/config');
if (!r.ok) return;
const d = await r.json();
document.getElementById('displayThemeSelect').value = d.current;
updateThemePreview(d.current);
const cfg = await r.json();
const current = cfg.display_design || 'minimal';
document.querySelectorAll('.design-option').forEach(el => {
el.classList.toggle('selected', el.dataset.design === current);
});
const badge = document.getElementById('currentDesignBadge');
if (badge) badge.textContent = DESIGN_LABELS[current] || current;
}
async function setDisplayTheme(theme) {
updateThemePreview(theme);
const r = await fetch('/api/display_theme', {
async function setDisplayDesign(design) {
await fetch('/api/display_design', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({theme})
body: JSON.stringify({design}),
});
if (r.ok) triggerRefresh();
document.querySelectorAll('.design-option').forEach(el => {
el.classList.toggle('selected', el.dataset.design === design);
});
const badge = document.getElementById('currentDesignBadge');
if (badge) badge.textContent = DESIGN_LABELS[design] || design;
}
// ============ Init ============
@@ -1105,7 +1631,7 @@
refreshNetStatus();
refreshScan();
refreshSaved();
loadDisplayTheme();
loadDisplayDesign();
setInterval(refreshNetStatus, 10000);
setInterval(refreshScan, 60000);
setInterval(refreshPreview, 10000);