fix: mobile action menus open from top, add missing actions to AddToMenu (#569)

- AddToMenu now includes queue and share actions (matching TrackActionsMenu)
- Both menus open from top on mobile instead of bottom (doesn't cover player)
- Added backdrop for proper dismissal on mobile
- Track detail page passes shareUrl and onQueue props to AddToMenu

🤖 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 71de1066 1d8942ad

Changed files
+86 -16
frontend
src
lib
routes
track
+76 -9
frontend/src/lib/components/AddToMenu.svelte
··· 14 14 disabledReason?: string; 15 15 onLikeChange?: (_liked: boolean) => void; 16 16 excludePlaylistId?: string; 17 + shareUrl?: string; 18 + onQueue?: () => void; 17 19 } 18 20 19 21 let { ··· 25 27 disabled = false, 26 28 disabledReason, 27 29 onLikeChange, 28 - excludePlaylistId 30 + excludePlaylistId, 31 + shareUrl, 32 + onQueue 29 33 }: Props = $props(); 30 34 31 35 let liked = $state(initialLiked); ··· 116 120 } finally { 117 121 loading = false; 118 122 menuOpen = false; 123 + } 124 + } 125 + 126 + function handleQueue(e: Event) { 127 + e.stopPropagation(); 128 + if (onQueue) { 129 + onQueue(); 130 + menuOpen = false; 131 + } 132 + } 133 + 134 + async function handleShare(e: Event) { 135 + e.stopPropagation(); 136 + if (!shareUrl) return; 137 + try { 138 + await navigator.clipboard.writeText(shareUrl); 139 + toast.success('link copied'); 140 + menuOpen = false; 141 + } catch { 142 + toast.error('failed to copy link'); 119 143 } 120 144 } 121 145 ··· 255 279 256 280 {#if menuOpen} 257 281 <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions --> 282 + <div class="menu-backdrop" role="presentation" onclick={() => { menuOpen = false; showPlaylistPicker = false; }}></div> 283 + <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions --> 258 284 <div class="menu-dropdown" class:open-upward={openUpward} role="menu" tabindex="-1" onclick={(e) => { 259 285 // don't stop propagation for links - let SvelteKit handle navigation 260 286 if (e.target instanceof HTMLAnchorElement || (e.target as HTMLElement).closest('a')) { ··· 283 309 <svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 284 310 <path d="M9 18l6-6-6-6"/> 285 311 </svg> 312 + </button> 313 + {/if} 314 + {#if onQueue} 315 + <button class="menu-item" onclick={handleQueue}> 316 + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"> 317 + <line x1="5" y1="15" x2="5" y2="21"></line> 318 + <line x1="2" y1="18" x2="8" y2="18"></line> 319 + <line x1="9" y1="6" x2="21" y2="6"></line> 320 + <line x1="9" y1="12" x2="21" y2="12"></line> 321 + <line x1="9" y1="18" x2="21" y2="18"></line> 322 + </svg> 323 + <span>add to queue</span> 324 + </button> 325 + {/if} 326 + {#if shareUrl} 327 + <button class="menu-item" onclick={handleShare}> 328 + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 329 + <circle cx="18" cy="5" r="3"></circle> 330 + <circle cx="6" cy="12" r="3"></circle> 331 + <circle cx="18" cy="19" r="3"></circle> 332 + <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line> 333 + <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line> 334 + </svg> 335 + <span>share</span> 286 336 </button> 287 337 {/if} 288 338 {:else} ··· 680 730 to { transform: rotate(360deg); } 681 731 } 682 732 683 - /* mobile: show as bottom sheet */ 733 + /* backdrop - hidden on desktop, visible on mobile */ 734 + .menu-backdrop { 735 + display: none; 736 + } 737 + 738 + /* mobile: show as top sheet */ 684 739 @media (max-width: 768px) { 685 740 .trigger-button { 686 741 width: 28px; ··· 692 747 height: 14px; 693 748 } 694 749 750 + .menu-backdrop { 751 + display: block; 752 + position: fixed; 753 + top: 0; 754 + left: 0; 755 + right: 0; 756 + bottom: 0; 757 + z-index: 100; 758 + background: rgba(0, 0, 0, 0.4); 759 + } 760 + 695 761 .menu-dropdown { 696 762 position: fixed; 697 - top: auto; 698 - bottom: 0; 763 + top: 0; 764 + bottom: auto; 699 765 left: 0; 700 766 right: 0; 701 767 min-width: 100%; 702 - border-radius: 16px 16px 0 0; 703 - padding-bottom: env(safe-area-inset-bottom, 0); 704 - animation: slideUp 0.2s ease-out; 768 + border-radius: 0 0 16px 16px; 769 + padding-top: env(safe-area-inset-top, 0); 770 + animation: slideDown 0.2s ease-out; 771 + z-index: 101; 705 772 } 706 773 707 - @keyframes slideUp { 774 + @keyframes slideDown { 708 775 from { 709 - transform: translateY(100%); 776 + transform: translateY(-100%); 710 777 } 711 778 to { 712 779 transform: translateY(0);
+7 -7
frontend/src/lib/components/TrackActionsMenu.svelte
··· 422 422 423 423 .menu-panel { 424 424 position: fixed; 425 - bottom: 0; 425 + top: 0; 426 426 left: 0; 427 427 right: 0; 428 428 background: var(--bg-secondary); 429 - border-radius: 16px 16px 0 0; 430 - box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.4); 429 + border-radius: 0 0 16px 16px; 430 + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4); 431 431 z-index: 101; 432 - animation: slideUp 0.2s ease-out; 433 - padding-bottom: env(safe-area-inset-bottom, 0); 432 + animation: slideDown 0.2s ease-out; 433 + padding-top: env(safe-area-inset-top, 0); 434 434 max-height: 70vh; 435 435 overflow-y: auto; 436 436 } 437 437 438 - @keyframes slideUp { 438 + @keyframes slideDown { 439 439 from { 440 - transform: translateY(100%); 440 + transform: translateY(-100%); 441 441 } 442 442 to { 443 443 transform: translateY(0);
+3
frontend/src/routes/track/[id]/+page.svelte
··· 115 115 116 116 function addToQueue() { 117 117 queue.addTracks([track]); 118 + toast.success(`queued ${track.title}`, 1800); 118 119 } 119 120 120 121 async function loadComments() { ··· 465 466 trackUri={track.atproto_record_uri} 466 467 trackCid={track.atproto_record_cid} 467 468 initialLiked={track.is_liked || false} 469 + shareUrl={shareUrl} 470 + onQueue={addToQueue} 468 471 /> 469 472 {/if} 470 473 <ShareButton url={shareUrl} title="share track" />