@json-render/core 0.5.1 → 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() })
560
+ ]);
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
325
586
  ]);
326
- var LogicExpressionSchema = z2.lazy(
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,39 +1441,104 @@ 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:");
1177
- lines.push(
1178
- "Output JSONL (one JSON object per line) with patches 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
- "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."
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
  );
1183
1476
  lines.push("");
1184
1477
  lines.push("Example output (each line is a separate JSON object):");
1185
1478
  lines.push("");
1186
- lines.push(`{"op":"add","path":"/root","value":"blog"}
1187
- {"op":"add","path":"/elements/blog","value":{"type":"Stack","props":{"direction":"vertical","gap":"md"},"children":["heading","posts-grid"]}}
1188
- {"op":"add","path":"/elements/heading","value":{"type":"Heading","props":{"text":"Blog","level":"h1"},"children":[]}}
1189
- {"op":"add","path":"/elements/posts-grid","value":{"type":"Grid","props":{"columns":2,"gap":"md"},"repeat":{"path":"/posts","key":"id"},"children":["post-card"]}}
1190
- {"op":"add","path":"/elements/post-card","value":{"type":"Card","props":{"title":{"$path":"$item/title"}},"children":["post-meta"]}}
1191
- {"op":"add","path":"/elements/post-meta","value":{"type":"Text","props":{"text":{"$path":"$item/author"},"variant":"muted"},"children":[]}}
1192
- {"op":"add","path":"/state/posts","value":[]}
1193
- {"op":"add","path":"/state/posts/0","value":{"id":"1","title":"Getting Started","author":"Jane","date":"Jan 15"}}
1194
- {"op":"add","path":"/state/posts/1","value":{"id":"2","title":"Advanced Tips","author":"Bob","date":"Feb 3"}}
1479
+ const allComponents = catalog.data.components;
1480
+ const cn = catalog.componentNames;
1481
+ const comp1 = cn[0] || "Component";
1482
+ const comp2 = cn.length > 1 ? cn[1] : comp1;
1483
+ const comp1Def = allComponents?.[comp1];
1484
+ const comp2Def = allComponents?.[comp2];
1485
+ const comp1Props = comp1Def ? getExampleProps(comp1Def) : {};
1486
+ const comp2Props = comp2Def ? getExampleProps(comp2Def) : {};
1487
+ const dynamicPropName = comp2Def?.props ? findFirstStringProp(comp2Def.props) : null;
1488
+ const dynamicProps = dynamicPropName ? { ...comp2Props, [dynamicPropName]: { $item: "title" } } : comp2Props;
1489
+ const exampleOutput = [
1490
+ JSON.stringify({ op: "add", path: "/root", value: "main" }),
1491
+ JSON.stringify({
1492
+ op: "add",
1493
+ path: "/elements/main",
1494
+ value: {
1495
+ type: comp1,
1496
+ props: comp1Props,
1497
+ children: ["child-1", "list"]
1498
+ }
1499
+ }),
1500
+ JSON.stringify({
1501
+ op: "add",
1502
+ path: "/elements/child-1",
1503
+ value: { type: comp2, props: comp2Props, children: [] }
1504
+ }),
1505
+ JSON.stringify({
1506
+ op: "add",
1507
+ path: "/elements/list",
1508
+ value: {
1509
+ type: comp1,
1510
+ props: comp1Props,
1511
+ repeat: { statePath: "/items", key: "id" },
1512
+ children: ["item"]
1513
+ }
1514
+ }),
1515
+ JSON.stringify({
1516
+ op: "add",
1517
+ path: "/elements/item",
1518
+ value: { type: comp2, props: dynamicProps, children: [] }
1519
+ }),
1520
+ JSON.stringify({ op: "add", path: "/state/items", value: [] }),
1521
+ JSON.stringify({
1522
+ op: "add",
1523
+ path: "/state/items/0",
1524
+ value: { id: "1", title: "First Item" }
1525
+ }),
1526
+ JSON.stringify({
1527
+ op: "add",
1528
+ path: "/state/items/1",
1529
+ value: { id: "2", title: "Second Item" }
1530
+ })
1531
+ ].join("\n");
1532
+ lines.push(`${exampleOutput}
1195
1533
 
1196
- Note: state patches appear right after the elements that use them, so the UI fills in as it streams.`);
1534
+ Note: state patches appear right after the elements that use them, so the UI fills in as it streams. ONLY use component types from the AVAILABLE COMPONENTS list below.`);
1197
1535
  lines.push("");
1198
1536
  lines.push("INITIAL STATE:");
1199
1537
  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."
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."
1201
1539
  );
