@json-render/core 0.5.2 → 0.6.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
@@ -5,26 +5,26 @@ var DynamicValueSchema = z.union([
5
5
  z.number(),
6
6
  z.boolean(),
7
7
  z.null(),
8
- z.object({ path: z.string() })
8
+ z.object({ $state: z.string() })
9
9
  ]);
10
10
  var DynamicStringSchema = z.union([
11
11
  z.string(),
12
- z.object({ path: z.string() })
12
+ z.object({ $state: z.string() })
13
13
  ]);
14
14
  var DynamicNumberSchema = z.union([
15
15
  z.number(),
16
- z.object({ path: z.string() })
16
+ z.object({ $state: z.string() })
17
17
  ]);
18
18
  var DynamicBooleanSchema = z.union([
19
19
  z.boolean(),
20
- z.object({ path: z.string() })
20
+ z.object({ $state: z.string() })
21
21
  ]);
22
22
  function resolveDynamicValue(value, stateModel) {
23
23
  if (value === null || value === void 0) {
24
24
  return void 0;
25
25
  }
26
- if (typeof value === "object" && "path" in value) {
27
- return getByPath(stateModel, value.path);
26
+ if (typeof value === "object" && "$state" in value) {
27
+ return getByPath(stateModel, value.$state);
28
28
  }
29
29
  return value;
30
30
  }
@@ -171,7 +171,7 @@ function deepEqual(a, b) {
171
171
  if (aKeys.length !== bKeys.length) return false;
172
172
  return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
173
173
  }
174
- function findFormValue(fieldName, params, data) {
174
+ function findFormValue(fieldName, params, state) {
175
175
  if (params?.[fieldName] !== void 0) {
176
176
  const val = params[fieldName];
177
177
  if (typeof val !== "string" || !val.includes(".")) {
@@ -188,19 +188,15 @@ function findFormValue(fieldName, params, data) {
188
188
  }
189
189
  }
190
190
  }
191
- if (data) {
192
- for (const key of Object.keys(data)) {
191
+ if (state) {
192
+ for (const key of Object.keys(state)) {
193
193
  if (key === fieldName || key.endsWith(`.${fieldName}`)) {
194
- return data[key];
194
+ return state[key];
195
195
  }
196
196
  }
197
- const prefixes = ["form", "newCustomer", "customer", ""];
198
- for (const prefix of prefixes) {
199
- const path = prefix ? `${prefix}/${fieldName}` : fieldName;
200
- const val = getByPath(data, path);
201
- if (val !== void 0) {
202
- return val;
203
- }
197
+ const val = getByPath(state, fieldName);
198
+ if (val !== void 0) {
199
+ return val;
204
200
  }
205
201
  }
206
202
  return void 0;
@@ -254,6 +250,44 @@ function applySpecStreamPatch(obj, patch) {
254
250
  }
255
251
  return obj;
256
252
  }
253
+ function applySpecPatch(spec, patch) {
254
+ applySpecStreamPatch(spec, patch);
255
+ return spec;
256
+ }
257
+ function nestedToFlat(nested) {
258
+ const elements = {};
259
+ let counter = 0;
260
+ function walk(node) {
261
+ const key = `el-${counter++}`;
262
+ const { type, props, children: rawChildren, ...rest } = node;
263
+ const childKeys = [];
264
+ if (Array.isArray(rawChildren)) {
265
+ for (const child of rawChildren) {
266
+ if (child && typeof child === "object" && "type" in child) {
267
+ childKeys.push(walk(child));
268
+ }
269
+ }
270
+ }
271
+ const element = {
272
+ type: type ?? "unknown",
273
+ props: props ?? {},
274
+ children: childKeys
275
+ };
276
+ for (const [k, v] of Object.entries(rest)) {
277
+ if (k !== "state" && v !== void 0) {
278
+ element[k] = v;
279
+ }
280
+ }
281
+ elements[key] = element;
282
+ return key;
283
+ }
284
+ const root = walk(nested);
285
+ const spec = { root, elements };
286
+ if (nested.state && typeof nested.state === "object" && !Array.isArray(nested.state)) {
287
+ spec.state = nested.state;
288
+ }
289
+ return spec;
290
+ }
257
291
  function compileSpecStream(stream, initial = {}) {
258
292
  const lines = stream.split("\n");
259
293
  const result = { ...initial };
@@ -316,129 +350,304 @@ function createSpecStreamCompiler(initial = {}) {
316
350
  }
317
351
  };
318
352
  }
353
+ function createMixedStreamParser(callbacks) {
354
+ let buffer = "";
355
+ let inSpecFence = false;
356
+ function processLine(line) {
357
+ const trimmed = line.trim();
358
+ if (!inSpecFence && trimmed.startsWith("```spec")) {
359
+ inSpecFence = true;
360
+ return;
361
+ }
362
+ if (inSpecFence && trimmed === "```") {
363
+ inSpecFence = false;
364
+ return;
365
+ }
366
+ if (!trimmed) return;
367
+ if (inSpecFence) {
368
+ const patch2 = parseSpecStreamLine(trimmed);
369
+ if (patch2) {
370
+ callbacks.onPatch(patch2);
371
+ }
372
+ return;
373
+ }
374
+ const patch = parseSpecStreamLine(trimmed);
375
+ if (patch) {
376
+ callbacks.onPatch(patch);
377
+ } else {
378
+ callbacks.onText(line);
379
+ }
380
+ }
381
+ return {
382
+ push(chunk) {
383
+ buffer += chunk;
384
+ const lines = buffer.split("\n");
385
+ buffer = lines.pop() || "";
386
+ for (const line of lines) {
387
+ processLine(line);
388
+ }
389
+ },
390
+ flush() {
391
+ if (buffer.trim()) {
392
+ processLine(buffer);
393
+ }
394
+ buffer = "";
395
+ }
396
+ };
397
+ }
398
+ var SPEC_FENCE_OPEN = "```spec";
399
+ var SPEC_FENCE_CLOSE = "```";
400
+ function createJsonRenderTransform() {
401
+ let lineBuffer = "";
402
+ let currentTextId = "";
403
+ let buffering = false;
404
+ let inSpecFence = false;
405
+ function emitPatch(patch, controller) {
406
+ controller.enqueue({
407
+ type: SPEC_DATA_PART_TYPE,
408
+ data: { type: "patch", patch }
409
+ });
410
+ }
411
+ function flushBuffer(controller) {
412
+ if (!lineBuffer) return;
413
+ const trimmed = lineBuffer.trim();
414
+ if (inSpecFence) {
415
+ if (trimmed) {
416
+ const patch = parseSpecStreamLine(trimmed);
417
+ if (patch) emitPatch(patch, controller);
418
+ }
419
+ lineBuffer = "";
420
+ buffering = false;
421
+ return;
422
+ }
423
+ if (trimmed) {
424
+ const patch = parseSpecStreamLine(trimmed);
425
+ if (patch) {
426
+ emitPatch(patch, controller);
427
+ } else {
428
+ controller.enqueue({
429
+ type: "text-delta",
430
+ id: currentTextId,
431
+ delta: lineBuffer
432
+ });
433
+ }
434
+ } else {
435
+ controller.enqueue({
436
+ type: "text-delta",
437
+ id: currentTextId,
438
+ delta: lineBuffer
439
+ });
440
+ }
441
+ lineBuffer = "";
442
+ buffering = false;
443
+ }
444
+ function processCompleteLine(line, controller) {
445
+ const trimmed = line.trim();
446
+ if (!inSpecFence && trimmed.startsWith(SPEC_FENCE_OPEN)) {
447
+ inSpecFence = true;
448
+ return;
449
+ }
450
+ if (inSpecFence && trimmed === SPEC_FENCE_CLOSE) {
451
+ inSpecFence = false;
452
+ return;
453
+ }
454
+ if (inSpecFence) {
455
+ if (trimmed) {
456
+ const patch2 = parseSpecStreamLine(trimmed);
457
+ if (patch2) emitPatch(patch2, controller);
458
+ }
459
+ return;
460
+ }
461
+ if (!trimmed) {
462
+ controller.enqueue({
463
+ type: "text-delta",
464
+ id: currentTextId,
465
+ delta: "\n"
466
+ });
467
+ return;
468
+ }
469
+ const patch = parseSpecStreamLine(trimmed);
470
+ if (patch) {
471
+ emitPatch(patch, controller);
472
+ } else {
473
+ controller.enqueue({
474
+ type: "text-delta",
475
+ id: currentTextId,
476
+ delta: line + "\n"
477
+ });
478
+ }
479
+ }
480
+ return new TransformStream({
481
+ transform(chunk, controller) {
482
+ switch (chunk.type) {
483
+ case "text-start": {
484
+ currentTextId = chunk.id;
485
+ controller.enqueue(chunk);
486
+ break;
487
+ }
488
+ case "text-delta": {
489
+ const delta = chunk;
490
+ currentTextId = delta.id;
491
+ const text = delta.delta;
492
+ for (let i = 0; i < text.length; i++) {
493
+ const ch = text.charAt(i);
494
+ if (ch === "\n") {
495
+ if (buffering) {
496
+ processCompleteLine(lineBuffer, controller);
497
+ lineBuffer = "";
498
+ buffering = false;
499
+ } else {
500
+ if (!inSpecFence) {
501
+ controller.enqueue({
502
+ type: "text-delta",
503
+ id: currentTextId,
504
+ delta: "\n"
505
+ });
506
+ }
507
+ }
508
+ } else if (lineBuffer.length === 0 && !buffering) {
509
+ if (inSpecFence || ch === "{" || ch === "`") {
510
+ buffering = true;
511
+ lineBuffer += ch;
512
+ } else {
513
+ controller.enqueue({
514
+ type: "text-delta",
515
+ id: currentTextId,
516
+ delta: ch
517
+ });
518
+ }
519
+ } else if (buffering) {
520
+ lineBuffer += ch;
521
+ } else {
522
+ controller.enqueue({
523
+ type: "text-delta",
524
+ id: currentTextId,
525
+ delta: ch
526
+ });
527
+ }
528
+ }
529
+ break;
530
+ }
531
+ case "text-end": {
532
+ flushBuffer(controller);
533
+ controller.enqueue(chunk);
534
+ break;
535
+ }
536
+ default: {
537
+ controller.enqueue(chunk);
538
+ break;
539
+ }
540
+ }
541
+ },
542
+ flush(controller) {
543
+ flushBuffer(controller);
544
+ }
545
+ });
546
+ }
547
+ var SPEC_DATA_PART = "spec";
548
+ var SPEC_DATA_PART_TYPE = `data-${SPEC_DATA_PART}`;
549
+ function pipeJsonRender(stream) {
550
+ return stream.pipeThrough(
551
+ createJsonRenderTransform()
552
+ );
553
+ }
319
554
 
320
555
  // src/visibility.ts
321
556
  import { z as z2 } from "zod";
322
- var DynamicNumberValueSchema = z2.union([
557
+ var numericOrStateRef = z2.union([
323
558
  z2.number(),
324
- z2.object({ path: z2.string() })
559
+ z2.object({ $state: z2.string() })
325
560
  ]);
326
- var LogicExpressionSchema = z2.lazy(
561
+ var comparisonOps = {
562
+ eq: z2.unknown().optional(),
563
+ neq: z2.unknown().optional(),
564
+ gt: numericOrStateRef.optional(),
565
+ gte: numericOrStateRef.optional(),
566
+ lt: numericOrStateRef.optional(),
567
+ lte: numericOrStateRef.optional(),
568
+ not: z2.literal(true).optional()
569
+ };
570
+ var StateConditionSchema = z2.object({
571
+ $state: z2.string(),
572
+ ...comparisonOps
573
+ });
574
+ var ItemConditionSchema = z2.object({
575
+ $item: z2.string(),
576
+ ...comparisonOps
577
+ });
578
+ var IndexConditionSchema = z2.object({
579
+ $index: z2.literal(true),
580
+ ...comparisonOps
581
+ });
582
+ var SingleConditionSchema = z2.union([
583
+ StateConditionSchema,
584
+ ItemConditionSchema,
585
+ IndexConditionSchema
586
+ ]);
587
+ var VisibilityConditionSchema = z2.lazy(
327
588
  () => z2.union([
328
- z2.object({ and: z2.array(LogicExpressionSchema) }),
329
- z2.object({ or: z2.array(LogicExpressionSchema) }),
330
- z2.object({ not: LogicExpressionSchema }),
331
- z2.object({ path: z2.string() }),
332
- z2.object({ eq: z2.tuple([DynamicValueSchema, DynamicValueSchema]) }),
333
- z2.object({ neq: z2.tuple([DynamicValueSchema, DynamicValueSchema]) }),
334
- z2.object({
335
- gt: z2.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema])
336
- }),
337
- z2.object({
338
- gte: z2.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema])
339
- }),
340
- z2.object({
341
- lt: z2.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema])
342
- }),
343
- z2.object({
344
- lte: z2.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema])
345
- })
589
+ z2.boolean(),
590
+ SingleConditionSchema,
591
+ z2.array(SingleConditionSchema),
592
+ z2.object({ $and: z2.array(VisibilityConditionSchema) }),
593
+ z2.object({ $or: z2.array(VisibilityConditionSchema) })
346
594
  ])
