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