/ src / components / Modal / Modal.tsx
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  }