diff --git a/product-notes/audio-player-desktop-redesign.md b/product-notes/audio-player-desktop-redesign.md new file mode 100644 index 0000000..30d0e5b --- /dev/null +++ b/product-notes/audio-player-desktop-redesign.md @@ -0,0 +1,217 @@ +# AudioPlayerBar — Desktop Redesign Proposal + +**Status:** Proposal (design only — no source edited). +**Scope:** Desktop branch of `AudioPlayerBar.razor` (`@if (_isDesktop)`). Mobile branch left untouched. +**Author context:** Written against the wireframe palette currently in `DeepDrftPalettes.cs` and `deepdrft-tokens.css`, not the retired Charleston/Lowcountry identity. + +--- + +## 0. Reframe before you read the rest (the headline) + +The brief asks to integrate the player with "the Charleston and Lowcountry themes via raw CSS variables." **Those themes no longer exist.** They were retired when the app migrated to the wireframe palette (navy / green / warm off-white). `DeepDrftPalettes.cs` says so explicitly: *"the coral/lowcountry identity has been retired."* + +That changes the nature of this task. This is not "make the messy player prettier." It is: + +> **The player bar was left behind by the palette migration. Its entire theming layer references CSS custom properties that are no longer defined anywhere. Migrating it onto MudBlazor's theme system *is* the redesign.** + +Concretely, `AudioPlayerBar.razor.css` and `SpectrumVisualizer.razor.css` reference these tokens, **none of which are defined in `deepdrft-tokens.css` (verified by grep — zero definitions):** + +| Token referenced in player CSS | Defined anywhere? | Current runtime effect | +| --- | --- | --- | +| `--deepdrft-theme-background-gray` | No | backdrop background resolves to nothing → falls through | +| `--deepdrft-theme-primary` | No | backdrop border colour is invalid → no border / UA default | +| `--deepdrft-theme-secondary` / `-tertiary` | No | minimized dock gradient + spectrum bars have no colour | +| `--charleston-cream` / `-iron` / `-rose` / `-gold` | No | light-mode `:global(.deepdrft-theme-light)` block is dead | +| `--lowcountry-night` / `-coral` / `-twilight` / `-gold` / `-moonlight` | No | dark-mode `:global(.deepdrft-theme-dark)` block is dead | + +So **today the desktop player is rendering with broken styling** — invalid `color-mix()`/`var()` references collapse to `transparent` or are dropped, and the `:global(.deepdrft-theme-*)` overrides target a wrapper class that still exists (`MainLayout` sets `deepdrft-theme-dark`/`-light`) but whose custom-property payload was deleted. The bar looks half-styled because it *is* half-styled. + +This means the redesign has a clean justification that the brief's own framing obscures: we are not inventing a new look, we are **finishing a migration the rest of the app already completed.** The correct colour source is the MudBlazor theme (`DeepDrftPalettes.Default`), reached through MudBlazor component props and `var(--mud-palette-*)` — not a parallel hand-maintained token set that has already drifted into nonexistence once. + +The redesign goals in the brief (rounded `MudPaper`, theme-driven opaque background, MudBlazor layout components, encapsulated zones) are all still right. The palette names in the brief are just stale; substitute the wireframe palette and everything else holds. + +--- + +## 1. Diagnosis — what's specifically wrong + +### 1.1 The theming layer points at deleted tokens (load-bearing) +Covered above. This is the single most important finding and the strongest argument for doing the work now: the player is visibly broken against the current palette, not merely inelegant. + +### 1.2 The container background is hand-rolled CSS, not a theme surface +`.player-backdrop` builds its own surface from scratch: a `var(...)` background, a hard-coded `backdrop-filter: blur(15px)`, a 2px border in a (now-undefined) theme colour, and a `box-shadow`. MudBlazor already has a surface primitive that reads `--mud-palette-surface` and applies elevation shadows that respond to light/dark automatically — `MudPaper`. The component is reimplementing `MudPaper` badly and then theming it with dead variables. + +### 1.3 Layout is raw flex divs with utility-class soup +The desktop branch is six nested `
`s carrying `d-flex`, `flex-column`, `align-center`, `gap-3`, `gap-2`, `gap-1`, `mx-3`, `flex-grow-1`. The three-zone structure (left transport / centre seek+spectrum / right volume) is *implied* by div nesting and `min-width: 200px` on `.controls-left`, not *expressed* by any named component. A reader has to mentally execute the flexbox to see the zones. + +### 1.4 Zones are not encapsulated +- The left zone (play/stop + loading spinner + timestamp) is an anonymous `
` with inline logic for the progress circle. +- The centre zone (seek slider + spectrum) is an anonymous `
` that *also* owns the pointer-event seek handlers inline in the markup. +- The window controls (minimize/close) are an absolutely-positioned `
` floating over the content. + +None of these are components or even named slots. The seek pointer-handler block (`@onpointerdown`/`@onpointerup`/`@onpointerleave` with an inline lambda) is duplicated nearly verbatim between desktop and mobile branches. + +### 1.5 The outer structure mixes three responsibilities in one tree +`.player-outer-container` (fixed positioning) → `MudContainer` (max-width centring) → `.player-backdrop` (the visible surface) → layout. Fixed-position docking, horizontal centring, and the visible card are three concerns stacked into three divs where MudBlazor offers a cleaner split: scoped CSS owns *only* the fixed dock; `MudPaper` owns the visible surface; a `Style`/`Class` width cap owns centring. + +### 1.6 Sub-components each wrap themselves in a bespoke `
` +`PlayerControls`, `VolumeControls`, `TimestampLabel`, `SpectrumVisualizer` each open with a hand-rolled `
` and a sibling scoped `.css` that just re-implements `display:flex; align-items:center; gap:...`. That is `MudStack` with parameters. Each is a small, mechanical swap. + +--- + +## 2. Visual design — the rounded container + +### 2.1 Target look +A single rounded card floating above the bottom edge of the viewport, horizontally centred, capped at a comfortable reading width, with a **mostly-opaque themed surface** so content behind it doesn't bleed through and hurt the seek/spectrum legibility. Soft elevation shadow, theme-coloured hairline accent, generous internal padding. It should read as the same material family as the menu bar and track cards — because it will now share their palette source. + +### 2.2 Where the colour comes from +**`MudPaper` carrying the theme surface, not a hand-built background.** `MudPaper` paints `var(--mud-palette-surface)` and applies a Material elevation shadow that already differs between the light and dark palettes (MudBlazor swaps the whole palette when `MudThemeProvider IsDarkMode` flips, which `MainLayout` already drives). So: + +- **Light (wireframe):** surface resolves to `#FAFAF8` (warm off-white) — `Surface` in `PaletteLight`. Elevation shadow is the standard Material dark-on-light. +- **Dark (wireframe):** surface resolves to `#162437` (navy-mid) — `Surface` in `PaletteDark`. Elevation shadow is the deeper dark-mode Material shadow. + +Both are *already mostly opaque solids* in the palette — which is exactly what the brief asks for and what the seek/spectrum legibility wants. We do **not** need the old `color-mix(... 88%, transparent)` trick; the palette surfaces are opaque by design. If a hint of translucency is still wanted, prefer a single MudBlazor-aware rule (`background-color: var(--mud-palette-surface)` with an `opacity` on a pseudo-layer) over re-introducing alpha tokens — but the default recommendation is **opaque surface, no backdrop-filter**, because `backdrop-filter: blur()` over an opaque surface is wasted GPU work and was only ever there to rescue the translucent look. + +### 2.3 Concrete prop choices + +```razor + + ... + +``` + +- **`Elevation="8"`** — high enough to read as a floating dock above page content; MudBlazor's elevation system supplies the light/dark-appropriate shadow so the hand-rolled `box-shadow: 0 4px 20px rgba(0,0,0,0.9)` goes away entirely. +- **Rounding** — `MudPaper` rounds by default via `--mud-default-borderradius`. If a *larger* radius is wanted (the old CSS used `1rem`), set it via `Style="border-radius:16px"` or a single scoped `.player-surface { border-radius: 16px; }` rule. One line, theme-independent — radius is geometry, not colour, so scoped CSS is the right home (see §5). +- **Accent hairline** — the old design had a theme-coloured border (iron in light, coral in dark). To keep a hairline accent that *tracks the theme*, use `var(--mud-palette-primary)` in a single scoped rule: `.player-surface { border: 1px solid var(--mud-palette-lines-default); }` for a neutral hairline, or `var(--mud-palette-primary)` for a stronger accent (navy in light, green-accent in dark). This is the **one** place we keep a `var(--mud-palette-*)` reference in scoped CSS, and it is legitimate because `--mud-palette-*` *is* defined by `MudThemeProvider` at runtime — unlike the dead `--charleston-*` tokens. The distinction is the whole point: theme-driven via MudBlazor's own variables, not a parallel hand-maintained set. + +### 2.4 The minimized dock and window controls +- **Minimized dock** (`.minimized-dock`) currently builds a 3-stop gradient from the dead tokens. Replace with a `MudFab` (`Color="Color.Primary"`, `Icon="@Icons.Material.Filled.ExpandLess"`) — a circular floating action button is exactly this control, it picks up the themed primary colour for free, and it carries its own elevation/hover. The bespoke `.minimized-button` `!important` overrides disappear. The only scoped CSS that survives is the **fixed positioning** (`bottom`/`right`/`z-index`) and the responsive position shift. +- **Window controls** (minimize/close, top-right) stay as two `MudIconButton`s but move into a named slot/component (§3) and are positioned with a `MudStack Row` + absolute-position scoped rule, unchanged in behaviour. + +--- + +## 3. Layout blueprint — MudBlazor components replacing raw divs + +### 3.1 Outer shell + +| Current | Replacement | Owns | +| --- | --- | --- | +| `.player-outer-container` (`
`, `position:fixed`) | Keep a thin scoped-CSS `
` **or** a `MudPaper` with the fixed positioning in scoped CSS | Fixed docking to viewport bottom, z-index, full-width | +| `MudContainer MaxWidth="Large"` | `MudContainer MaxWidth="MaxWidth.Large"` (keep — it's already the right component) | Horizontal centring + max width | +| `.player-backdrop` (`
`) | **`MudPaper Elevation="8"`** | The visible rounded themed surface (§2) | + +Positioning (`position: fixed; bottom: 0; left/right: 0; z-index: 1200`) is geometry and **stays in scoped CSS** — MudBlazor has no fixed-dock primitive and shouldn't. Everything *visual* about the surface moves to `MudPaper`. + +### 3.2 The three-zone desktop interior + +Replace the top-level `
` with a **`MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3"`**. Each child is a named zone: + +```razor + + + @* LEFT ZONE — transport + timestamp *@ + + + @* CENTRE ZONE — seek + spectrum (flex-grow) *@ + + + @* RIGHT ZONE — volume *@ + + + +``` + +| Zone | Current markup | New home | +| --- | --- | --- | +| Left (transport + spinner + timestamp) | `
` with inline spinner + `TimestampLabel` | **New `PlayerTransportZone` sub-component** wrapping `PlayerControls`, the `MudProgressCircular`, and `TimestampLabel` in a `MudStack Column AlignItems="Center" Spacing="2"` | +| Centre (seek + spectrum) | `
` with inline pointer handlers + `MudSlider` + `SpectrumVisualizer` | **New `PlayerSeekZone` sub-component** owning the seek pointer-handler logic once, plus the `MudSlider` and `SpectrumVisualizer`, in a `MudStack Column` | +| Right (volume) | `
` wrapping `VolumeControls` | `VolumeControls` directly — the wrapper div was empty of behaviour (its only CSS rule was commented out) | +| Window controls (top-right) | absolutely-positioned `
` | **New `PlayerWindowControls` sub-component** (`MudStack Row`) positioned via one scoped absolute rule | + +### 3.3 Why `MudStack` over `MudGrid` here +`MudGrid`/`MudItem` is a 12-column responsive grid — overkill and clumsy for a single fixed-height toolbar row where the centre should simply *take the remaining space*. `MudStack` (flexbox wrapper) with `flex-grow-1` on the centre zone is the natural fit and is the direct, idiomatic replacement for the existing `d-flex gap-*` divs. **`MudToolBar`** was considered (it's literally a player-bar-shaped component) but it forces a fixed height and dense horizontal layout that fights the centre zone's stacked seek-over-spectrum arrangement; `MudStack` inside `MudPaper` gives more control. Note that recommendation for the implementer. + +### 3.4 New sub-components worth extracting (summary) +1. **`PlayerTransportZone`** — left cluster. Removes inline spinner logic from the parent and gives the left zone a name. Reused by mobile? No — keep desktop-only for now; mobile composes its own arrangement (brief says leave mobile alone). +2. **`PlayerSeekZone`** — centre cluster. **Highest-value extraction**: it ends the duplicated pointer-handler block between desktop and mobile by owning that logic in one place. Even though mobile stays as-is for layout, mobile could later consume this same component (one source, multiple views — consistent with `CONTEXT.md §6` and the project's "same VM, divergence only in rendering" instinct). Design it so mobile *can* adopt it later without a rewrite, even though we don't wire mobile now. +3. **`PlayerWindowControls`** — minimize/close cluster. Trivial but gives the absolutely-positioned controls a name and a home for their one positioning rule. + +--- + +## 4. Sub-component dispositions + +| Component | Verdict | Change | +| --- | --- | --- | +| **`PlayerControls`** | Minor internal swap | Replace `
` (flex/gap CSS) with ``. Delete `PlayerControls.razor.css` entirely — its only rule is the flex container `MudStack` now provides. Buttons unchanged. | +| **`VolumeControls`** | Minor internal swap | Replace `
` with ``. The `width:140px` / slider `width:100px` sizing can stay as a scoped rule **or** move to `Style="width:140px"` on the stack. Keep the `MudIcon` + `MudSlider`. Trim `VolumeControls.razor.css` to (at most) the width caps. | +| **`TimestampLabel`** | Mostly fine | `
` → optional `` or just keep the div for centring + min-width; this one is borderline. The monospace font is now redundant with the theme — `Typo.Subtitle1`/`Caption` already map to **Geist Mono** in `DeepDrftPalettes.Typography`. **Recommend:** switch `MudText` to `Typo="Typo.Caption"` (Geist Mono via theme) and drop the `font-family: monospace` scoped rule — that's the migration-to-theme move in microcosm. Keep `min-width:120px` (layout stability so the timestamp doesn't reflow as digits change). | +| **`SpectrumVisualizer`** | **Must change — dead tokens** | The bar colour uses `--deepdrft-theme-secondary` and the `:global(.deepdrft-theme-light/dark)` overrides use `--charleston-*` / `--lowcountry-*` — **all undefined**. Bars currently have no reliable colour. **Fix:** point the bar `background` at `var(--mud-palette-primary)` (themed: navy in light, green-accent in dark) or a `linear-gradient` from `var(--mud-palette-primary)` to `var(--mud-palette-tertiary)`. Drop both `:global` theme-override blocks — a single `var(--mud-palette-*)` rule auto-tracks the theme, so the per-theme duplication is no longer needed. The geometry CSS (`.spectrum-bars` flex, `.spectrum-bar` sizing, `--bar-height` animation) **stays** — it's layout/animation, not colour. | + +--- + +## 5. CSS strategy — what stays scoped vs. moves to MudBlazor props + +**Principle:** scoped `.css` owns *geometry, positioning, and animation*. The MudBlazor theme (via component props and `var(--mud-palette-*)`) owns *colour, surface, and elevation*. The dead `--charleston-*` / `--lowcountry-*` / `--deepdrft-theme-*` token references are deleted wholesale. + +### Stays in `AudioPlayerBar.razor.css` +- `.player-dock` — fixed positioning (`position:fixed; bottom; left; right; z-index:1200`). No MudBlazor equivalent; correct to keep. +- `.player-surface` — **at most**: `border-radius` (if larger than default) and *one* hairline `border: 1px solid var(--mud-palette-lines-default)` (or `--mud-palette-primary` for a stronger accent). `var(--mud-palette-*)` is allowed here because MudBlazor defines it at runtime. +- `.minimized-dock` positioning — `bottom`/`right`/`z-index` and the `:hover { transform: scale(1.1) }` micro-interaction. Colour/gradient/shadow all go away with the move to `MudFab`. +- `.player-window-controls` — the single `position:absolute; top; right` rule. +- `.player-spacer` — the `height:140px` content-overlap spacer (and its `@media` override). Pure geometry. +- All `@media (max-width: 768px)` blocks — responsive geometry. Keep. (They affect the minimized dock and spacer, which are shared with mobile; do not disturb.) +- `SpectrumVisualizer` bar geometry + `--bar-height` transition. + +### Moves out of scoped CSS entirely (deleted) +- `.player-backdrop` background / `backdrop-filter` / `box-shadow` / border → **`MudPaper Elevation` + surface**. +- Both `:global(.deepdrft-theme-light/dark) .player-backdrop` blocks → gone; MudBlazor swaps the palette. +- The `.minimized-dock` gradient + per-theme override blocks → **`MudFab Color="Color.Primary"`**. +- `.minimized-button` `!important` block → gone (was overriding `MudIconButton`; replaced by `MudFab`). +- `PlayerControls.razor.css` (whole file) → `MudStack`. +- `VolumeControls.razor.css` flex rules → `MudStack` (keep only width caps if desired). +- `TimestampLabel` `font-family: monospace` → theme typography (`Typo.Caption`). +- `SpectrumVisualizer` colour rules (`--deepdrft-theme-secondary` + both `:global` theme blocks) → single `var(--mud-palette-*)` rule. + +### Net effect +`AudioPlayerBar.razor.css` shrinks from ~176 lines (most of it dead-token theming) to roughly the positioning/spacer/responsive core — maybe 40–50 lines, all of it geometry. Two sub-component `.css` files (`PlayerControls`, `VolumeControls`) can be deleted or reduced to a width cap. + +--- + +## 6. Acceptance criteria + +An implementer is done when: + +1. **No dead tokens remain.** `grep` for `--charleston-`, `--lowcountry-`, and `--deepdrft-theme-` across `Controls/AudioPlayerBar/**` returns **zero** matches. (This is the migration's definition of done.) +2. **The visible surface is a `MudPaper`** with `Elevation` set, carrying the themed surface colour — no hand-rolled `background`/`box-shadow`/`border` colour in scoped CSS for the surface. Toggling dark mode flips the player surface between off-white (`#FAFAF8`) and navy-mid (`#162437`) **with no player-specific code path** — it rides the `MudThemeProvider` palette swap that `MainLayout` already drives. +3. **The desktop interior is `MudStack`-based**, not `
`. The three zones are named components (`PlayerTransportZone`, `PlayerSeekZone`, `VolumeControls`) and the window controls are a named component (`PlayerWindowControls`). +4. **The seek pointer-handler logic exists in exactly one place** (`PlayerSeekZone`), not duplicated inline in the parent. (Mobile may still call its own copy until mobile is migrated — but the desktop branch no longer carries inline pointer handlers in `AudioPlayerBar.razor`.) +5. **`PlayerControls` and `VolumeControls` wrap in `MudStack`**, not bespoke flex divs; their now-redundant `.razor.css` flex rules are deleted. +6. **`SpectrumVisualizer` bars are visibly coloured in both themes** via `var(--mud-palette-*)`, and its two `:global(.deepdrft-theme-*)` colour blocks are gone. +7. **The minimized state is a `MudFab`** (or themed `MudIconButton`), not a div with a hand-rolled gradient; no `!important` overrides remain on it. +8. **Mobile is byte-for-byte unchanged in behaviour and appearance** — the `@if (_isDesktop)` mobile branch and all mobile `@media` rules render identically to before. (If `PlayerSeekZone` is adopted by mobile, that is a *separate, later* change and out of scope here.) +9. **Scoped CSS contains only geometry/positioning/animation** — a reviewer scanning `AudioPlayerBar.razor.css` finds no colour values except `var(--mud-palette-*)` references (and ideally none at all beyond an optional hairline border). +10. **No "wrong theme flash" regression** — the surface colour is correct on first paint, since it now derives from the same `MudThemeProvider` that the existing prerender/dark-mode round-trip already seeds. + +--- + +## 7. Trade-offs and notes for the implementer + +- **`backdrop-filter: blur()` is dropped.** With an opaque themed surface there is nothing to blur. If Daniel specifically wants the frosted-glass look back, that's a deliberate re-add over a *translucent* surface — and it should use a MudBlazor-aware translucent surface, not a re-introduced alpha token. Default recommendation: drop it; opaque reads cleaner and is cheaper. +- **`MudStack` vs `MudToolBar`:** I recommend `MudStack` (§3.3). If the implementer finds `MudToolBar` cleaner for the row, that's an acceptable substitution *provided* the centre zone still flex-grows; flag it if `MudToolBar`'s fixed height fights the stacked seek+spectrum. +- **`PlayerTransportZone` / `PlayerSeekZone` are new files.** They add two components but remove ~four anonymous divs and one duplicated logic block. Net structural win. If Daniel prefers fewer files, the minimum viable version keeps `PlayerSeekZone` (it kills the duplication) and inlines the transport cluster as a `MudStack` without extracting a component. Recommend the full extraction; note the lighter option. +- **This is a desktop-only migration by request, which leaves the app in a mixed state**: desktop on the MudBlazor theme, mobile still on (dead) tokens. Mobile is *also* currently broken against the live palette for the same reason — its spectrum bars and any shared dead-token rules have no colour. Worth surfacing to Daniel: a follow-up to migrate mobile is implied, even though this task explicitly excludes it. Captured here so it isn't lost. + +--- + +## 8. Roadmap placement + +This work isn't currently in `PLAN.md`. It fits most naturally as a new item under **Phase 2 — Product surface** (it's a UI-surface correctness + polish task), or as a small standalone entry. Because it's partly a *bug fix* (broken theming against the live palette) and partly a *redesign*, recommend logging it as a Phase 2 item with a note that the dead-token breakage is the triggering defect. If Daniel approves this proposal, I'll draft the `PLAN.md` entry — and flag the implied mobile follow-up (§7) as a sibling item.