feat(hikes): view-transition exit flow off detail page

Fires the same strip-slide-out + below-strip-fly-down animation whenever
a hike detail page is left for anywhere other than another slug — not
just on the back-to-/hikes path. Layout adds a new `vt-exit-hike-detail`
class on the document root for that case; the css rules tag onto the
existing `vt-enter-hikes` selectors so both exits drive identical
animations.
This commit is contained in:
2026-05-26 22:54:35 +02:00
parent 0f6c50f854
commit ac76bfba34
3 changed files with 33 additions and 7 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "homepage", "name": "homepage",
"version": "1.92.0", "version": "1.93.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+24 -6
View File
@@ -533,16 +533,32 @@ html.vt-exit-hikes::view-transition-old(.hike-fly-in):only-child {
animation: hikes-fly-down 700ms cubic-bezier(0.4, 0.1, 0.4, 1) both; animation: hikes-fly-down 700ms cubic-bezier(0.4, 0.1, 0.4, 1) both;
} }
/* Photo strip slides in from the right when arriving at the detail page, /* Photo strip slides in from the right when arriving at a detail page,
* and slides back out when returning to /hikes. Overrides the global root * and slides back out whenever the detail page is left for any other
* `animation: none` only for these hike-specific transitions. */ * route (back to /hikes, off to /, /hikes/route-builder, …). Both exit
* scopes (vt-enter-hikes for the back-nav case, vt-exit-hike-detail for
* everywhere else) trigger the same animation. */
html.vt-enter-hike-detail::view-transition-new(hike-strip):only-child { html.vt-enter-hike-detail::view-transition-new(hike-strip):only-child {
animation: hike-strip-in-right 600ms cubic-bezier(0.2, 0.7, 0.2, 1) both; animation: hike-strip-in-right 600ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
} }
html.vt-enter-hikes::view-transition-old(hike-strip):only-child { html.vt-enter-hikes::view-transition-old(hike-strip):only-child,
html.vt-exit-hike-detail::view-transition-old(hike-strip):only-child {
animation: hike-strip-out-right 600ms cubic-bezier(0.4, 0.1, 0.4, 1) both; animation: hike-strip-out-right 600ms cubic-bezier(0.4, 0.1, 0.4, 1) both;
} }
/* Everything below the photo strip on a detail page (metrics, tags,
* elevation chart, scroll area, meta footer) slides up from the bottom
* on enter and back down on any exit. Wrapper element carries
* `view-transition-name: hike-below-strip`; the rest of the page chrome
* still cross-fades via the root-pseudo rule above. */
html.vt-enter-hike-detail::view-transition-new(hike-below-strip):only-child {
animation: hikes-fly-up 700ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
html.vt-enter-hikes::view-transition-old(hike-below-strip):only-child,
html.vt-exit-hike-detail::view-transition-old(hike-below-strip):only-child {
animation: hikes-fly-down 700ms cubic-bezier(0.4, 0.1, 0.4, 1) both;
}
/* Cross-fade the rest of the page (root pseudo) during hike transitions so /* Cross-fade the rest of the page (root pseudo) during hike transitions so
* the destination's chrome — metrics + content + footer on the detail page, * the destination's chrome — metrics + content + footer on the detail page,
* overview hero + credit on the index — phases in instead of snapping in * overview hero + credit on the index — phases in instead of snapping in
@@ -550,12 +566,14 @@ html.vt-enter-hikes::view-transition-old(hike-strip):only-child {
* other routes' transitions on their existing instant-swap behavior. */ * other routes' transitions on their existing instant-swap behavior. */
html.vt-enter-hike-detail::view-transition-old(root), html.vt-enter-hike-detail::view-transition-old(root),
html.vt-enter-hikes::view-transition-old(root), html.vt-enter-hikes::view-transition-old(root),
html.vt-exit-hikes::view-transition-old(root) { html.vt-exit-hikes::view-transition-old(root),
html.vt-exit-hike-detail::view-transition-old(root) {
animation: hikes-root-fade-out 450ms ease-out both; animation: hikes-root-fade-out 450ms ease-out both;
} }
html.vt-enter-hike-detail::view-transition-new(root), html.vt-enter-hike-detail::view-transition-new(root),
html.vt-enter-hikes::view-transition-new(root), html.vt-enter-hikes::view-transition-new(root),
html.vt-exit-hikes::view-transition-new(root) { html.vt-exit-hikes::view-transition-new(root),
html.vt-exit-hike-detail::view-transition-new(root) {
animation: hikes-root-fade-in 450ms ease-out both; animation: hikes-root-fade-in 450ms ease-out both;
} }
+8
View File
@@ -79,15 +79,22 @@
// (Covers /hikes → / AND /hikes → /hikes/[slug], where the clicked // (Covers /hikes → / AND /hikes → /hikes/[slug], where the clicked
// card pairs into the hero and the rest fly out.) // card pairs into the hero and the rest fly out.)
// - vt-enter-hike-detail: arriving at a hike detail page (card → zoom). // - vt-enter-hike-detail: arriving at a hike detail page (card → zoom).
// - vt-exit-hike-detail: leaving a hike detail page for anywhere
// else (back to /hikes, off to /, route-builder, …) → photo strip
// slides back out to the right and the below-strip block flies
// down. Excluded for slug → slug navigations (both sides share the
// same route.id, so paired UA transitions handle them).
const intoHikesIndex = toId === '/hikes' && fromId !== '/hikes'; const intoHikesIndex = toId === '/hikes' && fromId !== '/hikes';
const outOfHikesIndex = fromId === '/hikes' && toId !== '/hikes'; const outOfHikesIndex = fromId === '/hikes' && toId !== '/hikes';
const intoHikeDetail = toId === '/hikes/[slug]'; const intoHikeDetail = toId === '/hikes/[slug]';
const outOfHikeDetail = fromId === '/hikes/[slug]' && toId !== '/hikes/[slug]';
return new Promise((resolve) => { return new Promise((resolve) => {
const root = document.documentElement; const root = document.documentElement;
if (intoHikesIndex) root.classList.add('vt-enter-hikes'); if (intoHikesIndex) root.classList.add('vt-enter-hikes');
if (outOfHikesIndex) root.classList.add('vt-exit-hikes'); if (outOfHikesIndex) root.classList.add('vt-exit-hikes');
if (intoHikeDetail) root.classList.add('vt-enter-hike-detail'); if (intoHikeDetail) root.classList.add('vt-enter-hike-detail');
if (outOfHikeDetail) root.classList.add('vt-exit-hike-detail');
const transition = (/** @type {any} */ (document)).startViewTransition(async () => { const transition = (/** @type {any} */ (document)).startViewTransition(async () => {
resolve(); resolve();
await navigation.complete; await navigation.complete;
@@ -96,6 +103,7 @@
root.classList.remove('vt-enter-hikes'); root.classList.remove('vt-enter-hikes');
root.classList.remove('vt-exit-hikes'); root.classList.remove('vt-exit-hikes');
root.classList.remove('vt-enter-hike-detail'); root.classList.remove('vt-enter-hike-detail');
root.classList.remove('vt-exit-hike-detail');
}); });
}); });
}); });