Compare commits

..
Author SHA1 Message Date
Chris Tate c7a7bfb9ba add docs 2026-02-20 02:30:06 -06:00
295 changed files with 1427 additions and 22587 deletions
+2 -8
View File
@@ -10,13 +10,7 @@
"@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",
"@json-render/image"
] ]
], ],
"linked": [], "linked": [],
@@ -24,7 +18,7 @@
"baseBranch": "main", "baseBranch": "main",
"updateInternalDependencies": "patch", "updateInternalDependencies": "patch",
"privatePackages": { "privatePackages": {
"version": true, "version": false,
"tag": false "tag": false
} }
} }
+9 -2
View File
@@ -6,8 +6,11 @@ node_modules
.pnp.js .pnp.js
# Local env files # Local env files
.env* .env
!.env.example .env.local
.env.development.local
.env.test.local
.env.production.local
# Testing # Testing
coverage coverage
@@ -41,6 +44,10 @@ 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
-72
View File
@@ -26,42 +26,6 @@ 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>`
- **Web app docs (`apps/web/`):** Never use Markdown table syntax (`| col | col |`). Always use HTML `<table>` with `<thead>`, `<tbody>`, `<tr>`, `<th>`, `<td>`. Markdown tables do not render correctly in the web app. Inside HTML table cells, curly braces must be escaped as JSX expressions (e.g. `<code>{'{ "$state": "/path" }'}</code>`) because MDX parses `{` as a JSX expression boundary.
## 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
@@ -69,45 +33,9 @@ Do **not** add `--port` flags -- portless handles port assignment automatically.
- 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)
## Releases
This monorepo uses [Changesets](https://github.com/changesets/changesets) for versioning and publishing.
### Fixed version group
All public `@json-render/*` packages are in a **fixed** group (see `.changeset/config.json`). A changeset that bumps any one of them bumps all of them to the same version. You only need to list the packages that actually changed in the changeset front matter — the fixed group handles the rest.
### Preparing a release
When asked to prepare a release (e.g. "prepare v0.12.0"):
1. **Create a changeset file** at `.changeset/v0-<N>-release.md` following the existing pattern:
- YAML front matter listing changed packages with bump type (`minor` for feature releases, `patch` for bug-fix-only releases)
- A one-line summary, then `### New:` / `### Improved:` / `### Fixed:` sections describing each change
- Always list `@json-render/core` plus any packages with actual code changes
2. **Do NOT bump versions** in `package.json` files — CI runs `pnpm ci:version` (which calls `changeset version`) to do that automatically
3. **Do NOT manually write `CHANGELOG.md`** entries — `changeset version` generates them from the changeset file
4. **Add new packages to the fixed group** in `.changeset/config.json` if they should be versioned together with the rest
5. **Fill documentation gaps** — every public package should have:
- A row in the root `README.md` packages table
- A renderer section in the root `README.md` (if it's a renderer)
- An API reference page at `apps/web/app/(main)/docs/api/<name>/page.mdx`
- An entry in `apps/web/lib/page-titles.ts` and `apps/web/lib/docs-navigation.ts`
- An entry in the docs-chat system prompt (`apps/web/app/api/docs-chat/route.ts`)
- A skill at `skills/json-render-<name>/SKILL.md`
- A `packages/<name>/README.md`
6. **Run `pnpm type-check`** after all changes to verify nothing is broken
### CI scripts
- `pnpm changeset` — interactively create a new changeset
- `pnpm ci:version` — run `changeset version` + lockfile update (CI only)
- `pnpm ci:publish` — build all packages and publish to npm (CI only)
<!-- opensrc:start --> <!-- opensrc:start -->
## Source Code Reference ## Source Code Reference
+11 -90
View File
@@ -5,18 +5,15 @@
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
# for React with pre-built shadcn/ui components # pre-built shadcn/ui components
npm install @json-render/shadcn npm install @json-render/shadcn
# or for React Native # or for mobile
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?
@@ -26,7 +23,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, Vue (web), React Native (mobile) from the same catalog - **Cross-Platform** - React (web) and 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
@@ -35,7 +32,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/schema"; import { schema } from "@json-render/react";
import { z } from "zod"; import { z } from "zod";
const catalog = defineCatalog(schema, { const catalog = defineCatalog(schema, {
@@ -113,16 +110,10 @@ 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/image` | Image renderer for SVG/PNG output (OG images, social cards) via Satori |
| `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` |
| `@json-render/zustand` | Zustand adapter for `StateStore` |
| `@json-render/jotai` | Jotai adapter for `StateStore` |
| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` |
## Renderers ## Renderers
@@ -130,7 +121,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/schema"; import { schema } from "@json-render/react";
// Flat spec format (root key + elements map) // Flat spec format (root key + elements map)
const spec = { const spec = {
@@ -154,32 +145,11 @@ 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 } from "@json-render/react/schema"; import { schema, defineRegistry, Renderer } from "@json-render/react";
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";
@@ -288,35 +258,6 @@ const spec = {
const buffer = await renderToBuffer(spec); const buffer = await renderToBuffer(spec);
``` ```
### Image (SVG/PNG)
```typescript
import { renderToPng } from "@json-render/image/render";
const spec = {
root: "frame",
elements: {
frame: {
type: "Frame",
props: { width: 1200, height: 630, backgroundColor: "#1a1a2e" },
children: ["heading"],
},
heading: {
type: "Heading",
props: { text: "Hello World", level: "h1", color: "#ffffff" },
children: [],
},
},
};
// Render to PNG (requires @resvg/resvg-js)
const png = await renderToPng(spec, { fonts });
// Or render to SVG string
import { renderToSvg } from "@json-render/image/render";
const svg = await renderToSvg(spec, { fonts });
```
## Features ## Features
### Streaming (SpecStream) ### Streaming (SpecStream)
@@ -372,12 +313,10 @@ Any prop value can be data-driven using expressions:
} }
``` ```
Expression forms: Two 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 and picks a branch - **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - evaluates a condition (same syntax as visibility conditions) 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
@@ -393,22 +332,6 @@ 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
@@ -420,12 +343,10 @@ pnpm install
pnpm dev pnpm dev
``` ```
- http://json-render.localhost:1355 - Docs & Playground - http://localhost:3000 - Docs & Playground
- http://dashboard-demo.json-render.localhost:1355 - Example Dashboard - http://localhost:3001 - Example Dashboard
- http://remotion-demo.json-render.localhost:1355 - Remotion Video Example - http://localhost:3002 - 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
-37
View File
@@ -1,37 +0,0 @@
# web
## 0.1.4
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/codegen@0.11.0
- @json-render/react@0.11.0
## 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
View File
@@ -14,7 +14,7 @@ pnpm dev
bun dev bun dev
``` ```
Open [http://json-render.localhost:1355](http://json-render.localhost:1355) with your browser to see the result. Open [http://localhost:3000](http://localhost:3000) 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.
+2 -3
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "A2UI Integration" }
export const metadata = pageMetadata("docs/a2ui")
# A2UI Integration # A2UI Integration
@@ -67,7 +66,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/schema'; import { schema } from '@json-render/react';
import { z } from 'zod'; import { z } from 'zod';
// A2UI BoundValue schema // A2UI BoundValue schema
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Adaptive Cards Integration" }
export const metadata = pageMetadata("docs/adaptive-cards")
# Adaptive Cards Integration # Adaptive Cards Integration
@@ -90,7 +89,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/schema'; import { schema } from '@json-render/react';
import { z } from 'zod'; import { z } from 'zod';
// Common Adaptive Cards properties // Common Adaptive Cards properties
+2 -3
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "AG-UI Integration" }
export const metadata = pageMetadata("docs/ag-ui")
# AG-UI Integration # AG-UI Integration
@@ -151,7 +150,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/schema'; import { schema } from '@json-render/react';
import { z } from 'zod'; import { z } from 'zod';
export const aguiCatalog = defineCatalog(schema, { export const aguiCatalog = defineCatalog(schema, {
+1 -2
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "AI SDK Integration" }
export const metadata = pageMetadata("docs/ai-sdk")
# AI SDK Integration # AI SDK Integration
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "@json-render/codegen API" }
export const metadata = pageMetadata("docs/api/codegen")
# @json-render/codegen # @json-render/codegen
+3 -4
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "@json-render/core API" }
export const metadata = pageMetadata("docs/api/core")
# @json-render/core # @json-render/core
@@ -11,7 +10,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/schema'; import { schema } from '@json-render/react';
function defineCatalog<T extends ZodType>( function defineCatalog<T extends ZodType>(
s: T, s: T,
@@ -119,7 +118,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/schema'; import { schema } from '@json-render/react';
// schema defines: // schema defines:
// - Spec shape: { root: string, elements: Record<string, UIElement> } // - Spec shape: { root: string, elements: Record<string, UIElement> }
-364
View File
@@ -1,364 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/image")
# @json-render/image
Image renderer. Turn JSON specs into SVG and PNG images using [Satori](https://github.com/vercel/satori).
## Install
```bash
npm install @json-render/core @json-render/image
```
For PNG output, also install the optional peer dependency:
```bash
npm install @resvg/resvg-js
```
See the [Image example](https://github.com/vercel-labs/json-render/tree/main/examples/image) for a full working example.
## schema
The image element schema for image specs. Use with `defineCatalog` from core.
```typescript
import { defineCatalog } from '@json-render/core';
import { schema, standardComponentDefinitions } from '@json-render/image';
const catalog = defineCatalog(schema, {
components: standardComponentDefinitions,
});
```
## Render Functions
Server-side functions for producing image output. Both accept a spec and optional `RenderOptions`.
```typescript
import { renderToSvg, renderToPng } from '@json-render/image/render';
const svg = await renderToSvg(spec, { fonts });
const png = await renderToPng(spec, { fonts });
await writeFile('output.png', png);
```
### RenderOptions
```typescript
interface RenderOptions {
registry?: ComponentRegistry;
includeStandard?: boolean; // default: true
state?: Record<string, unknown>;
fonts?: SatoriOptions['fonts'];
width?: number;
height?: number;
}
```
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>fonts</code></td>
<td><code>{"SatoriOptions['fonts']"}</code></td>
<td><code>[]</code></td>
<td>Font data for text rendering (required for meaningful output)</td>
</tr>
<tr>
<td><code>width</code></td>
<td><code>number</code></td>
<td>Frame prop</td>
<td>Override the output image width</td>
</tr>
<tr>
<td><code>height</code></td>
<td><code>number</code></td>
<td>Frame prop</td>
<td>Override the output image height</td>
</tr>
<tr>
<td><code>registry</code></td>
<td><code>{"Record<string, ComponentRenderer>"}</code></td>
<td><code>{"{}"}</code></td>
<td>Custom component map (merged with standard components)</td>
</tr>
<tr>
<td><code>includeStandard</code></td>
<td><code>boolean</code></td>
<td><code>true</code></td>
<td>Include built-in standard components</td>
</tr>
<tr>
<td><code>state</code></td>
<td><code>{"Record<string, unknown>"}</code></td>
<td><code>{"{}"}</code></td>
<td>Initial state for <code>$state</code> / <code>$cond</code> dynamic prop resolution</td>
</tr>
</tbody>
</table>
## Standard Components
### Root
#### Frame
Root image container. Defines the output image dimensions and background. Must be the root element.
```typescript
{
width: number;
height: number;
backgroundColor: string | null;
padding: number | null;
display: "flex" | "none" | null;
flexDirection: "row" | "column" | null;
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
}
```
### Layout
#### Box
Generic container with padding, margin, background, border, and flex alignment. Supports absolute positioning.
```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;
width: number | string | null;
height: number | string | null;
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
flexDirection: "row" | "column" | null;
position: "relative" | "absolute" | null;
top: number | null;
left: number | null;
right: number | null;
bottom: number | null;
overflow: "visible" | "hidden" | 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
Heading text at various levels. h1 is largest, h4 is smallest.
```typescript
{
text: string;
level: "h1" | "h2" | "h3" | "h4" | null;
color: string | null;
align: "left" | "center" | "right" | null;
letterSpacing: number | string | null;
lineHeight: number | null;
}
```
#### Text
Body text with configurable size, color, weight, and alignment.
```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;
letterSpacing: number | string | null;
textDecoration: "none" | "underline" | "line-through" | null;
}
```
#### Image
Image from a URL with optional dimensions and fit.
```typescript
{
src: string;
width: number | null;
height: number | null;
borderRadius: number | null;
objectFit: "contain" | "cover" | "fill" | "none" | 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;
}
```
## Catalog Definitions
Pre-built definitions for creating image catalogs:
```typescript
import { standardComponentDefinitions } from '@json-render/image/catalog';
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/image';
const catalog = defineCatalog(schema, {
components: {
...standardComponentDefinitions,
// Add custom components
},
});
```
## Server-Safe Import
Import schema and catalog definitions without pulling in React or Satori:
```typescript
import { schema, standardComponentDefinitions } from '@json-render/image/server';
```
## Sub-path Exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>@json-render/image</code></td>
<td>Full package: schema, renderer, components, render functions</td>
</tr>
<tr>
<td><code>@json-render/image/server</code></td>
<td>Schema and catalog definitions only (no React or Satori)</td>
</tr>
<tr>
<td><code>@json-render/image/catalog</code></td>
<td>Standard component definitions and types</td>
</tr>
<tr>
<td><code>@json-render/image/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>ImageSchema</code></td>
<td>Schema type for image specs</td>
</tr>
<tr>
<td><code>ImageSpec</code></td>
<td>Spec type for image output</td>
</tr>
<tr>
<td><code>RenderOptions</code></td>
<td>Options for render functions</td>
</tr>
<tr>
<td><code>ComponentRenderProps</code></td>
<td>Props passed to component render functions</td>
</tr>
<tr>
<td><code>ComponentRenderer</code></td>
<td>Component render function type</td>
</tr>
<tr>
<td><code>ComponentRegistry</code></td>
<td>Map of component names to render functions</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,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "@json-render/react-native API" }
export const metadata = pageMetadata("docs/api/react-native")
# @json-render/react-native # @json-render/react-native
@@ -9,120 +8,65 @@ React Native renderer with standard components, providers, and hooks.
### Layout ### Layout
<table> | Component | Props | Description |
<thead> |-----------|-------|-------------|
<tr><th>Component</th><th>Props</th><th>Description</th></tr> | `Container` | `padding`, `background`, `borderRadius`, `borderColor`, `flex` | Basic wrapper with styling |
</thead> | `Row` | `gap`, `align`, `justify`, `flex`, `wrap` | Horizontal flex layout |
<tbody> | `Column` | `gap`, `align`, `justify`, `flex` | Vertical flex layout |
<tr><td><code>Container</code></td><td><code>padding</code>, <code>background</code>, <code>borderRadius</code>, <code>borderColor</code>, <code>flex</code></td><td>Basic wrapper with styling</td></tr> | `ScrollContainer` | `direction` | Scrollable area (vertical or horizontal) |
<tr><td><code>Row</code></td><td><code>gap</code>, <code>align</code>, <code>justify</code>, <code>flex</code>, <code>wrap</code></td><td>Horizontal flex layout</td></tr> | `SafeArea` | `edges` | Safe area insets for notch/home indicator |
<tr><td><code>Column</code></td><td><code>gap</code>, <code>align</code>, <code>justify</code>, <code>flex</code></td><td>Vertical flex layout</td></tr> | `Pressable` | `action`, `actionParams` | Touchable wrapper that triggers actions |
<tr><td><code>ScrollContainer</code></td><td><code>direction</code></td><td>Scrollable area (vertical or horizontal)</td></tr> | `Spacer` | `size`, `flex` | Fixed or flexible spacing |
<tr><td><code>SafeArea</code></td><td><code>edges</code></td><td>Safe area insets for notch/home indicator</td></tr> | `Divider` | `color`, `thickness` | Thin line separator |
<tr><td><code>Pressable</code></td><td><code>action</code>, <code>actionParams</code></td><td>Touchable wrapper that triggers actions</td></tr>
<tr><td><code>Spacer</code></td><td><code>size</code>, <code>flex</code></td><td>Fixed or flexible spacing</td></tr>
<tr><td><code>Divider</code></td><td><code>color</code>, <code>thickness</code></td><td>Thin line separator</td></tr>
</tbody>
</table>
### Content ### Content
<table> | Component | Props | Description |
<thead> |-----------|-------|-------------|
<tr><th>Component</th><th>Props</th><th>Description</th></tr> | `Heading` | `text`, `level`, `align`, `color` | Heading text (levels 1-6) |
</thead> | `Paragraph` | `text`, `align`, `color` | Body text |
<tbody> | `Label` | `text`, `color`, `bold` | Small label text |
<tr><td><code>Heading</code></td><td><code>text</code>, <code>level</code>, <code>align</code>, <code>color</code></td><td>Heading text (levels 1-6)</td></tr> | `Image` | `uri`, `width`, `height`, `resizeMode`, `borderRadius` | Image display |
<tr><td><code>Paragraph</code></td><td><code>text</code>, <code>align</code>, <code>color</code></td><td>Body text</td></tr> | `Avatar` | `uri`, `size`, `fallback` | Circular avatar |
<tr><td><code>Label</code></td><td><code>text</code>, <code>color</code>, <code>bold</code></td><td>Small label text</td></tr> | `Badge` | `label`, `color`, `textColor` | Status badge |
<tr><td><code>Image</code></td><td><code>uri</code>, <code>width</code>, <code>height</code>, <code>resizeMode</code>, <code>borderRadius</code></td><td>Image display</td></tr> | `Chip` | `label`, `selected`, `color` | Tag/chip |
<tr><td><code>Avatar</code></td><td><code>uri</code>, <code>size</code>, <code>fallback</code></td><td>Circular avatar</td></tr>
<tr><td><code>Badge</code></td><td><code>label</code>, <code>color</code>, <code>textColor</code></td><td>Status badge</td></tr>
<tr><td><code>Chip</code></td><td><code>label</code>, <code>selected</code>, <code>color</code></td><td>Tag/chip</td></tr>
</tbody>
</table>
### Input ### Input
<table> | Component | Props | Description |
<thead> |-----------|-------|-------------|
<tr><th>Component</th><th>Props</th><th>Description</th></tr> | `Button` | `label`, `variant`, `size`, `disabled`, `action`, `actionParams` | Pressable button |
</thead> | `TextInput` | `placeholder`, `value` (use `$bindState`), `secure`, `keyboardType`, `multiline` | Text input field |
<tbody> | `Switch` | `checked` (use `$bindState`), `label` | Toggle switch |
<tr><td><code>Button</code></td><td><code>label</code>, <code>variant</code>, <code>size</code>, <code>disabled</code>, <code>action</code>, <code>actionParams</code></td><td>Pressable button</td></tr> | `Checkbox` | `checked` (use `$bindState`), `label` | Checkbox with label |
<tr><td><code>TextInput</code></td><td><code>placeholder</code>, <code>value</code> (use <code>$bindState</code>), <code>secure</code>, <code>keyboardType</code>, <code>multiline</code></td><td>Text input field</td></tr> | `Slider` | `value` (use `$bindState`), `min`, `max`, `step` | Range slider |
<tr><td><code>Switch</code></td><td><code>checked</code> (use <code>$bindState</code>), <code>label</code></td><td>Toggle switch</td></tr> | `SearchBar` | `placeholder`, `value` (use `$bindState`) | Search input |
<tr><td><code>Checkbox</code></td><td><code>checked</code> (use <code>$bindState</code>), <code>label</code></td><td>Checkbox with label</td></tr>
<tr><td><code>Slider</code></td><td><code>value</code> (use <code>$bindState</code>), <code>min</code>, <code>max</code>, <code>step</code></td><td>Range slider</td></tr>
<tr><td><code>SearchBar</code></td><td><code>placeholder</code>, <code>value</code> (use <code>$bindState</code>)</td><td>Search input</td></tr>
</tbody>
</table>
### Feedback ### Feedback
<table> | Component | Props | Description |
<thead> |-----------|-------|-------------|
<tr><th>Component</th><th>Props</th><th>Description</th></tr> | `Spinner` | `size`, `color` | Loading indicator |
</thead> | `ProgressBar` | `progress`, `color`, `trackColor` | Progress indicator |
<tbody>
<tr><td><code>Spinner</code></td><td><code>size</code>, <code>color</code></td><td>Loading indicator</td></tr>
<tr><td><code>ProgressBar</code></td><td><code>progress</code>, <code>color</code>, <code>trackColor</code></td><td>Progress indicator</td></tr>
</tbody>
</table>
### Composite ### Composite
<table> | Component | Props | Description |
<thead> |-----------|-------|-------------|
<tr><th>Component</th><th>Props</th><th>Description</th></tr> | `Card` | `title`, `subtitle`, `padding` | Card container |
</thead> | `ListItem` | `title`, `subtitle`, `leading`, `trailing`, `action`, `actionParams` | List row |
<tbody> | `Modal` | `visible`, `title` | Bottom sheet modal |
<tr><td><code>Card</code></td><td><code>title</code>, <code>subtitle</code>, <code>padding</code></td><td>Card container</td></tr>
<tr><td><code>ListItem</code></td><td><code>title</code>, <code>subtitle</code>, <code>leading</code>, <code>trailing</code>, <code>action</code>, <code>actionParams</code></td><td>List row</td></tr>
<tr><td><code>Modal</code></td><td><code>visible</code>, <code>title</code></td><td>Bottom sheet modal</td></tr>
</tbody>
</table>
## Providers ## Providers
### StateProvider ### StateProvider
```tsx ```tsx
<StateProvider initialState={object} onStateChange={fn}> <StateProvider initialState={object}>
{children} {children}
</StateProvider> </StateProvider>
``` ```
<table>
<thead>
<tr><th>Prop</th><th>Type</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>store</code></td><td><code>StateStore</code></td><td>External store (controlled mode). When provided, <code>initialState</code> and <code>onStateChange</code> are ignored.</td></tr>
<tr><td><code>initialState</code></td><td><code>Record&lt;string, unknown&gt;</code></td><td>Initial state model (uncontrolled mode).</td></tr>
<tr><td><code>onStateChange</code></td><td><code>{'(changes: Array<{ path: string; value: unknown }>) => void'}</code></td><td>Callback when state changes (uncontrolled mode). Called once per <code>set</code> or <code>update</code> with all changed entries.</td></tr>
</tbody>
</table>
#### 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
@@ -220,13 +164,8 @@ import { standardComponentDefinitions, standardActionDefinitions } from "@json-r
import { schema } from "@json-render/react-native/schema"; import { schema } from "@json-render/react-native/schema";
``` ```
<table> | Export | Purpose |
<thead> |--------|---------|
<tr><th>Export</th><th>Purpose</th></tr> | `standardComponentDefinitions` | Catalog definitions for all 25+ standard components |
</thead> | `standardActionDefinitions` | Catalog definitions for standard actions (setState, navigate) |
<tbody> | `schema` | React Native element tree schema |
<tr><td><code>standardComponentDefinitions</code></td><td>Catalog definitions for all 25+ standard components</td></tr>
<tr><td><code>standardActionDefinitions</code></td><td>Catalog definitions for standard actions (setState, navigate)</td></tr>
<tr><td><code>schema</code></td><td>React Native element tree schema</td></tr>
</tbody>
</table>
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "@json-render/react-pdf API" }
export const metadata = pageMetadata("docs/api/react-pdf")
# @json-render/react-pdf # @json-render/react-pdf
@@ -108,17 +107,14 @@ const PDFRenderer = createRenderer(catalog, components);
```typescript ```typescript
interface CreateRendererProps { interface CreateRendererProps {
spec: Spec | null; spec: Spec | null;
store?: StateStore;
state?: Record<string, unknown>; state?: Record<string, unknown>;
onAction?: (actionName: string, params?: Record<string, unknown>) => void; onAction?: (actionName: string, params?: Record<string, unknown>) => void;
onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void; onStateChange?: (path: string, value: unknown) => void;
loading?: boolean; loading?: boolean;
fallback?: ComponentRenderer; fallback?: ComponentRenderer;
} }
``` ```
When `store` is provided, `state` and `onStateChange` are ignored (controlled mode).
## Renderer ## Renderer
The main component that renders a spec to `@react-pdf/renderer` elements. The main component that renders a spec to `@react-pdf/renderer` elements.
@@ -347,19 +343,6 @@ Renders current page number and total pages. Format uses `{pageNumber}` and `{to
} }
``` ```
## 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 ## Server-Safe Import
Import schema and catalog definitions without pulling in React or `@react-pdf/renderer`: Import schema and catalog definitions without pulling in React or `@react-pdf/renderer`:
+2 -92
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "@json-render/react API" }
export const metadata = pageMetadata("docs/api/react")
# @json-render/react # @json-render/react
@@ -10,57 +9,11 @@ React components, providers, and hooks.
### StateProvider ### StateProvider
```tsx ```tsx
<StateProvider initialState={object} onStateChange={fn}> <StateProvider initialState={object}>
{children} {children}
</StateProvider> </StateProvider>
``` ```
<table>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>store</code></td>
<td><code>StateStore</code></td>
<td>External store (controlled mode). When provided, <code>initialState</code> and <code>onStateChange</code> are ignored.</td>
</tr>
<tr>
<td><code>initialState</code></td>
<td><code>Record&lt;string, unknown&gt;</code></td>
<td>Initial state model (uncontrolled mode).</td>
</tr>
<tr>
<td><code>onStateChange</code></td>
<td><code>{'(changes: Array<{ path: string; value: unknown }>) => void'}</code></td>
<td>Callback when state changes (uncontrolled mode). Called once per <code>set</code> or <code>update</code> with all changed entries.</td>
</tr>
</tbody>
</table>
#### 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
@@ -130,40 +83,6 @@ 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:
<table>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>functions</code></td>
<td><code>Record&lt;string, ComputedFunction&gt;</code></td>
<td>Named functions for <code>$computed</code> expressions in props</td>
</tr>
</tbody>
</table>
```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
@@ -292,15 +211,6 @@ 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,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "@json-render/remotion API" }
export const metadata = pageMetadata("docs/api/remotion")
# @json-render/remotion # @json-render/remotion
+53 -227
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "@json-render/shadcn API" }
export const metadata = pageMetadata("docs/api/shadcn")
# @json-render/shadcn # @json-render/shadcn
@@ -15,27 +14,10 @@ Your app must have Tailwind CSS configured.
## Entry Points ## Entry Points
<table> | Entry Point | Exports | Use For |
<thead> |-------------|---------|---------|
<tr> | `@json-render/shadcn` | `shadcnComponents` | React implementations |
<th>Entry Point</th> | `@json-render/shadcn/catalog` | `shadcnComponentDefinitions` | Catalog schemas (no React dependency, safe for server) |
<th>Exports</th>
<th>Use For</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>@json-render/shadcn</code></td>
<td><code>shadcnComponents</code></td>
<td>React implementations</td>
</tr>
<tr>
<td><code>@json-render/shadcn/catalog</code></td>
<td><code>shadcnComponentDefinitions</code></td>
<td>Catalog schemas (no React dependency, safe for server)</td>
</tr>
</tbody>
</table>
## Usage ## Usage
@@ -120,225 +102,69 @@ const { registry } = defineRegistry(catalog, {
### Layout ### Layout
<table> | Component | Description |
<thead> |-----------|-------------|
<tr> | `Card` | Container card with optional title, description, maxWidth, centered |
<th>Component</th> | `Stack` | Flex container with direction, gap, align, justify |
<th>Description</th> | `Grid` | Grid layout with columns (1-6) and gap |
</tr> | `Separator` | Visual separator line with orientation |
</thead>
<tbody>
<tr>
<td><code>Card</code></td>
<td>Container card with optional title, description, maxWidth, centered</td>
</tr>
<tr>
<td><code>Stack</code></td>
<td>Flex container with direction, gap, align, justify</td>
</tr>
<tr>
<td><code>Grid</code></td>
<td>Grid layout with columns (1-6) and gap</td>
</tr>
<tr>
<td><code>Separator</code></td>
<td>Visual separator line with orientation</td>
</tr>
</tbody>
</table>
### Navigation ### Navigation
<table> | Component | Description |
<thead> |-----------|-------------|
<tr> | `Tabs` | Tabbed navigation with tabs array, defaultValue, value |
<th>Component</th> | `Accordion` | Collapsible sections with items array and type (single/multiple) |
<th>Description</th> | `Collapsible` | Single collapsible section with title and defaultOpen |
</tr> | `Pagination` | Page navigation with totalPages and page |
</thead>
<tbody>
<tr>
<td><code>Tabs</code></td>
<td>Tabbed navigation with tabs array, defaultValue, value</td>
</tr>
<tr>
<td><code>Accordion</code></td>
<td>Collapsible sections with items array and type (single/multiple)</td>
</tr>
<tr>
<td><code>Collapsible</code></td>
<td>Single collapsible section with title and defaultOpen</td>
</tr>
<tr>
<td><code>Pagination</code></td>
<td>Page navigation with totalPages and page</td>
</tr>
</tbody>
</table>
### Overlay ### Overlay
<table> | Component | Description |
<thead> |-----------|-------------|
<tr> | `Dialog` | Modal dialog with title, description, openPath |
<th>Component</th> | `Drawer` | Bottom drawer with title, description, openPath |
<th>Description</th> | `Tooltip` | Hover tooltip with content and text |
</tr> | `Popover` | Click-triggered popover with trigger and content |
</thead> | `DropdownMenu` | Dropdown menu with label and items array |
<tbody>
<tr>
<td><code>Dialog</code></td>
<td>Modal dialog with title, description, openPath</td>
</tr>
<tr>
<td><code>Drawer</code></td>
<td>Bottom drawer with title, description, openPath</td>
</tr>
<tr>
<td><code>Tooltip</code></td>
<td>Hover tooltip with content and text</td>
</tr>
<tr>
<td><code>Popover</code></td>
<td>Click-triggered popover with trigger and content</td>
</tr>
<tr>
<td><code>DropdownMenu</code></td>
<td>Dropdown menu with label and items array</td>
</tr>
</tbody>
</table>
### Content ### Content
<table> | Component | Description |
<thead> |-----------|-------------|
<tr> | `Heading` | Heading text with level (h1-h4) |
<th>Component</th> | `Text` | Paragraph with variant (body, caption, muted, lead, code) |
<th>Description</th> | `Image` | Image with alt, width, height |
</tr> | `Avatar` | User avatar with src, name, size |
</thead> | `Badge` | Status badge with text and variant |
<tbody> | `Alert` | Alert banner with title, message, type |
<tr> | `Carousel` | Horizontally scrollable carousel with items |
<td><code>Heading</code></td> | `Table` | Data table with columns and rows |
<td>Heading text with level (h1-h4)</td>
</tr>
<tr>
<td><code>Text</code></td>
<td>Paragraph with variant (body, caption, muted, lead, code)</td>
</tr>
<tr>
<td><code>Image</code></td>
<td>Image with alt, width, height</td>
</tr>
<tr>
<td><code>Avatar</code></td>
<td>User avatar with src, name, size</td>
</tr>
<tr>
<td><code>Badge</code></td>
<td>Status badge with text and variant</td>
</tr>
<tr>
<td><code>Alert</code></td>
<td>Alert banner with title, message, type</td>
</tr>
<tr>
<td><code>Carousel</code></td>
<td>Horizontally scrollable carousel with items</td>
</tr>
<tr>
<td><code>Table</code></td>
<td>Data table with columns and rows</td>
</tr>
</tbody>
</table>
### Feedback ### Feedback
<table> | Component | Description |
<thead> |-----------|-------------|
<tr> | `Progress` | Progress bar with value, max, label |
<th>Component</th> | `Skeleton` | Loading placeholder with width, height, rounded |
<th>Description</th> | `Spinner` | Loading spinner with size and label |
</tr>
</thead>
<tbody>
<tr>
<td><code>Progress</code></td>
<td>Progress bar with value, max, label</td>
</tr>
<tr>
<td><code>Skeleton</code></td>
<td>Loading placeholder with width, height, rounded</td>
</tr>
<tr>
<td><code>Spinner</code></td>
<td>Loading spinner with size and label</td>
</tr>
</tbody>
</table>
### Input ### Input
<table> | Component | Description |
<thead> |-----------|-------------|
<tr> | `Button` | Clickable button with label, variant, disabled |
<th>Component</th> | `Link` | Anchor link with label and href |
<th>Description</th> | `Input` | Text input with label, name, type, placeholder, value, checks |
</tr> | `Textarea` | Multi-line text input with label, name, placeholder, rows, value, checks |
</thead> | `Select` | Dropdown select with label, name, options, value, checks |
<tbody> | `Checkbox` | Checkbox with label, name, checked |
<tr> | `Radio` | Radio button group with label, name, options, value |
<td><code>Button</code></td> | `Switch` | Toggle switch with label, name, checked |
<td>Clickable button with label, variant, disabled</td> | `Slider` | Range slider with label, min, max, step, value |
</tr> | `Toggle` | Toggle button with label, pressed, variant |
<tr> | `ToggleGroup` | Group of toggle buttons with items, type, value |
<td><code>Link</code></td> | `ButtonGroup` | Group of buttons with buttons array and selected |
<td>Anchor link with label and href</td>
</tr>
<tr>
<td><code>Input</code></td>
<td>Text input with label, name, type, placeholder, value, checks</td>
</tr>
<tr>
<td><code>Textarea</code></td>
<td>Multi-line text input with label, name, placeholder, rows, value, checks</td>
</tr>
<tr>
<td><code>Select</code></td>
<td>Dropdown select with label, name, options, value, checks</td>
</tr>
<tr>
<td><code>Checkbox</code></td>
<td>Checkbox with label, name, checked</td>
</tr>
<tr>
<td><code>Radio</code></td>
<td>Radio button group with label, name, options, value</td>
</tr>
<tr>
<td><code>Switch</code></td>
<td>Toggle switch with label, name, checked</td>
</tr>
<tr>
<td><code>Slider</code></td>
<td>Range slider with label, min, max, step, value</td>
</tr>
<tr>
<td><code>Toggle</code></td>
<td>Toggle button with label, pressed, variant</td>
</tr>
<tr>
<td><code>ToggleGroup</code></td>
<td>Group of toggle buttons with items, type, value</td>
</tr>
<tr>
<td><code>ButtonGroup</code></td>
<td>Group of buttons with buttons array and selected</td>
</tr>
</tbody>
</table>
## Notes ## Notes
-337
View File
@@ -1,337 +0,0 @@
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>
```
<table>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>store</code></td>
<td><code>StateStore</code></td>
<td>External store (controlled mode). When provided, <code>initialState</code> and <code>onStateChange</code> are ignored.</td>
</tr>
<tr>
<td><code>initialState</code></td>
<td><code>Record&lt;string, unknown&gt;</code></td>
<td>Initial state model (uncontrolled mode).</td>
</tr>
<tr>
<td><code>onStateChange</code></td>
<td><code>{'(changes: Array<{ path: string; value: unknown }>) => void'}</code></td>
<td>Callback when state changes (uncontrolled mode). Called once per <code>set</code> or <code>update</code> with all changed entries.</td>
</tr>
</tbody>
</table>
#### 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`
<table>
<thead>
<tr>
<th>API</th>
<th>React</th>
<th>Vue</th>
<th>Note</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>useStateStore().state</code></td>
<td><code>StateModel</code> (plain object)</td>
<td><code>ShallowRef&lt;StateModel&gt;</code></td>
<td>Vue reactivity; use <code>state.value</code></td>
</tr>
<tr>
<td><code>useStateValue()</code></td>
<td><code>T | undefined</code></td>
<td><code>ComputedRef&lt;T | undefined&gt;</code></td>
<td>Vue reactivity; use <code>.value</code></td>
</tr>
<tr>
<td><code>useStateBinding()</code></td>
<td><code>[T | undefined, setter]</code></td>
<td><code>[ComputedRef&lt;T | undefined&gt;, setter]</code></td>
<td>Vue reactivity; use <code>value.value</code></td>
</tr>
<tr>
<td><code>useAction().isLoading</code></td>
<td><code>boolean</code></td>
<td><code>ComputedRef&lt;boolean&gt;</code></td>
<td>Vue reactivity; use <code>.value</code></td>
</tr>
<tr>
<td><code>useFieldValidation().state</code></td>
<td><code>FieldValidationState</code></td>
<td><code>ComputedRef&lt;FieldValidationState&gt;</code></td>
<td>Vue reactivity; use <code>.value</code></td>
</tr>
<tr>
<td><code>useFieldValidation().errors</code></td>
<td><code>string[]</code></td>
<td><code>ComputedRef&lt;string[]&gt;</code></td>
<td>Vue reactivity; use <code>.value</code></td>
</tr>
<tr>
<td><code>useFieldValidation().isValid</code></td>
<td><code>boolean</code></td>
<td><code>ComputedRef&lt;boolean&gt;</code></td>
<td>Vue reactivity; use <code>.value</code></td>
</tr>
<tr>
<td><code>VisibilityContextValue.ctx</code></td>
<td><code>CoreVisibilityContext</code></td>
<td><code>ComputedRef&lt;CoreVisibilityContext&gt;</code></td>
<td>Vue reactivity; use <code>ctx.value</code></td>
</tr>
<tr>
<td><code>children</code> type</td>
<td><code>React.ReactNode</code></td>
<td><code>VNode | VNode[]</code></td>
<td>Platform-specific</td>
</tr>
<tr>
<td><code>useBoundProp</code></td>
<td>exported</td>
<td>exported</td>
<td>Same API; returns <code>[value, setValue]</code></td>
</tr>
<tr>
<td><code>VisibilityProviderProps</code></td>
<td>exported</td>
<td>not exported (no props)</td>
<td>Vue uses slot, no prop needed</td>
</tr>
<tr>
<td>Streaming hooks</td>
<td><code>useUIStream</code>, <code>useChatUI</code></td>
<td><code>useUIStream</code>, <code>useChatUI</code></td>
<td>Same API; returns Vue <code>Ref</code> values</td>
</tr>
</tbody>
</table>
+2 -3
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Catalog" }
export const metadata = pageMetadata("docs/catalog")
# Catalog # Catalog
@@ -19,7 +18,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/schema'; // or '@json-render/react-native/schema' import { schema } from '@json-render/react'; // or '@json-render/react-native'
import { z } from 'zod'; import { z } from 'zod';
const catalog = defineCatalog(schema, { const catalog = defineCatalog(schema, {
+1 -179
View File
@@ -1,187 +1,9 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Changelog" }
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,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Code Export" }
export const metadata = pageMetadata("docs/code-export")
# Code Export # Code Export
@@ -135,6 +134,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://dashboard-demo.json-render.localhost:1355 # Open http://localhost:3001
# Generate a widget, then click "Export Project" # Generate a widget, then click "Export Project"
``` ```
@@ -1,119 +0,0 @@
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,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Custom Schema & Renderer" }
export const metadata = pageMetadata("docs/custom-schema")
# Custom Schema & Renderer # Custom Schema & Renderer
+1 -47
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Data Binding" }
export const metadata = pageMetadata("docs/data-binding")
# Data Binding # Data Binding
@@ -213,22 +212,6 @@ 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">
@@ -271,39 +254,10 @@ See [Computed Values](/docs/computed-values) for details on `$template` and `$co
<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,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Generation Modes" }
export const metadata = pageMetadata("docs/generation-modes")
# Generation Modes # Generation Modes
+6 -24
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Installation" }
export const metadata = pageMetadata("docs/installation")
# Installation # Installation
@@ -9,18 +8,6 @@ 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:
@@ -37,19 +24,14 @@ 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" />
## For External State Management (Optional) ## 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: json-render requires the following peer dependencies:
<PackageInstall packages="@json-render/zustand" /> - `react` ^19.0.0
- `zod` ^4.0.0
<PackageInstall packages="@json-render/redux" /> <PackageInstall packages="react zod" />
<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
+32 -98
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Migration Guide" }
export const metadata = pageMetadata("docs/migration")
# Migration Guide # Migration Guide
@@ -31,23 +30,15 @@ import { StateProvider } from "@json-render/react";
`StateProvider` now manages state internally. Use `useStateStore()` to access `get`, `set`, and `update`. `StateProvider` now manages state internally. Use `useStateStore()` to access `get`, `set`, and `update`.
<table> | Before | After |
<thead> |--------|-------|
<tr> | `DataProvider` | `StateProvider` |
<th>Before</th> | `data` prop | `initialState` prop |
<th>After</th> | `getValue` / `setValue` props | Removed (use `useStateStore()` hook for `get` / `set`) |
</tr> | `useData` | `useStateStore` |
</thead> | `useDataValue` | `useStateValue` |
<tbody> | `useDataBinding` | `useStateBinding` (deprecated, use `useBoundProp` instead) |
<tr><td><code>DataProvider</code></td><td><code>StateProvider</code></td></tr> | `DataModel` type | `StateModel` type |
<tr><td><code>data</code> prop</td><td><code>initialState</code> prop</td></tr>
<tr><td><code>getValue</code> / <code>setValue</code> props</td><td>Removed (use <code>useStateStore()</code> hook for <code>get</code> / <code>set</code>)</td></tr>
<tr><td><code>useData</code></td><td><code>useStateStore</code></td></tr>
<tr><td><code>useDataValue</code></td><td><code>useStateValue</code></td></tr>
<tr><td><code>useDataBinding</code></td><td><code>useStateBinding</code> (deprecated, use <code>useBoundProp</code> instead)</td></tr>
<tr><td><code>DataModel</code> type</td><td><code>StateModel</code> type</td></tr>
</tbody>
</table>
## Dynamic Expressions ## Dynamic Expressions
@@ -89,18 +80,10 @@ Inside repeat scopes, use `$item` and `$index`:
} }
``` ```
<table> | Before | After |
<thead> |--------|-------|
<tr> | `{ "$path": "/..." }` | `{ "$state": "/..." }` |
<th>Before</th> | `{ "$data": "/..." }` | `{ "$state": "/..." }` |
<th>After</th>
</tr>
</thead>
<tbody>
<tr><td><code>{'{ "$path": "/..." }'}</code></td><td><code>{'{ "$state": "/..." }'}</code></td></tr>
<tr><td><code>{'{ "$data": "/..." }'}</code></td><td><code>{'{ "$state": "/..." }'}</code></td></tr>
</tbody>
</table>
## Two-Way Binding ## Two-Way Binding
@@ -331,19 +314,11 @@ const chatPrompt = catalog.prompt({ mode: "chat" });
{ "type": "minLength", "args": { "min": 8 }, "message": "Too short" } { "type": "minLength", "args": { "min": 8 }, "message": "Too short" }
``` ```
<table> | Before | After |
<thead> |--------|-------|
<tr> | `{ fn: "required" }` | `{ type: "required" }` |
<th>Before</th> | `ValidationProvider functions={...}` | `ValidationProvider customFunctions={...}` |
<th>After</th> | `useFieldValidation(path, checks)` | `useFieldValidation(path, config)` where config is `{ checks, validateOn? }` |
</tr>
</thead>
<tbody>
<tr><td><code>{'{ fn: "required" }'}</code></td><td><code>{'{ type: "required" }'}</code></td></tr>
<tr><td><code>{'ValidationProvider functions={...}'}</code></td><td><code>{'ValidationProvider customFunctions={...}'}</code></td></tr>
<tr><td><code>useFieldValidation(path, checks)</code></td><td><code>useFieldValidation(path, config)</code> where config is <code>{'{ checks, validateOn? }'}</code></td></tr>
</tbody>
</table>
## Visibility Provider ## Visibility Provider
@@ -422,57 +397,16 @@ Action params in specs now use `statePath` instead of `path`.
The following exports have been removed from `@json-render/core`: The following exports have been removed from `@json-render/core`:
<table> | Removed | Replacement |
<thead> |---------|-------------|
<tr> | `createCatalog` | `defineCatalog(schema, config)` |
<th>Removed</th> | `generateCatalogPrompt` | `catalog.prompt()` |
<th>Replacement</th> | `generateSystemPrompt` | `catalog.prompt()` |
</tr> | `ComponentDefinition` | Use catalog component config directly |
</thead> | `CatalogConfig` | Use `defineCatalog` parameters |
<tbody> | `SystemPromptOptions` | Use `PromptOptions` |
<tr> | `LogicExpression` | Use `VisibilityCondition` |
<td><code>createCatalog</code></td> | `AuthState` | Model auth as regular state (e.g. `/auth/isSignedIn`) |
<td><code>defineCatalog(schema, config)</code></td> | `evaluateLogicExpression` | Use `evaluateVisibility` |
</tr> | `createRendererFromCatalog` | Use `defineRegistry` |
<tr> | `traverseTree` (codegen) | Use `traverseSpec` |
<td><code>generateCatalogPrompt</code></td>
<td><code>catalog.prompt()</code></td>
</tr>
<tr>
<td><code>generateSystemPrompt</code></td>
<td><code>catalog.prompt()</code></td>
</tr>
<tr>
<td><code>ComponentDefinition</code></td>
<td>Use catalog component config directly</td>
</tr>
<tr>
<td><code>CatalogConfig</code></td>
<td>Use <code>defineCatalog</code> parameters</td>
</tr>
<tr>
<td><code>SystemPromptOptions</code></td>
<td>Use <code>PromptOptions</code></td>
</tr>
<tr>
<td><code>LogicExpression</code></td>
<td>Use <code>VisibilityCondition</code></td>
</tr>
<tr>
<td><code>AuthState</code></td>
<td>Model auth as regular state (e.g. <code>/auth/isSignedIn</code>)</td>
</tr>
<tr>
<td><code>evaluateLogicExpression</code></td>
<td>Use <code>evaluateVisibility</code></td>
</tr>
<tr>
<td><code>createRendererFromCatalog</code></td>
<td>Use <code>defineRegistry</code></td>
</tr>
<tr>
<td><code>traverseTree</code> (codegen)</td>
<td>Use <code>traverseSpec</code></td>
</tr>
</tbody>
</table>
+2 -3
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "OpenAPI Integration" }
export const metadata = pageMetadata("docs/openapi")
# OpenAPI Integration # OpenAPI Integration
@@ -100,7 +99,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/schema'; import { schema } from '@json-render/react';
import { z } from 'zod'; import { z } from 'zod';
export const openapiCatalog = defineCatalog(schema, { export const openapiCatalog = defineCatalog(schema, {
+2 -3
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Introduction" }
export const metadata = pageMetadata("docs")
# Introduction # Introduction
@@ -23,7 +22,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/schema'; import { schema } from '@json-render/react';
import { z } from 'zod'; import { z } from 'zod';
export const catalog = defineCatalog(schema, { export const catalog = defineCatalog(schema, {
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Quick Start" }
export const metadata = pageMetadata("docs/quick-start")
# Quick Start # Quick Start
@@ -12,7 +11,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/schema'; import { schema } from '@json-render/react';
import { z } from 'zod'; import { z } from 'zod';
export const catalog = defineCatalog(schema, { export const catalog = defineCatalog(schema, {
+2 -3
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Registry" }
export const metadata = pageMetadata("docs/registry")
# Registry # Registry
@@ -133,7 +132,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/schema'; import { schema } from '@json-render/react';
import { z } from 'zod'; import { z } from 'zod';
const catalog = defineCatalog(schema, { const catalog = defineCatalog(schema, {
-197
View File
@@ -1,197 +0,0 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/renderers")
# Renderers
json-render supports multiple output targets. Each renderer takes the same core concept -- a JSON spec constrained to a catalog -- and renders it natively on a different platform or into a different format.
All renderers share the same workflow:
1. Define a catalog with `defineCatalog`
2. AI generates a JSON spec
3. The renderer turns the spec into platform-native output
<table>
<thead>
<tr>
<th>Renderer</th>
<th>Package</th>
<th>Output</th>
</tr>
</thead>
<tbody>
<tr>
<td>React</td>
<td><code>@json-render/react</code></td>
<td>React component tree</td>
</tr>
<tr>
<td>Vue</td>
<td><code>@json-render/vue</code></td>
<td>Vue 3 component tree</td>
</tr>
<tr>
<td>shadcn/ui</td>
<td><code>@json-render/shadcn</code></td>
<td>Pre-built Radix UI + Tailwind components (uses React renderer)</td>
</tr>
<tr>
<td>React Native</td>
<td><code>@json-render/react-native</code></td>
<td>Native mobile views</td>
</tr>
<tr>
<td>Image</td>
<td><code>@json-render/image</code></td>
<td>SVG / PNG (via Satori)</td>
</tr>
<tr>
<td>React PDF</td>
<td><code>@json-render/react-pdf</code></td>
<td>PDF documents</td>
</tr>
<tr>
<td>Remotion</td>
<td><code>@json-render/remotion</code></td>
<td>Video compositions</td>
</tr>
</tbody>
</table>
## React
Render specs as React component trees in the browser. Supports data binding, streaming, actions, validation, visibility, and computed values.
```tsx
import { defineRegistry, Renderer } from "@json-render/react";
import { schema } from "@json-render/react/schema";
const { registry } = defineRegistry(catalog, { components });
<Renderer spec={spec} registry={registry} />
```
Use `StateProvider`, `VisibilityProvider`, and `ActionProvider` for full interactivity. See the [@json-render/react API reference](/docs/api/react) for details.
## Vue
Vue 3 renderer with full feature parity with React: data binding, visibility, actions, validation, repeat scopes, and streaming.
```typescript
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]),
},
});
```
Uses composables (`useStateStore`, `useStateBinding`, `useActions`, etc.) instead of React hooks. See the [@json-render/vue API reference](/docs/api/vue) for details.
## shadcn/ui
36 pre-built components using Radix UI and Tailwind CSS. Built on top of `@json-render/react` -- no custom renderer needed.
```tsx
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { defineRegistry, Renderer } from "@json-render/react";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
import { shadcnComponents } from "@json-render/shadcn";
const catalog = defineCatalog(schema, {
components: {
Card: shadcnComponentDefinitions.Card,
Button: shadcnComponentDefinitions.Button,
},
});
const { registry } = defineRegistry(catalog, {
components: {
Card: shadcnComponents.Card,
Button: shadcnComponents.Button,
},
});
```
See the [@json-render/shadcn API reference](/docs/api/shadcn) for the full component list.
## React Native
Render specs as native mobile views. Includes 25+ standard components and standard action definitions.
```tsx
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react-native/schema";
import { standardComponentDefinitions, standardActionDefinitions } from "@json-render/react-native/catalog";
import { defineRegistry, Renderer } from "@json-render/react-native";
const catalog = defineCatalog(schema, {
components: { ...standardComponentDefinitions },
actions: standardActionDefinitions,
});
const { registry } = defineRegistry(catalog, { components: {} });
<Renderer spec={spec} registry={registry} />
```
See the [@json-render/react-native API reference](/docs/api/react-native) for details.
## Image
Generate SVG and PNG images from JSON specs using Satori. Ideal for OG images, social cards, and banners.
```typescript
import { renderToSvg, renderToPng } from "@json-render/image/render";
const svg = await renderToSvg(spec, { fonts });
const png = await renderToPng(spec, { fonts });
```
Nine standard components: Frame, Box, Row, Column, Heading, Text, Image, Divider, Spacer. PNG output requires `@resvg/resvg-js` as an optional peer dependency.
See the [@json-render/image API reference](/docs/api/image) for details.
## React PDF
Generate PDF documents from JSON specs using `@react-pdf/renderer`. Render to buffer, stream, or file.
```typescript
import { renderToBuffer, renderToStream, renderToFile } from "@json-render/react-pdf";
const buffer = await renderToBuffer(spec);
const stream = await renderToStream(spec);
await renderToFile(spec, "./output.pdf");
```
Standard components include Document, Page, View, Row, Column, Heading, Text, Image, Table, List, Divider, Spacer, Link, and PageNumber.
See the [@json-render/react-pdf API reference](/docs/api/react-pdf) for details.
## Remotion
Turn JSON timeline specs into video compositions with Remotion.
```tsx
import { Player } from "@remotion/player";
import { Renderer } from "@json-render/remotion";
<Player
component={Renderer}
inputProps={{ spec }}
durationInFrames={spec.composition.durationInFrames}
fps={spec.composition.fps}
compositionWidth={spec.composition.width}
compositionHeight={spec.composition.height}
/>
```
Uses a timeline spec format with compositions, tracks, and clips. Includes standard components (TitleCard, TypingText, ImageSlide, etc.), transitions (fade, slide, zoom, wipe), and effects.
See the [@json-render/remotion API reference](/docs/api/remotion) for details.
## Custom Renderers
You can build your own renderer for any output target. See the [Custom Schema & Renderer](/docs/custom-schema) guide for how to define a custom schema and wire it to your own rendering logic.
+1 -2
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Schemas" }
export const metadata = pageMetadata("docs/schemas")
# Schemas # Schemas
+1 -2
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Specs" }
export const metadata = pageMetadata("docs/specs")
# Specs # Specs
+1 -2
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Streaming" }
export const metadata = pageMetadata("docs/streaming")
# Streaming # Streaming
+9 -117
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Validation" }
export const metadata = pageMetadata("docs/validation")
# Validation # Validation
@@ -11,18 +10,11 @@ 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 (args: `{ "min": N }`) - `minLength` — Minimum string length
- `maxLength` — Maximum string length (args: `{ "max": N }`) - `maxLength` — Maximum string length
- `pattern` — Match a regex pattern (args: `{ "pattern": "regex" }`) - `pattern` — Match a regex pattern
- `min` — Minimum numeric value (args: `{ "min": N }`) - `min` — Minimum numeric value
- `max` — Maximum numeric value (args: `{ "max": N }`) - `max` — Maximum numeric value
- `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
@@ -74,7 +66,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/schema'; // or '@json-render/react-native/schema' import { schema } from '@json-render/react'; // or '@json-render/react-native'
import { z } from 'zod'; import { z } from 'zod';
const catalog = defineCatalog(schema, { const catalog = defineCatalog(schema, {
@@ -150,114 +142,14 @@ 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 (default for Input, Textarea) - `blur` — Validate when field loses focus
- `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
- [Computed Values](/docs/computed-values) — derive dynamic prop values Learn about [generation modes](/docs/generation-modes).
- [Watchers](/docs/watchers) — react to state changes
- [Generation Modes](/docs/generation-modes) — how AI generates specs
+1 -2
View File
@@ -1,5 +1,4 @@
import { pageMetadata } from "@/lib/page-metadata" export const metadata = { title: "Visibility" }
export const metadata = pageMetadata("docs/visibility")
# Visibility # Visibility
-167
View File
@@ -1,167 +0,0 @@
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`
<table>
<thead>
<tr>
<th>Mechanism</th>
<th>Trigger</th>
<th>Use Case</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>on</code></td>
<td>User interaction (press, change, blur)</td>
<td>Button clicks, input changes, form submissions</td>
</tr>
<tr>
<td><code>watch</code></td>
<td>State value change (any source)</td>
<td>Cascading data, derived state, cross-field sync</td>
</tr>
</tbody>
</table>
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
+1 -1
View File
@@ -16,7 +16,7 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render
GitHub repository: https://github.com/vercel-labs/json-render GitHub repository: https://github.com/vercel-labs/json-render
Documentation: https://json-render.dev/docs Documentation: https://json-render.dev/docs
npm packages: @json-render/core, @json-render/react, @json-render/image, @json-render/remotion, @json-render/codegen npm packages: @json-render/core, @json-render/react, @json-render/remotion, @json-render/codegen
You have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory. You have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory.
+2 -4
View File
@@ -1,6 +1,5 @@
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";
@@ -62,6 +61,7 @@ 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,9 +92,7 @@ export default async function RootLayout({
/> />
)} )}
</head> </head>
<body <body className={`${geistSans.variable} ${geistMono.variable}`}>
className={`${geistSans.variable} ${geistMono.variable} ${GeistPixelSquare.variable}`}
>
<ThemeProvider> <ThemeProvider>
{children} {children}
<DocsChat defaultOpen={chatOpen} defaultWidth={chatWidth} /> <DocsChat defaultOpen={chatOpen} defaultWidth={chatWidth} />
+8 -15
View File
@@ -5,20 +5,19 @@ 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; geistPixelSquare: Buffer } | null = null; let fontCache: { geistRegular: Buffer } | null = null;
async function loadFonts() { async function loadFonts() {
if (fontCache) return fontCache; if (fontCache) return fontCache;
const [geistRegular, geistPixelSquare] = await Promise.all([ const geistRegular = await readFile(
readFile(join(process.cwd(), "public/Geist-Regular.ttf")), 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, geistPixelSquare } = await loadFonts(); const { geistRegular } = await loadFonts();
return new ImageResponse( return new ImageResponse(
<div <div
@@ -54,8 +53,8 @@ export async function renderOgImage(title: string) {
<span <span
style={{ style={{
fontSize: 36, fontSize: 36,
fontFamily: "Geist Pixel Square", fontFamily: "Geist",
fontWeight: 500, fontWeight: 400,
color: "white", color: "white",
}} }}
> >
@@ -100,12 +99,6 @@ 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,
},
], ],
}, },
); );
+5 -2
View File
@@ -1,7 +1,10 @@
import { Playground } from "@/components/playground"; import { Playground } from "@/components/playground";
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("playground"); import { PAGE_TITLES } from "@/lib/page-titles";
export const metadata = {
title: PAGE_TITLES["playground"],
};
export default function PlaygroundPage() { export default function PlaygroundPage() {
return <Playground />; return <Playground />;
+1 -1
View File
@@ -57,7 +57,7 @@ export function Header() {
</svg> </svg>
</span> </span>
<Link href="/"> <Link href="/">
<span className="font-medium tracking-tight text-lg font-(family-name:--font-geist-pixel-square)"> <span className="font-medium tracking-tight text-lg">
json-render json-render
</span> </span>
</Link> </Link>
+31 -122
View File
@@ -16,20 +16,17 @@ 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" | "visual"; type Tab = "json" | "nested" | "stream" | "catalog";
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";
@@ -236,20 +233,6 @@ 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...";
@@ -496,69 +479,31 @@ ${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", "visual", "nested", "stream", "catalog"] as const).map( {(["json", "nested", "stream", "catalog"] as const).map((tab) => (
(tab) => ( <button
<button key={tab}
key={tab} onClick={() => setActiveTab(tab)}
onClick={() => setActiveTab(tab)} className={`text-xs font-mono transition-colors ${
className={`text-xs font-mono transition-colors ${ activeTab === tab
activeTab === tab ? "text-foreground"
? "text-foreground" : "text-muted-foreground hover:text-foreground"
: "text-muted-foreground hover:text-foreground" }`}
}`} >
> {tab}
{tab} </button>
</button> ))}
),
)}
<div className="flex-1" /> <div className="flex-1" />
{activeTab !== "catalog" && activeTab !== "visual" && ( {activeTab !== "catalog" && (
<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 === "visual" ? ( {activeTab === "catalog" ? (
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">
{( {(
@@ -790,21 +735,19 @@ ${jsx}
: 0} : 0}
</button> </button>
{/* Code tabs */} {/* Code tabs */}
{(["json", "visual", "nested", "stream", "catalog"] as const).map( {(["json", "nested", "stream", "catalog"] as const).map((tab) => (
(tab) => ( <button
<button key={tab}
key={tab} onClick={() => setMobileView(tab)}
onClick={() => setMobileView(tab)} className={`text-xs font-mono transition-colors shrink-0 ${
className={`text-xs font-mono transition-colors shrink-0 ${ mobileView === tab
mobileView === tab ? "text-foreground"
? "text-foreground" : "text-muted-foreground hover:text-foreground"
: "text-muted-foreground hover:text-foreground" }`}
}`} >
> {tab}
{tab} </button>
</button> ))}
),
)}
<div className="flex-1" /> <div className="flex-1" />
{/* Preview / code toggle */} {/* Preview / code toggle */}
{[ {[
@@ -827,41 +770,7 @@ ${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 === "visual" ? ( {mobileView === "catalog" ? (
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 -1
View File
@@ -1,4 +1,4 @@
import { nextJsConfig } from "@internal/eslint-config/next-js"; import { nextJsConfig } from "@repo/eslint-config/next-js";
/** @type {import("eslint").Linter.Config[]} */ /** @type {import("eslint").Linter.Config[]} */
export default [ export default [
-20
View File
@@ -27,16 +27,13 @@ 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" },
], ],
}, },
{ {
title: "Rendering", title: "Rendering",
items: [ items: [
{ title: "Renderers", href: "/docs/renderers" },
{ title: "Registry", href: "/docs/registry" }, { title: "Registry", href: "/docs/registry" },
{ title: "Streaming", href: "/docs/streaming" }, { title: "Streaming", href: "/docs/streaming" },
{ title: "Generation Modes", href: "/docs/generation-modes" }, { title: "Generation Modes", href: "/docs/generation-modes" },
@@ -70,21 +67,6 @@ export const docsNavigation: NavSection[] = [
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: "Image",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/image",
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,
},
], ],
}, },
{ {
@@ -112,9 +94,7 @@ export const docsNavigation: NavSection[] = [
{ title: "@json-render/react-pdf", href: "/docs/api/react-pdf" }, { 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/image", href: "/docs/api/image" },
{ 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" },
], ],
}, },
-39
View File
@@ -1,39 +0,0 @@
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],
},
};
}
-6
View File
@@ -23,10 +23,7 @@ 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/renderers": "Renderers",
"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",
@@ -41,13 +38,10 @@ 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-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/image": "@json-render/image API",
"docs/api/remotion": "@json-render/remotion API", "docs/api/remotion": "@json-render/remotion API",
"docs/api/shadcn": "@json-render/shadcn API",
}; };
/** /**
+5 -7
View File
@@ -1,11 +1,11 @@
{ {
"name": "web", "name": "web",
"version": "0.1.4", "version": "0.1.0",
"type": "module", "type": "module",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {
"dev": "portless json-render next dev --turbopack", "dev": "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,13 +28,11 @@
"@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",
@@ -50,11 +48,11 @@
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"unist-util-visit": "5.1.0", "unist-util-visit": "5.1.0",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^4.3.6" "zod": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@internal/eslint-config": "workspace:*", "@repo/eslint-config": "workspace:*",
"@internal/typescript-config": "workspace:*", "@repo/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 -1
View File
@@ -1,5 +1,5 @@
{ {
"extends": "@internal/typescript-config/nextjs.json", "extends": "@repo/typescript-config/nextjs.json",
"compilerOptions": { "compilerOptions": {
"plugins": [ "plugins": [
{ {
-37
View File
@@ -1,37 +0,0 @@
# example-chat
## 0.1.4
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/react@0.11.0
- @json-render/shadcn@0.11.0
## 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 -1
View File
@@ -1,4 +1,4 @@
import { nextJsConfig } from "@internal/eslint-config/next-js"; import { nextJsConfig } from "@repo/eslint-config/next-js";
/** @type {import("eslint").Linter.Config[]} */ /** @type {import("eslint").Linter.Config[]} */
export default [ export default [
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // 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 -4
View File
@@ -1,10 +1,10 @@
{ {
"name": "example-chat", "name": "example-chat",
"version": "0.1.4", "version": "0.1.0",
"type": "module", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "portless chat-demo.json-render next dev --turbopack", "dev": "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",
@@ -33,10 +33,10 @@
"streamdown": "^2.2.0", "streamdown": "^2.2.0",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"three": "^0.182.0", "three": "^0.182.0",
"zod": "4.3.6" "zod": "4.3.5"
}, },
"devDependencies": { "devDependencies": {
"@internal/eslint-config": "workspace:*", "@repo/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",
-37
View File
@@ -1,37 +0,0 @@
# example-dashboard
## 0.1.4
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/codegen@0.11.0
- @json-render/react@0.11.0
## 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
+17 -21
View File
@@ -196,28 +196,24 @@ export function Widget({
onDeleted?.(); onDeleted?.();
}, [widgetId, onDeleted]); }, [widgetId, onDeleted]);
const handleStateChange = useCallback( const handleStateChange = useCallback((path: string, value: unknown) => {
(changes: Array<{ path: string; value: unknown }>) => { setState((prev) => {
setState((prev) => { const next = { ...prev };
const next = { ...prev }; // Convert path like "customerForm/name" to nested object
for (const { path, value } of changes) { const parts = path.split("/");
const parts = path.split("/"); let current: Record<string, unknown> = next;
let current: Record<string, unknown> = next; for (let i = 0; i < parts.length - 1; i++) {
for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]!;
const part = parts[i]!; if (!(part in current) || typeof current[part] !== "object") {
if (!(part in current) || typeof current[part] !== "object") { current[part] = {};
current[part] = {};
}
current = current[part] as Record<string, unknown>;
}
const lastPart = parts[parts.length - 1]!;
current[lastPart] = value;
} }
return next; current = current[part] as Record<string, unknown>;
}); }
}, 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 -1
View File
@@ -1,4 +1,4 @@
import { nextJsConfig } from "@internal/eslint-config/next-js"; import { nextJsConfig } from "@repo/eslint-config/next-js";
/** @type {import("eslint").Linter.Config[]} */ /** @type {import("eslint").Linter.Config[]} */
export default [ export default [
+1 -1
View File
@@ -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?: (changes: Array<{ path: string; value: unknown }>) => void; onStateChange?: (path: string, value: unknown) => void;
loading?: boolean; loading?: boolean;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // 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 -4
View File
@@ -1,10 +1,10 @@
{ {
"name": "example-dashboard", "name": "example-dashboard",
"version": "0.1.4", "version": "0.1.0",
"type": "module", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "portless dashboard-demo.json-render next dev --turbopack", "dev": "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",
@@ -39,10 +39,10 @@
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^4.3.6" "zod": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@internal/eslint-config": "workspace:*", "@repo/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",
-9
View File
@@ -1,9 +0,0 @@
# example-image
## 0.1.1
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/image@0.11.0
-34
View File
@@ -1,34 +0,0 @@
import { streamText } from "ai";
import { buildUserPrompt, type Spec } from "@json-render/core";
import { imageCatalog } from "@/lib/catalog";
export const maxDuration = 60;
const SYSTEM_PROMPT = imageCatalog.prompt();
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
export async function POST(req: Request) {
const { prompt, startingSpec } = (await req.json()) as {
prompt: string;
startingSpec?: Spec | null;
};
if (!prompt || typeof prompt !== "string") {
return Response.json({ error: "prompt is required" }, { status: 400 });
}
const userPrompt = buildUserPrompt({
prompt,
currentSpec: startingSpec,
});
const result = streamText({
model: process.env.AI_GATEWAY_MODEL ?? DEFAULT_MODEL,
system: SYSTEM_PROMPT,
prompt: userPrompt,
temperature: 0.7,
});
return result.toTextStreamResponse();
}
-79
View File
@@ -1,79 +0,0 @@
import { renderToPng } from "@json-render/image/render";
import { examples } from "@/lib/examples";
import type { Spec } from "@json-render/core";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
let fontCache: ArrayBuffer | null = null;
async function loadFont(): Promise<ArrayBuffer> {
if (fontCache) return fontCache;
const fontPath = join(
process.cwd(),
"node_modules",
"geist",
"dist",
"fonts",
"geist-sans",
"Geist-Regular.ttf",
);
const buffer = await readFile(fontPath);
fontCache = buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength,
);
return fontCache;
}
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const name = searchParams.get("name") ?? "og-image";
const download = searchParams.get("download") === "1";
const example = examples.find((e) => e.name === name);
if (!example) {
return new Response("Example not found", { status: 404 });
}
return imageResponse(example.spec, name, download);
}
export async function POST(req: Request) {
const { spec, download, filename } = (await req.json()) as {
spec: Spec;
download?: boolean;
filename?: string;
};
if (!spec || !spec.root || !spec.elements) {
return new Response("Invalid spec", { status: 400 });
}
return imageResponse(spec, filename ?? "image", download ?? false);
}
async function imageResponse(spec: Spec, name: string, download: boolean) {
const fontData = await loadFont();
const fonts = [
{
name: "Geist Sans",
data: fontData,
weight: 400 as const,
style: "normal" as const,
},
];
const png = await renderToPng(spec, { fonts });
const disposition = download
? `attachment; filename="${name}.png"`
: `inline; filename="${name}.png"`;
return new Response(Buffer.from(png), {
headers: {
"Content-Type": "image/png",
"Content-Disposition": disposition,
"Cache-Control": "no-store",
},
});
}
-124
View File
@@ -1,124 +0,0 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
-22
View File
@@ -1,22 +0,0 @@
import type { Metadata } from "next";
import { Geist } from "next/font/google";
import "./globals.css";
const geist = Geist({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "json-render Image Example",
description: "Generate images from JSON specs with @json-render/image",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={geist.className}>{children}</body>
</html>
);
}
-627
View File
@@ -1,627 +0,0 @@
"use client";
import { useState, useCallback, useRef, useEffect } from "react";
import { examples } from "@/lib/examples";
import { createSpecStreamCompiler } from "@json-render/core";
import type { Spec } from "@json-render/core";
import { cn } from "@/lib/utils";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from "@/components/ui/resizable";
import { ImageIcon, Download, Loader2, ArrowRight, Square } from "lucide-react";
type Mode = "scratch" | "example";
type MobileView = "json" | "preview";
interface Selection {
mode: Mode;
exampleName?: string;
}
const IMAGE_REFRESH_INTERVAL_MS = 3000;
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
return (
<button
onClick={() => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}}
className="text-xs text-muted-foreground hover:text-foreground transition-colors font-mono"
>
{copied ? "copied" : "copy"}
</button>
);
}
function isRenderableSpec(spec: Spec | null): spec is Spec {
if (!spec?.root || !spec.elements) return false;
const root = spec.elements[spec.root];
return root?.type === "Frame";
}
export default function Page() {
const [selection, setSelection] = useState<Selection>({
mode: "example",
exampleName: examples[0]!.name,
});
const [prompt, setPrompt] = useState("");
const [generating, setGenerating] = useState(false);
const [generatedSpec, setGeneratedSpec] = useState<Spec | null>(null);
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [mobileView, setMobileView] = useState<MobileView>("preview");
const [examplesSheetOpen, setExamplesSheetOpen] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const imageUrlRef = useRef<string | null>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const mobileInputRef = useRef<HTMLTextAreaElement>(null);
const abortRef = useRef<AbortController | null>(null);
const codeScrollRef = useRef<HTMLDivElement>(null);
const mobileCodeScrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!generating) return;
codeScrollRef.current?.scrollTo({
top: codeScrollRef.current.scrollHeight,
});
mobileCodeScrollRef.current?.scrollTo({
top: mobileCodeScrollRef.current.scrollHeight,
});
}, [generating, generatedSpec]);
const currentExample =
selection.mode === "example"
? examples.find((e) => e.name === selection.exampleName)
: null;
const activeSpec = generatedSpec ?? currentExample?.spec ?? null;
const exampleImageUrl =
selection.mode === "example" && !generatedSpec
? `/api/image?name=${selection.exampleName}`
: null;
const displayImageUrl = imageUrl ?? exampleImageUrl;
useEffect(() => {
inputRef.current?.focus();
}, [selection.mode, selection.exampleName]);
const fetchImageBlob = useCallback(
async (spec: Spec, signal?: AbortSignal) => {
const res = await fetch("/api/image", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ spec }),
signal,
});
if (!res.ok) throw new Error("Failed to generate image");
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const prev = imageUrlRef.current;
imageUrlRef.current = url;
setImageUrl(url);
if (prev) URL.revokeObjectURL(prev);
},
[],
);
const lastRefreshSpec = useRef<string>("");
const generatedSpecRef = useRef<Spec | null>(null);
generatedSpecRef.current = generatedSpec;
useEffect(() => {
if (!generating) return;
const interval = setInterval(() => {
const spec = generatedSpecRef.current;
if (!spec) return;
const specKey = JSON.stringify(spec);
if (specKey === lastRefreshSpec.current) return;
if (!isRenderableSpec(spec)) return;
lastRefreshSpec.current = specKey;
setRefreshing(true);
fetchImageBlob(spec)
.catch(() => {})
.finally(() => setRefreshing(false));
}, IMAGE_REFRESH_INTERVAL_MS);
return () => clearInterval(interval);
}, [generating, fetchImageBlob]);
const handleGenerate = useCallback(async () => {
if (!prompt.trim()) return;
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
setGenerating(true);
setError(null);
lastRefreshSpec.current = "";
try {
const startingSpec =
selection.mode === "example" && currentExample
? currentExample.spec
: null;
const res = await fetch("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: prompt.trim(), startingSpec }),
signal: controller.signal,
});
if (!res.ok) throw new Error("Generation failed");
const reader = res.body?.getReader();
if (!reader) throw new Error("No response body");
const decoder = new TextDecoder();
const compiler = createSpecStreamCompiler<Spec>(
startingSpec ? { ...startingSpec } : {},
);
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const { result, newPatches } = compiler.push(chunk);
if (newPatches.length > 0) setGeneratedSpec(result);
}
const finalSpec = compiler.getResult();
setGeneratedSpec(finalSpec);
setGenerating(false);
await fetchImageBlob(finalSpec);
} catch (e) {
if (controller.signal.aborted) return;
setError(e instanceof Error ? e.message : "Something went wrong");
setGenerating(false);
}
}, [prompt, selection, currentExample, fetchImageBlob]);
const handleStop = useCallback(() => {
abortRef.current?.abort();
setGenerating(false);
if (isRenderableSpec(generatedSpec)) {
fetchImageBlob(generatedSpec).catch(() => {});
}
}, [generatedSpec, fetchImageBlob]);
const select = (next: Selection) => {
abortRef.current?.abort();
setSelection(next);
setGeneratedSpec(null);
setImageUrl(null);
setError(null);
setPrompt("");
setGenerating(false);
setExamplesSheetOpen(false);
};
const handleDownload = async () => {
if (!activeSpec) return;
if (generatedSpec) {
const res = await fetch("/api/image", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ spec: generatedSpec, download: true }),
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "image.png";
a.click();
URL.revokeObjectURL(url);
} else if (selection.mode === "example") {
window.open(
`/api/image?name=${selection.exampleName}&download=1`,
"_blank",
);
}
};
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleGenerate();
}
},
[handleGenerate],
);
const jsonCode = activeSpec
? JSON.stringify(activeSpec, null, 2)
: "// select an example or generate an image";
// ---------------------------------------------------------------------------
// Pane: Chat / Examples
// ---------------------------------------------------------------------------
const chatPane = (
<div className="h-full flex flex-col">
<div className="border-b border-border px-3 h-9 flex items-center gap-2">
<ImageIcon className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-mono text-muted-foreground">
json-render / image
</span>
</div>
<ScrollArea className="flex-1">
<div className="p-2 space-y-1">
<p className="px-2 pt-2 pb-1 text-[11px] font-mono text-muted-foreground">
start
</p>
<button
onClick={() => select({ mode: "scratch" })}
className={cn(
"w-full text-left px-3 py-2 rounded text-sm transition-colors",
selection.mode === "scratch"
? "bg-muted text-foreground"
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground",
)}
>
<span className="font-medium">From scratch</span>
</button>
<p className="px-2 pt-3 pb-1 text-[11px] font-mono text-muted-foreground">
examples
</p>
{examples.map((ex) => (
<button
key={ex.name}
onClick={() => select({ mode: "example", exampleName: ex.name })}
className={cn(
"w-full text-left px-3 py-2 rounded text-sm transition-colors",
selection.mode === "example" &&
selection.exampleName === ex.name
? "bg-muted text-foreground"
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground",
)}
>
<span className="font-medium">{ex.label}</span>
<p className="text-xs text-muted-foreground/70 mt-0.5 leading-snug">
{ex.description}
</p>
</button>
))}
</div>
</ScrollArea>
<div
className="border-t border-border p-3 cursor-text"
onMouseDown={(e) => {
const target = e.target as HTMLElement;
if (!target.closest("button") && target.tagName !== "TEXTAREA") {
e.preventDefault();
inputRef.current?.focus();
}
}}
>
{error && (
<div className="mb-2 rounded bg-destructive/10 px-3 py-1.5 text-xs text-destructive">
{error}
</div>
)}
<textarea
ref={inputRef}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={
selection.mode === "scratch"
? "Describe the image you want..."
: `Modify the ${currentExample?.label ?? "example"}...`
}
className="w-full bg-background text-sm resize-none outline-none placeholder:text-muted-foreground/50"
rows={2}
autoFocus
/>
<div className="flex justify-between items-center mt-2">
<span className="text-[11px] text-muted-foreground">
{selection.mode === "example" && currentExample
? currentExample.label
: "scratch"}
</span>
{generating ? (
<button
onClick={handleStop}
className="w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors"
aria-label="Stop"
>
<Square className="h-3 w-3" fill="currentColor" />
</button>
) : (
<button
onClick={handleGenerate}
disabled={!prompt.trim()}
className="w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30"
aria-label="Generate"
>
<ArrowRight className="h-3.5 w-3.5" />
</button>
)}
</div>
</div>
</div>
);
// ---------------------------------------------------------------------------
// Pane: JSON Spec
// ---------------------------------------------------------------------------
const codePane = (
<div className="h-full flex flex-col">
<div className="border-b border-border px-3 h-9 flex items-center gap-3">
<span className="text-xs font-mono text-foreground">json</span>
{generating && (
<Loader2 className="h-3 w-3 text-muted-foreground animate-spin" />
)}
<div className="flex-1" />
{activeSpec && <CopyButton text={jsonCode} />}
</div>
<div ref={codeScrollRef} className="flex-1 overflow-auto">
<pre className="p-3 text-xs leading-relaxed font-mono text-muted-foreground whitespace-pre">
{jsonCode}
</pre>
</div>
</div>
);
// ---------------------------------------------------------------------------
// Pane: Image Preview
// ---------------------------------------------------------------------------
const previewPane = (
<div className="h-full flex flex-col">
<div className="border-b border-border px-3 h-9 flex items-center gap-3">
<span className="text-xs font-mono text-foreground">preview</span>
{(generating || refreshing) && (
<Loader2 className="h-3.5 w-3.5 text-muted-foreground animate-spin" />
)}
<div className="flex-1" />
{activeSpec && (
<button
onClick={handleDownload}
className="text-xs text-muted-foreground hover:text-foreground transition-colors font-mono flex items-center gap-1"
>
<Download className="h-3 w-3" />
download
</button>
)}
</div>
<div className="flex-1 relative bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center p-4">
{displayImageUrl ? (
<img
key={displayImageUrl}
src={displayImageUrl}
alt="Generated image"
className="max-w-full max-h-full object-contain rounded shadow-lg"
/>
) : (
<div className="flex flex-col items-center justify-center gap-2 text-neutral-400">
<ImageIcon className="h-10 w-10" />
<p className="text-sm">
{selection.mode === "scratch"
? "Enter a prompt to generate an image"
: "Select an example to preview"}
</p>
</div>
)}
</div>
</div>
);
// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
return (
<div className="h-dvh flex flex-col">
{/* Desktop: 3-pane resizable layout */}
<div className="hidden lg:flex flex-1 min-h-0">
<ResizablePanelGroup className="flex-1">
<ResizablePanel defaultSize={25} minSize={15}>
{chatPane}
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={35} minSize={20}>
{codePane}
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={40} minSize={20}>
{previewPane}
</ResizablePanel>
</ResizablePanelGroup>
</div>
{/* Mobile: toolbar + content + prompt */}
<div className="flex lg:hidden flex-col flex-1 min-h-0">
<div className="border-b border-border h-10 flex items-center px-1">
<button
onClick={() => setExamplesSheetOpen(true)}
className="px-3 h-full text-xs font-mono text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1.5"
>
<ImageIcon className="h-3.5 w-3.5" />
{selection.mode === "example" && currentExample
? currentExample.label
: "scratch"}
</button>
<div className="flex-1" />
<button
onClick={() => setMobileView("json")}
className={cn(
"px-3 h-full text-xs font-mono transition-colors",
mobileView === "json"
? "text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
json
</button>
<button
onClick={() => setMobileView("preview")}
className={cn(
"px-3 h-full text-xs font-mono transition-colors",
mobileView === "preview"
? "text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
preview
</button>
{activeSpec && (
<button
onClick={handleDownload}
className="px-3 h-full text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<Download className="h-3.5 w-3.5" />
</button>
)}
</div>
<div className="flex-1 min-h-0">
{mobileView === "json" ? (
<div ref={mobileCodeScrollRef} className="h-full overflow-auto">
<pre className="p-3 text-xs leading-relaxed font-mono text-muted-foreground whitespace-pre">
{jsonCode}
</pre>
</div>
) : (
<div className="h-full bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center p-4">
{displayImageUrl ? (
<img
key={displayImageUrl}
src={displayImageUrl}
alt="Generated image"
className="max-w-full max-h-full object-contain rounded shadow-lg"
/>
) : (
<div className="flex flex-col items-center justify-center gap-2 text-neutral-400">
<ImageIcon className="h-10 w-10" />
<p className="text-sm">
{selection.mode === "scratch"
? "Enter a prompt to generate an image"
: "Select an example"}
</p>
</div>
)}
</div>
)}
</div>
<div
className="border-t border-border p-3 cursor-text"
onMouseDown={(e) => {
const target = e.target as HTMLElement;
if (!target.closest("button") && target.tagName !== "TEXTAREA") {
e.preventDefault();
mobileInputRef.current?.focus();
}
}}
>
{error && (
<div className="mb-2 rounded bg-destructive/10 px-3 py-1.5 text-xs text-destructive">
{error}
</div>
)}
<textarea
ref={mobileInputRef}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={
selection.mode === "scratch"
? "Describe the image you want..."
: `Modify the ${currentExample?.label ?? "example"}...`
}
className="w-full bg-background text-sm resize-none outline-none placeholder:text-muted-foreground/50"
rows={2}
/>
<div className="flex justify-end mt-2">
{generating ? (
<button
onClick={handleStop}
className="w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors"
aria-label="Stop"
>
<Square className="h-3 w-3" fill="currentColor" />
</button>
) : (
<button
onClick={handleGenerate}
disabled={!prompt.trim()}
className="w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30"
aria-label="Generate"
>
<ArrowRight className="h-3.5 w-3.5" />
</button>
)}
</div>
</div>
</div>
<Sheet open={examplesSheetOpen} onOpenChange={setExamplesSheetOpen}>
<SheetContent side="left" className="w-72 p-0">
<SheetTitle className="sr-only">Examples</SheetTitle>
<ScrollArea className="h-full">
<div className="p-2 space-y-1">
<p className="px-2 pt-2 pb-1 text-[11px] font-mono text-muted-foreground">
start
</p>
<button
onClick={() => select({ mode: "scratch" })}
className={cn(
"w-full text-left px-3 py-2 rounded text-sm transition-colors",
selection.mode === "scratch"
? "bg-muted text-foreground"
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground",
)}
>
<span className="font-medium">From scratch</span>
</button>
<p className="px-2 pt-3 pb-1 text-[11px] font-mono text-muted-foreground">
examples
</p>
{examples.map((ex) => (
<button
key={ex.name}
onClick={() =>
select({ mode: "example", exampleName: ex.name })
}
className={cn(
"w-full text-left px-3 py-2 rounded text-sm transition-colors",
selection.mode === "example" &&
selection.exampleName === ex.name
? "bg-muted text-foreground"
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground",
)}
>
<span className="font-medium">{ex.label}</span>
<p className="text-xs text-muted-foreground/70 mt-0.5 leading-snug">
{ex.description}
</p>
</button>
))}
</div>
</ScrollArea>
</SheetContent>
</Sheet>
</div>
);
}
-23
View File
@@ -1,23 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
@@ -1,53 +0,0 @@
"use client";
import { GripVerticalIcon } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils";
function ResizablePanelGroup({
className,
...props
}: ResizablePrimitive.GroupProps) {
return (
<ResizablePrimitive.Group
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full aria-[orientation=vertical]:flex-col",
className,
)}
{...props}
/>
);
}
function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
}
function ResizableHandle({
withHandle,
className,
...props
}: ResizablePrimitive.SeparatorProps & {
withHandle?: boolean;
}) {
return (
<ResizablePrimitive.Separator
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.Separator>
);
}
export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
@@ -1,58 +0,0 @@
"use client";
import * as React from "react";
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };
-143
View File
@@ -1,143 +0,0 @@
"use client";
import * as React from "react";
import { XIcon } from "lucide-react";
import { Dialog as SheetPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = "right",
showCloseButton = true,
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left";
showCloseButton?: boolean;
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Content>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};
-12
View File
@@ -1,12 +0,0 @@
import { nextJsConfig } from "@internal/eslint-config/next-js";
/** @type {import("eslint").Linter.Config[]} */
export default [
...nextJsConfig,
{
rules: {
"react/prop-types": "off",
"@next/next/no-img-element": "off",
},
},
];
-7
View File
@@ -1,7 +0,0 @@
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/image/server";
import { standardComponentDefinitions } from "@json-render/image/catalog";
export const imageCatalog = defineCatalog(schema, {
components: standardComponentDefinitions,
});
-771
View File
@@ -1,771 +0,0 @@
import type { Spec } from "@json-render/core";
export interface Example {
name: string;
label: string;
description: string;
spec: Spec;
}
export const examples: Example[] = [
// ==========================================================================
// OG Image
// ==========================================================================
{
name: "og-image",
label: "OG Image",
description: "Social sharing card for Open Graph (1200x630)",
spec: {
root: "frame",
elements: {
frame: {
type: "Frame",
props: {
width: 1200,
height: 630,
backgroundColor: "#0f172a",
padding: 60,
flexDirection: "column",
justifyContent: "space-between",
},
children: ["top-row", "content", "bottom-row"],
},
"top-row": {
type: "Row",
props: { gap: 12, alignItems: "center" },
children: ["logo-dot", "brand"],
},
"logo-dot": {
type: "Box",
props: {
width: 32,
height: 32,
backgroundColor: "#6366f1",
borderRadius: 16,
},
children: [],
},
brand: {
type: "Text",
props: { text: "acme.dev", fontSize: 24, color: "#94a3b8" },
children: [],
},
content: {
type: "Column",
props: { gap: 16 },
children: ["title", "subtitle"],
},
title: {
type: "Heading",
props: {
text: "Build faster with the modern developer platform",
level: "h1",
color: "#f8fafc",
letterSpacing: "-0.02em",
lineHeight: 1.1,
},
children: [],
},
subtitle: {
type: "Text",
props: {
text: "Ship production-ready apps in minutes, not months.",
fontSize: 24,
color: "#94a3b8",
lineHeight: 1.4,
},
children: [],
},
"bottom-row": {
type: "Row",
props: { gap: 8, alignItems: "center" },
children: ["url"],
},
url: {
type: "Text",
props: {
text: "acme.dev/platform",
fontSize: 18,
color: "#6366f1",
fontWeight: "bold",
},
children: [],
},
},
},
},
// ==========================================================================
// Blog Post Card
// ==========================================================================
{
name: "blog-card",
label: "Blog Post Card",
description: "Social card for a blog post with author info",
spec: {
root: "frame",
elements: {
frame: {
type: "Frame",
props: {
width: 1200,
height: 630,
backgroundColor: "#ffffff",
padding: 60,
flexDirection: "column",
justifyContent: "space-between",
},
children: ["header", "body", "footer"],
},
header: {
type: "Row",
props: { gap: 10, alignItems: "center" },
children: ["category-badge"],
},
"category-badge": {
type: "Box",
props: {
backgroundColor: "#dbeafe",
borderRadius: 20,
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 16,
paddingRight: 16,
},
children: ["category-text"],
},
"category-text": {
type: "Text",
props: {
text: "Engineering",
fontSize: 16,
color: "#1d4ed8",
fontWeight: "bold",
},
children: [],
},
body: {
type: "Column",
props: { gap: 20 },
children: ["blog-title", "blog-excerpt"],
},
"blog-title": {
type: "Heading",
props: {
text: "How We Reduced API Latency by 90% with Edge Computing",
level: "h1",
color: "#0f172a",
letterSpacing: "-0.02em",
lineHeight: 1.15,
},
children: [],
},
"blog-excerpt": {
type: "Text",
props: {
text: "A deep dive into our migration from centralized servers to edge functions, and the performance gains we achieved along the way.",
fontSize: 22,
color: "#64748b",
lineHeight: 1.5,
},
children: [],
},
footer: {
type: "Row",
props: { gap: 16, alignItems: "center" },
children: ["author-avatar", "author-info"],
},
"author-avatar": {
type: "Box",
props: {
width: 48,
height: 48,
backgroundColor: "#e2e8f0",
borderRadius: 24,
},
children: [],
},
"author-info": {
type: "Column",
props: { gap: 2 },
children: ["author-name", "author-date"],
},
"author-name": {
type: "Text",
props: {
text: "Sarah Chen",
fontSize: 18,
color: "#0f172a",
fontWeight: "bold",
},
children: [],
},
"author-date": {
type: "Text",
props: {
text: "February 15, 2026",
fontSize: 16,
color: "#94a3b8",
},
children: [],
},
},
},
},
// ==========================================================================
// Event Banner
// ==========================================================================
{
name: "event-banner",
label: "Event Banner",
description: "Wide promotional banner for an event (1920x1080)",
spec: {
root: "frame",
elements: {
frame: {
type: "Frame",
props: {
width: 1920,
height: 1080,
backgroundColor: "#18181b",
padding: 100,
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
},
children: [
"badge-row",
"spacer-1",
"event-title",
"spacer-2",
"details-row",
"spacer-3",
"cta",
],
},
"badge-row": {
type: "Row",
props: { alignItems: "center", justifyContent: "center" },
children: ["live-badge"],
},
"live-badge": {
type: "Box",
props: {
backgroundColor: "#dc2626",
borderRadius: 24,
paddingTop: 8,
paddingBottom: 8,
paddingLeft: 24,
paddingRight: 24,
},
children: ["live-text"],
},
"live-text": {
type: "Text",
props: {
text: "LIVE EVENT",
fontSize: 18,
color: "#ffffff",
fontWeight: "bold",
letterSpacing: "0.1em",
},
children: [],
},
"spacer-1": {
type: "Spacer",
props: { height: 40 },
children: [],
},
"event-title": {
type: "Heading",
props: {
text: "DevConf 2026",
level: "h1",
color: "#ffffff",
align: "center",
letterSpacing: "-0.03em",
lineHeight: 1.0,
},
children: [],
},
"spacer-2": {
type: "Spacer",
props: { height: 32 },
children: [],
},
"details-row": {
type: "Row",
props: {
gap: 40,
alignItems: "center",
justifyContent: "center",
},
children: ["date-text", "dot-sep", "location-text"],
},
"date-text": {
type: "Text",
props: {
text: "March 15-17, 2026",
fontSize: 28,
color: "#a1a1aa",
},
children: [],
},
"dot-sep": {
type: "Text",
props: { text: "/", fontSize: 28, color: "#52525b" },
children: [],
},
"location-text": {
type: "Text",
props: {
text: "San Francisco, CA",
fontSize: 28,
color: "#a1a1aa",
},
children: [],
},
"spacer-3": {
type: "Spacer",
props: { height: 48 },
children: [],
},
cta: {
type: "Box",
props: {
backgroundColor: "#6366f1",
borderRadius: 12,
paddingTop: 16,
paddingBottom: 16,
paddingLeft: 48,
paddingRight: 48,
alignItems: "center",
justifyContent: "center",
},
children: ["cta-text"],
},
"cta-text": {
type: "Text",
props: {
text: "Register Now",
fontSize: 24,
color: "#ffffff",
fontWeight: "bold",
letterSpacing: "0.02em",
},
children: [],
},
},
},
},
// ==========================================================================
// Social Square
// ==========================================================================
{
name: "social-square",
label: "Social Square",
description: "Square format card for Instagram or social media (1080x1080)",
spec: {
root: "frame",
elements: {
frame: {
type: "Frame",
props: {
width: 1080,
height: 1080,
backgroundColor: "#faf5ff",
padding: 80,
flexDirection: "column",
justifyContent: "space-between",
},
children: ["top-section", "quote-section", "bottom-section"],
},
"top-section": {
type: "Row",
props: {
alignItems: "center",
justifyContent: "space-between",
},
children: ["quote-icon", "slide-number"],
},
"quote-icon": {
type: "Heading",
props: { text: '"', level: "h1", color: "#9333ea" },
children: [],
},
"slide-number": {
type: "Text",
props: {
text: "01 / 05",
fontSize: 18,
color: "#a855f7",
fontWeight: "bold",
letterSpacing: "0.1em",
},
children: [],
},
"quote-section": {
type: "Column",
props: { gap: 24 },
children: ["quote-text"],
},
"quote-text": {
type: "Heading",
props: {
text: "The best way to predict the future is to create it.",
level: "h2",
color: "#1e1b4b",
letterSpacing: "-0.01em",
lineHeight: 1.3,
},
children: [],
},
"bottom-section": {
type: "Column",
props: { gap: 4 },
children: ["author-name-sq", "author-title"],
},
"author-name-sq": {
type: "Text",
props: {
text: "Peter Drucker",
fontSize: 22,
color: "#1e1b4b",
fontWeight: "bold",
},
children: [],
},
"author-title": {
type: "Text",
props: {
text: "Management Consultant & Author",
fontSize: 18,
color: "#7c3aed",
},
children: [],
},
},
},
},
// ==========================================================================
// Bar Graph
// ==========================================================================
{
name: "bar-graph",
label: "Bar Graph",
description: "Data visualization bar chart (1200x630)",
spec: {
root: "frame",
elements: {
frame: {
type: "Frame",
props: {
width: 1200,
height: 630,
backgroundColor: "#ffffff",
padding: 48,
flexDirection: "column",
},
children: ["header", "chart-area"],
},
header: {
type: "Column",
props: { gap: 4 },
children: ["chart-title", "chart-subtitle"],
},
"chart-title": {
type: "Heading",
props: {
text: "Monthly Revenue",
level: "h2",
color: "#0f172a",
letterSpacing: "-0.02em",
lineHeight: 1.2,
},
children: [],
},
"chart-subtitle": {
type: "Text",
props: {
text: "Q3-Q4 2025 (in thousands USD)",
fontSize: 18,
color: "#94a3b8",
},
children: [],
},
"chart-area": {
type: "Row",
props: {
gap: 0,
alignItems: "flex-end",
justifyContent: "space-between",
flex: 1,
},
children: [
"bar-jul",
"bar-aug",
"bar-sep",
"bar-oct",
"bar-nov",
"bar-dec",
],
},
"bar-jul": {
type: "Column",
props: {
gap: 8,
alignItems: "center",
justifyContent: "flex-end",
flex: 1,
},
children: ["val-jul", "rect-jul", "lbl-jul"],
},
"val-jul": {
type: "Text",
props: {
text: "$42k",
fontSize: 16,
color: "#64748b",
align: "center",
fontWeight: "bold",
},
children: [],
},
"rect-jul": {
type: "Box",
props: {
width: 80,
height: 168,
backgroundColor: "#6366f1",
borderRadius: 8,
},
children: [],
},
"lbl-jul": {
type: "Text",
props: {
text: "Jul",
fontSize: 16,
color: "#64748b",
align: "center",
},
children: [],
},
"bar-aug": {
type: "Column",
props: {
gap: 8,
alignItems: "center",
justifyContent: "flex-end",
flex: 1,
},
children: ["val-aug", "rect-aug", "lbl-aug"],
},
"val-aug": {
type: "Text",
props: {
text: "$58k",
fontSize: 16,
color: "#64748b",
align: "center",
fontWeight: "bold",
},
children: [],
},
"rect-aug": {
type: "Box",
props: {
width: 80,
height: 232,
backgroundColor: "#6366f1",
borderRadius: 8,
},
children: [],
},
"lbl-aug": {
type: "Text",
props: {
text: "Aug",
fontSize: 16,
color: "#64748b",
align: "center",
},
children: [],
},
"bar-sep": {
type: "Column",
props: {
gap: 8,
alignItems: "center",
justifyContent: "flex-end",
flex: 1,
},
children: ["val-sep", "rect-sep", "lbl-sep"],
},
"val-sep": {
type: "Text",
props: {
text: "$51k",
fontSize: 16,
color: "#64748b",
align: "center",
fontWeight: "bold",
},
children: [],
},
"rect-sep": {
type: "Box",
props: {
width: 80,
height: 204,
backgroundColor: "#6366f1",
borderRadius: 8,
},
children: [],
},
"lbl-sep": {
type: "Text",
props: {
text: "Sep",
fontSize: 16,
color: "#64748b",
align: "center",
},
children: [],
},
"bar-oct": {
type: "Column",
props: {
gap: 8,
alignItems: "center",
justifyContent: "flex-end",
flex: 1,
},
children: ["val-oct", "rect-oct", "lbl-oct"],
},
"val-oct": {
type: "Text",
props: {
text: "$73k",
fontSize: 16,
color: "#64748b",
align: "center",
fontWeight: "bold",
},
children: [],
},
"rect-oct": {
type: "Box",
props: {
width: 80,
height: 292,
backgroundColor: "#818cf8",
borderRadius: 8,
},
children: [],
},
"lbl-oct": {
type: "Text",
props: {
text: "Oct",
fontSize: 16,
color: "#64748b",
align: "center",
},
children: [],
},
"bar-nov": {
type: "Column",
props: {
gap: 8,
alignItems: "center",
justifyContent: "flex-end",
flex: 1,
},
children: ["val-nov", "rect-nov", "lbl-nov"],
},
"val-nov": {
type: "Text",
props: {
text: "$85k",
fontSize: 16,
color: "#64748b",
align: "center",
fontWeight: "bold",
},
children: [],
},
"rect-nov": {
type: "Box",
props: {
width: 80,
height: 340,
backgroundColor: "#818cf8",
borderRadius: 8,
},
children: [],
},
"lbl-nov": {
type: "Text",
props: {
text: "Nov",
fontSize: 16,
color: "#64748b",
align: "center",
},
children: [],
},
"bar-dec": {
type: "Column",
props: {
gap: 8,
alignItems: "center",
justifyContent: "flex-end",
flex: 1,
},
children: ["val-dec", "rect-dec", "lbl-dec"],
},
"val-dec": {
type: "Text",
props: {
text: "$97k",
fontSize: 16,
color: "#64748b",
align: "center",
fontWeight: "bold",
},
children: [],
},
"rect-dec": {
type: "Box",
props: {
width: 80,
height: 388,
backgroundColor: "#4f46e5",
borderRadius: 8,
},
children: [],
},
"lbl-dec": {
type: "Text",
props: {
text: "Dec",
fontSize: 16,
color: "#64748b",
align: "center",
},
children: [],
},
},
},
},
];
-6
View File
@@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
-6
View File
@@ -1,6 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
-7
View File
@@ -1,7 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
serverExternalPackages: ["@resvg/resvg-js", "satori"],
};
export default nextConfig;
-44
View File
@@ -1,44 +0,0 @@
{
"name": "example-image",
"version": "0.1.1",
"type": "module",
"private": true,
"scripts": {
"dev": "portless image-demo.json-render next dev --turbopack",
"build": "next build",
"start": "next start",
"check-types": "tsc --noEmit",
"lint": "eslint --max-warnings 0"
},
"dependencies": {
"@json-render/core": "workspace:*",
"@json-render/image": "workspace:*",
"@resvg/resvg-js": "^2.6.2",
"satori": "^0.19.2",
"@tailwindcss/postcss": "^4.1.18",
"ai": "6.0.103",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"geist": "^1.7.0",
"lucide-react": "^0.575.0",
"next": "16.1.6",
"radix-ui": "^1.4.3",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-resizable-panels": "^4.4.1",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.18",
"zod": "4.3.6"
},
"devDependencies": {
"@internal/eslint-config": "workspace:*",
"@internal/typescript-config": "workspace:*",
"@types/node": "^22.10.0",
"@types/react": "19.2.3",
"@types/react-dom": "19.2.3",
"eslint": "^9.39.1",
"shadcn": "^3.8.5",
"tw-animate-css": "^1.4.0",
"typescript": "^5.7.2"
}
}
-8
View File
@@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
-13
View File
@@ -1,13 +0,0 @@
{
"extends": "../../packages/typescript-config/nextjs.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"declaration": false,
"declarationMap": false,
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
-37
View File
@@ -1,37 +0,0 @@
# example-no-ai
## 0.1.4
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/react@0.11.0
- @json-render/shadcn@0.11.0
## 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
+41 -59
View File
@@ -2,31 +2,33 @@
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 { JSONUIProvider, Renderer } from "@json-render/react";
import type { Spec } from "@json-render/core";
import { import {
registry, Renderer,
actionHandlers, StateProvider,
computedFunctions, VisibilityProvider,
onConfetti, ActionProvider,
} from "@/lib/render/registry"; ValidationProvider,
} from "@json-render/react";
import type { Spec } from "@json-render/core";
import { registry, actionHandlers, 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 (
<JSONUIProvider <StateProvider initialState={spec.state ?? {}}>
registry={registry} <VisibilityProvider>
initialState={spec.state ?? {}} <ActionProvider handlers={actionHandlers}>
handlers={actionHandlers} <ValidationProvider>
functions={computedFunctions} <Renderer spec={spec} registry={registry} />
> </ValidationProvider>
<Renderer spec={spec} registry={registry} /> </ActionProvider>
</JSONUIProvider> </VisibilityProvider>
</StateProvider>
); );
} }
export default function Page() { export default function Page() {
const [selectedIndex, setSelectedIndex] = useState(0); const [selectedIndex] = 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);
@@ -39,50 +41,30 @@ export default function Page() {
useEffect(() => onConfetti(fireConfetti), [fireConfetti]); useEffect(() => onConfetti(fireConfetti), [fireConfetti]);
return ( return (
<div className="h-screen flex flex-col bg-muted/30"> <div className="h-screen flex items-center justify-center bg-muted/30">
{/* Example selector */} <div
<nav className="flex gap-1 p-3 overflow-x-auto border-b bg-background shrink-0"> className="relative bg-background border rounded-lg shadow-sm"
{examples.map((ex, i) => ( style={{ width: 960, height: 1080 }}
<button >
key={ex.name} {confettiActive && (
onClick={() => setSelectedIndex(i)} <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
className={`px-3 py-1.5 text-sm rounded-md whitespace-nowrap transition-colors ${ <ConfettiExplosion
i === selectedIndex key={confettiKey}
? "bg-primary text-primary-foreground" portal={false}
: "hover:bg-muted" force={0.8}
}`} duration={3500}
> particleCount={400}
{ex.name} particleSize={8}
</button> colors={["#00F0FF", "#7B61FF", "#FF3DFF", "#00FF94", "#FFE14D"]}
))} width={1600}
</nav> height="200vh"
zIndex={1}
{/* Render area */} onComplete={() => setConfettiActive(false)}
<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 -1
View File
@@ -1,4 +1,4 @@
import { nextJsConfig } from "@internal/eslint-config/next-js"; import { nextJsConfig } from "@repo/eslint-config/next-js";
/** @type {import("eslint").Linter.Config[]} */ /** @type {import("eslint").Linter.Config[]} */
export default [ export default [
-349
View File
@@ -508,353 +508,4 @@ 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: [],
},
},
},
},
]; ];
-9
View File
@@ -13,13 +13,4 @@ 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 -27
View File
@@ -1,7 +1,6 @@
"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";
@@ -14,14 +13,6 @@ 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,
@@ -33,23 +24,6 @@ export const { registry } = defineRegistry(catalog, {
}, },
}); });
export const actionHandlers: Record< export const actionHandlers: Record<string, () => void> = {
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] ?? [];
},
};
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // 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 -4
View File
@@ -1,10 +1,10 @@
{ {
"name": "example-no-ai", "name": "example-no-ai",
"version": "0.1.4", "version": "0.1.0",
"type": "module", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "portless no-ai-demo.json-render next dev --turbopack", "dev": "next dev --turbopack --port 3003",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint --max-warnings 0", "lint": "eslint --max-warnings 0",
@@ -23,10 +23,10 @@
"react-dom": "19.2.4", "react-dom": "19.2.4",
"react-confetti-explosion": "^3.0.3", "react-confetti-explosion": "^3.0.3",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"zod": "4.3.6" "zod": "4.3.5"
}, },
"devDependencies": { "devDependencies": {
"@internal/eslint-config": "workspace:*", "@repo/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",
-33
View File
@@ -1,33 +0,0 @@
# example-react-native
## 0.1.4
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/react-native@0.11.0
## 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
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "example-react-native", "name": "example-react-native",
"version": "0.1.4", "version": "0.1.0",
"private": true, "private": true,
"main": "expo-router/entry", "main": "expo-router/entry",
"scripts": { "scripts": {
@@ -25,7 +25,7 @@
"react-native": "0.81.4", "react-native": "0.81.4",
"react-native-safe-area-context": "~5.4.0", "react-native-safe-area-context": "~5.4.0",
"react-native-screens": "~4.11.1", "react-native-screens": "~4.11.1",
"zod": "^4.3.6" "zod": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
-33
View File
@@ -1,33 +0,0 @@
# example-react-pdf
## 0.1.4
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/react-pdf@0.11.0
## 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
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // 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 -4
View File
@@ -1,10 +1,10 @@
{ {
"name": "example-react-pdf", "name": "example-react-pdf",
"version": "0.1.4", "version": "0.1.0",
"type": "module", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "portless react-pdf-demo.json-render next dev --turbopack", "dev": "next dev --turbopack --port 3005",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"check-types": "tsc --noEmit" "check-types": "tsc --noEmit"
@@ -25,10 +25,10 @@
"react-resizable-panels": "^4.4.1", "react-resizable-panels": "^4.4.1",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"zod": "4.3.6" "zod": "4.3.5"
}, },
"devDependencies": { "devDependencies": {
"@internal/typescript-config": "workspace:*", "@repo/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",
-32
View File
@@ -1,32 +0,0 @@
# example-remotion
## 0.1.4
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/remotion@0.11.0
## 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
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // 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 -4
View File
@@ -1,10 +1,10 @@
{ {
"name": "example-remotion", "name": "example-remotion",
"version": "0.1.4", "version": "0.1.0",
"type": "module", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "portless remotion-demo.json-render next dev --turbopack", "dev": "next dev --turbopack --port 3002",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"check-types": "tsc --noEmit" "check-types": "tsc --noEmit"
@@ -26,10 +26,10 @@
"remotion": "4.0.418", "remotion": "4.0.418",
"shiki": "^3.21.0", "shiki": "^3.21.0",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"zod": "^4.3.6" "zod": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@internal/eslint-config": "workspace:*", "@repo/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
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // 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 -1
View File
@@ -4,7 +4,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "portless stripe-api-demo.json-render next dev", "dev": "next dev --port 3001",
"build": "next build", "build": "next build",
"start": "next start --port 3001" "start": "next start --port 3001"
}, },
@@ -1,33 +0,0 @@
# com.example.json-render-demo
## 0.0.5
### Patch Changes
- Updated dependencies [3f1e71e]
- @json-render/core@0.11.0
- @json-render/react@0.11.0
## 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

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