@json-render/react 0.2.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,238 +1,305 @@
1
1
  # @json-render/react
2
2
 
3
- **Predictable. Guardrailed. Fast.** React renderer for user-prompted dashboards, widgets, apps, and data visualizations.
4
-
5
- ## Features
6
-
7
- - **Visibility Filtering**: Components automatically show/hide based on visibility conditions
8
- - **Action Handling**: Built-in action execution with confirmation dialogs
9
- - **Validation**: Field validation with error display
10
- - **Data Binding**: Two-way data binding between UI and data model
11
- - **Streaming**: Progressive rendering from streamed UI trees
3
+ React renderer for json-render. Turn JSON specs into React components with data binding, visibility, and actions.
12
4
 
13
5
  ## Installation
14
6
 
15
7
  ```bash
16
- npm install @json-render/react @json-render/core
17
- # or
18
- pnpm add @json-render/react @json-render/core
8
+ npm install @json-render/react @json-render/core zod
19
9
  ```
20
10
 
21
11
  ## Quick Start
22
12
 
23
- ### Basic Setup
24
-
25
- ```tsx
26
- import { JSONUIProvider, Renderer, useUIStream } from '@json-render/react';
27
-
28
- // Define your component registry
29
- const registry = {
30
- Card: ({ element, children }) => (
31
- <div className="card">
32
- <h3>{element.props.title}</h3>
33
- {children}
34
- </div>
35
- ),
36
- Button: ({ element, onAction }) => (
37
- <button onClick={() => onAction?.(element.props.action)}>
38
- {element.props.label}
39
- </button>
40
- ),
41
- };
13
+ ### 1. Create a Catalog
42
14
 
43
- // Action handlers
44
- const actionHandlers = {
45
- submit: async (params) => {
46
- await api.submit(params);
15
+ ```typescript
16
+ import { defineCatalog } from "@json-render/core";
17
+ import { schema } from "@json-render/react";
18
+ import { z } from "zod";
19
+
20
+ export const catalog = defineCatalog(schema, {
21
+ components: {
22
+ Card: {
23
+ props: z.object({
24
+ title: z.string(),
25
+ description: z.string().nullable(),
26
+ }),
27
+ description: "A card container",
28
+ },
29
+ Button: {
30
+ props: z.object({
31
+ label: z.string(),
32
+ action: z.string(),
33
+ }),
34
+ description: "A clickable button",
35
+ },
36
+ Input: {
37
+ props: z.object({
38
+ label: z.string(),
39
+ placeholder: z.string().nullable(),
40
+ }),
41
+ description: "Text input field",
42
+ },
47
43
  },
48
- export: (params) => {
49
- download(params.format);
44
+ actions: {
45
+ submit: { description: "Submit the form" },
46
+ cancel: { description: "Cancel and close" },
50
47
  },
51
- };
48
+ });
49
+ ```
52
50
 
53
- function App() {
54
- const { tree, isStreaming, send, clear } = useUIStream({
55
- api: '/api/generate',
56
- });
51
+ ### 2. Define Component Implementations
57
52
 
58
- return (
59
- <JSONUIProvider
60
- registry={registry}
61
- initialData={{ user: { name: 'John' } }}
62
- authState={{ isSignedIn: true }}
63
- actionHandlers={actionHandlers}
64
- >
65
- <input
66
- placeholder="Describe the UI..."
67
- onKeyDown={(e) => e.key === 'Enter' && send(e.target.value)}
68
- />
69
- <Renderer tree={tree} registry={registry} loading={isStreaming} />
70
- </JSONUIProvider>
71
- );
72
- }
53
+ ```tsx
54
+ import { defineRegistry, useData } from "@json-render/react";
55
+ import { catalog } from "./catalog";
56
+
57
+ export const { registry } = defineRegistry(catalog, {
58
+ components: {
59
+ Card: ({ props, children }) => (
60
+ <div className="card">
61
+ <h3>{props.title}</h3>
62
+ {props.description && <p>{props.description}</p>}
63
+ {children}
64
+ </div>
65
+ ),
66
+ Button: ({ props, onAction }) => (
67
+ <button onClick={() => onAction?.({ name: props.action })}>
68
+ {props.label}
69
+ </button>
70
+ ),
71
+ Input: ({ props }) => {
72
+ const { get, set } = useData();
73
+ return (
74
+ <label>
75
+ {props.label}
76
+ <input
77
+ placeholder={props.placeholder ?? ""}
78
+ value={get("/form/value") ?? ""}
79
+ onChange={(e) => set("/form/value", e.target.value)}
80
+ />
81
+ </label>
82
+ );
83
+ },
84
+ },
85
+ });
73
86
  ```
74
87
 
75
- ### Using Contexts Directly
88
+ ### 3. Render Specs
76
89
 
77
90
  ```tsx
