@hyper-elements/react 0.0.1 → 0.3.1

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,3572 @@
1
+ import React from 'react';
2
+ import * as defaultBuild from 'react-dom/server';
3
+
4
+ // src/inspect/index.ts
5
+
6
+ // ../core/dist/index.js
7
+ function isNode(child) {
8
+ return typeof child === "object" && child !== null;
9
+ }
10
+ var Diagnostics = class {
11
+ constructor() {
12
+ this.list = [];
13
+ }
14
+ add(code, severity, message, path, nodeId, extra = {}) {
15
+ const d = { code, severity, message, path };
16
+ if (nodeId !== void 0) d.nodeId = nodeId;
17
+ if (extra.clients) d.clients = [...extra.clients];
18
+ if (extra.fix) d.fix = extra.fix;
19
+ if (extra.see) d.see = extra.see;
20
+ this.list.push(d);
21
+ }
22
+ error(code, message, path, nodeId) {
23
+ this.add(code, "error", message, path, nodeId);
24
+ }
25
+ warning(code, message, path, nodeId, extra) {
26
+ this.add(code, "warning", message, path, nodeId, extra);
27
+ }
28
+ get hasErrors() {
29
+ return this.list.some((d) => d.severity === "error");
30
+ }
31
+ };
32
+ var captured = (against, wordsWithin4px, lineBreaks) => ({ elements: "0.3.0", against, captured: "2026-11-08", wordsWithin4px, lineBreaks });
33
+ var tables = (name) => ({ name, widths: {}, dark: false, approximate: true, measured: null });
34
+ var EMAIL_CLIENTS = {
35
+ outlook: { name: "Classic Outlook", widths: { desktop: 820 }, dark: false, approximate: true, measured: captured("Outlook 16.0.20527 for Windows, 96 DPI", 0.85, 0.983) },
36
+ "outlook-com": { name: "Outlook.com and new Outlook", widths: { desktop: 820 }, dark: false, approximate: false, measured: captured("Outlook on the web", 1, 1) },
37
+ "outlook-mac": tables("Outlook for Mac"),
38
+ "outlook-app": tables("Outlook app (iOS and Android)"),
39
+ gmail: { name: "Gmail", widths: { desktop: 820 }, dark: false, approximate: false, measured: captured("Gmail on the web", 1, 1) },
40
+ "gmail-app": { name: "Gmail app", widths: { phone: 375 }, dark: false, approximate: true, measured: null },
41
+ "gmail-app-other": { name: "Gmail app, another account", widths: { phone: 375 }, dark: false, approximate: true, measured: null },
42
+ "apple-mail": { name: "Apple Mail", widths: { desktop: 820 }, dark: true, approximate: false, measured: captured("Mail 16 on macOS 26", 0.999, 0.999) },
43
+ yahoo: tables("Yahoo Mail"),
44
+ aol: tables("AOL Mail"),
45
+ "samsung-email": tables("Samsung Email"),
46
+ thunderbird: tables("Thunderbird"),
47
+ protonmail: tables("Proton Mail"),
48
+ fastmail: tables("Fastmail"),
49
+ hey: tables("HEY"),
50
+ gmx: tables("GMX"),
51
+ "web-de": tables("WEB.DE"),
52
+ orange: tables("Orange"),
53
+ sfr: tables("SFR"),
54
+ laposte: tables("La Poste"),
55
+ "free-fr": tables("Free"),
56
+ "t-online-de": tables("T-Online"),
57
+ "mail-ru": tables("Mail.ru"),
58
+ ionos: tables("IONOS (1&1)"),
59
+ "wp-pl": tables("WP Poczta"),
60
+ rainloop: tables("RainLoop")
61
+ };
62
+ var DEFAULT_EMAIL_CLIENTS = ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "apple-mail", "yahoo", "aol", "samsung-email"];
63
+ var caniemail = (feature) => `https://www.caniemail.com/features/${feature}/`;
64
+ function checkEmailClients(clients) {
65
+ if (clients === void 0) return;
66
+ const unknown = Array.isArray(clients) ? clients.filter((c) => !Object.prototype.hasOwnProperty.call(EMAIL_CLIENTS, c)) : [clients];
67
+ if (unknown.length) throw new TypeError(`[Elements] emailClients takes a list of ${Object.keys(EMAIL_CLIENTS).join(", ")}, not ${unknown.map((c) => JSON.stringify(c)).join(", ")}`);
68
+ }
69
+ function forEmailClients(diagnostics, selection = DEFAULT_EMAIL_CLIENTS) {
70
+ const out = [];
71
+ const seen = /* @__PURE__ */ new Map();
72
+ for (const d of diagnostics) {
73
+ if (!d.clients) {
74
+ out.push(d);
75
+ continue;
76
+ }
77
+ const clients = d.clients.filter((c) => selection.includes(c));
78
+ if (!clients.length) continue;
79
+ const key = JSON.stringify([d.code, d.path, d.severity, d.message]);
80
+ const same4 = seen.get(key);
81
+ if (same4) same4.clients = [.../* @__PURE__ */ new Set([...same4.clients, ...clients])];
82
+ else {
83
+ const kept = { ...d, clients };
84
+ seen.set(key, kept);
85
+ out.push(kept);
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+ var ok = (value) => ({ ok: true, value });
91
+ var fail = (message) => ({ ok: false, message });
92
+ var show = (input) => {
93
+ try {
94
+ return typeof input === "string" ? JSON.stringify(input) : typeof input === "function" ? "a function" : JSON.stringify(input) ?? String(input);
95
+ } catch {
96
+ return String(input);
97
+ }
98
+ };
99
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
100
+ function kind(spec) {
101
+ return spec;
102
+ }
103
+ var NUMBER = "-?(?:\\d+(?:\\.\\d*)?|\\.\\d+)";
104
+ var LENGTH = new RegExp(`^(${NUMBER})(px|%|pt|mm|in)?$`);
105
+ function readLength(input) {
106
+ if (typeof input === "number") return Number.isFinite(input) ? { value: input, unit: "px" } : void 0;
107
+ if (typeof input !== "string") return void 0;
108
+ const match = LENGTH.exec(input.trim());
109
+ return match ? { value: Number(match[1]), unit: match[2] ?? "px" } : void 0;
110
+ }
111
+ function px(input, min, what) {
112
+ const length3 = readLength(input);
113
+ if (!length3 || length3.unit !== "px") return fail(`${what} must be a number of px or a px string ("8px"), not ${show(input)}`);
114
+ if (length3.value < min) return fail(`${what} can't be less than ${min}, not ${show(input)}`);
115
+ return ok(length3.value);
116
+ }
117
+ function length(units = ["px"], options = {}) {
118
+ const min = options.min ?? 0;
119
+ const accepts = `a length (${units.map((u) => u === "px" ? "a number of px" : u).join(", ")})`;
120
+ return kind({
121
+ name: "length",
122
+ editor: "length",
123
+ accepts,
124
+ parse(input) {
125
+ const parsed = readLength(input);
126
+ if (!parsed) return fail(`expected ${accepts}, not ${show(input)}`);
127
+ if (!units.includes(parsed.unit)) return fail(`${parsed.unit} isn't accepted here; expected ${accepts}`);
128
+ if (parsed.value < min) return fail(`can't be less than ${min}, not ${show(input)}`);
129
+ return ok(parsed.unit === "px" ? parsed.value : `${parsed.value}${parsed.unit}`);
130
+ },
131
+ jsonSchema: () => ({
132
+ anyOf: [
133
+ ...units.includes("px") ? [{ type: "number", minimum: min }] : [],
134
+ { type: "string", pattern: `^${NUMBER}(${units.map((u) => u === "%" ? "%" : u).join("|")})${units.includes("px") ? "?" : ""}$` }
135
+ ]
136
+ })
137
+ });
138
+ }
139
+ function number(options = {}) {
140
+ const { min = -Infinity, max = Infinity, integer = false } = options;
141
+ const accepts = `${integer ? "an integer" : "a number"}${Number.isFinite(min) ? ` from ${min}` : ""}${Number.isFinite(max) ? ` to ${max}` : ""}`;
142
+ return kind({
143
+ name: "number",
144
+ editor: "number",
145
+ accepts,
146
+ parse(input) {
147
+ if (typeof input !== "number" || !Number.isFinite(input) || integer && !Number.isInteger(input) || input < min || input > max) {
148
+ return fail(`expected ${accepts}, not ${show(input)}`);
149
+ }
150
+ return ok(input);
151
+ },
152
+ jsonSchema: () => ({
153
+ type: integer ? "integer" : "number",
154
+ ...Number.isFinite(min) ? { minimum: min } : {},
155
+ ...Number.isFinite(max) ? { maximum: max } : {}
156
+ })
157
+ });
158
+ }
159
+ function flex() {
160
+ return kind({
161
+ name: "flex",
162
+ editor: "number",
163
+ accepts: 'a share of the free width (0 or more), or "none"',
164
+ parse: (input) => input === "none" || typeof input === "number" && Number.isFinite(input) && input >= 0 ? ok(input) : fail(`expected a share of the free width (0 or more) or "none", not ${show(input)}`),
165
+ jsonSchema: () => ({ anyOf: [{ type: "number", minimum: 0 }, { const: "none" }] })
166
+ });
167
+ }
168
+ var SIDES = ["top", "right", "bottom", "left"];
169
+ function fourSides(values) {
170
+ const [a2, b = a2, c = a2, d = b] = values;
171
+ return [a2, b, c, d];
172
+ }
173
+ function insets() {
174
+ const accepts = 'px for all sides, "8px 16px" (1 to 4 values), [v, h] or [t, r, b, l], or { top, right, bottom, left }';
175
+ const read2 = (values) => {
176
+ if (values.length < 1 || values.length > 4) return fail(`expected ${accepts}`);
177
+ const out = [];
178
+ for (const value of values) {
179
+ const side2 = px(value, 0, "each side");
180
+ if (!side2.ok) return side2;
181
+ out.push(side2.value);
182
+ }
183
+ const [top, right, bottom, left] = fourSides(out);
184
+ return ok({ top, right, bottom, left });
185
+ };
186
+ return kind({
187
+ name: "insets",
188
+ editor: "insets",
189
+ accepts,
190
+ parse(input) {
191
+ if (typeof input === "number") return read2([input]);
192
+ if (typeof input === "string") return read2(input.trim().split(/\s+/));
193
+ if (Array.isArray(input)) return read2(input);
194
+ if (isObject(input)) {
195
+ const extra = Object.keys(input).find((key) => !SIDES.includes(key));
196
+ if (extra) return fail(`"${extra}" isn't a side; expected top, right, bottom or left`);
197
+ const out = { top: 0, right: 0, bottom: 0, left: 0 };
198
+ for (const side2 of SIDES) {
199
+ if (input[side2] === void 0) continue;
200
+ const value = px(input[side2], 0, side2);
201
+ if (!value.ok) return value;
202
+ out[side2] = value.value;
203
+ }
204
+ return ok(out);
205
+ }
206
+ return fail(`expected ${accepts}, not ${show(input)}`);
207
+ },
208
+ jsonSchema: () => {
209
+ const side2 = { anyOf: [{ type: "number", minimum: 0 }, { type: "string", pattern: `^${NUMBER}(px)?$` }] };
210
+ return {
211
+ anyOf: [
212
+ side2.anyOf[0],
213
+ { type: "string" },
214
+ { type: "array", items: side2, minItems: 1, maxItems: 4 },
215
+ { type: "object", properties: Object.fromEntries(SIDES.map((s) => [s, side2])), additionalProperties: false }
216
+ ]
217
+ };
218
+ }
219
+ });
220
+ }
221
+ var BORDER_STYLES = ["solid", "dashed", "dotted", "none"];
222
+ function borderSide(input) {
223
+ const out = { width: 1, style: "solid", color: "#000000" };
224
+ if (typeof input === "string") {
225
+ for (const token of input.match(/rgba?\([^)]*\)|\S+/g) ?? []) {
226
+ if (readLength(token)) {
227
+ const width = px(token, 0, "a border's width");
228
+ if (!width.ok) return width;
229
+ out.width = width.value;
230
+ } else if (BORDER_STYLES.includes(token)) {
231
+ out.style = token;
232
+ } else {
233
+ const c = parseColor(token);
234
+ if (!c.ok) return fail(`"${token}" in ${show(input)} isn't a width, a style (${BORDER_STYLES.join(", ")}) or a color`);
235
+ out.color = c.value;
236
+ }
237
+ }
238
+ return ok(out);
239
+ }
240
+ if (!isObject(input)) return fail(`expected "1px solid #ccc" or { width, style, color }, not ${show(input)}`);
241
+ const extra = Object.keys(input).find((key) => !["width", "style", "color"].includes(key));
242
+ if (extra) return fail(`"${extra}" isn't part of a border side; expected width, style or color`);
243
+ if (input.width !== void 0) {
244
+ const width = px(input.width, 0, "a border's width");
245
+ if (!width.ok) return width;
246
+ out.width = width.value;
247
+ }
248
+ if (input.style !== void 0) {
249
+ if (!BORDER_STYLES.includes(input.style)) return fail(`a border's style is one of ${BORDER_STYLES.join(", ")}, not ${show(input.style)}`);
250
+ out.style = input.style;
251
+ }
252
+ if (input.color !== void 0) {
253
+ const c = parseColor(input.color);
254
+ if (!c.ok) return c;
255
+ out.color = c.value;
256
+ }
257
+ return ok(out);
258
+ }
259
+ function line() {
260
+ return kind({
261
+ name: "line",
262
+ editor: "border",
263
+ accepts: '"1px solid #ccc" or { width, style, color }',
264
+ parse: (input) => borderSide(input),
265
+ jsonSchema: () => ({
266
+ anyOf: [
267
+ { type: "string" },
268
+ {
269
+ type: "object",
270
+ properties: { width: { anyOf: [{ type: "number", minimum: 0 }, { type: "string" }] }, style: { enum: [...BORDER_STYLES] }, color: { type: "string" } },
271
+ additionalProperties: false
272
+ }
273
+ ]
274
+ })
275
+ });
276
+ }
277
+ function border() {
278
+ return kind({
279
+ name: "border",
280
+ editor: "border",
281
+ accepts: '"1px solid #ccc", { width, style, color } for all sides, or { top, right, bottom, left } with one of those each',
282
+ parse(input) {
283
+ const perSide = isObject(input) && Object.keys(input).some((key) => SIDES.includes(key));
284
+ if (!perSide) {
285
+ const side2 = borderSide(input);
286
+ return side2.ok ? ok({ top: side2.value, right: side2.value, bottom: side2.value, left: side2.value }) : side2;
287
+ }
288
+ const out = {};
289
+ for (const [key, value] of Object.entries(input)) {
290
+ if (!SIDES.includes(key)) return fail(`"${key}" isn't a side; expected top, right, bottom or left`);
291
+ const side2 = borderSide(value);
292
+ if (!side2.ok) return fail(`${key}: ${side2.message}`);
293
+ out[key] = side2.value;
294
+ }
295
+ return ok(out);
296
+ },
297
+ jsonSchema: () => {
298
+ const side2 = {
299
+ anyOf: [
300
+ { type: "string" },
301
+ {
302
+ type: "object",
303
+ properties: { width: { anyOf: [{ type: "number", minimum: 0 }, { type: "string" }] }, style: { enum: [...BORDER_STYLES] }, color: { type: "string" } },
304
+ additionalProperties: false
305
+ }
306
+ ]
307
+ };
308
+ return { anyOf: [...side2.anyOf, { type: "object", properties: Object.fromEntries(SIDES.map((s) => [s, side2])), additionalProperties: false }] };
309
+ }
310
+ });
311
+ }
312
+ var CORNERS = ["topLeft", "topRight", "bottomRight", "bottomLeft"];
313
+ function radius() {
314
+ const accepts = "px for every corner, [topLeft, topRight, bottomRight, bottomLeft], or { topLeft, topRight, bottomRight, bottomLeft }";
315
+ return kind({
316
+ name: "radius",
317
+ editor: "radius",
318
+ accepts,
319
+ parse(input) {
320
+ let values;
321
+ if (Array.isArray(input)) {
322
+ if (input.length !== 4) return fail(`expected ${accepts}`);
323
+ values = input;
324
+ } else if (isObject(input)) {
325
+ const extra = Object.keys(input).find((key) => !CORNERS.includes(key));
326
+ if (extra) return fail(`"${extra}" isn't a corner; expected ${CORNERS.join(", ")}`);
327
+ values = CORNERS.map((corner) => input[corner] ?? 0);
328
+ } else {
329
+ return px(input, 0, "a radius");
330
+ }
331
+ const out = [];
332
+ for (const value of values) {
333
+ const corner = px(value, 0, "a corner");
334
+ if (!corner.ok) return corner;
335
+ out.push(corner.value);
336
+ }
337
+ if (out.every((value) => value === out[0])) return ok(out[0]);
338
+ const [topLeft, topRight, bottomRight, bottomLeft] = out;
339
+ return ok({ topLeft, topRight, bottomRight, bottomLeft });
340
+ },
341
+ jsonSchema: () => {
342
+ const corner = { anyOf: [{ type: "number", minimum: 0 }, { type: "string", pattern: `^${NUMBER}(px)?$` }] };
343
+ return {
344
+ anyOf: [
345
+ ...corner.anyOf,
346
+ { type: "array", items: corner, minItems: 4, maxItems: 4 },
347
+ { type: "object", properties: Object.fromEntries(CORNERS.map((c) => [c, corner])), additionalProperties: false }
348
+ ]
349
+ };
350
+ }
351
+ });
352
+ }
353
+ function enumOf(values) {
354
+ const accepts = `one of ${values.map((v) => JSON.stringify(v)).join(", ")}`;
355
+ return kind({
356
+ name: "enum",
357
+ editor: "choice",
358
+ accepts,
359
+ parse: (input) => values.includes(input) ? ok(input) : fail(`expected ${accepts}, not ${show(input)}`),
360
+ jsonSchema: () => ({ enum: [...values] })
361
+ });
362
+ }
363
+ function text(options = {}) {
364
+ return kind({
365
+ name: "text",
366
+ editor: "text",
367
+ accepts: "text",
368
+ parse(input) {
369
+ if (typeof input !== "string") return fail(`expected text, not ${show(input)}`);
370
+ if (options.maxLength !== void 0 && input.length > options.maxLength) return fail(`is longer than ${options.maxLength} characters`);
371
+ if (options.pattern && !options.pattern.test(input)) return fail(options.mismatch ?? `doesn't match ${options.pattern.source}`);
372
+ return ok(input);
373
+ },
374
+ jsonSchema: () => ({ type: "string", ...options.maxLength !== void 0 ? { maxLength: options.maxLength } : {}, ...options.pattern ? { pattern: options.pattern.source } : {} })
375
+ });
376
+ }
377
+ function boolean() {
378
+ return kind({
379
+ name: "boolean",
380
+ editor: "toggle",
381
+ accepts: "true or false",
382
+ parse: (input) => typeof input === "boolean" ? ok(input) : fail(`expected true or false, not ${show(input)}`),
383
+ jsonSchema: () => ({ type: "boolean" })
384
+ });
385
+ }
386
+ var NAMED_COLORS = new Set(
387
+ "aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow grey honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen transparent".split(" ")
388
+ );
389
+ var HEX = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
390
+ var RGB = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d*\.?\d+%?)\s*)?\)$/i;
391
+ var hex2 = (n) => n.toString(16).padStart(2, "0");
392
+ function parseColor(input) {
393
+ if (typeof input !== "string") return fail(`expected a color ("#0879a1", "rgb(\u2026)", "rgba(\u2026)" or a CSS name), not ${show(input)}`);
394
+ const value = input.trim().toLowerCase();
395
+ const hex = HEX.exec(value);
396
+ if (hex) {
397
+ const digits = hex[1].length <= 4 ? [...hex[1]].map((d) => d + d).join("") : hex[1];
398
+ if (digits.length === 6 || digits.slice(6) === "ff") return ok(`#${digits.slice(0, 6)}`);
399
+ const alpha = Math.round(parseInt(digits.slice(6), 16) / 255 * 1e3) / 1e3;
400
+ return ok(`rgba(${parseInt(digits.slice(0, 2), 16)}, ${parseInt(digits.slice(2, 4), 16)}, ${parseInt(digits.slice(4, 6), 16)}, ${alpha})`);
401
+ }
402
+ const rgb = RGB.exec(value);
403
+ if (rgb) {
404
+ const channels = rgb.slice(1, 4).map(Number);
405
+ if (channels.some((c) => c > 255)) return fail(`a color channel is 0 to 255 in ${show(input)}`);
406
+ let alpha = 1;
407
+ if (rgb[4] !== void 0) {
408
+ alpha = rgb[4].endsWith("%") ? Number(rgb[4].slice(0, -1)) / 100 : Number(rgb[4]);
409
+ if (!(alpha >= 0 && alpha <= 1)) return fail(`a color's alpha is 0 to 1 in ${show(input)}`);
410
+ }
411
+ return ok(alpha === 1 ? `#${channels.map(hex2).join("")}` : `rgba(${channels.join(", ")}, ${alpha})`);
412
+ }
413
+ if (NAMED_COLORS.has(value)) return ok(value);
414
+ return fail(`expected a color ("#0879a1", "rgb(\u2026)", "rgba(\u2026)" or a CSS name), not ${show(input)}`);
415
+ }
416
+ function color() {
417
+ return kind({
418
+ name: "color",
419
+ editor: "color",
420
+ accepts: 'a color ("#0879a1", "rgb(\u2026)", "rgba(\u2026)" or a CSS name)',
421
+ parse: parseColor,
422
+ jsonSchema: () => ({ type: "string" })
423
+ });
424
+ }
425
+ var FONT_UNSAFE = /[;{}<>\\\n\r]/;
426
+ function fontFamily() {
427
+ const accepts = 'a font stack, as CSS writes it ("Inter, Arial, sans-serif")';
428
+ return kind({
429
+ name: "fontFamily",
430
+ editor: "fontFamily",
431
+ accepts,
432
+ parse(input) {
433
+ if (isObject(input) && "src" in input) return fail("a web font is declared once, in the root's fonts ({ family, src, weight, style }); fontFamily names it in a stack");
434
+ if (typeof input !== "string" || !input.trim() || FONT_UNSAFE.test(input)) return fail(`expected ${accepts}, not ${show(input)}`);
435
+ return ok(input.trim());
436
+ },
437
+ jsonSchema: () => ({ type: "string", minLength: 1 })
438
+ });
439
+ }
440
+ function fontStack(stack) {
441
+ return stack.split(",").map((family) => family.trim().replace(/^(["'])(.*)\1$/, "$2")).filter(Boolean);
442
+ }
443
+ var FONT_FORMATS = ["woff2", "woff", "truetype", "opentype"];
444
+ function items(fields, noun) {
445
+ const names = Object.keys(fields);
446
+ const shape2 = `{ ${names.join(", ")} }`;
447
+ const accepts = `a list of ${noun}s, each ${shape2}`;
448
+ return kind({
449
+ name: "items",
450
+ editor: "items",
451
+ accepts,
452
+ parse(input) {
453
+ if (!Array.isArray(input)) return fail(`expected ${accepts}, not ${show(input)}`);
454
+ const out = [];
455
+ for (const [index, item] of input.entries()) {
456
+ const at = `${noun} ${index + 1}`;
457
+ if (!isObject(item)) return fail(`${at}: expected ${shape2}, not ${show(item)}`);
458
+ const extra = Object.keys(item).find((key) => !names.includes(key));
459
+ if (extra) return fail(`${at}: "${extra}" isn't one of its fields (${names.join(", ")})`);
460
+ const value = {};
461
+ for (const name of names) {
462
+ if (item[name] === void 0) return fail(`${at}: ${name} is missing`);
463
+ const parsed = fields[name].parse(item[name]);
464
+ if (!parsed.ok) return fail(`${at}: ${name}: ${parsed.message}`);
465
+ value[name] = parsed.value;
466
+ }
467
+ out.push(value);
468
+ }
469
+ return ok(out);
470
+ },
471
+ jsonSchema: () => ({
472
+ type: "array",
473
+ items: { type: "object", required: names, properties: Object.fromEntries(names.map((name) => [name, fields[name].jsonSchema()])), additionalProperties: false }
474
+ })
475
+ });
476
+ }
477
+ function fontFaces() {
478
+ const accepts = "a list of { family, src, format, weight, style }";
479
+ const face = (input, index) => {
480
+ const at = `font ${index + 1}`;
481
+ if (!isObject(input)) return fail(`${at}: expected { family, src, format, weight, style }, not ${show(input)}`);
482
+ const extra = Object.keys(input).find((key) => !["family", "src", "format", "weight", "style"].includes(key));
483
+ if (extra) return fail(`${at}: "${extra}" isn't part of a font; expected family, src, format, weight or style`);
484
+ if (typeof input.family !== "string" || !input.family.trim() || FONT_UNSAFE.test(input.family) || input.family.includes(",")) {
485
+ return fail(`${at}: family is one font's name ("Inter"), not ${show(input.family)}`);
486
+ }
487
+ if (typeof input.src !== "string" || !input.src.trim() || unsafeUrl(input.src, /^data:(font|application)\//)) {
488
+ return fail(`${at}: src is the font file's http(s) URL (or a data:font URI), not ${show(input.src)}`);
489
+ }
490
+ const out = { family: input.family.trim(), src: input.src.trim() };
491
+ if (input.format !== void 0) {
492
+ if (!FONT_FORMATS.includes(input.format)) return fail(`${at}: format is one of ${FONT_FORMATS.join(", ")}, not ${show(input.format)}`);
493
+ out.format = input.format;
494
+ }
495
+ if (input.weight !== void 0) {
496
+ const range = typeof input.weight === "string" ? /^\s*(\d+)\s+(\d+)\s*$/.exec(input.weight) : Array.isArray(input.weight) && input.weight.length === 2 && input.weight.every(Number.isInteger) ? [null, ...input.weight] : null;
497
+ const weight = input.weight === "normal" ? 400 : input.weight === "bold" ? 700 : input.weight;
498
+ if (range && Number(range[1]) >= 1 && Number(range[2]) <= 1e3 && Number(range[1]) <= Number(range[2])) out.weight = [Number(range[1]), Number(range[2])];
499
+ else if (typeof weight === "number" && Number.isInteger(weight) && weight >= 1 && weight <= 1e3) out.weight = weight;
500
+ else return fail(`${at}: weight is 1 to 1000, "normal", "bold", or a range ("100 900"), not ${show(input.weight)}`);
501
+ }
502
+ if (input.style !== void 0) {
503
+ if (input.style !== "normal" && input.style !== "italic") return fail(`${at}: style is "normal" or "italic", not ${show(input.style)}`);
504
+ out.style = input.style;
505
+ }
506
+ return ok(out);
507
+ };
508
+ return kind({
509
+ name: "fontFaces",
510
+ editor: "fontFaces",
511
+ accepts,
512
+ parse(input) {
513
+ if (!Array.isArray(input)) return fail(`expected ${accepts}, not ${show(input)}`);
514
+ const out = [];
515
+ for (const [index, item] of input.entries()) {
516
+ const parsed = face(item, index);
517
+ if (!parsed.ok) return parsed;
518
+ out.push(parsed.value);
519
+ }
520
+ return ok(out);
521
+ },
522
+ jsonSchema: () => ({
523
+ type: "array",
524
+ items: {
525
+ type: "object",
526
+ required: ["family", "src"],
527
+ properties: {
528
+ family: { type: "string", minLength: 1 },
529
+ src: { type: "string", minLength: 1 },
530
+ format: { enum: [...FONT_FORMATS] },
531
+ weight: { anyOf: [{ type: "number", minimum: 1, maximum: 1e3 }, { type: "string" }, { type: "array", items: { type: "integer", minimum: 1, maximum: 1e3 }, minItems: 2, maxItems: 2 }] },
532
+ style: { enum: ["normal", "italic"] }
533
+ },
534
+ additionalProperties: false
535
+ }
536
+ })
537
+ });
538
+ }
539
+ function fontWeight() {
540
+ return kind({
541
+ name: "fontWeight",
542
+ editor: "fontWeight",
543
+ accepts: '100 to 900 in hundreds, "normal" or "bold"',
544
+ parse(input) {
545
+ if (input === "normal") return ok(400);
546
+ if (input === "bold") return ok(700);
547
+ if (typeof input === "number" && input >= 100 && input <= 900 && input % 100 === 0) return ok(input);
548
+ return fail(`expected 100 to 900 in hundreds, "normal" or "bold", not ${show(input)}`);
549
+ },
550
+ jsonSchema: () => ({ anyOf: [{ enum: [100, 200, 300, 400, 500, 600, 700, 800, 900] }, { enum: ["normal", "bold"] }] })
551
+ });
552
+ }
553
+ function lineHeight() {
554
+ const accepts = `a multiple of the font size (1.5), a percentage ("150%"), px ("24px") or "normal" (the font's own)`;
555
+ return kind({
556
+ name: "lineHeight",
557
+ editor: "lineHeight",
558
+ accepts,
559
+ parse(input) {
560
+ if (input === "normal") return ok("normal");
561
+ if (typeof input === "number") return input > 0 && Number.isFinite(input) ? ok(input) : fail(`expected ${accepts}, not ${show(input)}`);
562
+ const parsed = typeof input === "string" && /^\s*-?[\d.]+\s*$/.test(input) ? void 0 : readLength(input);
563
+ if (!parsed || parsed.value <= 0 || parsed.unit !== "px" && parsed.unit !== "%") return fail(`expected ${accepts}, not ${show(input)}`);
564
+ return ok(parsed.unit === "%" ? Math.round(parsed.value * 1e3) / 1e5 : `${parsed.value}px`);
565
+ },
566
+ jsonSchema: () => ({ anyOf: [{ type: "number", exclusiveMinimum: 0 }, { type: "string", pattern: `^${NUMBER}(px|%)$` }, { const: "normal" }] })
567
+ });
568
+ }
569
+ var SERVICE_TAG = /<%[\s\S]*?%>|\{%[\s\S]*?%\}|\{\{[\s\S]*?\}\}|\uFDD0\d+\uFDD1/g;
570
+ function unsafeUrl(url, allowedData) {
571
+ const compact = url.replace(SERVICE_TAG, "").replace(/[\u0000- \u007f-\u009f]/g, "").toLowerCase();
572
+ if (compact.startsWith("data:")) return !allowedData || !allowedData.test(compact);
573
+ return /^(javascript|vbscript|file):/.test(compact);
574
+ }
575
+ var IMAGE_DATA = /^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml)[;,]/;
576
+ function asset() {
577
+ const accepts = "an image URL (http, https or a data:image URI), or { src, width, height }";
578
+ return kind({
579
+ name: "asset",
580
+ editor: "image",
581
+ accepts,
582
+ parse(input) {
583
+ const src = typeof input === "string" ? input : isObject(input) ? input.src : void 0;
584
+ if (typeof src !== "string" || !src.trim()) return fail(`expected ${accepts}, not ${show(input)}`);
585
+ if (unsafeUrl(src, IMAGE_DATA)) return fail(`${show(src)} isn't an image URL Elements will write`);
586
+ if (typeof input === "string") return ok(src.trim());
587
+ const extra = Object.keys(input).find((key) => !["src", "width", "height"].includes(key));
588
+ if (extra) return fail(`"${extra}" isn't part of an image; expected src, width or height`);
589
+ const out = { src: src.trim() };
590
+ for (const side2 of ["width", "height"]) {
591
+ const value = input[side2];
592
+ if (value === void 0) continue;
593
+ if (typeof value !== "number" || !(value > 0)) return fail(`an image's ${side2} is its size in px, not ${show(value)}`);
594
+ out[side2] = value;
595
+ }
596
+ return ok(out);
597
+ },
598
+ jsonSchema: () => ({
599
+ anyOf: [
600
+ { type: "string", minLength: 1 },
601
+ {
602
+ type: "object",
603
+ required: ["src"],
604
+ properties: { src: { type: "string", minLength: 1 }, width: { type: "number", exclusiveMinimum: 0 }, height: { type: "number", exclusiveMinimum: 0 } },
605
+ additionalProperties: false
606
+ }
607
+ ]
608
+ })
609
+ });
610
+ }
611
+ var YOUTUBE = /^https:\/\/(?:www\.|m\.)?(?:youtube\.com\/(?:watch\?(?:[^#]*&)?v=|embed\/|shorts\/|live\/)|youtu\.be\/)([A-Za-z0-9_-]{6,})/;
612
+ var VIMEO = /^https:\/\/(?:www\.|player\.)?vimeo\.com\/(?:video\/)?(\d+)/;
613
+ function videoParts(url) {
614
+ const youtube = YOUTUBE.exec(url);
615
+ if (youtube) return { provider: "youtube", id: youtube[1], embed: `https://www.youtube.com/embed/${youtube[1]}`, thumbnail: `https://img.youtube.com/vi/${youtube[1]}/0.jpg` };
616
+ const vimeo = VIMEO.exec(url);
617
+ if (vimeo) return { provider: "vimeo", id: vimeo[1], embed: `https://player.vimeo.com/video/${vimeo[1]}` };
618
+ return void 0;
619
+ }
620
+ function video() {
621
+ const accepts = "a YouTube or Vimeo video's https link";
622
+ return kind({
623
+ name: "video",
624
+ editor: "link",
625
+ accepts,
626
+ parse(input) {
627
+ if (typeof input !== "string" || !videoParts(input.trim())) return fail(`expected ${accepts}, not ${show(input)}`);
628
+ return ok(input.trim());
629
+ },
630
+ jsonSchema: () => ({ type: "string", pattern: "^https://" })
631
+ });
632
+ }
633
+ function link() {
634
+ const accepts = "a URL (http, https, mailto, tel, a path, or a merge tag) or { href, target }";
635
+ return kind({
636
+ name: "link",
637
+ editor: "link",
638
+ accepts,
639
+ parse(input) {
640
+ const href = typeof input === "string" ? input : isObject(input) ? input.href : void 0;
641
+ if (typeof href !== "string" || !href.trim() && (typeof input === "string" || href !== "")) return fail(`expected ${accepts}, not ${show(input)}`);
642
+ if (unsafeUrl(href)) return fail(`${show(href)} isn't a link Elements will write (scripts, data and local files aren't)`);
643
+ if (typeof input === "string") return ok(href.trim());
644
+ const extra = Object.keys(input).find((key) => !["href", "target"].includes(key));
645
+ if (extra) return fail(`"${extra}" isn't part of a link; expected href or target`);
646
+ const target = input.target;
647
+ if (target === void 0) return ok(href ? href.trim() : { href });
648
+ if (target !== "_blank" && target !== "_self") return fail(`a link's target is "_blank" or "_self", not ${show(target)}`);
649
+ return ok({ href: href.trim(), target });
650
+ },
651
+ jsonSchema: () => ({
652
+ anyOf: [
653
+ { type: "string", minLength: 1 },
654
+ { type: "object", required: ["href"], properties: { href: { type: "string" }, target: { enum: ["_blank", "_self"] } }, additionalProperties: false }
655
+ ]
656
+ })
657
+ });
658
+ }
659
+ function attributes() {
660
+ const accepts = "attributes by name, each a text value (no on\u2026 handlers, href or style)";
661
+ return kind({
662
+ name: "attributes",
663
+ editor: "text",
664
+ accepts,
665
+ parse(input) {
666
+ if (!isObject(input)) return fail(`expected ${accepts}, not ${show(input)}`);
667
+ for (const [name, value] of Object.entries(input)) {
668
+ if (!/^[a-zA-Z_:][\w:.-]*$/.test(name) || /^(on|href$|style$)/i.test(name) || typeof value !== "string") return fail(`${show(name)} isn't an attribute Elements will write, with ${show(value)}`);
669
+ }
670
+ return ok(input);
671
+ },
672
+ jsonSchema: () => ({ type: "object", additionalProperties: { type: "string" } })
673
+ });
674
+ }
675
+ var MIN_PHONE_WIDTH = 240;
676
+ function breakpoints() {
677
+ const accepts = `{ mobile }: the phone width in px (from ${MIN_PHONE_WIDTH})`;
678
+ return kind({
679
+ name: "breakpoints",
680
+ editor: "number",
681
+ accepts,
682
+ parse(input) {
683
+ if (!isObject(input)) return fail(`expected ${accepts}, not ${show(input)}`);
684
+ const unknown = Object.keys(input).find((key) => key !== "mobile");
685
+ if (unknown !== void 0) return fail(`${show(unknown)} isn't a breakpoint a Page has: it has mobile`);
686
+ const { mobile } = input;
687
+ if (mobile === void 0) return ok({});
688
+ if (typeof mobile !== "number" || !Number.isInteger(mobile) || mobile < MIN_PHONE_WIDTH) return fail(`mobile is the widest window its phone styles apply to, an integer from ${MIN_PHONE_WIDTH}, not ${show(mobile)}`);
689
+ return ok({ mobile });
690
+ },
691
+ empty: (value) => value.mobile === void 0,
692
+ jsonSchema: () => ({ type: "object", properties: { mobile: { type: "integer", minimum: MIN_PHONE_WIDTH } }, additionalProperties: false })
693
+ });
694
+ }
695
+ function assetParts(value) {
696
+ return typeof value === "string" ? { src: value } : value;
697
+ }
698
+ var ROOT_NAMES = ["Email", "Page", "Document", "Canvas"];
699
+ function defineComponent(definition) {
700
+ return definition;
701
+ }
702
+ function prop(kind2, options) {
703
+ return { kind: kind2, ...options };
704
+ }
705
+ var MOBILE = {
706
+ name: "mobile",
707
+ on: "on phones",
708
+ for: "for phones",
709
+ roots: (root) => !!root?.responsive,
710
+ notAllowed: { code: "mobile/not-supported", message: "Phone styles are for an Email or a Page, which have a phone width" },
711
+ sheet: { media: "phone", selector: "&" },
712
+ allows: (spec) => !!spec.mobile,
713
+ support: { web: { level: "full", source: "writer" }, email: { level: "full", source: "writer" } },
714
+ styles: {
715
+ // The email's phone text styles are rules that override a size; "normal" isn't one.
716
+ lineHeight: { email: { level: "none", values: ["normal"], shows: "the line height it has on desktop", source: "writer" } }
717
+ },
718
+ outlookApplies: false,
719
+ hiddenKey: "mobile"
720
+ };
721
+ var HOVER = {
722
+ name: "hover",
723
+ on: "on hover",
724
+ for: "for hover",
725
+ // Keyboard focus gets the same styles as the pointer: a link or button reached by Tab shows its hover colors.
726
+ // A box around the link (an email Button's cell, its background) takes them while the link in it has focus.
727
+ sheet: { media: "all", selector: "&:hover,&:focus-visible", within: "&:hover,&:focus-within", level: "hover" },
728
+ allows: (_, key, definition) => !!definition.hover?.includes(key),
729
+ support: {
730
+ web: { level: "full", source: "css" },
731
+ // On type selectors only in Outlook.com and Outlook for Mac (Elements' are classes); the phone apps' results don't
732
+ // matter, as nothing hovers there.
733
+ email: {
734
+ level: "partial",
735
+ shows: "no change (where it shows, keyboard focus gets it too)",
736
+ source: "caniemail",
737
+ feature: "css-pseudo-class-hover",
738
+ clients: ["outlook", "outlook-com", "outlook-mac", "gmx", "web-de", "sfr", "laposte", "mail-ru"]
739
+ }
740
+ },
741
+ paged: (node, where) => `A hover isn't possible in ${where}: nothing points at it. The ${node.type} shows its own colors`,
742
+ outlookApplies: false
743
+ };
744
+ var CONDITIONS = [MOBILE, HOVER];
745
+ var CONDITION_NAMES = new Set(CONDITIONS.map((condition) => condition.name));
746
+ function blockOf(node, condition) {
747
+ const block = node[condition.name];
748
+ return block && Object.keys(block).length ? block : void 0;
749
+ }
750
+ function shownAnywhere(node) {
751
+ return !(node.hidden?.mobile && node.hidden?.desktop);
752
+ }
753
+ var writer = { level: "full", source: "writer" };
754
+ var css = { level: "full", source: "css" };
755
+ var textStyle = {
756
+ color: { kind: color(), label: "Text color", inherited: true, support: { email: writer, web: css } },
757
+ // A web font in a stack is reported from the root's fonts (whether it loads depends on the client).
758
+ fontFamily: { kind: fontFamily(), label: "Font", inherited: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
759
+ // Word rounds sizes to half points (13px is 10pt): a fraction of a px.
760
+ fontSize: { kind: length(["px"], { min: 1 }), label: "Font size", inherited: true, mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
761
+ fontWeight: {
762
+ kind: fontWeight(),
763
+ label: "Font weight",
764
+ inherited: true,
765
+ support: { email: { level: "outlook", values: [100, 200, 300, 500], shows: "normal weight: Word draws only normal and bold (600 and over)", source: "caniemail", feature: "css-font-weight" }, web: css }
766
+ },
767
+ fontStyle: { kind: enumOf(["normal", "italic"]), label: "Font style", inherited: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
768
+ // Written in px with mso-line-height-rule: exactly, which Word keeps (it drops unitless ones).
769
+ lineHeight: { kind: lineHeight(), label: "Line height", inherited: true, mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
770
+ letterSpacing: {
771
+ kind: length(["px"], { min: -100 }),
772
+ label: "Letter spacing",
773
+ inherited: true,
774
+ mobile: true,
775
+ support: { email: { level: "outlook", shows: "normal spacing: Word keeps letter spacing but doesn't draw it", source: "outlook-captures" }, web: css }
776
+ },
777
+ textAlign: { kind: enumOf(["left", "center", "right", "justify"]), label: "Alignment", inherited: true, mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
778
+ textTransform: {
779
+ kind: enumOf(["none", "uppercase", "lowercase", "capitalize"]),
780
+ label: "Letter case",
781
+ inherited: true,
782
+ support: { email: { level: "outlook", values: ["lowercase"], shows: "the text as written: Word doesn't lowercase it", source: "caniemail", feature: "css-text-transform" }, web: css }
783
+ },
784
+ textDecoration: {
785
+ kind: enumOf(["none", "underline", "line-through"]),
786
+ label: "Decoration",
787
+ support: {
788
+ // Removed, so "none" leaves a link the client's own underline: said only where a line is asked for.
789
+ email: { level: "partial", values: ["underline", "line-through"], shows: "no line: the client removes it", source: "caniemail", feature: "css-text-decoration", clients: ["gmail-app-other", "protonmail", "orange", "sfr"] },
790
+ web: css
791
+ }
792
+ }
793
+ };
794
+ var inheritedTextStyle = (({ textDecoration: _, ...rest }) => rest)(textStyle);
795
+ var boxStyle = {
796
+ // Widths are the resolver's, written as table, cell and image attributes in px (what Word sizes by).
797
+ width: { kind: length(["px", "%"]), label: "Width", mobile: true, support: { email: writer, web: css } },
798
+ minWidth: { kind: length(["px", "%"]), label: "Minimum width", support: { email: writer, web: css } },
799
+ maxWidth: { kind: length(["px", "%"]), label: "Maximum width", mobile: true, support: { email: writer, web: css } },
800
+ height: { kind: length(["px"]), label: "Height", support: { email: writer, web: css } },
801
+ // Cell padding, which Word keeps (it drops a div's).
802
+ padding: { kind: insets(), label: "Padding", mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
803
+ backgroundColor: { kind: color(), label: "Background color", support: { email: writer, web: css } },
804
+ border: { kind: border(), label: "Border", mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
805
+ borderRadius: {
806
+ kind: radius(),
807
+ label: "Corner radius",
808
+ support: {
809
+ email: [
810
+ { level: "outlook", shows: "square corners", source: "caniemail", feature: "css-border-radius" },
811
+ { level: "partial", shows: "square corners", source: "caniemail", feature: "css-border-radius", clients: ["gmx", "web-de", "orange"] }
812
+ ],
813
+ web: css
814
+ }
815
+ },
816
+ overflow: {
817
+ kind: enumOf(["visible", "hidden"]),
818
+ label: "Overflow",
819
+ description: "Whether content past a set height is cut off.",
820
+ support: { email: { level: "outlook", shows: "content past the height: Word doesn't cut it off", values: ["hidden"], source: "caniemail" }, web: css }
821
+ }
822
+ };
823
+ var layoutStyle = {
824
+ gap: { kind: length(["px"]), label: "Gap", description: "Space between children.", mobile: true, support: { email: writer, web: css } },
825
+ alignItems: {
826
+ kind: enumOf(["start", "center", "end", "stretch"]),
827
+ label: "Align items",
828
+ description: "Where children sit across the layout's direction (vertically in a Row, horizontally in a Column), as CSS align-items. Across a Column it places what has its own size (a Button, an Image, an Svg, a box with a width); a block of text fills the width, so its words move with its textAlign.",
829
+ // A cell's align, and a Row's cells' valign.
830
+ support: { email: writer, web: css }
831
+ },
832
+ justifyContent: {
833
+ kind: enumOf(["start", "center", "end", "space-between"]),
834
+ label: "Justify content",
835
+ description: "Where children sit along the layout's direction, as CSS justify-content.",
836
+ support: { email: writer, web: css }
837
+ }
838
+ };
839
+ var placementStyle = {
840
+ flex: {
841
+ kind: flex(),
842
+ label: "Share",
843
+ description: 'In a Row: its share of the width left after children with a width (default 1); "none" takes its own size.',
844
+ support: { email: writer, web: css }
845
+ }
846
+ };
847
+ function pick(group, ...keys) {
848
+ return Object.fromEntries(keys.map((key) => [key, group[key]]));
849
+ }
850
+ var STYLE_GROUP_OF = {
851
+ // How children are placed, and how a box sits among its siblings.
852
+ alignItems: "Layout",
853
+ justifyContent: "Layout",
854
+ justifyItems: "Layout",
855
+ alignSelf: "Layout",
856
+ verticalAlign: "Layout",
857
+ flex: "Layout",
858
+ flexWrap: "Layout",
859
+ overflow: "Layout",
860
+ // A box's size.
861
+ width: "Size",
862
+ minWidth: "Size",
863
+ maxWidth: "Size",
864
+ height: "Size",
865
+ boxSizing: "Size",
866
+ // Space inside a box and between its children.
867
+ padding: "Spacing",
868
+ gap: "Spacing",
869
+ rowGap: "Spacing",
870
+ indent: "Spacing",
871
+ cellPadding: "Spacing",
872
+ // Text.
873
+ color: "Typography",
874
+ fontFamily: "Typography",
875
+ fontSize: "Typography",
876
+ fontWeight: "Typography",
877
+ fontStyle: "Typography",
878
+ lineHeight: "Typography",
879
+ letterSpacing: "Typography",
880
+ textAlign: "Typography",
881
+ textTransform: "Typography",
882
+ textDecoration: "Typography",
883
+ // What's drawn behind content, and a shape's paint.
884
+ backgroundColor: "Background",
885
+ fill: "Background",
886
+ // Lines around and between boxes, and a shape's outline.
887
+ border: "Border",
888
+ borderTop: "Border",
889
+ borderRadius: "Border",
890
+ borderCollapse: "Border",
891
+ rowBorder: "Border",
892
+ separatorColor: "Border",
893
+ stroke: "Border",
894
+ strokeWidth: "Border",
895
+ opacity: "Effects"
896
+ };
897
+ function styleGroup(key, spec) {
898
+ return spec.group ?? STYLE_GROUP_OF[key];
899
+ }
900
+ var WRITTEN = { email: { level: "full", source: "writer" }, web: { level: "full", source: "writer" } };
901
+ var BY_PARTS = { email: { level: "full", source: "writer" }, web: css };
902
+ function defineComposite(definition) {
903
+ return { ...definition, custom: true };
904
+ }
905
+ var SOCIAL_ICONS = "https://cdn.tools.unlayer.com/social/icons";
906
+ var Social = defineComposite({
907
+ type: "Social",
908
+ label: "Social icons",
909
+ description: "Links to social profiles: a line of the networks' icons, each linked to its profile.",
910
+ categories: ["block"],
911
+ props: {
912
+ // Its icon is found by its name's letters and digits ("LinkedIn": linkedin.png): a name needs one.
913
+ icons: prop(items({ name: text({ maxLength: 40, pattern: /[A-Za-z0-9]/, mismatch: `has no letter (a to z) or digit, which its icon is found by ("Instagram")` }), href: link() }, "icon"), {
914
+ support: WRITTEN,
915
+ label: "Icons",
916
+ required: true,
917
+ description: 'Each network by name ("Instagram", "LinkedIn") and the link to its profile.'
918
+ }),
919
+ iconType: prop(enumOf(["circle", "rounded", "squared"]), { support: WRITTEN, label: "Icon style", default: "circle" }),
920
+ size: prop(number({ min: 1, integer: true }), { support: WRITTEN, label: "Icon size", description: "Each icon's width and height, in px.", default: 32 })
921
+ },
922
+ style: {
923
+ gap: { ...layoutStyle.gap, label: "Space between icons", support: BY_PARTS },
924
+ justifyContent: { kind: enumOf(["start", "center", "end"]), label: "Alignment", description: "Where the icons sit across the line.", support: BY_PARTS },
925
+ ...placementStyle
926
+ },
927
+ defaultStyle: { gap: 5, justifyContent: "center" },
928
+ children: {},
929
+ sizing: "stretch",
930
+ targets: { email: {}, web: {} },
931
+ editor: { groups: [{ title: "Social icons", paths: ["props.icons", "props.iconType", "props.size", "style.gap", "style.justifyContent"] }] },
932
+ expand: ({ icons, iconType, size: size2 }, { style }) => ({
933
+ type: "Row",
934
+ props: { stackOnMobile: false },
935
+ style: { gap: style.gap, justifyContent: style.justifyContent },
936
+ children: icons.map((icon) => ({
937
+ type: "Link",
938
+ // Each opens in a new window, as the builder's do, unless it says otherwise.
939
+ props: { href: typeof icon.href === "string" ? { href: icon.href, target: "_blank" } : icon.href, title: icon.name },
940
+ style: { flex: "none" },
941
+ children: [
942
+ {
943
+ type: "Image",
944
+ props: { src: `${SOCIAL_ICONS}/${iconType}/${icon.name.toLowerCase().replace(/[^a-z0-9]+/g, "")}.png`, alt: icon.name },
945
+ style: { width: size2 }
946
+ }
947
+ ]
948
+ }))
949
+ })
950
+ });
951
+ var MENU_TEXT = ["fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "textTransform"];
952
+ var Menu = defineComposite({
953
+ type: "Menu",
954
+ label: "Menu",
955
+ description: "A line of links (a site's sections, a footer's), onto more lines where they don't fit, with a separator between them on desktop; or links down a column.",
956
+ categories: ["block"],
957
+ props: {
958
+ items: prop(items({ text: text({ maxLength: 200 }), href: link() }, "item"), {
959
+ support: WRITTEN,
960
+ label: "Items",
961
+ required: true,
962
+ description: "Each link's text and where it goes."
963
+ }),
964
+ separator: prop(text({ maxLength: 10 }), { support: WRITTEN, label: "Separator", description: 'Between the links of a horizontal menu ("|", "\xB7"), on desktop: phones show none.', default: "" }),
965
+ layout: prop(enumOf(["horizontal", "vertical"]), { support: WRITTEN, label: "Layout", default: "horizontal" })
966
+ },
967
+ style: {
968
+ ...pick(textStyle, "color", ...MENU_TEXT),
969
+ separatorColor: { kind: color(), label: "Separator color", support: BY_PARTS },
970
+ padding: { kind: insets(), label: "Each link's padding", description: "Around each link: its sides are the space between them.", support: BY_PARTS },
971
+ justifyContent: { kind: enumOf(["start", "center", "end"]), label: "Alignment", description: "Where the links sit.", support: BY_PARTS },
972
+ ...placementStyle
973
+ },
974
+ defaultStyle: {
975
+ color: "#0068A5",
976
+ fontSize: 14,
977
+ lineHeight: 1.2,
978
+ separatorColor: "#444444",
979
+ padding: { top: 5, right: 15, bottom: 5, left: 15 },
980
+ justifyContent: "center"
981
+ },
982
+ children: {},
983
+ sizing: "stretch",
984
+ targets: { email: {}, web: {} },
985
+ editor: {
986
+ groups: [
987
+ { title: "Menu", paths: ["props.items", "props.layout", "props.separator", "style.justifyContent"] },
988
+ { title: "Text", paths: ["style.color", "style.separatorColor", ...MENU_TEXT.map((key) => `style.${key}`)] },
989
+ { title: "Spacing", paths: ["style.padding"] }
990
+ ]
991
+ },
992
+ expand: ({ items: links, separator, layout }, { style, responsive }) => {
993
+ const padding = style.padding;
994
+ const text2 = Object.fromEntries(MENU_TEXT.filter((key) => style[key] !== void 0).map((key) => [key, style[key]]));
995
+ const link3 = (item2) => ({ type: "Link", props: { href: item2.href }, style: { color: style.color, textDecoration: "none" }, children: [item2.text] });
996
+ if (layout === "vertical") {
997
+ return { type: "Column", style: { ...text2, gap: padding.top + padding.bottom, padding, alignItems: style.justifyContent }, children: links.map(link3) };
998
+ }
999
+ const item = (entry) => ({ type: "Text", style: { flex: "none", padding }, children: [link3(entry)] });
1000
+ const between = {
1001
+ type: "Text",
1002
+ ...responsive ? { hidden: { mobile: true } } : {},
1003
+ style: { flex: "none", padding: { top: padding.top, right: 0, bottom: padding.bottom, left: 0 }, color: style.separatorColor },
1004
+ children: [separator]
1005
+ };
1006
+ return {
1007
+ type: "Row",
1008
+ props: { stackOnMobile: false },
1009
+ // Links that don't fit on one line go onto another, as today's Menu's do.
1010
+ style: { ...text2, flexWrap: "wrap", justifyContent: style.justifyContent },
1011
+ children: links.flatMap((entry, index) => [...index && separator ? [between] : [], item(entry)])
1012
+ };
1013
+ }
1014
+ });
1015
+ var WRITTEN2 = { email: { level: "full", source: "writer" }, web: { level: "full", source: "writer" } };
1016
+ var BOX = [...Object.keys(boxStyle), "flex", ...Object.keys(inheritedTextStyle)];
1017
+ var only = (style, keys) => {
1018
+ const out = Object.fromEntries(Object.entries(style ?? {}).filter(([key]) => keys.includes(key)));
1019
+ return Object.keys(out).length ? out : void 0;
1020
+ };
1021
+ function arrangeGrid(node) {
1022
+ const columns = Math.max(1, Math.floor(Number(node.props?.columns ?? 2)));
1023
+ const stackOnMobile = node.props?.stackOnMobile !== false;
1024
+ const style = node.style ?? {};
1025
+ const gap = style.gap;
1026
+ const rowGap = style.rowGap ?? gap;
1027
+ const children = (node.children ?? []).filter((child) => typeof child === "object" && child !== null);
1028
+ const rows = [];
1029
+ for (let start = 0; start < children.length; start += columns) {
1030
+ const cells = children.slice(start, start + columns).map((child) => ({ type: "Column", children: [child] }));
1031
+ while (cells.length < columns) cells.push({ type: "Column", ...stackOnMobile ? { hidden: { mobile: true } } : {}, children: [] });
1032
+ const mobileGap = node.mobile?.gap;
1033
+ rows.push({
1034
+ type: "Row",
1035
+ props: { stackOnMobile },
1036
+ style: { ...gap !== void 0 ? { gap } : {}, ...style.alignItems !== void 0 ? { alignItems: style.alignItems } : {} },
1037
+ ...mobileGap !== void 0 ? { mobile: { gap: mobileGap } } : {},
1038
+ children: cells
1039
+ });
1040
+ }
1041
+ const box2 = only(style, BOX);
1042
+ const mobileBox = only(node.mobile, BOX);
1043
+ const phoneGap = node.mobile?.gap ?? gap ?? 0;
1044
+ const mobileRowGap = stackOnMobile ? phoneGap !== (rowGap ?? 0) ? phoneGap : void 0 : node.mobile?.rowGap ?? node.mobile?.gap;
1045
+ return {
1046
+ type: "Column",
1047
+ ...node.id ? { id: node.id } : {},
1048
+ style: { ...box2, ...rowGap !== void 0 ? { gap: rowGap } : {} },
1049
+ ...mobileBox || mobileRowGap !== void 0 ? { mobile: { ...mobileBox, ...mobileRowGap !== void 0 ? { gap: mobileRowGap } : {} } } : {},
1050
+ ...node.hidden ? { hidden: node.hidden } : {},
1051
+ children: rows
1052
+ };
1053
+ }
1054
+ var Grid = defineComponent({
1055
+ type: "Grid",
1056
+ label: "Grid",
1057
+ description: "Its children in columns of equal width, row after row; on phones, one under another.",
1058
+ categories: ["layout", "block"],
1059
+ props: {
1060
+ columns: prop(number({ min: 1, max: 12, integer: true }), { support: WRITTEN2, label: "Columns", description: "How many children to a row.", default: 2 }),
1061
+ stackOnMobile: prop(boolean(), { support: WRITTEN2, label: "Stack on phones", description: "Its children one under another on phones (in an Email or a Page).", default: true })
1062
+ },
1063
+ style: {
1064
+ ...inheritedTextStyle,
1065
+ ...boxStyle,
1066
+ gap: { ...layoutStyle.gap, description: "Space between its children, across and down (unless rowGap is set)." },
1067
+ rowGap: { ...layoutStyle.gap, label: "Row gap", description: "Space between its rows (gap by default); on phones, where the children stack, gap is between them." },
1068
+ alignItems: { ...layoutStyle.alignItems, description: "Where each child sits in its row's height, as CSS align-items." },
1069
+ ...placementStyle
1070
+ },
1071
+ children: { categories: ["layout", "block"] },
1072
+ sizing: "stretch",
1073
+ // Stacked on phones, every two of its children are its phone gap apart: there are no rows for a row gap.
1074
+ conditionSupport: {
1075
+ mobile: {
1076
+ rowGap: {
1077
+ web: { level: "none", when: (_, node) => node.props.stackOnMobile !== false, shows: "its phone gap between every two children", source: "writer" },
1078
+ email: { level: "none", when: (_, node) => node.props.stackOnMobile !== false, shows: "its phone gap between every two children", source: "writer" }
1079
+ }
1080
+ }
1081
+ },
1082
+ targets: { email: {}, web: {} },
1083
+ editor: {
1084
+ groups: [
1085
+ { title: "Grid", paths: ["props.columns", "props.stackOnMobile", "style.gap", "style.rowGap", "style.alignItems"] },
1086
+ { title: "Box", paths: ["style.backgroundColor", "style.padding", "style.border", "style.borderRadius", "style.width"] }
1087
+ ]
1088
+ },
1089
+ arrange: arrangeGrid
1090
+ });
1091
+ var Layer = defineComponent({
1092
+ type: "Layer",
1093
+ label: "Layer",
1094
+ description: "Its children over each other, in one box as tall as the tallest: words on an image (a background image, its first child). In an email, an Image with one child over it.",
1095
+ categories: ["layout", "block"],
1096
+ props: {},
1097
+ style: {
1098
+ ...inheritedTextStyle,
1099
+ ...pick(boxStyle, "width", "minWidth", "maxWidth", "backgroundColor", "border", "borderRadius"),
1100
+ height: { ...boxStyle.height, description: "Its height: the image under its children covers it, cut at its sides or its top and bottom." },
1101
+ alignItems: {
1102
+ ...layoutStyle.alignItems,
1103
+ description: "Where its children sit in its height, as CSS align-items.",
1104
+ support: { email: { level: "partial", values: ["stretch"], shows: "the child over the image as tall as its content", source: "writer" }, web: css }
1105
+ },
1106
+ justifyItems: {
1107
+ kind: enumOf(["start", "center", "end", "stretch"]),
1108
+ label: "Justify items",
1109
+ description: "Where its children sit across its width, as CSS justify-items: stretched to it by default.",
1110
+ support: { email: { level: "full", source: "writer" }, web: css }
1111
+ },
1112
+ ...placementStyle
1113
+ },
1114
+ defaultStyle: { alignItems: "start", justifyItems: "stretch" },
1115
+ children: { categories: ["layout", "block"] },
1116
+ sizing: "stretch",
1117
+ targets: { email: { note: "An Image as wide as the Layer, with one child over it" }, web: {} },
1118
+ editor: {
1119
+ groups: [
1120
+ { title: "Layer", paths: ["style.height", "style.alignItems", "style.justifyItems"] },
1121
+ { title: "Box", paths: ["style.backgroundColor", "style.border", "style.borderRadius", "style.width"] }
1122
+ ]
1123
+ }
1124
+ });
1125
+ var WRITTEN3 = { email: { level: "full", source: "writer" }, web: { level: "full", source: "writer" } };
1126
+ var NO_WEB_FONTS = ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "protonmail", "fastmail", "gmx", "web-de", "orange", "sfr", "laposte", "mail-ru", "ionos"];
1127
+ var ROOT_PROPS = {
1128
+ title: prop(text(), { support: WRITTEN3, label: "Title", description: "The document's title (shown in a browser tab, and by some email clients)." }),
1129
+ lang: prop(text({ maxLength: 35 }), {
1130
+ support: {
1131
+ ...WRITTEN3,
1132
+ email: { level: "partial", shows: "no language: the client removes it, and a screen reader reads the email in its own", source: "caniemail", feature: "html-lang", clients: ["hey", "gmx", "web-de", "orange", "ionos"] }
1133
+ },
1134
+ label: "Language",
1135
+ description: 'Its language, as a BCP 47 tag ("en", "ar").'
1136
+ }),
1137
+ dir: prop(enumOf(["ltr", "rtl", "auto"]), {
1138
+ support: {
1139
+ ...WRITTEN3,
1140
+ email: { level: "partial", values: ["rtl", "auto"], shows: "left to right: the client removes the direction", source: "caniemail", feature: "html-dir", clients: ["gmx", "web-de"] }
1141
+ },
1142
+ label: "Text direction"
1143
+ }),
1144
+ fonts: prop(fontFaces(), {
1145
+ support: {
1146
+ email: { level: "partial", shows: "no web font loads: the next font in each stack shows", source: "caniemail", feature: "css-at-font-face", clients: NO_WEB_FONTS },
1147
+ web: { level: "full", source: "writer" }
1148
+ },
1149
+ label: "Web fonts",
1150
+ description: "The font files the template loads: a family's file for each weight and style it uses. Text names a family in its fontFamily stack, before the fonts to show where it can't load."
1151
+ })
1152
+ };
1153
+ var ROOT_STYLE = {
1154
+ ...inheritedTextStyle,
1155
+ backgroundColor: { ...boxStyle.backgroundColor, label: "Background color", description: "The window's background, behind the content." },
1156
+ ...pick(layoutStyle, "gap", "alignItems")
1157
+ };
1158
+ var ROOT_DEFAULT_STYLE = {
1159
+ color: "#000000",
1160
+ fontFamily: "Arial, Helvetica, sans-serif",
1161
+ fontSize: 16,
1162
+ fontWeight: 400,
1163
+ lineHeight: 1.5,
1164
+ textAlign: "left"
1165
+ };
1166
+ var TEXT_EDITOR = { title: "Text", paths: ["style.fontFamily", "style.fontSize", "style.fontWeight", "style.color", "style.lineHeight", "style.letterSpacing", "style.textAlign"] };
1167
+ var WRITTEN4 = { email: { level: "full", source: "writer" }, web: { level: "full", source: "writer" } };
1168
+ var SIZES = {
1169
+ A3: [297 / 25.4, 420 / 25.4],
1170
+ A4: [210 / 25.4, 297 / 25.4],
1171
+ A5: [148 / 25.4, 210 / 25.4],
1172
+ Letter: [8.5, 11],
1173
+ Legal: [8.5, 14],
1174
+ Tabloid: [11, 17]
1175
+ };
1176
+ var PX_PER_INCH = 96;
1177
+ function pageBox(props) {
1178
+ const size2 = props.size ?? "A4";
1179
+ const [w, h2] = SIZES[size2] ?? SIZES.A4;
1180
+ const landscape = props.orientation === "landscape";
1181
+ const width = Math.round((landscape ? h2 : w) * PX_PER_INCH * 100) / 100;
1182
+ const height = Math.round((landscape ? w : h2) * PX_PER_INCH * 100) / 100;
1183
+ const margin = props.margin ?? { top: 48, right: 48, bottom: 48, left: 48 };
1184
+ return {
1185
+ width,
1186
+ height,
1187
+ margin,
1188
+ contentWidth: Math.round(width - margin.left - margin.right),
1189
+ contentHeight: Math.round(height - margin.top - margin.bottom),
1190
+ css: `${size2} ${landscape ? "landscape" : "portrait"}`
1191
+ };
1192
+ }
1193
+ var PAGE_STYLE = {
1194
+ ...inheritedTextStyle,
1195
+ backgroundColor: { ...boxStyle.backgroundColor, description: "The page's background, out to its edges." },
1196
+ ...pick(layoutStyle, "gap", "alignItems")
1197
+ };
1198
+ var Document = defineComponent({
1199
+ type: "Document",
1200
+ label: "Document",
1201
+ description: "A printed document: its children on pages of its size (Document.Page, or split by PageBreaks), a PageHeader and a PageFooter on each. Renders to print HTML, and to PDF with @hyper-elements/react/pdf.",
1202
+ categories: ["root"],
1203
+ props: {
1204
+ size: prop(enumOf(["A3", "A4", "A5", "Letter", "Legal", "Tabloid"]), { support: WRITTEN4, label: "Page size", default: "A4" }),
1205
+ orientation: prop(enumOf(["portrait", "landscape"]), { support: WRITTEN4, label: "Orientation", default: "portrait" }),
1206
+ margin: prop(insets(), {
1207
+ support: WRITTEN4,
1208
+ label: "Margins",
1209
+ description: "Around each page's content (its header and footer too), in px: 96 to the inch.",
1210
+ default: { top: 48, right: 48, bottom: 48, left: 48 }
1211
+ }),
1212
+ ...ROOT_PROPS
1213
+ },
1214
+ style: { ...ROOT_STYLE, backgroundColor: { ...ROOT_STYLE.backgroundColor, description: "Each page's background, out to its edges." } },
1215
+ defaultStyle: ROOT_DEFAULT_STYLE,
1216
+ children: { types: ["DocumentPage", "PageBreak", "PageHeader", "PageFooter"], categories: ["section", "layout", "block"] },
1217
+ sizing: "none",
1218
+ targets: { web: {} },
1219
+ editor: {
1220
+ groups: [
1221
+ { title: "Document", paths: ["props.size", "props.orientation", "props.margin", "props.title"] },
1222
+ { title: "Background", paths: ["style.backgroundColor"] },
1223
+ TEXT_EDITOR
1224
+ ]
1225
+ }
1226
+ });
1227
+ var DocumentPage = defineComponent({
1228
+ type: "DocumentPage",
1229
+ label: "Page",
1230
+ description: "One page of a Document (Document.Page in JSX): its children stacked down it, inside the Document's margins.",
1231
+ categories: [],
1232
+ props: {},
1233
+ style: PAGE_STYLE,
1234
+ children: { categories: ["section", "layout", "block"] },
1235
+ sizing: "stretch",
1236
+ targets: { web: {} },
1237
+ editor: { groups: [{ title: "Page", paths: ["style.backgroundColor", "style.gap"] }, TEXT_EDITOR] }
1238
+ });
1239
+ var PageBreak = defineComponent({
1240
+ type: "PageBreak",
1241
+ label: "Page break",
1242
+ description: "In a Document, where a page ends: what follows starts the next page.",
1243
+ categories: [],
1244
+ props: {},
1245
+ style: {},
1246
+ children: {},
1247
+ sizing: "none",
1248
+ targets: { web: {} }
1249
+ });
1250
+ var HEADER_STYLE = { ...inheritedTextStyle, ...pick(boxStyle, "padding", "backgroundColor", "border"), ...pick(layoutStyle, "gap", "alignItems") };
1251
+ var PageHeader = defineComponent({
1252
+ type: "PageHeader",
1253
+ label: "Page header",
1254
+ description: "In a Document: drawn at the top of every page, inside its margins.",
1255
+ categories: [],
1256
+ props: {},
1257
+ style: HEADER_STYLE,
1258
+ children: { categories: ["layout", "block"] },
1259
+ sizing: "stretch",
1260
+ targets: { web: {} },
1261
+ editor: { groups: [{ title: "Header", paths: ["style.padding", "style.backgroundColor", "style.border", "style.gap"] }, TEXT_EDITOR] }
1262
+ });
1263
+ var PageFooter = defineComponent({
1264
+ type: "PageFooter",
1265
+ label: "Page footer",
1266
+ description: "In a Document: drawn at the bottom of every page, inside its margins.",
1267
+ categories: [],
1268
+ props: {},
1269
+ style: HEADER_STYLE,
1270
+ children: { categories: ["layout", "block"] },
1271
+ sizing: "stretch",
1272
+ targets: { web: {} },
1273
+ editor: { groups: [{ title: "Footer", paths: ["style.padding", "style.backgroundColor", "style.border", "style.gap"] }, TEXT_EDITOR] }
1274
+ });
1275
+ var PageNumber = defineComponent({
1276
+ type: "PageNumber",
1277
+ label: "Page number",
1278
+ description: "In a Document's text: the number of the page it's on, or (total) how many pages there are.",
1279
+ categories: ["inline"],
1280
+ props: {
1281
+ total: prop(boolean(), { support: WRITTEN4, label: "Page count", description: "How many pages the Document has, instead of this page's number.", default: false })
1282
+ },
1283
+ style: {},
1284
+ children: {},
1285
+ sizing: "none",
1286
+ targets: { web: {} },
1287
+ // A Page's or a Canvas's text has no page to number.
1288
+ roots: ["Document"],
1289
+ notAllowed: { code: "document/page-number", message: (root) => `A PageNumber is a page's number in a Document; this ${root} has no pages` }
1290
+ });
1291
+ function documentPages(root) {
1292
+ const pages = [];
1293
+ let run = [];
1294
+ const end = () => {
1295
+ if (run.length) pages.push({ children: run });
1296
+ run = [];
1297
+ };
1298
+ let header;
1299
+ let footer;
1300
+ for (const child of root.children) {
1301
+ if (typeof child === "string") continue;
1302
+ if (child.type === "PageHeader") header = child;
1303
+ else if (child.type === "PageFooter") footer = child;
1304
+ else if (child.type === "PageBreak") end();
1305
+ else if (child.type === "DocumentPage") {
1306
+ end();
1307
+ pages.push({ page: child, children: child.children.filter((c) => typeof c !== "string") });
1308
+ } else run.push(child);
1309
+ }
1310
+ end();
1311
+ if (!pages.length) pages.push({ children: [] });
1312
+ return { pages, header, footer };
1313
+ }
1314
+ var WRITTEN5 = { web: { level: "full", source: "writer" } };
1315
+ var coordinate = (label, description) => prop(number(), { support: WRITTEN5, label, ...{}, default: 0 });
1316
+ var size = (label) => prop(number({ min: 0 }), { support: WRITTEN5, label, required: true });
1317
+ var Canvas = defineComponent({
1318
+ type: "Canvas",
1319
+ label: "Canvas",
1320
+ description: "An image of a fixed size (a social card, a banner): its children laid out in it as on a page. Written as PNG or SVG with @hyper-elements/react/image.",
1321
+ categories: ["root"],
1322
+ props: {
1323
+ width: prop(length(["px"], { min: 16 }), { support: WRITTEN5, label: "Width", description: "The image's width, in px.", default: 1200 }),
1324
+ height: prop(length(["px"], { min: 16 }), { support: WRITTEN5, label: "Height", description: "The image's height, in px: what's past it is cut.", default: 630 }),
1325
+ ...ROOT_PROPS
1326
+ },
1327
+ style: { ...ROOT_STYLE, padding: boxStyle.padding, justifyContent: layoutStyle.justifyContent },
1328
+ defaultStyle: ROOT_DEFAULT_STYLE,
1329
+ children: { categories: ["section", "layout", "block"] },
1330
+ sizing: "none",
1331
+ targets: { web: {} },
1332
+ editor: {
1333
+ groups: [
1334
+ { title: "Canvas", paths: ["props.width", "props.height", "style.backgroundColor", "style.padding"] },
1335
+ { title: "Layout", paths: ["style.gap", "style.alignItems", "style.justifyContent"] },
1336
+ TEXT_EDITOR
1337
+ ]
1338
+ }
1339
+ });
1340
+ var Svg = defineComponent({
1341
+ type: "Svg",
1342
+ label: "Vector drawing",
1343
+ description: "Shapes (Rect, Circle, Ellipse, Line, Polygon, Path) drawn in a box of its own: its width and height, and the coordinates its viewBox gives them.",
1344
+ categories: ["block"],
1345
+ props: {
1346
+ width: size("Width"),
1347
+ height: size("Height"),
1348
+ viewBox: prop(text({ maxLength: 100 }), { support: WRITTEN5, label: "View box", description: `The shapes' coordinates in it, as "x y width height" (its width and height by default).` }),
1349
+ title: prop(text({ maxLength: 200 }), { support: WRITTEN5, label: "Title", description: "What it shows, read out by screen readers." })
1350
+ },
1351
+ style: { ...pick(boxStyle, "padding", "backgroundColor", "border", "borderRadius"), ...placementStyle },
1352
+ children: { categories: ["shape"] },
1353
+ sizing: "shrink",
1354
+ targets: { web: {} },
1355
+ unsupported: { email: "Gmail and classic Outlook don't show inline SVG; use an Image of it (a PNG) instead" },
1356
+ editor: { groups: [{ title: "Drawing", paths: ["props.width", "props.height", "props.viewBox", "props.title"] }] }
1357
+ });
1358
+ var PAINT = {
1359
+ fill: { kind: color(), label: "Fill", description: 'Its inside ("transparent" for none).', support: { web: css } },
1360
+ stroke: { kind: color(), label: "Stroke", description: "Its outline's color.", support: { web: css } },
1361
+ strokeWidth: { kind: number({ min: 0 }), label: "Stroke width", support: { web: css } },
1362
+ opacity: { kind: number({ min: 0, max: 1 }), label: "Opacity", support: { web: css } }
1363
+ };
1364
+ var PAINT_DEFAULT = { fill: "#000000" };
1365
+ var shape = (type, label, description, props) => defineComponent({ type, label, description, categories: ["shape"], props, style: PAINT, defaultStyle: PAINT_DEFAULT, children: {}, sizing: "none", targets: { web: {} } });
1366
+ var Rect = shape("Rect", "Rectangle", "A rectangle at x, y in its Svg, rounded by rx.", {
1367
+ x: coordinate("X"),
1368
+ y: coordinate("Y"),
1369
+ width: size("Width"),
1370
+ height: size("Height"),
1371
+ rx: prop(number({ min: 0 }), { support: WRITTEN5, label: "Corner radius" })
1372
+ });
1373
+ var Circle = shape("Circle", "Circle", "A circle centered at cx, cy in its Svg.", { cx: coordinate("Center X"), cy: coordinate("Center Y"), r: size("Radius") });
1374
+ var Ellipse = shape("Ellipse", "Ellipse", "An ellipse centered at cx, cy in its Svg.", { cx: coordinate("Center X"), cy: coordinate("Center Y"), rx: size("Radius across"), ry: size("Radius down") });
1375
+ var Line = shape("Line", "Line", "A straight line from x1, y1 to x2, y2 in its Svg (drawn by its stroke).", {
1376
+ x1: coordinate("Start X"),
1377
+ y1: coordinate("Start Y"),
1378
+ x2: coordinate("End X"),
1379
+ y2: coordinate("End Y")
1380
+ });
1381
+ var Polygon = shape("Polygon", "Polygon", 'A closed shape through its points ("0,0 40,0 20,30"), in its Svg.', {
1382
+ points: prop(text({ maxLength: 1e4 }), { support: WRITTEN5, label: "Points", required: true })
1383
+ });
1384
+ var Path = shape("Path", "Path", "A shape drawn by an SVG path (d), in its Svg.", {
1385
+ d: prop(text({ maxLength: 1e5 }), { support: WRITTEN5, label: "Path", description: 'SVG path data ("M0 0 L40 0 L20 30 Z").', required: true })
1386
+ });
1387
+ var SHAPES = [Rect, Circle, Ellipse, Line, Polygon, Path];
1388
+ var WRITTEN6 = { email: { level: "full", source: "writer" }, web: { level: "full", source: "writer" } };
1389
+ var TEXT_DIR = {
1390
+ dir: prop(enumOf(["ltr", "rtl", "auto"]), { support: WRITTEN6, label: "Text direction", description: "Its text's direction, where it differs from the template's; it starts on that side unless aligned otherwise." })
1391
+ };
1392
+ var SPACING_EDITOR = { title: "Spacing", paths: ["style.padding"] };
1393
+ var Email = defineComponent({
1394
+ type: "Email",
1395
+ label: "Email",
1396
+ description: "An email. Its children are stacked down the content width, centered in the window.",
1397
+ categories: ["root"],
1398
+ props: {
1399
+ width: prop(length(["px"], { min: 200 }), { support: WRITTEN6, label: "Content width", default: 600 }),
1400
+ previewText: prop(text(), { support: WRITTEN6, label: "Preview text", description: "Shown after the subject in inbox lists, not in the email." }),
1401
+ ...ROOT_PROPS
1402
+ },
1403
+ style: ROOT_STYLE,
1404
+ defaultStyle: ROOT_DEFAULT_STYLE,
1405
+ children: { categories: ["section", "layout", "block"] },
1406
+ sizing: "none",
1407
+ responsive: true,
1408
+ targets: { email: {} },
1409
+ editor: {
1410
+ groups: [
1411
+ { title: "Email", paths: ["props.width", "props.previewText", "props.title"] },
1412
+ { title: "Background", paths: ["style.backgroundColor"] },
1413
+ TEXT_EDITOR
1414
+ ]
1415
+ }
1416
+ });
1417
+ var Page = defineComponent({
1418
+ type: "Page",
1419
+ label: "Page",
1420
+ description: "A web page. Its children are stacked down the content width, centered in the window.",
1421
+ categories: ["root"],
1422
+ props: {
1423
+ width: prop(length(["px"], { min: 200 }), { support: WRITTEN6, label: "Content width", description: "The widest the content gets.", default: 1200 }),
1424
+ description: prop(text(), { support: WRITTEN6, label: "Description", description: "The page's meta description." }),
1425
+ breakpoints: prop(breakpoints(), {
1426
+ support: { web: { level: "full", source: "writer" } },
1427
+ label: "Breakpoints",
1428
+ description: "Where its phone styles start: mobile, the widest window they apply to, in px (480 where it's not set)."
1429
+ }),
1430
+ ...ROOT_PROPS
1431
+ },
1432
+ style: ROOT_STYLE,
1433
+ defaultStyle: ROOT_DEFAULT_STYLE,
1434
+ children: { categories: ["section", "layout", "block"] },
1435
+ sizing: "none",
1436
+ responsive: true,
1437
+ // The page writer writes the root's phone gap, not its phone text styles.
1438
+ conditionSupport: {
1439
+ mobile: Object.fromEntries(
1440
+ ["fontSize", "lineHeight", "letterSpacing", "textAlign"].map((key) => [key, { web: { level: "none", shows: "its desktop value: set phone text styles on its Sections or Columns", source: "writer" } }])
1441
+ )
1442
+ },
1443
+ targets: { web: {} },
1444
+ editor: {
1445
+ groups: [
1446
+ { title: "Page", paths: ["props.width", "props.title", "props.description"] },
1447
+ { title: "Background", paths: ["style.backgroundColor"] },
1448
+ TEXT_EDITOR
1449
+ ]
1450
+ }
1451
+ });
1452
+ var Section = defineComponent({
1453
+ type: "Section",
1454
+ label: "Section",
1455
+ description: "A band across the whole window, directly in an Email or Page: its background spans the window, its children are stacked at the content width.",
1456
+ categories: ["section"],
1457
+ props: {},
1458
+ style: { ...inheritedTextStyle, ...pick(boxStyle, "backgroundColor", "padding", "border"), ...pick(layoutStyle, "gap", "alignItems") },
1459
+ children: { categories: ["layout", "block"] },
1460
+ sizing: "stretch",
1461
+ targets: { email: {}, web: {} },
1462
+ editor: { groups: [{ title: "Section", paths: ["style.backgroundColor", "style.padding", "style.border", "style.gap"] }, TEXT_EDITOR] }
1463
+ });
1464
+ var Row = defineComponent({
1465
+ type: "Row",
1466
+ label: "Row",
1467
+ description: "Lays its children out side by side. A child takes its width, or a share of what's left (its flex, 1 by default).",
1468
+ categories: ["layout", "block"],
1469
+ props: {
1470
+ stackOnMobile: prop(boolean(), { support: WRITTEN6, label: "Stack on phones", description: "Its children stack full-width on phones (in an Email or a Page).", default: true }),
1471
+ ...TEXT_DIR
1472
+ },
1473
+ style: {
1474
+ ...inheritedTextStyle,
1475
+ ...boxStyle,
1476
+ ...layoutStyle,
1477
+ alignItems: {
1478
+ ...layoutStyle.alignItems,
1479
+ support: { email: { level: "partial", values: ["stretch"], shows: "children not stretched to one height: each background or border ends with its content", source: "writer" }, web: css }
1480
+ },
1481
+ justifyContent: {
1482
+ ...layoutStyle.justifyContent,
1483
+ support: { email: { level: "none", values: ["space-between"], shows: "children start at the left", source: "writer" }, web: css }
1484
+ },
1485
+ flexWrap: {
1486
+ kind: enumOf(["nowrap", "wrap", "words"]),
1487
+ label: "Wrap",
1488
+ description: `"wrap": children that don't fit wrap onto another line, as CSS flex-wrap (in an email, in a Row kept side by side on phones, with no gap between the lines). "words": in an email, also a space of the text's font apart, on lines at least as tall as its, as words are.`,
1489
+ // A Row kept side by side wraps in an email (but classic Outlook, which the email's checks say where it matters).
1490
+ support: {
1491
+ email: { level: "none", values: ["wrap", "words"], when: (_, row) => row.props.stackOnMobile !== false, shows: "children on one line on desktop, stacked on phones", source: "writer" },
1492
+ web: css
1493
+ }
1494
+ },
1495
+ ...placementStyle
1496
+ },
1497
+ children: { categories: ["layout", "block"] },
1498
+ sizing: "stretch",
1499
+ targets: { email: {}, web: {} },
1500
+ editor: {
1501
+ groups: [
1502
+ { title: "Layout", paths: ["style.gap", "style.alignItems", "style.justifyContent", "props.stackOnMobile"] },
1503
+ { title: "Box", paths: ["style.backgroundColor", "style.padding", "style.border", "style.borderRadius"] }
1504
+ ]
1505
+ }
1506
+ });
1507
+ var Column = defineComponent({
1508
+ type: "Column",
1509
+ label: "Column",
1510
+ description: "Stacks its children vertically, with its own spacing and box.",
1511
+ categories: ["layout", "block"],
1512
+ props: {},
1513
+ style: {
1514
+ ...inheritedTextStyle,
1515
+ ...boxStyle,
1516
+ ...layoutStyle,
1517
+ justifyContent: {
1518
+ ...layoutStyle.justifyContent,
1519
+ support: { email: { level: "partial", values: ["center", "end", "space-between"], shows: "no effect: its children start at the top", source: "writer" }, web: css }
1520
+ },
1521
+ ...placementStyle
1522
+ },
1523
+ children: { categories: ["layout", "block"] },
1524
+ sizing: "stretch",
1525
+ targets: { email: {}, web: {} },
1526
+ editor: {
1527
+ groups: [
1528
+ { title: "Layout", paths: ["style.gap", "style.alignItems"] },
1529
+ { title: "Box", paths: ["style.backgroundColor", "style.padding", "style.border", "style.borderRadius", "style.width"] }
1530
+ ]
1531
+ }
1532
+ });
1533
+ var TEXT_BLOCK_STYLE = {
1534
+ ...textStyle,
1535
+ ...pick(boxStyle, "width", "maxWidth", "padding", "backgroundColor", "border", "borderRadius"),
1536
+ ...placementStyle
1537
+ };
1538
+ var TEXT_CHILDREN = { text: true, categories: ["inline"] };
1539
+ var Text = defineComponent({
1540
+ type: "Text",
1541
+ label: "Text",
1542
+ description: "A paragraph of text.",
1543
+ categories: ["block"],
1544
+ props: TEXT_DIR,
1545
+ style: TEXT_BLOCK_STYLE,
1546
+ children: { ...TEXT_CHILDREN, types: ["Image"] },
1547
+ sizing: "stretch",
1548
+ targets: { email: {}, web: {} },
1549
+ editor: { groups: [TEXT_EDITOR, SPACING_EDITOR] }
1550
+ });
1551
+ var Heading = defineComponent({
1552
+ type: "Heading",
1553
+ label: "Heading",
1554
+ description: "A heading, from level 1 (the page's title) to 6.",
1555
+ categories: ["block"],
1556
+ props: {
1557
+ level: prop(enumOf([1, 2, 3, 4, 5, 6]), { support: WRITTEN6, label: "Level", default: 1 }),
1558
+ ...TEXT_DIR
1559
+ },
1560
+ style: TEXT_BLOCK_STYLE,
1561
+ defaultStyle: { fontWeight: 700, lineHeight: 1.25 },
1562
+ defaultStyleBy: {
1563
+ prop: "level",
1564
+ values: { 1: { fontSize: 32 }, 2: { fontSize: 24 }, 3: { fontSize: 20 }, 4: { fontSize: 18 }, 5: { fontSize: 16 }, 6: { fontSize: 14 } }
1565
+ },
1566
+ children: TEXT_CHILDREN,
1567
+ sizing: "stretch",
1568
+ targets: { email: {}, web: {} },
1569
+ editor: { groups: [{ title: "Heading", paths: ["props.level"] }, TEXT_EDITOR, SPACING_EDITOR] }
1570
+ });
1571
+ var RUN_STYLE = {
1572
+ ...pick(textStyle, "color", "fontFamily", "fontSize", "fontWeight", "fontStyle", "letterSpacing", "textTransform", "textDecoration"),
1573
+ backgroundColor: boxStyle.backgroundColor
1574
+ };
1575
+ var Span = defineComponent({
1576
+ type: "Span",
1577
+ label: "Span",
1578
+ description: "Styled text inside text; raised or lowered (superscript, subscript) with verticalAlign.",
1579
+ categories: ["inline"],
1580
+ props: {},
1581
+ style: {
1582
+ ...RUN_STYLE,
1583
+ verticalAlign: {
1584
+ kind: enumOf(["baseline", "super", "sub"]),
1585
+ label: "Raise or lower",
1586
+ description: '"super": superscript (\xAE, a footnote mark); "sub": subscript; as CSS vertical-align.',
1587
+ // Written as <sup> and <sub>, which every client (Word too) raises and lowers.
1588
+ support: { email: { level: "full", source: "caniemail" }, web: css }
1589
+ }
1590
+ },
1591
+ children: { text: true, categories: ["inline"] },
1592
+ sizing: "none",
1593
+ targets: { email: {}, web: {} }
1594
+ });
1595
+ var Link = defineComponent({
1596
+ type: "Link",
1597
+ label: "Link",
1598
+ description: "A link: text inside text, or around an Image.",
1599
+ categories: ["inline", "block"],
1600
+ props: {
1601
+ href: prop(link(), { support: WRITTEN6, label: "Link", required: true }),
1602
+ title: prop(text(), { support: WRITTEN6, label: "Title", description: "Shown as a tooltip by mail clients and browsers that show one." }),
1603
+ "aria-label": prop(text(), { support: WRITTEN6, label: "Label for screen readers", description: `What a screen reader says for it, where its text alone doesn't say enough ("Read more about pricing").` }),
1604
+ attrs: prop(attributes(), { support: WRITTEN6, editable: false, label: "More attributes", description: "More attributes for it: a class, a `data-` attribute." })
1605
+ },
1606
+ style: { ...RUN_STYLE, ...placementStyle },
1607
+ defaultStyle: { color: "#0000ee", textDecoration: "underline" },
1608
+ hover: ["color", "textDecoration", "backgroundColor"],
1609
+ children: { text: true, categories: ["inline"], types: ["Image"] },
1610
+ sizing: "shrink",
1611
+ targets: { email: {}, web: {} },
1612
+ editor: { groups: [{ title: "Link", paths: ["props.href", "style.color", "style.textDecoration", "hover.color"] }] }
1613
+ });
1614
+ var buttonColors = {
1615
+ editor: "buttonColors",
1616
+ covers: ["style.color", "style.backgroundColor", "hover.color", "hover.backgroundColor"],
1617
+ read: (node) => ({
1618
+ color: node.style?.color,
1619
+ backgroundColor: node.style?.backgroundColor,
1620
+ hoverColor: node.hover?.color,
1621
+ hoverBackgroundColor: node.hover?.backgroundColor
1622
+ }),
1623
+ write: (value) => ({
1624
+ style: { color: value.color, backgroundColor: value.backgroundColor },
1625
+ hover: { color: value.hoverColor, backgroundColor: value.hoverBackgroundColor }
1626
+ })
1627
+ };
1628
+ var Button = defineComponent({
1629
+ type: "Button",
1630
+ label: "Button",
1631
+ description: "A link drawn as a button.",
1632
+ categories: ["block"],
1633
+ props: {
1634
+ href: prop(link(), { support: WRITTEN6, label: "Link", required: true }),
1635
+ title: prop(text(), { support: WRITTEN6, label: "Title", description: "Shown as a tooltip by mail clients and browsers that show one." }),
1636
+ "aria-label": prop(text(), { support: WRITTEN6, label: "Label for screen readers", description: `What a screen reader says for it, where its text alone doesn't say enough ("Read more about pricing").` }),
1637
+ attrs: prop(attributes(), { support: WRITTEN6, editable: false, label: "More attributes", description: "More attributes for its link: a class, a `data-` attribute." })
1638
+ },
1639
+ style: {
1640
+ ...textStyle,
1641
+ ...pick(boxStyle, "width", "maxWidth", "padding", "backgroundColor", "border", "borderRadius"),
1642
+ boxSizing: { kind: enumOf(["border-box", "content-box"]), label: "Width holds", description: "Whether its width holds its padding and border (border-box), or they go on top of it (content-box).", support: { email: { level: "none", shows: "a width that holds them", source: "writer" }, web: css } },
1643
+ alignSelf: { kind: enumOf(["start", "center", "end"]), label: "Place", description: "Where it sits in its stack, whatever the stack's alignItems: on phones too, another place than on desktop.", mobile: true, support: { email: { level: "full", source: "writer" }, web: css } },
1644
+ ...placementStyle
1645
+ },
1646
+ defaultStyle: {
1647
+ color: "#ffffff",
1648
+ backgroundColor: "#0879a1",
1649
+ fontSize: 14,
1650
+ lineHeight: 1.2,
1651
+ textAlign: "center",
1652
+ padding: { top: 10, right: 20, bottom: 10, left: 20 },
1653
+ borderRadius: 4
1654
+ },
1655
+ hover: ["color", "backgroundColor"],
1656
+ children: { text: true, types: ["Span", "MergeTag", "LineBreak"] },
1657
+ sizing: "shrink",
1658
+ targets: { email: {}, web: {} },
1659
+ editor: {
1660
+ groups: [
1661
+ { title: "Button", paths: ["props.href", "style.width"] },
1662
+ { title: "Colors", composite: buttonColors },
1663
+ TEXT_EDITOR,
1664
+ { title: "Spacing", paths: ["style.padding", "style.border", "style.borderRadius"] }
1665
+ ]
1666
+ }
1667
+ });
1668
+ var Image = defineComponent({
1669
+ type: "Image",
1670
+ label: "Image",
1671
+ description: "An image in the content. Without a width it takes its own size, no wider than its parent.",
1672
+ categories: ["block"],
1673
+ props: {
1674
+ src: prop(asset(), { support: WRITTEN6, label: "Image", required: true }),
1675
+ alt: prop(text(), { support: WRITTEN6, label: "Alternate text", description: "Read out by screen readers, and shown where images don't load.", default: "" }),
1676
+ title: prop(text(), { support: WRITTEN6, label: "Title", description: "Shown as a tooltip by mail clients and browsers that show one." })
1677
+ },
1678
+ style: {
1679
+ ...pick(boxStyle, "width", "maxWidth", "height", "border", "borderRadius"),
1680
+ ...placementStyle,
1681
+ alignSelf: { kind: enumOf(["start", "center", "end"]), label: "Place", description: "Where it sits in its stack on phones, where that's another place than on desktop.", mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
1682
+ verticalAlign: { kind: enumOf(["top", "middle"]), label: "Place in its line", description: "Where it sits in a line of text it's in.", support: { email: { level: "full", source: "caniemail" }, web: css } }
1683
+ },
1684
+ children: {},
1685
+ sizing: "shrink",
1686
+ conditionSupport: { mobile: { border: { email: { level: "none", shows: "its desktop border", source: "writer" } } } },
1687
+ targets: { email: {}, web: {} },
1688
+ editor: { groups: [{ title: "Image", paths: ["props.src", "props.alt", "style.width"] }, { title: "Box", paths: ["style.border", "style.borderRadius"] }] }
1689
+ });
1690
+ var Divider = defineComponent({
1691
+ type: "Divider",
1692
+ label: "Divider",
1693
+ description: "A horizontal line.",
1694
+ categories: ["block"],
1695
+ props: {},
1696
+ style: {
1697
+ borderTop: { kind: line(), label: "Line", description: 'The line, as CSS border-top: "1px solid #bbbbbb".', support: { email: { level: "full", source: "writer" }, web: css } },
1698
+ width: { kind: length(["px", "%"]), label: "Length", mobile: true, support: { email: { level: "full", source: "writer" }, web: css } },
1699
+ padding: { kind: insets(), label: "Padding", mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } }
1700
+ },
1701
+ defaultStyle: { borderTop: { width: 1, style: "solid", color: "#bbbbbb" }, width: "100%" },
1702
+ children: {},
1703
+ sizing: "stretch",
1704
+ targets: { email: {}, web: {} },
1705
+ editor: { groups: [{ title: "Line", paths: ["style.borderTop", "style.width"] }, SPACING_EDITOR] }
1706
+ });
1707
+ var LineBreak = defineComponent({
1708
+ type: "LineBreak",
1709
+ label: "Line break",
1710
+ description: "Starts a new line inside text.",
1711
+ categories: ["inline"],
1712
+ props: {},
1713
+ style: {},
1714
+ children: {},
1715
+ sizing: "none",
1716
+ targets: { email: {}, web: {} }
1717
+ });
1718
+ var MergeTag = defineComponent({
1719
+ type: "MergeTag",
1720
+ label: "Merge tag",
1721
+ description: "A value the sending service fills in (a name, a link), written in the service's own syntax.",
1722
+ categories: ["inline"],
1723
+ props: {
1724
+ name: prop(text({ maxLength: 200 }), { support: WRITTEN6, label: "Name", required: true })
1725
+ },
1726
+ style: {},
1727
+ children: {},
1728
+ sizing: "none",
1729
+ targets: { email: {}, web: {} }
1730
+ });
1731
+ var List = defineComponent({
1732
+ type: "List",
1733
+ label: "List",
1734
+ description: "A bulleted or numbered list of ListItems. Its markers sit in a column of their own (indent) before the items.",
1735
+ categories: ["block"],
1736
+ props: {
1737
+ type: prop(enumOf(["bullet", "number"]), { support: WRITTEN6, label: "Type", default: "bullet" }),
1738
+ start: prop(number({ min: 0, integer: true }), { support: WRITTEN6, label: "Starts at", description: "The first item's number, in a numbered list.", default: 1 })
1739
+ },
1740
+ style: {
1741
+ ...inheritedTextStyle,
1742
+ ...pick(boxStyle, "width", "maxWidth", "padding", "backgroundColor", "border"),
1743
+ gap: { kind: length(["px"]), label: "Gap", description: "Space between items.", mobile: true, support: { email: { level: "full", source: "writer" }, web: css } },
1744
+ indent: { kind: length(["px"], { min: 0 }), label: "Indent", description: "The width of the markers' column, before the items.", support: { email: { level: "full", source: "writer" }, web: css } },
1745
+ ...placementStyle
1746
+ },
1747
+ defaultStyle: { indent: 24 },
1748
+ children: { types: ["ListItem"] },
1749
+ sizing: "stretch",
1750
+ targets: { email: {}, web: {} },
1751
+ editor: { groups: [{ title: "List", paths: ["props.type", "props.start", "style.gap", "style.indent"] }, TEXT_EDITOR, SPACING_EDITOR] }
1752
+ });
1753
+ var ListItem = defineComponent({
1754
+ type: "ListItem",
1755
+ label: "List item",
1756
+ description: "An item of a List: text, and a List of its own to nest one.",
1757
+ categories: [],
1758
+ props: TEXT_DIR,
1759
+ style: pick(textStyle, "color", "fontFamily", "fontSize", "fontWeight", "fontStyle", "lineHeight", "letterSpacing", "textAlign", "textTransform", "textDecoration"),
1760
+ children: { text: true, categories: ["inline"], types: ["List"] },
1761
+ sizing: "stretch",
1762
+ targets: { email: {}, web: {} },
1763
+ editor: { groups: [TEXT_EDITOR] }
1764
+ });
1765
+ var Table = defineComponent({
1766
+ type: "Table",
1767
+ label: "Table",
1768
+ description: "A table of data: TableRows of TableCells. Columns take their cells' widths; the rest share what's left.",
1769
+ categories: ["block"],
1770
+ props: {},
1771
+ style: {
1772
+ ...inheritedTextStyle,
1773
+ ...pick(boxStyle, "width", "maxWidth", "backgroundColor", "border"),
1774
+ cellPadding: { kind: insets(), label: "Cell padding", description: "Every cell's padding, where the cell doesn't set its own.", support: { email: { level: "full", source: "writer" }, web: css } },
1775
+ rowBorder: { kind: line(), label: "Line between rows", support: { email: { level: "full", source: "writer" }, web: css } },
1776
+ borderCollapse: { kind: enumOf(["separate", "collapse"]), label: "Cell borders", description: "Whether cells side by side draw a border each (separate) or share one (collapse).", support: { email: { level: "full", source: "caniemail" }, web: css } },
1777
+ ...placementStyle
1778
+ },
1779
+ children: { types: ["TableRow"] },
1780
+ sizing: "stretch",
1781
+ targets: { email: {}, web: {} },
1782
+ editor: { groups: [{ title: "Table", paths: ["style.cellPadding", "style.rowBorder", "style.border", "style.backgroundColor"] }, TEXT_EDITOR] }
1783
+ });
1784
+ var TableRow = defineComponent({
1785
+ type: "TableRow",
1786
+ label: "Table row",
1787
+ description: "A row of a Table. A header row's cells are header cells, in bold.",
1788
+ categories: [],
1789
+ props: {
1790
+ header: prop(boolean(), { support: WRITTEN6, label: "Header", description: "Its cells head the columns below them.", default: false }),
1791
+ footer: prop(boolean(), { support: WRITTEN6, label: "Footer", description: "It ends the table (printed, on each page the table runs to).", default: false })
1792
+ },
1793
+ style: { ...pick(textStyle, "color", "fontFamily", "fontSize", "fontWeight", "fontStyle", "lineHeight", "letterSpacing", "textAlign", "textTransform"), backgroundColor: boxStyle.backgroundColor, height: boxStyle.height },
1794
+ defaultStyleBy: { prop: "header", values: { true: { fontWeight: 700 } } },
1795
+ children: { types: ["TableCell"] },
1796
+ sizing: "stretch",
1797
+ targets: { email: {}, web: {} },
1798
+ editor: { groups: [{ title: "Row", paths: ["props.header", "style.backgroundColor"] }, TEXT_EDITOR] }
1799
+ });
1800
+ var TableCell = defineComponent({
1801
+ type: "TableCell",
1802
+ label: "Table cell",
1803
+ description: "A cell of a TableRow: text, and images. Its width sets its column's.",
1804
+ categories: [],
1805
+ props: {
1806
+ colSpan: prop(number({ min: 1, integer: true }), { support: WRITTEN6, label: "Columns", description: "How many columns it spans.", default: 1 }),
1807
+ ...TEXT_DIR
1808
+ },
1809
+ style: {
1810
+ ...textStyle,
1811
+ ...pick(boxStyle, "padding", "backgroundColor", "border"),
1812
+ width: { kind: length(["px", "%"]), label: "Width", description: "Its column's width (of a cell spanning one column).", support: { email: { level: "full", source: "writer" }, web: css } },
1813
+ verticalAlign: { kind: enumOf(["top", "middle", "bottom"]), label: "Vertical alignment", support: { email: { level: "full", source: "caniemail" }, web: css } }
1814
+ },
1815
+ children: { text: true, categories: ["inline"], types: ["Image"] },
1816
+ sizing: "none",
1817
+ conditionSupport: { mobile: { border: { email: { level: "none", shows: "its desktop border", source: "writer" } } } },
1818
+ targets: { email: {}, web: {} },
1819
+ editor: { groups: [{ title: "Cell", paths: ["props.colSpan", "style.width", "style.verticalAlign"] }, TEXT_EDITOR, { title: "Box", paths: ["style.padding", "style.backgroundColor", "style.border"] }] }
1820
+ });
1821
+ var Video = defineComponent({
1822
+ type: "Video",
1823
+ label: "Video",
1824
+ description: "A YouTube or Vimeo video: its player on a page; in an email (which can't play one), its thumbnail, linked to it.",
1825
+ categories: ["block"],
1826
+ props: {
1827
+ src: prop(video(), { support: WRITTEN6, label: "Video", required: true, description: "The video's YouTube or Vimeo link." }),
1828
+ thumbnail: prop(asset(), {
1829
+ support: { email: { level: "full", source: "writer" }, web: { level: "none", shows: "the video's player: a page plays the video", source: "writer" } },
1830
+ label: "Thumbnail",
1831
+ description: "The image an email shows, linked to the video. A YouTube video's own where there's none; a Vimeo video's can't be found without the network."
1832
+ }),
1833
+ title: prop(text(), { support: WRITTEN6, label: "Title", description: "The player's title on a page, the thumbnail's alternate text in an email.", default: "Video" })
1834
+ },
1835
+ style: { ...pick(boxStyle, "width", "maxWidth"), ...placementStyle },
1836
+ children: {},
1837
+ sizing: "stretch",
1838
+ targets: { email: {}, web: {} },
1839
+ editor: { groups: [{ title: "Video", paths: ["props.src", "props.thumbnail", "props.title", "style.width", "style.maxWidth"] }] }
1840
+ });
1841
+ var Html = defineComponent({
1842
+ type: "Html",
1843
+ label: "HTML",
1844
+ description: "Markup written as it is, for what no component says (the builder's HTML block): in an email and on a page as given, nothing in it checked or escaped. A Document's PDF and a Canvas's image draw its plain text.",
1845
+ categories: ["block"],
1846
+ props: {
1847
+ html: prop(text(), { support: WRITTEN6, label: "HTML", required: true, description: "The markup, written as it is: only markup you trust. Or give it as JSX inside the block, which React writes." }),
1848
+ text: prop(text(), { support: WRITTEN6, label: "Plain text", description: "What it reads as in the plain-text version, and in a PDF or an image (its markup's words where there's none)." })
1849
+ },
1850
+ style: {},
1851
+ children: {},
1852
+ sizing: "stretch",
1853
+ targets: { email: {}, web: {} },
1854
+ editor: { groups: [{ title: "HTML", paths: ["props.html", "props.text"] }] }
1855
+ });
1856
+ var COMPONENTS = Object.freeze(
1857
+ Object.fromEntries(
1858
+ [Email, Page, Document, DocumentPage, PageBreak, PageHeader, PageFooter, PageNumber, Canvas, Svg, ...SHAPES, Section, Row, Column, Grid, Layer, Text, Heading, Span, Link, Button, Image, Divider, LineBreak, MergeTag, List, ListItem, Table, TableRow, TableCell, Video, Social, Menu, Html].map((d) => [
1859
+ d.type,
1860
+ d
1861
+ ])
1862
+ )
1863
+ );
1864
+ var CUSTOM_TYPE = /^[a-z][a-z0-9-]*\.[A-Z][A-Za-z0-9]*$/;
1865
+ function isCustom(definition) {
1866
+ return definition.custom === true;
1867
+ }
1868
+ var Registry = class {
1869
+ constructor(components = []) {
1870
+ this.custom = /* @__PURE__ */ new Map();
1871
+ for (const definition of components) this.add(definition);
1872
+ }
1873
+ add(definition) {
1874
+ if (!isCustom(definition) || !CUSTOM_TYPE.test(definition.type)) {
1875
+ throw new Error(`[Elements] ${JSON.stringify(definition?.type)} isn't a custom component (make one with component())`);
1876
+ }
1877
+ const known = this.custom.get(definition.type);
1878
+ if (known && known !== definition) throw new Error(`[Elements] Two custom components are named ${definition.type}`);
1879
+ this.custom.set(definition.type, definition);
1880
+ }
1881
+ get(type) {
1882
+ if (Object.prototype.hasOwnProperty.call(COMPONENTS, type)) return COMPONENTS[type];
1883
+ return this.custom.get(type);
1884
+ }
1885
+ get customs() {
1886
+ return [...this.custom.values()];
1887
+ }
1888
+ all() {
1889
+ return [...Object.values(COMPONENTS), ...this.custom.values()];
1890
+ }
1891
+ };
1892
+ function propsWithDefaults(definition, props) {
1893
+ const out = {};
1894
+ for (const [key, spec] of Object.entries(definition.props)) if (spec.default !== void 0) out[key] = spec.default;
1895
+ return { ...out, ...props };
1896
+ }
1897
+ function registryOf(options) {
1898
+ return options instanceof Registry ? options : new Registry(options?.components);
1899
+ }
1900
+ var NODE_KEYS = ["id", "type", "props", "style", "mobile", "hover", "hidden", "children"];
1901
+ var ID = /^[A-Za-z][A-Za-z0-9_-]*$/;
1902
+ var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1903
+ function checkDocument(input, options) {
1904
+ const registry = registryOf(options);
1905
+ const d = new Diagnostics();
1906
+ if (!isObject2(input)) {
1907
+ d.error("document/invalid", 'A document is an object: { root: { type: "Email", \u2026 } }', "");
1908
+ return { document: null, diagnostics: d.list };
1909
+ }
1910
+ const extra = Object.keys(input).filter((key) => key !== "root");
1911
+ for (const key of extra) d.error("document/unknown-key", `"${key}" isn't part of a document (root)`, key);
1912
+ const rootDefinition = isObject2(input.root) && typeof input.root.type === "string" ? registry.get(input.root.type) : void 0;
1913
+ const context = { d, registry, used: /* @__PURE__ */ new Set(), root: rootDefinition };
1914
+ const root = input.root === void 0 ? null : readNode(input.root, "root", null, context);
1915
+ if (input.root === void 0) d.error("document/root", 'A document has a root: { type: "Email" }, "Page", \u2026', "root");
1916
+ return { document: root ? { root } : null, diagnostics: d.list };
1917
+ }
1918
+ function checkFragment(input, options) {
1919
+ const d = new Diagnostics();
1920
+ const context = { d, registry: options.registry, idPrefix: options.idPrefix, used: new Set(options.taken), root: options.root };
1921
+ const node = readNode(input, options.path, options.parent, context);
1922
+ return { node, diagnostics: d.list };
1923
+ }
1924
+ function rootList(roots) {
1925
+ const named = roots.map((root) => a(root).replace(/^A/, "a"));
1926
+ return named.length > 1 ? `${named.slice(0, -1).join(", ")} or ${named[named.length - 1]}` : named[0] ?? "no root";
1927
+ }
1928
+ function rootTakes(roots, root) {
1929
+ return !roots || !root || roots.includes(root.type);
1930
+ }
1931
+ function sharesOutput(definition, root) {
1932
+ return Object.keys(definition.targets).some((target) => target in root.targets);
1933
+ }
1934
+ function allowedIn(parent, child) {
1935
+ const rule2 = parent.children;
1936
+ return !!rule2.types?.includes(child.type) || child.categories.some((category) => rule2.categories?.includes(category));
1937
+ }
1938
+ var GUIDE = "@hyper-elements/react/GUIDE.md";
1939
+ var seeing = (message, type) => `${message}${/[?.]$/.test(message) ? "" : "."} See ${type} in ${GUIDE}`;
1940
+ var a = (type) => `${/^[AEIOU]/.test(type) ? "An" : "A"} ${type}`;
1941
+ function nearest(key, names) {
1942
+ const lower = key.toLowerCase();
1943
+ let best;
1944
+ for (const name of names) {
1945
+ const distance = name.toLowerCase() === lower ? 0 : editDistance(lower, name.toLowerCase());
1946
+ if (distance <= (key.length > 4 ? 2 : 1) && (!best || distance < best.distance)) best = { name, distance };
1947
+ }
1948
+ return best?.name;
1949
+ }
1950
+ function editDistance(from, to) {
1951
+ let row = Array.from({ length: to.length + 1 }, (_, i) => i);
1952
+ for (let i = 1; i <= from.length; i++) {
1953
+ const next = [i];
1954
+ for (let j = 1; j <= to.length; j++) next[j] = Math.min(row[j] + 1, next[j - 1] + 1, row[j - 1] + (from[i - 1] === to[j - 1] ? 0 : 1));
1955
+ row = next;
1956
+ }
1957
+ return row[to.length];
1958
+ }
1959
+ var OTHER_BREAKPOINTS = /* @__PURE__ */ new Set(["base", "initial", "xs", "sm", "md", "lg", "xl", "2xl", "tablet", "desktop", "wide", "small", "medium", "large"]);
1960
+ function conditionHint(definition, key) {
1961
+ const name = key.trim().replace(/^(&?:+|_+|\$|@)/, "").replace(/Style$/, "").toLowerCase();
1962
+ const own = CUSTOM_TYPE.test(definition.type);
1963
+ const hoverStyles = definition.hover ?? [];
1964
+ const toHover = own ? `a template's own component takes only its place in a Row in its style: set hover styles in what it renders` : hoverStyles.length ? `hover styles go in its style's hover block, style={{ hover: { \u2026 } }} (it takes ${hoverStyles.join(", ")})` : `${a(definition.type).replace(/^A/, "a")} takes no hover styles (a Button's and a Link's colors do)`;
1965
+ const toPhone = own ? `a template's own component takes only its place in a Row in its style: set phone styles in what it renders` : `phone styles go in its style's mobile block, style={{ mobile: { \u2026 } }}`;
1966
+ if (name === "hover") return toHover;
1967
+ if (name === "mobile" || name === "phone" || name === "phones" || key.trim().startsWith("@media")) return toPhone;
1968
+ if (name === "focus" || name === "focus-visible" || name === "focusvisible" || name === "focus-within") {
1969
+ return hoverStyles.length && !own ? `keyboard focus gets its hover styles: set them in style={{ hover: { \u2026 } }}` : `there are no focus styles; keyboard focus gets a Button's and a Link's hover styles`;
1970
+ }
1971
+ if (OTHER_BREAKPOINTS.has(name)) {
1972
+ return `Elements has one breakpoint, phones: desktop styles go in style itself, phone styles in its mobile block, style={{ mobile: { \u2026 } }} (a Page sets where phones start with breakpoints={{ mobile }})`;
1973
+ }
1974
+ if (name === "active" || name === "pressed" || name === "press" || name === "visited" || name === "dark" || name === "light" || name === "print") {
1975
+ return `there are no ${name} styles: Elements has phone styles (mobile) and hover styles (hover) in style`;
1976
+ }
1977
+ return void 0;
1978
+ }
1979
+ function unknownProp(definition, key) {
1980
+ const condition = conditionHint(definition, key);
1981
+ if (condition) return `${a(definition.type)} has no prop "${key}": ${condition}`;
1982
+ if (Object.prototype.hasOwnProperty.call(definition.style, key)) return `${a(definition.type)} has no prop "${key}": it's a style, style={{ ${key}: \u2026 }}`;
1983
+ const known = Object.keys(definition.props);
1984
+ const meant = nearest(key, known);
1985
+ return seeing(`${a(definition.type)} has no prop "${key}"${meant ? `: did you mean "${meant}"?` : known.length ? ` (it has ${known.join(", ")})` : ""}`, definition.type);
1986
+ }
1987
+ function placementMessage(parent, child) {
1988
+ if (child.categories.includes("section")) return `A ${child.type} goes directly in an Email or Page, not in a ${parent.type}`;
1989
+ if (child.categories.includes("root")) return `A ${child.type} is a root: it can't be inside a ${parent.type}`;
1990
+ const takes = [...parent.children.text ? ["text"] : [], ...(parent.children.categories ?? []).map((c) => `${c} components`), ...parent.children.types ?? []];
1991
+ return takes.length ? seeing(`A ${parent.type} takes ${takes.join(", ")}; a ${child.type} can't go in it`, parent.type) : `A ${parent.type} has no children`;
1992
+ }
1993
+ function readNode(input, path, parent, context) {
1994
+ const { d } = context;
1995
+ if (!isObject2(input)) {
1996
+ d.error("node/invalid", `Expected a component ({ "type": \u2026 }), not ${JSON.stringify(input) ?? String(input)}`, path);
1997
+ return null;
1998
+ }
1999
+ if (typeof input.type !== "string") {
2000
+ d.error("node/type", 'A component has a "type" ("Row", "Heading", \u2026)', `${path}.type`);
2001
+ return null;
2002
+ }
2003
+ const definition = context.registry.get(input.type);
2004
+ if (!definition) {
2005
+ const custom = CUSTOM_TYPE.test(input.type);
2006
+ d.error(
2007
+ "node/unknown-type",
2008
+ custom ? `"${input.type}" is a custom component this document wasn't given: pass it in the components option` : `"${input.type}" isn't a component Elements knows`,
2009
+ `${path}.type`
2010
+ );
2011
+ return null;
2012
+ }
2013
+ if (!parent && !definition.categories.includes("root")) {
2014
+ d.error("root/invalid", `A document's root is an Email or Page, not a ${definition.type}`, `${path}.type`);
2015
+ return null;
2016
+ }
2017
+ if (parent && !allowedIn(parent, definition)) {
2018
+ d.error("children/not-allowed", placementMessage(parent, definition), path);
2019
+ return null;
2020
+ }
2021
+ const root = context.root;
2022
+ if (root && !rootTakes(definition.roots, root) && sharesOutput(definition, root)) {
2023
+ const { code, message } = definition.notAllowed ?? {
2024
+ code: "component/not-supported",
2025
+ message: (under) => `${a(definition.type)} goes in ${rootList(definition.roots ?? [])}, not in ${a(under).replace(/^A/, "a")}`
2026
+ };
2027
+ d.error(code, message(root.type), path);
2028
+ return null;
2029
+ }
2030
+ let id;
2031
+ if (input.id !== void 0) {
2032
+ if (typeof input.id !== "string" || !ID.test(input.id)) {
2033
+ d.error("id/invalid", `An id starts with a letter and holds letters, digits, "_" and "-", not ${JSON.stringify(input.id)}`, `${path}.id`);
2034
+ } else if (context.idPrefix === null) {
2035
+ d.add("id/dropped", "info", `The id "${input.id}" is left out: inside a custom component, ids are kept only when the component is given one (a block used many times can't own an id)`, `${path}.id`);
2036
+ } else {
2037
+ const full = context.idPrefix ? `${context.idPrefix}-${input.id}` : input.id;
2038
+ if (context.used.has(full)) d.error("id/duplicate", `The id "${full}" is already used by another component`, `${path}.id`);
2039
+ else {
2040
+ id = full;
2041
+ context.used.add(full);
2042
+ }
2043
+ }
2044
+ }
2045
+ for (const key of Object.keys(input)) {
2046
+ if (!NODE_KEYS.includes(key)) d.error("node/unknown-key", `"${key}" isn't part of a component (${NODE_KEYS.join(", ")})`, `${path}.${key}`, id);
2047
+ }
2048
+ const node = id === void 0 ? { type: definition.type } : { id, type: definition.type };
2049
+ const props = readProps(input.props, definition, `${path}.props`, id, d, context.root);
2050
+ if (props) node.props = props;
2051
+ const lifted = {};
2052
+ let styleInput = input.style;
2053
+ if (isObject2(styleInput) && !CUSTOM_TYPE.test(definition.type)) {
2054
+ const rest = {};
2055
+ for (const [key, value] of Object.entries(styleInput)) {
2056
+ if (CONDITION_NAMES.has(key)) lifted[key] = value;
2057
+ else rest[key] = value;
2058
+ }
2059
+ styleInput = rest;
2060
+ }
2061
+ const style = readStyles(styleInput, definition, definition.style, `${path}.style`, id, d, "style", () => true, void 0, context.root);
2062
+ if (style) node.style = style;
2063
+ for (const condition of CONDITIONS) {
2064
+ const inStyle = lifted[condition.name];
2065
+ const given2 = input[condition.name];
2066
+ if (inStyle !== void 0 && given2 !== void 0) {
2067
+ d.error(
2068
+ "style/twice",
2069
+ `${condition.name} is given twice: as ${condition.name}={\u2026} and in style. Give it once, in style: style={{ ${condition.name}: { \u2026 } }}`,
2070
+ `${path}.style.${condition.name}`,
2071
+ id
2072
+ );
2073
+ continue;
2074
+ }
2075
+ let value = inStyle ?? given2;
2076
+ if (value === void 0) continue;
2077
+ const at = inStyle !== void 0 ? `${path}.style.${condition.name}` : `${path}.${condition.name}`;
2078
+ if (condition.roots && !condition.roots(context.root)) {
2079
+ if (condition.notAllowed) d.error(condition.notAllowed.code, condition.notAllowed.message, at, id);
2080
+ continue;
2081
+ }
2082
+ if (isObject2(value) && Object.keys(value).some((key) => CONDITION_NAMES.has(key))) {
2083
+ const own = {};
2084
+ for (const [key, inner] of Object.entries(value)) {
2085
+ if (CONDITION_NAMES.has(key)) d.error("style/nested", `${key} can't be set inside ${condition.name}: a block holds styles, not another block`, `${at}.${key}`, id);
2086
+ else own[key] = inner;
2087
+ }
2088
+ value = own;
2089
+ }
2090
+ const read2 = readStyles(value, definition, definition.style, at, id, d, condition.name, (spec, key) => condition.allows(spec, key, definition), condition.for, context.root);
2091
+ if (read2) node[condition.name] = read2;
2092
+ }
2093
+ if (input.hidden !== void 0) {
2094
+ const hidden = readHidden(input.hidden, `${path}.hidden`, id, context);
2095
+ if (hidden) node.hidden = hidden;
2096
+ }
2097
+ const children = readChildren(input.children, definition, `${path}.children`, id, context);
2098
+ if (children.length) node.children = children;
2099
+ return node;
2100
+ }
2101
+ function readProps(input, definition, path, id, d, root) {
2102
+ const out = {};
2103
+ if (input !== void 0 && !isObject2(input)) {
2104
+ d.error("props/invalid", "props is an object", path, id);
2105
+ input = void 0;
2106
+ }
2107
+ for (const [key, value] of Object.entries(input ?? {})) {
2108
+ if (value === void 0) continue;
2109
+ const spec = Object.prototype.hasOwnProperty.call(definition.props, key) ? definition.props[key] : void 0;
2110
+ if (!spec) {
2111
+ d.error("prop/unknown", unknownProp(definition, key), `${path}.${key}`, id);
2112
+ continue;
2113
+ }
2114
+ if (!rootTakes(spec.roots, root)) {
2115
+ d.error("prop/not-supported", `${a(definition.type)}'s "${key}" is for ${rootList(spec.roots)}, not ${a(root.type).replace(/^A/, "a")}`, `${path}.${key}`, id);
2116
+ continue;
2117
+ }
2118
+ const parsed = spec.kind.parse(value);
2119
+ if (parsed.ok) {
2120
+ if (!spec.kind.empty?.(parsed.value)) out[key] = parsed.value;
2121
+ } else d.error("prop/invalid", `${key}: ${parsed.message}`, `${path}.${key}`, id);
2122
+ }
2123
+ for (const [key, spec] of Object.entries(definition.props)) {
2124
+ if (spec.required && !(key in out) && !(isObject2(input) && input[key] !== void 0)) {
2125
+ d.error("prop/required", `A ${definition.type} needs "${key}" (${spec.kind.accepts})`, `${path}.${key}`, id);
2126
+ }
2127
+ }
2128
+ return Object.keys(out).length ? out : void 0;
2129
+ }
2130
+ var UNSUPPORTED_CSS = {
2131
+ margin: "use the parent's gap for space between siblings, or padding for space inside",
2132
+ marginTop: "use the parent's gap, or padding",
2133
+ marginBottom: "use the parent's gap, or padding",
2134
+ marginLeft: "use padding",
2135
+ marginRight: "use padding",
2136
+ display: "the component sets it",
2137
+ position: "use a Layer for content placed over other content",
2138
+ backgroundImage: "put a Layer around it, its first child an Image (the Layer's height crops it to cover), the rest over it",
2139
+ background: "backgroundColor for a color; for an image, a Layer: its first child an Image, the rest over it",
2140
+ float: "use a Row to put things side by side",
2141
+ // Names Elements used before it took CSS's.
2142
+ align: "it's alignItems now",
2143
+ justify: `it's justifyContent now ("between" is "space-between")`,
2144
+ wrap: "a Row stays side by side on phones with its stackOnMobile prop set to false; flexWrap wraps it on the web",
2145
+ color: "a Divider's line is its borderTop, with its color",
2146
+ thickness: `a Divider's line is borderTop now: "1px solid #bbbbbb"`,
2147
+ lineStyle: `a Divider's line is borderTop now: "1px solid #bbbbbb"`
2148
+ };
2149
+ function styleHint(definition, specs, key) {
2150
+ if (Object.prototype.hasOwnProperty.call(definition.props, key)) return `: it's a prop, ${key}={\u2026}`;
2151
+ if (key === "padding" && definition.categories.includes("root")) return ": a root has no padding; put it on a Column (a card) or a Section (a band) inside it";
2152
+ if (UNSUPPORTED_CSS[key]) return `: ${UNSUPPORTED_CSS[key]}`;
2153
+ const condition = conditionHint(definition, key);
2154
+ if (condition) return `: ${condition}`;
2155
+ const meant = nearest(key, Object.keys(specs));
2156
+ return meant ? `: did you mean "${meant}"?` : "";
2157
+ }
2158
+ function readStyles(input, definition, specs, path, id, d, what, allowed, forLabel, root) {
2159
+ if (input === void 0) return void 0;
2160
+ if (!isObject2(input)) {
2161
+ d.error(`${what}/invalid`, `${what} is an object of style properties`, path, id);
2162
+ return void 0;
2163
+ }
2164
+ const out = {};
2165
+ for (const [key, value] of Object.entries(input)) {
2166
+ if (value === void 0) continue;
2167
+ const spec = Object.prototype.hasOwnProperty.call(specs, key) ? specs[key] : void 0;
2168
+ if (!spec) {
2169
+ d.error("style/unknown", seeing(`"${key}" isn't a style ${a(definition.type).replace(/^A/, "a")} takes${styleHint(definition, specs, key)}`, definition.type), `${path}.${key}`, id);
2170
+ continue;
2171
+ }
2172
+ if (!rootTakes(spec.roots, root)) {
2173
+ d.error("style/not-supported", `${a(definition.type)}'s "${key}" is for ${rootList(spec.roots)}, not ${a(root.type).replace(/^A/, "a")}`, `${path}.${key}`, id);
2174
+ continue;
2175
+ }
2176
+ if (!allowed(spec, key)) {
2177
+ d.error(`${what}/not-allowed`, `"${key}" can't be set ${forLabel ?? "here"}`, `${path}.${key}`, id);
2178
+ continue;
2179
+ }
2180
+ const parsed = spec.kind.parse(value);
2181
+ if (parsed.ok) out[key] = parsed.value;
2182
+ else d.error(`${what}/invalid`, `${key}: ${parsed.message}`, `${path}.${key}`, id);
2183
+ }
2184
+ return Object.keys(out).length ? out : void 0;
2185
+ }
2186
+ function readHidden(input, path, id, context) {
2187
+ const { d } = context;
2188
+ if (!context.root?.responsive) {
2189
+ d.error("hidden/not-supported", "Hiding on phones or desktops is for an Email or a Page", path, id);
2190
+ return void 0;
2191
+ }
2192
+ if (!isObject2(input) || Object.entries(input).some(([key, value]) => key !== "mobile" && key !== "desktop" || value !== void 0 && typeof value !== "boolean")) {
2193
+ d.error("hidden/invalid", "hidden is { mobile?: boolean, desktop?: boolean }", path, id);
2194
+ return void 0;
2195
+ }
2196
+ const out = {};
2197
+ if (input.mobile === true) out.mobile = true;
2198
+ if (input.desktop === true) out.desktop = true;
2199
+ if (!shownAnywhere({ hidden: out })) d.warning("hidden/everywhere", "Hidden on phones and on desktops, it never shows", path, id);
2200
+ return Object.keys(out).length ? out : void 0;
2201
+ }
2202
+ function readChildren(input, definition, path, id, context) {
2203
+ const { d } = context;
2204
+ if (input === void 0) return [];
2205
+ if (!Array.isArray(input)) {
2206
+ d.error("children/invalid", "children is an array of components and text", path, id);
2207
+ return [];
2208
+ }
2209
+ const out = [];
2210
+ input.forEach((child, index) => {
2211
+ const at = `${path}[${index}]`;
2212
+ if (typeof child === "string") {
2213
+ if (!child) return;
2214
+ if (definition.children.text) {
2215
+ const last = out[out.length - 1];
2216
+ if (typeof last === "string") out[out.length - 1] = last + child;
2217
+ else out.push(child);
2218
+ } else if (child.trim()) {
2219
+ d.error("children/text-not-allowed", `Text can't go straight in a ${definition.type}: put it in a Text or Heading`, at, id);
2220
+ }
2221
+ return;
2222
+ }
2223
+ const node = readNode(child, at, definition, context);
2224
+ if (!node) return;
2225
+ if (definition.type === "Document" && (node.type === "PageHeader" || node.type === "PageFooter") && out.some((other) => typeof other !== "string" && other.type === node.type)) {
2226
+ d.error("children/one-only", `A Document has one ${node.type}, drawn on every page: this is a second (put what both hold in one)`, at, node.id);
2227
+ }
2228
+ out.push(node);
2229
+ });
2230
+ return out;
2231
+ }
2232
+ function describeComponent(type, options) {
2233
+ const registry = registryOf(options);
2234
+ const definition = registry.get(type);
2235
+ if (!definition) return void 0;
2236
+ const described = describe(definition, registry, reachable(registry));
2237
+ return options?.root ? forRoot(described, options.root, registry) : described;
2238
+ }
2239
+ function describeComponents(options) {
2240
+ const registry = registryOf(options);
2241
+ const reach = reachable(registry);
2242
+ const all = registry.all().map((definition) => describe(definition, registry, reach));
2243
+ const root = options?.root;
2244
+ if (!root) return all;
2245
+ return all.flatMap((description) => forRoot(description, root, registry) ?? []);
2246
+ }
2247
+ function describeConditions(options) {
2248
+ const registry = registryOf(options);
2249
+ return CONDITIONS.map((condition) => ({
2250
+ name: condition.name,
2251
+ on: condition.on,
2252
+ roots: rootDefinitions(registry).filter((root) => !condition.roots || condition.roots(root)).map((root) => root.type)
2253
+ }));
2254
+ }
2255
+ function forRoot(description, root, registry) {
2256
+ if (!description.roots.includes(root)) return void 0;
2257
+ const responsive = !!registry.get(root)?.responsive;
2258
+ return {
2259
+ ...description,
2260
+ roots: [root],
2261
+ properties: description.properties.filter((property) => property.roots.includes(root)).map((property) => ({ ...property, roots: [root], mobile: property.mobile && responsive }))
2262
+ };
2263
+ }
2264
+ function rootDefinitions(registry) {
2265
+ return ROOT_NAMES.flatMap((name) => registry.get(name) ?? []);
2266
+ }
2267
+ function holds(parent, child) {
2268
+ return !!parent.children.types?.includes(child.type) || child.categories.some((category) => parent.children.categories?.includes(category));
2269
+ }
2270
+ function reachable(registry) {
2271
+ const all = registry.all();
2272
+ const out = /* @__PURE__ */ new Map();
2273
+ for (const root of rootDefinitions(registry)) {
2274
+ const seen = /* @__PURE__ */ new Set([root.type]);
2275
+ const queue = [root];
2276
+ while (queue.length) {
2277
+ const parent = queue.shift();
2278
+ for (const child of all) {
2279
+ if (seen.has(child.type) || child.categories.includes("root") || !holds(parent, child)) continue;
2280
+ seen.add(child.type);
2281
+ queue.push(child);
2282
+ }
2283
+ }
2284
+ out.set(root.type, seen);
2285
+ }
2286
+ return out;
2287
+ }
2288
+ function componentRoots(definition, options, reach = reachable(registryOf(options))) {
2289
+ const registry = registryOf(options);
2290
+ return rootDefinitions(registry).filter((root) => reach.get(root.type)?.has(definition.type)).filter((root) => Object.keys(definition.targets).some((target) => target in root.targets)).filter((root) => !definition.roots || definition.roots.includes(root.type)).map((root) => root.type);
2291
+ }
2292
+ function describe(definition, registry, reach) {
2293
+ const roots = componentRoots(definition, registry, reach);
2294
+ const within2 = (declared) => declared ? roots.filter((root) => declared.includes(root)) : roots;
2295
+ const properties = [];
2296
+ for (const [key, spec] of Object.entries(definition.props)) {
2297
+ properties.push({
2298
+ path: `props.${key}`,
2299
+ label: spec.label,
2300
+ ...spec.description ? { description: spec.description } : {},
2301
+ kind: spec.kind.name,
2302
+ accepts: spec.kind.accepts,
2303
+ editor: spec.kind.editor,
2304
+ editable: spec.editable !== false,
2305
+ required: !!spec.required,
2306
+ ...spec.default !== void 0 ? { default: spec.default } : {},
2307
+ inherited: false,
2308
+ mobile: false,
2309
+ roots: within2(spec.roots),
2310
+ schema: spec.kind.jsonSchema()
2311
+ });
2312
+ }
2313
+ const defaults = definition.defaultStyle ?? {};
2314
+ const hover = new Set(definition.hover ?? []);
2315
+ const targets = Object.keys(definition.targets);
2316
+ const unshownOnPhones = (key) => {
2317
+ const declared = definition.conditionSupport?.mobile?.[key];
2318
+ return !!declared && targets.every((target) => [].concat(declared[target] ?? []).some((rule2) => rule2.level === "none" && !rule2.values && !rule2.when));
2319
+ };
2320
+ for (const [key, spec] of Object.entries(definition.style)) {
2321
+ const group = styleGroup(key, spec);
2322
+ const base = {
2323
+ label: spec.label,
2324
+ ...spec.description ? { description: spec.description } : {},
2325
+ kind: spec.kind.name,
2326
+ accepts: spec.kind.accepts,
2327
+ editor: spec.kind.editor,
2328
+ editable: spec.editable !== false,
2329
+ required: false,
2330
+ inherited: !!spec.inherited,
2331
+ mobile: !!spec.mobile && !unshownOnPhones(key),
2332
+ roots: within2(spec.roots),
2333
+ ...group ? { group } : {},
2334
+ schema: spec.kind.jsonSchema()
2335
+ };
2336
+ properties.push({ path: `style.${key}`, ...base, ...defaults[key] !== void 0 ? { default: defaults[key] } : {} });
2337
+ if (hover.has(key)) properties.push({ path: `hover.${key}`, ...base, label: `${spec.label} on hover`, mobile: false });
2338
+ }
2339
+ return {
2340
+ type: definition.type,
2341
+ label: definition.label,
2342
+ description: definition.description,
2343
+ categories: definition.categories,
2344
+ properties,
2345
+ children: { text: !!definition.children.text, categories: definition.children.categories ?? [], types: definition.children.types ?? [] },
2346
+ targets: Object.keys(definition.targets),
2347
+ roots,
2348
+ groups: (definition.editor?.groups ?? []).map(
2349
+ (group) => "composite" in group ? { title: group.title, paths: group.composite.covers, composite: group.composite.editor } : { title: group.title, paths: group.paths }
2350
+ )
2351
+ };
2352
+ }
2353
+ var same = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
2354
+ function reportDeclared(d, node, at, key, value, declared, target, outlook = true) {
2355
+ if (!declared) return;
2356
+ for (const rule2 of [].concat(declared)) {
2357
+ if (rule2.level === "full") continue;
2358
+ if (rule2.level === "outlook" && !outlook) continue;
2359
+ if (rule2.values && !rule2.values.some((v) => same(v, value))) continue;
2360
+ if (rule2.when && !rule2.when(value, node)) continue;
2361
+ const asked = typeof value === "string" ? `${key}: "${value}"` : key;
2362
+ const clients = target === "email" ? rule2.clients ?? (rule2.level === "outlook" ? ["outlook"] : void 0) : void 0;
2363
+ const output = clients ? "some email clients" : target === "web" ? "a page" : target;
2364
+ const [code, severity, message] = rule2.level === "none" ? [`${target}/unsupported`, "warning", `${asked} isn't possible in ${output}: ${rule2.shows}`] : rule2.level === "partial" ? [`${target}/degraded`, "info", `${asked} on this ${node.type} in ${output}: ${rule2.shows}`] : ["email/outlook-degraded", "info", `${asked} on this ${node.type} in classic Outlook: ${rule2.shows}`];
2365
+ const [said, as] = rule2.source === "assumed" ? ["info", `${message} (not checked)`] : [severity, message];
2366
+ d.add(code, said, as, at, node.id, { clients, fix: rule2.fix, see: rule2.feature && caniemail(rule2.feature) });
2367
+ }
2368
+ }
2369
+ var MAX_DEPTH = 32;
2370
+ function templatePathsOf(document, paths) {
2371
+ const places = /* @__PURE__ */ new Map();
2372
+ const visit = (node, at) => {
2373
+ places.set(at, paths.get(node) ?? at);
2374
+ node.children?.forEach((child, index) => isNode(child) && visit(child, `${at}.children[${index}]`));
2375
+ };
2376
+ visit(document.root, "root");
2377
+ return (path) => {
2378
+ for (let at = path; at; at = at.slice(0, Math.max(at.lastIndexOf("."), at.lastIndexOf("["), 0))) {
2379
+ const place = places.get(at);
2380
+ if (place !== void 0) return place + path.slice(at.length);
2381
+ }
2382
+ return path;
2383
+ };
2384
+ }
2385
+ function expandDocument(document, options, target) {
2386
+ const registry = registryOf(options);
2387
+ const d = new Diagnostics();
2388
+ const paths = /* @__PURE__ */ new WeakMap();
2389
+ const taken = /* @__PURE__ */ new Set();
2390
+ const collect = (node) => {
2391
+ if (node.id) taken.add(node.id);
2392
+ node.children?.forEach((child) => isNode(child) && collect(child));
2393
+ };
2394
+ collect(document.root);
2395
+ const rootDefinition = registry.get(document.root.type);
2396
+ const responsive = !!rootDefinition?.responsive;
2397
+ const unshown = (node, path) => {
2398
+ const declared = target && registry.get(node.type)?.conditionSupport;
2399
+ if (!declared) return;
2400
+ for (const condition of CONDITIONS) {
2401
+ const block = blockOf(node, condition);
2402
+ const byStyle = declared[condition.name];
2403
+ if (!block || !byStyle || condition.hiddenKey && node.hidden?.[condition.hiddenKey]) continue;
2404
+ for (const [key, value] of Object.entries(block)) {
2405
+ reportDeclared(d, { type: node.type, id: node.id, props: node.props ?? {} }, `${path}.${condition.name}.${key}`, key, value, byStyle[key]?.[target], target);
2406
+ }
2407
+ }
2408
+ };
2409
+ const visit = (given2, path, depth, self = path) => {
2410
+ unshown(given2, path);
2411
+ const arrange = registry.get(given2.type)?.arrange;
2412
+ if (!arrange) return walk(given2, path, self, depth);
2413
+ const origins = /* @__PURE__ */ new Map();
2414
+ given2.children?.forEach((child, index) => isNode(child) && origins.set(child, `${path}.children[${index}]`));
2415
+ return walk(arrange(given2), path, self, depth, origins);
2416
+ };
2417
+ const walk = (node, path, self, depth, origins) => {
2418
+ const definition = registry.get(node.type);
2419
+ const children = node.children?.map((child, index) => {
2420
+ if (!isNode(child)) return child;
2421
+ const at = origins ? origins.get(child) ?? self : `${path}.children[${index}]`;
2422
+ const childDefinition = registry.get(child.type);
2423
+ if (childDefinition && isCustom(childDefinition)) return expandOne(child, childDefinition, definition, at, depth) ?? null;
2424
+ if (origins && !origins.has(child)) return walk(child, self, self, depth, origins);
2425
+ return visit(child, at, depth);
2426
+ });
2427
+ const out = children?.length ? { ...node, children: children.filter((child) => child !== null) } : node;
2428
+ paths.set(out, self);
2429
+ return out;
2430
+ };
2431
+ const expandOne = (node, definition, parent, path, depth, self = path) => {
2432
+ if (depth >= MAX_DEPTH) {
2433
+ d.error("custom/depth", `${node.type} expands into custom components ${MAX_DEPTH} deep: does it contain itself?`, path, node.id);
2434
+ return void 0;
2435
+ }
2436
+ let raw2;
2437
+ const reported = [];
2438
+ try {
2439
+ const style = { ...definition.defaultStyle, ...node.style };
2440
+ raw2 = definition.expand(propsWithDefaults(definition, node.props), { register: (custom) => registry.add(custom), style, responsive, report: (diagnostic) => reported.push(diagnostic) });
2441
+ } catch (error) {
2442
+ d.error("custom/render", `${node.type} failed to render: ${error instanceof Error ? error.message : String(error)}`, path, node.id);
2443
+ return void 0;
2444
+ }
2445
+ const at = `${path}(${node.type})`;
2446
+ for (const diagnostic of reported) d.list.push({ ...diagnostic, path: diagnostic.path === "root" || diagnostic.path.startsWith("root.") ? at + diagnostic.path.slice(4) : at });
2447
+ if (Array.isArray(raw2) || typeof raw2 !== "object" || raw2 === null) {
2448
+ d.error("custom/render", `${node.type} renders one component (a Column, a Section, \u2026), not ${Array.isArray(raw2) ? "several" : JSON.stringify(raw2)}`, path, node.id);
2449
+ return void 0;
2450
+ }
2451
+ const { node: expanded, diagnostics } = checkFragment(raw2, { parent, root: rootDefinition, idPrefix: node.id ?? null, taken, registry, path: `${path}(${node.type})` });
2452
+ for (const diagnostic of diagnostics) d.list.push(diagnostic);
2453
+ if (!expanded) return void 0;
2454
+ collectIds(expanded);
2455
+ if (node.style?.flex !== void 0) expanded.style = { ...expanded.style, flex: node.style.flex };
2456
+ if (node.hidden) expanded.hidden = { ...expanded.hidden, ...node.hidden };
2457
+ if (node.id && !expanded.id && !CUSTOM_TYPE.test(node.type)) expanded.id = node.id;
2458
+ for (const condition of CONDITIONS) {
2459
+ const rendered = registry.get(expanded.type);
2460
+ for (const [key, value] of Object.entries(node[condition.name] ?? {})) {
2461
+ const spec = rendered?.style[key];
2462
+ if (rendered && spec && condition.allows(spec, key, rendered)) expanded[condition.name] = { [key]: value, ...expanded[condition.name] };
2463
+ else d.warning(`${condition.name}/not-shown`, `${node.type}'s ${key} ${condition.on} isn't shown: what it renders (a ${expanded.type}) has no ${key} ${condition.for}`, `${path}.${condition.name}.${key}`, node.id);
2464
+ }
2465
+ }
2466
+ const inner = registry.get(expanded.type);
2467
+ if (isCustom(inner)) return expandOne(expanded, inner, parent, `${path}(${node.type})`, depth + 1, self);
2468
+ return visit(expanded, `${path}(${node.type})`, depth + 1, self);
2469
+ };
2470
+ const collectIds = (node) => {
2471
+ if (node.id) taken.add(node.id);
2472
+ node.children?.forEach((child) => isNode(child) && collectIds(child));
2473
+ };
2474
+ return { document: { ...document, root: visit(document.root, "root", 0) }, diagnostics: d.list, paths };
2475
+ }
2476
+ var INITIAL_TEXT = {
2477
+ color: "#000000",
2478
+ fontFamily: "Arial, Helvetica, sans-serif",
2479
+ fontSize: 16,
2480
+ fontWeight: 400,
2481
+ fontStyle: "normal",
2482
+ lineHeight: 1.5,
2483
+ letterSpacing: 0,
2484
+ textAlign: "left",
2485
+ textTransform: "none"
2486
+ };
2487
+ var INHERITED = Object.keys(INITIAL_TEXT);
2488
+ function toPx(value, of) {
2489
+ const length3 = readLength(value);
2490
+ if (!length3) return null;
2491
+ switch (length3.unit) {
2492
+ case "px":
2493
+ return length3.value;
2494
+ case "%":
2495
+ return of === null ? null : of * length3.value / 100;
2496
+ case "pt":
2497
+ return length3.value * 96 / 72;
2498
+ case "mm":
2499
+ return length3.value * 96 / 25.4;
2500
+ case "in":
2501
+ return length3.value * 96;
2502
+ }
2503
+ }
2504
+ function lineHeightPx(lineHeight3, fontSize) {
2505
+ if (lineHeight3 === "normal") return Math.round(1.2 * fontSize * 100) / 100;
2506
+ return typeof lineHeight3 === "number" ? Math.round(lineHeight3 * fontSize * 100) / 100 : Number.parseFloat(lineHeight3);
2507
+ }
2508
+ function horizontalFrame(style) {
2509
+ const padding = style.padding;
2510
+ const border3 = style.border;
2511
+ return (padding ? padding.left + padding.right : 0) + (border3?.left?.width ?? 0) + (border3?.right?.width ?? 0);
2512
+ }
2513
+ function resolveExpanded(document, options, paths) {
2514
+ const registry = registryOf(options);
2515
+ const d = new Diagnostics();
2516
+ const definition = registry.get(document.root.type);
2517
+ const context = { d, registry, responsive: !!definition.responsive, root: document.root.type };
2518
+ const root = resolveNode(document.root, "root", { ...INITIAL_TEXT, lineHeightPx: 24 }, null, context);
2519
+ const width = root.type === "Document" ? pageBox(root.props).contentWidth : toPx(root.props.width, null);
2520
+ root.width = width;
2521
+ root.contentWidth = width === null ? null : width - horizontalFrame(root.style);
2522
+ root.children = resolveChildren(document.root, "root", root, root.contentWidth, "stack", context);
2523
+ const templatePath = paths ? templatePathsOf(document, paths) : (path) => path;
2524
+ return { root, diagnostics: d.list.map((diagnostic) => ({ ...diagnostic, path: templatePath(diagnostic.path) })), templatePath };
2525
+ }
2526
+ function styleOf(node, definition, props, context) {
2527
+ const style = { ...definition.defaultStyle ?? {} };
2528
+ const by = definition.defaultStyleBy;
2529
+ if (by) Object.assign(style, by.values[String(props[by.prop])] ?? {});
2530
+ return { ...style, ...node.style };
2531
+ }
2532
+ function textOf(parent, style) {
2533
+ const text2 = { ...parent };
2534
+ for (const key of INHERITED) if (style[key] !== void 0) text2[key] = style[key];
2535
+ const out = text2;
2536
+ out.lineHeightPx = lineHeightPx(out.lineHeight, out.fontSize);
2537
+ return out;
2538
+ }
2539
+ function directed(text2, dir, ownAlign) {
2540
+ if (ownAlign !== void 0 || dir !== "rtl" && dir !== "ltr") return text2;
2541
+ return { ...text2, textAlign: dir === "rtl" ? "right" : "left" };
2542
+ }
2543
+ function resolveNode(node, path, parentText, available, context) {
2544
+ const definition = context.registry.get(node.type);
2545
+ const props = propsWithDefaults(definition, node.props);
2546
+ const style = styleOf(node, definition, props);
2547
+ return {
2548
+ ...node.id ? { id: node.id } : {},
2549
+ path,
2550
+ type: node.type,
2551
+ definition,
2552
+ props,
2553
+ style,
2554
+ ...node.hover ? { hover: node.hover } : {},
2555
+ ...node.mobile ? { mobile: node.mobile } : {},
2556
+ ...node.hidden ? { hidden: node.hidden } : {},
2557
+ text: directed(textOf(parentText, style), props.dir, node.style?.textAlign),
2558
+ available,
2559
+ width: null,
2560
+ contentWidth: null,
2561
+ children: []
2562
+ };
2563
+ }
2564
+ function clamp(width, style, of) {
2565
+ const min = toPx(style.minWidth, of);
2566
+ const max = toPx(style.maxWidth, of);
2567
+ if (max !== null) width = Math.min(width, max);
2568
+ if (min !== null) width = Math.max(width, min);
2569
+ return width;
2570
+ }
2571
+ function intrinsicWidth(resolved, available) {
2572
+ if (resolved.type === "Svg" && typeof resolved.props.width === "number") return resolved.props.width + horizontalFrame(resolved.style);
2573
+ if (resolved.type !== "Image" || resolved.props.src === void 0) return null;
2574
+ const own = assetParts(resolved.props.src).width ?? null;
2575
+ if (own === null) return null;
2576
+ return clamp(available === null ? own : Math.min(own, available), resolved.style, available);
2577
+ }
2578
+ function stackedWidth(resolved, available) {
2579
+ const set = toPx(resolved.style.width, available);
2580
+ if (set !== null) return clamp(set, resolved.style, available);
2581
+ if (resolved.definition.sizing === "stretch") return available === null ? null : clamp(available, resolved.style, available);
2582
+ if (resolved.definition.sizing === "shrink") return intrinsicWidth(resolved, available);
2583
+ return null;
2584
+ }
2585
+ function setWidth(resolved, width) {
2586
+ resolved.width = width === null ? null : Math.round(width);
2587
+ resolved.contentWidth = resolved.width === null ? null : Math.max(0, resolved.width - horizontalFrame(resolved.style));
2588
+ }
2589
+ function resolveChildren(node, path, parent, available, layout, context) {
2590
+ const out = [];
2591
+ const nodes = [];
2592
+ (node.children ?? []).forEach((child, index) => {
2593
+ if (!isNode(child)) {
2594
+ out.push(child);
2595
+ return;
2596
+ }
2597
+ const at = `${path}.children[${index}]`;
2598
+ const inText = layout === "text" && !!context.registry.get(child.type)?.categories.includes("inline");
2599
+ const resolved = resolveNode(child, at, parent.text, inText ? null : available, context);
2600
+ if (resolved.style.flex !== void 0 && layout !== "row") {
2601
+ context.d.warning("style/ignored", `flex only applies to a child of a Row; this ${child.type} is in a ${parent.type}`, `${at}.style.flex`, child.id);
2602
+ }
2603
+ nodes.push({ source: child, resolved, at });
2604
+ out.push(resolved);
2605
+ });
2606
+ if (layout === "row") layOutRow(parent, nodes, available, path, context);
2607
+ else {
2608
+ for (const { resolved, at, source } of nodes) {
2609
+ if (resolved.available === null && layout === "text") continue;
2610
+ const across = parent.type === "Layer" ? parent.style.justifyItems : parent.style.alignItems;
2611
+ const stretched = layout === "stack" && across === "stretch" && resolved.definition.sizing === "shrink" && resolved.style.width === void 0 && resolved.type !== "Svg";
2612
+ const width = stretched && resolved.available !== null ? clamp(resolved.available, resolved.style, resolved.available) : stackedWidth(resolved, resolved.available);
2613
+ if (width !== null && resolved.available !== null && width > resolved.available + 0.5) {
2614
+ context.d.warning("layout/overflow", `This ${source.type} is ${Math.round(width)}px wide, wider than the ${Math.round(resolved.available)}px it's in`, at, source.id);
2615
+ }
2616
+ setWidth(resolved, width);
2617
+ }
2618
+ }
2619
+ for (const { source, resolved, at } of nodes) {
2620
+ if (source.type === "Table") {
2621
+ resolved.children = resolveTable(source, at, resolved, context);
2622
+ continue;
2623
+ }
2624
+ const own = source.type === "Row" ? "row" : resolved.definition.children.text ? "text" : "stack";
2625
+ let inner = resolved.contentWidth ?? resolved.available ?? (layout === "text" ? available : null);
2626
+ if (source.type === "List" && inner !== null) inner = Math.max(0, inner - (toPx(resolved.style.indent, null) ?? 0));
2627
+ resolved.children = resolveChildren(source, at, resolved, inner, own, context);
2628
+ if (resolved.width === null && source.type === "Link" && layout !== "text") {
2629
+ const image = resolved.children.find((c) => typeof c !== "string" && c.type === "Image");
2630
+ if (image && image.width !== null) setWidth(resolved, image.width + horizontalFrame(resolved.style));
2631
+ }
2632
+ }
2633
+ return out;
2634
+ }
2635
+ function layOutRow(row, nodes, available, path, context) {
2636
+ if (available === null) return;
2637
+ const desktop = nodes.filter(({ resolved }) => !resolved.hidden?.desktop);
2638
+ for (const { resolved } of nodes) if (resolved.hidden?.desktop) setWidth(resolved, available);
2639
+ const gap = toPx(row.style.gap, null) ?? 0;
2640
+ const items2 = desktop.map(({ resolved }) => flexItem(resolved, available));
2641
+ const wraps = row.style.flexWrap === "wrap" && (context.root === "Page" || context.root === "Email" && row.props.stackOnMobile === false);
2642
+ for (const line2 of wraps ? wrappedLines(items2, gap, available) : [items2.map((_, index) => index)]) {
2643
+ const room = available - gap * Math.max(0, line2.length - 1);
2644
+ const ownItems = line2.map((index) => items2[index]);
2645
+ const widths = roundTogether(flexWidths(ownItems, room));
2646
+ line2.forEach((index, at) => setWidth(desktop[index].resolved, items2[index].unknown ? null : widths[at]));
2647
+ const asked = ownItems.reduce((sum, item) => sum + item.base, 0);
2648
+ const used = widths.reduce((sum, width) => sum + width, 0);
2649
+ if (used > room + 0.5) {
2650
+ context.d.warning("layout/overflow", `The Row's children need ${Math.round(used - room)}px more than the ${Math.round(available)}px it has, at their minimum widths`, path, row.id);
2651
+ } else if (asked > room + 0.5 && ownItems.some((item) => item.shrink > 0 && item.inner > 0)) {
2652
+ context.d.warning("layout/shrunk", `The Row's children's widths add up to ${Math.round(asked)}px, more than the ${Math.round(room)}px it has: they're narrowed to fit, as a browser does`, path, row.id);
2653
+ }
2654
+ }
2655
+ }
2656
+ function wrappedLines(items2, gap, available) {
2657
+ const lines = [];
2658
+ let used = 0;
2659
+ items2.forEach((item, index) => {
2660
+ const size2 = Math.max(item.min, Math.min(item.max, item.base));
2661
+ const line2 = lines[lines.length - 1];
2662
+ if (line2 && used + gap + size2 <= available + 0.5) {
2663
+ line2.push(index);
2664
+ used += gap + size2;
2665
+ } else {
2666
+ lines.push([index]);
2667
+ used = size2;
2668
+ }
2669
+ });
2670
+ return lines;
2671
+ }
2672
+ function flexItem(resolved, available) {
2673
+ const { style } = resolved;
2674
+ const frame2 = horizontalFrame(style);
2675
+ const width = toPx(style.width, available);
2676
+ const own = style.flex === "none" || style.flex === 0 || resolved.type === "Svg";
2677
+ const intrinsic = own && width === null ? intrinsicWidth(resolved, available) : null;
2678
+ const base = width !== null ? width : own ? intrinsic ?? 0 : frame2;
2679
+ const min = Math.max(frame2, toPx(style.minWidth, available) ?? 0);
2680
+ const max = Math.max(min, toPx(style.maxWidth, available) ?? Number.POSITIVE_INFINITY);
2681
+ return {
2682
+ base,
2683
+ inner: Math.max(0, base - frame2),
2684
+ min,
2685
+ max,
2686
+ grow: width !== null || own ? 0 : typeof style.flex === "number" ? style.flex : 1,
2687
+ shrink: own ? 0 : 1,
2688
+ unknown: own && width === null && intrinsic === null
2689
+ };
2690
+ }
2691
+ function flexWidths(items2, room) {
2692
+ const clamp2 = (item, width) => Math.max(item.min, Math.min(item.max, width));
2693
+ const sum = (values) => values.reduce((total, value) => total + value, 0);
2694
+ const hypothetical = items2.map((item) => clamp2(item, item.base));
2695
+ const growing = sum(hypothetical) <= room;
2696
+ const factor = (item) => growing ? item.grow : item.shrink;
2697
+ const target = items2.map((item) => item.base);
2698
+ const frozen = items2.map((item, index) => {
2699
+ const inflexible = factor(item) === 0 || (growing ? item.base > hypothetical[index] : item.base < hypothetical[index]);
2700
+ if (inflexible) target[index] = hypothetical[index];
2701
+ return inflexible;
2702
+ });
2703
+ const initialFree = room - sum(items2.map((item, index) => frozen[index] ? target[index] : item.base));
2704
+ for (let round = 0; round <= items2.length && frozen.includes(false); round++) {
2705
+ const open2 = items2.map((_, index) => index).filter((index) => !frozen[index]);
2706
+ let free = room - sum(items2.map((item, index) => frozen[index] ? target[index] : item.base));
2707
+ const factors = sum(open2.map((index) => factor(items2[index])));
2708
+ if (factors < 1 && Math.abs(initialFree * factors) < Math.abs(free)) free = initialFree * factors;
2709
+ const weights = open2.map((index) => growing ? items2[index].grow : items2[index].shrink * items2[index].inner);
2710
+ const total = sum(weights);
2711
+ open2.forEach((index, k) => {
2712
+ target[index] = items2[index].base + (total > 0 ? free * weights[k] / total : 0);
2713
+ });
2714
+ const violation = open2.map((index) => clamp2(items2[index], target[index]) - target[index]);
2715
+ const net = sum(violation);
2716
+ open2.forEach((index, k) => {
2717
+ const past = Math.abs(net) < 1e-9 || (net > 0 ? violation[k] > 0 : violation[k] < 0);
2718
+ target[index] = clamp2(items2[index], target[index]);
2719
+ if (past) frozen[index] = true;
2720
+ });
2721
+ }
2722
+ return target;
2723
+ }
2724
+ function roundTogether(widths) {
2725
+ const floors = widths.map(Math.floor);
2726
+ let left = Math.round(widths.reduce((sum, width) => sum + width, 0)) - floors.reduce((sum, width) => sum + width, 0);
2727
+ const order = widths.map((width, index) => ({ index, rest: width - Math.floor(width) })).sort((a2, b) => b.rest - a2.rest || a2.index - b.index);
2728
+ for (const { index } of order) {
2729
+ if (left <= 0) break;
2730
+ floors[index] += 1;
2731
+ left -= 1;
2732
+ }
2733
+ return floors;
2734
+ }
2735
+ function share(free, weights) {
2736
+ const total = weights.reduce((sum, w) => sum + w, 0);
2737
+ const room = Math.floor(free);
2738
+ const exact = weights.map((w) => total ? room * w / total : 0);
2739
+ const widths = exact.map(Math.floor);
2740
+ let left = room - widths.reduce((a2, b) => a2 + b, 0);
2741
+ const order = exact.map((value, index) => ({ index, rest: value - Math.floor(value) })).sort((a2, b) => b.rest - a2.rest || a2.index - b.index);
2742
+ for (const { index } of order) {
2743
+ if (left <= 0) break;
2744
+ widths[index] += 1;
2745
+ left -= 1;
2746
+ }
2747
+ return widths;
2748
+ }
2749
+ function resolveTable(source, path, table, context) {
2750
+ const available = table.contentWidth;
2751
+ const rows = (source.children ?? []).flatMap((child, index) => {
2752
+ if (!isNode(child)) return [];
2753
+ const at = `${path}.children[${index}]`;
2754
+ const row = resolveNode(child, at, table.text, available, context);
2755
+ setWidth(row, available);
2756
+ const cells = (child.children ?? []).flatMap((cell, cellIndex) => {
2757
+ if (!isNode(cell)) return [];
2758
+ const resolved = resolveNode(cell, `${at}.children[${cellIndex}]`, row.text, available, context);
2759
+ if (resolved.style.padding === void 0 && table.style.cellPadding !== void 0) resolved.style.padding = table.style.cellPadding;
2760
+ return [{ source: cell, resolved, at: `${at}.children[${cellIndex}]` }];
2761
+ });
2762
+ return [{ row, cells }];
2763
+ });
2764
+ const span = (cell) => Math.max(1, Number(cell.props.colSpan) || 1);
2765
+ const count = Math.max(0, ...rows.map(({ cells }) => cells.reduce((sum, { resolved }) => sum + span(resolved), 0)));
2766
+ let columns = null;
2767
+ if (available !== null && count > 0) {
2768
+ const set = new Array(count).fill(null);
2769
+ const own = new Array(count).fill(false);
2770
+ for (const { cells } of rows) {
2771
+ let column = 0;
2772
+ for (const { resolved } of cells) {
2773
+ if (span(resolved) === 1 && set[column] === null) {
2774
+ const width = toPx(resolved.style.width, available);
2775
+ set[column] = width === null ? null : Math.round(width);
2776
+ own[column] = typeof resolved.style.width === "number";
2777
+ }
2778
+ column += span(resolved);
2779
+ }
2780
+ }
2781
+ const fixed = set.reduce((sum, width) => sum + (width ?? 0), 0);
2782
+ if (fixed > available + count / 2) {
2783
+ context.d.warning("layout/overflow", `The Table's columns are ${Math.round(fixed - available)}px wider than the ${Math.round(available)}px it has`, path, table.id);
2784
+ }
2785
+ const free = set.map((width, index) => width === null ? index : -1).filter((index) => index >= 0);
2786
+ const shared = share(Math.max(0, available - fixed), free.map(() => 1));
2787
+ columns = set.map((width, index) => width === null ? shared[free.indexOf(index)] : width);
2788
+ if (free.length === 0 && fixed > 0 && Math.abs(fixed - available) > 0.5) columns = share(available, columns);
2789
+ if (free.length > 0 && own.some(Boolean)) {
2790
+ const sizes = set.map((width, index) => own[index] ? "px" : width === null ? "free" : "share");
2791
+ table.columnSizes = sizes;
2792
+ for (const { row } of rows) row.columnSizes = sizes;
2793
+ }
2794
+ }
2795
+ table.columns = columns;
2796
+ const rowBorder = table.style.rowBorder;
2797
+ rows.forEach(({ cells }, index) => {
2798
+ if (!rowBorder || index === rows.length - 1) return;
2799
+ for (const { resolved } of cells) {
2800
+ const own = resolved.style.border;
2801
+ if (!own?.bottom) resolved.style.border = { ...own, bottom: rowBorder };
2802
+ }
2803
+ });
2804
+ for (const { row, cells } of rows) {
2805
+ let column = 0;
2806
+ for (const { source: cell, resolved, at } of cells) {
2807
+ const width = columns ? columns.slice(column, column + span(resolved)).reduce((sum, w) => sum + w, 0) : null;
2808
+ column += span(resolved);
2809
+ resolved.available = width;
2810
+ setWidth(resolved, width);
2811
+ resolved.children = resolveChildren(cell, at, resolved, resolved.contentWidth, "text", context);
2812
+ }
2813
+ row.children = cells.map(({ resolved }) => resolved);
2814
+ }
2815
+ return rows.map(({ row }) => row);
2816
+ }
2817
+ function checkTargetExpanded(document, target, options, paths) {
2818
+ const registry = registryOf(options);
2819
+ const d = new Diagnostics();
2820
+ const visit = (node, path) => {
2821
+ const definition = registry.get(node.type);
2822
+ if (definition && !definition.targets[target]) {
2823
+ const why = definition.unsupported?.[target];
2824
+ d.error("target/unsupported", `A ${node.type} can't be rendered to ${target}${why ? `: ${why}` : ""}`, path, node.id);
2825
+ return;
2826
+ }
2827
+ node.children?.forEach((child, index) => isNode(child) && visit(child, `${path}.children[${index}]`));
2828
+ };
2829
+ visit(document.root, "root");
2830
+ const templatePath = paths ? templatePathsOf(document, paths) : (path) => path;
2831
+ return d.list.map((diagnostic) => ({ ...diagnostic, path: templatePath(diagnostic.path) }));
2832
+ }
2833
+ var isNode2 = (child) => typeof child !== "string";
2834
+ var shown = (node) => shownAnywhere(node);
2835
+ function layerShape(node) {
2836
+ const children = node.children.filter(isNode2).filter(shown);
2837
+ const [base, over] = children;
2838
+ if (!base || base.type !== "Image" || children.length > 2) {
2839
+ return { problem: `In an email, a Layer is an Image with one child over it (drawn as the image's background); this one has ${children.length > 2 ? `${children.length} children` : base ? `a ${base.type} under` : "no children"}`, path: node.path };
2840
+ }
2841
+ if (base.width === null || node.contentWidth === null || Math.abs(base.width - node.contentWidth) > 1) {
2842
+ return { problem: "In an email, a Layer's Image is as wide as the Layer (it's drawn as its background): give it width 100%", path: `${base.path}.style.width` };
2843
+ }
2844
+ if (typeof node.style.height === "number") return { base, over, height: node.style.height };
2845
+ const { width, height } = assetParts(base.props.src);
2846
+ if (!width || !height) {
2847
+ return { problem: "In an email, a Layer is as tall as its Image, which needs its file's width and height (src={{ src, width, height }}); or give the Layer a height", path: `${base.path}.props.src` };
2848
+ }
2849
+ return { base, over, height: Math.round(base.width * height / width) };
2850
+ }
2851
+ var CHARACTER = 0.45;
2852
+ var characters = (node) => node.children.reduce((sum, child) => sum + (typeof child === "string" ? child.length : child.type === "MergeTag" ? String(child.props.name ?? "").length + 4 : characters(child)), 0);
2853
+ function widthOf(node, phone) {
2854
+ const own = phone === void 0 ? void 0 : node.mobile?.width;
2855
+ if (typeof own === "number") return own;
2856
+ if (typeof own === "string") return Number.parseFloat(own) / 100 * phone;
2857
+ if (node.width !== null) return node.width;
2858
+ const images = node.children.reduce((sum, child) => sum + (typeof child !== "string" && child.type === "Image" && child.width !== null ? child.width : 0), 0);
2859
+ return characters(node) * node.text.fontSize * CHARACTER + images + horizontalFrame(node.style);
2860
+ }
2861
+ function oneLineWidth(row, phone) {
2862
+ const children = row.children.filter((child) => typeof child !== "string" && !(phone === void 0 ? child.hidden?.desktop : child.hidden?.mobile));
2863
+ const gap = gapOf2(row, phone !== void 0);
2864
+ return children.reduce((sum, child) => sum + widthOf(child, phone), 0) + gap * Math.max(0, children.length - 1);
2865
+ }
2866
+ var gapOf2 = (row, phone) => phone && typeof row.mobile?.gap === "number" ? row.mobile.gap : typeof row.style.gap === "number" ? row.style.gap : 0;
2867
+ function wrapsPastIts(row) {
2868
+ if (row.type !== "Row" || row.style.flexWrap !== "wrap" || row.contentWidth === null) return null;
2869
+ const width = oneLineWidth(row);
2870
+ return width > row.contentWidth + 0.5 ? Math.round(width) : null;
2871
+ }
2872
+ function wrapsWithGap(row) {
2873
+ if (row.props.stackOnMobile !== false || row.style.flexWrap !== "wrap" && row.style.flexWrap !== "words" || row.contentWidth === null) return null;
2874
+ const room = Math.min(row.contentWidth, 320);
2875
+ const desktop = !row.hidden?.desktop && gapOf2(row, false) > 0 && oneLineWidth(row) > row.contentWidth + 0.5;
2876
+ const phones = !row.hidden?.mobile && gapOf2(row, true) > 0 && oneLineWidth(row, room) > room + 0.5;
2877
+ return desktop && phones ? "desktop and phones" : desktop ? "desktop" : phones ? "phones" : null;
2878
+ }
2879
+ var isNode3 = (child) => typeof child !== "string";
2880
+ var same3 = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
2881
+ function support(root, target) {
2882
+ const d = new Diagnostics();
2883
+ const visit = (node) => {
2884
+ const { definition } = node;
2885
+ const byProp = definition.defaultStyleBy ? definition.defaultStyleBy.values[String(node.props[definition.defaultStyleBy.prop])] : void 0;
2886
+ for (const [key, value] of Object.entries(node.style)) {
2887
+ const fallback2 = byProp?.[key] ?? definition.defaultStyle?.[key];
2888
+ if (same3(value, fallback2)) continue;
2889
+ reportDeclared(d, node, `${node.path}.style.${key}`, key, value, definition.style[key]?.support?.[target], target);
2890
+ }
2891
+ for (const [key, value] of Object.entries(node.props)) {
2892
+ if (same3(value, definition.props[key]?.default)) continue;
2893
+ reportDeclared(d, node, `${node.path}.props.${key}`, key, value, definition.props[key]?.support?.[target], target);
2894
+ }
2895
+ for (const condition of CONDITIONS) {
2896
+ const block = blockOf(node, condition);
2897
+ if (!block || condition.hiddenKey && node.hidden?.[condition.hiddenKey]) continue;
2898
+ const own = condition.support[target];
2899
+ reportDeclared(d, node, `${node.path}.${condition.name}`, condition.name, block, own, target);
2900
+ if (!own || own.level === "none") continue;
2901
+ for (const [key, value] of Object.entries(block)) {
2902
+ const rules = condition.styles?.[key]?.[target] ?? definition.style[key]?.support?.[target];
2903
+ reportDeclared(d, node, `${node.path}.${condition.name}.${key}`, key, value, rules, target, condition.outlookApplies);
2904
+ }
2905
+ }
2906
+ for (const child of node.children) if (isNode3(child) && shownAnywhere(child)) visit(child);
2907
+ };
2908
+ visit(root);
2909
+ return d.list;
2910
+ }
2911
+ function emailSupport(root, templatePath = (path) => path) {
2912
+ const d = new Diagnostics();
2913
+ d.list.push(...support(root, "email"));
2914
+ const webFonts = new Set((root.props.fonts ?? []).map((face) => face.family.toLowerCase()));
2915
+ const phone = { styled: false, phoneHidden: false, desktopHidden: false };
2916
+ const visit = (node) => {
2917
+ structure(node, d);
2918
+ fallback(node, webFonts, d);
2919
+ if (node.type === "Html") htmlBlock(node, d);
2920
+ if (node.type === "Image") imageFormat(node, "src", d);
2921
+ if (node.type === "Video" && node.props.thumbnail !== void 0) imageFormat(node, "thumbnail", d);
2922
+ if (node.mobile && Object.keys(node.mobile).length) phone.styled = true;
2923
+ if (node.hidden?.mobile && !builtIn(templatePath(node.path))) phone.phoneHidden = true;
2924
+ if (node.hidden?.desktop && !builtIn(templatePath(node.path))) phone.desktopHidden = true;
2925
+ for (const child of node.children) if (isNode2(child) && shown(child)) visit(child);
2926
+ };
2927
+ visit(root);
2928
+ phoneStyles(root, phone, d);
2929
+ return d.list;
2930
+ }
2931
+ function builtIn(place) {
2932
+ const made = [...place.matchAll(/\((\w+)\)/g)].pop()?.[1];
2933
+ return !!made && Object.prototype.hasOwnProperty.call(COMPONENTS, made);
2934
+ }
2935
+ var NO_PHONE_STYLES = ["gmail-app-other", "sfr", "laposte", "gmx", "web-de", "rainloop"];
2936
+ function phoneStyles(root, phone, d) {
2937
+ const parts = [phone.desktopHidden && "what's hidden on desktop shows nowhere", phone.phoneHidden && "what's hidden on phones shows", phone.styled && "it keeps its desktop styles"].filter(Boolean);
2938
+ if (!parts.length) return;
2939
+ d.add("email/phone-styles", phone.desktopHidden ? "warning" : "info", `On phones, the email's phone styles don't apply: ${parts.join(", ")} (Rows still stack)`, root.path, root.id, {
2940
+ clients: NO_PHONE_STYLES,
2941
+ fix: phone.desktopHidden ? "show what a reader needs on desktop too, and hide on phones only what they can do without" : void 0,
2942
+ see: caniemail("css-at-media")
2943
+ });
2944
+ }
2945
+ var IMAGE_FORMATS = {
2946
+ webp: { clients: ["outlook"], feature: "image-webp", name: "a WebP" },
2947
+ avif: { clients: ["outlook", "fastmail", "gmx", "web-de", "orange", "sfr", "mail-ru", "ionos"], feature: "image-avif", name: "an AVIF" },
2948
+ svg: { clients: ["mail-ru"], feature: "image-svg", name: "an SVG" }
2949
+ };
2950
+ var FORMAT_IN = [/^data:image\/([a-z]+)/i, /[?&](?:fm|format|f)=(webp|avif|svg|png|jpe?g|gif)\b/i, /\/f_(webp|avif|svg|png|jpe?g|gif)[,/]/i, /\.([a-z]+)(?:[?#]|$)/i];
2951
+ function imageFormat(node, key, d) {
2952
+ const src = assetParts(node.props[key]).src;
2953
+ const format = FORMAT_IN.map((pattern) => pattern.exec(src)?.[1]).find(Boolean)?.toLowerCase();
2954
+ const known = format && Object.prototype.hasOwnProperty.call(IMAGE_FORMATS, format) ? IMAGE_FORMATS[format] : void 0;
2955
+ if (!known) return;
2956
+ const what = key === "src" ? "This Image is" : "This Video's thumbnail is";
2957
+ d.warning("email/image-format", `${what} ${known.name} file, which these email clients don't show`, `${node.path}.props.${key}`, node.id, {
2958
+ clients: known.clients,
2959
+ fix: "use a PNG, JPEG or GIF file",
2960
+ see: caniemail(known.feature)
2961
+ });
2962
+ }
2963
+ function fallback(node, webFonts, d) {
2964
+ const stack = node.style.fontFamily;
2965
+ if (typeof stack !== "string" || !webFonts.size) return;
2966
+ const families = fontStack(stack);
2967
+ const last = families[families.length - 1];
2968
+ if (!last || !webFonts.has(last.toLowerCase())) return;
2969
+ d.warning(
2970
+ "email/font-fallback",
2971
+ `This font stack ends with the web font "${last}": where it doesn't load, the email client picks its own font`,
2972
+ `${node.path}.style.fontFamily`,
2973
+ node.id,
2974
+ { clients: NO_WEB_FONTS, fix: `end it with a font the client has: "${stack}, Arial, sans-serif"`, see: caniemail("css-at-font-face") }
2975
+ );
2976
+ }
2977
+ function structure(node, d) {
2978
+ const { path, id } = node;
2979
+ if (node.type === "Row") {
2980
+ const past = node.props.stackOnMobile === false ? wrapsPastIts(node) : null;
2981
+ if (past !== null) {
2982
+ d.add(
2983
+ "email/outlook-degraded",
2984
+ "info",
2985
+ `What it holds looks about ${past}px wide on one line, wider than its ${node.contentWidth}px (an estimate: text isn't measured yet): classic Outlook can't wrap it onto another line as other clients do, and draws it wider`,
2986
+ path,
2987
+ id,
2988
+ { clients: ["outlook"], fix: "put less in it, or let it stack on phones (stackOnMobile)" }
2989
+ );
2990
+ }
2991
+ const lines = wrapsWithGap(node);
2992
+ if (lines !== null) {
2993
+ d.add(
2994
+ "email/degraded",
2995
+ "info",
2996
+ `What it holds looks too wide for one line on ${lines} (an estimate), and an email can't keep its gap between the lines it wraps onto: they touch. Give what's in it padding, or put each line in a Row of its own`,
2997
+ `${path}.style.gap`,
2998
+ id
2999
+ );
3000
+ }
3001
+ for (const child of node.children.filter(isNode2).filter(shown)) {
3002
+ if (child.width === null && child.type !== "Link" && child.type !== "Text") {
3003
+ d.warning("email/width", `This ${child.type} takes its own size in a Row, and its size isn't known: give it a width, so Outlook can lay the Row out`, `${child.path}.style.width`, child.id);
3004
+ }
3005
+ }
3006
+ }
3007
+ if (node.type === "Layer") {
3008
+ const shape2 = layerShape(node);
3009
+ if ("problem" in shape2) d.error("email/layer", shape2.problem, shape2.path, id);
3010
+ else if (node.style.height === void 0 && !shape2.base.hidden?.mobile) {
3011
+ d.add("email/layer-phone", "info", "On phones, the email keeps this Layer's height and cuts its image at the sides (a page scales it): give the Layer a height to draw both the same", `${path}.style.height`, id);
3012
+ }
3013
+ }
3014
+ if (node.type === "TableCell" && node.hidden) {
3015
+ d.warning("email/unsupported", "A TableCell can't be hidden in email (its row would lose a column): hide its TableRow", `${path}.hidden`, id);
3016
+ }
3017
+ if (node.type === "Video" && node.props.thumbnail === void 0 && !videoParts(String(node.props.src))?.thumbnail) {
3018
+ d.warning("email/video-thumbnail", "A Vimeo video's thumbnail can't be found without the network: give this Video a thumbnail, or the email shows a link with its title", `${path}.props.thumbnail`, id);
3019
+ }
3020
+ if (node.type === "Image" && node.width === null) {
3021
+ d.warning("email/image-width", "This Image has no width: Outlook shows it at the size of its file, however wide that is. Give it a width, or the file's width in src", `${path}.props.src`, id);
3022
+ }
3023
+ }
3024
+ var HTML_CSS = [
3025
+ { pattern: /!\s*(?!important\b)[iI][mM][pP][oO][rR][tT][aA][nN][tT]\b/, clients: ["gmail"], removes: "!important not written in lowercase: Gmail removes the whole style attribute, or the whole <style>, that holds it", fix: "write !important in lowercase" },
3026
+ { pattern: /margin[a-z-]*\s*:([^;"'}]*\s)?-\d/i, clients: ["gmail", "outlook-com"], removes: "a negative margin: the client removes the whole margin declaration", fix: "move it with padding on what holds it instead" },
3027
+ {
3028
+ pattern: /(^|[\s;{"'])position\s*:\s*absolute\b/i,
3029
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "hey", "sfr", "laposte", "mail-ru", "gmx", "web-de"],
3030
+ removes: "position: absolute: the client removes it",
3031
+ fix: "lay it out without position (a Layer puts content over an image)",
3032
+ feature: "css-position"
3033
+ },
3034
+ {
3035
+ // Outlook.com removes every position (captures); the others not named keep relative.
3036
+ pattern: /(^|[\s;{"'])position\s*:\s*relative\b/i,
3037
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "mail-ru", "gmx", "web-de"],
3038
+ removes: "position: relative: the client removes it, and what it moves stays where it is",
3039
+ fix: "move it with padding instead",
3040
+ feature: "css-position",
3041
+ use: "relative"
3042
+ },
3043
+ {
3044
+ pattern: /var\(\s*--|(^|[\s;{"'])--[\w-]+\s*:/i,
3045
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "orange", "mail-ru", "fastmail", "gmx", "web-de"],
3046
+ removes: "CSS variables: the client removes var() and their declarations",
3047
+ fix: "write the values themselves",
3048
+ feature: "css-variables"
3049
+ },
3050
+ {
3051
+ pattern: /gradient\(/i,
3052
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail-app", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
3053
+ removes: "a gradient: the client removes it",
3054
+ fix: "give it a solid background color too, or use an image",
3055
+ feature: "css-linear-gradient"
3056
+ },
3057
+ { pattern: /\b(oklch|oklab|lch|lab|hwb|color-mix|color)\(/i, clients: ["gmail", "outlook-com"], removes: "a newer color syntax: Outlook.com removes the declaration, and Gmail every inline style on its element", fix: "write the color as hex or rgb()" },
3058
+ { pattern: /rgba?\(\s*[\d.]+%?\s+[\d.]/i, clients: ["gmail"], removes: "rgb() with spaces between its numbers: Gmail removes the whole style attribute, or the whole <style>", fix: "separate them with commas: rgb(8, 121, 161)" },
3059
+ {
3060
+ pattern: /<style[\s>]/i,
3061
+ clients: ["gmail", "gmail-app", "gmail-app-other", "protonmail", "gmx", "web-de", "sfr", "laposte", "rainloop"],
3062
+ removes: "a <style> in the email's body: the client removes it, and what it styles shows without it",
3063
+ fix: "write the styles inline, on the elements they're for",
3064
+ feature: "html-style"
3065
+ },
3066
+ { pattern: /box-shadow\s*:/i, clients: ["outlook", "gmail", "gmail-app", "yahoo", "aol", "orange", "gmx", "web-de"], removes: "box-shadow: the client removes it", fix: "draw it with a border, or leave it out", feature: "css-box-shadow" },
3067
+ { pattern: /text-shadow\s*:/i, clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other"], removes: "text-shadow: the client removes it", fix: "leave it out, and keep the text readable without it", feature: "css-text-shadow" },
3068
+ { pattern: /display\s*:\s*flex\b/i, clients: ["outlook", "gmail-app-other", "orange", "gmx", "web-de"], removes: "display: flex: the client lays it out as blocks", fix: "lay it out with a Row", feature: "css-display-flex" },
3069
+ {
3070
+ pattern: /display\s*:\s*inline-flex\b/i,
3071
+ clients: ["outlook", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
3072
+ removes: "display: inline-flex: the client lays it out without flex",
3073
+ fix: "lay it out with a Row",
3074
+ feature: "css-display-flex",
3075
+ use: "inline"
3076
+ },
3077
+ { pattern: /display\s*:\s*(inline-)?grid\b/i, clients: ["outlook", "gmail-app-other", "orange", "gmx", "web-de"], removes: "display: grid: the client lays it out as blocks", fix: "lay it out with a Row or a Grid", feature: "css-display-grid" },
3078
+ { pattern: /(^|[\s;{"'])opacity\s*:/i, clients: ["outlook", "aol", "orange", "mail-ru", "gmx", "web-de"], removes: "opacity: the client shows it opaque", fix: "use a lighter color instead", feature: "css-opacity" },
3079
+ {
3080
+ pattern: /object-fit\s*:/i,
3081
+ clients: ["outlook", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
3082
+ removes: "object-fit: the client stretches the image to its box",
3083
+ fix: "crop the image file to the size it's shown at",
3084
+ feature: "css-object-fit"
3085
+ },
3086
+ {
3087
+ pattern: /(^|[\s;{"'])aspect-ratio\s*:/i,
3088
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "samsung-email", "thunderbird", "fastmail", "gmx", "web-de", "orange"],
3089
+ removes: "aspect-ratio: the client removes it",
3090
+ fix: "give it a width and a height",
3091
+ feature: "css-aspect-ratio"
3092
+ }
3093
+ ];
3094
+ var OUTLOOK_ONLY2 = /<!--\[if\s+(?:(?:gte?|lte?)\s+)?mso[^\]]*\]>[\s\S]*?<!\[endif\]-->/gi;
3095
+ function htmlBlock(node, d) {
3096
+ if (typeof node.props.html !== "string") return;
3097
+ const markup = node.props.html.replace(OUTLOOK_ONLY2, "");
3098
+ for (const check of HTML_CSS) {
3099
+ if (check.pattern.test(markup)) {
3100
+ d.add("email/html-css", "info", `This Html block's markup may have ${check.removes}`, `${node.path}.props.html`, node.id, { clients: check.clients, fix: check.fix, see: check.feature && caniemail(check.feature) });
3101
+ }
3102
+ }
3103
+ }
3104
+ var isNode4 = (child) => typeof child !== "string";
3105
+ var OUTPUT = { pdf: "a PDF", image: "an image" };
3106
+ function pagedSupport(root, output) {
3107
+ const d = new Diagnostics();
3108
+ const where = OUTPUT[output];
3109
+ const visit = (node) => {
3110
+ if (!shownAnywhere(node)) return;
3111
+ for (const condition of CONDITIONS) {
3112
+ if (condition.paged && blockOf(node, condition)) d.warning(`${output}/unsupported`, condition.paged(node, where), `${node.path}.${condition.name}`, node.id);
3113
+ }
3114
+ if (node.type === "Video") {
3115
+ const shows = output === "pdf" ? "its thumbnail (where its file is given) or its title, linked to it" : "its title";
3116
+ d.warning(`${output}/unsupported`, `A Video isn't possible in ${where}: it has no player. It shows ${shows}`, node.path, node.id);
3117
+ }
3118
+ if (node.type === "Html") {
3119
+ d.warning(`${output}/unsupported`, `Markup isn't possible in ${where}: it has no browser. The Html block shows its plain text`, node.path, node.id);
3120
+ }
3121
+ const past = wrapsPastIts(node);
3122
+ if (past !== null) {
3123
+ d.warning(
3124
+ `${output}/unsupported`,
3125
+ `What it holds looks about ${past}px wide on one line, wider than its ${node.contentWidth}px (an estimate: text isn't measured yet), and ${where} can't wrap it onto another line: it goes past the edge`,
3126
+ node.path,
3127
+ node.id
3128
+ );
3129
+ }
3130
+ node.children.filter(isNode4).forEach(visit);
3131
+ };
3132
+ visit(root);
3133
+ return d.list;
3134
+ }
3135
+ var CLEARLY = 1.25;
3136
+ var sides = (value) => {
3137
+ const s = value ?? {};
3138
+ return { top: s.top ?? 0, bottom: s.bottom ?? 0 };
3139
+ };
3140
+ var lineWidth = (line2) => line2 && typeof line2 === "object" && typeof line2.width === "number" ? line2.width : 0;
3141
+ var frame = (node) => {
3142
+ const padding = sides(node.style.padding);
3143
+ const border3 = node.style.border ?? {};
3144
+ return padding.top + padding.bottom + lineWidth(border3.top) + lineWidth(border3.bottom) + lineWidth(node.style.borderTop) + lineWidth(node.style.borderBottom);
3145
+ };
3146
+ var SIDE_BY_SIDE = /* @__PURE__ */ new Set(["Row", "TableRow", "Layer"]);
3147
+ function heightOf(node, available) {
3148
+ if (node.hidden?.desktop) return 0;
3149
+ const set = toPx(node.style.height, null);
3150
+ if (set !== null) return set;
3151
+ const width = node.contentWidth ?? node.width ?? available;
3152
+ if (node.children.some((child) => typeof child === "string" && child.trim())) {
3153
+ const perLine = Math.max(1, Math.floor(width / (node.text.fontSize * CHARACTER)));
3154
+ return Math.ceil(characters(node) / perLine) * node.text.lineHeightPx + frame(node);
3155
+ }
3156
+ if (node.type === "Image" && node.props.src !== void 0) {
3157
+ const { width: w, height: h2 } = assetParts(node.props.src);
3158
+ return (w && h2 ? (node.width ?? w) * h2 / w : 0) + frame(node);
3159
+ }
3160
+ const children = node.children.filter(isNode4).filter((child) => !child.hidden?.desktop);
3161
+ if (SIDE_BY_SIDE.has(node.type)) return Math.max(0, ...children.map((child) => heightOf(child, width))) + frame(node);
3162
+ const gap = toPx(node.style.gap, null) ?? 0;
3163
+ return children.reduce((sum, child) => sum + heightOf(child, width), 0) + gap * Math.max(0, children.length - 1) + frame(node);
3164
+ }
3165
+ function overflow(root) {
3166
+ const d = new Diagnostics();
3167
+ const say = (code, height, room, path, what) => {
3168
+ if (room <= 0 || height <= room * CLEARLY) return;
3169
+ d.warning(code, `${what} looks about ${Math.round(height / room * 100)}% as tall as its room (an estimate: text isn't measured yet). What doesn't fit is cut off`, path);
3170
+ };
3171
+ if (root.type === "Document") {
3172
+ const box2 = pageBox(root.props);
3173
+ const { pages, header, footer } = documentPages(root);
3174
+ const room = box2.contentHeight - (header ? heightOf(header, box2.contentWidth) : 0) - (footer ? heightOf(footer, box2.contentWidth) : 0);
3175
+ pages.forEach(({ page, children }, i) => {
3176
+ const holder = page ?? root;
3177
+ const gap = toPx(holder.style.gap, null) ?? 0;
3178
+ const height = children.reduce((sum, child) => sum + heightOf(child, box2.contentWidth), 0) + gap * Math.max(0, children.length - 1) + (page ? frame(page) : 0);
3179
+ say("document/overflow", height, room, page?.path ?? children[0]?.path ?? "root", `Page ${i + 1}'s content (split it with a PageBreak, or into pages)`);
3180
+ });
3181
+ } else if (root.type === "Canvas") {
3182
+ const room = Number(root.props.height) - frame(root);
3183
+ say("canvas/overflow", heightOf(root, Number(root.props.width)) - frame(root), room, "root", "The Canvas's content");
3184
+ }
3185
+ return d.list;
3186
+ }
3187
+ function pageBreakpoints(root) {
3188
+ const mobile = root.props.breakpoints?.mobile;
3189
+ const width = typeof root.props.width === "number" ? root.props.width : void 0;
3190
+ if (root.type !== "Page" || typeof mobile !== "number" || width === void 0 || mobile < width) return [];
3191
+ return [
3192
+ {
3193
+ code: "page/phone-width",
3194
+ severity: "warning",
3195
+ message: `Its phone styles apply to every window up to ${mobile}px, wider than its ${width}px content: on those, the content is at its full width with its phone styles. Set breakpoints.mobile below ${width}`,
3196
+ path: "root.props.breakpoints"
3197
+ }
3198
+ ];
3199
+ }
3200
+ var OUTPUTS = {
3201
+ Page: { target: "web", ready: true },
3202
+ // Print HTML: a page writer's, each page a box of the paper's size.
3203
+ Document: { target: "web", ready: true },
3204
+ // A Canvas as HTML, to look at (@hyper-elements/react/image writes the image).
3205
+ Canvas: { target: "web", ready: true },
3206
+ Email: { target: "email", ready: true }
3207
+ };
3208
+ function prepareTemplate(input, options = {}) {
3209
+ checkEmailClients(options.emailClients);
3210
+ const registry = registryOf(options);
3211
+ const checked = checkDocument(input, registry);
3212
+ const diagnostics = [...checked.diagnostics];
3213
+ const failed = () => diagnostics.some((d) => d.severity === "error");
3214
+ if (!checked.document || failed()) return { resolved: null, target: null, diagnostics };
3215
+ const root = checked.document.root.type;
3216
+ const done = (resolved2, target) => {
3217
+ if (root !== "Email" && options.emailClients) diagnostics.push({ code: "email/clients-ignored", severity: "info", message: `emailClients is for an Email; a ${root} isn't checked for email clients`, path: "root" });
3218
+ return { resolved: resolved2, target, diagnostics: root === "Email" ? forEmailClients(diagnostics, options.emailClients) : diagnostics };
3219
+ };
3220
+ const output = OUTPUTS[root];
3221
+ if (!output?.ready) {
3222
+ diagnostics.push({ code: "output/unavailable", severity: "error", message: output?.later ?? `A ${root} can't be rendered yet`, path: "root" });
3223
+ return done(null, null);
3224
+ }
3225
+ const expanded = expandDocument(checked.document, registry, output.target);
3226
+ diagnostics.push(...expanded.diagnostics);
3227
+ if (failed()) return done(null, output.target);
3228
+ diagnostics.push(...checkTargetExpanded(expanded.document, output.target, registry, expanded.paths));
3229
+ if (failed()) return done(null, output.target);
3230
+ const resolved = resolveExpanded(expanded.document, registry, expanded.paths);
3231
+ diagnostics.push(...resolved.diagnostics);
3232
+ const found = output.target === "email" ? emailSupport(resolved.root, resolved.templatePath) : support(resolved.root, output.target);
3233
+ if (root === "Document" || root === "Canvas") found.push(...overflow(resolved.root));
3234
+ found.push(...pageBreakpoints(resolved.root));
3235
+ diagnostics.push(...inTemplate(resolved, found));
3236
+ return done(failed() ? null : resolved, output.target);
3237
+ }
3238
+ function inTemplate(resolved, diagnostics) {
3239
+ const { templatePath } = resolved;
3240
+ return templatePath ? diagnostics.map((diagnostic) => ({ ...diagnostic, path: templatePath(diagnostic.path) })) : diagnostics;
3241
+ }
3242
+ function validateTemplate(input, options = {}) {
3243
+ const { diagnostics, resolved } = prepareTemplate(input, options);
3244
+ const root = resolved?.root;
3245
+ const paged = root?.type === "Document" ? pagedSupport(root, "pdf") : root?.type === "Canvas" ? pagedSupport(root, "image") : [];
3246
+ const all = [...diagnostics, ...resolved ? inTemplate(resolved, paged) : paged];
3247
+ return { valid: !all.some((d) => d.severity === "error"), diagnostics: all };
3248
+ }
3249
+
3250
+ // src/utils/collect.ts
3251
+ var SHARED = Symbol.for("@hyper-elements/react/collecting");
3252
+ var frames = globalThis[SHARED] ?? (globalThis[SHARED] = []);
3253
+ function collecting() {
3254
+ return frames[frames.length - 1] ?? void 0;
3255
+ }
3256
+ function within(frame2, run) {
3257
+ frames.push(frame2);
3258
+ try {
3259
+ return run();
3260
+ } finally {
3261
+ frames.pop();
3262
+ }
3263
+ }
3264
+ var RENDERS = Symbol.for("@hyper-elements/react/renders");
3265
+ function nonce() {
3266
+ const shared = globalThis;
3267
+ const clock = globalThis.performance;
3268
+ return [shared[RENDERS] = (shared[RENDERS] ?? 0) + 1, (clock?.timeOrigin ?? 0) * 1e3, (clock?.now() ?? 0) * 1e6].map((n) => Math.floor(n).toString(36)).join("_");
3269
+ }
3270
+ var SHARED2 = Symbol.for("@hyper-elements/react/render-builds");
3271
+ var using = globalThis[SHARED2] ?? (globalThis[SHARED2] = []);
3272
+ var same2 = (a2, b) => a2.renderToString === b.renderToString;
3273
+ function dispatcher() {
3274
+ const internals = React;
3275
+ return internals.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE?.H ?? internals.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentDispatcher?.current ?? null;
3276
+ }
3277
+ var dispatchers = /* @__PURE__ */ new Map();
3278
+ function dispatcherOf(build) {
3279
+ if (!dispatchers.has(build)) {
3280
+ let seen = null;
3281
+ try {
3282
+ class Probe extends React.Component {
3283
+ render() {
3284
+ seen = dispatcher();
3285
+ return null;
3286
+ }
3287
+ }
3288
+ build.renderToStaticMarkup(React.createElement(Probe));
3289
+ } catch {
3290
+ }
3291
+ dispatchers.set(build, seen);
3292
+ }
3293
+ return dispatchers.get(build);
3294
+ }
3295
+ var second;
3296
+ function secondBuild(outer) {
3297
+ if (second === void 0) {
3298
+ second = null;
3299
+ const node = globalThis.process;
3300
+ const requirers = [
3301
+ // (`module`'s, read so that a bundler doesn't take it for a require of a path worked out when it runs.)
3302
+ () => {
3303
+ return void 0;
3304
+ },
3305
+ // From this module's URL (as a stack names it: webpack 4 can't parse the syntax for it), the app's script, then
3306
+ // the folder Node started in; only where `react` is this React. Another React's isn't loaded (React 19's browser
3307
+ // build keeps Node running).
3308
+ ...[/file:\/\/\S+?(?=:\d+:\d+)/.exec(String(new Error().stack))?.[0], node?.argv?.[1], `${node?.cwd?.()}/`].map((from) => () => {
3309
+ const required = node?.getBuiltinModule?.("module")?.createRequire(from);
3310
+ return required?.("react")?.createElement === React.createElement ? required : void 0;
3311
+ })
3312
+ ];
3313
+ const ids = ["react-dom/server.bun", ...Number.parseInt(React.version) >= 19 ? [] : ["react-dom/server.browser"], "react-dom/server.node"];
3314
+ search: for (const id of ids) {
3315
+ for (const requirer of requirers) {
3316
+ try {
3317
+ const build = requirer()?.(id);
3318
+ if (build?.renderToStaticMarkup && !same2(build, defaultBuild) && ![null, outer].includes(dispatcherOf(build))) {
3319
+ second = build;
3320
+ break search;
3321
+ }
3322
+ } catch {
3323
+ }
3324
+ }
3325
+ }
3326
+ }
3327
+ return second ?? void 0;
3328
+ }
3329
+ var buildOf = (outer) => outer === null ? void 0 : [defaultBuild, second].find((b) => !!b && dispatcherOf(b) === outer);
3330
+ function withBuild(render) {
3331
+ const outer = dispatcher();
3332
+ let build = current();
3333
+ const theirs = using.length === 0 || collecting() !== void 0 ? buildOf(outer) : void 0;
3334
+ const busy = [];
3335
+ if (theirs) {
3336
+ const free = (b) => !same2(b, theirs) && !using.some((u) => same2(u, b));
3337
+ const other = free(defaultBuild) ? defaultBuild : secondBuild(outer);
3338
+ if (other && free(other)) {
3339
+ busy.push(theirs);
3340
+ build = other;
3341
+ } else build = theirs;
3342
+ }
3343
+ using.push(...busy, build);
3344
+ try {
3345
+ return render();
3346
+ } catch (error) {
3347
+ if (build !== theirs && !rendersHere(build)) throw Object.assign(new Error("[Unlayer] react-dom can't render with this React (React Server Components', as in Next.js's edge runtime): render emails in the Node.js runtime."), { cause: error });
3348
+ throw error;
3349
+ } finally {
3350
+ using.length -= busy.length + 1;
3351
+ }
3352
+ }
3353
+ function rendersHere(build) {
3354
+ try {
3355
+ build.renderToStaticMarkup(React.createElement(() => null));
3356
+ return true;
3357
+ } catch {
3358
+ return false;
3359
+ }
3360
+ }
3361
+ function current() {
3362
+ return using[using.length - 1] ?? defaultBuild;
3363
+ }
3364
+ var renderToString = (element, options) => current().renderToString(element, options);
3365
+
3366
+ // src/utils/render-scope.ts
3367
+ function withRenderScope(render) {
3368
+ return withBuild(() => within(null, render));
3369
+ }
3370
+
3371
+ // src/primitives/collect.tsx
3372
+ var SHARED3 = Symbol.for("@hyper-elements/react/primitives/collecting");
3373
+ var frames2 = globalThis[SHARED3] ?? (globalThis[SHARED3] = []);
3374
+ var MARK = "elements-node";
3375
+ var ENTITIES = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#x27;": "'" };
3376
+ var unescape = (text2) => text2.replace(/&amp;|&lt;|&gt;|&quot;|&#x27;/g, (entity) => ENTITIES[entity]);
3377
+ var NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "style", "hidden"]);
3378
+ function toJsonNode({ type, props }, diagnostics) {
3379
+ const node = { type, children: [] };
3380
+ const semantic = {};
3381
+ for (const [key, value] of Object.entries(props)) {
3382
+ if (value === void 0) continue;
3383
+ if (key === "compat") diagnostics.push({ code: "jsx/compat", severity: "warning", message: `A <${type}>'s compat isn't a prop of a template: it's left out`, path: "jsx" });
3384
+ else if (NODE_KEYS2.has(key)) node[key] = value;
3385
+ else semantic[key] = value;
3386
+ }
3387
+ if (Object.keys(semantic).length) node.props = semantic;
3388
+ return node;
3389
+ }
3390
+ function read(html, collection, diagnostics) {
3391
+ const pattern = new RegExp(`<${MARK} data-n="${collection.nonce}" data-i="(\\d+)" data-e="([oc])"></${MARK}>`, "g");
3392
+ const top = [];
3393
+ const open = [{ node: null, children: top }];
3394
+ let last = 0;
3395
+ const text2 = (between) => {
3396
+ var _a;
3397
+ if (!between) return;
3398
+ const into = open[open.length - 1];
3399
+ if (into.node?.type === "Html") {
3400
+ const props = (_a = into.node).props ?? (_a.props = {});
3401
+ props.html = `${typeof props.html === "string" ? props.html : ""}${between}`;
3402
+ return;
3403
+ }
3404
+ if (/<[a-zA-Z!/]/.test(between)) {
3405
+ const where = into.node ? `in a ${into.node.type}` : "around the root";
3406
+ diagnostics.push({
3407
+ code: "jsx/markup",
3408
+ severity: "error",
3409
+ message: `HTML ${where} isn't part of an Elements document: use the primitives (Text, Link, Image, \u2026) instead of ${JSON.stringify(between.slice(0, 60))}. Every component: @hyper-elements/react/GUIDE.md`,
3410
+ path: "jsx"
3411
+ });
3412
+ return;
3413
+ }
3414
+ into.children.push(unescape(between));
3415
+ };
3416
+ for (const match of html.matchAll(pattern)) {
3417
+ text2(html.slice(last, match.index));
3418
+ last = match.index + match[0].length;
3419
+ if (match[2] === "o") {
3420
+ const node = toJsonNode(collection.recorded[Number(match[1])], diagnostics);
3421
+ open[open.length - 1].children.push(node);
3422
+ open.push({ node, children: node.children });
3423
+ } else open.pop();
3424
+ }
3425
+ text2(html.slice(last));
3426
+ return top;
3427
+ }
3428
+ var COMMENTS = /<!--[\s\S]*?-->/g;
3429
+ var ASYNC_ERROR = /^(?:Objects are not valid as a React child \(found: \[object Promise\]\)|A component suspended while responding to synchronous input|Minified React error #426\b|Minified React error #31\b.*%5Bobject%20Promise%5D)/;
3430
+ var ASYNC_MESSAGE = "A component is async or suspends (an async function, React.lazy, use()): render() doesn't wait for it yet. Load its data first and pass it in as props";
3431
+ var SUSPENDED = /does not support Suspense|did not finish this Suspense boundary/;
3432
+ function unfinished(html) {
3433
+ const reasons = [];
3434
+ let [out, at] = ["", 0];
3435
+ for (let start = html.indexOf("<!--$!-->"); start >= 0; start = html.indexOf("<!--$!-->", at)) {
3436
+ out += html.slice(at, start);
3437
+ reasons.push(unescape(/^<!--\$!--><template[^>]*?\bdata-msg="([^"]*)"/.exec(html.slice(start))?.[1] ?? ""));
3438
+ const marks = /<!--(\$[!?]?|\/\$)-->/g;
3439
+ marks.lastIndex = start;
3440
+ let depth = 0;
3441
+ for (let mark = marks.exec(html); mark; mark = marks.exec(html)) {
3442
+ depth += mark[1] === "/$" ? -1 : 1;
3443
+ if (depth === 0) break;
3444
+ }
3445
+ at = depth === 0 ? marks.lastIndex : html.length;
3446
+ }
3447
+ return { html: out + html.slice(at), reasons };
3448
+ }
3449
+ function unfinishedDiagnostic(reason) {
3450
+ const said = reason.replace(/^Switched to client rendering because the server rendering errored:\s*/, "");
3451
+ const thrown = reason && !SUSPENDED.test(reason) && !ASYNC_ERROR.test(said) ? said : "";
3452
+ if (thrown) return { code: "jsx/error", severity: "error", message: `A component inside <Suspense> threw: ${thrown}`, path: "jsx" };
3453
+ const message = reason ? ASYNC_MESSAGE : `${ASYNC_MESSAGE}. (With React's production build, a component that threw inside <Suspense> looks the same: check it too.)`;
3454
+ return { code: "jsx/async", severity: "error", message, path: "jsx" };
3455
+ }
3456
+ function collectNodes(element) {
3457
+ const diagnostics = [];
3458
+ const collection = { nonce: nonce(), recorded: [], components: /* @__PURE__ */ new Map(), clashes: /* @__PURE__ */ new Set() };
3459
+ let html;
3460
+ try {
3461
+ html = withRenderScope(() => {
3462
+ frames2.push(collection);
3463
+ try {
3464
+ return within(collection, () => renderToString(element));
3465
+ } finally {
3466
+ frames2.pop();
3467
+ }
3468
+ });
3469
+ } catch (error) {
3470
+ if (!ASYNC_ERROR.test(String(error?.message))) throw error;
3471
+ diagnostics.push({ code: "jsx/async", severity: "error", message: ASYNC_MESSAGE, path: "jsx" });
3472
+ return { nodes: [], components: [...collection.components.values()], diagnostics, aborted: true };
3473
+ }
3474
+ if (collection.async) diagnostics.push({ code: "jsx/async", severity: "error", message: ASYNC_MESSAGE, path: "jsx" });
3475
+ const done = unfinished(html);
3476
+ for (const reason of done.reasons) {
3477
+ const diagnostic = unfinishedDiagnostic(reason);
3478
+ if (!diagnostics.some((d) => d.code === diagnostic.code && d.message === diagnostic.message)) diagnostics.push(diagnostic);
3479
+ }
3480
+ const nodes = read(done.html.replace(COMMENTS, ""), collection, diagnostics).filter((child) => typeof child !== "string" || child.trim());
3481
+ for (const type of collection.clashes) {
3482
+ diagnostics.push({
3483
+ code: "custom/duplicate",
3484
+ severity: "error",
3485
+ message: `Two different custom components are named ${type}. If component() ran more than once (inside a function or a component, or in a module loaded twice), call it once, at the top of a module; if they're different components, give each its own type`,
3486
+ path: "jsx"
3487
+ });
3488
+ }
3489
+ return { nodes, components: [...collection.components.values()], diagnostics, aborted: nodes.length === 0 && done.reasons.length > 0 };
3490
+ }
3491
+ function collectTemplate(element) {
3492
+ const { nodes: top, components, diagnostics, aborted } = collectNodes(element);
3493
+ if (top.length !== 1 || typeof top[0] === "string") {
3494
+ if (!aborted) {
3495
+ diagnostics.push({
3496
+ code: "jsx/root",
3497
+ severity: "error",
3498
+ message: top.length ? "A template renders one root (an Email or a Page) and nothing beside it" : "The JSX rendered no Elements root (an Email or a Page)",
3499
+ path: "jsx"
3500
+ });
3501
+ }
3502
+ return { document: null, diagnostics, components, collected: null };
3503
+ }
3504
+ const result = checkDocument({ root: top[0] }, new Registry(components));
3505
+ return { document: result.document, diagnostics: [...diagnostics, ...result.diagnostics], components, collected: top[0] };
3506
+ }
3507
+
3508
+ // src/inspect/index.ts
3509
+ var INSPECT_VERSION = 1;
3510
+ var coreOptions = (options) => ({ components: (options.components ?? []).map((component) => component.definition), ...options.root ? { root: options.root } : {} });
3511
+ function describeComponents2(options = {}) {
3512
+ const conditions = describeConditions(coreOptions(options));
3513
+ return {
3514
+ version: INSPECT_VERSION,
3515
+ ...options.root ? { root: options.root } : {},
3516
+ roots: options.root ? [options.root] : [...ROOT_NAMES],
3517
+ components: describeComponents(coreOptions(options)),
3518
+ conditions: options.root ? conditions.filter((condition) => condition.roots.includes(options.root)).map((condition) => ({ ...condition, roots: [options.root] })) : conditions
3519
+ };
3520
+ }
3521
+ function describeComponent2(type, options = {}) {
3522
+ const described = describeComponent(type, coreOptions(options));
3523
+ return described && { version: INSPECT_VERSION, ...described };
3524
+ }
3525
+ function inspect(element) {
3526
+ if (!React.isValidElement(element)) {
3527
+ throw new TypeError(`[Elements] inspect() takes a template as JSX (<Email>\u2026</Email>, <Page>\u2026</Page>), not ${element === null ? "null" : typeof element}`);
3528
+ }
3529
+ const { document, diagnostics: read2, components, collected } = collectTemplate(element);
3530
+ const failed = read2.some((d) => d.severity === "error");
3531
+ const diagnostics = [...read2, ...document && !failed ? validateTemplate(document, { components }).diagnostics : []];
3532
+ const registry = new Registry(components);
3533
+ const rootType = document?.root.type;
3534
+ const root = rootType && ROOT_NAMES.includes(rootType) ? rootType : null;
3535
+ const nodes = [];
3536
+ const visit = (node, path) => {
3537
+ const definition = registry.get(node.type);
3538
+ const roots = definition ? componentRoots(definition, registry) : [];
3539
+ nodes.push({
3540
+ path,
3541
+ type: node.type,
3542
+ ...typeof node.id === "string" && node.id ? { id: node.id } : {},
3543
+ roots,
3544
+ allowed: !!root && roots.includes(root),
3545
+ given: given(node),
3546
+ problems: diagnostics.filter((diagnostic) => on(diagnostic.path, path))
3547
+ });
3548
+ (Array.isArray(node.children) ? node.children : []).forEach((child, index) => {
3549
+ if (child && typeof child === "object") visit(child, `${path}.children[${index}]`);
3550
+ });
3551
+ };
3552
+ if (document && collected) visit(collected, "root");
3553
+ return { version: INSPECT_VERSION, root, valid: !diagnostics.some((d) => d.severity === "error"), nodes, diagnostics };
3554
+ }
3555
+ function given(node) {
3556
+ const keys = (prefix, values) => values && typeof values === "object" && !Array.isArray(values) ? Object.keys(values).map((key) => `${prefix}.${key}`) : [];
3557
+ const style = node.style && typeof node.style === "object" && !Array.isArray(node.style) ? node.style : {};
3558
+ const lifts = !CUSTOM_TYPE.test(node.type);
3559
+ const own = Object.keys(style).filter((key) => !(lifts && CONDITION_NAMES.has(key)));
3560
+ const condition = (name) => [...keys(name, node[name]), ...lifts ? keys(name, style[name]) : []];
3561
+ return [...keys("props", node.props), ...own.map((key) => `style.${key}`), ...condition("mobile"), ...condition("hover"), ...keys("hidden", node.hidden)];
3562
+ }
3563
+ function on(diagnostic, node) {
3564
+ if (diagnostic === node) return true;
3565
+ if (!diagnostic.startsWith(node)) return false;
3566
+ const rest = diagnostic.slice(node.length);
3567
+ return (rest.startsWith(".") || rest.startsWith("(")) && !rest.startsWith(".children[");
3568
+ }
3569
+
3570
+ export { INSPECT_VERSION, describeComponent2 as describeComponent, describeComponents2 as describeComponents, inspect };
3571
+ //# sourceMappingURL=inspect.js.map
3572
+ //# sourceMappingURL=inspect.js.map