@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.
@@ -9,21 +9,26 @@ var Diagnostics = class {
9
9
  constructor() {
10
10
  this.list = [];
11
11
  }
12
- add(code, severity, message, path, nodeId) {
13
- this.list.push(nodeId === void 0 ? { code, severity, message, path } : { code, severity, message, path, nodeId });
12
+ add(code, severity, message, path, nodeId, extra = {}) {
13
+ const d = { code, severity, message, path };
14
+ if (nodeId !== void 0) d.nodeId = nodeId;
15
+ if (extra.clients) d.clients = [...extra.clients];
16
+ if (extra.fix) d.fix = extra.fix;
17
+ if (extra.see) d.see = extra.see;
18
+ this.list.push(d);
14
19
  }
15
20
  error(code, message, path, nodeId) {
16
21
  this.add(code, "error", message, path, nodeId);
17
22
  }
18
- warning(code, message, path, nodeId) {
19
- this.add(code, "warning", message, path, nodeId);
23
+ warning(code, message, path, nodeId, extra) {
24
+ this.add(code, "warning", message, path, nodeId, extra);
20
25
  }
21
26
  get hasErrors() {
22
27
  return this.list.some((d) => d.severity === "error");
23
28
  }
24
29
  };
25
30
  function formatDiagnostics(diagnostics) {
26
- return diagnostics.map((d) => `${d.severity} ${d.code} at ${d.path}: ${d.message}`).join("\n");
31
+ 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");
27
32
  }
28
33
  var ElementsError = class extends Error {
29
34
  constructor(diagnostics) {
@@ -34,6 +39,64 @@ ${formatDiagnostics(errors)}`);
34
39
  this.diagnostics = diagnostics;
35
40
  }
36
41
  };
42
+ var captured = (against, wordsWithin4px, lineBreaks) => ({ elements: "0.3.0", against, captured: "2026-11-08", wordsWithin4px, lineBreaks });
43
+ var tables = (name) => ({ name, widths: {}, dark: false, approximate: true, measured: null });
44
+ var EMAIL_CLIENTS = {
45
+ 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) },
46
+ "outlook-com": { name: "Outlook.com and new Outlook", widths: { desktop: 820 }, dark: false, approximate: false, measured: captured("Outlook on the web", 1, 1) },
47
+ "outlook-mac": tables("Outlook for Mac"),
48
+ "outlook-app": tables("Outlook app (iOS and Android)"),
49
+ gmail: { name: "Gmail", widths: { desktop: 820 }, dark: false, approximate: false, measured: captured("Gmail on the web", 1, 1) },
50
+ "gmail-app": { name: "Gmail app", widths: { phone: 375 }, dark: false, approximate: true, measured: null },
51
+ "gmail-app-other": { name: "Gmail app, another account", widths: { phone: 375 }, dark: false, approximate: true, measured: null },
52
+ "apple-mail": { name: "Apple Mail", widths: { desktop: 820 }, dark: true, approximate: false, measured: captured("Mail 16 on macOS 26", 0.999, 0.999) },
53
+ yahoo: tables("Yahoo Mail"),
54
+ aol: tables("AOL Mail"),
55
+ "samsung-email": tables("Samsung Email"),
56
+ thunderbird: tables("Thunderbird"),
57
+ protonmail: tables("Proton Mail"),
58
+ fastmail: tables("Fastmail"),
59
+ hey: tables("HEY"),
60
+ gmx: tables("GMX"),
61
+ "web-de": tables("WEB.DE"),
62
+ orange: tables("Orange"),
63
+ sfr: tables("SFR"),
64
+ laposte: tables("La Poste"),
65
+ "free-fr": tables("Free"),
66
+ "t-online-de": tables("T-Online"),
67
+ "mail-ru": tables("Mail.ru"),
68
+ ionos: tables("IONOS (1&1)"),
69
+ "wp-pl": tables("WP Poczta"),
70
+ rainloop: tables("RainLoop")
71
+ };
72
+ var DEFAULT_EMAIL_CLIENTS = ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "apple-mail", "yahoo", "aol", "samsung-email"];
73
+ var caniemail = (feature) => `https://www.caniemail.com/features/${feature}/`;
74
+ function checkEmailClients(clients) {
75
+ if (clients === void 0) return;
76
+ const unknown = Array.isArray(clients) ? clients.filter((c) => !Object.prototype.hasOwnProperty.call(EMAIL_CLIENTS, c)) : [clients];
77
+ 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(", ")}`);
78
+ }
79
+ function forEmailClients(diagnostics, selection = DEFAULT_EMAIL_CLIENTS) {
80
+ const out = [];
81
+ const seen = /* @__PURE__ */ new Map();
82
+ for (const d of diagnostics) {
83
+ if (!d.clients) {
84
+ out.push(d);
85
+ continue;
86
+ }
87
+ const clients = d.clients.filter((c) => selection.includes(c));
88
+ if (!clients.length) continue;
89
+ const key = JSON.stringify([d.code, d.path, d.severity, d.message]);
90
+ const same42 = seen.get(key);
91
+ if (same42) same42.clients = [.../* @__PURE__ */ new Set([...same42.clients, ...clients])];
92
+ else {
93
+ const kept = { ...d, clients };
94
+ seen.set(key, kept);
95
+ out.push(kept);
96
+ }
97
+ }
98
+ return out;
99
+ }
37
100
  var ok = (value) => ({ ok: true, value });
38
101
  var fail = (message) => ({ ok: false, message });
39
102
  var show = (input) => {
@@ -619,6 +682,26 @@ function attributes() {
619
682
  jsonSchema: () => ({ type: "object", additionalProperties: { type: "string" } })
620
683
  });
621
684
  }
685
+ var MIN_PHONE_WIDTH = 240;
686
+ function breakpoints() {
687
+ const accepts = `{ mobile }: the phone width in px (from ${MIN_PHONE_WIDTH})`;
688
+ return kind({
689
+ name: "breakpoints",
690
+ editor: "number",
691
+ accepts,
692
+ parse(input) {
693
+ if (!isObject(input)) return fail(`expected ${accepts}, not ${show(input)}`);
694
+ const unknown = Object.keys(input).find((key) => key !== "mobile");
695
+ if (unknown !== void 0) return fail(`${show(unknown)} isn't a breakpoint a Page has: it has mobile`);
696
+ const { mobile } = input;
697
+ if (mobile === void 0) return ok({});
698
+ 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)}`);
699
+ return ok({ mobile });
700
+ },
701
+ empty: (value) => value.mobile === void 0,
702
+ jsonSchema: () => ({ type: "object", properties: { mobile: { type: "integer", minimum: MIN_PHONE_WIDTH } }, additionalProperties: false })
703
+ });
704
+ }
622
705
  function linkParts(value) {
623
706
  return typeof value === "string" ? { href: value } : value;
624
707
  }
@@ -632,6 +715,54 @@ function defineComponent(definition) {
632
715
  function prop(kind2, options) {
633
716
  return { kind: kind2, ...options };
634
717
  }
718
+ var MOBILE = {
719
+ name: "mobile",
720
+ on: "on phones",
721
+ for: "for phones",
722
+ roots: (root) => !!root?.responsive,
723
+ notAllowed: { code: "mobile/not-supported", message: "Phone styles are for an Email or a Page, which have a phone width" },
724
+ sheet: { media: "phone", selector: "&" },
725
+ allows: (spec) => !!spec.mobile,
726
+ support: { web: { level: "full", source: "writer" }, email: { level: "full", source: "writer" } },
727
+ styles: {
728
+ // The email's phone text styles are rules that override a size; "normal" isn't one.
729
+ lineHeight: { email: { level: "none", values: ["normal"], shows: "the line height it has on desktop", source: "writer" } }
730
+ },
731
+ outlookApplies: false,
732
+ hiddenKey: "mobile"
733
+ };
734
+ var HOVER = {
735
+ name: "hover",
736
+ on: "on hover",
737
+ for: "for hover",
738
+ // Keyboard focus gets the same styles as the pointer: a link or button reached by Tab shows its hover colors.
739
+ // A box around the link (an email Button's cell, its background) takes them while the link in it has focus.
740
+ sheet: { media: "all", selector: "&:hover,&:focus-visible", within: "&:hover,&:focus-within", level: "hover" },
741
+ allows: (_, key, definition) => !!definition.hover?.includes(key),
742
+ support: {
743
+ web: { level: "full", source: "css" },
744
+ // On type selectors only in Outlook.com and Outlook for Mac (Elements' are classes); the phone apps' results don't
745
+ // matter, as nothing hovers there.
746
+ email: {
747
+ level: "partial",
748
+ shows: "no change (where it shows, keyboard focus gets it too)",
749
+ source: "caniemail",
750
+ feature: "css-pseudo-class-hover",
751
+ clients: ["outlook", "outlook-com", "outlook-mac", "gmx", "web-de", "sfr", "laposte", "mail-ru"]
752
+ }
753
+ },
754
+ paged: (node, where) => `A hover isn't possible in ${where}: nothing points at it. The ${node.type} shows its own colors`,
755
+ outlookApplies: false
756
+ };
757
+ var CONDITIONS = [MOBILE, HOVER];
758
+ var CONDITION_NAMES = new Set(CONDITIONS.map((condition) => condition.name));
759
+ function blockOf(node, condition) {
760
+ const block = node[condition.name];
761
+ return block && Object.keys(block).length ? block : void 0;
762
+ }
763
+ function shownAnywhere(node) {
764
+ return !(node.hidden?.mobile && node.hidden?.desktop);
765
+ }
635
766
  var writer = { level: "full", source: "writer" };
636
767
  var css = { level: "full", source: "css" };
637
768
  var textStyle = {
@@ -640,7 +771,12 @@ var textStyle = {
640
771
  fontFamily: { kind: fontFamily(), label: "Font", inherited: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
641
772
  // Word rounds sizes to half points (13px is 10pt): a fraction of a px.
642
773
  fontSize: { kind: length(["px"], { min: 1 }), label: "Font size", inherited: true, mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
643
- fontWeight: { kind: fontWeight(), label: "Font weight", inherited: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
774
+ fontWeight: {
775
+ kind: fontWeight(),
776
+ label: "Font weight",
777
+ inherited: true,
778
+ 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 }
779
+ },
644
780
  fontStyle: { kind: enumOf(["normal", "italic"]), label: "Font style", inherited: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
645
781
  // Written in px with mso-line-height-rule: exactly, which Word keeps (it drops unitless ones).
646
782
  lineHeight: { kind: lineHeight(), label: "Line height", inherited: true, mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
@@ -652,8 +788,21 @@ var textStyle = {
652
788
  support: { email: { level: "outlook", shows: "normal spacing: Word keeps letter spacing but doesn't draw it", source: "outlook-captures" }, web: css }
653
789
  },
654
790
  textAlign: { kind: enumOf(["left", "center", "right", "justify"]), label: "Alignment", inherited: true, mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
655
- textTransform: { kind: enumOf(["none", "uppercase", "lowercase", "capitalize"]), label: "Letter case", inherited: true, support: { email: { level: "full", source: "assumed" }, web: css } },
656
- textDecoration: { kind: enumOf(["none", "underline", "line-through"]), label: "Decoration", support: { email: { level: "full", source: "caniemail" }, web: css } }
791
+ textTransform: {
792
+ kind: enumOf(["none", "uppercase", "lowercase", "capitalize"]),
793
+ label: "Letter case",
794
+ inherited: true,
795
+ support: { email: { level: "outlook", values: ["lowercase"], shows: "the text as written: Word doesn't lowercase it", source: "caniemail", feature: "css-text-transform" }, web: css }
796
+ },
797
+ textDecoration: {
798
+ kind: enumOf(["none", "underline", "line-through"]),
799
+ label: "Decoration",
800
+ support: {
801
+ // Removed, so "none" leaves a link the client's own underline: said only where a line is asked for.
802
+ 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"] },
803
+ web: css
804
+ }
805
+ }
657
806
  };
658
807
  var inheritedTextStyle = (({ textDecoration: _, ...rest }) => rest)(textStyle);
659
808
  var boxStyle = {
@@ -666,7 +815,17 @@ var boxStyle = {
666
815
  padding: { kind: insets(), label: "Padding", mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
667
816
  backgroundColor: { kind: color(), label: "Background color", support: { email: writer, web: css } },
668
817
  border: { kind: border(), label: "Border", mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
669
- borderRadius: { kind: radius(), label: "Corner radius", support: { email: { level: "outlook", shows: "square corners", source: "caniemail" }, web: css } },
818
+ borderRadius: {
819
+ kind: radius(),
820
+ label: "Corner radius",
821
+ support: {
822
+ email: [
823
+ { level: "outlook", shows: "square corners", source: "caniemail", feature: "css-border-radius" },
824
+ { level: "partial", shows: "square corners", source: "caniemail", feature: "css-border-radius", clients: ["gmx", "web-de", "orange"] }
825
+ ],
826
+ web: css
827
+ }
828
+ },
670
829
  overflow: {
671
830
  kind: enumOf(["visible", "hidden"]),
672
831
  label: "Overflow",
@@ -874,6 +1033,15 @@ var Grid = defineComponent({
874
1033
  },
875
1034
  children: { categories: ["layout", "block"] },
876
1035
  sizing: "stretch",
1036
+ // Stacked on phones, every two of its children are its phone gap apart: there are no rows for a row gap.
1037
+ conditionSupport: {
1038
+ mobile: {
1039
+ rowGap: {
1040
+ web: { level: "none", when: (_, node) => node.props.stackOnMobile !== false, shows: "its phone gap between every two children", source: "writer" },
1041
+ email: { level: "none", when: (_, node) => node.props.stackOnMobile !== false, shows: "its phone gap between every two children", source: "writer" }
1042
+ }
1043
+ }
1044
+ },
877
1045
  targets: { email: {}, web: {} },
878
1046
  editor: {
879
1047
  groups: [
@@ -886,7 +1054,7 @@ var Grid = defineComponent({
886
1054
  var Layer = defineComponent({
887
1055
  type: "Layer",
888
1056
  label: "Layer",
889
- 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.",
1057
+ 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.",
890
1058
  categories: ["layout", "block"],
891
1059
  props: {},
892
1060
  style: {
@@ -918,13 +1086,27 @@ var Layer = defineComponent({
918
1086
  }
919
1087
  });
920
1088
  var WRITTEN3 = { email: { level: "full", source: "writer" }, web: { level: "full", source: "writer" } };
1089
+ 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"];
921
1090
  var ROOT_PROPS = {
922
1091
  title: prop(text(), { support: WRITTEN3, label: "Title", description: "The document's title (shown in a browser tab, and by some email clients)." }),
923
- lang: prop(text({ maxLength: 35 }), { support: WRITTEN3, label: "Language", description: 'Its language, as a BCP 47 tag ("en", "ar").' }),
924
- dir: prop(enumOf(["ltr", "rtl", "auto"]), { support: WRITTEN3, label: "Text direction" }),
1092
+ lang: prop(text({ maxLength: 35 }), {
1093
+ support: {
1094
+ ...WRITTEN3,
1095
+ 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"] }
1096
+ },
1097
+ label: "Language",
1098
+ description: 'Its language, as a BCP 47 tag ("en", "ar").'
1099
+ }),
1100
+ dir: prop(enumOf(["ltr", "rtl", "auto"]), {
1101
+ support: {
1102
+ ...WRITTEN3,
1103
+ email: { level: "partial", values: ["rtl", "auto"], shows: "left to right: the client removes the direction", source: "caniemail", feature: "html-dir", clients: ["gmx", "web-de"] }
1104
+ },
1105
+ label: "Text direction"
1106
+ }),
925
1107
  fonts: prop(fontFaces(), {
926
1108
  support: {
927
- 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" },
1109
+ 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 },
928
1110
  web: { level: "full", source: "writer" }
929
1111
  },
930
1112
  label: "Web fonts",
@@ -1064,7 +1246,10 @@ var PageNumber = defineComponent({
1064
1246
  style: {},
1065
1247
  children: {},
1066
1248
  sizing: "none",
1067
- targets: { web: {} }
1249
+ targets: { web: {} },
1250
+ // A Page's or a Canvas's text has no page to number.
1251
+ roots: ["Document"],
1252
+ notAllowed: { code: "document/page-number", message: (root) => `A PageNumber is a page's number in a Document; this ${root} has no pages` }
1068
1253
  });
1069
1254
  function documentPages(root) {
1070
1255
  const pages = [];
@@ -1200,6 +1385,11 @@ var Page = defineComponent({
1200
1385
  props: {
1201
1386
  width: prop(length(["px"], { min: 200 }), { support: WRITTEN6, label: "Content width", description: "The widest the content gets.", default: 1200 }),
1202
1387
  description: prop(text(), { support: WRITTEN6, label: "Description", description: "The page's meta description." }),
1388
+ breakpoints: prop(breakpoints(), {
1389
+ support: { web: { level: "full", source: "writer" } },
1390
+ label: "Breakpoints",
1391
+ description: "Where its phone styles start: mobile, the widest window they apply to, in px (480 where it's not set)."
1392
+ }),
1203
1393
  ...ROOT_PROPS
1204
1394
  },
1205
1395
  style: ROOT_STYLE,
@@ -1207,6 +1397,12 @@ var Page = defineComponent({
1207
1397
  children: { categories: ["section", "layout", "block"] },
1208
1398
  sizing: "none",
1209
1399
  responsive: true,
1400
+ // The page writer writes the root's phone gap, not its phone text styles.
1401
+ conditionSupport: {
1402
+ mobile: Object.fromEntries(
1403
+ ["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" } }])
1404
+ )
1405
+ },
1210
1406
  targets: { web: {} },
1211
1407
  editor: {
1212
1408
  groups: [
@@ -1450,6 +1646,7 @@ var Image = defineComponent({
1450
1646
  },
1451
1647
  children: {},
1452
1648
  sizing: "shrink",
1649
+ conditionSupport: { mobile: { border: { email: { level: "none", shows: "its desktop border", source: "writer" } } } },
1453
1650
  targets: { email: {}, web: {} },
1454
1651
  editor: { groups: [{ title: "Image", paths: ["props.src", "props.alt", "style.width"] }, { title: "Box", paths: ["style.border", "style.borderRadius"] }] }
1455
1652
  });
@@ -1580,6 +1777,7 @@ var TableCell = defineComponent({
1580
1777
  },
1581
1778
  children: { text: true, categories: ["inline"], types: ["Image"] },
1582
1779
  sizing: "none",
1780
+ conditionSupport: { mobile: { border: { email: { level: "none", shows: "its desktop border", source: "writer" } } } },
1583
1781
  targets: { email: {}, web: {} },
1584
1782
  editor: { groups: [{ title: "Cell", paths: ["props.colSpan", "style.width", "style.verticalAlign"] }, TEXT_EDITOR, { title: "Box", paths: ["style.padding", "style.backgroundColor", "style.border"] }] }
1585
1783
  });
@@ -1697,17 +1895,27 @@ function checkDocument(input, options) {
1697
1895
  const extra = Object.keys(input).filter((key) => key !== "root");
1698
1896
  for (const key of extra) d.error("document/unknown-key", `"${key}" isn't part of a document (root)`, key);
1699
1897
  const rootDefinition = isObject2(input.root) && typeof input.root.type === "string" ? registry.get(input.root.type) : void 0;
1700
- const context = { d, registry, used: /* @__PURE__ */ new Set(), responsive: !!rootDefinition?.responsive };
1898
+ const context = { d, registry, used: /* @__PURE__ */ new Set(), root: rootDefinition };
1701
1899
  const root = input.root === void 0 ? null : readNode(input.root, "root", null, context);
1702
1900
  if (input.root === void 0) d.error("document/root", 'A document has a root: { type: "Email" }, "Page", \u2026', "root");
1703
1901
  return { document: root ? { root } : null, diagnostics: d.list };
1704
1902
  }
1705
1903
  function checkFragment(input, options) {
1706
1904
  const d = new Diagnostics();
1707
- const context = { d, registry: options.registry, idPrefix: options.idPrefix, used: new Set(options.taken), responsive: options.responsive };
1905
+ const context = { d, registry: options.registry, idPrefix: options.idPrefix, used: new Set(options.taken), root: options.root };
1708
1906
  const node = readNode(input, options.path, options.parent, context);
1709
1907
  return { node, diagnostics: d.list };
1710
1908
  }
1909
+ function rootList(roots) {
1910
+ const named = roots.map((root) => a(root).replace(/^A/, "a"));
1911
+ return named.length > 1 ? `${named.slice(0, -1).join(", ")} or ${named[named.length - 1]}` : named[0] ?? "no root";
1912
+ }
1913
+ function rootTakes(roots, root) {
1914
+ return !roots || !root || roots.includes(root.type);
1915
+ }
1916
+ function sharesOutput(definition, root) {
1917
+ return Object.keys(definition.targets).some((target) => target in root.targets);
1918
+ }
1711
1919
  function allowedIn(parent, child) {
1712
1920
  const rule2 = parent.children;
1713
1921
  return !!rule2.types?.includes(child.type) || child.categories.some((category) => rule2.categories?.includes(category));
@@ -1733,7 +1941,29 @@ function editDistance(from, to) {
1733
1941
  }
1734
1942
  return row[to.length];
1735
1943
  }
1944
+ var OTHER_BREAKPOINTS = /* @__PURE__ */ new Set(["base", "initial", "xs", "sm", "md", "lg", "xl", "2xl", "tablet", "desktop", "wide", "small", "medium", "large"]);
1945
+ function conditionHint(definition, key) {
1946
+ const name = key.trim().replace(/^(&?:+|_+|\$|@)/, "").replace(/Style$/, "").toLowerCase();
1947
+ const own = CUSTOM_TYPE.test(definition.type);
1948
+ const hoverStyles = definition.hover ?? [];
1949
+ const toHover = own ? `a template's own component takes only its place in a Row in its style: set hover styles in what it renders` : hoverStyles.length ? `hover styles go in its style's hover block, style={{ hover: { \u2026 } }} (it takes ${hoverStyles.join(", ")})` : `${a(definition.type).replace(/^A/, "a")} takes no hover styles (a Button's and a Link's colors do)`;
1950
+ const toPhone = own ? `a template's own component takes only its place in a Row in its style: set phone styles in what it renders` : `phone styles go in its style's mobile block, style={{ mobile: { \u2026 } }}`;
1951
+ if (name === "hover") return toHover;
1952
+ if (name === "mobile" || name === "phone" || name === "phones" || key.trim().startsWith("@media")) return toPhone;
1953
+ if (name === "focus" || name === "focus-visible" || name === "focusvisible" || name === "focus-within") {
1954
+ return hoverStyles.length && !own ? `keyboard focus gets its hover styles: set them in style={{ hover: { \u2026 } }}` : `there are no focus styles; keyboard focus gets a Button's and a Link's hover styles`;
1955
+ }
1956
+ if (OTHER_BREAKPOINTS.has(name)) {
1957
+ 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 }})`;
1958
+ }
1959
+ if (name === "active" || name === "pressed" || name === "press" || name === "visited" || name === "dark" || name === "light" || name === "print") {
1960
+ return `there are no ${name} styles: Elements has phone styles (mobile) and hover styles (hover) in style`;
1961
+ }
1962
+ return void 0;
1963
+ }
1736
1964
  function unknownProp(definition, key) {
1965
+ const condition = conditionHint(definition, key);
1966
+ if (condition) return `${a(definition.type)} has no prop "${key}": ${condition}`;
1737
1967
  if (Object.prototype.hasOwnProperty.call(definition.style, key)) return `${a(definition.type)} has no prop "${key}": it's a style, style={{ ${key}: \u2026 }}`;
1738
1968
  const known = Object.keys(definition.props);
1739
1969
  const meant = nearest(key, known);
@@ -1773,6 +2003,15 @@ function readNode(input, path, parent, context) {
1773
2003
  d.error("children/not-allowed", placementMessage(parent, definition), path);
1774
2004
  return null;
1775
2005
  }
2006
+ const root = context.root;
2007
+ if (root && !rootTakes(definition.roots, root) && sharesOutput(definition, root)) {
2008
+ const { code, message } = definition.notAllowed ?? {
2009
+ code: "component/not-supported",
2010
+ message: (under) => `${a(definition.type)} goes in ${rootList(definition.roots ?? [])}, not in ${a(under).replace(/^A/, "a")}`
2011
+ };
2012
+ d.error(code, message(root.type), path);
2013
+ return null;
2014
+ }
1776
2015
  let id;
1777
2016
  if (input.id !== void 0) {
1778
2017
  if (typeof input.id !== "string" || !ID.test(input.id)) {
@@ -1792,21 +2031,49 @@ function readNode(input, path, parent, context) {
1792
2031
  if (!NODE_KEYS.includes(key)) d.error("node/unknown-key", `"${key}" isn't part of a component (${NODE_KEYS.join(", ")})`, `${path}.${key}`, id);
1793
2032
  }
1794
2033
  const node = id === void 0 ? { type: definition.type } : { id, type: definition.type };
1795
- const props = readProps(input.props, definition, `${path}.props`, id, d);
2034
+ const props = readProps(input.props, definition, `${path}.props`, id, d, context.root);
1796
2035
  if (props) node.props = props;
1797
- const style = readStyles(input.style, definition, definition.style, `${path}.style`, id, d, "style", () => true);
1798
- if (style) node.style = style;
1799
- if (input.mobile !== void 0) {
1800
- 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);
1801
- else {
1802
- const mobile = readStyles(input.mobile, definition, definition.style, `${path}.mobile`, id, d, "mobile", (spec) => !!spec.mobile);
1803
- if (mobile) node.mobile = mobile;
2036
+ const lifted = {};
2037
+ let styleInput = input.style;
2038
+ if (isObject2(styleInput) && !CUSTOM_TYPE.test(definition.type)) {
2039
+ const rest = {};
2040
+ for (const [key, value] of Object.entries(styleInput)) {
2041
+ if (CONDITION_NAMES.has(key)) lifted[key] = value;
2042
+ else rest[key] = value;
1804
2043
  }
2044
+ styleInput = rest;
1805
2045
  }
1806
- if (input.hover !== void 0) {
1807
- const allowed = new Set(definition.hover ?? []);
1808
- const hover = readStyles(input.hover, definition, definition.style, `${path}.hover`, id, d, "hover", (_, key) => allowed.has(key));
1809
- if (hover) node.hover = hover;
2046
+ const style = readStyles(styleInput, definition, definition.style, `${path}.style`, id, d, "style", () => true, void 0, context.root);
2047
+ if (style) node.style = style;
2048
+ for (const condition of CONDITIONS) {
2049
+ const inStyle = lifted[condition.name];
2050
+ const given = input[condition.name];
2051
+ if (inStyle !== void 0 && given !== void 0) {
2052
+ d.error(
2053
+ "style/twice",
2054
+ `${condition.name} is given twice: as ${condition.name}={\u2026} and in style. Give it once, in style: style={{ ${condition.name}: { \u2026 } }}`,
2055
+ `${path}.style.${condition.name}`,
2056
+ id
2057
+ );
2058
+ continue;
2059
+ }
2060
+ let value = inStyle ?? given;
2061
+ if (value === void 0) continue;
2062
+ const at = inStyle !== void 0 ? `${path}.style.${condition.name}` : `${path}.${condition.name}`;
2063
+ if (condition.roots && !condition.roots(context.root)) {
2064
+ if (condition.notAllowed) d.error(condition.notAllowed.code, condition.notAllowed.message, at, id);
2065
+ continue;
2066
+ }
2067
+ if (isObject2(value) && Object.keys(value).some((key) => CONDITION_NAMES.has(key))) {
2068
+ const own = {};
2069
+ for (const [key, inner] of Object.entries(value)) {
2070
+ 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);
2071
+ else own[key] = inner;
2072
+ }
2073
+ value = own;
2074
+ }
2075
+ const read2 = readStyles(value, definition, definition.style, at, id, d, condition.name, (spec, key) => condition.allows(spec, key, definition), condition.for, context.root);
2076
+ if (read2) node[condition.name] = read2;
1810
2077
  }
1811
2078
  if (input.hidden !== void 0) {
1812
2079
  const hidden = readHidden(input.hidden, `${path}.hidden`, id, context);
@@ -1816,7 +2083,7 @@ function readNode(input, path, parent, context) {
1816
2083
  if (children.length) node.children = children;
1817
2084
  return node;
1818
2085
  }
1819
- function readProps(input, definition, path, id, d) {
2086
+ function readProps(input, definition, path, id, d, root) {
1820
2087
  const out = {};
1821
2088
  if (input !== void 0 && !isObject2(input)) {
1822
2089
  d.error("props/invalid", "props is an object", path, id);
@@ -1829,9 +2096,14 @@ function readProps(input, definition, path, id, d) {
1829
2096
  d.error("prop/unknown", unknownProp(definition, key), `${path}.${key}`, id);
1830
2097
  continue;
1831
2098
  }
2099
+ if (!rootTakes(spec.roots, root)) {
2100
+ 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);
2101
+ continue;
2102
+ }
1832
2103
  const parsed = spec.kind.parse(value);
1833
- if (parsed.ok) out[key] = parsed.value;
1834
- else d.error("prop/invalid", `${key}: ${parsed.message}`, `${path}.${key}`, id);
2104
+ if (parsed.ok) {
2105
+ if (!spec.kind.empty?.(parsed.value)) out[key] = parsed.value;
2106
+ } else d.error("prop/invalid", `${key}: ${parsed.message}`, `${path}.${key}`, id);
1835
2107
  }
1836
2108
  for (const [key, spec] of Object.entries(definition.props)) {
1837
2109
  if (spec.required && !(key in out) && !(isObject2(input) && input[key] !== void 0)) {
@@ -1848,6 +2120,8 @@ var UNSUPPORTED_CSS = {
1848
2120
  marginRight: "use padding",
1849
2121
  display: "the component sets it",
1850
2122
  position: "use a Layer for content placed over other content",
2123
+ backgroundImage: "put a Layer around it, its first child an Image (the Layer's height crops it to cover), the rest over it",
2124
+ background: "backgroundColor for a color; for an image, a Layer: its first child an Image, the rest over it",
1851
2125
  float: "use a Row to put things side by side",
1852
2126
  // Names Elements used before it took CSS's.
1853
2127
  align: "it's alignItems now",
@@ -1861,10 +2135,12 @@ function styleHint(definition, specs, key) {
1861
2135
  if (Object.prototype.hasOwnProperty.call(definition.props, key)) return `: it's a prop, ${key}={\u2026}`;
1862
2136
  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";
1863
2137
  if (UNSUPPORTED_CSS[key]) return `: ${UNSUPPORTED_CSS[key]}`;
2138
+ const condition = conditionHint(definition, key);
2139
+ if (condition) return `: ${condition}`;
1864
2140
  const meant = nearest(key, Object.keys(specs));
1865
2141
  return meant ? `: did you mean "${meant}"?` : "";
1866
2142
  }
1867
- function readStyles(input, definition, specs, path, id, d, what, allowed) {
2143
+ function readStyles(input, definition, specs, path, id, d, what, allowed, forLabel, root) {
1868
2144
  if (input === void 0) return void 0;
1869
2145
  if (!isObject2(input)) {
1870
2146
  d.error(`${what}/invalid`, `${what} is an object of style properties`, path, id);
@@ -1878,8 +2154,12 @@ function readStyles(input, definition, specs, path, id, d, what, allowed) {
1878
2154
  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);
1879
2155
  continue;
1880
2156
  }
2157
+ if (!rootTakes(spec.roots, root)) {
2158
+ 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);
2159
+ continue;
2160
+ }
1881
2161
  if (!allowed(spec, key)) {
1882
- d.error(`${what}/not-allowed`, what === "mobile" ? `"${key}" can't be set for phones` : `"${key}" can't be set for hover`, `${path}.${key}`, id);
2162
+ d.error(`${what}/not-allowed`, `"${key}" can't be set ${forLabel ?? "here"}`, `${path}.${key}`, id);
1883
2163
  continue;
1884
2164
  }
1885
2165
  const parsed = spec.kind.parse(value);
@@ -1890,7 +2170,7 @@ function readStyles(input, definition, specs, path, id, d, what, allowed) {
1890
2170
  }
1891
2171
  function readHidden(input, path, id, context) {
1892
2172
  const { d } = context;
1893
- if (!context.responsive) {
2173
+ if (!context.root?.responsive) {
1894
2174
  d.error("hidden/not-supported", "Hiding on phones or desktops is for an Email or a Page", path, id);
1895
2175
  return void 0;
1896
2176
  }
@@ -1901,7 +2181,7 @@ function readHidden(input, path, id, context) {
1901
2181
  const out = {};
1902
2182
  if (input.mobile === true) out.mobile = true;
1903
2183
  if (input.desktop === true) out.desktop = true;
1904
- if (out.mobile && out.desktop) d.warning("hidden/everywhere", "Hidden on phones and on desktops, it never shows", path, id);
2184
+ if (!shownAnywhere({ hidden: out })) d.warning("hidden/everywhere", "Hidden on phones and on desktops, it never shows", path, id);
1905
2185
  return Object.keys(out).length ? out : void 0;
1906
2186
  }
1907
2187
  function readChildren(input, definition, path, id, context) {
@@ -1934,6 +2214,22 @@ function readChildren(input, definition, path, id, context) {
1934
2214
  });
1935
2215
  return out;
1936
2216
  }
2217
+ var same = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
2218
+ function reportDeclared(d, node, at, key, value, declared, target, outlook = true) {
2219
+ if (!declared) return;
2220
+ for (const rule2 of [].concat(declared)) {
2221
+ if (rule2.level === "full") continue;
2222
+ if (rule2.level === "outlook" && !outlook) continue;
2223
+ if (rule2.values && !rule2.values.some((v) => same(v, value))) continue;
2224
+ if (rule2.when && !rule2.when(value, node)) continue;
2225
+ const asked = typeof value === "string" ? `${key}: "${value}"` : key;
2226
+ const clients = target === "email" ? rule2.clients ?? (rule2.level === "outlook" ? ["outlook"] : void 0) : void 0;
2227
+ const output = clients ? "some email clients" : target === "web" ? "a page" : target;
2228
+ 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}`];
2229
+ const [said, as] = rule2.source === "assumed" ? ["info", `${message} (not checked)`] : [severity, message];
2230
+ d.add(code, said, as, at, node.id, { clients, fix: rule2.fix, see: rule2.feature && caniemail(rule2.feature) });
2231
+ }
2232
+ }
1937
2233
  var MAX_DEPTH = 32;
1938
2234
  function templatePathsOf(document, paths) {
1939
2235
  const places = /* @__PURE__ */ new Map();
@@ -1950,7 +2246,7 @@ function templatePathsOf(document, paths) {
1950
2246
  return path;
1951
2247
  };
1952
2248
  }
1953
- function expandDocument(document, options) {
2249
+ function expandDocument(document, options, target) {
1954
2250
  const registry = registryOf(options);
1955
2251
  const d = new Diagnostics();
1956
2252
  const paths = /* @__PURE__ */ new WeakMap();
@@ -1962,7 +2258,20 @@ function expandDocument(document, options) {
1962
2258
  collect(document.root);
1963
2259
  const rootDefinition = registry.get(document.root.type);
1964
2260
  const responsive = !!rootDefinition?.responsive;
2261
+ const unshown = (node, path) => {
2262
+ const declared = target && registry.get(node.type)?.conditionSupport;
2263
+ if (!declared) return;
2264
+ for (const condition of CONDITIONS) {
2265
+ const block = blockOf(node, condition);
2266
+ const byStyle = declared[condition.name];
2267
+ if (!block || !byStyle || condition.hiddenKey && node.hidden?.[condition.hiddenKey]) continue;
2268
+ for (const [key, value] of Object.entries(block)) {
2269
+ reportDeclared(d, { type: node.type, id: node.id, props: node.props ?? {} }, `${path}.${condition.name}.${key}`, key, value, byStyle[key]?.[target], target);
2270
+ }
2271
+ }
2272
+ };
1965
2273
  const visit = (given, path, depth, self = path) => {
2274
+ unshown(given, path);
1966
2275
  const arrange = registry.get(given.type)?.arrange;
1967
2276
  if (!arrange) return walk(given, path, self, depth);
1968
2277
  const origins = /* @__PURE__ */ new Map();
@@ -1989,27 +2298,34 @@ function expandDocument(document, options) {
1989
2298
  return void 0;
1990
2299
  }
1991
2300
  let raw2;
2301
+ const reported = [];
1992
2302
  try {
1993
2303
  const style = { ...definition.defaultStyle, ...node.style };
1994
- raw2 = definition.expand(propsWithDefaults(definition, node.props), { register: (custom) => registry.add(custom), style, responsive });
2304
+ raw2 = definition.expand(propsWithDefaults(definition, node.props), { register: (custom) => registry.add(custom), style, responsive, report: (diagnostic) => reported.push(diagnostic) });
1995
2305
  } catch (error) {
1996
2306
  d.error("custom/render", `${node.type} failed to render: ${error instanceof Error ? error.message : String(error)}`, path, node.id);
1997
2307
  return void 0;
1998
2308
  }
2309
+ const at = `${path}(${node.type})`;
2310
+ for (const diagnostic of reported) d.list.push({ ...diagnostic, path: diagnostic.path === "root" || diagnostic.path.startsWith("root.") ? at + diagnostic.path.slice(4) : at });
1999
2311
  if (Array.isArray(raw2) || typeof raw2 !== "object" || raw2 === null) {
2000
2312
  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);
2001
2313
  return void 0;
2002
2314
  }
2003
- const { node: expanded, diagnostics } = checkFragment(raw2, { parent, responsive, idPrefix: node.id ?? null, taken, registry, path: `${path}(${node.type})` });
2315
+ const { node: expanded, diagnostics } = checkFragment(raw2, { parent, root: rootDefinition, idPrefix: node.id ?? null, taken, registry, path: `${path}(${node.type})` });
2004
2316
  for (const diagnostic of diagnostics) d.list.push(diagnostic);
2005
2317
  if (!expanded) return void 0;
2006
2318
  collectIds(expanded);
2007
2319
  if (node.style?.flex !== void 0) expanded.style = { ...expanded.style, flex: node.style.flex };
2008
2320
  if (node.hidden) expanded.hidden = { ...expanded.hidden, ...node.hidden };
2009
2321
  if (node.id && !expanded.id && !CUSTOM_TYPE.test(node.type)) expanded.id = node.id;
2010
- for (const [key, value] of Object.entries(node.mobile ?? {})) {
2011
- if (registry.get(expanded.type)?.style[key]?.mobile) expanded.mobile = { [key]: value, ...expanded.mobile };
2012
- 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);
2322
+ for (const condition of CONDITIONS) {
2323
+ const rendered = registry.get(expanded.type);
2324
+ for (const [key, value] of Object.entries(node[condition.name] ?? {})) {
2325
+ const spec = rendered?.style[key];
2326
+ if (rendered && spec && condition.allows(spec, key, rendered)) expanded[condition.name] = { [key]: value, ...expanded[condition.name] };
2327
+ 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);
2328
+ }
2013
2329
  }
2014
2330
  const inner = registry.get(expanded.type);
2015
2331
  if (isCustom(inner)) return expandOne(expanded, inner, parent, `${path}(${node.type})`, depth + 1, self);
@@ -2462,11 +2778,11 @@ var TEXT_CSS = [
2462
2778
  ["textAlign", "text-align", (v) => v],
2463
2779
  ["textTransform", "text-transform", (v) => v]
2464
2780
  ];
2465
- var same = (a2, b) => a2 === b || JSON.stringify(a2) === JSON.stringify(b);
2781
+ var same2 = (a2, b) => a2 === b || JSON.stringify(a2) === JSON.stringify(b);
2466
2782
  function textDeclarations(text2, parent, always = []) {
2467
2783
  const out = [];
2468
2784
  for (const [key, property, write3] of TEXT_CSS) {
2469
- if (!parent || always.includes(key) || !same(text2[key], parent[key])) out.push([property, write3(text2[key])]);
2785
+ if (!parent || always.includes(key) || !same2(text2[key], parent[key])) out.push([property, write3(text2[key])]);
2470
2786
  }
2471
2787
  return out;
2472
2788
  }
@@ -2542,7 +2858,9 @@ function classesOf(name, value) {
2542
2858
  if (name === "class") return (Array.isArray(value) ? value : [value]).filter((v) => typeof v === "string" && v !== "");
2543
2859
  const text2 = styleText(value, true);
2544
2860
  if (!text2) return [];
2545
- return [name === "phone" ? rule("phone", `&{${text2}}`) : rule("all", `&:hover{${text2}}`, "hover")];
2861
+ if (name === "phone") return [rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}{${text2}}`)];
2862
+ const selector = name === "hoverWithin" ? HOVER.sheet.within ?? HOVER.sheet.selector : HOVER.sheet.selector;
2863
+ return [rule(HOVER.sheet.media, `${selector}{${text2}}`, HOVER.sheet.level)];
2546
2864
  }
2547
2865
  function attributes2(props) {
2548
2866
  const out = [];
@@ -2550,7 +2868,7 @@ function attributes2(props) {
2550
2868
  let slot = -1;
2551
2869
  for (const [name, value] of Object.entries(props)) {
2552
2870
  if (name === "children" || name === "style" || value === void 0 || value === null || value === false) continue;
2553
- if (name === "class" || name === "phone" || name === "hover") {
2871
+ if (name === "class" || name === "phone" || name === "hover" || name === "hoverWithin") {
2554
2872
  const found = classesOf(name, value);
2555
2873
  if (found.length && slot < 0) slot = out.push("") - 1;
2556
2874
  classes.push(...found);
@@ -2566,8 +2884,8 @@ function withId(element, id) {
2566
2884
  if (!id) return element;
2567
2885
  const tag = /^<[a-zA-Z0-9]+/.exec(element.markup)?.[0];
2568
2886
  if (!tag) throw new Error("[Elements] An id goes on an element");
2569
- const insert = (markup) => `${tag} id="${escapeAttr(id)}"${markup.slice(tag.length)}`;
2570
- return new Html2(insert(element.markup), { ...element.options, start: element.options.start && insert(element.options.start) });
2887
+ const insert2 = (markup) => `${tag} id="${escapeAttr(id)}"${markup.slice(tag.length)}`;
2888
+ return new Html2(insert2(element.markup), { ...element.options, start: element.options.start && insert2(element.options.start) });
2571
2889
  }
2572
2890
  function h(type, props, ...children) {
2573
2891
  const all = props ?? {};
@@ -2594,6 +2912,98 @@ function Merged({ children }) {
2594
2912
  }
2595
2913
  var open = (element) => new Html2(element.options.start ?? element.markup);
2596
2914
  var close = (tag) => new Html2(`</${tag}>`);
2915
+ var BLOCK_MAP_VERSION = 1;
2916
+ var MARK_PAIRS = 16;
2917
+ var BlockMarks = class {
2918
+ constructor(pair) {
2919
+ this.nodes = [];
2920
+ this.open = String.fromCharCode(64976 + pair * 2);
2921
+ this.close = String.fromCharCode(64977 + pair * 2);
2922
+ }
2923
+ };
2924
+ var current = null;
2925
+ function withBlockMarks(marks, write3) {
2926
+ const before = current;
2927
+ current = marks;
2928
+ try {
2929
+ return write3();
2930
+ } finally {
2931
+ current = before;
2932
+ }
2933
+ }
2934
+ function markBlock(html, node) {
2935
+ if (!current) return html;
2936
+ const mark = `${current.open}${current.nodes.length}${current.close}`;
2937
+ const marked = insert(html, mark, false) ?? insert(html, mark, true);
2938
+ if (!marked) return html;
2939
+ current.nodes.push(node);
2940
+ return marked;
2941
+ }
2942
+ var OUTLOOK_OPEN = "<!--[if mso]>";
2943
+ var OUTLOOK_CLOSE = "<![endif]-->";
2944
+ var TAG2 = /<[a-zA-Z][a-zA-Z0-9]*/y;
2945
+ function tagEnd(markup, outlook) {
2946
+ for (let i = markup.indexOf("<"); i >= 0; i = markup.indexOf("<", i + 1)) {
2947
+ if (markup.startsWith(OUTLOOK_OPEN, i)) {
2948
+ if (outlook) {
2949
+ i += OUTLOOK_OPEN.length - 1;
2950
+ continue;
2951
+ }
2952
+ const end = markup.indexOf(OUTLOOK_CLOSE, i);
2953
+ if (end < 0) return -1;
2954
+ i = end + OUTLOOK_CLOSE.length - 1;
2955
+ continue;
2956
+ }
2957
+ if (markup.startsWith("<!--", i)) {
2958
+ const end = markup.indexOf("-->", i + 4);
2959
+ if (end < 0) return -1;
2960
+ i = end + 2;
2961
+ continue;
2962
+ }
2963
+ TAG2.lastIndex = i;
2964
+ const tag = TAG2.exec(markup);
2965
+ if (tag) return i + tag[0].length;
2966
+ }
2967
+ return -1;
2968
+ }
2969
+ function insert(html, mark, outlook) {
2970
+ const { list, kind: kind2, start } = html.options;
2971
+ if (list) {
2972
+ for (let i = 0; i < list.length; i++) {
2973
+ const marked = insert(list[i], mark, outlook);
2974
+ if (marked) return new Html2(html.markup, { ...html.options, list: list.map((piece, j) => j === i ? marked : piece) });
2975
+ }
2976
+ return null;
2977
+ }
2978
+ if (kind2 === "outlook" && !outlook) return null;
2979
+ const at = tagEnd(html.markup, outlook);
2980
+ if (at < 0) return null;
2981
+ const put = (text2) => text2.slice(0, at) + mark + text2.slice(at);
2982
+ return new Html2(put(html.markup), { ...html.options, start: start !== void 0 && at <= start.length ? put(start) : start });
2983
+ }
2984
+ function readMarks(marks, written) {
2985
+ const found = [];
2986
+ const seen = /* @__PURE__ */ new Set();
2987
+ const pieces2 = [];
2988
+ let length3 = 0;
2989
+ let last = 0;
2990
+ for (const match of written.matchAll(new RegExp(`${marks.open}(\\d+)${marks.close}`, "g"))) {
2991
+ const index = Number(match[1]);
2992
+ const node = marks.nodes[index];
2993
+ if (!node || seen.has(index)) return null;
2994
+ seen.add(index);
2995
+ const before = written.slice(last, match.index);
2996
+ pieces2.push(before);
2997
+ length3 += before.length;
2998
+ last = match.index + match[0].length;
2999
+ 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;
3000
+ found.push({ node, at: length3 });
3001
+ }
3002
+ pieces2.push(written.slice(last));
3003
+ const text2 = pieces2.join("");
3004
+ if (text2.includes(marks.open) || text2.includes(marks.close)) return null;
3005
+ return { text: text2, found };
3006
+ }
2597
3007
  function utf8Bytes(text2) {
2598
3008
  let bytes = 0;
2599
3009
  for (let i = 0; i < text2.length; i++) {
@@ -2680,7 +3090,7 @@ function fontFaces2(root, { swap }) {
2680
3090
  return [...faces.values()];
2681
3091
  }
2682
3092
  var isNode2 = (child) => typeof child !== "string";
2683
- var shown = (node) => !(node.hidden?.mobile && node.hidden?.desktop);
3093
+ var shown = (node) => shownAnywhere(node);
2684
3094
  function link2(props) {
2685
3095
  const { href, target } = linkParts(props.href);
2686
3096
  return {
@@ -2915,7 +3325,7 @@ var Button2 = ({ node, style, mobile, fill, phoneText: phoneText2, text: text2 }
2915
3325
  {
2916
3326
  align: TEXT_ALIGN[node.text.textAlign],
2917
3327
  bgcolor: bgcolor(style.backgroundColor),
2918
- hover: { backgroundColor: node.hover?.backgroundColor },
3328
+ hoverWithin: { backgroundColor: node.hover?.backgroundColor },
2919
3329
  phone: { border: mobile.border, backgroundColor: mobile.backgroundColor },
2920
3330
  style: { backgroundColor: style.backgroundColor, border: style.border, borderRadius: style.borderRadius, msoPaddingAlt: insets2(style.padding) }
2921
3331
  },
@@ -2926,8 +3336,11 @@ var Button2 = ({ node, style, mobile, fill, phoneText: phoneText2, text: text2 }
2926
3336
  phone: [phoneText2, { padding: mobile.padding }],
2927
3337
  hover: { color: node.hover?.color },
2928
3338
  style: [
2929
- // A button with a width fills it.
2930
- { display: width.width !== void 0 ? "block" : "inline-block", padding: style.padding },
3339
+ // A button with a width fills it. Its label's words stay whole (the email's word-break: break-word
3340
+ // is for long links in text): a button narrower than its padding otherwise sized itself to one letter
3341
+ // and drew its label a letter a line, in Gmail and browsers; it grows to its longest word instead, as
3342
+ // classic Outlook's and Apple Mail's do (captures of Elements 0.3.0, e3-fresh/g-0054).
3343
+ { display: width.width !== void 0 ? "block" : "inline-block", padding: style.padding, wordBreak: "normal" },
2931
3344
  fullText(node),
2932
3345
  { textDecoration: style.textDecoration ?? "none", borderRadius: style.borderRadius }
2933
3346
  ]
@@ -3391,27 +3804,42 @@ function propsOf(node, place, options) {
3391
3804
  options
3392
3805
  };
3393
3806
  }
3807
+ var UNMAPPED = /* @__PURE__ */ new Set(["LineBreak", "MergeTag"]);
3394
3808
  function write(node, place, options) {
3395
3809
  if (place.inline) {
3396
3810
  if (!shown(node)) return raw("");
3397
3811
  const component22 = INLINES[node.type];
3398
3812
  if (!component22) throw new Error(`[Elements] The email exporter can't render a ${node.type} inside text`);
3399
- return component22(propsOf(node, place, options));
3813
+ const html = component22(propsOf(node, place, options));
3814
+ return UNMAPPED.has(node.type) ? html : markBlock(html, node);
3400
3815
  }
3401
3816
  const component2 = BLOCKS[node.type];
3402
3817
  if (!component2) throw new Error(`[Elements] The email exporter can't render a ${node.type} here (in a ${place.parent.type})`);
3403
- return component2(propsOf(node, place, options));
3818
+ return markBlock(component2(propsOf(node, place, options)), node);
3404
3819
  }
3405
3820
  var EMAIL_PHONE_MARGIN = 20;
3406
3821
  var GMAIL_CLIP_BYTES = 102400;
3407
3822
  var GMAIL_STYLE_BYTES = 16384;
3408
3823
  function renderEmail(resolved, options = {}) {
3409
3824
  const root = resolved.root;
3410
- const sheet = new Stylesheet(options.classPrefix);
3411
3825
  const d = new Diagnostics();
3412
3826
  const fonts = fontFaces2(root, { swap: false });
3413
3827
  const width = root.width ?? 600;
3414
- const email = withStylesheet(sheet, () => String(Email2(propsOf(root, { parent: root, fill: false, mobile: {}, inline: false, rtl: false }, options))));
3828
+ const write3 = (marks) => {
3829
+ const sheet2 = new Stylesheet(options.classPrefix);
3830
+ const markup = withStylesheet(sheet2, () => withBlockMarks(marks, () => String(markBlock(Email2(propsOf(root, { parent: root, fill: false, mobile: {}, inline: false, rtl: false }, options)), root))));
3831
+ return { sheet: sheet2, markup };
3832
+ };
3833
+ let written = options.blockMap ? null : write3(null);
3834
+ let marked = null;
3835
+ for (let pair = 0; !written; pair++) {
3836
+ 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");
3837
+ const marks = new BlockMarks(pair);
3838
+ const attempt = write3(marks);
3839
+ marked = readMarks(marks, attempt.markup);
3840
+ if (marked) written = { sheet: attempt.sheet, markup: marked.text };
3841
+ }
3842
+ const { sheet, markup: email } = written;
3415
3843
  const background = root.style.backgroundColor;
3416
3844
  const lang = root.props.lang;
3417
3845
  const dir = root.props.dir;
@@ -3435,7 +3863,8 @@ function renderEmail(resolved, options = {}) {
3435
3863
  `<style>${stylesheet}</style>`
3436
3864
  ].join("");
3437
3865
  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>` : "";
3438
- 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>`;
3866
+ 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"]])}>`;
3867
+ const body = `${preheader}${opening}${email}</div>`;
3439
3868
  const bodyDeclarations = [
3440
3869
  ["margin", "0"],
3441
3870
  ["padding", "0"],
@@ -3443,20 +3872,38 @@ function renderEmail(resolved, options = {}) {
3443
3872
  ["background-color", background]
3444
3873
  ];
3445
3874
  const bodyStyle = css2(bodyDeclarations);
3446
- const html = `<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html${attrs({ lang, dir, xmlns: "http://www.w3.org/1999/xhtml", "xmlns:v": "urn:schemas-microsoft-com:vml", "xmlns:o": "urn:schemas-microsoft-com:office:office" })}><head>${head}</head><body${styleAttr(bodyDeclarations)}>${body}</body></html>`;
3875
+ 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)}>`;
3876
+ const html = `${shell}${body}</body></html>`;
3447
3877
  const bytes = utf8Bytes(html);
3878
+ const gmails = ["gmail", "gmail-app", "gmail-app-other"];
3448
3879
  if (bytes > GMAIL_CLIP_BYTES) {
3449
- 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");
3880
+ 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, {
3881
+ clients: gmails,
3882
+ fix: "shorten the email, or move long sections to a page: what follows the clip is behind a link"
3883
+ });
3450
3884
  }
3451
3885
  const styleBytes = utf8Bytes(stylesheet);
3452
3886
  if (styleBytes > GMAIL_STYLE_BYTES) {
3453
- d.warning("email/style-size", `The stylesheet is ${Math.round(styleBytes / 1024)} KB; Gmail reads ${GMAIL_STYLE_BYTES / 1024} KB of a <style>`, "root");
3887
+ 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, {
3888
+ clients: gmails,
3889
+ fix: "use fewer phone and hover styles: each one adds a rule"
3890
+ });
3454
3891
  }
3455
- return { html, head, body, bodyStyle, diagnostics: d.list };
3892
+ return { html, head, body, bodyStyle, diagnostics: d.list, ...marked ? { blocks: blockMap(resolved, marked.found, email, utf8Bytes(shell + preheader + opening)) } : {} };
3893
+ }
3894
+ function blockMap(resolved, found, email, start) {
3895
+ const templatePath = resolved.templatePath ?? ((path) => path);
3896
+ let offset = start;
3897
+ let from = 0;
3898
+ const entries = found.map(({ node, at }) => {
3899
+ offset += utf8Bytes(email.slice(from, at));
3900
+ from = at;
3901
+ return { path: templatePath(node.path), ...node.id ? { nodeId: node.id } : {}, offset };
3902
+ });
3903
+ return { version: BLOCK_MAP_VERSION, unit: "utf8", entries };
3456
3904
  }
3457
3905
  var isNode3 = (child) => typeof child !== "string";
3458
- var shown2 = (node) => !(node.hidden?.mobile && node.hidden?.desktop);
3459
- var same2 = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
3906
+ var same3 = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
3460
3907
  function support(root, target) {
3461
3908
  const d = new Diagnostics();
3462
3909
  const visit = (node) => {
@@ -3464,41 +3911,81 @@ function support(root, target) {
3464
3911
  const byProp = definition.defaultStyleBy ? definition.defaultStyleBy.values[String(node.props[definition.defaultStyleBy.prop])] : void 0;
3465
3912
  for (const [key, value] of Object.entries(node.style)) {
3466
3913
  const fallback2 = byProp?.[key] ?? definition.defaultStyle?.[key];
3467
- report(node, "style", key, value, fallback2, definition.style[key]?.support, target, d);
3914
+ if (same3(value, fallback2)) continue;
3915
+ reportDeclared(d, node, `${node.path}.style.${key}`, key, value, definition.style[key]?.support?.[target], target);
3468
3916
  }
3469
- for (const [key, value] of Object.entries(node.props)) report(node, "props", key, value, definition.props[key]?.default, definition.props[key]?.support, target, d);
3470
- for (const child of node.children) if (isNode3(child) && shown2(child)) visit(child);
3917
+ for (const [key, value] of Object.entries(node.props)) {
3918
+ if (same3(value, definition.props[key]?.default)) continue;
3919
+ reportDeclared(d, node, `${node.path}.props.${key}`, key, value, definition.props[key]?.support?.[target], target);
3920
+ }
3921
+ for (const condition of CONDITIONS) {
3922
+ const block = blockOf(node, condition);
3923
+ if (!block || condition.hiddenKey && node.hidden?.[condition.hiddenKey]) continue;
3924
+ const own = condition.support[target];
3925
+ reportDeclared(d, node, `${node.path}.${condition.name}`, condition.name, block, own, target);
3926
+ if (!own || own.level === "none") continue;
3927
+ for (const [key, value] of Object.entries(block)) {
3928
+ const rules = condition.styles?.[key]?.[target] ?? definition.style[key]?.support?.[target];
3929
+ reportDeclared(d, node, `${node.path}.${condition.name}.${key}`, key, value, rules, target, condition.outlookApplies);
3930
+ }
3931
+ }
3932
+ for (const child of node.children) if (isNode3(child) && shownAnywhere(child)) visit(child);
3471
3933
  };
3472
3934
  visit(root);
3473
3935
  return d.list;
3474
3936
  }
3475
- function report(node, where, key, value, fallback2, declared, target, d) {
3476
- const rules = declared?.[target];
3477
- if (!rules || same2(value, fallback2)) return;
3478
- for (const rule2 of [].concat(rules)) {
3479
- if (rule2.level === "full") continue;
3480
- if (rule2.values && !rule2.values.some((v) => same2(v, value))) continue;
3481
- if (rule2.when && !rule2.when(value, node)) continue;
3482
- const at = `${node.path}.${where}.${key}`;
3483
- const asked = typeof value === "string" ? `${key}: "${value}"` : key;
3484
- const output = target === "web" ? "a page" : target;
3485
- if (rule2.level === "none") d.warning(`${target}/unsupported`, `${asked} isn't possible in ${output}: ${rule2.shows}`, at, node.id);
3486
- else if (rule2.level === "partial") d.add(`${target}/degraded`, "info", `${asked} on this ${node.type} in ${output}: ${rule2.shows}`, at, node.id);
3487
- else d.add("email/outlook-degraded", "info", `${asked} on this ${node.type} in classic Outlook: ${rule2.shows}`, at, node.id);
3488
- }
3489
- }
3490
- function emailSupport(root) {
3937
+ function emailSupport(root, templatePath = (path) => path) {
3491
3938
  const d = new Diagnostics();
3492
3939
  d.list.push(...support(root, "email"));
3493
3940
  const webFonts = new Set((root.props.fonts ?? []).map((face) => face.family.toLowerCase()));
3941
+ const phone = { styled: false, phoneHidden: false, desktopHidden: false };
3494
3942
  const visit = (node) => {
3495
3943
  structure(node, d);
3496
3944
  fallback(node, webFonts, d);
3945
+ if (node.type === "Html") htmlBlock(node, d);
3946
+ if (node.type === "Image") imageFormat(node, "src", d);
3947
+ if (node.type === "Video" && node.props.thumbnail !== void 0) imageFormat(node, "thumbnail", d);
3948
+ if (node.mobile && Object.keys(node.mobile).length) phone.styled = true;
3949
+ if (node.hidden?.mobile && !builtIn(templatePath(node.path))) phone.phoneHidden = true;
3950
+ if (node.hidden?.desktop && !builtIn(templatePath(node.path))) phone.desktopHidden = true;
3497
3951
  for (const child of node.children) if (isNode2(child) && shown(child)) visit(child);
3498
3952
  };
3499
3953
  visit(root);
3954
+ phoneStyles(root, phone, d);
3500
3955
  return d.list;
3501
3956
  }
3957
+ function builtIn(place) {
3958
+ const made = [...place.matchAll(/\((\w+)\)/g)].pop()?.[1];
3959
+ return !!made && Object.prototype.hasOwnProperty.call(COMPONENTS, made);
3960
+ }
3961
+ var NO_PHONE_STYLES = ["gmail-app-other", "sfr", "laposte", "gmx", "web-de", "rainloop"];
3962
+ function phoneStyles(root, phone, d) {
3963
+ 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);
3964
+ if (!parts.length) return;
3965
+ 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, {
3966
+ clients: NO_PHONE_STYLES,
3967
+ fix: phone.desktopHidden ? "show what a reader needs on desktop too, and hide on phones only what they can do without" : void 0,
3968
+ see: caniemail("css-at-media")
3969
+ });
3970
+ }
3971
+ var IMAGE_FORMATS = {
3972
+ webp: { clients: ["outlook"], feature: "image-webp", name: "a WebP" },
3973
+ avif: { clients: ["outlook", "fastmail", "gmx", "web-de", "orange", "sfr", "mail-ru", "ionos"], feature: "image-avif", name: "an AVIF" },
3974
+ svg: { clients: ["mail-ru"], feature: "image-svg", name: "an SVG" }
3975
+ };
3976
+ 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];
3977
+ function imageFormat(node, key, d) {
3978
+ const src = assetParts(node.props[key]).src;
3979
+ const format = FORMAT_IN.map((pattern) => pattern.exec(src)?.[1]).find(Boolean)?.toLowerCase();
3980
+ const known = format && Object.prototype.hasOwnProperty.call(IMAGE_FORMATS, format) ? IMAGE_FORMATS[format] : void 0;
3981
+ if (!known) return;
3982
+ const what = key === "src" ? "This Image is" : "This Video's thumbnail is";
3983
+ d.warning("email/image-format", `${what} ${known.name} file, which these email clients don't show`, `${node.path}.props.${key}`, node.id, {
3984
+ clients: known.clients,
3985
+ fix: "use a PNG, JPEG or GIF file",
3986
+ see: caniemail(known.feature)
3987
+ });
3988
+ }
3502
3989
  function fallback(node, webFonts, d) {
3503
3990
  const stack = node.style.fontFamily;
3504
3991
  if (typeof stack !== "string" || !webFonts.size) return;
@@ -3507,9 +3994,10 @@ function fallback(node, webFonts, d) {
3507
3994
  if (!last || !webFonts.has(last.toLowerCase())) return;
3508
3995
  d.warning(
3509
3996
  "email/font-fallback",
3510
- `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"`,
3997
+ `This font stack ends with the web font "${last}": where it doesn't load, the email client picks its own font`,
3511
3998
  `${node.path}.style.fontFamily`,
3512
- node.id
3999
+ node.id,
4000
+ { clients: NO_WEB_FONTS, fix: `end it with a font the client has: "${stack}, Arial, sans-serif"`, see: caniemail("css-at-font-face") }
3513
4001
  );
3514
4002
  }
3515
4003
  function structure(node, d) {
@@ -3522,7 +4010,8 @@ function structure(node, d) {
3522
4010
  "info",
3523
4011
  `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`,
3524
4012
  path,
3525
- id
4013
+ id,
4014
+ { clients: ["outlook"], fix: "put less in it, or let it stack on phones (stackOnMobile)" }
3526
4015
  );
3527
4016
  }
3528
4017
  const lines2 = wrapsWithGap(node);
@@ -3558,15 +4047,95 @@ function structure(node, d) {
3558
4047
  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);
3559
4048
  }
3560
4049
  }
4050
+ var HTML_CSS = [
4051
+ { 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" },
4052
+ { 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" },
4053
+ {
4054
+ pattern: /(^|[\s;{"'])position\s*:\s*absolute\b/i,
4055
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "hey", "sfr", "laposte", "mail-ru", "gmx", "web-de"],
4056
+ removes: "position: absolute: the client removes it",
4057
+ fix: "lay it out without position (a Layer puts content over an image)",
4058
+ feature: "css-position"
4059
+ },
4060
+ {
4061
+ // Outlook.com removes every position (captures); the others not named keep relative.
4062
+ pattern: /(^|[\s;{"'])position\s*:\s*relative\b/i,
4063
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "mail-ru", "gmx", "web-de"],
4064
+ removes: "position: relative: the client removes it, and what it moves stays where it is",
4065
+ fix: "move it with padding instead",
4066
+ feature: "css-position",
4067
+ use: "relative"
4068
+ },
4069
+ {
4070
+ pattern: /var\(\s*--|(^|[\s;{"'])--[\w-]+\s*:/i,
4071
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "orange", "mail-ru", "fastmail", "gmx", "web-de"],
4072
+ removes: "CSS variables: the client removes var() and their declarations",
4073
+ fix: "write the values themselves",
4074
+ feature: "css-variables"
4075
+ },
4076
+ {
4077
+ pattern: /gradient\(/i,
4078
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail-app", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
4079
+ removes: "a gradient: the client removes it",
4080
+ fix: "give it a solid background color too, or use an image",
4081
+ feature: "css-linear-gradient"
4082
+ },
4083
+ { 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()" },
4084
+ { 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)" },
4085
+ {
4086
+ pattern: /<style[\s>]/i,
4087
+ clients: ["gmail", "gmail-app", "gmail-app-other", "protonmail", "gmx", "web-de", "sfr", "laposte", "rainloop"],
4088
+ removes: "a <style> in the email's body: the client removes it, and what it styles shows without it",
4089
+ fix: "write the styles inline, on the elements they're for",
4090
+ feature: "html-style"
4091
+ },
4092
+ { 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" },
4093
+ { 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" },
4094
+ { 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" },
4095
+ {
4096
+ pattern: /display\s*:\s*inline-flex\b/i,
4097
+ clients: ["outlook", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
4098
+ removes: "display: inline-flex: the client lays it out without flex",
4099
+ fix: "lay it out with a Row",
4100
+ feature: "css-display-flex",
4101
+ use: "inline"
4102
+ },
4103
+ { 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" },
4104
+ { 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" },
4105
+ {
4106
+ pattern: /object-fit\s*:/i,
4107
+ clients: ["outlook", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
4108
+ removes: "object-fit: the client stretches the image to its box",
4109
+ fix: "crop the image file to the size it's shown at",
4110
+ feature: "css-object-fit"
4111
+ },
4112
+ {
4113
+ pattern: /(^|[\s;{"'])aspect-ratio\s*:/i,
4114
+ clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "samsung-email", "thunderbird", "fastmail", "gmx", "web-de", "orange"],
4115
+ removes: "aspect-ratio: the client removes it",
4116
+ fix: "give it a width and a height",
4117
+ feature: "css-aspect-ratio"
4118
+ }
4119
+ ];
4120
+ var OUTLOOK_ONLY2 = /<!--\[if\s+(?:(?:gte?|lte?)\s+)?mso[^\]]*\]>[\s\S]*?<!\[endif\]-->/gi;
4121
+ function htmlBlock(node, d) {
4122
+ if (typeof node.props.html !== "string") return;
4123
+ const markup = node.props.html.replace(OUTLOOK_ONLY2, "");
4124
+ for (const check of HTML_CSS) {
4125
+ if (check.pattern.test(markup)) {
4126
+ 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) });
4127
+ }
4128
+ }
4129
+ }
3561
4130
  var isNode4 = (child) => typeof child !== "string";
3562
4131
  var OUTPUT = { pdf: "a PDF", image: "an image" };
3563
4132
  function pagedSupport(root, output) {
3564
4133
  const d = new Diagnostics();
3565
4134
  const where = OUTPUT[output];
3566
4135
  const visit = (node) => {
3567
- if (node.hidden?.desktop && node.hidden?.mobile) return;
3568
- if (node.hover && Object.keys(node.hover).length) {
3569
- d.warning(`${output}/unsupported`, `A hover isn't possible in ${where}: nothing points at it. The ${node.type} shows its own colors`, `${node.path}.hover`, node.id);
4136
+ if (!shownAnywhere(node)) return;
4137
+ for (const condition of CONDITIONS) {
4138
+ if (condition.paged && blockOf(node, condition)) d.warning(`${output}/unsupported`, condition.paged(node, where), `${node.path}.${condition.name}`, node.id);
3570
4139
  }
3571
4140
  if (node.type === "Video") {
3572
4141
  const shows = output === "pdf" ? "its thumbnail (where its file is given) or its title, linked to it" : "its title";
@@ -3719,15 +4288,15 @@ function classesFor(node, parent) {
3719
4288
  const classes = [];
3720
4289
  if (node.mobile) {
3721
4290
  const { width, ...rest } = node.mobile;
3722
- if (Object.keys(rest).length) classes.push(rule("phone", `&{${css2(declarationsOf(rest), true)}}`));
4291
+ if (Object.keys(rest).length) classes.push(rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}{${css2(declarationsOf(rest), true)}}`));
3723
4292
  const line2 = node.type === "Divider" && node.style.padding !== void 0 ? ">hr" : "";
3724
- if (width !== void 0) classes.push(rule("phone", `&${line2}{${css2(declarationsOf({ width }), true)}}`));
4293
+ if (width !== void 0) classes.push(rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}${line2}{${css2(declarationsOf({ width }), true)}}`));
3725
4294
  const own = node.style.width === void 0 && (node.style.flex === "none" || node.style.flex === 0);
3726
4295
  const basis = own ? void 0 : length2(phoneWidthInRow(node, parent));
3727
4296
  if (basis !== void 0) classes.push(rule("phone", `&{flex:0 1 ${basis}!important}`));
3728
4297
  }
3729
4298
  classes.push(...hidingClasses(node.hidden));
3730
- if (node.hover) classes.push(rule("all", `&:hover{${css2(declarationsOf(node.hover), true)}}`, "hover"));
4299
+ if (node.hover) classes.push(rule(HOVER.sheet.media, `${HOVER.sheet.selector}{${css2(declarationsOf(node.hover), true)}}`, HOVER.sheet.level));
3731
4300
  if (node.type === "Row" && node.props.stackOnMobile !== false) {
3732
4301
  classes.push(
3733
4302
  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")
@@ -3935,17 +4504,17 @@ var Layer3 = ({ node, style, parent, placement, classes, children }) => /* @__PU
3935
4504
  },
3936
4505
  children()
3937
4506
  );
3938
- var current = { page: 1, total: 1 };
4507
+ var current2 = { page: 1, total: 1 };
3939
4508
  function numbering(numbers, write3) {
3940
- const before = current;
3941
- current = numbers;
4509
+ const before = current2;
4510
+ current2 = numbers;
3942
4511
  try {
3943
4512
  return write3();
3944
4513
  } finally {
3945
- current = before;
4514
+ current2 = before;
3946
4515
  }
3947
4516
  }
3948
- var PageNumber2 = ({ node }) => /* @__PURE__ */ h(Fragment, null, String(node.props.total ? current.total : current.page));
4517
+ var PageNumber2 = ({ node }) => /* @__PURE__ */ h(Fragment, null, String(node.props.total ? current2.total : current2.page));
3949
4518
  function documentCss(root) {
3950
4519
  const box2 = pageBox(root.props);
3951
4520
  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}}`;
@@ -4119,8 +4688,23 @@ var Video3 = ({ node, style, placement, classes }) => /* @__PURE__ */ h(
4119
4688
  ))
4120
4689
  );
4121
4690
  var PAGE_PHONE_WIDTH = 480;
4122
- var PHONE = `@media (max-width: ${PAGE_PHONE_WIDTH}px)`;
4123
- var DESKTOP = `@media (min-width: ${PAGE_PHONE_WIDTH + 1}px)`;
4691
+ function pagePhoneWidth(root) {
4692
+ const mobile = root.props.breakpoints?.mobile;
4693
+ return typeof mobile === "number" ? mobile : PAGE_PHONE_WIDTH;
4694
+ }
4695
+ function pageBreakpoints(root) {
4696
+ const mobile = root.props.breakpoints?.mobile;
4697
+ const width = typeof root.props.width === "number" ? root.props.width : void 0;
4698
+ if (root.type !== "Page" || typeof mobile !== "number" || width === void 0 || mobile < width) return [];
4699
+ return [
4700
+ {
4701
+ code: "page/phone-width",
4702
+ severity: "warning",
4703
+ 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}`,
4704
+ path: "root.props.breakpoints"
4705
+ }
4706
+ ];
4707
+ }
4124
4708
  var COMPONENTS2 = {
4125
4709
  Section: Section3,
4126
4710
  Row: Row3,
@@ -4181,7 +4765,8 @@ function renderPage(resolved, options = {}) {
4181
4765
  const writer2 = document ? Document2 : root.type === "Canvas" ? Canvas2 : Page2;
4182
4766
  const body = withStylesheet(sheet, () => String(writer2(propsOf2(root, root, [], options))));
4183
4767
  const fixed = !root.definition.responsive;
4184
- const stylesheet = [...fonts, sheet.css(fixed ? null : PHONE, fixed ? null : DESKTOP), document ? documentCss(root) : ""].join("");
4768
+ const phoneWidth2 = pagePhoneWidth(root);
4769
+ const stylesheet = [...fonts, sheet.css(fixed ? null : `@media (max-width: ${phoneWidth2}px)`, fixed ? null : `@media (min-width: ${phoneWidth2 + 1}px)`), document ? documentCss(root) : ""].join("");
4185
4770
  const head = [
4186
4771
  '<meta charset="utf-8">',
4187
4772
  '<meta name="viewport" content="width=device-width, initial-scale=1">',
@@ -4201,7 +4786,7 @@ function renderText(resolved, options = {}) {
4201
4786
  const tag = options.mergeTag ?? ((name) => `{{${name}}}`);
4202
4787
  const inline = (node, upper = false) => node.children.map((child) => {
4203
4788
  if (typeof child === "string") return upper ? child.toUpperCase() : child;
4204
- if (child.hidden?.mobile && child.hidden?.desktop) return "";
4789
+ if (!shownAnywhere(child)) return "";
4205
4790
  switch (child.type) {
4206
4791
  case "LineBreak":
4207
4792
  return "\n";
@@ -4225,7 +4810,7 @@ function renderText(resolved, options = {}) {
4225
4810
  return alt ? `[${upper ? alt.toUpperCase() : alt}]` : "";
4226
4811
  };
4227
4812
  const blocks2 = (node) => node.children.flatMap((child) => {
4228
- if (typeof child === "string" || child.hidden?.mobile && child.hidden?.desktop) return [];
4813
+ if (typeof child === "string" || !shownAnywhere(child)) return [];
4229
4814
  switch (child.type) {
4230
4815
  case "Heading":
4231
4816
  return [tidy(inline(child, Number(child.props.level) <= 2))];
@@ -4263,8 +4848,8 @@ function renderText(resolved, options = {}) {
4263
4848
  return blocks2(child);
4264
4849
  }
4265
4850
  });
4266
- const shown32 = (node) => !(node.hidden?.mobile && node.hidden?.desktop);
4267
- const list = (node, indent) => node.children.filter((child) => typeof child !== "string" && shown32(child)).map((item, index) => {
4851
+ const shown22 = (node) => shownAnywhere(node);
4852
+ const list = (node, indent) => node.children.filter((child) => typeof child !== "string" && shown22(child)).map((item, index) => {
4268
4853
  const marker2 = node.props.type === "number" ? `${Number(node.props.start) + index}. ` : "- ";
4269
4854
  const lines2 = [];
4270
4855
  let run = [];
@@ -4280,13 +4865,13 @@ function renderText(resolved, options = {}) {
4280
4865
  }
4281
4866
  flush();
4282
4867
  if (!lines2.length) lines2.push(`${indent}${marker2}`.trimEnd());
4283
- if (shown32(child)) lines2.push(list(child, `${indent} `));
4868
+ if (shown22(child)) lines2.push(list(child, `${indent} `));
4284
4869
  }
4285
4870
  flush();
4286
4871
  return (lines2.length ? lines2 : [`${indent}${marker2}`.trimEnd()]).join("\n");
4287
4872
  }).join("\n");
4288
- const table = (node) => node.children.filter((row) => typeof row !== "string" && shown32(row)).map(
4289
- (row) => row.children.filter((cell) => typeof cell !== "string" && shown32(cell)).map((cell) => tidy(inline(cell))).join(" | ")
4873
+ const table = (node) => node.children.filter((row) => typeof row !== "string" && shown22(row)).map(
4874
+ (row) => row.children.filter((cell) => typeof cell !== "string" && shown22(cell)).map((cell) => tidy(inline(cell))).join(" | ")
4290
4875
  ).join("\n");
4291
4876
  return blocks2(resolved.root).filter((block) => block.length > 0).join("\n\n");
4292
4877
  }
@@ -4329,45 +4914,39 @@ var OUTPUTS = {
4329
4914
  Email: { target: "email", ready: true }
4330
4915
  };
4331
4916
  function prepareTemplate(input, options = {}) {
4917
+ checkEmailClients(options.emailClients);
4332
4918
  const registry = registryOf(options);
4333
4919
  const checked = checkDocument(input, registry);
4334
4920
  const diagnostics = [...checked.diagnostics];
4335
4921
  const failed = () => diagnostics.some((d) => d.severity === "error");
4336
4922
  if (!checked.document || failed()) return { resolved: null, target: null, diagnostics };
4337
4923
  const root = checked.document.root.type;
4924
+ const done = (resolved2, target) => {
4925
+ 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" });
4926
+ return { resolved: resolved2, target, diagnostics: root === "Email" ? forEmailClients(diagnostics, options.emailClients) : diagnostics };
4927
+ };
4338
4928
  const output = OUTPUTS[root];
4339
4929
  if (!output?.ready) {
4340
4930
  diagnostics.push({ code: "output/unavailable", severity: "error", message: output?.later ?? `A ${root} can't be rendered yet`, path: "root" });
4341
- return { resolved: null, target: null, diagnostics };
4931
+ return done(null, null);
4342
4932
  }
4343
- const expanded = expandDocument(checked.document, registry);
4933
+ const expanded = expandDocument(checked.document, registry, output.target);
4344
4934
  diagnostics.push(...expanded.diagnostics);
4345
- if (failed()) return { resolved: null, target: output.target, diagnostics };
4935
+ if (failed()) return done(null, output.target);
4346
4936
  diagnostics.push(...checkTargetExpanded(expanded.document, output.target, registry, expanded.paths));
4347
- if (failed()) return { resolved: null, target: output.target, diagnostics };
4937
+ if (failed()) return done(null, output.target);
4348
4938
  const resolved = resolveExpanded(expanded.document, registry, expanded.paths);
4349
4939
  diagnostics.push(...resolved.diagnostics);
4350
- const found = output.target === "email" ? emailSupport(resolved.root) : support(resolved.root, output.target);
4940
+ const found = output.target === "email" ? emailSupport(resolved.root, resolved.templatePath) : support(resolved.root, output.target);
4351
4941
  if (root === "Document" || root === "Canvas") found.push(...overflow(resolved.root));
4352
- if (root !== "Document") found.push(...pageNumbersOutside(resolved.root));
4942
+ found.push(...pageBreakpoints(resolved.root));
4353
4943
  diagnostics.push(...inTemplate(resolved, found));
4354
- return { resolved: failed() ? null : resolved, target: output.target, diagnostics };
4944
+ return done(failed() ? null : resolved, output.target);
4355
4945
  }
4356
4946
  function inTemplate(resolved, diagnostics) {
4357
4947
  const { templatePath } = resolved;
4358
4948
  return templatePath ? diagnostics.map((diagnostic) => ({ ...diagnostic, path: templatePath(diagnostic.path) })) : diagnostics;
4359
4949
  }
4360
- function pageNumbersOutside(root) {
4361
- const out = [];
4362
- const visit = (node) => {
4363
- if (node.type === "PageNumber") {
4364
- 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 } : {} });
4365
- }
4366
- for (const child of node.children) if (typeof child !== "string") visit(child);
4367
- };
4368
- visit(root);
4369
- return out;
4370
- }
4371
4950
  function validateTemplate(input, options = {}) {
4372
4951
  const { diagnostics, resolved } = prepareTemplate(input, options);
4373
4952
  const root = resolved?.root;
@@ -4376,12 +4955,22 @@ function validateTemplate(input, options = {}) {
4376
4955
  return { valid: !all.some((d) => d.severity === "error"), diagnostics: all };
4377
4956
  }
4378
4957
  function renderTemplate(input, options = {}) {
4958
+ 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" }]);
4379
4959
  const { resolved, target, diagnostics } = prepareTemplate(input, options);
4380
4960
  if (!resolved) throw new ElementsError(diagnostics);
4381
4961
  const text2 = renderText(resolved, options);
4382
4962
  const written = target === "email" ? renderEmail(resolved, options) : { ...renderPage(resolved, options), diagnostics: [] };
4383
- const shown32 = (html) => options.pretty ? prettyHtml(html) : html;
4384
- return { html: shown32(written.html), head: shown32(written.head), body: shown32(written.body), bodyStyle: written.bodyStyle, text: text2, diagnostics: [...diagnostics, ...inTemplate(resolved, written.diagnostics)] };
4963
+ const shown22 = (html) => options.pretty ? prettyHtml(html) : html;
4964
+ const blocks2 = "blocks" in written ? written.blocks : void 0;
4965
+ return {
4966
+ html: shown22(written.html),
4967
+ head: shown22(written.head),
4968
+ body: shown22(written.body),
4969
+ bodyStyle: written.bodyStyle,
4970
+ text: text2,
4971
+ diagnostics: [...diagnostics, ...inTemplate(resolved, forEmailClients(written.diagnostics, options.emailClients))],
4972
+ ...blocks2 ? { blocks: blocks2 } : {}
4973
+ };
4385
4974
  }
4386
4975
 
4387
4976
  // src/utils/collect.ts
@@ -4412,7 +5001,7 @@ function isAsyncComponent(type) {
4412
5001
  }
4413
5002
  var SHARED2 = Symbol.for("@hyper-elements/react/render-builds");
4414
5003
  var using = globalThis[SHARED2] ?? (globalThis[SHARED2] = []);
4415
- var same3 = (a2, b) => a2.renderToString === b.renderToString;
5004
+ var same4 = (a2, b) => a2.renderToString === b.renderToString;
4416
5005
  function dispatcher() {
4417
5006
  const internals = React2;
4418
5007
  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;
@@ -4458,7 +5047,7 @@ function secondBuild(outer) {
4458
5047
  for (const requirer of requirers) {
4459
5048
  try {
4460
5049
  const build = requirer()?.(id);
4461
- if (build?.renderToStaticMarkup && !same3(build, defaultBuild) && ![null, outer].includes(dispatcherOf(build))) {
5050
+ if (build?.renderToStaticMarkup && !same4(build, defaultBuild) && ![null, outer].includes(dispatcherOf(build))) {
4462
5051
  second = build;
4463
5052
  break search;
4464
5053
  }
@@ -4472,11 +5061,11 @@ function secondBuild(outer) {
4472
5061
  var buildOf = (outer) => outer === null ? void 0 : [defaultBuild, second].find((b) => !!b && dispatcherOf(b) === outer);
4473
5062
  function withBuild(render2) {
4474
5063
  const outer = dispatcher();
4475
- let build = current2();
5064
+ let build = current3();
4476
5065
  const theirs = using.length === 0 || collecting() !== void 0 ? buildOf(outer) : void 0;
4477
5066
  const busy = [];
4478
5067
  if (theirs) {
4479
- const free = (b) => !same3(b, theirs) && !using.some((u) => same3(u, b));
5068
+ const free = (b) => !same4(b, theirs) && !using.some((u) => same4(u, b));
4480
5069
  const other = free(defaultBuild) ? defaultBuild : secondBuild(outer);
4481
5070
  if (other && free(other)) {
4482
5071
  busy.push(theirs);
@@ -4501,10 +5090,10 @@ function rendersHere(build) {
4501
5090
  return false;
4502
5091
  }
4503
5092
  }
4504
- function current2() {
5093
+ function current3() {
4505
5094
  return using[using.length - 1] ?? defaultBuild;
4506
5095
  }
4507
- var renderToString = (element, options) => current2().renderToString(element, options);
5096
+ var renderToString = (element, options) => current3().renderToString(element, options);
4508
5097
 
4509
5098
  // src/utils/render-scope.ts
4510
5099
  function withRenderScope(render2) {
@@ -4558,7 +5147,7 @@ function withoutAsync(children, collection) {
4558
5147
  }
4559
5148
  var ENTITIES2 = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#x27;": "'" };
4560
5149
  var unescape = (text2) => text2.replace(/&amp;|&lt;|&gt;|&quot;|&#x27;/g, (entity) => ENTITIES2[entity]);
4561
- var NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "style", "mobile", "hover", "hidden"]);
5150
+ var NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "style", "hidden"]);
4562
5151
  function toJsonNode({ type, props }, diagnostics) {
4563
5152
  const node = { type, children: [] };
4564
5153
  const semantic = {};
@@ -4683,10 +5272,10 @@ function collectTemplate(element) {
4683
5272
  path: "jsx"
4684
5273
  });
4685
5274
  }
4686
- return { document: null, diagnostics, components };
5275
+ return { document: null, diagnostics, components, collected: null };
4687
5276
  }
4688
5277
  const result = checkDocument({ root: top[0] }, new Registry(components));
4689
- return { document: result.document, diagnostics: [...diagnostics, ...result.diagnostics], components };
5278
+ return { document: result.document, diagnostics: [...diagnostics, ...result.diagnostics], components, collected: top[0] };
4690
5279
  }
4691
5280
 
4692
5281
  // src/primitives/components.tsx
@@ -4776,7 +5365,8 @@ function component(spec) {
4776
5365
  expand(props, context) {
4777
5366
  const { nodes, components, diagnostics } = collectNodes(spec.render(props));
4778
5367
  for (const used of components) context.register(used);
4779
- if (diagnostics.length) throw new Error(formatDiagnostics(diagnostics));
5368
+ if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) throw new Error(formatDiagnostics(diagnostics));
5369
+ for (const diagnostic of diagnostics) context.report(diagnostic);
4780
5370
  return nodes.length === 1 ? nodes[0] : nodes;
4781
5371
  }
4782
5372
  });
@@ -4795,12 +5385,12 @@ function component(spec) {
4795
5385
  // src/primitives/outline.ts
4796
5386
  var LONGEST = 80;
4797
5387
  var clip = (text2) => text2.length > LONGEST ? `${text2.slice(0, LONGEST - 1)}\u2026` : text2;
4798
- var shown3 = (node) => !(node.hidden?.mobile && node.hidden?.desktop);
4799
- var blocks = (node) => node.children.filter((child) => typeof child !== "string" && shown3(child));
5388
+ var shown2 = (node) => shownAnywhere(node);
5389
+ var blocks = (node) => node.children.filter((child) => typeof child !== "string" && shown2(child));
4800
5390
  function words(node) {
4801
5391
  return node.children.map((child) => {
4802
5392
  if (typeof child === "string") return child;
4803
- if (!shown3(child) || child.type === "List") return "";
5393
+ if (!shown2(child) || child.type === "List") return "";
4804
5394
  if (child.type === "MergeTag") return `{{${child.props.name}}}`;
4805
5395
  if (child.type === "LineBreak") return " / ";
4806
5396
  if (child.type === "Image") return `[${child.props.alt ?? ""}]`;
@@ -4851,14 +5441,16 @@ function template(element, call) {
4851
5441
  return collectTemplate(element);
4852
5442
  }
4853
5443
  async function render(element, options = {}) {
5444
+ checkEmailClients(options.emailClients);
4854
5445
  const { document, diagnostics, components } = template(element, "render");
4855
5446
  if (!document || diagnostics.some((d) => d.severity === "error")) throw new ElementsError(diagnostics);
4856
5447
  const rendered = renderTemplate(document, { ...options, components });
4857
5448
  return { ...rendered, diagnostics: [...diagnostics, ...rendered.diagnostics] };
4858
5449
  }
4859
- function validate(element) {
5450
+ function validate(element, options = {}) {
5451
+ checkEmailClients(options.emailClients);
4860
5452
  const { document, diagnostics, components } = template(element, "validate");
4861
- const all = [...diagnostics, ...document && !diagnostics.some((d) => d.severity === "error") ? validateTemplate(document, { components }).diagnostics : []];
5453
+ const all = [...diagnostics, ...document && !diagnostics.some((d) => d.severity === "error") ? validateTemplate(document, { components, emailClients: options.emailClients }).diagnostics : []];
4862
5454
  return { valid: !all.some((d) => d.severity === "error"), diagnostics: all };
4863
5455
  }
4864
5456
  function outline(element) {
@@ -4869,15 +5461,16 @@ function outline(element) {
4869
5461
  if (!root) throw new ElementsError([...diagnostics, ...prepared.diagnostics]);
4870
5462
  return outlineOf(root);
4871
5463
  }
4872
- function prepare(element) {
5464
+ function prepare(element, options = {}) {
5465
+ checkEmailClients(options.emailClients);
4873
5466
  const { document, diagnostics, components } = template(element, "prepare");
4874
5467
  if (!document || diagnostics.some((d) => d.severity === "error")) return { root: null, diagnostics, layout: LAYOUT_VERSION };
4875
- const prepared = prepareTemplate(document, { components });
5468
+ const prepared = prepareTemplate(document, { components, emailClients: options.emailClients });
4876
5469
  const root = prepared.resolved?.root ?? null;
4877
5470
  const paged = root?.type === "Document" ? pagedSupport(root, "pdf") : root?.type === "Canvas" ? pagedSupport(root, "image") : [];
4878
5471
  return { root, diagnostics: [...diagnostics, ...prepared.diagnostics, ...paged], layout: LAYOUT_VERSION };
4879
5472
  }
4880
5473
 
4881
- export { Button4 as Button, Canvas3 as Canvas, Circle2 as Circle, Column4 as Column, Divider4 as Divider, Document3 as Document, ElementsError, Ellipse2 as Ellipse, Email3 as Email, Grid2 as Grid, Heading4 as Heading, Html3 as Html, Image4 as Image, Layer4 as Layer, Line2 as Line, LineBreak4 as LineBreak, Link4 as Link, List4 as List, ListItem4 as ListItem, Menu2 as Menu, MergeTag4 as MergeTag, Page3 as Page, PageBreak2 as PageBreak, PageFooter2 as PageFooter, PageHeader2 as PageHeader, PageNumber3 as PageNumber, Path2 as Path, Polygon2 as Polygon, Rect2 as Rect, Row4 as Row, Section4 as Section, Social2 as Social, Span4 as Span, Svg3 as Svg, Table5 as Table, TableCell4 as TableCell, TableRow4 as TableRow, Text4 as Text, Video4 as Video, component, formatDiagnostics, outline, prepare, prop2 as prop, render, validate };
5474
+ export { BLOCK_MAP_VERSION, Button4 as Button, Canvas3 as Canvas, Circle2 as Circle, Column4 as Column, DEFAULT_EMAIL_CLIENTS, Divider4 as Divider, Document3 as Document, EMAIL_CLIENTS, ElementsError, Ellipse2 as Ellipse, Email3 as Email, Grid2 as Grid, Heading4 as Heading, Html3 as Html, Image4 as Image, Layer4 as Layer, Line2 as Line, LineBreak4 as LineBreak, Link4 as Link, List4 as List, ListItem4 as ListItem, Menu2 as Menu, MergeTag4 as MergeTag, Page3 as Page, PageBreak2 as PageBreak, PageFooter2 as PageFooter, PageHeader2 as PageHeader, PageNumber3 as PageNumber, Path2 as Path, Polygon2 as Polygon, Rect2 as Rect, Row4 as Row, Section4 as Section, Social2 as Social, Span4 as Span, Svg3 as Svg, Table5 as Table, TableCell4 as TableCell, TableRow4 as TableRow, Text4 as Text, Video4 as Video, component, formatDiagnostics, outline, prepare, prop2 as prop, render, validate };
4882
5475
  //# sourceMappingURL=primitives.js.map
4883
5476
  //# sourceMappingURL=primitives.js.map