cooterlabs 1.0.1 → 2.0.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,113 @@
1
+ import * as React from "react"
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
3
+ import { clsx, type ClassValue } from "clsx"
4
+ import { twMerge } from "tailwind-merge"
5
+
6
+ export function cn(...inputs: ClassValue[]) {
7
+ return twMerge(clsx(inputs))
8
+ }
9
+
10
+ const ActionSheet = DialogPrimitive.Root
11
+
12
+ const ActionSheetTrigger = DialogPrimitive.Trigger
13
+
14
+ const ActionSheetPortal = DialogPrimitive.Portal
15
+
16
+ const ActionSheetOverlay = React.forwardRef<
17
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
18
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
19
+ >(({ className, ...props }, ref) => (
20
+ <DialogPrimitive.Overlay
21
+ ref={ref}
22
+ className={cn(
23
+ "fixed inset-0 z-50 bg-black/50 transition-opacity data-[state=closed]:opacity-0",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ ))
29
+ ActionSheetOverlay.displayName = "ActionSheetOverlay"
30
+
31
+ const ActionSheetContent = React.forwardRef<
32
+ React.ElementRef<typeof DialogPrimitive.Content>,
33
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
34
+ >(({ className, children, ...props }, ref) => (
35
+ <ActionSheetPortal>
36
+ <ActionSheetOverlay />
37
+ <DialogPrimitive.Content
38
+ ref={ref}
39
+ className={cn(
40
+ "fixed inset-x-0 bottom-0 z-50 flex w-full flex-col bg-background rounded-t-lg px-4 pb-4 pt-2 transition-transform data-[state=closed]:translate-y-full",
41
+ className
42
+ )}
43
+ {...props}
44
+ >
45
+ <div className="mx-auto mb-2 h-1 w-10 rounded-full bg-grey-200" />
46
+ {children}
47
+ </DialogPrimitive.Content>
48
+ </ActionSheetPortal>
49
+ ))
50
+ ActionSheetContent.displayName = "ActionSheetContent"
51
+
52
+ const ActionSheetHeader = ({
53
+ className,
54
+ ...props
55
+ }: React.HTMLAttributes<HTMLDivElement>) => (
56
+ <div
57
+ className={cn(
58
+ "flex flex-col gap-1 px-4 py-3 text-center text-c1 text-muted-foreground",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ ActionSheetHeader.displayName = "ActionSheetHeader"
65
+
66
+ export interface ActionSheetItemProps
67
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
68
+ variant?: "default" | "destructive"
69
+ }
70
+
71
+ const ActionSheetItem = React.forwardRef<
72
+ HTMLButtonElement,
73
+ ActionSheetItemProps
74
+ >(({ className, variant = "default", ...props }, ref) => (
75
+ <button
76
+ ref={ref}
77
+ className={cn(
78
+ "flex h-12 w-full items-center justify-center gap-2 text-b2 transition-colors hover:bg-secondary active:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-5 [&_svg]:shrink-0",
79
+ variant === "destructive" ? "text-error" : "text-foreground",
80
+ className
81
+ )}
82
+ {...props}
83
+ />
84
+ ))
85
+ ActionSheetItem.displayName = "ActionSheetItem"
86
+
87
+ const ActionSheetCancel = React.forwardRef<
88
+ HTMLButtonElement,
89
+ React.ButtonHTMLAttributes<HTMLButtonElement>
90
+ >(({ className, ...props }, ref) => (
91
+ <DialogPrimitive.Close asChild>
92
+ <button
93
+ ref={ref}
94
+ className={cn(
95
+ "mt-2 flex h-12 w-full items-center justify-center rounded-xs border border-border bg-background text-b2 text-foreground transition-colors hover:bg-secondary active:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
96
+ className
97
+ )}
98
+ {...props}
99
+ />
100
+ </DialogPrimitive.Close>
101
+ ))
102
+ ActionSheetCancel.displayName = "ActionSheetCancel"
103
+
104
+ export {
105
+ ActionSheet,
106
+ ActionSheetTrigger,
107
+ ActionSheetPortal,
108
+ ActionSheetOverlay,
109
+ ActionSheetContent,
110
+ ActionSheetHeader,
111
+ ActionSheetItem,
112
+ ActionSheetCancel,
113
+ }
@@ -0,0 +1,96 @@
1
+ import * as React from "react"
2
+ import { clsx, type ClassValue } from "clsx"
3
+ import { twMerge } from "tailwind-merge"
4
+
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs))
7
+ }
8
+
9
+ const alertVariants = {
10
+ filled: {
11
+ default:
12
+ "bg-primary-600 text-white [&_[data-slot=alert-description]]:text-white/80",
13
+ success:
14
+ "bg-success-600 text-white [&_[data-slot=alert-description]]:text-white/80",
15
+ info: "bg-info-600 text-white [&_[data-slot=alert-description]]:text-white/80",
16
+ warning:
17
+ "bg-warning-600 text-white [&_[data-slot=alert-description]]:text-white/80",
18
+ error: "bg-error-600 text-white [&_[data-slot=alert-description]]:text-white/80",
19
+ },
20
+ soft: {
21
+ default:
22
+ "border border-primary-500 bg-primary-50 text-primary-500 [&_[data-slot=alert-description]]:text-muted-foreground",
23
+ success:
24
+ "border border-success-500 bg-success-50 text-success-500 [&_[data-slot=alert-description]]:text-muted-foreground",
25
+ info: "border border-info-500 bg-info-50 text-info-500 [&_[data-slot=alert-description]]:text-muted-foreground",
26
+ warning:
27
+ "border border-warning-500 bg-warning-50 text-warning-500 [&_[data-slot=alert-description]]:text-muted-foreground",
28
+ error: "border border-error-500 bg-error-50 text-error-500 [&_[data-slot=alert-description]]:text-muted-foreground",
29
+ },
30
+ }
31
+
32
+ export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
33
+ severity?: keyof (typeof alertVariants)["filled"]
34
+ appearance?: keyof typeof alertVariants
35
+ }
36
+
37
+ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
38
+ (
39
+ { className, severity = "default", appearance = "filled", ...props },
40
+ ref
41
+ ) => (
42
+ <div
43
+ ref={ref}
44
+ role="alert"
45
+ className={cn(
46
+ "relative rounded-xs p-4 [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:size-5 [&>svg~*]:pl-8",
47
+ alertVariants[appearance][severity],
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ )
53
+ )
54
+ Alert.displayName = "Alert"
55
+
56
+ const AlertTitle = React.forwardRef<
57
+ HTMLHeadingElement,
58
+ React.HTMLAttributes<HTMLHeadingElement>
59
+ >(({ className, ...props }, ref) => (
60
+ <h5
61
+ ref={ref}
62
+ className={cn("mb-1 text-s2 font-semibold", className)}
63
+ {...props}
64
+ />
65
+ ))
66
+ AlertTitle.displayName = "AlertTitle"
67
+
68
+ const AlertDescription = React.forwardRef<
69
+ HTMLDivElement,
70
+ React.HTMLAttributes<HTMLDivElement>
71
+ >(({ className, ...props }, ref) => (
72
+ <div
73
+ ref={ref}
74
+ data-slot="alert-description"
75
+ className={cn("text-b3", className)}
76
+ {...props}
77
+ />
78
+ ))
79
+ AlertDescription.displayName = "AlertDescription"
80
+
81
+ const AlertActions = React.forwardRef<
82
+ HTMLDivElement,
83
+ React.HTMLAttributes<HTMLDivElement>
84
+ >(({ className, ...props }, ref) => (
85
+ <div
86
+ ref={ref}
87
+ className={cn(
88
+ "mt-3 flex items-center gap-4 text-b4 font-semibold",
89
+ className
90
+ )}
91
+ {...props}
92
+ />
93
+ ))
94
+ AlertActions.displayName = "AlertActions"
95
+
96
+ export { Alert, AlertTitle, AlertDescription, AlertActions }
@@ -7,14 +7,31 @@ export function cn(...inputs: ClassValue[]) {
7
7
  return twMerge(clsx(inputs))
8
8
  }
9
9
 
10
+ const avatarSizes = {
11
+ tiny: "h-6 w-6",
12
+ small: "h-8 w-8",
13
+ medium: "h-10 w-10",
14
+ large: "h-12 w-12",
15
+ giant: "h-14 w-14",
16
+ "xl-giant": "h-16 w-16",
17
+ "xxl-giant": "h-20 w-20",
18
+ "xxxl-giant": "h-24 w-24",
19
+ }
20
+
21
+ export interface AvatarProps
22
+ extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> {
23
+ size?: keyof typeof avatarSizes
24
+ }
25
+
10
26
  const Avatar = React.forwardRef<
11
27
  React.ElementRef<typeof AvatarPrimitive.Root>,
12
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
13
- >(({ className, ...props }, ref) => (
28
+ AvatarProps
29
+ >(({ className, size = "medium", ...props }, ref) => (
14
30
  <AvatarPrimitive.Root
15
31
  ref={ref}
16
32
  className={cn(
17
- "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
33
+ "relative flex shrink-0 overflow-hidden rounded-full",
34
+ avatarSizes[size],
18
35
  className
19
36
  )}
20
37
  {...props}
@@ -41,7 +58,7 @@ const AvatarFallback = React.forwardRef<
41
58
  <AvatarPrimitive.Fallback
42
59
  ref={ref}
43
60
  className={cn(
44
- "flex h-full w-full items-center justify-center rounded-full bg-muted",
61
+ "flex h-full w-full items-center justify-center rounded-full bg-primary-100 text-label font-medium text-primary",
45
62
  className
46
63
  )}
47
64
  {...props}
@@ -1,4 +1,5 @@
1
1
  import * as React from "react"
2
+ import { X } from "lucide-react"
2
3
  import { clsx, type ClassValue } from "clsx"
3
4
  import { twMerge } from "tailwind-merge"
4
5
 
@@ -6,31 +7,121 @@ export function cn(...inputs: ClassValue[]) {
6
7
  return twMerge(clsx(inputs))
7
8
  }
8
9
 
9
- export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
10
- variant?: "default" | "secondary" | "destructive" | "outline"
10
+ const badgeVariants = {
11
+ filled: {
12
+ primary: "bg-primary-600 text-white",
13
+ grey: "bg-grey-600 text-white",
14
+ success: "bg-success-600 text-white",
15
+ info: "bg-info-600 text-white",
16
+ warning: "bg-warning-600 text-white",
17
+ error: "bg-error-600 text-white",
18
+ },
19
+ soft: {
20
+ primary: "bg-primary-50 text-primary-600",
21
+ grey: "bg-grey-100 text-grey-600",
22
+ success: "bg-success-50 text-success-600",
23
+ info: "bg-info-50 text-info-600",
24
+ warning: "bg-warning-50 text-warning-600",
25
+ error: "bg-error-50 text-error-600",
26
+ },
11
27
  }
12
28
 
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",
29
+ export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
30
+ color?: keyof (typeof badgeVariants)["filled"]
31
+ variant?: keyof typeof badgeVariants
21
32
  }
22
33
 
23
- function Badge({ className, variant = "default", ...props }: BadgeProps) {
24
- return (
34
+ const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
35
+ ({ className, color = "primary", variant = "filled", ...props }, ref) => (
25
36
  <div
37
+ ref={ref}
26
38
  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],
39
+ "inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2 py-0.5 text-c2 font-medium [&_svg]:size-3 [&_svg]:shrink-0",
40
+ badgeVariants[variant][color],
29
41
  className
30
42
  )}
31
43
  {...props}
32
44
  />
33
45
  )
46
+ )
47
+ Badge.displayName = "Badge"
48
+
49
+ const chipVariants = {
50
+ outline: {
51
+ default:
52
+ "border border-primary-500 bg-background text-primary-600 hover:bg-accent active:bg-primary-100",
53
+ success:
54
+ "border border-success-500 bg-background text-success-600 hover:bg-success-50 active:bg-success-100",
55
+ info: "border border-info-500 bg-background text-info-600 hover:bg-info-50 active:bg-info-100",
56
+ warning:
57
+ "border border-warning-500 bg-background text-warning-600 hover:bg-warning-50 active:bg-warning-100",
58
+ error: "border border-error-500 bg-background text-error-600 hover:bg-error-50 active:bg-error-100",
59
+ },
60
+ filled: {
61
+ default: "bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800",
62
+ success: "bg-success-600 text-white hover:bg-success-700 active:bg-success-800",
63
+ info: "bg-info-600 text-white hover:bg-info-700 active:bg-info-800",
64
+ warning: "bg-warning-600 text-white hover:bg-warning-700 active:bg-warning-800",
65
+ error: "bg-error-600 text-white hover:bg-error-700 active:bg-error-800",
66
+ },
67
+ }
68
+
69
+ const chipSizes = {
70
+ medium: "h-10 gap-2 px-4 text-b4 [&_svg]:size-4",
71
+ small: "h-8 gap-1.5 px-3 text-c2 [&_svg]:size-4",
72
+ tiny: "h-6 gap-1 px-2 text-c3 [&_svg]:size-3",
73
+ }
74
+
75
+ export interface ChipProps
76
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
77
+ color?: keyof (typeof chipVariants)["outline"]
78
+ variant?: keyof typeof chipVariants
79
+ size?: keyof typeof chipSizes
80
+ selected?: boolean
81
+ onDismiss?: () => void
34
82
  }
35
83
 
36
- export { Badge }
84
+ const Chip = React.forwardRef<HTMLButtonElement, ChipProps>(
85
+ (
86
+ {
87
+ className,
88
+ color = "default",
89
+ variant = "outline",
90
+ size = "medium",
91
+ selected = false,
92
+ onDismiss,
93
+ children,
94
+ ...props
95
+ },
96
+ ref
97
+ ) => (
98
+ <button
99
+ ref={ref}
100
+ type="button"
101
+ aria-pressed={selected || undefined}
102
+ className={cn(
103
+ "inline-flex items-center justify-center whitespace-nowrap rounded-full 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:border-grey-200 disabled:bg-muted disabled:text-grey-400 [&_svg]:shrink-0",
104
+ chipVariants[selected ? "filled" : variant][color],
105
+ chipSizes[size],
106
+ className
107
+ )}
108
+ {...props}
109
+ >
110
+ {children}
111
+ {onDismiss && (
112
+ <X
113
+ role="button"
114
+ aria-label="Remove"
115
+ className="cursor-pointer"
116
+ onClick={(event) => {
117
+ event.stopPropagation()
118
+ onDismiss()
119
+ }}
120
+ />
121
+ )}
122
+ </button>
123
+ )
124
+ )
125
+ Chip.displayName = "Chip"
126
+
127
+ export { Badge, Chip }
@@ -0,0 +1,113 @@
1
+ import * as React from "react"
2
+ import { ChevronRight, MoreHorizontal } from "lucide-react"
3
+ import { clsx, type ClassValue } from "clsx"
4
+ import { twMerge } from "tailwind-merge"
5
+
6
+ export function cn(...inputs: ClassValue[]) {
7
+ return twMerge(clsx(inputs))
8
+ }
9
+
10
+ const Breadcrumb = React.forwardRef<
11
+ HTMLElement,
12
+ React.HTMLAttributes<HTMLElement>
13
+ >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
14
+ Breadcrumb.displayName = "Breadcrumb"
15
+
16
+ const BreadcrumbList = React.forwardRef<
17
+ HTMLOListElement,
18
+ React.OlHTMLAttributes<HTMLOListElement>
19
+ >(({ className, ...props }, ref) => (
20
+ <ol
21
+ ref={ref}
22
+ className={cn(
23
+ "flex flex-wrap items-center gap-2 text-b3 text-muted-foreground",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ ))
29
+ BreadcrumbList.displayName = "BreadcrumbList"
30
+
31
+ const BreadcrumbItem = React.forwardRef<
32
+ HTMLLIElement,
33
+ React.LiHTMLAttributes<HTMLLIElement>
34
+ >(({ className, ...props }, ref) => (
35
+ <li
36
+ ref={ref}
37
+ className={cn("inline-flex items-center gap-2", className)}
38
+ {...props}
39
+ />
40
+ ))
41
+ BreadcrumbItem.displayName = "BreadcrumbItem"
42
+
43
+ const BreadcrumbLink = React.forwardRef<
44
+ HTMLAnchorElement,
45
+ React.AnchorHTMLAttributes<HTMLAnchorElement>
46
+ >(({ className, ...props }, ref) => (
47
+ <a
48
+ ref={ref}
49
+ className={cn(
50
+ "rounded-xxs text-b3 text-muted-foreground ring-offset-background transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 aria-disabled:pointer-events-none aria-disabled:text-grey-400",
51
+ className
52
+ )}
53
+ {...props}
54
+ />
55
+ ))
56
+ BreadcrumbLink.displayName = "BreadcrumbLink"
57
+
58
+ const BreadcrumbPage = React.forwardRef<
59
+ HTMLSpanElement,
60
+ React.HTMLAttributes<HTMLSpanElement>
61
+ >(({ className, ...props }, ref) => (
62
+ <span
63
+ ref={ref}
64
+ role="link"
65
+ aria-disabled="true"
66
+ aria-current="page"
67
+ className={cn("text-b4 text-foreground", className)}
68
+ {...props}
69
+ />
70
+ ))
71
+ BreadcrumbPage.displayName = "BreadcrumbPage"
72
+
73
+ const BreadcrumbSeparator = ({
74
+ children,
75
+ className,
76
+ ...props
77
+ }: React.HTMLAttributes<HTMLLIElement>) => (
78
+ <li
79
+ role="presentation"
80
+ aria-hidden="true"
81
+ className={cn("[&_svg]:size-4", className)}
82
+ {...props}
83
+ >
84
+ {children ?? <ChevronRight />}
85
+ </li>
86
+ )
87
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
88
+
89
+ const BreadcrumbEllipsis = ({
90
+ className,
91
+ ...props
92
+ }: React.HTMLAttributes<HTMLSpanElement>) => (
93
+ <span
94
+ role="presentation"
95
+ aria-hidden="true"
96
+ className={cn("flex size-5 items-center justify-center", className)}
97
+ {...props}
98
+ >
99
+ <MoreHorizontal className="size-4" />
100
+ <span className="sr-only">More</span>
101
+ </span>
102
+ )
103
+ BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis"
104
+
105
+ export {
106
+ Breadcrumb,
107
+ BreadcrumbList,
108
+ BreadcrumbItem,
109
+ BreadcrumbLink,
110
+ BreadcrumbPage,
111
+ BreadcrumbSeparator,
112
+ BreadcrumbEllipsis,
113
+ }
@@ -7,18 +7,56 @@ export function cn(...inputs: ClassValue[]) {
7
7
  return twMerge(clsx(inputs))
8
8
  }
9
9
 
10
+ const buttonVariants = {
11
+ filled: "bg-primary text-primary-foreground hover:bg-primary-700 active:bg-primary-800 disabled:bg-muted disabled:text-grey-400",
12
+ outline:
13
+ "border border-primary bg-transparent text-primary hover:bg-accent active:bg-primary-100 disabled:border-grey-200 disabled:text-grey-400",
14
+ clear: "bg-transparent text-primary hover:bg-accent active:bg-primary-100 disabled:text-grey-400",
15
+ }
16
+
17
+ const buttonSizes = {
18
+ giant: "h-14 gap-2 px-6 text-btn-giant",
19
+ large: "h-12 gap-2 px-5 text-btn-large",
20
+ medium: "h-10 gap-2 px-4 text-btn-medium",
21
+ small: "h-8 gap-1 px-3 text-btn-small",
22
+ tiny: "h-6 gap-1 px-2 text-btn-tiny",
23
+ }
24
+
25
+ const iconOnlySizes = {
26
+ giant: "h-14 w-14 text-btn-giant",
27
+ large: "h-12 w-12 text-btn-large",
28
+ medium: "h-10 w-10 text-btn-medium",
29
+ small: "h-8 w-8 text-btn-small",
30
+ tiny: "h-6 w-6 text-btn-tiny",
31
+ }
32
+
10
33
  export interface ButtonProps
11
34
  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
35
+ variant?: keyof typeof buttonVariants
36
+ size?: keyof typeof buttonSizes
37
+ iconOnly?: boolean
12
38
  asChild?: boolean
13
39
  }
14
40
 
15
41
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
16
- ({ className, asChild = false, ...props }, ref) => {
42
+ (
43
+ {
44
+ className,
45
+ variant = "filled",
46
+ size = "medium",
47
+ iconOnly = false,
48
+ asChild = false,
49
+ ...props
50
+ },
51
+ ref
52
+ ) => {
17
53
  const Comp = asChild ? Slot : "button"
18
54
  return (
19
55
  <Comp
20
56
  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",
57
+ "inline-flex items-center justify-center whitespace-nowrap rounded-xs font-semibold 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 [&_svg]:size-[1.25em] [&_svg]:shrink-0",
58
+ buttonVariants[variant],
59
+ iconOnly ? iconOnlySizes[size] : buttonSizes[size],
22
60
  className
23
61
  )}
24
62
  ref={ref}
@@ -0,0 +1,93 @@
1
+ import * as React from "react"
2
+ import { clsx, type ClassValue } from "clsx"
3
+ import { twMerge } from "tailwind-merge"
4
+
5
+ function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs))
7
+ }
8
+
9
+ // Figma: Button Group items share Button's outline style and full size ramp
10
+ // (Position=Left/Center/Right joined edge-to-edge, State=Selected fills).
11
+ const buttonGroupItemSizes = {
12
+ giant: "h-14 gap-2 px-6 text-btn-giant",
13
+ large: "h-12 gap-2 px-5 text-btn-large",
14
+ medium: "h-10 gap-2 px-4 text-btn-medium",
15
+ small: "h-8 gap-1 px-3 text-btn-small",
16
+ tiny: "h-6 gap-1 px-2 text-btn-tiny",
17
+ }
18
+
19
+ const iconOnlySizes = {
20
+ giant: "h-14 w-14",
21
+ large: "h-12 w-12",
22
+ medium: "h-10 w-10",
23
+ small: "h-8 w-8",
24
+ tiny: "h-6 w-6",
25
+ }
26
+
27
+ interface ButtonGroupContextValue {
28
+ size: keyof typeof buttonGroupItemSizes
29
+ value?: string
30
+ onValueChange?: (value: string) => void
31
+ }
32
+
33
+ const ButtonGroupContext = React.createContext<ButtonGroupContextValue>({
34
+ size: "medium",
35
+ })
36
+
37
+ export interface ButtonGroupProps
38
+ extends React.HTMLAttributes<HTMLDivElement> {
39
+ size?: keyof typeof buttonGroupItemSizes
40
+ value?: string
41
+ onValueChange?: (value: string) => void
42
+ }
43
+
44
+ const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
45
+ ({ className, size = "medium", value, onValueChange, ...props }, ref) => (
46
+ <ButtonGroupContext.Provider value={{ size, value, onValueChange }}>
47
+ <div
48
+ ref={ref}
49
+ role="group"
50
+ className={cn("inline-flex items-center", className)}
51
+ {...props}
52
+ />
53
+ </ButtonGroupContext.Provider>
54
+ )
55
+ )
56
+ ButtonGroup.displayName = "ButtonGroup"
57
+
58
+ export interface ButtonGroupItemProps
59
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
60
+ value: string
61
+ iconOnly?: boolean
62
+ }
63
+
64
+ const ButtonGroupItem = React.forwardRef<
65
+ HTMLButtonElement,
66
+ ButtonGroupItemProps
67
+ >(({ className, value, iconOnly = false, onClick, ...props }, ref) => {
68
+ const ctx = React.useContext(ButtonGroupContext)
69
+ const selected = ctx.value === value
70
+ return (
71
+ <button
72
+ ref={ref}
73
+ type="button"
74
+ aria-pressed={selected}
75
+ onClick={(event) => {
76
+ ctx.onValueChange?.(value)
77
+ onClick?.(event)
78
+ }}
79
+ className={cn(
80
+ "-ml-px inline-flex items-center justify-center whitespace-nowrap border border-primary font-semibold ring-offset-background transition-colors first:-ml-0 first:rounded-l-xs last:rounded-r-xs focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:border-grey-200 disabled:text-grey-400 [&_svg]:size-[1.25em] [&_svg]:shrink-0",
81
+ iconOnly ? iconOnlySizes[ctx.size] : buttonGroupItemSizes[ctx.size],
82
+ selected
83
+ ? "z-10 bg-primary text-primary-foreground"
84
+ : "bg-transparent text-primary hover:bg-accent active:bg-primary-100",
85
+ className
86
+ )}
87
+ {...props}
88
+ />
89
+ )
90
+ })
91
+ ButtonGroupItem.displayName = "ButtonGroupItem"
92
+
93
+ export { ButtonGroup, ButtonGroupItem }