Compare commits

...
Author SHA1 Message Date
Chris Tate cf3e229d99 add solid example 2026-03-11 22:44:16 -05:00
Chris Tate c3c0a36f20 better visual (#206) 2026-03-11 12:16:51 -05:00
Patrick HallandPatrick 9bb82604f0 solid: add renderer implementation (#201)
* solid: add renderer implementation

This adds a Solid renderer with the same core capabilities as the
existing framework packages, including typed registry helpers,
providers, streaming hooks, docs, and example integration.

* fix: solid reactivity bugs in validation + state

---------

Co-authored-by: Patrick <phall@Patricks-MacBook-Pro.local>
2026-03-11 09:12:07 -05:00
github-actions[bot] caa90b8b84 chore: version packages (#204)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-11 02:19:24 -05:00
Chris Tate 54a1ecf817 Add v0.12.1 release changeset with mode renames and MCP fixes (#203)
This PR adds the changeset file to prepare for the v0.12.1 release, documenting important breaking changes and bug fixes.

## Changes

**@json-render/core**
- Renamed generation modes from `"generate"`/`"chat"` to `"standalone"`/`"inline"` (old names deprecated with warnings)

**@json-render/mcp**  
- Fixed React duplicate module error causing `useRef` to return null by adding Vite `resolve.dedupe` configuration
- Added `./build-app-html` export entry point

**Other improvements**
- Updated homepage URLs across all packages to https://json-render.dev
- Reorganized skills directory structure
- Added skills documentation page to web app

Fixes #198
2026-03-11 02:09:10 -05:00
Chris Tate dc9e9d17f3 rename modes (#197)
* rename modes

* improvements

* fixes

* fix homepage
2026-03-10 15:07:47 -05:00
Chris Tate 1977fb3a11 fix(mcp): resolve React duplicate module error with dedupe config (#192)
Fixes a React runtime error where `useRef` was null due to multiple React instances being bundled.

## Changes Made

- **Vite configuration**: Added `resolve.dedupe` for `react` and `react-dom` in `examples/mcp/vite.config.ts` to ensure only one instance of React is used during bundling
- **React version constraints**: Changed from exact versions (`19.2.3`) to flexible ranges (`^19.0.0`) in `examples/mcp/package.json` to improve compatibility
- **Build entry point**: Added new `build-app-html-entry.ts` file and updated `tsup.config.ts` to include it as a build target
- **Package exports**: Added `./build-app-html` export path in `packages/mcp/package.json`
- **Minor fixes**: Updated Next.js type reference path in stripe-app example

The root cause was React being duplicated in the bundle, causing hooks like `useRef` to fail when components tried to access React's internal state from the wrong instance. The `dedupe` configuration ensures Vite resolves all React imports to the same module instance.

Fixes #190
2026-03-09 21:21:07 -05:00
Chris Tate 4606c01085 skills page (#188) 2026-03-06 20:39:07 -06:00
Chris Tate c1a700d719 cleaner skills (#187) 2026-03-06 20:22:56 -06:00
github-actions[bot] 6f15faaae0 chore: version packages (#186)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-06 20:02:39 -06:00
Chris Tate 63c339b4bb prepare 0.12.0 (#185) 2026-03-06 19:58:08 -06:00
Chris Tate 1cc87310c9 mcp (#184)
* mcp

* fix lint
2026-03-06 19:41:27 -06:00
Chris Tate 5b929dffa6 fix(svelte): correct JSDoc comment and add missing zod peer dependency (#183)
Fixes #182
2026-03-06 19:16:17 -06:00
Simon H 512f7fe5c5 feat: Svelte-renderer (#168)
* create svelte-renderer

* helpers

* reactive API

* turn into provider components

* fixes

* examples

* docs

* correct component props for catalog, harmonize defineRegistry API with other APis

* shuffle things around

* createRenderer

* on

* boundary/warning

* functions

* fix

* fix types
2026-03-06 17:31:06 -06:00
365 changed files with 23014 additions and 751 deletions
+4 -1
View File
@@ -17,7 +17,10 @@
"@json-render/jotai",
"@json-render/vue",
"@json-render/xstate",
"@json-render/image"
"@json-render/image",
"@json-render/mcp",
"@json-render/svelte",
"@json-render/solid"
]
],
"linked": [],
+8
View File
@@ -0,0 +1,8 @@
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
}
}
}
+2
View File
@@ -4,6 +4,7 @@
node_modules
.pnp
.pnp.js
.pnpm-store/
# Local env files
.env*
@@ -27,6 +28,7 @@ out/
build
dist
*.tsbuildinfo
.svelte-kit/
# Debug
+9
View File
@@ -0,0 +1,9 @@
{
"servers": {
"json-render": {
"type": "stdio",
"command": "npx",
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
}
}
}
+1 -1
View File
@@ -98,7 +98,7 @@ When asked to prepare a release (e.g. "prepare v0.12.0"):
- 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/json-render-<name>/SKILL.md`
- A skill at `skills/<name>/SKILL.md`
- A `packages/<name>/README.md`
6. **Run `pnpm type-check`** after all changes to verify nothing is broken
+138 -40
View File
@@ -19,6 +19,10 @@ npm install @json-render/core @json-render/react-pdf
npm install @json-render/core @json-render/react-email @react-email/components @react-email/render
# or for Vue
npm install @json-render/core @json-render/vue
# or for Svelte
npm install @json-render/core @json-render/svelte
# or for SolidJS
npm install @json-render/core @json-render/solid
```
## Why json-render?
@@ -28,7 +32,7 @@ json-render is a **Generative UI** framework: AI generates interfaces from natur
- **Guardrailed** - AI can only use components in your catalog
- **Predictable** - JSON output matches your schema, every time
- **Fast** - Stream and render progressively as the model responds
- **Cross-Platform** - React, Vue (web), React Native (mobile) from the same catalog
- **Cross-Platform** - React, Vue, Svelte, Solid (web), React Native (mobile) from the same catalog
- **Batteries Included** - 36 pre-built shadcn/ui components ready to use
## Quick Start
@@ -89,9 +93,7 @@ const { registry } = defineRegistry(catalog, {
</div>
),
Button: ({ props, emit }) => (
<button onClick={() => emit("press")}>
{props.label}
</button>
<button onClick={() => emit("press")}>{props.label}</button>
),
},
});
@@ -111,21 +113,25 @@ function Dashboard({ spec }) {
## Packages
| Package | Description |
|---------|-------------|
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
| `@json-render/react` | React renderer, contexts, hooks |
| `@json-render/vue` | Vue 3 renderer, composables, providers |
| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
| `@json-render/react-native` | React Native renderer with standard mobile components |
| `@json-render/remotion` | Remotion video renderer, timeline schema |
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
| `@json-render/react-email` | React Email renderer for HTML/plain-text emails from specs |
| `@json-render/image` | Image renderer for SVG/PNG output (OG images, social cards) via Satori |
| `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` |
| `@json-render/zustand` | Zustand adapter for `StateStore` |
| `@json-render/jotai` | Jotai adapter for `StateStore` |
| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` |
| Package | Description |
| --------------------------- | ---------------------------------------------------------------------- |
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
| `@json-render/react` | React renderer, contexts, hooks |
| `@json-render/vue` | Vue 3 renderer, composables, providers |
| `@json-render/svelte` | Svelte 5 renderer with runes-based reactivity |
| `@json-render/solid` | SolidJS renderer with fine-grained reactive contexts |
| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
| `@json-render/react-native` | React Native renderer with standard mobile components |
| `@json-render/remotion` | Remotion video renderer, timeline schema |
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
| `@json-render/react-email` | React Email renderer for HTML/plain-text emails from specs |
| `@json-render/image` | Image renderer for SVG/PNG output (OG images, social cards) via Satori |
| `@json-render/codegen` | Utilities for generating code from json-render UI trees |
| `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` |
| `@json-render/zustand` | Zustand adapter for `StateStore` |
| `@json-render/jotai` | Jotai adapter for `StateStore` |
| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` |
| `@json-render/mcp` | MCP Apps integration for Claude, ChatGPT, Cursor, VS Code |
## Renderers
@@ -154,7 +160,7 @@ const spec = {
// defineRegistry creates a type-safe component registry
const { registry } = defineRegistry(catalog, { components });
<Renderer spec={spec} registry={registry} />
<Renderer spec={spec} registry={registry} />;
```
### Vue (UI)
@@ -177,6 +183,43 @@ const { registry } = defineRegistry(catalog, {
// <Renderer :spec="spec" :registry="registry" />
```
### Svelte (UI)
```typescript
import { defineRegistry, Renderer } from "@json-render/svelte";
import { schema } from "@json-render/svelte/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => /* Svelte 5 snippet */,
Button: ({ props, emit }) => /* Svelte 5 snippet */,
},
});
// In your Svelte component:
// <Renderer spec={spec} registry={registry} />
```
### Solid (UI)
```tsx
import { defineRegistry, Renderer } from "@json-render/solid";
import { schema } from "@json-render/solid/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: (renderProps) => <div>{renderProps.children}</div>,
Button: (renderProps) => (
<button onClick={() => renderProps.emit("press")}>
{renderProps.element.props.label as string}
</button>
),
},
});
<Renderer spec={spec} registry={registry} />;
```
### shadcn/ui (Web)
```tsx
@@ -207,7 +250,7 @@ const { registry } = defineRegistry(catalog, {
},
});
<Renderer spec={spec} registry={registry} />
<Renderer spec={spec} registry={registry} />;
```
### React Native (Mobile)
@@ -228,23 +271,40 @@ const catalog = defineCatalog(schema, {
});
const { registry } = defineRegistry(catalog, { components: {} });
<Renderer spec={spec} registry={registry} />
<Renderer spec={spec} registry={registry} />;
```
### Remotion (Video)
```tsx
import { Player } from "@remotion/player";
import { Renderer, schema, standardComponentDefinitions } from "@json-render/remotion";
import {
Renderer,
schema,
standardComponentDefinitions,
} from "@json-render/remotion";
// Timeline spec format
const spec = {
composition: { id: "video", fps: 30, width: 1920, height: 1080, durationInFrames: 300 },
composition: {
id: "video",
fps: 30,
width: 1920,
height: 1080,
durationInFrames: 300,
},
tracks: [{ id: "main", name: "Main", type: "video", enabled: true }],
clips: [
{ id: "clip-1", trackId: "main", component: "TitleCard", props: { title: "Hello" }, from: 0, durationInFrames: 90 }
{
id: "clip-1",
trackId: "main",
component: "TitleCard",
props: { title: "Hello" },
from: 0,
durationInFrames: 90,
},
],
audio: { tracks: [] }
audio: { tracks: [] },
};
<Player
@@ -254,7 +314,7 @@ const spec = {
fps={spec.composition.fps}
compositionWidth={spec.composition.width}
compositionHeight={spec.composition.height}
/>
/>;
```
### React PDF (Documents)
@@ -265,7 +325,11 @@ import { renderToBuffer } from "@json-render/react-pdf";
const spec = {
root: "doc",
elements: {
doc: { type: "Document", props: { title: "Invoice" }, children: ["page-1"] },
doc: {
type: "Document",
props: { title: "Invoice" },
children: ["page-1"],
},
"page-1": {
type: "Page",
props: { size: "A4" },
@@ -279,8 +343,14 @@ const spec = {
"table-1": {
type: "Table",
props: {
columns: [{ header: "Item", width: "60%" }, { header: "Price", width: "40%", align: "right" }],
rows: [["Widget A", "$10.00"], ["Widget B", "$25.00"]],
columns: [
{ header: "Item", width: "60%" },
{ header: "Price", width: "40%", align: "right" },
],
rows: [
["Widget A", "$10.00"],
["Widget B", "$25.00"],
],
},
children: [],
},
@@ -305,7 +375,11 @@ const catalog = defineCatalog(schema, {
const spec = {
root: "html-1",
elements: {
"html-1": { type: "Html", props: { lang: "en", dir: "ltr" }, children: ["head-1", "body-1"] },
"html-1": {
type: "Html",
props: { lang: "en", dir: "ltr" },
children: ["head-1", "body-1"],
},
"head-1": { type: "Head", props: {}, children: [] },
"body-1": {
type: "Body",
@@ -314,11 +388,17 @@ const spec = {
},
"container-1": {
type: "Container",
props: { style: { maxWidth: "600px", margin: "0 auto", padding: "20px" } },
props: {
style: { maxWidth: "600px", margin: "0 auto", padding: "20px" },
},
children: ["heading-1", "text-1"],
},
"heading-1": { type: "Heading", props: { text: "Welcome" }, children: [] },
"text-1": { type: "Text", props: { text: "Thanks for signing up." }, children: [] },
"text-1": {
type: "Text",
props: { text: "Thanks for signing up." },
children: [],
},
},
};
@@ -403,8 +483,16 @@ Any prop value can be data-driven using expressions:
{
"type": "Icon",
"props": {
"name": { "$cond": { "$state": "/activeTab", "eq": "home" }, "$then": "home", "$else": "home-outline" },
"color": { "$cond": { "$state": "/activeTab", "eq": "home" }, "$then": "#007AFF", "$else": "#8E8E93" }
"name": {
"$cond": { "$state": "/activeTab", "eq": "home" },
"$then": "home",
"$else": "home-outline"
},
"color": {
"$cond": { "$state": "/activeTab", "eq": "home" },
"$then": "#007AFF",
"$else": "#8E8E93"
}
}
}
```
@@ -423,7 +511,10 @@ Components can trigger actions, including the built-in `setState` action:
```json
{
"type": "Pressable",
"props": { "action": "setState", "actionParams": { "statePath": "/activeTab", "value": "home" } },
"props": {
"action": "setState",
"actionParams": { "statePath": "/activeTab", "value": "home" }
},
"children": ["home-icon"]
}
```
@@ -437,9 +528,15 @@ React to state changes by triggering actions:
```json
{
"type": "Select",
"props": { "value": { "$bindState": "/form/country" }, "options": ["US", "Canada", "UK"] },
"props": {
"value": { "$bindState": "/form/country" },
"options": ["US", "Canada", "UK"]
},
"watch": {
"/form/country": { "action": "loadCities", "params": { "country": { "$state": "/form/country" } } }
"/form/country": {
"action": "loadCities",
"params": { "country": { "$state": "/form/country" } }
}
}
}
```
@@ -462,8 +559,9 @@ pnpm dev
- http://react-email-demo.json-render.localhost:1355 - React Email Example
- http://remotion-demo.json-render.localhost:1355 - Remotion Video Example
- Chat Example: run `pnpm dev` in `examples/chat`
- Svelte Example: run `pnpm dev` in `examples/svelte` or `examples/svelte-chat`
- Vue Example: run `pnpm dev` in `examples/vue`
- Vite Renderers (React + Vue): run `pnpm dev` in `examples/vite-renderers`
- Vite Renderers (React + Vue + Svelte + Solid): run `pnpm dev` in `examples/vite-renderers`
- React Native example: run `npx expo start` in `examples/react-native`
## How It Works
@@ -473,7 +571,7 @@ flowchart LR
A[User Prompt] --> B[AI + Catalog]
B --> C[JSON Spec]
C --> D[Renderer]
B -.- E([guardrailed])
C -.- F([predictable])
D -.- G([streamed])
+18
View File
@@ -1,5 +1,23 @@
# web
## 0.1.6
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/codegen@0.12.1
- @json-render/react@0.12.1
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/codegen@0.12.0
- @json-render/react@0.12.0
## 0.1.4
### Patch Changes
+12 -12
View File
@@ -3,7 +3,7 @@ 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: **Generate** (standalone UI) and **Chat** (UI embedded in conversation). See [Generation Modes](/docs/generation-modes) for a detailed comparison.
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.
## Installation
@@ -11,9 +11,9 @@ Use json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless str
npm install ai @ai-sdk/react
```
## Generate Mode
## Standalone Mode
In generate mode, the AI outputs only JSONL patches. The entire response is a UI spec with no prose. This is the default mode and is ideal for playgrounds, builders, and dashboard generators.
In standalone mode, the AI outputs only JSONL patches. The entire response is a UI spec with no prose. This is the default mode and is ideal for playgrounds, builders, and dashboard generators.
### API Route
@@ -73,9 +73,9 @@ function GenerativeUI() {
}
```
## Chat Mode
## Inline Mode
In chat mode, the AI responds conversationally and includes JSONL patches inline. Text-only replies are allowed when no UI is needed. This is ideal for chatbots, copilots, and educational assistants.
In inline mode, the AI responds conversationally and includes JSONL patches inline. Text-only replies are allowed when no UI is needed. This is ideal for chatbots, copilots, and educational assistants.
### API Route
@@ -96,7 +96,7 @@ export async function POST(req: Request) {
const result = streamText({
model: yourModel,
system: catalog.prompt({ mode: "chat" }),
system: catalog.prompt({ mode: "inline" }),
messages,
});
@@ -182,13 +182,13 @@ const systemPrompt = catalog.prompt({
});
```
### Chat Mode Prompt
### Inline Mode Prompt
```typescript
const chatPrompt = catalog.prompt({ mode: "chat" });
const inlinePrompt = catalog.prompt({ mode: "inline" });
```
In chat mode, the prompt instructs the AI to respond conversationally first, then include JSONL patches on their own lines when UI is needed. Text-only replies are allowed.
In inline mode, the prompt instructs the AI to respond conversationally first, then include JSONL patches on their own lines when UI is needed. Text-only replies are allowed.
## Which Mode?
@@ -197,8 +197,8 @@ In chat mode, the prompt instructs the AI to respond conversationally first, the
<thead>
<tr>
<th></th>
<th>Generate</th>
<th>Chat</th>
<th>Standalone</th>
<th>Inline</th>
</tr>
</thead>
<tbody>
@@ -215,7 +215,7 @@ In chat mode, the prompt instructs the AI to respond conversationally first, the
<tr>
<td>System prompt</td>
<td><code>catalog.prompt()</code></td>
<td><code>{"catalog.prompt({ mode: \"chat\" })"}</code></td>
<td><code>{"catalog.prompt({ mode: \"inline\" })"}</code></td>
</tr>
<tr>
<td>Stream utility</td>
+4 -4
View File
@@ -75,7 +75,7 @@ interface Catalog {
interface PromptOptions {
system?: string; // Custom system message intro
customRules?: string[]; // Additional rules to append
mode?: "generate" | "chat"; // Output mode (default: "generate")
mode?: "standalone" | "inline" | "generate" | "chat"; // Output mode (default: "standalone")
}
interface SpecValidationResult<T> {
@@ -370,7 +370,7 @@ Most users should use `pipeJsonRender()` instead, which wraps this transform for
### createMixedStreamParser
Parse a mixed stream of text and JSONL patches (used for Chat + GenUI mode):
Parse a mixed stream of text and JSONL patches (used for Inline mode):
```typescript
import { createMixedStreamParser } from '@json-render/core';
@@ -389,7 +389,7 @@ parser.flush();
### pipeJsonRender
Pipe an AI SDK `UIMessageStream` through the json-render transform. Lines that parse as JSONL patches are emitted as `data-spec` parts; everything else passes through as text. Used in Chat mode API routes.
Pipe an AI SDK `UIMessageStream` through the json-render transform. Lines that parse as JSONL patches are emitted as `data-spec` parts; everything else passes through as text. Used in Inline mode API routes.
```typescript
import { pipeJsonRender } from '@json-render/core';
@@ -403,7 +403,7 @@ const stream = createUIMessageStream({
return createUIMessageStreamResponse({ stream });
```
See [Generation Modes](/docs/generation-modes) for full Chat mode setup.
See [Generation Modes](/docs/generation-modes) for full Inline mode setup.
### SpecStream Types
+104
View File
@@ -0,0 +1,104 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/jotai")
# @json-render/jotai
Jotai adapter for json-render's `StateStore` interface.
## Installation
```bash
npm install @json-render/jotai @json-render/core @json-render/react jotai
```
## jotaiStateStore
Create a `StateStore` backed by a Jotai atom.
```typescript
import { jotaiStateStore } from "@json-render/jotai";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>atom</code></td>
<td><code>{'WritableAtom<StateModel, [StateModel], void>'}</code></td>
<td>Yes</td>
<td>A writable atom holding the state model.</td>
</tr>
<tr>
<td><code>store</code></td>
<td>Jotai <code>Store</code></td>
<td>No</td>
<td>The Jotai store instance. Defaults to a new store created internally. Pass your own to share state with <code>{'<Provider>'}</code>.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { atom } from "jotai";
import { jotaiStateStore } from "@json-render/jotai";
import { StateProvider } from "@json-render/react";
const uiAtom = atom<Record<string, unknown>>({ count: 0 });
const store = jotaiStateStore({ atom: uiAtom });
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through Jotai */}
</StateProvider>
```
### Shared Jotai Store
If your app already uses a Jotai `<Provider>` with a custom store, pass it so both json-render and your components share the same state:
```typescript
import { atom, createStore } from "jotai";
import { Provider as JotaiProvider } from "jotai/react";
import { jotaiStateStore } from "@json-render/jotai";
import { StateProvider } from "@json-render/react";
const jStore = createStore();
const uiAtom = atom<Record<string, unknown>>({ count: 0 });
const store = jotaiStateStore({ atom: uiAtom, store: jStore });
```
```tsx
<JotaiProvider store={jStore}>
<StateProvider store={store}>
{/* Both json-render and useAtom() see the same state */}
</StateProvider>
</JotaiProvider>
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
+247
View File
@@ -0,0 +1,247 @@
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.
## Install
```bash
npm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk
```
For the iframe-side React UI, also install:
```bash
npm install @json-render/react react react-dom
```
See the [MCP example](https://github.com/vercel-labs/json-render/tree/main/examples/mcp) for a full working example.
## Overview
MCP Apps let MCP servers return interactive HTML UIs that render directly inside chat conversations. `@json-render/mcp` bridges json-render catalogs with the MCP Apps protocol:
1. Your **catalog** defines which components and actions the AI can use
2. The **MCP server** exposes the catalog as a tool with the spec schema
3. The **bundled HTML** renders json-render specs inside the host's sandboxed iframe
4. The AI generates a spec, the host renders it, and users interact with the live UI
## Server API
### createMcpApp
Create a fully-configured MCP server. This is the main entry point.
```typescript
import { createMcpApp } from "@json-render/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import fs from "node:fs";
const server = createMcpApp({
name: "My Dashboard",
version: "1.0.0",
catalog: myCatalog,
html: fs.readFileSync("dist/index.html", "utf-8"),
});
await server.connect(new StdioServerTransport());
```
#### CreateMcpAppOptions
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>name</code></td>
<td><code>string</code></td>
<td>Server name shown in client UIs</td>
</tr>
<tr>
<td><code>version</code></td>
<td><code>string</code></td>
<td>Server version</td>
</tr>
<tr>
<td><code>catalog</code></td>
<td><code>Catalog</code></td>
<td>json-render catalog defining available components</td>
</tr>
<tr>
<td><code>html</code></td>
<td><code>string</code></td>
<td>Self-contained HTML for the iframe UI</td>
</tr>
<tr>
<td><code>tool</code></td>
<td><code>McpToolOptions</code></td>
<td>Optional tool name/title/description overrides</td>
</tr>
</tbody>
</table>
### registerJsonRenderTool
Register a json-render tool on an existing `McpServer`. Use this when you need to add json-render to a server that has other tools.
```typescript
import { registerJsonRenderTool } from "@json-render/mcp";
registerJsonRenderTool(server, {
catalog,
name: "render-ui",
title: "Render UI",
description: "Render an interactive UI",
resourceUri: "ui://render-ui/view.html",
});
```
### registerJsonRenderResource
Register the UI resource that serves the bundled HTML.
```typescript
import { registerJsonRenderResource } from "@json-render/mcp";
registerJsonRenderResource(server, {
resourceUri: "ui://render-ui/view.html",
html: bundledHtml,
});
```
## Client API (`@json-render/mcp/app`)
These exports run inside the sandboxed iframe rendered by the MCP host.
### useJsonRenderApp
React hook that connects to the MCP host, listens for tool results, and maintains the current json-render spec.
```tsx
import { useJsonRenderApp } from "@json-render/mcp/app";
import { JSONUIProvider, Renderer } from "@json-render/react";
function McpAppView({ registry }) {
const { spec, loading, connected, error } = useJsonRenderApp({
name: "my-app",
version: "1.0.0",
});
if (error) return <div>Error: {error.message}</div>;
if (!spec) return <div>Waiting...</div>;
return (
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
<Renderer spec={spec} registry={registry} loading={loading} />
</JSONUIProvider>
);
}
```
#### UseJsonRenderAppReturn
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>spec</code></td>
<td><code>{'Spec | null'}</code></td>
<td>Current json-render spec</td>
</tr>
<tr>
<td><code>loading</code></td>
<td><code>boolean</code></td>
<td>Whether the spec is still being received</td>
</tr>
<tr>
<td><code>connected</code></td>
<td><code>boolean</code></td>
<td>Whether connected to the host</td>
</tr>
<tr>
<td><code>connecting</code></td>
<td><code>boolean</code></td>
<td>Whether currently connecting</td>
</tr>
<tr>
<td><code>error</code></td>
<td><code>{'Error | null'}</code></td>
<td>Connection error, if any</td>
</tr>
<tr>
<td><code>app</code></td>
<td><code>{'App | null'}</code></td>
<td>The underlying MCP App instance</td>
</tr>
<tr>
<td><code>callServerTool</code></td>
<td><code>{'(name, args?) => Promise<void>'}</code></td>
<td>Call an MCP server tool and update spec from result</td>
</tr>
</tbody>
</table>
### buildAppHtml
Generate a self-contained HTML page from bundled JavaScript and CSS.
```typescript
import { buildAppHtml } from "@json-render/mcp/app";
import fs from "node:fs";
const html = buildAppHtml({
title: "Dashboard",
js: fs.readFileSync("dist/app.js", "utf-8"),
css: fs.readFileSync("dist/app.css", "utf-8"),
});
```
## Client Configuration
### Cursor
Add to `.cursor/mcp.json`:
```json
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "path/to/server.ts", "--stdio"]
}
}
}
```
### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/server.ts", "--stdio"]
}
}
}
```
## Supported Clients
MCP Apps are supported by Claude (web and desktop), ChatGPT, VS Code (GitHub Copilot), Cursor, Goose, and Postman.
+104
View File
@@ -0,0 +1,104 @@
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.
## Installation
```bash
npm install @json-render/redux @json-render/core @json-render/react redux
# or with Redux Toolkit (recommended):
npm install @json-render/redux @json-render/core @json-render/react @reduxjs/toolkit
```
## reduxStateStore
Create a `StateStore` backed by a Redux store.
```typescript
import { reduxStateStore } from "@json-render/redux";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>store</code></td>
<td><code>Store</code></td>
<td>Yes</td>
<td>The Redux store instance.</td>
</tr>
<tr>
<td><code>selector</code></td>
<td><code>{'(state: S) => StateModel'}</code></td>
<td>No</td>
<td>Select the json-render slice from the Redux state tree. Defaults to <code>{'(state) => state'}</code>.</td>
</tr>
<tr>
<td><code>dispatch</code></td>
<td><code>{'(nextState: StateModel, store: Store) => void'}</code></td>
<td>Yes</td>
<td>Dispatch an action that replaces the selected slice with the next state.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { reduxStateStore } from "@json-render/redux";
import { StateProvider } from "@json-render/react";
const uiSlice = createSlice({
name: "ui",
initialState: { count: 0 } as Record<string, unknown>,
reducers: {
replaceUiState: (_state, action) => action.payload,
},
});
const reduxStore = configureStore({
reducer: { ui: uiSlice.reducer },
});
const store = reduxStateStore({
store: reduxStore,
selector: (state) => state.ui,
dispatch: (next, s) => s.dispatch(uiSlice.actions.replaceUiState(next)),
});
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through Redux */}
</StateProvider>
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
+201
View File
@@ -0,0 +1,201 @@
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.
## Installation
<PackageInstall packages="@json-render/core @json-render/solid" />
Peer dependencies: `solid-js ^1.9.0` and `zod ^4.0.0`.
<PackageInstall packages="solid-js zod" />
## Providers
### StateProvider
```tsx
<StateProvider
initialState={{}}
onStateChange={(changes) => console.log(changes)}
>
{/* children */}
</StateProvider>
```
<table>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>store</code>
</td>
<td>
<code>StateStore</code>
</td>
<td>
External store (controlled mode). When provided,{" "}
<code>initialState</code> and <code>onStateChange</code> are ignored.
</td>
</tr>
<tr>
<td>
<code>initialState</code>
</td>
<td>
<code>Record&lt;string, unknown&gt;</code>
</td>
<td>Initial state model for uncontrolled mode.</td>
</tr>
<tr>
<td>
<code>onStateChange</code>
</td>
<td>
<code>
{"(changes: Array<{ path: string; value: unknown }>) => void"}
</code>
</td>
<td>Called for uncontrolled state updates.</td>
</tr>
</tbody>
</table>
### ActionProvider
```tsx
<ActionProvider
handlers={{ submit: async (params) => {} }}
navigate={(path) => {}}
>
{/* children */}
</ActionProvider>
```
### VisibilityProvider
```tsx
<VisibilityProvider>{/* children */}</VisibilityProvider>
```
### ValidationProvider
```tsx
<ValidationProvider customFunctions={{ custom: (value) => Boolean(value) }}>
{/* children */}
</ValidationProvider>
```
### JSONUIProvider
Combined provider wrapper for state, visibility, validation, and actions.
```tsx
<JSONUIProvider
registry={registry}
initialState={{}}
handlers={handlers}
validationFunctions={validationFunctions}
>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
```
## defineRegistry
Create a typed component registry and action helpers from a catalog.
```tsx
const { registry, handlers, executeAction } = defineRegistry(catalog, {
components: {
Card: (renderProps) => <div>{renderProps.children}</div>,
Button: (renderProps) => (
<button onClick={() => renderProps.emit("press")}>
{renderProps.element.props.label as string}
</button>
),
},
actions: {
submit: async (params, setState, state) => {
// custom action logic
},
},
});
```
## Components
### Renderer
```tsx
<Renderer spec={spec} registry={registry} loading={false} />
```
Renders a `Spec` tree using your registry.
### createRenderer
Build an app-level renderer from catalog + components:
```tsx
const AppRenderer = createRenderer(catalog, {
Card: (renderProps) => <div>{renderProps.children}</div>,
});
<AppRenderer spec={spec} state={{}} onAction={(name, params) => {}} />;
```
## Hooks
- `useStateStore()`
- `useStateValue(path)` - returns an accessor
- `useStateBinding(path)` - returns `[Accessor<T | undefined>, setValue]`
- `useVisibility()` / `useIsVisible(condition)`
- `useActions()` / `useAction(binding)`
- `useValidation()` / `useOptionalValidation()`
- `useFieldValidation(path, config)` - returns accessor-backed `state`, `errors`, and `isValid`
- `useBoundProp(value, bindingPath)`
- `useUIStream(options)`
- `useChatUI(options)`
## Built-in Actions
`ActionProvider` handles these built-in actions:
- `setState`
- `pushState`
- `removeState`
- `validateForm`
## Component Props
Registry components receive:
```ts
interface ComponentRenderProps<P = Record<string, unknown>> {
element: UIElement<string, P>;
children?: JSX.Element;
emit: (event: string) => void;
on: (event: string) => EventHandle;
bindings?: Record<string, string>;
loading?: boolean;
}
```
Use `emit("event")` to dispatch event bindings. Use `on("event")` to access `EventHandle` metadata (`bound`, `shouldPreventDefault`, `emit`).
## Reactivity Notes
- Keep changing reads in JSX expressions, `createMemo`, or `createEffect`.
- Avoid props destructuring in component signatures when you need live updates.
- `StateProvider` and other contexts expose getter-backed values so consumers read live signals.
- `useStateValue`, `useStateBinding`, and `useFieldValidation` expose reactive accessors; call them as functions.
@@ -0,0 +1,126 @@
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.
## Installation
<PackageInstall packages="@json-render/core @json-render/svelte" />
Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.
<PackageInstall packages="svelte zod" />
## Components
### Renderer
```svelte
<Renderer
spec={spec} // Spec | null
registry={registry}
loading={false}
/>
```
Renders a spec with your component registry. If `spec` is `null`, it renders nothing.
### JsonUIProvider
Convenience wrapper around `StateProvider`, `VisibilityProvider`, `ValidationProvider`, and `ActionProvider`.
```svelte
<JsonUIProvider
initialState={{}}
handlers={handlers}
validationFunctions={validationFunctions}
>
<Renderer {spec} {registry} />
</JsonUIProvider>
```
## defineRegistry
Create a typed component registry and action handlers from a catalog.
```typescript
import { defineRegistry } from "@json-render/svelte";
const { registry, handlers, executeAction } = defineRegistry(catalog, {
components: {
Card,
Button,
},
actions: {
submit: async (params, setState, state) => {
// custom action logic
},
},
});
```
`handlers` is designed for `JsonUIProvider`/`ActionProvider`. `executeAction` is an imperative helper.
## Component Props
Registry components receive `BaseComponentProps<TProps>`:
```typescript
interface BaseComponentProps<TProps> {
props: TProps;
children?: Snippet;
emit: (event: string) => void;
bindings?: Record<string, string>;
loading?: boolean;
}
```
Use `emit("eventName")` to trigger handlers declared in the spec `on` bindings.
## Context Helpers
Use these helpers inside Svelte components:
- `getStateValue(path)` - read/write state via `.current`
- `getBoundProp(() => value, () => bindingPath)` - write back resolved `$bindState` / `$bindItem` values
- `isVisible(condition)` - evaluate visibility via `.current`
- `getAction(name)` - read a registered action handler via `.current`
- `getFieldValidation(ctx, path, config)` - get field validation state + actions
For advanced usage, access full contexts:
- `getStateContext()`
- `getActionContext()`
- `getVisibilityContext()`
- `getValidationContext()`
- `getOptionalValidationContext()`
## Streaming
### createUIStream
```typescript
const stream = createUIStream({
api: "/api/generate-ui",
onComplete: (spec) => console.log(spec),
});
await stream.send("Create a login form");
console.log(stream.spec);
console.log(stream.isStreaming);
```
### createChatUI
```typescript
const chat = createChatUI({ api: "/api/chat-ui" });
await chat.send("Build a settings panel");
console.log(chat.messages, chat.isStreaming);
```
## Schema Export
Use `schema` from `@json-render/svelte` when defining catalogs for Svelte specs.
@@ -0,0 +1,77 @@
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.
Requires `@xstate/store` v3+.
## Installation
```bash
npm install @json-render/xstate @json-render/core @json-render/react @xstate/store
```
## xstateStoreStateStore
Create a `StateStore` backed by an `@xstate/store` atom.
```typescript
import { xstateStoreStateStore } from "@json-render/xstate";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>atom</code></td>
<td><code>{'Atom<StateModel>'}</code></td>
<td>Yes</td>
<td>An <code>@xstate/store</code> atom (from <code>createAtom</code>) holding the json-render state model.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { createAtom } from "@xstate/store";
import { xstateStoreStateStore } from "@json-render/xstate";
import { StateProvider } from "@json-render/react";
const uiAtom = createAtom({ count: 0 });
const store = xstateStoreStateStore({ atom: uiAtom });
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through @xstate/store */}
</StateProvider>
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
@@ -0,0 +1,108 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/zustand")
# @json-render/zustand
Zustand adapter for json-render's `StateStore` interface.
Requires Zustand v5+. Zustand v4 is not supported due to breaking API changes in the vanilla store interface.
## Installation
```bash
npm install @json-render/zustand @json-render/core @json-render/react zustand
```
## zustandStateStore
Create a `StateStore` backed by a Zustand vanilla store.
```typescript
import { zustandStateStore } from "@json-render/zustand";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>store</code></td>
<td><code>{'StoreApi<S>'}</code></td>
<td>Yes</td>
<td>A Zustand vanilla store (from <code>createStore</code> in <code>zustand/vanilla</code>).</td>
</tr>
<tr>
<td><code>selector</code></td>
<td><code>{'(state: S) => StateModel'}</code></td>
<td>No</td>
<td>Select the json-render slice from the store state. Defaults to the entire state.</td>
</tr>
<tr>
<td><code>updater</code></td>
<td><code>{'(nextState: StateModel, store: StoreApi<S>) => void'}</code></td>
<td>No</td>
<td>Apply a state change back to the store. Defaults to a shallow merge.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { createStore } from "zustand/vanilla";
import { zustandStateStore } from "@json-render/zustand";
import { StateProvider } from "@json-render/react";
const bearStore = createStore(() => ({
count: 0,
name: "Bear",
}));
const store = zustandStateStore({ store: bearStore });
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through Zustand */}
</StateProvider>
```
### Nested Slice
```typescript
const appStore = createStore(() => ({
ui: { count: 0 },
auth: { token: null },
}));
const store = zustandStateStore({
store: appStore,
selector: (s) => s.ui,
updater: (next, s) => s.setState({ ui: next }),
});
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
@@ -341,6 +341,8 @@ February 2026
### New: Chat Mode (Inline GenUI)
> **Note:** These modes were renamed in v0.12.1 — "Generate" is now "Standalone" and "Chat" is now "Inline". The old names are accepted as deprecated aliases.
json-render now supports two generation modes: **Generate** (JSONL-only, the default) and **Chat** (text + JSONL inline). Chat mode lets the AI respond conversationally with embedded UI specs, ideal for chatbots and copilot experiences.
```typescript
@@ -3,15 +3,15 @@ export const metadata = pageMetadata("docs/generation-modes")
# Generation Modes
json-render supports two modes for AI-generated UI: **Generate mode** for standalone UI and **Chat mode** for inline UI within a conversation.
json-render supports two modes for AI-generated UI: **Standalone mode** for standalone UI and **Inline mode** for inline UI within a conversation.
The mode controls how the AI formats its output and how your app processes the stream. The underlying JSONL patch format is the same in both modes.
<GenerationModesDiagram />
## Generate Mode (Standalone)
## Standalone Mode
In generate mode, the AI outputs **only JSONL patches** — no prose, no markdown. The entire response is a UI spec.
In standalone mode, the AI outputs **only JSONL patches** — no prose, no markdown. The entire response is a UI spec.
This is the default mode and is ideal for:
@@ -25,7 +25,7 @@ This is the default mode and is ideal for:
```typescript
import { streamText } from "ai";
// Generate mode is the default (no mode option needed)
// Standalone mode is the default (no mode option needed)
const systemPrompt = catalog.prompt({
customRules: [
"Use Card as root for forms and small UIs.",
@@ -74,9 +74,9 @@ The AI outputs only JSONL — one patch per line, no surrounding text:
{"op":"add","path":"/elements/submit","value":{"type":"Button","props":{"label":"Sign In"}}}
```
## Chat Mode (Inline)
## Inline Mode
In chat mode, the AI responds **conversationally first**, then outputs JSONL patches on their own lines. Text-only replies are allowed when no UI is needed (e.g. greetings, clarifying questions).
In inline mode, the AI responds **conversationally first**, then outputs JSONL patches on their own lines. Text-only replies are allowed when no UI is needed (e.g. greetings, clarifying questions).
This is ideal for:
@@ -92,8 +92,8 @@ import { streamText } from "ai";
import { pipeJsonRender } from "@json-render/core";
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
// Enable chat mode
const systemPrompt = catalog.prompt({ mode: "chat" });
// Enable inline mode
const systemPrompt = catalog.prompt({ mode: "inline" });
const result = streamText({
model: yourModel,
@@ -180,8 +180,8 @@ If the user asks a simple question ("what does BTC stand for?"), the AI replies
<thead>
<tr>
<th />
<th>Generate</th>
<th>Chat</th>
<th>Standalone</th>
<th>Inline</th>
</tr>
</thead>
<tbody>
@@ -198,7 +198,7 @@ If the user asks a simple question ("what does BTC stand for?"), the AI replies
<tr>
<td>System prompt</td>
<td><code>{"catalog.prompt()"}</code></td>
<td><code>{'catalog.prompt({ mode: "chat" })'}</code></td>
<td><code>{'catalog.prompt({ mode: "inline" })'}</code></td>
</tr>
<tr>
<td>Stream utility</td>
@@ -21,6 +21,14 @@ Peer dependencies: `vue ^3.5.0` and `zod ^4.0.0`.
<PackageInstall packages="vue zod" />
## For Svelte
<PackageInstall packages="@json-render/core @json-render/svelte" />
Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.
<PackageInstall packages="svelte zod" />
## For React UI with shadcn/ui
Pre-built components for fast prototyping and production use:
+26 -1
View File
@@ -309,10 +309,35 @@ const catalog = defineCatalog(schema, {
const prompt = catalog.prompt();
// Chat mode prompt
// Inline mode prompt (formerly "chat")
const inlinePrompt = catalog.prompt({ mode: "inline" });
```
## Generation Modes
The generation mode values passed to `catalog.prompt()` have been renamed for clarity:
- `"generate"` is now `"standalone"`
- `"chat"` is now `"inline"`
The old names are accepted as deprecated aliases, so existing code will continue to work. Update when convenient.
**Before:**
```typescript
const prompt = catalog.prompt({ mode: "generate" });
const chatPrompt = catalog.prompt({ mode: "chat" });
```
**After:**
```typescript
const prompt = catalog.prompt({ mode: "standalone" });
const inlinePrompt = catalog.prompt({ mode: "inline" });
```
The default mode (when no `mode` option is provided) is `"standalone"`, which behaves identically to the previous `"generate"` default.
## Validation
`ValidationCheck` now uses `type` instead of `fn`, `ValidationProvider` uses `customFunctions` instead of `functions`, and `useFieldValidation` takes a config object instead of a checks array.
+1 -1
View File
@@ -91,7 +91,7 @@ The result is a native UI built from your own components — not an iframe, not
- **[Streaming](/docs/streaming)** — Render progressively as the AI responds. Each JSONL patch adds to the spec and the UI updates in real time.
- **[Data Binding](/docs/data-binding)** — Bind props to runtime data with `$state` paths, repeat elements over arrays, and wire two-way input bindings.
- **[Visibility](/docs/visibility)** — Show or hide elements based on state conditions. The AI can generate conditional UIs without writing logic.
- **[Generation Modes](/docs/generation-modes)** — Generate standalone UI (playground/builder) or inline UI within a chat conversation.
- **[Generation Modes](/docs/generation-modes)** — Standalone mode for full-page generated UI or inline mode for UI embedded in a conversation.
## Next
+85 -14
View File
@@ -1,5 +1,5 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/renderers")
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/renderers");
# Renderers
@@ -22,37 +22,65 @@ All renderers share the same workflow:
<tbody>
<tr>
<td>React</td>
<td><code>@json-render/react</code></td>
<td>
<code>@json-render/react</code>
</td>
<td>React component tree</td>
</tr>
<tr>
<td>Vue</td>
<td><code>@json-render/vue</code></td>
<td>
<code>@json-render/vue</code>
</td>
<td>Vue 3 component tree</td>
</tr>
<tr>
<td>Svelte</td>
<td>
<code>@json-render/svelte</code>
</td>
<td>Svelte 5 component tree</td>
</tr>
<tr>
<td>Solid</td>
<td>
<code>@json-render/solid</code>
</td>
<td>SolidJS component tree</td>
</tr>
<tr>
<td>shadcn/ui</td>
<td><code>@json-render/shadcn</code></td>
<td>
<code>@json-render/shadcn</code>
</td>
<td>Pre-built Radix UI + Tailwind components (uses React renderer)</td>
</tr>
<tr>
<td>React Native</td>
<td><code>@json-render/react-native</code></td>
<td>
<code>@json-render/react-native</code>
</td>
<td>Native mobile views</td>
</tr>
<tr>
<td>Image</td>
<td><code>@json-render/image</code></td>
<td>
<code>@json-render/image</code>
</td>
<td>SVG / PNG (via Satori)</td>
</tr>
<tr>
<td>React PDF</td>
<td><code>@json-render/react-pdf</code></td>
<td>
<code>@json-render/react-pdf</code>
</td>
<td>PDF documents</td>
</tr>
<tr>
<td>Remotion</td>
<td><code>@json-render/remotion</code></td>
<td>
<code>@json-render/remotion</code>
</td>
<td>Video compositions</td>
</tr>
</tbody>
@@ -67,7 +95,7 @@ import { defineRegistry, Renderer } from "@json-render/react";
import { schema } from "@json-render/react/schema";
const { registry } = defineRegistry(catalog, { components });
<Renderer spec={spec} registry={registry} />
<Renderer spec={spec} registry={registry} />;
```
Use `StateProvider`, `VisibilityProvider`, and `ActionProvider` for full interactivity. See the [@json-render/react API reference](/docs/api/react) for details.
@@ -90,6 +118,42 @@ const { registry } = defineRegistry(catalog, {
Uses composables (`useStateStore`, `useStateBinding`, `useActions`, etc.) instead of React hooks. See the [@json-render/vue API reference](/docs/api/vue) for details.
## Svelte
Svelte 5 renderer with runes-compatible context helpers, visibility conditions, actions, and streaming support.
```typescript
import { defineRegistry, Renderer } from "@json-render/svelte";
import { schema } from "@json-render/svelte/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => /* Svelte snippet */,
},
});
```
See the [@json-render/svelte API reference](/docs/api/svelte) for details.
## Solid
SolidJS renderer with fine-grained reactivity, state bindings, validation, visibility, and event-driven actions.
```tsx
import { defineRegistry, Renderer } from "@json-render/solid";
import { schema } from "@json-render/solid/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: (renderProps) => <div>{renderProps.children}</div>,
},
});
<Renderer spec={spec} registry={registry} />;
```
See the [@json-render/solid API reference](/docs/api/solid) for details.
## shadcn/ui
36 pre-built components using Radix UI and Tailwind CSS. Built on top of `@json-render/react` -- no custom renderer needed.
@@ -125,7 +189,10 @@ Render specs as native mobile views. Includes 25+ standard components and standa
```tsx
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react-native/schema";
import { standardComponentDefinitions, standardActionDefinitions } from "@json-render/react-native/catalog";
import {
standardComponentDefinitions,
standardActionDefinitions,
} from "@json-render/react-native/catalog";
import { defineRegistry, Renderer } from "@json-render/react-native";
const catalog = defineCatalog(schema, {
@@ -134,7 +201,7 @@ const catalog = defineCatalog(schema, {
});
const { registry } = defineRegistry(catalog, { components: {} });
<Renderer spec={spec} registry={registry} />
<Renderer spec={spec} registry={registry} />;
```
See the [@json-render/react-native API reference](/docs/api/react-native) for details.
@@ -159,7 +226,11 @@ See the [@json-render/image API reference](/docs/api/image) for details.
Generate PDF documents from JSON specs using `@react-pdf/renderer`. Render to buffer, stream, or file.
```typescript
import { renderToBuffer, renderToStream, renderToFile } from "@json-render/react-pdf";
import {
renderToBuffer,
renderToStream,
renderToFile,
} from "@json-render/react-pdf";
const buffer = await renderToBuffer(spec);
const stream = await renderToStream(spec);
@@ -185,7 +256,7 @@ import { Renderer } from "@json-render/remotion";
fps={spec.composition.fps}
compositionWidth={spec.composition.width}
compositionHeight={spec.composition.height}
/>
/>;
```
Uses a timeline spec format with compositions, tracks, and clips. Includes standard components (TitleCard, TypingText, ImageSlide, etc.), transitions (fade, slide, zoom, wipe), and effects.
+123
View File
@@ -0,0 +1,123 @@
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.
## Available Skills
- **core** — Core schemas, catalogs, and AI prompt generation.
- **react** — React renderer that turns JSON specs into React component trees.
- **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.
- **shadcn** — Pre-built shadcn/ui components (Radix UI + Tailwind).
- **image** — Image renderer that turns JSON specs into SVG and PNG via Satori.
- **remotion** — Remotion renderer for video generation from JSON timeline specs.
- **vue** — Vue 3 renderer for Vue component trees.
- **svelte** — Svelte 5 renderer for Svelte component trees.
- **solid** — SolidJS renderer for fine-grained reactive component trees.
- **codegen** — Code generation utilities for building custom exporters.
- **mcp** — MCP Apps integration for Claude, ChatGPT, Cursor, and VS Code.
- **redux** — Redux adapter for json-render's `StateStore` interface.
- **zustand** — Zustand adapter for json-render's `StateStore` interface.
- **jotai** — Jotai adapter for json-render's `StateStore` interface.
- **xstate** — XState Store adapter for json-render's `StateStore` interface.
## Installation
```bash
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 react-pdf
npx skills add vercel-labs/json-render --skill react-email
npx skills add vercel-labs/json-render --skill react-native
npx skills add vercel-labs/json-render --skill shadcn
npx skills add vercel-labs/json-render --skill image
npx skills add vercel-labs/json-render --skill remotion
npx skills add vercel-labs/json-render --skill vue
npx skills add vercel-labs/json-render --skill svelte
npx skills add vercel-labs/json-render --skill solid
npx skills add vercel-labs/json-render --skill codegen
npx skills add vercel-labs/json-render --skill mcp
npx skills add vercel-labs/json-render --skill redux
npx skills add vercel-labs/json-render --skill zustand
npx skills add vercel-labs/json-render --skill jotai
npx skills add vercel-labs/json-render --skill xstate
```
After installing, your AI agent will automatically activate the right skill when it encounters a matching request.
## core
The foundational skill. Teaches agents how to define catalogs, create schemas, build specs, and generate AI prompts. This is the starting point for any json-render project and covers `defineCatalog`, `defineSchema`, `specSchema`, `toPrompt`, and the full spec format.
## react
Teaches agents how to render JSON specs as React component trees using `JsonRender`, `JsonRenderClient`, and `useJsonRender`. Covers custom component registries, client-side interactivity, state management, and streaming integration.
## 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.
## react-email
Teaches agents how to render JSON specs as HTML or plain-text emails using React Email components. Covers the email-specific registry and rendering pipeline.
## react-native
Teaches agents how to render JSON specs as native mobile UIs with React Native. Covers the native component registry and platform-specific considerations.
## shadcn
Teaches agents how to use the pre-built shadcn/ui component registry with json-render. Includes Radix UI primitives, Tailwind styling, and the full set of available shadcn components.
## image
Teaches agents how to turn JSON specs into SVG and PNG images using Satori. Covers the image-specific registry, dimensions, fonts, and rendering options.
## remotion
Teaches agents how to generate videos from JSON timeline specs using Remotion. Covers compositions, sequences, timeline structure, and video rendering.
## vue
Teaches agents how to render JSON specs as Vue 3 component trees. Covers the Vue renderer API, custom component registries, and reactivity integration.
## svelte
Teaches agents how to render JSON specs as Svelte 5 component trees. Covers the Svelte renderer API and component registration.
## solid
Teaches agents how to render JSON specs as SolidJS component trees. Covers Solid-specific reactive patterns, provider wiring, bindings, actions, and streaming.
## codegen
Teaches agents how to use code generation utilities to export UI specs as framework-specific source code. Covers the codegen pipeline and custom exporter creation.
## mcp
Teaches agents how to build MCP Apps that serve json-render UIs inside AI tools like Claude, ChatGPT, Cursor, and VS Code. Covers MCP server setup, tool definitions, and UI streaming.
## redux
Teaches agents how to connect a Redux store to json-render's `StateStore` interface for state-driven UIs.
## zustand
Teaches agents how to connect a Zustand store to json-render's `StateStore` interface for lightweight state management.
## jotai
Teaches agents how to connect Jotai atoms to json-render's `StateStore` interface for atomic state management.
## xstate
Teaches agents how to connect an XState Store to json-render's `StateStore` interface for state-machine-driven UIs.
## Source
All skill files are in the [`skills/`](https://github.com/vercel-labs/json-render/tree/main/skills) directory of the repository.
+2 -1
View File
@@ -16,7 +16,8 @@ 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/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen
npm packages: @json-render/core, @json-render/react, @json-render/vue, @json-render/svelte, @json-render/solid, @json-render/shadcn, @json-render/react-native, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp, @json-render/redux, @json-render/zustand, @json-render/jotai, @json-render/xstate
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, react-pdf, react-email, react-native, shadcn, image, remotion, vue, svelte, solid, codegen, mcp, redux, zustand, jotai, xstate. See /docs/skills for details.
You have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory.
+233 -76
View File
@@ -1,100 +1,257 @@
"use client";
function Skeleton({ className = "" }: { className?: string }) {
return <div className={`rounded bg-muted-foreground/10 ${className}`} />;
}
/** Simple form wireframe reused in both diagrams */
function FormUI() {
function ScheduleItem({
time,
label,
color,
}: {
time: string;
label: string;
color: string;
}) {
return (
<div className="border border-border rounded-lg p-2.5 space-y-1.5 bg-muted/30">
{/* Title */}
<Skeleton className="h-2.5 w-14 mb-1" />
{/* Input fields */}
<Skeleton className="h-4 w-full rounded-sm" />
<Skeleton className="h-4 w-full rounded-sm" />
{/* Submit button */}
<Skeleton className="h-4 w-16 rounded-sm bg-muted-foreground/20 mt-1" />
<div className="flex items-center gap-2 py-1.5 border-t border-border/50 first:border-t-0">
<span className="text-[10px] text-muted-foreground/60 w-12 shrink-0 tabular-nums">
{time}
</span>
<span
className="w-1.5 h-1.5 rounded-full shrink-0"
style={{ background: color }}
/>
<span className="text-[11px] text-muted-foreground">{label}</span>
</div>
);
}
function ChatModeDiagram() {
function InlineModeDiagram() {
return (
<div className="flex flex-col h-full">
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
Chat Mode
<div className="text-sm font-medium text-foreground text-center mb-3">
Inline Mode
</div>
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-col">
{/* Chat area */}
<div className="flex-1 p-3 overflow-hidden flex justify-center">
<div className="w-1/3 min-w-[120px] space-y-3">
<div className="flex-1 border border-border rounded-2xl bg-background overflow-hidden flex flex-col">
<div className="flex-1 p-4 space-y-3 overflow-hidden">
{/* User message */}
<div className="flex justify-end">
<div className="bg-muted-foreground/20 rounded-2xl rounded-br px-3.5 py-2 max-w-[85%]">
<span className="text-[11px] text-foreground/80">
I have back-to-back meetings today
</span>
</div>
</div>
{/* AI response */}
<div className="space-y-2">
<p className="text-[11px] text-muted-foreground/70">
Packed day! Here&apos;s what you&apos;ve got:
</p>
<div className="bg-muted/40 border border-border rounded-xl p-3 w-[92%]">
<div className="text-[11px] font-semibold text-foreground/80 mb-2">
Today&apos;s Schedule
</div>
<ScheduleItem
time="10:00 AM"
label="Design Review"
color="#7aa2f7"
/>
<ScheduleItem
time="1:00 PM"
label="Sprint Planning"
color="#bb9af7"
/>
<ScheduleItem
time="3:30 PM"
label="Team Standup"
color="#9ece6a"
/>
<ScheduleItem
time="4:30 PM"
label="Eng All-Hands"
color="#e0af68"
/>
</div>
</div>
</div>
{/* Prompt input */}
<div className="p-2.5">
<div className="bg-muted/50 border border-border rounded-xl px-2.5 py-1.5 flex items-center gap-1.5">
<span className="text-[10px] text-muted-foreground/40 flex-1">
Message...
</span>
<div className="w-5 h-5 rounded-md bg-muted-foreground/20 flex items-center justify-center">
<svg
className="w-2.5 h-2.5 text-muted-foreground/50"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
transform="rotate(-90 12 12)"
/>
</svg>
</div>
</div>
</div>
</div>
<div className="mt-3 flex flex-col items-center gap-2">
<div className="text-xs font-medium text-muted-foreground">
AI decides when UI beats text
</div>
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground/50">
<span>AI chatbots</span>
<span className="text-muted-foreground/30">/</span>
<span>Copilots</span>
<span className="text-muted-foreground/30">/</span>
<span>Assistants</span>
</div>
</div>
</div>
);
}
function LandingPage() {
return (
<div className="w-full h-full flex flex-col bg-muted/20">
{/* Nav */}
<div className="flex items-center justify-between px-3 py-2 border-b border-border/50">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
className="shrink-0"
>
<rect
x="2"
y="14"
width="5"
height="8"
rx="1"
className="fill-muted-foreground/60"
/>
<rect
x="9.5"
y="8"
width="5"
height="14"
rx="1"
className="fill-muted-foreground/60"
/>
<rect
x="17"
y="2"
width="5"
height="20"
rx="1"
className="fill-muted-foreground/60"
/>
</svg>
<div className="flex items-center gap-2">
<span className="text-[9px] text-muted-foreground/50">Pricing</span>
<span className="text-[9px] text-muted-foreground/50">Blog</span>
<span className="text-[8px] font-semibold bg-foreground text-background rounded-md px-1.5 py-0.5 whitespace-nowrap">
Get Started
</span>
</div>
</div>
{/* Hero */}
<div className="flex-1 flex flex-col items-center justify-center gap-2.5 text-center px-4">
<span className="text-[8px] font-medium text-green-400 bg-green-400/10 border border-green-400/15 rounded-full px-2 py-0.5">
Trusted by 2,000+ teams
</span>
<div className="text-sm font-bold text-foreground leading-tight">
Analytics that
<br />
move the needle
</div>
<div className="text-[10px] text-muted-foreground/50 leading-relaxed">
Ship what matters. No setup required.
</div>
<div className="flex gap-1.5 mt-1">
<button className="bg-foreground text-background text-[9px] font-semibold rounded-lg px-3 py-1.5 whitespace-nowrap">
Start Free
</button>
<button className="border border-border text-muted-foreground text-[9px] font-medium rounded-lg px-3 py-1.5 whitespace-nowrap">
Book Demo
</button>
</div>
</div>
{/* Footer */}
<div className="flex justify-center gap-3 py-2 border-t border-border/50">
<span className="text-[8px] text-muted-foreground/30">Privacy</span>
<span className="text-[8px] text-muted-foreground/30">Terms</span>
<span className="text-[8px] text-muted-foreground/30">Status</span>
</div>
</div>
);
}
function StandaloneModeDiagram() {
return (
<div className="flex flex-col h-full">
<div className="text-sm font-medium text-foreground text-center mb-3">
Standalone Mode
</div>
<div className="flex-1 border border-border rounded-2xl bg-background overflow-hidden flex flex-row">
{/* Left panel - conversation */}
<div className="w-[38%] border-r border-border/50 flex flex-col">
<div className="flex-1 p-3 space-y-2.5">
{/* User message */}
<div className="flex justify-end">
<div className="bg-muted rounded-xl px-3 py-2">
<Skeleton className="h-2 w-14" />
<div className="bg-muted-foreground/20 rounded-2xl rounded-br px-2.5 py-1.5">
<span className="text-[10px] text-foreground/80 block leading-relaxed">
A landing page for my analytics startup
</span>
</div>
</div>
{/* AI text */}
<p className="text-[10px] text-muted-foreground/60 leading-relaxed">
Here&apos;s a clean landing page with a hero, nav, and CTAs.
</p>
</div>
{/* Assistant text */}
<div className="space-y-2">
<div className="space-y-1.5">
<Skeleton className="h-2 w-full" />
<Skeleton className="h-2 w-3/4" />
</div>
{/* Inline UI */}
<FormUI />
{/* More text after UI */}
<div className="space-y-1.5">
<Skeleton className="h-2 w-4/5" />
{/* Prompt input */}
<div className="p-2.5">
<div className="bg-muted/50 border border-border rounded-xl px-2.5 py-1.5 flex items-center gap-1.5">
<span className="text-[10px] text-muted-foreground/40 flex-1">
Message...
</span>
<div className="w-5 h-5 rounded-md bg-muted-foreground/20 flex items-center justify-center">
<svg
className="w-2.5 h-2.5 text-muted-foreground/50"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
transform="rotate(-90 12 12)"
/>
</svg>
</div>
</div>
</div>
</div>
{/* Input bar */}
<div className="p-2 flex justify-center">
<div className="w-1/3 min-w-[120px] flex items-center gap-2">
<Skeleton className="h-7 flex-1 rounded-md" />
<Skeleton className="h-7 w-7 rounded-md" />
</div>
{/* Right panel - landing page */}
<div className="flex-1">
<LandingPage />
</div>
</div>
<div className="text-[10px] text-muted-foreground/60 mt-2 text-center">
Text + UI interleaved in messages
</div>
</div>
);
}
function GenerateModeDiagram() {
return (
<div className="flex flex-col h-full">
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
Generate Mode
</div>
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-row">
{/* Left panel - prompt */}
<div className="w-[38%] border-r border-border flex flex-col">
<div className="flex-1" />
<div className="p-3 space-y-2">
<Skeleton className="h-7 w-full rounded-md" />
<Skeleton className="h-5 w-16 rounded-md" />
</div>
<div className="mt-3 flex flex-col items-center gap-2">
<div className="text-xs font-medium text-muted-foreground">
Prompt in, UI out
</div>
{/* Right panel - UI preview */}
<div className="flex-1 p-3 flex items-center justify-center">
<div className="w-3/4">
<FormUI />
</div>
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground/50">
<span>Website builders</span>
<span className="text-muted-foreground/30">/</span>
<span>Text-to-widget</span>
<span className="text-muted-foreground/30">/</span>
<span>Dashboards</span>
</div>
</div>
<div className="text-[10px] text-muted-foreground/60 mt-2 text-center">
Prompt separate from UI preview
</div>
</div>
);
}
@@ -103,11 +260,11 @@ export function GenerationModesDiagram() {
return (
<div className="not-prose my-8">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="h-[280px]">
<ChatModeDiagram />
<div className="h-[420px]">
<InlineModeDiagram />
</div>
<div className="h-[280px]">
<GenerateModeDiagram />
<div className="h-[420px]">
<StandaloneModeDiagram />
</div>
</div>
</div>
+19 -1
View File
@@ -16,6 +16,7 @@ export const docsNavigation: NavSection[] = [
{ title: "Introduction", href: "/docs" },
{ title: "Installation", href: "/docs/installation" },
{ title: "Quick Start", href: "/docs/quick-start" },
{ title: "Skills", href: "/docs/skills" },
{ title: "Migration Guide", href: "/docs/migration" },
{ title: "Changelog", href: "/docs/changelog" },
],
@@ -80,16 +81,26 @@ export const docsNavigation: NavSection[] = [
href: "https://github.com/vercel-labs/json-render/tree/main/examples/image",
external: true,
},
{
title: "Svelte",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/svelte",
external: true,
},
{
title: "Vue",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vue",
external: true,
},
{
title: "Renders with Vite (Vue / React)",
title: "Renders with Vite (Vue / React / Svelte / Solid)",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
external: true,
},
{
title: "MCP App",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/mcp",
external: true,
},
],
},
{
@@ -121,7 +132,14 @@ export const docsNavigation: NavSection[] = [
{ title: "@json-render/image", href: "/docs/api/image" },
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
{ title: "@json-render/vue", href: "/docs/api/vue" },
{ title: "@json-render/svelte", href: "/docs/api/svelte" },
{ title: "@json-render/solid", href: "/docs/api/solid" },
{ title: "@json-render/codegen", href: "/docs/api/codegen" },
{ title: "@json-render/mcp", href: "/docs/api/mcp" },
{ title: "@json-render/redux", href: "/docs/api/redux" },
{ title: "@json-render/zustand", href: "/docs/api/zustand" },
{ title: "@json-render/jotai", href: "/docs/api/jotai" },
{ title: "@json-render/xstate", href: "/docs/api/xstate" },
],
},
];
+8
View File
@@ -37,18 +37,26 @@ export const PAGE_TITLES: Record<string, string> = {
"docs/ag-ui": "AG-UI Integration",
"docs/migration": "Migration Guide",
"docs/changelog": "Changelog",
"docs/skills": "Skills",
// API references
"docs/api/core": "@json-render/core API",
"docs/api/react": "@json-render/react API",
"docs/api/vue": "@json-render/vue API",
"docs/api/solid": "@json-render/solid API",
"docs/api/react-pdf": "@json-render/react-pdf API",
"docs/api/react-email": "@json-render/react-email API",
"docs/api/react-native": "@json-render/react-native API",
"docs/api/svelte": "@json-render/svelte API",
"docs/api/codegen": "@json-render/codegen API",
"docs/api/image": "@json-render/image API",
"docs/api/remotion": "@json-render/remotion API",
"docs/api/shadcn": "@json-render/shadcn API",
"docs/api/mcp": "@json-render/mcp API",
"docs/api/redux": "@json-render/redux API",
"docs/api/zustand": "@json-render/zustand API",
"docs/api/jotai": "@json-render/jotai API",
"docs/api/xstate": "@json-render/xstate API",
};
/**
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.1.4",
"version": "0.1.6",
"type": "module",
"private": true,
"license": "Apache-2.0",
+18
View File
@@ -1,5 +1,23 @@
# example-chat
## 0.1.6
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/react@0.12.1
- @json-render/shadcn@0.12.1
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react@0.12.0
- @json-render/shadcn@0.12.0
## 0.1.4
### Patch Changes
+1 -1
View File
@@ -130,7 +130,7 @@ When the user asks for a quiz, test, or Q&A, build an interactive experience:
4. You can also add a final score section that becomes visible when all questions are submitted.
${explorerCatalog.prompt({
mode: "chat",
mode: "inline",
customRules: [
"NEVER use viewport height classes (min-h-screen, h-screen) — the UI renders inside a fixed-size container.",
"Prefer Grid with columns='2' or columns='3' for side-by-side layouts.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-chat",
"version": "0.1.4",
"version": "0.1.6",
"type": "module",
"private": true,
"scripts": {
+18
View File
@@ -1,5 +1,23 @@
# example-dashboard
## 0.1.6
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/codegen@0.12.1
- @json-render/react@0.12.1
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/codegen@0.12.0
- @json-render/react@0.12.0
## 0.1.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-dashboard",
"version": "0.1.4",
"version": "0.1.6",
"type": "module",
"private": true,
"scripts": {
+16
View File
@@ -1,5 +1,21 @@
# example-image
## 0.1.3
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/image@0.12.1
## 0.1.2
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/image@0.12.0
## 0.1.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-image",
"version": "0.1.1",
"version": "0.1.3",
"type": "module",
"private": true,
"scripts": {
+21
View File
@@ -0,0 +1,21 @@
# example-mcp
## 0.1.2
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/mcp@0.12.1
- @json-render/react@0.12.1
- @json-render/shadcn@0.12.1
## 0.1.1
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/mcp@0.12.0
- @json-render/react@0.12.0
- @json-render/shadcn@0.12.0
+63
View File
@@ -0,0 +1,63 @@
# MCP App Example
A json-render MCP App that serves interactive shadcn/ui-based UIs directly inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.
## Setup
```bash
pnpm install
pnpm build
```
## Usage
### With Cursor
Add to `.cursor/mcp.json`:
```json
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
}
}
}
```
### With Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/examples/mcp/server.ts", "--stdio"]
}
}
}
```
### HTTP Transport
```bash
pnpm start
# Server listens on http://localhost:3001/mcp
```
### Stdio Transport
```bash
pnpm start:stdio
```
## How It Works
1. The Vite build bundles the React app (with shadcn components and `useJsonRenderApp` hook) into a single self-contained HTML file
2. The MCP server registers a `render-ui` tool with the catalog prompt as its description
3. When the LLM calls the tool, it generates a json-render spec constrained to the catalog
4. The host renders the bundled HTML in a sandboxed iframe
5. The iframe receives the spec via the MCP Apps protocol and renders it with json-render
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; img-src * data: blob:; font-src * data:; connect-src *; media-src *;" />
<title>json-render MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+34
View File
@@ -0,0 +1,34 @@
{
"name": "example-mcp",
"version": "0.1.2",
"type": "module",
"private": true,
"scripts": {
"build": "vite build",
"start": "tsx server.ts",
"start:stdio": "tsx server.ts --stdio",
"dev": "tsx watch server.ts"
},
"dependencies": {
"@json-render/core": "workspace:*",
"@json-render/mcp": "workspace:*",
"@json-render/react": "workspace:*",
"@json-render/shadcn": "workspace:*",
"@modelcontextprotocol/ext-apps": "^1.2.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/react": "19.2.3",
"@types/react-dom": "19.2.3",
"@tailwindcss/vite": "^4.2.1",
"@vitejs/plugin-react": "^5.1.4",
"tailwindcss": "^4.2.1",
"tsx": "^4.21.0",
"typescript": "^5.7.2",
"vite": "^6.3.5",
"vite-plugin-singlefile": "^2.3.0"
}
}
+86
View File
@@ -0,0 +1,86 @@
import { createMcpApp } from "@json-render/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { catalog } from "./src/catalog.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function loadHtml(): string {
const htmlPath = path.join(__dirname, "dist", "index.html");
if (!fs.existsSync(htmlPath)) {
throw new Error(
`Built HTML not found at ${htmlPath}. Run 'pnpm build' first.`,
);
}
return fs.readFileSync(htmlPath, "utf-8");
}
async function startStdio() {
const html = loadHtml();
const server = await createMcpApp({
name: "json-render Example",
version: "1.0.0",
catalog,
html,
});
await server.connect(new StdioServerTransport());
}
async function startHttp() {
const html = loadHtml();
const port = parseInt(process.env.PORT ?? "3001", 10);
const expressApp = createMcpExpressApp({ host: "0.0.0.0" });
expressApp.all("/mcp", async (req, res) => {
const server = await createMcpApp({
name: "json-render Example",
version: "1.0.0",
catalog,
html,
});
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on("close", () => {
transport.close().catch(() => {});
server.close().catch(() => {});
});
try {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("MCP error:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
}
}
});
expressApp.listen(port, () => {
console.log(`MCP server listening on http://localhost:${port}/mcp`);
});
}
if (process.argv.includes("--stdio")) {
startStdio().catch((e) => {
console.error(e);
process.exit(1);
});
} else {
startHttp().catch((e) => {
console.error(e);
process.exit(1);
});
}
+10
View File
@@ -0,0 +1,10 @@
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
export const catalog = defineCatalog(schema, {
components: {
...shadcnComponentDefinitions,
},
actions: {},
});
+106
View File
@@ -0,0 +1,106 @@
@import "tailwindcss";
@source "../../../packages/shadcn/src";
@source "../../../packages/react/src";
@custom-variant dark (&:is([data-theme="dark"] *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
[data-theme="dark"] {
--background: var(--color-background-secondary, var(--vscode-editor-background, oklch(0.145 0 0)));
--foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--card: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--card-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--popover: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--popover-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--primary: var(--color-text-primary, var(--vscode-foreground, oklch(0.922 0 0)));
--primary-foreground: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--secondary: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--secondary-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--muted: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--muted-foreground: var(--color-text-secondary, var(--vscode-descriptionForeground, oklch(0.708 0 0)));
--accent: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--accent-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--destructive: var(--color-text-danger, var(--vscode-errorForeground, oklch(0.704 0.191 22.216)));
--border: var(--color-border-primary, var(--vscode-widget-border, oklch(1 0 0 / 10%)));
--input: var(--color-border-secondary, var(--vscode-editorWidget-border, oklch(1 0 0 / 15%)));
--ring: var(--color-ring-primary, var(--vscode-focusBorder, oklch(0.556 0 0)));
}
* {
box-sizing: border-box;
border-color: var(--color-border);
}
html, body, #root {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
background-color: var(--color-background);
color: var(--color-foreground);
font-family: var(--vscode-font-family, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply text-foreground;
background-color: var(--color-background) !important;
}
}
button {
cursor: pointer;
}
+269
View File
@@ -0,0 +1,269 @@
import "./globals.css";
import {
Component,
useState,
useEffect,
useCallback,
type ReactNode,
} from "react";
import { createRoot } from "react-dom/client";
import { App as McpApp } from "@modelcontextprotocol/ext-apps";
import { JSONUIProvider, Renderer, defineRegistry } from "@json-render/react";
import { shadcnComponents } from "@json-render/shadcn";
import type { Spec } from "@json-render/core";
import { catalog } from "./catalog";
const { registry } = defineRegistry(catalog, {
components: {
...shadcnComponents,
Avatar: ({ props }: { props: Record<string, unknown> }) => {
const name = (props.name as string) || (props.alt as string) || "?";
const src = props.src as string | undefined;
const initials = name
.split(" ")
.map((n) => n[0])
.join("")
.slice(0, 2)
.toUpperCase();
const size = props.size === "lg" ? 48 : props.size === "sm" ? 32 : 40;
const [imgFailed, setImgFailed] = useState(false);
const onError = useCallback(() => setImgFailed(true), []);
const showImg = src && !imgFailed;
return (
<div
style={{
width: size,
height: size,
borderRadius: "50%",
overflow: "hidden",
backgroundColor: "var(--color-background-tertiary, #27272a)",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
{showImg ? (
<img
src={src}
alt={name}
referrerPolicy="no-referrer"
crossOrigin="anonymous"
width={size}
height={size}
onError={onError}
style={{ objectFit: "cover", width: size, height: size }}
/>
) : (
<span
style={{
fontSize: size * 0.4,
color: "var(--color-text-secondary, #a1a1aa)",
}}
>
{initials}
</span>
)}
</div>
);
},
},
});
class ErrorBoundary extends Component<
{ children: ReactNode },
{ error: Error | null }
> {
state = { error: null as Error | null };
static getDerivedStateFromError(error: Error) {
return { error };
}
render() {
if (this.state.error) {
return (
<div
style={{
padding: 16,
color: "#dc2626",
fontFamily: "monospace",
fontSize: 13,
}}
>
Error: {this.state.error.message}
</div>
);
}
return this.props.children;
}
}
function forceFullWidth(spec: Spec): Spec {
if (!spec.elements) return spec;
const elements = { ...spec.elements };
for (const [key, el] of Object.entries(elements)) {
if (el.type === "Card" && el.props) {
elements[key] = {
...el,
props: { ...el.props, maxWidth: "full", centered: false },
};
}
}
return { ...spec, elements };
}
function parseSpec(
result: { content?: Array<{ type: string; text?: string }> } | undefined,
): Spec | null {
const text = result?.content?.find((c) => c.type === "text")?.text;
if (!text) return null;
try {
return forceFullWidth(JSON.parse(text) as Spec);
} catch {
return null;
}
}
function applyHostContext(ctx: {
theme?: string;
styles?: { variables?: Record<string, string> };
}) {
if (ctx.theme) {
document.documentElement.setAttribute("data-theme", ctx.theme);
document.documentElement.style.colorScheme = ctx.theme;
}
if (ctx.styles?.variables) {
const root = document.documentElement;
for (const [key, value] of Object.entries(ctx.styles.variables)) {
root.style.setProperty(key, value);
}
}
}
function McpAppView() {
const [spec, setSpec] = useState<Spec | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let specReceived = false;
function handleSpec(s: Spec) {
if (specReceived) return;
specReceived = true;
setSpec(s);
}
function onMessage(event: MessageEvent) {
if (specReceived) return;
const data = event.data as Record<string, unknown> | undefined;
if (!data || typeof data !== "object") return;
const method = data.method as string | undefined;
const params = data.params as Record<string, unknown> | undefined;
// Cursor sends the spec via tool-input
if (method === "ui/notifications/tool-input" && params?.arguments) {
const args = params.arguments as Record<string, unknown>;
const rawSpec = args.spec;
if (
rawSpec &&
typeof rawSpec === "object" &&
"root" in rawSpec &&
"elements" in rawSpec
) {
handleSpec(forceFullWidth(rawSpec as Spec));
}
}
}
window.addEventListener("message", onMessage);
const app = new McpApp({ name: "json-render", version: "1.0.0" });
app.ontoolresult = (result) => {
const parsed = parseSpec(
result as { content?: Array<{ type: string; text?: string }> },
);
if (parsed) handleSpec(parsed);
};
app.onhostcontextchanged = (ctx) =>
applyHostContext(ctx as Parameters<typeof applyHostContext>[0]);
app.onerror = (err: unknown) => {
setError(err instanceof Error ? err.message : String(err));
};
app
.connect()
.then(() => {
const ctx = app.getHostContext?.();
if (ctx)
applyHostContext(ctx as Parameters<typeof applyHostContext>[0]);
// Fallback: if host didn't provide theme, detect via media query
if (!ctx || !(ctx as Record<string, unknown>).theme) {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
document.documentElement.setAttribute(
"data-theme",
prefersDark ? "dark" : "light",
);
document.documentElement.style.colorScheme = prefersDark
? "dark"
: "light";
}
})
.catch((err: unknown) =>
setError(err instanceof Error ? err.message : String(err)),
);
return () => {
window.removeEventListener("message", onMessage);
app.close().catch(() => {});
};
}, []);
if (error) {
return (
<div
style={{
padding: 16,
color: "#dc2626",
fontFamily: "monospace",
fontSize: 13,
}}
>
{error}
</div>
);
}
if (!spec) {
return (
<div
style={{
padding: 16,
color: "#6b7280",
fontFamily: "sans-serif",
fontSize: 14,
}}
>
Loading...
</div>
);
}
return (
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
<div className="w-full">
<Renderer spec={spec} registry={registry} />
</div>
</JSONUIProvider>
);
}
createRoot(document.getElementById("root")!).render(
<ErrorBoundary>
<McpAppView />
</ErrorBoundary>,
);
+89
View File
@@ -0,0 +1,89 @@
import { JSONUIProvider, Renderer } from "@json-render/react";
import { shadcnComponents } from "@json-render/shadcn";
import { defineRegistry } from "@json-render/react";
import { useJsonRenderApp } from "@json-render/mcp/app";
import { catalog } from "./catalog";
import { useState, useEffect } from "react";
const { registry } = defineRegistry(catalog, {
components: {
...shadcnComponents,
},
});
const debugStyle = {
padding: 12,
fontFamily: "monospace",
fontSize: 12,
color: "#000",
background: "#fffbe6",
border: "1px solid #faad14",
borderRadius: 4,
margin: 8,
whiteSpace: "pre-wrap" as const,
wordBreak: "break-all" as const,
};
export function McpAppView() {
const [logs, setLogs] = useState<string[]>(["App mounted"]);
const addLog = (msg: string) => {
setLogs((prev) => [
...prev,
`${new Date().toISOString().slice(11, 23)} ${msg}`,
]);
};
const { spec, loading, connected, connecting, error } = useJsonRenderApp({
name: "json-render-mcp-example",
version: "1.0.0",
});
useEffect(() => {
addLog(
`State: connecting=${connecting} connected=${connected} error=${error?.message ?? "none"} loading=${loading} spec=${spec ? "yes" : "null"}`,
);
}, [connecting, connected, error, loading, spec]);
useEffect(() => {
if (spec) {
addLog(
`Spec received: root=${spec.root}, elements=${Object.keys(spec.elements ?? {}).join(",")}`,
);
}
}, [spec]);
if (error) {
return (
<div style={debugStyle}>
<div style={{ color: "#ef4444", fontWeight: "bold" }}>
Connection error: {error.message}
</div>
<div style={{ marginTop: 8 }}>{logs.join("\n")}</div>
</div>
);
}
if (!spec) {
return (
<div style={debugStyle}>
<div>
{connecting
? "Connecting to host..."
: loading
? "Waiting for UI spec..."
: "No spec received."}
</div>
<div style={{ marginTop: 8 }}>{logs.join("\n")}</div>
</div>
);
}
return (
<div>
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
<Renderer spec={spec} registry={registry} loading={loading} />
</JSONUIProvider>
</div>
);
}
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"jsx": "react-jsx"
},
"include": ["src", "server.ts"]
}
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss(), viteSingleFile()],
resolve: {
dedupe: ["react", "react-dom"],
},
build: {
outDir: "dist",
emptyOutDir: false,
rollupOptions: {
input: "index.html",
},
},
});
+18
View File
@@ -1,5 +1,23 @@
# example-no-ai
## 0.1.6
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/react@0.12.1
- @json-render/shadcn@0.12.1
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react@0.12.0
- @json-render/shadcn@0.12.0
## 0.1.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-no-ai",
"version": "0.1.4",
"version": "0.1.6",
"type": "module",
"private": true,
"scripts": {
+17
View File
@@ -0,0 +1,17 @@
# example-react-email
## 0.1.2
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/react-email@0.12.1
## 0.1.1
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react-email@0.12.0
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-email",
"version": "0.1.0",
"version": "0.1.2",
"type": "module",
"private": true,
"scripts": {
@@ -25,7 +25,7 @@
"react-resizable-panels": "^4.4.1",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.18",
"zod": "4.3.5"
"zod": "4.3.6"
},
"devDependencies": {
"@internal/typescript-config": "workspace:*",
+16
View File
@@ -1,5 +1,21 @@
# example-react-native
## 0.1.6
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/react-native@0.12.1
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react-native@0.12.0
## 0.1.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-native",
"version": "0.1.4",
"version": "0.1.6",
"private": true,
"main": "expo-router/entry",
"scripts": {
+16
View File
@@ -1,5 +1,21 @@
# example-react-pdf
## 0.1.6
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/react-pdf@0.12.1
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react-pdf@0.12.0
## 0.1.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-pdf",
"version": "0.1.4",
"version": "0.1.6",
"type": "module",
"private": true,
"scripts": {
+16
View File
@@ -1,5 +1,21 @@
# example-remotion
## 0.1.6
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/remotion@0.12.1
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/remotion@0.12.0
## 0.1.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-remotion",
"version": "0.1.4",
"version": "0.1.6",
"type": "module",
"private": true,
"scripts": {
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>json-render solid example</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
{
"name": "example-solid",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "portless solid-demo.json-render vite",
"build": "vite build",
"preview": "vite preview",
"check-types": "tsc --noEmit"
},
"dependencies": {
"@json-render/core": "workspace:*",
"@json-render/solid": "workspace:*",
"solid-js": "^1.9.11",
"zod": "^4.3.6"
},
"devDependencies": {
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-solid": "^2.11.10"
}
}
+13
View File
@@ -0,0 +1,13 @@
import { StateProvider } from "@json-render/solid";
import { demoSpec } from "./lib/spec";
import { DemoRenderer } from "./DemoRenderer";
export function App() {
const initialState = demoSpec.state ?? {};
return (
<StateProvider initialState={initialState}>
<DemoRenderer />
</StateProvider>
);
}
+51
View File
@@ -0,0 +1,51 @@
import {
ActionProvider,
ValidationProvider,
VisibilityProvider,
Renderer,
useStateStore,
} from "@json-render/solid";
import { demoSpec } from "./lib/spec";
import { registry } from "./lib/registry";
export function DemoRenderer() {
const stateStore = useStateStore();
const handlers = {
increment: async () => {
stateStore.set("/count", Number(stateStore.get("/count") || 0) + 1);
},
decrement: async () => {
stateStore.set(
"/count",
Math.max(0, Number(stateStore.get("/count") || 0) - 1),
);
},
reset: async () => {
stateStore.set("/count", 0);
},
toggleItem: async (params: Record<string, unknown>) => {
const index = params.index as number;
const todos = (
stateStore.get("/todos") as Array<{
id: number;
title: string;
completed: boolean;
}>
).map((item, i) =>
i === index ? { ...item, completed: !item.completed } : item,
);
stateStore.set("/todos", todos);
},
};
return (
<ActionProvider handlers={handlers}>
<VisibilityProvider>
<ValidationProvider>
<Renderer spec={demoSpec} registry={registry} />
</ValidationProvider>
</VisibilityProvider>
</ActionProvider>
);
}
+13
View File
@@ -0,0 +1,13 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f9fafb;
color: #111827;
min-height: 100vh;
}
+5
View File
@@ -0,0 +1,5 @@
import { render } from "solid-js/web";
import { App } from "./App";
import "./app.css";
render(() => <App />, document.getElementById("app")!);
+90
View File
@@ -0,0 +1,90 @@
import { schema } from "@json-render/solid/schema";
import { z } from "zod";
export const catalog = schema.createCatalog({
components: {
Stack: {
props: z.object({
gap: z.number().optional(),
padding: z.number().optional(),
direction: z.enum(["vertical", "horizontal"]).optional(),
align: z.enum(["start", "center", "end"]).optional(),
}),
slots: ["default"],
description:
"Layout container that stacks children vertically or horizontally",
},
Card: {
props: z.object({
title: z.string().optional(),
subtitle: z.string().optional(),
}),
slots: ["default"],
description: "A card container with optional title and subtitle",
},
Text: {
props: z.object({
content: z.string(),
size: z.enum(["sm", "md", "lg", "xl"]).optional(),
weight: z.enum(["normal", "medium", "bold"]).optional(),
color: z.string().optional(),
}),
slots: [],
description: "Displays a text string",
},
Button: {
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary", "danger"]).optional(),
disabled: z.boolean().optional(),
}),
slots: [],
description: "A clickable button that emits a 'press' event",
},
Badge: {
props: z.object({
label: z.string(),
color: z.string().optional(),
}),
slots: [],
description: "A small badge/tag label",
},
ListItem: {
props: z.object({
title: z.string(),
description: z.string().optional(),
completed: z.boolean().optional(),
}),
slots: [],
description: "A single item in a list",
},
Input: {
props: z.object({
value: z.string().optional(),
placeholder: z.string().optional(),
}),
slots: [],
description: "A text input field that supports two-way state binding",
},
},
actions: {
increment: {
params: z.object({}),
description: "Increment the counter by 1",
},
decrement: {
params: z.object({}),
description: "Decrement the counter by 1",
},
reset: {
params: z.object({}),
description: "Reset the counter to 0",
},
toggleItem: {
params: z.object({
index: z.number(),
}),
description: "Toggle the completed state of a todo item",
},
},
});
@@ -0,0 +1,27 @@
import type { BaseComponentProps } from "@json-render/solid";
interface BadgeProps {
label: string;
color?: string;
}
export function Badge(renderProps: BaseComponentProps<BadgeProps>) {
const color = () => renderProps.props.color ?? "#6b7280";
return (
<span
style={{
display: "inline-block",
padding: "4px 12px",
"border-radius": "9999px",
"font-size": "13px",
"font-weight": "500",
"background-color": `${color()}20`,
color: color(),
border: `1px solid ${color()}40`,
}}
>
{renderProps.props.label}
</span>
);
}
@@ -0,0 +1,47 @@
import type { BaseComponentProps } from "@json-render/solid";
interface ButtonProps {
label: string;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
}
const variantStyles: Record<string, Record<string, string>> = {
primary: {
background: "#2563eb",
color: "#ffffff",
border: "1px solid #2563eb",
},
secondary: {
background: "#f3f4f6",
color: "#374151",
border: "1px solid #d1d5db",
},
danger: {
background: "#fee2e2",
color: "#dc2626",
border: "1px solid #fca5a5",
},
};
export function Button(renderProps: BaseComponentProps<ButtonProps>) {
const variant = () => renderProps.props.variant ?? "primary";
return (
<button
disabled={renderProps.props.disabled}
onClick={() => renderProps.emit("press")}
style={{
padding: "8px 16px",
"border-radius": "8px",
"font-size": "14px",
"font-weight": "500",
cursor: renderProps.props.disabled ? "not-allowed" : "pointer",
opacity: renderProps.props.disabled ? "0.5" : "1",
...variantStyles[variant()],
}}
>
{renderProps.props.label}
</button>
);
}
@@ -0,0 +1,44 @@
import type { BaseComponentProps } from "@json-render/solid";
interface CardProps {
title?: string;
subtitle?: string;
}
export function Card(renderProps: BaseComponentProps<CardProps>) {
return (
<div
style={{
background: "#ffffff",
"border-radius": "12px",
border: "1px solid #e5e7eb",
padding: "20px",
"box-shadow": "0 1px 3px rgba(0,0,0,0.06)",
}}
>
{renderProps.props.title && (
<h2
style={{
"font-size": "18px",
"font-weight": "600",
"margin-bottom": renderProps.props.subtitle ? "4px" : "16px",
}}
>
{renderProps.props.title}
</h2>
)}
{renderProps.props.subtitle && (
<p
style={{
"font-size": "14px",
color: "#6b7280",
"margin-bottom": "16px",
}}
>
{renderProps.props.subtitle}
</p>
)}
{renderProps.children}
</div>
);
}
@@ -0,0 +1,30 @@
import type { BaseComponentProps } from "@json-render/solid";
import { useBoundProp } from "@json-render/solid";
interface InputProps {
value?: string;
placeholder?: string;
}
export function Input(renderProps: BaseComponentProps<InputProps>) {
const [value, setValue] = useBoundProp<string>(
renderProps.props.value,
renderProps.bindings?.value,
);
return (
<input
value={String(value ?? "")}
onInput={(e) => setValue(e.currentTarget.value)}
placeholder={renderProps.props.placeholder ?? ""}
style={{
padding: "8px 12px",
"border-radius": "8px",
border: "1px solid #d1d5db",
"font-size": "14px",
outline: "none",
width: "100%",
}}
/>
);
}
@@ -0,0 +1,55 @@
import type { BaseComponentProps } from "@json-render/solid";
interface ListItemProps {
title: string;
description?: string;
completed?: boolean;
}
export function ListItem(renderProps: BaseComponentProps<ListItemProps>) {
return (
<div
onClick={() => renderProps.emit("press")}
style={{
display: "flex",
"align-items": "center",
gap: "10px",
padding: "10px 12px",
"border-radius": "8px",
border: "1px solid #e5e7eb",
cursor: "pointer",
"user-select": "none",
background: renderProps.props.completed ? "#f0fdf4" : "#ffffff",
}}
>
<span
style={{
width: "22px",
height: "22px",
"border-radius": "6px",
border: renderProps.props.completed
? "2px solid #22c55e"
: "2px solid #d1d5db",
display: "flex",
"align-items": "center",
"justify-content": "center",
"font-size": "13px",
color: "#22c55e",
"flex-shrink": "0",
}}
>
{renderProps.props.completed ? "\u2713" : ""}
</span>
<span
style={{
"text-decoration": renderProps.props.completed
? "line-through"
: "none",
color: renderProps.props.completed ? "#9ca3af" : "#111827",
}}
>
{renderProps.props.title}
</span>
</div>
);
}
@@ -0,0 +1,27 @@
import type { BaseComponentProps } from "@json-render/solid";
interface StackProps {
gap?: number;
padding?: number;
direction?: "vertical" | "horizontal";
align?: "start" | "center" | "end";
}
export function Stack(renderProps: BaseComponentProps<StackProps>) {
return (
<div
style={{
display: "flex",
"flex-direction":
renderProps.props.direction === "horizontal" ? "row" : "column",
gap: renderProps.props.gap ? `${renderProps.props.gap}px` : undefined,
padding: renderProps.props.padding
? `${renderProps.props.padding}px`
: undefined,
"align-items": renderProps.props.align,
}}
>
{renderProps.children}
</div>
);
}
@@ -0,0 +1,35 @@
import type { BaseComponentProps } from "@json-render/solid";
interface TextProps {
content: string;
size?: "sm" | "md" | "lg" | "xl";
weight?: "normal" | "medium" | "bold";
color?: string;
}
const sizeMap: Record<string, string> = {
sm: "12px",
md: "14px",
lg: "18px",
xl: "24px",
};
const weightMap: Record<string, string> = {
normal: "400",
medium: "500",
bold: "700",
};
export function Text(renderProps: BaseComponentProps<TextProps>) {
return (
<span
style={{
"font-size": sizeMap[renderProps.props.size ?? "md"],
"font-weight": weightMap[renderProps.props.weight ?? "normal"],
color: renderProps.props.color,
}}
>
{String(renderProps.props.content ?? "")}
</span>
);
}
+29
View File
@@ -0,0 +1,29 @@
import { type Components, defineRegistry } from "@json-render/solid";
import { catalog } from "./catalog";
import { Stack } from "./components/Stack";
import { Card } from "./components/Card";
import { Text } from "./components/Text";
import { Button } from "./components/Button";
import { Badge } from "./components/Badge";
import { ListItem } from "./components/ListItem";
import { Input } from "./components/Input";
const components: Components<typeof catalog> = {
Stack,
Card,
Text,
Button,
Badge,
ListItem,
Input,
};
export const { registry, handlers: makeHandlers } = defineRegistry(catalog, {
components,
actions: {
increment: async () => {},
decrement: async () => {},
reset: async () => {},
toggleItem: async () => {},
},
});
+130
View File
@@ -0,0 +1,130 @@
import type { Spec } from "@json-render/core";
export const demoSpec: Spec = {
root: "root",
state: {
count: 0,
name: "",
todos: [
{ id: 1, title: "Learn SolidJS", completed: true },
{ id: 2, title: "Try @json-render/solid", completed: false },
{ id: 3, title: "Build something awesome", completed: false },
],
},
elements: {
root: {
type: "Stack",
props: { gap: 24, padding: 24, direction: "vertical" },
children: [
"header",
"counter-card",
"milestone-badge",
"todos-card",
"input-card",
],
},
header: {
type: "Text",
props: {
content: "@json-render/solid demo",
size: "xl",
weight: "bold",
},
},
"counter-card": {
type: "Card",
props: {
title: "Counter",
subtitle: "Click the buttons to change the count",
},
children: ["counter-body"],
},
"counter-body": {
type: "Stack",
props: { gap: 12, direction: "horizontal", align: "center" },
children: [
"decrement-btn",
"counter-value",
"increment-btn",
"reset-btn",
],
},
"decrement-btn": {
type: "Button",
props: { label: "\u2212", variant: "secondary" },
on: { press: { action: "decrement" } },
},
"counter-value": {
type: "Text",
props: {
content: { $state: "/count" },
size: "xl",
weight: "bold",
},
},
"increment-btn": {
type: "Button",
props: { label: "+", variant: "primary" },
on: { press: { action: "increment" } },
},
"reset-btn": {
type: "Button",
props: { label: "Reset", variant: "danger" },
on: { press: { action: "reset" } },
},
"milestone-badge": {
type: "Badge",
props: { label: "Milestone reached: 10!", color: "#10b981" },
visible: { $state: "/count", gte: 10 },
},
"todos-card": {
type: "Card",
props: { title: "Todo List", subtitle: "Your tasks" },
children: ["todos-list"],
},
"todos-list": {
type: "Stack",
props: { gap: 8, direction: "vertical" },
repeat: { statePath: "/todos", key: "id" },
children: ["todo-item"],
},
"todo-item": {
type: "ListItem",
props: {
title: { $item: "title" },
completed: { $item: "completed" },
},
on: {
press: { action: "toggleItem", params: { index: { $index: true } } },
},
},
"input-card": {
type: "Card",
props: {
title: "Bound Input",
subtitle: "Type to update state and see reactive text",
},
children: ["input-body"],
},
"input-body": {
type: "Stack",
props: { gap: 12, direction: "vertical" },
children: ["name-input", "name-display"],
},
"name-input": {
type: "Input",
props: {
value: { $bindState: "/name" },
placeholder: "Enter your name...",
},
},
"name-display": {
type: "Text",
props: {
content: { $state: "/name" },
size: "md",
color: "#6b7280",
},
},
},
};
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"jsx": "preserve",
"jsxImportSource": "solid-js"
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";
export default defineConfig({
plugins: [solid()],
});
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -1,5 +1,21 @@
# com.example.json-render-demo
## 0.0.7
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/react@0.12.1
## 0.0.6
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react@0.12.0
## 0.0.5
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "com.example.json-render-demo",
"version": "0.0.5",
"version": "0.0.7",
"description": "Test",
"private": true,
"license": "~~proprietary~~",
@@ -1,5 +1,21 @@
# com.example.json-render-fullpage-demo
## 0.0.7
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/react@0.12.1
## 0.0.6
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react@0.12.0
## 0.0.5
### Patch Changes
@@ -1,6 +1,6 @@
{
"name": "com.example.json-render-fullpage-demo",
"version": "0.0.5",
"version": "0.0.7",
"description": "Full-page Stripe App example (alpha)",
"private": true,
"license": "~~proprietary~~",
+23
View File
@@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+17
View File
@@ -0,0 +1,17 @@
# svelte-chat
## 0.0.3
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/svelte@0.12.1
## 0.0.2
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/svelte@0.12.0
+42
View File
@@ -0,0 +1,42 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
npx sv create --template minimal --types ts --no-install svelte-chat
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"tailwind": {
"css": "src/app.css",
"baseColor": "zinc"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true,
"registry": "https://shadcn-svelte.com/registry"
}
+39
View File
@@ -0,0 +1,39 @@
{
"name": "svelte-chat",
"private": true,
"version": "0.0.3",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check-types": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@internationalized/date": "^3.10.0",
"@lucide/svelte": "^0.561.0",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/vite": "^4.0.0",
"bits-ui": "^2.14.4",
"svelte": "^5.49.2",
"svelte-check": "^4.3.6",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.0.0",
"typescript": "^5.9.3",
"vite": "^7.3.1"
},
"dependencies": {
"@ai-sdk/gateway": "^3.0.46",
"@ai-sdk/svelte": "^4.0.96",
"@json-render/core": "workspace:*",
"@json-render/svelte": "workspace:*",
"ai": "^6.0.86",
"clsx": "^2.1.1",
"lucide-svelte": "^0.500.0",
"tailwind-merge": "^3.2.0",
"zod": "4.3.6"
}
}
+125
View File
@@ -0,0 +1,125 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
button {
cursor: pointer;
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.animate-shimmer {
background: linear-gradient(
90deg,
currentColor 25%,
hsl(0 0% 64%) 50%,
currentColor 75%
);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: shimmer 2s ease-in-out infinite;
}
+13
View File
@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+92
View File
@@ -0,0 +1,92 @@
import { ToolLoopAgent, stepCountIs } from "ai";
import { createGateway } from "@ai-sdk/gateway";
import { explorerCatalog } from "./render/catalog";
import { getWeather } from "./tools/weather";
import { getGitHubRepo, getGitHubPullRequests } from "./tools/github";
import { getCryptoPrice, getCryptoPriceHistory } from "./tools/crypto";
import { getHackerNewsTop } from "./tools/hackernews";
import { webSearch } from "./tools/search";
import { env } from "$env/dynamic/private";
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
const AGENT_INSTRUCTIONS = `You are a knowledgeable assistant that helps users explore data and learn about any topic. You look up real-time information, build visual dashboards, and create rich educational content.
WORKFLOW:
1. Call the appropriate tools to gather relevant data. Use webSearch for general topics not covered by specialized tools.
2. Respond with a brief, conversational summary of what you found.
3. Then output the JSONL UI spec wrapped in a \`\`\`spec fence to render a rich visual experience.
RULES:
- Always call tools FIRST to get real data. Never make up data.
- Embed the fetched data directly in /state paths so components can reference it.
- Use Card components to group related information.
- NEVER nest a Card inside another Card. If you need sub-sections inside a Card, use Stack, Separator, Heading, or Accordion instead.
- Use Grid for multi-column layouts.
- Use Metric for key numeric values (temperature, stars, price, etc.).
- Use Table for lists of items (stories, forecasts, languages, etc.).
- Use BarChart or LineChart for numeric trends and time-series data.
- Use PieChart for compositional/proportional data (market share, breakdowns, distributions).
- Use Tabs when showing multiple categories of data side by side.
- Use Badge for status indicators.
- Use Callout for key facts, tips, warnings, or important takeaways.
- Use Accordion to organize detailed sections the user can expand for deeper reading.
- Use Timeline for historical events, processes, step-by-step explanations, or milestones.
- When teaching about a topic, combine multiple component types to create a rich, engaging experience.
DATA BINDING:
- The state model is the single source of truth. Put fetched data in /state, then reference it with { "$state": "/json/pointer" } in any prop.
- $state works on ANY prop at ANY nesting level. The renderer resolves expressions before components receive props.
- Scalar binding: "title": { "$state": "/quiz/title" }
- Array binding: "items": { "$state": "/quiz/questions" } (for Accordion, Timeline, etc.)
- For Table, BarChart, LineChart, and PieChart, use { "$state": "/path" } on the data prop to bind read-only data from state.
- Always emit /state patches BEFORE the elements that reference them, so data is available when the UI renders.
- Always use the { "$state": "/foo" } object syntax for data binding.
INTERACTIVITY:
- You can use visible, repeat, on.press, and $cond/$then/$else freely.
- visible: Conditionally show/hide elements based on state. e.g. "visible": { "$state": "/q1/answer", "eq": "a" }
- repeat: Iterate over state arrays. e.g. "repeat": { "statePath": "/items" }
- on.press: Trigger actions on button clicks. e.g. "on": { "press": { "action": "setState", "params": { "statePath": "/submitted", "value": true } } }
- $cond/$then/$else: Conditional prop values. e.g. { "$cond": { "$state": "/correct" }, "$then": "Correct!", "$else": "Try again" }
BUILT-IN ACTIONS (use with on.press):
- setState: Set a value at a state path. params: { statePath: "/foo", value: "bar" }
- pushState: Append to an array. params: { statePath: "/items", value: { ... } }
- removeState: Remove by index. params: { statePath: "/items", index: 0 }
INPUT COMPONENTS:
- RadioGroup: Renders radio buttons. Writes selected value to statePath automatically.
- SelectInput: Dropdown select. Writes selected value to statePath automatically.
- TextInput: Text input field. Writes entered value to statePath automatically.
- Button: Clickable button. Use on.press to trigger actions.
${explorerCatalog.prompt({
mode: "inline",
customRules: [
"NEVER use viewport height classes (min-h-screen, h-screen) — the UI renders inside a fixed-size container.",
"Prefer Grid with columns='2' or columns='3' for side-by-side layouts.",
"Use Metric components for key numbers instead of plain Text.",
"Put chart data arrays in /state and reference them with { $state: '/path' } on the data prop.",
"Keep the UI clean and information-dense — no excessive padding or empty space.",
"For educational prompts ('teach me about', 'explain', 'what is'), use a mix of Callout, Accordion, Timeline, and charts to make the content visually rich.",
],
})}`;
export const gateway = createGateway({ apiKey: env.AI_GATEWAY_API_KEY });
export const agent = new ToolLoopAgent({
model: gateway(env.AI_GATEWAY_MODEL || DEFAULT_MODEL),
instructions: AGENT_INSTRUCTIONS,
tools: {
getWeather,
getGitHubRepo,
getGitHubPullRequests,
getCryptoPrice,
getCryptoPriceHistory,
getHackerNewsTop,
webSearch,
},
stopWhen: stepCountIs(5),
temperature: 0.7,
});
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,22 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
</script>
<AccordionPrimitive.Content
bind:ref
data-slot="accordion-content"
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...restProps}
>
<div class={cn("pt-0 pb-4", className)}>
{@render children?.()}
</div>
</AccordionPrimitive.Content>
@@ -0,0 +1,17 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AccordionPrimitive.ItemProps = $props();
</script>
<AccordionPrimitive.Item
bind:ref
data-slot="accordion-item"
class={cn("border-b last:border-b-0", className)}
{...restProps}
/>
@@ -0,0 +1,32 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
level = 3,
children,
...restProps
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
level?: AccordionPrimitive.HeaderProps["level"];
} = $props();
</script>
<AccordionPrimitive.Header {level} class="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
bind:ref
class={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-start text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronDownIcon
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
@@ -0,0 +1,16 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
let {
ref = $bindable(null),
value = $bindable(),
...restProps
}: AccordionPrimitive.RootProps = $props();
</script>
<AccordionPrimitive.Root
bind:ref
bind:value={value as never}
data-slot="accordion"
{...restProps}
/>
@@ -0,0 +1,16 @@
import Root from "./accordion.svelte";
import Content from "./accordion-content.svelte";
import Item from "./accordion-item.svelte";
import Trigger from "./accordion-trigger.svelte";
export {
Root,
Content,
Item,
Trigger,
//
Root as Accordion,
Content as AccordionContent,
Item as AccordionItem,
Trigger as AccordionTrigger,
};
@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-description"
class={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-title"
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,44 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const alertVariants = tv({
base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
},
},
defaultVariants: {
variant: "default",
},
});
export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
</script>
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
variant?: AlertVariant;
} = $props();
</script>
<div
bind:this={ref}
data-slot="alert"
class={cn(alertVariants({ variant }), className)}
{...restProps}
role="alert"
>
{@render children?.()}
</div>
@@ -0,0 +1,14 @@
import Root from "./alert.svelte";
import Description from "./alert-description.svelte";
import Title from "./alert-title.svelte";
export { alertVariants, type AlertVariant } from "./alert.svelte";
export {
Root,
Description,
Title,
//
Root as Alert,
Description as AlertDescription,
Title as AlertTitle,
};
@@ -0,0 +1,50 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const badgeVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
variants: {
variant: {
default:
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
destructive:
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
});
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
</script>
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
href,
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
} = $props();
</script>
<svelte:element
this={href ? "a" : "span"}
bind:this={ref}
data-slot="badge"
{href}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</svelte:element>

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