"use client"; import { useEffect, useRef, type ReactNode } from "react"; import { cx } from "../../utils"; export type FoxModalSize = "sm" | "md" | "lg" | "full"; export interface FoxModalProps { /** 열림 여부. 호출부가 소유한다 — 모달이 스스로 닫지 않는다. */ open: boolean; /** 닫기 버튼·Esc·바깥 클릭에서 부른다. */ onClose: () => void; children?: ReactNode; size?: FoxModalSize; /** 넘기지 않으면 제목 줄을 그리지 않는다. */ title?: ReactNode; /** 아래쪽 오른편. 보통 버튼 묶음이다. */ actions?: ReactNode; /** 아래쪽 왼편. 시안은 "오늘 하루 보지 않기" 같은 체크박스 자리다. */ utility?: ReactNode; /** 닫기 버튼을 감춘다. */ closable?: boolean; closeLabel?: string; /** 바깥을 눌러도 닫히지 않게 한다. */ dismissible?: boolean; /** 배치 조정용. */ className?: string; } /** * @fox 모달. 네이티브 ``를 쓴다 — 초점 가두기·Esc 닫기·뒷배경·맨 위 레이어를 * 브라우저가 처리하므로 그걸 직접 구현하지 않는다. * * 열림 여부는 호출부가 소유한다. 사용자가 Esc를 누르거나 바깥을 눌러도 모달이 스스로 * 닫지 않고 `onClose`만 부른다 — 닫을지 말지는 호출부가 정한다. * * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` * (또는 개별 파티셜)로 한 번 불러와야 한다. */ export function FoxModal({ open, onClose, children, size = "md", title, actions, utility, closable = true, closeLabel = "닫기", dismissible = true, className, }: FoxModalProps) { const ref = useRef(null); // `open` 속성만 세우면 뒷배경도 초점 가두기도 없는 그냥 상자가 된다. 모달로 띄우려면 // `showModal()`을 불러야 한다. useEffect(() => { const dialog = ref.current; if (!dialog) { return; } if (open && !dialog.open) { dialog.showModal(); } else if (!open && dialog.open) { dialog.close(); } }, [open]); return ( { // 파일 선택 취소도 cancel을 버블링시킨다 — 대상이 dialog일 때만 닫기로 본다. if (event.target !== event.currentTarget) { return; } event.preventDefault(); onClose(); }} onClick={(event) => { // ``는 뒷배경을 눌러도 자기 자신이 대상이 된다 — 상자 밖을 눌렀는지는 // 좌표로 가린다. if (!dismissible || event.target !== event.currentTarget) { return; } const box = event.currentTarget.getBoundingClientRect(); const outside = event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom; if (outside) { onClose(); } }} >
{/* 제목이 없어도 줄은 남긴다 — 그래야 닫기 버튼과 내용이 겹치지 않는다. 빈 제목을 심으면 이름 없는 heading이 되므로 `

`는 있을 때만 낸다. */}
{title &&

{title}

}
{children}

{(utility || actions) && (
{utility &&
{utility}
} {actions &&
{actions}
}
)} {closable && (