Files

162 lines
5.0 KiB
TypeScript

"use client";
/**
* MorePopover — generic secondary-action menu for the selection toolbar.
*
* The parent (`TextSelectionToolbar`) owns all side effects and builds
* the appropriate `items` array based on auth state. Items can be marked
* `locked` to render a small Lock badge — used for the public flow
* where every More action sits behind the auth wall.
*/
import { type ReactNode, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { Lock } from "lucide-react";
import { type Position } from "./shared";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerTitle,
} from "@/components/ui/drawer";
export type MoreItem = {
key: string;
label: string;
description: string;
icon: ReactNode;
onClick: () => void;
/** Show a lock badge — indicates the action is behind the auth wall. */
locked?: boolean;
};
export function MorePopover({
open,
position,
onClose,
items,
isMobile,
}: {
open: boolean;
position: Position;
onClose: () => void;
items: MoreItem[];
isMobile: boolean;
}) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open || isMobile) return;
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
}
// Delay so the click that opened us doesn't immediately fire it.
const timer = setTimeout(() => {
document.addEventListener("mousedown", handleClick);
}, 10);
return () => {
clearTimeout(timer);
document.removeEventListener("mousedown", handleClick);
};
}, [open, onClose, isMobile]);
if (!open) return null;
// Mobile: bottom drawer
if (isMobile) {
return (
<Drawer open={open} onOpenChange={(o) => !o && onClose()}>
<DrawerContent className="bg-black dark:bg-[#1a1a1a] border-white/10">
<DrawerHeader className="text-left border-b border-white/10 pb-4">
<DrawerTitle className="text-white">More Options</DrawerTitle>
</DrawerHeader>
<div className="p-4 space-y-1">
{items.map((item) => (
<button
key={item.key}
onClick={(e) => {
e.stopPropagation();
item.onClick();
}}
className="flex items-center gap-3 w-full px-3 py-3 rounded-lg text-left hover:bg-white/10 transition-colors"
>
<div className="flex items-center justify-center size-10 rounded-lg bg-white/10 text-white/60">
{item.icon}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-white/90 truncate">
{item.label}
</p>
<p className="text-xs text-white/50 truncate">
{item.description}
</p>
</div>
{item.locked ? (
<Lock className="size-3.5 text-white/40 shrink-0" />
) : null}
</button>
))}
</div>
<div className="p-4 pt-0">
<DrawerClose asChild>
<button className="w-full py-2.5 rounded-lg border border-white/15 text-sm font-medium text-white/60 hover:bg-white/10 transition-colors">
Cancel
</button>
</DrawerClose>
</div>
</DrawerContent>
</Drawer>
);
}
// Desktop: floating popover anchored to `position`, flipping when it
// would overflow the viewport.
const popoverW = 260;
const popoverH = items.length * 56 + 8;
const pad = 8;
let popLeft = position.x;
let popTop = position.y + 8;
if (popLeft + popoverW > window.innerWidth - pad) {
popLeft = window.innerWidth - popoverW - pad;
}
if (popLeft < pad) popLeft = pad;
if (popTop + popoverH > window.innerHeight - pad) {
popTop = position.y - popoverH - 8;
}
if (popTop < pad) popTop = pad;
return createPortal(
<div
ref={ref}
style={{ position: "fixed", zIndex: 90, left: popLeft, top: popTop }}
className="w-64 rounded-lg border border-white/10 bg-black shadow-2xl py-1.5 animate-in fade-in-0 zoom-in-95 dark:bg-[#1a1a1a] dark:border-white/15"
>
{items.map((item) => (
<button
key={item.key}
onClick={(e) => {
e.stopPropagation();
item.onClick();
}}
className="flex items-start gap-3 w-full px-3 py-2.5 text-left hover:bg-white/10 transition-colors"
>
<span className="text-white/60 mt-0.5">{item.icon}</span>
<span className="min-w-0 flex-1">
<span className="block text-sm text-white/90 truncate">
{item.label}
</span>
<span className="block text-xs text-white/50 truncate">
{item.description}
</span>
</span>
{item.locked ? (
<Lock className="size-3.5 text-white/40 shrink-0 mt-1" />
) : null}
</button>
))}
</div>,
document.body,
);
}