@tracked/emails 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/emails/client-onboarded.d.ts +35 -0
  2. package/dist/emails/client-onboarded.d.ts.map +1 -0
  3. package/dist/emails/client-onboarded.js +152 -0
  4. package/dist/emails/client-onboarded.js.map +1 -0
  5. package/dist/emails/index.d.ts +1 -0
  6. package/dist/emails/index.d.ts.map +1 -1
  7. package/dist/emails/index.js +1 -0
  8. package/dist/emails/index.js.map +1 -1
  9. package/dist/emails/monthly-report.d.ts.map +1 -1
  10. package/dist/emails/monthly-report.js +31 -47
  11. package/dist/emails/monthly-report.js.map +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -2
  15. package/dist/index.js.map +1 -1
  16. package/package.json +20 -20
  17. package/src/components/content.tsx +351 -0
  18. package/src/components/index.ts +44 -0
  19. package/src/components/interactive.tsx +260 -0
  20. package/src/components/layout.tsx +217 -0
  21. package/src/components/tokens.ts +74 -0
  22. package/src/components/typography.tsx +148 -0
  23. package/src/emails/anniversary.tsx +133 -0
  24. package/src/emails/app-review-request.tsx +100 -0
  25. package/src/emails/bodyweight-goal-reached.tsx +202 -350
  26. package/src/emails/client-inactive-alert.tsx +130 -0
  27. package/src/emails/client-onboarded.tsx +272 -0
  28. package/src/emails/coach-invite.tsx +67 -250
  29. package/src/emails/coach-removed-client.tsx +36 -197
  30. package/src/emails/direct-message.tsx +69 -227
  31. package/src/emails/feature-discovery.tsx +82 -266
  32. package/src/emails/first-workout-assigned.tsx +52 -238
  33. package/src/emails/first-workout-completed.tsx +88 -294
  34. package/src/emails/inactive-reengagement.tsx +81 -0
  35. package/src/emails/index.tsx +1 -0
  36. package/src/emails/monthly-report.tsx +195 -525
  37. package/src/emails/new-follower.tsx +60 -238
  38. package/src/emails/nps-survey.tsx +149 -0
  39. package/src/emails/subscription-canceled.tsx +88 -294
  40. package/src/emails/support-email.tsx +33 -67
  41. package/src/emails/team-invite.tsx +47 -240
  42. package/src/emails/team-member-removed-email.tsx +23 -218
  43. package/src/emails/tracked-magic-link-activate.tsx +29 -237
  44. package/src/emails/tracked-magic-link.tsx +31 -251
  45. package/src/emails/week-one-checkin.tsx +108 -329
  46. package/src/emails/weekly-progress-digest.tsx +248 -0
  47. package/src/emails/welcome.tsx +58 -326
  48. package/src/index.ts +19 -2
  49. package/dist/emails/client-accepted-invitation.d.ts +0 -10
  50. package/dist/emails/client-accepted-invitation.d.ts.map +0 -1
  51. package/dist/emails/client-accepted-invitation.js +0 -99
  52. package/dist/emails/client-accepted-invitation.js.map +0 -1
  53. package/src/emails/client-accepted-invitation.tsx +0 -258
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tracked/emails",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Email templates for Tracked Training Platform",
5
5
  "author": "Tracked Training Platform Inc.",
6
6
  "license": "MIT",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "build": "rm -rf ./dist && tsc",
27
- "dev:email": "email dev",
27
+ "dev:email": "email dev --dir src/emails",
28
28
  "export": "email export",
29
29
  "typecheck": "tsc --noEmit",
30
30
  "test": "vitest run",
@@ -37,26 +37,26 @@
37
37
  "clean": "rm -rf dist node_modules .turbo"
38
38
  },
