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
@inject MixVisualizerControlState ControlState
@* The Mix visualizer controls (lava reframe Wave R4). SEVEN continuous RadialKnobs — scroll speed,
gradient rotation speed, lava gravity, lava heat, blob density, collision strength, waveform width —
each its own dedicated control with a Material-icon caption (no more R2 temp-remapping: no knob
caption misrepresents its function). The bar lives INLINE in the mix-detail controls area and
ANIMATES open/closed in place via CSS transition off the @Expanded flag — it reads as the controls
collapsing/expanding, NOT a floating popover/drawer (§7b).
@* The Mix visualizer controls. SEVEN continuous RadialKnobs — scroll speed, gradient rotation speed,
lava gravity, lava heat, blob density, collision strength, waveform width — each its own dedicated
control with a Material-icon caption. Visibility is controlled by Blazor, not CSS: the host page
renders this component only while the lava-lamp toggle is on (@if-guarded), so when off it is not in
the DOM at all. There is no collapse/expand animation and no glass surface — the knobs simply appear
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
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
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
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
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-controls-bar">
<div class="mix-visualizer-control" role="group" aria-label="Waveform scroll speed">
<RadialKnob Value="@ControlState.ScrollSpeed"
@@ -91,12 +87,6 @@
</div>
@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
// 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.
@@ -1,63 +1,14 @@
/* The seven-knob container lives IN-FLOW in the scaffold's top-row center zone, between the back link
and the lava-lamp toggle, and grows open/closed in place (lava reframe §7b) — NOT a popover, drawer,
or floating overlay. Collapsed, it has zero footprint and is fully transparent; the @Expanded flag
(mirrored to the .is-expanded class) transitions it open by growing its width. The PRIMARY animated
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. */
/* The seven-knob band. Blazor gates its presence (the host renders this component only while the
lava-lamp is on), so this is purely a layout rule for the visible state — no collapse machinery, no
transitions, no glass surface. A plain transparent horizontal flex row of the seven knobs that wraps
to a second line only if the band is genuinely too narrow. */
.mix-visualizer-controls-bar {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: flex-start;
justify-content: center;
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;
}
margin: 0.5rem 0;
}
/* 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">
@* Three-zone top row: back link (left) | optional center affordance | optional action (right), on
one SpaceBetween row. Both the center and action slots stay null for media that don't supply them
(Track/Cut/Session), so SpaceBetween collapses to the back link alone at the left edge. The Mix
detail page fills the center with its in-flow visualizer-controls container (§7b). The native
wrapper lets the row flex-wrap the center zone to its own line on narrow widths, keeping the
controls in-flow rather than clipping. *@
@* Two-end top row: back link (left) | optional action (right), on one SpaceBetween row. The action
slot stays null for media that don't supply it (Track/Cut/Session), so SpaceBetween collapses to
the back link alone at the left edge. The Mix detail page fills the action with its lava-lamp
toggle and renders its knob band below via TopContent. *@
<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">
&larr; @BackLabel
</MudLink>
@TopRowCenter
@TopRightAction
</MudStack>
</div>
@@ -25,21 +25,13 @@ public partial class ReleaseDetailScaffold : ComponentBase
[Parameter] public string BackLabel { get; set; } = "Archive";
/// <summary>
/// Optional medium-specific content rendered between the back link and the masthead — the "below
/// the back button, above the details" band. The Mix detail page uses it for the visualizer
/// controls row; other media leave it null.
/// Optional medium-specific content rendered as its own full-width band between the back/action top
/// row and the masthead — the "below the back button, above the details" band. The Mix detail page
/// uses it for the visualizer knob row (gated by Blazor on the lava-lamp toggle); other media leave
/// it null.
/// </summary>
[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>
/// 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
@@ -3,12 +3,3 @@
justify-content: center;
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"
ShowMeta="@(hasGenre || hasDate)"
ShowShareRow="false">
<TopRowCenter>
@* In-flow seven-knob control container, between the back link and the lava-lamp toggle.
It grows in place (width/opacity transition) when expanded and collapses to zero
footprint when closed — never a popover, drawer, or floating overlay (§7b). The
container mutates the shared MixVisualizerControlState; the backdrop bridge pushes the
dials. A knob drag does not collapse it — the toggle flips only on the lamp's click. *@
<MixVisualizerControls Expanded="@_controlsExpanded" />
</TopRowCenter>
<TopContent>
@* The seven-knob band lives in its own full-width area below the back/lamp top row.
Blazor — not CSS — controls its visibility: it is rendered only while the lava-lamp is
on, so when off it is not in the DOM at all. No background, no animation, no reflow of
the row above. The band mutates the shared MixVisualizerControlState; the backdrop
bridge pushes the dials. A knob drag does not toggle it — the lamp's click does. *@
@if (_controlsExpanded)
{
<MixVisualizerControls />
}
</TopContent>
<TopRightAction>
@* Lava-lamp button top-right, across from the back link. Toggles the in-flow control
container in the center zone. The icon swaps to its FILLED variant while the
container is expanded (§7f / Part B). *@
@* Lava-lamp button top-right, across from the back link. Toggles the knob band below the
row. The icon swaps to its FILLED variant while the band is shown (§7f / Part B). *@
<MudTooltip Text="Visualizer settings">
<MudIconButton Icon="@(_controlsExpanded ? DDIcons.LavaLampFilled : DDIcons.LavaLamp)"
Size="Size.Large"
@@ -116,9 +118,9 @@ else
@code {
protected override string PersistKey => "mix-detail";
// Lava-lamp inline knob-bar expanded state. Pure presentation over MixVisualizerControlState — the
// bar discloses the seven knobs and animates open/closed; toggling it touches no control value or
// bridge push. The lava-lamp button's filled/outline glyph is driven off this same flag.
// Lava-lamp knob-band visibility. Pure presentation over MixVisualizerControlState — gates whether
// the seven-knob MixVisualizerControls is rendered into the TopContent band; toggling it touches no
// control value or bridge push. The lava-lamp button's filled/outline glyph is driven off this flag.
private bool _controlsExpanded;
private void ToggleSettings() => _controlsExpanded = !_controlsExpanded;