react-email 4.0.0-alpha.5 → 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 (173) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/cli/index.js +1175 -2659
  3. package/dist/cli/index.mjs +16 -14
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +31 -31
  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 -11
  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/chunks/171.js +14 -0
  34. package/dist/preview/.next/server/chunks/446.js +6 -0
  35. package/dist/preview/.next/server/chunks/600.js +8 -0
  36. package/dist/preview/.next/server/chunks/811.js +13 -0
  37. package/dist/preview/.next/server/chunks/833.js +1 -0
  38. package/dist/preview/.next/server/functions-config-manifest.json +4 -1
  39. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  40. package/dist/preview/.next/server/pages/500.html +1 -1
  41. package/dist/preview/.next/server/pages/_app.js +1 -1
  42. package/dist/preview/.next/server/pages/_app.js.nft.json +1 -1
  43. package/dist/preview/.next/server/pages/_document.js +1 -1
  44. package/dist/preview/.next/server/pages/_document.js.nft.json +1 -1
  45. package/dist/preview/.next/server/pages/_error.js +1 -1
  46. package/dist/preview/.next/server/pages/_error.js.nft.json +1 -1
  47. package/dist/preview/.next/server/pages-manifest.json +5 -1
  48. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  49. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  50. package/dist/preview/.next/server/webpack-runtime.js +1 -1
  51. package/dist/preview/.next/static/chunks/416-56f79fc7e689f06f.js +1 -0
  52. package/dist/preview/.next/static/chunks/683-8bbfd191e5105f01.js +1 -0
  53. package/dist/preview/.next/static/chunks/744-79730358b37b2212.js +1 -0
  54. package/dist/preview/.next/static/chunks/781-5f16c6bc9d9d4cc1.js +1 -0
  55. package/dist/preview/.next/static/chunks/832ad4be-cb988facfb8f955f.js +1 -0
  56. package/dist/preview/.next/static/chunks/87-38e35f08507de015.js +1 -0
  57. package/dist/preview/.next/static/chunks/{afa401a5-a600c227dacf3ab4.js → afa401a5-3e949a1cfd317dd3.js} +3 -3
  58. package/dist/preview/.next/static/chunks/app/_not-found/page-09d694081cc9d4dc.js +1 -0
  59. package/dist/preview/.next/static/chunks/app/layout-a6640e62690d8fd6.js +1 -0
  60. package/dist/preview/.next/static/chunks/app/page-ba68f50b287e7478.js +1 -0
  61. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-4a5b026ab543e27f.js +1 -0
  62. package/dist/preview/.next/static/chunks/framework-c2bd6d936e3077bc.js +1 -0
  63. package/dist/preview/.next/static/chunks/main-44463a8301435b64.js +1 -0
  64. package/dist/preview/.next/static/chunks/main-app-c2e686acf8d370d7.js +1 -0
  65. package/dist/preview/.next/static/chunks/pages/_app-f3011d3f00bb8dba.js +1 -0
  66. package/dist/preview/.next/static/chunks/pages/_error-39a87dee2e97a2a3.js +1 -0
  67. package/dist/preview/.next/static/chunks/{webpack-2eb145a20ee6cb77.js → webpack-41e2667c9f086a4f.js} +1 -1
  68. package/dist/preview/.next/static/css/d7df9cfc3e182163.css +3 -0
  69. package/dist/preview/.next/static/gFk9UfWL8joM4iD7-wlKF/_buildManifest.js +1 -0
  70. package/dist/preview/.next/trace +26 -22
  71. package/dist/preview/.next/types/cache-life.d.ts +3 -3
  72. package/package.json +14 -9
  73. package/scripts/build-preview-server.mjs +32 -0
  74. package/scripts/fill-caniemail-data.mjs +36 -0
  75. package/src/actions/email-validation/caniemail-data.ts +85993 -0
  76. package/src/actions/email-validation/check-compatibility.ts +322 -0
  77. package/src/actions/email-validation/check-images.spec.tsx +2 -2
  78. package/src/actions/email-validation/check-images.ts +2 -2
  79. package/src/actions/email-validation/check-links.spec.tsx +4 -4
  80. package/src/actions/email-validation/check-links.ts +2 -2
  81. package/src/actions/get-email-path-from-slug.ts +1 -1
  82. package/src/actions/render-email-by-path.tsx +2 -1
  83. package/src/{utils/emails-directory-absolute-path.ts → app/env.ts} +2 -0
  84. package/src/app/layout.tsx +1 -1
  85. package/src/app/page.tsx +1 -1
  86. package/src/app/preview/[...slug]/page.tsx +73 -16
  87. package/src/app/preview/[...slug]/preview.tsx +11 -57
  88. package/src/components/code.tsx +0 -1
  89. package/src/components/toolbar/linter.tsx +267 -124
  90. package/src/components/toolbar/spam-assassin.tsx +20 -31
  91. package/src/components/toolbar/toolbar-button.tsx +50 -0
  92. package/src/components/toolbar/use-cached-state.ts +33 -0
  93. package/src/components/toolbar.tsx +106 -98
  94. package/src/components/topbar/view-size-controls.tsx +1 -0
  95. package/src/components/topbar.tsx +3 -9
  96. package/src/contexts/emails.tsx +2 -1
  97. package/src/contexts/preview.tsx +81 -0
  98. package/src/hooks/use-email-rendering-result.ts +2 -1
  99. package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
  100. package/src/utils/caniemail/all-css-properties.ts +358 -0
  101. package/src/utils/caniemail/ast/get-object-variables.ts +61 -0
  102. package/src/utils/caniemail/ast/get-used-style-properties.ts +91 -0
  103. package/src/utils/caniemail/get-compatibility-stats-for-entry.ts +118 -0
  104. package/src/utils/caniemail/get-css-functions.ts +25 -0
  105. package/src/utils/caniemail/get-css-property-names.ts +32 -0
  106. package/src/utils/caniemail/get-css-property-with-value.ts +14 -0
  107. package/src/utils/caniemail/get-css-unit.ts +3 -0
  108. package/src/utils/caniemail/get-element-attributes.ts +7 -0
  109. package/src/utils/caniemail/get-element-names.ts +20 -0
  110. package/src/utils/caniemail/tailwind/generate-tailwind-rules.ts +30 -0
  111. package/src/utils/caniemail/tailwind/get-tailwind-config.ts +205 -0
  112. package/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts +25 -0
  113. package/src/utils/caniemail/tailwind/get-tailwind-metadata.ts +45 -0
  114. package/src/utils/caniemail/tailwind/setup-tailwind-context.ts +15 -0
  115. package/src/utils/get-email-component.ts +34 -67
  116. package/src/utils/linting.ts +85 -0
  117. package/src/utils/result.ts +49 -0
  118. package/src/utils/run-bundled-code.ts +64 -0
  119. package/tailwind-internals.d.ts +133 -0
  120. package/tsconfig.json +9 -3
  121. package/build-preview-server.mjs +0 -25
  122. package/dist/preview/.next/cache/images/TcyzHbFXGFjrOu3wEMvDoSmqCh3qP3iiNqJf0QbED9Y/60.1741728556140.cQ5qicbpvoXZ7leVmWqG2ElLwXB1ynYeSv8MBSA-QeM.Vy8iMWM3MGUtMTk1ODcxYmIyNzMi.webp +0 -0
  123. package/dist/preview/.next/cache/webpack/client-development/0.pack.gz +0 -0
  124. package/dist/preview/.next/cache/webpack/client-development/1.pack.gz +0 -0
  125. package/dist/preview/.next/cache/webpack/client-development/10.pack.gz +0 -0
  126. package/dist/preview/.next/cache/webpack/client-development/11.pack.gz +0 -0
  127. package/dist/preview/.next/cache/webpack/client-development/12.pack.gz +0 -0
  128. package/dist/preview/.next/cache/webpack/client-development/13.pack.gz +0 -0
  129. package/dist/preview/.next/cache/webpack/client-development/2.pack.gz +0 -0
  130. package/dist/preview/.next/cache/webpack/client-development/3.pack.gz +0 -0
  131. package/dist/preview/.next/cache/webpack/client-development/4.pack.gz +0 -0
  132. package/dist/preview/.next/cache/webpack/client-development/5.pack.gz +0 -0
  133. package/dist/preview/.next/cache/webpack/client-development/6.pack.gz +0 -0
  134. package/dist/preview/.next/cache/webpack/client-development/7.pack.gz +0 -0
  135. package/dist/preview/.next/cache/webpack/client-development/8.pack.gz +0 -0
  136. package/dist/preview/.next/cache/webpack/client-development/9.pack.gz +0 -0
  137. package/dist/preview/.next/cache/webpack/client-development/index.pack.gz +0 -0
  138. package/dist/preview/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
  139. package/dist/preview/.next/cache/webpack/server-development/0.pack.gz +0 -0
  140. package/dist/preview/.next/cache/webpack/server-development/1.pack.gz +0 -0
  141. package/dist/preview/.next/cache/webpack/server-development/2.pack.gz +0 -0
  142. package/dist/preview/.next/cache/webpack/server-development/3.pack.gz +0 -0
  143. package/dist/preview/.next/cache/webpack/server-development/4.pack.gz +0 -0
  144. package/dist/preview/.next/cache/webpack/server-development/5.pack.gz +0 -0
  145. package/dist/preview/.next/cache/webpack/server-development/6.pack.gz +0 -0
  146. package/dist/preview/.next/cache/webpack/server-development/7.pack.gz +0 -0
  147. package/dist/preview/.next/cache/webpack/server-development/8.pack.gz +0 -0
  148. package/dist/preview/.next/cache/webpack/server-development/9.pack.gz +0 -0
  149. package/dist/preview/.next/cache/webpack/server-development/index.pack.gz +0 -0
  150. package/dist/preview/.next/cache/webpack/server-development/index.pack.gz.old +0 -0
  151. package/dist/preview/.next/server/chunks/143.js +0 -6
  152. package/dist/preview/.next/server/chunks/409.js +0 -5
  153. package/dist/preview/.next/server/chunks/46.js +0 -1
  154. package/dist/preview/.next/server/chunks/478.js +0 -14
  155. package/dist/preview/.next/server/chunks/707.js +0 -13
  156. package/dist/preview/.next/static/B4EYZiVzdylEG9lAIl-aO/_buildManifest.js +0 -1
  157. package/dist/preview/.next/static/chunks/575-bc52750855c25df4.js +0 -2
  158. package/dist/preview/.next/static/chunks/684-0f1ef7361c499798.js +0 -1
  159. package/dist/preview/.next/static/chunks/684c6b30-0c65da32762fc4ee.js +0 -1
  160. package/dist/preview/.next/static/chunks/81-e7539b08d9d3fb4d.js +0 -1
  161. package/dist/preview/.next/static/chunks/883-70c8267c50bc4133.js +0 -1
  162. package/dist/preview/.next/static/chunks/921-d1dc8c63f49e85d6.js +0 -1
  163. package/dist/preview/.next/static/chunks/app/_not-found/page-03ce767859c36d4e.js +0 -1
  164. package/dist/preview/.next/static/chunks/app/layout-7cf14e28880544f1.js +0 -1
  165. package/dist/preview/.next/static/chunks/app/page-065cb49b0a078541.js +0 -1
  166. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-656510fd180c803c.js +0 -1
  167. package/dist/preview/.next/static/chunks/framework-2a724981073c3a29.js +0 -1
  168. package/dist/preview/.next/static/chunks/main-552b9719bbc3a274.js +0 -1
  169. package/dist/preview/.next/static/chunks/main-app-914a73336fd45af5.js +0 -1
  170. package/dist/preview/.next/static/chunks/pages/_app-77ca34bce25ac75c.js +0 -1
  171. package/dist/preview/.next/static/chunks/pages/_error-73f611c46abbb495.js +0 -1
  172. package/dist/preview/.next/static/css/2df96d9ee014e8de.css +0 -3
  173. /package/dist/preview/.next/static/{B4EYZiVzdylEG9lAIl-aO → gFk9UfWL8joM4iD7-wlKF}/_ssgManifest.js +0 -0
