@velumo/core 0.1.0-beta.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.
Files changed (53) hide show
  1. package/dist/background-validation.d.ts +3 -0
  2. package/dist/background-validation.d.ts.map +1 -0
  3. package/dist/background-validation.js +177 -0
  4. package/dist/background-validation.js.map +1 -0
  5. package/dist/builders.d.ts +134 -0
  6. package/dist/builders.d.ts.map +1 -0
  7. package/dist/builders.js +332 -0
  8. package/dist/builders.js.map +1 -0
  9. package/dist/capabilities.d.ts +7 -0
  10. package/dist/capabilities.d.ts.map +1 -0
  11. package/dist/capabilities.js +59 -0
  12. package/dist/capabilities.js.map +1 -0
  13. package/dist/index.d.ts +9 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +7 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/layout-validation.d.ts +4 -0
  18. package/dist/layout-validation.d.ts.map +1 -0
  19. package/dist/layout-validation.js +745 -0
  20. package/dist/layout-validation.js.map +1 -0
  21. package/dist/manifest-schema.d.ts +1328 -0
  22. package/dist/manifest-schema.d.ts.map +1 -0
  23. package/dist/manifest-schema.js +774 -0
  24. package/dist/manifest-schema.js.map +1 -0
  25. package/dist/theme-validation.d.ts +3 -0
  26. package/dist/theme-validation.d.ts.map +1 -0
  27. package/dist/theme-validation.js +239 -0
  28. package/dist/theme-validation.js.map +1 -0
  29. package/dist/tsconfig.tsbuildinfo +1 -0
  30. package/dist/types.d.ts +398 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/types.js +2 -0
  33. package/dist/types.js.map +1 -0
  34. package/dist/validation-issue.d.ts +18 -0
  35. package/dist/validation-issue.d.ts.map +1 -0
  36. package/dist/validation-issue.js +30 -0
  37. package/dist/validation-issue.js.map +1 -0
  38. package/dist/validation.d.ts +4 -0
  39. package/dist/validation.d.ts.map +1 -0
  40. package/dist/validation.js +274 -0
  41. package/dist/validation.js.map +1 -0
  42. package/package.json +16 -0
  43. package/src/background-validation.ts +315 -0
  44. package/src/builders.ts +626 -0
  45. package/src/capabilities.ts +99 -0
  46. package/src/index.ts +147 -0
  47. package/src/layout-validation.ts +1385 -0
  48. package/src/manifest-schema.ts +823 -0
  49. package/src/theme-validation.ts +410 -0
  50. package/src/types.ts +560 -0
  51. package/src/validation-issue.ts +43 -0
  52. package/src/validation.ts +468 -0
  53. package/tsconfig.json +9 -0
