Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions apps/web/app/(main)/docs/api/core/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -339,17 +339,18 @@ setSpec({ ...applySpecPatch(spec, patch) });

### nestedToFlat

Convert a nested element tree (with inline children) into the flat `Spec` format:
Convert a nested element tree (with inline children and named slots) into the flat `Spec` format:

```typescript
import { nestedToFlat } from '@json-render/core';

const flat = nestedToFlat({
type: "Card",
props: { title: "Hello" },
children: [
{ type: "Text", props: { content: "World" }, children: [] }
],
type: "Layout",
props: {},
children: [{ type: "Text", props: { content: "Main" }, children: [] }],
slots: {
header: [{ type: "Heading", props: { text: "Header" }, children: [] }],
},
});
// { root: "el-0", elements: { "el-0": ..., "el-1": ... } }
```
Expand Down Expand Up @@ -648,6 +649,7 @@ interface UIElement {
type: string;
props: Record<string, unknown>;
children?: string[]; // Keys of child elements
slots?: Record<string, string[]>; // Named slots mapped to child keys
visible?: VisibilityCondition;
on?: Record<string, ActionBinding | ActionBinding[]>; // Event bindings
repeat?: { statePath: string | { $item: string }; key?: string }; // Repeat for arrays
Expand All @@ -666,7 +668,7 @@ interface Spec {
}
```

Elements are stored as a flat map with string keys. The tree structure is built by following the `children` arrays.
Elements are stored as a flat map with string keys. The tree structure is built by following `children` and named `slots` references.

### ActionBinding

Expand Down
40 changes: 25 additions & 15 deletions apps/web/app/(main)/docs/catalog/page.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/catalog")
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/catalog");

# Catalog

Expand All @@ -18,9 +18,9 @@ A catalog is the vocabulary for your UI. While the [schema](/docs/schemas) defin
`defineCatalog` is from `@json-render/core`. The `schema` import comes from your platform package (`@json-render/react` or `@json-render/react-native`) and defines the element structure the catalog targets. The catalog definition itself is framework-agnostic.

```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema'; // or '@json-render/react-native/schema'
import { z } from 'zod';
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema"; // or '@json-render/react-native/schema'
import { z } from "zod";

const catalog = defineCatalog(schema, {
components: {
Expand All @@ -29,35 +29,35 @@ const catalog = defineCatalog(schema, {
props: z.object({
title: z.string(),
description: z.string().nullable(),
padding: z.enum(['sm', 'md', 'lg']).nullable(),
padding: z.enum(["sm", "md", "lg"]).nullable(),
}),
slots: ["default"], // Can contain other components
description: "Container card for grouping content",
},

Metric: {
props: z.object({
label: z.string(),
value: z.union([z.string(), z.number()]),
format: z.enum(['currency', 'percent', 'number']),
format: z.enum(["currency", "percent", "number"]),
}),
description: "Display a single metric value",
},
},

actions: {
submit_form: {
params: z.object({
formId: z.string(),
}),
description: 'Submit a form',
description: "Submit a form",
},

export_data: {
params: z.object({
format: z.enum(['csv', 'pdf', 'json']),
format: z.enum(["csv", "pdf", "json"]),
}),
description: 'Export data in various formats',
description: "Export data in various formats",
},
},
});
Expand All @@ -70,12 +70,22 @@ Each component in the catalog has:
```typescript
{
props: z.object({...}), // Zod schema for props (use .nullable() for optional)
slots?: string[], // Named slots for children (e.g., ["default"])
slots?: string[], // Available slots (e.g., ["default", "header", "footer"])
description?: string, // Help AI understand when to use it
}
```

Use `slots: ["default"]` for components that can contain children. The slot name corresponds to where child elements are rendered.
Use `"default"` for regular children. Add named slots when a component places content in multiple regions:

```typescript
Layout: {
props: z.object({}),
slots: ["default", "header", "footer"],
description: "Page layout with header, content, and footer regions",
}
```

React specs use `children` for the default slot and a `slots` object for the other names.

## Generating AI Prompts

Expand Down
95 changes: 56 additions & 39 deletions apps/web/app/(main)/docs/registry/page.mdx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/registry")
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("docs/registry");

# Registry

A registry maps your [catalog](/docs/catalog) definitions to platform-specific implementations. The catalog defines *what* AI can generatethe registry provides the *how*.
A registry maps your [catalog](/docs/catalog) definitions to platform-specific implementations. The catalog defines _what_ AI can generate; the registry provides the _how_.

What a registry contains depends on the schema you use. Each package defines its own schema, which determines the shape of both the catalog and the registry.

Expand All @@ -19,8 +19,8 @@ What a registry contains depends on the schema you use. Each package defines its
Use `defineRegistry` to create a type-safe registry from your catalog. Pass your components, actions, or both:

```tsx
import { defineRegistry } from '@json-render/react';
import { myCatalog } from './catalog';
import { defineRegistry } from "@json-render/react";
import { myCatalog } from "./catalog";

export const { registry, handlers, executeAction } = defineRegistry(myCatalog, {
components: {
Expand All @@ -33,16 +33,14 @@ export const { registry, handlers, executeAction } = defineRegistry(myCatalog, {
),

Button: ({ props, emit }) => (
<button onClick={() => emit("press")}>
{props.label}
</button>
<button onClick={() => emit("press")}>{props.label}</button>
),
},

actions: {
submit_form: async (params, setState) => {
const res = await fetch('/api/submit', {
method: 'POST',
const res = await fetch("/api/submit", {
method: "POST",
body: JSON.stringify(params),
});
const result = await res.json();
Expand All @@ -69,23 +67,36 @@ Each component receives a `ComponentContext` object:

```typescript
interface ComponentContext {
props: T; // Type-safe props from your catalog
children?: React.ReactNode; // Rendered children (for slot components)
emit: (event: string) => void; // Emit a named event (always defined)
on: (event: string) => EventHandle; // Get event handle with metadata
loading?: boolean; // Whether the renderer is in a loading state
bindings?: Record<string, string>; // State paths from $bindState/$bindItem expressions
props: T; // Type-safe props from your catalog
children?: React.ReactNode; // Rendered children (for slot components)
slots?: Record<string, React.ReactNode>; // Rendered named slots
emit: (event: string) => void; // Emit a named event (always defined)
on: (event: string) => EventHandle; // Get event handle with metadata
loading?: boolean; // Whether the renderer is in a loading state
bindings?: Record<string, string>; // State paths from $bindState/$bindItem expressions
}

interface EventHandle {
emit: () => void; // Fire the event
emit: () => void; // Fire the event
shouldPreventDefault: boolean; // Whether any binding requested preventDefault
bound: boolean; // Whether any handler is bound
bound: boolean; // Whether any handler is bound
}
```

Props are automatically inferred from your catalog, so `props.title` is typed as `string` if your catalog defines it that way.

For components with named slots, read the default content from `children` and other regions from `slots`:

```tsx
Layout: ({ children, slots }) => (
<div>
<header>{slots?.header}</header>
<main>{children}</main>
<footer>{slots?.footer}</footer>
</div>
),
```

Use `emit("press")` for simple event firing. Use `on("click")` when you need to inspect event metadata:

```tsx
Expand Down Expand Up @@ -128,27 +139,29 @@ TextInput: ({ props, bindings }) => {

### Action Handlers

Instead of AI generating arbitrary code, it declares *intent* by name. Your application provides the implementation. This is a core guardrail.
Instead of AI generating arbitrary code, it declares _intent_ by name. Your application provides the implementation. This is a core guardrail.

Actions are declared in your [catalog](/docs/catalog). The `@json-render/react` schema supports an `actions` key where you define what operations AI can trigger:

```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
import { z } from 'zod';
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { z } from "zod";

const catalog = defineCatalog(schema, {
components: { /* ... */ },
components: {
/* ... */
},
actions: {
submit_form: {
params: z.object({
formId: z.string(),
}),
description: 'Submit a form',
description: "Submit a form",
},
export_data: {
params: z.object({
format: z.enum(['csv', 'pdf', 'json']),
format: z.enum(["csv", "pdf", "json"]),
}),
},
navigate: {
Expand All @@ -166,8 +179,8 @@ Action handlers receive `(params, setState, state)` and are defined inside `defi
export const { handlers, executeAction } = defineRegistry(catalog, {
actions: {
submit_form: async (params, setState) => {
const response = await fetch('/api/submit', {
method: 'POST',
const response = await fetch("/api/submit", {
method: "POST",
body: JSON.stringify({ formId: params.formId }),
});
const result = await response.json();
Expand Down Expand Up @@ -219,14 +232,14 @@ For read-only state access (e.g. displaying a value from state), use `$state` ex
Wire everything together with providers and the `<Renderer />` component:

```tsx
import { useMemo, useRef } from 'react';
import { useMemo, useRef } from "react";
import {
Renderer,
StateProvider,
VisibilityProvider,
ActionProvider,
} from '@json-render/react';
import { registry, handlers } from './registry';
} from "@json-render/react";
import { registry, handlers } from "./registry";

function App({ spec, state, setState }) {
const stateRef = useRef(state);
Expand All @@ -235,7 +248,11 @@ function App({ spec, state, setState }) {
setStateRef.current = setState;

const actionHandlers = useMemo(
() => handlers(() => setStateRef.current, () => stateRef.current),
() =>
handlers(
() => setStateRef.current,
() => stateRef.current,
),
[],
);

Expand All @@ -256,8 +273,8 @@ function App({ spec, state, setState }) {
`@json-render/react-native` uses the same `defineRegistry` API. The only difference is that components return React Native elements instead of HTML:

```tsx
import { defineRegistry } from '@json-render/react-native';
import { View, Text, Pressable } from 'react-native';
import { defineRegistry } from "@json-render/react-native";
import { View, Text, Pressable } from "react-native";

export const { registry } = defineRegistry(catalog, {
components: {
Expand All @@ -284,14 +301,14 @@ See the [@json-render/react-native API reference](/docs/api/react-native) for th
`@json-render/react-email` uses `defineRegistry` like React and React Native. Components render to React Email primitives (`@react-email/components`). Use `renderToHtml` or `renderToPlainText` for server-side email output:

```tsx
import { defineRegistry } from '@json-render/react-email';
import { renderToHtml } from '@json-render/react-email';
import { Body, Container, Heading, Text } from '@react-email/components';
import { defineRegistry } from "@json-render/react-email";
import { renderToHtml } from "@json-render/react-email";
import { Body, Container, Heading, Text } from "@react-email/components";

export const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
<Container style={{ padding: 16, backgroundColor: '#fff' }}>
<Container style={{ padding: 16, backgroundColor: "#fff" }}>
<Heading>{props.title}</Heading>
{children}
</Container>
Expand All @@ -309,10 +326,10 @@ See the [@json-render/react-email API reference](/docs/api/react-email) for the
`@json-render/remotion` takes a different approach. Instead of `defineRegistry`, it uses a plain component registry with built-in standard components for video production:

```tsx
import { Renderer, standardComponents } from '@json-render/remotion';
import { Renderer, standardComponents } from "@json-render/remotion";

// Use the standard components directly
<Renderer spec={timelineSpec} components={standardComponents} />
<Renderer spec={timelineSpec} components={standardComponents} />;

// Or extend with your own
const components = {
Expand Down
Loading
Loading