Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0148XFUpPfpiuTjxF1EK9c95
47 lines
1.6 KiB
TypeScript
47 lines
1.6 KiB
TypeScript
/* =============================================================================
|
|
* ui_template_modal.ts
|
|
* 공용 모달 겉틀 — 배경 · 제목줄 · 닫기 · Esc · 포커스. 몸은 `mount` 가 채움.
|
|
* M01 · M02 가 함께 씀 — `dialogClass` · `backdropClass` 로 화면별 겉모양을 덧붙임.
|
|
* ========================================================================== */
|
|
|
|
import "./ui_template_modal.css";
|
|
import { createButton, el } from "./ui_template_elements";
|
|
|
|
export interface ModalOptions {
|
|
title: string;
|
|
closeLabel: string;
|
|
mount: (body: HTMLElement, close: () => void) => void;
|
|
dialogClass?: string;
|
|
backdropClass?: string;
|
|
}
|
|
|
|
export function openModal(opts: ModalOptions): { close: () => void } {
|
|
const body = el("div", { className: "ui-modal__body" });
|
|
const close = (): void => backdrop.remove();
|
|
const dialog = el("div", {
|
|
className: `ui-modal ${opts.dialogClass ?? ""}`.trim(),
|
|
attrs: { role: "dialog", "aria-label": opts.title },
|
|
children: [
|
|
el("div", {
|
|
className: "ui-modal__top",
|
|
children: [
|
|
el("h3", { text: opts.title }),
|
|
createButton({ label: opts.closeLabel, variant: "ghost", onClick: close }),
|
|
],
|
|
}),
|
|
body,
|
|
],
|
|
});
|
|
const backdrop = el("div", {
|
|
className: `ui-modal__backdrop ${opts.backdropClass ?? ""}`.trim(),
|
|
children: [dialog],
|
|
});
|
|
backdrop.addEventListener("click", (ev) => ev.target === backdrop && close());
|
|
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close());
|
|
opts.mount(body, close);
|
|
document.body.append(backdrop);
|
|
dialog.tabIndex = -1;
|
|
dialog.focus();
|
|
return { close };
|
|
}
|