react-email 4.0.0-alpha.4 → 4.0.0-alpha.6

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 (189) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cli/index.js +1175 -2658
  3. package/dist/cli/index.mjs +18 -12
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +31 -34
  6. package/dist/preview/.next/app-path-routes-manifest.json +6 -1
  7. package/dist/preview/.next/build-manifest.json +14 -14
  8. package/dist/preview/.next/cache/.rscinfo +1 -1
  9. package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
  10. package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
  11. package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
  12. package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
  13. package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
  14. package/dist/preview/.next/diagnostics/framework.json +1 -1
  15. package/dist/preview/.next/export-marker.json +6 -1
  16. package/dist/preview/.next/images-manifest.json +57 -1
  17. package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
  18. package/dist/preview/.next/next-server.js.nft.json +1 -1
  19. package/dist/preview/.next/prerender-manifest.json +41 -1
  20. package/dist/preview/.next/required-server-files.json +310 -1
  21. package/dist/preview/.next/routes-manifest.json +64 -1
  22. package/dist/preview/.next/server/app/_not-found/page.js +1 -1
  23. package/dist/preview/.next/server/app/_not-found/page.js.nft.json +1 -1
  24. package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  25. package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
  26. package/dist/preview/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  27. package/dist/preview/.next/server/app/page.js +1 -1
  28. package/dist/preview/.next/server/app/page.js.nft.json +1 -1
  29. package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
  30. package/dist/preview/.next/server/app/preview/[...slug]/page.js +47 -10
  31. package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  32. package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  33. package/dist/preview/.next/server/app-paths-manifest.json +1 -1
  34. package/dist/preview/.next/server/chunks/171.js +14 -0
  35. package/dist/preview/.next/server/chunks/446.js +6 -0
  36. package/dist/preview/.next/server/chunks/600.js +8 -0
  37. package/dist/preview/.next/server/chunks/811.js +13 -0
  38. package/dist/preview/.next/server/chunks/833.js +1 -0
  39. package/dist/preview/.next/server/functions-config-manifest.json +4 -1
  40. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  41. package/dist/preview/.next/server/next-font-manifest.js +1 -1
  42. package/dist/preview/.next/server/next-font-manifest.json +1 -1
  43. package/dist/preview/.next/server/pages/500.html +1 -1
  44. package/dist/preview/.next/server/pages/_app.js +1 -1
  45. package/dist/preview/.next/server/pages/_app.js.nft.json +1 -1
  46. package/dist/preview/.next/server/pages/_document.js +1 -1
  47. package/dist/preview/.next/server/pages/_document.js.nft.json +1 -1
  48. package/dist/preview/.next/server/pages/_error.js +1 -1
  49. package/dist/preview/.next/server/pages/_error.js.nft.json +1 -1
  50. package/dist/preview/.next/server/pages-manifest.json +5 -1
  51. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  52. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  53. package/dist/preview/.next/server/webpack-runtime.js +1 -1
  54. package/dist/preview/.next/static/chunks/416-56f79fc7e689f06f.js +1 -0
  55. package/dist/preview/.next/static/chunks/683-8bbfd191e5105f01.js +1 -0
  56. package/dist/preview/.next/static/chunks/744-79730358b37b2212.js +1 -0
  57. package/dist/preview/.next/static/chunks/781-5f16c6bc9d9d4cc1.js +1 -0
  58. package/dist/preview/.next/static/chunks/832ad4be-cb988facfb8f955f.js +1 -0
  59. package/dist/preview/.next/static/chunks/87-38e35f08507de015.js +1 -0
  60. package/dist/preview/.next/static/chunks/{afa401a5-9ebf2515b1397993.js → afa401a5-3e949a1cfd317dd3.js} +3 -3
  61. package/dist/preview/.next/static/chunks/app/_not-found/page-09d694081cc9d4dc.js +1 -0
  62. package/dist/preview/.next/static/chunks/app/layout-a6640e62690d8fd6.js +1 -0
  63. package/dist/preview/.next/static/chunks/app/page-ba68f50b287e7478.js +1 -0
  64. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-4a5b026ab543e27f.js +1 -0
  65. package/dist/preview/.next/static/chunks/framework-c2bd6d936e3077bc.js +1 -0
  66. package/dist/preview/.next/static/chunks/main-44463a8301435b64.js +1 -0
  67. package/dist/preview/.next/static/chunks/main-app-c2e686acf8d370d7.js +1 -0
  68. package/dist/preview/.next/static/chunks/pages/_app-f3011d3f00bb8dba.js +1 -0
  69. package/dist/preview/.next/static/chunks/pages/_error-39a87dee2e97a2a3.js +1 -0
  70. package/dist/preview/.next/static/chunks/{webpack-9255716c9496e606.js → webpack-41e2667c9f086a4f.js} +1 -1
  71. package/dist/preview/.next/static/css/d7df9cfc3e182163.css +3 -0
  72. package/dist/preview/.next/static/gFk9UfWL8joM4iD7-wlKF/_buildManifest.js +1 -0
  73. package/dist/preview/.next/static/media/05613964ce6c782e-s.p.otf +0 -0
  74. package/dist/preview/.next/static/media/11c6126b9369e85e-s.p.otf +0 -0
  75. package/dist/preview/.next/static/media/26cb97734d8cb717-s.p.otf +0 -0
  76. package/dist/preview/.next/static/media/bb6462617151f6b7-s.p.otf +0 -0
  77. package/dist/preview/.next/static/media/cf6daef822ab0142-s.p.otf +0 -0
  78. package/dist/preview/.next/static/media/e4051546b3043204-s.p.otf +0 -0
  79. package/dist/preview/.next/trace +26 -22
  80. package/dist/preview/.next/types/cache-life.d.ts +3 -3
  81. package/package.json +17 -11
  82. package/scripts/build-preview-server.mjs +32 -0
  83. package/scripts/fill-caniemail-data.mjs +36 -0
  84. package/src/actions/email-validation/caniemail-data.ts +85993 -0
  85. package/src/actions/email-validation/check-compatibility.ts +322 -0
  86. package/src/actions/email-validation/check-images.spec.tsx +21 -12
  87. package/src/actions/email-validation/check-images.ts +88 -86
  88. package/src/actions/email-validation/check-links.spec.tsx +24 -14
  89. package/src/actions/email-validation/check-links.ts +59 -56
  90. package/src/actions/get-email-path-from-slug.ts +1 -1
  91. package/src/actions/render-email-by-path.tsx +2 -1
  92. package/src/{utils/emails-directory-absolute-path.ts → app/env.ts} +2 -0
  93. package/src/app/fonts/SFMono/SFMonoBold.otf +0 -0
  94. package/src/app/fonts/SFMono/SFMonoBoldItalic.otf +0 -0
  95. package/src/app/fonts/SFMono/SFMonoHeavy.otf +0 -0
  96. package/src/app/fonts/SFMono/SFMonoHeavyItalic.otf +0 -0
  97. package/src/app/fonts/SFMono/SFMonoLight.otf +0 -0
  98. package/src/app/fonts/SFMono/SFMonoLightItalic.otf +0 -0
  99. package/src/app/fonts/SFMono/SFMonoMedium.otf +0 -0
  100. package/src/app/fonts/SFMono/SFMonoMediumItalic.otf +0 -0
  101. package/src/app/fonts/SFMono/SFMonoRegular.otf +0 -0
  102. package/src/app/fonts/SFMono/SFMonoRegularItalic.otf +0 -0
  103. package/src/app/fonts/SFMono/SFMonoSemibold.otf +0 -0
  104. package/src/app/fonts/SFMono/SFMonoSemiboldItalic.otf +0 -0
  105. package/src/app/fonts.ts +39 -0
  106. package/src/app/layout.tsx +6 -3
  107. package/src/app/page.tsx +4 -4
  108. package/src/app/preview/[...slug]/page.tsx +73 -16
  109. package/src/app/preview/[...slug]/preview.tsx +49 -77
  110. package/src/components/code.tsx +0 -1
  111. package/src/components/icons/icon-base.tsx +4 -2
  112. package/src/components/icons/icon-reload.tsx +19 -0
  113. package/src/components/icons/icon-scanner.tsx +19 -0
  114. package/src/components/icons/icon-scissors.tsx +19 -0
  115. package/src/components/icons/icon-warning.tsx +31 -0
  116. package/src/components/send.tsx +1 -2
  117. package/src/components/shell.tsx +52 -88
  118. package/src/components/sidebar/file-tree-directory-children.tsx +1 -1
  119. package/src/components/sidebar/file-tree.tsx +1 -1
  120. package/src/components/sidebar/sidebar.tsx +23 -378
  121. package/src/components/toolbar/linter.tsx +310 -0
  122. package/src/components/toolbar/results-table.tsx +0 -0
  123. package/src/components/toolbar/results.tsx +48 -0
  124. package/src/components/toolbar/spam-assassin.tsx +144 -0
  125. package/src/components/toolbar/toolbar-button.tsx +50 -0
  126. package/src/components/toolbar/use-cached-state.ts +33 -0
  127. package/src/components/toolbar.tsx +197 -0
  128. package/src/components/tooltip-content.tsx +1 -2
  129. package/src/components/topbar/view-size-controls.tsx +1 -0
  130. package/src/components/topbar.tsx +29 -48
  131. package/src/contexts/emails.tsx +2 -1
  132. package/src/contexts/preview.tsx +81 -0
  133. package/src/hooks/use-email-rendering-result.ts +2 -1
  134. package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
  135. package/src/utils/caniemail/all-css-properties.ts +358 -0
  136. package/src/utils/caniemail/ast/get-object-variables.ts +61 -0
  137. package/src/utils/caniemail/ast/get-used-style-properties.ts +91 -0
  138. package/src/utils/caniemail/get-compatibility-stats-for-entry.ts +118 -0
  139. package/src/utils/caniemail/get-css-functions.ts +25 -0
  140. package/src/utils/caniemail/get-css-property-names.ts +32 -0
  141. package/src/utils/caniemail/get-css-property-with-value.ts +14 -0
  142. package/src/utils/caniemail/get-css-unit.ts +3 -0
  143. package/src/utils/caniemail/get-element-attributes.ts +7 -0
  144. package/src/utils/caniemail/get-element-names.ts +20 -0
  145. package/src/utils/caniemail/tailwind/generate-tailwind-rules.ts +30 -0
  146. package/src/utils/caniemail/tailwind/get-tailwind-config.ts +205 -0
  147. package/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts +25 -0
  148. package/src/utils/caniemail/tailwind/get-tailwind-metadata.ts +45 -0
  149. package/src/utils/caniemail/tailwind/setup-tailwind-context.ts +15 -0
  150. package/src/utils/get-email-component.ts +34 -67
  151. package/src/utils/linting.ts +85 -0
  152. package/src/utils/result.ts +49 -0
  153. package/src/utils/run-bundled-code.ts +64 -0
  154. package/tailwind-internals.d.ts +133 -0
  155. package/tailwind.config.ts +1 -0
  156. package/tsconfig.json +9 -3
  157. package/build-preview-server.mjs +0 -25
  158. package/dist/preview/.next/server/chunks/196.js +0 -5
  159. package/dist/preview/.next/server/chunks/300.js +0 -13
  160. package/dist/preview/.next/server/chunks/631.js +0 -6
  161. package/dist/preview/.next/server/chunks/644.js +0 -1
  162. package/dist/preview/.next/server/chunks/734.js +0 -15
  163. package/dist/preview/.next/static/Pt6wqIrWnQxbiyqaKNFOx/_buildManifest.js +0 -1
  164. package/dist/preview/.next/static/chunks/285-dbf6306a0d45c33d.js +0 -1
  165. package/dist/preview/.next/static/chunks/447-886131c35ca42b91.js +0 -1
  166. package/dist/preview/.next/static/chunks/490-d5745684930d49e0.js +0 -1
  167. package/dist/preview/.next/static/chunks/5fec7a0a-5179023f3f5a9421.js +0 -1
  168. package/dist/preview/.next/static/chunks/603-36207c8905355e23.js +0 -1
  169. package/dist/preview/.next/static/chunks/797-46f6c20952f0a280.js +0 -2
  170. package/dist/preview/.next/static/chunks/app/_not-found/page-96d3eac723be3ee2.js +0 -1
  171. package/dist/preview/.next/static/chunks/app/layout-d06046b8a368df3b.js +0 -1
  172. package/dist/preview/.next/static/chunks/app/page-ef1c23b954fbd0b5.js +0 -1
  173. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-ea8e1ae2b5a4a0ec.js +0 -1
  174. package/dist/preview/.next/static/chunks/framework-e7cae9cecd5c9ba2.js +0 -1
  175. package/dist/preview/.next/static/chunks/main-app-9f2fb5ea26e2765b.js +0 -1
  176. package/dist/preview/.next/static/chunks/main-df761fde212f9cda.js +0 -1
  177. package/dist/preview/.next/static/chunks/pages/_app-203a61b355820ccf.js +0 -1
  178. package/dist/preview/.next/static/chunks/pages/_error-1764ca54938748c8.js +0 -1
  179. package/dist/preview/.next/static/css/e4822d5ba3082a95.css +0 -3
  180. package/dist/preview/.next/static/css/ec5d7e66bd3b6cb8.css +0 -1
  181. package/src/app/inter.ts +0 -7
  182. package/src/components/icons/icon-circle-check.tsx +0 -21
  183. package/src/components/icons/icon-circle-close.tsx +0 -17
  184. package/src/components/icons/icon-circle-warning.tsx +0 -17
  185. package/src/components/sidebar/image-checker.tsx +0 -162
  186. package/src/components/sidebar/link-checker.tsx +0 -151
  187. package/src/components/sidebar/spam-assassin.tsx +0 -158
  188. /package/dist/preview/.next/static/{Pt6wqIrWnQxbiyqaKNFOx → gFk9UfWL8joM4iD7-wlKF}/_ssgManifest.js +0 -0
  189. /package/src/components/{sidebar → toolbar}/checking-results.tsx +0 -0
