react-email 3.0.6 → 4.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/cli/index.js +768 -763
  3. package/dist/cli/index.mjs +480 -476
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +14 -12
  6. package/dist/preview/.next/build-manifest.json +5 -5
  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 +1 -1
  16. package/dist/preview/.next/server/app/_not-found/page.js +1 -1
  17. package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/dist/preview/.next/server/app/page.js +1 -1
  19. package/dist/preview/.next/server/app/page.js.nft.json +1 -1
  20. package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
  21. package/dist/preview/.next/server/app/preview/[...slug]/page.js +6 -6
  22. package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  23. package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  24. package/dist/preview/.next/server/chunks/273.js +1 -0
  25. package/dist/preview/.next/server/chunks/594.js +10 -0
  26. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  27. package/dist/preview/.next/server/pages/500.html +1 -1
  28. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  29. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  30. package/dist/preview/.next/server/webpack-runtime.js +1 -1
  31. package/dist/preview/.next/static/chunks/18b16e15-6ad9b58e10ff8891.js +1 -0
  32. package/dist/preview/.next/static/chunks/490-48951f2e19ae3aef.js +1 -0
  33. package/dist/preview/.next/static/chunks/600-2e2ca4c8bbd97b61.js +1 -0
  34. package/dist/preview/.next/static/chunks/app/{layout-a2901ed1c2c53661.js → layout-490964e2c3604d33.js} +1 -1
  35. package/dist/preview/.next/static/chunks/app/page-d2432acd08db8fc0.js +1 -0
  36. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-f4e211e00c026401.js +1 -0
  37. package/dist/preview/.next/static/chunks/webpack-7bf1ffb05f5540be.js +1 -0
  38. package/dist/preview/.next/static/css/5e0736cafbb392a9.css +3 -0
  39. package/dist/preview/.next/trace +21 -21
  40. package/package.json +12 -7
  41. package/postcss.config.js +1 -1
  42. package/src/actions/email-validation/check-links.ts +88 -0
  43. package/src/actions/email-validation/get-line-and-column-from-index.spec.ts +22 -0
  44. package/src/actions/email-validation/get-line-and-column-from-index.ts +43 -0
  45. package/src/actions/email-validation/quick-fetch.ts +12 -0
  46. package/src/actions/get-email-path-from-slug.ts +7 -4
  47. package/src/actions/render-email-by-path.tsx +3 -3
  48. package/src/animated-icons-data/help.json +1082 -0
  49. package/src/animated-icons-data/link.json +1309 -0
  50. package/src/animated-icons-data/load.json +443 -0
  51. package/src/animated-icons-data/mail.json +1320 -0
  52. package/src/app/globals.css +0 -24
  53. package/src/app/layout.tsx +7 -3
  54. package/src/app/page.tsx +9 -10
  55. package/src/app/preview/[...slug]/page.tsx +3 -2
  56. package/src/app/preview/[...slug]/preview.tsx +5 -5
  57. package/src/app/preview/[...slug]/rendering-error.tsx +6 -6
  58. package/src/components/button.tsx +8 -8
  59. package/src/components/code-container.tsx +7 -7
  60. package/src/components/code-snippet.tsx +11 -0
  61. package/src/components/code.tsx +4 -4
  62. package/src/components/heading.tsx +1 -1
  63. package/src/components/icons/icon-button.tsx +1 -1
  64. package/src/components/icons/icon-circle-check.tsx +21 -0
  65. package/src/components/icons/icon-circle-close.tsx +17 -0
  66. package/src/components/icons/icon-circle-warning.tsx +17 -0
  67. package/src/components/icons/icon-email.tsx +18 -0
  68. package/src/components/icons/icon-link.tsx +14 -0
  69. package/src/components/icons/icon-stamp.tsx +14 -0
  70. package/src/components/send.tsx +9 -9
  71. package/src/components/shell.tsx +32 -34
  72. package/src/components/sidebar/{sidebar-directory-children.tsx → file-tree-directory-children.tsx} +22 -18
  73. package/src/components/sidebar/{sidebar-directory.tsx → file-tree-directory.tsx} +11 -12
  74. package/src/components/sidebar/file-tree.tsx +31 -0
  75. package/src/components/sidebar/link-checker.tsx +291 -0
  76. package/src/components/sidebar/sidebar.tsx +296 -22
  77. package/src/components/text.tsx +1 -1
  78. package/src/components/tooltip-content.tsx +3 -3
  79. package/src/components/tooltip.tsx +1 -1
  80. package/src/components/topbar.tsx +14 -17
  81. package/src/hooks/use-email-rendering-result.ts +2 -2
  82. package/src/hooks/use-icon-animation.ts +44 -0
  83. package/src/utils/cn.ts +1 -1
  84. package/src/utils/esbuild/renderring-utilities-exporter.ts +1 -1
  85. package/src/utils/get-email-component.ts +6 -6
  86. package/src/utils/get-emails-directory-metadata.spec.ts +0 -1
  87. package/src/utils/improve-error-with-sourcemap.ts +1 -1
  88. package/src/utils/static-node-modules-for-vm.ts +6 -6
  89. package/tsconfig.json +2 -6
  90. package/.eslintrc.js +0 -52
  91. package/.prettierignore +0 -3
  92. package/.prettierrc.js +0 -8
  93. package/dist/preview/.next/cache/eslint/.cache_1c3sgg +0 -1
  94. package/dist/preview/.next/server/chunks/391.js +0 -1
  95. package/dist/preview/.next/server/chunks/720.js +0 -10
  96. package/dist/preview/.next/static/chunks/12-b9450aa0845e7574.js +0 -1
  97. package/dist/preview/.next/static/chunks/154-4202f86af36ccff4.js +0 -1
  98. package/dist/preview/.next/static/chunks/app/page-54a86772095e22e0.js +0 -1
  99. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-2bfad134b65ddd79.js +0 -1
  100. package/dist/preview/.next/static/chunks/webpack-9255716c9496e606.js +0 -1
  101. package/dist/preview/.next/static/css/eb0a93282704d7ab.css +0 -3
  102. /package/dist/preview/.next/static/{Trk1e7GzgKOLunAXBDCy- → fZaiKz58wDr55pxLu9uHa}/_buildManifest.js +0 -0
  103. /package/dist/preview/.next/static/{Trk1e7GzgKOLunAXBDCy- → fZaiKz58wDr55pxLu9uHa}/_ssgManifest.js +0 -0
