mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
feat: add Expo QR code generation and modal for mobile preview
Introduce Expo QR code functionality to allow users to preview their projects on mobile devices. Added a new QR code modal component, integrated it into the chat and preview components, and implemented Expo URL detection in the shell process. This enhances the mobile development workflow by providing a seamless way to test Expo projects directly on devices. - Clean up and consolidate Preview icon buttons while removing redundant ones.
This commit is contained in:
@@ -4,6 +4,8 @@ import { IconButton } from '~/components/ui/IconButton';
|
||||
import { workbenchStore } from '~/lib/stores/workbench';
|
||||
import { PortDropdown } from './PortDropdown';
|
||||
import { ScreenshotSelector } from './ScreenshotSelector';
|
||||
import { expoUrlAtom } from '~/stores/qrCodeStore';
|
||||
import { ExpoQrModal } from '~/components/workbench/ExpoQrModal';
|
||||
|
||||
type ResizeSide = 'left' | 'right' | null;
|
||||
|
||||
@@ -53,7 +55,6 @@ export const Preview = memo(() => {
|
||||
const [activePreviewIndex, setActivePreviewIndex] = useState(0);
|
||||
const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [isPreviewOnly, setIsPreviewOnly] = useState(false);
|
||||
const hasSelectedPreview = useRef(false);
|
||||
const previews = useStore(workbenchStore.previews);
|
||||
const activePreview = previews[activePreviewIndex];
|
||||
@@ -86,6 +87,8 @@ export const Preview = memo(() => {
|
||||
const [isLandscape, setIsLandscape] = useState(false);
|
||||
const [showDeviceFrame, setShowDeviceFrame] = useState(true);
|
||||
const [showDeviceFrameInPreview, setShowDeviceFrameInPreview] = useState(false);
|
||||
const expoUrl = useStore(expoUrlAtom);
|
||||
const [isExpoQrModalOpen, setIsExpoQrModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activePreview) {
|
||||
@@ -636,10 +639,7 @@ export const Preview = memo(() => {
|
||||
}, [showDeviceFrameInPreview]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`w-full h-full flex flex-col relative ${isPreviewOnly ? 'fixed inset-0 z-50 bg-white' : ''}`}
|
||||
>
|
||||
<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)} />
|
||||
)}
|
||||
@@ -693,6 +693,10 @@ export const Preview = memo(() => {
|
||||
title={isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode'}
|
||||
/>
|
||||
|
||||
{expoUrl && <IconButton icon="i-ph:qr-code" onClick={() => setIsExpoQrModalOpen(true)} title="Show QR" />}
|
||||
|
||||
<ExpoQrModal open={isExpoQrModalOpen} onClose={() => setIsExpoQrModalOpen(false)} />
|
||||
|
||||
{isDeviceModeOn && (
|
||||
<>
|
||||
<IconButton
|
||||
@@ -708,60 +712,17 @@ export const Preview = memo(() => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
icon="i-ph:layout-light"
|
||||
onClick={() => setIsPreviewOnly(!isPreviewOnly)}
|
||||
title={isPreviewOnly ? 'Show Full Interface' : 'Show Preview Only'}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
|
||||
onClick={toggleFullscreen}
|
||||
title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
|
||||
/>
|
||||
|
||||
{/* Simple preview button */}
|
||||
<IconButton
|
||||
icon="i-ph:browser"
|
||||
onClick={() => {
|
||||
if (!activePreview?.baseUrl) {
|
||||
console.warn('[Preview] No active preview available');
|
||||
return;
|
||||
}
|
||||
|
||||
const match = activePreview.baseUrl.match(
|
||||
/^https?:\/\/([^.]+)\.local-credentialless\.webcontainer-api\.io/,
|
||||
);
|
||||
|
||||
if (!match) {
|
||||
console.warn('[Preview] Invalid WebContainer URL:', activePreview.baseUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const previewId = match[1];
|
||||
const previewUrl = `/webcontainer/preview/${previewId}`;
|
||||
|
||||
// Open in a new window with simple parameters
|
||||
window.open(
|
||||
previewUrl,
|
||||
`preview-${previewId}`,
|
||||
'width=1280,height=720,menubar=no,toolbar=no,location=no,status=no,resizable=yes',
|
||||
);
|
||||
}}
|
||||
title="Open Preview in New Window"
|
||||
/>
|
||||
|
||||
<div className="flex items-center relative">
|
||||
<IconButton
|
||||
icon="i-ph:arrow-square-out"
|
||||
onClick={() => openInNewWindow(selectedWindowSize)}
|
||||
title={`Open Preview in ${selectedWindowSize.name} Window`}
|
||||
/>
|
||||
<IconButton
|
||||
icon="i-ph:caret-down"
|
||||
icon="i-ph:list"
|
||||
onClick={() => setIsWindowSizeDropdownOpen(!isWindowSizeDropdownOpen)}
|
||||
className="ml-1"
|
||||
title="Select Window Size"
|
||||
title="New Window Options"
|
||||
/>
|
||||
|
||||
{isWindowSizeDropdownOpen && (
|
||||
@@ -770,7 +731,7 @@ export const Preview = memo(() => {
|
||||
<div className="absolute right-0 top-full mt-2 z-50 min-w-[240px] max-h-[400px] overflow-y-auto bg-white dark:bg-black rounded-xl shadow-2xl border border-[#E5E7EB] dark:border-[rgba(255,255,255,0.1)] overflow-hidden">
|
||||
<div className="p-3 border-b border-[#E5E7EB] dark:border-[rgba(255,255,255,0.1)]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-[#111827] dark:text-gray-300">Device Options</span>
|
||||
<span className="text-sm font-medium text-[#111827] dark:text-gray-300">Window Options</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
@@ -782,6 +743,37 @@ export const Preview = memo(() => {
|
||||
<span>Open in new tab</span>
|
||||
<div className="i-ph:arrow-square-out h-5 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className={`flex w-full justify-between items-center text-start bg-transparent text-xs text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary`}
|
||||
onClick={() => {
|
||||
if (!activePreview?.baseUrl) {
|
||||
console.warn('[Preview] No active preview available');
|
||||
return;
|
||||
}
|
||||
|
||||
const match = activePreview.baseUrl.match(
|
||||
/^https?:\/\/([^.]+)\.local-credentialless\.webcontainer-api\.io/,
|
||||
);
|
||||
|
||||
if (!match) {
|
||||
console.warn('[Preview] Invalid WebContainer URL:', activePreview.baseUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const previewId = match[1];
|
||||
const previewUrl = `/webcontainer/preview/${previewId}`;
|
||||
|
||||
// Open in a new window with simple parameters
|
||||
window.open(
|
||||
previewUrl,
|
||||
`preview-${previewId}`,
|
||||
'width=1280,height=720,menubar=no,toolbar=no,location=no,status=no,resizable=yes',
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span>Open in new window</span>
|
||||
<div className="i-ph:browser h-5 w-4" />
|
||||
</button>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-bolt-elements-textTertiary">Show Device Frame</span>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user