finish animations and add elevation

This commit is contained in:
Vincent van der Wal
2026-08-06 22:19:17 +02:00
parent d0f94094ca
commit 83b0e46e64
21 changed files with 328 additions and 112 deletions
+26 -9
View File
@@ -7,6 +7,7 @@
import { page } from '$app/stores';
import {
mapTransitionCover,
markPageLoading,
markPageReady,
pageContentReady
@@ -104,10 +105,14 @@
const OVERLAY_CEILING_MS = 15000;
// The map is a cross-origin iframe, and a browser does not paint one into a
// view transition snapshot. Any transition with the maps page on either side
// animates a hole where the map is: leaving it, the map drops out at frame one
// and that blank sits under the incoming page for the whole run. Swapping
// outright is the honest answer - there is nothing here to cross-fade.
// view transition snapshot - captured bare, any transition with the maps page
// on either side would animate a hole where the map is. The way out is to
// make sure the map is never what gets captured: the maps page keeps an
// opaque cover over the iframe while the map boots (so an arrival fades into
// a clean panel, and the map fades up once ready), and raises the same cover
// again just before a departure is captured (see mapTransitionCover). Both
// snapshots then hold real pixels and the maps page transitions like any
// other route.
const MAPS_ROUTE = '/weather/maps';
const onMapsPage = () => routePath(get(page).url.pathname).startsWith(MAPS_ROUTE);
@@ -187,7 +192,7 @@
/**
* True when the navigation lands on the route and params the page is already
* showing - the sidebar's home link from the 7-day page it points at, or a
* showing - the sidebar's home link from the week page it points at, or a
* link that differs only in the query string.
*
* SvelteKit keeps the page component mounted for those, and nothing it holds
@@ -206,7 +211,7 @@
return JSON.stringify(from.params ?? {}) === JSON.stringify(to.params ?? {});
}
onNavigate((navigation) => {
onNavigate(async (navigation) => {
const pending =
PENDING_ROUTES.has(navigation.to?.route?.id ?? '') && !landsOnCurrentPage(navigation);
// Either way the flag is set explicitly: leaving a page that never resolved
@@ -217,9 +222,16 @@
// `startViewTransition` decides whether a transition is possible at all
// (support, reduced motion, one already capturing) and runs the update
// inline when it is not - so there is exactly one path from here down.
const touchesMap =
navigation.from?.route?.id === MAPS_ROUTE || navigation.to?.route?.id === MAPS_ROUTE;
const underTransition = canStartViewTransition() && !touchesMap;
const underTransition = canStartViewTransition();
// Leaving the maps page: cover the iframe before the outgoing state is
// captured, so the snapshot holds a clean panel instead of a hole where
// the map was. The tick makes sure the cover is actually in the DOM by
// the time the capture reads it.
if (underTransition && navigation.from?.route?.id === MAPS_ROUTE) {
mapTransitionCover.set(true);
await tick();
}
return new Promise<void>((swap) => {
void startViewTransition(
@@ -242,6 +254,11 @@
let mainEl = $state<HTMLElement | null>(null);
afterNavigate((navigation) => {
// The departure snapshot (if any) is taken by now, so the maps cover has
// done its job; lowering it here also means a later visit to the maps page
// starts from its own boot cover rather than a stuck one.
mapTransitionCover.set(false);
// The page scrolls inside <main>, not the window, so SvelteKit's own scroll
// handling never touches it and a new page would open half way down.
// Back/forward and in-page anchors keep their position.