1202
1540
  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."
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."
1204
1542
  );
1205
1543
  lines.push(
1206
1544
  "Output state patches right after the elements that reference them, so the UI fills in progressively as it streams."
@@ -1218,7 +1556,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
1218
1556
  ' Initialize the array first if needed: {"op":"add","path":"/state/posts","value":[]}'
1219
1557
  );
1220
1558
  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.'
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.'
1222
1560
  );
1223
1561
  lines.push(
1224
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."
@@ -1226,16 +1564,16 @@ Note: state patches appear right after the elements that use them, so the UI fil
1226
1564
  lines.push("");
1227
1565
  lines.push("DYNAMIC LISTS (repeat field):");
1228
1566
  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" } }.'
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" } }.'
1230
1568
  );
1231
1569
  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.'
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.'
1233
1571
  );
1234
1572
  lines.push(
1235
- 'Example: { "type": "Column", "props": { "gap": 8 }, "repeat": { "path": "/todos", "key": "id" }, "children": ["todo-item"] }'
1573
+ `Example: ${JSON.stringify({ type: comp1, props: comp1Props, repeat: { statePath: "/todos", key: "id" }, children: ["todo-item"] })}`
1236
1574
  );
1237
1575
  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.'
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.'
1239
1577
  );
1240
1578
  lines.push(
1241
1579
  "ALWAYS use the repeat field for lists backed by state arrays. NEVER hardcode individual elements for each array item."
@@ -1246,19 +1584,19 @@ Note: state patches appear right after the elements that use them, so the UI fil
1246
1584
  lines.push("");
1247
1585
  lines.push("ARRAY STATE ACTIONS:");
1248
1586
  lines.push(
1249
- '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" }.'
1250
1588
  );
1251
1589
  lines.push(
1252
- '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).'
1253
1591
  );
1254
1592
  lines.push(
1255
1593
  'Use "$id" inside a pushState value to auto-generate a unique ID.'
1256
1594
  );
1257
1595
  lines.push(
1258
- '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" } } }'
1259
1597
  );
1260
1598
  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.`
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.`
1262
1600
  );
1263
1601
  lines.push(
1264
1602
  "For lists where users can add/remove items (todos, carts, etc.), use pushState and removeState instead of hardcoding with setState."
@@ -1268,7 +1606,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
1268
1606
  '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
1607
  );
1270
1608
  lines.push("");
1271
- const components = catalog.data.components;
1609
+ const components = allComponents;
1272
1610
  if (components) {
1273
1611
  lines.push(`AVAILABLE COMPONENTS (${catalog.componentNames.length}):`);
1274
1612
  lines.push("");
@@ -1301,11 +1639,11 @@ Note: state patches appear right after the elements that use them, so the UI fil
1301
1639
  lines.push("");
1302
1640
  lines.push("Example:");
1303
1641
  lines.push(
1304
- ' {"type":"Button","props":{"label":"Save"},"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: [] })}`
1305
1643
  );
1306
1644
  lines.push("");
1307
1645
  lines.push(
1308
- '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" }.'
1309
1647
  );
1310
1648
  lines.push(
1311
1649
  "IMPORTANT: Do NOT put action/actionParams inside props. Always use the `on` field for event bindings."
@@ -1313,62 +1651,141 @@ Note: state patches appear right after the elements that use them, so the UI fil
1313
1651
  lines.push("");
1314
1652
  lines.push("VISIBILITY CONDITIONS:");
1315
1653
  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."
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."
1655
+ );
1656
+ lines.push(
1657
+ `Correct: ${JSON.stringify({ type: comp1, props: comp1Props, visible: { $state: "/activeTab", eq: "home" }, children: ["..."] })}`
1658
+ );
1659
+ lines.push(
1660
+ '- `{ "$state": "/path" }` - visible when state at path is truthy'
1661
+ );
1662
+ lines.push(
1663
+ '- `{ "$state": "/path", "not": true }` - visible when state at path is falsy'
1664
+ );
1665
+ lines.push(
1666
+ '- `{ "$state": "/path", "eq": "value" }` - visible when state equals value'
1317
1667
  );
1318
1668
  lines.push(
1319
- 'Correct: {"type":"Column","props":{"gap":8},"visible":{"eq":[{"path":"/tab"},"home"]},"children":[...]}'
1669
+ '- `{ "$state": "/path", "neq": "value" }` - visible when state does not equal value'
1320
1670
  );
1321
1671
  lines.push(
1322
- '- `{ "eq": [{ "path": "/statePath" }, "value"] }` - visible when state at path equals value'
1672
+ '- `{ "$state": "/path", "gt": N }` / `gte` / `lt` / `lte` - numeric comparisons'
1323
1673
  );
1324
1674
  lines.push(
1325
- '- `{ "neq": [{ "path": "/statePath" }, "value"] }` - visible when state at path does not equal value'
1675
+ "- Use ONE operator per condition (eq, neq, gt, gte, lt, lte). Do not combine multiple operators."
1326
1676
  );
1327
- lines.push('- `{ "path": "/statePath" }` - visible when path is truthy');
1677
+ lines.push('- Any condition can add `"not": true` to invert its result');
1328
1678
  lines.push(
1329
- '- `{ "and": [...] }`, `{ "or": [...] }`, `{ "not": {...} }` - combine conditions'
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)'
1330
1686
  );
