@json-render/react 0.1.0 → 0.4.0

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,316 @@
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
13
+ ### 1. Create a Catalog
14
+
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
+ },
43
+ },
44
+ actions: {
45
+ submit: { description: "Submit the form" },
46
+ cancel: { description: "Cancel and close" },
47
+ },
48
+ });
49
+ ```
50
+
51
+ ### 2. Define Component Implementations
24
52
 
25
53
  ```tsx
26
- import { JSONUIProvider, Renderer, useUIStream } from '@json-render/react';
54
+ import { defineComponents, useData } from "@json-render/react";
27
55
 
28
- // Define your component registry
29
- const registry = {
30
- Card: ({ element, children }) => (
56
+ export const components = defineComponents(catalog, {
57
+ Card: ({ props, children }) => (
31
58
  <div className="card">
32
- <h3>{element.props.title}</h3>
59
+ <h3>{props.title}</h3>
60
+ {props.description && <p>{props.description}</p>}
33
61
  {children}
34
62
  </div>
35
63
  ),
36
- Button: ({ element, onAction }) => (
37
- <button onClick={() => onAction?.(element.props.action)}>
38
- {element.props.label}
64
+ Button: ({ props, onAction }) => (
65
+ <button onClick={() => onAction?.(props.action)}>
66
+ {props.label}
39
67
  </button>
40
68
  ),
41
- };
42
-
43
- // Action handlers
44
- const actionHandlers = {
45
- submit: async (params) => {
46
- await api.submit(params);
47
- },
48
- export: (params) => {
49
- download(params.format);
69
+ Input: ({ props }) => {
70
+ const { get, set } = useData();
71
+ return (
72
+ <label>
73
+ {props.label}
74
+ <input
75
+ placeholder={props.placeholder ?? ""}
76
+ value={get("/form/value") ?? ""}
77
+ onChange={(e) => set("/form/value", e.target.value)}
78
+ />
79
+ </label>
80
+ );
50
81
  },
51
- };
82
+ });
83
+ ```
52
84
 
53
- function App() {
54
- const { tree, isStreaming, send, clear } = useUIStream({
55
- api: '/api/generate',
56
- });
85
+ ### 3. Render Specs
86
+
87
+ ```tsx
88
+ import { Renderer, DataProvider, ActionProvider } from "@json-render/react";
89
+
90
+ function App({ spec }) {
91
+ const handleAction = (action: string) => {
92
+ console.log("Action triggered:", action);
93
+ };
57
94
 
58
95
  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>
96
+ <DataProvider initialData={{ form: { value: "" } }}>
97
+ <ActionProvider onAction={handleAction}>
98
+ <Renderer
99
+ spec={spec}
100
+ catalog={catalog}
101
+ components={components}
102
+ />
103
+ </ActionProvider>
104
+ </DataProvider>
71
105
  );
72
106
  }
73
107
  ```
74
108
 
75
- ### Using Contexts Directly
109
+ ## Spec Format
76
110
 
77
- ```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
-
94
- return (
95
- <input
96
- value={value}
97
- onChange={(e) => set('/user/name', e.target.value)}
98
- />
99
- );
100
- }
111
+ The React renderer uses an element tree format:
101
112
 
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>;
113
+ ```typescript
114
+ interface Spec {
115
+ root: Element;
111
116
  }
112
117
 
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
- );
118
+ interface Element {
119
+ type: string; // Component name from catalog
120
+ props: object; // Component props
121
+ children?: Element[]; // Nested elements
122
+ visible?: VisibilityCondition;
125
123
  }
124
+ ```
126
125
 
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
- );
126
+ Example spec:
127
+
128
+ ```json
129
+ {
130
+ "root": {
131
+ "type": "Card",
132
+ "props": { "title": "Welcome" },
133
+ "children": [
134
+ {
135
+ "type": "Input",
136
+ "props": { "label": "Name", "placeholder": "Enter name" }
137
+ },
138
+ {
139
+ "type": "Button",
140
+ "props": { "label": "Submit", "action": "submit" }
141
+ }
142
+ ]
143
+ }
142
144
  }
143
145
  ```
144
146
 
145
- ### Streaming UI
147
+ ## Contexts
148
+
149
+ ### DataProvider
150
+
151
+ Share data across components with JSON Pointer paths:
152
+
153
+ ```tsx
154
+ <DataProvider initialData={{ user: { name: "John" } }}>
155
+ {children}
156
+ </DataProvider>
157
+
158
+ // In components:
159
+ const { data, get, set } = useData();
160
+ const name = get("/user/name"); // "John"
161
+ set("/user/age", 25);
162
+ ```
163
+
164
+ ### ActionProvider
165
+
166
+ Handle actions from components:
146
167
 
