@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/AGENTS.md +5 -1
- package/CHANGELOG.md +34 -0
- package/README.md +11 -0
- package/dist/{assets-BIsyMQ92.d.ts → assets-B0yseju_.d.ts} +9 -12
- package/dist/{brand-C0MKTwmX.js → brand-DTctcDmf.js} +19 -25
- package/dist/brand-plugin-CHhcei-j.d.ts +32 -0
- package/dist/brand.d.ts +10 -8
- package/dist/brand.js +4 -4
- package/dist/compiler.d.ts +22 -24
- package/dist/compiler.js +43 -40
- package/dist/components-Bl6DT21n.js +275 -0
- package/dist/diagnostics.d.ts +3 -3
- package/dist/diagnostics.js +3 -3
- package/dist/edit.js +1 -1
- package/dist/email.js +1 -1
- package/dist/{components-BZCqegS8.js → html-DjPxwjLi.js} +89 -316
- package/dist/{ir-CLhlklKV.js → ir-De6pTH51.js} +2 -2
- package/dist/{profile-pWGtWZXF.js → profile-PUfi3GPH.js} +34 -33
- package/dist/{render-BQmfh94k.js → render-D2ht57ZZ.js} +4 -3
- package/dist/{render-BPonDpCl.d.ts → render-DQKz4Aco.d.ts} +10 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.js +3 -3
- package/dist/{theme-css-JtRCm1fb.js → theme-css-CEUrDpqq.js} +2 -2
- package/generated/contract.json +3 -3
- package/generated/reference.md +3 -3
- package/package.json +4 -4
- package/dist/unsubscribe-link-CoesnQDU.js +0 -26
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 {
|
|
4
|
-
import { c as parseBrandCss, d as formatColor, f as parseColor, i as brandTailwindCss, u as downlevelColors } from "./theme-css-
|
|
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-
|
|
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
|
|
8
|
-
import { c as
|
|
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-
|
|
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 (
|
|
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
|
-
/**
|
|
1051
|
-
const
|
|
1052
|
-
/** The
|
|
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(
|
|
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
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
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:
|
|
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
|
|
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
|
|
1216
|
-
const
|
|
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:
|
|
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`
|
|
1235
|
-
*
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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": {
|
package/dist/diagnostics.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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-
|
|
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-
|
|
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 };
|