@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/README.md +126 -0
- package/dist/index.d.mts +366 -166
- package/dist/index.d.ts +366 -166
- package/dist/index.js +467 -56
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +459 -56
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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,
|
|
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(
|
|
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 {
|
|
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 },
|
|
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,
|
|
372
|
-
const rightValue = resolveDynamicValue(right,
|
|
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,
|
|
378
|
-
const rightValue = resolveDynamicValue(right,
|
|
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
|
-
|
|
385
|
+
stateModel
|
|
386
386
|
);
|
|
387
387
|
const rightValue = resolveDynamicValue(
|
|
388
388
|
right,
|
|
389
|
-
|
|
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
|
-
|
|
400
|
+
stateModel
|
|
401
401
|
);
|
|
402
402
|
const rightValue = resolveDynamicValue(
|
|
403
403
|
right,
|
|
404
|
-
|
|
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
|
-
|
|
415
|
+
stateModel
|
|
416
416
|
);
|
|
417
417
|
const rightValue = resolveDynamicValue(
|
|
418
418
|
right,
|
|
419
|
-
|
|
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
|
-
|
|
430
|
+
stateModel
|
|
431
431
|
);
|
|
432
432
|
const rightValue = resolveDynamicValue(
|
|
433
433
|
right,
|
|
434
|
-
|
|
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.
|
|
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
|
|
524
|
-
|
|
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
|
-
|
|
568
|
+
var ActionSchema = ActionBindingSchema;
|
|
569
|
+
function resolveAction(binding, stateModel) {
|
|
531
570
|
const resolvedParams = {};
|
|
532
|
-
if (
|
|
533
|
-
for (const [key, value] of Object.entries(
|
|
534
|
-
resolvedParams[key] = resolveDynamicValue(value,
|
|
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 =
|
|
576
|
+
let confirm = binding.confirm;
|
|
538
577
|
if (confirm) {
|
|
539
578
|
confirm = {
|
|
540
579
|
...confirm,
|
|
541
|
-
message: interpolateString(confirm.message,
|
|
542
|
-
title: interpolateString(confirm.title,
|
|
580
|
+
message: interpolateString(confirm.message, stateModel),
|
|
581
|
+
title: interpolateString(confirm.title, stateModel)
|
|
543
582
|
};
|
|
544
583
|
}
|
|
545
584
|
return {
|
|
546
|
-
|
|
585
|
+
action: binding.action,
|
|
547
586
|
params: resolvedParams,
|
|
548
587
|
confirm,
|
|
549
|
-
onSuccess:
|
|
550
|
-
onError:
|
|
588
|
+
onSuccess: binding.onSuccess,
|
|
589
|
+
onError: binding.onError
|
|
551
590
|
};
|
|
552
591
|
}
|
|
553
|
-
function interpolateString(template,
|
|
592
|
+
function interpolateString(template, stateModel) {
|
|
554
593
|
return template.replace(/\$\{([^}]+)\}/g, (_, path) => {
|
|
555
|
-
const value = resolveDynamicValue({ path },
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
590
|
-
/** Create a simple action */
|
|
591
|
-
simple: (
|
|
592
|
-
|
|
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: (
|
|
597
|
-
|
|
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: (
|
|
603
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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":"
|
|
997
|
-
{"op":"add","path":"/elements/
|
|
998
|
-
{"op":"add","path":"/elements/
|
|
999
|
-
{"op":"add","path":"/elements/
|
|
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
|
|
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
|
|
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": "/
|
|
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
|