cooterlabs 1.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ import * as React from "react"
2
+ import { X } from "lucide-react"
3
+ import { cn } from "../../lib/utils"
4
+
5
+ const alertVariants = {
6
+ filled: {
7
+ default:
8
+ "bg-primary-500 text-white [&_[data-slot=alert-description]]:text-white/80",
9
+ success:
10
+ "bg-success-500 text-white [&_[data-slot=alert-description]]:text-white/80",
11
+ info: "bg-info-500 text-white [&_[data-slot=alert-description]]:text-white/80",
12
+ warning:
13
+ "bg-warning-500 text-white [&_[data-slot=alert-description]]:text-white/80",
14
+ error: "bg-error-500 text-white [&_[data-slot=alert-description]]:text-white/80",
15
+ },
16
+ outline: {
17
+ default:
18
+ "border border-primary-500 bg-primary-50 text-primary-500 [&_[data-slot=alert-description]]:text-muted-foreground",
19
+ success:
20
+ "border border-success-500 bg-success-50 text-success-500 [&_[data-slot=alert-description]]:text-muted-foreground",
21
+ info: "border border-info-500 bg-info-50 text-info-500 [&_[data-slot=alert-description]]:text-muted-foreground",
22
+ warning:
23
+ "border border-warning-500 bg-warning-50 text-warning-500 [&_[data-slot=alert-description]]:text-muted-foreground",
24
+ error: "border border-error-500 bg-error-50 text-error-500 [&_[data-slot=alert-description]]:text-muted-foreground",
25
+ },
26
+ }
27
+
28
+ const alertPositions = {
29
+ "top-left": "fixed left-4 top-4 z-50",
30
+ "top-center": "fixed left-1/2 top-4 z-50 -translate-x-1/2",
31
+ "top-right": "fixed right-4 top-4 z-50",
32
+ "bottom-left": "fixed bottom-4 left-4 z-50",
33
+ "bottom-center": "fixed bottom-4 left-1/2 z-50 -translate-x-1/2",
34
+ "bottom-right": "fixed bottom-4 right-4 z-50",
35
+ }
36
+
37
+ export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
38
+ severity?: keyof (typeof alertVariants)["filled"]
39
+ variant?: keyof typeof alertVariants
40
+ position?: keyof typeof alertPositions
41
+ /** seconds until the alert dismisses itself; pauses on hover */
42
+ duration?: number
43
+ onDismiss?: () => void
44
+ }
45
+
46
+ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
47
+ (
48
+ {
49
+ className,
50
+ severity = "default",
51
+ variant = "filled",
52
+ position,
53
+ duration,
54
+ onDismiss,
55
+ children,
56
+ ...props
57
+ },
58
+ ref
59
+ ) => {
60
+ const [visible, setVisible] = React.useState(true)
61
+ if (!visible) return null
62
+ const dismissible = duration != null || onDismiss != null
63
+ const dismiss = () => {
64
+ setVisible(false)
65
+ onDismiss?.()
66
+ }
67
+ return (
68
+ <div
69
+ ref={ref}
70
+ role="alert"
71
+ className={cn(
72
+ "group relative overflow-hidden rounded-xs p-4 [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:size-5 [&>svg~*]:pl-8",
73
+ alertVariants[variant][severity],
74
+ position && alertPositions[position],
75
+ dismissible && "pr-12",
76
+ className
77
+ )}
78
+ {...props}
79
+ >
80
+ {children}
81
+ {dismissible && (
82
+ <button
83
+ type="button"
84
+ aria-label="Dismiss"
85
+ onClick={dismiss}
86
+ className="absolute right-4 top-4"
87
+ >
88
+ <X className="size-4" />
89
+ </button>
90
+ )}
91
+ {duration != null && (
92
+ <div
93
+ className="absolute bottom-0 left-0 h-1 bg-current opacity-30 group-hover:[animation-play-state:paused]"
94
+ style={{
95
+ animation: `alert-timer ${duration}s linear forwards`,
96
+ }}
97
+ onAnimationEnd={dismiss}
98
+ />
99
+ )}
100
+ </div>
101
+ )
102
+ }
103
+ )
104
+ Alert.displayName = "Alert"
105
+
106
+ const AlertTitle = React.forwardRef<
107
+ HTMLHeadingElement,
108
+ React.HTMLAttributes<HTMLHeadingElement>
109
+ >(({ className, ...props }, ref) => (
110
+ <h5
111
+ ref={ref}
112
+ className={cn("mb-1 text-s2 font-semibold", className)}
113
+ {...props}
114
+ />
115
+ ))
116
+ AlertTitle.displayName = "AlertTitle"
117
+
118
+ const AlertDescription = React.forwardRef<
119
+ HTMLDivElement,
120
+ React.HTMLAttributes<HTMLDivElement>
121
+ >(({ className, ...props }, ref) => (
122
+ <div
123
+ ref={ref}
124
+ data-slot="alert-description"
125
+ className={cn("text-b3", className)}
126
+ {...props}
127
+ />
128
+ ))
129
+ AlertDescription.displayName = "AlertDescription"
130
+
131
+ const AlertActions = React.forwardRef<
132
+ HTMLDivElement,
133
+ React.HTMLAttributes<HTMLDivElement>
134
+ >(({ className, ...props }, ref) => (
135
+ <div
136
+ ref={ref}
137
+ className={cn(
138
+ "mt-3 flex items-center gap-4 text-b4 font-semibold",
139
+ className
140
+ )}
141
+ {...props}
142
+ />
143
+ ))
144
+ AlertActions.displayName = "AlertActions"
145
+
146
+ export { Alert, AlertTitle, AlertDescription, AlertActions }
@@ -1,20 +1,32 @@
1
1
  import * as React from "react"
