react-email 4.0.0-alpha.1 → 4.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/dist/cli/index.js +3 -1
  3. package/dist/cli/index.mjs +3 -1
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +9 -9
  6. package/dist/preview/.next/build-manifest.json +2 -2
  7. package/dist/preview/.next/cache/.rscinfo +1 -1
  8. package/dist/preview/.next/cache/webpack/client-development/0.pack.gz +0 -0
  9. package/dist/preview/.next/cache/webpack/client-development/1.pack.gz +0 -0
  10. package/dist/preview/.next/cache/webpack/client-development/2.pack.gz +0 -0
  11. package/dist/preview/.next/cache/webpack/client-development/index.pack.gz +0 -0
  12. package/dist/preview/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
  13. package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
  14. package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
  15. package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
  16. package/dist/preview/.next/cache/webpack/server-development/0.pack.gz +0 -0
  17. package/dist/preview/.next/cache/webpack/server-development/1.pack.gz +0 -0
  18. package/dist/preview/.next/cache/webpack/server-development/index.pack.gz +0 -0
  19. package/dist/preview/.next/cache/webpack/server-development/index.pack.gz.old +0 -0
  20. package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
  21. package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
  22. package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
  23. package/dist/preview/.next/next-server.js.nft.json +1 -1
  24. package/dist/preview/.next/prerender-manifest.json +1 -1
  25. package/dist/preview/.next/server/app/_not-found/page.js +1 -1
  26. package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  27. package/dist/preview/.next/server/app/page.js +1 -1
  28. package/dist/preview/.next/server/app/page.js.nft.json +1 -1
  29. package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
  30. package/dist/preview/.next/server/app/preview/[...slug]/page.js +5 -5
  31. package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  32. package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  33. package/dist/preview/.next/server/chunks/509.js +1 -0
  34. package/dist/preview/.next/server/chunks/{282.js → 734.js} +6 -6
  35. package/dist/preview/.next/server/pages/500.html +1 -1
  36. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  37. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  38. package/dist/preview/.next/static/chunks/285-dbf6306a0d45c33d.js +1 -0
  39. package/dist/preview/.next/static/chunks/490-0db0db14b377daca.js +1 -0
  40. package/dist/preview/.next/static/chunks/603-36207c8905355e23.js +1 -0
  41. package/dist/preview/.next/static/chunks/app/{layout-f1bad3fcfbc7eb6b.js → layout-f6f64b817a2cf938.js} +1 -1
  42. package/dist/preview/.next/static/chunks/app/{page-800163ba6c6d943d.js → page-f5f96bd66526060f.js} +1 -1
  43. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-fb2bf0253c2dada4.js +1 -0
  44. package/dist/preview/.next/static/css/778d574c88a1db3c.css +3 -0
  45. package/dist/preview/.next/trace +22 -22
  46. package/package.json +3 -1
  47. package/src/actions/email-validation/check-images.spec.tsx +0 -1
  48. package/src/actions/email-validation/check-images.ts +0 -1
  49. package/src/actions/email-validation/check-links.spec.tsx +0 -1
  50. package/src/app/preview/[...slug]/preview.tsx +128 -40
  51. package/src/components/code-snippet.tsx +0 -2
  52. package/src/components/logo.tsx +0 -2
  53. package/src/components/resizable-wrapper.tsx +176 -0
  54. package/src/components/shell.tsx +17 -3
  55. package/src/components/sidebar/sidebar.tsx +1 -1
  56. package/src/components/topbar/active-view-toggle-group.tsx +86 -0
  57. package/src/components/topbar/view-size-controls.tsx +247 -0
  58. package/src/components/topbar.tsx +50 -125
  59. package/src/hooks/use-clamped-state.ts +24 -0
  60. package/tailwind.config.ts +12 -17
  61. package/tsconfig.json +6 -2
  62. package/tsconfig.test.json +8 -0
  63. package/vitest.config.ts +13 -0
  64. package/dist/preview/.next/server/chunks/667.js +0 -1
  65. package/dist/preview/.next/static/chunks/207-7ab46c2d84f60fed.js +0 -1
  66. package/dist/preview/.next/static/chunks/490-9a10c001ec2dffb2.js +0 -1
  67. package/dist/preview/.next/static/chunks/860-38d96c8819ba6f19.js +0 -1
  68. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-5b5c4557fc89db64.js +0 -1
  69. package/dist/preview/.next/static/css/d6c4def4cc3fb858.css +0 -3
  70. /package/dist/preview/.next/static/{Mn2FuRztLqr32yO8CKHi9 → iP6qiNn8FML_AvKcxGPhM}/_buildManifest.js +0 -0
  71. /package/dist/preview/.next/static/{Mn2FuRztLqr32yO8CKHi9 → iP6qiNn8FML_AvKcxGPhM}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "4.0.0-alpha.1",
