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

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 (110) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cli/index.js +18 -13
  3. package/dist/cli/index.mjs +26 -21
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +14 -13
  6. package/dist/preview/.next/build-manifest.json +3 -3
  7. package/dist/preview/.next/cache/.rscinfo +1 -1
  8. package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
  9. package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
  10. package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
  11. package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
  12. package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
  13. package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
  14. package/dist/preview/.next/next-server.js.nft.json +1 -1
  15. package/dist/preview/.next/prerender-manifest.json +3 -3
  16. package/dist/preview/.next/required-server-files.json +3 -3
  17. package/dist/preview/.next/server/app/_not-found/page.js +1 -1
  18. package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  19. package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
  20. package/dist/preview/.next/server/app/page.js +1 -1
  21. package/dist/preview/.next/server/app/page.js.nft.json +1 -1
  22. package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
  23. package/dist/preview/.next/server/app/preview/[...slug]/page.js +133 -25
  24. package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  25. package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  26. package/dist/preview/.next/server/chunks/42.js +1 -0
  27. package/dist/preview/.next/server/chunks/600.js +3 -3
  28. package/dist/preview/.next/server/chunks/{171.js → 816.js} +6 -6
  29. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  30. package/dist/preview/.next/server/next-font-manifest.js +1 -1
  31. package/dist/preview/.next/server/next-font-manifest.json +1 -1
  32. package/dist/preview/.next/server/pages/500.html +1 -1
  33. package/dist/preview/.next/server/pages-manifest.json +1 -1
  34. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  35. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  36. package/dist/preview/.next/static/chunks/287-7864b805e6bdc854.js +1 -0
  37. package/dist/preview/.next/static/chunks/412-31817e53b50a3e73.js +1 -0
  38. package/dist/preview/.next/static/chunks/683-b769e5d91bdf9a82.js +1 -0
  39. package/dist/preview/.next/static/chunks/880-9c0b721328117b8b.js +1 -0
  40. package/dist/preview/.next/static/chunks/app/layout-7dee682873546401.js +1 -0
  41. package/dist/preview/.next/static/chunks/app/page-9ea0bd45cd6294b0.js +1 -0
  42. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-a610d641c64448cc.js +1 -0
  43. package/dist/preview/.next/static/chunks/{main-app-c2e686acf8d370d7.js → main-app-256b213b179a95cc.js} +1 -1
  44. package/dist/preview/.next/static/css/e68ebc9bb8f7b3f4.css +3 -0
  45. package/dist/preview/.next/trace +26 -26
  46. package/dist/preview/.next/types/app/layout.ts +1 -1
  47. package/dist/preview/.next/types/app/page.ts +84 -0
  48. package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
  49. package/package.json +1 -1
  50. package/src/actions/email-validation/check-compatibility.ts +16 -5
  51. package/src/actions/email-validation/check-images.spec.tsx +13 -11
  52. package/src/actions/email-validation/check-images.ts +6 -0
  53. package/src/actions/email-validation/check-links.spec.tsx +23 -11
  54. package/src/actions/email-validation/check-links.ts +6 -0
  55. package/src/actions/email-validation/get-code-location-from-ast-element.ts +18 -0
  56. package/src/actions/render-email-by-path.tsx +2 -2
  57. package/src/app/env.ts +3 -0
  58. package/src/app/preview/[...slug]/page.tsx +24 -11
  59. package/src/app/preview/[...slug]/preview.tsx +15 -12
  60. package/src/components/code-container.tsx +90 -71
  61. package/src/components/code.tsx +106 -42
  62. package/src/components/icons/icon-info.tsx +18 -0
  63. package/src/components/icons/icon-reload.tsx +13 -14
  64. package/src/components/logo.tsx +3 -2
  65. package/src/components/resizable-wrapper.tsx +1 -4
  66. package/src/components/sidebar/file-tree-directory-children.tsx +13 -2
  67. package/src/components/sidebar/file-tree-directory.tsx +26 -18
  68. package/src/components/sidebar/file-tree.tsx +2 -2
  69. package/src/components/sidebar/sidebar.tsx +16 -19
  70. package/src/components/toolbar/code-preview-line-link.tsx +40 -0
  71. package/src/components/toolbar/compatibility.tsx +113 -0
  72. package/src/components/toolbar/linter.tsx +69 -111
  73. package/src/components/toolbar/results.tsx +5 -2
  74. package/src/components/toolbar/spam-assassin.tsx +31 -20
  75. package/src/components/toolbar/toolbar-button.tsx +4 -2
  76. package/src/components/toolbar/use-cached-state.ts +2 -2
  77. package/src/components/toolbar.tsx +152 -30
  78. package/src/components/tooltip-content.tsx +1 -1
  79. package/src/components/topbar/view-size-controls.tsx +1 -2
  80. package/src/components/topbar.tsx +1 -20
  81. package/src/contexts/fragment-identifier.tsx +46 -0
  82. package/src/hooks/use-fragment-identifier.ts +14 -0
  83. package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
  84. package/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap +74 -0
  85. package/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap +24 -0
  86. package/src/utils/caniemail/ast/get-object-variables.spec.ts +19 -0
  87. package/src/utils/caniemail/ast/get-used-style-properties.spec.ts +23 -0
  88. package/src/utils/caniemail/get-css-property-with-value.ts +2 -2
  89. package/src/utils/caniemail/tailwind/get-tailwind-config.ts +0 -2
  90. package/src/utils/get-email-component.ts +1 -1
  91. package/src/utils/get-line-and-column-from-offset.spec.ts +11 -0
  92. package/src/utils/get-line-and-column-from-offset.ts +11 -0
  93. package/src/utils/index.ts +1 -0
  94. package/src/utils/linting.ts +5 -30
  95. package/src/utils/load-stream.ts +15 -0
  96. package/src/utils/sanitize.ts +6 -0
  97. package/dist/preview/.next/server/chunks/833.js +0 -1
  98. package/dist/preview/.next/static/chunks/416-56f79fc7e689f06f.js +0 -1
  99. package/dist/preview/.next/static/chunks/683-8bbfd191e5105f01.js +0 -1
  100. package/dist/preview/.next/static/chunks/87-38e35f08507de015.js +0 -1
  101. package/dist/preview/.next/static/chunks/app/layout-a6640e62690d8fd6.js +0 -1
  102. package/dist/preview/.next/static/chunks/app/page-ba68f50b287e7478.js +0 -1
  103. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-4a5b026ab543e27f.js +0 -1
  104. package/dist/preview/.next/static/css/d7df9cfc3e182163.css +0 -3
  105. package/src/actions/email-validation/get-line-and-column-from-index.spec.ts +0 -22
  106. package/src/actions/email-validation/get-line-and-column-from-index.ts +0 -43
  107. package/src/components/icons/icon-scanner.tsx +0 -19
  108. package/src/components/icons/icon-scissors.tsx +0 -19
  109. /package/dist/preview/.next/static/{gFk9UfWL8joM4iD7-wlKF → SoPVDfPAp9R983pBBriVn}/_buildManifest.js +0 -0
  110. /package/dist/preview/.next/static/{gFk9UfWL8joM4iD7-wlKF → SoPVDfPAp9R983pBBriVn}/_ssgManifest.js +0 -0
