Przeglądaj źródła

remove key/parentKey from flat specs (#77)

Chris Tate 5 miesięcy temu
rodzic
commit
711e79b069

+ 0 - 16
apps/web/app/(main)/docs/openapi/page.tsx

@@ -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 {

+ 4 - 14
apps/web/app/(main)/docs/schemas/page.tsx

@@ -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>

+ 20 - 70
apps/web/app/(main)/docs/specs/page.tsx

@@ -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",

+ 4 - 3
apps/web/app/(main)/docs/streaming/page.tsx

@@ -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">

+ 11 - 13
apps/web/app/(main)/page.tsx

@@ -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"

+ 4 - 19
apps/web/components/demo.tsx

@@ -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"}}}',
   },
 ];
 

Plik diff jest za duży
+ 0 - 0
examples/remotion/tsconfig.tsbuildinfo


+ 0 - 6
examples/stripe-app/README.md

@@ -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",
     },
   },
 };

+ 0 - 50
examples/stripe-app/src/views/CustomerDetails.tsx

@@ -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",
     };
   });
 

+ 0 - 22
examples/stripe-app/src/views/Customers.tsx

@@ -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",
     };
   });
 

+ 0 - 82
examples/stripe-app/src/views/Home.tsx

@@ -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",
     };
   });
 

+ 0 - 30
examples/stripe-app/src/views/Invoices.tsx

@@ -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",
     };
   });
 

+ 0 - 38
examples/stripe-app/src/views/PaymentDetails.tsx

@@ -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",
     };
   });
 

+ 0 - 26
examples/stripe-app/src/views/Payments.tsx

@@ -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",
     };
   });
 

+ 0 - 36
examples/stripe-app/src/views/Products.tsx

@@ -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}`,
     };
   });
 

+ 0 - 30
examples/stripe-app/src/views/Subscriptions.tsx

@@ -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",
     };
   });
 

+ 4 - 13
packages/codegen/src/traverse.test.ts

@@ -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" },
         },

+ 10 - 5
packages/codegen/src/traverse.ts

@@ -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:")) {

+ 2 - 10
packages/core/src/catalog.test.ts

@@ -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: [],

+ 2 - 6
packages/core/src/catalog.ts

@@ -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];

+ 1 - 0
packages/core/src/index.ts

@@ -5,6 +5,7 @@ export type {
   DynamicNumber,
   DynamicBoolean,
   UIElement,
+  FlatElement,
   Spec,
   VisibilityCondition,
   LogicExpression,

+ 5 - 6
packages/core/src/schema.ts

@@ -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) => {

+ 15 - 4
packages/core/src/types.ts

@@ -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
  */

+ 11 - 6
packages/react/src/hooks.ts

@@ -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: [],

+ 0 - 4
packages/react/src/schema.ts

@@ -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(),
       }),

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików