@react-email/preview-server 4.1.0-canary.9 → 4.1.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 (117) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +32 -32
  3. package/.next/app-path-routes-manifest.json +1 -1
  4. package/.next/build-manifest.json +14 -14
  5. package/.next/diagnostics/framework.json +1 -1
  6. package/.next/next-minimal-server.js.nft.json +1 -1
  7. package/.next/next-server.js.nft.json +1 -1
  8. package/.next/prerender-manifest.json +3 -3
  9. package/.next/required-server-files.json +5 -5
  10. package/.next/server/app/_not-found/page.js +1 -1
  11. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/favicon.ico/route.js +1 -1
  14. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  15. package/.next/server/app/page.js +1 -1
  16. package/.next/server/app/page.js.nft.json +1 -1
  17. package/.next/server/app/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/preview/[...slug]/page.js +356 -174
  19. package/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  20. package/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  21. package/.next/server/app-paths-manifest.json +1 -1
  22. package/.next/server/chunks/277.js +1 -0
  23. package/.next/server/chunks/{879.js → 532.js} +7 -7
  24. package/.next/server/chunks/550.js +6 -0
  25. package/.next/server/chunks/834.js +15 -0
  26. package/.next/server/chunks/851.js +20 -0
  27. package/.next/server/chunks/905.js +1 -0
  28. package/.next/server/middleware-build-manifest.js +1 -1
  29. package/.next/server/next-font-manifest.js +1 -1
  30. package/.next/server/next-font-manifest.json +1 -1
  31. package/.next/server/pages/500.html +1 -1
  32. package/.next/server/pages/_app.js +1 -1
  33. package/.next/server/pages/_app.js.nft.json +1 -1
  34. package/.next/server/pages/_document.js +1 -1
  35. package/.next/server/pages/_document.js.nft.json +1 -1
  36. package/.next/server/pages/_error.js +1 -1
  37. package/.next/server/pages/_error.js.nft.json +1 -1
  38. package/.next/server/server-reference-manifest.js +1 -1
  39. package/.next/server/server-reference-manifest.json +1 -1
  40. package/.next/static/{pT9c18D6E8gNsHAfpxTUA → K0QONxIGIQiCTm2sc3hH-}/_buildManifest.js +1 -1
  41. package/.next/static/chunks/343-0aa226f42148dce4.js +1 -0
  42. package/.next/static/chunks/353-5e7d05b120b2e25f.js +1 -0
  43. package/.next/static/chunks/445-576b0c51a037fe4d.js +1 -0
  44. package/.next/static/chunks/483-03a4518fac962ed2.js +1 -0
  45. package/.next/static/chunks/624-9d1650c8211771cd.js +1 -0
  46. package/.next/static/chunks/755-01456539a6894956.js +1 -0
  47. package/.next/static/chunks/app/_not-found/{page-aee2c5619f94a965.js → page-e9c9ee8e737ce3f8.js} +1 -1
  48. package/.next/static/chunks/app/layout-a5cce4132e5e62fa.js +1 -0
  49. package/.next/static/chunks/app/page-88bd6f22b758e157.js +1 -0
  50. package/.next/static/chunks/app/preview/[...slug]/page-65586b6a48695068.js +1 -0
  51. package/.next/static/chunks/{04c799b8-f2be86d6e75dbf29.js → e8809b48-6a73b3f51ba71e9c.js} +1 -1
  52. package/.next/static/chunks/{f33a14d2-188715a58266ac15.js → f33a14d2-13f6de3d216cf617.js} +1 -1
  53. package/.next/static/chunks/framework-4f208795521d076c.js +1 -0
  54. package/.next/static/chunks/main-0000d0a5ac74fec0.js +1 -0
  55. package/.next/static/chunks/main-app-5d960817b9651318.js +1 -0
  56. package/.next/static/chunks/pages/_app-550e3587fddcaa19.js +1 -0
  57. package/.next/static/chunks/pages/_error-870af0ad63b0e49e.js +1 -0
  58. package/.next/static/chunks/{webpack-c09dcf4c47767be8.js → webpack-fc6f324c4c88f6e3.js} +1 -1
  59. package/.next/static/css/48dcea18d820a298.css +3 -0
  60. package/.next/trace +27 -27
  61. package/.next/types/app/layout.ts +1 -1
  62. package/.next/types/app/page.ts +1 -1
  63. package/.next/types/app/preview/[...slug]/page.ts +1 -1
  64. package/CHANGELOG.md +27 -0
  65. package/package.json +6 -4
  66. package/readme.md +35 -0
  67. package/scripts/dev.mts +47 -0
  68. package/scripts/{fill-caniemail-data.mjs → fill-caniemail-data.mts} +0 -5
  69. package/scripts/seed.mts +21 -0
  70. package/scripts/utils/default-seed/auth/account-confirmation.tsx +68 -0
  71. package/scripts/utils/default-seed/auth/forgot-password.tsx +71 -0
  72. package/scripts/utils/default-seed/communications/payment-overdue.tsx +82 -0
  73. package/scripts/utils/default-seed/communications/team-invite.tsx +78 -0
  74. package/scripts/utils/default-seed/communications/webhooks-failed.tsx +89 -0
  75. package/scripts/utils/default-seed/feedback-request.tsx +78 -0
  76. package/scripts/utils/default-seed/marketing/changelog.tsx +98 -0
  77. package/src/actions/email-validation/__snapshots__/check-images.spec.tsx.snap +84 -0
  78. package/src/actions/email-validation/check-images.spec.tsx +1 -80
  79. package/src/app/layout.tsx +2 -2
  80. package/src/app/preview/[...slug]/preview.tsx +1 -1
  81. package/src/components/code-container.tsx +3 -1
  82. package/src/components/code.tsx +60 -59
  83. package/src/components/logo.tsx +68 -63
  84. package/src/components/send.tsx +8 -5
  85. package/src/components/shell.tsx +4 -1
  86. package/src/components/sidebar/file-tree-directory-children.tsx +4 -1
  87. package/src/components/toolbar.tsx +9 -5
  88. package/src/contexts/emails.tsx +2 -2
  89. package/src/contexts/preview.tsx +2 -2
  90. package/src/hooks/use-email-rendering-result.ts +2 -2
  91. package/src/utils/__snapshots__/get-email-component.spec.ts.snap +183 -1
  92. package/src/utils/caniemail/ast/get-used-style-properties.ts +1 -0
  93. package/src/utils/contains-email-template.spec.ts +12 -0
  94. package/src/utils/contains-email-template.ts +26 -9
  95. package/src/utils/get-email-component.spec.ts +3 -0
  96. package/.next/server/chunks/200.js +0 -1
  97. package/.next/server/chunks/3.js +0 -15
  98. package/.next/server/chunks/784.js +0 -6
  99. package/.next/server/chunks/823.js +0 -1
  100. package/.next/server/chunks/915.js +0 -20
  101. package/.next/static/chunks/188-e278e5f1e9c4067e.js +0 -1
  102. package/.next/static/chunks/336-0767fdc0ff1ceaa1.js +0 -1
  103. package/.next/static/chunks/551-d9ce0a77591e6bf7.js +0 -1
  104. package/.next/static/chunks/651-337b0388fdb8c85c.js +0 -1
  105. package/.next/static/chunks/713-6c7607cb54b8278e.js +0 -1
  106. package/.next/static/chunks/834-e77d5196a2631efe.js +0 -1
  107. package/.next/static/chunks/app/layout-4403d8068fbec772.js +0 -1
  108. package/.next/static/chunks/app/page-ddc5b5b5b8d2de0e.js +0 -1
  109. package/.next/static/chunks/app/preview/[...slug]/page-873416b81678fe6b.js +0 -1
  110. package/.next/static/chunks/framework-f71c687711cb40cc.js +0 -1
  111. package/.next/static/chunks/main-697e2f65b52ad05c.js +0 -1
  112. package/.next/static/chunks/main-app-deb9839bdc2bcbd4.js +0 -1
  113. package/.next/static/chunks/pages/_app-2e335329c12ac3e7.js +0 -1
  114. package/.next/static/chunks/pages/_error-73094c2ebe868f44.js +0 -1
  115. package/.next/static/css/692fd92effde156a.css +0 -3
  116. /package/.next/static/{pT9c18D6E8gNsHAfpxTUA → K0QONxIGIQiCTm2sc3hH-}/_ssgManifest.js +0 -0
  117. /package/scripts/{build-preview-server.mjs → build-preview-server.mts} +0 -0
