"use client"; /** * 通用 Dialog 基础组件(headless + 简单样式) * * 原始 Dart: 散落在各处的 `showDialog` + `Dialog` widget(quota_dialog, pwa_install_dialog, * username_dialog, pronouns_dialog, subscription_dialog, environment_dialog, auth_other_options_dialog, * facebook_login_dialog, external_browser_dialog)。 * * 设计目标:~70 行覆盖所有 9 种 dialog 的通用逻辑(portal, ESC, scroll lock, click-outside), * 调用方只关心内容布局。 * * 注意:项目不引入 Radix/shadcn,纯依赖 React + CSS Modules。 */ import { type ReactNode, useEffect } from "react"; import { createPortal } from "react-dom"; import styles from "./dialog.module.css"; export interface DialogProps { open: boolean; onClose: () => void; children: ReactNode; /** 内容最大宽度(px)。默认 360。 */ maxWidth?: number; /** 蒙层透明度(0-1)。默认 0.5。 */ scrimOpacity?: number; /** 禁用点击外部 / ESC 关闭。 */ persistent?: boolean; /** ARIA 标签。 */ ariaLabel?: string; } export function Dialog({ open, onClose, children, maxWidth = 360, scrimOpacity = 0.5, persistent = false, ariaLabel, }: DialogProps) { useEffect(() => { if (!open || typeof document === "undefined") return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape" && !persistent) onClose(); }; document.addEventListener("keydown", onKey); const prevOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = prevOverflow; }; }, [open, onClose, persistent]); if (!open || typeof document === "undefined") return null; return createPortal(
{ if (!persistent && e.target === e.currentTarget) onClose(); }} >
e.stopPropagation()} > {children}
, document.body, ); }