1import { cva, type VariantProps } from 'class-variance-authority';
2import React from 'react';
3import { cn } from '../lib/cn';
4
5type IconProps = React.SVGProps<SVGSVGElement> & {
6 size?: number | string;
7};
8
9function AlertCircleIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {
10 return (
11 <svg
12 xmlns="http://www.w3.org/2000/svg"
13 width={size}
14 height={size}
15 viewBox="0 0 24 24"
16 fill="none"
17 stroke="currentColor"
18 strokeWidth={strokeWidth}
19 strokeLinecap="round"
20 strokeLinejoin="round"
21 aria-hidden="true"
22 {...props}
23 >
24 <circle cx="12" cy="12" r="9.5" />
25 <path d="M12 7.5V13" />
26 <path d="M12 16.5h.01" />
27 </svg>
28 );
29}
30
31function AlertTriangleIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {
32 return (
33 <svg
34 xmlns="http://www.w3.org/2000/svg"
35 width={size}
36 height={size}
37 viewBox="0 0 24 24"
38 fill="none"
39 stroke="currentColor"
40 strokeWidth={strokeWidth}
41 strokeLinecap="round"
42 strokeLinejoin="round"
43 aria-hidden="true"
44 {...props}
45 >
46 <path d="M10.3 4.2 2.9 17a2 2 0 0 0 1.7 3h14.8a2 2 0 0 0 1.7-3L13.7 4.2a2 2 0 0 0-3.4 0Z" />
47 <path d="M12 9v4.5" />
48 <path d="M12 16.5h.01" />
49 </svg>
50 );
51}
52
53function CheckCircleIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {
54 return (
55 <svg
56 xmlns="http://www.w3.org/2000/svg"
57 width={size}
58 height={size}
59 viewBox="0 0 24 24"
60 fill="none"
61 stroke="currentColor"
62 strokeWidth={strokeWidth}
63 strokeLinecap="round"
64 strokeLinejoin="round"
65 aria-hidden="true"
66 {...props}
67 >
68 <circle cx="12" cy="12" r="9.5" />
69 <path d="m8 12 2.7 2.7L16 9.3" />
70 </svg>
71 );
72}
73
74function InfoIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {
75 return (
76 <svg
77 xmlns="http://www.w3.org/2000/svg"
78 width={size}
79 height={size}
80 viewBox="0 0 24 24"
81 fill="none"
82 stroke="currentColor"
83 strokeWidth={strokeWidth}
84 strokeLinecap="round"
85 strokeLinejoin="round"
86 aria-hidden="true"
87 {...props}
88 >
89 <circle cx="12" cy="12" r="9.5" />
90 <path d="M12 11v5.5" />
91 <path d="M12 7.5h.01" />
92 </svg>
93 );
94}
95
96const calloutVariants = cva(
97 'group/callout flex w-full items-start gap-3 rounded-md border p-3.5 text-sm',
98 {
99 variants: {
100 variant: {
101 default: 'bg-surface-muted text-card-foreground border-border',
102 info: 'bg-info-surface text-info-text border-info-border',
103 warning: 'bg-warning-surface text-warning-text border-warning-border',
104 danger: 'bg-destructive-surface text-destructive-text border-destructive-border',
105 success: 'bg-success-surface text-success-text border-success-border',
106 },
107 },
108 defaultVariants: {
109 variant: 'default',
110 },
111 },
112);
113
114type CalloutProps = React.HTMLAttributes<HTMLDivElement> &
115 VariantProps<typeof calloutVariants> & {
116 ref?: React.Ref<HTMLDivElement>;
117 icon?: React.ReactNode;
118 };
119
120type CalloutVariant = NonNullable<CalloutProps['variant']>;
121
122const getDefaultIcon = (variant: CalloutVariant) => {
123 switch (variant) {
124 case 'info':
125 return <InfoIcon size={18} />;
126 case 'warning':
127 return <AlertTriangleIcon size={18} />;
128 case 'danger':
129 return <AlertCircleIcon size={18} />;
130 case 'success':
131 return <CheckCircleIcon size={18} />;
132 default:
133 return null;
134 }
135};
136
137type DivWithRefProps = React.HTMLAttributes<HTMLDivElement> & {
138 ref?: React.Ref<HTMLDivElement>;
139};
140
141const CalloutRoot = ({
142 className,
143 variant = 'default',
144 icon,
145 children,
146 ref,
147 ...props
148}: CalloutProps) => {
149 const resolvedVariant: CalloutVariant = variant ?? 'default';
150 const role = 'note';
151 const resolvedIcon = icon ?? getDefaultIcon(resolvedVariant);
152
153 return (
154 <div
155 ref={ref}
156 role={role}
157 data-variant={resolvedVariant}
158 className={cn(calloutVariants({ variant: resolvedVariant }), className)}
159 {...props}
160 >
161 {resolvedIcon && (
162 <div aria-hidden="true" className="mt-0.5 shrink-0 text-base select-none">
163 {resolvedIcon}
164 </div>
165 )}
166 <div className="flex-1 space-y-1">{children}</div>
167 </div>
168 );
169};
170CalloutRoot.displayName = 'Callout';
171
172const CalloutTitle = ({ className, ref, ...props }: DivWithRefProps) => (
173 <div
174 ref={ref}
175 className={cn('leading-none font-semibold tracking-tight', className)}
176 {...props}
177 />
178);
179CalloutTitle.displayName = 'CalloutTitle';
180
181const CalloutContent = ({ className, ref, ...props }: DivWithRefProps) => (
182 <div
183 ref={ref}
184 className={cn(
185 'group-data-[variant=default]/callout:text-muted-foreground [&_p]:leading-relaxed',
186 className,
187 )}
188 {...props}
189 />
190);
191CalloutContent.displayName = 'CalloutContent';
192
193const Callout = Object.assign(CalloutRoot, {
194 Title: CalloutTitle,
195 Content: CalloutContent,
196});
197
198export { Callout, calloutVariants };
199export type { CalloutProps };