Compare commits

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

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