49ddabbc36
A user-chosen icon ("mouse", "motherboard", "keyboard"…) renders as a
44x44 instrument-panel face plate at the leading edge of .mod-head on
device cards. Optional per-card; null hides the plate and reverts to
the existing badge-only head.
- Storage/schema: new icon, icon_color fields on Device + DeviceUpdate /
DeviceResponse. SQLite stores entities as a JSON blob, so no migration
is needed; from_dict defaults handle existing rows.
- Curated 47-icon library across six categories (Hardware / Lighting /
Rooms / Media / Signal / Ambience), reusing the existing Lucide path
module; adds circuit-board, bed, armchair, leaf paths.
- mod-card.ts: ModHeadOpts gains iconHtml / iconColor / iconAttrs;
ModMenuItemOpts gains optional dataAttrs. The plate is rendered when
iconHtml is supplied; otherwise no layout change.
- Picker modal (icon-picker.html + features/icon-picker.ts): live
preview, search, six category tabs, recent strip, channel-color
override toggle. Wired through document-level click delegation on
[data-icon-picker-trigger="<deviceId>"] — no window globals, no
inline onclick string. Sets the precedent for migrating other card
actions off window in a follow-up.
- en/ru/zh locales for picker UI + categories.
Includes a docs/ mockup that's the source-of-truth for the design.
1604 lines
83 KiB
HTML
1604 lines
83 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 · Custom Card Icon · Proposal</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>
|
||
/* =====================================================================
|
||
CUSTOM CARD ICON — proposal layered onto the existing .module / .mod-*
|
||
instrument-panel language. A square "icon plate" sits at the leading
|
||
edge of the head row, tinted by the channel accent. Click to open the
|
||
picker; behaviour falls through transparently for cards that don't set
|
||
an icon (current default).
|
||
===================================================================== */
|
||
|
||
:root{
|
||
--primary-color:#4CAF50;
|
||
--primary-text:#66bb6a;
|
||
--primary-contrast:#fff;
|
||
|
||
--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-hairline:1px;
|
||
|
||
--ch-signal:#4CAF50;
|
||
--ch-cyan:#00d8ff;
|
||
--ch-magenta:#ff4ade;
|
||
--ch-amber:#ffb800;
|
||
--ch-coral:#ff5e5e;
|
||
--ch-violet:#8b7eff;
|
||
|
||
--duration:.22s;
|
||
--ease:cubic-bezier(.16,1,.3,1);
|
||
}
|
||
|
||
[data-theme="dark"]{
|
||
--bg-page:#000;
|
||
--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;
|
||
--shadow-rack:0 1px 0 rgba(255,255,255,.03), 0 8px 24px rgba(0,0,0,.5);
|
||
color-scheme:dark;
|
||
}
|
||
[data-theme="light"]{
|
||
--bg-page:#f0f1f4;
|
||
--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;
|
||
--shadow-rack:0 1px 0 rgba(255,255,255,.6), 0 6px 18px rgba(15,20,25,.08);
|
||
--primary-color:#2e7d32;
|
||
--ch-signal:#2e7d32;
|
||
--ch-cyan:#006b88;
|
||
--ch-magenta:#b01a99;
|
||
--ch-amber:#a56a00;
|
||
--ch-coral:#d8392e;
|
||
--ch-violet:#5b4fd0;
|
||
color-scheme:light;
|
||
}
|
||
|
||
*{box-sizing:border-box;margin:0;padding:0;}
|
||
html,body{
|
||
background:var(--bg-page);color:var(--lux-ink);
|
||
font-family:var(--font-body);min-height:100vh;
|
||
}
|
||
body{
|
||
padding:48px 32px 80px;-webkit-font-smoothing:antialiased;
|
||
background-image:
|
||
radial-gradient(circle at 88% -10%, color-mix(in srgb, var(--ch-signal) 9%, transparent), transparent 45%),
|
||
radial-gradient(circle at -10% 30%, color-mix(in srgb, var(--ch-violet) 7%, transparent), transparent 50%),
|
||
linear-gradient(var(--lux-line) 1px, transparent 1px),
|
||
linear-gradient(90deg, var(--lux-line) 1px, transparent 1px);
|
||
background-size: auto, auto, 80px 80px, 80px 80px;
|
||
background-blend-mode: normal, normal, multiply, multiply;
|
||
}
|
||
[data-theme="dark"] body::before{
|
||
content:'';position:fixed;inset:0;pointer-events:none;z-index:0;
|
||
background:radial-gradient(ellipse 80% 60% at 50% -10%, rgba(76,175,80,.06), transparent);
|
||
}
|
||
|
||
.page{max-width:1360px;margin:0 auto;position:relative;z-index:1;}
|
||
|
||
/* =================== Page chrome =================== */
|
||
.page-head{
|
||
display:flex;align-items:flex-end;justify-content:space-between;gap:24px;
|
||
padding-bottom:28px;margin-bottom:36px;
|
||
border-bottom:1px solid var(--lux-line);
|
||
position:relative;
|
||
}
|
||
.page-head::after{
|
||
content:'';position:absolute;left:0;bottom:-1px;width:160px;height:2px;
|
||
background:linear-gradient(90deg,var(--ch-signal),transparent);
|
||
}
|
||
.eyebrow{
|
||
font-family:var(--font-mono);font-size:.7rem;letter-spacing:.32em;
|
||
text-transform:uppercase;color:var(--lux-ink-mute);margin-bottom:10px;
|
||
display:flex;align-items:center;gap:10px;
|
||
}
|
||
.eyebrow::before{
|
||
content:'';width:6px;height:6px;border-radius:50%;background:var(--ch-signal);
|
||
box-shadow:0 0 0 3px color-mix(in srgb, var(--ch-signal) 20%, transparent);
|
||
animation:pulse 2s ease-in-out infinite;
|
||
}
|
||
@keyframes pulse{50%{box-shadow:0 0 0 6px color-mix(in srgb, var(--ch-signal) 0%, transparent);}}
|
||
h1{
|
||
font-family:var(--font-display);
|
||
font-size:clamp(2.4rem,5vw,3.8rem);font-weight:900;
|
||
letter-spacing:-.02em;line-height:.92;color:var(--lux-ink);
|
||
}
|
||
h1 em{font-style:normal;color:var(--ch-signal);}
|
||
.lede{
|
||
color:var(--lux-ink-dim);max-width:58ch;font-size:.95rem;line-height:1.55;
|
||
margin-top:14px;
|
||
}
|
||
.lede code{
|
||
font-family:var(--font-mono);font-size:.78rem;
|
||
background:var(--lux-bg-0);border:1px solid var(--lux-line);
|
||
padding:1px 5px;border-radius:3px;color:var(--ch-cyan);
|
||
}
|
||
.theme-toggle{
|
||
display:inline-flex;align-items:center;gap:0;font-family:var(--font-mono);
|
||
font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
|
||
border:1px solid var(--lux-line-bold);border-radius:999px;
|
||
background:var(--lux-bg-1);overflow:hidden;
|
||
}
|
||
.theme-toggle button{
|
||
appearance:none;background:none;border:none;color:var(--lux-ink-mute);
|
||
padding:8px 16px;cursor:pointer;font:inherit;letter-spacing:inherit;
|
||
transition:color .2s,background .2s;
|
||
}
|
||
.theme-toggle button.is-active{background:var(--lux-ink);color:var(--lux-bg-1);}
|
||
|
||
/* =================== Section heading =================== */
|
||
.section-title{
|
||
font-family:var(--font-mono);font-size:.72rem;letter-spacing:.3em;
|
||
text-transform:uppercase;color:var(--lux-ink-mute);
|
||
margin:64px 0 22px;display:flex;align-items:center;gap:14px;
|
||
}
|
||
.section-title::after{content:'';flex:1;height:1px;background:var(--lux-line);}
|
||
.section-title .num{
|
||
font-family:var(--font-display);font-size:1.6rem;font-weight:800;
|
||
color:var(--lux-ink);letter-spacing:0;
|
||
border-right:1px solid var(--lux-line);padding-right:14px;
|
||
}
|
||
.tag{
|
||
display:inline-flex;align-items:center;gap:4px;
|
||
font-family:var(--font-mono);font-size:.6rem;font-weight:600;
|
||
letter-spacing:.16em;text-transform:uppercase;
|
||
color:var(--ch-signal);
|
||
border:1px solid color-mix(in srgb, var(--ch-signal) 30%, transparent);
|
||
background:color-mix(in srgb, var(--ch-signal) 10%, transparent);
|
||
padding:2px 7px;border-radius:3px;
|
||
}
|
||
|
||
/* =================== Module / card vocabulary (verbatim from project) === */
|
||
.grid{
|
||
display:grid;
|
||
grid-template-columns:repeat(auto-fill,minmax(min(380px,100%),1fr));
|
||
gap:14px;
|
||
}
|
||
.module{
|
||
--ch:var(--ch-signal);
|
||
position:relative;overflow:hidden;
|
||
display:flex;flex-direction:column;gap:14px;
|
||
padding:16px 18px 14px 22px;
|
||
background:var(--lux-bg-1);
|
||
border:var(--lux-hairline) solid var(--lux-line);
|
||
border-radius:var(--lux-r-md);
|
||
transition:box-shadow .2s ease, transform .2s ease, border-color .2s ease;
|
||
}
|
||
.module::before{
|
||
content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
|
||
background:var(--ch);opacity:.6;
|
||
box-shadow:0 0 8px color-mix(in srgb, var(--ch) 40%, transparent);
|
||
transition:opacity .2s ease, box-shadow .2s ease, width .2s ease;
|
||
}
|
||
.module::after{
|
||
content:'';position:absolute;top:8px;right:8px;width:12px;height:12px;
|
||
border-top:var(--lux-hairline) solid var(--lux-line-bold);
|
||
border-right:var(--lux-hairline) solid var(--lux-line-bold);
|
||
pointer-events:none;opacity:.7;
|
||
}
|
||
.module:hover{
|
||
box-shadow:var(--shadow-rack);
|
||
border-color:var(--lux-line-bold);
|
||
transform:translateY(-1px);
|
||
}
|
||
.module:hover::before{
|
||
opacity:1;
|
||
box-shadow:0 0 14px color-mix(in srgb, var(--ch) 70%, transparent);
|
||
}
|
||
.module.is-running{
|
||
border-color:color-mix(in srgb, var(--ch) 32%, var(--lux-line));
|
||
box-shadow:0 0 0 1px color-mix(in srgb, var(--ch) 18%, transparent),
|
||
0 6px 20px rgba(0,0,0,.25);
|
||
}
|
||
.module.is-running::before{
|
||
opacity:1;width:4px;
|
||
box-shadow:0 0 14px color-mix(in srgb, var(--ch) 70%, transparent),
|
||
0 0 4px color-mix(in srgb, var(--ch) 90%, transparent);
|
||
}
|
||
|
||
.module[data-ch="signal"] {--ch:var(--ch-signal);}
|
||
.module[data-ch="cyan"] {--ch:var(--ch-cyan);}
|
||
.module[data-ch="magenta"] {--ch:var(--ch-magenta);}
|
||
.module[data-ch="amber"] {--ch:var(--ch-amber);}
|
||
.module[data-ch="coral"] {--ch:var(--ch-coral);}
|
||
.module[data-ch="violet"] {--ch:var(--ch-violet);}
|
||
|
||
.mod-head{display:flex;align-items:flex-start;gap:12px;}
|
||
.mod-id{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1;}
|
||
.mod-badge{
|
||
display:inline-flex;align-items:center;gap:6px;align-self:flex-start;
|
||
font-family:var(--font-mono);font-size:.55rem;font-weight:600;
|
||
letter-spacing:.2em;text-transform:uppercase;color:var(--ch);
|
||
padding:2px 6px;
|
||
border:var(--lux-hairline) solid color-mix(in srgb, var(--ch) 35%, var(--lux-line));
|
||
border-radius:3px;
|
||
background:color-mix(in srgb, var(--ch) 8%, transparent);
|
||
line-height:1.4;white-space:nowrap;
|
||
}
|
||
.mod-name{
|
||
font-family:var(--font-body);
|
||
font-size:1.05rem;font-weight:700;letter-spacing:-.01em;
|
||
color:var(--lux-ink);
|
||
display:flex;align-items:center;gap:6px;
|
||
min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
||
}
|
||
.mod-meta{
|
||
font-family:var(--font-mono);font-size:.66rem;letter-spacing:.06em;
|
||
color:var(--lux-ink-mute);
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
}
|
||
.mod-leds{
|
||
display:flex;align-items:center;gap:4px;padding:5px 7px;
|
||
background:var(--lux-bg-0);
|
||
border:var(--lux-hairline) solid var(--lux-line);
|
||
border-radius:var(--lux-r-sm);flex-shrink:0;
|
||
}
|
||
.mod-leds .led{
|
||
width:6px;height:6px;border-radius:50%;
|
||
background:var(--lux-ink-faint);
|
||
box-shadow:inset 0 0 0 1px rgba(0,0,0,.5);
|
||
}
|
||
.mod-leds .led.on{
|
||
background:var(--ch);
|
||
box-shadow:0 0 6px color-mix(in srgb, var(--ch) 80%, transparent);
|
||
}
|
||
.mod-leds .led.blink{animation:ledBlink 1.2s ease-in-out infinite;}
|
||
.mod-leds .led.blink:nth-child(2){animation-delay:.2s;}
|
||
.mod-leds .led.blink:nth-child(3){animation-delay:.4s;}
|
||
@keyframes ledBlink{0%,100%{opacity:1;}50%{opacity:.28;}}
|
||
.mod-leds .led.fault{
|
||
background:var(--ch-coral);
|
||
box-shadow:0 0 8px color-mix(in srgb, var(--ch-coral) 70%, transparent);
|
||
}
|
||
|
||
.mod-metrics{
|
||
display:grid;grid-template-columns:1.2fr 1fr 1fr;
|
||
background:var(--lux-bg-0);
|
||
border:var(--lux-hairline) solid var(--lux-line);
|
||
border-radius:var(--lux-r-sm);overflow:hidden;
|
||
}
|
||
.mod-metric{
|
||
padding:9px 12px 10px;
|
||
border-right:var(--lux-hairline) solid var(--lux-line);
|
||
display:flex;flex-direction:column;gap:3px;min-width:0;
|
||
}
|
||
.mod-metric:last-child{border-right:none;}
|
||
.mod-metric .k{
|
||
font-family:var(--font-mono);font-size:.55rem;font-weight:600;
|
||
letter-spacing:.18em;text-transform:uppercase;
|
||
color:var(--lux-ink-mute);
|
||
}
|
||
.mod-metric .v{
|
||
font-family:var(--font-display);
|
||
font-size:1.95rem;font-weight:800;line-height:1;color:var(--lux-ink);
|
||
font-variant-numeric:tabular-nums;letter-spacing:-.01em;
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
}
|
||
.mod-metric .v.signal{color:var(--ch);}
|
||
.mod-metric .v small{
|
||
font-family:var(--font-mono);font-size:.65rem;font-weight:500;
|
||
color:var(--lux-ink-mute);letter-spacing:.08em;margin-left:3px;
|
||
}
|
||
|
||
.mod-fader{
|
||
display:flex;align-items:center;gap:10px;padding:7px 10px;
|
||
background:var(--lux-bg-0);
|
||
border:var(--lux-hairline) solid var(--lux-line);
|
||
border-radius:var(--lux-r-sm);
|
||
}
|
||
.mod-fader__k{
|
||
font-family:var(--font-mono);font-size:.55rem;
|
||
letter-spacing:.2em;text-transform:uppercase;
|
||
color:var(--lux-ink-mute);min-width:42px;
|
||
}
|
||
.mod-fader__track{
|
||
flex:1;position:relative;height:5px;border-radius:99px;
|
||
background:color-mix(in srgb, var(--ch) 12%, var(--lux-bg-3));
|
||
overflow:hidden;box-shadow:inset 0 1px 2px rgba(0,0,0,.4);
|
||
}
|
||
.mod-fader__fill{
|
||
position:absolute;left:0;top:0;bottom:0;
|
||
background:linear-gradient(90deg,
|
||
color-mix(in srgb, var(--ch) 50%, transparent), var(--ch));
|
||
box-shadow:0 0 8px color-mix(in srgb, var(--ch) 60%, transparent);
|
||
}
|
||
.mod-fader__v{
|
||
font-family:var(--font-mono);font-size:.78rem;font-weight:700;
|
||
color:var(--lux-ink);min-width:34px;text-align:right;
|
||
font-variant-numeric:tabular-nums;
|
||
}
|
||
|
||
.mod-foot{display:flex;align-items:center;gap:8px;padding-top:2px;}
|
||
.mod-patch{
|
||
display:flex;align-items:center;gap:6px;
|
||
font-family:var(--font-mono);font-size:.6rem;font-weight:600;
|
||
letter-spacing:.1em;text-transform:uppercase;
|
||
color:var(--lux-ink-mute);margin-right:auto;min-width:0;
|
||
}
|
||
.mod-patch .patch-dot{
|
||
width:8px;height:8px;border-radius:50%;
|
||
background:var(--lux-bg-0);
|
||
border:var(--lux-hairline) solid color-mix(in srgb, var(--ch) 55%, var(--lux-line-bold));
|
||
flex-shrink:0;position:relative;
|
||
}
|
||
.mod-patch .patch-dot.is-live::after{
|
||
content:'';position:absolute;inset:1px;border-radius:50%;
|
||
background:var(--ch);box-shadow:0 0 6px var(--ch);
|
||
animation:pulse 2s ease-in-out infinite;
|
||
}
|
||
|
||
.mod-btn{
|
||
display:inline-flex;align-items:center;justify-content:center;gap:6px;
|
||
font-family:var(--font-mono);font-size:.7rem;font-weight:600;
|
||
letter-spacing:.08em;text-transform:uppercase;
|
||
padding:7px 14px;flex:0 0 auto;
|
||
border:var(--lux-hairline) solid var(--lux-line-bold);
|
||
border-radius:var(--lux-r-sm);
|
||
background:var(--lux-bg-2);color:var(--lux-ink-dim);
|
||
cursor:pointer;transition:all .15s ease;
|
||
}
|
||
.mod-btn:hover{
|
||
color:var(--lux-ink);
|
||
border-color:color-mix(in srgb, var(--ch) 40%, var(--lux-line-bold));
|
||
background:var(--lux-bg-3);
|
||
}
|
||
.mod-btn-stop{
|
||
color:var(--ch-coral);
|
||
border-color:color-mix(in srgb, var(--ch-coral) 40%, transparent);
|
||
}
|
||
.mod-btn-go{
|
||
background:var(--ch);color:#fff;border-color:var(--ch);
|
||
box-shadow:0 0 14px color-mix(in srgb, var(--ch) 35%, transparent);
|
||
}
|
||
.mod-btn svg{width:12px;height:12px;}
|
||
.mod-btn-icon{padding:7px 9px;}
|
||
.mod-btn-icon svg{width:14px;height:14px;}
|
||
|
||
/* =====================================================================
|
||
THE PROPOSAL — .mod-icon (the icon plate)
|
||
A 52x52 panel-mount tile that leads the head row. Channel-tinted.
|
||
Click → opens picker. Empty → renders a placeholder "+" plate so the
|
||
slot stays visually consistent (and discoverable).
|
||
===================================================================== */
|
||
.mod-head--with-icon{align-items:stretch;}
|
||
.mod-icon{
|
||
--plate-size:52px;
|
||
flex:0 0 var(--plate-size);
|
||
width:var(--plate-size);height:var(--plate-size);
|
||
position:relative;
|
||
display:inline-flex;align-items:center;justify-content:center;
|
||
background:linear-gradient(180deg,
|
||
color-mix(in srgb, var(--ch) 10%, var(--lux-bg-0)) 0%,
|
||
var(--lux-bg-0) 100%);
|
||
border:var(--lux-hairline) solid color-mix(in srgb, var(--ch) 28%, var(--lux-line));
|
||
border-radius:var(--lux-r-sm);
|
||
color:var(--ch);
|
||
cursor:pointer;
|
||
transition:transform .18s var(--ease),
|
||
border-color .18s var(--ease),
|
||
box-shadow .18s var(--ease),
|
||
background .18s var(--ease);
|
||
box-shadow:
|
||
inset 0 1px 0 color-mix(in srgb, var(--ch) 14%, transparent),
|
||
inset 0 -8px 14px color-mix(in srgb, var(--ch) 6%, transparent),
|
||
0 0 0 0 color-mix(in srgb, var(--ch) 35%, transparent);
|
||
overflow:hidden;
|
||
isolation:isolate;
|
||
}
|
||
/* Inner corner-bracket detail: ties to the card's silkscreen language */
|
||
.mod-icon::before{
|
||
content:'';position:absolute;top:4px;right:4px;width:7px;height:7px;
|
||
border-top:1px solid color-mix(in srgb, var(--ch) 55%, var(--lux-line));
|
||
border-right:1px solid color-mix(in srgb, var(--ch) 55%, var(--lux-line));
|
||
opacity:.7;pointer-events:none;
|
||
}
|
||
/* Subtle scanline texture so the plate reads as instrument-grade, not flat */
|
||
.mod-icon::after{
|
||
content:'';position:absolute;inset:0;pointer-events:none;
|
||
background:repeating-linear-gradient(180deg,
|
||
rgba(255,255,255,.02) 0 1px,
|
||
transparent 1px 3px);
|
||
mix-blend-mode:overlay;opacity:.5;
|
||
}
|
||
.mod-icon svg{
|
||
width:28px;height:28px;
|
||
stroke:currentColor;stroke-width:1.6;
|
||
fill:none;stroke-linecap:round;stroke-linejoin:round;
|
||
filter:drop-shadow(0 0 6px color-mix(in srgb, var(--ch) 35%, transparent));
|
||
z-index:1;transition:transform .25s var(--ease);
|
||
}
|
||
.mod-icon:hover{
|
||
transform:translateY(-1px);
|
||
border-color:color-mix(in srgb, var(--ch) 60%, var(--lux-line));
|
||
box-shadow:
|
||
inset 0 1px 0 color-mix(in srgb, var(--ch) 24%, transparent),
|
||
inset 0 -10px 18px color-mix(in srgb, var(--ch) 10%, transparent),
|
||
0 0 0 3px color-mix(in srgb, var(--ch) 18%, transparent),
|
||
0 6px 16px color-mix(in srgb, var(--ch) 22%, transparent);
|
||
}
|
||
.mod-icon:hover svg{transform:scale(1.05);}
|
||
.mod-icon:focus-visible{
|
||
outline:none;
|
||
box-shadow:0 0 0 2px color-mix(in srgb, var(--ch) 60%, transparent);
|
||
}
|
||
|
||
/* "Edit" pencil hint — appears on hover, sits inside the bottom-right */
|
||
.mod-icon__edit{
|
||
position:absolute;bottom:3px;right:3px;
|
||
width:14px;height:14px;border-radius:2px;
|
||
background:color-mix(in srgb, var(--ch) 18%, var(--lux-bg-0));
|
||
border:1px solid color-mix(in srgb, var(--ch) 50%, transparent);
|
||
display:flex;align-items:center;justify-content:center;
|
||
color:var(--ch);opacity:0;transform:translateY(2px);
|
||
transition:opacity .15s, transform .15s;pointer-events:none;
|
||
}
|
||
.mod-icon__edit svg{
|
||
width:9px;height:9px;stroke-width:2;filter:none;
|
||
}
|
||
.mod-icon:hover .mod-icon__edit,
|
||
.mod-icon:focus-visible .mod-icon__edit{
|
||
opacity:1;transform:translateY(0);
|
||
}
|
||
|
||
/* Running cards: the plate "breathes" in sync with the live indicator */
|
||
.module.is-running .mod-icon{
|
||
animation:platePulse 2.6s ease-in-out infinite;
|
||
}
|
||
@keyframes platePulse{
|
||
0%,100%{
|
||
box-shadow:
|
||
inset 0 1px 0 color-mix(in srgb, var(--ch) 14%, transparent),
|
||
inset 0 -8px 14px color-mix(in srgb, var(--ch) 6%, transparent),
|
||
0 0 0 0 color-mix(in srgb, var(--ch) 35%, transparent);
|
||
}
|
||
50%{
|
||
box-shadow:
|
||
inset 0 1px 0 color-mix(in srgb, var(--ch) 22%, transparent),
|
||
inset 0 -8px 18px color-mix(in srgb, var(--ch) 12%, transparent),
|
||
0 0 0 4px color-mix(in srgb, var(--ch) 12%, transparent),
|
||
0 0 16px color-mix(in srgb, var(--ch) 30%, transparent);
|
||
}
|
||
}
|
||
|
||
/* Offline / fault states */
|
||
.module[data-state="offline"] .mod-icon{
|
||
color:var(--lux-ink-mute);
|
||
border-color:var(--lux-line);
|
||
background:var(--lux-bg-0);
|
||
}
|
||
.module[data-state="offline"] .mod-icon svg{
|
||
filter:none;opacity:.55;
|
||
}
|
||
.module[data-state="fault"] .mod-icon{
|
||
color:var(--ch-coral);
|
||
border-color:color-mix(in srgb, var(--ch-coral) 45%, var(--lux-line));
|
||
background:linear-gradient(180deg,
|
||
color-mix(in srgb, var(--ch-coral) 14%, var(--lux-bg-0)) 0%,
|
||
var(--lux-bg-0) 100%);
|
||
}
|
||
|
||
/* Empty / placeholder plate — kept visible so the slot is discoverable.
|
||
Styled as a dashed outline with a quiet "+" glyph. */
|
||
.mod-icon.is-empty{
|
||
background:transparent;
|
||
border-style:dashed;
|
||
border-color:var(--lux-line-bold);
|
||
color:var(--lux-ink-mute);
|
||
box-shadow:none;
|
||
}
|
||
.mod-icon.is-empty::before{display:none;}
|
||
.mod-icon.is-empty::after{display:none;}
|
||
.mod-icon.is-empty svg{filter:none;opacity:.7;width:18px;height:18px;}
|
||
.mod-icon.is-empty:hover{
|
||
border-color:color-mix(in srgb, var(--ch) 60%, var(--lux-line-bold));
|
||
color:var(--ch);
|
||
background:color-mix(in srgb, var(--ch) 5%, transparent);
|
||
}
|
||
|
||
/* =====================================================================
|
||
Anatomy section — exploded view of the plate with annotations
|
||
===================================================================== */
|
||
.anatomy{
|
||
display:grid;
|
||
grid-template-columns:minmax(280px,420px) 1fr;
|
||
gap:32px;align-items:center;
|
||
padding:36px 32px;
|
||
background:linear-gradient(180deg,var(--lux-bg-1),var(--lux-bg-0) 140%);
|
||
border:1px solid var(--lux-line);
|
||
border-radius:var(--lux-r-md);
|
||
position:relative;overflow:hidden;
|
||
}
|
||
.anatomy::before{
|
||
content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
|
||
background:linear-gradient(180deg,var(--ch-cyan),var(--ch-violet));
|
||
opacity:.7;
|
||
}
|
||
.anatomy-stage{
|
||
display:flex;justify-content:center;align-items:center;
|
||
aspect-ratio:1;
|
||
background:
|
||
radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--ch-signal) 14%, transparent), transparent 60%),
|
||
repeating-linear-gradient(0deg, var(--lux-line) 0 1px, transparent 1px 28px),
|
||
repeating-linear-gradient(90deg, var(--lux-line) 0 1px, transparent 1px 28px),
|
||
var(--lux-bg-0);
|
||
border:1px solid var(--lux-line);
|
||
border-radius:var(--lux-r-sm);
|
||
position:relative;
|
||
}
|
||
.anatomy-stage .mod-icon{
|
||
--plate-size:160px;
|
||
cursor:default;animation:platePulse 3.4s ease-in-out infinite;
|
||
}
|
||
.anatomy-stage .mod-icon svg{width:88px;height:88px;stroke-width:1.4;}
|
||
.anatomy-stage .mod-icon::before{width:18px;height:18px;top:8px;right:8px;}
|
||
|
||
/* Annotation lines */
|
||
.anno{
|
||
position:absolute;
|
||
font-family:var(--font-mono);font-size:.6rem;
|
||
letter-spacing:.16em;text-transform:uppercase;
|
||
color:var(--lux-ink-dim);
|
||
display:flex;align-items:center;gap:8px;
|
||
white-space:nowrap;
|
||
}
|
||
.anno::before{
|
||
content:'';width:24px;height:1px;background:var(--lux-ink-mute);
|
||
}
|
||
.anno-1{top:18%;left:calc(50% + 92px);}
|
||
.anno-2{top:60%;left:calc(50% + 92px);}
|
||
.anno-3{bottom:18%;right:calc(50% + 92px);flex-direction:row-reverse;}
|
||
.anno-3::before{margin-left:0;}
|
||
.anno-4{top:38%;right:calc(50% + 92px);flex-direction:row-reverse;}
|
||
.anno-4::before{margin-left:0;}
|
||
.anno b{font-weight:700;color:var(--lux-ink);letter-spacing:.16em;}
|
||
|
||
.anatomy-copy h3{
|
||
font-family:var(--font-display);font-size:2rem;font-weight:800;
|
||
letter-spacing:-.01em;line-height:1;margin-bottom:14px;
|
||
}
|
||
.anatomy-copy h3 em{font-style:normal;color:var(--ch-cyan);}
|
||
.anatomy-copy p{
|
||
color:var(--lux-ink-dim);line-height:1.55;font-size:.95rem;
|
||
margin-bottom:14px;max-width:54ch;
|
||
}
|
||
.spec-list{
|
||
list-style:none;display:flex;flex-direction:column;gap:10px;margin-top:18px;
|
||
}
|
||
.spec-list li{
|
||
display:grid;grid-template-columns:96px 1fr;gap:14px;
|
||
font-size:.86rem;color:var(--lux-ink-dim);line-height:1.5;
|
||
}
|
||
.spec-list .k{
|
||
font-family:var(--font-mono);font-size:.62rem;
|
||
letter-spacing:.18em;text-transform:uppercase;
|
||
color:var(--ch-signal);align-self:start;padding-top:2px;
|
||
border-left:2px solid color-mix(in srgb, var(--ch-signal) 50%, transparent);
|
||
padding-left:10px;
|
||
}
|
||
.spec-list strong{color:var(--lux-ink);font-weight:700;}
|
||
.spec-list code{
|
||
font-family:var(--font-mono);font-size:.78rem;
|
||
background:var(--lux-bg-0);border:1px solid var(--lux-line);
|
||
padding:1px 5px;border-radius:3px;color:var(--ch-cyan);
|
||
}
|
||
|
||
/* =====================================================================
|
||
Variants strip — show the plate in idle / running / offline / empty
|
||
===================================================================== */
|
||
.variants{
|
||
display:grid;grid-template-columns:repeat(4, 1fr);gap:14px;
|
||
margin-bottom:24px;
|
||
}
|
||
.variant{
|
||
padding:18px;
|
||
background:var(--lux-bg-1);
|
||
border:1px solid var(--lux-line);
|
||
border-radius:var(--lux-r-md);
|
||
display:flex;flex-direction:column;align-items:center;gap:14px;
|
||
position:relative;overflow:hidden;
|
||
}
|
||
.variant::before{
|
||
content:'';position:absolute;left:0;top:0;bottom:0;width:2px;
|
||
background:var(--ch);opacity:.5;
|
||
}
|
||
.variant[data-ch="signal"]{--ch:var(--ch-signal);}
|
||
.variant[data-ch="cyan"]{--ch:var(--ch-cyan);}
|
||
.variant[data-ch="amber"]{--ch:var(--ch-amber);}
|
||
.variant[data-ch="coral"]{--ch:var(--ch-coral);}
|
||
.variant .label{
|
||
font-family:var(--font-mono);font-size:.6rem;
|
||
letter-spacing:.22em;text-transform:uppercase;
|
||
color:var(--lux-ink-mute);
|
||
}
|
||
.variant .label b{color:var(--ch);font-weight:700;}
|
||
.variant .stage{
|
||
--plate-size:84px;
|
||
width:var(--plate-size);height:var(--plate-size);
|
||
}
|
||
.variant .stage .mod-icon{--plate-size:84px;}
|
||
.variant .stage .mod-icon svg{width:42px;height:42px;}
|
||
.variant .desc{
|
||
font-size:.78rem;color:var(--lux-ink-dim);text-align:center;line-height:1.5;
|
||
}
|
||
|
||
/* =====================================================================
|
||
Picker modal — shown inline as a "live" demo (not really a backdrop;
|
||
it sits in the page flow with a dimmed shell)
|
||
===================================================================== */
|
||
.picker-stage{
|
||
position:relative;padding:48px 24px;
|
||
background:
|
||
radial-gradient(ellipse at 50% 50%, color-mix(in srgb, var(--ch-violet) 12%, transparent), transparent 60%),
|
||
var(--lux-bg-0);
|
||
border:1px solid var(--lux-line);
|
||
border-radius:var(--lux-r-md);
|
||
overflow:hidden;
|
||
}
|
||
.picker-stage::before,
|
||
.picker-stage::after{
|
||
content:'';position:absolute;width:140px;height:140px;
|
||
border:1px solid color-mix(in srgb, var(--ch-violet) 40%, var(--lux-line));
|
||
border-radius:50%;opacity:.4;pointer-events:none;
|
||
}
|
||
.picker-stage::before{top:-40px;left:-40px;}
|
||
.picker-stage::after{bottom:-60px;right:-60px;border-color:color-mix(in srgb, var(--ch-cyan) 40%, var(--lux-line));}
|
||
|
||
.picker{
|
||
width:min(720px,100%);margin:0 auto;
|
||
background:var(--lux-bg-1);
|
||
border:1px solid var(--lux-line-bold);
|
||
border-radius:var(--lux-r-md);
|
||
box-shadow:
|
||
0 1px 0 rgba(255,255,255,.03),
|
||
0 24px 48px rgba(0,0,0,.55);
|
||
overflow:hidden;position:relative;
|
||
--ch:var(--ch-cyan);
|
||
}
|
||
.picker::before{
|
||
content:'';position:absolute;left:0;top:0;right:0;height:2px;
|
||
background:linear-gradient(90deg,var(--ch-cyan),var(--ch-violet),var(--ch-magenta));
|
||
opacity:.85;
|
||
}
|
||
.picker-head{
|
||
display:flex;align-items:center;gap:14px;
|
||
padding:18px 22px 14px;border-bottom:1px solid var(--lux-line);
|
||
}
|
||
.picker-head .preview{
|
||
--plate-size:64px;
|
||
flex:0 0 var(--plate-size);
|
||
}
|
||
.picker-head .preview .mod-icon{--plate-size:64px;cursor:default;}
|
||
.picker-head .preview .mod-icon svg{width:34px;height:34px;}
|
||
.picker-head .meta{flex:1;min-width:0;}
|
||
.picker-head .eyebrow-mini{
|
||
font-family:var(--font-mono);font-size:.58rem;font-weight:600;
|
||
letter-spacing:.24em;text-transform:uppercase;
|
||
color:var(--ch-cyan);margin-bottom:4px;
|
||
}
|
||
.picker-head h2{
|
||
font-family:var(--font-display);font-size:1.45rem;font-weight:800;
|
||
letter-spacing:-.005em;line-height:1.05;color:var(--lux-ink);
|
||
}
|
||
.picker-head .sub{
|
||
font-family:var(--font-mono);font-size:.66rem;color:var(--lux-ink-mute);
|
||
letter-spacing:.06em;margin-top:3px;
|
||
}
|
||
.picker-close{
|
||
width:28px;height:28px;border-radius:3px;background:transparent;
|
||
border:1px solid var(--lux-line);color:var(--lux-ink-mute);cursor:pointer;
|
||
display:flex;align-items:center;justify-content:center;
|
||
transition:.15s;
|
||
}
|
||
.picker-close:hover{background:var(--lux-bg-3);color:var(--lux-ink);}
|
||
.picker-close svg{width:12px;height:12px;}
|
||
|
||
.picker-toolbar{
|
||
display:flex;gap:10px;padding:12px 22px;
|
||
background:var(--lux-bg-0);
|
||
border-bottom:1px solid var(--lux-line);
|
||
align-items:center;
|
||
}
|
||
.picker-search{
|
||
flex:1;display:flex;align-items:center;gap:8px;
|
||
padding:8px 12px;background:var(--lux-bg-1);
|
||
border:1px solid var(--lux-line-bold);
|
||
border-radius:var(--lux-r-sm);
|
||
}
|
||
.picker-search svg{width:13px;height:13px;color:var(--lux-ink-mute);flex-shrink:0;}
|
||
.picker-search input{
|
||
flex:1;background:none;border:none;outline:none;
|
||
font-family:var(--font-mono);font-size:.78rem;letter-spacing:.04em;
|
||
color:var(--lux-ink);
|
||
}
|
||
.picker-search input::placeholder{
|
||
color:var(--lux-ink-mute);text-transform:uppercase;letter-spacing:.16em;font-size:.66rem;
|
||
}
|
||
.picker-search kbd{
|
||
font-family:var(--font-mono);font-size:.6rem;color:var(--lux-ink-mute);
|
||
border:1px solid var(--lux-line);border-radius:2px;
|
||
padding:1px 5px;background:var(--lux-bg-0);
|
||
}
|
||
.color-toggle{
|
||
display:flex;align-items:center;gap:8px;padding:8px 12px;
|
||
background:var(--lux-bg-1);border:1px solid var(--lux-line-bold);
|
||
border-radius:var(--lux-r-sm);cursor:pointer;
|
||
font-family:var(--font-mono);font-size:.62rem;font-weight:600;
|
||
letter-spacing:.18em;text-transform:uppercase;
|
||
color:var(--lux-ink-dim);
|
||
}
|
||
.color-toggle .swatch{
|
||
width:14px;height:14px;border-radius:2px;
|
||
background:var(--ch);
|
||
border:1px solid color-mix(in srgb, var(--ch) 60%, var(--lux-line-bold));
|
||
box-shadow:0 0 0 0 color-mix(in srgb, var(--ch) 40%, transparent);
|
||
}
|
||
.color-toggle:hover{color:var(--lux-ink);}
|
||
|
||
.picker-tabs{
|
||
display:flex;gap:0;padding:0 22px;
|
||
background:var(--lux-bg-0);border-bottom:1px solid var(--lux-line);
|
||
overflow-x:auto;
|
||
}
|
||
.picker-tab{
|
||
appearance:none;background:none;border:none;
|
||
padding:11px 16px;font-family:var(--font-mono);
|
||
font-size:.62rem;font-weight:600;
|
||
letter-spacing:.2em;text-transform:uppercase;
|
||
color:var(--lux-ink-mute);cursor:pointer;
|
||
border-bottom:2px solid transparent;
|
||
transition:.15s;display:flex;align-items:center;gap:8px;
|
||
white-space:nowrap;
|
||
}
|
||
.picker-tab .count{
|
||
font-size:.55rem;color:var(--lux-ink-faint);letter-spacing:.05em;
|
||
background:var(--lux-bg-2);padding:1px 5px;border-radius:99px;
|
||
}
|
||
.picker-tab:hover{color:var(--lux-ink-dim);}
|
||
.picker-tab.is-active{
|
||
color:var(--ch);
|
||
border-bottom-color:var(--ch);
|
||
}
|
||
.picker-tab.is-active .count{
|
||
background:color-mix(in srgb, var(--ch) 14%, transparent);
|
||
color:var(--ch);
|
||
}
|
||
|
||
.picker-recent{
|
||
display:flex;align-items:center;gap:14px;
|
||
padding:12px 22px;border-bottom:1px solid var(--lux-line);
|
||
background:var(--lux-bg-0);
|
||
}
|
||
.picker-recent .label{
|
||
font-family:var(--font-mono);font-size:.58rem;font-weight:600;
|
||
letter-spacing:.22em;text-transform:uppercase;color:var(--lux-ink-mute);
|
||
flex-shrink:0;
|
||
}
|
||
.picker-recent .strip{
|
||
display:flex;gap:6px;flex:1;overflow-x:auto;
|
||
}
|
||
.picker-recent .icon-tile{width:34px;height:34px;}
|
||
.picker-recent .icon-tile svg{width:18px;height:18px;}
|
||
|
||
.picker-grid-wrap{
|
||
padding:18px 22px 14px;max-height:360px;overflow-y:auto;
|
||
background:var(--lux-bg-1);
|
||
}
|
||
.picker-cat{
|
||
font-family:var(--font-mono);font-size:.58rem;font-weight:600;
|
||
letter-spacing:.24em;text-transform:uppercase;color:var(--lux-ink-mute);
|
||
margin:14px 0 10px;display:flex;align-items:center;gap:10px;
|
||
}
|
||
.picker-cat:first-child{margin-top:0;}
|
||
.picker-cat::after{content:'';flex:1;height:1px;background:var(--lux-line);}
|
||
|
||
.picker-grid{
|
||
display:grid;grid-template-columns:repeat(8,1fr);gap:6px;
|
||
}
|
||
.icon-tile{
|
||
--ch:var(--ch-cyan);
|
||
appearance:none;border:none;
|
||
width:42px;height:42px;
|
||
background:var(--lux-bg-0);
|
||
border:1px solid var(--lux-line);
|
||
border-radius:3px;color:var(--lux-ink-dim);
|
||
display:flex;align-items:center;justify-content:center;
|
||
cursor:pointer;transition:.15s;position:relative;
|
||
}
|
||
.icon-tile svg{
|
||
width:20px;height:20px;
|
||
stroke:currentColor;stroke-width:1.5;
|
||
fill:none;stroke-linecap:round;stroke-linejoin:round;
|
||
}
|
||
.icon-tile:hover{
|
||
color:var(--ch);
|
||
border-color:color-mix(in srgb, var(--ch) 50%, var(--lux-line));
|
||
background:color-mix(in srgb, var(--ch) 8%, var(--lux-bg-0));
|
||
box-shadow:0 0 12px color-mix(in srgb, var(--ch) 22%, transparent);
|
||
}
|
||
.icon-tile.is-selected{
|
||
color:var(--ch);
|
||
border-color:var(--ch);
|
||
background:color-mix(in srgb, var(--ch) 16%, var(--lux-bg-0));
|
||
box-shadow:0 0 16px color-mix(in srgb, var(--ch) 35%, transparent),
|
||
inset 0 0 0 1px color-mix(in srgb, var(--ch) 35%, transparent);
|
||
}
|
||
.icon-tile.is-selected::after{
|
||
content:'';position:absolute;top:3px;right:3px;
|
||
width:6px;height:6px;border-radius:50%;
|
||
background:var(--ch);box-shadow:0 0 6px var(--ch);
|
||
}
|
||
.icon-tile .name{display:none;}
|
||
|
||
.picker-foot{
|
||
display:flex;align-items:center;gap:10px;
|
||
padding:14px 22px;border-top:1px solid var(--lux-line);
|
||
background:var(--lux-bg-0);
|
||
}
|
||
.picker-foot .hint{
|
||
font-family:var(--font-mono);font-size:.6rem;letter-spacing:.16em;
|
||
text-transform:uppercase;color:var(--lux-ink-mute);
|
||
margin-right:auto;display:flex;align-items:center;gap:8px;
|
||
}
|
||
.picker-foot .hint kbd{
|
||
border:1px solid var(--lux-line);border-radius:2px;
|
||
padding:1px 5px;background:var(--lux-bg-1);
|
||
color:var(--lux-ink-dim);
|
||
}
|
||
.picker-btn{
|
||
appearance:none;border:1px solid var(--lux-line-bold);
|
||
padding:8px 16px;border-radius:var(--lux-r-sm);
|
||
background:var(--lux-bg-1);color:var(--lux-ink-dim);
|
||
font-family:var(--font-mono);font-size:.66rem;font-weight:600;
|
||
letter-spacing:.16em;text-transform:uppercase;cursor:pointer;
|
||
transition:.15s;
|
||
}
|
||
.picker-btn:hover{color:var(--lux-ink);background:var(--lux-bg-3);}
|
||
.picker-btn--ghost{border-color:transparent;background:transparent;}
|
||
.picker-btn--primary{
|
||
background:var(--ch);color:#fff;border-color:var(--ch);
|
||
box-shadow:0 0 18px color-mix(in srgb, var(--ch) 40%, transparent);
|
||
}
|
||
.picker-btn--primary:hover{filter:brightness(1.08);color:#fff;background:var(--ch);}
|
||
.picker-btn--danger{
|
||
color:var(--ch-coral);
|
||
border-color:color-mix(in srgb, var(--ch-coral) 40%, var(--lux-line-bold));
|
||
}
|
||
.picker-btn--danger:hover{
|
||
background:color-mix(in srgb, var(--ch-coral) 12%, transparent);
|
||
color:var(--ch-coral);
|
||
}
|
||
|
||
/* =====================================================================
|
||
Notes
|
||
===================================================================== */
|
||
.notes{
|
||
margin-top:64px;padding:30px 34px;
|
||
background:var(--lux-bg-1);border:1px solid var(--lux-line);
|
||
border-radius:var(--lux-r-md);position:relative;overflow:hidden;
|
||
}
|
||
.notes::before{
|
||
content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
|
||
background:linear-gradient(180deg,var(--ch-signal),var(--ch-cyan),var(--ch-violet));
|
||
}
|
||
.notes h2{
|
||
font-family:var(--font-display);font-size:1.6rem;font-weight:800;
|
||
letter-spacing:-.01em;margin-bottom:18px;
|
||
}
|
||
.notes ul{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px 28px;}
|
||
.notes li{
|
||
font-size:.86rem;color:var(--lux-ink-dim);line-height:1.55;
|
||
padding-left:18px;position:relative;
|
||
}
|
||
.notes li::before{
|
||
content:'';position:absolute;left:0;top:.65em;width:8px;height:1px;background:var(--ch-signal);
|
||
}
|
||
.notes strong{color:var(--lux-ink);font-weight:700;}
|
||
.notes code{
|
||
font-family:var(--font-mono);font-size:.78rem;
|
||
background:var(--lux-bg-0);border:1px solid var(--lux-line);
|
||
padding:1px 5px;border-radius:3px;color:var(--ch-cyan);
|
||
}
|
||
|
||
@media (max-width: 880px){
|
||
.anatomy{grid-template-columns:1fr;}
|
||
.variants{grid-template-columns:repeat(2,1fr);}
|
||
.picker-grid{grid-template-columns:repeat(6,1fr);}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
||
<!-- ── Icon library (subset of a Lucide-like set) ── -->
|
||
<defs>
|
||
<symbol id="i-mouse" viewBox="0 0 24 24"><rect x="6" y="3" width="12" height="18" rx="6"/><line x1="12" y1="7" x2="12" y2="12"/></symbol>
|
||
<symbol id="i-keyboard" viewBox="0 0 24 24"><rect x="2" y="6" width="20" height="13" rx="2"/><line x1="6" y1="10" x2="6.01" y2="10"/><line x1="10" y1="10" x2="10.01" y2="10"/><line x1="14" y1="10" x2="14.01" y2="10"/><line x1="18" y1="10" x2="18.01" y2="10"/><line x1="6" y1="14" x2="6.01" y2="14"/><line x1="18" y1="14" x2="18.01" y2="14"/><line x1="9" y1="14" x2="15" y2="14"/></symbol>
|
||
<symbol id="i-monitor" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol>
|
||
<symbol id="i-tv" viewBox="0 0 24 24"><rect x="2" y="6" width="20" height="14" rx="2"/><polyline points="17 2 12 7 7 2"/></symbol>
|
||
<symbol id="i-cpu" viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></symbol>
|
||
<symbol id="i-board" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><rect x="7" y="7" width="6" height="6"/><line x1="15" y1="7" x2="17" y2="7"/><line x1="15" y1="10" x2="17" y2="10"/><line x1="7" y1="16" x2="9" y2="16"/><line x1="7" y1="19" x2="9" y2="19"/><circle cx="17" cy="16" r="1.5"/><circle cx="13" cy="19" r="1"/></symbol>
|
||
<symbol id="i-gpu" viewBox="0 0 24 24"><rect x="2" y="7" width="20" height="10" rx="1"/><circle cx="8" cy="12" r="2.5"/><circle cx="14" cy="12" r="2.5"/><line x1="22" y1="9" x2="22" y2="15"/></symbol>
|
||
<symbol id="i-ram" viewBox="0 0 24 24"><rect x="2" y="8" width="20" height="9"/><rect x="5" y="11" width="2" height="3"/><rect x="9" y="11" width="2" height="3"/><rect x="13" y="11" width="2" height="3"/><rect x="17" y="11" width="2" height="3"/><line x1="2" y1="17" x2="22" y2="17"/><line x1="6" y1="17" x2="6" y2="20"/><line x1="18" y1="17" x2="18" y2="20"/></symbol>
|
||
<symbol id="i-ssd" viewBox="0 0 24 24"><rect x="3" y="6" width="18" height="12" rx="2"/><line x1="3" y1="14" x2="21" y2="14"/><circle cx="7" cy="16.5" r=".7"/><circle cx="10" cy="16.5" r=".7"/><circle cx="17" cy="9.5" r="1.3"/></symbol>
|
||
<symbol id="i-router" viewBox="0 0 24 24"><rect x="2" y="14" width="20" height="6" rx="2"/><line x1="6.5" y1="17" x2="6.51" y2="17"/><line x1="10" y1="17" x2="10.01" y2="17"/><path d="M5 10c0-3.87 3.13-7 7-7s7 3.13 7 7"/><path d="M8.5 10c0-1.93 1.57-3.5 3.5-3.5s3.5 1.57 3.5 3.5"/></symbol>
|
||
<symbol id="i-camera" viewBox="0 0 24 24"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></symbol>
|
||
<symbol id="i-mic" viewBox="0 0 24 24"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></symbol>
|
||
<symbol id="i-speaker" viewBox="0 0 24 24"><rect x="4" y="2" width="16" height="20" rx="2"/><circle cx="12" cy="14" r="4"/><line x1="12" y1="6" x2="12.01" y2="6"/></symbol>
|
||
<symbol id="i-headphones" viewBox="0 0 24 24"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3z"/><path d="M3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></symbol>
|
||
<symbol id="i-controller" viewBox="0 0 24 24"><line x1="6" y1="11" x2="10" y2="11"/><line x1="8" y1="9" x2="8" y2="13"/><line x1="15" y1="12" x2="15.01" y2="12"/><line x1="18" y1="10" x2="18.01" y2="10"/><path d="M17.32 5H6.68a4 4 0 0 0-3.978 3.59L2 14a2 2 0 0 0 2 2h.5a3 3 0 0 0 2.7-1.7L8 12.5h8l.8 1.8a3 3 0 0 0 2.7 1.7H20a2 2 0 0 0 2-2l-.7-5.41A4 4 0 0 0 17.32 5z"/></symbol>
|
||
<symbol id="i-bulb" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></symbol>
|
||
<symbol id="i-strip" viewBox="0 0 24 24"><rect x="2" y="9" width="20" height="6" rx="1"/><circle cx="6" cy="12" r="1.2" fill="currentColor"/><circle cx="10" cy="12" r="1.2" fill="currentColor"/><circle cx="14" cy="12" r="1.2" fill="currentColor"/><circle cx="18" cy="12" r="1.2" fill="currentColor"/></symbol>
|
||
<symbol id="i-panel" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="12" cy="8" r="1.2" fill="currentColor"/><circle cx="16" cy="8" r="1.2" fill="currentColor"/><circle cx="8" cy="12" r="1.2" fill="currentColor"/><circle cx="12" cy="12" r="1.2" fill="currentColor"/><circle cx="16" cy="12" r="1.2" fill="currentColor"/><circle cx="8" cy="16" r="1.2" fill="currentColor"/><circle cx="12" cy="16" r="1.2" fill="currentColor"/><circle cx="16" cy="16" r="1.2" fill="currentColor"/></symbol>
|
||
<symbol id="i-ring" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/></symbol>
|
||
<symbol id="i-lamp" viewBox="0 0 24 24"><path d="M8 2h8l3 7H5z"/><path d="M12 9v9"/><path d="M9 18h6"/></symbol>
|
||
<symbol id="i-spot" viewBox="0 0 24 24"><path d="M6 4h12l-2 8H8z"/><line x1="12" y1="12" x2="12" y2="22"/><line x1="9" y1="22" x2="15" y2="22"/></symbol>
|
||
<symbol id="i-bed" viewBox="0 0 24 24"><path d="M2 18v-7a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v7"/><path d="M2 18h20"/><path d="M2 22v-4"/><path d="M22 22v-4"/><circle cx="7" cy="11" r="1.5"/></symbol>
|
||
<symbol id="i-sofa" viewBox="0 0 24 24"><path d="M20 9V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v2"/><path d="M2 13a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v4H2z"/><line x1="6" y1="17" x2="6" y2="20"/><line x1="18" y1="17" x2="18" y2="20"/></symbol>
|
||
<symbol id="i-desk" viewBox="0 0 24 24"><line x1="2" y1="9" x2="22" y2="9"/><line x1="2" y1="6" x2="22" y2="6"/><line x1="6" y1="9" x2="6" y2="20"/><line x1="18" y1="9" x2="18" y2="20"/><line x1="6" y1="15" x2="18" y2="15"/></symbol>
|
||
<symbol id="i-window" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="12" y1="3" x2="12" y2="21"/></symbol>
|
||
<symbol id="i-door" viewBox="0 0 24 24"><path d="M19 22V4a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v18"/><line x1="2" y1="22" x2="22" y2="22"/><circle cx="14" cy="13" r=".7" fill="currentColor"/></symbol>
|
||
<symbol id="i-fan" viewBox="0 0 24 24"><circle cx="12" cy="12" r="2"/><path d="M12 4a4 4 0 0 1 4 4c0 2-4 6-4 6s-4-4-4-6a4 4 0 0 1 4-4z"/><path d="M20 12a4 4 0 0 1-4 4c-2 0-6-4-6-4s4-4 6-4a4 4 0 0 1 4 4z"/><path d="M12 20a4 4 0 0 1-4-4c0-2 4-6 4-6s4 4 4 6a4 4 0 0 1-4 4z"/><path d="M4 12a4 4 0 0 1 4-4c2 0 6 4 6 4s-4 4-6 4a4 4 0 0 1-4-4z"/></symbol>
|
||
<symbol id="i-thermo" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="2"/><line x1="12" y1="6" x2="12" y2="9"/><line x1="12" y1="15" x2="12" y2="18"/><line x1="6" y1="12" x2="9" y2="12"/><line x1="15" y1="12" x2="18" y2="12"/></symbol>
|
||
<symbol id="i-power" viewBox="0 0 24 24"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/></symbol>
|
||
<symbol id="i-wifi" viewBox="0 0 24 24"><path d="M5 12.55a11 11 0 0 1 14 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></symbol>
|
||
<symbol id="i-bt" viewBox="0 0 24 24"><polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"/></symbol>
|
||
<symbol id="i-antenna" viewBox="0 0 24 24"><line x1="2" y1="22" x2="22" y2="22"/><line x1="12" y1="22" x2="12" y2="2"/><path d="M5 8c0-3 3-5 7-5s7 2 7 5"/><path d="M8 11c0-2 2-3 4-3s4 1 4 3"/></symbol>
|
||
<symbol id="i-server" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="6" rx="1"/><rect x="2" y="13" width="20" height="6" rx="1"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="16" x2="6.01" y2="16"/><line x1="10" y1="6" x2="10.01" y2="6"/><line x1="10" y1="16" x2="10.01" y2="16"/></symbol>
|
||
<symbol id="i-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><line x1="12" y1="2" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="22"/><line x1="2" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="22" y2="12"/><line x1="4.93" y1="4.93" x2="6.34" y2="6.34"/><line x1="17.66" y1="17.66" x2="19.07" y2="19.07"/><line x1="4.93" y1="19.07" x2="6.34" y2="17.66"/><line x1="17.66" y1="6.34" x2="19.07" y2="4.93"/></symbol>
|
||
<symbol id="i-moon" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol>
|
||
<symbol id="i-flame" viewBox="0 0 24 24"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></symbol>
|
||
<symbol id="i-leaf" viewBox="0 0 24 24"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19.2 2.96c1 4.34.06 9.65-3.4 13.04A6.96 6.96 0 0 1 11 20z"/><path d="M2 21c0-3 1.85-5.36 5.08-6"/></symbol>
|
||
<symbol id="i-music" viewBox="0 0 24 24"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></symbol>
|
||
<symbol id="i-film" viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="20" rx="2"/><line x1="7" y1="2" x2="7" y2="22"/><line x1="17" y1="2" x2="17" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="2" y1="7" x2="7" y2="7"/><line x1="2" y1="17" x2="7" y2="17"/><line x1="17" y1="17" x2="22" y2="17"/><line x1="17" y1="7" x2="22" y2="7"/></symbol>
|
||
<symbol id="i-ha" viewBox="0 0 24 24"><path d="M3 12l9-9 9 9"/><path d="M5 10v10h14V10"/><circle cx="12" cy="14" r="2"/></symbol>
|
||
<symbol id="i-cloud" viewBox="0 0 24 24"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/></symbol>
|
||
<symbol id="i-edit" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4z"/></symbol>
|
||
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></symbol>
|
||
<symbol id="i-x" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></symbol>
|
||
<symbol id="i-plus" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></symbol>
|
||
<symbol id="i-stop" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="1"/></symbol>
|
||
<symbol id="i-settings" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></symbol>
|
||
<symbol id="i-refresh" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></symbol>
|
||
</defs>
|
||
</svg>
|
||
|
||
<div class="page">
|
||
|
||
<header class="page-head">
|
||
<div>
|
||
<div class="eyebrow">Proposal · Card · Custom Icon</div>
|
||
<h1>A face plate for<br>every <em>module.</em></h1>
|
||
<p class="lede">Cards inherit the channel-color stripe and instrument badges that already define the rack. Adding a user-chosen <code>icon</code> turns each card into something the user actually <em>recognises at a glance</em> — a mouse becomes a mouse, a motherboard becomes a motherboard. The badge stays as the type-of-thing label; the icon answers <strong>which thing</strong>. Optional, channel-tinted, and slotted in at the leading edge of <code>.mod-head</code> so the rest of the head row's typography is untouched.</p>
|
||
</div>
|
||
<div class="theme-toggle" role="group" aria-label="Theme">
|
||
<button class="is-active" data-theme-set="dark">DARK</button>
|
||
<button data-theme-set="light">LIGHT</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- ===================================================================== -->
|
||
<div class="section-title"><span class="num">01</span> Before / After <span class="tag">CH · SIGNAL</span></div>
|
||
|
||
<div class="grid">
|
||
|
||
<!-- BEFORE — current device card (badge-only head) -->
|
||
<article class="module is-running" data-ch="signal">
|
||
<div class="mod-head">
|
||
<div class="mod-id">
|
||
<span class="mod-badge">WLED · OUT</span>
|
||
<div class="mod-name"><span>Living Room Strip</span></div>
|
||
<div class="mod-meta">192.168.1.42 · v0.14</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led on"></span><span class="led on blink"></span><span class="led on blink"></span>
|
||
</div>
|
||
</div>
|
||
<div class="mod-metrics">
|
||
<div class="mod-metric"><div class="k">PIXELS</div><div class="v">144</div></div>
|
||
<div class="mod-metric"><div class="k">LAT</div><div class="v signal">8<small>ms</small></div></div>
|
||
<div class="mod-metric"><div class="k">CHIP</div><div class="v" style="font-size:.92rem;font-family:var(--font-mono);font-weight:700;letter-spacing:.04em;">WS2812B</div></div>
|
||
</div>
|
||
<div class="mod-fader">
|
||
<span class="mod-fader__k">Bright</span>
|
||
<div class="mod-fader__track"><div class="mod-fader__fill" style="width:78%"></div></div>
|
||
<span class="mod-fader__v">198</span>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot is-live"></span><span>ONLINE</span></div>
|
||
<button class="mod-btn mod-btn-stop"><svg width="12" height="12"><use href="#i-stop"/></svg><span>STOP</span></button>
|
||
<button class="mod-btn mod-btn-icon" title="Refresh"><svg width="14" height="14"><use href="#i-refresh"/></svg></button>
|
||
<button class="mod-btn mod-btn-icon" title="Settings"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
|
||
</div>
|
||
<div style="position:absolute;left:50%;top:6px;transform:translateX(-50%);font-family:var(--font-mono);font-size:.55rem;letter-spacing:.24em;text-transform:uppercase;color:var(--lux-ink-mute);background:var(--lux-bg-0);border:1px solid var(--lux-line);padding:2px 8px;border-radius:99px;">Before</div>
|
||
</article>
|
||
|
||
<!-- AFTER — same card, with icon plate -->
|
||
<article class="module is-running" data-ch="signal">
|
||
<div class="mod-head mod-head--with-icon">
|
||
<button class="mod-icon" type="button" aria-label="Change icon">
|
||
<svg><use href="#i-strip"/></svg>
|
||
<span class="mod-icon__edit" aria-hidden="true"><svg><use href="#i-edit"/></svg></span>
|
||
</button>
|
||
<div class="mod-id">
|
||
<span class="mod-badge">WLED · OUT</span>
|
||
<div class="mod-name"><span>Living Room Strip</span></div>
|
||
<div class="mod-meta">192.168.1.42 · v0.14</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led on"></span><span class="led on blink"></span><span class="led on blink"></span>
|
||
</div>
|
||
</div>
|
||
<div class="mod-metrics">
|
||
<div class="mod-metric"><div class="k">PIXELS</div><div class="v">144</div></div>
|
||
<div class="mod-metric"><div class="k">LAT</div><div class="v signal">8<small>ms</small></div></div>
|
||
<div class="mod-metric"><div class="k">CHIP</div><div class="v" style="font-size:.92rem;font-family:var(--font-mono);font-weight:700;letter-spacing:.04em;">WS2812B</div></div>
|
||
</div>
|
||
<div class="mod-fader">
|
||
<span class="mod-fader__k">Bright</span>
|
||
<div class="mod-fader__track"><div class="mod-fader__fill" style="width:78%"></div></div>
|
||
<span class="mod-fader__v">198</span>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot is-live"></span><span>ONLINE</span></div>
|
||
<button class="mod-btn mod-btn-stop"><svg width="12" height="12"><use href="#i-stop"/></svg><span>STOP</span></button>
|
||
<button class="mod-btn mod-btn-icon" title="Refresh"><svg width="14" height="14"><use href="#i-refresh"/></svg></button>
|
||
<button class="mod-btn mod-btn-icon" title="Settings"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
|
||
</div>
|
||
<div style="position:absolute;left:50%;top:6px;transform:translateX(-50%);font-family:var(--font-mono);font-size:.55rem;letter-spacing:.24em;text-transform:uppercase;color:var(--ch-signal);background:var(--lux-bg-0);border:1px solid color-mix(in srgb,var(--ch-signal) 50%,var(--lux-line));padding:2px 8px;border-radius:99px;">After · Strip</div>
|
||
</article>
|
||
</div>
|
||
|
||
<!-- ===================================================================== -->
|
||
<div class="section-title"><span class="num">02</span> Anatomy <span class="tag" style="--ch-signal:var(--ch-cyan);color:var(--ch-cyan);border-color:color-mix(in srgb, var(--ch-cyan) 30%, transparent);background:color-mix(in srgb, var(--ch-cyan) 10%, transparent);">Plate</span></div>
|
||
|
||
<div class="anatomy">
|
||
<div class="anatomy-stage">
|
||
<div class="module" data-ch="cyan" style="background:transparent;border:none;padding:0;">
|
||
<div class="mod-icon" aria-hidden="true">
|
||
<svg><use href="#i-board"/></svg>
|
||
<span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span>
|
||
</div>
|
||
</div>
|
||
<span class="anno anno-1"><b>52 × 52</b> · plate</span>
|
||
<span class="anno anno-2">channel · <b>tint</b></span>
|
||
<span class="anno anno-3"><b>edit pin</b> · on hover</span>
|
||
<span class="anno anno-4">corner · <b>silkscreen</b></span>
|
||
</div>
|
||
<div class="anatomy-copy">
|
||
<h3>One plate.<br><em>Five quiet signals.</em></h3>
|
||
<p>The plate isn't a box around an icon. It's a small instrument-panel face that re-uses the card's channel color, the corner-bracket silkscreen, and the modular-rack scanline texture. Touch it and it lifts. Live cards make it breathe.</p>
|
||
<ul class="spec-list">
|
||
<li><span class="k">Size</span><div><strong>52 × 52 px</strong> on cards · <strong>40 × 40</strong> on dashboard tiles · <strong>34 × 34</strong> on perf charts. Scales with the card's existing breakpoints — no new layout math.</div></li>
|
||
<li><span class="k">Tint</span><div>Inherits <code>--ch</code> from the card (same variable that drives stripe, badge, fader). Pick a palette icon → it picks up the channel automatically.</div></li>
|
||
<li><span class="k">Override</span><div>Optional per-card hex via the picker's <strong>color toggle</strong>. Stored as <code>device.icon_color</code>; falls back to <code>--ch</code> when null.</div></li>
|
||
<li><span class="k">States</span><div><strong>Idle</strong> · <strong>Running</strong> (breathing) · <strong>Offline</strong> (desaturated) · <strong>Fault</strong> (coral) · <strong>Empty</strong> (dashed placeholder).</div></li>
|
||
<li><span class="k">Storage</span><div>Single field on the entity: <code>icon: "motherboard"</code>. Backwards-compatible — null hides the plate and the head reverts to today's badge-led layout.</div></li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===================================================================== -->
|
||
<div class="section-title"><span class="num">03</span> States</div>
|
||
|
||
<div class="variants">
|
||
<div class="variant" data-ch="signal">
|
||
<div class="label"><b>IDLE</b> · default</div>
|
||
<div class="stage">
|
||
<div class="mod-icon">
|
||
<svg><use href="#i-mouse"/></svg>
|
||
</div>
|
||
</div>
|
||
<div class="desc">Resting state · channel-tinted · corner bracket visible · scanline texture.</div>
|
||
</div>
|
||
<div class="variant" data-ch="cyan">
|
||
<div class="label"><b>RUNNING</b> · live</div>
|
||
<div class="stage">
|
||
<div class="module is-running" style="background:transparent;border:none;padding:0;">
|
||
<div class="mod-icon">
|
||
<svg><use href="#i-camera"/></svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="desc">2.6s breath syncs to the patch dot pulse · widens the stripe in lockstep.</div>
|
||
</div>
|
||
<div class="variant" data-ch="amber">
|
||
<div class="label"><b>OFFLINE</b></div>
|
||
<div class="stage">
|
||
<div class="module" data-state="offline" style="background:transparent;border:none;padding:0;">
|
||
<div class="mod-icon">
|
||
<svg><use href="#i-tv"/></svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="desc">Glyph desaturates to <code>--lux-ink-mute</code> · plate keeps its outline so the slot stays anchored.</div>
|
||
</div>
|
||
<div class="variant" data-ch="coral">
|
||
<div class="label"><b>FAULT</b> / <b>EMPTY</b></div>
|
||
<div class="stage" style="display:flex;flex-direction:row;gap:10px;align-items:center;justify-content:center;">
|
||
<div class="module" data-state="fault" style="background:transparent;border:none;padding:0;">
|
||
<div class="mod-icon" style="--plate-size:62px;width:62px;height:62px;">
|
||
<svg style="width:30px;height:30px;"><use href="#i-bulb"/></svg>
|
||
</div>
|
||
</div>
|
||
<div class="mod-icon is-empty" style="--plate-size:62px;width:62px;height:62px;">
|
||
<svg style="width:18px;height:18px;"><use href="#i-plus"/></svg>
|
||
</div>
|
||
</div>
|
||
<div class="desc">Fault tints to coral. Empty state is dashed — a discoverable invitation, not a hole.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===================================================================== -->
|
||
<div class="section-title"><span class="num">04</span> Coverage <span class="tag" style="--ch-signal:var(--ch-magenta);color:var(--ch-magenta);border-color:color-mix(in srgb, var(--ch-magenta) 30%, transparent);background:color-mix(in srgb, var(--ch-magenta) 10%, transparent);">All Card Types</span></div>
|
||
|
||
<div class="grid">
|
||
|
||
<!-- WLED · LED Strip -->
|
||
<article class="module is-running" data-ch="signal">
|
||
<div class="mod-head mod-head--with-icon">
|
||
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-strip"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
|
||
<div class="mod-id">
|
||
<span class="mod-badge">WLED · OUT</span>
|
||
<div class="mod-name"><span>Couch Underglow</span></div>
|
||
<div class="mod-meta">192.168.1.81 · v0.15</div>
|
||
</div>
|
||
<div class="mod-leds"><span class="led on"></span><span class="led on blink"></span><span class="led on blink"></span></div>
|
||
</div>
|
||
<div class="mod-metrics">
|
||
<div class="mod-metric"><div class="k">PIXELS</div><div class="v">90</div></div>
|
||
<div class="mod-metric"><div class="k">LAT</div><div class="v signal">12<small>ms</small></div></div>
|
||
<div class="mod-metric"><div class="k">CHIP</div><div class="v" style="font-size:.92rem;font-family:var(--font-mono);font-weight:700;letter-spacing:.04em;">SK6812</div></div>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot is-live"></span><span>LIVE · OUT-2</span></div>
|
||
<button class="mod-btn mod-btn-stop"><svg width="12" height="12"><use href="#i-stop"/></svg><span>STOP</span></button>
|
||
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Motherboard (mock-style desktop ambient) -->
|
||
<article class="module" data-ch="cyan">
|
||
<div class="mod-head mod-head--with-icon">
|
||
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-board"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
|
||
<div class="mod-id">
|
||
<span class="mod-badge">OPENRGB · OUT</span>
|
||
<div class="mod-name"><span>ROG Strix Z790-E</span></div>
|
||
<div class="mod-meta">openrgb://localhost:6742/3</div>
|
||
</div>
|
||
<div class="mod-leds"><span class="led on"></span><span class="led on blink"></span><span class="led"></span></div>
|
||
</div>
|
||
<div class="mod-metrics">
|
||
<div class="mod-metric"><div class="k">PIXELS</div><div class="v">22</div></div>
|
||
<div class="mod-metric"><div class="k">LAT</div><div class="v signal">3<small>ms</small></div></div>
|
||
<div class="mod-metric"><div class="k">ZONE</div><div class="v" style="font-size:.92rem;font-family:var(--font-mono);font-weight:700;letter-spacing:.04em;">AURA</div></div>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot is-live"></span><span>STANDBY</span></div>
|
||
<button class="mod-btn mod-btn-go"><span>START</span></button>
|
||
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Mouse -->
|
||
<article class="module" data-ch="cyan">
|
||
<div class="mod-head mod-head--with-icon">
|
||
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-mouse"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
|
||
<div class="mod-id">
|
||
<span class="mod-badge">OPENRGB · OUT</span>
|
||
<div class="mod-name"><span>G502 Lightspeed</span></div>
|
||
<div class="mod-meta">openrgb://localhost:6742/4</div>
|
||
</div>
|
||
<div class="mod-leds"><span class="led on"></span></div>
|
||
</div>
|
||
<div class="mod-metrics mod-metrics--2">
|
||
<div class="mod-metric"><div class="k">PIXELS</div><div class="v">3</div></div>
|
||
<div class="mod-metric"><div class="k">LAT</div><div class="v signal">2<small>ms</small></div></div>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot is-live"></span><span>READY</span></div>
|
||
<button class="mod-btn mod-btn-go"><span>START</span></button>
|
||
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Keyboard -->
|
||
<article class="module is-running" data-ch="cyan">
|
||
<div class="mod-head mod-head--with-icon">
|
||
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-keyboard"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
|
||
<div class="mod-id">
|
||
<span class="mod-badge">OPENRGB · OUT</span>
|
||
<div class="mod-name"><span>Keychron Q1 Pro</span></div>
|
||
<div class="mod-meta">openrgb://localhost:6742/2</div>
|
||
</div>
|
||
<div class="mod-leds"><span class="led on"></span><span class="led on blink"></span><span class="led on"></span></div>
|
||
</div>
|
||
<div class="mod-metrics">
|
||
<div class="mod-metric"><div class="k">PIXELS</div><div class="v">81</div></div>
|
||
<div class="mod-metric"><div class="k">LAT</div><div class="v signal">4<small>ms</small></div></div>
|
||
<div class="mod-metric"><div class="k">ZONE</div><div class="v" style="font-size:.92rem;font-family:var(--font-mono);font-weight:700;letter-spacing:.04em;">PER-KEY</div></div>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot is-live"></span><span>LIVE · OUT-3</span></div>
|
||
<button class="mod-btn mod-btn-stop"><svg width="12" height="12"><use href="#i-stop"/></svg><span>STOP</span></button>
|
||
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Bedside lamp (HA Light target) -->
|
||
<article class="module" data-ch="signal">
|
||
<div class="mod-head mod-head--with-icon">
|
||
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-bulb"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
|
||
<div class="mod-id">
|
||
<span class="mod-badge">HA · LIGHT</span>
|
||
<div class="mod-name"><span>Hue Bedside Lamp</span></div>
|
||
<div class="mod-meta">light.bedside_lamp · 2700K</div>
|
||
</div>
|
||
<div class="mod-leds"><span class="led on"></span></div>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot"></span><span>STANDBY</span></div>
|
||
<button class="mod-btn mod-btn-go"><span>START</span></button>
|
||
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Camera/Screen capture (source) -->
|
||
<article class="module is-running" data-ch="cyan">
|
||
<div class="mod-head mod-head--with-icon">
|
||
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-monitor"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
|
||
<div class="mod-id">
|
||
<span class="mod-badge">SCREEN · IN</span>
|
||
<div class="mod-name"><span>Primary Display</span></div>
|
||
<div class="mod-meta">3440×1440 · DXGI · HDR</div>
|
||
</div>
|
||
<div class="mod-leds"><span class="led on blink"></span><span class="led on"></span></div>
|
||
</div>
|
||
<div class="mod-metrics">
|
||
<div class="mod-metric"><div class="k">FPS</div><div class="v signal">59.7</div></div>
|
||
<div class="mod-metric"><div class="k">RES</div><div class="v" style="font-size:.92rem;font-family:var(--font-mono);font-weight:700;letter-spacing:.04em;">3440</div></div>
|
||
<div class="mod-metric"><div class="k">CPU</div><div class="v">3<small>%</small></div></div>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot is-live"></span><span>CAPTURING</span></div>
|
||
<button class="mod-btn mod-btn-stop"><svg width="12" height="12"><use href="#i-stop"/></svg><span>STOP</span></button>
|
||
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Microphone (audio source) -->
|
||
<article class="module is-running" data-ch="magenta">
|
||
<div class="mod-head mod-head--with-icon">
|
||
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-mic"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
|
||
<div class="mod-id">
|
||
<span class="mod-badge">AUDIO · IN</span>
|
||
<div class="mod-name"><span>Røde NT-USB Mini</span></div>
|
||
<div class="mod-meta">48kHz · stereo · −12.4dB</div>
|
||
</div>
|
||
<div class="mod-leds"><span class="led on blink"></span><span class="led on"></span></div>
|
||
</div>
|
||
<div class="mod-fader">
|
||
<span class="mod-fader__k">Level</span>
|
||
<div class="mod-fader__track"><div class="mod-fader__fill" style="width:62%"></div></div>
|
||
<span class="mod-fader__v">62</span>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot is-live"></span><span>STREAMING</span></div>
|
||
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- GPU (mock) -->
|
||
<article class="module" data-ch="amber">
|
||
<div class="mod-head mod-head--with-icon">
|
||
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-gpu"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
|
||
<div class="mod-id">
|
||
<span class="mod-badge">OPENRGB · OUT</span>
|
||
<div class="mod-name"><span>RTX 4080 Aorus Master</span></div>
|
||
<div class="mod-meta">openrgb://localhost:6742/1</div>
|
||
</div>
|
||
<div class="mod-leds"><span class="led on"></span><span class="led"></span></div>
|
||
</div>
|
||
<div class="mod-metrics mod-metrics--2">
|
||
<div class="mod-metric"><div class="k">PIXELS</div><div class="v">12</div></div>
|
||
<div class="mod-metric"><div class="k">TEMP</div><div class="v">52<small>°C</small></div></div>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot"></span><span>STANDBY</span></div>
|
||
<button class="mod-btn mod-btn-go"><span>START</span></button>
|
||
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Game controller (game integration) -->
|
||
<article class="module" data-ch="amber">
|
||
<div class="mod-head mod-head--with-icon">
|
||
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-controller"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
|
||
<div class="mod-id">
|
||
<span class="mod-badge">GAME · IN</span>
|
||
<div class="mod-name"><span>Cyberpunk 2077</span></div>
|
||
<div class="mod-meta">SDK · health · ammo · radio</div>
|
||
</div>
|
||
<div class="mod-leds"><span class="led on"></span></div>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot"></span><span>WAITING</span></div>
|
||
<button class="mod-btn"><span>HOOK</span></button>
|
||
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Empty/unset card -->
|
||
<article class="module" data-ch="violet">
|
||
<div class="mod-head mod-head--with-icon">
|
||
<button class="mod-icon is-empty" aria-label="Add icon"><svg><use href="#i-plus"/></svg></button>
|
||
<div class="mod-id">
|
||
<span class="mod-badge">CLOCK · SYNC</span>
|
||
<div class="mod-name"><span>Studio Tempo</span></div>
|
||
<div class="mod-meta">120 BPM · 4/4 · master</div>
|
||
</div>
|
||
<div class="mod-leds"><span class="led on blink"></span></div>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot"></span><span>NO ICON</span></div>
|
||
<button class="mod-btn"><span>PICK ICON</span></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Fault state (offline router) -->
|
||
<article class="module" data-ch="coral" data-state="fault" style="opacity:.92;">
|
||
<div class="mod-head mod-head--with-icon">
|
||
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-router"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
|
||
<div class="mod-id">
|
||
<span class="mod-badge">WLED · OUT</span>
|
||
<div class="mod-name"><span>Garage Strip</span></div>
|
||
<div class="mod-meta">10.0.4.18 · timeout</div>
|
||
</div>
|
||
<div class="mod-leds"><span class="led fault"></span></div>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot"></span><span>OFFLINE · 2h 14m</span></div>
|
||
<button class="mod-btn"><span>RETRY</span></button>
|
||
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Speaker (audio source) -->
|
||
<article class="module" data-ch="magenta">
|
||
<div class="mod-head mod-head--with-icon">
|
||
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-speaker"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
|
||
<div class="mod-id">
|
||
<span class="mod-badge">AUDIO · IN</span>
|
||
<div class="mod-name"><span>Studio Monitor L+R</span></div>
|
||
<div class="mod-meta">loopback · 44.1kHz</div>
|
||
</div>
|
||
<div class="mod-leds"><span class="led on"></span></div>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot"></span><span>STANDBY</span></div>
|
||
<button class="mod-btn mod-btn-go"><span>START</span></button>
|
||
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
|
||
<!-- ===================================================================== -->
|
||
<div class="section-title"><span class="num">05</span> Picker <span class="tag" style="--ch-signal:var(--ch-violet);color:var(--ch-violet);border-color:color-mix(in srgb, var(--ch-violet) 30%, transparent);background:color-mix(in srgb, var(--ch-violet) 10%, transparent);">Modal · Open</span></div>
|
||
|
||
<div class="picker-stage">
|
||
<div class="picker">
|
||
<div class="picker-head">
|
||
<div class="preview">
|
||
<div class="mod-icon" data-preview style="--ch:var(--ch-cyan);">
|
||
<svg><use href="#i-board"/></svg>
|
||
</div>
|
||
</div>
|
||
<div class="meta">
|
||
<div class="eyebrow-mini">Card · ROG Strix Z790-E</div>
|
||
<h2>Choose an icon</h2>
|
||
<div class="sub">Tinted with the card's channel · click any tile to apply.</div>
|
||
</div>
|
||
<button class="picker-close" aria-label="Close"><svg><use href="#i-x"/></svg></button>
|
||
</div>
|
||
|
||
<div class="picker-toolbar">
|
||
<label class="picker-search">
|
||
<svg><use href="#i-search"/></svg>
|
||
<input type="text" placeholder="Search · Try "mouse", "ring", "sun"…" value="">
|
||
<kbd>⌘K</kbd>
|
||
</label>
|
||
<button class="color-toggle" type="button">
|
||
<span class="swatch" data-swatch></span>
|
||
<span>Match channel</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="picker-tabs" role="tablist">
|
||
<button class="picker-tab is-active" role="tab" data-cat="all">All <span class="count">42</span></button>
|
||
<button class="picker-tab" role="tab" data-cat="hardware">Hardware <span class="count">10</span></button>
|
||
<button class="picker-tab" role="tab" data-cat="lighting">Lighting <span class="count">7</span></button>
|
||
<button class="picker-tab" role="tab" data-cat="rooms">Rooms <span class="count">7</span></button>
|
||
<button class="picker-tab" role="tab" data-cat="media">Media <span class="count">5</span></button>
|
||
<button class="picker-tab" role="tab" data-cat="signal">Signal <span class="count">5</span></button>
|
||
<button class="picker-tab" role="tab" data-cat="ambience">Ambience <span class="count">8</span></button>
|
||
</div>
|
||
|
||
<div class="picker-recent">
|
||
<span class="label">Recent</span>
|
||
<div class="strip">
|
||
<button class="icon-tile"><svg><use href="#i-mouse"/></svg></button>
|
||
<button class="icon-tile"><svg><use href="#i-keyboard"/></svg></button>
|
||
<button class="icon-tile"><svg><use href="#i-strip"/></svg></button>
|
||
<button class="icon-tile"><svg><use href="#i-bulb"/></svg></button>
|
||
<button class="icon-tile"><svg><use href="#i-monitor"/></svg></button>
|
||
<button class="icon-tile"><svg><use href="#i-mic"/></svg></button>
|
||
<button class="icon-tile"><svg><use href="#i-controller"/></svg></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="picker-grid-wrap">
|
||
<div class="picker-cat">Hardware</div>
|
||
<div class="picker-grid">
|
||
<button class="icon-tile" title="Motherboard"><svg><use href="#i-board"/></svg></button>
|
||
<button class="icon-tile is-selected" title="CPU"><svg><use href="#i-cpu"/></svg></button>
|
||
<button class="icon-tile" title="GPU"><svg><use href="#i-gpu"/></svg></button>
|
||
<button class="icon-tile" title="RAM"><svg><use href="#i-ram"/></svg></button>
|
||
<button class="icon-tile" title="SSD"><svg><use href="#i-ssd"/></svg></button>
|
||
<button class="icon-tile" title="Mouse"><svg><use href="#i-mouse"/></svg></button>
|
||
<button class="icon-tile" title="Keyboard"><svg><use href="#i-keyboard"/></svg></button>
|
||
<button class="icon-tile" title="Controller"><svg><use href="#i-controller"/></svg></button>
|
||
<button class="icon-tile" title="Headphones"><svg><use href="#i-headphones"/></svg></button>
|
||
<button class="icon-tile" title="Server"><svg><use href="#i-server"/></svg></button>
|
||
</div>
|
||
|
||
<div class="picker-cat">Lighting</div>
|
||
<div class="picker-grid">
|
||
<button class="icon-tile" title="Bulb"><svg><use href="#i-bulb"/></svg></button>
|
||
<button class="icon-tile" title="LED Strip"><svg><use href="#i-strip"/></svg></button>
|
||
<button class="icon-tile" title="LED Panel"><svg><use href="#i-panel"/></svg></button>
|
||
<button class="icon-tile" title="Halo Ring"><svg><use href="#i-ring"/></svg></button>
|
||
<button class="icon-tile" title="Floor Lamp"><svg><use href="#i-lamp"/></svg></button>
|
||
<button class="icon-tile" title="Spotlight"><svg><use href="#i-spot"/></svg></button>
|
||
<button class="icon-tile" title="Power"><svg><use href="#i-power"/></svg></button>
|
||
</div>
|
||
|
||
<div class="picker-cat">Rooms & Furniture</div>
|
||
<div class="picker-grid">
|
||
<button class="icon-tile" title="Bed"><svg><use href="#i-bed"/></svg></button>
|
||
<button class="icon-tile" title="Sofa"><svg><use href="#i-sofa"/></svg></button>
|
||
<button class="icon-tile" title="Desk"><svg><use href="#i-desk"/></svg></button>
|
||
<button class="icon-tile" title="Window"><svg><use href="#i-window"/></svg></button>
|
||
<button class="icon-tile" title="Door"><svg><use href="#i-door"/></svg></button>
|
||
<button class="icon-tile" title="Fan"><svg><use href="#i-fan"/></svg></button>
|
||
<button class="icon-tile" title="Thermostat"><svg><use href="#i-thermo"/></svg></button>
|
||
</div>
|
||
|
||
<div class="picker-cat">Media</div>
|
||
<div class="picker-grid">
|
||
<button class="icon-tile" title="Monitor"><svg><use href="#i-monitor"/></svg></button>
|
||
<button class="icon-tile" title="TV"><svg><use href="#i-tv"/></svg></button>
|
||
<button class="icon-tile" title="Camera"><svg><use href="#i-camera"/></svg></button>
|
||
<button class="icon-tile" title="Mic"><svg><use href="#i-mic"/></svg></button>
|
||
<button class="icon-tile" title="Speaker"><svg><use href="#i-speaker"/></svg></button>
|
||
</div>
|
||
|
||
<div class="picker-cat">Signal</div>
|
||
<div class="picker-grid">
|
||
<button class="icon-tile" title="WiFi"><svg><use href="#i-wifi"/></svg></button>
|
||
<button class="icon-tile" title="Bluetooth"><svg><use href="#i-bt"/></svg></button>
|
||
<button class="icon-tile" title="Antenna"><svg><use href="#i-antenna"/></svg></button>
|
||
<button class="icon-tile" title="Router"><svg><use href="#i-router"/></svg></button>
|
||
<button class="icon-tile" title="Cloud"><svg><use href="#i-cloud"/></svg></button>
|
||
</div>
|
||
|
||
<div class="picker-cat">Ambience</div>
|
||
<div class="picker-grid">
|
||
<button class="icon-tile" title="Sun"><svg><use href="#i-sun"/></svg></button>
|
||
<button class="icon-tile" title="Moon"><svg><use href="#i-moon"/></svg></button>
|
||
<button class="icon-tile" title="Flame"><svg><use href="#i-flame"/></svg></button>
|
||
<button class="icon-tile" title="Leaf"><svg><use href="#i-leaf"/></svg></button>
|
||
<button class="icon-tile" title="Music"><svg><use href="#i-music"/></svg></button>
|
||
<button class="icon-tile" title="Film"><svg><use href="#i-film"/></svg></button>
|
||
<button class="icon-tile" title="Home"><svg><use href="#i-ha"/></svg></button>
|
||
<button class="icon-tile" title="TV"><svg><use href="#i-tv"/></svg></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="picker-foot">
|
||
<div class="hint"><kbd>↵</kbd> Apply · <kbd>Esc</kbd> Cancel</div>
|
||
<button class="picker-btn picker-btn--danger">Remove icon</button>
|
||
<button class="picker-btn picker-btn--ghost">Cancel</button>
|
||
<button class="picker-btn picker-btn--primary">Apply</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===================================================================== -->
|
||
<aside class="notes">
|
||
<h2>Implementation hints</h2>
|
||
<ul>
|
||
<li><strong>Schema:</strong> add <code>icon: str | None</code> and <code>icon_color: str | None</code> to the entity dataclass and Pydantic schema. Null on both → today's behaviour, no plate, no migration risk.</li>
|
||
<li><strong>Icon library:</strong> ship a curated subset of <code>lucide</code> as inline SVG <code><symbol></code>s in <code>icon-library.svg</code> — referenced by id (<code><use href="#i-mouse"/></code>). Tree-shaken at build time, no runtime import.</li>
|
||
<li><strong>Render slot:</strong> a new optional <code>head.icon: { id: string, color?: string }</code> on <code>ModHeadOpts</code>. <code>renderModHead</code> emits the plate before <code>.mod-id</code> when present and adds <code>mod-head--with-icon</code> for the head row alignment tweak.</li>
|
||
<li><strong>Picker entry points:</strong> ① click the plate on a card · ② new "Icon" row in <code>device-settings</code> modal · ③ <code>data-i18n</code> action in the kebab menu (“Change icon…”).</li>
|
||
<li><strong>Persistence:</strong> stored on the entity itself, not in a separate file — same JSON store, no migration needed for null values.</li>
|
||
<li><strong>Dashboard convergence:</strong> <code>.dashboard-target</code> already uses <code>.mod-head</code>, so the plate renders identically there. Smaller plate on <code>.perf-chart-card</code> via the <code>--plate-size</code> override.</li>
|
||
<li><strong>Search index:</strong> tag each icon with synonyms ("ssd → drive, disk, storage") so localisation lands cleanly through <code>t('icon.ssd.aliases')</code>.</li>
|
||
<li><strong>Keyboard:</strong> picker is <code>role="dialog"</code>; focus trap, <kbd>↵</kbd> applies, <kbd>Esc</kbd> cancels, <kbd>⌘K</kbd> jumps to search. Arrow-key grid navigation matches the existing <code>IconSelect</code> component.</li>
|
||
</ul>
|
||
</aside>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
(() => {
|
||
// Theme toggle
|
||
const root = document.documentElement;
|
||
document.querySelectorAll('[data-theme-set]').forEach(b => {
|
||
b.addEventListener('click', () => {
|
||
const t = b.dataset.themeSet;
|
||
root.dataset.theme = t;
|
||
document.querySelectorAll('[data-theme-set]').forEach(x => x.classList.toggle('is-active', x === b));
|
||
});
|
||
});
|
||
|
||
// Picker — clicking a tile updates preview + selection
|
||
const grid = document.querySelector('.picker-grid-wrap');
|
||
const preview = document.querySelector('[data-preview] svg use');
|
||
const swatch = document.querySelector('[data-swatch]');
|
||
const channels = ['var(--ch-cyan)', 'var(--ch-signal)', 'var(--ch-magenta)', 'var(--ch-amber)', 'var(--ch-violet)', 'var(--ch-coral)'];
|
||
let chIdx = 0;
|
||
|
||
grid.addEventListener('click', (e) => {
|
||
const tile = e.target.closest('.icon-tile');
|
||
if (!tile) return;
|
||
grid.querySelectorAll('.icon-tile.is-selected').forEach(x => x.classList.remove('is-selected'));
|
||
tile.classList.add('is-selected');
|
||
const useEl = tile.querySelector('svg use');
|
||
if (useEl && preview) preview.setAttribute('href', useEl.getAttribute('href'));
|
||
});
|
||
|
||
// Color toggle cycles channel for the preview
|
||
document.querySelector('.color-toggle')?.addEventListener('click', () => {
|
||
chIdx = (chIdx + 1) % channels.length;
|
||
const ch = channels[chIdx];
|
||
const previewPlate = document.querySelector('[data-preview]');
|
||
if (previewPlate) previewPlate.style.setProperty('--ch', ch);
|
||
if (swatch) swatch.style.setProperty('background', ch);
|
||
});
|
||
|
||
// Tabs (visual only)
|
||
document.querySelectorAll('.picker-tab').forEach(t => {
|
||
t.addEventListener('click', () => {
|
||
document.querySelectorAll('.picker-tab').forEach(x => x.classList.toggle('is-active', x === t));
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|