Files
deepdrft/product-notes/mix-detail-hero-overlay.md

27 KiB
Raw Permalink Blame History

Mix Detail — Hero + MetaContent overlay rework (mirror the Sessions hero) — Design Spec

Status: shipped on dev (2026-06-16). Author: product-designer. Date: 2026-06-16.

0. Goal

Rework the Mix detail Hero + MetaContent so the cover art becomes a background image with all metadata laid out on top of it, inside a max-medium square cover-art region — mirroring the already-shipped Session detail hero-overlay composition. This consolidates the masthead + cover + meta into one overlaid block, frees vertical room for the lava-lamp visualizer behind the content, and cleans up the aesthetic so the Mix page reads as a member of the same design family as Sessions.

The Mix visualizer + eight-knob controls layout landed in the Phase 10 reframe (TopRowCenter slot, in-flow controls container between the back link and the lava-lamp) must be preserved unchanged — this rework touches only what sits below that top row.


1. What is actually shipped today (confirmed from live source, not spec)

Read of the live tree on 2026-06-16:

Session detail (Pages/SessionDetail.razor + .razor.css) — the target aesthetic, already shipped:

  • Does NOT use ReleaseDetailScaffold. It composes its own hero overlay directly inside a MudContainer MaxWidth="Large", and wires PlayTrack in its own @code block (the scaffold's play-toggle logic is duplicated here deliberately — see SessionDetail.razor lines 131145).
  • Structure: a .session-hero positioning context (position: relative; aspect-ratio: 16/10; max-height: 70vh; min-height: 420px; overflow: hidden; border-radius: 8px) holding:
    • .session-hero-img — a MudPaper whose background-image is the hero/cover image (background-size: cover), or a .session-hero-placeholder when none.
    • .session-hero-shim — a plain <div> darkening gradient (stronger top + bottom) for overlay legibility.
    • .session-hero-top — absolutely-positioned overlay row: genre chip + release-date + SharePopover.
    • .session-hero-bottom — absolutely-positioned overlay row: optional cover thumbnail + title/artist + PlayStateIcon.
  • The back link (.deepdrft-track-detail-back) sits above the hero, in normal flow.
  • CSS uses ::deep on every class that lands on a MudBlazor child component's native output (.session-hero-img on MudPaper, .session-overlay-chip.mud-chip on MudChip, .session-hero-bottom-row on MudStack, the play/share icon-color overrides on MudIconButton).

Mix detail (Pages/MixDetail.razor + .razor.css) — the page to rework, uses the scaffold:

  • <MixWaveformVisualizer> paints a fixed full-viewport backdrop (z-index: 0); a .mix-detail-foreground wrapper (z-index: 1) lifts the content above it; inside a MudContainer MaxWidth="Large" (class mix-detail-container) sits a <ReleaseDetailScaffold>.
  • The scaffold's TopRowCenter slot holds <MixVisualizerControls>; TopRightAction holds the lava-lamp MudIconButton (the Phase 10 reframe layout — do not touch).
  • The scaffold's Hero slot today is a .mix-detail-cover (square, max-width: 220px, centered) holding a MudPaper cover-art / placeholder. The MetaContent slot holds genre chip + release date. The masthead (title + artist) is rendered by the scaffold's default header region above the hero. The BodyContent slot holds the share row.

The scaffold (Controls/ReleaseDetailScaffold.razor + .razor.cs) — the shared chrome:

  • Vertical order: top row (back | TopRowCenter | TopRightAction) → TopContent → header (masthead + play, or a custom Header) → Hero → divider + MetaContent (gated by ShowMeta) → BodyContent → default track-keyed share row (gated by ShowShareRow).
  • Owns the back link and the play-toggle wiring. Cut and Track also consume it; Session does not.

Render boundary (both detail pages are identical here — confirmed): both SessionDetail and MixDetail derive from ReleaseDetailBase, run under InteractiveAuto, and bridge the prerendered release+track across the prerender→WASM seam via PersistentComponentState (keyed session-detail / mix-detail). The release DTO (title, artist, genre, release date, ImagePath) is available at first render in both passes — there is no render-mode divergence to design around. The image is served from api/image/{EscapeDataString(key)} in both pages. Conclusion: the data read at render time is the same shape and same timing for both; the overlay rework is purely presentational.


2. The DRY/SOLID question — where is the source of truth?

This is the load-bearing decision. The brief asks: extract a shared hero-overlay shell that both Sessions and Mixes consume, or per-page duplication?

The honest finding: the two pages are structurally divergent by design today

Sessions deliberately does not use the scaffold — it forks the whole chrome to get the overlay composition. Mixes does use the scaffold, and the scaffold's Hero/MetaContent/masthead are separate stacked regions, which is the opposite of "everything overlaid on the cover." So the two pages do not share a hero today, and the scaffold's stacked-region model is not the overlay model.

Three meaningfully different directions, in shape:

Direction A — Per-page copy. Inline the .session-hero overlay structure into MixDetail.razor, copy the .session-* CSS into MixDetail.razor.css (renamed .mix-hero-*), tune the square/medium sizing. No shared component.

  • Pro: fastest; zero risk to Sessions; Mix can diverge freely (it has a visualizer behind it that Sessions does not).
  • Con: two copies of the overlay cascade to keep in sync — exactly the drift the track-view-css- consolidation note spent a pass undoing. The shim gradient, the overlay-label typography, the ::deep icon-color overrides, the responsive wrap rules all get duplicated. A future overlay tweak has to land twice. Violates the "one source, multiple views" instinct (Daniel's standing preference).

Direction B — Extract a shared ReleaseHeroOverlay component, both pages consume it. Pull the .session-hero overlay (image/placeholder + shim + top overlay + bottom overlay) into a new Controls/ReleaseHeroOverlay.razor that takes the release data (+ optional play affordance + optional share slot + optional cover-thumb) and renders the overlaid hero. Sessions swaps its inline hero for the component; Mix renders the component inside its .mix-detail-foreground, bypassing the scaffold's Hero/MetaContent/masthead regions (Mix stops using the scaffold for the hero, same as Sessions already does).

  • Pro: one source of truth for the overlay — the headline DRY win. Honors "same data shape, different rendering": both pages feed the same release DTO into one overlay VM/parameter set, the divergence (visualizer backdrop, square-vs-wide aspect) rides parameters/CSS, not a second copy. SOLID: the overlay is a single-responsibility presentational component; the pages compose it.
  • Con: touches the already-shipped Sessions page (regression surface on a working view) and means Mix no longer routes its hero through the scaffold — Mix keeps the scaffold only for the back/controls top row, or drops the scaffold entirely (see §4 sub-decision). More upfront work; a real refactor.

Direction C — Teach the scaffold an overlay mode. Add an overlay-hero capability to ReleaseDetailScaffold so it can render its Hero/masthead/MetaContent as one overlaid block when a flag/slot says so; migrate Sessions onto the scaffold in overlay mode and switch Mix to overlay mode.

  • Pro: one component owns all detail chrome including the overlay; Sessions finally joins the scaffold.
  • Con: largest blast radius and worst SOLID. It bloats the scaffold with a second layout personality (stacked-regions and overlaid-hero) gated by a flag — exactly the "variance rides a flag" anti-pattern the scaffold's own convention forbids (Phase 9 §5.3: "layout variance rides a slot, never a boolean"). It also drags the working Sessions page through a chrome migration for no user-visible gain. Over-engineered for a two-consumer overlay.

Recommendation: Direction B — extract ReleaseHeroOverlay, both pages consume it.

Rationale, with the trade-off stated plainly:

  • The source of truth should be one overlay component, not the scaffold and not a copy. Sessions already proved the overlay wants to live outside the scaffold's stacked-region model — forcing it back into the scaffold (Direction C) fights that and bloats shared chrome. Copying it (Direction A) reintroduces the exact cascade-duplication this codebase already paid down once.
  • It satisfies "one source, multiple views" directly: one overlay fed the same release DTO, rendering differences (Mix's visualizer backdrop, the square medium cover vs. Sessions' wide hero) expressed as component parameters + a CSS class, never as a forked structure.
  • The cost is real and must be owned: Direction B edits the shipped Sessions page. That is a regression surface on a working view. Mitigation: extract by moving Sessions' exact current markup + CSS into the component first (a behavior-preserving lift — Sessions should look pixel-identical after), then point Mix at it. The Sessions migration is the risky step; treat it as its own wave with a before/after visual check (§7 acceptance).

If Daniel wants to minimize risk to Sessions and ship Mix fast, Direction A is the acceptable fallback — but it takes on the duplication debt knowingly, and a later consolidation pass (like the track-card one) becomes likely. Recommended only if the Sessions page is considered too load-bearing to touch right now. Flagging this as the one open decision for Daniel (§8, Q1).


3. The shared component — ReleaseHeroOverlay (Direction B shape)

A new plain-shell presentational component: DeepDrftPublic.Client/Controls/ReleaseHeroOverlay.razor (+ .razor.css carrying the overlay cascade, moved from SessionDetail.razor.css).

Responsibility (single): given a release's display data, render the background-image hero with the metadata overlaid (top row: genre/date + share slot; bottom row: optional cover thumb + title/artist + optional play affordance). It owns no data fetch, no player wiring beyond invoking a passed-in callback, no JS interop.

Parameters (the "same data shape" contract):

Parameter Type Purpose
HeroImageKey string? The background image entry key (Sessions: hero-then-cover precedence; Mix: cover). Null → placeholder.
PlaceholderIcon string Material icon for the no-image placeholder (Sessions: Piano; Mix: Album).
CoverThumbKey string? Optional small cover thumbnail shown in the bottom row (Sessions shows it only when it differs from the hero image; Mix likely null — see §4).
Title / Artist string / string? Overlaid title + artist.
Genre string? Genre chip (top overlay) when present.
ReleaseDate DateOnly? Release date (top overlay) when present.
ShareContent RenderFragment? The share affordance (each page passes its SharePopover with the right release params).
PlayContent RenderFragment? The play affordance (each page passes its PlayStateIcon wired to its own toggle).
Class string? Extra class for per-page aspect/sizing variance (mix-hero vs default wide).

Play/share ride as slots, not as wired-in player logic, so the component stays presentational and each page keeps owning its own play-toggle (Sessions already does; the scaffold does for Mix today — this preserves that ownership without the component reaching for the cascaded player).

House constraint — plain-div shell. The overlay shell (.release-hero, .release-hero-shim, .release-hero-top, .release-hero-bottom) is plain <div>s with project CSS classes, exactly as Sessions does today. The background-image surface stays a MudPaper only because Sessions already uses one there — but note it carries no card affordances; if staff-engineer prefers, it can become a plain <div class="release-hero-img"> with the same background-image style, which is more aligned with the plain-shell rule. Recommendation: make it a plain <div> and drop the MudPaper — there is no MudPaper behavior being used (Elevation=0, Square=true), so the Mud wrapper is dead weight. This is a small improvement over the shipped Sessions code; flag it to Daniel as an incidental cleanup (§8, Q2). Chips/icons remain Mud components (they carry real behavior) and keep their ::deep overrides.


4. Mix-specific composition + the square medium cover

Daniel's ask: "metadata laid out on top of a max-medium square cover-art region." Two design notes specific to Mix that differ from Sessions:

4a. Aspect ratio — square medium, not Sessions' wide hero

Sessions uses aspect-ratio: 16/10; max-height: 70vh; min-height: 420px (a wide, tall hero). Daniel wants Mix to use a square region at a max-medium size. The current Mix cover is max-width: 220px (small). "Max-medium square" reads as: a centered aspect-ratio: 1/1 block, capped at a medium width — recommend max-width: 420480px (medium: bigger than the 360px track cover, smaller than the Large container) — so the overlaid metadata has room to sit on the cover without crowding, while the lava-lamp visualizer keeps the surrounding canvas. This rides the Class parameter (mix-hero) + a Mix CSS rule, not a forked component.

  • Why square, not wide: the Mix cover art is square album art; a 16/10 crop would letterbox or distort it, and the point is to free room for the visualizer, which a smaller square does better than a full-bleed wide hero.
  • Open sub-question (§8, Q3): on a square overlay, the top overlay row (genre/date/share) + bottom overlay row (title/artist/play) over a 480px square may feel cramped versus Sessions' tall hero. Acceptable mitigation: keep the same two-overlay structure but let the shim carry more darkening, or drop the cover thumbnail (4b). Daniel tunes on screen per his standing preference.

Sessions shows a small cover thumb in the bottom overlay only when the hero image differs from the cover (it has a dedicated hero image distinct from the cover). Mix has no separate hero image — the cover art is the background. So a cover thumbnail would duplicate the background. Recommend Mix passes CoverThumbKey = null — the bottom overlay is just title/artist + play. This falls out of the shared component's existing showCover logic for free.

4c. Mix keeps the scaffold for the top row only — or drops it

With the hero now a self-composed overlay (not the scaffold's Hero/MetaContent/masthead regions), Mix has a sub-decision:

  • Option (i) — keep ReleaseDetailScaffold for the back/controls top row only. Mix keeps supplying TopRowCenter (controls) + TopRightAction (lava-lamp) to the scaffold, but stops supplying Hero / MetaContent / the default masthead, and instead renders <ReleaseHeroOverlay> in BodyContent (or a new slot). Problem: the scaffold's default header region renders the masthead (title+artist) and a second PlayStateIcon — which would now duplicate the overlay's title/artist/play. Suppressing the scaffold's masthead requires the Header slot to render nothing, which is awkward (the slot exists to replace the masthead, and an empty Header is a smell).
  • Option (ii) — Mix drops the scaffold, composes directly like Sessions does. Mix renders the back link + the controls top row + the lava-lamp + <ReleaseHeroOverlay> itself inside .mix-detail-foreground, mirroring how Sessions composes directly. But this loses the scaffold's ownership of the back link and the three-zone top-row structure that the Phase 10 reframe specifically built into the scaffold (TopRowCenter), and would duplicate that row.

Recommendation: Option (i), with the scaffold's masthead suppressed by passing an empty-but-present Header fragmentor, cleaner, the overlay renders inside the scaffold's existing Hero slot and Mix passes a Header fragment that renders nothing so the scaffold contributes only the top row + hero. The least-smelly realization: keep the scaffold (it owns the Phase 10 top row — the constraint we must preserve), put <ReleaseHeroOverlay> in the Hero slot, leave MetaContent null (metadata now lives in the overlay), and pass a no-op Header fragment to suppress the duplicate masthead/play. This preserves the visualizer/controls layout exactly (§5) while moving the hero+meta into the overlay.

Flag for staff-engineer: an empty Header fragment to suppress the masthead is slightly awkward but is the lowest-risk way to keep the Phase 10 top row intact. If it reads badly in code, the alternative is a small scaffold change (a ShowHeader gate mirroring ShowMeta/ShowShareRow) — that is a minimal, slot-consistent scaffold edit (a gate, not a layout flag), acceptable under the Phase 9 §5.3 convention because it suppresses an optional region rather than switching layouts. Recommend the ShowHeader gate over the empty-fragment hack if staff-engineer touches the scaffold anyway. Noted as §8 Q4.


5. Preserving the Phase 10 visualizer + controls layout (hard constraint)

The Phase 10 reframe put the controls in the scaffold's TopRowCenter slot and the lava-lamp in TopRightAction, with the in-flow grow/collapse behavior and the flex-wrap responsive drop. This rework must not disturb any of that. Concretely:

  • Keep the scaffold and its three-zone top row. Mix continues supplying TopRowCenter (<MixVisualizerControls>) and TopRightAction (the lava-lamp MudIconButton) unchanged. The _controlsExpanded flag, ToggleSettings, and the filled/outline lamp glyph are untouched.
  • The overlay goes below the top row, in the Hero slot — it occupies the space the old .mix-detail-cover + masthead + MetaContent occupied. Net effect: the region below the controls row gets shorter (one overlaid block instead of masthead + 220px cover + meta divider + meta row), which frees more canvas for the lava-lamp visualizer — exactly Daniel's stated goal.
  • MixWaveformVisualizer is unchanged. It is a fixed full-viewport backdrop at z-index: 0; the overlay rides inside .mix-detail-foreground (z-index: 1) like the current cover does. The footer clip / lava-rest-line work (Phase 10 §2c) is independent of this and unaffected.
  • MetaContent becomes null (metadata moves into the overlay), so the scaffold's divider + .deepdrft-track-detail-meta row no longer renders for Mix. The share row moves into the overlay's ShareContent slot (matching Sessions, which overlays share top-right).

Acceptance tie-in: after the rework, expanding the controls still grows the in-flow container between back and lamp, still wraps on narrow widths, and the lava-lamp still toggles it — no change to that interaction (§7).


6. CSS approach

  • The overlay cascade moves into ReleaseHeroOverlay.razor.css (Direction B), renamed from .session-* to neutral .release-hero-*. Sessions' SessionDetail.razor.css keeps only what is page-specific (the .session-detail-page padding, the per-page aspect overrides if any); Mix's MixDetail.razor.css keeps only the .mix-detail-foreground z-index lift + the mix-hero square/ medium sizing override.
  • ::deep is required wherever a class lands on a MudBlazor child component's native output — this is unchanged from Sessions today and must carry into the shared component's scoped CSS:
    • the background-image surface if it stays MudPaper (::deep .release-hero-img) — avoidable by making it a plain <div>, §3, which removes the ::deep need for that element entirely;
    • the genre chip on MudChip (::deep .release-overlay-chip.mud-chip);
    • the bottom-row MudStack wrap (::deep .release-hero-bottom-row);
    • the play/share icon-color overrides on MudIconButton/MudProgressCircular (::deep .release-hero-play .mud-icon-button, etc.).
  • The square/medium aspect for Mix is a single override scoped to the mix-hero class (aspect-ratio: 1/1; max-width: ~480px; margin-inline: auto) — it overrides the component's default wide aspect. This is the "divergence rides CSS, not structure" realization of the one-source rule.
  • No MudCard/MudPaper shell for the overlay container (house rule). The .release-hero positioning context, the shim, and both overlay rows are plain <div>s — as Sessions already does. The only Mud wrappers that remain are the chip, the icon buttons, and (optionally) the image surface — each carrying real component behavior, none acting as a layout shell. If staff-engineer reaches for a Mud wrapper as a layout shell, stop and flag it (per the standing constraint).

7. Acceptance criteria (observable)

  1. Cover-as-background. On a Mix detail page with a cover image, the cover renders as a background-image hero (background-size: cover), not as a bordered thumbnail; metadata sits overlaid on top of it.
  2. Square medium region. The hero is a centered square (aspect-ratio: 1/1) capped at a medium width (~480px), visibly larger than the old 220px cover, with the surrounding canvas left to the visualizer.
  3. All metadata overlaid. Title, artist, genre (when present), release date (when present), and the share affordance all render over the cover image — no separate masthead block, no separate meta divider/row below the cover.
  4. Placeholder path. A Mix with no cover image shows the placeholder treatment (Album icon over the soft-secondary gradient) with metadata still legibly overlaid.
  5. Legibility. The darkening shim keeps overlaid text readable over light and dark cover art, in both light and dark theme.
  6. Visualizer/controls preserved. The lava-lamp toggle still opens the in-flow eight-knob controls container between the back link and the lamp; it still grows in place, still wraps on narrow widths, and the lava-lamp glyph still swaps filled/outline. No regression to the Phase 10 layout.
  7. More canvas for the visualizer. The content block below the controls row is shorter than before (one overlaid hero vs. masthead + cover + meta), leaving more visible visualizer area.
  8. Sessions unchanged (Direction B). After the shared-component extraction, the Session detail page renders pixel-identically to before (before/after visual check on a session with a dedicated hero image, a session with cover-only, and a session with no image).
  9. One source of truth (Direction B). The overlay markup + cascade exists in exactly one component; grepping for .session-hero / .release-hero finds no duplicate structure across the two pages.
  10. No Mud layout shell. The overlay container, shim, and overlay rows are plain <div>s; no MudCard/MudPaper acts as a layout wrapper (image surface excepted only if kept as MudPaper).

8. Open questions for Daniel

  1. DRY decision (the load-bearing one). Recommended Direction B — extract a shared ReleaseHeroOverlay both pages consume (one source of truth; edits the shipped Sessions page). Fallback Direction A — per-page copy in Mix (fast, zero Sessions risk, takes on duplication debt). Which do you want? (B is the "one source, multiple views" call; A is the play-it-safe-on-Sessions call.)
  2. Drop the MudPaper on the image surface for a plain <div class="release-hero-img"> while we're in here? It carries no Mud behavior (Elevation=0/Square=true), so it is dead weight and a plain div is more on-spec with the plain-shell rule. Incidental cleanup — yes/no?
  3. Square hero size. Recommended ~480px max-width square. Crowded vs. roomy is a feel call you'll tune on screen — is ~480px the right starting point, or do you want it bigger (closer to the Sessions hero scale) / smaller (more visualizer room)?
  4. Suppressing the duplicate masthead. Mix's hero now carries title/artist/play, so the scaffold's default masthead must be suppressed. Recommended: add a small ShowHeader gate to the scaffold (slot-consistent with ShowMeta/ShowShareRow), vs. the hackier empty-Header-fragment. OK to add the ShowHeader gate?
  5. Square overlay crowding (deferrable). If the genre/date/share top row + title/artist/play bottom row feel cramped on a 480px square, are you fine tuning shim/spacing on screen, or do you want a different overlay arrangement for the square (e.g. a single bottom-stacked overlay) speced now?

9. File-change inventory (for staff-engineer, Direction B)

New:

  • DeepDrftPublic.Client/Controls/ReleaseHeroOverlay.razor (+ .razor.css) — the shared overlay, lifted from Sessions' current hero markup + CSS, parameterized per §3.

Edited:

  • DeepDrftPublic.Client/Pages/SessionDetail.razor — replace the inline .session-hero block with <ReleaseHeroOverlay ... />, passing its hero/cover precedence, share, and play slots. Behavior- preserving (must render identically).
  • DeepDrftPublic.Client/Pages/SessionDetail.razor.css — remove the overlay cascade now living in the component; keep only page-specific rules.
  • DeepDrftPublic.Client/Pages/MixDetail.razor — replace the .mix-detail-cover Hero slot with <ReleaseHeroOverlay Class="mix-hero" ... CoverThumbKey="null" />; drop MetaContent; move the share row into the overlay's ShareContent slot; suppress the scaffold masthead (per Q4). TopRowCenter / TopRightAction / the visualizer / _controlsExpanded wiring untouched.
  • DeepDrftPublic.Client/Pages/MixDetail.razor.css — keep .mix-detail-foreground; add the mix-hero square/medium sizing override; remove .mix-detail-cover.
  • DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor[.cs]only if Q4 = ShowHeader gate: add a bool ShowHeader = true gate around the default header region.
  • DeepDrftPublic/wwwroot/styles/deepdrft-styles.css — no change required; .deepdrft-track-detail- cover* stays (Track detail still uses it). The .mix-detail-container .deepdrft-track-detail- container width override stays (the scaffold still hosts the top row).

For Direction A (fallback): no new component; the overlay markup is inlined into MixDetail.razor and the .session-* cascade is copied + renamed into MixDetail.razor.css. Sessions untouched.