docs(phase-10): respec Mix visualizer controls as in-flow container between back link and lava-lamp

This commit is contained in:
daniel-c-harvey
2026-06-16 20:05:59 -04:00
parent f7366b167c
commit b7a60f24c5
2 changed files with 129 additions and 49 deletions
@@ -66,8 +66,12 @@ Cross-references (read these before implementing):
- `DeepDrftPublic.Client/Controls/MixZoomMapping.cs` — reused unchanged for the scroll-speed control.
- `DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor[.cs/.css]` — the bridge. Extend the handle
with the new control setters; the `.css` gains the overflow-clip work (§2).
- `DeepDrftPublic.Client/Pages/MixDetail.razor[.css]` — the page; the controls area gains the inline
collapse/expand knob-bar (§7).
- `DeepDrftPublic.Client/Pages/MixDetail.razor[.css]` — the page; the controls move into an **in-flow
container between the back link and the lava-lamp** on the detail top row (§7b). The old
`.mix-visualizer-controls-anchor` + `position:absolute` floating structure is **removed**.
- `DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor[.cs]` — the detail chrome; gains a new
optional **`TopRowCenter`** slot so the top row hosts `back | center-controls | action` (§7b). The slot
stays null for Track/Cut/Session, which render the back link alone — reusability preserved.
- `DeepDrftPublic.Client/Pages/SessionDetail.razor[.css]` — the **NowPlaying / hero aesthetic** the
knob-bar must match (§7e).
- `DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor[.css]` — the footer/player bar
@@ -85,8 +89,9 @@ Cross-references (read these before implementing):
metaballs in the existing fragment shader, sharing the *same plane* as the waveform **with real 2D
collision** — the waveform pushes the fluid out of its way. Replace the navy↔moss treatment with a
**three-color, OKLab-interpolated, per-segment-baked gradient** that animates along three combined
motions. Replace the four-knob popover with a **seven-knob inline collapse/expand knob-bar** styled to
match the hero NowPlaying aesthetic. Remove the static noise texture that makes the screen look dirty.
motions. Replace the four-knob popover with a **seven-knob in-flow controls container that sits between
the back link and the lava-lamp toggle** on the detail top row (reflowing the layout in place — not a
floating/absolutely-positioned bar), styled to match the hero NowPlaying aesthetic. Remove the static noise texture that makes the screen look dirty.
Redraw the lava-lamp trigger glyph to the classic 1970s silhouette (§7f).
**In scope.**
@@ -100,8 +105,8 @@ Redraw the lava-lamp trigger glyph to the classic 1970s silhouette (§7f).
height (§6).
- **Seven controls** replacing the four: scroll speed, gradient rotation speed, lava gravity, lava heat,
blob density/size, collision strength, waveform width (§7).
- The **inline collapse/expand knob-bar** (replacing the popover) styled to the NowPlaying hero
aesthetic (§7).
- The **in-flow controls container between the back link and the lava-lamp** (replacing the popover, and
superseding the rejected `position:absolute` floating bar) styled to the NowPlaying hero aesthetic (§7b).
- A **redrawn `DDIcons.LavaLamp`** glyph — classic 1970s lava-lamp silhouette (§7f).
- **Overflow clipping** to the dynamic footer height (§2).
- **Removing the static noise texture** (§3).
@@ -133,7 +138,7 @@ Redraw the lava-lamp trigger glyph to the classic 1970s silhouette (§7f).
| Color model (HSL navy↔moss) | — | OKLab three-color gradient, 3 motions (§6) |
| Glass treatment | — | folded into the blob shading (§4f); no separate glass dials |
| Static noise/frost texture | — | **removed** (§3) |
| Controls (4 knobs, popover) | — | 7 knobs in an inline collapse/expand bar (§7) |
| Controls (4 knobs, popover) | — | 7 knobs in an in-flow container between back & lamp (§7) |
| Lava-lamp trigger glyph | trigger placement kept | glyph **redrawn** (§7f) |
---
@@ -534,41 +539,109 @@ live). Recommended starting points: scroll speed ~mid, rotation ~0.3, gravity ~0
~0.4, collision ~0.5, width ~0.6. The **~20% gravity / ~100% heat** anchor reflects Daniel's stated sweet
spot (§4c). These are feel-anchors, not commitments.
### 7b. The inline collapse/expand knob-bar (decided — NOT a popover or drawer)
### 7b. The in-flow controls container — between the back link and the lava-lamp (redesigned 2026-06-16)
**Decision (Daniel, 2026-06-16): the controls are an inline collapse/expand knob-bar, NOT a floating
popover or a drawer.** The seven RadialKnobs live **inline in the controls area** (where the controls sit
today) as an **`@if`-guarded flex row** that **animates open and closed in place**. It must read as
**part of the controls collapsing/expanding** — the knob row expanding out from its predecessor — **not**
a separate surface hanging off the icon.
**Decision (Daniel, 2026-06-16): the controls are an in-flow container that lives BETWEEN the back link
(left) and the lava-lamp toggle (right), on the detail top row. It is NOT a floating popover, NOT a
drawer, and NOT a `position:absolute` element anchored under the lamp.** Expanding it **reflows the
layout in place** — the container grows in the row's flow between back and lamp — it never overlays the
page, never clips, and never overlaps the masthead/hero.
**The mechanism (no MudPopover, no MudDrawer):**
> **This redesign supersedes the first realization of §7b.** The first implementation read the
> "inline collapse/expand" intent as an *absolutely-positioned floating bar anchored under the lamp*
> (`.mix-visualizer-controls-anchor` stacked the lamp over the bar; the bar was
> `position:absolute; top:calc(100% + 0.5rem); right:0; z-index:3`). That is a "floating-but-inline
> popover" — Daniel rejects it: it clipped to a vertical sliver (only ~4 of 7 knobs visible) and read as
> a detached window hanging off the icon. **Remove the `position:absolute` rule and the
> `.mix-visualizer-controls-anchor` floating structure entirely.** The container must take real layout
> space in the row, not float over it.
- A **bound `bool`** (e.g. `_controlsExpanded`) gates the knob row. The **lava-lamp icon button toggles
it** — the same `DDIcons.LavaLamp` trigger kept from Phase 10 §7c, now redrawn (§7f), now a
collapse/expand toggle instead of a popover anchor.
- When toggled open, the flex row of seven knobs **animates open** via **CSS transition** — width / opacity
/ transform — **expanding out from the icon / its predecessor element**, so it reads as the controls
growing in place. When toggled closed it collapses back the same way. (Seven knobs is wider than six —
see the wrap note below.)
- **Animation intent:** a smooth in-place expansion (expand-from-icon / slide-open flex row), reading as
one continuous collapse/expand of the controls area — not a panel popping into existence over the page.
Use the codebase's existing transition vocabulary (the existing controls/transition CSS) so the motion
feels native, not bolted on.
- **No floating surface.** There is no overlay, no anchored popover panel, no edge drawer. The knob row is
a real inline child of the controls area that occupies layout when open and collapses to nothing
(or to just the icon) when closed.
**Placement — restructure the scaffold's top row into three zones (recommended).**
**Why this over the Phase 10 popover.** The popover read as a detached panel hanging off the icon; Daniel
wants the controls to *be* the controls — collapsing and expanding in place. The inline animated flex row
keeps the seven knobs in the page's flow, makes the open/close a property of the controls themselves, and
avoids the popover/drawer overlay machinery entirely. (Prior-art touchstone: an inline "expand for
advanced settings" disclosure row — e.g. a toolbar that grows a secondary row of controls in place —
rather than a flyout/menu.)
`ReleaseDetailScaffold`'s top row is today a two-zone `MudStack Row Justify="SpaceBetween"`:
`back-link (left) | @TopRightAction (right)`. Restructure it into **three zones**:
**Layout note.** Seven knobs in a flex row may wrap on narrow viewports (e.g. 4×3 / 3×4 / 2-row) — a
layout call, but all seven must remain reachable, and the wrap must still read as part of the inline
collapse/expand (the whole block grows/shrinks), not as a separate surface.
```
back-link (left) | @TopRowCenter (the controls container) | @TopRightAction (the lamp)
```
- **Add a new optional `RenderFragment? TopRowCenter` slot** to the scaffold, rendered between the back
link and `@TopRightAction` on the same row. The row becomes a three-child flex row: back link pinned
left, lamp pinned right, the center slot occupying the space between (and growing/shrinking with the
container's expand/collapse — see motion below).
- **Mix** supplies the seven-knob `MixVisualizerControls` to `TopRowCenter` and keeps the lava-lamp
`MudIconButton` in `TopRightAction`. The lamp toggles the container; the container reveals in the
center zone.
- **Reusability holds.** Track / Cut / Session supply neither `TopRowCenter` nor `TopRightAction`, so the
row degrades to **back-link-alone**, exactly as today. The center slot is a generic "affordance between
back and action" seam — it follows the scaffold's existing "variance rides a slot, never a flag"
convention (Phase 9 §5.3), the same way `TopRightAction` and `TopContent` already do. With
`Justify="SpaceBetween"` an absent center slot collapses to nothing and back/action sit at the two
edges unchanged.
**Why a scaffold center slot over a MixDetail-composed row.** Composing the whole `back | controls | lamp`
row inside MixDetail (and suppressing the scaffold's own back row) would duplicate the back-link markup,
break the scaffold's "owns the back link" invariant, and fork the one place every medium's back
navigation lives. The center slot keeps the scaffold the single owner of the row and adds Mix's piece as
data, not as a structural fork. **Recommended: the scaffold gains the `TopRowCenter` slot.**
(`TopContent` — the existing below-the-row band — is the *responsive fallback target*, not the primary
home; see the responsive note in §7b-responsive below.)
**The expand/collapse mechanism — in-flow, no float, no overlay (no MudPopover, no MudDrawer):**
- A **bound `bool`** (`_controlsExpanded`) gates the container. The **lava-lamp icon button toggles it**
the same `DDIcons.LavaLamp` trigger kept from Phase 10 §7c, now redrawn (§7f), now an in-flow
expand/collapse toggle. The icon swaps to its FILLED variant while expanded (§7f / Part B); a knob drag
never collapses the container (the toggle flips only on the lamp's click).
- **Collapsed:** the container occupies **no or minimal space** in the center zone — `max-width: 0` (or
`width: 0`) + `opacity: 0` + `overflow: hidden`, and `visibility: hidden` + `pointer-events: none` so
the collapsed knobs are not focusable or hit-testable. Back and lamp sit at the row's two edges with the
center collapsed between them.
- **Expanded:** the container **grows horizontally in place** between back and lamp — a `max-width` (or
`width`) + `opacity` (+ optional slight `transform`) CSS transition — so the seven knobs reveal **in the
row's flow**, pushing nothing over the page. **No `position: absolute`, no `float`, no `z-index` stacking
hack, no anchored panel.** The row reflows to accommodate the container; when the container is wider than
the available center space it wraps in-flow (see responsive, below) — it never clips to a sliver.
- **Motion intent:** a smooth horizontal grow/shrink of a real in-flow element, reading as **the controls
area opening between the back link and the lamp** — not a panel appearing over the page. Reuse the
existing transition vocabulary (the `cubic-bezier(0.22, 0.61, 0.36, 1)` easing already in
`MixVisualizerControls.razor.css`) so the motion feels native. The `max-height` collapse can stay as a
secondary axis for the wrap case, but the **primary** animated axis is horizontal width in the row.
**Must not overlap the masthead/hero.** Because the container lives in the top row (above the masthead,
which the scaffold renders below this row) and reflows in-flow, an expanded container **pushes the row's
own height** — it must never paint over the masthead, the hero/cover, or the waveform backdrop. If the
expanded container is tall (wrapped to multiple knob rows on narrow widths), the top row grows and the
masthead moves down with normal document flow; no overlap, no clipping.
**Why this over the rejected floating bar.** The floating `position:absolute` bar read as a detached
window hanging off the lamp and clipped because its width was constrained by the lamp's narrow column.
An in-flow container between back and lamp *is* the controls sitting in the layout — the open/close is a
property of the row reflowing, the seven knobs get the row's full horizontal space, and there is zero
overlay machinery. (Prior-art touchstone: a toolbar that grows a secondary in-flow region of controls in
place between two pinned end-affordances — e.g. a browser address bar revealing inline controls between
the back button and the menu — not a flyout anchored to an icon.)
### 7b-responsive. Narrow-width behavior — stay in-flow, never clip
The seven knobs are wide; on a narrow row the center zone cannot hold them on one line. **Recommended:
the container wraps to a second in-flow line under the back/lamp row** rather than clipping or scrolling
off-edge. Two acceptable realizations (staff-engineer's call, both in-flow):
1. **Flex-wrap in place.** The seven-knob flex row `flex-wrap: wrap`s within the center zone; when the
center zone is too narrow the whole top row grows taller and the knobs reflow to 4×3 / 3×4 / two rows.
The row's height grows in-flow; the masthead moves down. All seven stay reachable.
2. **Drop to the `TopContent` band when narrow.** Below a breakpoint, the container renders in the
scaffold's existing **`TopContent` slot** (the full-width in-flow band below the back/lamp row, above
the masthead) instead of the center zone — giving it the full container width to lay the seven knobs
out without crowding the back/lamp. This is still fully in-flow (it is literally the `TopContent`
position) and never floats. Use the center zone on wide rows, the `TopContent` band on narrow rows.
**Either way: in-flow, no overlay, no edge-clipping, no horizontal-scroll-that-hides-knobs.** A
horizontally-scrolling inline strip is a *last-resort* fallback only if wrapping proves visually worse —
and even then it must be a real in-flow scroll region with a visible affordance, never a clipped sliver.
All seven knobs must always be reachable. The wrap/drop must still read as part of the controls
container opening, not as a separate surface.
### 7c. State — widen to seven properties
@@ -746,9 +819,13 @@ convention, and the theme-alignment discipline — not the exact coordinates.)
19. **Waveform width.** Dragging the waveform-width control narrows/widens the waveform band across its
range; narrowing it visibly clears horizontal space for the lava (and shrinks the collision boundary
the fluid parts around). The datum/scroll geometry is otherwise unchanged.
20. **Inline collapse/expand.** Clicking the lava-lamp icon expands the seven-knob flex row **in place**
(animated open/closed via CSS transition), reading as the controls collapsing/expanding — **not** a
floating popover or drawer. Clicking again collapses it; dragging a knob does not collapse it.
20. **In-flow container between back and lamp.** The controls container sits **inline on the detail top
row, between the back link (left) and the lava-lamp toggle (right)**. Clicking the lava-lamp icon
expands it **in the layout flow** (animated open/closed via CSS width/opacity transition), reflowing
the row — **not** a floating popover, drawer, or `position:absolute` bar, and **not** clipped to a
sliver. Collapsed it takes no/minimal space (back and lamp at the row's two edges). All seven knobs are
visible and horizontal when expanded; the expansion never overlaps the masthead/hero. Clicking again
collapses it; dragging a knob does not collapse it.
21. **NowPlaying aesthetic.** The knob-bar's surface color + structure match the session hero now-playing
overlay (translucent dark glass, overlay-label typography, `Color.Secondary` accents).
22. **Persistence + read-only.** All seven positions survive SPA nav within a session, reset on fresh
@@ -795,13 +872,15 @@ sourced from `DeepDrftPalettes` with no hardcoded duplicates. **Acceptance:** §
per-segment-bake requirement (§6b motion 2) uses the mix-time-keyed realization so it travels with the
segment by construction (decided — §6b).
### Wave R4 — Seven controls + the NowPlaying-styled inline knob-bar
### Wave R4 — Seven controls + the NowPlaying-styled in-flow controls container
Widen `MixVisualizerControlState` to **seven** properties (including the new **waveform width** control,
§7a); replace the four-knob popover with the seven-knob **inline collapse/expand knob-bar** (§7b —
`@if`-guarded animated flex row, lava-lamp icon toggles it, no popover/drawer); wire the waveform-width
control to scale the waveform-band extent and its collision boundary (§7a); style it to the session-hero
aesthetic; extend the bridge handle with the new setters (including `setWaveformWidth`). **Acceptance:**
§8 #18#22.
§7a); add the scaffold's **`TopRowCenter`** slot and move the seven-knob `MixVisualizerControls` into an
**in-flow container between the back link and the lava-lamp** (§7b — animated horizontal grow/shrink in
the row's flow, lava-lamp icon toggles it, **no popover/drawer, no `position:absolute`**; **remove** the
old `.mix-visualizer-controls-anchor` + `position:absolute` floating structure); handle the narrow-width
in-flow wrap/drop (§7b-responsive); wire the waveform-width control to scale the waveform-band extent and
its collision boundary (§7a); style it to the session-hero aesthetic; extend the bridge handle with the
new setters (including `setWaveformWidth`). **Acceptance:** §8 #18#22.
**Dependency shape:** `Wave R1 → Wave R2 → (Wave R3 ‖ Wave R4)`. Wave R1 is a quick unblock (and folds in
the independent icon redraw). Wave R2 is the prerequisite for everything visual. Waves R3 (color) and R4
@@ -814,8 +893,9 @@ ranges/transfer-functions by hand once on screen throughout (his standing prefer
## 10. Open items
Tuning knobs, one undecided behavior call, and one deferred future idea — the controls-UI fork and the
per-segment-storage fork are both now **decided** (§7b inline knob-bar; §6b mix-time-keyed). None of the
items below block starting Wave R1.
per-segment-storage fork are both now **decided** (§7b in-flow controls container between back & lamp —
redesigned 2026-06-16, superseding the rejected `position:absolute` floating bar; §6b mix-time-keyed).
None of the items below block starting Wave R1.
- **§4c, §5d — transfer functions:** heat 0..1 → rise/morph intensity; collision 0..1 → soft↔hard blend
shape; restitution coefficients; penetration-penalty curve. All staff-engineer tuning tasks with the