/** * ConfirmDialog - Reusable themed confirmation dialog. * * Replaces native window.confirm() with a styled modal that matches the app theme. * Controlled via useConfirmDialog() hook for imperative usage. */ import { useCallback, useEffect, useRef, useState } from 'react'; import { AlertTriangle } from 'lucide-react'; interface ConfirmDialogState { isOpen: boolean; title: string; message: string; confirmLabel?: string; cancelLabel?: string; variant?: 'default' | 'danger'; } type ConfirmResolver = ((confirmed: boolean) => void) | null; const initialState: ConfirmDialogState = { isOpen: false, title: '', message: '', }; // Singleton state — one dialog at a time let globalSetState: ((state: ConfirmDialogState) => void) | null = null; let globalResolver: ConfirmResolver = null; /** * Imperatively show a themed confirm dialog. Returns a promise that resolves * to true (confirmed) or false (cancelled). * * Usage: * const confirmed = await confirm({ title: 'Delete?', message: 'This cannot be undone.' }); */ // eslint-disable-next-line react-refresh/only-export-components -- imperative API shares singleton state with component export async function confirm(opts: { title: string; message: string; confirmLabel?: string; cancelLabel?: string; variant?: 'default' | 'danger'; }): Promise { return new Promise((resolve) => { // If a previous dialog is open, resolve it as cancelled if (globalResolver) { globalResolver(false); } globalResolver = resolve; globalSetState?.({ isOpen: true, title: opts.title, message: opts.message, confirmLabel: opts.confirmLabel, cancelLabel: opts.cancelLabel, variant: opts.variant, }); }); } /** * ConfirmDialog component. Mount once at the app root (e.g. in App.tsx). */ export const ConfirmDialog = (): React.JSX.Element | null => { const [state, setState] = useState(initialState); const dialogRef = useRef(null); // Register singleton setter useEffect(() => { globalSetState = setState; return () => { globalSetState = null; }; }, []); const close = useCallback((confirmed: boolean) => { if (globalResolver) { globalResolver(confirmed); globalResolver = null; } setState(initialState); }, []); // Escape key closes useEffect(() => { if (!state.isOpen) return; const handleKeyDown = (e: KeyboardEvent): void => { if (e.key === 'Escape') close(false); }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [state.isOpen, close]); // Auto-focus confirm button useEffect(() => { if (state.isOpen && dialogRef.current) { const btn = dialogRef.current.querySelector('[data-confirm-btn]'); btn?.focus(); } }, [state.isOpen]); if (!state.isOpen) return null; const isDanger = state.variant === 'danger'; return (
{/* Backdrop */}
); };