78
- import {
79
- DataProvider,
80
- VisibilityProvider,
81
- ActionProvider,
82
- ValidationProvider,
83
- useData,
84
- useVisibility,
85
- useActions,
86
- useFieldValidation,
87
- } from '@json-render/react';
88
-
89
- // Data context
90
- function MyComponent() {
91
- const { data, get, set } = useData();
92
- const value = get('/user/name');
93
-
91
+ import { Renderer, DataProvider, ActionProvider } from "@json-render/react";
92
+ import { registry } from "./registry";
93
+
94
+ function App({ spec }) {
94
95
  return (
95
- <input
96
- value={value}
97
- onChange={(e) => set('/user/name', e.target.value)}
98
- />
96
+ <DataProvider initialData={{ form: { value: "" } }}>
97
+ <ActionProvider handlers={{
98
+ submit: () => console.log("Submit"),
99
+ }}>
100
+ <Renderer spec={spec} registry={registry} />
101
+ </ActionProvider>
102
+ </DataProvider>
99
103
  );
100
104
  }
105
+ ```
101
106
 
102
- // Visibility context
103
- function ConditionalComponent({ visible }) {
104
- const { isVisible } = useVisibility();
105
-
106
- if (!isVisible(visible)) {
107
- return null;
108
- }
109
-
110
- return <div>Visible content</div>;
107
+ ## Spec Format
108
+
109
+ The React renderer uses an element tree format:
110
+
111
+ ```typescript
112
+ interface Spec {
113
+ root: Element;
111
114
  }
112
115
 
113
- // Action context
114
- function ActionButton({ action }) {
115
- const { execute, loadingActions } = useActions();
116
-
117
- return (
118
- <button
119
- onClick={() => execute(action)}
120
- disabled={loadingActions.has(action.name)}
121
- >
122
- {action.name}
123
- </button>
124
- );
116
+ interface Element {
117
+ type: string; // Component name from catalog
118
+ props: object; // Component props
119
+ children?: Element[]; // Nested elements
120
+ visible?: VisibilityCondition;
125
121
  }
122
+ ```
126
123
 
127
- // Validation context
128
- function ValidatedInput({ path, checks }) {
129
- const { errors, validate, touch } = useFieldValidation(path, { checks });
130
- const [value, setValue] = useDataBinding(path);
131
-
132
- return (
133
- <div>
134
- <input
135
- value={value}
136
- onChange={(e) => setValue(e.target.value)}
137
- onBlur={() => { touch(); validate(); }}
138
- />
139
- {errors.map((err) => <span key={err}>{err}</span>)}
140
- </div>
141
- );
124
+ Example spec:
125
+
126
+ ```json
127
+ {
128
+ "root": {
129
+ "type": "Card",
130
+ "props": { "title": "Welcome" },
131
+ "children": [
132
+ {
133
+ "type": "Input",
134
+ "props": { "label": "Name", "placeholder": "Enter name" }
135
+ },
136
+ {
137
+ "type": "Button",
138
+ "props": { "label": "Submit", "action": "submit" }
139
+ }
140
+ ]
141
+ }
142
142
  }
143
143
  ```
144
144
 
145
- ### Streaming UI
145
+ ## Contexts
146
+
147
+ ### DataProvider
148
+
149
+ Share data across components with JSON Pointer paths:
146
150
 
147
151
  ```tsx
148
- import { useUIStream } from '@json-render/react';
149
-
150
- function StreamingDemo() {
151
- const {
152
- tree, // Current UI tree
153
- isStreaming, // Whether currently streaming
154
- error, // Error if any
155
- send, // Send a prompt
156
- clear, // Clear the tree
157
- } = useUIStream({
158
- api: '/api/generate',
159
- onComplete: (tree) => console.log('Done:', tree),
160
- onError: (err) => console.error('Error:', err),
161
- });
152
+ <DataProvider initialData={{ user: { name: "John" } }}>
153
+ {children}
154
+ </DataProvider>
155
+
156
+ // In components:
157
+ const { data, get, set } = useData();
158
+ const name = get("/user/name"); // "John"
159
+ set("/user/age", 25);
160
+ ```
162
161
 
163
- return (
164
- <div>
165
- <button onClick={() => send('Create a dashboard')}>
166
- Generate
167
- </button>
168
- {isStreaming && <span>Generating...</span>}
169
- {tree && <Renderer tree={tree} registry={registry} />}
170
- </div>
171
- );
172
- }
162
+ ### ActionProvider
163
+
164
+ Handle actions from components:
165
+
166
+ ```tsx
167
+ <ActionProvider
168
+ onAction={(action) => {
169
+ if (action === "submit") handleSubmit();
170
+ if (action === "cancel") handleCancel();
171
+ }}
172
+ >
173
+ {children}
174
+ </ActionProvider>
173
175
  ```
174
176
 
175
- ## API Reference
177
+ ### VisibilityProvider
176
178
 
177
- ### Providers
179
+ Control element visibility based on data:
180
+
181
+ ```tsx
182
+ <VisibilityProvider>
183
+ {children}
184
+ </VisibilityProvider>
185
+
186
+ // Elements can use visibility conditions:
187
+ {
188
+ "type": "Alert",
189
+ "props": { "message": "Error!" },
190
+ "visible": { "path": "/form/hasError" }
191
+ }
192
+ ```
178
193
 
179
- - `JSONUIProvider` - Combined provider for all contexts
180
- - `DataProvider` - Data model context
181
- - `VisibilityProvider` - Visibility evaluation context
182
- - `ActionProvider` - Action execution context
183
- - `ValidationProvider` - Validation context
194
+ ### ValidationProvider
184
195
 
185
- ### Hooks
196
+ Add field validation:
186
197
 
187
- - `useData()` - Access data model
188
- - `useDataValue(path)` - Get a single value
189
- - `useDataBinding(path)` - Two-way binding like useState
190
- - `useVisibility()` - Access visibility evaluation
191
- - `useIsVisible(condition)` - Check if condition is visible
192
- - `useActions()` - Access action execution
193
- - `useAction(action)` - Execute a specific action
194
- - `useValidation()` - Access validation context
195
- - `useFieldValidation(path, config)` - Field-level validation
198
+ ```tsx
199
+ <ValidationProvider>
200
+ {children}
201
+ </ValidationProvider>
202
+
203
+ // Use validation hooks:
204
+ const { errors, validate } = useFieldValidation("/form/email", {
205
+ checks: [
206
+ { fn: "required", message: "Email required" },
207
+ { fn: "email", message: "Invalid email" },
208
+ ],
209
+ });
210
+ ```
196
211
 
197
- ### Components
212
+ ## Hooks
198
213
 
199
- - `Renderer` - Render a UI tree
200
- - `ConfirmDialog` - Default confirmation dialog
214
+ | Hook | Purpose |
215
+ |------|---------|
216
+ | `useData()` | Access data context (`data`, `get`, `set`) |
217
+ | `useDataValue(path)` | Get single value from data |
218
+ | `useVisibility()` | Access visibility evaluation |
219
+ | `useIsVisible(condition)` | Check if condition is met |
220
+ | `useActions()` | Access action context |
221
+ | `useFieldValidation(path, config)` | Field validation state |
201
222
 
202
- ### Utilities
223
+ ## Visibility Conditions
203
224
 
204
- - `useUIStream(options)` - Hook for streaming UI generation
205
- - `flatToTree(elements)` - Convert flat list to tree
225
+ ```typescript
226
+ // Simple path check (truthy)
227
+ { "path": "/user/isAdmin" }
228
+
229
+ // Auth state
230
+ { "auth": "signedIn" }
231
+
232
+ // Comparisons
233
+ { "eq": [{ "path": "/status" }, "active"] }
234
+ { "gt": [{ "path": "/count" }, 10] }
235
+
236
+ // Logical operators
237
+ {
238
+ "and": [
239
+ { "path": "/feature/enabled" },
240
+ { "not": { "path": "/maintenance" } }
241
+ ]
242
+ }
243
+
244
+ {
245
+ "or": [
246
+ { "path": "/user/isAdmin" },
247
+ { "path": "/user/isModerator" }
248
+ ]
249
+ }
250
+ ```
206
251
 
207
252
  ## Component Props
208
253
 
209
- Components in your registry receive these props:
254
+ When using `defineRegistry`, components receive these props:
210
255
 
211
256
  ```typescript
212
- interface ComponentRenderProps<P = Record<string, unknown>> {
213
- element: UIElement<string, P>; // The element definition
214
- children?: ReactNode; // Rendered children
215
- onAction?: (action: Action) => void; // Action callback
216
- loading?: boolean; // Streaming in progress
257
+ interface ComponentContext<P> {
258
+ props: P; // Typed props from the catalog
259
+ children?: React.ReactNode; // Rendered children
260
+ onAction?: (action: { name: string; params?: Record<string, unknown> }) => void;
261
+ loading?: boolean; // Whether the parent is loading
217
262
  }
218
263
  ```
219
264
 
220
- ## Example Component
265
+ ## Generate AI Prompts
266
+
267
+ ```typescript
268
+ const systemPrompt = catalog.prompt();
269
+ // Returns detailed prompt with component/action descriptions
270
+ ```
271
+
272
+ ## Full Example
221
273
 
222
274
  ```tsx
223
- function MetricComponent({ element }: ComponentRenderProps) {
224
- const { label, valuePath, format } = element.props;
225
- const value = useDataValue(valuePath);
226
-
227
- const formatted = format === 'currency'
228
- ? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value)
229
- : String(value);
230
-
231
- return (
232
- <div className="metric">
233
- <span className="label">{label}</span>
234
- <span className="value">{formatted}</span>
235
- </div>
236
- );
275
+ import { defineCatalog } from "@json-render/core";
276
+ import { schema, defineRegistry, Renderer } from "@json-render/react";
277
+ import { z } from "zod";
278
+
279
+ const catalog = defineCatalog(schema, {
280
+ components: {
281
+ Greeting: {
282
+ props: z.object({ name: z.string() }),
283
+ description: "Displays a greeting",
284
+ },
285
+ },
286
+ actions: {},
287
+ });
288
+
289
+ const { registry } = defineRegistry(catalog, {
290
+ components: {
291
+ Greeting: ({ props }) => <h1>Hello, {props.name}!</h1>,
292
+ },
293
+ });
294
+
295
+ const spec = {
296
+ root: {
297
+ type: "Greeting",
298
+ props: { name: "World" },
299
+ },
300
+ };
301
+
302
+ function App() {
303
+ return <Renderer spec={spec} registry={registry} />;
237
304
  }
238
305
  ```
@@ -0,0 +1,52 @@
1
+ // src/schema.ts
2
+ import { defineSchema } from "@json-render/core";
3
+ var schema = defineSchema((s) => ({
4
+ // What the AI-generated SPEC looks like
5
+ spec: s.object({
6
+ /** Root element key */
7
+ root: s.string(),
8
+ /** Flat map of elements by key */
9
+ elements: s.record(
10
+ s.object({
11
+ /** Unique key for this element */
12
+ key: s.string(),
13
+ /** Component type from catalog */
14
+ type: s.ref("catalog.components"),
15
+ /** Component props */
16
+ props: s.propsOf("catalog.components"),
17
+ /** Child element keys (flat reference) */
18
+ children: s.array(s.string()),
19
+ /** Parent element key (null for root) */
20
+ parentKey: s.string(),
21
+ /** Visibility condition */
22
+ visible: s.any()
23
+ })
24
+ )
25
+ }),
26
+ // What the CATALOG must provide
27
+ catalog: s.object({
28
+ /** Component definitions */
29
+ components: s.map({
30
+ /** Zod schema for component props */
31
+ props: s.zod(),
32
+ /** Slots for this component. Use ['default'] for children, or named slots like ['header', 'footer'] */
33
+ slots: s.array(s.string()),
34
+ /** Description for AI generation hints */
35
+ description: s.string()
36
+ }),
37
+ /** Action definitions (optional) */
38
+ actions: s.map({
39
+ /** Zod schema for action params */
40
+ params: s.zod(),
41
+ /** Description for AI generation hints */
42
+ description: s.string()
43
+ })
44
+ })
45
+ }));
46
+ var elementTreeSchema = schema;
47
+
48
+ export {
49
+ schema,
50
+ elementTreeSchema
51
+ };
52
+ //# sourceMappingURL=chunk-IGPI5WNB.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema.ts"],"sourcesContent":["import { defineSchema } from \"@json-render/core\";\n\n/**\n * The schema for @json-render/react\n *\n * Defines:\n * - Spec: A flat tree of elements with keys, types, props, and children references\n * - Catalog: Components with props schemas, and optional actions\n */\nexport const schema = defineSchema((s) => ({\n // What the AI-generated SPEC looks like\n spec: s.object({\n /** Root element key */\n root: s.string(),\n /** Flat map of elements by key */\n elements: s.record(\n s.object({\n /** Unique key for this element */\n key: s.string(),\n /** Component type from catalog */\n type: s.ref(\"catalog.components\"),\n /** Component props */\n props: s.propsOf(\"catalog.components\"),\n /** Child element keys (flat reference) */\n children: s.array(s.string()),\n /** Parent element key (null for root) */\n parentKey: s.string(),\n /** Visibility condition */\n visible: s.any(),\n }),\n ),\n }),\n\n // What the CATALOG must provide\n catalog: s.object({\n /** Component definitions */\n components: s.map({\n /** Zod schema for component props */\n props: s.zod(),\n /** Slots for this component. Use ['default'] for children, or named slots like ['header', 'footer'] */\n slots: s.array(s.string()),\n /** Description for AI generation hints */\n description: s.string(),\n }),\n /** Action definitions (optional) */\n actions: s.map({\n /** Zod schema for action params */\n params: s.zod(),\n /** Description for AI generation hints */\n description: s.string(),\n }),\n }),\n}));\n\n/**\n * Type for the React schema\n */\nexport type ReactSchema = typeof schema;\n\n/**\n * Infer the spec type from a catalog\n */\nexport type ReactSpec<TCatalog> = typeof schema extends {\n createCatalog: (catalog: TCatalog) => { _specType: infer S };\n}\n ? S\n : never;\n\n// Backward compatibility aliases\n/** @deprecated Use `schema` instead */\nexport const elementTreeSchema = schema;\n/** @deprecated Use `ReactSchema` instead */\nexport type ElementTreeSchema = ReactSchema;\n/** @deprecated Use `ReactSpec` instead */\nexport type ElementTreeSpec<T> = ReactSpec<T>;\n"],"mappings":";AAAA,SAAS,oBAAoB;AAStB,IAAM,SAAS,aAAa,CAAC,OAAO;AAAA;AAAA,EAEzC,MAAM,EAAE,OAAO;AAAA;AAAA,IAEb,MAAM,EAAE,OAAO;AAAA;AAAA,IAEf,UAAU,EAAE;AAAA,MACV,EAAE,OAAO;AAAA;AAAA,QAEP,KAAK,EAAE,OAAO;AAAA;AAAA,QAEd,MAAM,EAAE,IAAI,oBAAoB;AAAA;AAAA,QAEhC,OAAO,EAAE,QAAQ,oBAAoB;AAAA;AAAA,QAErC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC;AAAA;AAAA,QAE5B,WAAW,EAAE,OAAO;AAAA;AAAA,QAEpB,SAAS,EAAE,IAAI;AAAA,MACjB,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AAAA;AAAA,EAGD,SAAS,EAAE,OAAO;AAAA;AAAA,IAEhB,YAAY,EAAE,IAAI;AAAA;AAAA,MAEhB,OAAO,EAAE,IAAI;AAAA;AAAA,MAEb,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC;AAAA;AAAA,MAEzB,aAAa,EAAE,OAAO;AAAA,IACxB,CAAC;AAAA;AAAA,IAED,SAAS,EAAE,IAAI;AAAA;AAAA,MAEb,QAAQ,EAAE,IAAI;AAAA;AAAA,MAEd,aAAa,EAAE,OAAO;AAAA,IACxB,CAAC;AAAA,EACH,CAAC;AACH,EAAE;AAkBK,IAAM,oBAAoB;","names":[]}