1import React from "react";
2import { CloseButton } from "./CloseButton";
3
4type AlertVariant = "default" | "success" | "warning" | "error";
5
6interface AlertProps {
7 variant?: AlertVariant;
8 title: string;
9 description?: string;
10 onDismiss?: () => void;
11 className?: string;
12}
13
14/* Tiles share Toast's lit-from-above gradients, so an alert and the toast
15 it came from look like the same object. */
16const variantConfig: Record<AlertVariant, {
17 tile: string;
18 icon: React.ReactNode;
19}> = {
20 default: {
21 tile: "var(--face-dark)",
22 icon: (
23 <svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.75" viewBox="0 0 24 24">
24 <circle cx="12" cy="12" r="10" />
25 <path strokeLinecap="round" d="M12 8v4m0 4h.01" />
26 </svg>
27 ),
28 },
29 success: {
30 tile: "linear-gradient(180deg, #10b981 0%, #059669 100%)",
31 icon: (
32 <svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
33 <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
34 </svg>
35 ),
36 },
37 warning: {
38 tile: "linear-gradient(180deg, #fbbf24 0%, #f59e0b 100%)",
39 icon: (
40 <svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.75" viewBox="0 0 24 24">
41 <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
42 </svg>
43 ),
44 },
45 error: {
46 tile: "linear-gradient(180deg, #f87171 0%, #dc2626 100%)",
47 icon: (
48 <svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
49 <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
50 </svg>
51 ),
52 },
53};
54
55export function Alert({ variant = "default", title, description, onDismiss, className = "" }: AlertProps) {
56 const { tile, icon } = variantConfig[variant];
57
58 return (
59 <div
60 // Errors and warnings interrupt; everything else waits its turn
61 role={variant === "error" || variant === "warning" ? "alert" : "status"}
62 className={["rounded-[18px] p-1.5", className].join(" ")}
63 style={{ background: "var(--panel-bezel)", boxShadow: "var(--panel-shadow)" }}
64 >
65 <div
66 className="bg-white rounded-[13px] pl-3 pr-2 py-3 flex items-start gap-3"
67 style={{ boxShadow: "var(--panel-face-shadow)" }}
68 >
69 <div
70 aria-hidden
71 className="shrink-0 w-7 h-7 rounded-[8px] flex items-center justify-center text-white"
72 style={{
73 background: tile,
74 boxShadow:
75 "0 0 3px rgba(0,0,0,0.212), 0 1px 2px rgba(0,0,0,0.15), inset 0 2px 3px -2px rgba(255,255,255,0.188)",
76 }}
77 >
78 {icon}
79 </div>
80
81 <div className="flex-1 min-w-0 pt-[5px]">
82 <p className="text-[13px] font-semibold text-neutral-900 leading-tight tracking-[-0.01em]">{title}</p>
83 {description && (
84 <p className="text-[12.5px] text-neutral-500 mt-1 leading-relaxed">{description}</p>
85 )}
86 </div>
87
88 {onDismiss && <CloseButton size="sm" label="Dismiss" onClick={onDismiss} />}
89 </div>
90 </div>
91 );
92}