feat(ui): add active state to play button on playlist/album pages (#667)

- Play button toggles between play/pause when collection is playing
- Shows pause icon (no text) when active, "play now" when inactive
- Subtle breathing glow animation indicates playback state
- Shared keyframes in layout for consistent behavior across pages

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

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

authored by zzstoatzz.io Claude Opus 4.5 and committed by GitHub 7447422b b9d0a2c7

Changed files
+66 -17
frontend
src
routes
playlist
u
[handle]
album
[slug]
+10
frontend/src/routes/+layout.svelte
··· 534 534 color: var(--accent-muted); 535 535 } 536 536 537 + /* shared animation for active play buttons */ 538 + @keyframes -global-ethereal-glow { 539 + 0%, 100% { 540 + box-shadow: 0 0 8px 1px color-mix(in srgb, var(--accent) 25%, transparent); 541 + } 542 + 50% { 543 + box-shadow: 0 0 14px 3px color-mix(in srgb, var(--accent) 45%, transparent); 544 + } 545 + } 546 + 537 547 :global(body) { 538 548 margin: 0; 539 549 padding: 0;
+27 -10
frontend/src/routes/playlist/[id]/+page.svelte
··· 607 607 608 608 // check if user owns this playlist 609 609 const isOwner = $derived(auth.user?.did === playlist.owner_did); 610 + 611 + // check if this playlist is currently playing 612 + const isPlaylistPlaying = $derived( 613 + player.currentTrack !== null && 614 + !player.paused && 615 + tracks.some(t => t.id === player.currentTrack?.id) 616 + ); 610 617 </script> 611 618 612 619 <svelte:window on:keydown={handleKeydown} /> ··· 865 872 </div> 866 873 867 874 <div class="playlist-actions"> 868 - <button class="play-button" onclick={playNow}> 869 - <svg 870 - width="20" 871 - height="20" 872 - viewBox="0 0 24 24" 873 - fill="currentColor" 874 - > 875 - <path d="M8 5v14l11-7z" /> 876 - </svg> 877 - play now 875 + <button 876 + class="play-button" 877 + class:is-playing={isPlaylistPlaying} 878 + onclick={() => isPlaylistPlaying ? player.togglePlayPause() : playNow()} 879 + > 880 + {#if isPlaylistPlaying} 881 + <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 882 + <rect x="6" y="4" width="4" height="16" /> 883 + <rect x="14" y="4" width="4" height="16" /> 884 + </svg> 885 + {:else} 886 + <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 887 + <path d="M8 5v14l11-7z" /> 888 + </svg> 889 + play now 890 + {/if} 878 891 </button> 879 892 <button class="queue-button" onclick={addToQueue}> 880 893 <svg ··· 1579 1592 1580 1593 .play-button:hover { 1581 1594 transform: scale(1.05); 1595 + } 1596 + 1597 + .play-button.is-playing { 1598 + animation: ethereal-glow 3s ease-in-out infinite; 1582 1599 } 1583 1600 1584 1601 .queue-button {
+29 -7
frontend/src/routes/u/[handle]/album/[slug]/+page.svelte
··· 27 27 tracks = [...data.album.tracks]; 28 28 }); 29 29 30 + // local mutable copy of tracks for reordering 31 + let tracks = $state<Track[]>([...data.album.tracks]); 32 + 30 33 // check if current user owns this album 31 34 const isOwner = $derived(auth.user?.did === albumMetadata.artist_did); 32 35 // can only reorder if owner and album has an ATProto list 33 36 const canReorder = $derived(isOwner && !!albumMetadata.list_uri); 34 37 35 - // local mutable copy of tracks for reordering 36 - let tracks = $state<Track[]>([...data.album.tracks]); 38 + // check if this album is currently playing 39 + const isAlbumPlaying = $derived( 40 + player.currentTrack !== null && 41 + !player.paused && 42 + tracks.some(t => t.id === player.currentTrack?.id) 43 + ); 37 44 38 45 // edit mode state 39 46 let isEditMode = $state(false); ··· 545 552 </div> 546 553 547 554 <div class="album-actions"> 548 - <button class="play-button" onclick={playNow}> 549 - <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 550 - <path d="M8 5v14l11-7z"/> 551 - </svg> 552 - play now 555 + <button 556 + class="play-button" 557 + class:is-playing={isAlbumPlaying} 558 + onclick={() => isAlbumPlaying ? player.togglePlayPause() : playNow()} 559 + > 560 + {#if isAlbumPlaying} 561 + <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 562 + <rect x="6" y="4" width="4" height="16" /> 563 + <rect x="14" y="4" width="4" height="16" /> 564 + </svg> 565 + {:else} 566 + <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 567 + <path d="M8 5v14l11-7z"/> 568 + </svg> 569 + play now 570 + {/if} 553 571 </button> 554 572 <button class="queue-button" onclick={addToQueue}> 555 573 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"> ··· 967 985 968 986 .play-button:hover { 969 987 transform: scale(1.05); 988 + } 989 + 990 + .play-button.is-playing { 991 + animation: ethereal-glow 3s ease-in-out infinite; 970 992 } 971 993 972 994 .queue-button {