From 4838df315931bb883f704ec3e1abe2685f296cdf Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Sat, 22 Apr 2023 19:52:26 +0800 Subject: 😀 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/Authors.tsx | 22 + docs/components/Avatar.tsx | 37 ++ docs/components/Badge.tsx | 24 + docs/components/Callout.tsx | 55 ++ docs/components/Container.tsx | 9 + docs/components/ExamplesArea.tsx | 30 ++ docs/components/ExtraContent.tsx | 10 + docs/components/Feature.tsx | 88 ++++ docs/components/Features.tsx | 46 ++ docs/components/Footer.tsx | 252 ++++++++++ docs/components/FullTurboCTA.tsx | 41 ++ docs/components/HeaderLogo.tsx | 36 ++ docs/components/Icons.tsx | 165 ++++++ docs/components/Logo.tsx | 119 +++++ docs/components/LogoContext/icons.tsx | 228 +++++++++ docs/components/LogoContext/index.tsx | 169 +++++++ docs/components/LogoContext/items.tsx | 91 ++++ docs/components/LogoContext/types.ts | 25 + docs/components/MonorepoHandbook.tsx | 142 ++++++ docs/components/Navigation.tsx | 47 ++ docs/components/QuickStart.tsx | 89 ++++ docs/components/RemoteCacheCounter.tsx | 47 ++ docs/components/SiteSwitcher.tsx | 66 +++ docs/components/Social.tsx | 32 ++ docs/components/Tabs.tsx | 37 ++ docs/components/TurbopackFeatures.tsx | 106 ++++ docs/components/TurbopackQuickstart.tsx | 28 ++ docs/components/Tweet.tsx | 46 ++ docs/components/blog/Date.tsx | 21 + docs/components/clients/Clients.tsx | 61 +++ docs/components/clients/Filters.tsx | 42 ++ docs/components/clients/Logo.tsx | 67 +++ docs/components/clients/Marquee.tsx | 14 + docs/components/clients/users.ts | 556 +++++++++++++++++++++ docs/components/counters.module.css | 6 - docs/components/counters.tsx | 24 - docs/components/header-logo.module.css | 20 + docs/components/image/ImageFigure.tsx | 40 ++ docs/components/image/ThemedImage.tsx | 45 ++ docs/components/image/ThemedImageFigure.tsx | 47 ++ docs/components/logos/PackLogo.tsx | 16 + docs/components/logos/RepoLogo.tsx | 16 + docs/components/logos/Turbo.tsx | 62 +++ docs/components/logos/TurboAnimated.tsx | 157 ++++++ docs/components/logos/Vercel.tsx | 11 + docs/components/logos/og/PackLogo.tsx | 72 +++ docs/components/logos/og/RepoLogo.tsx | 70 +++ docs/components/logos/og/TurboLogo.tsx | 54 ++ docs/components/logos/og/VercelLogo.tsx | 16 + docs/components/output-mode-table.mdx | 7 + docs/components/pages/confirm.tsx | 36 ++ docs/components/pages/home-shared/CTAButton.tsx | 43 ++ docs/components/pages/home-shared/FadeIn.tsx | 50 ++ docs/components/pages/home-shared/FeatureBox.tsx | 42 ++ .../components/pages/home-shared/FeaturesBento.tsx | 38 ++ docs/components/pages/home-shared/GlobalStyles.tsx | 20 + docs/components/pages/home-shared/Gradient.tsx | 47 ++ .../pages/home-shared/GradientSectionBorder.tsx | 37 ++ docs/components/pages/home-shared/Headings.tsx | 56 +++ .../pages/home-shared/gradients.module.css | 231 +++++++++ .../pages/landing/TurboHeroBackground.tsx | 33 ++ docs/components/pages/landing/Turbopack.tsx | 27 + docs/components/pages/landing/Turborepo.tsx | 27 + docs/components/pages/landing/index.module.css | 184 +++++++ docs/components/pages/landing/index.tsx | 199 ++++++++ .../pages/landing/turbohero-background.module.css | 108 ++++ .../pages/pack-home/DocsBenchmarkStat.tsx | 53 ++ .../pages/pack-home/DocsBenchmarksGraph.tsx | 31 ++ .../pages/pack-home/PackBenchmarkTabs.tsx | 149 ++++++ docs/components/pages/pack-home/PackBenchmarks.tsx | 97 ++++ .../pages/pack-home/PackBenchmarksGraph.tsx | 333 ++++++++++++ .../pages/pack-home/PackBenchmarksPicker.tsx | 20 + docs/components/pages/pack-home/PackDropdown.tsx | 117 +++++ docs/components/pages/pack-home/PackFeatures.tsx | 12 + docs/components/pages/pack-home/PackHero.tsx | 115 +++++ docs/components/pages/pack-home/PackLetter.tsx | 114 +++++ .../pages/pack-home/benchmark-data/README.md | 7 + .../pages/pack-home/benchmark-data/data.json | 54 ++ docs/components/pages/pack-home/index.tsx | 24 + docs/components/pages/repo-home/RepoFeatures.tsx | 15 + docs/components/pages/repo-home/RepoHero.tsx | 114 +++++ docs/components/pages/repo-home/RepoLetter.tsx | 118 +++++ docs/components/pages/repo-home/index.tsx | 22 + docs/components/pages/showcase.tsx | 47 ++ docs/components/useIsomorphicLayoutEffect.tsx | 7 + docs/components/usePrefersReducedMotion.tsx | 44 ++ 86 files changed, 6152 insertions(+), 30 deletions(-) create mode 100644 docs/components/Authors.tsx create mode 100644 docs/components/Avatar.tsx create mode 100644 docs/components/Badge.tsx create mode 100644 docs/components/Callout.tsx create mode 100644 docs/components/Container.tsx create mode 100644 docs/components/ExamplesArea.tsx create mode 100644 docs/components/ExtraContent.tsx create mode 100644 docs/components/Feature.tsx create mode 100644 docs/components/Features.tsx create mode 100644 docs/components/Footer.tsx create mode 100644 docs/components/FullTurboCTA.tsx create mode 100644 docs/components/HeaderLogo.tsx create mode 100644 docs/components/Icons.tsx create mode 100644 docs/components/Logo.tsx create mode 100644 docs/components/LogoContext/icons.tsx create mode 100644 docs/components/LogoContext/index.tsx create mode 100644 docs/components/LogoContext/items.tsx create mode 100644 docs/components/LogoContext/types.ts create mode 100644 docs/components/MonorepoHandbook.tsx create mode 100644 docs/components/Navigation.tsx create mode 100644 docs/components/QuickStart.tsx create mode 100644 docs/components/RemoteCacheCounter.tsx create mode 100644 docs/components/SiteSwitcher.tsx create mode 100644 docs/components/Social.tsx create mode 100644 docs/components/Tabs.tsx create mode 100644 docs/components/TurbopackFeatures.tsx create mode 100644 docs/components/TurbopackQuickstart.tsx create mode 100644 docs/components/Tweet.tsx create mode 100644 docs/components/blog/Date.tsx create mode 100644 docs/components/clients/Clients.tsx create mode 100644 docs/components/clients/Filters.tsx create mode 100644 docs/components/clients/Logo.tsx create mode 100644 docs/components/clients/Marquee.tsx create mode 100644 docs/components/clients/users.ts delete mode 100644 docs/components/counters.module.css delete mode 100644 docs/components/counters.tsx create mode 100644 docs/components/header-logo.module.css create mode 100644 docs/components/image/ImageFigure.tsx create mode 100644 docs/components/image/ThemedImage.tsx create mode 100644 docs/components/image/ThemedImageFigure.tsx create mode 100644 docs/components/logos/PackLogo.tsx create mode 100644 docs/components/logos/RepoLogo.tsx create mode 100644 docs/components/logos/Turbo.tsx create mode 100644 docs/components/logos/TurboAnimated.tsx create mode 100644 docs/components/logos/Vercel.tsx create mode 100644 docs/components/logos/og/PackLogo.tsx create mode 100644 docs/components/logos/og/RepoLogo.tsx create mode 100644 docs/components/logos/og/TurboLogo.tsx create mode 100644 docs/components/logos/og/VercelLogo.tsx create mode 100644 docs/components/output-mode-table.mdx create mode 100644 docs/components/pages/confirm.tsx create mode 100644 docs/components/pages/home-shared/CTAButton.tsx create mode 100644 docs/components/pages/home-shared/FadeIn.tsx create mode 100644 docs/components/pages/home-shared/FeatureBox.tsx create mode 100644 docs/components/pages/home-shared/FeaturesBento.tsx create mode 100644 docs/components/pages/home-shared/GlobalStyles.tsx create mode 100644 docs/components/pages/home-shared/Gradient.tsx create mode 100644 docs/components/pages/home-shared/GradientSectionBorder.tsx create mode 100644 docs/components/pages/home-shared/Headings.tsx create mode 100644 docs/components/pages/home-shared/gradients.module.css create mode 100644 docs/components/pages/landing/TurboHeroBackground.tsx create mode 100644 docs/components/pages/landing/Turbopack.tsx create mode 100644 docs/components/pages/landing/Turborepo.tsx create mode 100644 docs/components/pages/landing/index.module.css create mode 100644 docs/components/pages/landing/index.tsx create mode 100644 docs/components/pages/landing/turbohero-background.module.css create mode 100644 docs/components/pages/pack-home/DocsBenchmarkStat.tsx create mode 100644 docs/components/pages/pack-home/DocsBenchmarksGraph.tsx create mode 100644 docs/components/pages/pack-home/PackBenchmarkTabs.tsx create mode 100644 docs/components/pages/pack-home/PackBenchmarks.tsx create mode 100644 docs/components/pages/pack-home/PackBenchmarksGraph.tsx create mode 100644 docs/components/pages/pack-home/PackBenchmarksPicker.tsx create mode 100644 docs/components/pages/pack-home/PackDropdown.tsx create mode 100644 docs/components/pages/pack-home/PackFeatures.tsx create mode 100644 docs/components/pages/pack-home/PackHero.tsx create mode 100644 docs/components/pages/pack-home/PackLetter.tsx create mode 100644 docs/components/pages/pack-home/benchmark-data/README.md create mode 100644 docs/components/pages/pack-home/benchmark-data/data.json create mode 100644 docs/components/pages/pack-home/index.tsx create mode 100644 docs/components/pages/repo-home/RepoFeatures.tsx create mode 100644 docs/components/pages/repo-home/RepoHero.tsx create mode 100644 docs/components/pages/repo-home/RepoLetter.tsx create mode 100644 docs/components/pages/repo-home/index.tsx create mode 100644 docs/components/pages/showcase.tsx create mode 100644 docs/components/useIsomorphicLayoutEffect.tsx create mode 100644 docs/components/usePrefersReducedMotion.tsx (limited to 'docs/components') diff --git a/docs/components/Authors.tsx b/docs/components/Authors.tsx new file mode 100644 index 0000000..634bd90 --- /dev/null +++ b/docs/components/Authors.tsx @@ -0,0 +1,22 @@ +import { Avatar } from "./Avatar"; +import cn from "classnames"; +import TURBO_TEAM from "../content/team"; +import type { Author } from "../content/team"; + +export function Authors({ authors }: { authors: Array }) { + const validAuthors = authors.filter((author) => TURBO_TEAM[author]); + return ( +
+
4 && "max-w-3xl" + )} + > + {validAuthors.map((username) => ( + + ))} +
+
+ ); +} diff --git a/docs/components/Avatar.tsx b/docs/components/Avatar.tsx new file mode 100644 index 0000000..8826daf --- /dev/null +++ b/docs/components/Avatar.tsx @@ -0,0 +1,37 @@ +import Image from "next/image"; +import type { AuthorDetails } from "../content/team"; + +export const Avatar = ({ name, picture, twitterUsername }: AuthorDetails) => ( +
+
+ {name} +
+
+
Name
+
{name}
+ {twitterUsername && ( + <> +
Twitter
+
+ + {`@${twitterUsername}`} + +
+ + )} +
+
+); diff --git a/docs/components/Badge.tsx b/docs/components/Badge.tsx new file mode 100644 index 0000000..45e5a0c --- /dev/null +++ b/docs/components/Badge.tsx @@ -0,0 +1,24 @@ +import cn from "classnames"; + +import type { ReactNode } from "react"; + +export type BadgeProps = React.ComponentProps<"span"> & { + children: ReactNode; + className?: string; +}; + +export default function Badge(props: BadgeProps) { + const { children, className, ...rest } = props; + + return ( + + {children} + + ); +} diff --git a/docs/components/Callout.tsx b/docs/components/Callout.tsx new file mode 100644 index 0000000..d284bae --- /dev/null +++ b/docs/components/Callout.tsx @@ -0,0 +1,55 @@ +import React, { ReactElement, ReactNode } from "react"; +import { + LightBulbIcon, + ExclamationIcon, + ExclamationCircleIcon, + InformationCircleIcon, +} from "@heroicons/react/solid"; + +const THEMES = { + info: { + classes: + "bg-blue-100 text-blue-800 dark:text-blue-300 dark:bg-blue-200 dark:bg-opacity-10", + icon: , + }, + idea: { + classes: + "bg-gray-100 text-gray-800 dark:text-gray-300 dark:bg-gray-200 dark:bg-opacity-10", + icon: , + }, + error: { + classes: + "bg-red-200 text-red-900 dark:text-red-200 dark:bg-red-600 dark:bg-opacity-30", + icon: , + }, + default: { + classes: + "bg-orange-100 text-orange-800 dark:text-orange-300 dark:bg-orange-200 dark:bg-opacity-10", + icon: , + }, +}; + +export default function Callout({ + children, + type = "default", + icon, +}: { + children: ReactNode; + type: keyof typeof THEMES; + icon?: ReactElement; +}) { + return ( +
+
+ {icon || THEMES[type].icon} +
+
{children}
+
+ ); +} diff --git a/docs/components/Container.tsx b/docs/components/Container.tsx new file mode 100644 index 0000000..a6db4d8 --- /dev/null +++ b/docs/components/Container.tsx @@ -0,0 +1,9 @@ +import type { ReactNode } from "react"; + +type Props = { + children?: ReactNode; +}; + +export const Container = ({ children }: Props) => { + return
{children}
; +}; diff --git a/docs/components/ExamplesArea.tsx b/docs/components/ExamplesArea.tsx new file mode 100644 index 0000000..8b38263 --- /dev/null +++ b/docs/components/ExamplesArea.tsx @@ -0,0 +1,30 @@ +import { useSSG } from "nextra/ssg"; +import { DetailedFeatureLink } from "./Feature"; +import { GitHubIcon } from "./Icons"; + +export const ExamplesArea = ({ + filter = "featured", +}: { + filter: "featured" | "all"; +}) => { + const { examples } = useSSG(); + + return ( +
+ {examples + .filter(({ featured }) => (filter === "featured" ? featured : true)) + .map(({ name, description, slug }) => ( + + ))} +
+ ); +}; diff --git a/docs/components/ExtraContent.tsx b/docs/components/ExtraContent.tsx new file mode 100644 index 0000000..f489e36 --- /dev/null +++ b/docs/components/ExtraContent.tsx @@ -0,0 +1,10 @@ +import RemoteCacheCounter from "./RemoteCacheCounter"; +import { useTurboSite } from "./SiteSwitcher"; + +export default function ExtraContent() { + const site = useTurboSite(); + + if (site === "repo") { + return ; + } +} diff --git a/docs/components/Feature.tsx b/docs/components/Feature.tsx new file mode 100644 index 0000000..1a8a8b2 --- /dev/null +++ b/docs/components/Feature.tsx @@ -0,0 +1,88 @@ +import classNames from "classnames"; +import Link from "next/link"; +import type { Feature } from "../content/legacy-features"; + +type FeatureProps = { + feature: Omit; + // include feature description + detailed?: boolean; +}; + +const DetailedFeatureInner = (props: { feature: FeatureProps["feature"] }) => { + const { Icon, name, description } = props.feature; + return ( + <> +
+
+
+

+ {name} +

+
+
+

+ {description} +

+
+ + + ); +}; + +const featureWrapperClasses = `relative block overflow-hidden p-10 bg-white shadow-lg rounded-xl dark:bg-opacity-5 no-underline text-black dark:text-white`; + +export const DetailedFeatureLink = (props: { + href: string; + feature: FeatureProps["feature"]; + target?: string; +}) => { + const { href, feature, ...rest } = props; + return ( + + + + ); +}; + +export default function Feature(props: FeatureProps) { + const { feature, detailed = false } = props; + const { Icon, name } = feature; + + if (detailed) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+
+
{name}
+
+
+ ); +} diff --git a/docs/components/Features.tsx b/docs/components/Features.tsx new file mode 100644 index 0000000..c8a6eed --- /dev/null +++ b/docs/components/Features.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { + LEGACY_REPO_DOCS_FEATURES, + LEGACY_REPO_HOME_FEATURES, +} from "../content/legacy-features"; +import Feature from "./Feature"; + +export function HomeFeatures() { + return ( + + {LEGACY_REPO_HOME_FEATURES.map((feature) => ( + + ))} + + ); +} + +export function DocsFeatures({ detailed = true }: { detailed?: boolean }) { + return ( +
+ {LEGACY_REPO_DOCS_FEATURES.map((feature) => ( + + ))} +
+ ); +} + +export function DetailedFeaturesGrid({ + children, +}: { + children?: React.ReactNode; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/docs/components/Footer.tsx b/docs/components/Footer.tsx new file mode 100644 index 0000000..e27c848 --- /dev/null +++ b/docs/components/Footer.tsx @@ -0,0 +1,252 @@ +import { useRouter } from "next/router"; +import Link from "next/link"; +import { useState, ReactNode, ReactElement } from "react"; +import cn from "classnames"; +import { ThemeSwitch } from "nextra-theme-docs"; +import VercelLogo from "./logos/Vercel"; +import { useTurboSite, TurboSite } from "./SiteSwitcher"; + +function FooterLink({ href, children }: { href: string; children: ReactNode }) { + const classes = + "text-sm text-[#666666] dark:text-[#888888] no-underline betterhover:hover:text-gray-700 betterhover:hover:dark:text-white transition"; + if (href.startsWith("http")) { + return ( + + {children} + + ); + } + return ( + + {children} + + ); +} + +function FooterHeader({ children }: { children: ReactNode }) { + return

{children}

; +} + +const navigation = { + general: [ + { name: "Blog", href: "/blog" }, + { name: "Releases", href: "https://github.com/vercel/turbo/releases" }, + ], + repo: [ + { name: "Documentation", href: "/repo/docs" }, + { + name: "API Reference", + href: "/repo/docs/reference/command-line-reference", + }, + { name: "FAQ", href: "/repo/docs/faq" }, + ], + pack: [ + { name: "Documentation", href: "/pack/docs" }, + { name: "Features", href: "/pack/docs/features" }, + ], + support: [ + { + name: "GitHub", + href: "https://github.com/vercel/turbo", + }, + { + name: "Discord", + href: "https://turbo.build/discord", + }, + ], + company: (site: TurboSite) => [ + { name: "Vercel", href: "https://vercel.com" }, + { + name: "Open Source Software", + href: "https://vercel.com/oss?utm_source=turbo.build&utm_medium=referral&utm_campaign=footer-ossLink", + }, + { + name: "Contact Sales", + href: `https://vercel.com/${ + site === "repo" ? "solutions/turborepo" : "contact/sales" + }?utm_source=turbo.build&utm_medium=referral&utm_campaign=footer-enterpriseLink`, + }, + { name: "Twitter", href: "https://twitter.com/vercel" }, + ], + legal: [ + { name: "Privacy Policy", href: "/privacy" }, + { name: "Terms of Service", href: "/terms" }, + ], +}; + +export function FooterContent() { + const site = useTurboSite(); + return ( +
+ +
+
+
+
+
+ Resources +
    + {navigation.general.map((item) => ( +
  • + {item.name} +
  • + ))} +
+
+
+ Turborepo +
    + {navigation.repo.map((item) => ( +
  • + {item.name} +
  • + ))} +
+
+
+ Turbopack +
    + {navigation.pack.map((item) => ( +
  • + {item.name} +
  • + ))} +
+
+
+ Company +
    + {navigation.company(site).map((item) => ( +
  • + {item.name} +
  • + ))} +
+
+
+ Legal +
    + {navigation.legal.map((item) => ( +
  • + {item.name} +
  • + ))} +
+
+
+ Support +
    + {navigation.support.map((item) => ( +
  • + {item.name} +
  • + ))} +
+
+
+
+
+ Subscribe to our newsletter +

+ Subscribe to the Turbo newsletter and stay updated on new releases + and features, guides, and case studies. +

+ +
+
+ +
+
+ + + +

+ © {new Date().getFullYear()} Vercel, Inc. All rights + reserved. +

+
+
+
+
+ ); +} + +function SubmitForm() { + const [email, setEmail] = useState(""); + const router = useRouter(); + return ( +
{ + fetch("/api/signup", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email }), + }) + .then((res) => res.json()) + .then((res) => { + return router.push("/confirm"); + }); + e.preventDefault(); + }} + > + + setEmail(e.target.value)} + className="border-[#666666] dark:border-[#888888] w-full min-w-0 px-4 py-2 text-base text-gray-900 placeholder-gray-500 bg-white border rounded-md appearance-none dark:text-white sm:text-sm dark:bg-transparent focus:outline-none focus:ring-2 focus:ring-gray-800 dark:focus:border-white focus:placeholder-gray-400" + placeholder="you@example.com" + /> +
+ +
+
+ ); +} + +export function Footer({ menu }: { menu?: boolean }): ReactElement { + return ( +
+
+
+ +
+
+
+ +
+
+ ); +} diff --git a/docs/components/FullTurboCTA.tsx b/docs/components/FullTurboCTA.tsx new file mode 100644 index 0000000..0c3a44b --- /dev/null +++ b/docs/components/FullTurboCTA.tsx @@ -0,0 +1,41 @@ +import { Container } from "./Container"; +import Callout from "./Callout"; +import Link from "next/link"; + +function FullTurboCTA() { + return ( +
+
+

+ Ready to go + {">>>"} FULL TURBO + at your organization? +

+
+ Vercel's Experts can bring your entire team up to speed quickly +
+
+
+ + Talk to an Expert + +
+ +
+ ); +} + +export default FullTurboCTA; diff --git a/docs/components/HeaderLogo.tsx b/docs/components/HeaderLogo.tsx new file mode 100644 index 0000000..a1fc395 --- /dev/null +++ b/docs/components/HeaderLogo.tsx @@ -0,0 +1,36 @@ +import SiteSwitcher from "./SiteSwitcher"; +import Link from "next/link"; +import styles from "./header-logo.module.css"; +import TurboAnimated from "./logos/TurboAnimated"; +import { LogoContext } from "./LogoContext"; + +function HeaderLogo() { + return ( + <> + + + + + + + + +
+ +
+ + ); +} + +export default HeaderLogo; diff --git a/docs/components/Icons.tsx b/docs/components/Icons.tsx new file mode 100644 index 0000000..c203b3d --- /dev/null +++ b/docs/components/Icons.tsx @@ -0,0 +1,165 @@ +import React, { ComponentProps } from "react"; + +export type IconType = ((props: ComponentProps<"svg">) => JSX.Element) & { + requiresFill?: boolean; +}; + +export const TailwindIcon: IconType = (props) => { + return ( + + ); +}; + +export const GitHubIcon: IconType = ({ height = 28, ...props }) => { + return ( + + ); +}; + +export const DockerIcon: IconType = ({ height = 28, ...props }) => { + return ( + + + + + + ); +}; + +export const RectangleGroupIcon: IconType = (props) => { + return ( + + + + ); +}; + +export const FaceSmileIcon: IconType = (props) => { + return ( + + + + ); +}; + +export const RectangleStackIcon: IconType = (props) => { + return ( + + + + ); +}; + +export const JSIcon: IconType = (props) => { + return ( + + + + ); +}; + +JSIcon.requiresFill = true; + +export const TSIcon: IconType = (props) => { + return ( + + + + ); +}; + +TSIcon.requiresFill = true; + +export const CSSIcon: IconType = (props) => { + return ( + + + + ); +}; + +CSSIcon.requiresFill = true; diff --git a/docs/components/Logo.tsx b/docs/components/Logo.tsx new file mode 100644 index 0000000..4c697ea --- /dev/null +++ b/docs/components/Logo.tsx @@ -0,0 +1,119 @@ +export const Logo = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/docs/components/LogoContext/icons.tsx b/docs/components/LogoContext/icons.tsx new file mode 100644 index 0000000..dabefc2 --- /dev/null +++ b/docs/components/LogoContext/icons.tsx @@ -0,0 +1,228 @@ +import classNames from "classnames"; + +export const VercelLogo = ({ className }: { className?: string }) => ( + + + +); + +export const TurborepoLogo = ({ className }: { className?: string }) => ( + + + + + + + + + + +); + +export const TurbopackLogo = ({ className }: { className?: string }) => ( + + + + + + + + + + + + + + + + + + + + +); + +export const NextJSLogo = ({ className }: { className?: string }) => ( + + + + + + + + + + + + + + + + + + + + +); + +export const DesignSystemLogo = ({ className }: { className?: string }) => ( + + + + + + +); + +export const IconType = ({ className }: { className?: string }) => ( + + + + + +); diff --git a/docs/components/LogoContext/index.tsx b/docs/components/LogoContext/index.tsx new file mode 100644 index 0000000..3f03740 --- /dev/null +++ b/docs/components/LogoContext/index.tsx @@ -0,0 +1,169 @@ +import { useEffect, useCallback, useState, useRef } from "react"; +import { useTheme } from "nextra-theme-docs"; +import Link from "next/link"; +import classNames from "classnames"; +import { VercelLogo } from "./icons"; +import { PRODUCT_MENU_ITEMS, PLATFORM_MENU_ITEMS } from "./items"; +import type { MenuItemProps } from "./types"; +import { MouseEvent } from "react"; +import { useTurboSite } from "../SiteSwitcher"; + +function MenuDivider({ children, ...other }: { children: string }) { + return ( +

+ {children} +

+ ); +} + +function MenuItem({ + children, + prefix, + className, + type, + href, + onClick, + closeMenu, + disabled, + ...other +}: MenuItemProps) { + const [copied, setCopied] = useState(false); + + const handleClick = () => { + if (onClick) { + onClick(); + } + if (type === "copy") { + setCopied(true); + } else { + closeMenu(); + } + }; + + useEffect(() => { + if (copied) { + const timeout = setTimeout(() => { + setCopied(false); + closeMenu(); + }, 2000); + return () => clearTimeout(timeout); + } + }, [copied, closeMenu]); + + const classes = classNames( + className, + "group flex items-center px-4 py-2 text-sm dark:hover:bg-gray-800 hover:bg-gray-200 w-full rounded-md" + ); + if (type === "internal") { + return ( + + {prefix} + {children} + + ); + } + if (type === "external") { + return ( + + {prefix} + {children} + + ); + } + + if (type === "copy") { + return ( + + ); + } +} + +export function LogoContext() { + const [open, setOpen] = useState(false); + const site = useTurboSite(); + const menu = useRef(null); + const { theme = "dark" } = useTheme(); + + const toggleMenu = (e: MouseEvent) => { + e.preventDefault(); + if (e.type === "contextmenu") { + setOpen((prev) => !prev); + } else { + setOpen(false); + window.open(`https://vercel.com`, "_blank"); + } + }; + + const onClickOutside: EventListener = useCallback( + (e) => { + if (menu.current && open && !menu.current.contains(e.target)) { + setOpen(false); + } + }, + [open] + ); + + useEffect(() => { + document.addEventListener("click", onClickOutside, true); + return () => { + document.removeEventListener("click", onClickOutside, true); + }; + }, [onClickOutside]); + + return ( +
+ + {open && ( +
+
+ Platform + {PLATFORM_MENU_ITEMS({ theme, site }).map((item) => ( + setOpen(false)} + {...item} + > + {item.children} + + ))} + Products + {PRODUCT_MENU_ITEMS({ theme, site }).map((item) => ( + setOpen(false)} + {...item} + > + {item.children} + + ))} +
+
+ )} +
+ ); +} diff --git a/docs/components/LogoContext/items.tsx b/docs/components/LogoContext/items.tsx new file mode 100644 index 0000000..6ce1fe6 --- /dev/null +++ b/docs/components/LogoContext/items.tsx @@ -0,0 +1,91 @@ +import { + VercelLogo, + TurborepoLogo, + TurbopackLogo, + IconType, + NextJSLogo, + DesignSystemLogo, +} from "./icons"; +import type { ContextItem, ContextList } from "./types"; +import copy from "copy-to-clipboard"; + +export const PLATFORM_MENU_ITEMS = ({ + theme, +}: ContextList): Array => [ + { + name: "copy-logo", + "aria-label": "Copy Logo as SVG to Clipboard", + children: "Copy Logo as SVG", + prefix: , + type: "copy", + onClick: () => { + copy( + ` + + ` + ); + }, + }, + { + name: "copy-wordmark", + "aria-label": "Copy Wordmark as SVG to Clipboard", + children: "Copy Wordmark as SVG", + prefix: , + type: "copy", + onClick: () => { + copy( + // NOTE: We include `xmlns` as this is required when the SVG isn't inlined. + `` + ); + }, + }, + { + name: "brand-guidelines", + "aria-label": "Open Brand Guidelines in New Tab", + children: "Brand Guidelines", + prefix: , + type: "external", + href: "https://vercel.com/design/brands", + }, +]; + +export const PRODUCT_MENU_ITEMS = ({ + site, +}: ContextList): Array => [ + { + name: "next-js", + "aria-label": "Open Next.js Home in New Tab", + children: "Next.js", + prefix: , + type: "external", + href: "https://nextjs.org", + }, + { + name: "turborepo", + "aria-label": "Open Turborepo Home in New Tab", + disabled: site === "repo", + children: "Turborepo", + prefix: , + type: "internal", + href: "/repo", + }, + { + name: "turbopack", + "aria-label": "Open Turbopack Home in New Tab", + disabled: site === "pack", + children: "Turbopack", + prefix: , + type: "internal", + href: "/pack", + }, +]; diff --git a/docs/components/LogoContext/types.ts b/docs/components/LogoContext/types.ts new file mode 100644 index 0000000..ff7f644 --- /dev/null +++ b/docs/components/LogoContext/types.ts @@ -0,0 +1,25 @@ +import type { ReactNode } from "react"; +import { TurboSite } from "../SiteSwitcher"; + +type MenuItemType = "internal" | "external" | "copy"; + +export interface MenuItemProps extends ContextItem { + closeMenu?: () => void; + className?: string; +} + +export interface ContextList { + theme: string; + site: TurboSite; +} + +export interface ContextItem { + name: string; + "aria-label": string; + disabled?: boolean; + type: MenuItemType; + children: ReactNode; + prefix: ReactNode; + href?: string; + onClick?: () => void; +} diff --git a/docs/components/MonorepoHandbook.tsx b/docs/components/MonorepoHandbook.tsx new file mode 100644 index 0000000..d7e2fe5 --- /dev/null +++ b/docs/components/MonorepoHandbook.tsx @@ -0,0 +1,142 @@ +import { + BanIcon, + ChatAlt2Icon, + CloudDownloadIcon, + CloudUploadIcon, + CodeIcon, + CubeIcon, + LibraryIcon, + PencilAltIcon, + ShareIcon, + ShieldExclamationIcon, + StarIcon, +} from "@heroicons/react/outline"; +import React from "react"; +import { DetailedFeatureLink } from "./Feature"; +import { DockerIcon } from "./Icons"; + +const Wrapper = ({ children }: { children: React.ReactNode }) => { + return ( +
+ {children} +
+ ); +}; + +export const FundamentalsArea = () => { + return ( + + + + + + + + + ); +}; + +export const TasksArea = () => { + return ( + + + + + + + + + ); +}; diff --git a/docs/components/Navigation.tsx b/docs/components/Navigation.tsx new file mode 100644 index 0000000..d72b714 --- /dev/null +++ b/docs/components/Navigation.tsx @@ -0,0 +1,47 @@ +import { Navbar } from "nextra-theme-docs"; +import { useTurboSite } from "./SiteSwitcher"; + +function Navigation(props) { + const site = useTurboSite(); + + /* + Inject a dynamic docs link when NOT on root + 1. Points to /repo/docs when on /repo + 2. Points to /pack/docs when on /pack + */ + const leadingItem = props.items[0]; + if (leadingItem?.id !== "contextual-docs" && site) { + props.items.unshift({ + title: "Docs", + type: "page", + route: `/${site}/docs`, + id: "contextual-docs", + key: "contextual-docs", + }); + } + + const lastItem = props.items[props.items.length - 1]; + if (lastItem?.id !== "contextual-enterprise") { + props.items.push({ + title: "Enterprise", + newWindow: true, + // https://github.com/shuding/nextra/issues/1028 + route: "enterprise", + href: `https://vercel.com/${ + site === "repo" ? "solutions/turborepo" : "contact/sales" + }?utm_source=turbo.build&utm_medium=referral&utm_campaign=header-enterpriseLink`, + id: "contextual-enterprise", + key: "contextual-enterprise", + }); + } + + // remove the top level repo and pack links + const headerItems = props.items.filter((item) => { + return item.name !== "repo" && item.name !== "pack"; + }); + + // items last to override the default + return ; +} + +export default Navigation; diff --git a/docs/components/QuickStart.tsx b/docs/components/QuickStart.tsx new file mode 100644 index 0000000..040871e --- /dev/null +++ b/docs/components/QuickStart.tsx @@ -0,0 +1,89 @@ +import { + BookOpenIcon, + CloudDownloadIcon, + CloudUploadIcon, + LightBulbIcon, + LightningBoltIcon, + PencilIcon, + ServerIcon, + SparklesIcon, +} from "@heroicons/react/outline"; +import { DetailedFeatureLink } from "./Feature"; +import Turbo from "./logos/Turbo"; + +export const QuickStartArea = () => { + return ( +
+ + + +
+ ); +}; + +export const MonoreposArea = () => { + return ( +
+ + +
+ ); +}; + +export const LearnMoreArea = () => { + return ( +
+ + +
+ ); +}; diff --git a/docs/components/RemoteCacheCounter.tsx b/docs/components/RemoteCacheCounter.tsx new file mode 100644 index 0000000..ef4dc6e --- /dev/null +++ b/docs/components/RemoteCacheCounter.tsx @@ -0,0 +1,47 @@ +import cn from "classnames"; +import { useState, useEffect } from "react"; +import { animated, useSpring, config } from "@react-spring/web"; +import useTurborepoMinutesSaved from "../lib/useTurborepoMinutesSaved"; +import Link from "next/link"; + +const counterFormatter = Intl.NumberFormat(undefined, { + minimumIntegerDigits: 7, + maximumFractionDigits: 0, +}); + +export default function RemoteCacheCounter() { + const [targetMinutes, setTargetMinutes] = useState(0); + const timeSaved = useTurborepoMinutesSaved(); + useEffect(() => { + if (timeSaved) { + setTargetMinutes( + timeSaved.local_cache_minutes_saved + + timeSaved.remote_cache_minutes_saved + ); + } + }, [timeSaved]); + + const spring = useSpring({ + from: { minutesSaved: 0 }, + minutesSaved: targetMinutes, + config: config.molasses, + }); + + return ( + +
+ + {spring.minutesSaved.to((t) => counterFormatter.format(t))} + +
Total Compute Minutes Saved
+ +
+ Get Started With Remote Caching → +
+
+ + ); +} diff --git a/docs/components/SiteSwitcher.tsx b/docs/components/SiteSwitcher.tsx new file mode 100644 index 0000000..e5d58d2 --- /dev/null +++ b/docs/components/SiteSwitcher.tsx @@ -0,0 +1,66 @@ +import cn from "classnames"; +import { useRouter } from "next/router"; +import Link from "next/link"; + +export type TurboSite = "pack" | "repo"; + +export function useTurboSite(): TurboSite | undefined { + const { pathname } = useRouter(); + + if (pathname.startsWith("/repo")) { + return "repo"; + } + + if (pathname.startsWith("/pack")) { + return "pack"; + } + + return undefined; +} + +function SiteSwitcherLink({ href, text, isActive }) { + const classes = + "py-1 transition-colors duration-300 inline-block w-[50px] cursor-pointer hover:text-black dark:hover:text-white"; + + const conditionalClasses = { + "text-black dark:text-white": !!isActive, + }; + + return ( + + {text} + + ); +} + +function SiteSwitcher() { + const site = useTurboSite(); + + return ( +
+ + + + + + +
+ ); +} + +export default SiteSwitcher; diff --git a/docs/components/Social.tsx b/docs/components/Social.tsx new file mode 100644 index 0000000..2ed0df0 --- /dev/null +++ b/docs/components/Social.tsx @@ -0,0 +1,32 @@ +import { DiscordIcon, GitHubIcon } from "nextra/icons"; + +function Github() { + return ( + + {/* Nextra icons have a attribute providing alt text */} + <GitHubIcon /> + </a> + ); +} + +function Discord() { + return ( + <a + href="https://turbo.build/discord" + className="hidden p-2 text-current sm:flex hover:opacity-75" + title="Turbo Discord server" + target="_blank" + rel="noreferrer" + > + <DiscordIcon /> + </a> + ); +} + +export { Github, Discord }; diff --git a/docs/components/Tabs.tsx b/docs/components/Tabs.tsx new file mode 100644 index 0000000..f75019a --- /dev/null +++ b/docs/components/Tabs.tsx @@ -0,0 +1,37 @@ +import type { FC, ReactElement } from "react"; + +import { Tabs as NextraTabs, Tab } from "nextra-theme-docs"; +import useSWR from "swr"; + +export { Tab }; + +export const Tabs: FC<{ + storageKey?: string; + items: string[]; + children: ReactElement; +}> = function ({ storageKey = "tab-index", items, children = null, ...props }) { + // Use SWR so all tabs with the same key can sync their states. + const { data, mutate } = useSWR(storageKey, (key) => { + try { + return JSON.parse(localStorage.getItem(key)); + } catch (e) { + return null; + } + }); + + const selectedIndex = items.indexOf(data); + + return ( + <NextraTabs + onChange={(index) => { + localStorage.setItem(storageKey, JSON.stringify(items[index])); + mutate(items[index], false); + }} + selectedIndex={selectedIndex === -1 ? undefined : selectedIndex} + items={items} + {...props} + > + {children} + </NextraTabs> + ); +}; diff --git a/docs/components/TurbopackFeatures.tsx b/docs/components/TurbopackFeatures.tsx new file mode 100644 index 0000000..5570a52 --- /dev/null +++ b/docs/components/TurbopackFeatures.tsx @@ -0,0 +1,106 @@ +import { + AdjustmentsIcon, + ArchiveIcon, + DesktopComputerIcon, + DownloadIcon, + ServerIcon, +} from "@heroicons/react/outline"; +import { DetailedFeatureLink } from "./Feature"; +import { CSSIcon, JSIcon, TSIcon } from "./Icons"; + +export const TurbopackFeatures = () => { + return ( + <div className="grid grid-cols-1 mt-12 gap-x-6 gap-y-12 sm:grid-cols-2 lg:mt-16 lg:gap-x-8 lg:gap-y-12"> + <DetailedFeatureLink + feature={{ + Icon: JSIcon, + description: `Supports all ESNext features, Browserslist and top-level await.`, + name: "JavaScript", + }} + href="/pack/docs/features/javascript" + ></DetailedFeatureLink> + <DetailedFeatureLink + feature={{ + Icon: TSIcon, + description: ( + <> + Supports TypeScript out of the box, including resolving{" "} + <code>paths</code> and <code>baseUrl</code>. + </> + ), + name: "TypeScript", + }} + href="/pack/docs/features/typescript" + ></DetailedFeatureLink> + <DetailedFeatureLink + feature={{ + Icon: DownloadIcon, + description: ( + <> + Supports <code>require</code>, <code>import</code>, dynamic + imports and more. + </> + ), + name: "Imports", + }} + href="/pack/docs/features/imports" + ></DetailedFeatureLink> + <DetailedFeatureLink + feature={{ + Icon: DesktopComputerIcon, + description: `Our optimized dev server supports Hot Module Reloading (HMR) and Fast Refresh.`, + name: "Dev Server", + }} + href="/pack/docs/features/dev-server" + ></DetailedFeatureLink> + <DetailedFeatureLink + feature={{ + Icon: CSSIcon, + description: ( + <> + Supports Global CSS, CSS Modules, postcss-nested and{" "} + <code>@import</code>. + </> + ), + name: "CSS", + }} + href="/pack/docs/features/css" + ></DetailedFeatureLink> + <DetailedFeatureLink + feature={{ + Icon: ArchiveIcon, + description: + "Learn about Next.js, Svelte, Vue and React Server Components support.", + name: "Frameworks", + }} + href="/pack/docs/features/frameworks" + ></DetailedFeatureLink> + <DetailedFeatureLink + feature={{ + Icon: ServerIcon, + description: ( + <> + Supports the <code>/public</code> directory, JSON imports, and + importing assets via ESM. + </> + ), + name: "Static Assets", + }} + href="/pack/docs/features/static-assets" + ></DetailedFeatureLink> + <DetailedFeatureLink + feature={{ + Icon: AdjustmentsIcon, + description: ( + <> + Supports environment variables via <code>.env</code>,{" "} + <code>.env.local</code>, and more. + </> + ), + name: "Environment Variables", + }} + href="/pack/docs/features/environment-variables" + ></DetailedFeatureLink> + </div> + ); +}; diff --git a/docs/components/TurbopackQuickstart.tsx b/docs/components/TurbopackQuickstart.tsx new file mode 100644 index 0000000..695a947 --- /dev/null +++ b/docs/components/TurbopackQuickstart.tsx @@ -0,0 +1,28 @@ +import { + LightBulbIcon, + QuestionMarkCircleIcon, +} from "@heroicons/react/outline"; +import { DetailedFeatureLink } from "./Feature"; + +export const TurbopackQuickstartArea = () => { + return ( + <div className="grid grid-cols-1 mt-12 gap-x-6 gap-y-12 sm:grid-cols-2 lg:mt-16 lg:gap-x-8 lg:gap-y-12"> + <DetailedFeatureLink + feature={{ + Icon: QuestionMarkCircleIcon, + description: `Learn why we created Turbopack, and why we think it’s the future of bundling for the web.`, + name: "Why Turbopack?", + }} + href="/pack/docs/why-turbopack" + ></DetailedFeatureLink> + <DetailedFeatureLink + feature={{ + Icon: LightBulbIcon, + description: `Learn about the innovative architecture that powers Turbopack’s speed improvements.`, + name: "Core Concepts", + }} + href="/pack/docs/core-concepts" + ></DetailedFeatureLink> + </div> + ); +}; diff --git a/docs/components/Tweet.tsx b/docs/components/Tweet.tsx new file mode 100644 index 0000000..e5ac388 --- /dev/null +++ b/docs/components/Tweet.tsx @@ -0,0 +1,46 @@ +import Image from "next/image"; + +function TweetLink({ href, children }) { + return ( + <a + href={href} + target="_blank" + rel="noopener noreferrer" + className="inline-block text-[#35ACDF]" + > + {children} + </a> + ); +} + +export function Mention({ children }) { + return ( + <TweetLink href={`https://twitter.com/${children.replace("@", "")}`}> + {children} + </TweetLink> + ); +} + +export default function Tweet({ url, username, name, avatar, date, children }) { + return ( + <div className="flex p-4 bg-white rounded-md shadow-xl dark:bg-opacity-10"> + <div className="flex-shrink-0 mr-4"> + <Image + className="w-12 h-12 rounded-full" + width={42} + height={42} + src={avatar} + alt={`${name} twitter avatar`} + /> + </div> + <div> + <div className="flex items-center space-x-1 text-sm"> + <h4 className="font-medium dark:text-white">{name}</h4> + <div className="truncate dark:text-gray-400">@{username}</div> + <div className="dark:text-gray-500 md:hidden xl:block">• {date}</div> + </div> + <div className="mt-1 text-sm dark:text-gray-200">{children}</div> + </div> + </div> + ); +} diff --git a/docs/components/blog/Date.tsx b/docs/components/blog/Date.tsx new file mode 100644 index 0000000..506eecc --- /dev/null +++ b/docs/components/blog/Date.tsx @@ -0,0 +1,21 @@ +import { ReactNode } from "react"; + +function Date({ + children, + update = null, +}: { + children: ReactNode; + update?: string; +}) { + return ( + <div className="text-sm mt-2 text-center text-gray-500 dark:text-gray-400 font-space-grotesk"> + {children} + + {update != null && ( + <div className="text-xs mt-1 text-center">Last updated {update}</div> + )} + </div> + ); +} + +export default Date; diff --git a/docs/components/clients/Clients.tsx b/docs/components/clients/Clients.tsx new file mode 100644 index 0000000..199794e --- /dev/null +++ b/docs/components/clients/Clients.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import cn from "classnames"; +import { users } from "./users"; +import { Logo } from "./Logo"; + +export function Clients({ + linked, + staticWidth, + companyList, +}: { + linked?: boolean; + staticWidth?: boolean; + companyList?: string[]; +}) { + const showcaseDark = []; + const showcaseLight = []; + + const LogoWrapper = ({ className, children }) => { + if (!staticWidth) return children; + return ( + <div + className={cn( + "w-48 lg:w-40 flex items-center justify-center", + className + )} + > + {children} + </div> + ); + }; + + users + .filter((i) => (companyList ? companyList.includes(i.caption) : true)) + .forEach((user) => { + if (user.pinned) { + showcaseDark.push( + <LogoWrapper + key={`${user.caption}-dark`} + className="flex dark:hidden" + > + <Logo user={user} theme={"dark"} isLink={linked} /> + </LogoWrapper> + ); + showcaseLight.push( + <LogoWrapper + key={`${user.caption}-light`} + className="hidden dark:flex" + > + <Logo user={user} theme={"light"} isLink={linked} /> + </LogoWrapper> + ); + } + }); + + return ( + <> + {showcaseDark} + {showcaseLight} + </> + ); +} diff --git a/docs/components/clients/Filters.tsx b/docs/components/clients/Filters.tsx new file mode 100644 index 0000000..3c22edc --- /dev/null +++ b/docs/components/clients/Filters.tsx @@ -0,0 +1,42 @@ +import React from "react"; + +export const Filters: React.FC<{}> = () => ( + <> + <svg width={0} height={0}> + <defs> + <filter id="high-threshold"> + <feColorMatrix type="saturate" values="0" /> + <feComponentTransfer> + <feFuncR type="discrete" tableValues="0" /> + <feFuncG type="discrete" tableValues="0" /> + <feFuncB type="discrete" tableValues="0" /> + </feComponentTransfer> + </filter> + </defs> + </svg> + <svg width={0} height={0}> + <defs> + <filter id="medium-threshold"> + <feColorMatrix type="saturate" values="0" /> + <feComponentTransfer> + <feFuncR type="discrete" tableValues="0 1" /> + <feFuncG type="discrete" tableValues="0 1" /> + <feFuncB type="discrete" tableValues="0 1" /> + </feComponentTransfer> + </filter> + </defs> + </svg> + <svg width={0} height={0}> + <defs> + <filter id="low-threshold"> + <feColorMatrix type="saturate" values="0" /> + <feComponentTransfer> + <feFuncR type="discrete" tableValues="0 0 0 0 1" /> + <feFuncG type="discrete" tableValues="0 0 0 0 1" /> + <feFuncB type="discrete" tableValues="0 0 0 0 1" /> + </feComponentTransfer> + </filter> + </defs> + </svg> + </> +); diff --git a/docs/components/clients/Logo.tsx b/docs/components/clients/Logo.tsx new file mode 100644 index 0000000..79ab9c2 --- /dev/null +++ b/docs/components/clients/Logo.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import cn from "classnames"; +import Image from "next/image"; +import { TurboUser } from "./users"; + +const DEFAULT_SIZE = { + width: 100, + height: 75, +}; + +export function Logo({ + user, + theme, + isLink, +}: { + user: TurboUser; + theme: "dark" | "light"; + isLink: boolean; +}) { + const styles = { + ...DEFAULT_SIZE, + ...user.style, + }; + let numericWidth: number; + let numericHeight: number; + if (typeof styles.width === "number") { + numericWidth = styles.width; + } + if (typeof styles.height === "number") { + numericHeight = styles.height; + } + const logo = ( + <Image + src={user.image.replace( + "/logos", + theme === "light" ? "/logos/white" : "/logos/color" + )} + alt={`${user.caption}'s Logo`} + width={numericWidth} + height={numericHeight} + priority={true} + style={styles} + className={cn("mx-8", { + "hidden dark:inline": theme !== "dark", + "dark:hidden inline": theme === "dark", + })} + /> + ); + + if (isLink) { + return ( + <a + href={user.infoLink} + target="_blank" + rel="noopener noreferrer" + className={cn("flex justify-center item-center", { + "hidden dark:flex": theme !== "dark", + "dark:hidden flex": theme === "dark", + })} + > + {logo} + </a> + ); + } + + return logo; +} diff --git a/docs/components/clients/Marquee.tsx b/docs/components/clients/Marquee.tsx new file mode 100644 index 0000000..0e0fc6f --- /dev/null +++ b/docs/components/clients/Marquee.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +export function Marquee({ children, ...props }) { + return ( + <div className="overflow-x-hidden"> + <div className="sr-only"> + These are the logos of some but not all of our users. + </div> + <div className="relative"> + <div className="inline-block wrapper">{children}</div> + </div> + </div> + ); +} diff --git a/docs/components/clients/users.ts b/docs/components/clients/users.ts new file mode 100644 index 0000000..efd2172 --- /dev/null +++ b/docs/components/clients/users.ts @@ -0,0 +1,556 @@ +import { CSSProperties } from "react"; + +export type TurboUser = { + caption: string; + image: string; + infoLink: string; + pinned?: boolean; + style?: CSSProperties; +}; + +export const users: Array<TurboUser> = [ + { + caption: "Vercel", + image: "/images/logos/vercel.svg", + infoLink: "https://vercel.com/", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "AWS", + image: "/images/logos/aws.svg", + infoLink: "https://aws.amazon.com/", + pinned: true, + style: { + width: 75, + }, + }, + { + caption: "Microsoft", + image: "/images/logos/microsoft.svg", + infoLink: "https://www.microsoft.com/", + pinned: true, + style: { + width: 125, + }, + }, + { + caption: "Netflix", + image: "/images/logos/netflix.svg", + infoLink: "https://netflix.com/", + pinned: true, + style: { + width: 110, + }, + }, + { + caption: "Disney", + image: "/images/logos/disney.svg", + infoLink: "https://www.disney.com/", + pinned: true, + }, + { + caption: "Github", + image: "/images/logos/github.svg", + infoLink: "https://www.github.com/", + pinned: true, + style: { + width: 110, + }, + }, + { + caption: "Alibaba", + image: "/images/logos/alibaba.svg", + infoLink: "https://www.alibaba.com/", + pinned: true, + style: { + width: 125, + }, + }, + { + caption: "Ant Group", + image: "/images/logos/ant.svg", + infoLink: "https://antgroup.com/", + pinned: true, + style: { + width: 125, + }, + }, + { + caption: "Adobe", + image: "/images/logos/adobe.svg", + infoLink: "https://www.adobe.com/", + pinned: true, + }, + { + caption: "PayPal", + image: "/images/logos/paypal.svg", + infoLink: "https://www.paypal.com/", + pinned: true, + }, + + { + caption: "Snap", + image: "/images/logos/snap.svg", + infoLink: "https://snap.com/", + pinned: true, + }, + { + caption: "SAP", + image: "/images/logos/sap.svg", + infoLink: "https://www.sap.com/", + pinned: true, + style: { + width: 75, + }, + }, + + { + caption: "Shopify", + image: "/images/logos/shopify.svg", + infoLink: "https://www.shopify.com/", + pinned: true, + style: { + width: 125, + }, + }, + + { + caption: "Datadog", + image: "/images/logos/datadog.svg", + infoLink: "https://www.datadoghq.com/", + pinned: true, + style: { + width: 125, + }, + }, + { + caption: "Twilio", + image: "/images/logos/twilio.svg", + infoLink: "https://www.twilio.com/", + pinned: true, + }, + { + caption: "Segment", + image: "/images/logos/segment.svg", + infoLink: "https://segment.com/", + pinned: true, + style: { + width: 125, + }, + }, + { + caption: "Twitch", + image: "/images/logos/twitch.svg", + infoLink: "https://www.twitch.tv/", + pinned: true, + style: { + width: 125, + }, + }, + { + caption: "Xiaomi", + image: "/images/logos/xiaomi.svg", + infoLink: "https://www.mi.com/", + pinned: true, + style: { + width: 50, + }, + }, + { + caption: "Line", + image: "/images/logos/line.svg", + infoLink: "https://line.me/", + pinned: true, + style: { + width: 75, + }, + }, + { + caption: "ESPN", + image: "/images/logos/espn.svg", + infoLink: "https://www.espn.com/", + pinned: true, + style: { + width: 125, + }, + }, + { + caption: "Volvo", + image: "/images/logos/volvo.svg", + infoLink: "https://www.volvo.com/", + pinned: true, + style: { + width: 60, + }, + }, + { + caption: "Hearst", + image: "/images/logos/hearst.svg", + infoLink: "https://www.hearst.com/", + pinned: true, + style: { + width: 175, + }, + }, + { + caption: "The Washington Post", + image: "/images/logos/washingtonpost.svg", + infoLink: "https://www.washingtonpost.com/", + pinned: true, + style: { + width: 175, + }, + }, + { + caption: "Wayfair", + image: "/images/logos/wayfair.svg", + infoLink: "https://www.wayfair.com/", + pinned: true, + style: { + width: 125, + }, + }, + { + caption: "Hulu", + image: "/images/logos/hulu.svg", + infoLink: "https://www.hulu.com/", + pinned: true, + }, + { + caption: "CrowdStrike", + image: "/images/logos/crowdstrike.svg", + infoLink: "https://www.crowdstrike.com/", + pinned: true, + style: { + width: 150, + marginTop: 20, + }, + }, + { + caption: "Binance", + image: "/images/logos/binance.svg", + infoLink: "https://www.binance.com/", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "Plex", + image: "/images/logos/plex.svg", + infoLink: "https://www.plex.tv/", + pinned: true, + }, + { + caption: "Groupon", + image: "/images/logos/groupon.svg", + infoLink: "https://groupon.com/", + pinned: true, + style: { + width: 125, + }, + }, + { + caption: "Vimeo", + image: "/images/logos/vimeo.svg", + infoLink: "https://vimeo.com/", + pinned: true, + }, + { + caption: "GoodRx", + image: "/images/logos/goodrx.svg", + infoLink: "https://www.goodrx.com/", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "Tripadvisor", + image: "/images/logos/tripadvisor.svg", + infoLink: "https://www.tripadvisor.com/", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "RapidAPI", + image: "/images/logos/rapidapi.svg", + infoLink: "https://rapidapi.com/", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "Miro", + image: "/images/logos/miro.svg", + infoLink: "https://miro.com/", + pinned: true, + }, + { + caption: "Lattice", + image: "/images/logos/lattice.svg", + infoLink: "https://lattice.com/", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "Watershed", + image: "/images/logos/watershed.svg", + infoLink: "https://watershed.com/", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "N26", + image: "/images/logos/n26.svg", + infoLink: "https://n26.com/", + pinned: true, + style: { + width: 75, + }, + }, + { + caption: "Sourcegraph", + image: "/images/logos/sourcegraph.svg", + infoLink: "https://sourcegraph.com/", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "Big Commerce", + image: "/images/logos/bigcommerce.svg", + infoLink: "https://www.bigcommerce.com/", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "Stedi", + image: "/images/logos/stedi.svg", + infoLink: "https://www.stedi.com/", + pinned: true, + style: { + width: 75, + }, + }, + { + caption: "Framer", + image: "/images/logos/framer.svg", + infoLink: "https://www.framer.com/", + pinned: true, + }, + { + caption: "Maze", + image: "/images/logos/maze.svg", + infoLink: "https://maze.co/", + pinned: true, + style: { + width: 125, + }, + }, + { + caption: "Builder.io", + image: "/images/logos/builderio.svg", + infoLink: "https://www.builder.io/", + pinned: true, + style: { + width: 125, + }, + }, + { + caption: "Contentful", + image: "/images/logos/contentful.svg", + infoLink: "https://www.contentful.com/", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "Xata", + image: "/images/logos/xata.svg", + infoLink: "https://xata.io/", + pinned: true, + }, + { + caption: "Cal.com", + image: "/images/logos/calcom.svg", + infoLink: "https://cal.com/", + pinned: true, + }, + { + caption: "Codesandbox", + image: "/images/logos/codesandbox.svg", + infoLink: "https://codesandbox.io/", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "WooCommerce", + image: "/images/logos/woocommerce.svg", + infoLink: "https://woocommerce.com/", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "Expo", + image: "/images/logos/expo.svg", + infoLink: "https://expo.dev/", + pinned: true, + }, + { + caption: "TeeSpring", + image: "/images/logos/teespring.svg", + infoLink: "https://www.spri.ng/", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "Endear", + image: "/images/logos/endear.svg", + infoLink: "https://endearhq.com/", + pinned: true, + }, + { + caption: "Makeswift", + image: "/images/logos/makeswift.svg", + infoLink: "https://www.makeswift.com/", + pinned: true, + }, + { + caption: "Fandom", + image: "/images/logos/fandom.svg", + infoLink: "https://www.fandom.com/", + pinned: true, + }, + { + caption: "Waggel", + image: "/images/logos/waggel.svg", + infoLink: "https://www.waggel.co.uk/", + pinned: true, + }, + { + caption: "n8n", + image: "/images/logos/n8n.svg", + infoLink: "https://n8n.io/", + pinned: true, + }, + { + caption: "React Flow", + image: "/images/logos/reactflow.svg", + infoLink: "https://reactflow.dev/", + pinned: true, + }, + { + caption: "Agrotoken", + image: "/images/logos/agrotoken.svg", + infoLink: "https://agrotoken.io/", + pinned: true, + }, + { + caption: "Rocket.Chat", + image: "/images/logos/rocketchat.svg", + infoLink: "https://www.rocket.chat/", + pinned: true, + }, + { + caption: "Backpack", + image: "/images/logos/backpack.svg", + infoLink: "https://www.backpack.app/", + pinned: true, + }, + { + caption: "Supernova.io", + image: "/images/logos/supernova.svg", + infoLink: "https://www.supernova.io/", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "BuildPass", + image: "/images/logos/buildpass.svg", + infoLink: "https://www.buildpass.com.au/", + pinned: true, + style: { + width: 175, + }, + }, + { + caption: "SolanaFM", + image: "/images/logos/solanafm.svg", + infoLink: "https://solana.fm/", + pinned: true, + }, + { + caption: "Authdog", + image: "/images/logos/authdog.svg", + infoLink: "https://www.authdog.com/", + pinned: true, + style: { + width: 175, + }, + }, + { + caption: "Nhost", + image: "/images/logos/nhost.svg", + infoLink: "https://nhost.io/", + pinned: true, + }, + { + caption: "LG U+", + image: "/images/logos/lguplus.svg", + infoLink: "https://www.lguplus.com/about/en", + pinned: true, + style: { + width: 150, + }, + }, + { + caption: "Comparastore", + image: "/images/logos/comparastore.svg", + infoLink: "https://www.comparastore.com", + pinned: true, + style: { + width: 175, + }, + }, + { + caption: "Block Protocol", + image: "/images/logos/blockprotocol.svg", + infoLink: "https://blockprotocol.org/", + pinned: true, + }, + { + caption: "HASH", + image: "/images/logos/hash.svg", + infoLink: "https://hash.dev/", + pinned: true, + }, + { + caption: "Chợ Tốt", + image: "/images/logos/chotot.svg", + infoLink: "https://chotot.com/", + pinned: true, + }, +]; diff --git a/docs/components/counters.module.css b/docs/components/counters.module.css deleted file mode 100644 index 4a5d0c8..0000000 --- a/docs/components/counters.module.css +++ /dev/null @@ -1,6 +0,0 @@ -.counter { - border: 1px solid #ccc; - border-radius: 5px; - padding: 2px 6px; - margin: 12px 0 0; -} diff --git a/docs/components/counters.tsx b/docs/components/counters.tsx deleted file mode 100644 index b78f12d..0000000 --- a/docs/components/counters.tsx +++ /dev/null @@ -1,24 +0,0 @@ -// Example from https://beta.reactjs.org/learn - -import { useState } from 'react' -import styles from './counters.module.css' - -function MyButton() { - const [count, setCount] = useState(0) - - function handleClick() { - setCount(count + 1) - } - - return ( - <div> - <button onClick={handleClick} className={styles.counter}> - Clicked {count} times - </button> - </div> - ) -} - -export default function MyApp() { - return <MyButton /> -} diff --git a/docs/components/header-logo.module.css b/docs/components/header-logo.module.css new file mode 100644 index 0000000..515486e --- /dev/null +++ b/docs/components/header-logo.module.css @@ -0,0 +1,20 @@ +.desktopLogo { + display: none; + + @media (min-width: 768px) { + display: block; + } +} + +.siteSwitcher { + position: absolute; + left: 50%; + transform: translateX(-50%); + + @media (min-width: 768px) { + margin-left: 12px; + position: relative; + left: 0; + transform: none; + } +} diff --git a/docs/components/image/ImageFigure.tsx b/docs/components/image/ImageFigure.tsx new file mode 100644 index 0000000..b90237e --- /dev/null +++ b/docs/components/image/ImageFigure.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import Image from "next/image"; + +type ImageProps = Parameters<typeof Image>[0]; + +export type ImageFigureProps = ImageProps & { + caption?: string; + margin?: number; + captionSpacing?: number; + shadow?: boolean; + borderRadius?: boolean; +}; + +export function ImageFigure(props: ImageFigureProps): React.ReactNode { + const { + caption, + margin = 40, + captionSpacing = null, + shadow = false, + borderRadius = false, + ...rest + } = props; + + return ( + <figure className="block text-center" style={{ margin: `${margin}px 0` }}> + <div className="relative inline-block w-full max-w-full overflow-hidden border-box text-[0px]"> + {/* eslint-disable-next-line jsx-a11y/alt-text */} + <Image {...rest} /> + </div> + {caption && ( + <figcaption + className="m-0 text-xs text-center text-gray-500" + style={captionSpacing ? { marginTop: captionSpacing } : {}} + > + {caption} + </figcaption> + )} + </figure> + ); +} diff --git a/docs/components/image/ThemedImage.tsx b/docs/components/image/ThemedImage.tsx new file mode 100644 index 0000000..0416bd9 --- /dev/null +++ b/docs/components/image/ThemedImage.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import Image from "next/image"; + +export interface Image { + height: number; + width: number; + source: string; +} + +export interface ThemedImageProps { + title?: string; + dark?: Image; + light?: Image; + priority?: boolean; +} + +export function ThemedImage({ + title, + light, + dark, + priority = false, +}: ThemedImageProps) { + return ( + <> + <div className="block w-full dark:hidden"> + <Image + alt={title} + src={light.source} + width={light.width} + height={light.height} + priority={priority} + /> + </div> + <div className="hidden w-full dark:block"> + <Image + alt={title} + src={dark.source} + width={dark.width} + height={dark.height} + priority={priority} + /> + </div> + </> + ); +} diff --git a/docs/components/image/ThemedImageFigure.tsx b/docs/components/image/ThemedImageFigure.tsx new file mode 100644 index 0000000..9c50dc7 --- /dev/null +++ b/docs/components/image/ThemedImageFigure.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { ImageFigureProps } from "./ImageFigure"; +import { ThemedImage, ThemedImageProps } from "./ThemedImage"; +import cn from "classnames"; +export type ThemedImageFigureProps = Omit<ImageFigureProps, "src"> & + ThemedImageProps; + +export function ThemedImageFigure( + props: ThemedImageFigureProps +): React.ReactNode { + const { + caption, + margin = 40, + captionSpacing = null, + shadow = false, + borderRadius = false, + ...rest + } = props; + + return ( + <figure + className="block -mx-4 text-center sm:-mx-4 md:-mx-7 lg:-mx-12" + style={{ marginTop: `${margin}px`, marginBottom: `${margin}px` }} + > + <div + className={cn( + "relative inline-block max-w-full overflow-hidden border-box text-[0px]", + { + "rounded-md": borderRadius, + "shadow-lg": shadow, + } + )} + > + {/* eslint-disable-next-line jsx-a11y/alt-text */} + <ThemedImage {...rest} /> + </div> + {caption && ( + <figcaption + className="m-0 text-xs text-center text-gray-500" + style={captionSpacing ? { marginTop: captionSpacing } : {}} + > + {caption} + </figcaption> + )} + </figure> + ); +} diff --git a/docs/components/logos/PackLogo.tsx b/docs/components/logos/PackLogo.tsx new file mode 100644 index 0000000..ddb1d36 --- /dev/null +++ b/docs/components/logos/PackLogo.tsx @@ -0,0 +1,16 @@ +const PackLogo = (props) => ( + <svg {...props} viewBox="0 0 485 49" xmlns="http://www.w3.org/2000/svg"> + <title>Turbopack logo + + + + + + + + + + +); + +export default PackLogo; diff --git a/docs/components/logos/RepoLogo.tsx b/docs/components/logos/RepoLogo.tsx new file mode 100644 index 0000000..2751733 --- /dev/null +++ b/docs/components/logos/RepoLogo.tsx @@ -0,0 +1,16 @@ +const RepoLogo = (props) => ( + + Turborepo logo + + + + + + + + + + +); + +export default RepoLogo; diff --git a/docs/components/logos/Turbo.tsx b/docs/components/logos/Turbo.tsx new file mode 100644 index 0000000..d873e5f --- /dev/null +++ b/docs/components/logos/Turbo.tsx @@ -0,0 +1,62 @@ +type LogoProps = { + className?: string; + height?: number; +}; + +const Turbo = ({ height = 32, className = "" }: LogoProps) => ( + + Turborepo + + + + + + + + + + + + + + +); + +export default Turbo; diff --git a/docs/components/logos/TurboAnimated.tsx b/docs/components/logos/TurboAnimated.tsx new file mode 100644 index 0000000..c70f42d --- /dev/null +++ b/docs/components/logos/TurboAnimated.tsx @@ -0,0 +1,157 @@ +import { AnimatePresence, motion, Variants } from "framer-motion"; +import { useTurboSite } from "../SiteSwitcher"; +import cn from "classnames"; +import styles from "../header-logo.module.css"; + +type LogoProps = { + className?: string; + height?: number; +}; + +const LEFT_PADDING = 8; +const RIGHT_PADDING = 12; +// The width of the logo + wordmark. This does not include the "invisible" padding. +const VISUAL_WIDTH = 112; + +const TurboAnimated = ({ height = 32, className = "" }: LogoProps) => { + const site = useTurboSite(); + + return ( + + Turborepo + + + + + + + + + + + + + + + + + + + + {site === "repo" || site === undefined ? ( + + + + + ) : ( + + + + + )} + + + + {/* Turbo Wordmark */} + + + + + + + + + ); +}; + +export default TurboAnimated; + +const variants: Variants = { + visible: { + opacity: 1, + x: 0, + transition: { + duration: 0.3, + }, + }, + hidden: (distance) => ({ + opacity: 0, + x: distance, + transition: { + duration: 0.3, + }, + }), +}; diff --git a/docs/components/logos/Vercel.tsx b/docs/components/logos/Vercel.tsx new file mode 100644 index 0000000..36b5e81 --- /dev/null +++ b/docs/components/logos/Vercel.tsx @@ -0,0 +1,11 @@ +const Vercel = ({ height = 20 }: { height?: number }) => ( + + Vercel + + +); + +export default Vercel; diff --git a/docs/components/logos/og/PackLogo.tsx b/docs/components/logos/og/PackLogo.tsx new file mode 100644 index 0000000..a2bc95a --- /dev/null +++ b/docs/components/logos/og/PackLogo.tsx @@ -0,0 +1,72 @@ +const PackLogo = (props) => ( + + + + + + + + + + + + + + + + + + + +); + +export default PackLogo; diff --git a/docs/components/logos/og/RepoLogo.tsx b/docs/components/logos/og/RepoLogo.tsx new file mode 100644 index 0000000..f09a1ff --- /dev/null +++ b/docs/components/logos/og/RepoLogo.tsx @@ -0,0 +1,70 @@ +const RepoLogo = (props) => ( + + + + + + + + + + + + + + + + + + + +); + +export default RepoLogo; diff --git a/docs/components/logos/og/TurboLogo.tsx b/docs/components/logos/og/TurboLogo.tsx new file mode 100644 index 0000000..5646ce2 --- /dev/null +++ b/docs/components/logos/og/TurboLogo.tsx @@ -0,0 +1,54 @@ +const TurboLogo = (props) => ( + + + + + + + + + + + + + + + +); + +export default TurboLogo; diff --git a/docs/components/logos/og/VercelLogo.tsx b/docs/components/logos/og/VercelLogo.tsx new file mode 100644 index 0000000..020f271 --- /dev/null +++ b/docs/components/logos/og/VercelLogo.tsx @@ -0,0 +1,16 @@ +const VercelLogo = ({ + fill = "none", + height = 20, +}: { + fill?: string; + height?: number; +}) => ( + + + +); + +export default VercelLogo; diff --git a/docs/components/output-mode-table.mdx b/docs/components/output-mode-table.mdx new file mode 100644 index 0000000..56fd80a --- /dev/null +++ b/docs/components/output-mode-table.mdx @@ -0,0 +1,7 @@ +| option | description | +| ----------- | ---------------------------------------- | +| full | This is the default. Displays all output | +| hash-only | Show only the hashes of the tasks | +| new-only | Only show output from cache misses | +| errors-only | Only show output from task failures | +| none | Hides all task output | diff --git a/docs/components/pages/confirm.tsx b/docs/components/pages/confirm.tsx new file mode 100644 index 0000000..b9295cf --- /dev/null +++ b/docs/components/pages/confirm.tsx @@ -0,0 +1,36 @@ +/* eslint-disable react/no-unescaped-entities */ +import Head from "next/head"; +import { Container } from "../Container"; + +export default function Confirm() { + return ( + <> + + Confirm + + + +
+
+
+
+
+

Thanks so much!

+

+ Keep an eye on your inbox for product updates and + announcements from Turbo and Vercel. +

{" "} +

+ Thanks, +
+ The Turbo Team +

+
+
+
+
+
+
+ + ); +} diff --git a/docs/components/pages/home-shared/CTAButton.tsx b/docs/components/pages/home-shared/CTAButton.tsx new file mode 100644 index 0000000..57da227 --- /dev/null +++ b/docs/components/pages/home-shared/CTAButton.tsx @@ -0,0 +1,43 @@ +import cn from "classnames"; +import { MouseEventHandler } from "react"; +import gradients from "./gradients.module.css"; + +export function CTAButton({ + children, + outline, + onClick, + monospace, +}: { + outline?: boolean; + children: React.ReactNode; + onClick?: MouseEventHandler; + monospace?: boolean; +}) { + const outlineClasses = + "border dark:border-neutral-400 dark:text-neutral-200 dark:hover:border-white dark:hover:text-white border-[#EAEAEA] text-neutral-800 hover:border-black hover:text-black"; + const filledClasses = + "dark:text-black text-white border-transparent bg-black dark:bg-white"; + + return ( +
+ + {!outline && ( +
+ )} +
+ ); +} diff --git a/docs/components/pages/home-shared/FadeIn.tsx b/docs/components/pages/home-shared/FadeIn.tsx new file mode 100644 index 0000000..826a078 --- /dev/null +++ b/docs/components/pages/home-shared/FadeIn.tsx @@ -0,0 +1,50 @@ +import { motion, useInView } from "framer-motion"; +import { useRef } from "react"; + +export function FadeIn({ + children, + className, + noVertical, + delay, + viewTriggerOffset, +}: { + children: React.ReactNode; + className?: string; + noVertical?: boolean; + delay?: number; + viewTriggerOffset?: boolean; +}) { + const ref = useRef(null); + const inView = useInView(ref, { + once: true, + margin: viewTriggerOffset ? "-128px" : "0px", + }); + + const fadeUpVariants = { + initial: { + opacity: 0, + y: noVertical ? 0 : 24, + }, + animate: { + opacity: 1, + y: 0, + }, + }; + + return ( + + {children} + + ); +} diff --git a/docs/components/pages/home-shared/FeatureBox.tsx b/docs/components/pages/home-shared/FeatureBox.tsx new file mode 100644 index 0000000..c9d46c1 --- /dev/null +++ b/docs/components/pages/home-shared/FeatureBox.tsx @@ -0,0 +1,42 @@ +import Image from "next/image"; +import type { ReactNode } from "react"; + +export function FeatureBox({ + name, + description, + iconDark, + iconLight, +}: { + iconDark: Parameters[0]["src"]; + iconLight: Parameters[0]["src"]; + name: string; + description: ReactNode; +}) { + return ( +
+ + +
+

+ {name} +

+ +

{description}

+
+
+ ); +} diff --git a/docs/components/pages/home-shared/FeaturesBento.tsx b/docs/components/pages/home-shared/FeaturesBento.tsx new file mode 100644 index 0000000..f2664db --- /dev/null +++ b/docs/components/pages/home-shared/FeaturesBento.tsx @@ -0,0 +1,38 @@ +import type { Features } from "../../../content/features"; +import { FadeIn } from "./FadeIn"; +import { SectionHeader, SectionSubtext } from "./Headings"; +import { FeatureBox } from "./FeatureBox"; + +export function FeaturesBento({ + header, + body, + features, +}: { + header: string; + body: string; + features: Features; +}) { + return ( +
+ + {header} + {body} + +
+ {features.map((feature) => ( + + + + ))} +
+
+ ); +} diff --git a/docs/components/pages/home-shared/GlobalStyles.tsx b/docs/components/pages/home-shared/GlobalStyles.tsx new file mode 100644 index 0000000..5f695f3 --- /dev/null +++ b/docs/components/pages/home-shared/GlobalStyles.tsx @@ -0,0 +1,20 @@ +import Head from "next/head"; + +export function LandingPageGlobalStyles() { + return ( + + + + ); +} diff --git a/docs/components/pages/home-shared/Gradient.tsx b/docs/components/pages/home-shared/Gradient.tsx new file mode 100644 index 0000000..9a03a99 --- /dev/null +++ b/docs/components/pages/home-shared/Gradient.tsx @@ -0,0 +1,47 @@ +import cn from "classnames"; +import gradients from "./gradients.module.css"; + +export function Gradient({ + width = 1000, + height = 200, + opacity, + pink, + blue, + conic, + gray, + className, + small, +}: { + width?: number | string; + height?: number | string; + opacity?: number; + pink?: boolean; + blue?: boolean; + conic?: boolean; + gray?: boolean; + className?: string; + small?: boolean; +}) { + return ( + + ); +} diff --git a/docs/components/pages/home-shared/GradientSectionBorder.tsx b/docs/components/pages/home-shared/GradientSectionBorder.tsx new file mode 100644 index 0000000..ef1e824 --- /dev/null +++ b/docs/components/pages/home-shared/GradientSectionBorder.tsx @@ -0,0 +1,37 @@ +import cn from "classnames"; +import { FadeIn } from "./FadeIn"; +import gradients from "../home-shared/gradients.module.css"; + +export function GradientSectionBorder({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ + + + + + {children} +
+ ); +} diff --git a/docs/components/pages/home-shared/Headings.tsx b/docs/components/pages/home-shared/Headings.tsx new file mode 100644 index 0000000..43a5e52 --- /dev/null +++ b/docs/components/pages/home-shared/Headings.tsx @@ -0,0 +1,56 @@ +import cn from "classnames"; +import gradients from "./gradients.module.css"; + +export function HeroText({ + children, + className, + h1, +}: { + children: React.ReactNode; + className?: string; + h1?: boolean; +}) { + const combinedClassname = cn( + gradients.heroHeading, + "font-extrabold tracking-[-0.04em] leading-none text-[40px] md:text-5xl lg:text-[80px] max-w-lg md:max-w-xl lg:max-w-4xl text-center text-transparent", + className + ); + + if (h1) { + return

{children}

; + } + return

{children}

; +} + +export function SectionHeader({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ); +} + +export function SectionSubtext({ + hero, + children, +}: { + hero?: boolean; + children: React.ReactNode; +}) { + const textClasses = hero + ? "text-[20px] lg:text-xl" + : "text-[16px] lg:text-[20px]"; + + return ( +

+ {children} +

+ ); +} diff --git a/docs/components/pages/home-shared/gradients.module.css b/docs/components/pages/home-shared/gradients.module.css new file mode 100644 index 0000000..b9c50a9 --- /dev/null +++ b/docs/components/pages/home-shared/gradients.module.css @@ -0,0 +1,231 @@ +.benchmarkTurbo { + background: linear-gradient(288.43deg, #ff1e56 28.29%, #9c51a1 78.78%); + box-shadow: 0px 0px 16px #f02662; + :global(.light) & { + background: linear-gradient( + 268.86deg, + #ff1e56 -5.68%, + #d67fdc 107.63%, + #9c51a1 107.64% + ); + box-shadow: none; + } +} + +.benchmarkActiveTab { + background: radial-gradient( + 50% 50% at 50% 100%, + rgba(255, 255, 255, 0.2) 0%, + rgba(255, 255, 255, 0) 100% + ), + linear-gradient(0deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.12)), + radial-gradient( + 128.57% 128.57% at 50% 0%, + rgba(255, 255, 255, 0.1) 0%, + rgba(255, 255, 255, 0) 100% + ), + radial-gradient( + 100% 427.04% at 100% 0%, + rgba(255, 255, 255, 0.1) 0%, + rgba(255, 255, 255, 0) 100% + ), + radial-gradient( + 100% 462.63% at 0% 0%, + rgba(255, 255, 255, 0.1) 0%, + rgba(255, 255, 255, 0) 100% + ); + :global(.light) & { + background: linear-gradient( + 0deg, + rgba(255, 255, 255, 0.8), + rgba(255, 255, 255, 0.8) + ), + radial-gradient( + 50% 50% at 49.66% 0%, + rgba(255, 255, 255, 0.1) 0%, + rgba(255, 255, 255, 0) 100% + ); + } +} + +.benchmarkTurboLabel { + background: linear-gradient(288.43deg, #ff1e56 28.29%, #9c51a1 78.78%); + color: transparent; + background-clip: text; + :global(.light) & { + background: linear-gradient( + 268.86deg, + #ff1e56 -5.68%, + #d67fdc 107.63%, + #9c51a1 107.64% + ), + linear-gradient(288.43deg, #ff1e56 28.29%, #9c51a1 78.78%); + color: transparent; + background-clip: text; + } +} + +.benchmark { + background: linear-gradient(270deg, #5c5c5c 0%, #1f1f1f 100%); + :global(.light) & { + background: linear-gradient(89.98deg, #e0e0e0 0.01%, #9c9c9c 99.49%); + } +} + +.barBorder { + border: rgba(255, 255, 255, 0.4) 1px solid; + :global(.light) & { + border: rgba(0, 0, 0, 0.6) 1px solid; + } +} + +.tooltipArrow { + display: block; + border-left: 8px solid transparent; + border-bottom: 8px solid #333333; + border-right: 8px solid transparent; + :global(.light) & { + border-bottom: 8px solid #f5f5f5; + } +} +.translatingGlow { + background: linear-gradient(32deg, #2a8af6 0%, #a853ba 50%, #e92a67 100%); + background-size: 200% 200%; + animation: translateGlow 7s linear infinite; + will-change: filter; +} + +@keyframes translateGlow { + 0% { + background-position: -20% -20%; + } + 25% { + background-position: 30% 80%; + } + 50% { + background-position: 110% 110%; + } + 75% { + background-position: 80% 30%; + } + 100% { + background-position: -20% -20%; + } +} + +.turbopackHeaderText { + background: linear-gradient( + 90deg, + rgba(200, 221, 255, 0.75) 0%, + rgba(255, 202, 222, 0.75) 100% + ), + linear-gradient(0deg, #ffffff, #ffffff); + + :global(.light) & { + background: linear-gradient( + 90deg, + rgba(200, 221, 255, 0.1) 0%, + rgba(255, 202, 222, 0.1) 100% + ), + #000000; + background-clip: text; + } + background-clip: text; +} + +.heroHeading { + background: linear-gradient(180deg, #ffffff 0%, #aaaaaa 100%), #ffffff; + :global(.light) & { + background: linear-gradient(180deg, rgba(0, 0, 0, 0.8) 0%, #000000 100%); + background-clip: text; + } + background-clip: text; +} + +.letterLine { + opacity: 0.2; + background: linear-gradient( + 90deg, + #000000 0%, + #ffffff 20%, + #ffffff 80%, + #000000 100% + ); + :global(.light) & { + background: linear-gradient( + 90deg, + #ffffff 0%, + #000000 20%, + #000000 80%, + #ffffff 100% + ); + } +} + +.glow { + mix-blend-mode: normal; + filter: blur(75px); + will-change: filter; +} + +.glowSmall { + filter: blur(32px); +} + +.glowBlue { + background: linear-gradient(180deg, #58a5ff 0%, #a67af4 100%); +} + +.glowPink { + background: linear-gradient(180deg, #ff3358 0%, #ff4fd8 100%); +} + +.glowConic { + background: conic-gradient( + from 180deg at 50% 50%, + #2a8af6 0deg, + #a853ba 180deg, + #e92a67 360deg + ); +} + +.glowGray { + background: rgba(255, 255, 255, 0.15); +} + +.gradientSectionBorder { + --gradient-y-offset: -200px; + --gradient-x-offset: -200px; + --height: 255px; + position: relative; + overflow: hidden; + will-change: filter; +} + +.gradientSectionBorderLeft { + position: absolute; + width: 60vw; + height: var(--height); + left: var(--gradient-x-offset); + top: var(--gradient-y-offset); + background: linear-gradient(180deg, #58a5ff 0%, #a67af4 100%); + border-radius: 100%; + mix-blend-mode: normal; + filter: blur(50px); +} + +.gradientSectionBorderRight { + width: 60vw; + position: absolute; + height: var(--height); + right: var(--gradient-x-offset); + top: var(--gradient-y-offset); + background: linear-gradient(180deg, #ff3358 0%, #ff4fd8 100%); + border-radius: 100%; + mix-blend-mode: normal; + filter: blur(50px); +} + +.gradientSectionBorderDivider { + background: linear-gradient(90deg, #288cf9 0%, #e32c6b 100%); +} diff --git a/docs/components/pages/landing/TurboHeroBackground.tsx b/docs/components/pages/landing/TurboHeroBackground.tsx new file mode 100644 index 0000000..dffa5b6 --- /dev/null +++ b/docs/components/pages/landing/TurboHeroBackground.tsx @@ -0,0 +1,33 @@ +import cn from "classnames"; +import styles from "./turbohero-background.module.css"; + +export function TurboheroBackground(): JSX.Element { + return ( +
+
+
+
+
+
+ ); +} diff --git a/docs/components/pages/landing/Turbopack.tsx b/docs/components/pages/landing/Turbopack.tsx new file mode 100644 index 0000000..1db1076 --- /dev/null +++ b/docs/components/pages/landing/Turbopack.tsx @@ -0,0 +1,27 @@ +import Image from "next/image"; + +export function Turbopack() { + return ( +
+
+
+ +
+
+ +
+
+ ); +} diff --git a/docs/components/pages/landing/Turborepo.tsx b/docs/components/pages/landing/Turborepo.tsx new file mode 100644 index 0000000..022f37f --- /dev/null +++ b/docs/components/pages/landing/Turborepo.tsx @@ -0,0 +1,27 @@ +import Image from "next/image"; + +export function Turborepo() { + return ( +
+
+
+ Turborepo Logo +
+
+ Turborepo Logo +
+
+ ); +} diff --git a/docs/components/pages/landing/index.module.css b/docs/components/pages/landing/index.module.css new file mode 100644 index 0000000..ac18d80 --- /dev/null +++ b/docs/components/pages/landing/index.module.css @@ -0,0 +1,184 @@ +.leftLights::before { + content: ""; + position: absolute; + pointer-events: none; + width: 25%; + height: 900px; + left: -12.5%; + top: calc(50% - 900px / 2 + 151px); + opacity: 0.2; + background: linear-gradient(180deg, #77b8ff 0%, rgba(42, 138, 246, 0.4) 100%); + filter: blur(125px); + transform: rotate(-15deg); + border-bottom-left-radius: 25% 25%; + border-bottom-right-radius: 25% 25%; + border-top-left-radius: 100% 100%; + border-top-right-radius: 100% 100%; + z-index: 200; + will-change: filter; + mix-blend-mode: normal; +} + +.leftLights::after { + content: ""; + position: absolute; + pointer-events: none; + width: 40%; + height: 422px; + left: 0px; + top: calc(50% - 422px / 2 + 298px); + opacity: 0.5; + background: linear-gradient( + 180deg, + rgba(29, 92, 162, 0.2) 0%, + rgba(42, 138, 246, 0.4) 100% + ); + filter: blur(125px); + will-change: filter; + mix-blend-mode: normal; +} + +.rightLights::before { + z-index: 200; + content: ""; + position: absolute; + pointer-events: none; + width: 25%; + height: 900px; + right: -12.5%; + top: calc(50% - 900px / 2 + 151px); + background-image: linear-gradient( + 180deg, + rgba(236, 151, 207, 0.4) 0%, + rgba(233, 42, 103, 1) 100% + ); + filter: blur(125px); + transform: rotate(15deg); + border-bottom-left-radius: 25% 25%; + border-bottom-right-radius: 25% 25%; + border-top-left-radius: 100% 100%; + border-top-right-radius: 100% 100%; + opacity: 0.2; + overflow: hidden; + will-change: filter; + mix-blend-mode: normal; +} + +.rightLights::after { + content: ""; + position: absolute; + pointer-events: none; + width: 40%; + height: 422px; + right: 0px; + top: calc(50% - 422px / 2 + 298px); + opacity: 0.25; + + background: linear-gradient( + 180deg, + rgba(236, 151, 207, 0.4) 0%, + rgba(233, 42, 103, 1) 100% + ); + transform: matrix(-1, 0, 0, 1, 0, 0); + filter: blur(125px); + will-change: filter; + mix-blend-mode: normal; +} + +.counter-border { + --border-radius: 12px; + --border-size: 1px; + --padding: 1px; + --border-bg: conic-gradient( + from 180deg at 50% 50%, + #e92a67 0deg, + #a853ba 112.5deg, + #2a8af6 228.75deg, + rgba(42, 138, 246, 0) 360deg + ); + position: relative; + overflow: hidden; + font-size: 2rem; + padding: calc(var(--padding) + var(--border-size)); + border-radius: var(--border-radius); + display: inline-block; + z-index: 0; + backface-visibility: hidden; + perspective: 1000; + transform: translate3d(0, 0, 0); +} + +.counter-border:hover { + cursor: pointer; +} + +.counter-border i { + content: ""; + position: absolute; + top: var(--border-size); + right: var(--border-size); + bottom: var(--border-size); + left: var(--border-size); + padding: var(--border-size); + mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + mask-composite: exclude; + z-index: -1; + border-radius: calc(var(--border-radius) + var(--border-size)); +} + +.counter-border i::before { + content: ""; + display: block; + background: var(--border-bg); + box-shadow: 0px 0px 40px 20px --var(--border-bg); + width: calc(100% * 1.41421356237); + padding-bottom: calc(100% * 1.41421356237); + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + border-radius: 100%; + z-index: -2; + animation: spin 5s linear infinite; +} + +@media (prefers-reduced-motion) { + .counter-border i::before { + animation: none; + } +} + +@keyframes spin { + from { + transform: translate(-50%, -50%) rotate(360deg); + } + to { + transform: translate(-50%, -50%) rotate(0); + } +} + +.leftBottomLights { + position: absolute; + width: 387px; + height: 404px; + left: calc(50% - 387px / 2 - 80px); + bottom: -32px; + background: linear-gradient(180deg, #58a5ff 0%, #a67af4 100%); + mix-blend-mode: normal; + opacity: 0.15; + filter: blur(50px); + will-change: filter; +} + +.rightBottomLights { + position: absolute; + width: 387px; + height: 404px; + left: calc(50% - 387px / 2 + 81px); + bottom: -32px; + background: linear-gradient(180deg, #ff3358 0%, #ff4fd8 100%); + mix-blend-mode: normal; + opacity: 0.15; + filter: blur(50px); + will-change: filter; +} diff --git a/docs/components/pages/landing/index.tsx b/docs/components/pages/landing/index.tsx new file mode 100644 index 0000000..deb63f3 --- /dev/null +++ b/docs/components/pages/landing/index.tsx @@ -0,0 +1,199 @@ +import React from "react"; +import Head from "next/head"; +import cn from "classnames"; +import Link from "next/link"; +import { motion } from "framer-motion"; +import { Clients } from "../../clients/Clients"; +import { Marquee } from "../../clients/Marquee"; +import { TurboheroBackground } from "./TurboHeroBackground"; +import { Turborepo } from "./Turborepo"; +import { Turbopack } from "./Turbopack"; +import { FadeIn } from "../home-shared/FadeIn"; +import { LandingPageGlobalStyles } from "../home-shared/GlobalStyles"; +import styles from "./index.module.css"; +import PackLogo from "../../logos/PackLogo"; +import RepoLogo from "../../logos/RepoLogo"; + +function Background() { + return ( +
+
+ + + + + +
+ ); +} + +export function CardBadge({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +const variants = { + hidden: { opacity: 0 }, + active: { opacity: 1 }, +}; + +function Card({ + alt, + href, + title, + icon: Icon, + className, + children, +}: { + href: string; + icon: React.ElementType; + title: "repo" | "pack"; + alt?: string; + className?: string; + children: React.ReactNode; +}) { + const [hovering, setHovering] = React.useState(false); + return ( + setHovering(true)} + onMouseLeave={() => setHovering(false)} + > + +
+
+ +
+ +
+ {title == "pack" ? ( + + ) : ( + + )} + {children} +
+
+ + ); +} + +function SiteCards() { + return ( +
+ + +

+ High-performance build system for JavaScript and TypeScript + codebases. +

+
+
+ + +
+ alpha +
+

+ Introducing the Rust-powered successor to Webpack. +

+
+
+
+ ); +} + +function Teams() { + return ( +
+

+ Trusted by teams from +
around the world +

+
+ +
+
+ ); +} + +function LandingPage() { + return ( + <> + +
+ + +

+ Make Ship Happen +

+

+ Turbo is an incremental bundler and build system optimized for + JavaScript and TypeScript, written in Rust. +

+
+ + + + +
+ + ); +} + +export default LandingPage; diff --git a/docs/components/pages/landing/turbohero-background.module.css b/docs/components/pages/landing/turbohero-background.module.css new file mode 100644 index 0000000..8f157e1 --- /dev/null +++ b/docs/components/pages/landing/turbohero-background.module.css @@ -0,0 +1,108 @@ +.container { + position: absolute; + z-index: -6; + overflow: hidden; + inset: 0; + transition: perspective 3000ms ease 0s; +} + +.lines { + --right: #f8cde8; + --left: #b9ddff; + position: absolute; + width: 200vw; + margin-left: -50%; + transform: translateY(0); + background-image: linear-gradient( + to right, + var(--left) 45%, + rgba(0, 0, 0, 0) 50%, + var(--right) 55% + ); + mask-image: linear-gradient( + to right, + rgba(0, 0, 0, 1) 2px, + rgba(0, 0, 0, 0) 1px + ), + linear-gradient(to bottom, rgba(0, 0, 0, 1) 2px, rgba(0, 0, 0, 0) 1px); + mask-size: 60px 60px; + overflow: hidden; + mask-repeat: repeat repeat; + display: flex; + align-items: center; + justify-content: center; + inset: -100% 0px; + background-position-y: 100%; + mask-position: 50% 0px; + animation: go-up 60s linear infinite; +} + +@media (min-width: 1024px) { + .lines { + animation-duration: 30s; + mask-size: 80px 80px; + } +} + +:global(.dark) .lines { + --right: #4c2638; + --left: #223b67; +} + +@keyframes go-up { + 0% { + transform: translateY(0); + } + + 100% { + transform: translateY(calc(50% + 28px)); + } +} + +.pulse::before { + content: ""; + position: absolute; + inset: 0px; + animation: pulse-frames ease-out 8s infinite; + animation-delay: 0s; + background: rgba(0, 0, 0, 0) + linear-gradient( + to top, + rgba(0, 0, 0, 0) 45%, + var(--pulse-color) 50%, + rgba(0, 0, 0, 0) 90% + ) + no-repeat; + z-index: 211; + animation-delay: var(--delay); +} + +@keyframes pulse-frames { + 0% { + transform: translateY(0%); + } + 50% { + transform: translateY(200%); + } + 100% { + transform: translateY(200%); + } +} + +@media (prefers-reduced-motion) { + .lines { + animation: none; + } + .pulse::before { + animation: none; + } +} + +@media (prefers-reduced-motion) { + .lines { + animation: none; + } + .pulse::before { + animation: none; + } +} diff --git a/docs/components/pages/pack-home/DocsBenchmarkStat.tsx b/docs/components/pages/pack-home/DocsBenchmarkStat.tsx new file mode 100644 index 0000000..5b8dcf6 --- /dev/null +++ b/docs/components/pages/pack-home/DocsBenchmarkStat.tsx @@ -0,0 +1,53 @@ +import benchmarkData from "./benchmark-data/data.json"; + +type StatFunc = (data: typeof benchmarkData) => string; + +/** + * Replace with satisfies keyword when TS 4.9 drops + */ +const satisfies = + () => + (t: U) => + t; + +const formatToSeconds = (seconds: number) => `${seconds.toFixed(1)}s`; +const formatPercentage = (percentage: number) => `${percentage.toFixed(1)}x`; + +const stats = satisfies>()({ + "next12-cold-1000": (data) => formatToSeconds(data.cold[1000].next12), + "turbopack-cold-1000": (data) => formatToSeconds(data.cold[1000].next13), + "turbopack-cold-vs-next12": (data) => + formatPercentage(data.cold[1000].next12 / data.cold[1000].next13), + "turbopack-cold-vs-next12-30000": (data) => + formatPercentage(data.cold[30000].next12 / data.cold[30000].next13), + "turbopack-update-vs-next12": (data) => + formatPercentage( + data.file_change[1000].next12 / data.file_change[1000].next13 + ), + "turbopack-update-vs-next12-30000": (data) => + formatPercentage( + data.file_change[30000].next12 / data.file_change[30000].next13 + ), + "vite-cold-1000": (data) => formatToSeconds(data.cold[1000].vite), + "turbopack-cold-vs-vite": (data) => + formatPercentage(data.cold[1000].vite / data.cold[1000].next13), + "turbopack-cold-vs-vite-30000": (data) => + formatPercentage(data.cold[30000].vite / data.cold[30000].next13), + "turbopack-update-vs-vite": (data) => + formatPercentage( + data.file_change[1000].vite / data.file_change[1000].next13 + ), + "turbopack-update-vs-vite-30000": (data) => + formatPercentage( + data.file_change[30000].vite / data.file_change[30000].next13 + ), +}); + +type Stat = keyof typeof stats; + +export function DocsBenchmarkStat(props: { stat: Stat }) { + if (!stats[props.stat]) { + throw new Error(`Invalid stat: ${props.stat}`); + } + return stats[props.stat](benchmarkData); +} diff --git a/docs/components/pages/pack-home/DocsBenchmarksGraph.tsx b/docs/components/pages/pack-home/DocsBenchmarksGraph.tsx new file mode 100644 index 0000000..7fb9d55 --- /dev/null +++ b/docs/components/pages/pack-home/DocsBenchmarksGraph.tsx @@ -0,0 +1,31 @@ +import { useState } from "react"; +import { + BenchmarkBar, + BenchmarkCategory, + BenchmarkNumberOfModules, +} from "./PackBenchmarks"; +import { BenchmarksGraph } from "./PackBenchmarksGraph"; +import { PackBenchmarksPicker } from "./PackBenchmarksPicker"; + +export function DocsBenchmarksGraph(props: { + bars: BenchmarkBar[]; + category: BenchmarkCategory; +}) { + const [numberOfModules, setNumberOfModules] = + useState("1000"); + return ( +
+ +
+ +
+
+ ); +} diff --git a/docs/components/pages/pack-home/PackBenchmarkTabs.tsx b/docs/components/pages/pack-home/PackBenchmarkTabs.tsx new file mode 100644 index 0000000..f771fb1 --- /dev/null +++ b/docs/components/pages/pack-home/PackBenchmarkTabs.tsx @@ -0,0 +1,149 @@ +import { useRef, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { BenchmarkCategory } from "./PackBenchmarks"; +import classNames from "classnames"; +import gradients from "../home-shared/gradients.module.css"; + +const TABS: { + id: BenchmarkCategory; + title: string; + soon: boolean; + tooltip: string; +}[] = [ + { + id: "cold", + title: "Cold Start", + soon: false, + tooltip: "First run", + }, + { + id: "file_change", + title: "File Change", + soon: false, + tooltip: "Hot Reload (HMR)", + }, + { + id: "code_build", + title: "Code Build", + soon: true, + tooltip: "First Build", + }, + { + id: "build_from_cache", + title: "Build from Cache", + soon: true, + tooltip: "Second Build", + }, +]; + +const TRANSITION = { + duration: 0.3, + ease: [0.59, 0.15, 0.18, 0.93], +}; + +function SoonBadge() { + return ( + + Soon + + ); +} + +export function PackBenchmarkTabs({ + onTabChange, +}: { + onTabChange: (tab: BenchmarkCategory) => void; +}) { + const [activeTab, setActiveTab] = useState(0); + + const onTabClick = (index: number) => { + if (TABS[index].soon) return; + setActiveTab(index); + onTabChange(TABS[index].id); + }; + + return ( +
+
+ +
+ {TABS.map((tab, index) => ( + + ))} +
+
+
+
+ ); +} + +function ToolTip({ text, children }: { text; children: React.ReactNode }) { + const [show, setShow] = useState(false); + const timeout = useRef(); + + const onMouseEnter = () => { + timeout.current = setTimeout(() => { + setShow(true); + }, 800); + }; + + const onMouseLeave = () => { + clearTimeout(timeout.current); + setShow(false); + }; + + return ( +
+ +
+
+

{text}

+
+ +
{children}
+
+ ); +} diff --git a/docs/components/pages/pack-home/PackBenchmarks.tsx b/docs/components/pages/pack-home/PackBenchmarks.tsx new file mode 100644 index 0000000..9bd1db8 --- /dev/null +++ b/docs/components/pages/pack-home/PackBenchmarks.tsx @@ -0,0 +1,97 @@ +import { useState } from "react"; +import { FadeIn } from "../home-shared/FadeIn"; +import { SectionHeader, SectionSubtext } from "../home-shared/Headings"; +import { BenchmarksGraph } from "./PackBenchmarksGraph"; +import { PackBenchmarksPicker } from "./PackBenchmarksPicker"; +import { PackBenchmarkTabs } from "./PackBenchmarkTabs"; + +export type BenchmarkNumberOfModules = "1000" | "5000" | "10000" | "30000"; +export type BenchmarkCategory = + | "cold" + | "from_cache" + | "file_change" + | "code_build" + | "build_from_cache"; +export interface BenchmarkData { + next13: number; + next12: number; + vite: number; + next11: number; +} + +export interface BenchmarkBar { + label: string; + key: keyof BenchmarkData; + turbo?: true; + swc?: true; +} + +export const DEFAULT_BARS: BenchmarkBar[] = [ + { + key: "next13", + label: "Next.js 13", + turbo: true, + }, + { + key: "next12", + label: "Next.js 12", + }, + { + key: "vite", + label: "Vite", + swc: true, + }, + { + key: "next11", + label: "Next.js 11", + }, +]; +export const HMR_BARS: BenchmarkBar[] = [ + { + key: "next13", + label: "Next.js 13", + turbo: true, + }, + { + key: "vite", + label: "Vite", + swc: true, + }, + { + key: "next12", + label: "Next.js 12", + }, + { + key: "next11", + label: "Next.js 11", + }, +]; + +export function PackBenchmarks() { + const [numberOfModules, setNumberOfModules] = + useState("1000"); + const [category, setCategory] = useState("cold"); + + return ( + +
+ Faster Than Fast + + Crafted by the creators of Webpack, Turbopack delivers unparalleled + performance at scale. + +
+
+ + +
+ +
+ ); +} diff --git a/docs/components/pages/pack-home/PackBenchmarksGraph.tsx b/docs/components/pages/pack-home/PackBenchmarksGraph.tsx new file mode 100644 index 0000000..a4792b8 --- /dev/null +++ b/docs/components/pages/pack-home/PackBenchmarksGraph.tsx @@ -0,0 +1,333 @@ +import cn from "classnames"; +import { + animate, + motion, + useInView, + useAnimation, + AnimationPlaybackControls, +} from "framer-motion"; +import Image from "next/image"; +import { useEffect, useRef, useState } from "react"; +import benchmarkData from "./benchmark-data/data.json"; +import { Gradient } from "../home-shared/Gradient"; +import gradients from "../home-shared/gradients.module.css"; +import { + BenchmarkBar, + BenchmarkCategory, + BenchmarkData, + BenchmarkNumberOfModules, +} from "./PackBenchmarks"; + +interface BenchmarksGraphProps { + category: BenchmarkCategory; + numberOfModules: BenchmarkNumberOfModules; + bars: BenchmarkBar[]; + pinTime?: true; +} + +export function BenchmarksGraph({ + category, + numberOfModules, + bars, + pinTime, +}: BenchmarksGraphProps) { + const data: BenchmarkData = benchmarkData[category][numberOfModules]; + const keys = bars.map((bar) => bar.key); + const longestTime = Math.max(...keys.map((key) => data[key])) * 1000; + const longestTimeWithPadding = longestTime * 1.15; + const graphRef = useRef(null); + const graphInView = useInView(graphRef, { once: true, margin: "-128px" }); + + return ( +
+
+ +
+
+ {bars.map((bar) => { + return ( + + } + duration={data[bar.key] * 1000} + longestTime={longestTimeWithPadding} + inView={graphInView} + pinTime={pinTime} + > + ); + })} +
+
+ ); +} + +const START_DELAY = 0.0; + +const graphBarVariants = { + initial: { + width: 0, + }, + progress: { + width: "100%", + }, +}; + +const graphBarWrapperVariants = { + hidden: { + opacity: 0, + }, + show: { + opacity: 1, + }, +}; + +function GraphBar({ + turbo, + duration, + longestTime, + inView, + Label, + pinTime, +}: { + turbo?: boolean; + duration: number; + longestTime: number; + Label: JSX.Element; + inView?: boolean; + // Pin the time + pinTime?: true; +}) { + const controls = useAnimation(); + const [timer, setTimer] = useState(0); + const [timerAnimation, setTimerAnimation] = + useState(); + const [barWidth, setBarWidth] = useState(0); + const [, setFinished] = useState(false); + + async function stopAnimation() { + timerAnimation && timerAnimation.stop(); + controls.stop(); + } + + async function resetAnimation() { + setTimer(0); + setFinished(false); + await controls.start("initial"); + } + + async function startAnimation() { + const transition = { + duration: duration / 1000, + delay: START_DELAY, + }; + setBarWidth((duration / longestTime) * 100); + await controls.start("show"); + controls + .start("progress", { + ...transition, + ease: "linear", + }) + .then(() => { + setFinished(true); + }); + const timerAnimationRef = animate(0, duration, { + ...transition, + ease: "linear", + onUpdate(value) { + setTimer(value); + }, + }); + setTimerAnimation(timerAnimationRef); + } + + async function playFullAnimation() { + await stopAnimation(); + await controls.start("hidden"); + await resetAnimation(); + await startAnimation(); + } + + useEffect(() => { + if (inView) { + void startAnimation(); + } else { + void stopAnimation(); + void resetAnimation(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [inView]); + + useEffect(() => { + if (!inView) return; + void playFullAnimation(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [duration, longestTime]); + + return ( +
+
{Label}
+
+ + + + + + +
+
+ ); +} + +const GraphTimer = ({ + turbo, + timer, + duration, +}: { + turbo: boolean; + timer: number; + duration: number; +}) => { + return ( +
+ {turbo && ( +
+ Turbopack + Turbopack + +
+ )} +

+

+
+ ); +}; + +function roundTo(num: number, decimals: number) { + const factor = Math.pow(10, decimals); + return Math.round(num * factor) / factor; +} + +const Time = ({ + value, + maxValue, +}: { + value: number; + maxValue: number; +}): JSX.Element => { + let unitValue: string; + let unit: string; + if (maxValue < 1000) { + unitValue = Math.round(value).toFixed(0); + unit = "ms"; + } else { + const roundedValue = roundTo(value / 1000, 1); + unitValue = roundedValue.toFixed(1); + unit = "s"; + } + + return ( + <> + {unitValue} + {unit} + + ); +}; + +function GraphLabel({ + label, + turbo, + swc, + mobileOnly, + esbuild, +}: { + label: string; + turbo?: boolean; + swc?: boolean; + mobileOnly?: boolean; + esbuild?: boolean; +}) { + return ( +
+

{label}

+ {turbo && ( +

+ turbo +

+ )} + {swc && ( +

+ with SWC +

+ )} + {esbuild && ( +

esbuild

+ )} +
+ ); +} diff --git a/docs/components/pages/pack-home/PackBenchmarksPicker.tsx b/docs/components/pages/pack-home/PackBenchmarksPicker.tsx new file mode 100644 index 0000000..dd3fc15 --- /dev/null +++ b/docs/components/pages/pack-home/PackBenchmarksPicker.tsx @@ -0,0 +1,20 @@ +import { BenchmarkNumberOfModules } from "./PackBenchmarks"; +import { PackDropdown } from "./PackDropdown"; + +export function PackBenchmarksPicker(props: { + setNumberOfModules: (num: BenchmarkNumberOfModules) => void; +}) { + return ( +
+ ); +} diff --git a/docs/components/pages/pack-home/PackDropdown.tsx b/docs/components/pages/pack-home/PackDropdown.tsx new file mode 100644 index 0000000..7ff5d76 --- /dev/null +++ b/docs/components/pages/pack-home/PackDropdown.tsx @@ -0,0 +1,117 @@ +import { useState, Fragment } from "react"; +import { Listbox, Transition } from "@headlessui/react"; +import { BenchmarkNumberOfModules } from "./PackBenchmarks"; + +export function PackDropdown({ + onOptionSelected, +}: { + onOptionSelected: (option: BenchmarkNumberOfModules) => void; +}) { + const [selectedOption, setSelectedOption] = + useState("1000"); + + const onSelect = (option: BenchmarkNumberOfModules) => { + onOptionSelected(option); + setSelectedOption(option); + }; + + return ( +
+ + + {Number(selectedOption).toLocaleString()} + + + + + + + `relative cursor-default select-none py-1 text-sm pl-3 text-gray-400 ${ + active ? "bg-gray-800 text-gray-100" : "text-gray-900" + }` + } + > + 1000 + + + `relative cursor-default select-none py-1 text-sm pl-3 text-gray-400 ${ + active ? "bg-gray-800 text-gray-100" : "text-gray-900" + }` + } + value="5000" + > + 5000 + + + `relative cursor-default select-none py-1 text-sm pl-3 text-gray-400 ${ + active ? "bg-gray-800 text-gray-100" : "text-gray-900" + }` + } + value="10000" + > + 10000 + + + `relative cursor-default select-none py-1 text-sm pl-3 text-gray-400 ${ + active ? "bg-gray-800 text-gray-100" : "text-gray-900" + }` + } + value="30000" + > + 30000 + + + + +
+ ); +} + +function BenchmarkOption({ + value, + onSelect, +}: { + value: BenchmarkNumberOfModules; + onSelect: (value: string) => void; +}) { + return ( +
onSelect(value)} + > +

+ {Number(value).toLocaleString()} +

+
+ ); +} + +function Arrow() { + return ( + + + + ); +} diff --git a/docs/components/pages/pack-home/PackFeatures.tsx b/docs/components/pages/pack-home/PackFeatures.tsx new file mode 100644 index 0000000..b668153 --- /dev/null +++ b/docs/components/pages/pack-home/PackFeatures.tsx @@ -0,0 +1,12 @@ +import { PACK_HOME_FEATURES } from "../../../content/features"; +import { FeaturesBento } from "../home-shared/FeaturesBento"; + +export function PackFeatures() { + return ( + + ); +} diff --git a/docs/components/pages/pack-home/PackHero.tsx b/docs/components/pages/pack-home/PackHero.tsx new file mode 100644 index 0000000..eb5b348 --- /dev/null +++ b/docs/components/pages/pack-home/PackHero.tsx @@ -0,0 +1,115 @@ +import cn from "classnames"; +import Image from "next/image"; +import Link from "next/link"; +// import { Marquee } from "../../clients/Marquee"; +// import { Clients } from "../../clients/Clients"; +import gradients from "../home-shared/gradients.module.css"; +import { HeroText, SectionSubtext } from "../home-shared/Headings"; +import { Gradient } from "../home-shared/Gradient"; +import { FadeIn } from "../home-shared/FadeIn"; +import { CTAButton } from "../home-shared/CTAButton"; +import PackLogo from "../../logos/PackLogo"; + +export function PackHero() { + return ( + <> + + +
+ Turbopack + Turbopack +
+
+ +
+ +
+ + +
+
+ +
+ + + The Rust-powered successor to Webpack + + Turbopack is an incremental bundler optimized for JavaScript and + TypeScript, written in Rust. + + + + +

