/** * Player-height spacer observer. * * The audio player docks `position: fixed` to the viewport bottom, so it sits * outside normal flow and would overlap page content. A spacer div in the layout * reserves the equivalent space — but the player's height is not constant: it * reflows across the four breakpoints and grows when an error banner appears. A * static height can't track that, so we mirror the player's live border-box * height into the `--player-height` custom property on :root and let the spacer * read it. One observer at a time, re-pointed on each `observe` call; the var * resets to 0 on `unobserve` (player minimized / disposed) so the spacer * collapses. * * SOLE CONSUMER (post visualizer-viewport-framing). `--player-height` now feeds * ONLY the layout spacer div (MainLayout.razor.css `.player-spacer.expanded`). * The ambient WaveformVisualizer backdrop is anchored `inset: 0` and no longer * reads this var, so a height change here only resizes the spacer's `height` — a * cheap, side-effect-free layout write. There is no GL backing store to clear and * no theater-flash to debounce against, so we publish every observed frame * directly: the spacer tracks the bar exactly through the Theater-Mode ease with * no settle lag, and this stays the SOLE writer of `--player-height`. */ const HEIGHT_VAR = '--player-height'; let observer: ResizeObserver | null = null; let lastWritten = -1; function setVar(px: number): void { // Round up so sub-pixel heights never leave a hairline of overlap. const rounded = Math.ceil(px); if (rounded === lastWritten) return; lastWritten = rounded; document.documentElement.style.setProperty(HEIGHT_VAR, `${rounded}px`); } function measure(entry: ResizeObserverEntry): number { // Prefer the border-box measurement; fall back to contentRect on the // (older) engines that don't populate borderBoxSize. const box = entry.borderBoxSize?.[0]; return box ? box.blockSize : entry.contentRect.height; } export function observe(element: Element): void { unobserve(); if (!element) return; observer = new ResizeObserver(entries => { const entry = entries[0]; if (!entry) return; setVar(measure(entry)); }); observer.observe(element); // Seed synchronously so the spacer is correct on this frame, before the // first ResizeObserver callback fires. setVar(element.getBoundingClientRect().height); } export function unobserve(): void { observer?.disconnect(); observer = null; setVar(0); }