feat(web): coupled section scrolling, earlier infinite-scroll, fuller player cover

Three coordinated polish changes:

1. PlayerBar cover bumps from h-20 (80px) to h-24 (96px) and the bar's
   vertical padding tightens from py-4 to py-1.5. Bar height stays
   ~108px but the cover now fills ~89% of it (was ~74%) — reads as the
   substantial primary content the operator wanted, not a thumbnail.

2. InfiniteScrollSentinel default rootMargin moves from 300px to 800px
   so the next page fetches well before the user reaches the bottom of
   the rendered set. Empirically that's ~3-4 rows of cards on a typical
   library grid — loading feels seamless rather than catching up.

3. HorizontalScrollRow takes rows: T[][] instead of items: T[]. Multiple
   rows of items now render inside one shared overflow-x-auto container,
   so the rows scroll together as a single coupled section. Recently
   added (2 album rows) and Most played (3 track rows) on the home page
   now scroll as one unit. Rediscover keeps two separate scrollers
   because its rows are different card types (square albums vs circular
   artists) — coupling those would interleave shapes awkwardly. The
   item snippet's second arg is now the global flat index so consumers
   like CompactTrackCard (which needs sectionTracks + index for play
   actions) work without per-row re-indexing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 23:52:03 -04:00
parent 8d05f623cb
commit 2946ec4222
5 changed files with 85 additions and 54 deletions
+30 -32
View File
@@ -28,7 +28,7 @@
{:else if showSkeleton.value && !data}
<p class="text-text-secondary">Loading…</p>
{:else if data}
<!-- Recently added: 50 albums in 2 rows of 25 -->
<!-- Recently added: 50 albums in 2 rows of 25, scrolling together -->
<section class="space-y-3">
{#if data.recently_added_albums.length === 0}
<header>
@@ -36,21 +36,21 @@
</header>
<p class="text-text-secondary">Nothing added yet. Scan a folder via the server's config.</p>
{:else}
{#each chunk(data.recently_added_albums, 25) as row, i (i)}
<HorizontalScrollRow
items={row}
title={i === 0 ? 'Recently added' : undefined}
ariaLabel="Recently added row {i + 1}"
>
{#snippet item(album: import('$lib/api/types').AlbumRef)}
<div class="w-44"><AlbumCard {album} /></div>
{/snippet}
</HorizontalScrollRow>
{/each}
<HorizontalScrollRow
rows={chunk(data.recently_added_albums, 25)}
title="Recently added"
ariaLabel="Recently added"
>
{#snippet item(album: import('$lib/api/types').AlbumRef)}
<div class="w-44"><AlbumCard {album} /></div>
{/snippet}
</HorizontalScrollRow>
{/if}
</section>
<!-- Rediscover: albums + artists -->
<!-- Rediscover: albums + artists. Kept as two scrollers because the
card types differ (square vs circular); coupling them in one
scroller would interleave shapes awkwardly. -->
<section class="space-y-3">
{#if data.rediscover_albums.length === 0 && data.rediscover_artists.length === 0}
<header>
@@ -60,7 +60,7 @@
{:else}
{#if data.rediscover_albums.length > 0}
<HorizontalScrollRow
items={data.rediscover_albums}
rows={[data.rediscover_albums]}
title="Rediscover"
ariaLabel="Rediscover albums"
>
@@ -71,7 +71,7 @@
{/if}
{#if data.rediscover_artists.length > 0}
<HorizontalScrollRow
items={data.rediscover_artists}
rows={[data.rediscover_artists]}
title={data.rediscover_albums.length === 0 ? 'Rediscover' : undefined}
ariaLabel="Rediscover artists"
>
@@ -83,7 +83,7 @@
{/if}
</section>
<!-- Most played: 75 tracks in 3 rows of 25 -->
<!-- Most played: 75 tracks in 3 rows of 25, scrolling together -->
<section class="space-y-3">
{#if data.most_played_tracks.length === 0}
<header>
@@ -91,21 +91,19 @@
</header>
<p class="text-text-secondary">No plays to draw from. Listen to something.</p>
{:else}
{#each chunk(data.most_played_tracks, 25) as row, i (i)}
<HorizontalScrollRow
items={row}
title={i === 0 ? 'Most played' : undefined}
ariaLabel="Most played row {i + 1}"
>
{#snippet item(track: import('$lib/api/types').TrackRef, idx: number)}
<CompactTrackCard
{track}
sectionTracks={row}
index={idx}
/>
{/snippet}
</HorizontalScrollRow>
{/each}
<HorizontalScrollRow
rows={chunk(data.most_played_tracks, 25)}
title="Most played"
ariaLabel="Most played"
>
{#snippet item(track: import('$lib/api/types').TrackRef, globalIdx: number)}
<CompactTrackCard
{track}
sectionTracks={data.most_played_tracks}
index={globalIdx}
/>
{/snippet}
</HorizontalScrollRow>
{/if}
</section>
@@ -118,7 +116,7 @@
<p class="text-text-secondary">No recent plays.</p>
{:else}
<HorizontalScrollRow
items={data.last_played_artists}
rows={[data.last_played_artists]}
title="Last played"
ariaLabel="Last played artists"
>