@wervt/nuxt 0.2.15 → 0.3.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.
@@ -5,21 +5,58 @@ const SUBJECT_KEY = Symbol("wervt-email-subject");
5
5
  const emailTheme = {
6
6
  page: "#fafafa",
7
7
  card: "#ffffff",
8
- border: "#ebebeb",
9
- text: "#171717",
8
+ border: "#e5e5e5",
9
+ rule: "#d4d4d4",
10
+ text: "#262626",
10
11
  highlighted: "#0a0a0a",
11
12
  muted: "#666666",
12
13
  dimmed: "#8f8f8f",
13
- button: "#171717",
14
+ accent: "#2f5bff",
15
+ accentSoft: "#eef2ff",
16
+ accentBorder: "#d6defe",
17
+ band: "#0a0b0f",
18
+ bandText: "#d4d7de",
19
+ bandDimmed: "#6b7080",
20
+ button: "#2f5bff",
14
21
  buttonText: "#ffffff",
15
- codeBackground: "#f2f2f2",
16
- radius: "6px",
22
+ codeBackground: "#f6f8ff",
23
+ radius: "8px",
17
24
  font: "Poppins, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
18
- mono: "'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
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"
19
30
  };
20
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
+ `;
21
57
  /**
22
- * The whole email: document, preheader, card and footer. Put it at the root of every template.
58
+ * The whole email: document, preheader, header with the logo, body and footer. Put it at the
59
+ * root of every template.
23
60
  *
24
61
  * ```vue
25
62
  * <WervtEmail subject="Your code" app="kontor"> … </WervtEmail>
@@ -33,7 +70,7 @@ const WervtEmail = defineComponent({
33
70
  type: String,
34
71
  required: true
35
72
  },
36
- /** Shown above the card and in the footer, e.g. the app's name. */
73
+ /** The app that sends the mail, shown in the header and the footer. */
37
74
  app: {
38
75
  type: String,
39
76
  default: "wervt"
@@ -43,10 +80,15 @@ const WervtEmail = defineComponent({
43
80
  type: String,
44
81
  default: ""
45
82
  },
46
- /** Replaces the footer line ("Sent by <app> on wervt."). */
83
+ /** Replaces the footer's "Sent by <app> via wervt" line. */
47
84
  footer: {
48
85
  type: String,
49
86
  default: ""
87
+ },
88
+ /** Why the recipient got this mail, shown small in the footer. */
89
+ reason: {
90
+ type: String,
91
+ default: ""
50
92
  }
51
93
  },
52
94
  setup(props, { slots }) {
@@ -59,22 +101,26 @@ const WervtEmail = defineComponent({
59
101
  }),
60
102
  h("meta", {
61
103
  name: "color-scheme",
62
- content: "light"
104
+ content: "light dark"
63
105
  }),
64
106
  h("meta", {
65
107
  name: "supported-color-schemes",
66
- content: "light"
108
+ content: "light dark"
67
109
  }),
68
110
  h("title", props.subject),
69
111
  h("link", {
70
112
  rel: "stylesheet",
71
- href: "https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap"
72
- })
73
- ]), h("body", { style: {
74
- margin: 0,
75
- padding: 0,
76
- background: t.page
77
- } }, [props.preview ? h("div", {
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", {
78
124
  class: "wervt-preheader",
79
125
  style: {
80
126
  display: "none",
@@ -88,75 +134,333 @@ const WervtEmail = defineComponent({
88
134
  }
89
135
  }, props.preview) : null, table({
90
136
  width: "100%",
137
+ class: "wv-page",
91
138
  style: { background: t.page }
92
139
  }, [h("tr", [h("td", {
93
140
  align: "center",
94
- style: { padding: "40px 16px" }
141
+ style: { padding: "0 12px" }
95
142
  }, table({
96
143
  width: "100%",
97
- style: { maxWidth: "560px" }
144
+ style: { maxWidth: "600px" }
98
145
  }, [
99
- h("tr", [h("td", { style: {
100
- padding: "0 4px 16px",
101
- fontFamily: t.font,
102
- fontSize: "14px",
103
- fontWeight: 600,
104
- color: t.highlighted
105
- } }, props.app)]),
106
- h("tr", [h("td", { style: {
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",
107
155
  background: t.card,
108
- border: `1px solid ${t.border}`,
109
- borderRadius: t.radius,
110
- padding: "32px",
156
+ padding: "36px 40px 20px",
111
157
  fontFamily: t.font,
112
158
  color: t.text
113
- } }, slots.default?.())]),
114
- h("tr", [h("td", { style: {
115
- padding: "16px 4px 0",
116
- fontFamily: t.font,
117
- fontSize: "12px",
118
- lineHeight: "18px",
119
- color: t.dimmed
120
- } }, props.footer || (props.app === "wervt" ? "Sent by wervt." : `Sent by ${props.app} on wervt.`))])
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()
121
177
  ]))])])])]);
122
178
  }
123
179
  });
124
- const EmailHeading = defineComponent({
125
- name: "EmailHeading",
126
- setup(_, { slots }) {
127
- return () => h("h1", { style: {
128
- margin: "0 0 16px",
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",
129
210
  fontFamily: t.font,
130
- fontSize: "20px",
131
- lineHeight: "28px",
211
+ fontSize: "24px",
212
+ lineHeight: "32px",
132
213
  fontWeight: 600,
133
- color: t.highlighted
134
- } }, slots.default?.());
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?.())];
135
433
  }
136
434
  });
