@json-render/core 0.4.4 → 0.5.1

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.js CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ActionBindingSchema: () => ActionBindingSchema,
23
24
  ActionConfirmSchema: () => ActionConfirmSchema,
24
25
  ActionOnErrorSchema: () => ActionOnErrorSchema,
25
26
  ActionOnSuccessSchema: () => ActionOnSuccessSchema,
@@ -33,8 +34,11 @@ __export(index_exports, {
33
34
  ValidationConfigSchema: () => ValidationConfigSchema,
34
35
  VisibilityConditionSchema: () => VisibilityConditionSchema,
35
36
  action: () => action,
37
+ actionBinding: () => actionBinding,
36
38
  addByPath: () => addByPath,
37
39
  applySpecStreamPatch: () => applySpecStreamPatch,
40
+ autoFixSpec: () => autoFixSpec,
41
+ buildUserPrompt: () => buildUserPrompt,
38
42
  builtInValidationFunctions: () => builtInValidationFunctions,
39
43
  check: () => check,
40
44
  compileSpecStream: () => compileSpecStream,
@@ -46,6 +50,7 @@ __export(index_exports, {
46
50
  evaluateVisibility: () => evaluateVisibility,
47
51
  executeAction: () => executeAction,
48
52
  findFormValue: () => findFormValue,
53
+ formatSpecIssues: () => formatSpecIssues,
49
54
  generateCatalogPrompt: () => generateCatalogPrompt,
50
55
  generateSystemPrompt: () => generateSystemPrompt,
51
56
  getByPath: () => getByPath,
@@ -54,9 +59,12 @@ __export(index_exports, {
54
59
  removeByPath: () => removeByPath,
55
60
  resolveAction: () => resolveAction,
56
61
  resolveDynamicValue: () => resolveDynamicValue,
62
+ resolveElementProps: () => resolveElementProps,
63
+ resolvePropValue: () => resolvePropValue,
57
64
  runValidation: () => runValidation,
58
65
  runValidationCheck: () => runValidationCheck,
59
66
  setByPath: () => setByPath,
67
+ validateSpec: () => validateSpec,
60
68
  visibility: () => visibility
61
69
  });
62
70
  module.exports = __toCommonJS(index_exports);
@@ -82,12 +90,12 @@ var DynamicBooleanSchema = import_zod.z.union([
82
90
  import_zod.z.boolean(),
83
91
  import_zod.z.object({ path: import_zod.z.string() })
84
92
  ]);
85
- function resolveDynamicValue(value, dataModel) {
93
+ function resolveDynamicValue(value, stateModel) {
86
94
  if (value === null || value === void 0) {
87
95
  return void 0;
88
96
  }
89
97
  if (typeof value === "object" && "path" in value) {
90
- return getByPath(dataModel, value.path);
98
+ return getByPath(stateModel, value.path);
91
99
  }
92
100
  return value;
93
101
  }
@@ -415,7 +423,7 @@ var VisibilityConditionSchema = import_zod2.z.union([
415
423
  LogicExpressionSchema
416
424
  ]);
417
425
  function evaluateLogicExpression(expr, ctx) {
418
- const { dataModel } = ctx;
426
+ const { stateModel } = ctx;
419
427
  if ("and" in expr) {
420
428
  return expr.and.every((subExpr) => evaluateLogicExpression(subExpr, ctx));
421
429
  }
@@ -426,30 +434,30 @@ function evaluateLogicExpression(expr, ctx) {
426
434
  return !evaluateLogicExpression(expr.not, ctx);
427
435
  }
428
436
  if ("path" in expr) {
429
- const value = resolveDynamicValue({ path: expr.path }, dataModel);
437
+ const value = resolveDynamicValue({ path: expr.path }, stateModel);
430
438
  return Boolean(value);
431
439
  }
432
440
  if ("eq" in expr) {
433
441
  const [left, right] = expr.eq;
434
- const leftValue = resolveDynamicValue(left, dataModel);
435
- const rightValue = resolveDynamicValue(right, dataModel);
442
+ const leftValue = resolveDynamicValue(left, stateModel);
443
+ const rightValue = resolveDynamicValue(right, stateModel);
436
444
  return leftValue === rightValue;
437
445
  }
438
446
  if ("neq" in expr) {
439
447
  const [left, right] = expr.neq;
440
- const leftValue = resolveDynamicValue(left, dataModel);
441
- const rightValue = resolveDynamicValue(right, dataModel);
448
+ const leftValue = resolveDynamicValue(left, stateModel);
449
+ const rightValue = resolveDynamicValue(right, stateModel);
442
450
  return leftValue !== rightValue;
443
451
  }
444
452
  if ("gt" in expr) {
445
453
  const [left, right] = expr.gt;
446
454
  const leftValue = resolveDynamicValue(
447
455
  left,
448
- dataModel
456
+ stateModel
449
457
  );
450
458
  const rightValue = resolveDynamicValue(
451
459
  right,
452
- dataModel
460
+ stateModel
453
461
  );
454
462
  if (typeof leftValue === "number" && typeof rightValue === "number") {
455
463
  return leftValue > rightValue;
@@ -460,11 +468,11 @@ function evaluateLogicExpression(expr, ctx) {
460
468
  const [left, right] = expr.gte;
461
469
  const leftValue = resolveDynamicValue(
462
470
  left,
463
- dataModel
471
+ stateModel
464
472
  );
465
473
  const rightValue = resolveDynamicValue(
466
474
  right,
467
- dataModel
475
+ stateModel
468
476
  );
469
477
  if (typeof leftValue === "number" && typeof rightValue === "number") {
470
478
  return leftValue >= rightValue;
@@ -475,11 +483,11 @@ function evaluateLogicExpression(expr, ctx) {
475
483
  const [left, right] = expr.lt;
476
484
  const leftValue = resolveDynamicValue(
477
485
  left,
478
- dataModel
486
+ stateModel
479
487
  );
480
488
  const rightValue = resolveDynamicValue(
481
489
  right,
482
- dataModel
490
+ stateModel
483
491
  );
484
492
  if (typeof leftValue === "number" && typeof rightValue === "number") {
485
493
  return leftValue < rightValue;
@@ -490,11 +498,11 @@ function evaluateLogicExpression(expr, ctx) {
490
498
  const [left, right] = expr.lte;
491
499
  const leftValue = resolveDynamicValue(
492
500
  left,
493
- dataModel
501
+ stateModel
494
502
  );
495
503
  const rightValue = resolveDynamicValue(
496
504
  right,
497
- dataModel
505
+ stateModel
498
506
  );
499
507
  if (typeof leftValue === "number" && typeof rightValue === "number") {
500
508
  return leftValue <= rightValue;
@@ -511,7 +519,7 @@ function evaluateVisibility(condition, ctx) {
511
519
  return condition;
512
520
  }
513
521
  if ("path" in condition && !("and" in condition) && !("or" in condition)) {
514
- const value = resolveDynamicValue({ path: condition.path }, ctx.dataModel);
522
+ const value = resolveDynamicValue({ path: condition.path }, ctx.stateModel);
515
523
  return Boolean(value);
516
524
  }
517
525
  if ("auth" in condition) {
@@ -565,6 +573,44 @@ var visibility = {
565
573
  lte: (left, right) => ({ lte: [left, right] })
566
574
  };
567
575
 
576
+ // src/props.ts
577
+ function isPathExpression(value) {
578
+ return typeof value === "object" && value !== null && "$path" in value && typeof value.$path === "string";
579
+ }
580
+ function isCondExpression(value) {
581
+ return typeof value === "object" && value !== null && "$cond" in value && "$then" in value && "$else" in value;
582
+ }
583
+ function resolvePropValue(value, ctx) {
584
+ if (value === null || value === void 0) {
585
+ return value;
586
+ }
587
+ if (isPathExpression(value)) {
588
+ return getByPath(ctx.stateModel, value.$path);
589
+ }
590
+ if (isCondExpression(value)) {
591
+ const result = evaluateVisibility(value.$cond, ctx);
592
+ return resolvePropValue(result ? value.$then : value.$else, ctx);
593
+ }
594
+ if (Array.isArray(value)) {
595
+ return value.map((item) => resolvePropValue(item, ctx));
596
+ }
597
+ if (typeof value === "object") {
598
+ const resolved = {};
599
+ for (const [key, val] of Object.entries(value)) {
600
+ resolved[key] = resolvePropValue(val, ctx);
601
+ }
602
+ return resolved;
603
+ }
604
+ return value;
605
+ }
606
+ function resolveElementProps(props, ctx) {
607
+ const resolved = {};
608
+ for (const [key, value] of Object.entries(props)) {
609
+ resolved[key] = resolvePropValue(value, ctx);
610
+ }
611
+ return resolved;
612
+ }
613
+
568
614
  // src/actions.ts
569
615
  var import_zod3 = require("zod");
570
616
  var ActionConfirmSchema = import_zod3.z.object({
@@ -583,44 +629,45 @@ var ActionOnErrorSchema = import_zod3.z.union([
583
629
  import_zod3.z.object({ set: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()) }),
584
630
  import_zod3.z.object({ action: import_zod3.z.string() })
585
631
  ]);
586
- var ActionSchema = import_zod3.z.object({
587
- name: import_zod3.z.string(),
632
+ var ActionBindingSchema = import_zod3.z.object({
633
+ action: import_zod3.z.string(),
588
634
  params: import_zod3.z.record(import_zod3.z.string(), DynamicValueSchema).optional(),
589
635
  confirm: ActionConfirmSchema.optional(),
590
636
  onSuccess: ActionOnSuccessSchema.optional(),
591
637
  onError: ActionOnErrorSchema.optional()
592
638
  });
593
- function resolveAction(action2, dataModel) {
639
+ var ActionSchema = ActionBindingSchema;
640
+ function resolveAction(binding, stateModel) {
594
641
  const resolvedParams = {};
595
- if (action2.params) {
596
- for (const [key, value] of Object.entries(action2.params)) {
597
- resolvedParams[key] = resolveDynamicValue(value, dataModel);
642
+ if (binding.params) {
643
+ for (const [key, value] of Object.entries(binding.params)) {
644
+ resolvedParams[key] = resolveDynamicValue(value, stateModel);
598
645
  }
599
646
  }
600
- let confirm = action2.confirm;
647
+ let confirm = binding.confirm;
601
648
  if (confirm) {
602
649
  confirm = {
603
650
  ...confirm,
604
- message: interpolateString(confirm.message, dataModel),
605
- title: interpolateString(confirm.title, dataModel)
651
+ message: interpolateString(confirm.message, stateModel),
652
+ title: interpolateString(confirm.title, stateModel)
606
653
  };
607
654
  }
608
655
  return {
609
- name: action2.name,
656
+ action: binding.action,
610
657
  params: resolvedParams,
611
658
  confirm,
612
- onSuccess: action2.onSuccess,
613
- onError: action2.onError
659
+ onSuccess: binding.onSuccess,
660
+ onError: binding.onError
614
661
  };
615
662
  }
616
- function interpolateString(template, dataModel) {
663
+ function interpolateString(template, stateModel) {
617
664
  return template.replace(/\$\{([^}]+)\}/g, (_, path) => {
618
- const value = resolveDynamicValue({ path }, dataModel);
665
+ const value = resolveDynamicValue({ path }, stateModel);
619
666
  return String(value ?? "");
620
667
  });
621
668
  }
622
669
  async function executeAction(ctx) {
623
- const { action: action2, handler, setData, navigate, executeAction: executeAction2 } = ctx;
670
+ const { action: action2, handler, setState, navigate, executeAction: executeAction2 } = ctx;
624
671
  try {
625
672
  await handler(action2.params);
626
673
  if (action2.onSuccess) {
@@ -628,7 +675,7 @@ async function executeAction(ctx) {
628
675
  navigate(action2.onSuccess.navigate);
629
676
  } else if ("set" in action2.onSuccess) {
630
677
  for (const [path, value] of Object.entries(action2.onSuccess.set)) {
631
- setData(path, value);
678
+ setState(path, value);
632
679
  }
633
680
  } else if ("action" in action2.onSuccess && executeAction2) {
634
681
  await executeAction2(action2.onSuccess.action);
@@ -639,7 +686,7 @@ async function executeAction(ctx) {
639
686
  if ("set" in action2.onError) {
640
687
  for (const [path, value] of Object.entries(action2.onError.set)) {
641
688
  const resolvedValue = typeof value === "string" && value === "$error.message" ? error.message : value;
642
- setData(path, resolvedValue);
689
+ setState(path, resolvedValue);
643
690
  }
644
691
  } else if ("action" in action2.onError && executeAction2) {
645
692
  await executeAction2(action2.onError.action);
@@ -649,25 +696,26 @@ async function executeAction(ctx) {
649
696
  }
650
697
  }
651
698
  }
652
- var action = {
653
- /** Create a simple action */
654
- simple: (name, params) => ({
655
- name,
699
+ var actionBinding = {
700
+ /** Create a simple action binding */
701
+ simple: (actionName, params) => ({
702
+ action: actionName,
656
703
  params
657
704
  }),
658
- /** Create an action with confirmation */
659
- withConfirm: (name, confirm, params) => ({
660
- name,
705
+ /** Create an action binding with confirmation */
706
+ withConfirm: (actionName, confirm, params) => ({
707
+ action: actionName,
661
708
  params,
662
709
  confirm
663
710
  }),
664
- /** Create an action with success handler */
665
- withSuccess: (name, onSuccess, params) => ({
666
- name,
711
+ /** Create an action binding with success handler */
712
+ withSuccess: (actionName, onSuccess, params) => ({
713
+ action: actionName,
667
714
  params,
668
715
  onSuccess
669
716
  })
670
717
  };
718
+ var action = actionBinding;
671
719
 
672
720
  // src/validation.ts
673
721
  var import_zod4 = require("zod");
@@ -776,11 +824,11 @@ var builtInValidationFunctions = {
776
824
  }
777
825
  };
778
826
  function runValidationCheck(check2, ctx) {
779
- const { value, dataModel, customFunctions } = ctx;
827
+ const { value, stateModel, customFunctions } = ctx;
780
828
  const resolvedArgs = {};
781
829
  if (check2.args) {
782
830
  for (const [key, argValue] of Object.entries(check2.args)) {
783
- resolvedArgs[key] = resolveDynamicValue(argValue, dataModel);
831
+ resolvedArgs[key] = resolveDynamicValue(argValue, stateModel);
784
832
  }
785
833
  }
786
834
  const fn = builtInValidationFunctions[check2.fn] ?? customFunctions?.[check2.fn];
@@ -805,7 +853,7 @@ function runValidation(config, ctx) {
805
853
  const errors = [];
806
854
  if (config.enabled) {
807
855
  const enabled = evaluateLogicExpression(config.enabled, {
808
- dataModel: ctx.dataModel,
856
+ stateModel: ctx.stateModel,
809
857
  authState: ctx.authState
810
858
  });
811
859
  if (!enabled) {
@@ -872,6 +920,155 @@ var check = {
872
920
  })
873
921
  };
874
922
 
923
+ // src/spec-validator.ts
924
+ function validateSpec(spec, options = {}) {
925
+ const { checkOrphans = false } = options;
926
+ const issues = [];
927
+ if (!spec.root) {
928
+ issues.push({
929
+ severity: "error",
930
+ message: "Spec has no root element defined.",
931
+ code: "missing_root"
932
+ });
933
+ return { valid: false, issues };
934
+ }
935
+ if (!spec.elements[spec.root]) {
936
+ issues.push({
937
+ severity: "error",
938
+ message: `Root element "${spec.root}" not found in elements map.`,
939
+ code: "root_not_found"
940
+ });
941
+ }
942
+ if (Object.keys(spec.elements).length === 0) {
943
+ issues.push({
944
+ severity: "error",
945
+ message: "Spec has no elements.",
946
+ code: "empty_spec"
947
+ });
948
+ return { valid: false, issues };
949
+ }
950
+ for (const [key, element] of Object.entries(spec.elements)) {
951
+ if (element.children) {
952
+ for (const childKey of element.children) {
953
+ if (!spec.elements[childKey]) {
954
+ issues.push({
955
+ severity: "error",
956
+ message: `Element "${key}" references child "${childKey}" which does not exist in the elements map.`,
957
+ elementKey: key,
958
+ code: "missing_child"
959
+ });
960
+ }
961
+ }
962
+ }
963
+ const props = element.props;
964
+ if (props && "visible" in props && props.visible !== void 0) {
965
+ issues.push({
966
+ severity: "error",
967
+ message: `Element "${key}" has "visible" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
968
+ elementKey: key,
969
+ code: "visible_in_props"
970
+ });
971
+ }
972
+ if (props && "on" in props && props.on !== void 0) {
973
+ issues.push({
974
+ severity: "error",
975
+ message: `Element "${key}" has "on" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
976
+ elementKey: key,
977
+ code: "on_in_props"
978
+ });
979
+ }
980
+ if (props && "repeat" in props && props.repeat !== void 0) {
981
+ issues.push({
982
+ severity: "error",
983
+ message: `Element "${key}" has "repeat" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
984
+ elementKey: key,
985
+ code: "repeat_in_props"
986
+ });
987
+ }
988
+ }
989
+ if (checkOrphans) {
990
+ const reachable = /* @__PURE__ */ new Set();
991
+ const walk = (key) => {
992
+ if (reachable.has(key)) return;
993
+ reachable.add(key);
994
+ const el = spec.elements[key];
995
+ if (el?.children) {
996
+ for (const childKey of el.children) {
997
+ if (spec.elements[childKey]) {
998
+ walk(childKey);
999
+ }
1000
+ }
1001
+ }
1002
+ };
1003
+ if (spec.elements[spec.root]) {
1004
+ walk(spec.root);
1005
+ }
1006
+ for (const key of Object.keys(spec.elements)) {
1007
+ if (!reachable.has(key)) {
1008
+ issues.push({
1009
+ severity: "warning",
1010
+ message: `Element "${key}" is not reachable from root "${spec.root}".`,
1011
+ elementKey: key,
1012
+ code: "orphaned_element"
1013
+ });
1014
+ }
1015
+ }
1016
+ }
1017
+ const hasErrors = issues.some((i) => i.severity === "error");
1018
+ return { valid: !hasErrors, issues };
1019
+ }
1020
+ function autoFixSpec(spec) {
1021
+ const fixes = [];
1022
+ const fixedElements = {};
1023
+ for (const [key, element] of Object.entries(spec.elements)) {
1024
+ const props = element.props;
1025
+ let fixed = element;
1026
+ if (props && "visible" in props && props.visible !== void 0) {
1027
+ const { visible, ...restProps } = fixed.props;
1028
+ fixed = {
1029
+ ...fixed,
1030
+ props: restProps,
1031
+ visible
1032
+ };
1033
+ fixes.push(`Moved "visible" from props to element level on "${key}".`);
1034
+ }
1035
+ let currentProps = fixed.props;
1036
+ if (currentProps && "on" in currentProps && currentProps.on !== void 0) {
1037
+ const { on, ...restProps } = currentProps;
1038
+ fixed = {
1039
+ ...fixed,
1040
+ props: restProps,
1041
+ on
1042
+ };
1043
+ fixes.push(`Moved "on" from props to element level on "${key}".`);
1044
+ }
1045
+ currentProps = fixed.props;
1046
+ if (currentProps && "repeat" in currentProps && currentProps.repeat !== void 0) {
1047
+ const { repeat, ...restProps } = currentProps;
1048
+ fixed = {
1049
+ ...fixed,
1050
+ props: restProps,
1051
+ repeat
1052
+ };
1053
+ fixes.push(`Moved "repeat" from props to element level on "${key}".`);
1054
+ }
1055
+ fixedElements[key] = fixed;
1056
+ }
1057
+ return {
1058
+ spec: { root: spec.root, elements: fixedElements },
1059
+ fixes
1060
+ };
1061
+ }
1062
+ function formatSpecIssues(issues) {
1063
+ const errors = issues.filter((i) => i.severity === "error");
1064
+ if (errors.length === 0) return "";
1065
+ const lines = ["The generated UI spec has the following errors:"];
1066
+ for (const issue of errors) {
1067
+ lines.push(`- ${issue.message}`);
1068
+ }
1069
+ return lines.join("\n");
1070
+ }
1071
+
875
1072
  // src/schema.ts
876
1073
  var import_zod5 = require("zod");
877
1074
  function createBuilder() {
@@ -896,6 +1093,7 @@ function defineSchema(builder, options) {
896
1093
  return {
897
1094
  definition,
898
1095
  promptTemplate: options?.promptTemplate,
1096
+ defaultRules: options?.defaultRules,
899
1097
  createCatalog(catalog) {
900
1098
  return createCatalogFromSchema(this, catalog);
901
1099
  }
@@ -1051,15 +1249,95 @@ function generatePrompt(catalog, options) {
1051
1249
  "Output JSONL (one JSON object per line) with patches to build a UI tree."
1052
1250
  );
1053
1251
  lines.push(
1054
- "Each line is a JSON patch operation. Start with the root, then add each element."
1252
+ "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."
1055
1253
  );
1056
1254
  lines.push("");
1057
1255
  lines.push("Example output (each line is a separate JSON object):");
1058
1256
  lines.push("");
1059
- lines.push(`{"op":"add","path":"/root","value":"card-1"}
1060
- {"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Dashboard"},"children":["metric-1","chart-1"]}}
1061
- {"op":"add","path":"/elements/metric-1","value":{"type":"Metric","props":{"label":"Revenue","valuePath":"analytics.revenue","format":"currency"},"children":[]}}
1062
- {"op":"add","path":"/elements/chart-1","value":{"type":"Chart","props":{"type":"bar","dataPath":"analytics.salesByRegion"},"children":[]}}`);
1257
+ lines.push(`{"op":"add","path":"/root","value":"blog"}
1258
+ {"op":"add","path":"/elements/blog","value":{"type":"Stack","props":{"direction":"vertical","gap":"md"},"children":["heading","posts-grid"]}}
1259
+ {"op":"add","path":"/elements/heading","value":{"type":"Heading","props":{"text":"Blog","level":"h1"},"children":[]}}
1260
+ {"op":"add","path":"/elements/posts-grid","value":{"type":"Grid","props":{"columns":2,"gap":"md"},"repeat":{"path":"/posts","key":"id"},"children":["post-card"]}}
1261
+ {"op":"add","path":"/elements/post-card","value":{"type":"Card","props":{"title":{"$path":"$item/title"}},"children":["post-meta"]}}
1262
+ {"op":"add","path":"/elements/post-meta","value":{"type":"Text","props":{"text":{"$path":"$item/author"},"variant":"muted"},"children":[]}}
1263
+ {"op":"add","path":"/state/posts","value":[]}
1264
+ {"op":"add","path":"/state/posts/0","value":{"id":"1","title":"Getting Started","author":"Jane","date":"Jan 15"}}
1265
+ {"op":"add","path":"/state/posts/1","value":{"id":"2","title":"Advanced Tips","author":"Bob","date":"Feb 3"}}
1266
+
1267
+ Note: state patches appear right after the elements that use them, so the UI fills in as it streams.`);
1268
+ lines.push("");
1269
+ lines.push("INITIAL STATE:");
1270
+ lines.push(
1271
+ "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."
1272
+ );
1273
+ lines.push(
1274
+ "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."
1275
+ );
1276
+ lines.push(
1277
+ "Output state patches right after the elements that reference them, so the UI fills in progressively as it streams."
1278
+ );
1279
+ lines.push(
1280
+ "Stream state progressively - output one patch per array item instead of one giant blob:"
1281
+ );
1282
+ lines.push(
1283
+ ' For arrays: {"op":"add","path":"/state/posts/0","value":{"id":"1","title":"First Post",...}} then /state/posts/1, /state/posts/2, etc.'
1284
+ );
1285
+ lines.push(
1286
+ ' For scalars: {"op":"add","path":"/state/newTodoText","value":""}'
1287
+ );
1288
+ lines.push(
1289
+ ' Initialize the array first if needed: {"op":"add","path":"/state/posts","value":[]}'
1290
+ );
1291
+ lines.push(
1292
+ '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.'
1293
+ );
1294
+ lines.push(
1295
+ "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."
1296
+ );
1297
+ lines.push("");
1298
+ lines.push("DYNAMIC LISTS (repeat field):");
1299
+ lines.push(
1300
+ '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" } }.'
1301
+ );
1302
+ lines.push(
1303
+ '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.'
1304
+ );
1305
+ lines.push(
1306
+ 'Example: { "type": "Column", "props": { "gap": 8 }, "repeat": { "path": "/todos", "key": "id" }, "children": ["todo-item"] }'
1307
+ );
1308
+ lines.push(
1309
+ '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.'
1310
+ );
1311
+ lines.push(
1312
+ "ALWAYS use the repeat field for lists backed by state arrays. NEVER hardcode individual elements for each array item."
1313
+ );
1314
+ lines.push(
1315
+ 'IMPORTANT: "repeat" is a top-level field on the element (sibling of type/props/children), NOT inside props.'
1316
+ );
1317
+ lines.push("");
1318
+ lines.push("ARRAY STATE ACTIONS:");
1319
+ lines.push(
1320
+ 'Use action "pushState" to append items to arrays. Params: { path: "/arrayPath", value: { ...item }, clearPath: "/inputPath" }.'
1321
+ );
1322
+ lines.push(
1323
+ 'Values inside pushState can contain { "path": "/statePath" } references to read current state (e.g. the text from an input field).'
1324
+ );
1325
+ lines.push(
1326
+ 'Use "$id" inside a pushState value to auto-generate a unique ID.'
1327
+ );
1328
+ lines.push(
1329
+ 'Example: on: { "press": { "action": "pushState", "params": { "path": "/todos", "value": { "id": "$id", "title": { "path": "/newTodoText" }, "completed": false }, "clearPath": "/newTodoText" } } }'
1330
+ );
1331
+ lines.push(
1332
+ `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.`
1333
+ );
1334
+ lines.push(
1335
+ "For lists where users can add/remove items (todos, carts, etc.), use pushState and removeState instead of hardcoding with setState."
1336
+ );
1337
+ lines.push("");
1338
+ lines.push(
1339
+ '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.'
1340
+ );
1063
1341
  lines.push("");
1064
1342
  const components = catalog.data.components;
1065
1343
  if (components) {
@@ -1069,8 +1347,9 @@ function generatePrompt(catalog, options) {
1069
1347
  const propsStr = def.props ? formatZodType(def.props) : "{}";
1070
1348
  const hasChildren = def.slots && def.slots.length > 0;
1071
1349
  const childrenStr = hasChildren ? " [accepts children]" : "";
1350
+ const eventsStr = def.events && def.events.length > 0 ? ` [events: ${def.events.join(", ")}]` : "";
1072
1351
  const descStr = def.description ? ` - ${def.description}` : "";
1073
- lines.push(`- ${name}: ${propsStr}${descStr}${childrenStr}`);
1352
+ lines.push(`- ${name}: ${propsStr}${descStr}${childrenStr}${eventsStr}`);
1074
1353
  }
1075
1354
  lines.push("");
1076
1355
  }
@@ -1083,16 +1362,90 @@ function generatePrompt(catalog, options) {
1083
1362
  }
1084
1363
  lines.push("");
1085
1364
  }
1365
+ lines.push("EVENTS (the `on` field):");
1366
+ lines.push(
1367
+ "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."
1368
+ );
1369
+ lines.push(
1370
+ 'Each key in `on` is an event name (from the component\'s supported events), and the value is an action binding: `{ "action": "<actionName>", "params": { ... } }`.'
1371
+ );
1372
+ lines.push("");
1373
+ lines.push("Example:");
1374
+ lines.push(
1375
+ ' {"type":"Button","props":{"label":"Save"},"on":{"press":{"action":"setState","params":{"path":"/saved","value":true}}},"children":[]}'
1376
+ );
1377
+ lines.push("");
1378
+ lines.push(
1379
+ 'Action params can use dynamic path references to read from state: { "path": "/statePath" }.'
1380
+ );
1381
+ lines.push(
1382
+ "IMPORTANT: Do NOT put action/actionParams inside props. Always use the `on` field for event bindings."
1383
+ );
1384
+ lines.push("");
1385
+ lines.push("VISIBILITY CONDITIONS:");
1386
+ lines.push(
1387
+ "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."
1388
+ );
1389
+ lines.push(
1390
+ 'Correct: {"type":"Column","props":{"gap":8},"visible":{"eq":[{"path":"/tab"},"home"]},"children":[...]}'
1391
+ );
1392
+ lines.push(
1393
+ '- `{ "eq": [{ "path": "/statePath" }, "value"] }` - visible when state at path equals value'
1394
+ );
1395
+ lines.push(
1396
+ '- `{ "neq": [{ "path": "/statePath" }, "value"] }` - visible when state at path does not equal value'
1397
+ );
1398
+ lines.push('- `{ "path": "/statePath" }` - visible when path is truthy');
1399
+ lines.push(
1400
+ '- `{ "and": [...] }`, `{ "or": [...] }`, `{ "not": {...} }` - combine conditions'
1401
+ );
1402
+ lines.push("- `true` / `false` - always visible/hidden");
1403
+ lines.push("");
1404
+ lines.push(
1405
+ "Use the Pressable component with on.press bound to setState to update state and drive visibility."
1406
+ );
1407
+ lines.push(
1408
+ '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.'
1409
+ );
1410
+ lines.push("");
1411
+ lines.push("DYNAMIC PROPS:");
1412
+ lines.push(
1413
+ "Any prop value can be a dynamic expression that resolves based on state. Two forms are supported:"
1414
+ );
1415
+ lines.push("");
1416
+ lines.push(
1417
+ '1. State binding: `{ "$path": "/statePath" }` - resolves to the value at that state path.'
1418
+ );
1419
+ lines.push(
1420
+ ' Example: `"color": { "$path": "/theme/primary" }` reads the color from state.'
1421
+ );
1422
+ lines.push("");
1423
+ lines.push(
1424
+ '2. Conditional: `{ "$cond": <condition>, "$then": <value>, "$else": <value> }` - evaluates the condition (same syntax as visibility conditions) and picks the matching value.'
1425
+ );
1426
+ lines.push(
1427
+ ' Example: `"color": { "$cond": { "eq": [{ "path": "/activeTab" }, "home"] }, "$then": "#007AFF", "$else": "#8E8E93" }`'
1428
+ );
1429
+ lines.push(
1430
+ ' Example: `"name": { "$cond": { "eq": [{ "path": "/activeTab" }, "home"] }, "$then": "home", "$else": "home-outline" }`'
1431
+ );
1432
+ lines.push("");
1433
+ lines.push(
1434
+ "Use dynamic props instead of duplicating elements with opposing visible conditions when only prop values differ."
1435
+ );
1436
+ lines.push("");
1086
1437
  lines.push("RULES:");
1087
1438
  const baseRules = [
1088
1439
  "Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
1089
- 'First line sets root: {"op":"add","path":"/root","value":"<root-key>"}',
1440
+ 'First set root: {"op":"add","path":"/root","value":"<root-key>"}',
1090
1441
  'Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}',
1442
+ "Output /state patches right after the elements that use them, one per array item for progressive loading. REQUIRED whenever using $path, repeat, or statePath.",
1091
1443
  "ONLY use components listed above",
1092
1444
  "Each element value needs: type, props, children (array of child keys)",
1093
1445
  "Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')"
1094
1446
  ];
1095
- const allRules = [...baseRules, ...customRules];
1447
+ const schemaRules = catalog.schema.defaultRules ?? [];
1448
+ const allRules = [...baseRules, ...schemaRules, ...customRules];
1096
1449
  allRules.forEach((rule, i) => {
1097
1450
  lines.push(`${i + 1}. ${rule}`);
1098
1451
  });
@@ -1230,6 +1583,56 @@ function defineCatalog(schema, catalog) {
1230
1583
  return schema.createCatalog(catalog);
1231
1584
  }
1232
1585
 
1586
+ // src/prompt.ts
1587
+ function isNonEmptySpec(spec) {
1588
+ if (!spec || typeof spec !== "object") return false;
1589
+ const s = spec;
1590
+ return typeof s.root === "string" && typeof s.elements === "object" && s.elements !== null && Object.keys(s.elements).length > 0;
1591
+ }
1592
+ var PATCH_INSTRUCTIONS = `IMPORTANT: The current UI is already loaded. Output ONLY the patches needed to make the requested change:
1593
+ - To add a new element: {"op":"add","path":"/elements/new-key","value":{...}}
1594
+ - To modify an existing element: {"op":"replace","path":"/elements/existing-key","value":{...}}
1595
+ - To remove an element: {"op":"remove","path":"/elements/old-key"}
1596
+ - To update the root: {"op":"replace","path":"/root","value":"new-root-key"}
1597
+ - To add children: update the parent element with new children array
1598
+
1599
+ DO NOT output patches for elements that don't need to change. Only output what's necessary for the requested modification.`;
1600
+ function buildUserPrompt(options) {
1601
+ const { prompt, currentSpec, state, maxPromptLength } = options;
1602
+ let userText = String(prompt || "");
1603
+ if (maxPromptLength !== void 0 && maxPromptLength > 0) {
1604
+ userText = userText.slice(0, maxPromptLength);
1605
+ }
1606
+ if (isNonEmptySpec(currentSpec)) {
1607
+ const parts2 = [];
1608
+ parts2.push(
1609
+ `CURRENT UI STATE (already loaded, DO NOT recreate existing elements):`
1610
+ );
1611
+ parts2.push(JSON.stringify(currentSpec, null, 2));
1612
+ parts2.push("");
1613
+ parts2.push(`USER REQUEST: ${userText}`);
1614
+ if (state && Object.keys(state).length > 0) {
1615
+ parts2.push("");
1616
+ parts2.push(`AVAILABLE STATE:
1617
+ ${JSON.stringify(state, null, 2)}`);
1618
+ }
1619
+ parts2.push("");
1620
+ parts2.push(PATCH_INSTRUCTIONS);
1621
+ return parts2.join("\n");
1622
+ }
1623
+ const parts = [userText];
1624
+ if (state && Object.keys(state).length > 0) {
1625
+ parts.push(`
1626
+ AVAILABLE STATE:
1627
+ ${JSON.stringify(state, null, 2)}`);
1628
+ }
1629
+ parts.push(
1630
+ `
1631
+ 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.`
1632
+ );
1633
+ return parts.join("\n");
1634
+ }
1635
+
1233
1636
  // src/catalog.ts
1234
1637
  var import_zod6 = require("zod");
1235
1638
  function createCatalog(config) {
@@ -1339,7 +1742,7 @@ function generateCatalogPrompt(catalog) {
1339
1742
  lines.push("");
1340
1743
  lines.push("Components can have a `visible` property:");
1341
1744
  lines.push("- `true` / `false` - Always visible/hidden");
1342
- lines.push('- `{ "path": "/data/path" }` - Visible when path is truthy');
1745
+ lines.push('- `{ "path": "/state/path" }` - Visible when path is truthy');
1343
1746
  lines.push('- `{ "auth": "signedIn" }` - Visible when user is signed in');
1344
1747
  lines.push('- `{ "and": [...] }` - All conditions must be true');
1345
1748
  lines.push('- `{ "or": [...] }` - Any condition must be true');
@@ -1510,6 +1913,7 @@ function generateSystemPrompt(catalog, options = {}) {
1510
1913
  }
1511
1914
  // Annotate the CommonJS export names for ESM import in node:
1512
1915
  0 && (module.exports = {
1916
+ ActionBindingSchema,
1513
1917
  ActionConfirmSchema,
1514
1918
  ActionOnErrorSchema,
1515
1919
  ActionOnSuccessSchema,
@@ -1523,8 +1927,11 @@ function generateSystemPrompt(catalog, options = {}) {
1523
1927
  ValidationConfigSchema,
1524
1928
  VisibilityConditionSchema,
1525
1929
  action,
1930
+ actionBinding,
1526
1931
  addByPath,
1527
1932
  applySpecStreamPatch,
1933
+ autoFixSpec,
1934
+ buildUserPrompt,
1528
1935
  builtInValidationFunctions,
1529
1936
  check,
1530
1937
  compileSpecStream,
@@ -1536,6 +1943,7 @@ function generateSystemPrompt(catalog, options = {}) {
1536
1943
  evaluateVisibility,
1537
1944
  executeAction,
1538
1945
  findFormValue,
1946
+ formatSpecIssues,
1539
1947
  generateCatalogPrompt,
1540
1948
  generateSystemPrompt,
1541
1949
  getByPath,
@@ -1544,9 +1952,12 @@ function generateSystemPrompt(catalog, options = {}) {
1544
1952
  removeByPath,
1545
1953
  resolveAction,
1546
1954
  resolveDynamicValue,
1955
+ resolveElementProps,
1956
+ resolvePropValue,
1547
1957
  runValidation,
1548
1958
  runValidationCheck,
1549
1959
  setByPath,
1960
+ validateSpec,
1550
1961
  visibility
1551
1962
  });
1552
1963
  //# sourceMappingURL=index.js.map