@react-email/preview-server 1.0.0-canary.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 (265) hide show
  1. package/.next/BUILD_ID +1 -0
  2. package/.next/app-build-manifest.json +44 -0
  3. package/.next/app-path-routes-manifest.json +6 -0
  4. package/.next/build-manifest.json +33 -0
  5. package/.next/diagnostics/build-diagnostics.json +6 -0
  6. package/.next/diagnostics/framework.json +1 -0
  7. package/.next/export-marker.json +6 -0
  8. package/.next/images-manifest.json +57 -0
  9. package/.next/next-minimal-server.js.nft.json +1 -0
  10. package/.next/next-server.js.nft.json +1 -0
  11. package/.next/package.json +1 -0
  12. package/.next/prerender-manifest.json +41 -0
  13. package/.next/react-loadable-manifest.json +1 -0
  14. package/.next/required-server-files.json +311 -0
  15. package/.next/routes-manifest.json +64 -0
  16. package/.next/server/app/_not-found/page.js +1 -0
  17. package/.next/server/app/_not-found/page.js.nft.json +1 -0
  18. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  19. package/.next/server/app/favicon.ico/route.js +1 -0
  20. package/.next/server/app/favicon.ico/route.js.nft.json +1 -0
  21. package/.next/server/app/favicon.ico.body +0 -0
  22. package/.next/server/app/favicon.ico.meta +1 -0
  23. package/.next/server/app/page.js +1 -0
  24. package/.next/server/app/page.js.nft.json +1 -0
  25. package/.next/server/app/page_client-reference-manifest.js +1 -0
  26. package/.next/server/app/preview/[...slug]/page.js +321 -0
  27. package/.next/server/app/preview/[...slug]/page.js.nft.json +1 -0
  28. package/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -0
  29. package/.next/server/app-paths-manifest.json +6 -0
  30. package/.next/server/chunks/134.js +6 -0
  31. package/.next/server/chunks/235.js +15 -0
  32. package/.next/server/chunks/278.js +1 -0
  33. package/.next/server/chunks/343.js +20 -0
  34. package/.next/server/chunks/428.js +14 -0
  35. package/.next/server/chunks/963.js +1 -0
  36. package/.next/server/chunks/999.js +1 -0
  37. package/.next/server/functions-config-manifest.json +4 -0
  38. package/.next/server/interception-route-rewrite-manifest.js +1 -0
  39. package/.next/server/middleware-build-manifest.js +1 -0
  40. package/.next/server/middleware-manifest.json +6 -0
  41. package/.next/server/middleware-react-loadable-manifest.js +1 -0
  42. package/.next/server/next-font-manifest.js +1 -0
  43. package/.next/server/next-font-manifest.json +1 -0
  44. package/.next/server/pages/500.html +1 -0
  45. package/.next/server/pages/_app.js +1 -0
  46. package/.next/server/pages/_app.js.nft.json +1 -0
  47. package/.next/server/pages/_document.js +1 -0
  48. package/.next/server/pages/_document.js.nft.json +1 -0
  49. package/.next/server/pages/_error.js +1 -0
  50. package/.next/server/pages/_error.js.nft.json +1 -0
  51. package/.next/server/pages-manifest.json +5 -0
  52. package/.next/server/server-reference-manifest.js +1 -0
  53. package/.next/server/server-reference-manifest.json +1 -0
  54. package/.next/server/webpack-runtime.js +1 -0
  55. package/.next/static/VkyJa9K30jCrKBesOgrQT/_buildManifest.js +1 -0
  56. package/.next/static/VkyJa9K30jCrKBesOgrQT/_ssgManifest.js +1 -0
  57. package/.next/static/chunks/107-3043079e7cb8bcae.js +1 -0
  58. package/.next/static/chunks/293-67391ef0e44ffa4f.js +1 -0
  59. package/.next/static/chunks/3bd82e28-cda2c00a924937c5.js +1 -0
  60. package/.next/static/chunks/45-1021fac82f766268.js +1 -0
  61. package/.next/static/chunks/484-1969fe871b27074e.js +1 -0
  62. package/.next/static/chunks/484-e02de792ff5f9ea5.js +1 -0
  63. package/.next/static/chunks/589-817d8691661d370e.js +1 -0
  64. package/.next/static/chunks/902-c34acb56733e0ce1.js +1 -0
  65. package/.next/static/chunks/app/_not-found/page-4cbc7dce3ad33336.js +1 -0
  66. package/.next/static/chunks/app/layout-74628781c0b7e7bf.js +1 -0
  67. package/.next/static/chunks/app/layout-daeba68330ab58bb.js +1 -0
  68. package/.next/static/chunks/app/page-55cf199b7ca71958.js +1 -0
  69. package/.next/static/chunks/app/preview/[...slug]/page-07dd9a701d0b3e56.js +1 -0
  70. package/.next/static/chunks/app/preview/[...slug]/page-61b0ea70a8d72916.js +1 -0
  71. package/.next/static/chunks/f33a14d2-ec7c5f0b91818561.js +6 -0
  72. package/.next/static/chunks/framework-b887e9fc751a9906.js +1 -0
  73. package/.next/static/chunks/main-9a03e7ba8acb1900.js +1 -0
  74. package/.next/static/chunks/main-app-5bc2d814f500db60.js +1 -0
  75. package/.next/static/chunks/pages/_app-542a93a5a214e1c0.js +1 -0
  76. package/.next/static/chunks/pages/_error-d5fe1b1612642f76.js +1 -0
  77. package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  78. package/.next/static/chunks/webpack-31c45daa2bd82a7b.js +1 -0
  79. package/.next/static/css/35e8811589f0962b.css +3 -0
  80. package/.next/static/j4oDiQwPKPQgK5jAoiUTk/_buildManifest.js +1 -0
  81. package/.next/static/j4oDiQwPKPQgK5jAoiUTk/_ssgManifest.js +1 -0
  82. package/.next/static/media/05613964ce6c782e-s.p.otf +0 -0
  83. package/.next/static/media/11c6126b9369e85e-s.p.otf +0 -0
  84. package/.next/static/media/26a46d62cd723877-s.woff2 +0 -0
  85. package/.next/static/media/26cb97734d8cb717-s.p.otf +0 -0
  86. package/.next/static/media/55c55f0601d81cf3-s.woff2 +0 -0
  87. package/.next/static/media/581909926a08bbc8-s.woff2 +0 -0
  88. package/.next/static/media/6d93bde91c0c2823-s.woff2 +0 -0
  89. package/.next/static/media/97e0cb1ae144a2a9-s.woff2 +0 -0
  90. package/.next/static/media/a34f9d1faa5f3315-s.p.woff2 +0 -0
  91. package/.next/static/media/bb6462617151f6b7-s.p.otf +0 -0
  92. package/.next/static/media/cf6daef822ab0142-s.p.otf +0 -0
  93. package/.next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
  94. package/.next/static/media/e4051546b3043204-s.p.otf +0 -0
  95. package/.next/static/media/logo.2ce2a759.png +0 -0
  96. package/.next/trace +27 -0
  97. package/.next/types/app/layout.ts +84 -0
  98. package/.next/types/app/page.ts +84 -0
  99. package/.next/types/app/preview/[...slug]/page.ts +84 -0
  100. package/.next/types/cache-life.d.ts +141 -0
  101. package/.next/types/package.json +1 -0
  102. package/_index.js +4 -0
  103. package/license.md +7 -0
  104. package/module-punycode.d.ts +3 -0
  105. package/next-env.d.ts +5 -0
  106. package/next.config.js +22 -0
  107. package/package.json +82 -0
  108. package/postcss.config.js +8 -0
  109. package/scripts/build-preview-server.mjs +29 -0
  110. package/scripts/fill-caniemail-data.mjs +36 -0
  111. package/src/actions/email-validation/caniemail-data.ts +85993 -0
  112. package/src/actions/email-validation/check-compatibility.ts +333 -0
  113. package/src/actions/email-validation/check-images.spec.tsx +100 -0
  114. package/src/actions/email-validation/check-images.ts +160 -0
  115. package/src/actions/email-validation/check-links.spec.tsx +113 -0
  116. package/src/actions/email-validation/check-links.ts +113 -0
  117. package/src/actions/email-validation/get-code-location-from-ast-element.ts +18 -0
  118. package/src/actions/email-validation/quick-fetch.ts +14 -0
  119. package/src/actions/get-email-path-from-slug.ts +32 -0
  120. package/src/actions/get-emails-directory-metadata-action.ts +19 -0
  121. package/src/actions/render-email-by-path.tsx +121 -0
  122. package/src/animated-icons-data/help.json +1082 -0
  123. package/src/animated-icons-data/link.json +1309 -0
  124. package/src/animated-icons-data/load.json +443 -0
  125. package/src/animated-icons-data/mail.json +1320 -0
  126. package/src/app/env.ts +15 -0
  127. package/src/app/favicon.ico +0 -0
  128. package/src/app/fonts/SFMono/SFMonoBold.otf +0 -0
  129. package/src/app/fonts/SFMono/SFMonoBoldItalic.otf +0 -0
  130. package/src/app/fonts/SFMono/SFMonoHeavy.otf +0 -0
  131. package/src/app/fonts/SFMono/SFMonoHeavyItalic.otf +0 -0
  132. package/src/app/fonts/SFMono/SFMonoLight.otf +0 -0
  133. package/src/app/fonts/SFMono/SFMonoLightItalic.otf +0 -0
  134. package/src/app/fonts/SFMono/SFMonoMedium.otf +0 -0
  135. package/src/app/fonts/SFMono/SFMonoMediumItalic.otf +0 -0
  136. package/src/app/fonts/SFMono/SFMonoRegular.otf +0 -0
  137. package/src/app/fonts/SFMono/SFMonoRegularItalic.otf +0 -0
  138. package/src/app/fonts/SFMono/SFMonoSemibold.otf +0 -0
  139. package/src/app/fonts/SFMono/SFMonoSemiboldItalic.otf +0 -0
  140. package/src/app/fonts.ts +39 -0
  141. package/src/app/globals.css +15 -0
  142. package/src/app/layout.tsx +43 -0
  143. package/src/app/logo.png +0 -0
  144. package/src/app/page.tsx +46 -0
  145. package/src/app/preview/[...slug]/page.tsx +157 -0
  146. package/src/app/preview/[...slug]/preview.tsx +216 -0
  147. package/src/app/preview/[...slug]/rendering-error.tsx +40 -0
  148. package/src/components/button.tsx +101 -0
  149. package/src/components/code-container.tsx +164 -0
  150. package/src/components/code-snippet.tsx +9 -0
  151. package/src/components/code.tsx +184 -0
  152. package/src/components/heading.tsx +113 -0
  153. package/src/components/icons/icon-arrow-down.tsx +16 -0
  154. package/src/components/icons/icon-base.tsx +26 -0
  155. package/src/components/icons/icon-bug.tsx +19 -0
  156. package/src/components/icons/icon-button.tsx +23 -0
  157. package/src/components/icons/icon-check.tsx +19 -0
  158. package/src/components/icons/icon-clipboard.tsx +40 -0
  159. package/src/components/icons/icon-download.tsx +19 -0
  160. package/src/components/icons/icon-email.tsx +18 -0
  161. package/src/components/icons/icon-file.tsx +19 -0
  162. package/src/components/icons/icon-folder-open.tsx +19 -0
  163. package/src/components/icons/icon-folder.tsx +18 -0
  164. package/src/components/icons/icon-hide-sidebar.tsx +23 -0
  165. package/src/components/icons/icon-image.tsx +19 -0
  166. package/src/components/icons/icon-info.tsx +18 -0
  167. package/src/components/icons/icon-link.tsx +14 -0
  168. package/src/components/icons/icon-monitor.tsx +19 -0
  169. package/src/components/icons/icon-phone.tsx +26 -0
  170. package/src/components/icons/icon-reload.tsx +18 -0
  171. package/src/components/icons/icon-source.tsx +19 -0
  172. package/src/components/icons/icon-stamp.tsx +14 -0
  173. package/src/components/icons/icon-warning.tsx +31 -0
  174. package/src/components/index.ts +7 -0
  175. package/src/components/logo.tsx +63 -0
  176. package/src/components/resizable-wrapper.tsx +173 -0
  177. package/src/components/send.tsx +134 -0
  178. package/src/components/shell.tsx +92 -0
  179. package/src/components/sidebar/file-tree-directory-children.tsx +139 -0
  180. package/src/components/sidebar/file-tree-directory.tsx +92 -0
  181. package/src/components/sidebar/file-tree.tsx +31 -0
  182. package/src/components/sidebar/index.ts +1 -0
  183. package/src/components/sidebar/sidebar.tsx +43 -0
  184. package/src/components/text.tsx +99 -0
  185. package/src/components/toolbar/checking-results.tsx +150 -0
  186. package/src/components/toolbar/code-preview-line-link.tsx +40 -0
  187. package/src/components/toolbar/compatibility.tsx +113 -0
  188. package/src/components/toolbar/linter.tsx +278 -0
  189. package/src/components/toolbar/results-table.tsx +0 -0
  190. package/src/components/toolbar/results.tsx +51 -0
  191. package/src/components/toolbar/spam-assassin.tsx +155 -0
  192. package/src/components/toolbar/toolbar-button.tsx +52 -0
  193. package/src/components/toolbar/use-cached-state.ts +33 -0
  194. package/src/components/toolbar.tsx +349 -0
  195. package/src/components/tooltip-content.tsx +31 -0
  196. package/src/components/tooltip.tsx +19 -0
  197. package/src/components/topbar/active-view-toggle-group.tsx +86 -0
  198. package/src/components/topbar/view-size-controls.tsx +247 -0
  199. package/src/components/topbar.tsx +59 -0
  200. package/src/contexts/emails.tsx +59 -0
  201. package/src/contexts/fragment-identifier.tsx +48 -0
  202. package/src/contexts/preview.tsx +79 -0
  203. package/src/hooks/use-clamped-state.ts +24 -0
  204. package/src/hooks/use-email-rendering-result.ts +58 -0
  205. package/src/hooks/use-fragment-identifier.ts +14 -0
  206. package/src/hooks/use-hot-reload.ts +31 -0
  207. package/src/hooks/use-icon-animation.ts +41 -0
  208. package/src/hooks/use-rendering-metadata.ts +36 -0
  209. package/src/utils/__snapshots__/get-email-component.spec.ts.snap +3 -0
  210. package/src/utils/caniemail/all-css-properties.ts +358 -0
  211. package/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap +74 -0
  212. package/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap +24 -0
  213. package/src/utils/caniemail/ast/get-object-variables.spec.ts +19 -0
  214. package/src/utils/caniemail/ast/get-object-variables.ts +61 -0
  215. package/src/utils/caniemail/ast/get-used-style-properties.spec.ts +23 -0
  216. package/src/utils/caniemail/ast/get-used-style-properties.ts +91 -0
  217. package/src/utils/caniemail/get-compatibility-stats-for-entry.ts +118 -0
  218. package/src/utils/caniemail/get-css-functions.ts +25 -0
  219. package/src/utils/caniemail/get-css-property-names.ts +32 -0
  220. package/src/utils/caniemail/get-css-property-with-value.ts +14 -0
  221. package/src/utils/caniemail/get-css-unit.ts +3 -0
  222. package/src/utils/caniemail/get-element-attributes.ts +7 -0
  223. package/src/utils/caniemail/get-element-names.ts +20 -0
  224. package/src/utils/caniemail/tailwind/generate-tailwind-rules.ts +30 -0
  225. package/src/utils/caniemail/tailwind/get-tailwind-config.ts +187 -0
  226. package/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts +25 -0
  227. package/src/utils/caniemail/tailwind/get-tailwind-metadata.ts +45 -0
  228. package/src/utils/caniemail/tailwind/setup-tailwind-context.ts +15 -0
  229. package/src/utils/cn.ts +6 -0
  230. package/src/utils/constants.ts +6 -0
  231. package/src/utils/contains-email-template.spec.ts +124 -0
  232. package/src/utils/contains-email-template.ts +33 -0
  233. package/src/utils/copy-text-to-clipboard.ts +7 -0
  234. package/src/utils/esbuild/escape-string-for-regex.ts +3 -0
  235. package/src/utils/esbuild/renderring-utilities-exporter.ts +63 -0
  236. package/src/utils/get-email-component.spec.ts +41 -0
  237. package/src/utils/get-email-component.ts +134 -0
  238. package/src/utils/get-emails-directory-metadata.spec.ts +82 -0
  239. package/src/utils/get-emails-directory-metadata.ts +141 -0
  240. package/src/utils/get-line-and-column-from-offset.spec.ts +11 -0
  241. package/src/utils/get-line-and-column-from-offset.ts +11 -0
  242. package/src/utils/improve-error-with-sourcemap.ts +85 -0
  243. package/src/utils/index.ts +6 -0
  244. package/src/utils/js-email-detection.spec.ts +24 -0
  245. package/src/utils/language-map.ts +7 -0
  246. package/src/utils/linting.ts +60 -0
  247. package/src/utils/load-stream.ts +15 -0
  248. package/src/utils/register-spinner-autostopping.ts +28 -0
  249. package/src/utils/result.ts +49 -0
  250. package/src/utils/run-bundled-code.ts +64 -0
  251. package/src/utils/sanitize.ts +6 -0
  252. package/src/utils/static-node-modules-for-vm.ts +93 -0
  253. package/src/utils/testing/js-email-export-default.js +17 -0
  254. package/src/utils/testing/js-email-test.js +18 -0
  255. package/src/utils/testing/mdx-email-test.js +128 -0
  256. package/src/utils/testing/request-response-email.tsx +9 -0
  257. package/src/utils/types/as.ts +26 -0
  258. package/src/utils/types/email-template.ts +8 -0
  259. package/src/utils/types/error-object.ts +11 -0
  260. package/src/utils/types/hot-reload-change.ts +13 -0
  261. package/src/utils/unreachable.ts +8 -0
  262. package/tailwind-internals.d.ts +133 -0
  263. package/tailwind.config.ts +99 -0
  264. package/tsconfig.json +45 -0
  265. package/vitest.config.ts +15 -0