@@ -0,0 +1,197 @@
1
+ 'use client';
2
+ import * as Tabs from '@radix-ui/react-tabs';
3
+ import { LayoutGroup } from 'framer-motion';
4
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
5
+ import { use, useEffect } from 'react';
6
+ import { isBuilding } from '../app/env';
7
+ import { PreviewContext } from '../contexts/preview';
8
+ import { cn } from '../utils';
9
+ import { IconArrowDown } from './icons/icon-arrow-down';
10
+ import { IconReload } from './icons/icon-reload';
11
+ import { IconScanner } from './icons/icon-scanner';
12
+ import { IconScissors } from './icons/icon-scissors';
13
+ import { Linter, type LintingRow, useLinter } from './toolbar/linter';
14
+ import {
15
+ SpamAssassin,
16
+ type SpamCheckingResult,
17
+ useSpamAssassin,
18
+ } from './toolbar/spam-assassin';
19
+ import { ToolbarButton } from './toolbar/toolbar-button';
20
+ import { useCachedState } from './toolbar/use-cached-state';
21
+
22
+ export type ToolbarTabValue = 'linter' | 'spam-assassin';
23
+
24
+ const ToolbarInner = ({
25
+ serverLintingRows,
26
+ serverSpamCheckingResult,
27
+
28
+ markup,
29
+ reactMarkup,
30
+ plainText,
31
+ emailPath,
32
+ emailSlug,
33
+ }: ToolbarProps & {
34
+ markup: string;
35
+ reactMarkup: string;
36
+ plainText: string;
37
+ emailSlug: string;
38
+ emailPath: string;
39
+ }) => {
40
+ const pathname = usePathname();
41
+ const searchParams = useSearchParams();
42
+ const router = useRouter();
43
+
44
+ const activeTab = (searchParams.get('toolbar-panel') ?? undefined) as
45
+ | ToolbarTabValue
46
+ | undefined;
47
+
48
+ const toggled = activeTab !== undefined;
49
+
50
+ const setActivePanelValue = (newValue: ToolbarTabValue | undefined) => {
51
+ const params = new URLSearchParams(searchParams);
52
+ if (newValue === undefined) {
53
+ params.delete('toolbar-panel');
54
+ } else {
55
+ params.set('toolbar-panel', newValue);
56
+ }
57
+ router.push(`${pathname}?${params.toString()}`);
58
+ };
59
+
60
+ const [cachedSpamCheckingResult, setCachedSpamCheckingResult] =
61
+ useCachedState<SpamCheckingResult>(
62
+ `spam-assassin-${emailSlug.replaceAll('/', '-')}`,
63
+ );
64
+ const [spamCheckingResult, { load: loadSpamChecking }] = useSpamAssassin({
65
+ markup,
66
+ plainText,
67
+
68
+ initialResult: serverSpamCheckingResult ?? cachedSpamCheckingResult,
69
+ });
70
+
71
+ const [cachedLintingRows, setCachedLintingRows] = useCachedState<
72
+ LintingRow[]
73
+ >(`linter-${emailSlug.replaceAll('/', '-')}`);
74
+ const [lintingRows, { load: loadLinting }] = useLinter({
75
+ reactMarkup,
76
+ emailPath,
77
+ markup,
78
+
79
+ initialRows: serverLintingRows ?? cachedLintingRows,
80
+ });
81
+
82
+ if (!isBuilding) {
83
+ useEffect(() => {
84
+ (async () => {
85
+ const lintingRows = await loadLinting();
86
+ setCachedLintingRows(lintingRows);
87
+
88
+ const spamCheckingResult = await loadSpamChecking();
89
+ setCachedSpamCheckingResult(spamCheckingResult);
90
+ })();
91
+ }, []);
92
+ }
93
+
94
+ return (
95
+ <div
96
+ data-toggled={toggled}
97
+ className={cn(
98
+ 'bg-black group/toolbar text-xs text-slate-11 h-48 transition-all',
99
+ 'data-[toggled=false]:h-8',
100
+ )}
101
+ >
102
+ <Tabs.Root
103
+ value={activeTab}
104
+ onValueChange={(newValue) => {
105
+ setActivePanelValue(newValue as ToolbarTabValue);
106
+ }}
107
+ asChild
108
+ >
109
+ <div className="flex flex-col h-full">
110
+ <Tabs.List className="flex gap-4 px-2 border-b border-solid border-slate-6 h-7 w-full">
111
+ <LayoutGroup id="toolbar">
112
+ <Tabs.Trigger asChild value="spam-assassin">
113
+ <ToolbarButton active={activeTab === 'spam-assassin'}>
114
+ <IconScissors />
115
+ Spam Assassin
116
+ </ToolbarButton>
117
+ </Tabs.Trigger>
118
+ <Tabs.Trigger asChild value="linter">
119
+ <ToolbarButton active={activeTab === 'linter'}>
120
+ <IconScanner />
121
+ Linter
122
+ </ToolbarButton>
123
+ </Tabs.Trigger>
124
+ </LayoutGroup>
125
+ <div className="flex gap-1 ml-auto">
126
+ {isBuilding ? null : (
127
+ <ToolbarButton
128
+ tooltip="Reload"
129
+ onClick={async () => {
130
+ if (activeTab === undefined) {
131
+ setActivePanelValue('linter');
132
+ }
133
+ if (activeTab === 'spam-assassin') {
134
+ await loadSpamChecking();
135
+ } else {
136
+ await loadLinting();
137
+ }
138
+ }}
139
+ >
140
+ <IconReload />
141
+ </ToolbarButton>
142
+ )}
143
+ <ToolbarButton
144
+ tooltip="Toggle toolbar"
145
+ onClick={() => {
146
+ if (activeTab === undefined) {
147
+ setActivePanelValue('linter');
148
+ } else {
149
+ setActivePanelValue(undefined);
150
+ }
151
+ }}
152
+ >
153
+ <IconArrowDown className="transition-transform group-data-[toggled=false]/toolbar:rotate-180" />
154
+ </ToolbarButton>
155
+ </div>
156
+ </Tabs.List>
157
+
158
+ <div className="flex-grow transition-opacity opacity-100 group-data-[toggled=false]/toolbar:opacity-0 overflow-y-auto px-2">
159
+ <Tabs.Content value="linter">
160
+ <Linter rows={lintingRows} />
161
+ </Tabs.Content>
162
+ <Tabs.Content value="spam-assassin">
163
+ <SpamAssassin result={spamCheckingResult} />
164
+ </Tabs.Content>
165
+ </div>
166
+ </div>
167
+ </Tabs.Root>
168
+ </div>
169
+ );
170
+ };
171
+
172
+ interface ToolbarProps {
173
+ serverSpamCheckingResult: SpamCheckingResult | undefined;
174
+ serverLintingRows: LintingRow[] | undefined;
175
+ }
176
+
177
+ export const Toolbar = ({
178
+ serverLintingRows,
179
+ serverSpamCheckingResult,
180
+ }: ToolbarProps) => {
181
+ const { emailPath, emailSlug, renderedEmailMetadata } = use(PreviewContext)!;
182
+
183
+ if (renderedEmailMetadata === undefined) return null;
184
+ const { markup, plainText, reactMarkup } = renderedEmailMetadata;
185
+
186
+ return (
187
+ <ToolbarInner
188
+ emailPath={emailPath}
189
+ emailSlug={emailSlug}
190
+ markup={markup}
191
+ reactMarkup={reactMarkup}
192
+ plainText={plainText}
193
+ serverLintingRows={serverLintingRows}
194
+ serverSpamCheckingResult={serverSpamCheckingResult}
195
+ />
196
+ );
197
+ };
@@ -1,6 +1,5 @@
1
1
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
2
2
  import * as React from 'react';
