mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 04:18:15 +08:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
726ddc1d4f | ||
|
|
429e456a4f | ||
|
|
0e16ca33d4 | ||
|
|
edbeb5a637 | ||
|
|
d9a4efdbeb | ||
|
|
f435643817 | ||
|
|
458f3a728c | ||
|
|
d5734e975c | ||
|
|
3d2d1adb2d |
@@ -1,22 +1,25 @@
|
|||||||
# json-render
|
# json-render
|
||||||
|
|
||||||
**Predictable. Guardrailed. Fast.**
|
**The framework for User-Generated Interfaces (UGI).**
|
||||||
|
|
||||||
Let end users generate dashboards, widgets, apps, and videos from prompts — safely constrained to components you define.
|
Dynamic, personalized UIs per user without sacrificing reliability. Predefined components and actions for safe, predictable output.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @json-render/core @json-render/react
|
npm install @json-render/core @json-render/react
|
||||||
|
# or for mobile
|
||||||
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why json-render?
|
## Why json-render?
|
||||||
|
|
||||||
When users prompt for UI, you need guarantees. json-render gives AI a **constrained vocabulary** so output is always predictable:
|
json-render enables **User-Generated Interfaces**: dynamic UIs that end users create through natural language prompts, powered by Generative UI. You define the guardrails, AI generates within them:
|
||||||
|
|
||||||
- **Guardrailed** — AI can only use components in your catalog
|
- **Guardrailed** - AI can only use components in your catalog
|
||||||
- **Predictable** — JSON output matches your schema, every time
|
- **Predictable** - JSON output matches your schema, every time
|
||||||
- **Fast** — Stream and render progressively as the model responds
|
- **Fast** - Stream and render progressively as the model responds
|
||||||
|
- **Cross-Platform** - React (web) and React Native (mobile) from the same catalog
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -75,8 +78,8 @@ const { registry } = defineRegistry(catalog, {
|
|||||||
<span>{format(props.value, props.format)}</span>
|
<span>{format(props.value, props.format)}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
Button: ({ props, onAction }) => (
|
Button: ({ props, emit }) => (
|
||||||
<button onClick={() => onAction?.({ name: props.action })}>
|
<button onClick={() => emit?.("press")}>
|
||||||
{props.label}
|
{props.label}
|
||||||
</button>
|
</button>
|
||||||
),
|
),
|
||||||
@@ -129,6 +132,27 @@ const { registry } = defineRegistry(catalog, { components });
|
|||||||
<Renderer spec={spec} registry={registry} />
|
<Renderer spec={spec} registry={registry} />
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### React Native (Mobile)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { defineCatalog } from "@json-render/core";
|
||||||
|
import { schema } from "@json-render/react-native/schema";
|
||||||
|
import {
|
||||||
|
standardComponentDefinitions,
|
||||||
|
standardActionDefinitions,
|
||||||
|
} from "@json-render/react-native/catalog";
|
||||||
|
import { defineRegistry, Renderer } from "@json-render/react-native";
|
||||||
|
|
||||||
|
// 25+ standard components included
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: { ...standardComponentDefinitions },
|
||||||
|
actions: standardActionDefinitions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, { components: {} });
|
||||||
|
<Renderer spec={spec} registry={registry} />
|
||||||
|
```
|
||||||
|
|
||||||
### Remotion (Video)
|
### Remotion (Video)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
@@ -214,8 +238,8 @@ Any prop value can be data-driven using expressions:
|
|||||||
|
|
||||||
Two expression forms:
|
Two expression forms:
|
||||||
|
|
||||||
- **`{ "$path": "/state/key" }`** -- reads a value from the data model
|
- **`{ "$path": "/state/key" }`** - reads a value from the data model
|
||||||
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** -- evaluates a condition (same syntax as visibility conditions) and picks a branch
|
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - evaluates a condition (same syntax as visibility conditions) and picks a branch
|
||||||
|
|
||||||
### Actions
|
### Actions
|
||||||
|
|
||||||
@@ -229,7 +253,7 @@ Components can trigger actions, including the built-in `setState` action:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `setState` action updates the data 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -242,9 +266,10 @@ pnpm install
|
|||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
- http://localhost:3000 — Docs & Playground
|
- http://localhost:3000 - Docs & Playground
|
||||||
- http://localhost:3001 — Example Dashboard
|
- http://localhost:3001 - Example Dashboard
|
||||||
- http://localhost:3002 — Remotion Video Example
|
- http://localhost:3002 - Remotion Video Example
|
||||||
|
- React Native example: run `npx expo start` in `examples/react-native`
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
@@ -259,10 +284,10 @@ flowchart LR
|
|||||||
D -.- G([streamed])
|
D -.- G([streamed])
|
||||||
```
|
```
|
||||||
|
|
||||||
1. **Define the guardrails** — what components, actions, and data bindings AI can use
|
1. **Define the guardrails** - what components, actions, and data bindings AI can use
|
||||||
2. **Users prompt** — end users describe what they want in natural language
|
2. **Users generate** - end users describe what they want in natural language
|
||||||
3. **AI generates JSON** — output is always predictable, constrained to your catalog
|
3. **AI generates JSON** - output is always predictable, constrained to your catalog
|
||||||
4. **Render fast** — stream and render progressively as the model responds
|
4. **Render fast** - stream and render progressively as the model responds
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
export const metadata = { title: "@json-render/react-native API" }
|
||||||
|
|
||||||
|
# @json-render/react-native
|
||||||
|
|
||||||
|
React Native renderer with standard components, providers, and hooks.
|
||||||
|
|
||||||
|
## Standard Components
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
| Component | Props | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| `Container` | `padding`, `background`, `borderRadius`, `borderColor`, `flex` | Basic wrapper with styling |
|
||||||
|
| `Row` | `gap`, `align`, `justify`, `flex`, `wrap` | Horizontal flex layout |
|
||||||
|
| `Column` | `gap`, `align`, `justify`, `flex` | Vertical flex layout |
|
||||||
|
| `ScrollContainer` | `direction` | Scrollable area (vertical or horizontal) |
|
||||||
|
| `SafeArea` | `edges` | Safe area insets for notch/home indicator |
|
||||||
|
| `Pressable` | `action`, `actionParams` | Touchable wrapper that triggers actions |
|
||||||
|
| `Spacer` | `size`, `flex` | Fixed or flexible spacing |
|
||||||
|
| `Divider` | `color`, `thickness` | Thin line separator |
|
||||||
|
|
||||||
|
### Content
|
||||||
|
|
||||||
|
| Component | Props | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| `Heading` | `text`, `level`, `align`, `color` | Heading text (levels 1-6) |
|
||||||
|
| `Paragraph` | `text`, `align`, `color` | Body text |
|
||||||
|
| `Label` | `text`, `color`, `bold` | Small label text |
|
||||||
|
| `Image` | `uri`, `width`, `height`, `resizeMode`, `borderRadius` | Image display |
|
||||||
|
| `Avatar` | `uri`, `size`, `fallback` | Circular avatar |
|
||||||
|
| `Badge` | `label`, `color`, `textColor` | Status badge |
|
||||||
|
| `Chip` | `label`, `selected`, `color` | Tag/chip |
|
||||||
|
|
||||||
|
### Input
|
||||||
|
|
||||||
|
| Component | Props | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| `Button` | `label`, `variant`, `size`, `disabled`, `action`, `actionParams` | Pressable button |
|
||||||
|
| `TextInput` | `placeholder`, `statePath`, `secure`, `keyboardType`, `multiline` | Text input field |
|
||||||
|
| `Switch` | `statePath`, `label` | Toggle switch |
|
||||||
|
| `Checkbox` | `statePath`, `label` | Checkbox with label |
|
||||||
|
| `Slider` | `statePath`, `min`, `max`, `step` | Range slider |
|
||||||
|
| `SearchBar` | `placeholder`, `statePath` | Search input |
|
||||||
|
|
||||||
|
### Feedback
|
||||||
|
|
||||||
|
| Component | Props | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| `Spinner` | `size`, `color` | Loading indicator |
|
||||||
|
| `ProgressBar` | `progress`, `color`, `trackColor` | Progress indicator |
|
||||||
|
|
||||||
|
### Composite
|
||||||
|
|
||||||
|
| Component | Props | Description |
|
||||||
|
|-----------|-------|-------------|
|
||||||
|
| `Card` | `title`, `subtitle`, `padding` | Card container |
|
||||||
|
| `ListItem` | `title`, `subtitle`, `leading`, `trailing`, `action`, `actionParams` | List row |
|
||||||
|
| `Modal` | `visible`, `title` | Bottom sheet modal |
|
||||||
|
|
||||||
|
## Providers
|
||||||
|
|
||||||
|
### StateProvider
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<StateProvider initialState={object}>
|
||||||
|
{children}
|
||||||
|
</StateProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ActionProvider
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ActionProvider handlers={Record<string, ActionHandler>}>
|
||||||
|
{children}
|
||||||
|
</ActionProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
### VisibilityProvider
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<VisibilityProvider>
|
||||||
|
{children}
|
||||||
|
</VisibilityProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ValidationProvider
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ValidationProvider>
|
||||||
|
{children}
|
||||||
|
</ValidationProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
## defineRegistry
|
||||||
|
|
||||||
|
Create a type-safe component registry. Standard components are built-in; only register custom components.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { defineRegistry, type Components } from '@json-render/react-native';
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Icon: ({ props }) => <Ionicons name={props.name} size={props.size ?? 24} />,
|
||||||
|
} as Components<typeof catalog>,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
### useUIStream
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const {
|
||||||
|
spec, // Spec | null - current UI state
|
||||||
|
isStreaming, // boolean - true while streaming
|
||||||
|
error, // Error | null
|
||||||
|
send, // (prompt: string) => Promise<void>
|
||||||
|
clear, // () => void - reset spec and error
|
||||||
|
} = useUIStream({
|
||||||
|
api: string,
|
||||||
|
onComplete?: (spec: Spec) => void,
|
||||||
|
onError?: (error: Error) => void,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### useStateStore
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { state, get, set, update } = useStateStore();
|
||||||
|
```
|
||||||
|
|
||||||
|
### useStateValue
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const value = useStateValue(path: string);
|
||||||
|
```
|
||||||
|
|
||||||
|
### useStateBinding
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const [value, setValue] = useStateBinding(path: string);
|
||||||
|
```
|
||||||
|
|
||||||
|
### useActions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { execute } = useActions();
|
||||||
|
```
|
||||||
|
|
||||||
|
### useIsVisible
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const isVisible = useIsVisible(condition?: VisibilityCondition);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Catalog Exports
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { standardComponentDefinitions, standardActionDefinitions } from "@json-render/react-native/catalog";
|
||||||
|
import { schema } from "@json-render/react-native/schema";
|
||||||
|
```
|
||||||
|
|
||||||
|
| Export | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `standardComponentDefinitions` | Catalog definitions for all 25+ standard components |
|
||||||
|
| `standardActionDefinitions` | Catalog definitions for standard actions (setState, navigate) |
|
||||||
|
| `schema` | React Native element tree schema |
|
||||||
@@ -49,7 +49,7 @@ type ValidatorFn = (value: unknown, args?: object) => boolean | Promise<boolean>
|
|||||||
|
|
||||||
## defineRegistry
|
## defineRegistry
|
||||||
|
|
||||||
Create a type-safe component registry from a catalog. Components receive `props`, `children`, `onAction`, and `loading` with catalog-inferred types.
|
Create a type-safe component registry from a catalog. Components receive `props`, `children`, `emit`, and `loading` with catalog-inferred types.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { defineRegistry } from '@json-render/react';
|
import { defineRegistry } from '@json-render/react';
|
||||||
@@ -57,8 +57,8 @@ import { defineRegistry } from '@json-render/react';
|
|||||||
const { registry } = defineRegistry(catalog, {
|
const { registry } = defineRegistry(catalog, {
|
||||||
components: {
|
components: {
|
||||||
Card: ({ props, children }) => <div>{props.title}{children}</div>,
|
Card: ({ props, children }) => <div>{props.title}{children}</div>,
|
||||||
Button: ({ props, onAction }) => (
|
Button: ({ props, emit }) => (
|
||||||
<button onClick={() => onAction?.({ name: props.action })}>
|
<button onClick={() => emit?.("press")}>
|
||||||
{props.label}
|
{props.label}
|
||||||
</button>
|
</button>
|
||||||
),
|
),
|
||||||
@@ -90,7 +90,7 @@ type Registry = Record<string, React.ComponentType<ComponentRenderProps>>;
|
|||||||
interface ComponentContext<P> {
|
interface ComponentContext<P> {
|
||||||
props: P; // Typed props from catalog
|
props: P; // Typed props from catalog
|
||||||
children?: React.ReactNode; // Rendered children (for slot components)
|
children?: React.ReactNode; // Rendered children (for slot components)
|
||||||
onAction?: (action: { name: string; params?: object }) => void;
|
emit?: (event: string) => void; // Emit a named event
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -4,6 +4,125 @@ export const metadata = { title: "Changelog" }
|
|||||||
|
|
||||||
Notable changes and updates to json-render.
|
Notable changes and updates to json-render.
|
||||||
|
|
||||||
|
## v0.5.0
|
||||||
|
|
||||||
|
February 2026
|
||||||
|
|
||||||
|
### New: @json-render/react-native
|
||||||
|
|
||||||
|
Full React Native renderer with 25+ standard components, data binding, visibility, actions, and dynamic props. Build AI-generated native mobile UIs with the same catalog-driven approach as web.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { defineCatalog } from "@json-render/core";
|
||||||
|
import { schema } from "@json-render/react-native/schema";
|
||||||
|
import {
|
||||||
|
standardComponentDefinitions,
|
||||||
|
standardActionDefinitions,
|
||||||
|
} from "@json-render/react-native/catalog";
|
||||||
|
import { defineRegistry, Renderer } from "@json-render/react-native";
|
||||||
|
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: { ...standardComponentDefinitions },
|
||||||
|
actions: standardActionDefinitions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, { components: {} });
|
||||||
|
|
||||||
|
<Renderer spec={spec} registry={registry} />
|
||||||
|
```
|
||||||
|
|
||||||
|
Includes standard components for layout (Container, Row, Column, ScrollContainer, SafeArea, Pressable, Spacer, Divider), content (Heading, Paragraph, Label, Image, Avatar, Badge, Chip), input (Button, TextInput, Switch, Checkbox, Slider, SearchBar), feedback (Spinner, ProgressBar), and composite (Card, ListItem, Modal).
|
||||||
|
|
||||||
|
### New: Event System
|
||||||
|
|
||||||
|
Components now use `emit` to fire named events instead of directly dispatching actions. The element's `on` field maps events to action bindings, decoupling component logic from action handling.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Component emits a named event
|
||||||
|
Button: ({ props, emit }) => (
|
||||||
|
<button onClick={() => emit?.("press")}>{props.label}</button>
|
||||||
|
),
|
||||||
|
|
||||||
|
// Element spec maps events to actions
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"props": { "label": "Submit" },
|
||||||
|
"on": { "press": { "action": "submit", "params": { "formId": "main" } } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### New: Repeat/List Rendering
|
||||||
|
|
||||||
|
Elements can now iterate over state arrays using the `repeat` field. Child elements use `$item` and `$index` tokens in `$path` expressions to reference the current item.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "Column",
|
||||||
|
"repeat": { "path": "/posts", "key": "id" },
|
||||||
|
"children": ["post-card"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "Card",
|
||||||
|
"props": { "title": { "$path": "$item/title" } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### New: User Prompt Builder
|
||||||
|
|
||||||
|
Build structured user prompts with optional spec refinement and state context:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { buildUserPrompt } from "@json-render/core";
|
||||||
|
|
||||||
|
// Fresh generation
|
||||||
|
buildUserPrompt({ prompt: "create a todo app" });
|
||||||
|
|
||||||
|
// Refinement (patch-only mode)
|
||||||
|
buildUserPrompt({ prompt: "add a toggle", currentSpec: spec });
|
||||||
|
|
||||||
|
// With runtime state
|
||||||
|
buildUserPrompt({ prompt: "show data", state: { todos: [] } });
|
||||||
|
```
|
||||||
|
|
||||||
|
### New: Spec Validation
|
||||||
|
|
||||||
|
Validate spec structure and auto-fix common issues:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { validateSpec, autoFixSpec } from "@json-render/core";
|
||||||
|
|
||||||
|
const { valid, issues } = validateSpec(spec, catalog);
|
||||||
|
const fixed = autoFixSpec(spec);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Improved: State Management
|
||||||
|
|
||||||
|
`DataProvider` has been renamed to `StateProvider` with a clearer API. State is now a first-class part of specs. Elements can bind to state via `$path` expressions, and the built-in `setState` action updates state directly.
|
||||||
|
|
||||||
|
### Improved: AI Prompts
|
||||||
|
|
||||||
|
Schema prompts now include streaming best practices, repeat/list examples, and state patching guidance. Schemas can also define `defaultRules` that are always included in generated prompts.
|
||||||
|
|
||||||
|
### Improved: Documentation
|
||||||
|
|
||||||
|
- All documentation pages migrated to MDX
|
||||||
|
- AI-powered documentation chat
|
||||||
|
- Dynamic Open Graph images for all docs pages
|
||||||
|
- Improved playground
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- `DataProvider` renamed to `StateProvider`
|
||||||
|
- `useData` renamed to `useStateStore`, `useDataValue` to `useStateValue`, `useDataBinding` to `useStateBinding`
|
||||||
|
- `onAction` renamed to `emit` in component context
|
||||||
|
- `DataModel` type renamed to `StateModel`
|
||||||
|
- `Action` type renamed to `ActionBinding` (old name still available but deprecated)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.4.0
|
## v0.4.0
|
||||||
|
|
||||||
February 2026
|
February 2026
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ 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" />
|
||||||
|
|
||||||
|
## For React Native
|
||||||
|
|
||||||
|
<PackageInstall packages="@json-render/core @json-render/react-native" />
|
||||||
|
|
||||||
## For Remotion Video
|
## For Remotion Video
|
||||||
|
|
||||||
<PackageInstall packages="@json-render/core @json-render/remotion remotion @remotion/player" />
|
<PackageInstall packages="@json-render/core @json-render/remotion remotion @remotion/player" />
|
||||||
|
|||||||
@@ -2,17 +2,19 @@ export const metadata = { title: "Introduction" }
|
|||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
|
|
||||||
Predictable. Guardrailed. Fast. Let users generate dashboards, widgets, apps, and data visualizations from prompts.
|
The framework for User-Generated Interfaces (UGI). Dynamic, personalized UIs per user without sacrificing reliability.
|
||||||
|
|
||||||
## What is json-render?
|
## What is json-render?
|
||||||
|
|
||||||
json-render lets end users generate UI from natural language prompts — safely constrained to components you define. You set the guardrails: what components exist, what props they take, what actions are available. AI generates JSON that matches your schema, and your components render it natively.
|
json-render is the framework for **User-Generated Interfaces**: dynamic UIs that end users generate through natural language prompts, powered by Generative UI. You define the guardrails: what components exist, what props they take, what actions are available. AI generates JSON that matches your schema, and your components render it natively on web or mobile.
|
||||||
|
|
||||||
|
Every interface is unique to the user, but every interface is safe and predictable.
|
||||||
|
|
||||||
## Why json-render?
|
## Why json-render?
|
||||||
|
|
||||||
### Guardrailed
|
### Guardrailed
|
||||||
|
|
||||||
AI can only use components in your catalog. No arbitrary code generation.
|
AI can only use components in your catalog. No arbitrary code generation. Predefined components and actions for safe, predictable output.
|
||||||
|
|
||||||
### Predictable
|
### Predictable
|
||||||
|
|
||||||
@@ -22,9 +24,13 @@ JSON output matches your schema, every time. Actions are declared by name, you c
|
|||||||
|
|
||||||
Stream and render progressively as the model responds. No waiting for completion.
|
Stream and render progressively as the model responds. No waiting for completion.
|
||||||
|
|
||||||
|
### Cross-Platform
|
||||||
|
|
||||||
|
Render on web with React and on mobile with React Native from the same catalog and spec format.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
1. Define the guardrails — what components, actions, and data bindings AI can use
|
1. Define the guardrails - what components, actions, and data bindings AI can use
|
||||||
2. Users prompt — end users describe what they want in natural language
|
2. Users generate - end users describe what they want in natural language
|
||||||
3. AI generates JSON — output is always predictable, constrained to your catalog
|
3. AI generates JSON - output is always predictable, constrained to your catalog
|
||||||
4. Render fast — stream and render progressively as the model responds
|
4. Render fast - stream and render progressively as the model responds
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const catalog = defineCatalog(schema, {
|
|||||||
|
|
||||||
## 2. Define your components
|
## 2. Define your components
|
||||||
|
|
||||||
Use `defineRegistry` to map catalog types to React components. Each component receives type-safe `props`, `children`, and `onAction`:
|
Use `defineRegistry` to map catalog types to React components. Each component receives type-safe `props`, `children`, and `emit`:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// lib/registry.tsx
|
// lib/registry.tsx
|
||||||
@@ -71,10 +71,10 @@ export const { registry } = defineRegistry(catalog, {
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
Button: ({ props, onAction }) => (
|
Button: ({ props, emit }) => (
|
||||||
<button
|
<button
|
||||||
className="px-4 py-2 bg-blue-500 text-white rounded"
|
className="px-4 py-2 bg-blue-500 text-white rounded"
|
||||||
onClick={() => onAction?.({ name: props.action })}
|
onClick={() => emit?.("press")}
|
||||||
>
|
>
|
||||||
{props.label}
|
{props.label}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ export default function Home() {
|
|||||||
<>
|
<>
|
||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
<section className="max-w-5xl mx-auto px-6 pt-24 pb-16 text-center">
|
<section className="max-w-5xl mx-auto px-6 pt-24 pb-16 text-center">
|
||||||
<h1 className="text-5xl sm:text-6xl md:text-7xl font-bold tracking-tighter mb-6">
|
<p className="text-xs sm:text-sm font-medium text-muted-foreground tracking-widest uppercase mb-4">
|
||||||
|
The framework for User-Generated Interfaces
|
||||||
|
</p>
|
||||||
|
<h1 className="text-4xl sm:text-6xl md:text-7xl font-bold tracking-tighter mb-6">
|
||||||
AI → json-render → UI
|
AI → json-render → UI
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto mb-12 leading-relaxed">
|
<p className="text-lg text-muted-foreground max-w-2xl mx-auto mb-12 leading-relaxed">
|
||||||
Define a component catalog. Users prompt. AI outputs JSON constrained
|
Dynamic, personalized UIs per user without sacrificing reliability.
|
||||||
to your catalog. Your components render it.
|
Predefined components and actions for safe, predictable output.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Demo />
|
<Demo />
|
||||||
@@ -65,10 +68,10 @@ export default function Home() {
|
|||||||
<div className="text-xs text-muted-foreground font-mono mb-3">
|
<div className="text-xs text-muted-foreground font-mono mb-3">
|
||||||
02
|
02
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold mb-2">Users Prompt</h3>
|
<h3 className="text-lg font-semibold mb-2">Users Generate</h3>
|
||||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
End users describe what they want. AI generates JSON constrained
|
End users describe what they want. AI generates JSON constrained
|
||||||
to your catalog.
|
to your catalog. Every interface is unique to the user.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -244,6 +247,10 @@ export default function Page() {
|
|||||||
<h2 className="text-2xl font-semibold mb-12 text-center">Features</h2>
|
<h2 className="text-2xl font-semibold mb-12 text-center">Features</h2>
|
||||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
{[
|
{[
|
||||||
|
{
|
||||||
|
title: "User-Generated Interfaces",
|
||||||
|
desc: "Dynamic, personalized UIs per user powered by Generative UI",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Guardrails",
|
title: "Guardrails",
|
||||||
desc: "AI can only use components you define in the catalog",
|
desc: "AI can only use components you define in the catalog",
|
||||||
@@ -253,20 +260,16 @@ export default function Page() {
|
|||||||
desc: "Progressive rendering as JSON streams from the model",
|
desc: "Progressive rendering as JSON streams from the model",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Code Export",
|
title: "React & React Native",
|
||||||
desc: "Export as standalone React code with no runtime dependencies",
|
desc: "Render on web and mobile from the same catalog and spec format",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Data Binding",
|
title: "Data Binding",
|
||||||
desc: "Two-way binding with JSON Pointer paths",
|
desc: "Two-way state binding with dynamic prop expressions",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Actions",
|
title: "Code Export",
|
||||||
desc: "Named actions handled by your application",
|
desc: "Export as standalone React code with no runtime dependencies",
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Visibility",
|
|
||||||
desc: "Conditional show/hide based on data or auth",
|
|
||||||
},
|
},
|
||||||
].map((feature) => (
|
].map((feature) => (
|
||||||
<div key={feature.title}>
|
<div key={feature.title}>
|
||||||
|
|||||||
+10
-6
@@ -22,11 +22,15 @@ export const metadata: Metadata = {
|
|||||||
template: "%s | json-render",
|
template: "%s | json-render",
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
"Let users generate dashboards, widgets, apps, and data visualizations from prompts — safely constrained to components you define.",
|
"The framework for User-Generated Interfaces (UGI). Let users generate dashboards, widgets, and apps from prompts — safely constrained to components you define.",
|
||||||
keywords: [
|
keywords: [
|
||||||
"json-render",
|
"json-render",
|
||||||
|
"UGI",
|
||||||
|
"User-Generated Interfaces",
|
||||||
"AI UI generation",
|
"AI UI generation",
|
||||||
|
"generative UI",
|
||||||
"React components",
|
"React components",
|
||||||
|
"React Native",
|
||||||
"guardrails",
|
"guardrails",
|
||||||
"structured output",
|
"structured output",
|
||||||
"dashboard builder",
|
"dashboard builder",
|
||||||
@@ -38,23 +42,23 @@ export const metadata: Metadata = {
|
|||||||
locale: "en_US",
|
locale: "en_US",
|
||||||
url: "https://json-render.dev",
|
url: "https://json-render.dev",
|
||||||
siteName: "json-render",
|
siteName: "json-render",
|
||||||
title: "json-render | AI-generated UI with guardrails",
|
title: "json-render | The framework for User-Generated Interfaces",
|
||||||
description:
|
description:
|
||||||
"Let users generate dashboards, widgets, apps, and data visualizations from prompts — safely constrained to components you define.",
|
"The framework for User-Generated Interfaces (UGI). Let users generate dashboards, widgets, and apps from prompts — safely constrained to components you define.",
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: "/og",
|
url: "/og",
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
height: 630,
|
||||||
alt: "json-render - AI-generated UI with guardrails",
|
alt: "json-render - The framework for User-Generated Interfaces",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: "json-render | AI-generated UI with guardrails",
|
title: "json-render | The framework for User-Generated Interfaces",
|
||||||
description:
|
description:
|
||||||
"Let users generate dashboards, widgets, apps, and data visualizations from prompts — safely constrained to components you define.",
|
"The framework for User-Generated Interfaces (UGI). Let users generate dashboards, widgets, and apps from prompts — safely constrained to components you define.",
|
||||||
images: ["/og"],
|
images: ["/og"],
|
||||||
creator: "@verabornnot",
|
creator: "@verabornnot",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -66,23 +66,27 @@ export async function renderOgImage(title: string) {
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
flexDirection: "column",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
{title.split("\n").map((line, i) => (
|
||||||
style={{
|
<span
|
||||||
fontSize: 72,
|
key={i}
|
||||||
fontFamily: "Geist",
|
style={{
|
||||||
fontWeight: 400,
|
fontSize: 72,
|
||||||
color: "white",
|
fontFamily: "Geist",
|
||||||
letterSpacing: "-0.02em",
|
fontWeight: 400,
|
||||||
textAlign: "center",
|
color: "white",
|
||||||
lineHeight: 1.2,
|
letterSpacing: "-0.02em",
|
||||||
}}
|
textAlign: "center",
|
||||||
>
|
lineHeight: 1.2,
|
||||||
{title}
|
}}
|
||||||
</span>
|
>
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ export const docsNavigation: NavSection[] = [
|
|||||||
href: "https://github.com/vercel-labs/json-render/tree/main/examples/dashboard",
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/dashboard",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "React Native",
|
||||||
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/react-native",
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Remotion",
|
title: "Remotion",
|
||||||
href: "https://github.com/vercel-labs/json-render/tree/main/examples/remotion",
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/remotion",
|
||||||
@@ -69,6 +74,7 @@ export const docsNavigation: NavSection[] = [
|
|||||||
items: [
|
items: [
|
||||||
{ title: "@json-render/core", href: "/docs/api/core" },
|
{ title: "@json-render/core", href: "/docs/api/core" },
|
||||||
{ title: "@json-render/react", href: "/docs/api/react" },
|
{ title: "@json-render/react", href: "/docs/api/react" },
|
||||||
|
{ title: "@json-render/react-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/codegen", href: "/docs/api/codegen" },
|
{ title: "@json-render/codegen", href: "/docs/api/codegen" },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
export const PAGE_TITLES: Record<string, string> = {
|
export const PAGE_TITLES: Record<string, string> = {
|
||||||
// Home (no slug)
|
// Home (no slug)
|
||||||
"": "AI-generated UI with guardrails",
|
"": "The framework for\nUser-Generated Interfaces",
|
||||||
|
|
||||||
// Top-level
|
// Top-level
|
||||||
playground: "Playground",
|
playground: "Playground",
|
||||||
@@ -36,6 +36,7 @@ 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/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",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const playgroundCatalog = defineCatalog(schema, {
|
|||||||
slots: ["default"],
|
slots: ["default"],
|
||||||
description:
|
description:
|
||||||
"Container card for content sections. Use for forms/content boxes, NOT for page headers.",
|
"Container card for content sections. Use for forms/content boxes, NOT for page headers.",
|
||||||
|
example: { title: "Overview", description: "Your account summary" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Stack: {
|
Stack: {
|
||||||
@@ -37,6 +38,7 @@ export const playgroundCatalog = defineCatalog(schema, {
|
|||||||
}),
|
}),
|
||||||
slots: ["default"],
|
slots: ["default"],
|
||||||
description: "Flex container for layouts",
|
description: "Flex container for layouts",
|
||||||
|
example: { direction: "vertical", gap: "md" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Grid: {
|
Grid: {
|
||||||
@@ -46,6 +48,7 @@ export const playgroundCatalog = defineCatalog(schema, {
|
|||||||
}),
|
}),
|
||||||
slots: ["default"],
|
slots: ["default"],
|
||||||
description: "Grid layout (1-6 columns)",
|
description: "Grid layout (1-6 columns)",
|
||||||
|
example: { columns: 3, gap: "md" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Separator: {
|
Separator: {
|
||||||
@@ -137,6 +140,13 @@ export const playgroundCatalog = defineCatalog(schema, {
|
|||||||
}),
|
}),
|
||||||
description:
|
description:
|
||||||
'Data table. columns: header labels. rows: 2D array of cell strings, e.g. [["Alice","admin"],["Bob","user"]].',
|
'Data table. columns: header labels. rows: 2D array of cell strings, e.g. [["Alice","admin"],["Bob","user"]].',
|
||||||
|
example: {
|
||||||
|
columns: ["Name", "Role"],
|
||||||
|
rows: [
|
||||||
|
["Alice", "Admin"],
|
||||||
|
["Bob", "User"],
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Heading: {
|
Heading: {
|
||||||
@@ -145,6 +155,7 @@ export const playgroundCatalog = defineCatalog(schema, {
|
|||||||
level: z.enum(["h1", "h2", "h3", "h4"]).nullable(),
|
level: z.enum(["h1", "h2", "h3", "h4"]).nullable(),
|
||||||
}),
|
}),
|
||||||
description: "Heading text (h1-h4)",
|
description: "Heading text (h1-h4)",
|
||||||
|
example: { text: "Welcome", level: "h1" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Text: {
|
Text: {
|
||||||
@@ -155,6 +166,7 @@ export const playgroundCatalog = defineCatalog(schema, {
|
|||||||
.nullable(),
|
.nullable(),
|
||||||
}),
|
}),
|
||||||
description: "Paragraph text",
|
description: "Paragraph text",
|
||||||
|
example: { text: "Hello, world!" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Image: {
|
Image: {
|
||||||
@@ -173,6 +185,7 @@ export const playgroundCatalog = defineCatalog(schema, {
|
|||||||
size: z.enum(["sm", "md", "lg"]).nullable(),
|
size: z.enum(["sm", "md", "lg"]).nullable(),
|
||||||
}),
|
}),
|
||||||
description: "User avatar with fallback initials",
|
description: "User avatar with fallback initials",
|
||||||
|
example: { name: "Jane Doe", size: "md" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Badge: {
|
Badge: {
|
||||||
@@ -181,6 +194,7 @@ export const playgroundCatalog = defineCatalog(schema, {
|
|||||||
variant: z.enum(["default", "success", "warning", "danger"]).nullable(),
|
variant: z.enum(["default", "success", "warning", "danger"]).nullable(),
|
||||||
}),
|
}),
|
||||||
description: "Status badge",
|
description: "Status badge",
|
||||||
|
example: { text: "Active", variant: "success" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Alert: {
|
Alert: {
|
||||||
@@ -190,6 +204,11 @@ export const playgroundCatalog = defineCatalog(schema, {
|
|||||||
type: z.enum(["info", "success", "warning", "error"]).nullable(),
|
type: z.enum(["info", "success", "warning", "error"]).nullable(),
|
||||||
}),
|
}),
|
||||||
description: "Alert banner",
|
description: "Alert banner",
|
||||||
|
example: {
|
||||||
|
title: "Note",
|
||||||
|
message: "Your changes have been saved.",
|
||||||
|
type: "success",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Progress: {
|
Progress: {
|
||||||
@@ -199,6 +218,7 @@ export const playgroundCatalog = defineCatalog(schema, {
|
|||||||
label: z.string().nullable(),
|
label: z.string().nullable(),
|
||||||
}),
|
}),
|
||||||
description: "Progress bar (value 0-100)",
|
description: "Progress bar (value 0-100)",
|
||||||
|
example: { value: 65, max: 100, label: "Upload progress" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Skeleton: {
|
Skeleton: {
|
||||||
@@ -281,6 +301,12 @@ export const playgroundCatalog = defineCatalog(schema, {
|
|||||||
}),
|
}),
|
||||||
events: ["submit", "focus", "blur"],
|
events: ["submit", "focus", "blur"],
|
||||||
description: "Text input field. Use statePath for two-way binding.",
|
description: "Text input field. Use statePath for two-way binding.",
|
||||||
|
example: {
|
||||||
|
label: "Email",
|
||||||
|
name: "email",
|
||||||
|
type: "email",
|
||||||
|
placeholder: "you@example.com",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Textarea: {
|
Textarea: {
|
||||||
@@ -360,6 +386,7 @@ export const playgroundCatalog = defineCatalog(schema, {
|
|||||||
}),
|
}),
|
||||||
events: ["press"],
|
events: ["press"],
|
||||||
description: "Clickable button. Bind on.press for handler.",
|
description: "Clickable button. Bind on.press for handler.",
|
||||||
|
example: { label: "Submit", variant: "primary" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Link: {
|
Link: {
|
||||||
|
|||||||
@@ -823,7 +823,12 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
|||||||
const [localValue, setLocalValue] = useState<string>("");
|
const [localValue, setLocalValue] = useState<string>("");
|
||||||
const value = props.statePath ? (boundValue ?? "") : localValue;
|
const value = props.statePath ? (boundValue ?? "") : localValue;
|
||||||
const setValue = props.statePath ? setBoundValue! : setLocalValue;
|
const setValue = props.statePath ? setBoundValue! : setLocalValue;
|
||||||
const options = props.options ?? [];
|
const rawOptions = props.options ?? [];
|
||||||
|
// Coerce options to strings – AI may produce objects/numbers instead of
|
||||||
|
// plain strings which would cause duplicate `[object Object]` keys.
|
||||||
|
const options = rawOptions.map((opt) =>
|
||||||
|
typeof opt === "string" ? opt : String(opt ?? ""),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -839,8 +844,11 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
|||||||
<SelectValue placeholder={props.placeholder ?? "Select..."} />
|
<SelectValue placeholder={props.placeholder ?? "Select..."} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{options.map((opt) => (
|
{options.map((opt, idx) => (
|
||||||
<SelectItem key={opt} value={opt}>
|
<SelectItem
|
||||||
|
key={`${idx}-${opt}`}
|
||||||
|
value={opt || `option-${idx}`}
|
||||||
|
>
|
||||||
{opt}
|
{opt}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -876,7 +884,10 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Radio: ({ props, emit }) => {
|
Radio: ({ props, emit }) => {
|
||||||
const options = props.options ?? [];
|
const rawOptions = props.options ?? [];
|
||||||
|
const options = rawOptions.map((opt) =>
|
||||||
|
typeof opt === "string" ? opt : String(opt ?? ""),
|
||||||
|
);
|
||||||
const [boundValue, setBoundValue] = props.statePath
|
const [boundValue, setBoundValue] = props.statePath
|
||||||
? useStateBinding<string>(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
|
? useStateBinding<string>(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
|
||||||
: [undefined, undefined];
|
: [undefined, undefined];
|
||||||
@@ -894,11 +905,17 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
|||||||
emit?.("change");
|
emit?.("change");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{options.map((opt) => (
|
{options.map((opt, idx) => (
|
||||||
<div key={opt} className="flex items-center space-x-2">
|
<div
|
||||||
<RadioGroupItem value={opt} id={`${props.name}-${opt}`} />
|
key={`${idx}-${opt}`}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<RadioGroupItem
|
||||||
|
value={opt || `option-${idx}`}
|
||||||
|
id={`${props.name}-${idx}-${opt}`}
|
||||||
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={`${props.name}-${opt}`}
|
htmlFor={`${props.name}-${idx}-${opt}`}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
{opt}
|
{opt}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const dashboardCatalog = defineCatalog(schema, {
|
|||||||
}),
|
}),
|
||||||
slots: ["default"],
|
slots: ["default"],
|
||||||
description: "Flex layout container",
|
description: "Flex layout container",
|
||||||
|
example: { direction: "vertical", gap: "md" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Accordion: {
|
Accordion: {
|
||||||
@@ -50,6 +51,7 @@ export const dashboardCatalog = defineCatalog(schema, {
|
|||||||
}),
|
}),
|
||||||
description:
|
description:
|
||||||
"Clickable button. Use actionParams to pass parameters to the action (e.g., { limit: 5, sort: 'newest' })",
|
"Clickable button. Use actionParams to pass parameters to the action (e.g., { limit: 5, sort: 'newest' })",
|
||||||
|
example: { label: "Save", variant: "default", action: "formSubmit" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Input: {
|
Input: {
|
||||||
@@ -60,6 +62,12 @@ export const dashboardCatalog = defineCatalog(schema, {
|
|||||||
type: z.enum(["text", "email", "password", "number", "tel"]).nullable(),
|
type: z.enum(["text", "email", "password", "number", "tel"]).nullable(),
|
||||||
}),
|
}),
|
||||||
description: "Text input field",
|
description: "Text input field",
|
||||||
|
example: {
|
||||||
|
label: "Email",
|
||||||
|
valuePath: "/form/email",
|
||||||
|
placeholder: "you@example.com",
|
||||||
|
type: "email",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Form: {
|
Form: {
|
||||||
@@ -81,6 +89,7 @@ export const dashboardCatalog = defineCatalog(schema, {
|
|||||||
.nullable(),
|
.nullable(),
|
||||||
}),
|
}),
|
||||||
description: "Status badge",
|
description: "Status badge",
|
||||||
|
example: { text: "Active", variant: "default" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Alert: {
|
Alert: {
|
||||||
@@ -304,6 +313,13 @@ export const dashboardCatalog = defineCatalog(schema, {
|
|||||||
emptyMessage: z.string().nullable(),
|
emptyMessage: z.string().nullable(),
|
||||||
}),
|
}),
|
||||||
description: "Data table with optional row actions (delete, edit, etc.)",
|
description: "Data table with optional row actions (delete, edit, etc.)",
|
||||||
|
example: {
|
||||||
|
statePath: "/customers/data",
|
||||||
|
columns: [
|
||||||
|
{ key: "name", label: "Name" },
|
||||||
|
{ key: "email", label: "Email" },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Typography
|
// Typography
|
||||||
@@ -313,6 +329,7 @@ export const dashboardCatalog = defineCatalog(schema, {
|
|||||||
level: z.enum(["h1", "h2", "h3", "h4"]).nullable(),
|
level: z.enum(["h1", "h2", "h3", "h4"]).nullable(),
|
||||||
}),
|
}),
|
||||||
description: "Section heading",
|
description: "Section heading",
|
||||||
|
example: { text: "Dashboard", level: "h1" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Text: {
|
Text: {
|
||||||
@@ -321,6 +338,7 @@ export const dashboardCatalog = defineCatalog(schema, {
|
|||||||
muted: z.boolean().nullable(),
|
muted: z.boolean().nullable(),
|
||||||
}),
|
}),
|
||||||
description: "Text content",
|
description: "Text content",
|
||||||
|
example: { content: "Welcome back! Here is your overview." },
|
||||||
},
|
},
|
||||||
|
|
||||||
// Charts
|
// Charts
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export const catalog = defineCatalog(schema, {
|
|||||||
slots: [],
|
slots: [],
|
||||||
description:
|
description:
|
||||||
"Icon display using Ionicons. Use for action buttons, navigation items, and indicators. ALWAYS use this instead of emoji characters for UI icons. Use Ionicons naming convention (e.g. heart, heart-outline, chatbubble-outline, share-social-outline).",
|
"Icon display using Ionicons. Use for action buttons, navigation items, and indicators. ALWAYS use this instead of emoji characters for UI icons. Use Ionicons naming convention (e.g. heart, heart-outline, chatbubble-outline, share-social-outline).",
|
||||||
|
example: { name: "heart-outline", size: 24, color: "#007AFF" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: standardActionDefinitions,
|
actions: standardActionDefinitions,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const stripeCatalog = defineCatalog(schema, {
|
|||||||
}),
|
}),
|
||||||
description:
|
description:
|
||||||
"Flex layout container for arranging children horizontally or vertically with configurable gap and alignment",
|
"Flex layout container for arranging children horizontally or vertically with configurable gap and alignment",
|
||||||
|
example: { direction: "vertical", gap: "medium" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Inline: {
|
Inline: {
|
||||||
@@ -71,6 +72,7 @@ export const stripeCatalog = defineCatalog(schema, {
|
|||||||
.default("large"),
|
.default("large"),
|
||||||
}),
|
}),
|
||||||
description: "Display a heading/title text with configurable size",
|
description: "Display a heading/title text with configurable size",
|
||||||
|
example: { text: "Overview", size: "large" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Text: {
|
Text: {
|
||||||
@@ -94,6 +96,7 @@ export const stripeCatalog = defineCatalog(schema, {
|
|||||||
}),
|
}),
|
||||||
description:
|
description:
|
||||||
"Display body text with configurable color, size, and weight",
|
"Display body text with configurable color, size, and weight",
|
||||||
|
example: { content: "Payment received successfully.", color: "primary" },
|
||||||
},
|
},
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -111,6 +114,13 @@ export const stripeCatalog = defineCatalog(schema, {
|
|||||||
}),
|
}),
|
||||||
description:
|
description:
|
||||||
"Display a key metric with label, value, and optional trend indicator for KPIs",
|
"Display a key metric with label, value, and optional trend indicator for KPIs",
|
||||||
|
example: {
|
||||||
|
label: "Revenue",
|
||||||
|
value: "$12,450",
|
||||||
|
change: "+8.2%",
|
||||||
|
changeType: "positive",
|
||||||
|
format: "currency",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Badge: {
|
Badge: {
|
||||||
@@ -128,6 +138,7 @@ export const stripeCatalog = defineCatalog(schema, {
|
|||||||
.default("neutral"),
|
.default("neutral"),
|
||||||
}),
|
}),
|
||||||
description: "Status badge indicator with configurable color type",
|
description: "Status badge indicator with configurable color type",
|
||||||
|
example: { label: "Active", type: "positive" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Icon: {
|
Icon: {
|
||||||
@@ -284,6 +295,12 @@ export const stripeCatalog = defineCatalog(schema, {
|
|||||||
required: z.boolean().nullable(),
|
required: z.boolean().nullable(),
|
||||||
}),
|
}),
|
||||||
description: "Text input field with label, validation, and data binding",
|
description: "Text input field with label, validation, and data binding",
|
||||||
|
example: {
|
||||||
|
label: "Email",
|
||||||
|
placeholder: "customer@example.com",
|
||||||
|
valuePath: "/form/email",
|
||||||
|
type: "email",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
TextArea: {
|
TextArea: {
|
||||||
@@ -379,6 +396,11 @@ export const stripeCatalog = defineCatalog(schema, {
|
|||||||
}),
|
}),
|
||||||
description:
|
description:
|
||||||
"Action button with configurable style, size, and action handling",
|
"Action button with configurable style, size, and action handling",
|
||||||
|
example: {
|
||||||
|
label: "View Details",
|
||||||
|
action: "viewCustomer",
|
||||||
|
type: "primary",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
ButtonGroup: {
|
ButtonGroup: {
|
||||||
@@ -451,6 +473,14 @@ export const stripeCatalog = defineCatalog(schema, {
|
|||||||
}),
|
}),
|
||||||
description:
|
description:
|
||||||
"Data table with configurable columns and optional row actions",
|
"Data table with configurable columns and optional row actions",
|
||||||
|
example: {
|
||||||
|
title: "Recent Payments",
|
||||||
|
statePath: "/payments/data",
|
||||||
|
columns: [
|
||||||
|
{ key: "amount", label: "Amount" },
|
||||||
|
{ key: "status", label: "Status" },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -465,6 +495,11 @@ export const stripeCatalog = defineCatalog(schema, {
|
|||||||
}),
|
}),
|
||||||
description:
|
description:
|
||||||
"Card displaying customer information with name, email, and status",
|
"Card displaying customer information with name, email, and status",
|
||||||
|
example: {
|
||||||
|
name: "Jane Smith",
|
||||||
|
email: "jane@example.com",
|
||||||
|
status: "active",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
PaymentCard: {
|
PaymentCard: {
|
||||||
|
|||||||
@@ -1,5 +1,30 @@
|
|||||||
# @json-render/codegen
|
# @json-render/codegen
|
||||||
|
|
||||||
|
## 0.5.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 429e456: Fix LLM hallucinations by dynamically generating prompt examples from the user's catalog instead of hardcoding component names. Adds optional `example` field to `ComponentDefinition` with Zod schema introspection fallback. Mentions RFC 6902 in output format section.
|
||||||
|
- Updated dependencies [429e456]
|
||||||
|
- @json-render/core@0.5.2
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- @json-render/core@0.5.1
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [3d2d1ad]
|
||||||
|
- @json-render/core@0.5.0
|
||||||
|
|
||||||
## 0.4.4
|
## 0.4.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/codegen",
|
"name": "@json-render/codegen",
|
||||||
"version": "0.4.4",
|
"version": "0.5.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Utilities for generating code from json-render UI trees",
|
"description": "Utilities for generating code from json-render UI trees",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @json-render/core
|
# @json-render/core
|
||||||
|
|
||||||
|
## 0.5.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 429e456: Fix LLM hallucinations by dynamically generating prompt examples from the user's catalog instead of hardcoding component names. Adds optional `example` field to `ComponentDefinition` with Zod schema introspection fallback. Mentions RFC 6902 in output format section.
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.
|
||||||
|
|
||||||
## 0.4.4
|
## 0.4.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ Any prop value can be a dynamic expression that resolves based on data state at
|
|||||||
|
|
||||||
### Data Binding (`$path`)
|
### Data Binding (`$path`)
|
||||||
|
|
||||||
Read a value directly from the data model:
|
Read a value directly from the state model:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/core",
|
"name": "@json-render/core",
|
||||||
"version": "0.4.4",
|
"version": "0.5.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
|
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -403,6 +403,7 @@ describe("defineCatalog (new schema API)", () => {
|
|||||||
components: s.map({
|
components: s.map({
|
||||||
props: s.zod(),
|
props: s.zod(),
|
||||||
description: s.string(),
|
description: s.string(),
|
||||||
|
example: s.any(),
|
||||||
}),
|
}),
|
||||||
actions: s.map({
|
actions: s.map({
|
||||||
description: s.string(),
|
description: s.string(),
|
||||||
@@ -560,4 +561,157 @@ describe("defineCatalog (new schema API)", () => {
|
|||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not include hardcoded component names in prompt (issue #88)", () => {
|
||||||
|
// When a catalog only has "Text", the generated prompt should NOT
|
||||||
|
// reference components like Stack, Grid, Heading, Card, Column,
|
||||||
|
// Button, or Pressable that are not in the catalog.
|
||||||
|
const catalog = defineCatalog(testSchema, {
|
||||||
|
components: {
|
||||||
|
Text: {
|
||||||
|
props: z.object({ content: z.string() }),
|
||||||
|
description: "Display text content",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const prompt = catalog.prompt();
|
||||||
|
|
||||||
|
// The prompt should contain the actual catalog component
|
||||||
|
expect(prompt).toContain("Text");
|
||||||
|
expect(prompt).toContain("Display text content");
|
||||||
|
|
||||||
|
// The prompt should NOT contain hardcoded component names not in the catalog
|
||||||
|
// Check that these don't appear as component types in JSON examples
|
||||||
|
const hardcodedComponents = [
|
||||||
|
"Stack",
|
||||||
|
"Grid",
|
||||||
|
"Heading",
|
||||||
|
"Card",
|
||||||
|
"Column",
|
||||||
|
"Pressable",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const comp of hardcodedComponents) {
|
||||||
|
// Check for "type":"<ComponentName>" patterns in JSON examples
|
||||||
|
expect(prompt).not.toContain(`"type":"${comp}"`);
|
||||||
|
// Also check for "type": "<ComponentName>" with space
|
||||||
|
expect(prompt).not.toContain(`"type": "${comp}"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses actual catalog component names in prompt examples", () => {
|
||||||
|
const catalog = defineCatalog(testSchema, {
|
||||||
|
components: {
|
||||||
|
MyBox: {
|
||||||
|
props: z.object({ padding: z.number() }),
|
||||||
|
description: "A box",
|
||||||
|
},
|
||||||
|
MyLabel: {
|
||||||
|
props: z.object({ text: z.string() }),
|
||||||
|
description: "A label",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const prompt = catalog.prompt();
|
||||||
|
|
||||||
|
// The example output should use MyBox and MyLabel, not hardcoded names
|
||||||
|
expect(prompt).toContain('"type":"MyBox"');
|
||||||
|
expect(prompt).toContain('"type":"MyLabel"');
|
||||||
|
|
||||||
|
// Should not contain any hardcoded component names in type fields
|
||||||
|
expect(prompt).not.toContain('"type":"Stack"');
|
||||||
|
expect(prompt).not.toContain('"type":"Grid"');
|
||||||
|
expect(prompt).not.toContain('"type":"Heading"');
|
||||||
|
expect(prompt).not.toContain('"type":"Column"');
|
||||||
|
expect(prompt).not.toContain('"type":"Button"');
|
||||||
|
expect(prompt).not.toContain('"type":"Pressable"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates example props from Zod schema when no example provided", () => {
|
||||||
|
const catalog = defineCatalog(testSchema, {
|
||||||
|
components: {
|
||||||
|
Text: {
|
||||||
|
props: z.object({
|
||||||
|
content: z.string(),
|
||||||
|
size: z.number(),
|
||||||
|
bold: z.boolean(),
|
||||||
|
variant: z.enum(["body", "heading"]),
|
||||||
|
color: z.string().optional(),
|
||||||
|
}),
|
||||||
|
description: "Display text",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const prompt = catalog.prompt();
|
||||||
|
|
||||||
|
// Required props should appear with generated values in examples
|
||||||
|
// (string -> "example", number -> 0, boolean -> true, enum -> first value)
|
||||||
|
expect(prompt).toContain('"content":"example"');
|
||||||
|
expect(prompt).toContain('"size":0');
|
||||||
|
expect(prompt).toContain('"bold":true');
|
||||||
|
expect(prompt).toContain('"variant":"body"');
|
||||||
|
|
||||||
|
// Optional props should NOT appear in examples (keeps them concise)
|
||||||
|
// The prop name "color" should still appear in the AVAILABLE COMPONENTS
|
||||||
|
// section but not in the JSON example objects
|
||||||
|
const exampleSection = prompt.split("AVAILABLE COMPONENTS")[0]!;
|
||||||
|
expect(exampleSection).not.toContain('"color"');
|
||||||
|
|
||||||
|
// Prompt examples should never have empty props:{}
|
||||||
|
expect(exampleSection).not.toContain('"props":{}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses explicit example field over Zod-generated values", () => {
|
||||||
|
const catalog = defineCatalog(testSchema, {
|
||||||
|
components: {
|
||||||
|
Heading: {
|
||||||
|
props: z.object({
|
||||||
|
text: z.string(),
|
||||||
|
level: z.enum(["h1", "h2", "h3"]),
|
||||||
|
}),
|
||||||
|
description: "A heading",
|
||||||
|
example: { text: "Welcome to My App", level: "h1" },
|
||||||
|
},
|
||||||
|
Paragraph: {
|
||||||
|
props: z.object({ content: z.string() }),
|
||||||
|
description: "A paragraph",
|
||||||
|
example: { content: "Lorem ipsum dolor sit amet" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const prompt = catalog.prompt();
|
||||||
|
|
||||||
|
// Should use the explicit example values, not "example" or first enum value
|
||||||
|
expect(prompt).toContain('"text":"Welcome to My App"');
|
||||||
|
expect(prompt).toContain('"level":"h1"');
|
||||||
|
expect(prompt).toContain('"content":"Lorem ipsum dolor sit amet"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses $path binding on first string prop for repeat example", () => {
|
||||||
|
const catalog = defineCatalog(testSchema, {
|
||||||
|
components: {
|
||||||
|
Card: {
|
||||||
|
props: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
subtitle: z.string(),
|
||||||
|
}),
|
||||||
|
description: "A card",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const prompt = catalog.prompt();
|
||||||
|
|
||||||
|
// In the repeat/item example, the first string prop should get a $path binding
|
||||||
|
expect(prompt).toContain('"title":{"$path":"$item/title"}');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export interface ComponentDefinition<
|
|||||||
hasChildren?: boolean;
|
hasChildren?: boolean;
|
||||||
/** Description for AI generation */
|
/** Description for AI generation */
|
||||||
description?: string;
|
description?: string;
|
||||||
|
/** Example prop values used in prompt examples (auto-generated from Zod schema if omitted) */
|
||||||
|
example?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+240
-30
@@ -540,27 +540,84 @@ function generatePrompt<TDef extends SchemaDefinition, TCatalog>(
|
|||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
// Output format section - explain JSONL streaming patch format
|
// Output format section - explain JSONL streaming patch format
|
||||||
lines.push("OUTPUT FORMAT:");
|
lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):");
|
||||||
lines.push(
|
lines.push(
|
||||||
"Output JSONL (one JSON object per line) with patches to build a UI tree.",
|
"Output JSONL (one JSON object per line) using RFC 6902 JSON Patch operations to build a UI tree.",
|
||||||
);
|
);
|
||||||
lines.push(
|
lines.push(
|
||||||
"Each line is a JSON patch operation. Start with /root, then stream /elements and /state patches interleaved so the UI fills in progressively as it streams.",
|
"Each line is a JSON patch operation (add, remove, replace). Start with /root, then stream /elements and /state patches interleaved so the UI fills in progressively as it streams.",
|
||||||
);
|
);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Example output (each line is a separate JSON object):");
|
lines.push("Example output (each line is a separate JSON object):");
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push(`{"op":"add","path":"/root","value":"blog"}
|
|
||||||
{"op":"add","path":"/elements/blog","value":{"type":"Stack","props":{"direction":"vertical","gap":"md"},"children":["heading","posts-grid"]}}
|
|
||||||
{"op":"add","path":"/elements/heading","value":{"type":"Heading","props":{"text":"Blog","level":"h1"},"children":[]}}
|
|
||||||
{"op":"add","path":"/elements/posts-grid","value":{"type":"Grid","props":{"columns":2,"gap":"md"},"repeat":{"path":"/posts","key":"id"},"children":["post-card"]}}
|
|
||||||
{"op":"add","path":"/elements/post-card","value":{"type":"Card","props":{"title":{"$path":"$item/title"}},"children":["post-meta"]}}
|
|
||||||
{"op":"add","path":"/elements/post-meta","value":{"type":"Text","props":{"text":{"$path":"$item/author"},"variant":"muted"},"children":[]}}
|
|
||||||
{"op":"add","path":"/state/posts","value":[]}
|
|
||||||
{"op":"add","path":"/state/posts/0","value":{"id":"1","title":"Getting Started","author":"Jane","date":"Jan 15"}}
|
|
||||||
{"op":"add","path":"/state/posts/1","value":{"id":"2","title":"Advanced Tips","author":"Bob","date":"Feb 3"}}
|
|
||||||
|
|
||||||
Note: state patches appear right after the elements that use them, so the UI fills in as it streams.`);
|
// Build example using actual catalog component names and props to avoid hallucinations
|
||||||
|
const allComponents = (catalog.data as Record<string, unknown>).components as
|
||||||
|
| Record<string, CatalogComponentDef>
|
||||||
|
| undefined;
|
||||||
|
const cn = catalog.componentNames;
|
||||||
|
const comp1 = cn[0] || "Component";
|
||||||
|
const comp2 = cn.length > 1 ? cn[1]! : comp1;
|
||||||
|
const comp1Def = allComponents?.[comp1];
|
||||||
|
const comp2Def = allComponents?.[comp2];
|
||||||
|
const comp1Props = comp1Def ? getExampleProps(comp1Def) : {};
|
||||||
|
const comp2Props = comp2Def ? getExampleProps(comp2Def) : {};
|
||||||
|
|
||||||
|
// Find a string prop on comp2 to demonstrate $path dynamic bindings
|
||||||
|
const dynamicPropName = comp2Def?.props
|
||||||
|
? findFirstStringProp(comp2Def.props)
|
||||||
|
: null;
|
||||||
|
const dynamicProps = dynamicPropName
|
||||||
|
? { ...comp2Props, [dynamicPropName]: { $path: "$item/title" } }
|
||||||
|
: comp2Props;
|
||||||
|
|
||||||
|
const exampleOutput = [
|
||||||
|
JSON.stringify({ op: "add", path: "/root", value: "main" }),
|
||||||
|
JSON.stringify({
|
||||||
|
op: "add",
|
||||||
|
path: "/elements/main",
|
||||||
|
value: {
|
||||||
|
type: comp1,
|
||||||
|
props: comp1Props,
|
||||||
|
children: ["child-1", "list"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
op: "add",
|
||||||
|
path: "/elements/child-1",
|
||||||
|
value: { type: comp2, props: comp2Props, children: [] },
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
op: "add",
|
||||||
|
path: "/elements/list",
|
||||||
|
value: {
|
||||||
|
type: comp1,
|
||||||
|
props: comp1Props,
|
||||||
|
repeat: { path: "/items", key: "id" },
|
||||||
|
children: ["item"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
op: "add",
|
||||||
|
path: "/elements/item",
|
||||||
|
value: { type: comp2, props: dynamicProps, children: [] },
|
||||||
|
}),
|
||||||
|
JSON.stringify({ op: "add", path: "/state/items", value: [] }),
|
||||||
|
JSON.stringify({
|
||||||
|
op: "add",
|
||||||
|
path: "/state/items/0",
|
||||||
|
value: { id: "1", title: "First Item" },
|
||||||
|
}),
|
||||||
|
JSON.stringify({
|
||||||
|
op: "add",
|
||||||
|
path: "/state/items/1",
|
||||||
|
value: { id: "2", title: "Second Item" },
|
||||||
|
}),
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
lines.push(`${exampleOutput}
|
||||||
|
|
||||||
|
Note: state patches appear right after the elements that use them, so the UI fills in as it streams. ONLY use component types from the AVAILABLE COMPONENTS list below.`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
// Initial state section
|
// Initial state section
|
||||||
@@ -601,7 +658,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|||||||
'The element itself renders once (as the container), and its children are expanded once per array item. "path" is the state array path. "key" is an optional field name on each item for stable React keys.',
|
'The element itself renders once (as the container), and its children are expanded once per array item. "path" is the state array path. "key" is an optional field name on each item for stable React keys.',
|
||||||
);
|
);
|
||||||
lines.push(
|
lines.push(
|
||||||
'Example: { "type": "Column", "props": { "gap": 8 }, "repeat": { "path": "/todos", "key": "id" }, "children": ["todo-item"] }',
|
`Example: ${JSON.stringify({ type: comp1, props: comp1Props, repeat: { path: "/todos", key: "id" }, children: ["todo-item"] })}`,
|
||||||
);
|
);
|
||||||
lines.push(
|
lines.push(
|
||||||
'Inside children of a repeated element, use "$item/field" for per-item paths: statePath:"$item/completed", { "$path": "$item/title" }. Use "$index" for the current array index.',
|
'Inside children of a repeated element, use "$item/field" for per-item paths: statePath:"$item/completed", { "$path": "$item/title" }. Use "$index" for the current array index.',
|
||||||
@@ -638,18 +695,8 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|||||||
);
|
);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
// Components section
|
// Components section — reuse the typed reference from example generation
|
||||||
const components = (catalog.data as Record<string, unknown>).components as
|
const components = allComponents;
|
||||||
| Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
props?: z.ZodType;
|
|
||||||
description?: string;
|
|
||||||
slots?: string[];
|
|
||||||
events?: string[];
|
|
||||||
}
|
|
||||||
>
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
if (components) {
|
if (components) {
|
||||||
lines.push(`AVAILABLE COMPONENTS (${catalog.componentNames.length}):`);
|
lines.push(`AVAILABLE COMPONENTS (${catalog.componentNames.length}):`);
|
||||||
@@ -694,7 +741,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Example:");
|
lines.push("Example:");
|
||||||
lines.push(
|
lines.push(
|
||||||
' {"type":"Button","props":{"label":"Save"},"on":{"press":{"action":"setState","params":{"path":"/saved","value":true}}},"children":[]}',
|
` ${JSON.stringify({ type: comp1, props: comp1Props, on: { press: { action: "setState", params: { path: "/saved", value: true } } }, children: [] })}`,
|
||||||
);
|
);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push(
|
lines.push(
|
||||||
@@ -711,7 +758,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|||||||
"Elements can have an optional `visible` field to conditionally show/hide based on data state. IMPORTANT: `visible` is a top-level field on the element object (sibling of type/props/children), NOT inside props.",
|
"Elements can have an optional `visible` field to conditionally show/hide based on data state. IMPORTANT: `visible` is a top-level field on the element object (sibling of type/props/children), NOT inside props.",
|
||||||
);
|
);
|
||||||
lines.push(
|
lines.push(
|
||||||
'Correct: {"type":"Column","props":{"gap":8},"visible":{"eq":[{"path":"/tab"},"home"]},"children":[...]}',
|
`Correct: ${JSON.stringify({ type: comp1, props: comp1Props, visible: { eq: [{ path: "/tab" }, "home"] }, children: ["..."] })}`,
|
||||||
);
|
);
|
||||||
lines.push(
|
lines.push(
|
||||||
'- `{ "eq": [{ "path": "/statePath" }, "value"] }` - visible when state at path equals value',
|
'- `{ "eq": [{ "path": "/statePath" }, "value"] }` - visible when state at path equals value',
|
||||||
@@ -726,10 +773,10 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|||||||
lines.push("- `true` / `false` - always visible/hidden");
|
lines.push("- `true` / `false` - always visible/hidden");
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push(
|
lines.push(
|
||||||
"Use the Pressable component with on.press bound to setState to update state and drive visibility.",
|
"Use a component with on.press bound to setState to update state and drive visibility.",
|
||||||
);
|
);
|
||||||
lines.push(
|
lines.push(
|
||||||
'Example: A Pressable with on: { "press": { "action": "setState", "params": { "path": "/activeTab", "value": "home" } } } sets state, then a container with visible: { "eq": [{ "path": "/activeTab" }, "home"] } shows only when that tab is active.',
|
`Example: A ${comp1} with on: { "press": { "action": "setState", "params": { "path": "/activeTab", "value": "home" } } } sets state, then a container with visible: { "eq": [{ "path": "/activeTab" }, "home"] } shows only when that tab is active.`,
|
||||||
);
|
);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
@@ -781,6 +828,169 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Example Value Generation from Zod Schemas
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component definition shape as it appears in catalog data
|
||||||
|
*/
|
||||||
|
interface CatalogComponentDef {
|
||||||
|
props?: z.ZodType;
|
||||||
|
description?: string;
|
||||||
|
slots?: string[];
|
||||||
|
events?: string[];
|
||||||
|
example?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get example props for a catalog component.
|
||||||
|
* Uses the explicit `example` field if provided, otherwise generates from Zod schema.
|
||||||
|
*/
|
||||||
|
function getExampleProps(def: CatalogComponentDef): Record<string, unknown> {
|
||||||
|
if (def.example && Object.keys(def.example).length > 0) {
|
||||||
|
return def.example;
|
||||||
|
}
|
||||||
|
if (def.props) {
|
||||||
|
return generateExamplePropsFromZod(def.props);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate example prop values from a Zod object schema.
|
||||||
|
* Only includes required fields to keep examples concise.
|
||||||
|
*/
|
||||||
|
function generateExamplePropsFromZod(
|
||||||
|
schema: z.ZodType,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
if (!schema || !schema._def) return {};
|
||||||
|
const def = schema._def as unknown as Record<string, unknown>;
|
||||||
|
const typeName = getZodTypeName(schema);
|
||||||
|
|
||||||
|
if (typeName !== "ZodObject" && typeName !== "object") return {};
|
||||||
|
|
||||||
|
const shape =
|
||||||
|
typeof def.shape === "function"
|
||||||
|
? (def.shape as () => Record<string, z.ZodType>)()
|
||||||
|
: (def.shape as Record<string, z.ZodType>);
|
||||||
|
if (!shape) return {};
|
||||||
|
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
for (const [key, value] of Object.entries(shape)) {
|
||||||
|
const innerTypeName = getZodTypeName(value);
|
||||||
|
// Skip optional props to keep examples concise
|
||||||
|
if (
|
||||||
|
innerTypeName === "ZodOptional" ||
|
||||||
|
innerTypeName === "optional" ||
|
||||||
|
innerTypeName === "ZodNullable" ||
|
||||||
|
innerTypeName === "nullable"
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result[key] = generateExampleValue(value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a single example value from a Zod type.
|
||||||
|
*/
|
||||||
|
function generateExampleValue(schema: z.ZodType): unknown {
|
||||||
|
if (!schema || !schema._def) return "...";
|
||||||
|
const def = schema._def as unknown as Record<string, unknown>;
|
||||||
|
const typeName = getZodTypeName(schema);
|
||||||
|
|
||||||
|
switch (typeName) {
|
||||||
|
case "ZodString":
|
||||||
|
case "string":
|
||||||
|
return "example";
|
||||||
|
case "ZodNumber":
|
||||||
|
case "number":
|
||||||
|
return 0;
|
||||||
|
case "ZodBoolean":
|
||||||
|
case "boolean":
|
||||||
|
return true;
|
||||||
|
case "ZodLiteral":
|
||||||
|
case "literal":
|
||||||
|
return def.value;
|
||||||
|
case "ZodEnum":
|
||||||
|
case "enum": {
|
||||||
|
if (Array.isArray(def.values) && def.values.length > 0)
|
||||||
|
return def.values[0];
|
||||||
|
if (def.entries && typeof def.entries === "object") {
|
||||||
|
const values = Object.values(def.entries as Record<string, string>);
|
||||||
|
return values.length > 0 ? values[0] : "example";
|
||||||
|
}
|
||||||
|
return "example";
|
||||||
|
}
|
||||||
|
case "ZodOptional":
|
||||||
|
case "optional":
|
||||||
|
case "ZodNullable":
|
||||||
|
case "nullable":
|
||||||
|
case "ZodDefault":
|
||||||
|
case "default": {
|
||||||
|
const inner = (def.innerType as z.ZodType) ?? (def.wrapped as z.ZodType);
|
||||||
|
return inner ? generateExampleValue(inner) : null;
|
||||||
|
}
|
||||||
|
case "ZodArray":
|
||||||
|
case "array":
|
||||||
|
return [];
|
||||||
|
case "ZodObject":
|
||||||
|
case "object":
|
||||||
|
return generateExamplePropsFromZod(schema);
|
||||||
|
case "ZodUnion":
|
||||||
|
case "union": {
|
||||||
|
const options = def.options as z.ZodType[] | undefined;
|
||||||
|
return options && options.length > 0
|
||||||
|
? generateExampleValue(options[0]!)
|
||||||
|
: "...";
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the name of the first required string prop in a Zod object schema.
|
||||||
|
* Used to demonstrate $path dynamic bindings in examples.
|
||||||
|
*/
|
||||||
|
function findFirstStringProp(schema?: z.ZodType): string | null {
|
||||||
|
if (!schema || !schema._def) return null;
|
||||||
|
const def = schema._def as unknown as Record<string, unknown>;
|
||||||
|
const typeName = getZodTypeName(schema);
|
||||||
|
|
||||||
|
if (typeName !== "ZodObject" && typeName !== "object") return null;
|
||||||
|
|
||||||
|
const shape =
|
||||||
|
typeof def.shape === "function"
|
||||||
|
? (def.shape as () => Record<string, z.ZodType>)()
|
||||||
|
: (def.shape as Record<string, z.ZodType>);
|
||||||
|
if (!shape) return null;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(shape)) {
|
||||||
|
const innerTypeName = getZodTypeName(value);
|
||||||
|
// Skip optional props
|
||||||
|
if (
|
||||||
|
innerTypeName === "ZodOptional" ||
|
||||||
|
innerTypeName === "optional" ||
|
||||||
|
innerTypeName === "ZodNullable" ||
|
||||||
|
innerTypeName === "nullable"
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Unwrap to check the actual type
|
||||||
|
if (innerTypeName === "ZodString" || innerTypeName === "string") {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Zod Introspection Helpers
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Zod type name from schema (handles different Zod versions)
|
* Get Zod type name from schema (handles different Zod versions)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# @json-render/react-native
|
||||||
|
|
||||||
|
## 0.5.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 429e456: Fix LLM hallucinations by dynamically generating prompt examples from the user's catalog instead of hardcoding component names. Adds optional `example` field to `ComponentDefinition` with Zod schema introspection fallback. Mentions RFC 6902 in output format section.
|
||||||
|
- Updated dependencies [429e456]
|
||||||
|
- @json-render/core@0.5.2
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- d9a4efd: Prevent rendering errors from crashing the application. Added error boundaries to all renderers so a single bad component silently disappears instead of causing a white-screen-of-death. Fixed Select and Radio components to handle non-string option values from AI output.
|
||||||
|
- @json-render/core@0.5.1
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [3d2d1ad]
|
||||||
|
- @json-render/core@0.5.0
|
||||||
@@ -59,7 +59,7 @@ export const { registry } = defineRegistry(catalog, {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Standard components (Container, Row, Column, Button, TextInput, etc.) are included by default -- you only need to register custom ones.
|
Standard components (Container, Row, Column, Button, TextInput, etc.) are included by default. You only need to register custom ones.
|
||||||
|
|
||||||
### 3. Render Specs
|
### 3. Render Specs
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ The `Pressable` component wraps children and triggers an action on press. It's e
|
|||||||
|
|
||||||
## Built-in Actions
|
## Built-in Actions
|
||||||
|
|
||||||
The `setState` action is handled automatically by `ActionProvider`. It updates the data model, which triggers re-evaluation of visibility conditions and dynamic prop expressions:
|
The `setState` action is handled automatically by `ActionProvider`. It updates the state model, which triggers re-evaluation of visibility conditions and dynamic prop expressions:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/react-native",
|
"name": "@json-render/react-native",
|
||||||
"version": "0.4.3",
|
"version": "0.5.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "React Native renderer for @json-render/core. JSON becomes React Native components.",
|
"description": "React Native renderer for @json-render/core. JSON becomes React Native components.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const standardComponentDefinitions = {
|
|||||||
slots: ["default"],
|
slots: ["default"],
|
||||||
description:
|
description:
|
||||||
"Generic container wrapper. Use for grouping elements with padding, margin, and background color.",
|
"Generic container wrapper. Use for grouping elements with padding, margin, and background color.",
|
||||||
|
example: { padding: 16, backgroundColor: "#FFFFFF" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Row: {
|
Row: {
|
||||||
@@ -53,6 +54,7 @@ export const standardComponentDefinitions = {
|
|||||||
slots: ["default"],
|
slots: ["default"],
|
||||||
description:
|
description:
|
||||||
"Horizontal flex layout. Use for placing elements side by side.",
|
"Horizontal flex layout. Use for placing elements side by side.",
|
||||||
|
example: { gap: 12, alignItems: "center" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Column: {
|
Column: {
|
||||||
@@ -77,6 +79,7 @@ export const standardComponentDefinitions = {
|
|||||||
slots: ["default"],
|
slots: ["default"],
|
||||||
description:
|
description:
|
||||||
"Vertical flex layout. Use for stacking elements top to bottom.",
|
"Vertical flex layout. Use for stacking elements top to bottom.",
|
||||||
|
example: { gap: 12, padding: 16 },
|
||||||
},
|
},
|
||||||
|
|
||||||
ScrollContainer: {
|
ScrollContainer: {
|
||||||
@@ -143,6 +146,7 @@ export const standardComponentDefinitions = {
|
|||||||
slots: [],
|
slots: [],
|
||||||
description:
|
description:
|
||||||
"Heading text at various levels. h1 is largest, h4 is smallest.",
|
"Heading text at various levels. h1 is largest, h4 is smallest.",
|
||||||
|
example: { text: "Welcome", level: "h1" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Paragraph: {
|
Paragraph: {
|
||||||
@@ -156,6 +160,7 @@ export const standardComponentDefinitions = {
|
|||||||
slots: [],
|
slots: [],
|
||||||
description:
|
description:
|
||||||
"Body text paragraph. Use for descriptions and longer content.",
|
"Body text paragraph. Use for descriptions and longer content.",
|
||||||
|
example: { text: "This is a paragraph of body text." },
|
||||||
},
|
},
|
||||||
|
|
||||||
Label: {
|
Label: {
|
||||||
@@ -168,6 +173,7 @@ export const standardComponentDefinitions = {
|
|||||||
slots: [],
|
slots: [],
|
||||||
description:
|
description:
|
||||||
"Small utility text. Use for captions, form labels, and secondary information.",
|
"Small utility text. Use for captions, form labels, and secondary information.",
|
||||||
|
example: { text: "Status", size: "sm" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Image: {
|
Image: {
|
||||||
@@ -181,6 +187,12 @@ export const standardComponentDefinitions = {
|
|||||||
}),
|
}),
|
||||||
slots: [],
|
slots: [],
|
||||||
description: "Image display. Provide a source URL and optional dimensions.",
|
description: "Image display. Provide a source URL and optional dimensions.",
|
||||||
|
example: {
|
||||||
|
src: "https://picsum.photos/300/200",
|
||||||
|
alt: "Photo",
|
||||||
|
width: 300,
|
||||||
|
height: 200,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Avatar: {
|
Avatar: {
|
||||||
@@ -193,6 +205,7 @@ export const standardComponentDefinitions = {
|
|||||||
slots: [],
|
slots: [],
|
||||||
description:
|
description:
|
||||||
"Circular avatar showing an image or initials. Use for user profiles and contacts.",
|
"Circular avatar showing an image or initials. Use for user profiles and contacts.",
|
||||||
|
example: { initials: "JD", size: "md" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Badge: {
|
Badge: {
|
||||||
@@ -205,6 +218,7 @@ export const standardComponentDefinitions = {
|
|||||||
slots: [],
|
slots: [],
|
||||||
description:
|
description:
|
||||||
"Small colored indicator with a label. Use for status, counts, and categories.",
|
"Small colored indicator with a label. Use for status, counts, and categories.",
|
||||||
|
example: { label: "New", variant: "info" },
|
||||||
},
|
},
|
||||||
|
|
||||||
Chip: {
|
Chip: {
|
||||||
@@ -237,6 +251,7 @@ export const standardComponentDefinitions = {
|
|||||||
slots: [],
|
slots: [],
|
||||||
description:
|
description:
|
||||||
"Pressable button with label. Set variant for styling. Bind on.press for the handler to call on press.",
|
"Pressable button with label. Set variant for styling. Bind on.press for the handler to call on press.",
|
||||||
|
example: { label: "Submit", variant: "primary" },
|
||||||
},
|
},
|
||||||
|
|
||||||
TextInput: {
|
TextInput: {
|
||||||
@@ -257,6 +272,11 @@ export const standardComponentDefinitions = {
|
|||||||
slots: [],
|
slots: [],
|
||||||
description:
|
description:
|
||||||
"Text input field. Use statePath to bind to the state model for two-way binding. The value typed by the user is stored at the statePath.",
|
"Text input field. Use statePath to bind to the state model for two-way binding. The value typed by the user is stored at the statePath.",
|
||||||
|
example: {
|
||||||
|
placeholder: "Enter text...",
|
||||||
|
statePath: "/inputValue",
|
||||||
|
label: "Name",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Switch: {
|
Switch: {
|
||||||
@@ -351,6 +371,7 @@ export const standardComponentDefinitions = {
|
|||||||
slots: ["default"],
|
slots: ["default"],
|
||||||
description:
|
description:
|
||||||
"Elevated card container with optional title. Use for grouping related content.",
|
"Elevated card container with optional title. Use for grouping related content.",
|
||||||
|
example: { title: "Details", padding: 16, elevated: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
ListItem: {
|
ListItem: {
|
||||||
@@ -365,6 +386,11 @@ export const standardComponentDefinitions = {
|
|||||||
slots: [],
|
slots: [],
|
||||||
description:
|
description:
|
||||||
"List row with title, subtitle, and optional leading/trailing text. Bind on.press for the press handler.",
|
"List row with title, subtitle, and optional leading/trailing text. Bind on.press for the press handler.",
|
||||||
|
example: {
|
||||||
|
title: "Settings",
|
||||||
|
subtitle: "Manage your preferences",
|
||||||
|
showChevron: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Modal: {
|
Modal: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, {
|
import React, {
|
||||||
type ComponentType,
|
type ComponentType,
|
||||||
|
type ErrorInfo,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -81,6 +82,51 @@ export interface RendererProps {
|
|||||||
fallback?: ComponentRenderer;
|
fallback?: ComponentRenderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ElementErrorBoundary – catches rendering errors in individual elements so
|
||||||
|
// a single bad component never crashes the whole page.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface ElementErrorBoundaryProps {
|
||||||
|
elementType: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ElementErrorBoundaryState {
|
||||||
|
hasError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ElementErrorBoundary extends React.Component<
|
||||||
|
ElementErrorBoundaryProps,
|
||||||
|
ElementErrorBoundaryState
|
||||||
|
> {
|
||||||
|
constructor(props: ElementErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(): ElementErrorBoundaryState {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||||
|
console.error(
|
||||||
|
`[json-render] Rendering error in <${this.props.elementType}>:`,
|
||||||
|
error,
|
||||||
|
info.componentStack,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
// Render nothing – the element silently disappears rather than
|
||||||
|
// crashing the entire application.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Element renderer component
|
* Element renderer component
|
||||||
*/
|
*/
|
||||||
@@ -214,9 +260,11 @@ function ElementRenderer({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component element={resolvedElement} emit={emit} loading={loading}>
|
<ElementErrorBoundary elementType={resolvedElement.type}>
|
||||||
{children}
|
<Component element={resolvedElement} emit={emit} loading={loading}>
|
||||||
</Component>
|
{children}
|
||||||
|
</Component>
|
||||||
|
</ElementErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export const schema = defineSchema(
|
|||||||
slots: s.array(s.string()),
|
slots: s.array(s.string()),
|
||||||
/** Description for AI generation hints */
|
/** Description for AI generation hints */
|
||||||
description: s.string(),
|
description: s.string(),
|
||||||
|
/** Example prop values used in prompt examples (auto-generated from Zod schema if omitted) */
|
||||||
|
example: s.any(),
|
||||||
}),
|
}),
|
||||||
/** Action definitions (optional) */
|
/** Action definitions (optional) */
|
||||||
actions: s.map({
|
actions: s.map({
|
||||||
@@ -51,37 +53,37 @@ export const schema = defineSchema(
|
|||||||
{
|
{
|
||||||
defaultRules: [
|
defaultRules: [
|
||||||
// Layout patterns
|
// Layout patterns
|
||||||
"FIXED BOTTOM BAR PATTERN: When building a screen with a fixed header and/or fixed bottom tab bar, the outermost Column must have flex:1 so it fills the SafeArea. The scrollable content area must also have flex:1. Structure: SafeArea > Column(flex:1, gap:0) > [header, Container(flex:1) > [ScrollContainer(...)], bottom-tabs]. Both the outer Column AND the content Container need flex:1.",
|
"FIXED BOTTOM BAR PATTERN: When building a screen with a fixed header and/or fixed bottom tab bar, the outermost vertical layout component must have flex:1 so it fills the screen. The scrollable content area must also have flex:1. Structure: screen wrapper > vertical layout(flex:1, gap:0) > [header, content wrapper(flex:1) > [scroll container(...)], bottom-tabs]. Both the outer layout AND the content wrapper need flex:1. ONLY use components from the AVAILABLE COMPONENTS list.",
|
||||||
"NEVER place a bottom tab bar or fixed footer inside a ScrollContainer. It must be a sibling AFTER the flex:1 container that holds the ScrollContainer.",
|
"NEVER place a bottom tab bar or fixed footer inside a scroll container. It must be a sibling AFTER the flex:1 container that holds the scroll content.",
|
||||||
|
|
||||||
// Element integrity
|
// Element integrity
|
||||||
"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.",
|
"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.",
|
||||||
"SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.",
|
"SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.",
|
||||||
'When building repeating content backed by a state array (e.g. todos, posts, cart items), use the "repeat" field on the container element. Example: { "type": "Column", "props": { "gap": 8 }, "repeat": { "path": "/todos", "key": "id" }, "children": ["todo-item"] }. Inside repeated children, use "$item/field" for per-item state paths and "$index" for the current array index. Do NOT hardcode individual elements for each array item.',
|
'When building repeating content backed by a state array (e.g. todos, posts, cart items), use the "repeat" field on a container element from the AVAILABLE COMPONENTS list. Example: { "type": "<ContainerComponent>", "props": { "gap": 8 }, "repeat": { "path": "/todos", "key": "id" }, "children": ["todo-item"] }. Inside repeated children, use "$item/field" for per-item state paths and "$index" for the current array index. Do NOT hardcode individual elements for each array item.',
|
||||||
|
|
||||||
// Visible field placement
|
// Visible field placement
|
||||||
'CRITICAL: The "visible" field goes on the ELEMENT object, NOT inside "props". Correct: {"type":"Column","props":{"gap":8},"visible":{"eq":[{"path":"/activeTab"},"home"]},"children":[...]}. WRONG: {"type":"Column","props":{"gap":8,"visible":{...}},"children":[...]}.',
|
'CRITICAL: The "visible" field goes on the ELEMENT object, NOT inside "props". Correct: {"type":"<ComponentName>","props":{},"visible":{"eq":[{"path":"/activeTab"},"home"]},"children":[...]}. WRONG: {"type":"<ComponentName>","props":{},"visible":{...},"children":[...]} with visible inside props.',
|
||||||
|
|
||||||
// Tab navigation pattern
|
// Tab navigation pattern
|
||||||
"TAB NAVIGATION PATTERN: When building a UI with multiple tabs, use Pressable + setState action + visible conditions to make tabs functional.",
|
"TAB NAVIGATION PATTERN: When building a UI with multiple tabs, use a pressable/tappable component + setState action + visible conditions to make tabs functional. ONLY use components from the AVAILABLE COMPONENTS list.",
|
||||||
'Each tab button should be a Pressable wrapping its icon/label children, with action "setState" and actionParams { "path": "/activeTab", "value": "tabName" }.',
|
'Each tab button should be a pressable component wrapping its icon/label children, with action "setState" and actionParams { "path": "/activeTab", "value": "tabName" }.',
|
||||||
'Each tab\'s content section should have a visible condition: { "eq": [{ "path": "/activeTab" }, "tabName"] }.',
|
'Each tab\'s content section should have a visible condition: { "eq": [{ "path": "/activeTab" }, "tabName"] }.',
|
||||||
"The first tab's content should NOT have a visible condition (so it shows by default when no tab is selected yet). All other tabs MUST have a visible condition.",
|
"The first tab's content should NOT have a visible condition (so it shows by default when no tab is selected yet). All other tabs MUST have a visible condition.",
|
||||||
|
|
||||||
// Tab active state highlighting (using dynamic props)
|
// Tab active state highlighting (using dynamic props)
|
||||||
"TAB ACTIVE STYLING: Use $cond dynamic props on the Icon inside each tab Pressable so a single Icon changes appearance based on the active tab.",
|
"TAB ACTIVE STYLING: Use $cond dynamic props on icon elements inside each tab button so a single icon changes appearance based on the active tab.",
|
||||||
' - For the icon name: { "$cond": { "eq": [{ "path": "/activeTab" }, "thisTabName"] }, "$then": "home", "$else": "home-outline" }',
|
' - For the icon name: { "$cond": { "eq": [{ "path": "/activeTab" }, "thisTabName"] }, "$then": "home", "$else": "home-outline" }',
|
||||||
' - For the icon color: { "$cond": { "eq": [{ "path": "/activeTab" }, "thisTabName"] }, "$then": "#007AFF", "$else": "#8E8E93" }',
|
' - For the icon color: { "$cond": { "eq": [{ "path": "/activeTab" }, "thisTabName"] }, "$then": "#007AFF", "$else": "#8E8E93" }',
|
||||||
" - For labels, use $cond on the color prop similarly.",
|
" - For labels, use $cond on the color prop similarly.",
|
||||||
' - For the FIRST/DEFAULT tab, use { "$cond": { "or": [{ "eq": [{ "path": "/activeTab" }, "thisTabName"] }, { "not": { "path": "/activeTab" } }] }, "$then": "#007AFF", "$else": "#8E8E93" } so it appears active before any tab is tapped.',
|
' - For the FIRST/DEFAULT tab, use { "$cond": { "or": [{ "eq": [{ "path": "/activeTab" }, "thisTabName"] }, { "not": { "path": "/activeTab" } }] }, "$then": "#007AFF", "$else": "#8E8E93" } so it appears active before any tab is tapped.',
|
||||||
|
|
||||||
// Push/pop screen navigation (all screens in one spec)
|
// Push/pop screen navigation (all screens in one spec)
|
||||||
'SCREEN NAVIGATION: Use Pressable with action "push" and actionParams { "screen": "screenName" } to navigate to a new screen. Use action "pop" to go back. All screens must be defined in the SAME spec.',
|
'SCREEN NAVIGATION: Use a pressable component with action "push" and actionParams { "screen": "screenName" } to navigate to a new screen. Use action "pop" to go back. All screens must be defined in the SAME spec. ONLY use components from the AVAILABLE COMPONENTS list.',
|
||||||
'Each screen section uses a visible condition on /currentScreen: { "eq": [{ "path": "/currentScreen" }, "screenName"] }. The default/home screen should also be visible when /currentScreen is not set: { "or": [{ "eq": [{ "path": "/currentScreen" }, "home"] }, { "not": { "path": "/currentScreen" } }] }.',
|
'Each screen section uses a visible condition on /currentScreen: { "eq": [{ "path": "/currentScreen" }, "screenName"] }. The default/home screen should also be visible when /currentScreen is not set: { "or": [{ "eq": [{ "path": "/currentScreen" }, "home"] }, { "not": { "path": "/currentScreen" } }] }.',
|
||||||
"push automatically maintains a /navStack in the state model so pop always returns to the previous screen.",
|
"push automatically maintains a /navStack in the state model so pop always returns to the previous screen.",
|
||||||
'Include a back button on pushed screens using action "pop". Example: Pressable(action:"pop") > Row > Icon(name:"chevron-back") + Label(text:"Back").',
|
'Include a back button on pushed screens using action "pop". Example: pressable(action:"pop") > row layout > back icon + back label. ONLY use components from the AVAILABLE COMPONENTS list.',
|
||||||
"Use push/pop for drill-down flows: tapping a list item to see details, opening a profile, etc. Use setState + visible conditions for tab switching within a screen.",
|
"Use push/pop for drill-down flows: tapping a list item to see details, opening a profile, etc. Use setState + visible conditions for tab switching within a screen.",
|
||||||
'Example: A list screen with items that push to detail: Pressable(action:"push", actionParams:{screen:"repo-detail"}) wrapping each list item card. The detail screen section has visible:{"eq":[{"path":"/currentScreen"},"repo-detail"]} and contains a back button with action:"pop".',
|
'Example: A list screen with items that push to detail: a pressable component with action:"push" and actionParams:{screen:"detail"} wrapping each list item. The detail screen section has visible:{"eq":[{"path":"/currentScreen"},"detail"]} and contains a back button with action:"pop". ONLY use components from the AVAILABLE COMPONENTS list.',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
# @json-render/react
|
# @json-render/react
|
||||||
|
|
||||||
|
## 0.5.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 429e456: Fix LLM hallucinations by dynamically generating prompt examples from the user's catalog instead of hardcoding component names. Adds optional `example` field to `ComponentDefinition` with Zod schema introspection fallback. Mentions RFC 6902 in output format section.
|
||||||
|
- Updated dependencies [429e456]
|
||||||
|
- @json-render/core@0.5.2
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- d9a4efd: Prevent rendering errors from crashing the application. Added error boundaries to all renderers so a single bad component silently disappears instead of causing a white-screen-of-death. Fixed Select and Radio components to handle non-string option values from AI output.
|
||||||
|
- @json-render/core@0.5.1
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [3d2d1ad]
|
||||||
|
- @json-render/core@0.5.0
|
||||||
|
|
||||||
## 0.4.4
|
## 0.4.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ export const { registry } = defineRegistry(catalog, {
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
Button: ({ props, onAction }) => (
|
Button: ({ props, emit }) => (
|
||||||
<button onClick={() => onAction?.({ name: props.action })}>
|
<button onClick={() => emit?.("press")}>
|
||||||
{props.label}
|
{props.label}
|
||||||
</button>
|
</button>
|
||||||
),
|
),
|
||||||
@@ -274,7 +274,7 @@ See [@json-render/core](../core/README.md) for full expression syntax.
|
|||||||
|
|
||||||
## Built-in Actions
|
## Built-in Actions
|
||||||
|
|
||||||
The `setState` action is handled automatically by `ActionProvider`. It updates the data model, which triggers re-evaluation of visibility conditions and dynamic prop expressions:
|
The `setState` action is handled automatically by `ActionProvider`. It updates the state model, which triggers re-evaluation of visibility conditions and dynamic prop expressions:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -295,7 +295,7 @@ When using `defineRegistry`, components receive these props:
|
|||||||
interface ComponentContext<P> {
|
interface ComponentContext<P> {
|
||||||
props: P; // Typed props from the catalog
|
props: P; // Typed props from the catalog
|
||||||
children?: React.ReactNode; // Rendered children
|
children?: React.ReactNode; // Rendered children
|
||||||
onAction?: (action: { name: string; params?: Record<string, unknown> }) => void;
|
emit?: (event: string) => void; // Emit a named event
|
||||||
loading?: boolean; // Whether the parent is loading
|
loading?: boolean; // Whether the parent is loading
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/react",
|
"name": "@json-render/react",
|
||||||
"version": "0.4.4",
|
"version": "0.5.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "React renderer for @json-render/core. JSON becomes React components.",
|
"description": "React renderer for @json-render/core. JSON becomes React components.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
type ComponentType,
|
type ComponentType,
|
||||||
|
type ErrorInfo,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -77,6 +78,51 @@ export interface RendererProps {
|
|||||||
fallback?: ComponentRenderer;
|
fallback?: ComponentRenderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ElementErrorBoundary – catches rendering errors in individual elements so
|
||||||
|
// a single bad component never crashes the whole page.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface ElementErrorBoundaryProps {
|
||||||
|
elementType: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ElementErrorBoundaryState {
|
||||||
|
hasError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ElementErrorBoundary extends React.Component<
|
||||||
|
ElementErrorBoundaryProps,
|
||||||
|
ElementErrorBoundaryState
|
||||||
|
> {
|
||||||
|
constructor(props: ElementErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(): ElementErrorBoundaryState {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||||
|
console.error(
|
||||||
|
`[json-render] Rendering error in <${this.props.elementType}>:`,
|
||||||
|
error,
|
||||||
|
info.componentStack,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
// Render nothing – the element silently disappears rather than
|
||||||
|
// crashing the entire application.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Element renderer component
|
* Element renderer component
|
||||||
*/
|
*/
|
||||||
@@ -210,9 +256,11 @@ function ElementRenderer({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component element={resolvedElement} emit={emit} loading={loading}>
|
<ElementErrorBoundary elementType={resolvedElement.type}>
|
||||||
{children}
|
<Component element={resolvedElement} emit={emit} loading={loading}>
|
||||||
</Component>
|
{children}
|
||||||
|
</Component>
|
||||||
|
</ElementErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export const schema = defineSchema(
|
|||||||
slots: s.array(s.string()),
|
slots: s.array(s.string()),
|
||||||
/** Description for AI generation hints */
|
/** Description for AI generation hints */
|
||||||
description: s.string(),
|
description: s.string(),
|
||||||
|
/** Example prop values used in prompt examples (auto-generated from Zod schema if omitted) */
|
||||||
|
example: s.any(),
|
||||||
}),
|
}),
|
||||||
/** Action definitions (optional) */
|
/** Action definitions (optional) */
|
||||||
actions: s.map({
|
actions: s.map({
|
||||||
@@ -55,16 +57,16 @@ export const schema = defineSchema(
|
|||||||
"SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.",
|
"SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.",
|
||||||
|
|
||||||
// Field placement
|
// Field placement
|
||||||
'CRITICAL: The "visible" field goes on the ELEMENT object, NOT inside "props". Correct: {"type":"Stack","props":{"gap":"md"},"visible":{"eq":[{"path":"/tab"},"home"]},"children":[...]}.',
|
'CRITICAL: The "visible" field goes on the ELEMENT object, NOT inside "props". Correct: {"type":"<ComponentName>","props":{},"visible":{"eq":[{"path":"/tab"},"home"]},"children":[...]}.',
|
||||||
'CRITICAL: The "on" field goes on the ELEMENT object, NOT inside "props". Use on.press, on.change, on.submit etc. NEVER put action/actionParams inside props.',
|
'CRITICAL: The "on" field goes on the ELEMENT object, NOT inside "props". Use on.press, on.change, on.submit etc. NEVER put action/actionParams inside props.',
|
||||||
|
|
||||||
// State and data
|
// State and data
|
||||||
"When the user asks for a UI that displays data (e.g. blog posts, products, users), ALWAYS include a state field with realistic sample data. The state field is a top-level field on the spec (sibling of root/elements).",
|
"When the user asks for a UI that displays data (e.g. blog posts, products, users), ALWAYS include a state field with realistic sample data. The state field is a top-level field on the spec (sibling of root/elements).",
|
||||||
'When building repeating content backed by a state array (e.g. posts, products, items), use the "repeat" field on the container element. Example: { "type": "Grid", "props": { "columns": 3, "gap": "md" }, "repeat": { "path": "/posts", "key": "id" }, "children": ["post-card"] }. Inside repeated children, use "$item/field" for per-item state paths and "$index" for the current array index. Do NOT hardcode individual elements for each array item.',
|
'When building repeating content backed by a state array (e.g. posts, products, items), use the "repeat" field on a container element. Example: { "type": "<ContainerComponent>", "props": {}, "repeat": { "path": "/posts", "key": "id" }, "children": ["post-card"] }. Replace <ContainerComponent> with an appropriate component from the AVAILABLE COMPONENTS list. Inside repeated children, use "$item/field" for per-item state paths and "$index" for the current array index. Do NOT hardcode individual elements for each array item.',
|
||||||
|
|
||||||
// Design quality
|
// Design quality
|
||||||
"Design with visual hierarchy: use Cards to group content, Headings for section titles, proper spacing with Stack gaps, and Badges/colors for status indicators.",
|
"Design with visual hierarchy: use container components to group content, heading components for section titles, proper spacing, and status indicators. ONLY use components from the AVAILABLE COMPONENTS list.",
|
||||||
"For data-rich UIs, use Grid for multi-column layouts (2-3 columns for cards/posts). For forms and single-column content, use Stack with direction:vertical.",
|
"For data-rich UIs, use multi-column layout components if available. For forms and single-column content, use vertical layout components. ONLY use components from the AVAILABLE COMPONENTS list.",
|
||||||
"Always include realistic, professional-looking sample data. For blogs include 3-4 posts with varied titles, authors, dates, categories. For products include names, prices, images. Never leave data empty.",
|
"Always include realistic, professional-looking sample data. For blogs include 3-4 posts with varied titles, authors, dates, categories. For products include names, prices, images. Never leave data empty.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
# @json-render/remotion
|
# @json-render/remotion
|
||||||
|
|
||||||
|
## 0.5.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 429e456: Fix LLM hallucinations by dynamically generating prompt examples from the user's catalog instead of hardcoding component names. Adds optional `example` field to `ComponentDefinition` with Zod schema introspection fallback. Mentions RFC 6902 in output format section.
|
||||||
|
- Updated dependencies [429e456]
|
||||||
|
- @json-render/core@0.5.2
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- d9a4efd: Prevent rendering errors from crashing the application. Added error boundaries to all renderers so a single bad component silently disappears instead of causing a white-screen-of-death. Fixed Select and Radio components to handle non-string option values from AI output.
|
||||||
|
- @json-render/core@0.5.1
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [3d2d1ad]
|
||||||
|
- @json-render/core@0.5.0
|
||||||
|
|
||||||
## 0.4.4
|
## 0.4.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/remotion",
|
"name": "@json-render/remotion",
|
||||||
"version": "0.4.4",
|
"version": "0.5.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Remotion renderer for @json-render/core. JSON becomes video compositions.",
|
"description": "Remotion renderer for @json-render/core. JSON becomes video compositions.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const standardComponentDefinitions = {
|
|||||||
defaultDuration: 90,
|
defaultDuration: 90,
|
||||||
description:
|
description:
|
||||||
"Full-screen title card with centered text. Use for intros, outros, and section breaks.",
|
"Full-screen title card with centered text. Use for intros, outros, and section breaks.",
|
||||||
|
example: { title: "Welcome", subtitle: "An introduction" },
|
||||||
},
|
},
|
||||||
|
|
||||||
ImageSlide: {
|
ImageSlide: {
|
||||||
@@ -34,6 +35,11 @@ export const standardComponentDefinitions = {
|
|||||||
defaultDuration: 150,
|
defaultDuration: 150,
|
||||||
description:
|
description:
|
||||||
"Full-screen image display. Use for product shots, photos, and visual content.",
|
"Full-screen image display. Use for product shots, photos, and visual content.",
|
||||||
|
example: {
|
||||||
|
src: "https://picsum.photos/1920/1080?random=1",
|
||||||
|
alt: "Hero image",
|
||||||
|
fit: "cover",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
SplitScreen: {
|
SplitScreen: {
|
||||||
@@ -61,6 +67,10 @@ export const standardComponentDefinitions = {
|
|||||||
defaultDuration: 150,
|
defaultDuration: 150,
|
||||||
description:
|
description:
|
||||||
"Quote display with author. Props: quote, author, textColor, backgroundColor. Set transparent:true when using as overlay on images.",
|
"Quote display with author. Props: quote, author, textColor, backgroundColor. Set transparent:true when using as overlay on images.",
|
||||||
|
example: {
|
||||||
|
quote: "The best way to predict the future is to invent it.",
|
||||||
|
author: "Alan Kay",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
StatCard: {
|
StatCard: {
|
||||||
@@ -74,6 +84,7 @@ export const standardComponentDefinitions = {
|
|||||||
type: "scene",
|
type: "scene",
|
||||||
defaultDuration: 90,
|
defaultDuration: 90,
|
||||||
description: "Large statistic display. Use for key metrics and numbers.",
|
description: "Large statistic display. Use for key metrics and numbers.",
|
||||||
|
example: { value: "10M+", label: "Users worldwide", prefix: "" },
|
||||||
},
|
},
|
||||||
|
|
||||||
TypingText: {
|
TypingText: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import React, { type ErrorInfo, type ReactNode } from "react";
|
||||||
import { AbsoluteFill, Sequence } from "remotion";
|
import { AbsoluteFill, Sequence } from "remotion";
|
||||||
import type { TimelineSpec, ComponentRegistry, Clip } from "./types";
|
import type { TimelineSpec, ComponentRegistry, Clip } from "./types";
|
||||||
|
|
||||||
@@ -33,6 +34,50 @@ export const standardComponents: ComponentRegistry = {
|
|||||||
VideoClip,
|
VideoClip,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ClipErrorBoundary – catches rendering errors in individual clips so
|
||||||
|
// a single bad clip never crashes the entire composition.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface ClipErrorBoundaryProps {
|
||||||
|
clipId: string;
|
||||||
|
component: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClipErrorBoundaryState {
|
||||||
|
hasError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClipErrorBoundary extends React.Component<
|
||||||
|
ClipErrorBoundaryProps,
|
||||||
|
ClipErrorBoundaryState
|
||||||
|
> {
|
||||||
|
constructor(props: ClipErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(): ClipErrorBoundaryState {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||||
|
console.error(
|
||||||
|
`[json-render/remotion] Rendering error in clip "${this.props.clipId}" (<${this.props.component}>):`,
|
||||||
|
error,
|
||||||
|
info.componentStack,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface RendererProps {
|
interface RendererProps {
|
||||||
/** The timeline spec to render */
|
/** The timeline spec to render */
|
||||||
spec: TimelineSpec;
|
spec: TimelineSpec;
|
||||||
@@ -102,7 +147,9 @@ export function Renderer({
|
|||||||
from={clip.from}
|
from={clip.from}
|
||||||
durationInFrames={clip.durationInFrames}
|
durationInFrames={clip.durationInFrames}
|
||||||
>
|
>
|
||||||
<Component clip={clip} />
|
<ClipErrorBoundary clipId={clip.id} component={clip.component}>
|
||||||
|
<Component clip={clip} />
|
||||||
|
</ClipErrorBoundary>
|
||||||
</Sequence>
|
</Sequence>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ const finalSpec = compiler.getResult();
|
|||||||
|
|
||||||
Any prop value can be a dynamic expression resolved at render time:
|
Any prop value can be a dynamic expression resolved at render time:
|
||||||
|
|
||||||
- **`{ "$path": "/state/key" }`** -- reads a value from the data model
|
- **`{ "$path": "/state/key" }`** - reads a value from the state model
|
||||||
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** -- evaluates a visibility condition and picks a branch
|
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - evaluates a visibility condition and picks a branch
|
||||||
|
|
||||||
`$cond` uses the same syntax as visibility conditions (`eq`, `neq`, `path`, `and`, `or`, `not`). `$then` and `$else` can themselves be expressions (recursive).
|
`$cond` uses the same syntax as visibility conditions (`eq`, `neq`, `path`, `and`, `or`, `not`). `$then` and `$else` can themselves be expressions (recursive).
|
||||||
|
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ Use `Pressable` with the built-in `setState` action for interactive UIs like tab
|
|||||||
|
|
||||||
Any prop value can be a data-driven expression resolved at render time:
|
Any prop value can be a data-driven expression resolved at render time:
|
||||||
|
|
||||||
- **`{ "$path": "/state/key" }`** -- reads from data model
|
- **`{ "$path": "/state/key" }`** - reads from data model
|
||||||
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** -- conditional value
|
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - conditional value
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -123,11 +123,11 @@ Any prop value can be a data-driven expression resolved at render time:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Components receive already-resolved props -- no changes needed to component implementations.
|
Components receive already-resolved props. No changes needed to component implementations.
|
||||||
|
|
||||||
## Built-in Actions
|
## Built-in Actions
|
||||||
|
|
||||||
The `setState` action is handled automatically by `ActionProvider` and updates the data model directly, which re-evaluates visibility conditions and dynamic prop expressions:
|
The `setState` action is handled automatically by `ActionProvider` and updates the state model directly, which re-evaluates visibility conditions and dynamic prop expressions:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "action": "setState", "actionParams": { "path": "/activeTab", "value": "home" } }
|
{ "action": "setState", "actionParams": { "path": "/activeTab", "value": "home" } }
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ The React schema uses an element tree format:
|
|||||||
| Provider | Purpose |
|
| Provider | Purpose |
|
||||||
|----------|---------|
|
|----------|---------|
|
||||||
| `StateProvider` | Share state across components (JSON Pointer paths) |
|
| `StateProvider` | Share state across components (JSON Pointer paths) |
|
||||||
| `ActionProvider` | Handle actions dispatched from components |
|
| `ActionProvider` | Handle actions dispatched via the event system |
|
||||||
| `VisibilityProvider` | Enable conditional rendering based on state |
|
| `VisibilityProvider` | Enable conditional rendering based on state |
|
||||||
| `ValidationProvider` | Form field validation |
|
| `ValidationProvider` | Form field validation |
|
||||||
|
|
||||||
@@ -93,8 +93,8 @@ The React schema uses an element tree format:
|
|||||||
|
|
||||||
Any prop value can be a data-driven expression resolved by the renderer before components receive props:
|
Any prop value can be a data-driven expression resolved by the renderer before components receive props:
|
||||||
|
|
||||||
- **`{ "$path": "/state/key" }`** -- reads from data model
|
- **`{ "$path": "/state/key" }`** - reads from data model
|
||||||
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** -- conditional value
|
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - conditional value
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -106,11 +106,30 @@ Any prop value can be a data-driven expression resolved by the renderer before c
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Components receive already-resolved props -- no changes needed to component implementations.
|
Components receive already-resolved props. No changes needed to component implementations.
|
||||||
|
|
||||||
|
## Event System
|
||||||
|
|
||||||
|
Components use `emit` to fire named events. The element's `on` field maps events to action bindings:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Component emits a named event
|
||||||
|
Button: ({ props, emit }) => (
|
||||||
|
<button onClick={() => emit?.("press")}>{props.label}</button>
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"props": { "label": "Submit" },
|
||||||
|
"on": { "press": { "action": "submit" } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Built-in Actions
|
## Built-in Actions
|
||||||
|
|
||||||
The `setState` action is handled automatically by `ActionProvider` and updates the data model directly, which re-evaluates visibility conditions and dynamic prop expressions:
|
The `setState` action is handled automatically by `ActionProvider` and updates the state model directly, which re-evaluates visibility conditions and dynamic prop expressions:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "action": "setState", "actionParams": { "path": "/activeTab", "value": "home" } }
|
{ "action": "setState", "actionParams": { "path": "/activeTab", "value": "home" } }
|
||||||
@@ -123,9 +142,9 @@ The `setState` action is handled automatically by `ActionProvider` and updates t
|
|||||||
| `defineRegistry` | Create a type-safe component registry from a catalog |
|
| `defineRegistry` | Create a type-safe component registry from a catalog |
|
||||||
| `Renderer` | Render a spec using a registry |
|
| `Renderer` | Render a spec using a registry |
|
||||||
| `schema` | Element tree schema |
|
| `schema` | Element tree schema |
|
||||||
| `useStateStore` | Access data context |
|
| `useStateStore` | Access state context |
|
||||||
| `useStateValue` | Get single value from data |
|
| `useStateValue` | Get single value from state |
|
||||||
| `useStateBinding` | Two-way data binding |
|
| `useStateBinding` | Two-way state binding |
|
||||||
| `useActions` | Access actions context |
|
| `useActions` | Access actions context |
|
||||||
| `useAction` | Get a single action dispatch function |
|
| `useAction` | Get a single action dispatch function |
|
||||||
| `useUIStream` | Stream specs from an API endpoint |
|
| `useUIStream` | Stream specs from an API endpoint |
|
||||||
|
|||||||
Reference in New Issue
Block a user