diff --git a/PLAN.md b/PLAN.md index c213aba..3000963 100644 --- a/PLAN.md +++ b/PLAN.md @@ -261,6 +261,57 @@ The phase deferred behind the home-hero **Plays** stat card (`NowPlayingStats.ra --- +## Phase 17 — Player-Bar Queue View + +Phase 11 (wave 11.F) built the queue **engine** (`IQueueService`/`QueueService` — ordered playback, +auto-advance, skip-prev/next, armed-idle release embeds) but gave it **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. Phase 17 surfaces the queue as a first-class, visible, editable object in the player bar — +**without** moving any player/queue state out of the layout-level `AudioPlayerProvider` (project +memory; the cascade stays at layout level) and **without** changing the streaming/playback seam. + +Four Daniel-stated capabilities, gated by a single new **Queue** toggle button (shown only when a +queue is loaded, mirroring the skip-affordance gating so single-track play is byte-for-byte its +pre-queue self): + +1. **Queue button** in the bar — below the transport controls, left of the timestamp. +2. **Non-Fixed (overlay) mode** — a centered, mostly-square panel (borrowing the Phase 15 + visualizer-popover idiom) listing the queue with **drag-reorder + per-track removal**. +3. **Fixed (embed) mode** — the queue list is **always shown below** the bar controls (not a toggle), + **read-only** for shared release queues (no reorder, no remove); the embed iframe is resized to fit + the panel. +4. **Add to Queue** affordance — an icon button + tooltip beside every detail-page play button for a + release (→ `EnqueueRange`) or track (→ `Enqueue`), lighting up the dormant `Enqueue` path. + +**Architectural spine.** Engine grows **two additive members only** — `Move(from, to)` and +`RemoveAt(index)` — interop-free state mutations that re-emit `QueueChanged` and **never re-stream or +interrupt the playing track** (the engine's stated open/closed posture; existing members untouched). +Both view modes render **one shared `QueueList` presentational component** off the same cascaded +`IQueueService.Items`, differing only in presentation + an `Editable` flag (project memory: *one +source, multiple views*). Reorder/remove run safely during prerender (no JS) — only playback +transitions touch interop. + +**Sequenced as three waves.** `17.1 → {17.2, 17.3}`. **17.1 (engine `Move`/`RemoveAt` + the shared +`QueueList` view) is the cold-start prerequisite**, settled and independent of the UI decisions — +it can begin immediately. 17.2 (docked overlay, editable) and 17.3 (Fixed embed panel + snippet +resize + Add-to-Queue) hang off it and are largely parallel. Add-to-Queue may split to a standalone +17.4 (it needs only the existing `Enqueue`/`EnqueueRange`, not 17.1's new members). + +Full design — goal, constraints, use cases, acceptance criteria, test cases, wave decomposition, and +the open-question set: `product-notes/phase-17-player-queue-view.md`. + +**Open questions (Daniel's call — spec §10).** OQ1 (Queue button behavior in embed mode given the +panel is always shown — recommend repurpose as collapse/expand toggle); OQ2 (click-a-row to jump — +recommend yes, both modes); OQ3 (terminal/empty states after removal); OQ4 (drag mechanism + touch +viability — `MudDropContainer` vs. pointer-interop vs. up/down-arrow fallback; mobile is a primary +surface); OQ5 (Clear-queue in the UI and whether it stops playback); OQ6 (embed panel height — +fixed+scroll vs. grow-to-cap); OQ7 (Material vs. bespoke `DDIcons` glyph); OQ8 (Add-to-Queue into a +dormant/empty queue — recommend pure append, keep "add" ≠ "play"); OQ9 (exclude `StreamNowButton` — +no fixed track); OQ10 (`ReleaseGallery` cards — recommend defer; cards have no play button today); +OQ11 (removing the current track — recommend keep playing to natural end). **None block 17.1.** + +--- + ## Working with this file - **Add items by extending an existing phase first**; only create a new phase when the addition genuinely doesn't fit any of 1–5. Phase numbers are organisational, not sequencing. diff --git a/product-notes/phase-17-player-queue-view.md b/product-notes/phase-17-player-queue-view.md new file mode 100644 index 0000000..85307a9 --- /dev/null +++ b/product-notes/phase-17-player-queue-view.md @@ -0,0 +1,385 @@ +# Phase 17 — Player-Bar Queue View + +Status: **design spec — open questions pending Daniel.** Author: product-designer. Date: 2026-06-19. +**Plan only — no code has been written by this doc.** + +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`), `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 must be touch-viable.** The public site's primary listening surface includes + mobile (per `PLAN.md §1.7`). Whatever DnD mechanism is chosen must work with touch, or the design + must offer a non-drag fallback for reordering (up/down affordance). **Flag — see OQ4.** +- **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 (recommend) — 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 without changing the snippet height (collapsed just shows less; the iframe stays sized for + expanded). Consistent button meaning across modes ("toggle the queue view"). +- **Option B — 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. + +This is **OQ1** — Daniel's call. The brief explicitly flagged it. + +--- + +## 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 (define):** if the queue is **empty/dormant** (`CurrentIndex == -1`), what + does "Add to Queue" do? `Enqueue` appends but does not set a current index or start playback, so + adding to an empty queue leaves it with items but `Current == null` and nothing playing. Options: + (a) pure append, listener must press play; (b) if dormant, first add also stages/sets current + without auto-playing; (c) if dormant, first add behaves like `PlayRelease`/play. **Recommend (a)** + — Add-to-Queue is explicitly *not* play; keep the verbs distinct. But this needs a tiny engine + affordance so an appended-into-empty queue has a sensible `CurrentIndex` for the next skip/play. + **OQ8.** + +### 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, arguably out of the literal brief ("anywhere there + is a play button"). **OQ10** — do cards get one? If yes, they probably want a play button too, which + is a bigger card-redesign question. Recommend: **defer card-level affordances**; scope item 4 to the + detail-page play sites (rows 1–3 above) where a play button genuinely exists today. +- **`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 + +- **OQ1 — Queue button in embed (Fixed) mode.** Panel is always shown there. Option A: keep the + button as a collapse/expand toggle (recommend). Option B: omit the button entirely. **Daniel's + call** — explicitly flagged in the brief. +- **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.** MudBlazor has `MudDropContainer` (HTML5 DnD, weak on + touch) vs. a JS-interop pointer-drag (like `RadialKnob`'s `capturePointer`) vs. a non-drag + up/down-arrow reorder fallback. Mobile is a primary surface (C6). **Which mechanism?** Recommend: + pointer-based interop reorder (touch-viable, matches the codebase's existing pointer-capture + pattern) **or** ship up/down arrows for v1 and add drag later. Daniel's taste call. +- **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.** Pure append (recommend — keep "add" ≠ "play"), + vs. first-add also stages current, vs. first-add plays. Affects the engine's index handling. +- **OQ9 — `StreamNowButton`.** Exclude from Add-to-Queue (recommend — no fixed track until resolved)? +- **OQ10 — `ReleaseGallery` cards.** Do browse-grid cards get an Add-to-Queue (and implicitly a play) + button? Recommend **defer** — cards have no play button today; adding one is a card redesign beyond + this brief. Scope item 4 to the detail-page play sites. +- **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`). Build a single presentational `QueueList` component that + renders `Items` with the current marked and takes an `Editable` flag (drag/remove on when true). + 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) button behavior; `EmbedSnippetBuilder.ForRelease` height bump; + the Add-to-Queue buttons at the detail-page play sites (§5.1 grounded scope). **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 +(OQ1–OQ11) 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.