@json-render/core 0.4.4 → 0.5.0

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
@@ -19,12 +19,12 @@ var DynamicBooleanSchema = z.union([
19
19
  z.boolean(),
20
20
  z.object({ path: z.string() })
21
21
  ]);
22
- function resolveDynamicValue(value, dataModel) {
22
+ function resolveDynamicValue(value, stateModel) {
23
23
  if (value === null || value === void 0) {
24
24
  return void 0;
25
25
  }
26
26
  if (typeof value === "object" && "path" in value) {
27
- return getByPath(dataModel, value.path);
27
+ return getByPath(stateModel, value.path);
28
28
  }
29
29
  return value;
30
30
  }
@@ -352,7 +352,7 @@ var VisibilityConditionSchema = z2.union([
352
352
  LogicExpressionSchema
353
353
  ]);
354
354
  function evaluateLogicExpression(expr, ctx) {
355
- const { dataModel } = ctx;
355
+ const { stateModel } = ctx;
356
356
  if ("and" in expr) {
357
357
  return expr.and.every((subExpr) => evaluateLogicExpression(subExpr, ctx));
358
358
  }
@@ -363,30 +363,30 @@ function evaluateLogicExpression(expr, ctx) {
363
363
  return !evaluateLogicExpression(expr.not, ctx);
364
364
  }
365
365
  if ("path" in expr) {
366
- const value = resolveDynamicValue({ path: expr.path }, dataModel);
366
+ const value = resolveDynamicValue({ path: expr.path }, stateModel);
367
367
  return Boolean(value);
368
368
  }
369
369
  if ("eq" in expr) {
370
370
  const [left, right] = expr.eq;
371
- const leftValue = resolveDynamicValue(left, dataModel);
372
- const rightValue = resolveDynamicValue(right, dataModel);
371
+ const leftValue = resolveDynamicValue(left, stateModel);
372
+ const rightValue = resolveDynamicValue(right, stateModel);
373
373
  return leftValue === rightValue;
374
374
  }
375
375
  if ("neq" in expr) {
376
376
  const [left, right] = expr.neq;
377
- const leftValue = resolveDynamicValue(left, dataModel);
378
- const rightValue = resolveDynamicValue(right, dataModel);
377
+ const leftValue = resolveDynamicValue(left, stateModel);
378
+ const rightValue = resolveDynamicValue(right, stateModel);
379
379
  return leftValue !== rightValue;
380
380
  }
381
381
  if ("gt" in expr) {
382
382
  const [left, right] = expr.gt;
383
383
  const leftValue = resolveDynamicValue(
384
384
  left,
385
- dataModel
385
+ stateModel
386
386
  );
387
387
  const rightValue = resolveDynamicValue(
388
388
  right,
389
- dataModel
389
+ stateModel
390
390
  );
391
391
  if (typeof leftValue === "number" && typeof rightValue === "number") {
392
392
  return leftValue > rightValue;
@@ -397,11 +397,11 @@ function evaluateLogicExpression(expr, ctx) {
397
397
  const [left, right] = expr.gte;
398
398
  const leftValue = resolveDynamicValue(
399
399
  left,
400
- dataModel
400
+ stateModel
401
401
  );
402
402
  const rightValue = resolveDynamicValue(
403
403
  right,
404
- dataModel
404
+ stateModel
405
405
  );
406
406
  if (typeof leftValue === "number" && typeof rightValue === "number") {
407
407
  return leftValue >= rightValue;
@@ -412,11 +412,11 @@ function evaluateLogicExpression(expr, ctx) {
412
412
  const [left, right] = expr.lt;
413
413
  const leftValue = resolveDynamicValue(
414
414
  left,
415
- dataModel
415
+ stateModel
416
416
  );
417
417
  const rightValue = resolveDynamicValue(
418
418
  right,
419
- dataModel
419
+ stateModel
420
420
  );
421
421
  if (typeof leftValue === "number" && typeof rightValue === "number") {
422
422
  return leftValue < rightValue;
@@ -427,11 +427,11 @@ function evaluateLogicExpression(expr, ctx) {
427
427
  const [left, right] = expr.lte;
428
428
  const leftValue = resolveDynamicValue(
429
429
  left,
430
- dataModel
430
+ stateModel
431
431
  );
432
432
  const rightValue = resolveDynamicValue(
433
433
  right,
434
- dataModel
434
+ stateModel
435
435
  );
436
436
  if (typeof leftValue === "number" && typeof rightValue === "number") {
437
437
  return leftValue <= rightValue;
@@ -448,7 +448,7 @@ function evaluateVisibility(condition, ctx) {
448
448
  return condition;
449
449
  }
450
450
  if ("path" in condition && !("and" in condition) && !("or" in condition)) {
451
- const value = resolveDynamicValue({ path: condition.path }, ctx.dataModel);
451
+ const value = resolveDynamicValue({ path: condition.path }, ctx.stateModel);
452
452
  return Boolean(value);
453
453
  }
454
454
  if ("auth" in condition) {
@@ -502,6 +502,44 @@ var visibility = {
502
502
  lte: (left, right) => ({ lte: [left, right] })
503
503
  };
504
504
 
505
+ // src/props.ts
506
+ function isPathExpression(value) {
507
+ return typeof value === "object" && value !== null && "$path" in value && typeof value.$path === "string";
508
+ }
509
+ function isCondExpression(value) {
510
+ return typeof value === "object" && value !== null && "$cond" in value && "$then" in value && "$else" in value;
511
+ }
512
+ function resolvePropValue(value, ctx) {
513
+ if (value === null || value === void 0) {
514
+ return value;
515
+ }
516
+ if (isPathExpression(value)) {
517
+ return getByPath(ctx.stateModel, value.$path);
518
+ }
519
+ if (isCondExpression(value)) {
520
+ const result = evaluateVisibility(value.$cond, ctx);
521
+ return resolvePropValue(result ? value.$then : value.$else, ctx);
522
+ }
523
+ if (Array.isArray(value)) {
524
+ return value.map((item) => resolvePropValue(item, ctx));
525
+ }
526
+ if (typeof value === "object") {
527
+ const resolved = {};
528
+ for (const [key, val] of Object.entries(value)) {
529
+ resolved[key] = resolvePropValue(val, ctx);
530
+ }
531
+ return resolved;
532
+ }
533
+ return value;
534
+ }
535
+ function resolveElementProps(props, ctx) {
536
+ const resolved = {};
537
+ for (const [key, value] of Object.entries(props)) {
538
+ resolved[key] = resolvePropValue(value, ctx);
539
+ }
540
+ return resolved;
541
+ }
542
+
505
543
  // src/actions.ts
506
544
  import { z as z3 } from "zod";
507
545
  var ActionConfirmSchema = z3.object({
@@ -520,44 +558,45 @@ var ActionOnErrorSchema = z3.union([
520
558
  z3.object({ set: z3.record(z3.string(), z3.unknown()) }),
521
559
  z3.object({ action: z3.string() })
522
560
  ]);
523
- var ActionSchema = z3.object({
524
- name: z3.string(),
561
+ var ActionBindingSchema = z3.object({
562
+ action: z3.string(),
525
563
  params: z3.record(z3.string(), DynamicValueSchema).optional(),
526
564
  confirm: ActionConfirmSchema.optional(),
527
565
  onSuccess: ActionOnSuccessSchema.optional(),
528
566
  onError: ActionOnErrorSchema.optional()
529
567
  });
530
- function resolveAction(action2, dataModel) {
568
+ var ActionSchema = ActionBindingSchema;
569
+ function resolveAction(binding, stateModel) {
531
570
  const resolvedParams = {};
532
- if (action2.params) {
533
- for (const [key, value] of Object.entries(action2.params)) {
534
- resolvedParams[key] = resolveDynamicValue(value, dataModel);
571
+ if (binding.params) {
572
+ for (const [key, value] of Object.entries(binding.params)) {
573
+ resolvedParams[key] = resolveDynamicValue(value, stateModel);
535
574
  }
536
575
  }
537
- let confirm = action2.confirm;
576
+ let confirm = binding.confirm;
538
577
  if (confirm) {
539
578
  confirm = {
540
579
  ...confirm,
541
- message: interpolateString(confirm.message, dataModel),
542
- title: interpolateString(confirm.title, dataModel)
580
+ message: interpolateString(confirm.message, stateModel),
581
+ title: interpolateString(confirm.title, stateModel)
543
582
  };
544
583
  }
545
584
  return {
546
- name: action2.name,
585
+ action: binding.action,
547
586
  params: resolvedParams,
548
587
  confirm,
549
- onSuccess: action2.onSuccess,
550
- onError: action2.onError
588
+ onSuccess: binding.onSuccess,
589
+ onError: binding.onError
551
590
  };
552
591
  }
553
- function interpolateString(template, dataModel) {
592
+ function interpolateString(template, stateModel) {
554
593
  return template.replace(/\$\{([^}]+)\}/g, (_, path) => {
555
- const value = resolveDynamicValue({ path }, dataModel);
594
+ const value = resolveDynamicValue({ path }, stateModel);
556
595
  return String(value ?? "");
557
596
  });
558
597
  }
559
598
  async function executeAction(ctx) {
560
- const { action: action2, handler, setData, navigate, executeAction: executeAction2 } = ctx;
599
+ const { action: action2, handler, setState, navigate, executeAction: executeAction2 } = ctx;
561
600
  try {
562
601
  await handler(action2.params);
563
602
  if (action2.onSuccess) {
@@ -565,7 +604,7 @@ async function executeAction(ctx) {
565
604
  navigate(action2.onSuccess.navigate);
566
605
  } else if ("set" in action2.onSuccess) {
567
606
  for (const [path, value] of Object.entries(action2.onSuccess.set)) {
568
- setData(path, value);
607
+ setState(path, value);
569
608
  }
570
609
  } else if ("action" in action2.onSuccess && executeAction2) {
571
610
  await executeAction2(action2.onSuccess.action);
@@ -576,7 +615,7 @@ async function executeAction(ctx) {
576
615
  if ("set" in action2.onError) {
577
616
  for (const [path, value] of Object.entries(action2.onError.set)) {
578
617
  const resolvedValue = typeof value === "string" && value === "$error.message" ? error.message : value;
579
- setData(path, resolvedValue);
618
+ setState(path, resolvedValue);
580
619
  }
581
620
  } else if ("action" in action2.onError && executeAction2) {
582
621
  await executeAction2(action2.onError.action);
@@ -586,25 +625,26 @@ async function executeAction(ctx) {
586
625
  }
587
626
  }
588
627
  }
589
- var action = {
590
- /** Create a simple action */
591
- simple: (name, params) => ({
592
- name,
628
+ var actionBinding = {
629
+ /** Create a simple action binding */
630
+ simple: (actionName, params) => ({
631
+ action: actionName,
593
632
  params
594
633
  }),
595
- /** Create an action with confirmation */
596
- withConfirm: (name, confirm, params) => ({
597
- name,
634
+ /** Create an action binding with confirmation */
635
+ withConfirm: (actionName, confirm, params) => ({
636
+ action: actionName,
598
637
  params,
599
638
  confirm
600
639
  }),
601
- /** Create an action with success handler */
602
- withSuccess: (name, onSuccess, params) => ({
603
- name,
640
+ /** Create an action binding with success handler */
641
+ withSuccess: (actionName, onSuccess, params) => ({
642
+ action: actionName,
604
643
  params,
605
644
  onSuccess
606
645
  })
607
646
  };
647
+ var action = actionBinding;
608
648
 
609
649
  // src/validation.ts
610
650
  import { z as z4 } from "zod";
@@ -713,11 +753,11 @@ var builtInValidationFunctions = {
713
753
  }
714
754
  };
715
755
  function runValidationCheck(check2, ctx) {
716
- const { value, dataModel, customFunctions } = ctx;
756
+ const { value, stateModel, customFunctions } = ctx;
717
757
  const resolvedArgs = {};
718
758
  if (check2.args) {
719
759
  for (const [key, argValue] of Object.entries(check2.args)) {
720
- resolvedArgs[key] = resolveDynamicValue(argValue, dataModel);
760
+ resolvedArgs[key] = resolveDynamicValue(argValue, stateModel);
721
761
  }
722
762
  }
723
763
  const fn = builtInValidationFunctions[check2.fn] ?? customFunctions?.[check2.fn];
@@ -742,7 +782,7 @@ function runValidation(config, ctx) {
742
782
  const errors = [];
743
783
  if (config.enabled) {
744
784
  const enabled = evaluateLogicExpression(config.enabled, {
745
- dataModel: ctx.dataModel,
785
+ stateModel: ctx.stateModel,
746
786
  authState: ctx.authState
747
787
  });
748
788
  if (!enabled) {
@@ -809,6 +849,155 @@ var check = {
809
849
  })
810
850
  };
811
851
 
852
+ // src/spec-validator.ts
853
+ function validateSpec(spec, options = {}) {
854
+ const { checkOrphans = false } = options;
855
+ const issues = [];
856
+ if (!spec.root) {
857
+ issues.push({
858
+ severity: "error",
859
+ message: "Spec has no root element defined.",
860
+ code: "missing_root"
861
+ });
862
+ return { valid: false, issues };
863
+ }
864
+ if (!spec.elements[spec.root]) {
865
+ issues.push({
866
+ severity: "error",
867
+ message: `Root element "${spec.root}" not found in elements map.`,
868
+ code: "root_not_found"
869
+ });
870
+ }
871
+ if (Object.keys(spec.elements).length === 0) {
872
+ issues.push({
873
+ severity: "error",
874
+ message: "Spec has no elements.",
875
+ code: "empty_spec"
876
+ });
877
+ return { valid: false, issues };
878
+ }
879
+ for (const [key, element] of Object.entries(spec.elements)) {
880
+ if (element.children) {
881
+ for (const childKey of element.children) {
882
+ if (!spec.elements[childKey]) {
883
+ issues.push({
884
+ severity: "error",
885
+ message: `Element "${key}" references child "${childKey}" which does not exist in the elements map.`,
886
+ elementKey: key,
887
+ code: "missing_child"
888
+ });
889
+ }
890
+ }
891
+ }
892
+ const props = element.props;
893
+ if (props && "visible" in props && props.visible !== void 0) {
894
+ issues.push({
895
+ severity: "error",
896
+ message: `Element "${key}" has "visible" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
897
+ elementKey: key,
898
+ code: "visible_in_props"
899
+ });
900
+ }
901
+ if (props && "on" in props && props.on !== void 0) {
902
+ issues.push({
903
+ severity: "error",
904
+ message: `Element "${key}" has "on" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
905
+ elementKey: key,
906
+ code: "on_in_props"
907
+ });
908
+ }
909
+ if (props && "repeat" in props && props.repeat !== void 0) {
910
+ issues.push({
911
+ severity: "error",
912
+ message: `Element "${key}" has "repeat" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
913
+ elementKey: key,
914
+ code: "repeat_in_props"
915
+ });
916
+ }
917
+ }
918
+ if (checkOrphans) {
919
+ const reachable = /* @__PURE__ */ new Set();
920
+ const walk = (key) => {
921
+ if (reachable.has(key)) return;
922
+ reachable.add(key);
923
+ const el = spec.elements[key];
924
+ if (el?.children) {
925
+ for (const childKey of el.children) {
926
+ if (spec.elements[childKey]) {
927
+ walk(childKey);
928
+ }
929
+ }
930
+ }
931
+ };
932
+ if (spec.elements[spec.root]) {
933
+ walk(spec.root);
934
+ }
935
+ for (const key of Object.keys(spec.elements)) {
936
+ if (!reachable.has(key)) {
937
+ issues.push({
938
+ severity: "warning",
939
+ message: `Element "${key}" is not reachable from root "${spec.root}".`,
940
+ elementKey: key,
941
+ code: "orphaned_element"
942
+ });
943
+ }
944
+ }
945
+ }
946
+ const hasErrors = issues.some((i) => i.severity === "error");
947
+ return { valid: !hasErrors, issues };
948
+ }
949
+ function autoFixSpec(spec) {
950
+ const fixes = [];
951
+ const fixedElements = {};
952
+ for (const [key, element] of Object.entries(spec.elements)) {
953
+ const props = element.props;
954
+ let fixed = element;
955
+ if (props && "visible" in props && props.visible !== void 0) {
956
+ const { visible, ...restProps } = fixed.props;
957
+ fixed = {
958
+ ...fixed,
959
+ props: restProps,
960
+ visible
961
+ };
962
+ fixes.push(`Moved "visible" from props to element level on "${key}".`);
963
+ }
964
+ let currentProps = fixed.props;
965
+ if (currentProps && "on" in currentProps && currentProps.on !== void 0) {
966
+ const { on, ...restProps } = currentProps;
967
+ fixed = {
968
+ ...fixed,
969
+ props: restProps,
970
+ on
971
+ };
972
+ fixes.push(`Moved "on" from props to element level on "${key}".`);
973
+ }
974
+ currentProps = fixed.props;
975
+ if (currentProps && "repeat" in currentProps && currentProps.repeat !== void 0) {
976
+ const { repeat, ...restProps } = currentProps;
977
+ fixed = {
978
+ ...fixed,
979
+ props: restProps,
980
+ repeat
981
+ };
982
+ fixes.push(`Moved "repeat" from props to element level on "${key}".`);
983
+ }
984
+ fixedElements[key] = fixed;
985
+ }
986
+ return {
987
+ spec: { root: spec.root, elements: fixedElements },
988
+ fixes
989
+ };
990
+ }
991
+ function formatSpecIssues(issues) {
992
+ const errors = issues.filter((i) => i.severity === "error");
993
+ if (errors.length === 0) return "";
994
+ const lines = ["The generated UI spec has the following errors:"];
995
+ for (const issue of errors) {
996
+ lines.push(`- ${issue.message}`);
997
+ }
998
+ return lines.join("\n");
999
+ }
1000
+
812
1001
  // src/schema.ts
813
1002
  import { z as z5 } from "zod";
814
1003
  function createBuilder() {
@@ -833,6 +1022,7 @@ function defineSchema(builder, options) {
833
1022
  return {
834
1023
  definition,
835
1024
  promptTemplate: options?.promptTemplate,
1025
+ defaultRules: options?.defaultRules,
836
1026
  createCatalog(catalog) {
837
1027
  return createCatalogFromSchema(this, catalog);
838
1028
  }
@@ -988,15 +1178,95 @@ function generatePrompt(catalog, options) {
988
1178
  "Output JSONL (one JSON object per line) with patches to build a UI tree."
989
1179
  );
990
1180
  lines.push(
991
- "Each line is a JSON patch operation. Start with the root, then add each element."
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."
992
1182
  );
993
1183
  lines.push("");
994
1184
  lines.push("Example output (each line is a separate JSON object):");
995
1185
  lines.push("");
996
- lines.push(`{"op":"add","path":"/root","value":"card-1"}
997
- {"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Dashboard"},"children":["metric-1","chart-1"]}}
998
- {"op":"add","path":"/elements/metric-1","value":{"type":"Metric","props":{"label":"Revenue","valuePath":"analytics.revenue","format":"currency"},"children":[]}}
999
- {"op":"add","path":"/elements/chart-1","value":{"type":"Chart","props":{"type":"bar","dataPath":"analytics.salesByRegion"},"children":[]}}`);
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"}}
1195
+
1196
+ Note: state patches appear right after the elements that use them, so the UI fills in as it streams.`);
1197
+ lines.push("");
1198
+ lines.push("INITIAL STATE:");
1199
+ lines.push(
1200
+ "Specs include a /state field to seed the state model. Components with statePath read from and write to this state, and $path expressions read from it."
1201
+ );
1202
+ lines.push(
1203
+ "CRITICAL: You MUST include state patches whenever your UI displays data via $path expressions, uses repeat to iterate over arrays, or uses statePath bindings. Without state, $path references resolve to nothing and repeat lists render zero items."
1204
+ );
1205
+ lines.push(
1206
+ "Output state patches right after the elements that reference them, so the UI fills in progressively as it streams."
1207
+ );
1208
+ lines.push(
1209
+ "Stream state progressively - output one patch per array item instead of one giant blob:"
1210
+ );
1211
+ lines.push(
1212
+ ' For arrays: {"op":"add","path":"/state/posts/0","value":{"id":"1","title":"First Post",...}} then /state/posts/1, /state/posts/2, etc.'
1213
+ );
1214
+ lines.push(
1215
+ ' For scalars: {"op":"add","path":"/state/newTodoText","value":""}'
1216
+ );
1217
+ lines.push(
1218
+ ' Initialize the array first if needed: {"op":"add","path":"/state/posts","value":[]}'
1219
+ );
1220
+ lines.push(
1221
+ 'When content comes from the state model, use { "$path": "/some/path" } dynamic props to display it instead of hardcoding the same value in both state and props. The state model is the single source of truth.'
1222
+ );
1223
+ lines.push(
1224
+ "Include realistic sample data in state. For blogs: 3-4 posts with titles, excerpts, authors, dates. For product lists: 3-5 items with names, prices, descriptions. Never leave arrays empty."
1225
+ );
1226
+ lines.push("");
1227
+ lines.push("DYNAMIC LISTS (repeat field):");
1228
+ lines.push(
1229
+ 'Any element can have a top-level "repeat" field to render its children once per item in a state array: { "repeat": { "path": "/arrayPath", "key": "id" } }.'
1230
+ );
1231
+ lines.push(
1232
+ '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
+ );
1234
+ lines.push(
1235
+ 'Example: { "type": "Column", "props": { "gap": 8 }, "repeat": { "path": "/todos", "key": "id" }, "children": ["todo-item"] }'
1236
+ );
1237
+ lines.push(
1238
+ '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.'
1239
+ );
1240
+ lines.push(
1241
+ "ALWAYS use the repeat field for lists backed by state arrays. NEVER hardcode individual elements for each array item."
1242
+ );
1243
+ lines.push(
1244
+ 'IMPORTANT: "repeat" is a top-level field on the element (sibling of type/props/children), NOT inside props.'
1245
+ );
1246
+ lines.push("");
1247
+ lines.push("ARRAY STATE ACTIONS:");
1248
+ lines.push(
1249
+ 'Use action "pushState" to append items to arrays. Params: { path: "/arrayPath", value: { ...item }, clearPath: "/inputPath" }.'
1250
+ );
1251
+ lines.push(
1252
+ 'Values inside pushState can contain { "path": "/statePath" } references to read current state (e.g. the text from an input field).'
1253
+ );
1254
+ lines.push(
1255
+ 'Use "$id" inside a pushState value to auto-generate a unique ID.'
1256
+ );
1257
+ lines.push(
1258
+ 'Example: on: { "press": { "action": "pushState", "params": { "path": "/todos", "value": { "id": "$id", "title": { "path": "/newTodoText" }, "completed": false }, "clearPath": "/newTodoText" } } }'
1259
+ );
1260
+ lines.push(
1261
+ `Use action "removeState" to remove items from arrays by index. Params: { path: "/arrayPath", index: N }. Inside a repeated element's children, use "$index" for the current item index.`
1262
+ );
1263
+ lines.push(
1264
+ "For lists where users can add/remove items (todos, carts, etc.), use pushState and removeState instead of hardcoding with setState."
1265
+ );
1266
+ lines.push("");
1267
+ lines.push(
1268
+ '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
+ );
1000
1270
  lines.push("");
1001
1271
  const components = catalog.data.components;
1002
1272
  if (components) {
@@ -1006,8 +1276,9 @@ function generatePrompt(catalog, options) {
1006
1276
  const propsStr = def.props ? formatZodType(def.props) : "{}";
1007
1277
  const hasChildren = def.slots && def.slots.length > 0;
1008
1278
  const childrenStr = hasChildren ? " [accepts children]" : "";
1279
+ const eventsStr = def.events && def.events.length > 0 ? ` [events: ${def.events.join(", ")}]` : "";
1009
1280
  const descStr = def.description ? ` - ${def.description}` : "";
1010
- lines.push(`- ${name}: ${propsStr}${descStr}${childrenStr}`);
1281
+ lines.push(`- ${name}: ${propsStr}${descStr}${childrenStr}${eventsStr}`);
1011
1282
  }
1012
1283
  lines.push("");
1013
1284
  }
@@ -1020,16 +1291,90 @@ function generatePrompt(catalog, options) {
1020
1291
  }
1021
1292
  lines.push("");
1022
1293
  }
1294
+ lines.push("EVENTS (the `on` field):");
1295
+ lines.push(
1296
+ "Elements can have an optional `on` field to bind events to actions. The `on` field is a top-level field on the element (sibling of type/props/children), NOT inside props."
1297
+ );
1298
+ lines.push(
1299
+ 'Each key in `on` is an event name (from the component\'s supported events), and the value is an action binding: `{ "action": "<actionName>", "params": { ... } }`.'
1300
+ );
1301
+ lines.push("");
1302
+ lines.push("Example:");
1303
+ lines.push(
1304
+ ' {"type":"Button","props":{"label":"Save"},"on":{"press":{"action":"setState","params":{"path":"/saved","value":true}}},"children":[]}'
1305
+ );
1306
+ lines.push("");
1307
+ lines.push(
1308
+ 'Action params can use dynamic path references to read from state: { "path": "/statePath" }.'
1309
+ );
1310
+ lines.push(
1311
+ "IMPORTANT: Do NOT put action/actionParams inside props. Always use the `on` field for event bindings."
1312
+ );
1313
+ lines.push("");
1314
+ lines.push("VISIBILITY CONDITIONS:");
1315
+ lines.push(
1316
+ "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
+ );
1318
+ lines.push(
1319
+ 'Correct: {"type":"Column","props":{"gap":8},"visible":{"eq":[{"path":"/tab"},"home"]},"children":[...]}'
1320
+ );
1321
+ lines.push(
1322
+ '- `{ "eq": [{ "path": "/statePath" }, "value"] }` - visible when state at path equals value'
1323
+ );
1324
+ lines.push(
1325
+ '- `{ "neq": [{ "path": "/statePath" }, "value"] }` - visible when state at path does not equal value'
1326
+ );
1327
+ lines.push('- `{ "path": "/statePath" }` - visible when path is truthy');
1328
+ lines.push(
1329
+ '- `{ "and": [...] }`, `{ "or": [...] }`, `{ "not": {...} }` - combine conditions'
1330
+ );
1331
+ lines.push("- `true` / `false` - always visible/hidden");
1332
+ lines.push("");
1333
+ lines.push(
1334
+ "Use the Pressable component with on.press bound to setState to update state and drive visibility."
1335
+ );
1336
+ 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.'
1338
+ );
1339
+ lines.push("");
1340
+ lines.push("DYNAMIC PROPS:");
1341
+ lines.push(
1342
+ "Any prop value can be a dynamic expression that resolves based on state. Two forms are supported:"
1343
+ );
1344
+ lines.push("");
1345
+ lines.push(
1346
+ '1. State binding: `{ "$path": "/statePath" }` - resolves to the value at that state path.'
1347
+ );
1348
+ lines.push(
1349
+ ' Example: `"color": { "$path": "/theme/primary" }` reads the color from state.'
1350
+ );
1351
+ lines.push("");
1352
+ lines.push(
1353
+ '2. Conditional: `{ "$cond": <condition>, "$then": <value>, "$else": <value> }` - evaluates the condition (same syntax as visibility conditions) and picks the matching value.'
1354
+ );
1355
+ lines.push(
1356
+ ' Example: `"color": { "$cond": { "eq": [{ "path": "/activeTab" }, "home"] }, "$then": "#007AFF", "$else": "#8E8E93" }`'
1357
+ );
1358
+ lines.push(
1359
+ ' Example: `"name": { "$cond": { "eq": [{ "path": "/activeTab" }, "home"] }, "$then": "home", "$else": "home-outline" }`'
1360
+ );
1361
+ lines.push("");
1362
+ lines.push(
1363
+ "Use dynamic props instead of duplicating elements with opposing visible conditions when only prop values differ."
1364
+ );
1365
+ lines.push("");
1023
1366
  lines.push("RULES:");
1024
1367
  const baseRules = [
1025
1368
  "Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
1026
- 'First line sets root: {"op":"add","path":"/root","value":"<root-key>"}',
1369
+ 'First set root: {"op":"add","path":"/root","value":"<root-key>"}',
1027
1370
  'Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}',
1371
+ "Output /state patches right after the elements that use them, one per array item for progressive loading. REQUIRED whenever using $path, repeat, or statePath.",
1028
1372
  "ONLY use components listed above",
1029
1373
  "Each element value needs: type, props, children (array of child keys)",
1030
1374
  "Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')"
1031
1375
  ];
1032
- const allRules = [...baseRules, ...customRules];
1376
+ const schemaRules = catalog.schema.defaultRules ?? [];
1377
+ const allRules = [...baseRules, ...schemaRules, ...customRules];
1033
1378
  allRules.forEach((rule, i) => {
1034
1379
  lines.push(`${i + 1}. ${rule}`);
1035
1380
  });
@@ -1167,6 +1512,56 @@ function defineCatalog(schema, catalog) {
1167
1512
  return schema.createCatalog(catalog);
1168
1513
  }
1169
1514
 
1515
+ // src/prompt.ts
1516
+ function isNonEmptySpec(spec) {
1517
+ if (!spec || typeof spec !== "object") return false;
1518
+ const s = spec;
1519
+ return typeof s.root === "string" && typeof s.elements === "object" && s.elements !== null && Object.keys(s.elements).length > 0;
1520
+ }
1521
+ var PATCH_INSTRUCTIONS = `IMPORTANT: The current UI is already loaded. Output ONLY the patches needed to make the requested change:
1522
+ - To add a new element: {"op":"add","path":"/elements/new-key","value":{...}}
1523
+ - To modify an existing element: {"op":"replace","path":"/elements/existing-key","value":{...}}
1524
+ - To remove an element: {"op":"remove","path":"/elements/old-key"}
1525
+ - To update the root: {"op":"replace","path":"/root","value":"new-root-key"}
1526
+ - To add children: update the parent element with new children array
1527
+
1528
+ DO NOT output patches for elements that don't need to change. Only output what's necessary for the requested modification.`;
1529
+ function buildUserPrompt(options) {
1530
+ const { prompt, currentSpec, state, maxPromptLength } = options;
1531
+ let userText = String(prompt || "");
1532
+ if (maxPromptLength !== void 0 && maxPromptLength > 0) {
1533
+ userText = userText.slice(0, maxPromptLength);
1534
+ }
1535
+ if (isNonEmptySpec(currentSpec)) {
1536
+ const parts2 = [];
1537
+ parts2.push(
1538
+ `CURRENT UI STATE (already loaded, DO NOT recreate existing elements):`
1539
+ );
1540
+ parts2.push(JSON.stringify(currentSpec, null, 2));
1541
+ parts2.push("");
1542
+ parts2.push(`USER REQUEST: ${userText}`);
1543
+ if (state && Object.keys(state).length > 0) {
1544
+ parts2.push("");
1545
+ parts2.push(`AVAILABLE STATE:
1546
+ ${JSON.stringify(state, null, 2)}`);
1547
+ }
1548
+ parts2.push("");
1549
+ parts2.push(PATCH_INSTRUCTIONS);
1550
+ return parts2.join("\n");
1551
+ }
1552
+ const parts = [userText];
1553
+ if (state && Object.keys(state).length > 0) {
1554
+ parts.push(`
1555
+ AVAILABLE STATE:
1556
+ ${JSON.stringify(state, null, 2)}`);
1557
+ }
1558
+ parts.push(
1559
+ `
1560
+ Remember: Output /root first, then interleave /elements and /state patches so the UI fills in progressively as it streams. Output each state patch right after the elements that use it, one per array item.`
1561
+ );
1562
+ return parts.join("\n");
1563
+ }
1564
+
1170
1565
  // src/catalog.ts
1171
1566
  import { z as z6 } from "zod";
1172
1567
  function createCatalog(config) {
@@ -1276,7 +1671,7 @@ function generateCatalogPrompt(catalog) {
1276
1671
  lines.push("");
1277
1672
  lines.push("Components can have a `visible` property:");
1278
1673
  lines.push("- `true` / `false` - Always visible/hidden");
1279
- lines.push('- `{ "path": "/data/path" }` - Visible when path is truthy');
1674
+ lines.push('- `{ "path": "/state/path" }` - Visible when path is truthy');
1280
1675
  lines.push('- `{ "auth": "signedIn" }` - Visible when user is signed in');
1281
1676
  lines.push('- `{ "and": [...] }` - All conditions must be true');
1282
1677
  lines.push('- `{ "or": [...] }` - Any condition must be true');
@@ -1446,6 +1841,7 @@ function generateSystemPrompt(catalog, options = {}) {
1446
1841
  return lines.join("\n");
1447
1842
  }
1448
1843
  export {
1844
+ ActionBindingSchema,
1449
1845
  ActionConfirmSchema,
1450
1846
  ActionOnErrorSchema,
1451
1847
  ActionOnSuccessSchema,
@@ -1459,8 +1855,11 @@ export {
1459
1855
  ValidationConfigSchema,
1460
1856
  VisibilityConditionSchema,
1461
1857
  action,
1858
+ actionBinding,
1462
1859
  addByPath,
1463
1860
  applySpecStreamPatch,
1861
+ autoFixSpec,
1862
+ buildUserPrompt,
1464
1863
  builtInValidationFunctions,
1465
1864
  check,
1466
1865
  compileSpecStream,
@@ -1472,6 +1871,7 @@ export {
1472
1871
  evaluateVisibility,
1473
1872
  executeAction,
1474
1873
  findFormValue,
1874
+ formatSpecIssues,
1475
1875
  generateCatalogPrompt,
1476
1876
  generateSystemPrompt,
1477
1877
  getByPath,
@@ -1480,9 +1880,12 @@ export {
1480
1880
  removeByPath,
1481
1881
  resolveAction,
1482
1882
  resolveDynamicValue,
1883
+ resolveElementProps,
1884
+ resolvePropValue,
1483
1885
  runValidation,
1484
1886
  runValidationCheck,
1485
1887
  setByPath,
1888
+ validateSpec,
1486
1889
  visibility
1487
1890
  };
1488
1891
  //# sourceMappingURL=index.mjs.map