Skip to content

Commit 5a24271

Browse files
committed
refactor(ui): reimplement AlertBox with class-variance-authority
1 parent 7aeb0ee commit 5a24271

File tree

4 files changed

+165
-161
lines changed

4 files changed

+165
-161
lines changed

bun.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@vercel/analytics": "^1.6.1",
2121
"@vercel/og": "^0.8.5",
2222
"@vercel/speed-insights": "^1.3.1",
23+
"class-variance-authority": "^0.7.1",
2324
"date-fns": "^4.1.0",
2425
"framer-motion": "^12.23.26",
2526
"graphql": "^16.12.0",
@@ -714,6 +715,8 @@
714715

715716
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
716717

718+
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
719+
717720
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
718721

719722
"clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="],

components/ui/alert-box.tsx

Lines changed: 106 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { type VariantProps, cva } from "class-variance-authority";
34
import {
45
AlertCircle,
56
AlertTriangle,
@@ -15,97 +16,98 @@ import { memo, type ReactNode } from "react";
1516

1617
import { cn } from "@/lib/utils";
1718

18-
export type AlertBoxType =
19-
| "info"
20-
| "warning"
21-
| "danger"
22-
| "tip"
23-
| "success"
24-
| "note"
25-
| "question"
26-
| "important"
27-
| "example";
19+
const alertContainerVariants = cva(
20+
"my-6 rounded-lg p-4 shadow-xs backdrop-blur-xs transition-colors duration-200",
21+
{
22+
variants: {
23+
type: {
24+
info: "border border-blue-500/20 bg-blue-500/10 dark:border-blue-500/20 dark:bg-blue-500/10",
25+
warning:
26+
"border border-yellow-500/20 bg-yellow-500/10 dark:border-yellow-500/20 dark:bg-yellow-500/10",
27+
danger: "border border-red-500/20 bg-red-500/10 dark:border-red-500/20 dark:bg-red-500/10",
28+
tip: "border border-green-500/20 bg-green-500/10 dark:border-green-500/20 dark:bg-green-500/10",
29+
success:
30+
"border border-emerald-500/20 bg-emerald-500/10 dark:border-emerald-500/20 dark:bg-emerald-500/10",
31+
note: "border border-purple-500/20 bg-purple-500/10 dark:border-purple-500/20 dark:bg-purple-500/10",
32+
question:
33+
"border border-indigo-500/20 bg-indigo-500/10 dark:border-indigo-500/20 dark:bg-indigo-500/10",
34+
important:
35+
"border border-amber-500/20 bg-amber-500/10 dark:border-amber-500/20 dark:bg-amber-500/10",
36+
example:
37+
"border border-cyan-500/20 bg-cyan-500/10 dark:border-cyan-500/20 dark:bg-cyan-500/10",
38+
},
39+
},
40+
defaultVariants: {
41+
type: "info",
42+
},
43+
}
44+
);
2845

29-
export type AlertBoxProps = {
30-
type: AlertBoxType; // Alert type (affects style and icon)
31-
children: ReactNode; // Main alert content
32-
title?: ReactNode; // Optional alert title
33-
className?: string; // Optional extra CSS classes
34-
};
35-
36-
const alertStyles = {
37-
info: {
38-
container:
39-
"bg-blue-500/10 border border-blue-500/20 dark:bg-blue-500/10 dark:border-blue-500/20",
40-
icon: "text-blue-500 dark:text-blue-400",
41-
text: "text-blue-800 dark:text-blue-200",
42-
title: "text-blue-900 dark:text-blue-100",
43-
role: "status",
44-
},
45-
warning: {
46-
container:
47-
"bg-yellow-500/10 border border-yellow-500/20 dark:bg-yellow-500/10 dark:border-yellow-500/20",
48-
icon: "text-yellow-500 dark:text-yellow-400",
49-
text: "text-yellow-800 dark:text-yellow-200",
50-
title: "text-yellow-900 dark:text-yellow-100",
51-
role: "alert",
52-
},
53-
danger: {
54-
container: "bg-red-500/10 border border-red-500/20 dark:bg-red-500/10 dark:border-red-500/20",
55-
icon: "text-red-500 dark:text-red-400",
56-
text: "text-red-800 dark:text-red-200",
57-
title: "text-red-900 dark:text-red-100",
58-
role: "alert",
46+
const alertIconVariants = cva("h-5 w-5", {
47+
variants: {
48+
type: {
49+
info: "text-blue-500 dark:text-blue-400",
50+
warning: "text-yellow-500 dark:text-yellow-400",
51+
danger: "text-red-500 dark:text-red-400",
52+
tip: "text-green-500 dark:text-green-400",
53+
success: "text-emerald-500 dark:text-emerald-400",
54+
note: "text-purple-500 dark:text-purple-400",
55+
question: "text-indigo-500 dark:text-indigo-400",
56+
important: "text-amber-500 dark:text-amber-400",
57+
example: "text-cyan-500 dark:text-cyan-400",
58+
},
5959
},
60-
tip: {
61-
container:
62-
"bg-green-500/10 border border-green-500/20 dark:bg-green-500/10 dark:border-green-500/20",
63-
icon: "text-green-500 dark:text-green-400",
64-
text: "text-green-800 dark:text-green-200",
65-
title: "text-green-900 dark:text-green-100",
66-
role: "status",
60+
defaultVariants: {
61+
type: "info",
6762
},
68-
success: {
69-
container:
70-
"bg-emerald-500/10 border border-emerald-500/20 dark:bg-emerald-500/10 dark:border-emerald-500/20",
71-
icon: "text-emerald-500 dark:text-emerald-400",
72-
text: "text-emerald-800 dark:text-emerald-200",
73-
title: "text-emerald-900 dark:text-emerald-100",
74-
role: "status",
75-
},
76-
note: {
77-
container:
78-
"bg-purple-500/10 border border-purple-500/20 dark:bg-purple-500/10 dark:border-purple-500/20",
79-
icon: "text-purple-500 dark:text-purple-400",
80-
text: "text-purple-800 dark:text-purple-200",
81-
title: "text-purple-900 dark:text-purple-100",
82-
role: "note",
63+
});
64+
65+
const alertTextVariants = cva("prose-sm wrap-break-word md:prose-base max-w-full overflow-x-auto", {
66+
variants: {
67+
type: {
68+
info: "text-blue-800 dark:text-blue-200",
69+
warning: "text-yellow-800 dark:text-yellow-200",
70+
danger: "text-red-800 dark:text-red-200",
71+
tip: "text-green-800 dark:text-green-200",
72+
success: "text-emerald-800 dark:text-emerald-200",
73+
note: "text-purple-800 dark:text-purple-200",
74+
question: "text-indigo-800 dark:text-indigo-200",
75+
important: "text-amber-800 dark:text-amber-200",
76+
example: "text-cyan-800 dark:text-cyan-200",
77+
},
8378
},
84-
question: {
85-
container:
86-
"bg-indigo-500/10 border border-indigo-500/20 dark:bg-indigo-500/10 dark:border-indigo-500/20",
87-
icon: "text-indigo-500 dark:text-indigo-400",
88-
text: "text-indigo-800 dark:text-indigo-200",
89-
title: "text-indigo-900 dark:text-indigo-100",
90-
role: "note",
79+
defaultVariants: {
80+
type: "info",
9181
},
92-
important: {
93-
container:
94-
"bg-amber-500/10 border border-amber-500/20 dark:bg-amber-500/10 dark:border-amber-500/20",
95-
icon: "text-amber-500 dark:text-amber-400",
96-
text: "text-amber-800 dark:text-amber-200",
97-
title: "text-amber-900 dark:text-amber-100",
98-
role: "alert",
82+
});
83+
84+
const alertTitleVariants = cva("mb-1 font-semibold text-sm md:text-base", {
85+
variants: {
86+
type: {
87+
info: "text-blue-900 dark:text-blue-100",
88+
warning: "text-yellow-900 dark:text-yellow-100",
89+
danger: "text-red-900 dark:text-red-100",
90+
tip: "text-green-900 dark:text-green-100",
91+
success: "text-emerald-900 dark:text-emerald-100",
92+
note: "text-purple-900 dark:text-purple-100",
93+
question: "text-indigo-900 dark:text-indigo-100",
94+
important: "text-amber-900 dark:text-amber-100",
95+
example: "text-cyan-900 dark:text-cyan-100",
96+
},
9997
},
100-
example: {
101-
container:
102-
"bg-cyan-500/10 border border-cyan-500/20 dark:bg-cyan-500/10 dark:border-cyan-500/20",
103-
icon: "text-cyan-500 dark:text-cyan-400",
104-
text: "text-cyan-800 dark:text-cyan-200",
105-
title: "text-cyan-900 dark:text-cyan-100",
106-
role: "note",
98+
defaultVariants: {
99+
type: "info",
107100
},
108-
} as const;
101+
});
102+
103+
export type AlertBoxType = NonNullable<VariantProps<typeof alertContainerVariants>["type"]>;
104+
105+
export type AlertBoxProps = {
106+
type: AlertBoxType;
107+
children: ReactNode;
108+
title?: ReactNode;
109+
className?: string;
110+
};
109111

110112
const icons = {
111113
info: Info,
@@ -119,7 +121,7 @@ const icons = {
119121
example: BookOpen,
120122
} as const;
121123

122-
const defaultTitles = {
124+
const defaultTitles: Record<AlertBoxType, string> = {
123125
info: "INFO!",
124126
warning: "Read this!",
125127
danger: "Danger!",
@@ -129,7 +131,19 @@ const defaultTitles = {
129131
question: "Question",
130132
important: "Important",
131133
example: "Example",
132-
} as const;
134+
};
135+
136+
const roles: Record<AlertBoxType, string> = {
137+
info: "status",
138+
warning: "alert",
139+
danger: "alert",
140+
tip: "status",
141+
success: "status",
142+
note: "note",
143+
question: "note",
144+
important: "alert",
145+
example: "note",
146+
};
133147

134148
export const AlertBox = memo(function AlertBoxComponent({
135149
type,
@@ -138,8 +152,8 @@ export const AlertBox = memo(function AlertBoxComponent({
138152
className,
139153
}: AlertBoxProps) {
140154
const Icon = icons[type];
141-
const styles = alertStyles[type];
142155
const defaultTitle = defaultTitles[type];
156+
const role = roles[type];
143157

144158
const ariaLabel =
145159
typeof title === "string"
@@ -148,32 +162,22 @@ export const AlertBox = memo(function AlertBoxComponent({
148162

149163
return (
150164
<div
151-
role={styles.role}
165+
role={role}
152166
{...(ariaLabel ? { "aria-label": ariaLabel } : {})}
153-
className={cn(
154-
"my-6 rounded-lg p-4 shadow-xs backdrop-blur-xs transition-colors duration-200",
155-
styles.container,
156-
className
157-
)}
167+
className={cn(alertContainerVariants({ type }), className)}
158168
>
159169
<div className="flex flex-wrap items-start gap-3">
160170
{/* Alert icon */}
161171
<div aria-hidden="true" className="mt-0.5 shrink-0">
162-
<Icon className={`h-5 w-5 ${styles.icon}`} />
172+
<Icon className={alertIconVariants({ type })} />
163173
</div>
164174

165175
{/* Alert content */}
166176
<div className="min-w-0 flex-1">
167177
{!!(title || defaultTitle) && (
168-
<h5 className={`mb-1 font-semibold text-sm md:text-base ${styles.title}`}>
169-
{title || defaultTitle}
170-
</h5>
178+
<h5 className={alertTitleVariants({ type })}>{title || defaultTitle}</h5>
171179
)}
172-
<div
173-
className={`prose-sm wrap-break-word md:prose-base max-w-full overflow-x-auto ${styles.text}`}
174-
>
175-
{children}
176-
</div>
180+
<div className={alertTextVariants({ type })}>{children}</div>
177181
</div>
178182
</div>
179183
</div>

0 commit comments

Comments
 (0)