@samva/markup 0.13.0 → 0.14.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.
- package/AGENTS.md +11 -1
- package/CHANGELOG.md +50 -0
- package/dist/{assets-DCULM6QD.d.ts → assets-tISHaQNF.d.ts} +1 -1
- package/dist/{brand-C45QBKX9.js → brand-B-t62NJB.js} +4 -4
- package/dist/brand.d.ts +48 -25
- package/dist/brand.js +3 -3
- package/dist/compiler.d.ts +23 -6
- package/dist/compiler.js +657 -199
- package/dist/components-CwWSoG5N.js +401 -0
- package/dist/dark-scheme-CbCwGDWH.js +12 -0
- package/dist/{diagnostic-codes-DdZ4tka5.js → diagnostic-codes-UXwOO3A-.js} +22 -0
- package/dist/diagnostics.d.ts +17 -0
- package/dist/diagnostics.js +1 -1
- package/dist/edit.js +2 -2
- package/dist/email.d.ts +52 -9
- package/dist/email.js +1 -1
- package/dist/{ir-C-ubz6N3.js → ir-D-gTGquU.js} +202 -78
- package/dist/{profile-CXnkfEMi.js → profile-Cc8VxOKA.js} +72 -11
- package/dist/{render-Cja448i0.js → render-6gLei61p.js} +7 -5
- package/dist/{render-C6OTH8BQ.d.ts → render-C1X3S5or.d.ts} +5 -0
- package/dist/render.d.ts +1 -1
- package/dist/render.js +2 -2
- package/dist/{theme-css-CEUrDpqq.js → theme-css-s9QwD5Ko.js} +108 -46
- package/generated/contract.json +176 -2
- package/generated/reference.md +165 -118
- package/package.json +1 -1
- package/skills/sml/SKILL.md +63 -5
- package/dist/components-DycSEKva.js +0 -273
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { i as escapeText, r as escapeAttribute } from "./html-eOZfDwVf.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/primitive-css.ts
|
|
4
|
+
/** The classes the primitives put on the elements they generate. */
|
|
5
|
+
const PRIMITIVE_CLASSES = {
|
|
6
|
+
email: "sml-email",
|
|
7
|
+
button: "sml-button",
|
|
8
|
+
link: "sml-link",
|
|
9
|
+
divider: "sml-divider",
|
|
10
|
+
stack: "sml-stack"
|
|
11
|
+
};
|
|
12
|
+
const PRIMITIVE_CSS = `@layer components {
|
|
13
|
+
.${PRIMITIVE_CLASSES.email} {
|
|
14
|
+
color: var(--color-foreground);
|
|
15
|
+
font-family: var(--font-sans);
|
|
16
|
+
padding-left: 16px;
|
|
17
|
+
padding-right: 16px;
|
|
18
|
+
}
|
|
19
|
+
.${PRIMITIVE_CLASSES.button} {
|
|
20
|
+
background-color: var(--color-primary);
|
|
21
|
+
color: var(--color-primary-foreground);
|
|
22
|
+
font-family: var(--font-sans);
|
|
23
|
+
border-radius: var(--radius);
|
|
24
|
+
}
|
|
25
|
+
.${PRIMITIVE_CLASSES.link} {
|
|
26
|
+
color: var(--color-primary);
|
|
27
|
+
text-decoration: underline;
|
|
28
|
+
}
|
|
29
|
+
.${PRIMITIVE_CLASSES.divider} {
|
|
30
|
+
border-top: 1px solid var(--color-border);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
@media only screen and (max-width: 599px) {
|
|
34
|
+
.${PRIMITIVE_CLASSES.stack} {
|
|
35
|
+
display: block;
|
|
36
|
+
width: 100%;
|
|
37
|
+
max-width: 100%;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/email/components.ts
|
|
43
|
+
/** Marks the hidden preview block so the renderer can read it back and the text lane can keep it. */
|
|
44
|
+
const PREHEADER_ATTRIBUTE = "data-samva-preheader";
|
|
45
|
+
const PREHEADER_PADDING_ATTRIBUTE = "data-samva-preheader-padding";
|
|
46
|
+
const PREHEADER_TARGET_LENGTH = 200;
|
|
47
|
+
const PREHEADER_PADDING = "\xA0";
|
|
48
|
+
const presentation = (table, row, rowProps = {}) => jsx("table", {
|
|
49
|
+
role: "presentation",
|
|
50
|
+
border: 0,
|
|
51
|
+
cellpadding: 0,
|
|
52
|
+
cellspacing: 0,
|
|
53
|
+
...table,
|
|
54
|
+
children: jsx("tbody", { children: jsx("tr", {
|
|
55
|
+
...rowProps,
|
|
56
|
+
children: row
|
|
57
|
+
}) })
|
|
58
|
+
});
|
|
59
|
+
/** A primitive's own class, then the author's. */
|
|
60
|
+
const classNames = (own, authored) => authored === void 0 || authored.trim() === "" ? own : `${own} ${authored}`;
|
|
61
|
+
const optionalNumber = (component, name, value, bounds = {}) => {
|
|
62
|
+
if (value === void 0) return void 0;
|
|
63
|
+
if (typeof value === "number" && Number.isFinite(value) && (bounds.min === void 0 || value >= bounds.min)) return value;
|
|
64
|
+
reportEmailDiagnostic({
|
|
65
|
+
code: "invalid-number-prop",
|
|
66
|
+
severity: "error",
|
|
67
|
+
message: `<${component} ${name}> must be a finite number${bounds.min === void 0 ? "" : ` of at least ${bounds.min}`}.`
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
/** The hidden inbox preview block. Padding characters stop the client borrowing body copy. */
|
|
71
|
+
const preheaderNode = (text) => jsx("div", {
|
|
72
|
+
[PREHEADER_ATTRIBUTE]: text,
|
|
73
|
+
style: {
|
|
74
|
+
display: "none",
|
|
75
|
+
overflow: "hidden",
|
|
76
|
+
lineHeight: "1px",
|
|
77
|
+
opacity: 0,
|
|
78
|
+
maxHeight: 0,
|
|
79
|
+
maxWidth: 0
|
|
80
|
+
},
|
|
81
|
+
children: [text, text.length >= PREHEADER_TARGET_LENGTH ? null : jsx("div", {
|
|
82
|
+
[PREHEADER_PADDING_ATTRIBUTE]: true,
|
|
83
|
+
children: PREHEADER_PADDING.repeat(Math.ceil((PREHEADER_TARGET_LENGTH - text.length) / 7))
|
|
84
|
+
})]
|
|
85
|
+
});
|
|
86
|
+
/**
|
|
87
|
+
* Document shell: the head every target client needs, a body painted with the page color, and
|
|
88
|
+
* the content cell. The content cell sets the theme's text color and body font
|
|
89
|
+
* (`--color-foreground`, `--font-sans`) for everything inside it, and pads the content off the
|
|
90
|
+
* screen edge (`gutter`).
|
|
91
|
+
*/
|
|
92
|
+
const Email = ({ className, lang = "en", dir = "ltr", title, backgroundColor = "var(--color-background)", gutter, style, children }) => {
|
|
93
|
+
const safeGutter = optionalNumber("Email", "gutter", gutter, { min: 0 });
|
|
94
|
+
const cellStyle = {
|
|
95
|
+
...safeGutter === void 0 ? {} : {
|
|
96
|
+
paddingLeft: `${safeGutter}px`,
|
|
97
|
+
paddingRight: `${safeGutter}px`
|
|
98
|
+
},
|
|
99
|
+
...style
|
|
100
|
+
};
|
|
101
|
+
return jsx("html", {
|
|
102
|
+
lang,
|
|
103
|
+
dir,
|
|
104
|
+
xmlns: "http://www.w3.org/1999/xhtml",
|
|
105
|
+
"xmlns:o": "urn:schemas-microsoft-com:office:office",
|
|
106
|
+
children: [jsx("head", { children: [
|
|
107
|
+
jsx("meta", { charset: "utf-8" }),
|
|
108
|
+
jsx("meta", {
|
|
109
|
+
"http-equiv": "Content-Type",
|
|
110
|
+
content: "text/html; charset=UTF-8"
|
|
111
|
+
}),
|
|
112
|
+
jsx("meta", {
|
|
113
|
+
name: "viewport",
|
|
114
|
+
content: "width=device-width, initial-scale=1"
|
|
115
|
+
}),
|
|
116
|
+
jsx("meta", { name: "x-apple-disable-message-reformatting" }),
|
|
117
|
+
jsx("meta", {
|
|
118
|
+
name: "color-scheme",
|
|
119
|
+
content: "light dark"
|
|
120
|
+
}),
|
|
121
|
+
jsx("meta", {
|
|
122
|
+
name: "supported-color-schemes",
|
|
123
|
+
content: "light dark"
|
|
124
|
+
}),
|
|
125
|
+
title === void 0 ? null : jsx("title", { children: title }),
|
|
126
|
+
rawMarkup("<!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->")
|
|
127
|
+
] }), jsx("body", {
|
|
128
|
+
style: {
|
|
129
|
+
margin: 0,
|
|
130
|
+
padding: 0,
|
|
131
|
+
width: "100%",
|
|
132
|
+
backgroundColor
|
|
133
|
+
},
|
|
134
|
+
children: [presentation({
|
|
135
|
+
align: "center",
|
|
136
|
+
width: "100%",
|
|
137
|
+
bgcolor: backgroundColor,
|
|
138
|
+
style: {
|
|
139
|
+
width: "100%",
|
|
140
|
+
backgroundColor
|
|
141
|
+
}
|
|
142
|
+
}, jsx("td", {
|
|
143
|
+
className: classNames(PRIMITIVE_CLASSES.email, className),
|
|
144
|
+
...Object.keys(cellStyle).length === 0 ? {} : { style: cellStyle },
|
|
145
|
+
children
|
|
146
|
+
}))]
|
|
147
|
+
})]
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
/** A pixel width as `width: 100%` capped at it, so a fixed-width table still fits a phone. */
|
|
151
|
+
const fluidWidth = (width) => {
|
|
152
|
+
const pixels = typeof width === "number" ? width : /^\d+(?:\.\d+)?(?:px)?$/.test(width) ? width : void 0;
|
|
153
|
+
if (pixels === void 0) return void 0;
|
|
154
|
+
return {
|
|
155
|
+
width: "100%",
|
|
156
|
+
maxWidth: typeof pixels === "number" ? `${pixels}px` : `${Number.parseFloat(pixels)}px`
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
/** A full-width band of content, as the single-cell table every client agrees on. */
|
|
160
|
+
const Section = ({ className, align = "center", width = "100%", bgcolor, style, tableStyle, children }) => {
|
|
161
|
+
const fluid = fluidWidth(width);
|
|
162
|
+
const table = fluid === void 0 ? tableStyle : {
|
|
163
|
+
...fluid,
|
|
164
|
+
...tableStyle
|
|
165
|
+
};
|
|
166
|
+
return presentation({
|
|
167
|
+
align,
|
|
168
|
+
width,
|
|
169
|
+
...bgcolor === void 0 ? {} : { bgcolor },
|
|
170
|
+
...table === void 0 ? {} : { style: table }
|
|
171
|
+
}, jsx("td", {
|
|
172
|
+
className,
|
|
173
|
+
...style === void 0 ? {} : { style },
|
|
174
|
+
children
|
|
175
|
+
}));
|
|
176
|
+
};
|
|
177
|
+
/** The equal share of a row that each of `count` cells takes, whole percent rounded down. */
|
|
178
|
+
const equalColumnWidth = (count) => `${Math.floor(100 / count)}%`;
|
|
179
|
+
/**
|
|
180
|
+
* Give a `Column` that states no width its equal share, keeping the primitive's attribute order,
|
|
181
|
+
* and the stacking class when its row stacks.
|
|
182
|
+
*/
|
|
183
|
+
const withColumnLayout = (node, width, stack) => {
|
|
184
|
+
if (node.type === "Fragment") return EmailNode.Fragment({ children: node.children.map((child) => withColumnLayout(child, width, stack)) });
|
|
185
|
+
if (node.type !== "Element" || node.tag !== "td") return node;
|
|
186
|
+
const props = {};
|
|
187
|
+
const sized = width === void 0 || node.props.width !== void 0;
|
|
188
|
+
for (const [key, value] of Object.entries(node.props)) {
|
|
189
|
+
if (key === "style" && !sized) props.width = width;
|
|
190
|
+
props[key] = value;
|
|
191
|
+
}
|
|
192
|
+
if (!sized && props.width === void 0) props.width = width;
|
|
193
|
+
if (stack) props.className = classNames(PRIMITIVE_CLASSES.stack, typeof props.className === "string" ? props.className : void 0);
|
|
194
|
+
return EmailNode.Element({
|
|
195
|
+
...node,
|
|
196
|
+
props
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
/** The cell between two `Column`s: `gap` wide in a row, `gap` tall once the row stacks. */
|
|
200
|
+
const gapCell = (gap, stack) => jsx("td", {
|
|
201
|
+
...stack ? { className: PRIMITIVE_CLASSES.stack } : {},
|
|
202
|
+
width: gap,
|
|
203
|
+
style: {
|
|
204
|
+
width: `${gap}px`,
|
|
205
|
+
...stack ? { height: `${gap}px` } : {},
|
|
206
|
+
fontSize: "1px",
|
|
207
|
+
lineHeight: "1px"
|
|
208
|
+
},
|
|
209
|
+
children: ""
|
|
210
|
+
});
|
|
211
|
+
/** Cells with a gap cell between each two that render something. */
|
|
212
|
+
const withGaps = (cells, gap, stack) => {
|
|
213
|
+
if (gap === void 0 || gap === 0) return cells;
|
|
214
|
+
const output = [];
|
|
215
|
+
for (const cell of cells) {
|
|
216
|
+
if (cell.type === "Text" && cell.value.trim() === "") {
|
|
217
|
+
output.push(cell);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (output.some((placed) => placed.type !== "Text" || placed.value.trim() !== "")) output.push(gapCell(gap, stack));
|
|
221
|
+
output.push(cell);
|
|
222
|
+
}
|
|
223
|
+
return output;
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* A row of side-by-side `Column`s that stacks on a narrow viewport (`stack`). No flex or grid
|
|
227
|
+
* conversion happens here. With `each` it repeats its cells over a list, `per` to a row, and
|
|
228
|
+
* splits the row equally between cells that state no width.
|
|
229
|
+
*/
|
|
230
|
+
const Columns = ({ className, align = "center", width = "100%", style, each, per, stack = true, gap, children }) => {
|
|
231
|
+
const table = {
|
|
232
|
+
className,
|
|
233
|
+
align,
|
|
234
|
+
width,
|
|
235
|
+
...style === void 0 ? {} : { style }
|
|
236
|
+
};
|
|
237
|
+
const safeGap = optionalNumber("Columns", "gap", gap, { min: 0 });
|
|
238
|
+
if (each === void 0 || typeof children !== "function") return presentation(table, withGaps(normalizeChildren(children).map((cell) => withColumnLayout(cell, void 0, stack)), safeGap, stack), { style: { width: "100%" } });
|
|
239
|
+
if (per !== void 0 && (!Number.isInteger(per) || per < 1)) reportEmailDiagnostic({
|
|
240
|
+
code: "invalid-columns-per",
|
|
241
|
+
severity: "error",
|
|
242
|
+
message: "<Columns per> must be a positive integer."
|
|
243
|
+
});
|
|
244
|
+
const size = per !== void 0 && Number.isInteger(per) && per >= 1 ? per : void 0;
|
|
245
|
+
const chunks = [];
|
|
246
|
+
if (size === void 0) chunks.push(each);
|
|
247
|
+
else for (let at = 0; at < each.length; at += size) chunks.push(each.slice(at, at + size));
|
|
248
|
+
const share = equalColumnWidth(size ?? Math.max(each.length, 1));
|
|
249
|
+
return jsx(Fragment, { children: chunks.map((chunk) => presentation(table, withGaps(chunk.flatMap((item, index) => normalizeChildren(children(item, index)).map((cell) => withColumnLayout(cell, share, stack))), safeGap, stack), { style: { width: "100%" } })) });
|
|
250
|
+
};
|
|
251
|
+
/** One cell of a `Columns` row. */
|
|
252
|
+
const Column = ({ className, align, valign, width, style, children }) => jsx("td", {
|
|
253
|
+
className,
|
|
254
|
+
...align === void 0 ? {} : { align },
|
|
255
|
+
...valign === void 0 ? {} : { valign },
|
|
256
|
+
...width === void 0 ? {} : { width },
|
|
257
|
+
...style === void 0 ? {} : { style },
|
|
258
|
+
children
|
|
259
|
+
});
|
|
260
|
+
/** Average advance of a bold sans-serif glyph, as a fraction of the font size. */
|
|
261
|
+
const BOLD_GLYPH_ADVANCE = .6;
|
|
262
|
+
const invalidButtonNumber = (name) => reportEmailDiagnostic({
|
|
263
|
+
code: "invalid-button-number",
|
|
264
|
+
severity: "error",
|
|
265
|
+
message: `<Button ${name}> must be a finite number.`
|
|
266
|
+
});
|
|
267
|
+
const buttonNumber = (name, value, fallback) => {
|
|
268
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
269
|
+
invalidButtonNumber(name);
|
|
270
|
+
return fallback;
|
|
271
|
+
};
|
|
272
|
+
const optionalButtonNumber = (name, value) => {
|
|
273
|
+
if (value === void 0) return void 0;
|
|
274
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
275
|
+
invalidButtonNumber(name);
|
|
276
|
+
};
|
|
277
|
+
const optionalPositiveButtonNumber = (name, value) => {
|
|
278
|
+
if (value === void 0) return void 0;
|
|
279
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
280
|
+
reportEmailDiagnostic({
|
|
281
|
+
code: "invalid-button-number",
|
|
282
|
+
severity: "error",
|
|
283
|
+
message: `<Button ${name}> must be a positive finite number.`
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
287
|
+
* A link styled as a button, with a VML fallback so classic Outlook draws the shape. Its fill,
|
|
288
|
+
* label color, font and corners default to the theme (`bg-primary`, `text-primary-foreground`,
|
|
289
|
+
* `font-sans`, `rounded`), so a class on the button restyles it. Classic Outlook draws the VML
|
|
290
|
+
* shape from the props alone: set `backgroundColor`, `color` and `borderRadius` when the button
|
|
291
|
+
* does not use the theme's primary fill.
|
|
292
|
+
*/
|
|
293
|
+
const Button = ({ className, href, width, height, paddingX = 16, backgroundColor, color, borderRadius, borderColor, borderWidth = 0, fontSize = 16, fontFamily, align = "left", style, children }) => {
|
|
294
|
+
const safeWidth = optionalButtonNumber("width", width);
|
|
295
|
+
const safeHeight = optionalPositiveButtonNumber("height", height);
|
|
296
|
+
const safePaddingX = buttonNumber("paddingX", paddingX, 16);
|
|
297
|
+
const safeBorderRadius = optionalButtonNumber("borderRadius", borderRadius);
|
|
298
|
+
const safeBorderWidth = buttonNumber("borderWidth", borderWidth, 0);
|
|
299
|
+
const safeFontSize = buttonNumber("fontSize", fontSize, 16);
|
|
300
|
+
const label = typeof children === "string" ? children : void 0;
|
|
301
|
+
const fallback = safeHeight !== void 0 && label !== void 0;
|
|
302
|
+
if (!fallback) reportEmailDiagnostic({
|
|
303
|
+
code: "button-outlook-fallback",
|
|
304
|
+
severity: "warning",
|
|
305
|
+
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.",
|
|
306
|
+
clients: ["outlook-windows"]
|
|
307
|
+
});
|
|
308
|
+
const arcsize = safeHeight === void 0 ? 0 : Math.round((safeBorderRadius ?? 0) / safeHeight * 100);
|
|
309
|
+
const fallbackWidth = safeWidth ?? Math.ceil((label?.length ?? 0) * safeFontSize * BOLD_GLYPH_ADVANCE) + 2 * safePaddingX;
|
|
310
|
+
const fill = backgroundColor ?? "var(--color-primary)";
|
|
311
|
+
const ink = color ?? "var(--color-primary-foreground)";
|
|
312
|
+
const family = fontFamily ?? "var(--font-sans)";
|
|
313
|
+
return presentation({
|
|
314
|
+
width: "100%",
|
|
315
|
+
style: { borderCollapse: "collapse" }
|
|
316
|
+
}, jsx("td", {
|
|
317
|
+
align,
|
|
318
|
+
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(fill)}"><w:anchorlock/><center style="color:${escapeAttribute(ink)};font-family:${escapeAttribute(family)};font-size:${safeFontSize}px;font-weight:bold;">${escapeText(label)}</center></v:roundrect><![endif]-->`) : null, jsx("a", {
|
|
319
|
+
className: classNames(PRIMITIVE_CLASSES.button, className),
|
|
320
|
+
href,
|
|
321
|
+
target: "_blank",
|
|
322
|
+
rel: "noopener noreferrer",
|
|
323
|
+
style: {
|
|
324
|
+
...backgroundColor === void 0 ? {} : { backgroundColor },
|
|
325
|
+
...color === void 0 ? {} : { color },
|
|
326
|
+
...safeBorderRadius === void 0 ? {} : { borderRadius: `${safeBorderRadius}px` },
|
|
327
|
+
display: "inline-block",
|
|
328
|
+
...fontFamily === void 0 ? {} : { fontFamily },
|
|
329
|
+
fontSize: `${safeFontSize}px`,
|
|
330
|
+
fontWeight: "bold",
|
|
331
|
+
lineHeight: safeHeight === void 0 ? "120%" : `${safeHeight - 2 * safeBorderWidth}px`,
|
|
332
|
+
textAlign: "center",
|
|
333
|
+
textDecoration: "none",
|
|
334
|
+
...safeWidth === void 0 ? { padding: `0 ${safePaddingX}px` } : {
|
|
335
|
+
width: `${safeWidth}px`,
|
|
336
|
+
maxWidth: "100%"
|
|
337
|
+
},
|
|
338
|
+
...borderColor === void 0 ? {} : { border: `${safeBorderWidth}px solid ${borderColor}` },
|
|
339
|
+
...fallback ? { msoHide: "all" } : {},
|
|
340
|
+
...style
|
|
341
|
+
},
|
|
342
|
+
children
|
|
343
|
+
})]
|
|
344
|
+
}));
|
|
345
|
+
};
|
|
346
|
+
/** Vertical space that survives clients which collapse margins. */
|
|
347
|
+
const Spacer = ({ height }) => presentation({ width: "100%" }, jsx("td", {
|
|
348
|
+
height,
|
|
349
|
+
style: {
|
|
350
|
+
height: `${height}px`,
|
|
351
|
+
lineHeight: `${height}px`,
|
|
352
|
+
fontSize: "1px"
|
|
353
|
+
},
|
|
354
|
+
children: ""
|
|
355
|
+
}));
|
|
356
|
+
/**
|
|
357
|
+
* A horizontal rule drawn as a cell border, which every target client renders. It defaults to a
|
|
358
|
+
* 1px line in the theme's `--color-border`.
|
|
359
|
+
*/
|
|
360
|
+
const Divider = ({ className, color, thickness, style }) => presentation({ width: "100%" }, jsx("td", {
|
|
361
|
+
className: classNames(PRIMITIVE_CLASSES.divider, className),
|
|
362
|
+
style: {
|
|
363
|
+
...color === void 0 && thickness === void 0 ? {} : { borderTop: `${thickness ?? 1}px solid ${color ?? "var(--color-border)"}` },
|
|
364
|
+
fontSize: "1px",
|
|
365
|
+
lineHeight: "1px",
|
|
366
|
+
...style
|
|
367
|
+
},
|
|
368
|
+
children: ""
|
|
369
|
+
}));
|
|
370
|
+
/** An image with the width/height attributes Outlook sizes from before CSS applies. */
|
|
371
|
+
const Image = ({ className, src, alt, width, height, align, style }) => jsx("img", {
|
|
372
|
+
className,
|
|
373
|
+
src,
|
|
374
|
+
alt,
|
|
375
|
+
border: 0,
|
|
376
|
+
...width === void 0 ? {} : { width },
|
|
377
|
+
...height === void 0 ? {} : { height },
|
|
378
|
+
...align === void 0 ? {} : { align },
|
|
379
|
+
style: {
|
|
380
|
+
display: "block",
|
|
381
|
+
outline: "none",
|
|
382
|
+
border: "none",
|
|
383
|
+
textDecoration: "none",
|
|
384
|
+
maxWidth: "100%",
|
|
385
|
+
...style
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
/**
|
|
389
|
+
* A text link that opens outside the client and cannot reach back at the opener. It defaults to
|
|
390
|
+
* the theme's `--color-primary`, underlined.
|
|
391
|
+
*/
|
|
392
|
+
const Link = ({ className, href, target = "_blank", rel = "noopener noreferrer", style, children }) => jsx("a", {
|
|
393
|
+
className: classNames(PRIMITIVE_CLASSES.link, className),
|
|
394
|
+
href,
|
|
395
|
+
target,
|
|
396
|
+
rel,
|
|
397
|
+
...style === void 0 ? {} : { style },
|
|
398
|
+
children
|
|
399
|
+
});
|
|
400
|
+
//#endregion
|
|
401
|
+
export { Email as a, PREHEADER_ATTRIBUTE as c, equalColumnWidth as d, preheaderNode as f, Divider as i, Section as l, PRIMITIVE_CSS as m, Column as n, Image as o, PRIMITIVE_CLASSES as p, Columns as r, Link as s, Button as t, Spacer as u };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/email/dark-scheme.ts
|
|
2
|
+
/** The media query a theme's dark-mode block and every generated dark-mode rule sit in. */
|
|
3
|
+
const DARK_SCHEME_MEDIA = "@media (prefers-color-scheme: dark)";
|
|
4
|
+
const DARK_SCHEME_QUERY = /^(?:only\s+)?(?:screen\s+and\s+)?\(\s*prefers-color-scheme\s*:\s*dark\s*\)$/i;
|
|
5
|
+
/** Whether a `@media` prelude is exactly the dark-mode query. */
|
|
6
|
+
const isDarkSchemeQuery = (prelude) => DARK_SCHEME_QUERY.test(prelude.trim());
|
|
7
|
+
/** Whether a rule's enclosing conditions are exactly the dark-mode query. */
|
|
8
|
+
const isDarkSchemeCondition = (conditions) => conditions.length === 1 && /^@media\s/i.test(conditions[0]) && isDarkSchemeQuery(conditions[0].replace(/^@media\s+/i, ""));
|
|
9
|
+
/** Whether any enclosing condition is the dark-mode query, as in a size query nested inside it. */
|
|
10
|
+
const withinDarkScheme = (conditions) => conditions.some((condition) => isDarkSchemeCondition([condition]));
|
|
11
|
+
//#endregion
|
|
12
|
+
export { withinDarkScheme as i, isDarkSchemeCondition as n, isDarkSchemeQuery as r, DARK_SCHEME_MEDIA as t };
|
|
@@ -230,10 +230,22 @@ const DIAGNOSTIC_CODES = codes({
|
|
|
230
230
|
summary: "<Columns per> is not a positive integer literal.",
|
|
231
231
|
fix: "Write per={2}."
|
|
232
232
|
},
|
|
233
|
+
"invalid-columns-gap": {
|
|
234
|
+
summary: "<Columns gap> is set on a Columns whose cells are repeated with each or rendered conditionally.",
|
|
235
|
+
fix: "Write each Column out unconditionally, or pad the cells instead of using gap."
|
|
236
|
+
},
|
|
237
|
+
"invalid-columns-stack": {
|
|
238
|
+
summary: "<Columns stack> is not a true or false literal.",
|
|
239
|
+
fix: "Write stack={false}, or leave it out to stack on narrow screens."
|
|
240
|
+
},
|
|
233
241
|
"invalid-button-number": {
|
|
234
242
|
summary: "A Button size prop is not a finite number.",
|
|
235
243
|
fix: "Pass a finite number."
|
|
236
244
|
},
|
|
245
|
+
"invalid-number-prop": {
|
|
246
|
+
summary: "A primitive's numeric prop, such as <Email gutter>, is not a finite number in range.",
|
|
247
|
+
fix: "Pass a finite number the message allows, for example gutter={16}."
|
|
248
|
+
},
|
|
237
249
|
"unsupported-element": {
|
|
238
250
|
summary: "An element outside the supported email vocabulary.",
|
|
239
251
|
fix: "Use an element from the contract's element table."
|
|
@@ -334,6 +346,16 @@ const DIAGNOSTIC_CODES = codes({
|
|
|
334
346
|
summary: "A custom property is redefined inside a conditional at-rule.",
|
|
335
347
|
fix: "Keep the override on a class rule."
|
|
336
348
|
},
|
|
349
|
+
"email-contrast": {
|
|
350
|
+
summary: "Text, or a filled button against its surroundings, falls below the contrast ratio in light or dark mode (below 1.5:1 a button is indistinguishable from its surface; WCAG 1.4.11 asks 3:1).",
|
|
351
|
+
fix: "Pair background and text tokens (bg-primary with text-primary-foreground), give the theme's dark-mode @theme a value for the token, or choose colors that reach the required ratio.",
|
|
352
|
+
upgrade: [
|
|
353
|
+
"Move the project's theme onto the theme vocabulary with one dark-mode block. In starter.css and theme.css, rename each color token to the vocabulary name for its role (reference.md, Theme): a page color to --color-background, the main text to --color-foreground, a surface to --color-card with --color-card-foreground, the brand or action color to --color-primary with --color-primary-foreground, subdued text to --color-muted-foreground, a highlight to --color-accent with --color-accent-foreground, rules to --color-border, body and heading fonts to --font-sans and --font-heading, and a corner radius to --radius. Keep every value.",
|
|
354
|
+
"Move each --color-<name>-dark value into @media (prefers-color-scheme: dark) { @theme { --color-<name>: <value>; } } under the renamed token, and delete the -dark tokens. In every template and partial, rename the utilities to match (bg-surface to bg-card, text-muted to text-muted-foreground, and so on) and delete each dark:…-dark class whose token now has a dark value; keep a dark: class only where it picks a different token in dark mode.",
|
|
355
|
+
"Then fix what email-contrast still reports at its location: pair each fill with its -foreground token, or give the dark block a value that reaches the ratio. Do not weaken the design to pass: keep the brand's colors and adjust the paired token.",
|
|
356
|
+
"Compile every entry and render every fixture in light and dark mode; confirm the light render is unchanged apart from fixes. Resolve errors before requesting one saved version for the whole project, approved by the user; do not publish."
|
|
357
|
+
].join("\n\n")
|
|
358
|
+
},
|
|
337
359
|
"email-font-fallback": {
|
|
338
360
|
summary: "A font stack names a project font without a generic fallback.",
|
|
339
361
|
fix: "End the stack with sans-serif, serif or monospace."
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -199,10 +199,22 @@ declare const DIAGNOSTIC_CODES: {
|
|
|
199
199
|
readonly summary: "<Columns per> is not a positive integer literal.";
|
|
200
200
|
readonly fix: "Write per={2}.";
|
|
201
201
|
};
|
|
202
|
+
readonly "invalid-columns-gap": {
|
|
203
|
+
readonly summary: "<Columns gap> is set on a Columns whose cells are repeated with each or rendered conditionally.";
|
|
204
|
+
readonly fix: "Write each Column out unconditionally, or pad the cells instead of using gap.";
|
|
205
|
+
};
|
|
206
|
+
readonly "invalid-columns-stack": {
|
|
207
|
+
readonly summary: "<Columns stack> is not a true or false literal.";
|
|
208
|
+
readonly fix: "Write stack={false}, or leave it out to stack on narrow screens.";
|
|
209
|
+
};
|
|
202
210
|
readonly "invalid-button-number": {
|
|
203
211
|
readonly summary: "A Button size prop is not a finite number.";
|
|
204
212
|
readonly fix: "Pass a finite number.";
|
|
205
213
|
};
|
|
214
|
+
readonly "invalid-number-prop": {
|
|
215
|
+
readonly summary: "A primitive's numeric prop, such as <Email gutter>, is not a finite number in range.";
|
|
216
|
+
readonly fix: "Pass a finite number the message allows, for example gutter={16}.";
|
|
217
|
+
};
|
|
206
218
|
readonly "unsupported-element": {
|
|
207
219
|
readonly summary: "An element outside the supported email vocabulary.";
|
|
208
220
|
readonly fix: "Use an element from the contract's element table.";
|
|
@@ -303,6 +315,11 @@ declare const DIAGNOSTIC_CODES: {
|
|
|
303
315
|
readonly summary: "A custom property is redefined inside a conditional at-rule.";
|
|
304
316
|
readonly fix: "Keep the override on a class rule.";
|
|
305
317
|
};
|
|
318
|
+
readonly "email-contrast": {
|
|
319
|
+
readonly summary: "Text, or a filled button against its surroundings, falls below the contrast ratio in light or dark mode (below 1.5:1 a button is indistinguishable from its surface; WCAG 1.4.11 asks 3:1).";
|
|
320
|
+
readonly fix: "Pair background and text tokens (bg-primary with text-primary-foreground), give the theme's dark-mode @theme a value for the token, or choose colors that reach the required ratio.";
|
|
321
|
+
readonly upgrade: string;
|
|
322
|
+
};
|
|
306
323
|
readonly "email-font-fallback": {
|
|
307
324
|
readonly summary: "A font stack names a project font without a generic fallback.";
|
|
308
325
|
readonly fix: "End the stack with sans-serif, serif or monospace.";
|
package/dist/diagnostics.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as createDiagnosticFormatter, i as buildLineMap, n as DIAGNOSTIC_CODE_FAMILIES, o as formatDiagnostic, r as hasUpgradeRecipe, s as lineColumn, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-
|
|
1
|
+
import { a as createDiagnosticFormatter, i as buildLineMap, n as DIAGNOSTIC_CODE_FAMILIES, o as formatDiagnostic, r as hasUpgradeRecipe, s as lineColumn, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-UXwOO3A-.js";
|
|
2
2
|
import { n as formatEmailDiagnostic, r as hasBlockingDiagnostic, t as EmailCompileError } from "./diagnostics-C0Tr8-eQ.js";
|
|
3
3
|
export { DIAGNOSTIC_CODES, DIAGNOSTIC_CODE_FAMILIES, EmailCompileError, buildLineMap, createDiagnosticFormatter, formatDiagnostic, formatEmailDiagnostic, hasBlockingDiagnostic, hasUpgradeRecipe, lineColumn };
|
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
|
-
import { i as buildLineMap } from "./diagnostic-codes-
|
|
4
|
-
import { c as WHATSAPP_HEADER_TYPES, n as introducedProfileErrors, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES, t as checkStaticProfile } from "./profile-
|
|
3
|
+
import { i as buildLineMap } from "./diagnostic-codes-UXwOO3A-.js";
|
|
4
|
+
import { c as WHATSAPP_HEADER_TYPES, n as introducedProfileErrors, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES, t as checkStaticProfile } from "./profile-Cc8VxOKA.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.d.ts
CHANGED
|
@@ -9,19 +9,38 @@ interface EmailProps {
|
|
|
9
9
|
readonly dir?: "ltr" | "rtl" | undefined;
|
|
10
10
|
/** Document title; several clients use it as the fallback preview line. */
|
|
11
11
|
readonly title?: string | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* The page color, painted across the whole body and, as `bgcolor`, on the wrapper table for
|
|
14
|
+
* clients that drop body styles. Defaults to the theme's `--color-background`, which switches
|
|
15
|
+
* in dark mode.
|
|
16
|
+
*/
|
|
12
17
|
readonly backgroundColor?: string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Pixels of space each side of the content, so it never touches the screen edge on a narrow
|
|
20
|
+
* viewport. Defaults to 16; `0` lets content run edge to edge.
|
|
21
|
+
*/
|
|
22
|
+
readonly gutter?: number | undefined;
|
|
13
23
|
/** Applied to the content cell, where Yahoo and AOL keep body styling. */
|
|
14
24
|
readonly style?: EmailStyle | undefined;
|
|
15
25
|
readonly children?: EmailChild;
|
|
16
26
|
}
|
|
17
27
|
/** The hidden inbox preview block. Padding characters stop the client borrowing body copy. */
|
|
18
28
|
declare const preheaderNode: (text: string) => EmailNode;
|
|
19
|
-
/**
|
|
20
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Document shell: the head every target client needs, a body painted with the page color, and
|
|
31
|
+
* the content cell. The content cell sets the theme's text color and body font
|
|
32
|
+
* (`--color-foreground`, `--font-sans`) for everything inside it, and pads the content off the
|
|
33
|
+
* screen edge (`gutter`).
|
|
34
|
+
*/
|
|
35
|
+
declare const Email: ({ className, lang, dir, title, backgroundColor, gutter, style, children }: EmailProps) => EmailNode;
|
|
21
36
|
interface SectionProps {
|
|
22
37
|
/** CSS classes applied to the primitive's content element. */
|
|
23
38
|
readonly className?: string | undefined;
|
|
24
39
|
readonly align?: "left" | "center" | "right" | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* The band's width. A pixel width (`600`) is a maximum: the band fills a narrower screen, and
|
|
42
|
+
* classic Outlook, which ignores `max-width`, draws it at that width.
|
|
43
|
+
*/
|
|
25
44
|
readonly width?: number | string | undefined;
|
|
26
45
|
readonly bgcolor?: string | undefined;
|
|
27
46
|
/** Applied to the content cell so padding survives Outlook and Klaviyo. */
|
|
@@ -44,17 +63,29 @@ interface ColumnsProps<Item = never> {
|
|
|
44
63
|
readonly each?: readonly Item[] | undefined;
|
|
45
64
|
/** With `each`, the cells in a row; each chunk of items is its own `Columns` table. */
|
|
46
65
|
readonly per?: number | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Stack the cells into one column, each full width, on a viewport narrower than 600px. On by
|
|
68
|
+
* default; `stack={false}` keeps a row of small cells (icons, a price beside a label) side by
|
|
69
|
+
* side. Clients that ignore media queries keep the row.
|
|
70
|
+
*/
|
|
71
|
+
readonly stack?: boolean | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Pixels between the cells of a row. A stacked row puts the same space between the stacked
|
|
74
|
+
* cells instead, so a gap needs no padding on the cells that would misalign them when stacked.
|
|
75
|
+
* The cells must be written out and always render.
|
|
76
|
+
*/
|
|
77
|
+
readonly gap?: number | undefined;
|
|
47
78
|
/** `Column` elements, or with `each` a function that returns one. */
|
|
48
79
|
readonly children?: EmailChild | ((item: Item, index: number) => EmailChild);
|
|
49
80
|
}
|
|
50
81
|
/** The equal share of a row that each of `count` cells takes, whole percent rounded down. */
|
|
51
82
|
declare const equalColumnWidth: (count: number) => string;
|
|
52
83
|
/**
|
|
53
|
-
* A row of side-by-side `Column`s
|
|
54
|
-
* its cells over a list, `per` to a row, and
|
|
55
|
-
* width.
|
|
84
|
+
* A row of side-by-side `Column`s that stacks on a narrow viewport (`stack`). No flex or grid
|
|
85
|
+
* conversion happens here. With `each` it repeats its cells over a list, `per` to a row, and
|
|
86
|
+
* splits the row equally between cells that state no width.
|
|
56
87
|
*/
|
|
57
|
-
declare const Columns: <Item = never>({ className, align, width, style, each, per, children }: ColumnsProps<Item>) => EmailNode;
|
|
88
|
+
declare const Columns: <Item = never>({ className, align, width, style, each, per, stack, gap, children }: ColumnsProps<Item>) => EmailNode;
|
|
58
89
|
interface ColumnProps {
|
|
59
90
|
/** CSS classes applied to the primitive's content element. */
|
|
60
91
|
readonly className?: string | undefined;
|
|
@@ -91,7 +122,13 @@ interface ButtonProps {
|
|
|
91
122
|
/** The label. A string label is required for the classic Outlook fallback. */
|
|
92
123
|
readonly children?: EmailChild;
|
|
93
124
|
}
|
|
94
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* A link styled as a button, with a VML fallback so classic Outlook draws the shape. Its fill,
|
|
127
|
+
* label color, font and corners default to the theme (`bg-primary`, `text-primary-foreground`,
|
|
128
|
+
* `font-sans`, `rounded`), so a class on the button restyles it. Classic Outlook draws the VML
|
|
129
|
+
* shape from the props alone: set `backgroundColor`, `color` and `borderRadius` when the button
|
|
130
|
+
* does not use the theme's primary fill.
|
|
131
|
+
*/
|
|
95
132
|
declare const Button: ({ className, href, width, height, paddingX, backgroundColor, color, borderRadius, borderColor, borderWidth, fontSize, fontFamily, align, style, children }: ButtonProps) => EmailNode;
|
|
96
133
|
interface SpacerProps {
|
|
97
134
|
readonly height: number;
|
|
@@ -105,7 +142,10 @@ interface DividerProps {
|
|
|
105
142
|
readonly thickness?: number | undefined;
|
|
106
143
|
readonly style?: EmailStyle | undefined;
|
|
107
144
|
}
|
|
108
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* A horizontal rule drawn as a cell border, which every target client renders. It defaults to a
|
|
147
|
+
* 1px line in the theme's `--color-border`.
|
|
148
|
+
*/
|
|
109
149
|
declare const Divider: ({ className, color, thickness, style }: DividerProps) => EmailNode;
|
|
110
150
|
interface ImageProps {
|
|
111
151
|
/** CSS classes applied to the primitive's content element. */
|
|
@@ -129,7 +169,10 @@ interface LinkProps {
|
|
|
129
169
|
readonly style?: EmailStyle | undefined;
|
|
130
170
|
readonly children?: EmailChild;
|
|
131
171
|
}
|
|
132
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* A text link that opens outside the client and cannot reach back at the opener. It defaults to
|
|
174
|
+
* the theme's `--color-primary`, underlined.
|
|
175
|
+
*/
|
|
133
176
|
declare const Link: ({ className, href, target, rel, style, children }: LinkProps) => EmailNode;
|
|
134
177
|
//#endregion
|
|
135
178
|
export { Button, ButtonProps, Column, ColumnProps, Columns, ColumnsProps, Divider, DividerProps, Email, EmailProps, Image, ImageProps, Link, LinkProps, PREHEADER_ATTRIBUTE, Section, SectionProps, Spacer, SpacerProps, equalColumnWidth, preheaderNode };
|
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-CwWSoG5N.js";
|
|
2
2
|
export { Button, Column, Columns, Divider, Email, Image, Link, PREHEADER_ATTRIBUTE, Section, Spacer, equalColumnWidth, preheaderNode };
|