mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 04:18:15 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7407fe8302 |
@@ -275,11 +275,9 @@ export const openapiCatalog = createCatalog({
|
||||
}
|
||||
|
||||
interface SpecElement {
|
||||
key: string;
|
||||
type: string;
|
||||
props: Record<string, unknown>;
|
||||
children: string[];
|
||||
parentKey: string;
|
||||
}
|
||||
|
||||
function schemaToSpec(
|
||||
@@ -295,7 +293,6 @@ function schemaToSpec(
|
||||
|
||||
if (schema.enum) {
|
||||
elements.set(key, {
|
||||
key,
|
||||
type: 'EnumField',
|
||||
props: {
|
||||
name,
|
||||
@@ -306,11 +303,9 @@ function schemaToSpec(
|
||||
defaultValue: schema.default as string,
|
||||
},
|
||||
children: [],
|
||||
parentKey,
|
||||
});
|
||||
} else if (schema.type === 'string') {
|
||||
elements.set(key, {
|
||||
key,
|
||||
type: 'StringField',
|
||||
props: {
|
||||
name,
|
||||
@@ -323,11 +318,9 @@ function schemaToSpec(
|
||||
defaultValue: schema.default as string,
|
||||
},
|
||||
children: [],
|
||||
parentKey,
|
||||
});
|
||||
} else if (schema.type === 'integer' || schema.type === 'number') {
|
||||
elements.set(key, {
|
||||
key,
|
||||
type: 'NumberField',
|
||||
props: {
|
||||
name,
|
||||
@@ -340,11 +333,9 @@ function schemaToSpec(
|
||||
defaultValue: schema.default as number,
|
||||
},
|
||||
children: [],
|
||||
parentKey,
|
||||
});
|
||||
} else if (schema.type === 'boolean') {
|
||||
elements.set(key, {
|
||||
key,
|
||||
type: 'BooleanField',
|
||||
props: {
|
||||
name,
|
||||
@@ -353,7 +344,6 @@ function schemaToSpec(
|
||||
defaultValue: schema.default as boolean,
|
||||
},
|
||||
children: [],
|
||||
parentKey,
|
||||
});
|
||||
} else if (schema.type === 'array' && schema.items) {
|
||||
const childKeys: string[] = [];
|
||||
@@ -361,7 +351,6 @@ function schemaToSpec(
|
||||
childKeys.push(itemKey);
|
||||
|
||||
elements.set(key, {
|
||||
key,
|
||||
type: 'ArrayField',
|
||||
props: {
|
||||
name,
|
||||
@@ -369,7 +358,6 @@ function schemaToSpec(
|
||||
description: schema.description,
|
||||
},
|
||||
children: childKeys,
|
||||
parentKey,
|
||||
});
|
||||
} else if (schema.type === 'object' && schema.properties) {
|
||||
const childKeys: string[] = [];
|
||||
@@ -386,7 +374,6 @@ function schemaToSpec(
|
||||
}
|
||||
|
||||
elements.set(key, {
|
||||
key,
|
||||
type: 'ObjectField',
|
||||
props: {
|
||||
name,
|
||||
@@ -394,7 +381,6 @@ function schemaToSpec(
|
||||
description: schema.description,
|
||||
},
|
||||
children: childKeys,
|
||||
parentKey,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -428,7 +414,6 @@ export function operationToSpec(
|
||||
}
|
||||
|
||||
elements.set(rootKey, {
|
||||
key: rootKey,
|
||||
type: 'Form',
|
||||
props: {
|
||||
operationId,
|
||||
@@ -438,7 +423,6 @@ export function operationToSpec(
|
||||
description,
|
||||
},
|
||||
children: childKeys,
|
||||
parentKey: '',
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -98,25 +98,19 @@ export default function SchemasPage() {
|
||||
"root": "card-1",
|
||||
"elements": {
|
||||
"card-1": {
|
||||
"key": "card-1",
|
||||
"type": "Card",
|
||||
"props": { "title": "Dashboard" },
|
||||
"children": ["text-1", "button-1"],
|
||||
"parentKey": ""
|
||||
"children": ["text-1", "button-1"]
|
||||
},
|
||||
"text-1": {
|
||||
"key": "text-1",
|
||||
"type": "Text",
|
||||
"props": { "content": "Welcome, $data.user.name" },
|
||||
"children": [],
|
||||
"parentKey": "card-1"
|
||||
"children": []
|
||||
},
|
||||
"button-1": {
|
||||
"key": "button-1",
|
||||
"type": "Button",
|
||||
"props": { "label": "Click me" },
|
||||
"children": [],
|
||||
"parentKey": "card-1"
|
||||
"children": []
|
||||
}
|
||||
}
|
||||
}`}</Code>
|
||||
@@ -129,11 +123,9 @@ export default function SchemasPage() {
|
||||
structure:
|
||||
</p>
|
||||
<Code lang="typescript">{`interface Element {
|
||||
key: string; // Unique identifier
|
||||
type: string; // Component type from catalog
|
||||
props: Record<string, any>; // Component properties
|
||||
children: string[]; // Array of child element keys
|
||||
parentKey: string; // Parent element key (empty for root)
|
||||
visible?: VisibilityRule; // Conditional display
|
||||
}`}</Code>
|
||||
|
||||
@@ -143,14 +135,12 @@ export default function SchemasPage() {
|
||||
<code className="text-foreground">$data</code> prefix in props:
|
||||
</p>
|
||||
<Code lang="json">{`{
|
||||
"key": "greeting",
|
||||
"type": "Text",
|
||||
"props": {
|
||||
"content": "$data.user.name",
|
||||
"count": "$data.items.length"
|
||||
},
|
||||
"children": [],
|
||||
"parentKey": "card-1"
|
||||
"children": []
|
||||
}`}</Code>
|
||||
|
||||
<h3 className="text-lg font-medium mt-8 mb-3">Action Format</h3>
|
||||
|
||||
@@ -50,18 +50,14 @@ export default function SpecsPage() {
|
||||
"root": "card-1",
|
||||
"elements": {
|
||||
"card-1": {
|
||||
"key": "card-1",
|
||||
"type": "Card",
|
||||
"props": { "title": "Welcome" },
|
||||
"children": ["text-1"],
|
||||
"parentKey": ""
|
||||
"children": ["text-1"]
|
||||
},
|
||||
"text-1": {
|
||||
"key": "text-1",
|
||||
"type": "Text",
|
||||
"props": { "content": "Hello, $data.user.name!" },
|
||||
"children": [],
|
||||
"parentKey": "card-1"
|
||||
"children": []
|
||||
}
|
||||
}
|
||||
}`}</Code>
|
||||
@@ -74,53 +70,39 @@ export default function SpecsPage() {
|
||||
"root": "card-1",
|
||||
"elements": {
|
||||
"card-1": {
|
||||
"key": "card-1",
|
||||
"type": "Card",
|
||||
"props": { "title": "User Profile", "padding": "md" },
|
||||
"children": ["row-1", "button-1"],
|
||||
"parentKey": ""
|
||||
"children": ["row-1", "button-1"]
|
||||
},
|
||||
"row-1": {
|
||||
"key": "row-1",
|
||||
"type": "Row",
|
||||
"props": { "gap": "md" },
|
||||
"children": ["avatar-1", "stack-1"],
|
||||
"parentKey": "card-1"
|
||||
"children": ["avatar-1", "stack-1"]
|
||||
},
|
||||
"avatar-1": {
|
||||
"key": "avatar-1",
|
||||
"type": "Avatar",
|
||||
"props": { "src": "$data.user.avatar", "alt": "$data.user.name" },
|
||||
"children": [],
|
||||
"parentKey": "row-1"
|
||||
"children": []
|
||||
},
|
||||
"stack-1": {
|
||||
"key": "stack-1",
|
||||
"type": "Stack",
|
||||
"props": { "gap": "sm" },
|
||||
"children": ["name-text", "email-text"],
|
||||
"parentKey": "row-1"
|
||||
"children": ["name-text", "email-text"]
|
||||
},
|
||||
"name-text": {
|
||||
"key": "name-text",
|
||||
"type": "Text",
|
||||
"props": { "content": "$data.user.name", "variant": "heading" },
|
||||
"children": [],
|
||||
"parentKey": "stack-1"
|
||||
"children": []
|
||||
},
|
||||
"email-text": {
|
||||
"key": "email-text",
|
||||
"type": "Text",
|
||||
"props": { "content": "$data.user.email", "variant": "caption" },
|
||||
"children": [],
|
||||
"parentKey": "stack-1"
|
||||
"children": []
|
||||
},
|
||||
"button-1": {
|
||||
"key": "button-1",
|
||||
"type": "Button",
|
||||
"props": { "label": "Edit Profile" },
|
||||
"children": [],
|
||||
"parentKey": "card-1"
|
||||
"children": []
|
||||
}
|
||||
}
|
||||
}`}</Code>
|
||||
@@ -133,21 +115,16 @@ export default function SpecsPage() {
|
||||
"root": "page",
|
||||
"elements": {
|
||||
"page": {
|
||||
"key": "page",
|
||||
"type": "Page",
|
||||
"props": {},
|
||||
"children": ["header", "hero", "features", "footer"],
|
||||
"parentKey": ""
|
||||
"children": ["header", "hero", "features", "footer"]
|
||||
},
|
||||
"header": {
|
||||
"key": "header",
|
||||
"type": "Header",
|
||||
"props": { "logo": "/logo.svg", "navItems": ["Products", "Pricing", "Docs"] },
|
||||
"children": [],
|
||||
"parentKey": "page"
|
||||
"children": []
|
||||
},
|
||||
"hero": {
|
||||
"key": "hero",
|
||||
"type": "Hero",
|
||||
"props": {
|
||||
"title": "Build UIs with JSON",
|
||||
@@ -155,43 +132,32 @@ export default function SpecsPage() {
|
||||
"ctaLabel": "Get Started",
|
||||
"ctaHref": "/docs"
|
||||
},
|
||||
"children": [],
|
||||
"parentKey": "page"
|
||||
"children": []
|
||||
},
|
||||
"features": {
|
||||
"key": "features",
|
||||
"type": "Features",
|
||||
"props": { "columns": 3 },
|
||||
"children": ["feature-1", "feature-2", "feature-3"],
|
||||
"parentKey": "page"
|
||||
"children": ["feature-1", "feature-2", "feature-3"]
|
||||
},
|
||||
"feature-1": {
|
||||
"key": "feature-1",
|
||||
"type": "Feature",
|
||||
"props": { "icon": "zap", "title": "Fast", "description": "Render UIs in milliseconds" },
|
||||
"children": [],
|
||||
"parentKey": "features"
|
||||
"children": []
|
||||
},
|
||||
"feature-2": {
|
||||
"key": "feature-2",
|
||||
"type": "Feature",
|
||||
"props": { "icon": "shield", "title": "Secure", "description": "Validate all specs against your catalog" },
|
||||
"children": [],
|
||||
"parentKey": "features"
|
||||
"children": []
|
||||
},
|
||||
"feature-3": {
|
||||
"key": "feature-3",
|
||||
"type": "Feature",
|
||||
"props": { "icon": "sparkles", "title": "AI-Ready", "description": "Generate prompts from your catalog" },
|
||||
"children": [],
|
||||
"parentKey": "features"
|
||||
"children": []
|
||||
},
|
||||
"footer": {
|
||||
"key": "footer",
|
||||
"type": "Footer",
|
||||
"props": { "copyright": "2025 Acme Inc", "links": ["Privacy", "Terms", "Contact"] },
|
||||
"children": [],
|
||||
"parentKey": "page"
|
||||
"children": []
|
||||
}
|
||||
}
|
||||
}`}</Code>
|
||||
@@ -209,11 +175,9 @@ export default function SpecsPage() {
|
||||
"root": "card-1",
|
||||
"elements": {
|
||||
"card-1": {
|
||||
"key": "card-1",
|
||||
"type": "Card",
|
||||
"props": { "title": "My Card" },
|
||||
"children": ["text-1"],
|
||||
"parentKey": ""
|
||||
"children": ["text-1"]
|
||||
},
|
||||
"text-1": { ... }
|
||||
}
|
||||
@@ -224,17 +188,11 @@ export default function SpecsPage() {
|
||||
Each element in the map has a consistent shape:
|
||||
</p>
|
||||
<Code lang="json">{`{
|
||||
"key": "unique-id",
|
||||
"type": "ComponentName",
|
||||
"props": { "label": "Hello" },
|
||||
"children": ["child-1", "child-2"],
|
||||
"parentKey": "parent-id"
|
||||
"children": ["child-1", "child-2"]
|
||||
}`}</Code>
|
||||
<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>
|
||||
<code className="text-foreground">type</code> — Component type from
|
||||
your catalog
|
||||
@@ -246,10 +204,6 @@ export default function SpecsPage() {
|
||||
<code className="text-foreground">children</code> — Array of child
|
||||
element keys
|
||||
</li>
|
||||
<li>
|
||||
<code className="text-foreground">parentKey</code> — Key of parent
|
||||
element (empty string for root)
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-lg font-medium mt-8 mb-3">Dynamic Data</h3>
|
||||
@@ -258,15 +212,13 @@ export default function SpecsPage() {
|
||||
<code className="text-foreground">$data</code> paths:
|
||||
</p>
|
||||
<Code lang="json">{`{
|
||||
"key": "metric-1",
|
||||
"type": "Metric",
|
||||
"props": {
|
||||
"label": "Total Revenue",
|
||||
"value": "$data.metrics.revenue",
|
||||
"change": "$data.metrics.revenueChange"
|
||||
},
|
||||
"children": [],
|
||||
"parentKey": "dashboard"
|
||||
"children": []
|
||||
}`}</Code>
|
||||
|
||||
<h3 className="text-lg font-medium mt-8 mb-3">Conditional Visibility</h3>
|
||||
@@ -275,13 +227,11 @@ export default function SpecsPage() {
|
||||
<code className="text-foreground">visible</code> property:
|
||||
</p>
|
||||
<Code lang="json">{`{
|
||||
"key": "alert-1",
|
||||
"type": "Alert",
|
||||
"props": {
|
||||
"message": "You have unsaved changes"
|
||||
},
|
||||
"children": [],
|
||||
"parentKey": "form",
|
||||
"visible": {
|
||||
"path": "$data.form.isDirty",
|
||||
"operator": "eq",
|
||||
|
||||
@@ -18,9 +18,10 @@ export default function StreamingPage() {
|
||||
format where each line is a JSON patch operation that progressively
|
||||
builds your spec:
|
||||
</p>
|
||||
<Code lang="json">{`{"op":"set","path":"/root","value":{"key":"root","type":"Card","props":{"title":"Dashboard"}}}
|
||||
{"op":"set","path":"/root/children/0","value":{"key":"metric-1","type":"Metric","props":{"label":"Revenue"}}}
|
||||
{"op":"set","path":"/root/children/1","value":{"key":"metric-2","type":"Metric","props":{"label":"Users"}}}`}</Code>
|
||||
<Code lang="json">{`{"op":"set","path":"/root","value":"root"}
|
||||
{"op":"set","path":"/elements/root","value":{"type":"Card","props":{"title":"Dashboard"},"children":["metric-1","metric-2"]}}
|
||||
{"op":"set","path":"/elements/metric-1","value":{"type":"Metric","props":{"label":"Revenue"}}}
|
||||
{"op":"set","path":"/elements/metric-2","value":{"type":"Metric","props":{"label":"Users"}}}`}</Code>
|
||||
|
||||
<h2 className="text-xl font-semibold mt-12 mb-4">useUIStream Hook</h2>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
|
||||
@@ -127,15 +127,16 @@ export const catalog = createCatalog({
|
||||
Constrained output that your components render natively.
|
||||
</p>
|
||||
<Code lang="json">{`{
|
||||
"key": "dashboard",
|
||||
"type": "Card",
|
||||
"props": {
|
||||
"title": "Revenue Dashboard",
|
||||
"description": null
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"key": "revenue",
|
||||
"root": "dashboard",
|
||||
"elements": {
|
||||
"dashboard": {
|
||||
"type": "Card",
|
||||
"props": {
|
||||
"title": "Revenue Dashboard"
|
||||
},
|
||||
"children": ["revenue"]
|
||||
},
|
||||
"revenue": {
|
||||
"type": "Metric",
|
||||
"props": {
|
||||
"label": "Total Revenue",
|
||||
@@ -143,7 +144,7 @@ export const catalog = createCatalog({
|
||||
"format": "currency"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`}</Code>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,13 +171,11 @@ export const catalog = createCatalog({
|
||||
"root": "card",
|
||||
"elements": {
|
||||
"card": {
|
||||
"key": "card",
|
||||
"type": "Card",
|
||||
"props": { "title": "Revenue" },
|
||||
"children": ["metric", "chart"]
|
||||
},
|
||||
"metric": {
|
||||
"key": "metric",
|
||||
"type": "Metric",
|
||||
"props": {
|
||||
"label": "Total Revenue",
|
||||
@@ -185,7 +184,6 @@ export const catalog = createCatalog({
|
||||
}
|
||||
},
|
||||
"chart": {
|
||||
"key": "chart",
|
||||
"type": "Chart",
|
||||
"props": {
|
||||
"dataPath": "analytics/salesByRegion"
|
||||
|
||||
@@ -29,7 +29,6 @@ const SIMULATION_STAGES: SimulationStage[] = [
|
||||
root: "card",
|
||||
elements: {
|
||||
card: {
|
||||
key: "card",
|
||||
type: "Card",
|
||||
props: { title: "Contact Us", maxWidth: "md" },
|
||||
children: [],
|
||||
@@ -43,110 +42,96 @@ const SIMULATION_STAGES: SimulationStage[] = [
|
||||
root: "card",
|
||||
elements: {
|
||||
card: {
|
||||
key: "card",
|
||||
type: "Card",
|
||||
props: { title: "Contact Us", maxWidth: "md" },
|
||||
children: ["name"],
|
||||
},
|
||||
name: {
|
||||
key: "name",
|
||||
type: "Input",
|
||||
props: { label: "Name", name: "name" },
|
||||
},
|
||||
},
|
||||
},
|
||||
stream:
|
||||
'{"op":"add","path":"/elements/card","value":{"key":"card","type":"Card","props":{"title":"Contact Us","maxWidth":"md"},"children":["name"]}}',
|
||||
'{"op":"add","path":"/elements/card","value":{"type":"Card","props":{"title":"Contact Us","maxWidth":"md"},"children":["name"]}}',
|
||||
},
|
||||
{
|
||||
tree: {
|
||||
root: "card",
|
||||
elements: {
|
||||
card: {
|
||||
key: "card",
|
||||
type: "Card",
|
||||
props: { title: "Contact Us", maxWidth: "md" },
|
||||
children: ["name", "email"],
|
||||
},
|
||||
name: {
|
||||
key: "name",
|
||||
type: "Input",
|
||||
props: { label: "Name", name: "name" },
|
||||
},
|
||||
email: {
|
||||
key: "email",
|
||||
type: "Input",
|
||||
props: { label: "Email", name: "email" },
|
||||
},
|
||||
},
|
||||
},
|
||||
stream:
|
||||
'{"op":"add","path":"/elements/email","value":{"key":"email","type":"Input","props":{"label":"Email","name":"email"}}}',
|
||||
'{"op":"add","path":"/elements/email","value":{"type":"Input","props":{"label":"Email","name":"email"}}}',
|
||||
},
|
||||
{
|
||||
tree: {
|
||||
root: "card",
|
||||
elements: {
|
||||
card: {
|
||||
key: "card",
|
||||
type: "Card",
|
||||
props: { title: "Contact Us", maxWidth: "md" },
|
||||
children: ["name", "email", "message"],
|
||||
},
|
||||
name: {
|
||||
key: "name",
|
||||
type: "Input",
|
||||
props: { label: "Name", name: "name" },
|
||||
},
|
||||
email: {
|
||||
key: "email",
|
||||
type: "Input",
|
||||
props: { label: "Email", name: "email" },
|
||||
},
|
||||
message: {
|
||||
key: "message",
|
||||
type: "Textarea",
|
||||
props: { label: "Message", name: "message" },
|
||||
},
|
||||
},
|
||||
},
|
||||
stream:
|
||||
'{"op":"add","path":"/elements/message","value":{"key":"message","type":"Textarea","props":{"label":"Message","name":"message"}}}',
|
||||
'{"op":"add","path":"/elements/message","value":{"type":"Textarea","props":{"label":"Message","name":"message"}}}',
|
||||
},
|
||||
{
|
||||
tree: {
|
||||
root: "card",
|
||||
elements: {
|
||||
card: {
|
||||
key: "card",
|
||||
type: "Card",
|
||||
props: { title: "Contact Us", maxWidth: "md" },
|
||||
children: ["name", "email", "message", "submit"],
|
||||
},
|
||||
name: {
|
||||
key: "name",
|
||||
type: "Input",
|
||||
props: { label: "Name", name: "name" },
|
||||
},
|
||||
email: {
|
||||
key: "email",
|
||||
type: "Input",
|
||||
props: { label: "Email", name: "email" },
|
||||
},
|
||||
message: {
|
||||
key: "message",
|
||||
type: "Textarea",
|
||||
props: { label: "Message", name: "message" },
|
||||
},
|
||||
submit: {
|
||||
key: "submit",
|
||||
type: "Button",
|
||||
props: { label: "Send Message", variant: "primary" },
|
||||
},
|
||||
},
|
||||
},
|
||||
stream:
|
||||
'{"op":"add","path":"/elements/submit","value":{"key":"submit","type":"Button","props":{"label":"Send Message","variant":"primary"}}}',
|
||||
'{"op":"add","path":"/elements/submit","value":{"type":"Button","props":{"label":"Send Message","variant":"primary"}}}',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -70,25 +70,19 @@ const spec: Spec = {
|
||||
root: "container",
|
||||
elements: {
|
||||
container: {
|
||||
key: "container",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "medium" },
|
||||
children: ["heading", "metric"],
|
||||
parentKey: null,
|
||||
},
|
||||
heading: {
|
||||
key: "heading",
|
||||
type: "Heading",
|
||||
props: { text: "Revenue", size: "large" },
|
||||
children: [],
|
||||
parentKey: "container",
|
||||
},
|
||||
metric: {
|
||||
key: "metric",
|
||||
type: "Metric",
|
||||
props: { label: "Total", value: "$12,345", format: "currency" },
|
||||
children: [],
|
||||
parentKey: "container",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -84,14 +84,11 @@ function createCustomerDetailSpec(
|
||||
|
||||
const elements: Spec["elements"] = {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "large" },
|
||||
children: ["header", "details", "sections"],
|
||||
parentKey: null,
|
||||
},
|
||||
header: {
|
||||
key: "header",
|
||||
type: "Stack",
|
||||
props: {
|
||||
direction: "horizontal",
|
||||
@@ -99,38 +96,28 @@ function createCustomerDetailSpec(
|
||||
distribute: "space-between",
|
||||
},
|
||||
children: ["customerInfo", "actions"],
|
||||
parentKey: "root",
|
||||
},
|
||||
customerInfo: {
|
||||
key: "customerInfo",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "xsmall" },
|
||||
children: ["customerName", "customerEmail"],
|
||||
parentKey: "header",
|
||||
},
|
||||
customerName: {
|
||||
key: "customerName",
|
||||
type: "Heading",
|
||||
props: { text: customer?.name ?? "Unknown Customer", size: "xlarge" },
|
||||
children: [],
|
||||
parentKey: "customerInfo",
|
||||
},
|
||||
customerEmail: {
|
||||
key: "customerEmail",
|
||||
type: "Text",
|
||||
props: { content: customer?.email ?? "", color: "secondary" },
|
||||
children: [],
|
||||
parentKey: "customerInfo",
|
||||
},
|
||||
actions: {
|
||||
key: "actions",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "small" },
|
||||
children: ["editBtn", "portalBtn"],
|
||||
parentKey: "header",
|
||||
},
|
||||
editBtn: {
|
||||
key: "editBtn",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Edit",
|
||||
@@ -139,10 +126,8 @@ function createCustomerDetailSpec(
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "actions",
|
||||
},
|
||||
portalBtn: {
|
||||
key: "portalBtn",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Billing Portal",
|
||||
@@ -151,31 +136,23 @@ function createCustomerDetailSpec(
|
||||
type: "primary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "actions",
|
||||
},
|
||||
details: {
|
||||
key: "details",
|
||||
type: "PropertyList",
|
||||
props: { orientation: "horizontal" },
|
||||
children: ["detailStatus", "detailCreated", "detailBalance"],
|
||||
parentKey: "root",
|
||||
},
|
||||
detailStatus: {
|
||||
key: "detailStatus",
|
||||
type: "PropertyListItem",
|
||||
props: { label: "Status", value: customer?.status ?? "Unknown" },
|
||||
children: [],
|
||||
parentKey: "details",
|
||||
},
|
||||
detailCreated: {
|
||||
key: "detailCreated",
|
||||
type: "PropertyListItem",
|
||||
props: { label: "Created", value: customer?.created ?? "Unknown" },
|
||||
children: [],
|
||||
parentKey: "details",
|
||||
},
|
||||
detailBalance: {
|
||||
key: "detailBalance",
|
||||
type: "PropertyListItem",
|
||||
props: {
|
||||
label: "Balance",
|
||||
@@ -184,42 +161,32 @@ function createCustomerDetailSpec(
|
||||
: "$0.00",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "details",
|
||||
},
|
||||
sections: {
|
||||
key: "sections",
|
||||
type: "Accordion",
|
||||
props: {},
|
||||
children: ["paymentsSection", "subscriptionsSection", "invoicesSection"],
|
||||
parentKey: "root",
|
||||
},
|
||||
|
||||
// Payments Section
|
||||
paymentsSection: {
|
||||
key: "paymentsSection",
|
||||
type: "AccordionItem",
|
||||
props: { title: `Payments (${payments?.total ?? 0})`, defaultOpen: true },
|
||||
children: paymentsList.length > 0 ? ["paymentsList"] : ["noPayments"],
|
||||
parentKey: "sections",
|
||||
},
|
||||
paymentsList: {
|
||||
key: "paymentsList",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "small" },
|
||||
children: paymentsList.slice(0, 5).map((_, i) => `payment${i}`),
|
||||
parentKey: "paymentsSection",
|
||||
},
|
||||
noPayments: {
|
||||
key: "noPayments",
|
||||
type: "Text",
|
||||
props: { content: "No payments found", color: "secondary" },
|
||||
children: [],
|
||||
parentKey: "paymentsSection",
|
||||
},
|
||||
|
||||
// Subscriptions Section
|
||||
subscriptionsSection: {
|
||||
key: "subscriptionsSection",
|
||||
type: "AccordionItem",
|
||||
props: {
|
||||
title: `Subscriptions (${subscriptions?.total ?? 0})`,
|
||||
@@ -229,54 +196,42 @@ function createCustomerDetailSpec(
|
||||
subscriptionsList.length > 0
|
||||
? ["subscriptionsList"]
|
||||
: ["noSubscriptions"],
|
||||
parentKey: "sections",
|
||||
},
|
||||
subscriptionsList: {
|
||||
key: "subscriptionsList",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "small" },
|
||||
children: subscriptionsList.slice(0, 5).map((_, i) => `subscription${i}`),
|
||||
parentKey: "subscriptionsSection",
|
||||
},
|
||||
noSubscriptions: {
|
||||
key: "noSubscriptions",
|
||||
type: "Text",
|
||||
props: { content: "No subscriptions found", color: "secondary" },
|
||||
children: [],
|
||||
parentKey: "subscriptionsSection",
|
||||
},
|
||||
|
||||
// Invoices Section
|
||||
invoicesSection: {
|
||||
key: "invoicesSection",
|
||||
type: "AccordionItem",
|
||||
props: {
|
||||
title: `Invoices (${invoices?.total ?? 0})`,
|
||||
defaultOpen: false,
|
||||
},
|
||||
children: invoicesList.length > 0 ? ["invoicesList"] : ["noInvoices"],
|
||||
parentKey: "sections",
|
||||
},
|
||||
invoicesList: {
|
||||
key: "invoicesList",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "small" },
|
||||
children: invoicesList.slice(0, 5).map((_, i) => `invoice${i}`),
|
||||
parentKey: "invoicesSection",
|
||||
},
|
||||
noInvoices: {
|
||||
key: "noInvoices",
|
||||
type: "Text",
|
||||
props: { content: "No invoices found", color: "secondary" },
|
||||
children: [],
|
||||
parentKey: "invoicesSection",
|
||||
},
|
||||
};
|
||||
|
||||
// Add payment cards
|
||||
paymentsList.slice(0, 5).forEach((p, i) => {
|
||||
elements[`payment${i}`] = {
|
||||
key: `payment${i}`,
|
||||
type: "PaymentCard",
|
||||
props: {
|
||||
amount: 0,
|
||||
@@ -286,14 +241,12 @@ function createCustomerDetailSpec(
|
||||
paymentId: p.id,
|
||||
},
|
||||
children: [],
|
||||
parentKey: "paymentsList",
|
||||
};
|
||||
});
|
||||
|
||||
// Add subscription cards
|
||||
subscriptionsList.slice(0, 5).forEach((s, i) => {
|
||||
elements[`subscription${i}`] = {
|
||||
key: `subscription${i}`,
|
||||
type: "SubscriptionCard",
|
||||
props: {
|
||||
planName: s.planName,
|
||||
@@ -304,14 +257,12 @@ function createCustomerDetailSpec(
|
||||
currentPeriodEnd: s.currentPeriodEnd,
|
||||
},
|
||||
children: [],
|
||||
parentKey: "subscriptionsList",
|
||||
};
|
||||
});
|
||||
|
||||
// Add invoice cards
|
||||
invoicesList.slice(0, 5).forEach((inv, i) => {
|
||||
elements[`invoice${i}`] = {
|
||||
key: `invoice${i}`,
|
||||
type: "InvoiceCard",
|
||||
props: {
|
||||
invoiceNumber: inv.invoiceNumber,
|
||||
@@ -321,7 +272,6 @@ function createCustomerDetailSpec(
|
||||
dueDate: inv.dueDate,
|
||||
},
|
||||
children: [],
|
||||
parentKey: "invoicesList",
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -36,28 +36,21 @@ function createCustomersListSpec(data: Record<string, unknown>): Spec {
|
||||
|
||||
const elements: Spec["elements"] = {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "large" },
|
||||
children: ["heading", "stats", "actions", "list", "pagination"],
|
||||
parentKey: null,
|
||||
},
|
||||
heading: {
|
||||
key: "heading",
|
||||
type: "Heading",
|
||||
props: { text: "Customer Directory", size: "xlarge" },
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
stats: {
|
||||
key: "stats",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "medium" },
|
||||
children: ["totalMetric"],
|
||||
parentKey: "root",
|
||||
},
|
||||
totalMetric: {
|
||||
key: "totalMetric",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Total Customers",
|
||||
@@ -65,17 +58,13 @@ function createCustomersListSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "neutral",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "stats",
|
||||
},
|
||||
actions: {
|
||||
key: "actions",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "small" },
|
||||
children: ["refreshBtn", "exportBtn"],
|
||||
parentKey: "root",
|
||||
},
|
||||
refreshBtn: {
|
||||
key: "refreshBtn",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Refresh",
|
||||
@@ -83,10 +72,8 @@ function createCustomersListSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "actions",
|
||||
},
|
||||
exportBtn: {
|
||||
key: "exportBtn",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Export CSV",
|
||||
@@ -95,24 +82,18 @@ function createCustomersListSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "actions",
|
||||
},
|
||||
list: {
|
||||
key: "list",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "small" },
|
||||
children: customerList.slice(0, 10).map((_, i) => `customer${i}`),
|
||||
parentKey: "root",
|
||||
},
|
||||
pagination: {
|
||||
key: "pagination",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "small" },
|
||||
children: customers?.hasMore ? ["loadMore"] : [],
|
||||
parentKey: "root",
|
||||
},
|
||||
loadMore: {
|
||||
key: "loadMore",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Load More",
|
||||
@@ -124,13 +105,11 @@ function createCustomersListSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "pagination",
|
||||
},
|
||||
};
|
||||
|
||||
customerList.slice(0, 10).forEach((c, i) => {
|
||||
elements[`customer${i}`] = {
|
||||
key: `customer${i}`,
|
||||
type: "CustomerCard",
|
||||
props: {
|
||||
name: c.name,
|
||||
@@ -139,7 +118,6 @@ function createCustomersListSpec(data: Record<string, unknown>): Spec {
|
||||
customerId: c.id,
|
||||
},
|
||||
children: [],
|
||||
parentKey: "list",
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -38,28 +38,21 @@ function createRevenueSpec(data: Record<string, unknown>): Spec {
|
||||
root: "root",
|
||||
elements: {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "large" },
|
||||
children: ["heading", "metrics", "refresh"],
|
||||
parentKey: null,
|
||||
},
|
||||
heading: {
|
||||
key: "heading",
|
||||
type: "Heading",
|
||||
props: { text: "Revenue Dashboard", size: "xlarge" },
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
metrics: {
|
||||
key: "metrics",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "medium" },
|
||||
children: ["m1", "m2", "m3"],
|
||||
parentKey: "root",
|
||||
},
|
||||
m1: {
|
||||
key: "m1",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Payment Volume",
|
||||
@@ -70,10 +63,8 @@ function createRevenueSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "positive",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
m2: {
|
||||
key: "m2",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Active Subscriptions",
|
||||
@@ -84,10 +75,8 @@ function createRevenueSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "positive",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
m3: {
|
||||
key: "m3",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Total Customers",
|
||||
@@ -95,10 +84,8 @@ function createRevenueSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "neutral",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
refresh: {
|
||||
key: "refresh",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Refresh Data",
|
||||
@@ -106,7 +93,6 @@ function createRevenueSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -129,28 +115,21 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
||||
|
||||
const elements: Spec["elements"] = {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "large" },
|
||||
children: ["heading", "stats", "payments", "refresh"],
|
||||
parentKey: null,
|
||||
},
|
||||
heading: {
|
||||
key: "heading",
|
||||
type: "Heading",
|
||||
props: { text: "Recent Payments", size: "xlarge" },
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
stats: {
|
||||
key: "stats",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "medium" },
|
||||
children: ["s1", "s2"],
|
||||
parentKey: "root",
|
||||
},
|
||||
s1: {
|
||||
key: "s1",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Total Volume",
|
||||
@@ -158,10 +137,8 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "positive",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "stats",
|
||||
},
|
||||
s2: {
|
||||
key: "s2",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Success Rate",
|
||||
@@ -169,17 +146,13 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "positive",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "stats",
|
||||
},
|
||||
payments: {
|
||||
key: "payments",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "small" },
|
||||
children: paymentsList.slice(0, 5).map((_, i) => `p${i}`),
|
||||
parentKey: "root",
|
||||
},
|
||||
refresh: {
|
||||
key: "refresh",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Refresh Payments",
|
||||
@@ -187,13 +160,11 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
};
|
||||
|
||||
paymentsList.slice(0, 5).forEach((p, i) => {
|
||||
elements[`p${i}`] = {
|
||||
key: `p${i}`,
|
||||
type: "PaymentCard",
|
||||
props: {
|
||||
amount: 0, // We'll use formattedAmount in description
|
||||
@@ -203,7 +174,6 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
||||
paymentId: p.id,
|
||||
},
|
||||
children: [],
|
||||
parentKey: "payments",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -229,28 +199,21 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
|
||||
const elements: Spec["elements"] = {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "large" },
|
||||
children: ["heading", "metrics", "alert", "subs", "refresh"],
|
||||
parentKey: null,
|
||||
},
|
||||
heading: {
|
||||
key: "heading",
|
||||
type: "Heading",
|
||||
props: { text: "Subscriptions Overview", size: "xlarge" },
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
metrics: {
|
||||
key: "metrics",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "medium" },
|
||||
children: ["m1", "m2", "m3"],
|
||||
parentKey: "root",
|
||||
},
|
||||
m1: {
|
||||
key: "m1",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Active",
|
||||
@@ -258,10 +221,8 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "positive",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
m2: {
|
||||
key: "m2",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Trialing",
|
||||
@@ -269,10 +230,8 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "positive",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
m3: {
|
||||
key: "m3",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Past Due",
|
||||
@@ -280,27 +239,21 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: subscriptions?.pastDue ? "negative" : "neutral",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
alert: {
|
||||
key: "alert",
|
||||
type: "Banner",
|
||||
props: {
|
||||
title: `${subscriptions?.trialing ?? 0} subscriptions currently trialing`,
|
||||
type: "default",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
subs: {
|
||||
key: "subs",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "small" },
|
||||
children: subsList.slice(0, 5).map((_, i) => `sub${i}`),
|
||||
parentKey: "root",
|
||||
},
|
||||
refresh: {
|
||||
key: "refresh",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Refresh Subscriptions",
|
||||
@@ -308,13 +261,11 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
};
|
||||
|
||||
subsList.slice(0, 5).forEach((s, i) => {
|
||||
elements[`sub${i}`] = {
|
||||
key: `sub${i}`,
|
||||
type: "SubscriptionCard",
|
||||
props: {
|
||||
planName: s.planName,
|
||||
@@ -323,7 +274,6 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
interval: (s.interval as "month" | "year") ?? "month",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "subs",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -346,28 +296,21 @@ function createCustomersSpec(data: Record<string, unknown>): Spec {
|
||||
|
||||
const elements: Spec["elements"] = {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "large" },
|
||||
children: ["heading", "stats", "customers", "refresh"],
|
||||
parentKey: null,
|
||||
},
|
||||
heading: {
|
||||
key: "heading",
|
||||
type: "Heading",
|
||||
props: { text: "Customer Directory", size: "xlarge" },
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
stats: {
|
||||
key: "stats",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "medium" },
|
||||
children: ["s1"],
|
||||
parentKey: "root",
|
||||
},
|
||||
s1: {
|
||||
key: "s1",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Total Customers",
|
||||
@@ -375,17 +318,13 @@ function createCustomersSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "neutral",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "stats",
|
||||
},
|
||||
customers: {
|
||||
key: "customers",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "small" },
|
||||
children: customersList.slice(0, 5).map((_, i) => `c${i}`),
|
||||
parentKey: "root",
|
||||
},
|
||||
refresh: {
|
||||
key: "refresh",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Refresh Customers",
|
||||
@@ -393,13 +332,11 @@ function createCustomersSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
};
|
||||
|
||||
customersList.slice(0, 5).forEach((c, i) => {
|
||||
elements[`c${i}`] = {
|
||||
key: `c${i}`,
|
||||
type: "CustomerCard",
|
||||
props: {
|
||||
name: c.name,
|
||||
@@ -408,7 +345,6 @@ function createCustomersSpec(data: Record<string, unknown>): Spec {
|
||||
customerId: c.id,
|
||||
},
|
||||
children: [],
|
||||
parentKey: "customers",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -434,28 +370,21 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
|
||||
const elements: Spec["elements"] = {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "large" },
|
||||
children: ["heading", "stats", "invoices", "refresh"],
|
||||
parentKey: null,
|
||||
},
|
||||
heading: {
|
||||
key: "heading",
|
||||
type: "Heading",
|
||||
props: { text: "Invoice Management", size: "xlarge" },
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
stats: {
|
||||
key: "stats",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "medium" },
|
||||
children: ["s1", "s2", "s3"],
|
||||
parentKey: "root",
|
||||
},
|
||||
s1: {
|
||||
key: "s1",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Outstanding",
|
||||
@@ -463,10 +392,8 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "neutral",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "stats",
|
||||
},
|
||||
s2: {
|
||||
key: "s2",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Paid",
|
||||
@@ -474,10 +401,8 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "positive",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "stats",
|
||||
},
|
||||
s3: {
|
||||
key: "s3",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Overdue",
|
||||
@@ -485,17 +410,13 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: invoices?.overdue !== "$0.00" ? "negative" : "neutral",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "stats",
|
||||
},
|
||||
invoices: {
|
||||
key: "invoices",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "small" },
|
||||
children: invoicesList.slice(0, 5).map((_, i) => `inv${i}`),
|
||||
parentKey: "root",
|
||||
},
|
||||
refresh: {
|
||||
key: "refresh",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Refresh Invoices",
|
||||
@@ -503,13 +424,11 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
};
|
||||
|
||||
invoicesList.slice(0, 5).forEach((inv, i) => {
|
||||
elements[`inv${i}`] = {
|
||||
key: `inv${i}`,
|
||||
type: "InvoiceCard",
|
||||
props: {
|
||||
invoiceNumber: inv.invoiceNumber,
|
||||
@@ -518,7 +437,6 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
dueDate: inv.dueDate,
|
||||
},
|
||||
children: [],
|
||||
parentKey: "invoices",
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
|
||||
const elements: Spec["elements"] = {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "large" },
|
||||
children: [
|
||||
@@ -52,24 +51,18 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
"list",
|
||||
"pagination",
|
||||
],
|
||||
parentKey: null,
|
||||
},
|
||||
heading: {
|
||||
key: "heading",
|
||||
type: "Heading",
|
||||
props: { text: "Invoices", size: "xlarge" },
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
metrics: {
|
||||
key: "metrics",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "medium" },
|
||||
children: ["outstandingMetric", "paidMetric", "overdueMetric"],
|
||||
parentKey: "root",
|
||||
},
|
||||
outstandingMetric: {
|
||||
key: "outstandingMetric",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Outstanding",
|
||||
@@ -77,10 +70,8 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "neutral",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
paidMetric: {
|
||||
key: "paidMetric",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Paid",
|
||||
@@ -88,10 +79,8 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "positive",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
overdueMetric: {
|
||||
key: "overdueMetric",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Overdue",
|
||||
@@ -99,10 +88,8 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: invoices?.overdue !== "$0.00" ? "negative" : "neutral",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
alert: {
|
||||
key: "alert",
|
||||
type: "Banner",
|
||||
props: {
|
||||
title:
|
||||
@@ -112,24 +99,18 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
type: invoices?.overdue !== "$0.00" ? "caution" : "default",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
filters: {
|
||||
key: "filters",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "small" },
|
||||
children: ["refreshBtn", "createBtn", "exportBtn"],
|
||||
parentKey: "root",
|
||||
},
|
||||
refreshBtn: {
|
||||
key: "refreshBtn",
|
||||
type: "Button",
|
||||
props: { label: "Refresh", action: "refreshInvoices", type: "secondary" },
|
||||
children: [],
|
||||
parentKey: "filters",
|
||||
},
|
||||
createBtn: {
|
||||
key: "createBtn",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Create Invoice",
|
||||
@@ -138,10 +119,8 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
type: "primary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "filters",
|
||||
},
|
||||
exportBtn: {
|
||||
key: "exportBtn",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Export CSV",
|
||||
@@ -150,24 +129,18 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "filters",
|
||||
},
|
||||
list: {
|
||||
key: "list",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "small" },
|
||||
children: invoicesList.slice(0, 10).map((_, i) => `invoice${i}`),
|
||||
parentKey: "root",
|
||||
},
|
||||
pagination: {
|
||||
key: "pagination",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "small" },
|
||||
children: invoices?.hasMore ? ["loadMore"] : [],
|
||||
parentKey: "root",
|
||||
},
|
||||
loadMore: {
|
||||
key: "loadMore",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Load More",
|
||||
@@ -176,13 +149,11 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "pagination",
|
||||
},
|
||||
};
|
||||
|
||||
invoicesList.slice(0, 10).forEach((inv, i) => {
|
||||
elements[`invoice${i}`] = {
|
||||
key: `invoice${i}`,
|
||||
type: "InvoiceCard",
|
||||
props: {
|
||||
invoiceNumber: inv.invoiceNumber,
|
||||
@@ -198,7 +169,6 @@ function createInvoicesSpec(data: Record<string, unknown>): Spec {
|
||||
customerEmail: inv.customerEmail,
|
||||
},
|
||||
children: [],
|
||||
parentKey: "list",
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -56,14 +56,11 @@ function createPaymentDetailSpec(
|
||||
|
||||
const elements: Spec["elements"] = {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "large" },
|
||||
children: ["header", "details", "actions", "refundsSection"],
|
||||
parentKey: null,
|
||||
},
|
||||
header: {
|
||||
key: "header",
|
||||
type: "Stack",
|
||||
props: {
|
||||
direction: "horizontal",
|
||||
@@ -71,31 +68,23 @@ function createPaymentDetailSpec(
|
||||
distribute: "space-between",
|
||||
},
|
||||
children: ["paymentInfo", "statusBadge"],
|
||||
parentKey: "root",
|
||||
},
|
||||
paymentInfo: {
|
||||
key: "paymentInfo",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "xsmall" },
|
||||
children: ["paymentAmount", "paymentId"],
|
||||
parentKey: "header",
|
||||
},
|
||||
paymentAmount: {
|
||||
key: "paymentAmount",
|
||||
type: "Heading",
|
||||
props: { text: payment?.formattedAmount ?? "$0.00", size: "xlarge" },
|
||||
children: [],
|
||||
parentKey: "paymentInfo",
|
||||
},
|
||||
paymentId: {
|
||||
key: "paymentId",
|
||||
type: "Text",
|
||||
props: { content: paymentId, color: "secondary", size: "small" },
|
||||
children: [],
|
||||
parentKey: "paymentInfo",
|
||||
},
|
||||
statusBadge: {
|
||||
key: "statusBadge",
|
||||
type: "Badge",
|
||||
props: {
|
||||
label: payment?.status ?? "unknown",
|
||||
@@ -107,10 +96,8 @@ function createPaymentDetailSpec(
|
||||
: "negative",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "header",
|
||||
},
|
||||
details: {
|
||||
key: "details",
|
||||
type: "PropertyList",
|
||||
props: { orientation: "vertical" },
|
||||
children: [
|
||||
@@ -119,54 +106,42 @@ function createPaymentDetailSpec(
|
||||
"detailCurrency",
|
||||
"detailCustomer",
|
||||
],
|
||||
parentKey: "root",
|
||||
},
|
||||
detailDescription: {
|
||||
key: "detailDescription",
|
||||
type: "PropertyListItem",
|
||||
props: {
|
||||
label: "Description",
|
||||
value: payment?.description ?? "No description",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "details",
|
||||
},
|
||||
detailCreated: {
|
||||
key: "detailCreated",
|
||||
type: "PropertyListItem",
|
||||
props: { label: "Created", value: payment?.created ?? "Unknown" },
|
||||
children: [],
|
||||
parentKey: "details",
|
||||
},
|
||||
detailCurrency: {
|
||||
key: "detailCurrency",
|
||||
type: "PropertyListItem",
|
||||
props: {
|
||||
label: "Currency",
|
||||
value: (payment?.currency ?? "usd").toUpperCase(),
|
||||
},
|
||||
children: [],
|
||||
parentKey: "details",
|
||||
},
|
||||
detailCustomer: {
|
||||
key: "detailCustomer",
|
||||
type: "PropertyListItem",
|
||||
props: { label: "Customer", value: payment?.customerId ?? "Guest" },
|
||||
children: [],
|
||||
parentKey: "details",
|
||||
},
|
||||
actions: {
|
||||
key: "actions",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "small" },
|
||||
children:
|
||||
payment?.status === "succeeded"
|
||||
? ["refundBtn", "viewBtn"]
|
||||
: ["viewBtn"],
|
||||
parentKey: "root",
|
||||
},
|
||||
refundBtn: {
|
||||
key: "refundBtn",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Refund Payment",
|
||||
@@ -175,10 +150,8 @@ function createPaymentDetailSpec(
|
||||
type: "destructive",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "actions",
|
||||
},
|
||||
viewBtn: {
|
||||
key: "viewBtn",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "View in Dashboard",
|
||||
@@ -187,44 +160,34 @@ function createPaymentDetailSpec(
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "actions",
|
||||
},
|
||||
refundsSection: {
|
||||
key: "refundsSection",
|
||||
type: "Accordion",
|
||||
props: {},
|
||||
children: ["refundsAccordion"],
|
||||
parentKey: "root",
|
||||
},
|
||||
refundsAccordion: {
|
||||
key: "refundsAccordion",
|
||||
type: "AccordionItem",
|
||||
props: {
|
||||
title: `Refunds (${refunds?.total ?? 0})`,
|
||||
defaultOpen: refundsList.length > 0,
|
||||
},
|
||||
children: refundsList.length > 0 ? ["refundsList"] : ["noRefunds"],
|
||||
parentKey: "refundsSection",
|
||||
},
|
||||
refundsList: {
|
||||
key: "refundsList",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "small" },
|
||||
children: refundsList.slice(0, 5).map((_, i) => `refund${i}`),
|
||||
parentKey: "refundsAccordion",
|
||||
},
|
||||
noRefunds: {
|
||||
key: "noRefunds",
|
||||
type: "Text",
|
||||
props: { content: "No refunds", color: "secondary" },
|
||||
children: [],
|
||||
parentKey: "refundsAccordion",
|
||||
},
|
||||
};
|
||||
|
||||
refundsList.slice(0, 5).forEach((r, i) => {
|
||||
elements[`refund${i}`] = {
|
||||
key: `refund${i}`,
|
||||
type: "RefundCard",
|
||||
props: {
|
||||
amount: r.amount,
|
||||
@@ -233,7 +196,6 @@ function createPaymentDetailSpec(
|
||||
reason: r.reason,
|
||||
},
|
||||
children: [],
|
||||
parentKey: "refundsList",
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -38,28 +38,21 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
||||
|
||||
const elements: Spec["elements"] = {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "large" },
|
||||
children: ["heading", "metrics", "filters", "list", "pagination"],
|
||||
parentKey: null,
|
||||
},
|
||||
heading: {
|
||||
key: "heading",
|
||||
type: "Heading",
|
||||
props: { text: "Payments", size: "xlarge" },
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
metrics: {
|
||||
key: "metrics",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "medium" },
|
||||
children: ["volumeMetric", "rateMetric", "countMetric"],
|
||||
parentKey: "root",
|
||||
},
|
||||
volumeMetric: {
|
||||
key: "volumeMetric",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Total Volume",
|
||||
@@ -67,10 +60,8 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "positive",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
rateMetric: {
|
||||
key: "rateMetric",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Success Rate",
|
||||
@@ -78,10 +69,8 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "positive",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
countMetric: {
|
||||
key: "countMetric",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Total Payments",
|
||||
@@ -89,24 +78,18 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "neutral",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
filters: {
|
||||
key: "filters",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "small" },
|
||||
children: ["refreshBtn", "exportBtn"],
|
||||
parentKey: "root",
|
||||
},
|
||||
refreshBtn: {
|
||||
key: "refreshBtn",
|
||||
type: "Button",
|
||||
props: { label: "Refresh", action: "refreshPayments", type: "secondary" },
|
||||
children: [],
|
||||
parentKey: "filters",
|
||||
},
|
||||
exportBtn: {
|
||||
key: "exportBtn",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Export CSV",
|
||||
@@ -115,24 +98,18 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "filters",
|
||||
},
|
||||
list: {
|
||||
key: "list",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "small" },
|
||||
children: paymentsList.slice(0, 10).map((_, i) => `payment${i}`),
|
||||
parentKey: "root",
|
||||
},
|
||||
pagination: {
|
||||
key: "pagination",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "small" },
|
||||
children: payments?.hasMore ? ["loadMore"] : [],
|
||||
parentKey: "root",
|
||||
},
|
||||
loadMore: {
|
||||
key: "loadMore",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Load More",
|
||||
@@ -144,13 +121,11 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "pagination",
|
||||
},
|
||||
};
|
||||
|
||||
paymentsList.slice(0, 10).forEach((p, i) => {
|
||||
elements[`payment${i}`] = {
|
||||
key: `payment${i}`,
|
||||
type: "PaymentCard",
|
||||
props: {
|
||||
amount: 0,
|
||||
@@ -160,7 +135,6 @@ function createPaymentsSpec(data: Record<string, unknown>): Spec {
|
||||
paymentId: p.id,
|
||||
},
|
||||
children: [],
|
||||
parentKey: "list",
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -49,28 +49,21 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
||||
|
||||
const elements: Spec["elements"] = {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "large" },
|
||||
children: ["heading", "metrics", "filters", "list", "pagination"],
|
||||
parentKey: null,
|
||||
},
|
||||
heading: {
|
||||
key: "heading",
|
||||
type: "Heading",
|
||||
props: { text: "Products & Prices", size: "xlarge" },
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
metrics: {
|
||||
key: "metrics",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "medium" },
|
||||
children: ["productsMetric", "pricesMetric", "activeMetric"],
|
||||
parentKey: "root",
|
||||
},
|
||||
productsMetric: {
|
||||
key: "productsMetric",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Total Products",
|
||||
@@ -78,10 +71,8 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "neutral",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
pricesMetric: {
|
||||
key: "pricesMetric",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Total Prices",
|
||||
@@ -89,10 +80,8 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "neutral",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
activeMetric: {
|
||||
key: "activeMetric",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Active Products",
|
||||
@@ -100,24 +89,18 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "positive",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
filters: {
|
||||
key: "filters",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "small" },
|
||||
children: ["refreshBtn", "createBtn"],
|
||||
parentKey: "root",
|
||||
},
|
||||
refreshBtn: {
|
||||
key: "refreshBtn",
|
||||
type: "Button",
|
||||
props: { label: "Refresh", action: "fetchProducts", type: "secondary" },
|
||||
children: [],
|
||||
parentKey: "filters",
|
||||
},
|
||||
createBtn: {
|
||||
key: "createBtn",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Create Product",
|
||||
@@ -126,24 +109,18 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
||||
type: "primary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "filters",
|
||||
},
|
||||
list: {
|
||||
key: "list",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "small" },
|
||||
children: productsList.slice(0, 10).map((_, i) => `product${i}`),
|
||||
parentKey: "root",
|
||||
},
|
||||
pagination: {
|
||||
key: "pagination",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "small" },
|
||||
children: products?.hasMore ? ["loadMore"] : [],
|
||||
parentKey: "root",
|
||||
},
|
||||
loadMore: {
|
||||
key: "loadMore",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Load More",
|
||||
@@ -152,7 +129,6 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "pagination",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -169,15 +145,12 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
||||
: "No prices";
|
||||
|
||||
elements[`product${i}`] = {
|
||||
key: `product${i}`,
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "small" },
|
||||
children: [`productCard${i}`],
|
||||
parentKey: "list",
|
||||
};
|
||||
|
||||
elements[`productCard${i}`] = {
|
||||
key: `productCard${i}`,
|
||||
type: "PropertyList",
|
||||
props: { orientation: "vertical" },
|
||||
children: [
|
||||
@@ -186,39 +159,30 @@ function createProductsSpec(data: Record<string, unknown>): Spec {
|
||||
`productPrice${i}`,
|
||||
`productStatus${i}`,
|
||||
],
|
||||
parentKey: `product${i}`,
|
||||
};
|
||||
|
||||
elements[`productName${i}`] = {
|
||||
key: `productName${i}`,
|
||||
type: "PropertyListItem",
|
||||
props: { label: "Name", value: p.name },
|
||||
children: [],
|
||||
parentKey: `productCard${i}`,
|
||||
};
|
||||
|
||||
elements[`productDesc${i}`] = {
|
||||
key: `productDesc${i}`,
|
||||
type: "PropertyListItem",
|
||||
props: { label: "Description", value: p.description || "No description" },
|
||||
children: [],
|
||||
parentKey: `productCard${i}`,
|
||||
};
|
||||
|
||||
elements[`productPrice${i}`] = {
|
||||
key: `productPrice${i}`,
|
||||
type: "PropertyListItem",
|
||||
props: { label: "Prices", value: priceDisplay },
|
||||
children: [],
|
||||
parentKey: `productCard${i}`,
|
||||
};
|
||||
|
||||
elements[`productStatus${i}`] = {
|
||||
key: `productStatus${i}`,
|
||||
type: "PropertyListItem",
|
||||
props: { label: "Status", value: p.active ? "Active" : "Archived" },
|
||||
children: [],
|
||||
parentKey: `productCard${i}`,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
|
||||
const elements: Spec["elements"] = {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "large" },
|
||||
children: [
|
||||
@@ -53,17 +52,13 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
"list",
|
||||
"pagination",
|
||||
],
|
||||
parentKey: null,
|
||||
},
|
||||
heading: {
|
||||
key: "heading",
|
||||
type: "Heading",
|
||||
props: { text: "Subscriptions", size: "xlarge" },
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
metrics: {
|
||||
key: "metrics",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "medium" },
|
||||
children: [
|
||||
@@ -72,10 +67,8 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
"pastDueMetric",
|
||||
"canceledMetric",
|
||||
],
|
||||
parentKey: "root",
|
||||
},
|
||||
activeMetric: {
|
||||
key: "activeMetric",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Active",
|
||||
@@ -83,10 +76,8 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "positive",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
trialingMetric: {
|
||||
key: "trialingMetric",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Trialing",
|
||||
@@ -94,10 +85,8 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "positive",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
pastDueMetric: {
|
||||
key: "pastDueMetric",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Past Due",
|
||||
@@ -105,10 +94,8 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: subscriptions?.pastDue ? "negative" : "neutral",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
canceledMetric: {
|
||||
key: "canceledMetric",
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Canceled",
|
||||
@@ -116,10 +103,8 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
changeType: "neutral",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "metrics",
|
||||
},
|
||||
alert: {
|
||||
key: "alert",
|
||||
type: "Banner",
|
||||
props: {
|
||||
title: subscriptions?.pastDue
|
||||
@@ -128,17 +113,13 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
type: subscriptions?.pastDue ? "caution" : "default",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "root",
|
||||
},
|
||||
filters: {
|
||||
key: "filters",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "small" },
|
||||
children: ["refreshBtn", "exportBtn"],
|
||||
parentKey: "root",
|
||||
},
|
||||
refreshBtn: {
|
||||
key: "refreshBtn",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Refresh",
|
||||
@@ -146,10 +127,8 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "filters",
|
||||
},
|
||||
exportBtn: {
|
||||
key: "exportBtn",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Export CSV",
|
||||
@@ -158,26 +137,20 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "filters",
|
||||
},
|
||||
list: {
|
||||
key: "list",
|
||||
type: "Stack",
|
||||
props: { direction: "vertical", gap: "small" },
|
||||
children: subscriptionsList
|
||||
.slice(0, 10)
|
||||
.map((_, i) => `subscription${i}`),
|
||||
parentKey: "root",
|
||||
},
|
||||
pagination: {
|
||||
key: "pagination",
|
||||
type: "Stack",
|
||||
props: { direction: "horizontal", gap: "small" },
|
||||
children: subscriptions?.hasMore ? ["loadMore"] : [],
|
||||
parentKey: "root",
|
||||
},
|
||||
loadMore: {
|
||||
key: "loadMore",
|
||||
type: "Button",
|
||||
props: {
|
||||
label: "Load More",
|
||||
@@ -186,13 +159,11 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
type: "secondary",
|
||||
},
|
||||
children: [],
|
||||
parentKey: "pagination",
|
||||
},
|
||||
};
|
||||
|
||||
subscriptionsList.slice(0, 10).forEach((s, i) => {
|
||||
elements[`subscription${i}`] = {
|
||||
key: `subscription${i}`,
|
||||
type: "SubscriptionCard",
|
||||
props: {
|
||||
planName: s.planName,
|
||||
@@ -208,7 +179,6 @@ function createSubscriptionsSpec(data: Record<string, unknown>): Spec {
|
||||
currentPeriodEnd: s.currentPeriodEnd,
|
||||
},
|
||||
children: [],
|
||||
parentKey: "list",
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -13,18 +13,15 @@ describe("traverseSpec", () => {
|
||||
root: "root",
|
||||
elements: {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Card",
|
||||
props: {},
|
||||
children: ["child1", "child2"],
|
||||
},
|
||||
child1: {
|
||||
key: "child1",
|
||||
type: "Text",
|
||||
props: {},
|
||||
},
|
||||
child2: {
|
||||
key: "child2",
|
||||
type: "Button",
|
||||
props: {},
|
||||
},
|
||||
@@ -32,8 +29,8 @@ describe("traverseSpec", () => {
|
||||
};
|
||||
|
||||
const visited: string[] = [];
|
||||
traverseSpec(spec, (element) => {
|
||||
visited.push(element.key);
|
||||
traverseSpec(spec, (_element, key) => {
|
||||
visited.push(key);
|
||||
});
|
||||
|
||||
expect(visited).toEqual(["root", "child1", "child2"]);
|
||||
@@ -41,8 +38,8 @@ describe("traverseSpec", () => {
|
||||
|
||||
it("handles empty spec", () => {
|
||||
const visited: string[] = [];
|
||||
traverseSpec(null as unknown as Spec, (element) => {
|
||||
visited.push(element.key);
|
||||
traverseSpec(null as unknown as Spec, (_element, key) => {
|
||||
visited.push(key);
|
||||
});
|
||||
expect(visited).toEqual([]);
|
||||
});
|
||||
@@ -54,18 +51,15 @@ describe("collectUsedComponents", () => {
|
||||
root: "root",
|
||||
elements: {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Card",
|
||||
props: {},
|
||||
children: ["child1", "child2"],
|
||||
},
|
||||
child1: {
|
||||
key: "child1",
|
||||
type: "Text",
|
||||
props: {},
|
||||
},
|
||||
child2: {
|
||||
key: "child2",
|
||||
type: "Text",
|
||||
props: {},
|
||||
},
|
||||
@@ -83,7 +77,6 @@ describe("collectDataPaths", () => {
|
||||
root: "root",
|
||||
elements: {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Metric",
|
||||
props: { valuePath: "analytics/revenue" },
|
||||
},
|
||||
@@ -99,7 +92,6 @@ describe("collectDataPaths", () => {
|
||||
root: "root",
|
||||
elements: {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Text",
|
||||
props: { content: { path: "user/name" } },
|
||||
},
|
||||
@@ -117,7 +109,6 @@ describe("collectActions", () => {
|
||||
root: "root",
|
||||
elements: {
|
||||
root: {
|
||||
key: "root",
|
||||
type: "Button",
|
||||
props: { action: "submit_form" },
|
||||
},
|
||||
|
||||
@@ -4,7 +4,12 @@ import type { Spec, UIElement } from "@json-render/core";
|
||||
* Visitor function for spec traversal
|
||||
*/
|
||||
export interface TreeVisitor {
|
||||
(element: UIElement, depth: number, parent: UIElement | null): void;
|
||||
(
|
||||
element: UIElement,
|
||||
key: string,
|
||||
depth: number,
|
||||
parent: UIElement | null,
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,7 +30,7 @@ export function traverseSpec(
|
||||
const element = spec.elements[key];
|
||||
if (!element) return;
|
||||
|
||||
visitor(element, depth, parent);
|
||||
visitor(element, key, depth, parent);
|
||||
|
||||
if (element.children) {
|
||||
for (const childKey of element.children) {
|
||||
@@ -43,7 +48,7 @@ export function traverseSpec(
|
||||
export function collectUsedComponents(spec: Spec): Set<string> {
|
||||
const components = new Set<string>();
|
||||
|
||||
traverseSpec(spec, (element) => {
|
||||
traverseSpec(spec, (element, _key) => {
|
||||
components.add(element.type);
|
||||
});
|
||||
|
||||
@@ -56,7 +61,7 @@ export function collectUsedComponents(spec: Spec): Set<string> {
|
||||
export function collectDataPaths(spec: Spec): Set<string> {
|
||||
const paths = new Set<string>();
|
||||
|
||||
traverseSpec(spec, (element) => {
|
||||
traverseSpec(spec, (element, _key) => {
|
||||
// Check props for data paths
|
||||
for (const [propName, propValue] of Object.entries(element.props)) {
|
||||
// Check for path props (e.g., valuePath, dataPath, bindPath)
|
||||
@@ -141,7 +146,7 @@ function collectPathsFromCondition(
|
||||
export function collectActions(spec: Spec): Set<string> {
|
||||
const actions = new Set<string>();
|
||||
|
||||
traverseSpec(spec, (element) => {
|
||||
traverseSpec(spec, (element, _key) => {
|
||||
for (const propValue of Object.values(element.props)) {
|
||||
// Check for action prop (string action name)
|
||||
if (typeof propValue === "string" && propValue.startsWith("action:")) {
|
||||
|
||||
@@ -72,12 +72,10 @@ describe("createCatalog", () => {
|
||||
});
|
||||
|
||||
const validElement = {
|
||||
key: "1",
|
||||
type: "text",
|
||||
props: { content: "Hello" },
|
||||
};
|
||||
const invalidElement = {
|
||||
key: "1",
|
||||
type: "text",
|
||||
props: { content: 123 },
|
||||
};
|
||||
@@ -96,7 +94,7 @@ describe("createCatalog", () => {
|
||||
const validSpec = {
|
||||
root: "1",
|
||||
elements: {
|
||||
"1": { key: "1", type: "text", props: { content: "Hello" } },
|
||||
"1": { type: "text", props: { content: "Hello" } },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -120,16 +118,13 @@ describe("createCatalog", () => {
|
||||
root: "card-1",
|
||||
elements: {
|
||||
"card-1": {
|
||||
key: "card-1",
|
||||
type: "card",
|
||||
props: { title: "Hello" },
|
||||
children: ["text-1"],
|
||||
},
|
||||
"text-1": {
|
||||
key: "text-1",
|
||||
type: "text",
|
||||
props: { content: "World" },
|
||||
parentKey: "card-1",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -147,7 +142,7 @@ describe("createCatalog", () => {
|
||||
const invalidSpec = {
|
||||
root: "1",
|
||||
elements: {
|
||||
"1": { key: "1", type: "nonexistent", props: { content: "Hello" } },
|
||||
"1": { type: "nonexistent", props: { content: "Hello" } },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -398,7 +393,6 @@ describe("defineCatalog (new schema API)", () => {
|
||||
root: s.string(),
|
||||
elements: s.record(
|
||||
s.object({
|
||||
key: s.string(),
|
||||
type: s.ref("catalog.components"),
|
||||
props: s.any(),
|
||||
children: s.array(s.string()),
|
||||
@@ -489,7 +483,6 @@ describe("defineCatalog (new schema API)", () => {
|
||||
root: "text-1",
|
||||
elements: {
|
||||
"text-1": {
|
||||
key: "text-1",
|
||||
type: "Text",
|
||||
props: { content: "Hello" },
|
||||
children: [],
|
||||
@@ -558,7 +551,6 @@ describe("defineCatalog (new schema API)", () => {
|
||||
root: "text-1",
|
||||
elements: {
|
||||
"text-1": {
|
||||
key: "text-1",
|
||||
type: "Text",
|
||||
props: { content: "Hello" },
|
||||
children: [],
|
||||
|
||||
@@ -143,11 +143,9 @@ export function createCatalog<
|
||||
const def = components[componentName]!;
|
||||
|
||||
return z.object({
|
||||
key: z.string(),
|
||||
type: z.literal(componentName as string),
|
||||
props: def.props,
|
||||
children: z.array(z.string()).optional(),
|
||||
parentKey: z.string().nullable().optional(),
|
||||
visible: VisibilityConditionSchema.optional(),
|
||||
});
|
||||
});
|
||||
@@ -157,11 +155,9 @@ export function createCatalog<
|
||||
|
||||
if (componentSchemas.length === 0) {
|
||||
elementSchema = z.object({
|
||||
key: z.string(),
|
||||
type: z.string(),
|
||||
props: z.record(z.string(), z.unknown()),
|
||||
children: z.array(z.string()).optional(),
|
||||
parentKey: z.string().nullable().optional(),
|
||||
visible: VisibilityConditionSchema.optional(),
|
||||
}) as unknown as z.ZodType<UIElement>;
|
||||
} else if (componentSchemas.length === 1) {
|
||||
@@ -500,7 +496,7 @@ export function generateSystemPrompt<
|
||||
lines.push("OUTPUT FORMAT (JSONL):");
|
||||
lines.push('{"op":"set","path":"/root","value":"element-key"}');
|
||||
lines.push(
|
||||
'{"op":"add","path":"/elements/key","value":{"key":"...","type":"...","props":{...},"children":[...]}}',
|
||||
'{"op":"add","path":"/elements/key","value":{"type":"...","props":{...},"children":[...]}}',
|
||||
);
|
||||
lines.push('{"op":"remove","path":"/elements/key"}');
|
||||
lines.push("");
|
||||
@@ -513,7 +509,7 @@ export function generateSystemPrompt<
|
||||
"Remove elements with op:remove - also update the parent's children array to exclude the removed key",
|
||||
"Children array contains string keys, not objects",
|
||||
"Parent first, then children",
|
||||
"Each element needs: key, type, props",
|
||||
"Each element needs: type, props",
|
||||
"ONLY use props listed above - never invent new props",
|
||||
];
|
||||
const allRules = [...baseRules, ...customRules];
|
||||
|
||||
@@ -5,6 +5,7 @@ export type {
|
||||
DynamicNumber,
|
||||
DynamicBoolean,
|
||||
UIElement,
|
||||
FlatElement,
|
||||
Spec,
|
||||
VisibilityCondition,
|
||||
LogicExpression,
|
||||
|
||||
@@ -546,9 +546,9 @@ function generatePrompt<TDef extends SchemaDefinition, TCatalog>(
|
||||
lines.push("Example output (each line is a separate JSON object):");
|
||||
lines.push("");
|
||||
lines.push(`{"op":"set","path":"/root","value":"card-1"}
|
||||
{"op":"set","path":"/elements/card-1","value":{"key":"card-1","type":"Card","props":{"title":"Dashboard"},"children":["metric-1","chart-1"],"parentKey":""}}
|
||||
{"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":"set","path":"/elements/chart-1","value":{"key":"chart-1","type":"Chart","props":{"type":"bar","dataPath":"analytics.salesByRegion"},"children":[],"parentKey":"card-1"}}`);
|
||||
{"op":"set","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Dashboard"},"children":["metric-1","chart-1"]}}
|
||||
{"op":"set","path":"/elements/metric-1","value":{"type":"Metric","props":{"label":"Revenue","valuePath":"analytics.revenue","format":"currency"},"children":[]}}
|
||||
{"op":"set","path":"/elements/chart-1","value":{"type":"Chart","props":{"type":"bar","dataPath":"analytics.salesByRegion"},"children":[]}}`);
|
||||
lines.push("");
|
||||
|
||||
// Components section
|
||||
@@ -594,9 +594,8 @@ function generatePrompt<TDef extends SchemaDefinition, TCatalog>(
|
||||
'First line sets root: {"op":"set","path":"/root","value":"<root-key>"}',
|
||||
'Then add each element: {"op":"set","path":"/elements/<key>","value":{...}}',
|
||||
"ONLY use components listed above",
|
||||
"Each element value needs: key, type, props, children (array of child keys), parentKey",
|
||||
"Use unique keys (e.g., 'header', 'metric-1', 'chart-revenue')",
|
||||
"Root element's parentKey is empty string, children reference their parent's key",
|
||||
"Each element value needs: type, props, children (array of child keys)",
|
||||
"Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')",
|
||||
];
|
||||
const allRules = [...baseRules, ...customRules];
|
||||
allRules.forEach((rule, i) => {
|
||||
|
||||
@@ -53,20 +53,31 @@ export interface UIElement<
|
||||
T extends string = string,
|
||||
P = Record<string, unknown>,
|
||||
> {
|
||||
/** Unique key for reconciliation */
|
||||
key: string;
|
||||
/** Component type from the catalog */
|
||||
type: T;
|
||||
/** Component props */
|
||||
props: P;
|
||||
/** Child element keys (flat structure) */
|
||||
children?: string[];
|
||||
/** Parent element key (null for root) */
|
||||
parentKey?: string | null;
|
||||
/** 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) */
|
||||
parentKey?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visibility condition types
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import type { Spec, UIElement, JsonPatch } from "@json-render/core";
|
||||
import type {
|
||||
Spec,
|
||||
UIElement,
|
||||
FlatElement,
|
||||
JsonPatch,
|
||||
} from "@json-render/core";
|
||||
import { setByPath } from "@json-render/core";
|
||||
|
||||
/**
|
||||
@@ -236,18 +241,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: Array<UIElement & { parentKey?: string | null }>,
|
||||
): Spec {
|
||||
export function flatToTree(elements: FlatElement[]): Spec {
|
||||
const elementMap: Record<string, UIElement> = {};
|
||||
let root = "";
|
||||
|
||||
// First pass: add all elements to map
|
||||
for (const element of elements) {
|
||||
elementMap[element.key] = {
|
||||
key: element.key,
|
||||
type: element.type,
|
||||
props: element.props,
|
||||
children: [],
|
||||
|
||||
@@ -15,16 +15,12 @@ export const schema = defineSchema((s) => ({
|
||||
/** Flat map of elements by key */
|
||||
elements: s.record(
|
||||
s.object({
|
||||
/** Unique key for this element */
|
||||
key: s.string(),
|
||||
/** Component type from catalog */
|
||||
type: s.ref("catalog.components"),
|
||||
/** Component props */
|
||||
props: s.propsOf("catalog.components"),
|
||||
/** Child element keys (flat reference) */
|
||||
children: s.array(s.string()),
|
||||
/** Parent element key (null for root) */
|
||||
parentKey: s.string(),
|
||||
/** Visibility condition */
|
||||
visible: s.any(),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user