Refine agent sharing dialog (#3699)

## Summary

- Refine the agent share dialog around recipient sharing, link copying,
catalog sharing, and export.
- Show memory settings only when a linked agent has memories to include.
- Use a catalog toggle for custom agents and keep built-in agents out of
the catalog flow.

## Validation

- `pnpm typecheck`
- Focused Playwright share and catalog flows

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
klopez4212
2026-07-30 16:23:16 +01:00
committed by GitHub
parent c9aa55505c
commit 9a386a0def
2 changed files with 137 additions and 222 deletions

View File

@@ -37,10 +37,13 @@ import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import { Separator } from "@/shared/ui/separator";
import { Spinner } from "@/shared/ui/spinner";
import { Switch } from "@/shared/ui/switch";
import {
formatShareRecipientName,
@@ -63,7 +66,7 @@ type PersonaShareDialogProps = {
};
type SnapshotShareDialogProps = {
afterLink?: React.ReactNode;
beforeExport?: React.ReactNode;
displayName: string;
encodeSnapshot: (
memoryLevel: SnapshotMemoryLevel,
@@ -198,7 +201,6 @@ function MemoryShareConfirmation({
function ShareLevelControl({
ariaLabel,
disabled,
hasMemoryOptions,
testId,
value,
options,
@@ -206,27 +208,11 @@ function ShareLevelControl({
}: {
ariaLabel: string;
disabled: boolean;
hasMemoryOptions: boolean;
testId: string;
value: SnapshotMemoryLevel;
options: { value: SnapshotMemoryLevel; label: string }[];
onChange: (level: SnapshotMemoryLevel) => void;
}) {
if (!hasMemoryOptions) {
// Nothing to choose from, so there is no dropdown to open. State the
// outcome rather than naming the sole option: the memory-level labels
// ("Agent only", "+ core memory", …) are comparative and only make sense
// when the alternatives are actually offered.
return (
<span
className="inline-flex h-8 w-auto shrink-0 items-center justify-end px-2 text-sm text-muted-foreground"
data-testid={testId}
>
No memories included
</span>
);
}
return (
<SnapshotOptionMenu
ariaLabel={ariaLabel}
@@ -240,7 +226,7 @@ function ShareLevelControl({
}
export function SnapshotShareDialog({
afterLink,
beforeExport,
displayName,
encodeSnapshot,
hasMemoryOptions,
@@ -452,7 +438,6 @@ export function SnapshotShareDialog({
return (
<Dialog onOpenChange={handleDialogOpenChange} open={open}>
<DialogContent
aria-describedby={undefined}
className="max-w-xl gap-3 bg-transparent p-0 shadow-none"
data-testid={`${testIdPrefix}-dialog`}
showCloseButton={false}
@@ -461,10 +446,16 @@ export function SnapshotShareDialog({
className="relative rounded-2xl bg-background p-6 pb-4 shadow-2xl"
data-testid={`${testIdPrefix}-main-card`}
>
<DialogHeader className="space-y-0">
<DialogHeader>
<DialogTitle className="min-w-0 truncate pr-10">
Share {displayName}
</DialogTitle>
<DialogDescription
data-testid={`${testIdPrefix}-share-description`}
>
Anyone you share this {itemLabel} with will receive a copy they
can add and use. Changes you make later won’t sync.
</DialogDescription>
</DialogHeader>
<DialogClose
className="absolute right-4 top-4 flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors duration-150 ease-out hover:bg-accent hover:text-accent-foreground focus:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-100"
@@ -521,34 +512,44 @@ export function SnapshotShareDialog({
) : null}
</AnimatePresence>
</div>
<p
className="text-xs text-secondary-foreground/75"
data-testid={`${testIdPrefix}-send-description`}
>
They’ll receive a copy they can add and use. Changes you make
later won’t sync.
</p>
</div>
{hasMemoryOptions ? (
<section
className="space-y-2"
data-testid={`${testIdPrefix}-link-settings`}
>
<h3 className="text-xs font-medium text-secondary-foreground/75">
Share settings
</h3>
<div
className="flex items-center gap-3"
data-testid={`${testIdPrefix}-share-level-row`}
>
<h4 className="min-w-0 flex-1 text-sm font-medium">
What’s included
</h4>
<ShareLevelControl
ariaLabel="What to include"
disabled={isInterfacePending}
onChange={setShareLevel}
options={shareLevels}
testId={`${testIdPrefix}-share-level`}
value={shareLevel}
/>
</div>
</section>
) : null}
<Separator
className="bg-input/40"
data-testid={`${testIdPrefix}-link-divider`}
/>
<div
className="flex items-center gap-3 pt-2"
className="flex justify-end"
data-testid={`${testIdPrefix}-link-row`}
>
<span
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground"
data-testid={`${testIdPrefix}-link-icon`}
>
<Link2 className="h-4 w-4" />
</span>
<div
className="min-w-0 flex-1"
data-testid={`${testIdPrefix}-link-copy`}
>
<h3 className="text-sm font-medium">Share with a link</h3>
<p className="text-xs text-secondary-foreground/75">
Anyone with the link can add and use a copy.
</p>
</div>
<Button
asChild
className="shrink-0 border-border shadow-none disabled:opacity-100"
@@ -622,24 +623,6 @@ export function SnapshotShareDialog({
</Button>
</div>
<div
className="flex items-center gap-3"
data-testid={`${testIdPrefix}-share-level-row`}
>
<h3 className="min-w-0 flex-1 text-sm font-medium">
What’s included
</h3>
<ShareLevelControl
ariaLabel="What to include"
disabled={isInterfacePending}
hasMemoryOptions={hasMemoryOptions}
onChange={setShareLevel}
options={shareLevels}
testId={`${testIdPrefix}-share-level`}
value={shareLevel}
/>
</div>
<AnimatePresence initial={false}>
{showMemoryWarning ? (
<motion.div
@@ -664,10 +647,9 @@ export function SnapshotShareDialog({
</motion.div>
) : null}
</AnimatePresence>
{afterLink}
</div>
</div>
{beforeExport}
<button
className="relative flex min-h-14 w-full items-center gap-3 rounded-2xl bg-background px-5 py-4 text-left text-sm font-medium shadow-2xl outline-hidden transition-colors hover:bg-muted focus-visible:bg-muted disabled:cursor-default disabled:opacity-100"
data-testid={`${testIdPrefix}-export`}
@@ -703,13 +685,6 @@ export function PersonaShareDialog({
persona,
}: PersonaShareDialogProps) {
const encodeSnapshotMutation = useEncodeAgentSnapshotForSendMutation();
const catalogShareLevels = React.useMemo(
() => [
{ value: "not-shared", label: "Not shared" },
{ value: "none", label: "Shared" },
],
[],
);
const encodeSnapshot = React.useCallback(
async (memoryLevel: SnapshotMemoryLevel) =>
encodeSnapshotMutation.mutateAsync({
@@ -729,15 +704,13 @@ export function PersonaShareDialog({
return (
<SnapshotShareDialog
afterLink={
beforeExport={
persona.isBuiltIn ? null : (
<section
className="flex min-h-16 w-full items-center gap-3"
className="relative flex min-h-16 w-full items-center gap-3 rounded-2xl bg-background px-5 py-4 shadow-2xl"
data-testid="persona-share-catalog"
>
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
<BookUser className="h-4 w-4" />
</span>
<BookUser className="h-4 w-4 shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<h3 className="text-sm font-medium">Share to catalog</h3>
<p className="text-xs text-secondary-foreground/75">
@@ -746,20 +719,16 @@ export function PersonaShareDialog({
included.
</p>
</div>
<div className="flex shrink-0 items-center">
<SnapshotOptionMenu
ariaLabel="What to share in the catalog"
disabled={isPending}
onValueChange={(nextValue) =>
onCatalogShareLevelChange(
nextValue as CatalogPersonaShareLevel,
)
}
options={catalogShareLevels}
testId="persona-share-catalog-access"
value={catalogShareLevel}
/>
</div>
<Switch
aria-label="Share to catalog"
checked={catalogShareLevel !== "not-shared"}
data-testid="persona-share-catalog-access"
disabled={isPending}
onCheckedChange={(checked) =>
onCatalogShareLevelChange(checked ? "none" : "not-shared")
}
style={{ cursor: "default" }}
/>
</section>
)
}

View File

@@ -93,10 +93,10 @@ async function sharePersonaToCatalog(
) {
await page.getByLabel(`Open actions for ${displayName}`).click();
await page.getByRole("menuitem", { name: "Share" }).click();
await page.getByTestId("persona-share-catalog-access").click();
await page
.getByRole("menuitemradio", { name: "Shared", exact: true })
.click();
const catalogToggle = page.getByTestId("persona-share-catalog-access");
await expect(catalogToggle).not.toBeChecked();
await catalogToggle.click();
await expect(catalogToggle).toBeChecked();
await page
.getByTestId("persona-share-dialog")
.getByRole("button", { name: "Close" })
@@ -979,21 +979,27 @@ test("custom personas share with people and keep export separate", async ({
page.getByRole("heading", { name: "Share Animation Auditor" }),
).toBeVisible();
await expect(shareDialog.getByText("Added by You")).toHaveCount(0);
const sendDescription = shareDialog.getByTestId(
"persona-share-send-description",
const shareDescription = shareDialog.getByTestId(
"persona-share-share-description",
);
await expect(sendDescription).toHaveText(
"They’ll receive a copy they can add and use. Changes you make later won’t sync.",
await expect(shareDescription).toHaveText(
"Anyone you share this agent with will receive a copy they can add and use. Changes you make later won’t sync.",
);
await expect(sendDescription).toHaveClass(
/text-xs.*text-secondary-foreground\/75/,
await expect(shareDescription).toHaveClass(/text-sm.*text-muted-foreground/);
const shareDescriptionId = await shareDescription.getAttribute("id");
expect(shareDescriptionId).toBeTruthy();
await expect(shareDialog).toHaveAttribute(
"aria-describedby",
shareDescriptionId ?? "",
);
const sendDescriptionMetrics = await sendDescription.evaluate((element) => ({
height: element.getBoundingClientRect().height,
lineHeight: Number.parseFloat(getComputedStyle(element).lineHeight),
}));
expect(sendDescriptionMetrics.height).toBeLessThanOrEqual(
sendDescriptionMetrics.lineHeight + 1,
const shareDescriptionMetrics = await shareDescription.evaluate(
(element) => ({
height: element.getBoundingClientRect().height,
lineHeight: Number.parseFloat(getComputedStyle(element).lineHeight),
}),
);
expect(shareDescriptionMetrics.height).toBeLessThanOrEqual(
shareDescriptionMetrics.lineHeight * 2 + 1,
);
await expect(
shareDialog.getByRole("heading", { name: "Who has access" }),
@@ -1001,57 +1007,49 @@ test("custom personas share with people and keep export separate", async ({
await expect(shareDialog.getByText("Owner", { exact: true })).toHaveCount(0);
await expect(shareDialog.getByText("(You)", { exact: true })).toHaveCount(0);
const linkRow = page.getByTestId("persona-share-link-row");
await expect(page.getByTestId("persona-share-copy-link")).toBeVisible();
await expect(
linkRow.getByRole("heading", { name: "Share with a link" }),
).toBeVisible();
shareDialog.getByRole("heading", { name: "Share with a link" }),
).toHaveCount(0);
await expect(
linkRow.getByText("Anyone with the link can add and use a copy."),
).toHaveClass(/text-xs.*text-secondary-foreground\/75/);
shareDialog.getByText("Anyone with the link can add and use a copy."),
).toHaveCount(0);
await expect(page.getByTestId("persona-share-send")).toHaveCount(0);
const copyLinkButton = page.getByTestId("persona-share-copy-link");
const linkIcon = page.getByTestId("persona-share-link-icon");
const linkCopy = page.getByTestId("persona-share-link-copy");
const catalogSection = page.getByTestId("persona-share-catalog");
const staticShareLevel = page.getByTestId("persona-share-share-level");
const shareLevelRow = page.getByTestId("persona-share-share-level-row");
const shareMainCard = page.getByTestId("persona-share-main-card");
const exportAgentRow = page.getByTestId("persona-share-export");
await waitForAnimations(page);
const [
linkRowBox,
initialCopyLinkButtonBox,
linkIconBox,
linkCopyBox,
catalogSectionBox,
staticShareLevelBox,
shareLevelRowBox,
shareMainCardBox,
exportAgentRowBox,
] = await Promise.all([
linkRow.boundingBox(),
copyLinkButton.boundingBox(),
linkIcon.boundingBox(),
linkCopy.boundingBox(),
catalogSection.boundingBox(),
staticShareLevel.boundingBox(),
shareLevelRow.boundingBox(),
shareMainCard.boundingBox(),
exportAgentRow.boundingBox(),
]);
const sendDescriptionBox = await sendDescription.boundingBox();
const shareDescriptionBox = await shareDescription.boundingBox();
const recipientFieldBox = await page
.getByTestId("persona-share-recipient-field")
.boundingBox();
// Reading order: who → how it goes out → what's included → catalog.
expect(sendDescriptionBox?.y ?? 0).toBeGreaterThanOrEqual(
(recipientFieldBox?.y ?? 0) + (recipientFieldBox?.height ?? 0),
// Without memories, the recipient field flows directly into the link action.
expect(recipientFieldBox?.y ?? 0).toBeGreaterThanOrEqual(
(shareDescriptionBox?.y ?? 0) + (shareDescriptionBox?.height ?? 0),
);
await expect(page.getByTestId("persona-share-link-settings")).toHaveCount(0);
await expect(page.getByTestId("persona-share-share-level-row")).toHaveCount(
0,
);
expect(linkRowBox?.y ?? 0).toBeGreaterThanOrEqual(
(sendDescriptionBox?.y ?? 0) + (sendDescriptionBox?.height ?? 0),
(recipientFieldBox?.y ?? 0) + (recipientFieldBox?.height ?? 0),
);
expect(shareLevelRowBox?.y ?? 0).toBeGreaterThanOrEqual(
(linkRowBox?.y ?? 0) + (linkRowBox?.height ?? 0),
);
expect(catalogSectionBox?.y ?? 0).toBeGreaterThanOrEqual(
(shareLevelRowBox?.y ?? 0) + (shareLevelRowBox?.height ?? 0),
);
// Copy link is the link row's own action, not a stranded footer button, so
// it rides on that row, vertically centred with the link icon and flush to
// the row's right edge.
// Copy link stays inside the main card, after the shared settings divider, while catalog and
// export are separate rows below it.
expect(initialCopyLinkButtonBox?.y ?? 0).toBeGreaterThanOrEqual(
linkRowBox?.y ?? 0,
);
@@ -1059,40 +1057,15 @@ test("custom personas share with people and keep export separate", async ({
(initialCopyLinkButtonBox?.y ?? 0) +
(initialCopyLinkButtonBox?.height ?? 0),
).toBeLessThanOrEqual((linkRowBox?.y ?? 0) + (linkRowBox?.height ?? 0) + 1);
expect(
Math.abs(
(initialCopyLinkButtonBox?.y ?? 0) +
(initialCopyLinkButtonBox?.height ?? 0) / 2 -
((linkIconBox?.y ?? 0) + (linkIconBox?.height ?? 0) / 2),
),
).toBeLessThanOrEqual(1);
expect(
Math.abs(
(linkRowBox?.x ?? 0) +
(linkRowBox?.width ?? 0) -
((initialCopyLinkButtonBox?.x ?? 0) +
(initialCopyLinkButtonBox?.width ?? 0)),
),
).toBeLessThanOrEqual(1);
expect(
Math.abs(
(linkCopyBox?.y ?? 0) +
(linkCopyBox?.height ?? 0) / 2 -
((linkIconBox?.y ?? 0) + (linkIconBox?.height ?? 0) / 2),
),
).toBeLessThanOrEqual(1);
await expect(page.getByTestId("persona-share-link-divider")).toHaveCount(0);
await expect(page.getByTestId("persona-share-copy-link-footer")).toHaveCount(
0,
await expect(page.getByTestId("persona-share-link-divider")).toHaveClass(
/bg-input\/40/,
);
expect(catalogSectionBox?.y ?? 0).toBeGreaterThanOrEqual(
(shareMainCardBox?.y ?? 0) + (shareMainCardBox?.height ?? 0) + 12,
);
expect(exportAgentRowBox?.y ?? 0).toBeGreaterThanOrEqual(
(catalogSectionBox?.y ?? 0) + (catalogSectionBox?.height ?? 0) + 12,
);
expect(
Math.abs(
(shareLevelRowBox?.y ?? 0) +
(shareLevelRowBox?.height ?? 0) / 2 -
((staticShareLevelBox?.y ?? 0) +
(staticShareLevelBox?.height ?? 0) / 2),
),
).toBeLessThanOrEqual(1);
await expect(copyLinkButton).toHaveClass(
/border.*bg-background.*border-border/,
);
@@ -1109,12 +1082,7 @@ test("custom personas share with people and keep export separate", async ({
await expect.poll(copyLinkHasVisibleShadow).toBe(false);
await copyLinkButton.hover();
await expect.poll(copyLinkHasVisibleShadow).toBe(false);
await expect(page.getByTestId("persona-share-share-level")).toHaveText(
"No memories included",
);
await expect(
shareDialog.getByText("No memories included", { exact: true }),
).toHaveCount(1);
await expect(page.getByTestId("persona-share-share-level")).toHaveCount(0);
await expect(page.getByTestId("persona-share-recipient-access")).toHaveCount(
0,
);
@@ -1131,20 +1099,10 @@ test("custom personas share with people and keep export separate", async ({
await expect(
shareDialog.getByText("File format", { exact: true }),
).toHaveCount(0);
const shareMainCard = page.getByTestId("persona-share-main-card");
const exportAgentRow = page.getByTestId("persona-share-export");
await expect(exportAgentRow).toHaveText("Export agent");
await expect(shareMainCard.getByTestId("persona-share-export")).toHaveCount(
0,
);
await waitForAnimations(page);
const shareMainCardBox = await shareMainCard.boundingBox();
const exportAgentRowBox = await exportAgentRow.boundingBox();
const shareCardGap =
(exportAgentRowBox?.y ?? 0) -
((shareMainCardBox?.y ?? 0) + (shareMainCardBox?.height ?? 0));
expect(shareCardGap).toBeGreaterThanOrEqual(12);
expect(shareCardGap).toBeLessThan(16);
const [shareMainCardStyles, exportAgentRowStyles] = await Promise.all([
shareMainCard.evaluate((element) => ({
borderRadius: getComputedStyle(element).borderRadius,
@@ -1528,9 +1486,9 @@ This deliberately long fenced-code example must not establish the minimum width
const shareMainCard = shareDialog.getByTestId("persona-share-main-card");
const copyLinkButton = shareDialog.getByTestId("persona-share-copy-link");
const catalogSection = shareDialog.getByTestId("persona-share-catalog");
await expect(
shareMainCard.getByTestId("persona-share-catalog"),
).toBeVisible();
await expect(shareMainCard.getByTestId("persona-share-catalog")).toHaveCount(
0,
);
await expect(catalogSection).toContainText("Share to catalog");
await expect(catalogSection).toContainText(
"Anyone in this community can find and use a copy.",
@@ -1544,26 +1502,18 @@ This deliberately long fenced-code example must not establish the minimum width
catalogSection.boundingBox(),
shareMainCard.boundingBox(),
]);
// Copy link belongs to the link row above, so the catalog is the section
// that closes the card rather than trailing an orphaned button.
// Catalog is its own card below the sharing controls.
expect(
(copyLinkButtonBox?.y ?? 0) + (copyLinkButtonBox?.height ?? 0),
).toBeLessThanOrEqual(catalogSectionBox?.y ?? 0);
expect(
(catalogSectionBox?.y ?? 0) + (catalogSectionBox?.height ?? 0),
).toBeLessThanOrEqual(
(shareMainCardBox?.y ?? 0) + (shareMainCardBox?.height ?? 0),
);
await expect(catalogAccess).toHaveText("Not shared");
expect(catalogSectionBox?.y ?? 0).toBeGreaterThanOrEqual(
(shareMainCardBox?.y ?? 0) + (shareMainCardBox?.height ?? 0),
);
await expect(catalogAccess).not.toBeChecked();
await catalogAccess.click();
await expect(page.getByRole("menuitemradio")).toHaveText([
"Not shared",
"Shared",
]);
await page
.getByRole("menuitemradio", { name: "Shared", exact: true })
.click();
await expect(catalogAccess).toHaveText("Shared");
await expect(catalogAccess).toBeChecked();
const storedPersonas = await invokeTauri<
Array<{ id: string; shared: boolean }>
>(page, "list_personas");
@@ -1651,11 +1601,9 @@ This deliberately long fenced-code example must not establish the minimum width
await page.getByLabel("Open actions for Catalog Analyst").click();
await page.getByRole("menuitem", { name: "Share" }).click();
await expect(catalogAccess).toHaveText("Shared");
await expect(catalogAccess).toBeChecked();
await catalogAccess.click();
await page
.getByRole("menuitemradio", { name: "Not shared", exact: true })
.click();
await expect(catalogAccess).not.toBeChecked();
await page
.getByTestId("persona-share-dialog")
.getByRole("button", { name: "Close" })
@@ -1687,9 +1635,6 @@ test("a queued catalog share is not presented as relay-published", async ({
await page.getByLabel("Open actions for Queued Catalog Agent").click();
await page.getByRole("menuitem", { name: "Share" }).click();
await page.getByTestId("persona-share-catalog-access").click();
await page
.getByRole("menuitemradio", { name: "Shared", exact: true })
.click();
await expect(
page.getByText(
@@ -1918,7 +1863,7 @@ test("one share level selector drives both the link and send paths", async ({
const shareLevel = shareDialog.getByLabel("What to include", {
exact: true,
});
const catalogAccess = shareDialog.getByLabel("What to share in the catalog");
const catalogAccess = shareDialog.getByLabel("Share to catalog");
const recipientField = page.getByTestId("persona-share-recipient-field");
const emptyRecipientFieldBox = await recipientField.boundingBox();
await expect(shareDialog.getByTestId("persona-share-send")).toHaveCount(0);
@@ -1930,13 +1875,14 @@ test("one share level selector drives both the link and send paths", async ({
await expect(shareLevel).toHaveCSS("text-decoration-line", "none");
await expect(shareLevel).toHaveCSS("padding-left", "8px");
await expect(shareLevel).toHaveCSS("padding-right", "8px");
await expect(catalogAccess).toHaveText("Not shared");
await catalogAccess.click();
await expect(page.getByRole("menuitemradio")).toHaveText([
"Not shared",
"Shared",
]);
await page.keyboard.press("Escape");
await expect(catalogAccess).not.toBeChecked();
await expect(catalogAccess).toHaveCSS("cursor", "default");
await expect(
shareDialog.getByTestId("persona-share-link-settings"),
).toContainText("Share settings");
await expect(
shareDialog.getByText("Share settings", { exact: true }),
).toHaveClass(/text-xs.*text-secondary-foreground\/75/);
const copyLinkButton = shareDialog.getByTestId("persona-share-copy-link");
const recipientFieldBox = await recipientField.boundingBox();
const [shareLevelBox, copyLinkButtonBox, catalogAccessBox] =
@@ -1945,15 +1891,15 @@ test("one share level selector drives both the link and send paths", async ({
copyLinkButton.boundingBox(),
catalogAccess.boundingBox(),
]);
// Reading order: who → how it goes out → what's included → catalog.
expect(copyLinkButtonBox?.y ?? 0).toBeGreaterThanOrEqual(
// Reading order: who → what's included → copy link → catalog.
expect(shareLevelBox?.y ?? 0).toBeGreaterThanOrEqual(
(recipientFieldBox?.y ?? 0) + (recipientFieldBox?.height ?? 0),
);
expect(shareLevelBox?.y ?? 0).toBeGreaterThanOrEqual(
(copyLinkButtonBox?.y ?? 0) + (copyLinkButtonBox?.height ?? 0),
expect(copyLinkButtonBox?.y ?? 0).toBeGreaterThanOrEqual(
(shareLevelBox?.y ?? 0) + (shareLevelBox?.height ?? 0),
);
expect(catalogAccessBox?.y ?? 0).toBeGreaterThanOrEqual(
(shareLevelBox?.y ?? 0) + (shareLevelBox?.height ?? 0),
(copyLinkButtonBox?.y ?? 0) + (copyLinkButtonBox?.height ?? 0),
);
// The memory choice is stated once, governing both delivery actions —
// neither the recipients row nor the link row carries its own copy.