@@ -0,0 +1,98 @@
1
+ import {
2
+ Body,
3
+ Container,
4
+ Head,
5
+ Heading,
6
+ Hr,
7
+ Html,
8
+ Preview,
9
+ Section,
10
+ Tailwind,
11
+ Text,
12
+ } from '@react-email/components';
13
+
14
+ interface ChangelogProps {
15
+ receiver: string;
16
+ changes: Partial<Record<'features' | 'fixes' | 'improvements', string[]>>;
17
+ date: string;
18
+ }
19
+
20
+ interface ChangesProps {
21
+ title: React.ReactNode;
22
+ content: string[];
23
+ }
24
+
25
+ function Changes({ title, content }: ChangesProps) {
26
+ return (
27
+ <Section>
28
+ <Heading as="h2" className="my-[12px]">
29
+ {title}
30
+ </Heading>
31
+ <ul className="list-disc pl-6">
32
+ {content.map((feature, index) => (
33
+ <li key={index} className="mb-2">
34
+ {feature}
35
+ </li>
36
+ ))}
37
+ </ul>
38
+ </Section>
39
+ );
40
+ }
41
+
42
+ export default function Changelog({ receiver, date, changes }: ChangelogProps) {
43
+ return (
44
+ <Html>
45
+ <Head />
46
+ <Tailwind>
47
+ <Body className="bg-black text-white">
48
+ <Preview>
49
+ The changes as of {date} particularly tailored to you
50
+ </Preview>
51
+ <Container className="mx-auto">
52
+ <Heading className="font-bold text-center my-[48px] text-[32px]">
53
+ Hello {receiver},
54
+ </Heading>
55
+ <Text>
56
+ We've been hard at work making improvements to our service, and we
57
+ wanted to share the latest changes as of{' '}
58
+ <span className="font-bold">{date}</span> with you.
59
+ </Text>
60
+ {changes.features && changes.features.length > 0 ? (
61
+ <Changes title="Features" content={changes.features} />
62
+ ) : null}
63
+ {changes.improvements && changes.improvements.length > 0 ? (
64
+ <Changes title="Improvements" content={changes.improvements} />
65
+ ) : null}
66
+ {changes.fixes && changes.fixes.length > 0 ? (
67
+ <Changes title="Fixes" content={changes.fixes} />
68
+ ) : null}
69
+ <Text className="mt-6">- React Email team</Text>
70
+ <Hr style={{ borderTopColor: '#404040' }} />
71
+ <Text className="text-[#606060] font-bold">
72
+ React Email, 999 React St, Email City, EC 12345
73
+ </Text>
74
+ </Container>
75
+ </Body>
76
+ </Tailwind>
77
+ </Html>
78
+ );
79
+ }
80
+
81
+ Changelog.PreviewProps = {
82
+ receiver: 'user',
83
+ changes: {
84
+ features: [
85
+ 'Added a new feature to enhance user experience',
86
+ 'Introduced a dark mode option for better accessibility',
87
+ ],
88
+ fixes: [
89
+ 'Fixed a bug that caused the app to crash on startup',
90
+ 'Resolved an issue with email notifications not being sent',
91
+ ],
92
+ improvements: [
93
+ 'Improved performance of the application by optimizing database queries',
94
+ 'Enhanced security measures to protect user data',
95
+ ],
96
+ },
97
+ date: 'June 4th, 2025',
98
+ } satisfies ChangelogProps;
@@ -0,0 +1,84 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`checkImages() 1`] = `
4
+ [
5
+ {
6
+ "checks": [
7
+ {
8
+ "metadata": {
9
+ "alt": undefined,
10
+ },
11
+ "passed": false,
12
+ "type": "accessibility",
13
+ },
14
+ {
15
+ "passed": true,
16
+ "type": "syntax",
17
+ },
18
+ {
19
+ "passed": true,
20
+ "type": "security",
21
+ },
22
+ {
23
+ "metadata": {
24
+ "fetchStatusCode": 200,
25
+ },
26
+ "passed": true,
27
+ "type": "fetch_attempt",
28
+ },
29
+ {
30
+ "metadata": {
31
+ "byteCount": 26808,
32
+ },
33
+ "passed": true,
34
+ "type": "image_size",
35
+ },
36
+ ],
37
+ "codeLocation": {
38
+ "column": 3,
39
+ "line": 2,
40
+ },
41
+ "source": "https://resend.com/static/brand/resend-icon-white.png",
42
+ "status": "warning",
43
+ },
44
+ {
45
+ "checks": [
46
+ {
47
+ "metadata": {
48
+ "alt": "codepen challenges",
49
+ },
50
+ "passed": true,
51
+ "type": "accessibility",
52
+ },
53
+ {
54
+ "passed": true,
55
+ "type": "syntax",
56
+ },
57
+ {
58
+ "passed": true,
59
+ "type": "security",
60
+ },
61
+ {
62
+ "metadata": {
63
+ "fetchStatusCode": 200,
64
+ },
65
+ "passed": true,
66
+ "type": "fetch_attempt",
67
+ },
68
+ {
69
+ "metadata": {
70
+ "byteCount": 111922,
71
+ },
72
+ "passed": true,
73
+ "type": "image_size",
74
+ },
75
+ ],
76
+ "codeLocation": {
77
+ "column": 3,
78
+ "line": 3,
79
+ },
80
+ "source": "/static/codepen-challengers.png",
81
+ "status": "success",
82
+ },
83
+ ]
84
+ `;
@@ -17,84 +17,5 @@ test('checkImages()', async () => {
17
17
  break;
18
18
  }
19
19
  }
