diff --git a/.changeset/afraid-bananas-peel.md b/.changeset/afraid-bananas-peel.md new file mode 100644 index 0000000000..2b443bd64a --- /dev/null +++ b/.changeset/afraid-bananas-peel.md @@ -0,0 +1,5 @@ +--- +"@workflow/web": patch +--- + +Support standalone deploy to vercel diff --git a/.changeset/cold-lands-boil.md b/.changeset/cold-lands-boil.md new file mode 100644 index 0000000000..b0cc50aa1d --- /dev/null +++ b/.changeset/cold-lands-boil.md @@ -0,0 +1,7 @@ +--- +"@workflow/core": patch +"@workflow/web-shared": patch +"@workflow/web": patch +--- + +Make encrypted markers clickable to trigger decryption and detect encryption at run level before span selection. Persist `features.encryption` flag in `executionContext` at run creation so the UI can detect encryption without a probe fetch. diff --git a/.changeset/config.json b/.changeset/config.json index 8057e1b746..1c6e7d95e3 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -11,6 +11,10 @@ [ "workflow", "@workflow/core" + ], + [ + "@workflow/web", + "@workflow/web-shared" ] ], "linked": [], diff --git a/.changeset/fix-discovery-weakmap-cache.md b/.changeset/fix-discovery-weakmap-cache.md new file mode 100644 index 0000000000..ec4c712bfc --- /dev/null +++ b/.changeset/fix-discovery-weakmap-cache.md @@ -0,0 +1,5 @@ +--- +"@workflow/builders": patch +--- + +Fix discovery WeakMap cache miss causing duplicate esbuild passes during dev rebuilds diff --git a/.changeset/fix-malformed-tool-call-input.md b/.changeset/fix-malformed-tool-call-input.md new file mode 100644 index 0000000000..eac8cac742 --- /dev/null +++ b/.changeset/fix-malformed-tool-call-input.md @@ -0,0 +1,5 @@ +--- +'@workflow/ai': patch +--- + +Preserve malformed streamed tool-call input until repair hooks can run diff --git a/.changeset/fix-next-build-stale-socket.md b/.changeset/fix-next-build-stale-socket.md new file mode 100644 index 0000000000..c3fd808f2d --- /dev/null +++ b/.changeset/fix-next-build-stale-socket.md @@ -0,0 +1,7 @@ +--- +"@workflow/next": patch +--- + +fix(next): guard socket-info filesystem fallback behind lazy discovery flag + +Prevents `ECONNREFUSED` during `next build` when a stale `workflow-socket.json` file exists from a previous `next dev` session. diff --git a/.changeset/fix-next-version-resolution.md b/.changeset/fix-next-version-resolution.md new file mode 100644 index 0000000000..a36fe29c06 --- /dev/null +++ b/.changeset/fix-next-version-resolution.md @@ -0,0 +1,5 @@ +--- +"@workflow/next": patch +--- + +Fix `next/package.json` resolution failure in npm workspaces monorepos diff --git a/.changeset/fix-step-bundle-dynamic-imports.md b/.changeset/fix-step-bundle-dynamic-imports.md new file mode 100644 index 0000000000..d080230c46 --- /dev/null +++ b/.changeset/fix-step-bundle-dynamic-imports.md @@ -0,0 +1,7 @@ +--- +"@workflow/builders": patch +--- + +fix(builders): add `webpackIgnore` comments to dynamic imports in generated step bundle + +Prevents Turbopack/webpack "Module not found" errors for runtime-resolved dynamic `import()` calls (e.g. from `@vercel/queue`) that are inlined into the step route bundle. diff --git a/.changeset/many-peas-jog.md b/.changeset/many-peas-jog.md new file mode 100644 index 0000000000..9b625f08ab --- /dev/null +++ b/.changeset/many-peas-jog.md @@ -0,0 +1,5 @@ +--- +"@workflow/cli": patch +--- + +Fix false "data expired" warning for runs with future expiredAt diff --git a/.changeset/o11y-run-ref-rendering.md b/.changeset/o11y-run-ref-rendering.md new file mode 100644 index 0000000000..5fe3ec63fa --- /dev/null +++ b/.changeset/o11y-run-ref-rendering.md @@ -0,0 +1,7 @@ +--- +"@workflow/core": patch +"@workflow/web-shared": patch +"@workflow/web": patch +--- + +Add clickable Run reference rendering in observability UI diff --git a/.changeset/pre.json b/.changeset/pre.json index 40919092a7..92d3b7c708 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -46,8 +46,7 @@ "workflow-sdk-compiler-playground": "4.0.0", "@workflow/example-nitro": "4.0.0", "@workflow/vitest": "4.0.0", - "@workflow/vitest-workbench": "4.0.0", - "@workflow/swc-playground-wasm": "0.0.0" + "@workflow/vitest-workbench": "4.0.0" }, "changesets": [ "allow-sync-step-functions", diff --git a/.changeset/vast-oranges-fail.md b/.changeset/vast-oranges-fail.md new file mode 100644 index 0000000000..5471ebeb2f --- /dev/null +++ b/.changeset/vast-oranges-fail.md @@ -0,0 +1,5 @@ +--- +"@workflow/web-shared": patch +--- + +Support in-line decryption for data inspector on the detail panel diff --git a/.changeset/witty-mails-smile.md b/.changeset/witty-mails-smile.md new file mode 100644 index 0000000000..affff22cd9 --- /dev/null +++ b/.changeset/witty-mails-smile.md @@ -0,0 +1,5 @@ +--- +"@workflow/next": patch +--- + +Allow `WORKFLOW_NEXT_LAZY_DISCOVERY=0` to explicitly disable deferred Next.js discovery diff --git a/.github/scripts/generate-docs-data.js b/.github/scripts/generate-docs-data.js index 7adbab3cf2..b0ef918e75 100644 --- a/.github/scripts/generate-docs-data.js +++ b/.github/scripts/generate-docs-data.js @@ -144,7 +144,7 @@ function parseE2EResults(files) { // Extract framework from filename for detailed breakdown const basename = path.basename(file, '.json'); const frameworkMatch = basename.match( - /-(nextjs-turbopack|nextjs-webpack|nitro|nuxt|sveltekit|vite|hono|express|fastify|astro)(?:-(canary|stable))?$/ + /-(nextjs-turbopack|nextjs-webpack|nitro|nuxt|sveltekit|vite|hono|express|fastify|astro)(?:-(?:canary|stable(?:-lazy-discovery-(?:enabled|disabled))?))?$/ ); if (frameworkMatch) { const framework = frameworkMatch[1]; diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 72d48fb4aa..8ad66046e7 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -1,7 +1,7 @@ name: Backport to stable on: - pull_request: + pull_request_target: types: [closed, labeled] branches: [main] @@ -13,8 +13,7 @@ jobs: if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'backport-stable') runs-on: ubuntu-latest permissions: - contents: write - pull-requests: write + contents: read steps: - name: Generate GitHub App Token id: app-token @@ -36,12 +35,103 @@ jobs: git config user.name "$(git log -1 --format='%an' "$MERGE_SHA")" git config user.email "$(git log -1 --format='%ae' "$MERGE_SHA")" git checkout stable - git cherry-pick "$MERGE_SHA" --no-edit - echo "cherry_pick_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - git push origin stable + if git cherry-pick "$MERGE_SHA" --no-edit; then + echo "status=clean" >> "$GITHUB_OUTPUT" + echo "cherry_pick_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + else + echo "status=conflict" >> "$GITHUB_OUTPUT" + fi - - name: Comment on success - if: success() + - name: Push clean backport + if: steps.cherry-pick.outputs.status == 'clean' + run: git push origin stable + + - name: Setup Node.js + if: steps.cherry-pick.outputs.status == 'conflict' + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + + - name: Install opencode + if: steps.cherry-pick.outputs.status == 'conflict' + run: npm install -g opencode-ai@1.4.3 + + - name: Configure opencode + if: steps.cherry-pick.outputs.status == 'conflict' + env: + AI_GATEWAY_TOKEN: ${{ secrets.AI_GATEWAY_TOKEN }} + run: | + mkdir -p ~/.local/share/opencode + jq -n --arg key "$AI_GATEWAY_TOKEN" '{vercel:{type:"api",key:$key}}' > ~/.local/share/opencode/auth.json + + - name: Resolve conflicts with opencode + if: steps.cherry-pick.outputs.status == 'conflict' + id: ai-resolve + continue-on-error: true + env: + OPENCODE_PERMISSION: '{"allow":["*"]}' + MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + COMMIT_MSG=$(git log -1 --format='%B' "$MERGE_SHA") + + cat > /tmp/backport-prompt.txt <>>>>>> is the incoming change from main. + + After resolving each file, run git add on it to mark it as resolved. + Do NOT run git cherry-pick --continue or git commit. + PROMPT + + opencode run --model vercel/anthropic/claude-opus-4.6 "$(cat /tmp/backport-prompt.txt)" + + # Verify all conflicts are resolved + REMAINING=$(git diff --name-only --diff-filter=U || true) + if [ -z "$REMAINING" ]; then + GIT_EDITOR=true git cherry-pick --continue + echo "resolved=true" >> "$GITHUB_OUTPUT" + echo "cherry_pick_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + fi + + - name: Create backport PR with AI-resolved conflicts + if: steps.cherry-pick.outputs.status == 'conflict' && steps.ai-resolve.outputs.resolved == 'true' + id: backport-pr + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + BRANCH="backport/pr-${PR_NUMBER}-to-stable" + git checkout -B "$BRANCH" + git push --force-with-lease origin "$BRANCH" + + EXISTING_PR=$(gh pr list --state open --base stable --head "$BRANCH" --json url --jq '.[0].url') + + if [ -n "$EXISTING_PR" ]; then + PR_URL="$EXISTING_PR" + else + PR_URL=$(gh pr create \ + --base stable \ + --head "$BRANCH" \ + --title "Backport #${PR_NUMBER}: $PR_TITLE" \ + --body "$(cat <<'BODY' + Automated backport of #${{ github.event.pull_request.number }} to `stable`. + + Merge conflicts were resolved by AI ([opencode](https://opencode.ai) with Claude Opus). **Please review the conflict resolution before merging.** + BODY + )") + fi + echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" + + - name: Comment on clean backport + if: steps.cherry-pick.outputs.status == 'clean' uses: actions/github-script@v7 with: github-token: ${{ steps.app-token.outputs.token }} @@ -55,8 +145,21 @@ jobs: body: `Backported to \`stable\` (${originalSha} -> ${cherryPickSha}).` }); - - name: Comment on failure - if: failure() + - name: Comment on AI-resolved backport + if: steps.cherry-pick.outputs.status == 'conflict' && steps.ai-resolve.outputs.resolved == 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: 'Cherry-pick to `stable` had conflicts that were resolved by AI. Please review the backport PR: ${{ steps.backport-pr.outputs.pr_url }}' + }); + + - name: Comment on conflict failure + if: always() && steps.cherry-pick.outputs.status == 'conflict' && steps.ai-resolve.outputs.resolved != 'true' uses: actions/github-script@v7 with: github-token: ${{ steps.app-token.outputs.token }} @@ -67,7 +170,7 @@ jobs: repo: context.repo.repo, issue_number: context.payload.pull_request.number, body: [ - '**Backport to `stable` failed** — the cherry-pick could not be applied cleanly.', + '**Backport to `stable` failed** — the cherry-pick had conflicts that could not be resolved automatically.', '', 'To resolve manually:', '```bash', @@ -80,3 +183,17 @@ jobs: '```' ].join('\n') }); + + - name: Comment on unexpected failure + if: always() && steps.cherry-pick.outputs.status != 'clean' && steps.cherry-pick.outputs.status != 'conflict' + uses: actions/github-script@v7 + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: `**Backport to \`stable\` failed** — unexpected error before the cherry-pick could be attempted. [See workflow run](${runUrl}).` + }); diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 768e857a2f..064205b842 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -253,6 +253,7 @@ jobs: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} WORKFLOW_PUBLIC_MANIFEST: '1' + WORKFLOW_NEXT_LAZY_DISCOVERY: ${{ matrix.app.canary != true && (matrix.app.name == 'nextjs-turbopack' || matrix.app.name == 'nextjs-webpack') && '0' || '' }} steps: - name: Checkout Repo uses: actions/checkout@v4 @@ -332,7 +333,7 @@ jobs: run: echo "matrix=$(node ./scripts/create-test-matrix.mjs)" >> $GITHUB_OUTPUT e2e-local-dev: - name: E2E Local Dev Tests (${{ matrix.app.name }} - ${{ matrix.app.canary && 'canary' || 'stable' }}) + name: E2E Local Dev Tests (${{ matrix.app.name }} - ${{ matrix.app.runLabel }}) runs-on: ubuntu-latest timeout-minutes: 30 if: ${{ !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }} @@ -345,6 +346,7 @@ jobs: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} WORKFLOW_PUBLIC_MANIFEST: '1' + WORKFLOW_NEXT_LAZY_DISCOVERY: ${{ matrix.app.lazyDiscovery == false && '0' || matrix.app.lazyDiscovery == true && '1' || '' }} steps: - name: Checkout Repo @@ -382,7 +384,7 @@ jobs: cd "${{ steps.prepare-workbench.outputs.workbench_app_path }}" && pnpm dev & echo "starting tests in 10 seconds" && sleep 10 pnpm vitest run packages/core/e2e/dev.test.ts; sleep 10 - pnpm run test:e2e --reporter=default --reporter=json --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-dev-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json + pnpm run test:e2e --reporter=default --reporter=json --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-dev-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json env: NODE_OPTIONS: "--enable-source-maps" APP_NAME: ${{ matrix.app.name }} @@ -393,19 +395,19 @@ jobs: - name: Generate E2E summary if: always() - run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Dev (${{ matrix.app.name }})" >> $GITHUB_STEP_SUMMARY || true + run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Dev (${{ matrix.app.name }} - ${{ matrix.app.runLabel }})" >> $GITHUB_STEP_SUMMARY || true - name: Upload E2E results if: always() uses: actions/upload-artifact@v4 with: - name: e2e-results-local-dev-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }} - path: e2e-local-dev-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json + name: e2e-results-local-dev-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }} + path: e2e-local-dev-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json retention-days: 7 if-no-files-found: ignore e2e-local-prod: - name: E2E Local Prod Tests (${{ matrix.app.name }} - ${{ matrix.app.canary && 'canary' || 'stable' }}) + name: E2E Local Prod Tests (${{ matrix.app.name }} - ${{ matrix.app.runLabel }}) runs-on: ubuntu-latest timeout-minutes: 30 if: ${{ !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }} @@ -418,6 +420,7 @@ jobs: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} WORKFLOW_PUBLIC_MANIFEST: '1' + WORKFLOW_NEXT_LAZY_DISCOVERY: ${{ matrix.app.lazyDiscovery == false && '0' || matrix.app.lazyDiscovery == true && '1' || '' }} steps: - name: Checkout Repo @@ -460,7 +463,7 @@ jobs: run: | cd "${{ steps.prepare-workbench.outputs.workbench_app_path }}" && pnpm start & echo "starting tests in 10 seconds" && sleep 10 - pnpm run test:e2e --reporter=default --reporter=json --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-prod-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json + pnpm run test:e2e --reporter=default --reporter=json --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-prod-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json env: NODE_OPTIONS: "--enable-source-maps" APP_NAME: ${{ matrix.app.name }} @@ -470,19 +473,19 @@ jobs: - name: Generate E2E summary if: always() - run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Prod (${{ matrix.app.name }})" >> $GITHUB_STEP_SUMMARY || true + run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Prod (${{ matrix.app.name }} - ${{ matrix.app.runLabel }})" >> $GITHUB_STEP_SUMMARY || true - name: Upload E2E results if: always() uses: actions/upload-artifact@v4 with: - name: e2e-results-local-prod-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }} - path: e2e-local-prod-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json + name: e2e-results-local-prod-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }} + path: e2e-local-prod-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json retention-days: 7 if-no-files-found: ignore e2e-local-postgres: - name: E2E Local Postgres Tests (${{ matrix.app.name }} - ${{ matrix.app.canary && 'canary' || 'stable' }}) + name: E2E Local Postgres Tests (${{ matrix.app.name }} - ${{ matrix.app.runLabel }}) runs-on: ubuntu-latest timeout-minutes: 30 if: ${{ !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }} @@ -512,6 +515,7 @@ jobs: WORKFLOW_PUBLIC_MANIFEST: '1' WORKFLOW_TARGET_WORLD: "@workflow/world-postgres" WORKFLOW_POSTGRES_URL: "postgres://world:world@localhost:5432/world" + WORKFLOW_NEXT_LAZY_DISCOVERY: ${{ matrix.app.lazyDiscovery == false && '0' || matrix.app.lazyDiscovery == true && '1' || '' }} steps: - name: Checkout Repo @@ -557,7 +561,7 @@ jobs: run: | cd "${{ steps.prepare-workbench.outputs.workbench_app_path }}" && pnpm start & echo "starting tests in 10 seconds" && sleep 10 - pnpm run test:e2e --reporter=default --reporter=json --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-postgres-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json + pnpm run test:e2e --reporter=default --reporter=json --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-postgres-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json env: NODE_OPTIONS: "--enable-source-maps" APP_NAME: ${{ matrix.app.name }} @@ -567,14 +571,14 @@ jobs: - name: Generate E2E summary if: always() - run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Postgres (${{ matrix.app.name }})" >> $GITHUB_STEP_SUMMARY || true + run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Postgres (${{ matrix.app.name }} - ${{ matrix.app.runLabel }})" >> $GITHUB_STEP_SUMMARY || true - name: Upload E2E results if: always() uses: actions/upload-artifact@v4 with: - name: e2e-results-local-postgres-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }} - path: e2e-local-postgres-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json + name: e2e-results-local-postgres-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }} + path: e2e-local-postgres-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json retention-days: 7 if-no-files-found: ignore diff --git a/AGENTS.md b/AGENTS.md index f325dc5d8c..592721821e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -190,7 +190,7 @@ Both branches trigger the release workflow (`.github/workflows/release.yml`) on To backport a change from `main` to `stable`, add the `backport-stable` label to the PR on `main`. A GitHub Action (`.github/workflows/backport.yml`) will automatically cherry-pick the squashed commit to `stable`. The label can be added before or after merging — the action triggers on both merge and label events. The changeset file is included in the cherry-pick, so the correct semver bump type is preserved on `stable`. -If the cherry-pick fails due to conflicts, the action will comment on the original PR with instructions for manual resolution. +If the cherry-pick fails due to conflicts, the action will attempt to resolve them automatically using [opencode](https://opencode.ai) (AI-powered conflict resolution). If successful, it creates a PR targeting `stable` for human review instead of pushing directly. If the AI cannot resolve the conflicts, the action will comment on the original PR with instructions for manual resolution. ### Pre-release Lifecycle diff --git a/Cargo.toml b/Cargo.toml index c427bd578b..a3e3b29ed7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["packages/swc-plugin-workflow", "packages/swc-playground-wasm"] +members = ["packages/swc-plugin-workflow", "workbench/swc-playground/wasm"] resolver = "2" 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..268a446aeb 100644 --- a/docs/app/[lang]/(home)/components/hero.tsx +++ b/docs/app/[lang]/(home)/components/hero.tsx @@ -1,11 +1,19 @@ 'use client'; -import { track } from '@vercel/analytics'; -import { CheckIcon, CopyIcon } from 'lucide-react'; -import Link from 'next/link'; -import { useState } from 'react'; -import { toast } from 'sonner'; -import { Button } from '@/components/ui/button'; +import { + CommandPromptContent, + CommandPromptCopy, + CommandPromptList, + CommandPromptPrefix, + CommandPromptRoot, + CommandPromptSurface, + CommandPromptTrigger, + CommandPromptTriggerDivider, + CommandPromptViewport, +} from '@/components/ui/command-prompt'; + +const COMMAND_FOR_HUMANS = 'npm install workflow'; +const COMMAND_FOR_AGENTS = 'npx skills add vercel/workflow@workflow-init'; type HeroProps = { title: string; @@ -13,30 +21,8 @@ type HeroProps = { }; export const Hero = ({ title, description }: HeroProps) => { - const [copied, setCopied] = useState(false); - - const handleCopy = () => { - try { - navigator.clipboard.writeText('npm install workflow'); - setCopied(true); - track('Copy installer command'); - setTimeout(() => { - setCopied(false); - }, 2000); - } catch (error) { - const message = - error instanceof Error - ? error.message - : 'Failed to copy text to clipboard'; - - toast.error(message); - } - }; - - const Icon = copied ? CheckIcon : CopyIcon; - return ( -
+

{title} @@ -50,24 +36,29 @@ export const Hero = ({ title, description }: HeroProps) => { with ease.

-
- -
-
-            npm i workflow
-          
- -
-
+ + + + For humans + + + + For agents + + + + $ + + + {COMMAND_FOR_HUMANS} + + + {COMMAND_FOR_AGENTS} + + + + +
); }; diff --git a/docs/app/[lang]/(home)/components/tweet-wall.tsx b/docs/app/[lang]/(home)/components/tweet-wall.tsx index aa2777d824..c08f7cc411 100644 --- a/docs/app/[lang]/(home)/components/tweet-wall.tsx +++ b/docs/app/[lang]/(home)/components/tweet-wall.tsx @@ -12,6 +12,21 @@ type Tweet = { }; const TWEETS: Tweet[] = [ + // Left column (indices 0-2) + { + url: 'https://x.com/resend/status/1981494746347630976', + name: 'Resend', + username: 'resend', + image: `${BLOB_URL}/resend.jpg`, + tweet: ( + <> + + Resend + @vercel Workflow Dev Kit + + A match made in heaven + + ), + }, { url: 'https://x.com/michaelcaaarter/status/1986078356325187762', name: 'Michael Carter', @@ -50,8 +65,9 @@ const TWEETS: Tweet[] = [ ), }, + // Middle column (indices 3-5) { - url: 'https://x.com/ryancarson/status/1996318671749120315', + url: 'https://x.com/ryancarson/status/1999857760335192159', name: 'Ryan Carson', username: 'ryancarson', image: `${BLOB_URL}/ryancarson.jpg`, @@ -92,6 +108,78 @@ const TWEETS: Tweet[] = [ ), }, + { + url: 'https://x.com/kumareth/status/1981434879805194265', + name: 'Kumar Abhirup', + username: 'kumareth', + image: `${BLOB_URL}/kumareth.jpg`, + tweet: ( + <> + + Vercel's use workflow is game changing. + + + Temporal existed for years, but AI Agents are what brought the + critical demand for durable execution. + + + ), + }, + // Right column (indices 6-8) + { + url: 'https://x.com/nikitabase/status/1982509352486682854', + name: 'Nikita | Scaling Postgres', + username: 'nikitabase', + image: `${BLOB_URL}/nikitabase.jpg`, + tweet: ( + + use workflow is beautiful + + ), + }, + { + url: 'https://x.com/YashSolanki_/status/1992131148823040327', + name: 'Yash Solanki', + username: 'YashSolanki_', + image: `${BLOB_URL}/YashSolanki_.jpg`, + tweet: ( + <> + + If you're building any agentic workflow, then you should + definitely watch this. + + + I feel like using workflow will become the go-to choice for your + projects. + + + Whether it's a side project or complex agentic flows,{' '} + use workflow by{' '} + @vercel just makes so much sense now. + + + Love that the logs screen makes it easier to see what's going. + + + ), + }, + { + url: 'https://x.com/eersnington/status/1982225225010782715', + name: 'Sree', + username: 'eersnington', + image: `${BLOB_URL}/eersnington.jpg`, + tweet: ( + <> + vercel has the vibe as apple during steve jobs era + + durable workflows aren't anything new (and a massive headache to + roll your own too), but vercel isn't afraid of doing things + differently and reinvent them in a way that feels clean and effortless + + this is really exciting to me + + ), + }, ]; function InlineCode({ children }: { children: ReactNode }) { @@ -103,7 +191,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 new file mode 100644 index 0000000000..cd0afcb8d3 --- /dev/null +++ b/docs/app/[lang]/cookbook/[[...slug]]/page.tsx @@ -0,0 +1,135 @@ +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 { ComponentProps } from 'react'; +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 { + DocsBody, + DocsDescription, + DocsPage, + DocsTitle, +} from '@/components/geistdocs/docs-page'; +import { EditSource } from '@/components/geistdocs/edit-source'; +import { Feedback } from '@/components/geistdocs/feedback'; +import { getMDXComponents } from '@/components/geistdocs/mdx-components'; +import { OpenInChat } from '@/components/geistdocs/open-in-chat'; +import { ScrollTop } from '@/components/geistdocs/scroll-top'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { getLLMText, getPageImage, source } from '@/lib/geistdocs/source'; + +const Page = async ({ params }: PageProps<'/[lang]/cookbook/[[...slug]]'>) => { + const { slug, lang } = await params; + + // Prepend 'cookbook' to resolve from the docs source + const resolvedSlug = slug ? ['cookbook', ...slug] : ['cookbook']; + const page = source.getPage(resolvedSlug, lang); + + if (!page) { + notFound(); + } + + const publicUrl = rewriteCookbookUrl(page.url); + const publicPage = { ...page, url: publicUrl } as typeof page; + + const markdown = rewriteCookbookUrlsInText(await getLLMText(page)); + const MDX = page.data.body; + + const RelativeLink = createRelativeLink(source, publicPage); + const PublicCookbookLink = (props: ComponentProps) => { + const href = + typeof props.href === 'string' + ? rewriteCookbookUrl(props.href) + : props.href; + return ; + }; + + return ( + + + + + + + + +
+ ), + }} + tableOfContentPopover={{ enabled: false }} + toc={page.data.toc} + > + + {page.data.title} + {page.data.description} + + + + + ); +}; + +export const generateStaticParams = () => { + // Generate params for all cookbook pages + const allParams = source.generateParams(); + return allParams + .filter((p) => Array.isArray(p.slug) && p.slug[0] === 'cookbook') + .map((p) => ({ + ...p, + slug: (p.slug as string[]).slice(1), // Remove 'cookbook' prefix + })); +}; + +export const generateMetadata = async ({ + params, +}: PageProps<'/[lang]/cookbook/[[...slug]]'>) => { + const { slug, lang } = await params; + const resolvedSlug = slug ? ['cookbook', ...slug] : ['cookbook']; + const page = source.getPage(resolvedSlug, lang); + + if (!page) { + notFound(); + } + + const publicPath = rewriteCookbookUrl(page.url); + + const metadata: Metadata = { + title: page.data.title, + description: page.data.description, + openGraph: { + images: getPageImage(page).url, + }, + alternates: { + canonical: publicPath, + types: { + 'text/markdown': `${publicPath}.md`, + }, + }, + }; + + return metadata; +}; + +export default Page; diff --git a/docs/app/[lang]/cookbook/layout.tsx b/docs/app/[lang]/cookbook/layout.tsx new file mode 100644 index 0000000000..63235fbb14 --- /dev/null +++ b/docs/app/[lang]/cookbook/layout.tsx @@ -0,0 +1,17 @@ +import { DocsLayout } from "@/components/geistdocs/docs-layout"; +import { getCookbookTree } from "@/lib/geistdocs/cookbook-source"; + +const Layout = async ({ + children, + params, +}: LayoutProps<"/[lang]/cookbook">) => { + const { lang } = await params; + + return ( +
+ {children} +
+ ); +}; + +export default Layout; diff --git a/docs/app/[lang]/docs/[[...slug]]/page.tsx b/docs/app/[lang]/docs/[[...slug]]/page.tsx index e1bca864c3..0c4ca276fe 100644 --- a/docs/app/[lang]/docs/[[...slug]]/page.tsx +++ b/docs/app/[lang]/docs/[[...slug]]/page.tsx @@ -2,7 +2,8 @@ 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 { notFound, permanentRedirect } from 'next/navigation'; +import { rewriteCookbookUrl } from '@/lib/geistdocs/cookbook-source'; import { AgentTraces } from '@/components/custom/agent-traces'; import { FluidComputeCallout } from '@/components/custom/fluid-compute-callout'; import { AskAI } from '@/components/geistdocs/ask-ai'; @@ -16,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'; @@ -31,6 +33,12 @@ const WorldTestingPerformanceNoop = () => null; const Page = async ({ params }: PageProps<'/[lang]/docs/[[...slug]]'>) => { const { slug, lang } = await params; + if (Array.isArray(slug) && slug[0] === 'cookbook') { + const rest = slug.slice(1).join('/'); + const legacyPath = `/docs/cookbook${rest ? `/${rest}` : ''}`; + permanentRedirect(`/${lang}${rewriteCookbookUrl(legacyPath)}`); + } + const page = source.getPage(slug, lang); if (!page) { @@ -57,8 +65,10 @@ const Page = async ({ params }: PageProps<'/[lang]/docs/[[...slug]]'>) => {
), }} + tableOfContentPopover={{ enabled: false }} toc={page.data.toc} > + {page.data.title} {page.data.description} @@ -85,7 +95,13 @@ const Page = async ({ params }: PageProps<'/[lang]/docs/[[...slug]]'>) => { ); }; -export const generateStaticParams = () => source.generateParams(); +export const generateStaticParams = () => + source + .generateParams() + .filter( + (params) => + !(Array.isArray(params.slug) && params.slug[0] === 'cookbook'), + ); export const generateMetadata = async ({ params, diff --git a/docs/app/[lang]/docs/layout.tsx b/docs/app/[lang]/docs/layout.tsx index 583e850925..582f3d7139 100644 --- a/docs/app/[lang]/docs/layout.tsx +++ b/docs/app/[lang]/docs/layout.tsx @@ -1,10 +1,16 @@ -import { DocsLayout } from '@/components/geistdocs/docs-layout'; -import { source } from '@/lib/geistdocs/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}; + return ( +
+ + {children} + +
+ ); }; export default Layout; diff --git a/docs/app/[lang]/layout.tsx b/docs/app/[lang]/layout.tsx index 98c8e61210..23b49d8cb2 100644 --- a/docs/app/[lang]/layout.tsx +++ b/docs/app/[lang]/layout.tsx @@ -28,7 +28,7 @@ const Layout = async ({ children, params }: LayoutProps<'/[lang]'>) => { return ( diff --git a/docs/app/[lang]/llms.mdx/[[...slug]]/route.ts b/docs/app/[lang]/llms.mdx/[[...slug]]/route.ts index 8f6eb71527..897e54d56a 100644 --- a/docs/app/[lang]/llms.mdx/[[...slug]]/route.ts +++ b/docs/app/[lang]/llms.mdx/[[...slug]]/route.ts @@ -1,4 +1,5 @@ import { notFound } from 'next/navigation'; +import { rewriteCookbookUrlsInText } from '@/lib/geistdocs/cookbook-source'; import { getLLMText, source } from '@/lib/geistdocs/source'; import { i18n } from '@/lib/geistdocs/i18n'; @@ -18,8 +19,10 @@ export async function GET( const sitemapPath = lang === i18n.defaultLanguage ? '/sitemap.md' : `/${lang}/sitemap.md`; + const text = await getLLMText(page); + return new Response( - (await getLLMText(page)) + + rewriteCookbookUrlsInText(text) + `\n\n## Sitemap [Overview of all docs pages](${sitemapPath})\n`, { diff --git a/docs/app/[lang]/llms.txt/route.ts b/docs/app/[lang]/llms.txt/route.ts index 96f061223d..343ad45b0c 100644 --- a/docs/app/[lang]/llms.txt/route.ts +++ b/docs/app/[lang]/llms.txt/route.ts @@ -1,4 +1,5 @@ import type { NextRequest } from 'next/server'; +import { rewriteCookbookUrlsInText } from '@/lib/geistdocs/cookbook-source'; import { getLLMText, source } from '@/lib/geistdocs/source'; export const revalidate = false; @@ -11,7 +12,7 @@ export const GET = async ( const scan = source.getPages(lang).map(getLLMText); const scanned = await Promise.all(scan); - return new Response(scanned.join('\n\n'), { + return new Response(rewriteCookbookUrlsInText(scanned.join('\n\n')), { headers: { 'Content-Type': 'text/markdown; charset=utf-8', }, diff --git a/docs/app/[lang]/sitemap.md/route.ts b/docs/app/[lang]/sitemap.md/route.ts index 1912d496d9..7c193e126d 100644 --- a/docs/app/[lang]/sitemap.md/route.ts +++ b/docs/app/[lang]/sitemap.md/route.ts @@ -1,4 +1,5 @@ import type { Node, Root } from 'fumadocs-core/page-tree'; +import { rewriteCookbookUrl } from '@/lib/geistdocs/cookbook-source'; import { source } from '@/lib/geistdocs/source'; export const revalidate = false; @@ -16,10 +17,10 @@ export async function GET( if ('type' in node) { if (node.type === 'page') { - mdText += `${indent}- [${node.name}](${node.url})\n`; + mdText += `${indent}- [${node.name}](${rewriteCookbookUrl(node.url)})\n`; } else if (node.type === 'folder') { if (node.index) { - mdText += `${indent}- [${node.name}](${node.index.url})\n`; + mdText += `${indent}- [${node.name}](${rewriteCookbookUrl(node.index.url)})\n`; } else { mdText += `${indent}- ${node.name}\n`; } @@ -30,7 +31,6 @@ export async function GET( } } } else if (node.children.length > 0) { - // Root node for (const child of node.children) { traverseTree(child, depth); } 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) => ( (