@react-email/preview-server 4.2.12 → 4.3.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 (43) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +9 -9
  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 +2 -2
  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 +20 -20
  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/{597.js → 385.js} +7 -7
  16. package/.next/server/chunks/{593.js → 727.js} +1 -1
  17. package/.next/server/pages/500.html +1 -1
  18. package/.next/server/server-reference-manifest.js +1 -1
  19. package/.next/server/server-reference-manifest.json +1 -1
  20. package/.next/static/chunks/442-9645091f2b304619.js +1 -0
  21. package/.next/static/chunks/{615-96e04174ae422082.js → 615-aa01e647fd9055dc.js} +1 -1
  22. package/.next/static/chunks/900-d73ea57316faa50d.js +1 -0
  23. package/.next/static/chunks/app/layout-0337303a89a72f7e.js +1 -0
  24. package/.next/static/chunks/app/{page-af54466b804e69f7.js → page-80a93dc65160c488.js} +1 -1
  25. package/.next/static/chunks/app/preview/[...slug]/page-3205284657cb4573.js +1 -0
  26. package/.next/static/css/7d8cf00703036864.css +3 -0
  27. package/.next/trace +29 -29
  28. package/CHANGELOG.md +6 -0
  29. package/package.json +12 -12
  30. package/src/app/preview/[...slug]/preview.tsx +25 -21
  31. package/src/components/resizable-wrapper.tsx +167 -69
  32. package/src/components/topbar/active-view-toggle-group.tsx +4 -4
  33. package/src/components/topbar/view-size-controls.tsx +107 -186
  34. package/src/utils/caniemail/tailwind/get-tailwind-config.spec.ts +2 -2
  35. package/src/utils/contains-email-template.spec.ts +6 -6
  36. package/src/utils/js-email-detection.spec.ts +3 -3
  37. package/.next/static/chunks/557-287480320fe241b8.js +0 -1
  38. package/.next/static/chunks/926-cd84f2c04e4197e1.js +0 -1
  39. package/.next/static/chunks/app/layout-873f139023fe1b60.js +0 -1
  40. package/.next/static/chunks/app/preview/[...slug]/page-2bd3b2c525aecbf3.js +0 -1
  41. package/.next/static/css/2016446a90966a61.css +0 -3
  42. /package/.next/static/{uokDo0eZqRUtNrDAJfT75 → bw7nsTv8dL6IXcqslAAMG}/_buildManifest.js +0 -0
  43. /package/.next/static/{uokDo0eZqRUtNrDAJfT75 → bw7nsTv8dL6IXcqslAAMG}/_ssgManifest.js +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @react-email/preview-server
2
2
 
3
+ ## 4.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c0f6ec2: Added resize snapping, refined UI and improved presets
8
+
3
9
  ## 4.2.12
4
10
 
5
11
  ## 4.2.11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-email/preview-server",
