react-email 4.0.5 → 4.1.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 (85) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cli/index.js +3 -4
  3. package/dist/cli/index.mjs +3 -4
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +31 -31
  6. package/dist/preview/.next/build-manifest.json +14 -14
  7. package/dist/preview/.next/diagnostics/framework.json +1 -1
  8. package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
  9. package/dist/preview/.next/next-server.js.nft.json +1 -1
  10. package/dist/preview/.next/prerender-manifest.json +3 -3
  11. package/dist/preview/.next/required-server-files.json +1 -2
  12. package/dist/preview/.next/server/app/_not-found/page.js +1 -1
  13. package/dist/preview/.next/server/app/_not-found/page.js.nft.json +1 -1
  14. package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
  16. package/dist/preview/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  17. package/dist/preview/.next/server/app/page.js +1 -1
  18. package/dist/preview/.next/server/app/page.js.nft.json +1 -1
  19. package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
  20. package/dist/preview/.next/server/app/preview/[...slug]/page.js +139 -139
  21. package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  22. package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  23. package/dist/preview/.next/server/chunks/395.js +1 -1
  24. package/dist/preview/.next/server/chunks/574.js +6 -0
  25. package/dist/preview/.next/server/chunks/71.js +1 -0
  26. package/dist/preview/.next/server/chunks/735.js +13 -0
  27. package/dist/preview/.next/server/chunks/{414.js → 840.js} +3 -3
  28. package/dist/preview/.next/server/chunks/886.js +8 -0
  29. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  30. package/dist/preview/.next/server/pages/500.html +1 -1
  31. package/dist/preview/.next/server/pages/_app.js +1 -1
  32. package/dist/preview/.next/server/pages/_app.js.nft.json +1 -1
  33. package/dist/preview/.next/server/pages/_document.js +1 -1
  34. package/dist/preview/.next/server/pages/_document.js.nft.json +1 -1
  35. package/dist/preview/.next/server/pages/_error.js +1 -1
  36. package/dist/preview/.next/server/pages/_error.js.nft.json +1 -1
  37. package/dist/preview/.next/server/pages-manifest.json +1 -1
  38. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  39. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  40. package/dist/preview/.next/static/{jfvVXR08owHTqinvfzxg5 → B5zsyvs_AWXezvTSi2Eyl}/_buildManifest.js +1 -1
  41. package/dist/preview/.next/static/chunks/246-e7336e2929971f63.js +1 -0
  42. package/dist/preview/.next/static/chunks/270-688096d43c717256.js +1 -0
  43. package/dist/preview/.next/static/chunks/539-6e9405ecdc007bb7.js +1 -0
  44. package/dist/preview/.next/static/chunks/587-e77ff7eb4b703903.js +1 -0
  45. package/dist/preview/.next/static/chunks/782947c8-c6cfd05e68542601.js +1 -0
  46. package/dist/preview/.next/static/chunks/803-db74f262c4755323.js +1 -0
  47. package/dist/preview/.next/static/chunks/853-a01d49f63a859f3d.js +1 -0
  48. package/dist/preview/.next/static/chunks/{afa401a5-68e1b62b50f76414.js → afa401a5-55858bf5265319eb.js} +1 -1
  49. package/dist/preview/.next/static/chunks/app/_not-found/page-85e83b2d4bd569a2.js +1 -0
  50. package/dist/preview/.next/static/chunks/app/layout-0f9706a93f161c1e.js +1 -0
  51. package/dist/preview/.next/static/chunks/app/page-0ee3a37f3a3f6f17.js +1 -0
  52. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-5fba95d740305052.js +1 -0
  53. package/dist/preview/.next/static/chunks/{framework-be649707ef7aa6cf.js → framework-b5f555f62da46ed9.js} +1 -1
  54. package/dist/preview/.next/static/chunks/main-a839e2fbd78a08fa.js +1 -0
  55. package/dist/preview/.next/static/chunks/main-app-a6a05ec7ce09e366.js +1 -0
  56. package/dist/preview/.next/static/chunks/pages/_app-ee26b5d329c4bb79.js +1 -0
  57. package/dist/preview/.next/static/chunks/pages/_error-90cb3f6367e28bcd.js +1 -0
  58. package/dist/preview/.next/static/chunks/{webpack-a91b84001c1374de.js → webpack-6755bde10ea8daa8.js} +1 -1
  59. package/dist/preview/.next/trace +27 -27
  60. package/package.json +4 -4
  61. package/src/app/preview/[...slug]/preview.tsx +19 -1
  62. package/src/components/icons/icon-moon.tsx +16 -0
  63. package/src/components/icons/icon-sun.tsx +16 -0
  64. package/src/components/topbar/theme-toggle-group.tsx +87 -0
  65. package/src/hooks/use-iframe-color-scheme.ts +35 -0
  66. package/dist/preview/.next/server/chunks/522.js +0 -8
  67. package/dist/preview/.next/server/chunks/616.js +0 -6
  68. package/dist/preview/.next/server/chunks/747.js +0 -13
  69. package/dist/preview/.next/server/chunks/988.js +0 -1
  70. package/dist/preview/.next/static/chunks/186-be509f142abcb591.js +0 -1
  71. package/dist/preview/.next/static/chunks/221-dc9af10e9928fdb1.js +0 -1
  72. package/dist/preview/.next/static/chunks/323-4c9f66d5bbb5efc5.js +0 -1
  73. package/dist/preview/.next/static/chunks/508-30b1f653385f71d5.js +0 -1
  74. package/dist/preview/.next/static/chunks/529-776647f7f3b9ca0a.js +0 -1
  75. package/dist/preview/.next/static/chunks/814-343c45533ab34e2a.js +0 -1
  76. package/dist/preview/.next/static/chunks/9e3860c5-aa584d592f360aac.js +0 -1
  77. package/dist/preview/.next/static/chunks/app/_not-found/page-3e1a74ee77aa34e0.js +0 -1
  78. package/dist/preview/.next/static/chunks/app/layout-a67e0fd96dad04d7.js +0 -1
  79. package/dist/preview/.next/static/chunks/app/page-80d082b8323795e9.js +0 -1
  80. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-6477d0878c7732eb.js +0 -1
  81. package/dist/preview/.next/static/chunks/main-1bbd9a2a9e1c1cc0.js +0 -1
  82. package/dist/preview/.next/static/chunks/main-app-670fa872bbeba501.js +0 -1
  83. package/dist/preview/.next/static/chunks/pages/_app-300091909fcee354.js +0 -1
  84. package/dist/preview/.next/static/chunks/pages/_error-f76efb2b82ccd7e7.js +0 -1
  85. /package/dist/preview/.next/static/{jfvVXR08owHTqinvfzxg5 → B5zsyvs_AWXezvTSi2Eyl}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "4.0.5",
