ShareTrigger.tsx5.0 KBView on GitHub
import type { ReactNode } from 'react';
import { Share2 } from 'lucide-react';

import { FieldPopover } from '@/components/ui/field';
import { SharePanel } from './SharePanel';
import { cn } from '@/lib/utils';

/**
 * The one control that opens the share panel, wherever sharing is reachable from.
 *
 * ── WHY THIS IS A COMPONENT AND NOT THREE CALL SITES ────────────────────────
 *
 * Sharing is offered from three places that look nothing alike — a document's title row,
 * an agent's title row, and a hovered file row in the tree — and the temptation at each is
 * to hand-roll "a button that opens SharePanel in a popover". That is how the previous
 * generation of this feature ended up with a Dialog on HTML documents and nothing
 * anywhere else. One component means the panel's width, alignment and mount are decided
 * once, and adding a fourth surface is a line rather than a decision.
 *
 * ── WHY SHARE IS NOT IN THE ROW MENU (AT NORMAL WIDTHS) ─────────────────────
 *
 * It is the act a person reaches for most on a file, and the ⋮ menu is where the acts you
 * reach for rarely go — New file, Rename, History, Delete. Putting Share four items down
 * that list makes the common case the hidden one. So at normal widths it is a pill
 * revealed on hover, and only BELOW the width that fits the pill does it fold into the
 * menu, as the first item, above the group that creates
 * (`useFileListNarrow` decides which). Design: `.design-sharing/FileTree.dc.html`.
 */
export function ShareTrigger({
  documentId,
  /**
   * What the popover holds. Defaults to the document panel; an AGENT passes its own,
   * because sharing an agent is a different act — the share is only the first half,
   * and the second happens when the recipient accepts. What must NOT differ is the
   * control: an agent used to carry a bespoke joined pill with its own eye button
   * beside it, so "share this" looked like a different verb on the one screen where
   * it is most likely to be a new one.
   */
  panel,
  /**
   * `pill` — the hover control on a file row: the glyph alone, because a word on a row
   * that is already carrying four columns is the thing that pushes a column off.
   * `button` — a title row, where the word is affordable and the control is permanent.
   * `anchor` — nothing to click. For the narrow case, where the ACT is a row in the ⋮
   * menu and this only says where the panel should open: beside the menu that launched
   * it, not floating at the edge of the viewport.
   */
  variant = 'button',
  label = 'Share',
  open,
  onOpenChange,
  className,
  children,
}: {
  documentId: string;
  panel?: ReactNode;
  variant?: 'pill' | 'button' | 'anchor';
  label?: string;
  /** Controlled, for `anchor` — something else owns the decision to open it. */
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
  className?: string;
  /** Replaces the trigger entirely — for a surface that already draws its own control. */
  children?: ReactNode;
}) {
  return (
    <FieldPopover
      align="end"
      // The panel opens showing who can see this — not the directory. The invite field
      // reveals its list on focus, so autofocusing it would open the panel on the
      // answer to a question nobody asked.
      autoFocus={false}
      className="w-100"
      open={open}
      onOpenChange={onOpenChange}
      trigger={
        children ??
        (variant === 'anchor' ? (
          <span aria-hidden className="block size-0" />
        ) : (
          <button
            type="button"
            aria-label={label}
            title={label}
            // `stopPropagation`: on a file row the row itself is the activator, and
            // asking who can see a file is not the same act as opening it.
            onClick={(event) => event.stopPropagation()}
            className={cn(
              'text-muted-foreground hover:text-foreground hover:bg-foreground/10',
              'flex cursor-pointer items-center gap-1.5 rounded-full transition-colors',
              'focus-visible:outline-none',
              // One step up the lightness ladder from the row's own hover wash, so the
              // pill reads as a control sitting ON the row rather than as a glyph
              // floating in it. `bg-raised` disappeared entirely: it IS the card colour.
              // `text-xs`, not `text-sm`: this sits in a title row beside a 16px ⋯ glyph
              // and a folder crumb, and at body size it was the heaviest thing on a row
              // where it is not the subject.
              variant === 'pill'
                ? 'bg-control hover:bg-hover size-6 justify-center'
                : 'h-7 px-2 text-xs font-medium',
              className,
            )}
          >
            <Share2 className="size-3.5" aria-hidden />
            {variant === 'button' ? label : null}
          </button>
        ))
      }
    >
      {panel ?? <SharePanel documentId={documentId} />}
    </FieldPopover>
  );
}