"use client"; import { Minus, MoreVertical, Plus as PlusIcon, RotateCcw } from "lucide-react"; import { Button } from "~/components/ui/button";import { Menu, MenuItem, MenuPopup, MenuSeparator, MenuTrigger } from "~/components/ui/menu";import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; import { previewBridge } from "./previewBridge"; interface Props { /** Active preview tab id. Tab-targeting actions are disabled without it. */ tabId: string | null; /** * True only after the desktop bridge has registered a `webContentsId` for * the active tab. Tab-targeting actions throw on the desktop side until * then; we disable those items so the menu doesn't fire silent no-ops. */ hasWebContents: boolean; /** Current zoom factor as a number (1.0 = 100%). */ zoomFactor: number; /** Fixed viewport modes expose the device toolbar and resize rails. */ deviceToolbarVisible: boolean; /** Switches between fill-panel mode and a fixed responsive viewport. */ onToggleDeviceToolbar: () => void;} /** * Three-dot menu in the chrome row. Wires Hard reload, DevTools, zoom * controls, and storage-clearing actions. Only mounted by `PreviewView` * when the desktop bridge is present, so we can call it unconditionally. */export function PreviewMoreMenu({ tabId, hasWebContents, zoomFactor, deviceToolbarVisible, onToggleDeviceToolbar,}: Props) { if (!previewBridge) return null; const bridge = previewBridge; const tabDisabled = !tabId || !hasWebContents; const callTab = (op: (tabId: string) => Promise<void>) => () => { if (!tabId) return; void op(tabId).catch(() => undefined); }; const zoomLabel = `${Math.round(zoomFactor * 100)}%`; return ( <Menu> <Tooltip> <TooltipTrigger render={ <MenuTrigger render={ <Button variant="ghost" size="icon-xs" type="button" aria-label="Preview menu" /> } /> } > <MoreVertical /> </TooltipTrigger> <TooltipPopup>More</TooltipPopup> </Tooltip> <MenuPopup align="end" sideOffset={6} className="min-w-56"> <MenuItem onClick={callTab(bridge.hardReload)} disabled={tabDisabled}> Hard reload </MenuItem> <MenuItem onClick={callTab(bridge.openDevTools)} disabled={tabDisabled}> Open DevTools </MenuItem> <MenuItem onClick={onToggleDeviceToolbar} disabled={tabDisabled}> {deviceToolbarVisible ? "Hide device toolbar" : "Show device toolbar"} </MenuItem> <MenuSeparator /> {/* Zoom row: label + inline control cluster. `closeOnClick=false` keeps the menu open while the user clicks the +/− buttons. */} <MenuItem closeOnClick={false} onClick={(event: React.MouseEvent) => event.preventDefault()} className="justify-between" disabled={tabDisabled} > <span>Zoom</span> <span className="flex items-center gap-1"> <Button variant="outline" size="icon-xs" type="button" onClick={callTab(bridge.zoomOut)} aria-label="Zoom out" disabled={tabDisabled} > <Minus /> </Button> <span className="min-w-12 text-center text-xs tabular-nums text-muted-foreground"> {zoomLabel} </span> <Button variant="outline" size="icon-xs" type="button" onClick={callTab(bridge.zoomIn)} aria-label="Zoom in" disabled={tabDisabled} > <PlusIcon /> </Button> <Button variant="ghost" size="icon-xs" type="button" onClick={callTab(bridge.resetZoom)} aria-label="Reset zoom" disabled={tabDisabled} > <RotateCcw /> </Button> </span> </MenuItem> <MenuSeparator /> <MenuItem onClick={() => void bridge.clearCookies().catch(() => undefined)}> Clear cookies </MenuItem> <MenuItem onClick={() => void bridge.clearCache().catch(() => undefined)}> Clear cache </MenuItem> </MenuPopup> </Menu> );}
"use client";
import { Minus, MoreVertical, Plus as PlusIcon, RotateCcw } from "lucide-react";
import { Button } from "~/components/ui/button";
import { Menu, MenuItem, MenuPopup, MenuSeparator, MenuTrigger } from "~/components/ui/menu";
import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
import { previewBridge } from "./previewBridge";
interface Props {
/** Active preview tab id. Tab-targeting actions are disabled without it. */
tabId: string | null;
/**
* True only after the desktop bridge has registered a `webContentsId` for
* the active tab. Tab-targeting actions throw on the desktop side until
* then; we disable those items so the menu doesn't fire silent no-ops.
*/
hasWebContents: boolean;
/** Current zoom factor as a number (1.0 = 100%). */
zoomFactor: number;
/** Fixed viewport modes expose the device toolbar and resize rails. */
deviceToolbarVisible: boolean;
/** Switches between fill-panel mode and a fixed responsive viewport. */
onToggleDeviceToolbar: () => void;
}
/**
* Three-dot menu in the chrome row. Wires Hard reload, DevTools, zoom
* controls, and storage-clearing actions. Only mounted by `PreviewView`
* when the desktop bridge is present, so we can call it unconditionally.
*/
export function PreviewMoreMenu({
tabId,
hasWebContents,
zoomFactor,
deviceToolbarVisible,
onToggleDeviceToolbar,
}: Props) {
if (!previewBridge) return null;
const bridge = previewBridge;
const tabDisabled = !tabId || !hasWebContents;
const callTab = (op: (tabId: string) => Promise<void>) => () => {
if (!tabId) return;
void op(tabId).catch(() => undefined);
};
const zoomLabel = `${Math.round(zoomFactor * 100)}%`;
return (
<Menu>
<Tooltip>
<TooltipTrigger
render={
<MenuTrigger
render={
<Button variant="ghost" size="icon-xs" type="button" aria-label="Preview menu" />
}
/>
}
>
<MoreVertical />
</TooltipTrigger>
<TooltipPopup>More</TooltipPopup>
</Tooltip>
<MenuPopup align="end" sideOffset={6} className="min-w-56">
<MenuItem onClick={callTab(bridge.hardReload)} disabled={tabDisabled}>
Hard reload
</MenuItem>
<MenuItem onClick={callTab(bridge.openDevTools)} disabled={tabDisabled}>
Open DevTools
</MenuItem>
<MenuItem onClick={onToggleDeviceToolbar} disabled={tabDisabled}>
{deviceToolbarVisible ? "Hide device toolbar" : "Show device toolbar"}
</MenuItem>
<MenuSeparator />
{/*
Zoom row: label + inline control cluster. `closeOnClick=false`
keeps the menu open while the user clicks the +/− buttons.
*/}
<MenuItem
closeOnClick={false}
onClick={(event: React.MouseEvent) => event.preventDefault()}
className="justify-between"
disabled={tabDisabled}
>
<span>Zoom</span>
<span className="flex items-center gap-1">
<Button
variant="outline"
size="icon-xs"
type="button"
onClick={callTab(bridge.zoomOut)}
aria-label="Zoom out"
disabled={tabDisabled}
>
<Minus />
</Button>
<span className="min-w-12 text-center text-xs tabular-nums text-muted-foreground">
{zoomLabel}
</span>
<Button
variant="outline"
size="icon-xs"
type="button"
onClick={callTab(bridge.zoomIn)}
aria-label="Zoom in"
disabled={tabDisabled}
>
<PlusIcon />
</Button>
<Button
variant="ghost"
size="icon-xs"
type="button"
onClick={callTab(bridge.resetZoom)}
aria-label="Reset zoom"
disabled={tabDisabled}
>
<RotateCcw />
</Button>
</span>
</MenuItem>
<MenuSeparator />
<MenuItem onClick={() => void bridge.clearCookies().catch(() => undefined)}>
Clear cookies
</MenuItem>
<MenuItem onClick={() => void bridge.clearCache().catch(() => undefined)}>
Clear cache
</MenuItem>
</MenuPopup>
</Menu>
);
}