| "use client"; |
|
|
| import { useEffect } from "react"; |
| import { cn } from "@/shared/utils/cn"; |
| import Button from "./Button"; |
| import Tooltip from "./Tooltip"; |
|
|
| export default function Modal({ |
| isOpen, |
| onClose, |
| title, |
| children, |
| footer, |
| size = "md", |
| closeOnOverlay = true, |
| showTrafficLights = true, |
| className, |
| }) { |
| const sizes = { |
| sm: "max-w-sm", |
| md: "max-w-md", |
| lg: "max-w-lg", |
| xl: "max-w-xl", |
| full: "max-w-4xl", |
| }; |
|
|
| useEffect(() => { |
| if (isOpen) { |
| document.body.style.overflow = "hidden"; |
| } else { |
| document.body.style.overflow = ""; |
| } |
| return () => { document.body.style.overflow = ""; }; |
| }, [isOpen]); |
|
|
| useEffect(() => { |
| const handleEscape = (e) => { |
| if (e.key === "Escape" && isOpen) onClose(); |
| }; |
| document.addEventListener("keydown", handleEscape); |
| return () => document.removeEventListener("keydown", handleEscape); |
| }, [isOpen, onClose]); |
|
|
| if (!isOpen) return null; |
|
|
| return ( |
| <div className="fixed inset-0 z-50 flex items-center justify-center p-4"> |
| {/* Overlay */} |
| <div |
| className="absolute inset-0 bg-black/50 backdrop-blur-[2px] fade-in" |
| onClick={closeOnOverlay ? onClose : undefined} |
| /> |
| |
| {/* Modal content */} |
| <div |
| className={cn( |
| "relative w-full bg-surface", |
| "border border-border-subtle", |
| "rounded-[14px] shadow-[var(--shadow-elev)]", |
| "fade-in", |
| sizes[size], |
| className |
| )} |
| > |
| {/* Header */} |
| {(title || showTrafficLights) && ( |
| <div className="flex items-center justify-between p-2 border-b border-border-subtle"> |
| <div className="flex items-center"> |
| {/* Traffic lights — desktop only */} |
| {showTrafficLights && ( |
| <div className="hidden md:flex items-center gap-2 mr-4 ml-2"> |
| <Tooltip text="Close" position="top" color="#FF5F56"> |
| <button |
| onClick={onClose} |
| aria-label="Close" |
| title="Close" |
| className="w-4 h-4 rounded-full bg-[#FF5F56] hover:brightness-90 transition-all cursor-pointer flex items-center justify-center group/dot" |
| > |
| <span className="text-[9px] font-bold text-white opacity-0 group-hover/dot:opacity-100 transition-opacity leading-none">✕</span> |
| </button> |
| </Tooltip> |
| <div className="w-4 h-4 rounded-full bg-[#3a3a3a]/20 dark:bg-white/15 cursor-not-allowed" /> |
| <div className="w-4 h-4 rounded-full bg-[#3a3a3a]/20 dark:bg-white/15 cursor-not-allowed" /> |
| </div> |
| )} |
| {title && ( |
| <h2 className="text-lg font-semibold text-text-main">{title}</h2> |
| )} |
| </div> |
| {/* X button — mobile only */} |
| <button |
| onClick={onClose} |
| aria-label="Close" |
| className="md:hidden p-1.5 rounded-[10px] text-text-muted hover:bg-surface-2 hover:text-text-main transition-colors" |
| > |
| <span className="material-symbols-outlined text-[20px]">close</span> |
| </button> |
| </div> |
| )} |
| |
| {/* Body */} |
| <div className="p-6 max-h-[calc(85vh-100px)] overflow-y-auto custom-scrollbar">{children}</div> |
| |
| {/* Footer */} |
| {footer && ( |
| <div className="flex items-center justify-end gap-3 p-6 border-t border-border-subtle"> |
| {footer} |
| </div> |
| )} |
| </div> |
| </div> |
| ); |
| } |
|
|
| export function ConfirmModal({ |
| isOpen, |
| onClose, |
| onConfirm, |
| title = "Confirm", |
| message, |
| confirmText = "Confirm", |
| cancelText = "Cancel", |
| variant = "danger", |
| loading = false, |
| }) { |
| return ( |
| <Modal |
| isOpen={isOpen} |
| onClose={onClose} |
| title={title} |
| size="sm" |
| footer={ |
| <> |
| <Button variant="ghost" onClick={onClose} disabled={loading}> |
| {cancelText} |
| </Button> |
| <Button variant={variant} onClick={onConfirm} loading={loading}> |
| {confirmText} |
| </Button> |
| </> |
| } |
| > |
| <p className="text-text-muted">{message}</p> |
| </Modal> |
| ); |
| } |
|
|