1- import type { ComponentPropsWithoutRef , ElementType } from "react" ;
2- import { forwardRef } from "react" ;
3- import { clsx } from "clsx" ;
1+ import type { ComponentPropsWithoutRef , ReactElement } from "react" ;
2+ import { cloneElement , forwardRef , isValidElement } from "react" ;
43import { Slot , Slottable } from "@radix-ui/react-slot" ;
54import type { AsChildProps } from "../types/props" ;
5+ import { cva , type VariantProps } from "class-variance-authority" ;
6+ import { twMerge } from "tailwind-merge" ;
7+ import type { OmitNull } from "../types/utils" ;
68
79type NativeButtonProps = ComponentPropsWithoutRef < "button" > ;
810
9- type ButtonSize = "md" | "sm" | "xs" ;
11+ type ButtonProps = AsChildProps < NativeButtonProps > &
12+ Variants & {
13+ className ?: string ;
14+ icon ?: ReactElement < { "aria-hidden" : boolean ; className ?: string } > ;
15+ } ;
1016
11- type ButtonProps = AsChildProps < NativeButtonProps > & {
12- className ?: string ;
13- icon ?: ElementType ;
14- variant : "primary" | "secondary" | "hidden" ;
15- size : ButtonSize ;
16- } ;
17+ type Variants = OmitNull < Required < VariantProps < typeof button > > > ;
1718
18- const ICON_SIZES = {
19- xs : "w-3" ,
20- sm : "w-4" ,
21- md : "w-4" ,
22- } satisfies Record < ButtonSize , string > ;
19+ const button = cva (
20+ [
21+ "flex" ,
22+ "items-center" ,
23+ "flex" ,
24+ "gap-2" ,
25+ "outline-none" ,
26+ "focus:ring-3" ,
27+ "focus:ring-offset-3" ,
28+ "focus:ring-offset-primary" ,
29+ "focus:dark:ring-offset-primary-dark" ,
30+ "focus:ring-focused" ,
31+ "focus:dark:ring-focused-dark" ,
32+ "disabled:bg-button-disabled" ,
33+ "disabled:dark:bg-button-disabled-dark" ,
34+ "disabled:text-disabled" ,
35+ "disabled:dark:text-disabled-dark" ,
36+ "disabled:cursor-not-allowed" ,
37+ "transition-colors" ,
38+ "duration-200" ,
39+ ] ,
40+ {
41+ variants : {
42+ variant : {
43+ hidden : [
44+ "text-primary" ,
45+ "dark:text-primary-dark" ,
46+ "hover:bg-button-secondaryHover" ,
47+ "hover:dark:bg-button-secondaryHover-dark" ,
48+ "active:bg-button-secondarySelected" ,
49+ "active:dark:bg-button-secondarySelected-dark" ,
50+ ] ,
51+ primary : [
52+ "text-white" ,
53+ "bg-button-primary" ,
54+ "dark:bg-button-primary-dark" ,
55+ "hover:bg-button-primaryHover" ,
56+ "hover:dark:bg-button-primaryHover-dark" ,
57+ "active:bg-selected" ,
58+ "active:dark:bg-selected-dark" ,
59+ ] ,
60+ secondary : [
61+ "text-primary" ,
62+ "dark:text-primary-dark" ,
63+ "border" ,
64+ "bg-button-secondary" ,
65+ "dark:bg-button-secondary-dark" ,
66+ "border-primary" ,
67+ "dark:border-primary-dark" ,
68+ "hover:bg-button-secondaryHover" ,
69+ "hover:dark:bg-button-secondaryHover-dark" ,
70+ "active:bg-selected" ,
71+ "active:dark:bg-selected-dark" ,
72+ ] ,
73+ } ,
74+ size : {
75+ xs : [
76+ "p-2" ,
77+ "rounded" ,
78+ "text-sm" ,
79+ "font-semibold" ,
80+ "has-[>svg:only-child]:p-1.5" ,
81+ ] ,
82+ sm : [
83+ "py-2" ,
84+ "px-3" ,
85+ "rounded" ,
86+ "text-sm" ,
87+ "font-semibold" ,
88+ "has-[>svg:only-child]:p-2" ,
89+ ] ,
90+ md : [
91+ "py-2" ,
92+ "px-3" ,
93+ "rounded-lg" ,
94+ "text-md" ,
95+ "font-semibold" ,
96+ "has-[>svg:only-child]:p-3" ,
97+ ] ,
98+ } ,
99+ } ,
100+ }
101+ ) ;
102+
103+ const iconSize = cva ( [ ] , {
104+ variants : {
105+ size : {
106+ xs : [ "w-3" ] ,
107+ sm : [ "w-4" ] ,
108+ md : [ "w-4" ] ,
109+ } ,
110+ } ,
111+ } ) ;
23112
24113export const Button = forwardRef < HTMLButtonElement , ButtonProps > (
25114 function Button (
26- { asChild, className, children, variant, size, icon : Icon , ...props } ,
115+ { asChild, className, children, variant, size, icon, ...props } ,
27116 ref
28117 ) {
29118 const Component = asChild ? Slot : "button" ;
@@ -32,25 +121,13 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
32121 < Component
33122 { ...props }
34123 ref = { ref }
35- className = { clsx (
36- className ,
37- "flex items-center gap-2 outline-none" ,
38- "focus:ring-3 focus:ring-offset-3 focus:ring-offset-primary focus:dark:ring-offset-primary-dark focus:ring-focused focus:dark:ring-focused-dark" ,
39- "disabled:bg-button-disabled disabled:dark:bg-button-disabled-dark disabled:text-disabled disabled:dark:text-disabled-dark disabled:cursor-not-allowed" ,
40- {
41- "py-2 px-3 rounded-lg text-md font-semibold" : size === "md" ,
42- "py-2 px-3 rounded text-sm font-semibold" : size === "sm" ,
43- "p-2 rounded text-sm font-semibold" : size === "xs" ,
44- "text-white bg-button-primary dark:bg-button-primary-dark hover:bg-button-primaryHover hover:dark:bg-button-primaryHover-dark active:bg-selected active:dark:bg-selected-dark" :
45- variant === "primary" ,
46- "text-primary dark:text-primary-dark border bg-button-secondary dark:bg-button-secondary-dark border-primary dark:border-primary-dark hover:bg-button-secondaryHover hover:dark:bg-button-secondaryHover-dark active:bg-selected active:dark:bg-selected-dark" :
47- variant === "secondary" ,
48- "text-primary dark:text-primary-dark hover:bg-button-secondaryHover hover:dark:bg-button-secondaryHover-dark active:bg-selected active:dark:bg-selected-dark" :
49- variant === "hidden" ,
50- }
51- ) }
124+ className = { twMerge ( button ( { variant, size } ) , className ) }
52125 >
53- { Icon && < Icon className = { ICON_SIZES [ size ] } /> }
126+ { isValidElement ( icon ) &&
127+ cloneElement ( icon , {
128+ "aria-hidden" : true ,
129+ className : twMerge ( iconSize ( { size } ) , icon . props . className ) ,
130+ } ) }
54131 < Slottable > { children } </ Slottable >
55132 </ Component >
56133 ) ;
0 commit comments