@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,86 @@
1
+ import * as ToggleGroup from '@radix-ui/react-toggle-group';
2
+ import { motion } from 'framer-motion';
3
+ import { cn } from '../../utils';
4
+ import { tabTransition } from '../../utils/constants';
5
+ import { IconMonitor } from '../icons/icon-monitor';
6
+ import { IconSource } from '../icons/icon-source';
7
+ import { Tooltip } from '../tooltip';
8
+
9
+ interface ActiveViewToggleGroupProps {
10
+ activeView: string;
11
+ setActiveView: (view: string) => void;
12
+ }
13
+
14
+ export const ActiveViewToggleGroup = ({
15
+ activeView,
16
+ setActiveView,
17
+ }: ActiveViewToggleGroupProps) => {
18
+ return (
19
+ <ToggleGroup.Root
20
+ aria-label="View mode"
21
+ className="inline-block items-center bg-slate-2 border border-slate-6 rounded-md overflow-hidden h-[36px]"
22
+ onValueChange={(value) => {
23
+ if (value) setActiveView(value);
24
+ }}
25
+ type="single"
26
+ value={activeView}
27
+ >
28
+ <ToggleGroup.Item value="preview">
29
+ <Tooltip>
30
+ <Tooltip.Trigger asChild>
31
+ <div
32
+ className={cn(
33
+ 'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
34
+ {
35
+ 'text-slate-11': activeView !== 'desktop',
36
+ 'text-slate-12': activeView === 'desktop',
37
+ },
38
+ )}
39
+ >
40
+ {activeView === 'preview' && (
41
+ <motion.span
42
+ animate={{ opacity: 1 }}
43
+ className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
44
+ exit={{ opacity: 0 }}
45
+ initial={{ opacity: 0 }}
46
+ layoutId="topbar-tabs"
47
+ transition={tabTransition}
48
+ />
49
+ )}
50
+ <IconMonitor />
51
+ </div>
52
+ </Tooltip.Trigger>
53
+ <Tooltip.Content>Preview</Tooltip.Content>
54
+ </Tooltip>
55
+ </ToggleGroup.Item>
56
+ <ToggleGroup.Item value="source">
57
+ <Tooltip>
58
+ <Tooltip.Trigger asChild>
59
+ <div
60
+ className={cn(
61
+ 'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
62
+ {
63
+ 'text-slate-11': activeView !== 'source',
64
+ 'text-slate-12': activeView === 'source',
65
+ },
66
+ )}
67
+ >
68
+ {activeView === 'source' && (
69
+ <motion.span
70
+ animate={{ opacity: 1 }}
71
+ className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
72
+ exit={{ opacity: 0 }}
73
+ initial={{ opacity: 0 }}
74
+ layoutId="topbar-tabs"
75
+ transition={tabTransition}
76
+ />
77
+ )}
78
+ <IconSource />
79
+ </div>
80
+ </Tooltip.Trigger>
81
+ <Tooltip.Content>Code</Tooltip.Content>
82
+ </Tooltip>
83
+ </ToggleGroup.Item>
84
+ </ToggleGroup.Root>
85
+ );
86
+ };
@@ -0,0 +1,247 @@
1
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
2
+ import { motion } from 'framer-motion';
3
+ import * as React from 'react';
4
+ import { cn } from '../../utils';
5
+ import { IconArrowDown } from '../icons/icon-arrow-down';
6
+ import { Tooltip } from '../tooltip';
7
+
8
+ interface ViewDimensions {
9
+ width: number;
10
+ height: number;
11
+ }
12
+
13
+ interface ViewSizeControlsProps {
14
+ viewWidth: number;
15
+ setViewWidth: (width: number) => void;
16
+ viewHeight: number;
17
+ setViewHeight: (height: number) => void;
18
+ }
19
+
20
+ interface DimensionInputProps {
21
+ icon: React.ReactNode;
22
+ isActive: boolean;
23
+ label: string;
24
+ onBlur: () => void;
25
+ onChange: (value: number) => void;
26
+ setIsActive: (active: boolean) => void;
27
+ value: number;
28
+ hasBorder?: boolean;
29
+ }
30
+
31
+ interface PresetOption {
32
+ name: string;
33
+ dimensions: ViewDimensions;
34
+ }
35
+
36
+ interface PresetMenuItemProps {
37
+ name: string;
38
+ dimensions: ViewDimensions;
39
+ onSelect: (dimensions: ViewDimensions) => void;
40
+ }
41
+
42
+ const VIEW_PRESETS: PresetOption[] = [
43
+ { name: 'Desktop', dimensions: { width: 600, height: 1024 } },
44
+ { name: 'Mobile', dimensions: { width: 375, height: 812 } },
45
+ ];
46
+
47
+ const inputVariant = {
48
+ active: {
49
+ width: '3.5rem',
50
+ padding: '0 0 0 0.5rem',
51
+ },
52
+ inactive: {
53
+ width: '0',
54
+ },
55
+ };
56
+
57
+ const DimensionInput = ({
58
+ icon,
59
+ isActive,
60
+ label,
61
+ onBlur,
62
+ onChange,
63
+ setIsActive,
64
+ value,
65
+ hasBorder,
66
+ }: DimensionInputProps) => {
67
+ const inputRef = React.useRef<HTMLInputElement>(null);
68
+
69
+ React.useEffect(() => {
70
+ if (isActive && inputRef.current) {
71
+ inputRef.current.focus();
72
+ }
73
+ }, [isActive]);
74
+
75
+ const handleButtonClick = () => {
76
+ if (isActive) {
77
+ setIsActive(false);
78
+ } else {
79
+ setIsActive(true);
80
+ }
81
+ };
82
+
83
+ return (
84
+ <Tooltip.Provider>
85
+ <Tooltip>
86
+ <Tooltip.Trigger asChild>
87
+ <motion.button
88
+ onClick={handleButtonClick}
89
+ className={cn('relative flex items-center justify-center p-2', {
90
+ 'border-slate-6 border-r': hasBorder,
91
+ })}
92
+ >
93
+ {icon}
94
+ <motion.input
95
+ ref={inputRef}
96
+ initial={false}
97
+ animate={isActive ? 'active' : 'inactive'}
98
+ className="arrow-hide relative flex h-8 items-center justify-center bg-black text-sm outline-none"
99
+ onChange={(e) => onChange(Number.parseInt(e.currentTarget.value))}
100
+ onBlur={onBlur}
101
+ type="number"
102
+ value={value}
103
+ variants={inputVariant}
104
+ />
105
+ </motion.button>
106
+ </Tooltip.Trigger>
107
+ <Tooltip.Content>
108
+ <span>{label}: </span>
109
+ <span className="font-mono">{value}px</span>
110
+ </Tooltip.Content>
111
+ </Tooltip>
112
+ </Tooltip.Provider>
113
+ );
114
+ };
115
+
116
+ const PresetMenuItem = ({
117
+ name,
118
+ dimensions,
119
+ onSelect,
120
+ }: PresetMenuItemProps) => (
121
+ <DropdownMenu.Item
122
+ className="group flex w-full cursor-pointer select-none items-center justify-between rounded-md py-1.5 pr-1 pl-2 text-sm outline-none transition-colors data-[highlighted]:bg-slate-5"
123
+ onClick={() => onSelect(dimensions)}
124
+ >
125
+ {name}
126
+ <span className="flex h-fit items-center rounded-full bg-slate-6 px-2 py-1 font-medium text-slate-11 text-xs">
127
+ {dimensions.width}x{dimensions.height}
128
+ </span>
129
+ </DropdownMenu.Item>
130
+ );
131
+
132
+ export const ViewSizeControls = ({
133
+ viewWidth,
134
+ viewHeight,
135
+ setViewWidth,
136
+ setViewHeight,
137
+ }: ViewSizeControlsProps) => {
138
+ const [isDropdownOpen, setIsDropdownOpen] = React.useState(false);
139
+ const [activeInput, setActiveInput] = React.useState<
140
+ 'width' | 'height' | null
141
+ >(null);
142
+
143
+ const handlePresetSelect = (dimensions: ViewDimensions) => {
144
+ setViewWidth(dimensions.width);
145
+ setViewHeight(dimensions.height);
146
+ };
147
+
148
+ const handleBlur = () => {
149
+ setActiveInput(null);
150
+ };
151
+
152
+ return (
153
+ <div className="relative flex h-9 w-fit overflow-hidden rounded-lg border border-slate-6 text-sm transition-colors duration-300 ease-in-out focus-within:border-slate-8 hover:border-slate-8">
154
+ <DimensionInput
155
+ icon={
156
+ <svg
157
+ xmlns="http://www.w3.org/2000/svg"
158
+ width="16"
159
+ height="16"
160
+ viewBox="0 0 24 24"
161
+ fill="none"
162
+ stroke="currentColor"
163
+ strokeWidth="2"
164
+ strokeLinecap="round"
165
+ strokeLinejoin="round"
166
+ >
167
+ <path d="M21 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v3" />
168
+ <path d="M21 16v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3" />
169
+ <path d="M4 12H2" />
170
+ <path d="M10 12H8" />
171
+ <path d="M16 12h-2" />
172
+ <path d="M22 12h-2" />
173
+ </svg>
174
+ }
175
+ value={viewWidth}
176
+ onChange={setViewWidth}
177
+ isActive={activeInput === 'width'}
178
+ setIsActive={(active) => setActiveInput(active ? 'width' : null)}
179
+ onBlur={handleBlur}
180
+ label="Width"
181
+ hasBorder
182
+ />
183
+ <DimensionInput
184
+ icon={
185
+ <svg
186
+ xmlns="http://www.w3.org/2000/svg"
187
+ width="16"
188
+ height="16"
189
+ viewBox="0 0 24 24"
190
+ fill="none"
191
+ stroke="currentColor"
192
+ strokeWidth="2"
193
+ strokeLinecap="round"
194
+ strokeLinejoin="round"
195
+ >
196
+ <path d="M8 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h3" />
197
+ <path d="M16 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3" />
198
+ <path d="M12 20v2" />
199
+ <path d="M12 14v2" />
200
+ <path d="M12 8v2" />
201
+ <path d="M12 2v2" />
202
+ </svg>
203
+ }
204
+ value={viewHeight}
205
+ onChange={setViewHeight}
206
+ isActive={activeInput === 'height'}
207
+ setIsActive={(active) => setActiveInput(active ? 'height' : null)}
208
+ onBlur={handleBlur}
209
+ label="Height"
210
+ />
211
+ <DropdownMenu.Root open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
212
+ <DropdownMenu.Trigger asChild>
213
+ <button
214
+ type="button"
215
+ className="relative flex items-center justify-center overflow-hidden bg-slate-5 p-2 text-slate-11 text-sm leading-none outline-none transition-colors ease-linear focus-within:text-slate-12 hover:text-slate-12 focus:text-slate-12"
216
+ >
217
+ <span className="sr-only">View presets</span>
218
+ <IconArrowDown
219
+ className={cn(
220
+ 'transform transition-transform duration-200 ease-[cubic-bezier(.36,.66,.6,1)]',
221
+ {
222
+ '-rotate-180': isDropdownOpen,
223
+ },
224
+ )}
225
+ />
226
+ </button>
227
+ </DropdownMenu.Trigger>
228
+ <DropdownMenu.Portal>
229
+ <DropdownMenu.Content
230
+ align="end"
231
+ className="flex min-w-[12rem] flex-col gap-2 rounded-md border border-slate-8 border-solid bg-black px-2 py-2 text-white"
232
+ sideOffset={5}
233
+ >
234
+ {VIEW_PRESETS.map((preset) => (
235
+ <PresetMenuItem
236
+ key={preset.name}
237
+ name={preset.name}
238
+ dimensions={preset.dimensions}
239
+ onSelect={handlePresetSelect}
240
+ />
241
+ ))}
242
+ </DropdownMenu.Content>
243
+ </DropdownMenu.Portal>
244
+ </DropdownMenu.Root>
245
+ </div>
246
+ );
247
+ };
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import { use } from 'react';
4
+ import { cn } from '../utils';
5
+ import { Heading } from './heading';
6
+ import { IconHideSidebar } from './icons/icon-hide-sidebar';
7
+ import { ShellContext } from './shell';
8
+ import { Tooltip } from './tooltip';
9
+
10
+ interface TopbarProps extends React.ComponentProps<'header'> {
11
+ emailTitle: string;
12
+ children: React.ReactNode;
13
+ }
14
+
15
+ export const Topbar = ({
16
+ emailTitle,
17
+ children,
18
+ className,
19
+ ...props
20
+ }: TopbarProps) => {
21
+ const { toggleSidebar } = use(ShellContext)!;
22
+
23
+ return (
24
+ <Tooltip.Provider>
25
+ <header
26
+ {...props}
27
+ className={cn(
28
+ 'flex h-14 items-center justify-between gap-3 border-slate-6 border-b px-3 py-2',
29
+ className,
30
+ )}
31
+ >
32
+ <div className="flex w-fit items-center gap-3">
33
+ <Tooltip>
34
+ <Tooltip.Trigger asChild>
35
+ <button
36
+ className="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"
37
+ onClick={() => {
38
+ toggleSidebar();
39
+ }}
40
+ type="button"
41
+ >
42
+ <IconHideSidebar height={20} width={20} />
43
+ </button>
44
+ </Tooltip.Trigger>
45
+ <Tooltip.Content>Show/hide sidebar</Tooltip.Content>
46
+ </Tooltip>
47
+ <div className="hidden items-center overflow-hidden text-center lg:flex">
48
+ <Heading as="h2" className="truncate" size="2" weight="medium">
49
+ {emailTitle}
50
+ </Heading>
51
+ </div>
52
+ </div>
53
+ <div className="flex w-full items-center justify-between gap-3 lg:w-fit lg:justify-start">
54
+ {children}
55
+ </div>
56
+ </header>
57
+ </Tooltip.Provider>
58
+ );
59
+ };
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+ import { createContext, useContext, useState } from 'react';
3
+ import { getEmailsDirectoryMetadataAction } from '../actions/get-emails-directory-metadata-action';
4
+ import { isBuilding } from '../app/env';
5
+ import { useHotreload } from '../hooks/use-hot-reload';
6
+ import type { EmailsDirectory } from '../utils/get-emails-directory-metadata';
7
+
8
+ const EmailsContext = createContext<
9
+ | {
10
+ emailsDirectoryMetadata: EmailsDirectory;
11
+ }
12
+ | undefined
13
+ >(undefined);
14
+
15
+ export const useEmails = () => {
16
+ const providerValue = useContext(EmailsContext);
17
+
18
+ if (typeof providerValue === 'undefined') {
19
+ throw new Error(
20
+ 'Cannot call `useEmail()` outside of an EmailsContext provider!',
21
+ );
22
+ }
23
+
24
+ return providerValue;
25
+ };
26
+
27
+ export const EmailsProvider = (props: {
28
+ initialEmailsDirectoryMetadata: EmailsDirectory;
29
+ children: React.ReactNode;
30
+ }) => {
31
+ const [emailsDirectoryMetadata, setEmailsDirectoryMetadata] =
32
+ useState<EmailsDirectory>(props.initialEmailsDirectoryMetadata);
33
+
34
+ if (!isBuilding) {
35
+ // biome-ignore lint/correctness/useHookAtTopLevel: this will not change on runtime so it doesn't violate the rules of hooks
36
+ useHotreload(async () => {
37
+ const metadata = await getEmailsDirectoryMetadataAction(
38
+ props.initialEmailsDirectoryMetadata.absolutePath,
39
+ );
40
+ if (metadata) {
41
+ setEmailsDirectoryMetadata(metadata);
42
+ } else {
43
+ throw new Error(
44
+ 'Hot reloading: unable to find the emails directory to update the sidebar',
45
+ );
46
+ }
47
+ });
48
+ }
49
+
50
+ return (
51
+ <EmailsContext.Provider
52
+ value={{
53
+ emailsDirectoryMetadata,
54
+ }}
55
+ >
56
+ {props.children}
57
+ </EmailsContext.Provider>
58
+ );
59
+ };
@@ -0,0 +1,48 @@
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
+ }: {
22
+ children: React.ReactNode;
23
+ }) => {
24
+ const [fragmentIdentifier, setFragmentIdentifier] = useState<string>();
25
+ const pathname = usePathname();
26
+ const searchParams = useSearchParams();
27
+
28
+ const update = () => {
29
+ setFragmentIdentifier(location.hash);
30
+ };
31
+
32
+ useEffect(() => {
33
+ update();
34
+ }, [pathname, searchParams]);
35
+
36
+ return (
37
+ <FragmentIdentifierContext.Provider
38
+ value={{
39
+ identifier: fragmentIdentifier,
40
+ update(value: string) {
41
+ setFragmentIdentifier(value);
42
+ },
43
+ }}
44
+ >
45
+ {children}
46
+ </FragmentIdentifierContext.Provider>
47
+ );
48
+ };
@@ -0,0 +1,79 @@
1
+ 'use client';
2
+ import { useRouter } from 'next/navigation';
3
+ import { createContext } from 'react';
4
+ import type {
5
+ EmailRenderingResult,
6
+ RenderedEmailMetadata,
7
+ } from '../actions/render-email-by-path';
8
+ import { isBuilding } from '../app/env';
9
+ import { useEmailRenderingResult } from '../hooks/use-email-rendering-result';
10
+ import { useHotreload } from '../hooks/use-hot-reload';
11
+ import { useRenderingMetadata } from '../hooks/use-rendering-metadata';
12
+
13
+ export const PreviewContext = createContext<
14
+ | {
15
+ renderedEmailMetadata: RenderedEmailMetadata | undefined;
16
+ renderingResult: EmailRenderingResult;
17
+
18
+ emailSlug: string;
19
+ emailPath: string;
20
+ }
21
+ | undefined
22
+ >(undefined);
23
+
24
+ interface PreviewProvider {
25
+ emailSlug: string;
26
+ emailPath: string;
27
+
28
+ serverRenderingResult: EmailRenderingResult;
29
+
30
+ children: React.ReactNode;
31
+ }
32
+
33
+ export const PreviewProvider = ({
34
+ emailSlug,
35
+ emailPath,
36
+ serverRenderingResult,
37
+ children,
38
+ }: PreviewProvider) => {
39
+ const router = useRouter();
40
+
41
+ const renderingResult = useEmailRenderingResult(
42
+ emailPath,
43
+ serverRenderingResult,
44
+ );
45
+
46
+ const renderedEmailMetadata = useRenderingMetadata(
47
+ emailPath,
48
+ renderingResult,
49
+ serverRenderingResult,
50
+ );
51
+
52
+ if (!isBuilding) {
53
+ // biome-ignore lint/correctness/useHookAtTopLevel: this will not change on runtime so it doesn't violate the rules of hooks
54
+ useHotreload((changes) => {
55
+ const changeForThisEmail = changes.find((change) =>
56
+ change.filename.includes(emailSlug),
57
+ );
58
+
59
+ if (typeof changeForThisEmail !== 'undefined') {
60
+ if (changeForThisEmail.event === 'unlink') {
61
+ router.push('/');
62
+ }
63
+ }
64
+ });
65
+ }
66
+
67
+ return (
68
+ <PreviewContext.Provider
69
+ value={{
70
+ emailPath,
71
+ emailSlug,
72
+ renderedEmailMetadata,
73
+ renderingResult,
74
+ }}
75
+ >
76
+ {children}
77
+ </PreviewContext.Provider>
78
+ );
79
+ };
@@ -0,0 +1,24 @@
1
+ import { useState } from 'react';
2
+
3
+ const clamp = (v: number, min: number, max: number) => {
4
+ return Math.min(Math.max(v, min), max);
5
+ };
6
+
7
+ export const useClampedState = (initial: number, min: number, max: number) => {
8
+ const [v, setV] = useState(initial);
9
+
10
+ return [
11
+ clamp(v, min, max),
12
+ (valueOrFunction: number | ((v: number) => number)) => {
13
+ if (typeof valueOrFunction === 'function') {
14
+ setV((value: number) => {
15
+ const currentValue = clamp(value, min, max);
16
+
17
+ return clamp(valueOrFunction(currentValue), min, max);
18
+ });
19
+ } else {
20
+ setV(clamp(valueOrFunction, min, max));
21
+ }
22
+ },
23
+ ] as const;
24
+ };
@@ -0,0 +1,58 @@
1
+ import { useState } from 'react';
2
+ import { getEmailPathFromSlug } from '../actions/get-email-path-from-slug';
3
+ import {
4
+ type EmailRenderingResult,
5
+ renderEmailByPath,
6
+ } from '../actions/render-email-by-path';
7
+ import { isBuilding } from '../app/env';
8
+ import { useEmails } from '../contexts/emails';
9
+ import { containsEmailTemplate } from '../utils/contains-email-template';
10
+ import { useHotreload } from './use-hot-reload';
11
+
12
+ export const useEmailRenderingResult = (
13
+ emailPath: string,
14
+ serverEmailRenderedResult: EmailRenderingResult,
15
+ ) => {
16
+ const [renderingResult, setRenderingResult] = useState(
17
+ serverEmailRenderedResult,
18
+ );
19
+
20
+ const { emailsDirectoryMetadata } = useEmails();
21
+
22
+ if (!isBuilding) {
23
+ // biome-ignore lint/correctness/useHookAtTopLevel: This is fine since isBuilding does not change at runtime
24
+ useHotreload(async (changes) => {
25
+ for await (const change of changes) {
26
+ const relativePathForChangedFile =
27
+ // ex: apple-receipt.tsx
28
+ // it will be the path relative to the emails directory, so it is already
29
+ // going to be equivalent to the slug
30
+ change.filename;
31
+
32
+ if (
33
+ !containsEmailTemplate(
34
+ relativePathForChangedFile,
35
+ emailsDirectoryMetadata,
36
+ )
37
+ ) {
38
+ continue;
39
+ }
40
+
41
+ const pathForChangedEmail = await getEmailPathFromSlug(
42
+ relativePathForChangedFile,
43
+ );
44
+
45
+ const newRenderingResult = await renderEmailByPath(
46
+ pathForChangedEmail,
47
+ true,
48
+ );
49
+
50
+ if (pathForChangedEmail === emailPath) {
51
+ setRenderingResult(newRenderingResult);
52
+ }
53
+ }
54
+ });
55
+ }
56
+
57
+ return renderingResult;
58
+ };
@@ -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
+ };