Merge p12-w2-t2-popover-panel into dev (12.E: popover-hosted waveform control panel)
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
@namespace DeepDrftPublic.Client.Controls
|
||||
@using DeepDrftShared.Client.Common
|
||||
|
||||
@* The single controls affordance, placed by an icon anywhere (Phase 12 §3d-revised). Closed state is
|
||||
just the lava-lamp icon; clicking it floats the eight-knob WaveformVisualizerControls panel over the
|
||||
surface. One panel, one popover host, reused on every host (Mix, Cut, Session, NowPlaying card) — the
|
||||
SOLID seam: variance is the per-host anchor (§8e), never a forked popover.
|
||||
|
||||
Anchoring follows the SharePopover precedent: Fixed so the panel reads the trigger's bounding rect
|
||||
rather than fighting CSS container tricks. AnchorOrigin/TransformOrigin are per-host
|
||||
parameters (§8e) defaulted to bottom-right open-down — the cleanest case (Mix's TopRightAction corner);
|
||||
tight hosts (the NowPlaying card) override to open away from the card body.
|
||||
|
||||
The popover owns NO control state and NO JS interop. The hosted WaveformVisualizerControls panel mutates
|
||||
the shared WaveformVisualizerControlState and raises Changed; the visualizer bridge subscribes. This host
|
||||
only toggles open/closed and places the panel — it stays purely presentational. *@
|
||||
|
||||
@* Backdrop dismissal mirrors SharePopover: a viewport overlay closes on outside click. AutoClose stays
|
||||
off so a knob drag (which can land pointer-up outside the panel's DOM subtree) does not dismiss. *@
|
||||
<MudOverlay Visible="@_open" OnClick="@Close" />
|
||||
|
||||
@* Activator and popover share this wrapper so MudPopover anchors off the trigger's bounding rect. *@
|
||||
<div>
|
||||
<MudTooltip Text="Visualizer settings">
|
||||
<MudIconButton Icon="@(_open ? DDIcons.LavaLampFilled : DDIcons.LavaLamp)"
|
||||
Size="@IconSize"
|
||||
Color="Color.Secondary"
|
||||
Disabled="@(!RendererInfo.IsInteractive)"
|
||||
OnClick="@Toggle"
|
||||
aria-label="Visualizer settings"
|
||||
aria-expanded="@_open" />
|
||||
</MudTooltip>
|
||||
|
||||
<MudPopover Open="@_open"
|
||||
Fixed="true"
|
||||
AnchorOrigin="@AnchorOrigin"
|
||||
TransformOrigin="@TransformOrigin"
|
||||
Class="waveform-visualizer-control-popover">
|
||||
<WaveformVisualizerControls PanelChrome="true" />
|
||||
</MudPopover>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Where the panel anchors relative to the trigger icon (§8e). Defaults to opening down-from the
|
||||
/// icon's bottom-right — fits Mix's top-right corner and the ambient hosts. Tight hosts (the
|
||||
/// NowPlaying card) override to open away from the card body.
|
||||
/// </summary>
|
||||
[Parameter] public Origin AnchorOrigin { get; set; } = Origin.BottomRight;
|
||||
|
||||
/// <summary>The panel's own corner that pins to <see cref="AnchorOrigin"/> (§8e).</summary>
|
||||
[Parameter] public Origin TransformOrigin { get; set; } = Origin.TopRight;
|
||||
|
||||
/// <summary>Trigger-icon size. Defaults Large to match the Phase 10 Mix lava-lamp button.</summary>
|
||||
[Parameter] public Size IconSize { get; set; } = Size.Large;
|
||||
|
||||
private bool _open;
|
||||
|
||||
private void Toggle() => _open = !_open;
|
||||
|
||||
private void Close() => _open = false;
|
||||
}
|
||||
@@ -2,102 +2,109 @@
|
||||
@using DeepDrftPublic.Client.Services
|
||||
@inject WaveformVisualizerControlState ControlState
|
||||
|
||||
@* The waveform visualizer controls. EIGHT continuous RadialKnobs — scroll speed, gradient rotation speed,
|
||||
lava gravity, lava heat, fluid amount, fluid viscosity, collision strength, waveform width — each its
|
||||
own dedicated control with a Material-icon caption. The single "bubbles" knob is split into
|
||||
fluid-amount + fluid-viscosity (Phase 10 §5).
|
||||
@* The waveform visualizer control PANEL (Phase 12 §3d-revised). EIGHT continuous RadialKnobs — scroll
|
||||
speed, gradient rotation speed, lava gravity, lava heat, fluid amount, fluid viscosity, collision
|
||||
strength, waveform width — each its own dedicated control with a Material-icon caption. The single
|
||||
"bubbles" knob is split into fluid-amount + fluid-viscosity (Phase 10 §5).
|
||||
|
||||
Visibility (Phase 10 §4): the host ALWAYS renders this component now and feeds the lava-lamp toggle
|
||||
into the @Visible parameter. THIS component decides knob visibility — it @if-gates the knobs but keeps
|
||||
the container's reserved size, so the content below the controls bar never pops when the lamp toggles.
|
||||
The gating is Blazor @if (matching the established "@if-gated knob band, no CSS hide/glass/animation"
|
||||
convention) — the knobs are simply not rendered when hidden, while a min-height container holds the
|
||||
layout. No collapse animation, no glass surface, no CSS visibility-hiding of populated knobs.
|
||||
This component is the PANEL CONTENT hosted inside WaveformVisualizerControlPopover. It no longer rides
|
||||
an inline TopRowCenter bar; it lays out as a wrapped grid sized for a popover, styled to the NowPlaying
|
||||
Hero look (§3g — dark-navy ground, green-accent knobs, light icons) from the deepdrft-* tokens.
|
||||
|
||||
It owns NO JS interop: it mutates the shared, session-scoped WaveformVisualizerControlState and raises its
|
||||
Changed event. The backdrop bridge (WaveformVisualizer) subscribes to that event and pushes the
|
||||
affected dial 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 (read-only contract §D).
|
||||
Because MudPopover PORTALS its content out of this component's DOM subtree, Blazor CSS isolation does
|
||||
not reach the rendered panel — so panel chrome lives in the GLOBAL deepdrft-styles.css
|
||||
(.waveform-visualizer-control-panel*), not in the scoped .razor.css. The scoped .razor.css carries only
|
||||
the inline-bar fallback (the .mix-visualizer-controls-bar reserved-height row) Mix's existing
|
||||
TopRowCenter mount still uses, which is NOT portaled and so resolves under isolation.
|
||||
|
||||
Visibility: the popover host always shows the panel when open (Visible defaults true). Mix's legacy
|
||||
inline mount still feeds its lava-lamp toggle into Visible (Phase 10 §4): the knobs @if-gate while the
|
||||
container holds a reserved min-height so content below never pops on toggle.
|
||||
|
||||
It owns NO JS interop: it mutates the shared, session-scoped WaveformVisualizerControlState and raises
|
||||
its Changed event. The visualizer bridge (WaveformVisualizer) subscribes to that event and pushes the
|
||||
affected dial 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 (read-only contract §D).
|
||||
|
||||
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. *@
|
||||
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. *@
|
||||
|
||||
<div class="mix-visualizer-controls-bar">
|
||||
<div class="@($"{_panelChromeClass} mix-visualizer-controls-bar".TrimStart())">
|
||||
|
||||
@if (Visible)
|
||||
{
|
||||
<div class="mix-visualizer-control" role="group" aria-label="Waveform scroll speed">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Waveform scroll speed">
|
||||
<RadialKnob Value="@ControlState.ScrollSpeed"
|
||||
ValueChanged="@OnScrollSpeedChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Speed" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Speed" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
|
||||
<div class="mix-visualizer-control" role="group" aria-label="Color gradient rotation speed">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Color gradient rotation speed">
|
||||
<RadialKnob Value="@ControlState.GradientRotationSpeed"
|
||||
ValueChanged="@OnGradientRotationSpeedChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Palette" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Palette" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
|
||||
<div class="mix-visualizer-control" role="group" aria-label="Lava gravity">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Lava gravity">
|
||||
<RadialKnob Value="@ControlState.LavaGravity"
|
||||
ValueChanged="@OnLavaGravityChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowDownward" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowDownward" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
|
||||
<div class="mix-visualizer-control" role="group" aria-label="Lava heat">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Lava heat">
|
||||
<RadialKnob Value="@ControlState.LavaHeat"
|
||||
ValueChanged="@OnLavaHeatChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocalFireDepartment" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocalFireDepartment" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
|
||||
<div class="mix-visualizer-control" role="group" aria-label="Fluid amount">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Fluid amount">
|
||||
<RadialKnob Value="@ControlState.FluidAmount"
|
||||
ValueChanged="@OnFluidAmountChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.BubbleChart" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.BubbleChart" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
|
||||
<div class="mix-visualizer-control" role="group" aria-label="Fluid viscosity">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Fluid viscosity">
|
||||
<RadialKnob Value="@ControlState.FluidViscosity"
|
||||
ValueChanged="@OnFluidViscosityChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Opacity" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Opacity" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
|
||||
<div class="mix-visualizer-control" role="group" aria-label="Collision strength">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Collision strength">
|
||||
<RadialKnob Value="@ControlState.CollisionStrength"
|
||||
ValueChanged="@OnCollisionStrengthChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Compress" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Compress" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
|
||||
<div class="mix-visualizer-control" role="group" aria-label="Waveform width">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Waveform width">
|
||||
<RadialKnob Value="@ControlState.WaveformWidth"
|
||||
ValueChanged="@OnWaveformWidthChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.SettingsEthernet" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.SettingsEthernet" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -105,12 +112,24 @@
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Whether the knob band is shown. The host wires its lava-lamp toggle straight into this — the host
|
||||
/// always renders this component, and THIS component decides knob visibility (Phase 10 §4). When
|
||||
/// Whether the knob band is shown. The popover host shows the panel whenever it is open, so the
|
||||
/// default is <c>true</c>. Mix's legacy inline mount still feeds its lava-lamp toggle into this — that
|
||||
/// mount always renders the component, and THIS component decides knob visibility (Phase 10 §4): when
|
||||
/// false the knobs are @if-gated out but the container holds its reserved height (CSS min-height), so
|
||||
/// content below the bar never pops as the lamp toggles.
|
||||
/// content below the inline bar never pops as the lamp toggles. Inside the popover the host owns
|
||||
/// open/closed, so the default keeps the panel populated.
|
||||
/// </summary>
|
||||
[Parameter] public bool Visible { get; set; }
|
||||
[Parameter] public bool Visible { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c>, applies the <c>waveform-visualizer-control-panel</c> class to the root element,
|
||||
/// enabling the global panel-chrome rules (dark-navy ground, border, max-width cap, pinned palette
|
||||
/// tokens). Set by <see cref="WaveformVisualizerControlPopover"/>; Mix's inline mount leaves this
|
||||
/// <c>false</c> so the chrome never leaks onto the inline bar.
|
||||
/// </summary>
|
||||
[Parameter] public bool PanelChrome { get; set; } = false;
|
||||
|
||||
private string _panelChromeClass => PanelChrome ? "waveform-visualizer-control-panel" : string.Empty;
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
/* The eight-knob band. Phase 10 §4: the host ALWAYS renders this component and the component @if-gates
|
||||
the knobs on its Visible parameter. So the container is permanent and reserves its height whether or
|
||||
not the knobs are present — content below the bar never pops on toggle. No collapse machinery, no
|
||||
transitions, no glass surface. A plain transparent horizontal flex row of the eight knobs that wraps
|
||||
to a second line only if the band is genuinely too narrow.
|
||||
/* SCOPED fallback for the LEGACY inline mount only (Mix's TopRowCenter bar). The popover-hosted panel's
|
||||
chrome lives in the GLOBAL deepdrft-styles.css (.waveform-visualizer-control-panel*) because MudPopover
|
||||
portals its content out of this component's DOM subtree, where Blazor CSS isolation cannot reach. These
|
||||
scoped rules apply only when the panel is mounted inline (not portaled) — i.e. Mix's existing bar.
|
||||
|
||||
min-height reserves one knob-row's worth of space (knob Size=64 + icon caption + gaps + margins) so
|
||||
the empty (hidden) state occupies the same vertical box the populated single-row state does. On very
|
||||
narrow viewports a populated band may wrap to a second row and exceed this floor — the no-pop
|
||||
guarantee is exact for the common single-row (desktop) layout. */
|
||||
Phase 10 §4: the inline host ALWAYS renders this component and the component @if-gates the knobs on its
|
||||
Visible parameter. So the container is permanent and reserves its height whether or not the knobs are
|
||||
present — content below the bar never pops on toggle. A plain transparent horizontal flex row of the
|
||||
eight knobs that wraps to a second line only if the band is genuinely too narrow.
|
||||
|
||||
min-height reserves one knob-row's worth of space (knob Size=64 + icon caption + gaps + margins) so the
|
||||
empty (hidden) state occupies the same vertical box the populated single-row state does. The
|
||||
popover-panel rule in the global sheet overrides this min-height (a popover does not reserve height). */
|
||||
.mix-visualizer-controls-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -19,7 +22,7 @@
|
||||
}
|
||||
|
||||
/* One control: a RadialKnob with its Material icon caption underneath. RadialKnob has no icon slot, so
|
||||
the icon rides adjacent (§7d). Center the pair so the seven read as a tidy bar. */
|
||||
the icon rides adjacent (§7d). Center the pair so the eight read as a tidy bar. */
|
||||
.mix-visualizer-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -28,8 +31,9 @@
|
||||
}
|
||||
|
||||
/* 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. Tinted to the secondary accent and the
|
||||
overlay-label opacity so it matches the session-hero NowPlaying captions (§7e). */
|
||||
attribute onto its element — reach it with ::deep. Tinted to the primary accent and the overlay-label
|
||||
opacity so it matches the session-hero NowPlaying captions (§7e). The portaled popover panel tints the
|
||||
same icons via the global sheet instead. */
|
||||
.mix-visualizer-control ::deep .mix-visualizer-control-icon {
|
||||
color: var(--mud-palette-primary);
|
||||
opacity: 0.78;
|
||||
|
||||
@@ -375,6 +375,50 @@ h2, h3, h4, h5, h6,
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
WAVEFORM VISUALIZER CONTROL PANEL (Phase 12 §3d-revised / §3g)
|
||||
The eight-knob panel hosted inside WaveformVisualizerControlPopover. MudPopover
|
||||
PORTALS its content out of the component's DOM subtree, so Blazor CSS isolation
|
||||
never reaches the rendered panel — its chrome must live here in the global sheet,
|
||||
not in the scoped WaveformVisualizerControls.razor.css. (The scoped file keeps only
|
||||
the inline-bar fallback Mix's legacy TopRowCenter mount uses, which is not portaled.)
|
||||
|
||||
The waveform-visualizer-control-panel class is applied ONLY when the component's
|
||||
PanelChrome="true" parameter is set — which WaveformVisualizerControlPopover does
|
||||
and Mix's inline mount does NOT. This prevents the chrome from leaking onto Mix's
|
||||
inline controls bar.
|
||||
|
||||
The NowPlaying Hero look (§3g): dark-navy ground, green-accent knobs, light icons,
|
||||
muted-navy filler — all from the deepdrft-* token source of truth, no hardcoded hex.
|
||||
The RadialKnob reads --mud-palette-* for its arc/track/center/label colours; we pin
|
||||
those palette vars to the Hero tokens ON THE PANEL so the panel reads the same
|
||||
navy/green/off-white regardless of the page's light/dark theme.
|
||||
============================================================================= */
|
||||
.waveform-visualizer-control-panel.mix-visualizer-controls-bar {
|
||||
/* Dark-navy elevated panel ground (§3g: navy-mid for the elevated surface). */
|
||||
background: var(--deepdrft-navy-mid);
|
||||
border: 1px solid var(--deepdrft-border-green);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
/* Popover panel: cap width so eight 64px knobs wrap to a tidy grid rather than one long bar.
|
||||
This OVERRIDES the inline-bar min-height reserve (which only matters for Mix's non-popover mount). */
|
||||
min-height: 0;
|
||||
max-width: 340px;
|
||||
/* Pin the MudBlazor palette vars the portaled RadialKnob consumes to the Hero tokens. */
|
||||
--mud-palette-primary: var(--deepdrft-green-accent); /* knob value arc / pointer / center stroke */
|
||||
--mud-palette-surface: var(--deepdrft-navy); /* knob center fill — darkest navy reads against the panel */
|
||||
--mud-palette-surface-variant: var(--deepdrft-muted); /* knob background track — muted-navy filler (§3g) */
|
||||
--mud-palette-text-primary: var(--deepdrft-white); /* knob value label — light (§3g) */
|
||||
}
|
||||
|
||||
/* Green-accent caption icons (§3g: light/green icons). MudIcon is portaled here too, so this is a
|
||||
plain global descendant selector — no ::deep, no scope attribute (CSS isolation does not reach
|
||||
inside the popover). */
|
||||
.waveform-visualizer-control-panel .waveform-visualizer-control-icon {
|
||||
color: var(--deepdrft-green-accent);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
@media (max-width: 419.98px) {
|
||||
.deepdrft-track-detail-meta {
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user