@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
@@ -0,0 +1,260 @@
1
+ import * as React from "react";
2
+ import { Img, Section } from "@react-email/components";
3
+ import { colors, borderRadius, spacing } from "./tokens";
4
+
5
+ // ============================================
6
+ // PrimaryButton - Main CTA button
7
+ // ============================================
8
+ interface PrimaryButtonProps {
9
+ href: string;
10
+ children: React.ReactNode;
11
+ fullWidth?: boolean;
12
+ }
13
+
14
+ export const PrimaryButton = ({
15
+ href,
16
+ children,
17
+ fullWidth = false,
18
+ }: PrimaryButtonProps) => {
19
+ return (
20
+ <Section style={{ margin: `${spacing.lg} 0`, textAlign: "left" as const }}>
21
+ <a
22
+ href={href}
23
+ style={{
24
+ backgroundColor: colors.primary,
25
+ borderRadius: borderRadius.md,
26
+ color: "#ffffff",
27
+ fontSize: "16px",
28
+ fontWeight: "bold",
29
+ textDecoration: "none",
30
+ padding: "12px 32px",
31
+ display: fullWidth ? "block" : "inline-block",
32
+ textAlign: "center" as const,
33
+ }}
34
+ >
35
+ {children}
36
+ </a>
37
+ </Section>
38
+ );
39
+ };
40
+
41
+ // ============================================
42
+ // SecondaryButton - Secondary action button
43
+ // ============================================
44
+ interface SecondaryButtonProps {
45
+ href: string;
46
+ children: React.ReactNode;
47
+ fullWidth?: boolean;
48
+ }
49
+
50
+ export const SecondaryButton = ({
51
+ href,
52
+ children,
53
+ fullWidth = false,
54
+ }: SecondaryButtonProps) => {
55
+ return (
56
+ <Section style={{ margin: `${spacing.md} 0`, textAlign: "left" as const }}>
57
+ <a
58
+ href={href}
59
+ style={{
60
+ backgroundColor: colors.surface,
61
+ border: `1px solid ${colors.borderStrong}`,
62
+ borderRadius: borderRadius.md,
63
+ color: colors.textPrimary,
64
+ fontSize: "16px",
65
+ fontWeight: "600",
66
+ textDecoration: "none",
67
+ padding: "12px 32px",
68
+ display: fullWidth ? "block" : "inline-block",
69
+ textAlign: "center" as const,
70
+ }}
71
+ >
72
+ {children}
73
+ </a>
74
+ </Section>
75
+ );
76
+ };
77
+
78
+ // ============================================
79
+ // SocialButtons - Discord and YouTube icon buttons
80
+ // ============================================
81
+ export const SocialButtons = () => {
82
+ return (
83
+ <Section style={{ margin: `${spacing.sm} 0`, textAlign: "center" as const }}>
84
+ <table cellPadding="0" cellSpacing="0" style={{ margin: "0 auto" }}>
85
+ <tr>
86
+ {/* Discord */}
87
+ <td style={{ paddingRight: "12px" }}>
88
+ <a
89
+ href="https://www.discord.gg/trackedgg"
90
+ style={{
91
+ display: "inline-block",
92
+ textDecoration: "none",
93
+ }}
94
+ >
95
+ <svg
96
+ width="24"
97
+ height="24"
98
+ viewBox="0 0 127.14 96.36"
99
+ >
100
+ <path
101
+ fill={colors.textSecondary}
102
+ d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"
103
+ />
104
+ </svg>
105
+ </a>
106
+ </td>
107
+ {/* YouTube */}
108
+ <td style={{ paddingRight: "12px" }}>
109
+ <a
110
+ href="https://www.youtube.com/@Keenanrmalloy"
111
+ style={{
112
+ display: "inline-block",
113
+ textDecoration: "none",
114
+ }}
115
+ >
116
+ <svg
117
+ width="24"
118
+ height="24"
119
+ viewBox="0 0 24 24"
120
+ >
121
+ <path
122
+ fill={colors.textSecondary}
123
+ d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
124
+ />
125
+ </svg>
126
+ </a>
127
+ </td>
128
+ {/* TikTok */}
129
+ <td style={{ paddingRight: "12px" }}>
130
+ <a
131
+ href="https://www.tiktok.com/@keenanrmalloy"
132
+ style={{
133
+ display: "inline-block",
134
+ textDecoration: "none",
135
+ }}
136
+ >
137
+ <svg
138
+ width="24"
139
+ height="24"
140
+ viewBox="0 0 24 24"
141
+ >
142
+ <path
143
+ fill={colors.textSecondary}
144
+ d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1-.1z"
145
+ />
146
+ </svg>
147
+ </a>
148
+ </td>
149
+ {/* Instagram */}
150
+ <td style={{ paddingRight: "12px" }}>
151
+ <a
152
+ href="https://www.instagram.com/keenanrmalloy/"
153
+ style={{
154
+ display: "inline-block",
155
+ textDecoration: "none",
156
+ }}
157
+ >
158
+ <svg
159
+ width="24"
160
+ height="24"
161
+ viewBox="0 0 24 24"
162
+ >
163
+ <path
164
+ fill={colors.textSecondary}
165
+ d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"
166
+ />
167
+ </svg>
168
+ </a>
169
+ </td>
170
+ {/* Reddit */}
171
+ <td>
172
+ <a
173
+ href="https://www.reddit.com/r/trackedapp/"
174
+ style={{
175
+ display: "inline-block",
176
+ textDecoration: "none",
177
+ }}
178
+ >
179
+ <svg
180
+ width="24"
181
+ height="24"
182
+ viewBox="0 0 24 24"
183
+ >
184
+ <path
185
+ fill={colors.textSecondary}
186
+ d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"
187
+ />
188
+ </svg>
189
+ </a>
190
+ </td>
191
+ </tr>
192
+ </table>
193
+ </Section>
194
+ );
195
+ };
196
+
197
+ // Keep DiscordButton for backward compatibility
198
+ export const DiscordButton = SocialButtons;
199
+
200
+ // ============================================
201
+ // AppStoreButtons - iOS/Android download buttons
202
+ // ============================================
203
+ export const AppStoreButtons = () => {
204
+ return (
205
+ <Section style={{ marginTop: spacing.lg, textAlign: "center" as const }}>
206
+ <table cellPadding="0" cellSpacing="0" style={{ margin: "0 auto" }}>
207
+ <tr>
208
+ <td style={{ paddingRight: "8px" }}>
209
+ <a
210
+ href="https://apps.apple.com/app/tracked-training/id6450913418"
211
+ style={{ display: "block", textDecoration: "none" }}
212
+ >
213
+ <Img
214
+ src="https://cdn.trckd.ca/assets/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg"
215
+ alt="Download on the App Store"
216
+ height="36"
217
+ style={{ display: "block" }}
218
+ />
219
+ </a>
220
+ </td>
221
+ <td style={{ paddingLeft: "8px" }}>
222
+ <a
223
+ href="https://play.google.com/store/apps/details?id=com.tracked.mobile"
224
+ style={{ display: "block", textDecoration: "none" }}
225
+ >
226
+ <Img
227
+ src="https://cdn.trckd.ca/assets/GetItOnGooglePlay_Badge_Web_color_English.svg"
228
+ alt="Get it on Google Play"
229
+ height="36"
230
+ style={{ display: "block" }}
231
+ />
232
+ </a>
233
+ </td>
234
+ </tr>
235
+ </table>
236
+ </Section>
237
+ );
238
+ };
239
+
240
+ // ============================================
241
+ // TextLink - Inline text link
242
+ // ============================================
243
+ interface TextLinkProps {
244
+ href: string;
245
+ children: React.ReactNode;
246
+ }
247
+
248
+ export const TextLink = ({ href, children }: TextLinkProps) => {
249
+ return (
250
+ <a
251
+ href={href}
252
+ style={{
253
+ color: colors.accent,
254
+ textDecoration: "underline",
255
+ }}
256
+ >
257
+ {children}
258
+ </a>
259
+ );
260
+ };
@@ -0,0 +1,217 @@
1
+ import * as React from "react";
2
+ import {
3
+ Body,
4
+ Container,
5
+ Head,
6
+ Hr,
7
+ Html,
8
+ Img,
9
+ Link,
10
+ Preview,
11
+ Section,
12
+ Text,
13
+ } from "@react-email/components";
14
+ import { colors, typography, spacing } from "./tokens";
15
+ import { AppStoreButtons } from "./interactive";
16
+
17
+ const baseUrl = "https://tracked.gg/android-chrome-192x192.png";
18
+ const defaultWebsiteUrl = "https://tracked.gg";
19
+
20
+ // ============================================
21
+ // EmailLayout - Main wrapper component
22
+ // ============================================
23
+ interface EmailLayoutProps {
24
+ preview: string;
25
+ children: React.ReactNode;
26
+ }
27
+
28
+ export const EmailLayout = ({ preview, children }: EmailLayoutProps) => {
29
+ return (
30
+ <Html>
31
+ <Head>
32
+ <meta name="color-scheme" content="light only" />
33
+ <meta name="supported-color-schemes" content="light only" />
34
+ </Head>
35
+ <Preview>{preview}</Preview>
36
+ <Body style={mainStyle}>
37
+ <Container style={containerStyle}>
38
+ <Section style={boxStyle}>{children}</Section>
39
+ </Container>
40
+ </Body>
41
+ </Html>
42
+ );
43
+ };
44
+
45
+ const mainStyle = {
46
+ backgroundColor: colors.background,
47
+ fontFamily: typography.fontFamily,
48
+ };
49
+
50
+ const containerStyle = {
51
+ backgroundColor: colors.background,
52
+ margin: "0 auto",
53
+ padding: "20px 0 48px",
54
+ maxWidth: "600px",
55
+ };
56
+
57
+ const boxStyle = {
58
+ padding: "0 24px",
59
+ };
60
+
61
+ // ============================================
62
+ // EmailHeader - Logo and brand name
63
+ // ============================================
64
+ interface EmailHeaderProps {
65
+ showDivider?: boolean;
66
+ }
67
+
68
+ export const EmailHeader = ({ showDivider = true }: EmailHeaderProps) => {
69
+ return (
70
+ <>
71
+ <table cellPadding="0" cellSpacing="0" style={{ marginBottom: spacing.sm }}>
72
+ <tr>
73
+ <td style={{ verticalAlign: "middle" }}>
74
+ <Img src={baseUrl} width="28" height="28" alt="Tracked" />
75
+ </td>
76
+ <td style={{ verticalAlign: "middle", paddingLeft: "6px" }}>
77
+ <Text style={logoStyle}>TRACKED</Text>
78
+ </td>
79
+ </tr>
80
+ </table>
81
+ {showDivider && <Hr style={headerDividerStyle} />}
82
+ </>
83
+ );
84
+ };
85
+
86
+ const logoStyle = {
87
+ fontSize: "28px",
88
+ fontWeight: "900" as const,
89
+ fontFamily: typography.brandFont,
90
+ color: colors.textPrimary,
91
+ margin: "0",
92
+ lineHeight: "32px",
93
+ letterSpacing: "-0.5px",
94
+ };
95
+
96
+ const headerDividerStyle = {
97
+ borderColor: colors.border,
98
+ margin: `${spacing.lg} 0`,
99
+ borderWidth: "1px",
100
+ };
101
+
102
+ // ============================================
103
+ // EmailFooter - Copyright and links
104
+ // ============================================
105
+ interface EmailFooterProps {
106
+ websiteUrl?: string;
107
+ marketing?: boolean;
108
+ unsubscribeUrl?: string;
109
+ }
110
+
111
+ export const EmailFooter = ({
112
+ websiteUrl = defaultWebsiteUrl,
113
+ marketing = false,
114
+ unsubscribeUrl,
115
+ }: EmailFooterProps) => {
116
+ return (
117
+ <>
118
+ <Hr style={footerDividerStyle} />
119
+ <AppStoreButtons />
120
+ <Text style={footerTextStyle}>
121
+ Copyright © {new Date().getFullYear()} Tracked Training Platform Inc.{" "}
122
+ <br />
123
+ 9101 Horne Street, Vancouver, BC
124
+ </Text>
125
+ <Section style={{ textAlign: "center" as const }}>
126
+ <Link href={`${websiteUrl}/terms`} style={footerLinkStyle}>
127
+ Terms
128
+ </Link>
129
+ <Text style={footerDividerTextStyle}> | </Text>
130
+ <Link href={`${websiteUrl}/privacy`} style={footerLinkStyle}>
131
+ Privacy
132
+ </Link>
133
+ <Text style={footerDividerTextStyle}> | </Text>
134
+ <Link href={`${websiteUrl}/support`} style={footerLinkStyle}>
135
+ Support
136
+ </Link>
137
+ {marketing && unsubscribeUrl && (
138
+ <>
139
+ <Text style={footerDividerTextStyle}> | </Text>
140
+ <Link href={unsubscribeUrl} style={footerLinkStyle}>
141
+ Unsubscribe
142
+ </Link>
143
+ </>
144
+ )}
145
+ </Section>
146
+ <Text style={footerDisclaimerStyle}>
147
+ {marketing
148
+ ? "You're receiving this email because you opted in to marketing communications from Tracked."
149
+ : "This is a service notification by the Tracked Training Platform."}
150
+ </Text>
151
+ </>
152
+ );
153
+ };
154
+
155
+ const footerDividerStyle = {
156
+ borderColor: colors.border,
157
+ margin: `${spacing.lg} 0`,
158
+ borderWidth: "1px",
159
+ };
160
+
161
+ const footerTextStyle = {
162
+ color: colors.textMuted,
163
+ fontSize: "12px",
164
+ lineHeight: "16px",
165
+ textAlign: "center" as const,
166
+ };
167
+
168
+ const footerLinkStyle = {
169
+ color: colors.textMuted,
170
+ fontSize: "12px",
171
+ textDecoration: "none",
172
+ };
173
+
174
+ const footerDividerTextStyle = {
175
+ color: colors.textMuted,
176
+ fontSize: "12px",
177
+ display: "inline" as const,
178
+ };
179
+
180
+ const footerDisclaimerStyle = {
181
+ color: colors.textMuted,
182
+ fontSize: "12px",
183
+ lineHeight: "16px",
184
+ textAlign: "center" as const,
185
+ marginTop: spacing.md,
186
+ };
187
+
188
+ // ============================================
189
+ // ContentSection - Padded content wrapper
190
+ // ============================================
191
+ interface ContentSectionProps {
192
+ children: React.ReactNode;
193
+ style?: React.CSSProperties;
194
+ }
195
+
196
+ export const ContentSection = ({ children, style }: ContentSectionProps) => {
197
+ return <Section style={{ marginBottom: spacing.lg, ...style }}>{children}</Section>;
198
+ };
199
+
200
+ // ============================================
201
+ // Divider - Horizontal rule
202
+ // ============================================
203
+ interface DividerProps {
204
+ accent?: boolean;
205
+ }
206
+
207
+ export const Divider = ({ accent = false }: DividerProps) => {
208
+ return (
209
+ <Hr
210
+ style={{
211
+ borderColor: accent ? colors.borderAccent : colors.border,
212
+ margin: `${spacing.lg} 0`,
213
+ borderWidth: "1px",
214
+ }}
215
+ />
216
+ );
217
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Design tokens for email templates
3
+ * Light/professional theme
4
+ */
5
+
6
+ export const colors = {
7
+ // Backgrounds
8
+ background: "#ffffff",
9
+ surface: "#f8fafc", // slate-50
10
+ surfaceAlt: "#f1f5f9", // slate-100
11
+
12
+ // Text
13
+ textPrimary: "#0f172a", // slate-900
14
+ textSecondary: "#475569", // slate-600
15
+ textMuted: "#94a3b8", // slate-400
16
+
17
+ // Brand
18
+ primary: "#0f172a", // dark navy for buttons
19
+ accent: "#10b981", // emerald-500 for highlights
20
+ discord: "#5865F2",
21
+ youtube: "#FF0000",
22
+ tiktok: "#000000",
23
+ instagram: "#E4405F",
24
+
25
+ // Borders
26
+ border: "#e2e8f0", // slate-200
27
+ borderStrong: "#cbd5e1", // slate-300
28
+ borderAccent: "#10b981", // emerald-500
29
+
30
+ // Status
31
+ success: "#10b981", // emerald-500
32
+ error: "#ef4444", // red-500
33
+ warning: "#f59e0b", // amber-500
34
+ };
35
+
36
+ export const typography = {
37
+ fontFamily:
38
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
39
+ brandFont:
40
+ 'Raleway, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
41
+ };
42
+
43
+ export const spacing = {
44
+ xs: "4px",
45
+ sm: "8px",
46
+ md: "16px",
47
+ lg: "24px",
48
+ xl: "32px",
49
+ xxl: "48px",
50
+ };
51
+
52
+ export const borderRadius = {
53
+ sm: "4px",
54
+ md: "8px",
55
+ lg: "12px",
56
+ full: "9999px",
57
+ };
58
+
59
+ // Base styles used across components
60
+ export const baseStyles = {
61
+ main: {
62
+ backgroundColor: colors.background,
63
+ fontFamily: typography.fontFamily,
64
+ },
65
+ container: {
66
+ backgroundColor: colors.background,
67
+ margin: "0 auto",
68
+ padding: "20px 0 48px",
69
+ maxWidth: "600px",
70
+ },
71
+ box: {
72
+ padding: "0 24px",
73
+ },
74
+ };
@@ -0,0 +1,148 @@
1
+ import * as React from "react";
2
+ import { Text, Heading as EmailHeading } from "@react-email/components";
3
+ import { colors, spacing } from "./tokens";
4
+
5
+ // ============================================
6
+ // Heading - Title text with variants
7
+ // ============================================
8
+ interface HeadingProps {
9
+ children: React.ReactNode;
10
+ as?: "h1" | "h2" | "h3";
11
+ style?: React.CSSProperties;
12
+ }
13
+
14
+ export const Heading = ({ children, as = "h1", style }: HeadingProps) => {
15
+ const baseStyle = headingStyles[as];
16
+ return (
17
+ <EmailHeading as={as} style={{ ...baseStyle, ...style }}>
18
+ {children}
19
+ </EmailHeading>
20
+ );
21
+ };
22
+
23
+ const headingStyles = {
24
+ h1: {
25
+ color: colors.textPrimary,
26
+ fontSize: "24px",
27
+ lineHeight: "32px",
28
+ fontWeight: "bold" as const,
29
+ marginBottom: spacing.md,
30
+ marginTop: "0",
31
+ },
32
+ h2: {
33
+ color: colors.textPrimary,
34
+ fontSize: "20px",
35
+ lineHeight: "28px",
36
+ fontWeight: "600" as const,
37
+ marginBottom: spacing.sm,
38
+ marginTop: "0",
39
+ },
40
+ h3: {
41
+ color: colors.textPrimary,
42
+ fontSize: "16px",
43
+ lineHeight: "24px",
44
+ fontWeight: "600" as const,
45
+ marginBottom: spacing.sm,
46
+ marginTop: "0",
47
+ },
48
+ };
49
+
50
+ // ============================================
51
+ // Paragraph - Standard body text
52
+ // ============================================
53
+ interface ParagraphProps {
54
+ children: React.ReactNode;
55
+ muted?: boolean;
56
+ style?: React.CSSProperties;
57
+ }
58
+
59
+ export const Paragraph = ({ children, muted = false, style }: ParagraphProps) => {
60
+ return (
61
+ <Text
62
+ style={{
63
+ color: muted ? colors.textSecondary : colors.textPrimary,
64
+ fontSize: "16px",
65
+ lineHeight: "24px",
66
+ textAlign: "left" as const,
67
+ margin: `0 0 ${spacing.md} 0`,
68
+ ...style,
69
+ }}
70
+ >
71
+ {children}
72
+ </Text>
73
+ );
74
+ };
75
+
76
+ // ============================================
77
+ // Label - Small muted text for labels
78
+ // ============================================
79
+ interface LabelProps {
80
+ children: React.ReactNode;
81
+ style?: React.CSSProperties;
82
+ }
83
+
84
+ export const Label = ({ children, style }: LabelProps) => {
85
+ return (
86
+ <Text
87
+ style={{
88
+ color: colors.textMuted,
89
+ fontSize: "14px",
90
+ lineHeight: "20px",
91
+ margin: "0",
92
+ ...style,
93
+ }}
94
+ >
95
+ {children}
96
+ </Text>
97
+ );
98
+ };
99
+
100
+ // ============================================
101
+ // HighlightText - Accent-colored text
102
+ // ============================================
103
+ interface HighlightTextProps {
104
+ children: React.ReactNode;
105
+ style?: React.CSSProperties;
106
+ }
107
+
108
+ export const HighlightText = ({ children, style }: HighlightTextProps) => {
109
+ return (
110
+ <Text
111
+ style={{
112
+ color: colors.accent,
113
+ fontSize: "16px",
114
+ fontWeight: "600" as const,
115
+ lineHeight: "24px",
116
+ margin: "0",
117
+ ...style,
118
+ }}
119
+ >
120
+ {children}
121
+ </Text>
122
+ );
123
+ };
124
+
125
+ // ============================================
126
+ // SmallText - Smaller body text
127
+ // ============================================
128
+ interface SmallTextProps {
129
+ children: React.ReactNode;
130
+ muted?: boolean;
131
+ style?: React.CSSProperties;
132
+ }
133
+
134
+ export const SmallText = ({ children, muted = false, style }: SmallTextProps) => {
135
+ return (
136
+ <Text
137
+ style={{
138
+ color: muted ? colors.textMuted : colors.textSecondary,
139
+ fontSize: "14px",
140
+ lineHeight: "20px",
141
+ margin: "0",
142
+ ...style,
143
+ }}
144
+ >
145
+ {children}
146
+ </Text>
147
+ );
148
+ };