feat(web): alphabet rail chases pages on click for unloaded letters
test-web / test (push) Successful in 32s

Operator: prior rail disabled empty buckets, so clicking a letter
like 'Z' did nothing if it wasn't loaded yet. AlphabeticalGrid now
accepts a paginated source and walks pages on click until the
target letter surfaces.

- New props: hasMore, onLoadMore. When hasMore=true, every empty
  bucket stays enabled (clicking will chase pages); when hasMore=
  false, only populated buckets are clickable.
- jumpTo(bucket): if populated, scrollIntoView. Otherwise loop
  onLoadMore + tick() until the bucket appears or no more pages.
  Loader2 spinner replaces the letter on the pending button;
  cursor:wait + aria-busy. Other rail buttons disable while one is
  pending so clicks don't stack.
- Library Artists + Albums pass hasMore + onLoadMore through to the
  grid. Existing InfiniteScrollSentinel still handles scroll-driven
  loading.

Test: new case asserts rail enables empty buckets when hasMore=true
(the click would trigger onLoadMore).
This commit is contained in:
2026-06-01 22:29:13 -04:00
parent ad1a000ad5
commit d4c6bb3f2d
4 changed files with 93 additions and 9 deletions
@@ -73,6 +73,8 @@
<AlphabeticalGrid
items={filtered}
getKey={(a: AlbumRef) => a.sort_title || a.title}
hasMore={!!query.hasNextPage}
onLoadMore={() => query.fetchNextPage()}
>
{#snippet item(album: AlbumRef)}
<AlbumCard {album} />
@@ -71,6 +71,8 @@
<AlphabeticalGrid
items={filtered}
getKey={(a: ArtistRef) => a.sort_name || a.name}
hasMore={!!query.hasNextPage}
onLoadMore={() => query.fetchNextPage()}
>
{#snippet item(a: ArtistRef)}
<ArtistCard artist={a} />