1331
1687
  lines.push("- `true` / `false` - always visible/hidden");
1332
1688
  lines.push("");
1333
1689
  lines.push(
1334
- "Use the Pressable component with on.press bound to setState to update state and drive visibility."
1690
+ "Use a component with on.press bound to setState to update state and drive visibility."
1335
1691
  );
1336
1692
  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.'
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.'
1338
1698
  );
1339
1699
  lines.push("");
1340
1700
  lines.push("DYNAMIC PROPS:");
1341
1701
  lines.push(
1342
- "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:"
1343
1703
  );
1344
1704
  lines.push("");
1345
1705
  lines.push(
1346
- '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).'
1347
1707
  );
1348
1708
  lines.push(
1349
- ' Example: `"color": { "$path": "/theme/primary" }` reads the color from state.'
1709
+ ' Example: `"color": { "$state": "/theme/primary" }` reads the color from state.'
1350
1710
  );
1351
1711
  lines.push("");
1352
1712
  lines.push(
1353
- '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.).'
1714
+ );
1715
+ lines.push(
1716
+ ' Example: `"value": { "$bindState": "/form/email" }` binds the input value to /form/email.'
1717
+ );
1718
+ lines.push(
1719
+ ' Inside repeat scopes: `"checked": { "$bindItem": "completed" }` binds to the current item\'s completed field.'
1354
1720
  );
1721
+ lines.push("");
1355
1722
  lines.push(
1356
- ' Example: `"color": { "$cond": { "eq": [{ "path": "/activeTab" }, "home"] }, "$then": "#007AFF", "$else": "#8E8E93" }`'
1723
+ '3. Conditional: `{ "$cond": <condition>, "$then": <value>, "$else": <value> }` - evaluates the condition (same syntax as visibility conditions) and picks the matching value.'
1357
1724
  );
1358
1725
  lines.push(
1359
- ' Example: `"name": { "$cond": { "eq": [{ "path": "/activeTab" }, "home"] }, "$then": "home", "$else": "home-outline" }`'
1726
+ ' Example: `"color": { "$cond": { "$state": "/activeTab", "eq": "home" }, "$then": "#007AFF", "$else": "#8E8E93" }`'
1360
1727
  );
1361
1728
  lines.push("");
1362
1729
  lines.push(
1363
- "Use dynamic props instead of duplicating elements with opposing visible conditions when only prop values differ."
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."
1364
1731
  );
1365
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
+ }
1366
1774
  lines.push("RULES:");
1367
- 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
+ ] : [
1368
1785
  "Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
1369
1786
  'First set root: {"op":"add","path":"/root","value":"<root-key>"}',
1370
1787
  '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.",
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.",
1372
1789
  "ONLY use components listed above",
1373
1790
  "Each element value needs: type, props, children (array of child keys)",
1374
1791
  "Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')"
@@ -1380,6 +1797,101 @@ Note: state patches appear right after the elements that use them, so the UI fil
1380
1797
  });
1381
1798
  return lines.join("\n");
