Modal.tsx
1 import * as React from 'react' 2 import * as Dialog from '@radix-ui/react-dialog' 3 import { X } from 'lucide-react' 4 import { cn } from '../../lib/utils' 5 6 export interface ModalProps { 7 open: boolean 8 onOpenChange: (open: boolean) => void 9 title: string 10 description?: string 11 children: React.ReactNode 12 size?: 'sm' | 'md' | 'lg' 13 } 14 15 const sizeMap = { 16 sm: 'max-w-sm', 17 md: 'max-w-md', 18 lg: 'max-w-lg', 19 } 20 21 export function Modal({ open, onOpenChange, title, description, children, size = 'md' }: ModalProps) { 22 return ( 23 <Dialog.Root open={open} onOpenChange={onOpenChange}> 24 <Dialog.Portal> 25 <Dialog.Overlay 26 className={cn( 27 'fixed inset-0 z-50 bg-black/60 backdrop-blur-sm', 28 'data-[state=open]:animate-in data-[state=closed]:animate-out', 29 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0' 30 )} 31 /> 32 <Dialog.Content 33 className={cn( 34 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2', 35 'w-full rounded-[var(--radius-lg,8px)]', 36 'border border-[oklch(38%_0_0)] bg-[oklch(20%_0_0)]', 37 'p-6 shadow-xl', 38 'data-[state=open]:animate-in data-[state=closed]:animate-out', 39 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', 40 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95', 41 'data-[state=closed]:slide-out-to-left-1/2 data-[state=open]:slide-in-from-left-1/2', 42 'data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%]', 43 'focus:outline-none', 44 sizeMap[size] 45 )} 46 aria-labelledby="modal-title" 47 aria-describedby={description ? 'modal-description' : undefined} 48 > 49 <div className="flex items-start justify-between"> 50 <div> 51 <Dialog.Title 52 id="modal-title" 53 className="text-base font-semibold text-[oklch(95%_0_0)]" 54 > 55 {title} 56 </Dialog.Title> 57 {description && ( 58 <Dialog.Description 59 id="modal-description" 60 className="mt-1 text-sm text-[oklch(65%_0_0)]" 61 > 62 {description} 63 </Dialog.Description> 64 )} 65 </div> 66 <Dialog.Close 67 className={cn( 68 'rounded p-1 text-[oklch(55%_0_0)] opacity-70 transition-opacity hover:opacity-100', 69 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[oklch(52.4%_0.22_264)] focus-visible:ring-offset-1 focus-visible:ring-offset-[oklch(20%_0_0)]' 70 )} 71 aria-label="Close dialog" 72 > 73 <X size={16} aria-hidden="true" /> 74 </Dialog.Close> 75 </div> 76 <div className="mt-4">{children}</div> 77 </Dialog.Content> 78 </Dialog.Portal> 79 </Dialog.Root> 80 ) 81 }