Files
ledgrab/docs/settings-modal-redesign.html
alexei.dolgolyov 9d4a534ec6 feat(ui): release notes overlay v2 + settings/streams/dashboard polish
Release Notes overlay redesign (scoped via .release-notes-shell)
- Backend exposes release.assets (name/size/download_url) through
  UpdateReleaseInfo so the frontend can render real download links.
- New masthead: eyebrow + display-font title + tag/published/pre-release
  chip strip + close/external action buttons; opts out of layout.css's
  global `header { height: 60px }` and `header::before` accent bar that
  were leaking into the overlay's <header>.
- Markdown body: <code> filenames are wrapped in clickable <a> via fuzzy
  asset match (exact basename, then same-extension token-overlap), with
  per-asset description tooltip and a small download glyph.
- Per-asset description derived from filename pattern (Windows installer
  /portable/msi, Linux tarball/AppImage/deb/rpm, macOS dmg/pkg, Android
  apk/aab, iOS ipa, generic archives) with i18n keys in en/ru/zh.
- Hide checksum / signature side-files (.sha256/.sha512/.sig/.asc/...).

Settings modal & dashboard polish
- ds-section refresh, rail-channel routing, notif matrix updates.
- Dashboard customize panel + per-account layout updates.
- New docs/settings-modal-redesign.html design reference.

Streams / targets / color-strip
- Stream cards rewrite (cards.css, streams.css, streams.ts).
- Composite stream + metrics history adjustments.
- WLED target processor + color-strip pipeline refinements.
- Color-strip WS source streamer touch-ups.

Misc
- Perf charts overhaul; tabular game-integration / HA / MQTT / weather
  source cards; donation/sync-clocks/scene-presets minor polish.
- New i18n keys across en/ru/zh.

Test infrastructure
- conftest pre-creates the test DB so main.py's legacy-data migration
  doesn't shovel the user's production DB into the test temp dir.
- test_preferences_notifications wipes its own setting at the start of
  the defaults test (was relying on isolation it never enforced).

Pre-commit gates: ruff clean, tsc clean, npm run build clean,
pytest 899/899 passing.
2026-04-29 17:14:05 +03:00

1752 lines
74 KiB
HTML

