189 lines · 6.1 KB · TypeScript
RawDownload
"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<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={onNativePictureInPicture} disabled={tabDisabled}>          {nativePictureInPicture            ? "Close separate preview window"            : "Open separate preview window"}        </MenuItem>        <MenuItem onClick={onToggleDeviceToolbar} disabled={tabDisabled}>          {deviceToolbarVisible ? "Hide device toolbar" : "Show device toolbar"}        </MenuItem>        <MenuSub>          <MenuSubTrigger disabled={tabDisabled}>Appearance</MenuSubTrigger>          <MenuSubPopup className="min-w-32">            <MenuRadioGroup              value={colorScheme}              onValueChange={(value) => {                if (!tabId) return;                void bridge                  .setColorScheme(tabId, value as DesktopPreviewColorScheme)                  .catch(() => undefined);              }}            >              {COLOR_SCHEME_OPTIONS.map((option) => (                <MenuRadioItem key={option.value} value={option.value}>                  {option.label}                </MenuRadioItem>              ))}            </MenuRadioGroup>          </MenuSubPopup>        </MenuSub>        <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"              className="[:hover,[data-pressed]]:bg-foreground/10"              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>  );}