This commit is contained in:
Vula Builder
2026-07-31 14:12:34 +00:00
parent b233cef1e0
commit fd786ba641
+102
View File
@@ -0,0 +1,102 @@
'use client'
import { cn } from "@/lib/utils"
import { X } from 'lucide-react'
import { ReactNode, useEffect, useId, useRef } from "react"
interface ModalProps {
open: boolean
onClose: () => void
title?: string
description?: string
children: ReactNode
className?: string
closeOnBackdrop?: boolean
}
export default function Modal({
open,
onClose,
title,
description,
children,
className,
closeOnBackdrop = true,
}: ModalProps) {
const panelRef = useRef<HTMLDivElement>(null)
const titleId = useId()
const descriptionId = useId()
useEffect(() => {
if (!open) return
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
onClose()
}
}
document.addEventListener("keydown", handleKeyDown)
const originalOverflow = document.body.style.overflow
document.body.style.overflow = "hidden"
return () => {
document.removeEventListener("keydown", handleKeyDown)
document.body.style.overflow = originalOverflow
}
}, [open, onClose])
useEffect(() => {
if (open) {
panelRef.current?.focus()
}
}, [open])
if (!open) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
onClick={closeOnBackdrop ? onClose : undefined}
aria-hidden="true"
/>
<div
ref={panelRef}
role="dialog"
aria-modal="true"
aria-labelledby={title ? titleId : undefined}
aria-describedby={description ? descriptionId : undefined}
tabIndex={-1}
className={cn(
"relative w-full max-w-lg rounded-lg border border-border bg-card text-card-foreground shadow-lg focus:outline-none",
className
)}
>
<div className="flex items-start justify-between gap-4 p-6 pb-4">
<div className="space-y-1.5">
{title ? (
<h2 id={titleId} className="text-2xl font-semibold text-foreground">
{title}
</h2>
) : null}
{description ? (
<p id={descriptionId} className="text-sm text-muted-foreground">
{description}
</p>
) : null}
</div>
<button
type="button"
onClick={onClose}
aria-label="Close dialog"
className="rounded-md p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-primary"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="px-6 pb-6">{children}</div>
</div>
</div>
)
}