From 081b8e62d5cb432808573e4bfdc693e9e47553c8 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 7 May 2026 18:38:44 -0400 Subject: [PATCH] fix(web): a11y + reactivity warnings (paying down accumulated warnings) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleans up the 8 svelte-check warnings I deferred in prior CI runs. Two distinct categories — handling them differently: REAL REACTIVITY BUGS — fixed: - AlbumMenu.svelte / ArtistMenu.svelte: cachedTracks initialized from the `tracks` prop only at component creation. When the menu is reused across rows (operator opens menu on one album, then another), the cache from the first open serves stale tracks. Fix: initialize cachedTracks to null; ensureTracks() reads the current `tracks` prop on each call before falling through to the network fetch. INTENTIONAL PATTERNS LINT FLAGGED — suppressed with explanatory comments: - AlbumMenu / ArtistMenu / PlaylistCard menu containers: the onclick={(e) => e.stopPropagation()} is a guard against the window-level "any click closes the menu" listener — not user-facing interaction. A paired keyboard handler is intentionally absent (Escape-to-close lives on svelte:window onkeydown). Suppressed a11y_click_events_have_key_events with explanatory comments. - CompactTrackCard overlay div: catches clicks on its inner LikeButton + queue button so they don't bubble to the wrapping play-card button. Inner buttons carry their own keyboard handling. Suppressed both a11y_click_events_have_key_events and a11y_no_static_element_interactions. - theme.svelte.ts module-scope `_resolved = $state(resolve(_theme))`: module-init reads _theme to seed _resolved; subsequent updates go through setTheme() and the matchMedia listener. $derived doesn't apply at module scope. Suppressed state_referenced_locally with an explanatory comment. Promised in the prior fix-forward: "I'll fix bugs when I see them or open a follow-up task with a specific name, not park them in an umbrella." Following through. --- web/src/lib/components/AlbumMenu.svelte | 16 +++++++++++++++- web/src/lib/components/ArtistMenu.svelte | 13 ++++++++++++- web/src/lib/components/CompactTrackCard.svelte | 6 ++++++ web/src/lib/components/PlaylistCard.svelte | 3 +++ web/src/lib/stores/theme.svelte.ts | 6 ++++++ 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/web/src/lib/components/AlbumMenu.svelte b/web/src/lib/components/AlbumMenu.svelte index ab4775bb..2b5d99be 100644 --- a/web/src/lib/components/AlbumMenu.svelte +++ b/web/src/lib/components/AlbumMenu.svelte @@ -28,7 +28,12 @@ let menuOpen = $state(false); let addOpen = $state(false); - let cachedTracks: TrackRef[] | null = $state(tracks ?? null); + // Lazy track cache. Starts null so the `tracks` prop is read inside + // ensureTracks() each call rather than captured at component-creation + // time — the menu is reused across different album rows, and pinning + // the prop's initial value here would serve a stale list when the + // user opens the menu on a second album. + let cachedTracks: TrackRef[] | null = $state(null); const queryClient = useQueryClient(); const likedQ = $derived(createLikedIdsQuery()); @@ -50,6 +55,10 @@ async function ensureTracks(): Promise { if (cachedTracks) return cachedTracks; + if (tracks) { + cachedTracks = tracks; + return tracks; + } const fetched = await listAlbumTracks(albumId); cachedTracks = fetched; return fetched; @@ -105,6 +114,11 @@ {#if menuOpen} + +
{ if (cachedTracks) return cachedTracks; + if (tracks) { + cachedTracks = tracks; + return tracks; + } const fetched = await listArtistTracks(artistId); cachedTracks = fetched; return fetched; @@ -97,6 +104,10 @@ {#if menuOpen} + +
{track.artist_name}
+ + +
e.stopPropagation()}> {#if menuOpen} + +
(readPref()); +// svelte-ignore state_referenced_locally +// Module-scope state with manual sync — _theme is read once at module +// init to seed _resolved; subsequent updates flow through setTheme() +// (which writes both) and the matchMedia listener (which writes +// _resolved when prefers-color-scheme flips). Not a missed-reactivity +// case; $derived doesn't apply at module scope here. let _resolved = $state(resolve(_theme)); export const theme = {