@@ -0,0 +1,134 @@
1
+ import * as Popover from '@radix-ui/react-popover';
2
+ import * as React from 'react';
3
+ import { toast } from 'sonner';
4
+ import { Button } from './button';
5
+ import { Text } from './text';
6
+
7
+ export const Send = ({ markup }: { markup: string }) => {
8
+ const [to, setTo] = React.useState('');
9
+ const [subject, setSubject] = React.useState('Testing React Email');
10
+ const [isSending, setIsSending] = React.useState(false);
11
+ const [isPopOverOpen, setIsPopOverOpen] = React.useState(false);
12
+
13
+ const onFormSubmit = async (e: React.FormEvent) => {
14
+ try {
15
+ e.preventDefault();
16
+ setIsSending(true);
17
+
18
+ const response = await fetch('https://react.email/api/send/test', {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body: JSON.stringify({
22
+ to,
23
+ subject,
24
+ html: markup,
25
+ }),
26
+ });
27
+
28
+ if (response.status === 429) {
29
+ const { error } = (await response.json()) as { error: string };
30
+ toast.error(error);
31
+ }
32
+
33
+ toast.success('Email sent! Check your inbox.');
34
+ } catch (exception) {
35
+ toast.error('Something went wrong. Please try again.');
36
+ } finally {
37
+ setIsSending(false);
38
+ }
39
+ };
40
+
41
+ return (
42
+ <Popover.Root
43
+ onOpenChange={() => {
44
+ if (!isPopOverOpen) {
45
+ document.body.classList.add('popup-open');
46
+ setIsPopOverOpen(true);
47
+ } else {
48
+ document.body.classList.remove('popup-open');
49
+ setIsPopOverOpen(false);
50
+ }
51
+ }}
52
+ open={isPopOverOpen}
53
+ >
54
+ <Popover.Trigger asChild>
55
+ <button
56
+ className="box-border flex h-5 w-20 items-center justify-center self-center rounded-lg border border-slate-6 bg-slate-2 px-4 py-4 text-center font-sans text-sm text-slate-11 outline-none transition duration-300 ease-in-out hover:border-slate-10 hover:text-slate-12"
57
+ type="submit"
58
+ >
59
+ Send
60
+ </button>
61
+ </Popover.Trigger>
62
+ <Popover.Anchor />
63
+ <Popover.Portal>
64
+ <Popover.Content
65
+ align="end"
66
+ className="-mt-10 w-80 rounded-lg border border-slate-6 bg-black/70 p-3 font-sans text-slate-11 shadow-md backdrop-blur-lg font-sans"
67
+ sideOffset={48}
68
+ >
69
+ <form className="mt-1" onSubmit={(e) => void onFormSubmit(e)}>
70
+ <label
71
+ className="mb-2 block text-xs uppercase text-slate-10"
72
+ htmlFor="to"
73
+ >
74
+ Recipient
75
+ </label>
76
+ <input
77
+ autoFocus
78
+ className="mb-3 w-full appearance-none rounded-lg border border-slate-6 bg-slate-3 px-2 py-1 text-sm text-slate-12 placeholder-slate-10 outline-none transition duration-300 ease-in-out focus:ring-1 focus:ring-slate-10"
79
+ defaultValue={to}
80
+ id="to"
81
+ onChange={(e) => {
82
+ setTo(e.target.value);
83
+ }}
84
+ placeholder="you@example.com"
85
+ required
86
+ type="email"
87
+ />
88
+ <label
89
+ className="mb-2 mt-1 block text-xs uppercase text-slate-10"
90
+ htmlFor="subject"
91
+ >
92
+ Subject
93
+ </label>
94
+ <input
95
+ className="mb-3 w-full appearance-none rounded-lg border border-slate-6 bg-slate-3 px-2 py-1 text-sm text-slate-12 placeholder-slate-10 outline-none transition duration-300 ease-in-out focus:ring-1 focus:ring-slate-10"
96
+ defaultValue={subject}
97
+ id="subject"
98
+ onChange={(e) => {
99
+ setSubject(e.target.value);
100
+ }}
101
+ placeholder="My Email"
102
+ required
103
+ type="text"
104
+ />
105
+ <input
106
+ className="appearance-none checked:bg-blue-500"
107
+ type="checkbox"
108
+ />
109
+ <div className="mt-3 flex items-center justify-between">
110
+ <Text className="inline-block" size="1">
111
+ Powered by{' '}
112
+ <a
113
+ className="text-white/85 transition duration-300 ease-in-out hover:text-slate-12"
114
+ href="https://resend.com"
115
+ rel="noreferrer"
116
+ target="_blank"
117
+ >
118
+ Resend
119
+ </a>
120
+ </Text>
121
+ <Button
122
+ className="disabled:border-transparent disabled:bg-slate-11"
123
+ disabled={subject.length === 0 || to.length === 0 || isSending}
124
+ type="submit"
125
+ >
126
+ Send
127
+ </Button>
128
+ </div>
129
+ </form>
130
+ </Popover.Content>
131
+ </Popover.Portal>
132
+ </Popover.Root>
133
+ );
134
+ };
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { cn } from '../utils';
5
+ import { Logo } from './logo';
6
+ import { Sidebar } from './sidebar';
7
+
8
+ interface ShellProps {
9
+ children: React.ReactNode;
10
+ currentEmailOpenSlug?: string;
11
+ }
12
+
13
+ interface ShellContextValue {
14
+ sidebarToggled: boolean;
15
+ toggleSidebar: () => void;
16
+ }
17
+
18
+ export const ShellContext = React.createContext<ShellContextValue | undefined>(
19
+ undefined,
20
+ );
21
+
22
+ export const Shell = ({ children, currentEmailOpenSlug }: ShellProps) => {
23
+ const [sidebarToggled, setSidebarToggled] = React.useState(true);
24
+
25
+ return (
26
+ <ShellContext.Provider
27
+ value={{
28
+ toggleSidebar: () => setSidebarToggled((v) => !v),
29
+ sidebarToggled,
30
+ }}
31
+ >
32
+ <div
33
+ className={
34
+ 'flex h-[4.375rem] items-center justify-between border-slate-6 border-b px-6 lg:hidden'
35
+ }
36
+ >
37
+ <div className="flex h-[4.375rem] items-center">
38
+ <Logo />
39
+ </div>
40
+ <button
41
+ className="flex h-6 w-6 items-center justify-center rounded text-white"
42
+ onClick={() => {
43
+ setSidebarToggled((v) => !v);
44
+ }}
45
+ type="button"
46
+ >
47
+ <svg
48
+ fill="none"
49
+ height="16"
50
+ stroke="white"
51
+ viewBox="0 0 15 15"
52
+ width="16"
53
+ xmlns="http://www.w3.org/2000/svg"
54
+ >
55
+ <title>Menu</title>
56
+ <path
57
+ clipRule="evenodd"
58
+ d="M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386 13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5 8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761 13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z"
59
+ fill="currentColor"
60
+ fillRule="evenodd"
61
+ />
62
+ </svg>
63
+ </button>
64
+ </div>
65
+ <div className="w-[100dvw] flex h-[calc(100dvh-4.375rem)] lg:h-[100dvh]">
66
+ <React.Suspense>
67
+ <Sidebar
68
+ className={cn(
69
+ 'fixed top-[4.375rem] left-0 z-[9999] h-full max-h-full w-full max-w-full will-change-auto [transition:width_0.2s_ease-in-out]',
70
+ 'lg:static lg:inline-block lg:z-auto lg:max-h-full lg:w-[16rem]',
71
+ {
72
+ '-translate-x-full lg:translate-x-0': sidebarToggled,
73
+ 'lg:w-0': !sidebarToggled,
74
+ },
75
+ )}
76
+ currentEmailOpenSlug={currentEmailOpenSlug}
77
+ />
78
+ </React.Suspense>
79
+ <main
80
+ className={cn(
81
+ 'inline-block relative overflow-hidden will-change-[width]',
82
+ 'w-full h-full',
83
+ '[transition:width_0.2s_ease-in-out,_transform_0.2s_ease-in-out]',
84
+ sidebarToggled && 'lg:w-[calc(100%-16rem)]',
85
+ )}
86
+ >
87
+ {children}
88
+ </main>
89
+ </div>
90
+ </ShellContext.Provider>
91
+ );
92
+ };
@@ -0,0 +1,139 @@
1
+ import * as Collapsible from '@radix-ui/react-collapsible';
2
+ import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
3
+ import Link from 'next/link';
4
+ import { useRouter, useSearchParams } from 'next/navigation';
5
+ import { cn } from '../../utils';
6
+ import type { EmailsDirectory } from '../../utils/get-emails-directory-metadata';
7
+ import { IconFile } from '../icons/icon-file';
8
+ import { FileTreeDirectory } from './file-tree-directory';
9
+
10
+ export const FileTreeDirectoryChildren = (props: {
11
+ emailsDirectoryMetadata: EmailsDirectory;
12
+ currentEmailOpenSlug?: string;
13
+ open: boolean;
14
+ isRoot?: boolean;
15
+ }) => {
16
+ const searchParams = useSearchParams();
17
+ const router = useRouter();
18
+
19
+ return (
20
+ <AnimatePresence initial={false}>
21
+ {props.open ? (
22
+ <Collapsible.Content
23
+ asChild
24
+ className="relative overflow-y-hidden pl-1"
25
+ forceMount
26
+ >
27
+ <motion.div
28
+ animate={{ opacity: 1, height: 'auto' }}
29
+ exit={{ opacity: 0, height: 0 }}
30
+ initial={{ opacity: 0, height: 0 }}
31
+ >
32
+ {props.isRoot ? null : (
33
+ <div className="line absolute left-2.5 h-full w-px bg-slate-6" />
34
+ )}
35
+ <div className="flex flex-col truncate">
36
+ <LayoutGroup id="sidebar">
37
+ {props.emailsDirectoryMetadata.subDirectories.map(
38
+ (subDirectory) => (
39
+ <FileTreeDirectory
40
+ className="p-0 data-[state=open]:mb-2"
41
+ currentEmailOpenSlug={props.currentEmailOpenSlug}
42
+ emailsDirectoryMetadata={subDirectory}
43
+ key={subDirectory.absolutePath}
44
+ />
45
+ ),
46
+ )}
47
+ {props.emailsDirectoryMetadata.emailFilenames.map(
48
+ (emailFilename, index) => {
49
+ const emailSlug = props.isRoot
50
+ ? emailFilename
51
+ : `${props.emailsDirectoryMetadata.relativePath}/${emailFilename}`;
52
+
53
+ const removeExtensionFrom = (path: string) => {
54
+ if (
55
+ path.split('.').pop() === 'tsx' ||
56
+ path.split('.').pop() === 'jsx' ||
57
+ path.split('.').pop() === 'js'
58
+ ) {
59
+ return path.split('.').slice(0, -1).join('.');
60
+ }
61
+
62
+ return path;
63
+ };
64
+ const isCurrentPage = props.currentEmailOpenSlug
65
+ ? removeExtensionFrom(props.currentEmailOpenSlug) ===
66
+ emailSlug
67
+ : false;
68
+
69
+ return (
70
+ <Link
71
+ href={{
72
+ pathname: `/preview/${emailSlug}`,
73
+ search: searchParams.toString(),
74
+ }}
75
+ onMouseOver={() => {
76
+ router.prefetch(
77
+ `/preview/${emailSlug}?${searchParams.toString()}`,
78
+ );
79
+ }}
80
+ key={emailSlug}
81
+ >
82
+ <motion.span
83
+ animate={{ x: 0, opacity: 1 }}
84
+ className={cn(
85
+ 'relative flex h-8 w-full items-center text-start gap-2 rounded-md align-middle text-slate-11 text-sm transition-colors duration-100 ease-[cubic-bezier(.6,.12,.34,.96)]',
86
+ props.isRoot ? undefined : 'pl-3',
87
+ {
88
+ 'text-cyan-11': isCurrentPage,
89
+ 'hover:text-slate-12':
90
+ props.currentEmailOpenSlug !== emailSlug,
91
+ },
92
+ )}
93
+ initial={{ x: -10 + -index * 1.5, opacity: 0 }}
94
+ transition={{
95
+ x: { delay: 0.03 * index, duration: 0.2 },
96
+ opacity: { delay: 0.03 * index, duration: 0.2 },
97
+ }}
98
+ >
99
+ {isCurrentPage ? (
100
+ <motion.span
101
+ animate={{ opacity: 1 }}
102
+ className="absolute inset-0 rounded-md bg-cyan-5 opacity-0 transition-all duration-200 ease-[cubic-bezier(.6,.12,.34,.96)]"
103
+ exit={{ opacity: 0 }}
104
+ initial={{ opacity: 0 }}
105
+ >
106
+ {props.isRoot ? null : (
107
+ <motion.div
108
+ className="absolute top-1 left-[0.4rem] inset-0 h-6 w-px rounded-sm bg-cyan-11"
109
+ layoutId="active-file"
110
+ transition={{
111
+ type: 'spring',
112
+ bounce: 0.2,
113
+ duration: 0.6,
114
+ }}
115
+ />
116
+ )}
117
+ </motion.span>
118
+ ) : null}
119
+ <IconFile
120
+ className="h-5 w-5"
121
+ height="20"
122
+ width="20"
123
+ />
124
+ <span className="truncate w-[calc(100%-1.25rem)]">
125
+ {emailFilename}
126
+ </span>
127
+ </motion.span>
128
+ </Link>
129
+ );
130
+ },
131
+ )}
132
+ </LayoutGroup>
133
+ </div>
134
+ </motion.div>
135
+ </Collapsible.Content>
136
+ ) : null}
137
+ </AnimatePresence>
138
+ );
139
+ };
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+ import * as Collapsible from '@radix-ui/react-collapsible';
3
+ import * as React from 'react';
4
+ import { cn } from '../../utils';
5
+ import type { EmailsDirectory } from '../../utils/get-emails-directory-metadata';
6
+ import { Heading } from '../heading';
7
+ import { IconArrowDown } from '../icons/icon-arrow-down';
8
+ import { IconFolder } from '../icons/icon-folder';
9
+ import { IconFolderOpen } from '../icons/icon-folder-open';
10
+ import { FileTreeDirectoryChildren } from './file-tree-directory-children';
11
+
12
+ interface SidebarDirectoryProps {
13
+ emailsDirectoryMetadata: EmailsDirectory;
14
+ className?: string;
15
+ currentEmailOpenSlug?: string;
16
+ }
17
+
18
+ const persistedOpenDirectories = new Set<string>();
19
+
20
+ export const FileTreeDirectory = ({
21
+ emailsDirectoryMetadata: directoryMetadata,
22
+ className,
23
+ currentEmailOpenSlug,
24
+ }: SidebarDirectoryProps) => {
25
+ const doesDirectoryContainCurrentEmailOpen = currentEmailOpenSlug
26
+ ? currentEmailOpenSlug.includes(directoryMetadata.relativePath)
27
+ : false;
28
+
29
+ const isEmpty =
30
+ directoryMetadata.emailFilenames.length === 0 &&
31
+ directoryMetadata.subDirectories.length === 0;
32
+
33
+ const [open, setOpen] = React.useState(
34
+ persistedOpenDirectories.has(directoryMetadata.absolutePath) ||
35
+ doesDirectoryContainCurrentEmailOpen,
36
+ );
37
+
38
+ return (
39
+ <Collapsible.Root
40
+ className={cn('group', className)}
41
+ onOpenChange={(isOpening) => {
42
+ if (isOpening) {
43
+ persistedOpenDirectories.add(directoryMetadata.absolutePath);
44
+ } else {
45
+ persistedOpenDirectories.delete(directoryMetadata.absolutePath);
46
+ }
47
+
48
+ setOpen(isOpening);
49
+ }}
50
+ open={open}
51
+ >
52
+ <Collapsible.Trigger
53
+ className={cn(
54
+ 'mt-1 mb-1.5 flex w-full items-center text-start justify-between gap-2 font-medium text-[14px]',
55
+ {
56
+ 'cursor-pointer': !isEmpty,
57
+ },
58
+ )}
59
+ >
60
+ {open ? (
61
+ <IconFolderOpen className="w-[20px]" height="20" width="20" />
62
+ ) : (
63
+ <IconFolder height="20" width="20" />
64
+ )}
65
+ <Heading
66
+ as="h3"
67
+ className="transition grow w-[calc(100%-40px)] truncate duration-200 ease-in-out hover:text-slate-12"
68
+ color="gray"
69
+ size="2"
70
+ weight="medium"
71
+ >
72
+ {directoryMetadata.directoryName}
73
+ </Heading>
74
+ {!isEmpty ? (
75
+ <IconArrowDown
76
+ width="20"
77
+ height="20"
78
+ className="ml-auto opacity-60 transition-transform data-[open=true]:rotate-180"
79
+ data-open={open}
80
+ />
81
+ ) : null}
82
+ </Collapsible.Trigger>
83
+ {!isEmpty ? (
84
+ <FileTreeDirectoryChildren
85
+ currentEmailOpenSlug={currentEmailOpenSlug}
86
+ emailsDirectoryMetadata={directoryMetadata}
87
+ open={open}
88
+ />
89
+ ) : null}
90
+ </Collapsible.Root>
91
+ );
92
+ };
@@ -0,0 +1,31 @@
1
+ import * as Collapsible from '@radix-ui/react-collapsible';
2
+ import * as React from 'react';
3
+ import type { EmailsDirectory } from '../../utils/get-emails-directory-metadata';
4
+ import { FileTreeDirectoryChildren } from './file-tree-directory-children';
5
+
6
+ interface FileTreeProps {
7
+ currentEmailOpenSlug: string | undefined;
8
+ emailsDirectoryMetadata: EmailsDirectory;
9
+ }
10
+
11
+ export const FileTree = ({
12
+ currentEmailOpenSlug,
13
+ emailsDirectoryMetadata,
14
+ }: FileTreeProps) => {
15
+ return (
16
+ <div className="flex w-full h-full flex-col lg:w-full lg:min-w-[14.5rem]">
17
+ <nav className="flex flex-grow flex-col p-4 pr-0 pl-0">
18
+ <Collapsible.Root open>
19
+ <React.Suspense>
20
+ <FileTreeDirectoryChildren
21
+ currentEmailOpenSlug={currentEmailOpenSlug}
22
+ emailsDirectoryMetadata={emailsDirectoryMetadata}
23
+ isRoot
24
+ open
25
+ />
26
+ </React.Suspense>
27
+ </Collapsible.Root>
28
+ </nav>
29
+ </div>
30
+ );
31
+ };
@@ -0,0 +1 @@
1
+ export * from './sidebar';
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+ import { clsx } from 'clsx';
3
+ import { useEmails } from '../../contexts/emails';
4
+ import { cn } from '../../utils';
5
+ import { Logo } from '../logo';
6
+ import { FileTree } from './file-tree';
7
+
8
+ interface SidebarProps {
9
+ className?: string;
10
+ currentEmailOpenSlug?: string;
11
+ }
12
+
13
+ export const Sidebar = ({ className, currentEmailOpenSlug }: SidebarProps) => {
14
+ const { emailsDirectoryMetadata } = useEmails();
15
+
16
+ return (
17
+ <aside
18
+ className={cn(
19
+ 'overflow-hidden',
20
+ 'lg:static lg:z-auto lg:max-h-screen lg:w-[16rem]',
21
+ className,
22
+ )}
23
+ >
24
+ <div className="flex w-full h-full overflow-hidden flex-col border-slate-6 border-r">
25
+ <div
26
+ className={clsx(
27
+ 'hidden min-h-[3.3125rem] flex-shrink items-center p-3 px-4 lg:flex',
28
+ )}
29
+ >
30
+ <h2>
31
+ <Logo />
32
+ </h2>
33
+ </div>
34
+ <div className="relative grow w-full h-full overflow-y-auto overflow-x-hidden border-slate-4 border-t px-4 pb-3">
35
+ <FileTree
36
+ currentEmailOpenSlug={currentEmailOpenSlug}
37
+ emailsDirectoryMetadata={emailsDirectoryMetadata}
38
+ />
39
+ </div>
40
+ </div>
41
+ </aside>
42
+ );
43
+ };
@@ -0,0 +1,99 @@
1
+ import * as SlotPrimitive from '@radix-ui/react-slot';
2
+ import * as React from 'react';
3
+ import { type As, cn, unreachable } from '../utils';
4
+
5
+ export type TextSize = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
6
+ export type TextColor = 'gray' | 'white';
7
+ export type TextTransform = 'uppercase' | 'lowercase' | 'capitalize';
8
+ export type TextWeight = 'normal' | 'medium';
9
+
10
+ interface TextOwnProps {
11
+ size?: TextSize;
12
+ color?: TextColor;
13
+ transform?: TextTransform;
14
+ weight?: TextWeight;
15
+ }
16
+
17
+ type TextProps = As<'span', 'div', 'p'> & TextOwnProps;
18
+
19
+ export const Text = React.forwardRef<HTMLSpanElement, Readonly<TextProps>>(
20
+ (
21
+ {
22
+ as: Tag = 'span',
23
+ size = '2',
24
+ color = 'gray',
25
+ transform,
26
+ weight = 'normal',
27
+ className,
28
+ children,
29
+ ...props
30
+ },
31
+ forwardedRef,
32
+ ) => (
33
+ <SlotPrimitive.Slot
34
+ className={cn(
35
+ className,
36
+ transform,
37
+ getSizesClassNames(size),
38
+ getColorClassNames(color),
39
+ getWeightClassNames(weight),
40
+ )}
41
+ ref={forwardedRef}
42
+ {...props}
43
+ >
44
+ <Tag>{children}</Tag>
45
+ </SlotPrimitive.Slot>
46
+ ),
47
+ );
48
+
49
+ const getSizesClassNames = (size: TextSize | undefined) => {
50
+ switch (size) {
51
+ case '1':
52
+ return 'text-xs';
53
+ case undefined:
54
+ case '2':
55
+ return 'text-sm';
56
+ case '3':
57
+ return 'text-base';
58
+ case '4':
59
+ return 'text-lg';
60
+ case '5':
61
+ return ['text-17px', 'md:text-xl tracking-[-0.16px]'];
62
+ case '6':
63
+ return 'text-2xl tracking-[-0.288px]';
64
+ case '7':
65
+ return 'text-[28px] leading-[34px] tracking-[-0.416px]';
66
+ case '8':
67
+ return 'text-[35px] leading-[42px] tracking-[-0.64px]';
68
+ case '9':
69
+ return 'text-6xl leading-[73px] tracking-[-0.896px]';
70
+ default:
71
+ return unreachable(size);
72
+ }
73
+ };
74
+
75
+ const getColorClassNames = (color: TextColor | undefined) => {
76
+ switch (color) {
77
+ case 'white':
78
+ return 'text-slate-12';
79
+ case undefined:
80
+ case 'gray':
81
+ return 'text-slate-11';
82
+ default:
83
+ return unreachable(color);
84
+ }
85
+ };
86
+
87
+ const getWeightClassNames = (weight: TextWeight | undefined) => {
88
+ switch (weight) {
89
+ case undefined:
90
+ case 'normal':
91
+ return 'font-normal';
92
+ case 'medium':
93
+ return 'font-medium';
94
+ default:
95
+ return unreachable(weight);
96
+ }
97
+ };
98
+
99
+ Text.displayName = 'Text';