Skip to content

Commit 87ff810

Browse files
authored
feat: added the "Open Preview in a New Tab" (#1101)
* added the "Open Preview in a New Tab" * enhancement [Open Preview] [▼] // Two buttons side by side | +-- [Mobile (375x667)] // Dropdown menu |-- [Tablet (768x1024)] |-- [Laptop (1366x768)] +-- [Desktop (1920x1080)] * Update Preview.tsx * Update Preview.tsx
1 parent c773824 commit 87ff810

File tree

3 files changed

+445
-27
lines changed

3 files changed

+445
-27
lines changed

app/components/workbench/Preview.tsx

Lines changed: 93 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
1-
import { useStore } from '@nanostores/react';
21
import { memo, useCallback, useEffect, useRef, useState } from 'react';
2+
import { useStore } from '@nanostores/react';
33
import { IconButton } from '~/components/ui/IconButton';
44
import { workbenchStore } from '~/lib/stores/workbench';
55
import { PortDropdown } from './PortDropdown';
66
import { ScreenshotSelector } from './ScreenshotSelector';
77

88
type ResizeSide = 'left' | 'right' | null;
99

10+
interface WindowSize {
11+
name: string;
12+
width: number;
13+
height: number;
14+
}
15+
16+
const WINDOW_SIZES: WindowSize[] = [
17+
{ name: 'Mobile (375x667)', width: 375, height: 667 },
18+
{ name: 'Tablet (768x1024)', width: 768, height: 1024 },
19+
{ name: 'Laptop (1366x768)', width: 1366, height: 768 },
20+
{ name: 'Desktop (1920x1080)', width: 1920, height: 1080 },
21+
];
22+
1023
export const Preview = memo(() => {
1124
const iframeRef = useRef<HTMLIFrameElement>(null);
1225
const containerRef = useRef<HTMLDivElement>(null);
@@ -15,6 +28,7 @@ export const Preview = memo(() => {
1528
const [activePreviewIndex, setActivePreviewIndex] = useState(0);
1629
const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false);
1730
const [isFullscreen, setIsFullscreen] = useState(false);
31+
const [isPreviewOnly, setIsPreviewOnly] = useState(false);
1832
const hasSelectedPreview = useRef(false);
1933
const previews = useStore(workbenchStore.previews);
2034
const activePreview = previews[activePreviewIndex];
@@ -27,7 +41,7 @@ export const Preview = memo(() => {
2741
const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);
2842

2943
// Use percentage for width
30-
const [widthPercent, setWidthPercent] = useState<number>(37.5); // 375px assuming 1000px window width initially
44+
const [widthPercent, setWidthPercent] = useState<number>(37.5);
3145

3246
const resizingState = useRef({
3347
isResizing: false,
@@ -37,8 +51,10 @@ export const Preview = memo(() => {
3751
windowWidth: window.innerWidth,
3852
});
3953

40-
// Define the scaling factor
41-
const SCALING_FACTOR = 2; // Adjust this value to increase/decrease sensitivity
54+
const SCALING_FACTOR = 2;
55+
56+
const [isWindowSizeDropdownOpen, setIsWindowSizeDropdownOpen] = useState(false);
57+
const [selectedWindowSize, setSelectedWindowSize] = useState<WindowSize>(WINDOW_SIZES[0]);
4258

4359
useEffect(() => {
4460
if (!activePreview) {
@@ -79,7 +95,6 @@ export const Preview = memo(() => {
7995
[],
8096
);
8197

82-
// When previews change, display the lowest port if user hasn't selected a preview
8398
useEffect(() => {
8499
if (previews.length > 1 && !hasSelectedPreview.current) {
85100
const minPortIndex = previews.reduce(findMinPortIndex, 0);
@@ -122,7 +137,6 @@ export const Preview = memo(() => {
122137
return;
123138
}
124139

125-
// Prevent text selection
126140
document.body.style.userSelect = 'none';
127141

128142
resizingState.current.isResizing = true;
@@ -134,7 +148,7 @@ export const Preview = memo(() => {
134148
document.addEventListener('mousemove', onMouseMove);
135149
document.addEventListener('mouseup', onMouseUp);
136150

137-
e.preventDefault(); // Prevent any text selection on mousedown
151+
e.preventDefault();
138152
};
139153

140154
const onMouseMove = (e: MouseEvent) => {
@@ -145,7 +159,6 @@ export const Preview = memo(() => {
145159
const dx = e.clientX - resizingState.current.startX;
146160
const windowWidth = resizingState.current.windowWidth;
147161

148-
// Apply scaling factor to increase sensitivity
149162
const dxPercent = (dx / windowWidth) * 100 * SCALING_FACTOR;
150163

151164
let newWidthPercent = resizingState.current.startWidthPercent;
@@ -156,7 +169,6 @@ export const Preview = memo(() => {
156169
newWidthPercent = resizingState.current.startWidthPercent - dxPercent;
157170
}
158171

159-
// Clamp the width between 10% and 90%
160172
newWidthPercent = Math.max(10, Math.min(newWidthPercent, 90));
161173

162174
setWidthPercent(newWidthPercent);
@@ -168,17 +180,12 @@ export const Preview = memo(() => {
168180
document.removeEventListener('mousemove', onMouseMove);
169181
document.removeEventListener('mouseup', onMouseUp);
170182

171-
// Restore text selection
172183
document.body.style.userSelect = '';
173184
};
174185

175-
// Handle window resize to ensure widthPercent remains valid
176186
useEffect(() => {
177187
const handleWindowResize = () => {
178-
/*
179-
* Optional: Adjust widthPercent if necessary
180-
* For now, since widthPercent is relative, no action is needed
181-
*/
188+
// Optional: Adjust widthPercent if necessary
182189
};
183190

184191
window.addEventListener('resize', handleWindowResize);
@@ -188,7 +195,6 @@ export const Preview = memo(() => {
188195
};
189196
}, []);
190197

191-
// A small helper component for the handle's "grip" icon
192198
const GripIcon = () => (
193199
<div
194200
style={{
@@ -213,8 +219,33 @@ export const Preview = memo(() => {
213219
</div>
214220
);
215221

222+
const openInNewWindow = (size: WindowSize) => {
223+
if (activePreview?.baseUrl) {
224+
const match = activePreview.baseUrl.match(/^https?:\/\/([^.]+)\.local-credentialless\.webcontainer-api\.io/);
225+
226+
if (match) {
227+
const previewId = match[1];
228+
const previewUrl = `/webcontainer/preview/${previewId}`;
229+
const newWindow = window.open(
230+
previewUrl,
231+
'_blank',
232+
`noopener,noreferrer,width=${size.width},height=${size.height},menubar=no,toolbar=no,location=no,status=no`,
233+
);
234+
235+
if (newWindow) {
236+
newWindow.focus();
237+
}
238+
} else {
239+
console.warn('[Preview] Invalid WebContainer URL:', activePreview.baseUrl);
240+
}
241+
}
242+
};
243+
216244
return (
217-
<div ref={containerRef} className="w-full h-full flex flex-col relative">
245+
<div
246+
ref={containerRef}
247+
className={`w-full h-full flex flex-col relative ${isPreviewOnly ? 'fixed inset-0 z-50 bg-white' : ''}`}
248+
>
218249
{isPortDropdownOpen && (
219250
<div className="z-iframe-overlay w-full h-full absolute" onClick={() => setIsPortDropdownOpen(false)} />
220251
)}
@@ -225,10 +256,7 @@ export const Preview = memo(() => {
225256
onClick={() => setIsSelectionMode(!isSelectionMode)}
226257
className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
227258
/>
228-
<div
229-
className="flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive
230-
focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
231-
>
259+
<div className="flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive">
232260
<input
233261
title="URL"
234262
ref={inputRef}
@@ -261,26 +289,65 @@ export const Preview = memo(() => {
261289
/>
262290
)}
263291

264-
{/* Device mode toggle button */}
265292
<IconButton
266293
icon="i-ph:devices"
267294
onClick={toggleDeviceMode}
268295
title={isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode'}
269296
/>
270297

271-
{/* Fullscreen toggle button */}
298+
<IconButton
299+
icon="i-ph:layout-light"
300+
onClick={() => setIsPreviewOnly(!isPreviewOnly)}
301+
title={isPreviewOnly ? 'Show Full Interface' : 'Show Preview Only'}
302+
/>
303+
272304
<IconButton
273305
icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
274306
onClick={toggleFullscreen}
275307
title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
276308
/>
309+
310+
<div className="relative">
311+
<IconButton
312+
icon="i-ph:arrow-square-out"
313+
onClick={() => openInNewWindow(selectedWindowSize)}
314+
title={`Open Preview in ${selectedWindowSize.name} Window`}
315+
/>
316+
<IconButton
317+
icon="i-ph:caret-down"
318+
onClick={() => setIsWindowSizeDropdownOpen(!isWindowSizeDropdownOpen)}
319+
className="ml-1"
320+
title="Select Window Size"
321+
/>
322+
323+
{isWindowSizeDropdownOpen && (
324+
<>
325+
<div className="fixed inset-0 z-50" onClick={() => setIsWindowSizeDropdownOpen(false)} />
326+
<div className="absolute right-0 top-full mt-1 z-50 bg-bolt-elements-background-depth-2 rounded-lg shadow-lg border border-bolt-elements-borderColor overflow-hidden">
327+
{WINDOW_SIZES.map((size) => (
328+
<button
329+
key={size.name}
330+
className="w-full px-4 py-2 text-left hover:bg-bolt-elements-background-depth-3 text-sm whitespace-nowrap"
331+
onClick={() => {
332+
setSelectedWindowSize(size);
333+
setIsWindowSizeDropdownOpen(false);
334+
openInNewWindow(size);
335+
}}
336+
>
337+
{size.name}
338+
</button>
339+
))}
340+
</div>
341+
</>
342+
)}
343+
</div>
277344
</div>
278345

279346
<div className="flex-1 border-t border-bolt-elements-borderColor flex justify-center items-center overflow-auto">
280347
<div
281348
style={{
282349
width: isDeviceModeOn ? `${widthPercent}%` : '100%',
283-
height: '100%', // Always full height
350+
height: '100%',
284351
overflow: 'visible',
285352
background: '#fff',
286353
position: 'relative',
@@ -294,7 +361,8 @@ export const Preview = memo(() => {
294361
title="preview"
295362
className="border-none w-full h-full bg-white"
296363
src={iframeUrl}
297-
allowFullScreen
364+
sandbox="allow-scripts allow-forms allow-popups allow-modals allow-storage-access-by-user-activation allow-same-origin"
365+
allow="cross-origin-isolated"
298366
/>
299367
<ScreenshotSelector
300368
isSelectionMode={isSelectionMode}
@@ -308,7 +376,6 @@ export const Preview = memo(() => {
308376

309377
{isDeviceModeOn && (
310378
<>
311-
{/* Left handle */}
312379
<div
313380
onMouseDown={(e) => startResizing(e, 'left')}
314381
style={{
@@ -333,7 +400,6 @@ export const Preview = memo(() => {
333400
<GripIcon />
334401
</div>
335402

336-
{/* Right handle */}
337403
<div
338404
onMouseDown={(e) => startResizing(e, 'right')}
339405
style={{

0 commit comments

Comments
 (0)