docs(phase-15): spec visualizer controls enhancements (modal popover, sectioned layout, lava/waveform toggles)
This commit is contained in:
@@ -239,6 +239,26 @@ Sequenced as **eight waves**; the critical path is `11.A → 11.B → 11.C → 1
|
||||
|
||||
---
|
||||
|
||||
## Phase 15 — Visualizer Controls Enhancements
|
||||
|
||||
A presentation + interaction rework of the **waveform visualizer control surface** — the eight-RadialKnob panel (Phase 12) hosted by `WaveformVisualizerControlPopover`. **Not** a renderer change: the WebGL2 visualizer, the eight continuous dial values + their defaults, and the `Changed`-event bridge seam are all unchanged. This phase reworks how the controls are *reached and presented*, adds **two on/off toggles** (lava, waveform), and gives the panel a **deterministic, sectioned layout** that encodes the visualizer's composition (lava field + waveform ribbon, optionally overlaid). Full design, layout contract, primitive rationale, tooltip copy, acceptance, and wave decomposition: `product-notes/phase-15-visualizer-controls-enhancements.md`.
|
||||
|
||||
**Phase number:** 14 is taken by the `p14-w1-releases-consolidation` worktree (concurrent session); 13 is the highest landed phase in `COMPLETED.md`. 15 is the next free number.
|
||||
|
||||
**The load-bearing reframe.** Today the eight knobs read as a flat, equal grid — the user cannot tell which knobs drive the lava vs. the waveform, and neither subsystem can be turned off. The new layout sections the controls by subsystem and the two toggles make "lava only" / "waveform only" first-class. The screen-centering and chrome are polish around that.
|
||||
|
||||
**Daniel's five requirements (verbatim intent):** (1) panel look-and-feel follows `NowPlayingCard` — square corners, lighter navy, thin light border; (2) popover becomes **screen-centered + modal-tinted** — use the right MudBlazor overlay primitive, do not fight an anchored popover into the center; (3) deterministic three-row layout — **row 1:** lava toggle, waveform toggle, then (only if both on) collisions knob, then color knob far-right; **row 2 (lava on):** "LAVA:" + Gravity/Heat/two-Fluid knobs; **row 3 (waveform on):** "WAVE:" + scroll/zoom **slider** + width knob far-right; (4) playful, non-technical tooltip per control; (5) knob caption icons go **light**, not accent-green.
|
||||
|
||||
**Primitive decision (spec §4): `MudOverlay` (centered, `DarkBackground` tint, modal), not `MudPopover`.** `MudPopover` is by design *anchored* to its trigger — screen-centering means fighting its positioning model (the "do not fight MudBlazor" Daniel called out). `MudOverlay` gives screen-centering + the modal tint with the smallest delta from today's idiom (we already host a `MudOverlay` for dismissal; it graduates from transparent click-catcher to tinted scrim that holds the panel). `MudDialog` is the documented fallback if `MudOverlay` centering fights the knob-drag overlay — escalate, don't hand-roll a fixed-position div. CSS-isolation constraint persists (overlay portals content out of the subtree → panel chrome stays in the **global** `deepdrft-styles.css`, not the scoped `.razor.css`).
|
||||
|
||||
**The one renderer-adjacent touch.** The two new toggles back two new `WaveformVisualizerControlState` booleans (`LavaEnabled` / `WaveformEnabled`, both default true). The bridge (`WaveformVisualizer`) must learn to enable/disable the corresponding subsystem in the WebGL module on `Changed` — the one place this phase reaches past pure presentation. The C# seam is fully specified; the TS-side enable/disable surface is staff-engineer's call against the live module (spec §6, open question §10.1).
|
||||
|
||||
**Sequenced as one wave, four tracks** (small enough to ship as a single bundled PR — recommended, since the tracks are tightly coupled around one component pair and splitting would be churn): **15.A** state booleans + bridge wiring (load-bearing); **15.B** screen-centered tinted-modal primitive + NowPlayingCard chrome; **15.C** deterministic re-layout + toggles + scroll-slider; **15.D** tooltips + light icon colour. Dependency shape: `15.A → {15.B, 15.C} → 15.D`.
|
||||
|
||||
**Open questions for Daniel (spec §10):** (1) does "off" mean fully not-drawn or dimmed (recommend not-drawn), and does the WebGL module already have a per-subsystem enable path; (2) scroll/zoom binds to `ScrollSpeed` alone (the only matching dial) — confirm; (3) "LAVA:"/"WAVE:" labels green-accent (NowPlayingCard idiom) or light to match the new icons (recommend green-accent); (4) toggle widget — iconographic (lamp lit/unlit, waveform shown/hidden) vs. plain switch; (5) tint opacity (~0.3–0.4, Daniel eyeballs). **Post-landing doc-keeper note** (not this phase's work): root `CLAUDE.md` wrongly places `DDIcons.cs` in `DeepDrftPublic.Client.Common` — it lives in `DeepDrftShared.Client/Common`.
|
||||
|
||||
---
|
||||
|
||||
## Working with this file
|
||||
|
||||
- **Add items by extending an existing phase first**; only create a new phase when the addition genuinely doesn't fit any of 1–5. Phase numbers are organisational, not sequencing.
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
# Phase 15 — Visualizer Controls Enhancements (Design Spec)
|
||||
|
||||
Status: **design-complete, implementation-ready** (with open questions flagged at §10). Author:
|
||||
product-designer. Date: 2026-06-17. **No code has been written by this doc.**
|
||||
|
||||
This is a presentation + interaction rework of the **waveform visualizer control surface** — the eight
|
||||
RadialKnob panel introduced in Phase 12 and hosted by `WaveformVisualizerControlPopover`. It does **not**
|
||||
touch the WebGL2 renderer, the `WaveformVisualizerControlState` value model, the `Changed`-event bridge
|
||||
seam, or any playback path. It is a widget/layout/primitive rework of how the controls are *presented and
|
||||
reached*, and it adds **two new on/off toggles** (lava, waveform) plus **deterministic conditional
|
||||
visibility** of the existing knobs. The visualizer stays read-only — no control added here is a seek
|
||||
surface (the standing read-only contract, Phase 10 §D / Phase 12).
|
||||
|
||||
## Phase numbering
|
||||
|
||||
This is **Phase 15**, not 14. Phase 13 (CMS Public Landing) is the highest landed phase in
|
||||
`COMPLETED.md`. Phase **14** is in active use by the `p14-w1-releases-consolidation` worktree (another
|
||||
concurrent session). A second worktree, `nowplaying-card-reactivity`, is also live and touches the
|
||||
NowPlaying surface this phase is adjacent to — so 14 is taken and 15 is the next genuinely-free number.
|
||||
|
||||
Cross-references (read these before implementing):
|
||||
- `DeepDrftPublic.Client/Controls/WaveformVisualizerControls.razor[.css]` — the eight-knob panel being
|
||||
re-laid-out. Today: a `flex-wrap` grid of eight knobs in `.mix-visualizer-controls-bar`, gated only by a
|
||||
single `Visible` bool.
|
||||
- `DeepDrftPublic.Client/Controls/WaveformVisualizerControlPopover.razor` — the `MudPopover`-based host
|
||||
being re-primitived (§4). Today: `MudPopover Fixed` anchored to the lava-lamp trigger + a transparent
|
||||
`MudOverlay` for dismissal (no tint).
|
||||
- `DeepDrftPublic.Client/Controls/NowPlayingCard.razor[.css]` — the **look-and-feel reference** for the
|
||||
panel chrome (§5). Square corners, faint light border, light type on a dark ground.
|
||||
- `DeepDrftPublic.Client/Controls/SharePopover.razor` — the established overlay/dismissal idiom
|
||||
(`MudOverlay Visible OnClick`, `AutoClose` off so a knob drag does not dismiss). Carry it forward.
|
||||
- `DeepDrftShared.Client/Components/RadialKnob.razor` — **consumed, not modified.** Fixed API. Note: it
|
||||
emits a single `<style>` block and its `Label` renders as SVG text — it has no icon slot and no aria
|
||||
attributes, which is why icons ride beside knobs (§7) and accessible names ride on a wrapping group div.
|
||||
- `DeepDrftShared.Client/Common/DDIcons.cs` — the lava-lamp trigger glyph (`LavaLamp`/`LavaLampFilled`),
|
||||
consumed unchanged. (Note: DDIcons lives in **DeepDrftShared.Client**, not DeepDrftPublic.Client — the
|
||||
root `CLAUDE.md` line that places it in `DeepDrftPublic.Client.Common` is stale; flag for doc-keeper.)
|
||||
- `DeepDrftPublic.Client/Services/WaveformVisualizerControlState.cs` — the eight continuous dials. This
|
||||
phase **adds two booleans** (`LavaEnabled`, `WaveformEnabled`) to it (§6).
|
||||
- `DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` — the **global** panel-chrome block
|
||||
(`.waveform-visualizer-control-panel*`). Because MudBlazor portals popover/overlay content out of the
|
||||
component's DOM subtree, Blazor CSS isolation cannot reach it; panel chrome lives in the global sheet,
|
||||
not the scoped `.razor.css`. **This constraint persists** under the new primitive (§4, §5).
|
||||
- `DeepDrftShared.Client/wwwroot/styles/deepdrft-tokens.css` — token source of truth. All chrome colours
|
||||
resolve from `--deepdrft-*`; **no hardcoded hex** (the RadialKnob's two filled-icon literals in DDIcons
|
||||
are the documented named exception and are untouched here).
|
||||
- `product-notes/mix-visualizer-webgl-renderer.md §7` and
|
||||
`product-notes/phase-12-waveform-visualizer-generalization.md` — the popover idiom this evolves from.
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Make the visualizer control surface (a) read as a deliberate, **screen-centered modal panel** rather than
|
||||
a corner-anchored dropdown, (b) match the **NowPlayingCard** chrome, (c) lay the controls out in a
|
||||
**deterministic, sectioned structure** that mirrors the two things the visualizer actually composes — the
|
||||
**lava** field and the **waveform** ribbon — each independently toggleable, and (d) make every control
|
||||
self-describing via a **playful tooltip**. This turns an undifferentiated grid of eight identical knobs
|
||||
into a legible "lava lamp control deck."
|
||||
|
||||
**The reframe worth naming.** Today the eight knobs are presented as a flat, equal grid — the user cannot
|
||||
tell at a glance which knobs affect the lava and which affect the waveform, and there is no way to turn
|
||||
either subsystem off. The deterministic re-layout is not just cosmetic: it **encodes the visualizer's
|
||||
composition** (lava + waveform, optionally overlaid) into the control surface, and the two new toggles
|
||||
make "I only want the lava" or "I only want the waveform" a first-class choice. That is the load-bearing
|
||||
change; the centering and chrome are the polish around it.
|
||||
|
||||
---
|
||||
|
||||
## 2. Scope boundary
|
||||
|
||||
**In scope.**
|
||||
- Two new on/off toggle controls (lava, waveform) and the `WaveformVisualizerControlState` booleans that
|
||||
back them (§6).
|
||||
- A deterministic three-row layout with conditional visibility (§3).
|
||||
- Re-primitiving the popover host to a **screen-centered, tinted modal** overlay (§4).
|
||||
- NowPlayingCard-matched panel chrome (§5).
|
||||
- One control changes widget type: **scroll/zoom becomes a slider, not a knob** (§3, §8).
|
||||
- Per-control playful tooltips (§7) and the icon-colour change to light (§9).
|
||||
|
||||
**Out of scope / unchanged.**
|
||||
- The **WebGL2 renderer** and all lava/waveform effects. This phase changes how control values are reached
|
||||
and which are visible, not what they do in the shader.
|
||||
- The eight **continuous dial values, their `Default*` consts, and the TS-side anchors.** The two new
|
||||
booleans are additive; no existing dial is renamed or retuned.
|
||||
- The **`Changed`-event bridge seam.** Controls still mutate `WaveformVisualizerControlState` + raise
|
||||
`Changed`; `WaveformVisualizer` still subscribes and pushes the affected dial. The bridge cannot tell a
|
||||
knob from a slider from a toggle — it re-reads state on `Changed`. **One bridge addition is required**:
|
||||
the bridge must learn to act on the two new booleans (enable/disable the lava and waveform subsystems in
|
||||
the WebGL module). That is a real renderer-side touch and is the one place this phase reaches past pure
|
||||
presentation — see §6 and the open question §10.1.
|
||||
- The lava-lamp **trigger glyph** (`DDIcons.LavaLamp`/`LavaLampFilled`) and its placement on each host
|
||||
(Mix `TopRightAction`, Cut/Session ambient, NowPlaying corner). Unchanged.
|
||||
- **Persistence model** — still DI-scoped `WaveformVisualizerControlState`: survives SPA nav, resets on
|
||||
fresh load. The two new booleans inherit this for free.
|
||||
- The **read-only contract** — no toggle, slider, or knob added here is a seek surface.
|
||||
|
||||
---
|
||||
|
||||
## 3. The layout contract (deterministic rows + conditional visibility)
|
||||
|
||||
The panel lays out as **up to three rows, top to bottom, in this exact order.** "Conditional" rows reserve
|
||||
no permanent height — they appear/disappear with their subsystem. The contract is deterministic: given the
|
||||
two toggle states, the visible controls and their order are fully determined.
|
||||
|
||||
### Row 1 — Mode row (always visible)
|
||||
|
||||
Left to right:
|
||||
1. **Lava toggle** — on/off toggle button. Turns the lava field on/off.
|
||||
2. **Waveform toggle** — on/off toggle button. Turns the waveform ribbon on/off.
|
||||
3. **Collisions knob** — visible **only if BOTH lava AND waveform are on** (collisions are the interaction
|
||||
between the two subsystems; with only one present there is nothing to collide). Backed by
|
||||
`CollisionStrength`.
|
||||
4. **Color knob** — pinned to the **far right** of row 1, always visible. Backed by `GradientRotationSpeed`
|
||||
(the gradient/colour dial). Colour applies to the whole field regardless of which subsystems are on, so
|
||||
it lives in the always-visible mode row, not in either conditional section.
|
||||
|
||||
Layout note: items 1–3 group left; item 4 is right-pinned (a `space-between` row, or a left group + a
|
||||
right-aligned color knob). When collisions is hidden the color knob stays put on the right — the row must
|
||||
not reflow the color knob leftward.
|
||||
|
||||
### Row 2 — LAVA section (visible only if lava is on)
|
||||
|
||||
A label **"LAVA:"** (mono, uppercase, light — the NowPlaying `.np-label` idiom) then, left to right:
|
||||
- **Gravity** knob — `LavaGravity`.
|
||||
- **Heat** knob — `LavaHeat`.
|
||||
- **Fluid amount** knob — `FluidAmount`.
|
||||
- **Fluid viscosity** knob — `FluidViscosity`.
|
||||
|
||||
(The "two Fluid knobs" in Daniel's brief = the Phase 10 split of the single density knob into
|
||||
`FluidAmount` + `FluidViscosity`. Both live here.)
|
||||
|
||||
### Row 3 — WAVE section (visible only if waveform is on)
|
||||
|
||||
A label **"WAVE:"** (same idiom as LAVA:) then, left to right:
|
||||
- **Scroll/zoom slider** — a **slider, not a knob** (the one widget-type change this phase makes; §8).
|
||||
Backed by `ScrollSpeed`. Rationale: scroll/zoom is a "position along a continuum" feel — a horizontal
|
||||
slider reads that more naturally than a rotary knob, and it visually distinguishes the one
|
||||
spatial/temporal control from the eight physical-property knobs. (Open question §10.2: is it
|
||||
`ScrollSpeed` alone, or does "scroll/zoom" want the zoom mapping too? `ScrollSpeed` is the only matching
|
||||
dial that exists; treated as `ScrollSpeed` here.)
|
||||
- **Width** knob — pinned to the **far right** of row 3. `WaveformWidth`.
|
||||
|
||||
### Visibility truth table
|
||||
|
||||
| Lava | Waveform | Row 1 controls | Row 2 (LAVA) | Row 3 (WAVE) |
|
||||
|------|----------|-----------------------------------------|---------------------|-------------------------|
|
||||
| off | off | Lava tgl, Wave tgl, Color (right) | hidden | hidden |
|
||||
| on | off | Lava tgl, Wave tgl, Color (right) | gravity/heat/fluids | hidden |
|
||||
| off | on | Lava tgl, Wave tgl, Color (right) | hidden | scroll slider, width |
|
||||
| on | on | Lava tgl, Wave tgl, **Collisions**, Color (right) | gravity/heat/fluids | scroll slider, width |
|
||||
|
||||
Note: the **both-off** state leaves only the two toggles + color knob — the panel never collapses to
|
||||
empty, so chrome (§5) always has content to frame.
|
||||
|
||||
### Layout reflow discipline
|
||||
|
||||
The panel re-lays-out as rows appear/disappear; this is fine in a centered modal (no surrounding page
|
||||
content to push). But the modal should not "jump" jarringly on every toggle — recommend the panel be
|
||||
top-anchored within the centered overlay (grows downward as rows appear) and animate height/opacity on the
|
||||
conditional rows if cheap. A layout call for staff-engineer; the **contract above is the requirement**, the
|
||||
animation is taste.
|
||||
|
||||
---
|
||||
|
||||
## 4. Primitive choice: screen-centered, tinted modal (the recommendation)
|
||||
|
||||
**Recommendation: replace `MudPopover` with a centered `MudOverlay` (tinted/dark-background, `Modal`) that
|
||||
hosts the control panel in its center.** Do **not** keep `MudPopover`.
|
||||
|
||||
### Why not `MudPopover`
|
||||
|
||||
`MudPopover` is, by design, an **anchored** floating panel — it positions itself relative to a trigger's
|
||||
bounding rect (that is its whole purpose; `Fixed="true"` + `AnchorOrigin`/`TransformOrigin` only choose
|
||||
*which corner* of the trigger it hangs off). Daniel's requirement is the panel **centered on the screen,
|
||||
independent of where the lava-lamp icon sits** (and the icon sits in four different places across hosts —
|
||||
Mix corner, Cut/Session ambient, NowPlaying corner). Forcing a screen-centered position out of an
|
||||
anchored popover means fighting its positioning model (transform overrides, `!important` CSS against
|
||||
portaled inline styles) — exactly the "do not fight MudBlazor" Daniel called out. So we change primitive.
|
||||
|
||||
### Why `MudOverlay` over `MudDialog`
|
||||
|
||||
Two viable centered-modal primitives:
|
||||
- **`MudOverlay`** (recommended): a full-viewport scrim we already use for dismissal (`SharePopover`,
|
||||
the current popover host). Set `DarkBackground="true"` for the tint, `Modal` semantics via the overlay
|
||||
itself, and center the panel as the overlay's child (the overlay is a flex container; center its
|
||||
content). This is the **smallest delta from today's idiom** — we already host a `MudOverlay` for
|
||||
dismissal; here it graduates from a transparent click-catcher to a tinted modal scrim that also *holds*
|
||||
the panel. No `IDialogService`, no dialog registration, no `<MudDialogProvider>` dependency surprises.
|
||||
- **`MudDialog`**: purpose-built for centered modals with built-in tint and focus management. Heavier:
|
||||
needs `IDialogService` + a `<MudDialogProvider>` in the layout, turns the panel into a dialog component
|
||||
invoked imperatively, and brings dialog chrome (title bar, close button) we would have to suppress to
|
||||
keep the NowPlayingCard look. More machinery than this needs.
|
||||
|
||||
**Verdict: `MudOverlay` with `DarkBackground` + centered child panel.** It gives screen-centering and the
|
||||
modal tint with the least new machinery and stays closest to the `SharePopover`/current-host idiom. If
|
||||
staff-engineer finds `MudOverlay` centering or focus-trapping fights the knob-drag overlay, `MudDialog` is
|
||||
the documented fallback — note it and escalate rather than hand-rolling a fixed-position div.
|
||||
|
||||
### Behavior contract
|
||||
|
||||
- Lava-lamp icon click → open the overlay (the icon trigger and its glyph are unchanged from Phase 12).
|
||||
- Overlay visible → a **slight page tint** behind the panel so the panel reads as modal (Daniel:
|
||||
"slight tint … reads as modal"). Use `MudOverlay`'s dark background at a **low opacity** — slight, not a
|
||||
blackout. Recommend ~0.3–0.4 scrim alpha; final value is Daniel's to eyeball on screen.
|
||||
- Click the tint (outside the panel) → close. Mirror `SharePopover`: `OnClick` on the overlay closes.
|
||||
- **`AutoClose` stays off / outside-click must not fire during a knob drag.** RadialKnob mounts its own
|
||||
full-viewport `position:fixed; z-index:9999` mouse-capture div while dragging (see `RadialKnob.razor`
|
||||
lines 5–9). That capture div sits **above** the overlay scrim — verify the knob drag's pointer-up does
|
||||
not register as an outside-click that closes the modal. The current host already guards this (`AutoClose`
|
||||
off, dismissal via explicit `OnClick`); preserve it. If the new z-order conflicts, gate close-on-tint off
|
||||
while any knob is dragging. **This is the highest-risk interaction detail in the phase — call it out in
|
||||
acceptance (§11).**
|
||||
- `Esc` closes (nice-to-have; follow `SharePopover` if it does this).
|
||||
- No auto-close on value change — the user tunes multiple controls per session.
|
||||
|
||||
### CSS isolation note (unchanged constraint)
|
||||
|
||||
`MudOverlay` portals its content to the document body just as `MudPopover` does, so Blazor CSS isolation
|
||||
still cannot reach the panel. Panel chrome **stays in the global `deepdrft-styles.css`**
|
||||
(`.waveform-visualizer-control-panel*`), and the icon-colour rules stay global descendant selectors (no
|
||||
`::deep`). The `PanelChrome` parameter on `WaveformVisualizerControls` keeps doing its job. The scoped
|
||||
`.razor.css` keeps only the legacy inline-bar fallback (if Mix still mounts inline anywhere — verify; Phase
|
||||
12 moved everyone to the popover, so the inline fallback may now be dead and removable, but that cleanup is
|
||||
**not** in this phase's scope — flag, don't cut).
|
||||
|
||||
---
|
||||
|
||||
## 5. Look and feel — NowPlayingCard tokens
|
||||
|
||||
The panel chrome follows `NowPlayingCard` (`.now-playing` in `NowPlayingCard.razor.css`): **square
|
||||
corners, a lighter-navy ground, a thin light border.**
|
||||
|
||||
| Aspect | NowPlayingCard today | Panel target |
|
||||
|-------------------|---------------------------------------------------|-------------------------------------------------------------------|
|
||||
| Corners | square (no `border-radius`) | **square** — drop the current `border-radius: 8px` |
|
||||
| Ground | `rgba(250,250,248,0.06)` over a dark surface | **lighter navy** — `--deepdrft-navy-mid` (current) is acceptable; Daniel says "lighter navy," so favour navy-mid over the darkest navy. Confirm on screen. |
|
||||
| Border | `1px solid rgba(250,250,248,0.12)` (thin, light) | **thin light border** — replace the current `--deepdrft-border-green` with a faint light border in the NowPlayingCard spirit (light-on-dark, ~0.12 alpha). |
|
||||
| Backdrop | `backdrop-filter: blur(8px)` | optional — nice over the visualizer; cheap on a small modal panel. Taste call. |
|
||||
| Label type | `--deepdrft-font-mono`, 0.6rem, 0.25em tracking, uppercase, `--deepdrft-green-accent` | reuse this idiom for the **"LAVA:" / "WAVE:" section labels** (§3). Daniel wants light icons (§9) but the *text labels* matching the NowPlaying green-accent label is consistent — confirm whether labels are green-accent (matching `.np-label`) or light (matching the icon change). **Open question §10.3.** |
|
||||
| Knob palette pins | n/a | keep the existing pinned `--mud-palette-*` → Hero tokens block (green-accent arc, navy center, light label). **Except**: icon colour changes to light (§9). |
|
||||
|
||||
The existing global block `.waveform-visualizer-control-panel.mix-visualizer-controls-bar` is where most of
|
||||
this lands: drop `border-radius`, swap the border token, confirm the ground token. **All colours stay
|
||||
token-sourced** — no hardcoded hex (deepdrft-tokens.css §). If a "thin light border on dark" token does not
|
||||
exist, the NowPlayingCard uses a literal `rgba(250,250,248,0.12)`; prefer adding/▮reusing a
|
||||
`--deepdrft-border-light` token over scattering the literal (a small token-hygiene win — flag to
|
||||
staff-engineer, optional).
|
||||
|
||||
---
|
||||
|
||||
## 6. State model additions
|
||||
|
||||
`WaveformVisualizerControlState` gains **two booleans**:
|
||||
|
||||
- `LavaEnabled` (default **true**) — backs the row-1 lava toggle.
|
||||
- `WaveformEnabled` (default **true**) — backs the row-1 waveform toggle.
|
||||
|
||||
Each gets a matching `DefaultLavaEnabled` / `DefaultWaveformEnabled` const, mirroring the existing
|
||||
eight-dial default convention. Defaults **true/true** so the current behavior (both subsystems on) is the
|
||||
out-of-the-box state — no visible change on first open.
|
||||
|
||||
The two toggles mutate their boolean + call `NotifyChanged()`, exactly as the knobs do. **The bridge
|
||||
(`WaveformVisualizer`) must learn to act on these two booleans** — on `Changed`, read `LavaEnabled` /
|
||||
`WaveformEnabled` and enable/disable the corresponding subsystem in the WebGL module (push an `enable`
|
||||
uniform/flag, or skip the subsystem's draw). This is the one renderer-adjacent touch in the phase. The
|
||||
exact JS-module surface (a `setLavaEnabled(bool)` / `setWaveformEnabled(bool)` interop pair, or folding
|
||||
into existing uniform pushes) is **staff-engineer's call against the live `WaveformVisualizer.ts`** — see
|
||||
open question §10.1. The C# side of the seam is fully specified here; the TS side is a small additive
|
||||
uniform/branch.
|
||||
|
||||
Conditional-visibility logic (§3) reads these same booleans in `WaveformVisualizerControls.razor` `@if`
|
||||
guards — single source of truth, no duplicated flag.
|
||||
|
||||
---
|
||||
|
||||
## 7. Per-control tooltip copy (useful-but-fun)
|
||||
|
||||
Every control gets a tooltip. This is a **lava-lamp visualizer** — copy should be playful and describe the
|
||||
*felt effect*, not the technical parameter. Wrap each control (or its group div) in a `MudTooltip Text=...`
|
||||
(the trigger already uses `MudTooltip`, so the idiom is in the file). Draft copy below — Daniel's to
|
||||
approve/punch-up; these are starting points, not final.
|
||||
|
||||
| Control | State field | Suggested tooltip copy |
|
||||
|------------------|-----------------------|-------------------------------------------------------------------------------|
|
||||
| Lava toggle | `LavaEnabled` | "Light the lamp — or let it go cold." |
|
||||
| Waveform toggle | `WaveformEnabled` | "Show the sound, or hide the ribbon." |
|
||||
| Collisions | `CollisionStrength` | "How hard the blobs body-check the beat." |
|
||||
| Color | `GradientRotationSpeed`| "How fast the lamp drifts through its colors." |
|
||||
| Gravity (LAVA) | `LavaGravity` | "How heavy the wax feels — float, or sink." |
|
||||
| Heat (LAVA) | `LavaHeat` | "Crank the burner. More heat, more rolling boil." |
|
||||
| Fluid amount | `FluidAmount` | "How much goo is in the lamp." |
|
||||
| Fluid viscosity | `FluidViscosity` | "Runny and gooey, or tight little globes." |
|
||||
| Scroll/zoom slider | `ScrollSpeed` | "How fast the sound rolls by." |
|
||||
| Width (WAVE) | `WaveformWidth` | "How wide the ribbon spreads across the lamp." |
|
||||
|
||||
Accessibility: RadialKnob has no aria capture, so the accessible name still rides on the wrapping group
|
||||
`div` (`role="group" aria-label=...`) as it does today. Keep the group `aria-label` (plain/technical for
|
||||
screen readers) **and** add the playful `MudTooltip` (visual hover) — they serve different audiences;
|
||||
don't collapse one into the other.
|
||||
|
||||
---
|
||||
|
||||
## 8. The one widget-type change: scroll/zoom → slider
|
||||
|
||||
Today all eight controls are `RadialKnob`s. Daniel wants the WAVE-row scroll/zoom to be a **slider, not a
|
||||
knob.** Use `MudSlider` (already in the codebase — `VolumeZone`, `WaveformSeeker` use it). Bind
|
||||
`Value`/`ValueChanged` to `ScrollSpeed` through the same `OnScrollSpeedChanged` → `NotifyChanged()` handler
|
||||
the knob uses today — the bridge does not care that the widget changed. Range `0–1`, step matching the
|
||||
knobs' `0.001` for live feel. Style it to the panel: track/thumb in green-accent (the pinned
|
||||
`--mud-palette-primary`), consistent with the knob arcs. A horizontal slider in the WAVE row reads as
|
||||
"position along the scroll continuum," visually distinct from the rotary physical-property knobs — that
|
||||
distinction is the point, not just Daniel's preference.
|
||||
|
||||
This is the **only** control that changes widget type; the other nine controls (two toggles + seven knobs)
|
||||
keep their types.
|
||||
|
||||
---
|
||||
|
||||
## 9. Icon colour — light, not accent green
|
||||
|
||||
The knob caption icons today are tinted `--deepdrft-green-accent` (global rule
|
||||
`.waveform-visualizer-control-panel .waveform-visualizer-control-icon { color: var(--deepdrft-green-accent); }`).
|
||||
Daniel wants them **light**. Change that rule's `color` to `--deepdrft-white` (the panel's light token),
|
||||
keeping the `opacity: 0.85`. This is a one-line token swap in the global sheet. The knob **arcs/pointers
|
||||
stay green-accent** (the `--mud-palette-primary` pin) — only the Material caption *icons* go light. (This
|
||||
is why §5's section-label colour is an open question §10.3 — with icons now light, a green-accent text
|
||||
label may look inconsistent; Daniel decides.)
|
||||
|
||||
The scoped fallback rule (`.mix-visualizer-control ::deep .mix-visualizer-control-icon` in the
|
||||
`.razor.css`) tints to `--mud-palette-primary` for the legacy inline mount — if that mount is dead post-
|
||||
Phase-12, this is moot; if it survives, align it to light too for consistency. Flag, don't assume.
|
||||
|
||||
---
|
||||
|
||||
## 10. Open questions for Daniel
|
||||
|
||||
1. **Bridge action on the new toggles (§6).** Disabling a subsystem in the renderer — does the WebGL module
|
||||
already have an enable/disable path per subsystem (lava draw, waveform draw), or does this need new
|
||||
interop (`setLavaEnabled` / `setWaveformEnabled`)? This is the one renderer-side touch; staff-engineer
|
||||
confirms against `WaveformVisualizer.ts`. **Does Daniel want "off" to mean fully not-drawn, or
|
||||
faded/dimmed?** (Recommend: fully not-drawn — a toggle should read as on/off, not a dimmer.)
|
||||
2. **Scroll/zoom binding (§3, §8).** "Scroll/zoom" — is this `ScrollSpeed` alone (the only matching dial),
|
||||
or does Daniel also want a zoom/resolution dimension folded in? The standalone resolution control was
|
||||
removed in Phase 10 (folded into scroll speed via `WaveformZoomMapping`). Treated here as `ScrollSpeed`;
|
||||
confirm.
|
||||
3. **Section-label colour (§5, §9).** With caption icons going light, should the "LAVA:" / "WAVE:" section
|
||||
labels be green-accent (matching NowPlayingCard's `.np-label`) or light (matching the new icon colour)?
|
||||
Recommend **green-accent** — it preserves the NowPlayingCard label idiom and gives the rows a colour
|
||||
anchor against the otherwise-light controls. Daniel's eye.
|
||||
4. **Toggle widget (§3 row 1).** Daniel said "toggle buttons." Recommend `MudToggleIconButton` or a
|
||||
`MudButton` pair styled to the panel (lit/unlit), so each toggle reads as on/off at a glance — possibly
|
||||
reusing the lava-lamp lit/unlit glyph spirit for the lava toggle. Exact widget is staff-engineer's;
|
||||
confirm whether Daniel wants iconographic toggles (lamp lit/unlit, waveform shown/hidden) or plain
|
||||
text/switch toggles.
|
||||
5. **Tint opacity (§4).** "Slight" tint — recommend ~0.3–0.4 scrim alpha. Daniel eyeballs final.
|
||||
|
||||
---
|
||||
|
||||
## 11. Acceptance criteria
|
||||
|
||||
1. **Centering:** opening the controls from the lava-lamp icon on **every** host (Mix, Cut, Session,
|
||||
NowPlaying) lands the panel **screen-centered**, not anchored to the icon — regardless of where the icon
|
||||
sits on that host.
|
||||
2. **Modal tint:** while the panel is open, the page behind shows a **slight** tint; the panel reads as
|
||||
modal. Clicking the tint (outside the panel) closes it.
|
||||
3. **Knob-drag safety:** dragging any knob (or the new slider) and releasing the mouse **outside** the
|
||||
panel does **not** close the modal (the §4 RadialKnob capture-div / outside-click guard holds).
|
||||
4. **Layout contract (§3):** the visibility truth table holds exactly — collisions appears iff both lava
|
||||
and waveform are on; LAVA row appears iff lava on; WAVE row appears iff waveform on; color knob is always
|
||||
far-right of row 1 and does not reflow when collisions hides; both-off leaves toggles + color only.
|
||||
5. **Chrome:** panel has square corners, lighter-navy ground, thin light border — visibly matching
|
||||
NowPlayingCard's treatment. All colours token-sourced; no new hardcoded hex.
|
||||
6. **Widget types:** scroll/zoom is a slider; the other seven continuous controls are knobs; lava/waveform
|
||||
are toggle buttons.
|
||||
7. **Icons light:** knob caption icons render light (not green-accent); knob arcs/pointers stay green-accent.
|
||||
8. **Tooltips:** every control has a hover tooltip with the playful copy (§7, as approved); group
|
||||
`aria-label`s remain for screen readers.
|
||||
9. **Toggles persist + drive the renderer:** flipping lava/waveform off visibly removes that subsystem from
|
||||
the visualizer; the state survives SPA nav and resets on fresh load (DI-scoped state). Both default on.
|
||||
10. **No regression:** the read-only contract holds (no control seeks); the eight existing dials behave
|
||||
exactly as before when their subsystem is on; the bridge `Changed` seam is intact.
|
||||
|
||||
---
|
||||
|
||||
## 12. Wave / track decomposition
|
||||
|
||||
Sequenced as **one wave with four tracks.** 15.A is the load-bearing state + bridge change everything else
|
||||
reads; 15.B (primitive/chrome) and 15.C (layout/widgets) both depend on 15.A's booleans existing; 15.D
|
||||
(tooltips + icon colour) is cosmetic polish that can land last or fold into 15.C. The phase is small enough
|
||||
to ship as a single PR if Daniel prefers (recommend one bundled PR — the tracks are tightly coupled around
|
||||
one component pair and splitting would be churn); the decomposition below is for orientation and parallel
|
||||
reasoning, not a mandate to split.
|
||||
|
||||
### 15.A — State booleans + bridge wiring (load-bearing prerequisite)
|
||||
- Add `LavaEnabled` / `WaveformEnabled` (+ `Default*` consts) to `WaveformVisualizerControlState`.
|
||||
- Teach `WaveformVisualizer` (the bridge) to push the two booleans to the WebGL module on `Changed`
|
||||
(resolves OQ §10.1 with staff-engineer).
|
||||
- **Touches:** `Services/WaveformVisualizerControlState.cs`, `Controls/WaveformVisualizer.razor`(.cs),
|
||||
and the TS module `DeepDrftPublic/Interop/visualizer/WaveformVisualizer.ts` (the one renderer touch).
|
||||
- **Gates:** 15.B, 15.C.
|
||||
|
||||
### 15.B — Screen-centered tinted modal primitive + NowPlayingCard chrome
|
||||
- Re-primitive `WaveformVisualizerControlPopover` from `MudPopover` (anchored) to `MudOverlay`
|
||||
(centered, `DarkBackground` tint, modal), preserving the `AutoClose`-off / knob-drag-safe dismissal idiom.
|
||||
- Update the global panel-chrome block to NowPlayingCard treatment (square corners, lighter-navy ground,
|
||||
thin light border) — `deepdrft-styles.css`.
|
||||
- **Touches:** `Controls/WaveformVisualizerControlPopover.razor`,
|
||||
`DeepDrftPublic/wwwroot/styles/deepdrft-styles.css`.
|
||||
- **Depends on:** 15.A (panel content references the new toggles; primitive itself does not, but ship
|
||||
together to avoid a half-state). **Coordinate with:** 15.C (both edit the panel/chrome).
|
||||
|
||||
### 15.C — Deterministic re-layout + toggles + scroll slider
|
||||
- Rewrite `WaveformVisualizerControls.razor`'s layout into the three-row contract (§3) with `@if`
|
||||
visibility off the two booleans.
|
||||
- Add the two toggle-button controls (row 1) and the collisions conditional.
|
||||
- Change scroll/zoom from `RadialKnob` to `MudSlider` (§8); keep the other seven knobs.
|
||||
- Layout/section-label CSS for the three rows (global sheet, portaled).
|
||||
- **Touches:** `Controls/WaveformVisualizerControls.razor`(.css),
|
||||
`DeepDrftPublic/wwwroot/styles/deepdrft-styles.css`.
|
||||
- **Depends on:** 15.A. **Coordinate with:** 15.B.
|
||||
|
||||
### 15.D — Tooltips + light icon colour (cosmetic polish)
|
||||
- Add `MudTooltip` playful copy to each control (§7, as Daniel approves).
|
||||
- Swap the caption-icon colour rule from green-accent to light (§9) — one line in the global sheet.
|
||||
- **Touches:** `Controls/WaveformVisualizerControls.razor`,
|
||||
`DeepDrftPublic/wwwroot/styles/deepdrft-styles.css`.
|
||||
- **Depends on:** 15.C (tooltips wrap the final control layout). Can fold into 15.C.
|
||||
|
||||
**Dependency shape:** `15.A → {15.B, 15.C} → 15.D`. 15.B and 15.C are parallel but both edit the global
|
||||
chrome sheet and the panel — if shipped as one PR (recommended), the coordination is free.
|
||||
|
||||
### Post-implementation doc-keeper note (not for this phase to execute)
|
||||
- The root `CLAUDE.md` places `DDIcons.cs` in `DeepDrftPublic.Client.Common`; it actually lives in
|
||||
`DeepDrftShared.Client/Common`. doc-keeper should correct this when the phase lands.
|
||||
- The `DeepDrftPublic.Client/CLAUDE.md` `WaveformVisualizerControls` / `WaveformVisualizerControlPopover`
|
||||
descriptions will need updating to the eight-knob-becomes-two-toggles-plus-seven-knobs-plus-slider
|
||||
layout, the `MudOverlay` (not `MudPopover`) primitive, and the two new state booleans — doc-keeper's
|
||||
task on landing, **not** product-designer's and **not** part of this spec.
|
||||
Reference in New Issue
Block a user