3
- import { inter } from '../app/inter';
4
3
  import { cn } from '../utils';
5
4
 
6
5
  type ContentElement = React.ComponentRef<typeof TooltipPrimitive.Content>;
@@ -19,7 +18,7 @@ export const TooltipContent = React.forwardRef<
19
18
  {...props}
20
19
  className={cn(
21
20
  'z-20 rounded-md border border-slate-6 bg-black px-3 py-2 text-white text-xs',
22
- `${inter.variable} font-sans`,
21
+ 'font-sans',
23
22
  )}
24
23
  ref={forwardedRef}
25
24
  sideOffset={sideOffset}
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
2
3
  import { motion } from 'framer-motion';
3
4
  import * as React from 'react';
@@ -1,37 +1,19 @@
1
1
  'use client';
2
2
 
3
+ import { use } from 'react';
3
4
  import { Heading } from './heading';
4
5
  import { IconHideSidebar } from './icons/icon-hide-sidebar';
5
- import { Send } from './send';
6
+ import { ShellContext } from './shell';
6
7
  import { Tooltip } from './tooltip';
7
- import { ActiveViewToggleGroup } from './topbar/active-view-toggle-group';
8
- import { ViewSizeControls } from './topbar/view-size-controls';
9
8
 
10
9
  interface TopbarProps {
11
- currentEmailOpenSlug: string;
12
- pathSeparator: string;
13
- markup?: string;
14
- onToggleSidebar?: () => void;
15
- activeView?: string;
16
- setActiveView?: (view: string) => void;
17
- viewWidth?: number;
18
- setViewWidth?: (width: number) => void;
19
- viewHeight?: number;
20
- setViewHeight?: (height: number) => void;
10
+ emailTitle: string;
11
+ children: React.ReactNode;
21
12
  }
