feat: directional slide-x page transitions in router

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ulas
2026-03-30 17:08:49 +02:00
parent 194728bbe9
commit bc3f855fa9
2 changed files with 61 additions and 15 deletions
+22 -5
View File
@@ -85,6 +85,16 @@ let currentPath = null;
// Router // Router
// -------------------------------------------------------- // --------------------------------------------------------
const ROUTE_ORDER = ['/', '/tasks', '/shopping', '/meals', '/calendar',
'/notes', '/contacts', '/budget', '/settings'];
function getDirection(fromPath, toPath) {
const fromIdx = ROUTE_ORDER.indexOf(fromPath ?? '/');
const toIdx = ROUTE_ORDER.indexOf(toPath);
if (fromIdx === -1 || toIdx === -1 || fromPath === toPath) return 'right';
return toIdx > fromIdx ? 'right' : 'left';
}
/** /**
* Navigiert zu einem Pfad und rendert die entsprechende Seite. * Navigiert zu einem Pfad und rendert die entsprechende Seite.
* @param {string} path * @param {string} path
@@ -100,6 +110,8 @@ async function navigate(path, userOrPushState = true, pushState = true) {
pushState = userOrPushState; pushState = userOrPushState;
} }
// Alten Pfad merken, bevor currentPath aktualisiert wird — für Richtungsberechnung
const previousPath = currentPath;
currentPath = path; currentPath = path;
const route = ROUTES.find((r) => r.path === path) ?? ROUTES.find((r) => r.path === '/'); const route = ROUTES.find((r) => r.path === path) ?? ROUTES.find((r) => r.path === '/');
@@ -126,7 +138,7 @@ async function navigate(path, userOrPushState = true, pushState = true) {
history.pushState({ path }, '', path); history.pushState({ path }, '', path);
} }
await renderPage(route); await renderPage(route, previousPath);
updateNav(path); updateNav(path);
updateThemeColorForRoute(route); updateThemeColorForRoute(route);
} }
@@ -134,8 +146,9 @@ async function navigate(path, userOrPushState = true, pushState = true) {
/** /**
* Lädt und rendert eine Seite dynamisch. * Lädt und rendert eine Seite dynamisch.
* @param {{ path: string, page: string }} route * @param {{ path: string, page: string }} route
* @param {string|null} previousPath - Pfad vor der Navigation (für Richtungsberechnung)
*/ */
async function renderPage(route) { async function renderPage(route, previousPath = null) {
const app = document.getElementById('app'); const app = document.getElementById('app');
const loading = document.getElementById('app-loading'); const loading = document.getElementById('app-loading');
@@ -158,18 +171,22 @@ async function renderPage(route) {
const content = document.getElementById('page-content') || app; const content = document.getElementById('page-content') || app;
// Richtung bestimmen (previousPath ist der alte Pfad vor der Navigation)
const direction = getDirection(previousPath, route.path);
const outClass = direction === 'right' ? 'page-transition--out-left' : 'page-transition--out-right';
const inClass = direction === 'right' ? 'page-transition--in-right' : 'page-transition--in-left';
// Alte Seite kurz ausfaden, falls vorhanden // Alte Seite kurz ausfaden, falls vorhanden
const oldPage = content.querySelector('.page-transition'); const oldPage = content.querySelector('.page-transition');
if (oldPage) { if (oldPage) {
oldPage.classList.add('page-transition--out'); oldPage.classList.add(outClass);
await new Promise(r => setTimeout(r, 120)); await new Promise(r => setTimeout(r, 120));
} }
// Seiten-Wrapper bereits jetzt in den DOM einfügen, damit // Seiten-Wrapper bereits jetzt in den DOM einfügen, damit
// document.getElementById() in render() die richtigen Elemente findet. // document.getElementById() in render() die richtigen Elemente findet.
const pageWrapper = document.createElement('div'); const pageWrapper = document.createElement('div');
pageWrapper.className = 'page-transition'; pageWrapper.className = `page-transition ${inClass}`;
pageWrapper.style.animation = 'page-in 0.2s ease forwards';
content.replaceChildren(pageWrapper); content.replaceChildren(pageWrapper);
await module.render(pageWrapper, { user: currentUser }); await module.render(pageWrapper, { user: currentUser });
+39 -10
View File
@@ -44,22 +44,51 @@
} }
/* -------------------------------------------------------- /* --------------------------------------------------------
* Seiten-Übergangs-Animation * Seiten-Übergangs-Animation (direktional)
* -------------------------------------------------------- */ * -------------------------------------------------------- */
@keyframes page-in { @keyframes page-slide-in-right {
from { opacity: 0; transform: translateY(4px); } from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateX(0); }
}
@keyframes page-slide-in-left {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes page-out-left {
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(-20px); }
}
@keyframes page-out-right {
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(20px); }
} }
@keyframes page-out { .page-transition--in-right {
from { opacity: 1; } animation: page-slide-in-right 0.2s var(--ease-out) forwards;
to { opacity: 0; }
} }
.page-transition--in-left {
.page-transition--out { animation: page-slide-in-left 0.2s var(--ease-out) forwards;
animation: page-out 0.12s ease forwards; }
.page-transition--out-left {
animation: page-out-left 0.12s ease forwards;
pointer-events: none; pointer-events: none;
} }
.page-transition--out-right {
animation: page-out-right 0.12s ease forwards;
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.page-transition--in-right,
.page-transition--in-left {
animation: none;
opacity: 1;
}
.page-transition--out-left,
.page-transition--out-right {
animation: none;
}
}
/* -------------------------------------------------------- /* --------------------------------------------------------
* Layout: Mobile (Standard, < 1024px) * Layout: Mobile (Standard, < 1024px)