1382
1799
  }
1800
+ function getExampleProps(def) {
1801
+ if (def.example && Object.keys(def.example).length > 0) {
1802
+ return def.example;
1803
+ }
1804
+ if (def.props) {
1805
+ return generateExamplePropsFromZod(def.props);
1806
+ }
1807
+ return {};
1808
+ }
1809
+ function generateExamplePropsFromZod(schema) {
1810
+ if (!schema || !schema._def) return {};
1811
+ const def = schema._def;
1812
+ const typeName = getZodTypeName(schema);
1813
+ if (typeName !== "ZodObject" && typeName !== "object") return {};
1814
+ const shape = typeof def.shape === "function" ? def.shape() : def.shape;
1815
+ if (!shape) return {};
1816
+ const result = {};
1817
+ for (const [key, value] of Object.entries(shape)) {
1818
+ const innerTypeName = getZodTypeName(value);
1819
+ if (innerTypeName === "ZodOptional" || innerTypeName === "optional" || innerTypeName === "ZodNullable" || innerTypeName === "nullable") {
1820
+ continue;
1821
+ }
1822
+ result[key] = generateExampleValue(value);
1823
+ }
1824
+ return result;
1825
+ }
1826
+ function generateExampleValue(schema) {
1827
+ if (!schema || !schema._def) return "...";
1828
+ const def = schema._def;
1829
+ const typeName = getZodTypeName(schema);
1830
+ switch (typeName) {
1831
+ case "ZodString":
1832
+ case "string":
1833
+ return "example";
1834
+ case "ZodNumber":
1835
+ case "number":
1836
+ return 0;
1837
+ case "ZodBoolean":
1838
+ case "boolean":
1839
+ return true;
1840
+ case "ZodLiteral":
1841
+ case "literal":
1842
+ return def.value;
1843
+ case "ZodEnum":
1844
+ case "enum": {
1845
+ if (Array.isArray(def.values) && def.values.length > 0)
1846
+ return def.values[0];
1847
+ if (def.entries && typeof def.entries === "object") {
1848
+ const values = Object.values(def.entries);
1849
+ return values.length > 0 ? values[0] : "example";
1850
+ }
1851
+ return "example";
1852
+ }
1853
+ case "ZodOptional":
1854
+ case "optional":
1855
+ case "ZodNullable":
1856
+ case "nullable":
1857
+ case "ZodDefault":
1858
+ case "default": {
1859
+ const inner = def.innerType ?? def.wrapped;
1860
+ return inner ? generateExampleValue(inner) : null;
1861
+ }
1862
+ case "ZodArray":
1863
+ case "array":
1864
+ return [];
1865
+ case "ZodObject":
1866
+ case "object":
1867
+ return generateExamplePropsFromZod(schema);
1868
+ case "ZodUnion":
1869
+ case "union": {
1870
+ const options = def.options;
1871
+ return options && options.length > 0 ? generateExampleValue(options[0]) : "...";
1872
+ }
1873
+ default:
1874
+ return "...";
1875
+ }
1876
+ }
1877
+ function findFirstStringProp(schema) {
1878
+ if (!schema || !schema._def) return null;
1879
+ const def = schema._def;
1880
+ const typeName = getZodTypeName(schema);
1881
+ if (typeName !== "ZodObject" && typeName !== "object") return null;
1882
+ const shape = typeof def.shape === "function" ? def.shape() : def.shape;
1883
+ if (!shape) return null;
1884
+ for (const [key, value] of Object.entries(shape)) {
1885
+ const innerTypeName = getZodTypeName(value);
1886
+ if (innerTypeName === "ZodOptional" || innerTypeName === "optional" || innerTypeName === "ZodNullable" || innerTypeName === "nullable") {
1887
+ continue;
1888
+ }
1889
+ if (innerTypeName === "ZodString" || innerTypeName === "string") {
1890
+ return key;
1891
+ }
1892
+ }
1893
+ return null;
1894
+ }
1383
1895
  function getZodTypeName(schema) {
1384
1896
  if (!schema || !schema._def) return "";
1385
1897
  const def = schema._def;
@@ -1561,285 +2073,6 @@ Remember: Output /root first, then interleave /elements and /state patches so th
1561
2073
  );
1562
2074
  return parts.join("\n");
1563
2075
  }
