1- import { useStore } from '@nanostores/react' ;
21import { memo , useCallback , useEffect , useRef , useState } from 'react' ;
2+ import { useStore } from '@nanostores/react' ;
33import { IconButton } from '~/components/ui/IconButton' ;
44import { workbenchStore } from '~/lib/stores/workbench' ;
55import { PortDropdown } from './PortDropdown' ;
66import { ScreenshotSelector } from './ScreenshotSelector' ;
77
88type 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+
1023export 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 ( / ^ h t t p s ? : \/ \/ ( [ ^ . ] + ) \. l o c a l - c r e d e n t i a l l e s s \. w e b c o n t a i n e r - a p i \. i o / ) ;
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