@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
package/.next/trace
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
[{"name":"generate-buildid","duration":
|
|
1
|
+
[{"name":"generate-buildid","duration":270,"timestamp":258208200,"id":4,"parentId":1,"tags":{},"startTime":1784148191305,"traceId":"eaf7a751603db501"},{"name":"load-custom-routes","duration":353,"timestamp":258208592,"id":5,"parentId":1,"tags":{},"startTime":1784148191305,"traceId":"eaf7a751603db501"},{"name":"create-dist-dir","duration":859,"timestamp":258208975,"id":6,"parentId":1,"tags":{},"startTime":1784148191305,"traceId":"eaf7a751603db501"},{"name":"clean","duration":286,"timestamp":258210389,"id":7,"parentId":1,"tags":{},"startTime":1784148191307,"traceId":"eaf7a751603db501"},{"name":"discover-routes","duration":4422,"timestamp":258229762,"id":8,"parentId":1,"tags":{},"startTime":1784148191326,"traceId":"eaf7a751603db501"},{"name":"create-root-mapping","duration":49,"timestamp":258234223,"id":9,"parentId":1,"tags":{},"startTime":1784148191331,"traceId":"eaf7a751603db501"},{"name":"generate-route-types","duration":11878,"timestamp":258235806,"id":10,"parentId":1,"tags":{},"startTime":1784148191332,"traceId":"eaf7a751603db501"},{"name":"public-dir-conflict-check","duration":86,"timestamp":258247732,"id":11,"parentId":1,"tags":{},"startTime":1784148191344,"traceId":"eaf7a751603db501"},{"name":"generate-routes-manifest","duration":1932,"timestamp":258247906,"id":12,"parentId":1,"tags":{},"startTime":1784148191344,"traceId":"eaf7a751603db501"},{"name":"run-turbopack","duration":10172916,"timestamp":258253358,"id":14,"parentId":1,"tags":{},"startTime":1784148191350,"traceId":"eaf7a751603db501"},{"name":"turbopack-build-events","duration":66,"timestamp":258503061,"id":15,"parentId":1,"tags":{},"startTime":1784148191599,"traceId":"eaf7a751603db501"},{"name":"run-typescript","duration":7161506,"timestamp":268429324,"id":16,"parentId":1,"tags":{},"startTime":1784148201526,"traceId":"eaf7a751603db501"},{"name":"generate-required-server-files","duration":1008,"timestamp":275591420,"id":18,"parentId":1,"tags":{},"startTime":1784148208688,"traceId":"eaf7a751603db501"},{"name":"check-static-error-page","duration":7164,"timestamp":275618464,"id":20,"parentId":19,"tags":{},"startTime":1784148208715,"traceId":"eaf7a751603db501"},{"name":"is-page-static","duration":242700,"timestamp":275629594,"id":27,"parentId":22,"tags":{},"startTime":1784148208726,"traceId":"eaf7a751603db501"},{"name":"check-page","duration":248962,"timestamp":275623414,"id":22,"parentId":19,"tags":{"page":"/_global-error"},"startTime":1784148208720,"traceId":"eaf7a751603db501"},{"name":"is-page-static","duration":305982,"timestamp":275636131,"id":29,"parentId":24,"tags":{},"startTime":1784148208733,"traceId":"eaf7a751603db501"},{"name":"check-page","duration":318530,"timestamp":275623650,"id":24,"parentId":19,"tags":{"page":"/"},"startTime":1784148208720,"traceId":"eaf7a751603db501"},{"name":"is-page-static","duration":340523,"timestamp":275629333,"id":26,"parentId":21,"tags":{},"startTime":1784148208726,"traceId":"eaf7a751603db501"},{"name":"check-page","duration":350588,"timestamp":275619331,"id":21,"parentId":19,"tags":{"page":"/_not-found"},"startTime":1784148208716,"traceId":"eaf7a751603db501"},{"name":"is-page-static","duration":364896,"timestamp":275629652,"id":28,"parentId":23,"tags":{},"startTime":1784148208726,"traceId":"eaf7a751603db501"},{"name":"check-page","duration":375497,"timestamp":275623568,"id":23,"parentId":19,"tags":{"page":"/favicon.ico"},"startTime":1784148208720,"traceId":"eaf7a751603db501"},{"name":"is-page-static","duration":609740,"timestamp":275636190,"id":30,"parentId":25,"tags":{},"startTime":1784148208733,"traceId":"eaf7a751603db501"},{"name":"check-page","duration":622294,"timestamp":275623700,"id":25,"parentId":19,"tags":{"page":"/preview/[...slug]"},"startTime":1784148208720,"traceId":"eaf7a751603db501"},{"name":"static-check","duration":628364,"timestamp":275617670,"id":19,"parentId":1,"tags":{},"startTime":1784148208714,"traceId":"eaf7a751603db501"},{"name":"write-routes-manifest","duration":233,"timestamp":276246587,"id":32,"parentId":1,"tags":{},"startTime":1784148209343,"traceId":"eaf7a751603db501"},{"name":"load-dotenv","duration":27,"timestamp":276253142,"id":35,"parentId":34,"tags":{},"startTime":1784148209350,"traceId":"eaf7a751603db501"},{"name":"run-export-path-map","duration":261,"timestamp":276254701,"id":36,"parentId":34,"tags":{},"startTime":1784148209351,"traceId":"eaf7a751603db501"},{"name":"next-export","duration":108669,"timestamp":276252290,"id":34,"parentId":1,"tags":{},"startTime":1784148209349,"traceId":"eaf7a751603db501"},{"name":"move-exported-app-global-error-","duration":398,"timestamp":276361940,"id":37,"parentId":33,"tags":{},"startTime":1784148209458,"traceId":"eaf7a751603db501"},{"name":"static-generation","duration":115151,"timestamp":276248292,"id":33,"parentId":1,"tags":{},"startTime":1784148209345,"traceId":"eaf7a751603db501"},{"name":"write-routes-manifest","duration":201,"timestamp":276363554,"id":38,"parentId":1,"tags":{},"startTime":1784148209460,"traceId":"eaf7a751603db501"},{"name":"print-tree-view","duration":2251,"timestamp":276367689,"id":39,"parentId":1,"tags":{},"startTime":1784148209464,"traceId":"eaf7a751603db501"},{"name":"write-route-bundle-stats","duration":5341,"timestamp":276369954,"id":40,"parentId":1,"tags":{},"startTime":1784148209466,"traceId":"eaf7a751603db501"},{"name":"telemetry-flush","duration":490023,"timestamp":276375313,"id":41,"parentId":1,"tags":{},"startTime":1784148209472,"traceId":"eaf7a751603db501"},{"name":"next-build","duration":18713954,"timestamp":258151412,"id":1,"tags":{"buildMode":"default","version":"16.2.6","bundler":"turbopack","has-custom-webpack-config":"false","use-build-worker":"true"},"startTime":1784148191248,"traceId":"eaf7a751603db501"}]
|
package/.next/trace-build
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
[{"name":"run-turbopack","duration":
|
|
1
|
+
[{"name":"run-turbopack","duration":10172916,"timestamp":258253358,"id":14,"parentId":1,"tags":{},"startTime":1784148191350,"traceId":"eaf7a751603db501"},{"name":"turbopack-build-events","duration":66,"timestamp":258503061,"id":15,"parentId":1,"tags":{},"startTime":1784148191599,"traceId":"eaf7a751603db501"},{"name":"run-typescript","duration":7161506,"timestamp":268429324,"id":16,"parentId":1,"tags":{},"startTime":1784148201526,"traceId":"eaf7a751603db501"},{"name":"static-check","duration":628364,"timestamp":275617670,"id":19,"parentId":1,"tags":{},"startTime":1784148208714,"traceId":"eaf7a751603db501"},{"name":"static-generation","duration":115151,"timestamp":276248292,"id":33,"parentId":1,"tags":{},"startTime":1784148209345,"traceId":"eaf7a751603db501"},{"name":"telemetry-flush","duration":490023,"timestamp":276375313,"id":41,"parentId":1,"tags":{},"startTime":1784148209472,"traceId":"eaf7a751603db501"},{"name":"next-build","duration":18713954,"timestamp":258151412,"id":1,"tags":{"buildMode":"default","version":"16.2.6","bundler":"turbopack","has-custom-webpack-config":"false","use-build-worker":"true"},"startTime":1784148191248,"traceId":"eaf7a751603db501"}]
|
package/.next/types/routes.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
type AppRoutes = "/" | "/preview/[...slug]"
|
|
5
5
|
type PageRoutes = never
|
|
6
|
-
type LayoutRoutes = "/"
|
|
6
|
+
type LayoutRoutes = "/" | "/preview"
|
|
7
7
|
type RedirectRoutes = never
|
|
8
8
|
type RewriteRoutes = never
|
|
9
9
|
type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRoutes
|
|
@@ -11,6 +11,7 @@ type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRo
|
|
|
11
11
|
|
|
12
12
|
interface ParamMap {
|
|
13
13
|
"/": {}
|
|
14
|
+
"/preview": {}
|
|
14
15
|
"/preview/[...slug]": { "slug": string[]; }
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -19,6 +20,7 @@ export type ParamsOf<Route extends Routes> = ParamMap[Route]
|
|
|
19
20
|
|
|
20
21
|
interface LayoutSlotMap {
|
|
21
22
|
"/": never
|
|
23
|
+
"/preview": never
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
|
package/.next/types/validator.ts
CHANGED
|
@@ -68,3 +68,12 @@ type LayoutConfig<Route extends LayoutRoutes = LayoutRoutes> = {
|
|
|
68
68
|
// @ts-ignore
|
|
69
69
|
type __Unused = __Check
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
// Validate ../../src/app/preview/layout.tsx
|
|
73
|
+
{
|
|
74
|
+
type __IsExpected<Specific extends LayoutConfig<"/preview">> = Specific
|
|
75
|
+
const handler = {} as typeof import("../../src/app/preview/layout.js")
|
|
76
|
+
type __Check = __IsExpected<typeof handler>
|
|
77
|
+
// @ts-ignore
|
|
78
|
+
type __Unused = __Check
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-email/ui",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.9.0",
|
|
4
4
|
"description": "A live preview of your emails right in your browser.",
|
|
5
5
|
"main": "./index.mjs",
|
|
6
6
|
"files": [
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"typescript": "5.9.3",
|
|
74
74
|
"use-debounce": "10.1.1",
|
|
75
75
|
"zod": "4.3.6",
|
|
76
|
-
"react-email": "6.
|
|
76
|
+
"react-email": "6.9.0"
|
|
77
77
|
},
|
|
78
78
|
"license": "MIT",
|
|
79
79
|
"repository": {
|
|
@@ -7,6 +7,7 @@ import { Toaster } from 'sonner';
|
|
|
7
7
|
import { useDebouncedCallback } from 'use-debounce';
|
|
8
8
|
import { Topbar } from '../../../components';
|
|
9
9
|
import { CodeContainer } from '../../../components/code-container';
|
|
10
|
+
import { PreviewPropsPanel } from '../../../components/preview-props-panel';
|
|
10
11
|
import {
|
|
11
12
|
makeIframeDocumentBubbleEvents,
|
|
12
13
|
ResizableWrapper,
|
|
@@ -16,8 +17,10 @@ import { useToolbarState } from '../../../components/toolbar';
|
|
|
16
17
|
import { Tooltip } from '../../../components/tooltip';
|
|
17
18
|
import { ActiveViewToggleGroup } from '../../../components/topbar/active-view-toggle-group';
|
|
18
19
|
import { EmulatedDarkModeToggle } from '../../../components/topbar/emulated-dark-mode-toggle';
|
|
20
|
+
import { PropsPanelToggle } from '../../../components/topbar/props-panel-toggle';
|
|
19
21
|
import { ViewSizeControls } from '../../../components/topbar/view-size-controls';
|
|
20
22
|
import { usePreviewContext } from '../../../contexts/preview';
|
|
23
|
+
import { usePropsPanel } from '../../../contexts/props-panel';
|
|
21
24
|
import { useClampedState } from '../../../hooks/use-clamped-state';
|
|
22
25
|
import { cn } from '../../../utils';
|
|
23
26
|
import { inferEmailTitle } from '../../../utils/infer-email-title';
|
|
@@ -104,6 +107,9 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
|
|
|
104
107
|
|
|
105
108
|
const { toggled: toolbarToggled } = useToolbarState();
|
|
106
109
|
|
|
110
|
+
const { open: propsPanelOpen, setOpen: handlePropsPanelChange } =
|
|
111
|
+
usePropsPanel();
|
|
112
|
+
|
|
107
113
|
return (
|
|
108
114
|
<>
|
|
109
115
|
<Topbar emailTitle={emailTitle}>
|
|
@@ -137,6 +143,12 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
|
|
|
137
143
|
activeView={activeView}
|
|
138
144
|
setActiveView={handleViewChange}
|
|
139
145
|
/>
|
|
146
|
+
{hasRenderingMetadata && !isRawHtmlEmail ? (
|
|
147
|
+
<PropsPanelToggle
|
|
148
|
+
onChange={handlePropsPanelChange}
|
|
149
|
+
open={propsPanelOpen}
|
|
150
|
+
/>
|
|
151
|
+
) : null}
|
|
140
152
|
{hasRenderingMetadata ? (
|
|
141
153
|
<div className="flex justify-end">
|
|
142
154
|
<Send
|
|
@@ -150,121 +162,131 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
|
|
|
150
162
|
</Topbar>
|
|
151
163
|
|
|
152
164
|
<div
|
|
153
|
-
{...props}
|
|
154
165
|
className={cn(
|
|
155
|
-
'h-[calc(100%-3.5rem-2.375rem)] will-change-[height]
|
|
156
|
-
activeView === 'preview' && 'bg-gray-200',
|
|
157
|
-
activeView === 'preview' && isDarkModeEnabled && 'bg-gray-400',
|
|
166
|
+
'flex h-[calc(100%-3.5rem-2.375rem)] will-change-[height] transition-[height] duration-300',
|
|
158
167
|
toolbarToggled && 'h-[calc(100%-3.5rem-13rem)]',
|
|
159
|
-
className,
|
|
160
168
|
)}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
>
|
|
170
|
+
<div
|
|
171
|
+
{...props}
|
|
172
|
+
className={cn(
|
|
173
|
+
'relative flex min-w-0 grow p-4',
|
|
174
|
+
activeView === 'preview' && 'bg-gray-200',
|
|
175
|
+
activeView === 'preview' && isDarkModeEnabled && 'bg-gray-400',
|
|
176
|
+
className,
|
|
177
|
+
)}
|
|
178
|
+
ref={(element) => {
|
|
179
|
+
const observer = new ResizeObserver((entry) => {
|
|
180
|
+
const [elementEntry] = entry;
|
|
181
|
+
if (elementEntry) {
|
|
182
|
+
setMaxWidth(elementEntry.contentRect.width);
|
|
183
|
+
setMaxHeight(elementEntry.contentRect.height);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (element) {
|
|
188
|
+
observer.observe(element);
|
|
167
189
|
}
|
|
168
|
-
});
|
|
169
190
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
191
|
+
return () => {
|
|
192
|
+
observer.disconnect();
|
|
193
|
+
};
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
{hasErrors ? <ErrorOverlay error={renderingResult.error} /> : null}
|
|
173
197
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
<>
|
|
183
|
-
{activeView === 'preview' && (
|
|
184
|
-
<ResizableWrapper
|
|
185
|
-
minHeight={minHeight}
|
|
186
|
-
minWidth={minWidth}
|
|
187
|
-
maxHeight={maxHeight}
|
|
188
|
-
maxWidth={maxWidth}
|
|
189
|
-
height={height}
|
|
190
|
-
onResizeEnd={() => {
|
|
191
|
-
handleSaveViewSize();
|
|
192
|
-
}}
|
|
193
|
-
onResize={(value, direction) => {
|
|
194
|
-
const isHorizontal =
|
|
195
|
-
direction === 'east' || direction === 'west';
|
|
196
|
-
if (isHorizontal) {
|
|
197
|
-
setWidth(Math.round(value));
|
|
198
|
-
} else {
|
|
199
|
-
setHeight(Math.round(value));
|
|
200
|
-
}
|
|
201
|
-
}}
|
|
202
|
-
width={width}
|
|
203
|
-
>
|
|
204
|
-
<EmailFrame
|
|
205
|
-
className="max-h-full rounded-lg bg-white [color-scheme:auto]"
|
|
206
|
-
darkMode={isDarkModeEnabled}
|
|
207
|
-
markup={renderedEmailMetadata.markup}
|
|
208
|
-
width={width}
|
|
198
|
+
{hasRenderingMetadata ? (
|
|
199
|
+
<>
|
|
200
|
+
{activeView === 'preview' && (
|
|
201
|
+
<ResizableWrapper
|
|
202
|
+
minHeight={minHeight}
|
|
203
|
+
minWidth={minWidth}
|
|
204
|
+
maxHeight={maxHeight}
|
|
205
|
+
maxWidth={maxWidth}
|
|
209
206
|
height={height}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
207
|
+
onResizeEnd={() => {
|
|
208
|
+
handleSaveViewSize();
|
|
209
|
+
}}
|
|
210
|
+
onResize={(value, direction) => {
|
|
211
|
+
const isHorizontal =
|
|
212
|
+
direction === 'east' || direction === 'west';
|
|
213
|
+
if (isHorizontal) {
|
|
214
|
+
setWidth(Math.round(value));
|
|
215
|
+
} else {
|
|
216
|
+
setHeight(Math.round(value));
|
|
217
|
+
}
|
|
215
218
|
}}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
+
width={width}
|
|
220
|
+
>
|
|
221
|
+
<EmailFrame
|
|
222
|
+
className="max-h-full rounded-lg bg-white [color-scheme:auto]"
|
|
223
|
+
darkMode={isDarkModeEnabled}
|
|
224
|
+
markup={renderedEmailMetadata.markup}
|
|
225
|
+
width={width}
|
|
226
|
+
height={height}
|
|
227
|
+
title={emailTitle}
|
|
228
|
+
ref={(iframe) => {
|
|
229
|
+
if (!iframe) return;
|
|
230
|
+
|
|
231
|
+
return makeIframeDocumentBubbleEvents(iframe);
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
234
|
+
</ResizableWrapper>
|
|
235
|
+
)}
|
|
219
236
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
237
|
+
{activeView === 'source' && (
|
|
238
|
+
<div className="h-full w-full">
|
|
239
|
+
<div className="m-auto h-full flex max-w-3xl p-6">
|
|
240
|
+
<Tooltip.Provider>
|
|
241
|
+
<CodeContainer
|
|
242
|
+
activeLang={activeLang}
|
|
243
|
+
basename={renderedEmailMetadata.basename}
|
|
244
|
+
markups={
|
|
245
|
+
isRawHtmlEmail
|
|
246
|
+
? [
|
|
247
|
+
{
|
|
248
|
+
language: 'html',
|
|
249
|
+
extension: 'html',
|
|
250
|
+
content: renderedEmailMetadata.prettyMarkup,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
language: 'markdown',
|
|
254
|
+
extension: 'md',
|
|
255
|
+
content: renderedEmailMetadata.plainText,
|
|
256
|
+
},
|
|
257
|
+
]
|
|
258
|
+
: [
|
|
259
|
+
{
|
|
260
|
+
language: 'tsx',
|
|
261
|
+
extension: renderedEmailMetadata.extname,
|
|
262
|
+
content: renderedEmailMetadata.reactMarkup,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
language: 'html',
|
|
266
|
+
content: renderedEmailMetadata.prettyMarkup,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
language: 'markdown',
|
|
270
|
+
extension: 'md',
|
|
271
|
+
content: renderedEmailMetadata.plainText,
|
|
272
|
+
},
|
|
273
|
+
]
|
|
274
|
+
}
|
|
275
|
+
setActiveLang={handleLangChange}
|
|
276
|
+
/>
|
|
277
|
+
</Tooltip.Provider>
|
|
278
|
+
</div>
|
|
261
279
|
</div>
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
) : null}
|
|
280
|
+
)}
|
|
281
|
+
</>
|
|
282
|
+
) : null}
|
|
266
283
|
|
|
267
|
-
|
|
284
|
+
<Toaster />
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{hasRenderingMetadata && !isRawHtmlEmail ? (
|
|
288
|
+
<PreviewPropsPanel open={propsPanelOpen} />
|
|
289
|
+
) : null}
|
|
268
290
|
</div>
|
|
269
291
|
</>
|
|
270
292
|
);
|
package/src/components/code.tsx
CHANGED
|
@@ -13,7 +13,7 @@ interface CodeProps {
|
|
|
13
13
|
language?: Language;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const
|
|
16
|
+
export const codeTheme = {
|
|
17
17
|
plain: {
|
|
18
18
|
color: '#EDEDEF',
|
|
19
19
|
fontSize: 13,
|
|
@@ -93,7 +93,7 @@ export const Code: React.FC<Readonly<CodeProps>> = ({
|
|
|
93
93
|
const value = children.trim();
|
|
94
94
|
|
|
95
95
|
return (
|
|
96
|
-
<Highlight code={value} language={language} theme={
|
|
96
|
+
<Highlight code={value} language={language} theme={codeTheme}>
|
|
97
97
|
{({ tokens, getLineProps, getTokenProps }) => (
|
|
98
98
|
<>
|
|
99
99
|
<div
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { IconElement, IconProps } from './icon-base';
|
|
3
|
+
import { IconBase } from './icon-base';
|
|
4
|
+
|
|
5
|
+
export const IconPanelRight = React.forwardRef<
|
|
6
|
+
IconElement,
|
|
7
|
+
Readonly<IconProps>
|
|
8
|
+
>(({ ...props }, forwardedRef) => (
|
|
9
|
+
<IconBase ref={forwardedRef} {...props}>
|
|
10
|
+
<path
|
|
11
|
+
clipRule="evenodd"
|
|
12
|
+
d="M5 2.25H19C20.5188 2.25 21.75 3.48122 21.75 5V19C21.75 20.5188 20.5188 21.75 19 21.75L5 21.75C3.48122 21.75 2.25 20.5188 2.25 19L2.25 5C2.25 3.48122 3.48122 2.25 5 2.25ZM5 20.25C4.30964 20.25 3.75 19.6904 3.75 19L3.75 5C3.75 4.30964 4.30964 3.75 5 3.75L14.25 3.75L14.25 20.25L5 20.25ZM15.75 20.25L15.75 3.75L19 3.75C19.6904 3.75 20.25 4.30965 20.25 5L20.25 19C20.25 19.6904 19.6904 20.25 19 20.25H15.75Z"
|
|
13
|
+
fill="currentColor"
|
|
14
|
+
fillRule="evenodd"
|
|
15
|
+
/>
|
|
16
|
+
</IconBase>
|
|
17
|
+
));
|
|
18
|
+
|
|
19
|
+
IconPanelRight.displayName = 'IconPanelRight';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Highlight } from 'prism-react-renderer';
|
|
4
|
+
import { cn } from '../utils';
|
|
5
|
+
import { codeTheme } from './code';
|
|
6
|
+
|
|
7
|
+
interface JsonEditorProps {
|
|
8
|
+
value: string;
|
|
9
|
+
onChange: (value: string) => void;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
'aria-label': string;
|
|
13
|
+
'aria-invalid'?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Text metrics must match between the highlighted <pre> and the transparent
|
|
17
|
+
// <textarea> on top of it, or the caret drifts from the characters.
|
|
18
|
+
const sharedTextClasses =
|
|
19
|
+
'whitespace-pre-wrap break-words p-2 text-[13px] leading-5 font-[MonoLisa,Menlo,monospace]';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Syntax-highlighted JSON editing surface: a highlighted render with an
|
|
23
|
+
* invisible textarea overlaid, so it looks like the source view but types
|
|
24
|
+
* like a plain input.
|
|
25
|
+
*/
|
|
26
|
+
export const JsonEditor = ({
|
|
27
|
+
value,
|
|
28
|
+
onChange,
|
|
29
|
+
disabled = false,
|
|
30
|
+
className,
|
|
31
|
+
...ariaProps
|
|
32
|
+
}: JsonEditorProps) => {
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
className={cn(
|
|
36
|
+
'relative min-h-40 rounded-md border border-slate-6 transition-colors',
|
|
37
|
+
'focus-within:border-slate-8',
|
|
38
|
+
disabled && 'opacity-60',
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
<Highlight code={value} language="json" theme={codeTheme}>
|
|
43
|
+
{({ tokens, getLineProps, getTokenProps }) => (
|
|
44
|
+
<pre aria-hidden className={sharedTextClasses}>
|
|
45
|
+
{tokens.map((line, lineIndex) => (
|
|
46
|
+
<div {...getLineProps({ line })} key={lineIndex}>
|
|
47
|
+
{line.map((token, tokenIndex) => (
|
|
48
|
+
<span {...getTokenProps({ token })} key={tokenIndex} />
|
|
49
|
+
))}
|
|
50
|
+
{line.length === 1 && line[0]?.empty ? '' : null}
|
|
51
|
+
</div>
|
|
52
|
+
))}
|
|
53
|
+
</pre>
|
|
54
|
+
)}
|
|
55
|
+
</Highlight>
|
|
56
|
+
<textarea
|
|
57
|
+
{...ariaProps}
|
|
58
|
+
className={cn(
|
|
59
|
+
sharedTextClasses,
|
|
60
|
+
'absolute inset-0 h-full w-full resize-none overflow-hidden bg-transparent text-transparent caret-slate-12 outline-none',
|
|
61
|
+
)}
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
onChange={(event) => {
|
|
64
|
+
onChange(event.currentTarget.value);
|
|
65
|
+
}}
|
|
66
|
+
spellCheck={false}
|
|
67
|
+
value={value}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { useDebouncedCallback } from 'use-debounce';
|
|
5
|
+
import { isBuilding } from '../app/env';
|
|
6
|
+
import { usePreviewContext } from '../contexts/preview';
|
|
7
|
+
import { cn } from '../utils';
|
|
8
|
+
import { JsonEditor } from './json-editor';
|
|
9
|
+
|
|
10
|
+
export const PreviewPropsEditor = () => {
|
|
11
|
+
const {
|
|
12
|
+
renderedEmailMetadata,
|
|
13
|
+
previewPropsOverride,
|
|
14
|
+
setPreviewPropsOverride,
|
|
15
|
+
} = usePreviewContext();
|
|
16
|
+
|
|
17
|
+
const renderedPropsJson = JSON.stringify(
|
|
18
|
+
renderedEmailMetadata?.previewProps ?? {},
|
|
19
|
+
null,
|
|
20
|
+
2,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// The applied override is the source of truth for what renders; the draft
|
|
24
|
+
// only preserves the user's raw text, which can be momentarily invalid
|
|
25
|
+
// JSON or formatted differently than the applied value.
|
|
26
|
+
const [draft, setDraft] = React.useState<string | undefined>(undefined);
|
|
27
|
+
const [parseError, setParseError] = React.useState<string | undefined>(
|
|
28
|
+
undefined,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const hasOverride = previewPropsOverride !== undefined;
|
|
32
|
+
|
|
33
|
+
// Applying is debounced because every applied value costs a server render:
|
|
34
|
+
// per-keystroke applies queue up behind each other and permanently cache
|
|
35
|
+
// every intermediate state.
|
|
36
|
+
const applyValue = useDebouncedCallback((newValue: string) => {
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(newValue) as unknown;
|
|
39
|
+
if (
|
|
40
|
+
parsed === null ||
|
|
41
|
+
typeof parsed !== 'object' ||
|
|
42
|
+
Array.isArray(parsed)
|
|
43
|
+
) {
|
|
44
|
+
setParseError('Props must be a JSON object');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
setParseError(undefined);
|
|
48
|
+
setPreviewPropsOverride(parsed as Record<string, unknown>);
|
|
49
|
+
} catch (exception) {
|
|
50
|
+
setParseError((exception as Error).message);
|
|
51
|
+
}
|
|
52
|
+
}, 400);
|
|
53
|
+
|
|
54
|
+
const handleChange = (newValue: string) => {
|
|
55
|
+
setDraft(newValue);
|
|
56
|
+
applyValue(newValue);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="flex h-full flex-col gap-3">
|
|
61
|
+
<div className="flex items-center justify-between gap-3">
|
|
62
|
+
{isBuilding ? (
|
|
63
|
+
<p className="text-slate-11">
|
|
64
|
+
Read-only. This preview was rendered at build time.
|
|
65
|
+
</p>
|
|
66
|
+
) : (
|
|
67
|
+
<button
|
|
68
|
+
className={cn(
|
|
69
|
+
'ml-auto shrink-0 rounded-md border border-slate-6 px-2 py-1 text-slate-11 transition-colors',
|
|
70
|
+
'hover:border-slate-8 hover:text-slate-12',
|
|
71
|
+
'disabled:opacity-40 disabled:hover:border-slate-6 disabled:hover:text-slate-11',
|
|
72
|
+
)}
|
|
73
|
+
disabled={!hasOverride && draft === undefined}
|
|
74
|
+
onClick={() => {
|
|
75
|
+
applyValue.cancel();
|
|
76
|
+
setDraft(undefined);
|
|
77
|
+
setParseError(undefined);
|
|
78
|
+
setPreviewPropsOverride(undefined);
|
|
79
|
+
}}
|
|
80
|
+
type="button"
|
|
81
|
+
>
|
|
82
|
+
Reset to defaults
|
|
83
|
+
</button>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
<JsonEditor
|
|
87
|
+
aria-invalid={parseError !== undefined}
|
|
88
|
+
aria-label="Preview props JSON"
|
|
89
|
+
className={cn(
|
|
90
|
+
parseError !== undefined && 'border-red-9 focus-within:border-red-9',
|
|
91
|
+
)}
|
|
92
|
+
disabled={isBuilding}
|
|
93
|
+
onChange={handleChange}
|
|
94
|
+
value={draft ?? renderedPropsJson}
|
|
95
|
+
/>
|
|
96
|
+
{parseError !== undefined ? (
|
|
97
|
+
<p className="pb-3 text-red-11">{parseError}</p>
|
|
98
|
+
) : null}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { usePreviewContext } from '../contexts/preview';
|
|
4
|
+
import { usePropsPanel } from '../contexts/props-panel';
|
|
5
|
+
import { cn } from '../utils';
|
|
6
|
+
import { PreviewPropsEditor } from './preview-props-editor';
|
|
7
|
+
|
|
8
|
+
export const PreviewPropsPanel = ({ open }: { open: boolean }) => {
|
|
9
|
+
const { animated } = usePropsPanel();
|
|
10
|
+
const { emailSlug } = usePreviewContext();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<aside
|
|
14
|
+
className={cn(
|
|
15
|
+
'shrink-0 overflow-hidden border-l border-slate-6',
|
|
16
|
+
animated && 'transition-[width] duration-200 will-change-[width]',
|
|
17
|
+
open ? 'w-72' : 'w-0 border-l-0',
|
|
18
|
+
)}
|
|
19
|
+
inert={!open}
|
|
20
|
+
>
|
|
21
|
+
<div className="flex h-full w-72 flex-col">
|
|
22
|
+
<div className="flex h-10 shrink-0 items-center border-b border-slate-6 px-4 font-medium text-slate-12 text-xs">
|
|
23
|
+
Props
|
|
24
|
+
</div>
|
|
25
|
+
<div className="grow overflow-y-auto px-4 pt-3 text-slate-11 text-xs">
|
|
26
|
+
<PreviewPropsEditor key={emailSlug} />
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</aside>
|
|
30
|
+
);
|
|
31
|
+
};
|