Skip to content

Commit a0eb0a0

Browse files
authored
Merge pull request #550 from wonderwhy-er/pr-549
Added Fullscreen and Resizing to Preview
2 parents 1890c4e + b6eef57 commit a0eb0a0

File tree

1 file changed

+234
-12
lines changed

1 file changed

+234
-12
lines changed

app/components/workbench/Preview.tsx

Lines changed: 234 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,40 @@ import { IconButton } from '~/components/ui/IconButton';
44
import { workbenchStore } from '~/lib/stores/workbench';
55
import { PortDropdown } from './PortDropdown';
66

7+
type ResizeSide = 'left' | 'right' | null;
8+
79
export 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

Comments
 (0)