@@ -0,0 +1,1385 @@
1
+ import type { ValidationIssue } from "./types.js";
2
+ import { describeType, issue } from "./validation-issue.js";
3
+
4
+ type IndexedRecord = Record<string, unknown>;
5
+
6
+ const FORBIDDEN_RENDERER_KEYS = new Set(["style", "className", "element"]);
7
+ const RENDERER_EVENT_HANDLER_PATTERN = /^on[A-Z]/;
8
+ const STACK_KEYS = new Set([
9
+ "kind",
10
+ "children",
11
+ "direction",
12
+ "align",
13
+ "gap",
14
+ "justify",
15
+ "meta",
16
+ ]);
17
+ const GRID_KEYS = new Set(["kind", "children", "columns", "rows", "meta"]);
18
+ const SPLIT_KEYS = new Set(["kind", "children", "weights", "meta"]);
19
+ const HERO_KEYS = new Set(["kind", "children", "meta"]);
20
+ const FRAME_KEYS = new Set(["kind", "children", "meta", "tone"]);
21
+ const HEADING_KEYS = new Set([
22
+ "kind",
23
+ "text",
24
+ "size",
25
+ "level",
26
+ "transform",
27
+ "decoration",
28
+ "glow",
29
+ "tone",
30
+ ]);
31
+ const HEADING_SIZE_VALUES = ["default", "display", "hero"] as const;
32
+ const TEXT_KEYS = new Set([
33
+ "kind",
34
+ "text",
35
+ "variant",
36
+ "transform",
37
+ "decoration",
38
+ "glow",
39
+ "tone",
40
+ ]);
41
+ const MEDIA_KEYS = new Set(["kind", "src", "alt", "fit"]);
42
+ const CALLOUT_KEYS = new Set(["kind", "children", "tone"]);
43
+ const LAYOUT_META_KEYS = new Set(["safeArea", "depth", "cameraHint"]);
44
+ const SAFE_AREA_VALUES = ["default", "full-bleed", "title-safe"] as const;
45
+ const CAMERA_HINT_VALUES = ["static", "push", "pan", "orbit"] as const;
46
+ const STACK_DIRECTION_VALUES = ["vertical", "horizontal"] as const;
47
+ const STACK_ALIGN_VALUES = ["start", "center", "end", "stretch"] as const;
48
+ const STACK_JUSTIFY_VALUES = ["start", "center", "between", "end"] as const;
49
+ const TEXT_VARIANT_VALUES = ["body", "lead", "caption"] as const;
50
+ const TEXT_TRANSFORM_VALUES = ["uppercase", "lowercase", "capitalize"] as const;
51
+ const TEXT_DECORATION_VALUES = ["underline", "line-through"] as const;
52
+ const TEXT_TONE_VALUES = [
53
+ "default",
54
+ "muted",
55
+ "accent",
56
+ "danger",
57
+ "magenta",
58
+ "cyan",
59
+ "purple",
60
+ "green",
61
+ ] as const;
62
+ const MEDIA_FIT_VALUES = ["contain", "cover", "fill"] as const;
63
+ const CALLOUT_TONE_VALUES = [
64
+ "info",
65
+ "warning",
66
+ "success",
67
+ "danger",
68
+ "quote",
69
+ ] as const;
70
+ const CANVAS_KEYS = new Set(["kind", "items", "meta"]);
71
+ const CANVAS_ITEM_KEYS = new Set(["x", "y", "width", "height", "child"]);
72
+ export const ANIMATION_NAME_VALUES = [
73
+ "rise",
74
+ "fade",
75
+ "zoom",
76
+ "draw",
77
+ "in-left",
78
+ "in-right",
79
+ "glow",
80
+ "blink",
81
+ "pulse",
82
+ "sweep",
83
+ ] as const;
84
+ const ANIMATE_KEYS = new Set([
85
+ "kind",
86
+ "name",
87
+ "delay",
88
+ "duration",
89
+ "easing",
90
+ "child",
91
+ ]);
92
+ const CODE_TOKEN_KIND_VALUES = [
93
+ "keyword",
94
+ "function",
95
+ "string",
96
+ "number",
97
+ "property",
98
+ "comment",
99
+ "punctuation",
100
+ "plain",
101
+ ] as const;
102
+ const CODE_KEYS = new Set(["kind", "lines"]);
103
+ const CODE_TOKEN_KEYS = new Set(["text", "kind"]);
104
+ const ICON_KEYS = new Set(["kind", "name", "tone", "label"]);
105
+ const ICON_NAME_VALUES = [
106
+ "check",
107
+ "cross",
108
+ "dash",
109
+ "ring",
110
+ "dot",
111
+ "arrow",
112
+ ] as const;
113
+ const TABLE_KEYS = new Set(["kind", "columns", "rows", "meta"]);
114
+ const TABLE_COLUMN_KEYS = new Set(["header", "weight", "align", "emphasis"]);
115
+ const TABLE_ALIGN_VALUES = ["start", "center", "end"] as const;
116
+ const CONNECTOR_KEYS = new Set([
117
+ "kind",
118
+ "from",
119
+ "to",
120
+ "control",
121
+ "arrow",
122
+ "dashed",
123
+ "tone",
124
+ ]);
125
+ const CONNECTOR_POINT_KEYS = new Set(["x", "y"]);
126
+ const CONNECTOR_ARROW_VALUES = ["none", "end", "start", "both"] as const;
127
+ const BADGE_KEYS = new Set(["kind", "text", "tone", "variant", "dot", "mono"]);
128
+ const BADGE_VARIANT_VALUES = ["outline", "soft", "solid"] as const;
129
+ const RULE_KEYS = new Set([
130
+ "kind",
131
+ "length",
132
+ "thickness",
133
+ "tone",
134
+ "gradient",
135
+ "radius",
136
+ ]);
137
+ const LAYOUT_KINDS =
138
+ "stack, grid, split, hero, frame, canvas, animate, heading, text, media, callout, code, icon, table, connector, badge, rule";
139
+
140
+ function isRecord(value: unknown): value is IndexedRecord {
141
+ return typeof value === "object" && value !== null && !Array.isArray(value);
142
+ }
143
+
144
+ function isNonEmptyString(value: unknown): value is string {
145
+ return typeof value === "string" && value.trim().length > 0;
146
+ }
147
+
148
+ function isPositiveInteger(value: unknown): value is number {
149
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
150
+ }
151
+
152
+ function isPercent(value: unknown): value is number {
153
+ return (
154
+ typeof value === "number" &&
155
+ Number.isFinite(value) &&
156
+ value >= 0 &&
157
+ value <= 100
158
+ );
159
+ }
160
+
161
+ function isAllowedString<TValue extends string>(
162
+ value: unknown,
163
+ allowed: readonly TValue[],
164
+ ): value is TValue {
165
+ return typeof value === "string" && allowed.includes(value as TValue);
166
+ }
167
+
168
+ function requiredIssue(path: string, message: string): ValidationIssue {
169
+ return issue(path, "required", message, {
170
+ suggestion: `Provide a value for \`${path}\`.`,
171
+ });
172
+ }
173
+
174
+ function typeIssue(
175
+ path: string,
176
+ message: string,
177
+ expected: string,
178
+ received: unknown,
179
+ ): ValidationIssue {
180
+ return issue(path, "wrong_type", message, {
181
+ expected,
182
+ received: describeType(received),
183
+ });
184
+ }
185
+
186
+ function enumIssue(
187
+ path: string,
188
+ message: string,
189
+ allowed: readonly string[],
190
+ received: unknown,
191
+ ): ValidationIssue {
192
+ return issue(path, "invalid_enum", message, {
193
+ expected: allowed.join(", "),
194
+ received: typeof received === "string" ? received : describeType(received),
195
+ suggestion: `Use one of: ${allowed.join(", ")}.`,
196
+ });
197
+ }
198
+
199
+ function valueIssue(
200
+ path: string,
201
+ message: string,
202
+ received: unknown,
203
+ ): ValidationIssue {
204
+ return issue(path, "invalid_value", message, {
205
+ received:
206
+ typeof received === "number" ? String(received) : describeType(received),
207
+ });
208
+ }
209
+
210
+ function validateSerializableValue(
211
+ value: unknown,
212
+ path: string,
213
+ ): readonly ValidationIssue[] {
214
+ const issues: ValidationIssue[] = [];
215
+
216
+ if (typeof value === "function") {
217
+ return [
218
+ issue(path, "not_serializable", `${path} must be JSON-serializable`, {
219
+ received: "function",
220
+ suggestion: "Remove functions from manifest data; it must be JSON.",
221
+ }),
222
+ ];
223
+ }
224
+
225
+ if (value === undefined) {
226
+ return [
227
+ issue(path, "not_serializable", `${path} must not be undefined`, {
228
+ received: "undefined",
229
+ }),
230
+ ];
231
+ }
232
+
233
+ if (typeof value === "number" && !Number.isFinite(value)) {
234
+ return [
235
+ issue(path, "not_serializable", `${path} must be a finite JSON number`, {
236
+ received: String(value),
237
+ }),
238
+ ];
239
+ }
240
+
241
+ if (Array.isArray(value)) {
242
+ value.forEach((item, index) => {
243
+ issues.push(...validateSerializableValue(item, `${path}[${index}]`));
244
+ });
245
+ return issues;
246
+ }
247
+
248
+ if (isRecord(value)) {
249
+ for (const [key, child] of Object.entries(value)) {
250
+ issues.push(...validateSerializableValue(child, `${path}.${key}`));
251
+ }
252
+ }
253
+
254
+ return issues;
255
+ }
256
+
257
+ function validateRendererNeutralFields(
258
+ node: IndexedRecord,
259
+ path: string,
260
+ ): readonly ValidationIssue[] {
261
+ const issues: ValidationIssue[] = [];
262
+
263
+ for (const key of Object.keys(node)) {
264
+ if (
265
+ FORBIDDEN_RENDERER_KEYS.has(key) ||
266
+ RENDERER_EVENT_HANDLER_PATTERN.test(key)
267
+ ) {
268
+ issues.push(
269
+ issue(
270
+ `${path}.${key}`,
271
+ "renderer_specific",
272
+ `${path}.${key} is renderer-specific and not allowed`,
273
+ {
274
+ suggestion:
275
+ "Keep manifest data renderer-neutral; move renderer concerns into the renderer.",
276
+ },
277
+ ),
278
+ );
279
+ }
280
+ }
281
+
282
+ issues.push(...validateSerializableValue(node, path));
283
+
284
+ return issues;
285
+ }
286
+
287
+ function validateLayoutMeta(
288
+ value: unknown,
289
+ path: string,
290
+ ): readonly ValidationIssue[] {
291
+ const issues: ValidationIssue[] = [];
292
+
293
+ if (value === undefined) {
294
+ return issues;
295
+ }
296
+
297
+ if (!isRecord(value)) {
298
+ return [typeIssue(path, `${path} must be an object`, "object", value)];
299
+ }
300
+
301
+ for (const key of Object.keys(value)) {
302
+ if (!LAYOUT_META_KEYS.has(key)) {
303
+ issues.push(
304
+ issue(
305
+ `${path}.${key}`,
306
+ "unknown_field",
307
+ `${path}.${key} is not allowed`,
308
+ {
309
+ suggestion: `Remove \`${key}\`; allowed meta fields are safeArea, depth, cameraHint.`,
310
+ },
311
+ ),
312
+ );
313
+ }
314
+ }
315
+
316
+ if (
317
+ value.safeArea !== undefined &&
318
+ !isAllowedString(value.safeArea, SAFE_AREA_VALUES)
319
+ ) {
320
+ issues.push(
321
+ enumIssue(
322
+ `${path}.safeArea`,
323
+ `${path}.safeArea must be default, full-bleed, or title-safe`,
324
+ SAFE_AREA_VALUES,
325
+ value.safeArea,
326
+ ),
327
+ );
328
+ }
329
+
330
+ if (
331
+ value.depth !== undefined &&
332
+ (typeof value.depth !== "number" || !Number.isFinite(value.depth))
333
+ ) {
334
+ issues.push(
335
+ valueIssue(
336
+ `${path}.depth`,
337
+ `${path}.depth must be a finite JSON number`,
338
+ value.depth,
339
+ ),
340
+ );
341
+ }
342
+
343
+ if (
344
+ value.cameraHint !== undefined &&
345
+ !isAllowedString(value.cameraHint, CAMERA_HINT_VALUES)
346
+ ) {
347
+ issues.push(
348
+ enumIssue(
349
+ `${path}.cameraHint`,
350
+ `${path}.cameraHint must be static, push, pan, or orbit`,
351
+ CAMERA_HINT_VALUES,
352
+ value.cameraHint,
353
+ ),
354
+ );
355
+ }
356
+
357
+ return issues;
358
+ }
359
+
360
+ function validateLayoutChildren(
361
+ value: unknown,
362
+ path: string,
363
+ ): readonly ValidationIssue[] {
364
+ const issues: ValidationIssue[] = [];
365
+
366
+ if (!Array.isArray(value)) {
367
+ return [typeIssue(path, `${path} must be an array`, "array", value)];
368
+ }
369
+
370
+ value.forEach((child, index) => {
371
+ issues.push(...validateLayoutNode(child, `${path}[${index}]`));
372
+ });
373
+
374
+ return issues;
375
+ }
376
+
377
+ function validateKnownFields(
378
+ node: IndexedRecord,
379
+ allowedKeys: ReadonlySet<string>,
380
+ path: string,
381
+ ): readonly ValidationIssue[] {
382
+ return Object.keys(node)
383
+ .filter((key) => !allowedKeys.has(key))
384
+ .map((key) =>
385
+ issue(
386
+ `${path}.${key}`,
387
+ "unknown_field",
388
+ `${path}.${key} is not allowed`,
389
+ {
390
+ expected: [...allowedKeys].join(", "),
391
+ suggestion: `Remove \`${key}\`; it is not a recognized field here.`,
392
+ },
393
+ ),
394
+ );
395
+ }
396
+
397
+ function validateConnectorPoint(
398
+ value: unknown,
399
+ path: string,
400
+ ): readonly ValidationIssue[] {
401
+ if (!isRecord(value)) {
402
+ return [
403
+ typeIssue(path, `${path} must be an object {x, y}`, "object", value),
404
+ ];
405
+ }
406
+ const issues: ValidationIssue[] = [];
407
+ for (const axis of ["x", "y"] as const) {
408
+ if (value[axis] === undefined) {
409
+ issues.push(
410
+ requiredIssue(`${path}.${axis}`, `${path}.${axis} is required`),
411
+ );
412
+ } else if (!isPercent(value[axis])) {
413
+ issues.push(
414
+ valueIssue(
415
+ `${path}.${axis}`,
416
+ `${path}.${axis} must be a percentage 0-100`,
417
+ value[axis],
418
+ ),
419
+ );
420
+ }
421
+ }
422
+ issues.push(...validateKnownFields(value, CONNECTOR_POINT_KEYS, path));
423
+ return issues;
424
+ }
425
+
426
+ function validateCanvasItems(
427
+ value: unknown,
428
+ path: string,
429
+ ): readonly ValidationIssue[] {
430
+ if (!Array.isArray(value)) {
431
+ if (value === undefined) {
432
+ return [requiredIssue(path, `${path} is required`)];
433
+ }
434
+ return [typeIssue(path, `${path} must be an array`, "array", value)];
435
+ }
436
+
437
+ const issues: ValidationIssue[] = [];
438
+ value.forEach((item, index) => {
439
+ const itemPath = `${path}[${index}]`;
440
+ if (!isRecord(item)) {
441
+ issues.push(
442
+ typeIssue(itemPath, `${itemPath} must be an object`, "object", item),
443
+ );
444
+ return;
445
+ }
446
+
447
+ issues.push(...validateKnownFields(item, CANVAS_ITEM_KEYS, itemPath));
448
+
449
+ for (const axis of ["x", "y"] as const) {
450
+ if (item[axis] === undefined) {
451
+ issues.push(
452
+ requiredIssue(
453
+ `${itemPath}.${axis}`,
454
+ `${itemPath}.${axis} is required`,
455
+ ),
456
+ );
457
+ } else if (!isPercent(item[axis])) {
458
+ issues.push(
459
+ valueIssue(
460
+ `${itemPath}.${axis}`,
461
+ `${itemPath}.${axis} must be a percentage 0-100`,
462
+ item[axis],
463
+ ),
464
+ );
465
+ }
466
+ }
467
+
468
+ for (const dim of ["width", "height"] as const) {
469
+ if (item[dim] !== undefined && !isPercent(item[dim])) {
470
+ issues.push(
471
+ valueIssue(
472
+ `${itemPath}.${dim}`,
473
+ `${itemPath}.${dim} must be a percentage 0-100`,
474
+ item[dim],
475
+ ),
476
+ );
477
+ }
478
+ }
479
+
480
+ if (item.child === undefined) {
481
+ issues.push(
482
+ requiredIssue(`${itemPath}.child`, `${itemPath}.child is required`),
483
+ );
484
+ } else {
485
+ issues.push(...validateLayoutNode(item.child, `${itemPath}.child`));
486
+ }
487
+ });
488
+ return issues;
489
+ }
490
+
491
+ function validateAnimate(
492
+ node: IndexedRecord,
493
+ path: string,
494
+ ): readonly ValidationIssue[] {
495
+ const issues: ValidationIssue[] = [];
496
+ issues.push(...validateKnownFields(node, ANIMATE_KEYS, path));
497
+
498
+ if (node.name === undefined) {
499
+ issues.push(requiredIssue(`${path}.name`, `${path}.name is required`));
500
+ } else if (!isAllowedString(node.name, ANIMATION_NAME_VALUES)) {
501
+ issues.push(
502
+ enumIssue(
503
+ `${path}.name`,
504
+ `${path}.animate.name must be one of: ${ANIMATION_NAME_VALUES.join(", ")}`,
505
+ ANIMATION_NAME_VALUES,
506
+ node.name,
507
+ ),
508
+ );
509
+ }
510
+
511
+ if (
512
+ node.delay !== undefined &&
513
+ !(
514
+ typeof node.delay === "number" &&
515
+ Number.isFinite(node.delay) &&
516
+ node.delay >= 0
517
+ )
518
+ ) {
519
+ issues.push(
520
+ valueIssue(
521
+ `${path}.delay`,
522
+ `${path}.animate.delay must be a number >= 0`,
523
+ node.delay,
524
+ ),
525
+ );
526
+ }
527
+
528
+ if (
529
+ node.duration !== undefined &&
530
+ !(
531
+ typeof node.duration === "number" &&
532
+ Number.isFinite(node.duration) &&
533
+ node.duration > 0
534
+ )
535
+ ) {
536
+ issues.push(
537
+ valueIssue(
538
+ `${path}.duration`,
539
+ `${path}.animate.duration must be a number > 0`,
540
+ node.duration,
541
+ ),
542
+ );
543
+ }
544
+
545
+ if (node.easing !== undefined) {
546
+ if (typeof node.easing !== "string") {
547
+ issues.push(
548
+ typeIssue(
549
+ `${path}.easing`,
550
+ `${path}.animate.easing must be a string`,
551
+ "string",
552
+ node.easing,
553
+ ),
554
+ );
555
+ } else if (node.easing.trim().length === 0) {
556
+ issues.push(
557
+ requiredIssue(
558
+ `${path}.easing`,
559
+ `${path}.animate.easing must not be empty`,
560
+ ),
561
+ );
562
+ }
563
+ }
564
+
565
+ if (node.child === undefined) {
566
+ issues.push(
567
+ requiredIssue(`${path}.child`, `${path}.animate.child is required`),
568
+ );
569
+ } else {
570
+ issues.push(...validateLayoutNode(node.child, `${path}.child`));
571
+ }
572
+
573
+ return issues;
574
+ }
575
+
576
+ function validateTextStyle(
577
+ node: IndexedRecord,
578
+ path: string,
579
+ ): readonly ValidationIssue[] {
580
+ const issues: ValidationIssue[] = [];
581
+
582
+ if (
583
+ node.transform !== undefined &&
584
+ !isAllowedString(node.transform, TEXT_TRANSFORM_VALUES)
585
+ ) {
586
+ issues.push(
587
+ enumIssue(
588
+ `${path}.transform`,
589
+ `${path}.transform must be uppercase, lowercase, or capitalize`,
590
+ TEXT_TRANSFORM_VALUES,
591
+ node.transform,
592
+ ),
593
+ );
594
+ }
595
+ if (
596
+ node.decoration !== undefined &&
597
+ !isAllowedString(node.decoration, TEXT_DECORATION_VALUES)
598
+ ) {
599
+ issues.push(
600
+ enumIssue(
601
+ `${path}.decoration`,
602
+ `${path}.decoration must be underline or line-through`,
603
+ TEXT_DECORATION_VALUES,
604
+ node.decoration,
605
+ ),
606
+ );
607
+ }
608
+ if (node.glow !== undefined && typeof node.glow !== "boolean") {
609
+ issues.push(
610
+ typeIssue(
611
+ `${path}.glow`,
612
+ `${path}.glow must be a boolean`,
613
+ "boolean",
614
+ node.glow,
615
+ ),
616
+ );
617
+ }
618
+ if (
619
+ node.tone !== undefined &&
620
+ !isAllowedString(node.tone, TEXT_TONE_VALUES)
621
+ ) {
622
+ issues.push(
623
+ enumIssue(
624
+ `${path}.tone`,
625
+ `${path}.tone must be default, muted, accent, or danger`,
626
+ TEXT_TONE_VALUES,
627
+ node.tone,
628
+ ),
629
+ );
630
+ }
631
+ return issues;
632
+ }
633
+
634
+ function validateCodeToken(
635
+ value: unknown,
636
+ path: string,
637
+ ): readonly ValidationIssue[] {
638
+ if (!isRecord(value)) {
639
+ return [typeIssue(path, `${path} must be an object`, "object", value)];
640
+ }
641
+ const issues: ValidationIssue[] = [];
642
+ issues.push(...validateKnownFields(value, CODE_TOKEN_KEYS, path));
643
+ if (typeof value.text !== "string") {
644
+ issues.push(
645
+ value.text === undefined
646
+ ? requiredIssue(`${path}.text`, `${path}.text is required`)
647
+ : typeIssue(
648
+ `${path}.text`,
649
+ `${path}.text must be a string`,
650
+ "string",
651
+ value.text,
652
+ ),
653
+ );
654
+ }
655
+ if (
656
+ value.kind !== undefined &&
657
+ !isAllowedString(value.kind, CODE_TOKEN_KIND_VALUES)
658
+ ) {
659
+ issues.push(
660
+ enumIssue(
661
+ `${path}.kind`,
662
+ `${path}.code token kind must be a known token kind`,
663
+ CODE_TOKEN_KIND_VALUES,
664
+ value.kind,
665
+ ),
666
+ );
667
+ }
668
+ return issues;
669
+ }
670
+
671
+ function validateCodeLines(
672
+ value: unknown,
673
+ path: string,
674
+ ): readonly ValidationIssue[] {
675
+ if (!Array.isArray(value)) {
676
+ if (value === undefined) {
677
+ return [requiredIssue(path, `${path} is required`)];
678
+ }
679
+ return [typeIssue(path, `${path} must be an array`, "array", value)];
680
+ }
681
+ const issues: ValidationIssue[] = [];
682
+ value.forEach((line, lineIndex) => {
683
+ const linePath = `${path}[${lineIndex}]`;
684
+ if (!Array.isArray(line)) {
685
+ issues.push(
686
+ typeIssue(linePath, `${linePath} must be an array`, "array", line),
687
+ );
688
+ return;
689
+ }
690
+ line.forEach((tok, tokenIndex) => {
691
+ issues.push(...validateCodeToken(tok, `${linePath}[${tokenIndex}]`));
692
+ });
693
+ });
694
+ return issues;
695
+ }
696
+
697
+ function validateTableColumns(
698
+ value: unknown,
699
+ path: string,
700
+ ): readonly ValidationIssue[] {
701
+ if (!Array.isArray(value)) {
702
+ return [typeIssue(path, `${path} must be an array`, "array", value)];
703
+ }
704
+ if (value.length === 0) {
705
+ return [
706
+ issue(path, "constraint", `${path} must contain at least one column`, {
707
+ suggestion: "Add a column, or use a different layout.",
708
+ }),
709
+ ];
710
+ }
711
+ const issues: ValidationIssue[] = [];
712
+ value.forEach((column, index) => {
713
+ const cPath = `${path}[${index}]`;
714
+ if (!isRecord(column)) {
715
+ issues.push(
716
+ typeIssue(cPath, `${cPath} must be an object`, "object", column),
717
+ );
718
+ return;
719
+ }
720
+ issues.push(...validateKnownFields(column, TABLE_COLUMN_KEYS, cPath));
721
+ if (column.header !== undefined) {
722
+ issues.push(...validateLayoutNode(column.header, `${cPath}.header`));
723
+ }
724
+ if (
725
+ column.weight !== undefined &&
726
+ (typeof column.weight !== "number" ||
727
+ !Number.isFinite(column.weight) ||
728
+ column.weight <= 0)
729
+ ) {
730
+ issues.push(
731
+ valueIssue(
732
+ `${cPath}.weight`,
733
+ `${cPath}.weight must be a number > 0`,
734
+ column.weight,
735
+ ),
736
+ );
737
+ }
738
+ if (
739
+ column.align !== undefined &&
740
+ !isAllowedString(column.align, TABLE_ALIGN_VALUES)
741
+ ) {
742
+ issues.push(
743
+ enumIssue(
744
+ `${cPath}.align`,
745
+ `${cPath}.align must be start, center, or end`,
746
+ TABLE_ALIGN_VALUES,
747
+ column.align,
748
+ ),
749
+ );
750
+ }
751
+ if (column.emphasis !== undefined && typeof column.emphasis !== "boolean") {
752
+ issues.push(
753
+ typeIssue(
754
+ `${cPath}.emphasis`,
755
+ `${cPath}.emphasis must be a boolean`,
756
+ "boolean",
757
+ column.emphasis,
758
+ ),
759
+ );
760
+ }
761
+ });
762
+ return issues;
763
+ }
764
+
765
+ function validateTableRows(
766
+ value: unknown,
767
+ columnCount: number,
768
+ path: string,
769
+ ): readonly ValidationIssue[] {
770
+ if (!Array.isArray(value)) {
771
+ return [typeIssue(path, `${path} must be an array`, "array", value)];
772
+ }
773
+ const issues: ValidationIssue[] = [];
774
+ value.forEach((row, rowIndex) => {
775
+ const rPath = `${path}[${rowIndex}]`;
776
+ if (!Array.isArray(row)) {
777
+ issues.push(
778
+ typeIssue(rPath, `${rPath} must be an array of cells`, "array", row),
779
+ );
780
+ return;
781
+ }
782
+ if (row.length !== columnCount) {
783
+ issues.push(
784
+ issue(
785
+ rPath,
786
+ "constraint",
787
+ `${rPath} must have ${columnCount} cells to match the columns; got ${row.length}`,
788
+ { suggestion: `Provide exactly ${columnCount} cells per row.` },
789
+ ),
790
+ );
791
+ }
792
+ row.forEach((cell, cellIndex) => {
793
+ issues.push(...validateLayoutNode(cell, `${rPath}[${cellIndex}]`));
794
+ });
795
+ });
796
+ return issues;
797
+ }
798
+
799
+ export function validateLayoutNode(
800
+ node: unknown,
801
+ path: string,
802
+ ): readonly ValidationIssue[] {
803
+ const issues: ValidationIssue[] = [];
804
+
805
+ if (!isRecord(node)) {
806
+ return [typeIssue(path, `${path} must be an object`, "object", node)];
807
+ }
808
+
809
+ issues.push(...validateRendererNeutralFields(node, path));
810
+
811
+ switch (node.kind) {
812
+ case "stack":
813
+ issues.push(...validateKnownFields(node, STACK_KEYS, path));
814
+ issues.push(...validateLayoutChildren(node.children, `${path}.children`));
815
+ if (
816
+ node.direction !== undefined &&
817
+ !isAllowedString(node.direction, STACK_DIRECTION_VALUES)
818
+ ) {
819
+ issues.push(
820
+ enumIssue(
821
+ `${path}.direction`,
822
+ `${path}.stack.direction must be vertical or horizontal`,
823
+ STACK_DIRECTION_VALUES,
824
+ node.direction,
825
+ ),
826
+ );
827
+ }
828
+ if (
829
+ node.align !== undefined &&
830
+ !isAllowedString(node.align, STACK_ALIGN_VALUES)
831
+ ) {
832
+ issues.push(
833
+ enumIssue(
834
+ `${path}.align`,
835
+ `${path}.stack.align must be start, center, end, or stretch`,
836
+ STACK_ALIGN_VALUES,
837
+ node.align,
838
+ ),
839
+ );
840
+ }
841
+ if (
842
+ node.justify !== undefined &&
843
+ !isAllowedString(node.justify, STACK_JUSTIFY_VALUES)
844
+ ) {
845
+ issues.push(
846
+ enumIssue(
847
+ `${path}.justify`,
848
+ `${path}.stack.justify must be start, center, between, or end`,
849
+ STACK_JUSTIFY_VALUES,
850
+ node.justify,
851
+ ),
852
+ );
853
+ }
854
+ if (node.gap !== undefined && typeof node.gap !== "string") {
855
+ issues.push(
856
+ typeIssue(
857
+ `${path}.gap`,
858
+ `${path}.stack.gap must be a string`,
859
+ "string",
860
+ node.gap,
861
+ ),
862
+ );
863
+ }
864
+ issues.push(...validateLayoutMeta(node.meta, `${path}.meta`));
865
+ break;
866
+
867
+ case "grid":
868
+ issues.push(...validateKnownFields(node, GRID_KEYS, path));
869
+ issues.push(...validateLayoutChildren(node.children, `${path}.children`));
870
+ if (node.columns !== undefined && !isPositiveInteger(node.columns)) {
871
+ issues.push(
872
+ valueIssue(
873
+ `${path}.columns`,
874
+ `${path}.grid.columns must be a positive integer`,
875
+ node.columns,
876
+ ),
877
+ );
878
+ }
879
+ if (node.rows !== undefined && !isPositiveInteger(node.rows)) {
880
+ issues.push(
881
+ valueIssue(
882
+ `${path}.rows`,
883
+ `${path}.grid.rows must be a positive integer`,
884
+ node.rows,
885
+ ),
886
+ );
887
+ }
888
+ issues.push(...validateLayoutMeta(node.meta, `${path}.meta`));
889
+ break;
890
+
891
+ case "split":
892
+ issues.push(...validateKnownFields(node, SPLIT_KEYS, path));
893
+ issues.push(...validateLayoutChildren(node.children, `${path}.children`));
894
+ if (Array.isArray(node.children) && node.children.length < 2) {
895
+ issues.push(
896
+ issue(
897
+ `${path}.children`,
898
+ "constraint",
899
+ `${path}.split must have at least two children`,
900
+ { suggestion: "A split needs two or more children." },
901
+ ),
902
+ );
903
+ }
904
+ if (node.weights !== undefined) {
905
+ if (!Array.isArray(node.weights)) {
906
+ issues.push(
907
+ typeIssue(
908
+ `${path}.weights`,
909
+ `${path}.split.weights must be an array`,
910
+ "array",
911
+ node.weights,
912
+ ),
913
+ );
914
+ } else {
915
+ if (
916
+ Array.isArray(node.children) &&
917
+ node.weights.length !== node.children.length
918
+ ) {
919
+ issues.push(
920
+ issue(
921
+ `${path}.weights`,
922
+ "constraint",
923
+ `${path}.split.weights length must match children`,
924
+ {
925
+ suggestion:
926
+ "Provide one weight per child, or omit weights entirely.",
927
+ },
928
+ ),
929
+ );
930
+ }
931
+ node.weights.forEach((weight, index) => {
932
+ if (
933
+ typeof weight !== "number" ||
934
+ !Number.isFinite(weight) ||
935
+ weight <= 0
936
+ ) {
937
+ issues.push(
938
+ valueIssue(
939
+ `${path}.weights[${index}]`,
940
+ `${path}.split.weights[${index}] must be a positive number`,
941
+ weight,
942
+ ),
943
+ );
944
+ }
945
+ });
946
+ }
947
+ }
948
+ issues.push(...validateLayoutMeta(node.meta, `${path}.meta`));
949
+ break;
950
+
951
+ case "hero":
952
+ issues.push(...validateKnownFields(node, HERO_KEYS, path));
953
+ issues.push(...validateLayoutChildren(node.children, `${path}.children`));
954
+ if (Array.isArray(node.children) && node.children.length < 1) {
955
+ issues.push(
956
+ issue(
957
+ `${path}.children`,
958
+ "constraint",
959
+ `${path}.hero must have one or more children`,
960
+ { suggestion: "Add at least one child to the hero." },
961
+ ),
962
+ );
963
+ }
964
+ issues.push(...validateLayoutMeta(node.meta, `${path}.meta`));
965
+ break;
966
+
967
+ case "frame":
968
+ issues.push(...validateKnownFields(node, FRAME_KEYS, path));
969
+ issues.push(...validateLayoutChildren(node.children, `${path}.children`));
970
+ issues.push(...validateLayoutMeta(node.meta, `${path}.meta`));
971
+ if (
972
+ node.tone !== undefined &&
973
+ !isAllowedString(node.tone, TEXT_TONE_VALUES)
974
+ ) {
975
+ issues.push(
976
+ enumIssue(
977
+ `${path}.tone`,
978
+ `${path}.frame.tone must be a known tone`,
979
+ TEXT_TONE_VALUES,
980
+ node.tone,
981
+ ),
982
+ );
983
+ }
984
+ break;
985
+
986
+ case "heading":
987
+ issues.push(...validateKnownFields(node, HEADING_KEYS, path));
988
+ if (!isNonEmptyString(node.text)) {
989
+ issues.push(
990
+ requiredIssue(`${path}.text`, `${path}.heading.text is required`),
991
+ );
992
+ }
993
+ if (
994
+ node.size !== undefined &&
995
+ !isAllowedString(node.size, HEADING_SIZE_VALUES)
996
+ ) {
997
+ issues.push(
998
+ enumIssue(
999
+ `${path}.size`,
1000
+ `${path}.heading.size must be default, display, or hero`,
1001
+ HEADING_SIZE_VALUES,
1002
+ node.size,
1003
+ ),
1004
+ );
1005
+ }
1006
+ if (
1007
+ node.level !== undefined &&
1008
+ (typeof node.level !== "number" ||
1009
+ !Number.isInteger(node.level) ||
1010
+ node.level < 1 ||
1011
+ node.level > 6)
1012
+ ) {
1013
+ issues.push(
1014
+ valueIssue(
1015
+ `${path}.level`,
1016
+ `${path}.heading.level must be between 1 and 6`,
1017
+ node.level,
1018
+ ),
1019
+ );
1020
+ }
1021
+ issues.push(...validateTextStyle(node, path));
1022
+ break;
1023
+
1024
+ case "text":
1025
+ issues.push(...validateKnownFields(node, TEXT_KEYS, path));
1026
+ if (typeof node.text !== "string") {
1027
+ issues.push(
1028
+ requiredIssue(`${path}.text`, `${path}.text.text is required`),
1029
+ );
1030
+ }
1031
+ if (
1032
+ node.variant !== undefined &&
1033
+ !isAllowedString(node.variant, TEXT_VARIANT_VALUES)
1034
+ ) {
1035
+ issues.push(
1036
+ enumIssue(
1037
+ `${path}.variant`,
1038
+ `${path}.text.variant must be body, lead, or caption`,
1039
+ TEXT_VARIANT_VALUES,
1040
+ node.variant,
1041
+ ),
1042
+ );
1043
+ }
1044
+ issues.push(...validateTextStyle(node, path));
1045
+ break;
1046
+
1047
+ case "media":
1048
+ issues.push(...validateKnownFields(node, MEDIA_KEYS, path));
1049
+ if (!isNonEmptyString(node.src)) {
1050
+ issues.push(
1051
+ requiredIssue(`${path}.src`, `${path}.media.src is required`),
1052
+ );
1053
+ }
1054
+ if (!isNonEmptyString(node.alt)) {
1055
+ issues.push(
1056
+ requiredIssue(`${path}.alt`, `${path}.media.alt is required`),
1057
+ );
1058
+ }
1059
+ if (
1060
+ node.fit !== undefined &&
1061
+ !isAllowedString(node.fit, MEDIA_FIT_VALUES)
1062
+ ) {
1063
+ issues.push(
1064
+ enumIssue(
1065
+ `${path}.fit`,
1066
+ `${path}.media.fit must be contain, cover, or fill`,
1067
+ MEDIA_FIT_VALUES,
1068
+ node.fit,
1069
+ ),
1070
+ );
1071
+ }
1072
+ break;
1073
+
1074
+ case "callout":
1075
+ issues.push(...validateKnownFields(node, CALLOUT_KEYS, path));
1076
+ issues.push(...validateLayoutChildren(node.children, `${path}.children`));
1077
+ if (
1078
+ node.tone !== undefined &&
1079
+ !isAllowedString(node.tone, CALLOUT_TONE_VALUES)
1080
+ ) {
1081
+ issues.push(
1082
+ enumIssue(
1083
+ `${path}.tone`,
1084
+ `${path}.callout.tone must be info, warning, success, danger, or quote`,
1085
+ CALLOUT_TONE_VALUES,
1086
+ node.tone,
1087
+ ),
1088
+ );
1089
+ }
1090
+ break;
1091
+
1092
+ case "canvas":
1093
+ issues.push(...validateKnownFields(node, CANVAS_KEYS, path));
1094
+ issues.push(...validateCanvasItems(node.items, `${path}.items`));
1095
+ issues.push(...validateLayoutMeta(node.meta, `${path}.meta`));
1096
+ break;
1097
+
1098
+ case "animate":
1099
+ issues.push(...validateAnimate(node, path));
1100
+ break;
1101
+
1102
+ case "code":
1103
+ issues.push(...validateKnownFields(node, CODE_KEYS, path));
1104
+ issues.push(...validateCodeLines(node.lines, `${path}.lines`));
1105
+ break;
1106
+
1107
+ case "icon":
1108
+ issues.push(...validateKnownFields(node, ICON_KEYS, path));
1109
+ if (!isAllowedString(node.name, ICON_NAME_VALUES)) {
1110
+ issues.push(
1111
+ node.name === undefined
1112
+ ? requiredIssue(`${path}.name`, `${path}.icon.name is required`)
1113
+ : enumIssue(
1114
+ `${path}.name`,
1115
+ `${path}.icon.name must be one of: ${ICON_NAME_VALUES.join(", ")}`,
1116
+ ICON_NAME_VALUES,
1117
+ node.name,
1118
+ ),
1119
+ );
1120
+ }
1121
+ if (
1122
+ node.tone !== undefined &&
1123
+ !isAllowedString(node.tone, TEXT_TONE_VALUES)
1124
+ ) {
1125
+ issues.push(
1126
+ enumIssue(
1127
+ `${path}.tone`,
1128
+ `${path}.icon.tone must be a known tone`,
1129
+ TEXT_TONE_VALUES,
1130
+ node.tone,
1131
+ ),
1132
+ );
1133
+ }
1134
+ if (node.label !== undefined && typeof node.label !== "string") {
1135
+ issues.push(
1136
+ typeIssue(
1137
+ `${path}.label`,
1138
+ `${path}.icon.label must be a string`,
1139
+ "string",
1140
+ node.label,
1141
+ ),
1142
+ );
1143
+ }
1144
+ break;
1145
+
1146
+ case "badge":
1147
+ issues.push(...validateKnownFields(node, BADGE_KEYS, path));
1148
+ if (typeof node.text !== "string") {
1149
+ issues.push(
1150
+ node.text === undefined
1151
+ ? requiredIssue(`${path}.text`, `${path}.badge.text is required`)
1152
+ : typeIssue(
1153
+ `${path}.text`,
1154
+ `${path}.badge.text must be a string`,
1155
+ "string",
1156
+ node.text,
1157
+ ),
1158
+ );
1159
+ }
1160
+ if (
1161
+ node.tone !== undefined &&
1162
+ !isAllowedString(node.tone, TEXT_TONE_VALUES)
1163
+ ) {
1164
+ issues.push(
1165
+ enumIssue(
1166
+ `${path}.tone`,
1167
+ `${path}.badge.tone must be a known tone`,
1168
+ TEXT_TONE_VALUES,
1169
+ node.tone,
1170
+ ),
1171
+ );
1172
+ }
1173
+ if (
1174
+ node.dot !== undefined &&
1175
+ !isAllowedString(node.dot, TEXT_TONE_VALUES)
1176
+ ) {
1177
+ issues.push(
1178
+ enumIssue(
1179
+ `${path}.dot`,
1180
+ `${path}.badge.dot must be a known tone`,
1181
+ TEXT_TONE_VALUES,
1182
+ node.dot,
1183
+ ),
1184
+ );
1185
+ }
1186
+ if (
1187
+ node.variant !== undefined &&
1188
+ !isAllowedString(node.variant, BADGE_VARIANT_VALUES)
1189
+ ) {
1190
+ issues.push(
1191
+ enumIssue(
1192
+ `${path}.variant`,
1193
+ `${path}.badge.variant must be outline, soft, or solid`,
1194
+ BADGE_VARIANT_VALUES,
1195
+ node.variant,
1196
+ ),
1197
+ );
1198
+ }
1199
+ if (node.mono !== undefined && typeof node.mono !== "boolean") {
1200
+ issues.push(
1201
+ typeIssue(
1202
+ `${path}.mono`,
1203
+ `${path}.badge.mono must be a boolean`,
1204
+ "boolean",
1205
+ node.mono,
1206
+ ),
1207
+ );
1208
+ }
1209
+ break;
1210
+
1211
+ case "rule":
1212
+ issues.push(...validateKnownFields(node, RULE_KEYS, path));
1213
+ if (
1214
+ node.length !== undefined &&
1215
+ typeof node.length !== "string" &&
1216
+ typeof node.length !== "number"
1217
+ ) {
1218
+ issues.push(
1219
+ typeIssue(
1220
+ `${path}.length`,
1221
+ `${path}.rule.length must be a string or number`,
1222
+ "string | number",
1223
+ node.length,
1224
+ ),
1225
+ );
1226
+ }
1227
+ if (node.thickness !== undefined && typeof node.thickness !== "number") {
1228
+ issues.push(
1229
+ typeIssue(
1230
+ `${path}.thickness`,
1231
+ `${path}.rule.thickness must be a number`,
1232
+ "number",
1233
+ node.thickness,
1234
+ ),
1235
+ );
1236
+ }
1237
+ if (node.radius !== undefined && typeof node.radius !== "number") {
1238
+ issues.push(
1239
+ typeIssue(
1240
+ `${path}.radius`,
1241
+ `${path}.rule.radius must be a number`,
1242
+ "number",
1243
+ node.radius,
1244
+ ),
1245
+ );
1246
+ }
1247
+ if (
1248
+ node.tone !== undefined &&
1249
+ !isAllowedString(node.tone, TEXT_TONE_VALUES)
1250
+ ) {
1251
+ issues.push(
1252
+ enumIssue(
1253
+ `${path}.tone`,
1254
+ `${path}.rule.tone must be a known tone`,
1255
+ TEXT_TONE_VALUES,
1256
+ node.tone,
1257
+ ),
1258
+ );
1259
+ }
1260
+ if (node.gradient !== undefined) {
1261
+ if (!Array.isArray(node.gradient)) {
1262
+ issues.push(
1263
+ typeIssue(
1264
+ `${path}.gradient`,
1265
+ `${path}.rule.gradient must be an array of two tones`,
1266
+ "array",
1267
+ node.gradient,
1268
+ ),
1269
+ );
1270
+ } else {
1271
+ if (node.gradient.length !== 2) {
1272
+ issues.push(
1273
+ issue(
1274
+ `${path}.gradient`,
1275
+ "constraint",
1276
+ `${path}.rule.gradient must contain exactly two tones`,
1277
+ {
1278
+ suggestion: "Provide exactly two tone names.",
1279
+ },
1280
+ ),
1281
+ );
1282
+ }
1283
+ node.gradient.forEach((stop: unknown, i: number) => {
1284
+ if (!isAllowedString(stop, TEXT_TONE_VALUES)) {
1285
+ issues.push(
1286
+ enumIssue(
1287
+ `${path}.gradient[${i}]`,
1288
+ `${path}.rule.gradient[${i}] must be a known tone`,
1289
+ TEXT_TONE_VALUES,
1290
+ stop,
1291
+ ),
1292
+ );
1293
+ }
1294
+ });
1295
+ }
1296
+ }
1297
+ break;
1298
+
1299
+ case "table": {
1300
+ issues.push(...validateKnownFields(node, TABLE_KEYS, path));
1301
+ issues.push(...validateTableColumns(node.columns, `${path}.columns`));
1302
+ const columnCount = Array.isArray(node.columns) ? node.columns.length : 0;
1303
+ issues.push(...validateTableRows(node.rows, columnCount, `${path}.rows`));
1304
+ issues.push(...validateLayoutMeta(node.meta, `${path}.meta`));
1305
+ break;
1306
+ }
1307
+
1308
+ case "connector":
1309
+ issues.push(...validateKnownFields(node, CONNECTOR_KEYS, path));
1310
+ if (node.from === undefined) {
1311
+ issues.push(
1312
+ requiredIssue(`${path}.from`, `${path}.connector.from is required`),
1313
+ );
1314
+ } else {
1315
+ issues.push(...validateConnectorPoint(node.from, `${path}.from`));
1316
+ }
1317
+ if (node.to === undefined) {
1318
+ issues.push(
1319
+ requiredIssue(`${path}.to`, `${path}.connector.to is required`),
1320
+ );
1321
+ } else {
1322
+ issues.push(...validateConnectorPoint(node.to, `${path}.to`));
1323
+ }
1324
+ if (node.control !== undefined) {
1325
+ issues.push(...validateConnectorPoint(node.control, `${path}.control`));
1326
+ }
1327
+ if (
1328
+ node.arrow !== undefined &&
1329
+ !isAllowedString(node.arrow, CONNECTOR_ARROW_VALUES)
1330
+ ) {
1331
+ issues.push(
1332
+ enumIssue(
1333
+ `${path}.arrow`,
1334
+ `${path}.connector.arrow must be none, end, start, or both`,
1335
+ CONNECTOR_ARROW_VALUES,
1336
+ node.arrow,
1337
+ ),
1338
+ );
1339
+ }
1340
+ if (node.dashed !== undefined && typeof node.dashed !== "boolean") {
1341
+ issues.push(
1342
+ typeIssue(
1343
+ `${path}.dashed`,
1344
+ `${path}.connector.dashed must be a boolean`,
1345
+ "boolean",
1346
+ node.dashed,
1347
+ ),
1348
+ );
1349
+ }
1350
+ if (
1351
+ node.tone !== undefined &&
1352
+ !isAllowedString(node.tone, TEXT_TONE_VALUES)
1353
+ ) {
1354
+ issues.push(
1355
+ enumIssue(
1356
+ `${path}.tone`,
1357
+ `${path}.connector.tone must be a known tone`,
1358
+ TEXT_TONE_VALUES,
1359
+ node.tone,
1360
+ ),
1361
+ );
1362
+ }
1363
+ break;
1364
+
1365
+ default:
1366
+ issues.push(
1367
+ issue(
1368
+ `${path}.kind`,
1369
+ "unknown_kind",
1370
+ `${path}.kind must be a known layout or content kind`,
1371
+ {
1372
+ expected: LAYOUT_KINDS,
1373
+ received:
1374
+ typeof node.kind === "string"
1375
+ ? node.kind
1376
+ : describeType(node.kind),
1377
+ suggestion: `Use one of the supported kinds: ${LAYOUT_KINDS}.`,
1378
+ },
1379
+ ),
1380
+ );
1381
+ break;
1382
+ }
1383
+
1384
+ return issues;
1385
+ }