@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.js CHANGED
@@ -59,21 +59,26 @@ var Diagnostics = class {
59
59
  constructor() {
60
60
  this.list = [];
61
61
  }
62
- add(code, severity, message, path, nodeId) {
63
- this.list.push(nodeId === void 0 ? { code, severity, message, path } : { code, severity, message, path, nodeId });
62
+ add(code, severity, message, path, nodeId, extra = {}) {
63
+ const d = { code, severity, message, path };
64
+ if (nodeId !== void 0) d.nodeId = nodeId;
65
+ if (extra.clients) d.clients = [...extra.clients];
66
+ if (extra.fix) d.fix = extra.fix;
67
+ if (extra.see) d.see = extra.see;
68
+ this.list.push(d);
64
69
  }
65
70
  error(code, message, path, nodeId) {
66
71
  this.add(code, "error", message, path, nodeId);
67
72
  }
68
- warning(code, message, path, nodeId) {
69
- this.add(code, "warning", message, path, nodeId);
73
+ warning(code, message, path, nodeId, extra) {
74
+ this.add(code, "warning", message, path, nodeId, extra);
70
75
  }
71
76
  get hasErrors() {
72
77
  return this.list.some((d) => d.severity === "error");
73
78
  }
74
79
  };
75
80
  function formatDiagnostics(diagnostics) {
76
- return diagnostics.map((d) => `${d.severity} ${d.code} at ${d.path}: ${d.message}`).join("\n");
81
+ 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");
77
82
  }
78
83
  var ElementsError = class extends Error {
79
84
  constructor(diagnostics) {
@@ -84,6 +89,64 @@ ${formatDiagnostics(errors)}`);
84
89
  this.diagnostics = diagnostics;
85
90
  }
86
91
  };
92
+ var captured = (against, wordsWithin4px, lineBreaks) => ({ elements: "0.3.0", against, captured: "2026-11-08", wordsWithin4px, lineBreaks });
93
+ var tables = (name) => ({ name, widths: {}, dark: false, approximate: true, measured: null });
94
+ var EMAIL_CLIENTS = {
95
+ 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) },
96
+ "outlook-com": { name: "Outlook.com and new Outlook", widths: { desktop: 820 }, dark: false, approximate: false, measured: captured("Outlook on the web", 1, 1) },
97
+ "outlook-mac": tables("Outlook for Mac"),
98
+ "outlook-app": tables("Outlook app (iOS and Android)"),
99
+ gmail: { name: "Gmail", widths: { desktop: 820 }, dark: false, approximate: false, measured: captured("Gmail on the web", 1, 1) },
100
+ "gmail-app": { name: "Gmail app", widths: { phone: 375 }, dark: false, approximate: true, measured: null },
101
+ "gmail-app-other": { name: "Gmail app, another account", widths: { phone: 375 }, dark: false, approximate: true, measured: null },
102
+ "apple-mail": { name: "Apple Mail", widths: { desktop: 820 }, dark: true, approximate: false, measured: captured("Mail 16 on macOS 26", 0.999, 0.999) },
103
+ yahoo: tables("Yahoo Mail"),
104
+ aol: tables("AOL Mail"),
105
+ "samsung-email": tables("Samsung Email"),
106
+ thunderbird: tables("Thunderbird"),
107
+ protonmail: tables("Proton Mail"),
108
+ fastmail: tables("Fastmail"),
109
+ hey: tables("HEY"),
110
+ gmx: tables("GMX"),
111
+ "web-de": tables("WEB.DE"),
112
+ orange: tables("Orange"),
113
+ sfr: tables("SFR"),
114
+ laposte: tables("La Poste"),
115
+ "free-fr": tables("Free"),
116
+ "t-online-de": tables("T-Online"),
117
+ "mail-ru": tables("Mail.ru"),
118
+ ionos: tables("IONOS (1&1)"),
119
+ "wp-pl": tables("WP Poczta"),
120
+ rainloop: tables("RainLoop")
121
+ };
122
+ var DEFAULT_EMAIL_CLIENTS = ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "apple-mail", "yahoo", "aol", "samsung-email"];
123
+ var caniemail = (feature) => `https://www.caniemail.com/features/${feature}/`;
124
+ function checkEmailClients(clients) {
125
+ if (clients === void 0) return;
126
+ const unknown = Array.isArray(clients) ? clients.filter((c) => !Object.prototype.hasOwnProperty.call(EMAIL_CLIENTS, c)) : [clients];
127
+ 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(", ")}`);
128
+ }
129
+ function forEmailClients(diagnostics, selection = DEFAULT_EMAIL_CLIENTS) {
130
+ const out = [];
131
+ const seen = /* @__PURE__ */ new Map();
132
+ for (const d of diagnostics) {
133
+ if (!d.clients) {
134
+ out.push(d);
135
+ continue;
136
+ }
137
+ const clients = d.clients.filter((c) => selection.includes(c));
138
+ if (!clients.length) continue;
139
+ const key = JSON.stringify([d.code, d.path, d.severity, d.message]);
140
+ const same42 = seen.get(key);
141
+ if (same42) same42.clients = [.../* @__PURE__ */ new Set([...same42.clients, ...clients])];
142
+ else {
143
+ const kept = { ...d, clients };
144
+ seen.set(key, kept);
145
+ out.push(kept);
146
+ }
147
+ }
148
+ return out;
149
+ }
87
150
  var ok = (value) => ({ ok: true, value });
88
151
  var fail = (message) => ({ ok: false, message });
89
152
  var show = (input) => {
@@ -669,6 +732,26 @@ function attributes() {
669
732
  jsonSchema: () => ({ type: "object", additionalProperties: { type: "string" } })
670
733
  });
671
734
  }
735
+ var MIN_PHONE_WIDTH = 240;
736
+ function breakpoints() {
737
+ const accepts = `{ mobile }: the phone width in px (from ${MIN_PHONE_WIDTH})`;
738
+ return kind({
739
+ name: "breakpoints",
740
+ editor: "number",
741
+ accepts,
742
+ parse(input) {
743
+ if (!isObject(input)) return fail(`expected ${accepts}, not ${show(input)}`);
744
+ const unknown = Object.keys(input).find((key) => key !== "mobile");
745
+ if (unknown !== void 0) return fail(`${show(unknown)} isn't a breakpoint a Page has: it has mobile`);
746
+ const { mobile } = input;
747
+ if (mobile === void 0) return ok({});
748
+ 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)}`);
749
+ return ok({ mobile });
750
+ },
751
+ empty: (value) => value.mobile === void 0,
752
+ jsonSchema: () => ({ type: "object", properties: { mobile: { type: "integer", minimum: MIN_PHONE_WIDTH } }, additionalProperties: false })
753
+ });
754
+ }
672
755
  function linkParts(value) {
673
756
  return typeof value === "string" ? { href: value } : value;
674
757
  }
@@ -681,6 +764,54 @@ function defineComponent(definition) {
681
764
  function prop(kind2, options) {
682
765
  return { kind: kind2, ...options };
683
766
  }
767
+ var MOBILE = {
768
+ name: "mobile",
769
+ on: "on phones",
770
+ for: "for phones",
771
+ roots: (root) => !!root?.responsive,
772
+ notAllowed: { code: "mobile/not-supported", message: "Phone styles are for an Email or a Page, which have a phone width" },
773
+ sheet: { media: "phone", selector: "&" },
774
+ allows: (spec) => !!spec.mobile,
775
+ support: { web: { level: "full", source: "writer" }, email: { level: "full", source: "writer" } },
776
+ styles: {
777
+ // The email's phone text styles are rules that override a size; "normal" isn't one.
778
+ lineHeight: { email: { level: "none", values: ["normal"], shows: "the line height it has on desktop", source: "writer" } }
779
+ },
780
+ outlookApplies: false,
781
+ hiddenKey: "mobile"
782
+ };
783
+ var HOVER = {
784
+ name: "hover",
785
+ on: "on hover",
786
+ for: "for hover",
787
+ // Keyboard focus gets the same styles as the pointer: a link or button reached by Tab shows its hover colors.
788
+ // A box around the link (an email Button's cell, its background) takes them while the link in it has focus.
789
+ sheet: { media: "all", selector: "&:hover,&:focus-visible", within: "&:hover,&:focus-within", level: "hover" },
790
+ allows: (_, key, definition) => !!definition.hover?.includes(key),
791
+ support: {
792
+ web: { level: "full", source: "css" },
793
+ // On type selectors only in Outlook.com and Outlook for Mac (Elements' are classes); the phone apps' results don't
794
+ // matter, as nothing hovers there.
795
+ email: {
796
+ level: "partial",
797
+ shows: "no change (where it shows, keyboard focus gets it too)",
798
+ source: "caniemail",
799
+ feature: "css-pseudo-class-hover",
800
+ clients: ["outlook", "outlook-com", "outlook-mac", "gmx", "web-de", "sfr", "laposte", "mail-ru"]
801
+ }
802
+ },
803
+ paged: (node, where) => `A hover isn't possible in ${where}: nothing points at it. The ${node.type} shows its own colors`,
804
+ outlookApplies: false
805
+ };
806
+ var CONDITIONS = [MOBILE, HOVER];
807
+ var CONDITION_NAMES = new Set(CONDITIONS.map((condition) => condition.name));
808
+ function blockOf(node, condition) {
809
+ const block2 = node[condition.name];
810
+ return block2 && Object.keys(block2).length ? block2 : void 0;
811
+ }
812
+ function shownAnywhere(node) {
813
+ return !(node.hidden?.mobile && node.hidden?.desktop);
814
+ }
684
815
  var writer = { level: "full", source: "writer" };
685
816
  var css = { level: "full", source: "css" };
686
817
  var textStyle = {
@@ -689,7 +820,12 @@ var textStyle = {
689
820
  fontFamily: { kind: fontFamily(), label: "Font", inherited: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
690
821
  // Word rounds sizes to half points (13px is 10pt): a fraction of a px.
691
822
  fontSize: { kind: length(["px"], { min: 1 }), label: "Font size", inherited: true, mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
692
- fontWeight: { kind: fontWeight(), label: "Font weight", inherited: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
823
+ fontWeight: {
824
+ kind: fontWeight(),
825
+ label: "Font weight",
826
+ inherited: true,
827
+ 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 }
828
+ },
693
829
  fontStyle: { kind: enumOf(["normal", "italic"]), label: "Font style", inherited: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
694
830
  // Written in px with mso-line-height-rule: exactly, which Word keeps (it drops unitless ones).
695
831
  lineHeight: { kind: lineHeight(), label: "Line height", inherited: true, mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
@@ -701,8 +837,21 @@ var textStyle = {
701
837
  support: { email: { level: "outlook", shows: "normal spacing: Word keeps letter spacing but doesn't draw it", source: "outlook-captures" }, web: css }
702
838
  },
703
839
  textAlign: { kind: enumOf(["left", "center", "right", "justify"]), label: "Alignment", inherited: true, mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
704
- textTransform: { kind: enumOf(["none", "uppercase", "lowercase", "capitalize"]), label: "Letter case", inherited: true, support: { email: { level: "full", source: "assumed" }, web: css } },
705
- textDecoration: { kind: enumOf(["none", "underline", "line-through"]), label: "Decoration", support: { email: { level: "full", source: "caniemail" }, web: css } }
840
+ textTransform: {
841
+ kind: enumOf(["none", "uppercase", "lowercase", "capitalize"]),
842
+ label: "Letter case",
843
+ inherited: true,
844
+ support: { email: { level: "outlook", values: ["lowercase"], shows: "the text as written: Word doesn't lowercase it", source: "caniemail", feature: "css-text-transform" }, web: css }
845
+ },
846
+ textDecoration: {
847
+ kind: enumOf(["none", "underline", "line-through"]),
848
+ label: "Decoration",
849
+ support: {
850
+ // Removed, so "none" leaves a link the client's own underline: said only where a line is asked for.
851
+ 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"] },
852
+ web: css
853
+ }
854
+ }
706
855
  };
707
856
  var inheritedTextStyle = (({ textDecoration: _, ...rest }) => rest)(textStyle);
708
857
  var boxStyle = {
@@ -715,7 +864,17 @@ var boxStyle = {
715
864
  padding: { kind: insets(), label: "Padding", mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
716
865
  backgroundColor: { kind: color(), label: "Background color", support: { email: writer, web: css } },
717
866
  border: { kind: border(), label: "Border", mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
718
- borderRadius: { kind: radius(), label: "Corner radius", support: { email: { level: "outlook", shows: "square corners", source: "caniemail" }, web: css } },
867
+ borderRadius: {
868
+ kind: radius(),
869
+ label: "Corner radius",
870
+ support: {
871
+ email: [
872
+ { level: "outlook", shows: "square corners", source: "caniemail", feature: "css-border-radius" },
873
+ { level: "partial", shows: "square corners", source: "caniemail", feature: "css-border-radius", clients: ["gmx", "web-de", "orange"] }
874
+ ],
875
+ web: css
876
+ }
877
+ },
719
878
  overflow: {
720
879
  kind: enumOf(["visible", "hidden"]),
721
880
  label: "Overflow",
@@ -923,6 +1082,15 @@ var Grid = defineComponent({
923
1082
  },
924
1083
  children: { categories: ["layout", "block"] },
925
1084
  sizing: "stretch",
1085
+ // Stacked on phones, every two of its children are its phone gap apart: there are no rows for a row gap.
1086
+ conditionSupport: {
1087
+ mobile: {
1088
+ rowGap: {
1089
+ web: { level: "none", when: (_, node) => node.props.stackOnMobile !== false, shows: "its phone gap between every two children", source: "writer" },
1090
+ email: { level: "none", when: (_, node) => node.props.stackOnMobile !== false, shows: "its phone gap between every two children", source: "writer" }
1091
+ }
1092
+ }
1093
+ },
926
1094
  targets: { email: {}, web: {} },
927
1095
  editor: {
928
1096
  groups: [
@@ -935,7 +1103,7 @@ var Grid = defineComponent({
935
1103
  var Layer = defineComponent({
936
1104
  type: "Layer",
937
1105
  label: "Layer",
938
- 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.",
1106
+ 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.",
939
1107
  categories: ["layout", "block"],
940
1108
  props: {},
941
1109
  style: {
@@ -967,13 +1135,27 @@ var Layer = defineComponent({
967
1135
  }
968
1136
  });
969
1137
  var WRITTEN3 = { email: { level: "full", source: "writer" }, web: { level: "full", source: "writer" } };
1138
+ 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"];
970
1139
  var ROOT_PROPS = {
971
1140
  title: prop(text(), { support: WRITTEN3, label: "Title", description: "The document's title (shown in a browser tab, and by some email clients)." }),
972
- lang: prop(text({ maxLength: 35 }), { support: WRITTEN3, label: "Language", description: 'Its language, as a BCP 47 tag ("en", "ar").' }),
973
- dir: prop(enumOf(["ltr", "rtl", "auto"]), { support: WRITTEN3, label: "Text direction" }),
1141
+ lang: prop(text({ maxLength: 35 }), {
1142
+ support: {
1143
+ ...WRITTEN3,
1144
+ 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"] }
1145
+ },
1146
+ label: "Language",
1147
+ description: 'Its language, as a BCP 47 tag ("en", "ar").'
1148
+ }),
1149
+ dir: prop(enumOf(["ltr", "rtl", "auto"]), {
1150
+ support: {
1151
+ ...WRITTEN3,
1152
+ email: { level: "partial", values: ["rtl", "auto"], shows: "left to right: the client removes the direction", source: "caniemail", feature: "html-dir", clients: ["gmx", "web-de"] }
1153
+ },
1154
+ label: "Text direction"
1155
+ }),
974
1156
  fonts: prop(fontFaces(), {
975
1157
  support: {
976
- 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" },
1158
+ 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 },
977
1159
  web: { level: "full", source: "writer" }
978
1160
  },
979
1161
  label: "Web fonts",
@@ -1113,7 +1295,10 @@ var PageNumber = defineComponent({
1113
1295
  style: {},
1114
1296
  children: {},
1115
1297
  sizing: "none",
1116
- targets: { web: {} }
1298
+ targets: { web: {} },
1299
+ // A Page's or a Canvas's text has no page to number.
1300
+ roots: ["Document"],
1301
+ notAllowed: { code: "document/page-number", message: (root) => `A PageNumber is a page's number in a Document; this ${root} has no pages` }
1117
1302
  });
1118
1303
  function documentPages(root) {
1119
1304
  const pages = [];
@@ -1249,6 +1434,11 @@ var Page = defineComponent({
1249
1434
  props: {
1250
1435
  width: prop(length(["px"], { min: 200 }), { support: WRITTEN6, label: "Content width", description: "The widest the content gets.", default: 1200 }),
1251
1436
  description: prop(text(), { support: WRITTEN6, label: "Description", description: "The page's meta description." }),
1437
+ breakpoints: prop(breakpoints(), {
1438
+ support: { web: { level: "full", source: "writer" } },
1439
+ label: "Breakpoints",
1440
+ description: "Where its phone styles start: mobile, the widest window they apply to, in px (480 where it's not set)."
1441
+ }),
1252
1442
  ...ROOT_PROPS
1253
1443
  },
1254
1444
  style: ROOT_STYLE,
@@ -1256,6 +1446,12 @@ var Page = defineComponent({
1256
1446
  children: { categories: ["section", "layout", "block"] },
1257
1447
  sizing: "none",
1258
1448
  responsive: true,
1449
+ // The page writer writes the root's phone gap, not its phone text styles.
1450
+ conditionSupport: {
1451
+ mobile: Object.fromEntries(
1452
+ ["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" } }])
1453
+ )
1454
+ },
1259
1455
  targets: { web: {} },
1260
1456
  editor: {
1261
1457
  groups: [
@@ -1499,6 +1695,7 @@ var Image = defineComponent({
1499
1695
  },
1500
1696
  children: {},
1501
1697
  sizing: "shrink",
1698
+ conditionSupport: { mobile: { border: { email: { level: "none", shows: "its desktop border", source: "writer" } } } },
1502
1699
  targets: { email: {}, web: {} },
1503
1700
  editor: { groups: [{ title: "Image", paths: ["props.src", "props.alt", "style.width"] }, { title: "Box", paths: ["style.border", "style.borderRadius"] }] }
1504
1701
  });
@@ -1629,6 +1826,7 @@ var TableCell = defineComponent({
1629
1826
  },
1630
1827
  children: { text: true, categories: ["inline"], types: ["Image"] },
1631
1828
  sizing: "none",
1829
+ conditionSupport: { mobile: { border: { email: { level: "none", shows: "its desktop border", source: "writer" } } } },
1632
1830
  targets: { email: {}, web: {} },
1633
1831
  editor: { groups: [{ title: "Cell", paths: ["props.colSpan", "style.width", "style.verticalAlign"] }, TEXT_EDITOR, { title: "Box", paths: ["style.padding", "style.backgroundColor", "style.border"] }] }
1634
1832
  });
@@ -1727,17 +1925,27 @@ function checkDocument(input, options) {
1727
1925
  const extra = Object.keys(input).filter((key) => key !== "root");
1728
1926
  for (const key of extra) d.error("document/unknown-key", `"${key}" isn't part of a document (root)`, key);
1729
1927
  const rootDefinition = isObject2(input.root) && typeof input.root.type === "string" ? registry.get(input.root.type) : void 0;
1730
- const context = { d, registry, used: /* @__PURE__ */ new Set(), responsive: !!rootDefinition?.responsive };
1928
+ const context = { d, registry, used: /* @__PURE__ */ new Set(), root: rootDefinition };
1731
1929
  const root = input.root === void 0 ? null : readNode(input.root, "root", null, context);
1732
1930
  if (input.root === void 0) d.error("document/root", 'A document has a root: { type: "Email" }, "Page", \u2026', "root");
1733
1931
  return { document: root ? { root } : null, diagnostics: d.list };
1734
1932
  }
1735
1933
  function checkFragment(input, options) {
1736
1934
  const d = new Diagnostics();
1737
- const context = { d, registry: options.registry, idPrefix: options.idPrefix, used: new Set(options.taken), responsive: options.responsive };
1935
+ const context = { d, registry: options.registry, idPrefix: options.idPrefix, used: new Set(options.taken), root: options.root };
1738
1936
  const node = readNode(input, options.path, options.parent, context);
1739
1937
  return { node, diagnostics: d.list };
1740
1938
  }
1939
+ function rootList(roots) {
1940
+ const named = roots.map((root) => a(root).replace(/^A/, "a"));
1941
+ return named.length > 1 ? `${named.slice(0, -1).join(", ")} or ${named[named.length - 1]}` : named[0] ?? "no root";
1942
+ }
1943
+ function rootTakes(roots, root) {
1944
+ return !roots || !root || roots.includes(root.type);
1945
+ }
1946
+ function sharesOutput(definition, root) {
1947
+ return Object.keys(definition.targets).some((target) => target in root.targets);
1948
+ }
1741
1949
  function allowedIn(parent, child) {
1742
1950
  const rule2 = parent.children;
1743
1951
  return !!rule2.types?.includes(child.type) || child.categories.some((category) => rule2.categories?.includes(category));
@@ -1763,7 +1971,29 @@ function editDistance(from, to) {
1763
1971
  }
1764
1972
  return row[to.length];
1765
1973
  }
1974
+ var OTHER_BREAKPOINTS = /* @__PURE__ */ new Set(["base", "initial", "xs", "sm", "md", "lg", "xl", "2xl", "tablet", "desktop", "wide", "small", "medium", "large"]);
1975
+ function conditionHint(definition, key) {
1976
+ const name = key.trim().replace(/^(&?:+|_+|\$|@)/, "").replace(/Style$/, "").toLowerCase();
1977
+ const own3 = CUSTOM_TYPE.test(definition.type);
1978
+ const hoverStyles = definition.hover ?? [];
1979
+ 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)`;
1980
+ 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 } }}`;
1981
+ if (name === "hover") return toHover;
1982
+ if (name === "mobile" || name === "phone" || name === "phones" || key.trim().startsWith("@media")) return toPhone;
1983
+ if (name === "focus" || name === "focus-visible" || name === "focusvisible" || name === "focus-within") {
1984
+ 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`;
1985
+ }
1986
+ if (OTHER_BREAKPOINTS.has(name)) {
1987
+ 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 }})`;
1988
+ }
1989
+ if (name === "active" || name === "pressed" || name === "press" || name === "visited" || name === "dark" || name === "light" || name === "print") {
1990
+ return `there are no ${name} styles: Elements has phone styles (mobile) and hover styles (hover) in style`;
1991
+ }
1992
+ return void 0;
1993
+ }
1766
1994
  function unknownProp(definition, key) {
1995
+ const condition = conditionHint(definition, key);
1996
+ if (condition) return `${a(definition.type)} has no prop "${key}": ${condition}`;
1767
1997
  if (Object.prototype.hasOwnProperty.call(definition.style, key)) return `${a(definition.type)} has no prop "${key}": it's a style, style={{ ${key}: \u2026 }}`;
1768
1998
  const known = Object.keys(definition.props);
1769
1999
  const meant = nearest(key, known);
@@ -1803,6 +2033,15 @@ function readNode(input, path, parent, context) {
1803
2033
  d.error("children/not-allowed", placementMessage(parent, definition), path);
1804
2034
  return null;
1805
2035
  }
2036
+ const root = context.root;
2037
+ if (root && !rootTakes(definition.roots, root) && sharesOutput(definition, root)) {
2038
+ const { code, message } = definition.notAllowed ?? {
2039
+ code: "component/not-supported",
2040
+ message: (under) => `${a(definition.type)} goes in ${rootList(definition.roots ?? [])}, not in ${a(under).replace(/^A/, "a")}`
2041
+ };
2042
+ d.error(code, message(root.type), path);
2043
+ return null;
2044
+ }
1806
2045
  let id;
1807
2046
  if (input.id !== void 0) {
1808
2047
  if (typeof input.id !== "string" || !ID.test(input.id)) {
@@ -1822,21 +2061,49 @@ function readNode(input, path, parent, context) {
1822
2061
  if (!NODE_KEYS.includes(key)) d.error("node/unknown-key", `"${key}" isn't part of a component (${NODE_KEYS.join(", ")})`, `${path}.${key}`, id);
1823
2062
  }
1824
2063
  const node = id === void 0 ? { type: definition.type } : { id, type: definition.type };
1825
- const props = readProps(input.props, definition, `${path}.props`, id, d);
2064
+ const props = readProps(input.props, definition, `${path}.props`, id, d, context.root);
1826
2065
  if (props) node.props = props;
1827
- const style = readStyles(input.style, definition, definition.style, `${path}.style`, id, d, "style", () => true);
1828
- if (style) node.style = style;
1829
- if (input.mobile !== void 0) {
1830
- 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);
1831
- else {
1832
- const mobile = readStyles(input.mobile, definition, definition.style, `${path}.mobile`, id, d, "mobile", (spec) => !!spec.mobile);
1833
- if (mobile) node.mobile = mobile;
2066
+ const lifted = {};
2067
+ let styleInput = input.style;
2068
+ if (isObject2(styleInput) && !CUSTOM_TYPE.test(definition.type)) {
2069
+ const rest = {};
2070
+ for (const [key, value] of Object.entries(styleInput)) {
2071
+ if (CONDITION_NAMES.has(key)) lifted[key] = value;
2072
+ else rest[key] = value;
1834
2073
  }
2074
+ styleInput = rest;
1835
2075
  }
1836
- if (input.hover !== void 0) {
1837
- const allowed = new Set(definition.hover ?? []);
1838
- const hover = readStyles(input.hover, definition, definition.style, `${path}.hover`, id, d, "hover", (_, key) => allowed.has(key));
1839
- if (hover) node.hover = hover;
2076
+ const style = readStyles(styleInput, definition, definition.style, `${path}.style`, id, d, "style", () => true, void 0, context.root);
2077
+ if (style) node.style = style;
2078
+ for (const condition of CONDITIONS) {
2079
+ const inStyle = lifted[condition.name];
2080
+ const given = input[condition.name];
2081
+ if (inStyle !== void 0 && given !== void 0) {
2082
+ d.error(
2083
+ "style/twice",
2084
+ `${condition.name} is given twice: as ${condition.name}={\u2026} and in style. Give it once, in style: style={{ ${condition.name}: { \u2026 } }}`,
2085
+ `${path}.style.${condition.name}`,
2086
+ id
2087
+ );
2088
+ continue;
2089
+ }
2090
+ let value = inStyle ?? given;
2091
+ if (value === void 0) continue;
2092
+ const at = inStyle !== void 0 ? `${path}.style.${condition.name}` : `${path}.${condition.name}`;
2093
+ if (condition.roots && !condition.roots(context.root)) {
2094
+ if (condition.notAllowed) d.error(condition.notAllowed.code, condition.notAllowed.message, at, id);
2095
+ continue;
2096
+ }
2097
+ if (isObject2(value) && Object.keys(value).some((key) => CONDITION_NAMES.has(key))) {
2098
+ const own3 = {};
2099
+ for (const [key, inner] of Object.entries(value)) {
2100
+ 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);
2101
+ else own3[key] = inner;
2102
+ }
2103
+ value = own3;
2104
+ }
2105
+ const read2 = readStyles(value, definition, definition.style, at, id, d, condition.name, (spec, key) => condition.allows(spec, key, definition), condition.for, context.root);
2106
+ if (read2) node[condition.name] = read2;
1840
2107
  }
1841
2108
  if (input.hidden !== void 0) {
1842
2109
  const hidden2 = readHidden(input.hidden, `${path}.hidden`, id, context);
@@ -1846,7 +2113,7 @@ function readNode(input, path, parent, context) {
1846
2113
  if (children.length) node.children = children;
1847
2114
  return node;
1848
2115
  }
1849
- function readProps(input, definition, path, id, d) {
2116
+ function readProps(input, definition, path, id, d, root) {
1850
2117
  const out = {};
1851
2118
  if (input !== void 0 && !isObject2(input)) {
1852
2119
  d.error("props/invalid", "props is an object", path, id);
@@ -1859,9 +2126,14 @@ function readProps(input, definition, path, id, d) {
1859
2126
  d.error("prop/unknown", unknownProp(definition, key), `${path}.${key}`, id);
1860
2127
  continue;
1861
2128
  }
2129
+ if (!rootTakes(spec.roots, root)) {
2130
+ 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);
2131
+ continue;
2132
+ }
1862
2133
  const parsed = spec.kind.parse(value);
1863
- if (parsed.ok) out[key] = parsed.value;
1864
- else d.error("prop/invalid", `${key}: ${parsed.message}`, `${path}.${key}`, id);
2134
+ if (parsed.ok) {
2135
+ if (!spec.kind.empty?.(parsed.value)) out[key] = parsed.value;
2136
+ } else d.error("prop/invalid", `${key}: ${parsed.message}`, `${path}.${key}`, id);
1865
2137
  }
1866
2138
  for (const [key, spec] of Object.entries(definition.props)) {
1867
2139
  if (spec.required && !(key in out) && !(isObject2(input) && input[key] !== void 0)) {
@@ -1878,6 +2150,8 @@ var UNSUPPORTED_CSS = {
1878
2150
  marginRight: "use padding",
1879
2151
  display: "the component sets it",
1880
2152
  position: "use a Layer for content placed over other content",
2153
+ backgroundImage: "put a Layer around it, its first child an Image (the Layer's height crops it to cover), the rest over it",
2154
+ background: "backgroundColor for a color; for an image, a Layer: its first child an Image, the rest over it",
1881
2155
  float: "use a Row to put things side by side",
1882
2156
  // Names Elements used before it took CSS's.
1883
2157
  align: "it's alignItems now",
@@ -1891,10 +2165,12 @@ function styleHint(definition, specs, key) {
1891
2165
  if (Object.prototype.hasOwnProperty.call(definition.props, key)) return `: it's a prop, ${key}={\u2026}`;
1892
2166
  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";
1893
2167
  if (UNSUPPORTED_CSS[key]) return `: ${UNSUPPORTED_CSS[key]}`;
2168
+ const condition = conditionHint(definition, key);
2169
+ if (condition) return `: ${condition}`;
1894
2170
  const meant = nearest(key, Object.keys(specs));
1895
2171
  return meant ? `: did you mean "${meant}"?` : "";
1896
2172
  }
1897
- function readStyles(input, definition, specs, path, id, d, what, allowed) {
2173
+ function readStyles(input, definition, specs, path, id, d, what, allowed, forLabel, root) {
1898
2174
  if (input === void 0) return void 0;
1899
2175
  if (!isObject2(input)) {
1900
2176
  d.error(`${what}/invalid`, `${what} is an object of style properties`, path, id);
@@ -1908,8 +2184,12 @@ function readStyles(input, definition, specs, path, id, d, what, allowed) {
1908
2184
  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);
1909
2185
  continue;
1910
2186
  }
2187
+ if (!rootTakes(spec.roots, root)) {
2188
+ 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);
2189
+ continue;
2190
+ }
1911
2191
  if (!allowed(spec, key)) {
1912
- d.error(`${what}/not-allowed`, what === "mobile" ? `"${key}" can't be set for phones` : `"${key}" can't be set for hover`, `${path}.${key}`, id);
2192
+ d.error(`${what}/not-allowed`, `"${key}" can't be set ${forLabel ?? "here"}`, `${path}.${key}`, id);
1913
2193
  continue;
1914
2194
  }
1915
2195
  const parsed = spec.kind.parse(value);
@@ -1920,7 +2200,7 @@ function readStyles(input, definition, specs, path, id, d, what, allowed) {
1920
2200
  }
1921
2201
  function readHidden(input, path, id, context) {
1922
2202
  const { d } = context;
1923
- if (!context.responsive) {
2203
+ if (!context.root?.responsive) {
1924
2204
  d.error("hidden/not-supported", "Hiding on phones or desktops is for an Email or a Page", path, id);
1925
2205
  return void 0;
1926
2206
  }
@@ -1931,7 +2211,7 @@ function readHidden(input, path, id, context) {
1931
2211
  const out = {};
1932
2212
  if (input.mobile === true) out.mobile = true;
1933
2213
  if (input.desktop === true) out.desktop = true;
1934
- if (out.mobile && out.desktop) d.warning("hidden/everywhere", "Hidden on phones and on desktops, it never shows", path, id);
2214
+ if (!shownAnywhere({ hidden: out })) d.warning("hidden/everywhere", "Hidden on phones and on desktops, it never shows", path, id);
1935
2215
  return Object.keys(out).length ? out : void 0;
1936
2216
  }
1937
2217
  function readChildren(input, definition, path, id, context) {
@@ -1964,6 +2244,22 @@ function readChildren(input, definition, path, id, context) {
1964
2244
  });
1965
2245
  return out;
1966
2246
  }
2247
+ var same = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
2248
+ function reportDeclared(d, node, at, key, value, declared, target, outlook = true) {
2249
+ if (!declared) return;
2250
+ for (const rule2 of [].concat(declared)) {
2251
+ if (rule2.level === "full") continue;
2252
+ if (rule2.level === "outlook" && !outlook) continue;
2253
+ if (rule2.values && !rule2.values.some((v) => same(v, value))) continue;
2254
+ if (rule2.when && !rule2.when(value, node)) continue;
2255
+ const asked = typeof value === "string" ? `${key}: "${value}"` : key;
2256
+ const clients = target === "email" ? rule2.clients ?? (rule2.level === "outlook" ? ["outlook"] : void 0) : void 0;
2257
+ const output = clients ? "some email clients" : target === "web" ? "a page" : target;
2258
+ 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}`];
2259
+ const [said, as] = rule2.source === "assumed" ? ["info", `${message} (not checked)`] : [severity, message];
2260
+ d.add(code, said, as, at, node.id, { clients, fix: rule2.fix, see: rule2.feature && caniemail(rule2.feature) });
2261
+ }
2262
+ }
1967
2263
  var MAX_DEPTH = 32;
1968
2264
  function templatePathsOf(document, paths) {
1969
2265
  const places = /* @__PURE__ */ new Map();
@@ -1980,7 +2276,7 @@ function templatePathsOf(document, paths) {
1980
2276
  return path;
1981
2277
  };
1982
2278
  }
1983
- function expandDocument(document, options) {
2279
+ function expandDocument(document, options, target) {
1984
2280
  const registry = registryOf(options);
1985
2281
  const d = new Diagnostics();
1986
2282
  const paths = /* @__PURE__ */ new WeakMap();
@@ -1992,7 +2288,20 @@ function expandDocument(document, options) {
1992
2288
  collect(document.root);
1993
2289
  const rootDefinition = registry.get(document.root.type);
1994
2290
  const responsive = !!rootDefinition?.responsive;
2291
+ const unshown = (node, path) => {
2292
+ const declared = target && registry.get(node.type)?.conditionSupport;
2293
+ if (!declared) return;
2294
+ for (const condition of CONDITIONS) {
2295
+ const block2 = blockOf(node, condition);
2296
+ const byStyle = declared[condition.name];
2297
+ if (!block2 || !byStyle || condition.hiddenKey && node.hidden?.[condition.hiddenKey]) continue;
2298
+ for (const [key, value] of Object.entries(block2)) {
2299
+ reportDeclared(d, { type: node.type, id: node.id, props: node.props ?? {} }, `${path}.${condition.name}.${key}`, key, value, byStyle[key]?.[target], target);
2300
+ }
2301
+ }
2302
+ };
1995
2303
  const visit = (given, path, depth, self = path) => {
2304
+ unshown(given, path);
1996
2305
  const arrange = registry.get(given.type)?.arrange;
1997
2306
  if (!arrange) return walk(given, path, self, depth);
1998
2307
  const origins = /* @__PURE__ */ new Map();
@@ -2019,27 +2328,34 @@ function expandDocument(document, options) {
2019
2328
  return void 0;
2020
2329
  }
2021
2330
  let raw2;
2331
+ const reported = [];
2022
2332
  try {
2023
2333
  const style = { ...definition.defaultStyle, ...node.style };
2024
- raw2 = definition.expand(propsWithDefaults(definition, node.props), { register: (custom) => registry.add(custom), style, responsive });
2334
+ raw2 = definition.expand(propsWithDefaults(definition, node.props), { register: (custom) => registry.add(custom), style, responsive, report: (diagnostic) => reported.push(diagnostic) });
2025
2335
  } catch (error) {
2026
2336
  d.error("custom/render", `${node.type} failed to render: ${error instanceof Error ? error.message : String(error)}`, path, node.id);
2027
2337
  return void 0;
2028
2338
  }
2339
+ const at = `${path}(${node.type})`;
2340
+ for (const diagnostic of reported) d.list.push({ ...diagnostic, path: diagnostic.path === "root" || diagnostic.path.startsWith("root.") ? at + diagnostic.path.slice(4) : at });
2029
2341
  if (Array.isArray(raw2) || typeof raw2 !== "object" || raw2 === null) {
2030
2342
  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);
2031
2343
  return void 0;
2032
2344
  }
2033
- const { node: expanded, diagnostics } = checkFragment(raw2, { parent, responsive, idPrefix: node.id ?? null, taken, registry, path: `${path}(${node.type})` });
2345
+ const { node: expanded, diagnostics } = checkFragment(raw2, { parent, root: rootDefinition, idPrefix: node.id ?? null, taken, registry, path: `${path}(${node.type})` });
2034
2346
  for (const diagnostic of diagnostics) d.list.push(diagnostic);
2035
2347
  if (!expanded) return void 0;
2036
2348
  collectIds(expanded);
2037
2349
  if (node.style?.flex !== void 0) expanded.style = { ...expanded.style, flex: node.style.flex };
2038
2350
  if (node.hidden) expanded.hidden = { ...expanded.hidden, ...node.hidden };
2039
2351
  if (node.id && !expanded.id && !CUSTOM_TYPE.test(node.type)) expanded.id = node.id;
2040
- for (const [key, value] of Object.entries(node.mobile ?? {})) {
2041
- if (registry.get(expanded.type)?.style[key]?.mobile) expanded.mobile = { [key]: value, ...expanded.mobile };
2042
- 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);
2352
+ for (const condition of CONDITIONS) {
2353
+ const rendered = registry.get(expanded.type);
2354
+ for (const [key, value] of Object.entries(node[condition.name] ?? {})) {
2355
+ const spec = rendered?.style[key];
2356
+ if (rendered && spec && condition.allows(spec, key, rendered)) expanded[condition.name] = { [key]: value, ...expanded[condition.name] };
2357
+ 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);
2358
+ }
2043
2359
  }
2044
2360
  const inner = registry.get(expanded.type);
2045
2361
  if (isCustom(inner)) return expandOne(expanded, inner, parent, `${path}(${node.type})`, depth + 1, self);
@@ -2491,11 +2807,11 @@ var TEXT_CSS = [
2491
2807
  ["textAlign", "text-align", (v) => v],
2492
2808
  ["textTransform", "text-transform", (v) => v]
2493
2809
  ];
2494
- var same = (a2, b) => a2 === b || JSON.stringify(a2) === JSON.stringify(b);
2810
+ var same2 = (a2, b) => a2 === b || JSON.stringify(a2) === JSON.stringify(b);
2495
2811
  function textDeclarations(text22, parent, always = []) {
2496
2812
  const out = [];
2497
2813
  for (const [key, property, write3] of TEXT_CSS) {
2498
- if (!parent || always.includes(key) || !same(text22[key], parent[key])) out.push([property, write3(text22[key])]);
2814
+ if (!parent || always.includes(key) || !same2(text22[key], parent[key])) out.push([property, write3(text22[key])]);
2499
2815
  }
2500
2816
  return out;
2501
2817
  }
@@ -2571,7 +2887,9 @@ function classesOf(name, value) {
2571
2887
  if (name === "class") return (Array.isArray(value) ? value : [value]).filter((v) => typeof v === "string" && v !== "");
2572
2888
  const text22 = styleText(value, true);
2573
2889
  if (!text22) return [];
2574
- return [name === "phone" ? rule("phone", `&{${text22}}`) : rule("all", `&:hover{${text22}}`, "hover")];
2890
+ if (name === "phone") return [rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}{${text22}}`)];
2891
+ const selector = name === "hoverWithin" ? HOVER.sheet.within ?? HOVER.sheet.selector : HOVER.sheet.selector;
2892
+ return [rule(HOVER.sheet.media, `${selector}{${text22}}`, HOVER.sheet.level)];
2575
2893
  }
2576
2894
  function attributes2(props) {
2577
2895
  const out = [];
@@ -2579,7 +2897,7 @@ function attributes2(props) {
2579
2897
  let slot = -1;
2580
2898
  for (const [name, value] of Object.entries(props)) {
2581
2899
  if (name === "children" || name === "style" || value === void 0 || value === null || value === false) continue;
2582
- if (name === "class" || name === "phone" || name === "hover") {
2900
+ if (name === "class" || name === "phone" || name === "hover" || name === "hoverWithin") {
2583
2901
  const found = classesOf(name, value);
2584
2902
  if (found.length && slot < 0) slot = out.push("") - 1;
2585
2903
  classes.push(...found);
@@ -2595,8 +2913,8 @@ function withId(element2, id) {
2595
2913
  if (!id) return element2;
2596
2914
  const tag = /^<[a-zA-Z0-9]+/.exec(element2.markup)?.[0];
2597
2915
  if (!tag) throw new Error("[Elements] An id goes on an element");
2598
- const insert = (markup2) => `${tag} id="${escapeAttr(id)}"${markup2.slice(tag.length)}`;
2599
- return new Html2(insert(element2.markup), { ...element2.options, start: element2.options.start && insert(element2.options.start) });
2916
+ const insert2 = (markup2) => `${tag} id="${escapeAttr(id)}"${markup2.slice(tag.length)}`;
2917
+ return new Html2(insert2(element2.markup), { ...element2.options, start: element2.options.start && insert2(element2.options.start) });
2600
2918
  }
2601
2919
  function h(type, props, ...children) {
2602
2920
  const all = props ?? {};
@@ -2623,6 +2941,98 @@ function Merged({ children }) {
2623
2941
  }
2624
2942
  var open = (element2) => new Html2(element2.options.start ?? element2.markup);
2625
2943
  var close = (tag) => new Html2(`</${tag}>`);
2944
+ var BLOCK_MAP_VERSION = 1;
2945
+ var MARK_PAIRS = 16;
2946
+ var BlockMarks = class {
2947
+ constructor(pair) {
2948
+ this.nodes = [];
2949
+ this.open = String.fromCharCode(64976 + pair * 2);
2950
+ this.close = String.fromCharCode(64977 + pair * 2);
2951
+ }
2952
+ };
2953
+ var current = null;
2954
+ function withBlockMarks(marks, write3) {
2955
+ const before = current;
2956
+ current = marks;
2957
+ try {
2958
+ return write3();
2959
+ } finally {
2960
+ current = before;
2961
+ }
2962
+ }
2963
+ function markBlock(html2, node) {
2964
+ if (!current) return html2;
2965
+ const mark = `${current.open}${current.nodes.length}${current.close}`;
2966
+ const marked = insert(html2, mark, false) ?? insert(html2, mark, true);
2967
+ if (!marked) return html2;
2968
+ current.nodes.push(node);
2969
+ return marked;
2970
+ }
2971
+ var OUTLOOK_OPEN = "<!--[if mso]>";
2972
+ var OUTLOOK_CLOSE = "<![endif]-->";
2973
+ var TAG2 = /<[a-zA-Z][a-zA-Z0-9]*/y;
2974
+ function tagEnd(markup2, outlook) {
2975
+ for (let i = markup2.indexOf("<"); i >= 0; i = markup2.indexOf("<", i + 1)) {
2976
+ if (markup2.startsWith(OUTLOOK_OPEN, i)) {
2977
+ if (outlook) {
2978
+ i += OUTLOOK_OPEN.length - 1;
2979
+ continue;
2980
+ }
2981
+ const end = markup2.indexOf(OUTLOOK_CLOSE, i);
2982
+ if (end < 0) return -1;
2983
+ i = end + OUTLOOK_CLOSE.length - 1;
2984
+ continue;
2985
+ }
2986
+ if (markup2.startsWith("<!--", i)) {
2987
+ const end = markup2.indexOf("-->", i + 4);
2988
+ if (end < 0) return -1;
2989
+ i = end + 2;
2990
+ continue;
2991
+ }
2992
+ TAG2.lastIndex = i;
2993
+ const tag = TAG2.exec(markup2);
2994
+ if (tag) return i + tag[0].length;
2995
+ }
2996
+ return -1;
2997
+ }
2998
+ function insert(html2, mark, outlook) {
2999
+ const { list, kind: kind2, start } = html2.options;
3000
+ if (list) {
3001
+ for (let i = 0; i < list.length; i++) {
3002
+ const marked = insert(list[i], mark, outlook);
3003
+ if (marked) return new Html2(html2.markup, { ...html2.options, list: list.map((piece, j) => j === i ? marked : piece) });
3004
+ }
3005
+ return null;
3006
+ }
3007
+ if (kind2 === "outlook" && !outlook) return null;
3008
+ const at = tagEnd(html2.markup, outlook);
3009
+ if (at < 0) return null;
3010
+ const put = (text22) => text22.slice(0, at) + mark + text22.slice(at);
3011
+ return new Html2(put(html2.markup), { ...html2.options, start: start !== void 0 && at <= start.length ? put(start) : start });
3012
+ }
3013
+ function readMarks(marks, written) {
3014
+ const found = [];
3015
+ const seen = /* @__PURE__ */ new Set();
3016
+ const pieces2 = [];
3017
+ let length32 = 0;
3018
+ let last = 0;
3019
+ for (const match of written.matchAll(new RegExp(`${marks.open}(\\d+)${marks.close}`, "g"))) {
3020
+ const index = Number(match[1]);
3021
+ const node = marks.nodes[index];
3022
+ if (!node || seen.has(index)) return null;
3023
+ seen.add(index);
3024
+ const before = written.slice(last, match.index);
3025
+ pieces2.push(before);
3026
+ length32 += before.length;
3027
+ last = match.index + match[0].length;
3028
+ 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;
3029
+ found.push({ node, at: length32 });
3030
+ }
3031
+ pieces2.push(written.slice(last));
3032
+ const text22 = pieces2.join("");
3033
+ if (text22.includes(marks.open) || text22.includes(marks.close)) return null;
3034
+ return { text: text22, found };
3035
+ }
2626
3036
  function utf8Bytes(text22) {
2627
3037
  let bytes = 0;
2628
3038
  for (let i = 0; i < text22.length; i++) {
@@ -2709,7 +3119,7 @@ function fontFaces2(root, { swap }) {
2709
3119
  return [...faces.values()];
2710
3120
  }
2711
3121
  var isNode2 = (child) => typeof child !== "string";
2712
- var shown = (node) => !(node.hidden?.mobile && node.hidden?.desktop);
3122
+ var shown = (node) => shownAnywhere(node);
2713
3123
  function link2(props) {
2714
3124
  const { href, target } = linkParts(props.href);
2715
3125
  return {
@@ -2944,7 +3354,7 @@ var Button2 = ({ node, style, mobile, fill, phoneText: phoneText22, text: text22
2944
3354
  {
2945
3355
  align: TEXT_ALIGN[node.text.textAlign],
2946
3356
  bgcolor: bgcolor(style.backgroundColor),
2947
- hover: { backgroundColor: node.hover?.backgroundColor },
3357
+ hoverWithin: { backgroundColor: node.hover?.backgroundColor },
2948
3358
  phone: { border: mobile.border, backgroundColor: mobile.backgroundColor },
2949
3359
  style: { backgroundColor: style.backgroundColor, border: style.border, borderRadius: style.borderRadius, msoPaddingAlt: insets2(style.padding) }
2950
3360
  },
@@ -2955,8 +3365,11 @@ var Button2 = ({ node, style, mobile, fill, phoneText: phoneText22, text: text22
2955
3365
  phone: [phoneText22, { padding: mobile.padding }],
2956
3366
  hover: { color: node.hover?.color },
2957
3367
  style: [
2958
- // A button with a width fills it.
2959
- { display: width.width !== void 0 ? "block" : "inline-block", padding: style.padding },
3368
+ // A button with a width fills it. Its label's words stay whole (the email's word-break: break-word
3369
+ // is for long links in text): a button narrower than its padding otherwise sized itself to one letter
3370
+ // and drew its label a letter a line, in Gmail and browsers; it grows to its longest word instead, as
3371
+ // classic Outlook's and Apple Mail's do (captures of Elements 0.3.0, e3-fresh/g-0054).
3372
+ { display: width.width !== void 0 ? "block" : "inline-block", padding: style.padding, wordBreak: "normal" },
2960
3373
  fullText(node),
2961
3374
  { textDecoration: style.textDecoration ?? "none", borderRadius: style.borderRadius }
2962
3375
  ]
@@ -3420,27 +3833,42 @@ function propsOf(node, place, options) {
3420
3833
  options
3421
3834
  };
3422
3835
  }
3836
+ var UNMAPPED = /* @__PURE__ */ new Set(["LineBreak", "MergeTag"]);
3423
3837
  function write(node, place, options) {
3424
3838
  if (place.inline) {
3425
3839
  if (!shown(node)) return raw("");
3426
3840
  const component2 = INLINES[node.type];
3427
3841
  if (!component2) throw new Error(`[Elements] The email exporter can't render a ${node.type} inside text`);
3428
- return component2(propsOf(node, place, options));
3842
+ const html2 = component2(propsOf(node, place, options));
3843
+ return UNMAPPED.has(node.type) ? html2 : markBlock(html2, node);
3429
3844
  }
3430
3845
  const component = BLOCKS[node.type];
3431
3846
  if (!component) throw new Error(`[Elements] The email exporter can't render a ${node.type} here (in a ${place.parent.type})`);
3432
- return component(propsOf(node, place, options));
3847
+ return markBlock(component(propsOf(node, place, options)), node);
3433
3848
  }
3434
3849
  var EMAIL_PHONE_MARGIN = 20;
3435
3850
  var GMAIL_CLIP_BYTES = 102400;
3436
3851
  var GMAIL_STYLE_BYTES = 16384;
3437
3852
  function renderEmail(resolved, options = {}) {
3438
3853
  const root = resolved.root;
3439
- const sheet = new Stylesheet(options.classPrefix);
3440
3854
  const d = new Diagnostics();
3441
3855
  const fonts = fontFaces2(root, { swap: false });
3442
3856
  const width = root.width ?? 600;
3443
- const email = withStylesheet(sheet, () => String(Email2(propsOf(root, { parent: root, fill: false, mobile: {}, inline: false, rtl: false }, options))));
3857
+ const write3 = (marks) => {
3858
+ const sheet2 = new Stylesheet(options.classPrefix);
3859
+ const markup2 = withStylesheet(sheet2, () => withBlockMarks(marks, () => String(markBlock(Email2(propsOf(root, { parent: root, fill: false, mobile: {}, inline: false, rtl: false }, options)), root))));
3860
+ return { sheet: sheet2, markup: markup2 };
3861
+ };
3862
+ let written = options.blockMap ? null : write3(null);
3863
+ let marked = null;
3864
+ for (let pair = 0; !written; pair++) {
3865
+ 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");
3866
+ const marks = new BlockMarks(pair);
3867
+ const attempt = write3(marks);
3868
+ marked = readMarks(marks, attempt.markup);
3869
+ if (marked) written = { sheet: attempt.sheet, markup: marked.text };
3870
+ }
3871
+ const { sheet, markup: email } = written;
3444
3872
  const background = root.style.backgroundColor;
3445
3873
  const lang = root.props.lang;
3446
3874
  const dir = root.props.dir;
@@ -3464,7 +3892,8 @@ function renderEmail(resolved, options = {}) {
3464
3892
  `<style>${stylesheet}</style>`
3465
3893
  ].join("");
3466
3894
  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>` : "";
3467
- 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>`;
3895
+ 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"]])}>`;
3896
+ const body = `${preheader}${opening}${email}</div>`;
3468
3897
  const bodyDeclarations = [
3469
3898
  ["margin", "0"],
3470
3899
  ["padding", "0"],
@@ -3472,20 +3901,38 @@ function renderEmail(resolved, options = {}) {
3472
3901
  ["background-color", background]
3473
3902
  ];
3474
3903
  const bodyStyle = css2(bodyDeclarations);
3475
- 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>`;
3904
+ 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)}>`;
3905
+ const html2 = `${shell}${body}</body></html>`;
3476
3906
  const bytes = utf8Bytes(html2);
3907
+ const gmails = ["gmail", "gmail-app", "gmail-app-other"];
3477
3908
  if (bytes > GMAIL_CLIP_BYTES) {
3478
- 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");
3909
+ 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, {
3910
+ clients: gmails,
3911
+ fix: "shorten the email, or move long sections to a page: what follows the clip is behind a link"
3912
+ });
3479
3913
  }
3480
3914
  const styleBytes = utf8Bytes(stylesheet);
3481
3915
  if (styleBytes > GMAIL_STYLE_BYTES) {
3482
- d.warning("email/style-size", `The stylesheet is ${Math.round(styleBytes / 1024)} KB; Gmail reads ${GMAIL_STYLE_BYTES / 1024} KB of a <style>`, "root");
3916
+ 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, {
3917
+ clients: gmails,
3918
+ fix: "use fewer phone and hover styles: each one adds a rule"
3919
+ });
3483
3920
  }
3484
- return { html: html2, head, body, bodyStyle, diagnostics: d.list };
3921
+ return { html: html2, head, body, bodyStyle, diagnostics: d.list, ...marked ? { blocks: blockMap(resolved, marked.found, email, utf8Bytes(shell + preheader + opening)) } : {} };
3922
+ }
3923
+ function blockMap(resolved, found, email, start) {
3924
+ const templatePath = resolved.templatePath ?? ((path) => path);
3925
+ let offset = start;
3926
+ let from = 0;
3927
+ const entries = found.map(({ node, at }) => {
3928
+ offset += utf8Bytes(email.slice(from, at));
3929
+ from = at;
3930
+ return { path: templatePath(node.path), ...node.id ? { nodeId: node.id } : {}, offset };
3931
+ });
3932
+ return { version: BLOCK_MAP_VERSION, unit: "utf8", entries };
3485
3933
  }
3486
3934
  var isNode3 = (child) => typeof child !== "string";
3487
- var shown2 = (node) => !(node.hidden?.mobile && node.hidden?.desktop);
3488
- var same2 = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
3935
+ var same3 = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
3489
3936
  function support(root, target) {
3490
3937
  const d = new Diagnostics();
3491
3938
  const visit = (node) => {
@@ -3493,41 +3940,81 @@ function support(root, target) {
3493
3940
  const byProp = definition.defaultStyleBy ? definition.defaultStyleBy.values[String(node.props[definition.defaultStyleBy.prop])] : void 0;
3494
3941
  for (const [key, value] of Object.entries(node.style)) {
3495
3942
  const fallback2 = byProp?.[key] ?? definition.defaultStyle?.[key];
3496
- report(node, "style", key, value, fallback2, definition.style[key]?.support, target, d);
3943
+ if (same3(value, fallback2)) continue;
3944
+ reportDeclared(d, node, `${node.path}.style.${key}`, key, value, definition.style[key]?.support?.[target], target);
3497
3945
  }
3498
- for (const [key, value] of Object.entries(node.props)) report(node, "props", key, value, definition.props[key]?.default, definition.props[key]?.support, target, d);
3499
- for (const child of node.children) if (isNode3(child) && shown2(child)) visit(child);
3946
+ for (const [key, value] of Object.entries(node.props)) {
3947
+ if (same3(value, definition.props[key]?.default)) continue;
3948
+ reportDeclared(d, node, `${node.path}.props.${key}`, key, value, definition.props[key]?.support?.[target], target);
3949
+ }
3950
+ for (const condition of CONDITIONS) {
3951
+ const block2 = blockOf(node, condition);
3952
+ if (!block2 || condition.hiddenKey && node.hidden?.[condition.hiddenKey]) continue;
3953
+ const own3 = condition.support[target];
3954
+ reportDeclared(d, node, `${node.path}.${condition.name}`, condition.name, block2, own3, target);
3955
+ if (!own3 || own3.level === "none") continue;
3956
+ for (const [key, value] of Object.entries(block2)) {
3957
+ const rules = condition.styles?.[key]?.[target] ?? definition.style[key]?.support?.[target];
3958
+ reportDeclared(d, node, `${node.path}.${condition.name}.${key}`, key, value, rules, target, condition.outlookApplies);
3959
+ }
3960
+ }
3961
+ for (const child of node.children) if (isNode3(child) && shownAnywhere(child)) visit(child);
3500
3962
  };
3501
3963
  visit(root);
3502
3964
  return d.list;
3503
3965
  }
3504
- function report(node, where, key, value, fallback2, declared, target, d) {
3505
- const rules = declared?.[target];
3506
- if (!rules || same2(value, fallback2)) return;
3507
- for (const rule2 of [].concat(rules)) {
3508
- if (rule2.level === "full") continue;
3509
- if (rule2.values && !rule2.values.some((v) => same2(v, value))) continue;
3510
- if (rule2.when && !rule2.when(value, node)) continue;
3511
- const at = `${node.path}.${where}.${key}`;
3512
- const asked = typeof value === "string" ? `${key}: "${value}"` : key;
3513
- const output = target === "web" ? "a page" : target;
3514
- if (rule2.level === "none") d.warning(`${target}/unsupported`, `${asked} isn't possible in ${output}: ${rule2.shows}`, at, node.id);
3515
- else if (rule2.level === "partial") d.add(`${target}/degraded`, "info", `${asked} on this ${node.type} in ${output}: ${rule2.shows}`, at, node.id);
3516
- else d.add("email/outlook-degraded", "info", `${asked} on this ${node.type} in classic Outlook: ${rule2.shows}`, at, node.id);
3517
- }
3518
- }
3519
- function emailSupport(root) {
3966
+ function emailSupport(root, templatePath = (path) => path) {
3520
3967
  const d = new Diagnostics();
3521
3968
  d.list.push(...support(root, "email"));
3522
3969
  const webFonts = new Set((root.props.fonts ?? []).map((face) => face.family.toLowerCase()));
3970
+ const phone = { styled: false, phoneHidden: false, desktopHidden: false };
3523
3971
  const visit = (node) => {
3524
3972
  structure(node, d);
3525
3973
  fallback(node, webFonts, d);
3974
+ if (node.type === "Html") htmlBlock(node, d);
3975
+ if (node.type === "Image") imageFormat(node, "src", d);
3976
+ if (node.type === "Video" && node.props.thumbnail !== void 0) imageFormat(node, "thumbnail", d);
3977
+ if (node.mobile && Object.keys(node.mobile).length) phone.styled = true;
3978
+ if (node.hidden?.mobile && !builtIn(templatePath(node.path))) phone.phoneHidden = true;
3979
+ if (node.hidden?.desktop && !builtIn(templatePath(node.path))) phone.desktopHidden = true;
3526
3980
  for (const child of node.children) if (isNode2(child) && shown(child)) visit(child);
3527
3981
  };
3528
3982
  visit(root);
3983
+ phoneStyles(root, phone, d);
3529
3984
  return d.list;
3530
3985
  }
3986
+ function builtIn(place) {
3987
+ const made = [...place.matchAll(/\((\w+)\)/g)].pop()?.[1];
3988
+ return !!made && Object.prototype.hasOwnProperty.call(COMPONENTS, made);
3989
+ }
3990
+ var NO_PHONE_STYLES = ["gmail-app-other", "sfr", "laposte", "gmx", "web-de", "rainloop"];
3991
+ function phoneStyles(root, phone, d) {
3992
+ 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);
3993
+ if (!parts.length) return;
3994
+ 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, {
3995
+ clients: NO_PHONE_STYLES,
3996
+ fix: phone.desktopHidden ? "show what a reader needs on desktop too, and hide on phones only what they can do without" : void 0,
3997
+ see: caniemail("css-at-media")
3998
+ });
3999
+ }
4000
+ var IMAGE_FORMATS = {
4001
+ webp: { clients: ["outlook"], feature: "image-webp", name: "a WebP" },
4002
+ avif: { clients: ["outlook", "fastmail", "gmx", "web-de", "orange", "sfr", "mail-ru", "ionos"], feature: "image-avif", name: "an AVIF" },
4003
+ svg: { clients: ["mail-ru"], feature: "image-svg", name: "an SVG" }
4004
+ };
4005
+ 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];
4006
+ function imageFormat(node, key, d) {
4007
+ const src = assetParts(node.props[key]).src;
4008
+ const format = FORMAT_IN.map((pattern) => pattern.exec(src)?.[1]).find(Boolean)?.toLowerCase();
4009
+ const known = format && Object.prototype.hasOwnProperty.call(IMAGE_FORMATS, format) ? IMAGE_FORMATS[format] : void 0;
4010
+ if (!known) return;
4011
+ const what = key === "src" ? "This Image is" : "This Video's thumbnail is";
4012
+ d.warning("email/image-format", `${what} ${known.name} file, which these email clients don't show`, `${node.path}.props.${key}`, node.id, {
4013
+ clients: known.clients,
4014
+ fix: "use a PNG, JPEG or GIF file",
4015
+ see: caniemail(known.feature)
4016
+ });
4017
+ }
3531
4018
  function fallback(node, webFonts, d) {
3532
4019
  const stack = node.style.fontFamily;
3533
4020
  if (typeof stack !== "string" || !webFonts.size) return;
@@ -3536,9 +4023,10 @@ function fallback(node, webFonts, d) {
3536
4023
  if (!last || !webFonts.has(last.toLowerCase())) return;
3537
4024
  d.warning(
3538
4025
  "email/font-fallback",
3539
- `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"`,
4026
+ `This font stack ends with the web font "${last}": where it doesn't load, the email client picks its own font`,
3540
4027
  `${node.path}.style.fontFamily`,
3541
- node.id
4028
+ node.id,
4029
+ { clients: NO_WEB_FONTS, fix: `end it with a font the client has: "${stack}, Arial, sans-serif"`, see: caniemail("css-at-font-face") }
3542
4030
  );
3543
4031
  }
3544
4032
  function structure(node, d) {
@@ -3551,7 +4039,8 @@ function structure(node, d) {
3551
4039
  "info",
3552
4040
  `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`,
3553
4041
  path,
3554
- id
4042
+ id,
4043
+ { clients: ["outlook"], fix: "put less in it, or let it stack on phones (stackOnMobile)" }
3555
4044
  );
3556
4045
  }
3557
4046
  const lines = wrapsWithGap(node);
@@ -3587,6 +4076,86 @@ function structure(node, d) {
3587
4076
  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);
3588
4077
  }
3589
4078
  }
4079
+ var HTML_CSS = [
4080
+ { 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" },
4081
+ { 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" },
4082
+ {
4083
+ pattern: /(^|[\s;{"'])position\s*:\s*absolute\b/i,
4084
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "hey", "sfr", "laposte", "mail-ru", "gmx", "web-de"],
4085
+ removes: "position: absolute: the client removes it",
4086
+ fix: "lay it out without position (a Layer puts content over an image)",
4087
+ feature: "css-position"
4088
+ },
4089
+ {
4090
+ // Outlook.com removes every position (captures); the others not named keep relative.
4091
+ pattern: /(^|[\s;{"'])position\s*:\s*relative\b/i,
4092
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "mail-ru", "gmx", "web-de"],
4093
+ removes: "position: relative: the client removes it, and what it moves stays where it is",
4094
+ fix: "move it with padding instead",
4095
+ feature: "css-position",
4096
+ use: "relative"
4097
+ },
4098
+ {
4099
+ pattern: /var\(\s*--|(^|[\s;{"'])--[\w-]+\s*:/i,
4100
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "orange", "mail-ru", "fastmail", "gmx", "web-de"],
4101
+ removes: "CSS variables: the client removes var() and their declarations",
4102
+ fix: "write the values themselves",
4103
+ feature: "css-variables"
4104
+ },
4105
+ {
4106
+ pattern: /gradient\(/i,
4107
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail-app", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
4108
+ removes: "a gradient: the client removes it",
4109
+ fix: "give it a solid background color too, or use an image",
4110
+ feature: "css-linear-gradient"
4111
+ },
4112
+ { 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()" },
4113
+ { 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)" },
4114
+ {
4115
+ pattern: /<style[\s>]/i,
4116
+ clients: ["gmail", "gmail-app", "gmail-app-other", "protonmail", "gmx", "web-de", "sfr", "laposte", "rainloop"],
4117
+ removes: "a <style> in the email's body: the client removes it, and what it styles shows without it",
4118
+ fix: "write the styles inline, on the elements they're for",
4119
+ feature: "html-style"
4120
+ },
4121
+ { 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" },
4122
+ { 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" },
4123
+ { 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" },
4124
+ {
4125
+ pattern: /display\s*:\s*inline-flex\b/i,
4126
+ clients: ["outlook", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
4127
+ removes: "display: inline-flex: the client lays it out without flex",
4128
+ fix: "lay it out with a Row",
4129
+ feature: "css-display-flex",
4130
+ use: "inline"
4131
+ },
4132
+ { 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" },
4133
+ { 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" },
4134
+ {
4135
+ pattern: /object-fit\s*:/i,
4136
+ clients: ["outlook", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
4137
+ removes: "object-fit: the client stretches the image to its box",
4138
+ fix: "crop the image file to the size it's shown at",
4139
+ feature: "css-object-fit"
4140
+ },
4141
+ {
4142
+ pattern: /(^|[\s;{"'])aspect-ratio\s*:/i,
4143
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "samsung-email", "thunderbird", "fastmail", "gmx", "web-de", "orange"],
4144
+ removes: "aspect-ratio: the client removes it",
4145
+ fix: "give it a width and a height",
4146
+ feature: "css-aspect-ratio"
4147
+ }
4148
+ ];
4149
+ var OUTLOOK_ONLY2 = /<!--\[if\s+(?:(?:gte?|lte?)\s+)?mso[^\]]*\]>[\s\S]*?<!\[endif\]-->/gi;
4150
+ function htmlBlock(node, d) {
4151
+ if (typeof node.props.html !== "string") return;
4152
+ const markup2 = node.props.html.replace(OUTLOOK_ONLY2, "");
4153
+ for (const check of HTML_CSS) {
4154
+ if (check.pattern.test(markup2)) {
4155
+ 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) });
4156
+ }
4157
+ }
4158
+ }
3590
4159
  var isNode4 = (child) => typeof child !== "string";
3591
4160
  var CLEARLY = 1.25;
3592
4161
  var sides = (value) => {
@@ -3718,15 +4287,15 @@ function classesFor(node, parent) {
3718
4287
  const classes = [];
3719
4288
  if (node.mobile) {
3720
4289
  const { width, ...rest } = node.mobile;
3721
- if (Object.keys(rest).length) classes.push(rule("phone", `&{${css2(declarationsOf(rest), true)}}`));
4290
+ if (Object.keys(rest).length) classes.push(rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}{${css2(declarationsOf(rest), true)}}`));
3722
4291
  const line22 = node.type === "Divider" && node.style.padding !== void 0 ? ">hr" : "";
3723
- if (width !== void 0) classes.push(rule("phone", `&${line22}{${css2(declarationsOf({ width }), true)}}`));
4292
+ if (width !== void 0) classes.push(rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}${line22}{${css2(declarationsOf({ width }), true)}}`));
3724
4293
  const own3 = node.style.width === void 0 && (node.style.flex === "none" || node.style.flex === 0);
3725
4294
  const basis = own3 ? void 0 : length2(phoneWidthInRow(node, parent));
3726
4295
  if (basis !== void 0) classes.push(rule("phone", `&{flex:0 1 ${basis}!important}`));
3727
4296
  }
3728
4297
  classes.push(...hidingClasses(node.hidden));
3729
- if (node.hover) classes.push(rule("all", `&:hover{${css2(declarationsOf(node.hover), true)}}`, "hover"));
4298
+ if (node.hover) classes.push(rule(HOVER.sheet.media, `${HOVER.sheet.selector}{${css2(declarationsOf(node.hover), true)}}`, HOVER.sheet.level));
3730
4299
  if (node.type === "Row" && node.props.stackOnMobile !== false) {
3731
4300
  classes.push(
3732
4301
  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")
@@ -3934,17 +4503,17 @@ var Layer3 = ({ node, style, parent, placement, classes, children }) => /* @__PU
3934
4503
  },
3935
4504
  children()
3936
4505
  );
3937
- var current = { page: 1, total: 1 };
4506
+ var current2 = { page: 1, total: 1 };
3938
4507
  function numbering(numbers, write3) {
3939
- const before = current;
3940
- current = numbers;
4508
+ const before = current2;
4509
+ current2 = numbers;
3941
4510
  try {
3942
4511
  return write3();
3943
4512
  } finally {
3944
- current = before;
4513
+ current2 = before;
3945
4514
  }
3946
4515
  }
3947
- var PageNumber2 = ({ node }) => /* @__PURE__ */ h(Fragment, null, String(node.props.total ? current.total : current.page));
4516
+ var PageNumber2 = ({ node }) => /* @__PURE__ */ h(Fragment, null, String(node.props.total ? current2.total : current2.page));
3948
4517
  function documentCss(root) {
3949
4518
  const box2 = pageBox(root.props);
3950
4519
  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}}`;
@@ -4118,8 +4687,23 @@ var Video3 = ({ node, style, placement, classes }) => /* @__PURE__ */ h(
4118
4687
  ))
4119
4688
  );
4120
4689
  var PAGE_PHONE_WIDTH = 480;
4121
- var PHONE = `@media (max-width: ${PAGE_PHONE_WIDTH}px)`;
4122
- var DESKTOP = `@media (min-width: ${PAGE_PHONE_WIDTH + 1}px)`;
4690
+ function pagePhoneWidth(root) {
4691
+ const mobile = root.props.breakpoints?.mobile;
4692
+ return typeof mobile === "number" ? mobile : PAGE_PHONE_WIDTH;
4693
+ }
4694
+ function pageBreakpoints(root) {
4695
+ const mobile = root.props.breakpoints?.mobile;
4696
+ const width = typeof root.props.width === "number" ? root.props.width : void 0;
4697
+ if (root.type !== "Page" || typeof mobile !== "number" || width === void 0 || mobile < width) return [];
4698
+ return [
4699
+ {
4700
+ code: "page/phone-width",
4701
+ severity: "warning",
4702
+ 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}`,
4703
+ path: "root.props.breakpoints"
4704
+ }
4705
+ ];
4706
+ }
4123
4707
  var COMPONENTS2 = {
4124
4708
  Section: Section3,
4125
4709
  Row: Row3,
@@ -4180,7 +4764,8 @@ function renderPage(resolved, options = {}) {
4180
4764
  const writer2 = document ? Document2 : root.type === "Canvas" ? Canvas2 : Page2;
4181
4765
  const body = withStylesheet(sheet, () => String(writer2(propsOf2(root, root, [], options))));
4182
4766
  const fixed = !root.definition.responsive;
4183
- const stylesheet = [...fonts, sheet.css(fixed ? null : PHONE, fixed ? null : DESKTOP), document ? documentCss(root) : ""].join("");
4767
+ const phoneWidth2 = pagePhoneWidth(root);
4768
+ const stylesheet = [...fonts, sheet.css(fixed ? null : `@media (max-width: ${phoneWidth2}px)`, fixed ? null : `@media (min-width: ${phoneWidth2 + 1}px)`), document ? documentCss(root) : ""].join("");
4184
4769
  const head = [
4185
4770
  '<meta charset="utf-8">',
4186
4771
  '<meta name="viewport" content="width=device-width, initial-scale=1">',
@@ -4200,7 +4785,7 @@ function renderText(resolved, options = {}) {
4200
4785
  const tag = options.mergeTag ?? ((name) => `{{${name}}}`);
4201
4786
  const inline2 = (node, upper = false) => node.children.map((child) => {
4202
4787
  if (typeof child === "string") return upper ? child.toUpperCase() : child;
4203
- if (child.hidden?.mobile && child.hidden?.desktop) return "";
4788
+ if (!shownAnywhere(child)) return "";
4204
4789
  switch (child.type) {
4205
4790
  case "LineBreak":
4206
4791
  return "\n";
@@ -4224,7 +4809,7 @@ function renderText(resolved, options = {}) {
4224
4809
  return alt ? `[${upper ? alt.toUpperCase() : alt}]` : "";
4225
4810
  };
4226
4811
  const blocks = (node) => node.children.flatMap((child) => {
4227
- if (typeof child === "string" || child.hidden?.mobile && child.hidden?.desktop) return [];
4812
+ if (typeof child === "string" || !shownAnywhere(child)) return [];
4228
4813
  switch (child.type) {
4229
4814
  case "Heading":
4230
4815
  return [tidy(inline2(child, Number(child.props.level) <= 2))];
@@ -4262,8 +4847,8 @@ function renderText(resolved, options = {}) {
4262
4847
  return blocks(child);
4263
4848
  }
4264
4849
  });
4265
- const shown3 = (node) => !(node.hidden?.mobile && node.hidden?.desktop);
4266
- const list = (node, indent) => node.children.filter((child) => typeof child !== "string" && shown3(child)).map((item, index) => {
4850
+ const shown2 = (node) => shownAnywhere(node);
4851
+ const list = (node, indent) => node.children.filter((child) => typeof child !== "string" && shown2(child)).map((item, index) => {
4267
4852
  const marker2 = node.props.type === "number" ? `${Number(node.props.start) + index}. ` : "- ";
4268
4853
  const lines = [];
4269
4854
  let run = [];
@@ -4279,13 +4864,13 @@ function renderText(resolved, options = {}) {
4279
4864
  }
4280
4865
  flush();
4281
4866
  if (!lines.length) lines.push(`${indent}${marker2}`.trimEnd());
4282
- if (shown3(child)) lines.push(list(child, `${indent} `));
4867
+ if (shown2(child)) lines.push(list(child, `${indent} `));
4283
4868
  }
4284
4869
  flush();
4285
4870
  return (lines.length ? lines : [`${indent}${marker2}`.trimEnd()]).join("\n");
4286
4871
  }).join("\n");
4287
- const table = (node) => node.children.filter((row) => typeof row !== "string" && shown3(row)).map(
4288
- (row) => row.children.filter((cell) => typeof cell !== "string" && shown3(cell)).map((cell) => tidy(inline2(cell))).join(" | ")
4872
+ const table = (node) => node.children.filter((row) => typeof row !== "string" && shown2(row)).map(
4873
+ (row) => row.children.filter((cell) => typeof cell !== "string" && shown2(cell)).map((cell) => tidy(inline2(cell))).join(" | ")
4289
4874
  ).join("\n");
4290
4875
  return blocks(resolved.root).filter((block2) => block2.length > 0).join("\n\n");
4291
4876
  }
@@ -4328,52 +4913,56 @@ var OUTPUTS = {
4328
4913
  Email: { target: "email", ready: true }
4329
4914
  };
4330
4915
  function prepareTemplate(input, options = {}) {
4916
+ checkEmailClients(options.emailClients);
4331
4917
  const registry = registryOf(options);
4332
4918
  const checked = checkDocument(input, registry);
4333
4919
  const diagnostics = [...checked.diagnostics];
4334
4920
  const failed = () => diagnostics.some((d) => d.severity === "error");
4335
4921
  if (!checked.document || failed()) return { resolved: null, target: null, diagnostics };
4336
4922
  const root = checked.document.root.type;
4923
+ const done = (resolved2, target) => {
4924
+ 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" });
4925
+ return { resolved: resolved2, target, diagnostics: root === "Email" ? forEmailClients(diagnostics, options.emailClients) : diagnostics };
4926
+ };
4337
4927
  const output = OUTPUTS[root];
4338
4928
  if (!output?.ready) {
4339
4929
  diagnostics.push({ code: "output/unavailable", severity: "error", message: output?.later ?? `A ${root} can't be rendered yet`, path: "root" });
4340
- return { resolved: null, target: null, diagnostics };
4930
+ return done(null, null);
4341
4931
  }
4342
- const expanded = expandDocument(checked.document, registry);
4932
+ const expanded = expandDocument(checked.document, registry, output.target);
4343
4933
  diagnostics.push(...expanded.diagnostics);
4344
- if (failed()) return { resolved: null, target: output.target, diagnostics };
4934
+ if (failed()) return done(null, output.target);
4345
4935
  diagnostics.push(...checkTargetExpanded(expanded.document, output.target, registry, expanded.paths));
4346
- if (failed()) return { resolved: null, target: output.target, diagnostics };
4936
+ if (failed()) return done(null, output.target);
4347
4937
  const resolved = resolveExpanded(expanded.document, registry, expanded.paths);
4348
4938
  diagnostics.push(...resolved.diagnostics);
4349
- const found = output.target === "email" ? emailSupport(resolved.root) : support(resolved.root, output.target);
4939
+ const found = output.target === "email" ? emailSupport(resolved.root, resolved.templatePath) : support(resolved.root, output.target);
4350
4940
  if (root === "Document" || root === "Canvas") found.push(...overflow(resolved.root));
4351
- if (root !== "Document") found.push(...pageNumbersOutside(resolved.root));
4941
+ found.push(...pageBreakpoints(resolved.root));
4352
4942
  diagnostics.push(...inTemplate(resolved, found));
4353
- return { resolved: failed() ? null : resolved, target: output.target, diagnostics };
4943
+ return done(failed() ? null : resolved, output.target);
4354
4944
  }
4355
4945
  function inTemplate(resolved, diagnostics) {
4356
4946
  const { templatePath } = resolved;
4357
4947
  return templatePath ? diagnostics.map((diagnostic) => ({ ...diagnostic, path: templatePath(diagnostic.path) })) : diagnostics;
4358
4948
  }
4359
- function pageNumbersOutside(root) {
4360
- const out = [];
4361
- const visit = (node) => {
4362
- if (node.type === "PageNumber") {
4363
- 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 } : {} });
4364
- }
4365
- for (const child of node.children) if (typeof child !== "string") visit(child);
4366
- };
4367
- visit(root);
4368
- return out;
4369
- }
4370
4949
  function renderTemplate(input, options = {}) {
4950
+ 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" }]);
4371
4951
  const { resolved, target, diagnostics } = prepareTemplate(input, options);
4372
4952
  if (!resolved) throw new ElementsError(diagnostics);
4373
4953
  const text22 = renderText(resolved, options);
4374
4954
  const written = target === "email" ? renderEmail(resolved, options) : { ...renderPage(resolved, options), diagnostics: [] };
4375
- const shown3 = (html2) => options.pretty ? prettyHtml(html2) : html2;
4376
- return { html: shown3(written.html), head: shown3(written.head), body: shown3(written.body), bodyStyle: written.bodyStyle, text: text22, diagnostics: [...diagnostics, ...inTemplate(resolved, written.diagnostics)] };
4955
+ const shown2 = (html2) => options.pretty ? prettyHtml(html2) : html2;
4956
+ const blocks = "blocks" in written ? written.blocks : void 0;
4957
+ return {
4958
+ html: shown2(written.html),
4959
+ head: shown2(written.head),
4960
+ body: shown2(written.body),
4961
+ bodyStyle: written.bodyStyle,
4962
+ text: text22,
4963
+ diagnostics: [...diagnostics, ...inTemplate(resolved, forEmailClients(written.diagnostics, options.emailClients))],
4964
+ ...blocks ? { blocks } : {}
4965
+ };
4377
4966
  }
4378
4967
 
4379
4968
  // src/utils/collect.ts
@@ -4404,7 +4993,7 @@ function isAsyncComponent(type) {
4404
4993
  }
4405
4994
  var SHARED2 = Symbol.for("@hyper-elements/react/render-builds");
4406
4995
  var using = globalThis[SHARED2] ?? (globalThis[SHARED2] = []);
4407
- var same3 = (a2, b) => a2.renderToString === b.renderToString;
4996
+ var same4 = (a2, b) => a2.renderToString === b.renderToString;
4408
4997
  function dispatcher() {
4409
4998
  const internals = React2;
4410
4999
  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;
@@ -4450,7 +5039,7 @@ function secondBuild(outer) {
4450
5039
  for (const requirer of requirers) {
4451
5040
  try {
4452
5041
  const build = requirer()?.(id);
4453
- if (build?.renderToStaticMarkup && !same3(build, defaultBuild) && ![null, outer].includes(dispatcherOf(build))) {
5042
+ if (build?.renderToStaticMarkup && !same4(build, defaultBuild) && ![null, outer].includes(dispatcherOf(build))) {
4454
5043
  second = build;
4455
5044
  break search;
4456
5045
  }
@@ -4464,11 +5053,11 @@ function secondBuild(outer) {
4464
5053
  var buildOf = (outer) => outer === null ? void 0 : [defaultBuild, second].find((b) => !!b && dispatcherOf(b) === outer);
4465
5054
  function withBuild(render) {
4466
5055
  const outer = dispatcher();
4467
- let build = current2();
5056
+ let build = current3();
4468
5057
  const theirs = using.length === 0 || collecting() !== void 0 ? buildOf(outer) : void 0;
4469
5058
  const busy = [];
4470
5059
  if (theirs) {
4471
- const free = (b) => !same3(b, theirs) && !using.some((u) => same3(u, b));
5060
+ const free = (b) => !same4(b, theirs) && !using.some((u) => same4(u, b));
4472
5061
  const other = free(defaultBuild) ? defaultBuild : secondBuild(outer);
4473
5062
  if (other && free(other)) {
4474
5063
  busy.push(theirs);
@@ -4493,10 +5082,10 @@ function rendersHere(build) {
4493
5082
  return false;
4494
5083
  }
4495
5084
  }
4496
- function current2() {
5085
+ function current3() {
4497
5086
  return using[using.length - 1] ?? defaultBuild;
4498
5087
  }
4499
- var renderToString = (element2, options) => current2().renderToString(element2, options);
5088
+ var renderToString = (element2, options) => current3().renderToString(element2, options);
4500
5089
 
4501
5090
  // src/utils/render-scope.ts
4502
5091
  function withRenderScope(render) {
@@ -4545,7 +5134,7 @@ function withoutAsync(children, collection) {
4545
5134
  }
4546
5135
  var ENTITIES2 = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#x27;": "'" };
4547
5136
  var unescape = (text4) => text4.replace(/&amp;|&lt;|&gt;|&quot;|&#x27;/g, (entity) => ENTITIES2[entity]);
4548
- var NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "style", "mobile", "hover", "hidden"]);
5137
+ var NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "style", "hidden"]);
4549
5138
  function toJsonNode({ type, props }, diagnostics) {
4550
5139
  const node = { type, children: [] };
4551
5140
  const semantic = {};
@@ -4670,10 +5259,10 @@ function collectTemplate(element2) {
4670
5259
  path: "jsx"
4671
5260
  });
4672
5261
  }
4673
- return { document: null, diagnostics, components };
5262
+ return { document: null, diagnostics, components, collected: null };
4674
5263
  }
4675
5264
  const result = checkDocument({ root: top[0] }, new Registry(components));
4676
- return { document: result.document, diagnostics: [...diagnostics, ...result.diagnostics], components };
5265
+ return { document: result.document, diagnostics: [...diagnostics, ...result.diagnostics], components, collected: top[0] };
4677
5266
  }
4678
5267
 
4679
5268
  // src/primitives/components.tsx
@@ -4744,9 +5333,8 @@ function propsOf3(node) {
4744
5333
  return {
4745
5334
  ...node.id !== void 0 ? { id: node.id } : {},
4746
5335
  ...node.props,
4747
- ...node.style && Object.keys(node.style).length ? { style: node.style } : {},
4748
- ...node.mobile ? { mobile: node.mobile } : {},
4749
- ...node.hover ? { hover: node.hover } : {},
5336
+ // Phone and hover styles inside the style, as a template writes them now.
5337
+ ...node.style && Object.keys(node.style).length || node.mobile || node.hover ? { style: { ...node.style, ...node.mobile ? { mobile: node.mobile } : {}, ...node.hover ? { hover: node.hover } : {} } } : {},
4750
5338
  ...node.hidden ? { hidden: node.hidden } : {}
4751
5339
  };
4752
5340
  }
@@ -4800,15 +5388,15 @@ function text2(value, flat) {
4800
5388
  return kept ? value : `{${JSON.stringify(value)}}`;
4801
5389
  }
4802
5390
  var inline = (node) => (node.children ?? []).some((child) => typeof child === "string" || ["Span", "Link", "LineBreak", "MergeTag", "PageNumber"].includes(child.type));
4803
- function element(node, indent, flat, path, report2) {
5391
+ function element(node, indent, flat, path, report) {
4804
5392
  if (!TYPE_NAME.test(node.type)) {
4805
- report2.push(`${path}: ${JSON.stringify(node.type)} isn't a component's name: it's left out of the code`);
5393
+ report.push(`${path}: ${JSON.stringify(node.type)} isn't a component's name: it's left out of the code`);
4806
5394
  return flat ? "" : "{/* left out: not a component's name */}";
4807
5395
  }
4808
5396
  const name = tagName(node.type);
4809
- 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("");
5397
+ 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("");
4810
5398
  const children = node.children ?? [];
4811
- const child = (c, i, inner2, oneLine) => typeof c === "string" ? text2(c, oneLine) : element(c, inner2, oneLine, `${path}.children[${i}]`, report2);
5399
+ const child = (c, i, inner2, oneLine) => typeof c === "string" ? text2(c, oneLine) : element(c, inner2, oneLine, `${path}.children[${i}]`, report);
4812
5400
  if (!children.length) return `<${name}${attributes5} />`;
4813
5401
  if (flat || inline(node)) return `<${name}${attributes5}>${children.map((c, i) => child(c, i, indent, true)).join("")}</${name}>`;
4814
5402
  const inner = `${indent} `;
@@ -4816,9 +5404,9 @@ function element(node, indent, flat, path, report2) {
4816
5404
  ${children.map((c, i) => `${inner}${child(c, i, inner, false)}`).join("\n")}
4817
5405
  ${indent}</${name}>`;
4818
5406
  }
4819
- function printTemplate(root, name = "Template", report2 = []) {
5407
+ function printTemplate(root, name = "Template", report = []) {
4820
5408
  if (!IDENTIFIER.test(name)) {
4821
- report2.push(`the component's name ${JSON.stringify(name)} isn't one JavaScript can write: it's named Template`);
5409
+ report.push(`the component's name ${JSON.stringify(name)} isn't one JavaScript can write: it's named Template`);
4822
5410
  name = "Template";
4823
5411
  }
4824
5412
  const used = /* @__PURE__ */ new Set();
@@ -4835,7 +5423,7 @@ function printTemplate(root, name = "Template", report2 = []) {
4835
5423
 
4836
5424
  export default function ${name}() {
4837
5425
  return (
4838
- ${element(root, " ", false, "root", report2)}
5426
+ ${element(root, " ", false, "root", report)}
4839
5427
  );
4840
5428
  }
4841
5429
  `;
@@ -5356,7 +5944,7 @@ function readHidden2(tool, type, values, say) {
5356
5944
  // src/builder/malformed.ts
5357
5945
  var DEPTH = 32;
5358
5946
  var coerces = (value) => ["toString", "valueOf"].some((key) => Object.hasOwn(value, key) && typeof value[key] !== "function");
5359
- function withoutMalformed(design, report2) {
5947
+ function withoutMalformed(design, report) {
5360
5948
  const found = [];
5361
5949
  const walk = (value, path) => {
5362
5950
  if (!value || typeof value !== "object" || path.length > DEPTH) return;
@@ -5374,7 +5962,7 @@ function withoutMalformed(design, report2) {
5374
5962
  const last = path[path.length - 1];
5375
5963
  if (Array.isArray(holder)) holder[Number(last)] = null;
5376
5964
  else if (holder) delete holder[last];
5377
- 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`);
5965
+ 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`);
5378
5966
  }
5379
5967
  return copy2;
5380
5968
  }
@@ -5383,8 +5971,8 @@ function withoutMalformed(design, report2) {
5383
5971
  var ZERO = { top: 0, right: 0, bottom: 0, left: 0 };
5384
5972
  var obj = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
5385
5973
  var str = (value) => typeof value === "string" ? value : void 0;
5386
- var same4 = (a2, b) => JSON.stringify(a2)?.toLowerCase() === JSON.stringify(b)?.toLowerCase();
5387
- var unless = (value, taken) => value === void 0 || same4(value, taken) ? void 0 : value;
5974
+ var same5 = (a2, b) => JSON.stringify(a2)?.toLowerCase() === JSON.stringify(b)?.toLowerCase();
5975
+ var unless = (value, taken) => value === void 0 || same5(value, taken) ? void 0 : value;
5388
5976
  var lineHeight4 = (value) => {
5389
5977
  const read2 = lineHeight3(value);
5390
5978
  return typeof read2 === "string" && read2.endsWith("%") ? Math.round(parseFloat(read2) * 100) / 1e4 : read2;
@@ -5433,7 +6021,7 @@ function phoneText2(values) {
5433
6021
  }
5434
6022
 
5435
6023
  // src/builder/read-runs.ts
5436
- var TAG2 = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)([^>]*?)\/?\s*>/g;
6024
+ var TAG3 = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)([^>]*?)\/?\s*>/g;
5437
6025
  var ATTRIBUTE = /([a-zA-Z-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
5438
6026
  var ENTITIES3 = {
5439
6027
  amp: "&",
@@ -5574,16 +6162,16 @@ function parseRuns(html2, { weight = 400, links = true, builder = false } = {})
5574
6162
  const left = [];
5575
6163
  if (/<[!?]|<\/(?![a-zA-Z])/.test(html2)) left.push("a comment or a processing instruction");
5576
6164
  const lines = [];
5577
- let current3 = { tag: "", node: null, children: [], weight };
5578
- const stack = [current3];
6165
+ let current4 = { tag: "", node: null, children: [], weight };
6166
+ const stack = [current4];
5579
6167
  const top = () => stack[stack.length - 1];
5580
6168
  const endParagraph = (last2 = false) => {
5581
6169
  if (stack.length > 1) left.push("a paragraph inside text that's styled");
5582
- const runs = tidy2(current3.children);
6170
+ const runs = tidy2(current4.children);
5583
6171
  if (runs.length > 1 && isBreak(runs[runs.length - 1]) && !(builder && last2)) runs.pop();
5584
6172
  if (runs.length) lines.push(runs.length === 1 && isBreak(runs[0]) ? [] : runs);
5585
- current3 = { tag: "", node: null, children: [], weight };
5586
- stack.splice(0, stack.length, current3);
6173
+ current4 = { tag: "", node: null, children: [], weight };
6174
+ stack.splice(0, stack.length, current4);
5587
6175
  };
5588
6176
  let last = 0;
5589
6177
  let afterParagraph = false;
@@ -5593,7 +6181,7 @@ function parseRuns(html2, { weight = 400, links = true, builder = false } = {})
5593
6181
  const value = decode3(raw2, left);
5594
6182
  if (value) top().children.push(value);
5595
6183
  };
5596
- for (const match of html2.matchAll(TAG2)) {
6184
+ for (const match of html2.matchAll(TAG3)) {
5597
6185
  const tag = match[2].toLowerCase();
5598
6186
  text4(html2.slice(last, match.index), tag === "p" || tag === "div");
5599
6187
  last = match.index + match[0].length;
@@ -5936,7 +6524,7 @@ function textJsonHtml(textJson) {
5936
6524
  }
5937
6525
 
5938
6526
  // src/builder/text.ts
5939
- var TAG3 = /<\s*(\/?)\s*([a-zA-Z][a-zA-Z0-9]*)([^>]*?)(\/?)\s*>/g;
6527
+ var TAG4 = /<\s*(\/?)\s*([a-zA-Z][a-zA-Z0-9]*)([^>]*?)(\/?)\s*>/g;
5940
6528
  var styleOf2 = (attrs2) => /\bstyle\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(attrs2)?.slice(1).find((v) => v !== void 0) ?? "";
5941
6529
  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()]));
5942
6530
  var ATTRIBUTES = `(?:[^>"']|"[^"]*"|'[^']*')*`;
@@ -6025,7 +6613,7 @@ var DEEPEST = 64;
6025
6613
  var VOID3 = /* @__PURE__ */ new Set(["br", "img", "hr", "input", "meta", "link", "wbr", "source", "area", "col", "embed", "param", "track"]);
6026
6614
  function tooDeep(html2) {
6027
6615
  let depth = 0;
6028
- for (const match of html2.matchAll(TAG3)) {
6616
+ for (const match of html2.matchAll(TAG4)) {
6029
6617
  if (match[1]) depth = Math.max(0, depth - 1);
6030
6618
  else if (!match[4] && !VOID3.has(match[2].toLowerCase()) && ++depth > DEEPEST) return true;
6031
6619
  }
@@ -6050,7 +6638,7 @@ function readText(html2, { weight = 400, pages = false, links = true, fontSize =
6050
6638
  inline2 = "";
6051
6639
  };
6052
6640
  let at = 0;
6053
- for (const match of [...html2.matchAll(TAG3)]) {
6641
+ for (const match of [...html2.matchAll(TAG4)]) {
6054
6642
  if (match.index < at) continue;
6055
6643
  const tag = match[2].toLowerCase();
6056
6644
  if (match[1] || !/^(p|ul|ol|h[1-6])$/.test(tag)) continue;
@@ -6147,7 +6735,7 @@ function readList(tag, attrs2, inner, runs, fontSize, left) {
6147
6735
  const items2 = [];
6148
6736
  let gap;
6149
6737
  let at = 0;
6150
- for (const match of [...inner.matchAll(TAG3)]) {
6738
+ for (const match of [...inner.matchAll(TAG4)]) {
6151
6739
  if (match.index < at || match[1] || match[2].toLowerCase() !== "li") continue;
6152
6740
  const end = closing(inner, "li", match.index + match[0].length);
6153
6741
  if (end === null) {
@@ -6393,14 +6981,14 @@ function sayUnread(tool, values, defaults, say) {
6393
6981
  const given = defaults ?? FRAMES[tool] ?? {};
6394
6982
  const known = /* @__PURE__ */ new Set([...NOTHING, ...read2.values, ...hidden2]);
6395
6983
  for (const [key, value] of Object.entries(values)) {
6396
- if (known.has(key) || tool === "body" && key.startsWith("popup") || empty(value) || Object.hasOwn(given, key) && same4(value, given[key])) continue;
6984
+ if (known.has(key) || tool === "body" && key.startsWith("popup") || empty(value) || Object.hasOwn(given, key) && same5(value, given[key])) continue;
6397
6985
  say(Object.hasOwn(WHAT, key) ? `its ${WHAT[key]}` : `its ${key} ${JSON.stringify(value) ?? ""} isn't read: left out`);
6398
6986
  }
6399
6987
  const override = values._override && typeof values._override === "object" ? values._override : {};
6400
6988
  const phone = override.mobile && typeof override.mobile === "object" ? override.mobile : {};
6401
6989
  const phoneKnown = /* @__PURE__ */ new Set([...read2.phone, ...hidden2]);
6402
6990
  for (const [key, value] of Object.entries(phone)) {
6403
- if (phoneKnown.has(key) || empty(value) || same4(value, values[key])) continue;
6991
+ if (phoneKnown.has(key) || empty(value) || same5(value, values[key])) continue;
6404
6992
  say(`its ${key} on phones ${JSON.stringify(value) ?? ""} isn't read: left out`);
6405
6993
  }
6406
6994
  for (const device of Object.keys(override)) {
@@ -6648,8 +7236,8 @@ function fold(blocks, space) {
6648
7236
  if (!some(s)) return block2;
6649
7237
  const holds = ["Text", "Heading", "Divider", "Column"].includes(block2.type) && !BOX2.some((key) => block2.style?.[key] !== void 0);
6650
7238
  if (holds) return { ...block2, style: { ...block2.style, padding: add2(block2.style?.padding ?? ZERO2, s) } };
6651
- const { hidden: hidden2, ...shown3 } = block2;
6652
- return { type: "Column", style: { padding: s }, ...hidden2 ? { hidden: hidden2 } : {}, children: [shown3] };
7239
+ const { hidden: hidden2, ...shown2 } = block2;
7240
+ return { type: "Column", style: { padding: s }, ...hidden2 ? { hidden: hidden2 } : {}, children: [shown2] };
6653
7241
  });
6654
7242
  }
6655
7243
  function phoneOf(columnValues) {
@@ -6682,14 +7270,14 @@ function columnOf(columnValues, children, share3, say) {
6682
7270
  function stretched(columns, stacks) {
6683
7271
  if (columns.length < 2) return { style: {} };
6684
7272
  const style = (column) => column.style ?? {};
6685
- const same5 = (values) => values[0] !== void 0 && values.every((value) => JSON.stringify(value) === JSON.stringify(values[0]));
7273
+ const same6 = (values) => values[0] !== void 0 && values.every((value) => JSON.stringify(value) === JSON.stringify(values[0]));
6686
7274
  const out = {};
6687
7275
  const backgrounds = columns.map((column) => style(column).backgroundColor);
6688
- if (same5(backgrounds) && columns.every((column) => style(column).borderRadius === void 0)) out.backgroundColor = backgrounds[0];
7276
+ if (same6(backgrounds) && columns.every((column) => style(column).borderRadius === void 0)) out.backgroundColor = backgrounds[0];
6689
7277
  const lines = {};
6690
7278
  for (const side2 of ["top", "bottom"]) {
6691
7279
  const each = columns.map((column) => style(column).border?.[side2]);
6692
- if (same5(each)) lines[side2] = each[0];
7280
+ if (same6(each)) lines[side2] = each[0];
6693
7281
  }
6694
7282
  if (!out.backgroundColor && !Object.keys(lines).length) return { style: {} };
6695
7283
  for (const column of columns) {
@@ -6786,13 +7374,13 @@ var defined3 = (record) => Object.fromEntries(Object.entries(record).filter(([,
6786
7374
  var wordsIn = (value, depth) => typeof value === "string" ? [value] : depth > 0 && value && typeof value === "object" ? Object.values(value).flatMap((held) => wordsIn(held, depth - 1)) : [];
6787
7375
  var ROOTS = { email: "Email", web: "Page", document: "Document" };
6788
7376
  var SIZES2 = /* @__PURE__ */ new Set(["A3", "A4", "A5", "Letter", "Legal", "Tabloid"]);
6789
- function readDesign(given, displayMode, report2, toSafeHtml) {
6790
- const design = withoutMalformed(given, report2);
7377
+ function readDesign(given, displayMode, report, toSafeHtml) {
7378
+ const design = withoutMalformed(given, report);
6791
7379
  const body = obj3(obj3(design).body);
6792
- if (!Object.keys(body).length) report2.push("the design has no body: an empty template");
7380
+ if (!Object.keys(body).length) report.push("the design has no body: an empty template");
6793
7381
  const values = { ...BodyDefaults, ...obj3(body.values) };
6794
7382
  const rowsOf = (key) => {
6795
- if (body[key] !== void 0 && !Array.isArray(body[key])) report2.push(`body.${key} isn't a list of rows: left out`);
7383
+ if (body[key] !== void 0 && !Array.isArray(body[key])) report.push(`body.${key} isn't a list of rows: left out`);
6796
7384
  return arr2(body[key]);
6797
7385
  };
6798
7386
  const rows = rowsOf("rows");
@@ -6800,15 +7388,15 @@ function readDesign(given, displayMode, report2, toSafeHtml) {
6800
7388
  const footers = rowsOf("footers");
6801
7389
  const breaks = rows.some((row) => arr2(obj3(row).columns).some((column) => arr2(obj3(column).contents).some((content2) => obj3(content2).type === "page_break")));
6802
7390
  const mode = displayMode && Object.hasOwn(ROOTS, displayMode) ? displayMode : headers.length || footers.length || breaks ? "document" : "email";
6803
- const root = rootOf(mode, values, (message) => report2.push(`body: ${message}`));
6804
- sayUnread("body", obj3(body.values), void 0, (message) => report2.push(`body: ${message}`));
7391
+ const root = rootOf(mode, values, (message) => report.push(`body: ${message}`));
7392
+ sayUnread("body", obj3(body.values), void 0, (message) => report.push(`body: ${message}`));
6805
7393
  const context = (where) => ({
6806
7394
  inherited: { color: root.style?.color ?? "#000000", fontFamily: root.style?.fontFamily ?? "Arial, Helvetica, sans-serif", fontWeight: root.style?.fontWeight ?? 400 },
6807
7395
  mode,
6808
7396
  dir: root.props?.dir ?? "ltr",
6809
7397
  safe: (html2) => toSafeHtml ? toSafeHtml(html2) : html2,
6810
7398
  document: mode === "document",
6811
- say: (message) => report2.push(`${where}: ${message}`)
7399
+ say: (message) => report.push(`${where}: ${message}`)
6812
7400
  });
6813
7401
  const children = [];
6814
7402
  const place = (list, part) => list.flatMap((row, r) => readRow(obj3(row), `${part}[${r}]`, { context, body: values, document: mode === "document", part: mode === "document" && part !== "rows" }));
@@ -6823,12 +7411,12 @@ function readDesign(given, displayMode, report2, toSafeHtml) {
6823
7411
  }
6824
7412
  const all = [...rows, ...headers, ...footers];
6825
7413
  const items2 = all.flatMap((row) => [row, ...arr2(obj3(row).columns).flatMap((column) => [column, ...arr2(obj3(column).contents)])]).map((item) => obj3(obj3(item).values));
6826
- 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");
7414
+ 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");
6827
7415
  const ownFonts = new Set(arr2(root.props?.fonts).map((face) => obj3(face).family));
6828
7416
  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)))];
6829
- 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`);
7417
+ 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`);
6830
7418
  const tags = [...new Set(wordsIn(all, 12).flatMap((text4) => [...text4.matchAll(/\[\[\s*[\w.-]+\s*\]\]/g)].map((match) => match[0])))];
6831
- 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`);
7419
+ 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`);
6832
7420
  return { ...root, children };
6833
7421
  }
6834
7422
  function rootOf(mode, values, say) {
@@ -6870,23 +7458,23 @@ function rootOf(mode, values, say) {
6870
7458
  // src/builder/check.ts
6871
7459
  var LINK = definitionOf("Link").props.href.kind;
6872
7460
  var holdsText = (type) => !!definitionOf(type)?.children?.text;
6873
- function itemLinks(value, key, path, report2) {
7461
+ function itemLinks(value, key, path, report) {
6874
7462
  if (!Array.isArray(value)) return value;
6875
7463
  return value.map((item, i) => {
6876
7464
  if (!item || typeof item !== "object" || Array.isArray(item) || !("href" in item) || LINK.parse(item.href).ok) return item;
6877
- report2.push(`${path}: its ${key}[${i}]'s link ${JSON.stringify(item.href)} isn't one Elements writes: it's "#"`);
7465
+ report.push(`${path}: its ${key}[${i}]'s link ${JSON.stringify(item.href)} isn't one Elements writes: it's "#"`);
6878
7466
  return { ...item, href: "#" };
6879
7467
  });
6880
7468
  }
6881
- function checkNode(node, path, report2, inText2 = false) {
7469
+ function checkNode(node, path, report, inText2 = false) {
6882
7470
  const definition = definitionOf(node.type);
6883
7471
  if (!definition) {
6884
- report2.push(`${path}: ${JSON.stringify(node.type)} isn't a component: left out`);
7472
+ report.push(`${path}: ${JSON.stringify(node.type)} isn't a component: left out`);
6885
7473
  return null;
6886
7474
  }
6887
7475
  const out = { ...node };
6888
7476
  if (out.id !== void 0 && !/^[A-Za-z][\w-]*$/.test(out.id)) {
6889
- report2.push(`${path}: its id ${JSON.stringify(out.id)} isn't one a template takes: left out`);
7477
+ report.push(`${path}: its id ${JSON.stringify(out.id)} isn't one a template takes: left out`);
6890
7478
  delete out.id;
6891
7479
  }
6892
7480
  for (const part of ["props", "style", "mobile", "hover"]) {
@@ -6895,7 +7483,7 @@ function checkNode(node, path, report2, inText2 = false) {
6895
7483
  const specs = part === "props" ? definition.props : definition.style;
6896
7484
  const kept = {};
6897
7485
  for (const [key, given] of Object.entries(values)) {
6898
- const value = part === "props" ? itemLinks(given, key, path, report2) : given;
7486
+ const value = part === "props" ? itemLinks(given, key, path, report) : given;
6899
7487
  const spec = Object.hasOwn(specs, key) ? specs[key] : void 0;
6900
7488
  const parsed = spec ? spec.kind.parse(value) : void 0;
6901
7489
  if (parsed?.ok) {
@@ -6904,35 +7492,35 @@ function checkNode(node, path, report2, inText2 = false) {
6904
7492
  }
6905
7493
  if (part === "props" && (key === "href" || key === "src")) {
6906
7494
  if (node.type === "Button") {
6907
- report2.push(`${path}: its link ${JSON.stringify(value)} isn't one Elements writes: it's "#"`);
7495
+ report.push(`${path}: its link ${JSON.stringify(value)} isn't one Elements writes: it's "#"`);
6908
7496
  kept.href = "#";
6909
7497
  continue;
6910
7498
  }
6911
7499
  if (node.type !== "Link") {
6912
- report2.push(`${path}: its ${key} ${JSON.stringify(value)} isn't one Elements writes: left out`);
7500
+ report.push(`${path}: its ${key} ${JSON.stringify(value)} isn't one Elements writes: left out`);
6913
7501
  return null;
6914
7502
  }
6915
- report2.push(`${path}: its ${key} ${JSON.stringify(value)} isn't one Elements writes: ${inText2 ? "its words" : "what it holds"} without it`);
6916
- const children = checkChildren(node.children ?? [], path, report2, true);
7503
+ report.push(`${path}: its ${key} ${JSON.stringify(value)} isn't one Elements writes: ${inText2 ? "its words" : "what it holds"} without it`);
7504
+ const children = checkChildren(node.children ?? [], path, report, true);
6917
7505
  return inText2 ? { type: "Span", children } : children;
6918
7506
  }
6919
- 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`);
7507
+ 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`);
6920
7508
  }
6921
7509
  if (Object.keys(kept).length) out[part] = kept;
6922
7510
  else delete out[part];
6923
7511
  }
6924
- if (node.children) out.children = checkChildren(node.children, path, report2, holdsText(node.type));
7512
+ if (node.children) out.children = checkChildren(node.children, path, report, holdsText(node.type));
6925
7513
  return out;
6926
7514
  }
6927
- function checkChildren(children, path, report2, inText2) {
7515
+ function checkChildren(children, path, report, inText2) {
6928
7516
  return children.flatMap((child, i) => {
6929
7517
  if (typeof child === "string") return [child];
6930
- const checked = checkNode(child, `${path}.children[${i}]`, report2, inText2);
7518
+ const checked = checkNode(child, `${path}.children[${i}]`, report, inText2);
6931
7519
  return Array.isArray(checked) ? checked : checked ? [checked] : [];
6932
7520
  });
6933
7521
  }
6934
- function checkTemplate(root, report2) {
6935
- const checked = checkNode(root.type === "Document" ? withoutPhones(root) : root, "root", report2);
7522
+ function checkTemplate(root, report) {
7523
+ const checked = checkNode(root.type === "Document" ? withoutPhones(root) : root, "root", report);
6936
7524
  return checked && !Array.isArray(checked) ? checked : { type: root.type };
6937
7525
  }
6938
7526
  function withoutPhones(node) {
@@ -7608,7 +8196,8 @@ function renderSubtree(node, width, root, components, inherited) {
7608
8196
  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;
7609
8197
  const own3 = tokens2(node);
7610
8198
  const around = { ...inherited, ...drawn < 200 ? { width: drawn } : {} };
7611
- 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] };
8199
+ const breakpoints2 = root.type === "Page" && root.props?.breakpoints ? { breakpoints: root.props.breakpoints } : {};
8200
+ 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] };
7612
8201
  const { head, body } = renderTemplate({ root: page }, { components });
7613
8202
  const style = /<style>[\s\S]*?<\/style>/.exec(head)?.[0] ?? "";
7614
8203
  const filled = body.replace(/^<main style="/, '<main style="width:100%;');
@@ -7620,14 +8209,14 @@ var holdsPageNumber = (node) => node.type === "PageNumber" || (node.children ??
7620
8209
  var PAINT2 = ["backgroundColor", "border", "borderRadius"];
7621
8210
  var BROWSER_TEXT = { fontSize: 16, fontStyle: "normal", letterSpacing: 0, textAlign: "left", textTransform: "none" };
7622
8211
  function raise(element2) {
7623
- const report2 = [];
8212
+ const report = [];
7624
8213
  const { document, diagnostics, components } = collectTemplate(element2);
7625
8214
  if (!document || diagnostics.some((d) => d.severity === "error")) return { body: null, report: diagnostics.filter((d) => d.severity === "error").map((d) => `${d.path}: ${d.message}`) };
7626
8215
  const root = document.root;
7627
8216
  if (root.type !== "Email" && root.type !== "Page" && root.type !== "Document") {
7628
8217
  return { body: null, report: [`root: the builder edits an Email, a Page or a Document, not a ${root.type}`] };
7629
8218
  }
7630
- 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");
8219
+ 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");
7631
8220
  const prepared = prepareTemplate(document, { components });
7632
8221
  if (!prepared.resolved) return { body: null, report: prepared.diagnostics.filter((d) => d.severity === "error").map((d) => `${d.path}: ${d.message}`) };
7633
8222
  const resolvedRoot = prepared.resolved.root;
@@ -7644,7 +8233,7 @@ function raise(element2) {
7644
8233
  };
7645
8234
  const sayLeftOut = (tool, node, path, fields) => {
7646
8235
  const left = leftOut(tool, node, fields);
7647
- if (left.length) report2.push(`${path}: the builder's ${tool} has no place for ${left.join(", ")}: left out`);
8236
+ if (left.length) report.push(`${path}: the builder's ${tool} has no place for ${left.join(", ")}: left out`);
7648
8237
  };
7649
8238
  const unsized = (node, path) => childrenOf2(node, path).some(({ child, path: childPath }) => !child.hidden?.desktop && resolved.get(childPath)?.width === null);
7650
8239
  const childrenOf2 = (node, path) => (node.children ?? []).map((child, i) => ({ child, path: `${path}.children[${i}]` })).filter((c) => typeof c.child !== "string");
@@ -7663,11 +8252,11 @@ function raise(element2) {
7663
8252
  };
7664
8253
  const place = {
7665
8254
  at,
7666
- report: report2,
8255
+ report,
7667
8256
  put,
7668
8257
  sayLeftOut,
7669
8258
  locked: (node, path, why, around) => {
7670
- 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)"}`);
8259
+ 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)"}`);
7671
8260
  const own3 = at(path);
7672
8261
  return put(lockedHtml(node, own3.width ?? own3.available ?? Number(resolvedRoot.width), root, components, around, inheritedText(path)));
7673
8262
  },
@@ -7801,7 +8390,7 @@ function raise(element2) {
7801
8390
  const own3 = at(path);
7802
8391
  sayLeftOut("row", child, path, ["style.backgroundColor", "style.padding", "style.border", "style.gap", "style.alignItems", ...TEXT_FOLDED]);
7803
8392
  const band = own3.style.backgroundColor !== void 0 ? { backgroundColor: own3.style.backgroundColor } : {};
7804
- if (child.mobile?.border !== void 0) report2.push(`${path}: the builder's row has no border on phones: left out`);
8393
+ if (child.mobile?.border !== void 0) report.push(`${path}: the builder's row has no border on phones: left out`);
7805
8394
  const sectionRows = rows(childrenOf2(child, path), own3, band, padding2(child), child);
7806
8395
  if (child.id && sectionRows[0]) put(sectionRows[0], builderId(child.id, "u_row"));
7807
8396
  body.push(...sectionRows.length ? sectionRows : [oneColumn([], own3, band, padding2(child), {}, hiding2(child))]);
@@ -7815,7 +8404,7 @@ function raise(element2) {
7815
8404
  } else if (child.type === "PageBreak") {
7816
8405
  flushLoose();
7817
8406
  const content2 = top.slice(i + 1).some((c) => !["PageHeader", "PageFooter", "PageBreak"].includes(c.child.type));
7818
- 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`);
8407
+ 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`);
7819
8408
  body.push(pageBreak());
7820
8409
  last = "break";
7821
8410
  } else if (child.type === "PageHeader" || child.type === "PageFooter") {
@@ -7858,10 +8447,10 @@ function raise(element2) {
7858
8447
  const bodyHidden = { ...hiddenOf("body", root, ["props.margin"]), ...margin, ...title, backgroundColor: style.backgroundColor ?? "" };
7859
8448
  if (root.type === "Document") {
7860
8449
  const paper = { documentSize: root.props?.size ?? "A4", documentOrientation: root.props?.orientation ?? "portrait" };
7861
- return { body: { root: root.type, fields: { ...common, ...paper }, values: bodyHidden, rows: body, headers, footers }, report: report2 };
8450
+ return { body: { root: root.type, fields: { ...common, ...paper }, values: bodyHidden, rows: body, headers, footers }, report };
7862
8451
  }
7863
8452
  const previewText = root.type === "Email" && root.props?.previewText ? { previewText: String(root.props.previewText) } : {};
7864
- return { body: { root: root.type, fields: common, ...previewText, values: bodyHidden, rows: body }, report: report2 };
8453
+ return { body: { root: root.type, fields: common, ...previewText, values: bodyHidden, rows: body }, report };
7865
8454
  }
7866
8455
  function countRows(children) {
7867
8456
  let count = 0;
@@ -7907,10 +8496,10 @@ function nodeOf(node) {
7907
8496
 
7908
8497
  // src/builder/write.ts
7909
8498
  function writeDesign(element2) {
7910
- const { body, report: report2 } = raise(element2);
8499
+ const { body, report } = raise(element2);
7911
8500
  if (!body) throw new Error(`[Elements] This template can't go to the builder:
7912
- ${report2.join("\n")}`);
7913
- return { design: withFontStylesheets(writeParts(body)), report: report2 };
8501
+ ${report.join("\n")}`);
8502
+ return { design: withFontStylesheets(writeParts(body)), report };
7914
8503
  }
7915
8504
 
7916
8505
  // src/builder/index.ts
@@ -7918,10 +8507,10 @@ function toDesign(element2) {
7918
8507
  return writeDesign(element2);
7919
8508
  }
7920
8509
  function fromDesign(design, options = {}) {
7921
- const report2 = [];
7922
- const root = checkTemplate(uniqueIds(readDesign(design, options.displayMode, report2, options.toSafeHtml)), report2);
7923
- const code = printTemplate(root, options.name, report2);
7924
- return { element: toElement(root), code, report: report2 };
8510
+ const report = [];
8511
+ const root = checkTemplate(uniqueIds(readDesign(design, options.displayMode, report, options.toSafeHtml)), report);
8512
+ const code = printTemplate(root, options.name, report);
8513
+ return { element: toElement(root), code, report };
7925
8514
  }
7926
8515
 
7927
8516
  export { fromDesign, toDesign };