@react-email/preview-server 4.2.11 → 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 (76) 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/next-minimal-server.js.nft.json +1 -1
  5. package/.next/next-server.js.nft.json +1 -1
  6. package/.next/prerender-manifest.json +3 -3
  7. package/.next/server/app/_not-found/page.js +1 -1
  8. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  9. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  10. package/.next/server/app/page.js +2 -2
  11. package/.next/server/app/page.js.nft.json +1 -1
  12. package/.next/server/app/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/preview/[...slug]/page.js +30 -30
  14. package/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  15. package/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  16. package/.next/server/chunks/235.js +1 -1
  17. package/.next/server/chunks/{597.js → 385.js} +7 -7
  18. package/.next/server/chunks/630.js +1 -1
  19. package/.next/server/chunks/727.js +1 -0
  20. package/.next/server/pages/500.html +1 -1
  21. package/.next/server/server-reference-manifest.js +1 -1
  22. package/.next/server/server-reference-manifest.json +1 -1
  23. package/.next/static/chunks/442-9645091f2b304619.js +1 -0
  24. package/.next/static/chunks/{615-5d450200bdf8a0cb.js → 615-aa01e647fd9055dc.js} +1 -1
  25. package/.next/static/chunks/900-d73ea57316faa50d.js +1 -0
  26. package/.next/static/chunks/app/layout-0337303a89a72f7e.js +1 -0
  27. package/.next/static/chunks/app/{page-af54466b804e69f7.js → page-80a93dc65160c488.js} +1 -1
  28. package/.next/static/chunks/app/preview/[...slug]/page-3205284657cb4573.js +1 -0
  29. package/.next/static/css/7d8cf00703036864.css +3 -0
  30. package/.next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
  31. package/.next/static/media/21350d82a1f187e9-s.woff2 +0 -0
  32. package/.next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
  33. package/.next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
  34. package/.next/trace +29 -29
  35. package/CHANGELOG.md +8 -0
  36. package/package.json +12 -12
  37. package/readme.md +3 -4
  38. package/src/actions/email-validation/check-compatibility.ts +1 -0
  39. package/src/actions/get-email-path-from-slug.ts +1 -1
  40. package/src/actions/render-email-by-path.tsx +8 -3
  41. package/src/app/env.ts +0 -4
  42. package/src/app/layout.tsx +8 -5
  43. package/src/app/page.tsx +2 -4
  44. package/src/app/preview/[...slug]/error-overlay.tsx +1 -0
  45. package/src/app/preview/[...slug]/page.tsx +3 -5
  46. package/src/app/preview/[...slug]/preview.tsx +28 -24
  47. package/src/components/resizable-wrapper.tsx +170 -71
  48. package/src/components/toolbar.tsx +2 -3
  49. package/src/components/topbar/active-view-toggle-group.tsx +4 -4
  50. package/src/components/topbar/view-size-controls.tsx +107 -186
  51. package/src/contexts/emails.tsx +3 -6
  52. package/src/contexts/preview.tsx +13 -1
  53. package/src/hooks/use-hot-reload.ts +2 -2
  54. package/src/utils/caniemail/get-compatibility-stats-for-entry.ts +0 -1
  55. package/src/utils/caniemail/tailwind/get-tailwind-config.spec.ts +2 -2
  56. package/src/utils/contains-email-template.spec.ts +6 -6
  57. package/src/utils/convert-stack-with-sourcemap.ts +0 -1
  58. package/src/utils/get-emails-directory-metadata.ts +0 -1
  59. package/src/utils/js-email-detection.spec.ts +3 -3
  60. package/src/utils/run-bundled-code.ts +1 -3
  61. package/src/utils/testing/request-response-email.tsx +0 -1
  62. package/tailwind-internals.d.ts +0 -2
  63. package/.next/server/chunks/420.js +0 -1
  64. package/.next/static/chunks/557-287480320fe241b8.js +0 -1
  65. package/.next/static/chunks/926-cd84f2c04e4197e1.js +0 -1
  66. package/.next/static/chunks/app/layout-581001443a6ac38a.js +0 -1
  67. package/.next/static/chunks/app/preview/[...slug]/page-356d4a373756b232.js +0 -1
  68. package/.next/static/css/e2f28c91a6a919eb.css +0 -3
  69. package/.next/static/media/26a46d62cd723877-s.woff2 +0 -0
  70. package/.next/static/media/55c55f0601d81cf3-s.woff2 +0 -0
  71. package/.next/static/media/581909926a08bbc8-s.woff2 +0 -0
  72. package/.next/static/media/97e0cb1ae144a2a9-s.woff2 +0 -0
  73. package/src/contexts/fragment-identifier.tsx +0 -48
  74. package/src/hooks/use-icon-animation.ts +0 -41
  75. /package/.next/static/{OIjazFWdAl7sqkAJKSCpp → bw7nsTv8dL6IXcqslAAMG}/_buildManifest.js +0 -0
  76. /package/.next/static/{OIjazFWdAl7sqkAJKSCpp → bw7nsTv8dL6IXcqslAAMG}/_ssgManifest.js +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
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
+
9
+ ## 4.2.12
10
+
3
11
  ## 4.2.11
