Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | 523x 523x 523x 274x 1x 92x 92x 523x 206x 61x 523x 140x 4x 3x 593x 593x 4x 589x | import { type ReactNode, useId, useEffect, useRef } from "react";
import { ArrowLeft, X } from "lucide-react";
import { useIsMobile } from "../../hooks/useIsMobile";
type DrawerDialogProps = {
open: boolean;
onClose: () => void;
title: string;
children: ReactNode;
/** Optional footer with action buttons */
footer?: ReactNode;
};
function DesktopDialog({
open,
onClose,
title,
children,
footer,
}: DrawerDialogProps) {
const titleId = useId();
const dialogRef = useRef<HTMLDivElement>(null);
// Close on Escape key
useEffect(() => {
if (!open) return;
function handleKeyDown(e: KeyboardEvent) {
Eif (e.key === "Escape") onClose();
}
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [open, onClose]);
// Auto-focus dialog for keyboard accessibility
useEffect(() => {
if (open && dialogRef.current) {
dialogRef.current.focus();
}
}, [open]);
if (!open) return null;
return (
<div
ref={dialogRef}
role="dialog"
aria-labelledby={titleId}
tabIndex={-1}
className="fixed inset-0 z-[60] flex items-start justify-center pt-[10vh] outline-none"
>
<button
type="button"
aria-label="Close dialog"
className="fixed inset-0 bg-black/50 cursor-default"
onClick={onClose}
tabIndex={-1}
/>
<div
role="document"
className="relative z-10 bg-background-elevated rounded-lg shadow-xl w-full max-w-lg mx-4 max-h-[80vh] flex flex-col animate-page-in"
>
<div className="flex items-center justify-between px-6 py-4 border-b border-border-default flex-shrink-0">
<h2
id={titleId}
className="text-lg font-semibold text-foreground-default"
>
{title}
</h2>
<button
type="button"
onClick={onClose}
className="p-1 text-foreground-subtle hover:text-foreground-default transition-colors"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-6">{children}</div>
{footer && (
<div className="flex-shrink-0 px-6 py-4 border-t border-border-default">
{footer}
</div>
)}
</div>
</div>
);
}
function MobileFullScreen({
open,
onClose,
title,
children,
footer,
}: DrawerDialogProps) {
if (!open) return null;
return (
<div className="fixed inset-0 z-[60] flex flex-col bg-background-base animate-page-in">
{/* Header */}
<div className="flex h-14 items-center gap-3 border-b border-border-default bg-background-elevated px-4 flex-shrink-0">
<button
type="button"
onClick={onClose}
className="p-2 -ml-2 text-foreground-muted hover:text-foreground-default rounded-md"
aria-label="Go back"
>
<ArrowLeft className="h-5 w-5" />
</button>
<h2 className="text-lg font-semibold text-foreground-default">{title}</h2>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4">{children}</div>
{/* Footer */}
{footer && (
<div className="flex-shrink-0 px-4 py-4 border-t border-border-default bg-background-elevated">
{footer}
</div>
)}
</div>
);
}
export function DrawerDialog(props: DrawerDialogProps) {
const isMobile = useIsMobile();
if (isMobile) {
return <MobileFullScreen {...props} />;
}
return <DesktopDialog {...props} />;
}
|