@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.
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +6 -6
- package/.next/build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/preview/[...slug]/page.js +25 -25
- package/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
- package/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/235.js +1 -1
- package/.next/server/chunks/74.js +1 -0
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/611-3eea98bdc9dc1702.js +1 -0
- package/.next/static/chunks/{615-e223b553c72ae727.js → 615-72dd0f206c15c68c.js} +1 -1
- package/.next/static/chunks/app/layout-e10b48dd22650439.js +1 -0
- package/.next/static/chunks/app/preview/[...slug]/page-30fcc9fc52a5a560.js +1 -0
- package/.next/static/css/{7d8cf00703036864.css → 1d22a5c848552bfc.css} +1 -1
- package/.next/trace +28 -28
- package/CHANGELOG.md +12 -0
- package/package.json +4 -2
- package/src/actions/render-email-by-path.tsx +1 -1
- package/src/app/preview/[...slug]/email-frame.tsx +306 -0
- package/src/app/preview/[...slug]/preview.tsx +48 -30
- package/src/components/icons/icon-base.tsx +2 -2
- package/src/components/icons/icon-moon.tsx +13 -0
- package/src/components/icons/icon-sun.tsx +13 -0
- package/src/components/topbar/emulated-dark-mode-toggle.tsx +36 -0
- package/src/utils/style-text.ts +11 -0
- package/.next/server/chunks/571.js +0 -1
- package/.next/static/chunks/442-9645091f2b304619.js +0 -1
- package/.next/static/chunks/app/layout-596f02410fa6cec2.js +0 -1
- package/.next/static/chunks/app/preview/[...slug]/page-337a48d611839d3a.js +0 -1
- /package/.next/static/{04HKn1-CcCqsWXfS4b-9r → ixMN5IIqPIc0E9e26HFvF}/_buildManifest.js +0 -0
- /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.
|
|
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.
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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.
|
|
4
|
-
export type RootProps = React.
|
|
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:()=>{}};
|