feat(p12-w4): ambient visualizer slot on scaffold + popover controls on all detail hosts

Add optional Ambient slot to ReleaseDetailScaffold (full-bleed layer behind content; absent = no regression). Cut mounts it + popover; Session mounts the engine directly behind its hero; Mix swaps its inline knob-bar for the lava-lamp popover.
This commit is contained in:
daniel-c-harvey
2026-06-17 12:11:03 -04:00
parent 9009f2c8cf
commit 955182d6da
7 changed files with 87 additions and 32 deletions
@@ -43,6 +43,21 @@ else
BackHref="/cuts"
BackLabel="All cuts"
ShowShareRow="false">
<Ambient>
@* Ambient living waveform behind the album hero + track list (Phase 12 §3c/§3f mode B).
Cut is multi-track: anchor to the release's EntryKey and default to the first track by
TrackNumber. The bridge follows the live playing track within the release automatically
(keys on TrackId match OR shared ReleaseEntryKey), so the field re-renders to whichever
track the listener starts; TrackEntryKey is the at-rest datum before playback. *@
<WaveformVisualizer ReleaseEntryKey="@release.EntryKey"
TrackId="@firstTrack?.Id"
TrackEntryKey="@firstTrack?.EntryKey" />
</Ambient>
<TopRightAction>
@* Lava-lamp icon → popover panel (full parity, §3d-revised). Sits top-right across from the
back link, clear of the header's own Play/Share affordances below. *@
<WaveformVisualizerControlPopover />
</TopRightAction>
<Header>
@* Header split: meta + Play/Share on the LEFT, bordered cover on the RIGHT (spec §3.1). *@
<div class="cut-detail-header">
+5 -27
View File
@@ -55,27 +55,12 @@ else
ShowHeader="false"
ShowMeta="false"
ShowShareRow="false">
<TopContent>
@* The eight-knob band lives in its own full-width area below the back/lamp top row.
Phase 10 §4: the control is ALWAYS rendered; the lava-lamp toggle feeds its Visible
parameter, and the control itself @if-gates the knobs while holding the container's
reserved height — so content below never pops on toggle. The band mutates the shared
WaveformVisualizerControlState; the backdrop bridge pushes the dials. A knob drag does not
toggle it — the lamp's click does. *@
<WaveformVisualizerControls Visible="@_controlsExpanded" />
</TopContent>
<TopRightAction>
@* 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"
Color="Color.Secondary"
Disabled="@(!RendererInfo.IsInteractive)"
OnClick="@ToggleSettings"
aria-label="Visualizer settings"
aria-expanded="@_controlsExpanded" />
</MudTooltip>
@* Lava-lamp icon → popover panel, top-right across from the back link (Phase 12
§3d-revised). Replaces the former inline TopContent knob-bar: the icon IS the toggle
and the popover IS the panel. Mix takes the cleanest anchor case (§8e) — the popover's
default bottom-right anchor opens down over the full-bleed field. *@
<WaveformVisualizerControlPopover />
</TopRightAction>
<Hero>
@* Cover-as-background hero with all metadata overlaid, square `mix-hero` sizing. The
@@ -128,11 +113,4 @@ else
await PlayerService.SelectTrackStreaming(track);
}
}
// Lava-lamp knob-band visibility. Pure presentation over WaveformVisualizerControlState — gates whether
// the seven-knob WaveformVisualizerControls 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;
}
@@ -40,11 +40,26 @@ else
// Hero image precedence: the session's dedicated hero, then the release cover, then a placeholder.
var heroImage = !string.IsNullOrEmpty(heroKey) ? heroKey : release.ImagePath;
<MudContainer MaxWidth="MaxWidth.Large" Class="session-detail-page">
@* Ambient living waveform behind the hero overlay (Phase 12 §3e option b / §3f mode B). Session does
NOT compose ReleaseDetailScaffold, so it mounts the shared engine directly with its own thin
full-bleed wrapper — the engine is single-source either way, only the mount differs (§3b). The
visualizer positions itself fixed/inset:0; the session-detail-foreground class lifts the content
above it. The bridge follows the live playing track; TrackEntryKey is the at-rest datum. *@
<WaveformVisualizer ReleaseEntryKey="@release.EntryKey"
TrackId="@ViewModel.Track?.Id"
TrackEntryKey="@ViewModel.Track?.EntryKey" />
<MudLink Href="/sessions" Typo="Typo.body2" Class="deepdrft-track-detail-back">
&larr; All sessions
</MudLink>
<MudContainer MaxWidth="MaxWidth.Large" Class="session-detail-page session-detail-foreground">
<div class="session-detail-top-row">
<MudLink Href="/sessions" Typo="Typo.body2" Class="deepdrft-track-detail-back">
&larr; All sessions
</MudLink>
@* Lava-lamp icon → popover panel (full parity, §3e/§3d-revised). Anchored top-right, clear of
the hero overlay and the share/play affordances overlaid on the hero below. *@
<WaveformVisualizerControlPopover />
</div>
@* The overlay shows the cover thumbnail only when it differs from the resolved hero image —
when there is no dedicated hero, heroImage already falls back to release.ImagePath, so the
@@ -6,3 +6,20 @@
padding-top: 2rem;
padding-bottom: 4rem;
}
/* Lifts the session content above the fixed full-bleed waveform layer (z-index: 0). Session mounts the
visualizer directly (it does not compose ReleaseDetailScaffold), so the foreground stacking context
lives here rather than on the scaffold (Phase 12 §3e option b). The class lands on the MudContainer's
rendered root, so ::deep is required to reach it. */
::deep .session-detail-foreground {
position: relative;
z-index: 1;
}
/* Back link (left) | lava-lamp popover trigger (right) on one row, mirroring the scaffold's top row.
The popover icon clears the hero overlay below and the share/play affordances overlaid on it. */
.session-detail-top-row {
display: flex;
align-items: center;
justify-content: space-between;
}