react-email 4.0.0-alpha.0 → 4.0.0-alpha.2

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 (92) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/cli/index.js +10 -10
  3. package/dist/cli/index.mjs +10 -13
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +19 -19
  6. package/dist/preview/.next/app-path-routes-manifest.json +1 -1
  7. package/dist/preview/.next/build-manifest.json +6 -6
  8. package/dist/preview/.next/cache/.rscinfo +1 -1
  9. package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
  10. package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
  11. package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
  12. package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
  13. package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
  14. package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
  15. package/dist/preview/.next/next-server.js.nft.json +1 -1
  16. package/dist/preview/.next/prerender-manifest.json +1 -1
  17. package/dist/preview/.next/required-server-files.json +1 -1
  18. package/dist/preview/.next/server/app/_not-found/page.js +1 -1
  19. package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
  21. package/dist/preview/.next/server/app/page.js +1 -1
  22. package/dist/preview/.next/server/app/page.js.nft.json +1 -1
  23. package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
  24. package/dist/preview/.next/server/app/preview/[...slug]/page.js +6 -6
  25. package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  26. package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  27. package/dist/preview/.next/server/app-paths-manifest.json +1 -1
  28. package/dist/preview/.next/server/chunks/196.js +2 -2
  29. package/dist/preview/.next/server/chunks/590.js +1 -0
  30. package/dist/preview/.next/server/chunks/631.js +2 -2
  31. package/dist/preview/.next/server/chunks/734.js +15 -0
  32. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  33. package/dist/preview/.next/server/next-font-manifest.js +1 -1
  34. package/dist/preview/.next/server/next-font-manifest.json +1 -1
  35. package/dist/preview/.next/server/pages/500.html +1 -1
  36. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  37. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  38. package/dist/preview/.next/static/chunks/285-dbf6306a0d45c33d.js +1 -0
  39. package/dist/preview/.next/static/chunks/490-d26ba2019ccd4d2f.js +1 -0
  40. package/dist/preview/.next/static/chunks/603-36207c8905355e23.js +1 -0
  41. package/dist/preview/.next/static/chunks/afa401a5-9ebf2515b1397993.js +6 -0
  42. package/dist/preview/.next/static/chunks/app/layout-b13c19549e2d3e57.js +1 -0
  43. package/dist/preview/.next/static/chunks/app/page-8f366f3c14282f33.js +1 -0
  44. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-9906dc842681db05.js +1 -0
  45. package/dist/preview/.next/static/chunks/main-app-d1b0aa870bcfb13e.js +1 -0
  46. package/dist/preview/.next/static/chunks/webpack-9255716c9496e606.js +1 -0
  47. package/dist/preview/.next/static/css/b60917edfd15a496.css +3 -0
  48. package/dist/preview/.next/trace +22 -21
  49. package/dist/preview/.next/types/app/layout.ts +1 -1
  50. package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
  51. package/module-punycode.d.ts +3 -0
  52. package/package.json +9 -9
  53. package/src/actions/email-validation/check-images.spec.tsx +89 -0
  54. package/src/actions/email-validation/check-images.ts +141 -0
  55. package/src/actions/email-validation/check-links.spec.tsx +91 -0
  56. package/src/actions/email-validation/check-links.ts +18 -15
  57. package/src/app/preview/[...slug]/preview.tsx +105 -19
  58. package/src/components/button.tsx +47 -36
  59. package/src/components/code-snippet.tsx +0 -2
  60. package/src/components/icons/icon-image.tsx +19 -0
  61. package/src/components/logo.tsx +0 -2
  62. package/src/components/resizable-wrapper.tsx +176 -0
  63. package/src/components/shell.tsx +17 -3
  64. package/src/components/sidebar/checking-results.tsx +150 -0
  65. package/src/components/sidebar/file-tree-directory-children.tsx +3 -6
  66. package/src/components/sidebar/image-checker.tsx +161 -0
  67. package/src/components/sidebar/link-checker.tsx +83 -223
  68. package/src/components/sidebar/sidebar.tsx +75 -27
  69. package/src/components/topbar/active-view-toggle-group.tsx +86 -0
  70. package/src/components/topbar/view-size-controls.tsx +247 -0
  71. package/src/components/topbar.tsx +50 -125
  72. package/src/hooks/use-clamped-state.ts +24 -0
  73. package/src/hooks/use-icon-animation.ts +4 -7
  74. package/src/utils/static-node-modules-for-vm.ts +2 -1
  75. package/tailwind.config.ts +12 -17
  76. package/tsconfig.json +6 -2
  77. package/tsconfig.test.json +8 -0
  78. package/vitest.config.ts +13 -0
  79. package/dist/preview/.next/server/chunks/273.js +0 -1
  80. package/dist/preview/.next/server/chunks/594.js +0 -10
  81. package/dist/preview/.next/static/chunks/18b16e15-6ad9b58e10ff8891.js +0 -1
  82. package/dist/preview/.next/static/chunks/490-48951f2e19ae3aef.js +0 -1
  83. package/dist/preview/.next/static/chunks/600-2e2ca4c8bbd97b61.js +0 -1
  84. package/dist/preview/.next/static/chunks/860-38d96c8819ba6f19.js +0 -1
  85. package/dist/preview/.next/static/chunks/app/layout-490964e2c3604d33.js +0 -1
  86. package/dist/preview/.next/static/chunks/app/page-d2432acd08db8fc0.js +0 -1
  87. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-f4e211e00c026401.js +0 -1
  88. package/dist/preview/.next/static/chunks/main-app-cd104297c6bcc87e.js +0 -1
  89. package/dist/preview/.next/static/chunks/webpack-7bf1ffb05f5540be.js +0 -1
  90. package/dist/preview/.next/static/css/5e0736cafbb392a9.css +0 -3
  91. /package/dist/preview/.next/static/{fZaiKz58wDr55pxLu9uHa → ll_lhpCErxdDFU8uF5Ujy}/_buildManifest.js +0 -0
  92. /package/dist/preview/.next/static/{fZaiKz58wDr55pxLu9uHa → ll_lhpCErxdDFU8uF5Ujy}/_ssgManifest.js +0 -0
