From e59271aa0077fc321ea79b5ff9e621f7a94299ec Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Tue, 16 Jun 2026 00:19:47 -0400 Subject: [PATCH] feat(mix): lava-lamp popover with RadialKnob controls + wider Mix detail body (P10 W4) --- .../Controls/MixVisualizerControls.razor | 94 +++++++++---------- .../Controls/MixVisualizerControls.razor.css | 35 +++---- .../Controls/ReleaseDetailScaffold.razor | 12 ++- .../Controls/ReleaseDetailScaffold.razor.cs | 7 ++ DeepDrftPublic.Client/Pages/MixDetail.razor | 43 +++++++-- .../wwwroot/styles/deepdrft-styles.css | 20 ++++ DeepDrftShared.Client/Common/DDIcons.cs | 15 +++ 7 files changed, 148 insertions(+), 78 deletions(-) diff --git a/DeepDrftPublic.Client/Controls/MixVisualizerControls.razor b/DeepDrftPublic.Client/Controls/MixVisualizerControls.razor index 53ed529..90e8a99 100644 --- a/DeepDrftPublic.Client/Controls/MixVisualizerControls.razor +++ b/DeepDrftPublic.Client/Controls/MixVisualizerControls.razor @@ -2,71 +2,71 @@ @using DeepDrftPublic.Client.Services @inject MixVisualizerControlState ControlState -@* The Mix visualizer controls row (Phase 10, Wave 2). Four continuous sliders — resolution, - bubblyness, detach, color-shift speed — placed above the mix details and below the back button. - This component owns NO JS interop: it mutates the shared, session-scoped MixVisualizerControlState - and raises its Changed event. The backdrop bridge (MixWaveformVisualizer) subscribes to that event - and pushes the affected uniform to the WebGL module. That keeps the JS module handle single-owned - by the bridge and this component purely presentational. None of these is a seek surface (spec §D). *@ +@* The Mix visualizer controls (Phase 10, Wave 4). Four continuous RadialKnobs — resolution, + bubblyness, detach, color-shift speed — laid out in a row. This component lives inside the + lava-lamp popover on the Mix detail page (Wave 4 moved it out of the always-visible TopContent row). + It owns NO JS interop: it mutates the shared, session-scoped MixVisualizerControlState and raises its + Changed event. The backdrop bridge (MixWaveformVisualizer) subscribes to that event and pushes the + affected uniform to the WebGL module. That keeps the JS module handle single-owned by the bridge and + this component purely presentational. None of these is a seek surface (spec §D). + + RadialKnob has no icon slot (its Label renders as SVG text), so each control's Material icon rides + beside its knob as an adjacent MudIcon caption (spec §7e). HoldValue stays false so the knobs are + live — ValueChanged fires continuously during drag, exactly as the sliders fired before, preserving + the "visibly and continuously affects its target" feel and the Changed/NotifyChanged seam. *@
-
+ @* RadialKnob exposes no aria-label/attribute-capture and is out of scope to modify, so the + accessible name rides on the wrapping group div instead (a plain element accepts it). *@ +
+ -
-
+
+ -
-
+
+ -
-
+
+ -
@code { - // Resolution rides the log mapping (slider fraction [0,1] ↔ visible seconds); the other three are + // Resolution rides the log mapping (knob fraction [0,1] ↔ visible seconds); the other three are // already normalized [0,1] and bind to their state properties directly. private double ResolutionFraction => MixZoomMapping.SecondsToFraction(ControlState.VisibleSeconds); diff --git a/DeepDrftPublic.Client/Controls/MixVisualizerControls.razor.css b/DeepDrftPublic.Client/Controls/MixVisualizerControls.razor.css index 0653f51..40d7fe2 100644 --- a/DeepDrftPublic.Client/Controls/MixVisualizerControls.razor.css +++ b/DeepDrftPublic.Client/Controls/MixVisualizerControls.razor.css @@ -1,34 +1,25 @@ -/* The controls row sits in the mix-detail foreground, below the back button and above the masthead. - A horizontal row of four icon+slider controls. On narrow viewports it wraps to keep all four - present (spec §3b: wrap is the chosen mobile behaviour — none may drop). */ +/* The controls live inside the lava-lamp popover (Wave 4). Four knob+icon stacks laid out in a row. + On a narrow viewport the row wraps to 2×2 so all four stay reachable (spec §7d: none may drop). */ .mix-visualizer-controls { display: flex; flex-wrap: wrap; - align-items: center; - gap: 0.75rem 1.5rem; - margin: 0.5rem 0 1.5rem; + align-items: flex-start; + justify-content: center; + gap: 1rem 1.25rem; + padding: 0.25rem; } -/* One control: a compact label icon followed by the slider. The slider gets a fixed-ish track width - so the four read as a tidy row rather than stretching unevenly. */ +/* One control: a RadialKnob with its Material icon as a caption underneath. RadialKnob has no icon + slot, so the icon rides adjacent (spec §7e). Center the pair so the four read as a tidy row. */ .mix-visualizer-control { display: flex; + flex-direction: column; align-items: center; - gap: 0.5rem; - flex: 1 1 180px; - min-width: 160px; - max-width: 260px; + gap: 0.35rem; } -.mix-visualizer-control-icon { - flex: 0 0 auto; +/* The caption icon is a MudIcon (a Razor component), so Blazor CSS isolation does not stamp the scope + attribute onto its element — reach it with ::deep. */ +.mix-visualizer-control ::deep .mix-visualizer-control-icon { opacity: 0.7; } - -/* MudSlider renders a Razor component, so its root is reached with ::deep (a bare scoped selector - would not be stamped onto the child component's element). Let the slider fill the remaining width - of its control so the icon+slider pair lays out cleanly. */ -.mix-visualizer-control ::deep .mud-slider { - flex: 1 1 auto; - margin: 0; -} diff --git a/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor b/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor index 947945d..8ac92cc 100644 --- a/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor +++ b/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor @@ -7,9 +7,15 @@
- - ← @BackLabel - + @* Back link top-left, optional medium action top-right, on one SpaceBetween row. The action slot + stays null for media that don't supply it (Track/Session), so they render the back link alone. *@ + + + ← @BackLabel + + + @TopRightAction + @TopContent diff --git a/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor.cs b/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor.cs index 9f90815..02defd4 100644 --- a/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor.cs +++ b/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor.cs @@ -31,6 +31,13 @@ public partial class ReleaseDetailScaffold : ComponentBase /// [Parameter] public RenderFragment? TopContent { get; set; } + /// + /// Optional action rendered at the top-right of the container, on the same SpaceBetween row as the + /// back link (back link left, action right). The Mix detail page uses it for the lava-lamp + /// visualizer-settings button; other media leave it null and render the back link alone. + /// + [Parameter] public RenderFragment? TopRightAction { get; set; } + /// Medium-specific hero visual (cover art, hero image, or waveform background). [Parameter] public RenderFragment? Hero { get; set; } diff --git a/DeepDrftPublic.Client/Pages/MixDetail.razor b/DeepDrftPublic.Client/Pages/MixDetail.razor index e206344..532d506 100644 --- a/DeepDrftPublic.Client/Pages/MixDetail.razor +++ b/DeepDrftPublic.Client/Pages/MixDetail.razor @@ -40,18 +40,40 @@ else
+ - - @* The four visualizer controls — resolution, bubblyness, detach, color-shift speed — - in a row below the back button and above the masthead (spec §3). They mutate the - shared MixVisualizerControlState; the backdrop bridge above pushes the uniforms. *@ - - + + @* Lava-lamp button top-right, across from the back link. Toggles a popover holding the + four visualizer knobs (spec §7c/§7d). The controls themselves are unchanged — they + mutate the shared MixVisualizerControlState; the backdrop bridge pushes the uniforms. + The popover only progressively-discloses them off the always-visible row. *@ + + + + + @* Outside-click close via MudOverlay (the SharePopover idiom). A knob drag never lands + on this overlay — the knob's own global capture overlay is a child of the popover + content above it — so dragging a knob does not dismiss the popover. *@ + + + + + +
@if (!string.IsNullOrEmpty(release.ImagePath)) @@ -85,9 +107,18 @@ else } +
} @code { protected override string PersistKey => "mix-detail"; + + // Lava-lamp settings popover open state. Pure presentation over MixVisualizerControlState — the + // popover discloses the four knobs; toggling it touches no control value or bridge push. + private bool _settingsOpen; + + private void ToggleSettings() => _settingsOpen = !_settingsOpen; + + private void CloseSettings() => _settingsOpen = false; } diff --git a/DeepDrftPublic/wwwroot/styles/deepdrft-styles.css b/DeepDrftPublic/wwwroot/styles/deepdrft-styles.css index 1032225..2601f6c 100644 --- a/DeepDrftPublic/wwwroot/styles/deepdrft-styles.css +++ b/DeepDrftPublic/wwwroot/styles/deepdrft-styles.css @@ -281,6 +281,17 @@ h2, h3, h4, h5, h6, padding: 3rem 1.5rem 4rem; } +/* Mix detail widens its body to the Sessions detail width (MudContainer Large, ~1280px) by hosting the + scaffold inside a MudContainer Large and neutralizing the scaffold's own 760px cap for that instance. + Both classes are global, so a plain descendant selector reaches the scaffold div without ::deep. The + horizontal gutter is dropped here because the wrapping MudContainer supplies its own. Mix-scoped, so + Track detail (which also uses .deepdrft-track-detail-container) stays at 760px. */ +.mix-detail-container .deepdrft-track-detail-container { + max-width: none; + padding-left: 0; + padding-right: 0; +} + .deepdrft-track-detail-back { display: inline-flex; align-items: center; @@ -353,6 +364,15 @@ h2, h3, h4, h5, h6, max-width: 360px; } +/* The lava-lamp visualizer-settings popover (Wave 4). Holds the four RadialKnobs in a row; sized so the + four read clearly with comfortable padding, wrapping to 2×2 on narrow viewports (the inner + .mix-visualizer-controls owns the flex-wrap). MudPopover renders into the popover-provider portal at + the document root, so this is a global class — not component-scoped. */ +.mix-visualizer-popover { + padding: 0.75rem; + max-width: 360px; +} + /* Monospace snippet so the iframe markup stays legible inside the readonly field. */ .deepdrft-share-embed-field { flex: 1 1 auto; diff --git a/DeepDrftShared.Client/Common/DDIcons.cs b/DeepDrftShared.Client/Common/DDIcons.cs index 11fecb6..38f1e02 100644 --- a/DeepDrftShared.Client/Common/DDIcons.cs +++ b/DeepDrftShared.Client/Common/DDIcons.cs @@ -22,4 +22,19 @@ public static class DDIcons """; + + /// + /// Lava lamp - the Mix visualizer settings glyph. Tapered base, tall rounded glass vessel, a cap, + /// and three suspended blobs at varied heights. The silhouette is currentColor so it themes with + /// its context (Color.Secondary tint, light/dark). The blobs carry a warm two-tone accent à la the + /// gas-lamp flame so the lamp reads as "lit" at icon size; the silhouette still tints with context. + /// + public const string LavaLamp = """ + + + + + + + """; }