diff --git a/PLAN.md b/PLAN.md index 5076889..44baa5b 100644 --- a/PLAN.md +++ b/PLAN.md @@ -251,11 +251,13 @@ A presentation + interaction rework of the **waveform visualizer control surface **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). +**The one renderer-adjacent touch.** The two new toggles back two new `WaveformVisualizerControlState` booleans (`LavaEnabled` / `WaveformEnabled`, both default true). The bridge (`WaveformVisualizer`) must enable/disable the corresponding subsystem in the WebGL module on `Changed` — the one place this phase reaches past pure presentation. **"Off" means fully absent** (resolved 2026-06-17): the subsystem is not drawn, contributes no collisions, incurs no render cost — not dimmed. This is a **real build**, not a flag flip: the implementer should expect the per-subsystem draw-skip seam **does not yet exist** in `WaveformVisualizer.ts` and that building it is part of 15.A (spec §6, §10.1). Budget the renderer touch as a draw-skip path, not a one-line uniform push. + +**Colour principle — green = interactive, light = non-interactive** (resolved 2026-06-17). One rule governs every control's colour: interactive elements (the lamp toggles, knob arcs/pointers, scroll slider) are **green-accent**; static/decorative elements (the "LAVA:" / "WAVE:" section labels, knob caption icons) are **light**. This unifies the original requirement 5 (icons light), the section-label colour, and the toggle colour into one principle — apply it uniformly, not per-control (spec §5). **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`. +**Open questions — all RESOLVED 2026-06-17 (spec §10, kept visible):** (1) "off" means **fully absent** (not drawn, no collisions, no render cost — not dimmed); the per-subsystem draw-skip seam is expected **not** to exist yet and building it is part of 15.A; (2) scroll/zoom binds to **`ScrollSpeed` alone**; (3) "LAVA:"/"WAVE:" labels are **light** (green is reserved for interactive elements); (4) toggles are **iconographic lamp toggles** (lit/unlit), **green** because interactive; (5) tint opacity is **mild** and resolves from a **single token/constant** (one point of change, no repeated magic number). **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`. --- diff --git a/product-notes/phase-15-visualizer-controls-enhancements.md b/product-notes/phase-15-visualizer-controls-enhancements.md index e72aa58..39dc079 100644 --- a/product-notes/phase-15-visualizer-controls-enhancements.md +++ b/product-notes/phase-15-visualizer-controls-enhancements.md @@ -1,7 +1,8 @@ # 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.** +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** @@ -87,8 +88,10 @@ change; the centering and chrome are the polish around it. `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 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 @@ -106,8 +109,10 @@ 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. +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`. @@ -121,7 +126,8 @@ 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: +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`. @@ -136,9 +142,8 @@ 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.) + 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 @@ -201,8 +206,11 @@ the documented fallback — note it and escalate rather than hand-rolling a fixe - 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. + "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` @@ -228,6 +236,24 @@ still cannot reach the panel. Panel chrome **stays in the global `deepdrft-style ## 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.** @@ -237,7 +263,7 @@ corners, a lighter-navy ground, a thin light border.** | 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.** | +| 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 @@ -262,12 +288,17 @@ 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. +`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. @@ -321,11 +352,12 @@ keep their types. 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.) +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- @@ -333,27 +365,27 @@ Phase-12, this is moot; if it survives, align it to light too for consistency. F --- -## 10. Open questions for Daniel +## 10. Open questions for Daniel — all RESOLVED 2026-06-17 -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. +(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.) --- @@ -371,13 +403,17 @@ Phase-12, this is moot; if it survives, align it to light too for consistency. F 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. +6. **Widget types:** scroll/zoom is a slider bound to `ScrollSpeed` alone; 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 visibly removes that subsystem from - the visualizer; the state survives SPA nav and resets on fresh load (DI-scoped state). Both default on. +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. @@ -392,12 +428,16 @@ to ship as a single PR if Daniel prefers (recommend one bundled PR — the track 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) +### 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` - (resolves OQ §10.1 with staff-engineer). +- 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). + 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