@namespace DeepDrftPublic.Client.Controls @using DeepDrftShared.Client.Common @using DeepDrftPublic.Client.Services @inject WaveformVisualizerControlState ControlState @* 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 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 RadialKnob + width RadialKnob (far-right). 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, Blazor CSS isolation does not reach the rendered panel — so panel chrome AND the row/section layout live in the GLOBAL deepdrft-styles.css (.waveform-visualizer-control-panel*), not the scoped .razor.css. The 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). 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 aria capture, so each control's Material icon rides beside its knob as a caption and the accessible name rides on the wrapping group div (§7); the playful MudTooltip rides alongside for sighted hover. *@
@if (Visible) { @* ── Row 1 — MODE (always visible). Toggles + collisions group left; color pinned right. ── *@
@* Collisions are the interaction BETWEEN the two subsystems — meaningless with only one present, so visible only when BOTH are on (§3 truth table). *@ @if (ControlState.LavaEnabled && ControlState.WaveformEnabled) {
} @*
*@ @* Color applies to the whole field regardless of which subsystems are on, so it is pinned far-right of row 1 and never reflows when collisions hides (§3). *@
@* ── Row 2 — 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) {
WAVE:
} @* ── Row 3 — LAVA section (only when lava on). ── *@ @if (ControlState.LavaEnabled) {
LAVA:
} } @code { /// /// Whether the control deck is shown. The overlay host shows the panel whenever it is open, so the /// default is true. Mix's legacy inline mount (if it survives) still feeds its lava-lamp toggle /// into this — that mount always renders the component, and THIS component decides deck visibility /// (Phase 10 §4): when false the rows are @if-gated out but the container holds its reserved height /// (CSS min-height) so content below the inline bar never pops. Inside the overlay the host owns /// open/closed, so the default keeps the panel populated. /// [Parameter] public bool Visible { get; set; } = true; /// /// When true, applies the waveform-visualizer-control-panel class to the root element, /// enabling the global panel-chrome rules (NowPlayingCard chrome — square corners, lighter-navy /// ground, thin light border — plus the row/section layout and pinned palette tokens). Set by /// ; Mix's inline mount leaves this false so the /// chrome never leaks onto the inline bar. /// [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 / subsystem-enable. All dial 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. The two // toggles flip a boolean (no value), driving the genuine per-subsystem draw-skip in the module (§6). private void ToggleLava() { ControlState.LavaEnabled = !ControlState.LavaEnabled; ControlState.NotifyChanged(); } private void ToggleWaveform() { ControlState.WaveformEnabled = !ControlState.WaveformEnabled; ControlState.NotifyChanged(); } private void OnScrollSpeedChanged(double value) { ControlState.ScrollSpeed = value; ControlState.NotifyChanged(); } private void OnGradientRotationSpeedChanged(double value) { ControlState.GradientRotationSpeed = value; ControlState.NotifyChanged(); } private void OnLavaGravityChanged(double value) { ControlState.LavaGravity = value; ControlState.NotifyChanged(); } private void OnLavaHeatChanged(double value) { ControlState.LavaHeat = value; ControlState.NotifyChanged(); } private void OnFluidAmountChanged(double value) { ControlState.FluidAmount = value; ControlState.NotifyChanged(); } private void OnFluidViscosityChanged(double value) { ControlState.FluidViscosity = value; ControlState.NotifyChanged(); } private void OnCollisionStrengthChanged(double value) { ControlState.CollisionStrength = value; ControlState.NotifyChanged(); } private void OnWaveformWidthChanged(double value) { ControlState.WaveformWidth = value; ControlState.NotifyChanged(); } }