@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
@@ -1,5 +1,4 @@
1
- import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
2
- import { motion } from 'framer-motion';
1
+ import * as Popover from '@radix-ui/react-popover';
3
2
  import * as React from 'react';
4
3
  import { cn } from '../../utils';
5
4
  import { IconArrowDown } from '../icons/icon-arrow-down';
@@ -11,208 +10,103 @@ interface ViewDimensions {
11
10
  }
12
11
 
13
12
  interface ViewSizeControlsProps {
13
+ minWidth: number;
14
+ minHeight: number;
14
15
  viewWidth: number;
15
16
  setViewWidth: (width: number) => void;
16
17
  viewHeight: number;
17
18
  setViewHeight: (height: number) => void;
18
19
  }
19
20
 
20
- interface DimensionInputProps {
21
- icon: React.ReactNode;
22
- isActive: boolean;
23
- label: string;
24
- onBlur: () => void;
25
- onChange: (value: number) => void;
26
- setIsActive: (active: boolean) => void;
27
- value: number;
28
- hasBorder?: boolean;
29
- }
30
-
31
21
  interface PresetOption {
32
22
  name: string;
33
23
  dimensions: ViewDimensions;
24
+ icon: React.ReactNode;
34
25
  }
35
26
 
36
- interface PresetMenuItemProps {
37
- name: string;
38
- dimensions: ViewDimensions;
39
- onSelect: (dimensions: ViewDimensions) => void;
40
- }
41
-
42
- const VIEW_PRESETS: PresetOption[] = [
43
- { name: 'Desktop', dimensions: { width: 600, height: 1024 } },
44
- { name: 'Mobile', dimensions: { width: 375, height: 812 } },
45
- ];
46
-
47
- const inputVariant = {
48
- active: {
49
- width: '3.5rem',
50
- padding: '0 0 0 0.5rem',
27
+ export const VIEW_PRESETS: PresetOption[] = [
28
+ {
29
+ name: 'Desktop',
30
+ dimensions: { width: 1024, height: 600 },
31
+ icon: (
32
+ <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
33
+ <path
34
+ d="M1 3.25C1 3.11193 1.11193 3 1.25 3H13.75C13.8881 3 14 3.11193 14 3.25V10.75C14 10.8881 13.8881 11 13.75 11H1.25C1.11193 11 1 10.8881 1 10.75V3.25ZM1.25 2C0.559643 2 0 2.55964 0 3.25V10.75C0 11.4404 0.559644 12 1.25 12H5.07341L4.82991 13.2986C4.76645 13.6371 5.02612 13.95 5.37049 13.95H9.62951C9.97389 13.95 10.2336 13.6371 10.1701 13.2986L9.92659 12H13.75C14.4404 12 15 11.4404 15 10.75V3.25C15 2.55964 14.4404 2 13.75 2H1.25ZM9.01091 12H5.98909L5.79222 13.05H9.20778L9.01091 12Z"
35
+ fill="currentColor"
36
+ fillRule="evenodd"
37
+ clipRule="evenodd"
38
+ />
39
+ </svg>
40
+ ),
51
41
  },
52
- inactive: {
53
- width: '0',
42
+ {
43
+ name: 'Mobile',
44
+ dimensions: { width: 375, height: 667 },
45
+ icon: (
46
+ <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
47
+ <path
48
+ d="M4 2.5C4 2.22386 4.22386 2 4.5 2H10.5C10.7761 2 11 2.22386 11 2.5V12.5C11 12.7761 10.7761 13 10.5 13H4.5C4.22386 13 4 12.7761 4 12.5V2.5ZM4.5 1C3.67157 1 3 1.67157 3 2.5V12.5C3 13.3284 3.67157 14 4.5 14H10.5C11.3284 14 12 13.3284 12 12.5V2.5C12 1.67157 11.3284 1 10.5 1H4.5ZM6 11.65C5.8067 11.65 5.65 11.8067 5.65 12C5.65 12.1933 5.8067 12.35 6 12.35H9C9.1933 12.35 9.35 12.1933 9.35 12C9.35 11.8067 9.1933 11.65 9 11.65H6Z"
49
+ fill="currentColor"
50
+ fillRule="evenodd"
51
+ clipRule="evenodd"
52
+ />
53
+ </svg>
54
+ ),
54
55
  },
55
- };
56
-
57
- const DimensionInput = ({
58
- icon,
59
- isActive,
60
- label,
61
- onBlur,
62
- onChange,
63
- setIsActive,
64
- value,
65
- hasBorder,
66
- }: DimensionInputProps) => {
67
- const inputRef = React.useRef<HTMLInputElement>(null);
68
-
69
- React.useEffect(() => {
70
- if (isActive && inputRef.current) {
71
- inputRef.current.focus();
72
- }
73
- }, [isActive]);
74
-
75
- const handleButtonClick = () => {
76
- if (isActive) {
77
- setIsActive(false);
78
- } else {
79
- setIsActive(true);
80
- }
81
- };
82
-
83
- return (
84
- <Tooltip.Provider>
85
- <Tooltip>
86
- <Tooltip.Trigger asChild>
87
- <motion.button
88
- onClick={handleButtonClick}
89
- className={cn('relative flex items-center justify-center p-2', {
90
- 'border-slate-6 border-r': hasBorder,
91
- })}
92
- >
93
- {icon}
94
- <motion.input
95
- ref={inputRef}
96
- initial={false}
97
- animate={isActive ? 'active' : 'inactive'}
98
- className="arrow-hide relative flex h-8 items-center justify-center bg-black text-sm outline-none"
99
- onChange={(e) => onChange(Number.parseInt(e.currentTarget.value))}
100
- onBlur={onBlur}
101
- type="number"
102
- value={value}
103
- variants={inputVariant}
104
- />
105
- </motion.button>
106
- </Tooltip.Trigger>
107
- <Tooltip.Content>
108
- <span>{label}: </span>
109
- <span className="font-mono">{value}px</span>
110
- </Tooltip.Content>
111
- </Tooltip>
112
- </Tooltip.Provider>
113
- );
114
- };
115
-
116
- const PresetMenuItem = ({
117
- name,
118
- dimensions,
119
- onSelect,
120
- }: PresetMenuItemProps) => (
121
- <DropdownMenu.Item
122
- className="group flex w-full cursor-pointer select-none items-center justify-between rounded-md py-1.5 pr-1 pl-2 text-sm outline-none transition-colors data-[highlighted]:bg-slate-5"
123
- onClick={() => onSelect(dimensions)}
124
- >
125
- {name}
126
- <span className="flex h-fit items-center rounded-full bg-slate-6 px-2 py-1 font-medium text-slate-11 text-xs">
127
- {dimensions.width}x{dimensions.height}
128
- </span>
129
- </DropdownMenu.Item>
130
- );
56
+ ];
131
57
 
132
58
  export const ViewSizeControls = ({
59
+ minWidth,
60
+ minHeight,
133
61
  viewWidth,
134
62
  viewHeight,
135
63
  setViewWidth,
136
64
  setViewHeight,
137
65
  }: ViewSizeControlsProps) => {
138
66
  const [isDropdownOpen, setIsDropdownOpen] = React.useState(false);
139
- const [activeInput, setActiveInput] = React.useState<
140
- 'width' | 'height' | null
141
- >(null);
67
+ const [internalWidth, setInternalWidth] = React.useState(viewWidth);
68
+ const [internalHeight, setInternalHeight] = React.useState(viewHeight);
142
69
 
143
70
  const handlePresetSelect = (dimensions: ViewDimensions) => {
144
71
  setViewWidth(dimensions.width);
145
72
  setViewHeight(dimensions.height);
146
73
  };
147
74
 
148
- const handleBlur = () => {
149
- setActiveInput(null);
150
- };
75
+ React.useEffect(() => {
76
+ setInternalWidth(viewWidth);
77
+ setInternalHeight(viewHeight);
78
+ }, [viewWidth, viewHeight]);
151
79
 
152
80
  return (
153
- <div className="relative flex h-9 w-fit overflow-hidden rounded-lg border border-slate-6 text-sm transition-colors duration-300 ease-in-out focus-within:border-slate-8 hover:border-slate-8">
154
- <DimensionInput
155
- icon={
156
- <svg
157
- xmlns="http://www.w3.org/2000/svg"
158
- width="16"
159
- height="16"
160
- viewBox="0 0 24 24"
161
- fill="none"
162
- stroke="currentColor"
163
- strokeWidth="2"
164
- strokeLinecap="round"
165
- strokeLinejoin="round"
166
- >
167
- <path d="M21 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v3" />
168
- <path d="M21 16v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3" />
169
- <path d="M4 12H2" />
170
- <path d="M10 12H8" />
171
- <path d="M16 12h-2" />
172
- <path d="M22 12h-2" />
173
- </svg>
174
- }
175
- value={viewWidth}
176
- onChange={setViewWidth}
177
- isActive={activeInput === 'width'}
178
- setIsActive={(active) => setActiveInput(active ? 'width' : null)}
179
- onBlur={handleBlur}
180
- label="Width"
181
- hasBorder
182
- />
183
- <DimensionInput
184
- icon={
185
- <svg
186
- xmlns="http://www.w3.org/2000/svg"
187
- width="16"
188
- height="16"
189
- viewBox="0 0 24 24"
190
- fill="none"
191
- stroke="currentColor"
192
- strokeWidth="2"
193
- strokeLinecap="round"
194
- strokeLinejoin="round"
195
- >
196
- <path d="M8 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h3" />
197
- <path d="M16 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3" />
198
- <path d="M12 20v2" />
199
- <path d="M12 14v2" />
200
- <path d="M12 8v2" />
201
- <path d="M12 2v2" />
202
- </svg>
203
- }
204
- value={viewHeight}
205
- onChange={setViewHeight}
206
- isActive={activeInput === 'height'}
207
- setIsActive={(active) => setActiveInput(active ? 'height' : null)}
208
- onBlur={handleBlur}
209
- label="Height"
210
- />
211
- <DropdownMenu.Root open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
212
- <DropdownMenu.Trigger asChild>
81
+ <div className="relative flex h-9 w-fit overflow-hidden rounded-lg border border-slate-6 text-sm transition-colors duration-300 ease-in-out">
82
+ {VIEW_PRESETS.map((preset) => (
83
+ <Tooltip>
84
+ <Tooltip.Trigger asChild>
85
+ <button
86
+ key={preset.name}
87
+ onClick={() => handlePresetSelect(preset.dimensions)}
88
+ className={cn(
89
+ 'relative flex items-center justify-center w-9 transition-colors hover:text-slate-12',
90
+ {
91
+ 'bg-slate-4':
92
+ viewWidth === preset.dimensions.width &&
93
+ viewHeight === preset.dimensions.height,
94
+ },
95
+ )}
96
+ type="button"
97
+ >
98
+ {preset.icon}
99
+ </button>
100
+ </Tooltip.Trigger>
101
+ <Tooltip.Content>{preset.name}</Tooltip.Content>
102
+ </Tooltip>
103
+ ))}
104
+
105
+ <Popover.Root open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
106
+ <Popover.Trigger asChild>
213
107
  <button
214
108
  type="button"
215
- className="relative flex items-center justify-center overflow-hidden bg-slate-5 p-2 text-slate-11 text-sm leading-none outline-none transition-colors ease-linear focus-within:text-slate-12 hover:text-slate-12 focus:text-slate-12"
109
+ className="relative flex items-center justify-center overflow-hidden w-9 text-slate-11 text-sm leading-none outline-none transition-colors ease-linear focus-within:text-slate-12 hover:text-slate-12 focus:text-slate-12"
216
110
  >
217
111
  <span className="sr-only">View presets</span>
218
112
  <IconArrowDown
@@ -224,24 +118,51 @@ export const ViewSizeControls = ({
224
118
  )}
225
119
  />
226
120
  </button>
227
- </DropdownMenu.Trigger>
228
- <DropdownMenu.Portal>
229
- <DropdownMenu.Content
121
+ </Popover.Trigger>
122
+ <Popover.Portal>
123
+ <Popover.Content
230
124
  align="end"
231
125
  className="flex min-w-[12rem] flex-col gap-2 rounded-md border border-slate-8 border-solid bg-black px-2 py-2 text-white"
232
126
  sideOffset={5}
233
127
  >
234
- {VIEW_PRESETS.map((preset) => (
235
- <PresetMenuItem
236
- key={preset.name}
237
- name={preset.name}
238
- dimensions={preset.dimensions}
239
- onSelect={handlePresetSelect}
128
+ <div className="flex w-full items-center justify-between text-sm gap-2">
129
+ <span className="font-medium text-slate-11 text-xs">Width</span>
130
+ <input
131
+ type="number"
132
+ value={internalWidth}
133
+ onChange={(e) => {
134
+ const value = Number(e.target.value);
135
+
136
+ setInternalWidth(value);
137
+
138
+ if (value >= minWidth) {
139
+ setViewWidth(value);
140
+ }
141
+ }}
142
+ className="w-20 appearance-none rounded-lg border border-slate-6 bg-slate-5 px-1 py-1 text-sm text-slate-12 placeholder-slate-10 outline-none transition duration-300 ease-in-out focus:ring-1 focus:ring-slate-10"
143
+ />
144
+ </div>
145
+
146
+ <div className="flex w-full items-center justify-between text-sm gap-2">
147
+ <span className="font-medium text-slate-11 text-xs">Height</span>
148
+ <input
149
+ type="number"
150
+ value={internalHeight}
151
+ onChange={(e) => {
152
+ const value = Number(e.target.value);
153
+
154
+ setInternalHeight(value);
155
+
156
+ if (value >= minHeight) {
157
+ setViewHeight(value);
158
+ }
159
+ }}
160
+ className="w-20 appearance-none rounded-lg border border-slate-6 bg-slate-5 px-1 py-1 text-sm text-slate-12 placeholder-slate-10 outline-none transition duration-300 ease-in-out focus:ring-1 focus:ring-slate-10"
240
161
  />
241
- ))}
242
- </DropdownMenu.Content>
243
- </DropdownMenu.Portal>
244
- </DropdownMenu.Root>
162
+ </div>
163
+ </Popover.Content>
164
+ </Popover.Portal>
165
+ </Popover.Root>
245
166
  </div>
246
167
  );
247
168
  };
@@ -5,7 +5,7 @@ import { pixelBasedPreset } from '@react-email/components';
5
5
  import { getTailwindConfig } from './get-tailwind-config';
6
6
 
7
7
  describe('getTailwindConfig()', () => {
8
- it("should work on the demo's Vercel Invite template", async () => {
8
+ it("works on the demo's Vercel Invite template", async () => {
9
9
  const sourcePath = path.resolve(
10
10
  __dirname,
11
11
  '../../../../../../apps/demo/emails/notifications/vercel-invite-user.tsx',
@@ -23,7 +23,7 @@ describe('getTailwindConfig()', () => {
23
23
  });
24
24
  });
25
25
 
26
- it('should work with email templates that import the tailwind config', async () => {
26
+ it('works with email templates that import the tailwind config', async () => {
27
27
  const sourcePath = path.resolve(
28
28
  __dirname,
29
29
  './tests/dummy-email-template.tsx',
@@ -5,19 +5,19 @@ import {
5
5
  import type { EmailsDirectory } from './get-emails-directory-metadata';
6
6
 
7
7
  describe('removeFilenameExtension()', () => {
8
- it('should work with a single .', () => {
8
+ it('works with a single .', () => {
9
9
  expect(removeFilenameExtension('email-template.tsx')).toBe(
10
10
  'email-template',
11
11
  );
12
12
  });
13
13
 
14
- it('should work with an example test file', () => {
14
+ it('works with an example test file', () => {
15
15
  expect(removeFilenameExtension('email-template.spec.tsx')).toBe(
16
16
  'email-template.spec',
17
17
  );
18
18
  });
19
19
 
20
- it('should do nothing when there is no extension', () => {
20
+ it('does nothing when there is no extension', () => {
21
21
  expect(removeFilenameExtension('email-template')).toBe('email-template');
22
22
  });
23
23
  });
@@ -112,11 +112,11 @@ describe('containsEmailTemplate()', () => {
112
112
  ],
113
113
  };
114
114
 
115
- it('should work with collapsed email directory', () => {
115
+ it('works with collapsed email directory', () => {
116
116
  expect(containsEmailTemplate('first/second/email', directory)).toBe(true);
117
117
  });
118
118
 
119
- it('should work with email inside a single sub directory', () => {
119
+ it('works with email inside a single sub directory', () => {
120
120
  expect(containsEmailTemplate('welcome/koala-welcome', directory)).toBe(
121
121
  true,
122
122
  );
@@ -125,7 +125,7 @@ describe('containsEmailTemplate()', () => {
125
125
  );
126
126
  });
127
127
 
128
- it('should work with email inside a second sub directory', () => {
128
+ it('works with email inside a second sub directory', () => {
129
129
  expect(
130
130
  containsEmailTemplate('magic-links/resend/verify-email', directory),
131
131
  ).toBe(true);
@@ -5,19 +5,19 @@ describe('JavaScript Email Detection', async () => {
5
5
  const testingDir = path.resolve(__dirname, 'testing');
6
6
  const emailsMetadata = await getEmailsDirectoryMetadata(testingDir, true);
7
7
 
8
- it('should detect JavaScript files with ES6 export default syntax', async () => {
8
+ it('detects JavaScript files with ES6 export default syntax', async () => {
9
9
  expect(emailsMetadata).toBeDefined();
10
10
  expect(emailsMetadata?.emailFilenames).toContain(
11
11
  'js-email-export-default.js',
12
12
  );
13
13
  });
14
14
 
15
- it('should detect JavaScript files with CommonJS module.exports', async () => {
15
+ it('detects JavaScript files with CommonJS module.exports', async () => {
16
16
  expect(emailsMetadata).toBeDefined();
17
17
  expect(emailsMetadata?.emailFilenames).toContain('js-email-test.js');
18
18
  });
19
19
 
20
- it('should detect MDX-style JavaScript files with named exports', async () => {
20
+ it('detects MDX-style JavaScript files with named exports', async () => {
21
21
  expect(emailsMetadata).toBeDefined();
22
22
  expect(emailsMetadata?.emailFilenames).toContain('mdx-email-test.js');
23
23
  });