@@ -4,18 +4,40 @@ import { IconButton } from '~/components/ui/IconButton';
44import { workbenchStore } from '~/lib/stores/workbench' ;
55import { PortDropdown } from './PortDropdown' ;
66
7+ type ResizeSide = 'left' | 'right' | null ;
8+
79export const Preview = memo ( ( ) => {
810 const iframeRef = useRef < HTMLIFrameElement > ( null ) ;
11+ const containerRef = useRef < HTMLDivElement > ( null ) ;
912 const inputRef = useRef < HTMLInputElement > ( null ) ;
13+
1014 const [ activePreviewIndex , setActivePreviewIndex ] = useState ( 0 ) ;
1115 const [ isPortDropdownOpen , setIsPortDropdownOpen ] = useState ( false ) ;
16+ const [ isFullscreen , setIsFullscreen ] = useState ( false ) ;
1217 const hasSelectedPreview = useRef ( false ) ;
1318 const previews = useStore ( workbenchStore . previews ) ;
1419 const activePreview = previews [ activePreviewIndex ] ;
1520
1621 const [ url , setUrl ] = useState ( '' ) ;
1722 const [ iframeUrl , setIframeUrl ] = useState < string | undefined > ( ) ;
1823
24+ // Toggle between responsive mode and device mode
25+ const [ isDeviceModeOn , setIsDeviceModeOn ] = useState ( false ) ;
26+
27+ // Use percentage for width
28+ const [ widthPercent , setWidthPercent ] = useState < number > ( 37.5 ) ; // 375px assuming 1000px window width initially
29+
30+ const resizingState = useRef ( {
31+ isResizing : false ,
32+ side : null as ResizeSide ,
33+ startX : 0 ,
34+ startWidthPercent : 37.5 ,
35+ windowWidth : window . innerWidth ,
36+ } ) ;
37+
38+ // Define the scaling factor
39+ const SCALING_FACTOR = 2 ; // Adjust this value to increase/decrease sensitivity
40+
1941 useEffect ( ( ) => {
2042 if ( ! activePreview ) {
2143 setUrl ( '' ) ;
@@ -25,10 +47,9 @@ export const Preview = memo(() => {
2547 }
2648
2749 const { baseUrl } = activePreview ;
28-
2950 setUrl ( baseUrl ) ;
3051 setIframeUrl ( baseUrl ) ;
31- } , [ activePreview , iframeUrl ] ) ;
52+ } , [ activePreview ] ) ;
3253
3354 const validateUrl = useCallback (
3455 ( value : string ) => {
@@ -56,28 +77,148 @@ export const Preview = memo(() => {
5677 [ ] ,
5778 ) ;
5879
59- // when previews change, display the lowest port if user hasn't selected a preview
80+ // When previews change, display the lowest port if user hasn't selected a preview
6081 useEffect ( ( ) => {
6182 if ( previews . length > 1 && ! hasSelectedPreview . current ) {
6283 const minPortIndex = previews . reduce ( findMinPortIndex , 0 ) ;
63-
6484 setActivePreviewIndex ( minPortIndex ) ;
6585 }
66- } , [ previews ] ) ;
86+ } , [ previews , findMinPortIndex ] ) ;
6787
6888 const reloadPreview = ( ) => {
6989 if ( iframeRef . current ) {
7090 iframeRef . current . src = iframeRef . current . src ;
7191 }
7292 } ;
7393
94+ const toggleFullscreen = async ( ) => {
95+ if ( ! isFullscreen && containerRef . current ) {
96+ await containerRef . current . requestFullscreen ( ) ;
97+ } else if ( document . fullscreenElement ) {
98+ await document . exitFullscreen ( ) ;
99+ }
100+ } ;
101+
102+ useEffect ( ( ) => {
103+ const handleFullscreenChange = ( ) => {
104+ setIsFullscreen ( ! ! document . fullscreenElement ) ;
105+ } ;
106+
107+ document . addEventListener ( 'fullscreenchange' , handleFullscreenChange ) ;
108+
109+ return ( ) => {
110+ document . removeEventListener ( 'fullscreenchange' , handleFullscreenChange ) ;
111+ } ;
112+ } , [ ] ) ;
113+
114+ const toggleDeviceMode = ( ) => {
115+ setIsDeviceModeOn ( ( prev ) => ! prev ) ;
116+ } ;
117+
118+ const startResizing = ( e : React . MouseEvent , side : ResizeSide ) => {
119+ if ( ! isDeviceModeOn ) {
120+ return ;
121+ }
122+
123+ // Prevent text selection
124+ document . body . style . userSelect = 'none' ;
125+
126+ resizingState . current . isResizing = true ;
127+ resizingState . current . side = side ;
128+ resizingState . current . startX = e . clientX ;
129+ resizingState . current . startWidthPercent = widthPercent ;
130+ resizingState . current . windowWidth = window . innerWidth ;
131+
132+ document . addEventListener ( 'mousemove' , onMouseMove ) ;
133+ document . addEventListener ( 'mouseup' , onMouseUp ) ;
134+
135+ e . preventDefault ( ) ; // Prevent any text selection on mousedown
136+ } ;
137+
138+ const onMouseMove = ( e : MouseEvent ) => {
139+ if ( ! resizingState . current . isResizing ) {
140+ return ;
141+ }
142+
143+ const dx = e . clientX - resizingState . current . startX ;
144+ const windowWidth = resizingState . current . windowWidth ;
145+
146+ // Apply scaling factor to increase sensitivity
147+ const dxPercent = ( dx / windowWidth ) * 100 * SCALING_FACTOR ;
148+
149+ let newWidthPercent = resizingState . current . startWidthPercent ;
150+
151+ if ( resizingState . current . side === 'right' ) {
152+ newWidthPercent = resizingState . current . startWidthPercent + dxPercent ;
153+ } else if ( resizingState . current . side === 'left' ) {
154+ newWidthPercent = resizingState . current . startWidthPercent - dxPercent ;
155+ }
156+
157+ // Clamp the width between 10% and 90%
158+ newWidthPercent = Math . max ( 10 , Math . min ( newWidthPercent , 90 ) ) ;
159+
160+ setWidthPercent ( newWidthPercent ) ;
161+ } ;
162+
163+ const onMouseUp = ( ) => {
164+ resizingState . current . isResizing = false ;
165+ resizingState . current . side = null ;
166+ document . removeEventListener ( 'mousemove' , onMouseMove ) ;
167+ document . removeEventListener ( 'mouseup' , onMouseUp ) ;
168+
169+ // Restore text selection
170+ document . body . style . userSelect = '' ;
171+ } ;
172+
173+ // Handle window resize to ensure widthPercent remains valid
174+ useEffect ( ( ) => {
175+ const handleWindowResize = ( ) => {
176+ /*
177+ * Optional: Adjust widthPercent if necessary
178+ * For now, since widthPercent is relative, no action is needed
179+ */
180+ } ;
181+
182+ window . addEventListener ( 'resize' , handleWindowResize ) ;
183+
184+ return ( ) => {
185+ window . removeEventListener ( 'resize' , handleWindowResize ) ;
186+ } ;
187+ } , [ ] ) ;
188+
189+ // A small helper component for the handle's "grip" icon
190+ const GripIcon = ( ) => (
191+ < div
192+ style = { {
193+ display : 'flex' ,
194+ justifyContent : 'center' ,
195+ alignItems : 'center' ,
196+ height : '100%' ,
197+ pointerEvents : 'none' ,
198+ } }
199+ >
200+ < div
201+ style = { {
202+ color : 'rgba(0,0,0,0.5)' ,
203+ fontSize : '10px' ,
204+ lineHeight : '5px' ,
205+ userSelect : 'none' ,
206+ marginLeft : '1px' ,
207+ } }
208+ >
209+ ••• •••
210+ </ div >
211+ </ div >
212+ ) ;
213+
74214 return (
75- < div className = "w-full h-full flex flex-col" >
215+ < div ref = { containerRef } className = "w-full h-full flex flex-col relative " >
76216 { isPortDropdownOpen && (
77217 < div className = "z-iframe-overlay w-full h-full absolute" onClick = { ( ) => setIsPortDropdownOpen ( false ) } />
78218 ) }
79219 < div className = "bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5" >
80220 < IconButton icon = "i-ph:arrow-clockwise" onClick = { reloadPreview } />
221+
81222 < div
82223 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
83224 focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
@@ -101,6 +242,7 @@ export const Preview = memo(() => {
101242 } }
102243 />
103244 </ div >
245+
104246 { previews . length > 1 && (
105247 < PortDropdown
106248 activePreviewIndex = { activePreviewIndex }
@@ -111,13 +253,93 @@ export const Preview = memo(() => {
111253 previews = { previews }
112254 />
113255 ) }
256+
257+ { /* Device mode toggle button */ }
258+ < IconButton
259+ icon = "i-ph:devices"
260+ onClick = { toggleDeviceMode }
261+ title = { isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode' }
262+ />
263+
264+ { /* Fullscreen toggle button */ }
265+ < IconButton
266+ icon = { isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out' }
267+ onClick = { toggleFullscreen }
268+ title = { isFullscreen ? 'Exit Full Screen' : 'Full Screen' }
269+ />
114270 </ div >
115- < div className = "flex-1 border-t border-bolt-elements-borderColor" >
116- { activePreview ? (
117- < iframe ref = { iframeRef } className = "border-none w-full h-full bg-white" src = { iframeUrl } />
118- ) : (
119- < div className = "flex w-full h-full justify-center items-center bg-white" > No preview available</ div >
120- ) }
271+
272+ < div className = "flex-1 border-t border-bolt-elements-borderColor flex justify-center items-center overflow-auto" >
273+ < div
274+ style = { {
275+ width : isDeviceModeOn ? `${ widthPercent } %` : '100%' ,
276+ height : '100%' , // Always full height
277+ overflow : 'visible' ,
278+ background : '#fff' ,
279+ position : 'relative' ,
280+ display : 'flex' ,
281+ } }
282+ >
283+ { activePreview ? (
284+ < iframe ref = { iframeRef } className = "border-none w-full h-full bg-white" src = { iframeUrl } allowFullScreen />
285+ ) : (
286+ < div className = "flex w-full h-full justify-center items-center bg-white" > No preview available</ div >
287+ ) }
288+
289+ { isDeviceModeOn && (
290+ < >
291+ { /* Left handle */ }
292+ < div
293+ onMouseDown = { ( e ) => startResizing ( e , 'left' ) }
294+ style = { {
295+ position : 'absolute' ,
296+ top : 0 ,
297+ left : 0 ,
298+ width : '15px' ,
299+ marginLeft : '-15px' ,
300+ height : '100%' ,
301+ cursor : 'ew-resize' ,
302+ background : 'rgba(255,255,255,.2)' ,
303+ display : 'flex' ,
304+ alignItems : 'center' ,
305+ justifyContent : 'center' ,
306+ transition : 'background 0.2s' ,
307+ userSelect : 'none' ,
308+ } }
309+ onMouseOver = { ( e ) => ( e . currentTarget . style . background = 'rgba(255,255,255,.5)' ) }
310+ onMouseOut = { ( e ) => ( e . currentTarget . style . background = 'rgba(255,255,255,.2)' ) }
311+ title = "Drag to resize width"
312+ >
313+ < GripIcon />
314+ </ div >
315+
316+ { /* Right handle */ }
317+ < div
318+ onMouseDown = { ( e ) => startResizing ( e , 'right' ) }
319+ style = { {
320+ position : 'absolute' ,
321+ top : 0 ,
322+ right : 0 ,
323+ width : '15px' ,
324+ marginRight : '-15px' ,
325+ height : '100%' ,
326+ cursor : 'ew-resize' ,
327+ background : 'rgba(255,255,255,.2)' ,
328+ display : 'flex' ,
329+ alignItems : 'center' ,
330+ justifyContent : 'center' ,
331+ transition : 'background 0.2s' ,
332+ userSelect : 'none' ,
333+ } }
334+ onMouseOver = { ( e ) => ( e . currentTarget . style . background = 'rgba(255,255,255,.5)' ) }
335+ onMouseOut = { ( e ) => ( e . currentTarget . style . background = 'rgba(255,255,255,.2)' ) }
336+ title = "Drag to resize width"
337+ >
338+ < GripIcon />
339+ </ div >
340+ </ >
341+ ) }
342+ </ div >
121343 </ div >
122344 </ div >
123345 ) ;
0 commit comments