@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/README.md +91 -16
- package/dist/index.d.mts +424 -212
- package/dist/index.d.ts +424 -212
- package/dist/index.js +768 -527
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +759 -522
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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({
|
|
8
|
+
z.object({ $state: z.string() })
|
|
9
9
|
]);
|
|
10
10
|
var DynamicStringSchema = z.union([
|
|
11
11
|
z.string(),
|
|
12
|
-
z.object({
|
|
12
|
+
z.object({ $state: z.string() })
|
|
13
13
|
]);
|
|
14
14
|
var DynamicNumberSchema = z.union([
|
|
15
15
|
z.number(),
|
|
16
|
-
z.object({
|
|
16
|
+
z.object({ $state: z.string() })
|
|
17
17
|
]);
|
|
18
18
|
var DynamicBooleanSchema = z.union([
|
|
19
19
|
z.boolean(),
|
|
20
|
-
z.object({
|
|
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" && "
|
|
27
|
-
return getByPath(stateModel, value
|
|
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,
|
|
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 (
|
|
192
|
-
for (const key of Object.keys(
|
|
191
|
+
if (state) {
|
|
192
|
+
for (const key of Object.keys(state)) {
|
|
193
193
|
if (key === fieldName || key.endsWith(`.${fieldName}`)) {
|
|
194
|
-
return
|
|
194
|
+
return state[key];
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
557
|
+
var numericOrStateRef = z2.union([
|
|
323
558
|
z2.number(),
|
|
324
|
-
z2.object({
|
|
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
|
|
587
|
+
var VisibilityConditionSchema = z2.lazy(
|
|
327
588
|
() => z2.union([
|
|
328
|
-
z2.
|
|
329
|
-
|
|
330
|
-
z2.
|
|
331
|
-
z2.object({
|
|
332
|
-
z2.object({
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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 (
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
|
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 (
|
|
451
|
-
|
|
452
|
-
return Boolean(value);
|
|
659
|
+
if (Array.isArray(condition)) {
|
|
660
|
+
return condition.every((c) => evaluateCondition(c, ctx));
|
|
453
661
|
}
|
|
454
|
-
if (
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
|
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
|
|
474
|
-
|
|
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: (
|
|
489
|
-
|
|
680
|
+
eq: (path, value) => ({
|
|
681
|
+
$state: path,
|
|
682
|
+
eq: value
|
|
490
683
|
}),
|
|
491
684
|
/** Not equal check */
|
|
492
|
-
neq: (
|
|
493
|
-
|
|
685
|
+
neq: (path, value) => ({
|
|
686
|
+
$state: path,
|
|
687
|
+
neq: value
|
|
494
688
|
}),
|
|
495
689
|
/** Greater than */
|
|
496
|
-
gt: (
|
|
690
|
+
gt: (path, value) => ({
|
|
691
|
+
$state: path,
|
|
692
|
+
gt: value
|
|
693
|
+
}),
|
|
497
694
|
/** Greater than or equal */
|
|
498
|
-
gte: (
|
|
695
|
+
gte: (path, value) => ({
|
|
696
|
+
$state: path,
|
|
697
|
+
gte: value
|
|
698
|
+
}),
|
|
499
699
|
/** Less than */
|
|
500
|
-
lt: (
|
|
700
|
+
lt: (path, value) => ({
|
|
701
|
+
$state: path,
|
|
702
|
+
lt: value
|
|
703
|
+
}),
|
|
501
704
|
/** Less than or equal */
|
|
502
|
-
lte: (
|
|
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
|
|
507
|
-
return typeof value === "object" && value !== null && "$
|
|
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 (
|
|
517
|
-
return getByPath(ctx.stateModel, value.$
|
|
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
|
-
|
|
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:
|
|
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
|
|
764
|
-
if (!
|
|
765
|
-
console.warn(`Unknown validation function: ${check2.
|
|
1037
|
+
const validationFn = builtInValidationFunctions[check2.type] ?? customFunctions?.[check2.type];
|
|
1038
|
+
if (!validationFn) {
|
|
1039
|
+
console.warn(`Unknown validation function: ${check2.type}`);
|
|
766
1040
|
return {
|
|
767
|
-
|
|
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 =
|
|
1047
|
+
const valid = validationFn(value, resolvedArgs);
|
|
774
1048
|
return {
|
|
775
|
-
|
|
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 =
|
|
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
|
-
|
|
1082
|
+
type: "required",
|
|
810
1083
|
message
|
|
811
1084
|
}),
|
|
812
1085
|
email: (message = "Invalid email address") => ({
|
|
813
|
-
|
|
1086
|
+
type: "email",
|
|
814
1087
|
message
|
|
815
1088
|
}),
|
|
816
1089
|
minLength: (min, message) => ({
|
|
817
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1100
|
+
type: "pattern",
|
|
828
1101
|
args: { pattern },
|
|
829
1102
|
message
|
|
830
1103
|
}),
|
|
831
1104
|
min: (min, message) => ({
|
|
832
|
-
|
|
1105
|
+
type: "min",
|
|
833
1106
|
args: { min },
|
|
834
1107
|
message: message ?? `Must be at least ${min}`
|
|
835
1108
|
}),
|
|
836
1109
|
max: (max, message) => ({
|
|
837
|
-
|
|
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
|
-
|
|
1115
|
+
type: "url",
|
|
843
1116
|
message
|
|
844
1117
|
}),
|
|
845
1118
|
matches: (otherPath, message = "Fields must match") => ({
|
|
846
|
-
|
|
847
|
-
args: { other: {
|
|
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
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
{
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
|
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 $
|
|
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 { "$
|
|
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": { "
|
|
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. "
|
|
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
|
-
|
|
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
|
|
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: {
|
|
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 { "
|
|
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": { "
|
|
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: {
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
'
|
|
1669
|
+
'- `{ "$state": "/path", "neq": "value" }` - visible when state does not equal value'
|
|
1320
1670
|
);
|
|
1321
1671
|
lines.push(
|
|
1322
|
-
'- `{ "
|
|
1672
|
+
'- `{ "$state": "/path", "gt": N }` / `gte` / `lt` / `lte` - numeric comparisons'
|
|
1323
1673
|
);
|
|
1324
1674
|
lines.push(
|
|
1325
|
-
|
|
1675
|
+
"- Use ONE operator per condition (eq, neq, gt, gte, lt, lte). Do not combine multiple operators."
|
|
1326
1676
|
);
|
|
1327
|
-
lines.push('- `
|
|
1677
|
+
lines.push('- Any condition can add `"not": true` to invert its result');
|
|
1328
1678
|
lines.push(
|
|
1329
|
-
|
|
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
|
|
1690
|
+
"Use a component with on.press bound to setState to update state and drive visibility."
|
|
1335
1691
|
);
|
|
1336
1692
|
lines.push(
|
|
1337
|
-
|
|
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.
|
|
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.
|
|
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": { "$
|
|
1709
|
+
' Example: `"color": { "$state": "/theme/primary" }` reads the color from state.'
|
|
1350
1710
|
);
|
|
1351
1711
|
lines.push("");
|
|
1352
1712
|
lines.push(
|
|
1353
|
-
'2.
|
|
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
|
-
'
|
|
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: `"
|
|
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 $
|
|
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
|
-
|
|
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
|
-
|
|
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,
|