From be315111ea0dabdedd447afde7310e692e7978c6 Mon Sep 17 00:00:00 2001 From: Aiden Date: Tue, 5 May 2026 20:56:53 +1000 Subject: [PATCH] UI updates and preroll buffer to 8 frames --- .../OpenGLComposite.cpp | 3 +- .../RuntimeHost.cpp | 74 +- .../RuntimeHost.h | 1 + ui/src/App.jsx | 28 +- ui/src/components/KvList.jsx | 18 +- ui/src/components/LayerStack.jsx | 6 +- ui/src/components/StackPresetToolbar.jsx | 112 +-- ui/src/components/StatusPanels.jsx | 82 +- ui/src/styles.css | 817 ++++++++++++------ 9 files changed, 763 insertions(+), 378 deletions(-) diff --git a/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp b/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp index da032c3..54e0939 100644 --- a/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp @@ -63,6 +63,7 @@ constexpr GLuint kDecodedVideoTextureUnit = 1; constexpr GLuint kSourceHistoryTextureUnitBase = 2; constexpr GLuint kPackedVideoTextureUnit = 2; constexpr GLuint kGlobalParamsBindingPoint = 0; +constexpr unsigned kPrerollFrameCount = 8; const char* kVertexShaderSource = "#version 430 core\n" "out vec2 vTexCoord;\n" @@ -1233,7 +1234,7 @@ bool OpenGLComposite::Start() mTotalPlayoutFrames = 0; // Preroll frames - for (unsigned i = 0; i < 5; i++) + for (unsigned i = 0; i < kPrerollFrameCount; i++) { // Take each video frame from the front of the queue and move it to the back IDeckLinkMutableVideoFrame* outputVideoFrame = mDLOutputVideoFrameQueue.front(); diff --git a/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp index 8e49e77..152197b 100644 --- a/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp @@ -53,6 +53,25 @@ bool MatchesControlKey(const std::string& candidate, const std::string& key) return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key); } +bool TryParseLayerIdNumber(const std::string& layerId, uint64_t& number) +{ + const std::string prefix = "layer-"; + if (layerId.rfind(prefix, 0) != 0 || layerId.size() == prefix.size()) + return false; + + uint64_t parsed = 0; + for (std::size_t index = prefix.size(); index < layerId.size(); ++index) + { + const unsigned char ch = static_cast(layerId[index]); + if (!std::isdigit(ch)) + return false; + parsed = parsed * 10 + static_cast(ch - '0'); + } + + number = parsed; + return true; +} + std::vector JsonArrayToNumbers(const JsonValue& value) { std::vector numbers; @@ -583,6 +602,7 @@ bool RuntimeHost::Initialize(std::string& error) return false; if (!ScanShaderPackages(error)) return false; + NormalizePersistentLayerIdsLocked(); for (LayerPersistentState& layer : mPersistentState.layers) { @@ -1469,15 +1489,31 @@ bool RuntimeHost::WriteTextFile(const std::filesystem::path& path, const std::st std::error_code fsError; std::filesystem::create_directories(path.parent_path(), fsError); - std::ofstream output(path, std::ios::binary); + const std::filesystem::path temporaryPath = path.string() + ".tmp"; + std::ofstream output(temporaryPath, std::ios::binary | std::ios::trunc); if (!output) { - error = "Could not write file: " + path.string(); + error = "Could not write file: " + temporaryPath.string(); return false; } output << contents; - return output.good(); + output.close(); + if (!output.good()) + { + error = "Could not finish writing file: " + temporaryPath.string(); + return false; + } + + if (!MoveFileExA(temporaryPath.string().c_str(), path.string().c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) + { + const DWORD lastError = GetLastError(); + std::filesystem::remove(temporaryPath, fsError); + error = "Could not replace file: " + path.string() + " (Win32 error " + std::to_string(lastError) + ")"; + return false; + } + + return true; } bool RuntimeHost::ResolvePaths(std::string& error) @@ -1728,6 +1764,38 @@ bool RuntimeHost::DeserializeLayerStackLocked(const JsonValue& layersValue, std: return true; } +void RuntimeHost::NormalizePersistentLayerIdsLocked() +{ + std::set usedIds; + uint64_t maxLayerNumber = mNextLayerId; + + for (LayerPersistentState& layer : mPersistentState.layers) + { + uint64_t layerNumber = 0; + const bool hasReusableId = !layer.id.empty() && + usedIds.find(layer.id) == usedIds.end() && + TryParseLayerIdNumber(layer.id, layerNumber); + + if (hasReusableId) + { + usedIds.insert(layer.id); + maxLayerNumber = std::max(maxLayerNumber, layerNumber); + continue; + } + + do + { + ++maxLayerNumber; + layer.id = "layer-" + std::to_string(maxLayerNumber); + } + while (usedIds.find(layer.id) != usedIds.end()); + + usedIds.insert(layer.id); + } + + mNextLayerId = maxLayerNumber; +} + std::vector RuntimeHost::GetStackPresetNamesLocked() const { std::vector presetNames; diff --git a/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h index 016533b..853b707 100644 --- a/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h +++ b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h @@ -112,6 +112,7 @@ private: JsonValue BuildStateValue() const; JsonValue SerializeLayerStackLocked() const; bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector& layers, std::string& error); + void NormalizePersistentLayerIdsLocked(); std::vector GetStackPresetNamesLocked() const; std::string MakeSafePresetFileStem(const std::string& presetName) const; JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const; diff --git a/ui/src/App.jsx b/ui/src/App.jsx index 1940394..0aa83ee 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -47,8 +47,11 @@ function App() { return (
-

