@wizeworks/silicaui-mcp 0.37.0 → 0.39.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.
@@ -0,0 +1,1131 @@
1
+ {
2
+ "entrypoint": "@wizeworks/silicaui-builder/email",
3
+ "reactEntrypoint": "@wizeworks/silicaui-builder/email/react",
4
+ "note": "The CLOSED node schema of the email builder — NOT one of the three delivery paths. An email document is body → section → (columns → column)* → content, projected to table-based, fully inline-styled HTML by `toEmailHtml`. Only these kinds exist and only these nestings are accepted: `EmailEditor.insert` returns undefined for anything else, silently. Fields are typed props on the node itself (not classes) and colors are literal hex (email clients can't resolve CSS custom properties or OKLCH).",
5
+ "sharedFields": [
6
+ {
7
+ "name": "id",
8
+ "optional": false,
9
+ "type": "string",
10
+ "doc": ""
11
+ },
12
+ {
13
+ "name": "ord",
14
+ "optional": true,
15
+ "type": "string",
16
+ "doc": "Fractional ORDERING KEY among siblings — a string chosen to sort strictly between its neighbors (`generateKeyBetween` in `@wizeworks/silicaui-html`). The same mechanism, and the same reasoning, as the site schema's `Node.ord`. `children` order still drives rendering; `ord` is what makes a position transportable in an op. An array index is not a stable address — \"insert at 2\" resolves differently depending on what else landed first — so two authors inserting into one section would produce a result neither saw. Authoring metadata: `toEmailHtml` never reads it, so it cannot reach sent markup. Backfilled at load by the engine for documents authored before it existed."
17
+ },
18
+ {
19
+ "name": "data",
20
+ "optional": true,
21
+ "type": "DataBinding",
22
+ "doc": "The SAME dynamic-content marker the site engine's `Node` carries (reused type, not reinvented) — an opaque `{ kind, ref, attr? }` the engine never parses. `value`/`action` are meaningful on any node; `collection` (repeat children once per item) is only meaningful on a node that actually HAS `children` (body/section/columns/column/link) — email's schema can't express a repeat on a leaf content node the way the site's uniform `Node` shape can, since a leaf kind has no `children` slot to repeat. See `resolve.ts`. ONE marker per node is the rule, not an oversight — it is what keeps a binding readable (\"this node comes from that field\") and what every consumer of `node.data` is written against. A card that needs two per-item values (an image `src` AND a destination) COMPOSES instead of stacking markers: a `LinkNode` binds the `href`, its `ImageNode` child binds the `src`."
23
+ },
24
+ {
25
+ "name": "locked",
26
+ "optional": true,
27
+ "type": "\"host\" | \"author\"",
28
+ "doc": "Structural immutability + its OWNER — the same two-tier flag the site schema carries (`silicaui-html`'s `NodeBase.locked`, host-nodes spec §B), reused here rather than reinvented, so a host that pins regions in both builders reasons about one concept. A locked node cannot be removed or moved; its OWN fields stay editable (a locked footer's copy is still typo-fixable) and so do its children. Presence IS locked; the value encodes who owns the lock: - `\"author\"` — the author locked it from the Inspector; the same toggle unlocks it. - `\"host\"` — the host locked it (a compliance block it stamps into a seeded document, or a runtime `setLocked` call). The author UI shows it locked and offers NO unlock; only the host can clear it. Authoring metadata: `toEmailHtml` never reads it, so it cannot reach sent markup. For chrome that must not live in the document at all — a brand bar or a legal footer that has to reflect the CURRENT brand on every send, and that an author must never even see as a deletable node — use `EmailFrame` (`frame.ts`) instead. A lock protects a node that IS part of the saved document; a frame is composed around it and never persisted."
29
+ }
30
+ ],
31
+ "bindingNote": "A node carries AT MOST ONE `data` marker. `value` fills one field (`attr` picks which — see each kind's `binding.fields`; omitting it targets `binding.default`); `collection` repeats a node's children once per item and is only meaningful on a kind with children; `visible` keeps or drops the subtree; `action` is an inert marker the host wires. Two per-item values on one card is COMPOSITION, not two markers: a `link` group binds the href while each child binds its own field.",
32
+ "documentTypes": [
33
+ {
34
+ "typeName": "SocialLink",
35
+ "doc": "",
36
+ "fields": [
37
+ {
38
+ "name": "platform",
39
+ "optional": false,
40
+ "type": "SocialPlatform",
41
+ "doc": ""
42
+ },
43
+ {
44
+ "name": "url",
45
+ "optional": false,
46
+ "type": "string",
47
+ "doc": ""
48
+ }
49
+ ]
50
+ },
51
+ {
52
+ "typeName": "EmailWebFont",
53
+ "doc": "One `@font-face` the projector emits into the document `<head>`. A webfont is a DOCUMENT-LEVEL DESIGN DECISION, so it lives in the schema — not in a caller-supplied CSS string. That way it travels with the document, round-trips through save/load, and is editable in the Inspector instead of being invisible code at the render call site. Two things every consumer needs to know, because neither is obvious and both bite in production: 1. **Reach is limited and that's fine.** Apple Mail, iOS Mail, Outlook for Mac, and Samsung Mail render webfonts. Gmail (every surface), Outlook on Windows, and Yahoo do NOT — they fall back to `EmailBody.fontFamily`. Always keep that stack a real, self-sufficient system stack. 2. **Use a hosted URL, not a `data:` URI.** Gmail CLIPS a message past roughly 102KB, hiding everything after the cut behind a \"View entire message\" link. An embedded font blows through that on its own and will silently truncate the email. The projector wraps the emitted `@font-face` in `@media screen` — Outlook's Word engine ignores that at-rule entirely, which stops it seeing a webfont it can't load and falling back to Times New Roman.",
54
+ "fields": [
55
+ {
56
+ "name": "family",
57
+ "optional": false,
58
+ "type": "string",
59
+ "doc": "The `font-family` name, e.g. `\"Sohne\"`. Quoted automatically when emitted."
60
+ },
61
+ {
62
+ "name": "src",
63
+ "optional": false,
64
+ "type": "string",
65
+ "doc": "A `url(...)`-able source. Hosted HTTPS strongly preferred — see above."
66
+ },
67
+ {
68
+ "name": "weight",
69
+ "optional": true,
70
+ "type": "number | string",
71
+ "doc": "Defaults to `400`."
72
+ },
73
+ {
74
+ "name": "style",
75
+ "optional": true,
76
+ "type": "\"normal\" | \"italic\"",
77
+ "doc": "Defaults to `normal`."
78
+ }
79
+ ]
80
+ },
81
+ {
82
+ "typeName": "EmailDocument",
83
+ "doc": "",
84
+ "fields": [
85
+ {
86
+ "name": "version",
87
+ "optional": false,
88
+ "type": "\"1\"",
89
+ "doc": ""
90
+ },
91
+ {
92
+ "name": "subject",
93
+ "optional": false,
94
+ "type": "string",
95
+ "doc": ""
96
+ },
97
+ {
98
+ "name": "preheader",
99
+ "optional": false,
100
+ "type": "string",
101
+ "doc": "Preview text shown next to the subject in an inbox list."
102
+ },
103
+ {
104
+ "name": "root",
105
+ "optional": false,
106
+ "type": "EmailBody",
107
+ "doc": ""
108
+ }
109
+ ]
110
+ },
111
+ {
112
+ "typeName": "EmailTemplate",
113
+ "doc": "One named email in a project — the unit the template switcher lists, adds, renames, and deletes. Mirrors the site engine's `Page` (name + its own tree), minus a route `slug` — a template has no URL to route.",
114
+ "fields": [
115
+ {
116
+ "name": "id",
117
+ "optional": false,
118
+ "type": "string",
119
+ "doc": ""
120
+ },
121
+ {
122
+ "name": "name",
123
+ "optional": false,
124
+ "type": "string",
125
+ "doc": ""
126
+ },
127
+ {
128
+ "name": "document",
129
+ "optional": false,
130
+ "type": "EmailDocument",
131
+ "doc": ""
132
+ }
133
+ ]
134
+ },
135
+ {
136
+ "typeName": "EmailProject",
137
+ "doc": "A project is one or more independent templates sharing nothing but the brand color defaults they were seeded with — mirrors the site engine's `Site` (pages sharing one theme + frame), scaled down: an email has no shared frame or theme tokens to hold at the project level, so a project is JUST the roster. `EmailEditor` edits one template at a time (its `activeTemplateId`).",
138
+ "fields": [
139
+ {
140
+ "name": "version",
141
+ "optional": false,
142
+ "type": "\"1\"",
143
+ "doc": ""
144
+ },
145
+ {
146
+ "name": "templates",
147
+ "optional": false,
148
+ "type": "EmailTemplate[]",
149
+ "doc": ""
150
+ }
151
+ ]
152
+ },
153
+ {
154
+ "typeName": "EmailColorDefaults",
155
+ "doc": "Hex color defaults new blocks / a fresh document seed with. Plain hex — no `Theme`/OKLCH knowledge here, so the engine stays framework-neutral; the React layer (`email/react/theme-defaults.ts`) resolves an actual brand `Theme` down to this shape before constructing an `EmailEditor`, since email HTML can't ship OKLCH (Outlook and most clients don't support CSS color functions) — every stored color must already be a literal hex string.",
156
+ "fields": [
157
+ {
158
+ "name": "primary",
159
+ "optional": false,
160
+ "type": "string",
161
+ "doc": "Button background / links / accents."
162
+ },
163
+ {
164
+ "name": "primaryContent",
165
+ "optional": false,
166
+ "type": "string",
167
+ "doc": "Text color that reads on top of `primary`."
168
+ },
169
+ {
170
+ "name": "baseContent",
171
+ "optional": false,
172
+ "type": "string",
173
+ "doc": "Body copy color."
174
+ },
175
+ {
176
+ "name": "base100",
177
+ "optional": false,
178
+ "type": "string",
179
+ "doc": "Section / content background."
180
+ },
181
+ {
182
+ "name": "base200",
183
+ "optional": false,
184
+ "type": "string",
185
+ "doc": "Outer canvas background."
186
+ },
187
+ {
188
+ "name": "base300",
189
+ "optional": false,
190
+ "type": "string",
191
+ "doc": "Divider color."
192
+ },
193
+ {
194
+ "name": "secondary",
195
+ "optional": false,
196
+ "type": "string",
197
+ "doc": "The rest of the theme's semantic roles (`rolesOf`'s `SEMANTIC_ROLES`, minus `primary` above) — resolved purely so the Inspector's color swatches offer the SAME palette breadth as the site builder's, not used to seed any block default (only `primary`/`base*` are — see the block `make()` functions in `../palette.ts`)."
198
+ },
199
+ {
200
+ "name": "accent",
201
+ "optional": false,
202
+ "type": "string",
203
+ "doc": ""
204
+ },
205
+ {
206
+ "name": "neutral",
207
+ "optional": false,
208
+ "type": "string",
209
+ "doc": ""
210
+ },
211
+ {
212
+ "name": "info",
213
+ "optional": false,
214
+ "type": "string",
215
+ "doc": ""
216
+ },
217
+ {
218
+ "name": "success",
219
+ "optional": false,
220
+ "type": "string",
221
+ "doc": ""
222
+ },
223
+ {
224
+ "name": "warning",
225
+ "optional": false,
226
+ "type": "string",
227
+ "doc": ""
228
+ },
229
+ {
230
+ "name": "error",
231
+ "optional": false,
232
+ "type": "string",
233
+ "doc": ""
234
+ }
235
+ ]
236
+ }
237
+ ],
238
+ "kinds": [
239
+ {
240
+ "kind": "text",
241
+ "typeName": "TextNode",
242
+ "doc": "",
243
+ "isContent": true,
244
+ "container": false,
245
+ "holds": [],
246
+ "allowedParents": [
247
+ "link",
248
+ "column",
249
+ "section"
250
+ ],
251
+ "fields": [
252
+ {
253
+ "name": "html",
254
+ "optional": false,
255
+ "type": "string",
256
+ "doc": "Minimal inline-safe HTML: `<b>`, `<i>`, `<a href>`, `<br>` — nothing block-level."
257
+ },
258
+ {
259
+ "name": "align",
260
+ "optional": false,
261
+ "type": "Align",
262
+ "doc": ""
263
+ },
264
+ {
265
+ "name": "color",
266
+ "optional": false,
267
+ "type": "string",
268
+ "doc": ""
269
+ },
270
+ {
271
+ "name": "colorAuto",
272
+ "optional": true,
273
+ "type": "boolean",
274
+ "doc": "When true, `color` tracks the brand theme's `baseContent` role live (see `EmailEditor.setColorDefaults`) instead of being a frozen manual pick."
275
+ },
276
+ {
277
+ "name": "colorRole",
278
+ "optional": true,
279
+ "type": "keyof EmailColorDefaults",
280
+ "doc": "Which `EmailColorDefaults` role `color` tracks while `colorAuto` — see `AutoColorRule` in `engine.ts`. Omitted means the kind's default role (`baseContent`)."
281
+ },
282
+ {
283
+ "name": "linkColor",
284
+ "optional": true,
285
+ "type": "string",
286
+ "doc": "Color for `<a>` elements inside `html`. Unset (the default) leaves anchors to the client's own hyperlink blue — the historical behavior, and the one that keeps output byte-identical for documents that never set this. There is no LINK NODE and deliberately so: a link is inline INSIDE copy and this schema has no inline level to hold one. So the projector applies this by rewriting anchor tags in `html` — the one place it parses markup at all, acceptable only because a `TextNode`'s `html` is the constrained inline-safe subset documented above (arbitrary markup belongs in an `HtmlNode`, which stays untouched). An anchor that already carries its own `color:` wins."
287
+ },
288
+ {
289
+ "name": "linkColorAuto",
290
+ "optional": true,
291
+ "type": "boolean",
292
+ "doc": "Tracks the theme's `primary` role live — see `colorAuto`."
293
+ },
294
+ {
295
+ "name": "linkColorRole",
296
+ "optional": true,
297
+ "type": "keyof EmailColorDefaults",
298
+ "doc": ""
299
+ },
300
+ {
301
+ "name": "fontSize",
302
+ "optional": false,
303
+ "type": "number",
304
+ "doc": ""
305
+ },
306
+ {
307
+ "name": "fontWeight",
308
+ "optional": false,
309
+ "type": "FontWeight",
310
+ "doc": ""
311
+ },
312
+ {
313
+ "name": "lineHeight",
314
+ "optional": false,
315
+ "type": "number",
316
+ "doc": ""
317
+ }
318
+ ],
319
+ "binding": {
320
+ "default": "html",
321
+ "fields": {
322
+ "html": "string"
323
+ }
324
+ },
325
+ "sourceFile": "silicaui-builder/src/email/schema.ts"
326
+ },
327
+ {
328
+ "kind": "image",
329
+ "typeName": "ImageNode",
330
+ "doc": "",
331
+ "isContent": true,
332
+ "container": false,
333
+ "holds": [],
334
+ "allowedParents": [
335
+ "link",
336
+ "column",
337
+ "section"
338
+ ],
339
+ "fields": [
340
+ {
341
+ "name": "src",
342
+ "optional": false,
343
+ "type": "string",
344
+ "doc": ""
345
+ },
346
+ {
347
+ "name": "alt",
348
+ "optional": false,
349
+ "type": "string",
350
+ "doc": ""
351
+ },
352
+ {
353
+ "name": "href",
354
+ "optional": true,
355
+ "type": "string",
356
+ "doc": ""
357
+ },
358
+ {
359
+ "name": "width",
360
+ "optional": false,
361
+ "type": "number",
362
+ "doc": "Pixels; the projector clamps it to the body width."
363
+ },
364
+ {
365
+ "name": "align",
366
+ "optional": false,
367
+ "type": "Align",
368
+ "doc": ""
369
+ }
370
+ ],
371
+ "binding": {
372
+ "default": "src",
373
+ "fields": {
374
+ "src": "string",
375
+ "alt": "string",
376
+ "href": "string",
377
+ "width": "number"
378
+ }
379
+ },
380
+ "sourceFile": "silicaui-builder/src/email/schema.ts"
381
+ },
382
+ {
383
+ "kind": "button",
384
+ "typeName": "ButtonNode",
385
+ "doc": "",
386
+ "isContent": true,
387
+ "container": false,
388
+ "holds": [],
389
+ "allowedParents": [
390
+ "link",
391
+ "column",
392
+ "section"
393
+ ],
394
+ "fields": [
395
+ {
396
+ "name": "label",
397
+ "optional": false,
398
+ "type": "string",
399
+ "doc": ""
400
+ },
401
+ {
402
+ "name": "href",
403
+ "optional": false,
404
+ "type": "string",
405
+ "doc": ""
406
+ },
407
+ {
408
+ "name": "variant",
409
+ "optional": true,
410
+ "type": "ButtonVariant",
411
+ "doc": "Omitted means `filled` — so a document authored before variants existed projects byte-identically."
412
+ },
413
+ {
414
+ "name": "bg",
415
+ "optional": false,
416
+ "type": "string",
417
+ "doc": ""
418
+ },
419
+ {
420
+ "name": "bgAuto",
421
+ "optional": true,
422
+ "type": "boolean",
423
+ "doc": "See `TextNode.colorAuto` — tracks the theme's `primary` role live."
424
+ },
425
+ {
426
+ "name": "bgRole",
427
+ "optional": true,
428
+ "type": "keyof EmailColorDefaults",
429
+ "doc": ""
430
+ },
431
+ {
432
+ "name": "color",
433
+ "optional": false,
434
+ "type": "string",
435
+ "doc": ""
436
+ },
437
+ {
438
+ "name": "colorAuto",
439
+ "optional": true,
440
+ "type": "boolean",
441
+ "doc": "Tracks the theme's `primaryContent` role live."
442
+ },
443
+ {
444
+ "name": "colorRole",
445
+ "optional": true,
446
+ "type": "keyof EmailColorDefaults",
447
+ "doc": ""
448
+ },
449
+ {
450
+ "name": "borderColor",
451
+ "optional": true,
452
+ "type": "string",
453
+ "doc": "Border color; defaults to `bg` when a width is set but no color is."
454
+ },
455
+ {
456
+ "name": "borderColorAuto",
457
+ "optional": true,
458
+ "type": "boolean",
459
+ "doc": "Tracks the theme's `primary` role live."
460
+ },
461
+ {
462
+ "name": "borderColorRole",
463
+ "optional": true,
464
+ "type": "keyof EmailColorDefaults",
465
+ "doc": ""
466
+ },
467
+ {
468
+ "name": "borderWidth",
469
+ "optional": true,
470
+ "type": "number",
471
+ "doc": "Border thickness in px. A `filled` button renders a border only when this is set; an `outline` button falls back to 1px when it isn't."
472
+ },
473
+ {
474
+ "name": "radius",
475
+ "optional": false,
476
+ "type": "number",
477
+ "doc": ""
478
+ },
479
+ {
480
+ "name": "align",
481
+ "optional": false,
482
+ "type": "Align",
483
+ "doc": ""
484
+ },
485
+ {
486
+ "name": "paddingX",
487
+ "optional": false,
488
+ "type": "number",
489
+ "doc": ""
490
+ },
491
+ {
492
+ "name": "paddingY",
493
+ "optional": false,
494
+ "type": "number",
495
+ "doc": ""
496
+ }
497
+ ],
498
+ "binding": {
499
+ "default": "label",
500
+ "fields": {
501
+ "label": "string",
502
+ "href": "string",
503
+ "bg": "string",
504
+ "color": "string"
505
+ }
506
+ },
507
+ "sourceFile": "silicaui-builder/src/email/schema.ts"
508
+ },
509
+ {
510
+ "kind": "divider",
511
+ "typeName": "DividerNode",
512
+ "doc": "",
513
+ "isContent": true,
514
+ "container": false,
515
+ "holds": [],
516
+ "allowedParents": [
517
+ "link",
518
+ "column",
519
+ "section"
520
+ ],
521
+ "fields": [
522
+ {
523
+ "name": "color",
524
+ "optional": false,
525
+ "type": "string",
526
+ "doc": ""
527
+ },
528
+ {
529
+ "name": "colorAuto",
530
+ "optional": true,
531
+ "type": "boolean",
532
+ "doc": "Tracks the theme's `base300` role live."
533
+ },
534
+ {
535
+ "name": "colorRole",
536
+ "optional": true,
537
+ "type": "keyof EmailColorDefaults",
538
+ "doc": ""
539
+ },
540
+ {
541
+ "name": "thickness",
542
+ "optional": false,
543
+ "type": "number",
544
+ "doc": ""
545
+ }
546
+ ],
547
+ "binding": {
548
+ "fields": {
549
+ "color": "string",
550
+ "thickness": "number"
551
+ }
552
+ },
553
+ "sourceFile": "silicaui-builder/src/email/schema.ts"
554
+ },
555
+ {
556
+ "kind": "spacer",
557
+ "typeName": "SpacerNode",
558
+ "doc": "",
559
+ "isContent": true,
560
+ "container": false,
561
+ "holds": [],
562
+ "allowedParents": [
563
+ "link",
564
+ "column",
565
+ "section"
566
+ ],
567
+ "fields": [
568
+ {
569
+ "name": "height",
570
+ "optional": false,
571
+ "type": "number",
572
+ "doc": ""
573
+ }
574
+ ],
575
+ "binding": {
576
+ "fields": {
577
+ "height": "number"
578
+ }
579
+ },
580
+ "sourceFile": "silicaui-builder/src/email/schema.ts"
581
+ },
582
+ {
583
+ "kind": "social",
584
+ "typeName": "SocialNode",
585
+ "doc": "",
586
+ "isContent": true,
587
+ "container": false,
588
+ "holds": [],
589
+ "allowedParents": [
590
+ "link",
591
+ "column",
592
+ "section"
593
+ ],
594
+ "fields": [
595
+ {
596
+ "name": "links",
597
+ "optional": false,
598
+ "type": "SocialLink[]",
599
+ "doc": ""
600
+ },
601
+ {
602
+ "name": "align",
603
+ "optional": false,
604
+ "type": "Align",
605
+ "doc": ""
606
+ },
607
+ {
608
+ "name": "iconSize",
609
+ "optional": false,
610
+ "type": "number",
611
+ "doc": ""
612
+ },
613
+ {
614
+ "name": "gap",
615
+ "optional": false,
616
+ "type": "number",
617
+ "doc": ""
618
+ }
619
+ ],
620
+ "binding": null,
621
+ "sourceFile": "silicaui-builder/src/email/schema.ts"
622
+ },
623
+ {
624
+ "kind": "html",
625
+ "typeName": "HtmlNode",
626
+ "doc": "Raw HTML passthrough for power users — merge tags (`{{first_name}}`, an ESP's own syntax) pass through untouched since the projector never parses this string, just emits it verbatim.",
627
+ "isContent": true,
628
+ "container": false,
629
+ "holds": [],
630
+ "allowedParents": [
631
+ "link",
632
+ "column",
633
+ "section"
634
+ ],
635
+ "fields": [
636
+ {
637
+ "name": "html",
638
+ "optional": false,
639
+ "type": "string",
640
+ "doc": ""
641
+ }
642
+ ],
643
+ "binding": {
644
+ "default": "html",
645
+ "fields": {
646
+ "html": "string"
647
+ }
648
+ },
649
+ "sourceFile": "silicaui-builder/src/email/schema.ts"
650
+ },
651
+ {
652
+ "kind": "video",
653
+ "typeName": "VideoNode",
654
+ "doc": "A video: email clients can't embed/autoplay `<video>` reliably (most strip it), so the universal technique is a linked thumbnail image that opens the video's real URL — this node IS that, not a video embed.",
655
+ "isContent": true,
656
+ "container": false,
657
+ "holds": [],
658
+ "allowedParents": [
659
+ "link",
660
+ "column",
661
+ "section"
662
+ ],
663
+ "fields": [
664
+ {
665
+ "name": "href",
666
+ "optional": false,
667
+ "type": "string",
668
+ "doc": ""
669
+ },
670
+ {
671
+ "name": "thumbnail",
672
+ "optional": false,
673
+ "type": "string",
674
+ "doc": ""
675
+ },
676
+ {
677
+ "name": "width",
678
+ "optional": false,
679
+ "type": "number",
680
+ "doc": ""
681
+ },
682
+ {
683
+ "name": "align",
684
+ "optional": false,
685
+ "type": "Align",
686
+ "doc": ""
687
+ },
688
+ {
689
+ "name": "showPlayButton",
690
+ "optional": false,
691
+ "type": "boolean",
692
+ "doc": "A centered play-glyph overlay drawn over the thumbnail."
693
+ }
694
+ ],
695
+ "binding": {
696
+ "default": "href",
697
+ "fields": {
698
+ "href": "string",
699
+ "thumbnail": "string",
700
+ "width": "number"
701
+ }
702
+ },
703
+ "sourceFile": "silicaui-builder/src/email/schema.ts"
704
+ },
705
+ {
706
+ "kind": "link",
707
+ "typeName": "LinkNode",
708
+ "doc": "A CLICKABLE GROUP of content — one destination shared by an image, a title, and a price, so a repeated card can deep-link to its own record. ── Why this node exists ────────────────────────────────────────────────────── Every node carries at most ONE `data` marker, so an `ImageNode` inside a `collection` repeat can bind its `src` from `item.imageUrl` OR its `href` from `item.url` — never both. And a `TextNode` has no `href` field at all: a link inside copy is authored as inline `<a>` markup, which is a literal string and therefore identical on every repeated item. That left a product rail with no way to send each card to its own PDP. This node is the missing composition step: it holds the `href`, so `data: { kind: 'value', ref: 'url', attr: 'href' }` on it binds per item while each child keeps its own marker for its own field. ── How it projects, and why NOT as one `<a>` ───────────────────────────────── The obvious lowering — wrap the card's markup in a single anchor — is exactly what does NOT work in email. An `<a>` around block-level content (a table, a `<div>`) is invalid in the HTML dialect Outlook's Word engine parses, and it drops the link there: the card renders, looks clickable, and silently isn't, for the one audience most likely to be reading a transactional email on a desktop client. So the projector DISTRIBUTES the link down onto each child that can carry one instead (see `renderLink`): an image becomes `<a><img></a>`, a text block's copy is wrapped in an anchor. Both are plain inline anchors — bulletproof in every client, Outlook included. The consequence, stated plainly because it is a real difference from the site engine's link box: the CONTENT of the card is clickable, the padding and gaps around it are not. That is the honest ceiling of email link support, and it beats a whole-card hit area that evaporates in Outlook. A child that carries its OWN destination keeps it — an explicit link beats an inherited one, so a \"Buy now\" button inside a card still goes wherever its own `href` says. Same for `video`/`social`, which are links already.",
709
+ "isContent": false,
710
+ "container": true,
711
+ "holds": [
712
+ "text",
713
+ "image",
714
+ "button",
715
+ "divider",
716
+ "spacer",
717
+ "social",
718
+ "html",
719
+ "video"
720
+ ],
721
+ "allowedParents": [
722
+ "column",
723
+ "section"
724
+ ],
725
+ "fields": [
726
+ {
727
+ "name": "href",
728
+ "optional": false,
729
+ "type": "string",
730
+ "doc": "Where the group points. Bindable per item inside a `collection` repeat via `data: { kind: 'value', ref: '<field>', attr: 'href' }` (it is also the default target, so a bare `value` bind with no `attr` fills it). Empty means \"no link yet\" — children render exactly as if they weren't grouped."
731
+ },
732
+ {
733
+ "name": "children",
734
+ "optional": false,
735
+ "type": "ContentNode[]",
736
+ "doc": "Content only — no nested `columns`, and no nested `link`. Both are type errors rather than runtime guards, and the second is load-bearing: nested anchors are invalid HTML, and \"which of the two destinations wins\" has no answer an author would predict."
737
+ }
738
+ ],
739
+ "binding": {
740
+ "default": "href",
741
+ "fields": {
742
+ "href": "string"
743
+ }
744
+ },
745
+ "sourceFile": "silicaui-builder/src/email/schema.ts"
746
+ },
747
+ {
748
+ "kind": "column",
749
+ "typeName": "ColumnNode",
750
+ "doc": "",
751
+ "isContent": false,
752
+ "container": true,
753
+ "holds": [
754
+ "text",
755
+ "image",
756
+ "button",
757
+ "divider",
758
+ "spacer",
759
+ "social",
760
+ "html",
761
+ "video",
762
+ "link",
763
+ "columns"
764
+ ],
765
+ "allowedParents": [
766
+ "columns"
767
+ ],
768
+ "fields": [
769
+ {
770
+ "name": "widthPct",
771
+ "optional": false,
772
+ "type": "number",
773
+ "doc": "This column's share of the row; a row's columns should sum to 100."
774
+ },
775
+ {
776
+ "name": "children",
777
+ "optional": false,
778
+ "type": "LayoutChild[]",
779
+ "doc": ""
780
+ }
781
+ ],
782
+ "binding": null,
783
+ "sourceFile": "silicaui-builder/src/email/schema.ts"
784
+ },
785
+ {
786
+ "kind": "columns",
787
+ "typeName": "ColumnsNode",
788
+ "doc": "",
789
+ "isContent": false,
790
+ "container": true,
791
+ "holds": [
792
+ "column"
793
+ ],
794
+ "allowedParents": [
795
+ "column",
796
+ "section"
797
+ ],
798
+ "fields": [
799
+ {
800
+ "name": "children",
801
+ "optional": false,
802
+ "type": "ColumnNode[]",
803
+ "doc": ""
804
+ },
805
+ {
806
+ "name": "stackOnMobile",
807
+ "optional": false,
808
+ "type": "boolean",
809
+ "doc": ""
810
+ }
811
+ ],
812
+ "binding": null,
813
+ "sourceFile": "silicaui-builder/src/email/schema.ts"
814
+ },
815
+ {
816
+ "kind": "section",
817
+ "typeName": "SectionNode",
818
+ "doc": "",
819
+ "isContent": false,
820
+ "container": true,
821
+ "holds": [
822
+ "text",
823
+ "image",
824
+ "button",
825
+ "divider",
826
+ "spacer",
827
+ "social",
828
+ "html",
829
+ "video",
830
+ "link",
831
+ "columns"
832
+ ],
833
+ "allowedParents": [
834
+ "body"
835
+ ],
836
+ "fields": [
837
+ {
838
+ "name": "bg",
839
+ "optional": false,
840
+ "type": "string",
841
+ "doc": ""
842
+ },
843
+ {
844
+ "name": "bgAuto",
845
+ "optional": true,
846
+ "type": "boolean",
847
+ "doc": "Tracks the theme's `base100` role live."
848
+ },
849
+ {
850
+ "name": "bgRole",
851
+ "optional": true,
852
+ "type": "keyof EmailColorDefaults",
853
+ "doc": "Which `EmailColorDefaults` role `bg` tracks while `bgAuto` is on. Omitted means `base100` (the historical, hardcoded behavior). This is what makes a TINTED surface — a card or footer on `base200`/`base300`, or a brand-colored hero band on `primary` — follow each tenant's palette instead of freezing a literal neutral hex the moment you want anything but white."
854
+ },
855
+ {
856
+ "name": "bgImage",
857
+ "optional": true,
858
+ "type": "string",
859
+ "doc": "An optional background image URL. Email clients vary wildly on support (Outlook desktop needs a VML fallback, which the projector emits); `bg` always renders too, underneath, as the graceful-degradation fallback."
860
+ },
861
+ {
862
+ "name": "paddingX",
863
+ "optional": false,
864
+ "type": "number",
865
+ "doc": ""
866
+ },
867
+ {
868
+ "name": "paddingY",
869
+ "optional": false,
870
+ "type": "number",
871
+ "doc": ""
872
+ },
873
+ {
874
+ "name": "align",
875
+ "optional": true,
876
+ "type": "Align",
877
+ "doc": "Horizontal alignment of the section's `<td>` — what inline-block `columns` rows and inline content align against. Omitted means `center`, which is what the projector used to hardcode, so existing documents are unaffected."
878
+ },
879
+ {
880
+ "name": "radius",
881
+ "optional": true,
882
+ "type": "number",
883
+ "doc": "── Box decoration ────────────────────────────────────────────────────────── Any of `radius`/`borderWidth`/`marginX`/`marginY` promotes the section from a bare `<tr><td>` to a nested-table \"card\": an outer cell carrying the margin as padding (a `<td>` can't take real margin in Outlook's Word engine) wrapping an inner table that carries the fill, border, and radius. `radius` is IGNORED by Outlook desktop — corners go square there. That's the normal, accepted degradation for rounded email cards, not a bug to work around."
884
+ },
885
+ {
886
+ "name": "borderColor",
887
+ "optional": true,
888
+ "type": "string",
889
+ "doc": ""
890
+ },
891
+ {
892
+ "name": "borderColorAuto",
893
+ "optional": true,
894
+ "type": "boolean",
895
+ "doc": "Tracks the theme's `base300` role live."
896
+ },
897
+ {
898
+ "name": "borderColorRole",
899
+ "optional": true,
900
+ "type": "keyof EmailColorDefaults",
901
+ "doc": ""
902
+ },
903
+ {
904
+ "name": "borderWidth",
905
+ "optional": true,
906
+ "type": "number",
907
+ "doc": "Border thickness in px; a border renders only when this is > 0."
908
+ },
909
+ {
910
+ "name": "marginX",
911
+ "optional": true,
912
+ "type": "number",
913
+ "doc": "Outer inset, px — space OUTSIDE the section's own fill."
914
+ },
915
+ {
916
+ "name": "marginY",
917
+ "optional": true,
918
+ "type": "number",
919
+ "doc": ""
920
+ },
921
+ {
922
+ "name": "children",
923
+ "optional": false,
924
+ "type": "LayoutChild[]",
925
+ "doc": ""
926
+ }
927
+ ],
928
+ "binding": {
929
+ "fields": {
930
+ "bg": "string",
931
+ "bgImage": "string"
932
+ }
933
+ },
934
+ "sourceFile": "silicaui-builder/src/email/schema.ts"
935
+ },
936
+ {
937
+ "kind": "body",
938
+ "typeName": "EmailBody",
939
+ "doc": "",
940
+ "isContent": false,
941
+ "container": true,
942
+ "holds": [
943
+ "section"
944
+ ],
945
+ "allowedParents": [],
946
+ "fields": [
947
+ {
948
+ "name": "width",
949
+ "optional": false,
950
+ "type": "number",
951
+ "doc": "The email's canvas width in px (classic email default: 600)."
952
+ },
953
+ {
954
+ "name": "bg",
955
+ "optional": false,
956
+ "type": "string",
957
+ "doc": "Background behind the body (visible as side \"wallpaper\" in wide clients)."
958
+ },
959
+ {
960
+ "name": "bgAuto",
961
+ "optional": true,
962
+ "type": "boolean",
963
+ "doc": "Tracks the theme's `base200` role live."
964
+ },
965
+ {
966
+ "name": "bgRole",
967
+ "optional": true,
968
+ "type": "keyof EmailColorDefaults",
969
+ "doc": ""
970
+ },
971
+ {
972
+ "name": "contentBg",
973
+ "optional": false,
974
+ "type": "string",
975
+ "doc": "The body's own background."
976
+ },
977
+ {
978
+ "name": "contentBgAuto",
979
+ "optional": true,
980
+ "type": "boolean",
981
+ "doc": "Tracks the theme's `base100` role live."
982
+ },
983
+ {
984
+ "name": "contentBgRole",
985
+ "optional": true,
986
+ "type": "keyof EmailColorDefaults",
987
+ "doc": ""
988
+ },
989
+ {
990
+ "name": "fontFamily",
991
+ "optional": false,
992
+ "type": "string",
993
+ "doc": ""
994
+ },
995
+ {
996
+ "name": "webFonts",
997
+ "optional": true,
998
+ "type": "EmailWebFont[]",
999
+ "doc": "`@font-face` declarations; the families are prepended to `fontFamily` in the emitted body font stack, so `fontFamily` stays the fallback."
1000
+ },
1001
+ {
1002
+ "name": "colorScheme",
1003
+ "optional": true,
1004
+ "type": "\"light\" | \"dark\" | \"light dark\"",
1005
+ "doc": "Declares which color schemes this email is designed for — emitted as the `color-scheme`/`supported-color-schemes` `<meta>` pair plus the matching `:root` rule. Set this and Apple Mail / Outlook for Mac will honor your own `@media (prefers-color-scheme: dark)` rules (supplied via the projector's `head.css` hook). Gmail and Outlook.com IGNORE it and forcibly invert colors on their own terms regardless — so treat dark mode as progressive enhancement for a minority of clients, never as a design you can rely on."
1006
+ },
1007
+ {
1008
+ "name": "children",
1009
+ "optional": false,
1010
+ "type": "SectionNode[]",
1011
+ "doc": ""
1012
+ }
1013
+ ],
1014
+ "binding": {
1015
+ "fields": {
1016
+ "bg": "string",
1017
+ "contentBg": "string",
1018
+ "fontFamily": "string"
1019
+ }
1020
+ },
1021
+ "sourceFile": "silicaui-builder/src/email/schema.ts"
1022
+ }
1023
+ ],
1024
+ "palette": [
1025
+ {
1026
+ "key": "section",
1027
+ "label": "Section",
1028
+ "hint": "A full-width row — the top-level building block",
1029
+ "icon": "section",
1030
+ "kind": "section"
1031
+ },
1032
+ {
1033
+ "key": "columns-2",
1034
+ "label": "2 columns",
1035
+ "hint": "An even two-column row",
1036
+ "icon": "columns",
1037
+ "kind": "columns"
1038
+ },
1039
+ {
1040
+ "key": "columns-3",
1041
+ "label": "3 columns",
1042
+ "hint": "An even three-column row",
1043
+ "icon": "columns",
1044
+ "kind": "columns"
1045
+ },
1046
+ {
1047
+ "key": "columns-4",
1048
+ "label": "4 columns",
1049
+ "hint": "An even four-column row",
1050
+ "icon": "columns",
1051
+ "kind": "columns"
1052
+ },
1053
+ {
1054
+ "key": "link",
1055
+ "label": "Link group",
1056
+ "hint": "One destination for the blocks inside — bind it per item to link a repeated card",
1057
+ "icon": "link",
1058
+ "kind": "link"
1059
+ },
1060
+ {
1061
+ "key": "link-card",
1062
+ "label": "Linked card",
1063
+ "hint": "Image + title + price, all pointing at one URL — the product/article card",
1064
+ "icon": "link",
1065
+ "kind": "link"
1066
+ },
1067
+ {
1068
+ "key": "text",
1069
+ "label": "Text",
1070
+ "hint": "A paragraph of copy",
1071
+ "icon": "text",
1072
+ "kind": "text"
1073
+ },
1074
+ {
1075
+ "key": "image",
1076
+ "label": "Image",
1077
+ "hint": "A hosted image, optionally linked",
1078
+ "icon": "image",
1079
+ "kind": "image"
1080
+ },
1081
+ {
1082
+ "key": "button",
1083
+ "label": "Button",
1084
+ "hint": "A bulletproof call-to-action link",
1085
+ "icon": "button",
1086
+ "kind": "button"
1087
+ },
1088
+ {
1089
+ "key": "button-outline",
1090
+ "label": "Outline button",
1091
+ "hint": "A secondary call-to-action that won't compete with the primary",
1092
+ "icon": "button",
1093
+ "kind": "button"
1094
+ },
1095
+ {
1096
+ "key": "divider",
1097
+ "label": "Divider",
1098
+ "hint": "A thin horizontal rule",
1099
+ "icon": "divider",
1100
+ "kind": "divider"
1101
+ },
1102
+ {
1103
+ "key": "spacer",
1104
+ "label": "Spacer",
1105
+ "hint": "Vertical breathing room",
1106
+ "icon": "spacer",
1107
+ "kind": "spacer"
1108
+ },
1109
+ {
1110
+ "key": "social",
1111
+ "label": "Social icons",
1112
+ "hint": "A row of linked platform badges",
1113
+ "icon": "share",
1114
+ "kind": "social"
1115
+ },
1116
+ {
1117
+ "key": "video",
1118
+ "label": "Video",
1119
+ "hint": "A linked thumbnail — email clients can't embed real video",
1120
+ "icon": "video",
1121
+ "kind": "video"
1122
+ },
1123
+ {
1124
+ "key": "html",
1125
+ "label": "Custom HTML",
1126
+ "hint": "Raw HTML passthrough — for merge tags or hand-authored markup",
1127
+ "icon": "code",
1128
+ "kind": "html"
1129
+ }
1130
+ ]
1131
+ }