fix: remove stopPropagation from tag links to preserve playback (#435)

stopPropagation() on link click handlers breaks SvelteKit's client-side
router, causing full page reloads that unmount the player and interrupt
audio playback.

this pattern was previously fixed for artist links in commit a6376f6
but was reintroduced when tag links were added.

the parent button handler already checks if the click target is an anchor
element (lines 93-98), making stopPropagation() on the links unnecessary.

also adds docs/frontend/navigation.md documenting this pattern to prevent
future recurrence.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>

authored by zzstoatzz.io Claude and committed by GitHub 0b5d392e 6e180a83

Changed files
+89 -1
docs
frontend
frontend
src
lib
components
+88
docs/frontend/navigation.md
···
··· 1 + # client-side navigation 2 + 3 + ## preserving player state across navigation 4 + 5 + the player lives in the root layout (`+layout.svelte`) and persists across all page navigations. to maintain uninterrupted playback, client-side navigation must work correctly. 6 + 7 + ## critical rule: never use `stopPropagation()` on links 8 + 9 + **problem**: calling `e.stopPropagation()` on link click handlers breaks SvelteKit's client-side router, causing full page reloads that unmount and remount the player. 10 + 11 + ```svelte 12 + <!-- ❌ WRONG - causes full page reload, interrupts playback --> 13 + <a href="/tag/{tag}" onclick={(e) => e.stopPropagation()}>{tag}</a> 14 + ``` 15 + 16 + ```svelte 17 + <!-- ✅ CORRECT - client-side navigation works, playback continues --> 18 + <a href="/tag/{tag}">{tag}</a> 19 + ``` 20 + 21 + ## handling links inside clickable containers 22 + 23 + when you have links nested inside a clickable button/div, check the event target instead of using `stopPropagation()`: 24 + 25 + ```svelte 26 + <button 27 + onclick={(e) => { 28 + // skip if user clicked a link inside 29 + if (e.target instanceof HTMLAnchorElement || (e.target as HTMLElement).closest('a')) { 30 + return; 31 + } 32 + doSomething(); 33 + }} 34 + > 35 + <span>click me</span> 36 + <a href="/other-page">or click this link</a> 37 + </button> 38 + ``` 39 + 40 + this pattern: 41 + 1. lets the link trigger proper client-side navigation 42 + 2. only calls `doSomething()` when clicking non-link elements 43 + 3. preserves all global state including the player 44 + 45 + ## why this matters 46 + 47 + SvelteKit's client-side router intercepts `<a>` tag clicks to: 48 + - avoid full page reload 49 + - preserve global state (player, queue, auth) 50 + - enable smooth transitions 51 + 52 + when `stopPropagation()` is called, the click event never reaches SvelteKit's router, falling back to native browser navigation which: 53 + - performs a full page reload 54 + - unmounts and remounts all components 55 + - resets audio playback 56 + 57 + ## examples from the codebase 58 + 59 + ### TrackItem.svelte 60 + 61 + the track container is a button that plays the track on click. it contains multiple links (artist, album, tags) that should navigate without affecting playback: 62 + 63 + ```svelte 64 + <button 65 + class="track" 66 + onclick={(e) => { 67 + // only play if clicking the track itself, not a link inside 68 + if (e.target instanceof HTMLAnchorElement || (e.target as HTMLElement).closest('a')) { 69 + return; 70 + } 71 + onPlay(track); 72 + }} 73 + > 74 + <a href="/u/{track.artist_handle}" class="artist-link">{track.artist}</a> 75 + <a href="/tag/{tag}" class="tag-badge">{tag}</a> 76 + </button> 77 + ``` 78 + 79 + ## debugging navigation issues 80 + 81 + **symptom**: clicking a link stops music playback 82 + 83 + **diagnosis**: 84 + 1. check if the link has `onclick={(e) => e.stopPropagation()}` 85 + 2. check parent elements for event handling that might interfere 86 + 3. verify the route uses SvelteKit conventions (`+page.svelte`, `+page.ts`) 87 + 88 + **fix**: remove `stopPropagation()` and use event target checking in parent handlers instead
+1 -1
frontend/src/lib/components/TrackItem.svelte
··· 176 {#if track.tags && track.tags.length > 0} 177 <span class="tags-line"> 178 {#each track.tags as tag} 179 - <a href="/tag/{encodeURIComponent(tag)}" class="tag-badge" onclick={(e) => e.stopPropagation()}>{tag}</a> 180 {/each} 181 </span> 182 {/if}
··· 176 {#if track.tags && track.tags.length > 0} 177 <span class="tags-line"> 178 {#each track.tags as tag} 179 + <a href="/tag/{encodeURIComponent(tag)}" class="tag-badge">{tag}</a> 180 {/each} 181 </span> 182 {/if}