Skip to content

Commit a88fa53

Browse files
committed
implement framer-motion page transitions and entry animations across landing, portal, and doc pages
1 parent 8c351f0 commit a88fa53

6 files changed

Lines changed: 167 additions & 34 deletions

File tree

package-lock.json

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@easyops-cn/docusaurus-search-local": "^0.55.1",
2020
"@mdx-js/react": "^3.0.0",
2121
"clsx": "^2.1.0",
22+
"framer-motion": "^12.38.0",
2223
"react": "^18.2.0",
2324
"react-dom": "^18.2.0",
2425
"remark-github-admonitions-to-directives": "^2.1.0"

src/components/ui/Card.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11

2+
import { motion, HTMLMotionProps } from 'framer-motion';
3+
24
interface CardGridProps {
35
children: React.ReactNode;
46
}
@@ -11,15 +13,15 @@ export function CardGrid({ children }: CardGridProps) {
1113
);
1214
}
1315

14-
interface CardProps {
16+
interface CardProps extends HTMLMotionProps<"div"> {
1517
title: string;
1618
emoji?: string;
1719
children: React.ReactNode;
1820
}
1921

20-
export function Card({ title, emoji = '⭐', children }: CardProps) {
22+
export function Card({ title, emoji = '⭐', children, ...motionProps }: CardProps) {
2123
return (
22-
<div className="col col--6" style={{ marginBottom: '1.5rem' }}>
24+
<motion.div className="col col--6" style={{ marginBottom: '1.5rem' }} {...motionProps}>
2325
<div className="card" style={{ height: '100%', padding: '1.25rem', border: '1px solid var(--ifm-color-emphasis-200)', background: 'var(--ifm-color-emphasis-100)' }}>
2426
<div className="card__header" style={{ padding: 0, marginBottom: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
2527
<span style={{ fontSize: '1.125rem' }}>{emoji}</span>
@@ -29,6 +31,6 @@ export function Card({ title, emoji = '⭐', children }: CardProps) {
2931
{children}
3032
</div>
3133
</div>
32-
</div>
34+
</motion.div>
3335
);
3436
}

src/components/ui/ProductLandingPage.tsx

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@ import Link from '@docusaurus/Link';
22
import Layout from '@theme/Layout';
33
import Heading from '@theme/Heading';
44
import { Card, CardGrid } from '@site/src/components/ui/Card';
5+
import { motion, AnimatePresence } from 'framer-motion';
6+
import { useLocation } from '@docusaurus/router';
7+
8+
const containerVariants = {
9+
hidden: { opacity: 0 },
10+
show: {
11+
opacity: 1,
12+
transition: { staggerChildren: 0.1 }
13+
}
14+
};
15+
16+
const itemVariants = {
17+
hidden: { opacity: 0, y: 20 },
18+
show: { opacity: 1, y: 0, transition: { type: 'spring' as const, stiffness: 300, damping: 24 } }
19+
};
520

621
export interface ActionButton {
722
text: string;
@@ -32,25 +47,38 @@ export function ProductLandingPage({
3247
actions,
3348
features,
3449
}: ProductLandingPageProps): React.JSX.Element {
50+
const location = useLocation();
51+
3552
return (
3653
<Layout
3754
title={`${title} - Leaderboard Plugin`}
3855
description={description || tagline}
3956
>
40-
<header className="hero hero--primary" style={{ padding: '5rem 0', textAlign: 'center' }}>
57+
<AnimatePresence mode="wait">
58+
<motion.div
59+
key={location.pathname}
60+
variants={containerVariants}
61+
initial="hidden"
62+
animate="show"
63+
exit={{ opacity: 0, y: -20, transition: { duration: 0.2 } }}
64+
>
65+
<header className="hero hero--primary" style={{ padding: '5rem 0', textAlign: 'center' }}>
4166
<div className="container">
42-
<img
67+
<motion.img
68+
variants={itemVariants}
4369
src={logo}
4470
alt={`${title} Logo`}
4571
style={{ width: '120px', height: '120px', marginBottom: '1.5rem' }}
4672
/>
47-
<Heading as="h1" className="hero__title" style={{ fontSize: '3rem' }}>
48-
{title}
49-
</Heading>
50-
<p className="hero__subtitle" style={{ fontSize: '1.4rem', marginBottom: '2rem' }}>
73+
<motion.div variants={itemVariants}>
74+
<Heading as="h1" className="hero__title" style={{ fontSize: '3rem' }}>
75+
{title}
76+
</Heading>
77+
</motion.div>
78+
<motion.p variants={itemVariants} className="hero__subtitle" style={{ fontSize: '1.4rem', marginBottom: '2rem' }}>
5179
{tagline}
52-
</p>
53-
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'center', flexWrap: 'wrap' }}>
80+
</motion.p>
81+
<motion.div variants={itemVariants} style={{ display: 'flex', gap: '1rem', justifyContent: 'center', flexWrap: 'wrap' }}>
5482
{actions.map((action, idx) => {
5583
const isExternal = action.link.startsWith('http');
5684
if (isExternal) {
@@ -76,21 +104,23 @@ export function ProductLandingPage({
76104
</Link>
77105
);
78106
})}
79-
</div>
107+
</motion.div>
80108
</div>
81109
</header>
82110

83111
<main style={{ padding: '4rem 0' }}>
84112
<div className="container">
85113
<CardGrid>
86114
{features.map((feature, idx) => (
87-
<Card key={idx} title={feature.title} emoji={feature.emoji}>
115+
<Card key={idx} title={feature.title} emoji={feature.emoji} variants={itemVariants}>
88116
{feature.description}
89117
</Card>
90118
))}
91119
</CardGrid>
92120
</div>
93121
</main>
122+
</motion.div>
123+
</AnimatePresence>
94124
</Layout>
95125
);
96126
}

src/pages/index.tsx

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,36 @@ import Link from '@docusaurus/Link';
22
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
33
import Layout from '@theme/Layout';
44
import Heading from '@theme/Heading';
5+
import { motion, AnimatePresence } from 'framer-motion';
6+
import { useLocation } from '@docusaurus/router';
7+
8+
const containerVariants = {
9+
hidden: { opacity: 0 },
10+
show: {
11+
opacity: 1,
12+
transition: { staggerChildren: 0.1 }
13+
}
14+
};
15+
16+
const itemVariants = {
17+
hidden: { opacity: 0, y: 20 },
18+
show: { opacity: 1, y: 0, transition: { type: 'spring' as const, stiffness: 300, damping: 24 } }
19+
};
520

621
function HomepageHeader() {
722
return (
823
<header className="hero hero--primary" style={{ padding: '4rem 0', textAlign: 'center' }}>
924
<div className="container">
10-
<Heading as="h1" className="hero__title">
11-
Topper Wiki Portal
12-
</Heading>
13-
<p className="hero__subtitle">
14-
Welcome to the documentation hub for Topper and its ecosystem plugins.
15-
</p>
25+
<motion.div variants={itemVariants}>
26+
<Heading as="h1" className="hero__title">
27+
Topper Wiki Portal
28+
</Heading>
29+
</motion.div>
30+
<motion.div variants={itemVariants}>
31+
<p className="hero__subtitle">
32+
Welcome to the documentation hub for Topper and its ecosystem plugins.
33+
</p>
34+
</motion.div>
1635
</div>
1736
</header>
1837
);
@@ -54,7 +73,7 @@ const PortalList: PortalItem[] = [
5473

5574
function PortalCard({ title, emoji, description, link }: PortalItem) {
5675
return (
57-
<div className="col col--6" style={{ marginBottom: '2rem' }}>
76+
<motion.div variants={itemVariants} className="col col--6" style={{ marginBottom: '2rem' }}>
5877
<div className="card" style={{ height: '100%', border: '1px solid var(--ifm-color-emphasis-300)', display: 'flex', flexDirection: 'column' }}>
5978
<div className="card__header" style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', padding: '1.25rem' }}>
6079
<span style={{ fontSize: '2.5rem' }}>{emoji}</span>
@@ -69,28 +88,40 @@ function PortalCard({ title, emoji, description, link }: PortalItem) {
6988
</div>
7089
</div>
7190
</div>
72-
</div>
91+
</motion.div>
7392
);
7493
}
7594

7695
export default function Home(): React.JSX.Element {
96+
const location = useLocation();
97+
7798
return (
7899
<Layout
79100
title="Welcome to Topper Wiki Portal"
80101
description="Gateway to Topper, TimedTopper, GroupTopper, and Cachy plugin documentations."
81102
>
82-
<HomepageHeader />
83-
<main>
84-
<section style={{ padding: '3rem 0' }}>
85-
<div className="container">
86-
<div className="row">
87-
{PortalList.map((props, idx) => (
88-
<PortalCard key={idx} {...props} />
89-
))}
90-
</div>
91-
</div>
92-
</section>
93-
</main>
103+
<AnimatePresence mode="wait">
104+
<motion.div
105+
key={location.pathname}
106+
variants={containerVariants}
107+
initial="hidden"
108+
animate="show"
109+
exit={{ opacity: 0, y: -20, transition: { duration: 0.2 } }}
110+
>
111+
<HomepageHeader />
112+
<main>
113+
<section style={{ padding: '3rem 0' }}>
114+
<div className="container">
115+
<div className="row">
116+
{PortalList.map((props, idx) => (
117+
<PortalCard key={idx} {...props} />
118+
))}
119+
</div>
120+
</div>
121+
</section>
122+
</main>
123+
</motion.div>
124+
</AnimatePresence>
94125
</Layout>
95126
);
96127
}

src/theme/DocItem/Layout/index.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
import Layout from '@theme-original/DocItem/Layout';
3+
import type LayoutType from '@theme/DocItem/Layout';
4+
import type {WrapperProps} from '@docusaurus/types';
5+
import { motion, AnimatePresence } from 'framer-motion';
6+
import { useLocation } from '@docusaurus/router';
7+
8+
type Props = WrapperProps<typeof LayoutType>;
9+
10+
export default function DocItemLayoutWrapper(props: Props): JSX.Element {
11+
const location = useLocation();
12+
13+
return (
14+
<AnimatePresence mode="wait">
15+
<motion.div
16+
key={location.pathname}
17+
initial={{ opacity: 0, scale: 0.97, filter: 'blur(5px)' }}
18+
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
19+
exit={{ opacity: 0, scale: 0.97, filter: 'blur(5px)' }}
20+
transition={{ duration: 0.3, ease: 'easeOut' }}
21+
>
22+
<Layout {...props} />
23+
</motion.div>
24+
</AnimatePresence>
25+
);
26+
}

0 commit comments

Comments
 (0)