147
168
  ```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
- });
169
+ <ActionProvider
170
+ onAction={(action) => {
171
+ if (action === "submit") handleSubmit();
172
+ if (action === "cancel") handleCancel();
173
+ }}
174
+ >
175
+ {children}
176
+ </ActionProvider>
177
+ ```
162
178
 
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
- );
179
+ ### VisibilityProvider
180
+
181
+ Control element visibility based on data:
182
+
183
+ ```tsx
184
+ <VisibilityProvider>
185
+ {children}
186
+ </VisibilityProvider>
187
+
188
+ // Elements can use visibility conditions:
189
+ {
190
+ "type": "Alert",
191
+ "props": { "message": "Error!" },
192
+ "visible": { "path": "/form/hasError" }
172
193
  }
173
194
  ```
174
195
 
175
- ## API Reference
176
-
177
- ### Providers
196
+ ### ValidationProvider
178
197
 
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
198
+ Add field validation:
184
199
 
185
- ### Hooks
200
+ ```tsx
201
+ <ValidationProvider>
202
+ {children}
203
+ </ValidationProvider>
204
+
205
+ // Use validation hooks:
206
+ const { errors, validate } = useFieldValidation("/form/email", {
207
+ checks: [
208
+ { fn: "required", message: "Email required" },
209
+ { fn: "email", message: "Invalid email" },
210
+ ],
211
+ });
212
+ ```
186
213
 
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
214
+ ## Hooks
196
215
 
197
- ### Components
216
+ | Hook | Purpose |
217
+ |------|---------|
218
+ | `useData()` | Access data context (`data`, `get`, `set`) |
219
+ | `useDataValue(path)` | Get single value from data |
220
+ | `useVisibility()` | Access visibility evaluation |
221
+ | `useIsVisible(condition)` | Check if condition is met |
222
+ | `useActions()` | Access action context |
223
+ | `useFieldValidation(path, config)` | Field validation state |
198
224
 
199
- - `Renderer` - Render a UI tree
200
- - `ConfirmDialog` - Default confirmation dialog
225
+ ## Visibility Conditions
201
226
 
202
- ### Utilities
227
+ ```typescript
228
+ // Simple path check (truthy)
229
+ { "path": "/user/isAdmin" }
230
+
231
+ // Auth state
232
+ { "auth": "signedIn" }
233
+
234
+ // Comparisons
235
+ { "eq": [{ "path": "/status" }, "active"] }
236
+ { "gt": [{ "path": "/count" }, 10] }
237
+
238
+ // Logical operators
239
+ {
240
+ "and": [
241
+ { "path": "/feature/enabled" },
242
+ { "not": { "path": "/maintenance" } }
243
+ ]
244
+ }
203
245
 
204
- - `useUIStream(options)` - Hook for streaming UI generation
205
- - `flatToTree(elements)` - Convert flat list to tree
246
+ {
247
+ "or": [
248
+ { "path": "/user/isAdmin" },
249
+ { "path": "/user/isModerator" }
250
+ ]
251
+ }
252
+ ```
206
253
 
207
254
  ## Component Props
208
255
 
209
- Components in your registry receive these props:
256
+ Components receive these props:
210
257
 
211
258
  ```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
259
+ interface ComponentProps<P> {
260
+ props: P; // Props from spec
261
+ children?: React.ReactNode; // Rendered children
262
+ onAction?: (action: string) => void;
217
263
  }
218
264
  ```
219
265
 
220
- ## Example Component
266
+ ## Generate AI Prompts
267
+
268
+ ```typescript
269
+ const systemPrompt = catalog.prompt();
270
+ // Returns detailed prompt with component/action descriptions
271
+ ```
272
+
273
+ ## Full Example
221
274
 
222
275
  ```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
-
276
+ import { defineCatalog } from "@json-render/core";
277
+ import {
278
+ schema,
279
+ defineComponents,
280
+ Renderer,
281
+ DataProvider,
282
+ ActionProvider,
283
+ } from "@json-render/react";
284
+ import { z } from "zod";
285
+
286
+ const catalog = defineCatalog(schema, {
287
+ components: {
288
+ Greeting: {
289
+ props: z.object({ name: z.string() }),
290
+ description: "Displays a greeting",
291
+ },
292
+ },
293
+ actions: {},
294
+ });
295
+
296
+ const components = defineComponents(catalog, {
297
+ Greeting: ({ props }) => <h1>Hello, {props.name}!</h1>,
298
+ });
299
+
300
+ const spec = {
301
+ root: {
302
+ type: "Greeting",
303
+ props: { name: "World" },
304
+ },
305
+ };
306
+
307
+ function App() {
231
308
  return (
232
- <div className="metric">
233
- <span className="label">{label}</span>
234
- <span className="value">{formatted}</span>
235
- </div>
309
+ <DataProvider initialData={{}}>
310
+ <ActionProvider onAction={() => {}}>
311
+ <Renderer spec={spec} catalog={catalog} components={components} />
312
+ </ActionProvider>
313
+ </DataProvider>
236
314
  );
237
315
  }
238
316
  ```
@@ -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":[]}