import * as React from "react"; import * as RadixDialog from "@radix-ui/react-dialog"; import { X } from "lucide-react"; import { cn } from "@/lib/utils"; /** * Dialog primitive — thin styled wrapper over @radix-ui/react-dialog. * Radix handles focus trap, scroll lock, Escape, focus restoration, and ARIA. * * The exported API mirrors the previous custom implementation so consumers * (Drawer, CommandPalette, ShortcutsOverlay) didn't need restructuring: * * * * ... * ... * ... * * */ interface DialogProps { open: boolean; onOpenChange: (open: boolean) => void; children: React.ReactNode; /** Compatibility shim — ariaLabel is now supplied on DialogPanel directly. */ ariaLabel?: string; /** Block close from overlay click */ closeOnOverlayClick?: boolean; /** Block close from Escape */ closeOnEscape?: boolean; } const DialogCtx = React.createContext<{ ariaLabel?: string; closeOnOverlayClick: boolean; closeOnEscape: boolean; }>({ closeOnOverlayClick: true, closeOnEscape: true }); export function Dialog({ open, onOpenChange, children, ariaLabel, closeOnOverlayClick = true, closeOnEscape = true, }: DialogProps) { return ( {children} ); } export const DialogTrigger = RadixDialog.Trigger; export const DialogClose = RadixDialog.Close; interface DialogPanelProps extends React.HTMLAttributes { /** Accessible label when the panel has no DialogTitle */ ariaLabel?: string; /** Accessible description id reference */ ariaDescribedBy?: string; } export const DialogPanel = React.forwardRef( ({ className, children, ariaLabel, ariaDescribedBy, ...props }, ref) => { const ctx = React.useContext(DialogCtx); const label = ariaLabel ?? ctx.ariaLabel; return ( { if (!ctx.closeOnOverlayClick) e.preventDefault(); }} onEscapeKeyDown={(e) => { if (!ctx.closeOnEscape) e.preventDefault(); }} className={cn( "fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2", "flex max-h-[90vh] w-full max-w-lg flex-col overflow-hidden", "rounded-xl border border-border bg-surface-raised shadow-xl outline-none", "data-[state=open]:animate-fade-in-up", className, )} {...props} > {/* Radix warns if no Title is present. If consumer didn't provide one, expose ariaLabel via VisuallyHidden Title as a fallback. */} {label && !hasDialogTitle(children) && ( {label} )} {children} ); }, ); DialogPanel.displayName = "DialogPanel"; /** Best-effort detection: does the children tree include a ? */ function hasDialogTitle(node: React.ReactNode): boolean { let found = false; React.Children.forEach(node, (child) => { if (found) return; if (!React.isValidElement(child)) return; if (child.type === DialogTitle) { found = true; return; } const subtree = (child.props as { children?: React.ReactNode } | undefined) ?.children; if (subtree && hasDialogTitle(subtree)) found = true; }); return found; } export function DialogHeader({ className, children, onClose, ...props }: React.HTMLAttributes & { onClose?: () => void }) { return (
{children}
{onClose && ( )}
); } export const DialogTitle = React.forwardRef< HTMLHeadingElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)); DialogTitle.displayName = "DialogTitle"; export const DialogDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)); DialogDescription.displayName = "DialogDescription"; export function DialogBody({ className, ...props }: React.HTMLAttributes) { return (

); } export function DialogFooter({ className, ...props }: React.HTMLAttributes) { return (
); }