20
- expect(results).toEqual([
21
- {
22
- source: 'https://resend.com/static/brand/resend-icon-white.png',
23
- codeLocation: {
24
- line: 2,
25
- column: 3,
26
- },
27
- checks: [
28
- {
29
- passed: false,
30
- type: 'accessibility',
31
- metadata: {
32
- alt: undefined,
33
- },
34
- },
35
- {
36
- passed: true,
37
- type: 'syntax',
38
- },
39
- {
40
- passed: true,
41
- type: 'security',
42
- },
43
- {
44
- passed: true,
45
- type: 'fetch_attempt',
46
- metadata: {
47
- fetchStatusCode: 200,
48
- },
49
- },
50
- {
51
- passed: true,
52
- type: 'image_size',
53
- metadata: {
54
- byteCount: 23_138,
55
- },
56
- },
57
- ],
58
- status: 'warning',
59
- },
60
- {
61
- codeLocation: {
62
- line: 3,
63
- column: 3,
64
- },
65
- checks: [
66
- {
67
- metadata: {
68
- alt: 'codepen challenges',
69
- },
70
- passed: true,
71
- type: 'accessibility',
72
- },
73
- {
74
- passed: true,
75
- type: 'syntax',
76
- },
77
- {
78
- passed: true,
79
- type: 'security',
80
- },
81
- {
82
- metadata: {
83
- fetchStatusCode: 200,
84
- },
85
- passed: true,
86
- type: 'fetch_attempt',
87
- },
88
- {
89
- metadata: {
90
- byteCount: 111_922,
91
- },
92
- passed: true,
93
- type: 'image_size',
94
- },
95
- ],
96
- source: '/static/codepen-challengers.png',
97
- status: 'success',
98
- },
99
- ] satisfies ImageCheckingResult[]);
20
+ expect(results).toMatchSnapshot();
100
21
  });
