Compare commits

...
Author SHA1 Message Date
github-actions[bot] 22545b49cc chore: version packages (#221)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-13 17:05:54 -05:00
Chris Tate a8afd8bfe2 prepare v0.14 (#220) 2026-03-13 13:50:56 -05:00
Chris Tate dc489601be add yaml wire format and universal edit modes (#219)
* add yaml wire format and universal edit modes

**Summary**

- Add `@json-render/yaml` package with streaming parser, YAML prompt generation, and AI SDK transform
- Add universal edit modes (patch, merge, diff) to `@json-render/core`, usable across both JSONL and YAML formats
- Integrate YAML support and edit mode selection into the web playground with format toggle, token usage display, and prompt caching

* improvements

* fixes

* fixes ci

* docs
2026-03-13 13:37:21 -05:00
Chris Tate 7758d4bbd5 faster ci (#212)
* faster ci

* fix workflow
2026-03-12 22:47:02 -05:00
github-actions[bot] 6225fc41eb chore: version packages (#211)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-12 10:20:48 -05:00
Chris Tate 5b32de8720 prepare v0.13 (#210) 2026-03-12 10:18:18 -05:00
Chris Tate f6d1c5134b react-three-fiber (#209) 2026-03-12 10:14:11 -05:00
Chris Tate 316439fcd7 add solid example (#208) 2026-03-11 22:54:56 -05:00
Chris Tateandctate c180f529a0 feat: add strict mode to jsonSchema() for LLM structured outputs (#193)
* feat: add strict mode to jsonSchema() for LLM structured outputs

This PR adds support for generating strict JSON schemas compatible with LLM structured output APIs (OpenAI, Google Gemini, Anthropic, etc.).

## Problem
`catalog.jsonSchema()` was generating schemas that OpenAI's strict mode rejected due to:
- Use of `propertyNames` keyword (not permitted)
- Missing `additionalProperties: false` on nested objects
- Optional properties not being handled correctly for strict validation

## Changes
- Added `JsonSchemaOptions` interface with `strict` boolean flag
- Updated `catalog.jsonSchema()` to accept optional `JsonSchemaOptions` parameter
- When `strict: true`, the schema generator:
  - Sets `additionalProperties: false` on all object types
  - Removes `propertyNames` constraints
  - Lists all properties (including optional ones) in `required` arrays, using nullable types for optionals
  - Converts record types to fixed-key objects without additional property schemas
- Fixed Zod 4 compatibility issue in type name resolution
- Added comprehensive test suite covering all strict mode requirements
- Exported `JsonSchemaOptions` type from core package

## Usage
```typescript
// Default behavior (unchanged)
const schema = catalog.jsonSchema();

// Strict mode for LLM APIs
const strictSchema = catalog.jsonSchema({ strict: true });
```

The default behavior remains unchanged to maintain backward compatibility.

Fixes #180

* fix: document record type limitation and add anyOf nullable test

- Add clear JSDoc documenting that record/map types become opaque objects
  in strict mode (LLM strict schemas require additionalProperties: false)
- Improve inline comment on the record case in zodToJsonSchema
- Add test exercising the anyOf nullable wrapping for optional properties
  using a non-record schema so the path is directly verified

---------

Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
2026-03-11 22:17:09 -05:00
Chris Tate c3c0a36f20 better visual (#206) 2026-03-11 12:16:51 -05:00
Patrick HallandPatrick 9bb82604f0 solid: add renderer implementation (#201)
* solid: add renderer implementation

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

* fix: solid reactivity bugs in validation + state

---------

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

## Changes

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

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

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

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

* improvements

* fixes

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

## Changes Made

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

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

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

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

* helpers

* reactive API

* turn into provider components

* fixes

* examples

* docs

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

* shuffle things around

* createRenderer

* on

* boundary/warning

* functions

* fix

* fix types
2026-03-06 17:31:06 -06:00
b6f12d4d53 Feat/react email support (#159)
* add react-email workspace dependencies

* add react-email package

* add react-email example with Vercel Invite, Stripe Welcome and Nike Receipt

* add render tests for react-email package

* fix missing style prop on stripe-welcome logo Image

* add react email documentation

* add json render react email skill

* fix @internal/react-state alias in vitest config

Pre-existing issue: vitest could not resolve @internal/react-state,
causing 6 test suites in packages/react and packages/react-pdf to fail.

* fix PR review feedback

- fix tsconfig: remove rootDir, expand include for test imports
- fix render tests: add non-null assertions for noUncheckedIndexedAccess
- fix dev script: add portless to match other examples
- fix .env.example: correct comment from video to email generation
- fix docs: add blank line before heading in installation page

* merge latest

* fix: update tsconfig extends from @repo to @internal/typescript-config

* fixes

* fixes

---------

Co-authored-by: WManzoli <willmanzoli@gmail.com>
Co-authored-by: Chris Tate <chris@ctate.dev>
2026-03-02 16:43:03 -06:00
Chris Tate f29b1c2ef6 adds predev for portless to vue/vite (#178) 2026-03-02 11:52:08 -06:00
Michał Czapliński 8968bd648a Add missing portless dependency (#143)
* add portless dependency to package.json and update pnpm-lock.yaml

* fix: Add a `predev` command which warns that global portless install is required.

* fix: remove portless dependency

* fix: revert changes to pnpm-lock.yml

* revert again after pulling latest changes
2026-03-02 11:43:55 -06:00
github-actions[bot] 023ca789b2 chore: version packages (#176)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 13:29:16 -06:00
Chris Tate 3f1e71e779 prepare v0.11.0 (#175)
* prepare v0.11.0

* release instructions
2026-02-27 13:25:55 -06:00
Chris Tate 553c803422 image (#173)
* image

* fixes

* fixes

* fix ci

* fix ci

* disable @next/next/no-img-element rule in ESLint configuration

* update route.ts to use path.join for font resolution and adjust next-env.d.ts import path
2026-02-27 13:03:42 -06:00
Chris Tate 9f58d8712c fix docs (#167)
* fix docs

* fix docs
2026-02-25 16:19:29 -06:00
github-actions[bot] c2b397510e chore: version packages (#166)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-25 10:56:29 -06:00
Chris Tate 8506cfaa03 fix pkg name (#165)
* fix pkg name

* faster builds

* fixes

* Revert "fixes"

This reverts commit d0b43db972.

* Revert "faster builds"

This reverts commit 34a5190b07.
2026-02-25 10:50:59 -06:00
Chris Tate 9cef4e9142 prepare v0.10 (#164) 2026-02-25 10:27:25 -06:00
Chris Tate 3c11f19be4 vue improvements (#163)
* vue improvements

* fixes

* fixes

* fixes

* fix ci

* fix ci
2026-02-25 09:44:57 -06:00
Anthony Fu db3a8b41e9 feat: add Vue renderer (#162)
* feat: vue support

* feat: add a vite example

* use css instead of inline styles

* feat: add tests

* chore: use portless

* fix: update reactivity

* chore: update

* chore: build

* feat: add hooks, add missing zod peer deps
2026-02-25 08:21:11 -06:00
Chris Tate ea47b66dfc add dynamic forms support: computed values, watchers, cross-field validation (#156)
* add dynamic forms support: computed values, watchers, cross-field validation

- `$computed` and `$template` prop expressions for derived values and string interpolation
- Element-level `watch` field for cascading state dependencies
- Cross-field validators (`lessThan`, `greaterThan`, `equalTo`, `requiredIf`) with deep arg resolution
- `validateForm` built-in action for form-level validation

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* tests

* fixes

* update turbo

* tests

* fixes

* fixes

* fixes
2026-02-24 23:04:48 -06:00
Chris Tate cd82f969c8 xstate doc/test updates (#160) 2026-02-24 22:09:42 -06:00
David KhourshidandClaude Opus 4.6 6bcaaad57d feat: Add @xstate/store (atom) support (#157)
* feat: add @xstate/store integration using atoms

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add README for @json-render/xstate-store

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: rename xstateStoreStateStore to xstateStore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* README updates

* Keep naming convention

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 22:01:33 -06:00
github-actions[bot] 0b7d767cdd chore: version packages (#154)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-24 06:29:45 -06:00
Chris Tate b1036763d2 fixed: Install failure due to private dependency (#153)
* fix @internal/react-state import

* add changeset for @internal/react-state install fix

* docs: add v0.9.1 changelog entry
2026-02-24 06:25:34 -06:00
github-actions[bot] c502d5517e chore: version packages (#149)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-24 01:36:59 -06:00
Chris Tate 8740deb018 Update config.json (#148) 2026-02-24 01:34:16 -06:00
Chris Tate 1d755c104a prepare v0.9.0 (#147) 2026-02-24 01:21:02 -06:00
Chris Tate d904d45150 fix schema import to use server-safe subpath (#146)
- `@json-render/react` barrel-imports React contexts that call `createContext`, which crashes in Next.js App Router API routes (RSC runtime strips `createContext`)
- Updated all docs, READMEs, examples, and skills to 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

Fixes #123
2026-02-24 00:55:33 -06:00
Chris Tate a110c6e0ea fix chaining actions (#144)
* fix chaining actions

* improvements
2026-02-24 00:40:40 -06:00
Juzisuan965 7de08ccdf7 fix: safely resolve inner type for Zod arrays (#140) 2026-02-24 00:02:04 -06:00
Chris Tate 3201854481 external store adapter for state management (#139)
* external store adapter for state management

Introduces a `StateStore` interface that lets users plug in their own state management (Redux, Zustand, XState, etc.) instead of being locked into the internal `useState`-based store.

- Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`
- `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop for controlled mode
- 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

* improvements

* update docs

* improvements

* fixes

* fix CI

* add store adapters

* fixes

* fixes

* fixes

* fixes

* e2e tests

* improvements

* fixes

* fixes

* fixes

* fixes

* update lockfile for widened react peer deps

* fix dashboard build
2026-02-23 23:12:57 -06:00
Chris Tate 64c889221e fix playground og (#138) 2026-02-22 16:10:31 -06:00
Chris Tate 49838fa353 update og font (#137) 2026-02-22 16:02:07 -06:00
Chris Tate fa47b08869 update header font (#136) 2026-02-22 15:51:54 -06:00
Chris Tate 5ccb109c08 use visual-json (#135)
* use visual-json

* fix lint
2026-02-22 15:42:29 -06:00
Chris Tate 62932f6516 fix gitignore (#134) 2026-02-22 15:20:33 -06:00
Chris Tate ee28d548c1 use portless (#133) 2026-02-22 15:15:17 -06:00
Chris Tate 0e6f2afc6c fix og (#128) 2026-02-20 02:41:16 -06:00
Chris Tate bccedc2459 add docs (#127) 2026-02-20 02:30:33 -06:00
github-actions[bot] 0a404302ef chore: version packages (#126)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-20 02:17:13 -06:00
Chris Tate 09376db2f6 v0.8.0 changeset (#125) 2026-02-20 02:15:51 -06:00
Chris Tate 4c294417f4 pdf (#124)
* no-ai

* pdf

* pdf example

* fixes

* ai gateway

* fixes

* fixes

* fixes

* shadcn

* 3 panes

* fixes

* fixes

* fixes

* fixes

* fix CI

* fix
2026-02-20 02:08:25 -06:00
Brian Muenzenmeyer f77c1d6c98 Align rendering of ai-sdk table with other patterns (#122)
* Add table for AI SDK generation and chat modes

* Duplicate table for AI SDK generation modes
2026-02-18 21:02:38 -06:00
Chris Tate a66aef17f9 stripe updates (#121)
* stripe cleanup

* full screen flag

* fixes

* stripe cleanup

* refactor

* fixes

* progressive

* fix data

* fixes

* fixes actions

* fixes

* fixes
2026-02-18 13:12:21 -06:00
Chris Tate ba9ffa3c91 update star count (#120) 2026-02-18 09:21:04 -06:00
Chris Tate b7d5a75bfa update website docs (#119) 2026-02-17 02:26:57 -06:00
Chris Tate 0dfe07da45 v0.7.0 docs (#118) 2026-02-17 01:50:07 -06:00
github-actions[bot] c82eefd1c5 chore: version packages (#117)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-17 01:39:40 -06:00
Chris Tate 2d70fab00a v0.7.0 changeset (#116) 2026-02-17 01:37:35 -06:00
Chris Tate 320c935bfb shadcn (#115)
* shadcn

* fix build error

* fix readme

* fix CI error

* improvements

* fixes

* stronger types

* fix interleave

* fixes

* on arg

* minor fixes

* fix lock
2026-02-17 01:28:04 -06:00
github-actions[bot] e7103ce519 chore: version packages (#113)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-15 09:06:46 -06:00
Chris Tate 43ad534482 v0.6.1 changeset (#112) 2026-02-15 09:04:39 -06:00
Chris Tate ea97aff3e0 fix max update depth on homepage demo (#111)
When form inputs lack `$bindState` bindings (like in the homepage contact form simulation), `useFieldValidation` was called with `bindings?.value ?? ""` as the path. All three inputs registered at the same `""` path but with different validation configs. Each `registerField` call overwrote the previous one, triggering a re-render where the other inputs would see a mismatched config and re-register -- creating an infinite loop.

- Fix infinite re-render loop caused by multiple unbound form inputs (Input, Textarea, Select) all registering field validation at path `""` with different `checks` configs, causing them to overwrite each other endlessly
- Stabilize context values in ActionProvider, ValidationProvider, and useUIStream by using refs for state/callbacks, preventing unnecessary re-render cascades on every state update
2026-02-15 08:56:08 -06:00
github-actions[bot] 9af3f999e0 chore: version packages (#109)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-13 18:03:21 -06:00
Chris Tate 06b8745da7 v0.6.0 (#108) 2026-02-13 18:00:57 -06:00
Chris Tate ddae61805e inline mode (#107)
* ai sdk

* chat

* move more to lib

* fixes

* refactor

* fixes

* streamdown

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* data-spec

* fixes

* sortable

* github

* tools

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* tables

* fixes

* fixes

* rename to chat

* fixes docs

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fix lint

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes
2026-02-13 17:53:03 -06:00
Chris Tate f11283fa92 fix write file (#103) 2026-02-11 19:10:14 -06:00
Chris Tate fd8c7a489f fix missing close button on mobile (#102) 2026-02-11 14:55:24 -06:00
Chris Tate f8e39b30fe update docs (#101) 2026-02-11 14:50:25 -06:00
Chris Tate 68ba7c6d7d sidebar chat (#100)
* fix chat height

* fixes

* fixes

* fixes

* sidebar

* fix
2026-02-11 11:41:05 -06:00
Chris Tate 7c4a6fed9a better registry docs (#96) 2026-02-10 13:47:00 -06:00
Chris Tate 5b4fcaa349 chat improvements (#93)
* better chat

* fixes

* fix lint

* chat fixes
2026-02-09 11:15:57 -06:00
Chris Tate 2b3f3a723f better chat (#92)
* better chat

* fixes

* fix lint
2026-02-09 09:44:29 -06:00
github-actions[bot] 726ddc1d4f chore: version packages (#91)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-09 08:43:42 -06:00
Chris Tate 429e456a4f changeset (#90)
* changeset

* update
2026-02-09 08:41:41 -06:00
Chris Tate 0e16ca33d4 Fix LLM hallucinations by dynamically generating prompt examples from catalog (#89)
* smarter schema

* better examples
2026-02-09 08:37:03 -06:00
github-actions[bot] edbeb5a637 chore: version packages (#87)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-09 07:09:15 -06:00
Chris Tate d9a4efdbeb changeset (#86) 2026-02-09 07:07:17 -06:00
Chris Tate f435643817 more resilient (#85)
* more resilient

* more resilient
2026-02-09 07:05:16 -06:00
Chris Tate 458f3a728c update docs (#84)
* update docs

* fix og
2026-02-09 02:16:06 -06:00
github-actions[bot] d5734e975c chore: version packages (#83)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-09 01:53:27 -06:00
Chris Tate 3d2d1adb2d update docs (#82) 2026-02-09 01:45:07 -06:00
Chris Tate 801708a128 react-native and other things (#81)
* react native

* fixes

* pop/push

* fixes

* rename data -> state

* remove . tsbuildinfo

* fixes

* fixes

* user prompt

* fixes

* $id

* combine catalog.ts

* better actions

* repeat

* fix docs

* fixes

* fixes

* fixes

* better stream

* catalog

* fixes

* fixes

* fixes

* dialog

* sonner

* accordion

* catalog on homepage

* fixes

* more catalog

* fixes

* fixes

* refactor

* mv

* 404

* fixes

* nested

* fixes

* fixes

* fixes

* fixes

* mobile playground

* mdx

* fix mdx

* fixes

* streamdown

* fixes

* use haiku

* fixes

* great

* fixes

* fix ...

* fixes

* fixes

* fix build

* fix lint

* fix lint

* fix lint

* fix tests
2026-02-09 01:31:46 -06:00
github-actions[bot] e9ea9c782b chore: version packages (#80)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-07 12:31:01 -06:00
Chris Tate dd17549ee8 next release (#79) 2026-02-07 12:17:32 -06:00
Chris Tate 1eb6212dd7 fix json patch (#78) 2026-02-07 12:09:53 -06:00
Chris Tate 711e79b069 remove key/parentKey from flat specs (#77) 2026-02-07 11:46:16 -06:00
github-actions[bot] f3611860a7 chore: version packages (#75)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-06 18:02:12 -06:00
Chris Tate 61ee8e5291 changeset: include remove op in system prompt (#74) 2026-02-06 17:58:50 -06:00
Chris Tate d38b281e5e include remove op in system prompt (#73) 2026-02-06 17:56:03 -06:00
Chris Tate 39fcc192e9 fix stream docs (#72) 2026-02-06 14:48:32 -06:00
github-actions[bot] 96c7a452b3 chore: version packages (#71)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-06 14:33:12 -06:00
Chris Tate 844f18d1ee fix deps (#70) 2026-02-06 14:32:10 -06:00
Chris Tate 54bce097a5 add defineRegistry function (#67) 2026-02-06 14:30:06 -06:00
Chris Tate e2cb239c88 fix version (#69) 2026-02-06 14:28:38 -06:00
Chris Tate 90c59c674d update registry page (#68) 2026-02-06 14:27:07 -06:00
Chris Tate bba38727e8 ignore private packages (#66)
* ignore private packages

* fix ignore

* better ignore
2026-02-06 14:19:34 -06:00
Chris Tate 1d547b579c changeset (#65) 2026-02-06 14:12:00 -06:00
Chris Tate 795cdaac56 defineRegistry (#64)
* defineRegistry

* update dashboard example
2026-02-06 13:56:48 -06:00
Chris Tate 4ae049f218 stripe app (#62)
* stripe-app example

* fix lint

* add readme

* fix lint
2026-02-05 12:53:58 -06:00
Chris Tate f004caf95e combine docs nav (#61) 2026-02-05 10:22:51 -06:00
Chris Tate 72d6468e70 redirect for /docs/registry (#60) 2026-02-05 10:13:26 -06:00
Chris Tate e6f2b1e9b8 chore: bump @json-render/remotion to v0.4.1 (#59) 2026-02-05 05:05:21 -06:00
Chris Tate 85f82590a8 add motion to remotion (#58)
* add motion to remotion

* update example
2026-02-05 05:03:28 -06:00
Chris Tate 6129f312ea v0.4.0 (#57)
* v0.4.0

* slots
2026-02-05 04:15:00 -06:00
Chris Tate c1b1aaaddb custom schema system (#56)
* add rate limits

* fix lint

* better

generate prompt

update examples

* fixes

* fixes

* new api

* update docs

* fixes

* fixes

* generated prompt

* Add tests for catalog validation and prompt generation

- Test generateSystemPrompt with components, actions, custom rules
- Test new defineCatalog API from schema system
- Test catalog.prompt() method with custom rules
- Test catalog.validate() for valid and invalid specs
- Test catalog.zodSchema() for custom validation
- Test catalog.jsonSchema() for structured outputs
- Add tests for nested specs with children
- Add tests for rejecting invalid component types

* Fix lint: pass children as nested elements in renderer

Change from children={...} prop to nested children pattern
to satisfy react/no-children-prop rule.

* Fix lint errors in dashboard and web apps

- Disable react/prop-types in both eslint configs (TypeScript handles this)
- Allow styled-jsx 'jsx' property in dashboard
- Add DATABASE_URL to turbo env allowlist
- Remove unused drizzle-orm imports (and, sql)
- Suppress unused variable warnings where intentional

* Add server entry point for @json-render/remotion

The main package entry includes React components that require
client-side context (React.createContext). This causes build
failures when importing in server-side API routes.

Added `@json-render/remotion/server` entry point that exports
only schema and catalog definitions without React dependencies:
- schema, RemotionSchema, RemotionSpec
- standardComponentDefinitions, standardTransitionDefinitions
- standardEffectDefinitions
- Type exports for catalogs

Updated remotion example to import from /server in catalog.ts

* Make dashboard database connection lazy-initialized

The database connection threw an error at module load time if
DATABASE_URL was not set, causing builds to fail in CI where
the database is not available.

Changed to lazy initialization using a Proxy so the error only
occurs when the database is actually used at runtime, not at
build time.

* Fix previousSpec property name and lazy rate limiting

- Fix property name mismatch: playground now passes previousSpec
  instead of previousTree to match what useUIStream hook expects
- Make rate limiting lazy-initialized to avoid runtime errors when
  Redis env vars (KV_REST_API_URL, KV_REST_API_TOKEN) are not set
- Rate limiting gracefully becomes a no-op when Redis is unavailable
2026-02-05 03:59:56 -06:00
Chris Tate a48f6d665d add rate limits (#46)
* add rate limits

* fix lint
2026-01-25 13:37:03 -06:00
Chris Tate 06235798e1 ui fixes (#41) 2026-01-21 13:55:17 -06:00
Chris Tate e323d263d0 add playground (#40)
* add playground

* update title
2026-01-21 13:46:52 -06:00
1126 changed files with 170270 additions and 8794 deletions
+13
View File
@@ -0,0 +1,13 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in the repository](https://github.com/changesets/changesets).
## Adding a changeset
To add a changeset, run `pnpm changeset` in the root of the repository. This will prompt you to select
which packages have changed and what type of version bump (major, minor, or patch) should be applied.
All `@json-render/*` packages are versioned together -- a changeset for any one of them will bump all
packages to the same version.
+36
View File
@@ -0,0 +1,36 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [
[
"@json-render/core",
"@json-render/react",
"@json-render/react-email",
"@json-render/react-pdf",
"@json-render/shadcn",
"@json-render/react-native",
"@json-render/remotion",
"@json-render/codegen",
"@json-render/zustand",
"@json-render/redux",
"@json-render/jotai",
"@json-render/vue",
"@json-render/xstate",
"@json-render/image",
"@json-render/mcp",
"@json-render/svelte",
"@json-render/solid",
"@json-render/react-three-fiber",
"@json-render/yaml"
]
],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"privatePackages": {
"version": true,
"tag": false
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
}
}
}
+34 -27
View File
@@ -13,36 +13,43 @@ concurrency:
cancel-in-progress: true
jobs:
ci:
name: Lint, Type Check & Build
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.0.0
- name: Setup Node.js
uses: actions/setup-node@v4
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- name: Install dependencies
run: pnpm install --frozen-lockfile
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm turbo run build --filter='./packages/*'
- run: pnpm test
- name: Lint
run: pnpm lint
- name: Type check
run: pnpm type-check
- name: Test
run: pnpm test
- name: Build
run: pnpm build
typecheck:
name: Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm type-check
+47
View File
@@ -0,0 +1,47 @@
name: Release
on:
push:
branches:
- main
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
pull-requests: write
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Create Release Pull Request or Publish
uses: changesets/action@v1
with:
version: pnpm ci:version
publish: pnpm ci:publish
title: "chore: version packages"
commit: "chore: version packages"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_VERCEL_TOKEN_ELEVATED }}
+13 -5
View File
@@ -4,13 +4,11 @@
node_modules
.pnp
.pnp.js
.pnpm-store/
# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env*
!.env.example
# Testing
coverage
@@ -21,11 +19,16 @@ coverage
# Vercel
.vercel
# Expo
.expo/
# Build Outputs
.next/
out/
build
dist
*.tsbuildinfo
.svelte-kit/
# Debug
@@ -39,3 +42,8 @@ yarn-error.log*
# opensrc - source code for packages
opensrc/
# Stripe apps (generated from template + build artifacts)
examples/stripe-app/*/stripe-app.json
examples/stripe-app/*/.build
examples/stripe-app/*/yarn.lock
+9
View File
@@ -0,0 +1,9 @@
{
"servers": {
"json-render": {
"type": "stdio",
"command": "npx",
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
}
}
}
+98
View File
@@ -2,13 +2,111 @@
Instructions for AI coding agents working with this codebase.
## Package Management
**Always check the latest version before installing a package.**
Before adding or updating any dependency, verify the current latest version on npm:
```bash
npm view <package-name> version
```
Or check multiple packages at once:
```bash
npm view ai version
npm view @ai-sdk/provider-utils version
npm view zod version
```
This ensures we don't install outdated versions that may have incompatible types or missing features.
## Code Style
- Do not use emojis in code or UI
- 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
- Run `pnpm type-check` after each turn to ensure type safety
- When making user-facing changes (new packages, API changes, new features, renamed exports, changed behavior), update the relevant documentation:
- Package `README.md` files in `packages/*/README.md`
- 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)
- `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/<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 -->
+532 -130
View File
@@ -1,100 +1,111 @@
# json-render
**Predictable. Guardrailed. Fast.**
**The Generative UI framework.**
Let end users generate dashboards, widgets, apps, and data visualizations from prompts — safely constrained to components you define.
Generate dynamic, personalized UIs from prompts without sacrificing reliability. Predefined components and actions for safe, predictable output.
```bash
# for React
npm install @json-render/core @json-render/react
# for React with pre-built shadcn/ui components
npm install @json-render/shadcn
# or for React Native
npm install @json-render/core @json-render/react-native
# or for video
npm install @json-render/core @json-render/remotion
# or for PDF documents
npm install @json-render/core @json-render/react-pdf
# or for HTML email
npm install @json-render/core @json-render/react-email @react-email/components @react-email/render
# or for Vue
npm install @json-render/core @json-render/vue
# or for Svelte
npm install @json-render/core @json-render/svelte
# or for SolidJS
npm install @json-render/core @json-render/solid
# or for 3D scenes
npm install @json-render/core @json-render/react-three-fiber @react-three/fiber @react-three/drei three
```
## Why json-render?
When users prompt for UI, you need guarantees. json-render gives AI a **constrained vocabulary** so output is always predictable:
json-render is a **Generative UI** framework: AI generates interfaces from natural language prompts, constrained to components you define. You set the guardrails, AI generates within them:
- **Guardrailed** — AI can only use components in your catalog
- **Predictable** — JSON output matches your schema, every time
- **Fast** — Stream and render progressively as the model responds
- **Guardrailed** - AI can only use components in your catalog
- **Predictable** - JSON output matches your schema, every time
- **Fast** - Stream and render progressively as the model responds
- **Cross-Platform** - React, Vue, Svelte, Solid (web), React Native (mobile) from the same catalog
- **Batteries Included** - 36 pre-built shadcn/ui components ready to use
## Quick Start
### 1. Define Your Catalog (what AI can use)
### 1. Define Your Catalog
```typescript
import { createCatalog } from '@json-render/core';
import { z } from 'zod';
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { z } from "zod";
const catalog = createCatalog({
const catalog = defineCatalog(schema, {
components: {
Card: {
props: z.object({ title: z.string() }),
hasChildren: true,
description: "A card container",
},
Metric: {
props: z.object({
label: z.string(),
valuePath: z.string(), // Binds to your data
format: z.enum(['currency', 'percent', 'number']),
value: z.string(),
format: z.enum(["currency", "percent", "number"]).nullable(),
}),
description: "Display a metric value",
},
Button: {
props: z.object({
label: z.string(),
action: ActionSchema, // AI declares intent, you handle it
action: z.string(),
}),
description: "Clickable button",
},
},
actions: {
export_report: { description: 'Export dashboard to PDF' },
refresh_data: { description: 'Refresh all metrics' },
export_report: { description: "Export dashboard to PDF" },
refresh_data: { description: "Refresh all metrics" },
},
});
```
### 2. Register Your Components (how they render)
### 2. Define Your Components
```tsx
const registry = {
Card: ({ element, children }) => (
<div className="card">
<h3>{element.props.title}</h3>
{children}
</div>
),
Metric: ({ element }) => {
const value = useDataValue(element.props.valuePath);
return <div className="metric">{format(value)}</div>;
import { defineRegistry, Renderer } from "@json-render/react";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
<div className="card">
<h3>{props.title}</h3>
{children}
</div>
),
Metric: ({ props }) => (
<div className="metric">
<span>{props.label}</span>
<span>{format(props.value, props.format)}</span>
</div>
),
Button: ({ props, emit }) => (
<button onClick={() => emit("press")}>{props.label}</button>
),
},
Button: ({ element, onAction }) => (
<button onClick={() => onAction(element.props.action)}>
{element.props.label}
</button>
),
};
});
```
### 3. Let AI Generate
### 3. Render AI-Generated Specs
```tsx
import { DataProvider, ActionProvider, Renderer, useUIStream } from '@json-render/react';
function Dashboard() {
const { tree, send } = useUIStream({ api: '/api/generate' });
return (
<DataProvider initialData={{ revenue: 125000, growth: 0.15 }}>
<ActionProvider actions={{
export_report: () => downloadPDF(),
refresh_data: () => refetch(),
}}>
<input
placeholder="Create a revenue dashboard..."
onKeyDown={(e) => e.key === 'Enter' && send(e.target.value)}
/>
<Renderer tree={tree} components={registry} />
</ActionProvider>
</DataProvider>
);
function Dashboard({ spec }) {
return <Renderer spec={spec} registry={registry} />;
}
```
@@ -102,85 +113,483 @@ function Dashboard() {
---
## Packages
| Package | Description |
| --------------------------- | ---------------------------------------------------------------------- |
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
| `@json-render/react` | React renderer, contexts, hooks |
| `@json-render/vue` | Vue 3 renderer, composables, providers |
| `@json-render/svelte` | Svelte 5 renderer with runes-based reactivity |
| `@json-render/solid` | SolidJS renderer with fine-grained reactive contexts |
| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
| `@json-render/react-three-fiber` | React Three Fiber renderer for 3D scenes (19 built-in components) |
| `@json-render/react-native` | React Native renderer with standard mobile components |
| `@json-render/remotion` | Remotion video renderer, timeline schema |
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
| `@json-render/react-email` | React Email renderer for HTML/plain-text emails from specs |
| `@json-render/image` | Image renderer for SVG/PNG output (OG images, social cards) via Satori |
| `@json-render/codegen` | Utilities for generating code from json-render UI trees |
| `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` |
| `@json-render/zustand` | Zustand adapter for `StateStore` |
| `@json-render/jotai` | Jotai adapter for `StateStore` |
| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` |
| `@json-render/mcp` | MCP Apps integration for Claude, ChatGPT, Cursor, VS Code |
| `@json-render/yaml` | YAML wire format with streaming parser, edit modes, AI SDK transform |
## Renderers
### React (UI)
```tsx
import { defineRegistry, Renderer } from "@json-render/react";
import { schema } from "@json-render/react/schema";
// Flat spec format (root key + elements map)
const spec = {
root: "card-1",
elements: {
"card-1": {
type: "Card",
props: { title: "Hello" },
children: ["button-1"],
},
"button-1": {
type: "Button",
props: { label: "Click me" },
children: [],
},
},
};
// defineRegistry creates a type-safe component registry
const { registry } = defineRegistry(catalog, { components });
<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" />
```
### Svelte (UI)
```typescript
import { defineRegistry, Renderer } from "@json-render/svelte";
import { schema } from "@json-render/svelte/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => /* Svelte 5 snippet */,
Button: ({ props, emit }) => /* Svelte 5 snippet */,
},
});
// In your Svelte component:
// <Renderer spec={spec} registry={registry} />
```
### Solid (UI)
```tsx
import { defineRegistry, Renderer } from "@json-render/solid";
import { schema } from "@json-render/solid/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: (renderProps) => <div>{renderProps.children}</div>,
Button: (renderProps) => (
<button onClick={() => renderProps.emit("press")}>
{renderProps.element.props.label as string}
</button>
),
},
});
<Renderer spec={spec} registry={registry} />;
```
### shadcn/ui (Web)
```tsx
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";
// Pick components from the 36 standard definitions
const catalog = defineCatalog(schema, {
components: {
Card: shadcnComponentDefinitions.Card,
Stack: shadcnComponentDefinitions.Stack,
Heading: shadcnComponentDefinitions.Heading,
Button: shadcnComponentDefinitions.Button,
},
actions: {},
});
// Use matching implementations
const { registry } = defineRegistry(catalog, {
components: {
Card: shadcnComponents.Card,
Stack: shadcnComponents.Stack,
Heading: shadcnComponents.Heading,
Button: shadcnComponents.Button,
},
});
<Renderer spec={spec} registry={registry} />;
```
### React Native (Mobile)
```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";
// 25+ standard components included
const catalog = defineCatalog(schema, {
components: { ...standardComponentDefinitions },
actions: standardActionDefinitions,
});
const { registry } = defineRegistry(catalog, { components: {} });
<Renderer spec={spec} registry={registry} />;
```
### Remotion (Video)
```tsx
import { Player } from "@remotion/player";
import {
Renderer,
schema,
standardComponentDefinitions,
} from "@json-render/remotion";
// Timeline spec format
const spec = {
composition: {
id: "video",
fps: 30,
width: 1920,
height: 1080,
durationInFrames: 300,
},
tracks: [{ id: "main", name: "Main", type: "video", enabled: true }],
clips: [
{
id: "clip-1",
trackId: "main",
component: "TitleCard",
props: { title: "Hello" },
from: 0,
durationInFrames: 90,
},
],
audio: { tracks: [] },
};
<Player
component={Renderer}
inputProps={{ spec }}
durationInFrames={spec.composition.durationInFrames}
fps={spec.composition.fps}
compositionWidth={spec.composition.width}
compositionHeight={spec.composition.height}
/>;
```
### React PDF (Documents)
```typescript
import { renderToBuffer } from "@json-render/react-pdf";
const spec = {
root: "doc",
elements: {
doc: {
type: "Document",
props: { title: "Invoice" },
children: ["page-1"],
},
"page-1": {
type: "Page",
props: { size: "A4" },
children: ["heading-1", "table-1"],
},
"heading-1": {
type: "Heading",
props: { text: "Invoice #1234", level: "h1" },
children: [],
},
"table-1": {
type: "Table",
props: {
columns: [
{ header: "Item", width: "60%" },
{ header: "Price", width: "40%", align: "right" },
],
rows: [
["Widget A", "$10.00"],
["Widget B", "$25.00"],
],
},
children: [],
},
},
};
// Render to buffer, stream, or file
const buffer = await renderToBuffer(spec);
```
### React Email (Email)
```typescript
import { renderToHtml } from "@json-render/react-email";
import { schema, standardComponentDefinitions } from "@json-render/react-email";
import { defineCatalog } from "@json-render/core";
const catalog = defineCatalog(schema, {
components: standardComponentDefinitions,
});
const spec = {
root: "html-1",
elements: {
"html-1": {
type: "Html",
props: { lang: "en", dir: "ltr" },
children: ["head-1", "body-1"],
},
"head-1": { type: "Head", props: {}, children: [] },
"body-1": {
type: "Body",
props: { style: { backgroundColor: "#f6f9fc" } },
children: ["container-1"],
},
"container-1": {
type: "Container",
props: {
style: { maxWidth: "600px", margin: "0 auto", padding: "20px" },
},
children: ["heading-1", "text-1"],
},
"heading-1": { type: "Heading", props: { text: "Welcome" }, children: [] },
"text-1": {
type: "Text",
props: { text: "Thanks for signing up." },
children: [],
},
},
};
const html = await renderToHtml(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 });
```
### Three.js (3D)
```tsx
import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry } from "@json-render/react";
import {
threeComponentDefinitions,
threeComponents,
ThreeCanvas,
} from "@json-render/react-three-fiber";
const catalog = defineCatalog(schema, {
components: {
Box: threeComponentDefinitions.Box,
Sphere: threeComponentDefinitions.Sphere,
AmbientLight: threeComponentDefinitions.AmbientLight,
DirectionalLight: threeComponentDefinitions.DirectionalLight,
OrbitControls: threeComponentDefinitions.OrbitControls,
},
actions: {},
});
const { registry } = defineRegistry(catalog, {
components: {
Box: threeComponents.Box,
Sphere: threeComponents.Sphere,
AmbientLight: threeComponents.AmbientLight,
DirectionalLight: threeComponents.DirectionalLight,
OrbitControls: threeComponents.OrbitControls,
},
});
<ThreeCanvas
spec={spec}
registry={registry}
shadows
camera={{ position: [5, 5, 5], fov: 50 }}
style={{ width: "100%", height: "100vh" }}
/>;
```
## Features
### Conditional Visibility
### Streaming (SpecStream)
Show/hide components based on data, auth, or complex logic:
Stream AI responses progressively:
```typescript
import { createSpecStreamCompiler } from "@json-render/core";
const compiler = createSpecStreamCompiler<MySpec>();
// Process chunks as they arrive
const { result, newPatches } = compiler.push(chunk);
setSpec(result); // Update UI with partial result
// Get final result
const finalSpec = compiler.getResult();
```
### AI Prompt Generation
Generate system prompts from your catalog:
```typescript
const systemPrompt = catalog.prompt();
// Includes component descriptions, props schemas, available actions
```
### Conditional Visibility
```json
{
"type": "Alert",
"props": { "message": "Error occurred" },
"visible": {
"and": [
{ "path": "/form/hasError" },
{ "not": { "path": "/form/errorDismissed" } }
]
}
"visible": [
{ "$state": "/form/hasError" },
{ "$state": "/form/errorDismissed", "not": true }
]
}
```
```json
{
"type": "AdminPanel",
"visible": { "auth": "signedIn" }
}
```
### Dynamic Props
### Rich Actions
Actions with confirmation dialogs and callbacks:
Any prop value can be data-driven using expressions:
```json
{
"type": "Button",
"type": "Icon",
"props": {
"label": "Refund Payment",
"action": {
"name": "refund",
"params": {
"paymentId": { "path": "/selected/id" },
"amount": { "path": "/refund/amount" }
},
"confirm": {
"title": "Confirm Refund",
"message": "Refund ${/refund/amount} to customer?",
"variant": "danger"
},
"onSuccess": { "set": { "/ui/success": true } },
"onError": { "set": { "/ui/error": "$error.message" } }
"name": {
"$cond": { "$state": "/activeTab", "eq": "home" },
"$then": "home",
"$else": "home-outline"
},
"color": {
"$cond": { "$state": "/activeTab", "eq": "home" },
"$then": "#007AFF",
"$else": "#8E8E93"
}
}
}
```
### Built-in Validation
Expression forms:
- **`{ "$state": "/state/key" }`** - reads a value from the state model
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - evaluates a condition and picks a branch
- **`{ "$template": "Hello, ${/user/name}!" }`** - interpolates state values into strings
- **`{ "$computed": "fn", "args": { ... } }`** - calls a registered function with resolved args
### Actions
Components can trigger actions, including the built-in `setState` action:
```json
{
"type": "TextField",
"type": "Pressable",
"props": {
"label": "Email",
"valuePath": "/form/email",
"checks": [
{ "fn": "required", "message": "Email is required" },
{ "fn": "email", "message": "Invalid email" }
],
"validateOn": "blur"
"action": "setState",
"actionParams": { "statePath": "/activeTab", "value": "home" }
},
"children": ["home-icon"]
}
```
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.
---
## Packages
| Package | Description |
|---------|-------------|
| `@json-render/core` | Types, schemas, visibility, actions, validation |
| `@json-render/react` | React renderer, providers, hooks |
## Demo
```bash
@@ -190,40 +599,33 @@ pnpm install
pnpm dev
```
- http://localhost:3000 — Docs & Playground
- http://localhost:3001 — Example Dashboard
## Project Structure
```
json-render/
├── packages/
│ ├── core/ → @json-render/core
│ └── react/ → @json-render/react
├── apps/
│ └── web/ → Docs & Playground site
└── examples/
└── dashboard/ → Example dashboard app
```
- http://json-render.localhost:1355 - Docs & Playground
- http://dashboard-demo.json-render.localhost:1355 - Example Dashboard
- http://react-email-demo.json-render.localhost:1355 - React Email Example
- http://remotion-demo.json-render.localhost:1355 - Remotion Video Example
- Chat Example: run `pnpm dev` in `examples/chat`
- Svelte Example: run `pnpm dev` in `examples/svelte` or `examples/svelte-chat`
- Vue Example: run `pnpm dev` in `examples/vue`
- Vite Renderers (React + Vue + Svelte + Solid): run `pnpm dev` in `examples/vite-renderers`
- React Native example: run `npx expo start` in `examples/react-native`
## How It Works
```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ User Prompt │────▶│ AI + Catalog│────▶│ JSON Tree │
│ "dashboard" │ │ (guardrailed)│ │(predictable)│
└─────────────┘ └──────────────┘ └─────────────┘
│
┌──────────────┐ │
│ Your React │◀───────────┘
│ Components │ (streamed)
└──────────────┘
```mermaid
flowchart LR
A[User Prompt] --> B[AI + Catalog]
B --> C[JSON Spec]
C --> D[Renderer]
B -.- E([guardrailed])
C -.- F([predictable])
D -.- G([streamed])
```
1. **Define the guardrails** — what components, actions, and data bindings AI can use
2. **Users prompt** — end users describe what they want in natural language
3. **AI generates JSON** — output is always predictable, constrained to your catalog
4. **Render fast** — stream and render progressively as the model responds
1. **Define the guardrails** - what components, actions, and data bindings AI can use
2. **Prompt** - describe what you want in natural language
3. **AI generates JSON** - output is always predictable, constrained to your catalog
4. **Render fast** - stream and render progressively as the model responds
## License
+9
View File
@@ -7,3 +7,12 @@ AI_GATEWAY_API_KEY=
# Override the default model used for UI generation
# Default: anthropic/claude-haiku-4.5
AI_GATEWAY_MODEL=anthropic/claude-haiku-4.5
# Vercel KV (Rate Limiting)
# Automatically populated when you add Vercel KV to your project
KV_REST_API_URL=
KV_REST_API_TOKEN=
# Rate Limiting
# RATE_LIMIT_PER_MINUTE=10
# RATE_LIMIT_PER_DAY=100
+1
View File
@@ -35,3 +35,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
.env*.local
+74
View File
@@ -0,0 +1,74 @@
# web
## 0.1.8
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/yaml@0.14.0
- @json-render/codegen@0.14.0
- @json-render/react@0.14.0
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/codegen@0.13.0
- @json-render/react@0.13.0
## 0.1.6
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
- @json-render/codegen@0.12.1
- @json-render/react@0.12.1
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/codegen@0.12.0
- @json-render/react@0.12.0
## 0.1.4
### Patch Changes
- 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
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Open [http://json-render.localhost:1355](http://json-render.localhost:1355) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+285
View File
@@ -0,0 +1,285 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/a2ui")
# A2UI Integration
Use `@json-render/core` to support [A2UI](https://a2ui.org) natively.
<div className="rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 mb-8">
<p className="text-sm text-amber-700 dark:text-amber-300">
<strong>Concept:</strong> This page demonstrates how json-render can support A2UI. The examples are illustrative and may require adaptation for production use.
</p>
</div>
## Native A2UI Support
`@json-render/core` is schema-agnostic. Define a catalog that matches A2UI's format and build a renderer that understands it - no conversion layer needed.
## Example A2UI Message
A2UI uses an adjacency list model - a flat list of components with ID references. This makes it easy to patch individual components:
```json
{
"surfaceUpdate": {
"surfaceId": "main",
"components": [
{
"id": "header",
"component": {
"Text": {
"text": {"literalString": "Book Your Table"},
"usageHint": "h1"
}
}
},
{
"id": "date-picker",
"component": {
"DateTimeInput": {
"label": {"literalString": "Select Date"},
"value": {"path": "/reservation/date"},
"enableDate": true
}
}
},
{
"id": "submit-btn",
"component": {
"Button": {
"child": "submit-text",
"action": {"name": "confirm_booking"}
}
}
},
{
"id": "submit-text",
"component": {
"Text": {"text": {"literalString": "Confirm Reservation"}}
}
}
]
}
}
```
## Define the A2UI Catalog
```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
import { z } from 'zod';
// A2UI BoundValue schema
const BoundString = z.object({
literalString: z.string().optional(),
path: z.string().optional(),
}).refine(d => d.literalString || d.path);
// A2UI children schema
const Children = z.object({
explicitList: z.array(z.string()).optional(),
template: z.object({
dataBinding: z.string(),
componentId: z.string(),
}).optional(),
}).refine(d => d.explicitList || d.template);
export const a2uiCatalog = defineCatalog(schema, {
components: {
Text: {
description: 'Displays text content',
props: z.object({
text: BoundString,
usageHint: z.enum(['h1', 'h2', 'h3', 'body', 'caption']).optional(),
}),
},
Button: {
description: 'Interactive button',
props: z.object({
child: z.string(),
action: z.object({
name: z.string(),
context: z.array(z.object({
key: z.string(),
value: BoundString,
})).optional(),
}).optional(),
}),
},
DateTimeInput: {
description: 'Date/time picker',
props: z.object({
label: BoundString.optional(),
value: BoundString.optional(),
enableDate: z.boolean().optional(),
enableTime: z.boolean().optional(),
}),
},
Column: {
description: 'Vertical layout',
props: z.object({
children: Children,
}),
},
Row: {
description: 'Horizontal layout',
props: z.object({
children: Children,
}),
},
// Add more A2UI standard components...
},
});
```
## Define the A2UI Schema
Define the schema for A2UI message types:
```typescript
import { z } from 'zod';
// Component instance in the adjacency list
const A2UIComponent = z.object({
id: z.string(),
component: z.record(z.record(z.unknown())),
});
// Surface update message
const SurfaceUpdate = z.object({
surfaceId: z.string().optional(),
components: z.array(A2UIComponent),
});
// State model update message
const StateModelUpdate = z.object({
surfaceId: z.string().optional(),
path: z.string().optional(),
contents: z.array(z.object({
key: z.string(),
valueString: z.string().optional(),
valueNumber: z.number().optional(),
valueBoolean: z.boolean().optional(),
valueMap: z.array(z.unknown()).optional(),
})),
});
// Begin rendering message
const BeginRendering = z.object({
surfaceId: z.string().optional(),
root: z.string(),
catalogId: z.string().optional(),
});
// Complete A2UI message schema
export const A2UIMessage = z.object({
surfaceUpdate: SurfaceUpdate.optional(),
dataModelUpdate: StateModelUpdate.optional(),
beginRendering: BeginRendering.optional(),
deleteSurface: z.object({ surfaceId: z.string() }).optional(),
});
```
## Build an A2UI Renderer
Create a renderer that processes the A2UI adjacency list format:
```tsx
import { a2uiCatalog } from './catalog';
// Component registry
const components = {
Text: ({ text, usageHint }) => {
const Tag = usageHint?.startsWith('h') ? usageHint : 'p';
return <Tag>{text}</Tag>;
},
Button: ({ children, action, onAction }) => (
<button onClick={() => onAction?.(action)}>{children}</button>
),
DateTimeInput: ({ label, value, onChange }) => (
<label>
{label}
<input type="date" value={value} onChange={e => onChange?.(e.target.value)} />
</label>
),
Column: ({ children }) => <div className="flex flex-col gap-2">{children}</div>,
Row: ({ children }) => <div className="flex gap-2">{children}</div>,
};
// Render A2UI surface
export function renderA2UI(
componentMap: Map<string, any>,
dataModel: Record<string, any>,
rootId: string,
onAction?: (action: any) => void
) {
function resolveBoundValue(bound: any) {
if (!bound) return undefined;
if (bound.literalString) return bound.literalString;
if (bound.path) {
const parts = bound.path.replace(/^\//, '').split('/');
let value = dataModel;
for (const p of parts) value = value?.[p];
return value;
}
}
function render(id: string): React.ReactNode {
const comp = componentMap.get(id);
if (!comp) return null;
const [type, props] = Object.entries(comp.component)[0];
const Component = components[type];
if (!Component) return null;
// Resolve props
const resolved: any = {};
for (const [key, val] of Object.entries(props as any)) {
if (key === 'child') {
resolved.children = render(val as string);
} else if (key === 'children' && val?.explicitList) {
resolved.children = val.explicitList.map(render);
} else if (val && typeof val === 'object' && ('literalString' in val || 'path' in val)) {
resolved[key] = resolveBoundValue(val);
} else {
resolved[key] = val;
}
}
return <Component key={id} {...resolved} onAction={onAction} />;
}
return render(rootId);
}
```
## Usage
```tsx
const [components] = useState(() => new Map());
const [dataModel, setDataModel] = useState({});
const [rootId, setRootId] = useState<string | null>(null);
// Process A2UI messages
function handleMessage(msg: any) {
if (msg.surfaceUpdate) {
for (const comp of msg.surfaceUpdate.components) {
components.set(comp.id, comp);
}
}
if (msg.dataModelUpdate) {
setDataModel(prev => ({ ...prev, ...msg.dataModelUpdate.contents }));
}
if (msg.beginRendering) {
setRootId(msg.beginRendering.root);
}
}
// Render
{rootId && renderA2UI(components, dataModel, rootId, handleAction)}
```
## Next
Learn about [Adaptive Cards integration](/docs/adaptive-cards) for another UI protocol.
@@ -0,0 +1,408 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/adaptive-cards")
# Adaptive Cards Integration
Use json-render to render [Microsoft Adaptive Cards](https://adaptivecards.io) natively.
<div className="rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 mb-8">
<p className="text-sm text-amber-700 dark:text-amber-300">
<strong>Concept:</strong> This page demonstrates how json-render can support Adaptive Cards. The examples are illustrative and may require adaptation for production use.
</p>
</div>
## Adaptive Cards Overview
Adaptive Cards is a JSON-based format for platform-agnostic UI snippets. Cards have a `body` array of elements and an optional `actions` array for interactive buttons.
### Example Adaptive Card
```json
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "TextBlock",
"text": "Hello, Adaptive Cards!",
"size": "large",
"weight": "bolder"
},
{
"type": "Image",
"url": "https://example.com/image.png",
"altText": "Example image"
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "This is inside a container",
"wrap": true
}
]
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": "auto",
"items": [
{ "type": "TextBlock", "text": "Column 1" }
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{ "type": "TextBlock", "text": "Column 2" }
]
}
]
},
{
"type": "Input.Text",
"id": "userInput",
"placeholder": "Enter your name",
"label": "Name"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Submit"
},
{
"type": "Action.OpenUrl",
"title": "Learn More",
"url": "https://adaptivecards.io"
}
]
}
```
## Creating an Adaptive Cards Catalog
Define a catalog matching the Adaptive Cards element types:
```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
import { z } from 'zod';
// Common Adaptive Cards properties
const Spacing = z.enum(['none', 'small', 'default', 'medium', 'large', 'extraLarge', 'padding']);
const HorizontalAlignment = z.enum(['left', 'center', 'right']);
const VerticalAlignment = z.enum(['top', 'center', 'bottom']);
const FontSize = z.enum(['small', 'default', 'medium', 'large', 'extraLarge']);
const FontWeight = z.enum(['lighter', 'default', 'bolder']);
const ImageSize = z.enum(['auto', 'stretch', 'small', 'medium', 'large']);
const ImageStyle = z.enum(['default', 'person']);
// Base element properties shared by most elements
const BaseElement = {
id: z.string().optional(),
isVisible: z.boolean().optional(),
separator: z.boolean().optional(),
spacing: Spacing.optional(),
};
export const adaptiveCardsCatalog = defineCatalog(schema, {
components: {
// Root card
AdaptiveCard: {
description: 'Root Adaptive Card container',
props: z.object({
version: z.string(),
body: z.array(z.unknown()).optional(),
actions: z.array(z.unknown()).optional(),
fallbackText: z.string().optional(),
minHeight: z.string().optional(),
rtl: z.boolean().optional(),
verticalContentAlignment: VerticalAlignment.optional(),
}),
},
// Elements
TextBlock: {
description: 'Displays text with formatting options',
props: z.object({
...BaseElement,
text: z.string(),
color: z.enum(['default', 'dark', 'light', 'accent', 'good', 'warning', 'attention']).optional(),
fontType: z.enum(['default', 'monospace']).optional(),
horizontalAlignment: HorizontalAlignment.optional(),
isSubtle: z.boolean().optional(),
maxLines: z.number().optional(),
size: FontSize.optional(),
weight: FontWeight.optional(),
wrap: z.boolean().optional(),
}),
},
Image: {
description: 'Displays an image',
props: z.object({
...BaseElement,
url: z.string(),
altText: z.string().optional(),
backgroundColor: z.string().optional(),
height: z.string().optional(),
width: z.string().optional(),
horizontalAlignment: HorizontalAlignment.optional(),
size: ImageSize.optional(),
style: ImageStyle.optional(),
}),
},
Container: {
description: 'Groups elements together',
props: z.object({
...BaseElement,
items: z.array(z.unknown()),
style: z.enum(['default', 'emphasis', 'good', 'attention', 'warning', 'accent']).optional(),
verticalContentAlignment: VerticalAlignment.optional(),
bleed: z.boolean().optional(),
minHeight: z.string().optional(),
}),
},
ColumnSet: {
description: 'Arranges columns horizontally',
props: z.object({
...BaseElement,
columns: z.array(z.unknown()),
horizontalAlignment: HorizontalAlignment.optional(),
minHeight: z.string().optional(),
}),
},
Column: {
description: 'A column within a ColumnSet',
props: z.object({
...BaseElement,
items: z.array(z.unknown()).optional(),
width: z.union([z.string(), z.number()]).optional(),
style: z.enum(['default', 'emphasis', 'good', 'attention', 'warning', 'accent']).optional(),
verticalContentAlignment: VerticalAlignment.optional(),
}),
},
FactSet: {
description: 'Displays a series of facts as key/value pairs',
props: z.object({
...BaseElement,
facts: z.array(z.object({
title: z.string(),
value: z.string(),
})),
}),
},
// Inputs
'Input.Text': {
description: 'Text input field',
props: z.object({
...BaseElement,
id: z.string(),
isMultiline: z.boolean().optional(),
maxLength: z.number().optional(),
placeholder: z.string().optional(),
label: z.string().optional(),
value: z.string().optional(),
style: z.enum(['text', 'tel', 'url', 'email', 'password']).optional(),
isRequired: z.boolean().optional(),
errorMessage: z.string().optional(),
}),
},
'Input.Number': {
description: 'Number input field',
props: z.object({
...BaseElement,
id: z.string(),
max: z.number().optional(),
min: z.number().optional(),
placeholder: z.string().optional(),
label: z.string().optional(),
value: z.number().optional(),
isRequired: z.boolean().optional(),
errorMessage: z.string().optional(),
}),
},
'Input.Toggle': {
description: 'Toggle/checkbox input',
props: z.object({
...BaseElement,
id: z.string(),
title: z.string(),
label: z.string().optional(),
value: z.string().optional(),
valueOff: z.string().optional(),
valueOn: z.string().optional(),
isRequired: z.boolean().optional(),
}),
},
'Input.ChoiceSet': {
description: 'Dropdown or radio/checkbox group',
props: z.object({
...BaseElement,
id: z.string(),
choices: z.array(z.object({
title: z.string(),
value: z.string(),
})),
isMultiSelect: z.boolean().optional(),
style: z.enum(['compact', 'expanded']).optional(),
label: z.string().optional(),
value: z.string().optional(),
placeholder: z.string().optional(),
isRequired: z.boolean().optional(),
}),
},
// Actions
'Action.OpenUrl': {
description: 'Opens a URL',
props: z.object({
title: z.string().optional(),
url: z.string(),
iconUrl: z.string().optional(),
}),
},
'Action.Submit': {
description: 'Submits input data',
props: z.object({
title: z.string().optional(),
data: z.unknown().optional(),
iconUrl: z.string().optional(),
}),
},
'Action.ShowCard': {
description: 'Shows a card inline',
props: z.object({
title: z.string().optional(),
card: z.unknown(),
iconUrl: z.string().optional(),
}),
},
'Action.Execute': {
description: 'Universal action for bots',
props: z.object({
title: z.string().optional(),
verb: z.string().optional(),
data: z.unknown().optional(),
iconUrl: z.string().optional(),
}),
},
},
});
```
## Building an Adaptive Cards Renderer
Create a renderer that processes Adaptive Cards JSON. See the [A2UI integration](/docs/a2ui) page for a similar pattern. The key is mapping each Adaptive Card element type to a React component, resolving nested `items` and `columns` arrays recursively.
## Usage Example
Render an Adaptive Card and handle actions:
```tsx
'use client';
import { AdaptiveCardRenderer } from './adaptive-card-renderer';
const card = {
type: 'AdaptiveCard' as const,
version: '1.5',
body: [
{
type: 'TextBlock',
text: 'Contact Form',
size: 'large',
weight: 'bolder',
},
{
type: 'Input.Text',
id: 'name',
label: 'Your Name',
placeholder: 'Enter your name',
},
{
type: 'Input.Text',
id: 'message',
label: 'Message',
placeholder: 'Enter your message',
isMultiline: true,
},
],
actions: [
{
type: 'Action.Submit',
title: 'Send',
data: { action: 'submitForm' },
},
],
};
export function ContactCard() {
const handleAction = (action: any, inputData: Record<string, unknown>) => {
console.log('Action:', action);
console.log('Input data:', inputData);
// Send to your backend
fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, data: inputData }),
});
};
return <AdaptiveCardRenderer card={card} onAction={handleAction} />;
}
```
## Handling Action.Execute for Bots
For bot scenarios, handle `Action.Execute` with the verb and data:
```typescript
interface ActionExecutePayload {
action: {
type: 'Action.Execute';
verb: string;
data?: unknown;
};
inputs: Record<string, unknown>;
}
async function handleBotAction(payload: ActionExecutePayload) {
const response = await fetch('/api/bot/action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
verb: payload.action.verb,
data: payload.action.data,
inputs: payload.inputs,
}),
});
// Bot may return a new card to render
const result = await response.json();
if (result.card) {
return result.card; // New AdaptiveCard to render
}
}
```
## Next
Learn about [A2UI integration](/docs/a2ui) for another agent-driven UI protocol.
+385
View File
@@ -0,0 +1,385 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/ag-ui")
# AG-UI Integration
Use json-render to support [AG-UI](https://docs.copilotkit.ai/ag-ui) (Agent User Interaction Protocol) from CopilotKit.
<div className="rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 mb-8">
<p className="text-sm text-amber-700 dark:text-amber-300">
<strong>Concept:</strong> This page demonstrates how json-render can support AG-UI. The examples are illustrative and may require adaptation for production use.
</p>
</div>
## What is AG-UI?
AG-UI is an open protocol for connecting AI agents to user interfaces. It provides a standardized way for agents to render UI components, handle user input, and manage state. The protocol uses events streamed over HTTP to update the UI in real-time.
## AG-UI Event Types
AG-UI defines several event types for agent-UI communication:
- `TEXT_MESSAGE_START` / `TEXT_MESSAGE_CONTENT` / `TEXT_MESSAGE_END` — Streaming text messages
- `TOOL_CALL_START` / `TOOL_CALL_ARGS` / `TOOL_CALL_END` — Tool/function calls
- `STATE_SNAPSHOT` / `STATE_DELTA` — State updates
- `CUSTOM` — Custom events for UI rendering
### Example AG-UI Event Stream
```json
{"type": "RUN_STARTED", "threadId": "thread-123", "runId": "run-456"}
{"type": "TEXT_MESSAGE_START", "messageId": "msg-1", "role": "assistant"}
{"type": "TEXT_MESSAGE_CONTENT", "messageId": "msg-1", "delta": "Here's a dashboard for you:"}
{"type": "TEXT_MESSAGE_END", "messageId": "msg-1"}
{"type": "TOOL_CALL_START", "toolCallId": "tc-1", "toolCallName": "render_ui"}
{"type": "TOOL_CALL_ARGS", "toolCallId": "tc-1", "delta": "{\"component\": \"Dashboard\", \"props\": {\"title\": \"Sales\"}}"}
{"type": "TOOL_CALL_END", "toolCallId": "tc-1"}
{"type": "RUN_FINISHED"}
```
## Define the AG-UI Schema
Define schemas for AG-UI event types:
```typescript
import { z } from 'zod';
// Base event schema
const BaseEvent = z.object({
type: z.string(),
timestamp: z.number().optional(),
});
// Text message events
const TextMessageStart = BaseEvent.extend({
type: z.literal('TEXT_MESSAGE_START'),
messageId: z.string(),
role: z.enum(['user', 'assistant']),
});
const TextMessageContent = BaseEvent.extend({
type: z.literal('TEXT_MESSAGE_CONTENT'),
messageId: z.string(),
delta: z.string(),
});
const TextMessageEnd = BaseEvent.extend({
type: z.literal('TEXT_MESSAGE_END'),
messageId: z.string(),
});
// Tool call events
const ToolCallStart = BaseEvent.extend({
type: z.literal('TOOL_CALL_START'),
toolCallId: z.string(),
toolCallName: z.string(),
parentMessageId: z.string().optional(),
});
const ToolCallArgs = BaseEvent.extend({
type: z.literal('TOOL_CALL_ARGS'),
toolCallId: z.string(),
delta: z.string(),
});
const ToolCallEnd = BaseEvent.extend({
type: z.literal('TOOL_CALL_END'),
toolCallId: z.string(),
});
// State events
const StateSnapshot = BaseEvent.extend({
type: z.literal('STATE_SNAPSHOT'),
snapshot: z.record(z.unknown()),
});
const StateDelta = BaseEvent.extend({
type: z.literal('STATE_DELTA'),
delta: z.array(z.object({
op: z.enum(['add', 'remove', 'replace']),
path: z.string(),
value: z.unknown().optional(),
})),
});
// Custom event for UI components
const CustomEvent = BaseEvent.extend({
type: z.literal('CUSTOM'),
name: z.string(),
value: z.unknown(),
});
// Run lifecycle events
const RunStarted = BaseEvent.extend({
type: z.literal('RUN_STARTED'),
threadId: z.string(),
runId: z.string(),
});
const RunFinished = BaseEvent.extend({
type: z.literal('RUN_FINISHED'),
});
const RunError = BaseEvent.extend({
type: z.literal('RUN_ERROR'),
message: z.string(),
code: z.string().optional(),
});
// Union of all events
export const AGUIEvent = z.discriminatedUnion('type', [
TextMessageStart,
TextMessageContent,
TextMessageEnd,
ToolCallStart,
ToolCallArgs,
ToolCallEnd,
StateSnapshot,
StateDelta,
CustomEvent,
RunStarted,
RunFinished,
RunError,
]);
export type AGUIEvent = z.infer<typeof AGUIEvent>;
```
## Define the AG-UI Catalog
Create a catalog for UI components that agents can render:
```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
import { z } from 'zod';
export const aguiCatalog = defineCatalog(schema, {
components: {
Container: {
description: 'A container for grouping elements',
props: z.object({
direction: z.enum(['row', 'column']).optional(),
gap: z.enum(['none', 'sm', 'md', 'lg']).optional(),
padding: z.enum(['none', 'sm', 'md', 'lg']).optional(),
}),
},
Card: {
description: 'A card with optional title',
props: z.object({
title: z.string().optional(),
description: z.string().optional(),
}),
},
Text: {
description: 'Text content',
props: z.object({
content: z.string(),
variant: z.enum(['body', 'heading', 'caption', 'code']).optional(),
}),
},
Metric: {
description: 'Displays a metric value',
props: z.object({
label: z.string(),
value: z.union([z.string(), z.number()]),
change: z.number().optional(),
format: z.enum(['number', 'currency', 'percent']).optional(),
}),
},
Button: {
description: 'Interactive button',
props: z.object({
label: z.string(),
variant: z.enum(['primary', 'secondary', 'outline', 'ghost']).optional(),
disabled: z.boolean().optional(),
}),
},
Alert: {
description: 'Alert message',
props: z.object({
message: z.string(),
type: z.enum(['info', 'success', 'warning', 'error']).optional(),
}),
},
// Add more components...
},
actions: {
submit: {
description: 'Submit form data',
params: z.object({ formId: z.string() }),
},
navigate: {
description: 'Navigate to a URL',
params: z.object({ url: z.string() }),
},
callback: {
description: 'Trigger a callback to the agent',
params: z.object({
name: z.string(),
data: z.record(z.unknown()).optional(),
}),
},
},
});
```
## Build an AG-UI Event Processor
Process AG-UI events and render UI components:
```tsx
'use client';
import React, { useState, useCallback } from 'react';
import { AGUIEvent } from './schema';
interface AGUIState {
messages: Array<{
id: string;
role: 'user' | 'assistant';
content: string;
}>;
toolCalls: Map<string, {
name: string;
args: string;
result?: unknown;
}>;
state: Record<string, unknown>;
isRunning: boolean;
}
export function useAGUI() {
const [aguiState, setAGUIState] = useState<AGUIState>({
messages: [],
toolCalls: new Map(),
state: {},
isRunning: false,
});
const processEvent = useCallback((event: AGUIEvent) => {
switch (event.type) {
case 'RUN_STARTED':
setAGUIState(prev => ({ ...prev, isRunning: true }));
break;
case 'RUN_FINISHED':
setAGUIState(prev => ({ ...prev, isRunning: false }));
break;
case 'TEXT_MESSAGE_START':
setAGUIState(prev => ({
...prev,
messages: [...prev.messages, {
id: event.messageId,
role: event.role,
content: '',
}],
}));
break;
case 'TEXT_MESSAGE_CONTENT':
setAGUIState(prev => ({
...prev,
messages: prev.messages.map(msg =>
msg.id === event.messageId
? { ...msg, content: msg.content + event.delta }
: msg
),
}));
break;
case 'TOOL_CALL_START':
setAGUIState(prev => {
const toolCalls = new Map(prev.toolCalls);
toolCalls.set(event.toolCallId, { name: event.toolCallName, args: '' });
return { ...prev, toolCalls };
});
break;
case 'TOOL_CALL_ARGS':
setAGUIState(prev => {
const toolCalls = new Map(prev.toolCalls);
const tc = toolCalls.get(event.toolCallId);
if (tc) {
toolCalls.set(event.toolCallId, { ...tc, args: tc.args + event.delta });
}
return { ...prev, toolCalls };
});
break;
case 'STATE_SNAPSHOT':
setAGUIState(prev => ({ ...prev, state: event.snapshot }));
break;
}
}, []);
return { state: aguiState, processEvent };
}
```
## Usage Example
```tsx
'use client';
import { useAGUI } from './use-agui';
import { renderToolCallUI } from './renderer';
export function AGUIChat() {
const { state, processEvent } = useAGUI();
async function startRun(prompt: string) {
const response = await fetch('/api/agent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const lines = decoder.decode(value).split('\n').filter(Boolean);
for (const line of lines) {
const event = JSON.parse(line);
processEvent(event);
}
}
}
return (
<div className="space-y-4">
{state.messages.map(msg => (
<div key={msg.id} className={`p-3 rounded ${
msg.role === 'assistant' ? 'bg-muted' : 'bg-primary/10'
}`}>
{msg.content}
</div>
))}
{Array.from(state.toolCalls.values()).map((tc, i) => (
<div key={i}>{renderToolCallUI(tc)}</div>
))}
<form onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.querySelector('input');
if (input?.value) {
startRun(input.value);
input.value = '';
}
}}>
<input
type="text"
placeholder="Ask the agent..."
className="w-full px-4 py-2 border rounded"
disabled={state.isRunning}
/>
</form>
</div>
);
}
```
## Next
Learn about [OpenAPI integration](/docs/openapi) for rendering forms from API schemas.
+239
View File
@@ -0,0 +1,239 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/ai-sdk")
# AI SDK Integration
Use json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless streaming. json-render supports two modes: **Standalone** (standalone UI) and **Inline** (UI embedded in conversation). See [Generation Modes](/docs/generation-modes) for a detailed comparison.
## Installation
```bash
npm install ai @ai-sdk/react
```
## Standalone Mode
In standalone mode, the AI outputs only JSONL patches. The entire response is a UI spec with no prose. This is the default mode and is ideal for playgrounds, builders, and dashboard generators.
### API Route
```typescript
// app/api/generate/route.ts
import { streamText } from "ai";
import { catalog } from "@/lib/catalog";
export async function POST(req: Request) {
const { prompt, currentTree } = await req.json();
const systemPrompt = catalog.prompt();
// Optionally include current UI state for context
const contextPrompt = currentTree
? `\n\nCurrent UI state:\n${JSON.stringify(currentTree, null, 2)}`
: "";
const result = streamText({
model: yourModel,
system: systemPrompt + contextPrompt,
prompt,
});
return result.toTextStreamResponse();
}
```
### Client
Use `useUIStream` on the client to compile the JSONL stream into a spec:
```tsx
"use client";
import { useUIStream, Renderer } from "@json-render/react";
function GenerativeUI() {
const { spec, isStreaming, error, send } = useUIStream({
api: "/api/generate",
});
return (
<div>
<button
onClick={() => send("Create a dashboard with metrics")}
disabled={isStreaming}
>
{isStreaming ? "Generating..." : "Generate"}
</button>
{error && <p className="text-red-500">{error.message}</p>}
<Renderer spec={spec} registry={registry} loading={isStreaming} />
</div>
);
}
```
## Inline Mode
In inline mode, the AI responds conversationally and includes JSONL patches inline. Text-only replies are allowed when no UI is needed. This is ideal for chatbots, copilots, and educational assistants.
### API Route
Use `pipeJsonRender` to separate text from JSONL patches in the stream. Patches are emitted as data parts that the client can pick up.
```typescript
// app/api/chat/route.ts
import { streamText } from "ai";
import { pipeJsonRender } from "@json-render/core";
import {
createUIMessageStream,
createUIMessageStreamResponse,
} from "ai";
import { catalog } from "@/lib/catalog";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: yourModel,
system: catalog.prompt({ mode: "inline" }),
messages,
});
const stream = createUIMessageStream({
execute: async ({ writer }) => {
writer.merge(pipeJsonRender(result.toUIMessageStream()));
},
});
return createUIMessageStreamResponse({ stream });
}
```
### Client
Use `useChat` from the AI SDK and `useJsonRenderMessage` from json-render to extract the spec from each message:
```tsx
"use client";
import { useChat } from "@ai-sdk/react";
import { useJsonRenderMessage, Renderer } from "@json-render/react";
function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: "/api/chat",
});
return (
<div>
<div>
{messages.map((msg) => (
<ChatMessage key={msg.id} message={msg} />
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="Ask something..."
/>
<button type="submit">Send</button>
</form>
</div>
);
}
function ChatMessage({ message }: { message: { parts: Array<{ type: string; text?: string; data?: unknown }> } }) {
const { spec, text, hasSpec } = useJsonRenderMessage(message.parts);
return (
<div>
{text && <p>{text}</p>}
{hasSpec && spec && (
<Renderer spec={spec} registry={registry} />
)}
</div>
);
}
```
## Prompt Engineering
The `catalog.prompt()` method creates an optimized system prompt that:
- Lists all available components and their props
- Describes available actions
- Specifies the expected output format (JSONL-only or text + JSONL depending on mode)
- Includes examples for better generation
### Custom Rules
Pass custom rules to tailor AI behavior:
```typescript
const systemPrompt = catalog.prompt({
customRules: [
"Always use Card components for grouping related content",
"Prefer horizontal layouts (Row) for metrics",
"Use consistent spacing with padding=\"md\"",
],
});
```
### Inline Mode Prompt
```typescript
const inlinePrompt = catalog.prompt({ mode: "inline" });
```
In inline mode, the prompt instructs the AI to respond conversationally first, then include JSONL patches on their own lines when UI is needed. Text-only replies are allowed.
## Which Mode?
<div className="my-6 overflow-x-auto">
<table className="mdx-table w-full text-sm border-collapse">
<thead>
<tr>
<th></th>
<th>Standalone</th>
<th>Inline</th>
</tr>
</thead>
<tbody>
<tr>
<td>Output</td>
<td>JSONL only</td>
<td>Text + JSONL</td>
</tr>
<tr>
<td>Text-only replies</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>System prompt</td>
<td><code>catalog.prompt()</code></td>
<td><code>{"catalog.prompt({ mode: \"inline\" })"}</code></td>
</tr>
<tr>
<td>Stream utility</td>
<td><code>useUIStream</code></td>
<td><code>pipeJsonRender</code> + <code>useJsonRenderMessage</code></td>
</tr>
<tr>
<td>Use case</td>
<td>Playgrounds, builders</td>
<td>Chatbots, copilots</td>
</tr>
</tbody>
</table>
</div>
Learn more in the [Generation Modes](/docs/generation-modes) guide.
## Next
- Learn about [progressive streaming](/docs/streaming)
- See the [chat example](https://github.com/vercel-labs/json-render/tree/main/examples/chat) for a complete implementation
@@ -0,0 +1,142 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/codegen")
# @json-render/codegen
Utilities for generating code from UI trees.
## Tree Traversal
### traverseSpec
Walk the UI spec depth-first.
```typescript
function traverseSpec(
spec: Spec,
visitor: TreeVisitor,
startKey?: string
): void
interface TreeVisitor {
(element: UIElement, key: string, depth: number, parent: UIElement | null): void;
}
```
### collectUsedComponents
Get all unique component types used in a spec.
```typescript
function collectUsedComponents(spec: Spec): Set<string>
// Example
const components = collectUsedComponents(spec);
// Set { 'Card', 'Metric', 'Chart' }
```
### collectStatePaths
Get all state paths referenced in props (statePath, bindPath, etc.).
```typescript
function collectStatePaths(spec: Spec): Set<string>
// Example
const paths = collectStatePaths(spec);
// Set { 'analytics/revenue', 'analytics/customers' }
```
### collectActions
Get all action names used in the spec.
```typescript
function collectActions(spec: Spec): Set<string>
// Example
const actions = collectActions(spec);
// Set { 'submit_form', 'refresh_data' }
```
## Serialization
### serializePropValue
Serialize a single value to a code string.
```typescript
function serializePropValue(
value: unknown,
options?: SerializeOptions
): { value: string; needsBraces: boolean }
// Examples
serializePropValue("hello")
// { value: '"hello"', needsBraces: false }
serializePropValue(42)
// { value: '42', needsBraces: true }
serializePropValue({ $state: '/user/name' })
// { value: '{ $state: "/user/name" }', needsBraces: true }
```
### serializeProps
Serialize a props object to a JSX attributes string.
```typescript
function serializeProps(
props: Record<string, unknown>,
options?: SerializeOptions
): string
// Example
serializeProps({ title: 'Dashboard', columns: 3, disabled: true })
// 'title="Dashboard" columns={3} disabled'
```
### escapeString
Escape a string for use in code.
```typescript
function escapeString(
str: string,
quotes?: 'single' | 'double'
): string
```
## Types
### GeneratedFile
```typescript
interface GeneratedFile {
/** File path relative to project root */
path: string;
/** File contents */
content: string;
}
```
### CodeGenerator
```typescript
interface CodeGenerator {
/** Generate files from a UI spec */
generate(spec: Spec): GeneratedFile[];
}
```
### SerializeOptions
```typescript
interface SerializeOptions {
/** Quote style for strings */
quotes?: 'single' | 'double';
/** Indent for objects/arrays */
indent?: number;
}
```
+701
View File
@@ -0,0 +1,701 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/core")
# @json-render/core
Core types, schemas, and utilities.
## defineCatalog
Creates a type-safe catalog definition with schema validation.
```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
function defineCatalog<T extends ZodType>(
s: T,
config: CatalogConfig
): Catalog
// Use the React schema for standard UI specs
const catalog = defineCatalog(schema, {
components: {...},
actions: {...},
});
```
### CatalogConfig
```typescript
interface CatalogConfig {
components: Record<string, ComponentDefinition>;
actions?: Record<string, ActionDefinition>;
functions?: Record<string, FunctionDefinition>;
}
interface ComponentDefinition {
props: ZodObject; // Use .nullable() for optional props
slots?: string[]; // Named slots (e.g., ["default"])
description?: string; // Help AI understand usage
}
interface ActionDefinition {
params?: ZodObject;
description?: string;
}
interface FunctionDefinition {
description?: string;
}
```
### Catalog Instance
The returned catalog provides methods for AI prompt generation, validation, and schema export:
```typescript
interface Catalog {
// Data
readonly data: CatalogConfig; // The catalog configuration
readonly componentNames: string[]; // List of component names
readonly actionNames: string[]; // List of action names
// AI Prompt Generation
prompt(options?: PromptOptions): string;
// Validation
validate(spec: unknown): SpecValidationResult;
zodSchema(): z.ZodType; // Get the Zod schema for specs
// Export
jsonSchema(): object; // Export as JSON Schema
}
interface PromptOptions {
system?: string; // Custom system message intro
customRules?: string[]; // Additional rules to append
mode?: "standalone" | "inline" | "generate" | "chat"; // Output mode (default: "standalone")
editModes?: EditMode[]; // Edit modes to document in prompt (default: ["patch"])
}
interface SpecValidationResult<T> {
success: boolean;
data?: T; // Validated spec (if success)
error?: z.ZodError; // Validation errors (if failed)
}
```
### Catalog Methods
```typescript
// Generate AI system prompt
const systemPrompt = catalog.prompt({
customRules: ["Always use Card as root element"],
});
// Validate a spec from AI
const result = catalog.validate(aiOutput);
if (result.success) {
render(result.data);
} else {
console.error(result.error);
}
// Get Zod schema for custom validation
const schema = catalog.zodSchema();
const parsed = schema.safeParse(aiOutput);
// Export as JSON Schema (for structured outputs)
const jsonSchema = catalog.jsonSchema();
```
## Schema System
json-render uses a flexible schema system that defines both the AI output format (spec) and what catalogs must provide. Each renderer package provides its own schema (e.g., @json-render/react exports `schema`).
### schema
The schema for flat UI element trees. This is exported from @json-render/react.
```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
// schema defines:
// - Spec shape: { root: string, elements: Record<string, UIElement> }
// - Catalog shape: { components: {...}, actions: {...} }
const catalog = defineCatalog(schema, {
components: {
Card: {
props: z.object({ title: z.string() }),
slots: ["default"],
description: "Container card",
},
},
actions: {
submit: {
params: z.object({ formId: z.string() }),
description: "Submit a form",
},
},
});
```
### SchemaOptions
When creating schemas with `defineSchema`, you can pass options:
```typescript
interface SchemaOptions {
promptTemplate?: PromptTemplate; // Custom AI prompt generator
defaultRules?: string[]; // Default rules injected before custom rules in prompts
builtInActions?: BuiltInAction[]; // Actions always available at runtime, auto-injected into prompts
}
interface BuiltInAction {
name: string; // Action name (e.g. "setState")
description: string; // Human-readable description for the LLM
}
```
Built-in actions are injected into prompts as `[built-in]` and are handled by the runtime (e.g. `ActionProvider`) without requiring handlers in `defineRegistry`. The React schema declares `setState`, `pushState`, and `removeState` as built-in.
### defineSchema
Create custom schemas for different output formats (e.g., page-based, block-based).
```typescript
import { defineSchema } from '@json-render/core';
const mySchema = defineSchema((s) => ({
// What the AI outputs (spec)
spec: s.object({
title: s.string(),
blocks: s.array(s.object({
type: s.ref("catalog.blocks"),
content: s.any(),
})),
}),
// What the catalog must provide
catalog: s.object({
blocks: s.map({
props: s.zod(),
description: s.string(),
}),
}),
}));
```
### Schema Builder API
The schema builder provides these methods:
```typescript
// Primitive types
s.string() // String value
s.number() // Number value
s.boolean() // Boolean value
s.any() // Any value
// Compound types
s.array(item) // Array of items
s.object({ ... }) // Object with shape
s.record(value) // Record/map with value type
// Catalog references (for type safety)
s.ref("catalog.components") // Reference to catalog key (becomes enum)
s.propsOf("catalog.components") // Props schema from catalog entry
// Catalog definitions
s.map({ props: s.zod(), ... }) // Map of named entries with shared shape
s.zod() // Placeholder for user-provided Zod schema
// Modifiers
s.optional() // Mark field as optional
```
## Zod Schemas
Pre-built Zod schemas for common json-render types:
### Dynamic Value Schemas
```typescript
import {
DynamicValueSchema, // string | number | boolean | null | { $state: string }
DynamicStringSchema, // string | { $state: string }
DynamicNumberSchema, // number | { $state: string }
DynamicBooleanSchema, // boolean | { $state: string }
} from '@json-render/core';
// Dynamic values can be literals or state path references
type DynamicValue<T> = T | { $state: string };
// Example: a prop that can be a literal or bound to state
const schema = z.object({
label: DynamicStringSchema, // "Hello" or { $state: "/user/name" }
});
```
### Visibility Schemas
```typescript
import { VisibilityConditionSchema } from '@json-render/core';
// Use in component props that need conditional rendering
const schema = z.object({
visible: VisibilityConditionSchema.optional(),
});
```
### Action Schemas
```typescript
import {
ActionSchema, // Full action definition
ActionConfirmSchema, // Confirmation dialog config
ActionOnSuccessSchema, // Success handler config
ActionOnErrorSchema, // Error handler config
} from '@json-render/core';
```
### Validation Schemas
```typescript
import {
ValidationCheckSchema, // Single validation check
ValidationConfigSchema, // Full validation config with checks array
} from '@json-render/core';
```
## SpecStream
SpecStream is json-render's streaming format for progressively building specs from JSONL patches.
### createSpecStreamCompiler
Create a streaming compiler that incrementally builds a spec:
```typescript
import { createSpecStreamCompiler } from '@json-render/core';
const compiler = createSpecStreamCompiler<MySpec>();
// Process streaming chunks
const { result, newPatches } = compiler.push(chunk);
// Get final result
const spec = compiler.getResult();
// Reset for reuse
compiler.reset();
```
### compileSpecStream
Compile an entire SpecStream string at once:
```typescript
import { compileSpecStream } from '@json-render/core';
const jsonl = `{"op":"add","path":"/root","value":{}}
{"op":"add","path":"/root/type","value":"Card"}`;
const spec = compileSpecStream<MySpec>(jsonl);
```
### Low-Level Utilities
```typescript
import {
parseSpecStreamLine,
applySpecStreamPatch,
} from '@json-render/core';
// Parse a single line
const patch = parseSpecStreamLine('{"op":"add","path":"/root","value":{}}');
// Apply patch to object (mutates in place)
const obj = {};
applySpecStreamPatch(obj, patch);
```
### applySpecPatch
Apply a single SpecStream patch to a Spec object (mutates in place, returns the spec):
```typescript
import { applySpecPatch } from '@json-render/core';
let spec: Spec = { root: "", elements: {} };
applySpecPatch(spec, { op: "add", path: "/root", value: "main" });
// For React state updates, spread to create a new reference:
setSpec({ ...applySpecPatch(spec, patch) });
```
### nestedToFlat
Convert a nested element tree (with inline children) into the flat `Spec` format:
```typescript
import { nestedToFlat } from '@json-render/core';
const flat = nestedToFlat({
type: "Card",
props: { title: "Hello" },
children: [
{ type: "Text", props: { content: "World" }, children: [] }
],
});
// { root: "el-0", elements: { "el-0": ..., "el-1": ... } }
```
### createJsonRenderTransform
Low-level `TransformStream` that separates text from JSONL patches in a mixed AI stream. Lines that parse as JSONL patches are emitted as `data-spec` parts; everything else passes through as text.
The transform properly splits text blocks around spec data by emitting `text-end`/`text-start` pairs, ensuring the AI SDK creates separate text parts and preserving correct interleaving of prose and UI in `message.parts`.
```typescript
import { createJsonRenderTransform } from '@json-render/core';
const transform = createJsonRenderTransform();
// Use with ReadableStream.pipeThrough(transform) for custom pipelines
```
Most users should use `pipeJsonRender()` instead, which wraps this transform for the common AI SDK use case.
### createMixedStreamParser
Parse a mixed stream of text and JSONL patches (used for Inline mode):
```typescript
import { createMixedStreamParser } from '@json-render/core';
const parser = createMixedStreamParser({
onText: (text) => appendToMessage(text),
onPatch: (patch) => applySpecPatch(spec, patch),
});
// As chunks arrive from the stream:
for await (const chunk of stream) {
parser.push(chunk);
}
parser.flush();
```
### pipeJsonRender
Pipe an AI SDK `UIMessageStream` through the json-render transform. Lines that parse as JSONL patches are emitted as `data-spec` parts; everything else passes through as text. Used in Inline mode API routes.
```typescript
import { pipeJsonRender } from '@json-render/core';
import { createUIMessageStream, createUIMessageStreamResponse } from 'ai';
const stream = createUIMessageStream({
execute: async ({ writer }) => {
writer.merge(pipeJsonRender(result.toUIMessageStream()));
},
});
return createUIMessageStreamResponse({ stream });
```
See [Generation Modes](/docs/generation-modes) for full Inline mode setup.
### SpecStream Types
Fully compliant with [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902):
```typescript
interface SpecStreamLine {
op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test';
path: string;
value?: unknown; // Required for add, replace, test
from?: string; // Required for move, copy
}
interface SpecStreamCompiler<T> {
push(chunk: string): { result: T; newPatches: SpecStreamLine[] };
getResult(): T;
getPatches(): SpecStreamLine[];
reset(): void;
}
interface MixedStreamCallbacks {
onText: (text: string) => void;
onPatch: (patch: SpecStreamLine) => void;
}
interface MixedStreamParser {
push(chunk: string): void;
flush(): void;
}
```
## Utility Functions
### Path Utilities
```typescript
import { getByPath, setByPath } from '@json-render/core';
// Get value by JSON Pointer path
const value = getByPath(state, '/user/name'); // "Alice"
// Set value by path (mutates object)
setByPath(state, '/user/email', 'alice@example.com');
```
### resolveDynamicValue
```typescript
import { resolveDynamicValue } from '@json-render/core';
// Resolve a dynamic value against state
const name = resolveDynamicValue("Hello", state); // "Hello"
const name2 = resolveDynamicValue({ $state: "/user/name" }, state); // "Alice"
```
### findFormValue
```typescript
import { findFormValue } from '@json-render/core';
// Find form values regardless of path format
// Checks: params.name, params["form.name"], state["form.name"], state.form.name
const value = findFormValue("name", params, state);
```
## buildUserPrompt
Build structured user prompts for AI generation, with support for refinement and state context.
```typescript
import { buildUserPrompt } from '@json-render/core';
function buildUserPrompt(options: UserPromptOptions): string
interface UserPromptOptions {
prompt: string; // The user's text prompt
currentSpec?: Spec | null; // Existing spec to refine (triggers edit mode)
state?: Record<string, unknown> | null; // Runtime state context to include
maxPromptLength?: number; // Max length for user text (truncates before wrapping)
editModes?: EditMode[]; // Edit modes for refinement (default: ["patch"])
}
```
### Fresh generation
```typescript
const userPrompt = buildUserPrompt({ prompt: "create a todo app" });
```
### Refinement (edit modes)
When `currentSpec` is provided, the prompt instructs the AI to use the specified edit modes instead of recreating the entire spec. Available modes: `"patch"` (RFC 6902), `"merge"` (RFC 7396), and `"diff"` (unified diff).
```typescript
const userPrompt = buildUserPrompt({
prompt: "add a dark mode toggle",
currentSpec: existingSpec,
editModes: ["patch", "merge"],
});
```
### With state context
Include runtime state so the AI knows what data is available:
```typescript
const userPrompt = buildUserPrompt({
prompt: "show my data",
state: { todos: [{ text: "Buy milk" }] },
});
```
## Edit Modes
Universal edit mode utilities for modifying existing specs. Used by `buildUserPrompt` internally and available for direct use.
```typescript
import {
buildEditInstructions,
buildEditUserPrompt,
isNonEmptySpec,
type EditMode,
type EditConfig,
} from '@json-render/core';
type EditMode = "patch" | "merge" | "diff";
```
### buildEditInstructions
Generate the prompt section describing available edit modes. Supports both JSON and YAML formats.
```typescript
function buildEditInstructions(config: EditConfig, format: "json" | "yaml"): string
const instructions = buildEditInstructions({ modes: ["patch", "merge"] }, "json");
```
### buildEditUserPrompt
Build a user prompt for editing an existing spec. Includes the current spec (with line numbers when diff mode is enabled) and mode-specific instructions.
```typescript
function buildEditUserPrompt(options: BuildEditUserPromptOptions): string
interface BuildEditUserPromptOptions {
prompt: string;
currentSpec?: Spec | null;
config?: EditConfig;
format: "json" | "yaml";
maxPromptLength?: number;
serializer?: (spec: Spec) => string;
}
```
### isNonEmptySpec
Check whether a value is a non-empty spec (has a root string and at least one element).
```typescript
function isNonEmptySpec(spec: unknown): spec is Spec
```
## Deep Merge and Diff
Format-agnostic utilities for merging and diffing spec objects.
### deepMergeSpec
Deep-merge with RFC 7396 semantics: `null` deletes, arrays replace, objects recurse. Neither input is mutated.
```typescript
import { deepMergeSpec } from '@json-render/core';
function deepMergeSpec(
base: Record<string, unknown>,
patch: Record<string, unknown>
): Record<string, unknown>
const merged = deepMergeSpec(currentSpec, { elements: { main: { props: { title: "New" } } } });
```
### diffToPatches
Generate RFC 6902 JSON Patch operations that transform one object into another. Arrays are compared shallowly and replaced atomically; plain objects recurse.
```typescript
import { diffToPatches } from '@json-render/core';
function diffToPatches(
oldObj: Record<string, unknown>,
newObj: Record<string, unknown>,
basePath?: string
): JsonPatch[]
const patches = diffToPatches(oldSpec, newSpec);
// [{ op: "replace", path: "/elements/main/props/title", value: "New Title" }]
```
## evaluateVisibility
Evaluates a visibility condition against the state model.
```typescript
function evaluateVisibility(
condition: VisibilityCondition | undefined,
ctx: VisibilityContext
): boolean
interface VisibilityContext {
stateModel: StateModel;
repeatItem?: unknown; // Current repeat item (inside repeat scope)
repeatIndex?: number; // Current repeat array index (inside repeat scope)
}
type VisibilityCondition =
| { $state: string } // truthiness
| { $state: string; not: true } // falsy
| { $state: string; eq: unknown } // equality
| { $state: string; neq: unknown } // inequality
| { $state: string; gt: number } // greater than
| { $state: string; gte: number } // gte
| { $state: string; lt: number } // lt
| { $state: string; lte: number } // lte
| { $item: string } // item field (repeat scope)
| { $item: string; eq: unknown } // item field equality
| { $index: true } // index truthiness (repeat scope)
| { $index: true; gt: number } // index comparison
| VisibilityCondition[] // implicit AND
| { $and: VisibilityCondition[] } // explicit AND
| { $or: VisibilityCondition[] } // OR
| boolean; // always / never
```
## Types
### UIElement
```typescript
interface UIElement {
type: string;
props: Record<string, unknown>;
children?: string[]; // Keys of child elements
visible?: VisibilityCondition;
on?: Record<string, ActionBinding | ActionBinding[]>; // Event bindings
repeat?: { statePath: string; key?: string }; // Repeat for arrays
}
```
Elements are stored in the `elements` map keyed by string IDs. The key comes from the map, not from the element itself.
### Spec (Element Tree)
```typescript
interface Spec {
root: string | null; // Key of root element
elements: Record<string, UIElement>; // Flat element map
state?: Record<string, unknown>; // Initial state model
}
```
Elements are stored as a flat map with string keys. The tree structure is built by following the `children` arrays.
### ActionBinding
```typescript
interface ActionBinding {
action: string;
params?: Record<string, DynamicValue>;
confirm?: {
title: string;
message: string;
variant?: 'default' | 'danger';
};
onSuccess?: { set: Record<string, unknown> };
onError?: { set: Record<string, unknown> };
preventDefault?: boolean; // Prevent default browser behavior (e.g. navigation on links)
}
```
### ValidationSchema
```typescript
interface ValidationSchema {
checks: ValidationCheck[];
validateOn?: 'change' | 'blur' | 'submit';
}
interface ValidationCheck {
type: string;
args?: Record<string, unknown>;
message: string;
}
```
+364
View File
@@ -0,0 +1,364 @@
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>
+104
View File
@@ -0,0 +1,104 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/jotai")
# @json-render/jotai
Jotai adapter for json-render's `StateStore` interface.
## Installation
```bash
npm install @json-render/jotai @json-render/core @json-render/react jotai
```
## jotaiStateStore
Create a `StateStore` backed by a Jotai atom.
```typescript
import { jotaiStateStore } from "@json-render/jotai";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>atom</code></td>
<td><code>{'WritableAtom<StateModel, [StateModel], void>'}</code></td>
<td>Yes</td>
<td>A writable atom holding the state model.</td>
</tr>
<tr>
<td><code>store</code></td>
<td>Jotai <code>Store</code></td>
<td>No</td>
<td>The Jotai store instance. Defaults to a new store created internally. Pass your own to share state with <code>{'<Provider>'}</code>.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { atom } from "jotai";
import { jotaiStateStore } from "@json-render/jotai";
import { StateProvider } from "@json-render/react";
const uiAtom = atom<Record<string, unknown>>({ count: 0 });
const store = jotaiStateStore({ atom: uiAtom });
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through Jotai */}
</StateProvider>
```
### Shared Jotai Store
If your app already uses a Jotai `<Provider>` with a custom store, pass it so both json-render and your components share the same state:
```typescript
import { atom, createStore } from "jotai";
import { Provider as JotaiProvider } from "jotai/react";
import { jotaiStateStore } from "@json-render/jotai";
import { StateProvider } from "@json-render/react";
const jStore = createStore();
const uiAtom = atom<Record<string, unknown>>({ count: 0 });
const store = jotaiStateStore({ atom: uiAtom, store: jStore });
```
```tsx
<JotaiProvider store={jStore}>
<StateProvider store={store}>
{/* Both json-render and useAtom() see the same state */}
</StateProvider>
</JotaiProvider>
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
+247
View File
@@ -0,0 +1,247 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/mcp")
# @json-render/mcp
MCP Apps integration for json-render. Serve json-render UIs as interactive [MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps) inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.
## Install
```bash
npm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk
```
For the iframe-side React UI, also install:
```bash
npm install @json-render/react react react-dom
```
See the [MCP example](https://github.com/vercel-labs/json-render/tree/main/examples/mcp) for a full working example.
## Overview
MCP Apps let MCP servers return interactive HTML UIs that render directly inside chat conversations. `@json-render/mcp` bridges json-render catalogs with the MCP Apps protocol:
1. Your **catalog** defines which components and actions the AI can use
2. The **MCP server** exposes the catalog as a tool with the spec schema
3. The **bundled HTML** renders json-render specs inside the host's sandboxed iframe
4. The AI generates a spec, the host renders it, and users interact with the live UI
## Server API
### createMcpApp
Create a fully-configured MCP server. This is the main entry point.
```typescript
import { createMcpApp } from "@json-render/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import fs from "node:fs";
const server = createMcpApp({
name: "My Dashboard",
version: "1.0.0",
catalog: myCatalog,
html: fs.readFileSync("dist/index.html", "utf-8"),
});
await server.connect(new StdioServerTransport());
```
#### CreateMcpAppOptions
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>name</code></td>
<td><code>string</code></td>
<td>Server name shown in client UIs</td>
</tr>
<tr>
<td><code>version</code></td>
<td><code>string</code></td>
<td>Server version</td>
</tr>
<tr>
<td><code>catalog</code></td>
<td><code>Catalog</code></td>
<td>json-render catalog defining available components</td>
</tr>
<tr>
<td><code>html</code></td>
<td><code>string</code></td>
<td>Self-contained HTML for the iframe UI</td>
</tr>
<tr>
<td><code>tool</code></td>
<td><code>McpToolOptions</code></td>
<td>Optional tool name/title/description overrides</td>
</tr>
</tbody>
</table>
### registerJsonRenderTool
Register a json-render tool on an existing `McpServer`. Use this when you need to add json-render to a server that has other tools.
```typescript
import { registerJsonRenderTool } from "@json-render/mcp";
registerJsonRenderTool(server, {
catalog,
name: "render-ui",
title: "Render UI",
description: "Render an interactive UI",
resourceUri: "ui://render-ui/view.html",
});
```
### registerJsonRenderResource
Register the UI resource that serves the bundled HTML.
```typescript
import { registerJsonRenderResource } from "@json-render/mcp";
registerJsonRenderResource(server, {
resourceUri: "ui://render-ui/view.html",
html: bundledHtml,
});
```
## Client API (`@json-render/mcp/app`)
These exports run inside the sandboxed iframe rendered by the MCP host.
### useJsonRenderApp
React hook that connects to the MCP host, listens for tool results, and maintains the current json-render spec.
```tsx
import { useJsonRenderApp } from "@json-render/mcp/app";
import { JSONUIProvider, Renderer } from "@json-render/react";
function McpAppView({ registry }) {
const { spec, loading, connected, error } = useJsonRenderApp({
name: "my-app",
version: "1.0.0",
});
if (error) return <div>Error: {error.message}</div>;
if (!spec) return <div>Waiting...</div>;
return (
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
<Renderer spec={spec} registry={registry} loading={loading} />
</JSONUIProvider>
);
}
```
#### UseJsonRenderAppReturn
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>spec</code></td>
<td><code>{'Spec | null'}</code></td>
<td>Current json-render spec</td>
</tr>
<tr>
<td><code>loading</code></td>
<td><code>boolean</code></td>
<td>Whether the spec is still being received</td>
</tr>
<tr>
<td><code>connected</code></td>
<td><code>boolean</code></td>
<td>Whether connected to the host</td>
</tr>
<tr>
<td><code>connecting</code></td>
<td><code>boolean</code></td>
<td>Whether currently connecting</td>
</tr>
<tr>
<td><code>error</code></td>
<td><code>{'Error | null'}</code></td>
<td>Connection error, if any</td>
</tr>
<tr>
<td><code>app</code></td>
<td><code>{'App | null'}</code></td>
<td>The underlying MCP App instance</td>
</tr>
<tr>
<td><code>callServerTool</code></td>
<td><code>{'(name, args?) => Promise<void>'}</code></td>
<td>Call an MCP server tool and update spec from result</td>
</tr>
</tbody>
</table>
### buildAppHtml
Generate a self-contained HTML page from bundled JavaScript and CSS.
```typescript
import { buildAppHtml } from "@json-render/mcp/app";
import fs from "node:fs";
const html = buildAppHtml({
title: "Dashboard",
js: fs.readFileSync("dist/app.js", "utf-8"),
css: fs.readFileSync("dist/app.css", "utf-8"),
});
```
## Client Configuration
### Cursor
Add to `.cursor/mcp.json`:
```json
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "path/to/server.ts", "--stdio"]
}
}
}
```
### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/server.ts", "--stdio"]
}
}
}
```
## Supported Clients
MCP Apps are supported by Claude (web and desktop), ChatGPT, VS Code (GitHub Copilot), Cursor, Goose, and Postman.
@@ -0,0 +1,310 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react-email")
# @json-render/react-email
React Email renderer. Turn JSON specs into HTML or plain-text emails using `@react-email/components` and `@react-email/render`.
## Install
```bash
npm install @json-render/core @json-render/react-email @react-email/components @react-email/render
```
See the [React Email example](https://github.com/vercel-labs/json-render/tree/main/examples/react-email) for a full working example.
## schema
The email element schema for specs. Use with `defineCatalog` from core.
```typescript
import { defineCatalog } from '@json-render/core';
import { schema, standardComponentDefinitions } from '@json-render/react-email';
const catalog = defineCatalog(schema, {
components: standardComponentDefinitions,
});
```
## Render Functions
Server-side functions for producing email output. All accept a spec and optional `RenderOptions`.
```typescript
import { renderToHtml, renderToPlainText } from '@json-render/react-email';
const html = await renderToHtml(spec);
const plainText = await renderToPlainText(spec);
```
### RenderOptions
```typescript
interface RenderOptions {
registry?: ComponentRegistry;
includeStandard?: boolean; // default: true
state?: Record<string, unknown>;
}
```
<table>
<thead>
<tr>
<th>Option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>registry</code></td>
<td>Custom component map (merged with standard components)</td>
</tr>
<tr>
<td><code>includeStandard</code></td>
<td>Include built-in standard components (default: <code>true</code>)</td>
</tr>
<tr>
<td><code>state</code></td>
<td>Initial state for <code>$state</code> / <code>$cond</code> dynamic prop resolution</td>
</tr>
</tbody>
</table>
## defineRegistry
Create a type-safe component registry from a catalog. Components receive `{ props, children, emit, bindings, loading }`.
```tsx
import { defineRegistry } from '@json-render/react-email';
import { Container, Heading, Text } from '@react-email/components';
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
<Container style={{ padding: 16, backgroundColor: '#fff' }}>
<Heading>{props.title}</Heading>
{children}
</Container>
),
},
});
const html = await renderToHtml(spec, { registry });
```
## createRenderer
Create a standalone renderer component wired to state, actions, and validation (for interactive previews in the browser).
```typescript
import { createRenderer } from '@json-render/react-email';
const EmailRenderer = createRenderer(catalog, components);
```
## Renderer
The main component that renders a spec to React Email elements. Use inside `JSONUIProvider` when you need state, actions, or visibility.
```typescript
interface RendererProps {
spec: Spec | null;
registry?: ComponentRegistry;
includeStandard?: boolean; // default: true
loading?: boolean;
fallback?: ComponentRenderer;
}
```
## Standard Components
### Document structure
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Html</code></td>
<td>Top-level email wrapper. Must be the root element.</td>
</tr>
<tr>
<td><code>Head</code></td>
<td>Email head section. Place inside Html.</td>
</tr>
<tr>
<td><code>Body</code></td>
<td>Email body wrapper. Place inside Html.</td>
</tr>
</tbody>
</table>
### Layout
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Container</code></td>
<td>Constrains content width (e.g. max-width 600px).</td>
</tr>
<tr>
<td><code>Section</code></td>
<td>Groups related content.</td>
</tr>
<tr>
<td><code>Row</code></td>
<td>Horizontal layout row.</td>
</tr>
<tr>
<td><code>Column</code></td>
<td>Column within a Row.</td>
</tr>
</tbody>
</table>
### Content
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Heading</code></td>
<td>Heading text (h1-h6).</td>
</tr>
<tr>
<td><code>Text</code></td>
<td>Body text paragraph.</td>
</tr>
<tr>
<td><code>Link</code></td>
<td>Hyperlink with text and href.</td>
</tr>
<tr>
<td><code>Button</code></td>
<td>Call-to-action button (link styled as button).</td>
</tr>
<tr>
<td><code>Image</code></td>
<td>Image from URL.</td>
</tr>
<tr>
<td><code>Hr</code></td>
<td>Horizontal rule separator.</td>
</tr>
</tbody>
</table>
### Utility
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Preview</code></td>
<td>Preview text for inbox (inside Html).</td>
</tr>
<tr>
<td><code>Markdown</code></td>
<td>Renders markdown content as email-safe HTML.</td>
</tr>
</tbody>
</table>
## Server-Safe Import
Import schema and catalog definitions without pulling in React or `@react-email/components`:
```typescript
import { schema, standardComponentDefinitions } from '@json-render/react-email/server';
```
## Sub-path Exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>@json-render/react-email</code></td>
<td>Full package: schema, renderer, components, render functions</td>
</tr>
<tr>
<td><code>@json-render/react-email/server</code></td>
<td>Schema and catalog definitions only (no React)</td>
</tr>
<tr>
<td><code>@json-render/react-email/catalog</code></td>
<td>Standard component definitions and types</td>
</tr>
<tr>
<td><code>@json-render/react-email/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>ReactEmailSchema</code></td>
<td>Schema type for email specs</td>
</tr>
<tr>
<td><code>ReactEmailSpec</code></td>
<td>Spec type for email documents</td>
</tr>
<tr>
<td><code>RenderOptions</code></td>
<td>Options for render functions</td>
</tr>
<tr>
<td><code>ComponentContext</code></td>
<td>Typed component render function context</td>
</tr>
<tr>
<td><code>ComponentFn</code></td>
<td>Component render function type</td>
</tr>
<tr>
<td><code>StandardComponentDefinitions</code></td>
<td>Type of the standard component definitions object</td>
</tr>
<tr>
<td><code>StandardComponentProps&lt;K&gt;</code></td>
<td>Inferred props type for a standard component by name</td>
</tr>
</tbody>
</table>
@@ -0,0 +1,232 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react-native")
# @json-render/react-native
React Native renderer with standard components, providers, and hooks.
## Standard Components
### Layout
<table>
<thead>
<tr><th>Component</th><th>Props</th><th>Description</th></tr>
</thead>
<tbody>
<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>
<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>
<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>
<tr><td><code>ScrollContainer</code></td><td><code>direction</code></td><td>Scrollable area (vertical or horizontal)</td></tr>
<tr><td><code>SafeArea</code></td><td><code>edges</code></td><td>Safe area insets for notch/home indicator</td></tr>
<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
<table>
<thead>
<tr><th>Component</th><th>Props</th><th>Description</th></tr>
</thead>
<tbody>
<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>
<tr><td><code>Paragraph</code></td><td><code>text</code>, <code>align</code>, <code>color</code></td><td>Body text</td></tr>
<tr><td><code>Label</code></td><td><code>text</code>, <code>color</code>, <code>bold</code></td><td>Small label text</td></tr>
<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>
<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
<table>
<thead>
<tr><th>Component</th><th>Props</th><th>Description</th></tr>
</thead>
<tbody>
<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>
<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>
<tr><td><code>Switch</code></td><td><code>checked</code> (use <code>$bindState</code>), <code>label</code></td><td>Toggle switch</td></tr>
<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
<table>
<thead>
<tr><th>Component</th><th>Props</th><th>Description</th></tr>
</thead>
<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
<table>
<thead>
<tr><th>Component</th><th>Props</th><th>Description</th></tr>
</thead>
<tbody>
<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
### StateProvider
```tsx
<StateProvider initialState={object} onStateChange={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:
```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
```tsx
<ActionProvider handlers={Record<string, ActionHandler>}>
{children}
</ActionProvider>
```
### VisibilityProvider
```tsx
<VisibilityProvider>
{children}
</VisibilityProvider>
```
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
```tsx
<ValidationProvider>
{children}
</ValidationProvider>
```
## defineRegistry
Create a type-safe component registry. Standard components are built-in; only register custom components.
```tsx
import { defineRegistry, type Components } from '@json-render/react-native';
const { registry } = defineRegistry(catalog, {
components: {
Icon: ({ props }) => <Ionicons name={props.name} size={props.size ?? 24} />,
} as Components<typeof catalog>,
});
```
## Hooks
### useUIStream
```typescript
const {
spec, // Spec | null - current UI state
isStreaming, // boolean - true while streaming
error, // Error | null
send, // (prompt: string) => Promise<void>
clear, // () => void - reset spec and error
} = useUIStream({
api: string,
onComplete?: (spec: Spec) => void,
onError?: (error: Error) => void,
});
```
### useStateStore
```typescript
const { state, get, set, update } = useStateStore();
```
### useStateValue
```typescript
const value = useStateValue(path: string);
```
### useStateBinding (deprecated)
> **Deprecated.** Use `useBoundProp` with `$bindState` expressions instead.
```typescript
const [value, setValue] = useStateBinding(path: string);
```
### useActions
```typescript
const { execute } = useActions();
```
### useIsVisible
```typescript
const isVisible = useIsVisible(condition?: VisibilityCondition);
```
## Catalog Exports
```typescript
import { standardComponentDefinitions, standardActionDefinitions } from "@json-render/react-native/catalog";
import { schema } from "@json-render/react-native/schema";
```
<table>
<thead>
<tr><th>Export</th><th>Purpose</th></tr>
</thead>
<tbody>
<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>
@@ -0,0 +1,439 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react-pdf")
# @json-render/react-pdf
PDF document renderer. Turn JSON specs into PDFs using `@react-pdf/renderer`.
## Install
```bash
npm install @json-render/core @json-render/react-pdf
```
See the [React PDF example](https://github.com/vercel-labs/json-render/tree/main/examples/react-pdf) for a full working example.
## schema
The PDF element schema for document specs. Use with `defineCatalog` from core.
```typescript
import { defineCatalog } from '@json-render/core';
import { schema, standardComponentDefinitions } from '@json-render/react-pdf';
const catalog = defineCatalog(schema, {
components: standardComponentDefinitions,
});
```
## Render Functions
Server-side functions for producing PDF output. All accept a spec and optional `RenderOptions`.
```typescript
import { renderToBuffer, renderToStream, renderToFile } from '@json-render/react-pdf';
const buffer = await renderToBuffer(spec);
const stream = await renderToStream(spec);
stream.pipe(res);
await renderToFile(spec, './output.pdf');
```
### RenderOptions
```typescript
interface RenderOptions {
registry?: ComponentRegistry;
includeStandard?: boolean; // default: true
state?: Record<string, unknown>;
}
```
<table>
<thead>
<tr>
<th>Option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>registry</code></td>
<td>Custom component map (merged with standard components)</td>
</tr>
<tr>
<td><code>includeStandard</code></td>
<td>Include built-in standard components (default: <code>true</code>)</td>
</tr>
<tr>
<td><code>state</code></td>
<td>Initial state for <code>$state</code> / <code>$cond</code> dynamic prop resolution</td>
</tr>
</tbody>
</table>
## defineRegistry
Create a type-safe component registry from a catalog. Components receive `{ props, children, emit, bindings, loading }`.
```tsx
import { defineRegistry } from '@json-render/react-pdf';
import { View, Text } from '@react-pdf/renderer';
const { registry } = defineRegistry(catalog, {
components: {
Badge: ({ props }) => (
<View style={{ backgroundColor: props.color ?? '#e5e7eb', padding: 4, borderRadius: 4 }}>
<Text style={{ fontSize: 10 }}>{props.label}</Text>
</View>
),
},
});
const buffer = await renderToBuffer(spec, { registry });
```
## createRenderer
Create a standalone renderer component wired to state, actions, and validation.
```typescript
import { createRenderer } from '@json-render/react-pdf';
const PDFRenderer = createRenderer(catalog, components);
```
```typescript
interface CreateRendererProps {
spec: Spec | null;
store?: StateStore;
state?: Record<string, unknown>;
onAction?: (actionName: string, params?: Record<string, unknown>) => void;
onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
loading?: boolean;
fallback?: ComponentRenderer;
}
```
When `store` is provided, `state` and `onStateChange` are ignored (controlled mode).
## Renderer
The main component that renders a spec to `@react-pdf/renderer` elements.
```typescript
interface RendererProps {
spec: Spec | null;
registry?: ComponentRegistry;
includeStandard?: boolean; // default: true
loading?: boolean;
fallback?: ComponentRenderer;
}
```
## Standard Components
### Document Structure
#### Document
Top-level PDF wrapper. Must be the root element. Children must be `Page` components.
```typescript
{
title: string | null;
author: string | null;
subject: string | null;
}
```
#### Page
A page in the document with configurable size, orientation, and margins.
```typescript
{
size: "A4" | "A3" | "A5" | "LETTER" | "LEGAL" | "TABLOID" | null;
orientation: "portrait" | "landscape" | null;
marginTop: number | null;
marginBottom: number | null;
marginLeft: number | null;
marginRight: number | null;
backgroundColor: string | null;
}
```
### Layout
#### View
Generic container with padding, margin, background, border, and flex alignment.
```typescript
{
padding: number | null;
paddingTop: number | null;
paddingBottom: number | null;
paddingLeft: number | null;
paddingRight: number | null;
margin: number | null;
backgroundColor: string | null;
borderWidth: number | null;
borderColor: string | null;
borderRadius: number | null;
flex: number | null;
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
}
```
#### Row
Horizontal flex layout with optional wrapping.
```typescript
{
gap: number | null;
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
padding: number | null;
flex: number | null;
wrap: boolean | null;
}
```
#### Column
Vertical flex layout.
```typescript
{
gap: number | null;
alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null;
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null;
padding: number | null;
flex: number | null;
}
```
### Content
#### Heading
h1-h4 heading text with configurable color and alignment.
```typescript
{
text: string;
level: "h1" | "h2" | "h3" | "h4" | null;
color: string | null;
align: "left" | "center" | "right" | null;
}
```
#### Text
Body text with full styling control.
```typescript
{
text: string;
fontSize: number | null;
color: string | null;
align: "left" | "center" | "right" | null;
fontWeight: "normal" | "bold" | null;
fontStyle: "normal" | "italic" | null;
lineHeight: number | null;
}
```
#### Image
Image from a URL with optional dimensions and fit.
```typescript
{
src: string;
width: number | null;
height: number | null;
objectFit: "contain" | "cover" | "fill" | "none" | null;
}
```
#### Link
Hyperlink with visible text.
```typescript
{
text: string;
href: string;
fontSize: number | null;
color: string | null;
}
```
### Data
#### Table
Data table with typed columns and string rows. Supports header styling and striped rows.
```typescript
{
columns: { header: string; width?: string; align?: "left" | "center" | "right" }[];
rows: string[][];
headerBackgroundColor: string | null;
headerTextColor: string | null;
borderColor: string | null;
fontSize: number | null;
striped: boolean | null;
}
```
#### List
Ordered or unordered list.
```typescript
{
items: string[];
ordered: boolean | null;
fontSize: number | null;
color: string | null;
spacing: number | null;
}
```
### Decorative
#### Divider
Horizontal line separator.
```typescript
{
color: string | null;
thickness: number | null;
marginTop: number | null;
marginBottom: number | null;
}
```
#### Spacer
Empty vertical space.
```typescript
{
height: number | null;
}
```
### Page-Level
#### PageNumber
Renders current page number and total pages. Format uses `{pageNumber}` and `{totalPages}` placeholders.
```typescript
{
format: string | null; // default: "{pageNumber} / {totalPages}"
fontSize: number | null;
color: string | null;
align: "left" | "center" | "right" | null;
}
```
## External Store (Controlled Mode)
Pass a `StateStore` to `StateProvider`, `JSONUIProvider`, or `createRenderer` for full control over state:
```tsx
import { createStateStore, type StateStore } from "@json-render/react-pdf";
const store = createStateStore({ invoice: { total: 100 } });
store.set("/invoice/total", 200);
```
When `store` is provided, `initialState` / `state` and `onStateChange` are ignored.
## Server-Safe Import
Import schema and catalog definitions without pulling in React or `@react-pdf/renderer`:
```typescript
import { schema, standardComponentDefinitions } from '@json-render/react-pdf/server';
```
## Sub-path Exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>@json-render/react-pdf</code></td>
<td>Full package: schema, renderer, components, render functions</td>
</tr>
<tr>
<td><code>@json-render/react-pdf/server</code></td>
<td>Schema and catalog definitions only (no React)</td>
</tr>
<tr>
<td><code>@json-render/react-pdf/catalog</code></td>
<td>Standard component definitions and types</td>
</tr>
<tr>
<td><code>@json-render/react-pdf/render</code></td>
<td>Server-side render functions only</td>
</tr>
</tbody>
</table>
## Types
<table>
<thead>
<tr>
<th>Export</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ReactPdfSchema</code></td>
<td>Schema type for PDF specs</td>
</tr>
<tr>
<td><code>ReactPdfSpec</code></td>
<td>Spec type for PDF documents</td>
</tr>
<tr>
<td><code>RenderOptions</code></td>
<td>Options for render functions</td>
</tr>
<tr>
<td><code>ComponentContext</code></td>
<td>Typed component render function context</td>
</tr>
<tr>
<td><code>ComponentFn</code></td>
<td>Component render function type</td>
</tr>
<tr>
<td><code>StandardComponentDefinitions</code></td>
<td>Type of the standard component definitions object</td>
</tr>
<tr>
<td><code>StandardComponentProps&lt;K&gt;</code></td>
<td>Inferred props type for a standard component by name</td>
</tr>
</tbody>
</table>
@@ -0,0 +1,416 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react-three-fiber")
# @json-render/react-three-fiber
React Three Fiber renderer for json-render. 19 built-in 3D components for meshes, lights, models, environments, text, cameras, and controls.
## Installation
```bash
npm install @json-render/react-three-fiber @json-render/core @json-render/react @react-three/fiber @react-three/drei three zod
```
## Entry Points
<table>
<thead>
<tr>
<th>Entry Point</th>
<th>Exports</th>
<th>Use For</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>@json-render/react-three-fiber</code></td>
<td><code>threeComponents</code>, <code>ThreeRenderer</code>, <code>ThreeCanvas</code>, schemas</td>
<td>React Three Fiber implementations and renderer</td>
</tr>
<tr>
<td><code>@json-render/react-three-fiber/catalog</code></td>
<td><code>threeComponentDefinitions</code></td>
<td>Catalog schemas (no R3F dependency, safe for server)</td>
</tr>
</tbody>
</table>
## Usage
```tsx
import {'{ defineCatalog }'} from "@json-render/core";
import {'{ schema, defineRegistry }'} from "@json-render/react";
import {'{'}
threeComponentDefinitions,
threeComponents,
ThreeCanvas,
{'}'} from "@json-render/react-three-fiber";
const catalog = defineCatalog(schema, {'{'}
components: {'{'}
Box: threeComponentDefinitions.Box,
Sphere: threeComponentDefinitions.Sphere,
AmbientLight: threeComponentDefinitions.AmbientLight,
DirectionalLight: threeComponentDefinitions.DirectionalLight,
OrbitControls: threeComponentDefinitions.OrbitControls,
{'}'},
actions: {'{}'},
{'}'});
const {'{ registry }'} = defineRegistry(catalog, {'{'}
components: {'{'}
Box: threeComponents.Box,
Sphere: threeComponents.Sphere,
AmbientLight: threeComponents.AmbientLight,
DirectionalLight: threeComponents.DirectionalLight,
OrbitControls: threeComponents.OrbitControls,
{'}'},
{'}'});
```
### ThreeCanvas (convenience)
```tsx
<ThreeCanvas
spec={'{spec}'}
registry={'{registry}'}
shadows
camera={'{'}{'{ position: [5, 5, 5], fov: 50 }'}{'}'}
style={'{'}{'{ width: "100%", height: "100vh" }'}{'}'}
/>
```
### Manual Canvas Setup
```tsx
import {'{ Canvas }'} from "@react-three/fiber";
import {'{ ThreeRenderer }'} from "@json-render/react-three-fiber";
<Canvas shadows>
<ThreeRenderer spec={'{spec}'} registry={'{registry}'} />
</Canvas>
```
## Components
### Primitives
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
<th>Key Props</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Box</code></td>
<td>Box mesh (default 1x1x1)</td>
<td><code>width</code>, <code>height</code>, <code>depth</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Sphere</code></td>
<td>Sphere mesh</td>
<td><code>radius</code>, <code>widthSegments</code>, <code>heightSegments</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Cylinder</code></td>
<td>Cylinder mesh</td>
<td><code>radiusTop</code>, <code>radiusBottom</code>, <code>height</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Cone</code></td>
<td>Cone mesh</td>
<td><code>radius</code>, <code>height</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Torus</code></td>
<td>Torus (donut) mesh</td>
<td><code>radius</code>, <code>tube</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Plane</code></td>
<td>Flat plane mesh</td>
<td><code>width</code>, <code>height</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Capsule</code></td>
<td>Capsule mesh</td>
<td><code>radius</code>, <code>length</code>, <code>material</code></td>
</tr>
</tbody>
</table>
All primitives share: <code>position</code>, <code>rotation</code>, <code>scale</code>, <code>castShadow</code>, <code>receiveShadow</code>, <code>material</code>.
### Material Schema
<table>
<thead>
<tr>
<th>Property</th>
<th>Type</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>color</code></td>
<td><code>string</code></td>
<td><code>"#ffffff"</code></td>
</tr>
<tr>
<td><code>metalness</code></td>
<td><code>number</code></td>
<td><code>0</code></td>
</tr>
<tr>
<td><code>roughness</code></td>
<td><code>number</code></td>
<td><code>1</code></td>
</tr>
<tr>
<td><code>emissive</code></td>
<td><code>string</code></td>
<td><code>"#000000"</code></td>
</tr>
<tr>
<td><code>emissiveIntensity</code></td>
<td><code>number</code></td>
<td><code>1</code></td>
</tr>
<tr>
<td><code>opacity</code></td>
<td><code>number</code></td>
<td><code>1</code></td>
</tr>
<tr>
<td><code>transparent</code></td>
<td><code>boolean</code></td>
<td><code>false</code></td>
</tr>
<tr>
<td><code>wireframe</code></td>
<td><code>boolean</code></td>
<td><code>false</code></td>
</tr>
</tbody>
</table>
### Lights
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
<th>Key Props</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>AmbientLight</code></td>
<td>Uniform illumination</td>
<td><code>color</code>, <code>intensity</code></td>
</tr>
<tr>
<td><code>DirectionalLight</code></td>
<td>Sunlight-style</td>
<td><code>position</code>, <code>color</code>, <code>intensity</code>, <code>castShadow</code></td>
</tr>
<tr>
<td><code>PointLight</code></td>
<td>Radiates from a point</td>
<td><code>position</code>, <code>color</code>, <code>intensity</code>, <code>distance</code>, <code>decay</code></td>
</tr>
<tr>
<td><code>SpotLight</code></td>
<td>Cone of light</td>
<td><code>position</code>, <code>color</code>, <code>intensity</code>, <code>angle</code>, <code>penumbra</code></td>
</tr>
</tbody>
</table>
### Other Components
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
<th>Key Props</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Group</code></td>
<td>Container for children</td>
<td><code>position</code>, <code>rotation</code>, <code>scale</code></td>
</tr>
<tr>
<td><code>Model</code></td>
<td>GLTF/GLB model loader</td>
<td><code>url</code>, <code>position</code>, <code>rotation</code>, <code>scale</code></td>
</tr>
<tr>
<td><code>Environment</code></td>
<td>HDRI environment map</td>
<td><code>preset</code>, <code>background</code>, <code>blur</code>, <code>intensity</code></td>
</tr>
<tr>
<td><code>Fog</code></td>
<td>Linear fog effect</td>
<td><code>color</code>, <code>near</code>, <code>far</code></td>
</tr>
<tr>
<td><code>GridHelper</code></td>
<td>Reference grid</td>
<td><code>size</code>, <code>divisions</code>, <code>color</code></td>
</tr>
<tr>
<td><code>Text3D</code></td>
<td>3D text (SDF)</td>
<td><code>text</code>, <code>fontSize</code>, <code>color</code>, <code>anchorX</code>, <code>anchorY</code></td>
</tr>
<tr>
<td><code>PerspectiveCamera</code></td>
<td>Camera</td>
<td><code>position</code>, <code>fov</code>, <code>near</code>, <code>far</code>, <code>makeDefault</code></td>
</tr>
<tr>
<td><code>OrbitControls</code></td>
<td>Camera controls</td>
<td><code>enableDamping</code>, <code>enableZoom</code>, <code>autoRotate</code></td>
</tr>
</tbody>
</table>
## Shared Schemas
Reusable Zod schemas for custom 3D components:
```tsx
import {'{ vector3Schema, materialSchema, transformProps, shadowProps }'} from "@json-render/react-three-fiber";
```
<table>
<thead>
<tr>
<th>Export</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>vector3Schema</code></td>
<td><code>z.tuple([z.number(), z.number(), z.number()])</code></td>
</tr>
<tr>
<td><code>materialSchema</code></td>
<td>Standard material props (color, metalness, roughness, etc.)</td>
</tr>
<tr>
<td><code>transformProps</code></td>
<td><code>{'{ position, rotation, scale }'}</code> schema fields</td>
</tr>
<tr>
<td><code>shadowProps</code></td>
<td><code>{'{ castShadow, receiveShadow }'}</code> schema fields</td>
</tr>
</tbody>
</table>
## ThreeRenderer
<table>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>spec</code></td>
<td><code>Spec | null</code></td>
<td>The spec to render as a 3D scene</td>
</tr>
<tr>
<td><code>registry</code></td>
<td><code>ComponentRegistry</code></td>
<td>Component registry from <code>defineRegistry</code></td>
</tr>
<tr>
<td><code>store</code></td>
<td><code>StateStore</code></td>
<td>External state store (controlled mode)</td>
</tr>
<tr>
<td><code>initialState</code></td>
<td><code>Record&lt;string, unknown&gt;</code></td>
<td>Initial state (uncontrolled mode)</td>
</tr>
<tr>
<td><code>handlers</code></td>
<td><code>Record&lt;string, Function&gt;</code></td>
<td>Action handlers</td>
</tr>
<tr>
<td><code>loading</code></td>
<td><code>boolean</code></td>
<td>Whether the spec is streaming</td>
</tr>
<tr>
<td><code>children</code></td>
<td><code>ReactNode</code></td>
<td>Additional R3F elements alongside the spec</td>
</tr>
</tbody>
</table>
## ThreeCanvas
Extends <code>ThreeRendererProps</code> with Canvas options:
<table>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>shadows</code></td>
<td><code>boolean</code></td>
<td>Enable shadow maps</td>
</tr>
<tr>
<td><code>camera</code></td>
<td><code>object</code></td>
<td>Default camera config (position, fov, etc.)</td>
</tr>
<tr>
<td><code>className</code></td>
<td><code>string</code></td>
<td>CSS class for the canvas container</td>
</tr>
<tr>
<td><code>style</code></td>
<td><code>CSSProperties</code></td>
<td>Inline styles for the canvas container</td>
</tr>
</tbody>
</table>
## Type Helpers
```tsx
import type {'{ ThreeProps }'} from "@json-render/react-three-fiber";
type BoxProps = ThreeProps<"Box">;
type SphereProps = ThreeProps<"Sphere">;
```
+377
View File
@@ -0,0 +1,377 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react")
# @json-render/react
React components, providers, and hooks.
## Providers
### StateProvider
```tsx
<StateProvider initialState={object} onStateChange={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:
```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
```tsx
<ActionProvider handlers={Record<string, ActionHandler>}>
{children}
</ActionProvider>
type ActionHandler = (params: Record<string, unknown>) => void | Promise<void>;
```
### VisibilityProvider
```tsx
<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
```tsx
<ValidationProvider customFunctions={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.
```tsx
import { defineRegistry } from '@json-render/react';
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => <div>{props.title}{children}</div>,
Button: ({ props, emit }) => (
<button onClick={() => emit("press")}>
{props.label}
</button>
),
},
});
// Pass to <Renderer>
<Renderer spec={spec} registry={registry} />
```
## Components
### Renderer
```tsx
<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
/>
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)
```tsx
interface ComponentContext<P> {
props: P; // Typed props from catalog
children?: React.ReactNode; // Rendered children (for slot 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 to check metadata like `shouldPreventDefault`:
```tsx
Link: ({ props, on }) => {
const click = on("click");
return (
<a
href={props.href}
onClick={(e) => {
if (click.shouldPreventDefault) e.preventDefault();
click.emit();
}}
>
{props.label}
</a>
);
},
```
### BaseComponentProps
Catalog-agnostic base type for building reusable component libraries (e.g. `@json-render/shadcn`) that are not tied to a specific catalog:
```typescript
import type { BaseComponentProps } from "@json-render/react";
const Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => (
<div>{props.title}{children}</div>
);
```
## Hooks
### useUIStream
```typescript
const {
spec, // Spec | null - current UI state
isStreaming, // boolean - true while streaming
error, // Error | null
send, // (prompt: string, context?: Record<string, unknown>) => Promise<void>
clear, // () => void - reset spec and error
} = useUIStream({
api: string, // API endpoint URL
onComplete?: (spec: Spec) => void, // Called when streaming completes
onError?: (error: Error) => void, // Called when an error occurs
});
```
### useStateStore
```typescript
const {
state, // StateModel (Record<string, unknown>)
get, // (path: string) => unknown
set, // (path: string, value: unknown) => void
update, // (updates: Record<string, unknown>) => void
} = useStateStore();
```
### useStateValue
```typescript
const value = useStateValue(path: string);
```
### useStateBinding (deprecated)
> **Deprecated.** Use `useBoundProp` with `$bindState` expressions instead.
```typescript
const [value, setValue] = useStateBinding(path: string);
```
### useActions
```typescript
const { execute } = useActions();
// execute(binding: ActionBinding) => Promise<void>
```
### useAction
```typescript
const { execute, isLoading } = useAction(binding: ActionBinding);
// execute() => Promise<void>
```
### useIsVisible
```typescript
const isVisible = useIsVisible(condition?: VisibilityCondition);
```
### useFieldValidation
```typescript
const {
state, // FieldValidationState
validate, // () => ValidationResult
touch, // () => void
clear, // () => void
errors, // string[]
isValid, // boolean
} = useFieldValidation(path: string, config?: ValidationConfig);
```
`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
Two-way binding helper for `$bindState` / `$bindItem` expressions. Returns `[value, setValue]` where `setValue` writes back to the bound state path.
```typescript
const [value, setValue] = useBoundProp<T>(
propValue: T | undefined, // The already-resolved prop value
bindingPath: string | undefined // From bindings?.value
);
```
Use inside registry components:
```tsx
const Input: ComponentRenderer = ({ props, bindings }) => {
const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);
return <input value={value ?? ""} onChange={(e) => setValue(e.target.value)} />;
};
```
### Chat Hooks
Two hooks are available for chat + GenUI, depending on your setup:
- **`useChatUI`** -- Self-contained chat hook with its own message state, fetch logic, and mixed stream parsing. Use when you want a standalone chat experience without the Vercel AI SDK.
- **`useJsonRenderMessage`** -- Extracts spec + text from an AI SDK `UIMessage.parts` array. Use with the Vercel AI SDK's `useChat` for full AI SDK integration.
### useChatUI
Hook for chat + GenUI experiences. Manages a multi-turn conversation where each assistant message can contain both text and a json-render UI spec.
```typescript
const {
messages, // ChatMessage[] - all messages in the conversation
isStreaming, // boolean - true while streaming
error, // Error | null
send, // (text: string) => Promise<void>
clear, // () => void - reset conversation
} = useChatUI({
api: string, // API endpoint
onComplete?: (message: ChatMessage) => void, // Called when streaming completes
onError?: (error: Error) => void, // Called on error
});
interface ChatMessage {
id: string;
role: "user" | "assistant";
text: string;
spec: Spec | null;
}
```
### useJsonRenderMessage
Extract a spec and text content from an AI SDK message's `parts` array. Designed for integration with Vercel AI SDK's `useChat`.
```typescript
const { spec, text, hasSpec } = useJsonRenderMessage(parts: DataPart[]);
// spec: Spec | null - compiled from JSONL patches in data parts
// text: string - concatenated text parts
// hasSpec: boolean - true when spec is non-null
```
### buildSpecFromParts / getTextFromParts
Standalone utilities for extracting spec and text from AI SDK message parts (non-hook versions):
```typescript
import { buildSpecFromParts, getTextFromParts } from '@json-render/react';
const spec = buildSpecFromParts(message.parts); // Spec | null
const text = getTextFromParts(message.parts); // string
```
+104
View File
@@ -0,0 +1,104 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/redux")
# @json-render/redux
Redux / Redux Toolkit adapter for json-render's `StateStore` interface.
## Installation
```bash
npm install @json-render/redux @json-render/core @json-render/react redux
# or with Redux Toolkit (recommended):
npm install @json-render/redux @json-render/core @json-render/react @reduxjs/toolkit
```
## reduxStateStore
Create a `StateStore` backed by a Redux store.
```typescript
import { reduxStateStore } from "@json-render/redux";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>store</code></td>
<td><code>Store</code></td>
<td>Yes</td>
<td>The Redux store instance.</td>
</tr>
<tr>
<td><code>selector</code></td>
<td><code>{'(state: S) => StateModel'}</code></td>
<td>No</td>
<td>Select the json-render slice from the Redux state tree. Defaults to <code>{'(state) => state'}</code>.</td>
</tr>
<tr>
<td><code>dispatch</code></td>
<td><code>{'(nextState: StateModel, store: Store) => void'}</code></td>
<td>Yes</td>
<td>Dispatch an action that replaces the selected slice with the next state.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { reduxStateStore } from "@json-render/redux";
import { StateProvider } from "@json-render/react";
const uiSlice = createSlice({
name: "ui",
initialState: { count: 0 } as Record<string, unknown>,
reducers: {
replaceUiState: (_state, action) => action.payload,
},
});
const reduxStore = configureStore({
reducer: { ui: uiSlice.reducer },
});
const store = reduxStateStore({
store: reduxStore,
selector: (state) => state.ui,
dispatch: (next, s) => s.dispatch(uiSlice.actions.replaceUiState(next)),
});
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through Redux */}
</StateProvider>
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
@@ -0,0 +1,241 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/remotion")
# @json-render/remotion
Remotion video renderer. Turn JSON timeline specs into video compositions.
## schema
The timeline schema for video specs. Use with `defineCatalog` from core.
```typescript
import { defineCatalog } from '@json-render/core';
import { schema, standardComponentDefinitions } from '@json-render/remotion';
const catalog = defineCatalog(schema, {
components: standardComponentDefinitions,
transitions: standardTransitionDefinitions,
effects: standardEffectDefinitions,
});
```
## Renderer
The main composition component that renders timeline specs. Use with Remotion's Player or in a Remotion project.
```tsx
import { Player } from '@remotion/player';
import { Renderer } from '@json-render/remotion';
function VideoPlayer({ spec }) {
return (
<Player
component={Renderer}
inputProps={{ spec }}
durationInFrames={spec.composition.durationInFrames}
fps={spec.composition.fps}
compositionWidth={spec.composition.width}
compositionHeight={spec.composition.height}
controls
/>
);
}
```
### Custom Components
Pass custom components to the Renderer:
```tsx
import { Renderer, standardComponents } from '@json-render/remotion';
const customComponents = {
...standardComponents,
MyCustomClip: ({ clip }) => <div>{clip.props.text}</div>,
};
<Player
component={Renderer}
inputProps={{ spec, components: customComponents }}
// ...
/>
```
## Standard Components
Pre-built video components included in the package:
```typescript
import {
TitleCard, // Full-screen title with subtitle
ImageSlide, // Full-screen image display
SplitScreen, // Two-column layout
QuoteCard, // Quote with attribution
StatCard, // Large statistic display
LowerThird, // Name/title overlay
TextOverlay, // Centered text overlay
TypingText, // Terminal typing animation
LogoBug, // Corner logo watermark
VideoClip, // Video playback
} from '@json-render/remotion';
```
### TitleCard Props
```typescript
{
title: string;
subtitle?: string;
backgroundColor?: string; // default: "#1a1a1a"
textColor?: string; // default: "#ffffff"
}
```
### TypingText Props
```typescript
{
text: string;
charsPerSecond?: number; // default: 15
showCursor?: boolean; // default: true
cursorChar?: string; // default: "|"
fontFamily?: string; // default: "monospace"
fontSize?: number; // default: 48
textColor?: string; // default: "#00ff00"
backgroundColor?: string; // default: "#1e1e1e"
}
```
## Catalog Definitions
Pre-built definitions for creating catalogs:
```typescript
import {
standardComponentDefinitions, // All standard component definitions
standardTransitionDefinitions, // fade, slideLeft, slideRight, etc.
standardEffectDefinitions, // kenBurns, pulseGlow, colorShift
} from '@json-render/remotion';
// Use in your catalog
const catalog = defineCatalog(schema, {
components: {
...standardComponentDefinitions,
// Add custom components
},
transitions: standardTransitionDefinitions,
effects: standardEffectDefinitions,
});
```
## Hooks & Utilities
### useTransition
Calculate transition styles for a clip based on current frame:
```typescript
import { useTransition } from '@json-render/remotion';
import { useCurrentFrame } from 'remotion';
function MyComponent({ clip }) {
const frame = useCurrentFrame();
const transition = useTransition(clip, frame);
return (
<div style={{
opacity: transition.opacity,
transform: transition.transform,
}}>
Content
</div>
);
}
```
### ClipWrapper
Automatically apply transitions to clip content:
```tsx
import { ClipWrapper } from '@json-render/remotion';
function MyClip({ clip }) {
return (
<ClipWrapper clip={clip}>
<div>My content with automatic transitions</div>
</ClipWrapper>
);
}
```
## Types
### TimelineSpec
```typescript
interface TimelineSpec {
composition: {
id: string;
fps: number;
width: number;
height: number;
durationInFrames: number;
};
tracks: Track[];
clips: Clip[];
audio: {
tracks: AudioTrack[];
};
}
```
### Clip
```typescript
interface Clip {
id: string;
trackId: string;
component: string;
props: Record<string, unknown>;
from: number;
durationInFrames: number;
transitionIn?: {
type: string;
durationInFrames: number;
};
transitionOut?: {
type: string;
durationInFrames: number;
};
}
```
### TransitionStyles
```typescript
interface TransitionStyles {
opacity: number;
transform: string;
}
```
### ComponentRegistry
```typescript
type ClipComponent = React.ComponentType<{ clip: Clip }>;
type ComponentRegistry = Record<string, ClipComponent>;
```
## Transitions
Available transition types:
- `fade` - Opacity fade in/out
- `slideLeft` - Slide from right
- `slideRight` - Slide from left
- `slideUp` - Slide from bottom
- `slideDown` - Slide from top
- `zoom` - Scale zoom in/out
- `wipe` - Horizontal wipe
@@ -0,0 +1,349 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/shadcn")
# @json-render/shadcn
Pre-built [shadcn/ui](https://ui.shadcn.com/) components for json-render. 36 components built on Radix UI + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.
## Installation
```bash
npm install @json-render/shadcn @json-render/core @json-render/react zod
```
Your app must have Tailwind CSS configured.
## Entry Points
<table>
<thead>
<tr>
<th>Entry Point</th>
<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
Pick the components you need from the standard definitions:
```typescript
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
import { defineRegistry } from "@json-render/react";
import { shadcnComponents } from "@json-render/shadcn";
// Catalog: pick definitions
const catalog = defineCatalog(schema, {
components: {
Card: shadcnComponentDefinitions.Card,
Stack: shadcnComponentDefinitions.Stack,
Heading: shadcnComponentDefinitions.Heading,
Button: shadcnComponentDefinitions.Button,
Input: shadcnComponentDefinitions.Input,
},
actions: {},
});
// Registry: pick matching implementations
const { registry } = defineRegistry(catalog, {
components: {
Card: shadcnComponents.Card,
Stack: shadcnComponents.Stack,
Heading: shadcnComponents.Heading,
Button: shadcnComponents.Button,
Input: shadcnComponents.Input,
},
});
```
State actions (`setState`, `pushState`, `removeState`) are built into the React schema and handled by `ActionProvider` automatically. You don't need to declare them in your catalog.
## Extending with Custom Components
Add custom components alongside standard ones:
```typescript
import { z } from "zod";
const catalog = defineCatalog(schema, {
components: {
// Standard
Card: shadcnComponentDefinitions.Card,
Stack: shadcnComponentDefinitions.Stack,
Button: shadcnComponentDefinitions.Button,
// Custom
Metric: {
props: z.object({
label: z.string(),
value: z.string(),
trend: z.enum(["up", "down", "neutral"]).nullable(),
}),
description: "KPI metric display",
},
},
actions: {},
});
const { registry } = defineRegistry(catalog, {
components: {
Card: shadcnComponents.Card,
Stack: shadcnComponents.Stack,
Button: shadcnComponents.Button,
Metric: ({ props }) => (
<div>
<span>{props.label}</span>
<span>{props.value}</span>
</div>
),
},
});
```
## Available Components
### Layout
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</tr>
</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
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</tr>
</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
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</tr>
</thead>
<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
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Heading</code></td>
<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
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</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
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Button</code></td>
<td>Clickable button with label, variant, disabled</td>
</tr>
<tr>
<td><code>Link</code></td>
<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
- The `/catalog` entry point has no React dependency -- use it for server-side prompt generation
- Components use Tailwind CSS classes -- your app must have Tailwind configured
- Component implementations use bundled shadcn/ui primitives (not your app's `components/ui/`)
- Form inputs support `checks` for validation (type + message pairs)
- Events: inputs emit `change`/`submit`/`focus`/`blur`; buttons emit `press`; selects emit `change`/`select`
+201
View File
@@ -0,0 +1,201 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/api/solid");
# @json-render/solid
SolidJS components, providers, and hooks for rendering json-render specs.
## Installation
<PackageInstall packages="@json-render/core @json-render/solid" />
Peer dependencies: `solid-js ^1.9.0` and `zod ^4.0.0`.
<PackageInstall packages="solid-js zod" />
## Providers
### StateProvider
```tsx
<StateProvider
initialState={{}}
onStateChange={(changes) => console.log(changes)}
>
{/* children */}
</StateProvider>
```
<table>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>store</code>
</td>
<td>
<code>StateStore</code>
</td>
<td>
External store (controlled mode). When provided,{" "}
<code>initialState</code> and <code>onStateChange</code> are ignored.
</td>
</tr>
<tr>
<td>
<code>initialState</code>
</td>
<td>
<code>Record&lt;string, unknown&gt;</code>
</td>
<td>Initial state model for uncontrolled mode.</td>
</tr>
<tr>
<td>
<code>onStateChange</code>
</td>
<td>
<code>
{"(changes: Array<{ path: string; value: unknown }>) => void"}
</code>
</td>
<td>Called for uncontrolled state updates.</td>
</tr>
</tbody>
</table>
### ActionProvider
```tsx
<ActionProvider
handlers={{ submit: async (params) => {} }}
navigate={(path) => {}}
>
{/* children */}
</ActionProvider>
```
### VisibilityProvider
```tsx
<VisibilityProvider>{/* children */}</VisibilityProvider>
```
### ValidationProvider
```tsx
<ValidationProvider customFunctions={{ custom: (value) => Boolean(value) }}>
{/* children */}
</ValidationProvider>
```
### JSONUIProvider
Combined provider wrapper for state, visibility, validation, and actions.
```tsx
<JSONUIProvider
registry={registry}
initialState={{}}
handlers={handlers}
validationFunctions={validationFunctions}
>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
```
## defineRegistry
Create a typed component registry and action helpers from a catalog.
```tsx
const { registry, handlers, executeAction } = defineRegistry(catalog, {
components: {
Card: (renderProps) => <div>{renderProps.children}</div>,
Button: (renderProps) => (
<button onClick={() => renderProps.emit("press")}>
{renderProps.element.props.label as string}
</button>
),
},
actions: {
submit: async (params, setState, state) => {
// custom action logic
},
},
});
```
## Components
### Renderer
```tsx
<Renderer spec={spec} registry={registry} loading={false} />
```
Renders a `Spec` tree using your registry.
### createRenderer
Build an app-level renderer from catalog + components:
```tsx
const AppRenderer = createRenderer(catalog, {
Card: (renderProps) => <div>{renderProps.children}</div>,
});
<AppRenderer spec={spec} state={{}} onAction={(name, params) => {}} />;
```
## Hooks
- `useStateStore()`
- `useStateValue(path)` - returns an accessor
- `useStateBinding(path)` - returns `[Accessor<T | undefined>, setValue]`
- `useVisibility()` / `useIsVisible(condition)`
- `useActions()` / `useAction(binding)`
- `useValidation()` / `useOptionalValidation()`
- `useFieldValidation(path, config)` - returns accessor-backed `state`, `errors`, and `isValid`
- `useBoundProp(value, bindingPath)`
- `useUIStream(options)`
- `useChatUI(options)`
## Built-in Actions
`ActionProvider` handles these built-in actions:
- `setState`
- `pushState`
- `removeState`
- `validateForm`
## Component Props
Registry components receive:
```ts
interface ComponentRenderProps<P = Record<string, unknown>> {
element: UIElement<string, P>;
children?: JSX.Element;
emit: (event: string) => void;
on: (event: string) => EventHandle;
bindings?: Record<string, string>;
loading?: boolean;
}
```
Use `emit("event")` to dispatch event bindings. Use `on("event")` to access `EventHandle` metadata (`bound`, `shouldPreventDefault`, `emit`).
## Reactivity Notes
- Keep changing reads in JSX expressions, `createMemo`, or `createEffect`.
- Avoid props destructuring in component signatures when you need live updates.
- `StateProvider` and other contexts expose getter-backed values so consumers read live signals.
- `useStateValue`, `useStateBinding`, and `useFieldValidation` expose reactive accessors; call them as functions.
@@ -0,0 +1,126 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/svelte")
# @json-render/svelte
Svelte 5 components, providers, and helpers for rendering json-render specs.
## Installation
<PackageInstall packages="@json-render/core @json-render/svelte" />
Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.
<PackageInstall packages="svelte zod" />
## Components
### Renderer
```svelte
<Renderer
spec={spec} // Spec | null
registry={registry}
loading={false}
/>
```
Renders a spec with your component registry. If `spec` is `null`, it renders nothing.
### JsonUIProvider
Convenience wrapper around `StateProvider`, `VisibilityProvider`, `ValidationProvider`, and `ActionProvider`.
```svelte
<JsonUIProvider
initialState={{}}
handlers={handlers}
validationFunctions={validationFunctions}
>
<Renderer {spec} {registry} />
</JsonUIProvider>
```
## defineRegistry
Create a typed component registry and action handlers from a catalog.
```typescript
import { defineRegistry } from "@json-render/svelte";
const { registry, handlers, executeAction } = defineRegistry(catalog, {
components: {
Card,
Button,
},
actions: {
submit: async (params, setState, state) => {
// custom action logic
},
},
});
```
`handlers` is designed for `JsonUIProvider`/`ActionProvider`. `executeAction` is an imperative helper.
## Component Props
Registry components receive `BaseComponentProps<TProps>`:
```typescript
interface BaseComponentProps<TProps> {
props: TProps;
children?: Snippet;
emit: (event: string) => void;
bindings?: Record<string, string>;
loading?: boolean;
}
```
Use `emit("eventName")` to trigger handlers declared in the spec `on` bindings.
## Context Helpers
Use these helpers inside Svelte components:
- `getStateValue(path)` - read/write state via `.current`
- `getBoundProp(() => value, () => bindingPath)` - write back resolved `$bindState` / `$bindItem` values
- `isVisible(condition)` - evaluate visibility via `.current`
- `getAction(name)` - read a registered action handler via `.current`
- `getFieldValidation(ctx, path, config)` - get field validation state + actions
For advanced usage, access full contexts:
- `getStateContext()`
- `getActionContext()`
- `getVisibilityContext()`
- `getValidationContext()`
- `getOptionalValidationContext()`
## Streaming
### createUIStream
```typescript
const stream = createUIStream({
api: "/api/generate-ui",
onComplete: (spec) => console.log(spec),
});
await stream.send("Create a login form");
console.log(stream.spec);
console.log(stream.isStreaming);
```
### createChatUI
```typescript
const chat = createChatUI({ api: "/api/chat-ui" });
await chat.send("Build a settings panel");
console.log(chat.messages, chat.isStreaming);
```
## Schema Export
Use `schema` from `@json-render/svelte` when defining catalogs for Svelte specs.
+337
View File
@@ -0,0 +1,337 @@
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>
@@ -0,0 +1,77 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/xstate")
# @json-render/xstate
[XState Store](https://stately.ai/docs/xstate-store) adapter for json-render's `StateStore` interface.
Requires `@xstate/store` v3+.
## Installation
```bash
npm install @json-render/xstate @json-render/core @json-render/react @xstate/store
```
## xstateStoreStateStore
Create a `StateStore` backed by an `@xstate/store` atom.
```typescript
import { xstateStoreStateStore } from "@json-render/xstate";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>atom</code></td>
<td><code>{'Atom<StateModel>'}</code></td>
<td>Yes</td>
<td>An <code>@xstate/store</code> atom (from <code>createAtom</code>) holding the json-render state model.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { createAtom } from "@xstate/store";
import { xstateStoreStateStore } from "@json-render/xstate";
import { StateProvider } from "@json-render/react";
const uiAtom = createAtom({ count: 0 });
const store = xstateStoreStateStore({ atom: uiAtom });
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through @xstate/store */}
</StateProvider>
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
+232
View File
@@ -0,0 +1,232 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/yaml")
# @json-render/yaml
YAML wire format for json-render. Progressive rendering and surgical edits via streaming YAML.
## Prompt Generation
### yamlPrompt
Generate a YAML-format system prompt from any json-render catalog. Works with catalogs from any renderer.
```typescript
function yamlPrompt(
catalog: Catalog,
options?: YamlPromptOptions
): string
```
```typescript
import { yamlPrompt } from "@json-render/yaml";
const systemPrompt = yamlPrompt(catalog, {
mode: "standalone",
customRules: ["Always use dark theme"],
editModes: ["merge"],
});
```
### YamlPromptOptions
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>system</code></td>
<td><code>string</code></td>
<td><code>{'\"You are a UI generator that outputs YAML.\"'}</code></td>
<td>Custom system message intro</td>
</tr>
<tr>
<td><code>mode</code></td>
<td><code>{'\"standalone\" | \"inline\"'}</code></td>
<td><code>{'\"standalone\"'}</code></td>
<td>Standalone outputs only YAML; inline allows conversational responses with embedded YAML fences</td>
</tr>
<tr>
<td><code>customRules</code></td>
<td><code>{'string[]'}</code></td>
<td><code>{'[]'}</code></td>
<td>Additional rules appended to the prompt</td>
</tr>
<tr>
<td><code>editModes</code></td>
<td><code>{'EditMode[]'}</code></td>
<td><code>{'[\"merge\"]'}</code></td>
<td>Edit modes to document in the prompt (patch, merge, diff)</td>
</tr>
</tbody>
</table>
## AI SDK Transform
### createYamlTransform
Creates a `TransformStream` that intercepts AI SDK stream chunks and converts YAML spec/edit blocks into json-render patch data parts.
```typescript
function createYamlTransform(
options?: YamlTransformOptions
): TransformStream<StreamChunk, StreamChunk>
```
Recognized fence types:
- <code>{'```yaml-spec'}</code> -- Full YAML spec, parsed progressively
- <code>{'```yaml-edit'}</code> -- Partial YAML, deep-merged with current spec
- <code>{'```yaml-patch'}</code> -- RFC 6902 JSON Patch lines
- <code>{'```diff'}</code> -- Unified diff against serialized spec
### pipeYamlRender
Convenience wrapper that pipes an AI SDK stream through the YAML transform. Drop-in replacement for `pipeJsonRender` from `@json-render/core`.
```typescript
function pipeYamlRender<T>(
stream: ReadableStream<T>,
options?: YamlTransformOptions
): ReadableStream<T>
```
```typescript
import { pipeYamlRender } from "@json-render/yaml";
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
const stream = createUIMessageStream({
execute: async ({ writer }) => {
writer.merge(pipeYamlRender(result.toUIMessageStream()));
},
});
return createUIMessageStreamResponse({ stream });
```
## Streaming Parser
### createYamlStreamCompiler
Create a streaming YAML compiler that incrementally parses YAML text and emits JSON Patch operations by diffing each successful parse against the previous snapshot.
```typescript
function createYamlStreamCompiler<T>(
initial?: Partial<T>
): YamlStreamCompiler<T>
```
```typescript
import { createYamlStreamCompiler } from "@json-render/yaml";
const compiler = createYamlStreamCompiler<Spec>();
compiler.push("root: main\n");
compiler.push("elements:\n main:\n type: Card\n");
const { result, newPatches } = compiler.flush();
```
### YamlStreamCompiler
<table>
<thead>
<tr>
<th>Method</th>
<th>Returns</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>push(chunk)</code></td>
<td><code>{'{ result: T; newPatches: JsonPatch[] }'}</code></td>
<td>Push a chunk of text, returns current result and new patches</td>
</tr>
<tr>
<td><code>flush()</code></td>
<td><code>{'{ result: T; newPatches: JsonPatch[] }'}</code></td>
<td>Flush remaining buffer, return final result</td>
</tr>
<tr>
<td><code>getResult()</code></td>
<td><code>T</code></td>
<td>Get the current compiled result</td>
</tr>
<tr>
<td><code>getPatches()</code></td>
<td><code>{'JsonPatch[]'}</code></td>
<td>Get all patches applied so far</td>
</tr>
<tr>
<td><code>reset(initial?)</code></td>
<td><code>void</code></td>
<td>Reset to initial state</td>
</tr>
</tbody>
</table>
## Fence Constants
Exported string constants for fence detection in custom parsers:
<table>
<thead>
<tr>
<th>Constant</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>YAML_SPEC_FENCE</code></td>
<td><code>{'\"```yaml-spec\"'}</code></td>
</tr>
<tr>
<td><code>YAML_EDIT_FENCE</code></td>
<td><code>{'\"```yaml-edit\"'}</code></td>
</tr>
<tr>
<td><code>YAML_PATCH_FENCE</code></td>
<td><code>{'\"```yaml-patch\"'}</code></td>
</tr>
<tr>
<td><code>DIFF_FENCE</code></td>
<td><code>{'\"```diff\"'}</code></td>
</tr>
<tr>
<td><code>FENCE_CLOSE</code></td>
<td><code>{'\"```\"'}</code></td>
</tr>
</tbody>
</table>
## Re-exports from @json-render/core
### diffToPatches
Generate RFC 6902 JSON Patch operations that transform one object into another.
```typescript
function diffToPatches(
oldObj: Record<string, unknown>,
newObj: Record<string, unknown>,
basePath?: string
): JsonPatch[]
```
### deepMergeSpec
Deep-merge with RFC 7396 semantics: `null` deletes, arrays replace, objects recurse.
```typescript
function deepMergeSpec(
base: Record<string, unknown>,
patch: Record<string, unknown>
): Record<string, unknown>
```
@@ -0,0 +1,108 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/zustand")
# @json-render/zustand
Zustand adapter for json-render's `StateStore` interface.
Requires Zustand v5+. Zustand v4 is not supported due to breaking API changes in the vanilla store interface.
## Installation
```bash
npm install @json-render/zustand @json-render/core @json-render/react zustand
```
## zustandStateStore
Create a `StateStore` backed by a Zustand vanilla store.
```typescript
import { zustandStateStore } from "@json-render/zustand";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>store</code></td>
<td><code>{'StoreApi<S>'}</code></td>
<td>Yes</td>
<td>A Zustand vanilla store (from <code>createStore</code> in <code>zustand/vanilla</code>).</td>
</tr>
<tr>
<td><code>selector</code></td>
<td><code>{'(state: S) => StateModel'}</code></td>
<td>No</td>
<td>Select the json-render slice from the store state. Defaults to the entire state.</td>
</tr>
<tr>
<td><code>updater</code></td>
<td><code>{'(nextState: StateModel, store: StoreApi<S>) => void'}</code></td>
<td>No</td>
<td>Apply a state change back to the store. Defaults to a shallow merge.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { createStore } from "zustand/vanilla";
import { zustandStateStore } from "@json-render/zustand";
import { StateProvider } from "@json-render/react";
const bearStore = createStore(() => ({
count: 0,
name: "Bear",
}));
const store = zustandStateStore({ store: bearStore });
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through Zustand */}
</StateProvider>
```
### Nested Slice
```typescript
const appStore = createStore(() => ({
ui: { count: 0 },
auth: { token: null },
}));
const store = zustandStateStore({
store: appStore,
selector: (s) => s.ui,
updater: (next, s) => s.setState({ ui: next }),
});
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
+101
View File
@@ -0,0 +1,101 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/catalog")
# Catalog
The catalog defines what AI can generate. It's your guardrail.
## What is a Catalog?
A catalog is the vocabulary for your UI. While the [schema](/docs/schemas) defines the grammar (how specs are structured), the catalog defines the vocabulary (what components and actions are available). It lists:
- **Components** — UI elements AI can create (with props and optional slots)
- **Actions** — Operations AI can trigger
- **Functions** — Custom validation or transformation functions
## Creating a Catalog
`defineCatalog` is from `@json-render/core`. The `schema` import comes from your platform package (`@json-render/react` or `@json-render/react-native`) and defines the element structure the catalog targets. The catalog definition itself is framework-agnostic.
```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema'; // or '@json-render/react-native/schema'
import { z } from 'zod';
const catalog = defineCatalog(schema, {
components: {
// Define each component with its props schema
Card: {
props: z.object({
title: z.string(),
description: z.string().nullable(),
padding: z.enum(['sm', 'md', 'lg']).nullable(),
}),
slots: ["default"], // Can contain other components
description: "Container card for grouping content",
},
Metric: {
props: z.object({
label: z.string(),
value: z.union([z.string(), z.number()]),
format: z.enum(['currency', 'percent', 'number']),
}),
description: "Display a single metric value",
},
},
actions: {
submit_form: {
params: z.object({
formId: z.string(),
}),
description: 'Submit a form',
},
export_data: {
params: z.object({
format: z.enum(['csv', 'pdf', 'json']),
}),
description: 'Export data in various formats',
},
},
});
```
## Component Definition
Each component in the catalog has:
```typescript
{
props: z.object({...}), // Zod schema for props (use .nullable() for optional)
slots?: string[], // Named slots for children (e.g., ["default"])
description?: string, // Help AI understand when to use it
}
```
Use `slots: ["default"]` for components that can contain children. The slot name corresponds to where child elements are rendered.
## Generating AI Prompts
Use the `catalog.prompt()` method to generate a system prompt for AI:
```typescript
// Generate a system prompt from your catalog
const systemPrompt = catalog.prompt();
// Or with custom rules for the AI
const customPrompt = catalog.prompt({
customRules: [
"Always use Card as the root element for forms",
"Group related inputs in a Stack with direction=vertical",
],
});
// Pass this to your AI model as the system prompt
```
## Next
Learn how to [register components](/docs/registry) in your registry.
+758
View File
@@ -0,0 +1,758 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/changelog")
# Changelog
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
February 2026
### New: `@json-render/react-pdf`
PDF renderer for json-render, powered by [`@react-pdf/renderer`](https://react-pdf.org/). Define catalogs and registries the same way as `@json-render/react`, but output PDF documents instead of web UI.
```bash
npm install @json-render/core @json-render/react-pdf
```
```typescript
import { renderToBuffer } from "@json-render/react-pdf";
import type { Spec } from "@json-render/core";
const spec: Spec = {
root: "doc",
elements: {
doc: { type: "Document", props: { title: "Invoice" }, children: ["page"] },
page: {
type: "Page",
props: { size: "A4" },
children: ["heading", "table"],
},
heading: {
type: "Heading",
props: { text: "Invoice #1234", level: "h1" },
children: [],
},
table: {
type: "Table",
props: {
columns: [
{ header: "Item", width: "60%" },
{ header: "Price", width: "40%", align: "right" },
],
rows: [
["Widget A", "$10.00"],
["Widget B", "$25.00"],
],
},
children: [],
},
},
};
const buffer = await renderToBuffer(spec);
```
Server-side rendering APIs:
- `renderToBuffer(spec)` -- render to an in-memory PDF buffer
- `renderToStream(spec)` -- render to a readable stream (pipe to HTTP response)
- `renderToFile(spec, path)` -- render directly to a file
15 standard components covering document structure (Document, Page), layout (View, Row, Column), content (Heading, Text, Image, Link), data (Table, List), decorative (Divider, Spacer), and page-level (PageNumber).
Supports custom catalogs with `defineRegistry`, server-safe imports via `@json-render/react-pdf/server`, and full context support (state, visibility, actions, validation, repeat scopes).
---
## v0.7.0
February 2026
### New: `@json-render/shadcn`
Pre-built [shadcn/ui](https://ui.shadcn.com/) component library for json-render. 36 components built on Radix UI + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.
```bash
npm install @json-render/shadcn
```
```typescript
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
import { defineRegistry } from "@json-render/react";
import { shadcnComponents } from "@json-render/shadcn";
const catalog = defineCatalog(schema, {
components: {
Card: shadcnComponentDefinitions.Card,
Button: shadcnComponentDefinitions.Button,
Input: shadcnComponentDefinitions.Input,
},
actions: {},
});
const { registry } = defineRegistry(catalog, {
components: {
Card: shadcnComponents.Card,
Button: shadcnComponents.Button,
Input: shadcnComponents.Input,
},
});
```
Components include: layout (Card, Stack, Grid, Separator), navigation (Tabs, Accordion, Collapsible, Pagination), overlay (Dialog, Drawer, Tooltip, Popover, DropdownMenu), content (Heading, Text, Image, Avatar, Badge, Alert, Carousel, Table), feedback (Progress, Skeleton, Spinner), and input (Button, Link, Input, Textarea, Select, Checkbox, Radio, Switch, Slider, Toggle, ToggleGroup, ButtonGroup).
See the [API reference](/docs/api/shadcn) for full details.
### New: Event Handles (`on()`)
Components now receive an `on(event)` function in addition to `emit(event)`. The `on()` function returns an `EventHandle` with metadata:
- `emit()` -- fire the event
- `shouldPreventDefault` -- whether any action binding requested `preventDefault`
- `bound` -- whether any handler is bound to this event
```tsx
Link: ({ props, on }) => {
const click = on("click");
return (
<a href={props.href} onClick={(e) => {
if (click.shouldPreventDefault) e.preventDefault();
click.emit();
}}>{props.label}</a>
);
},
```
### New: `BaseComponentProps`
Catalog-agnostic base type for component render functions. Use when building reusable component libraries (like `@json-render/shadcn`) that are not tied to a specific catalog.
```typescript
import type { BaseComponentProps } from "@json-render/react";
const Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => (
<div>{props.title}{children}</div>
);
```
### New: Built-in Actions in Schema
Schemas can now declare `builtInActions` -- actions that are always available at runtime and automatically injected into prompts. The React schema declares `setState`, `pushState`, and `removeState` as built-in, so they appear in prompts without needing to be listed in catalog `actions`.
### New: `preventDefault` on `ActionBinding`
Action bindings now support a `preventDefault` boolean field, allowing the LLM to request that default browser behavior (e.g. navigation on links) be prevented.
### Improved: Stream Transform Text Block Splitting
`createJsonRenderTransform()` now properly splits text blocks around spec data by emitting `text-end`/`text-start` pairs. This ensures the AI SDK creates separate text parts, preserving correct interleaving of prose and UI in `message.parts`.
### Improved: `defineRegistry` Actions Requirement
`defineRegistry` now conditionally requires the `actions` field only when the catalog declares actions. Catalogs with no actions (e.g. `actions: {}`) no longer need to pass an empty actions object.
---
## v0.6.0
February 2026
### New: Chat Mode (Inline GenUI)
> **Note:** These modes were renamed in v0.12.1 — "Generate" is now "Standalone" and "Chat" is now "Inline". The old names are accepted as deprecated aliases.
json-render now supports two generation modes: **Generate** (JSONL-only, the default) and **Chat** (text + JSONL inline). Chat mode lets the AI respond conversationally with embedded UI specs, ideal for chatbots and copilot experiences.
```typescript
// Generate mode (default) — AI outputs only JSONL
const prompt = catalog.prompt();
// Chat mode — AI outputs text + JSONL inline
const chatPrompt = catalog.prompt({ mode: "chat" });
```
On the server, `pipeJsonRender()` separates text from JSONL patches in a mixed stream:
```typescript
import { pipeJsonRender } from "@json-render/core";
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
const stream = createUIMessageStream({
execute: async ({ writer }) => {
writer.merge(pipeJsonRender(result.toUIMessageStream()));
},
});
return createUIMessageStreamResponse({ stream });
```
On the client, `useJsonRenderMessage` extracts the spec and text from message parts:
```tsx
import { useJsonRenderMessage } from "@json-render/react";
function ChatMessage({ message }) {
const { spec, text, hasSpec } = useJsonRenderMessage(message.parts);
return (
<div>
{text && <Markdown>{text}</Markdown>}
{hasSpec && <Renderer spec={spec} registry={registry} />}
</div>
);
}
```
### New: AI SDK Integration
First-class Vercel AI SDK support with typed data parts and stream utilities.
- `SpecDataPart` type for `data-spec` stream parts (patch, flat, nested payloads)
- `SPEC_DATA_PART` / `SPEC_DATA_PART_TYPE` constants for type-safe part filtering
- `createJsonRenderTransform()` low-level TransformStream for custom pipelines
- `createMixedStreamParser()` for parsing mixed text + JSONL streams
### New: Two-Way Binding
Props can now use `$bindState` and `$bindItem` expressions for two-way data binding. The renderer resolves bindings and passes a `bindings` map to components, enabling write-back to state without custom `valuePath` props.
```json
{
"type": "Input",
"props": { "label": "Email", "value": { "$bindState": "/form/email" } }
}
```
```tsx
import { useBoundProp } from "@json-render/react";
Input: ({ props, bindings }) => {
const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);
return <input value={value ?? ""} onChange={(e) => setValue(e.target.value)} />;
}
```
### New: Expression-Based Props and Visibility
All dynamic expressions now use structured `$state`, `$item`, and `$index` objects instead of string token rewriting. This is simpler, more explicit, and works for both props and visibility conditions.
**Props:**
```json
{ "title": { "$state": "/user/name" } }
{ "label": { "$item": "title" } }
{ "position": { "$index": true } }
```
**Visibility:**
```json
{ "$state": "/isAdmin" }
{ "$state": "/role", "eq": "admin" }
[{ "$state": "/isAdmin" }, { "$state": "/feature" }]
{ "$or": [{ "$state": "/roleA" }, { "$state": "/roleB" }] }
{ "$item": "isActive" }
{ "$index": true, "gt": 0 }
```
Comparison operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `not`.
### New: React Chat Hooks
- `useChatUI()` — full chat hook with message history, streaming, and spec extraction
- `useJsonRenderMessage()` — extract spec + text from a message's parts array
- `buildSpecFromParts()` / `getTextFromParts()` — utilities for working with AI SDK message parts
- `useBoundProp()` — two-way binding hook for `$bindState` / `$bindItem`
### New: Chat Example
Full-featured chat example (`examples/chat`) with AI agent, tool calls (crypto, GitHub, Hacker News, weather, search), theme toggle, and streaming inline UI generation.
### Improved: Renderer Performance
- `ElementRenderer` is now `React.memo`'d for better performance with repeat lists
- `emit` is always defined (never `undefined`)
- Repeat scope passes the actual item object, eliminating string token rewriting
### Improved: Utilities
- `applySpecPatch()` — typed wrapper for applying a single patch to a Spec
- `nestedToFlat()` — convert nested tree specs to flat format
- `resolveBindings()` / `resolveActionParam()` — resolve binding paths and action params
### Breaking Changes
- `{ $path }` and `{ path }` replaced by `{ $state }`, `{ $item }`, `{ $index }` in props
- Visibility: `{ path }` -> `{ $state }`, `{ and/or/not }` -> `{ $and/$or }` with `not` as operator flag
- `DynamicValue`: `{ path: string }` -> `{ $state: string }`
- `repeat.path` -> `repeat.statePath`
- Action params: `path` -> `statePath` in setState action
- `actionHandlers` -> `handlers` on `JSONUIProvider` / `ActionProvider`
- `AuthState` and `{ auth }` visibility conditions removed (model auth as regular state)
- Legacy catalog API removed: `createCatalog`, `generateCatalogPrompt`, `generateSystemPrompt`
- React exports removed: `createRendererFromCatalog`, `rewriteRepeatTokens`
- Codegen: `traverseTree` -> `traverseSpec`
See the [Migration Guide](/docs/migration) for detailed upgrade instructions.
---
## v0.5.0
February 2026
### New: @json-render/react-native
Full React Native renderer with 25+ standard components, data binding, visibility, actions, and dynamic props. Build AI-generated native mobile UIs with the same catalog-driven approach as web.
```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} />
```
Includes standard components for layout (Container, Row, Column, ScrollContainer, SafeArea, Pressable, Spacer, Divider), content (Heading, Paragraph, Label, Image, Avatar, Badge, Chip), input (Button, TextInput, Switch, Checkbox, Slider, SearchBar), feedback (Spinner, ProgressBar), and composite (Card, ListItem, Modal).
### New: Event System
Components now use `emit` to fire named events instead of directly dispatching actions. The element's `on` field maps events to action bindings, decoupling component logic from action handling.
```tsx
// Component emits a named event
Button: ({ props, emit }) => (
<button onClick={() => emit("press")}>{props.label}</button>
),
// Element spec maps events to actions
{
"type": "Button",
"props": { "label": "Submit" },
"on": { "press": { "action": "submit", "params": { "formId": "main" } } }
}
```
### New: Repeat/List Rendering
Elements can now iterate over state arrays using the `repeat` field. Child elements use `{ "$item": "field" }` to read from the current item and `{ "$index": true }` for the current array index.
```json
{
"type": "Column",
"repeat": { "statePath": "/posts", "key": "id" },
"children": ["post-card"]
}
```
```json
{
"type": "Card",
"props": { "title": { "$item": "title" } }
}
```
### New: User Prompt Builder
Build structured user prompts with optional spec refinement and state context:
```typescript
import { buildUserPrompt } from "@json-render/core";
// Fresh generation
buildUserPrompt({ prompt: "create a todo app" });
// Refinement (patch-only mode)
buildUserPrompt({ prompt: "add a toggle", currentSpec: spec });
// With runtime state
buildUserPrompt({ prompt: "show data", state: { todos: [] } });
```
### New: Spec Validation
Validate spec structure and auto-fix common issues:
```typescript
import { validateSpec, autoFixSpec } from "@json-render/core";
const { valid, issues } = validateSpec(spec);
const fixed = autoFixSpec(spec);
```
### Improved: State Management
`DataProvider` has been renamed to `StateProvider` with a clearer API. State is now a first-class part of specs. Elements can bind to state via `$state` expressions, and the built-in `setState` action updates state directly.
### Improved: AI Prompts
Schema prompts now include streaming best practices, repeat/list examples, and state patching guidance. Schemas can also define `defaultRules` that are always included in generated prompts.
### Improved: Documentation
- All documentation pages migrated to MDX
- AI-powered documentation chat
- Dynamic Open Graph images for all docs pages
- Improved playground
### Breaking Changes
- `DataProvider` renamed to `StateProvider`
- `useData` renamed to `useStateStore`, `useDataValue` to `useStateValue`, `useDataBinding` to `useStateBinding`
- `onAction` renamed to `emit` in component context
- `DataModel` type renamed to `StateModel`
- `Action` type renamed to `ActionBinding` (old name still available but deprecated)
---
## v0.4.0
February 2026
### New: Custom Schema System
Create custom output formats with `defineSchema`. Each renderer now defines its own schema, enabling completely different spec formats for different use cases.
```typescript
import { defineSchema } from "@json-render/core";
const mySchema = defineSchema((s) => ({
spec: s.object({
pages: s.array(s.object({
title: s.string(),
blocks: s.array(s.ref("catalog.blocks")),
})),
}),
catalog: s.object({
blocks: s.map({ props: s.zod(), description: s.string() }),
}),
}), {
promptTemplate: myPromptTemplate,
});
```
### New: Component Slots
Components can now define which slots they accept. Use `["default"]` for regular children, or named slots like `["header", "footer"]` for more complex layouts.
```typescript
const catalog = defineCatalog(schema, {
components: {
Card: {
props: z.object({ title: z.string() }),
slots: ["default"], // accepts children
description: "A card container",
},
Layout: {
props: z.object({}),
slots: ["header", "content", "footer"], // named slots
description: "Page layout with header, content, footer",
},
},
});
```
### New: AI Prompt Generation
Catalogs now generate AI system prompts automatically with `catalog.prompt()`. The prompt includes all component definitions, props schemas, and action descriptions - ensuring the AI only generates valid specs.
```typescript
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
const catalog = defineCatalog(schema, {
components: { /* ... */ },
actions: { /* ... */ },
});
// Generate system prompt for AI
const systemPrompt = catalog.prompt();
// Use with any AI SDK
const result = await streamText({
model: "claude-haiku-4.5",
system: systemPrompt,
prompt: userMessage,
});
```
### New: @json-render/remotion
Generate AI-powered videos with Remotion. Define video catalogs, stream timeline specs, and render with the Remotion Player.
```tsx
import { Player } from "@remotion/player";
import { Renderer, schema, standardComponentDefinitions } from "@json-render/remotion";
const catalog = defineCatalog(schema, {
components: standardComponentDefinitions,
transitions: standardTransitionDefinitions,
});
<Player
component={Renderer}
inputProps={{ spec }}
durationInFrames={spec.composition.durationInFrames}
fps={spec.composition.fps}
compositionWidth={spec.composition.width}
compositionHeight={spec.composition.height}
/>
```
Includes 10 standard video components (TitleCard, TypingText, SplitScreen, etc.), 7 transition types, and the ClipWrapper utility for custom components.
### New: SpecStream
SpecStream is json-render's streaming format for progressively building specs from JSONL patches. The new compiler API makes it easy to process streaming AI responses.
```typescript
import { createSpecStreamCompiler } from "@json-render/core";
const compiler = createSpecStreamCompiler<MySpec>();
// Process streaming chunks
const { result, newPatches } = compiler.push(chunk);
setSpec(result); // Update UI with partial result
```
### Improved: Dashboard Example
The dashboard example is now a full-featured accounting dashboard with:
- Persistent SQLite database with Drizzle ORM
- RESTful API for customers, invoices, expenses, accounts
- Draggable widget reordering
- AI-powered widget generation with streaming
- Real data binding to database records
### Improved: Documentation
- Interactive playground for testing specs
- New guides: Custom Schema, Streaming, Code Export
- Full API reference for all packages
- Integration guides: A2UI, AG-UI, Adaptive Cards, OpenAPI
### Breaking Changes
- `UITree` type renamed to `Spec`
- Schema is now imported from renderer packages (`@json-render/react`) not core
- `defineCatalog` now requires a schema as first argument
---
## v0.3.0
January 2026
Internal release with codegen foundations.
- Added `@json-render/codegen` package (spec traversal and JSX serialization)
- Configurable AI model via environment variables
- Documentation improvements and bug fixes
*Note: Only @json-render/core was published to npm for this release.*
---
## v0.2.0
January 2026
Initial public release.
- Core catalog and spec types
- React renderer with contexts for data, actions, visibility
- AI prompt generation from catalogs
- Basic streaming support
- Dashboard example application
@@ -0,0 +1,140 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/code-export")
# Code Export
Export generated UI as standalone code for your framework.
## Overview
While json-render is designed for dynamic rendering, you can export generated UI as static code. The code generation is intentionally project-specific so you have full control over:
- Component templates (standalone, no json-render dependencies)
- Package.json and project structure
- Framework-specific patterns (Next.js, Remix, etc.)
- How data is passed to components
## Architecture
Code export is split into two parts:
### 1. @json-render/codegen (utilities)
Framework-agnostic utilities for building code generators:
```typescript
import {
traverseSpec, // Walk the UI spec
collectUsedComponents, // Get all component types used
collectStatePaths, // Get all data binding paths
collectActions, // Get all action names
serializeProps, // Convert props to JSX string
} from '@json-render/codegen';
```
### 2. Your Project (generator)
Custom code generator specific to your project and framework:
```typescript
// lib/codegen/generator.ts
import { collectUsedComponents, serializeProps } from '@json-render/codegen';
export function generateNextJSProject(spec: Spec): GeneratedFile[] {
const components = collectUsedComponents(spec);
return [
{ path: 'package.json', content: '...' },
{ path: 'app/page.tsx', content: '...' },
// ... component files
];
}
```
## Example: Next.js Export
See the dashboard example for a complete implementation that exports:
- `package.json` - Dependencies and scripts
- `tsconfig.json` - TypeScript config
- `next.config.js` - Next.js config
- `app/layout.tsx` - Root layout
- `app/globals.css` - Global styles
- `app/page.tsx` - Generated page with data
- `components/ui/*.tsx` - Standalone components
## Standalone Components
The exported components are standalone with no json-render dependencies. They receive data as props instead of using hooks:
```tsx
// Generated component (standalone)
interface MetricProps {
label: string;
statePath: string;
data?: Record<string, unknown>;
}
export function Metric({ label, statePath, data }: MetricProps) {
const value = data ? getByPath(data, statePath) : undefined;
return (
<div>
<span>{label}</span>
<span>{formatValue(value)}</span>
</div>
);
}
```
## Using the Utilities
### traverseSpec
```typescript
import { traverseSpec } from '@json-render/codegen';
traverseSpec(spec, (element, key, depth, parent) => {
console.log(' '.repeat(depth * 2) + `${key}: ${element.type}`);
});
```
### collectUsedComponents
```typescript
import { collectUsedComponents } from '@json-render/codegen';
const components = collectUsedComponents(spec);
// Set { 'Card', 'Metric', 'Chart', 'Table' }
// Generate only the needed component files
for (const component of components) {
files.push({
path: `components/ui/${component.toLowerCase()}.tsx`,
content: componentTemplates[component],
});
}
```
### serializeProps
```typescript
import { serializeProps } from '@json-render/codegen';
const propsStr = serializeProps({
title: 'Dashboard',
columns: 3,
disabled: true,
});
// 'title="Dashboard" columns={3} disabled'
```
## Try It
Run the dashboard example and click "Export Project" to see code generation in action:
```bash
cd examples/dashboard
pnpm dev
# Open http://dashboard-demo.json-render.localhost:1355
# Generate a widget, then click "Export Project"
```
@@ -0,0 +1,119 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/computed-values")
# Computed Values
Derive dynamic prop values using registered functions or string templates.
## `$template` — String Interpolation
Use `{ "$template": "..." }` to embed state values into a string. References use `${/path}` syntax where the path is a JSON Pointer:
```json
{
"type": "Text",
"props": {
"text": { "$template": "Hello, ${/user/name}! You have ${/inbox/count} messages." }
},
"children": []
}
```
If state is `{ "user": { "name": "Alice" }, "inbox": { "count": 3 } }`, the text renders as "Hello, Alice! You have 3 messages."
Missing paths resolve to an empty string.
## `$computed` — Registered Functions
Use `{ "$computed": "<name>", "args": { ... } }` to call a named function registered in your catalog. Each arg can be a literal value or any prop expression (`$state`, `$item`, `$cond`, etc.):
```json
{
"type": "Text",
"props": {
"text": {
"$computed": "fullName",
"args": {
"first": { "$state": "/form/firstName" },
"last": { "$state": "/form/lastName" }
}
}
},
"children": []
}
```
### Registering Functions
Functions are registered in the catalog and provided at runtime.
**Catalog definition (for AI prompt generation):**
```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
const catalog = defineCatalog(schema, {
components: { /* ... */ },
functions: {
fullName: {
description: 'Combines first and last name into a full name',
},
formatCurrency: {
description: 'Formats a number as currency',
},
},
});
```
**Runtime implementation:**
```tsx
import { JSONUIProvider } from '@json-render/react';
const functions = {
fullName: (args) => `${args.first ?? ''} ${args.last ?? ''}`.trim(),
formatCurrency: (args) => {
const value = Number(args.value ?? 0);
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: (args.currency as string) ?? 'USD',
}).format(value);
},
};
<JSONUIProvider registry={registry} functions={functions}>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
```
### Using with `createRenderer`
```tsx
const MyRenderer = createRenderer(catalog, components);
<MyRenderer
spec={spec}
functions={functions}
/>
```
## Combining Expressions
`$computed` args can use any expression type. This example computes a total from repeat item fields:
```json
{
"$computed": "lineTotal",
"args": {
"price": { "$item": "price" },
"quantity": { "$item": "quantity" }
}
}
```
## Next
- [Watchers](/docs/watchers) — react to state changes with cascading actions
- [Data Binding](/docs/data-binding) — all expression types
- [Validation](/docs/validation) — validate form inputs
@@ -0,0 +1,301 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/custom-schema")
# Custom Schema & Renderer
Build your own schema and renderer with `@json-render/core`.
## Overview
`@json-render/core` is schema-agnostic. While `@json-render/react` provides a ready-to-use schema and renderer, you can create your own to match any JSON structure - whether it's a domain-specific format, an existing protocol, or something entirely custom.
## 1. Define Your Schema
Start by defining the JSON structure your system will use. Here's an example of a simple dashboard schema:
```json
{
"layout": "grid",
"columns": 2,
"widgets": [
{
"type": "metric",
"title": "Revenue",
"value": "$12,345",
"trend": "up"
},
{
"type": "chart",
"title": "Sales",
"chartType": "line",
"dataKey": "salesData"
},
{
"type": "table",
"title": "Recent Orders",
"columns": ["id", "customer", "amount"],
"dataKey": "orders"
}
]
}
```
## 2. Create the Catalog
Define a catalog that describes your components and validates props using `defineCatalog` — see [Catalog](/docs/catalog).
```typescript
import { defineCatalog } from '@json-render/core';
import { z } from 'zod';
export const dashboardCatalog = defineCatalog(mySchema, {
components: {
metric: {
description: 'Displays a single metric value',
props: z.object({
title: z.string(),
value: z.string(),
trend: z.enum(['up', 'down', 'flat']).optional(),
change: z.string().optional(),
}),
},
chart: {
description: 'Renders a chart visualization',
props: z.object({
title: z.string(),
chartType: z.enum(['line', 'bar', 'pie', 'area']),
dataKey: z.string(),
height: z.number().optional(),
}),
},
table: {
description: 'Displays tabular data',
props: z.object({
title: z.string(),
columns: z.array(z.string()),
dataKey: z.string(),
pageSize: z.number().optional(),
}),
},
text: {
description: 'Displays text content',
props: z.object({
content: z.string(),
variant: z.enum(['heading', 'body', 'caption']).optional(),
}),
},
},
});
```
## 3. Define the Root Schema
Create a schema for the overall document structure:
```typescript
import { z } from 'zod';
const WidgetSchema = z.object({
type: z.string(),
title: z.string().optional(),
// Additional props validated by catalog
}).passthrough();
export const DashboardSchema = z.object({
layout: z.enum(['grid', 'stack', 'tabs']),
columns: z.number().optional(),
widgets: z.array(WidgetSchema),
});
export type Dashboard = z.infer<typeof DashboardSchema>;
export type Widget = z.infer<typeof WidgetSchema>;
```
## 4. Build the Renderer
Create a renderer that maps your schema to React components:
```tsx
import React from 'react';
import { dashboardCatalog } from './catalog';
import type { Dashboard, Widget } from './schema';
// Widget component registry
const widgetComponents: Record<string, React.FC<any>> = {
metric: ({ title, value, trend, change }) => (
<div className="p-4 rounded-lg border">
<p className="text-sm text-muted-foreground">{title}</p>
<p className="text-2xl font-bold">{value}</p>
{trend && (
<p className={`text-sm ${trend === 'up' ? 'text-green-500' : 'text-red-500'}`}>
{trend === 'up' ? '+' : '-'}{change}
</p>
)}
</div>
),
chart: ({ title, chartType, data }) => (
<div className="p-4 rounded-lg border">
<p className="font-medium mb-2">{title}</p>
<div className="h-48 bg-muted rounded flex items-center justify-center">
{/* Your chart library here */}
<span className="text-muted-foreground">{chartType} chart</span>
</div>
</div>
),
table: ({ title, columns, data }) => (
<div className="p-4 rounded-lg border">
<p className="font-medium mb-2">{title}</p>
<table className="w-full text-sm">
<thead>
<tr>
{columns.map((col: string) => (
<th key={col} className="text-left p-2 border-b">{col}</th>
))}
</tr>
</thead>
<tbody>
{data?.map((row: any, i: number) => (
<tr key={i}>
{columns.map((col: string) => (
<td key={col} className="p-2 border-b">{row[col]}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
),
text: ({ content, variant = 'body' }) => {
const className = {
heading: 'text-xl font-bold',
body: 'text-base',
caption: 'text-sm text-muted-foreground',
}[variant];
return <p className={className}>{content}</p>;
},
};
// Main renderer
export function DashboardRenderer({
spec,
data = {},
}: {
spec: Dashboard;
data?: Record<string, any>;
}) {
const layoutClass = {
grid: `grid gap-4 ${spec.columns ? `grid-cols-${spec.columns}` : 'grid-cols-2'}`,
stack: 'flex flex-col gap-4',
tabs: 'space-y-4',
}[spec.layout];
return (
<div className={layoutClass}>
{spec.widgets.map((widget, index) => {
const Component = widgetComponents[widget.type];
if (!Component) {
console.warn(`Unknown widget type: ${widget.type}`);
return null;
}
// Resolve data references
const widgetData = widget.dataKey ? data[widget.dataKey] : undefined;
return (
<Component
key={index}
{...widget}
data={widgetData}
/>
);
})}
</div>
);
}
```
## 5. Generate LLM Prompts
Use the catalog to generate system prompts for AI:
```typescript
const systemPrompt = dashboardCatalog.prompt({
customRules: [
'Use metric widgets for single KPI values',
'Use chart widgets for time-series data',
'Use table widgets for lists of records',
'Limit dashboards to 6 widgets maximum',
],
});
// Use with any LLM
const response = await generateText({
model: 'gpt-4',
system: systemPrompt,
prompt: 'Create a sales dashboard with revenue, orders, and a chart',
});
```
## 6. Validate Specs
Validate incoming specs against your schema. Use `catalog.validate()` to check AI output against the catalog's Zod schema:
```typescript
function validateDashboard(spec: unknown) {
// Validate root structure
const rootResult = DashboardSchema.safeParse(spec);
if (!rootResult.success) {
return { valid: false, errors: rootResult.error.errors };
}
// Validate each widget's props against the catalog
const result = dashboardCatalog.validate(spec);
if (!result.success) {
return { valid: false, errors: result.error.errors };
}
return { valid: true, errors: [] };
}
```
## Usage Example
```tsx
'use client';
import { useState } from 'react';
import { DashboardRenderer } from './renderer';
import type { Dashboard } from './schema';
const initialSpec: Dashboard = {
layout: 'grid',
columns: 2,
widgets: [
{ type: 'metric', title: 'Revenue', value: '$12,345', trend: 'up' },
{ type: 'metric', title: 'Orders', value: '156', trend: 'up' },
{ type: 'chart', title: 'Sales Trend', chartType: 'line', dataKey: 'sales' },
{ type: 'table', title: 'Recent Orders', columns: ['id', 'customer', 'amount'], dataKey: 'orders' },
],
};
const data = {
sales: [/* chart data */],
orders: [
{ id: '001', customer: 'Acme Inc', amount: '$500' },
{ id: '002', customer: 'Globex', amount: '$750' },
],
};
export function MyDashboard() {
const [spec, setSpec] = useState(initialSpec);
return <DashboardRenderer spec={spec} data={data} />;
}
```
## Next
See how to integrate with [A2UI](/docs/a2ui) or [Adaptive Cards](/docs/adaptive-cards) protocols.
@@ -0,0 +1,311 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/data-binding")
# Data Binding
Connect UI elements to dynamic data using expressions in your JSON specs.
## State Model
Every spec can include a `state` object that holds the data your UI reads from:
```json
{
"root": "greeting",
"elements": {
"greeting": {
"type": "Text",
"props": { "content": { "$state": "/user/name" } },
"children": []
}
},
"state": {
"user": { "name": "Alice" }
}
}
```
State can also be provided programmatically at runtime. In `@json-render/react`, this is done via `StateProvider` and hooks like `useStateStore`. See the [React API reference](/docs/api/react) for details.
## JSON Pointer Paths
All paths in json-render follow JSON Pointer (RFC 6901). A path is a string of `/`-separated tokens starting from the root:
```
Given this state:
{
"user": { "name": "Alice", "email": "alice@example.com" },
"todos": [
{ "title": "Buy milk", "done": false },
{ "title": "Walk dog", "done": true }
]
}
"/user/name" -> "Alice"
"/user/email" -> "alice@example.com"
"/todos/0/title" -> "Buy milk"
"/todos/1/done" -> true
```
## Expressions
Expressions are special objects you place in props to read dynamic values instead of hardcoding them. There are six expression types.
### `$state` — Read from state
Use `{ "$state": "/path" }` in any prop to read a value from the state model:
```json
{
"type": "Card",
"props": {
"title": { "$state": "/user/name" },
"subtitle": { "$state": "/user/email" }
},
"children": []
}
```
If state contains `{ "user": { "name": "Alice", "email": "alice@example.com" } }`, the Card renders with title "Alice" and subtitle "alice@example.com".
### `$item` — Read from the current repeat item
Use `{ "$item": "field" }` inside a [repeat](#repeat) to read a field from the current array item:
```json
{
"type": "Text",
"props": { "content": { "$item": "title" } },
"children": []
}
```
Use `{ "$item": "" }` to get the entire item object.
### `$index` — Current repeat index
Use `{ "$index": true }` inside a [repeat](#repeat) to get the current array index (zero-based number):
```json
{
"type": "Text",
"props": { "content": { "$index": true } },
"children": []
}
```
## Repeat
The `repeat` field on an element renders its children once per item in a state array. It is a top-level field on the element, sibling of `type`, `props`, and `children` — not inside `props`.
```json
{
"root": "todo-list",
"elements": {
"todo-list": {
"type": "Column",
"props": { "gap": 8 },
"repeat": { "statePath": "/todos", "key": "id" },
"children": ["todo-item"]
},
"todo-item": {
"type": "Card",
"props": {
"title": { "$item": "title" },
"subtitle": { "$item": "description" }
},
"children": []
}
},
"state": {
"todos": [
{ "id": "1", "title": "Buy milk", "description": "2% or whole" },
{ "id": "2", "title": "Walk dog", "description": "Around the park" }
]
}
}
```
- `repeat.statePath` — JSON Pointer to the state array
- `repeat.key` — field name on each item to use as a stable key for rendering
Inside `todo-item`, `{ "$item": "title" }` reads the `title` field from whichever array item is currently being rendered. `{ "$index": true }` would return `0` for the first item, `1` for the second, and so on.
## Two-Way Binding with `$bindState`
Form components use `{ "$bindState": "/path" }` on their natural value prop for two-way binding. The component reads from and writes to the state path.
### Value prop (text inputs)
```json
{
"type": "TextInput",
"props": {
"value": { "$bindState": "/form/email" },
"placeholder": "Enter your email"
},
"children": []
}
```
### Checked prop (switches, checkboxes)
```json
{
"type": "Switch",
"props": {
"label": "Enable notifications",
"checked": { "$bindState": "/settings/notifications" }
},
"children": []
}
```
### Pressed prop (toggle buttons)
```json
{
"type": "ToggleButton",
"props": {
"label": "Bold",
"pressed": { "$bindState": "/editor/bold" }
},
"children": []
}
```
## Two-Way Binding with `$bindItem`
Inside a repeat scope, use `{ "$bindItem": "field" }` to bind to a field on the current item:
```json
{
"type": "Switch",
"props": {
"label": "Done",
"checked": { "$bindItem": "completed" }
},
"children": []
}
```
Use `{ "$bindItem": "" }` to bind to the entire item.
`statePath` is not used for component binding. It remains for `repeat.statePath` (array iteration path) and action params like `setState.statePath` (target path for mutations).
## Conditional Props
Use `$cond` / `$then` / `$else` to pick a prop value based on a condition:
```json
{
"type": "Badge",
"props": {
"label": {
"$cond": { "$state": "/user/isAdmin" },
"$then": "Admin",
"$else": "Member"
}
},
"children": []
}
```
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
<div className="my-6 overflow-x-auto">
<table className="mdx-table w-full text-sm border-collapse">
<thead>
<tr>
<th>Expression</th>
<th>Syntax</th>
<th>Context</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>{"$state"}</code></td>
<td><code>{'{ "$state": "/path" }'}</code></td>
<td>Anywhere</td>
</tr>
<tr>
<td><code>{"$item"}</code></td>
<td><code>{'{ "$item": "field" }'}</code></td>
<td>Inside repeat only</td>
</tr>
<tr>
<td><code>{"$index"}</code></td>
<td><code>{'{ "$index": true }'}</code></td>
<td>Inside repeat only</td>
</tr>
<tr>
<td><code>{"$cond"}</code></td>
<td><code>{'{ "$cond": ..., "$then": ..., "$else": ... }'}</code></td>
<td>Anywhere</td>
</tr>
<tr>
<td><code>{"$bindState"}</code></td>
<td><code>{'{ "$bindState": "/path" }'}</code></td>
<td>Form components (value, checked, pressed)</td>
</tr>
<tr>
<td><code>{"$bindItem"}</code></td>
<td><code>{'{ "$bindItem": "field" }'}</code></td>
<td>Form components inside repeat</td>
</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>
</table>
</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
- [Visibility](/docs/visibility) — conditionally show or hide elements
- [Action handlers](/docs/registry#action-handlers) — respond to user interactions
- [React API reference](/docs/api/react) — React-specific hooks for programmatic state access
@@ -0,0 +1,222 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/generation-modes")
# Generation Modes
json-render supports two modes for AI-generated UI: **Standalone mode** for standalone UI and **Inline mode** for inline UI within a conversation.
The mode controls how the AI formats its output and how your app processes the stream. The underlying JSONL patch format is the same in both modes.
<GenerationModesDiagram />
## Standalone Mode
In standalone mode, the AI outputs **only JSONL patches** — no prose, no markdown. The entire response is a UI spec.
This is the default mode and is ideal for:
- Playground and builder tools
- Form generators
- Dashboard builders
- Any UI where the generated interface is the whole response
### Setup
```typescript
import { streamText } from "ai";
// Standalone mode is the default (no mode option needed)
const systemPrompt = catalog.prompt({
customRules: [
"Use Card as root for forms and small UIs.",
"Use Grid for multi-column layouts.",
],
});
const result = streamText({
model: "anthropic/claude-haiku-4.5",
system: systemPrompt,
prompt: userPrompt,
});
```
### Client
On the client, use `useUIStream` from `@json-render/react` or the lower-level `createSpecStreamCompiler` from `@json-render/core` to compile the JSONL stream into a spec:
```tsx
import { useUIStream } from "@json-render/react";
function Playground() {
const { spec, isStreaming, send } = useUIStream({
api: "/api/generate",
});
return (
<Renderer
spec={spec}
registry={registry}
loading={isStreaming}
/>
);
}
```
### Example output
The AI outputs only JSONL — one patch per line, no surrounding text:
```
{"op":"add","path":"/root","value":"card-1"}
{"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Sign In"},"children":["email","password","submit"]}}
{"op":"add","path":"/elements/email","value":{"type":"Input","props":{"label":"Email","name":"email","type":"email"}}}
{"op":"add","path":"/elements/password","value":{"type":"Input","props":{"label":"Password","name":"password","type":"password"}}}
{"op":"add","path":"/elements/submit","value":{"type":"Button","props":{"label":"Sign In"}}}
```
## Inline Mode
In inline mode, the AI responds **conversationally first**, then outputs JSONL patches on their own lines. Text-only replies are allowed when no UI is needed (e.g. greetings, clarifying questions).
This is ideal for:
- AI chatbots with rich UI responses
- Copilot experiences
- Educational assistants
- Any conversational interface where generated UI is embedded in chat messages
### Setup
```typescript
import { streamText } from "ai";
import { pipeJsonRender } from "@json-render/core";
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
// Enable inline mode
const systemPrompt = catalog.prompt({ mode: "inline" });
const result = streamText({
model: yourModel,
system: systemPrompt,
messages,
});
// In your API route, pipe the stream through pipeJsonRender
// to separate text from JSONL patches
const stream = createUIMessageStream({
execute: async ({ writer }) => {
writer.merge(pipeJsonRender(result.toUIMessageStream()));
},
});
return createUIMessageStreamResponse({ stream });
```
`pipeJsonRender` inspects each line of the AI's response. Lines that parse as JSONL patches are emitted as `data-spec` parts (which the renderer picks up). Everything else is passed through as text.
### Client
On the client, use `useJsonRenderMessage` from `@json-render/react` to extract the spec from a chat message's parts:
```tsx
import { useChat } from "@ai-sdk/react";
import { useJsonRenderMessage } from "@json-render/react";
function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
return (
<div>
{messages.map((msg) => (
<ChatMessage key={msg.id} message={msg} />
))}
{/* input form */}
</div>
);
}
function ChatMessage({ message }) {
const { spec } = useJsonRenderMessage(message.parts);
return (
<div>
{/* Render text parts */}
{message.parts
.filter((p) => p.type === "text")
.map((p, i) => <p key={i}>{p.text}</p>)}
{/* Render the generated UI inline */}
{spec && (
<Renderer
spec={spec}
registry={registry}
/>
)}
</div>
);
}
```
### Example output
The AI writes a brief explanation, then JSONL patches on their own lines:
```
Here's a dashboard showing the latest crypto prices:
{"op":"add","path":"/root","value":"dashboard"}
{"op":"add","path":"/state/prices","value":[{"name":"Bitcoin","price":98450},{"name":"Ethereum","price":3120}]}
{"op":"add","path":"/elements/dashboard","value":{"type":"Grid","props":{"columns":"2"},"children":["btc","eth"]}}
{"op":"add","path":"/elements/btc","value":{"type":"Metric","props":{"label":"Bitcoin","value":{"$state":"/prices/0/price"}}}}
{"op":"add","path":"/elements/eth","value":{"type":"Metric","props":{"label":"Ethereum","value":{"$state":"/prices/1/price"}}}}
```
If the user asks a simple question ("what does BTC stand for?"), the AI replies with text only — no JSONL.
## Quick Comparison
<div className="my-6 overflow-x-auto">
<table className="mdx-table w-full text-sm border-collapse">
<thead>
<tr>
<th />
<th>Standalone</th>
<th>Inline</th>
</tr>
</thead>
<tbody>
<tr>
<td>Output format</td>
<td>JSONL only</td>
<td>Text + JSONL</td>
</tr>
<tr>
<td>Text-only replies</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>System prompt</td>
<td><code>{"catalog.prompt()"}</code></td>
<td><code>{'catalog.prompt({ mode: "inline" })'}</code></td>
</tr>
<tr>
<td>Stream utility</td>
<td><code>{"useUIStream"}</code></td>
<td><code>{"pipeJsonRender"}</code>{" + "}<code>{"useJsonRenderMessage"}</code></td>
</tr>
<tr>
<td>Typical use case</td>
<td>Playground, builders</td>
<td>Chatbots, copilots</td>
</tr>
</tbody>
</table>
</div>
Both modes use the same JSONL patch format (RFC 6902) and the same catalog/registry system. The only difference is whether the AI is allowed to include prose alongside the patches.
## Next
- Learn about the [JSONL streaming format](/docs/streaming)
- See the [AI SDK integration](/docs/ai-sdk) for setup with the Vercel AI SDK
@@ -0,0 +1,70 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/installation")
# Installation
Install the core package plus your renderer of choice.
## For React UI
<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 Svelte
<PackageInstall packages="@json-render/core @json-render/svelte" />
Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.
<PackageInstall packages="svelte zod" />
## For React UI with shadcn/ui
Pre-built components for fast prototyping and production use:
<PackageInstall packages="@json-render/core @json-render/react @json-render/shadcn" />
Requires Tailwind CSS in your project. See the [@json-render/shadcn API reference](/docs/api/shadcn) for usage.
## For React Native
<PackageInstall packages="@json-render/core @json-render/react-native" />
## For Remotion Video
<PackageInstall packages="@json-render/core @json-render/remotion remotion @remotion/player" />
## For React Email
<PackageInstall packages="@json-render/core @json-render/react-email @react-email/components @react-email/render" />
## For External State Management (Optional)
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:
<PackageInstall packages="@json-render/zustand" />
<PackageInstall packages="@json-render/redux" />
<PackageInstall packages="@json-render/jotai" />
<PackageInstall packages="@json-render/xstate" />
See the [Data Binding](/docs/data-binding#external-store-controlled-mode) guide for usage.
## For AI Integration
To use json-render with AI models, you'll also need the Vercel AI SDK:
<PackageInstall packages="ai" />
+29
View File
@@ -0,0 +1,29 @@
import { DocsMobileNav } from "@/components/docs-mobile-nav";
import { DocsSidebar } from "@/components/docs-sidebar";
import { CopyPageButton } from "@/components/copy-page-button";
export default function DocsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<DocsMobileNav />
<div className="max-w-5xl mx-auto px-6 py-8 lg:py-12 flex gap-16">
{/* Sidebar */}
<aside className="w-48 shrink-0 hidden lg:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto">
<DocsSidebar />
</aside>
{/* Content */}
<div className="flex-1 min-w-0 max-w-2xl pb-20">
<div className="flex justify-end mb-4">
<CopyPageButton />
</div>
<article>{children}</article>
</div>
</div>
</>
);
}
+503
View File
@@ -0,0 +1,503 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/migration")
# Migration Guide
This guide covers breaking changes introduced in v0.6.0 and how to update your code.
## State Provider
`DataProvider` has been renamed to `StateProvider`, and its props have changed.
**Before:**
```tsx
import { DataProvider } from "@json-render/react";
<DataProvider data={myData} getValue={getter} setValue={setter}>
{children}
</DataProvider>
```
**After:**
```tsx
import { StateProvider } from "@json-render/react";
<StateProvider initialState={myData} onStateChange={(path, value) => console.log(path, value)}>
{children}
</StateProvider>
```
`StateProvider` now manages state internally. Use `useStateStore()` to access `get`, `set`, and `update`.
<table>
<thead>
<tr>
<th>Before</th>
<th>After</th>
</tr>
</thead>
<tbody>
<tr><td><code>DataProvider</code></td><td><code>StateProvider</code></td></tr>
<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
All dynamic value expressions have been renamed to use `$state`, `$item`, and `$index`.
**Before:**
```json
{
"type": "Text",
"props": {
"label": { "$path": "/user/name" },
"count": { "$data": "/items/length" }
}
}
```
**After:**
```json
{
"type": "Text",
"props": {
"label": { "$state": "/user/name" },
"count": { "$state": "/items/length" }
}
}
```
Inside repeat scopes, use `$item` and `$index`:
```json
{
"type": "Card",
"props": {
"title": { "$item": "name" },
"subtitle": { "$index": true }
}
}
```
<table>
<thead>
<tr>
<th>Before</th>
<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
Form components no longer use `valuePath` / `statePath` props. Instead, use `$bindState` expressions on the value prop, and `useBoundProp` in your registry.
**Before (catalog):**
```typescript
Input: {
props: z.object({
label: z.string(),
valuePath: z.string(),
placeholder: z.string().optional(),
}),
}
```
**Before (spec):**
```json
{
"type": "Input",
"props": { "label": "Email", "valuePath": "/form/email" }
}
```
**Before (registry):**
```tsx
Input: ({ props }) => {
const [value, setValue] = useStateBinding(props.valuePath);
return <input value={value ?? ""} onChange={(e) => setValue(e.target.value)} />;
}
```
**After (catalog):**
```typescript
Input: {
props: z.object({
label: z.string(),
value: z.string().optional(),
placeholder: z.string().optional(),
}),
}
```
**After (spec):**
```json
{
"type": "Input",
"props": { "label": "Email", "value": { "$bindState": "/form/email" } }
}
```
**After (registry):**
```tsx
Input: ({ props, bindings }) => {
const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);
return <input value={value ?? ""} onChange={(e) => setValue(e.target.value)} />;
}
```
`$bindState` reads from and writes to the given state path. Inside repeat scopes, use `$bindItem` to bind to a field on the current item:
```json
{
"type": "Checkbox",
"props": { "checked": { "$bindItem": "completed" } }
}
```
## Visibility Conditions
Visibility conditions have been renamed to use `$state`, `$and`, and `$or`.
**Before:**
```json
{ "path": "/isAdmin" }
{ "eq": [{ "path": "/role" }, "admin"] }
{ "and": [{ "path": "/isAdmin" }, { "path": "/feature" }] }
{ "or": [{ "path": "/roleA" }, { "path": "/roleB" }] }
```
**After:**
```json
{ "$state": "/isAdmin" }
{ "$state": "/role", "eq": "admin" }
{ "$and": [{ "$state": "/isAdmin" }, { "$state": "/feature" }] }
{ "$or": [{ "$state": "/roleA" }, { "$state": "/roleB" }] }
```
You can also use an array as shorthand for `$and`:
```json
[{ "$state": "/isAdmin" }, { "$state": "/feature" }]
```
Inside repeat scopes, use `$item` and `$index`:
```json
{ "$item": "isActive" }
{ "$index": true, "eq": 0 }
```
## Event System
Components now use `emit` to fire named events. `onAction` has been removed.
**Before:**
```tsx
Button: ({ props, onAction }) => (
<button onClick={() => onAction?.("press")}>{props.label}</button>
)
```
**After:**
```tsx
Button: ({ props, emit }) => (
<button onClick={() => emit("press")}>{props.label}</button>
)
```
`emit` is always defined (never `undefined`), so optional chaining is not needed.
## Actions Context
`dispatch` has been renamed to `execute`, and the provider prop has been renamed from `actionHandlers` to `handlers`.
**Before:**
```tsx
const { dispatch } = useActions();
dispatch({ action: "submit", params: {} });
<ActionProvider actionHandlers={myHandlers}>
```
**After:**
```tsx
const { execute } = useActions();
execute({ action: "submit", params: {} });
<ActionProvider handlers={myHandlers}>
```
## Repeat / List Rendering
The `repeat` field now uses `statePath` instead of `path`.
**Before:**
```json
{
"type": "Column",
"repeat": { "path": "/todos", "key": "id" },
"children": ["todo-item"]
}
```
**After:**
```json
{
"type": "Column",
"repeat": { "statePath": "/todos", "key": "id" },
"children": ["todo-item"]
}
```
## Catalog Creation
`createCatalog` and `generateSystemPrompt` have been replaced by `defineSchema` + `defineCatalog`.
**Before:**
```typescript
import { createCatalog, generateSystemPrompt } from "@json-render/core";
const catalog = createCatalog({
name: "my-app",
components: { /* ... */ },
actions: { /* ... */ },
});
const prompt = generateSystemPrompt(catalog);
```
**After:**
```typescript
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
const catalog = defineCatalog(schema, {
components: { /* ... */ },
actions: { /* ... */ },
});
const prompt = catalog.prompt();
// Inline mode prompt (formerly "chat")
const inlinePrompt = catalog.prompt({ mode: "inline" });
```
## Generation Modes
The generation mode values passed to `catalog.prompt()` have been renamed for clarity:
- `"generate"` is now `"standalone"`
- `"chat"` is now `"inline"`
The old names are accepted as deprecated aliases, so existing code will continue to work. Update when convenient.
**Before:**
```typescript
const prompt = catalog.prompt({ mode: "generate" });
const chatPrompt = catalog.prompt({ mode: "chat" });
```
**After:**
```typescript
const prompt = catalog.prompt({ mode: "standalone" });
const inlinePrompt = catalog.prompt({ mode: "inline" });
```
The default mode (when no `mode` option is provided) is `"standalone"`, which behaves identically to the previous `"generate"` default.
## Validation
`ValidationCheck` now uses `type` instead of `fn`, `ValidationProvider` uses `customFunctions` instead of `functions`, and `useFieldValidation` takes a config object instead of a checks array.
**Before:**
```json
{ "fn": "required", "message": "Required" }
{ "fn": "minLength", "args": { "length": 8 }, "message": "Too short" }
```
**After:**
```json
{ "type": "required", "message": "Required" }
{ "type": "minLength", "args": { "min": 8 }, "message": "Too short" }
```
<table>
<thead>
<tr>
<th>Before</th>
<th>After</th>
</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
The `auth` prop has been removed from `VisibilityProvider`. Auth state should be modeled as regular state.
**Before:**
```tsx
<VisibilityProvider auth={{ isSignedIn: true, role: "admin" }}>
```
```json
{ "auth": "signedIn" }
```
**After:**
```tsx
<StateProvider initialState={{ auth: { isSignedIn: true, role: "admin" } }}>
<VisibilityProvider>
```
```json
{ "$state": "/auth/isSignedIn" }
```
## Codegen
`traverseTree` has been renamed to `traverseSpec`, `SpecVisitor` to `TreeVisitor`, and the visitor callback now receives a `key` parameter.
**Before:**
```typescript
import { traverseTree } from "@json-render/codegen";
traverseTree(tree, (element) => {
// ...
});
```
**After:**
```typescript
import { traverseSpec } from "@json-render/codegen";
traverseSpec(spec, (element, key) => {
// ...
});
```
## Action Params
Action params in specs now use `statePath` instead of `path`.
**Before:**
```json
{
"on": {
"press": { "action": "setState", "params": { "path": "/count", "value": 0 } }
}
}
```
**After:**
```json
{
"on": {
"press": { "action": "setState", "params": { "statePath": "/count", "value": 0 } }
}
}
```
## Removed Exports
The following exports have been removed from `@json-render/core`:
<table>
<thead>
<tr>
<th>Removed</th>
<th>Replacement</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>createCatalog</code></td>
<td><code>defineCatalog(schema, config)</code></td>
</tr>
<tr>
<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>
+283
View File
@@ -0,0 +1,283 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/openapi")
# OpenAPI Integration
Use json-render to generate dynamic forms and UIs from [OpenAPI/Swagger](https://swagger.io/specification/) schemas.
<div className="rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 mb-8">
<p className="text-sm text-amber-700 dark:text-amber-300">
<strong>Concept:</strong> This page demonstrates how json-render can support OpenAPI schemas. The examples are illustrative and may require adaptation for production use.
</p>
</div>
## Why OpenAPI?
OpenAPI specifications describe your API's endpoints, request bodies, and response schemas. By converting OpenAPI schemas to json-render specs, you can:
- Automatically generate forms for API endpoints
- Display API responses with type-aware rendering
- Keep your UI in sync with your API schema
- Let AI generate UIs that match your API contracts
## Example OpenAPI Schema
A typical OpenAPI schema for a request body:
```json
{
"openapi": "3.0.0",
"paths": {
"/users": {
"post": {
"summary": "Create a new user",
"operationId": "createUser",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateUserRequest"
}
}
}
}
}
}
},
"components": {
"schemas": {
"CreateUserRequest": {
"type": "object",
"required": ["email", "name"],
"properties": {
"name": {
"type": "string",
"description": "User's full name",
"minLength": 1,
"maxLength": 100
},
"email": {
"type": "string",
"format": "email",
"description": "User's email address"
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150,
"description": "User's age"
},
"role": {
"type": "string",
"enum": ["admin", "user", "guest"],
"default": "user",
"description": "User's role"
},
"preferences": {
"type": "object",
"properties": {
"newsletter": {
"type": "boolean",
"default": false
},
"theme": {
"type": "string",
"enum": ["light", "dark", "system"]
}
}
}
}
}
}
}
}
```
## Define an OpenAPI-to-UI Catalog
Create components that map to OpenAPI data types:
```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
import { z } from 'zod';
export const openapiCatalog = defineCatalog(schema, {
components: {
Form: {
description: 'API form container',
props: z.object({
operationId: z.string(),
endpoint: z.string(),
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']),
title: z.string().optional(),
description: z.string().optional(),
}),
},
StringField: {
description: 'String input field',
props: z.object({
name: z.string(),
label: z.string(),
description: z.string().optional(),
required: z.boolean().optional(),
format: z.enum(['text', 'email', 'uri', 'uuid', 'date', 'date-time', 'password']).optional(),
minLength: z.number().optional(),
maxLength: z.number().optional(),
pattern: z.string().optional(),
placeholder: z.string().optional(),
defaultValue: z.string().optional(),
}),
},
NumberField: {
description: 'Number input field',
props: z.object({
name: z.string(),
label: z.string(),
description: z.string().optional(),
required: z.boolean().optional(),
type: z.enum(['integer', 'number']).optional(),
minimum: z.number().optional(),
maximum: z.number().optional(),
defaultValue: z.number().optional(),
}),
},
BooleanField: {
description: 'Boolean toggle field',
props: z.object({
name: z.string(),
label: z.string(),
description: z.string().optional(),
defaultValue: z.boolean().optional(),
}),
},
EnumField: {
description: 'Enum selection field',
props: z.object({
name: z.string(),
label: z.string(),
description: z.string().optional(),
required: z.boolean().optional(),
options: z.array(z.object({
value: z.string(),
label: z.string().optional(),
})),
defaultValue: z.string().optional(),
}),
},
ObjectField: {
description: 'Nested object group',
props: z.object({
name: z.string(),
label: z.string(),
description: z.string().optional(),
collapsible: z.boolean().optional(),
}),
},
},
actions: {
submit: {
description: 'Submit form to API endpoint',
params: z.object({ operationId: z.string() }),
},
reset: {
description: 'Reset form to defaults',
params: z.object({}),
},
},
});
```
## Convert OpenAPI Schema to Spec
Transform OpenAPI schemas into json-render specs by recursively walking the schema properties and mapping each type to the corresponding catalog component. The converter handles nested objects, enums, arrays, and all primitive types.
## Usage Example
```tsx
'use client';
import { OpenAPIForm } from './openapi-form';
import { operationToSpec } from './openapi-to-spec';
// Your OpenAPI schema (typically loaded from your API)
const createUserSchema = {
type: 'object',
required: ['email', 'name'],
properties: {
name: { type: 'string', description: "User's full name" },
email: { type: 'string', format: 'email', description: "User's email" },
age: { type: 'integer', minimum: 0, maximum: 150 },
role: { type: 'string', enum: ['admin', 'user', 'guest'], default: 'user' },
},
};
// Convert to spec
const spec = operationToSpec(
'createUser',
'POST',
'/api/users',
createUserSchema,
'Create User',
'Add a new user to the system',
);
export function CreateUserForm() {
const handleSubmit = async (data: Record<string, unknown>) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (response.ok) {
console.log('User created!');
}
};
return <OpenAPIForm spec={spec} onSubmit={handleSubmit} />;
}
```
## Auto-generating from OpenAPI Document
Load and parse an OpenAPI document to generate forms for all operations:
```typescript
import SwaggerParser from '@apidevtools/swagger-parser';
import { operationToSpec } from './openapi-to-spec';
export async function loadOpenAPISpecs(specUrl: string) {
const api = await SwaggerParser.dereference(specUrl);
const specs: Record<string, any> = {};
for (const [path, methods] of Object.entries(api.paths)) {
for (const [method, operation] of Object.entries(methods)) {
if (!operation.requestBody?.content?.['application/json']?.schema) continue;
const schema = operation.requestBody.content['application/json'].schema;
const operationId = operation.operationId || `${method}_${path.replace(/\//g, '_')}`;
specs[operationId] = operationToSpec(
operationId,
method,
path,
schema,
operation.summary,
operation.description,
);
}
}
return specs;
}
// Usage
const specs = await loadOpenAPISpecs('https://api.example.com/openapi.json');
// specs.createUser, specs.updateUser, etc.
```
## Next
Learn about [streaming](/docs/streaming) for progressive UI rendering.
+99
View File
@@ -0,0 +1,99 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs")
# Introduction
json-render is a framework for **Generative UI** — AI-generated interfaces that are safe, predictable, and render natively on any platform.
## What is Generative UI?
Most AI integrations treat the interface as fixed. Developers build layouts ahead of time, and AI fills in the data — a chatbot response, a summary, a recommendation. The UI itself never changes.
**Generative UI is different.** The AI generates the interface itself: which components to show, how to arrange them, what data to bind, what actions to wire up. Every response can produce a unique, purpose-built UI tailored to the user's request.
The challenge is that unconstrained AI output is unpredictable. It can hallucinate component names, produce invalid structures, or generate unsafe code. You need a way to let AI be creative with layout and composition while keeping it within boundaries you control.
That is what json-render does. You define a **catalog** of components and actions. AI generates JSON constrained to that catalog. Your components render the result natively — on web or mobile — with full type safety and no arbitrary code execution.
## How json-render Works
### 1. Define your catalog
A catalog declares what AI can use: components with typed props, actions with typed params.
```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
import { z } from 'zod';
export const catalog = defineCatalog(schema, {
components: {
Card: {
props: z.object({ title: z.string() }),
slots: ["default"],
},
Metric: {
props: z.object({
label: z.string(),
value: z.string(),
}),
},
},
});
```
### 2. AI generates a spec
Given a prompt like "show me a revenue dashboard", AI outputs a JSON spec — a flat tree of elements constrained to your catalog:
```json
{
"root": "card-1",
"elements": {
"card-1": {
"type": "Card",
"props": { "title": "Revenue Dashboard" },
"children": ["metric-1", "metric-2"]
},
"metric-1": {
"type": "Metric",
"props": { "label": "Total Revenue", "value": "$48,200" }
},
"metric-2": {
"type": "Metric",
"props": { "label": "Growth", "value": "+12%" }
}
}
}
```
### 3. Your components render it
Map catalog types to real components with a registry, then render the spec:
```tsx
import { Renderer, StateProvider, VisibilityProvider } from '@json-render/react';
<StateProvider initialState={{}}>
<VisibilityProvider>
<Renderer spec={spec} registry={registry} />
</VisibilityProvider>
</StateProvider>
```
The result is a native UI built from your own components — not an iframe, not markdown, not generated code. The AI chose the structure; you control everything else.
## Key Concepts
- **[Catalog](/docs/catalog)** — Define the components, actions, and validation functions AI can use. This is the contract between your app and the AI.
- **[Registry](/docs/registry)** — Map catalog types to platform-specific implementations. React components on web, React Native views on mobile.
- **[Specs](/docs/specs)** — The JSON output AI generates. A flat tree of typed elements with props, children, data bindings, and visibility conditions.
- **[Streaming](/docs/streaming)** — Render progressively as the AI responds. Each JSONL patch adds to the spec and the UI updates in real time.
- **[Data Binding](/docs/data-binding)** — Bind props to runtime data with `$state` paths, repeat elements over arrays, and wire two-way input bindings.
- **[Visibility](/docs/visibility)** — Show or hide elements based on state conditions. The AI can generate conditional UIs without writing logic.
- **[Generation Modes](/docs/generation-modes)** — Standalone mode for full-page generated UI or inline mode for UI embedded in a conversation.
## Next
- [Installation](/docs/installation) — Add json-render to your project
- [Quick Start](/docs/quick-start) — Build your first generative UI in 5 minutes
@@ -0,0 +1,214 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/quick-start")
# Quick Start
Get up and running with json-render in 5 minutes.
## 1. Define your catalog
Create a catalog that defines what components AI can use:
```typescript
// lib/catalog.ts
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
import { z } from 'zod';
export const catalog = defineCatalog(schema, {
components: {
Card: {
props: z.object({
title: z.string(),
description: z.string().nullable(),
}),
slots: ["default"],
description: "Container card with optional title",
},
Button: {
props: z.object({
label: z.string(),
action: z.string().nullable(),
}),
description: "Clickable button that triggers an action",
},
Text: {
props: z.object({
content: z.string(),
}),
description: "Text paragraph",
},
},
actions: {
submit: {
params: z.object({ formId: z.string() }),
description: "Submit a form",
},
navigate: {
params: z.object({ url: z.string() }),
description: "Navigate to a URL",
},
},
});
```
## 2. Define your components
Use `defineRegistry` to map catalog types to React components. Each component receives type-safe `props`, `children`, and `emit`:
```tsx
// lib/registry.tsx
import { defineRegistry } from '@json-render/react';
import { catalog } from './catalog';
export const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
<div className="p-4 border rounded-lg">
<h2 className="font-bold">{props.title}</h2>
{props.description && (
<p className="text-gray-600">{props.description}</p>
)}
{children}
</div>
),
Button: ({ props, emit }) => (
<button
className="px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => emit("press")}
>
{props.label}
</button>
),
Text: ({ props }) => (
<p>{props.content}</p>
),
},
});
```
## 3. Create an API route
Set up a streaming API route for AI generation:
```typescript
// app/api/generate/route.ts
import { streamText } from 'ai';
import { catalog } from '@/lib/catalog';
export async function POST(req: Request) {
const { prompt } = await req.json();
// Generate system prompt from catalog
const systemPrompt = catalog.prompt();
const result = streamText({
model: 'anthropic/claude-haiku-4.5',
system: systemPrompt,
prompt,
});
return result.toTextStreamResponse();
}
```
## 4. Render the UI
Use providers and the `Renderer` with your registry to display AI-generated UI:
```tsx
// app/page.tsx
'use client';
import { Renderer, StateProvider, ActionProvider, VisibilityProvider, ValidationProvider, useUIStream } from '@json-render/react';
import { registry } from '@/lib/registry';
export default function Page() {
const { spec, isStreaming, send } = useUIStream({
api: '/api/generate',
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
send(formData.get('prompt') as string);
};
return (
<StateProvider initialState={{}}>
<VisibilityProvider>
<ActionProvider handlers={{
submit: (params) => console.log('Submit:', params),
navigate: (params) => console.log('Navigate:', params),
}}>
<ValidationProvider customFunctions={{}}>
<form onSubmit={handleSubmit}>
<input
name="prompt"
placeholder="Describe what you want..."
className="border p-2 rounded"
/>
<button type="submit" disabled={isStreaming}>
Generate
</button>
</form>
<div className="mt-8">
<Renderer spec={spec} registry={registry} loading={isStreaming} />
</div>
</ValidationProvider>
</ActionProvider>
</VisibilityProvider>
</StateProvider>
);
}
```
## Quick Start with shadcn/ui
If you want to skip defining components from scratch, use `@json-render/shadcn` for 36 pre-built components:
```typescript
// lib/catalog.ts
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
import { shadcnComponentDefinitions } from '@json-render/shadcn/catalog';
export const catalog = defineCatalog(schema, {
components: {
Card: shadcnComponentDefinitions.Card,
Stack: shadcnComponentDefinitions.Stack,
Heading: shadcnComponentDefinitions.Heading,
Button: shadcnComponentDefinitions.Button,
Input: shadcnComponentDefinitions.Input,
},
actions: {},
});
```
```tsx
// lib/registry.tsx
import { defineRegistry } from '@json-render/react';
import { shadcnComponents } from '@json-render/shadcn';
import { catalog } from './catalog';
export const { registry } = defineRegistry(catalog, {
components: {
Card: shadcnComponents.Card,
Stack: shadcnComponents.Stack,
Heading: shadcnComponents.Heading,
Button: shadcnComponents.Button,
Input: shadcnComponents.Input,
},
});
```
See the [@json-render/shadcn API reference](/docs/api/shadcn) for the full component list.
## Next steps
- Learn about [catalogs](/docs/catalog) in depth
- Explore [data binding](/docs/data-binding) for dynamic values
- Add [action handlers](/docs/registry#action-handlers) for interactivity
- Implement [conditional visibility](/docs/visibility)
- Use [pre-built shadcn/ui components](/docs/api/shadcn) for fast prototyping
+330
View File
@@ -0,0 +1,330 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/registry")
# Registry
A registry maps your [catalog](/docs/catalog) definitions to platform-specific implementations. The catalog defines *what* AI can generate — the registry provides the *how*.
What a registry contains depends on the schema you use. Each package defines its own schema, which determines the shape of both the catalog and the registry.
- **`@json-render/react`** — Components (React elements) and action handlers
- **`@json-render/react-native`** — Components (React Native elements) and action handlers
- **`@json-render/react-email`** — Email components (React Email / HTML)
- **`@json-render/remotion`** — Clip components, transitions, and effects
## @json-render/react
### defineRegistry
Use `defineRegistry` to create a type-safe registry from your catalog. Pass your components, actions, or both:
```tsx
import { defineRegistry } from '@json-render/react';
import { myCatalog } from './catalog';
export const { registry, handlers, executeAction } = defineRegistry(myCatalog, {
components: {
Card: ({ props, children }) => (
<div className="card">
<h2>{props.title}</h2>
{props.description && <p>{props.description}</p>}
{children}
</div>
),
Button: ({ props, emit }) => (
<button onClick={() => emit("press")}>
{props.label}
</button>
),
},
actions: {
submit_form: async (params, setState) => {
const res = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(params),
});
const result = await res.json();
setState((prev) => ({ ...prev, formResult: result }));
},
export_data: async (params) => {
const blob = await generateExport(params.format);
downloadBlob(blob, `export.${params.format}`);
},
},
});
```
The returned object contains:
- `registry` — component registry for `<Renderer />`
- `handlers` — factory for ActionProvider-compatible handlers
- `executeAction` — imperative action dispatch (for use outside the React tree)
### Component Props
Each component receives a `ComponentContext` object:
```typescript
interface ComponentContext {
props: T; // Type-safe props from your catalog
children?: React.ReactNode; // Rendered children (for slot components)
emit: (event: string) => void; // Emit a named event (always defined)
on: (event: string) => EventHandle; // Get event handle with metadata
loading?: boolean; // Whether the renderer is in a loading state
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
}
```
Props are automatically inferred from your catalog, so `props.title` is typed as `string` if your catalog defines it that way.
Use `emit("press")` for simple event firing. Use `on("click")` when you need to inspect event metadata:
```tsx
Link: ({ props, on }) => {
const click = on("click");
return (
<a
href={props.href}
onClick={(e) => {
if (click.shouldPreventDefault) e.preventDefault();
click.emit();
}}
>
{props.label}
</a>
);
},
```
#### Using `bindings` for two-way binding
When a spec uses `{ "$bindState": "/path" }` or `{ "$bindItem": "field" }` on a prop, the renderer resolves the **value** into `props` and provides the **write-back path** in `bindings`. Use the `useBoundProp` hook to wire both together:
```tsx
import { useBoundProp, defineRegistry } from '@json-render/react';
// Inside your registry:
TextInput: ({ props, bindings }) => {
const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);
return (
<input
value={value ?? ""}
onChange={(e) => setValue(e.target.value)}
/>
);
},
```
`useBoundProp` returns `[resolvedValue, setter]`. The setter writes to the bound state path. If no binding exists (the prop is a literal), the setter is a no-op.
### Action Handlers
Instead of AI generating arbitrary code, it declares *intent* by name. Your application provides the implementation. This is a core guardrail.
Actions are declared in your [catalog](/docs/catalog). The `@json-render/react` schema supports an `actions` key where you define what operations AI can trigger:
```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
import { z } from 'zod';
const catalog = defineCatalog(schema, {
components: { /* ... */ },
actions: {
submit_form: {
params: z.object({
formId: z.string(),
}),
description: 'Submit a form',
},
export_data: {
params: z.object({
format: z.enum(['csv', 'pdf', 'json']),
}),
},
navigate: {
params: z.object({
url: z.string(),
}),
},
},
});
```
Action handlers receive `(params, setState, state)` and are defined inside `defineRegistry`:
```tsx
export const { handlers, executeAction } = defineRegistry(catalog, {
actions: {
submit_form: async (params, setState) => {
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ formId: params.formId }),
});
const result = await response.json();
setState((prev) => ({ ...prev, formResult: result }));
},
export_data: async (params) => {
const blob = await generateExport(params.format);
downloadBlob(blob, `export.${params.format}`);
},
navigate: (params) => {
window.location.href = params.url;
},
},
});
```
### Data Binding
Most data binding is handled automatically by the renderer — `$state`, `$item`, and `$index` expressions in props are resolved before your component receives them. See the [Data Binding](/docs/data-binding) guide for the full reference.
For two-way binding (form inputs), use `{ "$bindState": "/path" }` on the natural value prop (or `{ "$bindItem": "field" }` inside repeat scopes). The renderer provides a `bindings` map with the state path for each bound prop. Use `useBoundProp` to get `[value, setValue]`:
```tsx
import { useBoundProp } from '@json-render/react';
// Inside defineRegistry components:
Input: ({ props, bindings }) => {
const [value, setValue] = useBoundProp<string>(
props.value,
bindings?.value
);
return (
<input
value={value ?? ''}
onChange={(e) => setValue(e.target.value)}
placeholder={props.placeholder}
/>
);
},
```
For read-only state access (e.g. displaying a value from state), use `$state` expressions in props — they are resolved before the component receives them. For custom logic, use `useStateStore` and `getByPath` from `@json-render/core`.
### Using the Renderer
Wire everything together with providers and the `<Renderer />` component:
```tsx
import { useMemo, useRef } from 'react';
import {
Renderer,
StateProvider,
VisibilityProvider,
ActionProvider,
} from '@json-render/react';
import { registry, handlers } from './registry';
function App({ spec, state, setState }) {
const stateRef = useRef(state);
const setStateRef = useRef(setState);
stateRef.current = state;
setStateRef.current = setState;
const actionHandlers = useMemo(
() => handlers(() => setStateRef.current, () => stateRef.current),
[],
);
return (
<StateProvider initialState={state}>
<VisibilityProvider>
<ActionProvider handlers={actionHandlers}>
<Renderer spec={spec} registry={registry} />
</ActionProvider>
</VisibilityProvider>
</StateProvider>
);
}
```
## @json-render/react-native
`@json-render/react-native` uses the same `defineRegistry` API. The only difference is that components return React Native elements instead of HTML:
```tsx
import { defineRegistry } from '@json-render/react-native';
import { View, Text, Pressable } from 'react-native';
export const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
<View style={styles.card}>
<Text style={styles.title}>{props.title}</Text>
{children}
</View>
),
Button: ({ props, emit }) => (
<Pressable onPress={() => emit("press")}>
<Text>{props.label}</Text>
</Pressable>
),
},
});
```
See the [@json-render/react-native API reference](/docs/api/react-native) for the full API.
## @json-render/react-email
`@json-render/react-email` uses `defineRegistry` like React and React Native. Components render to React Email primitives (`@react-email/components`). Use `renderToHtml` or `renderToPlainText` for server-side email output:
```tsx
import { defineRegistry } from '@json-render/react-email';
import { renderToHtml } from '@json-render/react-email';
import { Body, Container, Heading, Text } from '@react-email/components';
export const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
<Container style={{ padding: 16, backgroundColor: '#fff' }}>
<Heading>{props.title}</Heading>
{children}
</Container>
),
},
});
const html = await renderToHtml(spec, { registry });
```
See the [@json-render/react-email API reference](/docs/api/react-email) for the full API.
## @json-render/remotion
`@json-render/remotion` takes a different approach. Instead of `defineRegistry`, it uses a plain component registry with built-in standard components for video production:
```tsx
import { Renderer, standardComponents } from '@json-render/remotion';
// Use the standard components directly
<Renderer spec={timelineSpec} components={standardComponents} />
// Or extend with your own
const components = {
...standardComponents,
CustomSlide: ({ clip }) => <AbsoluteFill>{/* ... */}</AbsoluteFill>,
};
```
The Remotion schema also supports `transitions` and `effects` in the catalog rather than actions.
See the [@json-render/remotion API reference](/docs/api/remotion) for the full API.
## Next
Learn about [data binding](/docs/data-binding) for dynamic values.
+268
View File
@@ -0,0 +1,268 @@
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>Svelte</td>
<td>
<code>@json-render/svelte</code>
</td>
<td>Svelte 5 component tree</td>
</tr>
<tr>
<td>Solid</td>
<td>
<code>@json-render/solid</code>
</td>
<td>SolidJS component tree</td>
</tr>
<tr>
<td>shadcn/ui</td>
<td>
<code>@json-render/shadcn</code>
</td>
<td>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.
## Svelte
Svelte 5 renderer with runes-compatible context helpers, visibility conditions, actions, and streaming support.
```typescript
import { defineRegistry, Renderer } from "@json-render/svelte";
import { schema } from "@json-render/svelte/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => /* Svelte snippet */,
},
});
```
See the [@json-render/svelte API reference](/docs/api/svelte) for details.
## Solid
SolidJS renderer with fine-grained reactivity, state bindings, validation, visibility, and event-driven actions.
```tsx
import { defineRegistry, Renderer } from "@json-render/solid";
import { schema } from "@json-render/solid/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: (renderProps) => <div>{renderProps.children}</div>,
},
});
<Renderer spec={spec} registry={registry} />;
```
See the [@json-render/solid API reference](/docs/api/solid) for details.
## shadcn/ui
36 pre-built components using Radix UI and Tailwind CSS. Built on top of `@json-render/react` -- no custom renderer needed.
```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.
+148
View File
@@ -0,0 +1,148 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/schemas")
# Schemas
Schemas define the structure and validation rules for your UI specs.
## What is a Schema?
A schema defines the JSON structure that describes your UI. It includes:
- **Element structure** — How components are nested and referenced
- **Property types** — What props each component accepts
- **Data binding syntax** — How to reference dynamic data
- **Action format** — How user interactions are defined
## Schema-Agnostic by Design
json-render can work with any JSON schema. `@json-render/core` provides the primitives to define catalogs and renderers for any format:
- **@json-render/react** — The built-in flat element tree schema
- **[A2UI](/docs/a2ui)** — Google's Agent-to-User Interaction protocol
- **[Adaptive Cards](/docs/adaptive-cards)** — Microsoft's platform-agnostic UI format
- **AG-UI** — CopilotKit's Agent User Interaction Protocol
- **OpenAPI/Swagger** — API documentation schemas for dynamic forms
- **Custom schemas** — Design your own format tailored to your domain
See the [Custom Schema guide](/docs/custom-schema) to learn how to implement support for any schema.
## Built-in Schema
`@json-render/react` uses a flat element tree schema with a root key and elements map:
```json
{
"root": "card-1",
"elements": {
"card-1": {
"type": "Card",
"props": { "title": "Dashboard" },
"children": ["text-1", "button-1"]
},
"text-1": {
"type": "Text",
"props": { "content": { "$state": "/user/name" } },
"children": []
},
"button-1": {
"type": "Button",
"props": { "label": "Click me" },
"children": []
}
}
}
```
## Schema Components
### Element Structure
In the built-in schema, each element in the elements map has this structure:
```typescript
interface Element {
type: string; // Component type from catalog
props: Record<string, any>; // Component properties
children: string[]; // Array of child element keys
visible?: VisibilityCondition; // Conditional display
}
```
### Data Binding Syntax
Reference dynamic data using `$state` expressions in props. The value is a JSON Pointer path into the state model:
```json
{
"type": "Text",
"props": {
"content": { "$state": "/user/name" },
"count": { "$state": "/items/count" }
},
"children": []
}
```
json-render also supports `$item` and `$index` expressions for lists, two-way binding via `$bindState` / `$bindItem`, and conditional props. See [Data Binding](/docs/data-binding) for the full reference.
### Action Format
Actions are defined in the catalog and referenced from components. The renderer handles action execution:
```typescript
// In your catalog
actions: {
navigate: {
params: z.object({ url: z.string() }),
description: 'Navigate to a URL',
},
apiCall: {
params: z.object({
endpoint: z.string(),
method: z.enum(['GET', 'POST', 'PUT', 'DELETE']),
}),
description: 'Make an API request',
},
}
```
## Custom Schemas
`@json-render/core` is schema-agnostic. You can define any JSON structure:
```typescript
import { z } from 'zod';
// Define your own element schema
const MyElementSchema = z.object({
component: z.string(),
settings: z.record(z.unknown()),
nested: z.array(z.lazy(() => MyElementSchema)).optional(),
});
// Define your own data binding format
const BoundValue = z.object({
literal: z.string().optional(),
source: z.string().optional(), // e.g., "/users/0/name"
});
// Define your own action format
const ActionSchema = z.object({
name: z.string(),
context: z.record(z.unknown()).optional(),
});
```
## Schema vs Catalog
The schema and catalog work together but serve different purposes:
- **Schema** — Defines the JSON structure (how elements are organized)
- **Catalog** — Defines available components and their props (what can be used)
The schema is the grammar; the catalog is the vocabulary.
## Next
Learn about [specs](/docs/specs) — the actual JSON documents that describe your UI.
+123
View File
@@ -0,0 +1,123 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/skills");
# Skills
json-render ships with skills that teach AI coding agents how to use each package. Install a skill and your agent in Cursor, Claude Code, or Codex can generate json-render UIs without manual guidance.
## Available Skills
- **core** — Core schemas, catalogs, and AI prompt generation.
- **react** — React renderer that turns JSON specs into React component trees.
- **react-pdf** — PDF renderer using `@react-pdf/renderer`.
- **react-email** — Email renderer that produces HTML or plain-text emails.
- **react-native** — React Native renderer for native mobile UIs.
- **shadcn** — Pre-built shadcn/ui components (Radix UI + Tailwind).
- **image** — Image renderer that turns JSON specs into SVG and PNG via Satori.
- **remotion** — Remotion renderer for video generation from JSON timeline specs.
- **vue** — Vue 3 renderer for Vue component trees.
- **svelte** — Svelte 5 renderer for Svelte component trees.
- **solid** — SolidJS renderer for fine-grained reactive component trees.
- **codegen** — Code generation utilities for building custom exporters.
- **mcp** — MCP Apps integration for Claude, ChatGPT, Cursor, and VS Code.
- **redux** — Redux adapter for json-render's `StateStore` interface.
- **zustand** — Zustand adapter for json-render's `StateStore` interface.
- **jotai** — Jotai adapter for json-render's `StateStore` interface.
- **xstate** — XState Store adapter for json-render's `StateStore` interface.
## Installation
```bash
npx skills add vercel-labs/json-render --skill core
npx skills add vercel-labs/json-render --skill react
npx skills add vercel-labs/json-render --skill react-pdf
npx skills add vercel-labs/json-render --skill react-email
npx skills add vercel-labs/json-render --skill react-native
npx skills add vercel-labs/json-render --skill shadcn
npx skills add vercel-labs/json-render --skill image
npx skills add vercel-labs/json-render --skill remotion
npx skills add vercel-labs/json-render --skill vue
npx skills add vercel-labs/json-render --skill svelte
npx skills add vercel-labs/json-render --skill solid
npx skills add vercel-labs/json-render --skill codegen
npx skills add vercel-labs/json-render --skill mcp
npx skills add vercel-labs/json-render --skill redux
npx skills add vercel-labs/json-render --skill zustand
npx skills add vercel-labs/json-render --skill jotai
npx skills add vercel-labs/json-render --skill xstate
```
After installing, your AI agent will automatically activate the right skill when it encounters a matching request.
## core
The foundational skill. Teaches agents how to define catalogs, create schemas, build specs, and generate AI prompts. This is the starting point for any json-render project and covers `defineCatalog`, `defineSchema`, `specSchema`, `toPrompt`, and the full spec format.
## react
Teaches agents how to render JSON specs as React component trees using `JsonRender`, `JsonRenderClient`, and `useJsonRender`. Covers custom component registries, client-side interactivity, state management, and streaming integration.
## react-pdf
Teaches agents how to generate PDFs from JSON specs using `@react-pdf/renderer`. Covers the PDF-specific component registry, page layout, and styling.
## react-email
Teaches agents how to render JSON specs as HTML or plain-text emails using React Email components. Covers the email-specific registry and rendering pipeline.
## react-native
Teaches agents how to render JSON specs as native mobile UIs with React Native. Covers the native component registry and platform-specific considerations.
## shadcn
Teaches agents how to use the pre-built shadcn/ui component registry with json-render. Includes Radix UI primitives, Tailwind styling, and the full set of available shadcn components.
## image
Teaches agents how to turn JSON specs into SVG and PNG images using Satori. Covers the image-specific registry, dimensions, fonts, and rendering options.
## remotion
Teaches agents how to generate videos from JSON timeline specs using Remotion. Covers compositions, sequences, timeline structure, and video rendering.
## vue
Teaches agents how to render JSON specs as Vue 3 component trees. Covers the Vue renderer API, custom component registries, and reactivity integration.
## svelte
Teaches agents how to render JSON specs as Svelte 5 component trees. Covers the Svelte renderer API and component registration.
## solid
Teaches agents how to render JSON specs as SolidJS component trees. Covers Solid-specific reactive patterns, provider wiring, bindings, actions, and streaming.
## codegen
Teaches agents how to use code generation utilities to export UI specs as framework-specific source code. Covers the codegen pipeline and custom exporter creation.
## mcp
Teaches agents how to build MCP Apps that serve json-render UIs inside AI tools like Claude, ChatGPT, Cursor, and VS Code. Covers MCP server setup, tool definitions, and UI streaming.
## redux
Teaches agents how to connect a Redux store to json-render's `StateStore` interface for state-driven UIs.
## zustand
Teaches agents how to connect a Zustand store to json-render's `StateStore` interface for lightweight state management.
## jotai
Teaches agents how to connect Jotai atoms to json-render's `StateStore` interface for atomic state management.
## xstate
Teaches agents how to connect an XState Store to json-render's `StateStore` interface for state-machine-driven UIs.
## Source
All skill files are in the [`skills/`](https://github.com/vercel-labs/json-render/tree/main/skills) directory of the repository.
+293
View File
@@ -0,0 +1,293 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/specs")
# Specs
A spec is a JSON document that describes your UI.
## What is a Spec?
A spec (specification) is the actual JSON that describes a UI. It uses components from a [catalog](/docs/catalog) and can optionally follow a [schema](/docs/schemas). Specs can be:
- Generated by AI in real-time
- Stored in a database
- Streamed progressively from a server
- Hand-authored as JSON files
json-render is schema-agnostic — your specs can follow any JSON structure you choose.
## Example Specs
### Simple Spec
A basic spec using the `@json-render/react` schema. Note the flat structure with a `root` key and `elements` map:
```json
{
"root": "card-1",
"elements": {
"card-1": {
"type": "Card",
"props": { "title": "Welcome" },
"children": ["text-1"]
},
"text-1": {
"type": "Text",
"props": { "content": { "$state": "/user/greeting" } },
"children": []
}
}
}
```
### Complex Spec
A more complex spec with multiple nested elements:
```json
{
"root": "card-1",
"elements": {
"card-1": {
"type": "Card",
"props": { "title": "User Profile", "padding": "md" },
"children": ["row-1", "button-1"]
},
"row-1": {
"type": "Row",
"props": { "gap": "md" },
"children": ["avatar-1", "stack-1"]
},
"avatar-1": {
"type": "Avatar",
"props": { "src": { "$state": "/user/avatar" }, "alt": { "$state": "/user/name" } },
"children": []
},
"stack-1": {
"type": "Stack",
"props": { "gap": "sm" },
"children": ["name-text", "email-text"]
},
"name-text": {
"type": "Text",
"props": { "content": { "$state": "/user/name" }, "variant": "heading" },
"children": []
},
"email-text": {
"type": "Text",
"props": { "content": { "$state": "/user/email" }, "variant": "caption" },
"children": []
},
"button-1": {
"type": "Button",
"props": { "label": "Edit Profile" },
"children": []
}
}
}
```
### Block-Level Spec
A high-level spec using semantic blocks for page layouts:
```json
{
"root": "page",
"elements": {
"page": {
"type": "Page",
"props": {},
"children": ["header", "hero", "features", "footer"]
},
"header": {
"type": "Header",
"props": { "logo": "/logo.svg", "navItems": ["Products", "Pricing", "Docs"] },
"children": []
},
"hero": {
"type": "Hero",
"props": {
"title": "Build UIs with JSON",
"subtitle": "Let AI generate your interfaces",
"ctaLabel": "Get Started",
"ctaHref": "/docs"
},
"children": []
},
"features": {
"type": "Features",
"props": { "columns": 3 },
"children": ["feature-1", "feature-2", "feature-3"]
},
"feature-1": {
"type": "Feature",
"props": { "icon": "zap", "title": "Fast", "description": "Render UIs in milliseconds" },
"children": []
},
"feature-2": {
"type": "Feature",
"props": { "icon": "shield", "title": "Secure", "description": "Validate all specs against your catalog" },
"children": []
},
"feature-3": {
"type": "Feature",
"props": { "icon": "sparkles", "title": "AI-Ready", "description": "Generate prompts from your catalog" },
"children": []
},
"footer": {
"type": "Footer",
"props": { "copyright": "2025 Acme Inc", "links": ["Privacy", "Terms", "Contact"] },
"children": []
}
}
}
```
## Spec Anatomy
Specs are schema-agnostic — the JSON structure is entirely up to you. The examples below use the `root` + `elements` flat tree format from the `@json-render/react` schema, which is optimized for AI generation and streaming.
### Root and Elements
In the React schema, a spec has a `root` key pointing to the entry element, and an `elements` map containing all elements:
```json
{
"root": "card-1",
"elements": {
"card-1": {
"type": "Card",
"props": { "title": "My Card" },
"children": ["text-1"]
},
"text-1": { ... }
}
}
```
### Element Structure
Each element in the map has a consistent shape:
```json
{
"type": "ComponentName",
"props": { "label": "Hello" },
"children": ["child-1", "child-2"]
}
```
- `type` — Component type from your catalog
- `props` — Component properties
- `children` — Array of child element keys
### Dynamic Data
Props can reference data from the state model using `$state` expressions. The value is a JSON Pointer (RFC 6901) path into the state:
```json
{
"type": "Metric",
"props": {
"label": "Total Revenue",
"value": { "$state": "/metrics/revenue" },
"change": { "$state": "/metrics/revenueChange" }
},
"children": []
}
```
See [Data Binding](/docs/data-binding) for the full reference including `$item`, `$index`, repeat, and two-way binding.
### Conditional Visibility
Control when elements appear using the `visible` property:
```json
{
"type": "Alert",
"props": {
"message": "You have unsaved changes"
},
"children": [],
"visible": {
"$state": "/form/isDirty",
"eq": true
}
}
```
## Working with Specs
### Validating a Spec
Use `validateSpec` from `@json-render/core` to check a spec for structural issues:
```typescript
import { validateSpec } from '@json-render/core';
const result = validateSpec(spec);
if (!result.valid) {
console.error('Invalid spec:', result.issues);
}
```
### Rendering a Spec (React)
With `@json-render/react`, wrap the `Renderer` in providers to supply state and visibility:
```tsx
import { Renderer, StateProvider, VisibilityProvider } from '@json-render/react';
import { registry } from './registry';
function MyApp({ spec, initialState }) {
return (
<StateProvider initialState={initialState}>
<VisibilityProvider>
<Renderer spec={spec} registry={registry} />
</VisibilityProvider>
</StateProvider>
);
}
```
See the [@json-render/react API reference](/docs/api/react) for full provider and hook documentation.
### Streaming a Spec (React)
With `@json-render/react`, use the `useUIStream` hook to stream specs incrementally:
```tsx
import { useUIStream } from '@json-render/react';
function GenerativeUI() {
const { spec, isStreaming } = useUIStream({
api: '/api/generate',
});
return (
<Renderer
spec={spec}
registry={registry}
loading={isStreaming}
/>
);
}
```
See [Streaming](/docs/streaming) for the full SpecStream format and server-side setup.
## Spec Sources
Specs can come from various sources:
- **AI Generation** — LLMs generate specs based on prompts and catalog
- **Database** — Store specs as JSON and load dynamically
- **API Response** — Server returns specs based on user/context
- **Static Files** — Pre-built specs for known UI patterns
## Next
Learn about [catalogs](/docs/catalog) — the vocabulary of components available in your specs.
+171
View File
@@ -0,0 +1,171 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/streaming")
# Streaming
Progressively render UI as AI generates it.
## SpecStream Format
json-render uses **SpecStream**, a JSONL-based streaming format where each line is a JSON patch operation that progressively builds your spec:
```json
{"op":"add","path":"/root","value":"root"}
{"op":"add","path":"/elements/root","value":{"type":"Card","props":{"title":"Dashboard"},"children":["metric-1","metric-2"]}}
{"op":"add","path":"/elements/metric-1","value":{"type":"Metric","props":{"label":"Revenue"}}}
{"op":"add","path":"/elements/metric-2","value":{"type":"Metric","props":{"label":"Users"}}}
```
## Patch Operations (RFC 6902)
SpecStream uses [RFC 6902 JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations:
- `add` — Add a value at a path (creates or replaces for objects, inserts for arrays)
- `remove` — Remove the value at a path
- `replace` — Replace an existing value at a path
- `move` — Move a value from one path to another (requires `from`)
- `copy` — Copy a value from one path to another (requires `from`)
- `test` — Assert that a value at a path equals the given value
## Path Format
Paths follow JSON Pointer (RFC 6901) into the spec object:
```bash
/root -> Root element key (string)
/elements/card-1 -> Element with key "card-1"
/elements/card-1/props -> Props of card-1
/elements/card-1/children -> Children of card-1
```
## Server-Side Setup
Ensure your API route streams properly:
```typescript
import { streamText } from 'ai';
import { catalog } from '@/lib/catalog';
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamText({
model: 'anthropic/claude-haiku-4.5',
system: catalog.prompt(),
prompt,
});
// Return as a streaming response
return result.toTextStreamResponse();
}
```
## Low-Level SpecStream API
For custom or framework-agnostic streaming implementations, use the SpecStream compiler from `@json-render/core` directly:
```typescript
import { createSpecStreamCompiler } from '@json-render/core';
// Create a compiler for your spec type
const compiler = createSpecStreamCompiler<MySpec>();
const decoder = new TextDecoder();
// Process streaming chunks from AI
async function processStream(reader: ReadableStreamDefaultReader<Uint8Array>) {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Decode the Uint8Array chunk to a string
const chunk = decoder.decode(value, { stream: true });
const { result, newPatches } = compiler.push(chunk);
if (newPatches.length > 0) {
// Update UI with partial result
setSpec(result);
}
}
// Get final compiled result
return compiler.getResult();
}
```
### One-Shot Compilation
For non-streaming scenarios, compile entire SpecStream at once:
```typescript
import { compileSpecStream } from '@json-render/core';
const jsonl = `{"op":"add","path":"/root","value":"card-1"}
{"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Hello"},"children":[]}}`;
const spec = compileSpecStream<Spec>(jsonl);
// { root: "card-1", elements: { "card-1": { type: "Card", props: { title: "Hello" }, children: [] } } }
```
## Usage with React
`@json-render/react` provides the `useUIStream` hook, which wraps the low-level compiler in a React-friendly API with state management, error handling, and abort support.
### useUIStream Hook
```tsx
import { useUIStream } from '@json-render/react';
function App() {
const {
spec, // Current UI spec state
isStreaming, // True while streaming
error, // Any error that occurred
send, // Function to start generation
clear, // Function to reset spec and error
} = useUIStream({
api: '/api/generate',
onComplete: (spec) => {}, // Optional: called when streaming completes
onError: (error) => {}, // Optional: called when an error occurs
});
}
```
### Progressive Rendering
The Renderer automatically updates as the spec changes:
```tsx
function App() {
const { spec, isStreaming } = useUIStream({ api: '/api/generate' });
return (
<div>
{isStreaming && <LoadingIndicator />}
<Renderer spec={spec} registry={registry} loading={isStreaming} />
</div>
);
}
```
### Aborting Streams
Calling `send` again automatically aborts the previous request. Use `clear` to reset the spec and error state:
```tsx
function App() {
const { isStreaming, send, clear } = useUIStream({
api: '/api/generate',
});
return (
<div>
<button onClick={() => send('Create dashboard')}>
Generate
</button>
<button onClick={clear}>Reset</button>
</div>
);
}
```
See the [@json-render/react API reference](/docs/api/react) for full `useUIStream` documentation.
@@ -0,0 +1,263 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/validation")
# Validation
Validate form inputs with built-in and custom functions.
## Built-in Validators
json-render includes common validation functions:
- `required` — Value must be non-empty
- `email` — Valid email format
- `minLength` — Minimum string length (args: `{ "min": N }`)
- `maxLength` — Maximum string length (args: `{ "max": N }`)
- `pattern` — Match a regex pattern (args: `{ "pattern": "regex" }`)
- `min` — Minimum numeric value (args: `{ "min": N }`)
- `max` — Maximum numeric value (args: `{ "max": N }`)
- `numeric` — Value must be a number
- `url` — Valid URL format
- `matches` — Must equal another field (args: `{ "other": { "$state": "/path" } }`)
- `equalTo` — Alias for matches (args: `{ "other": { "$state": "/path" } }`)
- `lessThan` — Value must be less than another field (args: `{ "other": { "$state": "/path" } }`)
- `greaterThan` — Value must be greater than another field (args: `{ "other": { "$state": "/path" } }`)
- `requiredIf` — Required only when another field is truthy (args: `{ "field": { "$state": "/path" } }`)
## Using Validation in JSON
Use `{ "$bindState": "/path" }` on the value prop for two-way binding. Validation checks run against the value at the bound path (available as `bindings?.value` in components):
```json
{
"type": "TextField",
"props": {
"label": "Email",
"value": { "$bindState": "/form/email" },
"checks": [
{ "type": "required", "message": "Email is required" },
{ "type": "email", "message": "Invalid email format" }
],
"validateOn": "blur"
}
}
```
## Validation with Parameters
```json
{
"type": "TextField",
"props": {
"label": "Password",
"value": { "$bindState": "/form/password" },
"checks": [
{ "type": "required", "message": "Password is required" },
{
"type": "minLength",
"args": { "min": 8 },
"message": "Password must be at least 8 characters"
},
{
"type": "pattern",
"args": { "pattern": "[A-Z]" },
"message": "Must contain at least one uppercase letter"
}
]
}
}
```
## Custom Validation Functions
Define custom validators in your catalog's `functions` field. The catalog itself is framework-agnostic — only the `schema` import varies by platform:
```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema'; // or '@json-render/react-native/schema'
import { z } from 'zod';
const catalog = defineCatalog(schema, {
components: { /* ... */ },
functions: {
isValidPhone: {
description: 'Validates phone number format',
},
isUniqueEmail: {
description: 'Checks if email is not already registered',
},
},
});
```
## Usage with React
In `@json-render/react`, use `ValidationProvider` to supply implementations for your custom validators:
```tsx
import { ValidationProvider } from '@json-render/react';
function App() {
const customValidators = {
isValidPhone: (value) => {
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
return phoneRegex.test(value);
},
isUniqueEmail: async (value) => {
const response = await fetch(`/api/check-email?email=${value}`);
const { available } = await response.json();
return available;
},
};
return (
<ValidationProvider customFunctions={customValidators}>
{/* Your UI */}
</ValidationProvider>
);
}
```
### Using in Components
The `useFieldValidation` and `useBoundProp` hooks wire validation into your registry components. Validation uses the path from `bindings?.value` (the bound state path):
```tsx
import { useFieldValidation, useBoundProp } from '@json-render/react';
function TextField({ props, bindings }) {
const [value, setValue] = useBoundProp(props.value, bindings?.value);
const { errors, isValid, validate, touch, clear } = useFieldValidation(
bindings?.value ?? null,
{ checks: props.checks, validateOn: props.validateOn }
);
return (
<div>
<label>{props.label}</label>
<input
value={value || ''}
onChange={(e) => setValue(e.target.value)}
onBlur={() => validate()}
/>
{errors.map((error, i) => (
<p key={i} className="text-red-500 text-sm">{error}</p>
))}
</div>
);
}
```
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
Control when validation runs with `validateOn`:
- `change` — Validate on every input change
- `blur` — Validate when field loses focus (default for Input, Textarea)
- `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
- [Computed Values](/docs/computed-values) — derive dynamic prop values
- [Watchers](/docs/watchers) — react to state changes
- [Generation Modes](/docs/generation-modes) — how AI generates specs
@@ -0,0 +1,339 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/visibility")
# Visibility
Conditionally show or hide components based on state values and logic.
## State-Based Visibility
Show/hide based on state values. Use `$state` with a JSON Pointer path:
```json
{
"type": "Alert",
"props": { "message": "Form has errors" },
"visible": { "$state": "/form/hasErrors" }
}
```
Visible when `/form/hasErrors` is truthy.
### Negation
Use `not: true` to invert a condition:
```json
{
"type": "WelcomeBanner",
"visible": { "$state": "/user/hasSeenWelcome", "not": true }
}
```
Visible when `/user/hasSeenWelcome` is falsy.
## Auth-Based Visibility
Show/hide based on authentication state. Expose your auth state in the state model (e.g. at `/auth/isSignedIn`):
```json
{
"type": "AdminPanel",
"visible": { "$state": "/auth/isSignedIn" }
}
```
For signed-out only:
```json
{
"type": "LoginPrompt",
"visible": { "$state": "/auth/isSignedIn", "not": true }
}
```
## Comparison Operators
Compare a state value to a literal or another state path. Use **one operator per condition** -- if multiple are provided, only the first one is evaluated (precedence: `eq` > `neq` > `gt` > `gte` > `lt` > `lte`). Add `"not": true` to invert the result of any condition.
```json
// Equal
{
"visible": { "$state": "/user/role", "eq": "admin" }
}
// Not equal
{
"visible": { "$state": "/tab", "neq": "home" }
}
// Greater than
{
"visible": { "$state": "/cart/total", "gt": 100 }
}
// Greater than or equal
{
"visible": { "$state": "/cart/itemCount", "gte": 1 }
}
// Less than
{
"visible": { "$state": "/cart/total", "lt": 1000 }
}
// Less than or equal
{
"visible": { "$state": "/cart/itemCount", "lte": 10 }
}
```
Comparison values can be literals or state references:
```json
{
"visible": { "$state": "/user/balance", "gte": { "$state": "/order/minimum" } }
}
```
## Combining Conditions (AND)
Place multiple conditions in an array for implicit AND:
```json
{
"type": "SubmitButton",
"visible": [
{ "$state": "/form/isValid" },
{ "$state": "/form/hasChanges" }
]
}
```
All conditions must be true for the element to be visible.
## OR Conditions
Use `$or` when at least one condition should be true:
```json
{
"type": "SpecialOffer",
"visible": { "$or": [
{ "$state": "/user/isVIP" },
{ "$state": "/cart/total", "gt": 200 }
]}
}
```
Visible when the user is VIP **or** the cart total exceeds 200. `$or` can contain any visibility conditions, including nested arrays (AND) and comparisons.
## Explicit AND
Use `$and` when you need to nest AND logic inside `$or`:
```json
{
"type": "PromoCard",
"visible": { "$or": [
{ "$and": [
{ "$state": "/user/isVIP" },
{ "$state": "/cart/total", "gt": 50 }
]},
{ "$state": "/promo/active" }
]}
}
```
For top-level AND, the implicit array form is simpler: `[condition, condition]`. Use `$and` only when nesting inside `$or`.
## Always / Never
Use boolean literals for constant visibility:
```json
{
"type": "Footer",
"visible": true
}
```
```json
{
"type": "DeprecatedPanel",
"visible": false
}
```
## Repeat-Scoped Conditions
Inside a [repeat](/docs/data-binding#repeat), use `$item` and `$index` conditions to show/hide based on the current item:
### `$item` — Condition on item field
```json
{
"type": "Badge",
"props": { "label": "Overdue" },
"visible": { "$item": "isOverdue" }
}
```
With comparison:
```json
{
"type": "DiscountTag",
"visible": { "$item": "price", "gt": 100 }
}
```
### `$index` — Condition on array index
```json
{
"type": "Divider",
"visible": { "$index": true, "gt": 0 }
}
```
This shows the divider for every item except the first (index 0).
`$item` and `$index` conditions support the same comparison operators as `$state` (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `not`).
## Complex Example
```json
{
"type": "RefundButton",
"props": { "label": "Process Refund" },
"visible": [
{ "$state": "/auth/isSignedIn" },
{ "$state": "/user/role", "eq": "support" },
{ "$state": "/order/amount", "gt": 0 },
{ "$state": "/order/isRefunded", "not": true }
]
}
```
## Quick Reference
<div className="my-6 overflow-x-auto">
<table className="mdx-table w-full text-sm border-collapse">
<thead>
<tr>
<th>Condition</th>
<th>Syntax</th>
</tr>
</thead>
<tbody>
<tr>
<td>Truthiness</td>
<td><code>{'{ "$state": "/path" }'}</code></td>
</tr>
<tr>
<td>Falsy (not)</td>
<td><code>{'{ "$state": "/path", "not": true }'}</code></td>
</tr>
<tr>
<td>Equal</td>
<td><code>{'{ "$state": "/path", "eq": value }'}</code></td>
</tr>
<tr>
<td>Not equal</td>
<td><code>{'{ "$state": "/path", "neq": value }'}</code></td>
</tr>
<tr>
<td>Greater than</td>
<td><code>{'{ "$state": "/path", "gt": number }'}</code></td>
</tr>
<tr>
<td>Greater or equal</td>
<td><code>{'{ "$state": "/path", "gte": number }'}</code></td>
</tr>
<tr>
<td>Less than</td>
<td><code>{'{ "$state": "/path", "lt": number }'}</code></td>
</tr>
<tr>
<td>Less or equal</td>
<td><code>{'{ "$state": "/path", "lte": number }'}</code></td>
</tr>
<tr>
<td>Item field (repeat)</td>
<td><code>{'{ "$item": "field" }'}</code></td>
</tr>
<tr>
<td>Item comparison</td>
<td><code>{'{ "$item": "field", "eq": value }'}</code></td>
</tr>
<tr>
<td>Index (repeat)</td>
<td><code>{'{ "$index": true, "gt": 0 }'}</code></td>
</tr>
<tr>
<td>AND (implicit)</td>
<td><code>{"[ condition, condition ]"}</code></td>
</tr>
<tr>
<td>AND (explicit)</td>
<td><code>{'{ "$and": [ condition, condition ] }'}</code></td>
</tr>
<tr>
<td>OR</td>
<td><code>{'{ "$or": [ condition, condition ] }'}</code></td>
</tr>
<tr>
<td>Always</td>
<td><code>{"true"}</code></td>
</tr>
<tr>
<td>Never</td>
<td><code>{"false"}</code></td>
</tr>
</tbody>
</table>
</div>
Comparison values can be literals or state references for state-to-state comparisons:
```json
{ "$state": "/a", "eq": { "$state": "/b" } }
```
## Usage with React
In `@json-render/react`, wrap your app with `VisibilityProvider` to enable conditional rendering. The `Renderer` handles visibility automatically — elements with unmet conditions are not rendered.
```tsx
import { VisibilityProvider, StateProvider } from '@json-render/react';
function App() {
return (
<StateProvider initialState={data}>
<VisibilityProvider>
{/* Components can now use visibility conditions */}
</VisibilityProvider>
</StateProvider>
);
}
```
For advanced use cases, the `useIsVisible` hook lets you evaluate visibility conditions programmatically:
```tsx
import { useIsVisible } from '@json-render/react';
function ConditionalContent({ condition, children }) {
const isVisible = useIsVisible(condition);
if (!isVisible) return null;
return <div>{children}</div>;
}
```
See the [@json-render/react API reference](/docs/api/react) for full details.
## Next
Learn about [form validation](/docs/validation).
+167
View File
@@ -0,0 +1,167 @@
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
+14
View File
@@ -0,0 +1,14 @@
import { Header } from "@/components/header";
export default function MainLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1">{children}</main>
</div>
);
}
@@ -9,12 +9,16 @@ export default function Home() {
<>
{/* Hero */}
<section className="max-w-5xl mx-auto px-6 pt-24 pb-16 text-center">
<h1 className="text-5xl sm:text-6xl md:text-7xl font-bold tracking-tighter mb-6">
Predictable. Guardrailed. Fast.
<p className="text-xs sm:text-sm font-medium text-muted-foreground tracking-widest uppercase mb-4">
The Generative UI Framework
</p>
<h1 className="text-4xl sm:text-6xl md:text-7xl font-bold tracking-tighter mb-6">
AI → json-render → UI
</h1>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto mb-12 leading-relaxed">
Let users generate dashboards, widgets, apps, and data visualizations
from prompts — safely constrained to components you define.
Generate dynamic, personalized UIs from prompts without sacrificing
reliability. Predefined components and actions for safe, predictable
output.
</p>
<Demo />
@@ -65,10 +69,10 @@ export default function Home() {
<div className="text-xs text-muted-foreground font-mono mb-3">
02
</div>
<h3 className="text-lg font-semibold mb-2">Users Prompt</h3>
<h3 className="text-lg font-semibold mb-2">AI Generates</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
End users describe what they want. AI generates JSON constrained
to your catalog.
Describe what you want. AI generates JSON constrained to your
catalog. Every interface is unique.
</p>
</div>
<div>
@@ -96,10 +100,12 @@ export default function Home() {
<p className="text-muted-foreground mb-6">
Components, actions, and validation functions.
</p>
<Code lang="typescript">{`import { createCatalog } from '@json-render/core';
<Code lang="typescript">{`import { defineSchema, defineCatalog } from '@json-render/core';
import { z } from 'zod';
export const catalog = createCatalog({
const schema = defineSchema({ /* ... */ });
export const catalog = defineCatalog(schema, {
components: {
Card: {
props: z.object({
@@ -111,7 +117,7 @@ export const catalog = createCatalog({
Metric: {
props: z.object({
label: z.string(),
valuePath: z.string(),
statePath: z.string(),
format: z.enum(['currency', 'percent']),
}),
},
@@ -127,23 +133,24 @@ export const catalog = createCatalog({
Constrained output that your components render natively.
</p>
<Code lang="json">{`{
"key": "dashboard",
"type": "Card",
"props": {
"title": "Revenue Dashboard",
"description": null
},
"children": [
{
"key": "revenue",
"root": "dashboard",
"elements": {
"dashboard": {
"type": "Card",
"props": {
"title": "Revenue Dashboard"
},
"children": ["revenue"]
},
"revenue": {
"type": "Metric",
"props": {
"label": "Total Revenue",
"valuePath": "/metrics/revenue",
"statePath": "/metrics/revenue",
"format": "currency"
}
}
]
}
}`}</Code>
</div>
</div>
@@ -170,25 +177,22 @@ export const catalog = createCatalog({
"root": "card",
"elements": {
"card": {
"key": "card",
"type": "Card",
"props": { "title": "Revenue" },
"children": ["metric", "chart"]
},
"metric": {
"key": "metric",
"type": "Metric",
"props": {
"label": "Total Revenue",
"valuePath": "analytics/revenue",
"statePath": "analytics/revenue",
"format": "currency"
}
},
"chart": {
"key": "chart",
"type": "Chart",
"props": {
"dataPath": "analytics/salesByRegion"
"statePath": "analytics/salesByRegion"
}
}
}
@@ -221,10 +225,10 @@ export default function Page() {
<Metric
data={data}
label="Total Revenue"
valuePath="analytics/revenue"
statePath="analytics/revenue"
format="currency"
/>
<Chart data={data} dataPath="analytics/salesByRegion" />
<Chart data={data} statePath="analytics/salesByRegion" />
</Card>
);
}`}</Code>
@@ -246,6 +250,10 @@ export default function Page() {
<h2 className="text-2xl font-semibold mb-12 text-center">Features</h2>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-8">
{[
{
title: "Generative UI",
desc: "Generate dynamic, personalized interfaces from prompts with AI",
},
{
title: "Guardrails",
desc: "AI can only use components you define in the catalog",
@@ -255,20 +263,16 @@ export default function Page() {
desc: "Progressive rendering as JSON streams from the model",
},
{
title: "Code Export",
desc: "Export as standalone React code with no runtime dependencies",
title: "React & React Native",
desc: "Render on web and mobile from the same catalog and spec format",
},
{
title: "Data Binding",
desc: "Two-way binding with JSON Pointer paths",
desc: "Connect props to state with $state, $item, $index, and two-way binding",
},
{
title: "Actions",
desc: "Named actions handled by your application",
},
{
title: "Visibility",
desc: "Conditional show/hide based on data or auth",
title: "Code Export",
desc: "Export as standalone React code with no runtime dependencies",
},
].map((feature) => (
<div key={feature.title}>
+131
View File
@@ -0,0 +1,131 @@
import { readFile } from "fs/promises";
import { join } from "path";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import type { ModelMessage, UIMessage } from "ai";
import { createBashTool } from "bash-tool";
import { headers } from "next/headers";
import { allDocsPages } from "@/lib/docs-navigation";
import { mdxToCleanMarkdown } from "@/lib/mdx-to-markdown";
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
export const maxDuration = 60;
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render, a library for AI-generated UI with guardrails.
GitHub repository: https://github.com/vercel-labs/json-render
Documentation: https://json-render.dev/docs
npm packages: @json-render/core, @json-render/react, @json-render/vue, @json-render/svelte, @json-render/solid, @json-render/shadcn, @json-render/react-three-fiber, @json-render/react-native, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp, @json-render/redux, @json-render/zustand, @json-render/jotai, @json-render/xstate, @json-render/yaml
Skills: json-render ships AI agent skills that teach coding agents how to use each package. Install with "npx skills add vercel-labs/json-render --skill <name>". Available skills: core, react, react-pdf, react-email, react-native, shadcn, react-three-fiber, image, remotion, vue, svelte, solid, codegen, mcp, redux, zustand, jotai, xstate, yaml. See /docs/skills for details.
You have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory.
When answering questions:
- Use the bash tool to list files (ls /workspace/docs/) or search for content (grep -r "keyword" /workspace/docs/)
- Use the readFile tool to read specific documentation pages (e.g. readFile with path "/workspace/docs/index.md")
- Do NOT use bash to write, create, modify, or delete files (no tee, cat >, sed -i, echo >, cp, mv, rm, mkdir, touch, etc.) — you are read-only
- Always base your answers on the actual documentation content
- Be concise and accurate
- If the docs don't cover a topic, say so honestly
- Do NOT include source references or file paths in your response
- Do NOT use emojis in your responses`;
async function loadDocsFiles(): Promise<Record<string, string>> {
const files: Record<string, string> = {};
const results = await Promise.allSettled(
allDocsPages.map(async (page) => {
const slug =
page.href === "/docs" ? "" : page.href.replace(/^\/docs\/?/, "");
const filePath = slug
? join(
process.cwd(),
"app",
"(main)",
"docs",
...slug.split("/"),
"page.mdx",
)
: join(process.cwd(), "app", "(main)", "docs", "page.mdx");
const raw = await readFile(filePath, "utf-8");
const md = mdxToCleanMarkdown(raw);
const fileName = slug ? `/docs/${slug}.md` : "/docs/index.md";
return { fileName, md };
}),
);
for (const result of results) {
if (result.status === "fulfilled") {
files[result.value.fileName] = result.value.md;
}
}
return files;
}
function addCacheControl(messages: ModelMessage[]): ModelMessage[] {
if (messages.length === 0) return messages;
return messages.map((message, index) => {
if (index === messages.length - 1) {
return {
...message,
providerOptions: {
...message.providerOptions,
anthropic: { cacheControl: { type: "ephemeral" } },
},
};
}
return message;
});
}
export async function POST(req: Request) {
const headersList = await headers();
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
const [minuteResult, dailyResult] = await Promise.all([
minuteRateLimit.limit(ip),
dailyRateLimit.limit(ip),
]);
if (!minuteResult.success || !dailyResult.success) {
const isMinuteLimit = !minuteResult.success;
return new Response(
JSON.stringify({
error: "Rate limit exceeded",
message: isMinuteLimit
? "Too many requests. Please wait a moment before trying again."
: "Daily limit reached. Please try again tomorrow.",
}),
{
status: 429,
headers: { "Content-Type": "application/json" },
},
);
}
const { messages }: { messages: UIMessage[] } = await req.json();
const docsFiles = await loadDocsFiles();
const {
tools: { bash, readFile },
} = await createBashTool({ files: docsFiles });
const result = streamText({
model: DEFAULT_MODEL,
system: SYSTEM_PROMPT,
messages: await convertToModelMessages(messages),
stopWhen: stepCountIs(5),
tools: {
bash,
readFile,
},
prepareStep: ({ messages: stepMessages }) => ({
messages: addCacheControl(stepMessages),
}),
});
return result.toUIMessageStreamResponse();
}
+55
View File
@@ -0,0 +1,55 @@
import { readFile } from "fs/promises";
import { join } from "path";
import { NextRequest, NextResponse } from "next/server";
import { mdxToCleanMarkdown } from "@/lib/mdx-to-markdown";
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const docPath = searchParams.get("path");
if (!docPath) {
return NextResponse.json(
{ error: "Missing ?path= parameter" },
{ status: 400 },
);
}
// Sanitize path: only allow docs paths, no traversal
const normalized = docPath
.replace(/^\//, "")
.replace(/\.\./g, "")
.replace(/[^a-zA-Z0-9/-]/g, "");
if (!normalized.startsWith("docs")) {
return NextResponse.json({ error: "Invalid path" }, { status: 400 });
}
// Map URL path to file path
// /docs -> /app/(main)/docs/page.mdx
// /docs/installation -> /app/(main)/docs/installation/page.mdx
const slug = normalized === "docs" ? "" : normalized.replace(/^docs\/?/, "");
const filePath = slug
? join(
process.cwd(),
"app",
"(main)",
"docs",
...slug.split("/"),
"page.mdx",
)
: join(process.cwd(), "app", "(main)", "docs", "page.mdx");
try {
const raw = await readFile(filePath, "utf-8");
const markdown = mdxToCleanMarkdown(raw);
return new NextResponse(markdown, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
"Cache-Control": "public, max-age=3600",
},
});
} catch {
return NextResponse.json({ error: "Page not found" }, { status: 404 });
}
}
+134 -81
View File
@@ -1,96 +1,149 @@
import { streamText } from "ai";
import { headers } from "next/headers";
import type { Spec, EditMode } from "@json-render/core";
import {
buildUserPrompt,
buildEditUserPrompt,
isNonEmptySpec,
} from "@json-render/core";
import { yamlPrompt } from "@json-render/yaml";
import { stringify as yamlStringify } from "yaml";
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
import { playgroundCatalog } from "@/lib/render/catalog";
export const maxDuration = 30;
const SYSTEM_PROMPT = `You are a UI generator that outputs JSONL (JSON Lines) patches.
const PLAYGROUND_RULES = [
"NEVER use viewport height classes (min-h-screen, h-screen) - the UI renders inside a fixed-size container.",
"NEVER use page background colors (bg-gray-50) - the container has its own background.",
"For forms or small UIs: use Card as root with maxWidth:'sm' or 'md' and centered:true.",
"For content-heavy UIs (blogs, dashboards, product listings): use Stack or Grid as root. Use Grid with 2-3 columns for card layouts.",
"Wrap each repeated item in a Card for visual separation and structure.",
"Use realistic, professional sample data. Include 3-5 items with varied content. Never leave state arrays empty.",
'For form inputs (Input, Textarea, Select), always include checks for validation (e.g. required, email, minLength). Always pair checks with a $bindState expression on the value prop (e.g. { "$bindState": "/path" }).',
];
AVAILABLE COMPONENTS (22):
Layout:
- Card: { title?: string, description?: string, maxWidth?: "sm"|"md"|"lg"|"full", centered?: boolean } - Container card for content sections. Has children. Use for forms/content boxes, NOT for page headers.
- Stack: { direction?: "horizontal"|"vertical", gap?: "sm"|"md"|"lg" } - Flex container. Has children.
- Grid: { columns?: 2|3|4, gap?: "sm"|"md"|"lg" } - Grid layout. Has children. ALWAYS use mobile-first: set columns:1 and use className for larger screens.
- Divider: {} - Horizontal separator line
Form Inputs:
- Input: { label: string, name: string, type?: "text"|"email"|"password"|"number", placeholder?: string } - Text input
- Textarea: { label: string, name: string, placeholder?: string, rows?: number } - Multi-line text
- Select: { label: string, name: string, options: string[], placeholder?: string } - Dropdown select
- Checkbox: { label: string, name: string, checked?: boolean } - Checkbox input
- Radio: { label: string, name: string, options: string[] } - Radio button group
- Switch: { label: string, name: string, checked?: boolean } - Toggle switch
Actions:
- Button: { label: string, variant?: "primary"|"secondary"|"danger", actionText?: string } - Clickable button. actionText is shown in toast on click (defaults to label)
- Link: { label: string, href: string } - Anchor link
Typography:
- Heading: { text: string, level?: 1|2|3|4 } - Heading text (h1-h4)
- Text: { content: string, variant?: "body"|"caption"|"muted" } - Paragraph text
Data Display:
- Image: { src: string, alt: string, width?: number, height?: number } - Image
- Avatar: { src?: string, name: string, size?: "sm"|"md"|"lg" } - User avatar with fallback initials
- Badge: { text: string, variant?: "default"|"success"|"warning"|"danger" } - Status badge
- Alert: { title: string, message?: string, type?: "info"|"success"|"warning"|"error" } - Alert banner
- Progress: { value: number, max?: number, label?: string } - Progress bar (value 0-100)
- Rating: { value: number, max?: number, label?: string } - Star rating display
Charts:
- BarGraph: { title?: string, data: Array<{label: string, value: number}> } - Vertical bar chart
- LineGraph: { title?: string, data: Array<{label: string, value: number}> } - Line chart with points
OUTPUT FORMAT (JSONL):
{"op":"set","path":"/root","value":"element-key"}
{"op":"add","path":"/elements/key","value":{"key":"...","type":"...","props":{...},"children":[...]}}
ALL COMPONENTS support: className?: string[] - array of Tailwind classes for custom styling
RULES:
1. First line sets /root to root element key
2. Add elements with /elements/{key}
3. Children array contains string keys, not objects
4. Parent first, then children
5. Each element needs: key, type, props
6. Use className for custom Tailwind styling when needed
FORBIDDEN CLASSES (NEVER USE):
- min-h-screen, h-screen, min-h-full, h-full, min-h-dvh, h-dvh - viewport heights break the small render container
- bg-gray-50, bg-slate-50 or any page background colors - container already has background
MOBILE-FIRST RESPONSIVE:
- ALWAYS design mobile-first. Single column on mobile, expand on larger screens.
- Grid: Use columns:1 prop, add className:["sm:grid-cols-2"] or ["md:grid-cols-3"] for larger screens
- DO NOT put page headers/titles inside Card - use Stack with Heading directly
- Horizontal stacks that may overflow should use className:["flex-wrap"]
- For forms (login, signup, contact): Card should be the root element, NOT wrapped in a centering Stack
EXAMPLE (Blog with responsive grid):
{"op":"set","path":"/root","value":"page"}
{"op":"add","path":"/elements/page","value":{"key":"page","type":"Stack","props":{"direction":"vertical","gap":"lg"},"children":["header","posts"]}}
{"op":"add","path":"/elements/header","value":{"key":"header","type":"Stack","props":{"direction":"vertical","gap":"sm"},"children":["title","desc"]}}
{"op":"add","path":"/elements/title","value":{"key":"title","type":"Heading","props":{"text":"My Blog","level":1}}}
{"op":"add","path":"/elements/desc","value":{"key":"desc","type":"Text","props":{"content":"Latest posts","variant":"muted"}}}
{"op":"add","path":"/elements/posts","value":{"key":"posts","type":"Grid","props":{"columns":1,"gap":"md","className":["sm:grid-cols-2","lg:grid-cols-3"]},"children":["post1"]}}
{"op":"add","path":"/elements/post1","value":{"key":"post1","type":"Card","props":{"title":"Post Title"},"children":["excerpt"]}}
{"op":"add","path":"/elements/excerpt","value":{"key":"excerpt","type":"Text","props":{"content":"Post content...","variant":"body"}}}
Generate JSONL:`;
const MAX_PROMPT_LENGTH = 140;
const MAX_PROMPT_LENGTH = 500;
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
export async function POST(req: Request) {
const { prompt } = await req.json();
function getSystemPrompt(isYaml: boolean, editModes?: EditMode[]): string {
if (isYaml) {
return yamlPrompt(playgroundCatalog, {
mode: "standalone",
customRules: PLAYGROUND_RULES,
editModes: editModes ?? ["merge"],
});
}
return playgroundCatalog.prompt({
customRules: PLAYGROUND_RULES,
editModes,
});
}
const sanitizedPrompt = String(prompt || "").slice(0, MAX_PROMPT_LENGTH);
function buildYamlUserPrompt(
prompt: string,
previousSpec?: Spec | null,
editModes?: EditMode[],
): string {
if (isNonEmptySpec(previousSpec)) {
return buildEditUserPrompt({
prompt,
currentSpec: previousSpec,
config: { modes: editModes ?? ["merge"] },
format: "yaml",
maxPromptLength: MAX_PROMPT_LENGTH,
serializer: (s) => yamlStringify(s, { indent: 2 }).trimEnd(),
});
}
const userText = prompt.slice(0, MAX_PROMPT_LENGTH);
return [
userText,
"",
"Output the full spec in a ```yaml-spec fence. Stream progressively — output elements one at a time.",
].join("\n");
}
export async function POST(req: Request) {
const headersList = await headers();
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
const [minuteResult, dailyResult] = await Promise.all([
minuteRateLimit.limit(ip),
dailyRateLimit.limit(ip),
]);
if (!minuteResult.success || !dailyResult.success) {
const isMinuteLimit = !minuteResult.success;
return new Response(
JSON.stringify({
error: "Rate limit exceeded",
message: isMinuteLimit
? "Too many requests. Please wait a moment before trying again."
: "Daily limit reached. Please try again tomorrow.",
}),
{
status: 429,
headers: { "Content-Type": "application/json" },
},
);
}
const { prompt, context, format, editModes } = await req.json();
const isYaml = format === "yaml";
const systemPrompt = getSystemPrompt(isYaml, editModes);
const userPrompt = isYaml
? buildYamlUserPrompt(prompt, context?.previousSpec, editModes)
: buildUserPrompt({
prompt,
currentSpec: context?.previousSpec,
maxPromptLength: MAX_PROMPT_LENGTH,
editModes,
});
const result = streamText({
model: process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL,
system: SYSTEM_PROMPT,
prompt: sanitizedPrompt,
system: [
{
role: "system",
content: systemPrompt,
providerOptions: {
anthropic: { cacheControl: { type: "ephemeral" } },
},
},
],
prompt: userPrompt,
temperature: 0.7,
});
return result.toTextStreamResponse();
const encoder = new TextEncoder();
const textStream = result.textStream;
const stream = new ReadableStream({
async start(controller) {
for await (const chunk of textStream) {
controller.enqueue(encoder.encode(chunk));
}
try {
const usage = await result.usage;
const meta = JSON.stringify({
__meta: "usage",
promptTokens: usage.inputTokens,
completionTokens: usage.outputTokens,
totalTokens: usage.totalTokens,
cachedTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0,
cacheWriteTokens: usage.inputTokenDetails?.cacheWriteTokens ?? 0,
});
controller.enqueue(encoder.encode(`\n${meta}\n`));
} catch {
// Usage not available
}
controller.close();
},
});
return new Response(stream, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
-163
View File
@@ -1,163 +0,0 @@
import Link from "next/link";
import { Code } from "@/components/code";
export const metadata = {
title: "Actions | json-render",
};
export default function ActionsPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">Actions</h1>
<p className="text-muted-foreground mb-8">
Handle user interactions safely with named actions.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">Why Named Actions?</h2>
<p className="text-sm text-muted-foreground mb-4">
Instead of AI generating arbitrary code, it declares <em>intent</em> by
name. Your application provides the implementation. This is a core
guardrail.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">Defining Actions</h2>
<p className="text-sm text-muted-foreground mb-4">
Define available actions in your catalog:
</p>
<Code lang="typescript">{`const catalog = createCatalog({
components: { /* ... */ },
actions: {
submit_form: {
params: z.object({
formId: z.string(),
}),
description: 'Submit a form',
},
export_data: {
params: z.object({
format: z.enum(['csv', 'pdf', 'json']),
filters: z.object({
dateRange: z.string().optional(),
}).optional(),
}),
},
navigate: {
params: z.object({
url: z.string(),
}),
},
},
});`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">ActionProvider</h2>
<p className="text-sm text-muted-foreground mb-4">
Provide action handlers to your app:
</p>
<Code lang="tsx">{`import { ActionProvider } from '@json-render/react';
function App() {
const handlers = {
submit_form: async (params) => {
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ formId: params.formId }),
});
return response.json();
},
export_data: async (params) => {
const blob = await generateExport(params.format, params.filters);
downloadBlob(blob, \`export.\${params.format}\`);
},
navigate: (params) => {
window.location.href = params.url;
},
};
return (
<ActionProvider handlers={handlers}>
{/* Your UI */}
</ActionProvider>
);
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">
Using Actions in Components
</h2>
<Code lang="tsx">{`const Button = ({ element, onAction }) => (
<button onClick={() => onAction(element.props.action, {})}>
{element.props.label}
</button>
);
// Or use the useAction hook
import { useAction } from '@json-render/react';
function SubmitButton() {
const submitForm = useAction('submit_form');
return (
<button onClick={() => submitForm({ formId: 'contact' })}>
Submit
</button>
);
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">
Actions with Confirmation
</h2>
<p className="text-sm text-muted-foreground mb-4">
AI can declare actions that require user confirmation:
</p>
<Code lang="json">{`{
"type": "Button",
"props": {
"label": "Delete Account",
"action": {
"name": "delete_account",
"params": { "userId": "123" },
"confirm": {
"title": "Delete Account?",
"message": "This action cannot be undone.",
"variant": "danger"
}
}
}
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Action Callbacks</h2>
<p className="text-sm text-muted-foreground mb-4">
Handle success and error states:
</p>
<Code lang="json">{`{
"type": "Button",
"props": {
"label": "Save",
"action": {
"name": "save_changes",
"params": { "documentId": "doc-1" },
"onSuccess": {
"set": { "/ui/savedMessage": "Changes saved!" }
},
"onError": {
"set": { "/ui/errorMessage": "$error.message" }
}
}
}
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
<p className="text-sm text-muted-foreground">
Learn about{" "}
<Link
href="/docs/visibility"
className="text-foreground hover:underline"
>
conditional visibility
</Link>
.
</p>
</article>
);
}
-117
View File
@@ -1,117 +0,0 @@
import Link from "next/link";
import { Code } from "@/components/code";
export const metadata = {
title: "AI SDK Integration | json-render",
};
export default function AiSdkPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">AI SDK Integration</h1>
<p className="text-muted-foreground mb-8">
Use json-render with the Vercel AI SDK for seamless streaming.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">Installation</h2>
<Code lang="bash">npm install ai</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">API Route Setup</h2>
<Code lang="typescript">{`// app/api/generate/route.ts
import { streamText } from 'ai';
import { generateCatalogPrompt } from '@json-render/core';
import { catalog } from '@/lib/catalog';
export async function POST(req: Request) {
const { prompt, currentTree } = await req.json();
const systemPrompt = generateCatalogPrompt(catalog);
// Optionally include current UI state for context
const contextPrompt = currentTree
? \`\\n\\nCurrent UI state:\\n\${JSON.stringify(currentTree, null, 2)}\`
: '';
const result = streamText({
model: 'anthropic/claude-haiku-4.5',
system: systemPrompt + contextPrompt,
prompt,
});
return new Response(result.textStream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Transfer-Encoding': 'chunked',
},
});
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Client-Side Hook</h2>
<p className="text-sm text-muted-foreground mb-4">
Use <code className="text-foreground">useUIStream</code> on the client:
</p>
<Code lang="tsx">{`'use client';
import { useUIStream } from '@json-render/react';
function GenerativeUI() {
const { tree, isLoading, error, generate } = useUIStream({
endpoint: '/api/generate',
});
return (
<div>
<button
onClick={() => generate('Create a dashboard with metrics')}
disabled={isLoading}
>
{isLoading ? 'Generating...' : 'Generate'}
</button>
{error && <p className="text-red-500">{error.message}</p>}
<Renderer tree={tree} registry={registry} />
</div>
);
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Prompt Engineering</h2>
<p className="text-sm text-muted-foreground mb-4">
The <code className="text-foreground">generateCatalogPrompt</code>{" "}
function creates an optimized prompt that:
</p>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1 mb-4">
<li>Lists all available components and their props</li>
<li>Describes available actions</li>
<li>Specifies the expected JSON output format</li>
<li>Includes examples for better generation</li>
</ul>
<h2 className="text-xl font-semibold mt-12 mb-4">
Custom System Prompts
</h2>
<Code lang="typescript">{`const basePrompt = generateCatalogPrompt(catalog);
const customPrompt = \`
\${basePrompt}
Additional instructions:
- Always use Card components for grouping related content
- Prefer horizontal layouts (Row) for metrics
- Use consistent spacing with padding="md"
\`;`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
<p className="text-sm text-muted-foreground">
Learn about{" "}
<Link
href="/docs/streaming"
className="text-foreground hover:underline"
>
progressive streaming
</Link>
.
</p>
</article>
);
}
-130
View File
@@ -1,130 +0,0 @@
import { Code } from "@/components/code";
export const metadata = {
title: "@json-render/codegen API | json-render",
};
export default function CodegenApiPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">@json-render/codegen</h1>
<p className="text-muted-foreground mb-8">
Utilities for generating code from UI trees.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">Tree Traversal</h2>
<h3 className="text-lg font-semibold mt-8 mb-4">traverseTree</h3>
<p className="text-sm text-muted-foreground mb-4">
Walk the UI tree depth-first.
</p>
<Code lang="typescript">{`function traverseTree(
tree: UITree,
visitor: TreeVisitor,
startKey?: string
): void
interface TreeVisitor {
(element: UIElement, depth: number, parent: UIElement | null): void;
}`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">collectUsedComponents</h3>
<p className="text-sm text-muted-foreground mb-4">
Get all unique component types used in a tree.
</p>
<Code lang="typescript">{`function collectUsedComponents(tree: UITree): Set<string>
// Example
const components = collectUsedComponents(tree);
// Set { 'Card', 'Metric', 'Chart' }`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">collectDataPaths</h3>
<p className="text-sm text-muted-foreground mb-4">
Get all data paths referenced in props (valuePath, dataPath, bindPath,
etc.).
</p>
<Code lang="typescript">{`function collectDataPaths(tree: UITree): Set<string>
// Example
const paths = collectDataPaths(tree);
// Set { 'analytics/revenue', 'analytics/customers' }`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">collectActions</h3>
<p className="text-sm text-muted-foreground mb-4">
Get all action names used in the tree.
</p>
<Code lang="typescript">{`function collectActions(tree: UITree): Set<string>
// Example
const actions = collectActions(tree);
// Set { 'submit_form', 'refresh_data' }`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Serialization</h2>
<h3 className="text-lg font-semibold mt-8 mb-4">serializePropValue</h3>
<p className="text-sm text-muted-foreground mb-4">
Serialize a single value to a code string.
</p>
<Code lang="typescript">{`function serializePropValue(
value: unknown,
options?: SerializeOptions
): { value: string; needsBraces: boolean }
// Examples
serializePropValue("hello")
// { value: '"hello"', needsBraces: false }
serializePropValue(42)
// { value: '42', needsBraces: true }
serializePropValue({ path: 'user/name' })
// { value: '{ path: "user/name" }', needsBraces: true }`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">serializeProps</h3>
<p className="text-sm text-muted-foreground mb-4">
Serialize a props object to a JSX attributes string.
</p>
<Code lang="typescript">{`function serializeProps(
props: Record<string, unknown>,
options?: SerializeOptions
): string
// Example
serializeProps({ title: 'Dashboard', columns: 3, disabled: true })
// 'title="Dashboard" columns={3} disabled'`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">escapeString</h3>
<p className="text-sm text-muted-foreground mb-4">
Escape a string for use in code.
</p>
<Code lang="typescript">{`function escapeString(
str: string,
quotes?: 'single' | 'double'
): string`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Types</h2>
<h3 className="text-lg font-semibold mt-8 mb-4">GeneratedFile</h3>
<Code lang="typescript">{`interface GeneratedFile {
/** File path relative to project root */
path: string;
/** File contents */
content: string;
}`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">CodeGenerator</h3>
<Code lang="typescript">{`interface CodeGenerator {
/** Generate files from a UI tree */
generate(tree: UITree): GeneratedFile[];
}`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">SerializeOptions</h3>
<Code lang="typescript">{`interface SerializeOptions {
/** Quote style for strings */
quotes?: 'single' | 'double';
/** Indent for objects/arrays */
indent?: number;
}`}</Code>
</article>
);
}
-112
View File
@@ -1,112 +0,0 @@
import { Code } from "@/components/code";
export const metadata = {
title: "@json-render/core API | json-render",
};
export default function CoreApiPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">@json-render/core</h1>
<p className="text-muted-foreground mb-8">
Core types, schemas, and utilities.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">createCatalog</h2>
<p className="text-sm text-muted-foreground mb-4">
Creates a catalog definition.
</p>
<Code lang="typescript">{`function createCatalog(config: CatalogConfig): Catalog
interface CatalogConfig {
components: Record<string, ComponentDefinition>;
actions?: Record<string, ActionDefinition>;
validationFunctions?: Record<string, ValidationFunctionDef>;
}
interface ComponentDefinition {
props: ZodObject;
hasChildren?: boolean;
description?: string;
}
interface ActionDefinition {
params?: ZodObject;
description?: string;
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">
generateCatalogPrompt
</h2>
<p className="text-sm text-muted-foreground mb-4">
Generates a system prompt for AI models.
</p>
<Code lang="typescript">{`function generateCatalogPrompt(catalog: Catalog): string`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">evaluateVisibility</h2>
<p className="text-sm text-muted-foreground mb-4">
Evaluates a visibility condition against data and auth state.
</p>
<Code lang="typescript">{`function evaluateVisibility(
condition: VisibilityCondition | undefined,
data: Record<string, unknown>,
auth?: AuthState
): boolean
type VisibilityCondition =
| { path: string }
| { auth: 'signedIn' | 'signedOut' | string }
| { and: VisibilityCondition[] }
| { or: VisibilityCondition[] }
| { not: VisibilityCondition }
| { eq: [DynamicValue, DynamicValue] }
| { gt: [DynamicValue, DynamicValue] }
| { gte: [DynamicValue, DynamicValue] }
| { lt: [DynamicValue, DynamicValue] }
| { lte: [DynamicValue, DynamicValue] };`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Types</h2>
<h3 className="text-lg font-semibold mt-8 mb-4">UIElement</h3>
<Code lang="typescript">{`interface UIElement {
key: string;
type: string;
props: Record<string, unknown>;
children?: UIElement[];
visible?: VisibilityCondition;
validation?: ValidationSchema;
}`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">UITree</h3>
<Code lang="typescript">{`interface UITree {
root: UIElement | null;
elements: Record<string, UIElement>;
}`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">Action</h3>
<Code lang="typescript">{`interface Action {
name: string;
params?: Record<string, unknown>;
confirm?: {
title: string;
message: string;
variant?: 'default' | 'danger';
};
onSuccess?: { set: Record<string, unknown> };
onError?: { set: Record<string, unknown> };
}`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">ValidationSchema</h3>
<Code lang="typescript">{`interface ValidationSchema {
checks: ValidationCheck[];
validateOn?: 'change' | 'blur' | 'submit';
}
interface ValidationCheck {
fn: string;
args?: Record<string, unknown>;
message: string;
}`}</Code>
</article>
);
}
-110
View File
@@ -1,110 +0,0 @@
import { Code } from "@/components/code";
export const metadata = {
title: "@json-render/react API | json-render",
};
export default function ReactApiPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">@json-render/react</h1>
<p className="text-muted-foreground mb-8">
React components, providers, and hooks.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">Providers</h2>
<h3 className="text-lg font-semibold mt-8 mb-4">DataProvider</h3>
<Code lang="tsx">{`<DataProvider initialData={object}>
{children}
</DataProvider>`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">ActionProvider</h3>
<Code lang="tsx">{`<ActionProvider handlers={Record<string, ActionHandler>}>
{children}
</ActionProvider>
type ActionHandler = (params: Record<string, unknown>) => void | Promise<void>;`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">VisibilityProvider</h3>
<Code lang="tsx">{`<VisibilityProvider auth={AuthState}>
{children}
</VisibilityProvider>
interface AuthState {
isSignedIn: boolean;
roles?: string[];
}`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">ValidationProvider</h3>
<Code lang="tsx">{`<ValidationProvider functions={Record<string, ValidatorFn>}>
{children}
</ValidationProvider>
type ValidatorFn = (value: unknown, args?: object) => boolean | Promise<boolean>;`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Components</h2>
<h3 className="text-lg font-semibold mt-8 mb-4">Renderer</h3>
<Code lang="tsx">{`<Renderer
tree={UITree}
registry={ComponentRegistry}
/>
type ComponentRegistry = Record<string, React.ComponentType<ComponentProps>>;
interface ComponentProps {
element: UIElement;
children?: React.ReactNode;
onAction: (name: string, params: object) => void;
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Hooks</h2>
<h3 className="text-lg font-semibold mt-8 mb-4">useUIStream</h3>
<Code lang="typescript">{`const {
tree, // UITree - current UI state
isLoading, // boolean - true while streaming
error, // Error | null
generate, // (prompt: string) => void
abort, // () => void
} = useUIStream({
endpoint: string,
});`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">useData</h3>
<Code lang="typescript">{`const {
data, // Record<string, unknown>
setData, // (data: object) => void
getValue, // (path: string) => unknown
setValue, // (path: string, value: unknown) => void
} = useData();`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">useDataValue</h3>
<Code lang="typescript">{`const value = useDataValue(path: string);`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">useDataBinding</h3>
<Code lang="typescript">{`const [value, setValue] = useDataBinding(path: string);`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">useActions</h3>
<Code lang="typescript">{`const { dispatch } = useActions();
// dispatch(actionName: string, params: object)`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">useAction</h3>
<Code lang="typescript">{`const submitForm = useAction('submit_form');
// submitForm(params: object)`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">useIsVisible</h3>
<Code lang="typescript">{`const isVisible = useIsVisible(condition?: VisibilityCondition);`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">useFieldValidation</h3>
<Code lang="typescript">{`const {
value, // unknown
setValue, // (value: unknown) => void
errors, // string[]
validate, // () => Promise<boolean>
isValid, // boolean
} = useFieldValidation(path: string, checks: ValidationCheck[]);`}</Code>
</article>
);
}
-120
View File
@@ -1,120 +0,0 @@
import Link from "next/link";
import { Code } from "@/components/code";
export const metadata = {
title: "Catalog | json-render",
};
export default function CatalogPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">Catalog</h1>
<p className="text-muted-foreground mb-8">
The catalog defines what AI can generate. It&apos;s your guardrail.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">What is a Catalog?</h2>
<p className="text-sm text-muted-foreground mb-4">
A catalog is a schema that defines:
</p>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1 mb-4">
<li>
<strong className="text-foreground">Components</strong> — UI elements
AI can create
</li>
<li>
<strong className="text-foreground">Actions</strong> — Operations AI
can trigger
</li>
<li>
<strong className="text-foreground">Validation Functions</strong> —
Custom validators for form inputs
</li>
</ul>
<h2 className="text-xl font-semibold mt-12 mb-4">Creating a Catalog</h2>
<Code lang="typescript">{`import { createCatalog } from '@json-render/core';
import { z } from 'zod';
const catalog = createCatalog({
components: {
// Define each component with its props schema
Card: {
props: z.object({
title: z.string(),
description: z.string().nullable(),
padding: z.enum(['sm', 'md', 'lg']).default('md'),
}),
hasChildren: true, // Can contain other components
},
Metric: {
props: z.object({
label: z.string(),
valuePath: z.string(), // JSON Pointer to data
format: z.enum(['currency', 'percent', 'number']),
}),
},
},
actions: {
submit_form: {
params: z.object({
formId: z.string(),
}),
description: 'Submit a form',
},
export_data: {
params: z.object({
format: z.enum(['csv', 'pdf', 'json']),
}),
},
},
validationFunctions: {
isValidEmail: {
description: 'Validates email format',
},
isPhoneNumber: {
description: 'Validates phone number',
},
},
});`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Component Definition</h2>
<p className="text-sm text-muted-foreground mb-4">
Each component in the catalog has:
</p>
<Code lang="typescript">{`{
props: z.object({...}), // Zod schema for props
hasChildren?: boolean, // Can it have children?
description?: string, // Help AI understand when to use it
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">
Generating AI Prompts
</h2>
<p className="text-sm text-muted-foreground mb-4">
Use <code className="text-foreground">generateCatalogPrompt</code> to
create a system prompt for AI:
</p>
<Code lang="typescript">{`import { generateCatalogPrompt } from '@json-render/core';
const systemPrompt = generateCatalogPrompt(catalog);
// Pass this to your AI model as the system prompt`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
<p className="text-sm text-muted-foreground">
Learn how to{" "}
<Link
href="/docs/components"
className="text-foreground hover:underline"
>
register React components
</Link>{" "}
for your catalog.
</p>
</article>
);
}
-170
View File
@@ -1,170 +0,0 @@
import { Code } from "@/components/code";
export const metadata = {
title: "Code Export | json-render",
};
export default function CodeExportPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">Code Export</h1>
<p className="text-muted-foreground mb-8">
Export generated UI as standalone code for your framework.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">Overview</h2>
<p className="text-sm text-muted-foreground mb-4">
While json-render is designed for dynamic rendering, you can export
generated UI as static code. The code generation is intentionally
project-specific so you have full control over:
</p>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-2 mb-8">
<li>Component templates (standalone, no json-render dependencies)</li>
<li>Package.json and project structure</li>
<li>Framework-specific patterns (Next.js, Remix, etc.)</li>
<li>How data is passed to components</li>
</ul>
<h2 className="text-xl font-semibold mt-12 mb-4">Architecture</h2>
<p className="text-sm text-muted-foreground mb-4">
Code export is split into two parts:
</p>
<h3 className="text-lg font-semibold mt-8 mb-4">
1. @json-render/codegen (utilities)
</h3>
<p className="text-sm text-muted-foreground mb-4">
Framework-agnostic utilities for building code generators:
</p>
<Code lang="typescript">{`import {
traverseTree, // Walk the UI tree
collectUsedComponents, // Get all component types used
collectDataPaths, // Get all data binding paths
collectActions, // Get all action names
serializeProps, // Convert props to JSX string
} from '@json-render/codegen';`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">
2. Your Project (generator)
</h3>
<p className="text-sm text-muted-foreground mb-4">
Custom code generator specific to your project and framework:
</p>
<Code lang="typescript">{`// lib/codegen/generator.ts
import { collectUsedComponents, serializeProps } from '@json-render/codegen';
export function generateNextJSProject(tree: UITree): GeneratedFile[] {
const components = collectUsedComponents(tree);
return [
{ path: 'package.json', content: '...' },
{ path: 'app/page.tsx', content: '...' },
// ... component files
];
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">
Example: Next.js Export
</h2>
<p className="text-sm text-muted-foreground mb-4">
See the dashboard example for a complete implementation that exports:
</p>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-2 mb-4">
<li>
<code className="text-foreground">package.json</code> - Dependencies
and scripts
</li>
<li>
<code className="text-foreground">tsconfig.json</code> - TypeScript
config
</li>
<li>
<code className="text-foreground">next.config.js</code> - Next.js
config
</li>
<li>
<code className="text-foreground">app/layout.tsx</code> - Root layout
</li>
<li>
<code className="text-foreground">app/globals.css</code> - Global
styles
</li>
<li>
<code className="text-foreground">app/page.tsx</code> - Generated page
with data
</li>
<li>
<code className="text-foreground">components/ui/*.tsx</code> -
Standalone components
</li>
</ul>
<h2 className="text-xl font-semibold mt-12 mb-4">
Standalone Components
</h2>
<p className="text-sm text-muted-foreground mb-4">
The exported components are standalone with no json-render dependencies.
They receive data as props instead of using hooks:
</p>
<Code lang="tsx">{`// Generated component (standalone)
interface MetricProps {
label: string;
valuePath: string;
data?: Record<string, unknown>;
}
export function Metric({ label, valuePath, data }: MetricProps) {
const value = data ? getByPath(data, valuePath) : undefined;
return (
<div>
<span>{label}</span>
<span>{formatValue(value)}</span>
</div>
);
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Using the Utilities</h2>
<h3 className="text-lg font-semibold mt-8 mb-4">traverseTree</h3>
<Code lang="typescript">{`import { traverseTree } from '@json-render/codegen';
traverseTree(tree, (element, depth, parent) => {
console.log(' '.repeat(depth * 2) + element.type);
});`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">collectUsedComponents</h3>
<Code lang="typescript">{`import { collectUsedComponents } from '@json-render/codegen';
const components = collectUsedComponents(tree);
// Set { 'Card', 'Metric', 'Chart', 'Table' }
// Generate only the needed component files
for (const component of components) {
files.push({
path: \`components/ui/\${component.toLowerCase()}.tsx\`,
content: componentTemplates[component],
});
}`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">serializeProps</h3>
<Code lang="typescript">{`import { serializeProps } from '@json-render/codegen';
const propsStr = serializeProps({
title: 'Dashboard',
columns: 3,
disabled: true,
});
// 'title="Dashboard" columns={3} disabled'`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Try It</h2>
<p className="text-sm text-muted-foreground mb-4">
Run the dashboard example and click &quot;Export Project&quot; to see
code generation in action:
</p>
<Code lang="bash">{`cd examples/dashboard
pnpm dev
# Open http://localhost:3001
# Generate a widget, then click "Export Project"`}</Code>
</article>
);
}
-111
View File
@@ -1,111 +0,0 @@
import Link from "next/link";
import { Code } from "@/components/code";
export const metadata = {
title: "Components | json-render",
};
export default function ComponentsPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">Components</h1>
<p className="text-muted-foreground mb-8">
Register React components to render your catalog types.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">Component Registry</h2>
<p className="text-sm text-muted-foreground mb-4">
Create a registry that maps catalog component types to React components:
</p>
<Code lang="tsx">{`const registry = {
Card: ({ element, children }) => (
<div className="card">
<h2>{element.props.title}</h2>
{element.props.description && (
<p>{element.props.description}</p>
)}
{children}
</div>
),
Button: ({ element, onAction }) => (
<button onClick={() => onAction(element.props.action, {})}>
{element.props.label}
</button>
),
};`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Component Props</h2>
<p className="text-sm text-muted-foreground mb-4">
Each component receives these props:
</p>
<Code lang="typescript">{`interface ComponentProps {
element: {
key: string;
type: string;
props: Record<string, unknown>;
children?: UIElement[];
visible?: VisibilityCondition;
validation?: ValidationSchema;
};
children?: React.ReactNode; // Rendered children
onAction: (name: string, params: object) => void;
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Using Data Binding</h2>
<p className="text-sm text-muted-foreground mb-4">
Use hooks to read and write data:
</p>
<Code lang="tsx">{`import { useDataValue, useDataBinding } from '@json-render/react';
const Metric = ({ element }) => {
// Read-only value
const value = useDataValue(element.props.valuePath);
return (
<div className="metric">
<span className="label">{element.props.label}</span>
<span className="value">{formatValue(value)}</span>
</div>
);
};
const TextField = ({ element }) => {
// Two-way binding
const [value, setValue] = useDataBinding(element.props.valuePath);
return (
<input
value={value || ''}
onChange={(e) => setValue(e.target.value)}
placeholder={element.props.placeholder}
/>
);
};`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Using the Renderer</h2>
<Code lang="tsx">{`import { Renderer } from '@json-render/react';
function App() {
return (
<Renderer
tree={uiTree}
registry={registry}
/>
);
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
<p className="text-sm text-muted-foreground">
Learn about{" "}
<Link
href="/docs/data-binding"
className="text-foreground hover:underline"
>
data binding
</Link>{" "}
for dynamic values.
</p>
</article>
);
}
-133
View File
@@ -1,133 +0,0 @@
import Link from "next/link";
import { Code } from "@/components/code";
export const metadata = {
title: "Data Binding | json-render",
};
export default function DataBindingPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">Data Binding</h1>
<p className="text-muted-foreground mb-8">
Connect UI components to your application data using JSON Pointer paths.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">JSON Pointer Paths</h2>
<p className="text-sm text-muted-foreground mb-4">
json-render uses JSON Pointer (RFC 6901) for data paths:
</p>
<Code lang="json">{`// Given this data:
{
"user": {
"name": "Alice",
"email": "alice@example.com"
},
"metrics": {
"revenue": 125000,
"growth": 0.15
}
}
// These paths access:
"/user/name" -> "Alice"
"/metrics/revenue" -> 125000
"/metrics/growth" -> 0.15`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">DataProvider</h2>
<p className="text-sm text-muted-foreground mb-4">
Wrap your app with DataProvider to enable data binding:
</p>
<Code lang="tsx">{`import { DataProvider } from '@json-render/react';
function App() {
const initialData = {
user: { name: 'Alice' },
form: { email: '', message: '' },
};
return (
<DataProvider initialData={initialData}>
{/* Your UI */}
</DataProvider>
);
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Reading Data</h2>
<p className="text-sm text-muted-foreground mb-4">
Use <code className="text-foreground">useDataValue</code> for read-only
access:
</p>
<Code lang="tsx">{`import { useDataValue } from '@json-render/react';
function UserGreeting() {
const name = useDataValue('/user/name');
return <h1>Hello, {name}!</h1>;
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Two-Way Binding</h2>
<p className="text-sm text-muted-foreground mb-4">
Use <code className="text-foreground">useDataBinding</code> for
read-write access:
</p>
<Code lang="tsx">{`import { useDataBinding } from '@json-render/react';
function EmailInput() {
const [email, setEmail] = useDataBinding('/form/email');
return (
<input
type="email"
value={email || ''}
onChange={(e) => setEmail(e.target.value)}
/>
);
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">
Using the Data Context
</h2>
<p className="text-sm text-muted-foreground mb-4">
Access the full data context for advanced use cases:
</p>
<Code lang="tsx">{`import { useData } from '@json-render/react';
function DataDebugger() {
const { data, setData, getValue, setValue } = useData();
// Read any path
const revenue = getValue('/metrics/revenue');
// Write any path
const updateRevenue = () => setValue('/metrics/revenue', 150000);
// Replace all data
const resetData = () => setData({ user: {}, form: {} });
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">In JSON UI Trees</h2>
<p className="text-sm text-muted-foreground mb-4">
AI can reference data paths in component props:
</p>
<Code lang="json">{`{
"type": "Metric",
"props": {
"label": "Total Revenue",
"valuePath": "/metrics/revenue",
"format": "currency"
}
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
<p className="text-sm text-muted-foreground">
Learn about{" "}
<Link href="/docs/actions" className="text-foreground hover:underline">
actions
</Link>{" "}
for user interactions.
</p>
</article>
);
}
-40
View File
@@ -1,40 +0,0 @@
import { PackageInstall } from "@/components/package-install";
export const metadata = {
title: "Installation | json-render",
};
export default function InstallationPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">Installation</h1>
<p className="text-muted-foreground mb-8">
Install the core and React packages to get started.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">Install packages</h2>
<PackageInstall packages="@json-render/core @json-render/react" />
<h2 className="text-xl font-semibold mt-12 mb-4">Peer Dependencies</h2>
<p className="text-sm text-muted-foreground mb-4">
json-render requires the following peer dependencies:
</p>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1 mb-4">
<li>
<code className="text-foreground">react</code> ^19.0.0
</li>
<li>
<code className="text-foreground">zod</code> ^4.0.0
</li>
</ul>
<PackageInstall packages="react zod" />
<h2 className="text-xl font-semibold mt-12 mb-4">For AI Integration</h2>
<p className="text-sm text-muted-foreground mb-4">
To use json-render with AI models, you&apos;ll also need the Vercel AI
SDK:
</p>
<PackageInstall packages="ai" />
</article>
);
}
-81
View File
@@ -1,81 +0,0 @@
import Link from "next/link";
import { DocsMobileNav } from "@/components/docs-mobile-nav";
const navigation = [
{
title: "Getting Started",
items: [
{ title: "Introduction", href: "/docs" },
{ title: "Installation", href: "/docs/installation" },
{ title: "Quick Start", href: "/docs/quick-start" },
],
},
{
title: "Core Concepts",
items: [
{ title: "Catalog", href: "/docs/catalog" },
{ title: "Components", href: "/docs/components" },
{ title: "Data Binding", href: "/docs/data-binding" },
{ title: "Actions", href: "/docs/actions" },
{ title: "Visibility", href: "/docs/visibility" },
{ title: "Validation", href: "/docs/validation" },
],
},
{
title: "Guides",
items: [
{ title: "AI SDK Integration", href: "/docs/ai-sdk" },
{ title: "Streaming", href: "/docs/streaming" },
{ title: "Code Export", href: "/docs/code-export" },
],
},
{
title: "API Reference",
items: [
{ title: "@json-render/core", href: "/docs/api/core" },
{ title: "@json-render/react", href: "/docs/api/react" },
{ title: "@json-render/codegen", href: "/docs/api/codegen" },
],
},
];
export default function DocsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<DocsMobileNav />
<div className="max-w-5xl mx-auto px-6 py-8 lg:py-12 flex gap-16">
{/* Sidebar */}
<aside className="w-48 shrink-0 hidden lg:block">
<nav className="sticky top-20 space-y-6">
{navigation.map((section) => (
<div key={section.title}>
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
{section.title}
</h4>
<ul className="space-y-1">
{section.items.map((item) => (
<li key={item.href}>
<Link
href={item.href}
className="text-sm text-muted-foreground hover:text-foreground transition-colors block py-1"
>
{item.title}
</Link>
</li>
))}
</ul>
</div>
))}
</nav>
</aside>
{/* Content */}
<div className="flex-1 min-w-0 max-w-2xl">{children}</div>
</div>
</>
);
}
-67
View File
@@ -1,67 +0,0 @@
export const metadata = {
title: "Introduction | json-render",
};
export default function DocsPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">Introduction</h1>
<p className="text-muted-foreground mb-8">
Predictable. Guardrailed. Fast. Let users generate dashboards, widgets,
apps, and data visualizations from prompts.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">What is json-render?</h2>
<p className="text-sm text-muted-foreground mb-4 leading-relaxed">
json-render lets end users generate UI from natural language prompts —
safely constrained to components you define. You set the guardrails:
what components exist, what props they take, what actions are available.
AI generates JSON that matches your schema, and your components render
it natively.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">Why json-render?</h2>
<div className="space-y-4 mb-8">
<div>
<h3 className="font-medium mb-1">Guardrailed</h3>
<p className="text-sm text-muted-foreground">
AI can only use components in your catalog. No arbitrary code
generation.
</p>
</div>
<div>
<h3 className="font-medium mb-1">Predictable</h3>
<p className="text-sm text-muted-foreground">
JSON output matches your schema, every time. Actions are declared by
name, you control what they do.
</p>
</div>
<div>
<h3 className="font-medium mb-1">Fast</h3>
<p className="text-sm text-muted-foreground">
Stream and render progressively as the model responds. No waiting
for completion.
</p>
</div>
</div>
<h2 className="text-xl font-semibold mt-12 mb-4">How it works</h2>
<ol className="list-decimal list-inside space-y-2 text-sm text-muted-foreground">
<li>
Define the guardrails — what components, actions, and data bindings AI
can use
</li>
<li>
Users prompt — end users describe what they want in natural language
</li>
<li>
AI generates JSON — output is always predictable, constrained to your
catalog
</li>
<li>
Render fast — stream and render progressively as the model responds
</li>
</ol>
</article>
);
}
-205
View File
@@ -1,205 +0,0 @@
import Link from "next/link";
import { Code } from "@/components/code";
export const metadata = {
title: "Quick Start | json-render",
};
export default function QuickStartPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">Quick Start</h1>
<p className="text-muted-foreground mb-8">
Get up and running with json-render in 5 minutes.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">
1. Define your catalog
</h2>
<p className="text-sm text-muted-foreground mb-4">
Create a catalog that defines what components AI can use:
</p>
<Code lang="typescript">{`// lib/catalog.ts
import { createCatalog } from '@json-render/core';
import { z } from 'zod';
export const catalog = createCatalog({
components: {
Card: {
props: z.object({
title: z.string(),
description: z.string().nullable(),
}),
hasChildren: true,
},
Button: {
props: z.object({
label: z.string(),
action: z.string(),
}),
},
Text: {
props: z.object({
content: z.string(),
}),
},
},
actions: {
submit: {
params: z.object({ formId: z.string() }),
},
navigate: {
params: z.object({ url: z.string() }),
},
},
});`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">
2. Create your components
</h2>
<p className="text-sm text-muted-foreground mb-4">
Register React components that render each catalog type:
</p>
<Code lang="tsx">{`// components/registry.tsx
export const registry = {
Card: ({ element, children }) => (
<div className="p-4 border rounded-lg">
<h2 className="font-bold">{element.props.title}</h2>
{element.props.description && (
<p className="text-gray-600">{element.props.description}</p>
)}
{children}
</div>
),
Button: ({ element, onAction }) => (
<button
className="px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => onAction(element.props.action, {})}
>
{element.props.label}
</button>
),
Text: ({ element }) => (
<p>{element.props.content}</p>
),
};`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">
3. Create an API route
</h2>
<p className="text-sm text-muted-foreground mb-4">
Set up a streaming API route for AI generation:
</p>
<Code lang="typescript">{`// app/api/generate/route.ts
import { streamText } from 'ai';
import { generateCatalogPrompt } from '@json-render/core';
import { catalog } from '@/lib/catalog';
export async function POST(req: Request) {
const { prompt } = await req.json();
const systemPrompt = generateCatalogPrompt(catalog);
const result = streamText({
model: 'anthropic/claude-haiku-4.5',
system: systemPrompt,
prompt,
});
return new Response(result.textStream, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">4. Render the UI</h2>
<p className="text-sm text-muted-foreground mb-4">
Use the providers and renderer to display AI-generated UI:
</p>
<Code lang="tsx">{`// app/page.tsx
'use client';
import { DataProvider, ActionProvider, VisibilityProvider, Renderer, useUIStream } from '@json-render/react';
import { registry } from '@/components/registry';
export default function Page() {
const { tree, isLoading, generate } = useUIStream({
endpoint: '/api/generate',
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
generate(formData.get('prompt') as string);
};
return (
<DataProvider initialData={{}}>
<VisibilityProvider>
<ActionProvider handlers={{
submit: (params) => console.log('Submit:', params),
navigate: (params) => console.log('Navigate:', params),
}}>
<form onSubmit={handleSubmit}>
<input
name="prompt"
placeholder="Describe what you want..."
className="border p-2 rounded"
/>
<button type="submit" disabled={isLoading}>
Generate
</button>
</form>
<div className="mt-8">
<Renderer tree={tree} registry={registry} />
</div>
</ActionProvider>
</VisibilityProvider>
</DataProvider>
);
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Next steps</h2>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-2">
<li>
Learn about{" "}
<Link
href="/docs/catalog"
className="text-foreground hover:underline"
>
catalogs
</Link>{" "}
in depth
</li>
<li>
Explore{" "}
<Link
href="/docs/data-binding"
className="text-foreground hover:underline"
>
data binding
</Link>{" "}
for dynamic values
</li>
<li>
Add{" "}
<Link
href="/docs/actions"
className="text-foreground hover:underline"
>
actions
</Link>{" "}
for interactivity
</li>
<li>
Implement{" "}
<Link
href="/docs/visibility"
className="text-foreground hover:underline"
>
conditional visibility
</Link>
</li>
</ul>
</article>
);
}
-133
View File
@@ -1,133 +0,0 @@
import { Code } from "@/components/code";
export const metadata = {
title: "Streaming | json-render",
};
export default function StreamingPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">Streaming</h1>
<p className="text-muted-foreground mb-8">
Progressively render UI as AI generates it.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">How Streaming Works</h2>
<p className="text-sm text-muted-foreground mb-4">
json-render uses JSONL (JSON Lines) streaming. As AI generates, each
line represents a patch operation:
</p>
<Code lang="json">{`{"op":"set","path":"/root","value":{"key":"root","type":"Card","props":{"title":"Dashboard"}}}
{"op":"add","path":"/root/children","value":{"key":"metric-1","type":"Metric","props":{"label":"Revenue"}}}
{"op":"add","path":"/root/children","value":{"key":"metric-2","type":"Metric","props":{"label":"Users"}}}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">useUIStream Hook</h2>
<p className="text-sm text-muted-foreground mb-4">
The hook handles parsing and state management:
</p>
<Code lang="tsx">{`import { useUIStream } from '@json-render/react';
function App() {
const {
tree, // Current UI tree state
isLoading, // True while streaming
error, // Any error that occurred
generate, // Function to start generation
abort, // Function to cancel streaming
} = useUIStream({
endpoint: '/api/generate',
});
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Patch Operations</h2>
<p className="text-sm text-muted-foreground mb-4">
Supported operations:
</p>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-2 mb-4">
<li>
<code className="text-foreground">set</code> — Set the value at a path
(creates if needed)
</li>
<li>
<code className="text-foreground">add</code> — Add to an array at a
path
</li>
<li>
<code className="text-foreground">replace</code> — Replace value at a
path
</li>
<li>
<code className="text-foreground">remove</code> — Remove value at a
path
</li>
</ul>
<h2 className="text-xl font-semibold mt-12 mb-4">Path Format</h2>
<p className="text-sm text-muted-foreground mb-4">
Paths use a key-based format for elements:
</p>
<Code lang="bash">{`/root -> Root element
/root/children -> Children of root
/elements/card-1 -> Element with key "card-1"
/elements/card-1/children -> Children of card-1`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Server-Side Setup</h2>
<p className="text-sm text-muted-foreground mb-4">
Ensure your API route streams properly:
</p>
<Code lang="typescript">{`export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamText({
model: 'anthropic/claude-haiku-4.5',
system: generateCatalogPrompt(catalog),
prompt,
});
// Return as a streaming response
return new Response(result.textStream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Transfer-Encoding': 'chunked',
'Cache-Control': 'no-cache',
},
});
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">
Progressive Rendering
</h2>
<p className="text-sm text-muted-foreground mb-4">
The Renderer automatically updates as the tree changes:
</p>
<Code lang="tsx">{`function App() {
const { tree, isLoading } = useUIStream({ endpoint: '/api/generate' });
return (
<div>
{isLoading && <LoadingIndicator />}
<Renderer tree={tree} registry={registry} />
</div>
);
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Aborting Streams</h2>
<Code lang="tsx">{`function App() {
const { isLoading, generate, abort } = useUIStream({
endpoint: '/api/generate',
});
return (
<div>
<button onClick={() => generate('Create dashboard')}>
Generate
</button>
{isLoading && (
<button onClick={abort}>Cancel</button>
)}
</div>
);
}`}</Code>
</article>
);
}
-185
View File
@@ -1,185 +0,0 @@
import Link from "next/link";
import { Code } from "@/components/code";
export const metadata = {
title: "Validation | json-render",
};
export default function ValidationPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">Validation</h1>
<p className="text-muted-foreground mb-8">
Validate form inputs with built-in and custom functions.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">Built-in Validators</h2>
<p className="text-sm text-muted-foreground mb-4">
json-render includes common validation functions:
</p>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1 mb-4">
<li>
<code className="text-foreground">required</code> — Value must be
non-empty
</li>
<li>
<code className="text-foreground">email</code> — Valid email format
</li>
<li>
<code className="text-foreground">minLength</code> — Minimum string
length
</li>
<li>
<code className="text-foreground">maxLength</code> — Maximum string
length
</li>
<li>
<code className="text-foreground">pattern</code> — Match a regex
pattern
</li>
<li>
<code className="text-foreground">min</code> — Minimum numeric value
</li>
<li>
<code className="text-foreground">max</code> — Maximum numeric value
</li>
</ul>
<h2 className="text-xl font-semibold mt-12 mb-4">
Using Validation in JSON
</h2>
<Code lang="json">{`{
"type": "TextField",
"props": {
"label": "Email",
"valuePath": "/form/email",
"checks": [
{ "fn": "required", "message": "Email is required" },
{ "fn": "email", "message": "Invalid email format" }
],
"validateOn": "blur"
}
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">
Validation with Parameters
</h2>
<Code lang="json">{`{
"type": "TextField",
"props": {
"label": "Password",
"valuePath": "/form/password",
"checks": [
{ "fn": "required", "message": "Password is required" },
{
"fn": "minLength",
"args": { "length": 8 },
"message": "Password must be at least 8 characters"
},
{
"fn": "pattern",
"args": { "pattern": "[A-Z]" },
"message": "Must contain at least one uppercase letter"
}
]
}
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">
Custom Validation Functions
</h2>
<p className="text-sm text-muted-foreground mb-4">
Define custom validators in your catalog:
</p>
<Code lang="typescript">{`const catalog = createCatalog({
components: { /* ... */ },
validationFunctions: {
isValidPhone: {
description: 'Validates phone number format',
},
isUniqueEmail: {
description: 'Checks if email is not already registered',
},
},
});`}</Code>
<p className="text-sm text-muted-foreground mb-4">
Then implement them in your ValidationProvider:
</p>
<Code lang="tsx">{`import { ValidationProvider } from '@json-render/react';
function App() {
const customValidators = {
isValidPhone: (value) => {
const phoneRegex = /^\\+?[1-9]\\d{1,14}$/;
return phoneRegex.test(value);
},
isUniqueEmail: async (value) => {
const response = await fetch(\`/api/check-email?email=\${value}\`);
const { available } = await response.json();
return available;
},
};
return (
<ValidationProvider functions={customValidators}>
{/* Your UI */}
</ValidationProvider>
);
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Using in Components</h2>
<Code lang="tsx">{`import { useFieldValidation } from '@json-render/react';
function TextField({ element }) {
const { value, setValue, errors, validate } = useFieldValidation(
element.props.valuePath,
element.props.checks
);
return (
<div>
<label>{element.props.label}</label>
<input
value={value || ''}
onChange={(e) => setValue(e.target.value)}
onBlur={() => validate()}
/>
{errors.map((error, i) => (
<p key={i} className="text-red-500 text-sm">{error}</p>
))}
</div>
);
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Validation Timing</h2>
<p className="text-sm text-muted-foreground mb-4">
Control when validation runs with{" "}
<code className="text-foreground">validateOn</code>:
</p>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
<li>
<code className="text-foreground">change</code> — Validate on every
input change
</li>
<li>
<code className="text-foreground">blur</code> — Validate when field
loses focus
</li>
<li>
<code className="text-foreground">submit</code> — Validate only on
form submission
</li>
</ul>
<h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
<p className="text-sm text-muted-foreground">
Learn about{" "}
<Link href="/docs/ai-sdk" className="text-foreground hover:underline">
AI SDK integration
</Link>
.
</p>
</article>
);
}
-147
View File
@@ -1,147 +0,0 @@
import Link from "next/link";
import { Code } from "@/components/code";
export const metadata = {
title: "Visibility | json-render",
};
export default function VisibilityPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">Visibility</h1>
<p className="text-muted-foreground mb-8">
Conditionally show or hide components based on data, auth, or logic.
</p>
<h2 className="text-xl font-semibold mt-12 mb-4">VisibilityProvider</h2>
<p className="text-sm text-muted-foreground mb-4">
Wrap your app with VisibilityProvider to enable conditional rendering:
</p>
<Code lang="tsx">{`import { VisibilityProvider } from '@json-render/react';
function App() {
return (
<DataProvider initialData={data}>
<VisibilityProvider>
{/* Components can now use visibility conditions */}
</VisibilityProvider>
</DataProvider>
);
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">
Path-Based Visibility
</h2>
<p className="text-sm text-muted-foreground mb-4">
Show/hide based on data values:
</p>
<Code lang="json">{`{
"type": "Alert",
"props": { "message": "Form has errors" },
"visible": { "path": "/form/hasErrors" }
}
// Visible when /form/hasErrors is truthy`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">
Auth-Based Visibility
</h2>
<p className="text-sm text-muted-foreground mb-4">
Show/hide based on authentication state:
</p>
<Code lang="json">{`{
"type": "AdminPanel",
"visible": { "auth": "signedIn" }
}
// Options: "signedIn", "signedOut", "admin", etc.`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Logic Expressions</h2>
<p className="text-sm text-muted-foreground mb-4">
Combine conditions with logic operators:
</p>
<Code lang="json">{`// AND - all conditions must be true
{
"type": "SubmitButton",
"visible": {
"and": [
{ "path": "/form/isValid" },
{ "path": "/form/hasChanges" }
]
}
}
// OR - any condition must be true
{
"type": "HelpText",
"visible": {
"or": [
{ "path": "/user/isNew" },
{ "path": "/settings/showHelp" }
]
}
}
// NOT - invert a condition
{
"type": "WelcomeBanner",
"visible": {
"not": { "path": "/user/hasSeenWelcome" }
}
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Comparison Operators</h2>
<Code lang="json">{`// Equal
{
"visible": {
"eq": [{ "path": "/user/role" }, "admin"]
}
}
// Greater than
{
"visible": {
"gt": [{ "path": "/cart/total" }, 100]
}
}
// Available: eq, ne, gt, gte, lt, lte`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Complex Example</h2>
<Code lang="json">{`{
"type": "RefundButton",
"props": { "label": "Process Refund" },
"visible": {
"and": [
{ "auth": "signedIn" },
{ "eq": [{ "path": "/user/role" }, "support"] },
{ "gt": [{ "path": "/order/amount" }, 0] },
{ "not": { "path": "/order/isRefunded" } }
]
}
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Using in Components</h2>
<Code lang="tsx">{`import { useIsVisible } from '@json-render/react';
function ConditionalContent({ element, children }) {
const isVisible = useIsVisible(element.visible);
if (!isVisible) return null;
return <div>{children}</div>;
}`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
<p className="text-sm text-muted-foreground">
Learn about{" "}
<Link
href="/docs/validation"
className="text-foreground hover:underline"
>
form validation
</Link>
.
</p>
</article>
);
}
+66 -13
View File
@@ -1,6 +1,8 @@
@import "tailwindcss";
@import "tw-animate-css";
@source "../node_modules/streamdown/dist/index.js";
@custom-variant dark (&:is(.dark *));
:root {
@@ -26,6 +28,7 @@
--border: oklch(0.85 0 0);
--input: oklch(0.85 0 0);
--ring: oklch(0.6 0 0);
--chat-bg: oklch(0.95 0 0);
}
.dark {
@@ -50,6 +53,7 @@
--border: oklch(0.25 0 0);
--input: oklch(0.25 0 0);
--ring: oklch(0.4 0 0);
--chat-bg: oklch(0.25 0 0);
}
@theme inline {
@@ -107,29 +111,77 @@
@apply bg-transparent p-0;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
/* Hide page scrollbar */
html {
scrollbar-width: none;
}
::-webkit-scrollbar-track {
@apply bg-background;
html::-webkit-scrollbar {
display: none;
}
::-webkit-scrollbar-thumb {
@apply bg-border rounded;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-muted-foreground;
}
}
button {
cursor: pointer;
}
/* Tool call shimmer animation */
@keyframes tool-shimmer {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
}
.animate-tool-shimmer {
animation: tool-shimmer 1.5s ease-in-out infinite;
}
/* Fix list rendering in chat content */
.docs-chat-content ul,
.docs-chat-content ol {
list-style-position: outside;
padding-left: 1.25em;
}
.docs-chat-content li > p {
display: inline;
margin: 0;
}
.docs-chat-content li {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
/* MDX table styles — applies to both GFM pipe tables and raw HTML tables */
.mdx-table th,
.mdx-table td,
article table th,
article table td {
border: 1px solid var(--border);
padding: 0.75rem 1rem;
text-align: left;
}
.mdx-table th,
article table th {
font-weight: 600;
background-color: var(--muted);
}
.mdx-table td,
article table td {
color: var(--muted-foreground);
}
article table {
width: 100%;
font-size: 0.875rem;
border-collapse: collapse;
margin: 1.5rem 0;
}
/* Shiki dual theme support */
.shiki,
.shiki span {
@@ -142,3 +194,4 @@ button {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}
+33 -17
View File
@@ -1,11 +1,13 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import { GeistPixelSquare } from "geist/font/pixel";
import "./globals.css";
import { Header } from "@/components/header";
import { Footer } from "@/components/footer";
import { ThemeProvider } from "@/components/theme-provider";
import { DocsChat } from "@/components/docs-chat";
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { PAGE_TITLES } from "@/lib/page-titles";
import { cookies } from "next/headers";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
@@ -19,15 +21,18 @@ const geistMono = localFont({
export const metadata: Metadata = {
metadataBase: new URL("https://json-render.dev"),
title: {
default: "json-render | AI-generated UI with guardrails",
default: `json-render | ${PAGE_TITLES[""]}`,
template: "%s | json-render",
},
description:
"Let users generate dashboards, widgets, apps, and data visualizations 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.",
keywords: [
"json-render",
"generative UI",
"AI UI generation",
"user-generated interfaces",
"React components",
"React Native",
"guardrails",
"structured output",
"dashboard builder",
@@ -39,25 +44,24 @@ export const metadata: Metadata = {
locale: "en_US",
url: "https://json-render.dev",
siteName: "json-render",
title: "json-render | AI-generated UI with guardrails",
title: "json-render | The Generative UI Framework",
description:
"Let users generate dashboards, widgets, apps, and data visualizations 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: [
{
url: "/og",
width: 1200,
height: 630,
alt: "json-render - AI-generated UI with guardrails",
alt: "json-render - The Generative UI Framework",
},
],
},
twitter: {
card: "summary_large_image",
title: "json-render | AI-generated UI with guardrails",
title: "json-render | The Generative UI Framework",
description:
"Let users generate dashboards, widgets, apps, and data visualizations 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"],
creator: "@verabornnot",
},
robots: {
index: true,
@@ -68,20 +72,32 @@ export const metadata: Metadata = {
},
};
export default function RootLayout({
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const cookieStore = await cookies();
const chatOpen = cookieStore.get("docs-chat-open")?.value === "true";
const chatWidth = Number(cookieStore.get("docs-chat-width")?.value) || 400;
return (
<html lang="en" suppressHydrationWarning>
<body className={`${geistSans.variable} ${geistMono.variable}`}>
<head>
{chatOpen && (
<style
dangerouslySetInnerHTML={{
__html: `@media(min-width:640px){body{padding-right:${chatWidth}px}}`,
}}
/>
)}
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} ${GeistPixelSquare.variable}`}
>
<ThemeProvider>
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1">{children}</main>
<Footer />
</div>
{children}
<DocsChat defaultOpen={chatOpen} defaultWidth={chatWidth} />
</ThemeProvider>
<Analytics />
<SpeedInsights />
+22
View File
@@ -0,0 +1,22 @@
import Link from "next/link";
import { Header } from "@/components/header";
export default function NotFound() {
return (
<div className="flex min-h-screen flex-col">
<Header />
<main className="flex flex-1 flex-col items-center justify-center gap-4 px-4 text-center">
<h1 className="text-6xl font-bold tracking-tight">404</h1>
<p className="text-lg text-muted-foreground">
This page could not be found.
</p>
<Link
href="/"
className="mt-2 inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
>
Go home
</Link>
</main>
</div>
);
}
+16
View File
@@ -0,0 +1,16 @@
import { NextResponse } from "next/server";
import { getPageTitle, renderOgImage } from "../og-image";
export async function GET(
_request: Request,
{ params }: { params: Promise<{ slug: string[] }> },
) {
const { slug } = await params;
const title = getPageTitle(slug.join("/"));
if (!title) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return renderOgImage(title);
}
+112
View File
@@ -0,0 +1,112 @@
import { ImageResponse } from "next/og";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
export { getPageTitle } from "@/lib/page-titles";
// Cache font data in memory after first load
let fontCache: { geistRegular: Buffer; geistPixelSquare: Buffer } | null = null;
async function loadFonts() {
if (fontCache) return fontCache;
const [geistRegular, geistPixelSquare] = await Promise.all([
readFile(join(process.cwd(), "public/Geist-Regular.ttf")),
readFile(join(process.cwd(), "public/GeistPixel-Square.ttf")),
]);
fontCache = { geistRegular, geistPixelSquare };
return fontCache;
}
export async function renderOgImage(title: string) {
const { geistRegular, geistPixelSquare } = await loadFonts();
return new ImageResponse(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
backgroundColor: "black",
padding: "60px 80px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "16px",
}}
>
<svg width="36" height="36" viewBox="0 0 16 16" fill="white">
<path fillRule="evenodd" clipRule="evenodd" d="M8 1L16 15H0L8 1Z" />
</svg>
<span
style={{
fontSize: 36,
color: "#666",
fontFamily: "Geist",
fontWeight: 400,
}}
>
/
</span>
<span
style={{
fontSize: 36,
fontFamily: "Geist Pixel Square",
fontWeight: 500,
color: "white",
}}
>
json-render
</span>
</div>
<div
style={{
display: "flex",
flex: 1,
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
{title.split("\n").map((line, i) => (
<span
key={i}
style={{
fontSize: 72,
fontFamily: "Geist",
fontWeight: 400,
color: "white",
letterSpacing: "-0.02em",
textAlign: "center",
lineHeight: 1.2,
}}
>
{line}
</span>
))}
</div>
</div>,
{
width: 1200,
height: 630,
fonts: [
{
name: "Geist",
data: geistRegular.buffer as ArrayBuffer,
style: "normal",
weight: 400,
},
{
name: "Geist Pixel Square",
data: geistPixelSquare.buffer as ArrayBuffer,
style: "normal",
weight: 500,
},
],
},
);
}
+4 -42
View File
@@ -1,44 +1,6 @@
import { ImageResponse } from "next/og";
import { getPageTitle, renderOgImage } from "./og-image";
export async function GET(request: Request) {
const geist = await fetch(new URL("/Geist-Regular.ttf", request.url)).then(
(res) => res.arrayBuffer(),
);
return new ImageResponse(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "black",
}}
>
<span
style={{
fontSize: 144,
fontFamily: "Geist",
fontWeight: 400,
color: "white",
letterSpacing: "-0.02em",
}}
>
json-render
</span>
</div>,
{
width: 1200,
height: 630,
fonts: [
{
name: "Geist",
data: geist,
style: "normal",
weight: 400,
},
],
},
);
export async function GET() {
const title = getPageTitle("")!;
return renderOgImage(title);
}
+7
View File
@@ -0,0 +1,7 @@
export default function PlaygroundLayout({
children,
}: {
children: React.ReactNode;
}) {
return <div className="h-dvh flex flex-col overflow-hidden">{children}</div>;
}
+4 -68
View File
@@ -1,72 +1,8 @@
import { Button } from "@/components/ui/button";
import { Playground } from "@/components/playground";
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = {
title: "Playground | json-render",
};
export const metadata = pageMetadata("playground");
export default function PlaygroundPage() {
return (
<div className="max-w-4xl mx-auto px-6 py-16">
<h1 className="text-3xl font-bold mb-4">Playground</h1>
<p className="text-muted-foreground mb-12">
Try json-render with a live example.
</p>
<div className="space-y-12">
<section>
<h2 className="text-xl font-semibold mb-4">Run locally</h2>
<p className="text-sm text-muted-foreground mb-4">
Clone the repository and run the example dashboard.
</p>
<pre className="text-sm mb-4">
<code>{`git clone https://github.com/vercel-labs/json-render
cd json-render
pnpm install
pnpm dev`}</code>
</pre>
<p className="text-sm text-muted-foreground">
Open <code>http://localhost:3001</code> for the example dashboard.
</p>
</section>
<section>
<h2 className="text-xl font-semibold mb-4">Example prompts</h2>
<p className="text-sm text-muted-foreground mb-4">
Try these prompts in the example dashboard:
</p>
<div className="space-y-2">
{[
"Create a revenue dashboard with monthly metrics",
"Build a user management panel with a table",
"Design a settings form with text inputs",
"Make a notification center with alerts",
].map((prompt) => (
<div
key={prompt}
className="p-3 border border-border rounded text-sm font-mono"
>
{prompt}
</div>
))}
</div>
</section>
<section>
<h2 className="text-xl font-semibold mb-4">Interactive playground</h2>
<p className="text-sm text-muted-foreground mb-6">
A browser-based playground is coming soon.
</p>
<Button variant="outline" asChild>
<a
href="https://github.com/vercel-labs/json-render"
target="_blank"
rel="noopener noreferrer"
>
Star on GitHub
</a>
</Button>
</section>
</div>
</div>
);
return <Playground />;
}
+2 -2
View File
@@ -145,7 +145,7 @@ function getHighlighter() {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
themes: [vercelLightTheme, vercelDarkTheme],
langs: ["json", "tsx", "typescript"],
langs: ["json", "tsx", "typescript", "yaml"],
});
}
return highlighterPromise;
@@ -158,7 +158,7 @@ if (typeof window !== "undefined") {
interface CodeBlockProps {
code: string;
lang: "json" | "tsx" | "typescript";
lang: "json" | "tsx" | "typescript" | "yaml";
fillHeight?: boolean;
hideCopyButton?: boolean;
}
+7 -4
View File
@@ -1,5 +1,6 @@
import { codeToHtml } from "shiki";
import { CopyButton } from "./copy-button";
import { ExpandableCode } from "./expandable-code";
const vercelDarkTheme = {
name: "vercel-dark",
@@ -158,10 +159,12 @@ export async function Code({ children, lang = "typescript" }: CodeProps) {
className="opacity-0 group-hover:opacity-100 text-neutral-500 dark:text-neutral-400 bg-neutral-100 dark:bg-[#0a0a0a]"
/>
</div>
<div
className="overflow-x-auto [&_pre]:bg-transparent! [&_pre]:m-0! [&_pre]:p-4! [&_code]:bg-transparent! [&_.shiki]:bg-transparent!"
dangerouslySetInnerHTML={{ __html: html }}
/>
<ExpandableCode>
<div
className="overflow-x-auto [&_pre]:bg-transparent! [&_pre]:m-0! [&_pre]:p-4! [&_code]:bg-transparent! [&_.shiki]:bg-transparent!"
dangerouslySetInnerHTML={{ __html: html }}
/>
</ExpandableCode>
</div>
);
}
+71
View File
@@ -0,0 +1,71 @@
"use client";
import { useState } from "react";
import { usePathname } from "next/navigation";
export function CopyPageButton() {
const pathname = usePathname();
const [state, setState] = useState<"idle" | "loading" | "copied">("idle");
const handleCopy = async () => {
setState("loading");
try {
const response = await fetch(
`/api/docs-markdown?path=${encodeURIComponent(pathname)}`,
);
if (!response.ok) {
throw new Error("Failed to fetch markdown");
}
const markdown = await response.text();
await navigator.clipboard.writeText(markdown);
setState("copied");
setTimeout(() => setState("idle"), 2000);
} catch {
setState("idle");
}
};
return (
<button
onClick={handleCopy}
disabled={state === "loading"}
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs text-muted-foreground hover:text-foreground border border-border rounded-md hover:bg-muted transition-colors disabled:opacity-50"
aria-label="Copy page as Markdown"
>
{state === "copied" ? (
<>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
Copied
</>
) : (
<>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
Copy Page
</>
)}
</button>
);
}
+373 -175
View File
@@ -7,168 +7,233 @@ import React, {
useRef,
useMemo,
} from "react";
import { Renderer, useUIStream, JSONUIProvider } from "@json-render/react";
import type { UITree } from "@json-render/core";
import { useUIStream } from "@json-render/react";
import type { Spec } from "@json-render/core";
import { collectUsedComponents, serializeProps } from "@json-render/codegen";
import { toast } from "sonner";
import { CodeBlock } from "./code-block";
import { CopyButton } from "./copy-button";
import { Toaster } from "./ui/sonner";
import {
demoRegistry,
fallbackComponent,
useInteractiveState,
} from "./demo/index";
import { PlaygroundRenderer } from "@/lib/render/renderer";
import { playgroundCatalog } from "@/lib/render/catalog";
import { buildCatalogDisplayData } from "@/lib/render/catalog-display";
const SIMULATION_PROMPT = "Create a contact form with name, email, and message";
interface SimulationStage {
tree: UITree;
tree: Spec;
stream: string;
}
// Shared state & element definitions for the progressive simulation stages.
const FORM_STATE = { form: { name: "", email: "", message: "" } };
const NAME_INPUT = {
type: "Input",
props: {
label: "Name",
name: "name",
statePath: "/form/name",
checks: [{ type: "required", message: "Name is required" }],
},
} as const;
const EMAIL_INPUT = {
type: "Input",
props: {
label: "Email",
name: "email",
type: "email",
statePath: "/form/email",
checks: [
{ type: "required", message: "Email is required" },
{ type: "email", message: "Please enter a valid email" },
],
},
} as const;
const MESSAGE_INPUT = {
type: "Textarea",
props: {
label: "Message",
name: "message",
statePath: "/form/message",
checks: [{ type: "required", message: "Message is required" }],
},
} as const;
const SUBMIT_BUTTON = {
type: "Button",
props: { label: "Send Message", variant: "primary" },
on: { press: { action: "formSubmit" } },
} as const;
const SIMULATION_STAGES: SimulationStage[] = [
{
tree: {
root: "card",
state: FORM_STATE,
elements: {
card: {
key: "card",
type: "Card",
props: { title: "Contact Us", maxWidth: "md" },
children: [],
},
},
},
stream: '{"op":"set","path":"/root","value":"card"}',
stream: '{"op":"add","path":"/root","value":"card"}',
},
{
tree: {
root: "card",
state: FORM_STATE,
elements: {
card: {
key: "card",
type: "Card",
props: { title: "Contact Us", maxWidth: "md" },
children: ["name"],
},
name: {
key: "name",
type: "Input",
props: { label: "Name", name: "name" },
},
name: NAME_INPUT,
},
},
stream:
'{"op":"add","path":"/elements/card","value":{"key":"card","type":"Card","props":{"title":"Contact Us","maxWidth":"md"},"children":["name"]}}',
'{"op":"add","path":"/elements/name","value":{"type":"Input","props":{"label":"Name","name":"name","statePath":"/form/name","checks":[{"type":"required","message":"Name is required"}]}}}',
},
{
tree: {
root: "card",
state: FORM_STATE,
elements: {
card: {
key: "card",
type: "Card",
props: { title: "Contact Us", maxWidth: "md" },
children: ["name", "email"],
},
name: {
key: "name",
type: "Input",
props: { label: "Name", name: "name" },
},
email: {
key: "email",
type: "Input",
props: { label: "Email", name: "email" },
},
name: NAME_INPUT,
email: EMAIL_INPUT,
},
},
stream:
'{"op":"add","path":"/elements/email","value":{"key":"email","type":"Input","props":{"label":"Email","name":"email"}}}',
'{"op":"add","path":"/elements/email","value":{"type":"Input","props":{"label":"Email","name":"email","type":"email","statePath":"/form/email","checks":[{"type":"required","message":"Email is required"},{"type":"email","message":"Please enter a valid email"}]}}}',
},
{
tree: {
root: "card",
state: FORM_STATE,
elements: {
card: {
key: "card",
type: "Card",
props: { title: "Contact Us", maxWidth: "md" },
children: ["name", "email", "message"],
},
name: {
key: "name",
type: "Input",
props: { label: "Name", name: "name" },
},
email: {
key: "email",
type: "Input",
props: { label: "Email", name: "email" },
},
message: {
key: "message",
type: "Textarea",
props: { label: "Message", name: "message" },
},
name: NAME_INPUT,
email: EMAIL_INPUT,
message: MESSAGE_INPUT,
},
},
stream:
'{"op":"add","path":"/elements/message","value":{"key":"message","type":"Textarea","props":{"label":"Message","name":"message"}}}',
'{"op":"add","path":"/elements/message","value":{"type":"Textarea","props":{"label":"Message","name":"message","statePath":"/form/message","checks":[{"type":"required","message":"Message is required"}]}}}',
},
{
tree: {
root: "card",
state: FORM_STATE,
elements: {
card: {
key: "card",
type: "Card",
props: { title: "Contact Us", maxWidth: "md" },
children: ["name", "email", "message", "submit"],
},
name: {
key: "name",
type: "Input",
props: { label: "Name", name: "name" },
},
email: {
key: "email",
type: "Input",
props: { label: "Email", name: "email" },
},
message: {
key: "message",
type: "Textarea",
props: { label: "Message", name: "message" },
},
submit: {
key: "submit",
type: "Button",
props: { label: "Send Message", variant: "primary" },
},
name: NAME_INPUT,
email: EMAIL_INPUT,
message: MESSAGE_INPUT,
submit: SUBMIT_BUTTON,
},
},
stream:
'{"op":"add","path":"/elements/submit","value":{"key":"submit","type":"Button","props":{"label":"Send Message","variant":"primary"}}}',
'{"op":"add","path":"/elements/submit","value":{"type":"Button","props":{"label":"Send Message","variant":"primary"},"on":{"press":{"action":"formSubmit"}}}}',
},
];
type Mode = "simulation" | "interactive";
type Phase = "typing" | "streaming" | "complete";
type Tab = "stream" | "json";
type Tab = "stream" | "json" | "nested" | "catalog";
type RenderView = "dynamic" | "static";
export function Demo() {
const [mode, setMode] = useState<Mode>("simulation");
const [phase, setPhase] = useState<Phase>("typing");
interface DemoProps {
fullscreen?: boolean;
skipSimulation?: boolean;
}
/**
* Convert a flat Spec into a nested tree structure that is easier for humans
* to read. Children keys are resolved recursively into inline objects.
*/
function specToNested(spec: Spec): Record<string, unknown> {
function resolve(key: string): Record<string, unknown> {
const el = spec.elements[key];
if (!el) return { _key: key, _missing: true };
const node: Record<string, unknown> = { type: el.type };
if (el.props && Object.keys(el.props).length > 0) {
node.props = el.props;
}
if (el.visible !== undefined) {
node.visible = el.visible;
}
if (el.on && Object.keys(el.on).length > 0) {
node.on = el.on;
}
if (el.repeat) {
node.repeat = el.repeat;
}
if (el.children && el.children.length > 0) {
node.children = el.children.map(resolve);
}
return node;
}
const result: Record<string, unknown> = {};
if (spec.state && Object.keys(spec.state).length > 0) {
result.state = spec.state;
}
result.elements = resolve(spec.root);
return result;
}
const EXAMPLE_PROMPTS = [
"Create a login form with email and password",
"Build a feedback form with rating stars",
"Design a contact card with avatar",
"Make a settings panel with toggles",
];
export function Demo({
fullscreen = false,
skipSimulation = false,
}: DemoProps) {
const [mode, setMode] = useState<Mode>(
skipSimulation ? "interactive" : "simulation",
);
const [phase, setPhase] = useState<Phase>(
skipSimulation ? "complete" : "typing",
);
const [typedPrompt, setTypedPrompt] = useState("");
const [userPrompt, setUserPrompt] = useState("");
const [stageIndex, setStageIndex] = useState(-1);
const [streamLines, setStreamLines] = useState<string[]>([]);
const [activeTab, setActiveTab] = useState<Tab>("json");
const [renderView, setRenderView] = useState<RenderView>("dynamic");
const [simulationTree, setSimulationTree] = useState<UITree | null>(null);
const [simulationTree, setSimulationTree] = useState<Spec | null>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const [showExportModal, setShowExportModal] = useState(false);
const [selectedExportFile, setSelectedExportFile] = useState<string | null>(
@@ -179,6 +244,15 @@ export function Demo() {
new Set(),
);
const inputRef = useRef<HTMLInputElement>(null);
const [catalogSection, setCatalogSection] = useState<
"components" | "actions"
>("components");
// Catalog data for the catalog tab
const catalogData = useMemo(
() => buildCatalogDisplayData(playgroundCatalog.data),
[],
);
// Disable body scroll when any modal is open
useEffect(() => {
@@ -194,18 +268,19 @@ export function Demo() {
// Use the library's useUIStream hook for real API calls
const {
tree: apiTree,
spec: apiSpec,
isStreaming,
send,
clear,
rawLines: apiRawLines,
} = useUIStream({
api: "/api/generate",
onError: (err: Error) => console.error("Generation error:", err),
onError: (err: Error) => {
console.error("Generation error:", err);
toast.error(err.message || "Generation failed. Please try again.");
},
} as Parameters<typeof useUIStream>[0]);
// Initialize interactive state for Select components
useInteractiveState();
const currentSimulationStage =
stageIndex >= 0 ? SIMULATION_STAGES[stageIndex] : null;
@@ -213,7 +288,7 @@ export function Demo() {
const currentTree =
mode === "simulation"
? currentSimulationStage?.tree || simulationTree
: apiTree || simulationTree;
: apiSpec || simulationTree;
const stopGeneration = useCallback(() => {
if (mode === "simulation") {
@@ -270,25 +345,12 @@ export function Demo() {
return () => clearInterval(interval);
}, [mode, phase]);
// Track stream lines from real API
// Track stream lines from real API (use raw JSONL patch lines)
useEffect(() => {
if (mode === "interactive" && apiTree) {
// Convert tree to stream line for display
const streamLine = JSON.stringify({ tree: apiTree });
if (
!streamLines.includes(streamLine) &&
Object.keys(apiTree.elements).length > 0
) {
setStreamLines((prev) => {
const lastLine = prev[prev.length - 1];
if (lastLine !== streamLine) {
return [...prev, streamLine];
}
return prev;
});
}
if (mode === "interactive" && apiRawLines.length > 0) {
setStreamLines(apiRawLines);
}
}, [mode, apiTree, streamLines]);
}, [mode, apiRawLines]);
const handleSubmit = useCallback(async () => {
if (!userPrompt.trim() || isStreaming) return;
@@ -296,23 +358,15 @@ export function Demo() {
await send(userPrompt);
}, [userPrompt, isStreaming, send]);
// Expose action handler for registry components - shows toast with text
useEffect(() => {
(
window as unknown as { __demoAction?: (text: string) => void }
).__demoAction = (text: string) => {
toast(text);
};
return () => {
delete (window as unknown as { __demoAction?: (text: string) => void })
.__demoAction;
};
}, []);
const jsonCode = currentTree
? JSON.stringify(currentTree, null, 2)
: "// waiting...";
const nestedCode = useMemo(() => {
if (!currentTree || !currentTree.root) return "// waiting...";
return JSON.stringify(specToNested(currentTree), null, 2);
}, [currentTree]);
// Generate all export files for Next.js project
const exportedFiles = useMemo(() => {
if (!currentTree || !currentTree.root) {
@@ -909,10 +963,25 @@ Open [http://localhost:3000](http://localhost:3000) to view.
const isStreamingSimulation = mode === "simulation" && phase === "streaming";
const showLoadingDots = isStreamingSimulation || isStreaming;
const handleExampleClick = useCallback((prompt: string) => {
setMode("interactive");
setPhase("complete");
setUserPrompt(prompt);
setTimeout(() => {
const el = inputRef.current;
if (el) {
el.focus();
el.setSelectionRange(prompt.length, prompt.length);
}
}, 0);
}, []);
return (
<div className="w-full max-w-4xl mx-auto text-left">
<div
className={`w-full text-left ${fullscreen ? "h-full flex flex-col" : "max-w-5xl mx-auto"}`}
>
{/* Prompt input */}
<div className="mb-6">
<div className={fullscreen ? "mb-4" : "mb-6"}>
<div
className="border border-border rounded p-3 bg-background font-mono text-sm min-h-[44px] flex items-center justify-between cursor-text"
onClick={() => {
@@ -1000,17 +1069,40 @@ Open [http://localhost:3000](http://localhost:3000) to view.
</button>
)}
</div>
<div className="mt-2 text-xs text-muted-foreground text-center">
Try: &quot;Create a login form&quot; or &quot;Build a feedback form
with rating&quot;
</div>
{fullscreen ? (
<div className="mt-3 flex flex-wrap gap-2 justify-center">
{EXAMPLE_PROMPTS.map((prompt) => (
<button
key={prompt}
onClick={() => handleExampleClick(prompt)}
className="text-xs px-3 py-1.5 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground/50 transition-colors"
>
{prompt}
</button>
))}
</div>
) : (
<div className="mt-2 flex flex-wrap gap-1.5 justify-center">
{EXAMPLE_PROMPTS.slice(0, 2).map((prompt) => (
<button
key={prompt}
onClick={() => handleExampleClick(prompt)}
className="text-xs px-2 py-1 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground/50 transition-colors"
>
{prompt}
</button>
))}
</div>
)}
</div>
<div className="grid lg:grid-cols-2 gap-4">
<div
className={`grid lg:grid-cols-2 gap-4 ${fullscreen ? "flex-1 min-h-0" : ""}`}
>
{/* Tabbed code/stream/json panel */}
<div className="min-w-0">
<div className="flex items-center gap-4 mb-2 h-6">
{(["json", "stream"] as const).map((tab) => (
<div className={`min-w-0 ${fullscreen ? "flex flex-col" : ""}`}>
<div className="flex items-center gap-4 mb-2 h-6 shrink-0">
{(["json", "nested", "stream", "catalog"] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
@@ -1024,17 +1116,25 @@ Open [http://localhost:3000](http://localhost:3000) to view.
</button>
))}
</div>
<div className="border border-border rounded bg-background font-mono text-xs h-96 text-left grid relative group">
<div className="absolute top-2 right-2 z-10">
<CopyButton
text={
activeTab === "stream" ? streamLines.join("\n") : jsonCode
}
className="opacity-0 group-hover:opacity-100 text-muted-foreground"
/>
</div>
<div
className={`border border-border rounded bg-background font-mono text-xs text-left grid relative group ${fullscreen ? "flex-1 min-h-0" : "h-[28rem]"}`}
>
{activeTab !== "catalog" && (
<div className="absolute top-2 right-2 z-10">
<CopyButton
text={
activeTab === "stream"
? streamLines.join("\n")
: activeTab === "nested"
? nestedCode
: jsonCode
}
className="opacity-0 group-hover:opacity-100 text-muted-foreground"
/>
</div>
)}
<div
className={`overflow-auto h-full ${activeTab === "stream" ? "" : "hidden"}`}
className={`overflow-auto ${activeTab === "stream" ? "" : "hidden"}`}
>
{streamLines.length > 0 ? (
<>
@@ -1059,7 +1159,7 @@ Open [http://localhost:3000](http://localhost:3000) to view.
)}
</div>
<div
className={`overflow-auto h-full ${activeTab === "json" ? "" : "hidden"}`}
className={`overflow-auto ${activeTab === "json" ? "" : "hidden"}`}
>
<CodeBlock
code={jsonCode}
@@ -1068,12 +1168,142 @@ Open [http://localhost:3000](http://localhost:3000) to view.
hideCopyButton
/>
</div>
<div
className={`overflow-auto ${activeTab === "nested" ? "" : "hidden"}`}
>
<CodeBlock
code={nestedCode}
lang="json"
fillHeight
hideCopyButton
/>
</div>
<div
className={`overflow-auto ${activeTab === "catalog" ? "" : "hidden"}`}
>
<div className="h-full flex flex-col text-sm font-sans">
<div className="flex items-center gap-3 px-3 h-9 border-b border-border">
{(
[
{
key: "components",
label: `components (${catalogData.components.length})`,
},
{
key: "actions",
label: `actions (${catalogData.actions.length})`,
},
] as const
).map(({ key, label }) => (
<button
key={key}
onClick={() => setCatalogSection(key)}
className={`text-xs font-mono transition-colors ${
catalogSection === key
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
{label}
</button>
))}
</div>
<div className="flex-1 overflow-auto p-3">
{catalogSection === "components" ? (
<div className="space-y-3">
{catalogData.components.map((comp) => (
<div
key={comp.name}
className="pb-3 border-b border-border last:border-b-0"
>
<div className="flex items-baseline gap-2 mb-1">
<span className="font-mono font-medium text-foreground">
{comp.name}
</span>
{comp.slots.length > 0 && (
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-muted text-muted-foreground">
slots: {comp.slots.join(", ")}
</span>
)}
</div>
{comp.description && (
<p className="text-xs text-muted-foreground mb-2">
{comp.description}
</p>
)}
{comp.props.length > 0 && (
<div className="flex flex-wrap gap-1 mb-1">
{comp.props.map((p) => (
<span
key={p.name}
className="text-[11px] font-mono px-1.5 py-0.5 rounded bg-green-500/10 text-green-700 dark:text-green-400"
>
{p.name}
<span className="text-green-700/50 dark:text-green-400/50">
: {p.type}
</span>
</span>
))}
</div>
)}
{comp.events.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1.5">
{comp.events.map((e) => (
<span
key={e}
className="text-[11px] font-mono px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400"
>
on.{e}
</span>
))}
</div>
)}
</div>
))}
</div>
) : (
<div className="space-y-3">
{catalogData.actions.map((action) => (
<div
key={action.name}
className="pb-3 border-b border-border last:border-b-0"
>
<span className="font-mono font-medium text-foreground">
{action.name}
</span>
{action.description && (
<p className="text-xs text-muted-foreground mt-1 mb-2">
{action.description}
</p>
)}
{action.params.length > 0 && (
<div className="flex flex-wrap gap-1">
{action.params.map((p) => (
<span
key={p.name}
className="text-[11px] font-mono px-1.5 py-0.5 rounded bg-green-500/10 text-green-700 dark:text-green-400"
>
{p.name}
<span className="text-green-700/50 dark:text-green-400/50">
: {p.type}
</span>
</span>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
</div>
{/* Rendered output using json-render */}
<div className="min-w-0">
<div className="flex items-center justify-between mb-2 h-6">
<div className={`min-w-0 ${fullscreen ? "flex flex-col" : ""}`}>
<div className="flex items-center justify-between mb-2 h-6 shrink-0">
<div className="flex items-center gap-4">
{(
[
@@ -1126,7 +1356,9 @@ Open [http://localhost:3000](http://localhost:3000) to view.
</button>
</div>
</div>
<div className="border border-border rounded bg-background h-96 grid relative group">
<div
className={`border border-border rounded bg-background grid relative group ${fullscreen ? "flex-1 min-h-0" : "h-[28rem]"}`}
>
{renderView === "static" && (
<div className="absolute top-2 right-2 z-10">
<CopyButton
@@ -1139,28 +1371,10 @@ Open [http://localhost:3000](http://localhost:3000) to view.
<div className="overflow-auto">
{currentTree && currentTree.root ? (
<div className="animate-in fade-in duration-200 w-full min-h-full flex items-center justify-center p-3 py-4">
<JSONUIProvider
registry={
demoRegistry as Parameters<
typeof JSONUIProvider
>[0]["registry"]
}
>
<Renderer
tree={currentTree}
registry={
demoRegistry as Parameters<
typeof Renderer
>[0]["registry"]
}
loading={isStreaming || isStreamingSimulation}
fallback={
fallbackComponent as Parameters<
typeof Renderer
>[0]["fallback"]
}
/>
</JSONUIProvider>
<PlaygroundRenderer
spec={currentTree}
loading={isStreaming || isStreamingSimulation}
/>
</div>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground/50 text-sm">
@@ -1211,26 +1425,10 @@ Open [http://localhost:3000](http://localhost:3000) to view.
<div className="flex-1 overflow-auto p-6">
{currentTree && currentTree.root ? (
<div className="w-full min-h-full flex items-center justify-center">
<JSONUIProvider
registry={
demoRegistry as Parameters<
typeof JSONUIProvider
>[0]["registry"]
}
>
<Renderer
tree={currentTree}
registry={
demoRegistry as Parameters<typeof Renderer>[0]["registry"]
}
loading={isStreaming || isStreamingSimulation}
fallback={
fallbackComponent as Parameters<
typeof Renderer
>[0]["fallback"]
}
/>
</JSONUIProvider>
<PlaygroundRenderer
spec={currentTree}
loading={isStreaming || isStreamingSimulation}
/>
</div>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground/50 text-sm">
-29
View File
@@ -1,29 +0,0 @@
"use client";
import type { ComponentRenderProps } from "./types";
import { baseClass, getCustomClass } from "./utils";
export function Alert({ element }: ComponentRenderProps) {
const { props } = element;
const customClass = getCustomClass(props);
const alertType = props.type as string;
const alertClass =
alertType === "success"
? "bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800 text-green-900 dark:text-green-100"
: alertType === "warning"
? "bg-yellow-50 dark:bg-yellow-950 border-yellow-200 dark:border-yellow-800 text-yellow-900 dark:text-yellow-100"
: alertType === "error"
? "bg-red-50 dark:bg-red-950 border-red-200 dark:border-red-800 text-red-900 dark:text-red-100"
: "bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800 text-blue-900 dark:text-blue-100";
return (
<div
className={`p-2 rounded border ${alertClass} ${baseClass} ${customClass}`}
>
<div className="text-xs font-medium">{props.title as string}</div>
{props.message ? (
<div className="text-[10px] mt-0.5">{props.message as string}</div>
) : null}
</div>
);
}
-30
View File
@@ -1,30 +0,0 @@
"use client";
import type { ComponentRenderProps } from "./types";
import { baseClass, getCustomClass } from "./utils";
export function Avatar({ element }: ComponentRenderProps) {
const { props } = element;
const customClass = getCustomClass(props);
const name = (props.name as string) || "?";
const initials = name
.split(" ")
.map((n) => n[0])
.join("")
.slice(0, 2)
.toUpperCase();
const avatarSize =
props.size === "lg"
? "w-10 h-10 text-sm"
: props.size === "sm"
? "w-6 h-6 text-[8px]"
: "w-8 h-8 text-[10px]";
return (
<div
className={`${avatarSize} rounded-full bg-muted flex items-center justify-center font-medium ${baseClass} ${customClass}`}
>
{initials}
</div>
);
}
-26
View File
@@ -1,26 +0,0 @@
"use client";
import type { ComponentRenderProps } from "./types";
import { baseClass, getCustomClass } from "./utils";
export function Badge({ element }: ComponentRenderProps) {
const { props } = element;
const customClass = getCustomClass(props);
const badgeVariant = props.variant as string;
const badgeClass =
badgeVariant === "success"
? "bg-green-100 text-green-800"
: badgeVariant === "warning"
? "bg-yellow-100 text-yellow-800"
: badgeVariant === "danger"
? "bg-red-100 text-red-800"
: "bg-muted text-foreground";
return (
<span
className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${badgeClass} ${baseClass} ${customClass}`}
>
{props.text as string}
</span>
);
}
-44
View File
@@ -1,44 +0,0 @@
"use client";
import type { ComponentRenderProps } from "./types";
import { baseClass, getCustomClass } from "./utils";
interface DataPoint {
label: string;
value: number;
}
export function BarGraph({ element }: ComponentRenderProps) {
const { props } = element;
const customClass = getCustomClass(props);
const data = (props.data as DataPoint[]) || [];
const title = props.title as string | undefined;
const maxValue = Math.max(...data.map((d) => d.value), 1);
return (
<div className={`${baseClass} ${customClass}`}>
{title ? (
<div className="text-xs font-medium mb-2 text-left">{title}</div>
) : null}
<div className="flex gap-1">
{data.map((d, i) => (
<div key={i} className="flex-1 flex flex-col items-center gap-1">
<div className="text-[8px] text-muted-foreground">{d.value}</div>
<div className="w-full h-20 flex items-end">
<div
className="w-full bg-foreground/80 rounded-t transition-all"
style={{
height: `${(d.value / maxValue) * 100}%`,
minHeight: 2,
}}
/>
</div>
<div className="text-[8px] text-muted-foreground truncate w-full text-center">
{d.label}
</div>
</div>
))}
</div>
</div>
);
}
-32
View File
@@ -1,32 +0,0 @@
"use client";
import type { ComponentRenderProps } from "./types";
import { baseClass, getCustomClass } from "./utils";
export function Button({ element }: ComponentRenderProps) {
const { props } = element;
const customClass = getCustomClass(props);
const variant = props.variant as string;
const label = props.label as string;
const actionText = (props.actionText as string) || label;
const btnClass =
variant === "danger"
? "bg-red-500 text-white"
: variant === "secondary"
? "bg-card border border-border text-foreground"
: "bg-foreground text-background";
return (
<button
type="button"
onClick={() =>
(
window as unknown as { __demoAction?: (text: string) => void }
).__demoAction?.(actionText)
}
className={`self-start px-3 py-1.5 rounded text-xs font-medium hover:opacity-90 transition-opacity ${btnClass} ${baseClass} ${customClass}`}
>
{label}
</button>
);
}
-36
View File
@@ -1,36 +0,0 @@
"use client";
import type { ComponentRenderProps } from "./types";
import { baseClass, getCustomClass } from "./utils";
export function Card({ element, children }: ComponentRenderProps) {
const { props } = element;
const customClass = getCustomClass(props);
const maxWidthClass =
props.maxWidth === "sm"
? "max-w-xs sm:min-w-[280px]"
: props.maxWidth === "md"
? "max-w-sm sm:min-w-[320px]"
: props.maxWidth === "lg"
? "max-w-md sm:min-w-[360px]"
: "w-full";
const centeredClass = props.centered ? "mx-auto" : "";
return (
<div
className={`border border-border rounded-lg p-3 bg-background overflow-hidden ${maxWidthClass} ${centeredClass} ${baseClass} ${customClass}`}
>
{props.title ? (
<div className="font-semibold text-sm mb-1 text-left">
{props.title as string}
</div>
) : null}
{props.description ? (
<div className="text-[10px] text-muted-foreground mb-2 text-left">
{props.description as string}
</div>
) : null}
<div className="space-y-2">{children}</div>
</div>
);
}
-39
View File
@@ -1,39 +0,0 @@
"use client";
import { useState } from "react";
import type { ComponentRenderProps } from "./types";
import { baseClass, getCustomClass } from "./utils";
export function Checkbox({ element }: ComponentRenderProps) {
const { props } = element;
const customClass = getCustomClass(props);
const [checked, setChecked] = useState(!!props.checked);
return (
<label
className={`flex items-center gap-2 text-xs cursor-pointer ${baseClass} ${customClass}`}
onClick={() => setChecked((prev) => !prev)}
>
<div
className={`w-3.5 h-3.5 border border-border rounded-sm flex items-center justify-center transition-colors ${checked ? "bg-foreground" : "bg-background"}`}
>
{checked && (
<svg
className="w-2.5 h-2.5 text-background"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
)}
</div>
{props.label as string}
</label>
);
}
-9
View File
@@ -1,9 +0,0 @@
"use client";
import type { ComponentRenderProps } from "./types";
import { baseClass, getCustomClass } from "./utils";
export function Divider({ element }: ComponentRenderProps) {
const customClass = getCustomClass(element.props);
return <hr className={`border-border my-2 ${baseClass} ${customClass}`} />;
}
-15
View File
@@ -1,15 +0,0 @@
"use client";
import type { ComponentRenderProps } from "./types";
import { baseClass, getCustomClass } from "./utils";
export function Fallback({ element }: ComponentRenderProps) {
const customClass = getCustomClass(element.props);
return (
<div
className={`text-[10px] text-muted-foreground ${baseClass} ${customClass}`}
>
[{element.type}]
</div>
);
}

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