@react-email/ui 6.8.0 → 6.9.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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +10 -9
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/preview/[...slug]/page/server-reference-manifest.json +8 -8
- package/.next/server/app/preview/[...slug]/page.js +5 -4
- package/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
- package/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/ssr/{0kci_next_dist_esm_build_templates_app-page_0o.jt0x.js → 0kci_next_dist_esm_build_templates_app-page_13jevt-.js} +3 -3
- package/.next/server/chunks/ssr/0kci_next_dist_esm_build_templates_app-page_13jevt-.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0jx5d~g._.js +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0k6754k._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0k6754k._.js.map +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__0d.h7dp._.js → [root-of-the-server]__13sdo3r._.js} +2 -2
- package/.next/server/chunks/ssr/_03liear._.js +1 -1
- package/.next/server/chunks/ssr/_03liear._.js.map +1 -1
- package/.next/server/chunks/ssr/_06p~r0t._.js +1 -1
- package/.next/server/chunks/ssr/_06p~r0t._.js.map +1 -1
- package/.next/server/chunks/ssr/_0au~b4w._.js +1 -1
- package/.next/server/chunks/ssr/_0au~b4w._.js.map +1 -1
- package/.next/server/chunks/ssr/_0bb37hq._.js +1 -1
- package/.next/server/chunks/ssr/_0bb37hq._.js.map +1 -1
- package/.next/server/chunks/ssr/_0jzzir7._.js +1 -1
- package/.next/server/chunks/ssr/_0jzzir7._.js.map +1 -1
- package/.next/server/chunks/ssr/_0o5084b._.js +5 -5
- package/.next/server/chunks/ssr/_0o5084b._.js.map +1 -1
- package/.next/server/chunks/ssr/_0z2_2b7._.js +1 -1
- package/.next/server/chunks/ssr/_0z2_2b7._.js.map +1 -1
- package/.next/server/chunks/ssr/packages_ui_src_0.ov~_x._.js +3 -0
- package/.next/server/chunks/ssr/packages_ui_src_0.ov~_x._.js.map +1 -0
- package/.next/server/chunks/ssr/packages_ui_src_11mz0xt._.js +3 -0
- package/.next/server/chunks/ssr/packages_ui_src_11mz0xt._.js.map +1 -0
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +9 -9
- package/.next/static/chunks/{0xfhv_nejv2yt.js → 0.__jzgiil6q7.js} +1 -1
- package/.next/static/chunks/00ecsosfoo~be.css +3 -0
- package/.next/static/chunks/{15~kc67eqdve8.js → 0cr8lx05fcstc.js} +1 -1
- package/.next/static/chunks/0nhnu1wkjtfwe.js +1 -0
- package/.next/static/chunks/{0m-l5t15j5bnl.js → 0ntsvdyzr-1u3.js} +1 -1
- package/.next/static/chunks/0owottqfmsjwp.js +42 -0
- package/.next/trace +1 -1
- package/.next/trace-build +1 -1
- package/.next/types/routes.d.ts +3 -1
- package/.next/types/validator.ts +9 -0
- package/package.json +2 -2
- package/src/app/preview/[...slug]/preview.tsx +126 -104
- package/src/app/preview/layout.tsx +9 -0
- package/src/components/code.tsx +2 -2
- package/src/components/icons/icon-panel-right.tsx +19 -0
- package/src/components/json-editor.tsx +71 -0
- package/src/components/preview-props-editor.tsx +101 -0
- package/src/components/preview-props-panel.tsx +31 -0
- package/src/components/toolbar/copy-for-ai.tsx +3 -3
- package/src/components/toolbar/toolbar-button.tsx +2 -2
- package/src/components/toolbar.tsx +231 -65
- package/src/components/topbar/props-panel-toggle.tsx +31 -0
- package/src/contexts/props-panel.tsx +61 -0
- package/src/hooks/use-hot-reload.ts +11 -10
- package/.next/server/chunks/ssr/0kci_next_dist_esm_build_templates_app-page_0o.jt0x.js.map +0 -1
- package/.next/static/chunks/0l15-vshddb6t.css +0 -3
- package/.next/static/chunks/0o7gbl8p~22d1.js +0 -42
- package/src/components/toolbar/preview-props-editor.tsx +0 -102
- /package/.next/server/chunks/ssr/{[root-of-the-server]__0d.h7dp._.js.map → [root-of-the-server]__13sdo3r._.js.map} +0 -0
- /package/.next/static/{RcXkYVVpqLeGe0HK28j4o → c8jFxxcwBv319fqx4lN5X}/_buildManifest.js +0 -0
- /package/.next/static/{RcXkYVVpqLeGe0HK28j4o → c8jFxxcwBv319fqx4lN5X}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{RcXkYVVpqLeGe0HK28j4o → c8jFxxcwBv319fqx4lN5X}/_ssgManifest.js +0 -0
|
@@ -16,7 +16,6 @@ type ActiveTab =
|
|
|
16
16
|
| 'linter'
|
|
17
17
|
| 'compatibility'
|
|
18
18
|
| 'spam-assassin'
|
|
19
|
-
| 'props'
|
|
20
19
|
| 'resend'
|
|
21
20
|
| undefined;
|
|
22
21
|
|
|
@@ -285,7 +284,7 @@ export const CopyForAI = ({
|
|
|
285
284
|
<button
|
|
286
285
|
type="button"
|
|
287
286
|
className={cn(
|
|
288
|
-
'flex items-center gap-1 h-7 px-2.5 rounded-md text-xs font-medium self-center',
|
|
287
|
+
'flex shrink-0 items-center gap-1 h-7 whitespace-nowrap px-2.5 rounded-md text-xs font-medium self-center',
|
|
289
288
|
'text-slate-11',
|
|
290
289
|
'hover:text-slate-12 transition-colors',
|
|
291
290
|
'outline-none',
|
|
@@ -297,9 +296,10 @@ export const CopyForAI = ({
|
|
|
297
296
|
|
|
298
297
|
<DropdownMenu.Portal>
|
|
299
298
|
<DropdownMenu.Content
|
|
300
|
-
className="
|
|
299
|
+
className="max-h-[var(--radix-dropdown-menu-content-available-height)] w-80 max-w-[calc(100vw-1rem)] overflow-y-auto rounded-xl p-1.5 shadow-2xl z-50 border border-white/10"
|
|
301
300
|
style={{ backgroundColor: '#0c0c0c' }}
|
|
302
301
|
sideOffset={8}
|
|
302
|
+
collisionPadding={8}
|
|
303
303
|
align="end"
|
|
304
304
|
side="top"
|
|
305
305
|
>
|
|
@@ -25,7 +25,7 @@ export const ToolbarButton = ({
|
|
|
25
25
|
type="button"
|
|
26
26
|
{...props}
|
|
27
27
|
className={cn(
|
|
28
|
-
'h-full w-fit font-regular flex text-sm text-slate-10 items-center align-middle justify-center px-1 gap-2 relative',
|
|
28
|
+
'h-full w-fit shrink-0 whitespace-nowrap font-regular flex text-sm text-slate-10 items-center align-middle justify-center px-1 gap-2 relative',
|
|
29
29
|
'hover:text-slate-12 transition-colors',
|
|
30
30
|
active && 'data-[state=active]:text-cyan-11',
|
|
31
31
|
className,
|
|
@@ -34,7 +34,7 @@ export const ToolbarButton = ({
|
|
|
34
34
|
{children}
|
|
35
35
|
{active ? (
|
|
36
36
|
<motion.span
|
|
37
|
-
className="
|
|
37
|
+
className="bottom-0 absolute rounded-xs left-0 w-full bg-cyan-11 h-px"
|
|
38
38
|
layoutId="active-toolbar-button"
|
|
39
39
|
transition={{
|
|
40
40
|
type: 'spring',
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
|
4
|
+
import * as Popover from '@radix-ui/react-popover';
|
|
3
5
|
import * as Tabs from '@radix-ui/react-tabs';
|
|
4
6
|
import { LayoutGroup } from 'framer-motion';
|
|
5
7
|
import { usePathname, useSearchParams } from 'next/navigation';
|
|
@@ -20,7 +22,6 @@ import { IconReload } from './icons/icon-reload';
|
|
|
20
22
|
import { Compatibility, useCompatibility } from './toolbar/compatibility';
|
|
21
23
|
import { CopyForAI } from './toolbar/copy-for-ai';
|
|
22
24
|
import { Linter, type LintingRow, useLinter } from './toolbar/linter';
|
|
23
|
-
import { PreviewPropsEditor } from './toolbar/preview-props-editor';
|
|
24
25
|
import { ResendIntegration } from './toolbar/resend';
|
|
25
26
|
import {
|
|
26
27
|
SpamAssassin,
|
|
@@ -29,19 +30,20 @@ import {
|
|
|
29
30
|
} from './toolbar/spam-assassin';
|
|
30
31
|
import { ToolbarButton } from './toolbar/toolbar-button';
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
const toolbarTabValues = [
|
|
34
|
+
'linter',
|
|
35
|
+
'compatibility',
|
|
36
|
+
'spam-assassin',
|
|
37
|
+
'resend',
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
export type ToolbarTabValue = (typeof toolbarTabValues)[number];
|
|
38
41
|
|
|
39
42
|
export const useToolbarState = () => {
|
|
40
43
|
const searchParams = useSearchParams();
|
|
41
44
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
| undefined;
|
|
45
|
+
const param = searchParams.get('toolbar-panel');
|
|
46
|
+
const activeTab = toolbarTabValues.find((value) => value === param);
|
|
45
47
|
|
|
46
48
|
return {
|
|
47
49
|
activeTab,
|
|
@@ -149,6 +151,52 @@ const ToolbarInner = ({
|
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
const id = React.useId();
|
|
154
|
+
const [isToolbarInfoOpen, setIsToolbarInfoOpen] = React.useState(false);
|
|
155
|
+
const infoPointerTypeRef = React.useRef('');
|
|
156
|
+
const infoBlurListenerRef = React.useRef<(() => void) | null>(null);
|
|
157
|
+
// Only a cleanup for when the component unmounts with the popover still
|
|
158
|
+
// open; opening/closing itself is handled directly in `onOpenChange`.
|
|
159
|
+
React.useEffect(() => {
|
|
160
|
+
return () => {
|
|
161
|
+
if (infoBlurListenerRef.current) {
|
|
162
|
+
window.removeEventListener('blur', infoBlurListenerRef.current);
|
|
163
|
+
infoBlurListenerRef.current = null;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
const toolbarPanelDescription =
|
|
169
|
+
(activeTab === 'linter' &&
|
|
170
|
+
'The Linter tab checks all the images and links for common issues like missing alt text, broken URLs, insecure HTTP methods, and more.') ||
|
|
171
|
+
(activeTab === 'spam-assassin' &&
|
|
172
|
+
'The Spam tab will look at the content and use a robust scoring framework to determine if the email is likely to be spam. Powered by SpamAssassin.') ||
|
|
173
|
+
(activeTab === 'compatibility' &&
|
|
174
|
+
'The Compatibility tab shows how well the HTML/CSS is supported across mail clients like Outlook, Gmail, etc. Powered by Can I Email.') ||
|
|
175
|
+
(activeTab === 'resend' &&
|
|
176
|
+
'The Resend tab allows you to upload your React Email code using the Resend Templates API.') ||
|
|
177
|
+
'Info';
|
|
178
|
+
|
|
179
|
+
const panelLabels: Record<ToolbarTabValue, string> = {
|
|
180
|
+
linter: 'Linter',
|
|
181
|
+
compatibility: 'Compatibility',
|
|
182
|
+
'spam-assassin': 'Spam',
|
|
183
|
+
resend: 'Resend',
|
|
184
|
+
};
|
|
185
|
+
const availablePanels: { value: ToolbarTabValue; label: string }[] = [
|
|
186
|
+
{ value: 'linter', label: panelLabels.linter },
|
|
187
|
+
...(isRawHtmlEmail
|
|
188
|
+
? []
|
|
189
|
+
: [
|
|
190
|
+
{ value: 'compatibility' as const, label: panelLabels.compatibility },
|
|
191
|
+
]),
|
|
192
|
+
{ value: 'spam-assassin', label: panelLabels['spam-assassin'] },
|
|
193
|
+
{ value: 'resend', label: panelLabels.resend },
|
|
194
|
+
];
|
|
195
|
+
// The label must reflect the URL-selected panel even when that panel is not
|
|
196
|
+
// offered for this template (e.g. compatibility on a raw HTML email), since
|
|
197
|
+
// the content area still renders that panel's state.
|
|
198
|
+
const activePanelLabel =
|
|
199
|
+
(activeTab ? panelLabels[activeTab] : undefined) ?? 'Linter';
|
|
152
200
|
|
|
153
201
|
return (
|
|
154
202
|
<div
|
|
@@ -167,39 +215,78 @@ const ToolbarInner = ({
|
|
|
167
215
|
asChild
|
|
168
216
|
>
|
|
169
217
|
<div className="flex flex-col h-full">
|
|
170
|
-
<
|
|
171
|
-
<
|
|
172
|
-
<
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
218
|
+
<div className="flex h-10 w-full shrink-0 items-center border-b border-solid border-slate-6 px-2 sm:px-4">
|
|
219
|
+
<div className="flex h-full min-w-0 flex-1 items-center sm:hidden">
|
|
220
|
+
<DropdownMenu.Root>
|
|
221
|
+
<DropdownMenu.Trigger asChild>
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
className="group flex h-full items-center gap-1 px-1 text-slate-11 text-sm transition-colors hover:text-slate-12"
|
|
225
|
+
>
|
|
226
|
+
{activePanelLabel}
|
|
227
|
+
<IconArrowDown
|
|
228
|
+
size={20}
|
|
229
|
+
className="transition-transform group-data-[state=open]:rotate-180"
|
|
230
|
+
/>
|
|
231
|
+
</button>
|
|
232
|
+
</DropdownMenu.Trigger>
|
|
233
|
+
<DropdownMenu.Portal>
|
|
234
|
+
<DropdownMenu.Content
|
|
235
|
+
align="start"
|
|
236
|
+
className="z-50 min-w-44 rounded-md border border-slate-6 bg-black p-1 font-sans"
|
|
237
|
+
collisionPadding={8}
|
|
238
|
+
side="top"
|
|
239
|
+
sideOffset={8}
|
|
240
|
+
>
|
|
241
|
+
{availablePanels.map((panel) => (
|
|
242
|
+
<DropdownMenu.Item
|
|
243
|
+
key={panel.value}
|
|
244
|
+
onSelect={() => {
|
|
245
|
+
setActivePanelValue(panel.value);
|
|
246
|
+
}}
|
|
247
|
+
className={cn(
|
|
248
|
+
'flex cursor-pointer items-center justify-between gap-2 rounded px-3 py-2 text-slate-11 text-sm outline-none',
|
|
249
|
+
'data-[highlighted]:bg-white/5 data-[highlighted]:text-slate-12',
|
|
250
|
+
activeTab === panel.value && 'text-cyan-11',
|
|
251
|
+
)}
|
|
252
|
+
>
|
|
253
|
+
{panel.label}
|
|
254
|
+
{activeTab === panel.value ? (
|
|
255
|
+
<IconCheck size={16} />
|
|
256
|
+
) : null}
|
|
257
|
+
</DropdownMenu.Item>
|
|
258
|
+
))}
|
|
259
|
+
</DropdownMenu.Content>
|
|
260
|
+
</DropdownMenu.Portal>
|
|
261
|
+
</DropdownMenu.Root>
|
|
262
|
+
</div>
|
|
263
|
+
<Tabs.List className="hidden h-full min-w-0 flex-1 gap-4 sm:flex">
|
|
264
|
+
<LayoutGroup id={`toolbar-${id}`}>
|
|
265
|
+
<Tabs.Trigger asChild value="linter">
|
|
266
|
+
<ToolbarButton active={activeTab === 'linter'}>
|
|
267
|
+
Linter
|
|
181
268
|
</ToolbarButton>
|
|
182
269
|
</Tabs.Trigger>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
<Tabs.Trigger asChild value="
|
|
191
|
-
<ToolbarButton active={activeTab === '
|
|
192
|
-
|
|
270
|
+
{isRawHtmlEmail ? null : (
|
|
271
|
+
<Tabs.Trigger asChild value="compatibility">
|
|
272
|
+
<ToolbarButton active={activeTab === 'compatibility'}>
|
|
273
|
+
Compatibility
|
|
274
|
+
</ToolbarButton>
|
|
275
|
+
</Tabs.Trigger>
|
|
276
|
+
)}
|
|
277
|
+
<Tabs.Trigger asChild value="spam-assassin">
|
|
278
|
+
<ToolbarButton active={activeTab === 'spam-assassin'}>
|
|
279
|
+
Spam
|
|
193
280
|
</ToolbarButton>
|
|
194
281
|
</Tabs.Trigger>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
</
|
|
200
|
-
</
|
|
201
|
-
</
|
|
202
|
-
<div className="flex items-center gap-1 ml-
|
|
282
|
+
<Tabs.Trigger asChild value="resend">
|
|
283
|
+
<ToolbarButton active={activeTab === 'resend'}>
|
|
284
|
+
Resend
|
|
285
|
+
</ToolbarButton>
|
|
286
|
+
</Tabs.Trigger>
|
|
287
|
+
</LayoutGroup>
|
|
288
|
+
</Tabs.List>
|
|
289
|
+
<div className="ml-2 flex shrink-0 items-center gap-1 sm:ml-4">
|
|
203
290
|
<CopyForAI
|
|
204
291
|
lintingRows={lintingRows}
|
|
205
292
|
compatibilityResults={compatibilityCheckingResults}
|
|
@@ -208,27 +295,111 @@ const ToolbarInner = ({
|
|
|
208
295
|
isRawHtmlEmail={isRawHtmlEmail}
|
|
209
296
|
activeTab={activeTab}
|
|
210
297
|
/>
|
|
211
|
-
<
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
298
|
+
<Popover.Root
|
|
299
|
+
onOpenChange={(open) => {
|
|
300
|
+
if (!open) {
|
|
301
|
+
setIsToolbarInfoOpen(false);
|
|
302
|
+
if (infoBlurListenerRef.current) {
|
|
303
|
+
window.removeEventListener(
|
|
304
|
+
'blur',
|
|
305
|
+
infoBlurListenerRef.current,
|
|
306
|
+
);
|
|
307
|
+
infoBlurListenerRef.current = null;
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
setIsToolbarInfoOpen(true);
|
|
313
|
+
if (infoBlurListenerRef.current === null) {
|
|
314
|
+
// Taps inside the email preview iframe do not reach
|
|
315
|
+
// Radix's dismiss layer, but they do blur the window.
|
|
316
|
+
const closeWhenPreviewGetsFocus = () => {
|
|
317
|
+
if (
|
|
318
|
+
!(document.activeElement instanceof HTMLIFrameElement)
|
|
319
|
+
) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
setIsToolbarInfoOpen(false);
|
|
323
|
+
if (infoBlurListenerRef.current) {
|
|
324
|
+
window.removeEventListener(
|
|
325
|
+
'blur',
|
|
326
|
+
infoBlurListenerRef.current,
|
|
327
|
+
);
|
|
328
|
+
infoBlurListenerRef.current = null;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
infoBlurListenerRef.current = closeWhenPreviewGetsFocus;
|
|
332
|
+
window.addEventListener('blur', closeWhenPreviewGetsFocus);
|
|
333
|
+
}
|
|
334
|
+
}}
|
|
335
|
+
open={isToolbarInfoOpen}
|
|
226
336
|
>
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
337
|
+
<Popover.Trigger asChild>
|
|
338
|
+
<ToolbarButton
|
|
339
|
+
aria-label="About the current toolbar panel"
|
|
340
|
+
delayDuration={0}
|
|
341
|
+
tooltip={
|
|
342
|
+
isToolbarInfoOpen ? undefined : toolbarPanelDescription
|
|
343
|
+
}
|
|
344
|
+
onPointerDown={(event) => {
|
|
345
|
+
// Read the pointer type from `pointerdown` rather than
|
|
346
|
+
// the `click` event: Safari before 18.2 dispatches
|
|
347
|
+
// `click` as a plain MouseEvent without `pointerType`.
|
|
348
|
+
infoPointerTypeRef.current = event.pointerType;
|
|
349
|
+
const clearPointerType = () => {
|
|
350
|
+
window.removeEventListener(
|
|
351
|
+
'pointerup',
|
|
352
|
+
clearPointerType,
|
|
353
|
+
true,
|
|
354
|
+
);
|
|
355
|
+
window.removeEventListener(
|
|
356
|
+
'pointercancel',
|
|
357
|
+
clearPointerType,
|
|
358
|
+
true,
|
|
359
|
+
);
|
|
360
|
+
// Deferred so the `click` for this same interaction
|
|
361
|
+
// can still read the pointer type; this also
|
|
362
|
+
// self-heals if the pointer is released off the
|
|
363
|
+
// button and no `click` ever fires.
|
|
364
|
+
setTimeout(() => {
|
|
365
|
+
infoPointerTypeRef.current = '';
|
|
366
|
+
}, 0);
|
|
367
|
+
};
|
|
368
|
+
window.addEventListener('pointerup', clearPointerType, {
|
|
369
|
+
capture: true,
|
|
370
|
+
});
|
|
371
|
+
window.addEventListener(
|
|
372
|
+
'pointercancel',
|
|
373
|
+
clearPointerType,
|
|
374
|
+
{ capture: true },
|
|
375
|
+
);
|
|
376
|
+
}}
|
|
377
|
+
onClick={(event) => {
|
|
378
|
+
// Mouse users already get this info on hover, so only
|
|
379
|
+
// touch and keyboard interactions open the popover.
|
|
380
|
+
// Keyboard activation dispatches `click` without a
|
|
381
|
+
// preceding `pointerdown`, leaving the ref empty.
|
|
382
|
+
if (infoPointerTypeRef.current === 'mouse') {
|
|
383
|
+
event.preventDefault();
|
|
384
|
+
}
|
|
385
|
+
}}
|
|
386
|
+
>
|
|
387
|
+
<IconInfo size={24} />
|
|
388
|
+
</ToolbarButton>
|
|
389
|
+
</Popover.Trigger>
|
|
390
|
+
<Popover.Portal>
|
|
391
|
+
<Popover.Content
|
|
392
|
+
align="end"
|
|
393
|
+
className="z-50 w-60 max-w-[calc(100vw-1rem)] rounded-md border border-slate-6 bg-black px-3 py-2 font-sans text-white text-xs"
|
|
394
|
+
collisionPadding={8}
|
|
395
|
+
side="top"
|
|
396
|
+
sideOffset={8}
|
|
397
|
+
>
|
|
398
|
+
{toolbarPanelDescription}
|
|
399
|
+
</Popover.Content>
|
|
400
|
+
</Popover.Portal>
|
|
401
|
+
</Popover.Root>
|
|
402
|
+
{isBuilding || activeTab === 'resend' ? null : (
|
|
232
403
|
<ToolbarButton
|
|
233
404
|
tooltip="Reload"
|
|
234
405
|
disabled={lintLoading || spamLoading || compatibilityLoading}
|
|
@@ -273,7 +444,7 @@ const ToolbarInner = ({
|
|
|
273
444
|
/>
|
|
274
445
|
</ToolbarButton>
|
|
275
446
|
</div>
|
|
276
|
-
</
|
|
447
|
+
</div>
|
|
277
448
|
|
|
278
449
|
<div className="grow transition-opacity opacity-100 group-data-[toggled=false]/toolbar:opacity-0 overflow-y-auto pr-3 pl-4 pt-3">
|
|
279
450
|
<Tabs.Content value="linter">
|
|
@@ -330,11 +501,6 @@ const ToolbarInner = ({
|
|
|
330
501
|
<SpamAssassin result={spamCheckingResult} />
|
|
331
502
|
)}
|
|
332
503
|
</Tabs.Content>
|
|
333
|
-
{isRawHtmlEmail || isBuilding ? null : (
|
|
334
|
-
<Tabs.Content className="h-full" value="props">
|
|
335
|
-
<PreviewPropsEditor />
|
|
336
|
-
</Tabs.Content>
|
|
337
|
-
)}
|
|
338
504
|
<Tabs.Content value="resend">
|
|
339
505
|
{hasSetupResendIntegration ? (
|
|
340
506
|
<ResendIntegration
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as Toggle from '@radix-ui/react-toggle';
|
|
2
|
+
import { cn } from '../../utils';
|
|
3
|
+
import { IconPanelRight } from '../icons/icon-panel-right';
|
|
4
|
+
import { Tooltip } from '../tooltip';
|
|
5
|
+
|
|
6
|
+
interface PropsPanelToggleProps {
|
|
7
|
+
open: boolean;
|
|
8
|
+
onChange: (open: boolean) => unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const PropsPanelToggle = ({ open, onChange }: PropsPanelToggleProps) => {
|
|
12
|
+
return (
|
|
13
|
+
<Tooltip>
|
|
14
|
+
<Tooltip.Trigger asChild>
|
|
15
|
+
<Toggle.Root
|
|
16
|
+
aria-label="Props panel"
|
|
17
|
+
className={cn(
|
|
18
|
+
'relative flex h-9 w-9 items-center justify-center rounded-lg border border-slate-6 text-sm transition duration-200 ease-in-out',
|
|
19
|
+
'text-slate-11 aria-pressed:bg-slate-4 hover:text-slate-12',
|
|
20
|
+
)}
|
|
21
|
+
onPressedChange={() => onChange(!open)}
|
|
22
|
+
pressed={open}
|
|
23
|
+
value="props"
|
|
24
|
+
>
|
|
25
|
+
<IconPanelRight size={20} />
|
|
26
|
+
</Toggle.Root>
|
|
27
|
+
</Tooltip.Trigger>
|
|
28
|
+
<Tooltip.Content>Show/hide the props panel</Tooltip.Content>
|
|
29
|
+
</Tooltip>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState } from 'react';
|
|
4
|
+
import { useCachedWorkspaceState } from '../hooks/use-cached-workspace-state';
|
|
5
|
+
|
|
6
|
+
const PropsPanelContext = createContext<
|
|
7
|
+
| {
|
|
8
|
+
open: boolean;
|
|
9
|
+
setOpen: (open: boolean) => void;
|
|
10
|
+
/**
|
|
11
|
+
* False until the user toggles the panel, so restoring the remembered
|
|
12
|
+
* state on load applies instantly instead of animating.
|
|
13
|
+
*/
|
|
14
|
+
animated: boolean;
|
|
15
|
+
}
|
|
16
|
+
| undefined
|
|
17
|
+
>(undefined);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Must stay mounted in the preview layout, which survives navigating between
|
|
21
|
+
* templates — otherwise the panel closes or replays its opening animation on
|
|
22
|
+
* every navigation.
|
|
23
|
+
*/
|
|
24
|
+
export const PropsPanelProvider = ({
|
|
25
|
+
children,
|
|
26
|
+
}: {
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
}) => {
|
|
29
|
+
const [cachedOpen, setCachedOpen] =
|
|
30
|
+
useCachedWorkspaceState<boolean>('props-panel-open');
|
|
31
|
+
// Writing to the workspace cache does not re-render, so toggles land in
|
|
32
|
+
// this state and the cached value only matters on load.
|
|
33
|
+
const [override, setOverride] = useState<boolean | undefined>(undefined);
|
|
34
|
+
const [animated, setAnimated] = useState(false);
|
|
35
|
+
|
|
36
|
+
const open = override ?? cachedOpen === true;
|
|
37
|
+
|
|
38
|
+
const setOpen = (newOpen: boolean) => {
|
|
39
|
+
setAnimated(true);
|
|
40
|
+
setOverride(newOpen);
|
|
41
|
+
setCachedOpen(newOpen);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<PropsPanelContext.Provider value={{ open, setOpen, animated }}>
|
|
46
|
+
{children}
|
|
47
|
+
</PropsPanelContext.Provider>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const usePropsPanel = () => {
|
|
52
|
+
const context = useContext(PropsPanelContext);
|
|
53
|
+
|
|
54
|
+
if (context === undefined) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'Cannot call `usePropsPanel` outside of a `PropsPanelProvider`.',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return context;
|
|
61
|
+
};
|
|
@@ -11,21 +11,22 @@ import type { HotReloadChange } from '../utils/types/hot-reload-change';
|
|
|
11
11
|
export const useHotreload = (
|
|
12
12
|
onShouldReload: (changes: HotReloadChange[]) => void,
|
|
13
13
|
) => {
|
|
14
|
-
const
|
|
14
|
+
const onShouldReloadRef = useRef(onShouldReload);
|
|
15
|
+
onShouldReloadRef.current = onShouldReload;
|
|
15
16
|
|
|
16
17
|
useEffect(() => {
|
|
17
|
-
|
|
18
|
-
socketRef.current = io();
|
|
19
|
-
}
|
|
20
|
-
const socket = socketRef.current;
|
|
18
|
+
const socket: Socket = io();
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
const handleReload = (changes: HotReloadChange[]) => {
|
|
23
21
|
console.debug('Reloading...');
|
|
24
|
-
void
|
|
25
|
-
}
|
|
22
|
+
void onShouldReloadRef.current(changes);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
socket.on('reload', handleReload);
|
|
26
26
|
|
|
27
27
|
return () => {
|
|
28
|
-
socket.off();
|
|
28
|
+
socket.off('reload', handleReload);
|
|
29
|
+
socket.disconnect();
|
|
29
30
|
};
|
|
30
|
-
}, [
|
|
31
|
+
}, []);
|
|
31
32
|
};
|