@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.
package/dist/builder.cjs CHANGED
@@ -84,21 +84,26 @@ var Diagnostics = class {
84
84
  constructor() {
85
85
  this.list = [];
86
86
  }
87
- add(code, severity, message, path, nodeId) {
88
- this.list.push(nodeId === void 0 ? { code, severity, message, path } : { code, severity, message, path, nodeId });
87
+ add(code, severity, message, path, nodeId, extra = {}) {
88
+ const d = { code, severity, message, path };
89
+ if (nodeId !== void 0) d.nodeId = nodeId;
90
+ if (extra.clients) d.clients = [...extra.clients];
91
+ if (extra.fix) d.fix = extra.fix;
92
+ if (extra.see) d.see = extra.see;
93
+ this.list.push(d);
89
94
  }
90
95
  error(code, message, path, nodeId) {
91
96
  this.add(code, "error", message, path, nodeId);
92
97
  }
93
- warning(code, message, path, nodeId) {
94
- this.add(code, "warning", message, path, nodeId);
98
+ warning(code, message, path, nodeId, extra) {
99
+ this.add(code, "warning", message, path, nodeId, extra);
95
100
  }
96
101
  get hasErrors() {
97
102
  return this.list.some((d) => d.severity === "error");
98
103
  }
99
104
  };
100
105
  function formatDiagnostics(diagnostics) {
101
- return diagnostics.map((d) => `${d.severity} ${d.code} at ${d.path}: ${d.message}`).join("\n");
106
+ 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");
102
107
  }
103
108
  var ElementsError = class extends Error {
104
109
  constructor(diagnostics) {
@@ -109,6 +114,64 @@ ${formatDiagnostics(errors)}`);
109
114
  this.diagnostics = diagnostics;
110
115
  }
111
116
  };
117
+ var captured = (against, wordsWithin4px, lineBreaks) => ({ elements: "0.3.0", against, captured: "2026-11-08", wordsWithin4px, lineBreaks });
118
+ var tables = (name) => ({ name, widths: {}, dark: false, approximate: true, measured: null });
119
+ var EMAIL_CLIENTS = {
120
+ 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) },
121
+ "outlook-com": { name: "Outlook.com and new Outlook", widths: { desktop: 820 }, dark: false, approximate: false, measured: captured("Outlook on the web", 1, 1) },
122
+ "outlook-mac": tables("Outlook for Mac"),
123
+ "outlook-app": tables("Outlook app (iOS and Android)"),
124
+ gmail: { name: "Gmail", widths: { desktop: 820 }, dark: false, approximate: false, measured: captured("Gmail on the web", 1, 1) },
125
+ "gmail-app": { name: "Gmail app", widths: { phone: 375 }, dark: false, approximate: true, measured: null },
126
+ "gmail-app-other": { name: "Gmail app, another account", widths: { phone: 375 }, dark: false, approximate: true, measured: null },
127
+ "apple-mail": { name: "Apple Mail", widths: { desktop: 820 }, dark: true, approximate: false, measured: captured("Mail 16 on macOS 26", 0.999, 0.999) },
128
+ yahoo: tables("Yahoo Mail"),
129
+ aol: tables("AOL Mail"),
130
+ "samsung-email": tables("Samsung Email"),
131
+ thunderbird: tables("Thunderbird"),
132
+ protonmail: tables("Proton Mail"),
133
+ fastmail: tables("Fastmail"),
134
+ hey: tables("HEY"),
135
+ gmx: tables("GMX"),
136
+ "web-de": tables("WEB.DE"),
137
+ orange: tables("Orange"),
138
+ sfr: tables("SFR"),
139
+ laposte: tables("La Poste"),
140
+ "free-fr": tables("Free"),
141
+ "t-online-de": tables("T-Online"),
142
+ "mail-ru": tables("Mail.ru"),
143
+ ionos: tables("IONOS (1&1)"),
144
+ "wp-pl": tables("WP Poczta"),
145
+ rainloop: tables("RainLoop")
146
+ };
147
+ var DEFAULT_EMAIL_CLIENTS = ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "apple-mail", "yahoo", "aol", "samsung-email"];
148
+ var caniemail = (feature) => `https://www.caniemail.com/features/${feature}/`;
149
+ function checkEmailClients(clients) {
150
+ if (clients === void 0) return;
151
+ const unknown = Array.isArray(clients) ? clients.filter((c) => !Object.prototype.hasOwnProperty.call(EMAIL_CLIENTS, c)) : [clients];
152
+ 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(", ")}`);
153
+ }
154
+ function forEmailClients(diagnostics, selection = DEFAULT_EMAIL_CLIENTS) {
155
+ const out = [];
156
+ const seen = /* @__PURE__ */ new Map();
157
+ for (const d of diagnostics) {
158
+ if (!d.clients) {
159
+ out.push(d);
160
+ continue;
161
+ }
162
+ const clients = d.clients.filter((c) => selection.includes(c));
163
+ if (!clients.length) continue;
164
+ const key = JSON.stringify([d.code, d.path, d.severity, d.message]);
165
+ const same42 = seen.get(key);
166
+ if (same42) same42.clients = [.../* @__PURE__ */ new Set([...same42.clients, ...clients])];
167
+ else {
168
+ const kept = { ...d, clients };
169
+ seen.set(key, kept);
170
+ out.push(kept);
171
+ }
172
+ }
173
+ return out;
174
+ }
112
175
  var ok = (value) => ({ ok: true, value });
113
176
  var fail = (message) => ({ ok: false, message });
114
177
  var show = (input) => {
@@ -694,6 +757,26 @@ function attributes() {
694
757
  jsonSchema: () => ({ type: "object", additionalProperties: { type: "string" } })
695
758
  });
696
759
  }
760
+ var MIN_PHONE_WIDTH = 240;
761
+ function breakpoints() {
762
+ const accepts = `{ mobile }: the phone width in px (from ${MIN_PHONE_WIDTH})`;
763
+ return kind({
764
+ name: "breakpoints",
765
+ editor: "number",
766
+ accepts,
767
+ parse(input) {
768
+ if (!isObject(input)) return fail(`expected ${accepts}, not ${show(input)}`);
769
+ const unknown = Object.keys(input).find((key) => key !== "mobile");
770
+ if (unknown !== void 0) return fail(`${show(unknown)} isn't a breakpoint a Page has: it has mobile`);
771
+ const { mobile } = input;
772
+ if (mobile === void 0) return ok({});
773
+ 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)}`);
774
+ return ok({ mobile });
775
+ },
776
+ empty: (value) => value.mobile === void 0,
777
+ jsonSchema: () => ({ type: "object", properties: { mobile: { type: "integer", minimum: MIN_PHONE_WIDTH } }, additionalProperties: false })
778
+ });
779
+ }
697
780
  function linkParts(value) {
698
781
  return typeof value === "string" ? { href: value } : value;
699
782
  }
@@ -706,6 +789,54 @@ function defineComponent(definition) {
706
789
  function prop(kind2, options) {
707
790
  return { kind: kind2, ...options };
708
791
  }
792
+ var MOBILE = {
793
+ name: "mobile",
794
+ on: "on phones",
795
+ for: "for phones",
796
+ roots: (root) => !!root?.responsive,
797
+ notAllowed: { code: "mobile/not-supported", message: "Phone styles are for an Email or a Page, which have a phone width" },
798
+ sheet: { media: "phone", selector: "&" },
799
+ allows: (spec) => !!spec.mobile,
800
+ support: { web: { level: "full", source: "writer" }, email: { level: "full", source: "writer" } },
801
+ styles: {
802
+ // The email's phone text styles are rules that override a size; "normal" isn't one.
803
+ lineHeight: { email: { level: "none", values: ["normal"], shows: "the line height it has on desktop", source: "writer" } }
804
+ },
805
+ outlookApplies: false,
806
+ hiddenKey: "mobile"
807
+ };
808
+ var HOVER = {
809
+ name: "hover",
810
+ on: "on hover",
811
+ for: "for hover",
812
+ // Keyboard focus gets the same styles as the pointer: a link or button reached by Tab shows its hover colors.
813
+ // A box around the link (an email Button's cell, its background) takes them while the link in it has focus.
814
+ sheet: { media: "all", selector: "&:hover,&:focus-visible", within: "&:hover,&:focus-within", level: "hover" },
815
+ allows: (_, key, definition) => !!definition.hover?.includes(key),
816
+ support: {
817
+ web: { level: "full", source: "css" },
818
+ // On type selectors only in Outlook.com and Outlook for Mac (Elements' are classes); the phone apps' results don't
819
+ // matter, as nothing hovers there.
820
+ email: {
821
+ level: "partial",
822
+ shows: "no change (where it shows, keyboard focus gets it too)",
823
+ source: "caniemail",
824
+ feature: "css-pseudo-class-hover",
825
+ clients: ["outlook", "outlook-com", "outlook-mac", "gmx", "web-de", "sfr", "laposte", "mail-ru"]
826
+ }
827
+ },
828
+ paged: (node, where) => `A hover isn't possible in ${where}: nothing points at it. The ${node.type} shows its own colors`,
829
+ outlookApplies: false
830
+ };
831
+ var CONDITIONS = [MOBILE, HOVER];
832
+ var CONDITION_NAMES = new Set(CONDITIONS.map((condition) => condition.name));
833
+ function blockOf(node, condition) {
834
+ const block2 = node[condition.name];
835
+ return block2 && Object.keys(block2).length ? block2 : void 0;
836
+ }
837
+ function shownAnywhere(node) {
838
+ return !(node.hidden?.mobile && node.hidden?.desktop);
839
+ }
709
840
  var writer = { level: "full", source: "writer" };
710
841
  var css = { level: "full", source: "css" };
711
842
  var textStyle = {
@@ -714,7 +845,12 @@ var textStyle = {
714
845
  fontFamily: { kind: fontFamily(), label: "Font", inherited: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
715
846
  // Word rounds sizes to half points (13px is 10pt): a fraction of a px.
716
847
  fontSize: { kind: length(["px"], { min: 1 }), label: "Font size", inherited: true, mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
717
- fontWeight: { kind: fontWeight(), label: "Font weight", inherited: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
848
+ fontWeight: {
849
+ kind: fontWeight(),
850
+ label: "Font weight",
851
+ inherited: true,
852
+ 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 }
853
+ },
718
854
  fontStyle: { kind: enumOf(["normal", "italic"]), label: "Font style", inherited: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
719
855
  // Written in px with mso-line-height-rule: exactly, which Word keeps (it drops unitless ones).
720
856
  lineHeight: { kind: lineHeight(), label: "Line height", inherited: true, mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
@@ -726,8 +862,21 @@ var textStyle = {
726
862
  support: { email: { level: "outlook", shows: "normal spacing: Word keeps letter spacing but doesn't draw it", source: "outlook-captures" }, web: css }
727
863
  },
728
864
  textAlign: { kind: enumOf(["left", "center", "right", "justify"]), label: "Alignment", inherited: true, mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
729
- textTransform: { kind: enumOf(["none", "uppercase", "lowercase", "capitalize"]), label: "Letter case", inherited: true, support: { email: { level: "full", source: "assumed" }, web: css } },
730
- textDecoration: { kind: enumOf(["none", "underline", "line-through"]), label: "Decoration", support: { email: { level: "full", source: "caniemail" }, web: css } }
865
+ textTransform: {
866
+ kind: enumOf(["none", "uppercase", "lowercase", "capitalize"]),
867
+ label: "Letter case",
868
+ inherited: true,
869
+ support: { email: { level: "outlook", values: ["lowercase"], shows: "the text as written: Word doesn't lowercase it", source: "caniemail", feature: "css-text-transform" }, web: css }
870
+ },
871
+ textDecoration: {
872
+ kind: enumOf(["none", "underline", "line-through"]),
873
+ label: "Decoration",
874
+ support: {
875
+ // Removed, so "none" leaves a link the client's own underline: said only where a line is asked for.
876
+ 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"] },
877
+ web: css
878
+ }
879
+ }
731
880
  };
732
881
  var inheritedTextStyle = (({ textDecoration: _, ...rest }) => rest)(textStyle);
733
882
  var boxStyle = {
@@ -740,7 +889,17 @@ var boxStyle = {
740
889
  padding: { kind: insets(), label: "Padding", mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
741
890
  backgroundColor: { kind: color(), label: "Background color", support: { email: writer, web: css } },
742
891
  border: { kind: border(), label: "Border", mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
743
- borderRadius: { kind: radius(), label: "Corner radius", support: { email: { level: "outlook", shows: "square corners", source: "caniemail" }, web: css } },
892
+ borderRadius: {
893
+ kind: radius(),
894
+ label: "Corner radius",
895
+ support: {
896
+ email: [
897
+ { level: "outlook", shows: "square corners", source: "caniemail", feature: "css-border-radius" },
898
+ { level: "partial", shows: "square corners", source: "caniemail", feature: "css-border-radius", clients: ["gmx", "web-de", "orange"] }
899
+ ],
900
+ web: css
901
+ }
902
+ },
744
903
  overflow: {
745
904
  kind: enumOf(["visible", "hidden"]),
746
905
  label: "Overflow",
@@ -948,6 +1107,15 @@ var Grid = defineComponent({
948
1107
  },
949
1108
  children: { categories: ["layout", "block"] },
950
1109
  sizing: "stretch",
1110
+ // Stacked on phones, every two of its children are its phone gap apart: there are no rows for a row gap.
1111
+ conditionSupport: {
1112
+ mobile: {
1113
+ rowGap: {
1114
+ web: { level: "none", when: (_, node) => node.props.stackOnMobile !== false, shows: "its phone gap between every two children", source: "writer" },
1115
+ email: { level: "none", when: (_, node) => node.props.stackOnMobile !== false, shows: "its phone gap between every two children", source: "writer" }
1116
+ }
1117
+ }
1118
+ },
951
1119
  targets: { email: {}, web: {} },
952
1120
  editor: {
953
1121
  groups: [
@@ -960,7 +1128,7 @@ var Grid = defineComponent({
960
1128
  var Layer = defineComponent({
961
1129
  type: "Layer",
962
1130
  label: "Layer",
963
- 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.",
1131
+ 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.",
964
1132
  categories: ["layout", "block"],
965
1133
  props: {},
966
1134
  style: {
@@ -992,13 +1160,27 @@ var Layer = defineComponent({
992
1160
  }
993
1161
  });
994
1162
  var WRITTEN3 = { email: { level: "full", source: "writer" }, web: { level: "full", source: "writer" } };
1163
+ 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"];
995
1164
  var ROOT_PROPS = {
996
1165
  title: prop(text(), { support: WRITTEN3, label: "Title", description: "The document's title (shown in a browser tab, and by some email clients)." }),
997
- lang: prop(text({ maxLength: 35 }), { support: WRITTEN3, label: "Language", description: 'Its language, as a BCP 47 tag ("en", "ar").' }),
998
- dir: prop(enumOf(["ltr", "rtl", "auto"]), { support: WRITTEN3, label: "Text direction" }),
1166
+ lang: prop(text({ maxLength: 35 }), {
1167
+ support: {
1168
+ ...WRITTEN3,
1169
+ 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"] }
1170
+ },
1171
+ label: "Language",
1172
+ description: 'Its language, as a BCP 47 tag ("en", "ar").'
1173
+ }),
1174
+ dir: prop(enumOf(["ltr", "rtl", "auto"]), {
1175
+ support: {
1176
+ ...WRITTEN3,
1177
+ email: { level: "partial", values: ["rtl", "auto"], shows: "left to right: the client removes the direction", source: "caniemail", feature: "html-dir", clients: ["gmx", "web-de"] }
1178
+ },
1179
+ label: "Text direction"
1180
+ }),
999
1181
  fonts: prop(fontFaces(), {
1000
1182
  support: {
1001
- 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" },
1183
+ 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 },
1002
1184
  web: { level: "full", source: "writer" }
1003
1185
  },
1004
1186
  label: "Web fonts",
@@ -1138,7 +1320,10 @@ var PageNumber = defineComponent({
1138
1320
  style: {},
1139
1321
  children: {},
1140
1322
  sizing: "none",
1141
- targets: { web: {} }
1323
+ targets: { web: {} },
1324
+ // A Page's or a Canvas's text has no page to number.
1325
+ roots: ["Document"],
1326
+ notAllowed: { code: "document/page-number", message: (root) => `A PageNumber is a page's number in a Document; this ${root} has no pages` }
1142
1327
  });
1143
1328
  function documentPages(root) {
1144
1329
  const pages = [];
@@ -1274,6 +1459,11 @@ var Page = defineComponent({
1274
1459
  props: {
1275
1460
  width: prop(length(["px"], { min: 200 }), { support: WRITTEN6, label: "Content width", description: "The widest the content gets.", default: 1200 }),
1276
1461
  description: prop(text(), { support: WRITTEN6, label: "Description", description: "The page's meta description." }),
1462
+ breakpoints: prop(breakpoints(), {
1463
+ support: { web: { level: "full", source: "writer" } },
1464
+ label: "Breakpoints",
1465
+ description: "Where its phone styles start: mobile, the widest window they apply to, in px (480 where it's not set)."
1466
+ }),
1277
1467
  ...ROOT_PROPS
1278
1468
  },
1279
1469
  style: ROOT_STYLE,
@@ -1281,6 +1471,12 @@ var Page = defineComponent({
1281
1471
  children: { categories: ["section", "layout", "block"] },
1282
1472
  sizing: "none",
1283
1473
  responsive: true,
1474
+ // The page writer writes the root's phone gap, not its phone text styles.
1475
+ conditionSupport: {
1476
+ mobile: Object.fromEntries(
1477
+ ["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" } }])
1478
+ )
1479
+ },
1284
1480
  targets: { web: {} },
1285
1481
  editor: {
1286
1482
  groups: [
@@ -1524,6 +1720,7 @@ var Image = defineComponent({
1524
1720
  },
1525
1721
  children: {},
1526
1722
  sizing: "shrink",
1723
+ conditionSupport: { mobile: { border: { email: { level: "none", shows: "its desktop border", source: "writer" } } } },
1527
1724
  targets: { email: {}, web: {} },
1528
1725
  editor: { groups: [{ title: "Image", paths: ["props.src", "props.alt", "style.width"] }, { title: "Box", paths: ["style.border", "style.borderRadius"] }] }
1529
1726
  });
@@ -1654,6 +1851,7 @@ var TableCell = defineComponent({
1654
1851
  },
1655
1852
  children: { text: true, categories: ["inline"], types: ["Image"] },
1656
1853
  sizing: "none",
1854
+ conditionSupport: { mobile: { border: { email: { level: "none", shows: "its desktop border", source: "writer" } } } },
1657
1855
  targets: { email: {}, web: {} },
1658
1856
  editor: { groups: [{ title: "Cell", paths: ["props.colSpan", "style.width", "style.verticalAlign"] }, TEXT_EDITOR, { title: "Box", paths: ["style.padding", "style.backgroundColor", "style.border"] }] }
1659
1857
  });
@@ -1752,17 +1950,27 @@ function checkDocument(input, options) {
1752
1950
  const extra = Object.keys(input).filter((key) => key !== "root");
1753
1951
  for (const key of extra) d.error("document/unknown-key", `"${key}" isn't part of a document (root)`, key);
1754
1952
  const rootDefinition = isObject2(input.root) && typeof input.root.type === "string" ? registry.get(input.root.type) : void 0;
1755
- const context = { d, registry, used: /* @__PURE__ */ new Set(), responsive: !!rootDefinition?.responsive };
1953
+ const context = { d, registry, used: /* @__PURE__ */ new Set(), root: rootDefinition };
1756
1954
  const root = input.root === void 0 ? null : readNode(input.root, "root", null, context);
1757
1955
  if (input.root === void 0) d.error("document/root", 'A document has a root: { type: "Email" }, "Page", \u2026', "root");
1758
1956
  return { document: root ? { root } : null, diagnostics: d.list };
1759
1957
  }
1760
1958
  function checkFragment(input, options) {
1761
1959
  const d = new Diagnostics();
1762
- const context = { d, registry: options.registry, idPrefix: options.idPrefix, used: new Set(options.taken), responsive: options.responsive };
1960
+ const context = { d, registry: options.registry, idPrefix: options.idPrefix, used: new Set(options.taken), root: options.root };
1763
1961
  const node = readNode(input, options.path, options.parent, context);
1764
1962
  return { node, diagnostics: d.list };
1765
1963
  }
1964
+ function rootList(roots) {
1965
+ const named = roots.map((root) => a(root).replace(/^A/, "a"));
1966
+ return named.length > 1 ? `${named.slice(0, -1).join(", ")} or ${named[named.length - 1]}` : named[0] ?? "no root";
1967
+ }
1968
+ function rootTakes(roots, root) {
1969
+ return !roots || !root || roots.includes(root.type);
1970
+ }
1971
+ function sharesOutput(definition, root) {
1972
+ return Object.keys(definition.targets).some((target) => target in root.targets);
1973
+ }
1766
1974
  function allowedIn(parent, child) {
1767
1975
  const rule2 = parent.children;
1768
1976
  return !!rule2.types?.includes(child.type) || child.categories.some((category) => rule2.categories?.includes(category));
@@ -1788,7 +1996,29 @@ function editDistance(from, to) {
1788
1996
  }
1789
1997
  return row[to.length];
1790
1998
  }
1999
+ var OTHER_BREAKPOINTS = /* @__PURE__ */ new Set(["base", "initial", "xs", "sm", "md", "lg", "xl", "2xl", "tablet", "desktop", "wide", "small", "medium", "large"]);
2000
+ function conditionHint(definition, key) {
2001
+ const name = key.trim().replace(/^(&?:+|_+|\$|@)/, "").replace(/Style$/, "").toLowerCase();
2002
+ const own3 = CUSTOM_TYPE.test(definition.type);
2003
+ const hoverStyles = definition.hover ?? [];
2004
+ const toHover = own3 ? `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)`;
2005
+ const toPhone = own3 ? `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 } }}`;
2006
+ if (name === "hover") return toHover;
2007
+ if (name === "mobile" || name === "phone" || name === "phones" || key.trim().startsWith("@media")) return toPhone;
2008
+ if (name === "focus" || name === "focus-visible" || name === "focusvisible" || name === "focus-within") {
2009
+ return hoverStyles.length && !own3 ? `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`;
2010
+ }
2011
+ if (OTHER_BREAKPOINTS.has(name)) {
2012
+ 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 }})`;
2013
+ }
2014
+ if (name === "active" || name === "pressed" || name === "press" || name === "visited" || name === "dark" || name === "light" || name === "print") {
2015
+ return `there are no ${name} styles: Elements has phone styles (mobile) and hover styles (hover) in style`;
2016
+ }
2017
+ return void 0;
2018
+ }
1791
2019
  function unknownProp(definition, key) {
2020
+ const condition = conditionHint(definition, key);
2021
+ if (condition) return `${a(definition.type)} has no prop "${key}": ${condition}`;
1792
2022
  if (Object.prototype.hasOwnProperty.call(definition.style, key)) return `${a(definition.type)} has no prop "${key}": it's a style, style={{ ${key}: \u2026 }}`;
1793
2023
  const known = Object.keys(definition.props);
1794
2024
  const meant = nearest(key, known);
@@ -1828,6 +2058,15 @@ function readNode(input, path, parent, context) {
1828
2058
  d.error("children/not-allowed", placementMessage(parent, definition), path);
1829
2059
  return null;
1830
2060
  }
2061
+ const root = context.root;
2062
+ if (root && !rootTakes(definition.roots, root) && sharesOutput(definition, root)) {
2063
+ const { code, message } = definition.notAllowed ?? {
2064
+ code: "component/not-supported",
2065
+ message: (under) => `${a(definition.type)} goes in ${rootList(definition.roots ?? [])}, not in ${a(under).replace(/^A/, "a")}`
2066
+ };
2067
+ d.error(code, message(root.type), path);
2068
+ return null;
2069
+ }
1831
2070
  let id;
1832
2071
  if (input.id !== void 0) {
1833
2072
  if (typeof input.id !== "string" || !ID.test(input.id)) {
@@ -1847,21 +2086,49 @@ function readNode(input, path, parent, context) {
1847
2086
  if (!NODE_KEYS.includes(key)) d.error("node/unknown-key", `"${key}" isn't part of a component (${NODE_KEYS.join(", ")})`, `${path}.${key}`, id);
1848
2087
  }
1849
2088
  const node = id === void 0 ? { type: definition.type } : { id, type: definition.type };
1850
- const props = readProps(input.props, definition, `${path}.props`, id, d);
2089
+ const props = readProps(input.props, definition, `${path}.props`, id, d, context.root);
1851
2090
  if (props) node.props = props;
1852
- const style = readStyles(input.style, definition, definition.style, `${path}.style`, id, d, "style", () => true);
1853
- if (style) node.style = style;
1854
- if (input.mobile !== void 0) {
1855
- 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);
1856
- else {
1857
- const mobile = readStyles(input.mobile, definition, definition.style, `${path}.mobile`, id, d, "mobile", (spec) => !!spec.mobile);
1858
- if (mobile) node.mobile = mobile;
2091
+ const lifted = {};
2092
+ let styleInput = input.style;
2093
+ if (isObject2(styleInput) && !CUSTOM_TYPE.test(definition.type)) {
2094
+ const rest = {};
2095
+ for (const [key, value] of Object.entries(styleInput)) {
2096
+ if (CONDITION_NAMES.has(key)) lifted[key] = value;
2097
+ else rest[key] = value;
1859
2098
  }
2099
+ styleInput = rest;
1860
2100
  }
1861
- if (input.hover !== void 0) {
1862
- const allowed = new Set(definition.hover ?? []);
1863
- const hover = readStyles(input.hover, definition, definition.style, `${path}.hover`, id, d, "hover", (_, key) => allowed.has(key));
1864
- if (hover) node.hover = hover;
2101
+ const style = readStyles(styleInput, definition, definition.style, `${path}.style`, id, d, "style", () => true, void 0, context.root);
2102
+ if (style) node.style = style;
2103
+ for (const condition of CONDITIONS) {
2104
+ const inStyle = lifted[condition.name];
2105
+ const given = input[condition.name];
2106
+ if (inStyle !== void 0 && given !== void 0) {
2107
+ d.error(
2108
+ "style/twice",
2109
+ `${condition.name} is given twice: as ${condition.name}={\u2026} and in style. Give it once, in style: style={{ ${condition.name}: { \u2026 } }}`,
2110
+ `${path}.style.${condition.name}`,
2111
+ id
2112
+ );
2113
+ continue;
2114
+ }
2115
+ let value = inStyle ?? given;
2116
+ if (value === void 0) continue;
2117
+ const at = inStyle !== void 0 ? `${path}.style.${condition.name}` : `${path}.${condition.name}`;
2118
+ if (condition.roots && !condition.roots(context.root)) {
2119
+ if (condition.notAllowed) d.error(condition.notAllowed.code, condition.notAllowed.message, at, id);
2120
+ continue;
2121
+ }
2122
+ if (isObject2(value) && Object.keys(value).some((key) => CONDITION_NAMES.has(key))) {
2123
+ const own3 = {};
2124
+ for (const [key, inner] of Object.entries(value)) {
2125
+ 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);
2126
+ else own3[key] = inner;
2127
+ }
2128
+ value = own3;
2129
+ }
2130
+ const read2 = readStyles(value, definition, definition.style, at, id, d, condition.name, (spec, key) => condition.allows(spec, key, definition), condition.for, context.root);
2131
+ if (read2) node[condition.name] = read2;
1865
2132
  }
1866
2133
  if (input.hidden !== void 0) {
1867
2134
  const hidden2 = readHidden(input.hidden, `${path}.hidden`, id, context);
@@ -1871,7 +2138,7 @@ function readNode(input, path, parent, context) {
1871
2138
  if (children.length) node.children = children;
1872
2139
  return node;
1873
2140
  }
1874
- function readProps(input, definition, path, id, d) {
2141
+ function readProps(input, definition, path, id, d, root) {
1875
2142
  const out = {};
1876
2143
  if (input !== void 0 && !isObject2(input)) {
1877
2144
  d.error("props/invalid", "props is an object", path, id);
@@ -1884,9 +2151,14 @@ function readProps(input, definition, path, id, d) {
1884
2151
  d.error("prop/unknown", unknownProp(definition, key), `${path}.${key}`, id);
1885
2152
  continue;
1886
2153
  }
2154
+ if (!rootTakes(spec.roots, root)) {
2155
+ 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);
2156
+ continue;
2157
+ }
1887
2158
  const parsed = spec.kind.parse(value);
1888
- if (parsed.ok) out[key] = parsed.value;
1889
- else d.error("prop/invalid", `${key}: ${parsed.message}`, `${path}.${key}`, id);
2159
+ if (parsed.ok) {
2160
+ if (!spec.kind.empty?.(parsed.value)) out[key] = parsed.value;
2161
+ } else d.error("prop/invalid", `${key}: ${parsed.message}`, `${path}.${key}`, id);
1890
2162
  }
1891
2163
  for (const [key, spec] of Object.entries(definition.props)) {
1892
2164
  if (spec.required && !(key in out) && !(isObject2(input) && input[key] !== void 0)) {
@@ -1903,6 +2175,8 @@ var UNSUPPORTED_CSS = {
1903
2175
  marginRight: "use padding",
1904
2176
  display: "the component sets it",
1905
2177
  position: "use a Layer for content placed over other content",
2178
+ backgroundImage: "put a Layer around it, its first child an Image (the Layer's height crops it to cover), the rest over it",
2179
+ background: "backgroundColor for a color; for an image, a Layer: its first child an Image, the rest over it",
1906
2180
  float: "use a Row to put things side by side",
1907
2181
  // Names Elements used before it took CSS's.
1908
2182
  align: "it's alignItems now",
@@ -1916,10 +2190,12 @@ function styleHint(definition, specs, key) {
1916
2190
  if (Object.prototype.hasOwnProperty.call(definition.props, key)) return `: it's a prop, ${key}={\u2026}`;
1917
2191
  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";
1918
2192
  if (UNSUPPORTED_CSS[key]) return `: ${UNSUPPORTED_CSS[key]}`;
2193
+ const condition = conditionHint(definition, key);
2194
+ if (condition) return `: ${condition}`;
1919
2195
  const meant = nearest(key, Object.keys(specs));
1920
2196
  return meant ? `: did you mean "${meant}"?` : "";
1921
2197
  }
1922
- function readStyles(input, definition, specs, path, id, d, what, allowed) {
2198
+ function readStyles(input, definition, specs, path, id, d, what, allowed, forLabel, root) {
1923
2199
  if (input === void 0) return void 0;
1924
2200
  if (!isObject2(input)) {
1925
2201
  d.error(`${what}/invalid`, `${what} is an object of style properties`, path, id);
@@ -1933,8 +2209,12 @@ function readStyles(input, definition, specs, path, id, d, what, allowed) {
1933
2209
  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);
1934
2210
  continue;
1935
2211
  }
2212
+ if (!rootTakes(spec.roots, root)) {
2213
+ 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);
2214
+ continue;
2215
+ }
1936
2216
  if (!allowed(spec, key)) {
1937
- d.error(`${what}/not-allowed`, what === "mobile" ? `"${key}" can't be set for phones` : `"${key}" can't be set for hover`, `${path}.${key}`, id);
2217
+ d.error(`${what}/not-allowed`, `"${key}" can't be set ${forLabel ?? "here"}`, `${path}.${key}`, id);
1938
2218
  continue;
1939
2219
  }
1940
2220
  const parsed = spec.kind.parse(value);
@@ -1945,7 +2225,7 @@ function readStyles(input, definition, specs, path, id, d, what, allowed) {
1945
2225
  }
1946
2226
  function readHidden(input, path, id, context) {
1947
2227
  const { d } = context;
1948
- if (!context.responsive) {
2228
+ if (!context.root?.responsive) {
1949
2229
  d.error("hidden/not-supported", "Hiding on phones or desktops is for an Email or a Page", path, id);
1950
2230
  return void 0;
1951
2231
  }
@@ -1956,7 +2236,7 @@ function readHidden(input, path, id, context) {
1956
2236
  const out = {};
1957
2237
  if (input.mobile === true) out.mobile = true;
1958
2238
  if (input.desktop === true) out.desktop = true;
1959
- if (out.mobile && out.desktop) d.warning("hidden/everywhere", "Hidden on phones and on desktops, it never shows", path, id);
2239
+ if (!shownAnywhere({ hidden: out })) d.warning("hidden/everywhere", "Hidden on phones and on desktops, it never shows", path, id);
1960
2240
  return Object.keys(out).length ? out : void 0;
1961
2241
  }
1962
2242
  function readChildren(input, definition, path, id, context) {
@@ -1989,6 +2269,22 @@ function readChildren(input, definition, path, id, context) {
1989
2269
  });
1990
2270
  return out;
1991
2271
  }
2272
+ var same = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
2273
+ function reportDeclared(d, node, at, key, value, declared, target, outlook = true) {
2274
+ if (!declared) return;
2275
+ for (const rule2 of [].concat(declared)) {
2276
+ if (rule2.level === "full") continue;
2277
+ if (rule2.level === "outlook" && !outlook) continue;
2278
+ if (rule2.values && !rule2.values.some((v) => same(v, value))) continue;
2279
+ if (rule2.when && !rule2.when(value, node)) continue;
2280
+ const asked = typeof value === "string" ? `${key}: "${value}"` : key;
2281
+ const clients = target === "email" ? rule2.clients ?? (rule2.level === "outlook" ? ["outlook"] : void 0) : void 0;
2282
+ const output = clients ? "some email clients" : target === "web" ? "a page" : target;
2283
+ 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}`];
2284
+ const [said, as] = rule2.source === "assumed" ? ["info", `${message} (not checked)`] : [severity, message];
2285
+ d.add(code, said, as, at, node.id, { clients, fix: rule2.fix, see: rule2.feature && caniemail(rule2.feature) });
2286
+ }
2287
+ }
1992
2288
  var MAX_DEPTH = 32;
1993
2289
  function templatePathsOf(document, paths) {
1994
2290
  const places = /* @__PURE__ */ new Map();
@@ -2005,7 +2301,7 @@ function templatePathsOf(document, paths) {
2005
2301
  return path;
2006
2302
  };
2007
2303
  }
2008
- function expandDocument(document, options) {
2304
+ function expandDocument(document, options, target) {
2009
2305
  const registry = registryOf(options);
2010
2306
  const d = new Diagnostics();
2011
2307
  const paths = /* @__PURE__ */ new WeakMap();
@@ -2017,7 +2313,20 @@ function expandDocument(document, options) {
2017
2313
  collect(document.root);
2018
2314
  const rootDefinition = registry.get(document.root.type);
2019
2315
  const responsive = !!rootDefinition?.responsive;
2316
+ const unshown = (node, path) => {
2317
+ const declared = target && registry.get(node.type)?.conditionSupport;
2318
+ if (!declared) return;
2319
+ for (const condition of CONDITIONS) {
2320
+ const block2 = blockOf(node, condition);
2321
+ const byStyle = declared[condition.name];
2322
+ if (!block2 || !byStyle || condition.hiddenKey && node.hidden?.[condition.hiddenKey]) continue;
2323
+ for (const [key, value] of Object.entries(block2)) {
2324
+ reportDeclared(d, { type: node.type, id: node.id, props: node.props ?? {} }, `${path}.${condition.name}.${key}`, key, value, byStyle[key]?.[target], target);
2325
+ }
2326
+ }
2327
+ };
2020
2328
  const visit = (given, path, depth, self = path) => {
2329
+ unshown(given, path);
2021
2330
  const arrange = registry.get(given.type)?.arrange;
2022
2331
  if (!arrange) return walk(given, path, self, depth);
2023
2332
  const origins = /* @__PURE__ */ new Map();
@@ -2044,27 +2353,34 @@ function expandDocument(document, options) {
2044
2353
  return void 0;
2045
2354
  }
2046
2355
  let raw2;
2356
+ const reported = [];
2047
2357
  try {
2048
2358
  const style = { ...definition.defaultStyle, ...node.style };
2049
- raw2 = definition.expand(propsWithDefaults(definition, node.props), { register: (custom) => registry.add(custom), style, responsive });
2359
+ raw2 = definition.expand(propsWithDefaults(definition, node.props), { register: (custom) => registry.add(custom), style, responsive, report: (diagnostic) => reported.push(diagnostic) });
2050
2360
  } catch (error) {
2051
2361
  d.error("custom/render", `${node.type} failed to render: ${error instanceof Error ? error.message : String(error)}`, path, node.id);
2052
2362
  return void 0;
2053
2363
  }
2364
+ const at = `${path}(${node.type})`;
2365
+ for (const diagnostic of reported) d.list.push({ ...diagnostic, path: diagnostic.path === "root" || diagnostic.path.startsWith("root.") ? at + diagnostic.path.slice(4) : at });
2054
2366
  if (Array.isArray(raw2) || typeof raw2 !== "object" || raw2 === null) {
2055
2367
  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);
2056
2368
  return void 0;
2057
2369
  }
2058
- const { node: expanded, diagnostics } = checkFragment(raw2, { parent, responsive, idPrefix: node.id ?? null, taken, registry, path: `${path}(${node.type})` });
2370
+ const { node: expanded, diagnostics } = checkFragment(raw2, { parent, root: rootDefinition, idPrefix: node.id ?? null, taken, registry, path: `${path}(${node.type})` });
2059
2371
  for (const diagnostic of diagnostics) d.list.push(diagnostic);
2060
2372
  if (!expanded) return void 0;
2061
2373
  collectIds(expanded);
2062
2374
  if (node.style?.flex !== void 0) expanded.style = { ...expanded.style, flex: node.style.flex };
2063
2375
  if (node.hidden) expanded.hidden = { ...expanded.hidden, ...node.hidden };
2064
2376
  if (node.id && !expanded.id && !CUSTOM_TYPE.test(node.type)) expanded.id = node.id;
2065
- for (const [key, value] of Object.entries(node.mobile ?? {})) {
2066
- if (registry.get(expanded.type)?.style[key]?.mobile) expanded.mobile = { [key]: value, ...expanded.mobile };
2067
- 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);
2377
+ for (const condition of CONDITIONS) {
2378
+ const rendered = registry.get(expanded.type);
2379
+ for (const [key, value] of Object.entries(node[condition.name] ?? {})) {
2380
+ const spec = rendered?.style[key];
2381
+ if (rendered && spec && condition.allows(spec, key, rendered)) expanded[condition.name] = { [key]: value, ...expanded[condition.name] };
2382
+ 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);
2383
+ }
2068
2384
  }
2069
2385
  const inner = registry.get(expanded.type);
2070
2386
  if (isCustom(inner)) return expandOne(expanded, inner, parent, `${path}(${node.type})`, depth + 1, self);
@@ -2516,11 +2832,11 @@ var TEXT_CSS = [
2516
2832
  ["textAlign", "text-align", (v) => v],
2517
2833
  ["textTransform", "text-transform", (v) => v]
2518
2834
  ];
2519
- var same = (a2, b) => a2 === b || JSON.stringify(a2) === JSON.stringify(b);
2835
+ var same2 = (a2, b) => a2 === b || JSON.stringify(a2) === JSON.stringify(b);
2520
2836
  function textDeclarations(text22, parent, always = []) {
2521
2837
  const out = [];
2522
2838
  for (const [key, property, write3] of TEXT_CSS) {
2523
- if (!parent || always.includes(key) || !same(text22[key], parent[key])) out.push([property, write3(text22[key])]);
2839
+ if (!parent || always.includes(key) || !same2(text22[key], parent[key])) out.push([property, write3(text22[key])]);
2524
2840
  }
2525
2841
  return out;
2526
2842
  }
@@ -2596,7 +2912,9 @@ function classesOf(name, value) {
2596
2912
  if (name === "class") return (Array.isArray(value) ? value : [value]).filter((v) => typeof v === "string" && v !== "");
2597
2913
  const text22 = styleText(value, true);
2598
2914
  if (!text22) return [];
2599
- return [name === "phone" ? rule("phone", `&{${text22}}`) : rule("all", `&:hover{${text22}}`, "hover")];
2915
+ if (name === "phone") return [rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}{${text22}}`)];
2916
+ const selector = name === "hoverWithin" ? HOVER.sheet.within ?? HOVER.sheet.selector : HOVER.sheet.selector;
2917
+ return [rule(HOVER.sheet.media, `${selector}{${text22}}`, HOVER.sheet.level)];
2600
2918
  }
2601
2919
  function attributes2(props) {
2602
2920
  const out = [];
@@ -2604,7 +2922,7 @@ function attributes2(props) {
2604
2922
  let slot = -1;
2605
2923
  for (const [name, value] of Object.entries(props)) {
2606
2924
  if (name === "children" || name === "style" || value === void 0 || value === null || value === false) continue;
2607
- if (name === "class" || name === "phone" || name === "hover") {
2925
+ if (name === "class" || name === "phone" || name === "hover" || name === "hoverWithin") {
2608
2926
  const found = classesOf(name, value);
2609
2927
  if (found.length && slot < 0) slot = out.push("") - 1;
2610
2928
  classes.push(...found);
@@ -2620,8 +2938,8 @@ function withId(element2, id) {
2620
2938
  if (!id) return element2;
2621
2939
  const tag = /^<[a-zA-Z0-9]+/.exec(element2.markup)?.[0];
2622
2940
  if (!tag) throw new Error("[Elements] An id goes on an element");
2623
- const insert = (markup2) => `${tag} id="${escapeAttr(id)}"${markup2.slice(tag.length)}`;
2624
- return new Html2(insert(element2.markup), { ...element2.options, start: element2.options.start && insert(element2.options.start) });
2941
+ const insert2 = (markup2) => `${tag} id="${escapeAttr(id)}"${markup2.slice(tag.length)}`;
2942
+ return new Html2(insert2(element2.markup), { ...element2.options, start: element2.options.start && insert2(element2.options.start) });
2625
2943
  }
2626
2944
  function h(type, props, ...children) {
2627
2945
  const all = props ?? {};
@@ -2648,6 +2966,98 @@ function Merged({ children }) {
2648
2966
  }
2649
2967
  var open = (element2) => new Html2(element2.options.start ?? element2.markup);
2650
2968
  var close = (tag) => new Html2(`</${tag}>`);
2969
+ var BLOCK_MAP_VERSION = 1;
2970
+ var MARK_PAIRS = 16;
2971
+ var BlockMarks = class {
2972
+ constructor(pair) {
2973
+ this.nodes = [];
2974
+ this.open = String.fromCharCode(64976 + pair * 2);
2975
+ this.close = String.fromCharCode(64977 + pair * 2);
2976
+ }
2977
+ };
2978
+ var current = null;
2979
+ function withBlockMarks(marks, write3) {
2980
+ const before = current;
2981
+ current = marks;
2982
+ try {
2983
+ return write3();
2984
+ } finally {
2985
+ current = before;
2986
+ }
2987
+ }
2988
+ function markBlock(html2, node) {
2989
+ if (!current) return html2;
2990
+ const mark = `${current.open}${current.nodes.length}${current.close}`;
2991
+ const marked = insert(html2, mark, false) ?? insert(html2, mark, true);
2992
+ if (!marked) return html2;
2993
+ current.nodes.push(node);
2994
+ return marked;
2995
+ }
2996
+ var OUTLOOK_OPEN = "<!--[if mso]>";
2997
+ var OUTLOOK_CLOSE = "<![endif]-->";
2998
+ var TAG2 = /<[a-zA-Z][a-zA-Z0-9]*/y;
2999
+ function tagEnd(markup2, outlook) {
3000
+ for (let i = markup2.indexOf("<"); i >= 0; i = markup2.indexOf("<", i + 1)) {
3001
+ if (markup2.startsWith(OUTLOOK_OPEN, i)) {
3002
+ if (outlook) {
3003
+ i += OUTLOOK_OPEN.length - 1;
3004
+ continue;
3005
+ }
3006
+ const end = markup2.indexOf(OUTLOOK_CLOSE, i);
3007
+ if (end < 0) return -1;
3008
+ i = end + OUTLOOK_CLOSE.length - 1;
3009
+ continue;
3010
+ }
3011
+ if (markup2.startsWith("<!--", i)) {
3012
+ const end = markup2.indexOf("-->", i + 4);
3013
+ if (end < 0) return -1;
3014
+ i = end + 2;
3015
+ continue;
3016
+ }
3017
+ TAG2.lastIndex = i;
3018
+ const tag = TAG2.exec(markup2);
3019
+ if (tag) return i + tag[0].length;
3020
+ }
3021
+ return -1;
3022
+ }
3023
+ function insert(html2, mark, outlook) {
3024
+ const { list, kind: kind2, start } = html2.options;
3025
+ if (list) {
3026
+ for (let i = 0; i < list.length; i++) {
3027
+ const marked = insert(list[i], mark, outlook);
3028
+ if (marked) return new Html2(html2.markup, { ...html2.options, list: list.map((piece, j) => j === i ? marked : piece) });
3029
+ }
3030
+ return null;
3031
+ }
3032
+ if (kind2 === "outlook" && !outlook) return null;
3033
+ const at = tagEnd(html2.markup, outlook);
3034
+ if (at < 0) return null;
3035
+ const put = (text22) => text22.slice(0, at) + mark + text22.slice(at);
3036
+ return new Html2(put(html2.markup), { ...html2.options, start: start !== void 0 && at <= start.length ? put(start) : start });
3037
+ }
3038
+ function readMarks(marks, written) {
3039
+ const found = [];
3040
+ const seen = /* @__PURE__ */ new Set();
3041
+ const pieces2 = [];
3042
+ let length32 = 0;
3043
+ let last = 0;
3044
+ for (const match of written.matchAll(new RegExp(`${marks.open}(\\d+)${marks.close}`, "g"))) {
3045
+ const index = Number(match[1]);
3046
+ const node = marks.nodes[index];
3047
+ if (!node || seen.has(index)) return null;
3048
+ seen.add(index);
3049
+ const before = written.slice(last, match.index);
3050
+ pieces2.push(before);
3051
+ length32 += before.length;
3052
+ last = match.index + match[0].length;
3053
+ 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;
3054
+ found.push({ node, at: length32 });
3055
+ }
3056
+ pieces2.push(written.slice(last));
3057
+ const text22 = pieces2.join("");
3058
+ if (text22.includes(marks.open) || text22.includes(marks.close)) return null;
3059
+ return { text: text22, found };
3060
+ }
2651
3061
  function utf8Bytes(text22) {
2652
3062
  let bytes = 0;
2653
3063
  for (let i = 0; i < text22.length; i++) {
@@ -2734,7 +3144,7 @@ function fontFaces2(root, { swap }) {
2734
3144
  return [...faces.values()];
2735
3145
  }
2736
3146
  var isNode2 = (child) => typeof child !== "string";
2737
- var shown = (node) => !(node.hidden?.mobile && node.hidden?.desktop);
3147
+ var shown = (node) => shownAnywhere(node);
2738
3148
  function link2(props) {
2739
3149
  const { href, target } = linkParts(props.href);
2740
3150
  return {
@@ -2969,7 +3379,7 @@ var Button2 = ({ node, style, mobile, fill, phoneText: phoneText22, text: text22
2969
3379
  {
2970
3380
  align: TEXT_ALIGN[node.text.textAlign],
2971
3381
  bgcolor: bgcolor(style.backgroundColor),
2972
- hover: { backgroundColor: node.hover?.backgroundColor },
3382
+ hoverWithin: { backgroundColor: node.hover?.backgroundColor },
2973
3383
  phone: { border: mobile.border, backgroundColor: mobile.backgroundColor },
2974
3384
  style: { backgroundColor: style.backgroundColor, border: style.border, borderRadius: style.borderRadius, msoPaddingAlt: insets2(style.padding) }
2975
3385
  },
@@ -2980,8 +3390,11 @@ var Button2 = ({ node, style, mobile, fill, phoneText: phoneText22, text: text22
2980
3390
  phone: [phoneText22, { padding: mobile.padding }],
2981
3391
  hover: { color: node.hover?.color },
2982
3392
  style: [
2983
- // A button with a width fills it.
2984
- { display: width.width !== void 0 ? "block" : "inline-block", padding: style.padding },
3393
+ // A button with a width fills it. Its label's words stay whole (the email's word-break: break-word
3394
+ // is for long links in text): a button narrower than its padding otherwise sized itself to one letter
3395
+ // and drew its label a letter a line, in Gmail and browsers; it grows to its longest word instead, as
3396
+ // classic Outlook's and Apple Mail's do (captures of Elements 0.3.0, e3-fresh/g-0054).
3397
+ { display: width.width !== void 0 ? "block" : "inline-block", padding: style.padding, wordBreak: "normal" },
2985
3398
  fullText(node),
2986
3399
  { textDecoration: style.textDecoration ?? "none", borderRadius: style.borderRadius }
2987
3400
  ]
@@ -3445,27 +3858,42 @@ function propsOf(node, place, options) {
3445
3858
  options
3446
3859
  };
3447
3860
  }
3861
+ var UNMAPPED = /* @__PURE__ */ new Set(["LineBreak", "MergeTag"]);
3448
3862
  function write(node, place, options) {
3449
3863
  if (place.inline) {
3450
3864
  if (!shown(node)) return raw("");
3451
3865
  const component2 = INLINES[node.type];
3452
3866
  if (!component2) throw new Error(`[Elements] The email exporter can't render a ${node.type} inside text`);
3453
- return component2(propsOf(node, place, options));
3867
+ const html2 = component2(propsOf(node, place, options));
3868
+ return UNMAPPED.has(node.type) ? html2 : markBlock(html2, node);
3454
3869
  }
3455
3870
  const component = BLOCKS[node.type];
3456
3871
  if (!component) throw new Error(`[Elements] The email exporter can't render a ${node.type} here (in a ${place.parent.type})`);
3457
- return component(propsOf(node, place, options));
3872
+ return markBlock(component(propsOf(node, place, options)), node);
3458
3873
  }
3459
3874
  var EMAIL_PHONE_MARGIN = 20;
3460
3875
  var GMAIL_CLIP_BYTES = 102400;
3461
3876
  var GMAIL_STYLE_BYTES = 16384;
3462
3877
  function renderEmail(resolved, options = {}) {
3463
3878
  const root = resolved.root;
3464
- const sheet = new Stylesheet(options.classPrefix);
3465
3879
  const d = new Diagnostics();
3466
3880
  const fonts = fontFaces2(root, { swap: false });
3467
3881
  const width = root.width ?? 600;
3468
- const email = withStylesheet(sheet, () => String(Email2(propsOf(root, { parent: root, fill: false, mobile: {}, inline: false, rtl: false }, options))));
3882
+ const write3 = (marks) => {
3883
+ const sheet2 = new Stylesheet(options.classPrefix);
3884
+ const markup2 = withStylesheet(sheet2, () => withBlockMarks(marks, () => String(markBlock(Email2(propsOf(root, { parent: root, fill: false, mobile: {}, inline: false, rtl: false }, options)), root))));
3885
+ return { sheet: sheet2, markup: markup2 };
3886
+ };
3887
+ let written = options.blockMap ? null : write3(null);
3888
+ let marked = null;
3889
+ for (let pair = 0; !written; pair++) {
3890
+ 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");
3891
+ const marks = new BlockMarks(pair);
3892
+ const attempt = write3(marks);
3893
+ marked = readMarks(marks, attempt.markup);
3894
+ if (marked) written = { sheet: attempt.sheet, markup: marked.text };
3895
+ }
3896
+ const { sheet, markup: email } = written;
3469
3897
  const background = root.style.backgroundColor;
3470
3898
  const lang = root.props.lang;
3471
3899
  const dir = root.props.dir;
@@ -3489,7 +3917,8 @@ function renderEmail(resolved, options = {}) {
3489
3917
  `<style>${stylesheet}</style>`
3490
3918
  ].join("");
3491
3919
  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>` : "";
3492
- 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>`;
3920
+ 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"]])}>`;
3921
+ const body = `${preheader}${opening}${email}</div>`;
3493
3922
  const bodyDeclarations = [
3494
3923
  ["margin", "0"],
3495
3924
  ["padding", "0"],
@@ -3497,20 +3926,38 @@ function renderEmail(resolved, options = {}) {
3497
3926
  ["background-color", background]
3498
3927
  ];
3499
3928
  const bodyStyle = css2(bodyDeclarations);
3500
- const html2 = `<!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>`;
3929
+ 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)}>`;
3930
+ const html2 = `${shell}${body}</body></html>`;
3501
3931
  const bytes = utf8Bytes(html2);
3932
+ const gmails = ["gmail", "gmail-app", "gmail-app-other"];
3502
3933
  if (bytes > GMAIL_CLIP_BYTES) {
3503
- 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");
3934
+ 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, {
3935
+ clients: gmails,
3936
+ fix: "shorten the email, or move long sections to a page: what follows the clip is behind a link"
3937
+ });
3504
3938
  }
3505
3939
  const styleBytes = utf8Bytes(stylesheet);
3506
3940
  if (styleBytes > GMAIL_STYLE_BYTES) {
3507
- d.warning("email/style-size", `The stylesheet is ${Math.round(styleBytes / 1024)} KB; Gmail reads ${GMAIL_STYLE_BYTES / 1024} KB of a <style>`, "root");
3941
+ 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, {
3942
+ clients: gmails,
3943
+ fix: "use fewer phone and hover styles: each one adds a rule"
3944
+ });
3508
3945
  }
3509
- return { html: html2, head, body, bodyStyle, diagnostics: d.list };
3946
+ return { html: html2, head, body, bodyStyle, diagnostics: d.list, ...marked ? { blocks: blockMap(resolved, marked.found, email, utf8Bytes(shell + preheader + opening)) } : {} };
3947
+ }
3948
+ function blockMap(resolved, found, email, start) {
3949
+ const templatePath = resolved.templatePath ?? ((path) => path);
3950
+ let offset = start;
3951
+ let from = 0;
3952
+ const entries = found.map(({ node, at }) => {
3953
+ offset += utf8Bytes(email.slice(from, at));
3954
+ from = at;
3955
+ return { path: templatePath(node.path), ...node.id ? { nodeId: node.id } : {}, offset };
3956
+ });
3957
+ return { version: BLOCK_MAP_VERSION, unit: "utf8", entries };
3510
3958
  }
3511
3959
  var isNode3 = (child) => typeof child !== "string";
3512
- var shown2 = (node) => !(node.hidden?.mobile && node.hidden?.desktop);
3513
- var same2 = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
3960
+ var same3 = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
3514
3961
  function support(root, target) {
3515
3962
  const d = new Diagnostics();
3516
3963
  const visit = (node) => {
@@ -3518,41 +3965,81 @@ function support(root, target) {
3518
3965
  const byProp = definition.defaultStyleBy ? definition.defaultStyleBy.values[String(node.props[definition.defaultStyleBy.prop])] : void 0;
3519
3966
  for (const [key, value] of Object.entries(node.style)) {
3520
3967
  const fallback2 = byProp?.[key] ?? definition.defaultStyle?.[key];
3521
- report(node, "style", key, value, fallback2, definition.style[key]?.support, target, d);
3968
+ if (same3(value, fallback2)) continue;
3969
+ reportDeclared(d, node, `${node.path}.style.${key}`, key, value, definition.style[key]?.support?.[target], target);
3522
3970
  }
3523
- for (const [key, value] of Object.entries(node.props)) report(node, "props", key, value, definition.props[key]?.default, definition.props[key]?.support, target, d);
3524
- for (const child of node.children) if (isNode3(child) && shown2(child)) visit(child);
3971
+ for (const [key, value] of Object.entries(node.props)) {
3972
+ if (same3(value, definition.props[key]?.default)) continue;
3973
+ reportDeclared(d, node, `${node.path}.props.${key}`, key, value, definition.props[key]?.support?.[target], target);
3974
+ }
3975
+ for (const condition of CONDITIONS) {
3976
+ const block2 = blockOf(node, condition);
3977
+ if (!block2 || condition.hiddenKey && node.hidden?.[condition.hiddenKey]) continue;
3978
+ const own3 = condition.support[target];
3979
+ reportDeclared(d, node, `${node.path}.${condition.name}`, condition.name, block2, own3, target);
3980
+ if (!own3 || own3.level === "none") continue;
3981
+ for (const [key, value] of Object.entries(block2)) {
3982
+ const rules = condition.styles?.[key]?.[target] ?? definition.style[key]?.support?.[target];
3983
+ reportDeclared(d, node, `${node.path}.${condition.name}.${key}`, key, value, rules, target, condition.outlookApplies);
3984
+ }
3985
+ }
3986
+ for (const child of node.children) if (isNode3(child) && shownAnywhere(child)) visit(child);
3525
3987
  };
3526
3988
  visit(root);
3527
3989
  return d.list;
3528
3990
  }
3529
- function report(node, where, key, value, fallback2, declared, target, d) {
3530
- const rules = declared?.[target];
3531
- if (!rules || same2(value, fallback2)) return;
3532
- for (const rule2 of [].concat(rules)) {
3533
- if (rule2.level === "full") continue;
3534
- if (rule2.values && !rule2.values.some((v) => same2(v, value))) continue;
3535
- if (rule2.when && !rule2.when(value, node)) continue;
3536
- const at = `${node.path}.${where}.${key}`;
3537
- const asked = typeof value === "string" ? `${key}: "${value}"` : key;
3538
- const output = target === "web" ? "a page" : target;
3539
- if (rule2.level === "none") d.warning(`${target}/unsupported`, `${asked} isn't possible in ${output}: ${rule2.shows}`, at, node.id);
3540
- else if (rule2.level === "partial") d.add(`${target}/degraded`, "info", `${asked} on this ${node.type} in ${output}: ${rule2.shows}`, at, node.id);
3541
- else d.add("email/outlook-degraded", "info", `${asked} on this ${node.type} in classic Outlook: ${rule2.shows}`, at, node.id);
3542
- }
3543
- }
3544
- function emailSupport(root) {
3991
+ function emailSupport(root, templatePath = (path) => path) {
3545
3992
  const d = new Diagnostics();
3546
3993
  d.list.push(...support(root, "email"));
3547
3994
  const webFonts = new Set((root.props.fonts ?? []).map((face) => face.family.toLowerCase()));
3995
+ const phone = { styled: false, phoneHidden: false, desktopHidden: false };
3548
3996
  const visit = (node) => {
3549
3997
  structure(node, d);
3550
3998
  fallback(node, webFonts, d);
3999
+ if (node.type === "Html") htmlBlock(node, d);
4000
+ if (node.type === "Image") imageFormat(node, "src", d);
4001
+ if (node.type === "Video" && node.props.thumbnail !== void 0) imageFormat(node, "thumbnail", d);
4002
+ if (node.mobile && Object.keys(node.mobile).length) phone.styled = true;
4003
+ if (node.hidden?.mobile && !builtIn(templatePath(node.path))) phone.phoneHidden = true;
4004
+ if (node.hidden?.desktop && !builtIn(templatePath(node.path))) phone.desktopHidden = true;
3551
4005
  for (const child of node.children) if (isNode2(child) && shown(child)) visit(child);
3552
4006
  };
3553
4007
  visit(root);
4008
+ phoneStyles(root, phone, d);
3554
4009
  return d.list;
3555
4010
  }
4011
+ function builtIn(place) {
4012
+ const made = [...place.matchAll(/\((\w+)\)/g)].pop()?.[1];
4013
+ return !!made && Object.prototype.hasOwnProperty.call(COMPONENTS, made);
4014
+ }
4015
+ var NO_PHONE_STYLES = ["gmail-app-other", "sfr", "laposte", "gmx", "web-de", "rainloop"];
4016
+ function phoneStyles(root, phone, d) {
4017
+ 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);
4018
+ if (!parts.length) return;
4019
+ 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, {
4020
+ clients: NO_PHONE_STYLES,
4021
+ fix: phone.desktopHidden ? "show what a reader needs on desktop too, and hide on phones only what they can do without" : void 0,
4022
+ see: caniemail("css-at-media")
4023
+ });
4024
+ }
4025
+ var IMAGE_FORMATS = {
4026
+ webp: { clients: ["outlook"], feature: "image-webp", name: "a WebP" },
4027
+ avif: { clients: ["outlook", "fastmail", "gmx", "web-de", "orange", "sfr", "mail-ru", "ionos"], feature: "image-avif", name: "an AVIF" },
4028
+ svg: { clients: ["mail-ru"], feature: "image-svg", name: "an SVG" }
4029
+ };
4030
+ 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];
4031
+ function imageFormat(node, key, d) {
4032
+ const src = assetParts(node.props[key]).src;
4033
+ const format = FORMAT_IN.map((pattern) => pattern.exec(src)?.[1]).find(Boolean)?.toLowerCase();
4034
+ const known = format && Object.prototype.hasOwnProperty.call(IMAGE_FORMATS, format) ? IMAGE_FORMATS[format] : void 0;
4035
+ if (!known) return;
4036
+ const what = key === "src" ? "This Image is" : "This Video's thumbnail is";
4037
+ d.warning("email/image-format", `${what} ${known.name} file, which these email clients don't show`, `${node.path}.props.${key}`, node.id, {
4038
+ clients: known.clients,
4039
+ fix: "use a PNG, JPEG or GIF file",
4040
+ see: caniemail(known.feature)
4041
+ });
4042
+ }
3556
4043
  function fallback(node, webFonts, d) {
3557
4044
  const stack = node.style.fontFamily;
3558
4045
  if (typeof stack !== "string" || !webFonts.size) return;
@@ -3561,9 +4048,10 @@ function fallback(node, webFonts, d) {
3561
4048
  if (!last || !webFonts.has(last.toLowerCase())) return;
3562
4049
  d.warning(
3563
4050
  "email/font-fallback",
3564
- `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"`,
4051
+ `This font stack ends with the web font "${last}": where it doesn't load, the email client picks its own font`,
3565
4052
  `${node.path}.style.fontFamily`,
3566
- node.id
4053
+ node.id,
4054
+ { clients: NO_WEB_FONTS, fix: `end it with a font the client has: "${stack}, Arial, sans-serif"`, see: caniemail("css-at-font-face") }
3567
4055
  );
3568
4056
  }
3569
4057
  function structure(node, d) {
@@ -3576,7 +4064,8 @@ function structure(node, d) {
3576
4064
  "info",
3577
4065
  `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`,
3578
4066
  path,
3579
- id
4067
+ id,
4068
+ { clients: ["outlook"], fix: "put less in it, or let it stack on phones (stackOnMobile)" }
3580
4069
  );
3581
4070
  }
3582
4071
  const lines = wrapsWithGap(node);
@@ -3612,6 +4101,86 @@ function structure(node, d) {
3612
4101
  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);
3613
4102
  }
3614
4103
  }
4104
+ var HTML_CSS = [
4105
+ { 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" },
4106
+ { 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" },
4107
+ {
4108
+ pattern: /(^|[\s;{"'])position\s*:\s*absolute\b/i,
4109
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "hey", "sfr", "laposte", "mail-ru", "gmx", "web-de"],
4110
+ removes: "position: absolute: the client removes it",
4111
+ fix: "lay it out without position (a Layer puts content over an image)",
4112
+ feature: "css-position"
4113
+ },
4114
+ {
4115
+ // Outlook.com removes every position (captures); the others not named keep relative.
4116
+ pattern: /(^|[\s;{"'])position\s*:\s*relative\b/i,
4117
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "mail-ru", "gmx", "web-de"],
4118
+ removes: "position: relative: the client removes it, and what it moves stays where it is",
4119
+ fix: "move it with padding instead",
4120
+ feature: "css-position",
4121
+ use: "relative"
4122
+ },
4123
+ {
4124
+ pattern: /var\(\s*--|(^|[\s;{"'])--[\w-]+\s*:/i,
4125
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "orange", "mail-ru", "fastmail", "gmx", "web-de"],
4126
+ removes: "CSS variables: the client removes var() and their declarations",
4127
+ fix: "write the values themselves",
4128
+ feature: "css-variables"
4129
+ },
4130
+ {
4131
+ pattern: /gradient\(/i,
4132
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail-app", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
4133
+ removes: "a gradient: the client removes it",
4134
+ fix: "give it a solid background color too, or use an image",
4135
+ feature: "css-linear-gradient"
4136
+ },
4137
+ { 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()" },
4138
+ { 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)" },
4139
+ {
4140
+ pattern: /<style[\s>]/i,
4141
+ clients: ["gmail", "gmail-app", "gmail-app-other", "protonmail", "gmx", "web-de", "sfr", "laposte", "rainloop"],
4142
+ removes: "a <style> in the email's body: the client removes it, and what it styles shows without it",
4143
+ fix: "write the styles inline, on the elements they're for",
4144
+ feature: "html-style"
4145
+ },
4146
+ { 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" },
4147
+ { 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" },
4148
+ { 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" },
4149
+ {
4150
+ pattern: /display\s*:\s*inline-flex\b/i,
4151
+ clients: ["outlook", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
4152
+ removes: "display: inline-flex: the client lays it out without flex",
4153
+ fix: "lay it out with a Row",
4154
+ feature: "css-display-flex",
4155
+ use: "inline"
4156
+ },
4157
+ { 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" },
4158
+ { 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" },
4159
+ {
4160
+ pattern: /object-fit\s*:/i,
4161
+ clients: ["outlook", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
4162
+ removes: "object-fit: the client stretches the image to its box",
4163
+ fix: "crop the image file to the size it's shown at",
4164
+ feature: "css-object-fit"
4165
+ },
4166
+ {
4167
+ pattern: /(^|[\s;{"'])aspect-ratio\s*:/i,
4168
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "samsung-email", "thunderbird", "fastmail", "gmx", "web-de", "orange"],
4169
+ removes: "aspect-ratio: the client removes it",
4170
+ fix: "give it a width and a height",
4171
+ feature: "css-aspect-ratio"
4172
+ }
4173
+ ];
4174
+ var OUTLOOK_ONLY2 = /<!--\[if\s+(?:(?:gte?|lte?)\s+)?mso[^\]]*\]>[\s\S]*?<!\[endif\]-->/gi;
4175
+ function htmlBlock(node, d) {
4176
+ if (typeof node.props.html !== "string") return;
4177
+ const markup2 = node.props.html.replace(OUTLOOK_ONLY2, "");
4178
+ for (const check of HTML_CSS) {
4179
+ if (check.pattern.test(markup2)) {
4180
+ 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) });
4181
+ }
4182
+ }
4183
+ }
3615
4184
  var isNode4 = (child) => typeof child !== "string";
3616
4185
  var CLEARLY = 1.25;
3617
4186
  var sides = (value) => {
@@ -3743,15 +4312,15 @@ function classesFor(node, parent) {
3743
4312
  const classes = [];
3744
4313
  if (node.mobile) {
3745
4314
  const { width, ...rest } = node.mobile;
3746
- if (Object.keys(rest).length) classes.push(rule("phone", `&{${css2(declarationsOf(rest), true)}}`));
4315
+ if (Object.keys(rest).length) classes.push(rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}{${css2(declarationsOf(rest), true)}}`));
3747
4316
  const line22 = node.type === "Divider" && node.style.padding !== void 0 ? ">hr" : "";
3748
- if (width !== void 0) classes.push(rule("phone", `&${line22}{${css2(declarationsOf({ width }), true)}}`));
4317
+ if (width !== void 0) classes.push(rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}${line22}{${css2(declarationsOf({ width }), true)}}`));
3749
4318
  const own3 = node.style.width === void 0 && (node.style.flex === "none" || node.style.flex === 0);
3750
4319
  const basis = own3 ? void 0 : length2(phoneWidthInRow(node, parent));
3751
4320
  if (basis !== void 0) classes.push(rule("phone", `&{flex:0 1 ${basis}!important}`));
3752
4321
  }
3753
4322
  classes.push(...hidingClasses(node.hidden));
3754
- if (node.hover) classes.push(rule("all", `&:hover{${css2(declarationsOf(node.hover), true)}}`, "hover"));
4323
+ if (node.hover) classes.push(rule(HOVER.sheet.media, `${HOVER.sheet.selector}{${css2(declarationsOf(node.hover), true)}}`, HOVER.sheet.level));
3755
4324
  if (node.type === "Row" && node.props.stackOnMobile !== false) {
3756
4325
  classes.push(
3757
4326
  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")
@@ -3959,17 +4528,17 @@ var Layer3 = ({ node, style, parent, placement, classes, children }) => /* @__PU
3959
4528
  },
3960
4529
  children()
3961
4530
  );
3962
- var current = { page: 1, total: 1 };
4531
+ var current2 = { page: 1, total: 1 };
3963
4532
  function numbering(numbers, write3) {
3964
- const before = current;
3965
- current = numbers;
4533
+ const before = current2;
4534
+ current2 = numbers;
3966
4535
  try {
3967
4536
  return write3();
3968
4537
  } finally {
3969
- current = before;
4538
+ current2 = before;
3970
4539
  }
3971
4540
  }
3972
- var PageNumber2 = ({ node }) => /* @__PURE__ */ h(Fragment, null, String(node.props.total ? current.total : current.page));
4541
+ var PageNumber2 = ({ node }) => /* @__PURE__ */ h(Fragment, null, String(node.props.total ? current2.total : current2.page));
3973
4542
  function documentCss(root) {
3974
4543
  const box2 = pageBox(root.props);
3975
4544
  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}}`;
@@ -4143,8 +4712,23 @@ var Video3 = ({ node, style, placement, classes }) => /* @__PURE__ */ h(
4143
4712
  ))
4144
4713
  );
4145
4714
  var PAGE_PHONE_WIDTH = 480;
4146
- var PHONE = `@media (max-width: ${PAGE_PHONE_WIDTH}px)`;
4147
- var DESKTOP = `@media (min-width: ${PAGE_PHONE_WIDTH + 1}px)`;
4715
+ function pagePhoneWidth(root) {
4716
+ const mobile = root.props.breakpoints?.mobile;
4717
+ return typeof mobile === "number" ? mobile : PAGE_PHONE_WIDTH;
4718
+ }
4719
+ function pageBreakpoints(root) {
4720
+ const mobile = root.props.breakpoints?.mobile;
4721
+ const width = typeof root.props.width === "number" ? root.props.width : void 0;
4722
+ if (root.type !== "Page" || typeof mobile !== "number" || width === void 0 || mobile < width) return [];
4723
+ return [
4724
+ {
4725
+ code: "page/phone-width",
4726
+ severity: "warning",
4727
+ 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}`,
4728
+ path: "root.props.breakpoints"
4729
+ }
4730
+ ];
4731
+ }
4148
4732
  var COMPONENTS2 = {
4149
4733
  Section: Section3,
4150
4734
  Row: Row3,
@@ -4205,7 +4789,8 @@ function renderPage(resolved, options = {}) {
4205
4789
  const writer2 = document ? Document2 : root.type === "Canvas" ? Canvas2 : Page2;
4206
4790
  const body = withStylesheet(sheet, () => String(writer2(propsOf2(root, root, [], options))));
4207
4791
  const fixed = !root.definition.responsive;
4208
- const stylesheet = [...fonts, sheet.css(fixed ? null : PHONE, fixed ? null : DESKTOP), document ? documentCss(root) : ""].join("");
4792
+ const phoneWidth2 = pagePhoneWidth(root);
4793
+ const stylesheet = [...fonts, sheet.css(fixed ? null : `@media (max-width: ${phoneWidth2}px)`, fixed ? null : `@media (min-width: ${phoneWidth2 + 1}px)`), document ? documentCss(root) : ""].join("");
4209
4794
  const head = [
4210
4795
  '<meta charset="utf-8">',
4211
4796
  '<meta name="viewport" content="width=device-width, initial-scale=1">',
@@ -4225,7 +4810,7 @@ function renderText(resolved, options = {}) {
4225
4810
  const tag = options.mergeTag ?? ((name) => `{{${name}}}`);
4226
4811
  const inline2 = (node, upper = false) => node.children.map((child) => {
4227
4812
  if (typeof child === "string") return upper ? child.toUpperCase() : child;
4228
- if (child.hidden?.mobile && child.hidden?.desktop) return "";
4813
+ if (!shownAnywhere(child)) return "";
4229
4814
  switch (child.type) {
4230
4815
  case "LineBreak":
4231
4816
  return "\n";
@@ -4249,7 +4834,7 @@ function renderText(resolved, options = {}) {
4249
4834
  return alt ? `[${upper ? alt.toUpperCase() : alt}]` : "";
4250
4835
  };
4251
4836
  const blocks = (node) => node.children.flatMap((child) => {
4252
- if (typeof child === "string" || child.hidden?.mobile && child.hidden?.desktop) return [];
4837
+ if (typeof child === "string" || !shownAnywhere(child)) return [];
4253
4838
  switch (child.type) {
4254
4839
  case "Heading":
4255
4840
  return [tidy(inline2(child, Number(child.props.level) <= 2))];
@@ -4287,8 +4872,8 @@ function renderText(resolved, options = {}) {
4287
4872
  return blocks(child);
4288
4873
  }
4289
4874
  });
4290
- const shown3 = (node) => !(node.hidden?.mobile && node.hidden?.desktop);
4291
- const list = (node, indent) => node.children.filter((child) => typeof child !== "string" && shown3(child)).map((item, index) => {
4875
+ const shown2 = (node) => shownAnywhere(node);
4876
+ const list = (node, indent) => node.children.filter((child) => typeof child !== "string" && shown2(child)).map((item, index) => {
4292
4877
  const marker2 = node.props.type === "number" ? `${Number(node.props.start) + index}. ` : "- ";
4293
4878
  const lines = [];
4294
4879
  let run = [];
@@ -4304,13 +4889,13 @@ function renderText(resolved, options = {}) {
4304
4889
  }
4305
4890
  flush();
4306
4891
  if (!lines.length) lines.push(`${indent}${marker2}`.trimEnd());
4307
- if (shown3(child)) lines.push(list(child, `${indent} `));
4892
+ if (shown2(child)) lines.push(list(child, `${indent} `));
4308
4893
  }
4309
4894
  flush();
4310
4895
  return (lines.length ? lines : [`${indent}${marker2}`.trimEnd()]).join("\n");
4311
4896
  }).join("\n");
4312
- const table = (node) => node.children.filter((row) => typeof row !== "string" && shown3(row)).map(
4313
- (row) => row.children.filter((cell) => typeof cell !== "string" && shown3(cell)).map((cell) => tidy(inline2(cell))).join(" | ")
4897
+ const table = (node) => node.children.filter((row) => typeof row !== "string" && shown2(row)).map(
4898
+ (row) => row.children.filter((cell) => typeof cell !== "string" && shown2(cell)).map((cell) => tidy(inline2(cell))).join(" | ")
4314
4899
  ).join("\n");
4315
4900
  return blocks(resolved.root).filter((block2) => block2.length > 0).join("\n\n");
4316
4901
  }
@@ -4353,52 +4938,56 @@ var OUTPUTS = {
4353
4938
  Email: { target: "email", ready: true }
4354
4939
  };
4355
4940
  function prepareTemplate(input, options = {}) {
4941
+ checkEmailClients(options.emailClients);
4356
4942
  const registry = registryOf(options);
4357
4943
  const checked = checkDocument(input, registry);
4358
4944
  const diagnostics = [...checked.diagnostics];
4359
4945
  const failed = () => diagnostics.some((d) => d.severity === "error");
4360
4946
  if (!checked.document || failed()) return { resolved: null, target: null, diagnostics };
4361
4947
  const root = checked.document.root.type;
4948
+ const done = (resolved2, target) => {
4949
+ 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" });
4950
+ return { resolved: resolved2, target, diagnostics: root === "Email" ? forEmailClients(diagnostics, options.emailClients) : diagnostics };
4951
+ };
4362
4952
  const output = OUTPUTS[root];
4363
4953
  if (!output?.ready) {
4364
4954
  diagnostics.push({ code: "output/unavailable", severity: "error", message: output?.later ?? `A ${root} can't be rendered yet`, path: "root" });
4365
- return { resolved: null, target: null, diagnostics };
4955
+ return done(null, null);
4366
4956
  }
4367
- const expanded = expandDocument(checked.document, registry);
4957
+ const expanded = expandDocument(checked.document, registry, output.target);
4368
4958
  diagnostics.push(...expanded.diagnostics);
4369
- if (failed()) return { resolved: null, target: output.target, diagnostics };
4959
+ if (failed()) return done(null, output.target);
4370
4960
  diagnostics.push(...checkTargetExpanded(expanded.document, output.target, registry, expanded.paths));
4371
- if (failed()) return { resolved: null, target: output.target, diagnostics };
4961
+ if (failed()) return done(null, output.target);
4372
4962
  const resolved = resolveExpanded(expanded.document, registry, expanded.paths);
4373
4963
  diagnostics.push(...resolved.diagnostics);
4374
- const found = output.target === "email" ? emailSupport(resolved.root) : support(resolved.root, output.target);
4964
+ const found = output.target === "email" ? emailSupport(resolved.root, resolved.templatePath) : support(resolved.root, output.target);
4375
4965
  if (root === "Document" || root === "Canvas") found.push(...overflow(resolved.root));
4376
- if (root !== "Document") found.push(...pageNumbersOutside(resolved.root));
4966
+ found.push(...pageBreakpoints(resolved.root));
4377
4967
  diagnostics.push(...inTemplate(resolved, found));
4378
- return { resolved: failed() ? null : resolved, target: output.target, diagnostics };
4968
+ return done(failed() ? null : resolved, output.target);
4379
4969
  }
4380
4970
  function inTemplate(resolved, diagnostics) {
4381
4971
  const { templatePath } = resolved;
4382
4972
  return templatePath ? diagnostics.map((diagnostic) => ({ ...diagnostic, path: templatePath(diagnostic.path) })) : diagnostics;
4383
4973
  }
4384
- function pageNumbersOutside(root) {
4385
- const out = [];
4386
- const visit = (node) => {
4387
- if (node.type === "PageNumber") {
4388
- 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 } : {} });
4389
- }
4390
- for (const child of node.children) if (typeof child !== "string") visit(child);
4391
- };
4392
- visit(root);
4393
- return out;
4394
- }
4395
4974
  function renderTemplate(input, options = {}) {
4975
+ 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" }]);
4396
4976
  const { resolved, target, diagnostics } = prepareTemplate(input, options);
4397
4977
  if (!resolved) throw new ElementsError(diagnostics);
4398
4978
  const text22 = renderText(resolved, options);
4399
4979
  const written = target === "email" ? renderEmail(resolved, options) : { ...renderPage(resolved, options), diagnostics: [] };
4400
- const shown3 = (html2) => options.pretty ? prettyHtml(html2) : html2;
4401
- return { html: shown3(written.html), head: shown3(written.head), body: shown3(written.body), bodyStyle: written.bodyStyle, text: text22, diagnostics: [...diagnostics, ...inTemplate(resolved, written.diagnostics)] };
4980
+ const shown2 = (html2) => options.pretty ? prettyHtml(html2) : html2;
4981
+ const blocks = "blocks" in written ? written.blocks : void 0;
4982
+ return {
4983
+ html: shown2(written.html),
4984
+ head: shown2(written.head),
4985
+ body: shown2(written.body),
4986
+ bodyStyle: written.bodyStyle,
4987
+ text: text22,
4988
+ diagnostics: [...diagnostics, ...inTemplate(resolved, forEmailClients(written.diagnostics, options.emailClients))],
4989
+ ...blocks ? { blocks } : {}
4990
+ };
4402
4991
  }
4403
4992
 
4404
4993
  // src/utils/collect.ts
@@ -4429,7 +5018,7 @@ function isAsyncComponent(type) {
4429
5018
  }
4430
5019
  var SHARED2 = Symbol.for("@hyper-elements/react/render-builds");
4431
5020
  var using = globalThis[SHARED2] ?? (globalThis[SHARED2] = []);
4432
- var same3 = (a2, b) => a2.renderToString === b.renderToString;
5021
+ var same4 = (a2, b) => a2.renderToString === b.renderToString;
4433
5022
  function dispatcher() {
4434
5023
  const internals = React2__default.default;
4435
5024
  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;
@@ -4476,7 +5065,7 @@ function secondBuild(outer) {
4476
5065
  for (const requirer of requirers) {
4477
5066
  try {
4478
5067
  const build = requirer()?.(id);
4479
- if (build?.renderToStaticMarkup && !same3(build, defaultBuild__namespace) && ![null, outer].includes(dispatcherOf(build))) {
5068
+ if (build?.renderToStaticMarkup && !same4(build, defaultBuild__namespace) && ![null, outer].includes(dispatcherOf(build))) {
4480
5069
  second = build;
4481
5070
  break search;
4482
5071
  }
@@ -4490,11 +5079,11 @@ function secondBuild(outer) {
4490
5079
  var buildOf = (outer) => outer === null ? void 0 : [defaultBuild__namespace, second].find((b) => !!b && dispatcherOf(b) === outer);
4491
5080
  function withBuild(render) {
4492
5081
  const outer = dispatcher();
4493
- let build = current2();
5082
+ let build = current3();
4494
5083
  const theirs = using.length === 0 || collecting() !== void 0 ? buildOf(outer) : void 0;
4495
5084
  const busy = [];
4496
5085
  if (theirs) {
4497
- const free = (b) => !same3(b, theirs) && !using.some((u) => same3(u, b));
5086
+ const free = (b) => !same4(b, theirs) && !using.some((u) => same4(u, b));
4498
5087
  const other = free(defaultBuild__namespace) ? defaultBuild__namespace : secondBuild(outer);
4499
5088
  if (other && free(other)) {
4500
5089
  busy.push(theirs);
@@ -4519,10 +5108,10 @@ function rendersHere(build) {
4519
5108
  return false;
4520
5109
  }
4521
5110
  }
4522
- function current2() {
5111
+ function current3() {
4523
5112
  return using[using.length - 1] ?? defaultBuild__namespace;
4524
5113
  }
4525
- var renderToString = (element2, options) => current2().renderToString(element2, options);
5114
+ var renderToString = (element2, options) => current3().renderToString(element2, options);
4526
5115
 
4527
5116
  // src/utils/render-scope.ts
4528
5117
  function withRenderScope(render) {
@@ -4571,7 +5160,7 @@ function withoutAsync(children, collection) {
4571
5160
  }
4572
5161
  var ENTITIES2 = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#x27;": "'" };
4573
5162
  var unescape = (text4) => text4.replace(/&amp;|&lt;|&gt;|&quot;|&#x27;/g, (entity) => ENTITIES2[entity]);
4574
- var NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "style", "mobile", "hover", "hidden"]);
5163
+ var NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "style", "hidden"]);
4575
5164
  function toJsonNode({ type, props }, diagnostics) {
4576
5165
  const node = { type, children: [] };
4577
5166
  const semantic = {};
@@ -4696,10 +5285,10 @@ function collectTemplate(element2) {
4696
5285
  path: "jsx"
4697
5286
  });
4698
5287
  }
4699
- return { document: null, diagnostics, components };
5288
+ return { document: null, diagnostics, components, collected: null };
4700
5289
  }
4701
5290
  const result = checkDocument({ root: top[0] }, new Registry(components));
4702
- return { document: result.document, diagnostics: [...diagnostics, ...result.diagnostics], components };
5291
+ return { document: result.document, diagnostics: [...diagnostics, ...result.diagnostics], components, collected: top[0] };
4703
5292
  }
4704
5293
 
4705
5294
  // src/primitives/components.tsx
@@ -4770,9 +5359,8 @@ function propsOf3(node) {
4770
5359
  return {
4771
5360
  ...node.id !== void 0 ? { id: node.id } : {},
4772
5361
  ...node.props,
4773
- ...node.style && Object.keys(node.style).length ? { style: node.style } : {},
4774
- ...node.mobile ? { mobile: node.mobile } : {},
4775
- ...node.hover ? { hover: node.hover } : {},
5362
+ // Phone and hover styles inside the style, as a template writes them now.
5363
+ ...node.style && Object.keys(node.style).length || node.mobile || node.hover ? { style: { ...node.style, ...node.mobile ? { mobile: node.mobile } : {}, ...node.hover ? { hover: node.hover } : {} } } : {},
4776
5364
  ...node.hidden ? { hidden: node.hidden } : {}
4777
5365
  };
4778
5366
  }
@@ -4826,15 +5414,15 @@ function text2(value, flat) {
4826
5414
  return kept ? value : `{${JSON.stringify(value)}}`;
4827
5415
  }
4828
5416
  var inline = (node) => (node.children ?? []).some((child) => typeof child === "string" || ["Span", "Link", "LineBreak", "MergeTag", "PageNumber"].includes(child.type));
4829
- function element(node, indent, flat, path, report2) {
5417
+ function element(node, indent, flat, path, report) {
4830
5418
  if (!TYPE_NAME.test(node.type)) {
4831
- report2.push(`${path}: ${JSON.stringify(node.type)} isn't a component's name: it's left out of the code`);
5419
+ report.push(`${path}: ${JSON.stringify(node.type)} isn't a component's name: it's left out of the code`);
4832
5420
  return flat ? "" : "{/* left out: not a component's name */}";
4833
5421
  }
4834
5422
  const name = tagName(node.type);
4835
- const attributes5 = Object.entries(propsOf3(node)).filter(([key]) => PROP_NAME.test(key) || (report2.push(`${path}: a prop named ${JSON.stringify(key)} isn't one JSX can write: it's left out of the code`), false)).map(([key, value]) => ` ${attribute(key, value)}`).join("");
5423
+ const attributes5 = Object.entries(propsOf3(node)).filter(([key]) => PROP_NAME.test(key) || (report.push(`${path}: a prop named ${JSON.stringify(key)} isn't one JSX can write: it's left out of the code`), false)).map(([key, value]) => ` ${attribute(key, value)}`).join("");
4836
5424
  const children = node.children ?? [];
4837
- const child = (c, i, inner2, oneLine) => typeof c === "string" ? text2(c, oneLine) : element(c, inner2, oneLine, `${path}.children[${i}]`, report2);
5425
+ const child = (c, i, inner2, oneLine) => typeof c === "string" ? text2(c, oneLine) : element(c, inner2, oneLine, `${path}.children[${i}]`, report);
4838
5426
  if (!children.length) return `<${name}${attributes5} />`;
4839
5427
  if (flat || inline(node)) return `<${name}${attributes5}>${children.map((c, i) => child(c, i, indent, true)).join("")}</${name}>`;
4840
5428
  const inner = `${indent} `;
@@ -4842,9 +5430,9 @@ function element(node, indent, flat, path, report2) {
4842
5430
  ${children.map((c, i) => `${inner}${child(c, i, inner, false)}`).join("\n")}
4843
5431
  ${indent}</${name}>`;
4844
5432
  }
4845
- function printTemplate(root, name = "Template", report2 = []) {
5433
+ function printTemplate(root, name = "Template", report = []) {
4846
5434
  if (!IDENTIFIER.test(name)) {
4847
- report2.push(`the component's name ${JSON.stringify(name)} isn't one JavaScript can write: it's named Template`);
5435
+ report.push(`the component's name ${JSON.stringify(name)} isn't one JavaScript can write: it's named Template`);
4848
5436
  name = "Template";
4849
5437
  }
4850
5438
  const used = /* @__PURE__ */ new Set();
@@ -4861,7 +5449,7 @@ function printTemplate(root, name = "Template", report2 = []) {
4861
5449
 
4862
5450
  export default function ${name}() {
4863
5451
  return (
4864
- ${element(root, " ", false, "root", report2)}
5452
+ ${element(root, " ", false, "root", report)}
4865
5453
  );
4866
5454
  }
4867
5455
  `;
@@ -5382,7 +5970,7 @@ function readHidden2(tool, type, values, say) {
5382
5970
  // src/builder/malformed.ts
5383
5971
  var DEPTH = 32;
5384
5972
  var coerces = (value) => ["toString", "valueOf"].some((key) => Object.hasOwn(value, key) && typeof value[key] !== "function");
5385
- function withoutMalformed(design, report2) {
5973
+ function withoutMalformed(design, report) {
5386
5974
  const found = [];
5387
5975
  const walk = (value, path) => {
5388
5976
  if (!value || typeof value !== "object" || path.length > DEPTH) return;
@@ -5400,7 +5988,7 @@ function withoutMalformed(design, report2) {
5400
5988
  const last = path[path.length - 1];
5401
5989
  if (Array.isArray(holder)) holder[Number(last)] = null;
5402
5990
  else if (holder) delete holder[last];
5403
- report2.push(`${path.map((key, i) => /^\d+$/.test(key) ? `[${key}]` : i ? `.${key}` : key).join("")}: a value whose own toString or valueOf isn't a function can't be read: left out`);
5991
+ report.push(`${path.map((key, i) => /^\d+$/.test(key) ? `[${key}]` : i ? `.${key}` : key).join("")}: a value whose own toString or valueOf isn't a function can't be read: left out`);
5404
5992
  }
5405
5993
  return copy2;
5406
5994
  }
@@ -5409,8 +5997,8 @@ function withoutMalformed(design, report2) {
5409
5997
  var ZERO = { top: 0, right: 0, bottom: 0, left: 0 };
5410
5998
  var obj = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
5411
5999
  var str = (value) => typeof value === "string" ? value : void 0;
5412
- var same4 = (a2, b) => JSON.stringify(a2)?.toLowerCase() === JSON.stringify(b)?.toLowerCase();
5413
- var unless = (value, taken) => value === void 0 || same4(value, taken) ? void 0 : value;
6000
+ var same5 = (a2, b) => JSON.stringify(a2)?.toLowerCase() === JSON.stringify(b)?.toLowerCase();
6001
+ var unless = (value, taken) => value === void 0 || same5(value, taken) ? void 0 : value;
5414
6002
  var lineHeight4 = (value) => {
5415
6003
  const read2 = lineHeight3(value);
5416
6004
  return typeof read2 === "string" && read2.endsWith("%") ? Math.round(parseFloat(read2) * 100) / 1e4 : read2;
@@ -5459,7 +6047,7 @@ function phoneText2(values) {
5459
6047
  }
5460
6048
 
5461
6049
  // src/builder/read-runs.ts
5462
- var TAG2 = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)([^>]*?)\/?\s*>/g;
6050
+ var TAG3 = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)([^>]*?)\/?\s*>/g;
5463
6051
  var ATTRIBUTE = /([a-zA-Z-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
5464
6052
  var ENTITIES3 = {
5465
6053
  amp: "&",
@@ -5600,16 +6188,16 @@ function parseRuns(html2, { weight = 400, links = true, builder = false } = {})
5600
6188
  const left = [];
5601
6189
  if (/<[!?]|<\/(?![a-zA-Z])/.test(html2)) left.push("a comment or a processing instruction");
5602
6190
  const lines = [];
5603
- let current3 = { tag: "", node: null, children: [], weight };
5604
- const stack = [current3];
6191
+ let current4 = { tag: "", node: null, children: [], weight };
6192
+ const stack = [current4];
5605
6193
  const top = () => stack[stack.length - 1];
5606
6194
  const endParagraph = (last2 = false) => {
5607
6195
  if (stack.length > 1) left.push("a paragraph inside text that's styled");
5608
- const runs = tidy2(current3.children);
6196
+ const runs = tidy2(current4.children);
5609
6197
  if (runs.length > 1 && isBreak(runs[runs.length - 1]) && !(builder && last2)) runs.pop();
5610
6198
  if (runs.length) lines.push(runs.length === 1 && isBreak(runs[0]) ? [] : runs);
5611
- current3 = { tag: "", node: null, children: [], weight };
5612
- stack.splice(0, stack.length, current3);
6199
+ current4 = { tag: "", node: null, children: [], weight };
6200
+ stack.splice(0, stack.length, current4);
5613
6201
  };
5614
6202
  let last = 0;
5615
6203
  let afterParagraph = false;
@@ -5619,7 +6207,7 @@ function parseRuns(html2, { weight = 400, links = true, builder = false } = {})
5619
6207
  const value = decode3(raw2, left);
5620
6208
  if (value) top().children.push(value);
5621
6209
  };
5622
- for (const match of html2.matchAll(TAG2)) {
6210
+ for (const match of html2.matchAll(TAG3)) {
5623
6211
  const tag = match[2].toLowerCase();
5624
6212
  text4(html2.slice(last, match.index), tag === "p" || tag === "div");
5625
6213
  last = match.index + match[0].length;
@@ -5962,7 +6550,7 @@ function textJsonHtml(textJson) {
5962
6550
  }
5963
6551
 
5964
6552
  // src/builder/text.ts
5965
- var TAG3 = /<\s*(\/?)\s*([a-zA-Z][a-zA-Z0-9]*)([^>]*?)(\/?)\s*>/g;
6553
+ var TAG4 = /<\s*(\/?)\s*([a-zA-Z][a-zA-Z0-9]*)([^>]*?)(\/?)\s*>/g;
5966
6554
  var styleOf2 = (attrs2) => /\bstyle\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(attrs2)?.slice(1).find((v) => v !== void 0) ?? "";
5967
6555
  var declarations2 = (css3) => Object.fromEntries(css3.split(";").map((d) => d.split(":")).filter((p) => p.length >= 2 && p[0].trim()).map(([name, ...rest]) => [name.trim().toLowerCase(), rest.join(":").trim()]));
5968
6556
  var ATTRIBUTES = `(?:[^>"']|"[^"]*"|'[^']*')*`;
@@ -6051,7 +6639,7 @@ var DEEPEST = 64;
6051
6639
  var VOID3 = /* @__PURE__ */ new Set(["br", "img", "hr", "input", "meta", "link", "wbr", "source", "area", "col", "embed", "param", "track"]);
6052
6640
  function tooDeep(html2) {
6053
6641
  let depth = 0;
6054
- for (const match of html2.matchAll(TAG3)) {
6642
+ for (const match of html2.matchAll(TAG4)) {
6055
6643
  if (match[1]) depth = Math.max(0, depth - 1);
6056
6644
  else if (!match[4] && !VOID3.has(match[2].toLowerCase()) && ++depth > DEEPEST) return true;
6057
6645
  }
@@ -6076,7 +6664,7 @@ function readText(html2, { weight = 400, pages = false, links = true, fontSize =
6076
6664
  inline2 = "";
6077
6665
  };
6078
6666
  let at = 0;
6079
- for (const match of [...html2.matchAll(TAG3)]) {
6667
+ for (const match of [...html2.matchAll(TAG4)]) {
6080
6668
  if (match.index < at) continue;
6081
6669
  const tag = match[2].toLowerCase();
6082
6670
  if (match[1] || !/^(p|ul|ol|h[1-6])$/.test(tag)) continue;
@@ -6173,7 +6761,7 @@ function readList(tag, attrs2, inner, runs, fontSize, left) {
6173
6761
  const items2 = [];
6174
6762
  let gap;
6175
6763
  let at = 0;
6176
- for (const match of [...inner.matchAll(TAG3)]) {
6764
+ for (const match of [...inner.matchAll(TAG4)]) {
6177
6765
  if (match.index < at || match[1] || match[2].toLowerCase() !== "li") continue;
6178
6766
  const end = closing(inner, "li", match.index + match[0].length);
6179
6767
  if (end === null) {
@@ -6419,14 +7007,14 @@ function sayUnread(tool, values, defaults, say) {
6419
7007
  const given = defaults ?? FRAMES[tool] ?? {};
6420
7008
  const known = /* @__PURE__ */ new Set([...NOTHING, ...read2.values, ...hidden2]);
6421
7009
  for (const [key, value] of Object.entries(values)) {
6422
- if (known.has(key) || tool === "body" && key.startsWith("popup") || empty(value) || Object.hasOwn(given, key) && same4(value, given[key])) continue;
7010
+ if (known.has(key) || tool === "body" && key.startsWith("popup") || empty(value) || Object.hasOwn(given, key) && same5(value, given[key])) continue;
6423
7011
  say(Object.hasOwn(WHAT, key) ? `its ${WHAT[key]}` : `its ${key} ${JSON.stringify(value) ?? ""} isn't read: left out`);
6424
7012
  }
6425
7013
  const override = values._override && typeof values._override === "object" ? values._override : {};
6426
7014
  const phone = override.mobile && typeof override.mobile === "object" ? override.mobile : {};
6427
7015
  const phoneKnown = /* @__PURE__ */ new Set([...read2.phone, ...hidden2]);
6428
7016
  for (const [key, value] of Object.entries(phone)) {
6429
- if (phoneKnown.has(key) || empty(value) || same4(value, values[key])) continue;
7017
+ if (phoneKnown.has(key) || empty(value) || same5(value, values[key])) continue;
6430
7018
  say(`its ${key} on phones ${JSON.stringify(value) ?? ""} isn't read: left out`);
6431
7019
  }
6432
7020
  for (const device of Object.keys(override)) {
@@ -6674,8 +7262,8 @@ function fold(blocks, space) {
6674
7262
  if (!some(s)) return block2;
6675
7263
  const holds = ["Text", "Heading", "Divider", "Column"].includes(block2.type) && !BOX2.some((key) => block2.style?.[key] !== void 0);
6676
7264
  if (holds) return { ...block2, style: { ...block2.style, padding: add2(block2.style?.padding ?? ZERO2, s) } };
6677
- const { hidden: hidden2, ...shown3 } = block2;
6678
- return { type: "Column", style: { padding: s }, ...hidden2 ? { hidden: hidden2 } : {}, children: [shown3] };
7265
+ const { hidden: hidden2, ...shown2 } = block2;
7266
+ return { type: "Column", style: { padding: s }, ...hidden2 ? { hidden: hidden2 } : {}, children: [shown2] };
6679
7267
  });
6680
7268
  }
6681
7269
  function phoneOf(columnValues) {
@@ -6708,14 +7296,14 @@ function columnOf(columnValues, children, share3, say) {
6708
7296
  function stretched(columns, stacks) {
6709
7297
  if (columns.length < 2) return { style: {} };
6710
7298
  const style = (column) => column.style ?? {};
6711
- const same5 = (values) => values[0] !== void 0 && values.every((value) => JSON.stringify(value) === JSON.stringify(values[0]));
7299
+ const same6 = (values) => values[0] !== void 0 && values.every((value) => JSON.stringify(value) === JSON.stringify(values[0]));
6712
7300
  const out = {};
6713
7301
  const backgrounds = columns.map((column) => style(column).backgroundColor);
6714
- if (same5(backgrounds) && columns.every((column) => style(column).borderRadius === void 0)) out.backgroundColor = backgrounds[0];
7302
+ if (same6(backgrounds) && columns.every((column) => style(column).borderRadius === void 0)) out.backgroundColor = backgrounds[0];
6715
7303
  const lines = {};
6716
7304
  for (const side2 of ["top", "bottom"]) {
6717
7305
  const each = columns.map((column) => style(column).border?.[side2]);
6718
- if (same5(each)) lines[side2] = each[0];
7306
+ if (same6(each)) lines[side2] = each[0];
6719
7307
  }
6720
7308
  if (!out.backgroundColor && !Object.keys(lines).length) return { style: {} };
6721
7309
  for (const column of columns) {
@@ -6812,13 +7400,13 @@ var defined3 = (record) => Object.fromEntries(Object.entries(record).filter(([,
6812
7400
  var wordsIn = (value, depth) => typeof value === "string" ? [value] : depth > 0 && value && typeof value === "object" ? Object.values(value).flatMap((held) => wordsIn(held, depth - 1)) : [];
6813
7401
  var ROOTS = { email: "Email", web: "Page", document: "Document" };
6814
7402
  var SIZES2 = /* @__PURE__ */ new Set(["A3", "A4", "A5", "Letter", "Legal", "Tabloid"]);
6815
- function readDesign(given, displayMode, report2, toSafeHtml) {
6816
- const design = withoutMalformed(given, report2);
7403
+ function readDesign(given, displayMode, report, toSafeHtml) {
7404
+ const design = withoutMalformed(given, report);
6817
7405
  const body = obj3(obj3(design).body);
6818
- if (!Object.keys(body).length) report2.push("the design has no body: an empty template");
7406
+ if (!Object.keys(body).length) report.push("the design has no body: an empty template");
6819
7407
  const values = { ...BodyDefaults, ...obj3(body.values) };
6820
7408
  const rowsOf = (key) => {
6821
- if (body[key] !== void 0 && !Array.isArray(body[key])) report2.push(`body.${key} isn't a list of rows: left out`);
7409
+ if (body[key] !== void 0 && !Array.isArray(body[key])) report.push(`body.${key} isn't a list of rows: left out`);
6822
7410
  return arr2(body[key]);
6823
7411
  };
6824
7412
  const rows = rowsOf("rows");
@@ -6826,15 +7414,15 @@ function readDesign(given, displayMode, report2, toSafeHtml) {
6826
7414
  const footers = rowsOf("footers");
6827
7415
  const breaks = rows.some((row) => arr2(obj3(row).columns).some((column) => arr2(obj3(column).contents).some((content2) => obj3(content2).type === "page_break")));
6828
7416
  const mode = displayMode && Object.hasOwn(ROOTS, displayMode) ? displayMode : headers.length || footers.length || breaks ? "document" : "email";
6829
- const root = rootOf(mode, values, (message) => report2.push(`body: ${message}`));
6830
- sayUnread("body", obj3(body.values), void 0, (message) => report2.push(`body: ${message}`));
7417
+ const root = rootOf(mode, values, (message) => report.push(`body: ${message}`));
7418
+ sayUnread("body", obj3(body.values), void 0, (message) => report.push(`body: ${message}`));
6831
7419
  const context = (where) => ({
6832
7420
  inherited: { color: root.style?.color ?? "#000000", fontFamily: root.style?.fontFamily ?? "Arial, Helvetica, sans-serif", fontWeight: root.style?.fontWeight ?? 400 },
6833
7421
  mode,
6834
7422
  dir: root.props?.dir ?? "ltr",
6835
7423
  safe: (html2) => toSafeHtml ? toSafeHtml(html2) : html2,
6836
7424
  document: mode === "document",
6837
- say: (message) => report2.push(`${where}: ${message}`)
7425
+ say: (message) => report.push(`${where}: ${message}`)
6838
7426
  });
6839
7427
  const children = [];
6840
7428
  const place = (list, part) => list.flatMap((row, r) => readRow(obj3(row), `${part}[${r}]`, { context, body: values, document: mode === "document", part: mode === "document" && part !== "rows" }));
@@ -6849,12 +7437,12 @@ function readDesign(given, displayMode, report2, toSafeHtml) {
6849
7437
  }
6850
7438
  const all = [...rows, ...headers, ...footers];
6851
7439
  const items2 = all.flatMap((row) => [row, ...arr2(obj3(row).columns).flatMap((column) => [column, ...arr2(obj3(column).contents)])]).map((item) => obj3(obj3(item).values));
6852
- if (items2.some((held) => held.hideMobile === true || held.hideDesktop === true)) report2.push("body: what it hides on phones or on desktop is written as an older design's, which the builder moves when it opens one: left out, so it shows. Open it in the builder and save it first");
7440
+ if (items2.some((held) => held.hideMobile === true || held.hideDesktop === true)) report.push("body: what it hides on phones or on desktop is written as an older design's, which the builder moves when it opens one: left out, so it shows. Open it in the builder and save it first");
6853
7441
  const ownFonts = new Set(arr2(root.props?.fonts).map((face) => obj3(face).family));
6854
7442
  const webFonts = [...new Set([values, ...items2].map((held) => obj3(held.fontFamily)).filter((family) => typeof family.url === "string" && family.url && !ownFonts.has(family.label)).map((family) => String(family.label ?? family.value)))];
6855
- if (webFonts.length) report2.push(`body: ${webFonts.join(", ")}: web fonts the builder loads from a stylesheet, which a root doesn't take (its fonts are font files): left out, so text shows in the next font of its stack`);
7443
+ if (webFonts.length) report.push(`body: ${webFonts.join(", ")}: web fonts the builder loads from a stylesheet, which a root doesn't take (its fonts are font files): left out, so text shows in the next font of its stack`);
6856
7444
  const tags = [...new Set(wordsIn(all, 12).flatMap((text4) => [...text4.matchAll(/\[\[\s*[\w.-]+\s*\]\]/g)].map((match) => match[0])))];
6857
- if (tags.length) report2.push(`body: ${tags.join(", ")}: design tags are filled in by the builder, from what it's given: written as they are`);
7445
+ if (tags.length) report.push(`body: ${tags.join(", ")}: design tags are filled in by the builder, from what it's given: written as they are`);
6858
7446
  return { ...root, children };
6859
7447
  }
6860
7448
  function rootOf(mode, values, say) {
@@ -6896,23 +7484,23 @@ function rootOf(mode, values, say) {
6896
7484
  // src/builder/check.ts
6897
7485
  var LINK = definitionOf("Link").props.href.kind;
6898
7486
  var holdsText = (type) => !!definitionOf(type)?.children?.text;
6899
- function itemLinks(value, key, path, report2) {
7487
+ function itemLinks(value, key, path, report) {
6900
7488
  if (!Array.isArray(value)) return value;
6901
7489
  return value.map((item, i) => {
6902
7490
  if (!item || typeof item !== "object" || Array.isArray(item) || !("href" in item) || LINK.parse(item.href).ok) return item;
6903
- report2.push(`${path}: its ${key}[${i}]'s link ${JSON.stringify(item.href)} isn't one Elements writes: it's "#"`);
7491
+ report.push(`${path}: its ${key}[${i}]'s link ${JSON.stringify(item.href)} isn't one Elements writes: it's "#"`);
6904
7492
  return { ...item, href: "#" };
6905
7493
  });
6906
7494
  }
6907
- function checkNode(node, path, report2, inText2 = false) {
7495
+ function checkNode(node, path, report, inText2 = false) {
6908
7496
  const definition = definitionOf(node.type);
6909
7497
  if (!definition) {
6910
- report2.push(`${path}: ${JSON.stringify(node.type)} isn't a component: left out`);
7498
+ report.push(`${path}: ${JSON.stringify(node.type)} isn't a component: left out`);
6911
7499
  return null;
6912
7500
  }
6913
7501
  const out = { ...node };
6914
7502
  if (out.id !== void 0 && !/^[A-Za-z][\w-]*$/.test(out.id)) {
6915
- report2.push(`${path}: its id ${JSON.stringify(out.id)} isn't one a template takes: left out`);
7503
+ report.push(`${path}: its id ${JSON.stringify(out.id)} isn't one a template takes: left out`);
6916
7504
  delete out.id;
6917
7505
  }
6918
7506
  for (const part of ["props", "style", "mobile", "hover"]) {
@@ -6921,7 +7509,7 @@ function checkNode(node, path, report2, inText2 = false) {
6921
7509
  const specs = part === "props" ? definition.props : definition.style;
6922
7510
  const kept = {};
6923
7511
  for (const [key, given] of Object.entries(values)) {
6924
- const value = part === "props" ? itemLinks(given, key, path, report2) : given;
7512
+ const value = part === "props" ? itemLinks(given, key, path, report) : given;
6925
7513
  const spec = Object.hasOwn(specs, key) ? specs[key] : void 0;
6926
7514
  const parsed = spec ? spec.kind.parse(value) : void 0;
6927
7515
  if (parsed?.ok) {
@@ -6930,35 +7518,35 @@ function checkNode(node, path, report2, inText2 = false) {
6930
7518
  }
6931
7519
  if (part === "props" && (key === "href" || key === "src")) {
6932
7520
  if (node.type === "Button") {
6933
- report2.push(`${path}: its link ${JSON.stringify(value)} isn't one Elements writes: it's "#"`);
7521
+ report.push(`${path}: its link ${JSON.stringify(value)} isn't one Elements writes: it's "#"`);
6934
7522
  kept.href = "#";
6935
7523
  continue;
6936
7524
  }
6937
7525
  if (node.type !== "Link") {
6938
- report2.push(`${path}: its ${key} ${JSON.stringify(value)} isn't one Elements writes: left out`);
7526
+ report.push(`${path}: its ${key} ${JSON.stringify(value)} isn't one Elements writes: left out`);
6939
7527
  return null;
6940
7528
  }
6941
- report2.push(`${path}: its ${key} ${JSON.stringify(value)} isn't one Elements writes: ${inText2 ? "its words" : "what it holds"} without it`);
6942
- const children = checkChildren(node.children ?? [], path, report2, true);
7529
+ report.push(`${path}: its ${key} ${JSON.stringify(value)} isn't one Elements writes: ${inText2 ? "its words" : "what it holds"} without it`);
7530
+ const children = checkChildren(node.children ?? [], path, report, true);
6943
7531
  return inText2 ? { type: "Span", children } : children;
6944
7532
  }
6945
- report2.push(`${path}: its ${part === "props" ? "" : `${part} `}${key} ${JSON.stringify(value)}${parsed && !parsed.ok ? ` (${parsed.message})` : ""} isn't one a ${node.type} takes: left out`);
7533
+ report.push(`${path}: its ${part === "props" ? "" : `${part} `}${key} ${JSON.stringify(value)}${parsed && !parsed.ok ? ` (${parsed.message})` : ""} isn't one a ${node.type} takes: left out`);
6946
7534
  }
6947
7535
  if (Object.keys(kept).length) out[part] = kept;
6948
7536
  else delete out[part];
6949
7537
  }
6950
- if (node.children) out.children = checkChildren(node.children, path, report2, holdsText(node.type));
7538
+ if (node.children) out.children = checkChildren(node.children, path, report, holdsText(node.type));
6951
7539
  return out;
6952
7540
  }
6953
- function checkChildren(children, path, report2, inText2) {
7541
+ function checkChildren(children, path, report, inText2) {
6954
7542
  return children.flatMap((child, i) => {
6955
7543
  if (typeof child === "string") return [child];
6956
- const checked = checkNode(child, `${path}.children[${i}]`, report2, inText2);
7544
+ const checked = checkNode(child, `${path}.children[${i}]`, report, inText2);
6957
7545
  return Array.isArray(checked) ? checked : checked ? [checked] : [];
6958
7546
  });
6959
7547
  }
6960
- function checkTemplate(root, report2) {
6961
- const checked = checkNode(root.type === "Document" ? withoutPhones(root) : root, "root", report2);
7548
+ function checkTemplate(root, report) {
7549
+ const checked = checkNode(root.type === "Document" ? withoutPhones(root) : root, "root", report);
6962
7550
  return checked && !Array.isArray(checked) ? checked : { type: root.type };
6963
7551
  }
6964
7552
  function withoutPhones(node) {
@@ -7634,7 +8222,8 @@ function renderSubtree(node, width, root, components, inherited) {
7634
8222
  const tokens2 = (n) => n.type === "PageNumber" ? { type: "MergeTag", props: { name: n.props?.total ? "page_count" : "page_number" } } : n.children ? { ...n, children: n.children.map((child) => typeof child === "string" ? child : tokens2(child)) } : n;
7635
8223
  const own3 = tokens2(node);
7636
8224
  const around = { ...inherited, ...drawn < 200 ? { width: drawn } : {} };
7637
- const page = { type: root.type === "Email" ? "Email" : "Page", props: { width: Math.max(200, drawn) }, style: root.style, children: [Object.keys(around).length ? { type: "Column", style: around, children: [own3] } : own3] };
8225
+ const breakpoints2 = root.type === "Page" && root.props?.breakpoints ? { breakpoints: root.props.breakpoints } : {};
8226
+ const page = { type: root.type === "Email" ? "Email" : "Page", props: { width: Math.max(200, drawn), ...breakpoints2 }, style: root.style, children: [Object.keys(around).length ? { type: "Column", style: around, children: [own3] } : own3] };
7638
8227
  const { head, body } = renderTemplate({ root: page }, { components });
7639
8228
  const style = /<style>[\s\S]*?<\/style>/.exec(head)?.[0] ?? "";
7640
8229
  const filled = body.replace(/^<main style="/, '<main style="width:100%;');
@@ -7646,14 +8235,14 @@ var holdsPageNumber = (node) => node.type === "PageNumber" || (node.children ??
7646
8235
  var PAINT2 = ["backgroundColor", "border", "borderRadius"];
7647
8236
  var BROWSER_TEXT = { fontSize: 16, fontStyle: "normal", letterSpacing: 0, textAlign: "left", textTransform: "none" };
7648
8237
  function raise(element2) {
7649
- const report2 = [];
8238
+ const report = [];
7650
8239
  const { document, diagnostics, components } = collectTemplate(element2);
7651
8240
  if (!document || diagnostics.some((d) => d.severity === "error")) return { body: null, report: diagnostics.filter((d) => d.severity === "error").map((d) => `${d.path}: ${d.message}`) };
7652
8241
  const root = document.root;
7653
8242
  if (root.type !== "Email" && root.type !== "Page" && root.type !== "Document") {
7654
8243
  return { body: null, report: [`root: the builder edits an Email, a Page or a Document, not a ${root.type}`] };
7655
8244
  }
7656
- if (holdsPageNumber(root)) report2.push("root: a PageNumber goes to the builder as its token ({{page_number}}), which the editor's PDF prints as it is: the builder has no page numbers yet");
8245
+ if (holdsPageNumber(root)) report.push("root: a PageNumber goes to the builder as its token ({{page_number}}), which the editor's PDF prints as it is: the builder has no page numbers yet");
7657
8246
  const prepared = prepareTemplate(document, { components });
7658
8247
  if (!prepared.resolved) return { body: null, report: prepared.diagnostics.filter((d) => d.severity === "error").map((d) => `${d.path}: ${d.message}`) };
7659
8248
  const resolvedRoot = prepared.resolved.root;
@@ -7670,7 +8259,7 @@ function raise(element2) {
7670
8259
  };
7671
8260
  const sayLeftOut = (tool, node, path, fields) => {
7672
8261
  const left = leftOut(tool, node, fields);
7673
- if (left.length) report2.push(`${path}: the builder's ${tool} has no place for ${left.join(", ")}: left out`);
8262
+ if (left.length) report.push(`${path}: the builder's ${tool} has no place for ${left.join(", ")}: left out`);
7674
8263
  };
7675
8264
  const unsized = (node, path) => childrenOf2(node, path).some(({ child, path: childPath }) => !child.hidden?.desktop && resolved.get(childPath)?.width === null);
7676
8265
  const childrenOf2 = (node, path) => (node.children ?? []).map((child, i) => ({ child, path: `${path}.children[${i}]` })).filter((c) => typeof c.child !== "string");
@@ -7689,11 +8278,11 @@ function raise(element2) {
7689
8278
  };
7690
8279
  const place = {
7691
8280
  at,
7692
- report: report2,
8281
+ report,
7693
8282
  put,
7694
8283
  sayLeftOut,
7695
8284
  locked: (node, path, why, around) => {
7696
- report2.push(`${path}: ${why}: ${root.type === "Document" ? `it's an HTML block of its own markup in the design: ${NOT_IN_DOCUMENTS}` : "it's an HTML block of its own markup in the builder (it can be moved, not edited)"}`);
8285
+ report.push(`${path}: ${why}: ${root.type === "Document" ? `it's an HTML block of its own markup in the design: ${NOT_IN_DOCUMENTS}` : "it's an HTML block of its own markup in the builder (it can be moved, not edited)"}`);
7697
8286
  const own3 = at(path);
7698
8287
  return put(lockedHtml(node, own3.width ?? own3.available ?? Number(resolvedRoot.width), root, components, around, inheritedText(path)));
7699
8288
  },
@@ -7827,7 +8416,7 @@ function raise(element2) {
7827
8416
  const own3 = at(path);
7828
8417
  sayLeftOut("row", child, path, ["style.backgroundColor", "style.padding", "style.border", "style.gap", "style.alignItems", ...TEXT_FOLDED]);
7829
8418
  const band = own3.style.backgroundColor !== void 0 ? { backgroundColor: own3.style.backgroundColor } : {};
7830
- if (child.mobile?.border !== void 0) report2.push(`${path}: the builder's row has no border on phones: left out`);
8419
+ if (child.mobile?.border !== void 0) report.push(`${path}: the builder's row has no border on phones: left out`);
7831
8420
  const sectionRows = rows(childrenOf2(child, path), own3, band, padding2(child), child);
7832
8421
  if (child.id && sectionRows[0]) put(sectionRows[0], builderId(child.id, "u_row"));
7833
8422
  body.push(...sectionRows.length ? sectionRows : [oneColumn([], own3, band, padding2(child), {}, hiding2(child))]);
@@ -7841,7 +8430,7 @@ function raise(element2) {
7841
8430
  } else if (child.type === "PageBreak") {
7842
8431
  flushLoose();
7843
8432
  const content2 = top.slice(i + 1).some((c) => !["PageHeader", "PageFooter", "PageBreak"].includes(c.child.type));
7844
- if (last === null || !content2) report2.push(`${path}: a page break with nothing before or after it is no page in Elements, and a blank page in the builder`);
8433
+ if (last === null || !content2) report.push(`${path}: a page break with nothing before or after it is no page in Elements, and a blank page in the builder`);
7845
8434
  body.push(pageBreak());
7846
8435
  last = "break";
7847
8436
  } else if (child.type === "PageHeader" || child.type === "PageFooter") {
@@ -7884,10 +8473,10 @@ function raise(element2) {
7884
8473
  const bodyHidden = { ...hiddenOf("body", root, ["props.margin"]), ...margin, ...title, backgroundColor: style.backgroundColor ?? "" };
7885
8474
  if (root.type === "Document") {
7886
8475
  const paper = { documentSize: root.props?.size ?? "A4", documentOrientation: root.props?.orientation ?? "portrait" };
7887
- return { body: { root: root.type, fields: { ...common, ...paper }, values: bodyHidden, rows: body, headers, footers }, report: report2 };
8476
+ return { body: { root: root.type, fields: { ...common, ...paper }, values: bodyHidden, rows: body, headers, footers }, report };
7888
8477
  }
7889
8478
  const previewText = root.type === "Email" && root.props?.previewText ? { previewText: String(root.props.previewText) } : {};
7890
- return { body: { root: root.type, fields: common, ...previewText, values: bodyHidden, rows: body }, report: report2 };
8479
+ return { body: { root: root.type, fields: common, ...previewText, values: bodyHidden, rows: body }, report };
7891
8480
  }
7892
8481
  function countRows(children) {
7893
8482
  let count = 0;
@@ -7933,10 +8522,10 @@ function nodeOf(node) {
7933
8522
 
7934
8523
  // src/builder/write.ts
7935
8524
  function writeDesign(element2) {
7936
- const { body, report: report2 } = raise(element2);
8525
+ const { body, report } = raise(element2);
7937
8526
  if (!body) throw new Error(`[Elements] This template can't go to the builder:
7938
- ${report2.join("\n")}`);
7939
- return { design: withFontStylesheets(writeParts(body)), report: report2 };
8527
+ ${report.join("\n")}`);
8528
+ return { design: withFontStylesheets(writeParts(body)), report };
7940
8529
  }
7941
8530
 
7942
8531
  // src/builder/index.ts
@@ -7944,10 +8533,10 @@ function toDesign(element2) {
7944
8533
  return writeDesign(element2);
7945
8534
  }
7946
8535
  function fromDesign(design, options = {}) {
7947
- const report2 = [];
7948
- const root = checkTemplate(uniqueIds(readDesign(design, options.displayMode, report2, options.toSafeHtml)), report2);
7949
- const code = printTemplate(root, options.name, report2);
7950
- return { element: toElement(root), code, report: report2 };
8536
+ const report = [];
8537
+ const root = checkTemplate(uniqueIds(readDesign(design, options.displayMode, report, options.toSafeHtml)), report);
8538
+ const code = printTemplate(root, options.name, report);
8539
+ return { element: toElement(root), code, report };
7951
8540
  }
7952
8541
 
7953
8542
  exports.fromDesign = fromDesign;