@samva/markup 0.18.1 → 0.19.0

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.
Files changed (54) hide show
  1. package/AGENTS.md +11 -4
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +89 -5
  4. package/dist/assets-BSpgZOSR.js +61 -0
  5. package/dist/assets-k7_v3IOJ.d.ts +39 -0
  6. package/dist/{assets-CS5Xa_mr.d.ts → brand-Bxb6Sm7y.d.ts} +34 -41
  7. package/dist/brand.d.ts +2 -2
  8. package/dist/brand.js +3 -3
  9. package/dist/compatibility.d.ts +15 -5
  10. package/dist/compatibility.js +48 -5
  11. package/dist/compile-C1GWLQ5C.js +490 -0
  12. package/dist/compile-CIeuGmP-.d.ts +98 -0
  13. package/dist/{compiled-template-Bc4Dq9zy.d.ts → compiled-template-CPDHvFlt.d.ts} +157 -6
  14. package/dist/compiler.d.ts +4 -89
  15. package/dist/compiler.js +8 -486
  16. package/dist/components-DNBCz20_.js +3267 -0
  17. package/dist/{css-color-DLM12Osu.js → dark-scheme-BGDCCaUq.js} +12 -1
  18. package/dist/{diagnostic-codes-DZKb0Xcg.js → diagnostic-codes-BCwfUgnF.js} +33 -36
  19. package/dist/diagnostic-model-DJtqbETd.js +36 -0
  20. package/dist/{diagnostics-D4vSLQrO.d.ts → diagnostics-C3TSx6SU.d.ts} +22 -2
  21. package/dist/diagnostics.d.ts +33 -1
  22. package/dist/diagnostics.js +2 -1
  23. package/dist/edit.d.ts +15 -4
  24. package/dist/edit.js +67 -4
  25. package/dist/email/jsx-dev-runtime.d.ts +2 -2
  26. package/dist/email/jsx-runtime.d.ts +1 -1
  27. package/dist/email.d.ts +6 -2
  28. package/dist/email.js +6 -2
  29. package/dist/index.d.ts +1 -1
  30. package/dist/{jsx-runtime-DL8NVF1f.d.ts → jsx-runtime-HHfrVZEf.d.ts} +1 -2
  31. package/dist/markdown.d.ts +69 -0
  32. package/dist/markdown.js +95 -0
  33. package/dist/parse-Bex7QFqj.js +921 -0
  34. package/dist/{preview-rNoJBEIY.d.ts → preview-DRLWgmfg.d.ts} +3 -3
  35. package/dist/profile-DFk4gs7o.js +56 -0
  36. package/dist/{profile-JTvgCD6X.js → project-B6D64SZS.js} +497 -234
  37. package/dist/render.d.ts +8 -8
  38. package/dist/render.js +1 -2
  39. package/dist/{css-B_aVKrDo.js → selectors-CKGuc9MO.js} +158 -4
  40. package/dist/sms.d.ts +1 -1
  41. package/dist/{template-DmNcoH4Q.d.ts → template-4Dse7_OC.d.ts} +1 -1
  42. package/dist/{theme-css-P9F5S7Ic.js → theme-css-BujDuuEA.js} +1 -2
  43. package/dist/whatsapp.d.ts +1 -1
  44. package/generated/contract.json +41 -1
  45. package/generated/reference.md +97 -89
  46. package/package.json +15 -4
  47. package/dist/brand-CIiEqkU7.js +0 -193
  48. package/dist/brand-plugin-Dj9SMpfS.d.ts +0 -33
  49. package/dist/components-C5850-Qg.js +0 -409
  50. package/dist/dark-scheme-CbCwGDWH.js +0 -12
  51. package/dist/html-DJUK2YJF.js +0 -483
  52. package/dist/render-template-DwPDm3Hq.js +0 -1226
  53. package/dist/selectors-CYWCoxZr.js +0 -157
  54. package/dist/source-locations-C8wCsiAy.d.ts +0 -22
@@ -1,16 +1,15 @@
1
1
  import { n as isObject } from "./guards-eV1qLbf2.js";
2
2
  import { n as inputSchema, r as jsonSchema } from "./input-schema-zjkgrXn2.js";
3
3
  import { n as TEMPLATE_NAME_PATTERN, t as TEMPLATE_ID_PATTERN } from "./template-zLUjcwfF.js";
4
- import { a as kebab, g as BRAND_FOOTER_MARKER, n as declarationValue, r as escapeAttribute, s as serializeStyle, t as checkAttribute } from "./html-DJUK2YJF.js";
5
- import { a as EMAIL_ELEMENTS, d as reservedBrandAttributes, f as samvaBrandPlugin, s as emailElement, u as parseBrandSpecifier } from "./render-template-DwPDm3Hq.js";
4
+ import { $ as reservedBrandAttributes, B as brandComponents, F as propFor, G as emailPresentationOverrides, I as rawParts, K as isValueNode, L as PRIMITIVE_CLASSES, M as lowerEmailNodes, N as Tokens, P as emailNodeToIr, Q as parseBrandSpecifier, R as PRIMITIVE_CSS, Y as emailElement, _ as kebab, a as Email, d as equalColumnWidth, et as samvaBrandPlugin, h as escapeAttribute, i as Divider, l as Section, m as declarationValue, n as Column, o as Image, p as checkAttribute, q as EMAIL_ELEMENTS, r as Columns, s as Link, t as Button, u as Spacer, y as serializeStyle } from "./components-DNBCz20_.js";
6
5
  import { FORMATTER_NAMES } from "./fmt.js";
7
6
  import { r as collectEmailDiagnostics, t as EmailNode } from "./jsx-runtime-qL19u3Fy.js";
