polish(p15): mute panel, revert scroll to knob, waveform icon + strong toggle state

Mute --deepdrft-panel-ground; WAVE scroll MudSlider back to RadialKnob; new DDIcons Waveform/WaveformFilled glyph for the waveform toggle; strong green ON-state chip vs dim OFF; refresh popover pointer-capture comment.
This commit is contained in:
daniel-c-harvey
2026-06-17 18:03:16 -04:00
parent 412b96ba16
commit 6ecc7f1f37
5 changed files with 80 additions and 50 deletions
@@ -15,13 +15,15 @@
mild modal tint (alpha from the single --deepdrft-modal-scrim-alpha token, §10.5). There is therefore
no AnchorOrigin/TransformOrigin: a centered modal has no anchor (Phase 15 drops those parameters).
KNOB-DRAG SAFETY (Phase 15 §4, highest-risk detail): RadialKnob mounts its own full-viewport
position:fixed; z-index:9999 mouse-capture div WHILE dragging (RadialKnob.razor lines 59). That capture
div sits ABOVE the overlay scrim, so a knob drag's pointer-up lands on the capture div, never the scrim
— the overlay's OnClick does not fire mid-drag, so releasing the mouse outside the panel does NOT close
the modal. AutoClose is left OFF (the default) and dismissal is via the explicit scrim OnClick only,
carrying the SharePopover idiom forward under the new primitive. The panel stops click propagation so a
click INSIDE it never bubbles to the scrim's close handler.
KNOB-DRAG SAFETY (Phase 15 §4, highest-risk detail): RadialKnob calls setPointerCapture on its own
knob element when a drag begins, so all pointermove/pointerup/pointercancel events for that pointer are
delivered to the knob element regardless of where the cursor moves. The synthesised click on pointerup
is also retargeted to the knob, not to whatever element is under the cursor — so releasing outside the
panel never fires the scrim's close handler. The full-viewport position:fixed; z-index:9999 div
RadialKnob renders while dragging is a belt-and-suspenders UX guard (blocks stray clicks on underlying
content) but is no longer the load-bearing anti-dismiss mechanism. AutoClose is left OFF (the default)
and dismissal is via the explicit scrim OnClick only. The panel stops click propagation so a click
INSIDE it never bubbles to the scrim's close handler.
The host owns NO control state and NO JS interop. The hosted WaveformVisualizerControls panel mutates
the shared WaveformVisualizerControlState and raises Changed; the visualizer bridge subscribes. This
@@ -3,19 +3,19 @@
@using DeepDrftPublic.Client.Services
@inject WaveformVisualizerControlState ControlState
@* The waveform visualizer control PANEL (Phase 12 §3d-revised → Phase 15 re-layout). The control deck for
the lava-lamp visualizer: a deterministic THREE-ROW, sectioned layout that encodes what the visualizer
composes — a LAVA field and a WAVEFORM ribbon, each independently toggleable (Phase 15 §3):
@* The waveform visualizer control PANEL (Phase 12 §3d-revised → Phase 15 re-layout → Phase 15 polish).
The control deck for the lava-lamp visualizer: a deterministic THREE-ROW, sectioned layout that encodes
what the visualizer composes — a LAVA field and a WAVEFORM ribbon, each independently toggleable (§3):
Row 1 (MODE, always): lava lamp-toggle, waveform lamp-toggle, [collisions knob — only when BOTH on],
then the color knob pinned far-right (applies to the whole field, so always visible).
Row 1 (MODE, always): lava lamp-toggle, waveform toggle (new waveform glyph), [collisions knob — only
when BOTH on], then the color knob pinned far-right (applies to the whole field, always visible).
Row 2 (LAVA, only when lava on): "LAVA:" label + gravity / heat / fluid-amount / fluid-viscosity.
Row 3 (WAVE, only when waveform on): "WAVE:" label + scroll SLIDER + width knob pinned far-right.
Row 3 (WAVE, only when waveform on): "WAVE:" label + scroll RadialKnob + width RadialKnob (far-right).
The two lamp toggles are iconographic (lit LavaLampFilled / unlit LavaLamp glyph), green because they
are INTERACTIVE (the §5 colour principle: green = interactive, light = non-interactive). The eight
continuous dials are unchanged in tuning; the one widget-type change is scroll/zoom → a MudSlider (§8,
bound to ScrollSpeed alone). None of these is a seek surface (read-only contract §D).
The two toggles have STRONG ACTIVE-STATE styling: when ON the toggle chip has a green-accent background
(unmistakably active); when OFF it is muted/dim. The lava toggle keeps the lava-lamp glyph; the waveform
toggle uses a new distinct waveform-bars glyph (DDIcons.Waveform / WaveformFilled). Green = interactive
(§5 colour principle); light = non-interactive. All colours are token-sourced — no hardcoded hex.
This is the PANEL CONTENT hosted inside WaveformVisualizerControlPopover, now a screen-centered tinted
MudOverlay (Phase 15 §4). Because the overlay PORTALS its content out of this component's DOM subtree,
@@ -24,10 +24,6 @@
scoped .razor.css carries only the legacy inline-bar fallback (Mix's old non-portaled mount), which may
now be dead post-Phase-12 but is left in place — flagged, not cut (out of Phase 15 scope).
COLOUR PRINCIPLE (§5): the lamp toggles + knob arcs/pointers + the slider track/thumb are green-accent
(interactive); the "LAVA:"/"WAVE:" section labels and the knob caption icons are LIGHT (static). All
colours are token-sourced (deepdrft-tokens.css) — no hardcoded hex.
It owns NO JS interop: it mutates the shared, session-scoped WaveformVisualizerControlState and raises
its Changed event. The visualizer bridge (WaveformVisualizer) subscribes and pushes the affected dial /
subsystem-enable to the WebGL module. RadialKnob has no icon slot (its Label renders as SVG text) and no
@@ -43,7 +39,7 @@
<div class="wvc-row-left">
<MudTooltip Text="Light the lamp — or let it go cold.">
<div class="wvc-toggle" role="group" aria-label="Lava field on or off">
<div class="wvc-toggle @(ControlState.LavaEnabled ? "wvc-toggle-on" : "wvc-toggle-off")" role="group" aria-label="Lava field on or off">
<MudIconButton Icon="@(ControlState.LavaEnabled ? DDIcons.LavaLampFilled : DDIcons.LavaLamp)"
Color="Color.Primary"
OnClick="@ToggleLava"
@@ -53,8 +49,8 @@
</MudTooltip>
<MudTooltip Text="Show the sound, or hide the ribbon.">
<div class="wvc-toggle" role="group" aria-label="Waveform ribbon on or off">
<MudIconButton Icon="@(ControlState.WaveformEnabled ? DDIcons.LavaLampFilled : DDIcons.LavaLamp)"
<div class="wvc-toggle @(ControlState.WaveformEnabled ? "wvc-toggle-on" : "wvc-toggle-off")" role="group" aria-label="Waveform ribbon on or off">
<MudIconButton Icon="@(ControlState.WaveformEnabled ? DDIcons.WaveformFilled : DDIcons.Waveform)"
Color="Color.Primary"
OnClick="@ToggleWaveform"
aria-label="Waveform ribbon on or off"
@@ -145,19 +141,20 @@
</div>
}
@* ── Row 3 — WAVE section (only when waveform on). Scroll is a SLIDER (§8); width pinned right. ── *@
@* ── Row 3 — WAVE section (only when waveform on). Both controls are RadialKnobs (scroll reverted
from MudSlider per Phase 15 polish); width pinned far-right via wvc-row-wave space-between. ── *@
@if (ControlState.WaveformEnabled)
{
<div class="wvc-row wvc-row-section wvc-row-wave">
<span class="wvc-section-label">WAVE:</span>
<MudTooltip Text="How fast the sound rolls by.">
<div class="wvc-slider" role="group" aria-label="Waveform scroll speed">
<MudSlider T="double"
Value="@ControlState.ScrollSpeed"
ValueChanged="@OnScrollSpeedChanged"
Min="0" Max="1" Step="0.001"
Color="Color.Primary" />
<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="waveform-visualizer-control-icon mix-visualizer-control-icon" />
</div>
</MudTooltip>