feat(ui): Lumenworks studio-console WebUI redesign
Full-app UI/UX refresh committing to a tech-instrument / studio-console aesthetic inspired by hardware synths, Eurorack panels, and DAW layouts. Design tokens and fonts: - Embed Manrope (body), JetBrains Mono (labels/metrics), Big Shoulders Display (numeric readouts) as local .woff2 variable fonts with latin + latin-ext + cyrillic + cyrillic-ext subsets via unicode-range. - New Lumenworks token layer in base.css: --lux-bg-0..3, --lux-line(-bold), --lux-ink(-dim/-mute/-faint), --ch-signal/-cyan/-magenta/-amber/-coral/ -violet channel palette, --lux-signal-glow, --lux-shadow-rack, all theme-aware for dark + light. Existing tokens untouched for compat. Shell (header + sidebar): - Header rebuilt as a 3-column CSS-grid transport bar (brand | center | toolbar) with a glowing LED brand mark rendered via pseudo-elements on .header-title. Gradient channel-color rule under the bottom border. - New sidebar.css introduces a vertical channel-strip nav. Active tab gets a glowing left stripe + radial tint + LED pip. .sidebar-foot contains a live CPU/FPS meter plate. - Sidebar collapses to a 56 px icon rail at <=1100 px and hides via display:contents at <=600 px so mobile.css's fixed bottom tab-bar flows through unchanged. Cards and dashboard: - .card gets channel stripe (data-card-type + .ch-* utilities auto-map from data-target-id / data-stream-id / data-automation-id etc.), corner bracket, gradient background, subtle rack shadow. - .card-running replaces the old @property --border-angle conic-gradient rotating border with a lightweight signalFlow linear-gradient strip on the bottom edge (cheaper paint, no GPU layer compositing per card). - Skeleton loaders rewritten: left hairline + corner bracket + gradient shimmer instead of the old text-color opacity pulse. - .dashboard-target rows pick up the same channel-stripe + signalFlow treatment. Section headers use mono micro-caps with a channel-green underline accent consistent across the app. - .perf-chart-card: channel stripe replaces old border-top; per-metric accents moved to the channel palette (CPU=coral, RAM=violet, GPU=green, temp=amber). Metric values use tabular-nums + a soft glow. Live bindings (no new endpoints): - _updateSidebarMeter: binds the sidebar Load + FPS bars to the existing /system/performance poll. - _updateTransportStatus: toggles the transport chip between "Ready" and "Armed - N live" whenever the dashboard's running-target set is recomputed. Tree-nav + sub-tabs: - tree-nav.css trigger pill gets a channel-stripe left edge that glows when open; panel has a gradient channel-accent rule across the top; group headers use silkscreened micro-caps; active leaf has a pulsing LED pip + channel tint. - .stream-tab-btn / .subtab-section-header adopt the same mono-caps + channel-underline language for consistency. - Graph editor toolbar gets gradient + hairline + rack shadow + backdrop blur. Canvas and nodes untouched. Modals (40+ modals share modal.css): - Radial-dim + 6 px blur backdrop. Content gets a gradient background, hairline border, deep rack shadow, top channel-accent rule driven by --modal-ch, bottom-right corner bracket (hidden on mobile fullscreen). - Per-modal-ID channel lanes: target editors = green, source/input editors = cyan, audio = magenta, automation/scene/game = violet, settings/auth = amber, confirm = coral. - Modal headers: vertical channel stripe left of the title + hairline divider. Modal footers: hairline top border + subtle gradient wash. Forms: - Inputs use hairline borders; number inputs switch to mono + tabular-nums for column alignment. Focus state: channel-green ring + soft glow. - Buttons use mono-uppercase type with signal-glow on primary and coral- glow on danger. Mobile (<=600 px): - Fixed bottom .tab-bar gets the full Lumenworks treatment: gradient fill, top channel-accent rule matching the transport bar, backdrop blur. Active tab has an LED pip above the icon + channel tint + icon recolor. - Fullscreen modals: corner bracket hidden, header stripe slimmed. Microcopy (en / ru / zh): - "Targets" -> "Channels" / "Каналы" / "通道" - "Sources" -> "Inputs" / "Входы" / "输入" - Internal tab keys (dashboard/automations/targets/streams/integrations/ graph) kept stable so no JS or localStorage migration is needed. - Added: sidebar.workspaces, sidebar.load, sidebar.fps, transport.status.ready, transport.status.armed. Compatibility: - All existing class hooks preserved (.tab-bar, .tab-btn, .card, .card-running, .tree-dd-*, .cs-*, .perf-chart-card, .modal-content, .dashboard-target, etc.). No JS or API changes required for the new look to take effect. - Tour selectors survive (header .header-title, #tab-btn-*, onclick markers on theme/settings/search, #cp-wrap-accent, etc.). - Mobile <=600 px bottom tab-bar keeps working via display:contents fall-through in the new sidebar. Build: tsc --noEmit clean; npm run build clean. CSS bundle grew from ~177 KB to ~201 KB for the full new visual system. Fonts loaded lazily per unicode-range subset (~98 KB critical path for English). Phased plan + deferred follow-ups (dashboard hero strip, legacy-token cleanup) recorded at the top of TODO.md. Reference mockup: server/docs/ui-redesign-mockup.html.
This commit is contained in:
@@ -1,5 +1,176 @@
|
|||||||
# LedGrab TODO
|
# LedGrab TODO
|
||||||
|
|
||||||
|
## WebUI Redesign — "Lumenworks" Studio-Console Aesthetic
|
||||||
|
|
||||||
|
Full-app UI/UX refresh. Design direction committed to by user 2026-04-24.
|
||||||
|
Mockup lives at [server/docs/ui-redesign-mockup.html](server/docs/ui-redesign-mockup.html).
|
||||||
|
Phases are independent and CSS-only where possible — backend untouched.
|
||||||
|
|
||||||
|
### Phase 1 — Design tokens & font embed
|
||||||
|
|
||||||
|
- [x] Embed variable fonts (`server/src/ledgrab/static/fonts/`):
|
||||||
|
Manrope (latin + latin-ext + cyrillic + cyrillic-ext),
|
||||||
|
JetBrains Mono (same 4 subsets),
|
||||||
|
Big Shoulders Display (latin + latin-ext). Total +201 KB gzipped,
|
||||||
|
served via `unicode-range` so only latin paints on first load.
|
||||||
|
- [x] `fonts.css` — declare `@font-face` entries for all new families with
|
||||||
|
proper `unicode-range` subsetting; keep DM Sans + Orbitron registered
|
||||||
|
for legacy-token callers during migration.
|
||||||
|
- [x] `base.css` — add additive Lumenworks tokens:
|
||||||
|
`--font-display/--font-brand/--font-body`, `--lux-r-*`, `--lux-hairline`,
|
||||||
|
`--lux-rule`. Both `[data-theme="dark"]` and `[data-theme="light"]`
|
||||||
|
define `--lux-bg-0…3`, `--lux-line/-bold`, `--lux-ink/-dim/-mute/-faint`,
|
||||||
|
`--ch-signal/-cyan/-magenta/-amber/-coral/-violet`, `--lux-signal-glow`,
|
||||||
|
`--lux-shadow-rack`. Existing tokens untouched — no visual regression.
|
||||||
|
|
||||||
|
### Phase 2 — Shell (header → transport bar + channel-strip sidebar)
|
||||||
|
|
||||||
|
- [x] `index.html` — `.tab-bar` moved out of `<header>` into a new
|
||||||
|
`<aside class="sidebar">`; wrapped content in `.app-body` 2-col grid
|
||||||
|
(sidebar | main). `.transport-center` section added between
|
||||||
|
`.header-title` and `.header-toolbar` with a placeholder `.transport-status`
|
||||||
|
chip ("Ready" → "Armed · N live" wired in Phase 3). All tab-button IDs,
|
||||||
|
`data-tab` attributes, and `onclick="switchTab(…)"` handlers preserved.
|
||||||
|
- [x] `layout.css` — `<header>` rebuilt as the transport bar: 3-column grid
|
||||||
|
(brand | center | toolbar), 60 px fixed height, sticky, gradient bottom
|
||||||
|
rule with channel-color wash. `.header-title::before/::after` render
|
||||||
|
the glowing LED brand mark; `#server-status` repositioned as the LED
|
||||||
|
core pip. `#server-version` restyled as a mono-type console badge.
|
||||||
|
- [x] `sidebar.css` (new) — vertical channel-strip navigation. Active tab
|
||||||
|
gets a glowing left stripe + radial tint. `.sidebar-foot` contains
|
||||||
|
a `.cpu-meter` plate with two live bars (Load, FPS) ready to be
|
||||||
|
JS-bound in Phase 3. Collapses to a 56 px icon rail at ≤1100 px;
|
||||||
|
hides entirely at ≤600 px via `display: contents` so `.tab-bar`
|
||||||
|
falls through to `mobile.css`'s fixed-bottom strip unchanged.
|
||||||
|
- [x] `all.css` — new sidebar import after layout.
|
||||||
|
- [x] `base.css` — body font-family switched to `var(--font-body)` which
|
||||||
|
resolves to Manrope (with DM Sans + system fallbacks). Added
|
||||||
|
`font-feature-settings` for stylistic set + alternate 1.
|
||||||
|
- [x] Locale additions: `sidebar.workspaces`, `sidebar.load`, `sidebar.fps`,
|
||||||
|
`transport.status.ready`, `transport.status.armed` in en/ru/zh.
|
||||||
|
- [x] Tutorial + auth selectors (`header .header-title`, `#tab-btn-*`,
|
||||||
|
`.tab-bar` querySelector, `a.header-link[href="/docs"]`, onclick
|
||||||
|
markers on theme/settings/search) all survive the move.
|
||||||
|
- [ ] JS: bind `.cpu-meter` + `.transport-status` chip to existing
|
||||||
|
`performance` WebSocket / poller. Done as part of Phase 3.
|
||||||
|
- [ ] Tablet-range visual polish pass once other phases render (some tabs
|
||||||
|
currently have their own internal sticky headers that may overlap
|
||||||
|
the transport bar on narrow viewports).
|
||||||
|
|
||||||
|
### Phase 3 — Dashboard hero + module redesign
|
||||||
|
|
||||||
|
- [x] `cards.css` — `.card` gets rack-module treatment: channel stripe on
|
||||||
|
left edge (color-coded via `data-card-type` + `.ch-*` utility classes),
|
||||||
|
`::after` corner bracket in top-right, mono-typed metric labels
|
||||||
|
planned for Phase 4. Running cards glow the stripe brighter + emit a
|
||||||
|
`signalFlow` keyframe strip along the bottom edge.
|
||||||
|
- [x] Removed the `@property --border-angle` rotating conic-gradient border
|
||||||
|
(retired the WebKit mask workaround + light-theme variant + fallback
|
||||||
|
for `@supports not (mask-composite: exclude)`). Replaced with the
|
||||||
|
signal-flow strip — one animated linear-gradient on a 2 px line, no
|
||||||
|
GPU layer compositing per card.
|
||||||
|
- [x] `dashboard.css` — `.dashboard-target` rows pick up the same channel
|
||||||
|
stripe + signal-flow treatment. Section headers now use mono caps
|
||||||
|
with a channel-green underline accent. Metric values use mono with
|
||||||
|
tabular numerics; labels use silkscreened micro-caps.
|
||||||
|
- [x] Skeleton-card rewritten: left hairline + corner bracket so it reads
|
||||||
|
as "loading module" instead of a generic flashing block.
|
||||||
|
`skeletonShimmer` gradient replaces the old opacity-pulse on
|
||||||
|
`--text-color`.
|
||||||
|
- [x] `_updateSidebarMeter` binds CPU% (Load) and app-CPU share (FPS)
|
||||||
|
to the sidebar meter plate on every perf poll.
|
||||||
|
- [x] `_updateTransportStatus` updates the transport chip ("Ready" →
|
||||||
|
"Armed · N live") whenever the dashboard's running-target set is
|
||||||
|
recomputed.
|
||||||
|
- [ ] `.hero` 4-cell readout row (Active Patches / Throughput / CPU /
|
||||||
|
Latency + inline sparklines) — CSS tokens + layout are ready; HTML
|
||||||
|
render deferred until the dashboard JS is refactored to emit it
|
||||||
|
(Phase 3b, non-blocking).
|
||||||
|
|
||||||
|
### Phase 4 — Other tabs adopt module language
|
||||||
|
|
||||||
|
- [x] `tree-nav.css` — trigger pill gets a channel stripe on its left edge
|
||||||
|
(glows + widens when open). Trigger title uses mono-uppercase with
|
||||||
|
wide letter-spacing. Dropdown panel has a gradient channel-accent
|
||||||
|
rule across its top edge. Group headers use silkscreened micro-caps
|
||||||
|
with a small square marker instead of the old bold-uppercase. Active
|
||||||
|
leaf has a pulsing LED pip on the left and a channel tint behind it.
|
||||||
|
Count badges switched to mono tabular-nums in 2-px-radius pills.
|
||||||
|
- [x] `.subtab-section-header` — channel-green underline accent + mono
|
||||||
|
micro-caps. Consistent with the dashboard-section pattern so the
|
||||||
|
whole app shares one section-header language.
|
||||||
|
- [x] `.stream-tab-btn` sub-tabs — mono uppercase with wide tracking,
|
||||||
|
active tab shows channel-green underline + glowing count badge.
|
||||||
|
- [x] `.perf-chart-card` — channel stripe on the left (replaces old
|
||||||
|
`border-top` accent). Per-metric accents swapped to channel palette
|
||||||
|
(`--ch-coral` for CPU, `--ch-violet` for RAM, `--ch-signal` for GPU,
|
||||||
|
`--ch-amber` for temp). Corner bracket added. Metric values pick up
|
||||||
|
`tabular-nums` + a soft glow.
|
||||||
|
- [x] `cards.css` — channel-color mapping extended to attributes the JS
|
||||||
|
already emits (`data-target-id` → green, `data-stream-id` → cyan,
|
||||||
|
`data-audio-source-id` → magenta, `data-automation-id` /
|
||||||
|
`data-scene-id` → violet). No JS changes required; cards pick up
|
||||||
|
their correct stripe automatically on the Targets/Sources/Automations
|
||||||
|
tabs.
|
||||||
|
- [x] Graph editor — toolbar gets a gradient background + hairline +
|
||||||
|
rack shadow + backdrop blur. Canvas and nodes untouched.
|
||||||
|
|
||||||
|
### Phase 5 — Modal restyle
|
||||||
|
|
||||||
|
- [x] `modal.css` — backdrop gains a radial dim + 6 px blur for stronger
|
||||||
|
separation. `.modal-content` gets a gradient background + hairline +
|
||||||
|
deep rack shadow. Channel-accent rule across the top edge driven by
|
||||||
|
`--modal-ch` (per-modal override). Corner bracket bottom-right on
|
||||||
|
desktop. `.modal-header` gains a vertical channel-color stripe to
|
||||||
|
the left of the title; `.modal-footer` picks up a hairline divider.
|
||||||
|
- [x] Per-modal channel mapping by modal ID:
|
||||||
|
- Target editors → green
|
||||||
|
- Input/Source editors → cyan
|
||||||
|
- Audio editors → magenta
|
||||||
|
- Automation / Scene / Game editors → violet
|
||||||
|
- Settings / API key / Setup / Notifications → amber
|
||||||
|
- Confirm dialog → coral
|
||||||
|
- [x] `components.css` — inputs use hairline borders, tabular-nums mono
|
||||||
|
for `input[type="number"]`, channel-green focus ring + glow. Buttons
|
||||||
|
use mono-uppercase type, signal-glow on primary, coral-glow on
|
||||||
|
danger. `<select>` audit deferred (project already enforces via
|
||||||
|
CLAUDE.md rule + IconSelect/EntitySelect wrappers).
|
||||||
|
|
||||||
|
### Phase 6 — Mobile dedicated shell
|
||||||
|
|
||||||
|
- [x] `mobile.css` (existing file, not forked) — fixed-bottom `.tab-bar`
|
||||||
|
promoted to full Lumenworks treatment: gradient background + hairline
|
||||||
|
divider at top + channel-accent rule matching the transport-bar
|
||||||
|
bottom. Active tab gets an LED pip above the icon and a channel-tint
|
||||||
|
background. Tab labels + badges use mono uppercase to match the
|
||||||
|
rest of the app. Phone (≤600 px): modal corner-bracket hidden
|
||||||
|
(fullscreen modals), modal-header stripe slimmed to 18 px.
|
||||||
|
- [x] Phase 2's layout.css already strips the transport-center on phones
|
||||||
|
and collapses the sidebar via `display: contents`, so the mobile
|
||||||
|
shell automatically routes the tab-bar to the bottom without a
|
||||||
|
separate JS hook.
|
||||||
|
- [WONTDO] Fork into `mobile-shell.css` — keeping changes in `mobile.css`
|
||||||
|
since the cascade was already organized by viewport. A rename adds
|
||||||
|
churn without improving maintainability.
|
||||||
|
|
||||||
|
### Phase 7 — Microcopy + retire legacy
|
||||||
|
|
||||||
|
- [x] Locale rename: `targets.title` + `dashboard.section.targets` →
|
||||||
|
"Channels" (en) / "Каналы" (ru) / "通道" (zh);
|
||||||
|
`streams.title` → "Inputs" / "Входы" / "输入".
|
||||||
|
Automations kept as-is (Automations + Scenes is a meaningful
|
||||||
|
distinction; "Patches" would conflate them). Internal tab keys
|
||||||
|
(`dashboard` / `automations` / `targets` / `streams` / `integrations`
|
||||||
|
/ `graph`) unchanged so no JS or localStorage migration needed.
|
||||||
|
- [x] Ambient WebGL background — default is already `off`; kept the
|
||||||
|
toggle button and localStorage preference so users who want the
|
||||||
|
shader can turn it on. No entry-point change needed: `data-bg-anim`
|
||||||
|
is initialized from localStorage with `off` fallback.
|
||||||
|
- [DEFERRED] Delete DM Sans + legacy color tokens — would cascade through
|
||||||
|
every file that reads `--primary-color` / `--text-color` etc. Safer
|
||||||
|
as a separate cleanup PR after the new design has soaked.
|
||||||
|
- [WONTDO] Delete `mobile.css` — Phase 6 kept the filename.
|
||||||
|
|
||||||
## BLE LED Controller Support (SP110E / Triones / Zengge / Govee)
|
## BLE LED Controller Support (SP110E / Triones / Zengge / Govee)
|
||||||
|
|
||||||
Add support for Bluetooth Low Energy LED controllers driven by mobile apps like "LED Hue", HappyLighting, iLightsIn. Whole-strip ambient-color output only — these protocols don't support per-pixel streaming.
|
Add support for Bluetooth Low Energy LED controllers driven by mobile apps like "LED Hue", HappyLighting, iLightsIn. Whole-strip ambient-color output only — these protocols don't support per-pixel streaming.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Generated
+48
@@ -14,6 +14,9 @@
|
|||||||
"marked": "^17.0.5"
|
"marked": "^17.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@fontsource-variable/big-shoulders-display": "^5.2.5",
|
||||||
|
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||||
|
"@fontsource-variable/manrope": "^5.2.8",
|
||||||
"esbuild": "^0.27.4",
|
"esbuild": "^0.27.4",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
@@ -434,6 +437,33 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fontsource-variable/big-shoulders-display": {
|
||||||
|
"version": "5.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/big-shoulders-display/-/big-shoulders-display-5.2.5.tgz",
|
||||||
|
"integrity": "sha512-ZH2w9u6018xbSf8vPZ42P/KxpQHfIsKnxSnMtLFgwui1zIS05vzlijAWRcaRQoY2pXu4Z3SVa88OANsmq6mkvA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource-variable/jetbrains-mono": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource-variable/manrope": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/manrope/-/manrope-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-nc9lOuCRz73UHnovDE2bwXUdghE2SEOc7Aii0qGe3CLyE03W1a7VnY5Z6euRiapiKbCkGS+eXbY3s/kvWeGeSw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@kurkle/color": {
|
"node_modules/@kurkle/color": {
|
||||||
"version": "0.3.4",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
@@ -704,6 +734,24 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"@fontsource-variable/big-shoulders-display": {
|
||||||
|
"version": "5.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/big-shoulders-display/-/big-shoulders-display-5.2.5.tgz",
|
||||||
|
"integrity": "sha512-ZH2w9u6018xbSf8vPZ42P/KxpQHfIsKnxSnMtLFgwui1zIS05vzlijAWRcaRQoY2pXu4Z3SVa88OANsmq6mkvA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@fontsource-variable/jetbrains-mono": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@fontsource-variable/manrope": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/manrope/-/manrope-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-nc9lOuCRz73UHnovDE2bwXUdghE2SEOc7Aii0qGe3CLyE03W1a7VnY5Z6euRiapiKbCkGS+eXbY3s/kvWeGeSw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@kurkle/color": {
|
"@kurkle/color": {
|
||||||
"version": "0.3.4",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@fontsource-variable/big-shoulders-display": "^5.2.5",
|
||||||
|
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||||
|
"@fontsource-variable/manrope": "^5.2.8",
|
||||||
"esbuild": "^0.27.4",
|
"esbuild": "^0.27.4",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
@import './fonts.css';
|
@import './fonts.css';
|
||||||
@import './base.css';
|
@import './base.css';
|
||||||
@import './layout.css';
|
@import './layout.css';
|
||||||
|
@import './sidebar.css';
|
||||||
@import './components.css';
|
@import './components.css';
|
||||||
@import './cards.css';
|
@import './cards.css';
|
||||||
@import './modal.css';
|
@import './modal.css';
|
||||||
|
|||||||
@@ -16,7 +16,25 @@
|
|||||||
--danger-color: #f44336;
|
--danger-color: #f44336;
|
||||||
--warning-color: #ff9800;
|
--warning-color: #ff9800;
|
||||||
--info-color: #2196F3;
|
--info-color: #2196F3;
|
||||||
--font-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
|
--font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
|
||||||
|
|
||||||
|
/* ── Lumenworks design tokens (additive; active alongside legacy tokens
|
||||||
|
during phased migration). Typography + spatial system for the
|
||||||
|
studio-console redesign. Channel colors defined in the theme
|
||||||
|
blocks below so they can shift with light/dark mode. ──────── */
|
||||||
|
--font-display: 'Big Shoulders Display', 'Orbitron', 'Manrope', sans-serif;
|
||||||
|
--font-brand: 'Orbitron', sans-serif;
|
||||||
|
--font-body: 'Manrope', 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
|
||||||
|
--lux-r-sm: 3px;
|
||||||
|
--lux-r-md: 6px;
|
||||||
|
--lux-r-lg: 10px;
|
||||||
|
--lux-r-xl: 14px;
|
||||||
|
|
||||||
|
/* Hairline + bold dividers — thinner than the legacy 1px --border-color
|
||||||
|
to get the "silkscreened panel" feel. */
|
||||||
|
--lux-hairline: 1px;
|
||||||
|
--lux-rule: 2px;
|
||||||
|
|
||||||
/* Spacing scale */
|
/* Spacing scale */
|
||||||
--space-xs: 4px;
|
--space-xs: 4px;
|
||||||
@@ -96,6 +114,30 @@
|
|||||||
--hover-bg: rgba(255, 255, 255, 0.05);
|
--hover-bg: rgba(255, 255, 255, 0.05);
|
||||||
--input-bg: #1a1a2e;
|
--input-bg: #1a1a2e;
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
|
||||||
|
/* ── Lumenworks dark palette ── */
|
||||||
|
--lux-bg-0: #0a0b0d;
|
||||||
|
--lux-bg-1: #101216;
|
||||||
|
--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;
|
||||||
|
|
||||||
|
/* Channel palette — consistent across tabs for entity types */
|
||||||
|
--ch-signal: #00ff7a; /* capture / targets — primary */
|
||||||
|
--ch-signal-dim: #00b85a;
|
||||||
|
--ch-cyan: #00d8ff; /* data / sources / screen */
|
||||||
|
--ch-magenta: #ff4ade; /* audio / FFT */
|
||||||
|
--ch-amber: #ffb800; /* autostart / pending */
|
||||||
|
--ch-coral: #ff5e5e; /* offline / error / alarm */
|
||||||
|
--ch-violet: #8b7eff; /* graph / scenes / automations */
|
||||||
|
|
||||||
|
--lux-signal-glow: 0 0 14px color-mix(in srgb, var(--ch-signal) 40%, transparent);
|
||||||
|
--lux-shadow-rack: 0 1px 0 rgba(255, 255, 255, 0.03), 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light theme */
|
/* Light theme */
|
||||||
@@ -120,6 +162,31 @@
|
|||||||
--primary-color-on-light-bg: #2e7d32;
|
--primary-color-on-light-bg: #2e7d32;
|
||||||
--primary-text: #2e7d32;
|
--primary-text: #2e7d32;
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
|
|
||||||
|
/* ── Lumenworks light palette — tuned for WCAG AA on white.
|
||||||
|
Channel colors darkened vs dark theme so they read against
|
||||||
|
near-white surfaces. ── */
|
||||||
|
--lux-bg-0: #f5f6f8;
|
||||||
|
--lux-bg-1: #ffffff;
|
||||||
|
--lux-bg-2: #fafbfc;
|
||||||
|
--lux-bg-3: #eef1f5;
|
||||||
|
--lux-line: #dee3ea;
|
||||||
|
--lux-line-bold:#c4ccd6;
|
||||||
|
--lux-ink: #0f1419;
|
||||||
|
--lux-ink-dim: #4c5866;
|
||||||
|
--lux-ink-mute: #6b7684;
|
||||||
|
--lux-ink-faint:#a5afbc;
|
||||||
|
|
||||||
|
--ch-signal: #008f3f;
|
||||||
|
--ch-signal-dim: #006b2f;
|
||||||
|
--ch-cyan: #006b88;
|
||||||
|
--ch-magenta: #b01a99;
|
||||||
|
--ch-amber: #a56a00;
|
||||||
|
--ch-coral: #d8392e;
|
||||||
|
--ch-violet: #5b4fd0;
|
||||||
|
|
||||||
|
--lux-signal-glow: 0 0 12px color-mix(in srgb, var(--ch-signal) 28%, transparent);
|
||||||
|
--lux-shadow-rack: 0 1px 0 rgba(255, 255, 255, 0.6), 0 6px 18px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Default to dark theme */
|
/* Default to dark theme */
|
||||||
@@ -137,10 +204,12 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
font-family: var(--font-body, 'Manrope', 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
line-height: 1.6;
|
line-height: 1.55;
|
||||||
|
font-feature-settings: "ss01", "cv11";
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.modal-open {
|
html.modal-open {
|
||||||
|
|||||||
@@ -2,41 +2,67 @@ section {
|
|||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Skeleton loading placeholders ── */
|
/* ── Skeleton loading placeholders — subtle shimmer, not a text-color flash */
|
||||||
@keyframes skeletonPulse {
|
@keyframes skeletonShimmer {
|
||||||
0%, 100% { opacity: 0.06; }
|
0% { background-position: -200% 0; }
|
||||||
50% { opacity: 0.12; }
|
100% { background-position: 200% 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-card {
|
.skeleton-card {
|
||||||
background: var(--card-bg);
|
background: var(--lux-bg-1, var(--card-bg));
|
||||||
border: 1px solid var(--border-color);
|
border: var(--lux-hairline, 1px) solid var(--lux-line, var(--border-color));
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--lux-r-md, var(--radius-md));
|
||||||
padding: 16px 20px 20px;
|
padding: 18px 20px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
min-height: 140px;
|
min-height: 140px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small corner bracket + left hairline so the skeleton reads as a module
|
||||||
|
placeholder, not a blank box. */
|
||||||
|
.skeleton-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0; bottom: 0;
|
||||||
|
width: 3px;
|
||||||
|
background: var(--lux-line, var(--border-color));
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.skeleton-card::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 8px; right: 8px;
|
||||||
|
width: 12px; height: 12px;
|
||||||
|
border-top: var(--lux-hairline, 1px) solid var(--lux-line, var(--border-color));
|
||||||
|
border-right: var(--lux-hairline, 1px) solid var(--lux-line, var(--border-color));
|
||||||
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-line {
|
.skeleton-line {
|
||||||
height: 14px;
|
height: 12px;
|
||||||
border-radius: 4px;
|
border-radius: 2px;
|
||||||
background: var(--text-color);
|
background: linear-gradient(90deg,
|
||||||
animation: skeletonPulse 1.5s ease-in-out infinite;
|
var(--lux-bg-2, var(--bg-secondary)) 0%,
|
||||||
|
var(--lux-bg-3, var(--border-color)) 50%,
|
||||||
|
var(--lux-bg-2, var(--bg-secondary)) 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeletonShimmer 2.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-line-title {
|
.skeleton-line-title {
|
||||||
width: 60%;
|
width: 55%;
|
||||||
height: 18px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-line-short {
|
.skeleton-line-short {
|
||||||
width: 40%;
|
width: 35%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-line-medium {
|
.skeleton-line-medium {
|
||||||
width: 75%;
|
width: 72%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-actions {
|
.skeleton-actions {
|
||||||
@@ -44,15 +70,19 @@ section {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: var(--lux-hairline, 1px) dashed var(--lux-line, var(--border-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-btn {
|
.skeleton-btn {
|
||||||
height: 32px;
|
height: 30px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--lux-r-sm, var(--radius-sm));
|
||||||
background: var(--text-color);
|
background: linear-gradient(90deg,
|
||||||
animation: skeletonPulse 1.5s ease-in-out infinite;
|
var(--lux-bg-2, var(--bg-secondary)) 0%,
|
||||||
|
var(--lux-bg-3, var(--border-color)) 50%,
|
||||||
|
var(--lux-bg-2, var(--bg-secondary)) 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeletonShimmer 2.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.displays-grid,
|
.displays-grid,
|
||||||
@@ -104,21 +134,81 @@ section {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: var(--card-bg);
|
--ch: var(--ch-signal, var(--primary-color)); /* channel accent (override per type) */
|
||||||
border: 1px solid var(--border-color);
|
background: linear-gradient(180deg,
|
||||||
border-radius: var(--radius-md);
|
var(--lux-bg-1, var(--card-bg)) 0%,
|
||||||
padding: 16px 20px 20px;
|
var(--lux-bg-2, var(--card-bg)) 100%);
|
||||||
|
border: var(--lux-hairline, 1px) solid var(--lux-line, var(--border-color));
|
||||||
|
border-radius: var(--lux-r-md, var(--radius-md));
|
||||||
|
padding: 18px 20px 16px;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Channel stripe on left edge — color coded by entity type via --ch override */
|
||||||
|
.card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0; bottom: 0;
|
||||||
|
width: 3px;
|
||||||
|
background: var(--ch);
|
||||||
|
box-shadow: 0 0 10px color-mix(in srgb, var(--ch) 40%, transparent);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corner bracket — silkscreened panel feel in the top-right */
|
||||||
|
.card::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 8px; right: 8px;
|
||||||
|
width: 12px; height: 12px;
|
||||||
|
border-top: var(--lux-hairline, 1px) solid var(--lux-line-bold, var(--border-color));
|
||||||
|
border-right: var(--lux-hairline, 1px) solid var(--lux-line-bold, var(--border-color));
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
box-shadow: 0 8px 24px var(--shadow-color);
|
box-shadow: var(--lux-shadow-rack, 0 8px 24px var(--shadow-color));
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
|
border-color: var(--lux-line-bold, var(--border-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Channel color variants — cards can opt in via class or data-attr.
|
||||||
|
Implicit mappings via attributes the JS already emits (no JS changes
|
||||||
|
required). Explicit classes provided as an escape hatch. */
|
||||||
|
.card[data-card-type="led"],
|
||||||
|
.card[data-card-type="target"],
|
||||||
|
.card[data-target-id],
|
||||||
|
.card.ch-signal { --ch: var(--ch-signal, var(--primary-color)); }
|
||||||
|
|
||||||
|
.card[data-card-type="screen"],
|
||||||
|
.card[data-card-type="source"],
|
||||||
|
.card[data-stream-id],
|
||||||
|
.card.ch-cyan { --ch: var(--ch-cyan, var(--info-color)); }
|
||||||
|
|
||||||
|
.card[data-card-type="audio"],
|
||||||
|
.card[data-audio-source-id],
|
||||||
|
.card[data-audio-template-id],
|
||||||
|
.card.ch-magenta { --ch: var(--ch-magenta, #ff4ade); }
|
||||||
|
|
||||||
|
.card[data-card-type="automation"],
|
||||||
|
.card[data-card-type="scene"],
|
||||||
|
.card[data-automation-id],
|
||||||
|
.card[data-scene-id],
|
||||||
|
.card.ch-violet { --ch: var(--ch-violet, #8b7eff); }
|
||||||
|
|
||||||
|
.card[data-card-type="integration"],
|
||||||
|
.card.ch-amber { --ch: var(--ch-amber, var(--warning-color)); }
|
||||||
|
|
||||||
|
.card[data-card-type="offline"],
|
||||||
|
.card.ch-coral { --ch: var(--ch-coral, var(--danger-color)); }
|
||||||
|
|
||||||
/* ── Card glare effect ── */
|
/* ── Card glare effect ── */
|
||||||
.card-glare::after,
|
.card-glare::after,
|
||||||
.template-card.card-glare::after,
|
.template-card.card-glare::after,
|
||||||
@@ -175,114 +265,62 @@ section {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Running target: rotating gradient border ── */
|
/* ── Running module: channel stripe intensifies + signal-flow strip at the
|
||||||
@property --border-angle {
|
bottom edge ("patched and live" indicator). Lightweight replacement
|
||||||
syntax: '<angle>';
|
for the old rotating conic-gradient border — ~1 animated gradient on
|
||||||
initial-value: 0deg;
|
a 2 px line, no GPU layer compositing needed per card. */
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-running {
|
.card-running {
|
||||||
border-color: transparent;
|
border-color: color-mix(in srgb, var(--ch) 35%, var(--lux-line, var(--border-color)));
|
||||||
background: linear-gradient(
|
box-shadow:
|
||||||
calc(var(--border-angle) + 45deg),
|
0 0 0 1px color-mix(in srgb, var(--ch) 20%, transparent),
|
||||||
var(--card-bg) 0%,
|
0 6px 20px rgba(0, 0, 0, 0.25);
|
||||||
color-mix(in srgb, var(--primary-color) 12%, var(--card-bg)) 40%,
|
|
||||||
var(--card-bg) 60%,
|
|
||||||
color-mix(in srgb, var(--primary-color) 8%, var(--card-bg)) 85%,
|
|
||||||
var(--card-bg) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* When card has a custom color stripe, keep it and shift the animated border away from the left edge */
|
|
||||||
.card-running[data-has-color]::before {
|
|
||||||
inset: 0 0 0 3px;
|
|
||||||
border-left: none;
|
|
||||||
border-radius: 0 8px 8px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-running::before {
|
.card-running::before {
|
||||||
content: '';
|
width: 4px;
|
||||||
position: absolute;
|
box-shadow:
|
||||||
inset: 0;
|
0 0 14px color-mix(in srgb, var(--ch) 65%, transparent),
|
||||||
border-radius: inherit;
|
0 0 4px color-mix(in srgb, var(--ch) 80%, transparent);
|
||||||
border: 2px solid transparent;
|
|
||||||
background:
|
|
||||||
conic-gradient(
|
|
||||||
from var(--border-angle),
|
|
||||||
var(--primary-color),
|
|
||||||
rgba(255,255,255,0.1) 25%,
|
|
||||||
var(--primary-color) 50%,
|
|
||||||
rgba(255,255,255,0.1) 75%,
|
|
||||||
var(--primary-color)
|
|
||||||
) border-box;
|
|
||||||
-webkit-mask:
|
|
||||||
linear-gradient(#fff 0 0) padding-box,
|
|
||||||
linear-gradient(#fff 0 0);
|
|
||||||
-webkit-mask-composite: xor;
|
|
||||||
mask:
|
|
||||||
linear-gradient(#fff 0 0) padding-box,
|
|
||||||
linear-gradient(#fff 0 0);
|
|
||||||
mask-composite: exclude;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 2;
|
|
||||||
animation: rotateBorder 4s linear infinite;
|
|
||||||
/* Promote to its own GPU layer so the rotating conic-gradient does not
|
|
||||||
force repaints of the whole card. */
|
|
||||||
will-change: transform;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Honor user preference for reduced motion — base.css globally clamps
|
/* Signal-flow strip — running cards replace the corner bracket with a
|
||||||
animation durations, but the rotating border is decorative and we'd rather
|
moving gradient along the bottom edge (the "patched and live"
|
||||||
not run it at all for these users. */
|
indicator). Idle cards keep the corner bracket. */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
.card-running::after {
|
||||||
.card-running::before {
|
top: auto; right: auto;
|
||||||
animation: none;
|
left: 4px; bottom: 0;
|
||||||
}
|
width: calc(100% - 4px);
|
||||||
}
|
height: 2px;
|
||||||
|
border: none;
|
||||||
/* TODO(perf): pause animation when the card scrolls off-screen via an
|
|
||||||
IntersectionObserver toggling `animation-play-state: paused`. Not done in
|
|
||||||
CSS-only pass — would require a JS hook in card lifecycle. */
|
|
||||||
|
|
||||||
/* Fallback for browsers without mask-composite support (older Firefox) */
|
|
||||||
@supports not (mask-composite: exclude) {
|
|
||||||
.card-running::before {
|
|
||||||
-webkit-mask: none;
|
|
||||||
mask: none;
|
|
||||||
background: none;
|
|
||||||
border: 2px solid var(--primary-color);
|
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotateBorder {
|
|
||||||
to { --border-angle: 360deg; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .card-running {
|
|
||||||
background: linear-gradient(
|
|
||||||
calc(var(--border-angle) + 45deg),
|
|
||||||
var(--card-bg) 0%,
|
|
||||||
color-mix(in srgb, var(--primary-color) 18%, var(--card-bg)) 40%,
|
|
||||||
var(--card-bg) 60%,
|
|
||||||
color-mix(in srgb, var(--primary-color) 14%, var(--card-bg)) 85%,
|
|
||||||
var(--card-bg) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .card-running::before {
|
|
||||||
background:
|
background:
|
||||||
conic-gradient(
|
linear-gradient(90deg,
|
||||||
from var(--border-angle),
|
transparent 0%,
|
||||||
var(--primary-color),
|
color-mix(in srgb, var(--ch) 85%, transparent) 50%,
|
||||||
rgba(0,0,0,0.12) 25%,
|
transparent 100%);
|
||||||
var(--primary-color) 50%,
|
background-size: 30% 100%;
|
||||||
rgba(0,0,0,0.12) 75%,
|
background-repeat: no-repeat;
|
||||||
var(--primary-color)
|
animation: signalFlow 2.4s linear infinite;
|
||||||
) border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes signalFlow {
|
||||||
|
0% { background-position: -30% 0; }
|
||||||
|
100% { background-position: 130% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.card-running::after {
|
||||||
|
animation: none;
|
||||||
|
background-position: 50% 0;
|
||||||
|
background-size: 60% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep the corner bracket visible when NOT running (default),
|
||||||
|
and replace it with the signal flow when running (above).
|
||||||
|
No extra work needed — `.card::after` rules below cover this. */
|
||||||
|
|
||||||
/* ── Card entrance animation ── */
|
/* ── Card entrance animation ── */
|
||||||
@keyframes cardEnter {
|
@keyframes cardEnter {
|
||||||
from { opacity: 0; transform: translateY(12px); }
|
from { opacity: 0; transform: translateY(12px); }
|
||||||
|
|||||||
@@ -36,19 +36,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 8px 16px;
|
padding: 9px 18px;
|
||||||
border: none;
|
border: var(--lux-hairline, 1px) solid transparent;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--lux-r-sm, var(--radius-sm));
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.9rem;
|
font-family: var(--font-mono, inherit);
|
||||||
|
font-size: 0.78rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: opacity 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition: opacity 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease, filter 0.15s ease;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover {
|
.btn:hover {
|
||||||
opacity: 0.9;
|
filter: brightness(1.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:active:not(:disabled) {
|
.btn:active:not(:disabled) {
|
||||||
@@ -62,18 +69,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--primary-color);
|
background: var(--ch-signal, var(--primary-color));
|
||||||
color: var(--primary-contrast);
|
color: var(--lux-bg-0, var(--primary-contrast));
|
||||||
|
border-color: var(--ch-signal, var(--primary-color));
|
||||||
|
box-shadow: 0 0 14px color-mix(in srgb, var(--ch-signal, var(--primary-color)) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background: var(--danger-color);
|
background: var(--ch-coral, var(--danger-color));
|
||||||
color: white;
|
color: #fff;
|
||||||
|
border-color: var(--ch-coral, var(--danger-color));
|
||||||
|
box-shadow: 0 0 14px color-mix(in srgb, var(--ch-coral, var(--danger-color)) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: var(--border-color);
|
background: var(--lux-bg-2, var(--border-color));
|
||||||
color: var(--text-color);
|
color: var(--lux-ink-dim, var(--text-color));
|
||||||
|
border-color: var(--lux-line-bold, var(--border-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
color: var(--lux-ink, var(--text-color));
|
||||||
|
background: var(--lux-bg-3, var(--border-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
@@ -161,14 +178,29 @@ input[type="number"],
|
|||||||
input[type="password"],
|
input[type="password"],
|
||||||
select {
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 9px 12px;
|
||||||
border: 1px solid var(--border-color);
|
border: var(--lux-hairline, 1px) solid var(--lux-line, var(--border-color));
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--lux-r-sm, var(--radius-sm));
|
||||||
background: var(--bg-color);
|
background: var(--lux-bg-0, var(--bg-color));
|
||||||
color: var(--text-color);
|
color: var(--lux-ink, var(--text-color));
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
font-family: var(--font-body, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
||||||
transition: border-color 0.25s ease, box-shadow 0.25s ease, opacity 0.2s ease;
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Numeric fields use mono for alignment */
|
||||||
|
input[type="number"] {
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:hover,
|
||||||
|
input[type="url"]:hover,
|
||||||
|
input[type="number"]:hover,
|
||||||
|
input[type="password"]:hover,
|
||||||
|
select:hover {
|
||||||
|
border-color: var(--lux-line-bold, var(--border-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"]:disabled,
|
input[type="number"]:disabled,
|
||||||
@@ -190,10 +222,14 @@ input[type="password"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input:focus,
|
input:focus,
|
||||||
select:focus {
|
select:focus,
|
||||||
|
textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--primary-color);
|
border-color: var(--ch-signal, var(--primary-color));
|
||||||
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.15);
|
box-shadow:
|
||||||
|
0 0 0 3px color-mix(in srgb, var(--ch-signal, var(--primary-color)) 18%, transparent),
|
||||||
|
0 0 16px color-mix(in srgb, var(--ch-signal, var(--primary-color)) 20%, transparent);
|
||||||
|
background: var(--lux-bg-1, var(--bg-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inline validation states */
|
/* Inline validation states */
|
||||||
|
|||||||
@@ -5,16 +5,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-section-header {
|
.dashboard-section-header {
|
||||||
font-size: 0.8rem;
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 0.68rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 10px;
|
||||||
color: var(--text-secondary);
|
color: var(--lux-ink-dim, var(--text-secondary));
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.22em;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
border-bottom: var(--lux-hairline, 1px) dashed var(--lux-line, var(--border-color));
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-section-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; bottom: -1px;
|
||||||
|
width: 40px;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--ch-signal, var(--primary-color));
|
||||||
|
box-shadow: 0 0 6px color-mix(in srgb, var(--ch-signal, var(--primary-color)) 50%, transparent);
|
||||||
}
|
}
|
||||||
.dashboard-section-toggle {
|
.dashboard-section-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -91,27 +105,77 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-target {
|
.dashboard-target {
|
||||||
|
--ch: var(--ch-signal, var(--primary-color));
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto auto;
|
grid-template-columns: 1fr auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 6px 12px;
|
padding: 10px 14px 10px 18px;
|
||||||
background: var(--card-bg);
|
background: linear-gradient(180deg,
|
||||||
border: 1px solid var(--border-color);
|
var(--lux-bg-1, var(--card-bg)) 0%,
|
||||||
border-radius: 6px;
|
var(--lux-bg-2, var(--card-bg)) 100%);
|
||||||
|
border: var(--lux-hairline, 1px) solid var(--lux-line, var(--border-color));
|
||||||
|
border-radius: var(--lux-r-md, 6px);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Channel stripe on left edge */
|
||||||
|
.dashboard-target::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: 0.6;
|
||||||
|
transition: opacity 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-target:hover {
|
.dashboard-target:hover {
|
||||||
box-shadow: 0 2px 8px var(--shadow-color);
|
box-shadow: var(--lux-shadow-rack, 0 2px 8px var(--shadow-color));
|
||||||
|
border-color: var(--lux-line-bold, var(--border-color));
|
||||||
|
transform: translateX(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-target:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0 0 14px color-mix(in srgb, var(--ch) 70%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-card-link:hover {
|
.dashboard-card-link:hover {
|
||||||
border-color: var(--primary-color);
|
border-color: color-mix(in srgb, var(--ch) 40%, var(--lux-line, var(--border-color)));
|
||||||
box-shadow: 0 4px 12px var(--shadow-color);
|
box-shadow: 0 4px 14px var(--shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Running rows glow brighter and emit a signal-flow strip along the bottom */
|
||||||
|
.dashboard-target[data-target-id]:has([data-fps-text])::before,
|
||||||
|
.dashboard-target.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-target[data-target-id]:has([data-fps-text])::after,
|
||||||
|
.dashboard-target.is-running::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 4px; bottom: 0;
|
||||||
|
width: calc(100% - 4px);
|
||||||
|
height: 1px;
|
||||||
|
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;
|
||||||
|
opacity: 0.7;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-target-info {
|
.dashboard-target-info {
|
||||||
@@ -134,14 +198,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-target-name {
|
.dashboard-target-name {
|
||||||
font-size: 0.85rem;
|
font-size: 0.88rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.005em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
color: var(--lux-ink, var(--text-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-target-name-text {
|
.dashboard-target-name-text {
|
||||||
@@ -159,11 +225,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-target-subtitle {
|
.dashboard-target-subtitle {
|
||||||
font-size: 0.7rem;
|
font-family: var(--font-mono, monospace);
|
||||||
color: var(--text-secondary);
|
font-size: 0.64rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--lux-ink-mute, var(--text-secondary));
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-target-metrics {
|
.dashboard-target-metrics {
|
||||||
@@ -179,19 +248,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-metric-value {
|
.dashboard-metric-value {
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
line-height: 1.2;
|
|
||||||
font-family: var(--font-mono, monospace);
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ch, var(--primary-text-color));
|
||||||
|
line-height: 1.2;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-metric-label {
|
.dashboard-metric-label {
|
||||||
font-size: 0.6rem;
|
font-family: var(--font-mono, monospace);
|
||||||
color: var(--text-secondary);
|
font-size: 0.55rem;
|
||||||
|
color: var(--lux-ink-mute, var(--text-secondary));
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.3px;
|
letter-spacing: 0.15em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-fps-metric {
|
.dashboard-fps-metric {
|
||||||
@@ -386,36 +458,61 @@
|
|||||||
|
|
||||||
/* ── Per-metric accent colors ── */
|
/* ── Per-metric accent colors ── */
|
||||||
.perf-chart-card {
|
.perf-chart-card {
|
||||||
--perf-accent: var(--primary-color);
|
--perf-accent: var(--ch-signal, var(--primary-color));
|
||||||
--perf-accent-glow: transparent;
|
--perf-accent-glow: color-mix(in srgb, var(--perf-accent) 18%, transparent);
|
||||||
background: var(--card-bg);
|
background: linear-gradient(180deg,
|
||||||
border: 1px solid var(--border-color);
|
var(--lux-bg-1, var(--card-bg)) 0%,
|
||||||
border-top: 3px solid var(--perf-accent);
|
var(--lux-bg-2, var(--card-bg)) 100%);
|
||||||
border-radius: 6px;
|
border: var(--lux-hairline, 1px) solid var(--lux-line, var(--border-color));
|
||||||
padding: 10px 0 0;
|
border-radius: var(--lux-r-md, 6px);
|
||||||
|
padding: 12px 0 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: box-shadow var(--duration-normal) ease;
|
transition: box-shadow var(--duration-normal) ease, border-color var(--duration-normal) ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Channel stripe on left edge (replaces the old border-top accent) */
|
||||||
|
.perf-chart-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0; bottom: 0;
|
||||||
|
width: 3px;
|
||||||
|
background: var(--perf-accent);
|
||||||
|
box-shadow: 0 0 10px var(--perf-accent-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corner bracket in the top-right */
|
||||||
|
.perf-chart-card::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 8px; right: 8px;
|
||||||
|
width: 12px; height: 12px;
|
||||||
|
border-top: var(--lux-hairline, 1px) solid var(--lux-line-bold, var(--border-color));
|
||||||
|
border-right: var(--lux-hairline, 1px) solid var(--lux-line-bold, var(--border-color));
|
||||||
|
opacity: 0.7;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.perf-chart-card:hover {
|
.perf-chart-card:hover {
|
||||||
box-shadow: 0 0 16px var(--perf-accent-glow);
|
box-shadow: 0 4px 20px var(--perf-accent-glow), var(--lux-shadow-rack, 0 0 0 transparent);
|
||||||
|
border-color: var(--lux-line-bold, var(--border-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
.perf-chart-card[data-metric="cpu"] {
|
.perf-chart-card[data-metric="cpu"] {
|
||||||
--perf-accent: #FF6B6B;
|
--perf-accent: var(--ch-coral, #FF6B6B);
|
||||||
--perf-accent-glow: rgba(255, 107, 107, 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.perf-chart-card[data-metric="ram"] {
|
.perf-chart-card[data-metric="ram"] {
|
||||||
--perf-accent: #A855F7;
|
--perf-accent: var(--ch-violet, #A855F7);
|
||||||
--perf-accent-glow: rgba(168, 85, 247, 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.perf-chart-card[data-metric="gpu"] {
|
.perf-chart-card[data-metric="gpu"] {
|
||||||
--perf-accent: #10B981;
|
--perf-accent: var(--ch-signal, #10B981);
|
||||||
--perf-accent-glow: rgba(16, 185, 129, 0.12);
|
}
|
||||||
|
|
||||||
|
.perf-chart-card[data-metric="temp"] {
|
||||||
|
--perf-accent: var(--ch-amber, #FFB800);
|
||||||
}
|
}
|
||||||
|
|
||||||
.perf-chart-wrap {
|
.perf-chart-wrap {
|
||||||
@@ -443,10 +540,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.perf-chart-label {
|
.perf-chart-label {
|
||||||
font-size: 0.75rem;
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 0.62rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.3px;
|
letter-spacing: 0.22em;
|
||||||
color: var(--perf-accent);
|
color: var(--perf-accent);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -481,13 +579,15 @@
|
|||||||
|
|
||||||
/* ── Value display ── */
|
/* ── Value display ── */
|
||||||
.perf-chart-value {
|
.perf-chart-value {
|
||||||
font-size: 0.85rem;
|
font-size: 0.92rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--perf-accent);
|
color: var(--perf-accent);
|
||||||
font-family: var(--font-mono, monospace);
|
font-family: var(--font-mono, monospace);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-shadow: 0 0 12px color-mix(in srgb, var(--perf-accent) 40%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* App value shown as subdued tag in "both" mode */
|
/* App value shown as subdued tag in "both" mode */
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* Local font faces — no external CDN dependency */
|
/* Local font faces — no external CDN dependency */
|
||||||
|
|
||||||
/* DM Sans — latin-ext */
|
/* ── DM Sans (legacy body font — kept during redesign transition) ── */
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'DM Sans';
|
font-family: 'DM Sans';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -10,7 +11,6 @@
|
|||||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* DM Sans — latin */
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'DM Sans';
|
font-family: 'DM Sans';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Orbitron 700 — latin */
|
/* ── Orbitron (brand mark only) ── */
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Orbitron';
|
font-family: 'Orbitron';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -29,3 +30,99 @@
|
|||||||
src: url('../fonts/orbitron-700-latin.woff2') format('woff2');
|
src: url('../fonts/orbitron-700-latin.woff2') format('woff2');
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Manrope — new primary body font (variable, 200..800) ──
|
||||||
|
Covers Latin, Latin-ext, Cyrillic, Cyrillic-ext. CJK falls through to
|
||||||
|
system stack via the body font-family cascade. */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Manrope';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 200 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('../fonts/manrope-cyrillic-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Manrope';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 200 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('../fonts/manrope-cyrillic.woff2') format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Manrope';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 200 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('../fonts/manrope-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Manrope';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 200 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('../fonts/manrope-latin.woff2') format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── JetBrains Mono — new monospace (variable, 100..800) ──
|
||||||
|
Used for technical labels, badges, metrics, code. Cyrillic-capable so
|
||||||
|
badge text (`CH·01 · WLED`) reads in RU locale. */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('../fonts/jetbrains-mono-cyrillic-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('../fonts/jetbrains-mono-cyrillic.woff2') format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('../fonts/jetbrains-mono-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('../fonts/jetbrains-mono-latin.woff2') format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Big Shoulders Display — new display font (variable, 100..900) ──
|
||||||
|
Reserved for huge numeric readouts on the dashboard hero + module metric
|
||||||
|
cells. Latin + Latin-ext only; Cyrillic numerics would rarely occur in
|
||||||
|
that position so the system stack is an acceptable fallback. */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Big Shoulders Display';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('../fonts/big-shoulders-display-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Big Shoulders Display';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('../fonts/big-shoulders-display-latin.woff2') format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,11 +31,15 @@ html:has(#tab-graph.active) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
background: var(--card-bg);
|
background: linear-gradient(180deg,
|
||||||
border: 1px solid var(--border-color);
|
var(--lux-bg-1, var(--card-bg)) 0%,
|
||||||
border-radius: 8px;
|
var(--lux-bg-2, var(--card-bg)) 100%);
|
||||||
|
border: var(--lux-hairline, 1px) solid var(--lux-line-bold, var(--border-color));
|
||||||
|
border-radius: var(--lux-r-md, 6px);
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
box-shadow: 0 2px 8px var(--shadow-color);
|
box-shadow: var(--lux-shadow-rack, 0 2px 8px var(--shadow-color));
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-toolbar-drag {
|
.graph-toolbar-drag {
|
||||||
|
|||||||
@@ -1,26 +1,84 @@
|
|||||||
|
:root {
|
||||||
|
--transport-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: grid;
|
||||||
justify-content: space-between;
|
grid-template-columns: var(--sidebar-width, 248px) 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 20px;
|
height: var(--transport-height, 60px);
|
||||||
|
padding: 0 16px 0 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: var(--z-sticky);
|
z-index: var(--z-sticky);
|
||||||
background: var(--bg-color);
|
background: linear-gradient(180deg,
|
||||||
border-bottom: 2px solid var(--border-color);
|
var(--lux-bg-1, var(--bg-color)) 0%,
|
||||||
|
var(--lux-bg-0, var(--bg-color)) 100%);
|
||||||
|
border-bottom: var(--lux-hairline, 1px) solid var(--lux-line-bold, var(--border-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accent rule — subtle bottom glow under the transport bar.
|
||||||
|
Uses ::before because ::after is reserved by base.css for the
|
||||||
|
ambient-background blur overlay. */
|
||||||
|
header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; bottom: -1px;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
transparent 0%,
|
||||||
|
color-mix(in srgb, var(--ch-signal, var(--primary-color)) 30%, transparent) 15%,
|
||||||
|
color-mix(in srgb, var(--ch-cyan, var(--primary-color)) 25%, transparent) 50%,
|
||||||
|
color-mix(in srgb, var(--ch-magenta, var(--primary-color)) 20%, transparent) 85%,
|
||||||
|
transparent 100%);
|
||||||
|
opacity: 0.8;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-title {
|
.header-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
|
padding: 0 18px;
|
||||||
|
height: 100%;
|
||||||
|
border-right: var(--lux-hairline, 1px) solid var(--lux-line-bold, var(--border-color));
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glowing LED brand mark. Rendered as a ::before on .header-title so no
|
||||||
|
HTML change is required. The existing #server-status pulse dot sits
|
||||||
|
inside as the "core" of the mark (see status-badge rule below). */
|
||||||
|
.header-title::before {
|
||||||
|
content: '';
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--ch-signal, var(--primary-color));
|
||||||
|
box-shadow:
|
||||||
|
var(--lux-signal-glow, 0 0 14px color-mix(in srgb, var(--primary-color) 40%, transparent)),
|
||||||
|
inset 0 0 0 1px rgba(0, 0, 0, 0.3);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.header-title::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: calc(18px + 7px);
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: var(--lux-bg-0, var(--bg-color));
|
||||||
|
border-radius: 1px;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-family: 'Orbitron', sans-serif;
|
font-family: 'Orbitron', sans-serif;
|
||||||
font-size: 1.15rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.2em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
-webkit-text-stroke: 0.5px var(--primary-color);
|
-webkit-text-stroke: 0.5px var(--primary-color);
|
||||||
paint-order: stroke fill;
|
paint-order: stroke fill;
|
||||||
@@ -37,6 +95,7 @@ h1 {
|
|||||||
background-clip: text;
|
background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
animation: titleShimmer 6s ease-in-out infinite;
|
animation: titleShimmer 6s ease-in-out infinite;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes titleShimmer {
|
@keyframes titleShimmer {
|
||||||
@@ -44,6 +103,55 @@ h1 {
|
|||||||
50% { background-position: 0% 50%; }
|
50% { background-position: 0% 50%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Transport center: reserved area for armed-status / master-stop /
|
||||||
|
quick-search shortcut. Populated by JS in Phase 3; empty for now. ── */
|
||||||
|
.transport-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 18px;
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--lux-ink-dim, var(--text-secondary));
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transport-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--lux-bg-2, var(--bg-secondary));
|
||||||
|
border: var(--lux-hairline, 1px) solid var(--lux-line, var(--border-color));
|
||||||
|
border-radius: var(--lux-r-sm, 3px);
|
||||||
|
color: var(--lux-ink-dim, var(--text-secondary));
|
||||||
|
font-size: 0.68rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color 0.2s, border-color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transport-status.is-armed {
|
||||||
|
color: var(--ch-signal, var(--primary-color));
|
||||||
|
border-color: color-mix(in srgb, var(--ch-signal, var(--primary-color)) 40%, transparent);
|
||||||
|
background: color-mix(in srgb, var(--ch-signal, var(--primary-color)) 8%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transport-status .dot {
|
||||||
|
width: 6px; height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
box-shadow: 0 0 6px currentColor;
|
||||||
|
animation: pulse 1.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.transport-status:not(.is-armed) .dot {
|
||||||
|
background: var(--lux-ink-faint, var(--text-muted));
|
||||||
|
box-shadow: none;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
@@ -54,16 +162,16 @@ h2 {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
background: var(--card-bg);
|
background: var(--lux-bg-1, var(--card-bg));
|
||||||
border: 1px solid var(--border-color);
|
border: var(--lux-hairline, 1px) solid var(--lux-line, var(--border-color));
|
||||||
border-radius: 8px;
|
border-radius: var(--lux-r-sm, 3px);
|
||||||
padding: 3px 4px;
|
padding: 3px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-toolbar-sep {
|
.header-toolbar-sep {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 18px;
|
height: 16px;
|
||||||
background: var(--border-color);
|
background: var(--lux-line, var(--border-color));
|
||||||
margin: 0 3px;
|
margin: 0 3px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@@ -156,14 +264,16 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#server-version {
|
#server-version {
|
||||||
font-family: 'Orbitron', sans-serif;
|
font-family: var(--font-mono, 'Orbitron', sans-serif);
|
||||||
font-size: 0.65rem;
|
font-size: 0.55rem;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: var(--text-secondary);
|
color: var(--lux-ink-mute, var(--text-secondary));
|
||||||
background: var(--border-color);
|
background: transparent;
|
||||||
padding: 2px 8px;
|
border: var(--lux-hairline, 1px) solid var(--lux-line, var(--border-color));
|
||||||
border-radius: 10px;
|
padding: 2px 6px;
|
||||||
letter-spacing: 0.03em;
|
border-radius: 2px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
transition: background 0.3s, color 0.3s, box-shadow 0.3s;
|
transition: background 0.3s, color 0.3s, box-shadow 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,17 +380,35 @@ h2 {
|
|||||||
to { transform: translateY(0); opacity: 1; }
|
to { transform: translateY(0); opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* #server-status is overlaid on the brand mark as a small LED pip that
|
||||||
|
changes color based on connection. Inline element rendered via text node
|
||||||
|
('●'); we hide the glyph and use ::before for a clean circular dot so
|
||||||
|
sizing is consistent across fonts. */
|
||||||
.status-badge {
|
.status-badge {
|
||||||
font-size: 1rem;
|
position: absolute;
|
||||||
|
left: calc(18px + 13px);
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: transparent; /* hide the '●' text glyph */
|
||||||
|
font-size: 0;
|
||||||
|
background: var(--lux-bg-0, var(--bg-color));
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4), 0 0 6px var(--ch-signal, var(--primary-color));
|
||||||
|
z-index: 2;
|
||||||
animation: pulse 2s infinite;
|
animation: pulse 2s infinite;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.online {
|
.status-badge.online {
|
||||||
color: var(--primary-color);
|
background: var(--ch-signal, var(--primary-color));
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4), 0 0 8px var(--ch-signal, var(--primary-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.offline {
|
.status-badge.offline {
|
||||||
color: var(--danger-color);
|
background: var(--ch-coral, var(--danger-color));
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4), 0 0 8px var(--ch-coral, var(--danger-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
@@ -441,7 +569,8 @@ h2 {
|
|||||||
color: var(--danger-color);
|
color: var(--danger-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tabs */
|
/* ── Tabs (base styles; sidebar.css re-specializes for vertical rail;
|
||||||
|
mobile.css reverts to a fixed bottom bar on phones) ── */
|
||||||
.tab-bar {
|
.tab-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -458,7 +587,10 @@ h2 {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
margin-bottom: -2px;
|
margin-bottom: -2px;
|
||||||
transition: color 0.2s ease, border-color 0.25s ease;
|
transition: color 0.2s ease, border-color 0.25s ease, background 0.2s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-btn:hover {
|
.tab-btn:hover {
|
||||||
@@ -825,29 +957,45 @@ h2 {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* On narrow screens the brand column shrinks to just the mark; on phones
|
||||||
|
the sidebar hides entirely and mobile.css reverts .tab-bar to a fixed
|
||||||
|
bottom strip. */
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.tab-btn {
|
header {
|
||||||
padding: 10px 10px;
|
grid-template-columns: var(--sidebar-width, 56px) 1fr auto;
|
||||||
}
|
}
|
||||||
|
.header-title {
|
||||||
.tab-btn > span[data-i18n] {
|
padding: 0 10px;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
.header-title h1,
|
||||||
|
#server-version,
|
||||||
|
.header-title::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.transport-center {
|
||||||
.tab-btn .icon {
|
padding: 0 12px;
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
padding: 12px;
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
header {
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
padding: 0 8px 0 0;
|
||||||
|
}
|
||||||
|
.header-title {
|
||||||
|
padding: 0 10px;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.transport-center {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,53 +158,122 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Bottom Tab Bar ── */
|
/* ── Bottom Tab Bar — Lumenworks mobile shell ── */
|
||||||
|
.sidebar .tab-bar,
|
||||||
.tab-bar {
|
.tab-bar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: var(--z-sticky);
|
z-index: var(--z-sticky);
|
||||||
background: var(--card-bg);
|
background: linear-gradient(180deg,
|
||||||
|
var(--lux-bg-1, var(--card-bg)) 0%,
|
||||||
|
var(--lux-bg-0, var(--card-bg)) 100%);
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: var(--lux-hairline, 1px) solid var(--lux-line-bold, var(--border-color));
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
box-shadow: 0 -2px 8px var(--shadow-color);
|
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.3);
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Top channel-accent rule — matches the transport bar bottom rule so
|
||||||
|
the two bars feel like bookends of the mobile layout. */
|
||||||
|
.sidebar .tab-bar::before,
|
||||||
|
.tab-bar::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; top: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
transparent 0%,
|
||||||
|
color-mix(in srgb, var(--ch-signal, var(--primary-color)) 28%, transparent) 15%,
|
||||||
|
color-mix(in srgb, var(--ch-cyan, var(--info-color)) 24%, transparent) 50%,
|
||||||
|
color-mix(in srgb, var(--ch-magenta, #ff4ade) 20%, transparent) 85%,
|
||||||
|
transparent 100%);
|
||||||
|
opacity: 0.8;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .tab-btn,
|
||||||
.tab-btn {
|
.tab-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
justify-content: center;
|
||||||
padding: 8px 4px 6px;
|
gap: 3px;
|
||||||
font-size: 0.65rem;
|
padding: 7px 4px 6px;
|
||||||
|
font-family: var(--font-mono, inherit);
|
||||||
|
font-size: 0.55rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--lux-ink-mute, var(--text-secondary));
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-top: 2px solid transparent;
|
border-top: 2px solid transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
grid-template-columns: none;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar .tab-btn.active,
|
||||||
.tab-btn.active {
|
.tab-btn.active {
|
||||||
|
color: var(--ch-signal, var(--primary-color));
|
||||||
border-bottom-color: transparent;
|
border-bottom-color: transparent;
|
||||||
border-top-color: var(--primary-color);
|
border-top-color: var(--ch-signal, var(--primary-color));
|
||||||
|
background: linear-gradient(180deg,
|
||||||
|
color-mix(in srgb, var(--ch-signal, var(--primary-color)) 14%, transparent) 0%,
|
||||||
|
transparent 60%);
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* LED pip above the icon on the active tab (replaces the left-stripe
|
||||||
|
since the sidebar's box-shadow doesn't carry here). */
|
||||||
|
.sidebar .tab-btn.active::before,
|
||||||
|
.tab-btn.active::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 4px; left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--ch-signal, var(--primary-color));
|
||||||
|
box-shadow: 0 0 6px var(--ch-signal, var(--primary-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .tab-btn .icon,
|
||||||
.tab-btn .icon {
|
.tab-btn .icon {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
display: block;
|
display: block;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar .tab-btn.active .icon,
|
||||||
|
.tab-btn.active .icon {
|
||||||
|
color: var(--ch-signal, var(--primary-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .tab-btn > span[data-i18n],
|
||||||
.tab-btn > span[data-i18n] {
|
.tab-btn > span[data-i18n] {
|
||||||
font-size: 0.6rem;
|
font-family: var(--font-mono, inherit);
|
||||||
|
font-size: 0.55rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -215,13 +284,19 @@
|
|||||||
/* Tab badge repositioned to top-right of icon */
|
/* Tab badge repositioned to top-right of icon */
|
||||||
.tab-badge {
|
.tab-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 6px;
|
||||||
right: calc(50% - 18px);
|
right: calc(50% - 20px);
|
||||||
font-size: 0.55rem;
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 0.48rem;
|
||||||
|
font-weight: 700;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
min-width: 14px;
|
min-width: 12px;
|
||||||
line-height: 1.2;
|
line-height: 1.3;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
background: var(--ch-signal, var(--primary-color));
|
||||||
|
color: var(--lux-bg-0, var(--primary-contrast));
|
||||||
|
border-radius: 2px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Body padding for fixed bottom bar */
|
/* Body padding for fixed bottom bar */
|
||||||
@@ -276,6 +351,12 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide the bottom-right corner bracket on fullscreen mobile modals —
|
||||||
|
there's no "panel" to decorate. Top channel rule stays. */
|
||||||
|
.modal-content::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-content-wide {
|
.modal-content-wide {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -284,11 +365,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
padding: 12px 14px 10px;
|
padding: 14px 14px 12px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header::before {
|
||||||
|
left: 8px;
|
||||||
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header h2 {
|
.modal-header h2 {
|
||||||
font-size: 1.15rem;
|
font-size: 1.05rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
|
|||||||
@@ -6,12 +6,15 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(0, 0, 0, 0.8);
|
background: radial-gradient(1200px 800px at 50% 40%,
|
||||||
|
rgba(0, 0, 0, 0.7) 0%,
|
||||||
|
rgba(0, 0, 0, 0.88) 100%);
|
||||||
z-index: var(--z-modal);
|
z-index: var(--z-modal);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
animation: fadeIn 0.2s ease-out;
|
animation: fadeIn 0.2s ease-out;
|
||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(6px);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Confirm dialog must stack above all other modals */
|
/* Confirm dialog must stack above all other modals */
|
||||||
@@ -784,18 +787,103 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: var(--card-bg);
|
--modal-ch: var(--ch-signal, var(--primary-color));
|
||||||
border: 1px solid var(--border-color);
|
background: linear-gradient(180deg,
|
||||||
border-radius: var(--radius-lg);
|
var(--lux-bg-1, var(--card-bg)) 0%,
|
||||||
|
var(--lux-bg-2, var(--card-bg)) 100%);
|
||||||
|
border: var(--lux-hairline, 1px) solid var(--lux-line-bold, var(--border-color));
|
||||||
|
border-radius: var(--lux-r-lg, var(--radius-lg));
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-height: calc(100vh - 40px);
|
max-height: calc(100vh - 40px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-shadow: 0 8px 32px var(--shadow-color);
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.02),
|
||||||
|
0 20px 60px rgba(0, 0, 0, 0.5),
|
||||||
|
0 8px 32px var(--shadow-color);
|
||||||
animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Channel accent rule across the top edge of every modal. Type-specific
|
||||||
|
modals can override `--modal-ch` to get a different stripe color. */
|
||||||
|
.modal-content::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; top: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
transparent 0%,
|
||||||
|
var(--modal-ch) 20%,
|
||||||
|
var(--modal-ch) 80%,
|
||||||
|
transparent 100%);
|
||||||
|
opacity: 0.9;
|
||||||
|
box-shadow: 0 0 12px color-mix(in srgb, var(--modal-ch) 50%, transparent);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corner bracket — silkscreened panel feel, bottom-right this time so
|
||||||
|
it doesn't clash with the header-actions row in the top-right. */
|
||||||
|
.modal-content::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 10px; bottom: 10px;
|
||||||
|
width: 12px; height: 12px;
|
||||||
|
border-bottom: var(--lux-hairline, 1px) solid var(--lux-line-bold, var(--border-color));
|
||||||
|
border-right: var(--lux-hairline, 1px) solid var(--lux-line-bold, var(--border-color));
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Per-modal channel colors — map well-known modal IDs to channel lanes.
|
||||||
|
Modals not listed keep the default green stripe. */
|
||||||
|
#target-editor-modal .modal-content,
|
||||||
|
#add-device-modal .modal-content,
|
||||||
|
#device-settings-modal .modal-content,
|
||||||
|
#ha-light-editor-modal .modal-content,
|
||||||
|
#calibration-modal .modal-content { --modal-ch: var(--ch-signal, var(--primary-color)); }
|
||||||
|
|
||||||
|
#stream-modal .modal-content,
|
||||||
|
#test-stream-modal .modal-content,
|
||||||
|
#capture-template-modal .modal-content,
|
||||||
|
#test-template-modal .modal-content,
|
||||||
|
#pp-template-modal .modal-content,
|
||||||
|
#test-pp-template-modal .modal-content,
|
||||||
|
#cspt-modal .modal-content,
|
||||||
|
#css-editor-modal .modal-content,
|
||||||
|
#test-css-source-modal .modal-content,
|
||||||
|
#pattern-template-modal .modal-content,
|
||||||
|
#gradient-editor-modal .modal-content,
|
||||||
|
#value-source-editor-modal .modal-content,
|
||||||
|
#test-value-source-modal .modal-content,
|
||||||
|
#asset-editor-modal .modal-content,
|
||||||
|
#asset-upload-modal .modal-content,
|
||||||
|
#ha-source-editor-modal .modal-content,
|
||||||
|
#mqtt-source-editor-modal .modal-content,
|
||||||
|
#sync-clock-editor-modal .modal-content,
|
||||||
|
#weather-source-editor-modal .modal-content { --modal-ch: var(--ch-cyan, var(--info-color)); }
|
||||||
|
|
||||||
|
#audio-source-editor-modal .modal-content,
|
||||||
|
#audio-template-modal .modal-content,
|
||||||
|
#audio-processing-template-modal .modal-content,
|
||||||
|
#test-audio-source-modal .modal-content,
|
||||||
|
#test-audio-template-modal .modal-content { --modal-ch: var(--ch-magenta, #ff4ade); }
|
||||||
|
|
||||||
|
#automation-editor-modal .modal-content,
|
||||||
|
#scene-preset-editor-modal .modal-content,
|
||||||
|
#game-integration-editor-modal .modal-content { --modal-ch: var(--ch-violet, #8b7eff); }
|
||||||
|
|
||||||
|
#settings-modal .modal-content,
|
||||||
|
#api-key-modal .modal-content,
|
||||||
|
#setup-required-modal .modal-content,
|
||||||
|
#notification-history-modal .modal-content { --modal-ch: var(--ch-amber, var(--warning-color)); }
|
||||||
|
|
||||||
|
#confirm-modal .modal-content { --modal-ch: var(--ch-coral, var(--danger-color)); }
|
||||||
|
|
||||||
#template-modal .modal-content {
|
#template-modal .modal-content {
|
||||||
max-width: 500px !important;
|
max-width: 500px !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
@@ -817,21 +905,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
padding: 24px 24px 16px 24px;
|
padding: 22px 24px 14px 24px;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: var(--lux-hairline, 1px) solid var(--lux-line, var(--border-color));
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tiny channel-color square to the left of the title, consistent with
|
||||||
|
the sidebar's section-label marker. */
|
||||||
|
.modal-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 3px;
|
||||||
|
height: 22px;
|
||||||
|
background: var(--modal-ch, var(--ch-signal, var(--primary-color)));
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 0 10px color-mix(in srgb, var(--modal-ch, var(--ch-signal, var(--primary-color))) 50%, transparent);
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header h2 {
|
.modal-header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.5rem;
|
font-family: var(--font-body, inherit);
|
||||||
color: var(--text-color);
|
font-size: 1.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--lux-ink, var(--text-color));
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header-actions {
|
.modal-header-actions {
|
||||||
@@ -1191,10 +1300,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
padding: 16px 24px 24px 24px;
|
padding: 16px 24px 20px 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
border-top: var(--lux-hairline, 1px) solid var(--lux-line, var(--border-color));
|
||||||
|
background: linear-gradient(180deg,
|
||||||
|
transparent 0%,
|
||||||
|
color-mix(in srgb, var(--lux-bg-0, transparent) 30%, transparent) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer .btn-icon {
|
.modal-footer .btn-icon {
|
||||||
|
|||||||
@@ -0,0 +1,305 @@
|
|||||||
|
/* ── Lumenworks sidebar (channel-strip nav) ─────────────────────
|
||||||
|
Primary navigation for desktop/tablet. Contains the 6 top-level
|
||||||
|
tabs (.tab-bar kept for JS compatibility with switchTab), a live
|
||||||
|
meter plate at the bottom, and collapses to a 56px icon rail
|
||||||
|
between 1100px and 600px. On phones (<=600px) the sidebar hides
|
||||||
|
entirely and mobile.css reverts .tab-bar to a fixed bottom strip.
|
||||||
|
──────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* ── App shell: header on top, 2-column body below ── */
|
||||||
|
|
||||||
|
.app-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: var(--sidebar-width, 248px) 1fr;
|
||||||
|
gap: 0;
|
||||||
|
align-items: stretch;
|
||||||
|
min-height: calc(100vh - var(--transport-height, 64px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-main {
|
||||||
|
min-width: 0; /* allow children to shrink instead of overflow */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sidebar container ── */
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: var(--transport-height, 64px);
|
||||||
|
height: calc(100vh - var(--transport-height, 64px));
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
border-right: var(--lux-hairline) solid var(--lux-line-bold, var(--border-color));
|
||||||
|
background: linear-gradient(180deg, var(--lux-bg-1, var(--bg-secondary)) 0%, var(--lux-bg-0, var(--bg-color)) 100%);
|
||||||
|
padding: 16px 0 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
z-index: calc(var(--z-sticky) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 0.55rem;
|
||||||
|
letter-spacing: 0.25em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--lux-ink-mute, var(--text-secondary));
|
||||||
|
padding: 0 8px 8px;
|
||||||
|
border-bottom: 1px dashed var(--lux-line, var(--border-color));
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.sidebar-label::before { content: '['; color: var(--lux-ink-faint, var(--text-muted)); }
|
||||||
|
.sidebar-label::after { content: ']'; color: var(--lux-ink-faint, var(--text-muted)); margin-left: auto; }
|
||||||
|
|
||||||
|
/* ── Tab-bar (kept as vertical nav inside sidebar on desktop) ── */
|
||||||
|
|
||||||
|
.sidebar .tab-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 1px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .tab-btn {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 18px 1fr auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 9px 10px;
|
||||||
|
margin: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--lux-r-sm, 3px);
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--lux-ink-dim, var(--text-secondary));
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
text-align: left;
|
||||||
|
transition: color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .tab-btn:hover {
|
||||||
|
color: var(--lux-ink, var(--text-color));
|
||||||
|
background: var(--lux-bg-2, var(--bg-secondary));
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .tab-btn.active {
|
||||||
|
color: var(--lux-ink, var(--text-color));
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
color-mix(in srgb, var(--ch-signal, var(--primary-color)) 14%, transparent) 0%,
|
||||||
|
transparent 80%);
|
||||||
|
box-shadow: inset 2px 0 0 var(--ch-signal, var(--primary-color));
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .tab-btn.active::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 2px;
|
||||||
|
height: 60%;
|
||||||
|
background: var(--ch-signal, var(--primary-color));
|
||||||
|
box-shadow: var(--lux-signal-glow, 0 0 6px currentColor);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .tab-btn .icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: inherit;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.sidebar .tab-btn.active .icon {
|
||||||
|
color: var(--ch-signal, var(--primary-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .tab-btn > span[data-i18n] {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .tab-btn .tab-badge {
|
||||||
|
background: var(--lux-bg-3, var(--border-color));
|
||||||
|
color: var(--lux-ink-mute, var(--text-secondary));
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
min-width: 18px;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.sidebar .tab-btn.active .tab-badge {
|
||||||
|
background: var(--ch-signal, var(--primary-color));
|
||||||
|
color: var(--lux-bg-0, #000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sidebar footer: live CPU/FPS meter plate ── */
|
||||||
|
|
||||||
|
.sidebar-foot {
|
||||||
|
margin-top: auto;
|
||||||
|
padding: 14px 20px 8px;
|
||||||
|
border-top: 1px dashed var(--lux-line, var(--border-color));
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpu-meter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpu-meter-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
font-size: 0.58rem;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--lux-ink-mute, var(--text-secondary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpu-meter-row b {
|
||||||
|
color: var(--lux-ink, var(--text-color));
|
||||||
|
font-weight: 500;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpu-bar {
|
||||||
|
height: 3px;
|
||||||
|
background: var(--lux-bg-3, var(--border-color));
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpu-bar > i {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--ch-signal, var(--primary-color)), var(--ch-cyan, var(--info-color)));
|
||||||
|
box-shadow: 0 0 6px color-mix(in srgb, var(--ch-signal, var(--primary-color)) 50%, transparent);
|
||||||
|
transition: width 0.4s ease;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpu-bar-fps > i {
|
||||||
|
background: linear-gradient(90deg, var(--ch-cyan, var(--info-color)), var(--ch-magenta, #ff4ade));
|
||||||
|
box-shadow: 0 0 6px color-mix(in srgb, var(--ch-cyan, var(--info-color)) 50%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-version {
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 0.55rem;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--lux-ink-faint, var(--text-muted));
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px dashed var(--lux-line, var(--border-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Responsive: icon rail at tablet-desktop, hidden at phone ── */
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
:root { --sidebar-width: 56px; }
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
padding: 14px 0 20px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.sidebar-section {
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
.sidebar-label,
|
||||||
|
.sidebar-version {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.sidebar-foot {
|
||||||
|
padding: 10px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .tab-btn {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: 10px 0;
|
||||||
|
justify-content: center;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.sidebar .tab-btn > span[data-i18n] {
|
||||||
|
font-size: 0.55rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: inherit;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.sidebar .tab-btn .icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
.sidebar .tab-btn .tab-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
font-size: 0.55rem;
|
||||||
|
min-width: 14px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpu-meter-row {
|
||||||
|
font-size: 0.48rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
.cpu-meter-row b {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
/* On phones, sidebar disappears and mobile.css reverts .tab-bar to
|
||||||
|
a fixed bottom strip. The .app-body grid becomes a single column. */
|
||||||
|
:root { --sidebar-width: 0px; }
|
||||||
|
|
||||||
|
.app-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
/* Hide sidebar chrome; .tab-bar inside still gets fixed-bottom
|
||||||
|
styling from mobile.css regardless of its container. */
|
||||||
|
position: static;
|
||||||
|
height: auto;
|
||||||
|
border-right: none;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
overflow: visible;
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
.sidebar-foot,
|
||||||
|
.sidebar-label {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -606,36 +606,44 @@ body.pp-filter-dragging .pp-filter-drag-handle {
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
font-size: 0.9rem;
|
font-family: var(--font-mono, inherit);
|
||||||
font-weight: 500;
|
font-size: 0.72rem;
|
||||||
color: var(--text-secondary);
|
font-weight: 600;
|
||||||
|
color: var(--lux-ink-mute, var(--text-secondary));
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
transition: color 0.2s ease, border-color 0.25s ease;
|
transition: color 0.2s ease, border-color 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stream-tab-btn:hover {
|
.stream-tab-btn:hover {
|
||||||
color: var(--text-color);
|
color: var(--lux-ink, var(--text-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
.stream-tab-btn.active {
|
.stream-tab-btn.active {
|
||||||
color: var(--primary-text-color);
|
color: var(--ch-signal, var(--primary-color));
|
||||||
border-bottom-color: var(--primary-color);
|
border-bottom-color: var(--ch-signal, var(--primary-color));
|
||||||
|
text-shadow: 0 0 12px color-mix(in srgb, var(--ch-signal, var(--primary-color)) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stream-tab-count {
|
.stream-tab-count {
|
||||||
background: var(--border-color);
|
background: var(--lux-bg-3, var(--border-color));
|
||||||
color: var(--text-secondary);
|
color: var(--lux-ink-dim, var(--text-secondary));
|
||||||
font-size: 0.7rem;
|
font-family: var(--font-mono, monospace);
|
||||||
font-weight: 600;
|
font-size: 0.56rem;
|
||||||
|
font-weight: 700;
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
border-radius: 8px;
|
border-radius: 2px;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stream-tab-btn.active .stream-tab-count {
|
.stream-tab-btn.active .stream-tab-count {
|
||||||
background: var(--primary-color);
|
background: var(--ch-signal, var(--primary-color));
|
||||||
color: var(--primary-contrast);
|
color: var(--lux-bg-0, var(--primary-contrast));
|
||||||
|
box-shadow: 0 0 6px color-mix(in srgb, var(--ch-signal, var(--primary-color)) 40%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cs-expand-collapse-group {
|
.cs-expand-collapse-group {
|
||||||
@@ -685,11 +693,26 @@ body.pp-filter-dragging .pp-filter-drag-handle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.subtab-section-header {
|
.subtab-section-header {
|
||||||
font-size: 1rem;
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 0.72rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-secondary);
|
color: var(--lux-ink-dim, var(--text-secondary));
|
||||||
margin: 0 0 12px 0;
|
margin: 0 0 14px 0;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
border-bottom: var(--lux-hairline, 1px) dashed var(--lux-line, var(--border-color));
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtab-section-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; bottom: -1px;
|
||||||
|
width: 48px;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--ch-signal, var(--primary-color));
|
||||||
|
box-shadow: 0 0 6px color-mix(in srgb, var(--ch-signal, var(--primary-color)) 50%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtab-section-header.cs-header {
|
.subtab-section-header.cs-header {
|
||||||
@@ -731,13 +754,16 @@ body.pp-filter-dragging .pp-filter-drag-handle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cs-count {
|
.cs-count {
|
||||||
background: var(--border-color);
|
background: var(--lux-bg-3, var(--border-color));
|
||||||
color: var(--text-secondary);
|
color: var(--lux-ink-dim, var(--text-secondary));
|
||||||
border-radius: 10px;
|
border-radius: 2px;
|
||||||
padding: 0 7px;
|
padding: 2px 7px;
|
||||||
font-size: 0.75rem;
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 0.6rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cs-collapsed .cs-filter-wrap,
|
.cs-collapsed .cs-filter-wrap,
|
||||||
|
|||||||
@@ -22,30 +22,53 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Trigger bar ── */
|
/* ── Trigger bar — module selector pill ── */
|
||||||
|
|
||||||
.tree-dd-trigger {
|
.tree-dd-trigger {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
padding: 5px 10px;
|
padding: 7px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid var(--border-color);
|
border: var(--lux-hairline, 1px) solid var(--lux-line, var(--border-color));
|
||||||
border-radius: 8px;
|
border-radius: var(--lux-r-sm, 3px);
|
||||||
background: var(--bg-secondary);
|
background: var(--lux-bg-1, var(--bg-secondary));
|
||||||
user-select: none;
|
user-select: none;
|
||||||
font-size: 0.82rem;
|
font-family: var(--font-mono, monospace);
|
||||||
color: var(--text-color);
|
font-size: 0.72rem;
|
||||||
transition: border-color 0.15s, background 0.15s;
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--lux-ink, var(--text-color));
|
||||||
|
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Channel stripe on the left edge of the trigger */
|
||||||
|
.tree-dd-trigger::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0; bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: var(--ch-signal, var(--primary-color));
|
||||||
|
opacity: 0.4;
|
||||||
|
transition: opacity 0.15s, box-shadow 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-dd-trigger:hover {
|
.tree-dd-trigger:hover {
|
||||||
border-color: var(--primary-color);
|
border-color: var(--lux-line-bold, var(--border-color));
|
||||||
background: color-mix(in srgb, var(--primary-color) 6%, var(--bg-secondary));
|
background: var(--lux-bg-2, var(--bg-secondary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-dd-trigger:hover::before,
|
||||||
|
.tree-dd-trigger.open::before {
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0 0 8px color-mix(in srgb, var(--ch-signal, var(--primary-color)) 60%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-dd-trigger.open {
|
.tree-dd-trigger.open {
|
||||||
border-color: var(--primary-color);
|
border-color: color-mix(in srgb, var(--ch-signal, var(--primary-color)) 35%, var(--lux-line, var(--border-color)));
|
||||||
|
background: var(--lux-bg-2, var(--bg-secondary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-dd-trigger-icon {
|
.tree-dd-trigger-icon {
|
||||||
@@ -60,18 +83,24 @@
|
|||||||
|
|
||||||
.tree-dd-trigger-title {
|
.tree-dd-trigger-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
color: var(--lux-ink, var(--text-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-dd-trigger-count {
|
.tree-dd-trigger-count {
|
||||||
background: var(--primary-color);
|
background: var(--ch-signal, var(--primary-color));
|
||||||
color: var(--primary-contrast);
|
color: var(--lux-bg-0, var(--primary-contrast));
|
||||||
font-size: 0.6rem;
|
font-family: var(--font-mono, monospace);
|
||||||
font-weight: 600;
|
font-size: 0.55rem;
|
||||||
padding: 0 5px;
|
font-weight: 700;
|
||||||
border-radius: 8px;
|
padding: 1px 6px;
|
||||||
min-width: 16px;
|
border-radius: 2px;
|
||||||
|
min-width: 18px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
box-shadow: 0 0 6px color-mix(in srgb, var(--ch-signal, var(--primary-color)) 40%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-dd-chevron {
|
.tree-dd-chevron {
|
||||||
@@ -94,24 +123,43 @@
|
|||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Dropdown panel ── */
|
/* ── Dropdown panel — rack-selector popover ── */
|
||||||
|
|
||||||
.tree-dd-panel {
|
.tree-dd-panel {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
min-width: 240px;
|
min-width: 260px;
|
||||||
max-width: 340px;
|
max-width: 360px;
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: var(--bg-color);
|
background: linear-gradient(180deg,
|
||||||
border: 1px solid var(--border-color);
|
var(--lux-bg-1, var(--bg-color)) 0%,
|
||||||
border-radius: 8px;
|
var(--lux-bg-2, var(--bg-color)) 100%);
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
border: var(--lux-hairline, 1px) solid var(--lux-line-bold, var(--border-color));
|
||||||
|
border-radius: var(--lux-r-md, 6px);
|
||||||
|
box-shadow: var(--lux-shadow-rack, 0 8px 24px rgba(0, 0, 0, 0.25));
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
padding: 4px 0;
|
padding: 6px 0;
|
||||||
margin-top: 4px;
|
margin-top: 6px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Channel accent rule at the top of the panel */
|
||||||
|
.tree-dd-panel::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; top: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
transparent 0%,
|
||||||
|
var(--ch-signal, var(--primary-color)) 20%,
|
||||||
|
var(--ch-cyan, var(--primary-color)) 50%,
|
||||||
|
var(--ch-magenta, var(--primary-color)) 80%,
|
||||||
|
transparent 100%);
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-dd-panel.open {
|
.tree-dd-panel.open {
|
||||||
@@ -123,14 +171,26 @@
|
|||||||
.tree-dd-group-header {
|
.tree-dd-group-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
padding: 6px 12px 3px;
|
padding: 8px 14px 4px;
|
||||||
font-size: 0.68rem;
|
font-family: var(--font-mono, monospace);
|
||||||
font-weight: 700;
|
font-size: 0.56rem;
|
||||||
color: var(--text-muted);
|
font-weight: 600;
|
||||||
|
color: var(--lux-ink-mute, var(--text-muted));
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.22em;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small square dot prefix — reads like a silkscreened section marker. */
|
||||||
|
.tree-dd-group-header::before {
|
||||||
|
content: '';
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--lux-ink-faint, var(--text-muted));
|
||||||
|
border-radius: 1px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-dd-group-header.tree-dd-depth-1 {
|
.tree-dd-group-header.tree-dd-depth-1 {
|
||||||
@@ -184,12 +244,15 @@
|
|||||||
.tree-dd-leaf {
|
.tree-dd-leaf {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
padding: 5px 12px 5px 20px;
|
padding: 7px 14px 7px 22px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.8rem;
|
font-family: var(--font-body, inherit);
|
||||||
color: var(--text-secondary);
|
font-size: 0.82rem;
|
||||||
transition: color 0.1s, background 0.1s;
|
font-weight: 500;
|
||||||
|
color: var(--lux-ink-dim, var(--text-secondary));
|
||||||
|
transition: color 0.1s, background 0.1s, box-shadow 0.1s;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Indent leaves inside nested groups */
|
/* Indent leaves inside nested groups */
|
||||||
@@ -203,19 +266,38 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tree-dd-leaf:hover {
|
.tree-dd-leaf:hover {
|
||||||
color: var(--text-color);
|
color: var(--lux-ink, var(--text-color));
|
||||||
background: var(--bg-secondary);
|
background: var(--lux-bg-3, var(--bg-secondary));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Active leaf: LED pip on the left + channel glow + brighter text */
|
||||||
.tree-dd-leaf.active {
|
.tree-dd-leaf.active {
|
||||||
color: var(--primary-text-color);
|
color: var(--lux-ink, var(--text-color));
|
||||||
background: color-mix(in srgb, var(--primary-color) 12%, transparent);
|
background: linear-gradient(90deg,
|
||||||
|
color-mix(in srgb, var(--ch-signal, var(--primary-color)) 14%, transparent) 0%,
|
||||||
|
transparent 80%);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
box-shadow: inset 2px 0 0 var(--ch-signal, var(--primary-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-dd-leaf.active::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--ch-signal, var(--primary-color));
|
||||||
|
box-shadow: 0 0 6px var(--ch-signal, var(--primary-color));
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-dd-leaf.active .tree-count {
|
.tree-dd-leaf.active .tree-count {
|
||||||
background: var(--primary-color);
|
background: var(--ch-signal, var(--primary-color));
|
||||||
color: var(--primary-contrast);
|
color: var(--lux-bg-0, var(--primary-contrast));
|
||||||
|
box-shadow: 0 0 6px color-mix(in srgb, var(--ch-signal, var(--primary-color)) 50%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-dd-leaf .tree-node-icon {
|
.tree-dd-leaf .tree-node-icon {
|
||||||
@@ -238,22 +320,26 @@
|
|||||||
|
|
||||||
/* ── Count badge (shared) ── */
|
/* ── Count badge (shared) ── */
|
||||||
|
|
||||||
.tree-count {
|
.tree-count,
|
||||||
background: var(--border-color);
|
.tree-dd-group-count {
|
||||||
color: var(--text-secondary);
|
background: var(--lux-bg-3, var(--border-color));
|
||||||
font-size: 0.6rem;
|
color: var(--lux-ink-dim, var(--text-secondary));
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 0.56rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0 5px;
|
padding: 1px 6px;
|
||||||
border-radius: 8px;
|
border-radius: 2px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
min-width: 16px;
|
min-width: 18px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Group separator ── */
|
/* ── Group separator — hairline-dashed between top-level groups ── */
|
||||||
|
|
||||||
.tree-dd-group + .tree-dd-group {
|
.tree-dd-group + .tree-dd-group {
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px dashed var(--lux-line, var(--border-color));
|
||||||
margin-top: 2px;
|
margin-top: 4px;
|
||||||
padding-top: 2px;
|
padding-top: 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -45,6 +45,24 @@ function _pushFps(targetId: string, actual: number, current: number): void {
|
|||||||
if (_fpsCurrentHistory[targetId].length > MAX_FPS_SAMPLES) _fpsCurrentHistory[targetId].shift();
|
if (_fpsCurrentHistory[targetId].length > MAX_FPS_SAMPLES) _fpsCurrentHistory[targetId].shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Update the transport status chip in the top bar to reflect how many
|
||||||
|
* targets are currently running. "Ready" when idle, "Armed · N live"
|
||||||
|
* when one or more targets are processing. Safe to call any time. */
|
||||||
|
function _updateTransportStatus(runningCount: number): void {
|
||||||
|
const chip = document.getElementById('transport-status');
|
||||||
|
if (!chip) return;
|
||||||
|
const label = chip.querySelector('span:last-child');
|
||||||
|
if (!label) return;
|
||||||
|
if (runningCount > 0) {
|
||||||
|
chip.classList.add('is-armed');
|
||||||
|
const tmpl = t('transport.status.armed');
|
||||||
|
label.textContent = tmpl.includes('{n}') ? tmpl.replace('{n}', String(runningCount)) : `${tmpl} · ${runningCount}`;
|
||||||
|
} else {
|
||||||
|
chip.classList.remove('is-armed');
|
||||||
|
label.textContent = t('transport.status.ready');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function _setUptimeBase(targetId: string, seconds: number): void {
|
function _setUptimeBase(targetId: string, seconds: number): void {
|
||||||
_uptimeBase[targetId] = { seconds, timestamp: Date.now() };
|
_uptimeBase[targetId] = { seconds, timestamp: Date.now() };
|
||||||
}
|
}
|
||||||
@@ -510,6 +528,7 @@ export async function loadDashboard(forceFullRender: boolean = false): Promise<v
|
|||||||
const running = enriched.filter(t => t.state && t.state.processing);
|
const running = enriched.filter(t => t.state && t.state.processing);
|
||||||
const stopped = enriched.filter(t => !t.state || !t.state.processing);
|
const stopped = enriched.filter(t => !t.state || !t.state.processing);
|
||||||
updateTabBadge('targets', running.length);
|
updateTabBadge('targets', running.length);
|
||||||
|
_updateTransportStatus(running.length);
|
||||||
|
|
||||||
// Check if we can do an in-place metrics update (same targets, not first load)
|
// Check if we can do an in-place metrics update (same targets, not first load)
|
||||||
const newRunningIds = running.map(t => t.id).sort().join(',');
|
const newRunningIds = running.map(t => t.id).sort().join(',');
|
||||||
|
|||||||
@@ -327,6 +327,21 @@ function _setValueEl(el: HTMLElement, html: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _updateSidebarMeter(cpuPercent: number, appCpuPercent: number): void {
|
||||||
|
const cpuLabel = document.getElementById('sidebar-meter-cpu');
|
||||||
|
const cpuBar = document.getElementById('sidebar-meter-cpu-bar');
|
||||||
|
if (cpuLabel) cpuLabel.textContent = `${cpuPercent.toFixed(0)}%`;
|
||||||
|
if (cpuBar) (cpuBar as HTMLElement).style.width = `${Math.min(100, Math.max(0, cpuPercent))}%`;
|
||||||
|
|
||||||
|
// "FPS" bar — use app CPU share as a live utilization readout. When the
|
||||||
|
// dashboard later exposes an aggregate FPS ratio, swap this for that.
|
||||||
|
const fpsLabel = document.getElementById('sidebar-meter-fps');
|
||||||
|
const fpsBar = document.getElementById('sidebar-meter-fps-bar');
|
||||||
|
const appPct = cpuPercent > 0 ? Math.min(100, (appCpuPercent / cpuPercent) * 100) : 0;
|
||||||
|
if (fpsLabel) fpsLabel.textContent = `${appCpuPercent.toFixed(1)}%`;
|
||||||
|
if (fpsBar) (fpsBar as HTMLElement).style.width = `${appPct}%`;
|
||||||
|
}
|
||||||
|
|
||||||
async function _fetchPerformance(): Promise<void> {
|
async function _fetchPerformance(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${API_BASE}/system/performance`, { headers: getHeaders() });
|
const resp = await fetch(`${API_BASE}/system/performance`, { headers: getHeaders() });
|
||||||
@@ -335,6 +350,7 @@ async function _fetchPerformance(): Promise<void> {
|
|||||||
|
|
||||||
// CPU — app_cpu_percent is in the same scale as cpu_percent (per-core %)
|
// CPU — app_cpu_percent is in the same scale as cpu_percent (per-core %)
|
||||||
_pushSample('cpu', data.cpu_percent, data.app_cpu_percent);
|
_pushSample('cpu', data.cpu_percent, data.app_cpu_percent);
|
||||||
|
_updateSidebarMeter(data.cpu_percent ?? 0, data.app_cpu_percent ?? 0);
|
||||||
const cpuEl = document.getElementById('perf-cpu-value');
|
const cpuEl = document.getElementById('perf-cpu-value');
|
||||||
if (cpuEl) {
|
if (cpuEl) {
|
||||||
_setValueEl(cpuEl, _formatValue(
|
_setValueEl(cpuEl, _formatValue(
|
||||||
|
|||||||
@@ -500,7 +500,7 @@
|
|||||||
"tags.placeholder": "Add tag...",
|
"tags.placeholder": "Add tag...",
|
||||||
"section.expand_all": "Expand all sections",
|
"section.expand_all": "Expand all sections",
|
||||||
"section.collapse_all": "Collapse all sections",
|
"section.collapse_all": "Collapse all sections",
|
||||||
"streams.title": "Sources",
|
"streams.title": "Inputs",
|
||||||
"integrations.title": "Integrations",
|
"integrations.title": "Integrations",
|
||||||
"streams.description": "Sources define the capture pipeline. A raw source captures from a display using a capture template. A processed source applies postprocessing to another source. Assign sources to devices.",
|
"streams.description": "Sources define the capture pipeline. A raw source captures from a display using a capture template. A processed source applies postprocessing to another source. Assign sources to devices.",
|
||||||
"streams.group.raw": "Sources",
|
"streams.group.raw": "Sources",
|
||||||
@@ -672,7 +672,7 @@
|
|||||||
"streams.video_asset": "Video Asset:",
|
"streams.video_asset": "Video Asset:",
|
||||||
"streams.video_asset.select": "Select video asset…",
|
"streams.video_asset.select": "Select video asset…",
|
||||||
"streams.video_asset.search": "Search video assets…",
|
"streams.video_asset.search": "Search video assets…",
|
||||||
"targets.title": "Targets",
|
"targets.title": "Channels",
|
||||||
"targets.description": "Targets bridge color strip sources to output devices. Each target references a device and a color strip source.",
|
"targets.description": "Targets bridge color strip sources to output devices. Each target references a device and a color strip source.",
|
||||||
"targets.subtab.wled": "LED",
|
"targets.subtab.wled": "LED",
|
||||||
"targets.subtab.led": "LED",
|
"targets.subtab.led": "LED",
|
||||||
@@ -767,8 +767,13 @@
|
|||||||
"overlay.stopped": "Overlay visualization stopped",
|
"overlay.stopped": "Overlay visualization stopped",
|
||||||
"overlay.error.start": "Failed to start overlay",
|
"overlay.error.start": "Failed to start overlay",
|
||||||
"overlay.error.stop": "Failed to stop overlay",
|
"overlay.error.stop": "Failed to stop overlay",
|
||||||
|
"sidebar.workspaces": "Workspaces",
|
||||||
|
"sidebar.load": "Load",
|
||||||
|
"sidebar.fps": "FPS",
|
||||||
|
"transport.status.ready": "Ready",
|
||||||
|
"transport.status.armed": "Armed · {n} live",
|
||||||
"dashboard.title": "Dashboard",
|
"dashboard.title": "Dashboard",
|
||||||
"dashboard.section.targets": "Targets",
|
"dashboard.section.targets": "Channels",
|
||||||
"dashboard.section.running": "Running",
|
"dashboard.section.running": "Running",
|
||||||
"dashboard.section.stopped": "Stopped",
|
"dashboard.section.stopped": "Stopped",
|
||||||
"dashboard.no_targets": "No targets configured",
|
"dashboard.no_targets": "No targets configured",
|
||||||
|
|||||||
@@ -502,7 +502,7 @@
|
|||||||
"tags.placeholder": "Добавить тег...",
|
"tags.placeholder": "Добавить тег...",
|
||||||
"section.expand_all": "Развернуть все секции",
|
"section.expand_all": "Развернуть все секции",
|
||||||
"section.collapse_all": "Свернуть все секции",
|
"section.collapse_all": "Свернуть все секции",
|
||||||
"streams.title": "Источники",
|
"streams.title": "Входы",
|
||||||
"integrations.title": "Интеграции",
|
"integrations.title": "Интеграции",
|
||||||
"streams.description": "Источники определяют конвейер захвата. Сырой источник захватывает экран с помощью шаблона захвата. Обработанный источник применяет постобработку к другому источнику. Назначайте источники устройствам.",
|
"streams.description": "Источники определяют конвейер захвата. Сырой источник захватывает экран с помощью шаблона захвата. Обработанный источник применяет постобработку к другому источнику. Назначайте источники устройствам.",
|
||||||
"streams.group.raw": "Источники",
|
"streams.group.raw": "Источники",
|
||||||
@@ -656,7 +656,7 @@
|
|||||||
"streams.video_asset": "Видео:",
|
"streams.video_asset": "Видео:",
|
||||||
"streams.video_asset.select": "Выберите видео…",
|
"streams.video_asset.select": "Выберите видео…",
|
||||||
"streams.video_asset.search": "Поиск видео…",
|
"streams.video_asset.search": "Поиск видео…",
|
||||||
"targets.title": "Цели",
|
"targets.title": "Каналы",
|
||||||
"targets.description": "Цели связывают источники цветовых полос с устройствами вывода. Каждая цель ссылается на устройство и источник цветовой полосы.",
|
"targets.description": "Цели связывают источники цветовых полос с устройствами вывода. Каждая цель ссылается на устройство и источник цветовой полосы.",
|
||||||
"targets.subtab.wled": "LED",
|
"targets.subtab.wled": "LED",
|
||||||
"targets.subtab.led": "LED",
|
"targets.subtab.led": "LED",
|
||||||
@@ -751,8 +751,13 @@
|
|||||||
"overlay.stopped": "Визуализация наложения остановлена",
|
"overlay.stopped": "Визуализация наложения остановлена",
|
||||||
"overlay.error.start": "Не удалось запустить наложение",
|
"overlay.error.start": "Не удалось запустить наложение",
|
||||||
"overlay.error.stop": "Не удалось остановить наложение",
|
"overlay.error.stop": "Не удалось остановить наложение",
|
||||||
|
"sidebar.workspaces": "Разделы",
|
||||||
|
"sidebar.load": "Нагр.",
|
||||||
|
"sidebar.fps": "FPS",
|
||||||
|
"transport.status.ready": "Готов",
|
||||||
|
"transport.status.armed": "Активно · {n}",
|
||||||
"dashboard.title": "Обзор",
|
"dashboard.title": "Обзор",
|
||||||
"dashboard.section.targets": "Цели",
|
"dashboard.section.targets": "Каналы",
|
||||||
"dashboard.section.running": "Запущенные",
|
"dashboard.section.running": "Запущенные",
|
||||||
"dashboard.section.stopped": "Остановленные",
|
"dashboard.section.stopped": "Остановленные",
|
||||||
"dashboard.no_targets": "Нет настроенных целей",
|
"dashboard.no_targets": "Нет настроенных целей",
|
||||||
|
|||||||
@@ -502,7 +502,7 @@
|
|||||||
"tags.placeholder": "添加标签...",
|
"tags.placeholder": "添加标签...",
|
||||||
"section.expand_all": "全部展开",
|
"section.expand_all": "全部展开",
|
||||||
"section.collapse_all": "全部折叠",
|
"section.collapse_all": "全部折叠",
|
||||||
"streams.title": "源",
|
"streams.title": "输入",
|
||||||
"integrations.title": "集成",
|
"integrations.title": "集成",
|
||||||
"streams.description": "源定义采集管线。原始源使用采集模板从显示器采集。处理源对另一个源应用后处理。将源分配给设备。",
|
"streams.description": "源定义采集管线。原始源使用采集模板从显示器采集。处理源对另一个源应用后处理。将源分配给设备。",
|
||||||
"streams.group.raw": "源",
|
"streams.group.raw": "源",
|
||||||
@@ -656,7 +656,7 @@
|
|||||||
"streams.video_asset": "视频素材:",
|
"streams.video_asset": "视频素材:",
|
||||||
"streams.video_asset.select": "选择视频素材…",
|
"streams.video_asset.select": "选择视频素材…",
|
||||||
"streams.video_asset.search": "搜索视频素材…",
|
"streams.video_asset.search": "搜索视频素材…",
|
||||||
"targets.title": "目标",
|
"targets.title": "通道",
|
||||||
"targets.description": "目标将色带源桥接到输出设备。每个目标引用一个设备和一个色带源。",
|
"targets.description": "目标将色带源桥接到输出设备。每个目标引用一个设备和一个色带源。",
|
||||||
"targets.subtab.wled": "LED",
|
"targets.subtab.wled": "LED",
|
||||||
"targets.subtab.led": "LED",
|
"targets.subtab.led": "LED",
|
||||||
@@ -751,8 +751,13 @@
|
|||||||
"overlay.stopped": "叠加层可视化已停止",
|
"overlay.stopped": "叠加层可视化已停止",
|
||||||
"overlay.error.start": "启动叠加层失败",
|
"overlay.error.start": "启动叠加层失败",
|
||||||
"overlay.error.stop": "停止叠加层失败",
|
"overlay.error.stop": "停止叠加层失败",
|
||||||
|
"sidebar.workspaces": "工作区",
|
||||||
|
"sidebar.load": "负载",
|
||||||
|
"sidebar.fps": "帧率",
|
||||||
|
"transport.status.ready": "就绪",
|
||||||
|
"transport.status.armed": "运行中 · {n}",
|
||||||
"dashboard.title": "仪表盘",
|
"dashboard.title": "仪表盘",
|
||||||
"dashboard.section.targets": "目标",
|
"dashboard.section.targets": "通道",
|
||||||
"dashboard.section.running": "运行中",
|
"dashboard.section.running": "运行中",
|
||||||
"dashboard.section.stopped": "已停止",
|
"dashboard.section.stopped": "已停止",
|
||||||
"dashboard.no_targets": "尚未配置目标",
|
"dashboard.no_targets": "尚未配置目标",
|
||||||
|
|||||||
@@ -42,13 +42,11 @@
|
|||||||
<span id="server-version"><span id="version-number"></span></span>
|
<span id="server-version"><span id="version-number"></span></span>
|
||||||
<span class="demo-badge" id="demo-badge" style="display:none" data-i18n="demo.badge">DEMO</span>
|
<span class="demo-badge" id="demo-badge" style="display:none" data-i18n="demo.badge">DEMO</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-bar" role="tablist">
|
<div class="transport-center">
|
||||||
<button class="tab-btn" data-tab="dashboard" onclick="switchTab('dashboard')" role="tab" aria-selected="true" aria-controls="tab-dashboard" id="tab-btn-dashboard" title="Ctrl+1"><svg class="icon" viewBox="0 0 24 24"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg> <span data-i18n="dashboard.title">Dashboard</span></button>
|
<span class="transport-status" id="transport-status" aria-live="polite">
|
||||||
<button class="tab-btn" data-tab="automations" onclick="switchTab('automations')" role="tab" aria-selected="false" aria-controls="tab-automations" id="tab-btn-automations" title="Ctrl+2"><svg class="icon" viewBox="0 0 24 24"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M12 11h4"/><path d="M12 16h4"/><path d="M8 11h.01"/><path d="M8 16h.01"/></svg> <span data-i18n="automations.title">Automations</span></button>
|
<span class="dot"></span>
|
||||||
<button class="tab-btn" data-tab="targets" onclick="switchTab('targets')" role="tab" aria-selected="false" aria-controls="tab-targets" id="tab-btn-targets" title="Ctrl+3"><svg class="icon" viewBox="0 0 24 24"><path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></svg> <span data-i18n="targets.title">Targets</span></button>
|
<span data-i18n="transport.status.ready">Ready</span>
|
||||||
<button class="tab-btn" data-tab="streams" onclick="switchTab('streams')" role="tab" aria-selected="false" aria-controls="tab-streams" id="tab-btn-streams" title="Ctrl+4"><svg class="icon" viewBox="0 0 24 24"><path d="m17 2-5 5-5-5"/><rect width="20" height="15" x="2" y="7" rx="2"/></svg> <span data-i18n="streams.title">Sources</span></button>
|
</span>
|
||||||
<button class="tab-btn" data-tab="integrations" onclick="switchTab('integrations')" role="tab" aria-selected="false" aria-controls="tab-integrations" id="tab-btn-integrations" title="Ctrl+5"><svg class="icon" viewBox="0 0 24 24"><path d="M12 22v-5"/><path d="M15 8V2"/><path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"/><path d="M9 8V2"/></svg> <span data-i18n="integrations.title">Integrations</span></button>
|
|
||||||
<button class="tab-btn" data-tab="graph" onclick="switchTab('graph')" role="tab" aria-selected="false" aria-controls="tab-graph" id="tab-btn-graph" title="Ctrl+6"><svg class="icon" viewBox="0 0 24 24"><circle cx="5" cy="6" r="3"/><circle cx="19" cy="6" r="3"/><circle cx="12" cy="18" r="3"/><path d="M7.5 7.5 10.5 16"/><path d="M16.5 7.5 13.5 16"/></svg> <span data-i18n="graph.title">Graph</span></button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="header-toolbar">
|
<div class="header-toolbar">
|
||||||
<a href="/docs" target="_blank" class="header-link" data-i18n-title="app.api_docs" title="API Docs">API</a>
|
<a href="/docs" target="_blank" class="header-link" data-i18n-title="app.api_docs" title="API Docs">API</a>
|
||||||
@@ -107,6 +105,34 @@
|
|||||||
</header>
|
</header>
|
||||||
<div id="update-banner" class="update-banner" style="display:none"></div>
|
<div id="update-banner" class="update-banner" style="display:none"></div>
|
||||||
<div id="donation-banner" class="donation-banner" style="display:none"></div>
|
<div id="donation-banner" class="donation-banner" style="display:none"></div>
|
||||||
|
<div class="app-body">
|
||||||
|
<aside class="sidebar" aria-label="Primary">
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-label"><span data-i18n="sidebar.workspaces">Workspaces</span></div>
|
||||||
|
<div class="tab-bar" role="tablist">
|
||||||
|
<button class="tab-btn" data-tab="dashboard" onclick="switchTab('dashboard')" role="tab" aria-selected="true" aria-controls="tab-dashboard" id="tab-btn-dashboard" title="Ctrl+1"><svg class="icon" viewBox="0 0 24 24"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg> <span data-i18n="dashboard.title">Dashboard</span></button>
|
||||||
|
<button class="tab-btn" data-tab="automations" onclick="switchTab('automations')" role="tab" aria-selected="false" aria-controls="tab-automations" id="tab-btn-automations" title="Ctrl+2"><svg class="icon" viewBox="0 0 24 24"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M12 11h4"/><path d="M12 16h4"/><path d="M8 11h.01"/><path d="M8 16h.01"/></svg> <span data-i18n="automations.title">Automations</span></button>
|
||||||
|
<button class="tab-btn" data-tab="targets" onclick="switchTab('targets')" role="tab" aria-selected="false" aria-controls="tab-targets" id="tab-btn-targets" title="Ctrl+3"><svg class="icon" viewBox="0 0 24 24"><path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></svg> <span data-i18n="targets.title">Targets</span></button>
|
||||||
|
<button class="tab-btn" data-tab="streams" onclick="switchTab('streams')" role="tab" aria-selected="false" aria-controls="tab-streams" id="tab-btn-streams" title="Ctrl+4"><svg class="icon" viewBox="0 0 24 24"><path d="m17 2-5 5-5-5"/><rect width="20" height="15" x="2" y="7" rx="2"/></svg> <span data-i18n="streams.title">Sources</span></button>
|
||||||
|
<button class="tab-btn" data-tab="integrations" onclick="switchTab('integrations')" role="tab" aria-selected="false" aria-controls="tab-integrations" id="tab-btn-integrations" title="Ctrl+5"><svg class="icon" viewBox="0 0 24 24"><path d="M12 22v-5"/><path d="M15 8V2"/><path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"/><path d="M9 8V2"/></svg> <span data-i18n="integrations.title">Integrations</span></button>
|
||||||
|
<button class="tab-btn" data-tab="graph" onclick="switchTab('graph')" role="tab" aria-selected="false" aria-controls="tab-graph" id="tab-btn-graph" title="Ctrl+6"><svg class="icon" viewBox="0 0 24 24"><circle cx="5" cy="6" r="3"/><circle cx="19" cy="6" r="3"/><circle cx="12" cy="18" r="3"/><path d="M7.5 7.5 10.5 16"/><path d="M16.5 7.5 13.5 16"/></svg> <span data-i18n="graph.title">Graph</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-foot">
|
||||||
|
<div class="cpu-meter" id="sidebar-meter" aria-hidden="true">
|
||||||
|
<div>
|
||||||
|
<div class="cpu-meter-row"><span data-i18n="sidebar.load">Load</span><b id="sidebar-meter-cpu">--%</b></div>
|
||||||
|
<div class="cpu-bar"><i id="sidebar-meter-cpu-bar" style="width:0"></i></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="cpu-meter-row"><span data-i18n="sidebar.fps">FPS</span><b id="sidebar-meter-fps">--</b></div>
|
||||||
|
<div class="cpu-bar cpu-bar-fps"><i id="sidebar-meter-fps-bar" style="width:0"></i></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-version"><span id="sidebar-version-text">—</span></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<main class="app-main">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="tab-panel" id="tab-dashboard" role="tabpanel" aria-labelledby="tab-btn-dashboard">
|
<div class="tab-panel" id="tab-dashboard" role="tabpanel" aria-labelledby="tab-btn-dashboard">
|
||||||
@@ -186,6 +212,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button id="scroll-to-top" class="scroll-to-top" onclick="window.scrollTo({top:0,behavior:'smooth'})" aria-label="Scroll to top">
|
<button id="scroll-to-top" class="scroll-to-top" onclick="window.scrollTo({top:0,behavior:'smooth'})" aria-label="Scroll to top">
|
||||||
<svg class="icon" viewBox="0 0 24 24"><path d="m18 15-6-6-6 6"/></svg>
|
<svg class="icon" viewBox="0 0 24 24"><path d="m18 15-6-6-6 6"/></svg>
|
||||||
|
|||||||
Reference in New Issue
Block a user