react-email 4.0.0-alpha.1 → 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 +6 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.mjs +3 -1
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +9 -9
- package/dist/preview/.next/app-path-routes-manifest.json +1 -1
- package/dist/preview/.next/build-manifest.json +2 -2
- 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/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/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 +5 -5
- 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/{282.js → 734.js} +6 -6
- 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/app/{layout-f1bad3fcfbc7eb6b.js → layout-b13c19549e2d3e57.js} +1 -1
- package/dist/preview/.next/static/chunks/app/{page-800163ba6c6d943d.js → page-8f366f3c14282f33.js} +1 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-9906dc842681db05.js +1 -0
- package/dist/preview/.next/static/css/b60917edfd15a496.css +3 -0
- package/dist/preview/.next/trace +22 -22
- package/package.json +3 -1
- package/src/actions/email-validation/check-images.spec.tsx +0 -1
- package/src/actions/email-validation/check-images.ts +0 -1
- package/src/actions/email-validation/check-links.spec.tsx +0 -1
- package/src/app/preview/[...slug]/preview.tsx +105 -19
- package/src/components/code-snippet.tsx +0 -2
- 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/sidebar.tsx +1 -1
- 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/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/667.js +0 -1
- package/dist/preview/.next/static/chunks/207-7ab46c2d84f60fed.js +0 -1
- package/dist/preview/.next/static/chunks/490-9a10c001ec2dffb2.js +0 -1
- package/dist/preview/.next/static/chunks/860-38d96c8819ba6f19.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-5b5c4557fc89db64.js +0 -1
- package/dist/preview/.next/static/css/d6c4def4cc3fb858.css +0 -3
- /package/dist/preview/.next/static/{Mn2FuRztLqr32yO8CKHi9 → ll_lhpCErxdDFU8uF5Ujy}/_buildManifest.js +0 -0
- /package/dist/preview/.next/static/{Mn2FuRztLqr32yO8CKHi9 → ll_lhpCErxdDFU8uF5Ujy}/_ssgManifest.js +0 -0
|
@@ -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
|
+
};
|
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
|
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TsconfigRaw } from 'esbuild';
|
|
2
|
+
import { defineConfig } from 'vitest/config';
|
|
3
|
+
import tsconfig from './tsconfig.test.json';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
test: {
|
|
7
|
+
globals: true,
|
|
8
|
+
environment: 'happy-dom',
|
|
9
|
+
},
|
|
10
|
+
esbuild: {
|
|
11
|
+
tsconfigRaw: tsconfig as TsconfigRaw,
|
|
12
|
+
},
|
|
13
|
+
});
|