@@ -1,45 +1,319 @@
1
1
  'use client';
2
2
 
3
- import * as React from 'react';
4
- import * as Collapsible from '@radix-ui/react-collapsible';
3
+ import * as Tabs from '@radix-ui/react-tabs';
4
+ import { clsx } from 'clsx';
5
+ import { motion } from 'framer-motion';
6
+ import Lottie from 'lottie-react';
7
+ import Link from 'next/link';
8
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
9
+ import type * as React from 'react';
10
+ import animatedHelpIcon from '../../animated-icons-data/help.json';
11
+ import animatedLinkIcon from '../../animated-icons-data/link.json';
12
+ import animatedMailIcon from '../../animated-icons-data/mail.json';
5
13
  import { useEmails } from '../../contexts/emails';
14
+ import { useIconAnimation } from '../../hooks/use-icon-animation';
6
15
  import { cn } from '../../utils';
7
- import { Logo } from '../logo';
8
- import { SidebarDirectoryChildren } from './sidebar-directory-children';
16
+ import { Button } from '../button';
17
+ import { Heading } from '../heading';
18
+ import { Tooltip } from '../tooltip';
19
+ import { FileTree } from './file-tree';
20
+ import { LinkChecker } from './link-checker';
21
+
22
+ type SidebarPanelValue = 'file-tree' | 'link-checker' | 'image-checker';
9
23
 
10
24
  interface SidebarProps {
11
25
  className?: string;
12
26
  currentEmailOpenSlug?: string;
27
+ markup?: string;
13
28
  style?: React.CSSProperties;
14
29
  }
15
30
 
