27 KiB
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 + seven-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 aMudContainer MaxWidth="Large", and wiresPlayTrackin its own@codeblock (the scaffold's play-toggle logic is duplicated here deliberately — seeSessionDetail.razorlines 131–145). - Structure: a
.session-heropositioning context (position: relative; aspect-ratio: 16/10; max-height: 70vh; min-height: 420px; overflow: hidden; border-radius: 8px) holding:.session-hero-img— aMudPaperwhosebackground-imageis the hero/cover image (background-size: cover), or a.session-hero-placeholderwhen 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
::deepon every class that lands on a MudBlazor child component's native output (.session-hero-imgonMudPaper,.session-overlay-chip.mud-chiponMudChip,.session-hero-bottom-rowonMudStack, the play/share icon-color overrides onMudIconButton).
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-foregroundwrapper (z-index: 1) lifts the content above it; inside aMudContainer MaxWidth="Large"(classmix-detail-container) sits a<ReleaseDetailScaffold>.- The scaffold's
TopRowCenterslot holds<MixVisualizerControls>;TopRightActionholds the lava-lampMudIconButton(the Phase 10 reframe layout — do not touch). - The scaffold's
Heroslot today is a.mix-detail-cover(square,max-width: 220px, centered) holding aMudPapercover-art / placeholder. TheMetaContentslot holds genre chip + release date. The masthead (title + artist) is rendered by the scaffold's default header region above the hero. TheBodyContentslot 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 customHeader) →Hero→ divider +MetaContent(gated byShowMeta) →BodyContent→ default track-keyed share row (gated byShowShareRow). - 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- consolidationnote spent a pass undoing. The shim gradient, the overlay-label typography, the::deepicon-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: 420–480px (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.
4b. No cover thumbnail in the bottom row (recommended)
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
ReleaseDetailScaffoldfor the back/controls top row only. Mix keeps supplyingTopRowCenter(controls) +TopRightAction(lava-lamp) to the scaffold, but stops supplyingHero/MetaContent/ the default masthead, and instead renders<ReleaseHeroOverlay>inBodyContent(or a new slot). Problem: the scaffold's default header region renders the masthead (title+artist) and a secondPlayStateIcon— which would now duplicate the overlay's title/artist/play. Suppressing the scaffold's masthead requires theHeaderslot to render nothing, which is awkward (the slot exists to replace the masthead, and an emptyHeaderis 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 fragment — or, 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
Headerfragment 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 (aShowHeadergate mirroringShowMeta/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 theShowHeadergate 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>) andTopRightAction(the lava-lampMudIconButton) unchanged. The_controlsExpandedflag,ToggleSettings, and the filled/outline lamp glyph are untouched. - The overlay goes below the top row, in the
Heroslot — it occupies the space the old.mix-detail-cover+ masthead +MetaContentoccupied. 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. MixWaveformVisualizeris unchanged. It is a fixed full-viewport backdrop atz-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.MetaContentbecomes null (metadata moves into the overlay), so the scaffold's divider +.deepdrft-track-detail-metarow no longer renders for Mix. The share row moves into the overlay'sShareContentslot (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.csskeeps only what is page-specific (the.session-detail-pagepadding, the per-page aspect overrides if any); Mix'sMixDetail.razor.csskeeps only the.mix-detail-foregroundz-index lift + themix-herosquare/ medium sizing override. ::deepis 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::deepneed for that element entirely; - the genre chip on
MudChip(::deep .release-overlay-chip.mud-chip); - the bottom-row
MudStackwrap (::deep .release-hero-bottom-row); - the play/share icon-color overrides on
MudIconButton/MudProgressCircular(::deep .release-hero-play .mud-icon-button, etc.).
- the background-image surface if it stays
- The square/medium aspect for Mix is a single override scoped to the
mix-heroclass (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/MudPapershell for the overlay container (house rule). The.release-heropositioning 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)
- 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. - 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. - 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.
- Placeholder path. A Mix with no cover image shows the placeholder treatment (Album icon over the soft-secondary gradient) with metadata still legibly overlaid.
- Legibility. The darkening shim keeps overlaid text readable over light and dark cover art, in both light and dark theme.
- Visualizer/controls preserved. The lava-lamp toggle still opens the in-flow seven-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.
- 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.
- 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).
- One source of truth (Direction B). The overlay markup + cascade exists in exactly one component;
grepping for
.session-hero/.release-herofinds no duplicate structure across the two pages. - No Mud layout shell. The overlay container, shim, and overlay rows are plain
<div>s; noMudCard/MudPaperacts as a layout wrapper (image surface excepted only if kept asMudPaper).
8. Open questions for Daniel
- DRY decision (the load-bearing one). Recommended Direction B — extract a shared
ReleaseHeroOverlayboth 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.) - Drop the
MudPaperon 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? - 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)?
- 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
ShowHeadergate to the scaffold (slot-consistent withShowMeta/ShowShareRow), vs. the hackier empty-Header-fragment. OK to add theShowHeadergate? - 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-heroblock 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-coverHeroslot with<ReleaseHeroOverlay Class="mix-hero" ... CoverThumbKey="null" />; dropMetaContent; move the share row into the overlay'sShareContentslot; suppress the scaffold masthead (per Q4).TopRowCenter/TopRightAction/ the visualizer /_controlsExpandedwiring untouched.DeepDrftPublic.Client/Pages/MixDetail.razor.css— keep.mix-detail-foreground; add themix-herosquare/medium sizing override; remove.mix-detail-cover.DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor[.cs]— only if Q4 = ShowHeader gate: add abool ShowHeader = truegate 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- containerwidth 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.