@@ -0,0 +1,86 @@
1
+ import * as ToggleGroup from '@radix-ui/react-toggle-group';
2
+ import { motion } from 'framer-motion';
3
+ import { cn } from '../../utils';
4
+ import { tabTransition } from '../../utils/constants';
5
+ import { IconMonitor } from '../icons/icon-monitor';
6
+ import { IconSource } from '../icons/icon-source';
7
+ import { Tooltip } from '../tooltip';
8
+
9
+ interface ActiveViewToggleGroupProps {
10
+ activeView: string;
11
+ setActiveView: (view: string) => void;
12
+ }
13
+
14
+ export const ActiveViewToggleGroup = ({
15
+ activeView,
16
+ setActiveView,
17
+ }: ActiveViewToggleGroupProps) => {
18
+ return (
19
+ <ToggleGroup.Root
20
+ aria-label="View mode"
21
+ className="inline-block items-center bg-slate-2 border border-slate-6 rounded-md overflow-hidden h-[36px]"
22
+ onValueChange={(value) => {
23
+ if (value) setActiveView(value);
24
+ }}
25
+ type="single"
26
+ value={activeView}
27
+ >
28
+ <ToggleGroup.Item value="preview">
29
+ <Tooltip>
30
+ <Tooltip.Trigger asChild>
31
+ <div
32
+ className={cn(
33
+ 'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
34
+ {
35
+ 'text-slate-11': activeView !== 'desktop',
36
+ 'text-slate-12': activeView === 'desktop',
37
+ },
38
+ )}
39
+ >
40
+ {activeView === 'preview' && (
41
+ <motion.span
42
+ animate={{ opacity: 1 }}
43
+ className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
44
+ exit={{ opacity: 0 }}
45
+ initial={{ opacity: 0 }}
46
+ layoutId="topbar-tabs"
47
+ transition={tabTransition}
48
+ />
49
+ )}
50
+ <IconMonitor />
51
+ </div>
52
+ </Tooltip.Trigger>
53
+ <Tooltip.Content>Preview</Tooltip.Content>
54
+ </Tooltip>
55
+ </ToggleGroup.Item>
56
+ <ToggleGroup.Item value="source">
57
+ <Tooltip>
58
+ <Tooltip.Trigger asChild>
59
+ <div
60
+ className={cn(
61
+ 'px-3 py-2 transition ease-in-out duration-200 relative hover:text-slate-12',
62
+ {
63
+ 'text-slate-11': activeView !== 'source',
64
+ 'text-slate-12': activeView === 'source',
65
+ },
66
+ )}
67
+ >
68
+ {activeView === 'source' && (
69
+ <motion.span
70
+ animate={{ opacity: 1 }}
71
+ className="absolute left-0 right-0 top-0 bottom-0 bg-slate-4"
72
+ exit={{ opacity: 0 }}
73
+ initial={{ opacity: 0 }}
74
+ layoutId="topbar-tabs"
75
+ transition={tabTransition}
76
+ />
77
+ )}
78
+ <IconSource />
79
+ </div>
80
+ </Tooltip.Trigger>
81
+ <Tooltip.Content>Code</Tooltip.Content>
82
+ </Tooltip>
83
+ </ToggleGroup.Item>
84
+ </ToggleGroup.Root>
85
+ );
86
+ };
@@ -0,0 +1,247 @@
1
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
2
+ import { motion } from 'framer-motion';
3
+ import * as React from 'react';
4
+ import { cn } from '../../utils';
5
+ import { IconArrowDown } from '../icons/icon-arrow-down';
6
+ import { Tooltip } from '../tooltip';
7
+
8
+ interface ViewDimensions {
9
+ width: number;
10
+ height: number;
11
+ }
12
+
13
+ interface ViewSizeControlsProps {
14
+ viewWidth: number;
15
+ setViewWidth: (width: number) => void;
16
+ viewHeight: number;
17
+ setViewHeight: (height: number) => void;
18
+ }
19
+
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
+ interface PresetOption {
32
+ name: string;
33
+ dimensions: ViewDimensions;
34
+ }
35
+
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',
51
+ },
52
+ inactive: {
53
+ width: '0',
54
+ },
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-1.5 py-0.5 font-bold text-white text-xs">
127
+ {dimensions.width}x{dimensions.height}
128
+ </span>
129
+ </DropdownMenu.Item>
130
+ );
131
+
132
+ export const ViewSizeControls = ({
133
+ viewWidth,
134
+ viewHeight,
135
+ setViewWidth,
136
+ setViewHeight,
137
+ }: ViewSizeControlsProps) => {
138
+ const [isDropdownOpen, setIsDropdownOpen] = React.useState(false);
139
+ const [activeInput, setActiveInput] = React.useState<
140
+ 'width' | 'height' | null
141
+ >(null);
142
+
143
+ const handlePresetSelect = (dimensions: ViewDimensions) => {
144
+ setViewWidth(dimensions.width);
145
+ setViewHeight(dimensions.height);
146
+ };
147
+
148
+ const handleBlur = () => {
149
+ setActiveInput(null);
150
+ };
151
+
152
+ 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>
213
+ <button
214
+ 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"
216
+ >
217
+ <span className="sr-only">View presets</span>
218
+ <IconArrowDown
219
+ className={cn(
220
+ 'transform transition-transform duration-200 ease-[cubic-bezier(.36,.66,.6,1)]',
221
+ {
222
+ '-rotate-180': isDropdownOpen,
223
+ },
224
+ )}
225
+ />
226
+ </button>
227
+ </DropdownMenu.Trigger>
228
+ <DropdownMenu.Portal>
229
+ <DropdownMenu.Content
230
+ align="end"
231
+ 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
+ sideOffset={5}
233
+ >
234
+ {VIEW_PRESETS.map((preset) => (
235
+ <PresetMenuItem
236
+ key={preset.name}
237
+ name={preset.name}
238
+ dimensions={preset.dimensions}
239
+ onSelect={handlePresetSelect}
240
+ />
241
+ ))}
242
+ </DropdownMenu.Content>
243
+ </DropdownMenu.Portal>
244
+ </DropdownMenu.Root>
245
+ </div>
246
+ );
247
+ };
@@ -1,24 +1,23 @@
1
1
  'use client';