@@ -27,8 +27,8 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
27
27
  className={`${inter.variable} ${sfMono.variable} font-sans`}
28
28
  lang="en"
29
29
  >
30
- <body className="relative flex h-screen flex-col bg-black text-slate-11 leading-loose selection:bg-cyan-5 selection:text-cyan-12">
31
- <div className="bg-gradient-to-t from-slate-3 flex">
30
+ <body className="relative h-screen bg-black text-slate-11 leading-loose selection:bg-cyan-5 selection:text-cyan-12">
31
+ <div className="bg-gradient-to-t from-slate-3 flex flex-col">
32
32
  <EmailsProvider
33
33
  initialEmailsDirectoryMetadata={emailsDirectoryMetadata}
34
34
  >
@@ -162,7 +162,7 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
162
162
  width={width}
163
163
  >
164
164
  <iframe
165
- className="solid max-h-full rounded-lg bg-white"
165
+ className="max-h-full rounded-lg bg-white [color-scheme:auto]"
166
166
  ref={(iframe) => {
167
167
  if (iframe) {
168
168
  return makeIframeDocumentBubbleEvents(iframe);
@@ -37,6 +37,8 @@ export const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({
37
37
  });
38
38
  }
39
39
 
40
+ const codeId = React.useId();
41
+
40
42
  return (
41
43
  <div
42
44
  className="relative max-h-[650px] w-full h-full whitespace-pre rounded-md border border-slate-6 text-sm"
@@ -49,7 +51,7 @@ export const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({
49
51
  >
50
52
  <div className="h-9 border-b border-slate-6">
51
53
  <div className="flex">
52
- <LayoutGroup id="code">
54
+ <LayoutGroup id={codeId}>
53
55
  {markups.map(({ language }) => {
54
56
  const isCurrentLang = activeLang === language;
55
57
  return (
@@ -105,78 +105,79 @@ export const Code: React.FC<Readonly<CodeProps>> = ({
105
105
  />
106
106
  <div
107
107
  ref={scrollerRef}
108
- className="flex max-h-[650px] h-full p-4 after:w-full after:static after:block after:h-4 after:content-[''] overflow-auto"
108
+ className="max-h-[650px] h-full p-4 after:w-full after:static after:block after:h-4 after:content-[''] overflow-auto"
109
109
  >
110
- <div className="text-[#49494f] text-[13px] font-light font-[MonoLisa,_Menlo,_monospace]">
111
- {tokens.map((_, i) => (
112
- <Link
113
- id={`L${i + 1}`}
114
- key={i}
115
- href={{
116
- hash: `#L${i + 1}`,
117
- search: searchParams.toString(),
118
- }}
119
- scroll={false}
120
- className={cn(
121
- 'align-middle block scroll-mt-[325px] rounded-l-sm select-none pr-3 cursor-pointer hover:text-slate-12',
122
- isHighlighting(i + 1) &&
123
- 'text-cyan-11 hover:text-cyan-11 bg-cyan-5',
124
- )}
125
- type="button"
126
- >
127
- {i + 1}
128
- </Link>
129
- ))}
130
- </div>
131
- <pre>
110
+ <div className="grid grid-cols-[auto_1fr] w-full">
132
111
  {tokens.map((line, i) => {
133
112
  const lineProps = getLineProps({
134
113
  line,
135
114
  key: i,
136
115
  });
116
+ const isHighlighted = isHighlighting(i + 1);
117
+ const isHighlightStart = highlight && highlight[0] === i + 1;
118
+ const isHighlightEnd = highlight && highlight[1] === i + 1;
119
+
137
120
  return (
138
- <div
139
- {...lineProps}
140
- className={cn(
141
- 'whitespace-pre flex transition-colors rounded-r-sm',
142
- isHighlighting(i + 1) && 'bg-cyan-5',
143
- {
121
+ <Fragment key={i}>
122
+ {/* Line number cell */}
123
+ <Link
124
+ id={`L${i + 1}`}
125
+ href={{
126
+ hash: `#L${i + 1}`,
127
+ search: searchParams.toString(),
128
+ }}
129
+ scroll={false}
130
+ aria-selected={isHighlighted}
131
+ className={cn(
132
+ 'text-[#49494f] relative text-[13px] font-light font-[MonoLisa,_Menlo,_monospace] align-middle scroll-mt-[325px] select-none pr-3 cursor-pointer hover:text-slate-12 transition-colors',
133
+ 'aria-selected:text-cyan-11 aria-selected:hover:text-cyan-11 aria-selected:bg-cyan-5 [&+*]:aria-selected:bg-cyan-5',
134
+ isHighlightStart && 'rounded-tl-sm',
135
+ isHighlightEnd && 'rounded-bl-sm',
136
+ )}
137
+ type="button"
138
+ >
139
+ {i + 1}
140
+ </Link>
141
+
142
+ {/* Code content cell */}
143
+ <div
144
+ {...lineProps}
145
+ className={cn('whitespace-pre transition-colors', {
144
146
  "before:mr-2 before:text-slate-11 before:content-['$']":
145
147
  language === 'bash' && tokens.length === 1,
146
- },
147
- )}
148
- key={i}
149
- >
150
- {line.map((token, key) => {
151
- const tokenProps = getTokenProps({
152
- token,
153
- });
154
- const isException =
155
- token.content === 'from' &&
156
- line[key + 1]?.content === ':';
157
- const newTypes = isException
158
- ? [...token.types, 'key-white']
159
- : token.types;
160
- token.types = newTypes;
148
+ })}
149
+ >
150
+ {line.map((token, key) => {
151
+ const tokenProps = getTokenProps({
152
+ token,
153
+ });
154
+ const isException =
155
+ token.content === 'from' &&
156
+ line[key + 1]?.content === ':';
157
+ const newTypes = isException
158
+ ? [...token.types, 'key-white']
159
+ : token.types;
160
+ token.types = newTypes;
161
161
 
162
- return (
163
- <Fragment key={key}>
164
- <span {...tokenProps} />
165
- </Fragment>
166
- );
167
- })}
168
- </div>
162
+ return (
163
+ <Fragment key={key}>
164
+ <span {...tokenProps} />
165
+ </Fragment>
166
+ );
167
+ })}
168
+ </div>
169
+ </Fragment>
169
170
  );
170
171
  })}
171
- </pre>
172
- <div
173
- className="absolute bottom-0 left-0 h-px w-[200px]"
174
- style={{
175
- background:
176
- 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',
177
- }}
178
- />
172
+ </div>
179
173
  </div>
174
+ <div
175
+ className="absolute bottom-0 left-0 h-px w-[200px]"
176
+ style={{
177
+ background:
178
+ 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',
179
+ }}
180
+ />
180
181
  </>
181
182
  )}
182
183
  </Highlight>