4
12
 
5
13
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-email/preview-server",
3
- "version": "4.2.11",
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.3"
63
+ "@react-email/components": "0.5.6"
64
64
  },
65
65
  "license": "MIT",
66
66
  "repository": {
package/readme.md CHANGED
@@ -2,11 +2,10 @@
2
2
  <div align="center">A live preview of your emails right in your browser.</div>
3
3
  <br />
4
4
  <div align="center">
5
- <a href="https://react.email">Website</a>
5
+ <a href="https://react.email">Website</a>
6
6
  <span> · </span>
7
- <a href="https://github.com/resend/react-email">GitHub</a>
8
- <span> · </span>
9
- <a href="https://react.email/discord">Discord</a>
7
+ <a href="https://github.com/resend/react-email">GitHub</a>
8
+
10
9
  </div>
11
10
 
12
11
  This package is used to store the preview server, it is also published and versioned so that it can be installed when the [CLI](../react-email) is being used.
@@ -1,4 +1,5 @@
1
1
  'use server';
2
+
2
3
  import { parse } from '@babel/parser';
3
4
  import traverse from '@babel/traverse';
4
5
  import {
@@ -1,10 +1,10 @@
1
1
  'use server';
2
+
2
3
  import fs from 'node:fs';
3
4
  import path from 'node:path';
4
5
  import { cache } from 'react';
5
6
  import { emailsDirectoryAbsolutePath } from '../app/env';
6
7
 
7
- // eslint-disable-next-line @typescript-eslint/require-await
8
8
  export const getEmailPathFromSlug = cache(async (slug: string) => {
9
9
  if (['.tsx', '.jsx', '.ts', '.js'].includes(path.extname(slug)))
10
10
  return path.join(emailsDirectoryAbsolutePath, slug);
@@ -1,4 +1,5 @@
1
1
  'use server';
2
+
2
3
  import fs from 'node:fs';
3
4
  import path from 'node:path';
4
5
  import { styleText } from 'node:util';
@@ -40,9 +41,13 @@ export const renderEmailByPath = async (
40
41
  emailPath: string,
41
42
  invalidatingCache = false,
42
43
  ): Promise<EmailRenderingResult> => {
43
- if (invalidatingCache) cache.delete(emailPath);
44
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
45
- if (cache.has(emailPath)) return cache.get(emailPath)!;
44
+ if (invalidatingCache) {
45
+ cache.delete(emailPath);
46
+ }
47
+
48
+ if (cache.has(emailPath)) {
49
+ return cache.get(emailPath)!;
50
+ }
46
51
 
47
52
  const timeBeforeEmailRendered = performance.now();
48
53
 
package/src/app/env.ts CHANGED
@@ -1,7 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
- /** ONLY ACCESSIBLE ON THE SERVER */
3
- export const emailsDirRelativePath = process.env.EMAILS_DIR_RELATIVE_PATH!;
4
-
5
1
  /** ONLY ACCESSIBLE ON THE SERVER */
6
2
  export const userProjectLocation = process.env.USER_PROJECT_LOCATION!;
7
3
 
@@ -1,5 +1,6 @@
1
- import type { Metadata } from 'next';
2
1
  import './globals.css';
2
+
3
+ import type { Metadata } from 'next';
3
4
  import { EmailsProvider } from '../contexts/emails';
4
5
  import { getEmailsDirectoryMetadata } from '../utils/get-emails-directory-metadata';
5
6
  import { emailsDirectoryAbsolutePath } from './env';
@@ -11,7 +12,11 @@ export const metadata: Metadata = {
11
12
 
12
13
  export const dynamic = 'force-dynamic';
13
14
 
14
- const RootLayout = async ({ children }: { children: React.ReactNode }) => {
15
+ export default async function RootLayout({
16
+ children,
17
+ }: {
18
+ children: React.ReactNode;
19
+ }) {
15
20
  const emailsDirectoryMetadata = await getEmailsDirectoryMetadata(
16
21
  emailsDirectoryAbsolutePath,
17
22
  );
@@ -38,6 +43,4 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
38
43
  </body>
39
44
  </html>
40
45
  );
41
- };
42
-
43
- export default RootLayout;
46
+ }
package/src/app/page.tsx CHANGED
@@ -7,7 +7,7 @@ import { Shell } from '../components/shell';
7
7
  import { emailsDirectoryAbsolutePath } from './env';
8
8
  import logo from './logo.png';
9
9
 
10
- const Home = () => {
10
+ export default function Home() {
11
11
  const baseEmailsDirectoryName = path.basename(emailsDirectoryAbsolutePath);
12
12
 
13
13
  return (
@@ -41,6 +41,4 @@ const Home = () => {
41
41
  </div>
42
42
  </Shell>
43
43
  );
44
- };
45
-
46
- export default Home;
44
+ }
@@ -1,4 +1,5 @@
1
1
  'use client';
2
+
2
3
  import type { ErrorObject } from '../../../utils/types/error-object';
3
4
 
4
5
  interface ErrorOverlayProps {
@@ -26,11 +26,11 @@ export interface PreviewParams {
26
26
  slug: string[];
27
27
  }
28
28
 
29
- const Page = async ({
29
+ export default async function Page({
30
30
  params: paramsPromise,
31
31
  }: {
32
32
  params: Promise<PreviewParams>;
33
- }) => {
33
+ }) {
34
34
  const params = await paramsPromise;
35
35
  // will come in here as segments of a relative path to the email
36
36
  // ex: ['authentication', 'verify-password.tsx']
@@ -141,7 +141,7 @@ This is most likely not an issue with the preview server. Maybe there was a typo
141
141
  </Shell>
142
142
  </PreviewProvider>
143
143
  );
144
- };
144
+ }
145
145
 
146
146
  export async function generateMetadata({
147
147
  params,
@@ -152,5 +152,3 @@ export async function generateMetadata({
152
152
 
153
153
  return { title: `${path.basename(slug.join('/'))} — React Email` };
154
154
  }
155
-
156
- export default Page;
@@ -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 { useState } from 'react';
5
5
  import { flushSync } from 'react-dom';
6
6
  import { Toaster } from 'sonner';
7
7
  import { useDebouncedCallback } from 'use-debounce';
@@ -16,7 +16,7 @@ import { useToolbarState } from '../../../components/toolbar';
16
16
  import { Tooltip } from '../../../components/tooltip';
17
17
  import { ActiveViewToggleGroup } from '../../../components/topbar/active-view-toggle-group';
18
18
  import { ViewSizeControls } from '../../../components/topbar/view-size-controls';
19
- import { PreviewContext } from '../../../contexts/preview';
19
+ import { usePreviewContext } from '../../../contexts/preview';
20
20
  import { useClampedState } from '../../../hooks/use-clamped-state';
21
21
  import { cn } from '../../../utils';
22
22
  import { ErrorOverlay } from './error-overlay';
@@ -26,7 +26,7 @@ interface PreviewProps extends React.ComponentProps<'div'> {
26
26
  }
27
27
 
28
28
  const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
29
- const { renderingResult, renderedEmailMetadata } = use(PreviewContext)!;
29
+ const { renderingResult, renderedEmailMetadata } = usePreviewContext();
30
30
 
31
31
  const router = useRouter();
32
32
  const pathname = usePathname();
@@ -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();
@@ -89,85 +131,142 @@ export const ResizableWrapper = ({
89
131
  };
90
132
 
91
133
  useEffect(() => {
92
- if (!window.document) return;
134
+ if (!window.document) {
135
+ return;
136
+ }
93
137
 
94
138
  return () => {
95
139
  handleStopResizing();
96
140
  };
97
- // eslint-disable-next-line react-hooks/exhaustive-deps
98
141
  }, []);
99
142
 
100
143
  return (
101
- <div
102
- {...rest}
103
- className={cn('relative mx-auto my-auto box-content', rest.className)}
104
- >
105
- <div
106
- aria-label="resize-west"
107
- aria-valuenow={width}
108
- aria-valuemin={minWidth}
109
- aria-valuemax={maxWidth}
110
- className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-2 cursor-w-resize p-2 [user-drag:none]"
111
- onDragStart={(event) => event.preventDefault()}
112
- draggable="false"
113
- onMouseDown={() => {
114
- handleStartResizing('west');
115
- }}
116
- role="slider"
117
- tabIndex={0}
118
- >
119
- <div className="h-8 w-1 rounded-md bg-black/30" />
120
- </div>
121
- <div
122
- aria-label="resize-east"
123
- aria-valuenow={width}
124
- aria-valuemin={minWidth}
125
- aria-valuemax={maxWidth}
126
- onDragStart={(event) => event.preventDefault()}
127
- className="-translate-x-full -translate-y-1/2 absolute top-1/2 left-full cursor-e-resize p-2 [user-drag:none]"
128
- draggable="false"
129
- onMouseDown={() => {
130
- handleStartResizing('east');
131
- }}
132
- role="slider"
133
- tabIndex={0}
134
- >
135
- <div className="h-8 w-1 rounded-md bg-black/30" />
136
- </div>
137
- <div
138
- aria-label="resize-north"
139
- aria-valuenow={height}
140
- aria-valuemin={minHeight}
141
- aria-valuemax={maxHeight}
142
- onDragStart={(event) => event.preventDefault()}
143
- className="-translate-x-1/2 -translate-y-1/2 absolute top-0 left-1/2 cursor-n-resize p-2 [user-drag:none]"
144
- draggable="false"
145
- onMouseDown={() => {
146
- handleStartResizing('north');
147
- }}
148
- role="slider"
149
- tabIndex={0}
150
- >
151
- <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>
152
176
  </div>
177
+
153
178
  <div
154
- aria-label="resize-south"
155
- aria-valuenow={height}
156
- aria-valuemin={minHeight}
157
- aria-valuemax={maxHeight}
158
- onDragStart={(event) => event.preventDefault()}
159
- className="-translate-x-1/2 -translate-y-1/2 absolute top-full left-1/2 cursor-s-resize p-2 [user-drag:none]"
160
- draggable="false"
161
- onMouseDown={() => {
162
- handleStartResizing('south');
163
- }}
164
- role="slider"
165
- tabIndex={0}
179
+ {...rest}
180
+ className={cn('relative mx-auto my-auto box-content', rest.className)}
166
181
  >
167
- <div className="h-1 w-8 rounded-md bg-black/30" />
168
- </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>
169
262
 
170
- <Slot ref={resizableRef}>{children}</Slot>
171
- </div>
263
+ <Slot
264
+ ref={resizableRef}
265
+ className={isResizing ? 'pointer-events-none select-none' : ''}
266
+ >
267
+ {children}
268
+ </Slot>
269
+ </div>
270
+ </>
172
271
  );
173
272
  };
@@ -5,7 +5,7 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
5
5
  import * as React from 'react';
6
6
  import type { CompatibilityCheckingResult } from '../actions/email-validation/check-compatibility';
7
7
  import { isBuilding } from '../app/env';
8
- import { PreviewContext } from '../contexts/preview';
8
+ import { usePreviewContext } from '../contexts/preview';
9
9
  import { cn } from '../utils';
10
10
  import { IconArrowDown } from './icons/icon-arrow-down';
11
11
  import { IconCheck } from './icons/icon-check';
@@ -332,8 +332,7 @@ export const Toolbar = ({
332
332
  serverSpamCheckingResult,
333
333
  serverCompatibilityResults,
334
334
  }: ToolbarProps) => {
335
- const { emailPath, emailSlug, renderedEmailMetadata } =
336
- React.use(PreviewContext)!;
335
+ const { emailPath, emailSlug, renderedEmailMetadata } = usePreviewContext();
337
336
 
338
337
  if (renderedEmailMetadata === undefined) return null;
339
338
  const { prettyMarkup, plainText, reactMarkup } = renderedEmailMetadata;
@@ -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>