2
- import * as ToggleGroup from '@radix-ui/react-toggle-group';
3
- import { motion } from 'framer-motion';
4
- import type * as React from 'react';
5
- import { cn } from '../utils';
6
- import { tabTransition } from '../utils/constants';
2
+
7
3
  import { Heading } from './heading';
8
4
  import { IconHideSidebar } from './icons/icon-hide-sidebar';
9
- import { IconMonitor } from './icons/icon-monitor';
10
- import { IconPhone } from './icons/icon-phone';
11
- import { IconSource } from './icons/icon-source';
12
5
  import { Send } from './send';
13
6
  import { Tooltip } from './tooltip';
7
+ import { ActiveViewToggleGroup } from './topbar/active-view-toggle-group';
8
+ import { ViewSizeControls } from './topbar/view-size-controls';
14
9
 
15
10
  interface TopbarProps {
16
11
  currentEmailOpenSlug: string;
17
12
  pathSeparator: string;
18
- activeView?: string;
19
13
  markup?: string;
20
14
  onToggleSidebar?: () => void;
15
+ activeView?: string;
21
16
  setActiveView?: (view: string) => void;
17
+ viewWidth?: number;
18
+ setViewWidth?: (width: number) => void;
19
+ viewHeight?: number;
20
+ setViewHeight?: (height: number) => void;
22
21
  }