8
- import { a as Email, d as equalColumnWidth, i as Divider, l as Section, n as Column, o as Image, p as PRIMITIVE_CLASSES, r as Columns, s as Link, t as Button, u as Spacer } from "./components-C5850-Qg.js";
9
- import { l as isAssetPath, n as brandComponents } from "./brand-CIiEqkU7.js";
10
- import { i as buildLineMap, s as lineColumn, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-DZKb0Xcg.js";
11
- import { i as withinDarkScheme, t as DARK_SCHEME_MEDIA } from "./dark-scheme-CbCwGDWH.js";
12
- import { a as lacksGenericFallback, i as fontShorthandFamilies, n as matchesSelector, r as fontFallbackDiagnostic, t as classListOf } from "./selectors-CYWCoxZr.js";
13
- import { parse } from "css-tree";
7
+ import { a as isAssetPath } from "./assets-BSpgZOSR.js";
8
+ import { i as lineColumn, t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
9
+ import { i as withinDarkScheme, t as DARK_SCHEME_MEDIA } from "./dark-scheme-BGDCCaUq.js";
10
+ import { a as fontShorthandFamilies, i as fontFallbackDiagnostic, n as matchesSelector, o as lacksGenericFallback, r as parseStylesheet, t as classListOf } from "./selectors-CKGuc9MO.js";
11
+ import { t as DIAGNOSTIC_CODES } from "./diagnostic-codes-BCwfUgnF.js";
12
+ import { generate, parse } from "css-tree";
14
13
  import { parse as parse$1 } from "@babel/parser";
15
14
  //#region src/email/theme-imports.ts
16
15
  /** The directory part of a project path, with no trailing slash (`""` at the root). */
@@ -145,6 +144,69 @@ var Findings = class {
145
144
  }
146
145
  };
147
146
  //#endregion
147
+ //#region src/sml/issue-content.ts
148
+ /** Copy parser nodes into the layout's source table and resolve its host-owned brand. */
149
+ const issueNodes = (nodes, sourceIndex, brand) => nodes.map((node) => {
150
+ if (isValueNode(node) || !("type" in node)) return node;
151
+ const kids = node.children === void 0 ? {} : { children: issueNodes(node.children, sourceIndex, brand) };
152
+ const src = node.src === void 0 ? {} : { src: [
153
+ node.src[0],
154
+ node.src[1],
155
+ sourceIndex
156
+ ] };
157
+ if (node.type === "component" && (node.name === "BrandLogo" || node.name === "BrandFooter") && brand !== void 0) {
158
+ const { css: _css, ...resolved } = brand;
159
+ return {
160
+ ...node,
161
+ ...kids,
162
+ ...src,
163
+ brand: resolved
164
+ };
165
+ }
166
+ return {
167
+ ...node,
168
+ ...kids,
169
+ ...src
170
+ };
171
+ });
172
+ /** Literal classes on parsed issue nodes join Tailwind's candidate set before the cascade. */
173
+ const issueClasses = (nodes) => nodes.flatMap((node) => {
174
+ if (isValueNode(node) || !("type" in node)) return [];
175
+ const value = node.type === "component" ? node.props?.className : node.attrs?.className ?? node.attrs?.class;
176
+ return [...typeof value === "string" ? value.split(/\s+/).filter(Boolean) : [], ...issueClasses(node.children ?? [])];
177
+ });
178
+ /** Internal marker: authored intrinsic tags cannot use this compiler-owned name. */
179
+ const ISSUE_SLOT_TAG = "sml-issue-content";
180
+ /** Resolve the final expanded layout, where partials have already used or discarded children. */
181
+ const replaceIssueSlots = (nodes, content, report) => {
182
+ let count = 0;
183
+ const walk = (items, conditional) => items.flatMap((node) => {
184
+ if (isValueNode(node)) return [node];
185
+ if ("type" in node && node.type !== "component" && node.tag === "sml-issue-content") {
186
+ count++;
187
+ if (conditional) report(node.src);
188
+ return [...content];
189
+ }
190
+ if ("if" in node) return [{
191
+ ...node,
192
+ then: walk(node.then, true),
193
+ ...node.else === void 0 ? {} : { else: walk(node.else, true) }
194
+ }];
195
+ if ("each" in node) return [{
196
+ ...node,
197
+ children: walk(node.children, true)
198
+ }];
199
+ return [{
200
+ ...node,
201
+ ..."children" in node && node.children !== void 0 ? { children: walk(node.children, conditional) } : {}
202
+ }];
203
+ });
204
+ return {
205
+ nodes: walk(nodes, false),
206
+ count
207
+ };
208
+ };
209
+ //#endregion
148
210
  //#region src/email/stylesheets.ts
149
211
  /** Sheets flattened into one, with rule order preserved across sheets. */
150
212
  const mergeStylesheets = (sheets) => {
@@ -178,6 +240,112 @@ const mergeStylesheets = (sheets) => {
178
240
  };
179
241
  };
180
242
  //#endregion
243
+ //#region src/ir/web-presentation.ts
244
+ /** Parse compiler-owned font and keyframe rules once; send time only serializes checked data. */
245
+ const cssNodes = (node, tokens) => {
246
+ if (node.type === "StyleSheet") return [...node.children].flatMap((child) => cssNodes(child, tokens));
247
+ if (node.type !== "Rule" && node.type !== "Atrule") return [];
248
+ const style = [];
249
+ const children = [];
250
+ for (const child of node.block?.children ?? []) if (child.type === "Declaration") style.push([child.property, tokens?.toValue(`${generate(child.value)}${child.important ? "!important" : ""}`) ?? `${generate(child.value)}${child.important ? "!important" : ""}`]);
251
+ else children.push(...cssNodes(child, tokens));
252
+ return [{
253
+ ...node.type === "Rule" ? { selector: generate(node.prelude) } : { at: `${node.name}${node.prelude === null ? "" : ` ${generate(node.prelude)}`}` },
254
+ ...style.length === 0 ? {} : { style },
255
+ ...children.length === 0 ? {} : { children }
256
+ }];
257
+ };
258
+ /** Retain project selectors, conditions and fonts independently of the email cascade. */
259
+ const webPresentation = (sheets, tailwind, body) => {
260
+ const rules = [];
261
+ const layers = [...new Set(sheets.flatMap((sheet) => sheet.rules.map((rule) => rule.layer)).filter((layer) => layer !== Number.MAX_SAFE_INTEGER))].sort((a, b) => a - b);
262
+ rules.push({ at: `layer sml-web-components${layers.map((layer) => `,sml-web-${layer}`).join("")}` });
263
+ const signature = (rule) => JSON.stringify([
264
+ rule.selector,
265
+ rule.declarations,
266
+ rule.conditions
267
+ ]);
268
+ const defaults = new Set(parseStylesheet(PRIMITIVE_CSS).rules.map(signature));
269
+ const fonts = /* @__PURE__ */ new Set();
270
+ for (const sheet of sheets) {
271
+ if (Object.keys(sheet.variables).length > 0) rules.push({
272
+ selector: ":root",
273
+ style: Object.entries(sheet.variables)
274
+ });
275
+ if (Object.keys(sheet.darkVariables).length > 0) rules.push({
276
+ at: "media (prefers-color-scheme:dark)",
277
+ children: [{
278
+ selector: ":root",
279
+ style: Object.entries(sheet.darkVariables)
280
+ }]
281
+ });
282
+ for (const rule of sheet.rules) {
283
+ if (sheet === tailwind && defaults.has(signature(rule))) continue;
284
+ let out = {
285
+ selector: rule.selector,
286
+ style: rule.declarations.filter(({ property }) => !/^mso-/i.test(property)).map(({ property, value, important }) => [property, `${value}${important ? "!important" : ""}`])
287
+ };
288
+ for (const condition of rule.conditions.toReversed()) out = {
289
+ at: condition.replace(/^@/, ""),
290
+ children: [out]
291
+ };
292
+ if (rule.layer !== Number.MAX_SAFE_INTEGER) out = {
293
+ at: `layer sml-web-${rule.layer}`,
294
+ children: [out]
295
+ };
296
+ rules.push(out);
297
+ }
298
+ for (const face of sheet.fontFaces) if (!fonts.has(face.css)) {
299
+ fonts.add(face.css);
300
+ rules.push(...cssNodes(parse(face.css)));
301
+ }
302
+ for (const css of sheet.headAtRules) rules.push(...cssNodes(parse(css)));
303
+ }
304
+ const componentLayer = tailwind?.rules.find((rule) => rule.selector === ".sml-email")?.layer;
305
+ let inlineSpecificity = sheets.reduce((max, sheet) => sheet.rules.reduce((highest, rule) => Math.max(highest, rule.specificity[0]), max), 0) + 1;
306
+ const stylesheets = [];
307
+ const authored = (node) => {
308
+ if (isValueNode(node)) return;
309
+ if ("type" in node && node.type !== "component" && node.tag === "style" || "el" in node && node.el === "style") {
310
+ const tokens = new Tokens();
311
+ const cssValue = (part) => {
312
+ if (typeof part === "string") return part;
313
+ if (part === null || typeof part === "boolean") return "";
314
+ if (typeof part === "number") return String(part);
315
+ if ("concat" in part) return part.concat.map(cssValue).join("");
316
+ return tokens.value(part);
317
+ };
318
+ const css = (node.children ?? []).map((part) => isValueNode(part) ? cssValue(part) : "unsupported").join("");
319
+ let parsed;
320
+ try {
321
+ parsed = cssNodes(parse(css), tokens);
322
+ } catch {
323
+ parsed = [];
324
+ }
325
+ stylesheets.push(parsed.length === 0 && css.trim() !== "" ? [{ at: "unsupported-dynamic-stylesheet" }] : parsed);
326
+ const selectorIds = (list) => {
327
+ for (const rule of list) {
328
+ inlineSpecificity = Math.max(inlineSpecificity, (rule.selector?.match(/#/g)?.length ?? 0) + 1);
329
+ selectorIds(rule.children ?? []);
330
+ }
331
+ };
332
+ selectorIds(parsed);
333
+ return;
334
+ }
335
+ if ("if" in node) {
336
+ for (const part of node.then) authored(part);
337
+ for (const part of node.else ?? []) authored(part);
338
+ } else if ("children" in node) for (const part of node.children ?? []) authored(part);
339
+ };
340
+ authored(body);
341
+ return {
342
+ rules,
343
+ ...componentLayer === void 0 ? {} : { componentLayer },
344
+ inlineSpecificity,
345
+ ...stylesheets.length === 0 ? {} : { stylesheets }
346
+ };
347
+ };
348
+ //#endregion
181
349
  //#region src/sml/ast.ts
182
350
  const isNode = (value) => isObject(value) && typeof value.type === "string" && typeof value.start === "number" && typeof value.end === "number";
183
351
  /** The child node under `key`, or `undefined`. */
@@ -260,14 +428,6 @@ const bindingValue = (binding) => {
260
428
  default: return;
261
429
  }
262
430
  };
263
- const isStaticScalar = (value) => value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
264
- /** True when a node in child position is a value (rendered as text) rather than structure. */
265
- const isValueNode = (node) => {
266
- if (typeof node !== "object" || node === null) return true;
267
- if ("el" in node || "each" in node || "raw" in node || "preheader" in node) return false;
268
- if ("if" in node && Array.isArray(node.then)) return false;
269
- return true;
270
- };
271
431
  //#endregion
272
432
  //#region src/channel-specs.ts
273
433
  const SMS_CATEGORIES = [
@@ -847,136 +1007,6 @@ const checkStructure = (roots, report) => {
847
1007
  visit(roots);
848
1008
  };
849
1009
  //#endregion
850
- //#region src/sml/tokens.ts
851
- const OPEN = "";
852
- const CLOSE = "";
853
- const TOKEN = /(\d+)/g;
854
- /** Values and node lists a component call reads at render time, addressable from inside a string. */
855
- var Tokens = class {
856
- entries = [];
857
- value(value) {
858
- if (typeof value === "string") return value;
859
- this.entries.push({ value });
860
- return `${OPEN}${this.entries.length - 1}${CLOSE}`;
861
- }
862
- nodes(nodes) {
863
- this.entries.push({ nodes });
864
- return `${OPEN}${this.entries.length - 1}${CLOSE}`;
865
- }
866
- /** A string with tokens in it, as static text and the entries it names. */
867
- parse(input) {
868
- const parts = [];
869
- let last = 0;
870
- for (const match of input.matchAll(TOKEN)) {
871
- if (match.index > last) parts.push(input.slice(last, match.index));
872
- const entry = this.entries[Number(match[1])];
873
- if (entry !== void 0) parts.push(entry);
874
- last = match.index + match[0].length;
875
- }
876
- if (last < input.length) parts.push(input.slice(last));
877
- return parts;
878
- }
879
- /** The value a string with tokens stands for: itself, one entry's value, or their concatenation. */
880
- toValue(input) {
881
- const parts = this.parse(input);
882
- if (parts.length === 0) return "";
883
- const values = [];
884
- for (const part of parts) if (typeof part === "string") values.push(part);
885
- else if ("value" in part) values.push(part.value);
886
- else return void 0;
887
- return values.length === 1 ? values[0] : { concat: values };
888
- }
889
- };
890
- /** A prop value for a primitive: a literal stays itself and anything else becomes a token. */
891
- const propFor = (tokens, value) => isStaticScalar(value) ? value : tokens.value(value);
892
- const SOURCE = (source) => source === void 0 ? {} : { src: source };
893
- /** Split compatibility markup into parts, deciding by position whether a token is in a tag. */
894
- const rawParts = (tokens, html) => {
895
- const parts = [];
896
- let inTag = false;
897
- let quote;
898
- let buffer = "";
899
- const flush = () => {
900
- if (buffer !== "") parts.push(buffer);
901
- buffer = "";
902
- };
903
- for (let index = 0; index < html.length; index++) {
904
- const character = html[index];
905
- if (character === OPEN) {
906
- const end = html.indexOf(CLOSE, index);
907
- const entry = tokens.parse(html.slice(index, end + 1))[0];
908
- if (end > index && entry !== void 0 && typeof entry !== "string" && "value" in entry) {
909
- flush();
910
- parts.push(inTag ? { attr: entry.value } : { text: entry.value });
911
- index = end;
912
- continue;
913
- }
914
- }
915
- if (!inTag && character === "<") inTag = true;
916
- else if (inTag && quote === void 0 && (character === "\"" || character === "'")) quote = character;
917
- else if (inTag && quote === character) quote = void 0;
918
- else if (inTag && quote === void 0 && character === ">") inTag = false;
919
- buffer += character;
920
- }
921
- flush();
922
- return parts;
923
- };
924
- const styleList = (tokens, style) => {
925
- if (!isObject(style)) return void 0;
926
- const entries = [];
927
- for (const [property, value] of Object.entries(style)) {
928
- if (value === void 0 || value === null) continue;
929
- if (typeof value === "number") entries.push([property, value]);
930
- else if (typeof value === "string") {
931
- const resolved = tokens.toValue(value);
932
- if (resolved !== void 0) entries.push([property, resolved]);
933
- }
934
- }
935
- return entries;
936
- };
937
- /** Turn the tree a primitive returned back into IR, restoring each token to what it stood for. */
938
- const emailNodeToIr = (node, tokens, source) => {
939
- if (node === null) return [];
940
- switch (node.type) {
941
- case "Text": {
942
- const output = [];
943
- for (const part of tokens.parse(node.value)) if (typeof part === "string") output.push(part);
944
- else if ("value" in part) output.push(part.value);
945
- else output.push(...part.nodes);
946
- return output;
947
- }
948
- case "Fragment": return node.children.flatMap((childNode) => emailNodeToIr(childNode, tokens, source));
949
- case "Raw": return [{
950
- raw: rawParts(tokens, node.html),
951
- ...node.text === "" ? {} : { text: node.text },
952
- ...SOURCE(source)
953
- }];
954
- case "Element": {
955
- const attrs = {};
956
- let style;
957
- for (const [key, value] of Object.entries(node.props)) {
958
- if (key === "key" || value === void 0 || value === null || value === false) continue;
959
- if (key === "style") {
960
- style = styleList(tokens, value);
961
- continue;
962
- }
963
- const name = key === "className" ? "class" : key;
964
- if (typeof value === "string") attrs[name] = tokens.toValue(value) ?? value;
965
- else if (typeof value === "number" || value === true) attrs[name] = value;
966
- }
967
- const reserved = node.props[BRAND_FOOTER_MARKER] === true;
968
- return [{
969
- el: node.tag,
970
- ...Object.keys(attrs).length === 0 ? {} : { attrs },
971
- ...style === void 0 || Array.isArray(style) && style.length === 0 ? {} : { style },
972
- ...node.children.length === 0 ? {} : { children: node.children.flatMap((childNode) => emailNodeToIr(childNode, tokens, source)) },
973
- ...reserved ? { reserved: true } : {},
974
- ...SOURCE(source)
975
- }];
976
- }
977
- }
978
- };
979
- //#endregion
980
1010
  //#region src/sml/components.ts
981
1011
  const NUMERIC = {
982
1012
  Email: ["gutter"],
@@ -1154,7 +1184,7 @@ const runBuiltinComponent = (call) => {
1154
1184
  kids = [chunked.cells];
1155
1185
  wrap = chunked.wrap;
1156
1186
  } else kids = lowerer.jsxChildren(childCtx, node);
1157
- if (exported === "Columns" && declared.has("gap") && kids.some((kid) => !isValueNode(kid) && !("el" in kid))) {
1187
+ if (exported === "Columns" && declared.has("gap") && kids.some((kid) => !isValueNode(kid) && !("el" in kid) && !("type" in kid))) {
1158
1188
  lowerer.error(ctx, node, "invalid-columns-gap", "<Columns gap> spaces Column children that always render; a conditional or repeated Column would leave a stray gap.", "Pad the cells instead (className=\"pr-6\"), or write each Column unconditionally.");
1159
1189
  return [];
1160
1190
  }
@@ -1183,7 +1213,40 @@ const runBuiltinComponent = (call) => {
1183
1213
  origins: [location]
1184
1214
  });
1185
1215
  if (output !== null && !isNode) return [];
1186
- return wrap([...emailNodeToIr(output, tokens, lowerer.src(ctx.module, node))]);
1216
+ if (ctx.semantic !== true) return wrap([...emailNodeToIr(output, tokens, lowerer.src(ctx.module, node))]);
1217
+ const semanticProp = (bound) => {
1218
+ if (bound.k === "object") {
1219
+ const object = {};
1220
+ for (const [name, field] of bound.fields) {
1221
+ const value = semanticProp(field);
1222
+ if (value !== void 0) object[name] = value;
1223
+ }
1224
+ return { object };
1225
+ }
1226
+ return bindingValue(bound);
1227
+ };
1228
+ const authored = {};
1229
+ for (const [name, bound] of declared) {
1230
+ const value = semanticProp(bound);
1231
+ if (value !== void 0) authored[name] = value;
1232
+ }
1233
+ const brand = isBrand && call.brand !== void 0 ? {
1234
+ slug: call.brand.slug,
1235
+ ...call.brand.assets === void 0 ? {} : { assets: call.brand.assets },
1236
+ ...call.brand.footer === void 0 ? {} : { footer: call.brand.footer }
1237
+ } : void 0;
1238
+ return wrap([{
1239
+ type: "component",
1240
+ name: exported,
1241
+ ...Object.keys(authored).length === 0 ? {} : { props: authored },
1242
+ ...kids.length === 0 ? {} : { children: kids },
1243
+ ...brand === void 0 ? {} : { brand },
1244
+ ...exported === "Column" ? { emailColumn: {
1245
+ ...ctx.columnWidth === void 0 ? {} : { width: ctx.columnWidth },
1246
+ stack: ctx.columnStack === true
1247
+ } } : {},
1248
+ src: lowerer.src(ctx.module, node)
1249
+ }]);
1187
1250
  };
1188
1251
  //#endregion
1189
1252
  //#region src/sml/values.ts
@@ -2202,7 +2265,15 @@ var Lowerer = class {
2202
2265
  }
2203
2266
  const kids = spec.content === "void" ? [] : this.jsxChildren(ctx, node);
2204
2267
  return [{
2205
- el: tag,
2268
+ ...ctx.semantic === true ? {
2269
+ type: [
2270
+ "inline",
2271
+ "link",
2272
+ "image",
2273
+ "break"
2274
+ ].includes(spec.layout) ? "inline" : "block",
2275
+ tag
2276
+ } : { el: tag },
2206
2277
  ...Object.keys(attrs).length === 0 ? {} : { attrs },
2207
2278
  ...style === void 0 ? {} : { style },
2208
2279
  ...kids.length === 0 ? {} : { children: kids },
@@ -2317,6 +2388,23 @@ var Lowerer = class {
2317
2388
  const imported = ctx.module.imports.get(head);
2318
2389
  const local = ctx.module.declarations.get(head);
2319
2390
  const attributes = children(opening, "attributes");
2391
+ if (imported?.kind === "markup" && imported.specifier === "@samva/markup/email" && imported.imported === "IssueContent") {
2392
+ const issue = this.options.issueContent;
2393
+ if (issue === void 0 || ctx.semantic !== true || attributes.length > 0 || children(node, "children").some((item) => item.type !== "JSXText" || (text(item, "value") ?? "").trim() !== "")) {
2394
+ this.error(ctx, node, "issue-slot", "IssueContent needs parsed issue content and takes no props or children; its layout position must be unconditional.", "Place one <IssueContent /> outside loops and conditions in the email layout.");
2395
+ return [];
2396
+ }
2397
+ const needsBrand = (nodes) => nodes.some((item) => typeof item === "object" && item !== null && "type" in item && (item.type === "component" && (item.name === "BrandLogo" || item.name === "BrandFooter") || needsBrand(item.children ?? [])));
2398
+ if (this.options.brand === void 0 && needsBrand(issue.nodes)) {
2399
+ this.error(ctx, node, "brand-unavailable", "This issue uses brand components but no brand was resolved.", "Supply the host-resolved brand when compiling the issue.");
2400
+ return [];
2401
+ }
2402
+ return [{
2403
+ type: "block",
2404
+ tag: ISSUE_SLOT_TAG,
2405
+ src: this.src(ctx.module, node)
2406
+ }];
2407
+ }
2320
2408
  if (imported?.kind === "markup" || imported?.kind === "brand") return runBuiltinComponent({
2321
2409
  lowerer: this,
2322
2410
  ctx,
@@ -2464,7 +2552,8 @@ var Lowerer = class {
2464
2552
  env: /* @__PURE__ */ new Map(),
2465
2553
  stack: [...ctx.stack, id],
2466
2554
  loops: ctx.loops,
2467
- depth: ctx.depth + 1
2555
+ depth: ctx.depth + 1,
2556
+ semantic: ctx.semantic
2468
2557
  };
2469
2558
  const env = /* @__PURE__ */ new Map();
2470
2559
  const param = children(fn, "params")[0];
@@ -3269,15 +3358,25 @@ const physicalPadding = (property, value, context) => {
3269
3358
  };
3270
3359
  /** Generated dark-mode classes share this prefix; it is reserved for the compiler. */
3271
3360
  const DARK_CLASS_PREFIX = "sml-dark-";
3272
- /** A class named for its declarations, so equal sets share one rule and builds are stable. */
3273
- const darkClassName = (body) => {
3361
+ /**
3362
+ * Generated lock classes share this prefix; it is reserved for the compiler.
3363
+ * An important color the dark-mode class restates is inlined without
3364
+ * `!important`, so a weaker retained rule (also marked `!important`) would
3365
+ * otherwise take it. The lock class carries that color on a more specific copy
3366
+ * of the retained selector.
3367
+ */
3368
+ const LOCK_CLASS_PREFIX = "sml-lock-";
3369
+ /** FNV-1a, 32-bit, so equal declaration sets share one class and builds are stable. */
3370
+ const classSuffix = (body) => {
3274
3371
  let hash = 2166136261;
3275
3372
  for (let index = 0; index < body.length; index++) {
3276
3373
  hash ^= body.charCodeAt(index);
3277
3374
  hash = Math.imul(hash, 16777619) >>> 0;
3278
3375
  }
3279
- return `${DARK_CLASS_PREFIX}${hash.toString(36)}`;
3376
+ return hash.toString(36);
3280
3377
  };
3378
+ /** A class named for its declarations, so equal sets share one rule and builds are stable. */
3379
+ const darkClassName = (body) => `${DARK_CLASS_PREFIX}${classSuffix(body)}`;
3281
3380
  const SIDES = [
3282
3381
  "top",
3283
3382
  "right",
@@ -3340,6 +3439,154 @@ const withClass = (props, name) => {
3340
3439
  [key]: typeof current === "string" && current.trim() !== "" ? `${current} ${name}` : name
3341
3440
  };
3342
3441
  };
3442
+ /** Index where the subject compound starts, after the last combinator. */
3443
+ const subjectSplit = (selector) => {
3444
+ let depth = 0;
3445
+ let split = 0;
3446
+ for (let index = 0; index < selector.length; index++) {
3447
+ const character = selector[index];
3448
+ if (character === "\\") {
3449
+ index += 1;
3450
+ continue;
3451
+ }
3452
+ if (character === "(" || character === "[") depth++;
3453
+ else if (character === ")" || character === "]") depth = Math.max(0, depth - 1);
3454
+ else if (depth === 0 && (character === ">" || character === "+" || character === "~")) split = index + 1;
3455
+ else if (depth === 0 && character === " ") split = index + 1;
3456
+ }
3457
+ return selector.slice(split).trim() === "" ? void 0 : split;
3458
+ };
3459
+ /** Single-colon names that still select a pseudo-element, not a pseudo-class. */
3460
+ const LEGACY_PSEUDO_ELEMENTS = /* @__PURE__ */ new Set([
3461
+ "before",
3462
+ "after",
3463
+ "first-line",
3464
+ "first-letter"
3465
+ ]);
3466
+ /**
3467
+ * A pseudo-element is a different box. An important color on the element does
3468
+ * not override a color the pseudo-element sets for itself, so a lock guard must
3469
+ * not be copied onto `::before`.
3470
+ */
3471
+ const targetsPseudoElement = (selector) => {
3472
+ const split = subjectSplit(selector);
3473
+ if (split === void 0) return false;
3474
+ const subject = selector.slice(split);
3475
+ let depth = 0;
3476
+ for (let index = 0; index < subject.length; index++) {
3477
+ const character = subject[index];
3478
+ if (character === "\\") {
3479
+ index += 1;
3480
+ continue;
3481
+ }
3482
+ if (character === "(" || character === "[") depth++;
3483
+ else if (character === ")" || character === "]") depth = Math.max(0, depth - 1);
3484
+ else if (depth !== 0 || character !== ":") continue;
3485
+ if (subject[index + 1] === ":") return true;
3486
+ const name = /^:([A-Za-z-]+)/.exec(subject.slice(index))?.[1]?.toLowerCase();
3487
+ if (name !== void 0 && LEGACY_PSEUDO_ELEMENTS.has(name)) return true;
3488
+ }
3489
+ return false;
3490
+ };
3491
+ /** Insert `className` into the subject compound, before its first pseudo. */
3492
+ const strengthenSelector = (selector, className) => {
3493
+ const split = subjectSplit(selector);
3494
+ if (split === void 0) return void 0;
3495
+ const subject = selector.slice(split);
3496
+ const token = `.${className}`;
3497
+ let depth = 0;
3498
+ let pseudo = -1;
3499
+ for (let index = 0; index < subject.length; index++) {
3500
+ const character = subject[index];
3501
+ if (character === "\\") {
3502
+ index += 1;
3503
+ continue;
3504
+ }
3505
+ if (character === "(" || character === "[") depth++;
3506
+ else if (character === ")" || character === "]") depth = Math.max(0, depth - 1);
3507
+ else if (depth === 0 && character === ":") {
3508
+ pseudo = index;
3509
+ break;
3510
+ }
3511
+ }
3512
+ const strengthened = pseudo < 0 ? `${subject}${token}` : `${subject.slice(0, pseudo)}${token}${subject.slice(pseudo)}`;
3513
+ return `${selector.slice(0, split)}${strengthened}`;
3514
+ };
3515
+ const retainedKey = (rule, declaration, position) => rank(declaration.important, false, rule.layer, rule.specificity, rule.order, position);
3516
+ /** Whether `item` wins the real cascade over this rule's overlapping declaration. */
3517
+ const threatens = (item, rule) => rule.declarations.some((declaration, position) => overlaps(declaration.property, item.property) && compareRank(item.key, retainedKey(rule, declaration, position)) > 0);
3518
+ const nestConditions = (conditions, body) => conditions.reduceRight((inner, condition) => `${condition}{${inner}}`, body);
3519
+ /**
3520
+ * Restate important colors that were inlined without `!important` on a copy of
3521
+ * each weaker retained selector. The copy carries one more class, so it beats
3522
+ * the retained rule, and the dark-scheme copy of it keeps the theme switch.
3523
+ * A rule that outranks the themed color is copied again afterwards, on that
3524
+ * same class, so the guard does not bury it. A shorthand in that copy also
3525
+ * restates every locked declaration that still outranks it, in cascade order,
3526
+ * so the shorthand does not reset a stronger inline longhand. When either
3527
+ * value changes in dark mode, the copy is repeated inside the dark-mode query
3528
+ * at the dark values. Pseudo-elements are left alone: their color is not the
3529
+ * element's.
3530
+ */
3531
+ const renderLockGuards = (locks, rules, variables, darkVariables) => {
3532
+ const seen = /* @__PURE__ */ new Set();
3533
+ const blocks = [];
3534
+ const guarded = /* @__PURE__ */ new Set();
3535
+ const darkQuery = DARK_SCHEME_MEDIA.replace(": ", ":");
3536
+ const emit = (wrapping, selector, body) => {
3537
+ const block = nestConditions(wrapping, `${selector}{${body}}`);
3538
+ if (seen.has(block)) return;
3539
+ seen.add(block);
3540
+ blocks.push(block);
3541
+ };
3542
+ for (const rule of rules) {
3543
+ if (targetsPseudoElement(rule.selector)) continue;
3544
+ const conditions = rule.conditions.map(downlevelMediaQuery);
3545
+ for (const [className, declarations] of locks) {
3546
+ const beaten = declarations.filter((item) => threatens(item, rule));
3547
+ if (beaten.length === 0) continue;
3548
+ const selector = strengthenSelector(rule.selector, className);
3549
+ if (selector === void 0) continue;
3550
+ const inDark = withinDarkScheme(conditions);
3551
+ const lightBody = beaten.map((item) => `${item.property}:${item.light}!important`).join(";");
3552
+ const darkBody = beaten.map((item) => `${item.property}:${item.dark}!important`).join(";");
3553
+ if (!inDark) emit(conditions, selector, lightBody);
3554
+ emit(inDark ? conditions : [...conditions, darkQuery], selector, darkBody);
3555
+ guarded.add(className);
3556
+ }
3557
+ }
3558
+ for (const [className, declarations] of locks) {
3559
+ if (!guarded.has(className)) continue;
3560
+ for (const rule of rules) {
3561
+ if (targetsPseudoElement(rule.selector)) continue;
3562
+ const selector = strengthenSelector(rule.selector, className);
3563
+ if (selector === void 0) continue;
3564
+ const conditions = rule.conditions.map(downlevelMediaQuery);
3565
+ const inDark = withinDarkScheme(conditions);
3566
+ const replay = (mode) => {
3567
+ const scope = mode === "dark" ? {
3568
+ ...variables,
3569
+ ...darkVariables
3570
+ } : variables;
3571
+ const parts = [];
3572
+ for (const [position, declaration] of rule.declarations.entries()) {
3573
+ const key = retainedKey(rule, declaration, position);
3574
+ if (!declarations.some((item) => overlaps(declaration.property, item.property) && compareRank(key, item.key) > 0)) continue;
3575
+ parts.push(`${declaration.property}:${finalizeValue(declaration.value, scope)}!important`);
3576
+ for (const item of declarations) if (overlaps(declaration.property, item.property) && compareRank(item.key, key) > 0) parts.push(`${item.property}:${mode === "dark" ? item.dark : item.light}!important`);
3577
+ }
3578
+ return parts.join(";");
3579
+ };
3580
+ const lightBody = replay(inDark ? "dark" : "light");
3581
+ if (lightBody === "") continue;
3582
+ emit(conditions, selector, lightBody);
3583
+ if (inDark) continue;
3584
+ const darkBody = replay("dark");
3585
+ if (darkBody !== lightBody) emit([...conditions, darkQuery], selector, darkBody);
3586
+ }
3587
+ }
3588
+ return blocks.join("");
3589
+ };
3343
3590
  /** The generated dark-mode classes as one `@media (prefers-color-scheme:dark)` block, written as the head CSS writes its media queries. */
3344
3591
  const renderDarkRules = (rules) => rules.size === 0 ? "" : `${DARK_SCHEME_MEDIA.replace(": ", ":")}{${[...rules].sort(([left], [right]) => left.localeCompare(right)).map(([name, body]) => `.${name}{${body}}`).join("")}}`;
3345
3592
  /**
@@ -3366,6 +3613,14 @@ const applyStylesheet = (root, sheet, options = {}) => {
3366
3613
  (rule.inlinable ? inlinable : retained).push(rule);
3367
3614
  for (const name of rule.classes) styledClasses.add(name);
3368
3615
  }
3616
+ const locks = /* @__PURE__ */ new Map();
3617
+ const lockClassFor = (declarations) => {
3618
+ if (!declarations.some((item) => retained.some((rule) => !targetsPseudoElement(rule.selector) && threatens(item, rule)))) return void 0;
3619
+ const body = declarations.map((item) => `${item.property}:${item.light}=${item.dark}@${item.key.join(".")}`).join(";");
3620
+ const name = `${LOCK_CLASS_PREFIX}${classSuffix(body)}`;
3621
+ locks.set(name, declarations);
3622
+ return name;
3623
+ };
3369
3624
  const conditionalWritingRules = retained.filter((rule) => rule.declarations.some((declaration) => declaration.property === "direction" || declaration.property === "writing-mode" || declaration.property === "all"));
3370
3625
  const unmatched = /* @__PURE__ */ new Map();
3371
3626
  const declaredFamilies = new Set(sheet.fontFaces.map((face) => face.family.toLowerCase()));
@@ -3455,12 +3710,32 @@ const applyStylesheet = (root, sheet, options = {}) => {
3455
3710
  direction: inheritedValue("direction", inheritedWriting.direction, "ltr", typeof element.props.dir === "string" ? element.props.dir : element.tag === "bdi" ? "auto" : inheritedWriting.direction),
3456
3711
  writingMode: winners.has("all") ? "unknown" : inheritedValue("writing-mode", inheritedWriting.writingMode, "horizontal-tb")
3457
3712
  };
3458
- const emitted = [...winners].filter(([property]) => !property.startsWith("--")).sort(([, left], [, right]) => compareRank(left.key, right.key)).map(([property, entry]) => [property, finalizeValue(entry.value, variables)]);
3713
+ const restated = /* @__PURE__ */ new Set();
3714
+ if (hasDarkTheme) {
3715
+ for (const [property, entry] of winners) {
3716
+ if (property.startsWith("--") || !entry.value.includes("var(")) continue;
3717
+ if (finalizeValue(entry.value, darkScope) !== finalizeValue(entry.value, variables)) restated.add(property);
3718
+ }
3719
+ for (let grew = restated.size > 0; grew;) {
3720
+ grew = false;
3721
+ for (const property of winners.keys()) if (!property.startsWith("--") && !restated.has(property) && [...restated].some((candidate) => overlaps(candidate, property))) {
3722
+ restated.add(property);
3723
+ grew = true;
3724
+ }
3725
+ }
3726
+ }
3727
+ const emitted = [...winners].filter(([property]) => !property.startsWith("--")).sort(([, left], [, right]) => compareRank(left.key, right.key)).map(([property, entry]) => {
3728
+ return [
3729
+ property,
3730
+ finalizeValue(entry.value, variables),
3731
+ entry.key[0] === 1 && !restated.has(property)
3732
+ ];
3733
+ });
3459
3734
  for (const [property, value] of emitted) checkFallback(property, value, element.origins.slice(0, 1));
3460
3735
  const physical = /* @__PURE__ */ new Map();
3461
- for (const [property, value] of emitted) for (const [name, lowered] of conditionalWriting ? [[property, value]] : physicalPadding(property, value, writing)) {
3736
+ for (const [property, value, important] of emitted) for (const [name, lowered] of conditionalWriting ? [[property, value]] : physicalPadding(property, value, writing)) {
3462
3737
  physical.delete(name);
3463
- physical.set(name, lowered);
3738
+ physical.set(name, important ? `${lowered}!important` : lowered);
3464
3739
  }
3465
3740
  const bgcolor = element.props.bgcolor;
3466
3741
  const lightProps = typeof bgcolor === "string" && bgcolor.includes("var(") ? {
@@ -3468,28 +3743,28 @@ const applyStylesheet = (root, sheet, options = {}) => {
3468
3743
  bgcolor: finalizeValue(bgcolor, variables)
3469
3744
  } : element.props;
3470
3745
  const dark = [];
3746
+ const locked = [];
3471
3747
  if (hasDarkTheme) {
3472
- const changed = [];
3473
- for (const [property, entry] of winners) {
3474
- if (property.startsWith("--") || !entry.value.includes("var(")) continue;
3475
- if (finalizeValue(entry.value, darkScope) !== finalizeValue(entry.value, variables)) changed.push(property);
3476
- }
3477
- const restated = new Set(changed);
3478
- for (let grew = changed.length > 0; grew;) {
3479
- grew = false;
3480
- for (const property of winners.keys()) if (!property.startsWith("--") && !restated.has(property) && [...restated].some((candidate) => overlaps(candidate, property))) {
3481
- restated.add(property);
3482
- grew = true;
3483
- }
3748
+ for (const [property, entry] of [...winners].sort(([, left], [, right]) => compareRank(left.key, right.key))) {
3749
+ if (!restated.has(property)) continue;
3750
+ const darkValue = finalizeValue(entry.value, darkScope);
3751
+ dark.push([property, darkValue]);
3752
+ if (entry.key[0] === 1) locked.push({
3753
+ property,
3754
+ light: finalizeValue(entry.value, variables),
3755
+ dark: darkValue,
3756
+ key: entry.key
3757
+ });
3484
3758
  }
3485
- for (const [property, entry] of [...winners].sort(([, left], [, right]) => compareRank(left.key, right.key))) if (restated.has(property)) dark.push([property, finalizeValue(entry.value, darkScope)]);
3486
3759
  if (typeof bgcolor === "string" && bgcolor.includes("var(") && !winners.has("background-color") && !winners.has("background")) {
3487
3760
  const value = finalizeValue(bgcolor, darkScope);
3488
3761
  if (value !== finalizeValue(bgcolor, variables)) dark.push(["background-color", value]);
3489
3762
  }
3490
3763
  }
3491
3764
  const darkClass = dark.length === 0 ? void 0 : darkClassFor(dark);
3492
- const props = darkClass === void 0 ? lightProps : withClass(lightProps, darkClass);
3765
+ const lockClass = lockClassFor(locked);
3766
+ const themed = darkClass === void 0 ? lightProps : withClass(lightProps, darkClass);
3767
+ const props = lockClass === void 0 ? themed : withClass(themed, lockClass);
3493
3768
  return EmailNode.Element({
3494
3769
  ...element,
3495
3770
  props: emitted.length === 0 ? props : {
@@ -3524,7 +3799,7 @@ const applyStylesheet = (root, sheet, options = {}) => {
3524
3799
  });
3525
3800
  return {
3526
3801
  tree,
3527
- headCss: renderHeadCss(retained, sheet.headAtRules, sheet.variables, sheet.darkVariables),
3802
+ headCss: `${renderHeadCss(retained, sheet.headAtRules, sheet.variables, sheet.darkVariables)}${renderLockGuards(locks, retained, sheet.variables, sheet.darkVariables)}`,
3528
3803
  darkRules,
3529
3804
  fontFaceCss: sheet.fontFaces.map((face) => face.css).join(""),
3530
3805
  diagnostics
@@ -4042,13 +4317,17 @@ const lowerProject = (options, project, findings, inputs) => {
4042
4317
  const localeRead = fields.has("locale") ? readStatic(staticContext, module, fields.get("locale")) : void 0;
4043
4318
  const locale = localeRead?.ok === true && typeof localeRead.value === "string" ? localeRead.value : void 0;
4044
4319
  if (fields.has("locale") && locale === void 0) findings.add(module.parsed, at("locale"), "invalid-locale", "The locale is a string literal.", "Write locale: \"en-US\".");
4045
- const lowerer = new Lowerer({
4320
+ const staticPart = {
4046
4321
  project,
4047
4322
  findings,
4323
+ issueContent: options.issueContent,
4048
4324
  staticContext,
4049
4325
  assetUrl: urlFor,
4050
4326
  brand: options.brand,
4051
- brandPlugin: options.brandPlugin ?? samvaBrandPlugin,
4327
+ brandPlugin: options.brandPlugin ?? samvaBrandPlugin
4328
+ };
4329
+ const lowerer = new Lowerer({
4330
+ ...staticPart,
4052
4331
  channel: channelLowering
4053
4332
  });
4054
4333
  const rootContext = (target) => ({
@@ -4117,17 +4396,47 @@ const lowerProject = (options, project, findings, inputs) => {
4117
4396
  if (!parts.has("subject") && object.type === "ObjectExpression") findings.add(module.parsed, emailField, "missing-subject", "The email channel needs a subject.", "Add `subject: (input) => …`.");
4118
4397
  const preheader = value("preheader");
4119
4398
  let body;
4399
+ let expanded;
4120
4400
  const bodyNode = parts.get("body");
4121
4401
  if (bodyNode === void 0 && object.type === "ObjectExpression") findings.add(module.parsed, emailField, "missing-body", "The email channel needs a body.", "Add `body: (input) => (<Email>…</Email>)`.");
4122
4402
  else if (bodyNode !== void 0) {
4123
4403
  const opened = openFunction(bodyNode);
4124
4404
  if (opened !== void 0) {
4125
- const nodes = lowerer.content(opened.ctx, opened.body);
4126
- if (nodes.length === 1) body = nodes[0];
4127
- else if (!findings.blocking) findings.add(opened.ctx.module.parsed, opened.body, "multiple-roots", nodes.length === 0 ? "The email body renders nothing." : "The email body renders more than one root.", "Wrap the body in a single element such as <Email>.");
4405
+ let nodes = lowerer.content({
4406
+ ...opened.ctx,
4407
+ semantic: true
4408
+ }, opened.body);
4409
+ if (options.issueContent !== void 0) {
4410
+ let index = lowerer.sources.indexOf(options.issueContent.file);
4411
+ if (index < 0) {
4412
+ index = lowerer.sources.length;
4413
+ lowerer.sources.push(options.issueContent.file);
4414
+ }
4415
+ const content = issueNodes(options.issueContent.nodes, index + 1, options.brand);
4416
+ const resolved = replaceIssueSlots(nodes, content, (source) => {
4417
+ const file = source?.[2] === void 0 ? options.entry : lowerer.sources[source[2] - 1] ?? options.entry;
4418
+ findings.report({
4419
+ code: "issue-slot",
4420
+ severity: "error",
4421
+ message: "IssueContent must render unconditionally, outside loops and conditions.",
4422
+ origins: [{
4423
+ fileName: file,
4424
+ lineNumber: source?.[0] ?? 1,
4425
+ columnNumber: source?.[1] ?? 1
4426
+ }]
4427
+ });
4428
+ });
4429
+ nodes = resolved.nodes;
4430
+ if (resolved.count !== 1) findings.add(opened.ctx.module.parsed, opened.body, "issue-slot", "An issue layout must render exactly one IssueContent slot after expanding partials.", "Render one <IssueContent /> outside loops and conditions; do not discard or duplicate its children.");
4431
+ }
4432
+ const roots = lowerEmailNodes(nodes, staticPart.brandPlugin);
4433
+ if (nodes.length === 1 && roots.length === 1) {
4434
+ body = nodes[0];
4435
+ expanded = roots[0];
4436
+ } else if (!findings.blocking) findings.add(opened.ctx.module.parsed, opened.body, "multiple-roots", roots.length === 0 ? "The email body renders nothing." : "The email body renders more than one root.", "Wrap the body in a single element such as <Email>.");
4128
4437
  }
4129
4438
  }
4130
- if (subject !== void 0 && body !== void 0) {
4439
+ if (subject !== void 0 && body !== void 0 && expanded !== void 0) {
4131
4440
  const sheets = [];
4132
4441
  if (inputs.tailwindSheet !== void 0) sheets.push(inputs.tailwindSheet);
4133
4442
  for (const path of stylesheetOrder(project)) {
@@ -4143,7 +4452,7 @@ const lowerProject = (options, project, findings, inputs) => {
4143
4452
  columnNumber: source[1]
4144
4453
  };
4145
4454
  };
4146
- const styled = applyStyles(body, mergeStylesheets(sheets), {
4455
+ const styled = applyStyles(expanded, mergeStylesheets(sheets), {
4147
4456
  locate,
4148
4457
  report: (diagnostic) => {
4149
4458
  findings.items.push(diagnostic);
@@ -4198,7 +4507,14 @@ const lowerProject = (options, project, findings, inputs) => {
4198
4507
  emailIr = {
4199
4508
  subject,
4200
4509
  ...preheader === void 0 ? {} : { preheader },
4201
- body: root
4510
+ body,
4511
+ web: webPresentation(sheets, inputs.tailwindSheet, body),
4512
+ presentation: {
4513
+ nodes: emailPresentationOverrides(expanded, styled.root),
4514
+ headCss: styled.headCss,
4515
+ fontFaceCss: styled.fontFaceCss,
4516
+ brandPlugin: staticPart.brandPlugin
4517
+ }
4202
4518
  };
4203
4519
  }
4204
4520
  }
@@ -4232,7 +4548,7 @@ const lowerProject = (options, project, findings, inputs) => {
4232
4548
  });
4233
4549
  }
4234
4550
  if (schemaJson !== void 0 && templateId !== "" && (emailIr !== void 0 || channelIr.sms !== void 0 || channelIr.whatsapp !== void 0)) template = {
4235
- sml: 1,
4551
+ sml: 2,
4236
4552
  template: templateId,
4237
4553
  ...locale === void 0 ? {} : { locale },
4238
4554
  sources,
@@ -4470,57 +4786,4 @@ const loadProject = (files, entryPath, findings, loadOptions) => {
4470
4786
  };
4471
4787
  };
4472
4788
  //#endregion
4473
- //#region src/sml/profile.ts
4474
- const DEFAULT_ENTRY = "template.tsx";
4475
- /** A stand-in base: a structural check has no build, and an imported asset must read as a project asset. */
4476
- const ASSET_BASE = "https://assets.invalid";
4477
- /** Findings that come from the inputs this check leaves out, not from the source. */
4478
- const MISSING_INPUT_CODES = /* @__PURE__ */ new Set(["brand-unavailable", "unmatched-class"]);
4479
- const asFiles = (files, entry) => typeof files === "string" ? { [entry]: files } : files;
4480
- /**
4481
- * Check an entry against the static profile. Pass the entry's source alone, or the project's files
4482
- * and the entry's path. Imports of files that are absent are trusted to exist, and the brand,
4483
- * Tailwind and theme are not resolved, so the result holds only findings the source itself owns.
4484
- */
4485
- const checkTemplate = (files, options = {}) => {
4486
- const entry = options.entry ?? DEFAULT_ENTRY;
4487
- const brandPlugin = options.brandPlugin ?? samvaBrandPlugin;
4488
- const findings = new Findings();
4489
- const project = loadProject(asFiles(files, entry), entry, findings, {
4490
- brandPlugin,
4491
- lenient: true
4492
- });
4493
- if (project === void 0) return findings.items;
4494
- lowerProject({
4495
- entry,
4496
- brand: void 0,
4497
- brandPlugin,
4498
- assetBase: ASSET_BASE
4499
- }, project, findings, {
4500
- assets: [],
4501
- stylesheets: /* @__PURE__ */ new Map(),
4502
- tailwindSheet: void 0,
4503
- urlFor: (path) => `${ASSET_BASE}/${path}`
4504
- });
4505
- const unresolved = [...project.unresolved];
4506
- return findings.items.filter((item) => !MISSING_INPUT_CODES.has(item.code) && !item.code.startsWith("tailwind-") && !unresolved.some((name) => item.message.includes(`<${name}`) || item.message.includes(`\`${name}\``)));
4507
- };
4508
- const key = (item) => `${item.code}\0${item.message}`;
4509
- /**
4510
- * The errors in `after` that `before` did not already have, compared by code and message so a
4511
- * finding that only moved does not count as new.
4512
- */
4513
- const introducedErrors = (before, after) => {
4514
- const known = /* @__PURE__ */ new Map();
4515
- for (const item of before) if (item.severity === "error") known.set(key(item), (known.get(key(item)) ?? 0) + 1);
4516
- const added = [];
4517
- for (const item of after) {
4518
- if (item.severity !== "error") continue;
4519
- const remaining = known.get(key(item)) ?? 0;
4520
- if (remaining > 0) known.set(key(item), remaining - 1);
4521
- else added.push(item);
4522
- }
4523
- return added;
4524
- };
4525
- //#endregion
4526
- export { lowerProject as a, SMS_CATEGORIES as c, Findings as d, projectDirectory as f, completed as i, WHATSAPP_CATEGORIES as l, resolveThemeImports as m, introducedErrors as n, applyStylesheet as o, resolveFromDirectory as p, loadProject as r, renderDarkRules as s, checkTemplate as t, WHATSAPP_HEADER_TYPES as u };
4789
+ export { renderDarkRules as a, WHATSAPP_HEADER_TYPES as c, projectDirectory as d, resolveFromDirectory as f, applyStylesheet as i, issueClasses as l, completed as n, SMS_CATEGORIES as o, resolveThemeImports as p, lowerProject as r, WHATSAPP_CATEGORIES as s, loadProject as t, Findings as u };