diff --git a/docs/app/[lang]/(home)/components/frameworks.tsx b/docs/app/[lang]/(home)/components/frameworks.tsx index 2c9d68deda..0f0b3ffbc6 100644 --- a/docs/app/[lang]/(home)/components/frameworks.tsx +++ b/docs/app/[lang]/(home)/components/frameworks.tsx @@ -1,12 +1,11 @@ -'use client'; +"use client"; -import { track } from '@vercel/analytics'; -import Link from 'next/link'; -import type { ComponentProps } from 'react'; -import { toast } from 'sonner'; -import { Badge } from '@/components/ui/badge'; +import { track } from "@vercel/analytics"; +import Link from "next/link"; +import type { ComponentProps } from "react"; +import { toast } from "sonner"; -export const Express = (props: ComponentProps<'svg'>) => ( +export const Express = (props: ComponentProps<"svg">) => ( ) => ( ); -export const Fastify = (props: ComponentProps<'svg'>) => ( +export const Fastify = (props: ComponentProps<"svg">) => ( ) => ( ); -export const AstroDark = (props: ComponentProps<'svg'>) => ( +export const AstroDark = (props: ComponentProps<"svg">) => ( ) => ( ); -export const AstroLight = (props: ComponentProps<'svg'>) => ( +export const AstroLight = (props: ComponentProps<"svg">) => ( ) => ( ); -export const AstroGray = (props: ComponentProps<'svg'>) => ( +export const AstroGray = (props: ComponentProps<"svg">) => ( ) => ( ); -export const TanStack = (props: ComponentProps<'svg'>) => ( +export const TanStack = (props: ComponentProps<"svg">) => ( ) => ( ); -export const TanStackGray = (props: ComponentProps<'svg'>) => ( +export const TanStackGray = (props: ComponentProps<"svg">) => ( + TanStack ) => ( ); -export const Vite = (props: ComponentProps<'svg'>) => ( +export const Vite = (props: ComponentProps<"svg">) => ( ) => ( ); -export const Nitro = (props: ComponentProps<'svg'>) => ( +export const Nitro = (props: ComponentProps<"svg">) => ( ) => ( /> ) => ( ); -export const SvelteKit = (props: ComponentProps<'svg'>) => ( +export const SvelteKit = (props: ComponentProps<"svg">) => ( ) => ( ); -export const SvelteKitGray = (props: ComponentProps<'svg'>) => ( +export const SvelteKitGray = (props: ComponentProps<"svg">) => ( ) => ( ); -export const Nuxt = (props: ComponentProps<'svg'>) => ( +export const Nuxt = (props: ComponentProps<"svg">) => ( ) => ( ); -export const NuxtGray = (props: ComponentProps<'svg'>) => ( +export const NuxtGray = (props: ComponentProps<"svg">) => ( ) => ( ); -export const Hono = (props: ComponentProps<'svg'>) => ( +export const Hono = (props: ComponentProps<"svg">) => ( Hono ) => ( ); -export const HonoGray = (props: ComponentProps<'svg'>) => ( +export const HonoGray = (props: ComponentProps<"svg">) => ( Hono ) => ( ); -export const Bun = (props: ComponentProps<'svg'>) => ( +export const Bun = (props: ComponentProps<"svg">) => ( ) => ( id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" fill="#ccbea7" - style={{ fillRule: 'evenodd' }} + style={{ fillRule: "evenodd" }} /> ) => ( ); -export const BunGray = (props: ComponentProps<'svg'>) => ( +export const BunGray = (props: ComponentProps<"svg">) => ( ) => ( id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" fill="var(--color-background)" - style={{ fillRule: 'evenodd' }} + style={{ fillRule: "evenodd" }} /> ) => ( ); -export const Nest = (props: ComponentProps<'svg'>) => ( +export const Nest = (props: ComponentProps<"svg">) => ( ) => ( ); -export const NestGray = (props: ComponentProps<'svg'>) => ( +export const NestGray = (props: ComponentProps<"svg">) => ( ) => ( ); -export const Next = (props: ComponentProps<'svg'>) => ( +export const Next = (props: ComponentProps<"svg">) => ( Next.js @@ -699,110 +699,97 @@ export const Next = (props: ComponentProps<'svg'>) => ( ); export const Frameworks = () => { - const handleRequest = (framework: string) => { - track('Framework requested', { framework: framework.toLowerCase() }); - toast.success('Request received', { - description: `Thanks for expressing interest in ${framework}. We will be adding support for it soon.`, + const handleRequest = () => { + track("Framework requested", { framework: "tanstack" }); + toast.success("Request received", { + description: + "Thanks for expressing interest in TanStack. We will be adding support for it soon.", }); }; return ( -
-
-

- Universally compatible. Works - with the frameworks you already use with more coming soon. +
+
+

+ Universally Compatible

+

+ Works with the frameworks you already use with more coming soon. +

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
); diff --git a/docs/app/[lang]/(home)/components/hero.tsx b/docs/app/[lang]/(home)/components/hero.tsx index a5eb732c28..86c43580b7 100644 --- a/docs/app/[lang]/(home)/components/hero.tsx +++ b/docs/app/[lang]/(home)/components/hero.tsx @@ -36,7 +36,7 @@ export const Hero = ({ title, description }: HeroProps) => { const Icon = copied ? CheckIcon : CopyIcon; return ( -
+

{title} diff --git a/docs/app/[lang]/(home)/components/tweet-wall.tsx b/docs/app/[lang]/(home)/components/tweet-wall.tsx index aa2777d824..7c2f63ec70 100644 --- a/docs/app/[lang]/(home)/components/tweet-wall.tsx +++ b/docs/app/[lang]/(home)/components/tweet-wall.tsx @@ -103,7 +103,7 @@ function InlineCode({ children }: { children: ReactNode }) { } function InlineLink({ children }: { children: ReactNode }) { - return {children}; + return {children}; } function VerifiedBadge() { diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/agents-visual.tsx b/docs/app/[lang]/(home)/components/vercel-com-visuals/agents-visual.tsx new file mode 100644 index 0000000000..df4d5997b3 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/agents-visual.tsx @@ -0,0 +1,242 @@ +'use client'; + +import type { JSX } from 'react'; +import { + motion, + useMotionValue, + useTransform, + animate, + useInView, + useReducedMotion, +} from 'motion/react'; +import { useEffect, useRef, useState, useCallback } from 'react'; +import { cn } from '@/lib/utils'; + +const ANIMATION_CONFIG = { + STAGGER_DELAY: 200, + ANIMATION_DURATION: 1500, + GRADIENT_FADE_DELAY: 300, + FADE_OUT_DELAY: 800, + FADE_OUT_DURATION: 500, + PAUSE_DURATION: 1000, + TOTAL_CYCLE_TIME: 4800, + COLOR_CHANGE_THRESHOLD: { RED: 60, GREEN: 80 }, +} as const; + +const ANIMATION_LINES = Array.from({ length: 7 }, (_, index) => ({ + id: `line-${index}`, + delay: index * ANIMATION_CONFIG.STAGGER_DELAY, +})); + +export function AgentsVisual(): JSX.Element { + const ref = useRef(null); + const isInView = useInView(ref); + const shouldReduceMotion = useReducedMotion(); + const [animationKey, setAnimationKey] = useState(0); + + const startAnimationCycle = useCallback(() => { + if (shouldReduceMotion) return; + + setAnimationKey((prev) => prev + 1); + }, [shouldReduceMotion]); + + useEffect(() => { + if (!isInView || shouldReduceMotion) return; + + let intervalId: NodeJS.Timeout; + + const scheduleNextCycle = () => { + intervalId = setTimeout(() => { + if (isInView && !shouldReduceMotion) { + startAnimationCycle(); + scheduleNextCycle(); + } + }, ANIMATION_CONFIG.TOTAL_CYCLE_TIME); + }; + + startAnimationCycle(); + scheduleNextCycle(); + + return () => { + clearTimeout(intervalId); + }; + }, [isInView, shouldReduceMotion, startAnimationCycle]); + + return ( +
+
+ {ANIMATION_LINES.map((line) => ( + + ))} +
+
+
+ ); +} + +interface AnimatedLineProps { + delay: number; + animationKey: number; + shouldReduceMotion: boolean | null; +} + +function AnimatedLine({ + delay, + animationKey, + shouldReduceMotion, +}: AnimatedLineProps): JSX.Element { + const width = useMotionValue(0); + const widthPct = useTransform(width, (v) => `${v}%`); + const opacity = useMotionValue(1); + const [hideGradient, setHideGradient] = useState(false); + const [gradientColor, setGradientColor] = useState<'red' | 'green'>('green'); + + useEffect(() => { + if (shouldReduceMotion) { + width.set(100); + opacity.set(1); + setHideGradient(true); + setGradientColor('green'); + return; + } + + setHideGradient(false); + width.set(0); + opacity.set(1); + + const widthControls = animate(width, 100, { + duration: ANIMATION_CONFIG.ANIMATION_DURATION / 1000, + delay: delay / 1000, + ease: [0.4, 0.04, 0.04, 1], + }); + + const timeoutIds: NodeJS.Timeout[] = []; + + const unsubscribe = width.on('change', (latest) => { + if ( + latest > ANIMATION_CONFIG.COLOR_CHANGE_THRESHOLD.RED && + latest < ANIMATION_CONFIG.COLOR_CHANGE_THRESHOLD.GREEN + ) { + setGradientColor('red'); + } + if (latest >= ANIMATION_CONFIG.COLOR_CHANGE_THRESHOLD.GREEN) { + setGradientColor('green'); + } + }); + + void widthControls.finished.then(() => { + const timeout1 = setTimeout( + () => setHideGradient(true), + ANIMATION_CONFIG.GRADIENT_FADE_DELAY + ); + const timeout2 = setTimeout( + () => setGradientColor('green'), + ANIMATION_CONFIG.GRADIENT_FADE_DELAY * 2 + ); + + timeoutIds.push(timeout1, timeout2); + + animate(opacity, 0, { + duration: ANIMATION_CONFIG.FADE_OUT_DURATION / 1000, + delay: ANIMATION_CONFIG.FADE_OUT_DELAY / 1000, + ease: [0.4, 0.04, 0.04, 1], + }); + }); + + return () => { + widthControls.stop(); + unsubscribe(); + timeoutIds.forEach(clearTimeout); + }; + }, [animationKey, width, opacity, delay, shouldReduceMotion]); + + return ( +
+ + +
+ + +
+
+ +
+ ); +} + +function SolidLine(): JSX.Element { + return
; +} + +function DashedLine(): JSX.Element { + return ( + + ); +} + +interface GradientLineProps { + hideGradient: boolean; + color?: 'green' | 'red'; +} + +function GradientLine({ + hideGradient, + color = 'red', +}: GradientLineProps): JSX.Element { + return ( +
+ ); +} + +function Arrow(): JSX.Element { + return ( + + ); +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/ai-sdk-visual.tsx b/docs/app/[lang]/(home)/components/vercel-com-visuals/ai-sdk-visual.tsx new file mode 100644 index 0000000000..c2458590cd --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/ai-sdk-visual.tsx @@ -0,0 +1,372 @@ +import type { JSX } from 'react'; + +export function AiSdkVisual(): JSX.Element { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/animated-bar.tsx b/docs/app/[lang]/(home)/components/vercel-com-visuals/animated-bar.tsx new file mode 100644 index 0000000000..a30e0efa34 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/animated-bar.tsx @@ -0,0 +1,152 @@ +'use client'; + +import type { JSX } from 'react'; +import { cn } from '@/lib/utils'; +import { Bar } from './bar'; +import { + motion, + useMotionValue, + useTransform, + animate, + useReducedMotion, +} from 'motion/react'; +import { useEffect, useState } from 'react'; + +export interface AnimatedBarProps { + className?: string; + counterFormat?: 'ms' | 's'; + delay: number; + duration: number; + ease?: string | number[]; + isInView: boolean; + left?: string; + onFinish?: () => void; + right: string; + shouldReduceMotion?: boolean | null; + showLine?: boolean; + size?: 'small' | 'large'; + targetValue: number; + variant?: 'blue' | 'green' | 'amber'; +} + +export function AnimatedBar({ + className, + counterFormat = 's', + delay, + duration, + ease = 'linear', + isInView, + left, + onFinish, + right, + shouldReduceMotion: shouldReduceMotionProp, + showLine, + size, + targetValue, + variant, +}: AnimatedBarProps): JSX.Element { + const shouldReduceMotionHook = useReducedMotion(); + const shouldReduceMotion = shouldReduceMotionProp ?? shouldReduceMotionHook; + + const width = useMotionValue(0); + const widthPct = useTransform(width, (v) => `${v}%`); + const counter = useMotionValue(0); + const [currentCounter, setCurrentCounter] = useState(0); + const [hideLine, setHideLine] = useState(false); + const [overflow, setOverflow] = useState<'visible' | 'hidden'>('hidden'); + + useEffect(() => { + const unsubscribe = counter.on('change', (latest) => { + setCurrentCounter(latest); + }); + return unsubscribe; + }, [counter]); + + useEffect(() => { + if (!isInView) return; + + if (shouldReduceMotion) { + width.set(100); + counter.set(targetValue); + setCurrentCounter(targetValue); + if (showLine) { + setOverflow('visible'); + setHideLine(true); + } + onFinish?.(); + return; + } + + if (showLine) { + setOverflow('visible'); + } + + // @ts-expect-error - TODO: fix + const controls = animate(width, 100, { + duration: duration / 1000, + delay: delay / 1000, + ease, + }); + + // @ts-expect-error - TODO: fix + const counterControls = animate(counter, targetValue, { + duration: duration / 1000, + delay: delay / 1000, + ease, + }); + + void Promise.all([controls.finished, counterControls.finished]).then(() => { + setHideLine(true); + onFinish?.(); + }); + + return () => { + controls.stop(); + counterControls.stop(); + }; + }, [ + isInView, + width, + counter, + delay, + duration, + targetValue, + ease, + onFinish, + showLine, + shouldReduceMotion, + ]); + + return ( + + + {showLine && ( +
+ )} + + ); +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/bar.tsx b/docs/app/[lang]/(home)/components/vercel-com-visuals/bar.tsx new file mode 100644 index 0000000000..b52069c8a3 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/bar.tsx @@ -0,0 +1,56 @@ +import { cva, type VariantProps } from 'class-variance-authority'; +import type { JSX } from 'react'; + +const barStyles = cva('flex items-center border border-solid', { + variants: { + align: { + between: 'justify-between', + center: 'justify-center', + }, + variant: { + blue: 'bg-blue-200 text-blue-900 border-blue-700', + green: 'bg-green-100 text-green-900 border-green-600', + amber: 'bg-amber-100 text-amber-900 border-amber-600', + }, + size: { + small: 'py-1 px-2 text-copy-13-mono rounded-md md:rounded-lg', + large: + 'py-2 px-2 md:px-3 lg:px-4 text-body-16 font-mono rounded-md md:rounded-lg', + }, + }, + defaultVariants: { + align: 'between', + variant: 'blue', + size: 'small', + }, +}); + +interface BarProps { + left?: string; + right: string; + variant?: VariantProps['variant']; + size?: VariantProps['size']; + className?: string; +} + +export function Bar({ + left, + right, + variant, + size, + className, +}: BarProps): JSX.Element { + return ( +
+ {left ?
{left}
: null} +
{right}
+
+ ); +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/downtime-visual.tsx b/docs/app/[lang]/(home)/components/vercel-com-visuals/downtime-visual.tsx new file mode 100644 index 0000000000..5cf5a3de2c --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/downtime-visual.tsx @@ -0,0 +1,133 @@ +'use client'; + +import type { JSX } from 'react'; +import { cn } from '@/lib/utils'; +import { useState, useEffect, useRef } from 'react'; +import { AnimatePresence, motion, useInView } from 'motion/react'; +import { Spinner } from '@/components/ui/spinner'; + +export function DowntimeVisual(): JSX.Element { + return ( +
+
+ + + + +
+
+
+ ); +} + +type ItemProps = { + title: string; + subtitle: string; + seconds?: string; +}; + +function Item({ title, subtitle, seconds }: ItemProps) { + const [inView, setInView] = useState(false); + const [isFinished, setIsFinished] = useState(Boolean(seconds)); + const [counter, setCounter] = useState(30); + const ref = useRef(null); + const isInView = useInView(ref); + + useEffect(() => { + if (isInView) { + setInView(true); + } + }, [isInView]); + + useEffect(() => { + if (seconds) { + return; // If seconds is provided, don't animate the counter. + } + + let interval: ReturnType | undefined; + + if (inView) { + setCounter(30); + interval = setInterval(() => { + setCounter((prev) => { + if (prev < 44) { + return prev + 1; + } else { + setIsFinished(true); + clearInterval(interval); + return prev; + } + }); + }, 1000); + } + + return () => { + if (interval) clearInterval(interval); + }; + }, [inView, seconds]); + + return ( +
+
+ {title} + {subtitle} +
+
+
+ + + + {isFinished ? ( + + Ready + + ) : ( + + Building + + )} + + +
+ + + + {isFinished ? null : ( + + + + )} + + + {seconds || `${counter}s`} + + + +
+
+ ); +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/feature-grid.tsx b/docs/app/[lang]/(home)/components/vercel-com-visuals/feature-grid.tsx new file mode 100644 index 0000000000..f479a59431 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/feature-grid.tsx @@ -0,0 +1,128 @@ +import type { JSX, ReactNode } from 'react'; + +import { AgentsVisual } from './agents-visual'; +import { AiSdkVisual } from './ai-sdk-visual'; +import { DowntimeVisual } from './downtime-visual'; +import { InfraVisual } from './infra-visual'; +import { O11yVisual } from './o11y-visual'; +import { TimeoutVisual } from './timeout-visual'; +import { UsageVisual } from './usage-visual'; + +interface Feature { + title: string; + description: string; + visual: ReactNode; +} + +const features: Feature[] = [ + { + title: 'Deep integration with AI SDK.', + description: + 'Use familiar AI SDK patterns, plus durability, observability, and retries so agents stay reliable in production.', + visual: , + }, + { + title: 'Durable agents by default.', + description: + 'High-performance streaming, persistence, and resumable runs work out of the box. No infrastructure setup required.', + visual: , + }, + { + title: 'Inspect every run end\u2011to\u2011end.', + description: + 'When deploying workflow on Vercel, deep workflow observability is built into the Vercel dashboard with no configuration or storage.', + visual: , + }, + { + title: 'Zero infrastructure management.', + description: + 'Fluid compute, serverless functions, queues and persistence work out of the box.', + visual: , + }, + { + title: 'Deploy confidently.', + description: + 'Running workflows continue on their original version while new executions use the latest code.', + visual: , + }, + { + title: 'No timeout limits.', + description: + 'Write long-running workflows without worrying about execution limits.', + visual: , + }, + { + title: 'Pay for what you use.', + description: 'Only pay for actual execution time, not idle resources.', + visual: , + }, +]; + +function FeatureCard({ title, description, visual }: Feature): JSX.Element { + return ( +
+

+ {title}{' '} + {description} +

+
+ {visual} +
+
+ ); +} + +function FeatureCardWide({ title, description, visual }: Feature): JSX.Element { + return ( +
+
+

+ {title} +

+

+ {description} +

+
+
{visual}
+
+ ); +} + +export function FeatureGrid(): JSX.Element { + return ( +
+ {features.slice(0, 2).map((feature) => ( + + ))} +
+ ); +} + +export function FeatureGridExtended(): JSX.Element { + return ( + <> + {/* AI SDK + Agents — 2 col */} +
+ {features.slice(0, 2).map((feature) => ( + + ))} +
+ {/* Observability — full width */} +
+ +
+ {/* Infra + Deploy — 2 col */} +
+ {features.slice(3, 5).map((feature) => ( + + ))} +
+ {/* Timeout + Usage — 2 col */} +
+ {features.slice(5, 7).map((feature) => ( + + ))} +
+ + ); +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/constants.ts b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/constants.ts new file mode 100644 index 0000000000..7f7539dd86 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/constants.ts @@ -0,0 +1,3 @@ +export const radius = 400; +export const strokeWidth = 1; +export const diameter = radius * 2; diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/context.ts b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/context.ts new file mode 100644 index 0000000000..b6d41dd71f --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/context.ts @@ -0,0 +1,27 @@ +import { createContext, useContext } from 'react'; +import type { Point } from './types'; + +interface GlobeContextProps { + longitudeDivisions: number; + latitudeDivisions: number; + longitudeSegmentLength: number; + nodeMatrix: Point[][]; + debug: boolean; + matrixRelativeToOrigin: (x: number, y: number) => Point; + perspectiveConstant: number; +} + +export const GlobeContext = createContext({ + longitudeDivisions: 0, + latitudeDivisions: 0, + longitudeSegmentLength: 0, + nodeMatrix: [], + debug: false, + matrixRelativeToOrigin: () => ({ x: 0, y: 0 }), + perspectiveConstant: 1 / 4, +}); +GlobeContext.displayName = 'GlobeContext'; + +export function useGlobeContext(): GlobeContextProps { + return useContext(GlobeContext); +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/globe.tsx b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/globe.tsx new file mode 100644 index 0000000000..9725e56206 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/globe.tsx @@ -0,0 +1,214 @@ +'use client'; + +import type { CSSProperties } from 'react'; +import { useMemo, type PropsWithChildren, useCallback } from 'react'; +import { Path } from './path'; +import { Node } from './node'; +import { diameter, radius, strokeWidth } from './constants'; +import { + dKey, + drawGreatArc, + drawHorizontalLine, + getPerspectiveLatitudeSegment, +} from './utils'; +import type { Point } from './types'; +import { GlobeContext } from './context'; + +interface GlobeProps { + half?: boolean; + longitudeDivisions: number; + latitudeDivisions: number; + topLit?: boolean; + fill?: string; + debug?: boolean; + color?: string; + gradientMask?: boolean; + style?: CSSProperties; + className?: string; + perspectiveConstant?: number; +} + +function Globe({ + debug = false, + children, + color, + half, + longitudeDivisions = 8, + latitudeDivisions = 10, + topLit, + fill, + gradientMask, + className, + style, + perspectiveConstant = 1 / 4, +}: PropsWithChildren): React.ReactNode { + const longitudeSegmentLength = diameter / latitudeDivisions; + + const arcs = Array.from({ length: longitudeDivisions + 1 }, (_, i) => i) + .map((x) => x - longitudeDivisions / 2) + .map((x) => { + const latitudeSegmentLength = getPerspectiveLatitudeSegment( + longitudeDivisions, + x, + perspectiveConstant + ); + const xRadius = latitudeSegmentLength * x; + const arc = drawGreatArc(xRadius, radius, 0, 180, x > 0); + return arc; + }); + + function createPerspectiveMatrix(): Point[][] { + const matrix: Point[][] = []; + for (let y = 0; y < latitudeDivisions; y++) { + const row: Point[] = []; + arcs.forEach((arc) => { + const yScaled = y * longitudeSegmentLength; + row.push({ x: arc.getXPointOnEllipse(yScaled), y: yScaled }); + }); + matrix.push(row); + } + return matrix; + } + + const nodeMatrix = createPerspectiveMatrix(); + + const xToTopLeft = useCallback( + (x: number): number => { + return x + longitudeDivisions / 2; + }, + [longitudeDivisions] + ); + + const yToTopLeft = useCallback( + (y: number): number => { + return latitudeDivisions / 2 - y; + }, + [latitudeDivisions] + ); + + const matrixRelativeToOrigin = useCallback( + (x: number, y: number): Point => { + return nodeMatrix[yToTopLeft(y)][xToTopLeft(x)]; + }, + [nodeMatrix, xToTopLeft, yToTopLeft] + ); + + const contextValue = useMemo( + () => ({ + longitudeDivisions, + latitudeDivisions, + nodeMatrix, + longitudeSegmentLength, + matrixRelativeToOrigin, + debug, + perspectiveConstant, + }), + [ + longitudeDivisions, + latitudeDivisions, + nodeMatrix, + longitudeSegmentLength, + matrixRelativeToOrigin, + debug, + perspectiveConstant, + ] + ); + + return ( + + + + {arcs.map((arc) => { + return ( + + ); + })} + {nodeMatrix.map((row, y) => { + const start = row[0]; + const end = row[row.length - 1]; + const line = drawHorizontalLine(start.x, end.x, start.y); + if (y === 0) return null; + return ( + + ); + })} + + {gradientMask ? ( + + + + ) : null} + + {children} + + + + + + + {gradientMask ? ( + + + + + ) : null} + + + ); +} +// Reassign the Globe components to the Globe object +Globe.Path = Path; +Globe.Node = Node; + +export { Globe }; diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/index.ts b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/index.ts new file mode 100644 index 0000000000..3892764e48 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/index.ts @@ -0,0 +1,3 @@ +import { Globe } from './globe'; + +export { Globe }; diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/node.module.css b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/node.module.css new file mode 100644 index 0000000000..dbf1d37e29 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/node.module.css @@ -0,0 +1,21 @@ +.nodeGroup:hover { + cursor: pointer; + + & .node { + stroke: var(--ds-gray-500); + } +} + +.dot { + display: none; +} + +@media (max-width: 600px) { + .dot { + display: block; + } + + .icon { + display: none; + } +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/node.tsx b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/node.tsx new file mode 100644 index 0000000000..c93aacc3bc --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/node.tsx @@ -0,0 +1,110 @@ +'use client'; + +import { clsx } from 'clsx'; +import { useGlobeContext } from './context'; +import styles from './node.module.css'; + +interface NodeProps { + x: number; + y: number; + children?: React.ReactNode; + vercelLogo?: boolean; + vercelLogoScale?: number; + vercelLogoOffset?: { x: number; y: number }; + className?: string; + radius?: number; + childrenOnly?: boolean; + securityShield?: boolean; +} + +export function Node({ + children, + x, + y, + vercelLogo, + className, + radius = 16, + vercelLogoScale = 0.9, + vercelLogoOffset = { x: -7.5, y: -8 }, + childrenOnly = false, + securityShield, +}: NodeProps): React.ReactNode { + const point = useGlobeContext().matrixRelativeToOrigin(x, y); + + if (securityShield) { + return ( + + + + + + ); + } + + return ( + + {children} + {!childrenOnly && ( + <> + + {vercelLogo ? ( + + ) : ( + + )} + + )} + {!childrenOnly && ( + + )} + + ); +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/path.module.css b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/path.module.css new file mode 100644 index 0000000000..2d8c012f76 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/path.module.css @@ -0,0 +1,9 @@ +.gradient { + --color: var(--normal-color) !important; + + @supports (color: oklch(0% 0% 0deg)) { + @media (color-gamut: p3) { + --color: var(--p3-color, var(--normal-color)) !important; + } + } +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/path.tsx b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/path.tsx new file mode 100644 index 0000000000..c097344d95 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/path.tsx @@ -0,0 +1,429 @@ +'use client'; + +import type { CSSProperties, JSX } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { useReducedMotion } from 'motion/react'; +import { diameter, radius } from './constants'; +import { + _r, + distance, + drawGreatArc, + drawHorizontalLine, + getPerspectiveLatitudeSegment, + removeClosePoints, +} from './utils'; +import type { Path, Point } from './types'; +import { useGlobeContext } from './context'; +import styles from './path.module.css'; + +interface PathProps { + color?: string; + p3Color?: string; + delay?: number; + duration?: number; + gradientSizeMultiplier?: number; + linearTiming?: boolean; + maxSegmentDuration?: number; + onAnimateComplete?: () => void; + onAnimationCompleteOffset?: number; + path: Path; + repeat?: number; + repeatDelay?: number; + gradientMask?: boolean; + 'data-testid'?: string; +} + +// biome-ignore lint/suspicious/noRedeclare: matches upstream +export function Path({ + path, + delay = 0, + repeatDelay = 0, + duration, + maxSegmentDuration = 0.3, + gradientSizeMultiplier = 1, + repeat = 0, + linearTiming = false, + color = 'white', + p3Color, + onAnimateComplete, + onAnimationCompleteOffset = 0, + gradientMask, +}: PathProps): JSX.Element { + const [jsLoaded, setJsLoaded] = useState(false); + + const { + longitudeDivisions, + latitudeDivisions, + longitudeSegmentLength, + matrixRelativeToOrigin, + debug, + perspectiveConstant, + } = useGlobeContext(); + + const id = `${path.directions.split('').join('')}${path.origin.x}${ + path.origin.y + }`; + const gradient = `${id}-gradient`; + + const { pathPoints, d } = createPath(); + + const xPath = [ + pathPoints.at(0)?.x, + ...pathPoints.map((p) => p.x), + pathPoints.at(-1)?.x, + ] as number[]; + + const yPath = [ + pathPoints.at(0)?.y, + ...pathPoints.map((p) => p.y), + pathPoints.at(-1)?.y, + ] as number[]; + + const xPathAhead = [ + ...pathPoints.map((p) => p.x), + pathPoints.at(-1)?.x, + pathPoints.at(-1)?.x, + ] as number[]; + + const yPathAhead = [ + ...pathPoints.map((p) => p.y), + pathPoints.at(-1)?.y, + pathPoints.at(-1)?.y, + ] as number[]; + + const numPoints = xPath.length; + const numDirections = path.directions.length; + + const segmentDuration = duration + ? duration / numDirections + : maxSegmentDuration; + + const distances = xPath.map((_, i) => + distance( + { x: xPath[i], y: yPath[i] }, + { x: xPathAhead[i], y: yPathAhead[i] } + ) + ); + const maxDistance = Math.max(...distances); + + const keyTimes = distances.map( + (dist) => (dist / maxDistance) * segmentDuration + ); + + const sumTimes = keyTimes.reduce((a, b) => a + b, 0); + + let total = 0; + const times = keyTimes.map((t) => { + const time = t / sumTimes; + total += time; + return total; + }); + + // remove the last element from timings + times.pop(); + + // insert 0 at the beginning + times.unshift(0); + + // make second to last times the average between the last and 3rd to last time + times[times.length - 2] = + (times[times.length - 1] + times[times.length - 3]) / 2; + + // make the second time the average between the first and third time + times[1] = (times[0] + times[2]) / 2; + + const opacityKeys = Array.from({ length: numPoints }).map((_, i) => + i === 0 || i === numPoints - 1 ? 0 : 1 + ); + + const radiusKeys = xPath.map((_, i) => + i === 0 || i === numPoints - 1 + ? 0 + : (radius / Math.max(longitudeDivisions, latitudeDivisions)) * + gradientSizeMultiplier + ); + + const transition = { + duration: segmentDuration * numDirections, + repeatDelay, + repeat, + ease: 'linear', + times: linearTiming ? times : undefined, + delay, + }; + + function createPath(): { d: string; pathPoints: Point[] } { + let dPath = ''; + const points: Point[] = []; + let x = path.origin.x; + let y = path.origin.y; + + path.directions.split('').forEach((dir, i) => { + const latitudeSegmentLength = getPerspectiveLatitudeSegment( + longitudeDivisions, + x, + perspectiveConstant + ); + const xRadius = latitudeSegmentLength * x; + + const angleFromY = (_y: number): number => { + return toDegrees(Math.acos((longitudeSegmentLength * _y) / radius)); + }; + + function toDegrees(radians: number): number { + return (radians * 180) / Math.PI; + } + const onRight = x > 0; + const onLeft = x < 0; + const centered = x === 0; + + switch (dir.toLowerCase()) { + case 'u': { + const upArc = drawGreatArc( + xRadius, + radius, + angleFromY(y), + angleFromY(y + 1), + onRight + ); + + dPath += upArc.d; + + if (i === 0) + points.push(onLeft || centered ? upArc.end : upArc.start); + points.push(onLeft || centered ? upArc.start : upArc.end); + + y += 1; + + break; + } + case 'd': { + const downArc = drawGreatArc( + xRadius, + radius, + angleFromY(y), + angleFromY(y - 1), + onLeft + ); + + dPath += downArc.d; + + if (i === 0) + points.push(onRight || centered ? downArc.end : downArc.start); + points.push(onRight || centered ? downArc.start : downArc.end); + + y -= 1; + + break; + } + case 'l': { + const leftStart = matrixRelativeToOrigin(x, y); + const leftEnd = matrixRelativeToOrigin(x - 1, y); + const leftLine = drawHorizontalLine( + leftStart.x, + leftEnd.x, + leftStart.y + ); + dPath += leftLine.d; + + points.push(leftLine.start, leftLine.end); + x -= 1; + break; + } + case 'r': { + const rightStart = matrixRelativeToOrigin(x, y); + const rightEnd = matrixRelativeToOrigin(x + 1, y); + const rightLine = drawHorizontalLine( + rightStart.x, + rightEnd.x, + rightStart.y + ); + dPath += rightLine.d; + + points.push(rightLine.start, rightLine.end); + + x += 1; + break; + } + default: + throw new Error(`Unknown direction ${dir}`); + } + }); + + return { d: dPath, pathPoints: removeClosePoints(points) }; + } + + const transitionProps = { + delay, + duration: segmentDuration * numDirections, + id, + repeat, + repeatCount: repeat === Number.POSITIVE_INFINITY ? 'indefinite' : repeat, + repeatDelay, + }; + + const AnimateCX = ( + + ); + const AnimateCY = ( + + ); + const AnimateR = ( + + ); + const AnimateOpacity = ( + + ); + + // biome-ignore lint/correctness/useExhaustiveDependencies: matches upstream + useEffect(() => { + setJsLoaded(true); + if (!onAnimateComplete || repeat === Number.POSITIVE_INFINITY) return; + const id = setTimeout( + () => { + onAnimateComplete(); + }, + (transition.duration * (repeat + 1) + + transition.delay + + onAnimationCompleteOffset) * + 1000 + ); + return () => clearTimeout(id); + }, []); + + return ( + + + {AnimateOpacity} + + {debug ? ( + <> + + + {AnimateOpacity} + + + + + {AnimateOpacity} + {AnimateCX} + {AnimateCY} + + + ) : null} + + + + + + {AnimateCX} + {AnimateCY} + {AnimateR} + + + + ); +} + +export function AnimateAttribute({ + attributeName, + values, + delay, + repeatCount = 'indefinite', + repeatDelay, + duration, + id, +}: { + attributeName: string; + values: (string | number)[]; + delay: number; + repeatCount?: number | string | undefined; + repeatDelay: number; + duration: number; + id: string; +}): JSX.Element { + const _id = `${attributeName}-${id}`; + const totalDur = repeatDelay + duration; + const proportionOfRepeatDelay = repeatDelay / totalDur; + const normalizedTotalDur = 1 - proportionOfRepeatDelay; + const ref = useRef(null); + const shouldReduceMotion = useReducedMotion(); + + // biome-ignore lint/correctness/useExhaustiveDependencies: matches upstream + useEffect(() => { + if (!ref.current) return; + ref.current.endElement(); + const id = setTimeout(() => { + if (!ref.current) return; + ref.current.beginElement(); + }, delay * 1000); + return () => clearTimeout(id); + }, []); + + const keyTimes = `${values + .map((_, i) => { + const time = i / (values.length - 1); + return _r(time * normalizedTotalDur); + }) + .join(';')};1`; + + const adjustedValues = `${values.map((v) => _r(v)).join(';')};0`; + + return ( + + ); +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/types.ts b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/types.ts new file mode 100644 index 0000000000..3c96630d50 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/types.ts @@ -0,0 +1,9 @@ +export interface Point { + x: number; + y: number; +} + +export interface Path { + origin: Point; + directions: string; +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/utils.ts b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/utils.ts new file mode 100644 index 0000000000..3c1e1187ce --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/globe/utils.ts @@ -0,0 +1,170 @@ +import { radius } from './constants'; +import type { Point } from './types'; + +/** + * Rounds a given number to three decimal places. + */ +export function _r(number: number | string): number { + return Math.round(Number(number) * 1000) / 1000; +} + +/** + * Gives a valid react key given a string + */ +export function dKey(d: string): string { + return d.replace(/ /g, ''); +} + +/** + * Calculates the length of a latitude segment based on the perspective effect. + */ +export function getPerspectiveLatitudeSegment( + longitudeDivisions: number, + x: number, + perspectiveConstant: number +): number { + const mappedToCos = + ((x + longitudeDivisions / 2) / longitudeDivisions) * Math.PI + Math.PI / 2; + const perspective = + (1 - Math.cos(mappedToCos)) * perspectiveConstant + + (1 - perspectiveConstant); + const latitudeSegmentLength = + ((2 * radius) / longitudeDivisions) * perspective; + return latitudeSegmentLength; +} + +/** + * Draws a great arc on an ellipse based on the given parameters. + */ +export function drawGreatArc( + xRadius: number, + yRadius: number, + startAngle: number, + endAngle: number, + flipAngles = false +): { + d: string; + start: Point; + end: Point; + getXPointOnEllipse: (y: number) => number; +} { + const adjustedStartAngle = flipAngles ? endAngle : startAngle; + const adjustedEndAngle = flipAngles ? startAngle : endAngle; + + const start = polarToCartesian( + radius, + radius, + xRadius, + yRadius, + adjustedEndAngle + ); + const end = polarToCartesian( + radius, + radius, + xRadius, + yRadius, + adjustedStartAngle + ); + + const largeArcFlag = adjustedEndAngle - adjustedStartAngle <= 180 ? '0' : '1'; + + function getXPointOnEllipse(y: number): number { + const r = radius; + return r + xRadius * Math.sqrt(1 - (y - r) ** 2 / (r * r)); + } + + const d = [ + 'M', + _r(start.x), + _r(start.y), + 'A', + _r(xRadius), + _r(yRadius), + 0, + largeArcFlag, + 0, + _r(end.x), + _r(end.y), + ].join(' '); + + return { d, getXPointOnEllipse, start, end }; +} + +/** + * Draws a horizontal line on the SVG canvas. + */ +export function drawHorizontalLine( + x1: number, + x2: number, + y: number +): { + d: string; + start: Point; + end: Point; +} { + const d = `M${_r(x1)},${_r(y)} h${_r(x2 - x1)}`; + return { + d, + start: { + x: x1, + y, + }, + end: { + x: x2, + y, + }, + }; +} + +/** + * Converts polar coordinates to Cartesian coordinates. + */ +function polarToCartesian( + centerX: number, + centerY: number, + xRadius: number, + yRadius: number, + angleInDegrees: number +): Point { + const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0; + + return { + x: centerX + xRadius * Math.cos(angleInRadians), + y: centerY + yRadius * Math.sin(angleInRadians), + }; +} + +/** + * Calculates the Euclidean distance between two points. + */ +export function distance(p1: Point, p2: Point): number { + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + return Math.sqrt(dx * dx + dy * dy); +} + +/** + * Removes closely spaced points from an array of points. + */ +export function removeClosePoints(points: Point[]): Point[] { + if (points.length < 2) { + return points; + } + + const result: Point[] = [points[0]]; + let currentIndex = 1; + + while (currentIndex < points.length) { + const currentPoint = points[currentIndex]; + const lastPoint = result[result.length - 1]; + const dist = distance(currentPoint, lastPoint); + + if (dist >= 0.1) { + result.push(currentPoint); + } + + currentIndex++; + } + + return result; +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/index.ts b/docs/app/[lang]/(home)/components/vercel-com-visuals/index.ts new file mode 100644 index 0000000000..bbc88e5f7c --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/index.ts @@ -0,0 +1 @@ +export { PlainGlobe } from './plain-globe'; diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/infra-visual.tsx b/docs/app/[lang]/(home)/components/vercel-com-visuals/infra-visual.tsx new file mode 100644 index 0000000000..351567f6ee --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/infra-visual.tsx @@ -0,0 +1,956 @@ +import type { JSX } from 'react'; + +export function InfraVisual(): JSX.Element { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/o11y-visual.tsx b/docs/app/[lang]/(home)/components/vercel-com-visuals/o11y-visual.tsx new file mode 100644 index 0000000000..d9ccdd0c15 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/o11y-visual.tsx @@ -0,0 +1,297 @@ +'use client'; + +import type { JSX } from 'react'; +import { AnimatedBar } from './animated-bar'; +import { cn } from '@/lib/utils'; +import { + motion, + useMotionValue, + animate, + useInView, + AnimatePresence, + useReducedMotion, +} from 'motion/react'; +import { useEffect, useRef, useState, useCallback } from 'react'; + +const ANIMATION_CONFIG = { + DURATION: 2000, + EASE: 'linear' as const, + TIMING_RATIOS: { + FETCH_ORDER: 0.25, + VALIDATE: 0.1666, + ENRICH_PRICING: 0.25, + SAVE_ORDER: 0.1666, + SEND_EMAIL: 0.1666, + }, + DELAY_RATIOS: { + VALIDATE: 0.25, + ENRICH_PRICING: 0.4166, + SAVE_ORDER: 0.6666, + SEND_EMAIL: 0.8332, + }, +} as const; + +const GRID_LINES = Array.from({ length: 15 }, (_, index) => ({ + id: `grid-line-${index}`, + isVisible: index !== 0 && index !== 14, +})); + +export function O11yVisual(): JSX.Element { + const [isFinished, setIsFinished] = useState(false); + const ref = useRef(null); + const isInView = useInView(ref); + const shouldReduceMotion = useReducedMotion(); + + const handleFinish = useCallback(() => { + setIsFinished(true); + }, []); + + return ( +
+ + ); +} + +interface CounterProps { + duration: number; + onFinish?: () => void; + targetValue: number; + isInView: boolean; + shouldReduceMotion?: boolean | null; +} + +function Counter({ + duration, + onFinish, + targetValue, + isInView, + shouldReduceMotion, +}: CounterProps): JSX.Element { + const counter = useMotionValue(0); + const [currentCounter, setCurrentCounter] = useState(0); + + useEffect(() => { + const unsubscribe = counter.on('change', (latest) => { + setCurrentCounter(latest); + }); + return unsubscribe; + }, [counter]); + + useEffect(() => { + if (!isInView) return; + + if (shouldReduceMotion) { + counter.set(targetValue); + setCurrentCounter(targetValue); + onFinish?.(); + return; + } + + const counterControls = animate(counter, targetValue, { + duration: duration / 1000, + ease: ANIMATION_CONFIG.EASE, + }); + + if (onFinish) { + void counterControls.finished.then(() => onFinish()); + } + + return () => { + counterControls.stop(); + }; + }, [isInView, counter, duration, targetValue, onFinish, shouldReduceMotion]); + + return {Math.round(currentCounter)}ms; +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/plain-globe.tsx b/docs/app/[lang]/(home)/components/vercel-com-visuals/plain-globe.tsx new file mode 100644 index 0000000000..a25aefded0 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/plain-globe.tsx @@ -0,0 +1,103 @@ +'use client'; + +import type { JSX } from 'react'; +import { Globe } from './globe'; + +export function PlainGlobe(): JSX.Element { + return ( + + {IDLE_PATHS.map((path, i) => { + const key = `idle-path-plain-globe-${path.directions}-${path.origin.x}-${path.origin.y}`; + return ( + + ); + })} + + ); +} + +const IDLE_PATHS = [ + { + origin: { + x: 3, + y: 3, + }, + directions: 'lldll', + color: '#EBE51A', + p3Color: 'color(display-p3 0.9176 0.898 0.3137)', + }, + { + origin: { + x: 2, + y: 2, + }, + directions: 'ld', + color: '#A4E600', + p3Color: 'color(display-p3 0.698 0.8941 0.2667)', + }, + { + origin: { + x: 3, + y: 1, + }, + directions: 'lull', + color: '#2DDD69', + p3Color: 'color(display-p3 0.4235 0.8549 0.4627)', + }, + { + origin: { + x: 2, + y: 1, + }, + directions: 'llld', + color: '#FF904D', + p3Color: 'color(display-p3 0.9843 0.5882 0.3608)', + }, + { + origin: { + x: -1, + y: 3, + }, + directions: 'lld', + color: '#62DE00', + p3Color: 'color(display-p3 0.5176 0.8588 0.251)', + }, + { + origin: { + x: -2, + y: 2, + }, + directions: 'lld', + color: '#FFBB3D', + p3Color: 'color(display-p3 0.9608 0.7451 0.3412)', + }, + { + origin: { + x: -1, + y: 1, + }, + directions: 'llld', + color: '#F8E52C', + p3Color: 'color(display-p3 0.9608 0.8988 0.3412)', + }, +]; diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/timeout-visual.tsx b/docs/app/[lang]/(home)/components/vercel-com-visuals/timeout-visual.tsx new file mode 100644 index 0000000000..771245d4e8 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/timeout-visual.tsx @@ -0,0 +1,139 @@ +'use client'; + +import type { JSX } from 'react'; +import { AnimatedBar } from './animated-bar'; +import { useInView, useReducedMotion } from 'motion/react'; +import { useRef } from 'react'; + +const ANIMATION_CONFIG = { + DURATION: 1250, + EASE: 'linear' as const, + TIMING_RATIOS: { + STEP_1: 0.245, + STEP_2: 0.367, + STEP_3: 0.141, + STEP_4: 0.247, + }, + DELAY_RATIOS: { + STEP_2: 0.245, + STEP_3: 0.612, + STEP_4: 0.753, + }, +} as const; + +const GRID_LINES = Array.from({ length: 6 }, (_, index) => ({ + id: `grid-line-${index}`, +})); + +export function TimeoutVisual(): JSX.Element { + const ref = useRef(null); + const isInView = useInView(ref); + const shouldReduceMotion = useReducedMotion(); + + return ( +
+ + ); +} diff --git a/docs/app/[lang]/(home)/components/vercel-com-visuals/usage-visual.tsx b/docs/app/[lang]/(home)/components/vercel-com-visuals/usage-visual.tsx new file mode 100644 index 0000000000..42375193a1 --- /dev/null +++ b/docs/app/[lang]/(home)/components/vercel-com-visuals/usage-visual.tsx @@ -0,0 +1,65 @@ +'use client'; + +import type { JSX } from 'react'; +import { Bar } from './bar'; + +export function UsageVisual(): JSX.Element { + return ( +
+
+ {Array.from({ length: 6 }).map((_, idx) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: visual lines are static +
+ ))} +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ ); +} + +function IdleTime() { + return ( +
+
+ {Array.from({ length: 56 }).map((_, idx) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: visual lines are static +
+ ))} +
+
+ idle +
+
+ ); +} diff --git a/docs/app/[lang]/(home)/layout.tsx b/docs/app/[lang]/(home)/layout.tsx index 3600925c2c..b0afe70348 100644 --- a/docs/app/[lang]/(home)/layout.tsx +++ b/docs/app/[lang]/(home)/layout.tsx @@ -6,7 +6,7 @@ const Layout = async ({ children, params }: LayoutProps<'/[lang]'>) => { return ( -
{children}
+
{children}
); }; diff --git a/docs/app/[lang]/(home)/page.tsx b/docs/app/[lang]/(home)/page.tsx index f0ca14e1d5..0527cef2d4 100644 --- a/docs/app/[lang]/(home)/page.tsx +++ b/docs/app/[lang]/(home)/page.tsx @@ -1,16 +1,15 @@ import type { Metadata } from 'next'; import { CTA } from './components/cta'; -import { Features } from './components/features'; import { Frameworks } from './components/frameworks'; import { Hero } from './components/hero'; import { Implementation } from './components/implementation'; import { Intro } from './components/intro/intro'; -import { Observability } from './components/observability'; import { PreviewBadge } from './components/preview-badge'; import { RunAnywhere } from './components/run-anywhere'; import { Templates } from './components/templates'; import { TweetWall } from './components/tweet-wall'; import { UseCases } from './components/use-cases-server'; +import { FeatureGridExtended } from './components/vercel-com-visuals/feature-grid'; const title = 'Make any TypeScript Function Durable'; const description = @@ -44,11 +43,8 @@ const Home = () => (
-
- - -
- + + diff --git a/docs/app/[lang]/cookbook/[[...slug]]/page.tsx b/docs/app/[lang]/cookbook/[[...slug]]/page.tsx index 7ac224920d..cd0afcb8d3 100644 --- a/docs/app/[lang]/cookbook/[[...slug]]/page.tsx +++ b/docs/app/[lang]/cookbook/[[...slug]]/page.tsx @@ -8,6 +8,7 @@ import { rewriteCookbookUrl, rewriteCookbookUrlsInText, } from '@/lib/geistdocs/cookbook-source'; +import { MobileDocsBar } from '@/components/geistdocs/mobile-docs-bar'; import { AskAI } from '@/components/geistdocs/ask-ai'; import { CopyPage } from '@/components/geistdocs/copy-page'; import { @@ -68,8 +69,10 @@ const Page = async ({ params }: PageProps<'/[lang]/cookbook/[[...slug]]'>) => {
), }} + tableOfContentPopover={{ enabled: false }} toc={page.data.toc} > + {page.data.title} {page.data.description} diff --git a/docs/app/[lang]/cookbook/layout.tsx b/docs/app/[lang]/cookbook/layout.tsx index fa72592679..63235fbb14 100644 --- a/docs/app/[lang]/cookbook/layout.tsx +++ b/docs/app/[lang]/cookbook/layout.tsx @@ -1,13 +1,17 @@ -import { DocsLayout } from '@/components/geistdocs/docs-layout'; -import { getCookbookTree } from '@/lib/geistdocs/cookbook-source'; +import { DocsLayout } from "@/components/geistdocs/docs-layout"; +import { getCookbookTree } from "@/lib/geistdocs/cookbook-source"; const Layout = async ({ children, params, -}: LayoutProps<'/[lang]/cookbook'>) => { +}: LayoutProps<"/[lang]/cookbook">) => { const { lang } = await params; - return {children}; + return ( +
+ {children} +
+ ); }; export default Layout; diff --git a/docs/app/[lang]/docs/[[...slug]]/page.tsx b/docs/app/[lang]/docs/[[...slug]]/page.tsx index 0aaaf5fb7f..0c4ca276fe 100644 --- a/docs/app/[lang]/docs/[[...slug]]/page.tsx +++ b/docs/app/[lang]/docs/[[...slug]]/page.tsx @@ -17,6 +17,7 @@ import { import { EditSource } from '@/components/geistdocs/edit-source'; import { Feedback } from '@/components/geistdocs/feedback'; import { getMDXComponents } from '@/components/geistdocs/mdx-components'; +import { MobileDocsBar } from '@/components/geistdocs/mobile-docs-bar'; import { OpenInChat } from '@/components/geistdocs/open-in-chat'; import { ScrollTop } from '@/components/geistdocs/scroll-top'; import * as AccordionComponents from '@/components/ui/accordion'; @@ -64,8 +65,10 @@ const Page = async ({ params }: PageProps<'/[lang]/docs/[[...slug]]'>) => {
), }} + tableOfContentPopover={{ enabled: false }} toc={page.data.toc} > + {page.data.title} {page.data.description} diff --git a/docs/app/[lang]/docs/layout.tsx b/docs/app/[lang]/docs/layout.tsx index b59f605872..582f3d7139 100644 --- a/docs/app/[lang]/docs/layout.tsx +++ b/docs/app/[lang]/docs/layout.tsx @@ -1,13 +1,15 @@ -import { DocsLayout } from '@/components/geistdocs/docs-layout'; -import { getDocsTreeWithoutCookbook } from '@/lib/geistdocs/cookbook-source'; +import { DocsLayout } from "@/components/geistdocs/docs-layout"; +import { getDocsTreeWithoutCookbook } from "@/lib/geistdocs/cookbook-source"; -const Layout = async ({ children, params }: LayoutProps<'/[lang]/docs'>) => { +const Layout = async ({ children, params }: LayoutProps<"/[lang]/docs">) => { const { lang } = await params; return ( - - {children} - +
+ + {children} + +
); }; diff --git a/docs/app/[lang]/worlds/[id]/page.tsx b/docs/app/[lang]/worlds/[id]/page.tsx index d8ad7643cb..cbc355eab8 100644 --- a/docs/app/[lang]/worlds/[id]/page.tsx +++ b/docs/app/[lang]/worlds/[id]/page.tsx @@ -1,19 +1,19 @@ -import type { Metadata } from 'next'; -import type { ReactNode } from 'react'; -import { notFound } from 'next/navigation'; import { Step, Steps } from 'fumadocs-ui/components/steps'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { createRelativeLink } from 'fumadocs-ui/mdx'; +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import type { ReactNode } from 'react'; +import { FluidComputeCallout } from '@/components/custom/fluid-compute-callout'; +import { getMDXComponents } from '@/components/geistdocs/mdx-components'; +import { WorldDataProvider } from '@/components/worlds/WorldDataProvider'; import { WorldDetailHero } from '@/components/worlds/WorldDetailHero'; import { WorldDetailToc } from '@/components/worlds/WorldDetailToc'; import { WorldInstructions } from '@/components/worlds/WorldInstructions'; import { WorldTestingPerformance } from '@/components/worlds/WorldTestingPerformance'; -import { WorldDataProvider } from '@/components/worlds/WorldDataProvider'; import { WorldTestingPerformanceMDX } from '@/components/worlds/WorldTestingPerformanceMDX'; -import { FluidComputeCallout } from '@/components/custom/fluid-compute-callout'; -import { getMDXComponents } from '@/components/geistdocs/mdx-components'; -import { getWorldData, getWorldIds } from '@/lib/worlds-data'; import { source } from '@/lib/geistdocs/source'; +import { getWorldData, getWorldIds } from '@/lib/worlds-data'; // Map world IDs to their MDX doc slugs const officialWorldMdxSlugs: Record = { @@ -120,7 +120,7 @@ export default async function WorldDetailPage({ params }: PageProps) {
{isOfficial && mdxContent ? ( // Official worlds: MDX controls the entire content structure -
+
{mdxContent}
) : ( diff --git a/docs/app/[lang]/worlds/page.tsx b/docs/app/[lang]/worlds/page.tsx index 2e1400549c..eccd943a88 100644 --- a/docs/app/[lang]/worlds/page.tsx +++ b/docs/app/[lang]/worlds/page.tsx @@ -1,9 +1,9 @@ import type { Metadata } from 'next'; import Link from 'next/link'; +import { PlainGlobe } from '@/app/[lang]/(home)/components/vercel-com-visuals'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { Globe } from '@/components/worlds/Globe'; -import { WorldCardSimple } from '@/components/worlds/WorldCardSimple'; +import { WorldsFilteredGrid } from '@/components/worlds/WorldsFilteredGrid'; import { getWorldsData } from '@/lib/worlds-data'; export const metadata: Metadata = { @@ -25,37 +25,20 @@ export default async function WorldsPage() { return a.name.localeCompare(b.name); }); - const officialCount = sortedWorlds.filter( - ([, w]) => w.type === 'official' - ).length; - const communityCount = sortedWorlds.filter( - ([, w]) => w.type === 'community' - ).length; - const passingCount = sortedWorlds.filter( - ([, w]) => w.e2e?.status === 'passing' - ).length; - - const managedIds = new Set(['vercel']); - const embeddedIds = new Set(['local', 'redis', 'turso']); - - const managedWorlds = sortedWorlds.filter(([id]) => managedIds.has(id)); - const selfHostedWorlds = sortedWorlds.filter( - ([id]) => !managedIds.has(id) && !embeddedIds.has(id) - ); - const embeddedWorlds = sortedWorlds.filter(([id]) => embeddedIds.has(id)); - return ( -
+
{/* Hero Section */} -
+
{/* Globe backdrop */}
- +
+ +
{/* Content */} -
+

Worlds

@@ -67,79 +50,8 @@ export default async function WorldsPage() {
- {/* Stats */} -
-
- - {sortedWorlds.length} Worlds - - - {officialCount} Official - - - {communityCount} Community - - - {passingCount} Fully Compatible - -
-
- - {/* World Cards — Managed */} -
-
-

- Managed -

-

- Production grade — zero configuration, high throughput, - infinitely-scalable, e2e encrypted, and integrated observability -

-
-
- {managedWorlds.map(([id, world]) => ( - - ))} -
-
- - {/* World Cards — Self-Hosted */} -
-
-

- Self-Hosted -

-

- Self hosted — control your data and scaling while running - workflows inside your own infrastructure -

-
-
- {selfHostedWorlds.map(([id, world]) => ( - - ))} -
-
- - {/* World Cards — Embedded */} -
-
-

- Embedded -

-

- Lightweight solutions for sidecars or local development -

-
-
- {embeddedWorlds.map(([id, world]) => ( - - ))} -
-
+ {/* Filters + World Cards */} + {/* Last Updated */}
@@ -162,90 +74,97 @@ export default async function WorldsPage() { {/* Provider Benchmarks Section */}
-
-
- {/* Left: Text content */} -
+
+ {/* Left: Text content */} +
+

Provider Benchmarks

-

- See how workflows compare across the different worlds deployed - on different providers. Lower execution time means faster - workflows. -

- + + Coming soon +
+

+ See how workflows compare across the different worlds deployed + on different providers. Lower execution time means faster + workflows. +

+ {/* */} +
- {/* Right: Benchmark preview visualization */} -
- {/* Header row */} -
-
-
-
Perf
+ {/* Right: Benchmark preview visualization */} +
+ {/* Header row */} +
+
+
+
+ Perf
- {/* Benchmark bars */} - {[ - { name: 'Local', time: 10.76, isFastest: true }, - { name: 'Vercel', time: 19.37, isFastest: false }, - { name: 'AWS', time: 25.82, isFastest: false }, - { name: 'GCP', time: 25.82, isFastest: false }, - ].map((provider) => { - const maxTime = 25.82; - const width = (provider.time / maxTime) * 100; +
- return ( -
-
- {provider.name} -
-
-
-
-
- {provider.time.toFixed(2)}s -
+ {/* Benchmark bars */} + {[ + { + name: 'Local', + time: 10.76, + color: 'bg-green-700 dark:bg-green-600', + }, + { name: 'Vercel', time: 19.37, color: 'bg-blue-700' }, + { name: 'AWS', time: 25.82, color: 'bg-blue-700' }, + { name: 'GCP', time: 25.82, color: 'bg-blue-700' }, + ].map((provider) => { + const maxTime = 25.82; + const width = (provider.time / maxTime) * 100; + + return ( +
+
+ {provider.name}
- ); - })} -

- For illustration purposes only -

-
+
+
+
+
+ {provider.time.toFixed(2)}s +
+
+ ); + })} +

+ For illustration purposes only +

{/* Learn More Section */} -
+
-

+

Learn more about worlds

To learn more about how worlds work or to create your own, check - the docs. + the docs. You can also build a custom world to connect workflows + to any storage or queuing backend.

-
- - @@ -436,7 +437,6 @@ export const Chat = ({ basePath, suggestions }: ChatProps) => { > diff --git a/docs/components/geistdocs/code-block.tsx b/docs/components/geistdocs/code-block.tsx index 733a6b3856..8a5dfb12f5 100644 --- a/docs/components/geistdocs/code-block.tsx +++ b/docs/components/geistdocs/code-block.tsx @@ -132,7 +132,7 @@ export const CodeBlock = ({ } return ( - +
{ const isMobile = useIsMobile(); return ( - + {items.map((item) => ( {item.href.startsWith('http') ? ( {item.label} - + ) : ( diff --git a/docs/components/geistdocs/docs-layout.tsx b/docs/components/geistdocs/docs-layout.tsx index 8390e6a085..cce5dc1b7c 100644 --- a/docs/components/geistdocs/docs-layout.tsx +++ b/docs/components/geistdocs/docs-layout.tsx @@ -8,14 +8,15 @@ import { } from '@/components/geistdocs/sidebar'; import { i18n } from '@/lib/geistdocs/i18n'; -type DocsLayoutProps = { - tree: ComponentProps['tree']; +interface DocsLayoutProps { children: ReactNode; -}; + tree: ComponentProps['tree']; +} export const DocsLayout = ({ tree, children }: DocsLayoutProps) => ( (