improvement(scroll): fade the page scroller's clipped edges

The subdued edge-fade the rail and side panels use now hints at more content on
the page scroller too, which the feed, the channel page and Plex all scroll
inside. Both <main>s move into one PageScroller so the invariants they share —
the load-bearing scrollbar gutter, and the bar staying visible because hiding it
would zero that gutter — are stated once.

Two fixes the hook needed first: it set fresh state on every scroll event, which
above a page tree would re-render it per frame; and it watched a single child
captured at mount, so a lazy page swapping out its Suspense fallback left the
observer on a detached node and the fade frozen at its pre-load value (measured
on the Plex page: no bottom fade over a 3119px list).
This commit is contained in:
2026-07-17 03:14:37 +02:00
parent 0b56eb9d53
commit a032324513
4 changed files with 72 additions and 24 deletions
+3 -8
View File
@@ -54,6 +54,7 @@ import NavSidebar from "./components/NavSidebar";
import Sidebar from "./components/Sidebar";
import PlaylistsRail from "./components/PlaylistsRail";
import BackToTop from "./components/BackToTop";
import PageScroller from "./components/PageScroller";
import GlassTuner from "./components/GlassTuner";
import ChatDock from "./components/ChatDock";
import Toaster from "./components/Toaster";
@@ -898,13 +899,7 @@ export default function App() {
>
{meQuery.data!.is_demo && <DemoBanner />}
<VersionBanner onOpen={() => openReleaseNotes(CURRENT_VERSION)} />
{/* scrollbar-gutter: stable is load-bearing, not cosmetic. The feed derives its column
count from this element's clientWidth and its thumbnails are aspect-ratio boxes, so
when the content lands within a scrollbar's width of the viewport height the two feed
each other: scrollbar appears -> 15px narrower -> shorter thumbs -> content fits ->
scrollbar goes -> wider -> taller -> repeat, every frame. Reserving the gutter keeps
clientWidth constant and breaks the loop. */}
<main className="flex-1 min-w-0 overflow-y-auto [scrollbar-gutter:stable]">
<PageScroller>
<Suspense fallback={pageFallback}>
{page === "channels" ? (
<Channels
@@ -986,7 +981,7 @@ export default function App() {
/>
)}
</Suspense>
</main>
</PageScroller>
</div>
</>
)}