@json-render/core 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1173,27 +1173,72 @@ function generatePrompt(catalog, options) {
1173
1173
  const lines = [];
1174
1174
  lines.push(system);
1175
1175
  lines.push("");
1176
- lines.push("OUTPUT FORMAT:");
1176
+ lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):");
1177
1177
  lines.push(
1178
- "Output JSONL (one JSON object per line) with patches to build a UI tree."
1178
+ "Output JSONL (one JSON object per line) using RFC 6902 JSON Patch operations to build a UI tree."
1179
1179
  );
1180
1180
  lines.push(
1181
- "Each line is a JSON patch operation. Start with /root, then stream /elements and /state patches interleaved so the UI fills in progressively as it streams."
1181
+ "Each line is a JSON patch operation (add, remove, replace). Start with /root, then stream /elements and /state patches interleaved so the UI fills in progressively as it streams."
1182
1182
  );
1183
1183
  lines.push("");
1184
1184
  lines.push("Example output (each line is a separate JSON object):");
1185
1185
  lines.push("");
1186
- lines.push(`{"op":"add","path":"/root","value":"blog"}
1187
- {"op":"add","path":"/elements/blog","value":{"type":"Stack","props":{"direction":"vertical","gap":"md"},"children":["heading","posts-grid"]}}
1188
- {"op":"add","path":"/elements/heading","value":{"type":"Heading","props":{"text":"Blog","level":"h1"},"children":[]}}
1189
- {"op":"add","path":"/elements/posts-grid","value":{"type":"Grid","props":{"columns":2,"gap":"md"},"repeat":{"path":"/posts","key":"id"},"children":["post-card"]}}
1190
- {"op":"add","path":"/elements/post-card","value":{"type":"Card","props":{"title":{"$path":"$item/title"}},"children":["post-meta"]}}
1191
- {"op":"add","path":"/elements/post-meta","value":{"type":"Text","props":{"text":{"$path":"$item/author"},"variant":"muted"},"children":[]}}
1192
- {"op":"add","path":"/state/posts","value":[]}
1193
- {"op":"add","path":"/state/posts/0","value":{"id":"1","title":"Getting Started","author":"Jane","date":"Jan 15"}}
1194
- {"op":"add","path":"/state/posts/1","value":{"id":"2","title":"Advanced Tips","author":"Bob","date":"Feb 3"}}
1186
+ const allComponents = catalog.data.components;
1187
+ const cn = catalog.componentNames;
1188
+ const comp1 = cn[0] || "Component";
1189
+ const comp2 = cn.length > 1 ? cn[1] : comp1;
1190
+ const comp1Def = allComponents?.[comp1];
1191
+ const comp2Def = allComponents?.[comp2];
1192
+ const comp1Props = comp1Def ? getExampleProps(comp1Def) : {};
1193
+ const comp2Props = comp2Def ? getExampleProps(comp2Def) : {};
1194
+ const dynamicPropName = comp2Def?.props ? findFirstStringProp(comp2Def.props) : null;
1195
+ const dynamicProps = dynamicPropName ? { ...comp2Props, [dynamicPropName]: { $path: "$item/title" } } : comp2Props;
1196
+ const exampleOutput = [
1197
+ JSON.stringify({ op: "add", path: "/root", value: "main" }),
1198
+ JSON.stringify({
1199
+ op: "add",
1200
+ path: "/elements/main",
1201
+ value: {
1202
+ type: comp1,
1203
+ props: comp1Props,
1204
+ children: ["child-1", "list"]
1205
+ }
1206
+ }),
1207
+ JSON.stringify({
1208
+ op: "add",
1209
+ path: "/elements/child-1",
1210
+ value: { type: comp2, props: comp2Props, children: [] }
1211
+ }),
1212
+ JSON.stringify({
1213
+ op: "add",
1214
+ path: "/elements/list",
1215
+ value: {
1216
+ type: comp1,
1217
+ props: comp1Props,
1218
+ repeat: { path: "/items", key: "id" },
1219
+ children: ["item"]
1220
+ }
1221
+ }),
1222
+ JSON.stringify({
1223
+ op: "add",
1224
+ path: "/elements/item",
1225
+ value: { type: comp2, props: dynamicProps, children: [] }
1226
+ }),
1227
+ JSON.stringify({ op: "add", path: "/state/items", value: [] }),
1228
+ JSON.stringify({
1229
+ op: "add",
1230
+ path: "/state/items/0",
1231
+ value: { id: "1", title: "First Item" }
1232
+ }),
1233
+ JSON.stringify({
1234
+ op: "add",
1235
+ path: "/state/items/1",
1236
+ value: { id: "2", title: "Second Item" }
1237
+ })
1238
+ ].join("\n");
1239
+ lines.push(`${exampleOutput}
1195
1240
 
1196
- Note: state patches appear right after the elements that use them, so the UI fills in as it streams.`);
1241
+ Note: state patches appear right after the elements that use them, so the UI fills in as it streams. ONLY use component types from the AVAILABLE COMPONENTS list below.`);
1197
1242
  lines.push("");