<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>LedGrab · Global Settings · redesign mockup</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Big+Shoulders+Display:wght@700;800;900&family=JetBrains+Mono:wght@400;500;600;700&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
/* =====================================================================
SETTINGS MODAL — redesign mockup
- Reuses Lumenworks tokens & .ds-section rack-panel vocabulary
- Replaces 6-icon top tab strip with a left rail (icon + label)
- Channel-coded sections, per-section save bar, notif matrix,
danger-zone Lifecycle panel
===================================================================== */
:root{
--primary-color:#4CAF50;
--primary-text:#66bb6a;
--primary-contrast:#fff;
--danger-color:#f44336;
--warning-color:#ff9800;
--info-color:#2196F3;
--font-display:'Big Shoulders Display','Orbitron',sans-serif;
--font-body:'Manrope',-apple-system,BlinkMacSystemFont,sans-serif;
--font-mono:'JetBrains Mono','Cascadia Code',ui-monospace,monospace;
--lux-r-sm:3px;
--lux-r-md:6px;
--lux-r-lg:10px;
--lux-hairline:1px;
--lux-rule:2px;
--space-xs:4px;
--space-sm:8px;
--space-md:12px;
--space-lg:20px;
--space-xl:40px;
--duration:.22s;
--ease:cubic-bezier(.16,1,.3,1);
/* channel palette — verbatim from base.css */
--ch-signal:#4CAF50;
--ch-signal-dim:#66bb6a;
--ch-cyan:#00d8ff;
--ch-magenta:#ff4ade;
--ch-amber:#ffb800;
--ch-coral:#ff5e5e;
--ch-violet:#8b7eff;
}
[data-theme="dark"]{
--bg-color:#000;
--bg-secondary:#0a0b0d;
--card-bg:#000;
--text-color:#e0e0e0;
--text-secondary:#999;
--text-muted:#777;
--border-color:#404040;
--shadow-color:rgba(0,0,0,.3);
--hover-bg:rgba(255,255,255,.05);
--input-bg:#1a1a2e;
--primary-text-color:#66bb6a;
--success-color:#28a745;
--lux-bg-0:#000;
--lux-bg-1:#0e1014;
--lux-bg-2:#15181d;
--lux-bg-3:#1c2027;
--lux-line:#232831;
--lux-line-bold:#2e3440;
--lux-ink:#e6ebf2;
--lux-ink-dim:#8b95a5;
--lux-ink-mute:#5b6473;
--lux-ink-faint:#3a414c;
color-scheme:dark;
}
[data-theme="light"]{
--bg-color:#fff;
--bg-secondary:#fafbfc;
--card-bg:#fff;
--text-color:#333;
--text-secondary:#595959;
--text-muted:#767676;
--border-color:#e0e0e0;
--shadow-color:rgba(0,0,0,.12);
--hover-bg:rgba(0,0,0,.05);
--input-bg:#f0f0f0;
--primary-color:#2e7d32;
--primary-text-color:#3d8b40;
--success-color:#2e7d32;
--lux-bg-0:#fff;
--lux-bg-1:#f6f8fb;
--lux-bg-2:#eef1f5;
--lux-bg-3:#e4e8ee;
--lux-line:#dee3ea;
--lux-line-bold:#c4ccd6;
--lux-ink:#0f1419;
--lux-ink-dim:#4c5866;
--lux-ink-mute:#6b7684;
--lux-ink-faint:#a5afbc;
color-scheme:light;
}
*{margin:0;padding:0;box-sizing:border-box}
body{
font-family:var(--font-body);
background:
radial-gradient(ellipse 80% 60% at 50% -10%, rgba(255,184,0,.05), transparent 60%),
radial-gradient(ellipse 60% 60% at 100% 100%, rgba(0,216,255,.04), transparent 60%),
var(--bg-color);
color:var(--text-color);
min-height:100vh;
display:flex;
flex-direction:column;
align-items:center;
padding:32px 16px 60px;
-webkit-font-smoothing:antialiased;
}
.demo-toolbar{
width:100%;
max-width:760px;
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:24px;
color:var(--lux-ink-dim);
font-size:.78rem;
letter-spacing:.06em;
text-transform:uppercase;
}
.demo-title{
font-family:var(--font-display);
font-weight:800;
font-size:1.6rem;
letter-spacing:.02em;
color:var(--lux-ink);
text-transform:none;
}
.demo-title small{
display:block;
font-family:var(--font-body);
font-weight:500;
font-size:.7rem;
letter-spacing:.16em;
color:var(--ch-amber);
margin-bottom:4px;
}
.theme-toggle{
display:flex;
gap:4px;
padding:3px;
background:var(--lux-bg-1);
border:1px solid var(--lux-line);
border-radius:999px;
}
.theme-toggle button{
background:none;border:none;color:var(--lux-ink-dim);
font:inherit; font-size:.72rem; letter-spacing:.1em; text-transform:uppercase;
padding:6px 12px; border-radius:999px; cursor:pointer;
transition:all var(--duration) var(--ease);
}
.theme-toggle button.active{
background:var(--ch-amber); color:#000; font-weight:700;
}
/* ── ICONS ── */
.icon{
display:inline-block; width:1em; height:1em; vertical-align:-.125em;
fill:none; stroke:currentColor; stroke-width:2;
stroke-linecap:round; stroke-linejoin:round; flex-shrink:0;
}
/* ====================================================================
MODAL SHELL — same Lumenworks anatomy as production modals
==================================================================== */
.modal{
width:100%;
max-width:760px;
background:linear-gradient(180deg,var(--lux-bg-1) 0%, var(--lux-bg-2) 100%);
border:var(--lux-hairline) solid var(--lux-line-bold);
border-radius:var(--lux-r-lg);
box-shadow:
0 0 0 1px rgba(255,255,255,.02),
0 20px 60px rgba(0,0,0,.5),
0 8px 32px var(--shadow-color);
display:flex; flex-direction:column;
max-height:calc(100vh - 100px);
position:relative;
overflow:hidden;
--modal-ch:var(--ch-amber);
animation:slideUp .4s var(--ease);
}
@keyframes slideUp{
from{transform:translateY(20px) scale(.98); opacity:0}
to{transform:translateY(0) scale(1); opacity:1}
}
/* channel stripe top */
.modal::before{
content:''; position:absolute; left:0; right:0; top:0; height:2px;
background:linear-gradient(90deg, transparent, var(--modal-ch) 20%, var(--modal-ch) 80%, transparent);
box-shadow:0 0 12px color-mix(in srgb, var(--modal-ch) 50%, transparent);
z-index:2;
}
/* corner bracket bottom right */
.modal::after{
content:''; position:absolute; right:10px; bottom:10px; width:12px; height:12px;
border-bottom:1px solid var(--lux-line-bold);
border-right:1px solid var(--lux-line-bold);
opacity:.5;
}
.modal-header{
padding:18px 24px 16px 28px;
border-bottom:1px solid var(--lux-line);
display:flex; align-items:center; justify-content:space-between;
position:relative;
}
.modal-header::before{
content:''; position:absolute; left:14px; top:50%; transform:translateY(-50%);
width:3px; height:22px; border-radius:2px;
background:var(--modal-ch);
box-shadow:0 0 10px color-mix(in srgb, var(--modal-ch) 50%, transparent);
opacity:.85;
}
.modal-header h2{
font-family:var(--font-body);
font-size:1.15rem;
font-weight:700;
letter-spacing:-.01em;
color:var(--lux-ink);
display:flex; align-items:center; gap:8px;
}
.modal-header h2 .icon{ color:var(--modal-ch); width:18px; height:18px; }
.modal-close-btn{
background:none; border:none; color:var(--text-muted);
width:32px; height:32px; display:flex; align-items:center; justify-content:center;
cursor:pointer; border-radius:4px; transition:.2s;
font-size:1.2rem;
}
.modal-close-btn:hover{ color:var(--text-color); background:rgba(128,128,128,.15); }
/* ====================================================================
SETTINGS LAYOUT — left rail + scrollable panel
==================================================================== */
.settings-layout{
display:flex;
min-height:0;
flex:1 1 auto;
}
.settings-rail{
flex:0 0 184px;
background:linear-gradient(180deg, color-mix(in srgb, var(--lux-bg-2) 60%, transparent), transparent);
border-right:1px solid var(--lux-line);
padding:16px 0 16px;
display:flex; flex-direction:column;
gap:2px;
overflow-y:auto;
}
.rail-section-label{
padding:8px 16px 4px;
font-size:.62rem;
font-weight:600;
letter-spacing:.18em;
text-transform:uppercase;
color:var(--lux-ink-faint);
display:flex; align-items:center; gap:6px;
}
.rail-section-label::before{
content:''; width:6px; height:1px; background:var(--lux-line-bold);
}
.rail-btn{
background:none; border:none; cursor:pointer;
width:100%;
padding:9px 16px 9px 18px;
display:flex; align-items:center; gap:10px;
color:var(--lux-ink-dim);
font:inherit; font-size:.85rem; font-weight:500;
text-align:left;
border-left:2px solid transparent;
transition:all var(--duration) var(--ease);
position:relative;
}
.rail-btn .icon{ width:16px; height:16px; flex-shrink:0; }
.rail-btn .rail-dot{
margin-left:auto;
width:5px; height:5px; border-radius:50%;
background:transparent;
transition:all var(--duration) var(--ease);
}
.rail-btn:hover{
color:var(--lux-ink);
background:color-mix(in srgb, var(--rail-ch, var(--ch-amber)) 6%, transparent);
}
.rail-btn.active{
color:var(--lux-ink);
background:color-mix(in srgb, var(--rail-ch, var(--ch-amber)) 10%, transparent);
border-left-color:var(--rail-ch, var(--ch-amber));
font-weight:600;
}
.rail-btn.active .icon{
color:var(--rail-ch, var(--ch-amber));
filter:drop-shadow(0 0 6px color-mix(in srgb, var(--rail-ch, var(--ch-amber)) 50%, transparent));
}
.rail-btn.active .rail-dot{
background:var(--rail-ch, var(--ch-amber));
box-shadow:0 0 6px color-mix(in srgb, var(--rail-ch, var(--ch-amber)) 70%, transparent);
}
.rail-btn .rail-badge{
margin-left:auto;
font-size:.6rem; font-weight:700;
padding:1px 6px; border-radius:999px;
background:color-mix(in srgb, var(--ch-coral) 80%, transparent);
color:#fff;
letter-spacing:.04em;
}
.rail-footer{
margin-top:auto;
padding:12px 16px 0;
border-top:1px solid var(--lux-line);
font-family:var(--font-mono);
font-size:.62rem;
letter-spacing:.08em;
color:var(--lux-ink-faint);
}
/* ── Body ── */
.settings-body{
flex:1 1 auto;
overflow-y:auto;
scrollbar-gutter:stable;
padding:20px 24px 24px;
min-width:0;
}
.settings-tab{ display:none; animation:fadeIn .3s var(--ease); }
.settings-tab.active{ display:block; }
@keyframes fadeIn{
from{opacity:0; transform:translateY(4px)}
to{opacity:1; transform:translateY(0)}
}
/* ====================================================================
.ds-section — verbatim Lumenworks rack-panel pattern
==================================================================== */
.ds-section{
--ds-ch:var(--ch-signal);
position:relative;
margin:0 0 14px;
padding:0 14px 4px;
border:1px solid var(--lux-line);
border-radius:var(--lux-r-md);
background:linear-gradient(180deg,
color-mix(in srgb, var(--ds-ch) 4%, var(--lux-bg-2)) 0%,
color-mix(in srgb, var(--lux-bg-1) 60%, transparent) 100%);
box-shadow:
inset 0 1px 0 rgba(255,255,255,.025),
0 1px 0 rgba(0,0,0,.18);
}
.ds-section[data-ch="cyan"] { --ds-ch:var(--ch-cyan); }
.ds-section[data-ch="amber"] { --ds-ch:var(--ch-amber); }
.ds-section[data-ch="violet"] { --ds-ch:var(--ch-violet); }
.ds-section[data-ch="coral"] { --ds-ch:var(--ch-coral); }
.ds-section[data-ch="magenta"]{ --ds-ch:var(--ch-magenta); }
.ds-section::before{
content:''; position:absolute; left:0; top:4px; bottom:4px;
width:2px; border-radius:2px;
background:linear-gradient(180deg,
color-mix(in srgb, var(--ds-ch) 70%, transparent) 0%,
transparent 100%);
opacity:.65;
}
.ds-section-header{
display:flex; align-items:center; gap:7px;
margin:0 -4px 6px; padding:8px 4px 4px;
line-height:1;
}
.ds-section-dot{
width:6px; height:6px; border-radius:50%;
background:var(--ds-ch);
box-shadow:
0 0 7px color-mix(in srgb, var(--ds-ch) 65%, transparent),
0 0 0 1px color-mix(in srgb, var(--ds-ch) 30%, transparent);
}
.ds-section-title{
font-size:.7rem; font-weight:700;
text-transform:uppercase; letter-spacing:.16em;
color:var(--lux-ink); opacity:.85;
}
.ds-section-index{
margin-left:auto;
font-family:var(--font-mono);
font-size:.62rem; font-weight:500;
color:var(--lux-ink-mute);
letter-spacing:.12em; opacity:.55;
font-variant-numeric:tabular-nums;
}
.ds-section-meta{
margin-left:8px;
font-family:var(--font-mono);
font-size:.65rem; font-weight:500;
color:var(--lux-ink-mute);
letter-spacing:.06em;
padding:2px 6px;
border:1px solid color-mix(in srgb, var(--ds-ch) 30%, transparent);
border-radius:3px;
background:color-mix(in srgb, var(--ds-ch) 8%, transparent);
}
.ds-section-body{ padding:0 0 8px; }
/* ── form-group basics ── */
.form-group{ margin-bottom:12px; }
.form-group:last-child{ margin-bottom:8px; }
.label-row{
display:flex; align-items:center; justify-content:space-between;
gap:8px; margin-bottom:6px;
}
.label-row label{
font-size:.78rem; font-weight:600;
color:var(--lux-ink); letter-spacing:.01em;
}
.hint-toggle{
width:18px; height:18px; border-radius:50%;
background:transparent;
border:1px solid var(--lux-line-bold);
color:var(--lux-ink-mute);
cursor:pointer; font-size:.7rem; font-weight:700;
display:flex; align-items:center; justify-content:center;
transition:all var(--duration) var(--ease);
}
.hint-toggle:hover{ color:var(--lux-ink); border-color:var(--ch-amber); }
.input-hint{
display:block;
font-size:.72rem; line-height:1.45;
color:var(--lux-ink-mute);
margin-bottom:6px;
}
input[type="text"], input[type="number"], input[type="url"], select{
width:100%;
padding:8px 10px;
background:var(--lux-bg-1);
border:1px solid var(--lux-line);
border-radius:var(--lux-r-sm);
color:var(--lux-ink);
font-family:var(--font-body);
font-size:.85rem;
transition:all var(--duration) var(--ease);
}
input:focus, select:focus{
outline:none;
border-color:var(--ch-amber);
box-shadow:0 0 0 3px color-mix(in srgb, var(--ch-amber) 18%, transparent);
}
select{
appearance:none;
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='none' stroke='%238b95a5' stroke-width='1.5' d='M1 1l4 4 4-4'/></svg>");
background-repeat:no-repeat;
background-position:right 10px center;
padding-right:28px;
}
/* ── Buttons ── */
.btn{
display:inline-flex; align-items:center; justify-content:center;
gap:6px;
padding:8px 14px;
background:var(--lux-bg-1);
border:1px solid var(--lux-line-bold);
border-radius:var(--lux-r-sm);
color:var(--lux-ink);
font-family:var(--font-body);
font-size:.78rem; font-weight:600;
letter-spacing:.02em;
cursor:pointer;
transition:all var(--duration) var(--ease);
}
.btn:hover{ border-color:var(--ch-amber); color:var(--lux-ink); transform:translateY(-1px); }
.btn .icon{ width:14px; height:14px; }
.btn-primary{
background:linear-gradient(180deg, color-mix(in srgb, var(--primary-color) 100%, transparent), color-mix(in srgb, var(--primary-color) 80%, transparent));
border-color:var(--primary-color);
color:#fff;
}
.btn-primary:hover{ background:var(--primary-color); border-color:var(--primary-color); }
.btn-danger{
background:linear-gradient(180deg, color-mix(in srgb, var(--ch-coral) 100%, transparent), color-mix(in srgb, var(--ch-coral) 78%, transparent));
border-color:var(--ch-coral);
color:#fff;
}
.btn-danger:hover{ background:var(--ch-coral); }
.btn-ghost{ background:transparent; border-color:var(--lux-line); }
.btn-sm{ padding:5px 9px; font-size:.72rem; }
/* ── Pair row ── */
.ds-pair-row{ display:grid; grid-template-columns:1fr 1fr; gap:12px; }
/* ── Inline action row ── */
.inline-row{
display:flex; gap:8px; align-items:center;
}
.inline-row > input{ flex:1; }
/* ── Toggle row (hardware switch feel) ── */
.ds-toggle-row{
display:grid; grid-template-columns:1fr auto;
gap:14px; align-items:center;
padding:12px 14px;
border:1px solid color-mix(in srgb, var(--lux-line) 80%, transparent);
border-radius:var(--lux-r-md);
background:color-mix(in srgb, var(--lux-bg-1) 55%, transparent);
margin-bottom:10px;
}
.ds-toggle-row .ttl{ font-weight:600; font-size:.82rem; color:var(--lux-ink); }
.ds-toggle-row .sub{ font-size:.72rem; color:var(--lux-ink-mute); margin-top:3px; line-height:1.4; }
/* ── Switch ── */
.switch{
position:relative; width:38px; height:22px;
background:var(--lux-bg-3); border-radius:999px;
border:1px solid var(--lux-line-bold);
cursor:pointer; transition:.25s var(--ease);
flex-shrink:0;
}
.switch::after{
content:''; position:absolute; top:2px; left:2px;
width:16px; height:16px; border-radius:50%;
background:var(--lux-ink-dim);
transition:.25s var(--ease);
}
.switch.on{
background:color-mix(in srgb, var(--primary-color) 80%, transparent);
border-color:var(--primary-color);
}
.switch.on::after{ left:18px; background:#fff; }
/* ====================================================================
API KEYS table (read-only)
==================================================================== */
.api-key-list{
display:flex; flex-direction:column;
gap:4px;
margin-top:4px;
}
.api-key-row{
display:grid;
grid-template-columns:auto 1fr auto;
gap:10px; align-items:center;
padding:7px 10px;
background:var(--lux-bg-1);
border:1px solid var(--lux-line);
border-radius:var(--lux-r-sm);
font-size:.78rem;
}
.api-key-name{
font-family:var(--font-mono);
font-weight:600;
color:var(--lux-ink);
font-size:.78rem;
}
.api-key-mask{
font-family:var(--font-mono);
color:var(--lux-ink-mute);
letter-spacing:.06em;
font-size:.74rem;
}
.api-key-tag{
font-size:.62rem; font-weight:700;
padding:2px 7px; border-radius:3px;
text-transform:uppercase; letter-spacing:.08em;
background:color-mix(in srgb, var(--ch-amber) 18%, transparent);
border:1px solid color-mix(in srgb, var(--ch-amber) 35%, transparent);
color:var(--ch-amber);
}
.api-key-empty-note{
font-size:.74rem;
color:var(--lux-ink-mute);
padding:10px; text-align:center;
background:color-mix(in srgb, var(--ch-amber) 4%, var(--lux-bg-1));
border:1px dashed var(--lux-line);
border-radius:var(--lux-r-sm);
}
/* ====================================================================
NOTIFICATIONS MATRIX
==================================================================== */
.notif-matrix{
display:grid;
grid-template-columns:1fr repeat(4, 60px);
gap:0;
border:1px solid var(--lux-line);
border-radius:var(--lux-r-md);
overflow:hidden;
background:var(--lux-bg-1);
}
.notif-matrix .head{
padding:8px 10px;
font-size:.62rem; font-weight:700;
text-transform:uppercase; letter-spacing:.12em;
color:var(--lux-ink-mute);
background:color-mix(in srgb, var(--ch-violet) 6%, var(--lux-bg-2));
border-bottom:1px solid var(--lux-line);
text-align:center;
}
.notif-matrix .head:first-child{ text-align:left; }
.notif-matrix .row-label{
padding:11px 10px;
font-size:.8rem; font-weight:500;
color:var(--lux-ink);
border-bottom:1px solid var(--lux-line);
display:flex; align-items:center; gap:8px;
}
.notif-matrix .row-label .icon{
width:14px; height:14px; color:var(--ch-violet);
}
.notif-matrix .cell{
border-bottom:1px solid var(--lux-line);
border-left:1px solid var(--lux-line);
display:flex; align-items:center; justify-content:center;
cursor:pointer;
transition:all .2s var(--ease);
}
.notif-matrix .row-label:nth-last-child(5){ border-bottom:none; }
.notif-matrix .cell:nth-last-child(-n+4){ border-bottom:none; }
.notif-cell-dot{
width:14px; height:14px; border-radius:50%;
border:1.5px solid var(--lux-line-bold);
background:transparent;
transition:.2s var(--ease);
}
.notif-matrix .cell:hover .notif-cell-dot{
border-color:var(--ch-violet);
transform:scale(1.15);
}
.notif-matrix .cell.selected{
background:color-mix(in srgb, var(--ch-violet) 14%, transparent);
}
.notif-matrix .cell.selected .notif-cell-dot{
background:var(--ch-violet);
border-color:var(--ch-violet);
box-shadow:0 0 8px color-mix(in srgb, var(--ch-violet) 60%, transparent);
}
/* ====================================================================
SAVE BAR — appears when section is dirty
==================================================================== */
.save-bar{
margin:6px 0 12px;
padding:9px 12px;
display:flex; align-items:center; justify-content:space-between;
gap:10px;
background:color-mix(in srgb, var(--ch-amber) 10%, var(--lux-bg-1));
border:1px solid color-mix(in srgb, var(--ch-amber) 36%, transparent);
border-left:3px solid var(--ch-amber);
border-radius:var(--lux-r-sm);
animation:saveSlide .3s var(--ease);
}
@keyframes saveSlide{
from{ opacity:0; transform:translateY(-4px); }
to{ opacity:1; transform:translateY(0); }
}
.save-bar-msg{
display:flex; align-items:center; gap:8px;
font-size:.78rem; font-weight:500;
color:var(--lux-ink);
}
.save-bar-msg::before{
content:''; width:6px; height:6px; border-radius:50%;
background:var(--ch-amber);
box-shadow:0 0 8px var(--ch-amber);
animation:pulse 1.6s ease-in-out infinite;
}
@keyframes pulse{
0%,100%{opacity:.6}
50%{opacity:1}
}
.save-bar-actions{ display:flex; gap:6px; }
/* ====================================================================
STATUS PILL (Updates tab)
==================================================================== */
.status-card{
display:grid;
grid-template-columns:auto 1fr auto;
gap:14px;
align-items:center;
padding:12px 14px;
background:linear-gradient(180deg,
color-mix(in srgb, var(--ch-signal) 8%, var(--lux-bg-2)) 0%,
var(--lux-bg-1) 100%);
border:1px solid color-mix(in srgb, var(--ch-signal) 30%, var(--lux-line));
border-radius:var(--lux-r-md);
margin-bottom:10px;
}
.status-icon{
width:36px; height:36px; border-radius:50%;
background:color-mix(in srgb, var(--ch-signal) 16%, transparent);
display:flex; align-items:center; justify-content:center;
color:var(--ch-signal);
}
.status-icon .icon{ width:20px; height:20px; }
.status-text-main{
font-family:var(--font-display);
font-size:1.05rem; font-weight:700;
color:var(--lux-ink);
letter-spacing:.005em;
}
.status-text-sub{
font-size:.72rem; color:var(--lux-ink-mute);
font-family:var(--font-mono);
letter-spacing:.04em;
margin-top:2px;
}
.status-meta{
text-align:right;
font-size:.7rem;
color:var(--lux-ink-dim);
line-height:1.4;
}
.status-meta strong{
display:block;
color:var(--lux-ink);
font-size:.85rem;
font-family:var(--font-mono);
font-weight:600;
}
/* ====================================================================
BACKUP LIST
==================================================================== */
.backup-list{
display:flex; flex-direction:column;
gap:4px;
max-height:180px;
overflow-y:auto;
}
.backup-row{
display:grid;
grid-template-columns:1fr auto auto;
gap:10px; align-items:center;
padding:8px 10px;
background:var(--lux-bg-1);
border:1px solid var(--lux-line);
border-radius:var(--lux-r-sm);
transition:all .2s var(--ease);
}
.backup-row:hover{ border-color:var(--ch-amber); }
.backup-name{
font-family:var(--font-mono);
font-size:.74rem;
color:var(--lux-ink);
}
.backup-meta{
font-size:.7rem;
color:var(--lux-ink-mute);
font-variant-numeric:tabular-nums;
}
.icon-btn{
width:26px; height:26px;
background:transparent;
border:1px solid var(--lux-line);
border-radius:4px;
color:var(--lux-ink-dim);
display:flex; align-items:center; justify-content:center;
cursor:pointer; transition:all .2s var(--ease);
}
.icon-btn:hover{ color:var(--lux-ink); border-color:var(--ch-amber); }
.icon-btn .icon{ width:13px; height:13px; }
.icon-btn.danger:hover{ color:var(--ch-coral); border-color:var(--ch-coral); }
/* ── ABOUT panel ── */
.about-hero{
text-align:center;
padding:22px 16px 18px;
}
.about-mark{
display:inline-block;
margin-bottom:10px;
width:54px; height:54px;
border-radius:14px;
background:linear-gradient(135deg, var(--ch-amber), var(--ch-coral));
color:#000;
display:inline-flex; align-items:center; justify-content:center;
font-family:var(--font-display);
font-size:1.7rem; font-weight:900;
box-shadow:0 8px 24px color-mix(in srgb, var(--ch-amber) 40%, transparent);
}
.about-name{
font-family:var(--font-display);
font-size:1.5rem; font-weight:800;
color:var(--lux-ink);
margin-bottom:2px;
letter-spacing:.005em;
}
.about-version{
display:inline-block;
margin-top:4px;
padding:3px 11px;
border-radius:999px;
font-family:var(--font-mono); font-size:.72rem; font-weight:600;
background:color-mix(in srgb, var(--ch-amber) 12%, transparent);
color:var(--ch-amber);
border:1px solid color-mix(in srgb, var(--ch-amber) 30%, transparent);
}
.about-tag{
margin-top:10px;
font-size:.78rem;
color:var(--lux-ink-dim);
}
.about-links{
display:flex; gap:6px; justify-content:center;
flex-wrap:wrap;
margin-top:14px;
}
/* ====================================================================
responsive
==================================================================== */
@media (max-width:680px){
.settings-rail{ flex:0 0 56px; }
.rail-btn{ padding:11px 0; justify-content:center; gap:0; }
.rail-btn span:not(.icon-wrap){ display:none; }
.rail-section-label{ display:none; }
.rail-footer{ display:none; }
.ds-pair-row{ grid-template-columns:1fr; }
.notif-matrix{ grid-template-columns:1fr repeat(4, 50px); }
.modal-header h2 span:last-child{ display:none; }
}
/* ── annotation pill (mockup only) ── */
.annot{
display:inline-flex; align-items:center; gap:4px;
padding:2px 6px;
border-radius:3px;
background:color-mix(in srgb, var(--ch-cyan) 10%, transparent);
border:1px solid color-mix(in srgb, var(--ch-cyan) 30%, transparent);
color:var(--ch-cyan);
font-family:var(--font-mono);
font-size:.6rem; font-weight:600;
letter-spacing:.06em;
text-transform:uppercase;
margin-left:6px;
}
/* ====================================================================
IMPLEMENTATION STATUS — bridges the mockup below to what's now live in
production. Self-contained .impl-* namespace so it can never collide
with the modal mockup styling.
==================================================================== */
.impl-status{
width:100%;
max-width:760px;
margin:0 0 24px;
padding:18px 22px 16px;
background:linear-gradient(180deg,
color-mix(in srgb, var(--ch-signal) 6%, var(--lux-bg-1)) 0%,
color-mix(in srgb, var(--lux-bg-1) 80%, transparent) 100%);
border:1px solid color-mix(in srgb, var(--ch-signal) 22%, var(--lux-line-bold));
border-radius:var(--lux-r-md);
box-shadow:
0 0 0 1px rgba(255,255,255,.02),
0 12px 40px rgba(0,0,0,.35);
position:relative;
overflow:hidden;
}
.impl-status::before{
content:''; position:absolute; left:0; right:0; top:0; height:2px;
background:linear-gradient(90deg,
transparent 0%,
var(--ch-signal) 25%,
var(--ch-cyan) 60%,
var(--ch-amber) 80%,
transparent 100%);
box-shadow:0 0 12px color-mix(in srgb, var(--ch-signal) 50%, transparent);
}
.impl-status-head{
display:flex; align-items:center; gap:10px;
margin-bottom:12px;
padding-bottom:10px;
border-bottom:1px solid color-mix(in srgb, var(--ch-signal) 14%, var(--lux-line));
}
.impl-status-head .impl-dot{
width:8px; height:8px; border-radius:50%;
background:var(--ch-signal);
box-shadow:
0 0 9px color-mix(in srgb, var(--ch-signal) 70%, transparent),
0 0 0 1px color-mix(in srgb, var(--ch-signal) 30%, transparent);
animation:implPulse 2.4s var(--ease) infinite;
}
@keyframes implPulse{
0%,100%{ opacity:1; }
50% { opacity:.55; }
}
.impl-status-head .impl-title{
font-family:var(--font-display);
font-size:1rem; font-weight:800;
letter-spacing:.04em;
color:var(--lux-ink);
text-transform:uppercase;
}
.impl-status-head .impl-tag{
font-family:var(--font-mono);
font-size:.62rem; font-weight:600;
letter-spacing:.12em;
padding:3px 8px; border-radius:3px;
background:color-mix(in srgb, var(--ch-signal) 14%, transparent);
border:1px solid color-mix(in srgb, var(--ch-signal) 35%, transparent);
color:var(--ch-signal-dim);
text-transform:uppercase;
}
.impl-status-head .impl-spacer{ flex:1; }
.impl-status-head .impl-meta{
font-family:var(--font-mono);
font-size:.62rem;
letter-spacing:.08em;
color:var(--lux-ink-faint);
}
.impl-status-lede{
color:var(--lux-ink-dim);
font-size:.85rem;
line-height:1.55;
margin-bottom:14px;
}
.impl-status-lede strong{ color:var(--lux-ink); font-weight:600; }
.impl-status-lede code{
font-family:var(--font-mono);
font-size:.78rem;
padding:1px 6px;
background:color-mix(in srgb, var(--lux-bg-3) 70%, transparent);
border:1px solid var(--lux-line);
border-radius:3px;
color:var(--ch-amber);
}
.impl-grid{
display:grid;
grid-template-columns:repeat(auto-fit, minmax(220px, 1fr));
gap:10px;
}
.impl-card{
--imp-ch:var(--ch-signal);
position:relative;
padding:11px 12px 10px;
background:linear-gradient(180deg,
color-mix(in srgb, var(--imp-ch) 5%, var(--lux-bg-2)) 0%,
color-mix(in srgb, var(--lux-bg-1) 70%, transparent) 100%);
border:1px solid color-mix(in srgb, var(--imp-ch) 18%, var(--lux-line));
border-radius:var(--lux-r-md);
box-shadow:inset 0 1px 0 rgba(255,255,255,.025);
}
.impl-card[data-kind="gotcha"] { --imp-ch:var(--ch-coral); }
.impl-card[data-kind="mockup"] { --imp-ch:var(--ch-violet); }
.impl-card::before{
content:''; position:absolute; left:0; top:6px; bottom:6px;
width:2px; border-radius:2px;
background:linear-gradient(180deg,
color-mix(in srgb, var(--imp-ch) 80%, transparent) 0%,
transparent 100%);
}
.impl-card-head{
display:flex; align-items:center; gap:6px;
margin-bottom:7px; padding-bottom:5px;
border-bottom:1px solid color-mix(in srgb, var(--imp-ch) 14%, var(--lux-line));
line-height:1;
}
.impl-card-head .impl-card-dot{
width:5px; height:5px; border-radius:50%;
background:var(--imp-ch);
box-shadow:0 0 6px color-mix(in srgb, var(--imp-ch) 70%, transparent);
}
.impl-card-head .impl-card-title{
font-size:.65rem; font-weight:700;
text-transform:uppercase; letter-spacing:.16em;
color:var(--lux-ink); opacity:.9;
}
.impl-card-head .impl-card-marker{
margin-left:auto;
font-family:var(--font-mono);
font-size:.58rem; font-weight:600;
letter-spacing:.1em;
color:color-mix(in srgb, var(--imp-ch) 80%, var(--lux-ink-mute));
text-transform:uppercase;
}
.impl-card ul{
list-style:none;
margin:0; padding:0;
display:flex; flex-direction:column; gap:5px;
}
.impl-card li{
font-size:.76rem;
line-height:1.5;
color:var(--lux-ink-dim);
padding-left:13px;
position:relative;
}
.impl-card li::before{
content:''; position:absolute; left:0; top:.55em;
width:5px; height:1px;
background:color-mix(in srgb, var(--imp-ch) 60%, transparent);
}
.impl-card li strong{
color:var(--lux-ink); font-weight:600;
}
.impl-card code{
font-family:var(--font-mono);
font-size:.7rem;
padding:1px 5px;
background:color-mix(in srgb, var(--lux-bg-3) 65%, transparent);
border:1px solid var(--lux-line);
border-radius:3px;
color:color-mix(in srgb, var(--imp-ch) 75%, var(--lux-ink));
white-space:nowrap;
}
@media (max-width:680px){
.impl-grid{ grid-template-columns:1fr; }
.impl-status-head .impl-meta{ display:none; }
}
</style>
</head>
<body>
<div class="demo-toolbar">
<div class="demo-title">
<small>SETTINGS · REDESIGN MOCKUP</small>
Global Settings — Lumenworks rack panel
</div>
<div class="theme-toggle">
<button data-theme-set="dark" class="active">Dark</button>
<button data-theme-set="light">Light</button>
</div>
</div>
<!-- ====================================================================
IMPLEMENTATION STATUS — what shipped to production from this mockup
vs. what is still concept-only. Eats its own dog food: the panel
itself uses the rack-panel pattern it documents.
==================================================================== -->
<aside class="impl-status" aria-labelledby="impl-status-title">
<div class="impl-status-head">
<span class="impl-dot" aria-hidden="true"></span>
<span class="impl-title" id="impl-status-title">Implementation status</span>
<span class="impl-tag">v0.5.1 · LIVE</span>
<span class="impl-spacer"></span>
<span class="impl-meta">2026-04 · device-settings.html</span>
</div>
<p class="impl-status-lede">
The <code>.ds-section</code> rack-panel pattern below is <strong>live in production</strong>
on the per-device <em>General Settings</em> modal
(<code>#device-settings-modal</code>). The unified Global Settings shell
with the left rail, notification matrix, and per-section save bars is
still a concept — only the section vocabulary itself shipped first.
</p>
<div class="impl-grid">
<!-- SHIPPED ────────────────────────────────────────────────── -->
<div class="impl-card" data-kind="shipped">
<div class="impl-card-head">
<span class="impl-card-dot"></span>
<span class="impl-card-title">Shipped</span>
<span class="impl-card-marker">SIGNAL</span>
</div>
<ul>
<li><strong>Sectioned device settings</strong>
<code>templates/modals/device-settings.html</code>
now groups fields into Identity (signal) · Connection (cyan)
· Hardware (amber) · Behavior (violet).</li>
<li><strong>Auto-hide empty sections</strong> via
<code>data-ds-empty</code>, set by
<code>_updateSettingsSectionVisibility()</code> in
<code>features/devices.ts</code> after device-type field
masking runs.</li>
<li><strong>Inline DMX pair row</strong>
<code>.ds-pair-row</code> places Universe + Channel on the
same row; collapses to single column &lt;480 px.</li>
<li><strong>Sub-panel toggle</strong>
<code>.ds-toggle-row</code> wraps Auto-Restore as a recessed
rack switch instead of a flat checkbox row.</li>
<li><strong>Floating hint popover</strong> — replaces the
legacy inline <code>&lt;small&gt;</code> reveal that
pushed every field below it down on every <code>?</code>
click. New <code>toggleHint()</code> in
<code>core/ui.ts</code> anchors a single shared
<code>.hint-popover</code> via
<code>getBoundingClientRect()</code>, auto-flips
above/below, dismisses on outside click / ESC / scroll
/ resize / language change / modal close.</li>
<li><strong>i18n keys</strong> in en/ru/zh:
<code>settings.section.identity</code>,
<code>.connection</code>, <code>.hardware</code>,
<code>.behavior</code>.</li>
</ul>
</div>
<!-- GOTCHA ─────────────────────────────────────────────────── -->
<div class="impl-card" data-kind="gotcha">
<div class="impl-card-head">
<span class="impl-card-dot"></span>
<span class="impl-card-title">Gotcha — header element</span>
<span class="impl-card-marker">CORAL</span>
</div>
<ul>
<li>The section header MUST be <code>&lt;div&gt;</code>, not
<code>&lt;header&gt;</code>.</li>
<li>The bare <code>header</code> selector in
<code>layout.css:5</code> styles the app's transport bar
(sticky, grid, <code>height: 60px</code>) — every
<code>&lt;header&gt;</code> on the page inherits it,
including section headers nested inside a modal.</li>
<li>Symptom: each section header rendered as a 60 px-tall
band with the title floating at the top, no matter how
aggressive the local <code>padding</code> override was.</li>
<li>Fix: use <code>&lt;div class="ds-section-header"&gt;</code>
everywhere; or scope the offending rule to
<code>body &gt; header</code> if a future rewrite of
<code>layout.css</code> happens.</li>
</ul>
</div>
<!-- MOCKUP-ONLY ────────────────────────────────────────────── -->
<div class="impl-card" data-kind="mockup">
<div class="impl-card-head">
<span class="impl-card-dot"></span>
<span class="impl-card-title">Still mockup-only</span>
<span class="impl-card-marker">VIOLET</span>
</div>
<ul>
<li><strong>Left rail navigation</strong> — current Global
Settings still uses the top tab strip.</li>
<li><strong>Notification routing matrix</strong>
cell-grid binding event types to channels.</li>
<li><strong>Per-section save bars</strong> with dirty
indicator + revert.</li>
<li><strong>Status / Auto-check / Release-notes panels</strong>
on the Updates tab.</li>
<li><strong>Lifecycle "danger zone"</strong> for
factory-reset / wipe-and-restore flows.</li>
<li><strong><code>ds-section-meta</code> badges</strong>
(e.g. <code>2 KEYS</code>, <code>NOISE</code>) — not yet
used by any production section.</li>
</ul>
</div>
</div>
</aside>
<!-- ====================================================================
MODAL
==================================================================== -->
<div class="modal" id="settings-modal">
<div class="modal-header">
<h2>
<svg class="icon" viewBox="0 0 24 24"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/></svg>
<span>Settings</span>
</h2>
<button class="modal-close-btn" title="Close">&#x2715;</button>
</div>
<div class="settings-layout">
<!-- ────────────────── LEFT RAIL ────────────────── -->
<nav class="settings-rail">
<div class="rail-section-label">Workspace</div>
<button class="rail-btn active" data-tab="general" style="--rail-ch:var(--ch-amber)">
<svg class="icon" viewBox="0 0 24 24"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/></svg>
<span>General</span>
<span class="rail-dot"></span>
</button>
<button class="rail-btn" data-tab="backup" style="--rail-ch:var(--ch-cyan)">
<svg class="icon" viewBox="0 0 24 24"><line x1="22" x2="2" y1="12" y2="12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><line x1="6" x2="6.01" y1="16" y2="16"/><line x1="10" x2="10.01" y1="16" y2="16"/></svg>
<span>Backup</span>
<span class="rail-dot"></span>
</button>
<button class="rail-btn" data-tab="notifications" style="--rail-ch:var(--ch-violet)">
<svg class="icon" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>
<span>Notifications</span>
<span class="rail-dot"></span>
</button>
<button class="rail-btn" data-tab="appearance" style="--rail-ch:var(--ch-magenta)">
<svg class="icon" viewBox="0 0 24 24"><path d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z"/><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/></svg>
<span>Appearance</span>
<span class="rail-dot"></span>
</button>
<div class="rail-section-label">System</div>
<button class="rail-btn" data-tab="updates" style="--rail-ch:var(--ch-signal)">
<svg class="icon" viewBox="0 0 24 24"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
<span>Updates</span>
<span class="rail-badge">1</span>
</button>
<button class="rail-btn" data-tab="about" style="--rail-ch:var(--ch-amber)">
<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
<span>About</span>
<span class="rail-dot"></span>
</button>
<div class="rail-footer">v 0.5.1 · BUILD 33ec</div>
</nav>
<!-- ────────────────── PANEL BODY ────────────────── -->
<div class="settings-body">
<!-- ============== GENERAL ============== -->
<div class="settings-tab active" id="tab-general">
<section class="ds-section" data-ch="amber">
<div class="ds-section-header">
<span class="ds-section-dot"></span>
<span class="ds-section-title">Identity & API</span>
<span class="ds-section-meta">2 KEYS</span>
<span class="ds-section-index">01</span>
</div>
<div class="ds-section-body">
<div class="form-group">
<div class="label-row">
<label>API Keys</label>
<button class="hint-toggle" title="Hint">?</button>
</div>
<small class="input-hint">Defined in <code style="font-family:var(--font-mono); color:var(--ch-amber)">config.yaml</code> — restart the server after editing.</small>
<div class="api-key-list">
<div class="api-key-row">
<span class="api-key-name">dev</span>
<span class="api-key-mask">••••••••••••3a92</span>
<span class="api-key-tag">Read-only</span>
</div>
<div class="api-key-row">
<span class="api-key-name">homeassistant</span>
<span class="api-key-mask">••••••••••••f1c0</span>
<span class="api-key-tag">Read-only</span>
</div>
</div>
</div>
</div>
</section>
<section class="ds-section" data-ch="cyan">
<div class="ds-section-header">
<span class="ds-section-dot"></span>
<span class="ds-section-title">Server</span>
<span class="ds-section-index">02</span>
</div>
<div class="ds-section-body">
<div class="form-group">
<div class="label-row">
<label>External URL</label>
<button class="hint-toggle">?</button>
</div>
<input type="text" placeholder="https://myserver.example.com:8080" value="https://leds.dolgolyov-family.by">
</div>
<div class="ds-pair-row">
<div class="form-group">
<div class="label-row"><label>Log level</label></div>
<select>
<option>DEBUG</option>
<option selected>INFO</option>
<option>WARNING</option>
<option>ERROR</option>
<option>CRITICAL</option>
</select>
</div>
<div class="form-group">
<div class="label-row"><label>Shutdown action</label></div>
<select>
<option selected>Stop targets</option>
<option>Nothing</option>
</select>
</div>
</div>
<div class="save-bar" id="general-save-bar">
<div class="save-bar-msg">Unsaved changes in <strong style="margin-left:4px">External URL</strong></div>
<div class="save-bar-actions">
<button class="btn btn-sm btn-ghost">Revert</button>
<button class="btn btn-sm btn-primary">
<svg class="icon" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>
Save
</button>
</div>
</div>
</div>
</section>
<section class="ds-section" data-ch="coral">
<div class="ds-section-header">
<span class="ds-section-dot"></span>
<span class="ds-section-title">Lifecycle</span>
<span class="ds-section-meta">DESTRUCTIVE</span>
<span class="ds-section-index">03</span>
</div>
<div class="ds-section-body">
<div class="ds-toggle-row">
<div>
<div class="ttl">Server logs</div>
<div class="sub">Live tail of server log output, filterable by level. Opens in a full-screen overlay.</div>
</div>
<button class="btn">
<svg class="icon" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" x2="8" y1="13" y2="13"/><line x1="16" x2="8" y1="17" y2="17"/></svg>
Open viewer
</button>
</div>
<div class="ds-toggle-row" style="border-color:color-mix(in srgb, var(--ch-coral) 28%, var(--lux-line))">
<div>
<div class="ttl">Restart server</div>
<div class="sub">Bounce the LedGrab process. Active capture and connected devices will pause for ~3 seconds.</div>
</div>
<button class="btn btn-danger">
<svg class="icon" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9"/><polyline points="3 4 3 10 9 10"/></svg>
Restart
</button>
</div>
</div>
</section>
</div>
<!-- ============== BACKUP ============== -->
<div class="settings-tab" id="tab-backup">
<section class="ds-section" data-ch="cyan">
<div class="ds-section-header">
<span class="ds-section-dot"></span>
<span class="ds-section-title">Manual</span>
<span class="ds-section-index">01</span>
</div>
<div class="ds-section-body">
<div class="ds-toggle-row">
<div>
<div class="ttl">Download backup</div>
<div class="sub">All devices, targets, streams, templates, automations into a single <code style="font-family:var(--font-mono);color:var(--ch-cyan)">.db</code> file.</div>
</div>
<button class="btn btn-primary">
<svg class="icon" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
Download
</button>
</div>
<div class="ds-toggle-row" style="border-color:color-mix(in srgb, var(--ch-coral) 24%, var(--lux-line))">
<div>
<div class="ttl">Restore from file</div>
<div class="sub">Replaces ALL configuration with the contents of an uploaded backup. Server restarts automatically.</div>
</div>
<button class="btn btn-danger">
<svg class="icon" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>
Restore…
</button>
</div>
</div>
</section>
<section class="ds-section" data-ch="amber">
<div class="ds-section-header">
<span class="ds-section-dot"></span>
<span class="ds-section-title">Auto-backup</span>
<span class="ds-section-meta">RUNNING</span>
<span class="ds-section-index">02</span>
</div>
<div class="ds-section-body">
<div class="ds-toggle-row">
<div>
<div class="ttl">Enable auto-backup</div>
<div class="sub">Last run · 3 hr ago · next at 18:00 · 4 backups stored.</div>
</div>
<div class="switch on" data-toggle></div>
</div>
<div class="ds-pair-row">
<div class="form-group">
<div class="label-row"><label>Interval</label></div>
<select>
<option>1 h</option>
<option>6 h</option>
<option selected>12 h</option>
<option>24 h</option>
<option>7 d</option>
</select>
</div>
<div class="form-group">
<div class="label-row"><label>Max backups</label></div>
<input type="number" value="10" min="1" max="100">
</div>
</div>
<div class="inline-row" style="margin-top:6px">
<button class="btn btn-primary" style="flex:1">Save schedule</button>
<button class="btn">
<svg class="icon" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9"/><polyline points="3 4 3 10 9 10"/></svg>
Backup now
</button>
</div>
</div>
</section>
<section class="ds-section" data-ch="cyan">
<div class="ds-section-header">
<span class="ds-section-dot"></span>
<span class="ds-section-title">Saved backups</span>
<span class="ds-section-meta">4 FILES · 1.2 MB</span>
<span class="ds-section-index">03</span>
</div>
<div class="ds-section-body">
<div class="backup-list">
<div class="backup-row">
<div>
<div class="backup-name">backup-2026-04-28T15-00.db</div>
<div class="backup-meta">312 KB · 3 hr ago</div>
</div>
<button class="icon-btn" title="Download">
<svg class="icon" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
</button>
<button class="icon-btn danger" title="Delete">
<svg class="icon" viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
</div>
<div class="backup-row">
<div>
<div class="backup-name">backup-2026-04-28T03-00.db</div>
<div class="backup-meta">309 KB · 15 hr ago</div>
</div>
<button class="icon-btn"><svg class="icon" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg></button>
<button class="icon-btn danger"><svg class="icon" viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg></button>
</div>
<div class="backup-row">
<div>
<div class="backup-name">backup-2026-04-27T15-00.db</div>
<div class="backup-meta">301 KB · 1 day ago</div>
</div>
<button class="icon-btn"><svg class="icon" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg></button>
<button class="icon-btn danger"><svg class="icon" viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg></button>
</div>
<div class="backup-row">
<div>
<div class="backup-name">backup-2026-04-27T03-00.db</div>
<div class="backup-meta">298 KB · 1 day ago</div>
</div>
<button class="icon-btn"><svg class="icon" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg></button>
<button class="icon-btn danger"><svg class="icon" viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg></button>
</div>
</div>
</div>
</section>
</div>
<!-- ============== NOTIFICATIONS ============== -->
<div class="settings-tab" id="tab-notifications">
<section class="ds-section" data-ch="violet">
<div class="ds-section-header">
<span class="ds-section-dot"></span>
<span class="ds-section-title">Channels</span>
<span class="ds-section-meta">4 EVENTS</span>
<span class="ds-section-index">01</span>
</div>
<div class="ds-section-body">
<small class="input-hint" style="margin-bottom:10px">Pick the delivery channel for each device event. <strong>Snack</strong> shows in-app, <strong>OS</strong> uses system notifications, <strong>Both</strong> fires both, <strong>None</strong> silences the event.</small>
<div class="notif-matrix" id="notif-matrix">
<div class="head">Event</div>
<div class="head">Snack</div>
<div class="head">OS</div>
<div class="head">Both</div>
<div class="head">None</div>
<div class="row-label">
<svg class="icon" viewBox="0 0 24 24"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
Device came online
</div>
<div class="cell selected" data-row="0" data-col="0"><span class="notif-cell-dot"></span></div>
<div class="cell" data-row="0" data-col="1"><span class="notif-cell-dot"></span></div>
<div class="cell" data-row="0" data-col="2"><span class="notif-cell-dot"></span></div>
<div class="cell" data-row="0" data-col="3"><span class="notif-cell-dot"></span></div>
<div class="row-label">
<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
Device went offline
</div>
<div class="cell" data-row="1" data-col="0"><span class="notif-cell-dot"></span></div>
<div class="cell" data-row="1" data-col="1"><span class="notif-cell-dot"></span></div>
<div class="cell selected" data-row="1" data-col="2"><span class="notif-cell-dot"></span></div>
<div class="cell" data-row="1" data-col="3"><span class="notif-cell-dot"></span></div>
<div class="row-label">
<svg class="icon" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
New device discovered
</div>
<div class="cell selected" data-row="2" data-col="0"><span class="notif-cell-dot"></span></div>
<div class="cell" data-row="2" data-col="1"><span class="notif-cell-dot"></span></div>
<div class="cell" data-row="2" data-col="2"><span class="notif-cell-dot"></span></div>
<div class="cell" data-row="2" data-col="3"><span class="notif-cell-dot"></span></div>
<div class="row-label">
<svg class="icon" viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-6.22-8.56"/></svg>
Discovered device lost
</div>
<div class="cell" data-row="3" data-col="0"><span class="notif-cell-dot"></span></div>
<div class="cell" data-row="3" data-col="1"><span class="notif-cell-dot"></span></div>
<div class="cell" data-row="3" data-col="2"><span class="notif-cell-dot"></span></div>
<div class="cell selected" data-row="3" data-col="3"><span class="notif-cell-dot"></span></div>
</div>
</div>
</section>
<section class="ds-section" data-ch="cyan">
<div class="ds-section-header">
<span class="ds-section-dot"></span>
<span class="ds-section-title">Discovery</span>
<span class="ds-section-index">02</span>
</div>
<div class="ds-section-body">
<div class="ds-toggle-row">
<div>
<div class="ttl">Background discovery</div>
<div class="sub">Continuously scan the LAN (mDNS) and serial bus for new devices. Disable to silence "discovered/lost" events at the source.</div>
</div>
<div class="switch on" data-toggle></div>
</div>
</div>
</section>
<section class="ds-section" data-ch="amber">
<div class="ds-section-header">
<span class="ds-section-dot"></span>
<span class="ds-section-title">OS Permission</span>
<span class="ds-section-meta">GRANTED</span>
<span class="ds-section-index">03</span>
</div>
<div class="ds-section-body">
<div class="ds-toggle-row">
<div>
<div class="ttl">System notifications enabled</div>
<div class="sub">Browser permission for OS-level notifications. Granted for <code style="color:var(--ch-amber); font-family:var(--font-mono)">leds.dolgolyov-family.by</code>.</div>
</div>
<button class="btn">Send test</button>
</div>
</div>
</section>
</div>
<!-- ============== APPEARANCE ============== -->
<div class="settings-tab" id="tab-appearance">
<section class="ds-section" data-ch="magenta">
<div class="ds-section-header">
<span class="ds-section-dot"></span>
<span class="ds-section-title">Style preset</span>
<span class="ds-section-meta">DEFAULT</span>
<span class="ds-section-index">01</span>
</div>
<div class="ds-section-body">
<small class="input-hint">Reuses the existing <code style="color:var(--ch-magenta); font-family:var(--font-mono)">.ap-card</code> grid — no change needed beyond wrapping it in a <code style="color:var(--ch-magenta); font-family:var(--font-mono)">.ds-section</code>.</small>
<div style="display:grid; grid-template-columns:repeat(4, 1fr); gap:8px; margin-top:6px">
<div style="aspect-ratio:1; background:linear-gradient(135deg,#000,#1a1a1a); border:2px solid var(--ch-magenta); border-radius:8px; display:flex; align-items:flex-end; justify-content:center; padding:8px; color:var(--lux-ink); font-size:.7rem">Default</div>
<div style="aspect-ratio:1; background:linear-gradient(135deg,#1d2435,#0f1320); border:1px solid var(--lux-line); border-radius:8px; display:flex; align-items:flex-end; justify-content:center; padding:8px; color:var(--lux-ink-dim); font-size:.7rem">Aurora</div>
<div style="aspect-ratio:1; background:linear-gradient(135deg,#fff,#eee); border:1px solid var(--lux-line); border-radius:8px; display:flex; align-items:flex-end; justify-content:center; padding:8px; color:#333; font-size:.7rem">Daylight</div>
<div style="aspect-ratio:1; background:linear-gradient(135deg,#2a1810,#180c08); border:1px solid var(--lux-line); border-radius:8px; display:flex; align-items:flex-end; justify-content:center; padding:8px; color:var(--lux-ink-dim); font-size:.7rem">Ember</div>
</div>
</div>
</section>
<section class="ds-section" data-ch="cyan">
<div class="ds-section-header">
<span class="ds-section-dot"></span>
<span class="ds-section-title">Background effect</span>
<span class="ds-section-meta">NOISE</span>
<span class="ds-section-index">02</span>
</div>
<div class="ds-section-body">
<div style="display:grid; grid-template-columns:repeat(4, 1fr); gap:8px">
<div style="aspect-ratio:1; background:#000; border:1px solid var(--lux-line); border-radius:8px; display:flex; align-items:flex-end; justify-content:center; padding:8px; color:var(--lux-ink-dim); font-size:.7rem">None</div>
<div style="aspect-ratio:1; background:radial-gradient(circle at 30% 30%, #1a1a1a, #000); border:2px solid var(--ch-cyan); border-radius:8px; display:flex; align-items:flex-end; justify-content:center; padding:8px; color:var(--lux-ink); font-size:.7rem">Noise</div>
<div style="aspect-ratio:1; background:linear-gradient(45deg,#082c4f,#000,#5a127a); border:1px solid var(--lux-line); border-radius:8px; display:flex; align-items:flex-end; justify-content:center; padding:8px; color:var(--lux-ink-dim); font-size:.7rem">Aurora</div>
<div style="aspect-ratio:1; background:radial-gradient(circle at 50% 50%, #1d3554, #000); border:1px solid var(--lux-line); border-radius:8px; display:flex; align-items:flex-end; justify-content:center; padding:8px; color:var(--lux-ink-dim); font-size:.7rem">Stars</div>
</div>
</div>
</section>
</div>
<!-- ============== UPDATES ============== -->
<div class="settings-tab" id="tab-updates">
<section class="ds-section" data-ch="signal" style="--ds-ch:var(--ch-signal)">
<div class="ds-section-header">
<span class="ds-section-dot"></span>
<span class="ds-section-title">Status</span>
<span class="ds-section-meta">UPDATE AVAILABLE</span>
<span class="ds-section-index">01</span>
</div>
<div class="ds-section-body">
<div class="status-card">
<div class="status-icon">
<svg class="icon" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
</div>
<div>
<div class="status-text-main">v0.6.0 ready to install</div>
<div class="status-text-sub">Channel: stable · 28.4 MB · checked 5 min ago</div>
</div>
<button class="btn btn-primary">Update now</button>
</div>
<div class="ds-toggle-row" style="margin-top:6px">
<div>
<div class="ttl">View release notes</div>
<div class="sub">What's new in v0.6.0 — major UI redesign, device event notifications, auto-backup history.</div>
</div>
<button class="btn">
<svg class="icon" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
Open
</button>
</div>
</div>
</section>
<section class="ds-section" data-ch="amber">
<div class="ds-section-header">
<span class="ds-section-dot"></span>
<span class="ds-section-title">Auto-check</span>
<span class="ds-section-index">02</span>
</div>
<div class="ds-section-body">
<div class="ds-toggle-row">
<div>
<div class="ttl">Check periodically</div>
<div class="sub">Background polls the release feed at the chosen interval.</div>
</div>
<div class="switch on" data-toggle></div>
</div>
<div class="ds-pair-row">
<div class="form-group">
<div class="label-row"><label>Interval</label></div>
<select>
<option>1 h</option>
<option>6 h</option>
<option selected>12 h</option>
<option>24 h</option>
</select>
</div>
<div class="form-group">
<div class="label-row"><label>Channel</label></div>
<select>
<option selected>Stable</option>
<option>Pre-release</option>
</select>
</div>
</div>
</div>
</section>
</div>
<!-- ============== ABOUT ============== -->
<div class="settings-tab" id="tab-about">
<section class="ds-section" data-ch="amber">
<div class="ds-section-body">
<div class="about-hero">
<div class="about-mark">L</div>
<div class="about-name">LedGrab</div>
<div class="about-version">v 0.5.1 · build 33ec</div>
<div class="about-tag">Studio-grade ambient lighting · made with care.</div>
<div class="about-links">
<button class="btn"><svg class="icon" viewBox="0 0 24 24"><path d="M9 18c-4.51 2-5-2-7-2"/><path d="M15 22v-4a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 19 5.77 5.07 5.07 0 0 0 18.91 2S17.73 1.65 15 3.48a13.38 13.38 0 0 0-7 0C5.27 1.65 4.09 2 4.09 2A5.07 5.07 0 0 0 4 5.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 8 19.13V22"/></svg> GitHub</button>
<button class="btn"><svg class="icon" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg> Docs</button>
<button class="btn"><svg class="icon" viewBox="0 0 24 24"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg> Discord</button>
</div>
</div>
</div>
</section>
</div>
</div>
</div>
</div>
<script>
// ──── Tab switching ────
document.querySelectorAll('.rail-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.rail-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const tab = btn.dataset.tab;
document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
document.getElementById('tab-' + tab).classList.add('active');
// Switch modal stripe color to match tab channel
const railCh = btn.style.getPropertyValue('--rail-ch') || 'var(--ch-amber)';
document.getElementById('settings-modal').style.setProperty('--modal-ch', railCh);
});
});
// ──── Switch toggles ────
document.querySelectorAll('[data-toggle]').forEach(s => {
s.addEventListener('click', () => s.classList.toggle('on'));
});
// ──── Notifications matrix ────
document.querySelectorAll('#notif-matrix .cell').forEach(cell => {
cell.addEventListener('click', () => {
const row = cell.dataset.row;
document.querySelectorAll(`#notif-matrix .cell[data-row="${row}"]`).forEach(c => c.classList.remove('selected'));
cell.classList.add('selected');
});
});
// ──── Theme toggle ────
document.querySelectorAll('.theme-toggle button').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('.theme-toggle button').forEach(x => x.classList.remove('active'));
b.classList.add('active');
document.documentElement.setAttribute('data-theme', b.dataset.themeSet);
});
});
</script>
</body>
</html>