Merge p12-w4-t1-ambient-slot into dev (12.C: ambient visualizer slot on scaffold + popover controls on all detail hosts)

This commit is contained in:
daniel-c-harvey
2026-06-17 12:23:34 -04:00
7 changed files with 87 additions and 32 deletions
@@ -5,7 +5,17 @@
hero visual and metadata block. The Cut/Session/Mix detail pages all compose this;
per-medium variance rides the Hero and MetaContent render fragments. *@
<div class="deepdrft-track-detail-container">
@* Ambient environment layer (Phase 12 §3c/§3f mode B): an optional full-bleed layer rendered BEHIND
the scaffold content. The visualizer mounted here positions itself fixed/inset:0 (its own CSS), so
this slot's only job is to render it before the content and put the content into a foreground
stacking context above it. Absent slot = no ambient layer and no foreground promotion → today's
plain background, byte-for-byte (Liskov). *@
@if (Ambient is not null)
{
@Ambient
}
<div class="deepdrft-track-detail-container @(Ambient is not null ? "deepdrft-track-detail-foreground" : null)">
@* 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
@@ -51,6 +51,16 @@ public partial class ReleaseDetailScaffold : ComponentBase
/// <summary>Medium-specific hero visual (cover art, hero image, or waveform background).</summary>
[Parameter] public RenderFragment? Hero { get; set; }
/// <summary>
/// Optional full-bleed ambient layer rendered BEHIND the scaffold content (Phase 12 §3c/§3f mode B).
/// A host that wants a living environment behind hero+content — e.g. Cut supplying a
/// <c>WaveformVisualizer</c> — places it here. The mounted layer positions itself fixed/inset:0
/// (its own CSS), so the scaffold only promotes its content into a foreground stacking context above
/// it. Absent = today's plain background, no regression (Liskov). Mode A (Mix) and mode C (the
/// NowPlaying card) mount the engine without this slot — see §3f.
/// </summary>
[Parameter] public RenderFragment? Ambient { get; set; }
/// <summary>
/// Optional body region rendered below the meta block — the Cut album's multi-track listing.
/// Single-track media leave it null.
@@ -3,3 +3,13 @@
justify-content: center;
margin-top: 1.5rem;
}
/* Foreground stacking context — applied only when an Ambient layer is present. Lifts the scaffold
content above the fixed full-bleed visualizer (z-index: 0) so hero + meta + body render over the
living waveform field (Phase 12 §3c — promotes the former Mix-bespoke .mix-detail-foreground into
the shared scaffold). Without an Ambient slot this class is absent and the container keeps its
default flow, so a slot-less host renders exactly as before. */
.deepdrft-track-detail-foreground {
position: relative;
z-index: 1;
}
@@ -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;
}