317 lines · 10.4 KB · TypeScript
RawDownload
import {  ArrowLeft,  ArrowRight,  Camera,  ExternalLink,  MousePointerClick,  PictureInPicture2,  RotateCw,} from "lucide-react";import {  type FormEvent,  type KeyboardEvent,  type ReactNode,  useEffect,  useRef,  useState,} from "react"; import { Button } from "~/components/ui/button";import { InputGroup, InputGroupAddon, InputGroupInput } from "~/components/ui/input-group";import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";import { cn } from "~/lib/utils"; interface Props {  url: string;  loading: boolean;  canGoBack: boolean;  canGoForward: boolean;  refreshDisabled: boolean;  inputDisabled?: boolean | undefined;  /** Bumping this value re-focuses and selects the URL input. */  focusUrlNonce?: number | undefined;  onBack: () => void;  onForward: () => void;  onRefresh: () => void;  onSubmit: (url: string) => void;  /** When provided, renders an "Open in browser" affordance to the right. */  onOpenInBrowser?: (() => void) | undefined;  onCapture?: ((record: boolean) => void) | undefined;  captureDisabled?: boolean | undefined;  recording?: boolean | undefined;  onPictureInPicture?: (() => void) | undefined;  pictureInPicture?: boolean | undefined;  pictureInPictureDisabled?: boolean | undefined;  /**   * When provided, renders an annotation-mode toggle button to the right of   * the URL input. Pressed while annotation mode is active (button shows in `pressed`   * state). Disabled in `pickDisabled` mode.   */  onPickElement?: (() => void) | undefined;  pickActive?: boolean | undefined;  pickDisabled?: boolean | undefined;  /** Optional reason string surfaced in the disabled tooltip. */  pickDisabledReason?: string | undefined;  /**   * Trailing slot rendered after the URL input. Used by the preview view   * to mount the three-dot menu (hard reload, devtools, zoom, clear data).   */  trailingActions?: ReactNode;} const NOOP = () => {}; export function PreviewChromeRow({  url,  loading,  canGoBack,  canGoForward,  refreshDisabled,  inputDisabled,  focusUrlNonce,  onBack,  onForward,  onRefresh,  onSubmit,  onOpenInBrowser,  onCapture,  captureDisabled,  recording,  onPictureInPicture,  pictureInPicture,  pictureInPictureDisabled,  onPickElement,  pickActive,  pickDisabled,  pickDisabledReason,  trailingActions,}: Props) {  const inputRef = useRef<HTMLInputElement | null>(null);  const [draft, setDraft] = useState(url);  const [inputFocused, setInputFocused] = useState(false);   useEffect(() => {    if (focusUrlNonce == null) return;    const node = inputRef.current;    if (!node) return;    node.focus();  }, [focusUrlNonce]);   const submit = (event?: FormEvent | KeyboardEvent) => {    event?.preventDefault();    const next = draft.trim();    if (next.length === 0) return;    onSubmit(next);    inputRef.current?.blur();  };   return (    <div className="relative">      <form        onSubmit={submit}        className="flex h-10 min-h-10 shrink-0 items-center gap-1 border-b border-border/60 bg-background px-2 in-data-[preview-panel-mode=inline]:mb-3 in-data-[preview-panel-mode=inline]:h-7 in-data-[preview-panel-mode=inline]:min-h-7 in-data-[preview-panel-mode=inline]:border-b-transparent"        data-surface-subheader      >        <div className="flex items-center gap-0.5" role="group" aria-label="Navigation">          <Tooltip>            <TooltipTrigger              render={                <Button                  variant="ghost"                  size="icon-xs"                  onClick={canGoBack ? onBack : NOOP}                  disabled={!canGoBack}                  aria-label="Back"                  type="button"                />              }            >              <ArrowLeft />            </TooltipTrigger>            <TooltipPopup>Back</TooltipPopup>          </Tooltip>          <Tooltip>            <TooltipTrigger              render={                <Button                  variant="ghost"                  size="icon-xs"                  onClick={canGoForward ? onForward : NOOP}                  disabled={!canGoForward}                  aria-label="Forward"                  type="button"                />              }            >              <ArrowRight />            </TooltipTrigger>            <TooltipPopup>Forward</TooltipPopup>          </Tooltip>          <Tooltip>            <TooltipTrigger              render={                <Button                  variant="ghost"                  size="icon-xs"                  onClick={refreshDisabled ? NOOP : onRefresh}                  disabled={refreshDisabled}                  aria-label={loading ? "Stop" : "Refresh"}                  type="button"                />              }            >              <RotateCw className={cn(loading && "animate-spin")} />            </TooltipTrigger>            <TooltipPopup>{loading ? "Loading…" : "Refresh"}</TooltipPopup>          </Tooltip>        </div>         <InputGroup variant="ghost" className="group/address h-7 flex-1">          <Tooltip>            <TooltipTrigger              render={                <InputGroupInput                  ref={inputRef}                  value={inputFocused ? draft : url}                  className={cn(                    onOpenInBrowser &&                      !inputFocused &&                      "group-hover/address:pe-7 transition-[padding]",                  )}                  onChange={(event) => setDraft(event.target.value)}                  onFocus={() => {                    setDraft(url);                    setInputFocused(true);                    queueMicrotask(() => inputRef.current?.select());                  }}                  onBlur={() => {                    setInputFocused(false);                  }}                  onKeyDown={(event) => {                    if (event.key === "Enter") submit(event);                    if (event.key === "Escape") {                      event.preventDefault();                      setDraft(url);                      inputRef.current?.blur();                    }                  }}                  placeholder="Search or enter URL"                  spellCheck={false}                  disabled={inputDisabled}                  data-preview-url-input                  size="sm"                />              }            />          </Tooltip>          {onOpenInBrowser && !inputFocused ? (            <InputGroupAddon              align="inline-end"              className="pointer-events-none absolute inset-y-0 right-0 opacity-0 transition-opacity group-hover/address:pointer-events-auto group-hover/address:opacity-100"            >              <Tooltip>                <TooltipTrigger                  render={                    <Button                      variant="ghost"                      size="icon-xs"                      onClick={onOpenInBrowser}                      aria-label="Open in system browser"                      type="button"                    />                  }                >                  <ExternalLink />                </TooltipTrigger>                <TooltipPopup>Open in system browser</TooltipPopup>              </Tooltip>            </InputGroupAddon>          ) : null}        </InputGroup>         {onPickElement ? (          <Tooltip>            <TooltipTrigger              render={                <Button                  variant={pickActive ? "secondary" : "ghost"}                  size="icon-xs"                  onClick={onPickElement}                  disabled={pickDisabled}                  aria-label={pickActive ? "Cancel annotation" : "Annotate preview"}                  aria-pressed={pickActive ? "true" : "false"}                  type="button"                />              }            >              <MousePointerClick className={cn(pickActive && "text-primary")} />            </TooltipTrigger>            <TooltipPopup>              {pickDisabled && pickDisabledReason                ? pickDisabledReason                : pickActive                  ? "Cancel annotation (Esc)"                  : "Annotate elements, regions, and drawings"}            </TooltipPopup>          </Tooltip>        ) : null}        {onCapture ? (          <Tooltip>            <TooltipTrigger              render={                <Button                  variant={recording ? "secondary" : "ghost"}                  size="icon-xs"                  onClick={(event) => onCapture(event.shiftKey)}                  aria-label={recording ? "Stop recording" : "Capture screenshot"}                  type="button"                  className="relative"                  disabled={captureDisabled}                />              }            >              <Camera className={cn(recording && "text-destructive")} />              {recording ? (                <span className="absolute right-0.5 top-0.5 size-1.5 animate-status-pulse rounded-full bg-destructive" />              ) : null}            </TooltipTrigger>            <TooltipPopup>              {recording ? "Stop recording" : "Screenshot · Shift-click to record"}            </TooltipPopup>          </Tooltip>        ) : null}        {onPictureInPicture ? (          <Tooltip>            <TooltipTrigger              render={                <Button                  variant={pictureInPicture ? "secondary" : "ghost"}                  size="icon-xs"                  onClick={onPictureInPicture}                  aria-label={                    pictureInPicture ? "Close floating preview" : "Float preview over chat"                  }                  aria-pressed={pictureInPicture ? "true" : "false"}                  type="button"                  disabled={pictureInPictureDisabled}                />              }            >              <PictureInPicture2 className={cn(pictureInPicture && "text-primary")} />            </TooltipTrigger>            <TooltipPopup>              {pictureInPicture ? "Close floating preview" : "Float preview over chat"}            </TooltipPopup>          </Tooltip>        ) : null}        {trailingActions}      </form>      <div        aria-hidden        data-loading={loading}        className="preview-loading-progress pointer-events-none absolute bottom-0 left-0 z-10 h-0.5 w-full origin-left rounded-r-full bg-primary"        style={{ boxShadow: "0 0 6px 1px var(--color-ring)" }}      />    </div>  );}