1198
1243
  lines.push("INITIAL STATE:");
1199
1244
  lines.push(
@@ -1232,7 +1277,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
1232
1277
  'The element itself renders once (as the container), and its children are expanded once per array item. "path" is the state array path. "key" is an optional field name on each item for stable React keys.'
1233
1278
  );
1234
1279
  lines.push(
1235
- 'Example: { "type": "Column", "props": { "gap": 8 }, "repeat": { "path": "/todos", "key": "id" }, "children": ["todo-item"] }'
1280
+ `Example: ${JSON.stringify({ type: comp1, props: comp1Props, repeat: { path: "/todos", key: "id" }, children: ["todo-item"] })}`
1236
1281
  );
1237
1282
  lines.push(
1238
1283
  'Inside children of a repeated element, use "$item/field" for per-item paths: statePath:"$item/completed", { "$path": "$item/title" }. Use "$index" for the current array index.'
@@ -1268,7 +1313,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
1268
1313
  'IMPORTANT: State paths use RFC 6901 JSON Pointer syntax (e.g. "/todos/0/title"). Do NOT use JavaScript-style dot notation (e.g. "/todos.length" is WRONG). To generate unique IDs for new items, use "$id" instead of trying to read array length.'
1269
1314
  );
1270
1315
  lines.push("");
1271
- const components = catalog.data.components;
1316
+ const components = allComponents;
1272
1317
  if (components) {
1273
1318
  lines.push(`AVAILABLE COMPONENTS (${catalog.componentNames.length}):`);
1274
1319
  lines.push("");
@@ -1301,7 +1346,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
1301
1346
  lines.push("");
1302
1347
  lines.push("Example:");
1303
1348
  lines.push(
1304
- ' {"type":"Button","props":{"label":"Save"},"on":{"press":{"action":"setState","params":{"path":"/saved","value":true}}},"children":[]}'
1349
+ ` ${JSON.stringify({ type: comp1, props: comp1Props, on: { press: { action: "setState", params: { path: "/saved", value: true } } }, children: [] })}`
1305
1350
  );
1306
1351
  lines.push("");
1307
1352
  lines.push(
@@ -1316,7 +1361,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
1316
1361
  "Elements can have an optional `visible` field to conditionally show/hide based on data state. IMPORTANT: `visible` is a top-level field on the element object (sibling of type/props/children), NOT inside props."
1317
1362
  );
1318
1363
  lines.push(
1319
- 'Correct: {"type":"Column","props":{"gap":8},"visible":{"eq":[{"path":"/tab"},"home"]},"children":[...]}'
1364
+ `Correct: ${JSON.stringify({ type: comp1, props: comp1Props, visible: { eq: [{ path: "/tab" }, "home"] }, children: ["..."] })}`
1320
1365
  );
1321
1366
  lines.push(
1322
1367
  '- `{ "eq": [{ "path": "/statePath" }, "value"] }` - visible when state at path equals value'
@@ -1331,10 +1376,10 @@ Note: state patches appear right after the elements that use them, so the UI fil
1331
1376
  lines.push("- `true` / `false` - always visible/hidden");
1332
1377
  lines.push("");
1333
1378
  lines.push(
1334
- "Use the Pressable component with on.press bound to setState to update state and drive visibility."
1379
+ "Use a component with on.press bound to setState to update state and drive visibility."
1335
1380
  );
1336
1381
  lines.push(
1337
- 'Example: A Pressable with on: { "press": { "action": "setState", "params": { "path": "/activeTab", "value": "home" } } } sets state, then a container with visible: { "eq": [{ "path": "/activeTab" }, "home"] } shows only when that tab is active.'
1382
+ `Example: A ${comp1} with on: { "press": { "action": "setState", "params": { "path": "/activeTab", "value": "home" } } } sets state, then a container with visible: { "eq": [{ "path": "/activeTab" }, "home"] } shows only when that tab is active.`
1338
1383
  );
1339
1384
  lines.push("");
1340
1385
  lines.push("DYNAMIC PROPS:");
@@ -1380,6 +1425,101 @@ Note: state patches appear right after the elements that use them, so the UI fil
1380
1425
  });
