feat: add configurable page transitions with framer-motion
This commit is contained in:
@@ -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>
|
||||||
<main className="main">{children}</main>
|
<PageTransition enabled={config.transitions}>
|
||||||
|
<main className="main">{children}</main>
|
||||||
|
</PageTransition>
|
||||||
<Footer />
|
<Footer />
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
{process.env.NODE_ENV === 'development' && <DevToolbar />}
|
{process.env.NODE_ENV === 'development' && <DevToolbar />}
|
||||||
|
|||||||
39
components/PageTransition.tsx
Normal file
39
components/PageTransition.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
43
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user