3f83e0f11c
Note the five round-2 changes in COMPLETED.md; mark §8 + the §2/§11 slider references superseded (scroll reverted to RadialKnob).
484 lines
34 KiB
Markdown
484 lines
34 KiB
Markdown
# Phase 15 — Visualizer Controls Enhancements (Design Spec)
|
||
|
||
Status: **design-complete, implementation-ready** (all five open questions resolved by Daniel 2026-06-17 —
|
||
see §10). Author: product-designer. Date: 2026-06-17 (resolutions folded 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). *(superseded 2026-06-17 — reverted to a RadialKnob; see §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). "Off" means the subsystem is **genuinely not drawn** (no render cost), which means
|
||
building a real per-subsystem draw-skip path in `WaveformVisualizer.ts` — the implementer should expect
|
||
this enable seam **does not yet exist** and is part of the work, not a one-line flag. That is the one
|
||
place this phase reaches past pure presentation — see §6 and resolved OQ §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** — an **iconographic lamp toggle** (lit/unlit lamp glyph), **green** (interactive, per
|
||
the §5 colour principle). Turns the lava field on/off. (Resolved 2026-06-17 — OQ §10.4.)
|
||
2. **Waveform toggle** — an **iconographic lamp toggle** (lit/unlit), **green** (interactive). Turns the
|
||
waveform ribbon on/off. Same lamp-toggle treatment as the lava toggle for a consistent row-1 pair.
|
||
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** — NowPlaying `.np-label` *typography*, recoloured light per
|
||
the §5 colour principle: labels are static, so light) 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. **Binds to `ScrollSpeed` alone**
|
||
(confirmed 2026-06-17 — OQ §10.2); no separate zoom dimension is folded in.
|
||
- **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 **mild** opacity — slight, not a
|
||
blackout (resolved 2026-06-17 — OQ §10.5: go mild). **The tint opacity must have one single point of
|
||
truth** — a single token/constant (e.g. a `--deepdrft-modal-scrim-alpha` token in `deepdrft-tokens.css`,
|
||
or one named const), **not a magic number repeated at call sites.** Mild ≈ 0.3 alpha as a starting eyeball
|
||
value, set once in that single token so a future change is one edit.
|
||
- 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
|
||
|
||
### Color principle (governs every control's colour, resolved 2026-06-17)
|
||
|
||
**green = interactive, light = non-interactive.** This single rule decides the colour of every element on
|
||
the panel:
|
||
- **Interactive** elements — the two lamp **toggles** (§3 row 1, §10.4), the knob **arcs/pointers**, the
|
||
scroll **slider** track/thumb, and any other control the user can grab — are **green-accent**
|
||
(`--deepdrft-green-accent` / the pinned `--mud-palette-primary`).
|
||
- **Static / decorative** elements — the **"LAVA:" / "WAVE:" section labels** (§10.3), the knob **caption
|
||
icons** (§9, requirement 5), and any other label or ornament the user cannot act on — are **light**
|
||
(`--deepdrft-white`).
|
||
|
||
This is one rule, not three instructions: Daniel's original requirement 5 (caption icons light), OQ3
|
||
(section labels light), and OQ4 (toggles green because interactive) are all the same principle. Apply it
|
||
uniformly; do not colour controls case-by-case. The affected sections below reference back to this rule
|
||
rather than restating the rationale.
|
||
|
||
### Chrome
|
||
|
||
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 **typographic** idiom (mono/tracking/uppercase) for the **"LAVA:" / "WAVE:" section labels** (§3), but recolour to **light** (`--deepdrft-white`), not green-accent. Labels are static, so by the colour principle above they are light; green is reserved for interactive elements. (Resolved 2026-06-17 — OQ §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.
|
||
|
||
**"Off" means fully absent (resolved 2026-06-17 — OQ §10.1).** When a subsystem is off it is **not drawn,
|
||
contributes no collisions, and incurs no render cost.** It is *not* dimmed, *not* drawn-then-hidden, *not*
|
||
faded — the subsystem's draw path is genuinely skipped. This sharpens the renderer touch beyond a flag
|
||
toggle: **the implementer should check `WaveformVisualizer.ts` for an existing per-subsystem enable seam
|
||
but should expect it does not yet exist.** Building that enable/disable seam — a real "don't render this
|
||
subsystem" path (skip the draw call / early-out, not a `visible=false` uniform on a subsystem that still
|
||
runs) — is **part of the work**, not a found primitive to flip. This is the one place the phase reaches
|
||
into the renderer, and it is a small build, not a one-line uniform push. The C# side of the seam is fully
|
||
specified here; the TS side is the additive draw-skip path staff-engineer builds against the live module.
|
||
|
||
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
|
||
|
||
> **SUPERSEDED (2026-06-17) — polish round 2.** Daniel reversed this decision after the initial landing:
|
||
> the WAVE-row scroll/zoom control was reverted from `MudSlider` back to a `RadialKnob`. The scroll
|
||
> control is now a knob like the other dials. The rationale below is the historical design record.
|
||
|
||
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** — this is the §5 colour principle applied: caption icons are static/decorative,
|
||
so 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) — they are the interactive part of the control, so green
|
||
by the same principle. Only the Material caption *icons* go light. The "LAVA:" / "WAVE:" section labels go
|
||
light by the same rule (§5, §10.3 resolved).
|
||
|
||
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 — all RESOLVED 2026-06-17
|
||
|
||
(Kept visible per the project convention: resolved OQs are marked, not deleted.)
|
||
|
||
1. **Bridge action on the new toggles (§6). — RESOLVED 2026-06-17:** "Off" means **fully absent** — the
|
||
subsystem is not drawn, contributes no collisions, and incurs **no render cost** (not dimmed, not
|
||
drawn-then-hidden). The implementer should check `WaveformVisualizer.ts` for an existing per-subsystem
|
||
enable seam but should **expect it does not yet exist**; building that genuine "don't render this
|
||
subsystem" path (skip the draw, not a no-op `visible` uniform) is **part of the work**. This is a real
|
||
renderer-side build, not a one-line flag toggle. (Reflected in §6 and the 15.A track scope.)
|
||
2. **Scroll/zoom binding (§3, §8). — RESOLVED 2026-06-17:** The scroll/zoom slider binds to **`ScrollSpeed`
|
||
alone**. No separate zoom/resolution dimension is folded in.
|
||
3. **Section-label colour (§5, §9). — RESOLVED 2026-06-17:** "LAVA:" / "WAVE:" labels are **LIGHT**, not
|
||
green. Rationale: green is reserved for interactive elements; labels are static, so light. (This is the
|
||
§5 colour principle — green = interactive, light = non-interactive.)
|
||
4. **Toggle widget (§3 row 1). — RESOLVED 2026-06-17:** The toggles **ARE iconographic lamp toggles**
|
||
(lit/unlit lamp glyph), and they are **green because they are interactive** (the §5 colour principle).
|
||
Not plain text/switch toggles.
|
||
5. **Tint opacity (§4). — RESOLVED 2026-06-17:** Go **mild**. There must be **one single point of
|
||
truth/change** for the tint opacity — a single token/constant, not a magic number repeated at call
|
||
sites. (≈ 0.3 alpha as a starting eyeball value, set once in that token.)
|
||
|
||
---
|
||
|
||
## 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 bound to `ScrollSpeed` alone *(superseded 2026-06-17 — reverted to a RadialKnob; see §8)*; the other seven continuous
|
||
controls are knobs; lava/waveform are **iconographic lamp toggles** (lit/unlit).
|
||
7. **Colour principle (green = interactive, light = non-interactive):** knob caption icons render **light**;
|
||
"LAVA:" / "WAVE:" section labels render **light**; knob arcs/pointers, the scroll slider, and the lamp
|
||
toggles render **green-accent**. No control is coloured against this rule. The tint scrim alpha resolves
|
||
from a single token/constant (one point of change), set to a mild value.
|
||
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 **fully removes** that subsystem
|
||
from the visualizer — not drawn, no collisions, no render cost (not dimmed). 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 + per-subsystem draw-skip seam (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`.
|
||
- **Build the per-subsystem enable/disable seam in `WaveformVisualizer.ts`** so an "off" subsystem is
|
||
**genuinely not drawn** (no render cost — skip the draw path, not a `visible=false` uniform on a
|
||
still-running subsystem). Per OQ §10.1, expect this seam **does not yet exist** — building it is part of
|
||
this track, not flipping a found flag. This is the one real renderer-side build in the phase.
|
||
- **Touches:** `Services/WaveformVisualizerControlState.cs`, `Controls/WaveformVisualizer.razor`(.cs),
|
||
and the TS module `DeepDrftPublic/Interop/visualizer/WaveformVisualizer.ts` (the one renderer touch —
|
||
now a build, not a uniform push; budget for it accordingly).
|
||
- **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.
|