3
+ "version": "4.1.0-canary.0",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/cli/index.js"
@@ -29,7 +29,7 @@
29
29
  "glob": "10.3.4",
30
30
  "log-symbols": "4.1.0",
31
31
  "mime-types": "2.1.35",
32
- "next": "15.2.3",
32
+ "next": "15.2.4",
33
33
  "normalize-path": "3.0.0",
34
34
  "ora": "5.4.1",
35
35
  "socket.io": "4.8.1"
@@ -82,10 +82,10 @@
82
82
  "typescript": "5.8.2",
83
83
  "use-debounce": "10.0.4",
84
84
  "zod": "3.24.2",
85
- "@react-email/components": "0.0.36"
85
+ "@react-email/components": "0.0.37-canary.0"
86
86
  },
87
87
  "scripts": {
88
- "build": "tsup-node && node ./scripts/build-preview-server.mjs",
88
+ "build": "tsup-node && node ./scripts/build-preview-server.mjs && pnpm install --frozen-lockfile",
89
89
  "caniemail:fetch": "node ./scripts/fill-caniemail-data.mjs",
90
90
  "clean": "rm -rf dist",
91
91
  "dev": "tsup-node --watch",
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { usePathname, useRouter, useSearchParams } from 'next/navigation';
4
- import { use, useState } from 'react';
4
+ import { use, useRef, useState } from 'react';
5
5
  import { flushSync } from 'react-dom';
6
6
  import { Toaster } from 'sonner';
7
7
  import { useDebouncedCallback } from 'use-debounce';
@@ -15,9 +15,11 @@ import { Send } from '../../../components/send';
15
15
  import { useToolbarState } from '../../../components/toolbar';
16
16
  import { Tooltip } from '../../../components/tooltip';
17
17
  import { ActiveViewToggleGroup } from '../../../components/topbar/active-view-toggle-group';
18
+ import { ThemeToggleGroup } from '../../../components/topbar/theme-toggle-group';
18
19
  import { ViewSizeControls } from '../../../components/topbar/view-size-controls';
19
20
  import { PreviewContext } from '../../../contexts/preview';
20
21
  import { useClampedState } from '../../../hooks/use-clamped-state';
22
+ import { useIframeColorScheme } from '../../../hooks/use-iframe-color-scheme';
21
23
  import { cn } from '../../../utils';
22
24
  import { RenderingError } from './rendering-error';
23
25
 
@@ -32,9 +34,17 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
32
34
  const pathname = usePathname();
33
35
  const searchParams = useSearchParams();
34
36
 
37
+ const activeTheme: 'dark' | 'light' =
38
+ searchParams.get('theme') === 'dark' ? 'dark' : 'light';
35
39
  const activeView = searchParams.get('view') ?? 'preview';
36
40
  const activeLang = searchParams.get('lang') ?? 'jsx';
37
41
 
42
+ const handleThemeChange = (theme: 'dark' | 'light') => {
43
+ const params = new URLSearchParams(searchParams);
44
+ params.set('theme', theme);
45
+ router.push(`${pathname}?${params.toString()}`);
46
+ };
47
+
38
48
  const handleViewChange = (view: string) => {
39
49
  const params = new URLSearchParams(searchParams);
40
50
  params.set('view', view);
@@ -51,6 +61,9 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
51
61
  );
52
62
  };
