@react-email/preview-server 5.0.0-canary.1 → 5.0.0-canary.3

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 (40) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +6 -6
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/server/app/_not-found/page.js +1 -1
  6. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  7. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/page.js +1 -1
  9. package/.next/server/app/page.js.nft.json +1 -1
  10. package/.next/server/app/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/preview/[...slug]/page.js +25 -25
  12. package/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  13. package/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  14. package/.next/server/chunks/235.js +1 -1
  15. package/.next/server/chunks/74.js +1 -0
  16. package/.next/server/pages/500.html +1 -1
  17. package/.next/server/server-reference-manifest.js +1 -1
  18. package/.next/server/server-reference-manifest.json +1 -1
  19. package/.next/static/chunks/611-3eea98bdc9dc1702.js +1 -0
  20. package/.next/static/chunks/{615-e223b553c72ae727.js → 615-72dd0f206c15c68c.js} +1 -1
  21. package/.next/static/chunks/app/layout-e10b48dd22650439.js +1 -0
  22. package/.next/static/chunks/app/preview/[...slug]/page-30fcc9fc52a5a560.js +1 -0
  23. package/.next/static/css/{7d8cf00703036864.css → 1d22a5c848552bfc.css} +1 -1
  24. package/.next/trace +28 -28
  25. package/CHANGELOG.md +12 -0
  26. package/package.json +4 -2
  27. package/src/actions/render-email-by-path.tsx +1 -1
  28. package/src/app/preview/[...slug]/email-frame.tsx +306 -0
  29. package/src/app/preview/[...slug]/preview.tsx +48 -30
  30. package/src/components/icons/icon-base.tsx +2 -2
  31. package/src/components/icons/icon-moon.tsx +13 -0
  32. package/src/components/icons/icon-sun.tsx +13 -0
  33. package/src/components/topbar/emulated-dark-mode-toggle.tsx +36 -0
  34. package/src/utils/style-text.ts +11 -0
  35. package/.next/server/chunks/571.js +0 -1
  36. package/.next/static/chunks/442-9645091f2b304619.js +0 -1
  37. package/.next/static/chunks/app/layout-596f02410fa6cec2.js +0 -1
  38. package/.next/static/chunks/app/preview/[...slug]/page-337a48d611839d3a.js +0 -1
  39. /package/.next/static/{04HKn1-CcCqsWXfS4b-9r → ixMN5IIqPIc0E9e26HFvF}/_buildManifest.js +0 -0
  40. /package/.next/static/{04HKn1-CcCqsWXfS4b-9r → ixMN5IIqPIc0E9e26HFvF}/_ssgManifest.js +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @react-email/preview-server
2
2
 
3
+ ## 5.0.0-canary.3
4
+
5
+ ### Minor Changes
6
+
7
+ - 95c7417: Dark mode switcher emulating email client color inversion
8
+
9
+ ## 5.0.0-canary.2
10
+
11
+ ### Patch Changes
12
+
13
+ - 1b3176e: fallback to not text coloring for Node.js < 20
14
+
3
15
  ## 5.0.0-canary.1
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-email/preview-server",
3
- "version": "5.0.0-canary.1",
3
+ "version": "5.0.0-canary.3",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "main": "./index.mjs",
6
6
  "dependencies": {
@@ -14,6 +14,7 @@
14
14
  "@radix-ui/react-popover": "1.1.15",
15
15
  "@radix-ui/react-slot": "1.2.3",
16
16
  "@radix-ui/react-tabs": "1.1.13",
17
+ "@radix-ui/react-toggle": "1.1.10",
17
18
  "@radix-ui/react-toggle-group": "1.1.11",
18
19
  "@radix-ui/react-tooltip": "1.2.8",
19
20
  "@react-email/tailwind": "2.0.0-canary.1",
@@ -24,6 +25,7 @@
24
25
  "@types/webpack": "5.28.5",
25
26
  "autoprefixer": "10.4.21",
26
27
  "clsx": "2.1.1",
28
+ "colorjs.io": "0.5.2",
27
29
  "esbuild": "0.25.10",
28
30
  "framer-motion": "12.23.22",
29
31
  "json5": "2.2.3",
@@ -61,7 +63,7 @@
61
63
  "autoprefixer": "10.4.21",
62
64
  "tailwindcss": "3.4.0",
63
65
  "typescript": "5.8.3",
64
- "@react-email/components": "1.0.0-canary.1"
66
+ "@react-email/components": "1.0.0-canary.2"
65
67
  },
