player bar: the overflow menu out from under the bar's own clip, and Lyrics wired for real

This commit is contained in:
KeelrLeek committed 2026-08-17 18:05:36 -05:00
1 parent df3d15553a
commit f4f26d6d34
1 file changed
+79 -36
+79 -36
View File
@@ -215,25 +215,58 @@
} }
// ---- the overflow menu ------------------------------------------------- // ---- the overflow menu -------------------------------------------------
// The popup renders OUTSIDE the footer and position:fixed (the SaveMenu
// idiom): the bar is overflow:hidden, so an absolute menu inside it got
// amputated at the bar's top edge — and what poked out still lost to the
// Now Playing panel's z-indexed layers. Fixed + measured coordinates
// escape both the clip and the stacking war. It must NOT live inside
// .extras: that element's centering transform would become a fixed
// element's containing block and hand it right back to the clip.
let menuOpen = $state(false); let menuOpen = $state(false);
let menuWrap = $state(null); let menuWrap = $state(null);
let menuEl = $state(null);
let menuPos = $state({ right: 0, bottom: 0 });
function toggleMenu() {
if (!menuOpen && menuWrap) {
const r = menuWrap.getBoundingClientRect();
// Right-aligned over the button, opening upward — the same seat
// the old absolute placement (bottom + right of the wrap) gave it.
menuPos = {
right: Math.max(8, window.innerWidth - r.right - 6),
bottom: window.innerHeight - r.top + 6,
};
}
menuOpen = !menuOpen;
}
$effect(() => { $effect(() => {
if (!menuOpen) return; if (!menuOpen) return;
const away = (e) => { const away = (e) => {
if (menuWrap && !menuWrap.contains(e.target)) menuOpen = false; const inside =
(menuWrap && menuWrap.contains(e.target)) ||
(menuEl && menuEl.contains(e.target));
if (!inside) menuOpen = false;
}; };
const esc = (e) => e.key === "Escape" && (menuOpen = false); const esc = (e) => e.key === "Escape" && (menuOpen = false);
// The menu is anchored to coordinates measured at open; a resize
// moves the anchor out from under them, so it closes instead.
const close = () => (menuOpen = false);
window.addEventListener("pointerdown", away); window.addEventListener("pointerdown", away);
window.addEventListener("keydown", esc); window.addEventListener("keydown", esc);
window.addEventListener("resize", close);
return () => { return () => {
window.removeEventListener("pointerdown", away); window.removeEventListener("pointerdown", away);
window.removeEventListener("keydown", esc); window.removeEventListener("keydown", esc);
window.removeEventListener("resize", close);
}; };
}); });
function openQueue() { function openQueue() {
menuOpen = false; menuOpen = false;
showNowPlaying(); // the queue lives in the panel; always reveal it showNowPlaying(); // the queue lives in the panel; always reveal it
} }
function openLyrics() {
menuOpen = false;
ui.lyricsFull = true; // the fullscreen overlay (LyricsOverlay.svelte)
}
// ---- Save to library, from the ⋯ menu --------------------------------- // ---- Save to library, from the ⋯ menu ---------------------------------
// Only a stream can be saved; a single queued stream has one destination // Only a stream can be saved; a single queued stream has one destination
@@ -1224,40 +1257,10 @@
title="More" title="More"
aria-haspopup="menu" aria-haspopup="menu"
aria-expanded={menuOpen} aria-expanded={menuOpen}
onclick={() => (menuOpen = !menuOpen)} onclick={toggleMenu}
> >
<Icon name="more-horizontal" size={17} /> <Icon name="more-horizontal" size={17} />
</button> </button>
{#if menuOpen}
<div class="menu" role="menu">
<button role="menuitem" onclick={openQueue}>
<Icon name="playlist" size={15} /> Queue
</button>
<!-- Save's new home. The bar's download button became the
expand toggle, and this menu is where everything past
Save was always going to live. Only a stream can be
saved, so the items appear only for one. -->
{#if canSave}
<button role="menuitem" onclick={() => saveFromMenu("track")}>
<Icon name="music" size={15} /> Save this track
</button>
{#if savingAlbum}
<button role="menuitem" onclick={() => saveFromMenu("album")}>
<Icon name="library" size={15} /> Save whole album
</button>
{/if}
{/if}
<button role="menuitem" disabled title="Not built yet">
<Icon name="lyrics" size={15} /> Lyrics <span class="soon">soon</span>
</button>
<button role="menuitem" disabled title="Not built yet">
<Icon name="panel-right" size={15} /> Mini player <span class="soon">soon</span>
</button>
<button role="menuitem" disabled title="Not built yet">
<Icon name="volume" size={15} /> Output device <span class="soon">soon</span>
</button>
</div>
{/if}
</div> </div>
<button class="vol-icon" title={player.muted ? "Unmute" : "Mute"} onclick={toggleMute}> <button class="vol-icon" title={player.muted ? "Unmute" : "Mute"} onclick={toggleMute}>
@@ -1283,6 +1286,45 @@
</div> </div>
</footer> </footer>
<!-- Outside the footer on purpose: see the note at menuOpen. -->
{#if menuOpen}
<div
class="menu"
role="menu"
bind:this={menuEl}
style="right: {menuPos.right}px; bottom: {menuPos.bottom}px;"
>
<button role="menuitem" onclick={openQueue}>
<Icon name="playlist" size={15} /> Queue
</button>
<!-- Save's new home. The bar's download button became the expand
toggle, and this menu is where everything past Save was always
going to live. Only a stream can be saved, so the items appear
only for one. -->
{#if canSave}
<button role="menuitem" onclick={() => saveFromMenu("track")}>
<Icon name="music" size={15} /> Save this track
</button>
{#if savingAlbum}
<button role="menuitem" onclick={() => saveFromMenu("album")}>
<Icon name="library" size={15} /> Save whole album
</button>
{/if}
{/if}
<!-- Enabled only with a track: the overlay closes itself the moment
player.track is empty, so opening it then would just blink. -->
<button role="menuitem" disabled={!currentItem} onclick={openLyrics}>
<Icon name="lyrics" size={15} /> Lyrics
</button>
<button role="menuitem" disabled title="Not built yet">
<Icon name="panel-right" size={15} /> Mini player <span class="soon">soon</span>
</button>
<button role="menuitem" disabled title="Not built yet">
<Icon name="volume" size={15} /> Output device <span class="soon">soon</span>
</button>
</div>
{/if}
<style> <style>
/* FOUR blocks, absolutely placed, so the collapsed row and the expanded /* FOUR blocks, absolutely placed, so the collapsed row and the expanded
stack are the same layout at two sets of coordinates and the bar can stack are the same layout at two sets of coordinates and the bar can
@@ -1708,10 +1750,11 @@
place-items: center; place-items: center;
} }
.menu { .menu {
position: absolute; /* Fixed positioning escapes the bar's overflow clipping and every
bottom: 30px; panel stacking context (the SaveMenu idiom, same tier). The
right: -6px; coordinates come measured from the trigger at open. */
z-index: 40; position: fixed;
z-index: 1000;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;