23
22
 
24
23
  export const Topbar = ({
@@ -27,127 +26,53 @@ export const Topbar = ({
27
26
  markup,
28
27
  activeView,
29
28
  setActiveView,
29
+ viewWidth,
30
+ setViewWidth,
31
+ viewHeight,
32
+ setViewHeight,
30
33
  onToggleSidebar,
31
34
  }: TopbarProps) => {
32
35
  return (
33
36
  <Tooltip.Provider>
34
- <header className="relative flex h-[3.3125rem] items-center justify-between border-slate-6 border-b px-3">
35
- <Tooltip>
36
- <Tooltip.Trigger asChild>
37
- <button
38
- className="relative hidden rounded-lg px-2 py-2 text-slate-11 transition duration-200 ease-in-out hover:bg-slate-5 hover:text-slate-12 lg:flex"
39
- onClick={() => {
40
- if (onToggleSidebar) {
41
- onToggleSidebar();
42
- }
43
- }}
44
- type="button"
45
- >
46
- <IconHideSidebar height={20} width={20} />
47
- </button>
48
- </Tooltip.Trigger>
49
- <Tooltip.Content>Show/hide sidebar</Tooltip.Content>
50
- </Tooltip>
51
- <div className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 hidden transform items-center overflow-hidden text-center lg:flex">
52
- <Heading as="h2" className="truncate" size="2" weight="medium">
53
- {currentEmailOpenSlug.split(pathSeparator).pop()}
54
- </Heading>
37
+ <header className="relative flex h-[3.3125rem] items-center justify-between gap-3 border-slate-6 border-b px-3">
38
+ <div className="relative flex w-fit items-center gap-3">
39
+ <Tooltip>
40
+ <Tooltip.Trigger asChild>
41
+ <button
42
+ className="relative hidden rounded-lg px-2 py-2 text-slate-11 transition duration-200 ease-in-out hover:bg-slate-5 hover:text-slate-12 lg:flex"
43
+ onClick={() => {
44
+ if (onToggleSidebar) {
45
+ onToggleSidebar();
46
+ }
47
+ }}
48
+ type="button"
49
+ >
50
+ <IconHideSidebar height={20} width={20} />
51
+ </button>
52
+ </Tooltip.Trigger>
53
+ <Tooltip.Content>Show/hide sidebar</Tooltip.Content>
54
+ </Tooltip>
55
+ <div className="hidden items-center overflow-hidden text-center lg:flex">
56
+ <Heading as="h2" className="truncate" size="2" weight="medium">
57
+ {currentEmailOpenSlug.split(pathSeparator).pop()}
58
+ </Heading>
59
+ </div>
55
60
  </div>
56
- <div className="flex w-full justify-between gap-3 lg:w-fit lg:justify-start">
57
- <ToggleGroup.Root
58
- aria-label="View mode"
59
- className="inline-block h-[36px] items-center overflow-hidden rounded-md border border-slate-6 bg-slate-2"
60
- onValueChange={(value) => {
61
- if (value) setActiveView?.(value);
62
- }}
63
- type="single"
64
- value={activeView}
65
- >
66
- <ToggleGroup.Item value="desktop">
67
- <Tooltip>
68
- <Tooltip.Trigger asChild>
69
- <div
70
- className={cn(
71
- 'relative px-3 py-2 transition duration-200 ease-in-out hover:text-slate-12',
72
- {
73
- 'text-slate-11': activeView !== 'desktop',
74
- 'text-slate-12': activeView === 'desktop',
75
- },
76
- )}
77
- >
78
- {activeView === 'desktop' && (
79
- <motion.span
80
- animate={{ opacity: 1 }}
81
- className="absolute top-0 right-0 bottom-0 left-0 bg-slate-4"
82
- exit={{ opacity: 0 }}
83
- initial={{ opacity: 0 }}
84
- layoutId="topbar-tabs"
85
- transition={tabTransition}
86
- />
87
- )}
88
- <IconMonitor />
89
- </div>
90
- </Tooltip.Trigger>
91
- <Tooltip.Content>Desktop</Tooltip.Content>
92
- </Tooltip>
93
- </ToggleGroup.Item>
94
- <ToggleGroup.Item value="mobile">
95
- <Tooltip>
96
- <Tooltip.Trigger asChild>
97
- <div
98
- className={cn(
99
- 'relative px-3 py-2 transition duration-200 ease-in-out hover:text-slate-12',
100
- {
101
- 'text-slate-11': activeView !== 'mobile',
102
- 'text-slate-12': activeView === 'mobile',
103
- },
104
- )}
105
- >
106
- {activeView === 'mobile' && (
107
- <motion.span
108
- animate={{ opacity: 1 }}
109
- className="absolute top-0 right-0 bottom-0 left-0 bg-slate-4"
110
- exit={{ opacity: 0 }}
111
- initial={{ opacity: 0 }}
112
- layoutId="topbar-tabs"
113
- transition={tabTransition}
114
- />
115
- )}
116
- <IconPhone />
117
- </div>
118
- </Tooltip.Trigger>
119
- <Tooltip.Content>Mobile</Tooltip.Content>
120
- </Tooltip>
121
- </ToggleGroup.Item>
122
- <ToggleGroup.Item value="source">
123
- <Tooltip>
124
- <Tooltip.Trigger asChild>
125
- <div
126
- className={cn(
127
- 'relative px-3 py-2 transition duration-200 ease-in-out hover:text-slate-12',
128
- {
129
- 'text-slate-11': activeView !== 'source',
130
- 'text-slate-12': activeView === 'source',
131
- },
132
- )}
133
- >
134
- {activeView === 'source' && (
135
- <motion.span
136
- animate={{ opacity: 1 }}
137
- className="absolute top-0 right-0 bottom-0 left-0 bg-slate-4"
138
- exit={{ opacity: 0 }}
139
- initial={{ opacity: 0 }}
140
- layoutId="topbar-tabs"
141
- transition={tabTransition}
142
- />
143
- )}
144
- <IconSource />
145
- </div>
146
- </Tooltip.Trigger>
147
- <Tooltip.Content>Code</Tooltip.Content>
148
- </Tooltip>
149
- </ToggleGroup.Item>
150
- </ToggleGroup.Root>
61
+ <div className="flex w-full items-center justify-between gap-3 lg:w-fit lg:justify-start">
62
+ {setViewWidth && setViewHeight && viewWidth && viewHeight ? (
63
+ <ViewSizeControls
64
+ setViewHeight={setViewHeight}
65
+ setViewWidth={setViewWidth}
66
+ viewHeight={viewHeight}
67
+ viewWidth={viewWidth}
68
+ />
69
+ ) : null}
70
+ {activeView && setActiveView ? (
71
+ <ActiveViewToggleGroup
72
+ activeView={activeView}
73
+ setActiveView={setActiveView}
74
+ />
75
+ ) : null}
151
76
  {markup ? (
152
77
  <div className="flex justify-end">
153
78
  <Send markup={markup} />
@@ -0,0 +1,24 @@
1
+ import { useState } from 'react';
2
+
3
+ const clamp = (v: number, min: number, max: number) => {
4
+ return Math.min(Math.max(v, min), max);
5
+ };
6
+
7
+ export const useClampedState = (initial: number, min: number, max: number) => {
8
+ const [v, setV] = useState(initial);
9
+
10
+ return [
11
+ clamp(v, min, max),
12
+ (valueOrFunction: number | ((v: number) => number)) => {
13
+ if (typeof valueOrFunction === 'function') {
14
+ setV((value: number) => {
15
+ const currentValue = clamp(value, min, max);
16
+
17
+ return clamp(valueOrFunction(currentValue), min, max);
18
+ });
19
+ } else {
20
+ setV(clamp(valueOrFunction, min, max));
21
+ }
22
+ },
23
+ ] as const;
24
+ };
@@ -1,11 +1,11 @@
1
- import type { LottieRefCurrentProps } from 'lottie-react';
1
+ import type { DotLottie } from '@lottiefiles/dotlottie-react';
2
2
  import * as React from 'react';
3
3
 
4
4
  const TIMEOUT = 150;
5
5
  const THRESHOLD_ANIMATION = 0.9;
6
6
 
7
7
  export const useIconAnimation = () => {
8
- const ref = React.useRef<LottieRefCurrentProps>(null);
8
+ const ref = React.useRef<DotLottie>(null);
9
9
  const timer = React.useRef<NodeJS.Timeout | null>(null);
10
10
 
11
11
  const onMouseLeave = React.useCallback(() => {
@@ -17,10 +17,8 @@ export const useIconAnimation = () => {
17
17
  return;
18
18
  }
19
19
 
20
- const total = Math.round(ref.current.animationItem?.totalFrames ?? 0);
21
- const current = Math.round(
22
- (ref.current.animationItem?.currentFrame ?? 0) + 1,
23
- );
20
+ const total = Math.round(ref.current.totalFrames ?? 0);
21
+ const current = Math.round((ref.current.currentFrame ?? 0) + 1);
24
22
 
25
23
  if (current === 1 || current >= total * THRESHOLD_ANIMATION) {
26
24
  timer.current = setTimeout(() => {
@@ -29,7 +27,6 @@ export const useIconAnimation = () => {
29
27
  }
30
28
 
31
29
  ref.current.stop();
32
- ref.current.setDirection(1);
33
30
  ref.current.setSpeed(1);
34
31
  ref.current.play();
35
32
  }, TIMEOUT);
@@ -23,7 +23,6 @@ import os from 'node:os';
23
23
  import path from 'node:path';
24
24
  import perfHooks from 'node:perf_hooks';
25
25
  import process from 'node:process';
26
- import punycode from 'node:punycode';
27
26
  import querystring from 'node:querystring';
28
27
  import readline from 'node:readline';
29
28
  import repl from 'node:repl';
@@ -40,6 +39,8 @@ import v8 from 'node:v8';
40
39
  import vm from 'node:vm';
41
40
  import workerThreads from 'node:worker_threads';
42
41
  import zlib from 'node:zlib';
42
+ // See https://github.com/resend/react-email/issues/1841#issuecomment-2589985562
43
+ import punycode from 'module-punycode';
43
44
 
44
45
  /**
45
46
  * A map of the name of the modules (including `node:` prefixed ones)
@@ -3,25 +3,20 @@ import colors = require('@radix-ui/colors');
3
3
  import { fontFamily } from 'tailwindcss/defaultTheme';
4
4
  import plugin from 'tailwindcss/plugin';
5
5
 
6
- const iOsHeight = plugin(({ addUtilities }) => {
7
- const supportsTouchRule = '@supports (-webkit-touch-callout: none)';
8
- const webkitFillAvailable = '-webkit-fill-available';
9
-
10
- const utilities = {
11
- '.min-h-screen-ios': {
12
- [supportsTouchRule]: {
13
- minHeight: webkitFillAvailable,
6
+ const numberInputArrowHide = plugin(({ addUtilities }) => {
7
+ addUtilities({
8
+ '.arrow-hide': {
9
+ appearance: 'textfield',
10
+ '&::-webkit-inner-spin-button': {
11
+ appearance: 'none',
12
+ margin: '0px',
14
13
  },
15
- },
16
- '.h-screen-ios': {
17
- [supportsTouchRule]: {
18
- height: webkitFillAvailable,
14
+ '&::-webkit-outer-spin-button': {
15
+ appearance: 'none',
16
+ margin: '0px',
19
17
  },
20
18
  },
21
- };
22
-
23
- // @ts-expect-error This works normally, not sure what this error is
24
- addUtilities(utilities, ['responsive']);
19
+ });
25
20
  });
26
21
 
27
22
  const config: Config = {
@@ -89,6 +84,6 @@ const config: Config = {
89
84
  },
90
85
  },
91
86
  },
92
- plugins: [iOsHeight],
87
+ plugins: [numberInputArrowHide],
93
88
  };
94
89
  export default config;
package/tsconfig.json CHANGED
@@ -14,7 +14,11 @@
14
14
  "preserveWatchOutput": true,
15
15
  "skipLibCheck": true,
16
16
  "strictNullChecks": true,
17
- "plugins": [{ "name": "next" }],
17
+ "plugins": [
18
+ {
19
+ "name": "next"
20
+ }
21
+ ],
18
22
  "allowJs": true,
19
23
  "declaration": false,
20
24
  "declarationMap": false,
@@ -31,5 +35,5 @@
31
35
  "outDir": "dist"
32
36
  },
33
37
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
34
- "exclude": ["tsconfig.export.json", ".next", "dist", "node_modules"]
38
+ "exclude": [".next", "dist", "node_modules", "**/*.spec.ts", "**/*.spec.tsx"]
35
39
  }