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

24 KiB
Raw Permalink Blame History

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

<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.
  • RoundingMudPaper 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 MudIconButtons 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:

<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-surfaceat 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.