diff --git a/app/layout.tsx b/app/layout.tsx
index 5fd7bc3..8ce0f67 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -12,6 +12,7 @@ import { ThemeToggle } from '@/components/ThemeToggle';
import { ScrollToTop } from '@/components/ScrollToTop';
import { Footer } from '@/components/Footer';
import { DevToolbar } from '@/components/DevToolbar';
+import { PageTransition } from '@/components/PageTransition';
import { getConfig, getGoogleFontsUrl } from '@/lib/config';
const siteTitle = process.env.SITE_TITLE || 'Gallery';
@@ -87,7 +88,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
- {children}
+
+ {children}
+
{process.env.NODE_ENV === 'development' && }
diff --git a/components/PageTransition.tsx b/components/PageTransition.tsx
new file mode 100644
index 0000000..230f33e
--- /dev/null
+++ b/components/PageTransition.tsx
@@ -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 (
+
+
+ {children}
+
+
+ );
+}
diff --git a/lib/config.ts b/lib/config.ts
index baf3b32..c096498 100644
--- a/lib/config.ts
+++ b/lib/config.ts
@@ -124,6 +124,7 @@ interface GalleryYaml {
albums?: string[];
exifOnHover?: boolean;
map?: boolean;
+ transitions?: boolean;
theme?:
| string
| {
@@ -243,6 +244,8 @@ export interface AppConfig {
footer: FooterConfig | null;
/** Show the /map page. Default: false. */
map: boolean;
+ /** Enable page transitions between routes. Default: true. */
+ transitions: boolean;
/** Explicit album name overrides from gallery.yaml. */
albumOverrides: Record;
cacheTtl: number;
@@ -474,6 +477,7 @@ export function getConfig(): AppConfig {
}
: null,
map: gallery.map === true,
+ transitions: gallery.transitions !== false,
albumOverrides,
cacheTtl: env.CACHE_TTL * 1000,
rateLimitRpm: env.RATE_LIMIT_RPM,
diff --git a/package-lock.json b/package-lock.json
index b1f590a..98b298c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@types/leaflet": "^1.9.21",
+ "framer-motion": "^12.35.0",
"gray-matter": "^4.0.3",
"js-yaml": "^4.1.1",
"leaflet": "^1.9.4",
@@ -4504,6 +4505,33 @@
"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": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -5703,6 +5731,21 @@
"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": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
diff --git a/package.json b/package.json
index c98c8da..4d49e3a 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
},
"dependencies": {
"@types/leaflet": "^1.9.21",
+ "framer-motion": "^12.35.0",
"gray-matter": "^4.0.3",
"js-yaml": "^4.1.1",
"leaflet": "^1.9.4",