2
2
  import * as AvatarPrimitive from "@radix-ui/react-avatar"
3
- import { clsx, type ClassValue } from "clsx"
4
- import { twMerge } from "tailwind-merge"
3
+ import { cn } from "../../lib/utils"
5
4
 
6
- export function cn(...inputs: ClassValue[]) {
7
- return twMerge(clsx(inputs))
5
+ const avatarSizes = {
6
+ tiny: "h-6 w-6",
7
+ small: "h-8 w-8",
8
+ medium: "h-10 w-10",
9
+ large: "h-12 w-12",
10
+ giant: "h-14 w-14",
11
+ "xl-giant": "h-16 w-16",
12
+ "xxl-giant": "h-20 w-20",
13
+ "xxxl-giant": "h-24 w-24",
14
+ }
15
+
16
+ export interface AvatarProps
17
+ extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> {
18
+ size?: keyof typeof avatarSizes
8
19
  }
9
20
 
10
21
  const Avatar = React.forwardRef<
11
22
  React.ElementRef<typeof AvatarPrimitive.Root>,
12
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
13
- >(({ className, ...props }, ref) => (
23
+ AvatarProps
24
+ >(({ className, size = "medium", ...props }, ref) => (
14
25
  <AvatarPrimitive.Root
15
26
  ref={ref}
16
27
  className={cn(
17
- "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
28
+ "relative flex shrink-0 overflow-hidden rounded-full",
29
+ avatarSizes[size],
18
30
  className
19
31
  )}
20
32
  {...props}
@@ -41,7 +53,7 @@ const AvatarFallback = React.forwardRef<
41
53
  <AvatarPrimitive.Fallback
42
54
  ref={ref}
43
55
  className={cn(
44
- "flex h-full w-full items-center justify-center rounded-full bg-muted",
56
+ "flex h-full w-full items-center justify-center rounded-full bg-primary-100 text-label font-medium text-primary",
45
57
  className
46
58
  )}
47
59
  {...props}
@@ -1,36 +1,125 @@
1
1
  import * as React from "react"
2
- import { clsx, type ClassValue } from "clsx"
3
- import { twMerge } from "tailwind-merge"
2
+ import { X } from "lucide-react"
3
+ import { cn } from "../../lib/utils"
4
4
 
5
- export function cn(...inputs: ClassValue[]) {
6
- return twMerge(clsx(inputs))
5
+ const badgeColors = {
6
+ outline: {
7
+ default: "border-[1.5px] border-primary-500 bg-background text-primary-600",
8
+ success: "border-[1.5px] border-success-500 bg-background text-success-600",
9
+ info: "border-[1.5px] border-info-500 bg-background text-info-600",
10
+ warning: "border-[1.5px] border-warning-500 bg-background text-warning-600",
11
+ error: "border-[1.5px] border-error-500 bg-background text-error-600",
12
+ },
13
+ filled: {
14
+ default: "bg-primary-600 text-white",
15
+ success: "bg-success-600 text-white",
16
+ info: "bg-info-600 text-white",
17
+ warning: "bg-warning-600 text-white",
18
+ error: "bg-error-600 text-white",
19
+ },
7
20
  }
8
21
 
9
- export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
10
- variant?: "default" | "secondary" | "destructive" | "outline"
22
+ const chipInteractions = {
23
+ outline: {
24
+ default: "hover:bg-accent active:bg-primary-100",
25
+ success: "hover:bg-success-50 active:bg-success-100",
26
+ info: "hover:bg-info-50 active:bg-info-100",
27
+ warning: "hover:bg-warning-50 active:bg-warning-100",
28
+ error: "hover:bg-error-50 active:bg-error-100",
29
+ },
30
+ filled: {
31
+ default: "hover:bg-primary-700 active:bg-primary-800",
32
+ success: "hover:bg-success-700 active:bg-success-800",
33
+ info: "hover:bg-info-700 active:bg-info-800",
34
+ warning: "hover:bg-warning-700 active:bg-warning-800",
35
+ error: "hover:bg-error-700 active:bg-error-800",
36
+ },
11
37
  }
12
38
 
13
- const badgeVariants = {
14
- default:
15
- "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
16
- secondary:
17
- "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
18
- destructive:
19
- "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
20
- outline: "text-foreground",
39
+ const badgeSizes = {
40
+ badge: {
41
+ medium: "h-10 gap-1 p-3 text-b4 rounded-[12px] [&_svg]:size-3",
42
+ small: "h-8 gap-1 p-2 text-c2 rounded-[8px] [&_svg]:size-3",
43
+ tiny: "h-6 gap-1 p-1.5 text-c3 rounded-[8px] [&_svg]:size-3",
44
+ },
45
+ chip: {
46
+ medium: "h-10 gap-2 p-3 rounded-[12px] text-b4 [&_svg]:size-4",
47
+ small: "h-8 gap-1.5 p-2 rounded-[8px] text-c2 [&_svg]:size-4",
48
+ tiny: "h-6 gap-1 p-1.5 rounded-[8px] text-c3 [&_svg]:size-3",
49
+ },
21
50
  }
22
51
 
23
- function Badge({ className, variant = "default", ...props }: BadgeProps) {
24
- return (
25
- <div
26
- className={cn(
27
- "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
28
- badgeVariants[variant],
29
- className
30
- )}
31
- {...props}
32
- />
33
- )
52
+ const iconOnlySizes = {
53
+ badge: { medium: "w-6 px-5", small: "w-6 px-3", tiny: "w-4 px-2" },
54
+ chip: { medium: "w-6 px-5", small: "w-6 px-3", tiny: "w-4 px-2" },
34
55
  }
35
56
 
57
+ export interface BadgeProps
58
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "type"> {
59
+ type?: "badge" | "chip"
60
+ variant?: keyof typeof badgeColors
61
+ color?: keyof (typeof badgeColors)["filled"]
62
+ size?: keyof (typeof badgeSizes)["badge"]
63
+ iconOnly?: boolean
64
+ // chip only
65
+ selected?: boolean
66
+ onDismiss?: () => void
67
+ }
68
+
69
+ const Badge = React.forwardRef<HTMLElement, BadgeProps>(
70
+ (
71
+ {
72
+ className,
73
+ type = "badge",
74
+ variant = "filled",
75
+ color = "default",
76
+ size = "medium",
77
+ iconOnly = false,
78
+ selected = false,
79
+ onDismiss,
80
+ children,
81
+ ...props
82
+ },
83
+ ref
84
+ ) => {
85
+ const isChip = type === "chip"
86
+ const activeVariant = selected ? "filled" : variant
87
+ const Comp = (isChip ? "button" : "div") as "button"
88
+ return (
89
+ <Comp
90
+ ref={ref as React.Ref<HTMLButtonElement>}
91
+ {...(isChip && {
92
+ type: "button" as const,
93
+ "aria-pressed": selected || undefined,
94
+ })}
95
+ className={cn(
96
+ "inline-flex items-center justify-center whitespace-nowrap font-medium [&_svg]:shrink-0",
97
+ isChip &&
98
+ " transition-colors focus-visible:outline-none focus-visible:shadow-focus disabled:pointer-events-none disabled:border-grey-200 disabled:bg-muted disabled:text-grey-400",
99
+ badgeColors[activeVariant][color],
100
+ isChip && chipInteractions[activeVariant][color],
101
+ badgeSizes[type][size],
102
+ iconOnly && iconOnlySizes[type][size],
103
+ className
104
+ )}
105
+ {...props}
106
+ >
107
+ {children}
108
+ {isChip && onDismiss && (
109
+ <X
110
+ role="button"
111
+ aria-label="Remove"
112
+ className="cursor-pointer"
113
+ onClick={(event) => {
114
+ event.stopPropagation()
115
+ onDismiss()
116
+ }}
117
+ />
118
+ )}
119
+ </Comp>
120
+ )
121
+ }
122
+ )
123
+ Badge.displayName = "Badge"
124
+
36
125
  export { Badge }
@@ -0,0 +1,108 @@
1
+ import * as React from "react"
2
+ import { ChevronRight, MoreHorizontal } from "lucide-react"
3
+ import { cn } from "../../lib/utils"
4
+
5
+ const Breadcrumb = React.forwardRef<
6
+ HTMLElement,
7
+ React.HTMLAttributes<HTMLElement>
8
+ >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
9
+ Breadcrumb.displayName = "Breadcrumb"
10
+
11
+ const BreadcrumbList = React.forwardRef<
12
+ HTMLOListElement,
13
+ React.OlHTMLAttributes<HTMLOListElement>
14
+ >(({ className, ...props }, ref) => (
15
+ <ol
16
+ ref={ref}
17
+ className={cn(
18
+ "flex flex-wrap items-center gap-2 text-b3 text-muted-foreground",
19
+ className
20
+ )}
21
+ {...props}
22
+ />
23
+ ))
24
+ BreadcrumbList.displayName = "BreadcrumbList"
25
+
26
+ const BreadcrumbItem = React.forwardRef<
27
+ HTMLLIElement,
28
+ React.LiHTMLAttributes<HTMLLIElement>
29
+ >(({ className, ...props }, ref) => (
30
+ <li
31
+ ref={ref}
32
+ className={cn("inline-flex items-center gap-2", className)}
33
+ {...props}
34
+ />
35
+ ))
36
+ BreadcrumbItem.displayName = "BreadcrumbItem"
37
+
38
+ const BreadcrumbLink = React.forwardRef<
39
+ HTMLAnchorElement,
40
+ React.AnchorHTMLAttributes<HTMLAnchorElement>
41
+ >(({ className, ...props }, ref) => (
42
+ <a
43
+ ref={ref}
44
+ className={cn(
45
+ "rounded-xxs text-b3 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:shadow-focus aria-disabled:pointer-events-none aria-disabled:text-grey-400",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ ))
51
+ BreadcrumbLink.displayName = "BreadcrumbLink"
52
+
53
+ const BreadcrumbPage = React.forwardRef<
54
+ HTMLSpanElement,
55
+ React.HTMLAttributes<HTMLSpanElement>
56
+ >(({ className, ...props }, ref) => (
57
+ <span
58
+ ref={ref}
59
+ role="link"
60
+ aria-disabled="true"
61
+ aria-current="page"
62
+ className={cn("text-b4 text-foreground", className)}
63
+ {...props}
64
+ />
65
+ ))
66
+ BreadcrumbPage.displayName = "BreadcrumbPage"
67
+
68
+ const BreadcrumbSeparator = ({
69
+ children,
70
+ className,
71
+ ...props
72
+ }: React.HTMLAttributes<HTMLLIElement>) => (
73
+ <li
74
+ role="presentation"
75
+ aria-hidden="true"
76
+ className={cn("[&_svg]:size-4", className)}
77
+ {...props}
78
+ >
79
+ {children ?? <ChevronRight />}
80
+ </li>
81
+ )
82
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
83
+
84
+ const BreadcrumbEllipsis = ({
85
+ className,
86
+ ...props
87
+ }: React.HTMLAttributes<HTMLSpanElement>) => (
88
+ <span
89
+ role="presentation"
90
+ aria-hidden="true"
91
+ className={cn("flex size-5 items-center justify-center", className)}
92
+ {...props}
93
+ >
94
+ <MoreHorizontal className="size-4" />
95
+ <span className="sr-only">More</span>
96
+ </span>
97
+ )
98
+ BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis"
99
+
100
+ export {
101
+ Breadcrumb,
102
+ BreadcrumbList,
103
+ BreadcrumbItem,
104
+ BreadcrumbLink,
105
+ BreadcrumbPage,
106
+ BreadcrumbSeparator,
107
+ BreadcrumbEllipsis,
108
+ }
@@ -1,24 +1,57 @@
1
1
  import * as React from "react"
2
2
  import { Slot } from "@radix-ui/react-slot"
3
- import { clsx, type ClassValue } from "clsx"
4
- import { twMerge } from "tailwind-merge"
3
+ import { cn } from "../../lib/utils"
5
4
 
6
- export function cn(...inputs: ClassValue[]) {
7
- return twMerge(clsx(inputs))
5
+ const buttonVariants = {
6
+ filled: "bg-primary text-white hover:bg-primary-700 active:bg-primary-800 disabled:bg-muted disabled:text-grey-400",
7
+ outline:
8
+ "border border-primary bg-transparent text-primary hover:bg-accent active:bg-primary-100 disabled:border-grey-200 disabled:text-grey-400",
9
+ clear: "bg-transparent text-primary hover:bg-accent active:bg-primary-100 disabled:text-grey-400",
10
+ }
11
+
12
+ const buttonSizes = {
13
+ giant: "h-14 gap-2 px-6 [font-size:var(--text-btn-giant)] [line-height:var(--text-btn-giant--line-height)] [font-weight:var(--text-btn-giant--font-weight)] rounded-md",
14
+ large: "h-12 gap-2 px-5 [font-size:var(--text-btn-large)] [line-height:var(--text-btn-large--line-height)] [font-weight:var(--text-btn-large--font-weight)] rounded-md",
15
+ medium: "h-10 gap-2 px-4 [font-size:var(--text-btn-medium)] [line-height:var(--text-btn-medium--line-height)] [font-weight:var(--text-btn-medium--font-weight)] rounded-md",
16
+ small: "h-8 gap-1 px-3 [font-size:var(--text-btn-small)] [line-height:var(--text-btn-small--line-height)] [font-weight:var(--text-btn-small--font-weight)] rounded-sm",
17
+ tiny: "h-6 gap-1 px-2 [font-size:var(--text-btn-tiny)] [line-height:var(--text-btn-tiny--line-height)] [font-weight:var(--text-btn-tiny--font-weight)] rounded-sm",
18
+ }
19
+
20
+ const iconOnlySizes = {
21
+ giant: "h-14 w-14 [font-size:var(--text-btn-giant)] [line-height:var(--text-btn-giant--line-height)] [font-weight:var(--text-btn-giant--font-weight)]",
22
+ large: "h-12 w-12 [font-size:var(--text-btn-large)] [line-height:var(--text-btn-large--line-height)] [font-weight:var(--text-btn-large--font-weight)]",
23
+ medium: "h-10 w-10 [font-size:var(--text-btn-medium)] [line-height:var(--text-btn-medium--line-height)] [font-weight:var(--text-btn-medium--font-weight)]",
24
+ small: "h-8 w-8 [font-size:var(--text-btn-small)] [line-height:var(--text-btn-small--line-height)] [font-weight:var(--text-btn-small--font-weight)]",
25
+ tiny: "h-6 w-6 [font-size:var(--text-btn-tiny)] [line-height:var(--text-btn-tiny--line-height)] [font-weight:var(--text-btn-tiny--font-weight)]",
8
26
  }
9
27
 
10
28
  export interface ButtonProps
11
29
  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
30
+ variant?: keyof typeof buttonVariants
31
+ size?: keyof typeof buttonSizes
32
+ iconOnly?: boolean
12
33
  asChild?: boolean
13
34
  }
14
35
 
15
36
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
16
- ({ className, asChild = false, ...props }, ref) => {
37
+ (
38
+ {
39
+ className,
40
+ variant = "filled",
41
+ size = "medium",
42
+ iconOnly = false,
43
+ asChild = false,
44
+ ...props
45
+ },
46
+ ref
47
+ ) => {
17
48
  const Comp = asChild ? Slot : "button"
18
49
  return (
19
50
  <Comp
20
51
  className={cn(
21
- "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2",
52
+ "inline-flex items-center justify-center whitespace-nowrap font-semibold transition-colors focus-visible:outline-none focus-visible:shadow-focus disabled:pointer-events-none [&_svg]:size-[1.25em] [&_svg]:shrink-0",
53
+ iconOnly ? iconOnlySizes[size] : buttonSizes[size],
54
+ buttonVariants[variant],
22
55
  className
23
56
  )}
24
57
  ref={ref}
@@ -0,0 +1,88 @@
1
+ import * as React from "react"
2
+ import { cn } from "../../lib/utils"
3
+
4
+ // Figma: Button Group items share Button's outline style and full size ramp
5
+ // (Position=Left/Center/Right joined edge-to-edge, State=Selected fills).
6
+ const buttonGroupItemSizes = {
7
+ giant: "h-14 gap-2 px-6 text-btn-giant",
8
+ large: "h-12 gap-2 px-5 text-btn-large",
9
+ medium: "h-10 gap-2 px-4 text-btn-medium",
10
+ small: "h-8 gap-1 px-3 text-btn-small",
11
+ tiny: "h-6 gap-1 px-2 text-btn-tiny",
12
+ }
13
+
14
+ const iconOnlySizes = {
15
+ giant: "h-14 w-14",
16
+ large: "h-12 w-12",
17
+ medium: "h-10 w-10",
18
+ small: "h-8 w-8",
19
+ tiny: "h-6 w-6",
20
+ }
21
+
22
+ interface ButtonGroupContextValue {
23
+ size: keyof typeof buttonGroupItemSizes
24
+ value?: string
25
+ onValueChange?: (value: string) => void
26
+ }
27
+
28
+ const ButtonGroupContext = React.createContext<ButtonGroupContextValue>({
29
+ size: "medium",
30
+ })
31
+
32
+ export interface ButtonGroupProps
33
+ extends React.HTMLAttributes<HTMLDivElement> {
34
+ size?: keyof typeof buttonGroupItemSizes
35
+ value?: string
36
+ onValueChange?: (value: string) => void
37
+ }
38
+
39
+ const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
40
+ ({ className, size = "medium", value, onValueChange, ...props }, ref) => (
41
+ <ButtonGroupContext.Provider value={{ size, value, onValueChange }}>
42
+ <div
43
+ ref={ref}
44
+ role="group"
45
+ className={cn("inline-flex items-center", className)}
46
+ {...props}
47
+ />
48
+ </ButtonGroupContext.Provider>
49
+ )
50
+ )
51
+ ButtonGroup.displayName = "ButtonGroup"
52
+
53
+ export interface ButtonGroupItemProps
54
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
55
+ value: string
56
+ iconOnly?: boolean
57
+ }
58
+
59
+ const ButtonGroupItem = React.forwardRef<
60
+ HTMLButtonElement,
61
+ ButtonGroupItemProps
62
+ >(({ className, value, iconOnly = false, onClick, ...props }, ref) => {
63
+ const ctx = React.useContext(ButtonGroupContext)
64
+ const selected = ctx.value === value
65
+ return (
66
+ <button
67
+ ref={ref}
68
+ type="button"
69
+ aria-pressed={selected}
70
+ onClick={(event) => {
71
+ ctx.onValueChange?.(value)
72
+ onClick?.(event)
73
+ }}
74
+ className={cn(
75
+ "-ml-px inline-flex items-center justify-center whitespace-nowrap border border-primary font-semibold transition-colors first:-ml-0 first:rounded-l-xs last:rounded-r-xs focus-visible:z-10 focus-visible:outline-none focus-visible:shadow-focus disabled:pointer-events-none disabled:border-grey-200 disabled:text-grey-400 [&_svg]:size-[1.25em] [&_svg]:shrink-0",
76
+ iconOnly ? iconOnlySizes[ctx.size] : buttonGroupItemSizes[ctx.size],
77
+ selected
78
+ ? "z-10 bg-primary text-primary-foreground"
79
+ : "bg-transparent text-primary hover:bg-accent active:bg-primary-100",
80
+ className
81
+ )}
82
+ {...props}
83
+ />
84
+ )
85
+ })
86
+ ButtonGroupItem.displayName = "ButtonGroupItem"
87
+
88
+ export { ButtonGroup, ButtonGroupItem }