feat(mix-visualizer): gate knob controls with Blazor @if in TopContent band; drop CSS collapse, glass, and TopRowCenter slot

This commit is contained in:
daniel-c-harvey
2026-06-16 20:31:42 -04:00
parent daafae8af6
commit fc7c9e978f
6 changed files with 39 additions and 117 deletions
@@ -2,12 +2,12 @@
@using DeepDrftPublic.Client.Services @using DeepDrftPublic.Client.Services
@inject MixVisualizerControlState ControlState @inject MixVisualizerControlState ControlState
@* The Mix visualizer controls (lava reframe Wave R4). SEVEN continuous RadialKnobs — scroll speed, @* The Mix visualizer controls. SEVEN continuous RadialKnobs — scroll speed, gradient rotation speed,
gradient rotation speed, lava gravity, lava heat, blob density, collision strength, waveform width — lava gravity, lava heat, blob density, collision strength, waveform width — each its own dedicated
each its own dedicated control with a Material-icon caption (no more R2 temp-remapping: no knob control with a Material-icon caption. Visibility is controlled by Blazor, not CSS: the host page
caption misrepresents its function). The bar lives INLINE in the mix-detail controls area and renders this component only while the lava-lamp toggle is on (@if-guarded), so when off it is not in
ANIMATES open/closed in place via CSS transition off the @Expanded flag — it reads as the controls the DOM at all. There is no collapse/expand animation and no glass surface — the knobs simply appear
collapsing/expanding, NOT a floating popover/drawer (§7b). in their own transparent band and disappear when un-rendered.
It owns NO JS interop: it mutates the shared, session-scoped MixVisualizerControlState and raises its 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 Changed event. The backdrop bridge (MixWaveformVisualizer) subscribes to that event and pushes the
@@ -17,13 +17,9 @@
RadialKnob has no icon slot (its Label renders as SVG text) and no aria attribute-capture, so each RadialKnob has no icon slot (its Label renders as SVG text) and no aria attribute-capture, so each
control's Material icon rides beside its knob as an adjacent MudIcon caption and the accessible name control's Material icon rides beside its knob as an adjacent MudIcon caption and the accessible name
rides on the wrapping group div (§7d). HoldValue stays false so the knobs are live — ValueChanged rides on the wrapping group div (§7d). HoldValue stays false so the knobs are live — ValueChanged
fires continuously during drag, preserving the Changed/NotifyChanged seam. fires continuously during drag, preserving the Changed/NotifyChanged seam. *@
Aesthetic: the bar matches the session-hero NowPlaying overlay (§7e) — a translucent dark glass <div class="mix-visualizer-controls-bar">
surface with overlay-label captions and Color.Secondary accents, so it reads as of-a-piece with the
hero rather than a generic MudBlazor panel. *@
<div class="mix-visualizer-controls-bar @(Expanded ? "is-expanded" : "")" aria-hidden="@(!Expanded)">
<div class="mix-visualizer-control" role="group" aria-label="Waveform scroll speed"> <div class="mix-visualizer-control" role="group" aria-label="Waveform scroll speed">
<RadialKnob Value="@ControlState.ScrollSpeed" <RadialKnob Value="@ControlState.ScrollSpeed"
@@ -91,12 +87,6 @@
</div> </div>
@code { @code {
/// <summary>
/// Whether the knob bar is expanded. Owned by the host page (the lava-lamp toggle button flips it);
/// drives the CSS open/close transition. When false the bar collapses to zero size in place.
/// </summary>
[Parameter] public bool Expanded { get; set; }
// Each handler mutates its own dedicated property then raises Changed — the bridge re-reads and // Each handler mutates its own dedicated property then raises Changed — the bridge re-reads and
// pushes the affected dial. All values are already normalized [0,1]; the bridge maps scroll speed // pushes the affected dial. All values are already normalized [0,1]; the bridge maps scroll speed
// to a visible time-span and routes the rest straight to the lava/colour dials. // to a visible time-span and routes the rest straight to the lava/colour dials.
@@ -1,63 +1,14 @@
/* The seven-knob container lives IN-FLOW in the scaffold's top-row center zone, between the back link /* The seven-knob band. Blazor gates its presence (the host renders this component only while the
and the lava-lamp toggle, and grows open/closed in place (lava reframe §7b) — NOT a popover, drawer, lava-lamp is on), so this is purely a layout rule for the visible state — no collapse machinery, no
or floating overlay. Collapsed, it has zero footprint and is fully transparent; the @Expanded flag transitions, no glass surface. A plain transparent horizontal flex row of the seven knobs that wraps
(mirrored to the .is-expanded class) transitions it open by growing its width. The PRIMARY animated to a second line only if the band is genuinely too narrow. */
axis is horizontal width — the controls area opening between the back link and the lamp; max-height is
a secondary axis for the wrap case. Closed state is pointer-events:none + visibility:hidden so
collapsed knobs are not focusable or hit-testable. */
.mix-visualizer-controls-bar { .mix-visualizer-controls-bar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: flex-start; align-items: flex-start;
justify-content: flex-start;
gap: 0.85rem 1rem;
/* Collapsed: zero horizontal footprint, transparent, no layout space. */
max-width: 0;
max-height: 0;
opacity: 0;
overflow: hidden;
visibility: hidden;
pointer-events: none;
/* NowPlaying glass surface (§7e): translucent dark shim, soft blur, rounded, secondary-tinted
hairline — matches the session-hero overlay family. Padding animates in with the size. */
padding: 0;
border-radius: 10px;
background: rgba(13, 27, 42, 0.55);
border: 1px solid color-mix(in srgb, var(--mud-palette-secondary) 22%, transparent);
backdrop-filter: blur(10px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
transition:
max-width 0.32s cubic-bezier(0.22, 0.61, 0.36, 1),
max-height 0.32s cubic-bezier(0.22, 0.61, 0.36, 1),
opacity 0.24s ease,
padding 0.32s cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* Expanded: grows horizontally in the row's flow. Wide enough to hold all seven 64px knobs (with
captions and gaps) on one line where the row has room; on a narrower center zone the knobs flex-wrap
to a second in-flow line and max-height absorbs the taller stack — never clipped, never floating. */
.mix-visualizer-controls-bar.is-expanded {
max-width: 720px;
max-height: 420px;
opacity: 1;
visibility: visible;
pointer-events: auto;
padding: 0.85rem 1rem;
}
/* Narrow rows: the controls container can't sit beside the back link and lamp on one line, so it takes
the full row width and the scaffold's flex-wrapped top row drops it to its own in-flow line below the
back/lamp pair (§7b-responsive). Still fully in-flow — never floats, never clips. The seven knobs get
the row's full width and wrap within it. */
@media (max-width: 959.98px) {
.mix-visualizer-controls-bar.is-expanded {
flex-basis: 100%;
max-width: 100%;
justify-content: center; justify-content: center;
} gap: 0.85rem 1rem;
margin: 0.5rem 0;
} }
/* One control: a RadialKnob with its Material icon caption underneath. RadialKnob has no icon slot, so /* One control: a RadialKnob with its Material icon caption underneath. RadialKnob has no icon slot, so
@@ -7,20 +7,16 @@
<div class="deepdrft-track-detail-container"> <div class="deepdrft-track-detail-container">
@* Three-zone top row: back link (left) | optional center affordance | optional action (right), on @* Two-end top row: back link (left) | optional action (right), on one SpaceBetween row. The action
one SpaceBetween row. Both the center and action slots stay null for media that don't supply them slot stays null for media that don't supply it (Track/Cut/Session), so SpaceBetween collapses to
(Track/Cut/Session), so SpaceBetween collapses to the back link alone at the left edge. The Mix the back link alone at the left edge. The Mix detail page fills the action with its lava-lamp
detail page fills the center with its in-flow visualizer-controls container (§7b). The native toggle and renders its knob band below via TopContent. *@
wrapper lets the row flex-wrap the center zone to its own line on narrow widths, keeping the
controls in-flow rather than clipping. *@
<div class="deepdrft-track-detail-top-row"> <div class="deepdrft-track-detail-top-row">
<MudStack Row AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="deepdrft-track-detail-top-row-stack"> <MudStack Row AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudLink Href="@BackHref" Typo="Typo.body2" Class="deepdrft-track-detail-back"> <MudLink Href="@BackHref" Typo="Typo.body2" Class="deepdrft-track-detail-back">
&larr; @BackLabel &larr; @BackLabel
</MudLink> </MudLink>
@TopRowCenter
@TopRightAction @TopRightAction
</MudStack> </MudStack>
</div> </div>
@@ -25,21 +25,13 @@ public partial class ReleaseDetailScaffold : ComponentBase
[Parameter] public string BackLabel { get; set; } = "Archive"; [Parameter] public string BackLabel { get; set; } = "Archive";
/// <summary> /// <summary>
/// Optional medium-specific content rendered between the back link and the masthead — the "below /// Optional medium-specific content rendered as its own full-width band between the back/action top
/// the back button, above the details" band. The Mix detail page uses it for the visualizer /// row and the masthead — the "below the back button, above the details" band. The Mix detail page
/// controls row; other media leave it null. /// uses it for the visualizer knob row (gated by Blazor on the lava-lamp toggle); other media leave
/// it null.
/// </summary> /// </summary>
[Parameter] public RenderFragment? TopContent { get; set; } [Parameter] public RenderFragment? TopContent { get; set; }
/// <summary>
/// Optional affordance rendered in the center of the top row, between the back link (left) and
/// <see cref="TopRightAction"/> (right). The Mix detail page uses it for the in-flow visualizer-
/// controls container that grows in place between the back link and the lava-lamp toggle (§7b);
/// other media leave it null, so the SpaceBetween row collapses the center and renders the back
/// link alone. Variance rides the slot, never a flag (Phase 9 §5.3).
/// </summary>
[Parameter] public RenderFragment? TopRowCenter { get; set; }
/// <summary> /// <summary>
/// Optional action rendered at the top-right of the container, on the same SpaceBetween row as the /// 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 /// back link (back link left, action right). The Mix detail page uses it for the lava-lamp
@@ -3,12 +3,3 @@
justify-content: center; justify-content: center;
margin-top: 1.5rem; margin-top: 1.5rem;
} }
/* The three-zone top row: back link | center affordance | action. The center zone (the Mix
visualizer-controls container) grows in-flow between the two pinned ends. On narrow widths the row
flex-wraps so the center zone drops to its own line below the back/action pair — keeping the seven
knobs in-flow with the full row width rather than clipping. The MudStack output is a child Razor
component's native div, so ::deep is required to reach it. */
::deep .deepdrft-track-detail-top-row-stack {
flex-wrap: wrap;
}
+16 -14
View File
@@ -48,18 +48,20 @@ else
BackLabel="All mixes" BackLabel="All mixes"
ShowMeta="@(hasGenre || hasDate)" ShowMeta="@(hasGenre || hasDate)"
ShowShareRow="false"> ShowShareRow="false">
<TopRowCenter> <TopContent>
@* In-flow seven-knob control container, between the back link and the lava-lamp toggle. @* The seven-knob band lives in its own full-width area below the back/lamp top row.
It grows in place (width/opacity transition) when expanded and collapses to zero Blazor — not CSS — controls its visibility: it is rendered only while the lava-lamp is
footprint when closed — never a popover, drawer, or floating overlay (§7b). The on, so when off it is not in the DOM at all. No background, no animation, no reflow of
container mutates the shared MixVisualizerControlState; the backdrop bridge pushes the the row above. The band mutates the shared MixVisualizerControlState; the backdrop
dials. A knob drag does not collapse it — the toggle flips only on the lamp's click. *@ bridge pushes the dials. A knob drag does not toggle it — the lamp's click does. *@
<MixVisualizerControls Expanded="@_controlsExpanded" /> @if (_controlsExpanded)
</TopRowCenter> {
<MixVisualizerControls />
}
</TopContent>
<TopRightAction> <TopRightAction>
@* Lava-lamp button top-right, across from the back link. Toggles the in-flow control @* Lava-lamp button top-right, across from the back link. Toggles the knob band below the
container in the center zone. The icon swaps to its FILLED variant while the row. The icon swaps to its FILLED variant while the band is shown (§7f / Part B). *@
container is expanded (§7f / Part B). *@
<MudTooltip Text="Visualizer settings"> <MudTooltip Text="Visualizer settings">
<MudIconButton Icon="@(_controlsExpanded ? DDIcons.LavaLampFilled : DDIcons.LavaLamp)" <MudIconButton Icon="@(_controlsExpanded ? DDIcons.LavaLampFilled : DDIcons.LavaLamp)"
Size="Size.Large" Size="Size.Large"
@@ -116,9 +118,9 @@ else
@code { @code {
protected override string PersistKey => "mix-detail"; protected override string PersistKey => "mix-detail";
// Lava-lamp inline knob-bar expanded state. Pure presentation over MixVisualizerControlState — the // Lava-lamp knob-band visibility. Pure presentation over MixVisualizerControlState — gates whether
// bar discloses the seven knobs and animates open/closed; toggling it touches no control value or // the seven-knob MixVisualizerControls is rendered into the TopContent band; toggling it touches no
// bridge push. The lava-lamp button's filled/outline glyph is driven off this same flag. // control value or bridge push. The lava-lamp button's filled/outline glyph is driven off this flag.
private bool _controlsExpanded; private bool _controlsExpanded;
private void ToggleSettings() => _controlsExpanded = !_controlsExpanded; private void ToggleSettings() => _controlsExpanded = !_controlsExpanded;