diff --git a/docs/app/[lang]/(home)/components/logo-wall.tsx b/docs/app/[lang]/(home)/components/logo-wall.tsx new file mode 100644 index 0000000000..73447b3435 --- /dev/null +++ b/docs/app/[lang]/(home)/components/logo-wall.tsx @@ -0,0 +1,97 @@ +import type { JSX, SVGProps } from 'react'; + +export const LogoWall = (): JSX.Element => ( +
+
+ + + +
+
+); + +function MuxLogo(props: SVGProps): JSX.Element { + return ( + + + + ); +} + +function WhopLogo(props: SVGProps): JSX.Element { + return ( + + + + + + + + + + ); +} + +function NeonLogo(props: SVGProps): JSX.Element { + return ( + + + + ); +} diff --git a/docs/app/[lang]/(home)/components/o11y-visual.tsx b/docs/app/[lang]/(home)/components/o11y-visual.tsx new file mode 100644 index 0000000000..829ddcce85 --- /dev/null +++ b/docs/app/[lang]/(home)/components/o11y-visual.tsx @@ -0,0 +1,467 @@ +'use client'; + +import type { JSX } from 'react'; +import { cn } from '@/lib/utils'; +import { + motion, + useMotionValue, + useTransform, + 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 ( +
+ + ); +} + +// --- AnimatedBar --- + +interface AnimatedBarProps { + className?: string; + counterFormat?: 'ms' | 's'; + delay: number; + duration: number; + ease?: string | number[]; + isInView: boolean; + left?: string; + right: string; + shouldReduceMotion?: boolean | null; + showLine?: boolean; + targetValue: number; + variant?: 'blue' | 'green' | 'amber'; +} + +function AnimatedBar({ + className, + counterFormat = 's', + delay, + duration, + ease = 'linear', + isInView, + left, + right, + shouldReduceMotion: shouldReduceMotionProp, + showLine, + targetValue, + variant, +}: AnimatedBarProps): JSX.Element { + const shouldReduceMotionHook = useReducedMotion(); + const shouldReduceMotion = shouldReduceMotionProp ?? shouldReduceMotionHook; + + const width = useMotionValue(0); + 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); + } + return; + } + + if (showLine) { + setOverflow('visible'); + } + + // @ts-expect-error motion type mismatch + const controls = animate(width, 100, { + duration: duration / 1000, + delay: delay / 1000, + ease, + }); + + // @ts-expect-error motion type mismatch + const counterControls = animate(counter, targetValue, { + duration: duration / 1000, + delay: delay / 1000, + ease, + }); + + void Promise.all([controls.finished, counterControls.finished]).then(() => { + setHideLine(true); + }); + + return () => { + controls.stop(); + counterControls.stop(); + }; + }, [ + isInView, + width, + counter, + delay, + duration, + targetValue, + ease, + showLine, + shouldReduceMotion, + ]); + + const formattedCounter = currentCounter + ? counterFormat === 'ms' + ? `${Math.round(currentCounter)}ms` + : `${Math.round(currentCounter / 1000)}s` + : right; + + return ( + `${v}%`), + overflow, + }} + className="h-full relative" + > + + {showLine && ( +
+ )} + + ); +} + +// --- Bar --- + +const barVariants = { + blue: 'bg-blue-100 dark:bg-blue-950 text-blue-800 dark:text-blue-300 border-blue-300 dark:border-blue-800', + green: + 'bg-green-100 dark:bg-green-950 text-green-800 dark:text-green-300 border-green-300 dark:border-green-800', + amber: + 'bg-amber-100 dark:bg-amber-950 text-amber-800 dark:text-amber-300 border-amber-300 dark:border-amber-800', +}; + +function Bar({ + left, + right, + variant = 'blue', + className, +}: { + left?: string; + right: string; + variant?: 'blue' | 'green' | 'amber'; + className?: string; +}): JSX.Element { + return ( +
+ {left ?
{left}
: null} +
{right}
+
+ ); +} + +// --- Counter --- + +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/observability.tsx b/docs/app/[lang]/(home)/components/observability.tsx index 3721b2eacf..13ad2c5f43 100644 --- a/docs/app/[lang]/(home)/components/observability.tsx +++ b/docs/app/[lang]/(home)/components/observability.tsx @@ -1,99 +1,12 @@ -'use client'; - -import { motion } from 'motion/react'; -import { cn } from '@/lib/utils'; - -const rows = [ - { - label: 'workflow()', - className: - 'bg-[#E1F0FF] dark:bg-[#00254D] border-[#99CEFF] text-[#0070F3] dark:border-[#0067D6] dark:text-[#52AEFF]', - start: 0, - duration: 100, - }, - { - label: 'process()', - className: - 'bg-[#DCF6DC] dark:bg-[#1B311E] border-[#99E59F] text-[#46A758] dark:border-[#297C3B] dark:text-[#6CDA76]', - start: 0, - duration: 20, - }, - { - label: 'parse()', - className: - 'bg-[#DCF6DC] dark:bg-[#1B311E] border-[#99E59F] text-[#46A758] dark:border-[#297C3B] dark:text-[#6CDA76]', - start: 20, - duration: 25, - }, - { - label: 'transform()', - className: - 'bg-[#DCF6DC] dark:bg-[#1B311E] border-[#99E59F] text-[#46A758] dark:border-[#297C3B] dark:text-[#6CDA76]', - start: 45, - duration: 20, - }, - { - label: 'enrich()', - className: - 'bg-[#DCF6DC] dark:bg-[#1B311E] border-[#99E59F] text-[#46A758] dark:border-[#297C3B] dark:text-[#6CDA76]', - start: 65, - duration: 15, - }, - { - label: 'validate()', - className: - 'bg-[#DCF6DC] dark:bg-[#1B311E] border-[#99E59F] text-[#46A758] dark:border-[#297C3B] dark:text-[#6CDA76]', - start: 80, - duration: 20, - }, -]; +import { O11yVisual } from './o11y-visual'; export const Observability = () => ( -
-