31
+ interface NavigationButtonProps {
32
+ children: React.ReactNode;
33
+ className?: string;
34
+ href?: string;
35
+ onMouseEnter?: () => void;
36
+ onMouseLeave?: () => void;
37
+ side?: 'top' | 'bottom' | 'left' | 'right';
38
+ tooltip?: string;
39
+ }
40
+
41
+ interface TabTriggerProps {
42
+ activeTabValue: SidebarPanelValue;
43
+ children?: React.ReactNode;
44
+ className?: string;
45
+ disabled?: boolean;
46
+ onMouseEnter?: () => void;
47
+ onMouseLeave?: () => void;
48
+ tabValue: SidebarPanelValue;
49
+ tooltipText: string;
50
+ }
51
+
52
+ interface PanelProps {
53
+ active: boolean;
54
+ children: React.ReactNode;
55
+ title: string;
56
+ }
57
+
58
+ const TAB_ACTION_BASE_CLASSES =
59
+ 'group relative aspect-square w-full cursor-pointer text-slate-12 transition-colors duration-150 ease-[cubic-bezier(.36,.66,.6,1)] hover:bg-slate-3 disabled:cursor-not-allowed disabled:bg-slate-2 disabled:text-slate-10';
60
+
61
+ const NavigationButton = ({
62
+ children,
63
+ className,
64
+ href,
65
+ onMouseEnter,
66
+ onMouseLeave,
67
+ side,
68
+ tooltip,
69
+ }: NavigationButtonProps) => (
70
+ <Tooltip.Provider>
71
+ <Tooltip>
72
+ <Tooltip.Trigger asChild>
73
+ <Link
74
+ href={href ?? '#'}
75
+ className={cn(TAB_ACTION_BASE_CLASSES, className)}
76
+ onMouseEnter={onMouseEnter}
77
+ onMouseLeave={onMouseLeave}
78
+ >
79
+ {children}
80
+ </Link>
81
+ </Tooltip.Trigger>
82
+ {tooltip && <Tooltip.Content side={side}>{tooltip}</Tooltip.Content>}
83
+ </Tooltip>
84
+ </Tooltip.Provider>
85
+ );
86
+
87
+ const TabTrigger = ({
88
+ activeTabValue,
89
+ children,
90
+ className,
91
+ disabled,
92
+ onMouseEnter,
93
+ onMouseLeave,
94
+ tabValue,
95
+ tooltipText,
96
+ }: TabTriggerProps) => {
97
+ const isActive = tabValue === activeTabValue;
98
+
99
+ return (
100
+ <Tooltip.Provider>
101
+ <Tooltip>
102
+ <Tooltip.Trigger asChild>
103
+ <Tabs.Trigger
104
+ className={clsx(TAB_ACTION_BASE_CLASSES, className, {
105
+ 'bg-slate-6': isActive,
106
+ })}
107
+ data-active={isActive}
108
+ disabled={disabled}
109
+ onMouseEnter={onMouseEnter}
110
+ onMouseLeave={onMouseLeave}
111
+ value={tabValue}
112
+ >
113
+ {isActive && (
114
+ <motion.div
115
+ className="absolute top-0 left-0 h-full w-1 bg-[#0BB9CD] transition-colors duration-300 ease-[bezier(.36,.66,.6,1)]"
116
+ layoutId="sidebar-active-tab"
117
+ transition={{ type: 'spring', bounce: 0.12, duration: 0.6 }}
118
+ />
119
+ )}
120
+ <div
121
+ aria-hidden
122
+ className="pointer-events-none absolute inset-0 flex items-center justify-center pl-1 transition-opacity duration-150 ease-in"
123
+ >
124
+ {children}
125
+ </div>
126
+ </Tabs.Trigger>
127
+ </Tooltip.Trigger>
128
+ <Tooltip.Content side="right">{tooltipText}</Tooltip.Content>
129
+ </Tooltip>
130
+ </Tooltip.Provider>
131
+ );
132
+ };
133
+
134
+ const Panel = ({ title, active, children }: PanelProps) => (
135
+ <>
136
+ <div
137
+ className={clsx(
138
+ 'hidden min-h-[3.3125rem] flex-shrink items-center p-3 px-4 lg:flex',
139
+ {
140
+ 'bg-slate-3': active,
141
+ },
142
+ )}
143
+ >
144
+ <Heading as="h2" className="truncate" size="2" weight="medium">
145
+ {title}
146
+ </Heading>
147
+ </div>
148
+ <div className="-mt-[.5px] relative h-[calc(100vh-4.375rem)] w-full border-slate-4 border-t px-4 pb-3">
149
+ {children}
150
+ </div>
151
+ </>
152
+ );
153
+
154
+ const ReactIcon = () => (
155
+ <svg
156
+ fill="none"
157
+ height="32"
158
+ viewBox="0 0 32 32"
159
+ width="32"
160
+ xmlns="http://www.w3.org/2000/svg"
161
+ className="pointer-events-none duration-300 ease-[cubic-bezier(.42,0,.58,1.8)] group-hover:rotate-90"
162
+ >
163
+ <g clipPath="url(#clip0_27_291)">
164
+ <path
165
+ clipRule="evenodd"
166
+ d="M24.4558 24.4853C25.2339 23.7073 25.3805 22.6549 25.2947 21.746C25.2078 20.8254 24.8697 19.8258 24.3896 18.8287C23.957 17.9302 23.3802 16.9745 22.6821 16C23.3802 15.0255 23.957 14.0698 24.3896 13.1713C24.8697 12.1742 25.2078 11.1746 25.2947 10.254C25.3805 9.34508 25.2339 8.29273 24.4558 7.51472C23.6778 6.73671 22.6255 6.59004 21.7165 6.67584C20.796 6.76273 19.7964 7.10086 18.7993 7.58094C17.9007 8.01357 16.945 8.59036 15.9706 9.28842C14.9961 8.59036 14.0404 8.01357 13.1418 7.58094C12.1447 7.10086 11.1451 6.76273 10.2246 6.67584C9.31564 6.59004 8.26329 6.73671 7.48528 7.51472C6.70727 8.29273 6.5606 9.34508 6.6464 10.254C6.7333 11.1746 7.07142 12.1742 7.5515 13.1713C7.98414 14.0698 8.56092 15.0255 9.25898 16C8.56092 16.9745 7.98414 17.9302 7.5515 18.8287C7.07142 19.8258 6.7333 20.8254 6.6464 21.746C6.5606 22.6549 6.70727 23.7073 7.48528 24.4853C8.26329 25.2633 9.31564 25.41 10.2246 25.3242C11.1451 25.2373 12.1447 24.8991 13.1418 24.4191C14.0404 23.9864 14.9961 23.4096 15.9706 22.7116C16.945 23.4096 17.9007 23.9864 18.7993 24.4191C19.7964 24.8991 20.796 25.2373 21.7165 25.3242C22.6255 25.41 23.6778 25.2633 24.4558 24.4853ZM15.9706 20.948C16.8399 20.2684 17.724 19.4874 18.591 18.6205C19.458 17.7535 20.239 16.8693 20.9186 16C20.239 15.1307 19.458 14.2465 18.591 13.3795C17.724 12.5126 16.8399 11.7316 15.9706 11.052C15.1012 11.7316 14.2171 12.5126 13.3501 13.3795C12.4831 14.2465 11.7021 15.1307 11.0225 16C11.7021 16.8693 12.4831 17.7535 13.3501 18.6205C14.2171 19.4874 15.1012 20.2684 15.9706 20.948ZM17.1498 21.8145C17.968 21.1558 18.7885 20.4195 19.5893 19.6187C20.39 18.818 21.1264 17.9974 21.7851 17.1792C23.7187 19.9919 24.4627 22.4819 23.4576 23.487C22.4524 24.4922 19.9625 23.7482 17.1498 21.8145ZM10.156 17.1792C10.8148 17.9974 11.5511 18.818 12.3518 19.6187C13.1526 20.4195 13.9731 21.1558 14.7914 21.8145C11.9786 23.7482 9.48871 24.4922 8.48355 23.487C7.47839 22.4819 8.22238 19.9919 10.156 17.1792ZM10.156 14.8208C10.8148 14.0026 11.5511 13.182 12.3518 12.3813C13.1526 11.5805 13.9731 10.8442 14.7914 10.1855C11.9786 8.25182 9.48871 7.50783 8.48355 8.51299C7.47839 9.51815 8.22238 12.0081 10.156 14.8208ZM17.1498 10.1855C17.968 10.8442 18.7885 11.5805 19.5893 12.3813C20.39 13.182 21.1264 14.0026 21.7851 14.8208C23.7187 12.0081 24.4627 9.51815 23.4576 8.51299C22.4524 7.50783 19.9625 8.25182 17.1498 10.1855Z"
167
+ fill="white"
168
+ fillRule="evenodd"
169
+ stroke="white"
170
+ strokeWidth="0.5"
171
+ />
172
+ </g>
173
+ </svg>
174
+ );
175
+
16
176
  export const Sidebar = ({
17
177
  className,
18
178
  currentEmailOpenSlug,
179
+ markup: emailMarkup,
19
180
  style,
20
181
  }: SidebarProps) => {
182
+ const pathname = usePathname();
183
+ const searchParams = useSearchParams();
184
+ const router = useRouter();
185
+ const activePanelValue = (searchParams.get('sidebar-panel') ??
186
+ 'file-tree') as SidebarPanelValue;
21
187
  const { emailsDirectoryMetadata } = useEmails();
22
188
 
189
+ const mailAnimation = useIconAnimation();
190
+ const linkAnimation = useIconAnimation();
191
+ const helpAnimation = useIconAnimation();
192
+
193
+ const setActivePanelValue = (newValue: SidebarPanelValue) => {
194
+ const params = new URLSearchParams(searchParams);
195
+ params.set('sidebar-panel', newValue);
196
+ router.push(`${pathname}?${params.toString()}`);
197
+ };
198
+
23
199
  return (
24
- <aside
25
- className={cn('border-r flex flex-col border-slate-6', className)}
26
- style={{ ...style }}
200
+ <Tabs.Root
201
+ asChild
202
+ onValueChange={(v) => setActivePanelValue(v as SidebarPanelValue)}
203
+ orientation="vertical"
204
+ value={activePanelValue}
27
205
  >
28
- <div className="p-4 h-[70px] flex-shrink items-center hidden lg:flex">
29
- <Logo />
30
- </div>
31
- <nav className="p-4 flex-grow lg:pt-0 pl-0 w-screen h-[calc(100vh_-_70px)] lg:w-full lg:min-w-[275px] lg:max-w-[275px] flex flex-col overflow-y-auto">
32
- <Collapsible.Root>
33
- <React.Suspense>
34
- <SidebarDirectoryChildren
35
- currentEmailOpenSlug={currentEmailOpenSlug}
36
- emailsDirectoryMetadata={emailsDirectoryMetadata}
37
- isRoot
38
- open
206
+ <aside
207
+ className={cn(
208
+ 'fixed top-[4.375rem] left-0 z-[9999] grid h-full max-h-[calc(100dvh-4.375rem)] w-screen max-w-full grid-cols-[3.375rem,1fr] overflow-hidden bg-black will-change-auto lg:top-0 lg:z-auto lg:max-h-screen lg:max-w-[20rem]',
209
+ className,
210
+ )}
211
+ style={style}
212
+ >
213
+ <Tabs.List className="flex h-full flex-col border-slate-6 border-r">
214
+ <TabTrigger
215
+ activeTabValue={activePanelValue}
216
+ onMouseEnter={mailAnimation.onMouseEnter}
217
+ onMouseLeave={mailAnimation.onMouseLeave}
218
+ tabValue="file-tree"
219
+ tooltipText="File Explorer"
220
+ >
221
+ <Lottie
222
+ animationData={animatedMailIcon as object}
223
+ autoPlay={false}
224
+ className="h-5 w-5"
225
+ loop={false}
226
+ lottieRef={mailAnimation.ref}
227
+ />
228
+ </TabTrigger>
229
+ <TabTrigger
230
+ activeTabValue={activePanelValue}
231
+ className="relative"
232
+ onMouseEnter={linkAnimation.onMouseEnter}
233
+ onMouseLeave={linkAnimation.onMouseLeave}
234
+ tabValue="link-checker"
235
+ tooltipText="Link Checker"
236
+ >
237
+ <Lottie
238
+ animationData={animatedLinkIcon as object}
239
+ autoPlay={false}
240
+ className="h-6 w-6"
241
+ loop={false}
242
+ lottieRef={linkAnimation.ref}
39
243
  />
40
- </React.Suspense>
41
- </Collapsible.Root>
42
- </nav>
43
- </aside>
244
+ </TabTrigger>
245
+ <div className="mt-auto flex flex-col">
246
+ <NavigationButton
247
+ className="flex items-center justify-center"
248
+ href="https://react.email/docs"
249
+ onMouseEnter={helpAnimation.onMouseEnter}
250
+ onMouseLeave={helpAnimation.onMouseLeave}
251
+ side="right"
252
+ tooltip="Documentation"
253
+ >
254
+ <Lottie
255
+ animationData={animatedHelpIcon as object}
256
+ autoPlay={false}
257
+ className="h-5 w-5"
258
+ loop={false}
259
+ lottieRef={helpAnimation.ref}
260
+ />
261
+ </NavigationButton>
262
+ <NavigationButton
263
+ className="flex items-center justify-center"
264
+ href="https://react.email"
265
+ side="right"
266
+ tooltip="Website"
267
+ >
268
+ <div className="flex h-7 w-7 items-center justify-center">
269
+ <ReactIcon />
270
+ </div>
271
+ </NavigationButton>
272
+ </div>
273
+ </Tabs.List>
274
+ <div className="flex overflow-y-auto overflow-x-hidden">
275
+ <div className="flex w-full flex-col border-slate-6 border-r">
276
+ {activePanelValue === 'link-checker' && (
277
+ <Panel
278
+ title="Link Checker"
279
+ active={activePanelValue === 'link-checker'}
280
+ >
281
+ {currentEmailOpenSlug && emailMarkup ? (
282
+ <LinkChecker
283
+ emailMarkup={emailMarkup}
284
+ emailSlug={currentEmailOpenSlug}
285
+ />
286
+ ) : (
287
+ <div className="mt-4 flex w-full flex-col gap-2 text-pretty text-xs leading-relaxed">
288
+ <div className="flex flex-col gap-1 rounded-lg border border-[#0BB9CD]/50 bg-[#0BB9CD]/20 text-white">
289
+ <span className="mx-2.5 mt-2">
290
+ To use the Link Checker, you need to select a template.
291
+ </span>
292
+ <Button
293
+ className="mx-2 my-2.5 transition-all disabled:border-transparent disabled:bg-slate-11"
294
+ onClick={() => setActivePanelValue('file-tree')}
295
+ >
296
+ Select a template
297
+ </Button>
298
+ </div>
299
+ </div>
300
+ )}
301
+ </Panel>
302
+ )}
303
+ {activePanelValue === 'file-tree' && (
304
+ <Panel
305
+ title="File Explorer"
306
+ active={activePanelValue === 'file-tree'}
307
+ >
308
+ <FileTree
309
+ currentEmailOpenSlug={currentEmailOpenSlug}
310
+ emailsDirectoryMetadata={emailsDirectoryMetadata}
311
+ />
312
+ </Panel>
313
+ )}
314
+ </div>
315
+ </div>
316
+ </aside>
317
+ </Tabs.Root>
44
318
  );
45
319
  };
