"use client"; import type { DesktopPreviewColorScheme } from "@t3tools/contracts"; import { Minus, MoreVertical, Plus as PlusIcon, RotateCcw } from "lucide-react"; import { Button } from "~/components/ui/button"; import { Menu, MenuItem, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSub, MenuSubPopup, MenuSubTrigger, MenuTrigger, } from "~/components/ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; import { previewBridge } from "./previewBridge"; const COLOR_SCHEME_OPTIONS: ReadonlyArray<{ value: DesktopPreviewColorScheme; label: string; }> = [ { value: "system", label: "System" }, { value: "light", label: "Light" }, { value: "dark", label: "Dark" }, ]; 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; /** Emulated `prefers-color-scheme` for the guest page. */ colorScheme: DesktopPreviewColorScheme; /** Fixed viewport modes expose the device toolbar and resize rails. */ deviceToolbarVisible: boolean; /** Switches between fill-panel mode and a fixed responsive viewport. */ onToggleDeviceToolbar: () => void; /** Whether the separate native always-on-top preview window is open. */ nativePictureInPicture: boolean; /** Toggles the optional native always-on-top preview window. */ onNativePictureInPicture: () => 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, colorScheme, deviceToolbarVisible, onToggleDeviceToolbar, nativePictureInPicture, onNativePictureInPicture, }: Props) { if (!previewBridge) return null; const bridge = previewBridge; const tabDisabled = !tabId || !hasWebContents; const callTab = (op: (tabId: string) => Promise) => () => { if (!tabId) return; void op(tabId).catch(() => undefined); }; const zoomLabel = `${Math.round(zoomFactor * 100)}%`; return ( } /> } > More Hard reload Open DevTools {nativePictureInPicture ? "Close separate preview window" : "Open separate preview window"} {deviceToolbarVisible ? "Hide device toolbar" : "Show device toolbar"} Appearance { if (!tabId) return; void bridge .setColorScheme(tabId, value as DesktopPreviewColorScheme) .catch(() => undefined); }} > {COLOR_SCHEME_OPTIONS.map((option) => ( {option.label} ))} {/* Zoom row: label + inline control cluster. `closeOnClick=false` keeps the menu open while the user clicks the +/− buttons. */} event.preventDefault()} className="justify-between" disabled={tabDisabled} > Zoom {zoomLabel} void bridge.clearCookies().catch(() => undefined)}> Clear cookies void bridge.clearCache().catch(() => undefined)}> Clear cache ); }