feat: allow toggling between screen sizes (#1582)

Implement the feature requested in issue #251 that allows users to
toggle between screen sizes
This commit is contained in:
Mohamed Aziz Mejri
2025-10-23 05:14:39 +01:00
committed by GitHub
parent b1095b7951
commit 517ce5134d
2 changed files with 204 additions and 13 deletions

View File

@@ -0,0 +1,79 @@
import { test, testSkipIfWindows, Timeout } from "./helpers/test_helper";
import { expect } from "@playwright/test";
test.describe("Toggle Screen Size Tests", () => {
async function setupApp(po: any) {
await po.setUp({ autoApprove: true });
await po.sendPrompt("tc=write-index");
const iframe = po.getPreviewIframeElement();
const frame = await iframe.contentFrame();
await expect(frame.getByText("Testing:write-index!")).toBeVisible({
timeout: Timeout.EXTRA_LONG,
});
}
testSkipIfWindows(
"should open and close device mode popover",
async ({ po }) => {
test.setTimeout(Timeout.EXTRA_LONG * 1.5);
await setupApp(po);
// Click the device mode button to open popover
const deviceModeButton = po.page.locator(
'[data-testid="device-mode-button"]',
);
await deviceModeButton.click();
// Verify popover is visible with device options
const originalButton = po.page.locator('[aria-label="Desktop view"]');
await expect(originalButton).toBeVisible();
// Close popover by clicking the button again
await deviceModeButton.click();
// Verify popover is closed
await expect(originalButton).toBeHidden();
},
);
testSkipIfWindows("should switch between device modes", async ({ po }) => {
test.setTimeout(Timeout.EXTRA_LONG * 1.5);
await setupApp(po);
const deviceModeButton = po.page.locator(
'[data-testid="device-mode-button"]',
);
const previewIframe = po.page.locator(
'[data-testid="preview-iframe-element"]',
);
// Switch to tablet mode
await deviceModeButton.click();
await po.page.locator('[aria-label="Tablet view"]').click();
// Wait for the iframe width to change to tablet size (768px)
await expect(previewIframe).toHaveAttribute("style", /width:\s*768px/);
// Verify iframe has tablet dimensions
const tabletWidth = await previewIframe.evaluate((el: HTMLIFrameElement) =>
el.style.width.replace("px", ""),
);
expect(tabletWidth).toBe("768");
// Switch to mobile mode
await deviceModeButton.click();
await po.page.locator('[aria-label="Mobile view"]').click();
// Wait for the iframe width to change to mobile size (375px)
await expect(previewIframe).toHaveAttribute("style", /width:\s*375px/);
// Verify iframe has mobile dimensions
const mobileWidth = await previewIframe.evaluate((el: HTMLIFrameElement) =>
el.style.width.replace("px", ""),
);
expect(mobileWidth).toBe("375");
});
});

View File

@@ -19,6 +19,10 @@ import {
ChevronRight,
MousePointerClick,
Power,
MonitorSmartphone,
Monitor,
Tablet,
Smartphone,
} from "lucide-react";
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
import { IpcClient } from "@/ipc/ipc_client";
@@ -39,6 +43,12 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { useRunApp } from "@/hooks/useRunApp";
import { useShortcut } from "@/hooks/useShortcut";
import { cn } from "@/lib/utils";
@@ -165,6 +175,17 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [isPicking, setIsPicking] = useState(false);
// Device mode state
type DeviceMode = "desktop" | "tablet" | "mobile";
const [deviceMode, setDeviceMode] = useState<DeviceMode>("desktop");
const [isDevicePopoverOpen, setIsDevicePopoverOpen] = useState(false);
// Device configurations
const deviceWidthConfig = {
tablet: 768,
mobile: 375,
};
//detect if the user is using Mac
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
@@ -547,6 +568,85 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
>
<ExternalLink size={16} />
</button>
{/* Device Mode Button */}
<Popover open={isDevicePopoverOpen} modal={false}>
<PopoverTrigger asChild>
<button
data-testid="device-mode-button"
onClick={() => {
// Toggle popover open/close
if (isDevicePopoverOpen) setDeviceMode("desktop");
setIsDevicePopoverOpen(!isDevicePopoverOpen);
}}
className={cn(
"p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-gray-300",
deviceMode !== "desktop" && "bg-gray-200 dark:bg-gray-700",
)}
title="Device Mode"
>
<MonitorSmartphone size={16} />
</button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-2"
onOpenAutoFocus={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
>
<TooltipProvider>
<ToggleGroup
type="single"
value={deviceMode}
onValueChange={(value) => {
if (value) {
setDeviceMode(value as DeviceMode);
setIsDevicePopoverOpen(false);
}
}}
variant="outline"
>
{/* Tooltips placed inside items instead of wrapping
to avoid asChild prop merging that breaks highlighting */}
<ToggleGroupItem value="desktop" aria-label="Desktop view">
<Tooltip>
<TooltipTrigger asChild>
<span className="flex items-center justify-center">
<Monitor size={16} />
</span>
</TooltipTrigger>
<TooltipContent>
<p>Desktop</p>
</TooltipContent>
</Tooltip>
</ToggleGroupItem>
<ToggleGroupItem value="tablet" aria-label="Tablet view">
<Tooltip>
<TooltipTrigger asChild>
<span className="flex items-center justify-center">
<Tablet size={16} className="scale-x-130" />
</span>
</TooltipTrigger>
<TooltipContent>
<p>Tablet</p>
</TooltipContent>
</Tooltip>
</ToggleGroupItem>
<ToggleGroupItem value="mobile" aria-label="Mobile view">
<Tooltip>
<TooltipTrigger asChild>
<span className="flex items-center justify-center">
<Smartphone size={16} />
</span>
</TooltipTrigger>
<TooltipContent>
<p>Mobile</p>
</TooltipContent>
</Tooltip>
</ToggleGroupItem>
</ToggleGroup>
</TooltipProvider>
</PopoverContent>
</Popover>
</div>
</div>
@@ -572,6 +672,12 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
</p>
</div>
) : (
<div
className={cn(
"w-full h-full",
deviceMode !== "desktop" && "flex justify-center",
)}
>
<iframe
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-orientation-lock allow-pointer-lock allow-presentation allow-downloads"
data-testid="preview-iframe-element"
@@ -582,9 +688,15 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
key={reloadKey}
title={`Preview for App ${selectedAppId}`}
className="w-full h-full border-none bg-white dark:bg-gray-950"
style={
deviceMode == "desktop"
? {}
: { width: `${deviceWidthConfig[deviceMode]}px` }
}
src={appUrl}
allow="clipboard-read; clipboard-write; fullscreen; microphone; camera; display-capture; geolocation; autoplay; picture-in-picture"
/>
</div>
)}
</div>
</div>