From 2c4bd3a394856bd3e47758a48dafd64f3588218b Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Tue, 16 Jun 2026 11:53:47 -0400 Subject: [PATCH] fix(p10-reframe-w1): clip visualizer to minimized FAB height; replace LavaLamp icon with SVG Repo glyph --- .../AudioPlayerBar/AudioPlayerBar.razor | 2 +- .../AudioPlayerBar/AudioPlayerBar.razor.cs | 37 +++++++++++++------ .../Controls/MixWaveformVisualizer.razor.css | 14 +++---- DeepDrftShared.Client/Common/DDIcons.cs | 23 +++--------- 4 files changed, 39 insertions(+), 37 deletions(-) diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor index 089f478..e9c379e 100644 --- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor +++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor @@ -1,6 +1,6 @@ @if (_isMinimized) { -
+
} diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs index 7f9a75e..9c70361 100644 --- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs +++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs @@ -27,7 +27,15 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable // var via a ResizeObserver (see Interop/layout/spacer.ts) rather than a static // value, because the player reflows across breakpoints and grows with the // error banner. + // + // _miniDock is the minimized FAB container. We observe it in minimized state so + // --player-height stays non-zero (the FAB's actual height) and the MixWaveformVisualizer + // clips to the top of the FAB rather than extending to the viewport bottom (fix §1). + // The player-spacer's .minimized class uses a hardcoded 60px and ignores the var, + // so publishing the FAB height here does not regress the spacer. private ElementReference _playerRoot; + private ElementReference _miniDock; + private ElementReference _lastObservedElement; private IJSObjectReference? _spacerModule; private bool _spacerObserved; @@ -114,22 +122,27 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable protected override async Task OnAfterRenderAsync(bool firstRender) { - // Only the docked, expanded shape needs a spacer: the Fixed embed is - // already in normal flow, and the minimized FAB floats clear of content. - // Toggle the observer on the minimized/expanded transition only — the - // ResizeObserver itself handles every size change in between. - var shouldObserve = !_isMinimized && !Fixed; - if (shouldObserve == _spacerObserved) return; + // The Fixed embed is already in normal flow — no spacer/clip needed. + // For the docked player: we observe in BOTH expanded and minimized states + // so --player-height always reflects the live height of whichever element + // is visible. This keeps the MixWaveformVisualizer clipped to the top of + // the footer in both states (fix §1). + // expanded → observe _playerRoot (full player bar, reflows across breakpoints) + // minimized → observe _miniDock (floating FAB container, ~56–60px) + // The player-spacer's .minimized class uses a hardcoded height and ignores + // the var, so publishing the FAB height here does not regress the spacer. + if (Fixed) return; + + var elementToObserve = _isMinimized ? _miniDock : _playerRoot; + var alreadyOnThisElement = _spacerObserved && elementToObserve.Id == _lastObservedElement.Id; + if (alreadyOnThisElement) return; var module = await GetSpacerModuleAsync(); if (module is null) return; - if (shouldObserve) - await module.InvokeVoidAsync("observe", _playerRoot); - else - await module.InvokeVoidAsync("unobserve"); - - _spacerObserved = shouldObserve; + await module.InvokeVoidAsync("observe", elementToObserve); + _spacerObserved = true; + _lastObservedElement = elementToObserve; } private async Task GetSpacerModuleAsync() diff --git a/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.css b/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.css index 91583b2..e1c5f4c 100644 --- a/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.css +++ b/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.css @@ -3,13 +3,13 @@ Footer clip (Phase 10 W1, spec §2c): the backdrop must stop cleanly ABOVE the audio player bar so no lava/waveform pixel paints over or under it. `overflow: hidden` clips the canvas to this box, and - `bottom` is inset by the player bar's LIVE height — `--player-height`, the custom property the player - already publishes on :root via its ResizeObserver (AudioPlayerBar + Interop/layout/spacer.ts). That - var tracks the expanded bar's border-box height across breakpoints/error-banner reflow, and resets to - 0 when the bar is minimized — so the clip line follows the bar's actual current height with no extra - coupling: when minimized the var is 0 and the backdrop reaches the viewport bottom (the floating FAB, - z-index 1300, simply sits over it — there is no full-width bar to clip to), matching spec §2c. The - 0px fallback keeps the backdrop full-height on any page that doesn't host the player. */ + `bottom` is inset by `--player-height`, which AudioPlayerBar publishes on :root via its ResizeObserver + (Interop/layout/spacer.ts). The observer now points at whichever element is live: + expanded → the full player dock (tracks breakpoint reflow + error-banner growth) + minimized → the minimized-dock FAB container (~56–60 px) + so --player-height is always non-zero while the player is mounted and the clip line follows the bar in + BOTH states (fix §1 / p10-reframe-w1-fix). The 0px fallback keeps the backdrop full-height on any + page that does not host the player. */ .mix-waveform-bg { position: fixed; inset: 0; diff --git a/DeepDrftShared.Client/Common/DDIcons.cs b/DeepDrftShared.Client/Common/DDIcons.cs index ab6f6d0..5be0a7e 100644 --- a/DeepDrftShared.Client/Common/DDIcons.cs +++ b/DeepDrftShared.Client/Common/DDIcons.cs @@ -24,24 +24,13 @@ public static class DDIcons """; /// - /// Lava lamp - the Mix visualizer settings glyph. Classic 1970s "Lava" silhouette, redrawn (Phase 10 - /// W1). Inner path/shape markup for MudBlazor's Icon= slot (no outer <svg> wrapper — MudBlazor - /// supplies that); coordinates in the 24×24 space matching viewBox="0 0 24 24". - /// - /// Bottom→top: a WIDE truncated-cone metal BASE (widest at the very bottom, tapering up to a narrow - /// waist); a tall tapered GLASS VESSEL on the base — bulbous/rounded at the bottom, tapering up to a - /// roundedly-pointed (teardrop/bullet) top; a small truncated-cone metal CAP on top mirroring the base. - /// The metal base + cap are currentColor so the silhouette stays tintable with its context - /// (Color.Secondary, light/dark). The fluid + blobs are hard navy/moss so the lamp reads as "lit" at - /// icon size: navy (#17283F) vessel fluid with 3 varied moss (#429D6A) lava blobs suspended in it. - /// NOTE: these hexes mirror the app theme tokens (navy ~#17283F/#0D1B2A, moss ~#3D7A68/#429D6A). + /// Lava lamp - the Mix visualizer settings glyph. Sourced from lava-lamp-svgrepo-com.svg + /// (SVG Repo, viewBox="0 0 50 50"). Wrapped in a scale(0.48) transform to fit MudBlazor's + /// inner 24×24 coordinate space (50 × 0.48 = 24). Fill uses currentColor so it themes with + /// its context (Color.Secondary, light/dark). Inner markup only — no outer <svg> wrapper, + /// MudBlazor supplies viewBox="0 0 24 24". /// public const string LavaLamp = """ - - - - - - + """; }