feat(ui): View Transitions crossfade for library switches

Where the browser supports it (Chrome 111+, Safari 18+, Firefox 139+),
switchWithTransition now uses the native View Transitions API: the old
shelves are snapshotted, the fetch + swap happen behind the dim, and the
two snapshots crossfade — no blank phase, no manual opacity
choreography. The dim + spinner is hidden inside the update so it blends
away as part of the transition. Browsers without the API keep the
existing dim + fade sequence unchanged. Zero dependencies — the
crossfade is the browser's own.
This commit is contained in:
John O'Keefe committed 2026-10-18 16:00:04 -04:00
1 parent a11b4a9cc4
commit a31da89df5
1 file changed
+27
+27
View File
@@ -15,6 +15,33 @@ export async function switchWithTransition(
if (!container || !loading) return;
// View Transitions (native, where supported): the browser snapshots the
// old shelves, fetchFn swaps the DOM, and the two snapshots crossfade —
// no blank phase, no manual opacity choreography. The dim + spinner is
// shown before the snapshot and hidden inside the update, so the dim
// blends away as part of the crossfade. Browsers without the API fall
// through to the dim + fade sequence below.
const doc = document as Document & {
startViewTransition?: (updateCallback: () => void | Promise<void>) => {
finished: Promise<void>;
};
};
if (doc.startViewTransition) {
try {
loading.classList.remove("hidden");
const transition = doc.startViewTransition(async () => {
await fetchFn();
loading.classList.add("hidden");
});
await transition.finished;
} catch (error) {
loading.classList.add("hidden");
showToast("Failed to load library", "error");
console.error("Switch library error:", error);
}
return;
}
try {
container.classList.add(
"opacity-0",