22
13
 
23
- export const Topbar = ({
24
- currentEmailOpenSlug,
25
- pathSeparator,
26
- markup,
27
- activeView,
28
- setActiveView,
29
- viewWidth,
30
- setViewWidth,
31
- viewHeight,
32
- setViewHeight,
33
- onToggleSidebar,
34
- }: TopbarProps) => {
14
+ export const Topbar = ({ emailTitle, children }: TopbarProps) => {
15
+ const { toggleSidebar } = use(ShellContext)!;
16
+
35
17
  return (
36
18
  <Tooltip.Provider>
37
19
  <header className="relative flex h-[3.3125rem] items-center justify-between gap-3 border-slate-6 border-b px-3">
@@ -41,9 +23,7 @@ export const Topbar = ({
41
23
  <button
42
24
  className="relative hidden rounded-lg px-2 py-2 text-slate-11 transition duration-200 ease-in-out hover:bg-slate-5 hover:text-slate-12 lg:flex"
43
25
  onClick={() => {
44
- if (onToggleSidebar) {
45
- onToggleSidebar();
46
- }
26
+ toggleSidebar();
47
27
  }}
48
28
  type="button"
49
29
  >
@@ -54,30 +34,31 @@ export const Topbar = ({
54
34
  </Tooltip>
55
35
  <div className="hidden items-center overflow-hidden text-center lg:flex">
56
36
  <Heading as="h2" className="truncate" size="2" weight="medium">
57
- {currentEmailOpenSlug.split(pathSeparator).pop()}
37
+ {emailTitle}
58
38
  </Heading>
59
39
  </div>
60
40
  </div>
61
41
  <div className="flex w-full items-center justify-between gap-3 lg:w-fit lg:justify-start">
62
- {setViewWidth && setViewHeight && viewWidth && viewHeight ? (
63
- <ViewSizeControls
64
- setViewHeight={setViewHeight}
65
- setViewWidth={setViewWidth}
66
- viewHeight={viewHeight}
67
- viewWidth={viewWidth}
68
- />
69
- ) : null}
70
- {activeView && setActiveView ? (
71
- <ActiveViewToggleGroup
72
- activeView={activeView}
73
- setActiveView={setActiveView}
74
- />
75
- ) : null}
76
- {markup ? (
77
- <div className="flex justify-end">
78
- <Send markup={markup} />
79
- </div>
80
- ) : null}
42
+ {children}
43
+ {/* {setViewWidth && setViewHeight && viewWidth && viewHeight ? ( */}
44
+ {/* <ViewSizeControls */}
45
+ {/* setViewHeight={setViewHeight} */}
46
+ {/* setViewWidth={setViewWidth} */}
47
+ {/* viewHeight={viewHeight} */}
48
+ {/* viewWidth={viewWidth} */}
49
+ {/* /> */}
50
+ {/* ) : null} */}
51
+ {/* {activeView && setActiveView ? ( */}
52
+ {/* <ActiveViewToggleGroup */}
53
+ {/* activeView={activeView} */}
54
+ {/* setActiveView={setActiveView} */}
55
+ {/* /> */}
56
+ {/* ) : null} */}
57
+ {/* {markup ? ( */}
58
+ {/* <div className="flex justify-end"> */}
59
+ {/* <Send markup={markup} /> */}
60
+ {/* </div> */}
61
+ {/* ) : null} */}
81
62
  </div>
82
63
  </header>
83
64
  </Tooltip.Provider>
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
  import { createContext, useContext, useState } from 'react';
3
3
  import { getEmailsDirectoryMetadataAction } from '../actions/get-emails-directory-metadata-action';
4
+ import { isBuilding } from '../app/env';
4
5
  import { useHotreload } from '../hooks/use-hot-reload';
5
6
  import type { EmailsDirectory } from '../utils/get-emails-directory-metadata';
6
7
 
@@ -30,7 +31,7 @@ export const EmailsProvider = (props: {
30
31
  const [emailsDirectoryMetadata, setEmailsDirectoryMetadata] =
31
32
  useState<EmailsDirectory>(props.initialEmailsDirectoryMetadata);
32
33
 
33
- if (process.env.NEXT_PUBLIC_IS_BUILDING !== 'true') {
34
+ if (!isBuilding) {
34
35
  // this will not change on runtime so it doesn't violate
35
36
  // the rules of hooks
36
37
  // eslint-disable-next-line react-hooks/rules-of-hooks
@@ -0,0 +1,81 @@
1
+ 'use client';
2
+ import { useRouter } from 'next/navigation';
3
+ import { createContext } from 'react';
4
+ import type {
5
+ EmailRenderingResult,
6
+ RenderedEmailMetadata,
7
+ } from '../actions/render-email-by-path';
8
+ import { isBuilding } from '../app/env';
9
+ import { useEmailRenderingResult } from '../hooks/use-email-rendering-result';
10
+ import { useHotreload } from '../hooks/use-hot-reload';
11
+ import { useRenderingMetadata } from '../hooks/use-rendering-metadata';
12
+
13
+ export const PreviewContext = createContext<
14
+ | {
15
+ renderedEmailMetadata: RenderedEmailMetadata | undefined;
16
+ renderingResult: EmailRenderingResult;
17
+
18
+ emailSlug: string;
19
+ emailPath: string;
20
+ }
21
+ | undefined
22
+ >(undefined);
23
+
24
+ interface PreviewProvider {
25
+ emailSlug: string;
26
+ emailPath: string;
27
+
28
+ serverRenderingResult: EmailRenderingResult;
29
+
30
+ children: React.ReactNode;
31
+ }
32
+
33
+ export const PreviewProvider = ({
34
+ emailSlug,
35
+ emailPath,
36
+ serverRenderingResult,
37
+ children,
38
+ }: PreviewProvider) => {
39
+ const router = useRouter();
40
+
41
+ const renderingResult = useEmailRenderingResult(
42
+ emailPath,
43
+ serverRenderingResult,
44
+ );
45
+
46
+ const renderedEmailMetadata = useRenderingMetadata(
47
+ emailPath,
48
+ renderingResult,
49
+ serverRenderingResult,
50
+ );
51
+
52
+ if (!isBuilding) {
53
+ // this will not change on runtime so it doesn't violate
54
+ // the rules of hooks
55
+ // eslint-disable-next-line react-hooks/rules-of-hooks
56
+ useHotreload((changes) => {
57
+ const changeForThisEmail = changes.find((change) =>
58
+ change.filename.includes(emailSlug),
59
+ );
60
+
61
+ if (typeof changeForThisEmail !== 'undefined') {
62
+ if (changeForThisEmail.event === 'unlink') {
63
+ router.push('/');
64
+ }
65
+ }
66
+ });
67
+ }
68
+
69
+ return (
70
+ <PreviewContext.Provider
71
+ value={{
72
+ emailPath,
73
+ emailSlug,
74
+ renderedEmailMetadata,
75
+ renderingResult,
76
+ }}
77
+ >
78
+ {children}
79
+ </PreviewContext.Provider>
80
+ );
81
+ };
@@ -4,6 +4,7 @@ import {
4
4
  type EmailRenderingResult,
5
5
  renderEmailByPath,
6
6
  } from '../actions/render-email-by-path';
7
+ import { isBuilding } from '../app/env';
7
8
  import { useHotreload } from './use-hot-reload';
8
9
 
9
10
  export const useEmailRenderingResult = (
@@ -14,7 +15,7 @@ export const useEmailRenderingResult = (
14
15
  serverEmailRenderedResult,
15
16
  );
16
17
 
17
- if (process.env.NEXT_PUBLIC_IS_BUILDING !== 'true') {
18
+ if (!isBuilding) {
18
19
  // eslint-disable-next-line react-hooks/rules-of-hooks
19
20
  useHotreload(async (changes) => {
20
21
  for await (const change of changes) {
@@ -1,3 +1,3 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`getEmailComponent() > with a demo email template 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="/static/vercel-logo.png"/><link rel="preload" as="image" href="/static/vercel-user.png"/><link rel="preload" as="image" href="/static/vercel-arrow.png"/><link rel="preload" as="image" href="/static/vercel-team.png"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><!--$--></head><body style="background-color:rgb(255,255,255);margin-top:auto;margin-bottom:auto;margin-left:auto;margin-right:auto;font-family:ui-sans-serif, system-ui, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;padding-left:0.5rem;padding-right:0.5rem"><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">Join Alan on Vercel<div> ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏</div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);border-radius:0.25rem;margin-top:40px;margin-bottom:40px;margin-left:auto;margin-right:auto;padding:20px;max-width:465px"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:32px"><tbody><tr><td><img alt="Vercel" height="37" src="/static/vercel-logo.png" style="margin-top:0px;margin-bottom:0px;margin-left:auto;margin-right:auto;display:block;outline:none;border:none;text-decoration:none" width="40"/></td></tr></tbody></table><h1 style="color:rgb(0,0,0);font-size:24px;font-weight:400;text-align:center;padding:0px;margin-top:30px;margin-bottom:30px;margin-left:0px;margin-right:0px">Join <strong>Enigma</strong> on <strong>Vercel</strong></h1><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin:16px 0">Hello <!-- -->alanturing<!-- -->,</p><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin:16px 0"><strong>Alan</strong> (<a href="mailto:alan.turing@example.com" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">alan.turing@example.com</a>) has invited you to the <strong>Enigma</strong> team on<!-- --> <strong>Vercel</strong>.</p><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td align="right" data-id="__react-email-column"><img height="64" src="/static/vercel-user.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td><td align="center" data-id="__react-email-column"><img alt="invited you to" height="9" src="/static/vercel-arrow.png" style="display:block;outline:none;border:none;text-decoration:none" width="12"/></td><td align="left" data-id="__react-email-column"><img height="64" src="/static/vercel-team.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:center;margin-top:32px;margin-bottom:32px"><tbody><tr><td><a href="https://vercel.com/teams/invite/foo" style="background-color:rgb(0,0,0);border-radius:0.25rem;color:rgb(255,255,255);font-size:12px;font-weight:600;text-decoration-line:none;text-align:center;padding-left:1.25rem;padding-right:1.25rem;padding-top:0.75rem;padding-bottom:0.75rem;line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;padding:12px 20px 12px 20px" target="_blank"><span><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:18" hidden>&#8202;&#8202;</i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">Join the team</span><span><!--[if mso]><i style="mso-font-width:500%" hidden>&#8202;&#8202;&#8203;</i><![endif]--></span></a></td></tr></tbody></table><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin:16px 0">or copy and paste this URL into your browser:<!-- --> <a href="https://vercel.com/teams/invite/foo" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">https://vercel.com/teams/invite/foo</a></p><hr style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);margin-top:26px;margin-bottom:26px;margin-left:0px;margin-right:0px;width:100%;border:none;border-top:1px solid #eaeaea"/><p style="color:rgb(102,102,102);font-size:12px;line-height:24px;margin:16px 0">This invitation was intended for<!-- --> <span style="color:rgb(0,0,0)">alanturing</span>. This invite was sent from <span style="color:rgb(0,0,0)">204.13.186.218</span> <!-- -->located in<!-- --> <span style="color:rgb(0,0,0)">São Paulo, Brazil</span>. If you were not expecting this invitation, you can ignore this email. If you are concerned about your account&#x27;s safety, please reply to this email to get in touch with us.</p></td></tr></tbody></table><!--/$--></body></html>"`;
3
+ exports[`getEmailComponent() > with a demo email template 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="/static/vercel-logo.png"/><link rel="preload" as="image" href="/static/vercel-user.png"/><link rel="preload" as="image" href="/static/vercel-arrow.png"/><link rel="preload" as="image" href="/static/vercel-team.png"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><!--$--></head><body style="background-color:rgb(255,255,255);margin-top:auto;margin-bottom:auto;margin-left:auto;margin-right:auto;font-family:ui-sans-serif, system-ui, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;padding-left:0.5rem;padding-right:0.5rem"><div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">Join Alan on Vercel<div> ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏</div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);border-radius:0.25rem;margin-top:40px;margin-bottom:40px;margin-left:auto;margin-right:auto;padding:20px;max-width:465px"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:32px"><tbody><tr><td><img alt="Vercel" height="37" src="/static/vercel-logo.png" style="margin-top:0px;margin-bottom:0px;margin-left:auto;margin-right:auto;display:block;outline:none;border:none;text-decoration:none" width="40"/></td></tr></tbody></table><h1 style="color:rgb(0,0,0);font-size:24px;font-weight:400;text-align:center;padding:0px;margin-top:30px;margin-bottom:30px;margin-left:0px;margin-right:0px">Join <strong>Enigma</strong> on <strong>Vercel</strong></h1><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px">Hello <!-- -->alanturing<!-- -->,</p><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px"><strong>Alan</strong> (<a href="mailto:alan.turing@example.com" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">alan.turing@example.com</a>) has invited you to the <strong>Enigma</strong> team on<!-- --> <strong>Vercel</strong>.</p><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td align="right" data-id="__react-email-column"><img height="64" src="/static/vercel-user.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td><td align="center" data-id="__react-email-column"><img alt="invited you to" height="9" src="/static/vercel-arrow.png" style="display:block;outline:none;border:none;text-decoration:none" width="12"/></td><td align="left" data-id="__react-email-column"><img height="64" src="/static/vercel-team.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64"/></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:center;margin-top:32px;margin-bottom:32px"><tbody><tr><td><a href="https://vercel.com/teams/invite/foo" style="background-color:rgb(0,0,0);border-radius:0.25rem;color:rgb(255,255,255);font-size:12px;font-weight:600;text-decoration-line:none;text-align:center;padding-left:1.25rem;padding-right:1.25rem;padding-top:0.75rem;padding-bottom:0.75rem;line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;padding:12px 20px 12px 20px" target="_blank"><span><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:18" hidden>&#8202;&#8202;</i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">Join the team</span><span><!--[if mso]><i style="mso-font-width:500%" hidden>&#8202;&#8202;&#8203;</i><![endif]--></span></a></td></tr></tbody></table><p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin-bottom:16px;margin-top:16px">or copy and paste this URL into your browser:<!-- --> <a href="https://vercel.com/teams/invite/foo" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">https://vercel.com/teams/invite/foo</a></p><hr style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);margin-top:26px;margin-bottom:26px;margin-left:0px;margin-right:0px;width:100%;border:none;border-top:1px solid #eaeaea"/><p style="color:rgb(102,102,102);font-size:12px;line-height:24px;margin-bottom:16px;margin-top:16px">This invitation was intended for<!-- --> <span style="color:rgb(0,0,0)">alanturing</span>. This invite was sent from <span style="color:rgb(0,0,0)">204.13.186.218</span> <!-- -->located in<!-- --> <span style="color:rgb(0,0,0)">São Paulo, Brazil</span>. If you were not expecting this invitation, you can ignore this email. If you are concerned about your account&#x27;s safety, please reply to this email to get in touch with us.</p></td></tr></tbody></table><!--/$--></body></html>"`;