Files
deepdrft/product-notes/audio-player-desktop-redesign.md
2026-06-04 18:49:23 -04:00

218 lines
24 KiB
Markdown
Raw Permalink 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.
# 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 `<div>`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 `<div class="controls-left d-flex flex-column...">` with inline logic for the progress circle.
- The centre zone (seek slider + spectrum) is an anonymous `<div class="d-flex flex-column flex-grow-1">` that *also* owns the pointer-event seek handlers inline in the markup.
- The window controls (minimize/close) are an absolutely-positioned `<div class="player-controls">` 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 `<div>`
`PlayerControls`, `VolumeControls`, `TimestampLabel`, `SpectrumVisualizer` each open with a hand-rolled `<div class="...">` 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
<MudPaper Elevation="8"
Class="player-surface"
Style="border-radius: var(--mud-default-borderradius);">
...
</MudPaper>
```
- **`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` (`<div d-flex flex-column>`, `position:fixed`) | Keep a thin scoped-CSS `<div class="player-dock">` **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` (`<div>`) | **`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 `<div class="d-flex align-center gap-3">` with a **`MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3"`**. Each child is a named zone:
```razor
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3" Class="player-row">
@* LEFT ZONE — transport + timestamp *@
<PlayerTransportZone IsPlaying="IsPlaying" IsLoaded="IsLoaded"
IsLoading="IsLoading" IsStreaming="IsStreaming"
LoadProgress="LoadProgress"
DisplayTime="DisplayTime" Duration="Duration"
TogglePlayPause="TogglePlayPause" Stop="Stop" />
@* CENTRE ZONE — seek + spectrum (flex-grow) *@
<PlayerSeekZone DisplayTime="DisplayTime" Duration="Duration"
CanSeek="CanSeek"
OnSeekStart="OnSeekStart" OnSeekEnd="OnSeekEnd"
OnSeekChange="OnSeekChange"
Class="flex-grow-1" />
@* RIGHT ZONE — volume *@
<VolumeControls Volume="Volume" VolumeChanged="OnVolumeChange" />
</MudStack>
```
| Zone | Current markup | New home |
| --- | --- | --- |
| Left (transport + spinner + timestamp) | `<div class="controls-left d-flex flex-column align-center gap-2">` 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) | `<div class="d-flex flex-column flex-grow-1">` 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) | `<div class="volume-right">` wrapping `VolumeControls` | `VolumeControls` directly — the wrapper div was empty of behaviour (its only CSS rule was commented out) |
| Window controls (top-right) | absolutely-positioned `<div class="player-controls">` | **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 `<div class="player-buttons">` (flex/gap CSS) with `<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">`. Delete `PlayerControls.razor.css` entirely — its only rule is the flex container `MudStack` now provides. Buttons unchanged. |
| **`VolumeControls`** | Minor internal swap | Replace `<div class="volume-controls">` with `<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">`. 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 | `<div class="timestamp-display">` → optional `<MudStack Row Justify="Justify.Center">` 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 4050 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 `<div class="d-flex gap-*">`. 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.