@@ -0,0 +1,50 @@
1
+ import { motion } from 'framer-motion';
2
+ import { cn } from '../../utils';
3
+ import { Tooltip } from '../tooltip';
4
+
5
+ interface ToolbarButtonProps extends React.ComponentProps<'button'> {
6
+ children: React.ReactNode;
7
+ active?: boolean;
8
+ tooltip?: React.ReactNode;
9
+ }
10
+
11
+ export const ToolbarButton = ({
12
+ children,
13
+ className,
14
+ active,
15
+ tooltip,
16
+ ...props
17
+ }: ToolbarButtonProps) => {
18
+ return (
19
+ <Tooltip.Provider>
20
+ <Tooltip>
21
+ <Tooltip.Trigger asChild>
22
+ <button
23
+ type="button"
24
+ {...props}
25
+ className={cn(
26
+ 'h-full w-fit font-medium flex text-sm text-slate-10 items-center align-middle justify-center px-1 py-2 gap-1 relative',
27
+ 'hover:text-slate-12 transition-colors',
28
+ active && 'data-[state=active]:text-cyan-11',
29
+ className,
30
+ )}
31
+ >
32
+ {children}
33
+ {active ? (
34
+ <motion.span
35
+ className="-bottom-px absolute rounded-sm left-0 w-full bg-cyan-11 h-px"
36
+ layoutId="active-toolbar-button"
37
+ transition={{
38
+ type: 'spring',
39
+ bounce: 0.2,
40
+ duration: 0.6,
41
+ }}
42
+ />
43
+ ) : null}
44
+ </button>
45
+ </Tooltip.Trigger>
46
+ {tooltip ? <Tooltip.Content>{tooltip}</Tooltip.Content> : null}
47
+ </Tooltip>
48
+ </Tooltip.Provider>
49
+ );
50
+ };
@@ -0,0 +1,33 @@
1
+ import { useSyncExternalStore } from 'react';
2
+
3
+ export const useCachedState = <T>(key: string) => {
4
+ let value: T | undefined = undefined;
5
+ if ('localStorage' in global) {
6
+ const storedValue = global.localStorage.getItem(key);
7
+ if (storedValue !== null) {
8
+ try {
9
+ value = JSON.parse(storedValue) as T;
10
+ } catch (exception) {
11
+ console.warn(
12
+ 'Failed to load stored value for',
13
+ key,
14
+ 'with value',
15
+ value,
16
+ );
17
+ }
18
+ }
19
+ }
20
+
21
+ return [
22
+ useSyncExternalStore(
23
+ () => () => {},
24
+ () => value,
25
+ () => undefined,
26
+ ),
27
+ function setValue(newValue: T | undefined) {
28
+ if ('localStorage' in global) {
29
+ global.localStorage.setItem(key, JSON.stringify(newValue));
30
+ }
31
+ },
32
+ ] as const;
33
+ };
@@ -1,90 +1,53 @@
1
+ 'use client';
1
2
  import * as Tabs from '@radix-ui/react-tabs';
