Compare commits

..
Author SHA1 Message Date
Chris Tate 7a2ce3e981 changeset 2026-02-06 14:04:53 -06:00
16 changed files with 159 additions and 236 deletions
+1 -4
View File
@@ -14,8 +14,5 @@
"access": "public", "access": "public",
"baseBranch": "main", "baseBranch": "main",
"updateInternalDependencies": "patch", "updateInternalDependencies": "patch",
"privatePackages": { "ignore": []
"version": false,
"tag": false
}
} }
+2
View File
@@ -24,6 +24,8 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
+6 -6
View File
@@ -94,15 +94,15 @@ type Registry = Record<string, React.ComponentType<ComponentRenderProps>>;`}</Co
<h3 className="text-lg font-semibold mt-8 mb-4">useUIStream</h3> <h3 className="text-lg font-semibold mt-8 mb-4">useUIStream</h3>
<Code lang="typescript">{`const { <Code lang="typescript">{`const {
spec, // Spec | null - current UI state spec, // Spec - current UI state
isStreaming, // boolean - true while streaming isStreaming, // boolean - true while streaming
error, // Error | null error, // Error | null
send, // (prompt: string, context?: Record<string, unknown>) => Promise<void> send, // (prompt: string) => void
clear, // () => void - reset spec and error abort, // () => void
} = useUIStream({ } = useUIStream({
api: string, // API endpoint URL api: string, // API endpoint URL
onComplete?: (spec: Spec) => void, // Called when streaming completes onChunk?: (chunk: string) => void, // Called for each chunk
onError?: (error: Error) => void, // Called when an error occurs onFinish?: (spec: Spec) => void, // Called when streaming completes
});`}</Code> });`}</Code>
<h3 className="text-lg font-semibold mt-8 mb-4">useData</h3> <h3 className="text-lg font-semibold mt-8 mb-4">useData</h3>
+112 -139
View File
@@ -14,86 +14,82 @@ export default function RegistryPage() {
life. life.
</p> </p>
{/* defineRegistry */} {/* Components Section */}
<h2 className="text-xl font-semibold mt-12 mb-4">defineRegistry</h2> <h2 className="text-xl font-semibold mt-12 mb-4">Component Registry</h2>
<p className="text-sm text-muted-foreground mb-4"> <p className="text-sm text-muted-foreground mb-4">
Use <code>defineRegistry</code> to create a type-safe registry from your Create a registry that maps catalog component types to React components:
catalog. Pass your components, actions, or both in a single call:
</p> </p>
<Code lang="tsx">{`import { defineRegistry } from '@json-render/react'; <Code lang="tsx">{`import { useAction } from '@json-render/react';
import { myCatalog } from './catalog';
export const { registry, handlers, executeAction } = defineRegistry(myCatalog, { const registry = {
components: { Card: ({ props, children }) => (
Card: ({ props, children }) => ( <div className="card">
<div className="card"> <h2>{props.title}</h2>
<h2>{props.title}</h2> {props.description && <p>{props.description}</p>}
{props.description && <p>{props.description}</p>} {children}
{children} </div>
</div> ),
),
Button: ({ props }) => {
Button: ({ props, onAction }) => ( const executeAction = useAction(props.action);
<button onClick={() => onAction?.({ name: props.action })}> return (
<button onClick={() => executeAction({})}>
{props.label} {props.label}
</button> </button>
), );
}, },
};`}</Code>
actions: {
submit_form: async (params, setData) => {
const res = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(params),
});
const result = await res.json();
setData((prev) => ({ ...prev, formResult: result }));
},
export_data: async (params) => {
const blob = await generateExport(params.format);
downloadBlob(blob, \`export.\${params.format}\`);
},
},
});`}</Code>
<p className="text-sm text-muted-foreground mt-4 mb-4">
The returned object contains:
</p>
<ul className="list-disc list-inside text-sm text-muted-foreground mb-4 space-y-1">
<li>
<code>registry</code> - component registry for{" "}
<code>{"<Renderer />"}</code>
</li>
<li>
<code>handlers</code> - factory for ActionProvider-compatible handlers
</li>
<li>
<code>executeAction</code> - imperative action dispatch (for use
outside the React tree)
</li>
</ul>
{/* Component Props */}
<h2 className="text-xl font-semibold mt-12 mb-4">Component Props</h2> <h2 className="text-xl font-semibold mt-12 mb-4">Component Props</h2>
<p className="text-sm text-muted-foreground mb-4"> <p className="text-sm text-muted-foreground mb-4">
Each component in the registry receives a <code>ComponentContext</code>{" "} Each component receives these props:
object:
</p> </p>
<Code lang="typescript">{`interface ComponentContext { <Code lang="typescript">{`interface ComponentProps<T = Record<string, unknown>> {
props: T; // Type-safe props from your catalog props: T; // Component props from the spec
children?: React.ReactNode; // Rendered children (for slot components) children?: React.ReactNode; // Rendered children (for slot components)
onAction?: (action: ActionTrigger) => void; // Dispatch an action }
loading?: boolean; // Whether the renderer is in a loading state
}`}</Code>
<p className="text-sm text-muted-foreground mt-4 mb-4"> // Type-safe props by extracting from your catalog
Props are automatically inferred from your catalog, so{" "} type CardProps = ComponentProps<{
<code>props.title</code> is typed as <code>string</code> if your catalog title: string;
defines it that way. description: string | null;
}>;
// Use hooks for actions and data within components
import { useAction, useDataValue } from '@json-render/react';`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Using Data Binding</h2>
<p className="text-sm text-muted-foreground mb-4">
Use hooks to read and write data:
</p> </p>
<Code lang="tsx">{`import { useDataValue, useDataBinding } from '@json-render/react';
{/* Action Handlers */} const Metric = ({ props }) => {
// Read-only value from data context
const value = useDataValue(props.valuePath);
return (
<div className="metric">
<span className="label">{props.label}</span>
<span className="value">{formatValue(value)}</span>
</div>
);
};
const TextField = ({ props }) => {
// Two-way binding to data context
const [value, setValue] = useDataBinding(props.valuePath);
return (
<input
value={value || ''}
onChange={(e) => setValue(e.target.value)}
placeholder={props.placeholder}
/>
);
};`}</Code>
{/* Actions Section */}
<h2 className="text-xl font-semibold mt-12 mb-4">Action Handlers</h2> <h2 className="text-xl font-semibold mt-12 mb-4">Action Handlers</h2>
<p className="text-sm text-muted-foreground mb-4"> <p className="text-sm text-muted-foreground mb-4">
Instead of AI generating arbitrary code, it declares <em>intent</em> by Instead of AI generating arbitrary code, it declares <em>intent</em> by
@@ -121,6 +117,9 @@ const catalog = defineCatalog(schema, {
export_data: { export_data: {
params: z.object({ params: z.object({
format: z.enum(['csv', 'pdf', 'json']), format: z.enum(['csv', 'pdf', 'json']),
filters: z.object({
dateRange: z.string().nullable(),
}).nullable(),
}), }),
}, },
navigate: { navigate: {
@@ -131,104 +130,78 @@ const catalog = defineCatalog(schema, {
}, },
});`}</Code> });`}</Code>
<h3 className="text-lg font-medium mt-8 mb-3"> <h3 className="text-lg font-medium mt-8 mb-3">ActionProvider</h3>
Implementing Action Handlers
</h3>
<p className="text-sm text-muted-foreground mb-4"> <p className="text-sm text-muted-foreground mb-4">
Action handlers receive <code>(params, setData, data)</code> and are Provide action handlers to your app:
defined inside <code>defineRegistry</code>:
</p> </p>
<Code lang="tsx">{`export const { handlers, executeAction } = defineRegistry(catalog, { <Code lang="tsx">{`import { ActionProvider } from '@json-render/react';
actions: {
submit_form: async (params, setData) => { function App() {
const handlers = {
submit_form: async (params) => {
const response = await fetch('/api/submit', { const response = await fetch('/api/submit', {
method: 'POST', method: 'POST',
body: JSON.stringify({ formId: params.formId }), body: JSON.stringify({ formId: params.formId }),
}); });
const result = await response.json(); return response.json();
setData((prev) => ({ ...prev, formResult: result }));
}, },
export_data: async (params) => { export_data: async (params) => {
const blob = await generateExport(params.format); const blob = await generateExport(params.format, params.filters);
downloadBlob(blob, \`export.\${params.format}\`); downloadBlob(blob, \`export.\${params.format}\`);
}, },
navigate: (params) => { navigate: (params) => {
window.location.href = params.url; window.location.href = params.url;
}, },
}, };
});`}</Code>
{/* Using Data Binding */}
<h2 className="text-xl font-semibold mt-12 mb-4">Using Data Binding</h2>
<p className="text-sm text-muted-foreground mb-4">
Use hooks inside your registry components to read and write data:
</p>
<Code lang="tsx">{`import { useData } from '@json-render/react';
import { getByPath } from '@json-render/core';
// Inside defineRegistry components:
Metric: ({ props }) => {
const { data } = useData();
const value = getByPath(data, props.valuePath);
return ( return (
<div className="metric"> <ActionProvider handlers={handlers}>
<span className="label">{props.label}</span> {/* Your UI */}
<span className="value">{formatValue(value)}</span> </ActionProvider>
</div>
); );
}, }`}</Code>
TextField: ({ props }) => { <h3 className="text-lg font-medium mt-8 mb-3">
const { data, set } = useData(); Using Actions in Components
const value = getByPath(data, props.valuePath) as string; </h3>
<Code lang="tsx">{`import { useAction } from '@json-render/react';
// Using the useAction hook (recommended)
const Button = ({ props }) => {
const executeAction = useAction(props.action);
return ( return (
<input <button onClick={() => executeAction({})}>
value={value || ''} {props.label}
onChange={(e) => set(props.valuePath, e.target.value)} </button>
placeholder={props.placeholder}
/>
); );
},`}</Code> };
// Or for standalone use
function SubmitButton() {
const submitForm = useAction('submit_form');
return (
<button onClick={() => submitForm({ formId: 'contact' })}>
Submit
</button>
);
}`}</Code>
{/* Renderer Section */} {/* Renderer Section */}
<h2 className="text-xl font-semibold mt-12 mb-4">Using the Renderer</h2> <h2 className="text-xl font-semibold mt-12 mb-4">Using the Renderer</h2>
<p className="text-sm text-muted-foreground mb-4"> <Code lang="tsx">{`import { Renderer, ActionProvider } from '@json-render/react';
Wire everything together with providers and the{" "}
<code>{"<Renderer />"}</code> component:
</p>
<Code lang="tsx">{`import { useMemo, useRef } from 'react';
import {
Renderer,
DataProvider,
VisibilityProvider,
ActionProvider,
} from '@json-render/react';
import { registry, handlers } from './registry';
function App({ spec, data, setData }) {
const dataRef = useRef(data);
const setDataRef = useRef(setData);
dataRef.current = data;
setDataRef.current = setData;
const actionHandlers = useMemo(
() => handlers(() => setDataRef.current, () => dataRef.current),
[],
);
function App() {
return ( return (
<DataProvider initialData={data}> <ActionProvider handlers={actionHandlers}>
<VisibilityProvider> <Renderer
<ActionProvider handlers={actionHandlers}> spec={uiSpec}
<Renderer spec={spec} registry={registry} /> registry={registry}
</ActionProvider> />
</VisibilityProvider> </ActionProvider>
</DataProvider>
); );
}`}</Code> }`}</Code>
+7 -11
View File
@@ -34,11 +34,11 @@ function App() {
isStreaming, // True while streaming isStreaming, // True while streaming
error, // Any error that occurred error, // Any error that occurred
send, // Function to start generation send, // Function to start generation
clear, // Function to reset spec and error abort, // Function to cancel streaming
} = useUIStream({ } = useUIStream({
api: '/api/generate', api: '/api/generate',
onComplete: (spec) => {}, // Optional: called when streaming completes onChunk: (chunk) => {}, // Optional: called for each chunk
onError: (error) => {}, // Optional: called when an error occurs onFinish: (spec) => {}, // Optional: called when complete
}); });
}`}</Code> }`}</Code>
@@ -112,14 +112,8 @@ export async function POST(req: Request) {
}`}</Code> }`}</Code>
<h2 className="text-xl font-semibold mt-12 mb-4">Aborting Streams</h2> <h2 className="text-xl font-semibold mt-12 mb-4">Aborting Streams</h2>
<p className="text-sm text-muted-foreground mb-4">
Calling <code className="text-foreground">send</code> again
automatically aborts the previous request. Use{" "}
<code className="text-foreground">clear</code> to reset the spec and
error state:
</p>
<Code lang="tsx">{`function App() { <Code lang="tsx">{`function App() {
const { isStreaming, send, clear } = useUIStream({ const { isStreaming, send, abort } = useUIStream({
api: '/api/generate', api: '/api/generate',
}); });
@@ -128,7 +122,9 @@ export async function POST(req: Request) {
<button onClick={() => send('Create dashboard')}> <button onClick={() => send('Create dashboard')}>
Generate Generate
</button> </button>
<button onClick={clear}>Reset</button> {isStreaming && (
<button onClick={abort}>Cancel</button>
)}
</div> </div>
); );
}`}</Code> }`}</Code>
+2 -2
View File
@@ -6,8 +6,8 @@
"license": "~~proprietary~~", "license": "~~proprietary~~",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@json-render/core": "workspace:*", "@json-render/core": "^0.4.0",
"@json-render/react": "workspace:*", "@json-render/react": "^0.4.0",
"@stripe/ui-extension-sdk": "^9.1.0", "@stripe/ui-extension-sdk": "^9.1.0",
"stripe": "^13.11.0" "stripe": "^13.11.0"
}, },
-17
View File
@@ -1,17 +0,0 @@
# @json-render/codegen
## 0.4.3
### Patch Changes
- 61ee8e5: include remove op in system prompt
- Updated dependencies [61ee8e5]
- @json-render/core@0.4.3
## 0.4.2
### Patch Changes
- 54bce09: add defineRegistry function
- Updated dependencies [54bce09]
- @json-render/core@0.4.2
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@json-render/codegen", "name": "@json-render/codegen",
"version": "0.4.3", "version": "0.4.0",
"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": [
-13
View File
@@ -1,13 +0,0 @@
# @json-render/core
## 0.4.3
### Patch Changes
- 61ee8e5: include remove op in system prompt
## 0.4.2
### Patch Changes
- 54bce09: add defineRegistry function
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@json-render/core", "name": "@json-render/core",
"version": "0.4.3", "version": "0.4.0",
"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": [
-2
View File
@@ -502,7 +502,6 @@ export function generateSystemPrompt<
lines.push( lines.push(
'{"op":"add","path":"/elements/key","value":{"key":"...","type":"...","props":{...},"children":[...]}}', '{"op":"add","path":"/elements/key","value":{"key":"...","type":"...","props":{...},"children":[...]}}',
); );
lines.push('{"op":"remove","path":"/elements/key"}');
lines.push(""); lines.push("");
// Rules // Rules
@@ -510,7 +509,6 @@ export function generateSystemPrompt<
const baseRules = [ const baseRules = [
"First line sets /root to root element key", "First line sets /root to root element key",
"Add elements with /elements/{key}", "Add elements with /elements/{key}",
"Remove elements with op:remove - also update the parent's children array to exclude the removed key",
"Children array contains string keys, not objects", "Children array contains string keys, not objects",
"Parent first, then children", "Parent first, then children",
"Each element needs: key, type, props", "Each element needs: key, type, props",
-17
View File
@@ -1,17 +0,0 @@
# @json-render/react
## 0.4.3
### Patch Changes
- 61ee8e5: include remove op in system prompt
- Updated dependencies [61ee8e5]
- @json-render/core@0.4.3
## 0.4.2
### Patch Changes
- 54bce09: add defineRegistry function
- Updated dependencies [54bce09]
- @json-render/core@0.4.2
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@json-render/react", "name": "@json-render/react",
"version": "0.4.3", "version": "0.4.0",
"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": [
-17
View File
@@ -1,17 +0,0 @@
# @json-render/remotion
## 0.4.3
### Patch Changes
- 61ee8e5: include remove op in system prompt
- Updated dependencies [61ee8e5]
- @json-render/core@0.4.3
## 0.4.2
### Patch Changes
- 54bce09: add defineRegistry function
- Updated dependencies [54bce09]
- @json-render/core@0.4.2
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@json-render/remotion", "name": "@json-render/remotion",
"version": "0.4.3", "version": "0.4.1",
"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": [
+25 -4
View File
@@ -348,11 +348,11 @@ importers:
examples/stripe-app: examples/stripe-app:
dependencies: dependencies:
'@json-render/core': '@json-render/core':
specifier: workspace:* specifier: ^0.4.0
version: link:../../packages/core version: 0.4.0(zod@4.3.5)
'@json-render/react': '@json-render/react':
specifier: workspace:* specifier: ^0.4.0
version: link:../../packages/react version: 0.4.0(react@18.3.1)(zod@4.3.5)
'@stripe/ui-extension-sdk': '@stripe/ui-extension-sdk':
specifier: ^9.1.0 specifier: ^9.1.0
version: 9.1.0(stripe@13.11.0) version: 9.1.0(stripe@13.11.0)
@@ -1793,6 +1793,16 @@ packages:
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@json-render/core@0.4.0':
resolution: {integrity: sha512-zcmNNetyXoqShG9qfXnipnkaGLn3XC7Kec/t3+C39mXAStg2RX29ciZwsZT+Fzo900LeRHRIGel7L/IHCdktrA==}
peerDependencies:
zod: ^4.0.0
'@json-render/react@0.4.0':
resolution: {integrity: sha512-OAXdWdOrAXHEFzpEF7xV+84D00JEmLMKlt5u0wc+C/P+q4q6TnpAWx2j28PBpDB2mpidkW9VnTUM+SCH9J8Lrw==}
peerDependencies:
react: ^19.0.0
'@manypkg/find-root@1.1.0': '@manypkg/find-root@1.1.0':
resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
@@ -7992,6 +8002,17 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@json-render/core@0.4.0(zod@4.3.5)':
dependencies:
zod: 4.3.5
'@json-render/react@0.4.0(react@18.3.1)(zod@4.3.5)':
dependencies:
'@json-render/core': 0.4.0(zod@4.3.5)
react: 18.3.1
transitivePeerDependencies:
- zod
'@manypkg/find-root@1.1.0': '@manypkg/find-root@1.1.0':
dependencies: dependencies:
'@babel/runtime': 7.28.6 '@babel/runtime': 7.28.6