@json-render/yaml 0.13.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 ADDED
@@ -0,0 +1,801 @@
1
+ // src/diff.ts
2
+ import { diffToPatches } from "@json-render/core";
3
+
4
+ // src/merge.ts
5
+ import { deepMergeSpec } from "@json-render/core";
6
+
7
+ // src/parser.ts
8
+ import { parse } from "yaml";
9
+ function createYamlStreamCompiler(initial) {
10
+ let accumulated = "";
11
+ let snapshot = initial ? { ...initial } : {};
12
+ let result = { ...snapshot };
13
+ const allPatches = [];
14
+ function tryParse() {
15
+ const newPatches = [];
16
+ try {
17
+ const parsed = parse(accumulated);
18
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
19
+ const patches = diffToPatches(
20
+ snapshot,
21
+ parsed
22
+ );
23
+ if (patches.length > 0) {
24
+ snapshot = structuredClone(parsed);
25
+ result = { ...snapshot };
26
+ allPatches.push(...patches);
27
+ newPatches.push(...patches);
28
+ }
29
+ }
30
+ } catch {
31
+ }
32
+ return { result, newPatches };
33
+ }
34
+ return {
35
+ push(chunk) {
36
+ accumulated += chunk;
37
+ if (chunk.includes("\n")) {
38
+ return tryParse();
39
+ }
40
+ return { result, newPatches: [] };
41
+ },
42
+ flush() {
43
+ return tryParse();
44
+ },
45
+ getResult() {
46
+ return result;
47
+ },
48
+ getPatches() {
49
+ return [...allPatches];
50
+ },
51
+ reset(newInitial) {
52
+ accumulated = "";
53
+ snapshot = newInitial ? { ...newInitial } : {};
54
+ result = { ...snapshot };
55
+ allPatches.length = 0;
56
+ }
57
+ };
58
+ }
59
+
60
+ // src/transform.ts
61
+ import { parse as parse2, stringify } from "yaml";
62
+ import { applyPatch as applyUnifiedDiff } from "diff";
63
+ import {
64
+ SPEC_DATA_PART_TYPE,
65
+ deepMergeSpec as deepMergeSpec2,
66
+ diffToPatches as diffToPatches2
67
+ } from "@json-render/core";
68
+ var YAML_SPEC_FENCE = "```yaml-spec";
69
+ var YAML_EDIT_FENCE = "```yaml-edit";
70
+ var YAML_PATCH_FENCE = "```yaml-patch";
71
+ var DIFF_FENCE = "```diff";
72
+ var FENCE_CLOSE = "```";
73
+ function createYamlTransform(options) {
74
+ let currentTextId = "";
75
+ let inTextBlock = false;
76
+ let textIdCounter = 0;
77
+ let lineBuffer = "";
78
+ let buffering = false;
79
+ let fenceMode = null;
80
+ let yamlAccumulated = "";
81
+ let diffAccumulated = "";
82
+ let compiler = createYamlStreamCompiler();
83
+ let currentSpec = options?.previousSpec ? structuredClone(
84
+ options.previousSpec
85
+ ) : {};
86
+ function closeTextBlock(controller) {
87
+ if (inTextBlock) {
88
+ controller.enqueue({ type: "text-end", id: currentTextId });
89
+ inTextBlock = false;
90
+ }
91
+ }
92
+ function ensureTextBlock(controller) {
93
+ if (!inTextBlock) {
94
+ textIdCounter++;
95
+ currentTextId = String(textIdCounter);
96
+ controller.enqueue({ type: "text-start", id: currentTextId });
97
+ inTextBlock = true;
98
+ }
99
+ }
100
+ function emitTextDelta(delta, controller) {
101
+ ensureTextBlock(controller);
102
+ controller.enqueue({ type: "text-delta", id: currentTextId, delta });
103
+ }
104
+ function emitPatch(patch, controller) {
105
+ closeTextBlock(controller);
106
+ controller.enqueue({
107
+ type: SPEC_DATA_PART_TYPE,
108
+ data: { type: "patch", patch }
109
+ });
110
+ }
111
+ function emitPatches(patches, controller) {
112
+ for (const patch of patches) {
113
+ emitPatch(patch, controller);
114
+ }
115
+ }
116
+ function feedYamlSpec(line, controller) {
117
+ yamlAccumulated += line + "\n";
118
+ const { newPatches } = compiler.push(line + "\n");
119
+ if (newPatches.length > 0) {
120
+ emitPatches(newPatches, controller);
121
+ }
122
+ }
123
+ function feedYamlEdit(line) {
124
+ yamlAccumulated += line + "\n";
125
+ }
126
+ function finaliseYamlEdit(controller) {
127
+ try {
128
+ const editObj = parse2(yamlAccumulated);
129
+ if (editObj && typeof editObj === "object" && !Array.isArray(editObj)) {
130
+ const merged = deepMergeSpec2(
131
+ currentSpec,
132
+ editObj
133
+ );
134
+ const patches = diffToPatches2(currentSpec, merged);
135
+ if (patches.length > 0) {
136
+ currentSpec = merged;
137
+ emitPatches(patches, controller);
138
+ }
139
+ }
140
+ } catch {
141
+ }
142
+ }
143
+ function finaliseYamlSpec(controller) {
144
+ const { newPatches } = compiler.flush();
145
+ if (newPatches.length > 0) {
146
+ emitPatches(newPatches, controller);
147
+ }
148
+ currentSpec = structuredClone(
149
+ compiler.getResult()
150
+ );
151
+ }
152
+ function finaliseYamlPatch(controller) {
153
+ for (const line of yamlAccumulated.split("\n")) {
154
+ const trimmed = line.trim();
155
+ if (!trimmed) continue;
156
+ try {
157
+ const patch = JSON.parse(trimmed);
158
+ if (patch.op) {
159
+ emitPatch(patch, controller);
160
+ if (patch.op === "add" || patch.op === "replace") {
161
+ const parts = patch.path.split("/").filter(Boolean);
162
+ let target = currentSpec;
163
+ for (let i = 0; i < parts.length - 1; i++) {
164
+ const key = parts[i];
165
+ if (typeof target[key] !== "object" || target[key] === null) {
166
+ target[key] = {};
167
+ }
168
+ target = target[key];
169
+ }
170
+ const lastKey = parts[parts.length - 1];
171
+ if (lastKey) target[lastKey] = patch.value;
172
+ } else if (patch.op === "remove") {
173
+ const parts = patch.path.split("/").filter(Boolean);
174
+ let target = currentSpec;
175
+ for (let i = 0; i < parts.length - 1; i++) {
176
+ const key = parts[i];
177
+ if (typeof target[key] !== "object" || target[key] === null)
178
+ break;
179
+ target = target[key];
180
+ }
181
+ const lastKey = parts[parts.length - 1];
182
+ if (lastKey) delete target[lastKey];
183
+ }
184
+ }
185
+ } catch {
186
+ }
187
+ }
188
+ }
189
+ function finaliseDiff(controller) {
190
+ try {
191
+ const currentYaml = stringify(currentSpec, { indent: 2 });
192
+ const patched = applyUnifiedDiff(currentYaml, diffAccumulated);
193
+ if (patched === false) return;
194
+ const newSpec = parse2(patched);
195
+ if (newSpec && typeof newSpec === "object" && !Array.isArray(newSpec)) {
196
+ const patches = diffToPatches2(
197
+ currentSpec,
198
+ newSpec
199
+ );
200
+ if (patches.length > 0) {
201
+ currentSpec = newSpec;
202
+ emitPatches(patches, controller);
203
+ }
204
+ }
205
+ } catch {
206
+ }
207
+ }
208
+ function processCompleteLine(line, controller) {
209
+ const trimmed = line.trim();
210
+ if (fenceMode === null) {
211
+ if (trimmed.startsWith(YAML_SPEC_FENCE)) {
212
+ fenceMode = "yaml-spec";
213
+ yamlAccumulated = "";
214
+ compiler.reset(currentSpec);
215
+ return;
216
+ }
217
+ if (trimmed.startsWith(YAML_EDIT_FENCE)) {
218
+ fenceMode = "yaml-edit";
219
+ yamlAccumulated = "";
220
+ return;
221
+ }
222
+ if (trimmed.startsWith(YAML_PATCH_FENCE)) {
223
+ fenceMode = "yaml-patch";
224
+ yamlAccumulated = "";
225
+ return;
226
+ }
227
+ if (trimmed.startsWith(DIFF_FENCE)) {
228
+ fenceMode = "diff";
229
+ diffAccumulated = "";
230
+ return;
231
+ }
232
+ }
233
+ if (fenceMode !== null && trimmed === FENCE_CLOSE) {
234
+ if (fenceMode === "yaml-spec") {
235
+ finaliseYamlSpec(controller);
236
+ } else if (fenceMode === "yaml-edit") {
237
+ finaliseYamlEdit(controller);
238
+ } else if (fenceMode === "yaml-patch") {
239
+ finaliseYamlPatch(controller);
240
+ } else if (fenceMode === "diff") {
241
+ finaliseDiff(controller);
242
+ }
243
+ fenceMode = null;
244
+ return;
245
+ }
246
+ if (fenceMode === "yaml-spec") {
247
+ feedYamlSpec(line, controller);
248
+ return;
249
+ }
250
+ if (fenceMode === "yaml-edit") {
251
+ feedYamlEdit(line);
252
+ return;
253
+ }
254
+ if (fenceMode === "yaml-patch") {
255
+ yamlAccumulated += line + "\n";
256
+ return;
257
+ }
258
+ if (fenceMode === "diff") {
259
+ diffAccumulated += line + "\n";
260
+ return;
261
+ }
262
+ if (!trimmed) {
263
+ emitTextDelta("\n", controller);
264
+ return;
265
+ }
266
+ emitTextDelta(line + "\n", controller);
267
+ }
268
+ function flushBuffer(controller) {
269
+ if (!lineBuffer) return;
270
+ if (fenceMode !== null) {
271
+ processCompleteLine(lineBuffer, controller);
272
+ } else {
273
+ emitTextDelta(lineBuffer, controller);
274
+ }
275
+ lineBuffer = "";
276
+ buffering = false;
277
+ }
278
+ return new TransformStream({
279
+ transform(chunk, controller) {
280
+ switch (chunk.type) {
281
+ case "text-start": {
282
+ const id = chunk.id;
283
+ const idNum = parseInt(id, 10);
284
+ if (!isNaN(idNum) && idNum >= textIdCounter) {
285
+ textIdCounter = idNum;
286
+ }
287
+ currentTextId = id;
288
+ inTextBlock = true;
289
+ controller.enqueue(chunk);
290
+ break;
291
+ }
292
+ case "text-delta": {
293
+ const delta = chunk;
294
+ const text = delta.delta;
295
+ for (let i = 0; i < text.length; i++) {
296
+ const ch = text.charAt(i);
297
+ if (ch === "\n") {
298
+ if (buffering) {
299
+ processCompleteLine(lineBuffer, controller);
300
+ lineBuffer = "";
301
+ buffering = false;
302
+ } else if (fenceMode === null) {
303
+ emitTextDelta("\n", controller);
304
+ }
305
+ } else if (lineBuffer.length === 0 && !buffering) {
306
+ if (fenceMode !== null || ch === "`") {
307
+ buffering = true;
308
+ lineBuffer += ch;
309
+ } else {
310
+ emitTextDelta(ch, controller);
311
+ }
312
+ } else if (buffering) {
313
+ lineBuffer += ch;
314
+ } else {
315
+ emitTextDelta(ch, controller);
316
+ }
317
+ }
318
+ break;
319
+ }
320
+ case "text-end": {
321
+ flushBuffer(controller);
322
+ if (inTextBlock) {
323
+ controller.enqueue({ type: "text-end", id: currentTextId });
324
+ inTextBlock = false;
325
+ }
326
+ break;
327
+ }
328
+ default: {
329
+ controller.enqueue(chunk);
330
+ break;
331
+ }
332
+ }
333
+ },
334
+ flush(controller) {
335
+ flushBuffer(controller);
336
+ if (fenceMode === "yaml-spec") {
337
+ finaliseYamlSpec(controller);
338
+ } else if (fenceMode === "yaml-edit") {
339
+ finaliseYamlEdit(controller);
340
+ } else if (fenceMode === "yaml-patch") {
341
+ finaliseYamlPatch(controller);
342
+ } else if (fenceMode === "diff") {
343
+ finaliseDiff(controller);
344
+ }
345
+ closeTextBlock(controller);
346
+ }
347
+ });
348
+ }
349
+ function pipeYamlRender(stream, options) {
350
+ return stream.pipeThrough(
351
+ createYamlTransform(options)
352
+ );
353
+ }
354
+
355
+ // src/prompt.ts
356
+ import { stringify as stringify2 } from "yaml";
357
+ import { buildEditInstructions } from "@json-render/core";
358
+ function getZodTypeName(schema) {
359
+ if (!schema?._def) return "";
360
+ const def = schema._def;
361
+ return def.typeName ?? (typeof def.type === "string" ? def.type : "") ?? "";
362
+ }
363
+ function formatZodType(schema) {
364
+ if (!schema?._def) return "unknown";
365
+ const def = schema._def;
366
+ const typeName = getZodTypeName(schema);
367
+ switch (typeName) {
368
+ case "ZodString":
369
+ case "string":
370
+ return "string";
371
+ case "ZodNumber":
372
+ case "number":
373
+ return "number";
374
+ case "ZodBoolean":
375
+ case "boolean":
376
+ return "boolean";
377
+ case "ZodLiteral":
378
+ case "literal":
379
+ return JSON.stringify(def.value);
380
+ case "ZodEnum":
381
+ case "enum": {
382
+ let values;
383
+ if (Array.isArray(def.values)) {
384
+ values = def.values;
385
+ } else if (def.entries && typeof def.entries === "object") {
386
+ values = Object.values(def.entries);
387
+ } else {
388
+ return "enum";
389
+ }
390
+ return values.map((v) => `"${v}"`).join(" | ");
391
+ }
392
+ case "ZodArray":
393
+ case "array": {
394
+ const inner = typeof def.element === "object" ? def.element : typeof def.type === "object" ? def.type : void 0;
395
+ return inner ? `Array<${formatZodType(inner)}>` : "Array<unknown>";
396
+ }
397
+ case "ZodObject":
398
+ case "object": {
399
+ const shape = typeof def.shape === "function" ? def.shape() : def.shape;
400
+ if (!shape) return "object";
401
+ const props = Object.entries(shape).map(([key, value]) => {
402
+ const innerTypeName = getZodTypeName(value);
403
+ const isOptional = innerTypeName === "ZodOptional" || innerTypeName === "ZodNullable" || innerTypeName === "optional" || innerTypeName === "nullable";
404
+ return `${key}${isOptional ? "?" : ""}: ${formatZodType(value)}`;
405
+ }).join(", ");
406
+ return `{ ${props} }`;
407
+ }
408
+ case "ZodOptional":
409
+ case "optional":
410
+ case "ZodNullable":
411
+ case "nullable": {
412
+ const inner = def.innerType ?? def.wrapped;
413
+ return inner ? formatZodType(inner) : "unknown";
414
+ }
415
+ case "ZodUnion":
416
+ case "union": {
417
+ const options = def.options;
418
+ return options ? options.map((opt) => formatZodType(opt)).join(" | ") : "unknown";
419
+ }
420
+ default:
421
+ return "unknown";
422
+ }
423
+ }
424
+ function getExampleProps(def) {
425
+ if (def.example && Object.keys(def.example).length > 0) return def.example;
426
+ if (!def.props?._def) return {};
427
+ const zodDef = def.props._def;
428
+ const typeName = getZodTypeName(def.props);
429
+ if (typeName !== "ZodObject" && typeName !== "object") return {};
430
+ const shape = typeof zodDef.shape === "function" ? zodDef.shape() : zodDef.shape;
431
+ if (!shape) return {};
432
+ const result = {};
433
+ for (const [key, value] of Object.entries(shape)) {
434
+ const inner = getZodTypeName(value);
435
+ if (inner === "ZodOptional" || inner === "optional" || inner === "ZodNullable" || inner === "nullable")
436
+ continue;
437
+ result[key] = exampleValue(value);
438
+ }
439
+ return result;
440
+ }
441
+ function exampleValue(schema) {
442
+ if (!schema?._def) return "...";
443
+ const def = schema._def;
444
+ const t = getZodTypeName(schema);
445
+ switch (t) {
446
+ case "ZodString":
447
+ case "string":
448
+ return "example";
449
+ case "ZodNumber":
450
+ case "number":
451
+ return 0;
452
+ case "ZodBoolean":
453
+ case "boolean":
454
+ return true;
455
+ case "ZodLiteral":
456
+ case "literal":
457
+ return def.value;
458
+ case "ZodEnum":
459
+ case "enum": {
460
+ if (Array.isArray(def.values) && def.values.length > 0)
461
+ return def.values[0];
462
+ if (def.entries && typeof def.entries === "object") {
463
+ const vals = Object.values(def.entries);
464
+ return vals.length > 0 ? vals[0] : "example";
465
+ }
466
+ return "example";
467
+ }
468
+ case "ZodOptional":
469
+ case "optional":
470
+ case "ZodNullable":
471
+ case "nullable":
472
+ case "ZodDefault":
473
+ case "default": {
474
+ const inner = def.innerType ?? def.wrapped;
475
+ return inner ? exampleValue(inner) : null;
476
+ }
477
+ case "ZodArray":
478
+ case "array":
479
+ return [];
480
+ case "ZodObject":
481
+ case "object":
482
+ return getExampleProps({ props: schema });
483
+ case "ZodUnion":
484
+ case "union": {
485
+ const options = def.options;
486
+ return options && options.length > 0 ? exampleValue(options[0]) : "...";
487
+ }
488
+ default:
489
+ return "...";
490
+ }
491
+ }
492
+ function toYaml(value) {
493
+ return stringify2(value, { indent: 2 }).trimEnd();
494
+ }
495
+ function yamlPrompt(catalog, options) {
496
+ const {
497
+ system = "You are a UI generator that outputs YAML.",
498
+ mode = "standalone",
499
+ customRules = [],
500
+ editModes = ["merge"]
501
+ } = options ?? {};
502
+ const lines = [];
503
+ lines.push(system);
504
+ lines.push("");
505
+ if (mode === "inline") {
506
+ lines.push("OUTPUT FORMAT (text + YAML):");
507
+ lines.push(
508
+ "You respond conversationally. When generating UI, first write a brief explanation (1-3 sentences), then output the YAML spec wrapped in a ```yaml-spec code fence."
509
+ );
510
+ lines.push(
511
+ "If the user's message does not require a UI (e.g. a greeting or clarifying question), respond with text only \u2014 no YAML."
512
+ );
513
+ } else {
514
+ lines.push("OUTPUT FORMAT (YAML):");
515
+ lines.push(
516
+ "Output a YAML document that describes a UI tree. Wrap it in a ```yaml-spec code fence."
517
+ );
518
+ }
519
+ lines.push("");
520
+ lines.push(
521
+ "The YAML document has three top-level keys: root, elements, and state (optional)."
522
+ );
523
+ lines.push(
524
+ "Stream progressively \u2014 output elements one at a time so the UI fills in as you write."
525
+ );
526
+ lines.push("");
527
+ const allComponents = catalog.data.components;
528
+ const cn = catalog.componentNames;
529
+ const comp1 = cn[0] || "Component";
530
+ const comp2 = cn.length > 1 ? cn[1] : comp1;
531
+ const comp1Def = allComponents?.[comp1];
532
+ const comp2Def = allComponents?.[comp2];
533
+ const comp1Props = comp1Def ? getExampleProps(comp1Def) : {};
534
+ const comp2Props = comp2Def ? getExampleProps(comp2Def) : {};
535
+ const exampleSpec = {
536
+ root: "main",
537
+ elements: {
538
+ main: {
539
+ type: comp1,
540
+ props: comp1Props,
541
+ children: ["child-1", "list"]
542
+ },
543
+ "child-1": {
544
+ type: comp2,
545
+ props: comp2Props,
546
+ children: []
547
+ },
548
+ list: {
549
+ type: comp1,
550
+ props: comp1Props,
551
+ repeat: { statePath: "/items", key: "id" },
552
+ children: ["item"]
553
+ },
554
+ item: {
555
+ type: comp2,
556
+ props: { ...comp2Props },
557
+ children: []
558
+ }
559
+ },
560
+ state: {
561
+ items: [
562
+ { id: "1", title: "First Item" },
563
+ { id: "2", title: "Second Item" }
564
+ ]
565
+ }
566
+ };
567
+ lines.push("Example:");
568
+ lines.push("");
569
+ lines.push("```yaml-spec");
570
+ lines.push(toYaml(exampleSpec));
571
+ lines.push("```");
572
+ lines.push("");
573
+ lines.push(buildEditInstructions({ modes: editModes }, "yaml"));
574
+ lines.push("INITIAL STATE:");
575
+ lines.push(
576
+ "The spec includes a top-level `state` key to seed the state model. Components using $state, $bindState, $bindItem, $item, or $index read from this state."
577
+ );
578
+ lines.push(
579
+ "CRITICAL: You MUST include state whenever your UI displays data via these expressions or uses repeat to iterate over arrays."
580
+ );
581
+ lines.push(
582
+ "Include realistic sample data. For lists: 3-5 items with relevant fields. Never leave arrays empty."
583
+ );
584
+ lines.push(
585
+ 'When content comes from the state model, use { "$state": "/some/path" } dynamic props instead of hardcoding values.'
586
+ );
587
+ lines.push(
588
+ 'State paths use RFC 6901 JSON Pointer syntax (e.g. "/todos/0/title"). Do NOT use dot notation.'
589
+ );
590
+ lines.push("");
591
+ lines.push("DYNAMIC LISTS (repeat field):");
592
+ lines.push(
593
+ "Any element can have a top-level `repeat` field to render its children once per item in a state array."
594
+ );
595
+ lines.push("Example:");
596
+ lines.push("");
597
+ lines.push(
598
+ toYaml({
599
+ list: {
600
+ type: comp1,
601
+ props: comp1Props,
602
+ repeat: { statePath: "/todos", key: "id" },
603
+ children: ["todo-item"]
604
+ }
605
+ })
606
+ );
607
+ lines.push("");
608
+ lines.push(
609
+ 'Inside repeated children, use { "$item": "field" } for item data and { "$index": true } for the array index.'
610
+ );
611
+ lines.push(
612
+ "ALWAYS use repeat for lists backed by state arrays. NEVER hardcode individual elements per item."
613
+ );
614
+ lines.push(
615
+ "IMPORTANT: `repeat` is a top-level field on the element (sibling of type/props/children), NOT inside props."
616
+ );
617
+ lines.push("");
618
+ lines.push("ARRAY STATE ACTIONS:");
619
+ lines.push(
620
+ 'Use "pushState" to append items to arrays. Use "removeState" to remove items by index.'
621
+ );
622
+ lines.push('Use "$id" inside pushState values to auto-generate a unique ID.');
623
+ lines.push("");
624
+ if (allComponents) {
625
+ lines.push(`AVAILABLE COMPONENTS (${catalog.componentNames.length}):`);
626
+ lines.push("");
627
+ for (const [name, def] of Object.entries(allComponents)) {
628
+ const propsStr = def.props ? formatZodType(def.props) : "{}";
629
+ const hasChildren = def.slots && def.slots.length > 0;
630
+ const childrenStr = hasChildren ? " [accepts children]" : "";
631
+ const eventsStr = def.events && def.events.length > 0 ? ` [events: ${def.events.join(", ")}]` : "";
632
+ const descStr = def.description ? ` - ${def.description}` : "";
633
+ lines.push(`- ${name}: ${propsStr}${descStr}${childrenStr}${eventsStr}`);
634
+ }
635
+ lines.push("");
636
+ }
637
+ const actions = catalog.data.actions;
638
+ const builtInActions = catalog.schema.builtInActions ?? [];
639
+ const hasCustomActions = actions && catalog.actionNames.length > 0;
640
+ const hasBuiltInActions = builtInActions.length > 0;
641
+ if (hasCustomActions || hasBuiltInActions) {
642
+ lines.push("AVAILABLE ACTIONS:");
643
+ lines.push("");
644
+ for (const action of builtInActions) {
645
+ lines.push(`- ${action.name}: ${action.description} [built-in]`);
646
+ }
647
+ if (hasCustomActions) {
648
+ for (const [name, def] of Object.entries(actions)) {
649
+ lines.push(`- ${name}${def.description ? `: ${def.description}` : ""}`);
650
+ }
651
+ }
652
+ lines.push("");
653
+ }
654
+ lines.push("EVENTS (the `on` field):");
655
+ lines.push(
656
+ "Elements can have an optional `on` field to bind events to actions. It is a top-level field (sibling of type/props/children), NOT inside props."
657
+ );
658
+ lines.push("Example:");
659
+ lines.push("");
660
+ lines.push(
661
+ toYaml({
662
+ "save-btn": {
663
+ type: comp1,
664
+ props: comp1Props,
665
+ on: {
666
+ press: {
667
+ action: "setState",
668
+ params: { statePath: "/saved", value: true }
669
+ }
670
+ },
671
+ children: []
672
+ }
673
+ })
674
+ );
675
+ lines.push("");
676
+ lines.push(
677
+ 'Action params can use dynamic references: { "$state": "/statePath" }.'
678
+ );
679
+ lines.push(
680
+ "IMPORTANT: Do NOT put action/actionParams inside props. Always use the `on` field."
681
+ );
682
+ lines.push("");
683
+ lines.push("VISIBILITY CONDITIONS:");
684
+ lines.push(
685
+ "Elements can have an optional `visible` field to conditionally show/hide based on state. It is a top-level field (sibling of type/props/children)."
686
+ );
687
+ lines.push("Conditions:");
688
+ lines.push('- { "$state": "/path" } \u2014 visible when truthy');
689
+ lines.push('- { "$state": "/path", "not": true } \u2014 visible when falsy');
690
+ lines.push('- { "$state": "/path", "eq": "value" } \u2014 visible when equal');
691
+ lines.push(
692
+ '- { "$state": "/path", "neq": "value" } \u2014 visible when not equal'
693
+ );
694
+ lines.push(
695
+ '- { "$state": "/path", "gt": N } / gte / lt / lte \u2014 numeric comparisons'
696
+ );
697
+ lines.push("- Use ONE operator per condition. Do not combine multiple.");
698
+ lines.push('- Add "not": true to any condition to invert it.');
699
+ lines.push("- [cond, cond] \u2014 implicit AND (all must be true)");
700
+ lines.push('- { "$and": [...] } \u2014 explicit AND');
701
+ lines.push('- { "$or": [...] } \u2014 at least one must be true');
702
+ lines.push("- true / false \u2014 always visible/hidden");
703
+ lines.push("");
704
+ lines.push("DYNAMIC PROPS:");
705
+ lines.push("Any prop value can be a dynamic expression:");
706
+ lines.push(
707
+ '1. Read-only: { "$state": "/path" } \u2014 resolves to the value at that state path.'
708
+ );
709
+ lines.push(
710
+ '2. Two-way binding: { "$bindState": "/path" } \u2014 read + write. Use on form inputs.'
711
+ );
712
+ lines.push(
713
+ ' Inside repeat scopes: { "$bindItem": "field" } for item-level binding.'
714
+ );
715
+ lines.push(
716
+ '3. Conditional: { "$cond": <condition>, "$then": <val>, "$else": <val> }'
717
+ );
718
+ lines.push(
719
+ '4. Template: { "$template": "Hello, ${/name}!" } \u2014 interpolates state references.'
720
+ );
721
+ lines.push("");
722
+ const catalogFunctions = catalog.data.functions;
723
+ if (catalogFunctions && Object.keys(catalogFunctions).length > 0) {
724
+ lines.push(
725
+ '5. Computed: { "$computed": "<fn>", "args": { "key": <expr> } }'
726
+ );
727
+ lines.push(" Available functions:");
728
+ for (const name of Object.keys(
729
+ catalogFunctions
730
+ )) {
731
+ lines.push(` - ${name}`);
732
+ }
733
+ lines.push("");
734
+ }
735
+ const hasChecks = allComponents ? Object.values(allComponents).some((def) => {
736
+ if (!def.props) return false;
737
+ return formatZodType(def.props).includes("checks");
738
+ }) : false;
739
+ if (hasChecks) {
740
+ lines.push("VALIDATION:");
741
+ lines.push(
742
+ "Form components with a `checks` prop support client-side validation."
743
+ );
744
+ lines.push(
745
+ "Built-in types: required, email, minLength, maxLength, pattern, min, max, numeric, url, matches, equalTo, lessThan, greaterThan, requiredIf."
746
+ );
747
+ lines.push(
748
+ "IMPORTANT: Components with checks must also use $bindState or $bindItem for two-way binding."
749
+ );
750
+ lines.push("");
751
+ }
752
+ if (hasCustomActions || hasBuiltInActions) {
753
+ lines.push("STATE WATCHERS:");
754
+ lines.push(
755
+ "Elements can have an optional `watch` field to trigger actions when state changes. Top-level field, NOT inside props."
756
+ );
757
+ lines.push(
758
+ "Maps state paths to action bindings. Fires when the watched value changes (not on initial render)."
759
+ );
760
+ lines.push("");
761
+ }
762
+ lines.push("RULES:");
763
+ const baseRules = mode === "inline" ? [
764
+ "When generating UI, wrap the YAML in a ```yaml-spec code fence",
765
+ "Write a brief conversational response before the YAML",
766
+ "When editing existing UI, use a ```yaml-edit fence with only the changed parts",
767
+ "The document must have: root (string), elements (map of elements), and optionally state",
768
+ "Each element must have: type, props, children (list of child keys)",
769
+ "ONLY use components listed above",
770
+ "Use unique, descriptive keys for elements (e.g. 'header', 'metric-1', 'chart-revenue')",
771
+ "Include state whenever using $state, $bindState, $bindItem, $item, $index, or repeat"
772
+ ] : [
773
+ "Output ONLY the YAML spec inside a ```yaml-spec fence \u2014 no prose, no extra markdown",
774
+ "When editing existing UI, use a ```yaml-edit fence with only the changed parts",
775
+ "The document must have: root (string), elements (map of elements), and optionally state",
776
+ "Each element must have: type, props, children (list of child keys)",
777
+ "ONLY use components listed above",
778
+ "Use unique, descriptive keys for elements (e.g. 'header', 'metric-1', 'chart-revenue')",
779
+ "Include state whenever using $state, $bindState, $bindItem, $item, $index, or repeat"
780
+ ];
781
+ const schemaRules = catalog.schema.defaultRules ?? [];
782
+ const allRules = [...baseRules, ...schemaRules, ...customRules];
783
+ allRules.forEach((rule, i) => {
784
+ lines.push(`${i + 1}. ${rule}`);
785
+ });
786
+ return lines.join("\n");
787
+ }
788
+ export {
789
+ DIFF_FENCE,
790
+ FENCE_CLOSE,
791
+ YAML_EDIT_FENCE,
792
+ YAML_PATCH_FENCE,
793
+ YAML_SPEC_FENCE,
794
+ createYamlStreamCompiler,
795
+ createYamlTransform,
796
+ deepMergeSpec,
797
+ diffToPatches,
798
+ pipeYamlRender,
799
+ yamlPrompt
800
+ };
801
+ //# sourceMappingURL=index.mjs.map