@@ -1,6 +1,6 @@
1
1
  import * as SlotPrimitive from '@radix-ui/react-slot';
2
2
  import * as React from 'react';
3
- import { type As, unreachable, cn } from '../utils';
3
+ import { type As, cn, unreachable } from '../utils';
4
4
 
5
5
  export type TextSize = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
6
6
  export type TextColor = 'gray' | 'white';
@@ -1,9 +1,9 @@
1
1
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
2
2
  import * as React from 'react';
3
- import { cn } from '../utils';
4
3
  import { inter } from '../app/inter';
4
+ import { cn } from '../utils';
5
5
 
6
- type ContentElement = React.ElementRef<typeof TooltipPrimitive.Content>;
6
+ type ContentElement = React.ComponentRef<typeof TooltipPrimitive.Content>;
7
7
  type ContentProps = React.ComponentPropsWithoutRef<
8
8
  typeof TooltipPrimitive.Content
9
9
  >;
@@ -18,7 +18,7 @@ export const TooltipContent = React.forwardRef<
18
18
  <TooltipPrimitive.Content
19
19
  {...props}
20
20
  className={cn(
21
- 'bg-black text-white border border-slate-6 z-20 px-3 py-2 rounded-md text-xs',
21
+ 'z-20 rounded-md border border-slate-6 bg-black px-3 py-2 text-white text-xs',
22
22
  `${inter.variable} font-sans`,
23
23
  )}