Loading

+

Loading

Waiting for control state from the native host.

+
); @@ -58,7 +61,7 @@ function App() {
-

Video Shader Toys

+

Video Shader Toys

Live shader stack, DeckLink status, and runtime controls.

@@ -66,6 +69,27 @@ function App() {
+
+
+
+
Shaders
+
{shaders.length}
+
+
+
Layers
+
{layers.length}
+
+
+
Signal
+
{video.hasSignal ? "Present" : "Missing"}
+
+
+
Render
+
{Number(performance.renderMs ?? 0).toFixed(2)} ms
+
+
+
+
+
{values.map(([key, value]) => ( - +
+
{key}
+
{value}
+
))}
); } - -function FragmentRow({ label, value }) { - return ( - <> -
{label}
-
{value}
- - ); -} diff --git a/ui/src/components/LayerStack.jsx b/ui/src/components/LayerStack.jsx index befd93d..0867a1a 100644 --- a/ui/src/components/LayerStack.jsx +++ b/ui/src/components/LayerStack.jsx @@ -83,8 +83,10 @@ export function LayerStack({ return (
-

Layers

-

Drag layers to reorder them. Each layer processes the output of the one above it.

+
+

Layers

+

Drag layers to reorder them. Each layer processes the output of the one above it.

+
diff --git a/ui/src/components/StackPresetToolbar.jsx b/ui/src/components/StackPresetToolbar.jsx index d843f47..c1867f3 100644 --- a/ui/src/components/StackPresetToolbar.jsx +++ b/ui/src/components/StackPresetToolbar.jsx @@ -11,73 +11,73 @@ export function StackPresetToolbar({
-

Stack Presets

+

Stack presets

Save or recall the current layer chain.

-
- -
- onPresetNameChange(event.target.value)} - /> - +
+ +
+ onPresetNameChange(event.target.value)} + /> + +
-
-
- -
- - +
+ +
+ + +
-
); } diff --git a/ui/src/components/StatusPanels.jsx b/ui/src/components/StatusPanels.jsx index bb59804..cd8daee 100644 --- a/ui/src/components/StatusPanels.jsx +++ b/ui/src/components/StatusPanels.jsx @@ -5,40 +5,66 @@ function formatNumber(value, digits = 3) { } export function StatusPanels({ app, performance, runtime, video }) { + const budgetUsedPercent = Math.max(0, Math.min(100, Number(performance.budgetUsedPercent) || 0)); + return ( <> -
-

Runtime

- -
+
+
+

Status

+
+ + {runtime.compileSucceeded ? "Ready" : "Error"} + + + {video.hasSignal ? "Signal" : "No signal"} + +
+
-
-

Video

- +
+
+

Runtime

+ +
+ Budget used + +
+ +
+

Video

+ +
+
-

Compiler

-
{runtime.compileMessage || "No compiler output."}
+

Compiler

+
+          {runtime.compileMessage || "No compiler output."}
+        
); diff --git a/ui/src/styles.css b/ui/src/styles.css index 66cb1d3..daa65d7 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -1,8 +1,27 @@ :root { color-scheme: dark; - font-family: Inter, "Segoe UI", system-ui, sans-serif; - background: #0c1017; - color: #eef4ff; + --app-bg: #11141a; + --app-surface: #171b23; + --app-surface-2: #1d2430; + --app-text: #e7ebf0; + --app-muted: #9aa7b6; + --app-border: #303947; + --app-primary: #1a9cdb; + --app-primary-strong: #147fb6; + --app-primary-soft: rgba(26, 156, 219, 0.22); + --app-warning: #e0ac34; + --app-warning-soft: rgba(224, 172, 52, 0.14); + --app-error: #d9534f; + --app-error-soft: rgba(217, 83, 79, 0.14); + --app-radius: 8px; + --app-radius-sm: 4px; + --app-space: 1rem; + --app-container: 980px; + --app-font: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --app-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + background: var(--app-bg); + color: var(--app-text); + font-family: var(--app-font); } * { @@ -17,9 +36,10 @@ body, body { margin: 0; - background: - radial-gradient(circle at 18% -12%, rgba(58, 113, 193, 0.22), transparent 30%), - linear-gradient(180deg, #0d1119 0%, #090d13 100%); + background: var(--app-bg); + color: var(--app-text); + font-family: var(--app-font); + line-height: 1.5; } button, @@ -29,233 +49,452 @@ pre { font: inherit; } -label, -h2, -h3 { - margin: 0; - font-size: 14px; - font-weight: 650; +button, +input, +select, +.panel, +.callout, +.definition-card, +.log-panel, +.layer-card, +.parameter, +.shader-picker__popover, +.shader-picker__list, +.shader-picker__trigger, +.shader-picker__option { + border-radius: var(--app-radius); } -h1 { - margin: 0; - font-size: 28px; - font-weight: 750; +h1, +h2, +h3, +p { + margin-top: 0; +} + +h2 { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 720; letter-spacing: 0; } +h3, +label { + margin-bottom: 0; + font-size: 0.95rem; + font-weight: 680; + letter-spacing: 0; +} + +h4 { + margin: 0; + color: var(--app-muted); + font-size: 0.78rem; + font-weight: 720; + letter-spacing: 0; + text-transform: uppercase; +} + +button, +input[type="number"], +input[type="text"], +select { + width: 100%; + min-height: 38px; + border: 1px solid var(--app-border); + background: var(--app-surface-2); + color: inherit; + padding: 0.65rem 0.75rem; + outline: none; +} + input[type="range"] { width: 100%; + accent-color: var(--app-primary); +} + +input[type="checkbox"] { + accent-color: var(--app-primary); } input[type="color"] { width: 100%; min-height: 38px; - border-radius: 6px; - border: 1px solid #303a4d; - background: #101722; + border: 1px solid var(--app-border); + border-radius: var(--app-radius-sm); + background: var(--app-surface-2); padding: 4px; } -input[type="number"], -input[type="text"], -select, -button { - width: 100%; - min-height: 38px; - border-radius: 6px; - border: 1px solid #303a4d; - background: #101722; - color: inherit; - padding: 8px 10px; - outline: none; -} - input:focus, select:focus, button:focus-visible { - border-color: #6d95d8; - box-shadow: 0 0 0 3px rgba(109, 149, 216, 0.18); + border-color: var(--app-primary); + box-shadow: 0 0 0 3px var(--app-primary-soft); } button { + border-color: transparent; + background: var(--app-primary); + color: #fff; cursor: pointer; - background: #213553; - border-color: #385174; transition: background 120ms ease, border-color 120ms ease, transform 120ms ease; } button:hover:not(:disabled) { - background: #294266; - border-color: #5172a0; + background: var(--app-primary-strong); } button:active:not(:disabled) { transform: translateY(1px); } -button:disabled { - cursor: default; - opacity: 0.48; +button:disabled, +input:disabled, +select:disabled { + cursor: not-allowed; + opacity: 0.55; } pre { margin: 0; white-space: pre-wrap; - color: #cbd7ea; } .layout { - width: calc(100% - 40px); + width: min(100% - 2rem, var(--app-container)); margin: 0 auto; - padding: 22px 0 28px; + padding: 21px 0 24px; display: grid; - gap: 18px; + gap: var(--app-space); } .app-header { display: flex; align-items: center; justify-content: space-between; - gap: 16px; - min-height: 64px; + gap: var(--app-space); + margin-bottom: 0; +} + +.muted, +.section-note { + margin-bottom: 0; + color: var(--app-muted); +} + +.panel { + margin: 0; + padding: var(--app-space); + border: 1px solid var(--app-border); + background: var(--app-surface); +} + +.panel__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.85rem; + margin-bottom: 0.9rem; +} + +.panel__header button { + width: auto; + min-width: 9rem; +} + +.dashboard-grid, +.stack-panel__grid, +.definition-grid, +.summary-grid, +.parameter-grid, +.toolbar__inline { + display: grid; + gap: var(--app-space); +} + +.dashboard-grid, +.stack-panel__grid { + grid-template-columns: 1fr 1fr; + align-items: stretch; +} + +.panel--compiler, +.panel--telemetry, +.stack-panel { + grid-column: 1 / -1; +} + +.definition-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin: var(--app-space) 0 0; + padding: 0; +} + +.definition-grid.compact { + gap: 0.75rem; +} + +.definition-card { + min-width: 0; + padding: 0.75rem 1rem; + border: 1px solid var(--app-border); + background: rgba(255, 255, 255, 0.025); +} + +.definition-card dt { + color: var(--app-muted); + font-size: 0.82rem; +} + +.definition-card dd { + margin: 0.15rem 0 0; + overflow-wrap: anywhere; +} + +.telemetry-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.9rem; +} + +.status-badges { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 0.5rem; +} + +.telemetry-sections { + display: grid; + grid-template-columns: minmax(0, 1.45fr) minmax(18rem, 0.8fr); + gap: 1.25rem; +} + +.telemetry-section { + min-width: 0; + display: grid; + align-content: start; + gap: 0.55rem; +} + +.mini-status { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 1.75rem; + padding: 0.2rem 0.55rem; + border: 1px solid var(--app-border); + border-radius: var(--app-radius-sm); + background: var(--app-surface-2); + font-size: 0.76rem; + font-weight: 700; + white-space: nowrap; +} + +.mini-status--ready { + color: #c5efd3; + border-color: rgba(94, 178, 121, 0.45); + background: rgba(67, 135, 89, 0.18); +} + +.mini-status--error { + color: #ffd0cf; + border-color: rgba(217, 83, 79, 0.45); + background: var(--app-error-soft); +} + +.kv-rows { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); + gap: 0; + margin: 0; + padding: 0; + border-top: 1px solid var(--app-border); +} + +.kv-row { + min-width: 0; + display: grid; + grid-template-columns: minmax(6.25rem, 0.75fr) minmax(0, 1fr); + gap: 0.65rem; + align-items: baseline; + padding: 0.55rem 0.75rem 0.55rem 0; + border-bottom: 1px solid rgba(48, 57, 71, 0.7); +} + +.kv-row dt { + color: var(--app-muted); + font-size: 0.86rem; +} + +.kv-row dd { + min-width: 0; + margin: 0; + overflow-wrap: break-word; + color: #f2f6fb; + font-size: 1rem; + font-weight: 650; + line-height: 1.35; +} + +.meter-row { + display: grid; + grid-template-columns: auto minmax(8rem, 1fr) auto; + gap: 0.75rem; + align-items: center; + margin-top: 0.75rem; + color: var(--app-muted); + font-size: 0.82rem; +} + +.meter-row strong { + color: var(--app-text); + font-size: 0.95rem; +} + +.app-summary { + padding: 0.75rem 1rem; +} + +.summary-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.75rem; + margin: 0; +} + +.summary-item { + min-width: 0; + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; + padding-right: 0.75rem; + border-right: 1px solid var(--app-border); +} + +.summary-item:last-child { + padding-right: 0; + border-right: 0; +} + +.summary-item dt { + color: var(--app-muted); + font-size: 0.82rem; +} + +.summary-item dd { + margin: 0; + overflow-wrap: anywhere; + font-weight: 700; } .status-pill { display: inline-flex; align-items: center; justify-content: center; - min-width: 112px; + width: auto; + min-width: 7rem; min-height: 34px; - padding: 6px 12px; - border-radius: 999px; - border: 1px solid #31405a; - background: #101722; - font-size: 13px; + padding: 0.35rem 0.7rem; + border: 1px solid var(--app-border); + border-radius: var(--app-radius); + background: var(--app-surface-2); + font-size: 0.82rem; font-weight: 700; + white-space: nowrap; } .status-pill--ready { - color: #bff0cc; - border-color: rgba(96, 177, 116, 0.45); - background: rgba(41, 94, 58, 0.32); + color: #c5efd3; + border-color: rgba(94, 178, 121, 0.45); + background: rgba(67, 135, 89, 0.22); } .status-pill--error { color: #ffd0cf; - border-color: rgba(222, 101, 101, 0.55); - background: rgba(112, 45, 48, 0.32); + border-color: rgba(217, 83, 79, 0.45); + background: var(--app-error-soft); } -.dashboard-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 16px; - align-items: stretch; +.progress-track { + height: 0.5rem; + overflow: hidden; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); } -.panel { - background: rgba(20, 26, 36, 0.92); - border: 1px solid #263144; - border-radius: 8px; - padding: 16px; - box-shadow: 0 14px 36px rgba(0, 0, 0, 0.18); +.progress-bar { + height: 100%; + border-radius: inherit; + background: var(--app-primary); + transition: width 180ms ease-out; } -.panel--compiler { - grid-column: 1 / -1; +.progress-bar.is-indeterminate { + width: 35%; + animation: app-look-loading-slide 1.4s ease-in-out infinite; } -.panel__header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 14px; -} - -.panel__header button { - width: auto; - min-width: 150px; -} - -.kv { - display: grid; - grid-template-columns: minmax(130px, 0.75fr) minmax(0, 1fr); - gap: 9px 14px; - margin: 12px 0 0; -} - -.kv dt { - color: #98aad0; -} - -.kv dd { - min-width: 0; - margin: 0; +.log-panel { + max-height: 15rem; + margin-top: var(--app-space); + overflow: auto; + padding: 0.9rem 1rem; + border: 1px solid var(--app-border); + background: var(--app-surface-2); + color: #cbd7ea; + font-family: var(--app-mono); + font-size: 0.875rem; + line-height: 1.45; overflow-wrap: anywhere; } .stack-panel { - grid-column: 1 / -1; display: grid; align-content: start; - gap: 14px; -} - -.stack-panel__header { - margin-bottom: 0; } .stack-panel__reload { - min-width: 132px; -} - -.stack-panel__grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 14px; + min-width: 8.25rem; } .toolbar__group { display: grid; - gap: 8px; + gap: 0.5rem; + min-width: 0; } .toolbar__inline { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(120px, 0.28fr); - gap: 8px; -} - -.toolbar__inline button { - width: 100%; - min-width: 92px; + grid-template-columns: minmax(0, 1fr) minmax(7.5rem, 0.28fr); + gap: 0.5rem; } .layer-stack { display: grid; - gap: 12px; + gap: 0.75rem; } .layer-card { display: grid; - gap: 14px; - padding: 14px; - border: 1px solid #273246; - border-radius: 8px; - background: #101722; - transition: border-color 120ms ease, background 120ms ease, transform 120ms ease; + gap: 0.9rem; + padding: 0.875rem; + border: 1px solid var(--app-border); + background: var(--app-surface-2); + transition: border-color 120ms ease, background 120ms ease; } .layer-card:hover { - border-color: #425a7b; + border-color: rgba(26, 156, 219, 0.45); } .layer-card--expanded { - border-color: #6d95d8; - background: #131d2b; - box-shadow: inset 0 0 0 1px rgba(109, 149, 216, 0.22); + border-color: var(--app-primary); + background: #182232; + box-shadow: inset 0 0 0 1px var(--app-primary-soft); } .layer-card--dragging { @@ -263,8 +502,12 @@ pre { } .layer-card--drop-target { - border-color: #9ebcf0; - box-shadow: 0 0 0 2px rgba(158, 188, 240, 0.2); + border-color: #9ecff0; + box-shadow: 0 0 0 2px rgba(26, 156, 219, 0.2); +} + +.layer-card--add { + border-style: dashed; } .layer-card__header, @@ -273,7 +516,7 @@ pre { .layer-card__subheader { display: flex; align-items: center; - gap: 10px; + gap: 0.625rem; } .layer-card__header, @@ -287,6 +530,16 @@ pre { flex-wrap: wrap; } +.layer-card__actions { + justify-content: flex-end; +} + +.layer-card__actions button, +.layer-card__subheader button { + width: auto; + min-width: 5.25rem; +} + .icon-button { width: 38px; min-width: 38px; @@ -302,10 +555,10 @@ pre { justify-content: center; width: 30px; height: 30px; - border-radius: 999px; - background: #203452; - color: #d7e4f8; - font-size: 12px; + border-radius: var(--app-radius-sm); + background: rgba(26, 156, 219, 0.18); + color: #dff2fc; + font-size: 0.75rem; font-weight: 750; } @@ -314,7 +567,8 @@ pre { min-width: 0; flex: 1 1 auto; text-align: left; - background: #1f314e; + background: #203047; + border-color: #34465d; } .layer-card__title--static { @@ -322,7 +576,7 @@ pre { } .layer-card__drag-handle { - color: #98aad0; + color: var(--app-muted); cursor: grab; user-select: none; display: inline-flex; @@ -330,55 +584,82 @@ pre { justify-content: center; width: 34px; height: 34px; - flex: 0 0 auto; + min-width: 34px; + padding: 0; + border-color: transparent; + background: transparent; } -.layer-card__actions { - justify-content: flex-end; +.layer-card__drag-handle:hover:not(:disabled), +.parameter__osc:hover:not(:disabled) { + background: transparent; + color: var(--app-text); } -.layer-card__actions button { - width: auto; - min-width: 78px; -} - -.layer-card__body { +.layer-card__body, +.layer-card__field, +.shader-picker, +.parameter { display: grid; - gap: 14px; -} - -.layer-card--add { - border-style: dashed; -} - -.layer-card__field { - display: grid; - gap: 8px; -} - -.shader-picker { - display: grid; - gap: 8px; - min-width: 0; + gap: 0.75rem; } .shader-picker__topline { display: flex; align-items: baseline; justify-content: space-between; - gap: 10px; + gap: 0.625rem; min-width: 0; } +.shader-picker__selected, +.shader-picker__meta, +.shader-picker__empty, +.parameter__value, +.parameter__alpha, +.parameter__osc { + color: var(--app-muted); + font-size: 0.78rem; +} + .shader-picker__selected { min-width: 0; - color: #98aad0; - font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.shader-picker__trigger { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.625rem; + min-height: 54px; + text-align: left; + background: #182232; + border-color: var(--app-border); +} + +.shader-picker__trigger > span { + display: grid; + gap: 0.125rem; + min-width: 0; +} + +.shader-picker__trigger svg, +.shader-picker__search svg { + flex: 0 0 auto; + color: var(--app-muted); +} + +.shader-picker__popover { + display: grid; + gap: 0.5rem; + padding: 0.5rem; + border: 1px solid var(--app-border); + background: #121821; +} + .shader-picker__search { position: relative; display: flex; @@ -387,74 +668,40 @@ pre { .shader-picker__search svg { position: absolute; - left: 10px; - color: #98aad0; + left: 0.625rem; pointer-events: none; } .shader-picker__search input { - padding-left: 34px; -} - -.shader-picker__trigger { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - min-height: 54px; - padding: 8px 10px; - text-align: left; - background: #121b28; - border-color: #26364e; -} - -.shader-picker__trigger > span { - display: grid; - gap: 2px; - min-width: 0; -} - -.shader-picker__trigger svg { - flex: 0 0 auto; - color: #98aad0; -} - -.shader-picker__popover { - display: grid; - gap: 8px; - padding: 8px; - border: 1px solid #273246; - border-radius: 8px; - background: #0d141f; + padding-left: 2.125rem; } .shader-picker__list { display: grid; - gap: 6px; + gap: 0.375rem; max-height: 220px; overflow-y: auto; - padding: 6px; - border: 1px solid #273246; - border-radius: 6px; - background: #0c121b; + padding: 0.375rem; + border: 1px solid var(--app-border); + background: #10151d; } .shader-picker__option { display: grid; - gap: 2px; + gap: 0.125rem; min-height: 58px; - padding: 8px 10px; + padding: 0.5rem 0.625rem; text-align: left; - background: #121b28; - border-color: #26364e; + background: #182232; + border-color: var(--app-border); align-content: center; line-height: 1.25; } .shader-picker__option--selected { - background: #233b5f; - border-color: #6d95d8; - box-shadow: inset 0 0 0 1px rgba(109, 149, 216, 0.25); + background: #203b54; + border-color: var(--app-primary); + box-shadow: inset 0 0 0 1px var(--app-primary-soft); } .shader-picker__name, @@ -468,40 +715,25 @@ pre { font-weight: 700; } -.shader-picker__meta, .shader-picker__empty { - color: #98aad0; - font-size: 12px; -} - -.shader-picker__empty { - padding: 10px; -} - -.layer-card__subheader button { - width: auto; - min-width: 96px; + padding: 0.625rem; } .parameter-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 10px; + grid-template-columns: repeat(auto-fit, minmax(17.5rem, 1fr)); + gap: 0.625rem; } .parameter { - display: grid; - gap: 7px; - padding: 10px; - border: 1px solid #273246; - border-radius: 8px; - background: #0f151f; + padding: 0.75rem; + border: 1px solid var(--app-border); + background: #141a23; } .parameter__header { display: grid; - grid-template-columns: minmax(90px, auto) minmax(0, 1fr); - gap: 8px; + grid-template-columns: minmax(5.5rem, auto) minmax(0, 1fr); + gap: 0.5rem; align-items: center; } @@ -509,23 +741,16 @@ pre { display: inline-flex; align-items: center; justify-content: flex-end; - gap: 6px; + gap: 0.375rem; width: auto; min-width: 0; min-height: 24px; - padding: 2px 0; + padding: 0; border: 0; background: transparent; - color: #98aad0; - font-size: 11px; font-weight: 500; } -.parameter__osc:hover:not(:disabled) { - background: transparent; - color: #c4d6f7; -} - .parameter__osc span { min-width: 0; overflow: hidden; @@ -537,45 +762,38 @@ pre { flex: 0 0 auto; } -.parameter__value { - color: #98aad0; - font-size: 12px; -} - .parameter__value--pending { - color: #d8bb76; + color: var(--app-warning); } .parameter__pair { display: grid; - grid-template-columns: repeat(auto-fit, minmax(82px, 1fr)); - gap: 8px; + grid-template-columns: repeat(auto-fit, minmax(5.125rem, 1fr)); + gap: 0.5rem; align-items: center; } .parameter__pair input[type="range"] { - min-width: 120px; + min-width: 7.5rem; } .parameter__color-row { display: grid; - grid-template-columns: minmax(92px, 0.42fr) minmax(120px, 0.58fr); - gap: 8px; + grid-template-columns: minmax(5.75rem, 0.42fr) minmax(7.5rem, 0.58fr); + gap: 0.5rem; align-items: end; } .parameter__alpha { display: grid; - gap: 4px; - color: #98aad0; - font-size: 11px; + gap: 0.25rem; font-weight: 600; } .toggle { display: inline-flex; align-items: center; - gap: 10px; + gap: 0.625rem; min-height: 36px; } @@ -587,23 +805,28 @@ pre { justify-content: flex-start; } -.muted { - margin: 0; - color: #98aad0; -} +@keyframes app-look-loading-slide { + 0% { + transform: translateX(-120%); + } -@media (max-width: 1100px) { - .dashboard-grid { - grid-template-columns: 1fr; + 100% { + transform: translateX(340%); } } -@media (max-width: 760px) { - .layout { - width: min(100% - 20px, 1500px); - padding-top: 14px; +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; } +} +@media (max-width: 900px) { .app-header, .panel__header, .layer-card__header, @@ -618,6 +841,11 @@ pre { grid-template-columns: 1fr; } + .telemetry-sections { + grid-template-columns: 1fr; + } + + .panel__header button, .toolbar__inline button, .stack-panel__reload, .layer-card__actions, @@ -626,3 +854,44 @@ pre { width: 100%; } } + +@media (max-width: 560px) { + .layout { + width: min(100% - 1rem, var(--app-container)); + padding-top: 14px; + } + + .definition-grid, + .summary-grid, + .kv-rows, + .parameter-grid, + .parameter__header, + .parameter__color-row { + grid-template-columns: 1fr; + } + + .kv-row { + grid-template-columns: minmax(6.25rem, 0.6fr) minmax(0, 1fr); + } + + .meter-row { + grid-template-columns: 1fr auto; + } + + .meter-row .progress-track { + grid-column: 1 / -1; + grid-row: 2; + } + + .summary-item { + padding-right: 0; + padding-bottom: 0.5rem; + border-right: 0; + border-bottom: 1px solid var(--app-border); + } + + .summary-item:last-child { + padding-bottom: 0; + border-bottom: 0; + } +}