Skip to content
File

Blob: src/client/components/ui/dialog.tsx

typescript82 lines
1import { useEffect, useId, useRef, type ReactNode } from "react";
2import { X } from "lucide-react";
3 
4type Props = {
5 open: boolean;
6 onClose: () => void;
7 title: ReactNode;
8 description?: ReactNode;
9 children: ReactNode;
10 footer?: ReactNode;
11 size?: "sm" | "md" | "lg";
12 closeOnBackdrop?: boolean;
13};
14 
15const sizes = {
16 sm: "max-w-sm",
17 md: "max-w-md",
18 lg: "max-w-2xl",
19} as const;
20 
21export function Dialog({
22 open,
23 onClose,
24 title,
25 description,
26 children,
27 footer,
28 size = "md",
29 closeOnBackdrop = true,
30}: Props) {
31 const ref = useRef<HTMLDialogElement>(null);
32 const titleId = useId();
33 
34 useEffect(() => {
35 const dialog = ref.current;
36 if (!dialog) return;
37 if (open && !dialog.open) dialog.showModal();
38 if (!open && dialog.open) dialog.close();
39 }, [open]);
40 
41 useEffect(() => {
42 const dialog = ref.current;
43 if (!dialog) return;
44 const handleClose = () => onClose();
45 dialog.addEventListener("close", handleClose);
46 return () => dialog.removeEventListener("close", handleClose);
47 }, [onClose]);
48 
49 return (
50 <dialog
51 ref={ref}
52 aria-labelledby={titleId}
53 onClick={(event) => {
54 if (!closeOnBackdrop) return;
55 if (event.target === ref.current) onClose();
56 }}
57 className={`fixed inset-0 m-auto h-fit max-h-[calc(100vh-2rem)] w-[calc(100%-2rem)] ${sizes[size]} animate-scale-fade overflow-y-auto rounded-2xl border border-zinc-700/60 bg-zinc-800/95 p-0 text-zinc-100 shadow-2xl backdrop:bg-zinc-950/60 backdrop:backdrop-blur-sm`}
58 >
59 <div className="flex items-start justify-between gap-3 border-b border-zinc-700/60 px-5 py-4">
60 <div className="space-y-1">
61 <h2 id={titleId} className="text-base font-semibold text-zinc-100">
62 {title}
63 </h2>
64 {description ? <p className="text-sm text-zinc-400">{description}</p> : null}
65 </div>
66 <button
67 type="button"
68 aria-label="Close dialog"
69 onClick={onClose}
70 className="-mr-1 rounded-md p-1 text-zinc-400 transition-colors hover:bg-zinc-700/60 hover:text-zinc-100"
71 >
72 <X className="h-4 w-4" strokeWidth={2.25} aria-hidden="true" />
73 </button>
74 </div>
75 <div className="px-5 py-4">{children}</div>
76 {footer ? (
77 <div className="flex items-center justify-end gap-2 border-t border-zinc-700/60 px-5 py-3">{footer}</div>
78 ) : null}
79 </dialog>
80 );
81}