Files
deepdrft/product-notes/phase-17-player-queue-view.md
T

421 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 17 — Player-Bar Queue View
Status: **design spec — 4 of 11 open questions resolved (Daniel, 2026-06-19); 7 still pending.** Author: product-designer. Date: 2026-06-19.
**Plan only — no code has been written by this doc.**
**Resolved (Daniel, 2026-06-19):** OQ1 → **Option A, conditional** (collapse/expand toggle *if* dynamic
iframe resize is achievable in the embed snippet; **else fall back to Option B**, omit the button —
feasibility call made during 17.3). OQ4 → **`MudDropContainer` for now** (pivot to a touch-viable
mechanism later if mobile issues surface; C6 softened — touch-viability is a known risk, not a pre-ship
blocker). OQ8 → **pure append, confirmed** (add ≠ play; first add into a dormant queue leaves a coherent
`CurrentIndex` via a small additive engine affordance, landing with wave 17.1). OQ10 → **deferred,
confirmed** (cards get no Add-to-Queue in Phase 17; the deferred card work is captured in `TODO.md`).
The remaining seven (OQ2, OQ3, OQ5, OQ6, OQ7, OQ9, OQ11) are **still pending** — their §10
recommendations stand but are not confirmed.
This phase makes the play-queue **visible and manipulable**. Phase 11 (wave 11.F) built the queue
*engine* (`IQueueService` / `QueueService`) and wired auto-advance, skip-prev/next, and release
embeds — but the queue itself has **no UI**. A listener can start an album and skip through it, but
cannot *see* what is next, reorder it, drop a track, or hand-build a queue. This phase surfaces it.
Cross-references (read before implementing):
- `PLAN.md §17` — the concise phase entry.
- `DeepDrftPublic.Client/Services/IQueueService.cs` / `QueueService.cs` — the engine this phase
drives. Its docstrings **already anticipate** reorder + removal as additive members (see §1).
- `DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor[.cs]` — the bar that gains the
Queue button + (Fixed) the embedded panel.
- `DeepDrftPublic.Client/Controls/AudioPlayerBar/PlayerControls.razor` / `PlayerTransportZone.razor`
— where transport buttons live; the Queue button sits relative to these.
- `DeepDrftPublic.Client/Controls/AudioPlayerProvider.razor[.cs]` — cascades the queue (`IsFixed`).
**Player + queue state stays here, at layout level** (project memory; do not move into pages).
- `DeepDrftPublic.Client/Pages/FramePlayer.razor` + `Layout/EmbedLayout.razor` — the Fixed/embed
surface; the iframe-host sizing constraint lives here (§4).
- `DeepDrftPublic.Client/Controls/SharePopover.razor` / `WaveformVisualizerControlPopover.razor`
the established overlay/dismissal idioms to borrow for the non-Fixed overlay (§3).
- `DeepDrftPublic.Client/Pages/CutDetail.razor`, `Controls/ReleaseHeroOverlay.razor`,
`Controls/ReleaseGallery.razor`, `Controls/StreamNowButton.razor` — the play-affordance sites the
"Add to Queue" button (§5) attaches beside.
---
## 0. State it inherits (verified 2026-06-19)
The queue engine is **mature and complete for orchestration**:
- `IQueueService` exposes `Items` (`IReadOnlyList<TrackDto>`), `CurrentIndex`, `Current`, `HasNext`,
`HasPrevious`, `IsArmed`, the `QueueChanged` event, and commands `PlayRelease`, `Arm`, `Start`,
`Enqueue`, `EnqueueRange`, `Next`, `Previous`, `Clear`. **There is no reorder or remove member
yet** — but the interface docstring explicitly reserves them as additive ("reordering mutates
`Items` and re-emits `QueueChanged`"). This phase adds them.
- The queue is cascaded from `AudioPlayerProvider` as an `IsFixed` cascade alongside the player.
- `AudioPlayerBar` already subscribes to `QueueChanged` to re-render skip affordances, and reads
`HasNext`/`HasPrevious`. Adding a Queue button + panel is the same subscription, more rendering.
- **Fixed** (`AudioPlayerBar.Fixed`) is the embed flag, set true by `FramePlayer` via `EmbedLayout`.
In Fixed mode the bar is in normal flow (no minimize/spacer/clip); the host iframe is sized by the
embed snippet (`EmbedSnippetBuilder`). Today a release embed arms a shared queue; a single-track
embed leaves the queue empty.
- **"Shared queue" today = a multi-track release** loaded via `PlayRelease` (docked) or `Arm`/`Start`
(embed). The queue is *populated from one release's ordered tracks*. There is no hand-assembled
multi-source queue yet — `Enqueue`/`EnqueueRange` exist on the interface but **no UI calls them**.
The gap this phase closes: the queue is a black box. You can drive it (skip) but not inspect or edit
it. Item 4 ("Add to Queue") also activates the dormant `Enqueue` path — the first time the queue
becomes a thing a listener *builds*, not just a release they *play*.
---
## 1. Goal
Make the queue a first-class, visible, editable object in the player bar — without moving any player
or queue state out of the layout-level provider, and without changing the streaming/playback seam.
Three user-visible capabilities, gated by a single new **Queue** toggle button in the bar:
1. **See** the queue — an ordered list of what is queued, with the current track marked.
2. **Edit** the queue (where editing is allowed) — drag-reorder and per-track removal.
3. **Grow** the queue — an "Add to Queue" affordance everywhere there is a play button.
The two view modes (overlay vs. embedded panel) differ only in **presentation and edit-permission**,
not in the data they read — both bind the same cascaded `IQueueService.Items` (project memory: *one
source, multiple views*).
---
## 2. Constraints (hard)
- **C1 — Player/queue state stays at layout level.** The Queue view reads the cascaded
`IQueueService`; it does not own or relocate queue state. No page-level player state. (Project
memory.)
- **C2 — No new playback semantics.** Reorder/remove mutate `Items` + `CurrentIndex` and re-emit
`QueueChanged`; they must **not** restart, re-stream, or interrupt the currently-playing track
(matching the engine's existing "the player stays a single-track device" posture). Reordering the
*currently-playing* item or items around it must keep playback uninterrupted.
- **C3 — Embed = read-only shared queue.** In Fixed (embed) mode, a shared (release) queue is
**fixed-order and non-editable**: no drag, no remove. The panel is a *display* of the up-next list.
- **C4 — The engine is extended additively, never reshaped.** New members (`Move`/`RemoveAt`) are
added to `IQueueService`; existing members and their contracts are untouched. Consumers written
against today's surface keep working (the interface's stated open/closed posture).
- **C5 — Reorder/remove are interop-free state mutations.** Like `Enqueue`/`Arm`, they touch only the
in-memory list + index + `QueueChanged`. They run safely during prerender. Only `PlayRelease`/
`Start`/`Next`/`Previous` touch JS.
- **C6 — Drag-and-drop should be touch-viable (softened — OQ4 resolved).** The public site's primary
listening surface includes mobile (per `PLAN.md §1.7`). **Resolved (Daniel, 2026-06-19): ship with
`MudDropContainer` now.** Touch-viability is **no longer a hard pre-ship blocker** — it is a known
risk with a planned pivot path: if mobile/touch issues surface in practice, pivot to a touch-viable
mechanism (pointer-interop reorder, matching `RadialKnob`'s `capturePointer`, or an up/down-arrow
fallback) in a later pass. See OQ4 (resolved).
- **C7 — Embedded panel must not break the iframe host.** Adding the panel in Fixed mode grows the
player's intrinsic height; the embed snippet's iframe dimensions must account for it (§4, OQ6).
---
## 3. Item 1+2 — Queue button & non-Fixed (overlay) mode
### 3.1 The Queue button
A new icon button in the player bar, shown **only when a queue is loaded** (`Items.Count > 0`).
Mirror the existing skip-affordance gating: with no queue (null) or empty queue, the button is
absent, so the bar is byte-for-byte its pre-queue self for single-track play.
- **Placement (Daniel):** to the **left of the timestamps and below the other player control
buttons.** Today `PlayerTransportZone` is a vertical `MudStack` of (transport-button row) over
(`TimestampLabel`). The Queue button sits **between** those two — a second row below the
play/skip controls and above the timestamp. That literally satisfies "below the control buttons,
to the left of the timestamps."
- **Glyph:** `Icons.Material.Filled.QueueMusic` (or a bespoke `DDIcons` queue glyph if Daniel wants
the hand-rolled aesthetic — **OQ7**). `Color.Primary` to match the transport row.
- **Active state:** when the overlay/panel is open, the button reads active (filled/highlighted),
matching the visualizer popover idiom.
### 3.2 Non-Fixed overlay panel
When the bar is **not Fixed** (the docked public player), clicking Queue toggles a **screen-centered
overlay** — borrow the exact idiom Phase 15 settled on for the visualizer controls: `MudOverlay`
(`DarkBackground`, `Modal`), panel stops click propagation, scrim-click closes, drag-safe (a
`position:fixed` drag capture sits above the scrim so a drag that ends outside the panel does not
dismiss). **Not** a `MudPopover` anchored to the bar — a centered modal is the proven pattern here
and avoids anchor math against a `position:fixed` dock.
- **Shape:** "mostly square" (Daniel) — a panel roughly `min(90vw, 520px)` square, scrollable track
list inside. Chrome matches `NowPlayingCard` / the visualizer panel (square corners, lighter-navy,
thin border) for family resemblance.
- **Contents:** ordered list of `Items`. Each row: drag handle, track number/position, track
name + artist, a now-playing marker on `Current` (index == `CurrentIndex`), a remove (×) button.
Clicking a row's body jumps playback to that track (`PlayRelease(Items, thatIndex)` — reuses the
existing "play from index" semantics; **OQ2** confirms this is desired vs. a dedicated jump).
- **Reorder:** drag-and-drop reordering of rows → `IQueueService.Move(from, to)` (new member, §6).
- **Remove:** per-row ×`IQueueService.RemoveAt(index)` (new member, §6).
- **Header:** title ("Queue" / "Up Next"), optional "Clear" action (calls existing `Clear()`;
**OQ5** — does Clear belong in the UI, and does it stop playback?).
- **Empty state:** the button is hidden when empty, so the overlay is never opened empty by the
button. But removal *can* empty a non-empty queue mid-session — define the terminal state (**OQ3**).
### 3.3 Borrowed precedent
The overlay shell, dismissal, and drag-safety are a **direct lift** of
`WaveformVisualizerControlPopover` (Phase 15 §4). The "mostly square panel, family chrome" is the
`NowPlayingCard` look. Reuse, don't reinvent.
---
## 4. Item 3 — Fixed (embedded) mode
When the bar **is** Fixed (the `/FramePlayer` iframe embed), the queue list is **always shown below
the existing player-bar controls** — not a toggle overlay. It is part of the embed's standing layout.
- **Permissions (C3):** the embedded shared (release) queue is **fixed-order, non-editable** — no
drag handles, no remove buttons. It is a read-only up-next display: ordered track list, current
track marked, future tracks listed. Tapping a future row *may* jump to it (**OQ2** — even read-only
queues could allow jump-to-track; reorder/remove are the forbidden ops, jump is arguably fine).
- **Container resize (C7):** the Fixed bar gains vertical height for the panel. Two coupled changes:
1. The `EmbedLayout` / `FramePlayer` Fixed bar renders the panel inline below the controls.
2. `EmbedSnippetBuilder.ForRelease(...)` must mint an iframe tall enough to show the panel.
`ForTrack(...)` stays at today's compact height (single-track embeds have no queue, so no
panel). **This makes the two embed snippets diverge in height** — track-embed compact,
release-embed taller. That is correct: only release embeds have a queue to show. (**OQ6** —
fixed taller height for all release embeds, vs. a height that scales with track count up to a
cap. Recommend: a fixed sensible height with the panel internally scrollable past N rows.)
### 4.1 The Queue button in embed mode (Daniel asked to clarify)
**Recommendation:** in Fixed mode the panel is always shown, so the Queue button is **not needed for
toggling** — but rather than hide it, **repurpose it as a collapse/expand control** for the panel so a
listener can reclaim the iframe space. Two viable readings, pick one:
- **Option A (resolved — preferred) — collapse toggle.** Queue button is present in Fixed mode and
collapses/expands the always-default-open panel. Default = expanded. Gives the embedder's viewer
control. Consistent button meaning across modes ("toggle the queue view").
- **Option B (fallback) — omit the button in Fixed mode entirely.** Panel is always open, no toggle.
Simpler, but the button's meaning then differs by mode (present+toggles-overlay when docked, absent
when embedded), and a viewer can't reclaim space.
**OQ1 — RESOLVED (Daniel, 2026-06-19): Option A, *conditional on feasibility*.** Use Option A **if**
the iframe can be dynamically resized appropriately for collapse/expand — e.g. an embed-snippet
`postMessage` → host resize handshake so the host iframe shrinks when the panel collapses and grows
when it expands. **Otherwise fall back to Option B** (omit the button, panel always open at fixed
height). **A is preferred; B is the fallback; the deciding factor is whether dynamic iframe resize is
achievable in the embed snippet.** This A-vs-B determination is **made during 17.3 implementation**
record it there. Note the interaction with OQ6 (embed panel height): a collapse/expand that does *not*
resize the iframe (the earlier "iframe stays sized for expanded; collapsed just shows less" reading) is
**not** Option A under this resolution — Option A requires the iframe to actually resize. A non-resizing
collapse is cosmetic and does not let the viewer reclaim space, so it collapses into Option B's value.
---
## 5. Item 4 — "Add to Queue" affordance
An **"Add to Queue" icon button with a tooltip** beside **every play button for a release or track.**
- **Glyph + tooltip:** `Icons.Material.Filled.QueueMusic` / `PlaylistAdd` with `MudTooltip`
"Add to queue" (track) / "Add release to queue" (release). (**OQ7** — Material vs. bespoke glyph.)
- **Behavior:**
- **Track context:** `IQueueService.Enqueue(track)` — appends without disturbing current playback
(existing member, no UI calls it today; this lights it up).
- **Release context:** `IQueueService.EnqueueRange(orderedTracks)` — appends the whole release's
ordered track list (existing member).
- **First-add semantics — RESOLVED (Daniel, 2026-06-19): pure append (option (a)).** Add-to-Queue is
explicitly *not* play; keep the verbs distinct. If the queue is **empty/dormant** (`CurrentIndex ==
-1`), the first add appends and **leaves a coherent `CurrentIndex`** (the small additive engine
affordance) so the next play/skip behaves correctly — but **does not auto-play**. The listener still
presses play to start. (Options (b) "first add also stages/sets current without auto-playing" and (c)
"first add behaves like `PlayRelease`/play" are declined — they blur add vs. play.) **This engine
tweak belongs with wave 17.1** (it is part of the `Enqueue`-into-empty index handling, alongside the
`Move`/`RemoveAt` additions). See **OQ8 (resolved)**.
### 5.1 Where the button attaches (inventory)
Every site that currently has a play affordance:
| Site | Current play affordance | Add-to-Queue target |
|---|---|---|
| `CutDetail` header | "Play" `MudButton` (whole album) | release → `EnqueueRange` |
| `CutDetail` track rows | per-row `PlayStateIcon` | track → `Enqueue` |
| `SessionDetail` / `MixDetail` | `PlayContent` slot in `ReleaseHeroOverlay` (single track) | track → `Enqueue` |
| `StreamNowButton` | random-track stream trigger | **edge case — OQ9** (random has no fixed track until resolved) |
| `ReleaseGallery` cards | **none today** (cards are pure links) | **scope question — OQ10** |
- **`ReleaseGallery` cards have no play button today** — they navigate to detail. Adding an
Add-to-Queue button there is *new* affordance, out of the literal brief ("anywhere there is a play
button"). **OQ10 — RESOLVED (Daniel, 2026-06-19): deferred, confirmed.** Cards get **no**
Add-to-Queue affordance in Phase 17. Scope item 4 to the detail-page play sites (rows 13 above)
where a play button genuinely exists today. The deferred work — giving cards a play + Add-to-Queue
affordance (a card-redesign question) — is captured as a `TODO.md` entry.
- **`StreamNowButton`** is a "surprise me" trigger, not a fixed track — Add-to-Queue has no concrete
item until a track resolves. Recommend: **exclude it** from item 4. **OQ9.**
So the **grounded scope** of item 4: the `CutDetail` header + track rows, and the Session/Mix hero
play slot. That is "every play button that targets a known release or track."
---
## 6. Engine additions (the only `IQueueService` changes)
Two new members, both interop-free state mutations that re-emit `QueueChanged`:
- **`void Move(int fromIndex, int toIndex)`** — reorders `Items`; adjusts `CurrentIndex` so the
*same track* stays current across the move (find current track's new position). No re-stream
(C2). No-op on out-of-range or equal indices.
- **`void RemoveAt(int index)`** — removes `Items[index]`.
- Removing a track **after** current: just remove; `CurrentIndex` unchanged.
- Removing a track **before** current: remove + decrement `CurrentIndex` (current track stays the
same item, new lower index). Playback uninterrupted.
- Removing the **current** track: **define** — recommend stop is *not* triggered (C2 says mutations
don't touch playback), so the playing track keeps playing to its natural end even though it's no
longer in the list, and `CurrentIndex` points at what is now at that slot (the next track) so the
next auto-advance / skip is coherent. Edge: removing current when it's the last item → queue
becomes shorter; auto-advance simply has nothing to advance to (existing behavior). **OQ3/OQ11.**
- Removing the **last remaining** track → empty queue (`CurrentIndex = -1`, dormant). The Queue
button disappears; if the overlay is open it closes (or shows empty state then closes). **OQ3.**
Both are **additive** (C4). Unit-testable against the existing `QueueServiceTests` fake-player
harness with no container (the engine's stated design intent).
---
## 7. Use cases
- **UC1 — Inspect up-next (docked).** Listener playing an album opens the Queue overlay, sees the
ordered remaining tracks with the current one marked, closes it. No edit.
- **UC2 — Reorder (docked).** Listener drags track 5 above track 2; playback continues uninterrupted;
the new order drives the next auto-advance.
- **UC3 — Remove (docked).** Listener removes a track they don't want; if it was upcoming, it's gone
from the up-next; if it was current, current keeps playing to natural end (per §6).
- **UC4 — Build a queue (docked).** Listener on a Cut detail page clicks "Add to Queue" on three
individual tracks across two albums, then opens the queue and plays from the top.
- **UC5 — Embed up-next (Fixed).** A blog embeds a release; the iframe shows the player *and* the
fixed-order track list below it; viewer sees what's coming, cannot reorder/remove; (Option A)
collapses the panel to reclaim space.
- **UC6 — Single-track embed unchanged.** A single-track embed shows no Queue button and no panel —
the compact iframe is byte-for-byte today's.
- **UC7 — Jump to track.** Listener clicks a queued row → playback jumps to that track and continues
from there (if OQ2 = yes).
---
## 8. Acceptance criteria
- **AC1** — When `Items.Count == 0`, the Queue button is absent and the docked bar renders exactly as
it does today (no layout shift, no empty panel).
- **AC2** — When `Items.Count > 0` and not Fixed, the Queue button appears below the transport
controls and to the left of the timestamp; clicking it opens a centered, mostly-square overlay
listing the queue with the current track marked.
- **AC3 (docked)** — Drag-reordering a row changes the queue order and is reflected in subsequent
skip/auto-advance; the currently-playing track is **not** re-streamed or interrupted by any reorder.
- **AC4 (docked)** — Removing an upcoming track removes it from the list and from auto-advance;
removing a track before the current one keeps the same track playing; removing the current track
does not stop playback (per §6 decision).
- **AC5 (Fixed)** — In a release embed, the queue panel renders below the controls by default, shows
the ordered tracks with the current marked, and exposes **no** drag handles or remove buttons.
- **AC6 (Fixed)** — The release embed iframe (via `EmbedSnippetBuilder.ForRelease`) is tall enough to
show the panel without clipping; the single-track embed (`ForTrack`) height is unchanged.
- **AC7** — An "Add to Queue" button with tooltip appears beside the Cut header Play, each Cut track
row's play, and the Session/Mix hero play; clicking it appends the track/release to the queue
without disturbing the currently-playing track.
- **AC8** — Add-to-Queue on a dormant (empty) queue leaves the queue populated with a coherent
`CurrentIndex` such that the next play/skip behaves correctly (per OQ8 resolution).
- **AC9** — `Move` and `RemoveAt` are additive to `IQueueService`; all existing `QueueServiceTests`
pass unchanged; no existing member's behavior changes.
- **AC10** — All queue-view rendering and the new engine mutations run during prerender without JS
interop (no prerender exceptions); only playback transitions touch JS.
---
## 9. Test cases
Engine (`QueueServiceTests`, fake-player harness — no container):
- **T1** `Move(2, 0)` on a 4-item queue reorders correctly; `Items` reflects new order;
`QueueChanged` fired once.
- **T2** `Move` keeps the *current track* current: with `CurrentIndex` at track X, moving other items
around it leaves `Current` == X; moving X itself updates `CurrentIndex` to X's new slot.
- **T3** `Move` does not call the player's stream method (assert fake player saw no `SelectTrackStreaming`).
- **T4** `RemoveAt` after current: `CurrentIndex` unchanged, item gone.
- **T5** `RemoveAt` before current: `CurrentIndex` decremented, same track current.
- **T6** `RemoveAt` of current: player not stopped (per §6); `CurrentIndex` resolves to the new
occupant or `-1` if it was last.
- **T7** `RemoveAt` of last remaining item → empty + dormant (`CurrentIndex == -1`, `Items` empty,
`QueueChanged` fired).
- **T8** `Move`/`RemoveAt` out-of-range / no-op cases do not throw and do not fire `QueueChanged`.
- **T9** `Enqueue` into a dormant queue then play behaves per OQ8 resolution.
- **T10** Reordering the currently-playing item to a new slot leaves auto-advance pointing at the
correct next track when the current ends.
Component (if/when component test coverage is in scope — today none exists for the client, per
`DeepDrftPublic.Client/CLAUDE.md`):
- **T11** Queue button absent when queue empty (render assertion).
- **T12** Overlay opens/closes via button + scrim; a simulated drag that ends on the scrim does not
dismiss.
- **T13** Fixed-mode panel renders inline with no drag/remove controls.
- **T14** `EmbedSnippetBuilder.ForRelease` height differs from `ForTrack` (unit test on the builder).
---
## 10. Open questions for Daniel
**Four resolved (Daniel, 2026-06-19): OQ1, OQ4, OQ8, OQ10. Seven still pending: OQ2, OQ3, OQ5, OQ6,
OQ7, OQ9, OQ11 — their recommendations below stand but are not confirmed.**
- **OQ1 — Queue button in embed (Fixed) mode. RESOLVED → Option A, conditional.** Use Option A
(collapse/expand toggle) **if** the iframe can be dynamically resized for collapse/expand (embed-snippet
`postMessage` → host resize handshake); **else fall back to Option B** (omit the button). A preferred,
B fallback; deciding factor = whether dynamic iframe resize is achievable in the embed snippet;
determination made during **17.3** (see §4.1).
- **OQ2 — Click-to-jump.** Does clicking a queued row jump playback to that track? Recommend **yes**
in both modes (reuses `PlayRelease(Items, index)`); jump is not a forbidden edit, only reorder/
remove are. Confirm for the read-only embed too.
- **OQ3 — Terminal/empty states.** When removal empties the queue mid-session: close the overlay?
Show an empty state then auto-close? And when the *current* track is removed, what should the panel
and the player show?
- **OQ4 — Drag-and-drop mechanism & touch. RESOLVED → `MudDropContainer` for now.** Ship with
MudBlazor's `MudDropContainer`. If mobile/touch issues surface in practice, pivot to a touch-viable
mechanism later (pointer-based interop reorder matching `RadialKnob`'s `capturePointer`, or an
up/down-arrow fallback). C6 softened: touch-viability is a known risk with a planned pivot path, **not**
a hard pre-ship blocker (see §2 C6).
- **OQ5 — "Clear queue" in the UI.** Include a Clear action in the overlay header? Does Clear stop
playback or just empty the up-next (engine's `Clear` does not stop the player today)?
- **OQ6 — Embed panel height.** Fixed sensible height with internal scroll past N rows (recommend),
vs. height that grows with track count up to a cap. Affects `EmbedSnippetBuilder.ForRelease`.
- **OQ7 — Glyph.** Material `QueueMusic`/`PlaylistAdd`, or a bespoke hand-rolled `DDIcons` queue
glyph to match the gas-lamp/lava-lamp family aesthetic?
- **OQ8 — Add-to-Queue into a dormant (empty) queue. RESOLVED → pure append.** Add ≠ play; first add
into a dormant queue appends and leaves a coherent `CurrentIndex` (small additive engine affordance)
so the next play/skip behaves correctly, but does **not** auto-play. The engine tweak lands with wave
**17.1**. (first-add-stages and first-add-plays both declined — see §5.)
- **OQ9 — `StreamNowButton`.** Exclude from Add-to-Queue (recommend — no fixed track until resolved)?
- **OQ10 — `ReleaseGallery` cards. RESOLVED → deferred, confirmed.** Browse-grid cards get **no**
Add-to-Queue affordance in Phase 17. Item 4 is scoped to the detail-page play sites (Cut header, Cut
track rows, Session/Mix hero). The deferred card work (play + Add-to-Queue, a card-redesign question)
is captured in `TODO.md`.
- **OQ11 — Removing the current track.** Confirm the recommended "keep playing to natural end, don't
stop" behavior (C2-consistent) vs. "removing current skips to next immediately."
---
## 11. Wave decomposition (implementation sequencing)
Three waves; **17.1 is the load-bearing prerequisite** (engine + the shared queue-list view that both
modes render). 17.2 and 17.3 then hang off it and are largely parallel.
- **17.1 — Engine additions + shared queue-list view.** Add `Move`/`RemoveAt` to `IQueueService`/
`QueueService` (+ `QueueServiceTests`). **Plus the OQ8 affordance:** `Enqueue` into a dormant/empty
queue leaves a coherent `CurrentIndex` (pure append, no auto-play) so the next play/skip is correct.
Build a single presentational `QueueList` component that renders `Items` with the current marked and
takes an `Editable` flag (drag/remove on when true; reorder via `MudDropContainer` per OQ4). This is
the *one source* both modes consume. **Cold-start. Gates 17.2 + 17.3.** No bar changes yet.
- **17.2 — Non-Fixed overlay.** Queue button in the bar (gated on `Items.Count > 0`, placed per
§3.1), the centered overlay borrowing the visualizer-popover idiom, hosting `QueueList Editable`.
Reorder/remove/jump wired. **Depends on 17.1.**
- **17.3 — Fixed embed panel + snippet resize + Add-to-Queue.** Inline `QueueList` (non-editable)
below the Fixed bar; **the OQ1 feasibility determination is made here** — implement Option A
(collapse/expand button + iframe resize handshake) **if** dynamic iframe resize proves achievable in
the embed snippet, else fall back to Option B (omit the button); `EmbedSnippetBuilder.ForRelease`
height bump; the Add-to-Queue buttons at the detail-page play sites (§5.1 grounded scope — **cards
excluded per OQ10**). **Depends on 17.1.**
(Add-to-Queue could split into its own wave 17.4 if Daniel wants it shipped independently — it only
needs the existing `Enqueue`/`EnqueueRange`, not 17.1's new members.)
**Dependency shape:** `17.1 → {17.2, 17.3}`. The cold-start item is 17.1. Most open questions
(OQ1OQ11) must be resolved before 17.2/17.3 land, but 17.1 can begin immediately — the engine
contract for `Move`/`RemoveAt` is settled (§6) and independent of the UI decisions.