1564
-
1565
- // src/catalog.ts
1566
- import { z as z6 } from "zod";
1567
- function createCatalog(config) {
1568
- const {
1569
- name = "unnamed",
1570
- components,
1571
- actions = {},
1572
- functions = {},
1573
- validation = "strict"
1574
- } = config;
1575
- const componentNames = Object.keys(components);
1576
- const actionNames = Object.keys(actions);
1577
- const functionNames = Object.keys(functions);
1578
- const componentSchemas = componentNames.map((componentName) => {
1579
- const def = components[componentName];
1580
- return z6.object({
1581
- type: z6.literal(componentName),
1582
- props: def.props,
1583
- children: z6.array(z6.string()).optional(),
1584
- visible: VisibilityConditionSchema.optional()
1585
- });
1586
- });
1587
- let elementSchema;
1588
- if (componentSchemas.length === 0) {
1589
- elementSchema = z6.object({
1590
- type: z6.string(),
1591
- props: z6.record(z6.string(), z6.unknown()),
1592
- children: z6.array(z6.string()).optional(),
1593
- visible: VisibilityConditionSchema.optional()
1594
- });
1595
- } else if (componentSchemas.length === 1) {
1596
- elementSchema = componentSchemas[0];
1597
- } else {
1598
- elementSchema = z6.discriminatedUnion("type", [
1599
- componentSchemas[0],
1600
- componentSchemas[1],
1601
- ...componentSchemas.slice(2)
1602
- ]);
1603
- }
1604
- const specSchema = z6.object({
1605
- root: z6.string(),
1606
- elements: z6.record(z6.string(), elementSchema)
1607
- });
1608
- return {
1609
- name,
1610
- componentNames,
1611
- actionNames,
1612
- functionNames,
1613
- validation,
1614
- components,
1615
- actions,
1616
- functions,
1617
- elementSchema,
1618
- specSchema,
1619
- hasComponent(type) {
1620
- return type in components;
1621
- },
1622
- hasAction(name2) {
1623
- return name2 in actions;
1624
- },
1625
- hasFunction(name2) {
1626
- return name2 in functions;
1627
- },
1628
- validateElement(element) {
1629
- const result = elementSchema.safeParse(element);
1630
- if (result.success) {
1631
- return { success: true, data: result.data };
1632
- }
1633
- return { success: false, error: result.error };
1634
- },
1635
- validateSpec(spec) {
1636
- const result = specSchema.safeParse(spec);
1637
- if (result.success) {
1638
- return { success: true, data: result.data };
1639
- }
1640
- return { success: false, error: result.error };
1641
- }
1642
- };
1643
- }
1644
- function generateCatalogPrompt(catalog) {
1645
- const lines = [
1646
- `# ${catalog.name} Component Catalog`,
1647
- "",
1648
- "## Available Components",
1649
- ""
1650
- ];
1651
- for (const name of catalog.componentNames) {
1652
- const def = catalog.components[name];
1653
- lines.push(`### ${String(name)}`);
1654
- if (def.description) {
1655
- lines.push(def.description);
1656
- }
1657
- lines.push("");
1658
- }
1659
- if (catalog.actionNames.length > 0) {
1660
- lines.push("## Available Actions");
1661
- lines.push("");
1662
- for (const name of catalog.actionNames) {
1663
- const def = catalog.actions[name];
1664
- lines.push(
1665
- `- \`${String(name)}\`${def.description ? `: ${def.description}` : ""}`
1666
- );
1667
- }
1668
- lines.push("");
1669
- }
1670
- lines.push("## Visibility Conditions");
1671
- lines.push("");
1672
- lines.push("Components can have a `visible` property:");
1673
- lines.push("- `true` / `false` - Always visible/hidden");
1674
- lines.push('- `{ "path": "/state/path" }` - Visible when path is truthy');
1675
- lines.push('- `{ "auth": "signedIn" }` - Visible when user is signed in');
1676
- lines.push('- `{ "and": [...] }` - All conditions must be true');
1677
- lines.push('- `{ "or": [...] }` - Any condition must be true');
1678
- lines.push('- `{ "not": {...} }` - Negates a condition');
1679
- lines.push('- `{ "eq": [a, b] }` - Equality check');
1680
- lines.push("");
1681
- lines.push("## Validation Functions");
1682
- lines.push("");
1683
- lines.push(
1684
- "Built-in: `required`, `email`, `minLength`, `maxLength`, `pattern`, `min`, `max`, `url`"
1685
- );
1686
- if (catalog.functionNames.length > 0) {
1687
- lines.push(`Custom: ${catalog.functionNames.map(String).join(", ")}`);
1688
- }
1689
- lines.push("");
1690
- return lines.join("\n");
1691
- }
1692
- function formatZodType2(schema, isOptional = false) {
1693
- const def = schema._def;
1694
- const typeName = def.typeName ?? "";
1695
- let result;
1696
- switch (typeName) {
1697
- case "ZodString":
1698
- result = "string";
1699
- break;
1700
- case "ZodNumber":
1701
- result = "number";
1702
- break;
1703
- case "ZodBoolean":
1704
- result = "boolean";
1705
- break;
1706
- case "ZodLiteral":
1707
- result = JSON.stringify(def.value);
1708
- break;
1709
- case "ZodEnum":
1710
- result = def.values.map((v) => `"${v}"`).join("|");
1711
- break;
1712
- case "ZodNativeEnum":
1713
- result = Object.values(def.values).map((v) => `"${v}"`).join("|");
1714
- break;
1715
- case "ZodArray":
1716
- result = def.type ? `Array<${formatZodType2(def.type)}>` : "Array<unknown>";
1717
- break;
1718
- case "ZodObject": {
1719
- if (!def.shape) {
1720
- result = "object";
1721
- break;
1722
- }
1723
- const shape = def.shape();
1724
- const props = Object.entries(shape).map(([key, value]) => {
1725
- const innerDef = value._def;
1726
- const innerOptional = innerDef.typeName === "ZodOptional" || innerDef.typeName === "ZodNullable";
1727
- return `${key}${innerOptional ? "?" : ""}: ${formatZodType2(value)}`;
1728
- }).join(", ");
1729
- result = `{ ${props} }`;
1730
- break;
1731
- }
1732
- case "ZodOptional":
1733
- return def.innerType ? formatZodType2(def.innerType, true) : "unknown?";
1734
- case "ZodNullable":
1735
- return def.innerType ? formatZodType2(def.innerType, true) : "unknown?";
1736
- case "ZodDefault":
1737
- return def.innerType ? formatZodType2(def.innerType, isOptional) : "unknown";
1738
- case "ZodUnion":
1739
- result = def.options ? def.options.map((opt) => formatZodType2(opt)).join("|") : "unknown";
1740
- break;
1741
- case "ZodNull":
1742
- result = "null";
1743
- break;
1744
- case "ZodUndefined":
1745
- result = "undefined";
1746
- break;
1747
- case "ZodAny":
1748
- result = "any";
1749
- break;
1750
- case "ZodUnknown":
1751
- result = "unknown";
1752
- break;
1753
- default:
1754
- result = "unknown";
1755
- }
1756
- return isOptional ? `${result}?` : result;
1757
- }
1758
- function extractPropsFromSchema(schema) {
1759
- const def = schema._def;
1760
- const typeName = def.typeName ?? "";
1761
- if (typeName !== "ZodObject" || !def.shape) {
1762
- return [];
1763
- }
1764
- const shape = def.shape();
1765
- return Object.entries(shape).map(([name, value]) => {
1766
- const innerDef = value._def;
1767
- const optional = innerDef.typeName === "ZodOptional" || innerDef.typeName === "ZodNullable";
1768
- return {
1769
- name,
1770
- type: formatZodType2(value),
1771
- optional
1772
- };
1773
- });
1774
- }
1775
- function formatPropsCompact(props) {
1776
- if (props.length === 0) return "{}";
1777
- const entries = props.map(
1778
- (p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`
1779
- );
1780
- return `{ ${entries.join(", ")} }`;
1781
- }
1782
- function generateSystemPrompt(catalog, options = {}) {
1783
- const {
1784
- system = "You are a UI generator that outputs JSONL (JSON Lines) patches.",
1785
- customRules = []
1786
- } = options;
1787
- const lines = [];
1788
- lines.push(system);
1789
- lines.push("");
1790
- const componentCount = catalog.componentNames.length;
1791
- lines.push(`AVAILABLE COMPONENTS (${componentCount}):`);
1792
- lines.push("");
1793
- for (const name of catalog.componentNames) {
1794
- const def = catalog.components[name];
1795
- const props = extractPropsFromSchema(def.props);
1796
- const propsStr = formatPropsCompact(props);
1797
- const hasChildrenStr = def.hasChildren ? " Has children." : "";
1798
- const descStr = def.description ? ` ${def.description}` : "";
1799
- lines.push(`- ${String(name)}: ${propsStr}${descStr}${hasChildrenStr}`);
1800
- }
1801
- lines.push("");
1802
- if (catalog.actionNames.length > 0) {
1803
- lines.push("AVAILABLE ACTIONS:");
1804
- lines.push("");
1805
- for (const name of catalog.actionNames) {
1806
- const def = catalog.actions[name];
1807
- lines.push(
1808
- `- ${String(name)}${def.description ? `: ${def.description}` : ""}`
1809
- );
1810
- }
1811
- lines.push("");
1812
- }
1813
- lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):");
1814
- lines.push('{"op":"add","path":"/root","value":"element-key"}');
1815
- lines.push(
1816
- '{"op":"add","path":"/elements/key","value":{"type":"...","props":{...},"children":[...]}}'
1817
- );
1818
- lines.push('{"op":"remove","path":"/elements/key"}');
1819
- lines.push("");
1820
- lines.push("RULES:");
1821
- const baseRules = [
1822
- 'First line sets /root to root element key: {"op":"add","path":"/root","value":"<key>"}',
1823
- 'Add elements with /elements/{key}: {"op":"add","path":"/elements/<key>","value":{...}}',
1824
- "Remove elements with op:remove - also update the parent's children array to exclude the removed key",
1825
- "Children array contains string keys, not objects",
1826
- "Parent first, then children",
1827
- "Each element needs: type, props",
1828
- "ONLY use props listed above - never invent new props"
1829
- ];
1830
- const allRules = [...baseRules, ...customRules];
1831
- allRules.forEach((rule, i) => {
1832
- lines.push(`${i + 1}. ${rule}`);
1833
- });
1834
- lines.push("");
1835
- if (catalog.functionNames.length > 0) {
1836
- lines.push("CUSTOM VALIDATION FUNCTIONS:");
1837
- lines.push(catalog.functionNames.map(String).join(", "));
1838
- lines.push("");
1839
- }
1840
- lines.push("Generate JSONL:");
1841
- return lines.join("\n");
1842
- }
1843
2076
  export {
1844
2077
  ActionBindingSchema,
1845
2078
  ActionConfirmSchema,
@@ -1850,35 +2083,39 @@ export {
1850
2083
  DynamicNumberSchema,
1851
2084
  DynamicStringSchema,
1852
2085
  DynamicValueSchema,
1853
- LogicExpressionSchema,
2086
+ SPEC_DATA_PART,
2087
+ SPEC_DATA_PART_TYPE,
1854
2088
  ValidationCheckSchema,
1855
2089
  ValidationConfigSchema,
1856
2090
  VisibilityConditionSchema,
1857
2091
  action,
1858
2092
  actionBinding,
1859
2093
  addByPath,
2094
+ applySpecPatch,
1860
2095
  applySpecStreamPatch,
1861
2096
  autoFixSpec,
1862
2097
  buildUserPrompt,
1863
2098
  builtInValidationFunctions,
1864
2099
  check,
1865
2100
  compileSpecStream,
1866
- createCatalog,
2101
+ createJsonRenderTransform,
2102
+ createMixedStreamParser,
1867
2103
  createSpecStreamCompiler,
1868
2104
  defineCatalog,
1869
2105
  defineSchema,
1870
- evaluateLogicExpression,
1871
2106
  evaluateVisibility,
1872
2107
  executeAction,
1873
2108
  findFormValue,
1874
2109
  formatSpecIssues,
1875
- generateCatalogPrompt,
1876
- generateSystemPrompt,
1877
2110
  getByPath,
1878
2111
  interpolateString,
2112
+ nestedToFlat,
1879
2113
  parseSpecStreamLine,
2114
+ pipeJsonRender,
1880
2115
  removeByPath,
1881
2116
  resolveAction,
2117
+ resolveActionParam,
2118
+ resolveBindings,
1882
2119
  resolveDynamicValue,
1883
2120
  resolveElementProps,
1884
2121
  resolvePropValue,