109 lines
2.6 KiB
TypeScript
109 lines
2.6 KiB
TypeScript
|
|
"use client";
|
|||
|
|
|
|||
|
|
import React from "react";
|
|||
|
|
import {
|
|||
|
|
CheckCircleIcon,
|
|||
|
|
InformationCircleIcon,
|
|||
|
|
ExclamationTriangleIcon,
|
|||
|
|
XCircleIcon,
|
|||
|
|
} from "@heroicons/react/24/outline";
|
|||
|
|
|
|||
|
|
type Variant = "success" | "info" | "warning" | "error";
|
|||
|
|
type IconType = React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|||
|
|
|
|||
|
|
const variantClasses: Record<
|
|||
|
|
Variant,
|
|||
|
|
{ bg: string; border: string; text: string; icon: string; Icon: IconType }
|
|||
|
|
> = {
|
|||
|
|
success: {
|
|||
|
|
bg: "bg-success-soft",
|
|||
|
|
border: "border-success/30",
|
|||
|
|
text: "text-success",
|
|||
|
|
icon: "text-success",
|
|||
|
|
Icon: CheckCircleIcon,
|
|||
|
|
},
|
|||
|
|
info: {
|
|||
|
|
bg: "bg-info-soft",
|
|||
|
|
border: "border-info/30",
|
|||
|
|
text: "text-info",
|
|||
|
|
icon: "text-info",
|
|||
|
|
Icon: InformationCircleIcon,
|
|||
|
|
},
|
|||
|
|
warning: {
|
|||
|
|
bg: "bg-warning-soft",
|
|||
|
|
border: "border-warning/35",
|
|||
|
|
text: "text-foreground",
|
|||
|
|
icon: "text-warning",
|
|||
|
|
Icon: ExclamationTriangleIcon,
|
|||
|
|
},
|
|||
|
|
error: {
|
|||
|
|
bg: "bg-danger-soft",
|
|||
|
|
border: "border-danger/30",
|
|||
|
|
text: "text-danger",
|
|||
|
|
icon: "text-danger",
|
|||
|
|
Icon: XCircleIcon,
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
interface AlertBannerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||
|
|
variant?: Variant;
|
|||
|
|
title?: string;
|
|||
|
|
children?: React.ReactNode;
|
|||
|
|
icon?: React.ReactNode;
|
|||
|
|
size?: "sm" | "md";
|
|||
|
|
elevated?: boolean;
|
|||
|
|
onClose?: () => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function AlertBanner({
|
|||
|
|
variant = "info",
|
|||
|
|
title,
|
|||
|
|
children,
|
|||
|
|
icon,
|
|||
|
|
size = "md",
|
|||
|
|
elevated = false,
|
|||
|
|
onClose,
|
|||
|
|
className,
|
|||
|
|
...rest
|
|||
|
|
}: AlertBannerProps) {
|
|||
|
|
const styles = variantClasses[variant];
|
|||
|
|
const Icon = styles.Icon;
|
|||
|
|
const padding = size === "sm" ? "p-3" : "p-4";
|
|||
|
|
const radius = "rounded-xl";
|
|||
|
|
const shadow = elevated ? "shadow-sm" : "";
|
|||
|
|
const role = variant === "error" || variant === "warning" ? "alert" : "status";
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
className={[radius, padding, "border", shadow, styles.bg, styles.border, className]
|
|||
|
|
.filter(Boolean)
|
|||
|
|
.join(" ")}
|
|||
|
|
role={role}
|
|||
|
|
{...rest}
|
|||
|
|
>
|
|||
|
|
<div className="flex items-start gap-3">
|
|||
|
|
<div className="mt-0.5 flex-shrink-0">
|
|||
|
|
{icon ? icon : <Icon className={["h-5 w-5", styles.icon].join(" ")} />}
|
|||
|
|
</div>
|
|||
|
|
<div className="flex-1">
|
|||
|
|
{title && <p className={["font-medium", styles.text].join(" ")}>{title}</p>}
|
|||
|
|
{children && (
|
|||
|
|
<div className={["text-sm mt-1 text-foreground/80"].join(" ")}>{children}</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
{onClose && (
|
|||
|
|
<button
|
|||
|
|
onClick={onClose}
|
|||
|
|
aria-label="Close alert"
|
|||
|
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
|||
|
|
>
|
|||
|
|
×
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export type { AlertBannerProps };
|