Files
jellytau/src/lib/utils/scrollRestore.ts
T
dtourolle ad48d89dfe chore(format): run prettier over src/ and scripts/
Formatting was configured but never enforced: `bun run format:check`
reported 199 unformatted files and ran in no workflow and in no git hook,
so .prettierrc (printWidth 100, trailing commas) described an intention
rather than the tree.

This is the one-time sweep that makes the check gateable. Whitespace and
token-reflow only -- no behavioural change: `bun run check` reports 0
errors and all 1053 frontend tests pass before and after.

Kept out of every other commit on purpose. A 199-file diff mixed with
real changes is unreviewable, and the next commit turns format:check
into a hard CI gate so this cannot silently accumulate again.
2026-08-21 17:41:44 +02:00

89 lines
3.4 KiB
TypeScript

/**
* Per-route scroll memory for the app's persistent scroll containers.
*
* The shell keeps its scrollers alive across navigation on purpose: the root
* layout, the home page and the library layout each own a
* `flex-1 overflow-y-auto` box that outlives the route rendered inside it. That
* is what makes the bottom UI a flex sibling rather than a measured overlay —
* but it also means the *element* never remounts, so its `scrollTop` survives a
* route change and the next page opens part-way down.
*
* SvelteKit's own scroll restoration cannot help here: it saves and restores
* `window` scroll, and in this app the window never scrolls at all.
*
* So each container gets its own memory, which reproduces normal browser
* behaviour:
*
* - **forward** (link/goto/form) — a fresh visit, always lands at the top;
* - **popstate** (hardware/gesture Back or Forward) — restores the offset the
* route was left at, so Back out of a detail page returns you to your place
* in the list rather than to the top of it;
* - **enter** (initial load) — left alone; there is nothing to leak yet.
*
* The decision is pure and lives here so it can be unit-tested without a DOM;
* `scrollContainer.svelte.ts` is the thin action that applies it.
*
* TRACES: UR-054 | DR-156
*/
/** How a navigation should affect a persistent scroll container. */
export type NavKind = "enter" | "popstate" | "forward";
/** What to do with the container once the new route has rendered. */
export type ScrollAction = { kind: "reset" } | { kind: "restore"; top: number } | { kind: "none" };
/**
* Collapse SvelteKit's navigation types into the three cases that matter.
*
* `enter` is the initial load. `popstate` is a Back/Forward gesture. Everything
* else — `link`, `goto`, `form` — is a forward move into a new page.
*/
export function classifyNavigation(nav: { type?: string | null }): NavKind {
if (nav.type === "enter") return "enter";
if (nav.type === "popstate") return "popstate";
return "forward";
}
/**
* Remembers the offset each route was left at, for one scroll container.
*
* One instance per container: the root scroller, the home scroller and the
* library scroller hold different content for the same URL, so a shared map
* would restore one container's offset into another.
*/
export class ScrollMemory {
#offsets = new Map<string, number>();
/** Record where `key` was scrolled to, before we navigate away from it. */
save(key: string, top: number): void {
this.#offsets.set(key, Math.max(0, top));
}
/**
* Decide what the container should do on arriving at `key`.
*
* Note this does not consume the saved offset: a route can be returned to
* more than once, and each Back should restore the same place.
*/
decide(key: string, kind: NavKind): ScrollAction {
if (kind === "enter") return { kind: "none" };
if (kind === "popstate") return { kind: "restore", top: this.#offsets.get(key) ?? 0 };
return { kind: "reset" };
}
/** Drop everything. Intended for tests and sign-out. */
clear(): void {
this.#offsets.clear();
}
}
/**
* The memory key for a URL.
*
* Path plus query: a library grid filtered by genre is a different list from
* the unfiltered one, and returning to it should restore its own place.
*/
export function scrollKey(url: { pathname: string; search?: string }): string {
return `${url.pathname}${url.search ?? ""}`;
}