1381
1426
  return lines.join("\n");
1382
1427
  }
1428
+ function getExampleProps(def) {
1429
+ if (def.example && Object.keys(def.example).length > 0) {
1430
+ return def.example;
1431
+ }
1432
+ if (def.props) {
1433
+ return generateExamplePropsFromZod(def.props);
1434
+ }
1435
+ return {};
1436
+ }
1437
+ function generateExamplePropsFromZod(schema) {
1438
+ if (!schema || !schema._def) return {};
1439
+ const def = schema._def;
1440
+ const typeName = getZodTypeName(schema);
1441
+ if (typeName !== "ZodObject" && typeName !== "object") return {};
1442
+ const shape = typeof def.shape === "function" ? def.shape() : def.shape;
1443
+ if (!shape) return {};
1444
+ const result = {};
1445
+ for (const [key, value] of Object.entries(shape)) {
1446
+ const innerTypeName = getZodTypeName(value);
1447
+ if (innerTypeName === "ZodOptional" || innerTypeName === "optional" || innerTypeName === "ZodNullable" || innerTypeName === "nullable") {
1448
+ continue;
1449
+ }
1450
+ result[key] = generateExampleValue(value);
1451
+ }
1452
+ return result;
1453
+ }
1454
+ function generateExampleValue(schema) {
1455
+ if (!schema || !schema._def) return "...";
1456
+ const def = schema._def;
1457
+ const typeName = getZodTypeName(schema);
1458
+ switch (typeName) {
1459
+ case "ZodString":
1460
+ case "string":
1461
+ return "example";
1462
+ case "ZodNumber":
1463
+ case "number":
1464
+ return 0;
1465
+ case "ZodBoolean":
1466
+ case "boolean":
1467
+ return true;
1468
+ case "ZodLiteral":
1469
+ case "literal":
1470
+ return def.value;
1471
+ case "ZodEnum":
1472
+ case "enum": {
1473
+ if (Array.isArray(def.values) && def.values.length > 0)
1474
+ return def.values[0];
1475
+ if (def.entries && typeof def.entries === "object") {
1476
+ const values = Object.values(def.entries);
1477
+ return values.length > 0 ? values[0] : "example";
1478
+ }
1479
+ return "example";
1480
+ }
1481
+ case "ZodOptional":
1482
+ case "optional":
1483
+ case "ZodNullable":
1484
+ case "nullable":
1485
+ case "ZodDefault":
1486
+ case "default": {
1487
+ const inner = def.innerType ?? def.wrapped;
1488
+ return inner ? generateExampleValue(inner) : null;
1489
+ }
1490
+ case "ZodArray":
1491
+ case "array":
1492
+ return [];
1493
+ case "ZodObject":
1494
+ case "object":
1495
+ return generateExamplePropsFromZod(schema);
1496
+ case "ZodUnion":
1497
+ case "union": {
1498
+ const options = def.options;
1499
+ return options && options.length > 0 ? generateExampleValue(options[0]) : "...";
1500
+ }
1501
+ default:
1502
+ return "...";
1503
+ }
1504
+ }
1505
+ function findFirstStringProp(schema) {
1506
+ if (!schema || !schema._def) return null;
1507
+ const def = schema._def;
1508
+ const typeName = getZodTypeName(schema);
1509
+ if (typeName !== "ZodObject" && typeName !== "object") return null;
1510
+ const shape = typeof def.shape === "function" ? def.shape() : def.shape;
1511
+ if (!shape) return null;
1512
+ for (const [key, value] of Object.entries(shape)) {
1513
+ const innerTypeName = getZodTypeName(value);
1514
+ if (innerTypeName === "ZodOptional" || innerTypeName === "optional" || innerTypeName === "ZodNullable" || innerTypeName === "nullable") {
1515
+ continue;
1516
+ }
1517
+ if (innerTypeName === "ZodString" || innerTypeName === "string") {
1518
+ return key;
1519
+ }
1520
+ }
1521
+ return null;
1522
+ }
1383
1523
  function getZodTypeName(schema) {
1384
1524
  if (!schema || !schema._def) return "";
1385
1525
  const def = schema._def;