39
39
  "dependencies": {
40
- "@react-email/components": "^0.5.7",
41
- "react": "19.0.0",
42
- "react-dom": "19.0.0",
43
- "react-email": "^4.3.2"
40
+ "@react-email/components": "^1.0.2",
41
+ "react": "^19.2.3",
42
+ "react-dom": "^19.2.3",
43
+ "react-email": "^5.1.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@react-email/preview-server": "latest",
47
- "@types/node": "^22.10.2",
48
- "@types/react": "^18.3.2",
49
- "@types/react-dom": "^18.3.0",
50
- "@typescript-eslint/eslint-plugin": "^8.20.0",
51
- "@typescript-eslint/parser": "^8.20.0",
52
- "@vitest/coverage-v8": "^2.1.8",
53
- "dotenv-cli": "^7.4.2",
54
- "eslint": "^9.17.0",
55
- "eslint-plugin-react": "^7.37.2",
56
- "eslint-plugin-react-hooks": "^5.1.0",
57
- "prettier": "^3.4.2",
58
- "typescript": "^5.8.3",
59
- "vitest": "^2.1.8"
46
+ "@react-email/preview-server": "5.1.0",
47
+ "@types/node": "^25.0.3",
48
+ "@types/react": "^19.2.7",
49
+ "@types/react-dom": "^19.2.3",
50
+ "@typescript-eslint/eslint-plugin": "^8.50.0",
51
+ "@typescript-eslint/parser": "^8.50.0",
52
+ "@vitest/coverage-v8": "^4.0.16",
53
+ "dotenv-cli": "^11.0.0",
54
+ "eslint": "^9.39.2",
55
+ "eslint-plugin-react": "^7.37.5",
56
+ "eslint-plugin-react-hooks": "^7.0.1",
57
+ "prettier": "^3.7.4",
58
+ "typescript": "^5.9.3",
59
+ "vitest": "^4.0.16"
60
60
  },
