@hyper-elements/react 0.3.0 → 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.
@@ -34,21 +34,26 @@ var Diagnostics = class {
34
34
  constructor() {
35
35
  this.list = [];
36
36
  }
37
- add(code, severity, message, path, nodeId) {
38
- this.list.push(nodeId === void 0 ? { code, severity, message, path } : { code, severity, message, path, nodeId });
37
+ add(code, severity, message, path, nodeId, extra = {}) {
38
+ const d = { code, severity, message, path };
39
+ if (nodeId !== void 0) d.nodeId = nodeId;
40
+ if (extra.clients) d.clients = [...extra.clients];
41
+ if (extra.fix) d.fix = extra.fix;
42
+ if (extra.see) d.see = extra.see;
43
+ this.list.push(d);
39
44
  }
40
45
  error(code, message, path, nodeId) {
41
46
  this.add(code, "error", message, path, nodeId);
42
47
  }
43
- warning(code, message, path, nodeId) {
44
- this.add(code, "warning", message, path, nodeId);
48
+ warning(code, message, path, nodeId, extra) {
49
+ this.add(code, "warning", message, path, nodeId, extra);
45
50
  }
46
51
  get hasErrors() {
47
52
  return this.list.some((d) => d.severity === "error");
48
53
  }
49
54
  };
50
55
  function formatDiagnostics(diagnostics) {
51
- return diagnostics.map((d) => `${d.severity} ${d.code} at ${d.path}: ${d.message}`).join("\n");
56
+ return diagnostics.map((d) => `${d.severity} ${d.code} at ${d.path}: ${d.message}${d.clients ? ` [${d.clients.join(", ")}]` : ""}${d.fix ? ` Fix: ${d.fix}` : ""}${d.see ? ` See: ${d.see}` : ""}`).join("\n");
52
57
  }
53
58
  var ElementsError = class extends Error {
54
59
  constructor(diagnostics) {
@@ -59,6 +64,64 @@ ${formatDiagnostics(errors)}`);
59
64
  this.diagnostics = diagnostics;
60
65
  }
61
66
  };
67
+ var captured = (against, wordsWithin4px, lineBreaks) => ({ elements: "0.3.0", against, captured: "2026-11-08", wordsWithin4px, lineBreaks });
68
+ var tables = (name) => ({ name, widths: {}, dark: false, approximate: true, measured: null });
69
+ var EMAIL_CLIENTS = {
70
+ 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) },
71
+ "outlook-com": { name: "Outlook.com and new Outlook", widths: { desktop: 820 }, dark: false, approximate: false, measured: captured("Outlook on the web", 1, 1) },
72
+ "outlook-mac": tables("Outlook for Mac"),
73
+ "outlook-app": tables("Outlook app (iOS and Android)"),
74
+ gmail: { name: "Gmail", widths: { desktop: 820 }, dark: false, approximate: false, measured: captured("Gmail on the web", 1, 1) },
75
+ "gmail-app": { name: "Gmail app", widths: { phone: 375 }, dark: false, approximate: true, measured: null },
76
+ "gmail-app-other": { name: "Gmail app, another account", widths: { phone: 375 }, dark: false, approximate: true, measured: null },
77
+ "apple-mail": { name: "Apple Mail", widths: { desktop: 820 }, dark: true, approximate: false, measured: captured("Mail 16 on macOS 26", 0.999, 0.999) },
78
+ yahoo: tables("Yahoo Mail"),
79
+ aol: tables("AOL Mail"),
80
+ "samsung-email": tables("Samsung Email"),
81
+ thunderbird: tables("Thunderbird"),
82
+ protonmail: tables("Proton Mail"),
83
+ fastmail: tables("Fastmail"),
84
+ hey: tables("HEY"),
85
+ gmx: tables("GMX"),
86
+ "web-de": tables("WEB.DE"),
87
+ orange: tables("Orange"),
88
+ sfr: tables("SFR"),
89
+ laposte: tables("La Poste"),
90
+ "free-fr": tables("Free"),
91
+ "t-online-de": tables("T-Online"),
92
+ "mail-ru": tables("Mail.ru"),
93
+ ionos: tables("IONOS (1&1)"),
94
+ "wp-pl": tables("WP Poczta"),
95
+ rainloop: tables("RainLoop")
96
+ };
97
+ var DEFAULT_EMAIL_CLIENTS = ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "apple-mail", "yahoo", "aol", "samsung-email"];
98
+ var caniemail = (feature) => `https://www.caniemail.com/features/${feature}/`;
99
+ function checkEmailClients(clients) {
100
+ if (clients === void 0) return;
101
+ const unknown = Array.isArray(clients) ? clients.filter((c) => !Object.prototype.hasOwnProperty.call(EMAIL_CLIENTS, c)) : [clients];
102
+ 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(", ")}`);
103
+ }
104
+ function forEmailClients(diagnostics, selection = DEFAULT_EMAIL_CLIENTS) {
105
+ const out = [];
106
+ const seen = /* @__PURE__ */ new Map();
107
+ for (const d of diagnostics) {
108
+ if (!d.clients) {
109
+ out.push(d);
110
+ continue;
111
+ }
112
+ const clients = d.clients.filter((c) => selection.includes(c));
113
+ if (!clients.length) continue;
114
+ const key = JSON.stringify([d.code, d.path, d.severity, d.message]);
115
+ const same42 = seen.get(key);
116
+ if (same42) same42.clients = [.../* @__PURE__ */ new Set([...same42.clients, ...clients])];
117
+ else {
118
+ const kept = { ...d, clients };
119
+ seen.set(key, kept);
120
+ out.push(kept);
121
+ }
122
+ }
123
+ return out;
124
+ }
62
125
  var ok = (value) => ({ ok: true, value });
63
126
  var fail = (message) => ({ ok: false, message });
64
127
  var show = (input) => {
@@ -644,6 +707,26 @@ function attributes() {
644
707
  jsonSchema: () => ({ type: "object", additionalProperties: { type: "string" } })
645
708
  });
646
709
  }
710
+ var MIN_PHONE_WIDTH = 240;
711
+ function breakpoints() {
712
+ const accepts = `{ mobile }: the phone width in px (from ${MIN_PHONE_WIDTH})`;
713
+ return kind({
714
+ name: "breakpoints",
715
+ editor: "number",
716
+ accepts,
717
+ parse(input) {
718
+ if (!isObject(input)) return fail(`expected ${accepts}, not ${show(input)}`);
719
+ const unknown = Object.keys(input).find((key) => key !== "mobile");
720
+ if (unknown !== void 0) return fail(`${show(unknown)} isn't a breakpoint a Page has: it has mobile`);
721
+ const { mobile } = input;
722
+ if (mobile === void 0) return ok({});
723
+ 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)}`);
724
+ return ok({ mobile });
725
+ },
726
+ empty: (value) => value.mobile === void 0,
727
+ jsonSchema: () => ({ type: "object", properties: { mobile: { type: "integer", minimum: MIN_PHONE_WIDTH } }, additionalProperties: false })
728
+ });
729
+ }
647
730
  function linkParts(value) {
648
731
  return typeof value === "string" ? { href: value } : value;
649
732
  }
@@ -657,6 +740,54 @@ function defineComponent(definition) {
657
740
  function prop(kind2, options) {
658
741
  return { kind: kind2, ...options };
659
742
  }
743
+ var MOBILE = {
744
+ name: "mobile",
745
+ on: "on phones",
746
+ for: "for phones",
747
+ roots: (root) => !!root?.responsive,
748
+ notAllowed: { code: "mobile/not-supported", message: "Phone styles are for an Email or a Page, which have a phone width" },
749
+ sheet: { media: "phone", selector: "&" },
750
+ allows: (spec) => !!spec.mobile,
751
+ support: { web: { level: "full", source: "writer" }, email: { level: "full", source: "writer" } },
752
+ styles: {
753
+ // The email's phone text styles are rules that override a size; "normal" isn't one.
754
+ lineHeight: { email: { level: "none", values: ["normal"], shows: "the line height it has on desktop", source: "writer" } }
755
+ },
756
+ outlookApplies: false,
757
+ hiddenKey: "mobile"
758
+ };
759
+ var HOVER = {
760
+ name: "hover",
761
+ on: "on hover",
762
+ for: "for hover",
763
+ // Keyboard focus gets the same styles as the pointer: a link or button reached by Tab shows its hover colors.
764
+ // A box around the link (an email Button's cell, its background) takes them while the link in it has focus.
765
+ sheet: { media: "all", selector: "&:hover,&:focus-visible", within: "&:hover,&:focus-within", level: "hover" },
766
+ allows: (_, key, definition) => !!definition.hover?.includes(key),
767
+ support: {
768
+ web: { level: "full", source: "css" },
769
+ // On type selectors only in Outlook.com and Outlook for Mac (Elements' are classes); the phone apps' results don't
770
+ // matter, as nothing hovers there.
771
+ email: {
772
+ level: "partial",
773
+ shows: "no change (where it shows, keyboard focus gets it too)",
774
+ source: "caniemail",
775
+ feature: "css-pseudo-class-hover",
776
+ clients: ["outlook", "outlook-com", "outlook-mac", "gmx", "web-de", "sfr", "laposte", "mail-ru"]
777
+ }
778
+ },
779
+ paged: (node, where) => `A hover isn't possible in ${where}: nothing points at it. The ${node.type} shows its own colors`,
780
+ outlookApplies: false
781
+ };
782
+ var CONDITIONS = [MOBILE, HOVER];
783
+ var CONDITION_NAMES = new Set(CONDITIONS.map((condition) => condition.name));
784
+ function blockOf(node, condition) {
785
+ const block = node[condition.name];
786
+ return block && Object.keys(block).length ? block : void 0;
787
+ }
788
+ function shownAnywhere(node) {
789
+ return !(node.hidden?.mobile && node.hidden?.desktop);
790
+ }
660
791
  var writer = { level: "full", source: "writer" };
661
792
  var css = { level: "full", source: "css" };
662
793
  var textStyle = {
@@ -665,7 +796,12 @@ var textStyle = {
665
796
  fontFamily: { kind: fontFamily(), label: "Font", inherited: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
666
797
  // Word rounds sizes to half points (13px is 10pt): a fraction of a px.
667
798
  fontSize: { kind: length(["px"], { min: 1 }), label: "Font size", inherited: true, mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
668
- fontWeight: { kind: fontWeight(), label: "Font weight", inherited: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
799
+ fontWeight: {
800
+ kind: fontWeight(),
801
+ label: "Font weight",
802
+ inherited: true,
803
+ 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 }
804
+ },
669
805
  fontStyle: { kind: enumOf(["normal", "italic"]), label: "Font style", inherited: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
670
806
  // Written in px with mso-line-height-rule: exactly, which Word keeps (it drops unitless ones).
671
807
  lineHeight: { kind: lineHeight(), label: "Line height", inherited: true, mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
@@ -677,8 +813,21 @@ var textStyle = {
677
813
  support: { email: { level: "outlook", shows: "normal spacing: Word keeps letter spacing but doesn't draw it", source: "outlook-captures" }, web: css }
678
814
  },
679
815
  textAlign: { kind: enumOf(["left", "center", "right", "justify"]), label: "Alignment", inherited: true, mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
680
- textTransform: { kind: enumOf(["none", "uppercase", "lowercase", "capitalize"]), label: "Letter case", inherited: true, support: { email: { level: "full", source: "assumed" }, web: css } },
681
- textDecoration: { kind: enumOf(["none", "underline", "line-through"]), label: "Decoration", support: { email: { level: "full", source: "caniemail" }, web: css } }
816
+ textTransform: {
817
+ kind: enumOf(["none", "uppercase", "lowercase", "capitalize"]),
818
+ label: "Letter case",
819
+ inherited: true,
820
+ support: { email: { level: "outlook", values: ["lowercase"], shows: "the text as written: Word doesn't lowercase it", source: "caniemail", feature: "css-text-transform" }, web: css }
821
+ },
822
+ textDecoration: {
823
+ kind: enumOf(["none", "underline", "line-through"]),
824
+ label: "Decoration",
825
+ support: {
826
+ // Removed, so "none" leaves a link the client's own underline: said only where a line is asked for.
827
+ 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"] },
828
+ web: css
829
+ }
830
+ }
682
831
  };
683
832
  var inheritedTextStyle = (({ textDecoration: _, ...rest }) => rest)(textStyle);
684
833
  var boxStyle = {
@@ -691,7 +840,17 @@ var boxStyle = {
691
840
  padding: { kind: insets(), label: "Padding", mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
692
841
  backgroundColor: { kind: color(), label: "Background color", support: { email: writer, web: css } },
693
842
  border: { kind: border(), label: "Border", mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
694
- borderRadius: { kind: radius(), label: "Corner radius", support: { email: { level: "outlook", shows: "square corners", source: "caniemail" }, web: css } },
843
+ borderRadius: {
844
+ kind: radius(),
845
+ label: "Corner radius",
846
+ support: {
847
+ email: [
848
+ { level: "outlook", shows: "square corners", source: "caniemail", feature: "css-border-radius" },
849
+ { level: "partial", shows: "square corners", source: "caniemail", feature: "css-border-radius", clients: ["gmx", "web-de", "orange"] }
850
+ ],
851
+ web: css
852
+ }
853
+ },
695
854
  overflow: {
696
855
  kind: enumOf(["visible", "hidden"]),
697
856
  label: "Overflow",
@@ -899,6 +1058,15 @@ var Grid = defineComponent({
899
1058
  },
900
1059
  children: { categories: ["layout", "block"] },
901
1060
  sizing: "stretch",
1061
+ // Stacked on phones, every two of its children are its phone gap apart: there are no rows for a row gap.
1062
+ conditionSupport: {
1063
+ mobile: {
1064
+ rowGap: {
1065
+ web: { level: "none", when: (_, node) => node.props.stackOnMobile !== false, shows: "its phone gap between every two children", source: "writer" },
1066
+ email: { level: "none", when: (_, node) => node.props.stackOnMobile !== false, shows: "its phone gap between every two children", source: "writer" }
1067
+ }
1068
+ }
1069
+ },
902
1070
  targets: { email: {}, web: {} },
903
1071
  editor: {
904
1072
  groups: [
@@ -911,7 +1079,7 @@ var Grid = defineComponent({
911
1079
  var Layer = defineComponent({
912
1080
  type: "Layer",
913
1081
  label: "Layer",
914
- description: "Its children over each other, in one box as tall as the tallest: an image with words on it. In an email, an Image with one child over it.",
1082
+ 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.",
915
1083
  categories: ["layout", "block"],
916
1084
  props: {},
917
1085
  style: {
@@ -943,13 +1111,27 @@ var Layer = defineComponent({
943
1111
  }
944
1112
  });
945
1113
  var WRITTEN3 = { email: { level: "full", source: "writer" }, web: { level: "full", source: "writer" } };
1114
+ 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"];
946
1115
  var ROOT_PROPS = {
947
1116
  title: prop(text(), { support: WRITTEN3, label: "Title", description: "The document's title (shown in a browser tab, and by some email clients)." }),
948
- lang: prop(text({ maxLength: 35 }), { support: WRITTEN3, label: "Language", description: 'Its language, as a BCP 47 tag ("en", "ar").' }),
949
- dir: prop(enumOf(["ltr", "rtl", "auto"]), { support: WRITTEN3, label: "Text direction" }),
1117
+ lang: prop(text({ maxLength: 35 }), {
1118
+ support: {
1119
+ ...WRITTEN3,
1120
+ 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"] }
1121
+ },
1122
+ label: "Language",
1123
+ description: 'Its language, as a BCP 47 tag ("en", "ar").'
1124
+ }),
1125
+ dir: prop(enumOf(["ltr", "rtl", "auto"]), {
1126
+ support: {
1127
+ ...WRITTEN3,
1128
+ email: { level: "partial", values: ["rtl", "auto"], shows: "left to right: the client removes the direction", source: "caniemail", feature: "html-dir", clients: ["gmx", "web-de"] }
1129
+ },
1130
+ label: "Text direction"
1131
+ }),
950
1132
  fonts: prop(fontFaces(), {
951
1133
  support: {
952
- email: { level: "partial", shows: "Apple Mail and iOS Mail load web fonts; Gmail and classic Outlook don't, and show the next font in each stack", source: "caniemail" },
1134
+ 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 },
953
1135
  web: { level: "full", source: "writer" }
954
1136
  },
955
1137
  label: "Web fonts",
@@ -1089,7 +1271,10 @@ var PageNumber = defineComponent({
1089
1271
  style: {},
1090
1272
  children: {},
1091
1273
  sizing: "none",
1092
- targets: { web: {} }
1274
+ targets: { web: {} },
1275
+ // A Page's or a Canvas's text has no page to number.
1276
+ roots: ["Document"],
1277
+ notAllowed: { code: "document/page-number", message: (root) => `A PageNumber is a page's number in a Document; this ${root} has no pages` }
1093
1278
  });
1094
1279
  function documentPages(root) {
1095
1280
  const pages = [];
@@ -1225,6 +1410,11 @@ var Page = defineComponent({
1225
1410
  props: {
1226
1411
  width: prop(length(["px"], { min: 200 }), { support: WRITTEN6, label: "Content width", description: "The widest the content gets.", default: 1200 }),
1227
1412
  description: prop(text(), { support: WRITTEN6, label: "Description", description: "The page's meta description." }),
1413
+ breakpoints: prop(breakpoints(), {
1414
+ support: { web: { level: "full", source: "writer" } },
1415
+ label: "Breakpoints",
1416
+ description: "Where its phone styles start: mobile, the widest window they apply to, in px (480 where it's not set)."
1417
+ }),
1228
1418
  ...ROOT_PROPS
1229
1419
  },
1230
1420
  style: ROOT_STYLE,
@@ -1232,6 +1422,12 @@ var Page = defineComponent({
1232
1422
  children: { categories: ["section", "layout", "block"] },
1233
1423
  sizing: "none",
1234
1424
  responsive: true,
1425
+ // The page writer writes the root's phone gap, not its phone text styles.
1426
+ conditionSupport: {
1427
+ mobile: Object.fromEntries(
1428
+ ["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" } }])
1429
+ )
1430
+ },
1235
1431
  targets: { web: {} },
1236
1432
  editor: {
1237
1433
  groups: [
@@ -1475,6 +1671,7 @@ var Image = defineComponent({
1475
1671
  },
1476
1672
  children: {},
1477
1673
  sizing: "shrink",
1674
+ conditionSupport: { mobile: { border: { email: { level: "none", shows: "its desktop border", source: "writer" } } } },
1478
1675
  targets: { email: {}, web: {} },
1479
1676
  editor: { groups: [{ title: "Image", paths: ["props.src", "props.alt", "style.width"] }, { title: "Box", paths: ["style.border", "style.borderRadius"] }] }
1480
1677
  });
@@ -1605,6 +1802,7 @@ var TableCell = defineComponent({
1605
1802
  },
1606
1803
  children: { text: true, categories: ["inline"], types: ["Image"] },
1607
1804
  sizing: "none",
1805
+ conditionSupport: { mobile: { border: { email: { level: "none", shows: "its desktop border", source: "writer" } } } },
1608
1806
  targets: { email: {}, web: {} },
1609
1807
  editor: { groups: [{ title: "Cell", paths: ["props.colSpan", "style.width", "style.verticalAlign"] }, TEXT_EDITOR, { title: "Box", paths: ["style.padding", "style.backgroundColor", "style.border"] }] }
1610
1808
  });
@@ -1722,17 +1920,27 @@ function checkDocument(input, options) {
1722
1920
  const extra = Object.keys(input).filter((key) => key !== "root");
1723
1921
  for (const key of extra) d.error("document/unknown-key", `"${key}" isn't part of a document (root)`, key);
1724
1922
  const rootDefinition = isObject2(input.root) && typeof input.root.type === "string" ? registry.get(input.root.type) : void 0;
1725
- const context = { d, registry, used: /* @__PURE__ */ new Set(), responsive: !!rootDefinition?.responsive };
1923
+ const context = { d, registry, used: /* @__PURE__ */ new Set(), root: rootDefinition };
1726
1924
  const root = input.root === void 0 ? null : readNode(input.root, "root", null, context);
1727
1925
  if (input.root === void 0) d.error("document/root", 'A document has a root: { type: "Email" }, "Page", \u2026', "root");
1728
1926
  return { document: root ? { root } : null, diagnostics: d.list };
1729
1927
  }
1730
1928
  function checkFragment(input, options) {
1731
1929
  const d = new Diagnostics();
1732
- const context = { d, registry: options.registry, idPrefix: options.idPrefix, used: new Set(options.taken), responsive: options.responsive };
1930
+ const context = { d, registry: options.registry, idPrefix: options.idPrefix, used: new Set(options.taken), root: options.root };
1733
1931
  const node = readNode(input, options.path, options.parent, context);
1734
1932
  return { node, diagnostics: d.list };
1735
1933
  }
1934
+ function rootList(roots) {
1935
+ const named = roots.map((root) => a(root).replace(/^A/, "a"));
1936
+ return named.length > 1 ? `${named.slice(0, -1).join(", ")} or ${named[named.length - 1]}` : named[0] ?? "no root";
1937
+ }
1938
+ function rootTakes(roots, root) {
1939
+ return !roots || !root || roots.includes(root.type);
1940
+ }
1941
+ function sharesOutput(definition, root) {
1942
+ return Object.keys(definition.targets).some((target) => target in root.targets);
1943
+ }
1736
1944
  function allowedIn(parent, child) {
1737
1945
  const rule2 = parent.children;
1738
1946
  return !!rule2.types?.includes(child.type) || child.categories.some((category) => rule2.categories?.includes(category));
@@ -1758,7 +1966,29 @@ function editDistance(from, to) {
1758
1966
  }
1759
1967
  return row[to.length];
1760
1968
  }
1969
+ var OTHER_BREAKPOINTS = /* @__PURE__ */ new Set(["base", "initial", "xs", "sm", "md", "lg", "xl", "2xl", "tablet", "desktop", "wide", "small", "medium", "large"]);
1970
+ function conditionHint(definition, key) {
1971
+ const name = key.trim().replace(/^(&?:+|_+|\$|@)/, "").replace(/Style$/, "").toLowerCase();
1972
+ const own = CUSTOM_TYPE.test(definition.type);
1973
+ const hoverStyles = definition.hover ?? [];
1974
+ 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)`;
1975
+ 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 } }}`;
1976
+ if (name === "hover") return toHover;
1977
+ if (name === "mobile" || name === "phone" || name === "phones" || key.trim().startsWith("@media")) return toPhone;
1978
+ if (name === "focus" || name === "focus-visible" || name === "focusvisible" || name === "focus-within") {
1979
+ 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`;
1980
+ }
1981
+ if (OTHER_BREAKPOINTS.has(name)) {
1982
+ 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 }})`;
1983
+ }
1984
+ if (name === "active" || name === "pressed" || name === "press" || name === "visited" || name === "dark" || name === "light" || name === "print") {
1985
+ return `there are no ${name} styles: Elements has phone styles (mobile) and hover styles (hover) in style`;
1986
+ }
1987
+ return void 0;
1988
+ }
1761
1989
  function unknownProp(definition, key) {
1990
+ const condition = conditionHint(definition, key);
1991
+ if (condition) return `${a(definition.type)} has no prop "${key}": ${condition}`;
1762
1992
  if (Object.prototype.hasOwnProperty.call(definition.style, key)) return `${a(definition.type)} has no prop "${key}": it's a style, style={{ ${key}: \u2026 }}`;
1763
1993
  const known = Object.keys(definition.props);
1764
1994
  const meant = nearest(key, known);
@@ -1798,6 +2028,15 @@ function readNode(input, path, parent, context) {
1798
2028
  d.error("children/not-allowed", placementMessage(parent, definition), path);
1799
2029
  return null;
1800
2030
  }
2031
+ const root = context.root;
2032
+ if (root && !rootTakes(definition.roots, root) && sharesOutput(definition, root)) {
2033
+ const { code, message } = definition.notAllowed ?? {
2034
+ code: "component/not-supported",
2035
+ message: (under) => `${a(definition.type)} goes in ${rootList(definition.roots ?? [])}, not in ${a(under).replace(/^A/, "a")}`
2036
+ };
2037
+ d.error(code, message(root.type), path);
2038
+ return null;
2039
+ }
1801
2040
  let id;
1802
2041
  if (input.id !== void 0) {
1803
2042
  if (typeof input.id !== "string" || !ID.test(input.id)) {
@@ -1817,21 +2056,49 @@ function readNode(input, path, parent, context) {
1817
2056
  if (!NODE_KEYS.includes(key)) d.error("node/unknown-key", `"${key}" isn't part of a component (${NODE_KEYS.join(", ")})`, `${path}.${key}`, id);
1818
2057
  }
1819
2058
  const node = id === void 0 ? { type: definition.type } : { id, type: definition.type };
1820
- const props = readProps(input.props, definition, `${path}.props`, id, d);
2059
+ const props = readProps(input.props, definition, `${path}.props`, id, d, context.root);
1821
2060
  if (props) node.props = props;
1822
- const style = readStyles(input.style, definition, definition.style, `${path}.style`, id, d, "style", () => true);
1823
- if (style) node.style = style;
1824
- if (input.mobile !== void 0) {
1825
- if (!context.responsive) d.error("mobile/not-supported", "Phone styles are for an Email or a Page, which have a phone width", `${path}.mobile`, id);
1826
- else {
1827
- const mobile = readStyles(input.mobile, definition, definition.style, `${path}.mobile`, id, d, "mobile", (spec) => !!spec.mobile);
1828
- if (mobile) node.mobile = mobile;
2061
+ const lifted = {};
2062
+ let styleInput = input.style;
2063
+ if (isObject2(styleInput) && !CUSTOM_TYPE.test(definition.type)) {
2064
+ const rest = {};
2065
+ for (const [key, value] of Object.entries(styleInput)) {
2066
+ if (CONDITION_NAMES.has(key)) lifted[key] = value;
2067
+ else rest[key] = value;
1829
2068
  }
2069
+ styleInput = rest;
1830
2070
  }
1831
- if (input.hover !== void 0) {
1832
- const allowed = new Set(definition.hover ?? []);
1833
- const hover = readStyles(input.hover, definition, definition.style, `${path}.hover`, id, d, "hover", (_, key) => allowed.has(key));
1834
- if (hover) node.hover = hover;
2071
+ const style = readStyles(styleInput, definition, definition.style, `${path}.style`, id, d, "style", () => true, void 0, context.root);
2072
+ if (style) node.style = style;
2073
+ for (const condition of CONDITIONS) {
2074
+ const inStyle = lifted[condition.name];
2075
+ const given = input[condition.name];
2076
+ if (inStyle !== void 0 && given !== void 0) {
2077
+ d.error(
2078
+ "style/twice",
2079
+ `${condition.name} is given twice: as ${condition.name}={\u2026} and in style. Give it once, in style: style={{ ${condition.name}: { \u2026 } }}`,
2080
+ `${path}.style.${condition.name}`,
2081
+ id
2082
+ );
2083
+ continue;
2084
+ }
2085
+ let value = inStyle ?? given;
2086
+ if (value === void 0) continue;
2087
+ const at = inStyle !== void 0 ? `${path}.style.${condition.name}` : `${path}.${condition.name}`;
2088
+ if (condition.roots && !condition.roots(context.root)) {
2089
+ if (condition.notAllowed) d.error(condition.notAllowed.code, condition.notAllowed.message, at, id);
2090
+ continue;
2091
+ }
2092
+ if (isObject2(value) && Object.keys(value).some((key) => CONDITION_NAMES.has(key))) {
2093
+ const own = {};
2094
+ for (const [key, inner] of Object.entries(value)) {
2095
+ 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);
2096
+ else own[key] = inner;
2097
+ }
2098
+ value = own;
2099
+ }
2100
+ const read2 = readStyles(value, definition, definition.style, at, id, d, condition.name, (spec, key) => condition.allows(spec, key, definition), condition.for, context.root);
2101
+ if (read2) node[condition.name] = read2;
1835
2102
  }
1836
2103
  if (input.hidden !== void 0) {
1837
2104
  const hidden = readHidden(input.hidden, `${path}.hidden`, id, context);
@@ -1841,7 +2108,7 @@ function readNode(input, path, parent, context) {
1841
2108
  if (children.length) node.children = children;
1842
2109
  return node;
1843
2110
  }
1844
- function readProps(input, definition, path, id, d) {
2111
+ function readProps(input, definition, path, id, d, root) {
1845
2112
  const out = {};
1846
2113
  if (input !== void 0 && !isObject2(input)) {
1847
2114
  d.error("props/invalid", "props is an object", path, id);
@@ -1854,9 +2121,14 @@ function readProps(input, definition, path, id, d) {
1854
2121
  d.error("prop/unknown", unknownProp(definition, key), `${path}.${key}`, id);
1855
2122
  continue;
1856
2123
  }
2124
+ if (!rootTakes(spec.roots, root)) {
2125
+ 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);
2126
+ continue;
2127
+ }
1857
2128
  const parsed = spec.kind.parse(value);
1858
- if (parsed.ok) out[key] = parsed.value;
1859
- else d.error("prop/invalid", `${key}: ${parsed.message}`, `${path}.${key}`, id);
2129
+ if (parsed.ok) {
2130
+ if (!spec.kind.empty?.(parsed.value)) out[key] = parsed.value;
2131
+ } else d.error("prop/invalid", `${key}: ${parsed.message}`, `${path}.${key}`, id);
1860
2132
  }
1861
2133
  for (const [key, spec] of Object.entries(definition.props)) {
1862
2134
  if (spec.required && !(key in out) && !(isObject2(input) && input[key] !== void 0)) {
@@ -1873,6 +2145,8 @@ var UNSUPPORTED_CSS = {
1873
2145
  marginRight: "use padding",
1874
2146
  display: "the component sets it",
1875
2147
  position: "use a Layer for content placed over other content",
2148
+ backgroundImage: "put a Layer around it, its first child an Image (the Layer's height crops it to cover), the rest over it",
2149
+ background: "backgroundColor for a color; for an image, a Layer: its first child an Image, the rest over it",
1876
2150
  float: "use a Row to put things side by side",
1877
2151
  // Names Elements used before it took CSS's.
1878
2152
  align: "it's alignItems now",
@@ -1886,10 +2160,12 @@ function styleHint(definition, specs, key) {
1886
2160
  if (Object.prototype.hasOwnProperty.call(definition.props, key)) return `: it's a prop, ${key}={\u2026}`;
1887
2161
  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";
1888
2162
  if (UNSUPPORTED_CSS[key]) return `: ${UNSUPPORTED_CSS[key]}`;
2163
+ const condition = conditionHint(definition, key);
2164
+ if (condition) return `: ${condition}`;
1889
2165
  const meant = nearest(key, Object.keys(specs));
1890
2166
  return meant ? `: did you mean "${meant}"?` : "";
1891
2167
  }
1892
- function readStyles(input, definition, specs, path, id, d, what, allowed) {
2168
+ function readStyles(input, definition, specs, path, id, d, what, allowed, forLabel, root) {
1893
2169
  if (input === void 0) return void 0;
1894
2170
  if (!isObject2(input)) {
1895
2171
  d.error(`${what}/invalid`, `${what} is an object of style properties`, path, id);
@@ -1903,8 +2179,12 @@ function readStyles(input, definition, specs, path, id, d, what, allowed) {
1903
2179
  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);
1904
2180
  continue;
1905
2181
  }
2182
+ if (!rootTakes(spec.roots, root)) {
2183
+ 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);
2184
+ continue;
2185
+ }
1906
2186
  if (!allowed(spec, key)) {
1907
- d.error(`${what}/not-allowed`, what === "mobile" ? `"${key}" can't be set for phones` : `"${key}" can't be set for hover`, `${path}.${key}`, id);
2187
+ d.error(`${what}/not-allowed`, `"${key}" can't be set ${forLabel ?? "here"}`, `${path}.${key}`, id);
1908
2188
  continue;
1909
2189
  }
1910
2190
  const parsed = spec.kind.parse(value);
@@ -1915,7 +2195,7 @@ function readStyles(input, definition, specs, path, id, d, what, allowed) {
1915
2195
  }
1916
2196
  function readHidden(input, path, id, context) {
1917
2197
  const { d } = context;
1918
- if (!context.responsive) {
2198
+ if (!context.root?.responsive) {
1919
2199
  d.error("hidden/not-supported", "Hiding on phones or desktops is for an Email or a Page", path, id);
1920
2200
  return void 0;
1921
2201
  }
@@ -1926,7 +2206,7 @@ function readHidden(input, path, id, context) {
1926
2206
  const out = {};
1927
2207
  if (input.mobile === true) out.mobile = true;
1928
2208
  if (input.desktop === true) out.desktop = true;
1929
- if (out.mobile && out.desktop) d.warning("hidden/everywhere", "Hidden on phones and on desktops, it never shows", path, id);
2209
+ if (!shownAnywhere({ hidden: out })) d.warning("hidden/everywhere", "Hidden on phones and on desktops, it never shows", path, id);
1930
2210
  return Object.keys(out).length ? out : void 0;
1931
2211
  }
1932
2212
  function readChildren(input, definition, path, id, context) {
@@ -1959,6 +2239,22 @@ function readChildren(input, definition, path, id, context) {
1959
2239
  });
1960
2240
  return out;
1961
2241
  }
2242
+ var same = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
2243
+ function reportDeclared(d, node, at, key, value, declared, target, outlook = true) {
2244
+ if (!declared) return;
2245
+ for (const rule2 of [].concat(declared)) {
2246
+ if (rule2.level === "full") continue;
2247
+ if (rule2.level === "outlook" && !outlook) continue;
2248
+ if (rule2.values && !rule2.values.some((v) => same(v, value))) continue;
2249
+ if (rule2.when && !rule2.when(value, node)) continue;
2250
+ const asked = typeof value === "string" ? `${key}: "${value}"` : key;
2251
+ const clients = target === "email" ? rule2.clients ?? (rule2.level === "outlook" ? ["outlook"] : void 0) : void 0;
2252
+ const output = clients ? "some email clients" : target === "web" ? "a page" : target;
2253
+ 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}`];
2254
+ const [said, as] = rule2.source === "assumed" ? ["info", `${message} (not checked)`] : [severity, message];
2255
+ d.add(code, said, as, at, node.id, { clients, fix: rule2.fix, see: rule2.feature && caniemail(rule2.feature) });
2256
+ }
2257
+ }
1962
2258
  var MAX_DEPTH = 32;
1963
2259
  function templatePathsOf(document, paths) {
1964
2260
  const places = /* @__PURE__ */ new Map();
@@ -1975,7 +2271,7 @@ function templatePathsOf(document, paths) {
1975
2271
  return path;
1976
2272
  };
1977
2273
  }
1978
- function expandDocument(document, options) {
2274
+ function expandDocument(document, options, target) {
1979
2275
  const registry = registryOf(options);
1980
2276
  const d = new Diagnostics();
1981
2277
  const paths = /* @__PURE__ */ new WeakMap();
@@ -1987,7 +2283,20 @@ function expandDocument(document, options) {
1987
2283
  collect(document.root);
1988
2284
  const rootDefinition = registry.get(document.root.type);
1989
2285
  const responsive = !!rootDefinition?.responsive;
2286
+ const unshown = (node, path) => {
2287
+ const declared = target && registry.get(node.type)?.conditionSupport;
2288
+ if (!declared) return;
2289
+ for (const condition of CONDITIONS) {
2290
+ const block = blockOf(node, condition);
2291
+ const byStyle = declared[condition.name];
2292
+ if (!block || !byStyle || condition.hiddenKey && node.hidden?.[condition.hiddenKey]) continue;
2293
+ for (const [key, value] of Object.entries(block)) {
2294
+ reportDeclared(d, { type: node.type, id: node.id, props: node.props ?? {} }, `${path}.${condition.name}.${key}`, key, value, byStyle[key]?.[target], target);
2295
+ }
2296
+ }
2297
+ };
1990
2298
  const visit = (given, path, depth, self = path) => {
2299
+ unshown(given, path);
1991
2300
  const arrange = registry.get(given.type)?.arrange;
1992
2301
  if (!arrange) return walk(given, path, self, depth);
1993
2302
  const origins = /* @__PURE__ */ new Map();
@@ -2014,27 +2323,34 @@ function expandDocument(document, options) {
2014
2323
  return void 0;
2015
2324
  }
2016
2325
  let raw2;
2326
+ const reported = [];
2017
2327
  try {
2018
2328
  const style = { ...definition.defaultStyle, ...node.style };
2019
- raw2 = definition.expand(propsWithDefaults(definition, node.props), { register: (custom) => registry.add(custom), style, responsive });
2329
+ raw2 = definition.expand(propsWithDefaults(definition, node.props), { register: (custom) => registry.add(custom), style, responsive, report: (diagnostic) => reported.push(diagnostic) });
2020
2330
  } catch (error) {
2021
2331
  d.error("custom/render", `${node.type} failed to render: ${error instanceof Error ? error.message : String(error)}`, path, node.id);
2022
2332
  return void 0;
2023
2333
  }
2334
+ const at = `${path}(${node.type})`;
2335
+ for (const diagnostic of reported) d.list.push({ ...diagnostic, path: diagnostic.path === "root" || diagnostic.path.startsWith("root.") ? at + diagnostic.path.slice(4) : at });
2024
2336
  if (Array.isArray(raw2) || typeof raw2 !== "object" || raw2 === null) {
2025
2337
  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);
2026
2338
  return void 0;
2027
2339
  }
2028
- const { node: expanded, diagnostics } = checkFragment(raw2, { parent, responsive, idPrefix: node.id ?? null, taken, registry, path: `${path}(${node.type})` });
2340
+ const { node: expanded, diagnostics } = checkFragment(raw2, { parent, root: rootDefinition, idPrefix: node.id ?? null, taken, registry, path: `${path}(${node.type})` });
2029
2341
  for (const diagnostic of diagnostics) d.list.push(diagnostic);
2030
2342
  if (!expanded) return void 0;
2031
2343
  collectIds(expanded);
2032
2344
  if (node.style?.flex !== void 0) expanded.style = { ...expanded.style, flex: node.style.flex };
2033
2345
  if (node.hidden) expanded.hidden = { ...expanded.hidden, ...node.hidden };
2034
2346
  if (node.id && !expanded.id && !CUSTOM_TYPE.test(node.type)) expanded.id = node.id;
2035
- for (const [key, value] of Object.entries(node.mobile ?? {})) {
2036
- if (registry.get(expanded.type)?.style[key]?.mobile) expanded.mobile = { [key]: value, ...expanded.mobile };
2037
- else d.warning("mobile/not-shown", `${node.type}'s ${key} on phones isn't shown: what it renders (a ${expanded.type}) has no ${key} for phones`, `${path}.mobile.${key}`, node.id);
2347
+ for (const condition of CONDITIONS) {
2348
+ const rendered = registry.get(expanded.type);
2349
+ for (const [key, value] of Object.entries(node[condition.name] ?? {})) {
2350
+ const spec = rendered?.style[key];
2351
+ if (rendered && spec && condition.allows(spec, key, rendered)) expanded[condition.name] = { [key]: value, ...expanded[condition.name] };
2352
+ 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);
2353
+ }
2038
2354
  }
2039
2355
  const inner = registry.get(expanded.type);
2040
2356
  if (isCustom(inner)) return expandOne(expanded, inner, parent, `${path}(${node.type})`, depth + 1, self);
@@ -2487,11 +2803,11 @@ var TEXT_CSS = [
2487
2803
  ["textAlign", "text-align", (v) => v],
2488
2804
  ["textTransform", "text-transform", (v) => v]
2489
2805
  ];
2490
- var same = (a2, b) => a2 === b || JSON.stringify(a2) === JSON.stringify(b);
2806
+ var same2 = (a2, b) => a2 === b || JSON.stringify(a2) === JSON.stringify(b);
2491
2807
  function textDeclarations(text2, parent, always = []) {
2492
2808
  const out = [];
2493
2809
  for (const [key, property, write3] of TEXT_CSS) {
2494
- if (!parent || always.includes(key) || !same(text2[key], parent[key])) out.push([property, write3(text2[key])]);
2810
+ if (!parent || always.includes(key) || !same2(text2[key], parent[key])) out.push([property, write3(text2[key])]);
2495
2811
  }
2496
2812
  return out;
2497
2813
  }
@@ -2567,7 +2883,9 @@ function classesOf(name, value) {
2567
2883
  if (name === "class") return (Array.isArray(value) ? value : [value]).filter((v) => typeof v === "string" && v !== "");
2568
2884
  const text2 = styleText(value, true);
2569
2885
  if (!text2) return [];
2570
- return [name === "phone" ? rule("phone", `&{${text2}}`) : rule("all", `&:hover{${text2}}`, "hover")];
2886
+ if (name === "phone") return [rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}{${text2}}`)];
2887
+ const selector = name === "hoverWithin" ? HOVER.sheet.within ?? HOVER.sheet.selector : HOVER.sheet.selector;
2888
+ return [rule(HOVER.sheet.media, `${selector}{${text2}}`, HOVER.sheet.level)];
2571
2889
  }
2572
2890
  function attributes2(props) {
2573
2891
  const out = [];
@@ -2575,7 +2893,7 @@ function attributes2(props) {
2575
2893
  let slot = -1;
2576
2894
  for (const [name, value] of Object.entries(props)) {
2577
2895
  if (name === "children" || name === "style" || value === void 0 || value === null || value === false) continue;
2578
- if (name === "class" || name === "phone" || name === "hover") {
2896
+ if (name === "class" || name === "phone" || name === "hover" || name === "hoverWithin") {
2579
2897
  const found = classesOf(name, value);
2580
2898
  if (found.length && slot < 0) slot = out.push("") - 1;
2581
2899
  classes.push(...found);
@@ -2591,8 +2909,8 @@ function withId(element, id) {
2591
2909
  if (!id) return element;
2592
2910
  const tag = /^<[a-zA-Z0-9]+/.exec(element.markup)?.[0];
2593
2911
  if (!tag) throw new Error("[Elements] An id goes on an element");
2594
- const insert = (markup) => `${tag} id="${escapeAttr(id)}"${markup.slice(tag.length)}`;
2595
- return new Html2(insert(element.markup), { ...element.options, start: element.options.start && insert(element.options.start) });
2912
+ const insert2 = (markup) => `${tag} id="${escapeAttr(id)}"${markup.slice(tag.length)}`;
2913
+ return new Html2(insert2(element.markup), { ...element.options, start: element.options.start && insert2(element.options.start) });
2596
2914
  }
2597
2915
  function h(type, props, ...children) {
2598
2916
  const all = props ?? {};
@@ -2619,6 +2937,98 @@ function Merged({ children }) {
2619
2937
  }
2620
2938
  var open = (element) => new Html2(element.options.start ?? element.markup);
2621
2939
  var close = (tag) => new Html2(`</${tag}>`);
2940
+ var BLOCK_MAP_VERSION = 1;
2941
+ var MARK_PAIRS = 16;
2942
+ var BlockMarks = class {
2943
+ constructor(pair) {
2944
+ this.nodes = [];
2945
+ this.open = String.fromCharCode(64976 + pair * 2);
2946
+ this.close = String.fromCharCode(64977 + pair * 2);
2947
+ }
2948
+ };
2949
+ var current = null;
2950
+ function withBlockMarks(marks, write3) {
2951
+ const before = current;
2952
+ current = marks;
2953
+ try {
2954
+ return write3();
2955
+ } finally {
2956
+ current = before;
2957
+ }
2958
+ }
2959
+ function markBlock(html, node) {
2960
+ if (!current) return html;
2961
+ const mark = `${current.open}${current.nodes.length}${current.close}`;
2962
+ const marked = insert(html, mark, false) ?? insert(html, mark, true);
2963
+ if (!marked) return html;
2964
+ current.nodes.push(node);
2965
+ return marked;
2966
+ }
2967
+ var OUTLOOK_OPEN = "<!--[if mso]>";
2968
+ var OUTLOOK_CLOSE = "<![endif]-->";
2969
+ var TAG2 = /<[a-zA-Z][a-zA-Z0-9]*/y;
2970
+ function tagEnd(markup, outlook) {
2971
+ for (let i = markup.indexOf("<"); i >= 0; i = markup.indexOf("<", i + 1)) {
2972
+ if (markup.startsWith(OUTLOOK_OPEN, i)) {
2973
+ if (outlook) {
2974
+ i += OUTLOOK_OPEN.length - 1;
2975
+ continue;
2976
+ }
2977
+ const end = markup.indexOf(OUTLOOK_CLOSE, i);
2978
+ if (end < 0) return -1;
2979
+ i = end + OUTLOOK_CLOSE.length - 1;
2980
+ continue;
2981
+ }
2982
+ if (markup.startsWith("<!--", i)) {
2983
+ const end = markup.indexOf("-->", i + 4);
2984
+ if (end < 0) return -1;
2985
+ i = end + 2;
2986
+ continue;
2987
+ }
2988
+ TAG2.lastIndex = i;
2989
+ const tag = TAG2.exec(markup);
2990
+ if (tag) return i + tag[0].length;
2991
+ }
2992
+ return -1;
2993
+ }
2994
+ function insert(html, mark, outlook) {
2995
+ const { list, kind: kind2, start } = html.options;
2996
+ if (list) {
2997
+ for (let i = 0; i < list.length; i++) {
2998
+ const marked = insert(list[i], mark, outlook);
2999
+ if (marked) return new Html2(html.markup, { ...html.options, list: list.map((piece, j) => j === i ? marked : piece) });
3000
+ }
3001
+ return null;
3002
+ }
3003
+ if (kind2 === "outlook" && !outlook) return null;
3004
+ const at = tagEnd(html.markup, outlook);
3005
+ if (at < 0) return null;
3006
+ const put = (text2) => text2.slice(0, at) + mark + text2.slice(at);
3007
+ return new Html2(put(html.markup), { ...html.options, start: start !== void 0 && at <= start.length ? put(start) : start });
3008
+ }
3009
+ function readMarks(marks, written) {
3010
+ const found = [];
3011
+ const seen = /* @__PURE__ */ new Set();
3012
+ const pieces2 = [];
3013
+ let length3 = 0;
3014
+ let last = 0;
3015
+ for (const match of written.matchAll(new RegExp(`${marks.open}(\\d+)${marks.close}`, "g"))) {
3016
+ const index = Number(match[1]);
3017
+ const node = marks.nodes[index];
3018
+ if (!node || seen.has(index)) return null;
3019
+ seen.add(index);
3020
+ const before = written.slice(last, match.index);
3021
+ pieces2.push(before);
3022
+ length3 += before.length;
3023
+ last = match.index + match[0].length;
3024
+ if (!/<[a-zA-Z][a-zA-Z0-9]*$/.test(written.slice(Math.max(0, match.index - 32), match.index).replace(new RegExp(`${marks.open}\\d+${marks.close}`, "g"), ""))) return null;
3025
+ found.push({ node, at: length3 });
3026
+ }
3027
+ pieces2.push(written.slice(last));
3028
+ const text2 = pieces2.join("");
3029
+ if (text2.includes(marks.open) || text2.includes(marks.close)) return null;
3030
+ return { text: text2, found };
3031
+ }
2622
3032
  function utf8Bytes(text2) {
2623
3033
  let bytes = 0;
2624
3034
  for (let i = 0; i < text2.length; i++) {
@@ -2705,7 +3115,7 @@ function fontFaces2(root, { swap }) {
2705
3115
  return [...faces.values()];
2706
3116
  }
2707
3117
  var isNode2 = (child) => typeof child !== "string";
2708
- var shown = (node) => !(node.hidden?.mobile && node.hidden?.desktop);
3118
+ var shown = (node) => shownAnywhere(node);
2709
3119
  function link2(props) {
2710
3120
  const { href, target } = linkParts(props.href);
2711
3121
  return {
@@ -2940,7 +3350,7 @@ var Button2 = ({ node, style, mobile, fill, phoneText: phoneText2, text: text2 }
2940
3350
  {
2941
3351
  align: TEXT_ALIGN[node.text.textAlign],
2942
3352
  bgcolor: bgcolor(style.backgroundColor),
2943
- hover: { backgroundColor: node.hover?.backgroundColor },
3353
+ hoverWithin: { backgroundColor: node.hover?.backgroundColor },
2944
3354
  phone: { border: mobile.border, backgroundColor: mobile.backgroundColor },
2945
3355
  style: { backgroundColor: style.backgroundColor, border: style.border, borderRadius: style.borderRadius, msoPaddingAlt: insets2(style.padding) }
2946
3356
  },
@@ -2951,8 +3361,11 @@ var Button2 = ({ node, style, mobile, fill, phoneText: phoneText2, text: text2 }
2951
3361
  phone: [phoneText2, { padding: mobile.padding }],
2952
3362
  hover: { color: node.hover?.color },
2953
3363
  style: [
2954
- // A button with a width fills it.
2955
- { display: width.width !== void 0 ? "block" : "inline-block", padding: style.padding },
3364
+ // A button with a width fills it. Its label's words stay whole (the email's word-break: break-word
3365
+ // is for long links in text): a button narrower than its padding otherwise sized itself to one letter
3366
+ // and drew its label a letter a line, in Gmail and browsers; it grows to its longest word instead, as
3367
+ // classic Outlook's and Apple Mail's do (captures of Elements 0.3.0, e3-fresh/g-0054).
3368
+ { display: width.width !== void 0 ? "block" : "inline-block", padding: style.padding, wordBreak: "normal" },
2956
3369
  fullText(node),
2957
3370
  { textDecoration: style.textDecoration ?? "none", borderRadius: style.borderRadius }
2958
3371
  ]
@@ -3416,27 +3829,42 @@ function propsOf(node, place, options) {
3416
3829
  options
3417
3830
  };
3418
3831
  }
3832
+ var UNMAPPED = /* @__PURE__ */ new Set(["LineBreak", "MergeTag"]);
3419
3833
  function write(node, place, options) {
3420
3834
  if (place.inline) {
3421
3835
  if (!shown(node)) return raw("");
3422
3836
  const component22 = INLINES[node.type];
3423
3837
  if (!component22) throw new Error(`[Elements] The email exporter can't render a ${node.type} inside text`);
3424
- return component22(propsOf(node, place, options));
3838
+ const html = component22(propsOf(node, place, options));
3839
+ return UNMAPPED.has(node.type) ? html : markBlock(html, node);
3425
3840
  }
3426
3841
  const component2 = BLOCKS[node.type];
3427
3842
  if (!component2) throw new Error(`[Elements] The email exporter can't render a ${node.type} here (in a ${place.parent.type})`);
3428
- return component2(propsOf(node, place, options));
3843
+ return markBlock(component2(propsOf(node, place, options)), node);
3429
3844
  }
3430
3845
  var EMAIL_PHONE_MARGIN = 20;
3431
3846
  var GMAIL_CLIP_BYTES = 102400;
3432
3847
  var GMAIL_STYLE_BYTES = 16384;
3433
3848
  function renderEmail(resolved, options = {}) {
3434
3849
  const root = resolved.root;
3435
- const sheet = new Stylesheet(options.classPrefix);
3436
3850
  const d = new Diagnostics();
3437
3851
  const fonts = fontFaces2(root, { swap: false });
3438
3852
  const width = root.width ?? 600;
3439
- const email = withStylesheet(sheet, () => String(Email2(propsOf(root, { parent: root, fill: false, mobile: {}, inline: false, rtl: false }, options))));
3853
+ const write3 = (marks) => {
3854
+ const sheet2 = new Stylesheet(options.classPrefix);
3855
+ const markup = withStylesheet(sheet2, () => withBlockMarks(marks, () => String(markBlock(Email2(propsOf(root, { parent: root, fill: false, mobile: {}, inline: false, rtl: false }, options)), root))));
3856
+ return { sheet: sheet2, markup };
3857
+ };
3858
+ let written = options.blockMap ? null : write3(null);
3859
+ let marked = null;
3860
+ for (let pair = 0; !written; pair++) {
3861
+ if (pair === MARK_PAIRS) throw new Error("[Elements] blockMap can't mark this email: its text holds every pair of the noncharacters U+FDD0 to U+FDEF");
3862
+ const marks = new BlockMarks(pair);
3863
+ const attempt = write3(marks);
3864
+ marked = readMarks(marks, attempt.markup);
3865
+ if (marked) written = { sheet: attempt.sheet, markup: marked.text };
3866
+ }
3867
+ const { sheet, markup: email } = written;
3440
3868
  const background = root.style.backgroundColor;
3441
3869
  const lang = root.props.lang;
3442
3870
  const dir = root.props.dir;
@@ -3460,7 +3888,8 @@ function renderEmail(resolved, options = {}) {
3460
3888
  `<style>${stylesheet}</style>`
3461
3889
  ].join("");
3462
3890
  const preheader = preview ? `<div style="display:none;max-height:0;overflow:hidden;mso-hide:all" aria-hidden="true" data-skip-in-text="true">${escapeText(preview)}${"&#847;&zwnj;&nbsp;".repeat(30)}</div>` : "";
3463
- const body = preheader + `<div${attrs({ role: "article", "aria-roledescription": "email", "aria-label": title, lang, dir })}${styleAttr([["background-color", background], ["overflow-wrap", "break-word"], ["word-break", "break-word"]])}>${email}</div>`;
3891
+ const opening = `<div${attrs({ role: "article", "aria-roledescription": "email", "aria-label": title, lang, dir })}${styleAttr([["background-color", background], ["overflow-wrap", "break-word"], ["word-break", "break-word"]])}>`;
3892
+ const body = `${preheader}${opening}${email}</div>`;
3464
3893
  const bodyDeclarations = [
3465
3894
  ["margin", "0"],
3466
3895
  ["padding", "0"],
@@ -3468,20 +3897,38 @@ function renderEmail(resolved, options = {}) {
3468
3897
  ["background-color", background]
3469
3898
  ];
3470
3899
  const bodyStyle = css2(bodyDeclarations);
3471
- const html = `<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html${attrs({ lang, dir, xmlns: "http://www.w3.org/1999/xhtml", "xmlns:v": "urn:schemas-microsoft-com:vml", "xmlns:o": "urn:schemas-microsoft-com:office:office" })}><head>${head}</head><body${styleAttr(bodyDeclarations)}>${body}</body></html>`;
3900
+ const shell = `<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html${attrs({ lang, dir, xmlns: "http://www.w3.org/1999/xhtml", "xmlns:v": "urn:schemas-microsoft-com:vml", "xmlns:o": "urn:schemas-microsoft-com:office:office" })}><head>${head}</head><body${styleAttr(bodyDeclarations)}>`;
3901
+ const html = `${shell}${body}</body></html>`;
3472
3902
  const bytes = utf8Bytes(html);
3903
+ const gmails = ["gmail", "gmail-app", "gmail-app-other"];
3473
3904
  if (bytes > GMAIL_CLIP_BYTES) {
3474
- d.warning("email/size", `The email is ${Math.round(bytes / 1024)} KB; Gmail clips a message after ${GMAIL_CLIP_BYTES / 1024} KB ("[Message clipped]")`, "root");
3905
+ d.warning("email/size", `The email is ${Math.round(bytes / 1024)} KB; Gmail clips a message after ${GMAIL_CLIP_BYTES / 1024} KB ("[Message clipped]")`, "root", void 0, {
3906
+ clients: gmails,
3907
+ fix: "shorten the email, or move long sections to a page: what follows the clip is behind a link"
3908
+ });
3475
3909
  }
3476
3910
  const styleBytes = utf8Bytes(stylesheet);
3477
3911
  if (styleBytes > GMAIL_STYLE_BYTES) {
3478
- d.warning("email/style-size", `The stylesheet is ${Math.round(styleBytes / 1024)} KB; Gmail reads ${GMAIL_STYLE_BYTES / 1024} KB of a <style>`, "root");
3912
+ d.warning("email/style-size", `The stylesheet is ${Math.round(styleBytes / 1024)} KB; Gmail reads ${GMAIL_STYLE_BYTES / 1024} KB of a <style>`, "root", void 0, {
3913
+ clients: gmails,
3914
+ fix: "use fewer phone and hover styles: each one adds a rule"
3915
+ });
3479
3916
  }
3480
- return { html, head, body, bodyStyle, diagnostics: d.list };
3917
+ return { html, head, body, bodyStyle, diagnostics: d.list, ...marked ? { blocks: blockMap(resolved, marked.found, email, utf8Bytes(shell + preheader + opening)) } : {} };
3918
+ }
3919
+ function blockMap(resolved, found, email, start) {
3920
+ const templatePath = resolved.templatePath ?? ((path) => path);
3921
+ let offset = start;
3922
+ let from = 0;
3923
+ const entries = found.map(({ node, at }) => {
3924
+ offset += utf8Bytes(email.slice(from, at));
3925
+ from = at;
3926
+ return { path: templatePath(node.path), ...node.id ? { nodeId: node.id } : {}, offset };
3927
+ });
3928
+ return { version: BLOCK_MAP_VERSION, unit: "utf8", entries };
3481
3929
  }
3482
3930
  var isNode3 = (child) => typeof child !== "string";
3483
- var shown2 = (node) => !(node.hidden?.mobile && node.hidden?.desktop);
3484
- var same2 = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
3931
+ var same3 = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
3485
3932
  function support(root, target) {
3486
3933
  const d = new Diagnostics();
3487
3934
  const visit = (node) => {
@@ -3489,41 +3936,81 @@ function support(root, target) {
3489
3936
  const byProp = definition.defaultStyleBy ? definition.defaultStyleBy.values[String(node.props[definition.defaultStyleBy.prop])] : void 0;
3490
3937
  for (const [key, value] of Object.entries(node.style)) {
3491
3938
  const fallback2 = byProp?.[key] ?? definition.defaultStyle?.[key];
3492
- report(node, "style", key, value, fallback2, definition.style[key]?.support, target, d);
3939
+ if (same3(value, fallback2)) continue;
3940
+ reportDeclared(d, node, `${node.path}.style.${key}`, key, value, definition.style[key]?.support?.[target], target);
3493
3941
  }
3494
- for (const [key, value] of Object.entries(node.props)) report(node, "props", key, value, definition.props[key]?.default, definition.props[key]?.support, target, d);
3495
- for (const child of node.children) if (isNode3(child) && shown2(child)) visit(child);
3942
+ for (const [key, value] of Object.entries(node.props)) {
3943
+ if (same3(value, definition.props[key]?.default)) continue;
3944
+ reportDeclared(d, node, `${node.path}.props.${key}`, key, value, definition.props[key]?.support?.[target], target);
3945
+ }
3946
+ for (const condition of CONDITIONS) {
3947
+ const block = blockOf(node, condition);
3948
+ if (!block || condition.hiddenKey && node.hidden?.[condition.hiddenKey]) continue;
3949
+ const own = condition.support[target];
3950
+ reportDeclared(d, node, `${node.path}.${condition.name}`, condition.name, block, own, target);
3951
+ if (!own || own.level === "none") continue;
3952
+ for (const [key, value] of Object.entries(block)) {
3953
+ const rules = condition.styles?.[key]?.[target] ?? definition.style[key]?.support?.[target];
3954
+ reportDeclared(d, node, `${node.path}.${condition.name}.${key}`, key, value, rules, target, condition.outlookApplies);
3955
+ }
3956
+ }
3957
+ for (const child of node.children) if (isNode3(child) && shownAnywhere(child)) visit(child);
3496
3958
  };
3497
3959
  visit(root);
3498
3960
  return d.list;
3499
3961
  }
3500
- function report(node, where, key, value, fallback2, declared, target, d) {
3501
- const rules = declared?.[target];
3502
- if (!rules || same2(value, fallback2)) return;
3503
- for (const rule2 of [].concat(rules)) {
3504
- if (rule2.level === "full") continue;
3505
- if (rule2.values && !rule2.values.some((v) => same2(v, value))) continue;
3506
- if (rule2.when && !rule2.when(value, node)) continue;
3507
- const at = `${node.path}.${where}.${key}`;
3508
- const asked = typeof value === "string" ? `${key}: "${value}"` : key;
3509
- const output = target === "web" ? "a page" : target;
3510
- if (rule2.level === "none") d.warning(`${target}/unsupported`, `${asked} isn't possible in ${output}: ${rule2.shows}`, at, node.id);
3511
- else if (rule2.level === "partial") d.add(`${target}/degraded`, "info", `${asked} on this ${node.type} in ${output}: ${rule2.shows}`, at, node.id);
3512
- else d.add("email/outlook-degraded", "info", `${asked} on this ${node.type} in classic Outlook: ${rule2.shows}`, at, node.id);
3513
- }
3514
- }
3515
- function emailSupport(root) {
3962
+ function emailSupport(root, templatePath = (path) => path) {
3516
3963
  const d = new Diagnostics();
3517
3964
  d.list.push(...support(root, "email"));
3518
3965
  const webFonts = new Set((root.props.fonts ?? []).map((face) => face.family.toLowerCase()));
3966
+ const phone = { styled: false, phoneHidden: false, desktopHidden: false };
3519
3967
  const visit = (node) => {
3520
3968
  structure(node, d);
3521
3969
  fallback(node, webFonts, d);
3970
+ if (node.type === "Html") htmlBlock(node, d);
3971
+ if (node.type === "Image") imageFormat(node, "src", d);
3972
+ if (node.type === "Video" && node.props.thumbnail !== void 0) imageFormat(node, "thumbnail", d);
3973
+ if (node.mobile && Object.keys(node.mobile).length) phone.styled = true;
3974
+ if (node.hidden?.mobile && !builtIn(templatePath(node.path))) phone.phoneHidden = true;
3975
+ if (node.hidden?.desktop && !builtIn(templatePath(node.path))) phone.desktopHidden = true;
3522
3976
  for (const child of node.children) if (isNode2(child) && shown(child)) visit(child);
3523
3977
  };
3524
3978
  visit(root);
3979
+ phoneStyles(root, phone, d);
3525
3980
  return d.list;
3526
3981
  }
3982
+ function builtIn(place) {
3983
+ const made = [...place.matchAll(/\((\w+)\)/g)].pop()?.[1];
3984
+ return !!made && Object.prototype.hasOwnProperty.call(COMPONENTS, made);
3985
+ }
3986
+ var NO_PHONE_STYLES = ["gmail-app-other", "sfr", "laposte", "gmx", "web-de", "rainloop"];
3987
+ function phoneStyles(root, phone, d) {
3988
+ 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);
3989
+ if (!parts.length) return;
3990
+ 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, {
3991
+ clients: NO_PHONE_STYLES,
3992
+ fix: phone.desktopHidden ? "show what a reader needs on desktop too, and hide on phones only what they can do without" : void 0,
3993
+ see: caniemail("css-at-media")
3994
+ });
3995
+ }
3996
+ var IMAGE_FORMATS = {
3997
+ webp: { clients: ["outlook"], feature: "image-webp", name: "a WebP" },
3998
+ avif: { clients: ["outlook", "fastmail", "gmx", "web-de", "orange", "sfr", "mail-ru", "ionos"], feature: "image-avif", name: "an AVIF" },
3999
+ svg: { clients: ["mail-ru"], feature: "image-svg", name: "an SVG" }
4000
+ };
4001
+ 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];
4002
+ function imageFormat(node, key, d) {
4003
+ const src = assetParts(node.props[key]).src;
4004
+ const format = FORMAT_IN.map((pattern) => pattern.exec(src)?.[1]).find(Boolean)?.toLowerCase();
4005
+ const known = format && Object.prototype.hasOwnProperty.call(IMAGE_FORMATS, format) ? IMAGE_FORMATS[format] : void 0;
4006
+ if (!known) return;
4007
+ const what = key === "src" ? "This Image is" : "This Video's thumbnail is";
4008
+ d.warning("email/image-format", `${what} ${known.name} file, which these email clients don't show`, `${node.path}.props.${key}`, node.id, {
4009
+ clients: known.clients,
4010
+ fix: "use a PNG, JPEG or GIF file",
4011
+ see: caniemail(known.feature)
4012
+ });
4013
+ }
3527
4014
  function fallback(node, webFonts, d) {
3528
4015
  const stack = node.style.fontFamily;
3529
4016
  if (typeof stack !== "string" || !webFonts.size) return;
@@ -3532,9 +4019,10 @@ function fallback(node, webFonts, d) {
3532
4019
  if (!last || !webFonts.has(last.toLowerCase())) return;
3533
4020
  d.warning(
3534
4021
  "email/font-fallback",
3535
- `This font stack ends with the web font "${last}": where it doesn't load (Gmail, classic Outlook) the email client picks its own font. Add one it has: "${stack}, Arial, sans-serif"`,
4022
+ `This font stack ends with the web font "${last}": where it doesn't load, the email client picks its own font`,
3536
4023
  `${node.path}.style.fontFamily`,
3537
- node.id
4024
+ node.id,
4025
+ { clients: NO_WEB_FONTS, fix: `end it with a font the client has: "${stack}, Arial, sans-serif"`, see: caniemail("css-at-font-face") }
3538
4026
  );
3539
4027
  }
3540
4028
  function structure(node, d) {
@@ -3547,7 +4035,8 @@ function structure(node, d) {
3547
4035
  "info",
3548
4036
  `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`,
3549
4037
  path,
3550
- id
4038
+ id,
4039
+ { clients: ["outlook"], fix: "put less in it, or let it stack on phones (stackOnMobile)" }
3551
4040
  );
3552
4041
  }
3553
4042
  const lines2 = wrapsWithGap(node);
@@ -3583,15 +4072,95 @@ function structure(node, d) {
3583
4072
  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);
3584
4073
  }
3585
4074
  }
4075
+ var HTML_CSS = [
4076
+ { 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" },
4077
+ { 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" },
4078
+ {
4079
+ pattern: /(^|[\s;{"'])position\s*:\s*absolute\b/i,
4080
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "hey", "sfr", "laposte", "mail-ru", "gmx", "web-de"],
4081
+ removes: "position: absolute: the client removes it",
4082
+ fix: "lay it out without position (a Layer puts content over an image)",
4083
+ feature: "css-position"
4084
+ },
4085
+ {
4086
+ // Outlook.com removes every position (captures); the others not named keep relative.
4087
+ pattern: /(^|[\s;{"'])position\s*:\s*relative\b/i,
4088
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "mail-ru", "gmx", "web-de"],
4089
+ removes: "position: relative: the client removes it, and what it moves stays where it is",
4090
+ fix: "move it with padding instead",
4091
+ feature: "css-position",
4092
+ use: "relative"
4093
+ },
4094
+ {
4095
+ pattern: /var\(\s*--|(^|[\s;{"'])--[\w-]+\s*:/i,
4096
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "orange", "mail-ru", "fastmail", "gmx", "web-de"],
4097
+ removes: "CSS variables: the client removes var() and their declarations",
4098
+ fix: "write the values themselves",
4099
+ feature: "css-variables"
4100
+ },
4101
+ {
4102
+ pattern: /gradient\(/i,
4103
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail-app", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
4104
+ removes: "a gradient: the client removes it",
4105
+ fix: "give it a solid background color too, or use an image",
4106
+ feature: "css-linear-gradient"
4107
+ },
4108
+ { 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()" },
4109
+ { 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)" },
4110
+ {
4111
+ pattern: /<style[\s>]/i,
4112
+ clients: ["gmail", "gmail-app", "gmail-app-other", "protonmail", "gmx", "web-de", "sfr", "laposte", "rainloop"],
4113
+ removes: "a <style> in the email's body: the client removes it, and what it styles shows without it",
4114
+ fix: "write the styles inline, on the elements they're for",
4115
+ feature: "html-style"
4116
+ },
4117
+ { 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" },
4118
+ { 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" },
4119
+ { 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" },
4120
+ {
4121
+ pattern: /display\s*:\s*inline-flex\b/i,
4122
+ clients: ["outlook", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
4123
+ removes: "display: inline-flex: the client lays it out without flex",
4124
+ fix: "lay it out with a Row",
4125
+ feature: "css-display-flex",
4126
+ use: "inline"
4127
+ },
4128
+ { 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" },
4129
+ { 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" },
4130
+ {
4131
+ pattern: /object-fit\s*:/i,
4132
+ clients: ["outlook", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
4133
+ removes: "object-fit: the client stretches the image to its box",
4134
+ fix: "crop the image file to the size it's shown at",
4135
+ feature: "css-object-fit"
4136
+ },
4137
+ {
4138
+ pattern: /(^|[\s;{"'])aspect-ratio\s*:/i,
4139
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "samsung-email", "thunderbird", "fastmail", "gmx", "web-de", "orange"],
4140
+ removes: "aspect-ratio: the client removes it",
4141
+ fix: "give it a width and a height",
4142
+ feature: "css-aspect-ratio"
4143
+ }
4144
+ ];
4145
+ var OUTLOOK_ONLY2 = /<!--\[if\s+(?:(?:gte?|lte?)\s+)?mso[^\]]*\]>[\s\S]*?<!\[endif\]-->/gi;
4146
+ function htmlBlock(node, d) {
4147
+ if (typeof node.props.html !== "string") return;
4148
+ const markup = node.props.html.replace(OUTLOOK_ONLY2, "");
4149
+ for (const check of HTML_CSS) {
4150
+ if (check.pattern.test(markup)) {
4151
+ 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) });
4152
+ }
4153
+ }
4154
+ }
3586
4155
  var isNode4 = (child) => typeof child !== "string";
3587
4156
  var OUTPUT = { pdf: "a PDF", image: "an image" };
3588
4157
  function pagedSupport(root, output) {
3589
4158
  const d = new Diagnostics();
3590
4159
  const where = OUTPUT[output];
3591
4160
  const visit = (node) => {
3592
- if (node.hidden?.desktop && node.hidden?.mobile) return;
3593
- if (node.hover && Object.keys(node.hover).length) {
3594
- d.warning(`${output}/unsupported`, `A hover isn't possible in ${where}: nothing points at it. The ${node.type} shows its own colors`, `${node.path}.hover`, node.id);
4161
+ if (!shownAnywhere(node)) return;
4162
+ for (const condition of CONDITIONS) {
4163
+ if (condition.paged && blockOf(node, condition)) d.warning(`${output}/unsupported`, condition.paged(node, where), `${node.path}.${condition.name}`, node.id);
3595
4164
  }
3596
4165
  if (node.type === "Video") {
3597
4166
  const shows = output === "pdf" ? "its thumbnail (where its file is given) or its title, linked to it" : "its title";
@@ -3744,15 +4313,15 @@ function classesFor(node, parent) {
3744
4313
  const classes = [];
3745
4314
  if (node.mobile) {
3746
4315
  const { width, ...rest } = node.mobile;
3747
- if (Object.keys(rest).length) classes.push(rule("phone", `&{${css2(declarationsOf(rest), true)}}`));
4316
+ if (Object.keys(rest).length) classes.push(rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}{${css2(declarationsOf(rest), true)}}`));
3748
4317
  const line2 = node.type === "Divider" && node.style.padding !== void 0 ? ">hr" : "";
3749
- if (width !== void 0) classes.push(rule("phone", `&${line2}{${css2(declarationsOf({ width }), true)}}`));
4318
+ if (width !== void 0) classes.push(rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}${line2}{${css2(declarationsOf({ width }), true)}}`));
3750
4319
  const own = node.style.width === void 0 && (node.style.flex === "none" || node.style.flex === 0);
3751
4320
  const basis = own ? void 0 : length2(phoneWidthInRow(node, parent));
3752
4321
  if (basis !== void 0) classes.push(rule("phone", `&{flex:0 1 ${basis}!important}`));
3753
4322
  }
3754
4323
  classes.push(...hidingClasses(node.hidden));
3755
- if (node.hover) classes.push(rule("all", `&:hover{${css2(declarationsOf(node.hover), true)}}`, "hover"));
4324
+ if (node.hover) classes.push(rule(HOVER.sheet.media, `${HOVER.sheet.selector}{${css2(declarationsOf(node.hover), true)}}`, HOVER.sheet.level));
3756
4325
  if (node.type === "Row" && node.props.stackOnMobile !== false) {
3757
4326
  classes.push(
3758
4327
  rule("phone", "&{flex-direction:column!important;align-items:stretch!important}&>*{flex:0 0 auto!important;width:auto!important;max-width:100%!important;align-self:stretch!important}", "layout")
@@ -3960,17 +4529,17 @@ var Layer3 = ({ node, style, parent, placement, classes, children }) => /* @__PU
3960
4529
  },
3961
4530
  children()
3962
4531
  );
3963
- var current = { page: 1, total: 1 };
4532
+ var current2 = { page: 1, total: 1 };
3964
4533
  function numbering(numbers, write3) {
3965
- const before = current;
3966
- current = numbers;
4534
+ const before = current2;
4535
+ current2 = numbers;
3967
4536
  try {
3968
4537
  return write3();
3969
4538
  } finally {
3970
- current = before;
4539
+ current2 = before;
3971
4540
  }
3972
4541
  }
3973
- var PageNumber2 = ({ node }) => /* @__PURE__ */ h(Fragment, null, String(node.props.total ? current.total : current.page));
4542
+ var PageNumber2 = ({ node }) => /* @__PURE__ */ h(Fragment, null, String(node.props.total ? current2.total : current2.page));
3974
4543
  function documentCss(root) {
3975
4544
  const box2 = pageBox(root.props);
3976
4545
  return `@page{size:${box2.css};margin:0}body{-webkit-print-color-adjust:exact;print-color-adjust:exact}@media screen{body{background:#e5e7eb;padding:24px 0}.elements-page{box-shadow:0 1px 4px rgba(0,0,0,.2)}.elements-page+.elements-page{margin-top:24px!important}}`;
@@ -4144,8 +4713,23 @@ var Video3 = ({ node, style, placement, classes }) => /* @__PURE__ */ h(
4144
4713
  ))
4145
4714
  );
4146
4715
  var PAGE_PHONE_WIDTH = 480;
4147
- var PHONE = `@media (max-width: ${PAGE_PHONE_WIDTH}px)`;
4148
- var DESKTOP = `@media (min-width: ${PAGE_PHONE_WIDTH + 1}px)`;
4716
+ function pagePhoneWidth(root) {
4717
+ const mobile = root.props.breakpoints?.mobile;
4718
+ return typeof mobile === "number" ? mobile : PAGE_PHONE_WIDTH;
4719
+ }
4720
+ function pageBreakpoints(root) {
4721
+ const mobile = root.props.breakpoints?.mobile;
4722
+ const width = typeof root.props.width === "number" ? root.props.width : void 0;
4723
+ if (root.type !== "Page" || typeof mobile !== "number" || width === void 0 || mobile < width) return [];
4724
+ return [
4725
+ {
4726
+ code: "page/phone-width",
4727
+ severity: "warning",
4728
+ 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}`,
4729
+ path: "root.props.breakpoints"
4730
+ }
4731
+ ];
4732
+ }
4149
4733
  var COMPONENTS2 = {
4150
4734
  Section: Section3,
4151
4735
  Row: Row3,
@@ -4206,7 +4790,8 @@ function renderPage(resolved, options = {}) {
4206
4790
  const writer2 = document ? Document2 : root.type === "Canvas" ? Canvas2 : Page2;
4207
4791
  const body = withStylesheet(sheet, () => String(writer2(propsOf2(root, root, [], options))));
4208
4792
  const fixed = !root.definition.responsive;
4209
- const stylesheet = [...fonts, sheet.css(fixed ? null : PHONE, fixed ? null : DESKTOP), document ? documentCss(root) : ""].join("");
4793
+ const phoneWidth2 = pagePhoneWidth(root);
4794
+ const stylesheet = [...fonts, sheet.css(fixed ? null : `@media (max-width: ${phoneWidth2}px)`, fixed ? null : `@media (min-width: ${phoneWidth2 + 1}px)`), document ? documentCss(root) : ""].join("");
4210
4795
  const head = [
4211
4796
  '<meta charset="utf-8">',
4212
4797
  '<meta name="viewport" content="width=device-width, initial-scale=1">',
@@ -4226,7 +4811,7 @@ function renderText(resolved, options = {}) {
4226
4811
  const tag = options.mergeTag ?? ((name) => `{{${name}}}`);
4227
4812
  const inline = (node, upper = false) => node.children.map((child) => {
4228
4813
  if (typeof child === "string") return upper ? child.toUpperCase() : child;
4229
- if (child.hidden?.mobile && child.hidden?.desktop) return "";
4814
+ if (!shownAnywhere(child)) return "";
4230
4815
  switch (child.type) {
4231
4816
  case "LineBreak":
4232
4817
  return "\n";
@@ -4250,7 +4835,7 @@ function renderText(resolved, options = {}) {
4250
4835
  return alt ? `[${upper ? alt.toUpperCase() : alt}]` : "";
4251
4836
  };
4252
4837
  const blocks2 = (node) => node.children.flatMap((child) => {
4253
- if (typeof child === "string" || child.hidden?.mobile && child.hidden?.desktop) return [];
4838
+ if (typeof child === "string" || !shownAnywhere(child)) return [];
4254
4839
  switch (child.type) {
4255
4840
  case "Heading":
4256
4841
  return [tidy(inline(child, Number(child.props.level) <= 2))];
@@ -4288,8 +4873,8 @@ function renderText(resolved, options = {}) {
4288
4873
  return blocks2(child);
4289
4874
  }
4290
4875
  });
4291
- const shown32 = (node) => !(node.hidden?.mobile && node.hidden?.desktop);
4292
- const list = (node, indent) => node.children.filter((child) => typeof child !== "string" && shown32(child)).map((item, index) => {
4876
+ const shown22 = (node) => shownAnywhere(node);
4877
+ const list = (node, indent) => node.children.filter((child) => typeof child !== "string" && shown22(child)).map((item, index) => {
4293
4878
  const marker2 = node.props.type === "number" ? `${Number(node.props.start) + index}. ` : "- ";
4294
4879
  const lines2 = [];
4295
4880
  let run = [];
@@ -4305,13 +4890,13 @@ function renderText(resolved, options = {}) {
4305
4890
  }
4306
4891
  flush();
4307
4892
  if (!lines2.length) lines2.push(`${indent}${marker2}`.trimEnd());
4308
- if (shown32(child)) lines2.push(list(child, `${indent} `));
4893
+ if (shown22(child)) lines2.push(list(child, `${indent} `));
4309
4894
  }
4310
4895
  flush();
4311
4896
  return (lines2.length ? lines2 : [`${indent}${marker2}`.trimEnd()]).join("\n");
4312
4897
  }).join("\n");
4313
- const table = (node) => node.children.filter((row) => typeof row !== "string" && shown32(row)).map(
4314
- (row) => row.children.filter((cell) => typeof cell !== "string" && shown32(cell)).map((cell) => tidy(inline(cell))).join(" | ")
4898
+ const table = (node) => node.children.filter((row) => typeof row !== "string" && shown22(row)).map(
4899
+ (row) => row.children.filter((cell) => typeof cell !== "string" && shown22(cell)).map((cell) => tidy(inline(cell))).join(" | ")
4315
4900
  ).join("\n");
4316
4901
  return blocks2(resolved.root).filter((block) => block.length > 0).join("\n\n");
4317
4902
  }
@@ -4354,45 +4939,39 @@ var OUTPUTS = {
4354
4939
  Email: { target: "email", ready: true }
4355
4940
  };
4356
4941
  function prepareTemplate(input, options = {}) {
4942
+ checkEmailClients(options.emailClients);
4357
4943
  const registry = registryOf(options);
4358
4944
  const checked = checkDocument(input, registry);
4359
4945
  const diagnostics = [...checked.diagnostics];
4360
4946
  const failed = () => diagnostics.some((d) => d.severity === "error");
4361
4947
  if (!checked.document || failed()) return { resolved: null, target: null, diagnostics };
4362
4948
  const root = checked.document.root.type;
4949
+ const done = (resolved2, target) => {
4950
+ 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" });
4951
+ return { resolved: resolved2, target, diagnostics: root === "Email" ? forEmailClients(diagnostics, options.emailClients) : diagnostics };
4952
+ };
4363
4953
  const output = OUTPUTS[root];
4364
4954
  if (!output?.ready) {
4365
4955
  diagnostics.push({ code: "output/unavailable", severity: "error", message: output?.later ?? `A ${root} can't be rendered yet`, path: "root" });
4366
- return { resolved: null, target: null, diagnostics };
4956
+ return done(null, null);
4367
4957
  }
4368
- const expanded = expandDocument(checked.document, registry);
4958
+ const expanded = expandDocument(checked.document, registry, output.target);
4369
4959
  diagnostics.push(...expanded.diagnostics);
4370
- if (failed()) return { resolved: null, target: output.target, diagnostics };
4960
+ if (failed()) return done(null, output.target);
4371
4961
  diagnostics.push(...checkTargetExpanded(expanded.document, output.target, registry, expanded.paths));
4372
- if (failed()) return { resolved: null, target: output.target, diagnostics };
4962
+ if (failed()) return done(null, output.target);
4373
4963
  const resolved = resolveExpanded(expanded.document, registry, expanded.paths);
4374
4964
  diagnostics.push(...resolved.diagnostics);
4375
- const found = output.target === "email" ? emailSupport(resolved.root) : support(resolved.root, output.target);
4965
+ const found = output.target === "email" ? emailSupport(resolved.root, resolved.templatePath) : support(resolved.root, output.target);
4376
4966
  if (root === "Document" || root === "Canvas") found.push(...overflow(resolved.root));
4377
- if (root !== "Document") found.push(...pageNumbersOutside(resolved.root));
4967
+ found.push(...pageBreakpoints(resolved.root));
4378
4968
  diagnostics.push(...inTemplate(resolved, found));
4379
- return { resolved: failed() ? null : resolved, target: output.target, diagnostics };
4969
+ return done(failed() ? null : resolved, output.target);
4380
4970
  }
4381
4971
  function inTemplate(resolved, diagnostics) {
4382
4972
  const { templatePath } = resolved;
4383
4973
  return templatePath ? diagnostics.map((diagnostic) => ({ ...diagnostic, path: templatePath(diagnostic.path) })) : diagnostics;
4384
4974
  }
4385
- function pageNumbersOutside(root) {
4386
- const out = [];
4387
- const visit = (node) => {
4388
- if (node.type === "PageNumber") {
4389
- out.push({ code: "document/page-number", severity: "error", message: `A PageNumber is a page's number in a Document; this ${root.type} has no pages`, path: node.path, ...node.id ? { nodeId: node.id } : {} });
4390
- }
4391
- for (const child of node.children) if (typeof child !== "string") visit(child);
4392
- };
4393
- visit(root);
4394
- return out;
4395
- }
4396
4975
  function validateTemplate(input, options = {}) {
4397
4976
  const { diagnostics, resolved } = prepareTemplate(input, options);
4398
4977
  const root = resolved?.root;
@@ -4401,12 +4980,22 @@ function validateTemplate(input, options = {}) {
4401
4980
  return { valid: !all.some((d) => d.severity === "error"), diagnostics: all };
4402
4981
  }
4403
4982
  function renderTemplate(input, options = {}) {
4983
+ if (options.blockMap && options.pretty) throw new ElementsError([{ code: "render/block-map-pretty", severity: "error", message: "blockMap gives offsets in the compact HTML; leave pretty off", path: "root" }]);
4404
4984
  const { resolved, target, diagnostics } = prepareTemplate(input, options);
4405
4985
  if (!resolved) throw new ElementsError(diagnostics);
4406
4986
  const text2 = renderText(resolved, options);
4407
4987
  const written = target === "email" ? renderEmail(resolved, options) : { ...renderPage(resolved, options), diagnostics: [] };
4408
- const shown32 = (html) => options.pretty ? prettyHtml(html) : html;
4409
- return { html: shown32(written.html), head: shown32(written.head), body: shown32(written.body), bodyStyle: written.bodyStyle, text: text2, diagnostics: [...diagnostics, ...inTemplate(resolved, written.diagnostics)] };
4988
+ const shown22 = (html) => options.pretty ? prettyHtml(html) : html;
4989
+ const blocks2 = "blocks" in written ? written.blocks : void 0;
4990
+ return {
4991
+ html: shown22(written.html),
4992
+ head: shown22(written.head),
4993
+ body: shown22(written.body),
4994
+ bodyStyle: written.bodyStyle,
4995
+ text: text2,
4996
+ diagnostics: [...diagnostics, ...inTemplate(resolved, forEmailClients(written.diagnostics, options.emailClients))],
4997
+ ...blocks2 ? { blocks: blocks2 } : {}
4998
+ };
4410
4999
  }
4411
5000
 
4412
5001
  // src/utils/collect.ts
@@ -4437,7 +5026,7 @@ function isAsyncComponent(type) {
4437
5026
  }
4438
5027
  var SHARED2 = Symbol.for("@hyper-elements/react/render-builds");
4439
5028
  var using = globalThis[SHARED2] ?? (globalThis[SHARED2] = []);
4440
- var same3 = (a2, b) => a2.renderToString === b.renderToString;
5029
+ var same4 = (a2, b) => a2.renderToString === b.renderToString;
4441
5030
  function dispatcher() {
4442
5031
  const internals = React2__default.default;
4443
5032
  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;
@@ -4484,7 +5073,7 @@ function secondBuild(outer) {
4484
5073
  for (const requirer of requirers) {
4485
5074
  try {
4486
5075
  const build = requirer()?.(id);
4487
- if (build?.renderToStaticMarkup && !same3(build, defaultBuild__namespace) && ![null, outer].includes(dispatcherOf(build))) {
5076
+ if (build?.renderToStaticMarkup && !same4(build, defaultBuild__namespace) && ![null, outer].includes(dispatcherOf(build))) {
4488
5077
  second = build;
4489
5078
  break search;
4490
5079
  }
@@ -4498,11 +5087,11 @@ function secondBuild(outer) {
4498
5087
  var buildOf = (outer) => outer === null ? void 0 : [defaultBuild__namespace, second].find((b) => !!b && dispatcherOf(b) === outer);
4499
5088
  function withBuild(render2) {
4500
5089
  const outer = dispatcher();
4501
- let build = current2();
5090
+ let build = current3();
4502
5091
  const theirs = using.length === 0 || collecting() !== void 0 ? buildOf(outer) : void 0;
4503
5092
  const busy = [];
4504
5093
  if (theirs) {
4505
- const free = (b) => !same3(b, theirs) && !using.some((u) => same3(u, b));
5094
+ const free = (b) => !same4(b, theirs) && !using.some((u) => same4(u, b));
4506
5095
  const other = free(defaultBuild__namespace) ? defaultBuild__namespace : secondBuild(outer);
4507
5096
  if (other && free(other)) {
4508
5097
  busy.push(theirs);
@@ -4527,10 +5116,10 @@ function rendersHere(build) {
4527
5116
  return false;
4528
5117
  }
4529
5118
  }
4530
- function current2() {
5119
+ function current3() {
4531
5120
  return using[using.length - 1] ?? defaultBuild__namespace;
4532
5121
  }
4533
- var renderToString = (element, options) => current2().renderToString(element, options);
5122
+ var renderToString = (element, options) => current3().renderToString(element, options);
4534
5123
 
4535
5124
  // src/utils/render-scope.ts
4536
5125
  function withRenderScope(render2) {
@@ -4584,7 +5173,7 @@ function withoutAsync(children, collection) {
4584
5173
  }
4585
5174
  var ENTITIES2 = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#x27;": "'" };
4586
5175
  var unescape = (text2) => text2.replace(/&amp;|&lt;|&gt;|&quot;|&#x27;/g, (entity) => ENTITIES2[entity]);
4587
- var NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "style", "mobile", "hover", "hidden"]);
5176
+ var NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "style", "hidden"]);
4588
5177
  function toJsonNode({ type, props }, diagnostics) {
4589
5178
  const node = { type, children: [] };
4590
5179
  const semantic = {};
@@ -4709,10 +5298,10 @@ function collectTemplate(element) {
4709
5298
  path: "jsx"
4710
5299
  });
4711
5300
  }
4712
- return { document: null, diagnostics, components };
5301
+ return { document: null, diagnostics, components, collected: null };
4713
5302
  }
4714
5303
  const result = checkDocument({ root: top[0] }, new Registry(components));
4715
- return { document: result.document, diagnostics: [...diagnostics, ...result.diagnostics], components };
5304
+ return { document: result.document, diagnostics: [...diagnostics, ...result.diagnostics], components, collected: top[0] };
4716
5305
  }
4717
5306
 
4718
5307
  // src/primitives/components.tsx
@@ -4802,7 +5391,8 @@ function component(spec) {
4802
5391
  expand(props, context) {
4803
5392
  const { nodes, components, diagnostics } = collectNodes(spec.render(props));
4804
5393
  for (const used of components) context.register(used);
4805
- if (diagnostics.length) throw new Error(formatDiagnostics(diagnostics));
5394
+ if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) throw new Error(formatDiagnostics(diagnostics));
5395
+ for (const diagnostic of diagnostics) context.report(diagnostic);
4806
5396
  return nodes.length === 1 ? nodes[0] : nodes;
4807
5397
  }
4808
5398
  });
@@ -4821,12 +5411,12 @@ function component(spec) {
4821
5411
  // src/primitives/outline.ts
4822
5412
  var LONGEST = 80;
4823
5413
  var clip = (text2) => text2.length > LONGEST ? `${text2.slice(0, LONGEST - 1)}\u2026` : text2;
4824
- var shown3 = (node) => !(node.hidden?.mobile && node.hidden?.desktop);
4825
- var blocks = (node) => node.children.filter((child) => typeof child !== "string" && shown3(child));
5414
+ var shown2 = (node) => shownAnywhere(node);
5415
+ var blocks = (node) => node.children.filter((child) => typeof child !== "string" && shown2(child));
4826
5416
  function words(node) {
4827
5417
  return node.children.map((child) => {
4828
5418
  if (typeof child === "string") return child;
4829
- if (!shown3(child) || child.type === "List") return "";
5419
+ if (!shown2(child) || child.type === "List") return "";
4830
5420
  if (child.type === "MergeTag") return `{{${child.props.name}}}`;
4831
5421
  if (child.type === "LineBreak") return " / ";
4832
5422
  if (child.type === "Image") return `[${child.props.alt ?? ""}]`;
@@ -4877,14 +5467,16 @@ function template(element, call) {
4877
5467
  return collectTemplate(element);
4878
5468
  }
4879
5469
  async function render(element, options = {}) {
5470
+ checkEmailClients(options.emailClients);
4880
5471
  const { document, diagnostics, components } = template(element, "render");
4881
5472
  if (!document || diagnostics.some((d) => d.severity === "error")) throw new ElementsError(diagnostics);
4882
5473
  const rendered = renderTemplate(document, { ...options, components });
4883
5474
  return { ...rendered, diagnostics: [...diagnostics, ...rendered.diagnostics] };
4884
5475
  }
4885
- function validate(element) {
5476
+ function validate(element, options = {}) {
5477
+ checkEmailClients(options.emailClients);
4886
5478
  const { document, diagnostics, components } = template(element, "validate");
4887
- const all = [...diagnostics, ...document && !diagnostics.some((d) => d.severity === "error") ? validateTemplate(document, { components }).diagnostics : []];
5479
+ const all = [...diagnostics, ...document && !diagnostics.some((d) => d.severity === "error") ? validateTemplate(document, { components, emailClients: options.emailClients }).diagnostics : []];
4888
5480
  return { valid: !all.some((d) => d.severity === "error"), diagnostics: all };
4889
5481
  }
4890
5482
  function outline(element) {
@@ -4895,21 +5487,25 @@ function outline(element) {
4895
5487
  if (!root) throw new ElementsError([...diagnostics, ...prepared.diagnostics]);
4896
5488
  return outlineOf(root);
4897
5489
  }
4898
- function prepare(element) {
5490
+ function prepare(element, options = {}) {
5491
+ checkEmailClients(options.emailClients);
4899
5492
  const { document, diagnostics, components } = template(element, "prepare");
4900
5493
  if (!document || diagnostics.some((d) => d.severity === "error")) return { root: null, diagnostics, layout: LAYOUT_VERSION };
4901
- const prepared = prepareTemplate(document, { components });
5494
+ const prepared = prepareTemplate(document, { components, emailClients: options.emailClients });
4902
5495
  const root = prepared.resolved?.root ?? null;
4903
5496
  const paged = root?.type === "Document" ? pagedSupport(root, "pdf") : root?.type === "Canvas" ? pagedSupport(root, "image") : [];
4904
5497
  return { root, diagnostics: [...diagnostics, ...prepared.diagnostics, ...paged], layout: LAYOUT_VERSION };
4905
5498
  }
4906
5499
 
5500
+ exports.BLOCK_MAP_VERSION = BLOCK_MAP_VERSION;
4907
5501
  exports.Button = Button4;
4908
5502
  exports.Canvas = Canvas3;
4909
5503
  exports.Circle = Circle2;
4910
5504
  exports.Column = Column4;
5505
+ exports.DEFAULT_EMAIL_CLIENTS = DEFAULT_EMAIL_CLIENTS;
4911
5506
  exports.Divider = Divider4;
4912
5507
  exports.Document = Document3;
5508
+ exports.EMAIL_CLIENTS = EMAIL_CLIENTS;
4913
5509
  exports.ElementsError = ElementsError;
4914
5510
  exports.Ellipse = Ellipse2;
4915
5511
  exports.Email = Email3;