diff --git a/CLAUDE.md b/CLAUDE.md index 1941c06..2c87179 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `` (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 ``, outside the `.deepdrft-theme-dark` wrapper `
`, so the dark popover token never reached them. Fix: `MainLayout.razor` stamps `deepdrft-theme-dark` on `` 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` (0,3,0) `!important` to beat MudBlazor's standalone `.mud-secondary-text` (0,1,0) `!important` on the glyph svg — specificity wins; source order is not load-bearing for the glyph clause. 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 diff --git a/DeepDrftPublic.Client/CLAUDE.md b/DeepDrftPublic.Client/CLAUDE.md index 006cda3..2f771cb 100644 --- a/DeepDrftPublic.Client/CLAUDE.md +++ b/DeepDrftPublic.Client/CLAUDE.md @@ -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 the standalone rule `.mud-secondary-text { color: …secondary !important }` (0,1,0) on the glyph ``, so wrapper-level overrides never reach it — the reusable rule targets `.dd-accent-icon .mud-icon-button .mud-icon-root` (0,3,0) `!important`, which beats it on specificity alone; source order is not load-bearing for the glyph clause. 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 diff --git a/DeepDrftPublic.Client/Controls/ReleaseHeroOverlay.razor b/DeepDrftPublic.Client/Controls/ReleaseHeroOverlay.razor index 99933b3..600d2b2 100644 --- a/DeepDrftPublic.Client/Controls/ReleaseHeroOverlay.razor +++ b/DeepDrftPublic.Client/Controls/ReleaseHeroOverlay.razor @@ -52,7 +52,7 @@ @if (ShareContent is not null) { -
+
@ShareContent
} @@ -74,7 +74,7 @@
@if (PlayContent is not null) { -
+
@PlayContent
} diff --git a/DeepDrftPublic.Client/Controls/ReleaseHeroOverlay.razor.css b/DeepDrftPublic.Client/Controls/ReleaseHeroOverlay.razor.css index 3840a35..6031d46 100644 --- a/DeepDrftPublic.Client/Controls/ReleaseHeroOverlay.razor.css +++ b/DeepDrftPublic.Client/Controls/ReleaseHeroOverlay.razor.css @@ -151,14 +151,13 @@ 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: the former white override was removed + because its glyph clauses (.mud-icon-button .mud-icon-root) could not reach the + .mud-secondary-text !important glyph at wrapper specificity, and its spinner clause + (.mud-progress-circular) was live but is now correctly covered by .dd-accent-icon — + making the spinner green-accent (was white) in light mode, the one intentional light delta. */ @media (max-width: 599.98px) { .release-hero { diff --git a/DeepDrftPublic.Client/Controls/WaveformVisualizerControlPopover.razor b/DeepDrftPublic.Client/Controls/WaveformVisualizerControlPopover.razor index f776aa8..dbde94b 100644 --- a/DeepDrftPublic.Client/Controls/WaveformVisualizerControlPopover.razor +++ b/DeepDrftPublic.Client/Controls/WaveformVisualizerControlPopover.razor @@ -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. *@ -
+
} -
+
@* 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. *@ +
@track.TrackNumber
, 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. The glyph clause .dd-accent-icon .mud-icon-button .mud-icon-root is + specificity (0,3,0) + !important, which beats MudBlazor's standalone .mud-secondary-text + (0,1,0) + !important on specificity alone — source order is not load-bearing for the + glyph clause. 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); } /* =============================================================================