feat(library): genre sliders, artist links, and navigation utils
- music landing: diverse per-genre album sliders (online counts / offline wide-probe fallback) and home-screen library shortcuts - add ArtistLinks component and shared navigation/genreDiversity utils - player/playback-mode refinements across Rust and frontend
This commit is contained in:
@@ -26,17 +26,28 @@
|
||||
|
||||
let { availableLetters, onJump, bottomGap = "5rem" }: Props = $props();
|
||||
|
||||
// The strip stretches to fill the height of its scroll container (the page's
|
||||
// <main>, which sits below the sticky header) from the sticky top offset down
|
||||
// to the bottom gap reserved for the nav / mini-player bars. We measure the
|
||||
// container's visible height live so it stays correct regardless of header
|
||||
// size, platform, or window resizes.
|
||||
// The strip stretches from where it sits down to just above the bottom nav /
|
||||
// mini-player bars. Those bars are pinned to the bottom of the screen, so the
|
||||
// hard floor is `window.innerHeight - bottomGap`. We measure the strip's own
|
||||
// top against that floor (clamped to non-negative) and update on scroll/resize
|
||||
// so it never slides under the bars regardless of header or platform.
|
||||
let container = $state<HTMLDivElement | null>(null);
|
||||
let viewportHeight = $state(0);
|
||||
let stripHeight = $state(0);
|
||||
|
||||
function measure() {
|
||||
const scroller = container?.closest("main") ?? document.documentElement;
|
||||
viewportHeight = scroller.clientHeight;
|
||||
if (!container) return;
|
||||
const top = container.getBoundingClientRect().top;
|
||||
const floor = window.innerHeight - remToPx(bottomGap);
|
||||
stripHeight = Math.max(0, floor - top);
|
||||
}
|
||||
|
||||
function remToPx(len: string): number {
|
||||
const n = parseFloat(len);
|
||||
if (len.trim().endsWith("rem")) {
|
||||
const root = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
||||
return n * root;
|
||||
}
|
||||
return n; // assume px otherwise
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
@@ -44,13 +55,21 @@
|
||||
const scroller = container?.closest("main");
|
||||
const ro = new ResizeObserver(measure);
|
||||
if (scroller) ro.observe(scroller);
|
||||
scroller?.addEventListener("scroll", measure, { passive: true });
|
||||
window.addEventListener("resize", measure);
|
||||
return () => {
|
||||
ro.disconnect();
|
||||
scroller?.removeEventListener("scroll", measure);
|
||||
window.removeEventListener("resize", measure);
|
||||
};
|
||||
});
|
||||
|
||||
// Recompute when the reserved bottom gap changes (mini-player shows/hides).
|
||||
$effect(() => {
|
||||
bottomGap;
|
||||
measure();
|
||||
});
|
||||
|
||||
const letters = $derived([HASH, ...ALPHABET]);
|
||||
let activeLetter = $state<string | null>(null);
|
||||
|
||||
@@ -99,7 +118,7 @@
|
||||
<div
|
||||
bind:this={container}
|
||||
class="flex flex-col items-center justify-between select-none touch-none py-1"
|
||||
style="height: calc({viewportHeight}px - 1.5rem - {bottomGap})"
|
||||
style="height: {stripHeight}px"
|
||||
onpointerdown={handlePointerDown}
|
||||
onpointermove={handlePointerMove}
|
||||
role="navigation"
|
||||
|
||||
Reference in New Issue
Block a user