2
- import { LayoutGroup, motion } from 'framer-motion';
3
+ import { LayoutGroup } from 'framer-motion';
3
4
  import { usePathname, useRouter, useSearchParams } from 'next/navigation';
4
- import { useEffect } from 'react';
5
+ import { use, useEffect } from 'react';
6
+ import { isBuilding } from '../app/env';
7
+ import { PreviewContext } from '../contexts/preview';
5
8
  import { cn } from '../utils';
6
9
  import { IconArrowDown } from './icons/icon-arrow-down';
7
10
  import { IconReload } from './icons/icon-reload';
8
11
  import { IconScanner } from './icons/icon-scanner';
9
12
  import { IconScissors } from './icons/icon-scissors';
10
- import { Linter, useLinter } from './toolbar/linter';
11
- import { SpamAssassin, useSpamAssassin } from './toolbar/spam-assassin';
12
- import { Tooltip } from './tooltip';
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';
13
21
 
14
- type ToolbarProps = React.ComponentProps<'div'> & {
15
- emailSlug: string;
16
- markup: string;
17
- plainText: string;
18
- };
19
-
20
- type ActivePanelValue = 'linter' | 'spam-assassin';
22
+ export type ToolbarTabValue = 'linter' | 'spam-assassin';
21
23
 
22
- interface ToolbarButton extends React.ComponentProps<'button'> {
23
- children: React.ReactNode;
24
- active?: boolean;
25
- tooltip?: React.ReactNode;
26
- }
27
-
28
- const ToolbarButton = ({
29
- children,
30
- className,
31
- active,
32
- tooltip,
33
- ...props
34
- }: ToolbarButton) => {
35
- return (
36
- <Tooltip.Provider>
37
- <Tooltip>
38
- <Tooltip.Trigger asChild>
39
- <button
40
- type="button"
41
- {...props}
42
- className={cn(
43
- 'h-full w-fit font-medium flex text-sm text-slate-10 items-center align-middle justify-center px-1 py-2 gap-1 relative',
44
- 'hover:text-slate-12 transition-colors',
45
- active && 'data-[state=active]:text-cyan-11',
46
- className,
47
- )}
48
- >
49
- {children}
50
- {active ? (
51
- <motion.span
52
- className="-bottom-px absolute rounded-sm left-0 w-full bg-cyan-11 h-px"
53
- layoutId="active-toolbar-button"
54
- transition={{
55
- type: 'spring',
56
- bounce: 0.2,
57
- duration: 0.6,
58
- }}
59
- />
60
- ) : null}
61
- </button>
62
- </Tooltip.Trigger>
63
- {tooltip ? <Tooltip.Content>{tooltip}</Tooltip.Content> : null}
64
- </Tooltip>
65
- </Tooltip.Provider>
66
- );
67
- };
24
+ const ToolbarInner = ({
25
+ serverLintingRows,
26
+ serverSpamCheckingResult,
68
27
 
69
- export const Toolbar = ({
70
- emailSlug,
71
28
  markup,
29
+ reactMarkup,
72
30
  plainText,
73
- className,
74
- ...rest
75
- }: ToolbarProps) => {
31
+ emailPath,
32
+ emailSlug,
33
+ }: ToolbarProps & {
34
+ markup: string;
35
+ reactMarkup: string;
36
+ plainText: string;
37
+ emailSlug: string;
38
+ emailPath: string;
39
+ }) => {
76
40
  const pathname = usePathname();
77
41
  const searchParams = useSearchParams();
78
42
  const router = useRouter();
79
43
 
80
- const activePanelValue = (searchParams.get('toolbar-panel') ?? undefined) as
81
- | ActivePanelValue
44
+ const activeTab = (searchParams.get('toolbar-panel') ?? undefined) as
45
+ | ToolbarTabValue
82
46
  | undefined;
83
47
 
84
- const toggled = activePanelValue !== undefined;
48
+ const toggled = activeTab !== undefined;
85
49
 
86
- const setActivePanelValue = (newValue: ActivePanelValue | undefined) => {
87
- console.log(newValue);
50
+ const setActivePanelValue = (newValue: ToolbarTabValue | undefined) => {
88
51
  const params = new URLSearchParams(searchParams);
89
52
  if (newValue === undefined) {
90
53
  params.delete('toolbar-panel');
@@ -94,36 +57,52 @@ export const Toolbar = ({
94
57
  router.push(`${pathname}?${params.toString()}`);
95
58
  };
96
59
 
60
+ const [cachedSpamCheckingResult, setCachedSpamCheckingResult] =
61
+ useCachedState<SpamCheckingResult>(
62
+ `spam-assassin-${emailSlug.replaceAll('/', '-')}`,
63
+ );
97
64
  const [spamCheckingResult, { load: loadSpamChecking }] = useSpamAssassin({
98
- slug: emailSlug,
99
65
  markup,
100
66
  plainText,
67
+
68
+ initialResult: serverSpamCheckingResult ?? cachedSpamCheckingResult,
101
69
  });
102
70
 
103
- const [lintingResults, { load: loadLinting }] = useLinter({
104
- slug: emailSlug,
71
+ const [cachedLintingRows, setCachedLintingRows] = useCachedState<
72
+ LintingRow[]
73
+ >(`linter-${emailSlug.replaceAll('/', '-')}`);
74
+ const [lintingRows, { load: loadLinting }] = useLinter({
75
+ reactMarkup,
76
+ emailPath,
105
77
  markup,
78
+
79
+ initialRows: serverLintingRows ?? cachedLintingRows,
106
80
  });
107
81
 
108
- useEffect(() => {
109
- loadLinting();
110
- loadSpamChecking();
111
- }, []);
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
+ }
112
93
 
113
94
  return (
114
95
  <div
115
- {...rest}
116
96
  data-toggled={toggled}
117
97
  className={cn(
118
98
  'bg-black group/toolbar text-xs text-slate-11 h-48 transition-all',
119
99
  'data-[toggled=false]:h-8',
120
- className,
121
100
  )}
122
101
  >
123
102
  <Tabs.Root
124
- value={activePanelValue}
103
+ value={activeTab}
125
104
  onValueChange={(newValue) => {
126
- setActivePanelValue(newValue as ActivePanelValue);
105
+ setActivePanelValue(newValue as ToolbarTabValue);
127
106
  }}
128
107
  asChild
129
108
  >
@@ -131,38 +110,40 @@ export const Toolbar = ({
131
110
  <Tabs.List className="flex gap-4 px-2 border-b border-solid border-slate-6 h-7 w-full">
132
111
  <LayoutGroup id="toolbar">
133
112
  <Tabs.Trigger asChild value="spam-assassin">
134
- <ToolbarButton active={activePanelValue === 'spam-assassin'}>
113
+ <ToolbarButton active={activeTab === 'spam-assassin'}>
135
114
  <IconScissors />
136
115
  Spam Assassin
137
116
  </ToolbarButton>
138
117
  </Tabs.Trigger>
139
118
  <Tabs.Trigger asChild value="linter">
140
- <ToolbarButton active={activePanelValue === 'linter'}>
119
+ <ToolbarButton active={activeTab === 'linter'}>
141
120
  <IconScanner />
142
121
  Linter
143
122
  </ToolbarButton>
144
123
  </Tabs.Trigger>
145
124
  </LayoutGroup>
146
125
  <div className="flex gap-1 ml-auto">
147
- <ToolbarButton
148
- tooltip="Reload"
149
- onClick={() => {
150
- if (activePanelValue === 'spam-assassin') {
151
- void loadSpamChecking();
152
- } else if (activePanelValue === 'linter') {
153
- void loadLinting();
154
- } else {
155
- setActivePanelValue('linter');
156
- void loadLinting();
157
- }
158
- }}
159
- >
160
- <IconReload />
161
- </ToolbarButton>
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
+ )}
162
143
  <ToolbarButton
163
144
  tooltip="Toggle toolbar"
164
145
  onClick={() => {
165
- if (activePanelValue === undefined) {
146
+ if (activeTab === undefined) {
166
147
  setActivePanelValue('linter');
167
148
  } else {
168
149
  setActivePanelValue(undefined);
@@ -176,7 +157,7 @@ export const Toolbar = ({
176
157
 
177
158
  <div className="flex-grow transition-opacity opacity-100 group-data-[toggled=false]/toolbar:opacity-0 overflow-y-auto px-2">
178
159
  <Tabs.Content value="linter">
179
- <Linter results={lintingResults} />
160
+ <Linter rows={lintingRows} />
180
161
  </Tabs.Content>
181
162
  <Tabs.Content value="spam-assassin">
182
163
  <SpamAssassin result={spamCheckingResult} />
@@ -187,3 +168,30 @@ export const Toolbar = ({
187
168
  </div>
188
169
  );
189
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,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';
@@ -7,17 +7,11 @@ import { ShellContext } from './shell';
7
7
  import { Tooltip } from './tooltip';
8
8
 
9
9
  interface TopbarProps {
10
- currentEmailOpenSlug: string;
11
- pathSeparator: string;
12
-
10
+ emailTitle: string;
13
11
  children: React.ReactNode;
14
12
  }
15
13
 
16
- export const Topbar = ({
17
- currentEmailOpenSlug,
18
- pathSeparator,
19
- children,
20
- }: TopbarProps) => {
14
+ export const Topbar = ({ emailTitle, children }: TopbarProps) => {
21
15
  const { toggleSidebar } = use(ShellContext)!;
22
16
 
23
17
  return (
@@ -40,7 +34,7 @@ export const Topbar = ({
40
34
  </Tooltip>
41
35
  <div className="hidden items-center overflow-hidden text-center lg:flex">
42
36
  <Heading as="h2" className="truncate" size="2" weight="medium">
43
- {currentEmailOpenSlug.split(pathSeparator).pop()}
37
+ {emailTitle}
44
38
  </Heading>
45
39
  </div>
46
40
  </div>
@@ -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>"`;