@samva/markup 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/compiler.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { n as isObject } from "./guards-eV1qLbf2.js";
2
2
  import { n as TEMPLATE_NAME_PATTERN } from "./template-zLUjcwfF.js";
3
- import { k as renderIr } from "./components-BZCqegS8.js";
4
- import { c as parseBrandCss, d as formatColor, f as parseColor, i as brandTailwindCss, u as downlevelColors } from "./theme-css-JtRCm1fb.js";
5
- import { a as downlevelMediaQuery, c as renderHeadCss, i as matchesSelector, l as resolveVariables, n as applyStylesheet, o as finalizeValue, r as classListOf, s as foldCalc, u as unquote } from "./ir-CLhlklKV.js";
3
+ import { D as samvaBrandPlugin, T as parseBrandSpecifier, _ as renderIr, w as brandSpecifierPattern } from "./html-DjPxwjLi.js";
4
+ import { c as parseBrandCss, d as formatColor, f as parseColor, i as brandTailwindCss, u as downlevelColors } from "./theme-css-CEUrDpqq.js";
5
+ import { a as downlevelMediaQuery, c as renderHeadCss, i as matchesSelector, l as resolveVariables, n as applyStylesheet, o as finalizeValue, r as classListOf, s as foldCalc, u as unquote } from "./ir-De6pTH51.js";
6
6
  import { i as lineColumn, t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
7
- import { a as lowerProject, d as parseBrandSpecifier, f as projectDirectory, i as completed, l as Findings, m as resolveThemeImports, n as introducedProfileErrors, p as resolveFromDirectory, r as loadProject, t as checkStaticProfile, u as BRAND_SPECIFIER } from "./profile-pWGtWZXF.js";
8
- import { c as assetEntry, d as isAssetPath, i as brandComponents, l as assetUrl, o as withoutUnsubscribeRow, t as BRAND_COMPONENT_CLASSES, u as checkAssetBase } from "./brand-C0MKTwmX.js";
7
+ import { a as lowerProject, d as resolveFromDirectory, f as resolveThemeImports, i as completed, l as Findings, n as introducedProfileErrors, r as loadProject, t as checkStaticProfile, u as projectDirectory } from "./profile-PUfi3GPH.js";
8
+ import { c as checkAssetBase, i as withoutUnsubscribeRow, l as isAssetPath, n as brandComponents, o as assetEntry, s as assetUrl, t as BRAND_COMPONENT_CLASSES } from "./brand-DTctcDmf.js";
9
9
  import { t as EmailCompileError } from "./diagnostics-CnKFefKz.js";
10
- import { n as compileEmail, r as renderEmailContent } from "./render-BQmfh94k.js";
10
+ import { n as compileEmail, r as renderEmailContent } from "./render-D2ht57ZZ.js";
11
11
  import { caniemail, parseHtml, rawData } from "caniemail";
12
12
  import { generate, ident, parse, walk } from "css-tree";
13
13
  import { parse as parse$1 } from "@babel/parser";
@@ -869,7 +869,7 @@ const compileTailwind = async (candidates, options = {}, imports = { stylesheets
869
869
  base,
870
870
  content: builtIn
871
871
  });
872
- if (/^samva:brand(?:\/|$)/.test(id)) {
872
+ if (parseBrandSpecifier(id, imports.brandPlugin) !== void 0) {
873
873
  if (imports.brandCss === void 0) throw new TypeError(`${id} has no brand in this build`);
874
874
  return Promise.resolve({
875
875
  path: id,
@@ -1047,12 +1047,12 @@ const readFile = async (id, source, options, assets) => {
1047
1047
  };
1048
1048
  /** The Tailwind sheet's stylesheet origin. */
1049
1049
  const TAILWIND_ORIGIN = ".samva-tailwind.css";
1050
- /** `samva:brand` / `samva:brand/<slug>` as a module specifier in an import or export. */
1051
- const BRAND_MODULE_SPECIFIER = /(\b(?:from|import)\s*\(?\s*)(["'])(samva:brand(?:\/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)?)\2/g;
1052
- /** The `samva:brand` specifiers a script imports, with where it asks for each. */
1053
- const scriptBrandImports = (path, source) => {
1050
+ /** The brand specifier, bare or with a slug, as a module specifier in an import or export. */
1051
+ const brandModuleSpecifier = (plugin) => new RegExp(`(\\b(?:from|import)\\s*\\(?\\s*)(["'])(${brandSpecifierPattern(plugin)})\\2`, "g");
1052
+ /** The brand specifiers a script imports, with where it asks for each. */
1053
+ const scriptBrandImports = (path, source, plugin) => {
1054
1054
  const lines = buildLineMap(source);
1055
- return [...source.matchAll(BRAND_MODULE_SPECIFIER)].map((match) => {
1055
+ return [...source.matchAll(brandModuleSpecifier(plugin))].map((match) => {
1056
1056
  const at = lineColumn(lines, match.index + match[1].length);
1057
1057
  return {
1058
1058
  specifier: match[3],
@@ -1065,15 +1065,15 @@ const scriptBrandImports = (path, source) => {
1065
1065
  });
1066
1066
  };
1067
1067
  /**
1068
- * The `samva:brand` specifier a project imports — from its theme's `@import`s
1069
- * or its TSX — so a host can fetch that brand before it compiles. `undefined`
1070
- * when the project imports no brand. A project that names two brands gets the
1068
+ * The brand specifier a project imports — from its theme's `@import`s or its
1069
+ * TSX — so a host can fetch that brand before it compiles. `undefined` when
1070
+ * the project imports no brand. A project that names two brands gets the
1071
1071
  * first here and a refusal from the compile.
1072
1072
  */
1073
- const projectBrandSpecifier = (files, themePath = "theme.css") => {
1073
+ const projectBrandSpecifier = (files, themePath = "theme.css", brandPlugin = samvaBrandPlugin) => {
1074
1074
  const theme = files[themePath];
1075
- const fromTheme = typeof theme === "string" ? resolveThemeImports(themePath, theme, files).brandSpecifiers : [];
1076
- const fromScripts = Object.keys(files).sort().filter((path) => SCRIPT.test(path) && typeof files[path] === "string").flatMap((path) => scriptBrandImports(path, files[path])).map((request) => request.specifier);
1075
+ const fromTheme = typeof theme === "string" ? resolveThemeImports(themePath, theme, files, brandPlugin).brandSpecifiers : [];
1076
+ const fromScripts = Object.keys(files).sort().filter((path) => SCRIPT.test(path) && typeof files[path] === "string").flatMap((path) => scriptBrandImports(path, files[path], brandPlugin)).map((request) => request.specifier);
1077
1077
  return [...fromTheme, ...fromScripts][0];
1078
1078
  };
1079
1079
  /**
@@ -1083,11 +1083,11 @@ const projectBrandSpecifier = (files, themePath = "theme.css") => {
1083
1083
  * outside its grammar refuses the whole compile: a theme that silently lost a
1084
1084
  * layer would ship the wrong look.
1085
1085
  */
1086
- const resolveTheme = (files, tailwind, brand) => {
1086
+ const resolveTheme = (files, tailwind, brand, brandPlugin) => {
1087
1087
  const refusals = [];
1088
1088
  const css = tailwind?.css;
1089
1089
  const themePath = tailwind?.cssPath ?? "theme.css";
1090
- const imports = css === void 0 ? void 0 : resolveThemeImports(themePath, css, files);
1090
+ const imports = css === void 0 ? void 0 : resolveThemeImports(themePath, css, files, brandPlugin);
1091
1091
  refusals.push(...imports?.diagnostics ?? []);
1092
1092
  const themeOrigin = {
1093
1093
  fileName: themePath,
@@ -1098,7 +1098,7 @@ const resolveTheme = (files, tailwind, brand) => {
1098
1098
  specifier,
1099
1099
  origin: themeOrigin
1100
1100
  }));
1101
- const scriptRequests = Object.keys(files).sort().filter((path) => SCRIPT.test(path) && typeof files[path] === "string").flatMap((path) => scriptBrandImports(path, files[path]));
1101
+ const scriptRequests = Object.keys(files).sort().filter((path) => SCRIPT.test(path) && typeof files[path] === "string").flatMap((path) => scriptBrandImports(path, files[path], brandPlugin));
1102
1102
  const requests = [...cssRequests, ...scriptRequests];
1103
1103
  const specifiers = [...new Set(requests.map((request) => request.specifier))];
1104
1104
  if (specifiers.length > 1) refusals.push({
@@ -1109,7 +1109,7 @@ const resolveTheme = (files, tailwind, brand) => {
1109
1109
  });
1110
1110
  else if (specifiers.length === 1) {
1111
1111
  const specifier = specifiers[0];
1112
- const named = parseBrandSpecifier(specifier)?.slug;
1112
+ const named = parseBrandSpecifier(specifier, brandPlugin)?.slug;
1113
1113
  const origins = [requests[0].origin];
1114
1114
  if (brand === void 0) refusals.push({
1115
1115
  code: "brand-unavailable",
@@ -1150,6 +1150,7 @@ const resolveTheme = (files, tailwind, brand) => {
1150
1150
  return {
1151
1151
  stylesheets: imports?.stylesheets ?? /* @__PURE__ */ new Map(),
1152
1152
  brandCss,
1153
+ brandPlugin,
1153
1154
  brandModule: scriptRequests.length > 0 ? brand : void 0
1154
1155
  };
1155
1156
  };
@@ -1160,7 +1161,7 @@ const transformProject = async (files, options) => {
1160
1161
  const stylesheets = /* @__PURE__ */ new Map();
1161
1162
  const classes = /* @__PURE__ */ new Set();
1162
1163
  const wanted = options.tailwind === false ? void 0 : options.tailwind ?? {};
1163
- const theme = resolveTheme(files, wanted, options.brand);
1164
+ const theme = resolveTheme(files, wanted, options.brand, options.brandPlugin ?? samvaBrandPlugin);
1164
1165
  if (theme.brandModule !== void 0) for (const name of BRAND_COMPONENT_CLASSES) classes.add(name);
1165
1166
  const paths = Object.keys(files).sort();
1166
1167
  const binaries = new Map(await Promise.all(paths.filter(isAssetPath).map(async (path) => [path, await readFile(path, files[path], options)])));
@@ -1194,12 +1195,12 @@ const transformProject = async (files, options) => {
1194
1195
  //#endregion
1195
1196
  //#region src/email/brand-footer.ts
1196
1197
  /** The brand theme as Tailwind compiles it; a theme outside the grammar is a compile refusal. */
1197
- const brandTheme = (brand) => {
1198
+ const brandTheme = (brand, plugin) => {
1198
1199
  const parsed = parseBrandCss(brand.css);
1199
1200
  if (parsed.isErr()) throw new EmailCompileError({ diagnostics: parsed.error.map((finding) => ({
1200
1201
  code: finding.code,
1201
1202
  severity: "error",
1202
- message: `samva:brand: ${finding.message}`,
1203
+ message: `${plugin.specifier}: ${finding.message}`,
1203
1204
  origins: []
1204
1205
  })) });
1205
1206
  return brandTailwindCss(parsed.value);
@@ -1208,22 +1209,23 @@ const brandTheme = (brand) => {
1208
1209
  * Render `BrandFooter` against a brand on its own, for the send path to
1209
1210
  * append to a message whose template renders no footer of its own. It
1210
1211
  * compiles the same component, styled by the same brand theme, a template
1211
- * would get from `samva:brand`. Without `unsubscribe` the unsubscribe row is
1212
+ * would get from its brand import. Without `unsubscribe` the unsubscribe row is
1212
1213
  * left out.
1213
1214
  */
1214
1215
  const renderBrandFooter = async (brand, options) => {
1215
- const tailwind = await compileTailwind(BRAND_COMPONENT_CLASSES, { css: brandTheme(brand) });
1216
- const node = brandComponents(brand).BrandFooter({});
1216
+ const plugin = options.brandPlugin ?? samvaBrandPlugin;
1217
+ const tailwind = await compileTailwind(BRAND_COMPONENT_CLASSES, { css: brandTheme(brand, plugin) });
1218
+ const node = brandComponents(brand, plugin).BrandFooter({});
1217
1219
  if (node === null) return {
1218
1220
  html: "",
1219
1221
  text: ""
1220
1222
  };
1221
- const styled = applyStylesheet(node, parseStylesheet(tailwind.css, { origin: "samva:brand" }));
1222
- const compiled = compileEmail(styled.tree);
1223
+ const styled = applyStylesheet(node, parseStylesheet(tailwind.css, { origin: plugin.specifier }));
1224
+ const compiled = compileEmail(styled.tree, { brandPlugin: plugin });
1223
1225
  const rendered = options.unsubscribe ? compiled : withoutUnsubscribeRow({
1224
1226
  html: compiled.html,
1225
1227
  text: compiled.text
1226
- });
1228
+ }, plugin);
1227
1229
  const text = (rendered.text ?? "").trim();
1228
1230
  return {
1229
1231
  html: rendered.html ?? "",
@@ -1231,26 +1233,27 @@ const renderBrandFooter = async (brand, options) => {
1231
1233
  };
1232
1234
  };
1233
1235
  /**
1234
- * Render content against a brand as a project whose `theme.css` is
1235
- * `@import "samva:brand";` renders it: Tailwind compiles the brand theme for
1236
+ * Render content against a brand as a project whose `theme.css` imports only
1237
+ * the brand renders it: Tailwind compiles the brand theme for
1236
1238
  * the classes the content names, `BrandLogo` and `BrandFooter` are bound to the
1237
1239
  * brand, and the render inlines the result. A brand preview renders through
1238
1240
  * this, so it shows what a template importing the brand compiles to.
1239
1241
  */
1240
- const renderBrandSample = async (brand, sample) => {
1241
- const tailwind = await compileTailwind([...BRAND_COMPONENT_CLASSES, ...sample.classes], { css: "@import \"samva:brand\";" }, {
1242
+ const renderBrandSample = async (brand, sample, brandPlugin = samvaBrandPlugin) => {
1243
+ const tailwind = await compileTailwind([...BRAND_COMPONENT_CLASSES, ...sample.classes], { css: `@import ${JSON.stringify(brandPlugin.specifier)};` }, {
1242
1244
  stylesheets: /* @__PURE__ */ new Map(),
1243
- brandCss: brandTheme(brand)
1245
+ brandCss: brandTheme(brand, brandPlugin),
1246
+ brandPlugin
1244
1247
  });
1245
- const components = brandComponents(brand);
1246
- return renderEmailContent(parseStylesheet(tailwind.css, { origin: "samva:brand" }), () => sample.render(components));
1248
+ const components = brandComponents(brand, brandPlugin);
1249
+ return renderEmailContent(parseStylesheet(tailwind.css, { origin: brandPlugin.specifier }), () => sample.render(components), { brandPlugin });
1247
1250
  };
1248
1251
  //#endregion
1249
1252
  //#region src/sml/compile.ts
1250
1253
  /** Compile a project's entry template to IR without evaluating any of it. */
1251
1254
  const compileTemplate = async (options) => {
1252
1255
  const findings = new Findings();
1253
- const project = loadProject(options.files, options.entry, findings);
1256
+ const project = loadProject(options.files, options.entry, findings, { brandPlugin: options.brandPlugin ?? samvaBrandPlugin });
1254
1257
  if (project === void 0) return completed(findings, void 0);
1255
1258
  let transformed;
1256
1259
  try {
@@ -1307,6 +1310,6 @@ const checkTemplateCompatibility = (ir, fixtures, options = {}) => {
1307
1310
  return findings;
1308
1311
  };
1309
1312
  //#endregion
1310
- export { BRAND_SPECIFIER, COMPATIBILITY_PROVENANCE, EMAIL_CLIENT_MATRIX, ESSENTIAL_CONSTRUCTS, TAILWIND_VERSION, TEMPLATE_NAME_PATTERN, applyStylesheet, checkEmailCompatibility, checkStaticProfile, checkTemplateCompatibility, classListOf, compileSelector, compileTailwind, compileTemplate, discoverClassCandidates, downlevelColors, downlevelMediaQuery, finalizeValue, foldCalc, formatColor, hasBlockingCompatibility, introducedProfileErrors, matchesSelector, parseBrandSpecifier, parseColor, parseStylesheet, projectBrandSpecifier, renderBrandFooter, renderBrandSample, renderHeadCss, resolveVariables, transformProject, unknownCoverage };
1313
+ export { COMPATIBILITY_PROVENANCE, EMAIL_CLIENT_MATRIX, ESSENTIAL_CONSTRUCTS, TAILWIND_VERSION, TEMPLATE_NAME_PATTERN, applyStylesheet, checkEmailCompatibility, checkStaticProfile, checkTemplateCompatibility, classListOf, compileSelector, compileTailwind, compileTemplate, discoverClassCandidates, downlevelColors, downlevelMediaQuery, finalizeValue, foldCalc, formatColor, hasBlockingCompatibility, introducedProfileErrors, matchesSelector, parseBrandSpecifier, parseColor, parseStylesheet, projectBrandSpecifier, renderBrandFooter, renderBrandSample, renderHeadCss, resolveVariables, transformProject, unknownCoverage };
1311
1314
 
1312
1315
  //# sourceMappingURL=compiler.js.map
@@ -0,0 +1,275 @@
1
+ import { i as escapeText, r as escapeAttribute } from "./html-DjPxwjLi.js";
2
+ import { a as jsx, c as rawMarkup, l as reportEmailDiagnostic, n as Fragment, s as normalizeChildren, t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
3
+ //#region src/email/components.ts
4
+ /** Marks the hidden preview block so the renderer can read it back and the text lane can keep it. */
5
+ const PREHEADER_ATTRIBUTE = "data-samva-preheader";
6
+ const PREHEADER_PADDING_ATTRIBUTE = "data-samva-preheader-padding";
7
+ const PREHEADER_TARGET_LENGTH = 200;
8
+ const PREHEADER_PADDING = "\xA0‌​‍‎‏";
9
+ const presentation = (table, row, rowProps = {}) => jsx("table", {
10
+ role: "presentation",
11
+ border: 0,
12
+ cellpadding: 0,
13
+ cellspacing: 0,
14
+ ...table,
15
+ children: jsx("tbody", { children: jsx("tr", {
16
+ ...rowProps,
17
+ children: row
18
+ }) })
19
+ });
20
+ /** The hidden inbox preview block. Padding characters stop the client borrowing body copy. */
21
+ const preheaderNode = (text) => jsx("div", {
22
+ [PREHEADER_ATTRIBUTE]: text,
23
+ style: {
24
+ display: "none",
25
+ overflow: "hidden",
26
+ lineHeight: "1px",
27
+ opacity: 0,
28
+ maxHeight: 0,
29
+ maxWidth: 0
30
+ },
31
+ children: [text, text.length >= PREHEADER_TARGET_LENGTH ? null : jsx("div", {
32
+ [PREHEADER_PADDING_ATTRIBUTE]: true,
33
+ children: PREHEADER_PADDING.repeat(Math.ceil((PREHEADER_TARGET_LENGTH - text.length) / 7))
34
+ })]
35
+ });
36
+ /** Document shell: the head every target client needs, plus the body content cell. */
37
+ const Email = ({ className, lang = "en", dir = "ltr", title, backgroundColor, style, children }) => jsx("html", {
38
+ lang,
39
+ dir,
40
+ xmlns: "http://www.w3.org/1999/xhtml",
41
+ "xmlns:o": "urn:schemas-microsoft-com:office:office",
42
+ children: [jsx("head", { children: [
43
+ jsx("meta", { charset: "utf-8" }),
44
+ jsx("meta", {
45
+ "http-equiv": "Content-Type",
46
+ content: "text/html; charset=UTF-8"
47
+ }),
48
+ jsx("meta", {
49
+ name: "viewport",
50
+ content: "width=device-width, initial-scale=1"
51
+ }),
52
+ jsx("meta", { name: "x-apple-disable-message-reformatting" }),
53
+ jsx("meta", {
54
+ name: "color-scheme",
55
+ content: "light dark"
56
+ }),
57
+ jsx("meta", {
58
+ name: "supported-color-schemes",
59
+ content: "light dark"
60
+ }),
61
+ title === void 0 ? null : jsx("title", { children: title }),
62
+ rawMarkup("<!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->")
63
+ ] }), jsx("body", {
64
+ style: {
65
+ margin: 0,
66
+ padding: 0,
67
+ ...backgroundColor === void 0 ? {} : { backgroundColor }
68
+ },
69
+ children: [presentation({
70
+ align: "center",
71
+ width: "100%",
72
+ ...backgroundColor === void 0 ? {} : { bgcolor: backgroundColor }
73
+ }, jsx("td", {
74
+ className,
75
+ ...style === void 0 ? {} : { style },
76
+ children
77
+ }))]
78
+ })]
79
+ });
80
+ /** A full-width band of content, as the single-cell table every client agrees on. */
81
+ const Section = ({ className, align = "center", width = "100%", bgcolor, style, tableStyle, children }) => presentation({
82
+ align,
83
+ width,
84
+ ...bgcolor === void 0 ? {} : { bgcolor },
85
+ ...tableStyle === void 0 ? {} : { style: tableStyle }
86
+ }, jsx("td", {
87
+ className,
88
+ ...style === void 0 ? {} : { style },
89
+ children
90
+ }));
91
+ /** The equal share of a row that each of `count` cells takes, whole percent rounded down. */
92
+ const equalColumnWidth = (count) => `${Math.floor(100 / count)}%`;
93
+ /** Give a `Column` that states no width its equal share, keeping the primitive's attribute order. */
94
+ const withColumnWidth = (node, width) => {
95
+ if (node.type === "Fragment") return EmailNode.Fragment({ children: node.children.map((child) => withColumnWidth(child, width)) });
96
+ if (node.type !== "Element" || node.tag !== "td" || node.props.width !== void 0) return node;
97
+ const props = {};
98
+ for (const [key, value] of Object.entries(node.props)) {
99
+ if (key === "style") props.width = width;
100
+ props[key] = value;
101
+ }
102
+ if (props.width === void 0) props.width = width;
103
+ return EmailNode.Element({
104
+ ...node,
105
+ props
106
+ });
107
+ };
108
+ /**
109
+ * A row of side-by-side `Column`s. No flex or grid conversion happens here. With `each` it repeats
110
+ * its cells over a list, `per` to a row, and splits the row equally between cells that state no
111
+ * width.
112
+ */
113
+ const Columns = ({ className, align = "center", width = "100%", style, each, per, children }) => {
114
+ const table = {
115
+ className,
116
+ align,
117
+ width,
118
+ ...style === void 0 ? {} : { style }
119
+ };
120
+ if (each === void 0 || typeof children !== "function") return presentation(table, children, { style: { width: "100%" } });
121
+ if (per !== void 0 && (!Number.isInteger(per) || per < 1)) reportEmailDiagnostic({
122
+ code: "invalid-columns-per",
123
+ severity: "error",
124
+ message: "<Columns per> must be a positive integer."
125
+ });
126
+ const size = per !== void 0 && Number.isInteger(per) && per >= 1 ? per : void 0;
127
+ const chunks = [];
128
+ if (size === void 0) chunks.push(each);
129
+ else for (let at = 0; at < each.length; at += size) chunks.push(each.slice(at, at + size));
130
+ const share = equalColumnWidth(size ?? Math.max(each.length, 1));
131
+ return jsx(Fragment, { children: chunks.map((chunk) => presentation(table, chunk.flatMap((item, index) => normalizeChildren(children(item, index)).map((cell) => withColumnWidth(cell, share))), { style: { width: "100%" } })) });
132
+ };
133
+ /** One cell of a `Columns` row. */
134
+ const Column = ({ className, align, valign, width, style, children }) => jsx("td", {
135
+ className,
136
+ ...align === void 0 ? {} : { align },
137
+ ...valign === void 0 ? {} : { valign },
138
+ ...width === void 0 ? {} : { width },
139
+ ...style === void 0 ? {} : { style },
140
+ children
141
+ });
142
+ const DEFAULT_BUTTON_FONT = "Helvetica, Arial, sans-serif";
143
+ /** Average advance of a bold sans-serif glyph, as a fraction of the font size. */
144
+ const BOLD_GLYPH_ADVANCE = .6;
145
+ const invalidButtonNumber = (name) => reportEmailDiagnostic({
146
+ code: "invalid-button-number",
147
+ severity: "error",
148
+ message: `<Button ${name}> must be a finite number.`
149
+ });
150
+ const buttonNumber = (name, value, fallback) => {
151
+ if (typeof value === "number" && Number.isFinite(value)) return value;
152
+ invalidButtonNumber(name);
153
+ return fallback;
154
+ };
155
+ const optionalButtonNumber = (name, value) => {
156
+ if (value === void 0) return void 0;
157
+ if (typeof value === "number" && Number.isFinite(value)) return value;
158
+ invalidButtonNumber(name);
159
+ };
160
+ const optionalPositiveButtonNumber = (name, value) => {
161
+ if (value === void 0) return void 0;
162
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
163
+ reportEmailDiagnostic({
164
+ code: "invalid-button-number",
165
+ severity: "error",
166
+ message: `<Button ${name}> must be a positive finite number.`
167
+ });
168
+ };
169
+ /** A link styled as a button, with a VML fallback so classic Outlook draws the shape. */
170
+ const Button = ({ className, href, width, height, paddingX = 16, backgroundColor = "#000000", color = "#ffffff", borderRadius = 0, borderColor, borderWidth = 0, fontSize = 16, fontFamily = DEFAULT_BUTTON_FONT, align = "left", style, children }) => {
171
+ const safeWidth = optionalButtonNumber("width", width);
172
+ const safeHeight = optionalPositiveButtonNumber("height", height);
173
+ const safePaddingX = buttonNumber("paddingX", paddingX, 16);
174
+ const safeBorderRadius = buttonNumber("borderRadius", borderRadius, 0);
175
+ const safeBorderWidth = buttonNumber("borderWidth", borderWidth, 0);
176
+ const safeFontSize = buttonNumber("fontSize", fontSize, 16);
177
+ const label = typeof children === "string" ? children : void 0;
178
+ const fallback = safeHeight !== void 0 && label !== void 0;
179
+ if (!fallback) reportEmailDiagnostic({
180
+ code: "button-outlook-fallback",
181
+ severity: "warning",
182
+ message: "<Button> draws its classic Outlook shape from VML, which needs a height and a plain string label. Without both, classic Windows Outlook shows an unstyled link.",
183
+ clients: ["outlook-windows"]
184
+ });
185
+ const arcsize = safeHeight === void 0 ? 0 : Math.round(safeBorderRadius / safeHeight * 100);
186
+ const fallbackWidth = safeWidth ?? Math.ceil((label?.length ?? 0) * safeFontSize * BOLD_GLYPH_ADVANCE) + 2 * safePaddingX;
187
+ return presentation({
188
+ width: "100%",
189
+ style: { borderCollapse: "collapse" }
190
+ }, jsx("td", {
191
+ align,
192
+ children: [fallback ? rawMarkup(`<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${escapeAttribute(href)}" style="height:${safeHeight}px;v-text-anchor:middle;width:${fallbackWidth}px;" arcsize="${arcsize}%" ${borderColor === void 0 ? "stroke=\"false\"" : `strokecolor="${escapeAttribute(borderColor)}" strokeweight="${safeBorderWidth}px"`} fillcolor="${escapeAttribute(backgroundColor)}"><w:anchorlock/><center style="color:${escapeAttribute(color)};font-family:${escapeAttribute(fontFamily)};font-size:${safeFontSize}px;font-weight:bold;">${escapeText(label)}</center></v:roundrect><![endif]-->`) : null, jsx("a", {
193
+ className,
194
+ href,
195
+ target: "_blank",
196
+ rel: "noopener noreferrer",
197
+ style: {
198
+ backgroundColor,
199
+ color,
200
+ borderRadius: `${safeBorderRadius}px`,
201
+ display: "inline-block",
202
+ fontFamily,
203
+ fontSize: `${safeFontSize}px`,
204
+ fontWeight: "bold",
205
+ lineHeight: safeHeight === void 0 ? "120%" : `${safeHeight - 2 * safeBorderWidth}px`,
206
+ textAlign: "center",
207
+ textDecoration: "none",
208
+ ...safeWidth === void 0 ? { padding: `0 ${safePaddingX}px` } : {
209
+ width: `${safeWidth}px`,
210
+ maxWidth: "100%"
211
+ },
212
+ ...borderColor === void 0 ? {} : { border: `${safeBorderWidth}px solid ${borderColor}` },
213
+ ...fallback ? { msoHide: "all" } : {},
214
+ ...style
215
+ },
216
+ children
217
+ })]
218
+ }));
219
+ };
220
+ /** Vertical space that survives clients which collapse margins. */
221
+ const Spacer = ({ height }) => presentation({ width: "100%" }, jsx("td", {
222
+ height,
223
+ style: {
224
+ height: `${height}px`,
225
+ lineHeight: `${height}px`,
226
+ fontSize: "1px"
227
+ },
228
+ children: "​"
229
+ }));
230
+ /** A horizontal rule drawn as a cell border, which every target client renders. */
231
+ const Divider = ({ className, color = "#e5e7eb", thickness = 1, style }) => presentation({ width: "100%" }, jsx("td", {
232
+ className,
233
+ style: {
234
+ borderTop: `${thickness}px solid ${color}`,
235
+ fontSize: "1px",
236
+ lineHeight: "1px",
237
+ ...style
238
+ },
239
+ children: "​"
240
+ }));
241
+ /** An image with the width/height attributes Outlook sizes from before CSS applies. */
242
+ const Image = ({ className, src, alt, width, height, align, style }) => jsx("img", {
243
+ className,
244
+ src,
245
+ alt,
246
+ border: 0,
247
+ ...width === void 0 ? {} : { width },
248
+ ...height === void 0 ? {} : { height },
249
+ ...align === void 0 ? {} : { align },
250
+ style: {
251
+ display: "block",
252
+ outline: "none",
253
+ border: "none",
254
+ textDecoration: "none",
255
+ maxWidth: "100%",
256
+ ...style
257
+ }
258
+ });
259
+ /** A text link that opens outside the client and cannot reach back at the opener. */
260
+ const Link = ({ className, href, target = "_blank", rel = "noopener noreferrer", style, children }) => jsx("a", {
261
+ className,
262
+ href,
263
+ target,
264
+ rel,
265
+ style: {
266
+ color: "#067df7",
267
+ textDecoration: "none",
268
+ ...style
269
+ },
270
+ children
271
+ });
272
+ //#endregion
273
+ export { Email as a, PREHEADER_ATTRIBUTE as c, equalColumnWidth as d, preheaderNode as f, Divider as i, Section as l, Column as n, Image as o, Columns as r, Link as s, Button as t, Spacer as u };
274
+
275
+ //# sourceMappingURL=components-Bl6DT21n.js.map
@@ -153,7 +153,7 @@ declare const DIAGNOSTIC_CODES: {
153
153
  };
154
154
  readonly "reserved-attribute": {
155
155
  readonly summary: "An attribute is written only by a brand component.";
156
- readonly fix: "Render BrandFooter from samva:brand instead of marking an element as the footer.";
156
+ readonly fix: "Render BrandFooter from the brand import instead of marking an element as the footer.";
157
157
  };
158
158
  readonly "dynamic-class": {
159
159
  readonly summary: "A class name is built at run time, and Tailwind cannot see it.";
@@ -181,7 +181,7 @@ declare const DIAGNOSTIC_CODES: {
181
181
  };
182
182
  readonly "unknown-component": {
183
183
  readonly summary: "A component the profile cannot read.";
184
- readonly fix: "Import components from @samva/markup/email, samva:brand, or the project.";
184
+ readonly fix: "Import components from @samva/markup/email, the brand import, or the project.";
185
185
  };
186
186
  readonly "dynamic-numeric-prop": {
187
187
  readonly summary: "A prop that sizes layout at compile time is not a number literal.";
@@ -328,7 +328,7 @@ declare const DIAGNOSTIC_CODES: {
328
328
  readonly fix: "Use an https URL, an absolute path, or a relative path.";
329
329
  };
330
330
  readonly "brand-unavailable": {
331
- readonly summary: "The build has no brand to resolve a samva:brand import.";
331
+ readonly summary: "The build has no brand to resolve a brand import.";
332
332
  readonly fix: "Create or select the brand the import names.";
333
333
  };
334
334
  readonly "brand-import-conflict": {
@@ -151,7 +151,7 @@ const DIAGNOSTIC_CODES = codes({
151
151
  },
152
152
  "reserved-attribute": {
153
153
  summary: "An attribute is written only by a brand component.",
154
- fix: "Render BrandFooter from samva:brand instead of marking an element as the footer."
154
+ fix: "Render BrandFooter from the brand import instead of marking an element as the footer."
155
155
  },
156
156
  "dynamic-class": {
157
157
  summary: "A class name is built at run time, and Tailwind cannot see it.",
@@ -179,7 +179,7 @@ const DIAGNOSTIC_CODES = codes({
179
179
  },
180
180
  "unknown-component": {
181
181
  summary: "A component the profile cannot read.",
182
- fix: "Import components from @samva/markup/email, samva:brand, or the project."
182
+ fix: "Import components from @samva/markup/email, the brand import, or the project."
183
183
  },
184
184
  "dynamic-numeric-prop": {
185
185
  summary: "A prop that sizes layout at compile time is not a number literal.",
@@ -326,7 +326,7 @@ const DIAGNOSTIC_CODES = codes({
326
326
  fix: "Use an https URL, an absolute path, or a relative path."
327
327
  },
328
328
  "brand-unavailable": {
329
- summary: "The build has no brand to resolve a samva:brand import.",
329
+ summary: "The build has no brand to resolve a brand import.",
330
330
  fix: "Create or select the brand the import names."
331
331
  },
332
332
  "brand-import-conflict": {
package/dist/edit.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { n as isObject } from "./guards-eV1qLbf2.js";
2
2
  import { d as completeJsxSourceLocation, u as JSX_SOURCE_LOCATION_PROP } from "./jsx-runtime-CBlexoeR.js";
3
3
  import { t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
4
- import { c as WHATSAPP_HEADER_TYPES, n as introducedProfileErrors, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES, t as checkStaticProfile } from "./profile-pWGtWZXF.js";
4
+ import { c as WHATSAPP_HEADER_TYPES, n as introducedProfileErrors, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES, t as checkStaticProfile } from "./profile-PUfi3GPH.js";
5
5
  import { parse } from "@babel/parser";
6
6
  //#region src/source-edit.ts
7
7
  const node = (value) => isObject(value) && "type" in value && typeof value.type === "string" && "start" in value && typeof value.start === "number" && "end" in value && typeof value.end === "number";
package/dist/email.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as Email, c as PREHEADER_ATTRIBUTE, d as equalColumnWidth, f as preheaderNode, i as Divider, l as Section, n as Column, o as Image, r as Columns, s as Link, t as Button, u as Spacer } from "./components-BZCqegS8.js";
1
+ import { a as Email, c as PREHEADER_ATTRIBUTE, d as equalColumnWidth, f as preheaderNode, i as Divider, l as Section, n as Column, o as Image, r as Columns, s as Link, t as Button, u as Spacer } from "./components-Bl6DT21n.js";
2
2
  export { Button, Column, Columns, Divider, Email, Image, Link, PREHEADER_ATTRIBUTE, Section, Spacer, equalColumnWidth, preheaderNode };