@wervt/nuxt 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.js +37 -1
- package/dist/provision.d.ts +12 -0
- package/dist/provision.js +35 -1
- package/dist/runtime/app/useShape.d.ts +19 -11
- package/dist/runtime/app/useShape.js +3 -8
- package/dist/runtime/email/components.d.ts +191 -0
- package/dist/runtime/email/components.js +602 -0
- package/dist/runtime/email/index.d.ts +3 -0
- package/dist/runtime/email/index.js +3 -0
- package/dist/runtime/email/render.d.ts +17 -0
- package/dist/runtime/email/render.js +68 -0
- package/dist/runtime/server/email-preview.d.ts +4 -0
- package/dist/runtime/server/email-preview.js +30 -0
- package/dist/runtime/server/index.d.ts +5 -2
- package/dist/runtime/server/index.js +3 -1
- package/dist/runtime/server/mail.d.ts +23 -0
- package/dist/runtime/server/mail.js +43 -0
- package/dist/runtime/server/shape.d.ts +12 -5
- package/dist/runtime/server/shape.js +3 -1
- package/dist/runtime/shared/shape.d.ts +18 -0
- package/dist/runtime/shared/shape.js +16 -0
- package/dist/shapes.js +67 -0
- package/package.json +7 -1
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { defineComponent, h, inject } from "vue";
|
|
2
|
+
//#region src/runtime/email/components.ts
|
|
3
|
+
/** The template's subject, handed to renderEmail() while rendering. */
|
|
4
|
+
const SUBJECT_KEY = Symbol("wervt-email-subject");
|
|
5
|
+
const emailTheme = {
|
|
6
|
+
page: "#fafafa",
|
|
7
|
+
card: "#ffffff",
|
|
8
|
+
border: "#e5e5e5",
|
|
9
|
+
rule: "#d4d4d4",
|
|
10
|
+
text: "#262626",
|
|
11
|
+
highlighted: "#0a0a0a",
|
|
12
|
+
muted: "#666666",
|
|
13
|
+
dimmed: "#8f8f8f",
|
|
14
|
+
accent: "#2f5bff",
|
|
15
|
+
accentSoft: "#eef2ff",
|
|
16
|
+
accentBorder: "#d6defe",
|
|
17
|
+
band: "#0a0b0f",
|
|
18
|
+
bandText: "#d4d7de",
|
|
19
|
+
bandDimmed: "#6b7080",
|
|
20
|
+
button: "#2f5bff",
|
|
21
|
+
buttonText: "#ffffff",
|
|
22
|
+
codeBackground: "#f6f8ff",
|
|
23
|
+
radius: "8px",
|
|
24
|
+
font: "Poppins, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
|
25
|
+
mono: "'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
26
|
+
/** Where the logo images live. PNG, because Gmail and Outlook don't show SVG. */
|
|
27
|
+
assets: "https://wervt.app/email",
|
|
28
|
+
site: "https://wervt.app",
|
|
29
|
+
docs: "https://wervt.app/getting-started/introduction"
|
|
30
|
+
};
|
|
31
|
+
const t = emailTheme;
|
|
32
|
+
const darkStyles = `
|
|
33
|
+
:root { color-scheme: light dark; supported-color-schemes: light dark; }
|
|
34
|
+
@media (prefers-color-scheme: dark) {
|
|
35
|
+
.wv-page { background: #0a0a0a !important; }
|
|
36
|
+
.wv-card { background: #111113 !important; }
|
|
37
|
+
.wv-foot { background: #0a0a0a !important; }
|
|
38
|
+
.wv-frame { border-color: #262626 !important; }
|
|
39
|
+
.wv-line { background: #262626 !important; }
|
|
40
|
+
.wv-mark { background: #6b6b6b !important; }
|
|
41
|
+
.wv-sep { color: #404040 !important; }
|
|
42
|
+
.wv-text { color: #e5e5e5 !important; }
|
|
43
|
+
.wv-strong { color: #fafafa !important; }
|
|
44
|
+
.wv-muted { color: #a1a1a1 !important; }
|
|
45
|
+
.wv-accent { color: #7c9bff !important; }
|
|
46
|
+
.wv-code { background: #151a2e !important; border-color: #26304f !important; color: #fafafa !important; }
|
|
47
|
+
}
|
|
48
|
+
[data-ogsc] .wv-text { color: #e5e5e5 !important; }
|
|
49
|
+
[data-ogsc] .wv-strong { color: #fafafa !important; }
|
|
50
|
+
[data-ogsc] .wv-muted { color: #a1a1a1 !important; }
|
|
51
|
+
@media only screen and (max-width: 480px) {
|
|
52
|
+
.wv-pad { padding-left: 24px !important; padding-right: 24px !important; }
|
|
53
|
+
.wv-hero { font-size: 22px !important; line-height: 30px !important; }
|
|
54
|
+
.wv-codetext { font-size: 28px !important; letter-spacing: 6px !important; }
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
/**
|
|
58
|
+
* The whole email: document, preheader, header with the logo, body and footer. Put it at the
|
|
59
|
+
* root of every template.
|
|
60
|
+
*
|
|
61
|
+
* ```vue
|
|
62
|
+
* <WervtEmail subject="Your code" app="kontor"> … </WervtEmail>
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
const WervtEmail = defineComponent({
|
|
66
|
+
name: "WervtEmail",
|
|
67
|
+
props: {
|
|
68
|
+
/** The email's subject line. */
|
|
69
|
+
subject: {
|
|
70
|
+
type: String,
|
|
71
|
+
required: true
|
|
72
|
+
},
|
|
73
|
+
/** The app that sends the mail, shown in the header and the footer. */
|
|
74
|
+
app: {
|
|
75
|
+
type: String,
|
|
76
|
+
default: "wervt"
|
|
77
|
+
},
|
|
78
|
+
/** The preview line inboxes show after the subject. */
|
|
79
|
+
preview: {
|
|
80
|
+
type: String,
|
|
81
|
+
default: ""
|
|
82
|
+
},
|
|
83
|
+
/** Replaces the footer's "Sent by <app> via wervt" line. */
|
|
84
|
+
footer: {
|
|
85
|
+
type: String,
|
|
86
|
+
default: ""
|
|
87
|
+
},
|
|
88
|
+
/** Why the recipient got this mail, shown small in the footer. */
|
|
89
|
+
reason: {
|
|
90
|
+
type: String,
|
|
91
|
+
default: ""
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
setup(props, { slots }) {
|
|
95
|
+
inject(SUBJECT_KEY, void 0)?.(props.subject);
|
|
96
|
+
return () => h("html", { lang: "en" }, [h("head", [
|
|
97
|
+
h("meta", { charset: "utf-8" }),
|
|
98
|
+
h("meta", {
|
|
99
|
+
name: "viewport",
|
|
100
|
+
content: "width=device-width, initial-scale=1"
|
|
101
|
+
}),
|
|
102
|
+
h("meta", {
|
|
103
|
+
name: "color-scheme",
|
|
104
|
+
content: "light dark"
|
|
105
|
+
}),
|
|
106
|
+
h("meta", {
|
|
107
|
+
name: "supported-color-schemes",
|
|
108
|
+
content: "light dark"
|
|
109
|
+
}),
|
|
110
|
+
h("title", props.subject),
|
|
111
|
+
h("link", {
|
|
112
|
+
rel: "stylesheet",
|
|
113
|
+
href: "https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap"
|
|
114
|
+
}),
|
|
115
|
+
h("style", { innerHTML: darkStyles })
|
|
116
|
+
]), h("body", {
|
|
117
|
+
class: "wv-page",
|
|
118
|
+
style: {
|
|
119
|
+
margin: 0,
|
|
120
|
+
padding: 0,
|
|
121
|
+
background: t.page
|
|
122
|
+
}
|
|
123
|
+
}, [props.preview ? h("div", {
|
|
124
|
+
class: "wervt-preheader",
|
|
125
|
+
style: {
|
|
126
|
+
display: "none",
|
|
127
|
+
overflow: "hidden",
|
|
128
|
+
maxHeight: 0,
|
|
129
|
+
maxWidth: 0,
|
|
130
|
+
opacity: 0,
|
|
131
|
+
lineHeight: "1px",
|
|
132
|
+
fontSize: "1px",
|
|
133
|
+
color: t.page
|
|
134
|
+
}
|
|
135
|
+
}, props.preview) : null, table({
|
|
136
|
+
width: "100%",
|
|
137
|
+
class: "wv-page",
|
|
138
|
+
style: { background: t.page }
|
|
139
|
+
}, [h("tr", [h("td", {
|
|
140
|
+
align: "center",
|
|
141
|
+
style: { padding: "0 12px" }
|
|
142
|
+
}, table({
|
|
143
|
+
width: "100%",
|
|
144
|
+
style: { maxWidth: "600px" }
|
|
145
|
+
}, [
|
|
146
|
+
stub(),
|
|
147
|
+
rule(true, none, {
|
|
148
|
+
color: t.band,
|
|
149
|
+
image: `url('${t.assets}/header.png') right top / 600px 100px no-repeat`
|
|
150
|
+
}),
|
|
151
|
+
framed(header(props.app), { padding: 0 }),
|
|
152
|
+
framed(accentLine(), { padding: 0 }),
|
|
153
|
+
framed(slots.default?.(), {
|
|
154
|
+
class: "wv-card wv-pad",
|
|
155
|
+
background: t.card,
|
|
156
|
+
padding: "36px 40px 20px",
|
|
157
|
+
fontFamily: t.font,
|
|
158
|
+
color: t.text
|
|
159
|
+
}),
|
|
160
|
+
rule(true, {
|
|
161
|
+
color: t.card,
|
|
162
|
+
class: "wv-card"
|
|
163
|
+
}, {
|
|
164
|
+
color: t.page,
|
|
165
|
+
class: "wv-foot"
|
|
166
|
+
}),
|
|
167
|
+
framed(footer(props), {
|
|
168
|
+
class: "wv-foot wv-pad",
|
|
169
|
+
background: t.page,
|
|
170
|
+
padding: "24px 40px 28px"
|
|
171
|
+
}),
|
|
172
|
+
rule(false, {
|
|
173
|
+
color: t.page,
|
|
174
|
+
class: "wv-foot"
|
|
175
|
+
}, none),
|
|
176
|
+
stub()
|
|
177
|
+
]))])])])]);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
/** The dark band on top: the lockup over the dot grid, the sending app on the right. */
|
|
181
|
+
function header(app) {
|
|
182
|
+
return table({
|
|
183
|
+
width: "100%",
|
|
184
|
+
bgcolor: t.band,
|
|
185
|
+
background: `${t.assets}/header.png`,
|
|
186
|
+
style: { background: `${t.band} url('${t.assets}/header.png') right -4px / 600px 100px no-repeat` }
|
|
187
|
+
}, [h("tr", [h("td", {
|
|
188
|
+
class: "wv-pad wervt-skip-text",
|
|
189
|
+
style: {
|
|
190
|
+
padding: "30px 40px",
|
|
191
|
+
verticalAlign: "middle"
|
|
192
|
+
}
|
|
193
|
+
}, h("a", {
|
|
194
|
+
href: t.site,
|
|
195
|
+
style: {
|
|
196
|
+
textDecoration: "none",
|
|
197
|
+
color: "#fafafa"
|
|
198
|
+
}
|
|
199
|
+
}, h("img", {
|
|
200
|
+
src: `${t.assets}/lockup-on-dark.png`,
|
|
201
|
+
width: 116,
|
|
202
|
+
height: 32,
|
|
203
|
+
alt: "wervt",
|
|
204
|
+
style: {
|
|
205
|
+
display: "block",
|
|
206
|
+
width: "116px",
|
|
207
|
+
height: "32px",
|
|
208
|
+
border: 0,
|
|
209
|
+
outline: "none",
|
|
210
|
+
fontFamily: t.font,
|
|
211
|
+
fontSize: "24px",
|
|
212
|
+
lineHeight: "32px",
|
|
213
|
+
fontWeight: 600,
|
|
214
|
+
letterSpacing: "-0.5px",
|
|
215
|
+
color: "#fafafa"
|
|
216
|
+
}
|
|
217
|
+
}))), app === "wervt" ? null : h("td", {
|
|
218
|
+
class: "wv-pad wervt-skip-text",
|
|
219
|
+
align: "right",
|
|
220
|
+
style: {
|
|
221
|
+
padding: "30px 40px",
|
|
222
|
+
verticalAlign: "middle"
|
|
223
|
+
}
|
|
224
|
+
}, table({ align: "right" }, [h("tr", [h("td", { style: {
|
|
225
|
+
border: "1px solid #2a2e38",
|
|
226
|
+
borderRadius: "999px",
|
|
227
|
+
background: "#12141b",
|
|
228
|
+
padding: "5px 12px",
|
|
229
|
+
fontFamily: t.mono,
|
|
230
|
+
fontSize: "12px",
|
|
231
|
+
lineHeight: "16px",
|
|
232
|
+
color: t.bandText,
|
|
233
|
+
whiteSpace: "nowrap"
|
|
234
|
+
} }, [h("span", { style: { color: "#5885ff" } }, "●"), `\u00A0\u00A0${app}`])])]))])]);
|
|
235
|
+
}
|
|
236
|
+
/** The landing page's headline gradient, as a hairline under the header. */
|
|
237
|
+
function accentLine() {
|
|
238
|
+
return table({ width: "100%" }, [h("tr", [h("td", {
|
|
239
|
+
height: 2,
|
|
240
|
+
bgcolor: t.accent,
|
|
241
|
+
style: {
|
|
242
|
+
height: "2px",
|
|
243
|
+
fontSize: 0,
|
|
244
|
+
lineHeight: 0,
|
|
245
|
+
background: `${t.accent} linear-gradient(90deg, #60a5fa, ${t.accent} 45%, #a78bfa)`
|
|
246
|
+
}
|
|
247
|
+
})])]);
|
|
248
|
+
}
|
|
249
|
+
function footer(props) {
|
|
250
|
+
const sentBy = props.footer || (props.app === "wervt" ? "Sent by wervt" : `Sent by ${props.app} via wervt`);
|
|
251
|
+
const small = {
|
|
252
|
+
fontFamily: t.font,
|
|
253
|
+
fontSize: "12px",
|
|
254
|
+
lineHeight: "18px"
|
|
255
|
+
};
|
|
256
|
+
const link = {
|
|
257
|
+
color: t.muted,
|
|
258
|
+
textDecoration: "none"
|
|
259
|
+
};
|
|
260
|
+
return table({ width: "100%" }, [h("tr", [h("td", {
|
|
261
|
+
width: 40,
|
|
262
|
+
style: {
|
|
263
|
+
width: "40px",
|
|
264
|
+
verticalAlign: "top",
|
|
265
|
+
paddingTop: "2px"
|
|
266
|
+
}
|
|
267
|
+
}, h("img", {
|
|
268
|
+
src: `${t.assets}/mark-tile.png`,
|
|
269
|
+
width: 28,
|
|
270
|
+
height: 28,
|
|
271
|
+
alt: "",
|
|
272
|
+
style: {
|
|
273
|
+
display: "block",
|
|
274
|
+
width: "28px",
|
|
275
|
+
height: "28px",
|
|
276
|
+
border: 0,
|
|
277
|
+
borderRadius: "7px"
|
|
278
|
+
}
|
|
279
|
+
})), h("td", { style: { verticalAlign: "top" } }, [
|
|
280
|
+
h("p", {
|
|
281
|
+
class: "wv-strong",
|
|
282
|
+
style: {
|
|
283
|
+
...small,
|
|
284
|
+
margin: 0,
|
|
285
|
+
fontSize: "13px",
|
|
286
|
+
fontWeight: 500,
|
|
287
|
+
color: t.highlighted
|
|
288
|
+
}
|
|
289
|
+
}, sentBy),
|
|
290
|
+
props.reason ? h("p", {
|
|
291
|
+
class: "wv-muted",
|
|
292
|
+
style: {
|
|
293
|
+
...small,
|
|
294
|
+
margin: "4px 0 0",
|
|
295
|
+
color: t.muted
|
|
296
|
+
}
|
|
297
|
+
}, props.reason) : null,
|
|
298
|
+
h("p", {
|
|
299
|
+
class: "wv-muted",
|
|
300
|
+
style: {
|
|
301
|
+
...small,
|
|
302
|
+
margin: "10px 0 0",
|
|
303
|
+
fontFamily: t.mono,
|
|
304
|
+
fontSize: "11px",
|
|
305
|
+
color: t.dimmed
|
|
306
|
+
}
|
|
307
|
+
}, [
|
|
308
|
+
h("a", {
|
|
309
|
+
href: t.site,
|
|
310
|
+
class: "wv-muted",
|
|
311
|
+
style: link
|
|
312
|
+
}, "wervt.app"),
|
|
313
|
+
separator(),
|
|
314
|
+
h("a", {
|
|
315
|
+
href: t.docs,
|
|
316
|
+
class: "wv-muted",
|
|
317
|
+
style: link
|
|
318
|
+
}, "Docs")
|
|
319
|
+
])
|
|
320
|
+
])])]);
|
|
321
|
+
}
|
|
322
|
+
function separator() {
|
|
323
|
+
return h("span", {
|
|
324
|
+
class: "wv-sep",
|
|
325
|
+
style: { color: t.rule }
|
|
326
|
+
}, "\xA0\xA0/\xA0\xA0");
|
|
327
|
+
}
|
|
328
|
+
/** A row inside the frame: the hairlines run down both sides, like the landing page's sections. */
|
|
329
|
+
function framed(children, style) {
|
|
330
|
+
const { class: className, ...rest } = style;
|
|
331
|
+
return h("tr", [h("td", { style: { padding: "0 4px" } }, table({ width: "100%" }, [h("tr", [h("td", {
|
|
332
|
+
class: ["wv-frame", className],
|
|
333
|
+
style: {
|
|
334
|
+
borderLeft: `1px solid ${t.border}`,
|
|
335
|
+
borderRight: `1px solid ${t.border}`,
|
|
336
|
+
...rest
|
|
337
|
+
}
|
|
338
|
+
}, children)])]))]);
|
|
339
|
+
}
|
|
340
|
+
/** The frame's sides fading out above and below, so it reads as part of a larger sheet. */
|
|
341
|
+
function stub() {
|
|
342
|
+
return framed(h("div", { style: {
|
|
343
|
+
height: "28px",
|
|
344
|
+
lineHeight: "28px",
|
|
345
|
+
fontSize: 0
|
|
346
|
+
} }, "\xA0"), { padding: 0 });
|
|
347
|
+
}
|
|
348
|
+
const none = { color: "transparent" };
|
|
349
|
+
/**
|
|
350
|
+
* A horizontal rule across the frame, drawn as a 9px-high grid so the "+" marks can sit where it
|
|
351
|
+
* crosses the sides. `above` and `below` paint the frame's inside around the line, so the
|
|
352
|
+
* sections it separates start right at the line instead of 4px off.
|
|
353
|
+
*/
|
|
354
|
+
function rule(crosshairs, above, below) {
|
|
355
|
+
const mark = {
|
|
356
|
+
color: t.dimmed,
|
|
357
|
+
class: "wv-mark"
|
|
358
|
+
};
|
|
359
|
+
const line = {
|
|
360
|
+
color: t.border,
|
|
361
|
+
class: "wv-line"
|
|
362
|
+
};
|
|
363
|
+
const side = crosshairs ? mark : line;
|
|
364
|
+
const cell = (fill, width, height, colspan) => h("td", {
|
|
365
|
+
width,
|
|
366
|
+
height,
|
|
367
|
+
colspan,
|
|
368
|
+
bgcolor: fill === none ? void 0 : fill.color,
|
|
369
|
+
class: fill.class,
|
|
370
|
+
style: {
|
|
371
|
+
width: width === void 0 ? void 0 : `${width}px`,
|
|
372
|
+
height: `${height}px`,
|
|
373
|
+
fontSize: 0,
|
|
374
|
+
lineHeight: 0,
|
|
375
|
+
background: fill.image ? `${fill.color} ${fill.image}` : fill.color
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
const row = (fill, height) => h("tr", [
|
|
379
|
+
cell(none, 4, height),
|
|
380
|
+
cell(side, 1, height),
|
|
381
|
+
cell(fill, void 0, height, 3),
|
|
382
|
+
cell(side, 1, height),
|
|
383
|
+
cell(none, 4, height)
|
|
384
|
+
]);
|
|
385
|
+
const arm = crosshairs ? mark : line;
|
|
386
|
+
const crossing = h("tr", [
|
|
387
|
+
cell(crosshairs ? mark : none, 4, 1),
|
|
388
|
+
cell(side, 1, 1),
|
|
389
|
+
cell(arm, 4, 1),
|
|
390
|
+
cell(line, void 0, 1),
|
|
391
|
+
cell(arm, 4, 1),
|
|
392
|
+
cell(side, 1, 1),
|
|
393
|
+
cell(crosshairs ? mark : none, 4, 1)
|
|
394
|
+
]);
|
|
395
|
+
return h("tr", [h("td", table({ width: "100%" }, [
|
|
396
|
+
row(above, 4),
|
|
397
|
+
crossing,
|
|
398
|
+
row(below, 4)
|
|
399
|
+
]))]);
|
|
400
|
+
}
|
|
401
|
+
const EmailHeading = defineComponent({
|
|
402
|
+
name: "EmailHeading",
|
|
403
|
+
props: {
|
|
404
|
+
/** A short label above the heading, in the landing page's mono eyebrow style. */
|
|
405
|
+
eyebrow: {
|
|
406
|
+
type: String,
|
|
407
|
+
default: ""
|
|
408
|
+
} },
|
|
409
|
+
setup(props, { slots }) {
|
|
410
|
+
return () => [props.eyebrow ? h("p", {
|
|
411
|
+
class: "wv-accent",
|
|
412
|
+
style: {
|
|
413
|
+
margin: "0 0 10px",
|
|
414
|
+
fontFamily: t.mono,
|
|
415
|
+
fontSize: "12px",
|
|
416
|
+
lineHeight: "16px",
|
|
417
|
+
letterSpacing: "0.5px",
|
|
418
|
+
textTransform: "uppercase",
|
|
419
|
+
color: t.accent
|
|
420
|
+
}
|
|
421
|
+
}, props.eyebrow) : null, h("h1", {
|
|
422
|
+
class: "wv-strong wv-hero",
|
|
423
|
+
style: {
|
|
424
|
+
margin: "0 0 16px",
|
|
425
|
+
fontFamily: t.font,
|
|
426
|
+
fontSize: "26px",
|
|
427
|
+
lineHeight: "34px",
|
|
428
|
+
fontWeight: 600,
|
|
429
|
+
letterSpacing: "-0.6px",
|
|
430
|
+
color: t.highlighted
|
|
431
|
+
}
|
|
432
|
+
}, slots.default?.())];
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
const EmailText = defineComponent({
|
|
436
|
+
name: "EmailText",
|
|
437
|
+
setup(_, { slots }) {
|
|
438
|
+
return () => h("p", {
|
|
439
|
+
class: "wv-text",
|
|
440
|
+
style: {
|
|
441
|
+
margin: "0 0 16px",
|
|
442
|
+
fontFamily: t.font,
|
|
443
|
+
fontSize: "15px",
|
|
444
|
+
lineHeight: "24px",
|
|
445
|
+
color: t.text
|
|
446
|
+
}
|
|
447
|
+
}, slots.default?.());
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
/** Secondary text: hints, "ignore this if …". */
|
|
451
|
+
const EmailMuted = defineComponent({
|
|
452
|
+
name: "EmailMuted",
|
|
453
|
+
setup(_, { slots }) {
|
|
454
|
+
return () => h("p", {
|
|
455
|
+
class: "wv-muted",
|
|
456
|
+
style: {
|
|
457
|
+
margin: "0 0 16px",
|
|
458
|
+
fontFamily: t.font,
|
|
459
|
+
fontSize: "13px",
|
|
460
|
+
lineHeight: "20px",
|
|
461
|
+
color: t.muted
|
|
462
|
+
}
|
|
463
|
+
}, slots.default?.());
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
const EmailLink = defineComponent({
|
|
467
|
+
name: "EmailLink",
|
|
468
|
+
props: { href: {
|
|
469
|
+
type: String,
|
|
470
|
+
required: true
|
|
471
|
+
} },
|
|
472
|
+
setup(props, { slots }) {
|
|
473
|
+
return () => h("a", {
|
|
474
|
+
href: props.href,
|
|
475
|
+
class: "wv-accent",
|
|
476
|
+
style: {
|
|
477
|
+
color: t.accent,
|
|
478
|
+
textDecoration: "underline"
|
|
479
|
+
}
|
|
480
|
+
}, slots.default?.() ?? props.href);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
/** A call to action. Bulletproof: a table cell carries the background, so Outlook shows it too. */
|
|
484
|
+
const EmailButton = defineComponent({
|
|
485
|
+
name: "EmailButton",
|
|
486
|
+
props: {
|
|
487
|
+
href: {
|
|
488
|
+
type: String,
|
|
489
|
+
required: true
|
|
490
|
+
},
|
|
491
|
+
/** `primary` is the landing page's blue; `neutral` the dashboard's black. */
|
|
492
|
+
color: {
|
|
493
|
+
type: String,
|
|
494
|
+
default: "primary"
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
setup(props, { slots }) {
|
|
498
|
+
return () => {
|
|
499
|
+
const background = props.color === "neutral" ? "#171717" : t.button;
|
|
500
|
+
return table({ style: { margin: "8px 0 24px" } }, [h("tr", [h("td", {
|
|
501
|
+
bgcolor: background,
|
|
502
|
+
style: {
|
|
503
|
+
background,
|
|
504
|
+
borderRadius: t.radius
|
|
505
|
+
}
|
|
506
|
+
}, h("a", {
|
|
507
|
+
href: props.href,
|
|
508
|
+
style: {
|
|
509
|
+
display: "inline-block",
|
|
510
|
+
padding: "12px 22px",
|
|
511
|
+
border: `1px solid ${background}`,
|
|
512
|
+
fontFamily: t.font,
|
|
513
|
+
fontSize: "14px",
|
|
514
|
+
fontWeight: 500,
|
|
515
|
+
lineHeight: "20px",
|
|
516
|
+
color: t.buttonText,
|
|
517
|
+
textDecoration: "none",
|
|
518
|
+
borderRadius: t.radius
|
|
519
|
+
}
|
|
520
|
+
}, [slots.default?.(), h("span", { class: "wervt-skip-text" }, "\xA0\xA0→")]))])]);
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
/** A one-time code, large and spaced so it reads (and copies) easily. */
|
|
525
|
+
const EmailCode = defineComponent({
|
|
526
|
+
name: "EmailCode",
|
|
527
|
+
props: {
|
|
528
|
+
code: {
|
|
529
|
+
type: String,
|
|
530
|
+
required: true
|
|
531
|
+
},
|
|
532
|
+
/** A small line under the code, e.g. when it expires. */
|
|
533
|
+
caption: {
|
|
534
|
+
type: String,
|
|
535
|
+
default: ""
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
setup(props) {
|
|
539
|
+
return () => table({
|
|
540
|
+
width: "100%",
|
|
541
|
+
style: { margin: "8px 0 24px" }
|
|
542
|
+
}, [h("tr", [h("td", {
|
|
543
|
+
align: "center",
|
|
544
|
+
bgcolor: t.codeBackground,
|
|
545
|
+
class: "wv-code",
|
|
546
|
+
style: {
|
|
547
|
+
background: t.codeBackground,
|
|
548
|
+
border: `1px solid ${t.accentBorder}`,
|
|
549
|
+
borderRadius: "10px",
|
|
550
|
+
padding: props.caption ? "22px 16px 16px" : "22px 16px"
|
|
551
|
+
}
|
|
552
|
+
}, [h("div", {
|
|
553
|
+
class: "wv-strong wv-codetext",
|
|
554
|
+
style: {
|
|
555
|
+
fontFamily: t.mono,
|
|
556
|
+
fontSize: "36px",
|
|
557
|
+
lineHeight: "44px",
|
|
558
|
+
fontWeight: 600,
|
|
559
|
+
letterSpacing: "10px",
|
|
560
|
+
paddingLeft: "10px",
|
|
561
|
+
color: t.highlighted
|
|
562
|
+
}
|
|
563
|
+
}, props.code), props.caption ? h("div", {
|
|
564
|
+
class: "wv-muted",
|
|
565
|
+
style: {
|
|
566
|
+
marginTop: "8px",
|
|
567
|
+
fontFamily: t.mono,
|
|
568
|
+
fontSize: "12px",
|
|
569
|
+
lineHeight: "16px",
|
|
570
|
+
color: t.muted
|
|
571
|
+
}
|
|
572
|
+
}, props.caption) : null])])]);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
const EmailDivider = defineComponent({
|
|
576
|
+
name: "EmailDivider",
|
|
577
|
+
setup() {
|
|
578
|
+
return () => h("hr", {
|
|
579
|
+
class: "wv-frame",
|
|
580
|
+
style: {
|
|
581
|
+
border: "none",
|
|
582
|
+
borderTop: `1px solid ${t.border}`,
|
|
583
|
+
margin: "24px 0"
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
function table(attrs, children) {
|
|
589
|
+
return h("table", {
|
|
590
|
+
role: "presentation",
|
|
591
|
+
cellpadding: 0,
|
|
592
|
+
cellspacing: 0,
|
|
593
|
+
border: 0,
|
|
594
|
+
...attrs,
|
|
595
|
+
style: {
|
|
596
|
+
borderCollapse: "collapse",
|
|
597
|
+
...attrs.style
|
|
598
|
+
}
|
|
599
|
+
}, children);
|
|
600
|
+
}
|
|
601
|
+
//#endregion
|
|
602
|
+
export { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, EmailText, SUBJECT_KEY, WervtEmail, emailTheme };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, EmailText, WervtEmail, emailTheme } from "./components.js";
|
|
2
|
+
import { EmailProps, RenderedEmail, renderEmail } from "./render.js";
|
|
3
|
+
export { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, type EmailProps, EmailText, type RenderedEmail, WervtEmail, emailTheme, renderEmail };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, EmailText, WervtEmail, emailTheme } from "./components.js";
|
|
2
|
+
import { renderEmail } from "./render.js";
|
|
3
|
+
export { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, EmailText, WervtEmail, emailTheme, renderEmail };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Component } from "vue";
|
|
2
|
+
//#region src/runtime/email/render.d.ts
|
|
3
|
+
export interface RenderedEmail {
|
|
4
|
+
subject: string;
|
|
5
|
+
html: string;
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
/** The props a component declares, so templates are called with the right data. */
|
|
9
|
+
export type EmailProps<T> = T extends (new () => {
|
|
10
|
+
$props: infer P;
|
|
11
|
+
}) ? Omit<P, `on${string}`> : never;
|
|
12
|
+
/**
|
|
13
|
+
* Renders an email template (a component with `<WervtEmail>` at its root) to HTML, a plain-text
|
|
14
|
+
* part and its subject.
|
|
15
|
+
*/
|
|
16
|
+
export declare function renderEmail<T extends Component>(template: T, props?: EmailProps<T>): Promise<RenderedEmail>;
|
|
17
|
+
//#endregion
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { SUBJECT_KEY } from "./components.js";
|
|
2
|
+
import { createSSRApp } from "vue";
|
|
3
|
+
import { convert } from "html-to-text";
|
|
4
|
+
import { renderToString } from "vue/server-renderer";
|
|
5
|
+
//#region src/runtime/email/render.ts
|
|
6
|
+
const DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
|
|
7
|
+
/**
|
|
8
|
+
* Renders an email template (a component with `<WervtEmail>` at its root) to HTML, a plain-text
|
|
9
|
+
* part and its subject.
|
|
10
|
+
*/
|
|
11
|
+
async function renderEmail(template, props) {
|
|
12
|
+
let subject = "";
|
|
13
|
+
const app = createSSRApp(template, props);
|
|
14
|
+
app.provide(SUBJECT_KEY, (value) => subject = value);
|
|
15
|
+
const body = (await renderToString(app)).replace(/<!--[\s\S]*?-->/g, "");
|
|
16
|
+
if (!subject) throw new Error("Email templates need <WervtEmail subject=\"…\"> at their root");
|
|
17
|
+
const html = `${DOCTYPE}${body}`;
|
|
18
|
+
return {
|
|
19
|
+
subject,
|
|
20
|
+
html,
|
|
21
|
+
text: toText(html)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function toText(html) {
|
|
25
|
+
return convert(html, {
|
|
26
|
+
wordwrap: 78,
|
|
27
|
+
selectors: [
|
|
28
|
+
{
|
|
29
|
+
selector: ".wervt-preheader",
|
|
30
|
+
format: "skip"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
selector: ".wervt-skip-text",
|
|
34
|
+
format: "skip"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
selector: "title",
|
|
38
|
+
format: "skip"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
selector: "img",
|
|
42
|
+
format: "skip"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
selector: "h1",
|
|
46
|
+
options: { uppercase: false }
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
selector: "a",
|
|
50
|
+
options: { hideLinkHrefIfSameAsText: true }
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
selector: "table",
|
|
54
|
+
format: "block"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
selector: "tr",
|
|
58
|
+
format: "block"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
selector: "td",
|
|
62
|
+
format: "block"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}).trim();
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { renderEmail };
|