53
63
 
64
+ const iframeRef = useRef<HTMLIFrameElement>(null);
65
+ useIframeColorScheme(iframeRef, activeTheme);
66
+
54
67
  const hasRenderingMetadata = typeof renderedEmailMetadata !== 'undefined';
55
68
  const hasErrors = 'error' in renderingResult;
56
69
 
@@ -99,6 +112,10 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
99
112
  viewHeight={height}
100
113
  viewWidth={width}
101
114
  />
115
+ <ThemeToggleGroup
116
+ active={activeTheme}
117
+ onChange={(theme) => handleThemeChange(theme)}
118
+ />
102
119
  <ActiveViewToggleGroup
103
120
  activeView={activeView}
104
121
  setActiveView={handleViewChange}
@@ -164,6 +181,7 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
164
181
  <iframe
165
182
  className="solid max-h-full rounded-lg bg-white"
166
183
  ref={(iframe) => {
184
+ iframeRef.current = iframe;
167
185
  if (iframe) {
168
186
  return makeIframeDocumentBubbleEvents(iframe);
169
187
  }
@@ -0,0 +1,16 @@
1
+ import * as React from 'react';
2
+ import type { IconElement, IconProps } from './icon-base';
3
+ import { IconBase } from './icon-base';
4
+
5
+ export const IconMoon = React.forwardRef<IconElement, Readonly<IconProps>>(
6
+ ({ ...props }, forwardedRef) => (
7
+ <IconBase ref={forwardedRef} {...props}>
8
+ <path
9
+ fill="currentColor"
10
+ d="m17.75 4.09l-2.53 1.94l.91 3.06l-2.63-1.81l-2.63 1.81l.91-3.06l-2.53-1.94L12.44 4l1.06-3l1.06 3zm3.5 6.91l-1.64 1.25l.59 1.98l-1.7-1.17l-1.7 1.17l.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85c-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14c.4-.4.82-.76 1.27-1.08c.75-.53 1.93.36 1.85 1.19c-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82c-2.81 3.14-2.7 7.96.31 10.98c3.02 3.01 7.84 3.12 10.98.31"
11
+ />
12
+ </IconBase>
13
+ ),
14
+ );
15
+
16
+ IconMoon.displayName = 'IconMoon';
@@ -0,0 +1,16 @@
1
+ import * as React from 'react';
2
+ import type { IconElement, IconProps } from './icon-base';
3
+ import { IconBase } from './icon-base';
4
+
5
+ export const IconSun = React.forwardRef<IconElement, Readonly<IconProps>>(
6
+ ({ ...props }, forwardedRef) => (
7
+ <IconBase ref={forwardedRef} {...props}>
8
+ <path
9
+ fill="currentColor"
10
+ d="m3.55 19.09l1.41 1.41l1.8-1.79l-1.42-1.42M12 6c-3.31 0-6 2.69-6 6s2.69 6 6 6s6-2.69 6-6c0-3.32-2.69-6-6-6m8 7h3v-2h-3m-2.76 7.71l1.8 1.79l1.41-1.41l-1.79-1.8M20.45 5l-1.41-1.4l-1.8 1.79l1.42 1.42M13 1h-2v3h2M6.76 5.39L4.96 3.6L3.55 5l1.79 1.81zM1 13h3v-2H1m12 9h-2v3h2"
11
+ />
12
+ </IconBase>
13
+ ),
14
+ );
15
+
16
+ IconSun.displayName = 'IconSun';
@@ -0,0 +1,87 @@
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 { IconMoon } from '../icons/icon-moon';
6
+ import { IconSun } from '../icons/icon-sun';
7
+ import { Tooltip } from '../tooltip';
8
+
9
+ interface ThemeToggleGroupProps {
10
+ active: 'light' | 'dark';
11
+ onChange: (theme: 'light' | 'dark') => unknown;
12
+ }
13
+
14
+ export const ThemeToggleGroup = ({
15
+ active,
16
+ onChange,
17
+ }: ThemeToggleGroupProps) => {
18
+ return (
19
+ <ToggleGroup.Root
20
+ aria-label="Color Scheme"
21
+ className="inline-block items-center bg-slate-2 border border-slate-6 rounded-md overflow-hidden h-[36px]"
22
+ id="theme-toggle"
23
+ onValueChange={(value) => {
24
+ if (value) onChange(value as 'light' | 'dark');
25
+ }}
26
+ type="single"
27
+ value={active}
28
+ >
29
+ <ToggleGroup.Item value="light">
30
+ <Tooltip>
31
+ <Tooltip.Trigger asChild>
32
+ <div
33
+ className={cn(
34
+ 'relative px-3 py-2 transition duration-200 ease-in-out hover:text-slate-12',
35
+ {
36
+ 'text-slate-11': active !== 'light',
37
+ 'text-slate-12': active === 'light',
38
+ },
39
+ )}
40
+ >
41
+ {active === 'light' && (
42
+ <motion.span
43
+ animate={{ opacity: 1 }}
44
+ className="absolute top-0 right-0 bottom-0 left-0 bg-slate-4"
45
+ exit={{ opacity: 0 }}
46
+ initial={{ opacity: 0 }}
47
+ layoutId="topbar-theme-tabs"
48
+ transition={tabTransition}
49
+ />
50
+ )}
51
+ <IconSun />
52
+ </div>
53
+ </Tooltip.Trigger>
54
+ <Tooltip.Content>Light</Tooltip.Content>
55
+ </Tooltip>
56
+ </ToggleGroup.Item>
57
+ <ToggleGroup.Item value="dark">
58
+ <Tooltip>
59
+ <Tooltip.Trigger asChild>
60
+ <div
61
+ className={cn(
62
+ 'relative px-3 py-2 transition duration-200 ease-in-out hover:text-slate-12',
63
+ {
64
+ 'text-slate-11': active !== 'dark',
65
+ 'text-slate-12': active === 'dark',
66
+ },
67
+ )}
68
+ >
69
+ {active === 'dark' && (
70
+ <motion.span
71
+ animate={{ opacity: 1 }}
72
+ className="absolute top-0 right-0 bottom-0 left-0 bg-slate-4"
73
+ exit={{ opacity: 0 }}
74
+ initial={{ opacity: 0 }}
75
+ layoutId="topbar-theme-tabs"
76
+ transition={tabTransition}
77
+ />
78
+ )}
79
+ <IconMoon />
80
+ </div>
81
+ </Tooltip.Trigger>
82
+ <Tooltip.Content>Dark</Tooltip.Content>
83
+ </Tooltip>
84
+ </ToggleGroup.Item>
85
+ </ToggleGroup.Root>
86
+ );
87
+ };
@@ -0,0 +1,35 @@
1
+ import * as React from 'react';
2
+
3
+ export function useIframeColorScheme(
4
+ iframeRef: React.RefObject<HTMLIFrameElement | null>,
5
+ theme: string,
6
+ ) {
7
+ React.useEffect(() => {
8
+ const iframe = iframeRef.current;
9
+
10
+ if (!iframe) return;
11
+
12
+ // Set on iframe element itself
13
+ iframe.style.colorScheme = theme;
14
+
15
+ // Set on iframe's document if available
16
+ if (iframe.contentDocument) {
17
+ iframe.contentDocument.documentElement.style.colorScheme = theme;
18
+ iframe.contentDocument.body.style.colorScheme = theme;
19
+ }
20
+
21
+ // Ensure styles are applied after it loads
22
+ const handleLoad = () => {
23
+ if (iframe.contentDocument) {
24
+ iframe.contentDocument.documentElement.style.colorScheme = theme;
25
+ iframe.contentDocument.body.style.colorScheme = theme;
26
+ }
27
+ };
28
+
29
+ iframe.addEventListener('load', handleLoad);
30
+
31
+ return () => {
32
+ iframe.removeEventListener('load', handleLoad);
33
+ };
34
+ }, [theme, iframeRef]);
35
+ }