66
68
  "license": "MIT",
67
69
  "repository": {
@@ -2,7 +2,6 @@
2
2
 
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { styleText } from 'node:util';
6
5
  import logSymbols from 'log-symbols';
7
6
  import ora, { type Ora } from 'ora';
8
7
  import {
@@ -15,6 +14,7 @@ import { convertStackWithSourceMap } from '../utils/convert-stack-with-sourcemap
15
14
  import { createJsxRuntime } from '../utils/create-jsx-runtime';
16
15
  import { getEmailComponent } from '../utils/get-email-component';
17
16
  import { registerSpinnerAutostopping } from '../utils/register-spinner-autostopping';
17
+ import { styleText } from '../utils/style-text';
18
18
  import type { ErrorObject } from '../utils/types/error-object';
19
19
 
20
20
  export interface RenderedEmailMetadata {
@@ -0,0 +1,306 @@
1
+ import { Slot } from '@radix-ui/react-slot';
2
+ import Color from 'colorjs.io';
3
+ import type { ComponentProps } from 'react';
4
+
5
+ function* walkDom(element: Element): Generator<Element> {
6
+ if (element.children.length > 0) {
7
+ for (let i = 0; i < element.children.length; i++) {
8
+ const child = element.children.item(i)!;
9
+ yield child;
10
+ yield* walkDom(child);
11
+ }
12
+ }
13
+ }
14
+
15
+ function invertColor(colorString: string, mode: 'foreground' | 'background') {
16
+ try {
17
+ const color = new Color(colorString).to('lch');
18
+
19
+ if (mode === 'background') {
20
+ // Keeps the same lightness if it's already dark. If it's bright inverts the lightness
21
+ // - This is a characteristic from Outlook iOS
22
+ // - Parcel does something very similar
23
+ //
24
+ // The 0.75 factor ensures that, even if the lightness is 100%, the final inverted is going to be 25%
25
+ // - This is a characteristic from Apple Mail
26
+ //
27
+ // The two extra 50 terms are so that the lightness inversion doesn't become a step function
28
+ if (color.lch.l! >= 50) {
29
+ color.lch.l = 50 - (color.lch.l! - 50) * 0.75;
30
+ }
31
+ } else if (mode === 'foreground') {
32
+ // The same as what's done for background, but inverts the check for brightness.
33
+ // If the color is already bright, then it keeps the same. If the color is dark, then it inverts the brightness
34
+ if (color.lch.l! < 50) {
35
+ color.lch.l = 50 - (color.lch.l! - 50) * 0.75;
36
+ }
37
+ }
38
+
39
+ // While not exactly, I've found that email clients generally tend to reduce the chrome by 20%.
40
+ // Apple Mail specifically reduces by exactly 20%, so we're closer to Apple Mail in this sense as well.
41
+ color.lch.c! *= 0.8;
42
+
43
+ return color.toString();
44
+ } catch (exception) {
45
+ console.error(`couldn't invert color ${colorString}`, exception);
46
+ return colorString;
47
+ }
48
+ }
49
+
50
+ const colorRegex = () =>
51
+ /#[0-9a-f]{3,4}|#[0-9a-f]{6,8}|(rgb|rgba|hsl|hsv|oklab|oklch|lab|lch|hwb)\s*\(.*?\)/gi;
52
+ const namedColors = {
53
+ aliceblue: '#f0f8ff',
54
+ antiquewhite: '#faebd7',
55
+ aqua: '#00ffff',
56
+ aquamarine: '#7fffd4',
57
+ azure: '#f0ffff',
58
+ beige: '#f5f5dc',
59
+ bisque: '#ffe4c4',
60
+ black: '#000000',
61
+ blanchedalmond: '#ffebcd',
62
+ blue: '#0000ff',
63
+ blueviolet: '#8a2be2',
64
+ brown: '#a52a2a',
65
+ burlywood: '#deb887',
66
+ cadetblue: '#5f9ea0',
67
+ chartreuse: '#7fff00',
68
+ chocolate: '#d2691e',
69
+ coral: '#ff7f50',
70
+ cornflowerblue: '#6495ed',
71
+ cornsilk: '#fff8dc',
72
+ crimson: '#dc143c',
73
+ cyan: '#00ffff',
74
+ darkblue: '#00008b',
75
+ darkcyan: '#008b8b',
76
+ darkgoldenrod: '#b8860b',
77
+ darkgray: '#a9a9a9',
78
+ darkgreen: '#006400',
79
+ darkgrey: '#a9a9a9',
80
+ darkkhaki: '#bdb76b',
81
+ darkmagenta: '#8b008b',
82
+ darkolivegreen: '#556b2f',
83
+ darkorange: '#ff8c00',
84
+ darkorchid: '#9932cc',
85
+ darkred: '#8b0000',
86
+ darksalmon: '#e9967a',
87
+ darkseagreen: '#8fbc8f',
88
+ darkslateblue: '#483d8b',
89
+ darkslategray: '#2f4f4f',
90
+ darkslategrey: '#2f4f4f',
91
+ darkturquoise: '#00ced1',
92
+ darkviolet: '#9400d3',
93
+ deeppink: '#ff1493',
94
+ deepskyblue: '#00bfff',
95
+ dimgray: '#696969',
96
+ dimgrey: '#696969',
97
+ dodgerblue: '#1e90ff',
98
+ firebrick: '#b22222',
99
+ floralwhite: '#fffaf0',
100
+ forestgreen: '#228b22',
101
+ fuchsia: '#ff00ff',
102
+ gainsboro: '#dcdcdc',
103
+ ghostwhite: '#f8f8ff',
104
+ gold: '#ffd700',
105
+ goldenrod: '#daa520',
106
+ gray: '#808080',
107
+ green: '#008000',
108
+ greenyellow: '#adff2f',
109
+ grey: '#808080',
110
+ honeydew: '#f0fff0',
111
+ hotpink: '#ff69b4',
112
+ indianred: '#cd5c5c',
113
+ indigo: '#4b0082',
114
+ ivory: '#fffff0',
115
+ khaki: '#f0e68c',
116
+ lavender: '#e6e6fa',
117
+ lavenderblush: '#fff0f5',
118
+ lawngreen: '#7cfc00',
119
+ lemonchiffon: '#fffacd',
120
+ lightblue: '#add8e6',
121
+ lightcoral: '#f08080',
122
+ lightcyan: '#e0ffff',
123
+ lightgoldenrodyellow: '#fafad2',
124
+ lightgray: '#d3d3d3',
125
+ lightgreen: '#90ee90',
126
+ lightgrey: '#d3d3d3',
127
+ lightpink: '#ffb6c1',
128
+ lightsalmon: '#ffa07a',
129
+ lightseagreen: '#20b2aa',
130
+ lightskyblue: '#87cefa',
131
+ lightslategray: '#778899',
132
+ lightslategrey: '#778899',
133
+ lightsteelblue: '#b0c4de',
134
+ lightyellow: '#ffffe0',
135
+ lime: '#00ff00',
136
+ limegreen: '#32cd32',
137
+ linen: '#faf0e6',
138
+ magenta: '#ff00ff',
139
+ maroon: '#800000',
140
+ mediumaquamarine: '#66cdaa',
141
+ mediumblue: '#0000cd',
142
+ mediumorchid: '#ba55d3',
143
+ mediumpurple: '#9370db',
144
+ mediumseagreen: '#3cb371',
145
+ mediumslateblue: '#7b68ee',
146
+ mediumspringgreen: '#00fa9a',
147
+ mediumturquoise: '#48d1cc',
148
+ mediumvioletred: '#c71585',
149
+ midnightblue: '#191970',
150
+ mintcream: '#f5fffa',
151
+ mistyrose: '#ffe4e1',
152
+ moccasin: '#ffe4b5',
153
+ navajowhite: '#ffdead',
154
+ navy: '#000080',
155
+ oldlace: '#fdf5e6',
156
+ olive: '#808000',
157
+ olivedrab: '#6b8e23',
158
+ orange: '#ffa500',
159
+ orangered: '#ff4500',
160
+ orchid: '#da70d6',
161
+ palegoldenrod: '#eee8aa',
162
+ palegreen: '#98fb98',
163
+ paleturquoise: '#afeeee',
164
+ palevioletred: '#db7093',
165
+ papayawhip: '#ffefd5',
166
+ peachpuff: '#ffdab9',
167
+ peru: '#cd853f',
168
+ pink: '#ffc0cb',
169
+ plum: '#dda0dd',
170
+ powderblue: '#b0e0e6',
171
+ purple: '#800080',
172
+ rebeccapurple: '#663399',
173
+ red: '#ff0000',
174
+ rosybrown: '#bc8f8f',
175
+ royalblue: '#4169e1',
176
+ saddlebrown: '#8b4513',
177
+ salmon: '#fa8072',
178
+ sandybrown: '#f4a460',
179
+ seagreen: '#2e8b57',
180
+ seashell: '#fff5ee',
181
+ sienna: '#a0522d',
182
+ silver: '#c0c0c0',
183
+ skyblue: '#87ceeb',
184
+ slateblue: '#6a5acd',
185
+ slategray: '#708090',
186
+ slategrey: '#708090',
187
+ snow: '#fffafa',
188
+ springgreen: '#00ff7f',
189
+ steelblue: '#4682b4',
190
+ tan: '#d2b48c',
191
+ teal: '#008080',
192
+ thistle: '#d8bfd8',
193
+ tomato: '#ff6347',
194
+ transparent: 'rgba(0,0,0,0)',
195
+ turquoise: '#40e0d0',
196
+ violet: '#ee82ee',
197
+ wheat: '#f5deb3',
198
+ white: '#ffffff',
199
+ whitesmoke: '#f5f5f5',
200
+ yellow: '#ffff00',
201
+ yellowgreen: '#9acd32',
202
+ };
203
+ const namedColorRegex = new RegExp(
204
+ `${Object.keys(namedColors).join('|')}`,
205
+ 'gi',
206
+ );
207
+
208
+ function applyColorInversion(iframe: HTMLIFrameElement) {
209
+ const { contentDocument, contentWindow } = iframe;
210
+ if (!contentDocument || !contentWindow) return;
211
+
212
+ if (!contentDocument.body.style.color) {
213
+ contentDocument.body.style.color = 'rgb(0, 0, 0)';
214
+ }
215
+
216
+ for (const element of walkDom(contentDocument.documentElement)) {
217
+ if (
218
+ element instanceof
219
+ (contentWindow as unknown as typeof globalThis).HTMLElement
220
+ ) {
221
+ if (element.style.color) {
222
+ element.style.color = element.style.color
223
+ .replaceAll(colorRegex(), (color) => invertColor(color, 'foreground'))
224
+ .replaceAll(namedColorRegex, (namedColor) =>
225
+ invertColor(namedColors[namedColor], 'foreground'),
226
+ );
227
+ namedColorRegex.lastIndex = 0;
228
+ }
229
+ if (element.style.background) {
230
+ element.style.background = element.style.background
231
+ .replaceAll(colorRegex(), (color) => invertColor(color, 'background'))
232
+ .replaceAll(namedColorRegex, (namedColor) =>
233
+ invertColor(namedColors[namedColor], 'foreground'),
234
+ );
235
+ namedColorRegex.lastIndex = 0;
236
+ }
237
+ if (element.style.backgroundColor) {
238
+ element.style.backgroundColor = element.style.backgroundColor
239
+ .replaceAll(colorRegex(), (color) => invertColor(color, 'background'))
240
+ .replaceAll(namedColorRegex, (namedColor) =>
241
+ invertColor(namedColors[namedColor], 'foreground'),
242
+ );
243
+ namedColorRegex.lastIndex = 0;
244
+ }
245
+ if (element.style.borderColor) {
246
+ element.style.borderColor = element.style.borderColor
247
+ .replaceAll(colorRegex(), (color) => invertColor(color, 'background'))
248
+ .replaceAll(namedColorRegex, (namedColor) =>
249
+ invertColor(namedColors[namedColor], 'foreground'),
250
+ );
251
+ namedColorRegex.lastIndex = 0;
252
+ }
253
+ if (element.style.border) {
254
+ element.style.border = element.style.border
255
+ .replaceAll(colorRegex(), (color) => invertColor(color, 'background'))
256
+ .replaceAll(namedColorRegex, (namedColor) =>
257
+ invertColor(namedColors[namedColor], 'foreground'),
258
+ );
259
+ namedColorRegex.lastIndex = 0;
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ interface EmailFrameProps extends ComponentProps<'iframe'> {
266
+ markup: string;
267
+ width: number;
268
+ height: number;
269
+ darkMode: boolean;
270
+ }
271
+
272
+ export function EmailFrame({
273
+ markup,
274
+ width,
275
+ height,
276
+ darkMode,
277
+ ...rest
278
+ }: EmailFrameProps) {
279
+ return (
280
+ <Slot
281
+ ref={(iframe: HTMLIFrameElement) => {
282
+ if (!iframe) return;
283
+
284
+ if (darkMode) {
285
+ applyColorInversion(iframe);
286
+ }
287
+ }}
288
+ >
289
+ <iframe
290
+ srcDoc={markup}
291
+ width={width}
292
+ height={height}
293
+ onLoad={(event) => {
294
+ if (darkMode) {
295
+ const iframe = event.currentTarget;
296
+ applyColorInversion(iframe);
297
+ }
298
+ }}
299
+ {...rest}
300
+ // This key makes sure that the iframe itself remounts to the DOM when theme changes, so
301
+ // that the color changes in dark mode can be easily undone when switching to light mode.
302
+ key={darkMode ? 'iframe-inverted-colors' : 'iframe-normal-colors'}
303
+ />
304
+ </Slot>
305
+ );
306
+ }
@@ -15,10 +15,12 @@ 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 { EmulatedDarkModeToggle } from '../../../components/topbar/emulated-dark-mode-toggle';
18
19
  import { ViewSizeControls } from '../../../components/topbar/view-size-controls';
19
20
  import { usePreviewContext } from '../../../contexts/preview';
20
21
  import { useClampedState } from '../../../hooks/use-clamped-state';
21
22
  import { cn } from '../../../utils';
23
+ import { EmailFrame } from './email-frame';
22
24
  import { ErrorOverlay } from './error-overlay';
23
25
 
24
26
  interface PreviewProps extends React.ComponentProps<'div'> {
@@ -32,9 +34,20 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
32
34
  const pathname = usePathname();
33
35
  const searchParams = useSearchParams();
34
36
 
37
+ const isDarkModeEnabled = searchParams.get('dark') !== null;
35
38
  const activeView = searchParams.get('view') ?? 'preview';
36
39
  const activeLang = searchParams.get('lang') ?? 'jsx';
37
40
 
41
+ const handleDarkModeChange = (enabled: boolean) => {
42
+ const params = new URLSearchParams(searchParams);
43
+ if (enabled) {
44
+ params.set('dark', '');
45
+ } else {
46
+ params.delete('dark');
47
+ }
48
+ router.push(`${pathname}?${params.toString()}${location.hash}`);
49
+ };
50
+
38
51
  const handleViewChange = (view: string) => {
39
52
  const params = new URLSearchParams(searchParams);
40
53
  params.set('view', view);
@@ -83,26 +96,32 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
83
96
  return (
84
97
  <>
85
98
  <Topbar emailTitle={emailTitle}>
86
- {activeView === 'preview' && (
87
- <ViewSizeControls
88
- setViewHeight={(height) => {
89
- setHeight(height);
90
- flushSync(() => {
91
- handleSaveViewSize();
92
- });
93
- }}
94
- setViewWidth={(width) => {
95
- setWidth(width);
96
- flushSync(() => {
97
- handleSaveViewSize();
98
- });
99
- }}
100
- viewHeight={height}
101
- viewWidth={width}
102
- minWidth={minWidth}
103
- minHeight={minHeight}
104
- />
105
- )}
99
+ {activeView === 'preview' ? (
100
+ <>
101
+ <ViewSizeControls
102
+ setViewHeight={(height) => {
103
+ setHeight(height);
104
+ flushSync(() => {
105
+ handleSaveViewSize();
106
+ });
107
+ }}
108
+ setViewWidth={(width) => {
109
+ setWidth(width);
110
+ flushSync(() => {
111
+ handleSaveViewSize();
112
+ });
113
+ }}
114
+ viewHeight={height}
115
+ viewWidth={width}
116
+ minWidth={minWidth}
117
+ minHeight={minHeight}
118
+ />
119
+ <EmulatedDarkModeToggle
120
+ enabled={isDarkModeEnabled}
121
+ onChange={(enabled) => handleDarkModeChange(enabled)}
122
+ />
123
+ </>
124
+ ) : null}
106
125
  <ActiveViewToggleGroup
107
126
  activeView={activeView}
108
127
  setActiveView={handleViewChange}
@@ -165,19 +184,18 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
165
184
  }}
166
185
  width={width}
167
186
  >
168
- <iframe
187
+ <EmailFrame
169
188
  className="max-h-full rounded-lg bg-white [color-scheme:auto]"
189
+ darkMode={isDarkModeEnabled}
190
+ markup={renderedEmailMetadata.markup}
191
+ width={width}
192
+ height={height}
193
+ title={emailTitle}
170
194
  ref={(iframe) => {
171
- if (iframe) {
172
- return makeIframeDocumentBubbleEvents(iframe);
173
- }
174
- }}
175
- srcDoc={renderedEmailMetadata.markup}
176
- style={{
177
- width: `${width}px`,
178
- height: `${height}px`,
195
+ if (!iframe) return;
196
+
197
+ return makeIframeDocumentBubbleEvents(iframe);
179
198
  }}
180
- title={emailTitle}
181
199
  />
182
200
  </ResizableWrapper>
183
201
  )}
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
 
3
- export type IconElement = React.ElementRef<'svg'>;
4
- export type RootProps = React.ComponentPropsWithoutRef<'svg'>;
3
+ export type IconElement = React.ComponentRef<'svg'>;
4
+ export type RootProps = React.ComponentProps<'svg'>;
5
5
 
6
6
  export interface IconProps extends RootProps {
7
7
  size?: number;
@@ -0,0 +1,13 @@
1
+ import type { IconProps } from './icon-base';
2
+ import { IconBase } from './icon-base';
3
+
4
+ export const IconMoon = ({ ...props }: IconProps) => (
5
+ <IconBase {...props}>
6
+ <path
7
+ fill="currentColor"
8
+ 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"
9
+ />
10
+ </IconBase>
11
+ );
12
+
13
+ IconMoon.displayName = 'IconMoon';
@@ -0,0 +1,13 @@
1
+ import type { IconProps } from './icon-base';
2
+ import { IconBase } from './icon-base';
3
+
4
+ export const IconSun = ({ ...props }: IconProps) => (
5
+ <IconBase {...props}>
6
+ <path
7
+ fill="currentColor"
8
+ 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"
9
+ />
10
+ </IconBase>
11
+ );
12
+
13
+ IconSun.displayName = 'IconSun';
@@ -0,0 +1,36 @@
1
+ import * as Toggle from '@radix-ui/react-toggle';
2
+ import { cn } from '../../utils';
3
+ import { IconMoon } from '../icons/icon-moon';
4
+ import { Tooltip } from '../tooltip';
5
+
6
+ interface EmulatedDarkModeToggleProps {
7
+ enabled: boolean;
8
+ onChange: (enabled: boolean) => unknown;
9
+ }
10
+
11
+ export const EmulatedDarkModeToggle = ({
12
+ enabled,
13
+ onChange,
14
+ }: EmulatedDarkModeToggleProps) => {
15
+ return (
16
+ <Tooltip>
17
+ <Tooltip.Trigger asChild>
18
+ <Toggle.Root
19
+ value="dark"
20
+ className={cn(
21
+ 'relative w-9 h-9 flex items-center justify-center border border-slate-6 text-sm rounded-lg transition duration-200 ease-in-out',
22
+ 'text-slate-11 hover:text-slate-12 aria-pressed:text-slate-12 aria-pressed:bg-slate-4',
23
+ )}
24
+ pressed={enabled}
25
+ onPressedChange={() => onChange(!enabled)}
26
+ >
27
+ <IconMoon />
28
+ </Toggle.Root>
29
+ </Tooltip.Trigger>
30
+ <Tooltip.Content>
31
+ When enabled, inverts colors in the preview emulating what email clients
32
+ do in dark mode.
33
+ </Tooltip.Content>
34
+ </Tooltip>
35
+ );
36
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Centralized fallback for Node versions (<20.12.0) without util.styleText.
3
+ * Returns the original text when styleText is unavailable.
4
+ */
5
+ import * as nodeUtil from 'node:util';
6
+
7
+ type StyleTextFunction = (style: string, text: string) => string;
8
+
9
+ export const styleText: StyleTextFunction = (nodeUtil as any).styleText
10
+ ? (nodeUtil as any).styleText
11
+ : (_: string, text: string) => text;
@@ -1 +0,0 @@
1
- exports.id=571,exports.ids=[571],exports.modules={8332:(a,b,c)=>{"use strict";c.r(b),c.d(b,{getEmailsDirectoryMetadata:()=>i});var d=c(73024),e=c.n(d),f=c(76760),g=c.n(f);let h=async a=>{let b;try{b=await e().promises.open(a,"r")}catch(a){return console.warn(a),!1}if((await b.stat()).isDirectory())return await b.close(),!1;let{ext:c}=g().parse(a);if(![".js",".tsx",".jsx"].includes(c))return await b.close(),!1;let d=await b.readFile("utf8");await b.close();let f=/\bexport\s+default\b/gm.test(d),h=/\bmodule\.exports\s*=/gm.test(d),i=/\bexport\s+\{[^}]*\bdefault\b[^}]*\}/gm.test(d);return f||h||i},i=async(a,b=!1,c=!1,d=a)=>{if(!e().existsSync(a))return;let f=await e().promises.readdir(a,{withFileTypes:!0}),j=await Promise.all(f.map(b=>h(g().join(a,b.name)))),k=f.filter((a,b)=>j[b]).map(a=>b?a.name:a.name.replace(g().extname(a.name),"")),l=await Promise.all(f.filter(a=>a.isDirectory()&&!a.name.startsWith("_")&&"static"!==a.name).map(c=>i(g().join(a,c.name),b,!0,d))),m={absolutePath:a,relativePath:g().relative(d,a),directoryName:a.split(g().sep).pop(),emailFilenames:k,subDirectories:l};return c?(a=>{let b=a;for(;0===b.emailFilenames.length&&1===b.subDirectories.length;){let a=b.subDirectories[0];b={...a,directoryName:g().join(b.directoryName,a.directoryName)}}return b})(m):m}},18695:(a,b,c)=>{"use strict";c.d(b,{EmailsProvider:()=>l,J:()=>k});var d=c(6362),e=c(2179),f=c(14298);let g=(0,f.createServerReference)("7fa099a1c109e1aac584dd082e37860081313b0947",f.callServer,void 0,f.findSourceMapURL,"getEmailsDirectoryMetadataAction");var h=c(70985),i=c(39520);let j=(0,e.createContext)(void 0),k=()=>{let a=(0,e.useContext)(j);if(void 0===a)throw Error("Cannot call `useEmails` outside of an `EmailsContext` provider.");return a},l=a=>{let[b,c]=(0,e.useState)(a.initialEmailsDirectoryMetadata);return h.Hf||h.m4||(0,i.a)(async()=>{let b=await g(a.initialEmailsDirectoryMetadata.absolutePath);if(b)c(b);else throw Error("Hot reloading: unable to find the emails directory to update the sidebar")}),(0,d.jsx)(j.Provider,{value:{emailsDirectoryMetadata:b},children:a.children})}},25880:(a,b,c)=>{Promise.resolve().then(c.t.bind(c,36100,23)),Promise.resolve().then(c.t.bind(c,99455,23)),Promise.resolve().then(c.t.bind(c,26835,23)),Promise.resolve().then(c.t.bind(c,36726,23)),Promise.resolve().then(c.t.bind(c,33666,23)),Promise.resolve().then(c.t.bind(c,23670,23)),Promise.resolve().then(c.t.bind(c,30006,23)),Promise.resolve().then(c.t.bind(c,41971,23)),Promise.resolve().then(c.t.bind(c,40614,23))},27378:(a,b,c)=>{Promise.resolve().then(c.bind(c,90948))},35191:()=>{},35608:(a,b,c)=>{Promise.resolve().then(c.t.bind(c,5058,23)),Promise.resolve().then(c.t.bind(c,35621,23)),Promise.resolve().then(c.t.bind(c,44325,23)),Promise.resolve().then(c.t.bind(c,20908,23)),Promise.resolve().then(c.t.bind(c,83572,23)),Promise.resolve().then(c.t.bind(c,25928,23)),Promise.resolve().then(c.t.bind(c,57920,23)),Promise.resolve().then(c.t.bind(c,2761,23)),Promise.resolve().then(c.bind(c,98400))},36616:(a,b,c)=>{"use strict";c.r(b),c.d(b,{"7fa099a1c109e1aac584dd082e37860081313b0947":()=>l});var d=c(90466);c(69300);var e=c(73024),f=c.n(e),g=c(76760),h=c.n(g);let i=async a=>{let b;try{b=await f().promises.open(a,"r")}catch(a){return console.warn(a),!1}if((await b.stat()).isDirectory())return await b.close(),!1;let{ext:c}=h().parse(a);if(![".js",".tsx",".jsx"].includes(c))return await b.close(),!1;let d=await b.readFile("utf8");await b.close();let e=/\bexport\s+default\b/gm.test(d),g=/\bmodule\.exports\s*=/gm.test(d),i=/\bexport\s+\{[^}]*\bdefault\b[^}]*\}/gm.test(d);return e||g||i},j=async(a,b=!1,c=!1,d=a)=>{if(!f().existsSync(a))return;let e=await f().promises.readdir(a,{withFileTypes:!0}),g=await Promise.all(e.map(b=>i(h().join(a,b.name)))),k=e.filter((a,b)=>g[b]).map(a=>b?a.name:a.name.replace(h().extname(a.name),"")),l=await Promise.all(e.filter(a=>a.isDirectory()&&!a.name.startsWith("_")&&"static"!==a.name).map(c=>j(h().join(a,c.name),b,!0,d))),m={absolutePath:a,relativePath:h().relative(d,a),directoryName:a.split(h().sep).pop(),emailFilenames:k,subDirectories:l};return c?(a=>{let b=a;for(;0===b.emailFilenames.length&&1===b.subDirectories.length;){let a=b.subDirectories[0];b={...a,directoryName:h().join(b.directoryName,a.directoryName)}}return b})(m):m};var k=c(18012);let l=async(a,b=!1,c=!1,d=a)=>j(a,b,c,d);(0,k.D)([l]),(0,d.A)(l,"7fa099a1c109e1aac584dd082e37860081313b0947",null)},39520:(a,b,c)=>{"use strict";c.d(b,{a:()=>f});var d=c(2179),e=c(80095);let f=a=>{let b=(0,d.useRef)(null);(0,d.useEffect)(()=>{b.current||(b.current=(0,e.io)());let c=b.current;return c.on("reload",b=>{console.debug("Reloading..."),a(b)}),()=>{c.off()}},[a])}},50938:(a,b,c)=>{Promise.resolve().then(c.bind(c,18695))},54208:(a,b,c)=>{"use strict";c.r(b),c.d(b,{default:()=>n,dynamic:()=>m,metadata:()=>l});var d=c(51064);c(35191);var e=c(90948),f=c(8332),g=c(88155),h=c(84858),i=c.n(h),j=c(54629),k=c.n(j);let l={title:"React Email"},m="force-dynamic";async function n({children:a}){let b=await (0,f.getEmailsDirectoryMetadata)(g.Z8);if(void 0===b)throw Error(`Could not find the emails directory specified under ${g.Z8}!`);return(0,d.jsx)("html",{className:`${i().variable} ${k().variable} font-sans`,lang:"en",children:(0,d.jsx)("body",{className:"relative h-screen bg-black text-slate-11 leading-loose selection:bg-cyan-5 selection:text-cyan-12",children:(0,d.jsx)("div",{className:"bg-gradient-to-t from-slate-3 flex flex-col",children:(0,d.jsx)(e.EmailsProvider,{initialEmailsDirectoryMetadata:b,children:a})})})})}},70985:(a,b,c)=>{"use strict";c.d(b,{Hf:()=>d,m4:()=>e}),process.env.USER_PROJECT_LOCATION,process.env.PREVIEW_SERVER_LOCATION,process.env.EMAILS_DIR_ABSOLUTE_PATH;let d="true"===process.env.NEXT_PUBLIC_IS_BUILDING,e="true"===process.env.NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT},72112:()=>{},86944:(a,b,c)=>{"use strict";c.r(b),c.d(b,{default:()=>e});var d=c(89993);let e=async a=>[{type:"image/x-icon",sizes:"16x16",url:(0,d.fillMetadataSegment)(".",await a.params,"favicon.ico")+""}]},88155:(a,b,c)=>{"use strict";c.d(b,{Hf:()=>g,Z8:()=>f,m4:()=>h,n_:()=>e,w5:()=>d});let d=process.env.USER_PROJECT_LOCATION,e=process.env.PREVIEW_SERVER_LOCATION,f=process.env.EMAILS_DIR_ABSOLUTE_PATH,g="true"===process.env.NEXT_PUBLIC_IS_BUILDING,h="true"===process.env.NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT},90948:(a,b,c)=>{"use strict";c.d(b,{EmailsProvider:()=>e});var d=c(9756);(0,d.registerClientReference)(function(){throw Error("Attempted to call useEmails() from the server but useEmails is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.")},"/home/runner/actions-runner/_work/react-email/react-email/packages/preview-server/src/contexts/emails.tsx","useEmails");let e=(0,d.registerClientReference)(function(){throw Error("Attempted to call EmailsProvider() from the server but EmailsProvider is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.")},"/home/runner/actions-runner/_work/react-email/react-email/packages/preview-server/src/contexts/emails.tsx","EmailsProvider")},92665:()=>{}};