ConfirmDialog.tsx 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import { useEffect, useRef } from "react";
  2. import { createPortal } from "react-dom";
  3. import { AlertTriangle, X } from "lucide-react";
  4. interface ConfirmDialogProps {
  5. readonly open: boolean;
  6. readonly title: string;
  7. readonly message: string;
  8. readonly confirmLabel: string;
  9. readonly cancelLabel: string;
  10. readonly variant?: "danger" | "default";
  11. readonly onConfirm: () => void;
  12. readonly onCancel: () => void;
  13. }
  14. export function ConfirmDialog({
  15. open,
  16. title,
  17. message,
  18. confirmLabel,
  19. cancelLabel,
  20. variant = "default",
  21. onConfirm,
  22. onCancel,
  23. }: ConfirmDialogProps) {
  24. const overlayRef = useRef<HTMLDivElement>(null);
  25. useEffect(() => {
  26. if (!open) return;
  27. const handleKey = (e: KeyboardEvent) => {
  28. if (e.key === "Escape") onCancel();
  29. };
  30. document.addEventListener("keydown", handleKey);
  31. return () => document.removeEventListener("keydown", handleKey);
  32. }, [open, onCancel]);
  33. if (!open) return null;
  34. if (typeof document === "undefined") return null;
  35. const isDanger = variant === "danger";
  36. return createPortal(
  37. <div
  38. ref={overlayRef}
  39. className="fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm fade-in"
  40. onClick={(e) => { if (e.target === overlayRef.current) onCancel(); }}
  41. >
  42. <div className="bg-card border border-border rounded-2xl shadow-2xl shadow-primary/10 w-full max-w-md mx-4 overflow-hidden chat-msg-assistant">
  43. {/* Header */}
  44. <div className="flex items-center justify-between px-6 pt-6 pb-2">
  45. <div className="flex items-center gap-3">
  46. {isDanger && (
  47. <div className="w-10 h-10 rounded-xl bg-destructive/10 flex items-center justify-center">
  48. <AlertTriangle size={20} className="text-destructive" />
  49. </div>
  50. )}
  51. <h3 className="text-lg font-semibold">{title}</h3>
  52. </div>
  53. <button
  54. onClick={onCancel}
  55. className="p-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-secondary transition-colors"
  56. >
  57. <X size={16} />
  58. </button>
  59. </div>
  60. {/* Body */}
  61. <div className="px-6 py-4">
  62. <p className="text-sm text-muted-foreground leading-relaxed">{message}</p>
  63. </div>
  64. {/* Footer */}
  65. <div className="flex justify-end gap-3 px-6 pb-6">
  66. <button
  67. onClick={onCancel}
  68. className="px-4 py-2.5 text-sm font-medium rounded-xl bg-secondary text-foreground hover:bg-secondary/80 transition-all border border-border/50"
  69. >
  70. {cancelLabel}
  71. </button>
  72. <button
  73. onClick={onConfirm}
  74. className={`px-4 py-2.5 text-sm font-bold rounded-xl transition-all hover:scale-105 active:scale-95 shadow-sm ${
  75. isDanger
  76. ? "bg-destructive text-white hover:shadow-destructive/20"
  77. : "bg-primary text-primary-foreground hover:shadow-primary/20"
  78. }`}
  79. >
  80. {confirmLabel}
  81. </button>
  82. </div>
  83. </div>
  84. </div>,
  85. document.body,
  86. );
  87. }