3
+ "version": "4.0.0-alpha.3",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/cli/index.js"
@@ -37,6 +37,7 @@
37
37
  "devDependencies": {
38
38
  "@radix-ui/colors": "1.0.1",
39
39
  "@radix-ui/react-collapsible": "1.1.0",
40
+ "@radix-ui/react-dropdown-menu": "2.1.4",
40
41
  "@radix-ui/react-popover": "1.1.1",
41
42
  "@radix-ui/react-slot": "1.1.0",
42
43
  "@radix-ui/react-tabs": "1.1.1",
@@ -75,6 +76,7 @@
75
76
  "tsup": "7.2.0",
76
77
  "tsx": "4.9.0",
77
78
  "typescript": "5.1.6",
79
+ "use-debounce": "10.0.4",
78
80
  "vitest": "1.1.3",
79
81
  "@react-email/render": "1.0.5"
80
82
  },
@@ -1,5 +1,4 @@
1
1
  import { render } from '@react-email/render';
2
- import * as React from 'react';
3
2
  import { type ImageCheckingResult, checkImages } from './check-images';
4
3
 
5
4
  test('checkImages()', async () => {
@@ -1,7 +1,6 @@
1
1
  'use server';
2
2
 
3
3
  import type { IncomingMessage } from 'node:http';
4
- import { headers } from 'next/headers';
5
4
  import { parse } from 'node-html-parser';
6
5
  import { quickFetch } from './quick-fetch';
7
6
 
@@ -1,5 +1,4 @@
1
1
  import { render } from '@react-email/render';
2
- import * as React from 'react';
3
2
  import { type LinkCheckingResult, checkLinks } from './check-links';
4
3
 
5
4
  test('checkLinks()', async () => {
@@ -1,12 +1,19 @@
1
1
  'use client';
2
2
 
3
3
  import { usePathname, useRouter, useSearchParams } from 'next/navigation';
4
- import React from 'react';
4
+ import { useState } from 'react';
5
+ import { flushSync } from 'react-dom';
5
6
  import { Toaster } from 'sonner';
7
+ import { useDebouncedCallback } from 'use-debounce';
6
8
  import type { EmailRenderingResult } from '../../../actions/render-email-by-path';
7
9
  import { CodeContainer } from '../../../components/code-container';
10
+ import {
11
+ ResizableWarpper,
12
+ makeIframeDocumentBubbleEvents,
13
+ } from '../../../components/resizable-wrapper';
8
14
  import { Shell } from '../../../components/shell';
9
15
  import { Tooltip } from '../../../components/tooltip';
16
+ import { useClampedState } from '../../../hooks/use-clamped-state';
10
17
  import { useEmailRenderingResult } from '../../../hooks/use-email-rendering-result';
11
18
  import { useHotreload } from '../../../hooks/use-hot-reload';
12
19
  import { useRenderingMetadata } from '../../../hooks/use-rendering-metadata';
@@ -29,7 +36,7 @@ const Preview = ({
29
36
  const pathname = usePathname();
30
37
  const searchParams = useSearchParams();
31
38
 
32
- const activeView = searchParams.get('view') ?? 'desktop';
39
+ const activeView = searchParams.get('view') ?? 'preview';
33
40
  const activeLang = searchParams.get('lang') ?? 'jsx';
34
41
 
35
42
  const renderingResult = useEmailRenderingResult(
@@ -75,60 +82,141 @@ const Preview = ({
75
82
 
76
83
  const hasNoErrors = typeof renderedEmailMetadata !== 'undefined';
77
84
 
85
+ const [maxWidth, setMaxWidth] = useState(Number.POSITIVE_INFINITY);
86
+ const [maxHeight, setMaxHeight] = useState(Number.POSITIVE_INFINITY);
87
+ const minWidth = 350;
88
+ const minHeight = 600;
89
+ const storedWidth = searchParams.get('width');
90
+ const storedHeight = searchParams.get('height');
91
+ const [width, setWidth] = useClampedState(
92
+ storedWidth ? Number.parseInt(storedWidth) : 600,
93
+ 350,
94
+ maxWidth,
95
+ );
96
+ const [height, setHeight] = useClampedState(
97
+ storedHeight ? Number.parseInt(storedHeight) : 1024,
98
+ 600,
99
+ maxHeight,
100
+ );
101
+
102
+ const handleSaveViewSize = useDebouncedCallback(() => {
103
+ const params = new URLSearchParams(searchParams);
104
+ params.set('width', width.toString());
105
+ params.set('height', height.toString());
106
+ router.push(`${pathname}?${params.toString()}`);
107
+ }, 300);
108
+
78
109
  return (
79
110
  <Shell
80
- activeView={hasNoErrors ? activeView : undefined}
111
+ activeView={activeView}
81
112
  currentEmailOpenSlug={slug}
82
113
  markup={renderedEmailMetadata?.markup}
83
114
  pathSeparator={pathSeparator}
84
- setActiveView={hasNoErrors ? handleViewChange : undefined}
115
+ setActiveView={handleViewChange}
116
+ setViewHeight={(height) => {
117
+ setHeight(height);
118
+ flushSync(() => {
119
+ handleSaveViewSize();
120
+ });
121
+ }}
122
+ setViewWidth={(width) => {
123
+ setWidth(width);
124
+ flushSync(() => {
125
+ handleSaveViewSize();
126
+ });
127
+ }}
128
+ viewHeight={height}
129
+ viewWidth={width}
85
130
  >
86
131
  {/* This relative is so that when there is any error the user can still switch between emails */}
87
- <div className="relative h-full">
132
+ <div
133
+ className="relative flex h-full bg-gray-200 pb-8"
134
+ ref={(element) => {
135
+ const observer = new ResizeObserver((entry) => {
136
+ const [elementEntry] = entry;
137
+ if (elementEntry) {
138
+ setMaxWidth(elementEntry.contentRect.width - 80);
139
+ setMaxHeight(elementEntry.contentRect.height - 80);
140
+ }
141
+ });
142
+
143
+ if (element) {
144
+ observer.observe(element);
145
+ }
146
+
147
+ return () => {
148
+ observer.disconnect();
149
+ };
150
+ }}
151
+ >
88
152
  {'error' in renderingResult ? (
89
153
  <RenderingError error={renderingResult.error} />
90
154
  ) : null}
91
155
 
92
156
  {hasNoErrors ? (
93
157
  <>
94
- {activeView === 'desktop' && (
95
- <iframe
96
- className="h-full w-full bg-white"
97
- srcDoc={renderedEmailMetadata.markup}
98
- title={slug}
99
- />
100
- )}
101
-
102
- {activeView === 'mobile' && (
103
- <iframe
104
- className="mx-auto h-full w-[360px] bg-white"
105
- srcDoc={renderedEmailMetadata.markup}
106
- title={slug}
107
- />
158
+ {activeView === 'preview' && (
159
+ <ResizableWarpper
160
+ minHeight={minHeight}
161
+ minWidth={minWidth}
162
+ maxHeight={maxHeight}
163
+ maxWidth={maxWidth}
164
+ height={height}
165
+ onResizeEnd={() => {
166
+ handleSaveViewSize();
167
+ }}
168
+ onResize={(value, direction) => {
169
+ const isHorizontal =
170
+ direction === 'east' || direction === 'west';
171
+ if (isHorizontal) {
172
+ setWidth(value);
173
+ } else {
174
+ setHeight(value);
175
+ }
176
+ }}
177
+ width={width}
178
+ >
179
+ <iframe
180
+ className="solid max-h-full rounded-lg bg-white"
181
+ ref={(iframe) => {
182
+ if (iframe) {
183
+ return makeIframeDocumentBubbleEvents(iframe);
184
+ }
185
+ }}
186
+ srcDoc={renderedEmailMetadata.markup}
187
+ style={{
188
+ width: `${width}px`,
189
+ height: `${height}px`,
190
+ }}
191
+ title={slug}
192
+ />
193
+ </ResizableWarpper>
108
194
  )}
109
195
 
110
196
  {activeView === 'source' && (
111
- <div className="mx-auto flex max-w-3xl gap-6 p-6">
112
- <Tooltip.Provider>
113
- <CodeContainer
114
- activeLang={activeLang}
115
- markups={[
116
- {
117
- language: 'jsx',
118
- content: renderedEmailMetadata.reactMarkup,
119
- },
120
- {
121
- language: 'markup',
122
- content: renderedEmailMetadata.markup,
123
- },
124
- {
125
- language: 'markdown',
126
- content: renderedEmailMetadata.plainText,
127
- },
128
- ]}
129
- setActiveLang={handleLangChange}
130
- />
131
- </Tooltip.Provider>
197
+ <div className="h-full w-full bg-black">
198
+ <div className="m-auto flex max-w-3xl p-6">
199
+ <Tooltip.Provider>
200
+ <CodeContainer
201
+ activeLang={activeLang}
202
+ markups={[
203
+ {
204
+ language: 'jsx',
205
+ content: renderedEmailMetadata.reactMarkup,
206
+ },
207
+ {
208
+ language: 'markup',
209
+ content: renderedEmailMetadata.markup,
210
+ },
211
+ {
212
+ language: 'markdown',
213
+ content: renderedEmailMetadata.plainText,
214
+ },
215
+ ]}
216
+ setActiveLang={handleLangChange}
217
+ />
218
+ </Tooltip.Provider>
219
+ </div>
132
220
  </div>
133
221
  )}
134
222
  </>
@@ -1,5 +1,3 @@
1
- import React from 'react';
2
-
3
1
  const CodeSnippet = ({ children }) => {
4
2
  return (
5
3
  <code className="m-0.5 inline-block rounded-md bg-white/10 p-1 font-mono leading-none text-slate-12">
@@ -1,5 +1,3 @@
1
- import * as React from 'react';
2
-
3
1
  export const Logo = () => (
4
2
  <svg
5
3
  fill="none"
@@ -0,0 +1,176 @@
1
+ import { Slot } from '@radix-ui/react-slot';
2
+ import { type ComponentProps, useCallback, useEffect, useRef } from 'react';
3
+ import { cn } from '../utils';
4
+
5
+ type Direction = 'north' | 'south' | 'east' | 'west';
6
+
7
+ type ResizableWarpperProps = {
8
+ width: number;
9
+ height: number;
10
+
11
+ maxWidth: number;
12
+ maxHeight: number;
13
+ minWidth: number;
14
+ minHeight: number;
15
+
16
+ onResize: (newSize: number, direction: Direction) => void;
17
+ onResizeEnd?: () => void;
18
+
19
+ children: React.ReactNode;
20
+ } & Omit<ComponentProps<'div'>, 'onResize' | 'children'>;
21
+
22
+ export const makeIframeDocumentBubbleEvents = (iframe: HTMLIFrameElement) => {
23
+ const mouseMoveBubbler = (event: MouseEvent) => {
24
+ const bounds = iframe.getBoundingClientRect();
25
+ document.dispatchEvent(
26
+ new MouseEvent('mousemove', {
27
+ ...event,
28
+ clientX: event.clientX + bounds.x,
29
+ clientY: event.clientY + bounds.y,
30
+ }),
31
+ );
32
+ };
33
+ const mouseUpBubbler = (event: MouseEvent) => {
34
+ document.dispatchEvent(new MouseEvent('mouseup', event));
35
+ };
36
+ iframe.contentDocument?.addEventListener('mousemove', mouseMoveBubbler);
37
+ iframe.contentDocument?.addEventListener('mouseup', mouseUpBubbler);
38
+ return () => {
39
+ iframe.contentDocument?.removeEventListener('mousemove', mouseMoveBubbler);
40
+ iframe.contentDocument?.removeEventListener('mouseup', mouseUpBubbler);
41
+ };
42
+ };
43
+
44
+ export const ResizableWarpper = ({
45
+ width,
46
+ height,
47
+ onResize,
48
+ onResizeEnd,
49
+ children,
50
+
51
+ maxHeight,
52
+ maxWidth,
53
+ minHeight,
54
+ minWidth,
55
+
56
+ ...rest
57
+ }: ResizableWarpperProps) => {
58
+ const resizableRef = useRef<HTMLElement>(null);
59
+
60
+ const mouseMoveListener = useRef<(event: MouseEvent) => void>(null);
61
+
62
+ const handleStopResizing = useCallback(() => {
63
+ if (mouseMoveListener.current) {
64
+ document.removeEventListener('mousemove', mouseMoveListener.current);
65
+ }
66
+ document.removeEventListener('mouseup', handleStopResizing);
67
+ onResizeEnd?.();
68
+ }, []);
69
+
70
+ const handleStartResizing = (direction: Direction) => {
71
+ mouseMoveListener.current = (event) => {
72
+ if (event.button === 0 && resizableRef.current) {
73
+ const isHorizontal = direction === 'east' || direction === 'west';
74
+
75
+ const mousePosition = isHorizontal ? event.clientX : event.clientY;
76
+ const resizableBoundingRect =
77
+ resizableRef.current.getBoundingClientRect();
78
+ const center = isHorizontal
79
+ ? resizableBoundingRect.x + resizableBoundingRect.width / 2
80
+ : resizableBoundingRect.y + resizableBoundingRect.height / 2;
81
+ onResize(Math.abs(mousePosition - center) * 2, direction);
82
+ } else {
83
+ handleStopResizing();
84
+ }
85
+ };
86
+
87
+ document.addEventListener('mouseup', handleStopResizing);
88
+ document.addEventListener('mousemove', mouseMoveListener.current);
89
+ };
90
+
91
+ useEffect(() => {
92
+ if (!window.document) return;
93
+
94
+ return () => {
95
+ handleStopResizing();
96
+ };
97
+ // eslint-disable-next-line react-hooks/exhaustive-deps
98
+ }, []);
99
+
100
+ return (
101
+ <div
102
+ {...rest}
103
+ className={cn(
104
+ 'relative mx-auto my-auto box-content px-4 py-2',
105
+ rest.className,
106
+ )}
107
+ >
108
+ <div
109
+ aria-label="resize-west"
110
+ aria-valuenow={width}
111
+ aria-valuemin={minWidth}
112
+ aria-valuemax={maxWidth}
113
+ className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-2 cursor-w-resize p-2 [user-drag:none]"
114
+ onDragStart={(event) => event.preventDefault()}
115
+ draggable="false"
116
+ onMouseDown={() => {
117
+ handleStartResizing('west');
118
+ }}
119
+ role="slider"
120
+ tabIndex={0}
121
+ >
122
+ <div className="h-8 w-1 rounded-md bg-black/30" />
123
+ </div>
124
+ <div
125
+ aria-label="resize-east"
126
+ aria-valuenow={width}
127
+ aria-valuemin={minWidth}
128
+ aria-valuemax={maxWidth}
129
+ onDragStart={(event) => event.preventDefault()}
130
+ className="-translate-x-full -translate-y-1/2 absolute top-1/2 left-full cursor-e-resize p-2 [user-drag:none]"
131
+ draggable="false"
132
+ onMouseDown={() => {
133
+ handleStartResizing('east');
134
+ }}
135
+ role="slider"
136
+ tabIndex={0}
137
+ >
138
+ <div className="h-8 w-1 rounded-md bg-black/30" />
139
+ </div>
140
+ <div
141
+ aria-label="resize-north"
142
+ aria-valuenow={height}
143
+ aria-valuemin={minHeight}
144
+ aria-valuemax={maxHeight}
145
+ onDragStart={(event) => event.preventDefault()}
146
+ className="-translate-x-1/2 -translate-y-1/2 absolute top-0 left-1/2 cursor-n-resize p-2 [user-drag:none]"
147
+ draggable="false"
148
+ onMouseDown={() => {
149
+ handleStartResizing('north');
150
+ }}
151
+ role="slider"
152
+ tabIndex={0}
153
+ >
154
+ <div className="h-1 w-8 rounded-md bg-black/30" />
155
+ </div>
156
+ <div
157
+ aria-label="resize-south"
158
+ aria-valuenow={height}
159
+ aria-valuemin={minHeight}
160
+ aria-valuemax={maxHeight}
161
+ onDragStart={(event) => event.preventDefault()}
162
+ className="-translate-x-1/2 -translate-y-1/2 absolute top-full left-1/2 cursor-s-resize p-2 [user-drag:none]"
163
+ draggable="false"
164
+ onMouseDown={() => {
165
+ handleStartResizing('south');
166
+ }}
167
+ role="slider"
168
+ tabIndex={0}
169
+ >
170
+ <div className="h-1 w-8 rounded-md bg-black/30" />
171
+ </div>
172
+
173
+ <Slot ref={resizableRef}>{children}</Slot>
174
+ </div>
175
+ );
176
+ };
@@ -12,8 +12,14 @@ interface ShellProps extends RootProps {
12
12
  markup?: string;
13
13
  currentEmailOpenSlug?: string;
14
14
  pathSeparator?: string;
15
+
15
16
  activeView?: string;
16
17
  setActiveView?: (view: string) => void;
18
+
19
+ viewWidth?: number;
20
+ setViewWidth?: (width: number) => void;
21
+ viewHeight?: number;
22
+ setViewHeight?: (height: number) => void;
17
23
  }
18
24
 
19
25
  export const Shell = ({
@@ -23,6 +29,10 @@ export const Shell = ({
23
29
  markup,
24
30
  activeView,
25
31
  setActiveView,
32
+ viewHeight,
33
+ viewWidth,
34
+ setViewHeight,
35
+ setViewWidth,
26
36
  }: ShellProps) => {
27
37
  const [sidebarToggled, setSidebarToggled] = React.useState(false);
28
38
  const [triggerTransition, setTriggerTransition] = React.useState(false);
@@ -75,8 +85,8 @@ export const Shell = ({
75
85
  className={cn(
76
86
  'relative h-full max-h-full min-h-screen w-[100vw] overflow-hidden will-change-width sm:mt-[4.375rem] md:absolute md:right-0 lg:mt-0',
77
87
  {
78
- 'lg:w-[calc(100vw)] lg:translate-x-0': sidebarToggled,
79
- 'lg:w-[calc(100vw-20rem)] lg:translate-x-0': !sidebarToggled,
88
+ 'lg:w-[calc(100dvw)] lg:translate-x-0': sidebarToggled,
89
+ 'lg:w-[calc(100dvw-20rem)] lg:translate-x-0': !sidebarToggled,
80
90
  },
81
91
  )}
82
92
  style={{
@@ -104,9 +114,13 @@ export const Shell = ({
104
114
  }}
105
115
  pathSeparator={pathSeparator}
106
116
  setActiveView={setActiveView}
117
+ setViewHeight={setViewHeight}
118
+ setViewWidth={setViewWidth}
119
+ viewHeight={viewHeight}
120
+ viewWidth={viewWidth}
107
121
  />
108
122
  ) : null}
109
- <div className="relative mx-auto h-[calc(100vh-3.3125rem)] grow md:h-full">
123
+ <div className="relative mx-auto h-[calc(100dvh-3.3125rem)] grow md:h-full">
110
124
  {children}
111
125
  </div>
112
126
  </div>
@@ -147,7 +147,7 @@ const Panel = ({ title, active, children }: PanelProps) => (
147
147
  {title}
148
148
  </Heading>
149
149
  </div>
150
- <div className="-mt-[.5px] relative h-[calc(100vh-4.375rem)] w-full border-slate-4 border-t px-4 pb-3">
150
+ <div className="-mt-[.5px] relative h-[calc(100dvh-4.375rem)] w-full border-slate-4 border-t px-4 pb-3">
151
151
  {children}
152
152
  </div>
153
153
  </>
@@ -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
+ };