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>
);
}