| 1 | import { cva, type VariantProps } from "class-variance-authority"; |
| 2 | import { Children, isValidElement } from "react"; |
| 3 | import type * as React from "react"; |
| 4 | |
| 5 | import { cn } from "~/lib/utils"; |
| 6 | |
| 7 | const 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 | |
| 32 | function 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 | |
| 51 | function 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 | |
| 128 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { |
| 129 | return <div className={cn("font-medium", className)} data-slot="alert-title" {...props} />; |
| 130 | } |
| 131 | |
| 132 | function 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 | |
| 142 | function AlertAction({ className, ...props }: React.ComponentProps<"div">) { |
| 143 | return <div className={cn("flex gap-1", className)} data-slot="alert-action" {...props} />; |
| 144 | } |
| 145 | |
| 146 | AlertTitle.displayName = "AlertTitle"; |
| 147 | AlertDescription.displayName = "AlertDescription"; |
| 148 | AlertAction.displayName = "AlertAction"; |
| 149 | |
| 150 | export { Alert, AlertTitle, AlertDescription, AlertAction }; |