+
+

Observability. Inspect every run end‑to‑end. Pause, replay, and time‑travel through steps with traces, logs, and metrics automatically.

-
-
- {rows.map((row, index) => ( -
-
- -
- - {row.label} - - {index === 0 && ( - {row.duration}ms - )} -
-
-
-
- ))} -
-
+
); diff --git a/docs/app/[lang]/(home)/components/tweet-wall.tsx b/docs/app/[lang]/(home)/components/tweet-wall.tsx new file mode 100644 index 0000000000..6d38ef7269 --- /dev/null +++ b/docs/app/[lang]/(home)/components/tweet-wall.tsx @@ -0,0 +1,209 @@ +import type { JSX } from 'react'; +import { cn } from '@/lib/utils'; +import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; + +export const TweetWall = (): JSX.Element => ( +
+

+ What builders say about Workflow DevKit +

+
+ {TWEETS.map((tweet) => ( +
+ +
+ ))} +
+
+); + +type TweetProps = { + image: string; + name: string; + tweet: string | React.ReactNode; + url: string; + username: string; +}; + +function Tweet({ image, name, tweet, url, username }: TweetProps): JSX.Element { + return ( + +
+ + + + {name + .split(' ') + .map((n) => n[0]) + .join('')} + + +
+ + {name} + + + + + @{username} +
+
+

+ {tweet} +

+
+ ); +} + +function InlineCode({ + className, + ...props +}: React.ComponentProps<'code'>): JSX.Element { + return ( + + ); +} + +function InlineLink({ + className, + ...props +}: React.ComponentProps<'span'>): JSX.Element { + return ( + + ); +} + +const BLOB_URL = 'https://lishhsx6kmthaacj.public.blob.vercel-storage.com'; + +const TWEETS: TweetProps[] = [ + { + url: 'https://x.com/michaelcaaarter/status/1986078356325187762', + name: 'Michael Carter', + username: 'michaelcaaarter', + image: `${BLOB_URL}/michaelcaaarter.jpg`, + tweet: ( + + We just migrated to use workflow and it's + beautiful. Production app here, VC backed and many real fortune 100 + customers using our app daily… not sure why you wouldn't{' '} + use workflow to move fast and focus on building + a great experience. + + ), + }, + { + url: 'https://x.com/nick_tikhonov/status/1985971284577050699', + name: 'Nick Tikhonov', + username: 'nick_tikhonov', + image: `${BLOB_URL}/nick_tikhonov.jpg`, + tweet: ( + <> + + fully migrated to workflows - our use case are AI agents that execute + over a multiple-day time frame, making multiple outbound voice calls + and processing the results + + + before:
- scheduling service
- queues
- workers{' '} +
- cron jobs +
+ now: - 5 functions in one file + + ), + }, + { + url: 'https://x.com/karthikkalyan90/status/1981793765871534588', + name: 'Karthik Kalyan', + username: 'karthikkalyan90', + image: `${BLOB_URL}/karthikkalyan90.jpg`, + tweet: ( + <> + + Behind the elegant and seemingly simple looking{' '} + use workflow and{' '} + use step directives of the new workflow + development kit from @vercel lies a bunch of + compiler engineering. I was curious and decided to dive deeper into + the open source code. Buckle up and read this post if you are curious + too. + + + The workflow development kit (WDK) from Vercel is a new piece of + developer infrastructure that lets developers write workflows and + durable functions.{' '} + + + Durable functions are stateful workflows in a serverless environment. + Ordinary functions are stateless: once they finish, their context + disappears. But a durable function can be paused (suspended) while + waiting for an external event or timer, and later resumed from exactly + where it left off with full context preserved. + + + If you are building something that requires maintaining the state, you + would typically need to store the state in a database and maintain all + the baggage that comes with it. WDK aims to make this part easier. + + + ), + }, + { + url: 'https://x.com/ryancarson/status/1996318671749120315', + name: 'Ryan Carson', + username: 'ryancarson', + image: `${BLOB_URL}/ryancarson.jpg`, + tweet: ( + <> + What a time to be a content marketer. + Sheesh this is mind-blowing. + + Built a complete end-to-end workflow with{' '} + @ampcode using{' '} + @WorkflowDevKit and{' '} + @vercel AI Gateway + + + - Custom DurableAgent tools for research
- Opus 4.5 for + generation
- Gemini 3 Pro for content verification +
- Nano Banana for image creation +
+ AEO locked in. + + ), + }, + { + url: 'https://x.com/ale__vigano/status/1993822442616213851', + name: 'Ale Vigano', + username: 'ale__vigano', + image: `${BLOB_URL}/ale__vigano.jpg`, + tweet: ( + <> + + During my time at @mercadopago we struggled a + lot with concurrency issues handling millions of payments. + + + Hard to believe that almost all the complexity I remember from back + then is now solved with just a use workflow + + + ), + }, +]; diff --git a/docs/app/[lang]/(home)/page.tsx b/docs/app/[lang]/(home)/page.tsx index bd1596f0e7..72490403a0 100644 --- a/docs/app/[lang]/(home)/page.tsx +++ b/docs/app/[lang]/(home)/page.tsx @@ -5,9 +5,11 @@ import { Frameworks } from './components/frameworks'; import { Hero } from './components/hero'; import { Implementation } from './components/implementation'; import { Intro } from './components/intro/intro'; +import { LogoWall } from './components/logo-wall'; import { Observability } from './components/observability'; import { RunAnywhere } from './components/run-anywhere'; import { Templates } from './components/templates'; +import { TweetWall } from './components/tweet-wall'; import { UseCases } from './components/use-cases-server'; const title = 'Make any TypeScript Function Durable'; @@ -29,16 +31,16 @@ const Home = () => (
+
-
- - -
+ + +