9d4a534ec6
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.
1752 lines
74 KiB
HTML
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 <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><small></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><div></code>, not
|
|
<code><header></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><header></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><div class="ds-section-header"></code>
|
|
everywhere; or scope the offending rule to
|
|
<code>body > 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">✕</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>
|