License: MPL-2.0

+ + +
+ + + + ); +} diff --git a/docs/components/pages/pack-home/PackLetter.tsx b/docs/components/pages/pack-home/PackLetter.tsx new file mode 100644 index 0000000..d4b0f2f --- /dev/null +++ b/docs/components/pages/pack-home/PackLetter.tsx @@ -0,0 +1,114 @@ +import { HeroText } from "../home-shared/Headings"; +import Image from "next/image"; +import cn from "classnames"; +import gradients from "../home-shared/gradients.module.css"; +import { FadeIn } from "../home-shared/FadeIn"; +import { CTAButton } from "../home-shared/CTAButton"; +import Link from "next/link"; +import { Gradient } from "../home-shared/Gradient"; + +export function PackLetter() { + return ( +
+ + + Let's move +
+ the web forward +
+
+
+ +

+ It's time for a new beginning in compiler infrastructure for + the entire web ecosystem. Webpack has been downloaded over 3 billion + times. It's become an integral part of building for the web. + But just like Babel and Terser, it's time to go all-in on + native. I joined Vercel and assembled a team of world class + engineers to build the web's next generation bundler. +

+
+

+ This team has taken lessons from 10 years of Webpack, combined with + the innovations in incremental computation from Turborepo and + Google's Bazel, and invented an architecture ready to withstand + the next 10 years. +

+
+

+ With that, we're excited to introduce Turbopack, our + Rust-powered successor to Webpack. It will harness the power of our + build system, Turborepo, for massive performance improvements. + Turbopack is the new foundation of high-performance bare-metal + tooling and is now open source—we're excited to share it with + you. +

+
+ + + + +
+ Image of Tobias Koopers +
+
+ Tobias Koppers hand written signature + Tobias Koppers hand written signature +
+

Tobias Koppers

+

Creator of Webpack

+
+
+
+
+ +
+ + + Start Building + + +
+ +
+
+ ); +} diff --git a/docs/components/pages/pack-home/benchmark-data/README.md b/docs/components/pages/pack-home/benchmark-data/README.md new file mode 100644 index 0000000..9b53481 --- /dev/null +++ b/docs/components/pages/pack-home/benchmark-data/README.md @@ -0,0 +1,7 @@ +# `turbopack` Benchmark Data + +- `bench_startup`: Time from cold start of the bundler to the browser successfully retrieving bundled scripts. This does not include react hydration time. +- `bench_hydration`: Time from cold start of the bundler to the browser successfully retrieving bundled scripts. This does wait until react hydration has completed. +- `bench_restart`: Before measuring: warms up any available persistent cache (we don’t have one yet) by performing the equivalent of the bench_hydration benchmark, shuts down the server. Then, times another bench_hydration. +- `bench_hmr_to_eval`: Measures the time it takes from an incremental change to be made, bundled, sent over hmr, and evaluated by the browser. +- `bench_hmr_to_commit`: Measures the time it takes from an incremental change to be made, bundled, sent over hmr, evaluated by the browser, and committed by React (runs a useEffect). diff --git a/docs/components/pages/pack-home/benchmark-data/data.json b/docs/components/pages/pack-home/benchmark-data/data.json new file mode 100644 index 0000000..f88ae6f --- /dev/null +++ b/docs/components/pages/pack-home/benchmark-data/data.json @@ -0,0 +1,54 @@ +{ + "cold": { + "1000": { + "next13": 1.38187759, + "vite": 4.19890847, + "next12": 3.64327949, + "next11": 9.19035540 + }, + "5000": { + "next13": 3.99792562, + "vite": 16.59615430, + "next12": 12.14057345, + "next11": 32.89712268 + }, + "10000": { + "next13": 7.34248178, + "vite": 32.25177941, + "next12": 23.27525035, + "next11": 71.80680350 + }, + "30000": { + "next13": 21.97034306, + "vite": 97.74466099, + "next12": 89.07274544, + "next11": 237.61188540 + } + }, + "file_change": { + "1000": { + "next13": 0.01890358, + "vite": 0.10476515, + "next12": 0.14617346, + "next11": 0.21155549 + }, + "5000": { + "next13": 0.02379283, + "vite": 0.10963156, + "next12": 0.49470051, + "next11": 0.86600602 + }, + "10000": { + "next13": 0.02302405, + "vite": 0.11295908, + "next12": 1.15193035, + "next11": 2.35675312 + }, + "30000": { + "next13": 0.02246753, + "vite": 0.13328557, + "next12": 6.40370549, + "next11": 9.50431942 + } + } +} diff --git a/docs/components/pages/pack-home/index.tsx b/docs/components/pages/pack-home/index.tsx new file mode 100644 index 0000000..db73b25 --- /dev/null +++ b/docs/components/pages/pack-home/index.tsx @@ -0,0 +1,24 @@ +import { PackBenchmarks } from "./PackBenchmarks"; +import { PackHero } from "./PackHero"; +import { PackLetter } from "./PackLetter"; +import { PackFeatures } from "./PackFeatures"; +import { GradientSectionBorder } from "../home-shared/GradientSectionBorder"; +import { LandingPageGlobalStyles } from "../home-shared/GlobalStyles"; + +export default function Home() { + return ( + <> + +
+ + + + + + + + +
+ + ); +} diff --git a/docs/components/pages/repo-home/RepoFeatures.tsx b/docs/components/pages/repo-home/RepoFeatures.tsx new file mode 100644 index 0000000..4499725 --- /dev/null +++ b/docs/components/pages/repo-home/RepoFeatures.tsx @@ -0,0 +1,15 @@ +import { REPO_HOME_FEATURES } from "../../../content/features"; +import { FadeIn } from "../home-shared/FadeIn"; +import { FeaturesBento } from "../home-shared/FeaturesBento"; + +export function RepoFeatures() { + return ( + + + + ); +} diff --git a/docs/components/pages/repo-home/RepoHero.tsx b/docs/components/pages/repo-home/RepoHero.tsx new file mode 100644 index 0000000..a63777a --- /dev/null +++ b/docs/components/pages/repo-home/RepoHero.tsx @@ -0,0 +1,114 @@ +import cn from "classnames"; +import Image from "next/image"; +import Link from "next/link"; +import gradients from "../home-shared/gradients.module.css"; +import { HeroText, SectionSubtext } from "../home-shared/Headings"; +import { Gradient } from "../home-shared/Gradient"; +import { FadeIn } from "../home-shared/FadeIn"; +import { CTAButton } from "../home-shared/CTAButton"; +import RepoLogo from "../../logos/RepoLogo"; + +export function RepoHero() { + return ( + <> + + +
+ {/* TODO: On dark mode, there should be a "breathing" gradient inside the inner circle */} + Turborepo + Turborepo +
+
+ +
+ +
+ + +
+
+ +
+ + + The build system that makes ship happen + + Turborepo is a high-performance build system for JavaScript and + TypeScript codebases. + + + +
+ + + Get Started + + + + + GitHub + + +
+

License: MPL-2.0

+
+ +
+ + + + ); +} diff --git a/docs/components/pages/repo-home/RepoLetter.tsx b/docs/components/pages/repo-home/RepoLetter.tsx new file mode 100644 index 0000000..7093a3a --- /dev/null +++ b/docs/components/pages/repo-home/RepoLetter.tsx @@ -0,0 +1,118 @@ +import { HeroText } from "../home-shared/Headings"; +import Image from "next/image"; +import cn from "classnames"; +import gradients from "../home-shared/gradients.module.css"; +import { FadeIn } from "../home-shared/FadeIn"; +import { CTAButton } from "../home-shared/CTAButton"; +import Link from "next/link"; +import { Gradient } from "../home-shared/Gradient"; + +export function RepoLetter() { + return ( +
+ + + Scaling your Codebase +
+ shouldn't be so difficult +
+
+
+ +

+ The bigger your project grows, the slower it gets. Tasks like + linting, testing, and building begin to take enormous amounts of + time. +

+
+

+ If you're serving multiple applications, you might reach for a + monorepo. They're incredible for productivity, especially on + the frontend, but the tooling can be a nightmare. There's a lot + of stuff to do (and things to mess up). Nothing “just + works.” It's become completely normal to waste entire + days or weeks on plumbing—tweaking configs, writing one-off scripts, + and stitching stuff together. +

+
+

We need something else.

+

+

+ A fresh take on the whole setup. Designed to glue everything + together. A toolchain that works for you and not against you. With + sensible defaults, but even better escape hatches. Built with the + same techniques used by the big guys, but in a way that doesn't + require PhD to learn or a staff to maintain. +

+
+

With Turborepo, we're doing just that.

+
+

+ We're building a build system that can keep up with your team. + You'll see your CI get faster, duplicated work get cut, and + your NPM scripts get simpler. You'll get a world-class + development environment, without the maintenance burden. +

+
+ + + + +
+ Image of Jared Palmer +
+
+ Jared Palmer's hand written signature + Jared Palmer's hand written signature +
+

Jared Palmer

+

Founder of Turborepo

+
+
+
+
+ +
+ + + Start Building + + +
+ +
+
+ ); +} diff --git a/docs/components/pages/repo-home/index.tsx b/docs/components/pages/repo-home/index.tsx new file mode 100644 index 0000000..992a614 --- /dev/null +++ b/docs/components/pages/repo-home/index.tsx @@ -0,0 +1,22 @@ +import { RepoHero } from "./RepoHero"; +import { RepoFeatures } from "./RepoFeatures"; +import { RepoLetter } from "./RepoLetter"; +import { GradientSectionBorder } from "../home-shared/GradientSectionBorder"; +import { LandingPageGlobalStyles } from "../home-shared/GlobalStyles"; + +export default function Home() { + return ( + <> + +
+ + + + + + + +
+ + ); +} diff --git a/docs/components/pages/showcase.tsx b/docs/components/pages/showcase.tsx new file mode 100644 index 0000000..62c8f50 --- /dev/null +++ b/docs/components/pages/showcase.tsx @@ -0,0 +1,47 @@ +/* eslint-disable react/no-unescaped-entities */ +import { Container } from "../Container"; +import { Clients } from "../clients/Clients"; + +export default function Showcase() { + return ( +
+
+
+

+ Showcase +

+

+ Who's using Turbo? +

+

+ Turbo is the one of the fastest growing toolchains in the frontend + ecosystem. It's trusted by thousands of developers in production + including teams at Vercel, AWS, Netflix, Microsoft, Disney, and + more. +

+
+
+ +
+ +
+ +
+
+ Are you using Turbo? +
+ +
+
+
+ ); +} diff --git a/docs/components/useIsomorphicLayoutEffect.tsx b/docs/components/useIsomorphicLayoutEffect.tsx new file mode 100644 index 0000000..c685670 --- /dev/null +++ b/docs/components/useIsomorphicLayoutEffect.tsx @@ -0,0 +1,7 @@ +import * as React from "react"; + +const useIsomorphicLayoutEffect = + typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect; + +/* eslint-disable-next-line import/no-default-export -- TODO: Fix ESLint Error (#13355) */ +export default useIsomorphicLayoutEffect; diff --git a/docs/components/usePrefersReducedMotion.tsx b/docs/components/usePrefersReducedMotion.tsx new file mode 100644 index 0000000..dd9d82c --- /dev/null +++ b/docs/components/usePrefersReducedMotion.tsx @@ -0,0 +1,44 @@ +import { useState, useEffect } from "react"; + +const QUERY = "(prefers-reduced-motion: no-preference)"; +const isRenderingOnServer = typeof window === "undefined"; +/** + * All code here from https://www.joshwcomeau.com/snippets/react-hooks/use-prefers-reduced-motion/ + */ +const getInitialState = () => { + // For our initial server render, we won't know if the user + // prefers reduced motion, but it doesn't matter. This value + // will be overwritten on the client, before any animations + // occur. + return isRenderingOnServer ? true : !window.matchMedia(QUERY).matches; +}; + +/** + * Checks the user's device setting for `prefers-reduced-motion`. + * Use this if you can't use a media query in CSS. + * + * From https://www.joshwcomeau.com/snippets/react-hooks/use-prefers-reduced-motion/ + */ +export function usePrefersReducedMotion(): boolean { + const [prefersReducedMotion, setPrefersReducedMotion] = + useState(getInitialState); + useEffect(() => { + const mediaQueryList = window.matchMedia(QUERY); + const listener = (event: MediaQueryListEvent) => { + setPrefersReducedMotion(!event.matches); + }; + if (mediaQueryList.addEventListener) { + mediaQueryList.addEventListener("change", listener); + } else { + mediaQueryList.addListener(listener); + } + return () => { + if (mediaQueryList.removeEventListener) { + mediaQueryList.removeEventListener("change", listener); + } else { + mediaQueryList.removeListener(listener); + } + }; + }, []); + return prefersReducedMotion; +} -- cgit v1.2.3-70-g09d2