3
- "version": "4.2.12",
3
+ "version": "4.3.0",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "main": "./index.mjs",
6
6
  "dependencies": {
@@ -9,13 +9,13 @@
9
9
  "@babel/traverse": "7.27.0",
10
10
  "@lottiefiles/dotlottie-react": "0.13.3",
11
11
  "@radix-ui/colors": "3.0.0",
12
- "@radix-ui/react-collapsible": "1.1.7",
13
- "@radix-ui/react-dropdown-menu": "2.1.10",
14
- "@radix-ui/react-popover": "1.1.10",
15
- "@radix-ui/react-slot": "1.2.0",
16
- "@radix-ui/react-tabs": "1.1.7",
17
- "@radix-ui/react-toggle-group": "1.1.6",
18
- "@radix-ui/react-tooltip": "1.2.3",
12
+ "@radix-ui/react-collapsible": "1.1.12",
13
+ "@radix-ui/react-dropdown-menu": "2.1.16",
14
+ "@radix-ui/react-popover": "1.1.15",
15
+ "@radix-ui/react-slot": "1.2.3",
16
+ "@radix-ui/react-tabs": "1.1.13",
17
+ "@radix-ui/react-toggle-group": "1.1.11",
18
+ "@radix-ui/react-tooltip": "1.2.8",
19
19
  "@types/node": "22.14.1",
20
20
  "@types/normalize-path": "3.0.2",
21
21
  "@types/react": "19.0.10",
@@ -23,8 +23,8 @@
23
23
  "@types/webpack": "5.28.5",
24
24
  "autoprefixer": "10.4.21",
25
25
  "clsx": "2.1.1",
26
- "esbuild": "0.25.0",
27
- "framer-motion": "12.23.12",
26
+ "esbuild": "0.25.10",
27
+ "framer-motion": "12.23.22",
28
28
  "json5": "2.2.3",
29
29
  "log-symbols": "4.1.0",
30
30
  "module-punycode": "npm:punycode@2.3.1",
@@ -35,7 +35,7 @@
35
35
  "prism-react-renderer": "2.4.1",
36
36
  "react": "19.0.0",
37
37
  "react-dom": "19.0.0",
38
- "sharp": "0.34.1",
38
+ "sharp": "0.34.4",
39
39
  "socket.io-client": "4.8.1",
40
40
  "sonner": "2.0.3",
41
41
  "source-map-js": "1.2.1",
@@ -60,7 +60,7 @@
60
60
  "postcss": "8.5.3",
61
61
  "tailwindcss": "3.4.0",
62
62
  "typescript": "5.8.3",
63
- "@react-email/components": "0.5.5"
63
+ "@react-email/components": "0.5.6"
64
64
  },
65
65
  "license": "MIT",
66
66
  "repository": {
@@ -56,17 +56,17 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
56
56
 
57
57
  const [maxWidth, setMaxWidth] = useState(Number.POSITIVE_INFINITY);
58
58
  const [maxHeight, setMaxHeight] = useState(Number.POSITIVE_INFINITY);
59
- const minWidth = 100;
60
- const minHeight = 100;
59
+ const minWidth = 220;
60
+ const minHeight = minWidth * 1.6;
61
61
  const storedWidth = searchParams.get('width');
62
62
  const storedHeight = searchParams.get('height');
63
63
  const [width, setWidth] = useClampedState(
64
- storedWidth ? Number.parseInt(storedWidth) : 600,
64
+ storedWidth ? Number.parseInt(storedWidth) : 1024,
65
65
  minWidth,
66
66
  maxWidth,
67
67
  );
68
68
  const [height, setHeight] = useClampedState(
69
- storedHeight ? Number.parseInt(storedHeight) : 1024,
69
+ storedHeight ? Number.parseInt(storedHeight) : 600,
70
70
  minHeight,
71
71
  maxHeight,
72
72
  );
@@ -83,22 +83,26 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
83
83
  return (
84
84
  <>
85
85
  <Topbar emailTitle={emailTitle}>
86
- <ViewSizeControls
87
- setViewHeight={(height) => {
88
- setHeight(height);
89
- flushSync(() => {
90
- handleSaveViewSize();
91
- });
92
- }}
93
- setViewWidth={(width) => {
94
- setWidth(width);
95
- flushSync(() => {
96
- handleSaveViewSize();
97
- });
98
- }}
99
- viewHeight={height}
100
- viewWidth={width}
101
- />
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
+ )}
102
106
  <ActiveViewToggleGroup
103
107
  activeView={activeView}
104
108
  setActiveView={handleViewChange}
@@ -113,7 +117,7 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
113
117
  <div
114
118
  {...props}
