mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 12:28:17 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a2ce3e981 |
@@ -14,8 +14,5 @@
|
|||||||
"access": "public",
|
"access": "public",
|
||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
"updateInternalDependencies": "patch",
|
"updateInternalDependencies": "patch",
|
||||||
"privatePackages": {
|
"ignore": []
|
||||||
"version": false,
|
|
||||||
"tag": false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -273,8 +273,8 @@ compiler.reset();`}</Code>
|
|||||||
</p>
|
</p>
|
||||||
<Code lang="typescript">{`import { compileSpecStream } from '@json-render/core';
|
<Code lang="typescript">{`import { compileSpecStream } from '@json-render/core';
|
||||||
|
|
||||||
const jsonl = \`{"op":"add","path":"/root","value":{}}
|
const jsonl = \`{"op":"set","path":"/root","value":{}}
|
||||||
{"op":"add","path":"/root/type","value":"Card"}\`;
|
{"op":"set","path":"/root/type","value":"Card"}\`;
|
||||||
|
|
||||||
const spec = compileSpecStream<MySpec>(jsonl);`}</Code>
|
const spec = compileSpecStream<MySpec>(jsonl);`}</Code>
|
||||||
|
|
||||||
@@ -285,36 +285,22 @@ const spec = compileSpecStream<MySpec>(jsonl);`}</Code>
|
|||||||
} from '@json-render/core';
|
} from '@json-render/core';
|
||||||
|
|
||||||
// Parse a single line
|
// Parse a single line
|
||||||
const patch = parseSpecStreamLine('{"op":"add","path":"/root","value":{}}');
|
const patch = parseSpecStreamLine('{"op":"set","path":"/root","value":{}}');
|
||||||
|
|
||||||
// Apply patch to object (mutates in place)
|
// Apply patch to object (mutates in place)
|
||||||
const obj = {};
|
const obj = {};
|
||||||
applySpecStreamPatch(obj, patch);`}</Code>
|
applySpecStreamPatch(obj, patch);`}</Code>
|
||||||
|
|
||||||
<h3 className="text-lg font-semibold mt-8 mb-4">SpecStream Types</h3>
|
<h3 className="text-lg font-semibold mt-8 mb-4">SpecStream Types</h3>
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
|
||||||
Fully compliant with{" "}
|
|
||||||
<a
|
|
||||||
href="https://datatracker.ietf.org/doc/html/rfc6902"
|
|
||||||
className="underline"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
RFC 6902
|
|
||||||
</a>
|
|
||||||
:
|
|
||||||
</p>
|
|
||||||
<Code lang="typescript">{`interface SpecStreamLine {
|
<Code lang="typescript">{`interface SpecStreamLine {
|
||||||
op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test';
|
op: 'set' | 'add' | 'replace' | 'remove';
|
||||||
path: string;
|
path: string;
|
||||||
value?: unknown; // Required for add, replace, test
|
value?: unknown;
|
||||||
from?: string; // Required for move, copy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpecStreamCompiler<T> {
|
interface SpecStreamCompiler<T> {
|
||||||
push(chunk: string): { result: T; newPatches: SpecStreamLine[] };
|
push(chunk: string): { result: T; newPatches: SpecStreamLine[] };
|
||||||
getResult(): T;
|
getResult(): T;
|
||||||
getPatches(): SpecStreamLine[];
|
|
||||||
reset(): void;
|
reset(): void;
|
||||||
}`}</Code>
|
}`}</Code>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -275,9 +275,11 @@ export const openapiCatalog = createCatalog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SpecElement {
|
interface SpecElement {
|
||||||
|
key: string;
|
||||||
type: string;
|
type: string;
|
||||||
props: Record<string, unknown>;
|
props: Record<string, unknown>;
|
||||||
children: string[];
|
children: string[];
|
||||||
|
parentKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function schemaToSpec(
|
function schemaToSpec(
|
||||||
@@ -293,6 +295,7 @@ function schemaToSpec(
|
|||||||
|
|
||||||
if (schema.enum) {
|
if (schema.enum) {
|
||||||
elements.set(key, {
|
elements.set(key, {
|
||||||
|
key,
|
||||||
type: 'EnumField',
|
type: 'EnumField',
|
||||||
props: {
|
props: {
|
||||||
name,
|
name,
|
||||||
@@ -303,9 +306,11 @@ function schemaToSpec(
|
|||||||
defaultValue: schema.default as string,
|
defaultValue: schema.default as string,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey,
|
||||||
});
|
});
|
||||||
} else if (schema.type === 'string') {
|
} else if (schema.type === 'string') {
|
||||||
elements.set(key, {
|
elements.set(key, {
|
||||||
|
key,
|
||||||
type: 'StringField',
|
type: 'StringField',
|
||||||
props: {
|
props: {
|
||||||
name,
|
name,
|
||||||
@@ -318,9 +323,11 @@ function schemaToSpec(
|
|||||||
defaultValue: schema.default as string,
|
defaultValue: schema.default as string,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey,
|
||||||
});
|
});
|
||||||
} else if (schema.type === 'integer' || schema.type === 'number') {
|
} else if (schema.type === 'integer' || schema.type === 'number') {
|
||||||
elements.set(key, {
|
elements.set(key, {
|
||||||
|
key,
|
||||||
type: 'NumberField',
|
type: 'NumberField',
|
||||||
props: {
|
props: {
|
||||||
name,
|
name,
|
||||||
@@ -333,9 +340,11 @@ function schemaToSpec(
|
|||||||
defaultValue: schema.default as number,
|
defaultValue: schema.default as number,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey,
|
||||||
});
|
});
|
||||||
} else if (schema.type === 'boolean') {
|
} else if (schema.type === 'boolean') {
|
||||||
elements.set(key, {
|
elements.set(key, {
|
||||||
|
key,
|
||||||
type: 'BooleanField',
|
type: 'BooleanField',
|
||||||
props: {
|
props: {
|
||||||
name,
|
name,
|
||||||
@@ -344,6 +353,7 @@ function schemaToSpec(
|
|||||||
defaultValue: schema.default as boolean,
|
defaultValue: schema.default as boolean,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey,
|
||||||
});
|
});
|
||||||
} else if (schema.type === 'array' && schema.items) {
|
} else if (schema.type === 'array' && schema.items) {
|
||||||
const childKeys: string[] = [];
|
const childKeys: string[] = [];
|
||||||
@@ -351,6 +361,7 @@ function schemaToSpec(
|
|||||||
childKeys.push(itemKey);
|
childKeys.push(itemKey);
|
||||||
|
|
||||||
elements.set(key, {
|
elements.set(key, {
|
||||||
|
key,
|
||||||
type: 'ArrayField',
|
type: 'ArrayField',
|
||||||
props: {
|
props: {
|
||||||
name,
|
name,
|
||||||
@@ -358,6 +369,7 @@ function schemaToSpec(
|
|||||||
description: schema.description,
|
description: schema.description,
|
||||||
},
|
},
|
||||||
children: childKeys,
|
children: childKeys,
|
||||||
|
parentKey,
|
||||||
});
|
});
|
||||||
} else if (schema.type === 'object' && schema.properties) {
|
} else if (schema.type === 'object' && schema.properties) {
|
||||||
const childKeys: string[] = [];
|
const childKeys: string[] = [];
|
||||||
@@ -374,6 +386,7 @@ function schemaToSpec(
|
|||||||
}
|
}
|
||||||
|
|
||||||
elements.set(key, {
|
elements.set(key, {
|
||||||
|
key,
|
||||||
type: 'ObjectField',
|
type: 'ObjectField',
|
||||||
props: {
|
props: {
|
||||||
name,
|
name,
|
||||||
@@ -381,6 +394,7 @@ function schemaToSpec(
|
|||||||
description: schema.description,
|
description: schema.description,
|
||||||
},
|
},
|
||||||
children: childKeys,
|
children: childKeys,
|
||||||
|
parentKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,6 +428,7 @@ export function operationToSpec(
|
|||||||
}
|
}
|
||||||
|
|
||||||
elements.set(rootKey, {
|
elements.set(rootKey, {
|
||||||
|
key: rootKey,
|
||||||
type: 'Form',
|
type: 'Form',
|
||||||
props: {
|
props: {
|
||||||
operationId,
|
operationId,
|
||||||
@@ -423,6 +438,7 @@ export function operationToSpec(
|
|||||||
description,
|
description,
|
||||||
},
|
},
|
||||||
children: childKeys,
|
children: childKeys,
|
||||||
|
parentKey: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -98,19 +98,25 @@ export default function SchemasPage() {
|
|||||||
"root": "card-1",
|
"root": "card-1",
|
||||||
"elements": {
|
"elements": {
|
||||||
"card-1": {
|
"card-1": {
|
||||||
|
"key": "card-1",
|
||||||
"type": "Card",
|
"type": "Card",
|
||||||
"props": { "title": "Dashboard" },
|
"props": { "title": "Dashboard" },
|
||||||
"children": ["text-1", "button-1"]
|
"children": ["text-1", "button-1"],
|
||||||
|
"parentKey": ""
|
||||||
},
|
},
|
||||||
"text-1": {
|
"text-1": {
|
||||||
|
"key": "text-1",
|
||||||
"type": "Text",
|
"type": "Text",
|
||||||
"props": { "content": "Welcome, $data.user.name" },
|
"props": { "content": "Welcome, $data.user.name" },
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "card-1"
|
||||||
},
|
},
|
||||||
"button-1": {
|
"button-1": {
|
||||||
|
"key": "button-1",
|
||||||
"type": "Button",
|
"type": "Button",
|
||||||
"props": { "label": "Click me" },
|
"props": { "label": "Click me" },
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "card-1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`}</Code>
|
}`}</Code>
|
||||||
@@ -123,9 +129,11 @@ export default function SchemasPage() {
|
|||||||
structure:
|
structure:
|
||||||
</p>
|
</p>
|
||||||
<Code lang="typescript">{`interface Element {
|
<Code lang="typescript">{`interface Element {
|
||||||
|
key: string; // Unique identifier
|
||||||
type: string; // Component type from catalog
|
type: string; // Component type from catalog
|
||||||
props: Record<string, any>; // Component properties
|
props: Record<string, any>; // Component properties
|
||||||
children: string[]; // Array of child element keys
|
children: string[]; // Array of child element keys
|
||||||
|
parentKey: string; // Parent element key (empty for root)
|
||||||
visible?: VisibilityRule; // Conditional display
|
visible?: VisibilityRule; // Conditional display
|
||||||
}`}</Code>
|
}`}</Code>
|
||||||
|
|
||||||
@@ -135,12 +143,14 @@ export default function SchemasPage() {
|
|||||||
<code className="text-foreground">$data</code> prefix in props:
|
<code className="text-foreground">$data</code> prefix in props:
|
||||||
</p>
|
</p>
|
||||||
<Code lang="json">{`{
|
<Code lang="json">{`{
|
||||||
|
"key": "greeting",
|
||||||
"type": "Text",
|
"type": "Text",
|
||||||
"props": {
|
"props": {
|
||||||
"content": "$data.user.name",
|
"content": "$data.user.name",
|
||||||
"count": "$data.items.length"
|
"count": "$data.items.length"
|
||||||
},
|
},
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "card-1"
|
||||||
}`}</Code>
|
}`}</Code>
|
||||||
|
|
||||||
<h3 className="text-lg font-medium mt-8 mb-3">Action Format</h3>
|
<h3 className="text-lg font-medium mt-8 mb-3">Action Format</h3>
|
||||||
|
|||||||
@@ -50,14 +50,18 @@ export default function SpecsPage() {
|
|||||||
"root": "card-1",
|
"root": "card-1",
|
||||||
"elements": {
|
"elements": {
|
||||||
"card-1": {
|
"card-1": {
|
||||||
|
"key": "card-1",
|
||||||
"type": "Card",
|
"type": "Card",
|
||||||
"props": { "title": "Welcome" },
|
"props": { "title": "Welcome" },
|
||||||
"children": ["text-1"]
|
"children": ["text-1"],
|
||||||
|
"parentKey": ""
|
||||||
},
|
},
|
||||||
"text-1": {
|
"text-1": {
|
||||||
|
"key": "text-1",
|
||||||
"type": "Text",
|
"type": "Text",
|
||||||
"props": { "content": "Hello, $data.user.name!" },
|
"props": { "content": "Hello, $data.user.name!" },
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "card-1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`}</Code>
|
}`}</Code>
|
||||||
@@ -70,39 +74,53 @@ export default function SpecsPage() {
|
|||||||
"root": "card-1",
|
"root": "card-1",
|
||||||
"elements": {
|
"elements": {
|
||||||
"card-1": {
|
"card-1": {
|
||||||
|
"key": "card-1",
|
||||||
"type": "Card",
|
"type": "Card",
|
||||||
"props": { "title": "User Profile", "padding": "md" },
|
"props": { "title": "User Profile", "padding": "md" },
|
||||||
"children": ["row-1", "button-1"]
|
"children": ["row-1", "button-1"],
|
||||||
|
"parentKey": ""
|
||||||
},
|
},
|
||||||
"row-1": {
|
"row-1": {
|
||||||
|
"key": "row-1",
|
||||||
"type": "Row",
|
"type": "Row",
|
||||||
"props": { "gap": "md" },
|
"props": { "gap": "md" },
|
||||||
"children": ["avatar-1", "stack-1"]
|
"children": ["avatar-1", "stack-1"],
|
||||||
|
"parentKey": "card-1"
|
||||||
},
|
},
|
||||||
"avatar-1": {
|
"avatar-1": {
|
||||||
|
"key": "avatar-1",
|
||||||
"type": "Avatar",
|
"type": "Avatar",
|
||||||
"props": { "src": "$data.user.avatar", "alt": "$data.user.name" },
|
"props": { "src": "$data.user.avatar", "alt": "$data.user.name" },
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "row-1"
|
||||||
},
|
},
|
||||||
"stack-1": {
|
"stack-1": {
|
||||||
|
"key": "stack-1",
|
||||||
"type": "Stack",
|
"type": "Stack",
|
||||||
"props": { "gap": "sm" },
|
"props": { "gap": "sm" },
|
||||||
"children": ["name-text", "email-text"]
|
"children": ["name-text", "email-text"],
|
||||||
|
"parentKey": "row-1"
|
||||||
},
|
},
|
||||||
"name-text": {
|
"name-text": {
|
||||||
|
"key": "name-text",
|
||||||
"type": "Text",
|
"type": "Text",
|
||||||
"props": { "content": "$data.user.name", "variant": "heading" },
|
"props": { "content": "$data.user.name", "variant": "heading" },
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "stack-1"
|
||||||
},
|
},
|
||||||
"email-text": {
|
"email-text": {
|
||||||
|
"key": "email-text",
|
||||||
"type": "Text",
|
"type": "Text",
|
||||||
"props": { "content": "$data.user.email", "variant": "caption" },
|
"props": { "content": "$data.user.email", "variant": "caption" },
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "stack-1"
|
||||||
},
|
},
|
||||||
"button-1": {
|
"button-1": {
|
||||||
|
"key": "button-1",
|
||||||
"type": "Button",
|
"type": "Button",
|
||||||
"props": { "label": "Edit Profile" },
|
"props": { "label": "Edit Profile" },
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "card-1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`}</Code>
|
}`}</Code>
|
||||||
@@ -115,16 +133,21 @@ export default function SpecsPage() {
|
|||||||
"root": "page",
|
"root": "page",
|
||||||
"elements": {
|
"elements": {
|
||||||
"page": {
|
"page": {
|
||||||
|
"key": "page",
|
||||||
"type": "Page",
|
"type": "Page",
|
||||||
"props": {},
|
"props": {},
|
||||||
"children": ["header", "hero", "features", "footer"]
|
"children": ["header", "hero", "features", "footer"],
|
||||||
|
"parentKey": ""
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
|
"key": "header",
|
||||||
"type": "Header",
|
"type": "Header",
|
||||||
"props": { "logo": "/logo.svg", "navItems": ["Products", "Pricing", "Docs"] },
|
"props": { "logo": "/logo.svg", "navItems": ["Products", "Pricing", "Docs"] },
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "page"
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
|
"key": "hero",
|
||||||
"type": "Hero",
|
"type": "Hero",
|
||||||
"props": {
|
"props": {
|
||||||
"title": "Build UIs with JSON",
|
"title": "Build UIs with JSON",
|
||||||
@@ -132,32 +155,43 @@ export default function SpecsPage() {
|
|||||||
"ctaLabel": "Get Started",
|
"ctaLabel": "Get Started",
|
||||||
"ctaHref": "/docs"
|
"ctaHref": "/docs"
|
||||||
},
|
},
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "page"
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
|
"key": "features",
|
||||||
"type": "Features",
|
"type": "Features",
|
||||||
"props": { "columns": 3 },
|
"props": { "columns": 3 },
|
||||||
"children": ["feature-1", "feature-2", "feature-3"]
|
"children": ["feature-1", "feature-2", "feature-3"],
|
||||||
|
"parentKey": "page"
|
||||||
},
|
},
|
||||||
"feature-1": {
|
"feature-1": {
|
||||||
|
"key": "feature-1",
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"props": { "icon": "zap", "title": "Fast", "description": "Render UIs in milliseconds" },
|
"props": { "icon": "zap", "title": "Fast", "description": "Render UIs in milliseconds" },
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "features"
|
||||||
},
|
},
|
||||||
"feature-2": {
|
"feature-2": {
|
||||||
|
"key": "feature-2",
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"props": { "icon": "shield", "title": "Secure", "description": "Validate all specs against your catalog" },
|
"props": { "icon": "shield", "title": "Secure", "description": "Validate all specs against your catalog" },
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "features"
|
||||||
},
|
},
|
||||||
"feature-3": {
|
"feature-3": {
|
||||||
|
"key": "feature-3",
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"props": { "icon": "sparkles", "title": "AI-Ready", "description": "Generate prompts from your catalog" },
|
"props": { "icon": "sparkles", "title": "AI-Ready", "description": "Generate prompts from your catalog" },
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "features"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
|
"key": "footer",
|
||||||
"type": "Footer",
|
"type": "Footer",
|
||||||
"props": { "copyright": "2025 Acme Inc", "links": ["Privacy", "Terms", "Contact"] },
|
"props": { "copyright": "2025 Acme Inc", "links": ["Privacy", "Terms", "Contact"] },
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "page"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`}</Code>
|
}`}</Code>
|
||||||
@@ -175,9 +209,11 @@ export default function SpecsPage() {
|
|||||||
"root": "card-1",
|
"root": "card-1",
|
||||||
"elements": {
|
"elements": {
|
||||||
"card-1": {
|
"card-1": {
|
||||||
|
"key": "card-1",
|
||||||
"type": "Card",
|
"type": "Card",
|
||||||
"props": { "title": "My Card" },
|
"props": { "title": "My Card" },
|
||||||
"children": ["text-1"]
|
"children": ["text-1"],
|
||||||
|
"parentKey": ""
|
||||||
},
|
},
|
||||||
"text-1": { ... }
|
"text-1": { ... }
|
||||||
}
|
}
|
||||||
@@ -188,11 +224,17 @@ export default function SpecsPage() {
|
|||||||
Each element in the map has a consistent shape:
|
Each element in the map has a consistent shape:
|
||||||
</p>
|
</p>
|
||||||
<Code lang="json">{`{
|
<Code lang="json">{`{
|
||||||
|
"key": "unique-id",
|
||||||
"type": "ComponentName",
|
"type": "ComponentName",
|
||||||
"props": { "label": "Hello" },
|
"props": { "label": "Hello" },
|
||||||
"children": ["child-1", "child-2"]
|
"children": ["child-1", "child-2"],
|
||||||
|
"parentKey": "parent-id"
|
||||||
}`}</Code>
|
}`}</Code>
|
||||||
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1 mt-3 mb-4">
|
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1 mt-3 mb-4">
|
||||||
|
<li>
|
||||||
|
<code className="text-foreground">key</code> — Unique identifier for
|
||||||
|
this element
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<code className="text-foreground">type</code> — Component type from
|
<code className="text-foreground">type</code> — Component type from
|
||||||
your catalog
|
your catalog
|
||||||
@@ -204,6 +246,10 @@ export default function SpecsPage() {
|
|||||||
<code className="text-foreground">children</code> — Array of child
|
<code className="text-foreground">children</code> — Array of child
|
||||||
element keys
|
element keys
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<code className="text-foreground">parentKey</code> — Key of parent
|
||||||
|
element (empty string for root)
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 className="text-lg font-medium mt-8 mb-3">Dynamic Data</h3>
|
<h3 className="text-lg font-medium mt-8 mb-3">Dynamic Data</h3>
|
||||||
@@ -212,13 +258,15 @@ export default function SpecsPage() {
|
|||||||
<code className="text-foreground">$data</code> paths:
|
<code className="text-foreground">$data</code> paths:
|
||||||
</p>
|
</p>
|
||||||
<Code lang="json">{`{
|
<Code lang="json">{`{
|
||||||
|
"key": "metric-1",
|
||||||
"type": "Metric",
|
"type": "Metric",
|
||||||
"props": {
|
"props": {
|
||||||
"label": "Total Revenue",
|
"label": "Total Revenue",
|
||||||
"value": "$data.metrics.revenue",
|
"value": "$data.metrics.revenue",
|
||||||
"change": "$data.metrics.revenueChange"
|
"change": "$data.metrics.revenueChange"
|
||||||
},
|
},
|
||||||
"children": []
|
"children": [],
|
||||||
|
"parentKey": "dashboard"
|
||||||
}`}</Code>
|
}`}</Code>
|
||||||
|
|
||||||
<h3 className="text-lg font-medium mt-8 mb-3">Conditional Visibility</h3>
|
<h3 className="text-lg font-medium mt-8 mb-3">Conditional Visibility</h3>
|
||||||
@@ -227,11 +275,13 @@ export default function SpecsPage() {
|
|||||||
<code className="text-foreground">visible</code> property:
|
<code className="text-foreground">visible</code> property:
|
||||||
</p>
|
</p>
|
||||||
<Code lang="json">{`{
|
<Code lang="json">{`{
|
||||||
|
"key": "alert-1",
|
||||||
"type": "Alert",
|
"type": "Alert",
|
||||||
"props": {
|
"props": {
|
||||||
"message": "You have unsaved changes"
|
"message": "You have unsaved changes"
|
||||||
},
|
},
|
||||||
"children": [],
|
"children": [],
|
||||||
|
"parentKey": "form",
|
||||||
"visible": {
|
"visible": {
|
||||||
"path": "$data.form.isDirty",
|
"path": "$data.form.isDirty",
|
||||||
"operator": "eq",
|
"operator": "eq",
|
||||||
|
|||||||
@@ -18,10 +18,9 @@ export default function StreamingPage() {
|
|||||||
format where each line is a JSON patch operation that progressively
|
format where each line is a JSON patch operation that progressively
|
||||||
builds your spec:
|
builds your spec:
|
||||||
</p>
|
</p>
|
||||||
<Code lang="json">{`{"op":"add","path":"/root","value":"root"}
|
<Code lang="json">{`{"op":"set","path":"/root","value":{"key":"root","type":"Card","props":{"title":"Dashboard"}}}
|
||||||
{"op":"add","path":"/elements/root","value":{"type":"Card","props":{"title":"Dashboard"},"children":["metric-1","metric-2"]}}
|
{"op":"set","path":"/root/children/0","value":{"key":"metric-1","type":"Metric","props":{"label":"Revenue"}}}
|
||||||
{"op":"add","path":"/elements/metric-1","value":{"type":"Metric","props":{"label":"Revenue"}}}
|
{"op":"set","path":"/root/children/1","value":{"key":"metric-2","type":"Metric","props":{"label":"Users"}}}`}</Code>
|
||||||
{"op":"add","path":"/elements/metric-2","value":{"type":"Metric","props":{"label":"Users"}}}`}</Code>
|
|
||||||
|
|
||||||
<h2 className="text-xl font-semibold mt-12 mb-4">useUIStream Hook</h2>
|
<h2 className="text-xl font-semibold mt-12 mb-4">useUIStream Hook</h2>
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
@@ -35,55 +34,34 @@ 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>
|
||||||
|
|
||||||
<h2 className="text-xl font-semibold mt-12 mb-4">
|
<h2 className="text-xl font-semibold mt-12 mb-4">Patch Operations</h2>
|
||||||
Patch Operations (RFC 6902)
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
SpecStream uses{" "}
|
Supported operations:
|
||||||
<a
|
|
||||||
href="https://datatracker.ietf.org/doc/html/rfc6902"
|
|
||||||
className="underline"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
RFC 6902 JSON Patch
|
|
||||||
</a>{" "}
|
|
||||||
operations:
|
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-2 mb-4">
|
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-2 mb-4">
|
||||||
<li>
|
<li>
|
||||||
<code className="text-foreground">add</code> — Add a value at a path
|
<code className="text-foreground">set</code> — Set the value at a path
|
||||||
(creates or replaces for objects, inserts for arrays)
|
(creates if needed)
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<code className="text-foreground">remove</code> — Remove the value at
|
<code className="text-foreground">add</code> — Add to an array at a
|
||||||
a path
|
path
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<code className="text-foreground">replace</code> — Replace an existing
|
<code className="text-foreground">replace</code> — Replace value at a
|
||||||
value at a path
|
path
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<code className="text-foreground">move</code> — Move a value from one
|
<code className="text-foreground">remove</code> — Remove value at a
|
||||||
path to another (requires{" "}
|
path
|
||||||
<code className="text-foreground">from</code>)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code className="text-foreground">copy</code> — Copy a value from one
|
|
||||||
path to another (requires{" "}
|
|
||||||
<code className="text-foreground">from</code>)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code className="text-foreground">test</code> — Assert that a value at
|
|
||||||
a path equals the given value
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -134,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',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,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>
|
||||||
@@ -191,8 +165,8 @@ async function processStream(reader: ReadableStreamDefaultReader) {
|
|||||||
</p>
|
</p>
|
||||||
<Code lang="typescript">{`import { compileSpecStream } from '@json-render/core';
|
<Code lang="typescript">{`import { compileSpecStream } from '@json-render/core';
|
||||||
|
|
||||||
const jsonl = \`{"op":"add","path":"/root","value":{"type":"Card"}}
|
const jsonl = \`{"op":"set","path":"/root","value":{"type":"Card"}}
|
||||||
{"op":"add","path":"/root/props","value":{"title":"Hello"}}\`;
|
{"op":"set","path":"/root/props","value":{"title":"Hello"}}\`;
|
||||||
|
|
||||||
const spec = compileSpecStream<MySpec>(jsonl);
|
const spec = compileSpecStream<MySpec>(jsonl);
|
||||||
// { root: { type: "Card", props: { title: "Hello" } } }`}</Code>
|
// { root: { type: "Card", props: { title: "Hello" } } }`}</Code>
|
||||||
|
|||||||
@@ -127,16 +127,15 @@ export const catalog = createCatalog({
|
|||||||
Constrained output that your components render natively.
|
Constrained output that your components render natively.
|
||||||
</p>
|
</p>
|
||||||
<Code lang="json">{`{
|
<Code lang="json">{`{
|
||||||
"root": "dashboard",
|
"key": "dashboard",
|
||||||
"elements": {
|
"type": "Card",
|
||||||
"dashboard": {
|
"props": {
|
||||||
"type": "Card",
|
"title": "Revenue Dashboard",
|
||||||
"props": {
|
"description": null
|
||||||
"title": "Revenue Dashboard"
|
},
|
||||||
},
|
"children": [
|
||||||
"children": ["revenue"]
|
{
|
||||||
},
|
"key": "revenue",
|
||||||
"revenue": {
|
|
||||||
"type": "Metric",
|
"type": "Metric",
|
||||||
"props": {
|
"props": {
|
||||||
"label": "Total Revenue",
|
"label": "Total Revenue",
|
||||||
@@ -144,7 +143,7 @@ export const catalog = createCatalog({
|
|||||||
"format": "currency"
|
"format": "currency"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}`}</Code>
|
}`}</Code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,11 +170,13 @@ export const catalog = createCatalog({
|
|||||||
"root": "card",
|
"root": "card",
|
||||||
"elements": {
|
"elements": {
|
||||||
"card": {
|
"card": {
|
||||||
|
"key": "card",
|
||||||
"type": "Card",
|
"type": "Card",
|
||||||
"props": { "title": "Revenue" },
|
"props": { "title": "Revenue" },
|
||||||
"children": ["metric", "chart"]
|
"children": ["metric", "chart"]
|
||||||
},
|
},
|
||||||
"metric": {
|
"metric": {
|
||||||
|
"key": "metric",
|
||||||
"type": "Metric",
|
"type": "Metric",
|
||||||
"props": {
|
"props": {
|
||||||
"label": "Total Revenue",
|
"label": "Total Revenue",
|
||||||
@@ -184,6 +185,7 @@ export const catalog = createCatalog({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
|
"key": "chart",
|
||||||
"type": "Chart",
|
"type": "Chart",
|
||||||
"props": {
|
"props": {
|
||||||
"dataPath": "analytics/salesByRegion"
|
"dataPath": "analytics/salesByRegion"
|
||||||
|
|||||||
@@ -62,9 +62,8 @@ USER REQUEST: ${sanitizedPrompt}
|
|||||||
|
|
||||||
IMPORTANT: The current UI is already loaded. Output ONLY the patches needed to make the requested change:
|
IMPORTANT: The current UI is already loaded. Output ONLY the patches needed to make the requested change:
|
||||||
- To add a new element: {"op":"add","path":"/elements/new-key","value":{...}}
|
- To add a new element: {"op":"add","path":"/elements/new-key","value":{...}}
|
||||||
- To modify an existing element: {"op":"replace","path":"/elements/existing-key","value":{...}}
|
- To modify an existing element: {"op":"set","path":"/elements/existing-key","value":{...}}
|
||||||
- To remove an element: {"op":"remove","path":"/elements/old-key"}
|
- To update the root: {"op":"set","path":"/root","value":"new-root-key"}
|
||||||
- To update the root: {"op":"replace","path":"/root","value":"new-root-key"}
|
|
||||||
- To add children: update the parent element with new children array
|
- To add children: update the parent element with new children array
|
||||||
|
|
||||||
DO NOT output patches for elements that don't need to change. Only output what's necessary for the requested modification.`;
|
DO NOT output patches for elements that don't need to change. Only output what's necessary for the requested modification.`;
|
||||||
|
|||||||
@@ -29,109 +29,124 @@ const SIMULATION_STAGES: SimulationStage[] = [
|
|||||||
root: "card",
|
root: "card",
|
||||||
elements: {
|
elements: {
|
||||||
card: {
|
card: {
|
||||||
|
key: "card",
|
||||||
type: "Card",
|
type: "Card",
|
||||||
props: { title: "Contact Us", maxWidth: "md" },
|
props: { title: "Contact Us", maxWidth: "md" },
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
stream: '{"op":"add","path":"/root","value":"card"}',
|
stream: '{"op":"set","path":"/root","value":"card"}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tree: {
|
tree: {
|
||||||
root: "card",
|
root: "card",
|
||||||
elements: {
|
elements: {
|
||||||
card: {
|
card: {
|
||||||
|
key: "card",
|
||||||
type: "Card",
|
type: "Card",
|
||||||
props: { title: "Contact Us", maxWidth: "md" },
|
props: { title: "Contact Us", maxWidth: "md" },
|
||||||
children: ["name"],
|
children: ["name"],
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
|
key: "name",
|
||||||
type: "Input",
|
type: "Input",
|
||||||
props: { label: "Name", name: "name" },
|
props: { label: "Name", name: "name" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
stream:
|
stream:
|
||||||
'{"op":"add","path":"/elements/card","value":{"type":"Card","props":{"title":"Contact Us","maxWidth":"md"},"children":["name"]}}',
|
'{"op":"add","path":"/elements/card","value":{"key":"card","type":"Card","props":{"title":"Contact Us","maxWidth":"md"},"children":["name"]}}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tree: {
|
tree: {
|
||||||
root: "card",
|
root: "card",
|
||||||
elements: {
|
elements: {
|
||||||
card: {
|
card: {
|
||||||
|
key: "card",
|
||||||
type: "Card",
|
type: "Card",
|
||||||
props: { title: "Contact Us", maxWidth: "md" },
|
props: { title: "Contact Us", maxWidth: "md" },
|
||||||
children: ["name", "email"],
|
children: ["name", "email"],
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
|
key: "name",
|
||||||
type: "Input",
|
type: "Input",
|
||||||
props: { label: "Name", name: "name" },
|
props: { label: "Name", name: "name" },
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
|
key: "email",
|
||||||
type: "Input",
|
type: "Input",
|
||||||
props: { label: "Email", name: "email" },
|
props: { label: "Email", name: "email" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
stream:
|
stream:
|
||||||
'{"op":"add","path":"/elements/email","value":{"type":"Input","props":{"label":"Email","name":"email"}}}',
|
'{"op":"add","path":"/elements/email","value":{"key":"email","type":"Input","props":{"label":"Email","name":"email"}}}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tree: {
|
tree: {
|
||||||
root: "card",
|
root: "card",
|
||||||
elements: {
|
elements: {
|
||||||
card: {
|
card: {
|
||||||
|
key: "card",
|
||||||
type: "Card",
|
type: "Card",
|
||||||
props: { title: "Contact Us", maxWidth: "md" },
|
props: { title: "Contact Us", maxWidth: "md" },
|
||||||
children: ["name", "email", "message"],
|
children: ["name", "email", "message"],
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
|
key: "name",
|
||||||
type: "Input",
|
type: "Input",
|
||||||
props: { label: "Name", name: "name" },
|
props: { label: "Name", name: "name" },
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
|
key: "email",
|
||||||
type: "Input",
|
type: "Input",
|
||||||
props: { label: "Email", name: "email" },
|
props: { label: "Email", name: "email" },
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
|
key: "message",
|
||||||
type: "Textarea",
|
type: "Textarea",
|
||||||
props: { label: "Message", name: "message" },
|
props: { label: "Message", name: "message" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
stream:
|
stream:
|
||||||
'{"op":"add","path":"/elements/message","value":{"type":"Textarea","props":{"label":"Message","name":"message"}}}',
|
'{"op":"add","path":"/elements/message","value":{"key":"message","type":"Textarea","props":{"label":"Message","name":"message"}}}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tree: {
|
tree: {
|
||||||
root: "card",
|
root: "card",
|
||||||
elements: {
|
elements: {
|
||||||
card: {
|
card: {
|
||||||
|
key: "card",
|
||||||
type: "Card",
|
type: "Card",
|
||||||
props: { title: "Contact Us", maxWidth: "md" },
|
props: { title: "Contact Us", maxWidth: "md" },
|
||||||
children: ["name", "email", "message", "submit"],
|
children: ["name", "email", "message", "submit"],
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
|
key: "name",
|
||||||
type: "Input",
|
type: "Input",
|
||||||
props: { label: "Name", name: "name" },
|
props: { label: "Name", name: "name" },
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
|
key: "email",
|
||||||
type: "Input",
|
type: "Input",
|
||||||
props: { label: "Email", name: "email" },
|
props: { label: "Email", name: "email" },
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
|
key: "message",
|
||||||
type: "Textarea",
|
type: "Textarea",
|
||||||
props: { label: "Message", name: "message" },
|
props: { label: "Message", name: "message" },
|
||||||
},
|
},
|
||||||
submit: {
|
submit: {
|
||||||
|
key: "submit",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: { label: "Send Message", variant: "primary" },
|
props: { label: "Send Message", variant: "primary" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
stream:
|
stream:
|
||||||
'{"op":"add","path":"/elements/submit","value":{"type":"Button","props":{"label":"Send Message","variant":"primary"}}}',
|
'{"op":"add","path":"/elements/submit","value":{"key":"submit","type":"Button","props":{"label":"Send Message","variant":"primary"}}}',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -70,19 +70,25 @@ const spec: Spec = {
|
|||||||
root: "container",
|
root: "container",
|
||||||
elements: {
|
elements: {
|
||||||
container: {
|
container: {
|
||||||
|
key: "container",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "medium" },
|
props: { direction: "vertical", gap: "medium" },
|
||||||
children: ["heading", "metric"],
|
children: ["heading", "metric"],
|
||||||
|
parentKey: null,
|
||||||
},
|
},
|
||||||
heading: {
|
heading: {
|
||||||
|
key: "heading",
|
||||||
type: "Heading",
|
type: "Heading",
|
||||||
props: { text: "Revenue", size: "large" },
|
props: { text: "Revenue", size: "large" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "container",
|
||||||
},
|
},
|
||||||
metric: {
|
metric: {
|
||||||
|
key: "metric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: { label: "Total", value: "$12,345", format: "currency" },
|
props: { label: "Total", value: "$12,345", format: "currency" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "container",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -84,11 +84,14 @@ function createCustomerDetailSpec(
|
|||||||
|
|
||||||
const elements: Spec["elements"] = {
|
const elements: Spec["elements"] = {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "large" },
|
props: { direction: "vertical", gap: "large" },
|
||||||
children: ["header", "details", "sections"],
|
children: ["header", "details", "sections"],
|
||||||
|
parentKey: null,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
|
key: "header",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: {
|
props: {
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
@@ -96,28 +99,38 @@ function createCustomerDetailSpec(
|
|||||||
distribute: "space-between",
|
distribute: "space-between",
|
||||||
},
|
},
|
||||||
children: ["customerInfo", "actions"],
|
children: ["customerInfo", "actions"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
customerInfo: {
|
customerInfo: {
|
||||||
|
key: "customerInfo",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "xsmall" },
|
props: { direction: "vertical", gap: "xsmall" },
|
||||||
children: ["customerName", "customerEmail"],
|
children: ["customerName", "customerEmail"],
|
||||||
|
parentKey: "header",
|
||||||
},
|
},
|
||||||
customerName: {
|
customerName: {
|
||||||
|
key: "customerName",
|
||||||
type: "Heading",
|
type: "Heading",
|
||||||
props: { text: customer?.name ?? "Unknown Customer", size: "xlarge" },
|
props: { text: customer?.name ?? "Unknown Customer", size: "xlarge" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "customerInfo",
|
||||||
},
|
},
|
||||||
customerEmail: {
|
customerEmail: {
|
||||||
|
key: "customerEmail",
|
||||||
type: "Text",
|
type: "Text",
|
||||||
props: { content: customer?.email ?? "", color: "secondary" },
|
props: { content: customer?.email ?? "", color: "secondary" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "customerInfo",
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
key: "actions",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "small" },
|
props: { direction: "horizontal", gap: "small" },
|
||||||
children: ["editBtn", "portalBtn"],
|
children: ["editBtn", "portalBtn"],
|
||||||
|
parentKey: "header",
|
||||||
},
|
},
|
||||||
editBtn: {
|
editBtn: {
|
||||||
|
key: "editBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Edit",
|
label: "Edit",
|
||||||
@@ -126,8 +139,10 @@ function createCustomerDetailSpec(
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "actions",
|
||||||
},
|
},
|
||||||
portalBtn: {
|
portalBtn: {
|
||||||
|
key: "portalBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Billing Portal",
|
label: "Billing Portal",
|
||||||
@@ -136,23 +151,31 @@ function createCustomerDetailSpec(
|
|||||||
type: "primary",
|
type: "primary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "actions",
|
||||||
},
|
},
|
||||||
details: {
|
details: {
|
||||||
|
key: "details",
|
||||||
type: "PropertyList",
|
type: "PropertyList",
|
||||||
props: { orientation: "horizontal" },
|
props: { orientation: "horizontal" },
|
||||||
children: ["detailStatus", "detailCreated", "detailBalance"],
|
children: ["detailStatus", "detailCreated", "detailBalance"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
detailStatus: {
|
detailStatus: {
|
||||||
|
key: "detailStatus",
|
||||||
type: "PropertyListItem",
|
type: "PropertyListItem",
|
||||||
props: { label: "Status", value: customer?.status ?? "Unknown" },
|
props: { label: "Status", value: customer?.status ?? "Unknown" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "details",
|
||||||
},
|
},
|
||||||
detailCreated: {
|
detailCreated: {
|
||||||
|
key: "detailCreated",
|
||||||
type: "PropertyListItem",
|
type: "PropertyListItem",
|
||||||
props: { label: "Created", value: customer?.created ?? "Unknown" },
|
props: { label: "Created", value: customer?.created ?? "Unknown" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "details",
|
||||||
},
|
},
|
||||||
detailBalance: {
|
detailBalance: {
|
||||||
|
key: "detailBalance",
|
||||||
type: "PropertyListItem",
|
type: "PropertyListItem",
|
||||||
props: {
|
props: {
|
||||||
label: "Balance",
|
label: "Balance",
|
||||||
@@ -161,32 +184,42 @@ function createCustomerDetailSpec(
|
|||||||
: "$0.00",
|
: "$0.00",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "details",
|
||||||
},
|
},
|
||||||
sections: {
|
sections: {
|
||||||
|
key: "sections",
|
||||||
type: "Accordion",
|
type: "Accordion",
|
||||||
props: {},
|
props: {},
|
||||||
children: ["paymentsSection", "subscriptionsSection", "invoicesSection"],
|
children: ["paymentsSection", "subscriptionsSection", "invoicesSection"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Payments Section
|
// Payments Section
|
||||||
paymentsSection: {
|
paymentsSection: {
|
||||||
|
key: "paymentsSection",
|
||||||
type: "AccordionItem",
|
type: "AccordionItem",
|
||||||
props: { title: `Payments (${payments?.total ?? 0})`, defaultOpen: true },
|
props: { title: `Payments (${payments?.total ?? 0})`, defaultOpen: true },
|
||||||
children: paymentsList.length > 0 ? ["paymentsList"] : ["noPayments"],
|
children: paymentsList.length > 0 ? ["paymentsList"] : ["noPayments"],
|
||||||
|
parentKey: "sections",
|
||||||
},
|
},
|
||||||
paymentsList: {
|
paymentsList: {
|
||||||
|
key: "paymentsList",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "small" },
|
props: { direction: "vertical", gap: "small" },
|
||||||
children: paymentsList.slice(0, 5).map((_, i) => `payment${i}`),
|
children: paymentsList.slice(0, 5).map((_, i) => `payment${i}`),
|
||||||
|
parentKey: "paymentsSection",
|
||||||
},
|
},
|
||||||
noPayments: {
|
noPayments: {
|
||||||
|
key: "noPayments",
|
||||||
type: "Text",
|
type: "Text",
|
||||||
props: { content: "No payments found", color: "secondary" },
|
props: { content: "No payments found", color: "secondary" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "paymentsSection",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Subscriptions Section
|
// Subscriptions Section
|
||||||
subscriptionsSection: {
|
subscriptionsSection: {
|
||||||
|
key: "subscriptionsSection",
|
||||||
type: "AccordionItem",
|
type: "AccordionItem",
|
||||||
props: {
|
props: {
|
||||||
title: `Subscriptions (${subscriptions?.total ?? 0})`,
|
title: `Subscriptions (${subscriptions?.total ?? 0})`,
|
||||||
@@ -196,42 +229,54 @@ function createCustomerDetailSpec(
|
|||||||
subscriptionsList.length > 0
|
subscriptionsList.length > 0
|
||||||
? ["subscriptionsList"]
|
? ["subscriptionsList"]
|
||||||
: ["noSubscriptions"],
|
: ["noSubscriptions"],
|
||||||
|
parentKey: "sections",
|
||||||
},
|
},
|
||||||
subscriptionsList: {
|
subscriptionsList: {
|
||||||
|
key: "subscriptionsList",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "small" },
|
props: { direction: "vertical", gap: "small" },
|
||||||
children: subscriptionsList.slice(0, 5).map((_, i) => `subscription${i}`),
|
children: subscriptionsList.slice(0, 5).map((_, i) => `subscription${i}`),
|
||||||
|
parentKey: "subscriptionsSection",
|
||||||
},
|
},
|
||||||
noSubscriptions: {
|
noSubscriptions: {
|
||||||
|
key: "noSubscriptions",
|
||||||
type: "Text",
|
type: "Text",
|
||||||
props: { content: "No subscriptions found", color: "secondary" },
|
props: { content: "No subscriptions found", color: "secondary" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "subscriptionsSection",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Invoices Section
|
// Invoices Section
|
||||||
invoicesSection: {
|
invoicesSection: {
|
||||||
|
key: "invoicesSection",
|
||||||
type: "AccordionItem",
|
type: "AccordionItem",
|
||||||
props: {
|
props: {
|
||||||
title: `Invoices (${invoices?.total ?? 0})`,
|
title: `Invoices (${invoices?.total ?? 0})`,
|
||||||
defaultOpen: false,
|
defaultOpen: false,
|
||||||
},
|
},
|
||||||
children: invoicesList.length > 0 ? ["invoicesList"] : ["noInvoices"],
|
children: invoicesList.length > 0 ? ["invoicesList"] : ["noInvoices"],
|
||||||
|
parentKey: "sections",
|
||||||
},
|
},
|
||||||
invoicesList: {
|
invoicesList: {
|
||||||
|
key: "invoicesList",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "small" },
|
props: { direction: "vertical", gap: "small" },
|
||||||
children: invoicesList.slice(0, 5).map((_, i) => `invoice${i}`),
|
children: invoicesList.slice(0, 5).map((_, i) => `invoice${i}`),
|
||||||
|
parentKey: "invoicesSection",
|
||||||
},
|
},
|
||||||
noInvoices: {
|
noInvoices: {
|
||||||
|
key: "noInvoices",
|
||||||
type: "Text",
|
type: "Text",
|
||||||
props: { content: "No invoices found", color: "secondary" },
|
props: { content: "No invoices found", color: "secondary" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "invoicesSection",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add payment cards
|
// Add payment cards
|
||||||
paymentsList.slice(0, 5).forEach((p, i) => {
|
paymentsList.slice(0, 5).forEach((p, i) => {
|
||||||
elements[`payment${i}`] = {
|
elements[`payment${i}`] = {
|
||||||
|
key: `payment${i}`,
|
||||||
type: "PaymentCard",
|
type: "PaymentCard",
|
||||||
props: {
|
props: {
|
||||||
amount: 0,
|
amount: 0,
|
||||||
@@ -241,12 +286,14 @@ function createCustomerDetailSpec(
|
|||||||
paymentId: p.id,
|
paymentId: p.id,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "paymentsList",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subscription cards
|
// Add subscription cards
|
||||||
subscriptionsList.slice(0, 5).forEach((s, i) => {
|
subscriptionsList.slice(0, 5).forEach((s, i) => {
|
||||||
elements[`subscription${i}`] = {
|
elements[`subscription${i}`] = {
|
||||||
|
key: `subscription${i}`,
|
||||||
type: "SubscriptionCard",
|
type: "SubscriptionCard",
|
||||||
props: {
|
props: {
|
||||||
planName: s.planName,
|
planName: s.planName,
|
||||||
@@ -257,12 +304,14 @@ function createCustomerDetailSpec(
|
|||||||
currentPeriodEnd: s.currentPeriodEnd,
|
currentPeriodEnd: s.currentPeriodEnd,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "subscriptionsList",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add invoice cards
|
// Add invoice cards
|
||||||
invoicesList.slice(0, 5).forEach((inv, i) => {
|
invoicesList.slice(0, 5).forEach((inv, i) => {
|
||||||
elements[`invoice${i}`] = {
|
elements[`invoice${i}`] = {
|
||||||
|
key: `invoice${i}`,
|
||||||
type: "InvoiceCard",
|
type: "InvoiceCard",
|
||||||
props: {
|
props: {
|
||||||
invoiceNumber: inv.invoiceNumber,
|
invoiceNumber: inv.invoiceNumber,
|
||||||
@@ -272,6 +321,7 @@ function createCustomerDetailSpec(
|
|||||||
dueDate: inv.dueDate,
|
dueDate: inv.dueDate,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "invoicesList",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,21 +36,28 @@ function createCustomersListSpec(data: Record<string, unknown>): Spec {
|
|||||||
|
|
||||||
const elements: Spec["elements"] = {
|
const elements: Spec["elements"] = {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "large" },
|
props: { direction: "vertical", gap: "large" },
|
||||||
children: ["heading", "stats", "actions", "list", "pagination"],
|
children: ["heading", "stats", "actions", "list", "pagination"],
|
||||||
|
parentKey: null,
|
||||||
},
|
},
|
||||||
heading: {
|
heading: {
|
||||||
|
key: "heading",
|
||||||
type: "Heading",
|
type: "Heading",
|
||||||
props: { text: "Customer Directory", size: "xlarge" },
|
props: { text: "Customer Directory", size: "xlarge" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
|
key: "stats",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "medium" },
|
props: { direction: "horizontal", gap: "medium" },
|
||||||
children: ["totalMetric"],
|
children: ["totalMetric"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
totalMetric: {
|
totalMetric: {
|
||||||
|
key: "totalMetric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Total Customers",
|
label: "Total Customers",
|
||||||
@@ -58,13 +65,17 @@ function createCustomersListSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "neutral",
|
changeType: "neutral",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "stats",
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
key: "actions",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "small" },
|
props: { direction: "horizontal", gap: "small" },
|
||||||
children: ["refreshBtn", "exportBtn"],
|
children: ["refreshBtn", "exportBtn"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
refreshBtn: {
|
refreshBtn: {
|
||||||
|
key: "refreshBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Refresh",
|
label: "Refresh",
|
||||||
@@ -72,8 +83,10 @@ function createCustomersListSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "actions",
|
||||||
},
|
},
|
||||||
exportBtn: {
|
exportBtn: {
|
||||||
|
key: "exportBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Export CSV",
|
label: "Export CSV",
|
||||||
@@ -82,18 +95,24 @@ function createCustomersListSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "actions",
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
|
key: "list",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "small" },
|
props: { direction: "vertical", gap: "small" },
|
||||||
children: customerList.slice(0, 10).map((_, i) => `customer${i}`),
|
children: customerList.slice(0, 10).map((_, i) => `customer${i}`),
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
|
key: "pagination",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "small" },
|
props: { direction: "horizontal", gap: "small" },
|
||||||
children: customers?.hasMore ? ["loadMore"] : [],
|
children: customers?.hasMore ? ["loadMore"] : [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
loadMore: {
|
loadMore: {
|
||||||
|
key: "loadMore",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Load More",
|
label: "Load More",
|
||||||
@@ -105,11 +124,13 @@ function createCustomersListSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "pagination",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
customerList.slice(0, 10).forEach((c, i) => {
|
customerList.slice(0, 10).forEach((c, i) => {
|
||||||
elements[`customer${i}`] = {
|
elements[`customer${i}`] = {
|
||||||
|
key: `customer${i}`,
|
||||||
type: "CustomerCard",
|
type: "CustomerCard",
|
||||||
props: {
|
props: {
|
||||||
name: c.name,
|
name: c.name,
|
||||||
@@ -118,6 +139,7 @@ function createCustomersListSpec(data: Record<string, unknown>): Spec {
|
|||||||
customerId: c.id,
|
customerId: c.id,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "list",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,21 +38,28 @@ function createRevenueSpec(data: Record<string, unknown>): Spec {
|
|||||||
root: "root",
|
root: "root",
|
||||||
elements: {
|
elements: {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "large" },
|
props: { direction: "vertical", gap: "large" },
|
||||||
children: ["heading", "metrics", "refresh"],
|
children: ["heading", "metrics", "refresh"],
|
||||||
|
parentKey: null,
|
||||||
},
|
},
|
||||||
heading: {
|
heading: {
|
||||||
|
key: "heading",
|
||||||
type: "Heading",
|
type: "Heading",
|
||||||
props: { text: "Revenue Dashboard", size: "xlarge" },
|
props: { text: "Revenue Dashboard", size: "xlarge" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
metrics: {
|
metrics: {
|
||||||
|
key: "metrics",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "medium" },
|
props: { direction: "horizontal", gap: "medium" },
|
||||||
children: ["m1", "m2", "m3"],
|
children: ["m1", "m2", "m3"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
m1: {
|
m1: {
|
||||||
|
key: "m1",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Payment Volume",
|
label: "Payment Volume",
|
||||||
@@ -63,8 +70,10 @@ function createRevenueSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "positive",
|
changeType: "positive",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
m2: {
|
m2: {
|
||||||
|
key: "m2",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Active Subscriptions",
|
label: "Active Subscriptions",
|
||||||
@@ -75,8 +84,10 @@ function createRevenueSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "positive",
|
changeType: "positive",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
m3: {
|
m3: {
|
||||||
|
key: "m3",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Total Customers",
|
label: "Total Customers",
|
||||||
@@ -84,8 +95,10 @@ function createRevenueSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "neutral",
|
changeType: "neutral",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
refresh: {
|
refresh: {
|
||||||
|
key: "refresh",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Refresh Data",
|
label: "Refresh Data",
|
||||||
@@ -93,6 +106,7 @@ function createRevenueSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -115,21 +129,28 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
|||||||
|
|
||||||
const elements: Spec["elements"] = {
|
const elements: Spec["elements"] = {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "large" },
|
props: { direction: "vertical", gap: "large" },
|
||||||
children: ["heading", "stats", "payments", "refresh"],
|
children: ["heading", "stats", "payments", "refresh"],
|
||||||
|
parentKey: null,
|
||||||
},
|
},
|
||||||
heading: {
|
heading: {
|
||||||
|
key: "heading",
|
||||||
type: "Heading",
|
type: "Heading",
|
||||||
props: { text: "Recent Payments", size: "xlarge" },
|
props: { text: "Recent Payments", size: "xlarge" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
|
key: "stats",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "medium" },
|
props: { direction: "horizontal", gap: "medium" },
|
||||||
children: ["s1", "s2"],
|
children: ["s1", "s2"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
s1: {
|
s1: {
|
||||||
|
key: "s1",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Total Volume",
|
label: "Total Volume",
|
||||||
@@ -137,8 +158,10 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "positive",
|
changeType: "positive",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "stats",
|
||||||
},
|
},
|
||||||
s2: {
|
s2: {
|
||||||
|
key: "s2",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Success Rate",
|
label: "Success Rate",
|
||||||
@@ -146,13 +169,17 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "positive",
|
changeType: "positive",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "stats",
|
||||||
},
|
},
|
||||||
payments: {
|
payments: {
|
||||||
|
key: "payments",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "small" },
|
props: { direction: "vertical", gap: "small" },
|
||||||
children: paymentsList.slice(0, 5).map((_, i) => `p${i}`),
|
children: paymentsList.slice(0, 5).map((_, i) => `p${i}`),
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
refresh: {
|
refresh: {
|
||||||
|
key: "refresh",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Refresh Payments",
|
label: "Refresh Payments",
|
||||||
@@ -160,11 +187,13 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
paymentsList.slice(0, 5).forEach((p, i) => {
|
paymentsList.slice(0, 5).forEach((p, i) => {
|
||||||
elements[`p${i}`] = {
|
elements[`p${i}`] = {
|
||||||
|
key: `p${i}`,
|
||||||
type: "PaymentCard",
|
type: "PaymentCard",
|
||||||
props: {
|
props: {
|
||||||
amount: 0, // We'll use formattedAmount in description
|
amount: 0, // We'll use formattedAmount in description
|
||||||
@@ -174,6 +203,7 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
|||||||
paymentId: p.id,
|
paymentId: p.id,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "payments",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,21 +229,28 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
|
|
||||||
const elements: Spec["elements"] = {
|
const elements: Spec["elements"] = {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "large" },
|
props: { direction: "vertical", gap: "large" },
|
||||||
children: ["heading", "metrics", "alert", "subs", "refresh"],
|
children: ["heading", "metrics", "alert", "subs", "refresh"],
|
||||||
|
parentKey: null,
|
||||||
},
|
},
|
||||||
heading: {
|
heading: {
|
||||||
|
key: "heading",
|
||||||
type: "Heading",
|
type: "Heading",
|
||||||
props: { text: "Subscriptions Overview", size: "xlarge" },
|
props: { text: "Subscriptions Overview", size: "xlarge" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
metrics: {
|
metrics: {
|
||||||
|
key: "metrics",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "medium" },
|
props: { direction: "horizontal", gap: "medium" },
|
||||||
children: ["m1", "m2", "m3"],
|
children: ["m1", "m2", "m3"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
m1: {
|
m1: {
|
||||||
|
key: "m1",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Active",
|
label: "Active",
|
||||||
@@ -221,8 +258,10 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "positive",
|
changeType: "positive",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
m2: {
|
m2: {
|
||||||
|
key: "m2",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Trialing",
|
label: "Trialing",
|
||||||
@@ -230,8 +269,10 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "positive",
|
changeType: "positive",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
m3: {
|
m3: {
|
||||||
|
key: "m3",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Past Due",
|
label: "Past Due",
|
||||||
@@ -239,21 +280,27 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: subscriptions?.pastDue ? "negative" : "neutral",
|
changeType: subscriptions?.pastDue ? "negative" : "neutral",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
alert: {
|
alert: {
|
||||||
|
key: "alert",
|
||||||
type: "Banner",
|
type: "Banner",
|
||||||
props: {
|
props: {
|
||||||
title: `${subscriptions?.trialing ?? 0} subscriptions currently trialing`,
|
title: `${subscriptions?.trialing ?? 0} subscriptions currently trialing`,
|
||||||
type: "default",
|
type: "default",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
subs: {
|
subs: {
|
||||||
|
key: "subs",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "small" },
|
props: { direction: "vertical", gap: "small" },
|
||||||
children: subsList.slice(0, 5).map((_, i) => `sub${i}`),
|
children: subsList.slice(0, 5).map((_, i) => `sub${i}`),
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
refresh: {
|
refresh: {
|
||||||
|
key: "refresh",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Refresh Subscriptions",
|
label: "Refresh Subscriptions",
|
||||||
@@ -261,11 +308,13 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
subsList.slice(0, 5).forEach((s, i) => {
|
subsList.slice(0, 5).forEach((s, i) => {
|
||||||
elements[`sub${i}`] = {
|
elements[`sub${i}`] = {
|
||||||
|
key: `sub${i}`,
|
||||||
type: "SubscriptionCard",
|
type: "SubscriptionCard",
|
||||||
props: {
|
props: {
|
||||||
planName: s.planName,
|
planName: s.planName,
|
||||||
@@ -274,6 +323,7 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
interval: (s.interval as "month" | "year") ?? "month",
|
interval: (s.interval as "month" | "year") ?? "month",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "subs",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -296,21 +346,28 @@ function createCustomersSpec(data: Record<string, unknown>): Spec {
|
|||||||
|
|
||||||
const elements: Spec["elements"] = {
|
const elements: Spec["elements"] = {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "large" },
|
props: { direction: "vertical", gap: "large" },
|
||||||
children: ["heading", "stats", "customers", "refresh"],
|
children: ["heading", "stats", "customers", "refresh"],
|
||||||
|
parentKey: null,
|
||||||
},
|
},
|
||||||
heading: {
|
heading: {
|
||||||
|
key: "heading",
|
||||||
type: "Heading",
|
type: "Heading",
|
||||||
props: { text: "Customer Directory", size: "xlarge" },
|
props: { text: "Customer Directory", size: "xlarge" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
|
key: "stats",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "medium" },
|
props: { direction: "horizontal", gap: "medium" },
|
||||||
children: ["s1"],
|
children: ["s1"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
s1: {
|
s1: {
|
||||||
|
key: "s1",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Total Customers",
|
label: "Total Customers",
|
||||||
@@ -318,13 +375,17 @@ function createCustomersSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "neutral",
|
changeType: "neutral",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "stats",
|
||||||
},
|
},
|
||||||
customers: {
|
customers: {
|
||||||
|
key: "customers",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "small" },
|
props: { direction: "vertical", gap: "small" },
|
||||||
children: customersList.slice(0, 5).map((_, i) => `c${i}`),
|
children: customersList.slice(0, 5).map((_, i) => `c${i}`),
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
refresh: {
|
refresh: {
|
||||||
|
key: "refresh",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Refresh Customers",
|
label: "Refresh Customers",
|
||||||
@@ -332,11 +393,13 @@ function createCustomersSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
customersList.slice(0, 5).forEach((c, i) => {
|
customersList.slice(0, 5).forEach((c, i) => {
|
||||||
elements[`c${i}`] = {
|
elements[`c${i}`] = {
|
||||||
|
key: `c${i}`,
|
||||||
type: "CustomerCard",
|
type: "CustomerCard",
|
||||||
props: {
|
props: {
|
||||||
name: c.name,
|
name: c.name,
|
||||||
@@ -345,6 +408,7 @@ function createCustomersSpec(data: Record<string, unknown>): Spec {
|
|||||||
customerId: c.id,
|
customerId: c.id,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "customers",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -370,21 +434,28 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
|
|
||||||
const elements: Spec["elements"] = {
|
const elements: Spec["elements"] = {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "large" },
|
props: { direction: "vertical", gap: "large" },
|
||||||
children: ["heading", "stats", "invoices", "refresh"],
|
children: ["heading", "stats", "invoices", "refresh"],
|
||||||
|
parentKey: null,
|
||||||
},
|
},
|
||||||
heading: {
|
heading: {
|
||||||
|
key: "heading",
|
||||||
type: "Heading",
|
type: "Heading",
|
||||||
props: { text: "Invoice Management", size: "xlarge" },
|
props: { text: "Invoice Management", size: "xlarge" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
|
key: "stats",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "medium" },
|
props: { direction: "horizontal", gap: "medium" },
|
||||||
children: ["s1", "s2", "s3"],
|
children: ["s1", "s2", "s3"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
s1: {
|
s1: {
|
||||||
|
key: "s1",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Outstanding",
|
label: "Outstanding",
|
||||||
@@ -392,8 +463,10 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "neutral",
|
changeType: "neutral",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "stats",
|
||||||
},
|
},
|
||||||
s2: {
|
s2: {
|
||||||
|
key: "s2",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Paid",
|
label: "Paid",
|
||||||
@@ -401,8 +474,10 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "positive",
|
changeType: "positive",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "stats",
|
||||||
},
|
},
|
||||||
s3: {
|
s3: {
|
||||||
|
key: "s3",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Overdue",
|
label: "Overdue",
|
||||||
@@ -410,13 +485,17 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: invoices?.overdue !== "$0.00" ? "negative" : "neutral",
|
changeType: invoices?.overdue !== "$0.00" ? "negative" : "neutral",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "stats",
|
||||||
},
|
},
|
||||||
invoices: {
|
invoices: {
|
||||||
|
key: "invoices",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "small" },
|
props: { direction: "vertical", gap: "small" },
|
||||||
children: invoicesList.slice(0, 5).map((_, i) => `inv${i}`),
|
children: invoicesList.slice(0, 5).map((_, i) => `inv${i}`),
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
refresh: {
|
refresh: {
|
||||||
|
key: "refresh",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Refresh Invoices",
|
label: "Refresh Invoices",
|
||||||
@@ -424,11 +503,13 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
invoicesList.slice(0, 5).forEach((inv, i) => {
|
invoicesList.slice(0, 5).forEach((inv, i) => {
|
||||||
elements[`inv${i}`] = {
|
elements[`inv${i}`] = {
|
||||||
|
key: `inv${i}`,
|
||||||
type: "InvoiceCard",
|
type: "InvoiceCard",
|
||||||
props: {
|
props: {
|
||||||
invoiceNumber: inv.invoiceNumber,
|
invoiceNumber: inv.invoiceNumber,
|
||||||
@@ -437,6 +518,7 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
dueDate: inv.dueDate,
|
dueDate: inv.dueDate,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "invoices",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
|
|
||||||
const elements: Spec["elements"] = {
|
const elements: Spec["elements"] = {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "large" },
|
props: { direction: "vertical", gap: "large" },
|
||||||
children: [
|
children: [
|
||||||
@@ -51,18 +52,24 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
"list",
|
"list",
|
||||||
"pagination",
|
"pagination",
|
||||||
],
|
],
|
||||||
|
parentKey: null,
|
||||||
},
|
},
|
||||||
heading: {
|
heading: {
|
||||||
|
key: "heading",
|
||||||
type: "Heading",
|
type: "Heading",
|
||||||
props: { text: "Invoices", size: "xlarge" },
|
props: { text: "Invoices", size: "xlarge" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
metrics: {
|
metrics: {
|
||||||
|
key: "metrics",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "medium" },
|
props: { direction: "horizontal", gap: "medium" },
|
||||||
children: ["outstandingMetric", "paidMetric", "overdueMetric"],
|
children: ["outstandingMetric", "paidMetric", "overdueMetric"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
outstandingMetric: {
|
outstandingMetric: {
|
||||||
|
key: "outstandingMetric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Outstanding",
|
label: "Outstanding",
|
||||||
@@ -70,8 +77,10 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "neutral",
|
changeType: "neutral",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
paidMetric: {
|
paidMetric: {
|
||||||
|
key: "paidMetric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Paid",
|
label: "Paid",
|
||||||
@@ -79,8 +88,10 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "positive",
|
changeType: "positive",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
overdueMetric: {
|
overdueMetric: {
|
||||||
|
key: "overdueMetric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Overdue",
|
label: "Overdue",
|
||||||
@@ -88,8 +99,10 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: invoices?.overdue !== "$0.00" ? "negative" : "neutral",
|
changeType: invoices?.overdue !== "$0.00" ? "negative" : "neutral",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
alert: {
|
alert: {
|
||||||
|
key: "alert",
|
||||||
type: "Banner",
|
type: "Banner",
|
||||||
props: {
|
props: {
|
||||||
title:
|
title:
|
||||||
@@ -99,18 +112,24 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: invoices?.overdue !== "$0.00" ? "caution" : "default",
|
type: invoices?.overdue !== "$0.00" ? "caution" : "default",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
|
key: "filters",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "small" },
|
props: { direction: "horizontal", gap: "small" },
|
||||||
children: ["refreshBtn", "createBtn", "exportBtn"],
|
children: ["refreshBtn", "createBtn", "exportBtn"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
refreshBtn: {
|
refreshBtn: {
|
||||||
|
key: "refreshBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: { label: "Refresh", action: "refreshInvoices", type: "secondary" },
|
props: { label: "Refresh", action: "refreshInvoices", type: "secondary" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "filters",
|
||||||
},
|
},
|
||||||
createBtn: {
|
createBtn: {
|
||||||
|
key: "createBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Create Invoice",
|
label: "Create Invoice",
|
||||||
@@ -119,8 +138,10 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "primary",
|
type: "primary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "filters",
|
||||||
},
|
},
|
||||||
exportBtn: {
|
exportBtn: {
|
||||||
|
key: "exportBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Export CSV",
|
label: "Export CSV",
|
||||||
@@ -129,18 +150,24 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "filters",
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
|
key: "list",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "small" },
|
props: { direction: "vertical", gap: "small" },
|
||||||
children: invoicesList.slice(0, 10).map((_, i) => `invoice${i}`),
|
children: invoicesList.slice(0, 10).map((_, i) => `invoice${i}`),
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
|
key: "pagination",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "small" },
|
props: { direction: "horizontal", gap: "small" },
|
||||||
children: invoices?.hasMore ? ["loadMore"] : [],
|
children: invoices?.hasMore ? ["loadMore"] : [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
loadMore: {
|
loadMore: {
|
||||||
|
key: "loadMore",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Load More",
|
label: "Load More",
|
||||||
@@ -149,11 +176,13 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "pagination",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
invoicesList.slice(0, 10).forEach((inv, i) => {
|
invoicesList.slice(0, 10).forEach((inv, i) => {
|
||||||
elements[`invoice${i}`] = {
|
elements[`invoice${i}`] = {
|
||||||
|
key: `invoice${i}`,
|
||||||
type: "InvoiceCard",
|
type: "InvoiceCard",
|
||||||
props: {
|
props: {
|
||||||
invoiceNumber: inv.invoiceNumber,
|
invoiceNumber: inv.invoiceNumber,
|
||||||
@@ -169,6 +198,7 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
|||||||
customerEmail: inv.customerEmail,
|
customerEmail: inv.customerEmail,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "list",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -56,11 +56,14 @@ function createPaymentDetailSpec(
|
|||||||
|
|
||||||
const elements: Spec["elements"] = {
|
const elements: Spec["elements"] = {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "large" },
|
props: { direction: "vertical", gap: "large" },
|
||||||
children: ["header", "details", "actions", "refundsSection"],
|
children: ["header", "details", "actions", "refundsSection"],
|
||||||
|
parentKey: null,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
|
key: "header",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: {
|
props: {
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
@@ -68,23 +71,31 @@ function createPaymentDetailSpec(
|
|||||||
distribute: "space-between",
|
distribute: "space-between",
|
||||||
},
|
},
|
||||||
children: ["paymentInfo", "statusBadge"],
|
children: ["paymentInfo", "statusBadge"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
paymentInfo: {
|
paymentInfo: {
|
||||||
|
key: "paymentInfo",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "xsmall" },
|
props: { direction: "vertical", gap: "xsmall" },
|
||||||
children: ["paymentAmount", "paymentId"],
|
children: ["paymentAmount", "paymentId"],
|
||||||
|
parentKey: "header",
|
||||||
},
|
},
|
||||||
paymentAmount: {
|
paymentAmount: {
|
||||||
|
key: "paymentAmount",
|
||||||
type: "Heading",
|
type: "Heading",
|
||||||
props: { text: payment?.formattedAmount ?? "$0.00", size: "xlarge" },
|
props: { text: payment?.formattedAmount ?? "$0.00", size: "xlarge" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "paymentInfo",
|
||||||
},
|
},
|
||||||
paymentId: {
|
paymentId: {
|
||||||
|
key: "paymentId",
|
||||||
type: "Text",
|
type: "Text",
|
||||||
props: { content: paymentId, color: "secondary", size: "small" },
|
props: { content: paymentId, color: "secondary", size: "small" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "paymentInfo",
|
||||||
},
|
},
|
||||||
statusBadge: {
|
statusBadge: {
|
||||||
|
key: "statusBadge",
|
||||||
type: "Badge",
|
type: "Badge",
|
||||||
props: {
|
props: {
|
||||||
label: payment?.status ?? "unknown",
|
label: payment?.status ?? "unknown",
|
||||||
@@ -96,8 +107,10 @@ function createPaymentDetailSpec(
|
|||||||
: "negative",
|
: "negative",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "header",
|
||||||
},
|
},
|
||||||
details: {
|
details: {
|
||||||
|
key: "details",
|
||||||
type: "PropertyList",
|
type: "PropertyList",
|
||||||
props: { orientation: "vertical" },
|
props: { orientation: "vertical" },
|
||||||
children: [
|
children: [
|
||||||
@@ -106,42 +119,54 @@ function createPaymentDetailSpec(
|
|||||||
"detailCurrency",
|
"detailCurrency",
|
||||||
"detailCustomer",
|
"detailCustomer",
|
||||||
],
|
],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
detailDescription: {
|
detailDescription: {
|
||||||
|
key: "detailDescription",
|
||||||
type: "PropertyListItem",
|
type: "PropertyListItem",
|
||||||
props: {
|
props: {
|
||||||
label: "Description",
|
label: "Description",
|
||||||
value: payment?.description ?? "No description",
|
value: payment?.description ?? "No description",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "details",
|
||||||
},
|
},
|
||||||
detailCreated: {
|
detailCreated: {
|
||||||
|
key: "detailCreated",
|
||||||
type: "PropertyListItem",
|
type: "PropertyListItem",
|
||||||
props: { label: "Created", value: payment?.created ?? "Unknown" },
|
props: { label: "Created", value: payment?.created ?? "Unknown" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "details",
|
||||||
},
|
},
|
||||||
detailCurrency: {
|
detailCurrency: {
|
||||||
|
key: "detailCurrency",
|
||||||
type: "PropertyListItem",
|
type: "PropertyListItem",
|
||||||
props: {
|
props: {
|
||||||
label: "Currency",
|
label: "Currency",
|
||||||
value: (payment?.currency ?? "usd").toUpperCase(),
|
value: (payment?.currency ?? "usd").toUpperCase(),
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "details",
|
||||||
},
|
},
|
||||||
detailCustomer: {
|
detailCustomer: {
|
||||||
|
key: "detailCustomer",
|
||||||
type: "PropertyListItem",
|
type: "PropertyListItem",
|
||||||
props: { label: "Customer", value: payment?.customerId ?? "Guest" },
|
props: { label: "Customer", value: payment?.customerId ?? "Guest" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "details",
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
key: "actions",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "small" },
|
props: { direction: "horizontal", gap: "small" },
|
||||||
children:
|
children:
|
||||||
payment?.status === "succeeded"
|
payment?.status === "succeeded"
|
||||||
? ["refundBtn", "viewBtn"]
|
? ["refundBtn", "viewBtn"]
|
||||||
: ["viewBtn"],
|
: ["viewBtn"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
refundBtn: {
|
refundBtn: {
|
||||||
|
key: "refundBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Refund Payment",
|
label: "Refund Payment",
|
||||||
@@ -150,8 +175,10 @@ function createPaymentDetailSpec(
|
|||||||
type: "destructive",
|
type: "destructive",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "actions",
|
||||||
},
|
},
|
||||||
viewBtn: {
|
viewBtn: {
|
||||||
|
key: "viewBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "View in Dashboard",
|
label: "View in Dashboard",
|
||||||
@@ -160,34 +187,44 @@ function createPaymentDetailSpec(
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "actions",
|
||||||
},
|
},
|
||||||
refundsSection: {
|
refundsSection: {
|
||||||
|
key: "refundsSection",
|
||||||
type: "Accordion",
|
type: "Accordion",
|
||||||
props: {},
|
props: {},
|
||||||
children: ["refundsAccordion"],
|
children: ["refundsAccordion"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
refundsAccordion: {
|
refundsAccordion: {
|
||||||
|
key: "refundsAccordion",
|
||||||
type: "AccordionItem",
|
type: "AccordionItem",
|
||||||
props: {
|
props: {
|
||||||
title: `Refunds (${refunds?.total ?? 0})`,
|
title: `Refunds (${refunds?.total ?? 0})`,
|
||||||
defaultOpen: refundsList.length > 0,
|
defaultOpen: refundsList.length > 0,
|
||||||
},
|
},
|
||||||
children: refundsList.length > 0 ? ["refundsList"] : ["noRefunds"],
|
children: refundsList.length > 0 ? ["refundsList"] : ["noRefunds"],
|
||||||
|
parentKey: "refundsSection",
|
||||||
},
|
},
|
||||||
refundsList: {
|
refundsList: {
|
||||||
|
key: "refundsList",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "small" },
|
props: { direction: "vertical", gap: "small" },
|
||||||
children: refundsList.slice(0, 5).map((_, i) => `refund${i}`),
|
children: refundsList.slice(0, 5).map((_, i) => `refund${i}`),
|
||||||
|
parentKey: "refundsAccordion",
|
||||||
},
|
},
|
||||||
noRefunds: {
|
noRefunds: {
|
||||||
|
key: "noRefunds",
|
||||||
type: "Text",
|
type: "Text",
|
||||||
props: { content: "No refunds", color: "secondary" },
|
props: { content: "No refunds", color: "secondary" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "refundsAccordion",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
refundsList.slice(0, 5).forEach((r, i) => {
|
refundsList.slice(0, 5).forEach((r, i) => {
|
||||||
elements[`refund${i}`] = {
|
elements[`refund${i}`] = {
|
||||||
|
key: `refund${i}`,
|
||||||
type: "RefundCard",
|
type: "RefundCard",
|
||||||
props: {
|
props: {
|
||||||
amount: r.amount,
|
amount: r.amount,
|
||||||
@@ -196,6 +233,7 @@ function createPaymentDetailSpec(
|
|||||||
reason: r.reason,
|
reason: r.reason,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "refundsList",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,21 +38,28 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
|||||||
|
|
||||||
const elements: Spec["elements"] = {
|
const elements: Spec["elements"] = {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "large" },
|
props: { direction: "vertical", gap: "large" },
|
||||||
children: ["heading", "metrics", "filters", "list", "pagination"],
|
children: ["heading", "metrics", "filters", "list", "pagination"],
|
||||||
|
parentKey: null,
|
||||||
},
|
},
|
||||||
heading: {
|
heading: {
|
||||||
|
key: "heading",
|
||||||
type: "Heading",
|
type: "Heading",
|
||||||
props: { text: "Payments", size: "xlarge" },
|
props: { text: "Payments", size: "xlarge" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
metrics: {
|
metrics: {
|
||||||
|
key: "metrics",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "medium" },
|
props: { direction: "horizontal", gap: "medium" },
|
||||||
children: ["volumeMetric", "rateMetric", "countMetric"],
|
children: ["volumeMetric", "rateMetric", "countMetric"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
volumeMetric: {
|
volumeMetric: {
|
||||||
|
key: "volumeMetric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Total Volume",
|
label: "Total Volume",
|
||||||
@@ -60,8 +67,10 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "positive",
|
changeType: "positive",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
rateMetric: {
|
rateMetric: {
|
||||||
|
key: "rateMetric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Success Rate",
|
label: "Success Rate",
|
||||||
@@ -69,8 +78,10 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "positive",
|
changeType: "positive",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
countMetric: {
|
countMetric: {
|
||||||
|
key: "countMetric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Total Payments",
|
label: "Total Payments",
|
||||||
@@ -78,18 +89,24 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "neutral",
|
changeType: "neutral",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
|
key: "filters",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "small" },
|
props: { direction: "horizontal", gap: "small" },
|
||||||
children: ["refreshBtn", "exportBtn"],
|
children: ["refreshBtn", "exportBtn"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
refreshBtn: {
|
refreshBtn: {
|
||||||
|
key: "refreshBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: { label: "Refresh", action: "refreshPayments", type: "secondary" },
|
props: { label: "Refresh", action: "refreshPayments", type: "secondary" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "filters",
|
||||||
},
|
},
|
||||||
exportBtn: {
|
exportBtn: {
|
||||||
|
key: "exportBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Export CSV",
|
label: "Export CSV",
|
||||||
@@ -98,18 +115,24 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "filters",
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
|
key: "list",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "small" },
|
props: { direction: "vertical", gap: "small" },
|
||||||
children: paymentsList.slice(0, 10).map((_, i) => `payment${i}`),
|
children: paymentsList.slice(0, 10).map((_, i) => `payment${i}`),
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
|
key: "pagination",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "small" },
|
props: { direction: "horizontal", gap: "small" },
|
||||||
children: payments?.hasMore ? ["loadMore"] : [],
|
children: payments?.hasMore ? ["loadMore"] : [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
loadMore: {
|
loadMore: {
|
||||||
|
key: "loadMore",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Load More",
|
label: "Load More",
|
||||||
@@ -121,11 +144,13 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "pagination",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
paymentsList.slice(0, 10).forEach((p, i) => {
|
paymentsList.slice(0, 10).forEach((p, i) => {
|
||||||
elements[`payment${i}`] = {
|
elements[`payment${i}`] = {
|
||||||
|
key: `payment${i}`,
|
||||||
type: "PaymentCard",
|
type: "PaymentCard",
|
||||||
props: {
|
props: {
|
||||||
amount: 0,
|
amount: 0,
|
||||||
@@ -135,6 +160,7 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
|||||||
paymentId: p.id,
|
paymentId: p.id,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "list",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -49,21 +49,28 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
|||||||
|
|
||||||
const elements: Spec["elements"] = {
|
const elements: Spec["elements"] = {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "large" },
|
props: { direction: "vertical", gap: "large" },
|
||||||
children: ["heading", "metrics", "filters", "list", "pagination"],
|
children: ["heading", "metrics", "filters", "list", "pagination"],
|
||||||
|
parentKey: null,
|
||||||
},
|
},
|
||||||
heading: {
|
heading: {
|
||||||
|
key: "heading",
|
||||||
type: "Heading",
|
type: "Heading",
|
||||||
props: { text: "Products & Prices", size: "xlarge" },
|
props: { text: "Products & Prices", size: "xlarge" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
metrics: {
|
metrics: {
|
||||||
|
key: "metrics",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "medium" },
|
props: { direction: "horizontal", gap: "medium" },
|
||||||
children: ["productsMetric", "pricesMetric", "activeMetric"],
|
children: ["productsMetric", "pricesMetric", "activeMetric"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
productsMetric: {
|
productsMetric: {
|
||||||
|
key: "productsMetric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Total Products",
|
label: "Total Products",
|
||||||
@@ -71,8 +78,10 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "neutral",
|
changeType: "neutral",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
pricesMetric: {
|
pricesMetric: {
|
||||||
|
key: "pricesMetric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Total Prices",
|
label: "Total Prices",
|
||||||
@@ -80,8 +89,10 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "neutral",
|
changeType: "neutral",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
activeMetric: {
|
activeMetric: {
|
||||||
|
key: "activeMetric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Active Products",
|
label: "Active Products",
|
||||||
@@ -89,18 +100,24 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "positive",
|
changeType: "positive",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
|
key: "filters",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "small" },
|
props: { direction: "horizontal", gap: "small" },
|
||||||
children: ["refreshBtn", "createBtn"],
|
children: ["refreshBtn", "createBtn"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
refreshBtn: {
|
refreshBtn: {
|
||||||
|
key: "refreshBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: { label: "Refresh", action: "fetchProducts", type: "secondary" },
|
props: { label: "Refresh", action: "fetchProducts", type: "secondary" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "filters",
|
||||||
},
|
},
|
||||||
createBtn: {
|
createBtn: {
|
||||||
|
key: "createBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Create Product",
|
label: "Create Product",
|
||||||
@@ -109,18 +126,24 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "primary",
|
type: "primary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "filters",
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
|
key: "list",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "small" },
|
props: { direction: "vertical", gap: "small" },
|
||||||
children: productsList.slice(0, 10).map((_, i) => `product${i}`),
|
children: productsList.slice(0, 10).map((_, i) => `product${i}`),
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
|
key: "pagination",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "small" },
|
props: { direction: "horizontal", gap: "small" },
|
||||||
children: products?.hasMore ? ["loadMore"] : [],
|
children: products?.hasMore ? ["loadMore"] : [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
loadMore: {
|
loadMore: {
|
||||||
|
key: "loadMore",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Load More",
|
label: "Load More",
|
||||||
@@ -129,6 +152,7 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "pagination",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,12 +169,15 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
|||||||
: "No prices";
|
: "No prices";
|
||||||
|
|
||||||
elements[`product${i}`] = {
|
elements[`product${i}`] = {
|
||||||
|
key: `product${i}`,
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "small" },
|
props: { direction: "vertical", gap: "small" },
|
||||||
children: [`productCard${i}`],
|
children: [`productCard${i}`],
|
||||||
|
parentKey: "list",
|
||||||
};
|
};
|
||||||
|
|
||||||
elements[`productCard${i}`] = {
|
elements[`productCard${i}`] = {
|
||||||
|
key: `productCard${i}`,
|
||||||
type: "PropertyList",
|
type: "PropertyList",
|
||||||
props: { orientation: "vertical" },
|
props: { orientation: "vertical" },
|
||||||
children: [
|
children: [
|
||||||
@@ -159,30 +186,39 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
|||||||
`productPrice${i}`,
|
`productPrice${i}`,
|
||||||
`productStatus${i}`,
|
`productStatus${i}`,
|
||||||
],
|
],
|
||||||
|
parentKey: `product${i}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
elements[`productName${i}`] = {
|
elements[`productName${i}`] = {
|
||||||
|
key: `productName${i}`,
|
||||||
type: "PropertyListItem",
|
type: "PropertyListItem",
|
||||||
props: { label: "Name", value: p.name },
|
props: { label: "Name", value: p.name },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: `productCard${i}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
elements[`productDesc${i}`] = {
|
elements[`productDesc${i}`] = {
|
||||||
|
key: `productDesc${i}`,
|
||||||
type: "PropertyListItem",
|
type: "PropertyListItem",
|
||||||
props: { label: "Description", value: p.description || "No description" },
|
props: { label: "Description", value: p.description || "No description" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: `productCard${i}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
elements[`productPrice${i}`] = {
|
elements[`productPrice${i}`] = {
|
||||||
|
key: `productPrice${i}`,
|
||||||
type: "PropertyListItem",
|
type: "PropertyListItem",
|
||||||
props: { label: "Prices", value: priceDisplay },
|
props: { label: "Prices", value: priceDisplay },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: `productCard${i}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
elements[`productStatus${i}`] = {
|
elements[`productStatus${i}`] = {
|
||||||
|
key: `productStatus${i}`,
|
||||||
type: "PropertyListItem",
|
type: "PropertyListItem",
|
||||||
props: { label: "Status", value: p.active ? "Active" : "Archived" },
|
props: { label: "Status", value: p.active ? "Active" : "Archived" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: `productCard${i}`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
|
|
||||||
const elements: Spec["elements"] = {
|
const elements: Spec["elements"] = {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "large" },
|
props: { direction: "vertical", gap: "large" },
|
||||||
children: [
|
children: [
|
||||||
@@ -52,13 +53,17 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
"list",
|
"list",
|
||||||
"pagination",
|
"pagination",
|
||||||
],
|
],
|
||||||
|
parentKey: null,
|
||||||
},
|
},
|
||||||
heading: {
|
heading: {
|
||||||
|
key: "heading",
|
||||||
type: "Heading",
|
type: "Heading",
|
||||||
props: { text: "Subscriptions", size: "xlarge" },
|
props: { text: "Subscriptions", size: "xlarge" },
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
metrics: {
|
metrics: {
|
||||||
|
key: "metrics",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "medium" },
|
props: { direction: "horizontal", gap: "medium" },
|
||||||
children: [
|
children: [
|
||||||
@@ -67,8 +72,10 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
"pastDueMetric",
|
"pastDueMetric",
|
||||||
"canceledMetric",
|
"canceledMetric",
|
||||||
],
|
],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
activeMetric: {
|
activeMetric: {
|
||||||
|
key: "activeMetric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Active",
|
label: "Active",
|
||||||
@@ -76,8 +83,10 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "positive",
|
changeType: "positive",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
trialingMetric: {
|
trialingMetric: {
|
||||||
|
key: "trialingMetric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Trialing",
|
label: "Trialing",
|
||||||
@@ -85,8 +94,10 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "positive",
|
changeType: "positive",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
pastDueMetric: {
|
pastDueMetric: {
|
||||||
|
key: "pastDueMetric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Past Due",
|
label: "Past Due",
|
||||||
@@ -94,8 +105,10 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: subscriptions?.pastDue ? "negative" : "neutral",
|
changeType: subscriptions?.pastDue ? "negative" : "neutral",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
canceledMetric: {
|
canceledMetric: {
|
||||||
|
key: "canceledMetric",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: {
|
props: {
|
||||||
label: "Canceled",
|
label: "Canceled",
|
||||||
@@ -103,8 +116,10 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
changeType: "neutral",
|
changeType: "neutral",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "metrics",
|
||||||
},
|
},
|
||||||
alert: {
|
alert: {
|
||||||
|
key: "alert",
|
||||||
type: "Banner",
|
type: "Banner",
|
||||||
props: {
|
props: {
|
||||||
title: subscriptions?.pastDue
|
title: subscriptions?.pastDue
|
||||||
@@ -113,13 +128,17 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: subscriptions?.pastDue ? "caution" : "default",
|
type: subscriptions?.pastDue ? "caution" : "default",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
|
key: "filters",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "small" },
|
props: { direction: "horizontal", gap: "small" },
|
||||||
children: ["refreshBtn", "exportBtn"],
|
children: ["refreshBtn", "exportBtn"],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
refreshBtn: {
|
refreshBtn: {
|
||||||
|
key: "refreshBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Refresh",
|
label: "Refresh",
|
||||||
@@ -127,8 +146,10 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "filters",
|
||||||
},
|
},
|
||||||
exportBtn: {
|
exportBtn: {
|
||||||
|
key: "exportBtn",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Export CSV",
|
label: "Export CSV",
|
||||||
@@ -137,20 +158,26 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "filters",
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
|
key: "list",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "vertical", gap: "small" },
|
props: { direction: "vertical", gap: "small" },
|
||||||
children: subscriptionsList
|
children: subscriptionsList
|
||||||
.slice(0, 10)
|
.slice(0, 10)
|
||||||
.map((_, i) => `subscription${i}`),
|
.map((_, i) => `subscription${i}`),
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
|
key: "pagination",
|
||||||
type: "Stack",
|
type: "Stack",
|
||||||
props: { direction: "horizontal", gap: "small" },
|
props: { direction: "horizontal", gap: "small" },
|
||||||
children: subscriptions?.hasMore ? ["loadMore"] : [],
|
children: subscriptions?.hasMore ? ["loadMore"] : [],
|
||||||
|
parentKey: "root",
|
||||||
},
|
},
|
||||||
loadMore: {
|
loadMore: {
|
||||||
|
key: "loadMore",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {
|
props: {
|
||||||
label: "Load More",
|
label: "Load More",
|
||||||
@@ -159,11 +186,13 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
type: "secondary",
|
type: "secondary",
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "pagination",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
subscriptionsList.slice(0, 10).forEach((s, i) => {
|
subscriptionsList.slice(0, 10).forEach((s, i) => {
|
||||||
elements[`subscription${i}`] = {
|
elements[`subscription${i}`] = {
|
||||||
|
key: `subscription${i}`,
|
||||||
type: "SubscriptionCard",
|
type: "SubscriptionCard",
|
||||||
props: {
|
props: {
|
||||||
planName: s.planName,
|
planName: s.planName,
|
||||||
@@ -179,6 +208,7 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
|||||||
currentPeriodEnd: s.currentPeriodEnd,
|
currentPeriodEnd: s.currentPeriodEnd,
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
|
parentKey: "list",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
# @json-render/codegen
|
|
||||||
|
|
||||||
## 0.4.4
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- dd17549: remove key/parentKey from flat specs, RFC 6902 compliance for SpecStream
|
|
||||||
- Updated dependencies [dd17549]
|
|
||||||
- @json-render/core@0.4.4
|
|
||||||
|
|
||||||
## 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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/codegen",
|
"name": "@json-render/codegen",
|
||||||
"version": "0.4.4",
|
"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,15 +13,18 @@ describe("traverseSpec", () => {
|
|||||||
root: "root",
|
root: "root",
|
||||||
elements: {
|
elements: {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Card",
|
type: "Card",
|
||||||
props: {},
|
props: {},
|
||||||
children: ["child1", "child2"],
|
children: ["child1", "child2"],
|
||||||
},
|
},
|
||||||
child1: {
|
child1: {
|
||||||
|
key: "child1",
|
||||||
type: "Text",
|
type: "Text",
|
||||||
props: {},
|
props: {},
|
||||||
},
|
},
|
||||||
child2: {
|
child2: {
|
||||||
|
key: "child2",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: {},
|
props: {},
|
||||||
},
|
},
|
||||||
@@ -29,8 +32,8 @@ describe("traverseSpec", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const visited: string[] = [];
|
const visited: string[] = [];
|
||||||
traverseSpec(spec, (_element, key) => {
|
traverseSpec(spec, (element) => {
|
||||||
visited.push(key);
|
visited.push(element.key);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(visited).toEqual(["root", "child1", "child2"]);
|
expect(visited).toEqual(["root", "child1", "child2"]);
|
||||||
@@ -38,8 +41,8 @@ describe("traverseSpec", () => {
|
|||||||
|
|
||||||
it("handles empty spec", () => {
|
it("handles empty spec", () => {
|
||||||
const visited: string[] = [];
|
const visited: string[] = [];
|
||||||
traverseSpec(null as unknown as Spec, (_element, key) => {
|
traverseSpec(null as unknown as Spec, (element) => {
|
||||||
visited.push(key);
|
visited.push(element.key);
|
||||||
});
|
});
|
||||||
expect(visited).toEqual([]);
|
expect(visited).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -51,15 +54,18 @@ describe("collectUsedComponents", () => {
|
|||||||
root: "root",
|
root: "root",
|
||||||
elements: {
|
elements: {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Card",
|
type: "Card",
|
||||||
props: {},
|
props: {},
|
||||||
children: ["child1", "child2"],
|
children: ["child1", "child2"],
|
||||||
},
|
},
|
||||||
child1: {
|
child1: {
|
||||||
|
key: "child1",
|
||||||
type: "Text",
|
type: "Text",
|
||||||
props: {},
|
props: {},
|
||||||
},
|
},
|
||||||
child2: {
|
child2: {
|
||||||
|
key: "child2",
|
||||||
type: "Text",
|
type: "Text",
|
||||||
props: {},
|
props: {},
|
||||||
},
|
},
|
||||||
@@ -77,6 +83,7 @@ describe("collectDataPaths", () => {
|
|||||||
root: "root",
|
root: "root",
|
||||||
elements: {
|
elements: {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Metric",
|
type: "Metric",
|
||||||
props: { valuePath: "analytics/revenue" },
|
props: { valuePath: "analytics/revenue" },
|
||||||
},
|
},
|
||||||
@@ -92,6 +99,7 @@ describe("collectDataPaths", () => {
|
|||||||
root: "root",
|
root: "root",
|
||||||
elements: {
|
elements: {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Text",
|
type: "Text",
|
||||||
props: { content: { path: "user/name" } },
|
props: { content: { path: "user/name" } },
|
||||||
},
|
},
|
||||||
@@ -109,6 +117,7 @@ describe("collectActions", () => {
|
|||||||
root: "root",
|
root: "root",
|
||||||
elements: {
|
elements: {
|
||||||
root: {
|
root: {
|
||||||
|
key: "root",
|
||||||
type: "Button",
|
type: "Button",
|
||||||
props: { action: "submit_form" },
|
props: { action: "submit_form" },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,12 +4,7 @@ import type { Spec, UIElement } from "@json-render/core";
|
|||||||
* Visitor function for spec traversal
|
* Visitor function for spec traversal
|
||||||
*/
|
*/
|
||||||
export interface TreeVisitor {
|
export interface TreeVisitor {
|
||||||
(
|
(element: UIElement, depth: number, parent: UIElement | null): void;
|
||||||
element: UIElement,
|
|
||||||
key: string,
|
|
||||||
depth: number,
|
|
||||||
parent: UIElement | null,
|
|
||||||
): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,7 +25,7 @@ export function traverseSpec(
|
|||||||
const element = spec.elements[key];
|
const element = spec.elements[key];
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
visitor(element, key, depth, parent);
|
visitor(element, depth, parent);
|
||||||
|
|
||||||
if (element.children) {
|
if (element.children) {
|
||||||
for (const childKey of element.children) {
|
for (const childKey of element.children) {
|
||||||
@@ -48,7 +43,7 @@ export function traverseSpec(
|
|||||||
export function collectUsedComponents(spec: Spec): Set<string> {
|
export function collectUsedComponents(spec: Spec): Set<string> {
|
||||||
const components = new Set<string>();
|
const components = new Set<string>();
|
||||||
|
|
||||||
traverseSpec(spec, (element, _key) => {
|
traverseSpec(spec, (element) => {
|
||||||
components.add(element.type);
|
components.add(element.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,7 +56,7 @@ export function collectUsedComponents(spec: Spec): Set<string> {
|
|||||||
export function collectDataPaths(spec: Spec): Set<string> {
|
export function collectDataPaths(spec: Spec): Set<string> {
|
||||||
const paths = new Set<string>();
|
const paths = new Set<string>();
|
||||||
|
|
||||||
traverseSpec(spec, (element, _key) => {
|
traverseSpec(spec, (element) => {
|
||||||
// Check props for data paths
|
// Check props for data paths
|
||||||
for (const [propName, propValue] of Object.entries(element.props)) {
|
for (const [propName, propValue] of Object.entries(element.props)) {
|
||||||
// Check for path props (e.g., valuePath, dataPath, bindPath)
|
// Check for path props (e.g., valuePath, dataPath, bindPath)
|
||||||
@@ -146,7 +141,7 @@ function collectPathsFromCondition(
|
|||||||
export function collectActions(spec: Spec): Set<string> {
|
export function collectActions(spec: Spec): Set<string> {
|
||||||
const actions = new Set<string>();
|
const actions = new Set<string>();
|
||||||
|
|
||||||
traverseSpec(spec, (element, _key) => {
|
traverseSpec(spec, (element) => {
|
||||||
for (const propValue of Object.values(element.props)) {
|
for (const propValue of Object.values(element.props)) {
|
||||||
// Check for action prop (string action name)
|
// Check for action prop (string action name)
|
||||||
if (typeof propValue === "string" && propValue.startsWith("action:")) {
|
if (typeof propValue === "string" && propValue.startsWith("action:")) {
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
# @json-render/core
|
|
||||||
|
|
||||||
## 0.4.4
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- dd17549: remove key/parentKey from flat specs, RFC 6902 compliance for SpecStream
|
|
||||||
|
|
||||||
## 0.4.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 61ee8e5: include remove op in system prompt
|
|
||||||
|
|
||||||
## 0.4.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 54bce09: add defineRegistry function
|
|
||||||
@@ -116,16 +116,14 @@ while (streaming) {
|
|||||||
const finalSpec = compiler.getResult();
|
const finalSpec = compiler.getResult();
|
||||||
```
|
```
|
||||||
|
|
||||||
SpecStream format uses [RFC 6902 JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations (each line is a patch):
|
SpecStream format (each line is a JSON patch):
|
||||||
|
|
||||||
```jsonl
|
```jsonl
|
||||||
{"op":"add","path":"/root/type","value":"Card"}
|
{"op":"set","path":"/root/type","value":"Card"}
|
||||||
{"op":"add","path":"/root/props","value":{"title":"Hello"}}
|
{"op":"set","path":"/root/props","value":{"title":"Hello"}}
|
||||||
{"op":"add","path":"/root/children/0","value":{"type":"Button","props":{"label":"Click"}}}
|
{"op":"set","path":"/root/children/0","value":{"type":"Button","props":{"label":"Click"}}}
|
||||||
```
|
```
|
||||||
|
|
||||||
All six RFC 6902 operations are supported: `add`, `remove`, `replace`, `move`, `copy`, `test`.
|
|
||||||
|
|
||||||
### Low-Level Utilities
|
### Low-Level Utilities
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -136,8 +134,8 @@ import {
|
|||||||
} from "@json-render/core";
|
} from "@json-render/core";
|
||||||
|
|
||||||
// Parse a single line
|
// Parse a single line
|
||||||
const patch = parseSpecStreamLine('{"op":"add","path":"/root","value":{}}');
|
const patch = parseSpecStreamLine('{"op":"set","path":"/root","value":{}}');
|
||||||
// { op: "add", path: "/root", value: {} }
|
// { op: "set", path: "/root", value: {} }
|
||||||
|
|
||||||
// Apply a patch to an object
|
// Apply a patch to an object
|
||||||
const obj = {};
|
const obj = {};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/core",
|
"name": "@json-render/core",
|
||||||
"version": "0.4.4",
|
"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": [
|
||||||
|
|||||||
@@ -72,10 +72,12 @@ describe("createCatalog", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const validElement = {
|
const validElement = {
|
||||||
|
key: "1",
|
||||||
type: "text",
|
type: "text",
|
||||||
props: { content: "Hello" },
|
props: { content: "Hello" },
|
||||||
};
|
};
|
||||||
const invalidElement = {
|
const invalidElement = {
|
||||||
|
key: "1",
|
||||||
type: "text",
|
type: "text",
|
||||||
props: { content: 123 },
|
props: { content: 123 },
|
||||||
};
|
};
|
||||||
@@ -94,7 +96,7 @@ describe("createCatalog", () => {
|
|||||||
const validSpec = {
|
const validSpec = {
|
||||||
root: "1",
|
root: "1",
|
||||||
elements: {
|
elements: {
|
||||||
"1": { type: "text", props: { content: "Hello" } },
|
"1": { key: "1", type: "text", props: { content: "Hello" } },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,13 +120,16 @@ describe("createCatalog", () => {
|
|||||||
root: "card-1",
|
root: "card-1",
|
||||||
elements: {
|
elements: {
|
||||||
"card-1": {
|
"card-1": {
|
||||||
|
key: "card-1",
|
||||||
type: "card",
|
type: "card",
|
||||||
props: { title: "Hello" },
|
props: { title: "Hello" },
|
||||||
children: ["text-1"],
|
children: ["text-1"],
|
||||||
},
|
},
|
||||||
"text-1": {
|
"text-1": {
|
||||||
|
key: "text-1",
|
||||||
type: "text",
|
type: "text",
|
||||||
props: { content: "World" },
|
props: { content: "World" },
|
||||||
|
parentKey: "card-1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -142,7 +147,7 @@ describe("createCatalog", () => {
|
|||||||
const invalidSpec = {
|
const invalidSpec = {
|
||||||
root: "1",
|
root: "1",
|
||||||
elements: {
|
elements: {
|
||||||
"1": { type: "nonexistent", props: { content: "Hello" } },
|
"1": { key: "1", type: "nonexistent", props: { content: "Hello" } },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -393,6 +398,7 @@ describe("defineCatalog (new schema API)", () => {
|
|||||||
root: s.string(),
|
root: s.string(),
|
||||||
elements: s.record(
|
elements: s.record(
|
||||||
s.object({
|
s.object({
|
||||||
|
key: s.string(),
|
||||||
type: s.ref("catalog.components"),
|
type: s.ref("catalog.components"),
|
||||||
props: s.any(),
|
props: s.any(),
|
||||||
children: s.array(s.string()),
|
children: s.array(s.string()),
|
||||||
@@ -483,6 +489,7 @@ describe("defineCatalog (new schema API)", () => {
|
|||||||
root: "text-1",
|
root: "text-1",
|
||||||
elements: {
|
elements: {
|
||||||
"text-1": {
|
"text-1": {
|
||||||
|
key: "text-1",
|
||||||
type: "Text",
|
type: "Text",
|
||||||
props: { content: "Hello" },
|
props: { content: "Hello" },
|
||||||
children: [],
|
children: [],
|
||||||
@@ -551,6 +558,7 @@ describe("defineCatalog (new schema API)", () => {
|
|||||||
root: "text-1",
|
root: "text-1",
|
||||||
elements: {
|
elements: {
|
||||||
"text-1": {
|
"text-1": {
|
||||||
|
key: "text-1",
|
||||||
type: "Text",
|
type: "Text",
|
||||||
props: { content: "Hello" },
|
props: { content: "Hello" },
|
||||||
children: [],
|
children: [],
|
||||||
|
|||||||
@@ -143,9 +143,11 @@ export function createCatalog<
|
|||||||
const def = components[componentName]!;
|
const def = components[componentName]!;
|
||||||
|
|
||||||
return z.object({
|
return z.object({
|
||||||
|
key: z.string(),
|
||||||
type: z.literal(componentName as string),
|
type: z.literal(componentName as string),
|
||||||
props: def.props,
|
props: def.props,
|
||||||
children: z.array(z.string()).optional(),
|
children: z.array(z.string()).optional(),
|
||||||
|
parentKey: z.string().nullable().optional(),
|
||||||
visible: VisibilityConditionSchema.optional(),
|
visible: VisibilityConditionSchema.optional(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -155,9 +157,11 @@ export function createCatalog<
|
|||||||
|
|
||||||
if (componentSchemas.length === 0) {
|
if (componentSchemas.length === 0) {
|
||||||
elementSchema = z.object({
|
elementSchema = z.object({
|
||||||
|
key: z.string(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
props: z.record(z.string(), z.unknown()),
|
props: z.record(z.string(), z.unknown()),
|
||||||
children: z.array(z.string()).optional(),
|
children: z.array(z.string()).optional(),
|
||||||
|
parentKey: z.string().nullable().optional(),
|
||||||
visible: VisibilityConditionSchema.optional(),
|
visible: VisibilityConditionSchema.optional(),
|
||||||
}) as unknown as z.ZodType<UIElement>;
|
}) as unknown as z.ZodType<UIElement>;
|
||||||
} else if (componentSchemas.length === 1) {
|
} else if (componentSchemas.length === 1) {
|
||||||
@@ -493,23 +497,21 @@ export function generateSystemPrompt<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Output format
|
// Output format
|
||||||
lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):");
|
lines.push("OUTPUT FORMAT (JSONL):");
|
||||||
lines.push('{"op":"add","path":"/root","value":"element-key"}');
|
lines.push('{"op":"set","path":"/root","value":"element-key"}');
|
||||||
lines.push(
|
lines.push(
|
||||||
'{"op":"add","path":"/elements/key","value":{"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
|
||||||
lines.push("RULES:");
|
lines.push("RULES:");
|
||||||
const baseRules = [
|
const baseRules = [
|
||||||
'First line sets /root to root element key: {"op":"add","path":"/root","value":"<key>"}',
|
"First line sets /root to root element key",
|
||||||
'Add elements with /elements/{key}: {"op":"add","path":"/elements/<key>","value":{...}}',
|
"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: type, props",
|
"Each element needs: key, type, props",
|
||||||
"ONLY use props listed above - never invent new props",
|
"ONLY use props listed above - never invent new props",
|
||||||
];
|
];
|
||||||
const allRules = [...baseRules, ...customRules];
|
const allRules = [...baseRules, ...customRules];
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export type {
|
|||||||
DynamicNumber,
|
DynamicNumber,
|
||||||
DynamicBoolean,
|
DynamicBoolean,
|
||||||
UIElement,
|
UIElement,
|
||||||
FlatElement,
|
|
||||||
Spec,
|
Spec,
|
||||||
VisibilityCondition,
|
VisibilityCondition,
|
||||||
LogicExpression,
|
LogicExpression,
|
||||||
@@ -28,10 +27,8 @@ export {
|
|||||||
resolveDynamicValue,
|
resolveDynamicValue,
|
||||||
getByPath,
|
getByPath,
|
||||||
setByPath,
|
setByPath,
|
||||||
addByPath,
|
|
||||||
removeByPath,
|
|
||||||
findFormValue,
|
findFormValue,
|
||||||
// SpecStream - streaming format for building specs (RFC 6902)
|
// SpecStream - streaming format for building specs
|
||||||
parseSpecStreamLine,
|
parseSpecStreamLine,
|
||||||
applySpecStreamPatch,
|
applySpecStreamPatch,
|
||||||
compileSpecStream,
|
compileSpecStream,
|
||||||
|
|||||||
@@ -545,10 +545,10 @@ function generatePrompt<TDef extends SchemaDefinition, TCatalog>(
|
|||||||
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":"card-1"}
|
lines.push(`{"op":"set","path":"/root","value":"card-1"}
|
||||||
{"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Dashboard"},"children":["metric-1","chart-1"]}}
|
{"op":"set","path":"/elements/card-1","value":{"key":"card-1","type":"Card","props":{"title":"Dashboard"},"children":["metric-1","chart-1"],"parentKey":""}}
|
||||||
{"op":"add","path":"/elements/metric-1","value":{"type":"Metric","props":{"label":"Revenue","valuePath":"analytics.revenue","format":"currency"},"children":[]}}
|
{"op":"set","path":"/elements/metric-1","value":{"key":"metric-1","type":"Metric","props":{"label":"Revenue","valuePath":"analytics.revenue","format":"currency"},"children":[],"parentKey":"card-1"}}
|
||||||
{"op":"add","path":"/elements/chart-1","value":{"type":"Chart","props":{"type":"bar","dataPath":"analytics.salesByRegion"},"children":[]}}`);
|
{"op":"set","path":"/elements/chart-1","value":{"key":"chart-1","type":"Chart","props":{"type":"bar","dataPath":"analytics.salesByRegion"},"children":[],"parentKey":"card-1"}}`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
// Components section
|
// Components section
|
||||||
@@ -591,11 +591,12 @@ function generatePrompt<TDef extends SchemaDefinition, TCatalog>(
|
|||||||
lines.push("RULES:");
|
lines.push("RULES:");
|
||||||
const baseRules = [
|
const baseRules = [
|
||||||
"Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
|
"Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
|
||||||
'First line sets root: {"op":"add","path":"/root","value":"<root-key>"}',
|
'First line sets root: {"op":"set","path":"/root","value":"<root-key>"}',
|
||||||
'Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}',
|
'Then add each element: {"op":"set","path":"/elements/<key>","value":{...}}',
|
||||||
"ONLY use components listed above",
|
"ONLY use components listed above",
|
||||||
"Each element value needs: type, props, children (array of child keys)",
|
"Each element value needs: key, type, props, children (array of child keys), parentKey",
|
||||||
"Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')",
|
"Use unique keys (e.g., 'header', 'metric-1', 'chart-revenue')",
|
||||||
|
"Root element's parentKey is empty string, children reference their parent's key",
|
||||||
];
|
];
|
||||||
const allRules = [...baseRules, ...customRules];
|
const allRules = [...baseRules, ...customRules];
|
||||||
allRules.forEach((rule, i) => {
|
allRules.forEach((rule, i) => {
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import {
|
import { resolveDynamicValue, getByPath, setByPath } from "./types";
|
||||||
resolveDynamicValue,
|
|
||||||
getByPath,
|
|
||||||
setByPath,
|
|
||||||
addByPath,
|
|
||||||
removeByPath,
|
|
||||||
applySpecStreamPatch,
|
|
||||||
compileSpecStream,
|
|
||||||
createSpecStreamCompiler,
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
describe("getByPath", () => {
|
describe("getByPath", () => {
|
||||||
it("gets nested values with JSON pointer paths", () => {
|
it("gets nested values with JSON pointer paths", () => {
|
||||||
@@ -132,428 +123,3 @@ describe("resolveDynamicValue", () => {
|
|||||||
).toBeUndefined();
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// JSON Pointer (RFC 6901) escaping
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("JSON Pointer escaping (RFC 6901)", () => {
|
|
||||||
it("getByPath unescapes ~1 to /", () => {
|
|
||||||
const data = { "a/b": { c: 42 } };
|
|
||||||
expect(getByPath(data, "/a~1b/c")).toBe(42);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getByPath unescapes ~0 to ~", () => {
|
|
||||||
const data = { "a~b": 99 };
|
|
||||||
expect(getByPath(data, "/a~0b")).toBe(99);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getByPath handles combined ~0 and ~1 escaping", () => {
|
|
||||||
const data = { "a~/b": "found" };
|
|
||||||
expect(getByPath(data, "/a~0~1b")).toBe("found");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("setByPath unescapes ~1 to / in keys", () => {
|
|
||||||
const data: Record<string, unknown> = {};
|
|
||||||
setByPath(data, "/a~1b", "value");
|
|
||||||
expect(data["a/b"]).toBe("value");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("setByPath unescapes ~0 to ~ in keys", () => {
|
|
||||||
const data: Record<string, unknown> = {};
|
|
||||||
setByPath(data, "/a~0b", "value");
|
|
||||||
expect(data["a~b"]).toBe("value");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getByPath reads array elements properly", () => {
|
|
||||||
const data = { items: [10, 20, 30] };
|
|
||||||
expect(getByPath(data, "/items/0")).toBe(10);
|
|
||||||
expect(getByPath(data, "/items/2")).toBe(30);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// addByPath (RFC 6902 "add" semantics)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("addByPath", () => {
|
|
||||||
it("adds a property to an object", () => {
|
|
||||||
const data: Record<string, unknown> = { a: 1 };
|
|
||||||
addByPath(data, "/b", 2);
|
|
||||||
expect(data.b).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("replaces an existing object property (add semantics)", () => {
|
|
||||||
const data: Record<string, unknown> = { a: 1 };
|
|
||||||
addByPath(data, "/a", 99);
|
|
||||||
expect(data.a).toBe(99);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("inserts into an array at a specific index", () => {
|
|
||||||
const data: Record<string, unknown> = { arr: [1, 2, 3] };
|
|
||||||
addByPath(data, "/arr/1", 99);
|
|
||||||
expect(data.arr).toEqual([1, 99, 2, 3]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("appends to an array with - (end-of-array)", () => {
|
|
||||||
const data: Record<string, unknown> = { arr: [1, 2] };
|
|
||||||
addByPath(data, "/arr/-", 3);
|
|
||||||
expect(data.arr).toEqual([1, 2, 3]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("inserts at index 0 of an array", () => {
|
|
||||||
const data: Record<string, unknown> = { arr: ["b", "c"] };
|
|
||||||
addByPath(data, "/arr/0", "a");
|
|
||||||
expect(data.arr).toEqual(["a", "b", "c"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates intermediate objects", () => {
|
|
||||||
const data: Record<string, unknown> = {};
|
|
||||||
addByPath(data, "/x/y/z", "deep");
|
|
||||||
expect(
|
|
||||||
((data.x as Record<string, unknown>).y as Record<string, unknown>).z,
|
|
||||||
).toBe("deep");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// removeByPath (RFC 6902 "remove" semantics)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("removeByPath", () => {
|
|
||||||
it("deletes an object property", () => {
|
|
||||||
const data: Record<string, unknown> = { a: 1, b: 2 };
|
|
||||||
removeByPath(data, "/a");
|
|
||||||
expect(data).toEqual({ b: 2 });
|
|
||||||
expect("a" in data).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes an element from an array by index", () => {
|
|
||||||
const data: Record<string, unknown> = { arr: [1, 2, 3] };
|
|
||||||
removeByPath(data, "/arr/1");
|
|
||||||
expect(data.arr).toEqual([1, 3]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes nested properties", () => {
|
|
||||||
const data: Record<string, unknown> = { user: { name: "John", age: 30 } };
|
|
||||||
removeByPath(data, "/user/age");
|
|
||||||
expect(data.user).toEqual({ name: "John" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("is a no-op for non-existent paths", () => {
|
|
||||||
const data: Record<string, unknown> = { a: 1 };
|
|
||||||
removeByPath(data, "/b/c/d");
|
|
||||||
expect(data).toEqual({ a: 1 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// applySpecStreamPatch - RFC 6902 operations
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("applySpecStreamPatch", () => {
|
|
||||||
describe("add operation", () => {
|
|
||||||
it("adds a new object property", () => {
|
|
||||||
const obj: Record<string, unknown> = {};
|
|
||||||
applySpecStreamPatch(obj, { op: "add", path: "/name", value: "Alice" });
|
|
||||||
expect(obj.name).toBe("Alice");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds nested properties creating intermediates", () => {
|
|
||||||
const obj: Record<string, unknown> = {};
|
|
||||||
applySpecStreamPatch(obj, {
|
|
||||||
op: "add",
|
|
||||||
path: "/user/name",
|
|
||||||
value: "Bob",
|
|
||||||
});
|
|
||||||
expect((obj.user as Record<string, unknown>).name).toBe("Bob");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("inserts into array at index", () => {
|
|
||||||
const obj: Record<string, unknown> = { items: [1, 3] };
|
|
||||||
applySpecStreamPatch(obj, { op: "add", path: "/items/1", value: 2 });
|
|
||||||
expect(obj.items).toEqual([1, 2, 3]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("appends to array with -", () => {
|
|
||||||
const obj: Record<string, unknown> = { items: [1, 2] };
|
|
||||||
applySpecStreamPatch(obj, { op: "add", path: "/items/-", value: 3 });
|
|
||||||
expect(obj.items).toEqual([1, 2, 3]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("remove operation", () => {
|
|
||||||
it("removes an object property", () => {
|
|
||||||
const obj: Record<string, unknown> = { name: "Alice", age: 30 };
|
|
||||||
applySpecStreamPatch(obj, { op: "remove", path: "/age" });
|
|
||||||
expect(obj).toEqual({ name: "Alice" });
|
|
||||||
expect("age" in obj).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes from array by index", () => {
|
|
||||||
const obj: Record<string, unknown> = { items: ["a", "b", "c"] };
|
|
||||||
applySpecStreamPatch(obj, { op: "remove", path: "/items/1" });
|
|
||||||
expect(obj.items).toEqual(["a", "c"]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("replace operation", () => {
|
|
||||||
it("replaces an existing value", () => {
|
|
||||||
const obj: Record<string, unknown> = { name: "Alice" };
|
|
||||||
applySpecStreamPatch(obj, {
|
|
||||||
op: "replace",
|
|
||||||
path: "/name",
|
|
||||||
value: "Bob",
|
|
||||||
});
|
|
||||||
expect(obj.name).toBe("Bob");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("replaces nested values", () => {
|
|
||||||
const obj: Record<string, unknown> = { user: { name: "Alice" } };
|
|
||||||
applySpecStreamPatch(obj, {
|
|
||||||
op: "replace",
|
|
||||||
path: "/user/name",
|
|
||||||
value: "Bob",
|
|
||||||
});
|
|
||||||
expect((obj.user as Record<string, unknown>).name).toBe("Bob");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("move operation", () => {
|
|
||||||
it("moves a value from one path to another", () => {
|
|
||||||
const obj: Record<string, unknown> = { a: 1, b: 2 };
|
|
||||||
applySpecStreamPatch(obj, { op: "move", path: "/c", from: "/a" });
|
|
||||||
expect(obj).toEqual({ b: 2, c: 1 });
|
|
||||||
expect("a" in obj).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("moves nested values", () => {
|
|
||||||
const obj: Record<string, unknown> = {
|
|
||||||
source: { val: 42 },
|
|
||||||
target: {},
|
|
||||||
};
|
|
||||||
applySpecStreamPatch(obj, {
|
|
||||||
op: "move",
|
|
||||||
path: "/target/val",
|
|
||||||
from: "/source/val",
|
|
||||||
});
|
|
||||||
expect((obj.target as Record<string, unknown>).val).toBe(42);
|
|
||||||
expect("val" in (obj.source as Record<string, unknown>)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("is a no-op if from is missing", () => {
|
|
||||||
const obj: Record<string, unknown> = { a: 1 };
|
|
||||||
applySpecStreamPatch(obj, { op: "move", path: "/b" });
|
|
||||||
expect(obj).toEqual({ a: 1 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("copy operation", () => {
|
|
||||||
it("copies a value from one path to another", () => {
|
|
||||||
const obj: Record<string, unknown> = { a: 1 };
|
|
||||||
applySpecStreamPatch(obj, { op: "copy", path: "/b", from: "/a" });
|
|
||||||
expect(obj).toEqual({ a: 1, b: 1 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("copies complex values", () => {
|
|
||||||
const obj: Record<string, unknown> = {
|
|
||||||
source: { nested: { value: 42 } },
|
|
||||||
};
|
|
||||||
applySpecStreamPatch(obj, {
|
|
||||||
op: "copy",
|
|
||||||
path: "/dest",
|
|
||||||
from: "/source/nested",
|
|
||||||
});
|
|
||||||
expect(obj.dest).toEqual({ value: 42 });
|
|
||||||
// Source should still exist
|
|
||||||
expect((obj.source as Record<string, unknown>).nested).toEqual({
|
|
||||||
value: 42,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("is a no-op if from is missing", () => {
|
|
||||||
const obj: Record<string, unknown> = { a: 1 };
|
|
||||||
applySpecStreamPatch(obj, { op: "copy", path: "/b" });
|
|
||||||
expect(obj).toEqual({ a: 1 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("test operation", () => {
|
|
||||||
it("succeeds when values match", () => {
|
|
||||||
const obj: Record<string, unknown> = { name: "Alice" };
|
|
||||||
expect(() =>
|
|
||||||
applySpecStreamPatch(obj, {
|
|
||||||
op: "test",
|
|
||||||
path: "/name",
|
|
||||||
value: "Alice",
|
|
||||||
}),
|
|
||||||
).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("succeeds for matching objects", () => {
|
|
||||||
const obj: Record<string, unknown> = { user: { name: "Alice", age: 30 } };
|
|
||||||
expect(() =>
|
|
||||||
applySpecStreamPatch(obj, {
|
|
||||||
op: "test",
|
|
||||||
path: "/user",
|
|
||||||
value: { name: "Alice", age: 30 },
|
|
||||||
}),
|
|
||||||
).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("succeeds for matching arrays", () => {
|
|
||||||
const obj: Record<string, unknown> = { items: [1, 2, 3] };
|
|
||||||
expect(() =>
|
|
||||||
applySpecStreamPatch(obj, {
|
|
||||||
op: "test",
|
|
||||||
path: "/items",
|
|
||||||
value: [1, 2, 3],
|
|
||||||
}),
|
|
||||||
).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws when values do not match", () => {
|
|
||||||
const obj: Record<string, unknown> = { name: "Alice" };
|
|
||||||
expect(() =>
|
|
||||||
applySpecStreamPatch(obj, {
|
|
||||||
op: "test",
|
|
||||||
path: "/name",
|
|
||||||
value: "Bob",
|
|
||||||
}),
|
|
||||||
).toThrow('Test operation failed: value at "/name" does not match');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws when path does not exist", () => {
|
|
||||||
const obj: Record<string, unknown> = {};
|
|
||||||
expect(() =>
|
|
||||||
applySpecStreamPatch(obj, {
|
|
||||||
op: "test",
|
|
||||||
path: "/missing",
|
|
||||||
value: "anything",
|
|
||||||
}),
|
|
||||||
).toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// compileSpecStream
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("compileSpecStream", () => {
|
|
||||||
it("compiles a series of add patches", () => {
|
|
||||||
const stream = `{"op":"add","path":"/name","value":"Alice"}
|
|
||||||
{"op":"add","path":"/age","value":30}`;
|
|
||||||
const result = compileSpecStream(stream);
|
|
||||||
expect(result).toEqual({ name: "Alice", age: 30 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles remove operations", () => {
|
|
||||||
const stream = `{"op":"add","path":"/a","value":1}
|
|
||||||
{"op":"add","path":"/b","value":2}
|
|
||||||
{"op":"remove","path":"/a"}`;
|
|
||||||
const result = compileSpecStream(stream);
|
|
||||||
expect(result).toEqual({ b: 2 });
|
|
||||||
expect("a" in result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles replace operations", () => {
|
|
||||||
const stream = `{"op":"add","path":"/name","value":"Alice"}
|
|
||||||
{"op":"replace","path":"/name","value":"Bob"}`;
|
|
||||||
const result = compileSpecStream(stream);
|
|
||||||
expect(result).toEqual({ name: "Bob" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles move operations", () => {
|
|
||||||
const stream = `{"op":"add","path":"/old","value":"data"}
|
|
||||||
{"op":"move","path":"/new","from":"/old"}`;
|
|
||||||
const result = compileSpecStream(stream);
|
|
||||||
expect(result).toEqual({ new: "data" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles copy operations", () => {
|
|
||||||
const stream = `{"op":"add","path":"/original","value":"data"}
|
|
||||||
{"op":"copy","path":"/duplicate","from":"/original"}`;
|
|
||||||
const result = compileSpecStream(stream);
|
|
||||||
expect(result).toEqual({ original: "data", duplicate: "data" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("skips empty lines", () => {
|
|
||||||
const stream = `{"op":"add","path":"/a","value":1}
|
|
||||||
|
|
||||||
{"op":"add","path":"/b","value":2}`;
|
|
||||||
const result = compileSpecStream(stream);
|
|
||||||
expect(result).toEqual({ a: 1, b: 2 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses initial value", () => {
|
|
||||||
const stream = `{"op":"add","path":"/b","value":2}`;
|
|
||||||
const result = compileSpecStream(stream, { a: 1 });
|
|
||||||
expect(result).toEqual({ a: 1, b: 2 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// createSpecStreamCompiler
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("createSpecStreamCompiler", () => {
|
|
||||||
it("processes chunks incrementally", () => {
|
|
||||||
const compiler = createSpecStreamCompiler();
|
|
||||||
const { result, newPatches } = compiler.push(
|
|
||||||
'{"op":"add","path":"/name","value":"Alice"}\n',
|
|
||||||
);
|
|
||||||
expect(newPatches).toHaveLength(1);
|
|
||||||
expect(result).toEqual({ name: "Alice" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles partial lines across chunks", () => {
|
|
||||||
const compiler = createSpecStreamCompiler();
|
|
||||||
|
|
||||||
// First chunk is incomplete
|
|
||||||
const r1 = compiler.push('{"op":"add","path":"/na');
|
|
||||||
expect(r1.newPatches).toHaveLength(0);
|
|
||||||
|
|
||||||
// Complete the line
|
|
||||||
const r2 = compiler.push('me","value":"Alice"}\n');
|
|
||||||
expect(r2.newPatches).toHaveLength(1);
|
|
||||||
expect(r2.result).toEqual({ name: "Alice" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("processes remaining buffer on getResult", () => {
|
|
||||||
const compiler = createSpecStreamCompiler();
|
|
||||||
compiler.push('{"op":"add","path":"/name","value":"Alice"}');
|
|
||||||
// No newline, so not processed yet
|
|
||||||
const result = compiler.getResult();
|
|
||||||
expect(result).toEqual({ name: "Alice" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resets to clean state", () => {
|
|
||||||
const compiler = createSpecStreamCompiler();
|
|
||||||
compiler.push('{"op":"add","path":"/name","value":"Alice"}\n');
|
|
||||||
compiler.reset();
|
|
||||||
const result = compiler.getResult();
|
|
||||||
expect(result).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("tracks all applied patches", () => {
|
|
||||||
const compiler = createSpecStreamCompiler();
|
|
||||||
compiler.push('{"op":"add","path":"/a","value":1}\n');
|
|
||||||
compiler.push('{"op":"add","path":"/b","value":2}\n');
|
|
||||||
const patches = compiler.getPatches();
|
|
||||||
expect(patches).toHaveLength(2);
|
|
||||||
expect(patches[0]!.op).toBe("add");
|
|
||||||
expect(patches[1]!.op).toBe("add");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles all RFC 6902 operations", () => {
|
|
||||||
const compiler = createSpecStreamCompiler();
|
|
||||||
compiler.push('{"op":"add","path":"/x","value":1}\n');
|
|
||||||
compiler.push('{"op":"add","path":"/y","value":2}\n');
|
|
||||||
compiler.push('{"op":"move","path":"/z","from":"/x"}\n');
|
|
||||||
compiler.push('{"op":"copy","path":"/w","from":"/y"}\n');
|
|
||||||
compiler.push('{"op":"remove","path":"/y"}\n');
|
|
||||||
const result = compiler.getResult();
|
|
||||||
expect(result).toEqual({ z: 1, w: 2 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
+26
-207
@@ -53,29 +53,18 @@ export interface UIElement<
|
|||||||
T extends string = string,
|
T extends string = string,
|
||||||
P = Record<string, unknown>,
|
P = Record<string, unknown>,
|
||||||
> {
|
> {
|
||||||
|
/** Unique key for reconciliation */
|
||||||
|
key: string;
|
||||||
/** Component type from the catalog */
|
/** Component type from the catalog */
|
||||||
type: T;
|
type: T;
|
||||||
/** Component props */
|
/** Component props */
|
||||||
props: P;
|
props: P;
|
||||||
/** Child element keys (flat structure) */
|
/** Child element keys (flat structure) */
|
||||||
children?: string[];
|
children?: string[];
|
||||||
/** Visibility condition */
|
|
||||||
visible?: VisibilityCondition;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element with key and parentKey for use with flatToTree.
|
|
||||||
* When elements are in an array (not a keyed map), key and parentKey
|
|
||||||
* are needed to establish identity and parent-child relationships.
|
|
||||||
*/
|
|
||||||
export interface FlatElement<
|
|
||||||
T extends string = string,
|
|
||||||
P = Record<string, unknown>,
|
|
||||||
> extends UIElement<T, P> {
|
|
||||||
/** Unique key identifying this element */
|
|
||||||
key: string;
|
|
||||||
/** Parent element key (null for root) */
|
/** Parent element key (null for root) */
|
||||||
parentKey?: string | null;
|
parentKey?: string | null;
|
||||||
|
/** Visibility condition */
|
||||||
|
visible?: VisibilityCondition;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,20 +125,17 @@ export type ComponentSchema = z.ZodType<Record<string, unknown>>;
|
|||||||
export type ValidationMode = "strict" | "warn" | "ignore";
|
export type ValidationMode = "strict" | "warn" | "ignore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON patch operation types (RFC 6902)
|
* JSON patch operation types
|
||||||
*/
|
*/
|
||||||
export type PatchOp = "add" | "remove" | "replace" | "move" | "copy" | "test";
|
export type PatchOp = "add" | "remove" | "replace" | "set";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON patch operation (RFC 6902)
|
* JSON patch operation
|
||||||
*/
|
*/
|
||||||
export interface JsonPatch {
|
export interface JsonPatch {
|
||||||
op: PatchOp;
|
op: PatchOp;
|
||||||
path: string;
|
path: string;
|
||||||
/** Required for add, replace, test */
|
|
||||||
value?: unknown;
|
value?: unknown;
|
||||||
/** Required for move, copy (source location) */
|
|
||||||
from?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -171,30 +157,16 @@ export function resolveDynamicValue<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unescape a JSON Pointer token per RFC 6901 Section 4.
|
* Get a value from an object by JSON Pointer path
|
||||||
* ~1 is decoded to / and ~0 is decoded to ~ (order matters).
|
|
||||||
*/
|
|
||||||
function unescapeJsonPointer(token: string): string {
|
|
||||||
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a JSON Pointer path into unescaped segments.
|
|
||||||
*/
|
|
||||||
function parseJsonPointer(path: string): string[] {
|
|
||||||
const raw = path.startsWith("/") ? path.slice(1).split("/") : path.split("/");
|
|
||||||
return raw.map(unescapeJsonPointer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a value from an object by JSON Pointer path (RFC 6901)
|
|
||||||
*/
|
*/
|
||||||
export function getByPath(obj: unknown, path: string): unknown {
|
export function getByPath(obj: unknown, path: string): unknown {
|
||||||
if (!path || path === "/") {
|
if (!path || path === "/") {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
const segments = parseJsonPointer(path);
|
const segments = path.startsWith("/")
|
||||||
|
? path.slice(1).split("/")
|
||||||
|
: path.split("/");
|
||||||
|
|
||||||
let current: unknown = obj;
|
let current: unknown = obj;
|
||||||
|
|
||||||
@@ -203,10 +175,7 @@ export function getByPath(obj: unknown, path: string): unknown {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(current)) {
|
if (typeof current === "object") {
|
||||||
const index = parseInt(segment, 10);
|
|
||||||
current = current[index];
|
|
||||||
} else if (typeof current === "object") {
|
|
||||||
current = (current as Record<string, unknown>)[segment];
|
current = (current as Record<string, unknown>)[segment];
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -224,7 +193,7 @@ function isNumericIndex(str: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a value in an object by JSON Pointer path (RFC 6901).
|
* Set a value in an object by JSON Pointer path.
|
||||||
* Automatically creates arrays when the path segment is a numeric index.
|
* Automatically creates arrays when the path segment is a numeric index.
|
||||||
*/
|
*/
|
||||||
export function setByPath(
|
export function setByPath(
|
||||||
@@ -232,7 +201,9 @@ export function setByPath(
|
|||||||
path: string,
|
path: string,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): void {
|
): void {
|
||||||
const segments = parseJsonPointer(path);
|
const segments = path.startsWith("/")
|
||||||
|
? path.slice(1).split("/")
|
||||||
|
: path.split("/");
|
||||||
|
|
||||||
if (segments.length === 0) return;
|
if (segments.length === 0) return;
|
||||||
|
|
||||||
@@ -242,8 +213,7 @@ export function setByPath(
|
|||||||
const segment = segments[i]!;
|
const segment = segments[i]!;
|
||||||
const nextSegment = segments[i + 1];
|
const nextSegment = segments[i + 1];
|
||||||
const nextIsNumeric =
|
const nextIsNumeric =
|
||||||
nextSegment !== undefined &&
|
nextSegment !== undefined && isNumericIndex(nextSegment);
|
||||||
(isNumericIndex(nextSegment) || nextSegment === "-");
|
|
||||||
|
|
||||||
if (Array.isArray(current)) {
|
if (Array.isArray(current)) {
|
||||||
const index = parseInt(segment, 10);
|
const index = parseInt(segment, 10);
|
||||||
@@ -259,133 +229,15 @@ export function setByPath(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastSegment = segments[segments.length - 1]!;
|
|
||||||
if (Array.isArray(current)) {
|
|
||||||
if (lastSegment === "-") {
|
|
||||||
current.push(value);
|
|
||||||
} else {
|
|
||||||
const index = parseInt(lastSegment, 10);
|
|
||||||
current[index] = value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current[lastSegment] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a value per RFC 6902 "add" semantics.
|
|
||||||
* For objects: create-or-replace the member.
|
|
||||||
* For arrays: insert before the given index, or append if "-".
|
|
||||||
*/
|
|
||||||
export function addByPath(
|
|
||||||
obj: Record<string, unknown>,
|
|
||||||
path: string,
|
|
||||||
value: unknown,
|
|
||||||
): void {
|
|
||||||
const segments = parseJsonPointer(path);
|
|
||||||
|
|
||||||
if (segments.length === 0) return;
|
|
||||||
|
|
||||||
let current: Record<string, unknown> | unknown[] = obj;
|
|
||||||
|
|
||||||
for (let i = 0; i < segments.length - 1; i++) {
|
|
||||||
const segment = segments[i]!;
|
|
||||||
const nextSegment = segments[i + 1];
|
|
||||||
const nextIsNumeric =
|
|
||||||
nextSegment !== undefined &&
|
|
||||||
(isNumericIndex(nextSegment) || nextSegment === "-");
|
|
||||||
|
|
||||||
if (Array.isArray(current)) {
|
|
||||||
const index = parseInt(segment, 10);
|
|
||||||
if (current[index] === undefined || typeof current[index] !== "object") {
|
|
||||||
current[index] = nextIsNumeric ? [] : {};
|
|
||||||
}
|
|
||||||
current = current[index] as Record<string, unknown> | unknown[];
|
|
||||||
} else {
|
|
||||||
if (!(segment in current) || typeof current[segment] !== "object") {
|
|
||||||
current[segment] = nextIsNumeric ? [] : {};
|
|
||||||
}
|
|
||||||
current = current[segment] as Record<string, unknown> | unknown[];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastSegment = segments[segments.length - 1]!;
|
|
||||||
if (Array.isArray(current)) {
|
|
||||||
if (lastSegment === "-") {
|
|
||||||
current.push(value);
|
|
||||||
} else {
|
|
||||||
const index = parseInt(lastSegment, 10);
|
|
||||||
current.splice(index, 0, value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current[lastSegment] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a value per RFC 6902 "remove" semantics.
|
|
||||||
* For objects: delete the property.
|
|
||||||
* For arrays: splice out the element at the given index.
|
|
||||||
*/
|
|
||||||
export function removeByPath(obj: Record<string, unknown>, path: string): void {
|
|
||||||
const segments = parseJsonPointer(path);
|
|
||||||
|
|
||||||
if (segments.length === 0) return;
|
|
||||||
|
|
||||||
let current: Record<string, unknown> | unknown[] = obj;
|
|
||||||
|
|
||||||
for (let i = 0; i < segments.length - 1; i++) {
|
|
||||||
const segment = segments[i]!;
|
|
||||||
|
|
||||||
if (Array.isArray(current)) {
|
|
||||||
const index = parseInt(segment, 10);
|
|
||||||
if (current[index] === undefined || typeof current[index] !== "object") {
|
|
||||||
return; // path does not exist
|
|
||||||
}
|
|
||||||
current = current[index] as Record<string, unknown> | unknown[];
|
|
||||||
} else {
|
|
||||||
if (!(segment in current) || typeof current[segment] !== "object") {
|
|
||||||
return; // path does not exist
|
|
||||||
}
|
|
||||||
current = current[segment] as Record<string, unknown> | unknown[];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastSegment = segments[segments.length - 1]!;
|
const lastSegment = segments[segments.length - 1]!;
|
||||||
if (Array.isArray(current)) {
|
if (Array.isArray(current)) {
|
||||||
const index = parseInt(lastSegment, 10);
|
const index = parseInt(lastSegment, 10);
|
||||||
if (index >= 0 && index < current.length) {
|
current[index] = value;
|
||||||
current.splice(index, 1);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
delete current[lastSegment];
|
current[lastSegment] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Deep equality check for RFC 6902 "test" operation.
|
|
||||||
*/
|
|
||||||
function deepEqual(a: unknown, b: unknown): boolean {
|
|
||||||
if (a === b) return true;
|
|
||||||
if (a === null || b === null) return false;
|
|
||||||
if (typeof a !== typeof b) return false;
|
|
||||||
if (typeof a !== "object") return false;
|
|
||||||
|
|
||||||
if (Array.isArray(a)) {
|
|
||||||
if (!Array.isArray(b)) return false;
|
|
||||||
if (a.length !== b.length) return false;
|
|
||||||
return a.every((item, i) => deepEqual(item, b[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
const aObj = a as Record<string, unknown>;
|
|
||||||
const bObj = b as Record<string, unknown>;
|
|
||||||
const aKeys = Object.keys(aObj);
|
|
||||||
const bKeys = Object.keys(bObj);
|
|
||||||
|
|
||||||
if (aKeys.length !== bKeys.length) return false;
|
|
||||||
return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a form value from params and/or data.
|
* Find a form value from params and/or data.
|
||||||
* Useful in action handlers to locate form input values regardless of path format.
|
* Useful in action handlers to locate form input values regardless of path format.
|
||||||
@@ -482,50 +334,17 @@ export function parseSpecStreamLine(line: string): SpecStreamLine | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply a single RFC 6902 JSON Patch operation to an object.
|
* Apply a single SpecStream patch to an object.
|
||||||
* Mutates the object in place.
|
* Mutates the object in place.
|
||||||
*
|
|
||||||
* Supports all six RFC 6902 operations: add, remove, replace, move, copy, test.
|
|
||||||
*
|
|
||||||
* @throws {Error} If a "test" operation fails (value mismatch).
|
|
||||||
*/
|
*/
|
||||||
export function applySpecStreamPatch<T extends Record<string, unknown>>(
|
export function applySpecStreamPatch<T extends Record<string, unknown>>(
|
||||||
obj: T,
|
obj: T,
|
||||||
patch: SpecStreamLine,
|
patch: SpecStreamLine,
|
||||||
): T {
|
): T {
|
||||||
switch (patch.op) {
|
if (patch.op === "set" || patch.op === "add" || patch.op === "replace") {
|
||||||
case "add":
|
setByPath(obj, patch.path, patch.value);
|
||||||
addByPath(obj, patch.path, patch.value);
|
} else if (patch.op === "remove") {
|
||||||
break;
|
setByPath(obj, patch.path, undefined);
|
||||||
case "replace":
|
|
||||||
// RFC 6902: target must exist. For streaming tolerance we set regardless.
|
|
||||||
setByPath(obj, patch.path, patch.value);
|
|
||||||
break;
|
|
||||||
case "remove":
|
|
||||||
removeByPath(obj, patch.path);
|
|
||||||
break;
|
|
||||||
case "move": {
|
|
||||||
if (!patch.from) break;
|
|
||||||
const moveValue = getByPath(obj, patch.from);
|
|
||||||
removeByPath(obj, patch.from);
|
|
||||||
addByPath(obj, patch.path, moveValue);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "copy": {
|
|
||||||
if (!patch.from) break;
|
|
||||||
const copyValue = getByPath(obj, patch.from);
|
|
||||||
addByPath(obj, patch.path, copyValue);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "test": {
|
|
||||||
const actual = getByPath(obj, patch.path);
|
|
||||||
if (!deepEqual(actual, patch.value)) {
|
|
||||||
throw new Error(
|
|
||||||
`Test operation failed: value at "${patch.path}" does not match`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
@@ -535,8 +354,8 @@ export function applySpecStreamPatch<T extends Record<string, unknown>>(
|
|||||||
* Each line should be a patch operation.
|
* Each line should be a patch operation.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const stream = `{"op":"add","path":"/name","value":"Alice"}
|
* const stream = `{"op":"set","path":"/name","value":"Alice"}
|
||||||
* {"op":"add","path":"/age","value":30}`;
|
* {"op":"set","path":"/age","value":30}`;
|
||||||
* const result = compileSpecStream(stream);
|
* const result = compileSpecStream(stream);
|
||||||
* // { name: "Alice", age: 30 }
|
* // { name: "Alice", age: 30 }
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
# @json-render/react
|
|
||||||
|
|
||||||
## 0.4.4
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- dd17549: remove key/parentKey from flat specs, RFC 6902 compliance for SpecStream
|
|
||||||
- Updated dependencies [dd17549]
|
|
||||||
- @json-render/core@0.4.4
|
|
||||||
|
|
||||||
## 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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/react",
|
"name": "@json-render/react",
|
||||||
"version": "0.4.4",
|
"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": [
|
||||||
|
|||||||
+47
-105
@@ -1,18 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback, useRef, useEffect } from "react";
|
import { useState, useCallback, useRef, useEffect } from "react";
|
||||||
import type {
|
import type { Spec, UIElement, JsonPatch } from "@json-render/core";
|
||||||
Spec,
|
import { setByPath } from "@json-render/core";
|
||||||
UIElement,
|
|
||||||
FlatElement,
|
|
||||||
JsonPatch,
|
|
||||||
} from "@json-render/core";
|
|
||||||
import {
|
|
||||||
setByPath,
|
|
||||||
getByPath,
|
|
||||||
addByPath,
|
|
||||||
removeByPath,
|
|
||||||
} from "@json-render/core";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a single JSON patch line
|
* Parse a single JSON patch line
|
||||||
@@ -30,104 +20,56 @@ function parsePatchLine(line: string): JsonPatch | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a value at a spec path (for add/replace operations).
|
* Apply a JSON patch to the current spec
|
||||||
*/
|
|
||||||
function setSpecValue(newSpec: Spec, path: string, value: unknown): void {
|
|
||||||
if (path === "/root") {
|
|
||||||
newSpec.root = value as string;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path.startsWith("/elements/")) {
|
|
||||||
const pathParts = path.slice("/elements/".length).split("/");
|
|
||||||
const elementKey = pathParts[0];
|
|
||||||
if (!elementKey) return;
|
|
||||||
|
|
||||||
if (pathParts.length === 1) {
|
|
||||||
newSpec.elements[elementKey] = value as UIElement;
|
|
||||||
} else {
|
|
||||||
const element = newSpec.elements[elementKey];
|
|
||||||
if (element) {
|
|
||||||
const propPath = "/" + pathParts.slice(1).join("/");
|
|
||||||
const newElement = { ...element };
|
|
||||||
setByPath(
|
|
||||||
newElement as unknown as Record<string, unknown>,
|
|
||||||
propPath,
|
|
||||||
value,
|
|
||||||
);
|
|
||||||
newSpec.elements[elementKey] = newElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a value at a spec path.
|
|
||||||
*/
|
|
||||||
function removeSpecValue(newSpec: Spec, path: string): void {
|
|
||||||
if (path.startsWith("/elements/")) {
|
|
||||||
const pathParts = path.slice("/elements/".length).split("/");
|
|
||||||
const elementKey = pathParts[0];
|
|
||||||
if (!elementKey) return;
|
|
||||||
|
|
||||||
if (pathParts.length === 1) {
|
|
||||||
const { [elementKey]: _, ...rest } = newSpec.elements;
|
|
||||||
newSpec.elements = rest;
|
|
||||||
} else {
|
|
||||||
const element = newSpec.elements[elementKey];
|
|
||||||
if (element) {
|
|
||||||
const propPath = "/" + pathParts.slice(1).join("/");
|
|
||||||
const newElement = { ...element };
|
|
||||||
removeByPath(
|
|
||||||
newElement as unknown as Record<string, unknown>,
|
|
||||||
propPath,
|
|
||||||
);
|
|
||||||
newSpec.elements[elementKey] = newElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a value at a spec path.
|
|
||||||
*/
|
|
||||||
function getSpecValue(spec: Spec, path: string): unknown {
|
|
||||||
if (path === "/root") return spec.root;
|
|
||||||
return getByPath(spec as unknown as Record<string, unknown>, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply an RFC 6902 JSON patch to the current spec.
|
|
||||||
* Supports add, remove, replace, move, copy, and test operations.
|
|
||||||
*/
|
*/
|
||||||
function applyPatch(spec: Spec, patch: JsonPatch): Spec {
|
function applyPatch(spec: Spec, patch: JsonPatch): Spec {
|
||||||
const newSpec = { ...spec, elements: { ...spec.elements } };
|
const newSpec = { ...spec, elements: { ...spec.elements } };
|
||||||
|
|
||||||
switch (patch.op) {
|
switch (patch.op) {
|
||||||
|
case "set":
|
||||||
case "add":
|
case "add":
|
||||||
case "replace": {
|
case "replace": {
|
||||||
setSpecValue(newSpec, patch.path, patch.value);
|
// Handle root path
|
||||||
|
if (patch.path === "/root") {
|
||||||
|
newSpec.root = patch.value as string;
|
||||||
|
return newSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle elements paths
|
||||||
|
if (patch.path.startsWith("/elements/")) {
|
||||||
|
const pathParts = patch.path.slice("/elements/".length).split("/");
|
||||||
|
const elementKey = pathParts[0];
|
||||||
|
|
||||||
|
if (!elementKey) return newSpec;
|
||||||
|
|
||||||
|
if (pathParts.length === 1) {
|
||||||
|
// Setting entire element
|
||||||
|
newSpec.elements[elementKey] = patch.value as UIElement;
|
||||||
|
} else {
|
||||||
|
// Setting property of element
|
||||||
|
const element = newSpec.elements[elementKey];
|
||||||
|
if (element) {
|
||||||
|
const propPath = "/" + pathParts.slice(1).join("/");
|
||||||
|
const newElement = { ...element };
|
||||||
|
setByPath(
|
||||||
|
newElement as unknown as Record<string, unknown>,
|
||||||
|
propPath,
|
||||||
|
patch.value,
|
||||||
|
);
|
||||||
|
newSpec.elements[elementKey] = newElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "remove": {
|
case "remove": {
|
||||||
removeSpecValue(newSpec, patch.path);
|
if (patch.path.startsWith("/elements/")) {
|
||||||
break;
|
const elementKey = patch.path.slice("/elements/".length).split("/")[0];
|
||||||
}
|
if (elementKey) {
|
||||||
case "move": {
|
const { [elementKey]: _, ...rest } = newSpec.elements;
|
||||||
if (!patch.from) break;
|
newSpec.elements = rest;
|
||||||
const moveValue = getSpecValue(newSpec, patch.from);
|
}
|
||||||
removeSpecValue(newSpec, patch.from);
|
}
|
||||||
setSpecValue(newSpec, patch.path, moveValue);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "copy": {
|
|
||||||
if (!patch.from) break;
|
|
||||||
const copyValue = getSpecValue(newSpec, patch.from);
|
|
||||||
setSpecValue(newSpec, patch.path, copyValue);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "test": {
|
|
||||||
// test is a no-op for rendering purposes (validation only)
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,18 +236,18 @@ export function useUIStream({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a flat element list to a Spec.
|
* Convert a flat element list to a Spec
|
||||||
* Input elements use key/parentKey to establish identity and relationships.
|
|
||||||
* Output spec uses the map-based format where key is the map entry key
|
|
||||||
* and parent-child relationships are expressed through children arrays.
|
|
||||||
*/
|
*/
|
||||||
export function flatToTree(elements: FlatElement[]): Spec {
|
export function flatToTree(
|
||||||
|
elements: Array<UIElement & { parentKey?: string | null }>,
|
||||||
|
): Spec {
|
||||||
const elementMap: Record<string, UIElement> = {};
|
const elementMap: Record<string, UIElement> = {};
|
||||||
let root = "";
|
let root = "";
|
||||||
|
|
||||||
// First pass: add all elements to map
|
// First pass: add all elements to map
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
elementMap[element.key] = {
|
elementMap[element.key] = {
|
||||||
|
key: element.key,
|
||||||
type: element.type,
|
type: element.type,
|
||||||
props: element.props,
|
props: element.props,
|
||||||
children: [],
|
children: [],
|
||||||
|
|||||||
@@ -15,12 +15,16 @@ export const schema = defineSchema((s) => ({
|
|||||||
/** Flat map of elements by key */
|
/** Flat map of elements by key */
|
||||||
elements: s.record(
|
elements: s.record(
|
||||||
s.object({
|
s.object({
|
||||||
|
/** Unique key for this element */
|
||||||
|
key: s.string(),
|
||||||
/** Component type from catalog */
|
/** Component type from catalog */
|
||||||
type: s.ref("catalog.components"),
|
type: s.ref("catalog.components"),
|
||||||
/** Component props */
|
/** Component props */
|
||||||
props: s.propsOf("catalog.components"),
|
props: s.propsOf("catalog.components"),
|
||||||
/** Child element keys (flat reference) */
|
/** Child element keys (flat reference) */
|
||||||
children: s.array(s.string()),
|
children: s.array(s.string()),
|
||||||
|
/** Parent element key (null for root) */
|
||||||
|
parentKey: s.string(),
|
||||||
/** Visibility condition */
|
/** Visibility condition */
|
||||||
visible: s.any(),
|
visible: s.any(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
# @json-render/remotion
|
|
||||||
|
|
||||||
## 0.4.4
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- dd17549: remove key/parentKey from flat specs, RFC 6902 compliance for SpecStream
|
|
||||||
- Updated dependencies [dd17549]
|
|
||||||
- @json-render/core@0.4.4
|
|
||||||
|
|
||||||
## 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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/remotion",
|
"name": "@json-render/remotion",
|
||||||
"version": "0.4.4",
|
"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,12 +25,11 @@ function remotionPromptTemplate(context: PromptContext): string {
|
|||||||
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":"/composition","value":{"id":"intro","fps":30,"width":1920,"height":1080,"durationInFrames":300}}
|
lines.push(`{"op":"set","path":"/composition","value":{"id":"intro","fps":30,"width":1920,"height":1080,"durationInFrames":300}}
|
||||||
{"op":"add","path":"/tracks","value":[{"id":"main","name":"Main","type":"video","enabled":true},{"id":"overlay","name":"Overlay","type":"overlay","enabled":true}]}
|
{"op":"set","path":"/tracks","value":[{"id":"main","name":"Main","type":"video","enabled":true},{"id":"overlay","name":"Overlay","type":"overlay","enabled":true}]}
|
||||||
{"op":"add","path":"/clips","value":[]}
|
{"op":"set","path":"/clips/0","value":{"id":"clip-1","trackId":"main","component":"TitleCard","props":{"title":"Welcome","subtitle":"Getting Started"},"from":0,"durationInFrames":90,"transitionIn":{"type":"fade","durationInFrames":15},"transitionOut":{"type":"fade","durationInFrames":15},"motion":{"enter":{"opacity":0,"y":50,"scale":0.9,"duration":25},"spring":{"damping":15}}}}
|
||||||
{"op":"add","path":"/clips/-","value":{"id":"clip-1","trackId":"main","component":"TitleCard","props":{"title":"Welcome","subtitle":"Getting Started"},"from":0,"durationInFrames":90,"transitionIn":{"type":"fade","durationInFrames":15},"transitionOut":{"type":"fade","durationInFrames":15},"motion":{"enter":{"opacity":0,"y":50,"scale":0.9,"duration":25},"spring":{"damping":15}}}}
|
{"op":"set","path":"/clips/1","value":{"id":"clip-2","trackId":"main","component":"TitleCard","props":{"title":"Features"},"from":90,"durationInFrames":90,"motion":{"enter":{"opacity":0,"x":-100,"duration":20},"exit":{"opacity":0,"x":100,"duration":15}}}}
|
||||||
{"op":"add","path":"/clips/-","value":{"id":"clip-2","trackId":"main","component":"TitleCard","props":{"title":"Features"},"from":90,"durationInFrames":90,"motion":{"enter":{"opacity":0,"x":-100,"duration":20},"exit":{"opacity":0,"x":100,"duration":15}}}}
|
{"op":"set","path":"/audio","value":{"tracks":[]}}`);
|
||||||
{"op":"add","path":"/audio","value":{"tracks":[]}}`);
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
@@ -108,10 +107,10 @@ function remotionPromptTemplate(context: PromptContext): string {
|
|||||||
lines.push("RULES:");
|
lines.push("RULES:");
|
||||||
const baseRules = [
|
const baseRules = [
|
||||||
"Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
|
"Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
|
||||||
'First add /composition with {id, fps:30, width:1920, height:1080, durationInFrames}: {"op":"add","path":"/composition","value":{...}}',
|
"First set /composition with {id, fps:30, width:1920, height:1080, durationInFrames}",
|
||||||
'Then add /tracks array with video/overlay tracks: {"op":"add","path":"/tracks","value":[...]}',
|
"Then set /tracks array with video/overlay tracks",
|
||||||
'Then add each clip by appending to the array: {"op":"add","path":"/clips/-","value":{...}}',
|
"Then set each clip: /clips/0, /clips/1, etc.",
|
||||||
'Finally add /audio with {tracks:[]}: {"op":"add","path":"/audio","value":{...}}',
|
"Finally set /audio with {tracks:[]}",
|
||||||
"ONLY use components listed above",
|
"ONLY use components listed above",
|
||||||
"fps is always 30 (1 second = 30 frames, 10 seconds = 300 frames)",
|
"fps is always 30 (1 second = 30 frames, 10 seconds = 300 frames)",
|
||||||
'Clips on "main" track flow sequentially (from = previous clip\'s from + durationInFrames)',
|
'Clips on "main" track flow sequentially (from = previous clip\'s from + durationInFrames)',
|
||||||
|
|||||||
Generated
+25
-4
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user