Mirrored from GitHub
github.com/roostorg/coop
1import { cn } from '@/lib/utils';
2import { Slot } from '@radix-ui/react-slot';
3import { cva, type VariantProps } from 'class-variance-authority';
4import { LoaderCircle } from 'lucide-react';
5import * as React from 'react';
6
7const buttonVariants = cva(
8 'py-3 px-4 inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg disabled:opacity-50 disabled:pointer-events-none focus:outline-none',
9 {
10 variants: {
11 variant: {
12 default: 'border border-transparent',
13 outline: 'border',
14 ghost: 'border border-transparent',
15 soft: 'border border-transparent',
16 white:
17 'border border-gray-200 bg-white hover:bg-gray-50 focus:bg-gray-50 dark:bg-neutral-800 dark:border-neutral-700 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700',
18 link: 'border border-transparent hover:underline',
19 },
20 color: {
21 gray: '',
22 indigo: '',
23 red: '',
24 yellow: '',
25 teal: '',
26 },
27 size: {
28 default: 'py-3 px-4',
29 sm: 'py-2 px-3 text-sm',
30 lg: 'py-4 px-5 text-base',
31 icon: 'p-2',
32 },
33 },
34 compoundVariants: [
35 // Default variant compounds
36 {
37 variant: 'default',
38 color: 'gray',
39 class:
40 'bg-gray-800 text-white hover:bg-gray-900 focus:bg-gray-900 dark:bg-white dark:text-neutral-800',
41 },
42 {
43 variant: 'default',
44 color: 'indigo',
45 class:
46 'bg-indigo-500 text-white hover:bg-indigo-700 focus:bg-indigo-700',
47 },
48 {
49 variant: 'default',
50 color: 'red',
51 class: 'bg-red-600 text-white hover:bg-red-700 focus:bg-red-700',
52 },
53 {
54 variant: 'default',
55 color: 'yellow',
56 class:
57 'bg-yellow-500 text-white hover:bg-yellow-700 focus:bg-yellow-700',
58 },
59 {
60 variant: 'default',
61 color: 'teal',
62 class: 'bg-teal-500 text-white hover:bg-teal-700 focus:bg-teal-700',
63 },
64
65 // Outline variant compounds
66 {
67 variant: 'outline',
68 color: 'gray',
69 class:
70 'border-gray-600 text-gray-600 hover:bg-gray-50 hover:border-gray-800 hover:text-gray-800 focus:border-gray-800 focus:text-gray-800 dark:border-gray-500 dark:text-gray-500 dark:hover:border-gray-700 dark:hover:text-gray-700 dark:focus:border-gray-700 dark:focus:text-gray-700',
71 },
72 {
73 variant: 'outline',
74 color: 'indigo',
75 class:
76 'border-indigo-600 text-indigo-600 hover:bg-indigo-50 hover:border-indigo-800 hover:text-indigo-800 focus:border-indigo-800 focus:text-indigo-800 dark:border-indigo-500 dark:text-indigo-500 dark:hover:border-indigo-700 dark:hover:text-indigo-700 dark:focus:border-indigo-700 dark:focus:text-indigo-700',
77 },
78 {
79 variant: 'outline',
80 color: 'red',
81 class:
82 'border-red-600 text-red-600 hover:bg-red-50 hover:border-red-800 hover:text-red-800 focus:border-red-800 focus:text-red-800 dark:border-red-500 dark:text-red-500 dark:hover:border-red-700 dark:hover:text-red-700 dark:focus:border-red-700 dark:focus:text-red-700',
83 },
84 {
85 variant: 'outline',
86 color: 'yellow',
87 class:
88 'border-yellow-600 text-yellow-600 hover:bg-yellow-50 hover:border-yellow-800 hover:text-yellow-800 focus:border-yellow-800 focus:text-yellow-800 dark:border-yellow-500 dark:text-yellow-500 dark:hover:border-yellow-700 dark:hover:text-yellow-700 dark:focus:border-yellow-700 dark:focus:text-yellow-700',
89 },
90 {
91 variant: 'outline',
92 color: 'teal',
93 class:
94 'border-teal-600 text-teal-600 hover:bg-teal-50 hover:border-teal-800 hover:text-teal-800 focus:border-teal-800 focus:text-teal-800 dark:border-teal-500 dark:text-teal-500 dark:hover:border-teal-700 dark:hover:text-teal-700 dark:focus:border-teal-700 dark:focus:text-teal-700',
95 },
96
97 // Ghost variant compounds
98 {
99 variant: 'ghost',
100 color: 'gray',
101 class:
102 'text-gray-600 hover:bg-gray-100 hover:text-gray-800 focus:bg-gray-100 focus:text-gray-800 dark:text-gray-500 dark:hover:bg-gray-800/30 dark:hover:text-gray-400 dark:focus:bg-gray-800/30 dark:focus:text-gray-400',
103 },
104 {
105 variant: 'ghost',
106 color: 'indigo',
107 class:
108 'text-indigo-600 hover:bg-indigo-100 hover:text-indigo-800 focus:bg-indigo-100 focus:text-indigo-800 dark:text-indigo-500 dark:hover:bg-indigo-800/30 dark:hover:text-indigo-400 dark:focus:bg-indigo-800/30 dark:focus:text-indigo-400',
109 },
110 {
111 variant: 'ghost',
112 color: 'red',
113 class:
114 'text-red-600 hover:bg-red-100 hover:text-red-800 focus:bg-red-100 focus:text-red-800 dark:text-red-500 dark:hover:bg-red-800/30 dark:hover:text-red-400 dark:focus:bg-red-800/30 dark:focus:text-red-400',
115 },
116 {
117 variant: 'ghost',
118 color: 'yellow',
119 class:
120 'text-yellow-600 hover:bg-yellow-100 hover:text-yellow-800 focus:bg-yellow-100 focus:text-yellow-800 dark:text-yellow-500 dark:hover:bg-yellow-800/30 dark:hover:text-yellow-400 dark:focus:bg-yellow-800/30 dark:focus:text-yellow-400',
121 },
122 {
123 variant: 'ghost',
124 color: 'teal',
125 class:
126 'text-teal-600 hover:bg-teal-100 hover:text-teal-800 focus:bg-teal-100 focus:text-teal-800 dark:text-teal-500 dark:hover:bg-teal-800/30 dark:hover:text-teal-400 dark:focus:bg-teal-800/30 dark:focus:text-teal-400',
127 },
128
129 // Soft variant compounds
130 {
131 variant: 'soft',
132 color: 'gray',
133 class:
134 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:bg-gray-200 dark:text-gray-400 dark:hover:bg-gray-900 dark:focus:bg-gray-900',
135 },
136 {
137 variant: 'soft',
138 color: 'indigo',
139 class:
140 'bg-indigo-100 text-indigo-900 hover:bg-indigo-200 focus:bg-indigo-200 dark:text-indigo-400 dark:hover:bg-indigo-900 dark:focus:bg-indigo-900',
141 },
142 {
143 variant: 'soft',
144 color: 'red',
145 class:
146 'bg-red-100 text-red-900 hover:bg-red-200 focus:bg-red-200 dark:text-red-400 dark:hover:bg-red-900 dark:focus:bg-red-900',
147 },
148 {
149 variant: 'soft',
150 color: 'yellow',
151 class:
152 'bg-yellow-100 text-yellow-900 hover:bg-yellow-200 focus:bg-yellow-200 dark:text-yellow-400 dark:hover:bg-yellow-900 dark:focus:bg-yellow-900',
153 },
154 {
155 variant: 'soft',
156 color: 'teal',
157 class:
158 'bg-teal-100 text-teal-900 hover:bg-teal-200 focus:bg-teal-200 dark:text-teal-400 dark:hover:bg-teal-900 dark:focus:bg-teal-900',
159 },
160
161 // White variant compounds
162 {
163 variant: 'white',
164 color: 'gray',
165 class: 'text-gray-800 dark:text-white',
166 },
167 {
168 variant: 'white',
169 color: 'indigo',
170 class: 'text-indigo-600 dark:text-indigo-500',
171 },
172 {
173 variant: 'white',
174 color: 'red',
175 class: 'text-red-600 dark:text-red-500',
176 },
177 {
178 variant: 'white',
179 color: 'yellow',
180 class: 'text-yellow-600 dark:text-yellow-500',
181 },
182 {
183 variant: 'white',
184 color: 'teal',
185 class: 'text-teal-600 dark:text-teal-500',
186 },
187
188 // Link variant compounds
189 {
190 variant: 'link',
191 color: 'gray',
192 class:
193 'text-gray-600 hover:text-gray-800 focus:text-gray-800 dark:text-gray-500 dark:hover:text-gray-400 dark:focus:text-gray-400',
194 },
195 {
196 variant: 'link',
197 color: 'indigo',
198 class:
199 'text-indigo-600 hover:text-indigo-800 focus:text-indigo-800 dark:text-indigo-500 dark:hover:text-indigo-400 dark:focus:text-indigo-400',
200 },
201 {
202 variant: 'link',
203 color: 'red',
204 class:
205 'text-red-600 hover:text-red-800 focus:text-red-800 dark:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400',
206 },
207 {
208 variant: 'link',
209 color: 'yellow',
210 class:
211 'text-yellow-600 hover:text-yellow-800 focus:text-yellow-800 dark:text-yellow-500 dark:hover:text-yellow-400 dark:focus:text-yellow-400',
212 },
213 {
214 variant: 'link',
215 color: 'teal',
216 class:
217 'text-teal-600 hover:text-teal-800 focus:text-teal-800 dark:text-teal-500 dark:hover:text-teal-400 dark:focus:text-teal-400',
218 },
219 ],
220 defaultVariants: {
221 variant: 'default',
222 color: 'indigo',
223 size: 'default',
224 },
225 },
226);
227
228export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
229export type ButtonColor = VariantProps<typeof buttonVariants>['color'];
230export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
231
232export interface ButtonProps extends Omit<
233 React.ButtonHTMLAttributes<HTMLButtonElement>,
234 'color'
235> {
236 variant?: ButtonVariant;
237 color?: ButtonColor;
238 size?: ButtonSize;
239 asChild?: boolean;
240 loading?: boolean;
241 startIcon?: React.JSXElementConstructor<React.SVGProps<SVGSVGElement>>;
242 endIcon?: React.JSXElementConstructor<React.SVGProps<SVGSVGElement>>;
243}
244
245const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
246 (
247 {
248 className,
249 variant,
250 color,
251 size,
252 asChild = false,
253 loading = false,
254 startIcon: StartIcon,
255 endIcon: EndIcon,
256 children,
257 ...props
258 },
259 ref,
260 ) => {
261 const Comp = asChild ? Slot : 'button';
262
263 const finalColor =
264 variant === 'white' && !color ? 'gray' : (color ?? 'indigo');
265
266 const content = (
267 <>
268 {loading && (
269 <LoaderCircle
270 data-testid="loading-spinner"
271 className="h-4 w-4 animate-spin"
272 />
273 )}
274
275 {!loading && StartIcon && (
276 <span
277 data-testid="start-icon"
278 className="flex items-center justify-center mr-1"
279 >
280 {<StartIcon className="w-4 h-4" />}
281 </span>
282 )}
283
284 {size === 'icon' &&
285 React.isValidElement(children) &&
286 React.cloneElement(children, {
287 ...children.props,
288 className: cn(children.props.className, 'size-4'),
289 })}
290
291 {size !== 'icon' && children}
292
293 {!loading && EndIcon && (
294 <span className="ml-1" data-testid="end-icon">
295 {<EndIcon className="w-4 h-4" />}
296 </span>
297 )}
298 </>
299 );
300
301 return (
302 <Comp
303 className={cn(
304 buttonVariants({ variant, size, color: finalColor }),
305 className,
306 loading && 'pointer-events-none',
307 )}
308 ref={ref}
309 disabled={loading || props.disabled}
310 {...props}
311 >
312 {content}
313 </Comp>
314 );
315 },
316);
317Button.displayName = 'Button';
318
319export { Button, buttonVariants };