a56569b02f
Dashboard cards (mod-card system) - New mod-card / mod-menu modules backing dashboard cards - Reworked card colors, sections, dashboard layout, perf charts - Channel-stripe styling, hairline borders, signal-flow animation on running cards, refined metric grid Multiselect bulk toolbar - Replaced tri-state checkbox with explicit Select-all / Deselect-all icon buttons; both disable when not applicable - Dim + slight blur on non-selected siblings during selection mode so the active picks pop; selected card gains a subtle lift + primary-color glow halo - Bulk tick uses ICON_CHECK from the icon registry (was U+2713) and scale-pops in via a cubic-bezier overshoot keyframe - Toolbar restyled with luxury gradient bg, top accent stripe, glass blur, neon hover glows on each button group Settings modal - Tab bar converted to icon-only (cog / hard-drive / bell / palette / refresh / help) so labels never overflow at any locale; title and aria-label preserve translated names. Tabs distribute evenly via flex: 1 1 0 + space-around — no overflow possible - IconSelect auto-populates <option> elements when the underlying select is empty, fixing the blank notification triggers (root cause: setting .value on an empty select is a no-op) - Tab activation calls scrollIntoView on the active button as a safety net for narrow viewports Modal exit animation - Added symmetric fadeOut + slideDown keyframes; .modal.closing applies them with animation-fill-mode: forwards - Modal.forceClose() defers display:none until animationend (with timer fallback). State cleanup (focus, body lock, stack) runs immediately so callers querying state get correct values - isOpen returns false during the close animation; open() cancels any in-flight close so re-open works during the animation - prefers-reduced-motion disables all modal animations Locale picker - Dropped redundant English/Русский/中文 long-form labels — picker now shows only EN / RU / ZH - IconSelect trigger/cell hides empty icon/label slots via :empty so the layout collapses cleanly for minimal items Filter input (cards section) - Embedded magnifier icon via data URI (no HTML change); monospace uppercase placeholder, lux-bg-0 background, neon focus ring with inset shadow + outer glow - Reset button only shows when the input has content (CSS-only via :placeholder-shown sibling selector — JS-resilient) Snack toast - Glass background (gradient + backdrop-blur) with top channel-color accent stripe matching the modal/toolbar language - Per-type --toast-ch drives border/glow/timer color (success → primary, error → danger, info → info) - Undo button gets a tinted hover with channel-color halo Top header toolbar - Removed hairline border from .header-btn for a flatter look; hover keeps the subtle background tint and primary-color glow Device URL hyperlink - Styled .mod-meta__link to pick up the card's --ch accent (instead of inheriting browser-blue underline). Dotted underline at rest solidifies on hover; soft text-shadow glow; web icon dims at rest, brightens on hover Misc - ICON_CHECK and ICON_HARD_DRIVE added to the icon registry - Existing card-redesign demos checked in under docs/ - Removed obsolete docs/plans/device-typed-configs.md
1421 lines
67 KiB
HTML
1421 lines
67 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 · Entity Card · v2 (dashboard convergence)</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>
|
||
/* =====================================================================
|
||
ENTITY CARD v2 — propagates the existing .mod-* vocabulary from
|
||
dashboard.css to the entity-card grid. Same classes, same tokens,
|
||
same instrument-readout aesthetic. Net change: a single card system
|
||
spanning Dashboard, Targets, Sources, Profiles tabs.
|
||
===================================================================== */
|
||
|
||
: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;
|
||
|
||
/* channel palette — verbatim from base.css */
|
||
--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);
|
||
--primary-contrast:#fff;
|
||
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;
|
||
--primary-text:#2e7d32;
|
||
--primary-contrast:#fff;
|
||
--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;}
|
||
|
||
/* =================== Demo chrome =================== */
|
||
.page{max-width:1320px;margin:0 auto;}
|
||
|
||
.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:120px;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 5px 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);
|
||
font-family:var(--font-display);
|
||
}
|
||
.lede{
|
||
color:var(--lux-ink-dim);max-width:50ch;font-size:.95rem;line-height:1.55;
|
||
margin-top:14px;
|
||
}
|
||
.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;padding:0;
|
||
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:48px 0 18px;
|
||
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;
|
||
}
|
||
|
||
.grid{
|
||
display:grid;
|
||
grid-template-columns:repeat(auto-fill,minmax(min(370px,100%),1fr));
|
||
gap:14px;
|
||
}
|
||
|
||
/* =====================================================================
|
||
.module — entity-card shell, mirrors `.dashboard-target:has(.mod-head)`
|
||
from dashboard.css. Same class names below: .mod-head/.mod-id/...
|
||
===================================================================== */
|
||
.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;
|
||
}
|
||
|
||
/* Always-on left stripe — verbatim from .dashboard-target::before */
|
||
.module::before{
|
||
content:'';
|
||
position:absolute;left:0;top:0;bottom:0;width:3px;
|
||
background:var(--ch);
|
||
box-shadow:0 0 8px color-mix(in srgb, var(--ch) 40%, transparent);
|
||
opacity:.6;
|
||
transition:opacity .2s ease, box-shadow .2s ease, width .2s ease;
|
||
}
|
||
/* Silkscreened corner bracket — only on idle, only when there's no
|
||
kebab menu already occupying the corner. The kebab visually
|
||
replaces it for any card that has interactive controls. */
|
||
.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;
|
||
}
|
||
/* Modules with a head-row kebab don't get the idle bracket — too
|
||
much corner clutter. Running cards still get the bottom signal-
|
||
flow because that uses the same ::after at a different position. */
|
||
.module:has(.mod-menu-wrap):not(.is-running)::after{display:none;}
|
||
|
||
.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);
|
||
}
|
||
|
||
/* Running state — stripe widens, corner bracket → bottom signal-flow */
|
||
.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.is-running::after{
|
||
top:auto;right:auto;left:4px;bottom:0;
|
||
width:calc(100% - 4px);height:2px;
|
||
border:none;opacity:.7;
|
||
background:linear-gradient(90deg,
|
||
transparent 0%,
|
||
color-mix(in srgb, var(--ch) 85%, transparent) 50%,
|
||
transparent 100%);
|
||
background-size:30% 100%;background-repeat:no-repeat;
|
||
animation:signalFlow 2.4s linear infinite;
|
||
}
|
||
@keyframes signalFlow{
|
||
0%{background-position:-30% 0;}
|
||
100%{background-position:130% 0;}
|
||
}
|
||
|
||
/* Channel assignments — match dashboard.css mappings */
|
||
.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 — verbatim from dashboard.css with one tweak: drop
|
||
space-between so a third flex child (the kebab menu) can sit
|
||
alongside .mod-leds without the layout fighting itself. ─────── */
|
||
.mod-head{
|
||
display:flex;
|
||
align-items:flex-start;
|
||
gap:10px;
|
||
}
|
||
.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 2px 4px;
|
||
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;
|
||
position:relative;
|
||
}
|
||
/* Channel color marker — leading dot inside the badge. IS the card-color
|
||
picker trigger. Identity, not an action — lives with the data it labels. */
|
||
.mod-badge__color{
|
||
width:10px;height:10px;border-radius:50%;flex-shrink:0;
|
||
background:var(--ch);
|
||
border:var(--lux-hairline) solid color-mix(in srgb, var(--ch) 60%, var(--lux-line-bold));
|
||
cursor:pointer;
|
||
transition:transform .15s var(--ease), box-shadow .15s;
|
||
box-shadow:0 0 0 0 color-mix(in srgb, var(--ch) 40%, transparent);
|
||
}
|
||
.mod-badge__color:hover{
|
||
transform:scale(1.25);
|
||
box-shadow:0 0 0 3px color-mix(in srgb, var(--ch) 25%, transparent);
|
||
}
|
||
/* Custom-color override (user picked a personal hue) — swatch keeps its
|
||
channel-tint ring but fills with the picked color (set inline). */
|
||
.mod-badge__color[data-custom]{
|
||
background:var(--user-color);
|
||
}
|
||
.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-name > span:first-child{
|
||
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;
|
||
}
|
||
.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 — recessed bezel with multiple LEDs ─────────────── */
|
||
.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);
|
||
transition:background .2s, box-shadow .2s;
|
||
}
|
||
.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 — instrument-style metric grid ────────────────── */
|
||
.mod-metrics{
|
||
display:grid;
|
||
grid-template-columns:1.2fr 1fr 1fr;
|
||
gap:0;
|
||
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;position:relative;
|
||
}
|
||
.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);
|
||
display:inline-flex;align-items:center;gap:4px;
|
||
}
|
||
.mod-metric .k > svg{width:10px;height:10px;flex-shrink:0;opacity:.85;}
|
||
/* THE big numeric readout — display font, instrument-grade */
|
||
.mod-metric .v{
|
||
font-family:var(--font-display);
|
||
font-size:2rem;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.has-errors .v,
|
||
.mod-metric.has-errors .k,
|
||
.mod-metric.has-errors .k svg{color:var(--ch-coral);}
|
||
.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;
|
||
}
|
||
|
||
/* Two-cell metric variant (when a card only has 2 readouts) */
|
||
.mod-metrics--2{grid-template-columns:1fr 1fr;}
|
||
/* Single-cell variant (e.g. just one big readout) */
|
||
.mod-metrics--1{grid-template-columns:1fr;}
|
||
|
||
/* ── .mod-foot — patch indicator + action buttons ───────────────── */
|
||
.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 — verbatim from dashboard.css */
|
||
.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;min-width:0;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-go{
|
||
background:var(--ch);
|
||
color:var(--primary-contrast);
|
||
border-color:var(--ch);
|
||
box-shadow:0 0 14px color-mix(in srgb, var(--ch) 35%, transparent);
|
||
}
|
||
.mod-btn-go:hover{filter:brightness(1.1);color:var(--primary-contrast);}
|
||
.mod-btn-stop{
|
||
color:var(--ch-coral);
|
||
border-color:color-mix(in srgb, var(--ch-coral) 40%, transparent);
|
||
}
|
||
.mod-btn-stop:hover{
|
||
background:color-mix(in srgb, var(--ch-coral) 12%, transparent);
|
||
color:var(--ch-coral);
|
||
}
|
||
.mod-btn svg{width:12px;height:12px;}
|
||
.mod-btn-icon{
|
||
padding:7px 9px; /* square variant for tertiary actions */
|
||
}
|
||
.mod-btn-icon svg{width:14px;height:14px;}
|
||
|
||
/* ── Overflow menu — sits in the head row as a flex sibling of
|
||
.mod-leds, NOT absolutely positioned over the LED bezel.
|
||
This is the v2.1 fix: the kebab and the LED cluster live
|
||
side-by-side in the same band, never overlapping. ────────── */
|
||
.mod-menu-wrap{
|
||
position:relative;
|
||
flex-shrink:0;
|
||
align-self:flex-start;
|
||
}
|
||
.mod-menu-btn{
|
||
appearance:none;background:transparent;border:none;
|
||
width:26px;height:26px;border-radius:3px;
|
||
color:var(--lux-ink-mute);
|
||
display:inline-flex;align-items:center;justify-content:center;
|
||
cursor:pointer;transition:.15s;
|
||
opacity:.45; /* persistent but quiet */
|
||
}
|
||
.module:hover .mod-menu-btn,
|
||
.mod-menu-wrap.is-open .mod-menu-btn{
|
||
opacity:1;
|
||
}
|
||
.mod-menu-btn:hover,
|
||
.mod-menu-wrap.is-open .mod-menu-btn{
|
||
color:var(--lux-ink);
|
||
background:var(--lux-bg-3);
|
||
}
|
||
.mod-menu-btn:focus-visible{
|
||
outline:none;
|
||
box-shadow:0 0 0 2px color-mix(in srgb, var(--ch) 50%, transparent);
|
||
}
|
||
.mod-menu-btn svg{width:16px;height:16px;}
|
||
|
||
/* The menu itself — rendered as position:fixed so it floats above
|
||
the card's overflow:hidden boundary (needed so short cards like
|
||
gradients don't clip the dropdown). JS anchors it to the kebab
|
||
button at open time, and any scroll/resize closes it. */
|
||
.mod-menu{
|
||
position:fixed;z-index:100;min-width:172px;
|
||
background:var(--lux-bg-1);
|
||
border:var(--lux-hairline) solid var(--lux-line-bold);
|
||
border-radius:var(--lux-r-sm);
|
||
box-shadow:0 12px 32px rgba(0,0,0,.45),
|
||
0 0 0 1px rgba(255,255,255,.02);
|
||
padding:4px;
|
||
display:none;
|
||
flex-direction:column;
|
||
gap:0;
|
||
transform-origin:top right;
|
||
animation:menuIn .12s var(--ease);
|
||
}
|
||
.mod-menu-wrap.is-open .mod-menu{display:flex;}
|
||
@keyframes menuIn{
|
||
from{opacity:0;transform:scale(.96) translateY(-4px);}
|
||
to{opacity:1;transform:none;}
|
||
}
|
||
.mod-menu__item{
|
||
appearance:none;background:transparent;border:none;text-align:left;
|
||
display:flex;align-items:center;gap:10px;
|
||
padding:8px 12px;border-radius:2px;
|
||
font-family:var(--font-mono);
|
||
font-size:.66rem;font-weight:500;
|
||
letter-spacing:.12em;text-transform:uppercase;
|
||
color:var(--lux-ink-dim);
|
||
cursor:pointer;transition:.12s;
|
||
white-space:nowrap;
|
||
width:100%;
|
||
}
|
||
.mod-menu__item:hover{
|
||
background:var(--lux-bg-3);
|
||
color:var(--lux-ink);
|
||
}
|
||
.mod-menu__item svg{width:14px;height:14px;flex-shrink:0;color:var(--lux-ink-mute);}
|
||
.mod-menu__item:hover svg{color:var(--lux-ink);}
|
||
.mod-menu__item kbd{
|
||
margin-left:auto;
|
||
font-family:var(--font-mono);font-size:.6rem;
|
||
color:var(--lux-ink-faint);letter-spacing:.04em;
|
||
}
|
||
.mod-menu__sep{
|
||
height:1px;margin:4px 6px;background:var(--lux-line);
|
||
}
|
||
.mod-menu__item--danger{color:var(--ch-coral);}
|
||
.mod-menu__item--danger svg{color:var(--ch-coral);opacity:.85;}
|
||
.mod-menu__item--danger:hover{
|
||
background:color-mix(in srgb, var(--ch-coral) 12%, transparent);
|
||
color:var(--ch-coral);
|
||
}
|
||
.mod-menu__item--danger:hover svg{color:var(--ch-coral);opacity:1;}
|
||
|
||
/* Hidden card — entire module dimmed + dashed border, the menu's
|
||
"Show" item replaces "Hide" */
|
||
.module.is-hidden{
|
||
opacity:.55;
|
||
border-style:dashed;
|
||
}
|
||
|
||
/* ── Specialised slots ─────────────────────────────────────────── */
|
||
|
||
/* Property chips — used by template/source cards in lieu of metrics
|
||
when the data is qualitative (filter chains, tags, crosslinks).
|
||
Replaces both .card-meta and .stream-card-prop with one system. */
|
||
.mod-chips{display:flex;flex-wrap:wrap;align-items:center;gap:6px;}
|
||
.chip{
|
||
display:inline-flex;align-items:center;gap:6px;
|
||
font-family:var(--font-mono);font-size:.66rem;font-weight:500;
|
||
letter-spacing:.04em;color:var(--lux-ink-dim);
|
||
background:transparent;
|
||
border:var(--lux-hairline) solid var(--lux-line);
|
||
padding:3px 9px;border-radius:999px;
|
||
cursor:default;transition:.15s;
|
||
}
|
||
.chip svg{width:11px;height:11px;flex-shrink:0;color:var(--ch);}
|
||
.chip--link{cursor:pointer;}
|
||
.chip--link:hover{
|
||
color:var(--lux-ink);
|
||
border-color:color-mix(in srgb, var(--ch) 40%, transparent);
|
||
background:color-mix(in srgb, var(--ch) 12%, transparent);
|
||
}
|
||
.chip--tag{
|
||
color:var(--ch);
|
||
border-color:color-mix(in srgb, var(--ch) 22%, transparent);
|
||
background:color-mix(in srgb, var(--ch) 10%, transparent);
|
||
}
|
||
.chip--err{
|
||
color:var(--ch-coral);
|
||
border-color:color-mix(in srgb, var(--ch-coral) 30%, transparent);
|
||
background:color-mix(in srgb, var(--ch-coral) 10%, transparent);
|
||
}
|
||
.chain-arrow{color:var(--ch);opacity:.65;font-size:.72rem;}
|
||
|
||
/* Brightness fader (for LED targets/devices) — channel-tinted */
|
||
.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;
|
||
}
|
||
|
||
/* Preview surface — gradient strip / asset thumb / live LED preview */
|
||
.mod-preview{
|
||
height:28px;border-radius:var(--lux-r-sm);
|
||
box-shadow:inset 0 0 0 1px var(--lux-line);
|
||
overflow:hidden;position:relative;
|
||
}
|
||
.mod-preview--tall{height:64px;}
|
||
.mod-preview__tag{
|
||
position:absolute;left:6px;bottom:4px;
|
||
font-family:var(--font-mono);font-size:.55rem;letter-spacing:.18em;
|
||
color:#fff;text-shadow:0 1px 2px #000;
|
||
}
|
||
|
||
/* Body description / supporting copy (small, dim) */
|
||
.mod-desc{
|
||
font-size:.82rem;color:var(--lux-ink-dim);line-height:1.45;
|
||
}
|
||
|
||
/* Drag handle — persistent low-key */
|
||
.mod-drag{
|
||
position:absolute;left:6px;top:8px;
|
||
width:10px;height:18px;cursor:grab;z-index:3;
|
||
display:flex;flex-direction:column;justify-content:space-between;
|
||
opacity:.3;transition:opacity .15s;
|
||
}
|
||
.module:hover .mod-drag{opacity:.85;}
|
||
.mod-drag span{display:block;width:100%;height:1px;background:var(--lux-ink-mute);}
|
||
|
||
/* ===================== NOTES ===================== */
|
||
.notes{
|
||
margin-top:64px;padding:28px 32px;
|
||
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-magenta),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);
|
||
}
|
||
.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;
|
||
margin-left:8px;vertical-align:middle;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
|
||
<header class="page-head">
|
||
<div>
|
||
<div class="eyebrow">PROPOSAL · v2 · Convergence</div>
|
||
<h1>One card system,<br>the dashboard's <em>vocabulary</em>.</h1>
|
||
<p class="lede">v2 drops the parallel "patchbay rail" I sketched in v1 and adopts the existing <code>.mod-head</code> / <code>.mod-leds</code> / <code>.mod-metrics</code> / <code>.mod-patch</code> / <code>.mod-btn</code> classes from <code>dashboard.css</code> verbatim. Same instrument-style numerics, same recessed LED clusters, same patch indicator. Entity cards inherit dashboard polish; nothing new gets invented.</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> Output zone <span class="tag">CH · SIGNAL</span></div>
|
||
|
||
<div class="grid">
|
||
|
||
<!-- LED device, online + running -->
|
||
<article class="module is-running" data-ch="signal">
|
||
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="mod-corner-actions">
|
||
<span class="swatch" title="Channel color"></span>
|
||
<button class="mod-icon-btn" title="Hide"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg></button>
|
||
<button class="mod-icon-btn mod-icon-btn--danger" title="Delete"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
|
||
</div>
|
||
<div class="mod-head">
|
||
<div class="mod-id">
|
||
<span class="mod-badge">LED · CH-01</span>
|
||
<div class="mod-name"><span>Living Room Strip</span></div>
|
||
<div class="mod-meta">192.168.1.42 · WLED v0.14 · RGB</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"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h4l3-9 4 18 3-9h4"/></svg> FPS</div>
|
||
<div class="v signal">59.7</div>
|
||
</div>
|
||
<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">8<small>ms</small></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>PATCHED · OUT-1</span></div>
|
||
<button class="mod-btn mod-btn-stop"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="6" width="12" height="12" rx="1"/></svg> <span>STOP</span></button>
|
||
<button class="mod-btn mod-btn-icon" title="Settings"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- LED target (offline) -->
|
||
<article class="module" data-ch="signal" style="opacity:.78;">
|
||
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="mod-corner-actions">
|
||
<span class="swatch"></span>
|
||
<button class="mod-icon-btn mod-icon-btn--danger" title="Delete"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
|
||
</div>
|
||
<div class="mod-head">
|
||
<div class="mod-id">
|
||
<span class="mod-badge">LED · CH-02</span>
|
||
<div class="mod-name"><span>Bedroom Halo</span></div>
|
||
<div class="mod-meta">10.0.4.18 · Adalight 921k</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led fault"></span>
|
||
<span class="led"></span>
|
||
<span class="led"></span>
|
||
</div>
|
||
</div>
|
||
<div class="mod-metrics">
|
||
<div class="mod-metric">
|
||
<div class="k">FPS</div>
|
||
<div class="v" style="color:var(--lux-ink-mute);">— —</div>
|
||
</div>
|
||
<div class="mod-metric">
|
||
<div class="k">PIXELS</div>
|
||
<div class="v" style="color:var(--lux-ink-dim);">60</div>
|
||
</div>
|
||
<div class="mod-metric has-errors">
|
||
<div class="k"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> ERR</div>
|
||
<div class="v">14</div>
|
||
</div>
|
||
</div>
|
||
<div class="mod-chips">
|
||
<span class="chip chip--err">Connection refused · 2h 14m ago</span>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot"></span><span>OFFLINE</span></div>
|
||
<button class="mod-btn">RETRY</button>
|
||
<button class="mod-btn mod-btn-icon" title="Settings"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6m11-7h-6m-6 0H1"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- HA Light Target -->
|
||
<article class="module" data-ch="signal">
|
||
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="mod-corner-actions">
|
||
<span class="swatch"></span>
|
||
<button class="mod-icon-btn mod-icon-btn--danger" title="Delete"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
|
||
</div>
|
||
<div class="mod-head">
|
||
<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 · color_temp 2700K</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led on"></span>
|
||
</div>
|
||
</div>
|
||
<div class="mod-chips">
|
||
<span class="chip chip--link"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82M12 1v6m0 6v6"/></svg> Source · Cinematic</span>
|
||
<span class="chip chip--link">Brightness · Outdoor temp</span>
|
||
</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"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg> <span>START</span></button>
|
||
</div>
|
||
</article>
|
||
|
||
</div>
|
||
|
||
<div class="section-title"><span class="num">02</span> Input zone <span class="tag" style="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);">CH · CYAN</span> <span class="tag" style="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);">CH · MAGENTA</span></div>
|
||
|
||
<div class="grid">
|
||
|
||
<!-- Picture source -->
|
||
<article class="module" data-ch="cyan">
|
||
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="mod-corner-actions">
|
||
<span class="swatch"></span>
|
||
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
|
||
</div>
|
||
<div class="mod-head">
|
||
<div class="mod-id">
|
||
<span class="mod-badge">SCREEN · IN</span>
|
||
<div class="mod-name"><span>Cinematic Capture</span></div>
|
||
<div class="mod-meta">Display 2 · MSS · BGRA</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led"></span>
|
||
<span class="led"></span>
|
||
</div>
|
||
</div>
|
||
<div class="mod-metrics mod-metrics--2">
|
||
<div class="mod-metric">
|
||
<div class="k">REGION</div>
|
||
<div class="v">3840×<wbr>1080</div>
|
||
</div>
|
||
<div class="mod-metric">
|
||
<div class="k">TARGET</div>
|
||
<div class="v">60<small>fps</small></div>
|
||
</div>
|
||
</div>
|
||
<div class="mod-chips">
|
||
<span class="chip chip--link">Pre-process · Cinematic CSPT</span>
|
||
<span class="chip">Letterbox crop</span>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot"></span><span>STANDBY</span></div>
|
||
<button class="mod-btn">TEST</button>
|
||
<button class="mod-btn mod-btn-icon" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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-4 9.5-9.5z"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Audio FFT (running) -->
|
||
<article class="module is-running" data-ch="magenta">
|
||
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="mod-corner-actions">
|
||
<span class="swatch"></span>
|
||
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
|
||
</div>
|
||
<div class="mod-head">
|
||
<div class="mod-id">
|
||
<span class="mod-badge">FFT · IN</span>
|
||
<div class="mod-name"><span>Spotify Loopback</span></div>
|
||
<div class="mod-meta">WASAPI · 48 kHz · stereo</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led on blink"></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">PEAK</div>
|
||
<div class="v signal">-6.2<small>dB</small></div>
|
||
</div>
|
||
<div class="mod-metric">
|
||
<div class="k">BANDS</div>
|
||
<div class="v">32</div>
|
||
</div>
|
||
<div class="mod-metric">
|
||
<div class="k">CPU</div>
|
||
<div class="v">3.1<small>%</small></div>
|
||
</div>
|
||
</div>
|
||
<div class="mod-chips">
|
||
<span class="chip chip--tag">bass</span>
|
||
<span class="chip chip--tag">mids</span>
|
||
<span class="chip chip--tag">highs</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-stop"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="6" width="12" height="12" rx="1"/></svg> <span>STOP</span></button>
|
||
<button class="mod-btn mod-btn-icon" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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-4 9.5-9.5z"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Value source HA -->
|
||
<article class="module" data-ch="cyan">
|
||
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="mod-corner-actions">
|
||
<span class="swatch"></span>
|
||
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
|
||
</div>
|
||
<div class="mod-head">
|
||
<div class="mod-id">
|
||
<span class="mod-badge">VALUE · HA</span>
|
||
<div class="mod-name"><span>Outdoor temp</span></div>
|
||
<div class="mod-meta">sensor.outdoor_temp · linear · clamped</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led on"></span>
|
||
</div>
|
||
</div>
|
||
<div class="mod-metrics">
|
||
<div class="mod-metric">
|
||
<div class="k">NOW</div>
|
||
<div class="v signal">14.7<small>°</small></div>
|
||
</div>
|
||
<div class="mod-metric">
|
||
<div class="k">RANGE</div>
|
||
<div class="v">8 — 22</div>
|
||
</div>
|
||
<div class="mod-metric">
|
||
<div class="k">TICK</div>
|
||
<div class="v">2<small>s</small></div>
|
||
</div>
|
||
</div>
|
||
<div class="mod-chips">
|
||
<span class="chip">Bound to · brightness</span>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot is-live"></span><span>POLLING</span></div>
|
||
<button class="mod-btn mod-btn-icon" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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-4 9.5-9.5z"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
</div>
|
||
|
||
<div class="section-title"><span class="num">03</span> Logic zone <span class="tag" style="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);">CH · VIOLET</span> <span class="tag" style="color:var(--ch-amber);border-color:color-mix(in srgb, var(--ch-amber) 30%, transparent);background:color-mix(in srgb, var(--ch-amber) 10%, transparent);">CH · AMBER</span></div>
|
||
|
||
<div class="grid">
|
||
|
||
<!-- Automation -->
|
||
<article class="module" data-ch="violet">
|
||
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="mod-corner-actions">
|
||
<span class="swatch"></span>
|
||
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
|
||
</div>
|
||
<div class="mod-head">
|
||
<div class="mod-id">
|
||
<span class="mod-badge">AUTO · 07</span>
|
||
<div class="mod-name"><span>Movie Night</span></div>
|
||
<div class="mod-meta">Plex playing · 21:00 — 23:30</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led on"></span>
|
||
<span class="led"></span>
|
||
</div>
|
||
</div>
|
||
<div class="mod-chips">
|
||
<span class="chip chip--link"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> 21:00 — 23:30</span>
|
||
<span class="chain-arrow">+</span>
|
||
<span class="chip chip--link">Plex playing</span>
|
||
<span class="chain-arrow">→</span>
|
||
<span class="chip chip--link"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg> Cinema scene</span>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot is-live"></span><span>ARMED</span></div>
|
||
<button class="mod-btn mod-btn-stop">DISABLE</button>
|
||
<button class="mod-btn mod-btn-icon" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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-4 9.5-9.5z"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Scene preset -->
|
||
<article class="module" data-ch="violet">
|
||
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="mod-corner-actions">
|
||
<span class="swatch"></span>
|
||
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
|
||
</div>
|
||
<div class="mod-head">
|
||
<div class="mod-id">
|
||
<span class="mod-badge">SCN · 04</span>
|
||
<div class="mod-name"><span>Sunset Warmth</span></div>
|
||
<div class="mod-meta">4 targets · captured 21 Apr · used by 2 automations</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led"></span>
|
||
<span class="led"></span>
|
||
</div>
|
||
</div>
|
||
<div class="mod-desc">Warm tungsten cast on every fixture for a 19:00 unwind. Recapture re-snapshots all targets in their current colors.</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot"></span><span>PRESET</span></div>
|
||
<button class="mod-btn">RECAPTURE</button>
|
||
<button class="mod-btn mod-btn-go"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg> <span>ACTIVATE</span></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Sync clock (running) -->
|
||
<article class="module is-running" data-ch="violet">
|
||
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="mod-corner-actions">
|
||
<span class="swatch"></span>
|
||
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
|
||
</div>
|
||
<div class="mod-head">
|
||
<div class="mod-id">
|
||
<span class="mod-badge">CLK · MASTER</span>
|
||
<div class="mod-name"><span>Master Tempo</span></div>
|
||
<div class="mod-meta">Square · 1/16 sub · drift ±0.3ms</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led on blink"></span>
|
||
</div>
|
||
</div>
|
||
<div class="mod-metrics">
|
||
<div class="mod-metric">
|
||
<div class="k">BPM</div>
|
||
<div class="v signal">110.0</div>
|
||
</div>
|
||
<div class="mod-metric">
|
||
<div class="k">PHASE</div>
|
||
<div class="v">0.42</div>
|
||
</div>
|
||
<div class="mod-metric">
|
||
<div class="k">SUB</div>
|
||
<div class="v">1<small>/16</small></div>
|
||
</div>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot is-live"></span><span>TICKING</span></div>
|
||
<button class="mod-btn">TAP</button>
|
||
<button class="mod-btn mod-btn-stop"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="6" width="12" height="12"/></svg> <span>STOP</span></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Game integration -->
|
||
<article class="module" data-ch="amber">
|
||
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="mod-corner-actions">
|
||
<span class="swatch"></span>
|
||
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
|
||
</div>
|
||
<div class="mod-head">
|
||
<div class="mod-id">
|
||
<span class="mod-badge">GAME · CSGO</span>
|
||
<div class="mod-name"><span>Counter-Strike 2</span></div>
|
||
<div class="mod-meta">GSI · port 3456 · 12 mapped events</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led on"></span>
|
||
<span class="led on"></span>
|
||
</div>
|
||
</div>
|
||
<div class="mod-chips">
|
||
<span class="chip chip--tag">flash</span>
|
||
<span class="chip chip--tag">defuse</span>
|
||
<span class="chip chip--tag">round-end</span>
|
||
<span class="chip">+9 more</span>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot is-live"></span><span>LISTENING · 12s ago</span></div>
|
||
<button class="mod-btn">TEST</button>
|
||
<button class="mod-btn mod-btn-icon" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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-4 9.5-9.5z"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
</div>
|
||
|
||
<div class="section-title"><span class="num">04</span> Templates & assets</div>
|
||
|
||
<div class="grid">
|
||
|
||
<!-- Capture template -->
|
||
<article class="module" data-ch="cyan">
|
||
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="mod-corner-actions">
|
||
<span class="swatch"></span>
|
||
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
|
||
</div>
|
||
<div class="mod-head">
|
||
<div class="mod-id">
|
||
<span class="mod-badge">TPL · CAPTURE</span>
|
||
<div class="mod-name"><span>Desktop · 60 fps · region</span></div>
|
||
<div class="mod-meta">MSS · 3 keys configured · used by 5 sources</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led"></span>
|
||
</div>
|
||
</div>
|
||
<div class="mod-chips">
|
||
<span class="chip">crop</span><span class="chain-arrow">→</span>
|
||
<span class="chip">downsample</span><span class="chain-arrow">→</span>
|
||
<span class="chip">gamma</span><span class="chain-arrow">→</span>
|
||
<span class="chip">color-correct</span>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot"></span><span>TEMPLATE</span></div>
|
||
<button class="mod-btn">TEST</button>
|
||
<button class="mod-btn">CLONE</button>
|
||
<button class="mod-btn mod-btn-icon" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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-4 9.5-9.5z"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Color strip (running, with live preview) -->
|
||
<article class="module is-running" data-ch="signal">
|
||
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="mod-corner-actions">
|
||
<span class="swatch"></span>
|
||
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
|
||
</div>
|
||
<div class="mod-head">
|
||
<div class="mod-id">
|
||
<span class="mod-badge">STRIP · MAPPED</span>
|
||
<div class="mod-name"><span>Cinema map · 144 px</span></div>
|
||
<div class="mod-meta">Letterbox · smooth 0.35</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led on"></span>
|
||
<span class="led on blink"></span>
|
||
</div>
|
||
</div>
|
||
<!-- Live LED preview is the headline element; replaces metrics grid -->
|
||
<div class="mod-preview" style="background:linear-gradient(90deg,
|
||
#200035 0%, #5a1ca8 8%, #c14b8e 18%, #ff7a59 30%,
|
||
#ffd56b 40%, #95e7c9 50%, #00d8ff 65%, #2d6cdf 78%,
|
||
#1c2554 90%, #050816 100%);"></div>
|
||
<div class="mod-chips">
|
||
<span class="chip chip--link"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/></svg> Display 2</span>
|
||
<span class="chip chip--link">CSPT · 4 filters</span>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot is-live"></span><span>MAPPING</span></div>
|
||
<button class="mod-btn mod-btn-icon" title="Test"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg></button>
|
||
<button class="mod-btn mod-btn-icon" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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-4 9.5-9.5z"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Gradient — preview replaces metrics -->
|
||
<article class="module" data-ch="signal">
|
||
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="mod-corner-actions">
|
||
<span class="swatch"></span>
|
||
</div>
|
||
<div class="mod-head">
|
||
<div class="mod-id">
|
||
<span class="mod-badge">PALETTE · G-08</span>
|
||
<div class="mod-name"><span>Aurora <span style="font-size:.55rem;font-family:var(--font-mono);letter-spacing:.18em;color:var(--ch);background:color-mix(in srgb, var(--ch) 12%, transparent);border:1px solid color-mix(in srgb, var(--ch) 30%, transparent);padding:1px 5px;border-radius:3px;margin-left:6px;">BUILTIN</span></span></div>
|
||
<div class="mod-meta">5 stops · HSL space · loop</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led on"></span>
|
||
</div>
|
||
</div>
|
||
<div class="mod-preview" style="height:36px;background:linear-gradient(90deg,#003e6b 0%,#0085c4 25%,#22d3ee 55%,#a855f7 80%,#ec4899 100%);">
|
||
<span class="mod-preview__tag">5 STOPS · LOOP</span>
|
||
</div>
|
||
<div class="mod-chips">
|
||
<span class="chip">Used in 3 strips</span>
|
||
</div>
|
||
<div class="mod-foot">
|
||
<div class="mod-patch"><span class="patch-dot"></span><span>PRESET</span></div>
|
||
<button class="mod-btn">CLONE</button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Asset (image) -->
|
||
<article class="module" data-ch="cyan">
|
||
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="mod-corner-actions">
|
||
<span class="swatch"></span>
|
||
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
|
||
</div>
|
||
<div class="mod-head">
|
||
<div class="mod-id">
|
||
<span class="mod-badge">ASSET · IMG</span>
|
||
<div class="mod-name"><span>cosmic-loop-001.png</span></div>
|
||
<div class="mod-meta">PNG · 1920×1080 · 412 KB</div>
|
||
</div>
|
||
<div class="mod-leds" aria-hidden="true">
|
||
<span class="led on"></span>
|
||
</div>
|
||
</div>
|
||
<div class="mod-preview mod-preview--tall" style="background:
|
||
radial-gradient(circle at 30% 40%,#a855f7 0,transparent 35%),
|
||
radial-gradient(circle at 70% 60%,#22d3ee 0,transparent 30%),
|
||
#07090e;"></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-icon" title="Download"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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" y1="15" x2="12" y2="3"/></svg></button>
|
||
<button class="mod-btn">REPLACE</button>
|
||
</div>
|
||
</article>
|
||
|
||
</div>
|
||
|
||
<section class="notes">
|
||
<h2>How v2 maps to the dashboard</h2>
|
||
<ul>
|
||
<li><strong>Same shell, same classes.</strong> <code>.module</code> mirrors <code>.dashboard-target:has(.mod-head)</code> down to padding and the always-on left stripe at 0.6→1.0 opacity.</li>
|
||
<li><strong>Inline <code>.mod-badge</code> replaces my v1 top rail.</strong> Channel-tinted mono caps pill at the top-left of <code>.mod-id</code> — exactly the dashboard pattern, no extra band needed.</li>
|
||
<li><strong><code>.mod-leds</code> recessed cluster</strong> next to the title — multiple dots in a hairline bezel, real-hardware feel. Single LED card-types use one dot in the same bezel.</li>
|
||
<li><strong>Numerics use <code>var(--font-display)</code>.</strong> Big Shoulders Display, 2rem, 800-weight, tabular-nums. Already bundled in <code>fonts/big-shoulders-display-latin.woff2</code> and only used on dashboard + perf cards today.</li>
|
||
<li><strong><code>.mod-patch</code> footer label</strong> on every card: <code>PATCHED · OUT-1</code>, <code>STREAMING</code>, <code>OFFLINE</code>, <code>STANDBY</code>, <code>ARMED</code>, <code>PRESET</code>, <code>READY</code>. Channel-colored dot on the left, mono caps state label — the strongest "this is rack hardware" cue.</li>
|
||
<li><strong><code>.mod-btn</code> with text labels</strong> for primary/secondary actions in the footer (<code>START</code> / <code>STOP</code> / <code>EDIT</code>). Tertiary square <code>.mod-btn-icon</code> variant for settings.</li>
|
||
<li><strong>Card-color is identity, not an action.</strong> A 10px <code>.mod-badge__color</code> dot leads every <code>.mod-badge</code> — channel-tinted by default, fills with the user's picked color when overridden. Click to open the picker. Lives <em>with</em> the type label it tints, not in a destructive cluster.</li>
|
||
<li><strong>Single overflow menu</strong> (kebab) replaces the v2.0 corner huddle. Always visible at 0.34 opacity, brightens on card-hover. Click to open <code>Duplicate</code> / <code>Hide card</code> / ─── / <code>Delete</code>. One control, isolated, touch-and-keyboard accessible — and easily extended (<em>Move to section</em>, <em>Export</em>) without re-cramping the corner.</li>
|
||
<li><strong>Why this works:</strong> destructive actions are no longer adjacent to cosmetic ones (no more "delete next to color picker"); the menu groups management actions semantically; the corner reads as quiet metadata instead of a row of buttons; every interaction is discoverable without hover.</li>
|
||
<li><strong>Properties unify.</strong> <code>.card-meta</code> + <code>.stream-card-prop</code> → single <code>.chip</code> system with three modifiers: default, <code>.chip--link</code> for crosslinks, <code>.chip--tag</code> for tag pills, <code>.chip--err</code> for error states. Filter chains use chips + <code>.chain-arrow</code> separators.</li>
|
||
<li><strong>Three body slots</strong> compose every card type:
|
||
<code>.mod-metrics</code> (numeric readouts) ·
|
||
<code>.mod-chips</code> (qualitative metadata, crosslinks, filter chains) ·
|
||
<code>.mod-preview</code> (gradient strip / asset thumb / live LED preview).
|
||
Plus optional <code>.mod-fader</code> (LED targets) and <code>.mod-desc</code> (descriptions).</li>
|
||
</ul>
|
||
|
||
<h2 style="margin-top:42px;">v1 → v2 specifics</h2>
|
||
<ul>
|
||
<li>Dropped the 32px top rail band → moved to inline <code>.mod-badge</code> at top-left.</li>
|
||
<li>Mono LCD-style readouts → display-font instrument readouts at 2rem 800.</li>
|
||
<li>Single status dot → <code>.mod-leds</code> 1–3 LED bezel.</li>
|
||
<li>Square icon-only footer buttons → <code>.mod-btn</code> with text labels for state-changing actions; icon variant kept for settings/edit only.</li>
|
||
<li>Added <code>.mod-patch</code> footer indicator on every card.</li>
|
||
<li>Restored the corner bracket (idle) and the running signal-flow animation.</li>
|
||
<li><strong>Corner cluster killed.</strong> The "swatch + hide + delete" huddle is gone. Color is now a 10px identity dot inside the badge; hide and delete moved into a single overflow menu.</li>
|
||
<li>Field cleanup: paper-grain overlay removed (dashboard cards don't use it; not part of the established language).</li>
|
||
</ul>
|
||
|
||
<h2 style="margin-top:42px;">Implementation footprint</h2>
|
||
<ul>
|
||
<li><strong>CSS:</strong> the <code>.mod-*</code> rules already exist in <code>dashboard.css</code>. The entity-card variant just needs <code>.card:has(.mod-head)</code> / <code>.template-card:has(.mod-head)</code> selectors that copy <code>.dashboard-target:has(.mod-head)</code>'s padding/layout — or alias all three to a shared <code>.module</code> class.</li>
|
||
<li><strong>JS:</strong> <code>wrapCard()</code> in <code>card-colors.ts</code> already handles the wrapper, color stripe, and corner actions. Add an optional second signature: <code>wrapCard({ mod: { badge, name, meta, leds, metrics, chips, preview, fader, patch, primaryAction } })</code> that emits the dashboard markup. Existing <code>content/actions</code> path stays for incremental migration.</li>
|
||
<li><strong>Per feature:</strong> each <code>create*Card()</code> swaps its hand-rolled <code>.card-header</code> + <code>.card-subtitle</code> + <code>.stream-card-props</code> for the structured <code>mod</code> options. Localised, low-risk, can ship one card type per PR.</li>
|
||
<li><strong>Cards.css cleanup:</strong> remove the <code>[data-has-color="1"], .card-running</code> gate on the <code>::before</code> stripe so the channel signal becomes ambient. Single line change.</li>
|
||
</ul>
|
||
</section>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
// ── Theme toggle ─────────────────────────────────────────────────
|
||
document.querySelectorAll('.theme-toggle button').forEach(b=>{
|
||
b.addEventListener('click',()=>{
|
||
document.documentElement.dataset.theme=b.dataset.themeSet;
|
||
document.querySelectorAll('.theme-toggle button').forEach(x=>x.classList.toggle('is-active',x===b));
|
||
});
|
||
});
|
||
|
||
// ── Card-color identity dot — leading element of every .mod-badge.
|
||
// Single click target, lives where the channel color reads as
|
||
// identity, not as an action sitting in a destructive cluster. ──
|
||
document.querySelectorAll('.mod-badge').forEach(b=>{
|
||
if(b.querySelector('.mod-badge__color')) return;
|
||
const dot=document.createElement('span');
|
||
dot.className='mod-badge__color';
|
||
dot.setAttribute('role','button');
|
||
dot.setAttribute('tabindex','0');
|
||
dot.setAttribute('aria-label','Change card color');
|
||
dot.title='Card color';
|
||
b.prepend(dot);
|
||
});
|
||
|
||
// Demo: a couple of cards have a user-picked custom color so the
|
||
// reader can see the override state.
|
||
const CUSTOM_OVERRIDES={
|
||
'Bedroom Halo':'#ffb800',
|
||
'Sunset Warmth':'#ff7a59',
|
||
};
|
||
document.querySelectorAll('.module').forEach(card=>{
|
||
const name=card.querySelector('.mod-name span')?.textContent?.trim();
|
||
const hex=name && CUSTOM_OVERRIDES[name];
|
||
if(hex){
|
||
const dot=card.querySelector('.mod-badge__color');
|
||
dot.dataset.custom='1';
|
||
dot.style.setProperty('--user-color',hex);
|
||
}
|
||
});
|
||
|
||
// Stub picker — not the focus of the demo
|
||
document.addEventListener('click',e=>{
|
||
const dot=e.target.closest('.mod-badge__color');
|
||
if(!dot) return;
|
||
const card=dot.closest('.module');
|
||
const name=card?.querySelector('.mod-name span')?.textContent?.trim()||'card';
|
||
alert(`(Demo) opens the inline color picker for "${name}". In production this calls registerColorPicker() from color-picker.ts.`);
|
||
});
|
||
|
||
// ── Replace every .mod-corner-actions with the overflow menu ──
|
||
const ICON_HIDE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg>';
|
||
const ICON_CLONE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||
const ICON_TRASH = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>';
|
||
const ICON_KEBAB = '<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="19" cy="12" r="1.7"/></svg>';
|
||
|
||
document.querySelectorAll('.mod-corner-actions').forEach(el=>{
|
||
// Detect what the card already had so we keep the menu honest:
|
||
// - card had a delete (.mod-icon-btn--danger) → keep Delete item
|
||
// (gradient builtin card has no delete → no Delete item)
|
||
const hadDelete = !!el.querySelector('.mod-icon-btn--danger');
|
||
const isBuiltin = !hadDelete;
|
||
|
||
const items=[];
|
||
items.push(`<button class="mod-menu__item" role="menuitem">${ICON_CLONE} Duplicate</button>`);
|
||
items.push(`<button class="mod-menu__item" role="menuitem">${ICON_HIDE} Hide card</button>`);
|
||
if(!isBuiltin){
|
||
items.push(`<div class="mod-menu__sep"></div>`);
|
||
items.push(`<button class="mod-menu__item mod-menu__item--danger" role="menuitem">${ICON_TRASH} Delete</button>`);
|
||
}
|
||
|
||
const wrap=document.createElement('div');
|
||
wrap.className='mod-menu-wrap';
|
||
wrap.innerHTML=`
|
||
<button class="mod-menu-btn" type="button" aria-haspopup="true" aria-expanded="false" aria-label="Card options" title="More actions">
|
||
${ICON_KEBAB}
|
||
</button>
|
||
<div class="mod-menu" role="menu">
|
||
${items.join('')}
|
||
</div>`;
|
||
|
||
// v2.1 fix: append the menu inside .mod-head as a flex sibling
|
||
// of .mod-leds — never absolutely positioned over the LED bezel.
|
||
const card = el.closest('.module');
|
||
const head = card?.querySelector('.mod-head');
|
||
el.remove();
|
||
if(head) head.appendChild(wrap);
|
||
else card?.appendChild(wrap); // fallback (cards without a head)
|
||
});
|
||
|
||
// ── Menu open/close behavior ─────────────────────────────────────
|
||
function _closeAllMenus(){
|
||
document.querySelectorAll('.mod-menu-wrap.is-open').forEach(w=>{
|
||
w.classList.remove('is-open');
|
||
w.querySelector('.mod-menu-btn')?.setAttribute('aria-expanded','false');
|
||
});
|
||
}
|
||
function _positionMenu(wrap){
|
||
// Anchor the menu to the kebab button's bottom-right corner,
|
||
// using fixed positioning so the card's overflow:hidden never
|
||
// clips short-card dropdowns (gradient, asset, etc.).
|
||
const btn=wrap.querySelector('.mod-menu-btn');
|
||
const menu=wrap.querySelector('.mod-menu');
|
||
if(!btn||!menu) return;
|
||
const r=btn.getBoundingClientRect();
|
||
const menuH=menu.offsetHeight||140;
|
||
const spaceBelow=window.innerHeight-r.bottom;
|
||
// Open upward when there's not enough room below
|
||
if(spaceBelow<menuH+12){
|
||
menu.style.top=`${r.top-menuH-4}px`;
|
||
menu.style.transformOrigin='bottom right';
|
||
}else{
|
||
menu.style.top=`${r.bottom+4}px`;
|
||
menu.style.transformOrigin='top right';
|
||
}
|
||
menu.style.right=`${window.innerWidth-r.right}px`;
|
||
menu.style.left='auto';
|
||
}
|
||
document.addEventListener('click',e=>{
|
||
const btn=e.target.closest('.mod-menu-btn');
|
||
if(btn){
|
||
e.stopPropagation();
|
||
const wrap=btn.closest('.mod-menu-wrap');
|
||
const open=wrap.classList.contains('is-open');
|
||
_closeAllMenus();
|
||
if(!open){
|
||
wrap.classList.add('is-open');
|
||
btn.setAttribute('aria-expanded','true');
|
||
// Position after the menu becomes visible so offsetHeight is accurate
|
||
requestAnimationFrame(()=>_positionMenu(wrap));
|
||
}
|
||
return;
|
||
}
|
||
if(!e.target.closest('.mod-menu')) _closeAllMenus();
|
||
});
|
||
document.addEventListener('keydown',e=>{
|
||
if(e.key==='Escape') _closeAllMenus();
|
||
});
|
||
window.addEventListener('scroll',_closeAllMenus,{passive:true,capture:true});
|
||
window.addEventListener('resize',_closeAllMenus,{passive:true});
|
||
|
||
// Demo: clicking a menu item shows what action would fire and
|
||
// closes the menu — production wires these to the existing
|
||
// hideCard / deleteX / cloneX functions.
|
||
document.addEventListener('click',e=>{
|
||
const item=e.target.closest('.mod-menu__item');
|
||
if(!item) return;
|
||
const card=item.closest('.module');
|
||
const name=card?.querySelector('.mod-name span')?.textContent?.trim()||'card';
|
||
const action=item.textContent.trim();
|
||
_closeAllMenus();
|
||
if(action==='Hide card'){
|
||
card.classList.toggle('is-hidden');
|
||
item.firstChild.replaceWith(card.classList.contains('is-hidden')?'Show card':'Hide card');
|
||
}else{
|
||
// Toast-style notification, demo only
|
||
const t=document.createElement('div');
|
||
t.textContent=`(Demo) "${action}" → "${name}"`;
|
||
Object.assign(t.style,{position:'fixed',bottom:'24px',left:'50%',transform:'translateX(-50%)',
|
||
background:'var(--lux-bg-1)',color:'var(--lux-ink)',
|
||
border:'1px solid var(--lux-line-bold)',padding:'10px 16px',borderRadius:'4px',
|
||
fontFamily:'var(--font-mono)',fontSize:'.75rem',letterSpacing:'.08em',
|
||
boxShadow:'0 8px 24px rgba(0,0,0,.4)',zIndex:9999});
|
||
document.body.appendChild(t);
|
||
setTimeout(()=>t.remove(),1800);
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|