Merge popover-surface-retune into dev

Retune public-site popover surfaces: light reads as a near-page-background light
surface (8%->4% navy), dark skews bluer (navy-mid + green-accent). Root cause: popovers
portal to <body>, outside the theme wrapper; MainLayout now stamps the theme class on
<body> via a TS interop helper so portaled popovers receive the dark token.
This commit is contained in:
daniel-c-harvey
2026-06-20 00:28:20 -04:00
3 changed files with 68 additions and 9 deletions
@@ -6,6 +6,7 @@
@using Microsoft.AspNetCore.Components
@inherits LayoutComponentBase
@implements IDisposable
@implements IAsyncDisposable
<MudThemeProvider Theme="@DeepDrftPalettes.Default" IsDarkMode="_isDarkMode" />
<MudPopoverProvider />
@@ -42,10 +43,13 @@
private string _audioPlayerClass = "minimized";
private const string DarkModeKey = "darkMode";
private bool _isDarkMode = false;
private bool? _lastAppliedDarkMode = null;
private PersistingComponentStateSubscription _persistingSubscription;
private IJSObjectReference? _themeModule;
[Inject] public required PersistentComponentState PersistentState { get; set; }
[Inject] public required DarkModeSettings DarkModeSettings { get; set; }
[Inject] public required IJSRuntime JS { get; set; }
protected override void OnInitialized()
{
@@ -66,6 +70,24 @@
_persistingSubscription = PersistentState.RegisterOnPersisting(PersistDarkMode);
}
// Sync dark mode class on <body> so portaled MudBlazor elements (popovers, menus, selects)
// inherit --deepdrft-popover-surface from body.deepdrft-theme-dark rather than from :root only.
// Popovers portal outside the ThemeWrapperClass div, so only a body-level class can reach them.
// Gated: only fires on first render or when _isDarkMode actually changes, to avoid redundant
// JS calls on unrelated re-renders (e.g. audio player minimize/expand).
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender || _isDarkMode != _lastAppliedDarkMode)
{
_lastAppliedDarkMode = _isDarkMode;
_themeModule ??= await JS.InvokeAsync<IJSObjectReference>(
"import", "./_content/DeepDrftShared.Client/js/theme/theme.js");
await _themeModule.InvokeVoidAsync("setBodyThemeClass", _isDarkMode);
}
}
// Theme wrapper class for CSS targeting
private string ThemeWrapperClass => _isDarkMode ? "deepdrft-theme-dark" : "deepdrft-theme-light";
@@ -80,6 +102,15 @@
_persistingSubscription.Dispose();
}
public async ValueTask DisposeAsync()
{
if (_themeModule != null)
{
try { await _themeModule.DisposeAsync(); }
catch (JSDisconnectedException) { /* circuit torn down */ }
}
}
private void ToggleAudioPlayerMinimized(bool isMinimized)
{
_audioPlayerClass = isMinimized ? "minimized" : "expanded";
@@ -0,0 +1,15 @@
/**
* theme - body-class helpers for dark-mode theme toggling.
*
* Single Responsibility: apply or remove the deepdrft-theme-dark class on
* document.body so that portaled MudBlazor elements (popovers, menus, selects)
* inherit --deepdrft-popover-surface from body.deepdrft-theme-dark rather than
* from :root only. Popovers portal outside the ThemeWrapperClass div, so only
* a body-level class can reach them.
*/
/** Toggle the deepdrft-theme-dark class on document.body.
* @param isDark true to add the class, false to remove it. */
export function setBodyThemeClass(isDark: boolean): void {
document.body.classList.toggle('deepdrft-theme-dark', isDark);
}
@@ -85,10 +85,15 @@
--deepdrft-play-chip-soft: var(--deepdrft-soft);
/* Popover surface (Phase 18). Default MudBlazor popovers (selects/menus/tooltips/share
body) bind this. Light is a soft desaturated-navy wash so they read as a calm light
surface; dark uses the existing panel-ground charcoal. Bespoke dark-glass panels
(visualizer/queue/privacy) do NOT bind this — they keep --deepdrft-panel-ground directly. */
--deepdrft-popover-surface: color-mix(in srgb, var(--deepdrft-navy) 8%, var(--deepdrft-white));
body) bind this. Light uses a very subtle navy wash (4%) — near the page background but
just perceptibly off-white so the popover reads as an elevated surface. Dark uses a
bluer navy (colour-mix of navy-mid + green-accent at 20%), defined once in
--deepdrft-popover-surface-dark below and referenced by both the .deepdrft-theme-dark
wrapper block and the body.deepdrft-theme-dark block so portaled popover content (which
portals to <body>, outside the wrapper div) is also reached. Bespoke dark-glass panels
(visualizer/queue/privacy) do NOT bind this — they keep --deepdrft-panel-ground. */
--deepdrft-popover-surface-dark: color-mix(in srgb, var(--deepdrft-navy-mid) 80%, var(--deepdrft-green-accent) 20%);
--deepdrft-popover-surface: color-mix(in srgb, var(--deepdrft-navy) 4%, var(--deepdrft-white));
/* Fixed-nav height — single source of truth shared by the frosted-glass nav
(DeepDrftMenu.razor.css pins .dd-nav to this) and the main-content clearance
@@ -163,9 +168,17 @@
--deepdrft-play-glyph: var(--deepdrft-navy);
--deepdrft-play-chip-soft: color-mix(in srgb, var(--deepdrft-green-accent) 30%, transparent);
/* Popover surface (Phase 18). Symptom #1 is a LIGHT-mode complaint and the acceptance bar
requires dark popovers to stay unchanged, so dark binds the exact current MudBlazor dark
Surface (#162437, DeepDrftPalettes.Dark.Surface) — NOT §3's suggested panel-ground (#1a1c22),
which would have shifted dark popovers a shade. Pixel-identical dark; only light is retoned. */
--deepdrft-popover-surface: #162437;
/* Popover surface (Phase 18) — within .deepdrft-theme-dark wrapper this value applies to
non-portaled elements only (drawers, inline menus). Portaled MudBlazor popovers live at
<body> level; the body.deepdrft-theme-dark block below uses the same source token. */
--deepdrft-popover-surface: var(--deepdrft-popover-surface-dark);
}
/* Portal-scope dark popover surface. MudBlazor popovers (selects, menus, share body) portal
to <body>, placing them outside the .deepdrft-theme-dark wrapper div. MainLayout.razor syncs
deepdrft-theme-dark onto <body> via JS after each render, so this selector reaches portaled
content. Resolved from --deepdrft-popover-surface-dark (defined in :root above) — bluer navy
(navy-mid + 20% green-accent tint) rather than the pure charcoal #162437. */
body.deepdrft-theme-dark {
--deepdrft-popover-surface: var(--deepdrft-popover-surface-dark);
}