@samva/markup 0.10.0 → 0.11.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.
@@ -0,0 +1,275 @@
1
+ import { i as escapeText, r as escapeAttribute } from "./html-DjPxwjLi.js";
2
+ import { a as jsx, c as rawMarkup, l as reportEmailDiagnostic, n as Fragment, s as normalizeChildren, t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
3
+ //#region src/email/components.ts
4
+ /** Marks the hidden preview block so the renderer can read it back and the text lane can keep it. */
5
+ const PREHEADER_ATTRIBUTE = "data-samva-preheader";
6
+ const PREHEADER_PADDING_ATTRIBUTE = "data-samva-preheader-padding";
7
+ const PREHEADER_TARGET_LENGTH = 200;
8
+ const PREHEADER_PADDING = "\xA0‌​‍‎‏";
9
+ const presentation = (table, row, rowProps = {}) => jsx("table", {
10
+ role: "presentation",
11
+ border: 0,
12
+ cellpadding: 0,
13
+ cellspacing: 0,
14
+ ...table,
15
+ children: jsx("tbody", { children: jsx("tr", {
16
+ ...rowProps,
17
+ children: row
18
+ }) })
19
+ });
20
+ /** The hidden inbox preview block. Padding characters stop the client borrowing body copy. */
21
+ const preheaderNode = (text) => jsx("div", {
22
+ [PREHEADER_ATTRIBUTE]: text,
23
+ style: {
24
+ display: "none",
25
+ overflow: "hidden",
26
+ lineHeight: "1px",
27
+ opacity: 0,
28
+ maxHeight: 0,
29
+ maxWidth: 0
30
+ },
31
+ children: [text, text.length >= PREHEADER_TARGET_LENGTH ? null : jsx("div", {
32
+ [PREHEADER_PADDING_ATTRIBUTE]: true,
33
+ children: PREHEADER_PADDING.repeat(Math.ceil((PREHEADER_TARGET_LENGTH - text.length) / 7))
34
+ })]
35
+ });
36
+ /** Document shell: the head every target client needs, plus the body content cell. */
37
+ const Email = ({ className, lang = "en", dir = "ltr", title, backgroundColor, style, children }) => jsx("html", {
38
+ lang,
39
+ dir,
40
+ xmlns: "http://www.w3.org/1999/xhtml",
41
+ "xmlns:o": "urn:schemas-microsoft-com:office:office",
42
+ children: [jsx("head", { children: [
43
+ jsx("meta", { charset: "utf-8" }),
44
+ jsx("meta", {
45
+ "http-equiv": "Content-Type",
46
+ content: "text/html; charset=UTF-8"
47
+ }),
48
+ jsx("meta", {
49
+ name: "viewport",
50
+ content: "width=device-width, initial-scale=1"
51
+ }),
52
+ jsx("meta", { name: "x-apple-disable-message-reformatting" }),
53
+ jsx("meta", {
54
+ name: "color-scheme",
55
+ content: "light dark"
56
+ }),
57
+ jsx("meta", {
58
+ name: "supported-color-schemes",
59
+ content: "light dark"
60
+ }),
61
+ title === void 0 ? null : jsx("title", { children: title }),
62
+ rawMarkup("<!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->")
63
+ ] }), jsx("body", {
64
+ style: {
65
+ margin: 0,
66
+ padding: 0,
67
+ ...backgroundColor === void 0 ? {} : { backgroundColor }
68
+ },
69
+ children: [presentation({
70
+ align: "center",
71
+ width: "100%",
72
+ ...backgroundColor === void 0 ? {} : { bgcolor: backgroundColor }
73
+ }, jsx("td", {
74
+ className,
75
+ ...style === void 0 ? {} : { style },
76
+ children
77
+ }))]
78
+ })]
79
+ });
80
+ /** A full-width band of content, as the single-cell table every client agrees on. */
81
+ const Section = ({ className, align = "center", width = "100%", bgcolor, style, tableStyle, children }) => presentation({
82
+ align,
83
+ width,
84
+ ...bgcolor === void 0 ? {} : { bgcolor },
85
+ ...tableStyle === void 0 ? {} : { style: tableStyle }
86
+ }, jsx("td", {
87
+ className,
88
+ ...style === void 0 ? {} : { style },
89
+ children
90
+ }));
91
+ /** The equal share of a row that each of `count` cells takes, whole percent rounded down. */
92
+ const equalColumnWidth = (count) => `${Math.floor(100 / count)}%`;
93
+ /** Give a `Column` that states no width its equal share, keeping the primitive's attribute order. */
94
+ const withColumnWidth = (node, width) => {
95
+ if (node.type === "Fragment") return EmailNode.Fragment({ children: node.children.map((child) => withColumnWidth(child, width)) });
96
+ if (node.type !== "Element" || node.tag !== "td" || node.props.width !== void 0) return node;
97
+ const props = {};
98
+ for (const [key, value] of Object.entries(node.props)) {
99
+ if (key === "style") props.width = width;
100
+ props[key] = value;
101
+ }
102
+ if (props.width === void 0) props.width = width;
103
+ return EmailNode.Element({
104
+ ...node,
105
+ props
106
+ });
107
+ };
108
+ /**
109
+ * A row of side-by-side `Column`s. No flex or grid conversion happens here. With `each` it repeats
110
+ * its cells over a list, `per` to a row, and splits the row equally between cells that state no
111
+ * width.
112
+ */
113
+ const Columns = ({ className, align = "center", width = "100%", style, each, per, children }) => {
114
+ const table = {
115
+ className,
116
+ align,
117
+ width,
118
+ ...style === void 0 ? {} : { style }
119
+ };
120
+ if (each === void 0 || typeof children !== "function") return presentation(table, children, { style: { width: "100%" } });
121
+ if (per !== void 0 && (!Number.isInteger(per) || per < 1)) reportEmailDiagnostic({
122
+ code: "invalid-columns-per",
123
+ severity: "error",
124
+ message: "<Columns per> must be a positive integer."
125
+ });
126
+ const size = per !== void 0 && Number.isInteger(per) && per >= 1 ? per : void 0;
127
+ const chunks = [];
128
+ if (size === void 0) chunks.push(each);
129
+ else for (let at = 0; at < each.length; at += size) chunks.push(each.slice(at, at + size));
130
+ const share = equalColumnWidth(size ?? Math.max(each.length, 1));
131
+ return jsx(Fragment, { children: chunks.map((chunk) => presentation(table, chunk.flatMap((item, index) => normalizeChildren(children(item, index)).map((cell) => withColumnWidth(cell, share))), { style: { width: "100%" } })) });
132
+ };
133
+ /** One cell of a `Columns` row. */
134
+ const Column = ({ className, align, valign, width, style, children }) => jsx("td", {
135
+ className,
136
+ ...align === void 0 ? {} : { align },
137
+ ...valign === void 0 ? {} : { valign },
138
+ ...width === void 0 ? {} : { width },
139
+ ...style === void 0 ? {} : { style },
140
+ children
141
+ });
142
+ const DEFAULT_BUTTON_FONT = "Helvetica, Arial, sans-serif";
143
+ /** Average advance of a bold sans-serif glyph, as a fraction of the font size. */
144
+ const BOLD_GLYPH_ADVANCE = .6;
145
+ const invalidButtonNumber = (name) => reportEmailDiagnostic({
146
+ code: "invalid-button-number",
147
+ severity: "error",
148
+ message: `<Button ${name}> must be a finite number.`
149
+ });
150
+ const buttonNumber = (name, value, fallback) => {
151
+ if (typeof value === "number" && Number.isFinite(value)) return value;
152
+ invalidButtonNumber(name);
153
+ return fallback;
154
+ };
155
+ const optionalButtonNumber = (name, value) => {
156
+ if (value === void 0) return void 0;
157
+ if (typeof value === "number" && Number.isFinite(value)) return value;
158
+ invalidButtonNumber(name);
159
+ };
160
+ const optionalPositiveButtonNumber = (name, value) => {
161
+ if (value === void 0) return void 0;
162
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
163
+ reportEmailDiagnostic({
164
+ code: "invalid-button-number",
165
+ severity: "error",
166
+ message: `<Button ${name}> must be a positive finite number.`
167
+ });
168
+ };
169
+ /** A link styled as a button, with a VML fallback so classic Outlook draws the shape. */
170
+ const Button = ({ className, href, width, height, paddingX = 16, backgroundColor = "#000000", color = "#ffffff", borderRadius = 0, borderColor, borderWidth = 0, fontSize = 16, fontFamily = DEFAULT_BUTTON_FONT, align = "left", style, children }) => {
171
+ const safeWidth = optionalButtonNumber("width", width);
172
+ const safeHeight = optionalPositiveButtonNumber("height", height);
173
+ const safePaddingX = buttonNumber("paddingX", paddingX, 16);
174
+ const safeBorderRadius = buttonNumber("borderRadius", borderRadius, 0);
175
+ const safeBorderWidth = buttonNumber("borderWidth", borderWidth, 0);
176
+ const safeFontSize = buttonNumber("fontSize", fontSize, 16);
177
+ const label = typeof children === "string" ? children : void 0;
178
+ const fallback = safeHeight !== void 0 && label !== void 0;
179
+ if (!fallback) reportEmailDiagnostic({
180
+ code: "button-outlook-fallback",
181
+ severity: "warning",
182
+ message: "<Button> draws its classic Outlook shape from VML, which needs a height and a plain string label. Without both, classic Windows Outlook shows an unstyled link.",
183
+ clients: ["outlook-windows"]
184
+ });
185
+ const arcsize = safeHeight === void 0 ? 0 : Math.round(safeBorderRadius / safeHeight * 100);
186
+ const fallbackWidth = safeWidth ?? Math.ceil((label?.length ?? 0) * safeFontSize * BOLD_GLYPH_ADVANCE) + 2 * safePaddingX;
187
+ return presentation({
188
+ width: "100%",
189
+ style: { borderCollapse: "collapse" }
190
+ }, jsx("td", {
191
+ align,
192
+ children: [fallback ? rawMarkup(`<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${escapeAttribute(href)}" style="height:${safeHeight}px;v-text-anchor:middle;width:${fallbackWidth}px;" arcsize="${arcsize}%" ${borderColor === void 0 ? "stroke=\"false\"" : `strokecolor="${escapeAttribute(borderColor)}" strokeweight="${safeBorderWidth}px"`} fillcolor="${escapeAttribute(backgroundColor)}"><w:anchorlock/><center style="color:${escapeAttribute(color)};font-family:${escapeAttribute(fontFamily)};font-size:${safeFontSize}px;font-weight:bold;">${escapeText(label)}</center></v:roundrect><![endif]-->`) : null, jsx("a", {
193
+ className,
194
+ href,
195
+ target: "_blank",
196
+ rel: "noopener noreferrer",
197
+ style: {
198
+ backgroundColor,
199
+ color,
200
+ borderRadius: `${safeBorderRadius}px`,
201
+ display: "inline-block",
202
+ fontFamily,
203
+ fontSize: `${safeFontSize}px`,
204
+ fontWeight: "bold",
205
+ lineHeight: safeHeight === void 0 ? "120%" : `${safeHeight - 2 * safeBorderWidth}px`,
206
+ textAlign: "center",
207
+ textDecoration: "none",
208
+ ...safeWidth === void 0 ? { padding: `0 ${safePaddingX}px` } : {
209
+ width: `${safeWidth}px`,
210
+ maxWidth: "100%"
211
+ },
212
+ ...borderColor === void 0 ? {} : { border: `${safeBorderWidth}px solid ${borderColor}` },
213
+ ...fallback ? { msoHide: "all" } : {},
214
+ ...style
215
+ },
216
+ children
217
+ })]
218
+ }));
219
+ };
220
+ /** Vertical space that survives clients which collapse margins. */
221
+ const Spacer = ({ height }) => presentation({ width: "100%" }, jsx("td", {
222
+ height,
223
+ style: {
224
+ height: `${height}px`,
225
+ lineHeight: `${height}px`,
226
+ fontSize: "1px"
227
+ },
228
+ children: "​"
229
+ }));
230
+ /** A horizontal rule drawn as a cell border, which every target client renders. */
231
+ const Divider = ({ className, color = "#e5e7eb", thickness = 1, style }) => presentation({ width: "100%" }, jsx("td", {
232
+ className,
233
+ style: {
234
+ borderTop: `${thickness}px solid ${color}`,
235
+ fontSize: "1px",
236
+ lineHeight: "1px",
237
+ ...style
238
+ },
239
+ children: "​"
240
+ }));
241
+ /** An image with the width/height attributes Outlook sizes from before CSS applies. */
242
+ const Image = ({ className, src, alt, width, height, align, style }) => jsx("img", {
243
+ className,
244
+ src,
245
+ alt,
246
+ border: 0,
247
+ ...width === void 0 ? {} : { width },
248
+ ...height === void 0 ? {} : { height },
249
+ ...align === void 0 ? {} : { align },
250
+ style: {
251
+ display: "block",
252
+ outline: "none",
253
+ border: "none",
254
+ textDecoration: "none",
255
+ maxWidth: "100%",
256
+ ...style
257
+ }
258
+ });
259
+ /** A text link that opens outside the client and cannot reach back at the opener. */
260
+ const Link = ({ className, href, target = "_blank", rel = "noopener noreferrer", style, children }) => jsx("a", {
261
+ className,
262
+ href,
263
+ target,
264
+ rel,
265
+ style: {
266
+ color: "#067df7",
267
+ textDecoration: "none",
268
+ ...style
269
+ },
270
+ children
271
+ });
272
+ //#endregion
273
+ export { Email as a, PREHEADER_ATTRIBUTE as c, equalColumnWidth as d, preheaderNode as f, Divider as i, Section as l, Column as n, Image as o, Columns as r, Link as s, Button as t, Spacer as u };
274
+
275
+ //# sourceMappingURL=components-Bl6DT21n.js.map
@@ -153,7 +153,7 @@ declare const DIAGNOSTIC_CODES: {
153
153
  };
154
154
  readonly "reserved-attribute": {
155
155
  readonly summary: "An attribute is written only by a brand component.";
156
- readonly fix: "Render BrandFooter from samva:brand instead of marking an element as the footer.";
156
+ readonly fix: "Render BrandFooter from the brand import instead of marking an element as the footer.";
157
157
  };
158
158
  readonly "dynamic-class": {
159
159
  readonly summary: "A class name is built at run time, and Tailwind cannot see it.";
@@ -181,7 +181,7 @@ declare const DIAGNOSTIC_CODES: {
181
181
  };
182
182
  readonly "unknown-component": {
183
183
  readonly summary: "A component the profile cannot read.";
184
- readonly fix: "Import components from @samva/markup/email, samva:brand, or the project.";
184
+ readonly fix: "Import components from @samva/markup/email, the brand import, or the project.";
185
185
  };
186
186
  readonly "dynamic-numeric-prop": {
187
187
  readonly summary: "A prop that sizes layout at compile time is not a number literal.";
@@ -328,7 +328,7 @@ declare const DIAGNOSTIC_CODES: {
328
328
  readonly fix: "Use an https URL, an absolute path, or a relative path.";
329
329
  };
330
330
  readonly "brand-unavailable": {
331
- readonly summary: "The build has no brand to resolve a samva:brand import.";
331
+ readonly summary: "The build has no brand to resolve a brand import.";
332
332
  readonly fix: "Create or select the brand the import names.";
333
333
  };
334
334
  readonly "brand-import-conflict": {
@@ -151,7 +151,7 @@ const DIAGNOSTIC_CODES = codes({
151
151
  },
152
152
  "reserved-attribute": {
153
153
  summary: "An attribute is written only by a brand component.",
154
- fix: "Render BrandFooter from samva:brand instead of marking an element as the footer."
154
+ fix: "Render BrandFooter from the brand import instead of marking an element as the footer."
155
155
  },
156
156
  "dynamic-class": {
157
157
  summary: "A class name is built at run time, and Tailwind cannot see it.",
@@ -179,7 +179,7 @@ const DIAGNOSTIC_CODES = codes({
179
179
  },
180
180
  "unknown-component": {
181
181
  summary: "A component the profile cannot read.",
182
- fix: "Import components from @samva/markup/email, samva:brand, or the project."
182
+ fix: "Import components from @samva/markup/email, the brand import, or the project."
183
183
  },
184
184
  "dynamic-numeric-prop": {
185
185
  summary: "A prop that sizes layout at compile time is not a number literal.",
@@ -326,7 +326,7 @@ const DIAGNOSTIC_CODES = codes({
326
326
  fix: "Use an https URL, an absolute path, or a relative path."
327
327
  },
328
328
  "brand-unavailable": {
329
- summary: "The build has no brand to resolve a samva:brand import.",
329
+ summary: "The build has no brand to resolve a brand import.",
330
330
  fix: "Create or select the brand the import names."
331
331
  },
332
332
  "brand-import-conflict": {
package/dist/edit.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { n as isObject } from "./guards-eV1qLbf2.js";
2
2
  import { d as completeJsxSourceLocation, u as JSX_SOURCE_LOCATION_PROP } from "./jsx-runtime-CBlexoeR.js";
3
3
  import { t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
4
- import { c as WHATSAPP_HEADER_TYPES, n as introducedProfileErrors, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES, t as checkStaticProfile } from "./profile-pWGtWZXF.js";
4
+ import { c as WHATSAPP_HEADER_TYPES, n as introducedProfileErrors, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES, t as checkStaticProfile } from "./profile-PUfi3GPH.js";
5
5
  import { parse } from "@babel/parser";
6
6
  //#region src/source-edit.ts
7
7
  const node = (value) => isObject(value) && "type" in value && typeof value.type === "string" && "start" in value && typeof value.start === "number" && "end" in value && typeof value.end === "number";
package/dist/email.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as Email, c as PREHEADER_ATTRIBUTE, d as equalColumnWidth, f as preheaderNode, i as Divider, l as Section, n as Column, o as Image, r as Columns, s as Link, t as Button, u as Spacer } from "./components-BZCqegS8.js";
1
+ import { a as Email, c as PREHEADER_ATTRIBUTE, d as equalColumnWidth, f as preheaderNode, i as Divider, l as Section, n as Column, o as Image, r as Columns, s as Link, t as Button, u as Spacer } from "./components-Bl6DT21n.js";
2
2
  export { Button, Column, Columns, Divider, Email, Image, Link, PREHEADER_ATTRIBUTE, Section, Spacer, equalColumnWidth, preheaderNode };