Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<img width="100%" height="auto" alt="react-native-ease by App & Flow" src="https://github.com/user-attachments/assets/8006ed51-d373-4c97-9e80-9937eb9a569e" />
<img width="100%" height="auto" alt="Native Animations with React Native Ease, maintained by App & Flow" src="docs/static/img/github-banner.png" />

Lightweight declarative animations powered by platform APIs. Uses Core Animation on iOS and Animator on Android — zero JS overhead.

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ title: Getting started
slug: /
---

![react-native-ease hero](https://github.com/user-attachments/assets/8006ed51-d373-4c97-9e80-9937eb9a569e)
![Native Animations with React Native Ease](/img/github-banner.png)

Lightweight declarative animations powered by platform APIs. Uses Core Animation on iOS and Animator on Android — zero JS overhead.

Expand Down
6 changes: 3 additions & 3 deletions docs/docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type * as Preset from '@docusaurus/preset-classic';
const config: Config = {
title: 'React Native Ease',
tagline: 'Lightweight declarative animations powered by platform APIs',
favicon: 'img/favicon.png',
favicon: 'img/app-icon.png',
url: 'https://appandflow.github.io',
baseUrl: '/react-native-ease/',
organizationName: 'AppAndFlow',
Expand All @@ -21,7 +21,7 @@ const config: Config = {
'classic',
{
docs: {
routeBasePath: '/',
routeBasePath: '/docs',
sidebarPath: './sidebars.ts',
editUrl:
'https://github.com/AppAndFlow/react-native-ease/tree/main/docs/',
Expand All @@ -33,7 +33,7 @@ const config: Config = {
],
],
themeConfig: {
image: 'img/opengraph-image.png',
image: 'img/github-banner.png',
colorMode: {
defaultMode: 'light',
disableSwitch: false,
Expand Down
66 changes: 66 additions & 0 deletions docs/src/components/landing/CopyInstallCommand.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useEffect, useRef, useState } from 'react';
import styles from '../../css/landing.module.css';
import Illustration from './Illustration';

export default function CopyInstallCommand({
command,
}: {
command: string;
}): React.ReactElement {
const [status, setStatus] = useState('');
const copied = status === 'Copied';
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect(() => () => clearTimeout(timer.current), []);

async function copy() {
clearTimeout(timer.current);
try {
await navigator.clipboard.writeText(command);
setStatus('Copied');
timer.current = setTimeout(() => setStatus(''), 2000);
} catch {
setStatus('Select the command and copy it manually.');
}
}

return (
<div className={styles.install} data-copied={copied}>
<div className={styles.installRow}>
<code className={styles.command}>{command}</code>
<button
type="button"
className={styles.copyButton}
onClick={copy}
aria-label={copied ? 'Copied' : 'Copy install command'}
title={copied ? 'Copied' : 'Copy install command'}
>
<span aria-hidden="true" className={styles.copyIcons}>
<Illustration
file="copy.svg"
width={12}
height={12}
className={styles.copyIcon}
/>
<svg
viewBox="0 0 24 24"
width={16}
height={16}
className={styles.checkIcon}
>
<path
fill="currentColor"
d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"
/>
</svg>
</span>
</button>
</div>
<output
aria-live="polite"
className={copied ? styles.srOnly : styles.copyStatus}
>
{status}
</output>
</div>
);
}
26 changes: 26 additions & 0 deletions docs/src/components/landing/FeatureList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import styles from '../../css/landing.module.css';
import Link from '@docusaurus/Link';
import Illustration from './Illustration';

import { features } from '../../landing/content';

export default function FeatureList(): React.ReactElement {
return (
<section aria-label="Features" className={styles.features}>
<div className={styles.featureGrid}>
{features.map(({ title, description, to, icon }) => (
<article key={title}>
<h2 className={styles.featureTitle}>
<Link to={to} className={styles.featureLink}>
<Illustration file={icon} width={24} height={24} />
<span>{title}</span>
</Link>
</h2>
<p className={styles.featureDescription}>{description}</p>
</article>
))}
</div>
</section>
);
}
20 changes: 20 additions & 0 deletions docs/src/components/landing/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import styles from '../../css/landing.module.css';
import Link from '@docusaurus/Link';
import { github } from '../../landing/content';
export default function Footer(): React.ReactElement {
return (
<footer className={styles.footer}>
<span>
Made by{' '}
<Link href="https://appandflow.com" className={styles.attributionLink}>
App&amp;Flow
</Link>
</span>
<span aria-hidden="true">·</span>
<Link href={`${github}/blob/main/LICENSE`} className={styles.licenseLink}>
licensed under the MIT License
</Link>
</footer>
);
}
19 changes: 19 additions & 0 deletions docs/src/components/landing/Hero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import styles from '../../css/landing.module.css';
import Link from '@docusaurus/Link';
import CopyInstallCommand from './CopyInstallCommand';
import { pageTitle, blurb, command } from '../../landing/content';
export default function Hero(): React.ReactElement {
return (
<header className={styles.hero}>
<h1 className={styles.title}>{pageTitle}</h1>
<p className={styles.blurb}>{blurb}</p>
<div className={styles.heroActions}>
<Link to="/docs/" className={styles.getStarted}>
Get started
</Link>
<CopyInstallCommand command={command} />
</div>
</header>
);
}
10 changes: 10 additions & 0 deletions docs/src/components/landing/Illustration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import useBaseUrl from '@docusaurus/useBaseUrl';

export default function Illustration({
file,
alt = '',
...props
}: Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src'> & { file: string }) {
return <img src={useBaseUrl(`/img/landing/${file}`)} alt={alt} {...props} />;
}
32 changes: 32 additions & 0 deletions docs/src/components/landing/LandingLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import styles from '../../css/landing.module.css';
import LayoutProvider from '@theme/Layout/Provider';
import SkipToContent from '@theme/SkipToContent';
import {
PageMetadata,
SkipToContentFallbackId,
} from '@docusaurus/theme-common';
import Navigation from './Navigation';
import Footer from './Footer';
import { pageTitle, blurb } from '../../landing/content';

export default function LandingLayout({
children,
}: {
children: React.ReactNode;
}): React.ReactElement {
return (
<LayoutProvider>
<PageMetadata title={pageTitle} description={blurb} />
<SkipToContent />
<div className={styles.page}>
<div aria-hidden="true" className={styles.pageGuide} />
<Navigation />
<main id={SkipToContentFallbackId} className={styles.grid}>
{children}
</main>
<Footer />
</div>
</LayoutProvider>
);
}
37 changes: 37 additions & 0 deletions docs/src/components/landing/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import styles from '../../css/landing.module.css';
import Link from '@docusaurus/Link';
import Illustration from './Illustration';
import { github, productName } from '../../landing/content';
import packageJson from '../../../../package.json';

export default function Navigation(): React.ReactElement {
return (
<nav aria-label="Main navigation" className={styles.navigation}>
<Link
href="https://appandflow.com"
aria-label="App and Flow"
className={styles.mark}
>
<Illustration file="mark.svg" width={24} height={24} />
</Link>
<Link to="/" className={styles.productName}>
{productName}
</Link>
<Link
href={github}
aria-label={`GitHub repository, version ${packageJson.version}`}
className={styles.githubLink}
>
<Illustration file="github.svg" width={16} height={16} />
<span aria-hidden="true">·</span> v{packageJson.version}
</Link>
<Link to="/docs/" className={styles.navLink}>
Documentation
</Link>
<Link to="/docs/api-reference" className={styles.navLink}>
API
</Link>
</nav>
);
}
114 changes: 114 additions & 0 deletions docs/src/components/landing/PhoneIllustration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import styles from '../../css/landing.module.css';
import Illustration from './Illustration';
import RNLogo from './RNLogo';
import TopIsland from './TopIsland';
type IntroPhase = 'logo' | 'island' | 'numbers' | 'sheet' | 'done';

export default function PhoneIllustration(): React.ReactElement {
const [sheetLoaded, setSheetLoaded] = useState(false);
const sheetContainer = useRef<HTMLDivElement>(null);
const [introPhase, setIntroPhase] = useState<IntroPhase>('logo');
const [introStarted, setIntroStarted] = useState(false);
const phoneContainer = useRef<HTMLElement>(null);
const showSheet =
sheetLoaded &&
(introPhase === 'numbers' ||
introPhase === 'sheet' ||
introPhase === 'done');
const handleLogoEntranceComplete = useCallback(() => {
setIntroPhase((phase) => (phase === 'logo' ? 'island' : phase));
}, []);
const handleIslandExpansionComplete = useCallback(() => {
setIntroPhase((phase) => (phase === 'island' ? 'numbers' : phase));
}, []);
const handleIslandCountComplete = useCallback(() => {
setIntroPhase((phase) => (phase === 'numbers' ? 'sheet' : phase));
}, []);
useEffect(() => {
if (introStarted) return;
const phone = phoneContainer.current;
if (!phone) return;
const desktop = window.matchMedia('(min-width: 801px)');
const observer = new IntersectionObserver(
([entry]) => {
if (entry && entry.intersectionRatio >= 0.5) {
setIntroStarted(true);
observer.disconnect();
}
},
{ threshold: 0.5 },
);
const updateTrigger = () => {
if (desktop.matches) {
setIntroStarted(true);
observer.disconnect();
} else {
observer.observe(phone);
}
};
updateTrigger();
desktop.addEventListener('change', updateTrigger);
return () => {
observer.disconnect();
desktop.removeEventListener('change', updateTrigger);
};
}, [introStarted]);
useEffect(() => {
const motion = window.matchMedia('(prefers-reduced-motion: reduce)');
const skipIntro = () => {
if (motion.matches) setIntroPhase('done');
};
skipIntro();
motion.addEventListener('change', skipIntro);
return () => motion.removeEventListener('change', skipIntro);
}, []);
useEffect(() => {
// Cached images can finish loading before React attaches the load handler.
const image = sheetContainer.current?.querySelector('img');
if (image?.complete && image.naturalWidth > 0) {
setSheetLoaded(true);
}
}, []);
return (
<figure ref={phoneContainer} className={styles.phone}>
<Illustration
file="phone.png"
alt="Phone outline"
width={336}
height={780}
className={styles.responsiveImage}
/>
<TopIsland
active={introPhase !== 'logo'}
onExpansionComplete={handleIslandExpansionComplete}
onCountComplete={handleIslandCountComplete}
/>
<RNLogo
active={introStarted || introPhase === 'done'}
onEntranceComplete={handleLogoEntranceComplete}
/>
<div ref={sheetContainer} className={styles.sheetClip}>
<Illustration
file="bottom-sheet.png"
alt="Ease versus Reanimated latency comparison"
width={894}
height={723}
onLoad={() => setSheetLoaded(true)}
className={[
styles.bottomSheet,
showSheet ? styles.bottomSheetEnter : undefined,
]
.filter(Boolean)
.join(' ')}
onAnimationEnd={(event) => {
if (event.target === event.currentTarget) {
setIntroPhase((phase) => (phase === 'sheet' ? 'done' : phase));
}
}}
data-visible={showSheet}
/>
</div>
</figure>
);
}
Loading
Loading