mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 20:38:21 +08:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bb82604f0 | ||
|
|
caa90b8b84 | ||
|
|
54a1ecf817 | ||
|
|
dc9e9d17f3 |
@@ -19,7 +19,8 @@
|
||||
"@json-render/xstate",
|
||||
"@json-render/image",
|
||||
"@json-render/mcp",
|
||||
"@json-render/svelte"
|
||||
"@json-render/svelte",
|
||||
"@json-render/solid"
|
||||
]
|
||||
],
|
||||
"linked": [],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
.pnpm-store/
|
||||
|
||||
# Local env files
|
||||
.env*
|
||||
|
||||
@@ -21,6 +21,8 @@ npm install @json-render/core @json-render/react-email @react-email/components @
|
||||
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?
|
||||
@@ -30,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, Svelte (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
|
||||
@@ -91,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>
|
||||
),
|
||||
},
|
||||
});
|
||||
@@ -113,24 +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/svelte` | Svelte 5 renderer with runes-based reactivity |
|
||||
| `@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 |
|
||||
| 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
|
||||
|
||||
@@ -159,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)
|
||||
@@ -199,6 +200,26 @@ const { registry } = defineRegistry(catalog, {
|
||||
// <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
|
||||
@@ -229,7 +250,7 @@ const { registry } = defineRegistry(catalog, {
|
||||
},
|
||||
});
|
||||
|
||||
<Renderer spec={spec} registry={registry} />
|
||||
<Renderer spec={spec} registry={registry} />;
|
||||
```
|
||||
|
||||
### React Native (Mobile)
|
||||
@@ -250,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
|
||||
@@ -276,7 +314,7 @@ const spec = {
|
||||
fps={spec.composition.fps}
|
||||
compositionWidth={spec.composition.width}
|
||||
compositionHeight={spec.composition.height}
|
||||
/>
|
||||
/>;
|
||||
```
|
||||
|
||||
### React PDF (Documents)
|
||||
@@ -287,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" },
|
||||
@@ -301,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: [],
|
||||
},
|
||||
@@ -327,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",
|
||||
@@ -336,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: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -425,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -445,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"]
|
||||
}
|
||||
```
|
||||
@@ -459,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" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -486,7 +561,7 @@ pnpm dev
|
||||
- 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 + Svelte): 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
|
||||
@@ -496,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])
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# 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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<string, unknown></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.
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("docs/skills")
|
||||
export const metadata = pageMetadata("docs/skills");
|
||||
|
||||
# Skills
|
||||
|
||||
@@ -18,6 +18,7 @@ json-render ships with skills that teach AI coding agents how to use each packag
|
||||
- **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.
|
||||
@@ -38,6 +39,7 @@ 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
|
||||
@@ -88,6 +90,10 @@ Teaches agents how to render JSON specs as Vue 3 component trees. Covers the Vue
|
||||
|
||||
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.
|
||||
|
||||
@@ -16,8 +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/vue, @json-render/svelte, @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, codegen, mcp, redux, zustand, jotai, xstate. See /docs/skills for details.
|
||||
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.
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ function FormUI() {
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
Inline Mode
|
||||
</div>
|
||||
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-col">
|
||||
{/* Chat area */}
|
||||
@@ -69,11 +69,11 @@ function ChatModeDiagram() {
|
||||
);
|
||||
}
|
||||
|
||||
function GenerateModeDiagram() {
|
||||
function StandaloneModeDiagram() {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
|
||||
Generate Mode
|
||||
Standalone Mode
|
||||
</div>
|
||||
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-row">
|
||||
{/* Left panel - prompt */}
|
||||
@@ -104,10 +104,10 @@ export function GenerationModesDiagram() {
|
||||
<div className="not-prose my-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div className="h-[280px]">
|
||||
<ChatModeDiagram />
|
||||
<InlineModeDiagram />
|
||||
</div>
|
||||
<div className="h-[280px]">
|
||||
<GenerateModeDiagram />
|
||||
<StandaloneModeDiagram />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -92,7 +92,7 @@ export const docsNavigation: NavSection[] = [
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
title: "Renders with Vite (Vue / React / Svelte)",
|
||||
title: "Renders with Vite (Vue / React / Svelte / Solid)",
|
||||
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
|
||||
external: true,
|
||||
},
|
||||
@@ -133,6 +133,7 @@ export const docsNavigation: NavSection[] = [
|
||||
{ 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" },
|
||||
|
||||
@@ -43,6 +43,7 @@ export const PAGE_TITLES: Record<string, string> = {
|
||||
"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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# 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
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "example-chat",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-dashboard",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-image",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-mcp",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-no-ai",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-react-email",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-react-native",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"private": true,
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-react-pdf",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-remotion",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "com.example.json-render-demo",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"description": "Test",
|
||||
"private": true,
|
||||
"license": "~~proprietary~~",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "com.example.json-render-fullpage-demo",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"description": "Full-page Stripe App example (alpha)",
|
||||
"private": true,
|
||||
"license": "~~proprietary~~",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "svelte-chat",
|
||||
"private": true,
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -62,7 +62,7 @@ INPUT COMPONENTS:
|
||||
- Button: Clickable button. Use on.press to trigger actions.
|
||||
|
||||
${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,5 +1,13 @@
|
||||
# example-svelte
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
- @json-render/svelte@0.12.1
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-svelte",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# vite-renderers
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
- @json-render/react@0.12.1
|
||||
- @json-render/svelte@0.12.1
|
||||
- @json-render/vue@0.12.1
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vite-renderers",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -12,10 +12,12 @@
|
||||
"dependencies": {
|
||||
"@json-render/core": "workspace:*",
|
||||
"@json-render/react": "workspace:*",
|
||||
"@json-render/solid": "workspace:*",
|
||||
"@json-render/svelte": "workspace:*",
|
||||
"@json-render/vue": "workspace:*",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"solid-js": "^1.9.11",
|
||||
"svelte": "^5.49.2",
|
||||
"vue": "^3.5.29",
|
||||
"zod": "4.3.6"
|
||||
@@ -27,6 +29,7 @@
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-solid": "^2.11.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "./shared/styles.css";
|
||||
import { demoSpec } from "./spec";
|
||||
|
||||
type Renderer = "vue" | "react" | "svelte";
|
||||
type Renderer = "vue" | "react" | "svelte" | "solid";
|
||||
|
||||
const container = document.getElementById("renderer-root") as HTMLElement;
|
||||
|
||||
@@ -18,10 +18,14 @@ async function switchTo(renderer: Renderer) {
|
||||
const mod = await import("./react/mount.tsx");
|
||||
mod.mount(container, renderer, demoSpec);
|
||||
unmountCurrent = mod.unmount;
|
||||
} else {
|
||||
} else if (renderer === "svelte") {
|
||||
const mod = await import("./svelte/mount.ts");
|
||||
mod.mount(container, renderer, demoSpec);
|
||||
unmountCurrent = mod.unmount;
|
||||
} else {
|
||||
const mod = await import("./solid/mount.tsx");
|
||||
mod.mount(container, renderer, demoSpec);
|
||||
unmountCurrent = mod.unmount;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,9 @@ export const components: Components<AppCatalog> = {
|
||||
? "Rendered with Vue"
|
||||
: props.renderer === "react"
|
||||
? "Rendered with React"
|
||||
: "Rendered with Svelte"}
|
||||
: props.renderer === "svelte"
|
||||
? "Rendered with Svelte"
|
||||
: "Rendered with Solid"}
|
||||
</span>
|
||||
),
|
||||
|
||||
@@ -164,6 +166,17 @@ export const components: Components<AppCatalog> = {
|
||||
>
|
||||
Svelte
|
||||
</button>
|
||||
<button
|
||||
onClick={() => emit("pressSolid")}
|
||||
className={[
|
||||
"json-render-renderer-tab",
|
||||
props.renderer === "solid" && "json-render-renderer-tab--active",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
Solid
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -65,7 +65,7 @@ export const catalogDef = {
|
||||
props: z.object({ renderer: z.string() }),
|
||||
slots: [],
|
||||
description:
|
||||
"Segmented tab control for switching between Vue, React, and Svelte renderers",
|
||||
"Segmented tab control for switching between Vue, React, Svelte, and Solid renderers",
|
||||
},
|
||||
RendererBadge: {
|
||||
props: z.object({ renderer: z.string() }),
|
||||
@@ -99,5 +99,9 @@ export const catalogDef = {
|
||||
params: z.object({}),
|
||||
description: "Switch to the Svelte renderer",
|
||||
},
|
||||
switchToSolid: {
|
||||
params: z.object({}),
|
||||
description: "Switch to the Solid renderer",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ export const actionStubs = {
|
||||
switchToVue: async () => {},
|
||||
switchToReact: async () => {},
|
||||
switchToSvelte: async () => {},
|
||||
switchToSolid: async () => {},
|
||||
};
|
||||
|
||||
/** Creates action handlers that close over the state store's get/set */
|
||||
@@ -52,5 +53,10 @@ export function makeHandlers(get: Get, set: Set) {
|
||||
new CustomEvent("switch-renderer", { detail: "svelte" }),
|
||||
);
|
||||
},
|
||||
switchToSolid: async () => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("switch-renderer", { detail: "solid" }),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,9 +11,15 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.json-render-stack--align-start { align-items: flex-start; }
|
||||
.json-render-stack--align-center { align-items: center; }
|
||||
.json-render-stack--align-end { align-items: flex-end; }
|
||||
.json-render-stack--align-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.json-render-stack--align-center {
|
||||
align-items: center;
|
||||
}
|
||||
.json-render-stack--align-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
/* ---- Card ----------------------------------------------------------------- */
|
||||
|
||||
@@ -50,12 +56,22 @@
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.json-render-text--sm { font-size: 12px; }
|
||||
.json-render-text--lg { font-size: 16px; }
|
||||
.json-render-text--xl { font-size: 24px; }
|
||||
.json-render-text--sm {
|
||||
font-size: 12px;
|
||||
}
|
||||
.json-render-text--lg {
|
||||
font-size: 16px;
|
||||
}
|
||||
.json-render-text--xl {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.json-render-text--medium { font-weight: 500; }
|
||||
.json-render-text--bold { font-weight: 700; }
|
||||
.json-render-text--medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
.json-render-text--bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ---- Button --------------------------------------------------------------- */
|
||||
|
||||
@@ -211,7 +227,7 @@
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.json-render-renderer-tab:first-child {
|
||||
.json-render-renderer-tab:not(:last-child) {
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
@@ -261,3 +277,18 @@
|
||||
background-color: #ff3e00;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.renderer-solid .json-render-renderer-badge {
|
||||
color: #2c4f7c;
|
||||
background-color: #2c4f7c18;
|
||||
border-color: #2c4f7c40;
|
||||
}
|
||||
|
||||
.renderer-solid .json-render-renderer-dot {
|
||||
background-color: #2c4f7c;
|
||||
}
|
||||
|
||||
.renderer-solid .json-render-renderer-tab--active {
|
||||
background-color: #2c4f7c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Spec } from "@json-render/core";
|
||||
import { StateProvider } from "@json-render/solid";
|
||||
import { DemoRenderer } from "./DemoRenderer";
|
||||
|
||||
export function App(props: { initialRenderer?: string; spec: Spec }) {
|
||||
const renderer = props.initialRenderer ?? "solid";
|
||||
const initialState = {
|
||||
...props.spec.state,
|
||||
renderer,
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={`renderer-${renderer}`}>
|
||||
<StateProvider initialState={initialState}>
|
||||
<DemoRenderer spec={props.spec} />
|
||||
</StateProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Spec } from "@json-render/core";
|
||||
import {
|
||||
ActionProvider,
|
||||
ValidationProvider,
|
||||
VisibilityProvider,
|
||||
Renderer,
|
||||
defineRegistry,
|
||||
useStateStore,
|
||||
} from "@json-render/solid";
|
||||
import { catalog } from "./catalog";
|
||||
import { components } from "./registry";
|
||||
import { actionStubs, makeHandlers } from "../shared/handlers";
|
||||
|
||||
const { registry } = defineRegistry(catalog, {
|
||||
components,
|
||||
actions: actionStubs,
|
||||
});
|
||||
|
||||
export function DemoRenderer(props: { spec: Spec }) {
|
||||
const stateStore = useStateStore();
|
||||
const handlers = makeHandlers(stateStore.get, stateStore.set);
|
||||
|
||||
return (
|
||||
<ActionProvider handlers={handlers}>
|
||||
<VisibilityProvider>
|
||||
<ValidationProvider>
|
||||
<Renderer spec={props.spec} registry={registry} />
|
||||
</ValidationProvider>
|
||||
</VisibilityProvider>
|
||||
</ActionProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { schema } from "@json-render/solid/schema";
|
||||
import { catalogDef } from "../shared/catalog-def";
|
||||
|
||||
export const catalog = schema.createCatalog(catalogDef);
|
||||
export type AppCatalog = typeof catalog;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { render } from "solid-js/web";
|
||||
import type { Spec } from "@json-render/core";
|
||||
import { App } from "./App";
|
||||
|
||||
let dispose: (() => void) | null = null;
|
||||
|
||||
export function mount(container: HTMLElement, renderer: string, spec: Spec) {
|
||||
dispose = render(
|
||||
() => <App initialRenderer={renderer} spec={spec} />,
|
||||
container,
|
||||
);
|
||||
}
|
||||
|
||||
export function unmount() {
|
||||
dispose?.();
|
||||
dispose = null;
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import type { Components } from "@json-render/solid";
|
||||
import type { AppCatalog } from "./catalog";
|
||||
|
||||
export const components: Components<AppCatalog> = {
|
||||
Stack: (renderProps) => (
|
||||
<div
|
||||
class={[
|
||||
"json-render-stack",
|
||||
renderProps.props.direction === "horizontal" &&
|
||||
"json-render-stack--horizontal",
|
||||
renderProps.props.align &&
|
||||
`json-render-stack--align-${renderProps.props.align}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
style={{
|
||||
gap: renderProps.props.gap ? `${renderProps.props.gap}px` : undefined,
|
||||
padding: renderProps.props.padding
|
||||
? `${renderProps.props.padding}px`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{renderProps.children}
|
||||
</div>
|
||||
),
|
||||
|
||||
Card: (renderProps) => (
|
||||
<div class="json-render-card">
|
||||
{renderProps.props.title && (
|
||||
<div class="json-render-card-title-wrap">
|
||||
<h2 class="json-render-card-title">{renderProps.props.title}</h2>
|
||||
</div>
|
||||
)}
|
||||
{renderProps.props.subtitle && (
|
||||
<p class="json-render-card-subtitle">{renderProps.props.subtitle}</p>
|
||||
)}
|
||||
{renderProps.children}
|
||||
</div>
|
||||
),
|
||||
|
||||
Text: (renderProps) => (
|
||||
<span
|
||||
class={[
|
||||
"json-render-text",
|
||||
renderProps.props.size !== "md" &&
|
||||
renderProps.props.size &&
|
||||
`json-render-text--${renderProps.props.size}`,
|
||||
renderProps.props.weight !== "normal" &&
|
||||
renderProps.props.weight &&
|
||||
`json-render-text--${renderProps.props.weight}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
style={renderProps.props.color ? { color: renderProps.props.color } : {}}
|
||||
>
|
||||
{String(renderProps.props.content ?? "")}
|
||||
</span>
|
||||
),
|
||||
|
||||
Button: (renderProps) => (
|
||||
<button
|
||||
disabled={renderProps.props.disabled}
|
||||
onClick={() => renderProps.emit("press")}
|
||||
class={[
|
||||
"json-render-button",
|
||||
renderProps.props.variant &&
|
||||
`json-render-button--${renderProps.props.variant}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
{renderProps.props.label}
|
||||
</button>
|
||||
),
|
||||
|
||||
Badge: (renderProps) => (
|
||||
<span
|
||||
class="json-render-badge"
|
||||
style={
|
||||
renderProps.props.color
|
||||
? {
|
||||
"background-color": `${renderProps.props.color}20`,
|
||||
color: renderProps.props.color,
|
||||
"border-color": `${renderProps.props.color}40`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{renderProps.props.label}
|
||||
</span>
|
||||
),
|
||||
|
||||
ListItem: (renderProps) => (
|
||||
<div
|
||||
onClick={() => renderProps.emit("press")}
|
||||
class={[
|
||||
"json-render-list-item",
|
||||
renderProps.props.completed && "json-render-list-item--done",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
<div
|
||||
class={[
|
||||
"json-render-list-item-check",
|
||||
renderProps.props.completed && "json-render-list-item-check--done",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
{renderProps.props.completed ? "✓" : ""}
|
||||
</div>
|
||||
<span
|
||||
class={[
|
||||
"json-render-list-item-text",
|
||||
renderProps.props.completed && "json-render-list-item-text--done",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
{renderProps.props.title}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
|
||||
RendererBadge: (renderProps) => (
|
||||
<span class="json-render-renderer-badge">
|
||||
<span class="json-render-renderer-dot" />
|
||||
{renderProps.props.renderer === "vue"
|
||||
? "Rendered with Vue"
|
||||
: renderProps.props.renderer === "react"
|
||||
? "Rendered with React"
|
||||
: renderProps.props.renderer === "svelte"
|
||||
? "Rendered with Svelte"
|
||||
: "Rendered with Solid"}
|
||||
</span>
|
||||
),
|
||||
|
||||
RendererTabs: (renderProps) => (
|
||||
<div class="json-render-renderer-tabs-wrapper">
|
||||
<span class="json-render-renderer-tabs-label">Render</span>
|
||||
<div class="json-render-renderer-tabs">
|
||||
<button
|
||||
onClick={() => renderProps.emit("pressVue")}
|
||||
class={[
|
||||
"json-render-renderer-tab",
|
||||
renderProps.props.renderer === "vue" &&
|
||||
"json-render-renderer-tab--active",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
Vue
|
||||
</button>
|
||||
<button
|
||||
onClick={() => renderProps.emit("pressReact")}
|
||||
class={[
|
||||
"json-render-renderer-tab",
|
||||
renderProps.props.renderer === "react" &&
|
||||
"json-render-renderer-tab--active",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
React
|
||||
</button>
|
||||
<button
|
||||
onClick={() => renderProps.emit("pressSvelte")}
|
||||
class={[
|
||||
"json-render-renderer-tab",
|
||||
renderProps.props.renderer === "svelte" &&
|
||||
"json-render-renderer-tab--active",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
Svelte
|
||||
</button>
|
||||
<button
|
||||
onClick={() => renderProps.emit("pressSolid")}
|
||||
class={[
|
||||
"json-render-renderer-tab",
|
||||
renderProps.props.renderer === "solid" &&
|
||||
"json-render-renderer-tab--active",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
Solid
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -10,7 +10,7 @@ export const demoSpec: Spec = {
|
||||
{
|
||||
id: 2,
|
||||
title:
|
||||
"Try @json-render/vue, @json-render/react, and @json-render/svelte",
|
||||
"Try @json-render/vue, @json-render/react, @json-render/svelte, and @json-render/solid",
|
||||
completed: false,
|
||||
},
|
||||
{ id: 3, title: "Build something awesome", completed: false },
|
||||
@@ -49,6 +49,7 @@ export const demoSpec: Spec = {
|
||||
pressVue: { action: "switchToVue" },
|
||||
pressReact: { action: "switchToReact" },
|
||||
pressSvelte: { action: "switchToSvelte" },
|
||||
pressSolid: { action: "switchToSolid" },
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -12,5 +12,7 @@
|
||||
? "Rendered with Vue"
|
||||
: props.renderer === "react"
|
||||
? "Rendered with React"
|
||||
: "Rendered with Svelte"}
|
||||
: props.renderer === "svelte"
|
||||
? "Rendered with Svelte"
|
||||
: "Rendered with Solid"}
|
||||
</span>
|
||||
|
||||
@@ -42,5 +42,16 @@
|
||||
.join(" ")}>
|
||||
Svelte
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => emit("pressSolid")}
|
||||
class={[
|
||||
"json-render-renderer-tab",
|
||||
props.renderer === "solid" && "json-render-renderer-tab--active",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}>
|
||||
Solid
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -132,7 +132,9 @@ export const components: Components<AppCatalog> = {
|
||||
? "Rendered with Vue"
|
||||
: props.renderer === "react"
|
||||
? "Rendered with React"
|
||||
: "Rendered with Svelte",
|
||||
: props.renderer === "svelte"
|
||||
? "Rendered with Svelte"
|
||||
: "Rendered with Solid",
|
||||
]),
|
||||
|
||||
RendererTabs: ({ props, emit }) =>
|
||||
@@ -178,6 +180,19 @@ export const components: Components<AppCatalog> = {
|
||||
},
|
||||
"Svelte",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
onClick: () => emit("pressSolid"),
|
||||
class: [
|
||||
"json-render-renderer-tab",
|
||||
props.renderer === "solid" && "json-render-renderer-tab--active",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
},
|
||||
"Solid",
|
||||
),
|
||||
]),
|
||||
]),
|
||||
};
|
||||
|
||||
@@ -2,7 +2,13 @@ import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import solid from "vite-plugin-solid";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte(), vue(), react({ include: /\.tsx$/ })],
|
||||
plugins: [
|
||||
svelte(),
|
||||
vue(),
|
||||
react({ include: /src\/react\/.*\.tsx$/ }),
|
||||
solid({ include: [/src\/solid\/.*\.tsx$/, /packages\/solid\/.*\.tsx$/] }),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# example-vue
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
- @json-render/vue@0.12.1
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-vue",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
|
||||
|
||||
+11
-1
@@ -32,6 +32,7 @@
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@solidjs/testing-library": "^0.8.10",
|
||||
"@testing-library/svelte": "^5.2.0",
|
||||
"@types/react": "^19.2.3",
|
||||
"husky": "^9.1.7",
|
||||
@@ -40,9 +41,11 @@
|
||||
"prettier": "^3.7.4",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"solid-js": "^1.9.11",
|
||||
"svelte": "^5.0.0",
|
||||
"turbo": "^2.7.4",
|
||||
"typescript": "5.9.2",
|
||||
"vite-plugin-solid": "^2.11.10",
|
||||
"vitest": "^4.0.17"
|
||||
},
|
||||
"packageManager": "pnpm@10.29.3",
|
||||
@@ -51,5 +54,12 @@
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": "prettier --write"
|
||||
}
|
||||
},
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"examples/*",
|
||||
"examples/stripe-app/*",
|
||||
"packages/*",
|
||||
"tests/*"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/codegen
|
||||
|
||||
## 0.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/codegen",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Utilities for generating code from json-render UI trees",
|
||||
"keywords": [
|
||||
@@ -15,7 +15,7 @@
|
||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||
"directory": "packages/codegen"
|
||||
},
|
||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
||||
"homepage": "https://json-render.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# @json-render/core
|
||||
|
||||
## 0.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 54a1ecf: Rename generation modes and fix MCP React duplicate module error.
|
||||
|
||||
### Changed:
|
||||
- **`@json-render/core`** — Renamed generation modes from `"generate"` / `"chat"` to `"standalone"` / `"inline"`. The old names still work but emit a deprecation warning.
|
||||
|
||||
### Fixed:
|
||||
- **`@json-render/mcp`** — Resolved React duplicate module error (`useRef` returning null) by adding `resolve.dedupe` Vite configuration. Added `./build-app-html` export entry point.
|
||||
|
||||
### Other:
|
||||
- Updated `homepage` URLs across all packages to point to `https://json-render.dev`.
|
||||
- Reorganized skills directory structure for cleaner naming.
|
||||
- Added skills documentation page to the web app.
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -227,7 +227,7 @@ Schema options:
|
||||
| `ActionBinding` | Action binding with `action`, `params`, `confirm`, `preventDefault`, etc. |
|
||||
| `BuiltInAction` | Built-in action definition with `name` and `description` |
|
||||
|
||||
### Chat Mode (Mixed Streams)
|
||||
### Inline Mode (Mixed Streams)
|
||||
|
||||
| Export | Purpose |
|
||||
|--------|---------|
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/core",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"license": "Apache-2.0",
|
||||
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
|
||||
"keywords": [
|
||||
@@ -19,7 +19,7 @@
|
||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||
"directory": "packages/core"
|
||||
},
|
||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
||||
"homepage": "https://json-render.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||
},
|
||||
|
||||
@@ -257,7 +257,7 @@ describe("catalog.prompt", () => {
|
||||
expect(prompt).toContain("Keep UIs simple");
|
||||
});
|
||||
|
||||
it("generates chat mode prompt when mode is chat", () => {
|
||||
it("generates inline mode prompt when mode is inline", () => {
|
||||
const catalog = defineCatalog(testSchema, {
|
||||
components: {
|
||||
Text: {
|
||||
@@ -268,13 +268,13 @@ describe("catalog.prompt", () => {
|
||||
},
|
||||
actions: {},
|
||||
});
|
||||
const prompt = catalog.prompt({ mode: "chat" });
|
||||
const prompt = catalog.prompt({ mode: "inline" });
|
||||
expect(prompt).toContain("```spec");
|
||||
expect(prompt).toContain("conversationally");
|
||||
expect(prompt).toContain("text + JSONL");
|
||||
});
|
||||
|
||||
it("generates generate mode prompt by default", () => {
|
||||
it("generates standalone mode prompt by default", () => {
|
||||
const catalog = defineCatalog(testSchema, {
|
||||
components: {
|
||||
Text: {
|
||||
@@ -290,6 +290,38 @@ describe("catalog.prompt", () => {
|
||||
expect(prompt).not.toContain("conversationally");
|
||||
});
|
||||
|
||||
it("accepts deprecated 'chat' as alias for 'inline'", () => {
|
||||
const catalog = defineCatalog(testSchema, {
|
||||
components: {
|
||||
Text: {
|
||||
props: z.object({}),
|
||||
description: "",
|
||||
slots: [],
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
});
|
||||
const inlinePrompt = catalog.prompt({ mode: "inline" });
|
||||
const chatPrompt = catalog.prompt({ mode: "chat" });
|
||||
expect(chatPrompt).toEqual(inlinePrompt);
|
||||
});
|
||||
|
||||
it("accepts deprecated 'generate' as alias for 'standalone'", () => {
|
||||
const catalog = defineCatalog(testSchema, {
|
||||
components: {
|
||||
Text: {
|
||||
props: z.object({}),
|
||||
description: "",
|
||||
slots: [],
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
});
|
||||
const standalonePrompt = catalog.prompt({ mode: "standalone" });
|
||||
const generatePrompt = catalog.prompt({ mode: "generate" });
|
||||
expect(generatePrompt).toEqual(standalonePrompt);
|
||||
});
|
||||
|
||||
it("uses actual catalog component names in examples", () => {
|
||||
const catalog = defineCatalog(testSchema, {
|
||||
components: {
|
||||
|
||||
@@ -112,11 +112,14 @@ export interface PromptOptions {
|
||||
/**
|
||||
* Output mode for the generated prompt.
|
||||
*
|
||||
* - `"generate"` (default): The LLM should output only JSONL patches (no prose).
|
||||
* - `"chat"`: The LLM should respond conversationally first, then output JSONL patches.
|
||||
* - `"standalone"` (default): The LLM should output only JSONL patches (no prose).
|
||||
* - `"inline"`: The LLM should respond conversationally first, then output JSONL patches.
|
||||
* Includes rules about interleaving text with JSONL and not wrapping in code fences.
|
||||
*
|
||||
* @deprecated `"generate"` — use `"standalone"` instead.
|
||||
* @deprecated `"chat"` — use `"inline"` instead.
|
||||
*/
|
||||
mode?: "generate" | "chat";
|
||||
mode?: "standalone" | "inline" | "generate" | "chat";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -563,15 +566,28 @@ function generatePrompt<TDef extends SchemaDefinition, TCatalog>(
|
||||
const {
|
||||
system = "You are a UI generator that outputs JSON.",
|
||||
customRules = [],
|
||||
mode = "generate",
|
||||
mode: rawMode = "standalone",
|
||||
} = options;
|
||||
|
||||
const mode: "standalone" | "inline" =
|
||||
rawMode === "chat"
|
||||
? (console.warn(
|
||||
'[json-render] mode "chat" is deprecated, use "inline" instead',
|
||||
),
|
||||
"inline")
|
||||
: rawMode === "generate"
|
||||
? (console.warn(
|
||||
'[json-render] mode "generate" is deprecated, use "standalone" instead',
|
||||
),
|
||||
"standalone")
|
||||
: rawMode;
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(system);
|
||||
lines.push("");
|
||||
|
||||
// Output format section - explain JSONL streaming patch format
|
||||
if (mode === "chat") {
|
||||
if (mode === "inline") {
|
||||
lines.push("OUTPUT FORMAT (text + JSONL, RFC 6902 JSON Patch):");
|
||||
lines.push(
|
||||
"You respond conversationally. When generating UI, first write a brief explanation (1-3 sentences), then output JSONL patch lines wrapped in a ```spec code fence.",
|
||||
@@ -1014,7 +1030,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
||||
// Rules
|
||||
lines.push("RULES:");
|
||||
const baseRules =
|
||||
mode === "chat"
|
||||
mode === "inline"
|
||||
? [
|
||||
"When generating UI, wrap all JSONL patches in a ```spec code fence - one JSON object per line inside the fence",
|
||||
"Write a brief conversational response before any JSONL output",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/image
|
||||
|
||||
## 0.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/image",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.",
|
||||
"keywords": [
|
||||
@@ -20,7 +20,7 @@
|
||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||
"directory": "packages/image"
|
||||
},
|
||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
||||
"homepage": "https://json-render.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/jotai
|
||||
|
||||
## 0.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/jotai",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Jotai adapter for json-render StateStore",
|
||||
"keywords": [
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||
"directory": "packages/jotai"
|
||||
},
|
||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
||||
"homepage": "https://json-render.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# @json-render/mcp
|
||||
|
||||
## 0.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 54a1ecf: Rename generation modes and fix MCP React duplicate module error.
|
||||
|
||||
### Changed:
|
||||
- **`@json-render/core`** — Renamed generation modes from `"generate"` / `"chat"` to `"standalone"` / `"inline"`. The old names still work but emit a deprecation warning.
|
||||
|
||||
### Fixed:
|
||||
- **`@json-render/mcp`** — Resolved React duplicate module error (`useRef` returning null) by adding `resolve.dedupe` Vite configuration. Added `./build-app-html` export entry point.
|
||||
|
||||
### Other:
|
||||
- Updated `homepage` URLs across all packages to point to `https://json-render.dev`.
|
||||
- Reorganized skills directory structure for cleaner naming.
|
||||
- Added skills documentation page to the web app.
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/mcp",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"license": "Apache-2.0",
|
||||
"description": "MCP Apps integration for @json-render/core. Serve json-render UIs as interactive MCP Apps in Claude, ChatGPT, Cursor, and VS Code.",
|
||||
"keywords": [
|
||||
@@ -22,7 +22,7 @@
|
||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||
"directory": "packages/mcp"
|
||||
},
|
||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
||||
"homepage": "https://json-render.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/react-email
|
||||
|
||||
## 0.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/react-email",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"license": "Apache-2.0",
|
||||
"description": "React Email renderer for @json-render/core. JSON becomes HTML emails.",
|
||||
"keywords": [
|
||||
@@ -18,7 +18,7 @@
|
||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||
"directory": "packages/react-email"
|
||||
},
|
||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
||||
"homepage": "https://json-render.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/react-native
|
||||
|
||||
## 0.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/react-native",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"license": "Apache-2.0",
|
||||
"description": "React Native renderer for @json-render/core. JSON becomes React Native components.",
|
||||
"keywords": [
|
||||
@@ -19,7 +19,7 @@
|
||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||
"directory": "packages/react-native"
|
||||
},
|
||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
||||
"homepage": "https://json-render.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/react-pdf
|
||||
|
||||
## 0.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/react-pdf",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"license": "Apache-2.0",
|
||||
"description": "React PDF renderer for @json-render/core. JSON becomes PDF documents.",
|
||||
"keywords": [
|
||||
@@ -18,7 +18,7 @@
|
||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||
"directory": "packages/react-pdf"
|
||||
},
|
||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
||||
"homepage": "https://json-render.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @internal/react-state
|
||||
|
||||
## 0.8.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
|
||||
## 0.8.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@internal/react-state",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.6",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "Shared React state context for json-render renderer packages",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/react
|
||||
|
||||
## 0.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/react",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"license": "Apache-2.0",
|
||||
"description": "React renderer for @json-render/core. JSON becomes React components.",
|
||||
"keywords": [
|
||||
@@ -19,7 +19,7 @@
|
||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||
"directory": "packages/react"
|
||||
},
|
||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
||||
"homepage": "https://json-render.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/redux
|
||||
|
||||
## 0.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/redux",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Redux adapter for json-render StateStore",
|
||||
"keywords": [
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||
"directory": "packages/redux"
|
||||
},
|
||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
||||
"homepage": "https://json-render.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/remotion
|
||||
|
||||
## 0.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/remotion",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Remotion renderer for @json-render/core. JSON becomes video compositions.",
|
||||
"keywords": [
|
||||
@@ -19,7 +19,7 @@
|
||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||
"directory": "packages/remotion"
|
||||
},
|
||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
||||
"homepage": "https://json-render.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @json-render/shadcn
|
||||
|
||||
## 0.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [54a1ecf]
|
||||
- @json-render/core@0.12.1
|
||||
- @json-render/react@0.12.1
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/shadcn",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"license": "Apache-2.0",
|
||||
"description": "shadcn/ui component library for @json-render/core. JSON becomes beautiful Tailwind-styled React components.",
|
||||
"keywords": [
|
||||
@@ -22,7 +22,7 @@
|
||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||
"directory": "packages/shadcn"
|
||||
},
|
||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
||||
"homepage": "https://json-render.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
# @json-render/solid
|
||||
|
||||
SolidJS renderer for json-render. Turn JSON specs into Solid components with data binding, visibility, actions, validation, and streaming.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @json-render/core @json-render/solid zod
|
||||
```
|
||||
|
||||
Peer dependencies: `solid-js ^1.9.0` and `zod ^4.0.0`.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create a Catalog
|
||||
|
||||
```typescript
|
||||
import { defineCatalog } from "@json-render/core";
|
||||
import { schema } from "@json-render/solid/schema";
|
||||
import { z } from "zod";
|
||||
|
||||
export const catalog = defineCatalog(schema, {
|
||||
components: {
|
||||
Card: {
|
||||
props: z.object({
|
||||
title: z.string(),
|
||||
description: z.string().nullable(),
|
||||
}),
|
||||
description: "A card container",
|
||||
},
|
||||
Button: {
|
||||
props: z.object({
|
||||
label: z.string(),
|
||||
action: z.string(),
|
||||
}),
|
||||
description: "A clickable button",
|
||||
},
|
||||
Input: {
|
||||
props: z.object({
|
||||
value: z.union([z.string(), z.record(z.unknown())]).nullable(),
|
||||
label: z.string(),
|
||||
placeholder: z.string().nullable(),
|
||||
}),
|
||||
description: "Text input with optional state binding",
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
submit: { description: "Submit the form" },
|
||||
cancel: { description: "Cancel and close" },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Define Component Implementations
|
||||
|
||||
`defineRegistry` conditionally requires the `actions` field only when the catalog declares actions.
|
||||
|
||||
```tsx
|
||||
import { defineRegistry, useBoundProp } from "@json-render/solid";
|
||||
import { catalog } from "./catalog";
|
||||
|
||||
export const { registry } = defineRegistry(catalog, {
|
||||
components: {
|
||||
Card: (renderProps) => (
|
||||
<div class="card">
|
||||
<h3>{renderProps.element.props.title as string}</h3>
|
||||
{renderProps.children}
|
||||
</div>
|
||||
),
|
||||
Button: (renderProps) => (
|
||||
<button onClick={() => renderProps.emit("press")}>
|
||||
{renderProps.element.props.label as string}
|
||||
</button>
|
||||
),
|
||||
Input: (renderProps) => {
|
||||
const [value, setValue] = useBoundProp(
|
||||
renderProps.element.props.value,
|
||||
renderProps.bindings?.value,
|
||||
);
|
||||
return (
|
||||
<label>
|
||||
{renderProps.element.props.label as string}
|
||||
<input
|
||||
value={String(value() ?? "")}
|
||||
placeholder={String(renderProps.element.props.placeholder ?? "")}
|
||||
onInput={(e) => setValue(e.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
submit: async () => {},
|
||||
cancel: async () => {},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Render Specs
|
||||
|
||||
```tsx
|
||||
import { Renderer, StateProvider, ActionProvider } from "@json-render/solid";
|
||||
import { registry } from "./registry";
|
||||
|
||||
export function App(props: { spec: any }) {
|
||||
return (
|
||||
<StateProvider initialState={{ form: { name: "" } }}>
|
||||
<ActionProvider handlers={{ submit: () => console.log("submit") }}>
|
||||
<Renderer spec={props.spec} registry={registry} />
|
||||
</ActionProvider>
|
||||
</StateProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Spec Format
|
||||
|
||||
`@json-render/solid` uses the same flat element map format as React/Vue:
|
||||
|
||||
```typescript
|
||||
interface Spec {
|
||||
root: string;
|
||||
elements: Record<string, UIElement>;
|
||||
state?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface UIElement {
|
||||
type: string;
|
||||
props: Record<string, unknown>;
|
||||
children?: string[];
|
||||
visible?: VisibilityCondition;
|
||||
watch?: Record<string, ActionBinding | ActionBinding[]>;
|
||||
}
|
||||
```
|
||||
|
||||
## Providers
|
||||
|
||||
| Provider | Purpose |
|
||||
| --------------------- | ---------------------------------------------------- |
|
||||
| `StateProvider` | State model and JSON Pointer read/write APIs |
|
||||
| `ActionProvider` | Action dispatch, loading tracking, confirmation flow |
|
||||
| `VisibilityProvider` | Visibility condition evaluation from current state |
|
||||
| `ValidationProvider` | Field-level and full-form validation |
|
||||
| `RepeatScopeProvider` | Repeat context (`$item`, `$index`, `$bindItem`) |
|
||||
| `JSONUIProvider` | Combined provider wiring for renderer trees |
|
||||
|
||||
### External Store (Controlled Mode)
|
||||
|
||||
Pass a `StateStore` to `StateProvider`, `JSONUIProvider`, or the component returned by `createRenderer`.
|
||||
When `store` is provided, `initialState` and `onStateChange` are ignored.
|
||||
|
||||
```tsx
|
||||
import { createStateStore, StateProvider } from "@json-render/solid";
|
||||
|
||||
const store = createStateStore({ count: 0 });
|
||||
|
||||
<StateProvider store={store}>{/* ... */}</StateProvider>;
|
||||
```
|
||||
|
||||
## Hooks
|
||||
|
||||
| Hook | Purpose |
|
||||
| --------------------------------------------- | ----------------------------------------------------- |
|
||||
| `useStateStore()` | Access `state`, `get`, `set`, `update`, `getSnapshot` |
|
||||
| `useStateValue(path)` | Read a value by JSON Pointer path via accessor |
|
||||
| `useStateBinding(path)` | Legacy two-way binding helper returning an accessor |
|
||||
| `useVisibility()` / `useIsVisible()` | Visibility context and checks |
|
||||
| `useActions()` / `useAction()` | Action context and single-action helper |
|
||||
| `useValidation()` / `useOptionalValidation()` | Validation context (throwing/non-throwing) |
|
||||
| `useFieldValidation(path, config)` | Field state accessors plus validate/touch/clear |
|
||||
| `useBoundProp(value, binding)` | Fine-grained two-way binding helper |
|
||||
| `useUIStream(options)` | Stream UI specs from an endpoint |
|
||||
| `useChatUI(options)` | Chat-style spec generation hook |
|
||||
|
||||
## Built-in Actions
|
||||
|
||||
These actions are available in the Solid schema and handled by `ActionProvider`:
|
||||
|
||||
- `setState`
|
||||
- `pushState`
|
||||
- `removeState`
|
||||
- `validateForm`
|
||||
|
||||
`setState`/`pushState`/`removeState` mutate the state model. `validateForm` validates registered fields and writes `{ valid, errors }` to state (`/formValidation` by default).
|
||||
|
||||
## Events and Action Binding
|
||||
|
||||
Components can use either `emit("event")` or `on("event")`.
|
||||
|
||||
- `emit` fires named event bindings directly.
|
||||
- `on` returns an `EventHandle` with `emit`, `bound`, and `shouldPreventDefault`.
|
||||
|
||||
This mirrors the React package API while preserving Solid's fine-grained reactivity.
|
||||
|
||||
## Streaming
|
||||
|
||||
`useUIStream` and `useChatUI` support JSON patch streaming and mixed text/spec data parts.
|
||||
|
||||
```tsx
|
||||
import { useUIStream } from "@json-render/solid";
|
||||
|
||||
const stream = useUIStream({ api: "/api/generate" });
|
||||
await stream.send("Build me a dashboard");
|
||||
```
|
||||
|
||||
## Key Exports
|
||||
|
||||
| Export | Purpose |
|
||||
| ------------------ | -------------------------------------------------------------------------- |
|
||||
| `defineRegistry` | Create catalog-aware component and action registry helpers |
|
||||
| `Renderer` | Render a `Spec` with a component registry |
|
||||
| `createRenderer` | Build an app-level renderer with provider wiring |
|
||||
| `JSONUIProvider` | Combined provider tree (`state` + `visibility` + `validation` + `actions`) |
|
||||
| `schema` | Solid element schema with built-in actions |
|
||||
| `createStateStore` | Framework-agnostic in-memory `StateStore` |
|
||||
|
||||
### Types
|
||||
|
||||
| Export | Purpose |
|
||||
| --------------------------- | -------------------------------------------------------- |
|
||||
| `ComponentContext` | Catalog-aware component context type |
|
||||
| `BaseComponentProps` | Catalog-agnostic component props type |
|
||||
| `EventHandle` | Event metadata (`emit`, `bound`, `shouldPreventDefault`) |
|
||||
| `StateStore` | Controlled state backend interface |
|
||||
| `StateModel` | Renderer state model type |
|
||||
| `SolidSchema` / `SolidSpec` | Solid schema/spec types |
|
||||
|
||||
## Differences from `@json-render/react`
|
||||
|
||||
Most APIs are intentionally aligned, but there are runtime behavior differences due to Solid:
|
||||
|
||||
- Solid components run once, then update via signals.
|
||||
- Keep changing reads inside JSX expressions, `createMemo`, or `createEffect`.
|
||||
- Avoid props destructuring in component signatures when values should remain reactive.
|
||||
- Hooks that read changing state return accessors; call them inside JSX or effects.
|
||||
|
||||
## Documentation
|
||||
|
||||
Full docs: [json-render.dev/docs/api/solid](https://json-render.dev/docs/api/solid)
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "@json-render/solid",
|
||||
"version": "0.12.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "SolidJS renderer for @json-render/core. JSON becomes Solid components.",
|
||||
"keywords": [
|
||||
"json",
|
||||
"ui",
|
||||
"solid",
|
||||
"solidjs",
|
||||
"ai",
|
||||
"generative-ui",
|
||||
"llm",
|
||||
"renderer",
|
||||
"streaming",
|
||||
"components"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||
"directory": "packages/solid"
|
||||
},
|
||||
"homepage": "https://json-render.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
},
|
||||
"./schema": {
|
||||
"types": "./dist/schema.d.ts",
|
||||
"import": "./dist/schema.mjs",
|
||||
"require": "./dist/schema.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"check-types": "tsc --noEmit",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@json-render/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@internal/typescript-config": "workspace:*",
|
||||
"esbuild-plugin-solid": "^0.6.0",
|
||||
"solid-js": "^1.9.0",
|
||||
"tsup": "^8.0.2",
|
||||
"typescript": "^5.4.5",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.9.0",
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { JSX } from "solid-js";
|
||||
import type {
|
||||
Catalog,
|
||||
InferCatalogComponents,
|
||||
InferCatalogActions,
|
||||
InferComponentProps,
|
||||
InferActionParams,
|
||||
StateModel,
|
||||
} from "@json-render/core";
|
||||
|
||||
export type { StateModel };
|
||||
|
||||
// =============================================================================
|
||||
// State Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* State setter function for updating application state
|
||||
*/
|
||||
export type SetState = (
|
||||
updater: (prev: Record<string, unknown>) => Record<string, unknown>,
|
||||
) => void;
|
||||
|
||||
// =============================================================================
|
||||
// Component Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Handle returned by the `on()` function for a specific event.
|
||||
* Provides metadata about the event binding and a method to fire it.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const press = on("press");
|
||||
* if (press.shouldPreventDefault) e.preventDefault();
|
||||
* press.emit();
|
||||
* ```
|
||||
*/
|
||||
export interface EventHandle {
|
||||
/** Fire the event (resolve action bindings) */
|
||||
emit: () => void;
|
||||
/** Whether any binding requested preventDefault */
|
||||
shouldPreventDefault: boolean;
|
||||
/** Whether any handler is bound to this event */
|
||||
bound: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Catalog-agnostic base type for component render function arguments.
|
||||
* Use this when building reusable component libraries that are not tied
|
||||
* to a specific catalog.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const Card = (ctx: BaseComponentProps<{ title?: string }>) => (
|
||||
* <div>{ctx.props.title}{ctx.children}</div>
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export interface BaseComponentProps<P = Record<string, unknown>> {
|
||||
props: P;
|
||||
children?: JSX.Element;
|
||||
/** Simple event emitter (shorthand). Fires the event and returns void. */
|
||||
emit: (event: string) => void;
|
||||
/** Get an event handle with metadata. Use when you need shouldPreventDefault or bound checks. */
|
||||
on: (event: string) => EventHandle;
|
||||
/**
|
||||
* Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.
|
||||
* Maps prop name → absolute state path for write-back.
|
||||
*/
|
||||
bindings?: Record<string, string>;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context passed to component render functions
|
||||
* @example
|
||||
* const Button: ComponentFn<typeof catalog, 'Button'> = (ctx) => {
|
||||
* return <button onClick={() => ctx.emit("press")}>{ctx.props.label}</button>
|
||||
* }
|
||||
*/
|
||||
export interface ComponentContext<
|
||||
C extends Catalog,
|
||||
K extends keyof InferCatalogComponents<C>,
|
||||
> extends BaseComponentProps<InferComponentProps<C, K>> {}
|
||||
|
||||
/**
|
||||
* Component render function type for Solid
|
||||
* @example
|
||||
* const Button: ComponentFn<typeof catalog, 'Button'> = (ctx) => (
|
||||
* <button onClick={() => ctx.emit("press")}>{ctx.props.label}</button>
|
||||
* );
|
||||
*/
|
||||
export type ComponentFn<
|
||||
C extends Catalog,
|
||||
K extends keyof InferCatalogComponents<C>,
|
||||
> = (ctx: ComponentContext<C, K>) => JSX.Element;
|
||||
|
||||
/**
|
||||
* Registry of all component render functions for a catalog
|
||||
* @example
|
||||
* const components: Components<typeof myCatalog> = {
|
||||
* Button: (ctx) => <button>{ctx.props.label}</button>,
|
||||
* Input: (ctx) => <input placeholder={ctx.props.placeholder} />,
|
||||
* };
|
||||
*/
|
||||
export type Components<C extends Catalog> = {
|
||||
[K in keyof InferCatalogComponents<C>]: ComponentFn<C, K>;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Action Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Action handler function type
|
||||
* @example
|
||||
* const viewCustomers: ActionFn<typeof catalog, 'viewCustomers'> = async (params, setState) => {
|
||||
* const data = await fetch('/api/customers');
|
||||
* setState(prev => ({ ...prev, customers: data }));
|
||||
* };
|
||||
*/
|
||||
export type ActionFn<
|
||||
C extends Catalog,
|
||||
K extends keyof InferCatalogActions<C>,
|
||||
> = (
|
||||
params: InferActionParams<C, K> | undefined,
|
||||
setState: SetState,
|
||||
state: StateModel,
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Registry of all action handlers for a catalog
|
||||
* @example
|
||||
* const actions: Actions<typeof myCatalog> = {
|
||||
* viewCustomers: async (params, setState) => { ... },
|
||||
* createCustomer: async (params, setState) => { ... },
|
||||
* };
|
||||
*/
|
||||
export type Actions<C extends Catalog> = {
|
||||
[K in keyof InferCatalogActions<C>]: ActionFn<C, K>;
|
||||
};
|
||||
|
||||
/**
|
||||
* True when the catalog declares at least one action, false otherwise.
|
||||
* Used by defineRegistry to conditionally require the `actions` field.
|
||||
*/
|
||||
export type CatalogHasActions<C extends Catalog> = [
|
||||
InferCatalogActions<C>,
|
||||
] extends [never]
|
||||
? false
|
||||
: [keyof InferCatalogActions<C>] extends [never]
|
||||
? false
|
||||
: true;
|
||||
@@ -0,0 +1,193 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@solidjs/testing-library";
|
||||
import type { Spec } from "@json-render/core";
|
||||
import {
|
||||
JSONUIProvider,
|
||||
Renderer,
|
||||
type ComponentRenderProps,
|
||||
} from "./renderer";
|
||||
import { useStateStore } from "./contexts/state";
|
||||
|
||||
// DO NOT destructure props — Solid requires proxy access
|
||||
function Button(props: ComponentRenderProps<{ label: string }>) {
|
||||
return (
|
||||
<button data-testid="btn" onClick={() => props.emit("press")}>
|
||||
{props.element.props.label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Text(props: ComponentRenderProps<{ text: unknown }>) {
|
||||
const display = () => {
|
||||
const v = props.element.props.text;
|
||||
return typeof v === "string" ? v : JSON.stringify(v);
|
||||
};
|
||||
return <span data-testid={`text-${props.element.type}`}>{display()}</span>;
|
||||
}
|
||||
|
||||
let probeGetSnapshot: (() => Record<string, unknown>) | undefined;
|
||||
|
||||
function StateProbe() {
|
||||
const ctx = useStateStore();
|
||||
probeGetSnapshot = ctx.getSnapshot;
|
||||
return <div data-testid="state-probe" />;
|
||||
}
|
||||
|
||||
const registry = {
|
||||
Button,
|
||||
Text,
|
||||
};
|
||||
|
||||
function getProbeState(): Record<string, unknown> {
|
||||
return probeGetSnapshot!();
|
||||
}
|
||||
|
||||
describe("chained actions: live $state resolution (#141)", () => {
|
||||
it("setState after pushState sees the post-push value via $state", async () => {
|
||||
probeGetSnapshot = undefined;
|
||||
const spec: Spec = {
|
||||
state: { items: ["initial"], observed: "not yet set" },
|
||||
root: "main",
|
||||
elements: {
|
||||
main: {
|
||||
type: "Button",
|
||||
props: { label: "Add Item" },
|
||||
on: {
|
||||
press: [
|
||||
{
|
||||
action: "pushState",
|
||||
params: { statePath: "/items", value: "new-item" },
|
||||
},
|
||||
{
|
||||
action: "setState",
|
||||
params: {
|
||||
statePath: "/observed",
|
||||
value: { $state: "/items" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<JSONUIProvider registry={registry} initialState={spec.state}>
|
||||
<Renderer spec={spec} registry={registry} />
|
||||
<StateProbe />
|
||||
</JSONUIProvider>
|
||||
);
|
||||
}
|
||||
|
||||
render(() => <App />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("btn"));
|
||||
|
||||
await waitFor(() => {
|
||||
const state = getProbeState();
|
||||
expect(state.items).toEqual(["initial", "new-item"]);
|
||||
expect(state.observed).toEqual(["initial", "new-item"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("multiple pushState + setState chain resolves correctly", async () => {
|
||||
probeGetSnapshot = undefined;
|
||||
const spec: Spec = {
|
||||
state: { items: [], snapshot: null },
|
||||
root: "main",
|
||||
elements: {
|
||||
main: {
|
||||
type: "Button",
|
||||
props: { label: "Go" },
|
||||
on: {
|
||||
press: [
|
||||
{
|
||||
action: "pushState",
|
||||
params: { statePath: "/items", value: "a" },
|
||||
},
|
||||
{
|
||||
action: "pushState",
|
||||
params: { statePath: "/items", value: "b" },
|
||||
},
|
||||
{
|
||||
action: "setState",
|
||||
params: {
|
||||
statePath: "/snapshot",
|
||||
value: { $state: "/items" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<JSONUIProvider registry={registry} initialState={spec.state}>
|
||||
<Renderer spec={spec} registry={registry} />
|
||||
<StateProbe />
|
||||
</JSONUIProvider>
|
||||
);
|
||||
}
|
||||
|
||||
render(() => <App />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("btn"));
|
||||
|
||||
await waitFor(() => {
|
||||
const state = getProbeState();
|
||||
expect(state.items).toEqual(["a", "b"]);
|
||||
expect(state.snapshot).toEqual(["a", "b"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("setState reading a path mutated by an earlier setState sees fresh value", async () => {
|
||||
probeGetSnapshot = undefined;
|
||||
const spec: Spec = {
|
||||
state: { counter: 0, counterCopy: -1 },
|
||||
root: "main",
|
||||
elements: {
|
||||
main: {
|
||||
type: "Button",
|
||||
props: { label: "Go" },
|
||||
on: {
|
||||
press: [
|
||||
{
|
||||
action: "setState",
|
||||
params: { statePath: "/counter", value: 42 },
|
||||
},
|
||||
{
|
||||
action: "setState",
|
||||
params: {
|
||||
statePath: "/counterCopy",
|
||||
value: { $state: "/counter" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<JSONUIProvider registry={registry} initialState={spec.state}>
|
||||
<Renderer spec={spec} registry={registry} />
|
||||
<StateProbe />
|
||||
</JSONUIProvider>
|
||||
);
|
||||
}
|
||||
|
||||
render(() => <App />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("btn"));
|
||||
|
||||
await waitFor(() => {
|
||||
const state = getProbeState();
|
||||
expect(state.counter).toBe(42);
|
||||
expect(state.counterCopy).toBe(42);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,346 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render } from "@solidjs/testing-library";
|
||||
import type { JSX } from "solid-js";
|
||||
import { StateProvider, useStateStore } from "./state";
|
||||
import {
|
||||
ValidationProvider,
|
||||
useValidation,
|
||||
useFieldValidation,
|
||||
} from "./validation";
|
||||
import { ActionProvider, useActions, useAction } from "./actions";
|
||||
|
||||
/**
|
||||
* Build the full provider tree inline so that Solid's eager JSX evaluation
|
||||
* always runs ActionProvider/ValidationProvider INSIDE StateProvider's scope.
|
||||
*
|
||||
* IMPORTANT: Do NOT pre-build JSX fragments as variables — Solid evaluates
|
||||
* JSX eagerly, so `<ActionProvider>` would call `useStateStore()` before the
|
||||
* StateProvider context is available.
|
||||
*/
|
||||
function withProviders<T>(
|
||||
hook: () => T,
|
||||
options: {
|
||||
handlers?: Record<
|
||||
string,
|
||||
(params: Record<string, unknown>) => Promise<void> | void
|
||||
>;
|
||||
initialState?: Record<string, unknown>;
|
||||
withValidation?: boolean;
|
||||
} = {},
|
||||
): T {
|
||||
let result!: T;
|
||||
function TestComponent(): JSX.Element {
|
||||
result = hook();
|
||||
return (<div />) as JSX.Element;
|
||||
}
|
||||
|
||||
if (options.withValidation) {
|
||||
render(() => (
|
||||
<StateProvider initialState={options.initialState ?? {}}>
|
||||
<ValidationProvider>
|
||||
<ActionProvider handlers={options.handlers}>
|
||||
<TestComponent />
|
||||
</ActionProvider>
|
||||
</ValidationProvider>
|
||||
</StateProvider>
|
||||
));
|
||||
} else {
|
||||
render(() => (
|
||||
<StateProvider initialState={options.initialState ?? {}}>
|
||||
<ActionProvider handlers={options.handlers}>
|
||||
<TestComponent />
|
||||
</ActionProvider>
|
||||
</StateProvider>
|
||||
));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function withFullProviders(options: {
|
||||
handlers?: Record<
|
||||
string,
|
||||
(params: Record<string, unknown>) => Promise<void> | void
|
||||
>;
|
||||
initialState?: Record<string, unknown>;
|
||||
withValidation?: boolean;
|
||||
}): {
|
||||
stateCtx: ReturnType<typeof useStateStore>;
|
||||
actionsCtx: ReturnType<typeof useActions>;
|
||||
validationCtx?: ReturnType<typeof useValidation>;
|
||||
} {
|
||||
let stateCtx!: ReturnType<typeof useStateStore>;
|
||||
let actionsCtx!: ReturnType<typeof useActions>;
|
||||
let validationCtx: ReturnType<typeof useValidation> | undefined;
|
||||
function TestComponent(): JSX.Element {
|
||||
stateCtx = useStateStore();
|
||||
actionsCtx = useActions();
|
||||
if (options.withValidation) {
|
||||
validationCtx = useValidation();
|
||||
}
|
||||
return (<div />) as JSX.Element;
|
||||
}
|
||||
|
||||
if (options.withValidation) {
|
||||
render(() => (
|
||||
<StateProvider initialState={options.initialState ?? {}}>
|
||||
<ValidationProvider>
|
||||
<ActionProvider handlers={options.handlers}>
|
||||
<TestComponent />
|
||||
</ActionProvider>
|
||||
</ValidationProvider>
|
||||
</StateProvider>
|
||||
));
|
||||
} else {
|
||||
render(() => (
|
||||
<StateProvider initialState={options.initialState ?? {}}>
|
||||
<ActionProvider handlers={options.handlers}>
|
||||
<TestComponent />
|
||||
</ActionProvider>
|
||||
</StateProvider>
|
||||
));
|
||||
}
|
||||
return { stateCtx, actionsCtx, validationCtx };
|
||||
}
|
||||
|
||||
describe("ActionProvider — provide/inject", () => {
|
||||
it("useActions() throws outside a provider", () => {
|
||||
expect(() => useActions()).toThrow(
|
||||
"useActions must be used within an ActionProvider",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ActionProvider — built-in setState", () => {
|
||||
it("executes setState and updates state", async () => {
|
||||
const { stateCtx, actionsCtx } = withFullProviders({
|
||||
initialState: { count: 0 },
|
||||
});
|
||||
|
||||
await actionsCtx.execute({
|
||||
action: "setState",
|
||||
params: { statePath: "/count", value: 5 },
|
||||
});
|
||||
|
||||
expect(stateCtx.get("/count")).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ActionProvider — built-in pushState", () => {
|
||||
it("appends to existing array", async () => {
|
||||
const { stateCtx, actionsCtx } = withFullProviders({
|
||||
initialState: { items: ["a", "b"] },
|
||||
});
|
||||
|
||||
await actionsCtx.execute({
|
||||
action: "pushState",
|
||||
params: { statePath: "/items", value: "c" },
|
||||
});
|
||||
|
||||
expect(stateCtx.get("/items")).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
|
||||
it("creates array if path does not exist", async () => {
|
||||
const { stateCtx, actionsCtx } = withFullProviders({
|
||||
initialState: {},
|
||||
});
|
||||
|
||||
await actionsCtx.execute({
|
||||
action: "pushState",
|
||||
params: { statePath: "/newList", value: "first" },
|
||||
});
|
||||
|
||||
expect(stateCtx.get("/newList")).toEqual(["first"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ActionProvider — built-in removeState", () => {
|
||||
it("removes item by index", async () => {
|
||||
const { stateCtx, actionsCtx } = withFullProviders({
|
||||
initialState: { items: ["a", "b", "c"] },
|
||||
});
|
||||
|
||||
await actionsCtx.execute({
|
||||
action: "removeState",
|
||||
params: { statePath: "/items", index: 1 },
|
||||
});
|
||||
|
||||
expect(stateCtx.get("/items")).toEqual(["a", "c"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ActionProvider — built-in push/pop navigation", () => {
|
||||
it("push sets /currentScreen and /navStack", async () => {
|
||||
const { stateCtx, actionsCtx } = withFullProviders({
|
||||
initialState: { currentScreen: "home" },
|
||||
});
|
||||
|
||||
await actionsCtx.execute({
|
||||
action: "push",
|
||||
params: { screen: "settings" },
|
||||
});
|
||||
|
||||
expect(stateCtx.get("/currentScreen")).toBe("settings");
|
||||
expect(stateCtx.get("/navStack")).toEqual(["home"]);
|
||||
});
|
||||
|
||||
it("pop restores previous screen from /navStack", async () => {
|
||||
const { stateCtx, actionsCtx } = withFullProviders({
|
||||
initialState: { currentScreen: "settings", navStack: ["home"] },
|
||||
});
|
||||
|
||||
await actionsCtx.execute({ action: "pop" });
|
||||
|
||||
expect(stateCtx.get("/currentScreen")).toBe("home");
|
||||
expect(stateCtx.get("/navStack")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ActionProvider — custom handlers", () => {
|
||||
it("executes custom handler with params", async () => {
|
||||
const customHandler = vi.fn().mockResolvedValue(undefined);
|
||||
const { actionsCtx } = withFullProviders({
|
||||
handlers: { myAction: customHandler },
|
||||
});
|
||||
|
||||
await actionsCtx.execute({
|
||||
action: "myAction",
|
||||
params: { foo: "bar" },
|
||||
});
|
||||
|
||||
expect(customHandler).toHaveBeenCalledWith({ foo: "bar" });
|
||||
});
|
||||
|
||||
it("console.warn for unknown action", async () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const { actionsCtx } = withFullProviders({});
|
||||
|
||||
await actionsCtx.execute({ action: "unknownAction" });
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("unknownAction"),
|
||||
);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("tracks loading state during async action execution", async () => {
|
||||
let resolveHandler!: () => void;
|
||||
const slowHandler = vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
resolveHandler = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
let executeFn!: ReturnType<typeof useActions>["execute"];
|
||||
|
||||
const { findByTestId } = render(() => {
|
||||
function Inner(): JSX.Element {
|
||||
const actions = useActions();
|
||||
executeFn = actions.execute;
|
||||
return (
|
||||
<span data-testid="loading">
|
||||
{actions.loadingActions.has("slowAction") ? "true" : "false"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<StateProvider initialState={{}}>
|
||||
<ActionProvider handlers={{ slowAction: slowHandler }}>
|
||||
<Inner />
|
||||
</ActionProvider>
|
||||
</StateProvider>
|
||||
);
|
||||
});
|
||||
|
||||
// Before execution — not loading
|
||||
const el = await findByTestId("loading");
|
||||
expect(el.textContent).toBe("false");
|
||||
|
||||
const executePromise = executeFn({ action: "slowAction" });
|
||||
|
||||
// During execution — loading (need to wait for Solid's reactivity to flush)
|
||||
await vi.waitFor(() => {
|
||||
expect(el.textContent).toBe("true");
|
||||
});
|
||||
|
||||
resolveHandler();
|
||||
await executePromise;
|
||||
|
||||
// After execution — no longer loading
|
||||
await vi.waitFor(() => {
|
||||
expect(el.textContent).toBe("false");
|
||||
});
|
||||
});
|
||||
|
||||
it("registerHandler allows dynamic handler registration", async () => {
|
||||
const dynamicHandler = vi.fn().mockResolvedValue(undefined);
|
||||
const { actionsCtx } = withFullProviders({});
|
||||
|
||||
actionsCtx.registerHandler("dynamicAction", dynamicHandler);
|
||||
await actionsCtx.execute({
|
||||
action: "dynamicAction",
|
||||
params: { x: 1 },
|
||||
});
|
||||
|
||||
expect(dynamicHandler).toHaveBeenCalledWith({ x: 1 });
|
||||
});
|
||||
|
||||
it("handler receives resolved params object", async () => {
|
||||
const handler = vi.fn().mockResolvedValue(undefined);
|
||||
const { actionsCtx } = withFullProviders({
|
||||
handlers: { myAction: handler },
|
||||
});
|
||||
|
||||
await actionsCtx.execute({
|
||||
action: "myAction",
|
||||
params: { x: 1, y: "hello" },
|
||||
});
|
||||
|
||||
expect(handler).toHaveBeenCalledWith({ x: 1, y: "hello" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("ActionProvider — validateForm", () => {
|
||||
it("writes { valid, errors } to state", async () => {
|
||||
const { stateCtx, actionsCtx, validationCtx } = withFullProviders({
|
||||
initialState: {},
|
||||
withValidation: true,
|
||||
});
|
||||
|
||||
validationCtx!.registerField("/form/email", {
|
||||
checks: [{ type: "required", message: "Required" }],
|
||||
});
|
||||
|
||||
await actionsCtx.execute({ action: "validateForm" });
|
||||
|
||||
expect(stateCtx.get("/formValidation")).toEqual({
|
||||
valid: false,
|
||||
errors: { "/form/email": ["Required"] },
|
||||
});
|
||||
});
|
||||
|
||||
it("warns without ValidationProvider", async () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const { actionsCtx } = withFullProviders({
|
||||
withValidation: false,
|
||||
});
|
||||
|
||||
await actionsCtx.execute({ action: "validateForm" });
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("validateForm action was dispatched"),
|
||||
);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("useAction", () => {
|
||||
it("returns { execute, isLoading: false } before execution", () => {
|
||||
const result = withProviders(() => useAction({ action: "myAction" }), {
|
||||
handlers: { myAction: vi.fn() },
|
||||
});
|
||||
|
||||
expect(typeof result.execute).toBe("function");
|
||||
expect(result.isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user