@pushwoosh/rpc-v2-http-api-data 0.2.121 → 0.2.123

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,1491 @@
1
+ export type DocumentBlockType = 'DOCUMENT_BLOCK_TYPE_UNSPECIFIED' | 'DOCUMENT_BLOCK_TYPE_CARD' | 'DOCUMENT_BLOCK_TYPE_COLUMNS'
2
+ /**
3
+ * Top-level free-form Content block (text + buttons), no card
4
+ * framing or image slot. Allowed in WEB_POPUP only.
5
+ */
6
+ | 'DOCUMENT_BLOCK_TYPE_CONTENT'
7
+ /**
8
+ * Structural stack container — a "group" block in the kind registry.
9
+ * Wire-shape is AiBuilderBlock.group. Allowed in EMAIL and WEB_POPUP.
10
+ */
11
+ | 'DOCUMENT_BLOCK_TYPE_GROUP'
12
+ /**
13
+ * Client-created empty slot meant to be materialised into a real
14
+ * block by the AI on a follow-up edit. Wire-shape is
15
+ * AiBuilderBlock.placeholder.
16
+ */
17
+ | 'DOCUMENT_BLOCK_TYPE_PLACEHOLDER';
18
+ /**
19
+ * GroupDirection picks which axis an AiBuilderGroup lays its children
20
+ * along.
21
+ *
22
+ * Wire contract:
23
+ * UNSPECIFIED — "inherit / use the document default" (currently
24
+ * rendered as a vertical stack). Render-time fallback
25
+ * only; NOT wire-equivalent to VERTICAL — downstream
26
+ * consumers that diff documents see them as different
27
+ * values.
28
+ * VERTICAL — explicit vertical stack. Reserved for an "Explicit
29
+ * override" UX where the client wants to pin the value
30
+ * against a future default change. The backend never
31
+ * emits this on freshly generated groups; that case
32
+ * stays UNSPECIFIED.
33
+ * HORIZONTAL — explicit side-by-side layout. Emitted only by the
34
+ * client (the LLM never picks a direction).
35
+ *
36
+ * Recommended client flow: send UNSPECIFIED for the default and switch
37
+ * to a concrete value only when the user explicitly chose a non-default
38
+ * direction; revert to UNSPECIFIED when the user resets it.
39
+ *
40
+ * Server contract: the backend preserves whatever value the client
41
+ * sent across edits — direction is in the client-managed set on
42
+ * AiBuilderGroup. The enum lives at file scope so the same shape is
43
+ * shared between the document model and the AiGen wire types.
44
+ */
45
+ export type GroupDirection = 'GROUP_DIRECTION_UNSPECIFIED' | 'GROUP_DIRECTION_VERTICAL' | 'GROUP_DIRECTION_HORIZONTAL';
46
+ export type CardLayout = 'CARD_LAYOUT_UNSPECIFIED' | 'CARD_LAYOUT_RIGHT' | 'CARD_LAYOUT_LEFT' | 'CARD_LAYOUT_TOP' | 'CARD_LAYOUT_NO'
47
+ /**
48
+ * The card image renders as the card backdrop (dark overlay + light text).
49
+ * Takes precedence over BlockStyles.background_image on the same block.
50
+ */
51
+ | 'CARD_LAYOUT_BACKGROUND';
52
+ export type ButtonGroupAlign = 'BUTTON_GROUP_ALIGN_UNSPECIFIED' | 'BUTTON_GROUP_ALIGN_LEFT' | 'BUTTON_GROUP_ALIGN_CENTER' | 'BUTTON_GROUP_ALIGN_RIGHT';
53
+ /**
54
+ * TextAlign is the horizontal alignment of a single TextBlock's copy.
55
+ * UNSPECIFIED = inherit (renders left). LLM-managed: the content generator
56
+ * sets it for headline/caption centring; preserved verbatim on edit.
57
+ */
58
+ export type TextAlign = 'TEXT_ALIGN_UNSPECIFIED' | 'TEXT_ALIGN_LEFT' | 'TEXT_ALIGN_CENTER' | 'TEXT_ALIGN_RIGHT';
59
+ /**
60
+ * GroupAlign controls alignment of an AiBuilderGroup's children along
61
+ * the perpendicular axis (for direction=VERTICAL → horizontal
62
+ * alignment of children; for direction=HORIZONTAL → vertical
63
+ * alignment). Named in start/center/end terms (not left/right)
64
+ * because the concrete axis depends on direction.
65
+ *
66
+ * Wire contract:
67
+ * UNSPECIFIED — "inherit / use the document default" (currently
68
+ * rendered as START). Render-time fallback only; NOT
69
+ * wire-equivalent to START.
70
+ * START — explicit start alignment. Reserved for the same
71
+ * "Explicit override" UX as GroupDirection.VERTICAL.
72
+ * The backend never emits this on freshly generated
73
+ * groups; that case stays UNSPECIFIED.
74
+ * CENTER/END — explicit center / end alignment.
75
+ *
76
+ * Recommended client flow: same as GroupDirection — UNSPECIFIED for the
77
+ * default, a concrete value only when the user explicitly chose one,
78
+ * back to UNSPECIFIED on reset.
79
+ */
80
+ export type GroupAlign = 'GROUP_ALIGN_UNSPECIFIED' | 'GROUP_ALIGN_START' | 'GROUP_ALIGN_CENTER' | 'GROUP_ALIGN_END';
81
+ /**
82
+ * CardImageFit controls how the card's image fills its image slot.
83
+ * Why it exists: card slots have fixed proportions while images arrive
84
+ * with arbitrary aspect ratios. Without this knob the client would have
85
+ * to pick a single global default and some cards would always look bad.
86
+ * A product photo is fine with COVER, a company logo must be CONTAIN
87
+ * or it gets cropped.
88
+ */
89
+ export type CardImageFit = 'CARD_IMAGE_FIT_UNSPECIFIED'
90
+ /** Fill the slot, cropping overflow. Good default for photos. */
91
+ | 'CARD_IMAGE_FIT_COVER'
92
+ /**
93
+ * Fit entirely inside the slot, may leave padding. Required for logos
94
+ * and other assets that must not be cropped.
95
+ */
96
+ | 'CARD_IMAGE_FIT_CONTAIN'
97
+ /**
98
+ * Stretch to fit, distorting aspect ratio. Deprecated — no longer
99
+ * offered in the editor and dropped from the LLM schema; kept only for
100
+ * wire compatibility.
101
+ */
102
+ | 'CARD_IMAGE_FIT_FILL';
103
+ /** Unit for CardImageSize.value. */
104
+ export type CardImageSizeUnit =
105
+ /** value ignored → client default */
106
+ 'CARD_IMAGE_SIZE_UNIT_UNSPECIFIED'
107
+ /** absolute pixels */
108
+ | 'CARD_IMAGE_SIZE_UNIT_PX'
109
+ /** % of the card content-box width */
110
+ | 'CARD_IMAGE_SIZE_UNIT_PERCENT';
111
+ /**
112
+ * CardImageSize sizes the image slot. Which dimension it drives depends on
113
+ * AiBuilderCard.layout:
114
+ * LEFT / RIGHT → width of the image column
115
+ * TOP → height of the image
116
+ * BACKGROUND → height of the card (width is always 100%)
117
+ * NO → no image, ignored
118
+ * PERCENT is always measured against the card content-box WIDTH, so a
119
+ * percentage height yields a stable aspect ratio regardless of card width.
120
+ * Client-managed: the LLM never sets this; the backend round-trips it
121
+ * verbatim. value <= 0 (or unit UNSPECIFIED) is treated as unset → the
122
+ * client falls back to its default.
123
+ */
124
+ export type CardImageSize = {
125
+ value: number;
126
+ unit: CardImageSizeUnit;
127
+ };
128
+ /**
129
+ * CardImagePosition is the focus / anchor of the image within its slot
130
+ * (CSS object-position). One field serves both UI modes:
131
+ * CONTAIN → "set position" (where the fitted image sits)
132
+ * COVER → "focal point" (which point stays in frame when cropped)
133
+ * Percentages 0..100; (0,0) = top-left, (50,50) = centre. Client-managed:
134
+ * the LLM never sets this; the backend round-trips it verbatim.
135
+ */
136
+ export type CardImagePosition = {
137
+ /** horizontal, % (left) */
138
+ x: number;
139
+ /** vertical, % (top) */
140
+ y: number;
141
+ };
142
+ export type TextVariantStyle = {
143
+ fontSize: number;
144
+ fontWeight: number;
145
+ lineHeight: number;
146
+ /**
147
+ * Font-family override for this text variant (e.g. "headings in Inter,
148
+ * body in Roboto").
149
+ *
150
+ * Wire contract:
151
+ * - CSS font-family syntax; see DocumentSettings.font_family.
152
+ * - Unset (or empty string) = inherit from
153
+ * DocumentSettings.font_family, which itself falls back to the
154
+ * renderer default. Backend should emit unset rather than "".
155
+ * - Client-managed: the LLM never selects fonts.
156
+ */
157
+ fontFamily?: string;
158
+ /**
159
+ * Colour for text rendered with this variant (e.g. one colour for
160
+ * headings, another for body copy).
161
+ *
162
+ * Wire contract:
163
+ * - #RGB or #RRGGBB hex.
164
+ * - Unset (or empty string) = inherit. Cascade: TextBlock.color →
165
+ * this field → DocumentSettings.text_color → renderer default.
166
+ * - Client-managed: the LLM picks per-item colours via TextBlock.color;
167
+ * variant colours are user preferences, round-tripped verbatim.
168
+ */
169
+ color?: string;
170
+ /**
171
+ * Letter spacing (CSS letter-spacing) in px; fractional and negative
172
+ * values are meaningful ("-0.2" tightens display type). Unset = normal.
173
+ * Client-managed typography, round-tripped verbatim like font_family.
174
+ */
175
+ letterSpacing?: number;
176
+ };
177
+ export type ColorSchemeItem = {
178
+ color: string;
179
+ description: string;
180
+ };
181
+ /**
182
+ * Authored dark palette. Blocks store resolved hex, not slot references, so
183
+ * dark mode works by value: the renderer builds light-hex → dark-hex maps from
184
+ * the two palettes and rewrites the emitted HTML, pairing `pwd-N` class hooks
185
+ * with @media (prefers-color-scheme: dark) overrides.
186
+ *
187
+ * Absent is NOT the same as disabled-with-a-palette: without one the document
188
+ * declares `color-scheme: only light`, opting out of client recolouring
189
+ * entirely, because one light palette recoloured by a mail client collapses
190
+ * dark text onto a dark canvas.
191
+ */
192
+ export type DarkColorScheme = {
193
+ enabled: boolean;
194
+ colors: Record<string, ColorSchemeItem>;
195
+ };
196
+ export type EdgeInsets = {
197
+ top: number;
198
+ right: number;
199
+ bottom: number;
200
+ left: number;
201
+ };
202
+ /**
203
+ * BlockStyles is the universal per-block visual override applied at
204
+ * the AiBuilderBlock level — independent of the concrete block kind.
205
+ * Every block kind (card, columns, group, and any future kind we add)
206
+ * carries it through AiBuilderBlock.styles; the client renders the
207
+ * frame, the LLM emits the colour overrides, and the user adjusts the
208
+ * rest in the editor. Per-kind layout knobs (CardStyles padding,
209
+ * ColumnsStyles gap, …) live on their own messages and stay
210
+ * independent of this one.
211
+ *
212
+ * All fields are optional. An unset field means "inherit / no
213
+ * override" — the client falls back to DocumentSettings.default_styles
214
+ * and then to its renderer default.
215
+ *
216
+ * Tri-state semantics differ between string and numeric fields:
217
+ * - String fields (background_color, border_color): empty string ""
218
+ * is equivalent to unset — clients collapse both to "inherit".
219
+ * Backend should emit unset rather than ""; do not rely on "" as
220
+ * an "explicit reset" state for colours.
221
+ * - Numeric fields (border_width, border_radius): explicit 0 is
222
+ * distinct from unset (means "no border" / "sharp corners") and
223
+ * is preserved across edits.
224
+ *
225
+ * LLM-managed: background_color, border_color, border_radius — the
226
+ * block generator may emit these via AiGenBlockStyles and the
227
+ * pipeline lifts them into BlockStyles.
228
+ * Client-managed: border_width, border_radius_css, background_image,
229
+ * hide_on_mobile and hide_on_desktop — the LLM never emits them (absent
230
+ * from its schema), the backend preserves them verbatim across edits.
231
+ */
232
+ export type BlockStyles = {
233
+ /**
234
+ * Empty string "" is treated as unset by clients. Prefer emitting
235
+ * unset.
236
+ */
237
+ backgroundColor?: string;
238
+ /**
239
+ * Empty string "" is treated as unset by clients. Prefer emitting
240
+ * unset.
241
+ */
242
+ borderColor?: string;
243
+ /** Explicit 0 is distinct from unset and preserved. */
244
+ borderWidth?: number;
245
+ /** Explicit 0 is distinct from unset and preserved. */
246
+ borderRadius?: number;
247
+ /**
248
+ * CSS border-radius shorthand for per-corner rounding ("20px 20px 0px
249
+ * 0px"). Written by Unlayer conversion; clients render border_radius
250
+ * when set (user-edited uniform value wins), else this.
251
+ */
252
+ borderRadiusCss?: string;
253
+ /**
254
+ * Block backdrop image URL over background_color (CSS cover/center/no-repeat).
255
+ * Reintroduces a block backdrop the old field (tag 5) removed, now client-managed
256
+ * (user-set, LLM never touches it). "" ≡ unset. Ignored on a CARD_LAYOUT_BACKGROUND
257
+ * card — that layout's own image is the backdrop.
258
+ */
259
+ backgroundImage?: string;
260
+ /**
261
+ * Responsive visibility (Unlayer's hideMobile / hideDesktop): the emitted
262
+ * email hides the block via media-query classes. Client-managed.
263
+ */
264
+ hideOnMobile?: boolean;
265
+ hideOnDesktop?: boolean;
266
+ };
267
+ /** Two-stop CSS linear gradient: `linear-gradient(<angle>deg, from, to)`. */
268
+ export type BackgroundGradient = {
269
+ /** #RRGGBB */
270
+ from: string;
271
+ to: string;
272
+ /** CSS angle in degrees (180 = top→bottom). */
273
+ angle: number;
274
+ };
275
+ /**
276
+ * How a columns block (or a button group) stacks on mobile (max-width media
277
+ * query). Unset / UNSPECIFIED = stack in DOM order (the historical behaviour).
278
+ */
279
+ export type MobileStack = 'MOBILE_STACK_UNSPECIFIED'
280
+ /** Keep the items side by side on mobile (desktop widths). */
281
+ | 'MOBILE_STACK_NONE'
282
+ /** Stack in reverse order (last item on top). */
283
+ | 'MOBILE_STACK_REVERSE';
284
+ export type ColumnsStyles = {
285
+ /** Padding applied to each column in the columns block. */
286
+ columnPadding?: EdgeInsets;
287
+ gap?: number;
288
+ /** Padding applied around the columns block (outside the columns row). */
289
+ padding?: EdgeInsets;
290
+ /**
291
+ * Mobile (media-query) overrides of column_padding / padding. Unset =
292
+ * desktop value with the client's automatic mobile clamp. Client-managed,
293
+ * like the whole message (the LLM never sees ColumnsStyles).
294
+ */
295
+ mobileColumnPadding?: EdgeInsets;
296
+ mobilePadding?: EdgeInsets;
297
+ mobileStack?: MobileStack;
298
+ };
299
+ export type CardStyles = {
300
+ /**
301
+ * Padding applied to the card's content (text) area on top of any
302
+ * document-level default. Applies to all layouts that have a content
303
+ * area (i.e. every layout, including NO).
304
+ */
305
+ contentPadding?: EdgeInsets;
306
+ /**
307
+ * Mobile (media-query) override of content_padding; same contract as
308
+ * ColumnsStyles.mobile_* — unset = desktop value + automatic clamp.
309
+ */
310
+ mobileContentPadding?: EdgeInsets;
311
+ };
312
+ /** Document-level image defaults; per-image fields override these. Client-managed. */
313
+ export type ImageStyles = {
314
+ /** Corner rounding, px. Unset (or 0) = square. */
315
+ borderRadius?: number;
316
+ };
317
+ export type DefaultStyles = {
318
+ common: BlockStyles;
319
+ columns: ColumnsStyles;
320
+ card: CardStyles;
321
+ buttonGroup: ButtonGroupStyles;
322
+ button: ButtonStyles;
323
+ /**
324
+ * Document-level default divider line config. Per-divider fields override
325
+ * these; unset here falls back to the renderer defaults (1px, scheme
326
+ * border, solid, 100%).
327
+ */
328
+ divider: Divider;
329
+ image: ImageStyles;
330
+ };
331
+ export type DocumentSettings = {
332
+ width: number;
333
+ blockGap: number;
334
+ padding: EdgeInsets;
335
+ backgroundColor: string;
336
+ textColor: string;
337
+ defaultStyles: DefaultStyles;
338
+ /**
339
+ * Document-wide font-family override.
340
+ *
341
+ * Wire contract:
342
+ * - CSS font-family syntax: a comma-separated fallback chain, e.g.
343
+ * "Inter, system-ui, sans-serif". The backend stores it verbatim
344
+ * and never parses or rewrites it.
345
+ * - Unset (or empty string) = "no override" — the client falls
346
+ * back to its renderer default. Backend should emit unset rather
347
+ * than ""; do not rely on "" as an "explicit reset" state.
348
+ * - Client-managed: the LLM never sees this field and never
349
+ * selects fonts.
350
+ * - Overridden per-variant by TextVariantStyle.font_family and
351
+ * per-button by ButtonStyles.font_family. Cascade resolves
352
+ * button → variant → document → renderer default.
353
+ */
354
+ fontFamily?: string;
355
+ /**
356
+ * Document-wide backdrop behind all blocks (email body background).
357
+ * Same semantics as the BlockStyles pair. Client-managed.
358
+ */
359
+ backgroundImage?: string;
360
+ backgroundGradient?: BackgroundGradient;
361
+ /**
362
+ * Soft drop shadow around the content column, rendered only when a painted
363
+ * backdrop (background_color / gradient / image) is present.
364
+ *
365
+ * Wire contract:
366
+ * - Unset = shown (the default look); false is the explicit opt-out.
367
+ * - Client-managed: render-only chrome, the LLM never sees this field.
368
+ */
369
+ showShadow?: boolean;
370
+ /**
371
+ * Corner rounding of the content column, px. Unset (or 0) = square.
372
+ * Client-managed render-only chrome, like show_shadow.
373
+ */
374
+ borderRadius?: number;
375
+ };
376
+ export type TextBlock = {
377
+ variant: string;
378
+ text: string;
379
+ /**
380
+ * Per-text-item colour override.
381
+ *
382
+ * Wire contract:
383
+ * - #RGB or #RRGGBB hex.
384
+ * - Unset (or empty string) = inherit. Cascade: this field →
385
+ * DocumentSettings.text_color → renderer default. Backend should
386
+ * emit unset rather than "".
387
+ * - LLM-managed: the block generator picks an accent colour from the
388
+ * document's color_scheme for CTA-relevant / primary copy and leaves
389
+ * it empty for body text so the document default applies.
390
+ */
391
+ color?: string;
392
+ /**
393
+ * Horizontal alignment of this text item. UNSPECIFIED = inherit (left).
394
+ * LLM-managed: the content generator centres headlines/captions where it
395
+ * fits the design; preserved verbatim on edit.
396
+ */
397
+ align: TextAlign;
398
+ /**
399
+ * Per-language overrides of `text`, keyed by language code ("es", "de").
400
+ * The default language lives in `text` itself. Client-managed: the LLM
401
+ * writes only `text`; translations are round-tripped verbatim.
402
+ */
403
+ translations: Record<string, string>;
404
+ /**
405
+ * Per-item font-size override in px. Unset (or 0) = inherit the variant's
406
+ * font_size. Client-managed: typography stays user-owned; the LLM sizes
407
+ * text by picking a variant. NOTE: like translations, this is lost when
408
+ * the LLM regenerates the block (items are rewritten wholesale).
409
+ */
410
+ fontSize?: number;
411
+ /**
412
+ * Per-item font-family override. CSS font-family syntax (see
413
+ * DocumentSettings.font_family). Unset (or empty string) = inherit the
414
+ * variant's font_family → DocumentSettings.font_family. Client-managed.
415
+ */
416
+ fontFamily?: string;
417
+ /**
418
+ * Background colour of this text item's box (#RGB / #RRGGBB). Unset (or
419
+ * empty string) = transparent — the block backdrop shows through.
420
+ * Client-managed: user-owned styling; like font_size, it is lost when the
421
+ * LLM regenerates the block (items are rewritten wholesale).
422
+ */
423
+ backgroundColor?: string;
424
+ /**
425
+ * Mobile (media-query) font-size override in px. Unset (or 0) = the
426
+ * client's automatic mobile clamp of the desktop size. Client-managed;
427
+ * like font_size, lost when the LLM regenerates the block.
428
+ */
429
+ mobileFontSize?: number;
430
+ /** Mobile alignment override. UNSPECIFIED = inherit `align` (explicit LEFT survives). Client-managed. */
431
+ mobileAlign: TextAlign;
432
+ /**
433
+ * Per-item letter-spacing override in px (fractional/negative meaningful;
434
+ * explicit 0 = "normal", overriding a variant's tracking). Unset = inherit
435
+ * TextVariantStyle.letter_spacing. Client-managed, like font_size.
436
+ */
437
+ letterSpacing?: number;
438
+ };
439
+ export type ButtonActionHref = {
440
+ url: string;
441
+ };
442
+ /**
443
+ * Web-popup only: the button runs an inline JS handler instead of
444
+ * navigating (Unlayer's "Custom Javascript" link type). Never emitted for
445
+ * EMAIL documents — mail clients strip script handlers.
446
+ */
447
+ export type ButtonActionJs = {
448
+ onClick: string;
449
+ };
450
+ export type ButtonAction_action_href = {
451
+ type: 'href';
452
+ data: ButtonActionHref;
453
+ };
454
+ export type ButtonAction_action_js = {
455
+ type: 'js';
456
+ data: ButtonActionJs;
457
+ };
458
+ export type ButtonAction_action = ButtonAction_action_href | ButtonAction_action_js;
459
+ export type ButtonAction = {
460
+ action: ButtonAction_action;
461
+ };
462
+ export type Button = {
463
+ /** Label; TextBlock.text's inline-HTML subset minus links/<br> (plain text also valid). */
464
+ text: string;
465
+ action: ButtonAction;
466
+ styles?: ButtonStyles;
467
+ /** Per-language overrides of `text`; same contract as TextBlock.translations. */
468
+ translations: Record<string, string>;
469
+ };
470
+ export type ButtonStyles = {
471
+ backgroundColor?: string;
472
+ textColor?: string;
473
+ borderRadius?: number;
474
+ borderColor?: string;
475
+ borderWidth?: number;
476
+ fontSize?: number;
477
+ /**
478
+ * Font-family override for button labels.
479
+ *
480
+ * Wire contract:
481
+ * - CSS font-family syntax; see DocumentSettings.font_family.
482
+ * - Unset (or empty string) = inherit. Cascade: this field →
483
+ * TextVariantStyle.font_family of the button's variant (if any) →
484
+ * DocumentSettings.font_family → renderer default. Backend
485
+ * should emit unset rather than "".
486
+ * - Client-managed: the LLM never emits this field. Button styles
487
+ * aren't part of the block generator's JSON schema, so this is
488
+ * enforced by construction.
489
+ */
490
+ fontFamily?: string;
491
+ /** Label padding, px. Client-managed; unset = renderer default (8 / 16). */
492
+ padding?: EdgeInsets;
493
+ /** Width as % of the block row (1-100). Client-managed; unset = auto (fit the label). */
494
+ widthPercent?: number;
495
+ /**
496
+ * Mobile (media-query) override of width_percent (1-100). Client-managed;
497
+ * unset = the group's mobile layout decides (full width when stacked,
498
+ * width_percent / auto when the desktop row is kept).
499
+ */
500
+ mobileWidthPercent?: number;
501
+ /** Mobile (media-query) override of padding. Client-managed; unset = desktop value. */
502
+ mobilePadding?: EdgeInsets;
503
+ };
504
+ export type ButtonGroup = {
505
+ buttons: Button[];
506
+ styles?: ButtonGroupStyles;
507
+ /**
508
+ * Horizontal alignment of the buttons within the group container.
509
+ * UNSPECIFIED = inherit from CSS / client default.
510
+ */
511
+ align: ButtonGroupAlign;
512
+ };
513
+ export type ButtonGroupStyles = {
514
+ padding?: EdgeInsets;
515
+ backgroundColor?: string;
516
+ gap?: number;
517
+ /**
518
+ * How the group lays out on mobile. Unset / UNSPECIFIED = stack the
519
+ * buttons full-width in DOM order (the historical behaviour); NONE keeps
520
+ * the desktop row. Client-managed, like the whole message.
521
+ */
522
+ mobileStack?: MobileStack;
523
+ /**
524
+ * Mobile overrides of gap / padding. Client-managed; unset = the mobile
525
+ * default (stacked: fixed stack gap below each button; row kept: the
526
+ * desktop values).
527
+ */
528
+ mobileGap?: number;
529
+ mobilePadding?: EdgeInsets;
530
+ };
531
+ /**
532
+ * ListItem is one entry of a List. `text` carries the same inline-HTML
533
+ * subset as TextBlock.text (sanitised on render) so list entries can hold
534
+ * links / emphasis.
535
+ */
536
+ export type ListItem = {
537
+ text: string;
538
+ /** Per-language overrides of `text`; same contract as TextBlock.translations. */
539
+ translations: Record<string, string>;
540
+ };
541
+ /**
542
+ * List is a simple list of short entries — features, benefits, steps,
543
+ * FAQ-style runs, terms & conditions.
544
+ */
545
+ export type List = {
546
+ items: ListItem[];
547
+ /**
548
+ * true = numbered (<ol>), false/unset = bulleted (<ul>).
549
+ * CONTEXT for now to keep the generated schema byte-compatible with the
550
+ * legacy hand-written one; candidate for MODEL (the block-gen prompt
551
+ * already promises numbered lists).
552
+ */
553
+ ordered: boolean;
554
+ /**
555
+ * Text variant of the entries (same registry as TextBlock.variant);
556
+ * unset = "normal".
557
+ */
558
+ variant: string;
559
+ /**
560
+ * Background colour of the list's box; same contract as
561
+ * TextBlock.background_color. Client-managed.
562
+ */
563
+ backgroundColor?: string;
564
+ };
565
+ export type FormFieldType =
566
+ /** treated as TEXT */
567
+ 'FORM_FIELD_TYPE_UNSPECIFIED' | 'FORM_FIELD_TYPE_TEXT' | 'FORM_FIELD_TYPE_EMAIL' | 'FORM_FIELD_TYPE_PHONE' | 'FORM_FIELD_TYPE_NUMBER' | 'FORM_FIELD_TYPE_DATE' | 'FORM_FIELD_TYPE_CHECKBOX' | 'FORM_FIELD_TYPE_SELECT' | 'FORM_FIELD_TYPE_TEXTAREA' | 'FORM_FIELD_TYPE_HIDDEN' | 'FORM_FIELD_TYPE_RADIO';
568
+ export type FormField = {
569
+ /**
570
+ * Input name, submitted verbatim; the cloud-pages backend turns every
571
+ * field into a subscriber tag of the same name. Two names are special
572
+ * there: "email" registers an Email-platform device, "phone_number" an
573
+ * SMS one.
574
+ */
575
+ name: string;
576
+ label: string;
577
+ type: FormFieldType;
578
+ required: boolean;
579
+ placeholder: string;
580
+ /** Choices for SELECT / RADIO / CHECKBOX-group fields. */
581
+ options: string[];
582
+ };
583
+ /**
584
+ * A subscription form (WEB_PAGE documents). Client-managed end to end in
585
+ * the MVP: the LLM neither sees nor generates forms; the converter and the
586
+ * editor own them. The emitted HTML must follow the cloud-pages submit
587
+ * conventions: <form method="GET"> with NO action — the hosting service
588
+ * rewrites the method to POST and the form posts back to the page's own
589
+ * URL.
590
+ */
591
+ export type Form = {
592
+ fields: FormField[];
593
+ /** Submit button. */
594
+ submitLabel: string;
595
+ submitStyles?: ButtonStyles;
596
+ };
597
+ /**
598
+ * Countdown timer. Renders in email HTML as an animated GIF served by this
599
+ * service's public /timer/v1/countdown.gif endpoint (regenerated per open).
600
+ * Client-managed for the MVP: the LLM neither sees nor generates timers.
601
+ */
602
+ export type Timer = {
603
+ /**
604
+ * Countdown target as local wall-clock time in `timezone`
605
+ * ("2026-12-01T00:00:00") — mirrors the Unlayer widget's
606
+ * endTime+timezone pair.
607
+ */
608
+ endTime: string;
609
+ /** IANA zone name; empty = UTC. */
610
+ timezone: string;
611
+ /** Unit-label language (ISO 639-1); empty/unknown = "en". */
612
+ language: string;
613
+ showLabels: boolean;
614
+ /**
615
+ * Hex "#rrggbb"; empty = renderer defaults (white bg, dark digits,
616
+ * gray labels).
617
+ */
618
+ backgroundColor: string;
619
+ digitColor: string;
620
+ labelColor: string;
621
+ /** px; 0 = 40 */
622
+ digitFontSize: number;
623
+ /** px; 0 = 16 */
624
+ labelFontSize: number;
625
+ /** Canvas size in px; 0 = derived from the font sizes. */
626
+ width: number;
627
+ height: number;
628
+ /**
629
+ * Font families carried for fidelity/round-trip; the GIF renderer
630
+ * currently always uses Open Sans.
631
+ */
632
+ digitFontFamily: string;
633
+ labelFontFamily: string;
634
+ /**
635
+ * CSS numeric font weights. The GIF renderer maps >=600 to Open Sans
636
+ * Bold, anything else to Regular (only those two faces are embedded).
637
+ * 0 keeps the historical defaults: digits bold (700), labels regular (400).
638
+ */
639
+ digitFontWeight: number;
640
+ labelFontWeight: number;
641
+ };
642
+ /**
643
+ * An Unlayer HTML widget. The markup is deliberately kept verbatim: unlike
644
+ * TextBlock.text this is a complete, user-authored HTML fragment and flattening
645
+ * or sanitising it would change the email. The editor previews it in a sandbox;
646
+ * the final email renderer emits it as-is.
647
+ */
648
+ export type RawHtml = {
649
+ html: string;
650
+ /**
651
+ * Per-language overrides of `html`; same client-managed contract as
652
+ * TextBlock.translations.
653
+ */
654
+ translations: Record<string, string>;
655
+ };
656
+ export type MenuItem = {
657
+ text: string;
658
+ url: string;
659
+ /** Unlayer stores browser targets as "_self" / "_blank". */
660
+ target: string;
661
+ translations: Record<string, string>;
662
+ };
663
+ export type MenuLayout =
664
+ /** renders as HORIZONTAL */
665
+ 'MENU_LAYOUT_UNSPECIFIED' | 'MENU_LAYOUT_HORIZONTAL' | 'MENU_LAYOUT_VERTICAL';
666
+ /**
667
+ * Unlayer's stock Menu widget. These are the presentation fields exported by
668
+ * Unlayer itself; keeping them together avoids approximating a menu as a text
669
+ * run and preserves each link as a distinct editable item.
670
+ */
671
+ export type Menu = {
672
+ items: MenuItem[];
673
+ linkColor: string;
674
+ textColor: string;
675
+ fontFamily: string;
676
+ fontWeight: number;
677
+ fontSize: number;
678
+ letterSpacing: number;
679
+ layout: MenuLayout;
680
+ align: TextAlign;
681
+ separator: string;
682
+ itemPadding?: EdgeInsets;
683
+ /** Mobile overrides; UNSPECIFIED = inherit the desktop value. Client-managed. */
684
+ mobileAlign: TextAlign;
685
+ mobileLayout: MenuLayout;
686
+ };
687
+ /**
688
+ * Video teaser in the content flow. Email clients can't embed players, so
689
+ * the HTML renders the poster (thumbnail + play-button overlay composited by
690
+ * this service's public /video/v1/poster.jpg endpoint) as an image linking to
691
+ * `url`. Client-managed for the MVP: the LLM neither sees nor generates
692
+ * videos; the editor derives the thumbnail from the video URL.
693
+ */
694
+ export type Video = {
695
+ /** Watch-page URL the poster links to (YouTube / Vimeo / any https link). */
696
+ url: string;
697
+ /**
698
+ * Poster source image URL. Derived by the editor for YouTube/Vimeo links,
699
+ * user-replaceable. Empty = nothing is rendered in the email.
700
+ */
701
+ thumbnailUrl: string;
702
+ alt: string;
703
+ /**
704
+ * Rendered width; the client Image.width idiom: "560px", or ""/"auto" =
705
+ * natural poster size (always capped to the content width).
706
+ */
707
+ width: string;
708
+ /** UNSPECIFIED renders center (matches the image widget default). */
709
+ align: TextAlign;
710
+ /** Corner rounding, px. Unset = inherit DefaultStyles.image; explicit 0 = square. */
711
+ borderRadius?: number;
712
+ /**
713
+ * Composite the play button onto the poster. Unset = on; false is the
714
+ * explicit opt-out (same idiom as DocumentSettings.show_shadow).
715
+ */
716
+ showPlayButton?: boolean;
717
+ /** Mobile alignment override. UNSPECIFIED = inherit `align`. */
718
+ mobileAlign: TextAlign;
719
+ };
720
+ /**
721
+ * Row of social-network icon links in the content flow (Unlayer's "social"
722
+ * widget; same SocialLink shape as the footer row). Client-managed — the
723
+ * LLM never emits it; icons come from the client's stock icon set.
724
+ */
725
+ export type SocialRow = {
726
+ links: SocialLink[];
727
+ /** UNSPECIFIED renders center (Unlayer's social default). */
728
+ align: TextAlign;
729
+ /** px; 0 = client default (24) */
730
+ iconSize: number;
731
+ /** px between icons; 0 = client default (12) */
732
+ gap: number;
733
+ /** Mobile alignment override. UNSPECIFIED = inherit `align`. Client-managed. */
734
+ mobileAlign: TextAlign;
735
+ };
736
+ export type ContentItem_item_text = {
737
+ type: 'text';
738
+ data: TextBlock;
739
+ };
740
+ export type ContentItem_item_buttonGroup = {
741
+ type: 'buttonGroup';
742
+ data: ButtonGroup;
743
+ };
744
+ export type ContentItem_item_list = {
745
+ type: 'list';
746
+ data: List;
747
+ };
748
+ export type ContentItem_item_image = {
749
+ type: 'image';
750
+ data: Image;
751
+ };
752
+ export type ContentItem_item_form = {
753
+ type: 'form';
754
+ data: Form;
755
+ };
756
+ export type ContentItem_item_timer = {
757
+ type: 'timer';
758
+ data: Timer;
759
+ };
760
+ export type ContentItem_item_rawHtml = {
761
+ type: 'rawHtml';
762
+ data: RawHtml;
763
+ };
764
+ export type ContentItem_item_menu = {
765
+ type: 'menu';
766
+ data: Menu;
767
+ };
768
+ export type ContentItem_item_social = {
769
+ type: 'social';
770
+ data: SocialRow;
771
+ };
772
+ export type ContentItem_item_video = {
773
+ type: 'video';
774
+ data: Video;
775
+ };
776
+ export type ContentItem_item = ContentItem_item_text | ContentItem_item_buttonGroup | ContentItem_item_list | ContentItem_item_image | ContentItem_item_form | ContentItem_item_timer | ContentItem_item_rawHtml | ContentItem_item_menu | ContentItem_item_social | ContentItem_item_video;
777
+ export type ContentItem = {
778
+ /**
779
+ * Container padding of the item. Unset = client's default inter-item gap;
780
+ * set (even zeros) = used verbatim. Client-managed, hidden from the LLM.
781
+ */
782
+ padding?: EdgeInsets;
783
+ /**
784
+ * Responsive visibility of this item (Unlayer's hideMobile / hideDesktop);
785
+ * same media-query contract as BlockStyles. Client-managed.
786
+ */
787
+ hideOnMobile?: boolean;
788
+ hideOnDesktop?: boolean;
789
+ /**
790
+ * Mobile (media-query) override of `padding`. Unset = desktop value with
791
+ * the client's automatic mobile clamp. Client-managed, hidden from the LLM.
792
+ */
793
+ mobilePadding?: EdgeInsets;
794
+ /**
795
+ * Per-recipient visibility of this item, compiled to Liquid on save.
796
+ * Client-managed: the model must never invent targeting rules.
797
+ */
798
+ displayCondition?: DisplayCondition;
799
+ item: ContentItem_item;
800
+ };
801
+ export type Content = {
802
+ items: ContentItem[];
803
+ textPrompt?: string;
804
+ };
805
+ export type Image = {
806
+ prompt: string;
807
+ width: string;
808
+ height: string;
809
+ src: string;
810
+ alt: string;
811
+ mediaUuid: string;
812
+ origWidth: number;
813
+ origHeight: number;
814
+ /**
815
+ * Container padding of the image within its slot (card TOP image, column
816
+ * image). Same semantics as ContentItem.padding: client-managed, written
817
+ * by importers / the editor, never exposed to the LLM, round-tripped
818
+ * verbatim. Layouts without a discrete image box (BACKGROUND, LEFT/RIGHT
819
+ * fills) may ignore it.
820
+ */
821
+ padding?: EdgeInsets;
822
+ /**
823
+ * Horizontal alignment when the image is a content item; slots ignore it.
824
+ * UNSPECIFIED renders as center (Unlayer's image default).
825
+ */
826
+ align: TextAlign;
827
+ /**
828
+ * Optional click-through carried by Unlayer image widgets. Empty href =
829
+ * not clickable. Target is normalised to "_self" or "_blank".
830
+ */
831
+ href: string;
832
+ target: string;
833
+ /**
834
+ * Per-language overrides of src / alt (localized banners); same
835
+ * client-managed contract as TextBlock.translations.
836
+ */
837
+ srcTranslations: Record<string, string>;
838
+ altTranslations: Record<string, string>;
839
+ /** Corner rounding, px. Unset = inherit DefaultStyles.image; explicit 0 = square. Client-managed. */
840
+ borderRadius?: number;
841
+ /** Mobile alignment override (content items only). UNSPECIFIED = inherit `align`. Client-managed. */
842
+ mobileAlign: TextAlign;
843
+ /**
844
+ * Mobile width override as a CSS length ("200px" / "60%"). Unset or empty =
845
+ * the desktop width, scaled by the client. Only these two units are emitted;
846
+ * anything else is ignored rather than written into a rule. Client-managed.
847
+ */
848
+ mobileWidth?: string;
849
+ /**
850
+ * Content-item geometry, same contract as AiBuilderColumn's slot; unset size
851
+ * = the item's own width. Ignored in a slot, whose container owns it.
852
+ */
853
+ imageSize?: CardImageSize;
854
+ imageFit?: CardImageFit;
855
+ imagePosition?: CardImagePosition;
856
+ /** Crop box: image height as a percentage of the rendered width. Unset = natural. */
857
+ imageRatio?: number;
858
+ };
859
+ export type AiBuilderCard = {
860
+ layout: CardLayout;
861
+ image?: Image;
862
+ content: Content;
863
+ styles?: CardStyles;
864
+ /**
865
+ * How the image fills its slot. Relevant for layouts that have an
866
+ * image slot: LEFT, RIGHT, TOP, BACKGROUND. Ignored for layout NO.
867
+ * UNSPECIFIED lets the client fall back to its default fit.
868
+ */
869
+ imageFit: CardImageFit;
870
+ /**
871
+ * Size of the image slot (width for LEFT/RIGHT, height for TOP, card
872
+ * height for BACKGROUND; ignored for NO). Client-managed: the LLM never
873
+ * sets it, the backend round-trips it. Unset → client default.
874
+ */
875
+ imageSize?: CardImageSize;
876
+ /**
877
+ * Focus / anchor of the image within its slot (object-position).
878
+ * Client-managed: the LLM never sets it, the backend round-trips it.
879
+ * Unset → centre (50/50). Applies to the image layouts only.
880
+ */
881
+ imagePosition?: CardImagePosition;
882
+ /**
883
+ * LEFT/RIGHT only: float the image so copy flows around it instead of
884
+ * sitting in a separate column. Unset = off, which keeps pre-wrap documents
885
+ * on their two-column look. Client-managed.
886
+ */
887
+ imageWrap?: boolean;
888
+ /**
889
+ * Mobile override of image.padding for the TOP/LEFT/RIGHT slots.
890
+ * Client-managed.
891
+ */
892
+ imageMobilePadding?: EdgeInsets;
893
+ /**
894
+ * LEFT/RIGHT crop box: image height as a percentage of the image column's
895
+ * width. Unset = natural height. Client-managed.
896
+ */
897
+ sideImageRatio?: number;
898
+ };
899
+ export type AiBuilderColumns = {
900
+ columns: AiBuilderColumn[];
901
+ styles?: ColumnsStyles;
902
+ withImages: boolean;
903
+ };
904
+ export type AiBuilderColumn = {
905
+ image?: Image;
906
+ content: Content;
907
+ /** no_null: empty string (not JSON null) is this field's "unset" idiom. */
908
+ width?: string;
909
+ /**
910
+ * Container padding of this column, wrapping BOTH the image slot and the
911
+ * content (Unlayer column padding). Overrides ColumnsStyles.column_padding
912
+ * for this column when set. Client-managed; the LLM never sets it.
913
+ */
914
+ padding?: EdgeInsets;
915
+ /**
916
+ * Slot-image geometry, same contract as the card's: size drives the WIDTH
917
+ * (unset = spans the cell), an explicit height or a crop ratio fixes the
918
+ * box, and the fit crops into it. CLIENT even though the card's image_fit is
919
+ * MODEL — for columns these have always been the editor's, and widening a
920
+ * policy later is safe while narrowing one is not.
921
+ */
922
+ imageSize?: CardImageSize;
923
+ imageFit?: CardImageFit;
924
+ imagePosition?: CardImagePosition;
925
+ /** Crop box: image height as a percentage of the cell width. Unset = natural. */
926
+ imageRatio?: number;
927
+ };
928
+ /**
929
+ * AiBuilderGroup is a structural stack container. Children live as
930
+ * siblings in the same AiBuilderPage with their parent_id pointing at
931
+ * this block's id; the group itself has no copy and no image slot.
932
+ * Allowed in EMAIL and WEB_POPUP.
933
+ *
934
+ * Wire contract:
935
+ * - All fields (direction, align, margin, padding, gap) are
936
+ * client-managed. The backend preserves whatever the client sent
937
+ * verbatim across edits and emits proto-zero / unset for them on
938
+ * freshly generated groups.
939
+ * - UNSPECIFIED enums and unset optionals mean "inherit / use the
940
+ * document default" — see GroupDirection / GroupAlign for the
941
+ * render-time fallback. UNSPECIFIED is NOT wire-equivalent to the
942
+ * concrete default value; downstream consumers that diff documents
943
+ * see them as different states.
944
+ * - The LLM never sees these fields and never sets them; they round-
945
+ * trip through the backend untouched.
946
+ *
947
+ * Recommended client flow:
948
+ * - Default rendering (vertical stack, START alignment, document
949
+ * defaults for margin/padding/gap): leave UNSPECIFIED / unset.
950
+ * Don't normalise to a concrete enum just because that's what the
951
+ * user currently sees.
952
+ * - Send a concrete enum (VERTICAL, START, HORIZONTAL, CENTER, END)
953
+ * only when the user explicitly chose a non-default value, or when
954
+ * the user wants to pin the value against a future default change.
955
+ * - On reset, write back UNSPECIFIED / unset rather than the concrete
956
+ * default — that way the document still diffs cleanly against a
957
+ * freshly generated group.
958
+ */
959
+ export type AiBuilderGroup = {
960
+ direction: GroupDirection;
961
+ /** Outer spacing around the group block. */
962
+ margin?: EdgeInsets;
963
+ /** Inner spacing between the group's frame and its children. */
964
+ padding?: EdgeInsets;
965
+ /**
966
+ * Spacing between adjacent children. UNSPECIFIED (0) lets the
967
+ * client fall back to its default. Use a non-zero value to override.
968
+ */
969
+ gap?: number;
970
+ /**
971
+ * Alignment of children along the axis perpendicular to direction.
972
+ * UNSPECIFIED → render as START.
973
+ */
974
+ align: GroupAlign;
975
+ /**
976
+ * Mobile direction override. UNSPECIFIED = inherit `direction`. Only
977
+ * HORIZONTAL→VERTICAL is honored: a vertical group is table rows in the
978
+ * email, which CSS cannot re-flow into a row. Client-managed.
979
+ */
980
+ mobileDirection: GroupDirection;
981
+ };
982
+ /**
983
+ * Placeholder is a client-created empty slot meant to be replaced by
984
+ * the AI on a follow-up edit. The frontend builder inserts a
985
+ * Placeholder when the user adds a new block to the canvas without
986
+ * yet describing what it should be; on the next AI-edit the model
987
+ * replaces it with a real block kind (card / columns / content /
988
+ * group). Carries no rendered content of its own.
989
+ */
990
+ export type Placeholder = {
991
+ /**
992
+ * Optional user-supplied hint about what the slot should become,
993
+ * e.g. "CTA section with a button" or "product card". The AI uses
994
+ * it as an instruction on the next edit; empty string lets the
995
+ * model pick freely from surrounding context.
996
+ */
997
+ prompt?: string;
998
+ };
999
+ /** Line style for a Divider. UNSPECIFIED renders as solid. */
1000
+ export type DividerStyle = 'DIVIDER_STYLE_UNSPECIFIED' | 'DIVIDER_STYLE_SOLID' | 'DIVIDER_STYLE_DASHED' | 'DIVIDER_STYLE_DOTTED';
1001
+ /**
1002
+ * Divider is a horizontal separator line between sections. It carries no copy
1003
+ * — only line config. All fields are optional with sensible render defaults:
1004
+ * thickness 0 → 1px, empty color → the scheme border colour, style
1005
+ * UNSPECIFIED → solid, width_percent 0 → 100.
1006
+ */
1007
+ export type Divider = {
1008
+ thickness: number;
1009
+ color: string;
1010
+ style: DividerStyle;
1011
+ widthPercent: number;
1012
+ };
1013
+ /**
1014
+ * SyncedBlockRef links a block subtree root back to the saved-block template it was inserted from.
1015
+ * Client-managed end to end: the client refreshes the subtree from the template on load,
1016
+ * pushes edits back via TemplatesService, and re-applies the link after AI edits.
1017
+ */
1018
+ export type SyncedBlockRef = {
1019
+ /** TemplatesService code of the source template. */
1020
+ templateCode: string;
1021
+ /**
1022
+ * RFC3339 updated_at of the source template at last sync — the staleness check on load.
1023
+ * A string, not google.protobuf.Timestamp, on purpose: the value is client-opaque and lives in
1024
+ * documents stored as raw JSON (rpc-v2 smartcards content), where a Timestamp's generated
1025
+ * Date type would lie after a plain JSON revival; only the client ever parses it.
1026
+ */
1027
+ sourceUpdatedAt: string;
1028
+ /**
1029
+ * Companion email template holding the block's rendered fragment. The email
1030
+ * emitter writes `{% email_content "<code>" %}` instead of the baked block,
1031
+ * so the sending pipeline inlines the CURRENT fragment at send time.
1032
+ */
1033
+ fragmentCode: string;
1034
+ };
1035
+ export type DisplayConditionOp =
1036
+ /** Invalid: the rule is skipped at compile, as a malformed rule is today. */
1037
+ 'DISPLAY_CONDITION_OP_UNSPECIFIED' | 'DISPLAY_CONDITION_OP_EQ' | 'DISPLAY_CONDITION_OP_NEQ' | 'DISPLAY_CONDITION_OP_GT' | 'DISPLAY_CONDITION_OP_GTE' | 'DISPLAY_CONDITION_OP_LT' | 'DISPLAY_CONDITION_OP_LTE' | 'DISPLAY_CONDITION_OP_CONTAINS' | 'DISPLAY_CONDITION_OP_NOT_CONTAINS'
1038
+ /** Presence checks; `value` is ignored. */
1039
+ | 'DISPLAY_CONDITION_OP_EXISTS' | 'DISPLAY_CONDITION_OP_NOT_EXISTS';
1040
+ export type DisplayConditionValueType =
1041
+ /** Renders as STRING. */
1042
+ 'DISPLAY_CONDITION_VALUE_TYPE_UNSPECIFIED' | 'DISPLAY_CONDITION_VALUE_TYPE_STRING' | 'DISPLAY_CONDITION_VALUE_TYPE_NUMBER' | 'DISPLAY_CONDITION_VALUE_TYPE_BOOLEAN';
1043
+ export type DisplayConditionCombinator =
1044
+ /** Renders as AND. */
1045
+ 'DISPLAY_CONDITION_COMBINATOR_UNSPECIFIED' | 'DISPLAY_CONDITION_COMBINATOR_AND' | 'DISPLAY_CONDITION_COMBINATOR_OR';
1046
+ /** What to do for a recipient missing a referenced value. UNSPECIFIED = HIDE. */
1047
+ export type DisplayConditionUnknown = 'DISPLAY_CONDITION_UNKNOWN_UNSPECIFIED' | 'DISPLAY_CONDITION_UNKNOWN_HIDE' | 'DISPLAY_CONDITION_UNKNOWN_SHOW';
1048
+ /**
1049
+ * Branch role in an if/elsif/else chain of ADJACENT siblings; exactly one
1050
+ * branch renders. UNSPECIFIED = a standalone {% if %}.
1051
+ */
1052
+ export type DisplayConditionChain = 'DISPLAY_CONDITION_CHAIN_UNSPECIFIED' | 'DISPLAY_CONDITION_CHAIN_ELSIF' | 'DISPLAY_CONDITION_CHAIN_ELSE';
1053
+ export type DisplayConditionRule = {
1054
+ /**
1055
+ * Tag / dynamic-content variable exactly as the user typed it; the emitter
1056
+ * strips spaces to reach the Liquid binding name.
1057
+ */
1058
+ name: string;
1059
+ op: DisplayConditionOp;
1060
+ /** Raw input, parsed per value_type at compile. Ignored for EXISTS/NOT_EXISTS. */
1061
+ value: string;
1062
+ valueType: DisplayConditionValueType;
1063
+ };
1064
+ export type DisplayCondition = {
1065
+ /** False keeps the rules stored without emitting anything. */
1066
+ enabled: boolean;
1067
+ combinator: DisplayConditionCombinator;
1068
+ rules: DisplayConditionRule[];
1069
+ unknown: DisplayConditionUnknown;
1070
+ /**
1071
+ * Advanced mode: a raw Liquid boolean expression. Non-empty REPLACES `rules`
1072
+ * and is emitted verbatim — never escaped, never validated server-side.
1073
+ * "" = unset.
1074
+ */
1075
+ liquidExpr: string;
1076
+ chain: DisplayConditionChain;
1077
+ };
1078
+ export type ProductsSource =
1079
+ /** Renders as MANUAL. */
1080
+ 'PRODUCTS_SOURCE_UNSPECIFIED' | 'PRODUCTS_SOURCE_MANUAL' | 'PRODUCTS_SOURCE_RULE' | 'PRODUCTS_SOURCE_CART';
1081
+ export type ProductsRuleSort =
1082
+ /** Invalid: the rule degrades to MANUAL, as a malformed payload does today. */
1083
+ 'PRODUCTS_RULE_SORT_UNSPECIFIED' | 'PRODUCTS_RULE_SORT_NEWEST' | 'PRODUCTS_RULE_SORT_PRICE_ASC' | 'PRODUCTS_RULE_SORT_PRICE_DESC' | 'PRODUCTS_RULE_SORT_TITLE';
1084
+ /**
1085
+ * Catalog feed. A payload missing feed_origin, sort or a positive limit
1086
+ * degrades to MANUAL rather than emitting a call to a garbage URL.
1087
+ */
1088
+ export type ProductsRule = {
1089
+ application: string;
1090
+ /** Environment-resolved feed host. "" = unset → degrades to MANUAL. */
1091
+ feedOrigin: string;
1092
+ /** "" = unset (the query parameter is omitted). */
1093
+ category: string;
1094
+ sort: ProductsRuleSort;
1095
+ /** Grid cells emitted, and the feed limit. <= 0 → degrades to MANUAL. */
1096
+ limit: number;
1097
+ };
1098
+ /** Abandoned cart of the recipient. Same degradation contract as ProductsRule. */
1099
+ export type ProductsCart = {
1100
+ application: string;
1101
+ feedOrigin: string;
1102
+ limit: number;
1103
+ };
1104
+ /**
1105
+ * One product row. The card design lives in ProductsData.template; elements
1106
+ * whose plain text is exactly "$title" / "$description" / "$price" /
1107
+ * "$oldPrice" bind to these fields, as do an image src of "$image" and a
1108
+ * button url of "$url".
1109
+ */
1110
+ export type ProductItem = {
1111
+ /** Account catalog id when the row was picked from the catalog. "" = unset. */
1112
+ catalogId: string;
1113
+ image?: Image;
1114
+ title: string;
1115
+ titleTranslations: Record<string, string>;
1116
+ description: string;
1117
+ descriptionTranslations: Record<string, string>;
1118
+ price: string;
1119
+ oldPrice: string;
1120
+ url: string;
1121
+ };
1122
+ export type ProductsData = {
1123
+ source: ProductsSource;
1124
+ rule?: ProductsRule;
1125
+ cart?: ProductsCart;
1126
+ /**
1127
+ * Rendered-description cap in characters. Unset = renderer default,
1128
+ * explicit 0 = off.
1129
+ */
1130
+ descriptionLimit?: number;
1131
+ /**
1132
+ * Rows for MANUAL, and the baked preview rows shown for RULE / CART outside
1133
+ * the save path.
1134
+ */
1135
+ products: ProductItem[];
1136
+ /**
1137
+ * The card rendered once per row. Absent = the block falls back to the
1138
+ * stand-in card in the oneof.
1139
+ */
1140
+ template?: AiBuilderCard;
1141
+ templateStyles?: BlockStyles;
1142
+ /** Grid columns, clamped 1..3 by the renderer. Unset = renderer default. */
1143
+ columns?: number;
1144
+ /** Gap between cells in px. Unset = renderer default. */
1145
+ gap?: number;
1146
+ };
1147
+ /**
1148
+ * One article row of a news repeater; the card design lives in
1149
+ * NewsData.template and binds these fields through "$field" refs.
1150
+ */
1151
+ export type NewsItem = {
1152
+ /** Feed guid / atom id — identity for "already in the block" checks. "" = unset. */
1153
+ id: string;
1154
+ image?: Image;
1155
+ title: string;
1156
+ titleTranslations: Record<string, string>;
1157
+ summary: string;
1158
+ summaryTranslations: Record<string, string>;
1159
+ /**
1160
+ * Display string as baked ("11 Aug 2026"); published_at keeps the ISO one
1161
+ * so a language switch can reformat it.
1162
+ */
1163
+ date: string;
1164
+ publishedAt: string;
1165
+ author: string;
1166
+ category: string;
1167
+ url: string;
1168
+ };
1169
+ /**
1170
+ * News repeater: one card template per article, baked at edit time from the
1171
+ * block's RSS/Atom feed.
1172
+ */
1173
+ export type NewsData = {
1174
+ /** Feed the articles were taken from, kept for the panel's Refresh. */
1175
+ feedUrl: string;
1176
+ /**
1177
+ * Rendered-summary cap in characters. Unset = renderer default,
1178
+ * explicit 0 = off.
1179
+ */
1180
+ summaryLimit?: number;
1181
+ items: NewsItem[];
1182
+ /**
1183
+ * The card rendered once per article. Absent = the block falls back to the
1184
+ * stand-in card in the oneof.
1185
+ */
1186
+ template?: AiBuilderCard;
1187
+ templateStyles?: BlockStyles;
1188
+ /** Grid columns, clamped 1..3 by the renderer. Unset = renderer default. */
1189
+ columns?: number;
1190
+ /** Gap between cells in px. Unset = renderer default. */
1191
+ gap?: number;
1192
+ };
1193
+ export type AiBuilderBlock_block_card = {
1194
+ type: 'card';
1195
+ data: AiBuilderCard;
1196
+ };
1197
+ export type AiBuilderBlock_block_columns = {
1198
+ type: 'columns';
1199
+ data: AiBuilderColumns;
1200
+ };
1201
+ export type AiBuilderBlock_block_content = {
1202
+ type: 'content';
1203
+ data: Content;
1204
+ };
1205
+ export type AiBuilderBlock_block_group = {
1206
+ type: 'group';
1207
+ data: AiBuilderGroup;
1208
+ };
1209
+ export type AiBuilderBlock_block_placeholder = {
1210
+ type: 'placeholder';
1211
+ data: Placeholder;
1212
+ };
1213
+ export type AiBuilderBlock_block_divider = {
1214
+ type: 'divider';
1215
+ data: Divider;
1216
+ };
1217
+ export type AiBuilderBlock_block = AiBuilderBlock_block_card | AiBuilderBlock_block_columns | AiBuilderBlock_block_content | AiBuilderBlock_block_group | AiBuilderBlock_block_placeholder | AiBuilderBlock_block_divider;
1218
+ export type AiBuilderBlock = {
1219
+ id: string;
1220
+ styles?: BlockStyles;
1221
+ /** Synced-block link; present only on the root of an instance's subtree. */
1222
+ syncedRef?: SyncedBlockRef;
1223
+ /**
1224
+ * Id of the block-of-this-page that contains this one. Empty string
1225
+ * marks a top-level block. The parent (if set) must be a container
1226
+ * kind in the same AiBuilderPage. Containers may nest to arbitrary
1227
+ * depth; the server rejects trees deeper than 16 levels with
1228
+ * InvalidArgument (a sanity cap, not a product limit). Per-mode
1229
+ * soft hints in the planner prompt steer the model toward shallower
1230
+ * structures appropriate for each channel.
1231
+ */
1232
+ parentId: string;
1233
+ /** Per-recipient visibility of this block, compiled to Liquid on save. */
1234
+ displayCondition?: DisplayCondition;
1235
+ /**
1236
+ * Products repeater. When set it OWNS the block's rendering and the oneof
1237
+ * holds a stand-in card, so the model (and an older editor) still sees a
1238
+ * valid block.
1239
+ */
1240
+ products?: ProductsData;
1241
+ /**
1242
+ * News repeater. Same ownership contract as products: when set it OWNS the
1243
+ * block's rendering and the oneof holds a stand-in card.
1244
+ */
1245
+ news?: NewsData;
1246
+ block: AiBuilderBlock_block;
1247
+ };
1248
+ /**
1249
+ * Meta carries channel-agnostic document-level metadata generated by
1250
+ * the meta generator. Channel-specific fields (email subject /
1251
+ * preheader, future popup-only / inapp-only fields) live in their own
1252
+ * optional sub-messages on AiBuilderDocument (EmailSettings,
1253
+ * PopupSettings, ...) so this message stays small and uniform across
1254
+ * all modes.
1255
+ */
1256
+ export type Meta = {
1257
+ templateName: string;
1258
+ };
1259
+ /**
1260
+ * EmailType classifies a document as a commercial (marketing) message or a
1261
+ * transactional / relationship message. It drives footer compliance: marketing
1262
+ * emails must carry an unsubscribe link + postal address (CAN-SPAM), while
1263
+ * transactional ones (receipts, password resets, policy updates) are exempt.
1264
+ * The content generator classifies this from the campaign intent (see the
1265
+ * content-fill _meta); the client may override.
1266
+ */
1267
+ export type EmailType =
1268
+ /** treated as MARKETING (safe default) */
1269
+ 'EMAIL_TYPE_UNSPECIFIED' | 'EMAIL_TYPE_MARKETING' | 'EMAIL_TYPE_TRANSACTIONAL';
1270
+ export type SocialPlatform = 'SOCIAL_PLATFORM_UNSPECIFIED' | 'SOCIAL_PLATFORM_FACEBOOK' | 'SOCIAL_PLATFORM_X' | 'SOCIAL_PLATFORM_INSTAGRAM' | 'SOCIAL_PLATFORM_LINKEDIN' | 'SOCIAL_PLATFORM_YOUTUBE' | 'SOCIAL_PLATFORM_TIKTOK'
1271
+ /** The hosted icon set (pushwoosh.com/social-icons) already covers these. */
1272
+ | 'SOCIAL_PLATFORM_DISCORD' | 'SOCIAL_PLATFORM_GITHUB' | 'SOCIAL_PLATFORM_TELEGRAM' | 'SOCIAL_PLATFORM_THREADS' | 'SOCIAL_PLATFORM_WHATSAPP';
1273
+ export type AppStorePlatform = 'APP_STORE_PLATFORM_UNSPECIFIED' | 'APP_STORE_PLATFORM_APP_STORE' | 'APP_STORE_PLATFORM_GOOGLE_PLAY';
1274
+ export type NavLink = {
1275
+ label: string;
1276
+ url: string;
1277
+ /** Per-language overrides of `label`; same contract as MenuItem.translations. */
1278
+ translations: Record<string, string>;
1279
+ /** Per-language overrides of `url` (e.g. locale-specific Privacy Policy). */
1280
+ urlTranslations: Record<string, string>;
1281
+ };
1282
+ export type SocialLink = {
1283
+ platform: SocialPlatform;
1284
+ url: string;
1285
+ };
1286
+ export type AppBadge = {
1287
+ platform: AppStorePlatform;
1288
+ url: string;
1289
+ };
1290
+ /** Reusable brand footer data; never LLM-generated. */
1291
+ export type FooterContent = {
1292
+ companyName: string;
1293
+ address: string;
1294
+ copyright: string;
1295
+ unsubscribeLabel: string;
1296
+ links: NavLink[];
1297
+ social: SocialLink[];
1298
+ appBadges: AppBadge[];
1299
+ /**
1300
+ * Per-language overrides; same contract as EmailSettings.subject_translations.
1301
+ * URLs (links/social/badges) are the only fields shared across languages.
1302
+ */
1303
+ unsubscribeLabelTranslations: Record<string, string>;
1304
+ copyrightTranslations: Record<string, string>;
1305
+ companyNameTranslations: Record<string, string>;
1306
+ addressTranslations: Record<string, string>;
1307
+ /**
1308
+ * Brand logo shown at the top of the footer. Absolute image URL (email
1309
+ * clients need hosted raster assets); empty = no logo.
1310
+ */
1311
+ logoUrl?: string;
1312
+ /** Rendered logo width in px; 0/unset = the renderer default. */
1313
+ logoWidth?: number;
1314
+ /** Click-through URL for the logo; empty = plain image. */
1315
+ logoLink?: string;
1316
+ };
1317
+ /**
1318
+ * Per-template footer presentation. Deliberately separate from
1319
+ * FooterContent: FooterContent is reusable brand DATA (mirrored by the
1320
+ * account-level EmailFooter in rpc-v2), while these are render
1321
+ * preferences of one template. Client-managed; unset fields keep the
1322
+ * renderer defaults (muted gray, 12px, centered, underlined links).
1323
+ */
1324
+ export type FooterStyle = {
1325
+ /** #RGB/#RRGGBB hex; empty = renderer default (#8A94A6). */
1326
+ textColor?: string;
1327
+ /** px; 0/unset = renderer default (12). */
1328
+ fontSize?: number;
1329
+ /** UNSPECIFIED = renderer default (center). */
1330
+ align: TextAlign;
1331
+ /**
1332
+ * Underline the footer's text links (nav links, unsubscribe, web-version).
1333
+ * Optional so absent keeps the client default (underlined).
1334
+ */
1335
+ underlineLinks?: boolean;
1336
+ /**
1337
+ * Look of the social icons; both UNSPECIFIED reproduce today's defaults
1338
+ * (circle / black), so an absent value renders as it always has.
1339
+ */
1340
+ socialIconShape: SocialIconShape;
1341
+ socialIconVariant: SocialIconVariant;
1342
+ };
1343
+ /**
1344
+ * Renderer picks a hosted icon set from these two. The value names are not
1345
+ * cosmetic: they are the asset path segment and filename suffix
1346
+ * (<base>/<shape>/<platform>-<shape>-<variant>.png), so an invented value is a
1347
+ * 404, not a fallback.
1348
+ */
1349
+ export type SocialIconShape =
1350
+ /** Renders as CIRCLE. */
1351
+ 'SOCIAL_ICON_SHAPE_UNSPECIFIED' | 'SOCIAL_ICON_SHAPE_CIRCLE' | 'SOCIAL_ICON_SHAPE_SQUARE' | 'SOCIAL_ICON_SHAPE_REGULAR';
1352
+ export type SocialIconVariant =
1353
+ /** Renders as BLACK. */
1354
+ 'SOCIAL_ICON_VARIANT_UNSPECIFIED' | 'SOCIAL_ICON_VARIANT_BLACK' | 'SOCIAL_ICON_VARIANT_WHITE' | 'SOCIAL_ICON_VARIANT_COLOR';
1355
+ /**
1356
+ * EmailSettings holds the email-only document-level fields produced by
1357
+ * the meta generator under mode=EMAIL. Populated only when the
1358
+ * document was generated for the email channel; absent otherwise.
1359
+ */
1360
+ export type EmailSettings = {
1361
+ subject?: string;
1362
+ preheader?: string;
1363
+ /**
1364
+ * Marketing vs transactional. Classified by the content generator from the
1365
+ * campaign intent; drives footer compliance (see EmailType). The client may
1366
+ * override.
1367
+ */
1368
+ emailType: EmailType;
1369
+ /**
1370
+ * Reusable brand footer (company / address / links / …). Client-managed —
1371
+ * never LLM-generated. Absent = no footer configured yet.
1372
+ */
1373
+ footer?: FooterContent;
1374
+ /**
1375
+ * Thin "View in browser" strip at the top, linking %%PW_EMAIL_WEB_LINK%%.
1376
+ * optional: needs presence so an unset value stays absent (client default on)
1377
+ * instead of collapsing to proto3 false on save.
1378
+ */
1379
+ showWebVersion?: boolean;
1380
+ /**
1381
+ * Per-language overrides of subject / preheader; the default language
1382
+ * lives in the scalar fields. Client-managed, hidden from the LLM.
1383
+ */
1384
+ subjectTranslations: Record<string, string>;
1385
+ preheaderTranslations: Record<string, string>;
1386
+ /**
1387
+ * Managed unsubscribe link in the document footer. It is rendered only for
1388
+ * marketing email. Optional so absent keeps the client default (on).
1389
+ */
1390
+ showUnsubscribe?: boolean;
1391
+ /**
1392
+ * Footer presentation overrides for this template. Absent = renderer
1393
+ * defaults. Client-managed.
1394
+ */
1395
+ footerStyle?: FooterStyle;
1396
+ };
1397
+ /**
1398
+ * PopupAnimation chooses how a web-popup enters / exits the
1399
+ * viewport. UNSPECIFIED leaves the choice to the client default
1400
+ * (typically NONE or FADE). Client-managed: the LLM never selects an
1401
+ * animation.
1402
+ */
1403
+ export type PopupAnimation = 'POPUP_ANIMATION_UNSPECIFIED' | 'POPUP_ANIMATION_NONE' | 'POPUP_ANIMATION_FADE' | 'POPUP_ANIMATION_SLIDE';
1404
+ /**
1405
+ * PopupOverlay describes the backdrop drawn behind the popup. Both
1406
+ * fields are client-managed; the LLM does not see them.
1407
+ */
1408
+ export type PopupOverlay = {
1409
+ /** Hex color, e.g. "#000000". Empty string = no overlay. */
1410
+ color: string;
1411
+ /** 0.0 (fully transparent) to 1.0 (opaque). */
1412
+ opacity: number;
1413
+ };
1414
+ /**
1415
+ * PopupSettings holds the web-popup channel's client-managed UI
1416
+ * configuration on AiBuilderDocument. Populated by the client (the
1417
+ * editor) and preserved across edits by the server through the same
1418
+ * flow as AiBuilderGroup's client-managed fields: the LLM never sees
1419
+ * these values, server-side conversion never writes to them, the
1420
+ * client owns them end-to-end.
1421
+ */
1422
+ export type PopupSettings = {
1423
+ /** Corner radius of the popup container, in px. 0 = sharp corners. */
1424
+ borderRadius: number;
1425
+ /**
1426
+ * CSS box-shadow string, e.g. "0 4px 24px rgba(0,0,0,0.2)". Empty
1427
+ * string = no shadow.
1428
+ */
1429
+ boxShadow: string;
1430
+ /** Backdrop overlay. Absent = no overlay. */
1431
+ overlay?: PopupOverlay;
1432
+ /** Enter / exit animation. UNSPECIFIED = client default. */
1433
+ animation: PopupAnimation;
1434
+ /**
1435
+ * Edge offsets in px (CSS-style positioning). Any subset of
1436
+ * top / bottom / left / right may be set. When none are set the
1437
+ * popup is centered in the viewport. Typical combinations:
1438
+ * - centered: leave all unset
1439
+ * - bottom-left: set bottom + left (with the desired gutter)
1440
+ * - top-right: set top + right
1441
+ * - full-bleed
1442
+ * overlay band: set both top + bottom (and/or left + right);
1443
+ * the client decides how to fill the gap.
1444
+ * Conflicting pairs (top + bottom, left + right) are permitted on
1445
+ * the wire — the client decides whether to stretch the popup
1446
+ * between the two edges or to honour one of them.
1447
+ */
1448
+ top?: number;
1449
+ bottom?: number;
1450
+ left?: number;
1451
+ right?: number;
1452
+ };
1453
+ /**
1454
+ * AiBuilderPage groups a list of blocks under a single logical page. For
1455
+ * now every document has exactly one page; the structure is in place so
1456
+ * multi-page documents (landing pages with several screens, email digests
1457
+ * with tabs, etc.) can be added without a further schema break.
1458
+ */
1459
+ export type AiBuilderPage = {
1460
+ id: string;
1461
+ blocks: AiBuilderBlock[];
1462
+ };
1463
+ export type AiBuilderDocument = {
1464
+ /** MVP: always one page. Consumers may assume pages[0] exists. */
1465
+ pages: AiBuilderPage[];
1466
+ settings: DocumentSettings;
1467
+ textVariants: Record<string, TextVariantStyle>;
1468
+ colorScheme: Record<string, ColorSchemeItem>;
1469
+ meta: Meta;
1470
+ /**
1471
+ * Email channel-specific fields. Populated only when the document
1472
+ * was generated for mode=EMAIL.
1473
+ */
1474
+ emailSettings?: EmailSettings;
1475
+ /**
1476
+ * Web popup channel-specific fields. Populated only when the
1477
+ * document was generated for mode=WEB_POPUP.
1478
+ */
1479
+ popupSettings?: PopupSettings;
1480
+ /**
1481
+ * Languages this document is edited in, "default" first, then language
1482
+ * codes in a stable order. Drives the editor's language switcher; a
1483
+ * language may be listed before any translation exists. Client-managed.
1484
+ */
1485
+ languages: string[];
1486
+ /**
1487
+ * Authored dark palette, paired with color_scheme. Absent = the document
1488
+ * opts out of dark mode; see DarkColorScheme. Client-managed.
1489
+ */
1490
+ darkColorScheme?: DarkColorScheme;
1491
+ };