feat: add configurable page transitions with framer-motion

This commit is contained in:
ralksta
2026-03-07 08:30:12 +01:00
parent e0bf02b31d
commit 1e079760d8
5 changed files with 91 additions and 1 deletions

View File

@@ -12,6 +12,7 @@ import { ThemeToggle } from '@/components/ThemeToggle';
import { ScrollToTop } from '@/components/ScrollToTop'; import { ScrollToTop } from '@/components/ScrollToTop';
import { Footer } from '@/components/Footer'; import { Footer } from '@/components/Footer';
import { DevToolbar } from '@/components/DevToolbar'; import { DevToolbar } from '@/components/DevToolbar';
import { PageTransition } from '@/components/PageTransition';
import { getConfig, getGoogleFontsUrl } from '@/lib/config'; import { getConfig, getGoogleFontsUrl } from '@/lib/config';
const siteTitle = process.env.SITE_TITLE || 'Gallery'; const siteTitle = process.env.SITE_TITLE || 'Gallery';
@@ -87,7 +88,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<ThemeToggle /> <ThemeToggle />
</nav> </nav>
</header> </header>
<PageTransition enabled={config.transitions}>
<main className="main">{children}</main> <main className="main">{children}</main>
</PageTransition>
<Footer /> <Footer />
<ScrollToTop /> <ScrollToTop />
{process.env.NODE_ENV === 'development' && <DevToolbar />} {process.env.NODE_ENV === 'development' && <DevToolbar />}

View File

@@ -0,0 +1,39 @@
'use client';
import { motion, AnimatePresence } from 'framer-motion';
import { usePathname } from 'next/navigation';
import React from 'react';
interface PageTransitionProps {
children: React.ReactNode;
enabled: boolean;
}
/**
* Provides smooth page transitions using framer-motion.
* Can be toggled on/off via gallery.yaml.
*/
export function PageTransition({ children, enabled }: PageTransitionProps) {
const pathname = usePathname();
if (!enabled) {
return <>{children}</>;
}
return (
<AnimatePresence mode="wait">
<motion.div
key={pathname}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{
duration: 0.4,
ease: [0.22, 1, 0.36, 1], // Custom "silk-smooth" cubic bezier
}}
>
{children}
</motion.div>
</AnimatePresence>
);
}

View File

@@ -124,6 +124,7 @@ interface GalleryYaml {
albums?: string[]; albums?: string[];
exifOnHover?: boolean; exifOnHover?: boolean;
map?: boolean; map?: boolean;
transitions?: boolean;
theme?: theme?:
| string | string
| { | {
@@ -243,6 +244,8 @@ export interface AppConfig {
footer: FooterConfig | null; footer: FooterConfig | null;
/** Show the /map page. Default: false. */ /** Show the /map page. Default: false. */
map: boolean; map: boolean;
/** Enable page transitions between routes. Default: true. */
transitions: boolean;
/** Explicit album name overrides from gallery.yaml. */ /** Explicit album name overrides from gallery.yaml. */
albumOverrides: Record<string, string>; albumOverrides: Record<string, string>;
cacheTtl: number; cacheTtl: number;
@@ -474,6 +477,7 @@ export function getConfig(): AppConfig {
} }
: null, : null,
map: gallery.map === true, map: gallery.map === true,
transitions: gallery.transitions !== false,
albumOverrides, albumOverrides,
cacheTtl: env.CACHE_TTL * 1000, cacheTtl: env.CACHE_TTL * 1000,
rateLimitRpm: env.RATE_LIMIT_RPM, rateLimitRpm: env.RATE_LIMIT_RPM,

43
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"framer-motion": "^12.35.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
@@ -4504,6 +4505,33 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/framer-motion": {
"version": "12.35.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.35.0.tgz",
"integrity": "sha512-w8hghCMQ4oq10j6aZh3U2yeEQv5K69O/seDI/41PK4HtgkLrcBovUNc0ayBC3UyyU7V1mrY2yLzvYdWJX9pGZQ==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.35.0",
"motion-utils": "^12.29.2",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -5703,6 +5731,21 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/motion-dom": {
"version": "12.35.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.35.0.tgz",
"integrity": "sha512-FFMLEnIejK/zDABn+vqGVAUN4T0+3fw+cVAY8MMT65yR+j5uMuvWdd4npACWhh94OVWQs79CrBBuwOwGRZAQiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.29.2"
}
},
"node_modules/motion-utils": {
"version": "12.29.2",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz",
"integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View File

@@ -16,6 +16,7 @@
}, },
"dependencies": { "dependencies": {
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"framer-motion": "^12.35.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",