docs(plan): add Phase 20 Theater Mode spec and roadmap entry
This commit is contained in:
@@ -443,6 +443,59 @@ not the same work; this phase does not satisfy or depend on that one.
|
||||
|
||||
---
|
||||
|
||||
## Phase 20 — Theater Mode (public Release Detail views)
|
||||
|
||||
A presentation-only feature on the **public listener site** (`DeepDrftPublic` / `DeepDrftPublic.Client`;
|
||||
**no CMS, no API, no data, no schema change**). On a Release Detail view, a new toggle clears the page
|
||||
chrome away from the visualizer: hide the release content (`@if`-gated header/meta/track-list/blurb and
|
||||
the Session/Mix hero overlay) so the lava-lamp + waveform field fills the surface unobstructed, while
|
||||
the **player bar grows** to carry the release identity the hidden page would otherwise show — cover art,
|
||||
release title, and a release-mode share. Borrowed namesake: YouTube/Twitch "theater mode" — collapse the
|
||||
surrounding chrome to let the media (here, the visualizer) take over, one reversible toggle. Full design,
|
||||
component-by-component placement, the SOLID state seam, theming reuse, acceptance criteria, and open
|
||||
questions: `product-notes/phase-20-theater-mode.md`.
|
||||
|
||||
**Scope — the three detail views (verified):** `Pages/CutDetail.razor` (scaffold `Ambient` visualizer),
|
||||
`Pages/SessionDetail.razor` (mounts the visualizer directly — **does not** use `ReleaseDetailScaffold`),
|
||||
`Pages/MixDetail.razor` (scaffold, full-bleed mode-A visualizer). The feature must behave **identically**
|
||||
across all three; the Session-doesn't-use-the-scaffold asymmetry is the key constraint the state design
|
||||
works around.
|
||||
|
||||
**Architectural spine.** **One boolean, multiple observers** (memory: *one source, multiple views*).
|
||||
Recommended home: **widen `WaveformVisualizerControlState` with a `TheaterMode` flag** (Option A — the
|
||||
object is already scoped, session-persistent, observed via its `Changed` event, gated on the same
|
||||
`LavaEnabled || WaveformEnabled` the Theater button reads, and *explicitly designed to widen by adding a
|
||||
field + default*); the SRP-purist alternative is a dedicated `TheaterModeState` holder (Option B). The
|
||||
**detail pages own only the content `@if`** (each page gates the fragments it renders, so the scaffold
|
||||
stays Theater-unaware and Session is covered the same way); the **player bar owns only the enlargement**
|
||||
(reads `CurrentTrack.Release` — `Title`/`ImagePath`/`EntryKey`/`Medium`, all already on the DTO — and
|
||||
renders a small `NowShowingPanel` presentational sub-component); the **toggle button owns only the
|
||||
mutation**. No page reaches into the bar; the bar reaches into no page.
|
||||
|
||||
**Theming (DRY — hard requirement).** The toggle is a `MudIconButton` in `.dd-accent-icon` (green-accent
|
||||
glyph both themes, zero new CSS — same as the lava-lamp trigger it sits beside); the enlarged bar binds
|
||||
existing theme-aware aliases (`--deepdrft-page-surface`/`-text`/`-text-muted`), reuses the
|
||||
`deepdrft-track-detail-cover-art` cover idiom, and wraps the release `SharePopover` in `.dd-accent-icon`.
|
||||
**No new dark overrides, no new palette `Color`, no new token family.**
|
||||
|
||||
**Button placement + gating.** A new right-side icon button immediately **left of the lava-lamp toggle**,
|
||||
visible only when `LavaEnabled || WaveformEnabled`, disabled until interactive, with an on/off active
|
||||
state. Material `Theaters` glyph for v1 (bespoke `DDIcons` deferred — Phase 17 OQ7 precedent).
|
||||
|
||||
**Open questions for Daniel (spec §9) — none block a first cut, but several are genuine product calls:**
|
||||
(OQ1) Theater icon — Material `Theaters` vs. bespoke (recommend Material now); (OQ2) bar enlargement when
|
||||
nothing is playing — page's release vs. playing-release-only (recommend playing-only, keeps the seam
|
||||
clean); (OQ3) state home — Option A widen vs. Option B dedicated holder; (OQ4) back link stays in Theater
|
||||
(recommend keep — it's navigation chrome); (OQ5) persistence — session-scoped/reset-on-reload vs. cookie
|
||||
(recommend session-scoped, matches visualizer-state precedent); (OQ6) Theater on the home hero/NowPlaying
|
||||
panel too, or detail-pages-only (recommend detail-pages-only for v1, as scoped).
|
||||
|
||||
**Status: proposed — awaiting Daniel sign-off on §9 before scoping waves.** Cold-start once signed off;
|
||||
no dependency on any in-flight phase (Phases 11/16/17 — the player bar, queue, and visualizer it builds
|
||||
on — are all complete).
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
# Phase 20 — Theater Mode (public Release Detail views)
|
||||
|
||||
Product spec. Status: **proposed, awaiting Daniel sign-off on the open questions in §9.**
|
||||
Surface: **public listener site only** (`DeepDrftPublic` / `DeepDrftPublic.Client`). No CMS
|
||||
(`DeepDrftManager`) change. No API, data, or schema change — Theater Mode is a pure
|
||||
presentation-layer feature riding data the player already carries.
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal
|
||||
|
||||
On a Release Detail view, let the listener **clear the page chrome away from the visualizer** with one
|
||||
toggle — hiding the release content (header/meta/track-list/blurb) so the lava-lamp + waveform field
|
||||
fills the surface unobstructed — while the **player bar grows** to carry the now-essential release
|
||||
identity (cover art, release title, share) that the hidden page would otherwise have shown.
|
||||
|
||||
It is a "lean back and watch the lamp" mode. The visualizer is already the most distinctive thing the
|
||||
site does (Phase 10/12/15); Theater Mode makes it the *whole* thing on demand, and relocates the
|
||||
minimum release identity to the one piece of chrome that stays — the player bar.
|
||||
|
||||
**One-line framing:** Theater Mode trades the release page for the visualizer, and pays for the lost
|
||||
release identity by enlarging the player bar.
|
||||
|
||||
---
|
||||
|
||||
## 2. Scope — the three Release Detail views (verified against the code)
|
||||
|
||||
The feature must behave identically across all three release mediums. The relevant files:
|
||||
|
||||
| Medium | Page file | Visualizer mount | Lava-lamp toggle host |
|
||||
|---------|---------------------------------------------|---------------------------------------------------|------------------------------------------------|
|
||||
| CUTS | `DeepDrftPublic.Client/Pages/CutDetail.razor` | `<WaveformVisualizer>` in scaffold's `Ambient` slot (mode B) | `ReleaseDetailScaffold` `TopRightAction` slot |
|
||||
| SESSIONS| `DeepDrftPublic.Client/Pages/SessionDetail.razor` | `<WaveformVisualizer>` mounted directly (does **not** use scaffold) | inline in `.session-detail-top-row` |
|
||||
| MIXES | `DeepDrftPublic.Client/Pages/MixDetail.razor` | `<WaveformVisualizer>` mounted directly (mode A, full-bleed) | `ReleaseDetailScaffold` `TopRightAction` slot |
|
||||
|
||||
**The asymmetry to respect:** Cut and Mix compose `ReleaseDetailScaffold`; **Session deliberately does
|
||||
not** (it diverges for the hero-overlay layout — see `DeepDrftPublic.Client/CLAUDE.md`). So a
|
||||
"hide-content" gate placed only in the scaffold would miss Session. The feature must be expressed in a
|
||||
way that all three pages consume identically without forcing Session onto the scaffold. §6 resolves
|
||||
this.
|
||||
|
||||
**Supporting components in play:**
|
||||
- `DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor` — owns `TopRightAction`, the content
|
||||
regions (`Header` / `MetaContent` / `BodyContent` / share-row), and the `ShowHeader` / `ShowMeta` /
|
||||
`ShowShareRow` gates.
|
||||
- `DeepDrftPublic.Client/Controls/WaveformVisualizerControlPopover.razor` — the lava-lamp icon button
|
||||
unit (`MudIconButton` wrapped in `.dd-accent-icon`). The new Theater button sits **to its left**.
|
||||
- `DeepDrftPublic.Client/Services/WaveformVisualizerControlState.cs` — the scoped session-persistent
|
||||
holder for visualizer subsystem state (`LavaEnabled`, `WaveformEnabled`, …) and its `Changed` event.
|
||||
This is the model for where Theater-Mode state and "is anything visualizing?" live (§6).
|
||||
- `DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor` (+ `.razor.cs`) — the dock UI
|
||||
that grows in Theater Mode.
|
||||
- `DeepDrftPublic.Client/Controls/AudioPlayerBar/TrackMetaLabel.razor` — the now-playing identity row;
|
||||
the natural home for the enlarged "now showing" presentation (§7).
|
||||
- `DeepDrftModels/DTOs/TrackDto.cs` + `ReleaseDto.cs` — **already carry everything the enlarged bar
|
||||
needs**: `Track.Release.Title`, `Track.Release.ImagePath` (cover art), `Track.Release.EntryKey` +
|
||||
`Track.Release.Medium` (release-mode share). **No new data plumbing.**
|
||||
|
||||
---
|
||||
|
||||
## 3. The toggle button — placement and behavior
|
||||
|
||||
1. A new right-side action **icon button**, positioned **immediately to the left of the lava-lamp
|
||||
toggle** (the `WaveformVisualizerControlPopover` trigger), in the same top-right action cluster on
|
||||
each of the three pages.
|
||||
2. It is **visible only when the lava-lamp OR the waveform visualizer is active** — i.e. when
|
||||
`WaveformVisualizerControlState.LavaEnabled || WaveformEnabled`. If the listener has switched both
|
||||
subsystems off, there is nothing to go to theater *for*, so the button is absent. (This mirrors how
|
||||
the visualizer's own controls self-gate on subsystem state.)
|
||||
3. It is a **toggle** with an on/off visual state (active styling when Theater Mode is ON), exactly as
|
||||
the lava-lamp popover icon shows an open/closed state today.
|
||||
4. **Disabled until interactive** (`!RendererInfo.IsInteractive`) — same guard the lava-lamp button and
|
||||
Play buttons already carry, so it does nothing during prerender.
|
||||
|
||||
**Iconography:** Material `Theaters` (a film-strip glyph) is the obvious stock choice and reads as
|
||||
"theater" immediately. A bespoke `DDIcons` glyph in the hand-rolled house style is the higher-craft
|
||||
option but is **not** required for v1 (this matches the Phase 17 OQ7 precedent — Material icons now,
|
||||
bespoke later). **Open question OQ1 (§9).**
|
||||
|
||||
---
|
||||
|
||||
## 4. Visibility behavior (Theater ON)
|
||||
|
||||
When Theater Mode is ON, the release-detail **content** is conditionally removed from the render (an
|
||||
`@if` gate, not CSS `display:none` — Daniel's words, and it matches how the scaffold already gates
|
||||
`Header`/`MetaContent`/`BodyContent` and how `WaveformVisualizerControls` gates its rows). What hides:
|
||||
|
||||
- The masthead / header region (title, artist, genre, year, Play/Share affordance row).
|
||||
- The metadata block and the multi-track body (the Cut track-list; the `ReleaseDescription` blurb).
|
||||
- The hero overlay (Session/Mix) — the big background-image hero with its overlaid title/play/share.
|
||||
|
||||
**What stays visible in Theater Mode:**
|
||||
- The **visualizer** (the whole point — now unobstructed).
|
||||
- The **top action row**: the back link, the lava-lamp popover (so the listener can still tune the
|
||||
lamp), and the Theater toggle itself (so they can leave). These are the controls *over* the
|
||||
experience, not content *of* the release — they stay.
|
||||
- The **player bar**, now enlarged (§5/§7).
|
||||
|
||||
**Toggling OFF** restores the content exactly as it appears today — the `@if` re-includes it. Because
|
||||
the gate is render-inclusion, not a layout fork, OFF is byte-for-byte the current page (the Liskov
|
||||
discipline the scaffold already follows for its `Ambient` slot).
|
||||
|
||||
**Consistency across the three pages:** all three honor the same visibility rule and the same default
|
||||
(Theater starts OFF on every page load). See §6 for *how* the single rule reaches all three without
|
||||
forcing Session onto the scaffold.
|
||||
|
||||
---
|
||||
|
||||
## 5. Player-bar enlargement behavior (Theater ON)
|
||||
|
||||
When Theater Mode is ON, the player bar **grows** to surface — for the current track in the current
|
||||
release — the release identity the hidden page no longer shows:
|
||||
|
||||
1. **Cover art** — `Track.Release.ImagePath` rendered as a thumbnail (the `deepdrft-track-detail-cover-art`
|
||||
background-image idiom already used on the detail pages; reuse it, do not invent a new image
|
||||
treatment). Placeholder when null, matching the detail-page placeholder treatment.
|
||||
2. **Release title** — `Track.Release.Title`, linking to the release detail page via the existing
|
||||
`ReleaseRoutes.DetailHref(Track.Release)` resolver (the same link `TrackMetaLabel` already builds for
|
||||
the track title).
|
||||
3. **Share** — a release-mode `SharePopover` bound to `Track.Release.EntryKey` +
|
||||
`Track.Release.Medium` (the exact wiring the detail pages already use). This is the same share the
|
||||
hidden page carried, relocated to the bar.
|
||||
|
||||
The bar **may grow taller/larger** to accommodate this "now showing" block. The growth is conditional on
|
||||
Theater Mode being ON.
|
||||
|
||||
**Important seam:** the enlarged presentation lives in the player bar's **own** presentation layer
|
||||
(`TrackMetaLabel` / a small new sub-component), keyed off the **current track's `Release`** — not off
|
||||
the detail page. This matters because the player bar is mounted at layout level
|
||||
(`AudioPlayerProvider` → `MainLayout`), one instance for the whole app. It already shows whatever track
|
||||
is current regardless of route. So the enlarged "now showing" block is a property of *the bar reacting
|
||||
to Theater state*, not something the detail page pushes into it. See §6 for how the bar observes Theater
|
||||
state without the detail page reaching across to it.
|
||||
|
||||
**Edge — Theater ON but nothing playing:** the bar's enlargement keys off `CurrentTrack?.Release`. If no
|
||||
track is playing (the listener opened the page and toggled Theater without pressing play), there is no
|
||||
current release to surface in the bar. **Open question OQ2 (§9):** does Theater Mode surface *the page's*
|
||||
release in the bar even when not playing (the page knows its release), or only the *playing* release
|
||||
(simpler, but the bar shows nothing extra until play starts)? Recommendation: **playing-release only** —
|
||||
keeps the bar a pure function of player state and avoids a detail-page→bar data push that would
|
||||
re-entangle the two. The listener who toggles Theater is almost always already listening; the visualizer
|
||||
itself is blank until a track resolves, so a blank-ish enlarged bar in that rare pre-play window is
|
||||
coherent.
|
||||
|
||||
---
|
||||
|
||||
## 6. Where the toggle state lives (SOLID boundary)
|
||||
|
||||
**Recommendation: a small new scoped state holder, observed by both the pages and the player bar — the
|
||||
same decoupling pattern `WaveformVisualizerControlState` already establishes.**
|
||||
|
||||
The crux: three independent pages (two via scaffold, one not) AND the layout-level player bar all need
|
||||
to read one boolean and react to its change. The clean seam is a shared scoped service with a `Changed`
|
||||
event — not a cascading parameter from a page (the bar is not a descendant of the page), and not state
|
||||
on the scaffold (Session does not use the scaffold).
|
||||
|
||||
Two viable homes for the boolean:
|
||||
|
||||
- **Option A (recommended): extend `WaveformVisualizerControlState`** with a `TheaterMode` bool +
|
||||
`DefaultTheaterMode = false` + reuse its existing `Changed` event. Rationale: Theater Mode is
|
||||
*conceptually part of the visualizer experience* — it is literally "show only the visualizer," it is
|
||||
gated on the visualizer's own `LavaEnabled || WaveformEnabled`, and the state object is already scoped,
|
||||
already session-persistent-within-a-session, already observed by the visualizer bridge, and explicitly
|
||||
designed to **widen by adding a field + default without forcing any consumer constructor to change**
|
||||
(its class comment says exactly this). The Theater toggle button mutates `TheaterMode` and calls
|
||||
`NotifyChanged()`; the pages and the player bar subscribe to `Changed` and re-read. **Cost:** the state
|
||||
object's name now slightly under-describes its contents (it holds a presentation-mode flag, not just
|
||||
visualizer dials). Acceptable — the comment can note Theater Mode as a visualizer-experience flag.
|
||||
|
||||
- **Option B: a dedicated `TheaterModeState` scoped holder** (`bool IsOn`, `event Action? Changed`,
|
||||
`Toggle()`). Rationale: single-responsibility purity — the visualizer-control state stays strictly
|
||||
about visualizer dials. **Cost:** a second tiny observer-pattern holder that does the same shape of
|
||||
thing as the one next to it; the gating still has to *read* `WaveformVisualizerControlState`
|
||||
(`LavaEnabled || WaveformEnabled`) to know whether to show the button, so the two are coupled at the
|
||||
read site anyway.
|
||||
|
||||
**Steer: Option A.** The visualizer-control state is already the "how the visualizer presents" object,
|
||||
Theater Mode is a visualizer-presentation concern, and the object was explicitly designed to widen this
|
||||
way. Option B is the defensible SRP-purist alternative if Daniel wants the visualizer dials kept
|
||||
pristine. **Open question OQ3 (§9).** Final structural call is staff-engineer's at implementation
|
||||
(matching the standing convention on `IQueueService`-shape decisions).
|
||||
|
||||
**Why this satisfies SOLID / the "cleanly separated concerns" constraint:**
|
||||
- **Single source of truth, multiple observers.** One boolean; the three pages observe it for the
|
||||
content `@if`; the player bar observes it for the enlargement. No page reaches into the bar; the bar
|
||||
does not reach into a page. (Memory: *one source, multiple views* — divergence lives only in
|
||||
rendering.)
|
||||
- **The detail pages own only the visibility `@if`.** Each page wraps its content region(s) in
|
||||
`@if (!state.TheaterMode) { … }`. Cut/Mix can do this around the scaffold's slot content; Session does
|
||||
it around its own content. The scaffold itself needs **no Theater knowledge** if each page gates the
|
||||
fragments it passes in — keeping the scaffold's existing `ShowHeader`/`ShowMeta` gates uncomplicated.
|
||||
(Alternative: give the scaffold a `ShowContent`/`Theater` gate too; only helps the two scaffold pages,
|
||||
not Session — so gating at the page level is the consistent choice.)
|
||||
- **The player bar owns only the enlargement.** It reads `state.TheaterMode` + `CurrentTrack.Release`
|
||||
and renders the "now showing" block. No new parameter threads down from a page.
|
||||
- **The toggle button owns only the mutation.** Tap → flip `TheaterMode` → `NotifyChanged()`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Player-bar enlargement — component shape
|
||||
|
||||
Keep the bar from bloating. Two clean options:
|
||||
|
||||
- **Recommended: a new presentational sub-component** `NowShowingPanel.razor` (or fold into a new branch
|
||||
of `TrackMetaLabel`) under `Controls/AudioPlayerBar/`, rendered by `AudioPlayerBar.razor` **only when**
|
||||
`state.TheaterMode && CurrentTrack?.Release is not null`. It takes the current `TrackDto` (or just its
|
||||
`Release`) and renders cover + title-link + release-`SharePopover`. Purely presentational; owns no
|
||||
player logic and no Theater state (it is shown/hidden by the bar). This mirrors how `QueueList`,
|
||||
`ReleaseHeroOverlay`, and `ReleaseDescription` are split out as presentational shells.
|
||||
- Alternative: branch inside `TrackMetaLabel` on a new `Theater` bool parameter. Lighter file count, but
|
||||
pushes a layout-mode branch into the always-on label component — less clean. Prefer the sub-component.
|
||||
|
||||
`AudioPlayerBar` subscribes to `state.Changed` (it already subscribes to `IPlayerService.StateChanged`
|
||||
in `OnParametersSet` and disposes — add the visualizer-control-state subscription the same way) so the
|
||||
bar re-renders when Theater flips, and `StateHasChanged` already fires on track change so the enlarged
|
||||
block follows the playing release for free.
|
||||
|
||||
---
|
||||
|
||||
## 8. Theming reuse (DRY — hard requirement)
|
||||
|
||||
Everything binds the **existing** theme-aware token layer and the established interactive-accent icon
|
||||
convention. **No new per-component dark overrides.** Concretely:
|
||||
|
||||
- **The Theater toggle button** is a `MudIconButton` (`Color.Secondary`) wrapped in a
|
||||
**`.dd-accent-icon`** container — the exact pattern `WaveformVisualizerControlPopover` uses for the
|
||||
lava-lamp trigger. This gives it the green-accent glyph (`--deepdrft-green-accent`) in **both** themes
|
||||
with zero new CSS. Do **not** spawn a new dark override (root `CLAUDE.md`: "Add new green-accent icon
|
||||
affordances by applying this class, not by spawning a new dark override.").
|
||||
- **The enlarged player-bar "now showing" block:**
|
||||
- **Surface/background, text, borders** bind the player bar's existing surface treatment
|
||||
(`.player-surface` / the bar's own classes) and the theme-aware aliases —
|
||||
`--deepdrft-page-surface` / `--deepdrft-page-text` / `--deepdrft-page-text-muted` for neutral
|
||||
text/background, never raw source tokens. (The bar already lives inside the themed wrapper, so it is
|
||||
not a portaled-popover case — no `body.deepdrft-theme-dark` re-declaration needed.)
|
||||
- **The release title link** uses the bar's existing title-link treatment (`TrackMetaLabel`'s
|
||||
`.track-meta-title`) — reuse it, do not restyle.
|
||||
- **The Share affordance** is a `SharePopover` wrapped in **`.dd-accent-icon`** (its glyph goes
|
||||
green-accent in both themes — the same treatment the detail-page hero share already uses).
|
||||
- **The cover-art thumbnail** reuses the `deepdrft-track-detail-cover-art` background-image class (and
|
||||
the `deepdrft-gradient-soft-secondary` placeholder for null images) — the detail pages' existing
|
||||
cover idiom, theme-aware already.
|
||||
- **No new palette `Color` enum value**, no new token family unless a genuinely new surface appears
|
||||
(it should not — the bar surface and detail-cover idioms already exist). If the enlarged bar needs a
|
||||
divider or a subtle panel inset, bind an **existing** alias; flag to Daniel if a new alias seems
|
||||
unavoidable rather than inventing one silently.
|
||||
|
||||
---
|
||||
|
||||
## 9. Open questions (Daniel decisions, not implementation calls)
|
||||
|
||||
- **OQ1 — Theater toggle icon.** Material `Theaters` (film-strip) for v1, or commission a bespoke
|
||||
`DDIcons` glyph in the hand-rolled house style? *Recommend: Material `Theaters` now, bespoke deferred
|
||||
(Phase 17 OQ7 precedent).* — **product call.**
|
||||
- **OQ2 — bar enlargement when nothing is playing.** Surface the *page's* release in the bar even with no
|
||||
current track (needs a page→bar data path), or only the *playing* release (bar stays a pure function of
|
||||
player state)? *Recommend: playing-release only.* — **product + architecture call; the recommendation
|
||||
keeps the SOLID seam clean, so it leans toward an implementation default unless Daniel wants the
|
||||
page's release shown pre-play.**
|
||||
- **OQ3 — state home.** Extend `WaveformVisualizerControlState` with `TheaterMode` (Option A,
|
||||
recommended) or a dedicated `TheaterModeState` holder (Option B, SRP-purist)? — *Structural;
|
||||
staff-engineer's final call, but Daniel may have a taste here.*
|
||||
- **OQ4 — does the back link stay in Theater Mode?** Recommendation keeps it (it is navigation chrome,
|
||||
not release content). Confirm Daniel agrees the top action row (back + lava + theater) is "controls,"
|
||||
not "content." *Recommend: keep.* — **product call, low-stakes.**
|
||||
- **OQ5 — persistence scope.** Theater Mode follows the visualizer-state convention: persists across SPA
|
||||
navigation within a session, resets to OFF on a fresh page load (F5). Confirm that is the wanted
|
||||
behavior (vs. a cookie that remembers Theater across reloads). *Recommend: session-scoped, reset on
|
||||
reload — matches the visualizer-control-state precedent; no cookie round-trip.* — **product call.**
|
||||
- **OQ6 — Theater on the home hero / NowPlaying panel?** The `WaveformVisualizerControlPopover` also
|
||||
appears on the home hero's NowPlaying panel (mode C). This spec scopes Theater Mode to the **three
|
||||
Release Detail views only** (Daniel's framing). Flag: should the home hero get a Theater affordance
|
||||
too, or is it deliberately detail-pages-only? *Recommend: detail-pages-only for v1, as scoped.* —
|
||||
**product call, adjacent.**
|
||||
|
||||
---
|
||||
|
||||
## 10. Acceptance criteria
|
||||
|
||||
1. On each of `/cuts/{key}`, `/sessions/{key}`, `/mixes/{key}`, a Theater toggle icon button renders
|
||||
immediately to the left of the lava-lamp popover icon in the top action row.
|
||||
2. The Theater button is **absent** when both `LavaEnabled` and `WaveformEnabled` are false; it **appears**
|
||||
when either is true. It is disabled (inert) during prerender / before interactive.
|
||||
3. Toggling Theater **ON** removes the release content from the render (header/meta/track-list/blurb,
|
||||
and the hero overlay on Session/Mix) via `@if`, leaving the visualizer unobstructed plus the top
|
||||
action row (back, lava, theater) and the player bar.
|
||||
4. In Theater Mode the player bar **grows** and surfaces, for the current playing track's release:
|
||||
cover art, release title (linked to the release detail page), and a release-mode share affordance.
|
||||
5. Toggling Theater **OFF** restores the page byte-for-byte to its non-Theater appearance.
|
||||
6. Behavior is **identical across all three mediums** — same button, same placement, same visibility
|
||||
rule, same bar enlargement, same default (OFF on load).
|
||||
7. **Light and dark both correct with zero new dark overrides:** the toggle glyph and the bar's share
|
||||
glyph are green-accent in both themes via `.dd-accent-icon`; the enlarged bar's text/surface bind
|
||||
existing theme-aware aliases; the cover thumbnail uses the existing detail-cover class.
|
||||
8. No API / data / schema change. No CMS change. The enlarged bar reads only `CurrentTrack.Release`
|
||||
fields the DTO already carries.
|
||||
9. Theater Mode persists across SPA navigation within a session and resets to OFF on a fresh page load
|
||||
(per OQ5, if confirmed).
|
||||
|
||||
---
|
||||
|
||||
## 11. What this is NOT (scope guards)
|
||||
|
||||
- **Not** a fullscreen API call. Theater Mode hides page content; it does not request browser
|
||||
fullscreen. (A future enhancement could pair it with the Fullscreen API — note, don't build.)
|
||||
- **Not** a visualizer behavior change. The renderer, the bridge, the control dials, and the read-only
|
||||
contract are all untouched. Theater Mode only changes *what page chrome is shown around* the
|
||||
visualizer.
|
||||
- **Not** a player/queue change. The streaming seam, the queue engine, and the bar's transport controls
|
||||
are untouched; only the bar's *identity presentation* grows.
|
||||
- **Not** a CMS or embed-player feature. The embed (`FramePlayer` / Fixed bar mode) is out of scope —
|
||||
Theater Mode is for the docked detail-page experience.
|
||||
|
||||
---
|
||||
|
||||
## 12. Borrowed precedent
|
||||
|
||||
- **Media-player "theater mode" / "cinema mode"** (YouTube's theater toggle, Twitch's theater mode) —
|
||||
the direct namesake: collapse the surrounding page chrome to let the media fill more space, one toggle,
|
||||
reversible. The transplant here is that the "media" is the visualizer and the "chrome" is the release
|
||||
page.
|
||||
- **The visualizer-control popover idiom** (`WaveformVisualizerControlPopover`) — the toggle button's
|
||||
placement, `.dd-accent-icon` treatment, `IsInteractive` gating, and on/off visual state are lifted
|
||||
directly from the lava-lamp button it sits beside.
|
||||
- **`WaveformVisualizerControlState`'s observer seam** — the state-holder + `Changed`-event decoupling
|
||||
is the established pattern for "one piece of state, several components react"; Theater Mode reuses its
|
||||
exact shape (and, per Option A, possibly its exact object).
|
||||
Reference in New Issue
Block a user