fix: improve likers tooltip UX and blur sensitive avatars (#482)

- Wrap liker avatars in SensitiveImage component
- Add max-height (240px) and scrolling to likers list
- Fix hover interaction: delay close to allow entering tooltip
- Add Escape key to close tooltip

🤖 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 c88727d9 27f05692

Changed files
+32 -5
frontend
+11 -2
frontend/src/lib/components/LikersTooltip.svelte
··· 1 1 <script lang="ts"> 2 2 import { API_URL } from '$lib/config'; 3 + import SensitiveImage from './SensitiveImage.svelte'; 3 4 4 5 interface Liker { 5 6 did: string; ··· 12 13 interface Props { 13 14 trackId: number; 14 15 likeCount: number; 16 + onMouseEnter?: () => void; 17 + onMouseLeave?: () => void; 15 18 } 16 19 17 - let { trackId, likeCount }: Props = $props(); 20 + let { trackId, likeCount, onMouseEnter, onMouseLeave }: Props = $props(); 18 21 19 22 let likers = $state<Liker[]>([]); 20 23 let loading = $state(true); // start as loading ··· 67 70 <div 68 71 class="likers-tooltip" 69 72 role="tooltip" 73 + onmouseenter={onMouseEnter} 74 + onmouseleave={onMouseLeave} 70 75 > 71 76 {#if loading} 72 77 <div class="loading">loading...</div> ··· 80 85 class="liker" 81 86 > 82 87 {#if liker.avatar_url} 83 - <img src={liker.avatar_url} alt={liker.display_name} class="avatar" /> 88 + <SensitiveImage src={liker.avatar_url}> 89 + <img src={liker.avatar_url} alt={liker.display_name} class="avatar" /> 90 + </SensitiveImage> 84 91 {:else} 85 92 <div class="avatar-placeholder"> 86 93 {liker.display_name.charAt(0).toUpperCase()} ··· 134 141 display: flex; 135 142 flex-direction: column; 136 143 gap: 0.5rem; 144 + max-height: 240px; 145 + overflow-y: auto; 137 146 } 138 147 139 148 .liker {
+21 -3
frontend/src/lib/components/TrackItem.svelte
··· 75 75 toast.success(`queued ${track.title}`, 1800); 76 76 } 77 77 78 + let likersTooltipTimeout: ReturnType<typeof setTimeout> | null = null; 79 + 78 80 function handleLikesMouseEnter() { 79 - // show tooltip immediately, fetch will handle its own delay 81 + // cancel any pending close 82 + if (likersTooltipTimeout) { 83 + clearTimeout(likersTooltipTimeout); 84 + likersTooltipTimeout = null; 85 + } 80 86 showLikersTooltip = true; 81 87 } 82 88 83 89 function handleLikesMouseLeave() { 84 - showLikersTooltip = false; 90 + // delay closing to allow moving into the tooltip 91 + likersTooltipTimeout = setTimeout(() => { 92 + showLikersTooltip = false; 93 + likersTooltipTimeout = null; 94 + }, 150); 85 95 } 86 96 87 97 function handleLikesKeydown(event: KeyboardEvent) { 88 98 if (event.key === 'Enter' || event.key === ' ') { 89 99 event.preventDefault(); 90 100 showLikersTooltip = true; 101 + } 102 + if (event.key === 'Escape') { 103 + showLikersTooltip = false; 91 104 } 92 105 } 93 106 ··· 223 236 > 224 237 {likeCount} {likeCount === 1 ? 'like' : 'likes'} 225 238 {#if showLikersTooltip} 226 - <LikersTooltip trackId={track.id} likeCount={likeCount} /> 239 + <LikersTooltip 240 + trackId={track.id} 241 + likeCount={likeCount} 242 + onMouseEnter={handleLikesMouseEnter} 243 + onMouseLeave={handleLikesMouseLeave} 244 + /> 227 245 {/if} 228 246 </span> 229 247 {/if}