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.
- package/CHANGELOG.md +38 -0
- package/dist/cli/index.js +10 -10
- package/dist/cli/index.mjs +10 -13
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +19 -19
- package/dist/preview/.next/app-path-routes-manifest.json +1 -1
- package/dist/preview/.next/build-manifest.json +6 -6
- package/dist/preview/.next/cache/.rscinfo +1 -1
- package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
- package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
- package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
- package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
- package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
- package/dist/preview/.next/next-server.js.nft.json +1 -1
- package/dist/preview/.next/prerender-manifest.json +1 -1
- package/dist/preview/.next/required-server-files.json +1 -1
- package/dist/preview/.next/server/app/_not-found/page.js +1 -1
- package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
- package/dist/preview/.next/server/app/page.js +1 -1
- package/dist/preview/.next/server/app/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page.js +6 -6
- package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app-paths-manifest.json +1 -1
- package/dist/preview/.next/server/chunks/196.js +2 -2
- package/dist/preview/.next/server/chunks/590.js +1 -0
- package/dist/preview/.next/server/chunks/631.js +2 -2
- package/dist/preview/.next/server/chunks/734.js +15 -0
- package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
- package/dist/preview/.next/server/next-font-manifest.js +1 -1
- package/dist/preview/.next/server/next-font-manifest.json +1 -1
- package/dist/preview/.next/server/pages/500.html +1 -1
- package/dist/preview/.next/server/server-reference-manifest.js +1 -1
- package/dist/preview/.next/server/server-reference-manifest.json +1 -1
- package/dist/preview/.next/static/chunks/285-dbf6306a0d45c33d.js +1 -0
- package/dist/preview/.next/static/chunks/490-d26ba2019ccd4d2f.js +1 -0
- package/dist/preview/.next/static/chunks/603-36207c8905355e23.js +1 -0
- package/dist/preview/.next/static/chunks/afa401a5-9ebf2515b1397993.js +6 -0
- package/dist/preview/.next/static/chunks/app/layout-b13c19549e2d3e57.js +1 -0
- package/dist/preview/.next/static/chunks/app/page-8f366f3c14282f33.js +1 -0
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-9906dc842681db05.js +1 -0
- package/dist/preview/.next/static/chunks/main-app-d1b0aa870bcfb13e.js +1 -0
- package/dist/preview/.next/static/chunks/webpack-9255716c9496e606.js +1 -0
- package/dist/preview/.next/static/css/b60917edfd15a496.css +3 -0
- package/dist/preview/.next/trace +22 -21
- package/dist/preview/.next/types/app/layout.ts +1 -1
- package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
- package/module-punycode.d.ts +3 -0
- package/package.json +9 -9
- package/src/actions/email-validation/check-images.spec.tsx +89 -0
- package/src/actions/email-validation/check-images.ts +141 -0
- package/src/actions/email-validation/check-links.spec.tsx +91 -0
- package/src/actions/email-validation/check-links.ts +18 -15
- package/src/app/preview/[...slug]/preview.tsx +105 -19
- package/src/components/button.tsx +47 -36
- package/src/components/code-snippet.tsx +0 -2
- package/src/components/icons/icon-image.tsx +19 -0
- package/src/components/logo.tsx +0 -2
- package/src/components/resizable-wrapper.tsx +176 -0
- package/src/components/shell.tsx +17 -3
- package/src/components/sidebar/checking-results.tsx +150 -0
- package/src/components/sidebar/file-tree-directory-children.tsx +3 -6
- package/src/components/sidebar/image-checker.tsx +161 -0
- package/src/components/sidebar/link-checker.tsx +83 -223
- package/src/components/sidebar/sidebar.tsx +75 -27
- package/src/components/topbar/active-view-toggle-group.tsx +86 -0
- package/src/components/topbar/view-size-controls.tsx +247 -0
- package/src/components/topbar.tsx +50 -125
- package/src/hooks/use-clamped-state.ts +24 -0
- package/src/hooks/use-icon-animation.ts +4 -7
- package/src/utils/static-node-modules-for-vm.ts +2 -1
- package/tailwind.config.ts +12 -17
- package/tsconfig.json +6 -2
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +13 -0
- package/dist/preview/.next/server/chunks/273.js +0 -1
- package/dist/preview/.next/server/chunks/594.js +0 -10
- package/dist/preview/.next/static/chunks/18b16e15-6ad9b58e10ff8891.js +0 -1
- package/dist/preview/.next/static/chunks/490-48951f2e19ae3aef.js +0 -1
- package/dist/preview/.next/static/chunks/600-2e2ca4c8bbd97b61.js +0 -1
- package/dist/preview/.next/static/chunks/860-38d96c8819ba6f19.js +0 -1
- package/dist/preview/.next/static/chunks/app/layout-490964e2c3604d33.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-d2432acd08db8fc0.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-f4e211e00c026401.js +0 -1
- package/dist/preview/.next/static/chunks/main-app-cd104297c6bcc87e.js +0 -1
- package/dist/preview/.next/static/chunks/webpack-7bf1ffb05f5540be.js +0 -1
- package/dist/preview/.next/static/css/5e0736cafbb392a9.css +0 -3
- /package/dist/preview/.next/static/{fZaiKz58wDr55pxLu9uHa → ll_lhpCErxdDFU8uF5Ujy}/_buildManifest.js +0 -0
- /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
|
-
|
|
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
|
-
<
|
|
36
|
-
<Tooltip
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
onToggleSidebar
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 {
|
|
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<
|
|
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.
|
|
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)
|
package/tailwind.config.ts
CHANGED
|
@@ -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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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: [
|
|
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": [
|
|
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": ["
|
|
38
|
+
"exclude": [".next", "dist", "node_modules", "**/*.spec.ts", "**/*.spec.tsx"]
|
|
35
39
|
}
|