347
595
  );
348
- var VisibilityConditionSchema = z2.union([
349
- z2.boolean(),
350
- z2.object({ path: z2.string() }),
351
- z2.object({ auth: z2.enum(["signedIn", "signedOut"]) }),
352
- LogicExpressionSchema
353
- ]);
354
- function evaluateLogicExpression(expr, ctx) {
355
- const { stateModel } = ctx;
356
- if ("and" in expr) {
357
- return expr.and.every((subExpr) => evaluateLogicExpression(subExpr, ctx));
358
- }
359
- if ("or" in expr) {
360
- return expr.or.some((subExpr) => evaluateLogicExpression(subExpr, ctx));
361
- }
362
- if ("not" in expr) {
363
- return !evaluateLogicExpression(expr.not, ctx);
364
- }
365
- if ("path" in expr) {
366
- const value = resolveDynamicValue({ path: expr.path }, stateModel);
367
- return Boolean(value);
368
- }
369
- if ("eq" in expr) {
370
- const [left, right] = expr.eq;
371
- const leftValue = resolveDynamicValue(left, stateModel);
372
- const rightValue = resolveDynamicValue(right, stateModel);
373
- return leftValue === rightValue;
374
- }
375
- if ("neq" in expr) {
376
- const [left, right] = expr.neq;
377
- const leftValue = resolveDynamicValue(left, stateModel);
378
- const rightValue = resolveDynamicValue(right, stateModel);
379
- return leftValue !== rightValue;
380
- }
381
- if ("gt" in expr) {
382
- const [left, right] = expr.gt;
383
- const leftValue = resolveDynamicValue(
384
- left,
385
- stateModel
386
- );
387
- const rightValue = resolveDynamicValue(
388
- right,
389
- stateModel
390
- );
391
- if (typeof leftValue === "number" && typeof rightValue === "number") {
392
- return leftValue > rightValue;
596
+ function resolveComparisonValue(value, ctx) {
597
+ if (typeof value === "object" && value !== null) {
598
+ if ("$state" in value && typeof value.$state === "string") {
599
+ return getByPath(ctx.stateModel, value.$state);
393
600
  }
394
- return false;
395
601
  }
396
- if ("gte" in expr) {
397
- const [left, right] = expr.gte;
398
- const leftValue = resolveDynamicValue(
399
- left,
400
- stateModel
401
- );
402
- const rightValue = resolveDynamicValue(
403
- right,
404
- stateModel
405
- );
406
- if (typeof leftValue === "number" && typeof rightValue === "number") {
407
- return leftValue >= rightValue;
408
- }
409
- return false;
602
+ return value;
603
+ }
604
+ function isItemCondition(cond) {
605
+ return "$item" in cond;
606
+ }
607
+ function isIndexCondition(cond) {
608
+ return "$index" in cond;
609
+ }
610
+ function resolveConditionValue(cond, ctx) {
611
+ if (isIndexCondition(cond)) {
612
+ return ctx.repeatIndex;
410
613
  }
411
- if ("lt" in expr) {
412
- const [left, right] = expr.lt;
413
- const leftValue = resolveDynamicValue(
414
- left,
415
- stateModel
416
- );
417
- const rightValue = resolveDynamicValue(
418
- right,
419
- stateModel
420
- );
421
- if (typeof leftValue === "number" && typeof rightValue === "number") {
422
- return leftValue < rightValue;
423
- }
424
- return false;
614
+ if (isItemCondition(cond)) {
615
+ if (ctx.repeatItem === void 0) return void 0;
616
+ return cond.$item === "" ? ctx.repeatItem : getByPath(ctx.repeatItem, cond.$item);
425
617
  }
426
- if ("lte" in expr) {
427
- const [left, right] = expr.lte;
428
- const leftValue = resolveDynamicValue(
429
- left,
430
- stateModel
431
- );
432
- const rightValue = resolveDynamicValue(
433
- right,
434
- stateModel
435
- );
436
- if (typeof leftValue === "number" && typeof rightValue === "number") {
437
- return leftValue <= rightValue;
438
- }
439
- return false;
618
+ return getByPath(ctx.stateModel, cond.$state);
619
+ }
620
+ function evaluateCondition(cond, ctx) {
621
+ const value = resolveConditionValue(cond, ctx);
622
+ let result;
623
+ if (cond.eq !== void 0) {
624
+ const rhs = resolveComparisonValue(cond.eq, ctx);
625
+ result = value === rhs;
626
+ } else if (cond.neq !== void 0) {
627
+ const rhs = resolveComparisonValue(cond.neq, ctx);
628
+ result = value !== rhs;
629
+ } else if (cond.gt !== void 0) {
630
+ const rhs = resolveComparisonValue(cond.gt, ctx);
631
+ result = typeof value === "number" && typeof rhs === "number" ? value > rhs : false;
632
+ } else if (cond.gte !== void 0) {
633
+ const rhs = resolveComparisonValue(cond.gte, ctx);
634
+ result = typeof value === "number" && typeof rhs === "number" ? value >= rhs : false;
635
+ } else if (cond.lt !== void 0) {
636
+ const rhs = resolveComparisonValue(cond.lt, ctx);
637
+ result = typeof value === "number" && typeof rhs === "number" ? value < rhs : false;
638
+ } else if (cond.lte !== void 0) {
639
+ const rhs = resolveComparisonValue(cond.lte, ctx);
640
+ result = typeof value === "number" && typeof rhs === "number" ? value <= rhs : false;
641
+ } else {
642
+ result = Boolean(value);
440
643
  }
441
- return false;
644
+ return cond.not === true ? !result : result;
645
+ }
646
+ function isAndCondition(condition) {
647
+ return typeof condition === "object" && condition !== null && !Array.isArray(condition) && "$and" in condition;
648
+ }
649
+ function isOrCondition(condition) {
650
+ return typeof condition === "object" && condition !== null && !Array.isArray(condition) && "$or" in condition;
442
651
  }
443
652
  function evaluateVisibility(condition, ctx) {
444
653
  if (condition === void 0) {
@@ -447,74 +656,114 @@ function evaluateVisibility(condition, ctx) {
447
656
  if (typeof condition === "boolean") {
448
657
  return condition;
449
658
  }
450
- if ("path" in condition && !("and" in condition) && !("or" in condition)) {
451
- const value = resolveDynamicValue({ path: condition.path }, ctx.stateModel);
452
- return Boolean(value);
659
+ if (Array.isArray(condition)) {
660
+ return condition.every((c) => evaluateCondition(c, ctx));
453
661
  }
454
- if ("auth" in condition) {
455
- const isSignedIn = ctx.authState?.isSignedIn ?? false;
456
- if (condition.auth === "signedIn") {
457
- return isSignedIn;
458
- }
459
- if (condition.auth === "signedOut") {
460
- return !isSignedIn;
461
- }
462
- return false;
662
+ if (isAndCondition(condition)) {
663
+ return condition.$and.every((child) => evaluateVisibility(child, ctx));
664
+ }
665
+ if (isOrCondition(condition)) {
666
+ return condition.$or.some((child) => evaluateVisibility(child, ctx));
463
667
  }
464
- return evaluateLogicExpression(condition, ctx);
668
+ return evaluateCondition(condition, ctx);
465
669
  }
466
670
  var visibility = {
467
671
  /** Always visible */
468
672
  always: true,
469
673
  /** Never visible */
470
674
  never: false,
471
- /** Visible when path is truthy */
472
- when: (path) => ({ path }),
473
- /** Visible when signed in */
474
- signedIn: { auth: "signedIn" },
475
- /** Visible when signed out */
476
- signedOut: { auth: "signedOut" },
477
- /** AND multiple conditions */
478
- and: (...conditions) => ({
479
- and: conditions
480
- }),
481
- /** OR multiple conditions */
482
- or: (...conditions) => ({
483
- or: conditions
484
- }),
485
- /** NOT a condition */
486
- not: (condition) => ({ not: condition }),
675
+ /** Visible when state path is truthy */
676
+ when: (path) => ({ $state: path }),
677
+ /** Visible when state path is falsy */
678
+ unless: (path) => ({ $state: path, not: true }),
487
679
  /** Equality check */
488
- eq: (left, right) => ({
489
- eq: [left, right]
680
+ eq: (path, value) => ({
681
+ $state: path,
682
+ eq: value
490
683
  }),
491
684
  /** Not equal check */
492
- neq: (left, right) => ({
493
- neq: [left, right]
685
+ neq: (path, value) => ({
686
+ $state: path,
687
+ neq: value
494
688
  }),
495
689
  /** Greater than */
496
- gt: (left, right) => ({ gt: [left, right] }),
690
+ gt: (path, value) => ({
691
+ $state: path,
692
+ gt: value
693
+ }),
497
694
  /** Greater than or equal */
498
- gte: (left, right) => ({ gte: [left, right] }),
695
+ gte: (path, value) => ({
696
+ $state: path,
697
+ gte: value
698
+ }),
499
699
  /** Less than */
500
- lt: (left, right) => ({ lt: [left, right] }),
700
+ lt: (path, value) => ({
701
+ $state: path,
702
+ lt: value
703
+ }),
501
704
  /** Less than or equal */
502
- lte: (left, right) => ({ lte: [left, right] })
705
+ lte: (path, value) => ({
706
+ $state: path,
707
+ lte: value
708
+ }),
709
+ /** AND multiple conditions */
710
+ and: (...conditions) => ({
711
+ $and: conditions
712
+ }),
713
+ /** OR multiple conditions */
714
+ or: (...conditions) => ({
715
+ $or: conditions
716
+ })
503
717
  };
504
718
 
505
719
  // src/props.ts
506
- function isPathExpression(value) {
507
- return typeof value === "object" && value !== null && "$path" in value && typeof value.$path === "string";
720
+ function isStateExpression(value) {
721
+ return typeof value === "object" && value !== null && "$state" in value && typeof value.$state === "string";
722
+ }
723
+ function isItemExpression(value) {
724
+ return typeof value === "object" && value !== null && "$item" in value && typeof value.$item === "string";
725
+ }
726
+ function isIndexExpression(value) {
727
+ return typeof value === "object" && value !== null && "$index" in value && value.$index === true;
728
+ }
729
+ function isBindStateExpression(value) {
730
+ return typeof value === "object" && value !== null && "$bindState" in value && typeof value.$bindState === "string";
731
+ }
732
+ function isBindItemExpression(value) {
733
+ return typeof value === "object" && value !== null && "$bindItem" in value && typeof value.$bindItem === "string";
508
734
  }
509
735
  function isCondExpression(value) {
510
736
  return typeof value === "object" && value !== null && "$cond" in value && "$then" in value && "$else" in value;
511
737
  }
738
+ function resolveBindItemPath(itemPath, ctx) {
739
+ if (ctx.repeatBasePath == null) {
740
+ console.warn(`$bindItem used outside repeat scope: "${itemPath}"`);
741
+ return void 0;
742
+ }
743
+ if (itemPath === "") return ctx.repeatBasePath;
744
+ return ctx.repeatBasePath + "/" + itemPath;
745
+ }
512
746
  function resolvePropValue(value, ctx) {
513
747
  if (value === null || value === void 0) {
514
748
  return value;
515
749
  }
516
- if (isPathExpression(value)) {
517
- return getByPath(ctx.stateModel, value.$path);
750
+ if (isStateExpression(value)) {
751
+ return getByPath(ctx.stateModel, value.$state);
752
+ }
753
+ if (isItemExpression(value)) {
754
+ if (ctx.repeatItem === void 0) return void 0;
755
+ return value.$item === "" ? ctx.repeatItem : getByPath(ctx.repeatItem, value.$item);
756
+ }
757
+ if (isIndexExpression(value)) {
758
+ return ctx.repeatIndex;
759
+ }
760
+ if (isBindStateExpression(value)) {
761
+ return getByPath(ctx.stateModel, value.$bindState);
762
+ }
763
+ if (isBindItemExpression(value)) {
764
+ const resolvedPath = resolveBindItemPath(value.$bindItem, ctx);
765
+ if (resolvedPath === void 0) return void 0;
766
+ return getByPath(ctx.stateModel, resolvedPath);
518
767
  }
519
768
  if (isCondExpression(value)) {
520
769
  const result = evaluateVisibility(value.$cond, ctx);
@@ -539,6 +788,31 @@ function resolveElementProps(props, ctx) {
539
788
  }
540
789
  return resolved;
541
790
  }
791
+ function resolveBindings(props, ctx) {
792
+ let bindings;
793
+ for (const [key, value] of Object.entries(props)) {
794
+ if (isBindStateExpression(value)) {
795
+ if (!bindings) bindings = {};
796
+ bindings[key] = value.$bindState;
797
+ } else if (isBindItemExpression(value)) {
798
+ const resolved = resolveBindItemPath(value.$bindItem, ctx);
799
+ if (resolved !== void 0) {
800
+ if (!bindings) bindings = {};
801
+ bindings[key] = resolved;
802
+ }
803
+ }
804
+ }
805
+ return bindings;
806
+ }
807
+ function resolveActionParam(value, ctx) {
808
+ if (isItemExpression(value)) {
809
+ return resolveBindItemPath(value.$item, ctx);
810
+ }
811
+ if (isIndexExpression(value)) {
812
+ return ctx.repeatIndex;
813
+ }
814
+ return resolvePropValue(value, ctx);
815
+ }
542
816
 
543
817
  // src/actions.ts
544
818
  import { z as z3 } from "zod";
@@ -591,7 +865,7 @@ function resolveAction(binding, stateModel) {
591
865
  }
592
866
  function interpolateString(template, stateModel) {
593
867
  return template.replace(/\$\{([^}]+)\}/g, (_, path) => {
594
- const value = resolveDynamicValue({ path }, stateModel);
868
+ const value = resolveDynamicValue({ $state: path }, stateModel);
595
869
  return String(value ?? "");
596
870
  });
597
871
  }
@@ -649,14 +923,14 @@ var action = actionBinding;
649
923
  // src/validation.ts
650
924
  import { z as z4 } from "zod";
651
925
  var ValidationCheckSchema = z4.object({
652
- fn: z4.string(),
926
+ type: z4.string(),
653
927
  args: z4.record(z4.string(), DynamicValueSchema).optional(),
654
928
  message: z4.string()
655
929
  });
656
930
  var ValidationConfigSchema = z4.object({
657
931
  checks: z4.array(ValidationCheckSchema).optional(),
658
932
  validateOn: z4.enum(["change", "blur", "submit"]).optional(),
659
- enabled: LogicExpressionSchema.optional()
933
+ enabled: VisibilityConditionSchema.optional()
660
934
  });
661
935
  var builtInValidationFunctions = {
662
936
  /**
@@ -760,19 +1034,19 @@ function runValidationCheck(check2, ctx) {
760
1034
  resolvedArgs[key] = resolveDynamicValue(argValue, stateModel);
761
1035
  }
762
1036
  }
763
- const fn = builtInValidationFunctions[check2.fn] ?? customFunctions?.[check2.fn];
764
- if (!fn) {
765
- console.warn(`Unknown validation function: ${check2.fn}`);
1037
+ const validationFn = builtInValidationFunctions[check2.type] ?? customFunctions?.[check2.type];
1038
+ if (!validationFn) {
1039
+ console.warn(`Unknown validation function: ${check2.type}`);
766
1040
  return {
767
- fn: check2.fn,
1041
+ type: check2.type,
768
1042
  valid: true,
769
1043
  // Don't fail on unknown functions
770
1044
  message: check2.message
771
1045
  };
772
1046
  }
773
- const valid = fn(value, resolvedArgs);
1047
+ const valid = validationFn(value, resolvedArgs);
774
1048
  return {
775
- fn: check2.fn,
1049
+ type: check2.type,
776
1050
  valid,
777
1051
  message: check2.message
778
1052
  };
@@ -781,9 +1055,8 @@ function runValidation(config, ctx) {
781
1055
  const checks = [];
782
1056
  const errors = [];
783
1057
  if (config.enabled) {
784
- const enabled = evaluateLogicExpression(config.enabled, {
785
- stateModel: ctx.stateModel,
786
- authState: ctx.authState
1058
+ const enabled = evaluateVisibility(config.enabled, {
1059
+ stateModel: ctx.stateModel
787
1060
  });
788
1061
  if (!enabled) {
789
1062
  return { valid: true, errors: [], checks: [] };
@@ -806,45 +1079,45 @@ function runValidation(config, ctx) {
806
1079
  }
807
1080
  var check = {
808
1081
  required: (message = "This field is required") => ({
809
- fn: "required",
1082
+ type: "required",
810
1083
  message
811
1084
  }),
812
1085
  email: (message = "Invalid email address") => ({
813
- fn: "email",
1086
+ type: "email",
814
1087
  message
815
1088
  }),
816
1089
  minLength: (min, message) => ({
817
- fn: "minLength",
1090
+ type: "minLength",
818
1091
  args: { min },
819
1092
  message: message ?? `Must be at least ${min} characters`
820
1093
  }),
821
1094
  maxLength: (max, message) => ({
822
- fn: "maxLength",
1095
+ type: "maxLength",
823
1096
  args: { max },
824
1097
  message: message ?? `Must be at most ${max} characters`
825
1098
  }),
826
1099
  pattern: (pattern, message = "Invalid format") => ({
827
- fn: "pattern",
1100
+ type: "pattern",
828
1101
  args: { pattern },
829
1102
  message
830
1103
  }),
831
1104
  min: (min, message) => ({
832
- fn: "min",
1105
+ type: "min",
833
1106
  args: { min },
834
1107
  message: message ?? `Must be at least ${min}`
835
1108
  }),
836
1109
  max: (max, message) => ({
837
- fn: "max",
1110
+ type: "max",
838
1111
  args: { max },
839
1112
  message: message ?? `Must be at most ${max}`
840
1113
  }),
841
1114
  url: (message = "Invalid URL") => ({
842
- fn: "url",
1115
+ type: "url",
843
1116
  message
844
1117
  }),
845
1118
  matches: (otherPath, message = "Fields must match") => ({
846
- fn: "matches",
847
- args: { other: { path: otherPath } },
1119
+ type: "matches",
1120
+ args: { other: { $state: otherPath } },
848
1121
  message
849
1122
  })
850
1123
  };
@@ -984,7 +1257,7 @@ function autoFixSpec(spec) {
984
1257
  fixedElements[key] = fixed;
985
1258
  }
986
1259
  return {
987
- spec: { root: spec.root, elements: fixedElements },
1260
+ spec: { root: spec.root, elements: fixedElements, state: spec.state },
988
1261
  fixes
989
1262
  };
990
1263
  }
@@ -1168,15 +1441,35 @@ function generatePrompt(catalog, options) {
1168
1441
  }
1169
1442
  const {
1170
1443
  system = "You are a UI generator that outputs JSON.",
1171
- customRules = []
1444
+ customRules = [],
1445
+ mode = "generate"
1172
1446
  } = options;
1173
1447
  const lines = [];
1174
1448
  lines.push(system);
1175
1449
  lines.push("");
1176
- lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):");
1177
- lines.push(
1178
- "Output JSONL (one JSON object per line) using RFC 6902 JSON Patch operations to build a UI tree."
1179
- );
1450
+ if (mode === "chat") {
1451
+ lines.push("OUTPUT FORMAT (text + JSONL, RFC 6902 JSON Patch):");
1452
+ lines.push(
1453
+ "You respond conversationally. When generating UI, first write a brief explanation (1-3 sentences), then output JSONL patch lines wrapped in a ```spec code fence."
1454
+ );
1455
+ lines.push(
1456
+ "The JSONL lines use RFC 6902 JSON Patch operations to build a UI tree. Always wrap them in a ```spec fence block:"
1457
+ );
1458
+ lines.push(" ```spec");
1459
+ lines.push(' {"op":"add","path":"/root","value":"main"}');
1460
+ lines.push(
1461
+ ' {"op":"add","path":"/elements/main","value":{"type":"Card","props":{"title":"Hello"},"children":[]}}'
1462
+ );
1463
+ lines.push(" ```");
1464
+ lines.push(
1465
+ "If the user's message does not require a UI (e.g. a greeting or clarifying question), respond with text only \u2014 no JSONL."
1466
+ );
1467
+ } else {
1468
+ lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):");
1469
+ lines.push(
1470
+ "Output JSONL (one JSON object per line) using RFC 6902 JSON Patch operations to build a UI tree."
1471
+ );
1472
+ }
1180
1473
  lines.push(
1181
1474
  "Each line is a JSON patch operation (add, remove, replace). Start with /root, then stream /elements and /state patches interleaved so the UI fills in progressively as it streams."
1182
1475
  );
@@ -1192,7 +1485,7 @@ function generatePrompt(catalog, options) {
1192
1485
  const comp1Props = comp1Def ? getExampleProps(comp1Def) : {};
1193
1486
  const comp2Props = comp2Def ? getExampleProps(comp2Def) : {};
1194
1487
  const dynamicPropName = comp2Def?.props ? findFirstStringProp(comp2Def.props) : null;
1195
- const dynamicProps = dynamicPropName ? { ...comp2Props, [dynamicPropName]: { $path: "$item/title" } } : comp2Props;
1488
+ const dynamicProps = dynamicPropName ? { ...comp2Props, [dynamicPropName]: { $item: "title" } } : comp2Props;
1196
1489
  const exampleOutput = [
1197
1490
  JSON.stringify({ op: "add", path: "/root", value: "main" }),
1198
1491
  JSON.stringify({
@@ -1215,7 +1508,7 @@ function generatePrompt(catalog, options) {
1215
1508
  value: {
1216
1509
  type: comp1,
1217
1510
  props: comp1Props,
1218
- repeat: { path: "/items", key: "id" },
1511
+ repeat: { statePath: "/items", key: "id" },
1219
1512
  children: ["item"]
1220
1513
  }
1221
1514
  }),
@@ -1242,10 +1535,10 @@ Note: state patches appear right after the elements that use them, so the UI fil
1242
1535
  lines.push("");
1243
1536
  lines.push("INITIAL STATE:");
1244
1537
  lines.push(
1245
- "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."
1538
+ "Specs include a /state field to seed the state model. Components with { $bindState } or { $bindItem } read from and write to this state, and $state expressions read from it."
1246
1539
  );
1247
1540
  lines.push(
1248
- "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."
1541
+ "CRITICAL: You MUST include state patches whenever your UI displays data via $state, $bindState, $bindItem, $item, or $index expressions, or uses repeat to iterate over arrays. Without state, these references resolve to nothing and repeat lists render zero items."
1249
1542
  );
1250
1543
  lines.push(
1251
1544
  "Output state patches right after the elements that reference them, so the UI fills in progressively as it streams."
@@ -1263,7 +1556,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
1263
1556
  ' Initialize the array first if needed: {"op":"add","path":"/state/posts","value":[]}'
1264
1557
  );
1265
1558
  lines.push(
1266
- '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.'
1559
+ 'When content comes from the state model, use { "$state": "/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.'
1267
1560
  );
1268
1561
  lines.push(
1269
1562
  "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."
@@ -1271,16 +1564,16 @@ Note: state patches appear right after the elements that use them, so the UI fil
1271
1564
  lines.push("");
1272
1565
  lines.push("DYNAMIC LISTS (repeat field):");
1273
1566
  lines.push(
1274
- '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" } }.'
1567
+ 'Any element can have a top-level "repeat" field to render its children once per item in a state array: { "repeat": { "statePath": "/arrayPath", "key": "id" } }.'
1275
1568
  );
1276
1569
  lines.push(
1277
- 'The element itself renders once (as the container), and its children are expanded once per array item. "path" is the state array path. "key" is an optional field name on each item for stable React keys.'
1570
+ 'The element itself renders once (as the container), and its children are expanded once per array item. "statePath" is the state array path. "key" is an optional field name on each item for stable React keys.'
1278
1571
  );
1279
1572
  lines.push(
1280
- `Example: ${JSON.stringify({ type: comp1, props: comp1Props, repeat: { path: "/todos", key: "id" }, children: ["todo-item"] })}`
1573
+ `Example: ${JSON.stringify({ type: comp1, props: comp1Props, repeat: { statePath: "/todos", key: "id" }, children: ["todo-item"] })}`
1281
1574
  );
1282
1575
  lines.push(
1283
- 'Inside children of a repeated element, use "$item/field" for per-item paths: statePath:"$item/completed", { "$path": "$item/title" }. Use "$index" for the current array index.'
1576
+ 'Inside children of a repeated element, use { "$item": "field" } to read a field from the current item, and { "$index": true } to get the current array index. For two-way binding to an item field use { "$bindItem": "completed" } on the appropriate prop.'
1284
1577
  );
1285
1578
  lines.push(
1286
1579
  "ALWAYS use the repeat field for lists backed by state arrays. NEVER hardcode individual elements for each array item."
@@ -1291,19 +1584,19 @@ Note: state patches appear right after the elements that use them, so the UI fil
1291
1584
  lines.push("");
1292
1585
  lines.push("ARRAY STATE ACTIONS:");
1293
1586
  lines.push(
1294
- 'Use action "pushState" to append items to arrays. Params: { path: "/arrayPath", value: { ...item }, clearPath: "/inputPath" }.'
1587
+ 'Use action "pushState" to append items to arrays. Params: { statePath: "/arrayPath", value: { ...item }, clearStatePath: "/inputPath" }.'
1295
1588
  );
1296
1589
  lines.push(
1297
- 'Values inside pushState can contain { "path": "/statePath" } references to read current state (e.g. the text from an input field).'
1590
+ 'Values inside pushState can contain { "$state": "/statePath" } references to read current state (e.g. the text from an input field).'
1298
1591
  );
1299
1592
  lines.push(
1300
1593
  'Use "$id" inside a pushState value to auto-generate a unique ID.'
1301
1594
  );
1302
1595
  lines.push(
1303
- 'Example: on: { "press": { "action": "pushState", "params": { "path": "/todos", "value": { "id": "$id", "title": { "path": "/newTodoText" }, "completed": false }, "clearPath": "/newTodoText" } } }'
1596
+ 'Example: on: { "press": { "action": "pushState", "params": { "statePath": "/todos", "value": { "id": "$id", "title": { "$state": "/newTodoText" }, "completed": false }, "clearStatePath": "/newTodoText" } } }'
1304
1597
  );
1305
1598
  lines.push(
1306
- `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.`
1599
+ `Use action "removeState" to remove items from arrays by index. Params: { statePath: "/arrayPath", index: N }. Inside a repeated element's children, use { "$index": true } for the current item index. Action params support the same expressions as props: { "$item": "field" } resolves to the absolute state path, { "$index": true } resolves to the index number, and { "$state": "/path" } reads a value from state.`
1307
1600
  );
1308
1601
  lines.push(
1309
1602
  "For lists where users can add/remove items (todos, carts, etc.), use pushState and removeState instead of hardcoding with setState."
@@ -1346,11 +1639,11 @@ Note: state patches appear right after the elements that use them, so the UI fil
1346
1639
  lines.push("");
1347
1640
  lines.push("Example:");
1348
1641
  lines.push(
1349
- ` ${JSON.stringify({ type: comp1, props: comp1Props, on: { press: { action: "setState", params: { path: "/saved", value: true } } }, children: [] })}`
1642
+ ` ${JSON.stringify({ type: comp1, props: comp1Props, on: { press: { action: "setState", params: { statePath: "/saved", value: true } } }, children: [] })}`
1350
1643
  );
1351
1644
  lines.push("");
1352
1645
  lines.push(
1353
- 'Action params can use dynamic path references to read from state: { "path": "/statePath" }.'
1646
+ 'Action params can use dynamic references to read from state: { "$state": "/statePath" }.'
1354
1647
  );
1355
1648
  lines.push(
1356
1649
  "IMPORTANT: Do NOT put action/actionParams inside props. Always use the `on` field for event bindings."
@@ -1358,20 +1651,38 @@ Note: state patches appear right after the elements that use them, so the UI fil
1358
1651
  lines.push("");
1359
1652
  lines.push("VISIBILITY CONDITIONS:");
1360
1653
  lines.push(
1361
- "Elements can have an optional `visible` field to conditionally show/hide based on data state. IMPORTANT: `visible` is a top-level field on the element object (sibling of type/props/children), NOT inside props."
1654
+ "Elements can have an optional `visible` field to conditionally show/hide based on state. IMPORTANT: `visible` is a top-level field on the element object (sibling of type/props/children), NOT inside props."
1362
1655
  );
1363
1656
  lines.push(
1364
- `Correct: ${JSON.stringify({ type: comp1, props: comp1Props, visible: { eq: [{ path: "/tab" }, "home"] }, children: ["..."] })}`
1657
+ `Correct: ${JSON.stringify({ type: comp1, props: comp1Props, visible: { $state: "/activeTab", eq: "home" }, children: ["..."] })}`
1365
1658
  );
1366
1659
  lines.push(
1367
- '- `{ "eq": [{ "path": "/statePath" }, "value"] }` - visible when state at path equals value'
1660
+ '- `{ "$state": "/path" }` - visible when state at path is truthy'
1368
1661
  );
1369
1662
  lines.push(
1370
- '- `{ "neq": [{ "path": "/statePath" }, "value"] }` - visible when state at path does not equal value'
1663
+ '- `{ "$state": "/path", "not": true }` - visible when state at path is falsy'
1371
1664
  );
1372
- lines.push('- `{ "path": "/statePath" }` - visible when path is truthy');
1373
1665
  lines.push(
1374
- '- `{ "and": [...] }`, `{ "or": [...] }`, `{ "not": {...} }` - combine conditions'
1666
+ '- `{ "$state": "/path", "eq": "value" }` - visible when state equals value'
1667
+ );
1668
+ lines.push(
1669
+ '- `{ "$state": "/path", "neq": "value" }` - visible when state does not equal value'
1670
+ );
1671
+ lines.push(
1672
+ '- `{ "$state": "/path", "gt": N }` / `gte` / `lt` / `lte` - numeric comparisons'
1673
+ );
1674
+ lines.push(
1675
+ "- Use ONE operator per condition (eq, neq, gt, gte, lt, lte). Do not combine multiple operators."
1676
+ );
1677
+ lines.push('- Any condition can add `"not": true` to invert its result');
1678
+ lines.push(
1679
+ "- `[condition, condition]` - all conditions must be true (implicit AND)"
1680
+ );
1681
+ lines.push(
1682
+ '- `{ "$and": [condition, condition] }` - explicit AND (use when nesting inside $or)'
1683
+ );
1684
+ lines.push(
1685
+ '- `{ "$or": [condition, condition] }` - at least one must be true (OR)'
1375
1686
  );
1376
1687
  lines.push("- `true` / `false` - always visible/hidden");
1377
1688
  lines.push("");
@@ -1379,41 +1690,102 @@ Note: state patches appear right after the elements that use them, so the UI fil
1379
1690
  "Use a component with on.press bound to setState to update state and drive visibility."
1380
1691
  );
1381
1692
  lines.push(
1382
- `Example: A ${comp1} with on: { "press": { "action": "setState", "params": { "path": "/activeTab", "value": "home" } } } sets state, then a container with visible: { "eq": [{ "path": "/activeTab" }, "home"] } shows only when that tab is active.`
1693
+ `Example: A ${comp1} with on: { "press": { "action": "setState", "params": { "statePath": "/activeTab", "value": "home" } } } sets state, then a container with visible: { "$state": "/activeTab", "eq": "home" } shows only when that tab is active.`
1694
+ );
1695
+ lines.push("");
1696
+ lines.push(
1697
+ 'For tab patterns where the first/default tab should be visible when no tab is selected yet, use $or to handle both cases: visible: { "$or": [{ "$state": "/activeTab", "eq": "home" }, { "$state": "/activeTab", "not": true }] }. This ensures the first tab is visible both when explicitly selected AND when /activeTab is not yet set.'
1383
1698
  );
1384
1699
  lines.push("");
1385
1700
  lines.push("DYNAMIC PROPS:");
1386
1701
  lines.push(
1387
- "Any prop value can be a dynamic expression that resolves based on state. Two forms are supported:"
1702
+ "Any prop value can be a dynamic expression that resolves based on state. Three forms are supported:"
1388
1703
  );
1389
1704
  lines.push("");
1390
1705
  lines.push(
1391
- '1. State binding: `{ "$path": "/statePath" }` - resolves to the value at that state path.'
1706
+ '1. Read-only state: `{ "$state": "/statePath" }` - resolves to the value at that state path (one-way read).'
1392
1707
  );
1393
1708
  lines.push(
1394
- ' Example: `"color": { "$path": "/theme/primary" }` reads the color from state.'
1709
+ ' Example: `"color": { "$state": "/theme/primary" }` reads the color from state.'
1395
1710
  );
1396
1711
  lines.push("");
1397
1712
  lines.push(
1398
- '2. Conditional: `{ "$cond": <condition>, "$then": <value>, "$else": <value> }` - evaluates the condition (same syntax as visibility conditions) and picks the matching value.'
1713
+ '2. Two-way binding: `{ "$bindState": "/statePath" }` - resolves to the value at the state path AND enables write-back. Use on form input props (value, checked, pressed, etc.).'
1399
1714
  );
1400
1715
  lines.push(
1401
- ' Example: `"color": { "$cond": { "eq": [{ "path": "/activeTab" }, "home"] }, "$then": "#007AFF", "$else": "#8E8E93" }`'
1716
+ ' Example: `"value": { "$bindState": "/form/email" }` binds the input value to /form/email.'
1402
1717
  );
1403
1718
  lines.push(
1404
- ' Example: `"name": { "$cond": { "eq": [{ "path": "/activeTab" }, "home"] }, "$then": "home", "$else": "home-outline" }`'
1719
+ ' Inside repeat scopes: `"checked": { "$bindItem": "completed" }` binds to the current item\'s completed field.'
1405
1720
  );
1406
1721
  lines.push("");
1407
1722
  lines.push(
1408
- "Use dynamic props instead of duplicating elements with opposing visible conditions when only prop values differ."
1723
+ '3. Conditional: `{ "$cond": <condition>, "$then": <value>, "$else": <value> }` - evaluates the condition (same syntax as visibility conditions) and picks the matching value.'
1724
+ );
1725
+ lines.push(
1726
+ ' Example: `"color": { "$cond": { "$state": "/activeTab", "eq": "home" }, "$then": "#007AFF", "$else": "#8E8E93" }`'
1409
1727
  );
1410
1728
  lines.push("");
1729
+ lines.push(
1730
+ "Use $bindState for form inputs (text fields, checkboxes, selects, sliders, etc.) and $state for read-only data display. Inside repeat scopes, use $bindItem for form inputs bound to the current item. Use dynamic props instead of duplicating elements with opposing visible conditions when only prop values differ."
1731
+ );
1732
+ lines.push("");
1733
+ const hasChecksComponents = allComponents ? Object.entries(allComponents).some(([, def]) => {
1734
+ if (!def.props) return false;
1735
+ const formatted = formatZodType(def.props);
1736
+ return formatted.includes("checks");
1737
+ }) : false;
1738
+ if (hasChecksComponents) {
1739
+ lines.push("VALIDATION:");
1740
+ lines.push(
1741
+ "Form components that accept a `checks` prop support client-side validation."
1742
+ );
1743
+ lines.push(
1744
+ 'Each check is an object: { "type": "<name>", "message": "...", "args": { ... } }'
1745
+ );
1746
+ lines.push("");
1747
+ lines.push("Built-in validation types:");
1748
+ lines.push(" - required \u2014 value must be non-empty");
1749
+ lines.push(" - email \u2014 valid email format");
1750
+ lines.push(' - minLength \u2014 minimum string length (args: { "min": N })');
1751
+ lines.push(' - maxLength \u2014 maximum string length (args: { "max": N })');
1752
+ lines.push(' - pattern \u2014 match a regex (args: { "pattern": "regex" })');
1753
+ lines.push(' - min \u2014 minimum numeric value (args: { "min": N })');
1754
+ lines.push(' - max \u2014 maximum numeric value (args: { "max": N })');
1755
+ lines.push(" - numeric \u2014 value must be a number");
1756
+ lines.push(" - url \u2014 valid URL format");
1757
+ lines.push(
1758
+ ' - matches \u2014 must equal another field (args: { "other": "value" })'
1759
+ );
1760
+ lines.push("");
1761
+ lines.push("Example:");
1762
+ lines.push(
1763
+ ' "checks": [{ "type": "required", "message": "Email is required" }, { "type": "email", "message": "Invalid email" }]'
1764
+ );
1765
+ lines.push("");
1766
+ lines.push(
1767
+ "IMPORTANT: When using checks, the component must also have a { $bindState } or { $bindItem } on its value/checked prop for two-way binding."
1768
+ );
1769
+ lines.push(
1770
+ "Always include validation checks on form inputs for a good user experience (e.g. required, email, minLength)."
1771
+ );
1772
+ lines.push("");
1773
+ }
1411
1774
  lines.push("RULES:");
1412
- const baseRules = [
1775
+ const baseRules = mode === "chat" ? [
1776
+ "When generating UI, wrap all JSONL patches in a ```spec code fence - one JSON object per line inside the fence",
1777
+ "Write a brief conversational response before any JSONL output",
1778
+ 'First set root: {"op":"add","path":"/root","value":"<root-key>"}',
1779
+ 'Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}',
1780
+ "Output /state patches right after the elements that use them, one per array item for progressive loading. REQUIRED whenever using $state, $bindState, $bindItem, $item, $index, or repeat.",
1781
+ "ONLY use components listed above",
1782
+ "Each element value needs: type, props, children (array of child keys)",
1783
+ "Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')"
1784
+ ] : [
1413
1785
  "Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
1414
1786
  'First set root: {"op":"add","path":"/root","value":"<root-key>"}',
1415
1787
  'Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}',
1416
- "Output /state patches right after the elements that use them, one per array item for progressive loading. REQUIRED whenever using $path, repeat, or statePath.",
1788
+ "Output /state patches right after the elements that use them, one per array item for progressive loading. REQUIRED whenever using $state, $bindState, $bindItem, $item, $index, or repeat.",
1417
1789
  "ONLY use components listed above",
1418
1790
  "Each element value needs: type, props, children (array of child keys)",
1419
1791
  "Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')"
@@ -1701,285 +2073,6 @@ Remember: Output /root first, then interleave /elements and /state patches so th
1701
2073
  );
1702
2074
  return parts.join("\n");
1703
2075
  }
1704
-
1705
- // src/catalog.ts
1706
- import { z as z6 } from "zod";
1707
- function createCatalog(config) {
1708
- const {
1709
- name = "unnamed",
1710
- components,
1711
- actions = {},
1712
- functions = {},
1713
- validation = "strict"
1714
- } = config;
1715
- const componentNames = Object.keys(components);
1716
- const actionNames = Object.keys(actions);
1717
- const functionNames = Object.keys(functions);
1718
- const componentSchemas = componentNames.map((componentName) => {
1719
- const def = components[componentName];
1720
- return z6.object({
1721
- type: z6.literal(componentName),
1722
- props: def.props,
1723
- children: z6.array(z6.string()).optional(),
1724
- visible: VisibilityConditionSchema.optional()
1725
- });
1726
- });
1727
- let elementSchema;
1728
- if (componentSchemas.length === 0) {
1729
- elementSchema = z6.object({
1730
- type: z6.string(),
1731
- props: z6.record(z6.string(), z6.unknown()),
1732
- children: z6.array(z6.string()).optional(),
1733
- visible: VisibilityConditionSchema.optional()
1734
- });
1735
- } else if (componentSchemas.length === 1) {
1736
- elementSchema = componentSchemas[0];
1737
- } else {
1738
- elementSchema = z6.discriminatedUnion("type", [
1739
- componentSchemas[0],
1740
- componentSchemas[1],
1741
- ...componentSchemas.slice(2)
1742
- ]);
1743
- }
1744
- const specSchema = z6.object({
1745
- root: z6.string(),
1746
- elements: z6.record(z6.string(), elementSchema)
1747
- });
1748
- return {
1749
- name,
1750
- componentNames,
1751
- actionNames,
1752
- functionNames,
1753
- validation,
1754
- components,
1755
- actions,
1756
- functions,
1757
- elementSchema,
1758
- specSchema,
1759
- hasComponent(type) {
1760
- return type in components;
1761
- },
1762
- hasAction(name2) {
1763
- return name2 in actions;
1764
- },
1765
- hasFunction(name2) {
1766
- return name2 in functions;
1767
- },
1768
- validateElement(element) {
1769
- const result = elementSchema.safeParse(element);
1770
- if (result.success) {
1771
- return { success: true, data: result.data };
1772
- }
1773
- return { success: false, error: result.error };
1774
- },
1775
- validateSpec(spec) {
1776
- const result = specSchema.safeParse(spec);
1777
- if (result.success) {
1778
- return { success: true, data: result.data };
1779
- }
1780
- return { success: false, error: result.error };
1781
- }
1782
- };
1783
- }
1784
- function generateCatalogPrompt(catalog) {
1785
- const lines = [
1786
- `# ${catalog.name} Component Catalog`,
1787
- "",
1788
- "## Available Components",
1789
- ""
1790
- ];
1791
- for (const name of catalog.componentNames) {
1792
- const def = catalog.components[name];
1793
- lines.push(`### ${String(name)}`);
1794
- if (def.description) {
1795
- lines.push(def.description);
1796
- }
1797
- lines.push("");
1798
- }
1799
- if (catalog.actionNames.length > 0) {
1800
- lines.push("## Available Actions");
1801
- lines.push("");
1802
- for (const name of catalog.actionNames) {
1803
- const def = catalog.actions[name];
1804
- lines.push(
1805
- `- \`${String(name)}\`${def.description ? `: ${def.description}` : ""}`
1806
- );
1807
- }
1808
- lines.push("");
1809
- }
1810
- lines.push("## Visibility Conditions");
1811
- lines.push("");
1812
- lines.push("Components can have a `visible` property:");
1813
- lines.push("- `true` / `false` - Always visible/hidden");
1814
- lines.push('- `{ "path": "/state/path" }` - Visible when path is truthy');
1815
- lines.push('- `{ "auth": "signedIn" }` - Visible when user is signed in');
1816
- lines.push('- `{ "and": [...] }` - All conditions must be true');
1817
- lines.push('- `{ "or": [...] }` - Any condition must be true');
1818
- lines.push('- `{ "not": {...} }` - Negates a condition');
1819
- lines.push('- `{ "eq": [a, b] }` - Equality check');
1820
- lines.push("");
1821
- lines.push("## Validation Functions");
1822
- lines.push("");
1823
- lines.push(
1824
- "Built-in: `required`, `email`, `minLength`, `maxLength`, `pattern`, `min`, `max`, `url`"
1825
- );
1826
- if (catalog.functionNames.length > 0) {
1827
- lines.push(`Custom: ${catalog.functionNames.map(String).join(", ")}`);
1828
- }
1829
- lines.push("");
1830
- return lines.join("\n");
1831
- }
1832
- function formatZodType2(schema, isOptional = false) {
1833
- const def = schema._def;
1834
- const typeName = def.typeName ?? "";
1835
- let result;
1836
- switch (typeName) {
1837
- case "ZodString":
1838
- result = "string";
1839
- break;
1840
- case "ZodNumber":
1841
- result = "number";
1842
- break;
1843
- case "ZodBoolean":
1844
- result = "boolean";
1845
- break;
1846
- case "ZodLiteral":
1847
- result = JSON.stringify(def.value);
1848
- break;
1849
- case "ZodEnum":
1850
- result = def.values.map((v) => `"${v}"`).join("|");
1851
- break;
1852
- case "ZodNativeEnum":
1853
- result = Object.values(def.values).map((v) => `"${v}"`).join("|");
1854
- break;
1855
- case "ZodArray":
1856
- result = def.type ? `Array<${formatZodType2(def.type)}>` : "Array<unknown>";
1857
- break;
1858
- case "ZodObject": {
1859
- if (!def.shape) {
1860
- result = "object";
1861
- break;
1862
- }
1863
- const shape = def.shape();
1864
- const props = Object.entries(shape).map(([key, value]) => {
1865
- const innerDef = value._def;
1866
- const innerOptional = innerDef.typeName === "ZodOptional" || innerDef.typeName === "ZodNullable";
1867
- return `${key}${innerOptional ? "?" : ""}: ${formatZodType2(value)}`;
1868
- }).join(", ");
1869
- result = `{ ${props} }`;
1870
- break;
1871
- }
1872
- case "ZodOptional":
1873
- return def.innerType ? formatZodType2(def.innerType, true) : "unknown?";
1874
- case "ZodNullable":
1875
- return def.innerType ? formatZodType2(def.innerType, true) : "unknown?";
1876
- case "ZodDefault":
1877
- return def.innerType ? formatZodType2(def.innerType, isOptional) : "unknown";
1878
- case "ZodUnion":
1879
- result = def.options ? def.options.map((opt) => formatZodType2(opt)).join("|") : "unknown";
1880
- break;
1881
- case "ZodNull":
1882
- result = "null";
1883
- break;
1884
- case "ZodUndefined":
1885
- result = "undefined";
1886
- break;
1887
- case "ZodAny":
1888
- result = "any";
1889
- break;
1890
- case "ZodUnknown":
1891
- result = "unknown";
1892
- break;
1893
- default:
1894
- result = "unknown";
1895
- }
1896
- return isOptional ? `${result}?` : result;
1897
- }
1898
- function extractPropsFromSchema(schema) {
1899
- const def = schema._def;
1900
- const typeName = def.typeName ?? "";
1901
- if (typeName !== "ZodObject" || !def.shape) {
1902
- return [];
1903
- }
1904
- const shape = def.shape();
1905
- return Object.entries(shape).map(([name, value]) => {
1906
- const innerDef = value._def;
1907
- const optional = innerDef.typeName === "ZodOptional" || innerDef.typeName === "ZodNullable";
1908
- return {
1909
- name,
1910
- type: formatZodType2(value),
1911
- optional
1912
- };
1913
- });
1914
- }
1915
- function formatPropsCompact(props) {
1916
- if (props.length === 0) return "{}";
1917
- const entries = props.map(
1918
- (p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`
1919
- );
1920
- return `{ ${entries.join(", ")} }`;
1921
- }
1922
- function generateSystemPrompt(catalog, options = {}) {
1923
- const {
1924
- system = "You are a UI generator that outputs JSONL (JSON Lines) patches.",
1925
- customRules = []
1926
- } = options;
1927
- const lines = [];
1928
- lines.push(system);
1929
- lines.push("");
1930
- const componentCount = catalog.componentNames.length;
1931
- lines.push(`AVAILABLE COMPONENTS (${componentCount}):`);
1932
- lines.push("");
1933
- for (const name of catalog.componentNames) {
1934
- const def = catalog.components[name];
1935
- const props = extractPropsFromSchema(def.props);
1936
- const propsStr = formatPropsCompact(props);
1937
- const hasChildrenStr = def.hasChildren ? " Has children." : "";
1938
- const descStr = def.description ? ` ${def.description}` : "";
1939
- lines.push(`- ${String(name)}: ${propsStr}${descStr}${hasChildrenStr}`);
1940
- }
1941
- lines.push("");
1942
- if (catalog.actionNames.length > 0) {
1943
- lines.push("AVAILABLE ACTIONS:");
1944
- lines.push("");
1945
- for (const name of catalog.actionNames) {
1946
- const def = catalog.actions[name];
1947
- lines.push(
1948
- `- ${String(name)}${def.description ? `: ${def.description}` : ""}`
1949
- );
1950
- }
1951
- lines.push("");
1952
- }
1953
- lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):");
1954
- lines.push('{"op":"add","path":"/root","value":"element-key"}');
1955
- lines.push(
1956
- '{"op":"add","path":"/elements/key","value":{"type":"...","props":{...},"children":[...]}}'
1957
- );
1958
- lines.push('{"op":"remove","path":"/elements/key"}');
1959
- lines.push("");
1960
- lines.push("RULES:");
1961
- const baseRules = [
1962
- 'First line sets /root to root element key: {"op":"add","path":"/root","value":"<key>"}',
1963
- 'Add elements with /elements/{key}: {"op":"add","path":"/elements/<key>","value":{...}}',
1964
- "Remove elements with op:remove - also update the parent's children array to exclude the removed key",
1965
- "Children array contains string keys, not objects",
1966
- "Parent first, then children",
1967
- "Each element needs: type, props",
1968
- "ONLY use props listed above - never invent new props"
1969
- ];
1970
- const allRules = [...baseRules, ...customRules];
1971
- allRules.forEach((rule, i) => {
1972
- lines.push(`${i + 1}. ${rule}`);
1973
- });
1974
- lines.push("");
1975
- if (catalog.functionNames.length > 0) {
1976
- lines.push("CUSTOM VALIDATION FUNCTIONS:");
1977
- lines.push(catalog.functionNames.map(String).join(", "));
1978
- lines.push("");
1979
- }
1980
- lines.push("Generate JSONL:");
1981
- return lines.join("\n");
1982
- }
1983
2076
  export {
1984
2077
  ActionBindingSchema,
1985
2078
  ActionConfirmSchema,
@@ -1990,35 +2083,39 @@ export {
1990
2083
  DynamicNumberSchema,
1991
2084
  DynamicStringSchema,
1992
2085
  DynamicValueSchema,
1993
- LogicExpressionSchema,
2086
+ SPEC_DATA_PART,
2087
+ SPEC_DATA_PART_TYPE,
1994
2088
  ValidationCheckSchema,
1995
2089
  ValidationConfigSchema,
1996
2090
  VisibilityConditionSchema,
1997
2091
  action,
1998
2092
  actionBinding,
1999
2093
  addByPath,
2094
+ applySpecPatch,
2000
2095
  applySpecStreamPatch,
2001
2096
  autoFixSpec,
2002
2097
  buildUserPrompt,
2003
2098
  builtInValidationFunctions,
2004
2099
  check,
2005
2100
  compileSpecStream,
2006
- createCatalog,
2101
+ createJsonRenderTransform,
2102
+ createMixedStreamParser,
2007
2103
  createSpecStreamCompiler,
2008
2104
  defineCatalog,
2009
2105
  defineSchema,
2010
- evaluateLogicExpression,
2011
2106
  evaluateVisibility,
2012
2107
  executeAction,
2013
2108
  findFormValue,
2014
2109
  formatSpecIssues,
2015
- generateCatalogPrompt,
2016
- generateSystemPrompt,
2017
2110
  getByPath,
2018
2111
  interpolateString,
2112
+ nestedToFlat,
2019
2113
  parseSpecStreamLine,
2114
+ pipeJsonRender,
2020
2115
  removeByPath,
2021
2116
  resolveAction,
2117
+ resolveActionParam,
2118
+ resolveBindings,
2022
2119
  resolveDynamicValue,
2023
2120
  resolveElementProps,
2024
2121
  resolvePropValue,