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:
committed by
GitHub
parent
b1095b7951
commit
517ce5134d
79
e2e-tests/toggle_screen_sizes.spec.ts
Normal file
79
e2e-tests/toggle_screen_sizes.spec.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,6 +19,10 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
MousePointerClick,
|
MousePointerClick,
|
||||||
Power,
|
Power,
|
||||||
|
MonitorSmartphone,
|
||||||
|
Monitor,
|
||||||
|
Tablet,
|
||||||
|
Smartphone,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
|
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
@@ -39,6 +43,12 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} 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 { useRunApp } from "@/hooks/useRunApp";
|
||||||
import { useShortcut } from "@/hooks/useShortcut";
|
import { useShortcut } from "@/hooks/useShortcut";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -165,6 +175,17 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
|||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
const [isPicking, setIsPicking] = useState(false);
|
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
|
//detect if the user is using Mac
|
||||||
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||||
|
|
||||||
@@ -547,6 +568,85 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
|||||||
>
|
>
|
||||||
<ExternalLink size={16} />
|
<ExternalLink size={16} />
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -572,19 +672,31 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<iframe
|
<div
|
||||||
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-orientation-lock allow-pointer-lock allow-presentation allow-downloads"
|
className={cn(
|
||||||
data-testid="preview-iframe-element"
|
"w-full h-full",
|
||||||
onLoad={() => {
|
deviceMode !== "desktop" && "flex justify-center",
|
||||||
setErrorMessage(undefined);
|
)}
|
||||||
}}
|
>
|
||||||
ref={iframeRef}
|
<iframe
|
||||||
key={reloadKey}
|
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-orientation-lock allow-pointer-lock allow-presentation allow-downloads"
|
||||||
title={`Preview for App ${selectedAppId}`}
|
data-testid="preview-iframe-element"
|
||||||
className="w-full h-full border-none bg-white dark:bg-gray-950"
|
onLoad={() => {
|
||||||
src={appUrl}
|
setErrorMessage(undefined);
|
||||||
allow="clipboard-read; clipboard-write; fullscreen; microphone; camera; display-capture; geolocation; autoplay; picture-in-picture"
|
}}
|
||||||
/>
|
ref={iframeRef}
|
||||||
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user