115
119
  className={cn(
116
- 'h-[calc(100%-3.5rem-2.375rem)] will-change-[height] flex p-4 transition-[height] duration-300',
120
+ 'h-[calc(100%-3.5rem-2.375rem)] will-change-[height] flex p-4 transition-[height] duration-300 relative',
117
121
  activeView === 'preview' && 'bg-gray-200',
118
122
  toolbarToggled && 'h-[calc(100%-3.5rem-13rem)]',
119
123
  className,
@@ -1,6 +1,13 @@
1
1
  import { Slot } from '@radix-ui/react-slot';
2
- import { type ComponentProps, useCallback, useEffect, useRef } from 'react';
2
+ import {
3
+ type ComponentProps,
4
+ useCallback,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
3
9
  import { cn } from '../utils';
10
+ import { VIEW_PRESETS } from './topbar/view-size-controls';
4
11
 
5
12
  type Direction = 'north' | 'south' | 'east' | 'west';
6
13
 
@@ -56,14 +63,17 @@ export const ResizableWrapper = ({
56
63
  ...rest
57
64
  }: ResizableWrapperProps) => {
58
65
  const resizableRef = useRef<HTMLElement>(null);
59
-
66
+ const [isResizing, setIsResizing] = useState(false);
60
67
  const mouseMoveListener = useRef<(event: MouseEvent) => void>(null);
68
+ const [direction, setDirection] = useState<Direction | null>(null);
61
69
 
62
70
  const handleStopResizing = useCallback(() => {
63
71
  if (mouseMoveListener.current) {
64
72
  document.removeEventListener('mousemove', mouseMoveListener.current);
65
73
  }
66
74
  document.removeEventListener('mouseup', handleStopResizing);
75
+ setIsResizing(false);
76
+ setDirection(null);
67
77
  onResizeEnd?.();
68
78
  }, []);
69
79
 
@@ -78,6 +88,38 @@ export const ResizableWrapper = ({
78
88
  const center = isHorizontal
79
89
  ? resizableBoundingRect.x + resizableBoundingRect.width / 2
80
90
  : resizableBoundingRect.y + resizableBoundingRect.height / 2;
91
+
92
+ const newPosition = Math.abs(mousePosition - center) * 2;
93
+
94
+ setIsResizing(true);
95
+ setDirection(direction);
96
+
97
+ const threshold = 12;
98
+
99
+ for (let i = 0; i < VIEW_PRESETS.length; i++) {
100
+ const preset = VIEW_PRESETS[i];
101
+
102
+ if (preset) {
103
+ if (
104
+ isHorizontal &&
105
+ newPosition > preset.dimensions.width - threshold &&
106
+ newPosition < preset.dimensions.width + threshold
107
+ ) {
108
+ onResize(preset.dimensions.width, direction);
109
+ return;
110
+ }
111
+
112
+ if (
113
+ !isHorizontal &&
114
+ newPosition > preset.dimensions.height - threshold &&
115
+ newPosition < preset.dimensions.height + threshold
116
+ ) {
117
+ onResize(preset.dimensions.height, direction);
118
+ return;
119
+ }
120
+ }
121
+ }
122
+
81
123
  onResize(Math.abs(mousePosition - center) * 2, direction);
82
124
  } else {
83
125
  handleStopResizing();
@@ -99,76 +141,132 @@ export const ResizableWrapper = ({
99
141
  }, []);
100
142
 
101
143
  return (
102
- <div
103
- {...rest}
104
- className={cn('relative mx-auto my-auto box-content', rest.className)}
105
- >
106
- <div
107
- aria-label="resize-west"
108
- aria-valuenow={width}
109
- aria-valuemin={minWidth}
110
- aria-valuemax={maxWidth}
111
- className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-2 cursor-w-resize p-2 [user-drag:none]"
112
- onDragStart={(event) => event.preventDefault()}
113
- draggable="false"
114
- onMouseDown={() => {
115
- handleStartResizing('west');
116
- }}
117
- role="slider"
118
- tabIndex={0}
119
- >
120
- <div className="h-8 w-1 rounded-md bg-black/30" />
121
- </div>
122
- <div
123
- aria-label="resize-east"
124
- aria-valuenow={width}
125
- aria-valuemin={minWidth}
126
- aria-valuemax={maxWidth}
127
- onDragStart={(event) => event.preventDefault()}
128
- className="-translate-x-full -translate-y-1/2 absolute top-1/2 left-full cursor-e-resize p-2 [user-drag:none]"
129
- draggable="false"
130
- onMouseDown={() => {
131
- handleStartResizing('east');
132
- }}
133
- role="slider"
134
- tabIndex={0}
135
- >
136
- <div className="h-8 w-1 rounded-md bg-black/30" />
137
- </div>
138
- <div
139
- aria-label="resize-north"
140
- aria-valuenow={height}
141
- aria-valuemin={minHeight}
142
- aria-valuemax={maxHeight}
143
- onDragStart={(event) => event.preventDefault()}
144
- className="-translate-x-1/2 -translate-y-1/2 absolute top-0 left-1/2 cursor-n-resize p-2 [user-drag:none]"
145
- draggable="false"
146
- onMouseDown={() => {
147
- handleStartResizing('north');
148
- }}
149
- role="slider"
150
- tabIndex={0}
151
- >
152
- <div className="h-1 w-8 rounded-md bg-black/30" />
144
+ <>
145
+ <div className=" overflow-hidden absolute inset-0">
146
+ <div className="absolute mx-auto box-content -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2">
147
+ {VIEW_PRESETS.map((preset) => (
148
+ <div
149
+ key={preset.name}
150
+ className="-translate-x-1/2 -translate-y-1/2 absolute pointer-events-none select-none"
151
+ style={{
152
+ width: preset.dimensions.width,
153
+ height: preset.dimensions.height,
154
+ }}
155
+ >
156
+ {width === preset.dimensions.width &&
157
+ isResizing &&
158
+ (direction === 'east' || direction === 'west') && (
159
+ <>
160
+ <div className="absolute right-0 -top-[100vw] -bottom-[100vw] border-r-2 border-cyan-5" />
161
+ <div className="absolute left-0 -top-[100vw] -bottom-[100vw] border-l-2 border-cyan-5" />
162
+ </>
163
+ )}
164
+
165
+ {height === preset.dimensions.height &&
166
+ isResizing &&
167
+ (direction === 'north' || direction === 'south') && (
168
+ <>
169
+ <div className="absolute top-0 -left-[100vw] -right-[100vw] border-t-2 border-cyan-5" />
170
+ <div className="absolute bottom-0 -left-[100vw] -right-[100vw] border-b-2 border-cyan-5" />
171
+ </>
172
+ )}
173
+ </div>
174
+ ))}
175
+ </div>
153
176
  </div>
177
+
154
178
  <div
155
- aria-label="resize-south"
156
- aria-valuenow={height}
157
- aria-valuemin={minHeight}
158
- aria-valuemax={maxHeight}
159
- onDragStart={(event) => event.preventDefault()}
160
- className="-translate-x-1/2 -translate-y-1/2 absolute top-full left-1/2 cursor-s-resize p-2 [user-drag:none]"
161
- draggable="false"
162
- onMouseDown={() => {
163
- handleStartResizing('south');
164
- }}
165
- role="slider"
166
- tabIndex={0}
179
+ {...rest}
180
+ className={cn('relative mx-auto my-auto box-content', rest.className)}
167
181
  >
168
- <div className="h-1 w-8 rounded-md bg-black/30" />
169
- </div>
182
+ <div
183
+ aria-label="resize-west"
184
+ aria-valuenow={width}
185
+ aria-valuemin={minWidth}
186
+ aria-valuemax={maxWidth}
187
+ className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 -left-2 cursor-w-resize p-2 [user-drag:none]"
188
+ onDragStart={(event) => event.preventDefault()}
189
+ draggable="false"
190
+ onMouseDown={() => {
191
+ handleStartResizing('west');
192
+ }}
193
+ role="slider"
194
+ tabIndex={0}
195
+ >
196
+ <div
197
+ className={cn('h-8 w-1 rounded-md bg-black/50 transition-colors', {
198
+ 'bg-black': direction === 'west',
199
+ })}
200
+ />
201
+ </div>
202
+ <div
203
+ aria-label="resize-east"
204
+ aria-valuenow={width}
205
+ aria-valuemin={minWidth}
206
+ aria-valuemax={maxWidth}
207
+ onDragStart={(event) => event.preventDefault()}
208
+ className="translate-x-1/2 -translate-y-1/2 absolute top-1/2 -right-2 cursor-e-resize p-2 [user-drag:none]"
209
+ draggable="false"
210
+ onMouseDown={() => {
211
+ handleStartResizing('east');
212
+ }}
213
+ role="slider"
214
+ tabIndex={0}
215
+ >
216
+ <div
217
+ className={cn('h-8 w-1 rounded-md bg-black/50 transition-colors', {
218
+ 'bg-black': direction === 'east',
219
+ })}
220
+ />
221
+ </div>
222
+ <div
223
+ aria-label="resize-north"
224
+ aria-valuenow={height}
225
+ aria-valuemin={minHeight}
226
+ aria-valuemax={maxHeight}
227
+ onDragStart={(event) => event.preventDefault()}
228
+ className="-translate-x-1/2 -translate-y-1/2 absolute -top-2 left-1/2 cursor-n-resize p-2 [user-drag:none]"
229
+ draggable="false"
230
+ onMouseDown={() => {
231
+ handleStartResizing('north');
232
+ }}
233
+ role="slider"
234
+ tabIndex={0}
235
+ >
236
+ <div
237
+ className={cn('h-1 w-8 rounded-md bg-black/50 transition-colors', {
238
+ 'bg-black': direction === 'north',
239
+ })}
240
+ />
241
+ </div>
242
+ <div
243
+ aria-label="resize-south"
244
+ aria-valuenow={height}
245
+ aria-valuemin={minHeight}
246
+ aria-valuemax={maxHeight}
247
+ onDragStart={(event) => event.preventDefault()}
248
+ className="-translate-x-1/2 translate-y-1/2 absolute -bottom-2 left-1/2 cursor-s-resize p-2 [user-drag:none]"
249
+ draggable="false"
250
+ onMouseDown={() => {
251
+ handleStartResizing('south');
252
+ }}
253
+ role="slider"
254
+ tabIndex={0}
255
+ >
256
+ <div
257
+ className={cn('h-1 w-8 rounded-md bg-black/50 transition-colors', {
258
+ 'bg-black': direction === 'south',
259
+ })}
260
+ />
261
+ </div>
170
262
 
171
- <Slot ref={resizableRef}>{children}</Slot>
172
- </div>
263
+ <Slot
264
+ ref={resizableRef}
265
+ className={isResizing ? 'pointer-events-none select-none' : ''}
266
+ >
267
+ {children}
268
+ </Slot>
269
+ </div>
270
+ </>
173
271
  );
174
272
  };
@@ -30,7 +30,7 @@ export const ActiveViewToggleGroup = ({
30
30
  <Tooltip.Trigger asChild>
31
31
  <div
32
32
  className={cn(
33
- 'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
33
+ 'w-9 flex items-center py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
34
34
  {
35
35
  'text-slate-11': activeView !== 'desktop',
36
36
  'text-slate-12': activeView === 'desktop',
@@ -47,7 +47,7 @@ export const ActiveViewToggleGroup = ({
47
47
  transition={tabTransition}
48
48
  />
49
49
  )}
50
- <IconMonitor />
50
+ <IconMonitor className="m-auto" />
51
51
  </div>
52
52
  </Tooltip.Trigger>
53
53
  <Tooltip.Content>Preview</Tooltip.Content>
@@ -58,7 +58,7 @@ export const ActiveViewToggleGroup = ({
58
58
  <Tooltip.Trigger asChild>
59
59
  <div
60
60
  className={cn(
61
- 'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
61
+ 'w-9 flex py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
62
62
  {
63
63
  'text-slate-11': activeView !== 'source',
64
64
  'text-slate-12': activeView === 'source',
@@ -75,7 +75,7 @@ export const ActiveViewToggleGroup = ({
75
75
  transition={tabTransition}
76
76
  />
77
77
  )}
78
- <IconSource />
78
+ <IconSource className="m-auto" />
79
79
  </div>
80
80
  </Tooltip.Trigger>
81
81
  <Tooltip.Content>Code</Tooltip.Content>