24
24
  ref={forwardedRef}
@@ -1,5 +1,5 @@
1
1
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
2
- import * as React from 'react';
2
+ import type * as React from 'react';
3
3
  import { TooltipContent } from './tooltip-content';
4
4
 
5
5
  type RootProps = React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Root>;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import * as ToggleGroup from '@radix-ui/react-toggle-group';
3
3
  import { motion } from 'framer-motion';
4
- import * as React from 'react';
4
+ import type * as React from 'react';
5
5
  import { cn } from '../utils';
6
6
  import { tabTransition } from '../utils/constants';
7
7
  import { Heading } from './heading';
@@ -21,21 +21,21 @@ interface TopbarProps {
21
21
  setActiveView?: (view: string) => void;
22
22
  }
23
23
 
24
- export const Topbar: React.FC<Readonly<TopbarProps>> = ({
24
+ export const Topbar = ({
25
25
  currentEmailOpenSlug,
26
26
  pathSeparator,
27
27
  markup,
28
28
  activeView,
29
29
  setActiveView,
30
30
  onToggleSidebar,
31
- }) => {
31
+ }: TopbarProps) => {
32
32
  return (
33
33
  <Tooltip.Provider>
34
- <header className="flex relative items-center px-4 justify-between h-[70px] border-b border-slate-6">
34
+ <header className="relative flex h-[3.3125rem] items-center justify-between border-slate-6 border-b px-3">
35
35
  <Tooltip>
36
36
  <Tooltip.Trigger asChild>
37
37
  <button
38
- className="hidden lg:flex rounded-lg px-2 py-2 transition ease-in-out duration-200 relative hover:bg-slate-5 text-slate-11 hover:text-slate-12"
38
+ 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"
39
39
  onClick={() => {
40
40
  if (onToggleSidebar) {
41
41
  onToggleSidebar();
@@ -48,17 +48,15 @@ export const Topbar: React.FC<Readonly<TopbarProps>> = ({
48
48
  </Tooltip.Trigger>
49
49
  <Tooltip.Content>Show/hide sidebar</Tooltip.Content>
50
50
  </Tooltip>
51
-
52
- <div className="items-center overflow-hidden hidden lg:flex text-center absolute left-1/2 transform -translate-x-1/2 top-1/2 -translate-y-1/2">
51
+ <div className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 hidden transform items-center overflow-hidden text-center lg:flex">
53
52
  <Heading as="h2" className="truncate" size="2" weight="medium">
54
53
  {currentEmailOpenSlug.split(pathSeparator).pop()}
55
54
  </Heading>
56
55
  </div>
57
-
58
- <div className="flex gap-3 justify-between lg:justify-start w-full lg:w-fit">
56
+ <div className="flex w-full justify-between gap-3 lg:w-fit lg:justify-start">
59
57
  <ToggleGroup.Root
60
58
  aria-label="View mode"
61
- className="inline-block items-center bg-slate-2 border border-slate-6 rounded-md overflow-hidden h-[36px]"
59
+ className="inline-block h-[36px] items-center overflow-hidden rounded-md border border-slate-6 bg-slate-2"
62
60
  onValueChange={(value) => {
63
61
  if (value) setActiveView?.(value);
64
62
  }}
@@ -70,7 +68,7 @@ export const Topbar: React.FC<Readonly<TopbarProps>> = ({
70
68
  <Tooltip.Trigger asChild>
71
69
  <div
72
70
  className={cn(
73
- 'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
71
+ 'relative px-3 py-2 transition duration-200 ease-in-out hover:text-slate-12',
74
72
  {
75
73
  'text-slate-11': activeView !== 'desktop',
76
74
  'text-slate-12': activeView === 'desktop',
@@ -80,7 +78,7 @@ export const Topbar: React.FC<Readonly<TopbarProps>> = ({
80
78
  {activeView === 'desktop' && (
81
79
  <motion.span
82
80
  animate={{ opacity: 1 }}
83
- className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
81
+ className="absolute top-0 right-0 bottom-0 left-0 bg-slate-4"
84
82
  exit={{ opacity: 0 }}
85
83
  initial={{ opacity: 0 }}
86
84
  layoutId="topbar-tabs"
@@ -98,7 +96,7 @@ export const Topbar: React.FC<Readonly<TopbarProps>> = ({
98
96
  <Tooltip.Trigger asChild>
99
97
  <div
100
98
  className={cn(
101
- 'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
99
+ 'relative px-3 py-2 transition duration-200 ease-in-out hover:text-slate-12',
102
100
  {
103
101
  'text-slate-11': activeView !== 'mobile',
104
102
  'text-slate-12': activeView === 'mobile',
@@ -108,7 +106,7 @@ export const Topbar: React.FC<Readonly<TopbarProps>> = ({
108
106
  {activeView === 'mobile' && (
109
107
  <motion.span
110
108
  animate={{ opacity: 1 }}
111
- className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
109
+ className="absolute top-0 right-0 bottom-0 left-0 bg-slate-4"
112
110
  exit={{ opacity: 0 }}
113
111
  initial={{ opacity: 0 }}
114
112
  layoutId="topbar-tabs"
@@ -126,7 +124,7 @@ export const Topbar: React.FC<Readonly<TopbarProps>> = ({
126
124
  <Tooltip.Trigger asChild>
127
125
  <div
128
126
  className={cn(
129
- 'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
127
+ 'relative px-3 py-2 transition duration-200 ease-in-out hover:text-slate-12',
130
128
  {
131
129
  'text-slate-11': activeView !== 'source',
132
130
  'text-slate-12': activeView === 'source',
@@ -136,7 +134,7 @@ export const Topbar: React.FC<Readonly<TopbarProps>> = ({
136
134
  {activeView === 'source' && (
137
135
  <motion.span
138
136
  animate={{ opacity: 1 }}
139
- className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
137
+ className="absolute top-0 right-0 bottom-0 left-0 bg-slate-4"
140
138
  exit={{ opacity: 0 }}
141
139
  initial={{ opacity: 0 }}
142
140
  layoutId="topbar-tabs"
@@ -150,7 +148,6 @@ export const Topbar: React.FC<Readonly<TopbarProps>> = ({
150
148
  </Tooltip>
151
149
  </ToggleGroup.Item>
152
150
  </ToggleGroup.Root>
153
-
154
151
  {markup ? (
155
152
  <div className="flex justify-end">
156
153
  <Send markup={markup} />
@@ -1,9 +1,9 @@
1
1
  import { useState } from 'react';
2
+ import { getEmailPathFromSlug } from '../actions/get-email-path-from-slug';
2
3
  import {
3
- renderEmailByPath,
4
4
  type EmailRenderingResult,
5
+ renderEmailByPath,
5
6
  } from '../actions/render-email-by-path';
6
- import { getEmailPathFromSlug } from '../actions/get-email-path-from-slug';
7
7
  import { useHotreload } from './use-hot-reload';
8
8
 
9
9
  export const useEmailRenderingResult = (
@@ -0,0 +1,44 @@
1
+ import type { LottieRefCurrentProps } from 'lottie-react';
2
+ import * as React from 'react';
3
+
4
+ const TIMEOUT = 150;
5
+ const THRESHOLD_ANIMATION = 0.9;
6
+
7
+ export const useIconAnimation = () => {
8
+ const ref = React.useRef<LottieRefCurrentProps>(null);
9
+ const timer = React.useRef<NodeJS.Timeout | null>(null);
10
+
11
+ const onMouseLeave = React.useCallback(() => {
12
+ timer.current && clearTimeout(timer.current);
13
+ }, []);
14
+
15
+ const onMouseEnter = React.useCallback(() => {
16
+ if (!ref.current) {
17
+ return;
18
+ }
19
+
20
+ const total = Math.round(ref.current.animationItem?.totalFrames ?? 0);
21
+ const current = Math.round(
22
+ (ref.current.animationItem?.currentFrame ?? 0) + 1,
23
+ );
24
+
25
+ if (current === 1 || current >= total * THRESHOLD_ANIMATION) {
26
+ timer.current = setTimeout(() => {
27
+ if (!ref.current) {
28
+ return;
29
+ }
30
+
31
+ ref.current.stop();
32
+ ref.current.setDirection(1);
33
+ ref.current.setSpeed(1);
34
+ ref.current.play();
35
+ }, TIMEOUT);
36
+ }
37
+ }, []);
38
+
39
+ return {
40
+ ref,
41
+ onMouseLeave,
42
+ onMouseEnter,
43
+ };
44
+ };
package/src/utils/cn.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { clsx, type ClassValue } from 'clsx';
1
+ import { type ClassValue, clsx } from 'clsx';
2
2
  import { twMerge } from 'tailwind-merge';
3
3
 
4
4
  export const cn = (...inputs: ClassValue[]) => {
@@ -1,5 +1,5 @@
1
- import path from 'node:path';
2
1
  import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
3
  import type { Loader, PluginBuild, ResolveOptions } from 'esbuild';
4
4
  import { escapeStringForRegex } from './escape-string-for-regex';
5
5
 
@@ -1,15 +1,15 @@
1
1
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
2
  import path from 'node:path';
3
3
  import vm from 'node:vm';
4
- import type React from 'react';
5
- import { type RawSourceMap } from 'source-map-js';
6
- import { type OutputFile, build, type BuildFailure } from 'esbuild';
7
4
  import type { render } from '@react-email/render';
8
- import type { EmailTemplate as EmailComponent } from './types/email-template';
9
- import type { ErrorObject } from './types/error-object';
5
+ import { type BuildFailure, type OutputFile, build } from 'esbuild';
6
+ import type React from 'react';
7
+ import type { RawSourceMap } from 'source-map-js';
8
+ import { renderingUtilitiesExporter } from './esbuild/renderring-utilities-exporter';
10
9
  import { improveErrorWithSourceMap } from './improve-error-with-sourcemap';
11
10
  import { staticNodeModulesForVM } from './static-node-modules-for-vm';
12
- import { renderingUtilitiesExporter } from './esbuild/renderring-utilities-exporter';
11
+ import type { EmailTemplate as EmailComponent } from './types/email-template';
12
+ import type { ErrorObject } from './types/error-object';
13
13
 
14
14
  export const getEmailComponent = async (
15
15
  emailPath: string,
@@ -18,7 +18,6 @@ test('getEmailsDirectoryMetadata on demo emails', async () => {
18
18
  relativePath: 'magic-links',
19
19
  emailFilenames: [
20
20
  'aws-verify-email',
21
- 'jobaccepted-magic-link',
22
21
  'linear-login-code',
23
22
  'notion-magic-link',
24
23
  'plaid-verify-identity',
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
+ import { type RawSourceMap, SourceMapConsumer } from 'source-map-js';
2
3
  import * as stackTraceParser from 'stacktrace-parser';
3
- import { SourceMapConsumer, type RawSourceMap } from 'source-map-js';
4
4
  import type { ErrorObject } from './types/error-object';
5
5
 
6
6
  export const improveErrorWithSourceMap = (
@@ -48,15 +48,15 @@ import zlib from 'node:zlib';
48
48
  */
49
49
  export const staticNodeModulesForVM = {
50
50
  assert,
51
- 'async_hooks': asyncHooks,
51
+ async_hooks: asyncHooks,
52
52
  buffer,
53
- 'child_process': childProcess,
53
+ child_process: childProcess,
54
54
  cluster,
55
55
  console,
56
56
  constants,
57
57
  crypto,
58
58
  dgram,
59
- 'diagnostics_channel': diagnosticsChannel,
59
+ diagnostics_channel: diagnosticsChannel,
60
60
  dns,
61
61
  domain,
62
62
  events,
@@ -70,14 +70,14 @@ export const staticNodeModulesForVM = {
70
70
  net,
71
71
  os,
72
72
  path,
73
- 'perf_hooks': perfHooks,
73
+ perf_hooks: perfHooks,
74
74
  process,
75
75
  punycode,
76
76
  querystring,
77
77
  readline,
78
78
  repl,
79
79
  stream,
80
- 'string_decoder': stringDecoder,
80
+ string_decoder: stringDecoder,
81
81
  timers,
82
82
  'timers/promises': timersPromises,
83
83
  tls,
@@ -87,6 +87,6 @@ export const staticNodeModulesForVM = {
87
87
  'util/types': utilTypes,
88
88
  v8,
89
89
  vm,
90
- 'worker_threads': workerThreads,
90
+ worker_threads: workerThreads,
91
91
  zlib,
92
92
  };