1import { cva, type VariantProps } from "class-variance-authority";
2import { Children, isValidElement } from "react";
3import type * as React from "react";
4
5import { cn } from "~/lib/utils";
6
7const alertVariants = cva("relative rounded-xl border px-3.5 py-3 text-card-foreground text-sm", {
8 defaultVariants: {
9 surface: "default",
10 variant: "default",
11 },
12 variants: {
13 // "glass" floats the alert over content; alert-glass tints from data-variant.
14 surface: {
15 default: "",
16 glass: "alert-glass",
17 },
18 variant: {
19 default: "bg-transparent dark:bg-input/32 [&_svg]:text-muted-foreground",
20 sidebar:
21 "rounded-lg border-sidebar-border bg-sidebar-control-surface px-2 py-1.5 text-[11px] leading-4 [&_[data-slot=alert-description]]:block [&_[data-slot=alert-description]]:text-sidebar-muted-foreground",
22 error:
23 "border-error/32 bg-error-surface text-error-foreground [&_[data-slot=alert-description]]:text-error-foreground/80 [&_svg]:text-error",
24 info: "border-info/32 bg-info/4 [&_svg]:text-info",
25 success: "border-success/32 bg-success/4 [&_svg]:text-success",
26 warning:
27 "border-warning/32 bg-warning-surface text-warning-foreground [&_[data-slot=alert-description]]:text-warning-foreground/80 [&_svg]:text-warning",
28 },
29 },
30});
31
32function alertChildSlot(child: React.ReactElement): string | undefined {
33 const propsSlot = (child.props as Record<string, string | undefined>)["data-slot"];
34 if (propsSlot) {
35 return propsSlot;
36 }
37
38 const type = child.type as { displayName?: string; name?: string };
39 switch (type.displayName ?? type.name) {
40 case "AlertAction":
41 return "alert-action";
42 case "AlertTitle":
43 return "alert-title";
44 case "AlertDescription":
45 return "alert-description";
46 default:
47 return undefined;
48 }
49}
50
51function Alert({
52 className,
53 variant,
54 surface,
55 controlAlignment = "center",
56 children,
57 ...props
58}: React.ComponentProps<"div"> &
59 VariantProps<typeof alertVariants> & {
60 controlAlignment?: "center" | "first-line";
61 }) {
62 const icon: React.ReactNode[] = [];
63 const content: React.ReactNode[] = [];
64 const action: React.ReactNode[] = [];
65
66 Children.forEach(children, (child) => {
67 if (!isValidElement(child)) {
68 content.push(child);
69 return;
70 }
71 const slot = alertChildSlot(child);
72 if (slot === "alert-action") {
73 action.push(child);
74 } else if (slot === "alert-title" || slot === "alert-description") {
75 content.push(child);
76 } else {
77 icon.push(child);
78 }
79 });
80
81 return (
82 <div
83 className={cn(alertVariants({ surface, variant }), className)}
84 data-slot="alert"
85 data-variant={variant ?? "default"}
86 role="alert"
87 {...props}
88 >
89 <div
90 className={cn(
91 "flex gap-2",
92 controlAlignment === "first-line" ? "items-start" : "items-center",
93 controlAlignment === "first-line" &&
94 action.length > 0 &&
95 "min-h-7 pt-1 sm:min-h-6 sm:pt-0.5",
96 )}
97 >
98 {icon.length > 0 && (
99 <div
100 className={cn(
101 "flex shrink-0 items-center justify-center",
102 controlAlignment === "first-line"
103 ? "h-lh w-4 [&>svg]:size-4"
104 : "size-4 [&>svg]:size-full",
105 )}
106 >
107 {icon}
108 </div>
109 )}
110 {content.length > 0 && (
111 <div className="flex min-w-0 flex-1 flex-col gap-0.5">{content}</div>
112 )}
113 {action.length > 0 && (
114 <div
115 className={cn(
116 "flex shrink-0 items-center",
117 controlAlignment === "first-line" ? "h-lh self-start" : "self-center",
118 )}
119 >
120 {action}
121 </div>
122 )}
123 </div>
124 </div>
125 );
126}
127
128function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
129 return <div className={cn("font-medium", className)} data-slot="alert-title" {...props} />;
130}
131
132function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
133 return (
134 <div
135 className={cn("flex flex-col gap-2.5 text-muted-foreground", className)}
136 data-slot="alert-description"
137 {...props}
138 />
139 );
140}
141
142function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
143 return <div className={cn("flex gap-1", className)} data-slot="alert-action" {...props} />;
144}
145
146AlertTitle.displayName = "AlertTitle";
147AlertDescription.displayName = "AlertDescription";
148AlertAction.displayName = "AlertAction";
149
150export { Alert, AlertTitle, AlertDescription, AlertAction };