AlertModal.tsx 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. import { useEffect } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { AlertTriangle } from 'lucide-react';
  4. import { Card, CardContent } from './Card';
  5. import { Button } from './Button';
  6. interface AlertModalProps {
  7. title: string;
  8. message: string;
  9. /** Optional secondary line shown above the message — e.g. the file the
  10. * alert is about. */
  11. subtitle?: string;
  12. closeText?: string;
  13. variant?: 'error' | 'warning';
  14. onClose: () => void;
  15. }
  16. /**
  17. * A small acknowledge-only modal: title, message, single Close button.
  18. *
  19. * Use it to surface something the user must read and act on, where a toast
  20. * would auto-dismiss before it can be read (e.g. a slicer rejection message).
  21. * For confirm/cancel decisions use ConfirmModal instead.
  22. */
  23. export function AlertModal({
  24. title,
  25. message,
  26. subtitle,
  27. closeText,
  28. variant = 'error',
  29. onClose,
  30. }: AlertModalProps) {
  31. const { t } = useTranslation();
  32. const resolvedCloseText = closeText ?? t('common.close');
  33. // Close on Escape — matches ConfirmModal's behaviour.
  34. useEffect(() => {
  35. const handleKeyDown = (e: KeyboardEvent) => {
  36. if (e.key === 'Escape') onClose();
  37. };
  38. window.addEventListener('keydown', handleKeyDown);
  39. return () => window.removeEventListener('keydown', handleKeyDown);
  40. }, [onClose]);
  41. const iconColor = variant === 'warning' ? 'text-yellow-400' : 'text-red-400';
  42. return (
  43. <div
  44. // z-[120]: above other modals (z-50 / z-[110]) and the toast stack, so
  45. // a slice failure surfaced from app-level context is never occluded.
  46. className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-[120]"
  47. onClick={onClose}
  48. >
  49. <Card className="w-full max-w-md" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
  50. <CardContent className="p-6">
  51. <div className="flex items-start gap-4">
  52. <div className={`p-2 rounded-full bg-bambu-dark ${iconColor}`}>
  53. <AlertTriangle className="w-6 h-6" />
  54. </div>
  55. <div className="flex-1 min-w-0">
  56. <h3 className="text-lg font-semibold text-white mb-1">{title}</h3>
  57. {subtitle && <p className="text-xs text-bambu-gray mb-2 truncate">{subtitle}</p>}
  58. <p className="text-bambu-gray text-sm whitespace-pre-line break-words">{message}</p>
  59. </div>
  60. </div>
  61. <div className="flex mt-6">
  62. <Button onClick={onClose} className="flex-1">
  63. {resolvedCloseText}
  64. </Button>
  65. </div>
  66. </CardContent>
  67. </Card>
  68. </div>
  69. );
  70. }