fix(theme): green hero Share/Play/Queue glyphs in dark via shared .dd-accent-icon

Fold Session/Mix hero glyphs into the reusable accent-icon treatment so they reach
the glyph (beating .mud-secondary-text) green-accent in both themes; drop the dead
wrapper white rule and the redundant dark-only hero override. Light pixel-identical.
This commit is contained in:
daniel-c-harvey
2026-06-20 02:21:11 -04:00
parent 4c56eededc
commit 2fbb1c9b95
7 changed files with 72 additions and 67 deletions
+1
View File
@@ -82,6 +82,7 @@ Keep this seam clean — it is the most architecturally load-bearing part of the
- `DarkModeSettings` lives in `DeepDrftPublic.Client.Common` (consumed by both server prerender and client components).
- **Theme-aware token layer:** `DeepDrftShared.Client/wwwroot/styles/deepdrft-tokens.css` defines two kinds of CSS custom properties. *Source tokens* (`--deepdrft-navy`, `--deepdrft-white`, `--deepdrft-green-accent`, etc.) are brand constants — identical in `:root` and `.deepdrft-theme-dark`. *Theme-aware aliases* are defined in both blocks and flip when the theme wrapper class changes. Component and page CSS must bind the **alias**, not the source token, so neutral surfaces invert for free. Current alias families: `--deepdrft-page-surface`/`-text`/`-text-muted` (neutral page backgrounds and text), `--deepdrft-play-chip`/`-glyph`/`-chip-soft` (play-state icon chip and glyph), `--deepdrft-popover-surface` (default MudBlazor popover background — light: `color-mix(navy 4%, white)`, a near-page-background surface; dark: references source token `--deepdrft-popover-surface-dark`, a `color-mix(navy-mid 80%, green-accent 20%)` bluer navy defined once in `:root` and referenced by both the `.deepdrft-theme-dark` wrapper block and `body.deepdrft-theme-dark` so portaled popovers are reached). The bespoke glass panels (visualizer/queue/privacy) now bind their own theme-aware `--deepdrft-panel-surface`/`-text`/`-text-muted`/`-border`/`-row-hover` family: dark-glass charcoal (sourced from the `--deepdrft-panel-ground` constant) with light text in dark theme, and a light translucent glass with dark text in light theme. These tokens are re-declared in `body.deepdrft-theme-dark` because the panels are MudOverlay panels that portal to `<body>` (same portal scope as popovers); the `--deepdrft-panel-ground` source token is now consumed only via the dark `--deepdrft-panel-surface` value.
- **Portaled-popover body-class bridge:** MudBlazor popovers portal to `<body>`, outside the `.deepdrft-theme-dark` wrapper `<div>`, so the dark popover token never reached them. Fix: `MainLayout.razor` stamps `deepdrft-theme-dark` on `<body>` via the `setBodyThemeClass(isDark)` helper in `DeepDrftShared.Client/Interop/theme/theme.ts` (lazy-imported as `_content/DeepDrftShared.Client/js/theme/theme.js`). The call fires only on first render or when `_isDarkMode` actually changes (gated by `_lastAppliedDarkMode` comparison) to avoid redundant JS calls on unrelated re-renders. The `body.deepdrft-theme-dark` selector in `deepdrft-tokens.css` resolves `--deepdrft-popover-surface` from `--deepdrft-popover-surface-dark` for these portaled elements.
- **Interactive-accent icon treatment (`.dd-accent-icon` / `.dd-accent-fill`):** one reusable rule in `DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` for green-accent interactive icon affordances (Play / Share / Add-to-Queue / lava-lamp trigger), replacing the former pile of per-site dark overrides. Wrap the affordance container in `.dd-accent-icon` to colour its glyphs green-accent in both themes; add `.dd-accent-fill` when the container also holds a `Color.Secondary` filled button that must go green-accent in dark. It is a CSS class (not a palette `Color`) because no MudBlazor `Color` enum is green in both themes, and it targets `.dd-accent-icon .mud-icon-button .mud-icon-root` with `!important` (0,3,0) to beat MudBlazor's `.mud-secondary-text { …!important }` on the glyph svg. The Session/Mix release-detail hero Share/Play glyphs use this class too (already green-accent in light via `Color.Secondary`, so folding them in keeps light pixel-identical and fixes dark). The one genuinely theme-divergent affordance (gas-lamp toggle = inherited nav text in light) keeps its own dark-only rule. New green-accent icons use this class, not a new override. (Convention detail in `DeepDrftPublic.Client/CLAUDE.md`.)
- Typography: Google Fonts (Bodoni Moda, Cormorant, DM Sans). Hand-rolled gas-lamp icon (lit/unlit) lives in `DeepDrftShared.Client/Common/DDIcons.cs`.
### TypeScript interop, not raw JS
+6
View File
@@ -140,6 +140,12 @@ Component state lives in ViewModels (registered scoped in DI). Components render
- CSS classes prefixed `deepdrft-` live in `DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` (shared across server and client).
- Custom SVG icons: `DeepDrftShared.Client/Common/DDIcons.cs` (hand-rolled gas-lamp, lava-lamp, etc. — shared across public and CMS surfaces).
### Interactive-accent icons (`.dd-accent-icon` / `.dd-accent-fill`)
Green-accent interactive icon affordances (Play / Share / Add-to-Queue / lava-lamp trigger, etc.) use a **single reusable treatment** in `deepdrft-styles.css`, not per-site dark overrides. Wrap the affordance(s) in a container carrying `.dd-accent-icon`; the rule colours the inner `.mud-icon-root` glyph green-accent (`--deepdrft-green-accent`, the brand constant — same value in both palettes) in **both** themes. Add `.dd-accent-fill` to the same container when it also holds a filled `Color.Secondary` `MudButton` whose fill must go green-accent in **dark** (dark-only — light already renders green fill + white text).
Two reasons this is needed and why it's a class, not a palette colour: (1) no MudBlazor `Color` enum is green in both themes (`Dark.Secondary` is off-white), so palette-only solutions can't express "green in both"; (2) MudBlazor stamps `.mud-secondary-text { color: …secondary !important }` on the glyph `<svg>`, so wrapper-level overrides never reach it — the reusable rule targets `.dd-accent-icon .mud-icon-button .mud-icon-root` (specificity 0,3,0 + `!important`) to beat it. The Session/Mix release-detail hero Share/Play glyphs use this class too: they were already green-accent in light (via `Color.Secondary``Light.Secondary`), so folding them in keeps light pixel-identical while fixing the dark over-image glyphs — they are not actually theme-divergent. The one genuinely theme-divergent affordance (the gas-lamp toggle = inherited nav text in light) does **not** use this class — it keeps a narrow dark-only rule. **Add new green-accent icon affordances by applying this class, not by spawning a new dark override.**
## Development commands
```bash
@@ -52,7 +52,7 @@
</MudStack>
@if (ShareContent is not null)
{
<div class="release-hero-share">
<div class="release-hero-share dd-accent-icon">
@ShareContent
</div>
}
@@ -74,7 +74,7 @@
</div>
@if (PlayContent is not null)
{
<div class="release-hero-play">
<div class="release-hero-play dd-accent-icon">
@PlayContent
</div>
}
@@ -151,14 +151,10 @@
flex: 0 0 auto;
}
/* The play affordance and share button sit over a dark image — force their icon glyphs to the
light theme color regardless of MudBlazor's Secondary palette. Both PlayStateIcon and
SharePopover render MudIconButton / MudProgressCircular internals, so ::deep is required. */
::deep .release-hero-play .mud-icon-button,
::deep .release-hero-play .mud-progress-circular,
::deep .release-hero-share .mud-icon-button {
color: var(--deepdrft-white);
}
/* The play/share glyphs are coloured by the shared .dd-accent-icon treatment (green-accent in
both themes) applied on .release-hero-play / .release-hero-share in ReleaseHeroOverlay.razor —
see deepdrft-styles.css. No co-located colour rule here: a wrapper-level override could not
reach the .mud-secondary-text !important glyph anyway. */
@media (max-width: 599.98px) {
.release-hero {
@@ -29,7 +29,7 @@
the shared WaveformVisualizerControlState and raises Changed; the visualizer bridge subscribes. This
host only toggles open/closed and centers the panel — it stays purely presentational. *@
<div class="dd-lava-lamp-trigger">
<div class="dd-accent-icon">
<MudTooltip Text="Visualizer settings">
<MudIconButton Icon="@(_open ? DDIcons.LavaLampFilled : DDIcons.LavaLamp)"
Size="@IconSize"
+2 -2
View File
@@ -83,7 +83,7 @@ else
</div>
}
<div class="cut-detail-actions">
<div class="cut-detail-actions dd-accent-icon dd-accent-fill">
@* Header Play loads the full album into the queue at index 0 (§3.4 seam,
closed P11 W1). Disabled until at least one streamable track is resolved. *@
<MudButton Variant="Variant.Filled"
@@ -133,7 +133,7 @@ else
{
var track = ViewModel.Tracks[i];
var index = i;
<div class="cut-detail-track-row">
<div class="cut-detail-track-row dd-accent-icon">
<span class="cut-detail-track-number">@track.TrackNumber</span>
<div class="cut-detail-track-play">
<PlayStateIcon Track="@track"
@@ -751,68 +751,70 @@ body:has(.waveform-visualizer-control-overlay) {
}
}
/* Dark-theme interactive affordances in the Cut detail header and track rows.
In dark mode, Color.Secondary resolves to off-white (#FAFAF8), making filled
and icon buttons unreadable on the light-ish dark-navy surface. Override to
green (--deepdrft-primary = green-accent in dark, --deepdrft-navy as text) to
match the "green = interactive" convention (see hero button dark rule above).
Scoped to .cut-detail-actions and .cut-detail-track-row so the hero-overlay
icons (.release-hero-play / .release-hero-share, forced white in
ReleaseHeroOverlay.razor.css) are never touched. */
/* =============================================================================
INTERACTIVE-ACCENT ICON TREATMENT (.dd-accent-icon / .dd-accent-fill)
----------------------------------------------------------------------------
The single, reusable green-accent treatment for interactive icon affordances —
replaces the per-site dark-mode overrides that previously had to fight the palette.
/* Play button (Variant.Filled, Color.Secondary) in the cut detail header */
.deepdrft-theme-dark .cut-detail-actions .mud-button-filled {
background-color: var(--deepdrft-primary);
color: var(--gradient-base);
WHY a class and not a palette colour: no MudBlazor Color enum is green in BOTH
themes (Dark.Secondary is off-white, Dark.Primary is green; Light.Secondary is
green, Light.Primary is navy), so every "green in both" affordance had to be
patched per-site. --deepdrft-green-accent (#3D7A68) is the brand constant — the
SAME value in both palettes — so a non-theme-scoped rule is correct: light already
renders these glyphs green-accent (via Color.Secondary → Light.Secondary), so this
keeps light pixel-identical while fixing dark.
WHY it reaches the glyph: MudBlazor colours a Color.Secondary icon by stamping
.mud-secondary-text on the inner .mud-icon-root <svg>, and that rule is `!important`
(color: var(--mud-palette-secondary) !important). Targeting only the .mud-icon-button
wrapper therefore never wins — the svg keeps its own !important colour. The documented
override bug. To beat an !important declaration we need our own !important AND equal-or-
higher specificity: .dd-accent-icon .mud-icon-root (0,2,0) ties .mud-icon-root.mud-
secondary-text (0,2,0) and wins on source order (deepdrft-styles.css is linked LAST in
App.razor, after MudBlazor + isolated CSS). The .mud-icon-button selector carries the
Color.Inherit affordances (lava-lamp glyph inherits the wrapper colour, no
.mud-secondary-text to fight); the spinner covers the PlayStateIcon loading state.
Apply .dd-accent-icon to a CONTAINER of the affordance(s); add .dd-accent-fill
alongside it when the container ALSO holds a filled MudButton whose Color.Secondary
fill must go green-accent in dark (a filled button is a background fill, not a glyph —
light already renders green-accent fill + white text, so .dd-accent-fill is DARK-ONLY
to keep light pixel-identical). The Session/Mix hero Share/Play glyphs use this class
too (they were already green-accent in light via Color.Secondary, so folding them in
keeps light pixel-identical and fixes dark — the over-image glyphs are not actually
theme-divergent). The one genuinely theme-divergent affordance (gas-lamp = inherited
nav text in light) does NOT use this class — it keeps a dark-only rule below.
The glyph rule targets glyphs inside an ICON button (.mud-icon-button .mud-icon-root)
only — the filled Play button is a .mud-button-filled (not .mud-icon-button), so its
StartIcon is naturally excluded and keeps its own contrast colour (white in light,
navy in dark). The bare .mud-icon-button selector carries the Color.Inherit case
(lava-lamp glyph inherits the wrapper colour); the spinner covers the loading state. */
.dd-accent-icon .mud-icon-button .mud-icon-root,
.dd-accent-icon .mud-icon-button,
.dd-accent-icon .mud-progress-circular {
color: var(--deepdrft-green-accent) !important;
}
/* Share + Queue icon buttons in the cut detail header */
.deepdrft-theme-dark .cut-detail-actions .mud-icon-button {
color: var(--deepdrft-primary);
/* Filled-button variant (DARK-ONLY): green-accent fill + navy glyph/label, matching the
play-chip language. In dark, Color.Secondary fill resolves to off-white (unreadable);
here it becomes a clear green CTA. Light is untouched (already green fill + white text). */
.deepdrft-theme-dark .dd-accent-fill .mud-button-filled {
background-color: var(--deepdrft-green-accent);
color: var(--deepdrft-navy);
}
/* Share + Queue icon buttons in each cut detail track row */
.deepdrft-theme-dark .cut-detail-track-row .mud-icon-button {
color: var(--deepdrft-primary);
.deepdrft-theme-dark .dd-accent-fill .mud-button-filled .mud-icon-root {
color: var(--deepdrft-navy) !important;
}
/* Dark-theme interactive affordances in the Session/Mix release-detail hero overlay.
The co-located ReleaseHeroOverlay.razor.css forces .release-hero-play and
.release-hero-share icons to --deepdrft-white unconditionally (correct for the
over-image light-theme contract). In dark theme the play and share glyphs adopt
green to match the "green = interactive" convention. Both the scoped ::deep rule
([b-xxx] .release-hero-play .mud-icon-button) and this global rule
(.deepdrft-theme-dark .release-hero-play .mud-icon-button) compute to specificity
(0,3,0) — a tie — so the override wins on source order: deepdrft-styles.css is
linked after DeepDrftPublic.styles.css in App.razor, making it the later rule.
No !important needed. .mud-progress-circular is included alongside .mud-icon-button
in the play slot to colour the loading spinner green as well.
Light theme: pixel-identical to before. */
.deepdrft-theme-dark .release-hero-play .mud-icon-button,
.deepdrft-theme-dark .release-hero-play .mud-progress-circular,
.deepdrft-theme-dark .release-hero-share .mud-icon-button {
color: var(--deepdrft-primary);
}
/* Dark-theme lava-lamp visualizer-settings trigger.
The MudIconButton uses Color.Secondary, which resolves to off-white in dark mode.
The .dd-lava-lamp-trigger marker div (added to WaveformVisualizerControlPopover.razor)
scopes this rule to only that trigger, preventing bleed onto other Secondary icon
buttons sharing the same host pages. Applied at all host sites (Mix, Cut, Session,
NowPlaying) since the wrapper travels with the component. Light theme: unchanged. */
.deepdrft-theme-dark .dd-lava-lamp-trigger .mud-icon-button {
color: var(--deepdrft-primary);
}
/* Dark-theme gas-lamp dark-mode toggle.
The MudIconButton uses Color.Inherit and sits inside .dd-nav-actions (the right-side
cluster of the nav bar). In dark theme it inherits the nav text colour (off-white);
green makes it consistent with the "green = interactive" convention. The selector is
scoped to .dd-nav-actions so no other icon buttons are affected. Covers both the
desktop nav and the mobile nav (which also renders its gas-lamp inside .dd-nav-actions).
Light theme: unchanged. */
/* Theme-divergent affordance — gas-lamp dark-mode toggle.
Uses Color.Inherit, so in LIGHT it inherits the nav text colour (the contract to keep).
In dark theme it goes green-accent to match the convention. Scoped to .dd-nav-actions
(covers both desktop and mobile nav, which both render the gas-lamp there); dark-only. */
.deepdrft-theme-dark .dd-nav-actions .mud-icon-button {
color: var(--deepdrft-primary);
color: var(--deepdrft-green-accent);
}
/* =============================================================================