mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 04:18:15 +08:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2b397510e | ||
|
|
8506cfaa03 | ||
|
|
9cef4e9142 | ||
|
|
3c11f19be4 | ||
|
|
db3a8b41e9 | ||
|
|
ea47b66dfc | ||
|
|
cd82f969c8 | ||
|
|
6bcaaad57d | ||
|
|
0b7d767cdd | ||
|
|
b1036763d2 | ||
|
|
c502d5517e | ||
|
|
8740deb018 | ||
|
|
1d755c104a | ||
|
|
d904d45150 | ||
|
|
a110c6e0ea | ||
|
|
7de08ccdf7 | ||
|
|
3201854481 | ||
|
|
64c889221e | ||
|
|
49838fa353 | ||
|
|
fa47b08869 | ||
|
|
5ccb109c08 | ||
|
|
62932f6516 | ||
|
|
ee28d548c1 | ||
|
|
0e6f2afc6c | ||
|
|
bccedc2459 |
@@ -10,7 +10,12 @@
|
|||||||
"@json-render/shadcn",
|
"@json-render/shadcn",
|
||||||
"@json-render/react-native",
|
"@json-render/react-native",
|
||||||
"@json-render/remotion",
|
"@json-render/remotion",
|
||||||
"@json-render/codegen"
|
"@json-render/codegen",
|
||||||
|
"@json-render/zustand",
|
||||||
|
"@json-render/redux",
|
||||||
|
"@json-render/jotai",
|
||||||
|
"@json-render/vue",
|
||||||
|
"@json-render/xstate"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"linked": [],
|
"linked": [],
|
||||||
@@ -18,7 +23,7 @@
|
|||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
"updateInternalDependencies": "patch",
|
"updateInternalDependencies": "patch",
|
||||||
"privatePackages": {
|
"privatePackages": {
|
||||||
"version": false,
|
"version": true,
|
||||||
"tag": false
|
"tag": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-9
@@ -6,11 +6,8 @@ node_modules
|
|||||||
.pnp.js
|
.pnp.js
|
||||||
|
|
||||||
# Local env files
|
# Local env files
|
||||||
.env
|
.env*
|
||||||
.env.local
|
!.env.example
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
coverage
|
coverage
|
||||||
@@ -44,10 +41,6 @@ yarn-error.log*
|
|||||||
# opensrc - source code for packages
|
# opensrc - source code for packages
|
||||||
opensrc/
|
opensrc/
|
||||||
|
|
||||||
# json-studio (separate repo)
|
|
||||||
json-studio/
|
|
||||||
.env*.local
|
|
||||||
|
|
||||||
# Stripe apps (generated from template + build artifacts)
|
# Stripe apps (generated from template + build artifacts)
|
||||||
examples/stripe-app/*/stripe-app.json
|
examples/stripe-app/*/stripe-app.json
|
||||||
examples/stripe-app/*/.build
|
examples/stripe-app/*/.build
|
||||||
|
|||||||
@@ -27,12 +27,48 @@ This ensures we don't install outdated versions that may have incompatible types
|
|||||||
- Do not use emojis in code or UI
|
- Do not use emojis in code or UI
|
||||||
- Use shadcn CLI to add shadcn/ui components: `pnpm dlx shadcn@latest add <component>`
|
- Use shadcn CLI to add shadcn/ui components: `pnpm dlx shadcn@latest add <component>`
|
||||||
|
|
||||||
|
## AI SDK / AI Gateway
|
||||||
|
|
||||||
|
When using the Vercel AI SDK (`ai` package) with AI Gateway, pass the model as a plain string identifier -- do not import a provider constructor:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { streamText } from "ai";
|
||||||
|
|
||||||
|
const result = streamText({
|
||||||
|
model: "anthropic/claude-haiku-4.5",
|
||||||
|
prompt: "...",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
This requires `AI_GATEWAY_API_KEY` to be set in the environment. See `tests/e2e/` for examples.
|
||||||
|
|
||||||
|
## Dev Servers
|
||||||
|
|
||||||
|
All apps and examples with dev servers use [portless](https://github.com/vercel-labs/portless) to avoid hardcoded ports. Portless assigns random ports and exposes each app via `.localhost` URLs.
|
||||||
|
|
||||||
|
Naming convention:
|
||||||
|
- Main web app: `json-render` → `json-render.localhost:1355`
|
||||||
|
- Examples: `[name]-demo.json-render` → `[name]-demo.json-render.localhost:1355`
|
||||||
|
|
||||||
|
When adding a new example that runs a dev server, wrap its `dev` script with `portless <name>`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "portless my-example-demo.json-render next dev --turbopack"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Do **not** add `--port` flags -- portless handles port assignment automatically. Do **not** add portless as a project dependency; it must be installed globally.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
- Run `pnpm type-check` after each turn to ensure type safety
|
- Run `pnpm type-check` after each turn to ensure type safety
|
||||||
- When making user-facing changes (new packages, API changes, new features, renamed exports, changed behavior), update the relevant documentation:
|
- When making user-facing changes (new packages, API changes, new features, renamed exports, changed behavior), update the relevant documentation:
|
||||||
- Package `README.md` files in `packages/*/README.md`
|
- Package `README.md` files in `packages/*/README.md`
|
||||||
- Root `README.md` (if packages table, install commands, or examples are affected)
|
- Root `README.md` (if packages table, install commands, or examples are affected)
|
||||||
|
- Web app docs in `apps/web/` (if guides, API references, or examples need updating)
|
||||||
- Skills in `skills/*/SKILL.md` (if the package has a corresponding skill)
|
- Skills in `skills/*/SKILL.md` (if the package has a corresponding skill)
|
||||||
- `AGENTS.md` (if workflow or conventions change)
|
- `AGENTS.md` (if workflow or conventions change)
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,18 @@
|
|||||||
Generate dynamic, personalized UIs from prompts without sacrificing reliability. Predefined components and actions for safe, predictable output.
|
Generate dynamic, personalized UIs from prompts without sacrificing reliability. Predefined components and actions for safe, predictable output.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# for React
|
||||||
npm install @json-render/core @json-render/react
|
npm install @json-render/core @json-render/react
|
||||||
# pre-built shadcn/ui components
|
# for React with pre-built shadcn/ui components
|
||||||
npm install @json-render/shadcn
|
npm install @json-render/shadcn
|
||||||
# or for mobile
|
# or for React Native
|
||||||
npm install @json-render/core @json-render/react-native
|
npm install @json-render/core @json-render/react-native
|
||||||
# or for video
|
# or for video
|
||||||
npm install @json-render/core @json-render/remotion
|
npm install @json-render/core @json-render/remotion
|
||||||
# or for PDF documents
|
# or for PDF documents
|
||||||
npm install @json-render/core @json-render/react-pdf
|
npm install @json-render/core @json-render/react-pdf
|
||||||
|
# or for Vue
|
||||||
|
npm install @json-render/core @json-render/vue
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why json-render?
|
## Why json-render?
|
||||||
@@ -23,7 +26,7 @@ json-render is a **Generative UI** framework: AI generates interfaces from natur
|
|||||||
- **Guardrailed** - AI can only use components in your catalog
|
- **Guardrailed** - AI can only use components in your catalog
|
||||||
- **Predictable** - JSON output matches your schema, every time
|
- **Predictable** - JSON output matches your schema, every time
|
||||||
- **Fast** - Stream and render progressively as the model responds
|
- **Fast** - Stream and render progressively as the model responds
|
||||||
- **Cross-Platform** - React (web) and React Native (mobile) from the same catalog
|
- **Cross-Platform** - React, Vue (web), React Native (mobile) from the same catalog
|
||||||
- **Batteries Included** - 36 pre-built shadcn/ui components ready to use
|
- **Batteries Included** - 36 pre-built shadcn/ui components ready to use
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@@ -32,7 +35,7 @@ json-render is a **Generative UI** framework: AI generates interfaces from natur
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { defineCatalog } from "@json-render/core";
|
import { defineCatalog } from "@json-render/core";
|
||||||
import { schema } from "@json-render/react";
|
import { schema } from "@json-render/react/schema";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const catalog = defineCatalog(schema, {
|
const catalog = defineCatalog(schema, {
|
||||||
@@ -110,10 +113,15 @@ function Dashboard({ spec }) {
|
|||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
|
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
|
||||||
| `@json-render/react` | React renderer, contexts, hooks |
|
| `@json-render/react` | React renderer, contexts, hooks |
|
||||||
|
| `@json-render/vue` | Vue 3 renderer, composables, providers |
|
||||||
| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
|
| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
|
||||||
| `@json-render/react-native` | React Native renderer with standard mobile components |
|
| `@json-render/react-native` | React Native renderer with standard mobile components |
|
||||||
| `@json-render/remotion` | Remotion video renderer, timeline schema |
|
| `@json-render/remotion` | Remotion video renderer, timeline schema |
|
||||||
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
|
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
|
||||||
|
| `@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` |
|
||||||
|
|
||||||
## Renderers
|
## Renderers
|
||||||
|
|
||||||
@@ -121,7 +129,7 @@ function Dashboard({ spec }) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { defineRegistry, Renderer } from "@json-render/react";
|
import { defineRegistry, Renderer } from "@json-render/react";
|
||||||
import { schema } from "@json-render/react";
|
import { schema } from "@json-render/react/schema";
|
||||||
|
|
||||||
// Flat spec format (root key + elements map)
|
// Flat spec format (root key + elements map)
|
||||||
const spec = {
|
const spec = {
|
||||||
@@ -145,11 +153,32 @@ const { registry } = defineRegistry(catalog, { components });
|
|||||||
<Renderer spec={spec} registry={registry} />
|
<Renderer spec={spec} registry={registry} />
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Vue (UI)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { h } from "vue";
|
||||||
|
import { defineRegistry, Renderer } from "@json-render/vue";
|
||||||
|
import { schema } from "@json-render/vue/schema";
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card: ({ props, children }) =>
|
||||||
|
h("div", { class: "card" }, [h("h3", null, props.title), children]),
|
||||||
|
Button: ({ props, emit }) =>
|
||||||
|
h("button", { onClick: () => emit("press") }, props.label),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// In your Vue component template:
|
||||||
|
// <Renderer :spec="spec" :registry="registry" />
|
||||||
|
```
|
||||||
|
|
||||||
### shadcn/ui (Web)
|
### shadcn/ui (Web)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { defineCatalog } from "@json-render/core";
|
import { defineCatalog } from "@json-render/core";
|
||||||
import { schema, defineRegistry, Renderer } from "@json-render/react";
|
import { schema } from "@json-render/react/schema";
|
||||||
|
import { defineRegistry, Renderer } from "@json-render/react";
|
||||||
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
|
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
|
||||||
import { shadcnComponents } from "@json-render/shadcn";
|
import { shadcnComponents } from "@json-render/shadcn";
|
||||||
|
|
||||||
@@ -313,10 +342,12 @@ Any prop value can be data-driven using expressions:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Two expression forms:
|
Expression forms:
|
||||||
|
|
||||||
- **`{ "$state": "/state/key" }`** - reads a value from the state model
|
- **`{ "$state": "/state/key" }`** - reads a value from the state model
|
||||||
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - evaluates a condition (same syntax as visibility conditions) and picks a branch
|
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - evaluates a condition and picks a branch
|
||||||
|
- **`{ "$template": "Hello, ${/user/name}!" }`** - interpolates state values into strings
|
||||||
|
- **`{ "$computed": "fn", "args": { ... } }`** - calls a registered function with resolved args
|
||||||
|
|
||||||
### Actions
|
### Actions
|
||||||
|
|
||||||
@@ -332,6 +363,22 @@ Components can trigger actions, including the built-in `setState` action:
|
|||||||
|
|
||||||
The `setState` action updates the state model directly, which re-evaluates visibility conditions and dynamic prop expressions.
|
The `setState` action updates the state model directly, which re-evaluates visibility conditions and dynamic prop expressions.
|
||||||
|
|
||||||
|
### State Watchers
|
||||||
|
|
||||||
|
React to state changes by triggering actions:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "Select",
|
||||||
|
"props": { "value": { "$bindState": "/form/country" }, "options": ["US", "Canada", "UK"] },
|
||||||
|
"watch": {
|
||||||
|
"/form/country": { "action": "loadCities", "params": { "country": { "$state": "/form/country" } } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`watch` is a top-level field on elements (sibling of `type`/`props`/`children`). Watchers fire when the watched value changes, not on initial render.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
@@ -343,10 +390,12 @@ pnpm install
|
|||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
- http://localhost:3000 - Docs & Playground
|
- http://json-render.localhost:1355 - Docs & Playground
|
||||||
- http://localhost:3001 - Example Dashboard
|
- http://dashboard-demo.json-render.localhost:1355 - Example Dashboard
|
||||||
- http://localhost:3002 - Remotion Video Example
|
- http://remotion-demo.json-render.localhost:1355 - Remotion Video Example
|
||||||
- Chat Example: run `pnpm dev` in `examples/chat`
|
- Chat Example: run `pnpm dev` in `examples/chat`
|
||||||
|
- Vue Example: run `pnpm dev` in `examples/vue`
|
||||||
|
- Vite Renderers (React + Vue): run `pnpm dev` in `examples/vite-renderers`
|
||||||
- React Native example: run `npx expo start` in `examples/react-native`
|
- React Native example: run `npx expo start` in `examples/react-native`
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# web
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9cef4e9]
|
||||||
|
- @json-render/core@0.10.0
|
||||||
|
- @json-render/react@0.10.0
|
||||||
|
- @json-render/codegen@0.10.0
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [b103676]
|
||||||
|
- @json-render/react@0.9.1
|
||||||
|
- @json-render/core@0.9.1
|
||||||
|
- @json-render/codegen@0.9.1
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [1d755c1]
|
||||||
|
- @json-render/core@0.9.0
|
||||||
|
- @json-render/react@0.9.0
|
||||||
|
- @json-render/codegen@0.9.0
|
||||||
+1
-1
@@ -14,7 +14,7 @@ pnpm dev
|
|||||||
bun dev
|
bun dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
Open [http://json-render.localhost:1355](http://json-render.localhost:1355) with your browser to see the result.
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "A2UI Integration" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/a2ui")
|
||||||
|
|
||||||
# A2UI Integration
|
# A2UI Integration
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ A2UI uses an adjacency list model - a flat list of components with ID references
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { defineCatalog } from '@json-render/core';
|
import { defineCatalog } from '@json-render/core';
|
||||||
import { schema } from '@json-render/react';
|
import { schema } from '@json-render/react/schema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
// A2UI BoundValue schema
|
// A2UI BoundValue schema
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Adaptive Cards Integration" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/adaptive-cards")
|
||||||
|
|
||||||
# Adaptive Cards Integration
|
# Adaptive Cards Integration
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ Define a catalog matching the Adaptive Cards element types:
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { defineCatalog } from '@json-render/core';
|
import { defineCatalog } from '@json-render/core';
|
||||||
import { schema } from '@json-render/react';
|
import { schema } from '@json-render/react/schema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
// Common Adaptive Cards properties
|
// Common Adaptive Cards properties
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "AG-UI Integration" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/ag-ui")
|
||||||
|
|
||||||
# AG-UI Integration
|
# AG-UI Integration
|
||||||
|
|
||||||
@@ -150,7 +151,7 @@ Create a catalog for UI components that agents can render:
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { defineCatalog } from '@json-render/core';
|
import { defineCatalog } from '@json-render/core';
|
||||||
import { schema } from '@json-render/react';
|
import { schema } from '@json-render/react/schema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const aguiCatalog = defineCatalog(schema, {
|
export const aguiCatalog = defineCatalog(schema, {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "AI SDK Integration" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/ai-sdk")
|
||||||
|
|
||||||
# AI SDK Integration
|
# AI SDK Integration
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "@json-render/codegen API" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/codegen")
|
||||||
|
|
||||||
# @json-render/codegen
|
# @json-render/codegen
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "@json-render/core API" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/core")
|
||||||
|
|
||||||
# @json-render/core
|
# @json-render/core
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ Creates a type-safe catalog definition with schema validation.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { defineCatalog } from '@json-render/core';
|
import { defineCatalog } from '@json-render/core';
|
||||||
import { schema } from '@json-render/react';
|
import { schema } from '@json-render/react/schema';
|
||||||
|
|
||||||
function defineCatalog<T extends ZodType>(
|
function defineCatalog<T extends ZodType>(
|
||||||
s: T,
|
s: T,
|
||||||
@@ -118,7 +119,7 @@ The schema for flat UI element trees. This is exported from @json-render/react.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { defineCatalog } from '@json-render/core';
|
import { defineCatalog } from '@json-render/core';
|
||||||
import { schema } from '@json-render/react';
|
import { schema } from '@json-render/react/schema';
|
||||||
|
|
||||||
// schema defines:
|
// schema defines:
|
||||||
// - Spec shape: { root: string, elements: Record<string, UIElement> }
|
// - Spec shape: { root: string, elements: Record<string, UIElement> }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "@json-render/react-native API" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/react-native")
|
||||||
|
|
||||||
# @json-render/react-native
|
# @json-render/react-native
|
||||||
|
|
||||||
@@ -62,11 +63,36 @@ React Native renderer with standard components, providers, and hooks.
|
|||||||
### StateProvider
|
### StateProvider
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<StateProvider initialState={object}>
|
<StateProvider initialState={object} onStateChange={fn}>
|
||||||
{children}
|
{children}
|
||||||
</StateProvider>
|
</StateProvider>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `store` | `StateStore` | External store (controlled mode). When provided, `initialState` and `onStateChange` are ignored. |
|
||||||
|
| `initialState` | `Record<string, unknown>` | Initial state model (uncontrolled mode). |
|
||||||
|
| `onStateChange` | `(changes: Array<{ path: string; value: unknown }>) => void` | Callback when state changes (uncontrolled mode). Called once per `set` or `update` with all changed entries. |
|
||||||
|
|
||||||
|
#### External Store (Controlled Mode)
|
||||||
|
|
||||||
|
Pass a `StateStore` to bypass the internal state and wire json-render to any state management library:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createStateStore, type StateStore } from "@json-render/react-native";
|
||||||
|
|
||||||
|
const store = createStateStore({ count: 0 });
|
||||||
|
|
||||||
|
<StateProvider store={store}>
|
||||||
|
{children}
|
||||||
|
</StateProvider>
|
||||||
|
|
||||||
|
// Mutate from anywhere — components re-render automatically:
|
||||||
|
store.set("/count", 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
The `store` prop is also available on `JSONUIProvider` and `createRenderer`.
|
||||||
|
|
||||||
### ActionProvider
|
### ActionProvider
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
|
|||||||
@@ -0,0 +1,439 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/react-pdf")
|
||||||
|
|
||||||
|
# @json-render/react-pdf
|
||||||
|
|
||||||
|
PDF document renderer. Turn JSON specs into PDFs using `@react-pdf/renderer`.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/core @json-render/react-pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [React PDF example](https://github.com/vercel-labs/json-render/tree/main/examples/react-pdf) for a full working example.
|
||||||
|
|
||||||
|
## schema
|
||||||
|
|
||||||
|
The PDF element schema for document specs. Use with `defineCatalog` from core.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineCatalog } from '@json-render/core';
|
||||||
|
import { schema, standardComponentDefinitions } from '@json-render/react-pdf';
|
||||||
|
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: standardComponentDefinitions,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Render Functions
|
||||||
|
|
||||||
|
Server-side functions for producing PDF output. All accept a spec and optional `RenderOptions`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { renderToBuffer, renderToStream, renderToFile } from '@json-render/react-pdf';
|
||||||
|
|
||||||
|
const buffer = await renderToBuffer(spec);
|
||||||
|
|
||||||
|
const stream = await renderToStream(spec);
|
||||||
|
stream.pipe(res);
|
||||||
|
|
||||||
|
await renderToFile(spec, './output.pdf');
|
||||||
|
```
|
||||||
|
|
||||||
|
### RenderOptions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface RenderOptions {
|
||||||
|
registry?: ComponentRegistry;
|
||||||
|
includeStandard?: boolean; // default: true
|
||||||
|
state?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Option</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>registry</code></td>
|
||||||
|
<td>Custom component map (merged with standard components)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>includeStandard</code></td>
|
||||||
|
<td>Include built-in standard components (default: <code>true</code>)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>state</code></td>
|
||||||
|
<td>Initial state for <code>$state</code> / <code>$cond</code> dynamic prop resolution</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## defineRegistry
|
||||||
|
|
||||||
|
Create a type-safe component registry from a catalog. Components receive `{ props, children, emit, bindings, loading }`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { defineRegistry } from '@json-render/react-pdf';
|
||||||
|
import { View, Text } from '@react-pdf/renderer';
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Badge: ({ props }) => (
|
||||||
|
<View style={{ backgroundColor: props.color ?? '#e5e7eb', padding: 4, borderRadius: 4 }}>
|
||||||
|
<Text style={{ fontSize: 10 }}>{props.label}</Text>
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const buffer = await renderToBuffer(spec, { registry });
|
||||||
|
```
|
||||||
|
|
||||||
|
## createRenderer
|
||||||
|
|
||||||
|
Create a standalone renderer component wired to state, actions, and validation.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createRenderer } from '@json-render/react-pdf';
|
||||||
|
|
||||||
|
const PDFRenderer = createRenderer(catalog, components);
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface CreateRendererProps {
|
||||||
|
spec: Spec | null;
|
||||||
|
store?: StateStore;
|
||||||
|
state?: Record<string, unknown>;
|
||||||
|
onAction?: (actionName: string, params?: Record<string, unknown>) => void;
|
||||||
|
onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
|
||||||
|
loading?: boolean;
|
||||||
|
fallback?: ComponentRenderer;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When `store` is provided, `state` and `onStateChange` are ignored (controlled mode).
|
||||||
|
|
||||||
|
## Renderer
|
||||||
|
|
||||||
|
The main component that renders a spec to `@react-pdf/renderer` elements.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface RendererProps {
|
||||||
|
spec: Spec | null;
|
||||||
|
registry?: ComponentRegistry;
|
||||||
|
includeStandard?: boolean; // default: true
|
||||||
|
loading?: boolean;
|
||||||
|
fallback?: ComponentRenderer;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Standard Components
|
||||||
|
|
||||||
|
### Document Structure
|
||||||
|
|
||||||
|
#### Document
|
||||||
|
|
||||||
|
Top-level PDF wrapper. Must be the root element. Children must be `Page` components.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
title: string | null;
|
||||||
|
author: string | null;
|
||||||
|
subject: string | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Page
|
||||||
|
|
||||||
|
A page in the document with configurable size, orientation, and margins.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
size: "A4" | "A3" | "A5" | "LETTER" | "LEGAL" | "TABLOID" | null;
|
||||||
|
orientation: "portrait" | "landscape" | null;
|
||||||
|
marginTop: number | null;
|
||||||
|
marginBottom: number | null;
|
||||||
|
marginLeft: number | null;
|
||||||
|
marginRight: number | null;
|
||||||
|
backgroundColor: string | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
#### View
|
||||||
|
|
||||||
|
Generic container with padding, margin, background, border, and flex alignment.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
padding: number | null;
|
||||||
|
paddingTop: number | null;
|
||||||
|
paddingBottom: number | null;
|
||||||
|
paddingLeft: number | null;
|
||||||
|
paddingRight: number | null;
|
||||||
|
margin: number | null;
|
||||||
|
backgroundColor: string | null;
|
||||||
|
borderWidth: number | null;
|
||||||
|
borderColor: string | null;
|
||||||
|
borderRadius: number | null;
|
||||||
|
flex: number | null;
|
||||||
|
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
|
||||||
|
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Row
|
||||||
|
|
||||||
|
Horizontal flex layout with optional wrapping.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
gap: number | null;
|
||||||
|
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
|
||||||
|
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
|
||||||
|
padding: number | null;
|
||||||
|
flex: number | null;
|
||||||
|
wrap: boolean | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Column
|
||||||
|
|
||||||
|
Vertical flex layout.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
gap: number | null;
|
||||||
|
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
|
||||||
|
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
|
||||||
|
padding: number | null;
|
||||||
|
flex: number | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content
|
||||||
|
|
||||||
|
#### Heading
|
||||||
|
|
||||||
|
h1-h4 heading text with configurable color and alignment.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
text: string;
|
||||||
|
level: "h1" | "h2" | "h3" | "h4" | null;
|
||||||
|
color: string | null;
|
||||||
|
align: "left" | "center" | "right" | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Text
|
||||||
|
|
||||||
|
Body text with full styling control.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
text: string;
|
||||||
|
fontSize: number | null;
|
||||||
|
color: string | null;
|
||||||
|
align: "left" | "center" | "right" | null;
|
||||||
|
fontWeight: "normal" | "bold" | null;
|
||||||
|
fontStyle: "normal" | "italic" | null;
|
||||||
|
lineHeight: number | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Image
|
||||||
|
|
||||||
|
Image from a URL with optional dimensions and fit.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
src: string;
|
||||||
|
width: number | null;
|
||||||
|
height: number | null;
|
||||||
|
objectFit: "contain" | "cover" | "fill" | "none" | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Link
|
||||||
|
|
||||||
|
Hyperlink with visible text.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
text: string;
|
||||||
|
href: string;
|
||||||
|
fontSize: number | null;
|
||||||
|
color: string | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data
|
||||||
|
|
||||||
|
#### Table
|
||||||
|
|
||||||
|
Data table with typed columns and string rows. Supports header styling and striped rows.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
columns: { header: string; width?: string; align?: "left" | "center" | "right" }[];
|
||||||
|
rows: string[][];
|
||||||
|
headerBackgroundColor: string | null;
|
||||||
|
headerTextColor: string | null;
|
||||||
|
borderColor: string | null;
|
||||||
|
fontSize: number | null;
|
||||||
|
striped: boolean | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### List
|
||||||
|
|
||||||
|
Ordered or unordered list.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
items: string[];
|
||||||
|
ordered: boolean | null;
|
||||||
|
fontSize: number | null;
|
||||||
|
color: string | null;
|
||||||
|
spacing: number | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decorative
|
||||||
|
|
||||||
|
#### Divider
|
||||||
|
|
||||||
|
Horizontal line separator.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
color: string | null;
|
||||||
|
thickness: number | null;
|
||||||
|
marginTop: number | null;
|
||||||
|
marginBottom: number | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Spacer
|
||||||
|
|
||||||
|
Empty vertical space.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
height: number | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Page-Level
|
||||||
|
|
||||||
|
#### PageNumber
|
||||||
|
|
||||||
|
Renders current page number and total pages. Format uses `{pageNumber}` and `{totalPages}` placeholders.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
format: string | null; // default: "{pageNumber} / {totalPages}"
|
||||||
|
fontSize: number | null;
|
||||||
|
color: string | null;
|
||||||
|
align: "left" | "center" | "right" | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## External Store (Controlled Mode)
|
||||||
|
|
||||||
|
Pass a `StateStore` to `StateProvider`, `JSONUIProvider`, or `createRenderer` for full control over state:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createStateStore, type StateStore } from "@json-render/react-pdf";
|
||||||
|
|
||||||
|
const store = createStateStore({ invoice: { total: 100 } });
|
||||||
|
store.set("/invoice/total", 200);
|
||||||
|
```
|
||||||
|
|
||||||
|
When `store` is provided, `initialState` / `state` and `onStateChange` are ignored.
|
||||||
|
|
||||||
|
## Server-Safe Import
|
||||||
|
|
||||||
|
Import schema and catalog definitions without pulling in React or `@react-pdf/renderer`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { schema, standardComponentDefinitions } from '@json-render/react-pdf/server';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sub-path Exports
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Export</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>@json-render/react-pdf</code></td>
|
||||||
|
<td>Full package: schema, renderer, components, render functions</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>@json-render/react-pdf/server</code></td>
|
||||||
|
<td>Schema and catalog definitions only (no React)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>@json-render/react-pdf/catalog</code></td>
|
||||||
|
<td>Standard component definitions and types</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>@json-render/react-pdf/render</code></td>
|
||||||
|
<td>Server-side render functions only</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Export</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>ReactPdfSchema</code></td>
|
||||||
|
<td>Schema type for PDF specs</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>ReactPdfSpec</code></td>
|
||||||
|
<td>Spec type for PDF documents</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>RenderOptions</code></td>
|
||||||
|
<td>Options for render functions</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>ComponentContext</code></td>
|
||||||
|
<td>Typed component render function context</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>ComponentFn</code></td>
|
||||||
|
<td>Component render function type</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>StandardComponentDefinitions</code></td>
|
||||||
|
<td>Type of the standard component definitions object</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>StandardComponentProps<K></code></td>
|
||||||
|
<td>Inferred props type for a standard component by name</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "@json-render/react API" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/react")
|
||||||
|
|
||||||
# @json-render/react
|
# @json-render/react
|
||||||
|
|
||||||
@@ -9,11 +10,36 @@ React components, providers, and hooks.
|
|||||||
### StateProvider
|
### StateProvider
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<StateProvider initialState={object}>
|
<StateProvider initialState={object} onStateChange={fn}>
|
||||||
{children}
|
{children}
|
||||||
</StateProvider>
|
</StateProvider>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `store` | `StateStore` | External store (controlled mode). When provided, `initialState` and `onStateChange` are ignored. |
|
||||||
|
| `initialState` | `Record<string, unknown>` | Initial state model (uncontrolled mode). |
|
||||||
|
| `onStateChange` | `(changes: Array<{ path: string; value: unknown }>) => void` | Callback when state changes (uncontrolled mode). Called once per `set` or `update` with all changed entries. |
|
||||||
|
|
||||||
|
#### External Store (Controlled Mode)
|
||||||
|
|
||||||
|
Pass a `StateStore` to bypass the internal state and wire json-render to any state management library:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createStateStore, type StateStore } from "@json-render/react";
|
||||||
|
|
||||||
|
const store = createStateStore({ count: 0 });
|
||||||
|
|
||||||
|
<StateProvider store={store}>
|
||||||
|
{children}
|
||||||
|
</StateProvider>
|
||||||
|
|
||||||
|
// Mutate from anywhere — React re-renders automatically:
|
||||||
|
store.set("/count", 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
The `store` prop is also available on `JSONUIProvider` and `createRenderer`.
|
||||||
|
|
||||||
### ActionProvider
|
### ActionProvider
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
@@ -83,6 +109,27 @@ const { registry } = defineRegistry(catalog, {
|
|||||||
type Registry = Record<string, React.ComponentType<ComponentRenderProps>>;
|
type Registry = Record<string, React.ComponentType<ComponentRenderProps>>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### JSONUIProvider
|
||||||
|
|
||||||
|
Convenience wrapper that combines `StateProvider`, `VisibilityProvider`, `ValidationProvider`, and `ActionProvider`. Accepts all their props plus:
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `functions` | `Record<string, ComputedFunction>` | Named functions for `$computed` expressions in props |
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<JSONUIProvider
|
||||||
|
spec={spec}
|
||||||
|
catalog={catalog}
|
||||||
|
handlers={{ submit: async () => { /* ... */ } }}
|
||||||
|
functions={{ fullName: (args) => `${args.first} ${args.last}` }}
|
||||||
|
>
|
||||||
|
<Renderer spec={spec} registry={registry} />
|
||||||
|
</JSONUIProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `functions` prop is also available on `createRenderer`.
|
||||||
|
|
||||||
### Component Props (via defineRegistry)
|
### Component Props (via defineRegistry)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
@@ -211,6 +258,15 @@ const {
|
|||||||
|
|
||||||
`ValidationConfig` is `{ checks?: ValidationCheck[], validateOn?: 'change' | 'blur' | 'submit' }`.
|
`ValidationConfig` is `{ checks?: ValidationCheck[], validateOn?: 'change' | 'blur' | 'submit' }`.
|
||||||
|
|
||||||
|
### useOptionalValidation
|
||||||
|
|
||||||
|
Non-throwing variant of `useValidation()`. Returns `null` when no `ValidationProvider` is present, instead of throwing. Useful in components that may or may not be rendered inside a validation context.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const validation = useOptionalValidation();
|
||||||
|
// ValidationContextValue | null
|
||||||
|
```
|
||||||
|
|
||||||
### useBoundProp
|
### useBoundProp
|
||||||
|
|
||||||
Two-way binding helper for `$bindState` / `$bindItem` expressions. Returns `[value, setValue]` where `setValue` writes back to the bound state path.
|
Two-way binding helper for `$bindState` / `$bindItem` expressions. Returns `[value, setValue]` where `setValue` writes back to the bound state path.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "@json-render/remotion API" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/remotion")
|
||||||
|
|
||||||
# @json-render/remotion
|
# @json-render/remotion
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "@json-render/shadcn API" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/shadcn")
|
||||||
|
|
||||||
# @json-render/shadcn
|
# @json-render/shadcn
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/api/vue")
|
||||||
|
|
||||||
|
# @json-render/vue
|
||||||
|
|
||||||
|
Vue 3 components, providers, and composables.
|
||||||
|
|
||||||
|
## Providers
|
||||||
|
|
||||||
|
### StateProvider
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<StateProvider :initial-state="object" :on-state-change="fn">
|
||||||
|
<!-- children -->
|
||||||
|
</StateProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `store` | `StateStore` | External store (controlled mode). When provided, `initialState` and `onStateChange` are ignored. |
|
||||||
|
| `initialState` | `Record<string, unknown>` | Initial state model (uncontrolled mode). |
|
||||||
|
| `onStateChange` | `(changes: Array<{ path: string; value: unknown }>) => void` | Callback when state changes (uncontrolled mode). Called once per `set` or `update` with all changed entries. |
|
||||||
|
|
||||||
|
#### External Store (Controlled Mode)
|
||||||
|
|
||||||
|
Pass a `StateStore` to bypass the internal state and wire json-render to any state management library:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createStateStore, type StateStore } from "@json-render/vue";
|
||||||
|
|
||||||
|
const store = createStateStore({ count: 0 });
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<StateProvider :store="store">
|
||||||
|
<!-- children -->
|
||||||
|
</StateProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Mutate from anywhere — Vue re-renders automatically:
|
||||||
|
store.set("/count", 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
### ActionProvider
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<ActionProvider :handlers="Record<string, ActionHandler>" :navigate="fn">
|
||||||
|
<!-- children -->
|
||||||
|
</ActionProvider>
|
||||||
|
|
||||||
|
// type ActionHandler = (params: Record<string, unknown>) => void | Promise<void>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### VisibilityProvider
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<VisibilityProvider>
|
||||||
|
<!-- children -->
|
||||||
|
</VisibilityProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
`VisibilityProvider` reads state from the parent `StateProvider` automatically. Conditions in specs use the `VisibilityCondition` format with `$state` paths (e.g. `{ "$state": "/path" }`, `{ "$state": "/path", "eq": value }`). See [visibility](/docs/visibility) for the full syntax.
|
||||||
|
|
||||||
|
### ValidationProvider
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<ValidationProvider :custom-functions="Record<string, ValidationFunction>">
|
||||||
|
<!-- children -->
|
||||||
|
</ValidationProvider>
|
||||||
|
|
||||||
|
// type ValidationFunction = (value: unknown, args?: object) => boolean | Promise<boolean>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## defineRegistry
|
||||||
|
|
||||||
|
Create a type-safe component registry from a catalog. Components receive `props`, `children`, `emit`, `on`, and `loading` with catalog-inferred types.
|
||||||
|
|
||||||
|
When the catalog declares actions, the `actions` field is required. When the catalog has no actions (e.g. `actions: {}`), the field is optional. When passing stubs, any `async () => {}` is sufficient.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { h } from "vue";
|
||||||
|
import { defineRegistry } from "@json-render/vue";
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card: ({ props, children }) =>
|
||||||
|
h("div", { class: "card" }, [h("h3", null, props.title), children]),
|
||||||
|
Button: ({ props, emit }) =>
|
||||||
|
h("button", { onClick: () => emit("press") }, props.label),
|
||||||
|
},
|
||||||
|
// Required when catalog declares actions:
|
||||||
|
actions: {
|
||||||
|
submit: async (params) => { /* ... */ },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pass to <Renderer>
|
||||||
|
// <Renderer :spec="spec" :registry="registry" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Renderer
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<Renderer
|
||||||
|
:spec="Spec" // The UI spec to render
|
||||||
|
:registry="Registry" // Component registry (from defineRegistry)
|
||||||
|
:loading="boolean" // Optional loading state
|
||||||
|
:fallback="Component" // Optional fallback for unknown types
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Props (via defineRegistry)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { VNode } from "vue";
|
||||||
|
|
||||||
|
interface ComponentContext<P> {
|
||||||
|
props: P; // Typed props from catalog
|
||||||
|
children?: VNode | VNode[]; // Rendered children (for container components)
|
||||||
|
emit: (event: string) => void; // Emit a named event (always defined)
|
||||||
|
on: (event: string) => EventHandle; // Get event handle with metadata
|
||||||
|
loading?: boolean;
|
||||||
|
bindings?: Record<string, string>; // State paths from $bindState/$bindItem expressions
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventHandle {
|
||||||
|
emit: () => void; // Fire the event
|
||||||
|
shouldPreventDefault: boolean; // Whether any binding requested preventDefault
|
||||||
|
bound: boolean; // Whether any handler is bound
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `emit("press")` for simple event firing. Use `on("click")` when you need metadata like `shouldPreventDefault`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
Link: ({ props, on }) => {
|
||||||
|
const click = on("click");
|
||||||
|
return h("a", {
|
||||||
|
href: props.href,
|
||||||
|
onClick: (e: MouseEvent) => {
|
||||||
|
if (click.shouldPreventDefault) e.preventDefault();
|
||||||
|
click.emit();
|
||||||
|
},
|
||||||
|
}, props.label);
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### BaseComponentProps
|
||||||
|
|
||||||
|
Catalog-agnostic base type for building reusable component libraries that are not tied to a specific catalog:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { BaseComponentProps } from "@json-render/vue";
|
||||||
|
|
||||||
|
const Card = ({ props, children }: BaseComponentProps<{ title?: string }>) =>
|
||||||
|
h("div", null, [props.title, children]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Composables
|
||||||
|
|
||||||
|
### useStateStore
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const {
|
||||||
|
state, // ShallowRef<StateModel> — access with state.value
|
||||||
|
get, // (path: string) => unknown
|
||||||
|
set, // (path: string, value: unknown) => void
|
||||||
|
update, // (updates: Record<string, unknown>) => void
|
||||||
|
} = useStateStore();
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** `state` is a `ShallowRef<StateModel>`, not a plain object. Use `state.value` to read the current state. This differs from the React renderer.
|
||||||
|
|
||||||
|
### useStateValue
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const value = useStateValue(path: string); // ComputedRef<T | undefined>
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns a `ComputedRef` that automatically updates when the state at `path` changes. Use `.value` to access the current value.
|
||||||
|
|
||||||
|
### useStateBinding (deprecated)
|
||||||
|
|
||||||
|
> **Deprecated.** Use `$bindState` expressions with `bindings` prop instead.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const [value, setValue] = useStateBinding(path: string);
|
||||||
|
// value: ComputedRef<T | undefined>
|
||||||
|
// setValue: (value: T) => void
|
||||||
|
```
|
||||||
|
|
||||||
|
### useActions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { execute } = useActions();
|
||||||
|
// execute(binding: ActionBinding) => Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
### useAction
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { execute, isLoading } = useAction(binding: ActionBinding);
|
||||||
|
// execute: () => Promise<void>
|
||||||
|
// isLoading: ComputedRef<boolean>
|
||||||
|
```
|
||||||
|
|
||||||
|
### useIsVisible
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const isVisible = useIsVisible(condition?: VisibilityCondition);
|
||||||
|
```
|
||||||
|
|
||||||
|
### useFieldValidation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const {
|
||||||
|
state, // ComputedRef<FieldValidationState>
|
||||||
|
validate, // () => ValidationResult
|
||||||
|
touch, // () => void
|
||||||
|
clear, // () => void
|
||||||
|
errors, // ComputedRef<string[]>
|
||||||
|
isValid, // ComputedRef<boolean>
|
||||||
|
} = useFieldValidation(path: string, config?: ValidationConfig);
|
||||||
|
```
|
||||||
|
|
||||||
|
`ValidationConfig` is `{ checks?: ValidationCheck[], validateOn?: 'change' | 'blur' | 'submit' }`.
|
||||||
|
|
||||||
|
## Differences from `@json-render/react`
|
||||||
|
|
||||||
|
| API | React | Vue | Note |
|
||||||
|
|-----|-------|-----|------|
|
||||||
|
| `useStateStore().state` | `StateModel` (plain object) | `ShallowRef<StateModel>` | Vue reactivity; use `state.value` |
|
||||||
|
| `useStateValue()` | `T \| undefined` | `ComputedRef<T \| undefined>` | Vue reactivity; use `.value` |
|
||||||
|
| `useStateBinding()` | `[T \| undefined, setter]` | `[ComputedRef<T \| undefined>, setter]` | Vue reactivity; use `value.value` |
|
||||||
|
| `useAction().isLoading` | `boolean` | `ComputedRef<boolean>` | Vue reactivity; use `.value` |
|
||||||
|
| `useFieldValidation().state` | `FieldValidationState` | `ComputedRef<FieldValidationState>` | Vue reactivity; use `.value` |
|
||||||
|
| `useFieldValidation().errors` | `string[]` | `ComputedRef<string[]>` | Vue reactivity; use `.value` |
|
||||||
|
| `useFieldValidation().isValid` | `boolean` | `ComputedRef<boolean>` | Vue reactivity; use `.value` |
|
||||||
|
| `VisibilityContextValue.ctx` | `CoreVisibilityContext` | `ComputedRef<CoreVisibilityContext>` | Vue reactivity; use `ctx.value` |
|
||||||
|
| `children` type | `React.ReactNode` | `VNode \| VNode[]` | Platform-specific |
|
||||||
|
| `useBoundProp` | exported | exported | Same API; returns `[value, setValue]` |
|
||||||
|
| `VisibilityProviderProps` | exported | not exported (no props) | Vue uses slot, no prop needed |
|
||||||
|
| Streaming hooks | `useUIStream`, `useChatUI` | `useUIStream`, `useChatUI` | Same API; returns Vue `Ref` values |
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Catalog" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/catalog")
|
||||||
|
|
||||||
# Catalog
|
# Catalog
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ A catalog is the vocabulary for your UI. While the [schema](/docs/schemas) defin
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { defineCatalog } from '@json-render/core';
|
import { defineCatalog } from '@json-render/core';
|
||||||
import { schema } from '@json-render/react'; // or '@json-render/react-native'
|
import { schema } from '@json-render/react/schema'; // or '@json-render/react-native/schema'
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const catalog = defineCatalog(schema, {
|
const catalog = defineCatalog(schema, {
|
||||||
|
|||||||
@@ -1,9 +1,187 @@
|
|||||||
export const metadata = { title: "Changelog" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/changelog")
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
Notable changes and updates to json-render.
|
Notable changes and updates to json-render.
|
||||||
|
|
||||||
|
## v0.10.0
|
||||||
|
|
||||||
|
February 2026
|
||||||
|
|
||||||
|
### New: `@json-render/vue`
|
||||||
|
|
||||||
|
Vue 3 renderer for json-render with full feature parity with `@json-render/react`. Data binding, visibility conditions, actions, validation, repeat scopes, streaming, and external store support.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/core @json-render/vue
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { h } from "vue";
|
||||||
|
import { defineRegistry, Renderer } from "@json-render/vue";
|
||||||
|
import { schema } from "@json-render/vue/schema";
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card: ({ props, children }) =>
|
||||||
|
h("div", { class: "card" }, [h("h3", null, props.title), children]),
|
||||||
|
Button: ({ props, emit }) =>
|
||||||
|
h("button", { onClick: () => emit("press") }, props.label),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Providers: `StateProvider`, `ActionProvider`, `VisibilityProvider`, `ValidationProvider`. Composables: `useStateStore`, `useStateValue`, `useActions`, `useAction`, `useIsVisible`, `useFieldValidation`, `useBoundProp`, `useUIStream`, `useChatUI`.
|
||||||
|
|
||||||
|
See the [Vue API reference](/docs/api/vue) for details.
|
||||||
|
|
||||||
|
### New: `@json-render/xstate`
|
||||||
|
|
||||||
|
[XState Store](https://stately.ai/docs/xstate-store) (atom) adapter for json-render's `StateStore` interface. Wire an `@xstate/store` atom as the state backend for any renderer.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/xstate @xstate/store
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createAtom } from "@xstate/store";
|
||||||
|
import { xstateStoreStateStore } from "@json-render/xstate";
|
||||||
|
|
||||||
|
const atom = createAtom({ count: 0 });
|
||||||
|
const store = xstateStoreStateStore({ atom });
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires `@xstate/store` v3+.
|
||||||
|
|
||||||
|
### New: `$computed` and `$template` Expressions
|
||||||
|
|
||||||
|
Two new prop expression types for dynamic values:
|
||||||
|
|
||||||
|
- **`$template`** -- interpolate state values into strings: `{ "$template": "Hello, ${/user/name}!" }`
|
||||||
|
- **`$computed`** -- call registered functions: `{ "$computed": "fullName", "args": { "first": { "$state": "/form/firstName" } } }`
|
||||||
|
|
||||||
|
Register functions via the `functions` prop on `JSONUIProvider` or `createRenderer`. See [Computed Values](/docs/computed-values) for details.
|
||||||
|
|
||||||
|
### New: State Watchers
|
||||||
|
|
||||||
|
Elements can declare a `watch` field to trigger actions when state values change. Useful for cascading dependencies like country/city selects.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "Select",
|
||||||
|
"props": { "value": { "$bindState": "/form/country" }, "options": ["US", "Canada"] },
|
||||||
|
"watch": {
|
||||||
|
"/form/country": { "action": "loadCities", "params": { "country": { "$state": "/form/country" } } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`watch` is a top-level field on elements (sibling of type/props/children), not inside props. Watchers only fire on value changes, not on initial render. See [Watchers](/docs/watchers) for details.
|
||||||
|
|
||||||
|
### New: Cross-Field Validation
|
||||||
|
|
||||||
|
New built-in validation functions for cross-field comparisons:
|
||||||
|
|
||||||
|
- `equalTo` -- alias for `matches` with clearer semantics
|
||||||
|
- `lessThan` -- value must be less than another field
|
||||||
|
- `greaterThan` -- value must be greater than another field
|
||||||
|
- `requiredIf` -- required only when a condition field is truthy
|
||||||
|
|
||||||
|
Validation check args now resolve through `resolvePropValue`, so `$state` expressions work consistently.
|
||||||
|
|
||||||
|
### New: `validateForm` Action
|
||||||
|
|
||||||
|
Built-in action (React) that validates all registered form fields at once and writes `{ valid, errors }` to state:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"on": {
|
||||||
|
"press": [
|
||||||
|
{ "action": "validateForm", "params": { "statePath": "/formResult" } },
|
||||||
|
{ "action": "submitForm" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Improved: shadcn/ui Validation
|
||||||
|
|
||||||
|
All form components now support `checks` and `validateOn` props:
|
||||||
|
- Checkbox, Radio, Switch added validation support
|
||||||
|
- `validateOn` controls timing: `"change"` (default for Select, Checkbox, Radio, Switch), `"blur"` (default for Input, Textarea), or `"submit"`
|
||||||
|
|
||||||
|
### New Examples
|
||||||
|
|
||||||
|
- **Vue example** -- standalone Vue 3 app with custom components
|
||||||
|
- **Vite Renderers** -- side-by-side React and Vue renderers with shared catalog
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.9.1
|
||||||
|
|
||||||
|
February 2026
|
||||||
|
|
||||||
|
### Fixed: Install failure due to private dependency
|
||||||
|
|
||||||
|
`@json-render/react`, `@json-render/react-pdf`, and `@json-render/react-native` v0.9.0 failed to install because `@internal/react-state` (a private workspace package) was published as a dependency. The internal package is now bundled into each renderer at build time, so it no longer needs to be resolved from npm.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.9.0
|
||||||
|
|
||||||
|
February 2026
|
||||||
|
|
||||||
|
### New: External State Store
|
||||||
|
|
||||||
|
The `StateStore` interface lets you plug in your own state management (Redux, Zustand, Jotai, XState, etc.) instead of the built-in internal store. Pass a `store` prop to `StateProvider`, `JSONUIProvider`, or `createRenderer` for controlled mode.
|
||||||
|
|
||||||
|
- Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`
|
||||||
|
- `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop
|
||||||
|
- When `store` is provided, it becomes the single source of truth (`initialState`/`onStateChange` are ignored)
|
||||||
|
- When `store` is omitted, everything works exactly as before (fully backward compatible)
|
||||||
|
- Applied across all platform packages: react, react-native, react-pdf
|
||||||
|
- Store utilities (`createStoreAdapter`, `immutableSetByPath`, `flattenToPointers`) available via `@json-render/core/store-utils` for building custom adapters
|
||||||
|
|
||||||
|
New adapter packages: `@json-render/redux`, `@json-render/zustand`, `@json-render/jotai`.
|
||||||
|
|
||||||
|
See the [Data Binding](/docs/data-binding#external-store-controlled-mode) guide for usage.
|
||||||
|
|
||||||
|
### Changed: `onStateChange` signature updated (breaking)
|
||||||
|
|
||||||
|
The `onStateChange` callback now receives a single array of changed entries instead of being called once per path. This makes batch updates via `update()` easier to handle:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Before
|
||||||
|
onStateChange?: (path: string, value: unknown) => void
|
||||||
|
|
||||||
|
// After
|
||||||
|
onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void
|
||||||
|
```
|
||||||
|
|
||||||
|
The callback is only called when a `set()` or `update()` call actually changes the state. A `set()` call produces a single-element array; an `update()` call produces one array with all changed paths.
|
||||||
|
|
||||||
|
### Fixed: Server-safe schema import
|
||||||
|
|
||||||
|
`@json-render/react` barrel-imports React contexts that call `createContext`, which crashes in Next.js App Router API routes (RSC runtime strips `createContext`). All docs, examples, and skills now import `schema` from `@json-render/react/schema` instead of `@json-render/react`.
|
||||||
|
|
||||||
|
For combined imports, split into separate `schema` (subpath) and client API (main entry) lines:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { schema } from "@json-render/react/schema";
|
||||||
|
import { defineRegistry, Renderer } from "@json-render/react";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fixed: Chaining actions
|
||||||
|
|
||||||
|
Fixed an issue where chaining multiple actions on the same event (e.g. `setState` followed by a custom action) did not execute all actions. Affected `@json-render/react`, `@json-render/react-native`, and `@json-render/react-pdf`.
|
||||||
|
|
||||||
|
### Fixed: Zod array inner type resolution
|
||||||
|
|
||||||
|
Fixed safely resolving the inner type for Zod arrays in schema introspection, preventing errors when catalog component props use `z.array()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.8.0
|
## v0.8.0
|
||||||
|
|
||||||
February 2026
|
February 2026
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Code Export" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/code-export")
|
||||||
|
|
||||||
# Code Export
|
# Code Export
|
||||||
|
|
||||||
@@ -134,6 +135,6 @@ Run the dashboard example and click "Export Project" to see code generation in a
|
|||||||
```bash
|
```bash
|
||||||
cd examples/dashboard
|
cd examples/dashboard
|
||||||
pnpm dev
|
pnpm dev
|
||||||
# Open http://localhost:3001
|
# Open http://dashboard-demo.json-render.localhost:1355
|
||||||
# Generate a widget, then click "Export Project"
|
# Generate a widget, then click "Export Project"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/computed-values")
|
||||||
|
|
||||||
|
# Computed Values
|
||||||
|
|
||||||
|
Derive dynamic prop values using registered functions or string templates.
|
||||||
|
|
||||||
|
## `$template` — String Interpolation
|
||||||
|
|
||||||
|
Use `{ "$template": "..." }` to embed state values into a string. References use `${/path}` syntax where the path is a JSON Pointer:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "Text",
|
||||||
|
"props": {
|
||||||
|
"text": { "$template": "Hello, ${/user/name}! You have ${/inbox/count} messages." }
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If state is `{ "user": { "name": "Alice" }, "inbox": { "count": 3 } }`, the text renders as "Hello, Alice! You have 3 messages."
|
||||||
|
|
||||||
|
Missing paths resolve to an empty string.
|
||||||
|
|
||||||
|
## `$computed` — Registered Functions
|
||||||
|
|
||||||
|
Use `{ "$computed": "<name>", "args": { ... } }` to call a named function registered in your catalog. Each arg can be a literal value or any prop expression (`$state`, `$item`, `$cond`, etc.):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "Text",
|
||||||
|
"props": {
|
||||||
|
"text": {
|
||||||
|
"$computed": "fullName",
|
||||||
|
"args": {
|
||||||
|
"first": { "$state": "/form/firstName" },
|
||||||
|
"last": { "$state": "/form/lastName" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registering Functions
|
||||||
|
|
||||||
|
Functions are registered in the catalog and provided at runtime.
|
||||||
|
|
||||||
|
**Catalog definition (for AI prompt generation):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineCatalog } from '@json-render/core';
|
||||||
|
import { schema } from '@json-render/react/schema';
|
||||||
|
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: { /* ... */ },
|
||||||
|
functions: {
|
||||||
|
fullName: {
|
||||||
|
description: 'Combines first and last name into a full name',
|
||||||
|
},
|
||||||
|
formatCurrency: {
|
||||||
|
description: 'Formats a number as currency',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Runtime implementation:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { JSONUIProvider } from '@json-render/react';
|
||||||
|
|
||||||
|
const functions = {
|
||||||
|
fullName: (args) => `${args.first ?? ''} ${args.last ?? ''}`.trim(),
|
||||||
|
formatCurrency: (args) => {
|
||||||
|
const value = Number(args.value ?? 0);
|
||||||
|
return new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: (args.currency as string) ?? 'USD',
|
||||||
|
}).format(value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
<JSONUIProvider registry={registry} functions={functions}>
|
||||||
|
<Renderer spec={spec} registry={registry} />
|
||||||
|
</JSONUIProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using with `createRenderer`
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const MyRenderer = createRenderer(catalog, components);
|
||||||
|
|
||||||
|
<MyRenderer
|
||||||
|
spec={spec}
|
||||||
|
functions={functions}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Combining Expressions
|
||||||
|
|
||||||
|
`$computed` args can use any expression type. This example computes a total from repeat item fields:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$computed": "lineTotal",
|
||||||
|
"args": {
|
||||||
|
"price": { "$item": "price" },
|
||||||
|
"quantity": { "$item": "quantity" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next
|
||||||
|
|
||||||
|
- [Watchers](/docs/watchers) — react to state changes with cascading actions
|
||||||
|
- [Data Binding](/docs/data-binding) — all expression types
|
||||||
|
- [Validation](/docs/validation) — validate form inputs
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Custom Schema & Renderer" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/custom-schema")
|
||||||
|
|
||||||
# Custom Schema & Renderer
|
# Custom Schema & Renderer
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Data Binding" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/data-binding")
|
||||||
|
|
||||||
# Data Binding
|
# Data Binding
|
||||||
|
|
||||||
@@ -212,6 +213,22 @@ Use `$cond` / `$then` / `$else` to pick a prop value based on a condition:
|
|||||||
|
|
||||||
The condition uses the same [visibility](/docs/visibility) expression format.
|
The condition uses the same [visibility](/docs/visibility) expression format.
|
||||||
|
|
||||||
|
## Template Strings
|
||||||
|
|
||||||
|
Use `{ "$template": "..." }` to interpolate state values into a string using `${/path}` syntax:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "Text",
|
||||||
|
"props": {
|
||||||
|
"text": { "$template": "Welcome back, ${/user/name}!" }
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Computed Values](/docs/computed-values) for details on `$template` and `$computed` expressions.
|
||||||
|
|
||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
<div className="my-6 overflow-x-auto">
|
<div className="my-6 overflow-x-auto">
|
||||||
@@ -254,10 +271,39 @@ The condition uses the same [visibility](/docs/visibility) expression format.
|
|||||||
<td><code>{'{ "$bindItem": "field" }'}</code></td>
|
<td><code>{'{ "$bindItem": "field" }'}</code></td>
|
||||||
<td>Form components inside repeat</td>
|
<td>Form components inside repeat</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>{"$template"}</code></td>
|
||||||
|
<td><code>{'{ "$template": "Hello, ${/name}!" }'}</code></td>
|
||||||
|
<td>Anywhere (string props)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>{"$computed"}</code></td>
|
||||||
|
<td><code>{'{ "$computed": "fn", "args": { ... } }'}</code></td>
|
||||||
|
<td>Anywhere (requires registered function)</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## External Store (Controlled Mode)
|
||||||
|
|
||||||
|
For advanced use cases, you can pass a `StateStore` to `StateProvider` to use your own state management (Redux, Zustand, XState, etc.) instead of the built-in internal store:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createStateStore, type StateStore } from "@json-render/react";
|
||||||
|
|
||||||
|
const store = createStateStore({ user: { name: "Alice" } });
|
||||||
|
|
||||||
|
<StateProvider store={store}>
|
||||||
|
{children}
|
||||||
|
</StateProvider>
|
||||||
|
|
||||||
|
// Mutate from anywhere — React re-renders automatically:
|
||||||
|
store.set("/user/name", "Bob");
|
||||||
|
```
|
||||||
|
|
||||||
|
When `store` is provided, `initialState` and `onStateChange` are ignored. The store is the single source of truth. See the [React API reference](/docs/api/react#external-store-controlled-mode) for the full `StateStore` interface.
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
- [Visibility](/docs/visibility) — conditionally show or hide elements
|
- [Visibility](/docs/visibility) — conditionally show or hide elements
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Generation Modes" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/generation-modes")
|
||||||
|
|
||||||
# Generation Modes
|
# Generation Modes
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Installation" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/installation")
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
@@ -8,6 +9,18 @@ Install the core package plus your renderer of choice.
|
|||||||
|
|
||||||
<PackageInstall packages="@json-render/core @json-render/react" />
|
<PackageInstall packages="@json-render/core @json-render/react" />
|
||||||
|
|
||||||
|
Peer dependencies: `react ^19.0.0` and `zod ^4.0.0`.
|
||||||
|
|
||||||
|
<PackageInstall packages="react zod" />
|
||||||
|
|
||||||
|
## For Vue
|
||||||
|
|
||||||
|
<PackageInstall packages="@json-render/core @json-render/vue" />
|
||||||
|
|
||||||
|
Peer dependencies: `vue ^3.5.0` and `zod ^4.0.0`.
|
||||||
|
|
||||||
|
<PackageInstall packages="vue zod" />
|
||||||
|
|
||||||
## For React UI with shadcn/ui
|
## For React UI with shadcn/ui
|
||||||
|
|
||||||
Pre-built components for fast prototyping and production use:
|
Pre-built components for fast prototyping and production use:
|
||||||
@@ -24,14 +37,19 @@ Requires Tailwind CSS in your project. See the [@json-render/shadcn API referenc
|
|||||||
|
|
||||||
<PackageInstall packages="@json-render/core @json-render/remotion remotion @remotion/player" />
|
<PackageInstall packages="@json-render/core @json-render/remotion remotion @remotion/player" />
|
||||||
|
|
||||||
## Peer Dependencies
|
## For External State Management (Optional)
|
||||||
|
|
||||||
json-render requires the following peer dependencies:
|
If you want to wire json-render to an existing state management library instead of the built-in store, install the adapter for your library:
|
||||||
|
|
||||||
- `react` ^19.0.0
|
<PackageInstall packages="@json-render/zustand" />
|
||||||
- `zod` ^4.0.0
|
|
||||||
|
|
||||||
<PackageInstall packages="react zod" />
|
<PackageInstall packages="@json-render/redux" />
|
||||||
|
|
||||||
|
<PackageInstall packages="@json-render/jotai" />
|
||||||
|
|
||||||
|
<PackageInstall packages="@json-render/xstate" />
|
||||||
|
|
||||||
|
See the [Data Binding](/docs/data-binding#external-store-controlled-mode) guide for usage.
|
||||||
|
|
||||||
## For AI Integration
|
## For AI Integration
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Migration Guide" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/migration")
|
||||||
|
|
||||||
# Migration Guide
|
# Migration Guide
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "OpenAPI Integration" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/openapi")
|
||||||
|
|
||||||
# OpenAPI Integration
|
# OpenAPI Integration
|
||||||
|
|
||||||
@@ -99,7 +100,7 @@ Create components that map to OpenAPI data types:
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { defineCatalog } from '@json-render/core';
|
import { defineCatalog } from '@json-render/core';
|
||||||
import { schema } from '@json-render/react';
|
import { schema } from '@json-render/react/schema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const openapiCatalog = defineCatalog(schema, {
|
export const openapiCatalog = defineCatalog(schema, {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Introduction" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs")
|
||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ A catalog declares what AI can use: components with typed props, actions with ty
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { defineCatalog } from '@json-render/core';
|
import { defineCatalog } from '@json-render/core';
|
||||||
import { schema } from '@json-render/react';
|
import { schema } from '@json-render/react/schema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const catalog = defineCatalog(schema, {
|
export const catalog = defineCatalog(schema, {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Quick Start" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/quick-start")
|
||||||
|
|
||||||
# Quick Start
|
# Quick Start
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ Create a catalog that defines what components AI can use:
|
|||||||
```typescript
|
```typescript
|
||||||
// lib/catalog.ts
|
// lib/catalog.ts
|
||||||
import { defineCatalog } from '@json-render/core';
|
import { defineCatalog } from '@json-render/core';
|
||||||
import { schema } from '@json-render/react';
|
import { schema } from '@json-render/react/schema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const catalog = defineCatalog(schema, {
|
export const catalog = defineCatalog(schema, {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Registry" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/registry")
|
||||||
|
|
||||||
# Registry
|
# Registry
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ Actions are declared in your [catalog](/docs/catalog). The `@json-render/react`
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { defineCatalog } from '@json-render/core';
|
import { defineCatalog } from '@json-render/core';
|
||||||
import { schema } from '@json-render/react';
|
import { schema } from '@json-render/react/schema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const catalog = defineCatalog(schema, {
|
const catalog = defineCatalog(schema, {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Schemas" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/schemas")
|
||||||
|
|
||||||
# Schemas
|
# Schemas
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Specs" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/specs")
|
||||||
|
|
||||||
# Specs
|
# Specs
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Streaming" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/streaming")
|
||||||
|
|
||||||
# Streaming
|
# Streaming
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Validation" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/validation")
|
||||||
|
|
||||||
# Validation
|
# Validation
|
||||||
|
|
||||||
@@ -10,11 +11,18 @@ json-render includes common validation functions:
|
|||||||
|
|
||||||
- `required` — Value must be non-empty
|
- `required` — Value must be non-empty
|
||||||
- `email` — Valid email format
|
- `email` — Valid email format
|
||||||
- `minLength` — Minimum string length
|
- `minLength` — Minimum string length (args: `{ "min": N }`)
|
||||||
- `maxLength` — Maximum string length
|
- `maxLength` — Maximum string length (args: `{ "max": N }`)
|
||||||
- `pattern` — Match a regex pattern
|
- `pattern` — Match a regex pattern (args: `{ "pattern": "regex" }`)
|
||||||
- `min` — Minimum numeric value
|
- `min` — Minimum numeric value (args: `{ "min": N }`)
|
||||||
- `max` — Maximum numeric value
|
- `max` — Maximum numeric value (args: `{ "max": N }`)
|
||||||
|
- `numeric` — Value must be a number
|
||||||
|
- `url` — Valid URL format
|
||||||
|
- `matches` — Must equal another field (args: `{ "other": { "$state": "/path" } }`)
|
||||||
|
- `equalTo` — Alias for matches (args: `{ "other": { "$state": "/path" } }`)
|
||||||
|
- `lessThan` — Value must be less than another field (args: `{ "other": { "$state": "/path" } }`)
|
||||||
|
- `greaterThan` — Value must be greater than another field (args: `{ "other": { "$state": "/path" } }`)
|
||||||
|
- `requiredIf` — Required only when another field is truthy (args: `{ "field": { "$state": "/path" } }`)
|
||||||
|
|
||||||
## Using Validation in JSON
|
## Using Validation in JSON
|
||||||
|
|
||||||
@@ -66,7 +74,7 @@ Define custom validators in your catalog's `functions` field. The catalog itself
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { defineCatalog } from '@json-render/core';
|
import { defineCatalog } from '@json-render/core';
|
||||||
import { schema } from '@json-render/react'; // or '@json-render/react-native'
|
import { schema } from '@json-render/react/schema'; // or '@json-render/react-native/schema'
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const catalog = defineCatalog(schema, {
|
const catalog = defineCatalog(schema, {
|
||||||
@@ -142,14 +150,114 @@ function TextField({ props, bindings }) {
|
|||||||
|
|
||||||
See the [@json-render/react API reference](/docs/api/react) for full `ValidationProvider` and `useFieldValidation` documentation.
|
See the [@json-render/react API reference](/docs/api/react) for full `ValidationProvider` and `useFieldValidation` documentation.
|
||||||
|
|
||||||
|
## Cross-Field Validation
|
||||||
|
|
||||||
|
Validation args support `{ "$state": "/path" }` references to compare against other fields. This enables cross-field rules like "confirm password must match password":
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "Input",
|
||||||
|
"props": {
|
||||||
|
"label": "Confirm Password",
|
||||||
|
"value": { "$bindState": "/form/confirmPassword" },
|
||||||
|
"checks": [
|
||||||
|
{ "type": "required", "message": "Please confirm your password" },
|
||||||
|
{
|
||||||
|
"type": "matches",
|
||||||
|
"args": { "other": { "$state": "/form/password" } },
|
||||||
|
"message": "Passwords must match"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Other cross-field examples:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"type": "greaterThan",
|
||||||
|
"args": { "other": { "$state": "/form/startDate" } },
|
||||||
|
"message": "End date must be after start date"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"type": "requiredIf",
|
||||||
|
"args": { "field": { "$state": "/form/enableNotifications" } },
|
||||||
|
"message": "Email is required when notifications are enabled"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conditional Validation
|
||||||
|
|
||||||
|
Use the `enabled` field in the validation config to only run checks when a condition is met:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "Input",
|
||||||
|
"props": {
|
||||||
|
"label": "Company Name",
|
||||||
|
"value": { "$bindState": "/form/company" },
|
||||||
|
"checks": [
|
||||||
|
{ "type": "required", "message": "Company name is required" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the component implementation, you can pass `enabled` to `useFieldValidation`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
useFieldValidation(bindings?.value ?? "", {
|
||||||
|
checks: props.checks ?? [],
|
||||||
|
enabled: { "$state": "/form/accountType", eq: "business" },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
This only validates the company name when the account type is "business".
|
||||||
|
|
||||||
## Validation Timing
|
## Validation Timing
|
||||||
|
|
||||||
Control when validation runs with `validateOn`:
|
Control when validation runs with `validateOn`:
|
||||||
|
|
||||||
- `change` — Validate on every input change
|
- `change` — Validate on every input change
|
||||||
- `blur` — Validate when field loses focus
|
- `blur` — Validate when field loses focus (default for Input, Textarea)
|
||||||
- `submit` — Validate only on form submission
|
- `submit` — Validate only on form submission
|
||||||
|
|
||||||
|
## Form-Level Validation
|
||||||
|
|
||||||
|
Use the built-in `validateForm` action to validate all registered fields at once. This is useful for a "Submit" button that should validate the entire form before proceeding:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"props": { "label": "Submit" },
|
||||||
|
"on": {
|
||||||
|
"press": [
|
||||||
|
{ "action": "validateForm", "params": { "statePath": "/formResult" } },
|
||||||
|
{ "action": "submitForm" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `validateForm` action runs `validateAll()` and writes `{ valid: boolean }` to the specified state path (defaults to `/formValidation`). Your submit handler can then check `{ "$state": "/formResult/valid" }` to decide whether to proceed.
|
||||||
|
|
||||||
|
> **Note:** Actions in a list execute sequentially, but `submitForm` does not automatically gate on validation. Guard submission with a `$cond` visibility condition on the button or check `{ "$state": "/formResult/valid" }` inside your action handler to skip submission when the form is invalid.
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
Learn about [generation modes](/docs/generation-modes).
|
- [Computed Values](/docs/computed-values) — derive dynamic prop values
|
||||||
|
- [Watchers](/docs/watchers) — react to state changes
|
||||||
|
- [Generation Modes](/docs/generation-modes) — how AI generates specs
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const metadata = { title: "Visibility" }
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/visibility")
|
||||||
|
|
||||||
# Visibility
|
# Visibility
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata"
|
||||||
|
export const metadata = pageMetadata("docs/watchers")
|
||||||
|
|
||||||
|
# Watchers
|
||||||
|
|
||||||
|
React to state changes by triggering actions when watched paths update.
|
||||||
|
|
||||||
|
## The `watch` Field
|
||||||
|
|
||||||
|
Elements can have an optional `watch` field that maps state paths to action bindings. When the value at a watched path changes, the bound actions fire automatically.
|
||||||
|
|
||||||
|
`watch` is a **top-level field** on the element (sibling of `type`, `props`, `children`) — not inside `props`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "Select",
|
||||||
|
"props": {
|
||||||
|
"label": "Country",
|
||||||
|
"value": { "$bindState": "/form/country" },
|
||||||
|
"options": ["US", "Canada", "UK"]
|
||||||
|
},
|
||||||
|
"watch": {
|
||||||
|
"/form/country": {
|
||||||
|
"action": "loadCities",
|
||||||
|
"params": { "country": { "$state": "/form/country" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When the user selects a different country, the `loadCities` action fires with the new country value. The action handler can fetch city data and update state, causing a dependent city Select to re-render with new options.
|
||||||
|
|
||||||
|
## Cascading Selects
|
||||||
|
|
||||||
|
A common pattern is cascading dropdowns where selecting a value in one field loads options for another:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"root": "form",
|
||||||
|
"elements": {
|
||||||
|
"form": {
|
||||||
|
"type": "Stack",
|
||||||
|
"props": { "direction": "vertical", "gap": "md" },
|
||||||
|
"children": ["country-select", "city-select"]
|
||||||
|
},
|
||||||
|
"country-select": {
|
||||||
|
"type": "Select",
|
||||||
|
"props": {
|
||||||
|
"label": "Country",
|
||||||
|
"value": { "$bindState": "/form/country" },
|
||||||
|
"options": ["US", "Canada", "UK"]
|
||||||
|
},
|
||||||
|
"watch": {
|
||||||
|
"/form/country": [
|
||||||
|
{ "action": "loadCities", "params": { "country": { "$state": "/form/country" } } },
|
||||||
|
{ "action": "setState", "params": { "statePath": "/form/city", "value": "" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
"city-select": {
|
||||||
|
"type": "Select",
|
||||||
|
"props": {
|
||||||
|
"label": "City",
|
||||||
|
"value": { "$bindState": "/form/city" },
|
||||||
|
"options": { "$state": "/availableCities" },
|
||||||
|
"placeholder": "Select a city"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"form": { "country": "", "city": "" },
|
||||||
|
"availableCities": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The watcher on `country-select` fires two actions when the country changes:
|
||||||
|
1. `loadCities` — fetches and writes city options to `/availableCities`
|
||||||
|
2. `setState` — resets the city selection
|
||||||
|
|
||||||
|
The city Select reads its options from `{ "$state": "/availableCities" }`, so it automatically updates when the data is loaded.
|
||||||
|
|
||||||
|
### Action Handler
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const handlers = {
|
||||||
|
loadCities: async (params) => {
|
||||||
|
const cities = await fetchCities(params.country);
|
||||||
|
// setState is called by the runtime to write the result
|
||||||
|
return cities;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with `defineRegistry`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { registry, handlers } = defineRegistry(catalog, {
|
||||||
|
components: { /* ... */ },
|
||||||
|
actions: {
|
||||||
|
loadCities: async (params, setState) => {
|
||||||
|
const response = await fetch(`/api/cities?country=${params.country}`);
|
||||||
|
const cities = await response.json();
|
||||||
|
setState('/availableCities', cities);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multiple Watchers
|
||||||
|
|
||||||
|
An element can watch multiple state paths. Each path maps to one or more action bindings:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"watch": {
|
||||||
|
"/form/startDate": { "action": "validateDateRange" },
|
||||||
|
"/form/endDate": { "action": "validateDateRange" },
|
||||||
|
"/form/quantity": [
|
||||||
|
{ "action": "recalculateTotal" },
|
||||||
|
{ "action": "checkInventory", "params": { "qty": { "$state": "/form/quantity" } } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
- Watchers only fire on **value changes**, not on the initial render
|
||||||
|
- Comparison is by reference (`===`), not deep equality
|
||||||
|
- Action params support the same expressions as event bindings (`$state`, `$item`, `$index`)
|
||||||
|
- Multiple action bindings on the same path execute sequentially
|
||||||
|
|
||||||
|
## When to Use `watch` vs `on`
|
||||||
|
|
||||||
|
| Mechanism | Trigger | Use Case |
|
||||||
|
|-----------|---------|----------|
|
||||||
|
| `on` | User interaction (press, change, blur) | Button clicks, input changes, form submissions |
|
||||||
|
| `watch` | State value change (any source) | Cascading data, derived state, cross-field sync |
|
||||||
|
|
||||||
|
Use `on` when reacting to direct user actions. Use `watch` when a state change (from any source — user input, action handler, or external store update) should trigger side effects.
|
||||||
|
|
||||||
|
## Next
|
||||||
|
|
||||||
|
- [Data Binding](/docs/data-binding) — connect elements to state
|
||||||
|
- [Computed Values](/docs/computed-values) — derive prop values
|
||||||
|
- [Visibility](/docs/visibility) — conditionally show or hide elements
|
||||||
@@ -154,23 +154,34 @@ button {
|
|||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* MDX table styles — fallback for GFM-generated tables */
|
/* MDX table styles — applies to both GFM pipe tables and raw HTML tables */
|
||||||
.mdx-table th,
|
.mdx-table th,
|
||||||
.mdx-table td {
|
.mdx-table td,
|
||||||
|
article table th,
|
||||||
|
article table td {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdx-table th {
|
.mdx-table th,
|
||||||
|
article table th {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background-color: var(--muted);
|
background-color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdx-table td {
|
.mdx-table td,
|
||||||
|
article table td {
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
article table {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Shiki dual theme support */
|
/* Shiki dual theme support */
|
||||||
.shiki,
|
.shiki,
|
||||||
.shiki span {
|
.shiki span {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
|
import { GeistPixelSquare } from "geist/font/pixel";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { DocsChat } from "@/components/docs-chat";
|
import { DocsChat } from "@/components/docs-chat";
|
||||||
@@ -61,7 +62,6 @@ export const metadata: Metadata = {
|
|||||||
description:
|
description:
|
||||||
"The Generative UI framework. Generate dashboards, widgets, and apps from prompts — safely constrained to components you define.",
|
"The Generative UI framework. Generate dashboards, widgets, and apps from prompts — safely constrained to components you define.",
|
||||||
images: ["/og"],
|
images: ["/og"],
|
||||||
creator: "@verabornnot",
|
|
||||||
},
|
},
|
||||||
robots: {
|
robots: {
|
||||||
index: true,
|
index: true,
|
||||||
@@ -92,7 +92,9 @@ export default async function RootLayout({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</head>
|
</head>
|
||||||
<body className={`${geistSans.variable} ${geistMono.variable}`}>
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} ${GeistPixelSquare.variable}`}
|
||||||
|
>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
{children}
|
{children}
|
||||||
<DocsChat defaultOpen={chatOpen} defaultWidth={chatWidth} />
|
<DocsChat defaultOpen={chatOpen} defaultWidth={chatWidth} />
|
||||||
|
|||||||
@@ -5,19 +5,20 @@ import { join } from "node:path";
|
|||||||
export { getPageTitle } from "@/lib/page-titles";
|
export { getPageTitle } from "@/lib/page-titles";
|
||||||
|
|
||||||
// Cache font data in memory after first load
|
// Cache font data in memory after first load
|
||||||
let fontCache: { geistRegular: Buffer } | null = null;
|
let fontCache: { geistRegular: Buffer; geistPixelSquare: Buffer } | null = null;
|
||||||
|
|
||||||
async function loadFonts() {
|
async function loadFonts() {
|
||||||
if (fontCache) return fontCache;
|
if (fontCache) return fontCache;
|
||||||
const geistRegular = await readFile(
|
const [geistRegular, geistPixelSquare] = await Promise.all([
|
||||||
join(process.cwd(), "public/Geist-Regular.ttf"),
|
readFile(join(process.cwd(), "public/Geist-Regular.ttf")),
|
||||||
);
|
readFile(join(process.cwd(), "public/GeistPixel-Square.ttf")),
|
||||||
fontCache = { geistRegular };
|
]);
|
||||||
|
fontCache = { geistRegular, geistPixelSquare };
|
||||||
return fontCache;
|
return fontCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderOgImage(title: string) {
|
export async function renderOgImage(title: string) {
|
||||||
const { geistRegular } = await loadFonts();
|
const { geistRegular, geistPixelSquare } = await loadFonts();
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<div
|
<div
|
||||||
@@ -53,8 +54,8 @@ export async function renderOgImage(title: string) {
|
|||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: 36,
|
fontSize: 36,
|
||||||
fontFamily: "Geist",
|
fontFamily: "Geist Pixel Square",
|
||||||
fontWeight: 400,
|
fontWeight: 500,
|
||||||
color: "white",
|
color: "white",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -99,6 +100,12 @@ export async function renderOgImage(title: string) {
|
|||||||
style: "normal",
|
style: "normal",
|
||||||
weight: 400,
|
weight: 400,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Geist Pixel Square",
|
||||||
|
data: geistPixelSquare.buffer as ArrayBuffer,
|
||||||
|
style: "normal",
|
||||||
|
weight: 500,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { Playground } from "@/components/playground";
|
import { Playground } from "@/components/playground";
|
||||||
|
import { pageMetadata } from "@/lib/page-metadata";
|
||||||
|
|
||||||
import { PAGE_TITLES } from "@/lib/page-titles";
|
export const metadata = pageMetadata("playground");
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: PAGE_TITLES["playground"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PlaygroundPage() {
|
export default function PlaygroundPage() {
|
||||||
return <Playground />;
|
return <Playground />;
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function Header() {
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<span className="font-medium tracking-tight text-lg">
|
<span className="font-medium tracking-tight text-lg font-(family-name:--font-geist-pixel-square)">
|
||||||
json-render
|
json-render
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -16,17 +16,20 @@ import { CopyButton } from "./copy-button";
|
|||||||
import { Toaster } from "./ui/sonner";
|
import { Toaster } from "./ui/sonner";
|
||||||
import { Header } from "./header";
|
import { Header } from "./header";
|
||||||
import { Sheet, SheetContent, SheetTitle } from "./ui/sheet";
|
import { Sheet, SheetContent, SheetTitle } from "./ui/sheet";
|
||||||
|
import { JsonEditor } from "@visual-json/react";
|
||||||
|
import type { JsonValue } from "@visual-json/react";
|
||||||
import { PlaygroundRenderer } from "@/lib/render/renderer";
|
import { PlaygroundRenderer } from "@/lib/render/renderer";
|
||||||
import { playgroundCatalog } from "@/lib/render/catalog";
|
import { playgroundCatalog } from "@/lib/render/catalog";
|
||||||
import { buildCatalogDisplayData } from "@/lib/render/catalog-display";
|
import { buildCatalogDisplayData } from "@/lib/render/catalog-display";
|
||||||
|
|
||||||
type Tab = "json" | "nested" | "stream" | "catalog";
|
type Tab = "json" | "nested" | "stream" | "catalog" | "visual";
|
||||||
type RenderView = "preview" | "code";
|
type RenderView = "preview" | "code";
|
||||||
type MobileView =
|
type MobileView =
|
||||||
| "json"
|
| "json"
|
||||||
| "nested"
|
| "nested"
|
||||||
| "stream"
|
| "stream"
|
||||||
| "catalog"
|
| "catalog"
|
||||||
|
| "visual"
|
||||||
| "preview"
|
| "preview"
|
||||||
| "generated-code";
|
| "generated-code";
|
||||||
|
|
||||||
@@ -233,6 +236,20 @@ export function Playground() {
|
|||||||
[handleSubmit],
|
[handleSubmit],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleVisualChange = useCallback(
|
||||||
|
(value: JsonValue) => {
|
||||||
|
if (!selectedVersionId || isStreaming) return;
|
||||||
|
setVersions((prev) =>
|
||||||
|
prev.map((v) =>
|
||||||
|
v.id === selectedVersionId
|
||||||
|
? { ...v, tree: value as unknown as Spec }
|
||||||
|
: v,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[selectedVersionId, isStreaming],
|
||||||
|
);
|
||||||
|
|
||||||
const jsonCode = currentTree
|
const jsonCode = currentTree
|
||||||
? JSON.stringify(currentTree, null, 2)
|
? JSON.stringify(currentTree, null, 2)
|
||||||
: "// waiting...";
|
: "// waiting...";
|
||||||
@@ -479,31 +496,69 @@ ${jsx}
|
|||||||
? jsonCode
|
? jsonCode
|
||||||
: activeTab === "nested"
|
: activeTab === "nested"
|
||||||
? nestedCode
|
? nestedCode
|
||||||
: "";
|
: activeTab === "visual"
|
||||||
|
? jsonCode
|
||||||
|
: "";
|
||||||
|
|
||||||
const codePane = (
|
const codePane = (
|
||||||
<div className="h-full flex flex-col border-t border-border">
|
<div className="h-full flex flex-col border-t border-border">
|
||||||
<div className="border-b border-border px-3 h-9 flex items-center gap-3">
|
<div className="border-b border-border px-3 h-9 flex items-center gap-3">
|
||||||
{(["json", "nested", "stream", "catalog"] as const).map((tab) => (
|
{(["json", "visual", "nested", "stream", "catalog"] as const).map(
|
||||||
<button
|
(tab) => (
|
||||||
key={tab}
|
<button
|
||||||
onClick={() => setActiveTab(tab)}
|
key={tab}
|
||||||
className={`text-xs font-mono transition-colors ${
|
onClick={() => setActiveTab(tab)}
|
||||||
activeTab === tab
|
className={`text-xs font-mono transition-colors ${
|
||||||
? "text-foreground"
|
activeTab === tab
|
||||||
: "text-muted-foreground hover:text-foreground"
|
? "text-foreground"
|
||||||
}`}
|
: "text-muted-foreground hover:text-foreground"
|
||||||
>
|
}`}
|
||||||
{tab}
|
>
|
||||||
</button>
|
{tab}
|
||||||
))}
|
</button>
|
||||||
|
),
|
||||||
|
)}
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
{activeTab !== "catalog" && (
|
{activeTab !== "catalog" && activeTab !== "visual" && (
|
||||||
<CopyButton text={copyText} className="text-muted-foreground" />
|
<CopyButton text={copyText} className="text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
{activeTab === "catalog" ? (
|
{activeTab === "visual" ? (
|
||||||
|
currentTree ? (
|
||||||
|
<JsonEditor
|
||||||
|
value={currentTree as unknown as JsonValue}
|
||||||
|
onChange={handleVisualChange}
|
||||||
|
readOnly={isStreaming}
|
||||||
|
sidebarOpen={false}
|
||||||
|
height="100%"
|
||||||
|
className="h-full"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--vj-bg": "var(--background)",
|
||||||
|
"--vj-bg-panel": "var(--background)",
|
||||||
|
"--vj-bg-hover": "var(--muted)",
|
||||||
|
"--vj-bg-selected": "var(--primary)",
|
||||||
|
"--vj-bg-selected-muted": "var(--muted)",
|
||||||
|
"--vj-text": "var(--foreground)",
|
||||||
|
"--vj-text-selected": "var(--primary-foreground)",
|
||||||
|
"--vj-text-muted": "var(--muted-foreground)",
|
||||||
|
"--vj-text-dim": "var(--muted-foreground)",
|
||||||
|
"--vj-border": "var(--border)",
|
||||||
|
"--vj-border-subtle": "var(--border)",
|
||||||
|
"--vj-accent": "var(--primary)",
|
||||||
|
"--vj-accent-muted": "var(--muted)",
|
||||||
|
"--vj-input-bg": "var(--secondary)",
|
||||||
|
"--vj-input-border": "var(--border)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground/50 p-3 text-sm font-mono">
|
||||||
|
{"// generate a spec to edit visually"}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : activeTab === "catalog" ? (
|
||||||
<div className="h-full flex flex-col text-sm">
|
<div className="h-full flex flex-col text-sm">
|
||||||
<div className="flex items-center gap-3 px-3 h-9 border-b border-border">
|
<div className="flex items-center gap-3 px-3 h-9 border-b border-border">
|
||||||
{(
|
{(
|
||||||
@@ -735,19 +790,21 @@ ${jsx}
|
|||||||
: 0}
|
: 0}
|
||||||
</button>
|
</button>
|
||||||
{/* Code tabs */}
|
{/* Code tabs */}
|
||||||
{(["json", "nested", "stream", "catalog"] as const).map((tab) => (
|
{(["json", "visual", "nested", "stream", "catalog"] as const).map(
|
||||||
<button
|
(tab) => (
|
||||||
key={tab}
|
<button
|
||||||
onClick={() => setMobileView(tab)}
|
key={tab}
|
||||||
className={`text-xs font-mono transition-colors shrink-0 ${
|
onClick={() => setMobileView(tab)}
|
||||||
mobileView === tab
|
className={`text-xs font-mono transition-colors shrink-0 ${
|
||||||
? "text-foreground"
|
mobileView === tab
|
||||||
: "text-muted-foreground hover:text-foreground"
|
? "text-foreground"
|
||||||
}`}
|
: "text-muted-foreground hover:text-foreground"
|
||||||
>
|
}`}
|
||||||
{tab}
|
>
|
||||||
</button>
|
{tab}
|
||||||
))}
|
</button>
|
||||||
|
),
|
||||||
|
)}
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
{/* Preview / code toggle */}
|
{/* Preview / code toggle */}
|
||||||
{[
|
{[
|
||||||
@@ -770,7 +827,41 @@ ${jsx}
|
|||||||
|
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
<div className="flex-1 min-h-0 overflow-auto">
|
<div className="flex-1 min-h-0 overflow-auto">
|
||||||
{mobileView === "catalog" ? (
|
{mobileView === "visual" ? (
|
||||||
|
currentTree ? (
|
||||||
|
<JsonEditor
|
||||||
|
value={currentTree as unknown as JsonValue}
|
||||||
|
onChange={handleVisualChange}
|
||||||
|
readOnly={isStreaming}
|
||||||
|
sidebarOpen={false}
|
||||||
|
height="100%"
|
||||||
|
className="h-full"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--vj-bg": "var(--background)",
|
||||||
|
"--vj-bg-panel": "var(--background)",
|
||||||
|
"--vj-bg-hover": "var(--muted)",
|
||||||
|
"--vj-bg-selected": "var(--primary)",
|
||||||
|
"--vj-bg-selected-muted": "var(--muted)",
|
||||||
|
"--vj-text": "var(--foreground)",
|
||||||
|
"--vj-text-selected": "var(--primary-foreground)",
|
||||||
|
"--vj-text-muted": "var(--muted-foreground)",
|
||||||
|
"--vj-text-dim": "var(--muted-foreground)",
|
||||||
|
"--vj-border": "var(--border)",
|
||||||
|
"--vj-border-subtle": "var(--border)",
|
||||||
|
"--vj-accent": "var(--primary)",
|
||||||
|
"--vj-accent-muted": "var(--muted)",
|
||||||
|
"--vj-input-bg": "var(--secondary)",
|
||||||
|
"--vj-input-border": "var(--border)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground/50 p-3 text-sm font-mono">
|
||||||
|
{"// generate a spec to edit visually"}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : mobileView === "catalog" ? (
|
||||||
<div className="h-full flex flex-col text-sm">
|
<div className="h-full flex flex-col text-sm">
|
||||||
<div className="flex items-center gap-3 px-3 h-9 border-b border-border">
|
<div className="flex items-center gap-3 px-3 h-9 border-b border-border">
|
||||||
{(
|
{(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { nextJsConfig } from "@repo/eslint-config/next-js";
|
import { nextJsConfig } from "@internal/eslint-config/next-js";
|
||||||
|
|
||||||
/** @type {import("eslint").Linter.Config[]} */
|
/** @type {import("eslint").Linter.Config[]} */
|
||||||
export default [
|
export default [
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export const docsNavigation: NavSection[] = [
|
|||||||
{ title: "Schemas", href: "/docs/schemas" },
|
{ title: "Schemas", href: "/docs/schemas" },
|
||||||
{ title: "Catalog", href: "/docs/catalog" },
|
{ title: "Catalog", href: "/docs/catalog" },
|
||||||
{ title: "Data Binding", href: "/docs/data-binding" },
|
{ title: "Data Binding", href: "/docs/data-binding" },
|
||||||
|
{ title: "Computed Values", href: "/docs/computed-values" },
|
||||||
{ title: "Visibility", href: "/docs/visibility" },
|
{ title: "Visibility", href: "/docs/visibility" },
|
||||||
|
{ title: "Watchers", href: "/docs/watchers" },
|
||||||
{ title: "Validation", href: "/docs/validation" },
|
{ title: "Validation", href: "/docs/validation" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -57,11 +59,26 @@ export const docsNavigation: NavSection[] = [
|
|||||||
href: "https://github.com/vercel-labs/json-render/tree/main/examples/react-native",
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/react-native",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "React PDF",
|
||||||
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/react-pdf",
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Remotion",
|
title: "Remotion",
|
||||||
href: "https://github.com/vercel-labs/json-render/tree/main/examples/remotion",
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/remotion",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Vue",
|
||||||
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vue",
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Renders with Vite (Vue / React)",
|
||||||
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -86,9 +103,11 @@ export const docsNavigation: NavSection[] = [
|
|||||||
items: [
|
items: [
|
||||||
{ title: "@json-render/core", href: "/docs/api/core" },
|
{ title: "@json-render/core", href: "/docs/api/core" },
|
||||||
{ title: "@json-render/react", href: "/docs/api/react" },
|
{ title: "@json-render/react", href: "/docs/api/react" },
|
||||||
|
{ title: "@json-render/react-pdf", href: "/docs/api/react-pdf" },
|
||||||
{ title: "@json-render/shadcn", href: "/docs/api/shadcn" },
|
{ title: "@json-render/shadcn", href: "/docs/api/shadcn" },
|
||||||
{ title: "@json-render/react-native", href: "/docs/api/react-native" },
|
{ title: "@json-render/react-native", href: "/docs/api/react-native" },
|
||||||
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
|
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
|
||||||
|
{ title: "@json-render/vue", href: "/docs/api/vue" },
|
||||||
{ title: "@json-render/codegen", href: "/docs/api/codegen" },
|
{ title: "@json-render/codegen", href: "/docs/api/codegen" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { PAGE_TITLES } from "./page-titles";
|
||||||
|
|
||||||
|
const DESCRIPTION =
|
||||||
|
"The Generative UI framework. Generate dashboards, widgets, and apps from prompts — safely constrained to components you define.";
|
||||||
|
|
||||||
|
export function pageMetadata(slug: string): Metadata {
|
||||||
|
const title = PAGE_TITLES[slug];
|
||||||
|
if (!title) return {};
|
||||||
|
|
||||||
|
const displayTitle = title.replace(/\n/g, " ");
|
||||||
|
const fullTitle = `${displayTitle} | json-render`;
|
||||||
|
const ogImageUrl = slug ? `/og/${slug}` : "/og";
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: displayTitle,
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
locale: "en_US",
|
||||||
|
siteName: "json-render",
|
||||||
|
title: fullTitle,
|
||||||
|
description: DESCRIPTION,
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: ogImageUrl,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: `${displayTitle} - json-render`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: fullTitle,
|
||||||
|
description: DESCRIPTION,
|
||||||
|
images: [ogImageUrl],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -23,7 +23,9 @@ export const PAGE_TITLES: Record<string, string> = {
|
|||||||
"docs/streaming": "Streaming",
|
"docs/streaming": "Streaming",
|
||||||
"docs/validation": "Validation",
|
"docs/validation": "Validation",
|
||||||
"docs/data-binding": "Data Binding",
|
"docs/data-binding": "Data Binding",
|
||||||
|
"docs/computed-values": "Computed Values",
|
||||||
"docs/visibility": "Visibility",
|
"docs/visibility": "Visibility",
|
||||||
|
"docs/watchers": "Watchers",
|
||||||
"docs/generation-modes": "Generation Modes",
|
"docs/generation-modes": "Generation Modes",
|
||||||
"docs/code-export": "Code Export",
|
"docs/code-export": "Code Export",
|
||||||
"docs/custom-schema": "Custom Schema & Renderer",
|
"docs/custom-schema": "Custom Schema & Renderer",
|
||||||
@@ -38,9 +40,12 @@ export const PAGE_TITLES: Record<string, string> = {
|
|||||||
// API references
|
// API references
|
||||||
"docs/api/core": "@json-render/core API",
|
"docs/api/core": "@json-render/core API",
|
||||||
"docs/api/react": "@json-render/react API",
|
"docs/api/react": "@json-render/react API",
|
||||||
|
"docs/api/vue": "@json-render/vue API",
|
||||||
|
"docs/api/react-pdf": "@json-render/react-pdf API",
|
||||||
"docs/api/react-native": "@json-render/react-native API",
|
"docs/api/react-native": "@json-render/react-native API",
|
||||||
"docs/api/codegen": "@json-render/codegen API",
|
"docs/api/codegen": "@json-render/codegen API",
|
||||||
"docs/api/remotion": "@json-render/remotion API",
|
"docs/api/remotion": "@json-render/remotion API",
|
||||||
|
"docs/api/shadcn": "@json-render/shadcn API",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "portless json-render next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint --max-warnings 0",
|
"lint": "eslint --max-warnings 0",
|
||||||
@@ -28,11 +28,13 @@
|
|||||||
"@upstash/redis": "^1.36.1",
|
"@upstash/redis": "^1.36.1",
|
||||||
"@vercel/analytics": "^1.6.1",
|
"@vercel/analytics": "^1.6.1",
|
||||||
"@vercel/speed-insights": "^1.3.1",
|
"@vercel/speed-insights": "^1.3.1",
|
||||||
|
"@visual-json/react": "0.1.1",
|
||||||
"ai": "^6.0.33",
|
"ai": "^6.0.33",
|
||||||
"bash-tool": "1.3.14",
|
"bash-tool": "1.3.14",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"geist": "1.7.0",
|
||||||
"just-bash": "2.9.6",
|
"just-bash": "2.9.6",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "16.1.1",
|
"next": "16.1.1",
|
||||||
@@ -51,8 +53,8 @@
|
|||||||
"zod": "^4.0.0"
|
"zod": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/eslint-config": "workspace:*",
|
"@internal/eslint-config": "workspace:*",
|
||||||
"@repo/typescript-config": "workspace:*",
|
"@internal/typescript-config": "workspace:*",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/mdx": "^2.0.13",
|
"@types/mdx": "^2.0.13",
|
||||||
"@types/node": "^22.15.3",
|
"@types/node": "^22.15.3",
|
||||||
|
|||||||
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@repo/typescript-config/nextjs.json",
|
"extends": "@internal/typescript-config/nextjs.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# example-chat
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9cef4e9]
|
||||||
|
- @json-render/core@0.10.0
|
||||||
|
- @json-render/react@0.10.0
|
||||||
|
- @json-render/shadcn@0.10.0
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [b103676]
|
||||||
|
- @json-render/react@0.9.1
|
||||||
|
- @json-render/shadcn@0.9.1
|
||||||
|
- @json-render/core@0.9.1
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [1d755c1]
|
||||||
|
- @json-render/core@0.9.0
|
||||||
|
- @json-render/react@0.9.0
|
||||||
|
- @json-render/shadcn@0.9.0
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { nextJsConfig } from "@repo/eslint-config/next-js";
|
import { nextJsConfig } from "@internal/eslint-config/next-js";
|
||||||
|
|
||||||
/** @type {import("eslint").Linter.Config[]} */
|
/** @type {import("eslint").Linter.Config[]} */
|
||||||
export default [
|
export default [
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "example-chat",
|
"name": "example-chat",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "portless chat-demo.json-render next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint --max-warnings 0",
|
"lint": "eslint --max-warnings 0",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"zod": "4.3.5"
|
"zod": "4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/eslint-config": "workspace:*",
|
"@internal/eslint-config": "workspace:*",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
"@types/react": "19.2.3",
|
"@types/react": "19.2.3",
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# example-dashboard
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9cef4e9]
|
||||||
|
- @json-render/core@0.10.0
|
||||||
|
- @json-render/react@0.10.0
|
||||||
|
- @json-render/codegen@0.10.0
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [b103676]
|
||||||
|
- @json-render/react@0.9.1
|
||||||
|
- @json-render/core@0.9.1
|
||||||
|
- @json-render/codegen@0.9.1
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [1d755c1]
|
||||||
|
- @json-render/core@0.9.0
|
||||||
|
- @json-render/react@0.9.0
|
||||||
|
- @json-render/codegen@0.9.0
|
||||||
@@ -196,24 +196,28 @@ export function Widget({
|
|||||||
onDeleted?.();
|
onDeleted?.();
|
||||||
}, [widgetId, onDeleted]);
|
}, [widgetId, onDeleted]);
|
||||||
|
|
||||||
const handleStateChange = useCallback((path: string, value: unknown) => {
|
const handleStateChange = useCallback(
|
||||||
setState((prev) => {
|
(changes: Array<{ path: string; value: unknown }>) => {
|
||||||
const next = { ...prev };
|
setState((prev) => {
|
||||||
// Convert path like "customerForm/name" to nested object
|
const next = { ...prev };
|
||||||
const parts = path.split("/");
|
for (const { path, value } of changes) {
|
||||||
let current: Record<string, unknown> = next;
|
const parts = path.split("/");
|
||||||
for (let i = 0; i < parts.length - 1; i++) {
|
let current: Record<string, unknown> = next;
|
||||||
const part = parts[i]!;
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
if (!(part in current) || typeof current[part] !== "object") {
|
const part = parts[i]!;
|
||||||
current[part] = {};
|
if (!(part in current) || typeof current[part] !== "object") {
|
||||||
|
current[part] = {};
|
||||||
|
}
|
||||||
|
current = current[part] as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
const lastPart = parts[parts.length - 1]!;
|
||||||
|
current[lastPart] = value;
|
||||||
}
|
}
|
||||||
current = current[part] as Record<string, unknown>;
|
return next;
|
||||||
}
|
});
|
||||||
const lastPart = parts[parts.length - 1]!;
|
},
|
||||||
current[lastPart] = value;
|
[],
|
||||||
return next;
|
);
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Use spec from stream, or initial spec for saved widgets
|
// Use spec from stream, or initial spec for saved widgets
|
||||||
const currentSpec = spec || initialSpec;
|
const currentSpec = spec || initialSpec;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { nextJsConfig } from "@repo/eslint-config/next-js";
|
import { nextJsConfig } from "@internal/eslint-config/next-js";
|
||||||
|
|
||||||
/** @type {import("eslint").Linter.Config[]} */
|
/** @type {import("eslint").Linter.Config[]} */
|
||||||
export default [
|
export default [
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ interface DashboardRendererProps {
|
|||||||
spec: Spec | null;
|
spec: Spec | null;
|
||||||
state?: Record<string, unknown>;
|
state?: Record<string, unknown>;
|
||||||
setState?: SetState;
|
setState?: SetState;
|
||||||
onStateChange?: (path: string, value: unknown) => void;
|
onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "example-dashboard",
|
"name": "example-dashboard",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "portless dashboard-demo.json-render next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint --max-warnings 0",
|
"lint": "eslint --max-warnings 0",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"zod": "^4.0.0"
|
"zod": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/eslint-config": "workspace:*",
|
"@internal/eslint-config": "workspace:*",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
"@types/react": "19.2.3",
|
"@types/react": "19.2.3",
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# example-no-ai
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9cef4e9]
|
||||||
|
- @json-render/core@0.10.0
|
||||||
|
- @json-render/react@0.10.0
|
||||||
|
- @json-render/shadcn@0.10.0
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [b103676]
|
||||||
|
- @json-render/react@0.9.1
|
||||||
|
- @json-render/shadcn@0.9.1
|
||||||
|
- @json-render/core@0.9.1
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [1d755c1]
|
||||||
|
- @json-render/core@0.9.0
|
||||||
|
- @json-render/react@0.9.0
|
||||||
|
- @json-render/shadcn@0.9.0
|
||||||
+59
-41
@@ -2,33 +2,31 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback, type ReactNode } from "react";
|
import { useState, useEffect, useCallback, type ReactNode } from "react";
|
||||||
import ConfettiExplosion from "react-confetti-explosion";
|
import ConfettiExplosion from "react-confetti-explosion";
|
||||||
import {
|
import { JSONUIProvider, Renderer } from "@json-render/react";
|
||||||
Renderer,
|
|
||||||
StateProvider,
|
|
||||||
VisibilityProvider,
|
|
||||||
ActionProvider,
|
|
||||||
ValidationProvider,
|
|
||||||
} from "@json-render/react";
|
|
||||||
import type { Spec } from "@json-render/core";
|
import type { Spec } from "@json-render/core";
|
||||||
import { registry, actionHandlers, onConfetti } from "@/lib/render/registry";
|
import {
|
||||||
|
registry,
|
||||||
|
actionHandlers,
|
||||||
|
computedFunctions,
|
||||||
|
onConfetti,
|
||||||
|
} from "@/lib/render/registry";
|
||||||
import { examples } from "@/lib/examples";
|
import { examples } from "@/lib/examples";
|
||||||
|
|
||||||
function SpecRenderer({ spec }: { spec: Spec }): ReactNode {
|
function SpecRenderer({ spec }: { spec: Spec }): ReactNode {
|
||||||
return (
|
return (
|
||||||
<StateProvider initialState={spec.state ?? {}}>
|
<JSONUIProvider
|
||||||
<VisibilityProvider>
|
registry={registry}
|
||||||
<ActionProvider handlers={actionHandlers}>
|
initialState={spec.state ?? {}}
|
||||||
<ValidationProvider>
|
handlers={actionHandlers}
|
||||||
<Renderer spec={spec} registry={registry} />
|
functions={computedFunctions}
|
||||||
</ValidationProvider>
|
>
|
||||||
</ActionProvider>
|
<Renderer spec={spec} registry={registry} />
|
||||||
</VisibilityProvider>
|
</JSONUIProvider>
|
||||||
</StateProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [selectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
const selected = examples[selectedIndex]!;
|
const selected = examples[selectedIndex]!;
|
||||||
const [confettiKey, setConfettiKey] = useState(0);
|
const [confettiKey, setConfettiKey] = useState(0);
|
||||||
const [confettiActive, setConfettiActive] = useState(false);
|
const [confettiActive, setConfettiActive] = useState(false);
|
||||||
@@ -41,30 +39,50 @@ export default function Page() {
|
|||||||
useEffect(() => onConfetti(fireConfetti), [fireConfetti]);
|
useEffect(() => onConfetti(fireConfetti), [fireConfetti]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex items-center justify-center bg-muted/30">
|
<div className="h-screen flex flex-col bg-muted/30">
|
||||||
<div
|
{/* Example selector */}
|
||||||
className="relative bg-background border rounded-lg shadow-sm"
|
<nav className="flex gap-1 p-3 overflow-x-auto border-b bg-background shrink-0">
|
||||||
style={{ width: 960, height: 1080 }}
|
{examples.map((ex, i) => (
|
||||||
>
|
<button
|
||||||
{confettiActive && (
|
key={ex.name}
|
||||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
onClick={() => setSelectedIndex(i)}
|
||||||
<ConfettiExplosion
|
className={`px-3 py-1.5 text-sm rounded-md whitespace-nowrap transition-colors ${
|
||||||
key={confettiKey}
|
i === selectedIndex
|
||||||
portal={false}
|
? "bg-primary text-primary-foreground"
|
||||||
force={0.8}
|
: "hover:bg-muted"
|
||||||
duration={3500}
|
}`}
|
||||||
particleCount={400}
|
>
|
||||||
particleSize={8}
|
{ex.name}
|
||||||
colors={["#00F0FF", "#7B61FF", "#FF3DFF", "#00FF94", "#FFE14D"]}
|
</button>
|
||||||
width={1600}
|
))}
|
||||||
height="200vh"
|
</nav>
|
||||||
zIndex={1}
|
|
||||||
onComplete={() => setConfettiActive(false)}
|
{/* Render area */}
|
||||||
/>
|
<div className="flex-1 flex items-start justify-center overflow-auto p-6">
|
||||||
|
<div className="relative bg-background border rounded-lg shadow-sm w-full max-w-[960px]">
|
||||||
|
{confettiActive && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||||
|
<ConfettiExplosion
|
||||||
|
key={confettiKey}
|
||||||
|
portal={false}
|
||||||
|
force={0.8}
|
||||||
|
duration={3500}
|
||||||
|
particleCount={400}
|
||||||
|
particleSize={8}
|
||||||
|
colors={["#00F0FF", "#7B61FF", "#FF3DFF", "#00FF94", "#FFE14D"]}
|
||||||
|
width={1600}
|
||||||
|
height="200vh"
|
||||||
|
zIndex={1}
|
||||||
|
onComplete={() => setConfettiActive(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="p-6 relative z-10">
|
||||||
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
|
{selected.description}
|
||||||
|
</p>
|
||||||
|
<SpecRenderer key={selectedIndex} spec={selected.spec} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="h-full overflow-auto p-6 flex items-center justify-center relative z-10">
|
|
||||||
<SpecRenderer key={selectedIndex} spec={selected.spec} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { nextJsConfig } from "@repo/eslint-config/next-js";
|
import { nextJsConfig } from "@internal/eslint-config/next-js";
|
||||||
|
|
||||||
/** @type {import("eslint").Linter.Config[]} */
|
/** @type {import("eslint").Linter.Config[]} */
|
||||||
export default [
|
export default [
|
||||||
|
|||||||
@@ -508,4 +508,353 @@ export const examples: Example[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Advanced: Registration form with cross-field validation & $template
|
||||||
|
// =========================================================================
|
||||||
|
{
|
||||||
|
name: "Registration Form",
|
||||||
|
description:
|
||||||
|
"Cross-field validation, $template preview, and validateForm action",
|
||||||
|
spec: {
|
||||||
|
root: "card",
|
||||||
|
state: {
|
||||||
|
form: {
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
accountType: "personal",
|
||||||
|
company: "",
|
||||||
|
},
|
||||||
|
result: null,
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
card: {
|
||||||
|
type: "Card",
|
||||||
|
props: {
|
||||||
|
title: "Create Account",
|
||||||
|
description: "Fill out the form below to register",
|
||||||
|
maxWidth: "md",
|
||||||
|
centered: null,
|
||||||
|
},
|
||||||
|
children: ["formStack"],
|
||||||
|
},
|
||||||
|
formStack: {
|
||||||
|
type: "Stack",
|
||||||
|
props: {
|
||||||
|
direction: "vertical",
|
||||||
|
gap: "md",
|
||||||
|
align: null,
|
||||||
|
justify: null,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
"preview",
|
||||||
|
"sep0",
|
||||||
|
"nameInput",
|
||||||
|
"emailInput",
|
||||||
|
"passwordInput",
|
||||||
|
"confirmInput",
|
||||||
|
"sep1",
|
||||||
|
"accountTypeRadio",
|
||||||
|
"companyInput",
|
||||||
|
"sep2",
|
||||||
|
"actions",
|
||||||
|
"statusText",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// $template live preview
|
||||||
|
preview: {
|
||||||
|
type: "Text",
|
||||||
|
props: {
|
||||||
|
text: {
|
||||||
|
$template: "Welcome, ${/form/name}! Your email: ${/form/email}",
|
||||||
|
},
|
||||||
|
variant: "muted",
|
||||||
|
},
|
||||||
|
visible: { $state: "/form/name", neq: "" },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
sep0: { type: "Separator", props: { orientation: null }, children: [] },
|
||||||
|
|
||||||
|
nameInput: {
|
||||||
|
type: "Input",
|
||||||
|
props: {
|
||||||
|
label: "Full Name",
|
||||||
|
name: "name",
|
||||||
|
type: "text",
|
||||||
|
placeholder: "Jane Doe",
|
||||||
|
value: { $bindState: "/form/name" },
|
||||||
|
checks: [
|
||||||
|
{ type: "required", message: "Name is required" },
|
||||||
|
{
|
||||||
|
type: "minLength",
|
||||||
|
args: { min: 2 },
|
||||||
|
message: "Name must be at least 2 characters",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
validateOn: "blur",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
emailInput: {
|
||||||
|
type: "Input",
|
||||||
|
props: {
|
||||||
|
label: "Email",
|
||||||
|
name: "email",
|
||||||
|
type: "email",
|
||||||
|
placeholder: "jane@example.com",
|
||||||
|
value: { $bindState: "/form/email" },
|
||||||
|
checks: [
|
||||||
|
{ type: "required", message: "Email is required" },
|
||||||
|
{ type: "email", message: "Enter a valid email address" },
|
||||||
|
],
|
||||||
|
validateOn: "blur",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
passwordInput: {
|
||||||
|
type: "Input",
|
||||||
|
props: {
|
||||||
|
label: "Password",
|
||||||
|
name: "password",
|
||||||
|
type: "password",
|
||||||
|
placeholder: "At least 8 characters",
|
||||||
|
value: { $bindState: "/form/password" },
|
||||||
|
checks: [
|
||||||
|
{ type: "required", message: "Password is required" },
|
||||||
|
{
|
||||||
|
type: "minLength",
|
||||||
|
args: { min: 8 },
|
||||||
|
message: "Password must be at least 8 characters",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
validateOn: "blur",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
confirmInput: {
|
||||||
|
type: "Input",
|
||||||
|
props: {
|
||||||
|
label: "Confirm Password",
|
||||||
|
name: "confirmPassword",
|
||||||
|
type: "password",
|
||||||
|
placeholder: "Re-enter your password",
|
||||||
|
value: { $bindState: "/form/confirmPassword" },
|
||||||
|
checks: [
|
||||||
|
{ type: "required", message: "Please confirm your password" },
|
||||||
|
{
|
||||||
|
type: "matches",
|
||||||
|
args: { other: { $state: "/form/password" } },
|
||||||
|
message: "Passwords must match",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
validateOn: "blur",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
sep1: { type: "Separator", props: { orientation: null }, children: [] },
|
||||||
|
|
||||||
|
accountTypeRadio: {
|
||||||
|
type: "Radio",
|
||||||
|
props: {
|
||||||
|
label: "Account Type",
|
||||||
|
name: "accountType",
|
||||||
|
options: ["personal", "business"],
|
||||||
|
value: { $bindState: "/form/accountType" },
|
||||||
|
checks: null,
|
||||||
|
validateOn: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
companyInput: {
|
||||||
|
type: "Input",
|
||||||
|
props: {
|
||||||
|
label: "Company Name",
|
||||||
|
name: "company",
|
||||||
|
type: "text",
|
||||||
|
placeholder: "Acme Inc.",
|
||||||
|
value: { $bindState: "/form/company" },
|
||||||
|
checks: [
|
||||||
|
{
|
||||||
|
type: "requiredIf",
|
||||||
|
args: { field: { $state: "/form/accountType" } },
|
||||||
|
message: "Company name is required for business accounts",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
validateOn: "blur",
|
||||||
|
},
|
||||||
|
visible: { $state: "/form/accountType", eq: "business" },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
sep2: { type: "Separator", props: { orientation: null }, children: [] },
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
type: "Stack",
|
||||||
|
props: {
|
||||||
|
direction: "horizontal",
|
||||||
|
gap: "sm",
|
||||||
|
align: null,
|
||||||
|
justify: "end",
|
||||||
|
},
|
||||||
|
children: ["submitBtn"],
|
||||||
|
},
|
||||||
|
submitBtn: {
|
||||||
|
type: "Button",
|
||||||
|
props: { label: "Register", variant: "primary", disabled: null },
|
||||||
|
on: {
|
||||||
|
press: [
|
||||||
|
{
|
||||||
|
action: "validateForm",
|
||||||
|
params: { statePath: "/result" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Validation result
|
||||||
|
statusText: {
|
||||||
|
type: "Alert",
|
||||||
|
props: {
|
||||||
|
title: "Validation Result",
|
||||||
|
message: {
|
||||||
|
$cond: { $state: "/result/valid", eq: true },
|
||||||
|
$then: "All fields are valid -- ready to submit!",
|
||||||
|
$else: "Please fix the errors above before submitting.",
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
$cond: { $state: "/result/valid", eq: true },
|
||||||
|
$then: "success",
|
||||||
|
$else: "error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visible: { $state: "/result", neq: null },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Advanced: Cascading selects with watchers & $computed
|
||||||
|
// =========================================================================
|
||||||
|
{
|
||||||
|
name: "Cascading Selects",
|
||||||
|
description:
|
||||||
|
"Watchers reset dependent fields, $computed derives display values",
|
||||||
|
spec: {
|
||||||
|
root: "card",
|
||||||
|
state: {
|
||||||
|
form: { country: "", city: "" },
|
||||||
|
availableCities: [],
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
card: {
|
||||||
|
type: "Card",
|
||||||
|
props: {
|
||||||
|
title: "Shipping Address",
|
||||||
|
description: "Select your country to load available cities",
|
||||||
|
maxWidth: "md",
|
||||||
|
centered: null,
|
||||||
|
},
|
||||||
|
children: ["formStack"],
|
||||||
|
},
|
||||||
|
formStack: {
|
||||||
|
type: "Stack",
|
||||||
|
props: {
|
||||||
|
direction: "vertical",
|
||||||
|
gap: "md",
|
||||||
|
align: null,
|
||||||
|
justify: null,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
"countrySelect",
|
||||||
|
"citySelect",
|
||||||
|
"sep",
|
||||||
|
"addressPreview",
|
||||||
|
"templatePreview",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
countrySelect: {
|
||||||
|
type: "Select",
|
||||||
|
props: {
|
||||||
|
label: "Country",
|
||||||
|
name: "country",
|
||||||
|
options: ["US", "Canada", "UK", "Germany", "Japan"],
|
||||||
|
placeholder: "Choose a country",
|
||||||
|
value: { $bindState: "/form/country" },
|
||||||
|
checks: [{ type: "required", message: "Country is required" }],
|
||||||
|
validateOn: "change",
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
"/form/country": [
|
||||||
|
{
|
||||||
|
action: "setState",
|
||||||
|
params: {
|
||||||
|
statePath: "/availableCities",
|
||||||
|
value: {
|
||||||
|
$computed: "citiesForCountry",
|
||||||
|
args: { country: { $state: "/form/country" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "setState",
|
||||||
|
params: { statePath: "/form/city", value: "" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
citySelect: {
|
||||||
|
type: "Select",
|
||||||
|
props: {
|
||||||
|
label: "City",
|
||||||
|
name: "city",
|
||||||
|
options: { $state: "/availableCities" },
|
||||||
|
placeholder: "Select a city",
|
||||||
|
value: { $bindState: "/form/city" },
|
||||||
|
checks: [{ type: "required", message: "City is required" }],
|
||||||
|
validateOn: "change",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
sep: { type: "Separator", props: { orientation: null }, children: [] },
|
||||||
|
|
||||||
|
// $computed formatted address
|
||||||
|
addressPreview: {
|
||||||
|
type: "Heading",
|
||||||
|
props: {
|
||||||
|
text: {
|
||||||
|
$computed: "formatAddress",
|
||||||
|
args: {
|
||||||
|
city: { $state: "/form/city" },
|
||||||
|
country: { $state: "/form/country" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
level: "h3",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
// $template string interpolation
|
||||||
|
templatePreview: {
|
||||||
|
type: "Text",
|
||||||
|
props: {
|
||||||
|
text: {
|
||||||
|
$template:
|
||||||
|
"Shipping to: ${/form/city} in ${/form/country}. Cities available: ${/availableCities}",
|
||||||
|
},
|
||||||
|
variant: "muted",
|
||||||
|
},
|
||||||
|
visible: { $state: "/form/country", neq: "" },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -13,4 +13,13 @@ export const catalog = defineCatalog(schema, {
|
|||||||
description: "Fire confetti",
|
description: "Fire confetti",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
functions: {
|
||||||
|
formatAddress: {
|
||||||
|
description:
|
||||||
|
"Formats country and city into a single address string like 'City, Country'",
|
||||||
|
},
|
||||||
|
citiesForCountry: {
|
||||||
|
description: "Returns an array of city names for the given country code",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { defineRegistry } from "@json-render/react";
|
import { defineRegistry } from "@json-render/react";
|
||||||
|
import type { ComputedFunction } from "@json-render/core";
|
||||||
import { shadcnComponents } from "@json-render/shadcn";
|
import { shadcnComponents } from "@json-render/shadcn";
|
||||||
import { catalog } from "./catalog";
|
import { catalog } from "./catalog";
|
||||||
|
|
||||||
@@ -13,6 +14,14 @@ export function onConfetti(cb: () => void) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cityData: Record<string, string[]> = {
|
||||||
|
US: ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix"],
|
||||||
|
Canada: ["Toronto", "Vancouver", "Montreal", "Calgary", "Ottawa"],
|
||||||
|
UK: ["London", "Manchester", "Birmingham", "Edinburgh", "Bristol"],
|
||||||
|
Germany: ["Berlin", "Munich", "Hamburg", "Frankfurt", "Cologne"],
|
||||||
|
Japan: ["Tokyo", "Osaka", "Kyoto", "Yokohama", "Sapporo"],
|
||||||
|
};
|
||||||
|
|
||||||
export const { registry } = defineRegistry(catalog, {
|
export const { registry } = defineRegistry(catalog, {
|
||||||
components: {
|
components: {
|
||||||
...shadcnComponents,
|
...shadcnComponents,
|
||||||
@@ -24,6 +33,23 @@ export const { registry } = defineRegistry(catalog, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionHandlers: Record<string, () => void> = {
|
export const actionHandlers: Record<
|
||||||
|
string,
|
||||||
|
(params: Record<string, unknown>) => void
|
||||||
|
> = {
|
||||||
confetti: () => confettiListener?.(),
|
confetti: () => confettiListener?.(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const computedFunctions: Record<string, ComputedFunction> = {
|
||||||
|
formatAddress: (args) => {
|
||||||
|
const city = (args.city as string) ?? "";
|
||||||
|
const country = (args.country as string) ?? "";
|
||||||
|
if (!city && !country) return "No location selected";
|
||||||
|
if (!city) return country;
|
||||||
|
return `${city}, ${country}`;
|
||||||
|
},
|
||||||
|
citiesForCountry: (args) => {
|
||||||
|
const country = (args.country as string) ?? "";
|
||||||
|
return cityData[country] ?? [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "example-no-ai",
|
"name": "example-no-ai",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack --port 3003",
|
"dev": "portless no-ai-demo.json-render next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint --max-warnings 0",
|
"lint": "eslint --max-warnings 0",
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"zod": "4.3.5"
|
"zod": "4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/eslint-config": "workspace:*",
|
"@internal/eslint-config": "workspace:*",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
"@types/react": "19.2.3",
|
"@types/react": "19.2.3",
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# example-react-native
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9cef4e9]
|
||||||
|
- @json-render/core@0.10.0
|
||||||
|
- @json-render/react-native@0.10.0
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [b103676]
|
||||||
|
- @json-render/react-native@0.9.1
|
||||||
|
- @json-render/core@0.9.1
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [1d755c1]
|
||||||
|
- @json-render/core@0.9.0
|
||||||
|
- @json-render/react-native@0.9.0
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-native",
|
"name": "example-react-native",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# example-react-pdf
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9cef4e9]
|
||||||
|
- @json-render/core@0.10.0
|
||||||
|
- @json-render/react-pdf@0.10.0
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [b103676]
|
||||||
|
- @json-render/react-pdf@0.9.1
|
||||||
|
- @json-render/core@0.9.1
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [1d755c1]
|
||||||
|
- @json-render/core@0.9.0
|
||||||
|
- @json-render/react-pdf@0.9.0
|
||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-pdf",
|
"name": "example-react-pdf",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack --port 3005",
|
"dev": "portless react-pdf-demo.json-render next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit"
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"zod": "4.3.5"
|
"zod": "4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/typescript-config": "workspace:*",
|
"@internal/typescript-config": "workspace:*",
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
"@types/react": "19.2.3",
|
"@types/react": "19.2.3",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# example-remotion
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9cef4e9]
|
||||||
|
- @json-render/core@0.10.0
|
||||||
|
- @json-render/remotion@0.10.0
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- @json-render/core@0.9.1
|
||||||
|
- @json-render/remotion@0.9.1
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [1d755c1]
|
||||||
|
- @json-render/core@0.9.0
|
||||||
|
- @json-render/remotion@0.9.0
|
||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "example-remotion",
|
"name": "example-remotion",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack --port 3002",
|
"dev": "portless remotion-demo.json-render next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit"
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"zod": "^4.0.0"
|
"zod": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/eslint-config": "workspace:*",
|
"@internal/eslint-config": "workspace:*",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
"@types/react": "19.2.3",
|
"@types/react": "19.2.3",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --port 3001",
|
"dev": "portless stripe-api-demo.json-render next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start --port 3001"
|
"start": "next start --port 3001"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# com.example.json-render-demo
|
||||||
|
|
||||||
|
## 0.0.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9cef4e9]
|
||||||
|
- @json-render/core@0.10.0
|
||||||
|
- @json-render/react@0.10.0
|
||||||
|
|
||||||
|
## 0.0.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [b103676]
|
||||||
|
- @json-render/react@0.9.1
|
||||||
|
- @json-render/core@0.9.1
|
||||||
|
|
||||||
|
## 0.0.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [1d755c1]
|
||||||
|
- @json-render/core@0.9.0
|
||||||
|
- @json-render/react@0.9.0
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { config as reactConfig } from "@repo/eslint-config/react-internal";
|
import { config as reactConfig } from "@internal/eslint-config/react-internal";
|
||||||
|
|
||||||
/** @type {import("eslint").Linter.Config[]} */
|
/** @type {import("eslint").Linter.Config[]} */
|
||||||
export default [
|
export default [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.example.json-render-demo",
|
"name": "com.example.json-render-demo",
|
||||||
"version": "0.0.1",
|
"version": "0.0.4",
|
||||||
"description": "Test",
|
"description": "Test",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "~~proprietary~~",
|
"license": "~~proprietary~~",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/eslint-config": "workspace:*",
|
"@internal/eslint-config": "workspace:*",
|
||||||
"@stripe/ui-extension-tools": "^0.0.1",
|
"@stripe/ui-extension-tools": "^0.0.1",
|
||||||
"eslint": "^9.39.0"
|
"eslint": "^9.39.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export const API_GENERATE_URL = "http://localhost:3001/api/generate";
|
export const API_GENERATE_URL =
|
||||||
|
"http://stripe-api-demo.json-render.localhost:1355/api/generate";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineCatalog } from "@json-render/core";
|
import { defineCatalog } from "@json-render/core";
|
||||||
import { schema } from "@json-render/react";
|
import { schema } from "@json-render/react/schema";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export interface StripeRendererProps {
|
|||||||
/** Function to update data */
|
/** Function to update data */
|
||||||
setData?: SetState;
|
setData?: SetState;
|
||||||
/** Callback when data changes */
|
/** Callback when data changes */
|
||||||
onStateChange?: (path: string, value: unknown) => void;
|
onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
|
||||||
/** Whether the spec is currently loading/streaming */
|
/** Whether the spec is currently loading/streaming */
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# com.example.json-render-fullpage-demo
|
||||||
|
|
||||||
|
## 0.0.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9cef4e9]
|
||||||
|
- @json-render/core@0.10.0
|
||||||
|
- @json-render/react@0.10.0
|
||||||
|
|
||||||
|
## 0.0.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [b103676]
|
||||||
|
- @json-render/react@0.9.1
|
||||||
|
- @json-render/core@0.9.1
|
||||||
|
|
||||||
|
## 0.0.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [1d755c1]
|
||||||
|
- @json-render/core@0.9.0
|
||||||
|
- @json-render/react@0.9.0
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { config as reactConfig } from "@repo/eslint-config/react-internal";
|
import { config as reactConfig } from "@internal/eslint-config/react-internal";
|
||||||
|
|
||||||
/** @type {import("eslint").Linter.Config[]} */
|
/** @type {import("eslint").Linter.Config[]} */
|
||||||
export default [
|
export default [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.example.json-render-fullpage-demo",
|
"name": "com.example.json-render-fullpage-demo",
|
||||||
"version": "0.0.1",
|
"version": "0.0.4",
|
||||||
"description": "Full-page Stripe App example (alpha)",
|
"description": "Full-page Stripe App example (alpha)",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "~~proprietary~~",
|
"license": "~~proprietary~~",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/eslint-config": "workspace:*",
|
"@internal/eslint-config": "workspace:*",
|
||||||
"@stripe/ui-extension-tools": "^0.0.1",
|
"@stripe/ui-extension-tools": "^0.0.1",
|
||||||
"eslint": "^9.39.0"
|
"eslint": "^9.39.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export const API_GENERATE_URL = "http://localhost:3001/api/generate";
|
export const API_GENERATE_URL =
|
||||||
|
"http://stripe-api-demo.json-render.localhost:1355/api/generate";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineCatalog } from "@json-render/core";
|
import { defineCatalog } from "@json-render/core";
|
||||||
import { schema } from "@json-render/react";
|
import { schema } from "@json-render/react/schema";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export interface StripeRendererProps {
|
|||||||
/** Function to update data */
|
/** Function to update data */
|
||||||
setData?: SetState;
|
setData?: SetState;
|
||||||
/** Callback when data changes */
|
/** Callback when data changes */
|
||||||
onStateChange?: (path: string, value: unknown) => void;
|
onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
|
||||||
/** Whether the spec is currently loading/streaming */
|
/** Whether the spec is currently loading/streaming */
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# vite-renderers
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9cef4e9]
|
||||||
|
- @json-render/core@0.10.0
|
||||||
|
- @json-render/react@0.10.0
|
||||||
|
- @json-render/vue@0.10.0
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>@json-render multi-renderer demo</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #f3f4f6;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderer-root {
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="renderer-root"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-renderers",
|
||||||
|
"version": "0.1.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "portless vite-renderers.json-render vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@json-render/core": "workspace:*",
|
||||||
|
"@json-render/react": "workspace:*",
|
||||||
|
"@json-render/vue": "workspace:*",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
|
"vue": "^3.5.29",
|
||||||
|
"zod": "^4.3.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.4",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import "./shared/styles.css";
|
||||||
|
import { demoSpec } from "./spec";
|
||||||
|
|
||||||
|
type Renderer = "vue" | "react";
|
||||||
|
|
||||||
|
const container = document.getElementById("renderer-root") as HTMLElement;
|
||||||
|
|
||||||
|
let unmountCurrent: (() => void) | null = null;
|
||||||
|
|
||||||
|
async function switchTo(renderer: Renderer) {
|
||||||
|
unmountCurrent?.();
|
||||||
|
container.innerHTML = "";
|
||||||
|
if (renderer === "vue") {
|
||||||
|
const mod = await import("./vue/mount.ts");
|
||||||
|
mod.mount(container, renderer, demoSpec);
|
||||||
|
unmountCurrent = mod.unmount;
|
||||||
|
} else {
|
||||||
|
const mod = await import("./react/mount.tsx");
|
||||||
|
mod.mount(container, renderer, demoSpec);
|
||||||
|
unmountCurrent = mod.unmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The RendererTabs component (rendered by JSON renderer) dispatches this event
|
||||||
|
document.addEventListener("switch-renderer", (e: Event) => {
|
||||||
|
switchTo((e as CustomEvent<string>).detail as Renderer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default: Vue
|
||||||
|
switchTo("vue");
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import type { Spec } from "@json-render/core";
|
||||||
|
import {
|
||||||
|
StateProvider,
|
||||||
|
ActionProvider,
|
||||||
|
VisibilityProvider,
|
||||||
|
ValidationProvider,
|
||||||
|
Renderer,
|
||||||
|
defineRegistry,
|
||||||
|
useStateStore,
|
||||||
|
} from "@json-render/react";
|
||||||
|
import { catalog } from "./catalog";
|
||||||
|
import { components } from "./registry";
|
||||||
|
import { actionStubs, makeHandlers } from "../shared/handlers";
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components,
|
||||||
|
actions: actionStubs,
|
||||||
|
});
|
||||||
|
|
||||||
|
function DemoRenderer({ spec }: { spec: Spec }) {
|
||||||
|
const { get, set } = useStateStore();
|
||||||
|
const handlers = useMemo(() => makeHandlers(get, set), [get, set]);
|
||||||
|
return (
|
||||||
|
<ActionProvider handlers={handlers}>
|
||||||
|
<VisibilityProvider>
|
||||||
|
<ValidationProvider>
|
||||||
|
<Renderer spec={spec} registry={registry} />
|
||||||
|
</ValidationProvider>
|
||||||
|
</VisibilityProvider>
|
||||||
|
</ActionProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App({
|
||||||
|
initialRenderer = "vue",
|
||||||
|
spec,
|
||||||
|
}: {
|
||||||
|
initialRenderer?: string;
|
||||||
|
spec: Spec;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={`renderer-${initialRenderer}`}>
|
||||||
|
<StateProvider
|
||||||
|
initialState={{ ...spec.state, renderer: initialRenderer }}
|
||||||
|
>
|
||||||
|
<DemoRenderer spec={spec} />
|
||||||
|
</StateProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { schema } from "@json-render/react/schema";
|
||||||
|
import { catalogDef } from "../shared/catalog-def";
|
||||||
|
|
||||||
|
export const catalog = schema.createCatalog(catalogDef);
|
||||||
|
export type AppCatalog = typeof catalog;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { createRoot, type Root } from "react-dom/client";
|
||||||
|
import type { Spec } from "@json-render/core";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
let root: Root | null = null;
|
||||||
|
|
||||||
|
export function mount(container: HTMLElement, renderer: string, spec: Spec) {
|
||||||
|
root = createRoot(container);
|
||||||
|
root.render(<App initialRenderer={renderer} spec={spec} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unmount() {
|
||||||
|
root?.unmount();
|
||||||
|
root = null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import type { Components } from "@json-render/react";
|
||||||
|
import type { AppCatalog } from "./catalog";
|
||||||
|
|
||||||
|
export const components: Components<AppCatalog> = {
|
||||||
|
Stack: ({ props, children }) => (
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
"json-render-stack",
|
||||||
|
props.direction === "horizontal" && "json-render-stack--horizontal",
|
||||||
|
props.align && `json-render-stack--align-${props.align}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
style={{
|
||||||
|
gap: props.gap ? `${props.gap}px` : undefined,
|
||||||
|
padding: props.padding ? `${props.padding}px` : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
|
||||||
|
Card: ({ props, children }) => (
|
||||||
|
<div className="json-render-card">
|
||||||
|
{props.title && (
|
||||||
|
<div className="json-render-card-title-wrap">
|
||||||
|
<h2 className="json-render-card-title">{props.title}</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{props.subtitle && (
|
||||||
|
<p className="json-render-card-subtitle">{props.subtitle}</p>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
|
||||||
|
Text: ({ props }) => (
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"json-render-text",
|
||||||
|
props.size && props.size !== "md" && `json-render-text--${props.size}`,
|
||||||
|
props.weight &&
|
||||||
|
props.weight !== "normal" &&
|
||||||
|
`json-render-text--${props.weight}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
style={props.color ? { color: props.color } : undefined}
|
||||||
|
>
|
||||||
|
{String(props.content ?? "")}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
|
||||||
|
Button: ({ props, emit }) => (
|
||||||
|
<button
|
||||||
|
disabled={props.disabled}
|
||||||
|
onClick={() => emit("press")}
|
||||||
|
className={[
|
||||||
|
"json-render-button",
|
||||||
|
props.variant && `json-render-button--${props.variant}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
{props.label}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
|
||||||
|
Badge: ({ props }) => (
|
||||||
|
<span
|
||||||
|
className="json-render-badge"
|
||||||
|
style={
|
||||||
|
props.color
|
||||||
|
? {
|
||||||
|
backgroundColor: `${props.color}20`,
|
||||||
|
color: props.color,
|
||||||
|
borderColor: `${props.color}40`,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.label}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
|
||||||
|
ListItem: ({ props, emit }) => (
|
||||||
|
<div
|
||||||
|
onClick={() => emit("press")}
|
||||||
|
className={[
|
||||||
|
"json-render-list-item",
|
||||||
|
props.completed && "json-render-list-item--done",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
"json-render-list-item-check",
|
||||||
|
props.completed && "json-render-list-item-check--done",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
{props.completed ? "✓" : ""}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"json-render-list-item-text",
|
||||||
|
props.completed && "json-render-list-item-text--done",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
{props.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
|
||||||
|
RendererBadge: ({ props }) => (
|
||||||
|
<span className="json-render-renderer-badge">
|
||||||
|
<span className="json-render-renderer-dot" />
|
||||||
|
{props.renderer === "vue" ? "Rendered with Vue" : "Rendered with React"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
|
||||||
|
RendererTabs: ({ props, emit }) => (
|
||||||
|
<div className="json-render-renderer-tabs-wrapper">
|
||||||
|
<span className="json-render-renderer-tabs-label">Render</span>
|
||||||
|
<div className="json-render-renderer-tabs">
|
||||||
|
<button
|
||||||
|
onClick={() => emit("pressVue")}
|
||||||
|
className={[
|
||||||
|
"json-render-renderer-tab",
|
||||||
|
props.renderer === "vue" && "json-render-renderer-tab--active",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
Vue
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => emit("pressReact")}
|
||||||
|
className={[
|
||||||
|
"json-render-renderer-tab",
|
||||||
|
props.renderer === "react" && "json-render-renderer-tab--active",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
React
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared catalog definition — imported by both vue/catalog.ts and react/catalog.ts.
|
||||||
|
* Each renderer calls schema.createCatalog(catalogDef) with its own schema instance.
|
||||||
|
*/
|
||||||
|
export const catalogDef = {
|
||||||
|
components: {
|
||||||
|
Stack: {
|
||||||
|
props: z.object({
|
||||||
|
gap: z.number().optional(),
|
||||||
|
padding: z.number().optional(),
|
||||||
|
direction: z.enum(["vertical", "horizontal"]).optional(),
|
||||||
|
align: z.enum(["start", "center", "end"]).optional(),
|
||||||
|
}),
|
||||||
|
slots: ["default"],
|
||||||
|
description:
|
||||||
|
"Layout container that stacks children vertically or horizontally",
|
||||||
|
},
|
||||||
|
Card: {
|
||||||
|
props: z.object({
|
||||||
|
title: z.string().optional(),
|
||||||
|
subtitle: z.string().optional(),
|
||||||
|
}),
|
||||||
|
slots: ["default"],
|
||||||
|
description: "A card container with optional title and subtitle",
|
||||||
|
},
|
||||||
|
Text: {
|
||||||
|
props: z.object({
|
||||||
|
content: z.string(),
|
||||||
|
size: z.enum(["sm", "md", "lg", "xl"]).optional(),
|
||||||
|
weight: z.enum(["normal", "medium", "bold"]).optional(),
|
||||||
|
color: z.string().optional(),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description: "Displays a text string",
|
||||||
|
},
|
||||||
|
Button: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
variant: z.enum(["primary", "secondary", "danger"]).optional(),
|
||||||
|
disabled: z.boolean().optional(),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description: "A clickable button that emits a 'press' event",
|
||||||
|
},
|
||||||
|
Badge: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
color: z.string().optional(),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description: "A small badge/tag label",
|
||||||
|
},
|
||||||
|
ListItem: {
|
||||||
|
props: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
completed: z.boolean().optional(),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description: "A single item in a list",
|
||||||
|
},
|
||||||
|
RendererTabs: {
|
||||||
|
props: z.object({ renderer: z.string() }),
|
||||||
|
slots: [],
|
||||||
|
description:
|
||||||
|
"Segmented tab control for switching between Vue and React renderers",
|
||||||
|
},
|
||||||
|
RendererBadge: {
|
||||||
|
props: z.object({ renderer: z.string() }),
|
||||||
|
slots: [],
|
||||||
|
description: "Badge indicating which renderer is currently active",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
increment: {
|
||||||
|
params: z.object({}),
|
||||||
|
description: "Increment the counter by 1",
|
||||||
|
},
|
||||||
|
decrement: {
|
||||||
|
params: z.object({}),
|
||||||
|
description: "Decrement the counter by 1",
|
||||||
|
},
|
||||||
|
reset: { params: z.object({}), description: "Reset the counter to 0" },
|
||||||
|
toggleItem: {
|
||||||
|
params: z.object({ index: z.number() }),
|
||||||
|
description: "Toggle the completed state of a todo item",
|
||||||
|
},
|
||||||
|
switchToVue: {
|
||||||
|
params: z.object({}),
|
||||||
|
description: "Switch to the Vue renderer",
|
||||||
|
},
|
||||||
|
switchToReact: {
|
||||||
|
params: z.object({}),
|
||||||
|
description: "Switch to the React renderer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
type Get = (path: string) => unknown;
|
||||||
|
type Set = (path: string, value: unknown) => void;
|
||||||
|
|
||||||
|
/** Stub actions for defineRegistry (no-ops; real logic is in makeHandlers) */
|
||||||
|
export const actionStubs = {
|
||||||
|
increment: async () => {},
|
||||||
|
decrement: async () => {},
|
||||||
|
reset: async () => {},
|
||||||
|
toggleItem: async () => {},
|
||||||
|
switchToVue: async () => {},
|
||||||
|
switchToReact: async () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Creates action handlers that close over the state store's get/set */
|
||||||
|
export function makeHandlers(get: Get, set: Set) {
|
||||||
|
return {
|
||||||
|
increment: async () => {
|
||||||
|
set("/count", Number(get("/count") || 0) + 1);
|
||||||
|
},
|
||||||
|
decrement: async () => {
|
||||||
|
set("/count", Math.max(0, Number(get("/count") || 0) - 1));
|
||||||
|
},
|
||||||
|
reset: async () => {
|
||||||
|
set("/count", 0);
|
||||||
|
},
|
||||||
|
toggleItem: async (params: Record<string, unknown>) => {
|
||||||
|
const index = params.index as number;
|
||||||
|
const todos = (
|
||||||
|
get("/todos") as Array<{
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
completed: boolean;
|
||||||
|
}>
|
||||||
|
).slice();
|
||||||
|
const item = todos[index];
|
||||||
|
if (item) todos[index] = { ...item, completed: !item.completed };
|
||||||
|
set("/todos", todos);
|
||||||
|
},
|
||||||
|
switchToVue: async () => {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent("switch-renderer", { detail: "vue" }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
switchToReact: async () => {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent("switch-renderer", { detail: "react" }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user