@@ -3,13 +3,15 @@ import * as Tabs from '@radix-ui/react-tabs';
3
3
  import { LayoutGroup } from 'framer-motion';
4
4
  import { usePathname, useRouter, useSearchParams } from 'next/navigation';
5
5
  import { use, useEffect } from 'react';
6
+ import type { CompatibilityCheckingResult } from '../actions/email-validation/check-compatibility';
6
7
  import { isBuilding } from '../app/env';
7
8
  import { PreviewContext } from '../contexts/preview';
8
9
  import { cn } from '../utils';
9
10
  import { IconArrowDown } from './icons/icon-arrow-down';
11
+ import { IconCheck } from './icons/icon-check';
12
+ import { IconInfo } from './icons/icon-info';
10
13
  import { IconReload } from './icons/icon-reload';
11
- import { IconScanner } from './icons/icon-scanner';
12
- import { IconScissors } from './icons/icon-scissors';
14
+ import { Compatibility, useCompatibility } from './toolbar/compatibility';
13
15
  import { Linter, type LintingRow, useLinter } from './toolbar/linter';
14
16
  import {
15
17
  SpamAssassin,
@@ -19,11 +21,12 @@ import {
19
21
  import { ToolbarButton } from './toolbar/toolbar-button';
20
22
  import { useCachedState } from './toolbar/use-cached-state';
21
23
 
22
- export type ToolbarTabValue = 'linter' | 'spam-assassin';
24
+ export type ToolbarTabValue = 'linter' | 'compatibility' | 'spam-assassin';
23
25
 
24
26
  const ToolbarInner = ({
25
27
  serverLintingRows,
26
28
  serverSpamCheckingResult,
29
+ serverCompatibilityResults,
27
30
 
28
31
  markup,
29
32
  reactMarkup,
@@ -54,30 +57,42 @@ const ToolbarInner = ({
54
57
  } else {
55
58
  params.set('toolbar-panel', newValue);
56
59
  }
57
- router.push(`${pathname}?${params.toString()}`);
60
+ router.push(`${pathname}?${params.toString()}${location.hash}`);
58
61
  };
59
62
 
60
63
  const [cachedSpamCheckingResult, setCachedSpamCheckingResult] =
61
64
  useCachedState<SpamCheckingResult>(
62
65
  `spam-assassin-${emailSlug.replaceAll('/', '-')}`,
63
66
  );
64
- const [spamCheckingResult, { load: loadSpamChecking }] = useSpamAssassin({
65
- markup,
66
- plainText,
67
+ const [spamCheckingResult, { load: loadSpamChecking, loading: spamLoading }] =
68
+ useSpamAssassin({
69
+ markup,
70
+ plainText,
67
71
 
68
- initialResult: serverSpamCheckingResult ?? cachedSpamCheckingResult,
69
- });
72
+ initialResult: serverSpamCheckingResult ?? cachedSpamCheckingResult,
73
+ });
70
74
 
71
75
  const [cachedLintingRows, setCachedLintingRows] = useCachedState<
72
76
  LintingRow[]
73
77
  >(`linter-${emailSlug.replaceAll('/', '-')}`);
74
- const [lintingRows, { load: loadLinting }] = useLinter({
75
- reactMarkup,
76
- emailPath,
78
+ const [lintingRows, { load: loadLinting, loading: lintLoading }] = useLinter({
77
79
  markup,
78
80
 
79
81
  initialRows: serverLintingRows ?? cachedLintingRows,
80
82
  });
83
+ const [cachedCompatibilityResults, setCachedCompatibilityResults] =
84
+ useCachedState<CompatibilityCheckingResult[]>(
85
+ `compatibility-${emailSlug.replaceAll('/', '-')}`,
86
+ );
87
+ const [
88
+ compatibilityCheckingResults,
89
+ { load: loadCompatibility, loading: compatibilityLoading },
90
+ ] = useCompatibility({
91
+ emailPath,
92
+ reactMarkup,
93
+
94
+ initialResults: serverCompatibilityResults ?? cachedCompatibilityResults,
95
+ });
81
96
 
82
97
  if (!isBuilding) {
83
98
  useEffect(() => {
@@ -87,6 +102,9 @@ const ToolbarInner = ({
87
102
 
88
103
  const spamCheckingResult = await loadSpamChecking();
89
104
  setCachedSpamCheckingResult(spamCheckingResult);
105
+
106
+ const compatibilityCheckingResults = await loadCompatibility();
107
+ setCachedCompatibilityResults(compatibilityCheckingResults);
90
108
  })();
91
109
  }, []);
92
110
  }
@@ -95,49 +113,76 @@ const ToolbarInner = ({
95
113
  <div
96
114
  data-toggled={toggled}
97
115
  className={cn(
98
- 'bg-black group/toolbar text-xs text-slate-11 h-48 transition-all',
99
- 'data-[toggled=false]:h-8',
116
+ 'absolute bottom-0 left-0 right-0',
117
+ 'bg-black group/toolbar text-xs text-slate-11 h-52 transition-transform',
118
+ 'data-[toggled=false]:translate-y-[170px]',
100
119
  )}
101
120
  >
102
121
  <Tabs.Root
103
- value={activeTab}
122
+ value={activeTab ?? ''}
104
123
  onValueChange={(newValue) => {
105
124
  setActivePanelValue(newValue as ToolbarTabValue);
106
125
  }}
107
126
  asChild
108
127
  >
109
128
  <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">
129
+ <Tabs.List className="flex gap-4 px-4 border-b border-solid border-slate-6 h-10 w-full flex-shrink-0">
111
130
  <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
131
  <Tabs.Trigger asChild value="linter">
119
132
  <ToolbarButton active={activeTab === 'linter'}>
120
- <IconScanner />
121
133
  Linter
122
134
  </ToolbarButton>
123
135
  </Tabs.Trigger>
136
+ <Tabs.Trigger asChild value="compatibility">
137
+ <ToolbarButton active={activeTab === 'compatibility'}>
138
+ Compatibility
139
+ </ToolbarButton>
140
+ </Tabs.Trigger>
141
+ <Tabs.Trigger asChild value="spam-assassin">
142
+ <ToolbarButton active={activeTab === 'spam-assassin'}>
143
+ Spam
144
+ </ToolbarButton>
145
+ </Tabs.Trigger>
124
146
  </LayoutGroup>
125
- <div className="flex gap-1 ml-auto">
147
+ <div className="flex gap-0.5 ml-auto">
148
+ <ToolbarButton
149
+ delayDuration={0}
150
+ tooltip={
151
+ (activeTab === 'linter' &&
152
+ 'The Linter tab checks all the images and links for common issues like missing alt text, broken URLs, insecure HTTP methods, and more.') ||
153
+ (activeTab === 'spam-assassin' &&
154
+ 'The Spam tab will look at the content and use a robust scoring framework to determine if the email is likely to be spam. Powered by SpamAssassin.') ||
155
+ (activeTab === 'compatibility' &&
156
+ 'The Compatibility tab shows how well the HTML/CSS is supported across mail clients like Outlook, Gmail, etc. Powered by Can I Email.') ||
157
+ 'Info'
158
+ }
159
+ >
160
+ <IconInfo size={24} />
161
+ </ToolbarButton>
126
162
  {isBuilding ? null : (
127
163
  <ToolbarButton
128
164
  tooltip="Reload"
165
+ disabled={lintLoading || spamLoading}
129
166
  onClick={async () => {
130
167
  if (activeTab === undefined) {
131
168
  setActivePanelValue('linter');
132
169
  }
133
170
  if (activeTab === 'spam-assassin') {
134
171
  await loadSpamChecking();
135
- } else {
172
+ } else if (activeTab === 'linter') {
136
173
  await loadLinting();
174
+ } else if (activeTab === 'compatibility') {
175
+ await loadCompatibility();
137
176
  }
138
177
  }}
139
178
  >
140
- <IconReload />
179
+ <IconReload
180
+ size={24}
181
+ className={cn({
182
+ 'animate-spin opacity-60 animate-spin-fast':
183
+ lintLoading || spamLoading,
184
+ })}
185
+ />
141
186
  </ToolbarButton>
142
187
  )}
143
188
  <ToolbarButton
@@ -150,17 +195,65 @@ const ToolbarInner = ({
150
195
  }
151
196
  }}
152
197
  >
153
- <IconArrowDown className="transition-transform group-data-[toggled=false]/toolbar:rotate-180" />
198
+ <IconArrowDown
199
+ size={24}
200
+ className="transition-transform group-data-[toggled=false]/toolbar:rotate-180"
201
+ />
154
202
  </ToolbarButton>
155
203
  </div>
156
204
  </Tabs.List>
157
205
 
158
- <div className="flex-grow transition-opacity opacity-100 group-data-[toggled=false]/toolbar:opacity-0 overflow-y-auto px-2">
206
+ <div className="flex-grow transition-opacity opacity-100 group-data-[toggled=false]/toolbar:opacity-0 overflow-y-auto px-2 pt-3">
159
207
  <Tabs.Content value="linter">
160
- <Linter rows={lintingRows} />
208
+ {lintLoading ? (
209
+ <div className="animate-pulse text-slate-11 text-sm pt-1">
210
+ Running linting...
211
+ </div>
212
+ ) : lintingRows?.length === 0 ? (
213
+ <div className="flex flex-col items-center justify-center pt-8">
214
+ <SuccessIcon />
215
+ <SuccessTitle>All good</SuccessTitle>
216
+ <SuccessDescription>
217
+ No linting issues found.
218
+ </SuccessDescription>
219
+ </div>
220
+ ) : (
221
+ <Linter rows={lintingRows ?? []} />
222
+ )}
223
+ </Tabs.Content>
224
+ <Tabs.Content value="compatibility">
225
+ {compatibilityLoading ? (
226
+ <div className="animate-pulse text-slate-11 text-sm pt-1">
227
+ Running compatibility check...
228
+ </div>
229
+ ) : compatibilityCheckingResults?.length === 0 ? (
230
+ <div className="flex flex-col items-center justify-center py-8 px-4 my-4">
231
+ <SuccessIcon />
232
+ <SuccessTitle>Great compatibility</SuccessTitle>
233
+ <SuccessDescription>
234
+ It should render properly everywhere.
235
+ </SuccessDescription>
236
+ </div>
237
+ ) : (
238
+ <Compatibility results={compatibilityCheckingResults ?? []} />
239
+ )}
161
240
  </Tabs.Content>
162
241
  <Tabs.Content value="spam-assassin">
163
- <SpamAssassin result={spamCheckingResult} />
242
+ {spamLoading ? (
243
+ <div className="animate-pulse text-slate-11 text-sm pt-1">
244
+ Running spam check...
245
+ </div>
246
+ ) : spamCheckingResult?.isSpam === false ? (
247
+ <div className="flex flex-col items-center justify-center py-4 px-4 my-4">
248
+ <SuccessIcon />
249
+ <SuccessTitle>10/10</SuccessTitle>
250
+ <SuccessDescription>
251
+ Your email is clean of abuse indicators.
252
+ </SuccessDescription>
253
+ </div>
254
+ ) : (
255
+ <SpamAssassin result={spamCheckingResult} />
256
+ )}
164
257
  </Tabs.Content>
165
258
  </div>
166
259
  </div>
@@ -169,14 +262,42 @@ const ToolbarInner = ({
169
262
  );
170
263
  };
171
264
 
265
+ const SuccessIcon = () => {
266
+ return (
267
+ <div className="relative mb-8 flex items-center justify-center">
268
+ <div className="h-16 w-16 rounded-full bg-gradient-to-br from-green-300/20 opacity-80 to-emerald-500/30 blur-md absolute m-auto left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
269
+ <div className="h-12 w-12 rounded-full bg-gradient-to-br from-green-400/80 opacity-10 to-emerald-600/80 absolute m-auto left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 shadow-lg" />
270
+ <div className="h-10 w-10 rounded-full bg-gradient-to-br from-green-400 to-emerald-600 flex items-center justify-center absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 shadow-[inset_0_1px_1px_rgba(255,255,255,0.4)]">
271
+ <IconCheck size={24} className="text-white drop-shadow-sm" />
272
+ </div>
273
+ </div>
274
+ );
275
+ };
276
+
277
+ const SuccessTitle = ({ children }) => {
278
+ return (
279
+ <h3 className="text-slate-12 font-medium text-base mb-1">{children}</h3>
280
+ );
281
+ };
282
+
283
+ const SuccessDescription = ({ children }) => {
284
+ return (
285
+ <p className="text-slate-11 text-sm text-center max-w-[300px]">
286
+ {children}
287
+ </p>
288
+ );
289
+ };
290
+
172
291
  interface ToolbarProps {
173
292
  serverSpamCheckingResult: SpamCheckingResult | undefined;
174
293
  serverLintingRows: LintingRow[] | undefined;
294
+ serverCompatibilityResults: CompatibilityCheckingResult[] | undefined;
175
295
  }
176
296
 
177
297
  export const Toolbar = ({
178
298
  serverLintingRows,
179
299
  serverSpamCheckingResult,
300
+ serverCompatibilityResults,
180
301
  }: ToolbarProps) => {
181
302
  const { emailPath, emailSlug, renderedEmailMetadata } = use(PreviewContext)!;
182
303
 
@@ -192,6 +313,7 @@ export const Toolbar = ({
192
313
  plainText={plainText}
193
314
  serverLintingRows={serverLintingRows}
194
315
  serverSpamCheckingResult={serverSpamCheckingResult}
316
+ serverCompatibilityResults={serverCompatibilityResults}
195
317
  />
196
318
  );
197
319
  };
@@ -18,7 +18,7 @@ export const TooltipContent = React.forwardRef<
18
18
  {...props}
19
19
  className={cn(
20
20
  'z-20 rounded-md border border-slate-6 bg-black px-3 py-2 text-white text-xs',
21
- 'font-sans',
21
+ 'font-sans max-w-60',
22
22
  )}
23
23
  ref={forwardedRef}
24
24
  sideOffset={sideOffset}
@@ -1,4 +1,3 @@
1
- 'use client';
2
1
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
3
2
  import { motion } from 'framer-motion';
4
3
  import * as React from 'react';
@@ -124,7 +123,7 @@ const PresetMenuItem = ({
124
123
  onClick={() => onSelect(dimensions)}
125
124
  >
126
125
  {name}
127
- <span className="flex h-fit items-center rounded-full bg-slate-6 px-1.5 py-0.5 font-bold text-white text-xs">
126
+ <span className="flex h-fit items-center rounded-full bg-slate-6 px-2 py-1 font-medium text-slate-11 text-xs">
128
127
  {dimensions.width}x{dimensions.height}
129
128
  </span>
130
129
  </DropdownMenu.Item>
@@ -16,7 +16,7 @@ export const Topbar = ({ emailTitle, children }: TopbarProps) => {
16
16
 
17
17
  return (
18
18
  <Tooltip.Provider>
19
- <header className="relative flex h-[3.3125rem] items-center justify-between gap-3 border-slate-6 border-b px-3">
19
+ <header className="relative flex h-[3.3125rem] items-center justify-between gap-3 border-slate-6 border-b px-3 py-2">
20
20
  <div className="relative flex w-fit items-center gap-3">
21
21
  <Tooltip>
22
22
  <Tooltip.Trigger asChild>
@@ -40,25 +40,6 @@ export const Topbar = ({ emailTitle, children }: TopbarProps) => {
40
40
  </div>
41
41
  <div className="flex w-full items-center justify-between gap-3 lg:w-fit lg:justify-start">
42
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} */}
62
43
  </div>
63
44
  </header>
64
45
  </Tooltip.Provider>
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+ import { usePathname, useSearchParams } from 'next/navigation';
3
+ import { createContext, use, useEffect, useState } from 'react';
4
+
5
+ export const FragmentIdentifierContext = createContext<
6
+ | {
7
+ identifier: string | undefined;
8
+
9
+ update(value: string): void;
10
+ }
11
+ | undefined
12
+ >(undefined);
13
+
14
+ export const useFragmentIdentifier = () => {
15
+ const value = use(FragmentIdentifierContext);
16
+ return value?.identifier;
17
+ };
18
+
19
+ export const FragmentIdentifierProvider = ({
20
+ children,
21
+ }: { children: React.ReactNode }) => {
22
+ const [fragmentIdentifier, setFragmentIdentifier] = useState<string>();
23
+ const pathname = usePathname();
24
+ const searchParams = useSearchParams();
25
+
26
+ const update = () => {
27
+ setFragmentIdentifier(location.hash);
28
+ };
29
+
30
+ useEffect(() => {
31
+ update();
32
+ }, [pathname, searchParams]);
33
+
34
+ return (
35
+ <FragmentIdentifierContext.Provider
36
+ value={{
37
+ identifier: fragmentIdentifier,
38
+ update(value: string) {
39
+ setFragmentIdentifier(value);
40
+ },
41
+ }}
42
+ >
43
+ {children}
44
+ </FragmentIdentifierContext.Provider>
45
+ );
46
+ };
@@ -0,0 +1,14 @@
1
+ import { usePathname, useSearchParams } from 'next/navigation';
2
+ import { useEffect, useState } from 'react';
3
+
4
+ export const useFragmentIdentifier = () => {
5
+ const pathname = usePathname();
6
+ const searchParams = useSearchParams();
7
+ const [fragmentIdentifier, setFragmentIdentifier] = useState<string>();
8
+
9
+ useEffect(() => {
10
+ setFragmentIdentifier(global.location?.hash);
11
+ }, [pathname, searchParams]);
12
+
13
+ return fragmentIdentifier;
14
+ };
@@ -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-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>"`;
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 Logo" 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 alt="alanturing&#x27;s profile picture" 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="Arrow indicating invitation" 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 alt="Enigma team logo" 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" 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" style="color:rgb(37,99,235);text-decoration-line:none" target="_blank">https://vercel.com</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>"`;
@@ -0,0 +1,74 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`getObjectVariables() 1`] = `
4
+ {
5
+ "buttonStyle": [
6
+ Node {
7
+ "computed": false,
8
+ "end": 91,
9
+ "key": Node {
10
+ "end": 84,
11
+ "loc": SourceLocation {
12
+ "end": Position {
13
+ "column": 14,
14
+ "index": 84,
15
+ "line": 5,
16
+ },
17
+ "filename": undefined,
18
+ "identifierName": "borderRadius",
19
+ "start": Position {
20
+ "column": 2,
21
+ "index": 72,
22
+ "line": 5,
23
+ },
24
+ },
25
+ "name": "borderRadius",
26
+ "start": 72,
27
+ "type": "Identifier",
28
+ },
29
+ "loc": SourceLocation {
30
+ "end": Position {
31
+ "column": 21,
32
+ "index": 91,
33
+ "line": 5,
34
+ },
35
+ "filename": undefined,
36
+ "identifierName": undefined,
37
+ "start": Position {
38
+ "column": 2,
39
+ "index": 72,
40
+ "line": 5,
41
+ },
42
+ },
43
+ "method": false,
44
+ "shorthand": false,
45
+ "start": 72,
46
+ "type": "ObjectProperty",
47
+ "value": Node {
48
+ "end": 91,
49
+ "extra": {
50
+ "raw": "'5px'",
51
+ "rawValue": "5px",
52
+ },
53
+ "loc": SourceLocation {
54
+ "end": Position {
55
+ "column": 21,
56
+ "index": 91,
57
+ "line": 5,
58
+ },
59
+ "filename": undefined,
60
+ "identifierName": undefined,
61
+ "start": Position {
62
+ "column": 16,
63
+ "index": 86,
64
+ "line": 5,
65
+ },
66
+ },
67
+ "start": 86,
68
+ "type": "StringLiteral",
69
+ "value": "5px",
70
+ },
71
+ },
72
+ ],
73
+ }
74
+ `;
@@ -0,0 +1,24 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`getUsedStyleProperties() 1`] = `
4
+ [
5
+ {
6
+ "location": SourceLocation {
7
+ "end": Position {
8
+ "column": 21,
9
+ "index": 91,
10
+ "line": 5,
11
+ },
12
+ "filename": undefined,
13
+ "identifierName": undefined,
14
+ "start": Position {
15
+ "column": 2,
16
+ "index": 72,
17
+ "line": 5,
18
+ },
19
+ },
20
+ "name": "borderRadius",
21
+ "value": "5px",
22
+ },
23
+ ]
24
+ `;
@@ -0,0 +1,19 @@
1
+ import { parse } from '@babel/parser';
2
+ import { getObjectVariables } from './get-object-variables';
3
+
4
+ test('getObjectVariables()', () => {
5
+ const reactCode = `
6
+ <Button style={buttonStyle}>Click me</Button>
7
+
8
+ const buttonStyle = {
9
+ borderRadius: '5px',
10
+ };
11
+ `;
12
+ const ast = parse(reactCode, {
13
+ strictMode: false,
14
+ errorRecovery: true,
15
+ sourceType: 'unambiguous',
16
+ plugins: ['jsx', 'typescript', 'decorators'],
17
+ });
18
+ expect(getObjectVariables(ast)).toMatchSnapshot();
19
+ });
@@ -0,0 +1,23 @@
1
+ import { parse } from '@babel/parser';
2
+ import { getObjectVariables } from './get-object-variables';
3
+ import { getUsedStyleProperties } from './get-used-style-properties';
4
+
5
+ test('getUsedStyleProperties()', async () => {
6
+ const reactCode = `
7
+ <Button style={buttonStyle}>Click me</Button>
8
+
9
+ const buttonStyle = {
10
+ borderRadius: '5px',
11
+ };
12
+ `;
13
+ const ast = parse(reactCode, {
14
+ strictMode: false,
15
+ errorRecovery: true,
16
+ sourceType: 'unambiguous',
17
+ plugins: ['jsx', 'typescript', 'decorators'],
18
+ });
19
+ const objectVariables = getObjectVariables(ast);
20
+ expect(
21
+ await getUsedStyleProperties(ast, reactCode, '', objectVariables),
22
+ ).toMatchSnapshot();
23
+ });
@@ -6,8 +6,8 @@ export const getCssPropertyWithValue = (title: string) => {
6
6
  if (match) {
7
7
  const [_full, propertyName, propertyValue] = match;
8
8
  return {
9
- name: propertyName,
10
- value: propertyValue,
9
+ name: propertyName!,
10
+ value: propertyValue!,
11
11
  };
12
12
  }
13
13
  return undefined;
@@ -133,8 +133,6 @@ const getConfigFromImport = async (
133
133
  });
134
134
  }
135
135
 
136
- console.log(configModule);
137
-
138
136
  if (
139
137
  typeof configModule.value === 'object' &&
140
138
  configModule.value !== null &&
@@ -12,7 +12,7 @@ import type { EmailTemplate as EmailComponent } from './types/email-template';
12
12
  import type { ErrorObject } from './types/error-object';
13
13
 
14
14
  const EmailComponentModule = z.object({
15
- default: z.function(),
15
+ default: z.any(),
16
16
  render: z.function(),
17
17
  reactEmailCreateReactElement: z.function(),
18
18
  });
@@ -0,0 +1,11 @@
1
+ import { getLineAndColumnFromOffset } from './get-line-and-column-from-offset';
2
+
3
+ test('getLineAndColumnFromOffset()', () => {
4
+ const content = `export default function MyEmail() {
5
+ return <div className="testing classes to make sure this is not removed" id="my-div" aria-label="my beautiful div">
6
+ inside the div, should also stay unchanged
7
+ </div>;
8
+ }`;
9
+ const offset = content.indexOf('className');
10
+ expect(getLineAndColumnFromOffset(offset, content)).toEqual([2, 15]);
11
+ });