61
61
  "keywords": [
62
62
  "email",
@@ -0,0 +1,351 @@
1
+ import * as React from "react";
2
+ import { Section, Text, Row, Column } from "@react-email/components";
3
+ import { colors, borderRadius, spacing } from "./tokens";
4
+
5
+ // ============================================
6
+ // FeatureBox - Highlighted feature/info box
7
+ // ============================================
8
+ interface FeatureBoxProps {
9
+ title?: string;
10
+ children: React.ReactNode;
11
+ }
12
+
13
+ export const FeatureBox = ({ title, children }: FeatureBoxProps) => {
14
+ return (
15
+ <Section style={featureBoxStyle}>
16
+ {title && <Text style={featureBoxTitleStyle}>{title}</Text>}
17
+ {children}
18
+ </Section>
19
+ );
20
+ };
21
+
22
+ const featureBoxStyle = {
23
+ backgroundColor: colors.surface,
24
+ padding: `${spacing.md} ${spacing.lg}`,
25
+ borderRadius: borderRadius.md,
26
+ margin: `${spacing.lg} 0`,
27
+ border: `1px solid ${colors.border}`,
28
+ };
29
+
30
+ const featureBoxTitleStyle = {
31
+ color: colors.textPrimary,
32
+ fontSize: "16px",
33
+ fontWeight: "bold" as const,
34
+ marginBottom: spacing.sm,
35
+ marginTop: "0",
36
+ };
37
+
38
+ // ============================================
39
+ // TipBox - Bordered tip/callout box
40
+ // ============================================
41
+ interface TipBoxProps {
42
+ title?: string;
43
+ children: React.ReactNode;
44
+ }
45
+
46
+ export const TipBox = ({ title = "Tip", children }: TipBoxProps) => {
47
+ return (
48
+ <Section style={tipBoxStyle}>
49
+ <Text style={tipBoxTitleStyle}>{title}</Text>
50
+ <Text style={tipBoxTextStyle}>{children}</Text>
51
+ </Section>
52
+ );
53
+ };
54
+
55
+ const tipBoxStyle = {
56
+ backgroundColor: colors.surface,
57
+ padding: `${spacing.md} ${spacing.lg}`,
58
+ borderRadius: borderRadius.md,
59
+ margin: `${spacing.lg} 0`,
60
+ borderLeft: `4px solid ${colors.accent}`,
61
+ };
62
+
63
+ const tipBoxTitleStyle = {
64
+ color: colors.accent,
65
+ fontSize: "14px",
66
+ fontWeight: "bold" as const,
67
+ marginBottom: spacing.xs,
68
+ marginTop: "0",
69
+ };
70
+
71
+ const tipBoxTextStyle = {
72
+ color: colors.textSecondary,
73
+ fontSize: "14px",
74
+ lineHeight: "20px",
75
+ margin: "0",
76
+ };
77
+
78
+ // ============================================
79
+ // MetricCard - Stat display with label/value
80
+ // ============================================
81
+ interface MetricCardProps {
82
+ label: string;
83
+ value: string | number;
84
+ change?: React.ReactNode;
85
+ size?: "normal" | "small";
86
+ }
87
+
88
+ export const MetricCard = ({
89
+ label,
90
+ value,
91
+ change,
92
+ size = "normal",
93
+ }: MetricCardProps) => {
94
+ return (
95
+ <Section style={metricCardStyle}>
96
+ <Text style={metricLabelStyle}>{label}</Text>
97
+ <table>
98
+ <tr>
99
+ <td style={{ verticalAlign: "middle" }}>
100
+ <Text style={size === "small" ? metricValueSmallStyle : metricValueStyle}>
101
+ {value}
102
+ </Text>
103
+ </td>
104
+ {change && (
105
+ <td style={{ verticalAlign: "middle", paddingLeft: spacing.sm }}>
106
+ {change}
107
+ </td>
108
+ )}
109
+ </tr>
110
+ </table>
111
+ </Section>
112
+ );
113
+ };
114
+
115
+ const metricCardStyle = {
116
+ backgroundColor: colors.surface,
117
+ padding: `${spacing.sm} ${spacing.md} ${spacing.md} ${spacing.md}`,
118
+ borderRadius: borderRadius.md,
119
+ border: `1px solid ${colors.border}`,
120
+ };
121
+
122
+ const metricLabelStyle = {
123
+ color: colors.textMuted,
124
+ fontSize: "14px",
125
+ marginBottom: "2px",
126
+ marginTop: "0",
127
+ };
128
+
129
+ const metricValueStyle = {
130
+ color: colors.textPrimary,
131
+ fontSize: "24px",
132
+ fontWeight: "700" as const,
133
+ margin: "0",
134
+ };
135
+
136
+ const metricValueSmallStyle = {
137
+ color: colors.textPrimary,
138
+ fontSize: "20px",
139
+ fontWeight: "700" as const,
140
+ margin: "0",
141
+ };
142
+
143
+ // ============================================
144
+ // ChangeIndicator - Up/down/neutral indicator
145
+ // ============================================
146
+ interface ChangeIndicatorProps {
147
+ value: number | null;
148
+ previousValue: number | null;
149
+ decimals?: number;
150
+ }
151
+
152
+ export const ChangeIndicator = ({
153
+ value,
154
+ previousValue,
155
+ decimals = 1,
156
+ }: ChangeIndicatorProps) => {
157
+ if (value === null || previousValue === null) return null;
158
+
159
+ const diff = value - previousValue;
160
+ const formatted = Math.abs(diff).toFixed(decimals);
161
+
162
+ if (diff > 0) {
163
+ return <Text style={changePositiveStyle}>&#9650; {formatted}</Text>;
164
+ }
165
+ if (diff < 0) {
166
+ return <Text style={changeNegativeStyle}>&#9660; {formatted}</Text>;
167
+ }
168
+ return <Text style={changeNeutralStyle}>—</Text>;
169
+ };
170
+
171
+ const changePositiveStyle = {
172
+ color: colors.success,
173
+ fontSize: "14px",
174
+ margin: "0",
175
+ };
176
+
177
+ const changeNegativeStyle = {
178
+ color: colors.error,
179
+ fontSize: "14px",
180
+ margin: "0",
181
+ };
182
+
183
+ const changeNeutralStyle = {
184
+ color: colors.textMuted,
185
+ fontSize: "14px",
186
+ margin: "0",
187
+ };
188
+
189
+ // ============================================
190
+ // DataRow - Key-value pair row
191
+ // ============================================
192
+ interface DataRowProps {
193
+ label: string;
194
+ value: string | number;
195
+ isLast?: boolean;
196
+ }
197
+
198
+ export const DataRow = ({ label, value, isLast = false }: DataRowProps) => {
199
+ return (
200
+ <Row
201
+ style={{
202
+ padding: spacing.sm,
203
+ borderBottom: isLast ? "none" : `1px solid ${colors.border}`,
204
+ }}
205
+ >
206
+ <Column>
207
+ <Text style={dataRowLabelStyle}>{label}</Text>
208
+ </Column>
209
+ <Column style={{ textAlign: "right" as const }}>
210
+ <Text style={dataRowValueStyle}>{value}</Text>
211
+ </Column>
212
+ </Row>
213
+ );
214
+ };
215
+
216
+ const dataRowLabelStyle = {
217
+ color: colors.textSecondary,
218
+ fontSize: "14px",
219
+ margin: "0",
220
+ };
221
+
222
+ const dataRowValueStyle = {
223
+ color: colors.accent,
224
+ fontSize: "16px",
225
+ fontWeight: "600" as const,
226
+ margin: "0",
227
+ };
228
+
229
+ // ============================================
230
+ // FeatureList - Bulleted feature list
231
+ // ============================================
232
+ interface FeatureListItem {
233
+ title: string;
234
+ description?: string;
235
+ }
236
+
237
+ interface FeatureListProps {
238
+ items: FeatureListItem[];
239
+ }
240
+
241
+ export const FeatureList = ({ items }: FeatureListProps) => {
242
+ return (
243
+ <ul style={featureListStyle}>
244
+ {items.map((item, index) => (
245
+ <li key={index} style={featureListItemStyle}>
246
+ {item.description ? (
247
+ <>
248
+ <strong>{item.title}:</strong> {item.description}
249
+ </>
250
+ ) : (
251
+ item.title
252
+ )}
253
+ </li>
254
+ ))}
255
+ </ul>
256
+ );
257
+ };
258
+
259
+ const featureListStyle = {
260
+ margin: "0",
261
+ paddingLeft: "20px",
262
+ };
263
+
264
+ const featureListItemStyle = {
265
+ color: colors.textSecondary,
266
+ fontSize: "14px",
267
+ lineHeight: "24px",
268
+ marginBottom: spacing.sm,
269
+ };
270
+
271
+ // ============================================
272
+ // ListBox - Container for list items
273
+ // ============================================
274
+ interface ListBoxProps {
275
+ children: React.ReactNode;
276
+ }
277
+
278
+ export const ListBox = ({ children }: ListBoxProps) => {
279
+ return <Section style={listBoxStyle}>{children}</Section>;
280
+ };
281
+
282
+ const listBoxStyle = {
283
+ backgroundColor: colors.surface,
284
+ borderRadius: borderRadius.md,
285
+ overflow: "hidden" as const,
286
+ border: `1px solid ${colors.border}`,
287
+ };
288
+
289
+ // ============================================
290
+ // SectionHeading - Section title
291
+ // ============================================
292
+ interface SectionHeadingProps {
293
+ children: React.ReactNode;
294
+ }
295
+
296
+ export const SectionHeading = ({ children }: SectionHeadingProps) => {
297
+ return <Text style={sectionHeadingStyle}>{children}</Text>;
298
+ };
299
+
300
+ const sectionHeadingStyle = {
301
+ color: colors.textPrimary,
302
+ fontSize: "18px",
303
+ margin: `0 0 ${spacing.sm} 0`,
304
+ fontWeight: "600" as const,
305
+ textTransform: "uppercase" as const,
306
+ letterSpacing: "0.5px",
307
+ };
308
+
309
+ // ============================================
310
+ // HighlightBanner - Accent colored banner
311
+ // ============================================
312
+ interface HighlightBannerProps {
313
+ children: React.ReactNode;
314
+ }
315
+
316
+ export const HighlightBanner = ({ children }: HighlightBannerProps) => {
317
+ return <Section style={highlightBannerStyle}>{children}</Section>;
318
+ };
319
+
320
+ const highlightBannerStyle = {
321
+ backgroundColor: colors.accent,
322
+ padding: `${spacing.sm} ${spacing.lg}`,
323
+ textAlign: "center" as const,
324
+ borderRadius: borderRadius.md,
325
+ margin: `${spacing.lg} 0`,
326
+ };
327
+
328
+ // ============================================
329
+ // Avatar - Circular profile image
330
+ // ============================================
331
+ interface AvatarProps {
332
+ src: string;
333
+ alt: string;
334
+ size?: number;
335
+ }
336
+
337
+ export const Avatar = ({ src, alt, size = 64 }: AvatarProps) => {
338
+ return (
339
+ <img
340
+ src={src}
341
+ alt={alt}
342
+ width={size}
343
+ height={size}
344
+ style={{
345
+ borderRadius: "50%",
346
+ display: "block",
347
+ margin: "0 auto",
348
+ }}
349
+ />
350
+ );
351
+ };
@@ -0,0 +1,44 @@
1
+ // Design Tokens
2
+ export * from "./tokens";
3
+
4
+ // Layout Components
5
+ export {
6
+ EmailLayout,
7
+ EmailHeader,
8
+ EmailFooter,
9
+ ContentSection,
10
+ Divider,
11
+ } from "./layout";
12
+
13
+ // Typography Components
14
+ export {
15
+ Heading,
16
+ Paragraph,
17
+ Label,
18
+ HighlightText,
19
+ SmallText,
20
+ } from "./typography";
21
+
22
+ // Interactive Components
23
+ export {
24
+ PrimaryButton,
25
+ SecondaryButton,
26
+ DiscordButton,
27
+ SocialButtons,
28
+ AppStoreButtons,
29
+ TextLink,
30
+ } from "./interactive";
31
+
32
+ // Content Components
33
+ export {
34
+ FeatureBox,
35
+ TipBox,
36
+ MetricCard,
37
+ ChangeIndicator,
38
+ DataRow,
39
+ FeatureList,
40
+ ListBox,
41
+ SectionHeading,
42
+ HighlightBanner,
43
+ Avatar,
44
+ } from "./content";