172 lines
6.0 KiB
JavaScript
172 lines
6.0 KiB
JavaScript
import React, { useEffect, useRef } from 'react';
|
|
import { AlertTriangle } from 'lucide-react';
|
|
|
|
/**
|
|
* ConfirmModal — themed replacement for window.confirm().
|
|
*
|
|
* Props:
|
|
* open {boolean} Whether the modal is visible
|
|
* title {string} Heading text (e.g. "Delete Document")
|
|
* message {string|ReactNode} Body text / description
|
|
* confirmText {string} Label for the confirm button (default "Confirm")
|
|
* cancelText {string} Label for the cancel button (default "Cancel")
|
|
* variant {"danger"|"warning"|"default"} Controls accent color (default "danger")
|
|
* onConfirm {function} Called when user confirms
|
|
* onCancel {function} Called when user cancels or presses Escape
|
|
*/
|
|
export default function ConfirmModal({
|
|
open,
|
|
title = 'Confirm',
|
|
message = 'Are you sure?',
|
|
confirmText = 'Confirm',
|
|
cancelText = 'Cancel',
|
|
variant = 'danger',
|
|
onConfirm,
|
|
onCancel,
|
|
}) {
|
|
const confirmRef = useRef(null);
|
|
|
|
// Focus the confirm button when the modal opens and handle Escape key
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
|
|
// Small delay so the DOM is painted before we focus
|
|
const timer = setTimeout(() => confirmRef.current?.focus(), 50);
|
|
|
|
const handleKey = (e) => {
|
|
if (e.key === 'Escape') onCancel?.();
|
|
};
|
|
document.addEventListener('keydown', handleKey);
|
|
return () => {
|
|
clearTimeout(timer);
|
|
document.removeEventListener('keydown', handleKey);
|
|
};
|
|
}, [open, onCancel]);
|
|
|
|
if (!open) return null;
|
|
|
|
const accentMap = {
|
|
danger: { color: '#EF4444', bg: 'rgba(239,68,68,0.10)', bgHover: 'rgba(239,68,68,0.18)', border: 'rgba(239,68,68,0.3)' },
|
|
warning: { color: '#F59E0B', bg: 'rgba(245,158,11,0.10)', bgHover: 'rgba(245,158,11,0.18)', border: 'rgba(245,158,11,0.3)' },
|
|
default: { color: '#0EA5E9', bg: 'rgba(14,165,233,0.10)', bgHover: 'rgba(14,165,233,0.18)', border: 'rgba(14,165,233,0.3)' },
|
|
};
|
|
const accent = accentMap[variant] || accentMap.danger;
|
|
|
|
return (
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="confirm-modal-title"
|
|
style={{
|
|
position: 'fixed', inset: 0, zIndex: 70,
|
|
background: 'rgba(10, 14, 39, 0.95)',
|
|
backdropFilter: 'blur(8px)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
padding: '1rem',
|
|
animation: 'confirmFadeIn 0.15s ease-out',
|
|
}}
|
|
onClick={(e) => { if (e.target === e.currentTarget) onCancel?.(); }}
|
|
>
|
|
<div style={{
|
|
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
|
|
border: `1px solid ${accent.border}`,
|
|
borderRadius: '0.75rem',
|
|
boxShadow: `0 20px 60px rgba(0,0,0,0.7), 0 0 30px ${accent.color}10`,
|
|
width: '100%', maxWidth: '420px',
|
|
padding: '1.75rem 2rem',
|
|
animation: 'confirmSlideUp 0.15s ease-out',
|
|
}}>
|
|
{/* Header */}
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', gap: '0.625rem',
|
|
marginBottom: '1rem',
|
|
}}>
|
|
<div style={{
|
|
width: '32px', height: '32px', borderRadius: '0.5rem',
|
|
background: accent.bg,
|
|
border: `1px solid ${accent.border}`,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
flexShrink: 0,
|
|
}}>
|
|
<AlertTriangle style={{ width: '16px', height: '16px', color: accent.color }} />
|
|
</div>
|
|
<div
|
|
id="confirm-modal-title"
|
|
style={{
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
fontSize: '0.95rem', fontWeight: '700',
|
|
color: accent.color,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.08em',
|
|
}}
|
|
>
|
|
{title}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div style={{
|
|
fontSize: '0.82rem', color: '#CBD5E1',
|
|
lineHeight: '1.6', marginBottom: '1.5rem',
|
|
fontFamily: "'Outfit', system-ui, sans-serif",
|
|
}}>
|
|
{message}
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div style={{ display: 'flex', gap: '0.75rem' }}>
|
|
<button
|
|
onClick={onCancel}
|
|
style={{
|
|
flex: 1, padding: '0.625rem',
|
|
background: 'transparent',
|
|
border: '1px solid rgba(100,116,139,0.4)',
|
|
borderRadius: '0.375rem',
|
|
color: '#94A3B8', cursor: 'pointer',
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
fontSize: '0.78rem',
|
|
transition: 'all 0.2s ease',
|
|
}}
|
|
onMouseEnter={e => {
|
|
e.currentTarget.style.borderColor = 'rgba(100,116,139,0.8)';
|
|
e.currentTarget.style.color = '#CBD5E1';
|
|
}}
|
|
onMouseLeave={e => {
|
|
e.currentTarget.style.borderColor = 'rgba(100,116,139,0.4)';
|
|
e.currentTarget.style.color = '#94A3B8';
|
|
}}
|
|
>
|
|
{cancelText}
|
|
</button>
|
|
<button
|
|
ref={confirmRef}
|
|
onClick={onConfirm}
|
|
style={{
|
|
flex: 1.5, padding: '0.625rem',
|
|
background: accent.bg,
|
|
border: `1px solid ${accent.color}`,
|
|
borderRadius: '0.375rem',
|
|
color: accent.color, cursor: 'pointer',
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
fontSize: '0.78rem', fontWeight: '600',
|
|
textTransform: 'uppercase', letterSpacing: '0.05em',
|
|
transition: 'all 0.2s ease',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.4rem',
|
|
}}
|
|
onMouseEnter={e => {
|
|
e.currentTarget.style.background = accent.bgHover;
|
|
e.currentTarget.style.boxShadow = `0 0 20px ${accent.color}25`;
|
|
}}
|
|
onMouseLeave={e => {
|
|
e.currentTarget.style.background = accent.bg;
|
|
e.currentTarget.style.boxShadow = 'none';
|
|
}}
|
|
>
|
|
{confirmText}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|