fix(waveform): repaint on Vite HMR so theme CSS var edits show live (#1005)

* fix(waveform): repaint on Vite HMR so theme CSS var edits show live

The seekbar caches its colours and repaints the canvas on a data-theme
MutationObserver, so a manual theme switch picks up new waveform colours.
Editing a theme's CSS variables via Vite HMR changes the stylesheet but
not data-theme, so the observer never fires and the canvas keeps the old
palette until a theme switch — annoying during theme dev.

Add a dev-only effect that drops the colour cache and repaints on every
HMR update (vite:afterUpdate). import.meta.hot is undefined in production
builds, so the effect is stripped there — no runtime cost.

* fix(waveform): guard HMR effect against partial import.meta.hot in tests

vitest's SSR env exposes a partial import.meta.hot (has .on, no .off), so
the cleanup threw and failed all WaveformSeek/PlayerBar tests. Feature-detect
both .on and .off before wiring the vite:afterUpdate listener.
This commit is contained in:
Frank Stellmacher
2026-06-05 18:44:14 +02:00
committed by GitHub
parent 1c23305887
commit f706336e58
+21
View File
@@ -219,6 +219,27 @@ export default function WaveformSeek({ trackId }: Props) {
return () => observer.disconnect(); return () => observer.disconnect();
}, [seekbarStyle]); }, [seekbarStyle]);
// Dev-only: a theme's CSS variables can change via Vite HMR without the
// `data-theme` attribute changing, so the observer above never fires and the
// cached colours + canvas keep the old palette until a manual theme switch
// (annoying during theme dev — issue: waveform doesn't reflect CSS var edits).
// On every HMR update, drop the colour cache and repaint. `import.meta.hot` is
// undefined in production builds, so this whole effect is stripped there.
useEffect(() => {
// `import.meta.hot` is undefined in prod builds and only a partial stub in
// the test/SSR env (has `.on` but not `.off`), so feature-detect both before
// wiring — otherwise the cleanup throws under vitest.
const hot = import.meta.hot;
if (!hot || typeof hot.on !== 'function' || typeof hot.off !== 'function') return;
const repaint = () => {
invalidateColorCache();
const canvas = canvasRef.current;
if (canvas) drawSeekbar(canvas, seekbarStyle, heightsRef.current, progressRef.current, bufferedRef.current, animStateRef.current);
};
hot.on('vite:afterUpdate', repaint);
return () => { hot.off('vite:afterUpdate', repaint); };
}, [seekbarStyle]);
const trackIdRef = useRef(trackId); const trackIdRef = useRef(trackId);
trackIdRef.current = trackId; trackIdRef.current = trackId;
const seekRef = useRef(seek); const seekRef = useRef(seek);