Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Fullscreen and Resizing to Preview #549

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 240 additions & 18 deletions app/components/workbench/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,57 @@ import { IconButton } from '~/components/ui/IconButton';
import { workbenchStore } from '~/lib/stores/workbench';
import { PortDropdown } from './PortDropdown';

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

export const Preview = memo(() => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);

const [activePreviewIndex, setActivePreviewIndex] = useState(0);
const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const hasSelectedPreview = useRef(false);
const previews = useStore(workbenchStore.previews);
const activePreview = previews[activePreviewIndex];

const [url, setUrl] = useState('');
const [iframeUrl, setIframeUrl] = useState<string | undefined>();

// Toggle between responsive mode and device mode
const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);

// Use percentage for width
const [widthPercent, setWidthPercent] = useState<number>(37.5); // 375px assuming 1000px window width initially
// Height is always 100%
const height = '100%';

const resizingState = useRef({
isResizing: false,
side: null as ResizeSide,
startX: 0,
startWidthPercent: 37.5,
windowWidth: window.innerWidth,
});

// Define the scaling factor
const SCALING_FACTOR = 2; // Adjust this value to increase/decrease sensitivity

useEffect(() => {
if (!activePreview) {
setUrl('');
setIframeUrl(undefined);

return;
}

const { baseUrl } = activePreview;

setUrl(baseUrl);
setIframeUrl(baseUrl);
}, [activePreview, iframeUrl]);
}, [activePreview]);

const validateUrl = useCallback(
(value: string) => {
if (!activePreview) {
return false;
}

if (!activePreview) return false;
const { baseUrl } = activePreview;

if (value === baseUrl) {
Expand All @@ -56,28 +75,143 @@ export const Preview = memo(() => {
[],
);

// when previews change, display the lowest port if user hasn't selected a preview
// When previews change, display the lowest port if user hasn't selected a preview
useEffect(() => {
if (previews.length > 1 && !hasSelectedPreview.current) {
const minPortIndex = previews.reduce(findMinPortIndex, 0);

setActivePreviewIndex(minPortIndex);
}
}, [previews]);
}, [previews, findMinPortIndex]);

const reloadPreview = () => {
if (iframeRef.current) {
iframeRef.current.src = iframeRef.current.src;
}
};

const toggleFullscreen = async () => {
if (!isFullscreen && containerRef.current) {
await containerRef.current.requestFullscreen();
} else if (document.fullscreenElement) {
await document.exitFullscreen();
}
};

useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};

document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
};
}, []);

const toggleDeviceMode = () => {
setIsDeviceModeOn((prev) => !prev);
};

const startResizing = (e: React.MouseEvent, side: ResizeSide) => {
if (!isDeviceModeOn) return;

// Prevent text selection
document.body.style.userSelect = 'none';

resizingState.current.isResizing = true;
resizingState.current.side = side;
resizingState.current.startX = e.clientX;
resizingState.current.startWidthPercent = widthPercent;
resizingState.current.windowWidth = window.innerWidth;

document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);

e.preventDefault(); // Prevent any text selection on mousedown
};

const onMouseMove = (e: MouseEvent) => {
if (!resizingState.current.isResizing) return;
const dx = e.clientX - resizingState.current.startX;
const windowWidth = resizingState.current.windowWidth;

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

let newWidthPercent = resizingState.current.startWidthPercent;

if (resizingState.current.side === 'right') {
newWidthPercent = resizingState.current.startWidthPercent + dxPercent;
} else if (resizingState.current.side === 'left') {
newWidthPercent = resizingState.current.startWidthPercent - dxPercent;
}

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

setWidthPercent(newWidthPercent);
};

const onMouseUp = () => {
resizingState.current.isResizing = false;
resizingState.current.side = null;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);

// Restore text selection
document.body.style.userSelect = '';
};

// Handle window resize to ensure widthPercent remains valid
useEffect(() => {
const handleWindowResize = () => {
// Optional: Adjust widthPercent if necessary
// For now, since widthPercent is relative, no action is needed
};

window.addEventListener('resize', handleWindowResize);
return () => {
window.removeEventListener('resize', handleWindowResize);
};
}, []);

// A small helper component for the handle's "grip" icon
const GripIcon = () => (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
pointerEvents: 'none',
}}
>
<div
style={{
color: 'rgba(0,0,0,0.5)',
fontSize: '10px',
lineHeight: '5px',
userSelect: 'none',
marginLeft: '1px',
}}
>
•••
•••
</div>
</div>
);

return (
<div className="w-full h-full flex flex-col">
<div ref={containerRef} className="w-full h-full flex flex-col relative">
{isPortDropdownOpen && (
<div className="z-iframe-overlay w-full h-full absolute" onClick={() => setIsPortDropdownOpen(false)} />
<div
className="z-iframe-overlay w-full h-full absolute"
onClick={() => setIsPortDropdownOpen(false)}
/>
)}
<div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5">
<IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />

<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"
Expand All @@ -101,6 +235,7 @@ export const Preview = memo(() => {
}}
/>
</div>

{previews.length > 1 && (
<PortDropdown
activePreviewIndex={activePreviewIndex}
Expand All @@ -111,13 +246,100 @@ export const Preview = memo(() => {
previews={previews}
/>
)}

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

{/* Fullscreen toggle button */}
<IconButton
icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
onClick={toggleFullscreen}
title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
/>
</div>
<div className="flex-1 border-t border-bolt-elements-borderColor">
{activePreview ? (
<iframe ref={iframeRef} className="border-none w-full h-full bg-white" src={iframeUrl} />
) : (
<div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
)}

<div className="flex-1 border-t border-bolt-elements-borderColor flex justify-center items-center overflow-auto">
<div
style={{
width: isDeviceModeOn ? `${widthPercent}%` : '100%',
height: '100%', // Always full height
overflow: 'visible',
background: '#fff',
position: 'relative',
display: 'flex',
}}
>
{activePreview ? (
<iframe
ref={iframeRef}
className="border-none w-full h-full bg-white"
src={iframeUrl}
allowFullScreen
/>
) : (
<div className="flex w-full h-full justify-center items-center bg-white">
No preview available
</div>
)}

{isDeviceModeOn && (
<>
{/* Left handle */}
<div
onMouseDown={(e) => startResizing(e, 'left')}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '15px',
marginLeft: '-15px',
height: '100%',
cursor: 'ew-resize',
background: 'rgba(255,255,255,.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'background 0.2s',
userSelect: 'none',
}}
onMouseOver={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.5)')}
onMouseOut={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.2)')}
title="Drag to resize width"
>
<GripIcon />
</div>

{/* Right handle */}
<div
onMouseDown={(e) => startResizing(e, 'right')}
style={{
position: 'absolute',
top: 0,
right: 0,
width: '15px',
marginRight: '-15px',
height: '100%',
cursor: 'ew-resize',
background: 'rgba(255,255,255,.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'background 0.2s',
userSelect: 'none',
}}
onMouseOver={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.5)')}
onMouseOut={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,.2)')}
title="Drag to resize width"
>
<GripIcon />
</div>
</>
)}
</div>
</div>
</div>
);
Expand Down
Loading