Compare commits

..
Author SHA1 Message Date
Chris Tate d585afe9c4 Reveal completed Jev layouts atomically in the playground 2026-09-18 13:11:23 -05:00
149 changed files with 780 additions and 9474 deletions
-21
View File
@@ -53,27 +53,6 @@ jobs:
run: pnpm turbo run build --filter='./packages/*'
- run: pnpm test
docs:
name: Docs (${{ matrix.environment }})
runs-on: ubuntu-latest
strategy:
matrix:
environment: [production, preview]
env:
VERCEL_ENV: ${{ matrix.environment }}
DOCS_EXPECT_NOINDEX: ${{ matrix.environment == 'preview' && '1' || '0' }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm turbo run build --filter='web^...'
- run: pnpm --filter web build
- run: pnpm --filter web test:routes
typecheck:
name: Type Check
runs-on: ubuntu-latest
+1 -3
View File
@@ -66,8 +66,6 @@ Do **not** add `--port` flags -- portless handles port assignment automatically.
## Workflow
- Run `pnpm type-check` after each turn to ensure type safety
- Documentation lives in `apps/web/content/docs/` and uses Geistdocs frontmatter. Keep public `/docs` URLs, heading IDs, `lib/page-titles.ts`, `lib/docs-navigation.ts`, and the content `meta.json` files in sync.
- For docs routing or infrastructure changes, run `pnpm turbo run build --filter='web^...'`, `pnpm --filter web build`, and `pnpm --filter web test:routes`. Existing-page source and Markdown parity are covered by `apps/web/tests/fixtures/docs-baseline.json`; update fixtures only when intentionally changing the documented content.
- When making user-facing changes (new packages, API changes, new features, renamed exports, changed behavior), update the relevant documentation:
- Package `README.md` files in `packages/*/README.md`
- Root `README.md` (if packages table, install commands, or examples are affected)
@@ -92,7 +90,7 @@ When asked to prepare a release (e.g. "prepare v0.17.0"):
5. **Fill documentation gaps** — every public package should have:
- A row in the root `README.md` packages table
- A renderer section in the root `README.md` (if it's a renderer)
- An API reference page at `apps/web/content/docs/api/<name>.mdx`
- An API reference page at `apps/web/app/(main)/docs/api/<name>/page.mdx`
- An entry in `apps/web/lib/page-titles.ts` and `apps/web/lib/docs-navigation.ts`
- An entry in the docs-chat system prompt (`apps/web/app/api/docs-chat/route.ts`)
- A skill at `skills/<name>/SKILL.md`
+2 -20
View File
@@ -1,30 +1,11 @@
# Changelog
## 0.21.0
## 0.20.0
<!-- release:start -->
### New Features
- **TanStack Start renderer:** Added `@json-render/tanstack-start` for JSON-defined applications with file-based routes, reusable layouts, SSR loaders, head metadata, prerender paths, client navigation, and route fallbacks (#334)
- **Experimental Jev composition:** Added `experimental_composeSpec` and `experimental_createEvaluator` to compose validated specs from app-owned candidates, plus a Jev model option and iterative composition editing in the playground
### Improvements
- **Vue named slots:** Vue registries now support catalog-declared named slots alongside the default `children` slot (#323)
- **React streaming stability:** Stabilized streamed React renders and added coverage for incomplete streamed props and nested prop identities (#325)
- **Documentation and project status:** Expanded renderer, Jev, and package documentation and added Labs status badges to the project README
### Contributors
- @ctate
- @Railly
<!-- release:end -->
## 0.20.0
### New Features
- **Named slots for React:** Components can declare named slots such as `header` and `footer`, while `children` remains the default slot. Slots are preserved through validation, streaming, nested conversion, code export, playground views, and Devtools navigation (#320). Built from the original contribution by @wotnak in #105
- **Nested repeats:** `repeat.statePath` now accepts item-relative paths such as `{ "$item": "employees" }`, enabling nested data rendering across React, React Native, React Email, React PDF, Image, Ink, Solid, Svelte, and Vue (#319). Built from the original contribution by @tmchow in #256
- **Harness chat example:** Added a complete Next.js example using the AI SDK 7 harness adapter, agent delegation, sandbox transport, and json-render components (#302)
@@ -49,6 +30,7 @@
- @Railly
- @tmchow
- @wotnak
<!-- release:end -->
## 0.19.0
-1
View File
@@ -11,7 +11,6 @@
# next.js
/.next/
/.source/
/out/
# production
@@ -1,6 +1,7 @@
---
title: "A2UI Integration"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/a2ui")
# A2UI Integration
Use `@json-render/core` to support [A2UI](https://a2ui.org) natively.
@@ -1,6 +1,7 @@
---
title: "Adaptive Cards Integration"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/adaptive-cards")
# Adaptive Cards Integration
Use json-render to render [Microsoft Adaptive Cards](https://adaptivecards.io) natively.
@@ -1,6 +1,7 @@
---
title: "AG-UI Integration"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/ag-ui")
# AG-UI Integration
Use json-render to support [AG-UI](https://docs.copilotkit.ai/ag-ui) (Agent User Interaction Protocol) from CopilotKit.
@@ -1,6 +1,7 @@
---
title: "AI SDK Integration"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/ai-sdk")
# AI SDK Integration
Use json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless streaming. json-render supports two modes: **Standalone** (standalone UI) and **Inline** (UI embedded in conversation). See [Generation Modes](/docs/generation-modes) for a detailed comparison.
@@ -1,6 +1,7 @@
---
title: "@json-render/codegen"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/codegen")
# @json-render/codegen
Utilities for generating code from UI trees.
@@ -1,6 +1,7 @@
---
title: "@json-render/core"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/core")
# @json-render/core
Core types, schemas, and utilities.
@@ -550,19 +551,14 @@ const name2 = resolveDynamicValue({ $state: "/user/name" }, state); // "Alice"
### findFormValue
Read a value from resolved action parameters or state. A parameter value is literal, including strings with dots such as emails, URLs, and versions. Lookup order: a defined direct parameter, a parameter key ending in `.<fieldName>`, a matching flat state key, then a slash-delimited path in nested state.
```typescript
import { findFormValue } from '@json-render/core';
findFormValue("email", { email: "john.doe@example.com" }, {});
findFormValue("email", { "form.email": "john.doe@example.com" }, {});
findFormValue("email", {}, { "form.email": "john.doe@example.com" });
findFormValue("/form/email", {}, { form: { email: "john.doe@example.com" } });
// Find form values regardless of path format
// Checks: params.name, params["form.name"], state["form.name"], state.form.name
const value = findFormValue("name", params, state);
```
For action bindings, use `{ $state: "/form/email" }` to read nested state: the action resolver passes the resulting value to the handler. A raw string like `"form.email"` in parameters is not a state reference. A bare `"email"` field name does not search `state.form.email`.
## buildUserPrompt
Build structured user prompts for AI generation, with support for refinement and state context.
@@ -1,6 +1,7 @@
---
title: "@json-render/devtools-react"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/devtools-react")
# @json-render/devtools-react
React adapter for the json-render devtools. Drop-in `<JsonRenderDevtools />` component.
@@ -1,6 +1,7 @@
---
title: "@json-render/devtools-solid"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/devtools-solid")
# @json-render/devtools-solid
SolidJS adapter for the json-render devtools. Drop-in `<JsonRenderDevtools />` component.
@@ -1,6 +1,7 @@
---
title: "@json-render/devtools-svelte"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/devtools-svelte")
# @json-render/devtools-svelte
Svelte adapter for the json-render devtools. Drop-in `<JsonRenderDevtools />` component.
@@ -1,6 +1,7 @@
---
title: "@json-render/devtools-vue"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/devtools-vue")
# @json-render/devtools-vue
Vue adapter for the json-render devtools. Drop-in `<JsonRenderDevtools />` component.
@@ -1,6 +1,7 @@
---
title: "@json-render/devtools"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/devtools")
# @json-render/devtools
Framework-agnostic core for the json-render devtools — vanilla TS panel UI, event store, DOM picker, and stream tap utilities. Every framework-specific adapter package depends on this.
@@ -1,6 +1,7 @@
---
title: "@json-render/directives"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/directives")
# @json-render/directives
Pre-built custom directives for `@json-render/core`. Drop them into your catalog and renderer to add formatting, math, string manipulation, and i18n.
@@ -1,6 +1,7 @@
---
title: "@json-render/image"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/image")
# @json-render/image
Image renderer. Turn JSON specs into SVG and PNG images using [Satori](https://github.com/vercel/satori).
@@ -1,6 +1,7 @@
---
title: "@json-render/ink"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/ink")
# @json-render/ink
Terminal renderer for [Ink](https://github.com/vadimdemedes/ink) with multiple standard components, providers, hooks, and streaming support.
@@ -1,6 +1,7 @@
---
title: "@json-render/jotai"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/jotai")
# @json-render/jotai
Jotai adapter for json-render's `StateStore` interface.
@@ -1,6 +1,7 @@
---
title: "@json-render/mcp"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/mcp")
# @json-render/mcp
MCP Apps integration for json-render. Serve json-render UIs as interactive [MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps) inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.
@@ -1,6 +1,7 @@
---
title: "@json-render/next"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/next")
# @json-render/next
Next.js renderer. JSON becomes full Next.js applications with routes, layouts, metadata, and SSR.
@@ -1,6 +1,7 @@
---
title: "@json-render/react-email"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react-email")
# @json-render/react-email
React Email renderer. Turn JSON specs into HTML or plain-text emails using `@react-email/components` and `@react-email/render`.
@@ -1,6 +1,7 @@
---
title: "@json-render/react-native"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react-native")
# @json-render/react-native
React Native renderer with standard components, providers, and hooks.
@@ -1,6 +1,7 @@
---
title: "@json-render/react-pdf"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react-pdf")
# @json-render/react-pdf
PDF document renderer. Turn JSON specs into PDFs using `@react-pdf/renderer`.
@@ -1,6 +1,7 @@
---
title: "@json-render/react-three-fiber"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react-three-fiber")
# @json-render/react-three-fiber
React Three Fiber renderer for json-render. 20 built-in 3D components for meshes, lights, models, gaussian splats, environments, text, cameras, and controls.
@@ -1,6 +1,7 @@
---
title: "@json-render/react"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react")
# @json-render/react
React components, providers, and hooks.
@@ -1,6 +1,7 @@
---
title: "@json-render/redux"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/redux")
# @json-render/redux
Redux / Redux Toolkit adapter for json-render's `StateStore` interface.
@@ -1,6 +1,7 @@
---
title: "@json-render/remotion"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/remotion")
# @json-render/remotion
Remotion video renderer. Turn JSON timeline specs into video compositions.
@@ -1,6 +1,7 @@
---
title: "@json-render/shadcn-svelte"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/shadcn-svelte")
# @json-render/shadcn-svelte
Pre-built [shadcn-svelte](https://www.shadcn-svelte.com/) components for json-render. 36 components built on Svelte 5 + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.
@@ -1,6 +1,7 @@
---
title: "@json-render/shadcn"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/shadcn")
# @json-render/shadcn
Pre-built [shadcn/ui](https://ui.shadcn.com/) components for json-render. 36 components built on Radix UI + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.
@@ -1,6 +1,7 @@
---
title: "@json-render/solid"
---
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/api/solid");
# @json-render/solid
SolidJS components, providers, and hooks for rendering json-render specs.
@@ -1,6 +1,7 @@
---
title: "@json-render/svelte"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/svelte")
# @json-render/svelte
Svelte 5 components, providers, and helpers for rendering json-render specs.
@@ -1,6 +1,8 @@
---
title: "@json-render/tanstack-start"
---
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/api/tanstack-start");
# @json-render/tanstack-start
TanStack Start renderer for JSON-defined applications with routes, layouts,
head metadata, SSR loaders, prerender paths, and client navigation.
@@ -1,6 +1,7 @@
---
title: "@json-render/vue"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/vue")
# @json-render/vue
Vue 3 components, providers, and composables.
@@ -1,6 +1,7 @@
---
title: "@json-render/xstate"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/xstate")
# @json-render/xstate
[XState Store](https://stately.ai/docs/xstate-store) adapter for json-render's `StateStore` interface.
@@ -1,6 +1,7 @@
---
title: "@json-render/yaml"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/yaml")
# @json-render/yaml
YAML wire format for json-render. Progressive rendering and surgical edits via streaming YAML.
@@ -1,6 +1,7 @@
---
title: "@json-render/zustand"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/zustand")
# @json-render/zustand
Zustand adapter for json-render's `StateStore` interface.
@@ -1,6 +1,7 @@
---
title: "Catalog"
---
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/catalog");
# Catalog
The catalog defines what AI can generate. It's your guardrail.
@@ -1,35 +1,10 @@
---
title: "Changelog"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/changelog")
# Changelog
Notable changes and updates to json-render.
## v0.21.0
September 18, 2026
### New: TanStack Start Renderer
Added `@json-render/tanstack-start` for JSON-defined applications with file-based routes, reusable layouts, SSR loaders, head metadata, prerender paths, client navigation, and route fallbacks.
See the [TanStack Start API reference](/docs/api/tanstack-start) for setup and route configuration.
### New: Experimental Jev Composition
Added `experimental_composeSpec` and `experimental_createEvaluator` to `@json-render/core`. Apps can provide their own catalogs and bounded component candidates, then stream validated compositions through an evaluation model. The playground now includes a Jev model option and supports iterative composition edits.
These APIs are experimental and may change in any release. See the [Jev guide](/docs/jev) for the source-build workflow, examples, and current limitations.
### Improved: Vue Named Slots
Vue registries now support catalog-declared named slots alongside the default `children` slot.
### Fixed: React Streaming Stability
Stabilized streamed React renders and added coverage for incomplete streamed props and nested prop identities.
---
## v0.20.0
August 15, 2026
@@ -1,6 +1,7 @@
---
title: "Code Export"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/code-export")
# Code Export
Export generated UI as standalone code for your framework.
@@ -1,6 +1,7 @@
---
title: "Computed Values"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/computed-values")
# Computed Values
Derive dynamic prop values using registered functions or string templates.
@@ -1,6 +1,7 @@
---
title: "Custom Schema & Renderer"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/custom-schema")
# Custom Schema & Renderer
Build your own schema and renderer with `@json-render/core`.
@@ -1,6 +1,7 @@
---
title: "Data Binding"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/data-binding")
# Data Binding
Connect UI elements to dynamic data using expressions in your JSON specs.
@@ -1,6 +1,7 @@
---
title: "Devtools"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/devtools")
# Devtools
A drop-in inspector panel for any json-render app. See the spec tree, edit state inline, watch dispatched actions, follow stream patches live, browse your catalog, and pick DOM elements to map them back to spec keys.
@@ -1,6 +1,7 @@
---
title: "Directives"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/directives")
# Directives
Extend the spec language with custom `$`-prefixed dynamic values. Directives let you add formatting, math, string manipulation, i18n, and any other transformation without modifying core.
@@ -1,6 +1,7 @@
---
title: "Generation Modes"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/generation-modes")
# Generation Modes
json-render supports two modes for AI-generated UI: **Standalone mode** for standalone UI and **Inline mode** for inline UI within a conversation.
@@ -1,6 +1,7 @@
---
title: "Installation"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/installation")
# Installation
Install the core package plus your renderer of choice.
@@ -1,6 +1,8 @@
---
title: "Jev (Experimental)"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/jev")
# Jev (Experimental)
**Experimental:** `experimental_composeSpec` and `experimental_createEvaluator` are reusable APIs in `@json-render/core`. Like AI SDK's experimental APIs, names prefixed with `experimental_` or `Experimental_` may change in any release. Pin exact package versions (no `^` or `~`) and review release notes before upgrading.
**Availability:** these APIs are unreleased. You can try the source build below before they appear in a published npm version.
@@ -149,6 +151,8 @@ Initialize your renderer's state from `spec.state`. Keep user interaction disabl
On `complete`, inspect `stopReason`: `finish` means composition finished; `unavailable` means the evaluator could not fulfill the request; `limit` means a call, element, or depth budget prevented completion. A complete event can contain a partial spec, or `null` when no root was added. Completion is not a correctness guarantee. Errors and cancellation throw; retain the last snapshot and label it incomplete. Each batched trace is one evaluation (`select` or `layout`), with the individual choices in `step.answers` and usage/timing counted once.
To avoid visible rearrangement, the playground waits for a successful `complete` event before revealing a new tree in one render. Decision metadata still streams while composing. Follow-up edits continue to update the existing preview incrementally. Apps can choose the same display policy while retaining the core API's provisional snapshots for inspection.
The playground is a reference implementation: [candidates and server wrapper](https://github.com/vercel-labs/json-render/tree/main/apps/web/lib/jev), [streaming route](https://github.com/vercel-labs/json-render/blob/main/apps/web/app/api/generate/route.ts), and [client](https://github.com/vercel-labs/json-render/blob/main/apps/web/components/playground.tsx).
## Validation and v1 limits
+35
View File
@@ -0,0 +1,35 @@
import { DocsMobileNav } from "@/components/docs-mobile-nav";
import { DocsSidebar } from "@/components/docs-sidebar";
import { CopyPageButton } from "@/components/copy-page-button";
import { TableOfContents } from "@/components/table-of-contents";
export default function DocsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<DocsMobileNav />
<div className="max-w-7xl mx-auto px-6 py-8 lg:py-12 flex gap-12">
{/* Sidebar */}
<aside className="w-48 shrink-0 hidden lg:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto">
<DocsSidebar />
</aside>
{/* Content */}
<div className="flex-1 min-w-0 max-w-2xl pb-20">
<div className="flex justify-end mb-4">
<CopyPageButton />
</div>
<article>{children}</article>
</div>
{/* On this page */}
<aside className="w-44 shrink-0 hidden xl:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto">
<TableOfContents />
</aside>
</div>
</>
);
}
@@ -1,6 +1,7 @@
---
title: "Migration Guide"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/migration")
# Migration Guide
This guide covers breaking changes introduced in v0.6.0 and how to update your code.
@@ -1,6 +1,7 @@
---
title: "OpenAPI Integration"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/openapi")
# OpenAPI Integration
Use json-render to generate dynamic forms and UIs from [OpenAPI/Swagger](https://swagger.io/specification/) schemas.
@@ -1,6 +1,7 @@
---
title: "Introduction"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs")
# Introduction
json-render is a framework for **Generative UI** — AI-generated interfaces that are safe, predictable, and render natively on any platform.
@@ -1,6 +1,7 @@
---
title: "Quick Start"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/quick-start")
# Quick Start
Get up and running with json-render in 5 minutes.
@@ -1,6 +1,7 @@
---
title: "Registry"
---
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/registry");
# Registry
A registry maps your [catalog](/docs/catalog) definitions to platform-specific implementations. The catalog defines _what_ AI can generate; the registry provides the _how_.
@@ -1,6 +1,7 @@
---
title: "Renderers"
---
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/renderers");
# Renderers
json-render supports multiple output targets. Each renderer takes the same core concept -- a JSON spec constrained to a catalog -- and renders it natively on a different platform or into a different format.
@@ -1,6 +1,7 @@
---
title: "Schemas"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/schemas")
# Schemas
Schemas define the structure and validation rules for your UI specs.
@@ -1,6 +1,8 @@
---
title: "Skills"
---
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/skills");
# Skills
json-render ships with skills that teach AI coding agents how to use each package. Install a skill and your agent in Cursor, Claude Code, or Codex can generate json-render UIs without manual guidance.
@@ -9,11 +11,6 @@ json-render ships with skills that teach AI coding agents how to use each packag
- **core** — Core schemas, catalogs, and AI prompt generation.
- **react** — React renderer that turns JSON specs into React component trees.
- **tanstack-start** — Full TanStack Start applications with routes, layouts, SSR loaders, and head metadata.
- **devtools** — Framework-agnostic inspector panel for specs, state, actions, streams, catalogs, and DOM picking.
- **devtools-react** — React adapter for the json-render devtools panel.
- **devtools-vue** — Vue adapter for the json-render devtools panel.
- **devtools-svelte** — Svelte adapter for the json-render devtools panel.
- **devtools-solid** — SolidJS adapter for the json-render devtools panel.
- **react-pdf** — PDF renderer using `@react-pdf/renderer`.
- **react-email** — Email renderer that produces HTML or plain-text emails.
- **react-native** — React Native renderer for native mobile UIs.
@@ -36,11 +33,6 @@ json-render ships with skills that teach AI coding agents how to use each packag
npx skills add vercel-labs/json-render --skill core
npx skills add vercel-labs/json-render --skill react
npx skills add vercel-labs/json-render --skill tanstack-start
npx skills add vercel-labs/json-render --skill devtools
npx skills add vercel-labs/json-render --skill devtools-react
npx skills add vercel-labs/json-render --skill devtools-vue
npx skills add vercel-labs/json-render --skill devtools-svelte
npx skills add vercel-labs/json-render --skill devtools-solid
npx skills add vercel-labs/json-render --skill react-pdf
npx skills add vercel-labs/json-render --skill react-email
npx skills add vercel-labs/json-render --skill react-native
@@ -72,26 +64,6 @@ Teaches agents how to render JSON specs as React component trees using `JsonRend
Teaches agents how to build JSON-defined TanStack Start applications with splat routes, reusable layouts, SSR-safe loaders, head metadata, prerender paths, and client navigation.
## devtools
Teaches agents how to add and configure the framework-agnostic json-render devtools panel, including spec inspection, state editing, action and stream timelines, catalog browsing, and DOM picking.
## devtools-react
Teaches agents how to mount and control `@json-render/devtools-react` inside a React json-render provider, including the imperative devtools hook.
## devtools-vue
Teaches agents how to mount and configure `@json-render/devtools-vue` inside a Vue json-render provider.
## devtools-svelte
Teaches agents how to mount and configure `@json-render/devtools-svelte` inside a Svelte 5 json-render provider.
## devtools-solid
Teaches agents how to mount and configure `@json-render/devtools-solid` inside a SolidJS json-render provider.
## react-pdf
Teaches agents how to generate PDFs from JSON specs using `@react-pdf/renderer`. Covers the PDF-specific component registry, page layout, and styling.
@@ -1,6 +1,7 @@
---
title: "Specs"
---
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/specs");
# Specs
A spec is a JSON document that describes your UI.
@@ -1,6 +1,7 @@
---
title: "Streaming"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/streaming")
# Streaming
Progressively render UI as AI generates it.
@@ -1,6 +1,7 @@
---
title: "Validation"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/validation")
# Validation
Validate form inputs with built-in and custom functions.
@@ -1,6 +1,7 @@
---
title: "Visibility"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/visibility")
# Visibility
Conditionally show or hide components based on state values and logic.
@@ -1,6 +1,7 @@
---
title: "Watchers"
---
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/watchers")
# Watchers
React to state changes by triggering actions when watched paths update.
-8
View File
@@ -1,8 +0,0 @@
import type { ReactNode } from "react";
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("examples");
export default function ExamplesLayout({ children }: { children: ReactNode }) {
return children;
}
+12 -2
View File
@@ -1,7 +1,17 @@
export default function MainLayout({
import { Header } from "@/components/header";
import { getStarCount } from "@/lib/github";
export default async function MainLayout({
children,
}: {
children: React.ReactNode;
}) {
return <main className="min-h-[calc(100dvh-4rem)]">{children}</main>;
const stars = await getStarCount();
return (
<div className="min-h-screen flex flex-col">
<Header stars={stars} />
<main className="flex-1">{children}</main>
</div>
);
}
@@ -1,43 +0,0 @@
import { MobileDocsBar } from "@vercel/geistdocs/mobile-docs-bar";
import { createDocsPage } from "@vercel/geistdocs/pages/docs";
import { notFound } from "next/navigation";
import { GenerationModesDiagram } from "@/components/generation-modes-diagram";
import { PackageInstall } from "@/components/package-install";
import { isSafePathSegments } from "@/lib/docs-source";
import { config } from "@/lib/geistdocs/config";
import { geistdocsSource } from "@/lib/geistdocs/source";
import { pageMetadata } from "@/lib/page-metadata";
type PageProps = { params: Promise<{ lang: string; slug?: string[] }> };
async function validate(params: PageProps["params"]) {
const resolved = await params;
if (resolved.lang !== "en" || !isSafePathSegments(resolved.slug ?? []))
notFound();
try {
if (!geistdocsSource.source.getPage(resolved.slug, resolved.lang))
notFound();
} catch (error) {
if (error instanceof URIError) notFound();
throw error;
}
return resolved;
}
const docsPage = createDocsPage({
config,
source: geistdocsSource,
mdx: { GenerationModesDiagram, PackageInstall },
renderTop: ({ data }) => <MobileDocsBar toc={data.toc} />,
});
export default async function Page({ params }: PageProps) {
return <docsPage.Page params={Promise.resolve(await validate(params))} />;
}
export async function generateMetadata({ params }: PageProps) {
const { slug = [] } = await validate(params);
return pageMetadata(["docs", ...slug].join("/"));
}
export const generateStaticParams = docsPage.generateStaticParams;
-25
View File
@@ -1,25 +0,0 @@
import type { ReactNode } from "react";
import { notFound } from "next/navigation";
import { GeistdocsDocsLayout } from "@vercel/geistdocs/layout";
import { config } from "@/lib/geistdocs/config";
import { geistdocsSource } from "@/lib/geistdocs/source";
export default async function DocsLayout({
children,
params,
}: {
children: ReactNode;
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
if (lang !== "en") notFound();
return (
<GeistdocsDocsLayout
config={config}
tree={geistdocsSource.source.getPageTree(lang)}
containerProps={{ className: "mx-auto max-w-[1448px]" }}
>
{children}
</GeistdocsDocsLayout>
);
}
+35 -8
View File
@@ -1,8 +1,11 @@
import { readFile } from "fs/promises";
import { join } from "path";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import type { ModelMessage, UIMessage } from "ai";
import { createBashTool } from "bash-tool";
import { headers } from "next/headers";
import { loadAllDocsSources } from "@/lib/docs-source";
import { allDocsPages } from "@/lib/docs-navigation";
import { mdxToCleanMarkdown } from "@/lib/mdx-to-markdown";
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
export const maxDuration = 60;
@@ -14,7 +17,7 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render
GitHub repository: https://github.com/vercel-labs/json-render
Documentation: https://json-render.dev/docs
npm packages: @json-render/core, @json-render/react, @json-render/next, @json-render/tanstack-start, @json-render/ink, @json-render/vue, @json-render/svelte, @json-render/solid, @json-render/shadcn, @json-render/shadcn-svelte, @json-render/react-three-fiber, @json-render/react-native, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/directives, @json-render/codegen, @json-render/devtools, @json-render/devtools-react, @json-render/devtools-vue, @json-render/devtools-svelte, @json-render/devtools-solid, @json-render/mcp, @json-render/redux, @json-render/zustand, @json-render/jotai, @json-render/xstate, @json-render/yaml
Skills: json-render ships AI agent skills that teach coding agents how to use each package. Install with "npx skills add vercel-labs/json-render --skill <name>". Available skills: core, react, next, tanstack-start, ink, react-pdf, react-email, react-native, shadcn, shadcn-svelte, react-three-fiber, image, remotion, vue, svelte, solid, directives, codegen, devtools, devtools-react, devtools-vue, devtools-svelte, devtools-solid, mcp, redux, zustand, jotai, xstate, yaml. See /docs/skills for details.
Skills: json-render ships AI agent skills that teach coding agents how to use each package. Install with "npx skills add vercel-labs/json-render --skill <name>". Available skills: core, react, next, tanstack-start, ink, react-pdf, react-email, react-native, shadcn, shadcn-svelte, react-three-fiber, image, remotion, vue, svelte, solid, directives, codegen, devtools, mcp, redux, zustand, jotai, xstate, yaml. See /docs/skills for details.
Experimental Jev composition: core exports experimental_composeSpec and experimental_createEvaluator for app-owned catalogs/candidates through Vercel AI Gateway. See /docs/jev for availability, source-build setup, and limits; do not assume the currently published npm version includes it.
@@ -31,13 +34,37 @@ When answering questions:
- Do NOT use emojis in your responses`;
async function loadDocsFiles(): Promise<Record<string, string>> {
const pages = await loadAllDocsSources();
return Object.fromEntries(
pages.map((page) => [
page.href === "/docs" ? "/docs/index.md" : `${page.href}.md`,
page.markdown,
]),
const files: Record<string, string> = {};
const results = await Promise.allSettled(
allDocsPages.map(async (page) => {
const slug =
page.href === "/docs" ? "" : page.href.replace(/^\/docs\/?/, "");
const filePath = slug
? join(
process.cwd(),
"app",
"(main)",
"docs",
...slug.split("/"),
"page.mdx",
)
: join(process.cwd(), "app", "(main)", "docs", "page.mdx");
const raw = await readFile(filePath, "utf-8");
const md = mdxToCleanMarkdown(raw);
const fileName = slug ? `/docs/${slug}.md` : "/docs/index.md";
return { fileName, md };
}),
);
for (const result of results) {
if (result.status === "fulfilled") {
files[result.value.fileName] = result.value.md;
}
}
return files;
}
function addCacheControl(messages: ModelMessage[]): ModelMessage[] {
+44 -14
View File
@@ -1,25 +1,55 @@
import { readFile } from "fs/promises";
import { join } from "path";
import { NextRequest, NextResponse } from "next/server";
import { loadDocsSource } from "@/lib/docs-source";
import { mdxToCleanMarkdown } from "@/lib/mdx-to-markdown";
export async function GET(req: NextRequest) {
const docPath = req.nextUrl.searchParams.get("path");
if (!docPath)
const { searchParams } = new URL(req.url);
const docPath = searchParams.get("path");
if (!docPath) {
return NextResponse.json(
{ error: "Missing ?path= parameter" },
{ status: 400 },
);
const path = docPath.startsWith("/") ? docPath : `/${docPath}`;
if (!/^\/docs(?:\/[a-zA-Z0-9_-]+)*\/?$/.test(path)) {
}
// Sanitize path: only allow docs paths, no traversal
const normalized = docPath
.replace(/^\//, "")
.replace(/\.\./g, "")
.replace(/[^a-zA-Z0-9/-]/g, "");
if (!normalized.startsWith("docs")) {
return NextResponse.json({ error: "Invalid path" }, { status: 400 });
}
const page = await loadDocsSource(path);
if (!page)
// Map URL path to file path
// /docs -> /app/(main)/docs/page.mdx
// /docs/installation -> /app/(main)/docs/installation/page.mdx
const slug = normalized === "docs" ? "" : normalized.replace(/^docs\/?/, "");
const filePath = slug
? join(
process.cwd(),
"app",
"(main)",
"docs",
...slug.split("/"),
"page.mdx",
)
: join(process.cwd(), "app", "(main)", "docs", "page.mdx");
try {
const raw = await readFile(filePath, "utf-8");
const markdown = mdxToCleanMarkdown(raw);
return new NextResponse(markdown, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
"Cache-Control": "public, max-age=3600",
},
});
} catch {
return NextResponse.json({ error: "Page not found" }, { status: 404 });
return new NextResponse(page.markdown, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
"Cache-Control": "public, max-age=3600",
Link: `<${page.canonicalUrl}>; rel="canonical"`,
},
});
}
}
@@ -1,21 +0,0 @@
import { loadDocsSource, isSafePathSegments } from "@/lib/docs-source";
import { applyDocsResponseHeaders } from "@/lib/docs-response-headers";
export async function GET(
_request: Request,
{ params }: { params: Promise<{ slug?: string[] }> },
) {
const { slug = [] } = await params;
const path = `/docs${slug.length ? `/${slug.join("/")}` : ""}`;
const page = isSafePathSegments(slug) ? await loadDocsSource(path) : null;
const headers = new Headers({
"Content-Type": "text/markdown; charset=utf-8",
});
applyDocsResponseHeaders(headers);
if (page) headers.set("Link", `<${page.canonicalUrl}>; rel="canonical"`);
return new Response(
page?.markdown ??
"# Page Not Found\n\nSee [the documentation index](/llms.txt).\n",
{ status: page ? 200 : 404, headers },
);
}
-6
View File
@@ -1,13 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { getSearchIndex } from "@/lib/search-index";
import { createSearchRoute } from "@vercel/geistdocs/routes/search";
import { geistdocsSource } from "@/lib/geistdocs/source";
import { config } from "@/lib/geistdocs/config";
const docsSearch = createSearchRoute({ config, source: geistdocsSource });
export async function GET(req: NextRequest) {
if (req.nextUrl.searchParams.has("query")) return docsSearch(req);
const q = req.nextUrl.searchParams.get("q")?.trim().toLowerCase();
if (!q) {
+6 -35
View File
@@ -1,16 +1,9 @@
@import "@vercel/geistdocs/styles.css";
@theme {
--breakpoint-sm: 40rem;
--breakpoint-md: 48rem;
--breakpoint-lg: 64rem;
--breakpoint-xl: 80rem;
--breakpoint-2xl: 96rem;
}
@import "tailwindcss";
@import "tw-animate-css";
@source "../node_modules/streamdown/dist/index.js";
@custom-variant dark (&:is(.dark-theme *));
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.5rem;
@@ -38,7 +31,7 @@
--chat-bg: oklch(0.95 0 0);
}
.dark-theme {
.dark {
--ds-gray-500: oklch(0.39 0 0);
/* Monochrome dark theme */
--background: oklch(0.0 0 0);
@@ -196,31 +189,9 @@ article table {
background-color: var(--shiki-light-bg) !important;
}
.dark-theme .shiki,
.dark-theme .shiki span {
.dark .shiki,
.dark .shiki span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}
@container (width < 1200px) {
#nd-page { @apply px-6 pt-6; }
#nd-page > [data-mobile-docs-bar],
#nd-page > div:has([data-mobile-toc-trigger]) { display: flex; }
#nd-page [data-mobile-toc-trigger] { width: 44px; height: 44px; }
#nd-page > div:has(> h1) { padding-inline-end: 3.5rem; }
#nd-page > div:has(> h1) + div,
#nd-page > div:has(> h1) + p + div { margin-top: 0; }
}
@container (961px <= width < 1200px) {
#nd-page > [data-mobile-docs-bar] { display: none; }
}
header .pointer-events-none.opacity-0 {
visibility: hidden;
}
#nd-page div:has(> [aria-live="polite"]) > div > .max-sm\:hidden {
display: flex;
}
+6 -31
View File
@@ -3,16 +3,11 @@ import localFont from "next/font/local";
import { GeistPixelSquare } from "geist/font/pixel";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { DocsProvider } from "@/components/geistdocs-provider";
import { Navbar } from "@vercel/geistdocs/navbar";
import { Footer } from "@vercel/geistdocs/footer";
import { config } from "@/lib/geistdocs/config";
import { DocsChat } from "@/components/docs-chat";
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { PAGE_TITLES } from "@/lib/page-titles";
import { cookies } from "next/headers";
import { isPreview, siteUrl, siteDescription } from "@/lib/site";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
@@ -24,8 +19,7 @@ const geistMono = localFont({
});
export const metadata: Metadata = {
metadataBase: new URL(siteUrl),
alternates: { canonical: "/" },
metadataBase: new URL("https://json-render.dev"),
title: {
default: `json-render | ${PAGE_TITLES[""]}`,
template: "%s | json-render",
@@ -70,8 +64,8 @@ export const metadata: Metadata = {
images: ["/og"],
},
robots: {
index: !isPreview,
follow: !isPreview,
index: true,
follow: true,
},
icons: {
icon: "/favicon.ico",
@@ -85,30 +79,15 @@ export default async function RootLayout({
}>) {
const cookieStore = await cookies();
const chatOpen = cookieStore.get("docs-chat-open")?.value === "true";
const chatWidth = Math.min(
700,
Math.max(300, Number(cookieStore.get("docs-chat-width")?.value) || 400),
);
const chatWidth = Number(cookieStore.get("docs-chat-width")?.value) || 400;
return (
<html lang="en" suppressHydrationWarning>
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
name: "json-render",
url: siteUrl,
description: siteDescription,
}),
}}
/>
{chatOpen && (
<style
dangerouslySetInnerHTML={{
__html: `@media(min-width:640px){body{padding-right:min(${chatWidth}px, calc(100vw - 320px))}}`,
__html: `@media(min-width:640px){body{padding-right:${chatWidth}px}}`,
}}
/>
)}
@@ -117,11 +96,7 @@ export default async function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} ${GeistPixelSquare.variable}`}
>
<ThemeProvider>
<DocsProvider>
<Navbar config={config} />
{children}
<Footer />
</DocsProvider>
{children}
<DocsChat defaultOpen={chatOpen} defaultWidth={chatWidth} />
</ThemeProvider>
<Analytics />
-10
View File
@@ -1,10 +0,0 @@
import { loadAllDocsSources } from "@/lib/docs-source";
import { siteDescription, siteUrl } from "@/lib/site";
export async function GET() {
const pages = await loadAllDocsSources();
const body = `# json-render\n\n${siteDescription}\n\n## Documentation\n\n${pages.map((page) => `- [${page.title}](${siteUrl}${page.markdownUrl})`).join("\n")}\n`;
return new Response(body, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
+1 -5
View File
@@ -3,9 +3,5 @@ export default function PlaygroundLayout({
}: {
children: React.ReactNode;
}) {
return (
<main className="h-[calc(100dvh-4rem)] flex flex-col overflow-hidden">
{children}
</main>
);
return <div className="h-dvh flex flex-col overflow-hidden">{children}</div>;
}
-11
View File
@@ -1,11 +0,0 @@
import type { MetadataRoute } from "next";
import { isPreview, siteUrl } from "@/lib/site";
export default function robots(): MetadataRoute.Robots {
return {
rules: isPreview
? { userAgent: "*", disallow: "/" }
: { userAgent: "*", allow: "/" },
sitemap: `${siteUrl}/sitemap.xml`,
};
}
-10
View File
@@ -1,10 +0,0 @@
import { docsPages } from "@/lib/docs-source";
export function GET() {
return new Response(
`# json-render documentation\n\n${docsPages.map((page) => `- [${page.title}](${page.href})`).join("\n")}\n`,
{
headers: { "Content-Type": "text/markdown; charset=utf-8" },
},
);
}
-9
View File
@@ -1,9 +0,0 @@
import type { MetadataRoute } from "next";
import { PAGE_TITLES } from "@/lib/page-titles";
import { siteUrl } from "@/lib/site";
export default function sitemap(): MetadataRoute.Sitemap {
return Object.keys(PAGE_TITLES).map((slug) => ({
url: `${siteUrl}/${slug}`,
}));
}
+6 -63
View File
@@ -141,7 +141,6 @@ export function DocsChat({
);
const messagesScrollRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const launcherRef = useRef<HTMLButtonElement>(null);
const restoredRef = useRef(false);
const isDraggingRef = useRef(false);
@@ -174,51 +173,13 @@ export function DocsChat({
}
}, [open, hasMounted]);
useEffect(() => {
const launcher = launcherRef.current;
if (!hasMounted || open || !launcher) return;
const footer = document.querySelector("footer");
let frame = 0;
const update = () => {
frame = 0;
const rect = document
.querySelector("footer fieldset")
?.getBoundingClientRect();
const overlap =
rect && rect.width > 0 && rect.bottom > 0
? Math.max(0, innerHeight - rect.top)
: 0;
launcher.style.setProperty("--chat-launcher-bottom", `${24 + overlap}px`);
};
const schedule = () => {
if (!frame) frame = requestAnimationFrame(update);
};
const resize = new ResizeObserver(schedule);
resize.observe(document.body);
const mutation = new MutationObserver(schedule);
if (footer) {
resize.observe(footer);
mutation.observe(footer, { childList: true, subtree: true });
}
window.addEventListener("scroll", schedule, { passive: true });
window.addEventListener("resize", schedule);
update();
return () => {
cancelAnimationFrame(frame);
resize.disconnect();
mutation.disconnect();
window.removeEventListener("scroll", schedule);
window.removeEventListener("resize", schedule);
};
}, [hasMounted, open]);
// Push page content on desktop when pane is open.
// Use padding on body so the page scrollbar stays at the viewport edge (behind the sidebar)
// instead of appearing right next to the sidebar's scrollbar.
useEffect(() => {
const body = document.body;
if (isDesktop && open) {
body.style.paddingRight = `min(${desktopWidth}px, calc(100vw - 320px))`;
body.style.paddingRight = `${desktopWidth}px`;
if (!isDraggingRef.current) {
body.style.transition = "padding-right 150ms ease";
}
@@ -312,13 +273,7 @@ export function DocsChat({
return !prev;
});
}
if (
e.key === "Escape" &&
open &&
(isDesktop ||
(e.target instanceof Element &&
e.target.closest("#json-render-chat-mobile")))
) {
if (e.key === "Escape" && open && isDesktop) {
setOpen(false);
}
};
@@ -504,7 +459,6 @@ export function DocsChat({
rows={1}
enterKeyHint="send"
placeholder="Ask a question..."
aria-label="Ask a question"
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
@@ -542,19 +496,12 @@ export function DocsChat({
{/* Ask AI trigger button */}
{!open && (
<button
ref={launcherRef}
data-docs-chat-launcher
onClick={() => setOpen(true)}
className="fixed z-30 bottom-[calc(1rem+env(safe-area-inset-bottom))] left-1/2 -translate-x-1/2 min-[640px]:left-auto min-[640px]:translate-x-0 min-[640px]:right-6 min-[640px]:bottom-[var(--chat-launcher-bottom,24px)] flex h-10 items-center gap-2 px-4 py-2 rounded-lg border border-primary bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-colors text-sm font-medium"
className="fixed z-50 bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:translate-x-0 sm:right-4 flex items-center gap-2 px-4 py-2 rounded-lg border border-primary bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-colors text-sm font-medium"
aria-label="Ask AI"
aria-expanded={open}
aria-controls={
isDesktop ? "json-render-chat-desktop" : "json-render-chat-mobile"
}
aria-keyshortcuts="Meta+I Control+I"
>
Ask AI
<kbd className="hidden min-[640px]:inline-flex items-center gap-0.5 text-xs opacity-60 font-mono">
<kbd className="hidden sm:inline-flex items-center gap-0.5 text-xs opacity-60 font-mono">
<span>&#8984;</span>I
</kbd>
</button>
@@ -562,11 +509,9 @@ export function DocsChat({
{/* Desktop: resizable side pane — always rendered, hidden on mobile via CSS */}
<aside
id="json-render-chat-desktop"
inert={!open || !isDesktop}
className={`hidden sm:flex fixed top-0 right-0 bottom-0 z-40 border-l bg-background transition-transform duration-150 ease-in-out ${open ? "translate-x-0" : "translate-x-full"}`}
style={{ width: `min(${desktopWidth}px, calc(100vw - 320px))` }}
aria-hidden={!open || !isDesktop}
style={{ width: desktopWidth }}
aria-hidden={!open}
>
{/* Resize handle */}
<div
@@ -580,8 +525,6 @@ export function DocsChat({
{hasMounted && !isDesktop && (
<Sheet open={open} onOpenChange={setOpen}>
<SheetContent
id="json-render-chat-mobile"
aria-describedby={undefined}
side="right"
overlayClassName="!bg-background"
className="!inset-0 !w-full !h-full !max-w-none p-0 flex flex-col"
@@ -1,13 +0,0 @@
"use client";
import { GeistdocsProvider } from "@vercel/geistdocs/layout";
import type { ReactNode } from "react";
import { config } from "@/lib/geistdocs/config";
export function DocsProvider({ children }: { children: ReactNode }) {
return (
<GeistdocsProvider config={config} lang="en">
{children}
</GeistdocsProvider>
);
}
+5 -7
View File
@@ -22,6 +22,7 @@ import {
import { CodeBlock } from "./code-block";
import { CopyButton } from "./copy-button";
import { Toaster } from "./ui/sonner";
import { Header } from "./header";
import { InfoIcon } from "lucide-react";
import {
Tooltip,
@@ -345,11 +346,6 @@ export function Playground() {
const mobileInputRef = useRef<HTMLTextAreaElement>(null);
const versionsEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const input = inputRef.current;
if (input?.getClientRects().length) input.focus({ preventScroll: true });
}, []);
// Track the currently generating version ID
const generatingVersionIdRef = useRef<string | null>(null);
@@ -404,8 +400,7 @@ export function Playground() {
// Scroll to bottom when versions change
useEffect(() => {
const container = versionsEndRef.current?.parentElement;
container?.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
versionsEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [versions]);
// Update version when streaming completes
@@ -727,6 +722,7 @@ ${jsx}
maxLength={model === "typesafe-ai/jev" ? 1000 : undefined}
className="w-full bg-background text-base sm:text-sm resize-none outline-none placeholder:text-muted-foreground/50"
rows={2}
autoFocus
/>
<div className="flex justify-between items-end gap-2 mt-2">
<PlaygroundControls
@@ -1071,6 +1067,8 @@ ${jsx}
return (
<div className="h-full flex flex-col">
<Header />
{/* Desktop: 3-pane resizable layout */}
<div className="hidden lg:flex flex-1 min-h-0">
<ResizablePanelGroup className="flex-1">
-1
View File
@@ -6,7 +6,6 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<NextThemesProvider
attribute="class"
value={{ dark: "dark-theme", light: "light-theme" }}
defaultTheme="dark"
enableSystem
disableTransitionOnChange
-34
View File
@@ -1,34 +0,0 @@
{
"title": "API Reference",
"pages": [
"core",
"react",
"next",
"tanstack-start",
"react-pdf",
"react-email",
"shadcn",
"shadcn-svelte",
"react-native",
"image",
"remotion",
"ink",
"vue",
"svelte",
"solid",
"react-three-fiber",
"directives",
"codegen",
"devtools",
"devtools-react",
"devtools-vue",
"devtools-svelte",
"devtools-solid",
"mcp",
"redux",
"zustand",
"jotai",
"xstate",
"yaml"
]
}
-42
View File
@@ -1,42 +0,0 @@
{
"title": "Documentation",
"pages": [
"---Getting Started---",
"index",
"installation",
"quick-start",
"skills",
"migration",
"changelog",
"---Core---",
"specs",
"schemas",
"catalog",
"data-binding",
"computed-values",
"visibility",
"watchers",
"validation",
"directives",
"---Rendering---",
"renderers",
"registry",
"streaming",
"generation-modes",
"---Examples---",
"[Browse All Examples](/examples)",
"---Guides---",
"custom-schema",
"code-export",
"devtools",
"---Experimental---",
"jev",
"---Integrations---",
"ai-sdk",
"a2ui",
"adaptive-cards",
"ag-ui",
"openapi",
"api"
]
}
-1
View File
@@ -2,7 +2,6 @@ import { nextJsConfig } from "@internal/eslint-config/next-js";
/** @type {import("eslint").Linter.Config[]} */
export default [
{ ignores: [".source/**"] },
...nextJsConfig,
{
rules: {
-26
View File
@@ -1,26 +0,0 @@
const negotiationHeaders = [
"Accept",
"User-Agent",
"Signature-Agent",
"Sec-Fetch-Mode",
"Sec-Fetch-Dest",
"RSC",
"Next-Router-Prefetch",
"Next-Router-Segment-Prefetch",
"Purpose",
"Sec-Purpose",
];
export function applyDocsResponseHeaders(headers: Headers) {
const tokens = new Map<string, string>();
for (const token of [
...(headers.get("Vary") ?? "").split(/\s*,\s*/),
...negotiationHeaders,
]) {
if (token) tokens.set(token.toLowerCase(), token);
}
headers.set("Vary", [...tokens.values()].join(", "));
headers.set("Cache-Control", "private, no-store");
headers.set("CDN-Cache-Control", "no-store");
headers.set("Vercel-CDN-Cache-Control", "no-store");
}
-69
View File
@@ -1,69 +0,0 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { parse } from "yaml";
import { allDocsPages } from "./docs-navigation";
import { mdxToCleanMarkdown } from "./mdx-to-markdown";
import { siteUrl } from "./site";
export const docsPages = allDocsPages.filter(
(page) => page.href === "/docs" || page.href.startsWith("/docs/"),
);
const inventory = new Map(docsPages.map((page) => [page.href, page]));
const pending = new Map<string, Promise<DocsSource>>();
export type DocsSource = {
href: string;
title: string;
markdown: string;
markdownUrl: string;
canonicalUrl: string;
};
export function isSafePathSegments(segments: readonly string[]) {
return segments.every(
(part) =>
part.length > 0 &&
part !== "." &&
part !== ".." &&
!part.includes("/") &&
!part.includes("\\"),
);
}
export function loadDocsSource(pathname: string): Promise<DocsSource> | null {
const href = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
if (!inventory.has(href)) return null;
let result = pending.get(href);
if (!result) {
const slug = href === "/docs" ? "index" : href.slice("/docs/".length);
result = readFile(
join(process.cwd(), "content", "docs", `${slug}.mdx`),
"utf8",
).then((raw) => {
const frontmatter = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
if (!frontmatter) throw new Error(`Missing frontmatter for ${href}`);
const metadata = parse(frontmatter[1] ?? "") as {
title?: unknown;
} | null;
if (typeof metadata?.title !== "string")
throw new Error(`Missing title for ${href}`);
const title = metadata.title;
return {
href,
title,
markdown: mdxToCleanMarkdown(
`# ${title}\n${raw.slice(frontmatter[0].length)}`,
),
markdownUrl: `${href}.md`,
canonicalUrl: `${siteUrl}${href}`,
};
});
pending.set(href, result);
result.catch(() => pending.delete(href));
}
return result;
}
export async function loadAllDocsSources() {
return Promise.all(docsPages.map((page) => loadDocsSource(page.href)!));
}
-30
View File
@@ -1,30 +0,0 @@
import { defineConfig } from "@vercel/geistdocs/config";
import { siteUrl } from "@/lib/site";
export const config = defineConfig({
title: "json-render",
siteUrl,
defaultLanguage: "en",
logo: <span className="font-medium">json-render</span>,
navbarActiveProduct: "json-render",
navbarBrand: "labs",
github: {
owner: "vercel-labs",
repo: "json-render",
branch: "main",
editPath: "apps/web/content/docs",
},
content: [
{ id: "docs", label: "Documentation", dir: "content/docs", route: "/docs" },
],
nav: [
{ label: "Docs", href: "/docs" },
{ label: "Playground", href: "/playground" },
{ label: "Examples", href: "/examples" },
],
ai: { enabled: false },
feedback: { enabled: false },
language: { enabled: false },
pageActions: { askAI: false, openInChat: false },
webmcp: { enabled: true },
});
-20
View File
@@ -1,20 +0,0 @@
import {
createSource,
type GeistdocsSourceBundle,
} from "@vercel/geistdocs/source";
import { docs } from "@/.source/server";
import { loadDocsSource } from "@/lib/docs-source";
import { config } from "./config";
const bundle = createSource({ docs, config, baseUrl: "/docs" });
export const geistdocsSource: GeistdocsSourceBundle = {
...bundle,
async getPageMarkdown(page) {
const source = await loadDocsSource(
`/docs${page.slugs.length ? `/${page.slugs.join("/")}` : ""}`,
);
if (!source) throw new Error(`Documentation source not found: ${page.url}`);
return source.markdown;
},
};
+5 -5
View File
@@ -21,10 +21,10 @@ Select Jev, choose Create account settings, and send the request. Edit the name,
Jev exposes Choice, Boolean, and Score outputs. It does not produce free-form JSON or prose. We express new UI construction as two batches of finite choices:
1. Offer the root and independent component membership questions in one evaluation. Exclusive resource variants share a question; reusable recipes get bounded counts. Candidate values include state/action bindings owned by the app.
2. Assemble and validate the selected content, then stream a preview immediately. This preview uses catalog order and the root's default slot. Root selection takes precedence over speculative membership for the same recipe/resource.
3. Ask final parent slots and sibling positions in a second evaluation against the actual selected set. Validate the combined tree, including depth and cycles, before streaming it. Equal positions retain catalog order. A single root or one child in a single slot needs no second call. No separate finish call is needed.
2. Assemble and validate the selected content. The core API emits a provisional snapshot in catalog order and the root's default slot; the playground streams its decision metadata but waits to display new content. Root selection takes precedence over speculative membership for the same recipe/resource.
3. Ask final parent slots and sibling positions in a second evaluation against the actual selected set. Validate the combined tree, including depth and cycles, before revealing the finished spec in one JSON edit message. Equal positions retain catalog order. A single root or one child in a single slot needs no second call. No separate finish call is needed.
4. On follow-ups, use the selected spec with the sequential edit protocol: add, replace, remove, or move/reorder. Replacements and moves select a target, then choose a valid recipe or destination in a second evaluation. Preserve unchanged elements and earlier versions.
5. Each trace represents one evaluation. Batched traces use `select`/`layout` with the independent decisions in `answers`; timing and usage are counted once per call. Provider errors or invalid combined layouts preserve the last valid preview and report failure.
5. Each trace represents one evaluation. Batched traces use `select`/`layout` with the independent decisions in `answers`; timing and usage are counted once per call. Provider errors or invalid combined layouts report failure. New trees remain hidden until successful completion; follow-up edits retain their last valid preview.
There are **no complete UI templates** and no generative-model calls. The example prompt buttons only populate the request text. Jev chooses which elements to include, their order, grouping, and which offered action bindings to use. The registry owns appearance and behavior.
@@ -48,7 +48,7 @@ The composer validates tree structure and candidate values; it does not guarante
Name required sections explicitly. For example, request an orders table at the top, revenue/orders/customer metrics in a row, then a weekly revenue chart. The shorter request "a dashboard with the table at the top" can select only a table. Follow-up requests can move an existing table without reconstructing its data.
The code bounds new batches to 14 elements, each request to 14 evaluation calls, nesting depth four, ten seconds per provider request, and 55 seconds overall. The selected seed may contain up to 100 elements. A limit, cancellation, or error retains the current preview and labels it partial. The shared endpoint uses the web app's request rate limiters. Both models edit the selected version; Clear starts fresh. The stream tab exposes construction decisions alongside spec patches. Provider calls and spec assembly never execute the selected UI actions.
The code bounds new batches to 14 elements, each request to 14 evaluation calls, nesting depth four, ten seconds per provider request, and 55 seconds overall. The selected seed may contain up to 100 elements. A limit, cancellation, or error retains the current preview for edits and labels the result partial; new trees remain hidden. The shared endpoint uses the web app's request rate limiters. Both models edit the selected version; Clear starts fresh. The stream tab exposes construction decisions alongside spec patches. Provider calls and spec assembly never execute the selected UI actions.
Try `Design a user profile card`, then `Remove the bio` or `Make the avatar smaller`. For settings, try `Remove the email notifications switch`, `Change the heading to "Account settings"`, or `Move the email field above the name field`. The server shares existing display labels and matching candidate descriptions to identify edit targets, without sharing raw state or entered field values. Existing specs must use the supported expression subset and form a valid tree. Edits retain state from the selected spec, as in the default model flow; interactive preview state is not saved into version history.
@@ -64,7 +64,7 @@ The server uses Gateway's experimental v4 evaluation transport with model `types
- `packages/core/src/experimental-evaluator.ts`: public Gateway evaluator adapter.
- `compose.ts`: public API consumer with playground instructions and cost display.
- `../../app/api/generate/route.ts`: shared rate-limited endpoint, dispatching the selected model.
- `response.ts`: adapts composition snapshots into the playground's JSONL spec patches and decision metadata.
- `response.ts`: reveals finished new trees atomically using the JSON edit protocol, streams follow-up spec patches, and preserves decision metadata.
- `../../components/playground.tsx`: shared model toggle, experimental info tooltip, prompt, version history, live preview, and inspectors.
- `compose.test.ts`: structure, action boundaries, unknown usage, cancellation, and limits.
+65 -10
View File
@@ -1,6 +1,6 @@
// @vitest-environment node
import { afterEach, describe, expect, it, vi } from "vitest";
import { type JsonPatch, type Spec } from "@json-render/core";
import { deepMergeSpec, type Spec } from "@json-render/core";
import { applySpecPatch } from "../spec-patch";
import { createCompositionResponse } from "./response";
import { composeUI } from "./compose";
@@ -58,7 +58,7 @@ describe("playground composition response", () => {
expect(initialSpec.elements.card!.props.title).toBe("Before");
});
it("adapts snapshots to the existing patch stream, including the final decision", async () => {
it("reveals a new tree atomically after layout, preserving every decision", async () => {
vi.stubEnv("JEV_AI_GATEWAY_API_KEY", "test");
const spec: Spec = {
root: "card",
@@ -76,12 +76,31 @@ describe("playground composition response", () => {
elapsedMs: 10,
inputTokens: null,
};
const provisional: Spec = {
...spec,
elements: {
card: { ...spec.elements.card!, props: { title: "Provisional" } },
},
};
vi.mocked(composeUI).mockImplementation(async function* () {
yield { type: "step", spec, step };
yield {
type: "step",
spec: provisional,
step: { ...step, choice: "select" },
};
yield {
type: "step",
spec,
step: { ...step, index: 1, choice: "layout" },
};
yield {
type: "complete",
spec,
steps: [step, { ...step, index: 1, choice: "finish" }],
steps: [
{ ...step, choice: "select" },
{ ...step, index: 1, choice: "layout" },
{ ...step, index: 2, choice: "finish" },
],
stopReason: "finish",
elapsedMs: 20,
inputTokens: null,
@@ -96,23 +115,59 @@ describe("playground composition response", () => {
.trim()
.split("\n")
.map((line) => JSON.parse(line));
let actual: Spec = { root: "", elements: {} };
for (const line of lines)
if (line.op) actual = applySpecPatch(actual, line as JsonPatch);
expect(actual).toEqual(spec);
expect(lines.slice(0, 2).map((line) => line.__meta)).toEqual([
"decision",
"decision",
]);
const updates = lines.filter((line) => line.op || line.__json_edit);
expect(updates).toEqual([{ __json_edit: true, ...spec }]);
const { __json_edit, ...snapshot } = updates[0];
expect(deepMergeSpec({ root: "", elements: {} }, snapshot)).toEqual(spec);
expect(
lines
.filter((line) => line.__meta === "decision")
.map((line) => line.choice),
).toEqual(["card", "finish"]);
).toEqual(["select", "layout", "finish"]);
expect(lines.at(-1)).toMatchObject({
__meta: "composition",
stopReason: "finish",
calls: 2,
calls: 3,
inputTokens: null,
});
});
it.each(["limit", "unavailable"] as const)(
"does not reveal provisional content when creation ends with %s",
async (stopReason) => {
vi.stubEnv("JEV_AI_GATEWAY_API_KEY", "test");
vi.mocked(composeUI).mockImplementation(async function* () {
yield {
type: "complete",
spec: {
root: "card",
elements: { card: { type: "Card", props: {} } },
},
steps: [],
stopReason,
elapsedMs: 1,
inputTokens: null,
estimatedCostUsd: null,
};
});
const response = createCompositionResponse(
new Request("https://example.com/api/generate"),
"Create a card",
);
const lines = (await response.text())
.trim()
.split("\n")
.map((line) => JSON.parse(line));
expect(lines).toEqual([
expect.objectContaining({ __meta: "composition", stopReason }),
]);
},
);
it("retains unavailable outcomes and sends failures in the shared protocol", async () => {
vi.stubEnv("JEV_AI_GATEWAY_API_KEY", "test");
vi.mocked(composeUI).mockImplementation(async function* () {
+11 -2
View File
@@ -83,11 +83,20 @@ export function createCompositionResponse(
)) {
if (event.type === "error") throw new Error(event.message);
if (event.type === "step") {
sendSpec(event.spec);
// New trees have a provisional catalog order until layout finishes.
// Stream its decision metadata without displaying a moving UI.
if (initialSpec) sendSpec(event.spec);
send({ __meta: "decision", ...event.step });
decisions++;
} else {
if (event.spec) sendSpec(event.spec);
if (event.spec) {
if (initialSpec) sendSpec(event.spec);
else if (event.stopReason === "finish") {
// The existing JSON edit protocol applies a full new tree in
// one render, including its state and final child order.
send({ __json_edit: true, ...event.spec });
}
}
for (const step of event.steps.slice(decisions))
send({ __meta: "decision", ...step });
send({
-10
View File
@@ -1,6 +1,5 @@
import type { Metadata } from "next";
import { PAGE_TITLES } from "./page-titles";
import { isPreview, siteUrl } from "./site";
const DESCRIPTION =
"The Generative UI framework. Generate dashboards, widgets, and apps from prompts — safely constrained to components you define.";
@@ -15,16 +14,7 @@ export function pageMetadata(slug: string): Metadata {
return {
title: displayTitle,
description: DESCRIPTION,
robots: { index: !isPreview, follow: !isPreview },
alternates: {
canonical: `/${slug}`,
...(slug === "docs" || slug.startsWith("docs/")
? { types: { "text/markdown": `/${slug}.md` } }
: {}),
},
openGraph: {
url: `${siteUrl}/${slug}`,
type: "website",
locale: "en_US",
siteName: "json-render",
-26
View File
@@ -1,26 +0,0 @@
type MarkdownNode = {
type: string;
depth?: number;
value?: string;
children?: MarkdownNode[];
data?: { hProperties?: Record<string, unknown> };
};
function text(node: MarkdownNode): string {
return node.children ? node.children.map(text).join("") : (node.value ?? "");
}
export function remarkLegacyHeadings() {
return function walk(node: MarkdownNode) {
if (node.type === "heading" && (node.depth === 2 || node.depth === 3)) {
node.data ??= {};
node.data.hProperties ??= {};
node.data.hProperties.id = text(node)
.toLowerCase()
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-")
.trim();
}
node.children?.forEach(walk);
};
}
+15 -3
View File
@@ -1,5 +1,7 @@
import { readFile } from "fs/promises";
import { join } from "path";
import { docsNavigation } from "./docs-navigation";
import { loadDocsSource } from "./docs-source";
import { mdxToCleanMarkdown } from "./mdx-to-markdown";
export type IndexEntry = {
title: string;
@@ -31,6 +33,15 @@ function stripMarkdown(md: string): string {
);
}
function mdxFileForSlug(slug: string): string {
const docsRoot = join(process.cwd(), "app", "(main)", "docs");
if (slug === "/docs") {
return join(docsRoot, "page.mdx");
}
const rest = slug.replace(/^\/docs\/?/, "");
return join(docsRoot, ...rest.split("/"), "page.mdx");
}
export async function getSearchIndex(): Promise<IndexEntry[]> {
if (cached) return cached;
@@ -40,8 +51,9 @@ export async function getSearchIndex(): Promise<IndexEntry[]> {
for (const item of section.items) {
if (item.external) continue;
try {
const source = await loadDocsSource(item.href);
const content = stripMarkdown(source?.markdown ?? "");
const raw = await readFile(mdxFileForSlug(item.href), "utf-8");
const md = mdxToCleanMarkdown(raw);
const content = stripMarkdown(md);
entries.push({
title: item.title,
href: item.href,
-6
View File
@@ -1,6 +0,0 @@
export const siteUrl = "https://json-render.dev";
export const siteDescription =
"The Generative UI framework. Generate dashboards, widgets, and apps from prompts — safely constrained to components you define.";
export const isPreview = Boolean(
process.env.VERCEL_ENV && process.env.VERCEL_ENV !== "production",
);
+2 -24
View File
@@ -1,31 +1,9 @@
import process from "node:process";
import { createGeistdocs } from "@vercel/geistdocs/next";
import createMDX from "@next/mdx";
/** @type {import('next').NextConfig} */
const nextConfig = {
serverExternalPackages: ["bash-tool", "just-bash", "@mongodb-js/zstd"],
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
skipProxyUrlNormalize: true,
outputFileTracingIncludes: { "/*": ["./content/docs/**/*.mdx"] },
async headers() {
return process.env.VERCEL_ENV && process.env.VERCEL_ENV !== "production"
? [
{
source: "/:path*",
headers: [{ key: "X-Robots-Tag", value: "noindex, nofollow" }],
},
]
: [];
},
async rewrites() {
return {
beforeFiles: [
{ source: "/docs.md", destination: "/api/docs-md" },
{ source: "/docs/index.md", destination: "/api/docs-md" },
{ source: "/docs/:path*.md", destination: "/api/docs-md/:path*" },
],
};
},
async redirects() {
return [
{
@@ -42,7 +20,7 @@ const nextConfig = {
},
};
const withMDX = createGeistdocs();
const withMDX = createMDX({});
/** @type {import('next').NextConfig} */
const config = withMDX(nextConfig);
+5 -8
View File
@@ -7,13 +7,10 @@
"scripts": {
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
"dev": "portless json-render next dev --turbopack",
"postinstall": "fumadocs-mdx",
"prebuild": "fumadocs-mdx",
"build": "next build",
"start": "next start",
"test:routes": "node scripts/test-routes.mjs",
"lint": "eslint --max-warnings 0",
"check-types": "fumadocs-mdx && next typegen && tsc --noEmit"
"check-types": "next typegen && tsc --noEmit"
},
"dependencies": {
"@ai-sdk/gateway": "^3.0.13",
@@ -24,14 +21,16 @@
"@json-render/devtools-react": "workspace:*",
"@json-render/react": "workspace:*",
"@json-render/yaml": "workspace:*",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^16.1.6",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@upstash/ratelimit": "^2.0.8",
"@upstash/redis": "^1.36.1",
"@vercel/analytics": "^1.6.1",
"@vercel/geistdocs": "2.4.1",
"@vercel/speed-insights": "^1.3.1",
"@visual-json/react": "0.1.1",
"ai": "^6.0.33",
@@ -40,11 +39,9 @@
"clsx": "^2.1.1",
"diff": "^8.0.3",
"embla-carousel-react": "^8.6.0",
"fumadocs-core": "16.2.2",
"fumadocs-mdx": "14.0.4",
"geist": "1.7.0",
"lucide-react": "^0.562.0",
"next": "16.3.4",
"next": "16.1.1",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.2.3",
-62
View File
@@ -1,62 +0,0 @@
import { createProxy } from "@vercel/geistdocs/proxy";
import { createI18nMiddleware } from "fumadocs-core/i18n/middleware";
import {
NextResponse,
type NextRequest,
type NextFetchEvent,
} from "next/server";
import { config as geistdocsConfig } from "@/lib/geistdocs/config";
import { applyDocsResponseHeaders } from "@/lib/docs-response-headers";
const localeProxy = createI18nMiddleware({
defaultLanguage: "en",
languages: ["en"],
hideLocale: "default-locale",
});
const geistdocsProxy = createProxy({
config: geistdocsConfig,
markdownRoutes: [{ from: "/docs/*path", to: "/api/docs-md/*path" }],
before: async ({ request, context }) => {
if (
request.headers.get("rsc") === "1" ||
request.headers.has("next-router-prefetch") ||
request.headers.has("next-router-segment-prefetch") ||
/\bprefetch\b/i.test(request.headers.get("purpose") ?? "") ||
/\bprefetch\b/i.test(request.headers.get("sec-purpose") ?? "")
) {
return (await localeProxy(request, context)) ?? NextResponse.next();
}
},
});
export default async function proxy(
request: NextRequest,
event: NextFetchEvent,
) {
const { pathname } = request.nextUrl;
try {
decodeURIComponent(pathname);
} catch (error) {
if (!(error instanceof URIError)) throw error;
return new NextResponse("Not Found", { status: 404 });
}
let response: Response;
if (pathname === "/en/docs" || pathname.startsWith("/en/docs/")) {
const destination = request.nextUrl.clone();
destination.pathname = pathname.slice(3);
response = NextResponse.redirect(destination, 308);
} else if (
(pathname === "/docs" || pathname.startsWith("/docs/")) &&
!pathname.includes(".")
) {
response = await geistdocsProxy(request, event);
} else {
return NextResponse.next();
}
applyDocsResponseHeaders(response.headers);
return response;
}
export const config = {
matcher: ["/docs/:path*", "/en/docs/:path*", "/(.*%.*)"],
};
-106
View File
@@ -1,106 +0,0 @@
import process from "node:process";
import { spawn } from "node:child_process";
import { createServer } from "node:net";
import { setTimeout as sleep } from "node:timers/promises";
import { fileURLToPath } from "node:url";
const cwd = fileURLToPath(new URL("../", import.meta.url));
const socket = createServer();
await new Promise((resolve, reject) => {
socket.once("error", reject);
socket.listen(0, "127.0.0.1", resolve);
});
const port = socket.address().port;
await new Promise((resolve, reject) =>
socket.close((error) => (error ? reject(error) : resolve())),
);
function launch(args, env) {
const child = spawn(process.execPath, args, { cwd, stdio: "inherit", env });
const result = { child, done: false, exited: undefined };
result.exited = new Promise((resolve) => {
child.once("error", (error) => {
console.error(error.message);
result.done = true;
resolve(1);
});
child.once("exit", (code) => {
result.done = true;
resolve(code ?? 1);
});
});
return result;
}
const server = launch(
[
fileURLToPath(import.meta.resolve("next/dist/bin/next")),
"start",
"--hostname",
"127.0.0.1",
],
{ ...process.env, NODE_ENV: "production", PORT: String(port) },
);
const url = `http://127.0.0.1:${port}`;
let tests;
let shutdown;
let stopping = false;
const stop = () => {
stopping = true;
shutdown ??= Promise.all(
[tests, server].map(async (owned) => {
if (!owned || owned.done) return;
owned.child.kill("SIGTERM");
const deadline = setTimeout(() => owned.child.kill("SIGKILL"), 5000);
try {
await owned.exited;
} finally {
clearTimeout(deadline);
}
}),
);
return shutdown;
};
for (const [signal, code] of [
["SIGINT", 130],
["SIGTERM", 143],
]) {
process.once(signal, () => {
void stop().finally(() => process.exit(code));
});
}
try {
const deadline = Date.now() + 60000;
let ready = false;
while (Date.now() < deadline && !stopping) {
if (server.done)
throw new Error(`Docs server exited with ${await server.exited}`);
try {
const response = await fetch(`${url}/robots.txt`, {
signal: AbortSignal.timeout(1000),
});
await response.body?.cancel();
if (response.ok) {
ready = true;
break;
}
} catch {
ready = false;
}
await sleep(100);
}
if (!ready && !stopping)
throw new Error(
"Docs server did not become ready. Run the docs build first.",
);
if (!stopping) {
tests = launch(["--test", "tests/docs-routes.test.mjs"], {
...process.env,
DOCS_TEST_URL: url,
});
process.exitCode = await tests.exited;
}
} finally {
await stop();
}
-42
View File
@@ -1,42 +0,0 @@
import {
defineGeistdocsSourceConfig,
geistdocsFrontmatterSchema,
geistdocsMetaSchema,
} from "@vercel/geistdocs/source-config";
import { defineConfig, defineDocs } from "fumadocs-mdx/config";
import { remarkLegacyHeadings } from "./lib/remark-legacy-headings";
export const docs = defineDocs({
dir: "content/docs",
docs: {
schema: geistdocsFrontmatterSchema,
postprocess: { includeProcessedMarkdown: true },
},
meta: { schema: geistdocsMetaSchema },
});
const config = defineGeistdocsSourceConfig({
mdxOptions: {
remarkStructureOptions: {
types: [
"heading",
"paragraph",
"blockquote",
"tableCell",
"mdxJsxFlowElement",
"mdxJsxTextElement",
"text",
"inlineCode",
"code",
],
},
},
});
export default defineConfig({
...config,
mdxOptions: {
...config.mdxOptions,
remarkPlugins: [remarkLegacyHeadings],
},
});

Some files were not shown because too many files have changed in this diff Show More