fix(frontend): make infinite scroll viewport-relative, stop eager-loading

Lazy load fetched the entire list at once: every list's loader had a
"fill the viewport" recursion gated on
scrollContainer.scrollHeight <= clientHeight, but <main> is not the
scroller (the window/body is), so that condition is always true and it
recursed through every page (with a 10-item window, ~all pages fired at
once).

Move the filling logic into InfiniteScroll and base it on the sentinel's
viewport rect instead: load while the sentinel is within 300px of the
viewport bottom, re-checked synchronously after each load. This works
regardless of which element scrolls and loads only enough pages to reach
past the viewport.

Drop the per-page recursion (and now-unused scrollContainer refs / tick
imports) from the files, trash, tags, categories and pools lists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 00:14:04 +03:00
parent ffb8848a96
commit dc1af8c585
6 changed files with 35 additions and 48 deletions
@@ -7,23 +7,44 @@
let { loading = false, hasMore = true, onLoadMore }: Props = $props();
// Lookahead distance below the viewport at which we start loading.
const MARGIN = 300;
let sentinel = $state<HTMLDivElement | undefined>();
// Fire onLoadMore while the sentinel is within MARGIN px of the viewport
// bottom. Measuring the sentinel's viewport rect (rather than a scroll
// container's scrollHeight/clientHeight) makes this correct whether the page
// scrolls on <main> or on the window — and it loads exactly enough pages to
// reach past the viewport, instead of eagerly loading everything.
function maybeLoad() {
if (loading || !hasMore || !sentinel) return;
const rect = sentinel.getBoundingClientRect();
if (rect.top <= window.innerHeight + MARGIN) {
onLoadMore();
}
}
// Load on scroll: the observer notifies us when the sentinel nears the viewport.
$effect(() => {
if (!sentinel) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !loading && hasMore) {
onLoadMore();
}
if (entries[0].isIntersecting) maybeLoad();
},
{ rootMargin: '300px' },
{ rootMargin: `${MARGIN}px` },
);
observer.observe(sentinel);
return () => observer.disconnect();
});
// After each load settles (loading → false), re-check synchronously: if the
// freshly appended content still didn't push the sentinel past the viewport,
// load again. This fills short pages without the throttled observer lagging
// and over-fetching.
$effect(() => {
if (!loading) maybeLoad();
});
</script>
<div bind:this={sentinel} class="sentinel" aria-hidden="true"></div>
@@ -59,4 +80,4 @@
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</style>