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.
- package/README.md +228 -45
- package/bin/index.js +376 -22
- package/package.json +12 -4
- package/templates/_lib/utils.ts +28 -0
- package/templates/action-sheet/action-sheet.tsx +108 -0
- package/templates/alert/alert.tsx +146 -0
- package/templates/avatar/avatar.tsx +20 -8
- package/templates/badge/badge.tsx +114 -25
- package/templates/breadcrumb/breadcrumb.tsx +108 -0
- package/templates/button/button.tsx +39 -6
- package/templates/button-group/button-group.tsx +88 -0
- package/templates/card/card.tsx +31 -24
- package/templates/checkbox/checkbox.tsx +5 -9
- package/templates/context-menu/context-menu.tsx +172 -0
- package/templates/dialog/dialog.tsx +122 -0
- package/templates/dropdown/dropdown.tsx +175 -0
- package/templates/input/input.tsx +80 -14
- package/templates/label/label.tsx +2 -7
- package/templates/list/list.tsx +91 -0
- package/templates/loader/loader.tsx +33 -0
- package/templates/navbar-bottom/navbar-bottom.tsx +71 -0
- package/templates/navbar-top/navbar-top.tsx +79 -0
- package/templates/page-control/page-control.tsx +50 -0
- package/templates/pagination/pagination.tsx +104 -0
- package/templates/progress/progress.tsx +64 -0
- package/templates/radio/radio.tsx +36 -0
- package/templates/select/select.tsx +113 -0
- package/templates/stepper/stepper.tsx +88 -0
- package/templates/tabs/tabs.tsx +52 -0
- package/templates/textarea/textarea.tsx +2 -7
- package/templates/toggle/toggle.tsx +22 -0
- package/templates/tooltip/tooltip.tsx +32 -0
|
@@ -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 {
|
|
4
|
-
import { twMerge } from "tailwind-merge"
|
|
3
|
+
import { cn } from "../../lib/utils"
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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 {
|
|
3
|
-
import {
|
|
2
|
+
import { X } from "lucide-react"
|
|
3
|
+
import { cn } from "../../lib/utils"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 {
|
|
4
|
-
import { twMerge } from "tailwind-merge"
|
|
3
|
+
import { cn } from "../../lib/utils"
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
(
|
|
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
|
|
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 }
|