137
435
  const EmailText = defineComponent({
138
436
  name: "EmailText",
139
437
  setup(_, { slots }) {
140
- return () => h("p", { style: {
141
- margin: "0 0 16px",
142
- fontFamily: t.font,
143
- fontSize: "15px",
144
- lineHeight: "24px",
145
- color: t.text
146
- } }, slots.default?.());
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?.());
147
448
  }
148
449
  });
149
450
  /** Secondary text: hints, "ignore this if …". */
150
451
  const EmailMuted = defineComponent({
151
452
  name: "EmailMuted",
152
453
  setup(_, { slots }) {
153
- return () => h("p", { style: {
154
- margin: "0 0 16px",
155
- fontFamily: t.font,
156
- fontSize: "13px",
157
- lineHeight: "20px",
158
- color: t.muted
159
- } }, slots.default?.());
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?.());
160
464
  }
161
465
  });
162
466
  const EmailLink = defineComponent({
@@ -168,8 +472,9 @@ const EmailLink = defineComponent({
168
472
  setup(props, { slots }) {
169
473
  return () => h("a", {
170
474
  href: props.href,
475
+ class: "wv-accent",
171
476
  style: {
172
- color: t.highlighted,
477
+ color: t.accent,
173
478
  textDecoration: "underline"
174
479
  }
175
480
  }, slots.default?.() ?? props.href);
@@ -178,64 +483,106 @@ const EmailLink = defineComponent({
178
483
  /** A call to action. Bulletproof: a table cell carries the background, so Outlook shows it too. */
179
484
  const EmailButton = defineComponent({
180
485
  name: "EmailButton",
181
- props: { href: {
182
- type: String,
183
- required: true
184
- } },
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
+ },
185
497
  setup(props, { slots }) {
186
- return () => table({ style: { margin: "8px 0 24px" } }, [h("tr", [h("td", { style: {
187
- background: t.button,
188
- borderRadius: t.radius
189
- } }, h("a", {
190
- href: props.href,
191
- style: {
192
- display: "inline-block",
193
- padding: "10px 18px",
194
- fontFamily: t.font,
195
- fontSize: "14px",
196
- fontWeight: 500,
197
- lineHeight: "20px",
198
- color: t.buttonText,
199
- textDecoration: "none",
200
- borderRadius: t.radius
201
- }
202
- }, slots.default?.()))])]);
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
+ };
203
522
  }
204
523
  });
205
524
  /** A one-time code, large and spaced so it reads (and copies) easily. */
206
525
  const EmailCode = defineComponent({
207
526
  name: "EmailCode",
208
- props: { code: {
209
- type: String,
210
- required: true
211
- } },
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
+ },
212
538
  setup(props) {
213
539
  return () => table({
214
540
  width: "100%",
215
541
  style: { margin: "8px 0 24px" }
216
542
  }, [h("tr", [h("td", {
217
543
  align: "center",
544
+ bgcolor: t.codeBackground,
545
+ class: "wv-code",
218
546
  style: {
219
547
  background: t.codeBackground,
220
- borderRadius: t.radius,
221
- padding: "16px",
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: {
222
555
  fontFamily: t.mono,
223
- fontSize: "30px",
556
+ fontSize: "36px",
557
+ lineHeight: "44px",
224
558
  fontWeight: 600,
225
- letterSpacing: "8px",
559
+ letterSpacing: "10px",
560
+ paddingLeft: "10px",
226
561
  color: t.highlighted
227
562
  }
228
- }, props.code)])]);
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])])]);
229
573
  }
230
574
  });
231
575
  const EmailDivider = defineComponent({
232
576
  name: "EmailDivider",
233
577
  setup() {
234
- return () => h("hr", { style: {
235
- border: "none",
236
- borderTop: `1px solid ${t.border}`,
237
- margin: "24px 0"
238
- } });
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
+ });
239
586
  }
240
587
  });
241
588
  function table(attrs, children) {
@@ -29,6 +29,10 @@ function toText(html) {
29
29
  selector: ".wervt-preheader",
30
30
  format: "skip"
31
31
  },
32
+ {
33
+ selector: ".wervt-skip-text",
34
+ format: "skip"
35
+ },
32
36
  {
33
37
  selector: "title",
34
38
  format: "skip"
@@ -4,9 +4,11 @@ import { useDb } from "./db.js";
4
4
  import { createEvlogError, log, useLogger } from "./logger.js";
5
5
  import { SendMailOptions, sendMail } from "./mail.js";
6
6
  import { McpTokenContext } from "./mcp-auth.js";
7
- import { ShapeOptions, proxyShape } from "./shape.js";
7
+ import { seedTable } from "./seed.js";
8
+ import { ShapeResponse } from "../shared/shape.js";
9
+ import { ShapeOptions, ShapeRow, proxyShape } from "./shape.js";
8
10
  import { Task, TaskEvent, TaskResult, defineTask } from "./tasks.js";
9
11
  import { SafeResult, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
10
- import { Tool, defineTool } from "./tools.js";
12
+ import { Tool, ToolRoute, defineTool, defineToolRoute } from "./tools.js";
11
13
  import { useWervtUser } from "./user.js";
12
- export { type EmailProps, type McpTokenContext, type RenderedEmail, type SafeResult, type SendMailOptions, type ShapeOptions, type Task, type TaskEvent, type TaskResult, type Tool, type WervtUser, createEvlogError, defineTask, defineTool, log, proxyShape, renderEmail, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
14
+ export { type EmailProps, type McpTokenContext, type RenderedEmail, type SafeResult, type SendMailOptions, type ShapeOptions, type ShapeResponse, type ShapeRow, type Task, type TaskEvent, type TaskResult, type Tool, type ToolRoute, type WervtUser, createEvlogError, defineTask, defineTool, defineToolRoute, log, proxyShape, renderEmail, seedTable, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };