@bearmenu/ui 0.5.2 → 0.6.1
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/dist/{chunk-ERMIA7NP.js → chunk-7PPCH42H.js} +3 -3
- package/dist/{chunk-HLQ2IRE7.js → chunk-BAZFVSSG.js} +3 -3
- package/dist/chunk-D5QHEHXJ.js +26 -0
- package/dist/{chunk-UISA4CIG.js → chunk-N2KNBPAQ.js} +2 -2
- package/dist/{chunk-7H54PTVH.js → chunk-PSR7N36A.js} +38 -10
- package/dist/chunk-RIDXFY6M.js +38 -0
- package/dist/chunk-UUJVXN2O.js +82 -0
- package/dist/chunk-WKMLZDUT.js +122 -0
- package/dist/chunk-XK2ZDQVE.js +122 -0
- package/dist/{chunk-4KOMHDZD.js → chunk-Y5D5LP4A.js} +18 -3
- package/dist/chunk-YJ6OBKZJ.js +32 -0
- package/dist/{chunk-ESO2EM3L.js → chunk-ZBBUKXM5.js} +2 -2
- package/dist/chunk-ZFNOCP6W.js +106 -0
- package/dist/components/accordion.js +11 -5
- package/dist/components/alert-dialog.js +5 -3
- package/dist/components/alert.d.ts +4 -1
- package/dist/components/alert.js +25 -9
- package/dist/components/avatar.d.ts +17 -2
- package/dist/components/avatar.js +91 -12
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/breadcrumb.js +5 -2
- package/dist/components/button-group.d.ts +14 -0
- package/dist/components/button-group.js +39 -0
- package/dist/components/button.d.ts +3 -1
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.js +3 -3
- package/dist/components/card.d.ts +13 -5
- package/dist/components/card.js +2 -2
- package/dist/components/carousel.d.ts +20 -3
- package/dist/components/carousel.js +72 -12
- package/dist/components/checkbox.js +1 -1
- package/dist/components/circular-progress.d.ts +16 -0
- package/dist/components/circular-progress.js +105 -0
- package/dist/components/combobox.d.ts +27 -0
- package/dist/components/combobox.js +102 -0
- package/dist/components/command.d.ts +16 -3
- package/dist/components/command.js +2 -2
- package/dist/components/currency-input.d.ts +1 -0
- package/dist/components/currency-input.js +73 -52
- package/dist/components/date-input.d.ts +10 -0
- package/dist/components/date-input.js +40 -0
- package/dist/components/description-list.d.ts +15 -0
- package/dist/components/description-list.js +67 -0
- package/dist/components/dialog.d.ts +13 -2
- package/dist/components/dialog.js +1 -1
- package/dist/components/drawer.d.ts +5 -2
- package/dist/components/drawer.js +47 -9
- package/dist/components/dropdown-menu.d.ts +7 -2
- package/dist/components/dropdown-menu.js +22 -6
- package/dist/components/empty-state.js +2 -2
- package/dist/components/field.d.ts +22 -0
- package/dist/components/field.js +130 -0
- package/dist/components/form.d.ts +2 -1
- package/dist/components/form.js +11 -1
- package/dist/components/haptic-button.js +3 -3
- package/dist/components/input-group.d.ts +18 -0
- package/dist/components/input-group.js +92 -0
- package/dist/components/input.d.ts +8 -2
- package/dist/components/input.js +1 -1
- package/dist/components/item.d.ts +23 -0
- package/dist/components/item.js +120 -0
- package/dist/components/kbd.d.ts +13 -0
- package/dist/components/kbd.js +49 -0
- package/dist/components/lazy-markdown.d.ts +3 -1
- package/dist/components/lazy-markdown.js +7 -7
- package/dist/components/link-button.js +1 -1
- package/dist/components/multi-select-combobox.js +92 -83
- package/dist/components/multi-select-menu.d.ts +0 -4
- package/dist/components/multi-select-menu.js +28 -17
- package/dist/components/native-select.d.ts +12 -0
- package/dist/components/native-select.js +49 -0
- package/dist/components/pagination.d.ts +5 -2
- package/dist/components/pagination.js +66 -7
- package/dist/components/place-about-tab.js +4 -4
- package/dist/components/place-card.js +1 -1
- package/dist/components/place-details-mobile.js +4 -4
- package/dist/components/popover.d.ts +7 -1
- package/dist/components/popover.js +1 -1
- package/dist/components/progress.d.ts +5 -1
- package/dist/components/progress.js +17 -3
- package/dist/components/radio-group.js +12 -3
- package/dist/components/rating.d.ts +2 -0
- package/dist/components/rating.js +88 -70
- package/dist/components/scroll-fade.d.ts +15 -0
- package/dist/components/scroll-fade.js +116 -0
- package/dist/components/search-bar.d.ts +1 -2
- package/dist/components/search-bar.js +37 -60
- package/dist/components/searchable-select.js +76 -72
- package/dist/components/select.d.ts +5 -1
- package/dist/components/select.js +30 -14
- package/dist/components/selection-bar.d.ts +11 -0
- package/dist/components/selection-bar.js +65 -0
- package/dist/components/separator.js +3 -2
- package/dist/components/sheet.d.ts +1 -1
- package/dist/components/sheet.js +1 -1
- package/dist/components/skeleton-card.js +1 -1
- package/dist/components/skeleton.js +1 -1
- package/dist/components/slider.js +21 -4
- package/dist/components/sliding-panels.js +9 -6
- package/dist/components/spinner.d.ts +17 -0
- package/dist/components/spinner.js +50 -0
- package/dist/components/stepper.d.ts +18 -0
- package/dist/components/stepper.js +199 -0
- package/dist/components/switch.js +4 -3
- package/dist/components/table.d.ts +23 -5
- package/dist/components/table.js +121 -35
- package/dist/components/tabs.d.ts +3 -1
- package/dist/components/tabs.js +37 -9
- package/dist/components/tag-group.d.ts +9 -0
- package/dist/components/tag-group.js +26 -0
- package/dist/components/tag.d.ts +17 -0
- package/dist/components/tag.js +61 -0
- package/dist/components/text.d.ts +1 -1
- package/dist/components/text.js +1 -1
- package/dist/components/textarea.js +2 -1
- package/dist/components/toast.js +1 -1
- package/dist/components/toaster.js +1 -1
- package/dist/components/toggle-group.js +1 -1
- package/dist/components/toggle.js +1 -1
- package/dist/components/tooltip.d.ts +3 -2
- package/dist/components/tooltip.js +16 -2
- package/package.json +45 -26
- package/src/components/accordion.tsx +11 -6
- package/src/components/alert-dialog.tsx +4 -2
- package/src/components/alert.tsx +20 -8
- package/src/components/avatar.tsx +106 -13
- package/src/components/badge.tsx +12 -11
- package/src/components/breadcrumb.tsx +5 -2
- package/src/components/button-group.stories.tsx +52 -0
- package/src/components/button-group.tsx +40 -0
- package/src/components/button.test.tsx +174 -0
- package/src/components/button.tsx +42 -19
- package/src/components/calendar.tsx +2 -2
- package/src/components/card.tsx +55 -13
- package/src/components/carousel.tsx +85 -13
- package/src/components/checkbox.test.tsx +58 -0
- package/src/components/checkbox.tsx +8 -3
- package/src/components/circular-progress.stories.tsx +39 -0
- package/src/components/circular-progress.tsx +102 -0
- package/src/components/collapsible.stories.tsx +7 -3
- package/src/components/combobox.stories.tsx +82 -0
- package/src/components/combobox.test.tsx +118 -0
- package/src/components/combobox.tsx +136 -0
- package/src/components/command.test.tsx +91 -0
- package/src/components/command.tsx +38 -15
- package/src/components/currency-input.test.tsx +63 -0
- package/src/components/currency-input.tsx +84 -66
- package/src/components/date-input.stories.tsx +31 -0
- package/src/components/date-input.tsx +43 -0
- package/src/components/description-list.stories.tsx +62 -0
- package/src/components/description-list.tsx +68 -0
- package/src/components/dialog.test.tsx +84 -0
- package/src/components/dialog.tsx +64 -24
- package/src/components/drawer.tsx +72 -23
- package/src/components/dropdown-menu.test.tsx +180 -0
- package/src/components/dropdown-menu.tsx +31 -6
- package/src/components/field.stories.tsx +36 -0
- package/src/components/field.test.tsx +157 -0
- package/src/components/field.tsx +146 -0
- package/src/components/form.stories.tsx +3 -2
- package/src/components/form.test.tsx +95 -0
- package/src/components/form.tsx +9 -0
- package/src/components/haptic-button.tsx +1 -1
- package/src/components/input-group.stories.tsx +61 -0
- package/src/components/input-group.tsx +99 -0
- package/src/components/input.test.tsx +103 -0
- package/src/components/input.tsx +24 -7
- package/src/components/item.stories.tsx +52 -0
- package/src/components/item.tsx +115 -0
- package/src/components/kbd.stories.tsx +33 -0
- package/src/components/kbd.tsx +46 -0
- package/src/components/lazy-markdown.tsx +10 -7
- package/src/components/multi-select-combobox.test.tsx +101 -0
- package/src/components/multi-select-combobox.tsx +85 -82
- package/src/components/multi-select-menu.tsx +49 -41
- package/src/components/native-select.stories.tsx +52 -0
- package/src/components/native-select.tsx +45 -0
- package/src/components/pagination.tsx +64 -5
- package/src/components/popover.tsx +66 -3
- package/src/components/progress.tsx +27 -4
- package/src/components/radio-group.test.tsx +64 -0
- package/src/components/radio-group.tsx +11 -3
- package/src/components/rating.test.tsx +112 -0
- package/src/components/rating.tsx +97 -82
- package/src/components/scroll-fade.stories.tsx +98 -0
- package/src/components/scroll-fade.tsx +127 -0
- package/src/components/search-bar.stories.tsx +1 -32
- package/src/components/search-bar.tsx +22 -59
- package/src/components/searchable-select.tsx +69 -73
- package/src/components/select.tsx +34 -19
- package/src/components/selection-bar.stories.tsx +37 -0
- package/src/components/selection-bar.tsx +65 -0
- package/src/components/separator.tsx +5 -2
- package/src/components/sheet.tsx +1 -1
- package/src/components/skeleton.tsx +2 -2
- package/src/components/slider.test.tsx +45 -0
- package/src/components/slider.tsx +31 -12
- package/src/components/sliding-panels.tsx +8 -5
- package/src/components/spinner.stories.tsx +35 -0
- package/src/components/spinner.tsx +50 -0
- package/src/components/stepper.stories.tsx +71 -0
- package/src/components/stepper.tsx +220 -0
- package/src/components/switch.test.tsx +52 -0
- package/src/components/switch.tsx +4 -3
- package/src/components/table.tsx +140 -35
- package/src/components/tabs.test.tsx +107 -0
- package/src/components/tabs.tsx +70 -23
- package/src/components/tag-group.stories.tsx +39 -0
- package/src/components/tag-group.tsx +28 -0
- package/src/components/tag.stories.tsx +50 -0
- package/src/components/tag.tsx +63 -0
- package/src/components/text.test.tsx +1 -1
- package/src/components/text.tsx +19 -3
- package/src/components/textarea.test.tsx +48 -0
- package/src/components/textarea.tsx +2 -1
- package/src/components/toast.tsx +4 -3
- package/src/components/toggle.tsx +2 -2
- package/src/components/tooltip.tsx +12 -2
- package/src/globals.css +126 -90
- package/dist/chunk-2BMJRXQX.js +0 -80
- package/dist/chunk-4AVS6CV4.js +0 -89
- package/dist/chunk-4UO73SKC.js +0 -26
- package/dist/chunk-CW24PLDW.js +0 -24
- package/dist/chunk-IQASCH2V.js +0 -25
- package/dist/chunk-NKHH23YK.js +0 -31
- package/dist/chunk-QJ54Z7TC.js +0 -83
|
@@ -2,20 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
6
|
import { cn } from "../lib/utils";
|
|
6
7
|
|
|
8
|
+
const avatarVariants = cva(
|
|
9
|
+
"relative flex shrink-0 overflow-hidden rounded-full bg-muted",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
size: {
|
|
13
|
+
xs: "size-6 text-[10px]",
|
|
14
|
+
sm: "size-8 text-xs",
|
|
15
|
+
default: "size-10 text-sm",
|
|
16
|
+
lg: "size-12 text-base",
|
|
17
|
+
xl: "size-16 text-lg",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
size: "default",
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const AvatarContext = React.createContext<VariantProps<typeof avatarVariants>["size"]>("default");
|
|
27
|
+
|
|
7
28
|
const Avatar = React.forwardRef<
|
|
8
29
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
9
|
-
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
10
|
-
>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
30
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> &
|
|
31
|
+
VariantProps<typeof avatarVariants>
|
|
32
|
+
>(({ className, size = "default", ...props }, ref) => (
|
|
33
|
+
<AvatarContext.Provider value={size}>
|
|
34
|
+
<AvatarPrimitive.Root
|
|
35
|
+
ref={ref}
|
|
36
|
+
data-slot="avatar"
|
|
37
|
+
className={cn(avatarVariants({ size }), className)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
</AvatarContext.Provider>
|
|
19
41
|
));
|
|
20
42
|
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
|
21
43
|
|
|
@@ -25,7 +47,8 @@ const AvatarImage = React.forwardRef<
|
|
|
25
47
|
>(({ className, ...props }, ref) => (
|
|
26
48
|
<AvatarPrimitive.Image
|
|
27
49
|
ref={ref}
|
|
28
|
-
|
|
50
|
+
data-slot="avatar-image"
|
|
51
|
+
className={cn("aspect-square size-full object-cover", className)}
|
|
29
52
|
{...props}
|
|
30
53
|
/>
|
|
31
54
|
));
|
|
@@ -37,8 +60,9 @@ const AvatarFallback = React.forwardRef<
|
|
|
37
60
|
>(({ className, ...props }, ref) => (
|
|
38
61
|
<AvatarPrimitive.Fallback
|
|
39
62
|
ref={ref}
|
|
63
|
+
data-slot="avatar-fallback"
|
|
40
64
|
className={cn(
|
|
41
|
-
"flex
|
|
65
|
+
"flex size-full items-center justify-center rounded-full bg-muted font-medium text-muted-foreground",
|
|
42
66
|
className
|
|
43
67
|
)}
|
|
44
68
|
{...props}
|
|
@@ -46,4 +70,73 @@ const AvatarFallback = React.forwardRef<
|
|
|
46
70
|
));
|
|
47
71
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
|
48
72
|
|
|
49
|
-
|
|
73
|
+
const badgeOffset = {
|
|
74
|
+
xs: "size-2 ring-1",
|
|
75
|
+
sm: "size-2.5 ring-2",
|
|
76
|
+
default: "size-3 ring-2",
|
|
77
|
+
lg: "size-3.5 ring-2",
|
|
78
|
+
xl: "size-4 ring-2",
|
|
79
|
+
} as const;
|
|
80
|
+
|
|
81
|
+
const AvatarBadge = React.forwardRef<
|
|
82
|
+
HTMLSpanElement,
|
|
83
|
+
React.HTMLAttributes<HTMLSpanElement> & {
|
|
84
|
+
placement?: "top-right" | "bottom-right";
|
|
85
|
+
tone?: "success" | "warning" | "destructive" | "muted";
|
|
86
|
+
}
|
|
87
|
+
>(({ className, placement = "bottom-right", tone = "success", ...props }, ref) => {
|
|
88
|
+
const size = React.useContext(AvatarContext) ?? "default";
|
|
89
|
+
return (
|
|
90
|
+
<span
|
|
91
|
+
ref={ref}
|
|
92
|
+
data-slot="avatar-badge"
|
|
93
|
+
className={cn(
|
|
94
|
+
"absolute block rounded-full ring-background",
|
|
95
|
+
badgeOffset[size as keyof typeof badgeOffset],
|
|
96
|
+
placement === "top-right" ? "right-0 top-0" : "bottom-0 right-0",
|
|
97
|
+
tone === "success" && "bg-success",
|
|
98
|
+
tone === "warning" && "bg-warning",
|
|
99
|
+
tone === "destructive" && "bg-destructive",
|
|
100
|
+
tone === "muted" && "bg-muted-foreground",
|
|
101
|
+
className
|
|
102
|
+
)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
AvatarBadge.displayName = "AvatarBadge";
|
|
108
|
+
|
|
109
|
+
const AvatarGroup = React.forwardRef<
|
|
110
|
+
HTMLDivElement,
|
|
111
|
+
React.HTMLAttributes<HTMLDivElement> & { max?: number; spacing?: "tight" | "default" | "loose" }
|
|
112
|
+
>(({ className, children, max, spacing = "default", ...props }, ref) => {
|
|
113
|
+
const items = React.Children.toArray(children);
|
|
114
|
+
const visible = max ? items.slice(0, max) : items;
|
|
115
|
+
const overflow = max ? items.length - max : 0;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div
|
|
119
|
+
ref={ref}
|
|
120
|
+
data-slot="avatar-group"
|
|
121
|
+
className={cn(
|
|
122
|
+
"flex items-center",
|
|
123
|
+
spacing === "tight" && "-space-x-3",
|
|
124
|
+
spacing === "default" && "-space-x-2",
|
|
125
|
+
spacing === "loose" && "-space-x-1",
|
|
126
|
+
"[&>*]:ring-2 [&>*]:ring-background",
|
|
127
|
+
className
|
|
128
|
+
)}
|
|
129
|
+
{...props}
|
|
130
|
+
>
|
|
131
|
+
{visible}
|
|
132
|
+
{overflow > 0 && (
|
|
133
|
+
<span className="relative inline-flex size-10 items-center justify-center rounded-full bg-muted text-xs font-medium text-muted-foreground ring-2 ring-background">
|
|
134
|
+
+{overflow}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
AvatarGroup.displayName = "AvatarGroup";
|
|
141
|
+
|
|
142
|
+
export { Avatar, AvatarImage, AvatarFallback, AvatarBadge, AvatarGroup, avatarVariants };
|
package/src/components/badge.tsx
CHANGED
|
@@ -3,23 +3,24 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
3
3
|
import { cn } from "../lib/utils";
|
|
4
4
|
|
|
5
5
|
const badgeVariants = cva(
|
|
6
|
-
"inline-flex items-center rounded-lg border px-2.5 py-0.5 text-xs font-semibold transition-
|
|
6
|
+
"inline-flex items-center rounded-lg border px-2.5 py-0.5 text-xs font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
7
7
|
{
|
|
8
8
|
variants: {
|
|
9
9
|
variant: {
|
|
10
|
-
default:
|
|
11
|
-
"border-transparent bg-primary/80 text-primary-foreground shadow-lg hover:bg-primary hover:shadow-xl hover:shadow-primary/20",
|
|
10
|
+
default: "border-transparent bg-primary text-primary-foreground",
|
|
12
11
|
secondary:
|
|
13
|
-
"border-transparent bg-secondary
|
|
12
|
+
"border-transparent bg-secondary text-secondary-foreground",
|
|
14
13
|
destructive:
|
|
15
|
-
"border-transparent bg-destructive
|
|
16
|
-
outline: "text-foreground
|
|
17
|
-
overlay:
|
|
18
|
-
|
|
14
|
+
"border-transparent bg-destructive text-destructive-foreground",
|
|
15
|
+
outline: "text-foreground border-border",
|
|
16
|
+
overlay:
|
|
17
|
+
"border-transparent bg-foreground/70 text-background backdrop-blur-sm",
|
|
18
|
+
glass: "glass text-foreground border-border/50",
|
|
19
19
|
success:
|
|
20
|
-
"border-transparent bg-
|
|
20
|
+
"border-transparent bg-success text-success-foreground",
|
|
21
21
|
warning:
|
|
22
|
-
"border-transparent bg-
|
|
22
|
+
"border-transparent bg-warning text-warning-foreground",
|
|
23
|
+
brown: "border-transparent bg-brown text-brown-foreground",
|
|
23
24
|
},
|
|
24
25
|
},
|
|
25
26
|
defaultVariants: {
|
|
@@ -32,7 +33,7 @@ export interface BadgeProps
|
|
|
32
33
|
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
|
33
34
|
|
|
34
35
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
35
|
-
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
|
36
|
+
return <div data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export { Badge, badgeVariants };
|
|
@@ -43,7 +43,10 @@ function BreadcrumbLink({
|
|
|
43
43
|
return (
|
|
44
44
|
<Comp
|
|
45
45
|
data-slot="breadcrumb-link"
|
|
46
|
-
className={cn(
|
|
46
|
+
className={cn(
|
|
47
|
+
"rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
47
50
|
{...props}
|
|
48
51
|
/>
|
|
49
52
|
);
|
|
@@ -56,7 +59,7 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
|
|
56
59
|
role="link"
|
|
57
60
|
aria-disabled="true"
|
|
58
61
|
aria-current="page"
|
|
59
|
-
className={cn("text-foreground
|
|
62
|
+
className={cn("font-medium text-foreground", className)}
|
|
60
63
|
{...props}
|
|
61
64
|
/>
|
|
62
65
|
);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { ButtonGroup } from "./button-group";
|
|
3
|
+
import { Button } from "./button";
|
|
4
|
+
import { AlignLeft, AlignCenter, AlignRight } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/ButtonGroup",
|
|
8
|
+
component: ButtonGroup,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
} satisfies Meta<typeof ButtonGroup>;
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof meta>;
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<ButtonGroup>
|
|
18
|
+
<Button variant="outline">One</Button>
|
|
19
|
+
<Button variant="outline">Two</Button>
|
|
20
|
+
<Button variant="outline">Three</Button>
|
|
21
|
+
</ButtonGroup>
|
|
22
|
+
),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const Icons: Story = {
|
|
26
|
+
render: () => (
|
|
27
|
+
<ButtonGroup>
|
|
28
|
+
<Button variant="outline" size="icon" aria-label="Align left"><AlignLeft className="size-4" /></Button>
|
|
29
|
+
<Button variant="outline" size="icon" aria-label="Align center"><AlignCenter className="size-4" /></Button>
|
|
30
|
+
<Button variant="outline" size="icon" aria-label="Align right"><AlignRight className="size-4" /></Button>
|
|
31
|
+
</ButtonGroup>
|
|
32
|
+
),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const Vertical: Story = {
|
|
36
|
+
render: () => (
|
|
37
|
+
<ButtonGroup orientation="vertical">
|
|
38
|
+
<Button variant="outline">Top</Button>
|
|
39
|
+
<Button variant="outline">Middle</Button>
|
|
40
|
+
<Button variant="outline">Bottom</Button>
|
|
41
|
+
</ButtonGroup>
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const Detached: Story = {
|
|
46
|
+
render: () => (
|
|
47
|
+
<ButtonGroup attached={false}>
|
|
48
|
+
<Button>Save</Button>
|
|
49
|
+
<Button variant="outline">Cancel</Button>
|
|
50
|
+
</ButtonGroup>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
const buttonGroupVariants = cva("inline-flex", {
|
|
6
|
+
variants: {
|
|
7
|
+
orientation: {
|
|
8
|
+
horizontal:
|
|
9
|
+
"flex-row [&>[data-slot=button]]:rounded-none [&>[data-slot=button]:first-child]:rounded-l-xl [&>[data-slot=button]:last-child]:rounded-r-xl [&>[data-slot=button]:not(:first-child)]:-ml-px",
|
|
10
|
+
vertical:
|
|
11
|
+
"flex-col [&>[data-slot=button]]:rounded-none [&>[data-slot=button]:first-child]:rounded-t-xl [&>[data-slot=button]:last-child]:rounded-b-xl [&>[data-slot=button]:not(:first-child)]:-mt-px",
|
|
12
|
+
},
|
|
13
|
+
attached: {
|
|
14
|
+
true: "",
|
|
15
|
+
false: "gap-1",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
orientation: "horizontal",
|
|
20
|
+
attached: true,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
type ButtonGroupProps = React.HTMLAttributes<HTMLDivElement> &
|
|
25
|
+
VariantProps<typeof buttonGroupVariants>;
|
|
26
|
+
|
|
27
|
+
const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
|
|
28
|
+
({ className, orientation, attached, ...props }, ref) => (
|
|
29
|
+
<div
|
|
30
|
+
ref={ref}
|
|
31
|
+
role="group"
|
|
32
|
+
data-slot="button-group"
|
|
33
|
+
className={cn(buttonGroupVariants({ orientation, attached }), className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
ButtonGroup.displayName = "ButtonGroup";
|
|
39
|
+
|
|
40
|
+
export { ButtonGroup, buttonGroupVariants };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Button, buttonVariants } from "./button";
|
|
5
|
+
|
|
6
|
+
// ─── buttonVariants CVA ───────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
describe("buttonVariants", () => {
|
|
9
|
+
it("returns default variant + size classes when called with no args", () => {
|
|
10
|
+
const result = buttonVariants({});
|
|
11
|
+
expect(result).toContain("bg-primary");
|
|
12
|
+
expect(result).toContain("text-primary-foreground");
|
|
13
|
+
expect(result).toContain("h-11");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("maps variant prop to correct classes", () => {
|
|
17
|
+
expect(buttonVariants({ variant: "default" })).toContain("bg-primary");
|
|
18
|
+
expect(buttonVariants({ variant: "destructive" })).toContain("bg-destructive");
|
|
19
|
+
expect(buttonVariants({ variant: "outline" })).toContain("border-border");
|
|
20
|
+
expect(buttonVariants({ variant: "secondary" })).toContain("bg-secondary");
|
|
21
|
+
expect(buttonVariants({ variant: "ghost" })).toContain("hover:bg-accent");
|
|
22
|
+
expect(buttonVariants({ variant: "link" })).toContain("text-primary");
|
|
23
|
+
expect(buttonVariants({ variant: "glass" })).toContain("glass");
|
|
24
|
+
expect(buttonVariants({ variant: "brown" })).toContain("bg-brown");
|
|
25
|
+
expect(buttonVariants({ variant: "antagonist" })).toContain("bg-primary-foreground");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("maps size prop to correct classes", () => {
|
|
29
|
+
expect(buttonVariants({ size: "xs" })).toContain("h-8");
|
|
30
|
+
expect(buttonVariants({ size: "sm" })).toContain("h-9");
|
|
31
|
+
expect(buttonVariants({ size: "default" })).toContain("h-11");
|
|
32
|
+
expect(buttonVariants({ size: "lg" })).toContain("h-12");
|
|
33
|
+
expect(buttonVariants({ size: "icon" })).toContain("rounded-full");
|
|
34
|
+
expect(buttonVariants({ size: "icon-xs" })).toContain("h-8");
|
|
35
|
+
expect(buttonVariants({ size: "icon-sm" })).toContain("h-9");
|
|
36
|
+
expect(buttonVariants({ size: "icon-lg" })).toContain("h-12");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("applies fullWidth class when fullWidth is true", () => {
|
|
40
|
+
expect(buttonVariants({ fullWidth: true })).toContain("w-full");
|
|
41
|
+
expect(buttonVariants({ fullWidth: false })).not.toContain("w-full");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ─── <Button> component ───────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
describe("Button component", () => {
|
|
48
|
+
it("renders as <button> by default", () => {
|
|
49
|
+
render(<Button>click</Button>);
|
|
50
|
+
const el = screen.getByRole("button", { name: "click" });
|
|
51
|
+
expect(el.tagName).toBe("BUTTON");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("renders children correctly", () => {
|
|
55
|
+
render(<Button>label content</Button>);
|
|
56
|
+
expect(screen.getByText("label content")).toBeDefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("forwards ref to the underlying button element", () => {
|
|
60
|
+
const ref = { current: null as HTMLButtonElement | null };
|
|
61
|
+
render(<Button ref={ref}>ref</Button>);
|
|
62
|
+
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("renders as Slot child when asChild is true", () => {
|
|
66
|
+
render(
|
|
67
|
+
<Button asChild>
|
|
68
|
+
<a href="/x">link</a>
|
|
69
|
+
</Button>
|
|
70
|
+
);
|
|
71
|
+
const el = screen.getByRole("link", { name: "link" });
|
|
72
|
+
expect(el.tagName).toBe("A");
|
|
73
|
+
expect(el.getAttribute("href")).toBe("/x");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("applies data-slot=button attribute", () => {
|
|
77
|
+
render(<Button>x</Button>);
|
|
78
|
+
expect(screen.getByRole("button").getAttribute("data-slot")).toBe("button");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Disabled / loading states
|
|
82
|
+
|
|
83
|
+
it("disables the button when disabled prop is set", () => {
|
|
84
|
+
render(<Button disabled>x</Button>);
|
|
85
|
+
expect((screen.getByRole("button") as HTMLButtonElement).disabled).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("disables the button and sets aria-busy when isLoading", () => {
|
|
89
|
+
render(<Button isLoading>x</Button>);
|
|
90
|
+
const el = screen.getByRole("button");
|
|
91
|
+
expect((el as HTMLButtonElement).disabled).toBe(true);
|
|
92
|
+
expect(el.getAttribute("aria-busy")).toBe("true");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("renders spinner when isLoading", () => {
|
|
96
|
+
const { container } = render(<Button isLoading>x</Button>);
|
|
97
|
+
expect(container.querySelector(".animate-spin")).not.toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("does not set aria-busy when not loading", () => {
|
|
101
|
+
render(<Button>x</Button>);
|
|
102
|
+
expect(screen.getByRole("button").getAttribute("aria-busy")).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Click behavior
|
|
106
|
+
|
|
107
|
+
it("calls onClick when clicked", async () => {
|
|
108
|
+
const onClick = vi.fn();
|
|
109
|
+
const user = userEvent.setup();
|
|
110
|
+
render(<Button onClick={onClick}>x</Button>);
|
|
111
|
+
await user.click(screen.getByRole("button"));
|
|
112
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("does not call onClick when disabled", async () => {
|
|
116
|
+
const onClick = vi.fn();
|
|
117
|
+
const user = userEvent.setup();
|
|
118
|
+
render(
|
|
119
|
+
<Button onClick={onClick} disabled>
|
|
120
|
+
x
|
|
121
|
+
</Button>
|
|
122
|
+
);
|
|
123
|
+
await user.click(screen.getByRole("button"));
|
|
124
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("does not call onClick when loading", async () => {
|
|
128
|
+
const onClick = vi.fn();
|
|
129
|
+
const user = userEvent.setup();
|
|
130
|
+
render(
|
|
131
|
+
<Button onClick={onClick} isLoading>
|
|
132
|
+
x
|
|
133
|
+
</Button>
|
|
134
|
+
);
|
|
135
|
+
await user.click(screen.getByRole("button"));
|
|
136
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Class output
|
|
140
|
+
|
|
141
|
+
it("applies variant class from variant prop", () => {
|
|
142
|
+
render(<Button variant="destructive">x</Button>);
|
|
143
|
+
expect(screen.getByRole("button").className).toContain("bg-destructive");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("applies size class from size prop", () => {
|
|
147
|
+
render(<Button size="sm">x</Button>);
|
|
148
|
+
expect(screen.getByRole("button").className).toContain("h-9");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("applies fullWidth class when fullWidth is true", () => {
|
|
152
|
+
render(<Button fullWidth>x</Button>);
|
|
153
|
+
expect(screen.getByRole("button").className).toContain("w-full");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("merges className via twMerge (overrides defaults)", () => {
|
|
157
|
+
render(<Button className="bg-red-500">x</Button>);
|
|
158
|
+
const el = screen.getByRole("button");
|
|
159
|
+
expect(el.className).toContain("bg-red-500");
|
|
160
|
+
// twMerge strips conflicting bg-primary (but not hover:bg-primary/90)
|
|
161
|
+
expect(el.className).not.toMatch(/(^|\s)bg-primary(\s|$)/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("passes through arbitrary HTML attributes", () => {
|
|
165
|
+
render(
|
|
166
|
+
<Button id="my-btn" type="submit">
|
|
167
|
+
x
|
|
168
|
+
</Button>
|
|
169
|
+
);
|
|
170
|
+
const el = screen.getByRole("button") as HTMLButtonElement;
|
|
171
|
+
expect(el.id).toBe("my-btn");
|
|
172
|
+
expect(el.type).toBe("submit");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -1,40 +1,50 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { Slot } from "@radix-ui/react-slot";
|
|
3
3
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
4
5
|
import { cn } from "../lib/utils";
|
|
5
6
|
import { useHaptic } from "../hooks/use-haptic";
|
|
6
7
|
|
|
7
8
|
const buttonVariants = cva(
|
|
8
|
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-
|
|
9
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer [@media(pointer:coarse)]:active:scale-95",
|
|
9
10
|
{
|
|
10
11
|
variants: {
|
|
11
12
|
variant: {
|
|
12
13
|
default:
|
|
13
|
-
"bg-primary
|
|
14
|
+
"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 hover:shadow",
|
|
14
15
|
destructive:
|
|
15
|
-
"bg-destructive text-destructive-foreground shadow-
|
|
16
|
+
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/85 hover:shadow",
|
|
16
17
|
outline:
|
|
17
|
-
"border border-border bg-
|
|
18
|
+
"border border-border bg-background hover:bg-accent hover:text-accent-foreground hover:border-accent",
|
|
18
19
|
secondary:
|
|
19
|
-
"bg-secondary text-secondary-foreground shadow-
|
|
20
|
-
ghost: "hover:bg-
|
|
21
|
-
link: "text-primary underline-offset-4 hover:underline
|
|
22
|
-
glass: "glass glass-hover text-foreground
|
|
20
|
+
"bg-secondary text-secondary-foreground shadow-sm hover:bg-accent hover:text-accent-foreground border border-border hover:border-accent",
|
|
21
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
22
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
23
|
+
glass: "glass glass-hover text-foreground",
|
|
23
24
|
brown:
|
|
24
|
-
"bg-
|
|
25
|
+
"bg-brown text-brown-foreground shadow-sm hover:bg-brown/85 hover:shadow",
|
|
25
26
|
antagonist:
|
|
26
|
-
"bg-primary-foreground text-primary shadow-
|
|
27
|
+
"bg-primary-foreground text-primary shadow-sm hover:bg-accent hover:text-accent-foreground border border-border hover:border-accent",
|
|
27
28
|
},
|
|
28
29
|
size: {
|
|
29
|
-
|
|
30
|
+
xs: "h-8 rounded-lg px-3 text-xs [&_svg]:size-3.5",
|
|
30
31
|
sm: "h-9 rounded-lg px-4",
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
default: "h-11 px-6 py-2",
|
|
33
|
+
lg: "h-12 rounded-2xl px-8",
|
|
34
|
+
icon: "h-11 w-11 rounded-full",
|
|
35
|
+
"icon-xs": "h-8 w-8 rounded-full [&_svg]:size-3.5",
|
|
36
|
+
"icon-sm": "h-9 w-9 rounded-full",
|
|
37
|
+
"icon-lg": "h-12 w-12 rounded-full",
|
|
38
|
+
},
|
|
39
|
+
fullWidth: {
|
|
40
|
+
true: "w-full",
|
|
41
|
+
false: "",
|
|
33
42
|
},
|
|
34
43
|
},
|
|
35
44
|
defaultVariants: {
|
|
36
45
|
variant: "default",
|
|
37
46
|
size: "default",
|
|
47
|
+
fullWidth: false,
|
|
38
48
|
},
|
|
39
49
|
}
|
|
40
50
|
);
|
|
@@ -44,6 +54,7 @@ export interface ButtonProps
|
|
|
44
54
|
asChild?: boolean;
|
|
45
55
|
haptic?: boolean;
|
|
46
56
|
hapticStyle?: "light" | "medium" | "heavy";
|
|
57
|
+
isLoading?: boolean;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
@@ -52,10 +63,14 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
52
63
|
className,
|
|
53
64
|
variant,
|
|
54
65
|
size,
|
|
66
|
+
fullWidth,
|
|
55
67
|
asChild = false,
|
|
56
68
|
haptic = true,
|
|
57
69
|
hapticStyle = "light",
|
|
70
|
+
isLoading = false,
|
|
71
|
+
disabled,
|
|
58
72
|
onClick,
|
|
73
|
+
children,
|
|
59
74
|
...props
|
|
60
75
|
},
|
|
61
76
|
ref
|
|
@@ -64,28 +79,36 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
64
79
|
const { trigger } = useHaptic();
|
|
65
80
|
const [isMounted, setIsMounted] = React.useState(false);
|
|
66
81
|
|
|
67
|
-
// Track mounted state to prevent hydration issues
|
|
68
82
|
React.useEffect(() => {
|
|
69
83
|
setIsMounted(true);
|
|
70
84
|
}, []);
|
|
71
85
|
|
|
72
86
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
73
|
-
// Only trigger haptic feedback after component has mounted to prevent hydration issues
|
|
74
87
|
if (haptic && isMounted) {
|
|
75
88
|
trigger(hapticStyle);
|
|
76
89
|
}
|
|
77
|
-
|
|
78
|
-
// Call the original onClick handler
|
|
79
90
|
onClick?.(event);
|
|
80
91
|
};
|
|
81
92
|
|
|
82
93
|
return (
|
|
83
94
|
<Comp
|
|
84
|
-
|
|
95
|
+
data-slot="button"
|
|
96
|
+
className={cn(buttonVariants({ variant, size, fullWidth, className }))}
|
|
85
97
|
ref={ref}
|
|
86
98
|
onClick={handleClick}
|
|
99
|
+
disabled={disabled || isLoading}
|
|
100
|
+
aria-busy={isLoading || undefined}
|
|
87
101
|
{...props}
|
|
88
|
-
|
|
102
|
+
>
|
|
103
|
+
{isLoading ? (
|
|
104
|
+
<>
|
|
105
|
+
<Loader2 className="animate-spin" aria-hidden="true" />
|
|
106
|
+
{!asChild && children}
|
|
107
|
+
</>
|
|
108
|
+
) : (
|
|
109
|
+
children
|
|
110
|
+
)}
|
|
111
|
+
</Comp>
|
|
89
112
|
);
|
|
90
113
|
}
|
|
91
114
|
);
|
|
@@ -25,7 +25,7 @@ function Calendar({
|
|
|
25
25
|
<DayPicker
|
|
26
26
|
showOutsideDays={showOutsideDays}
|
|
27
27
|
className={cn(
|
|
28
|
-
"group/calendar bg-background p-3 [--cell-size:--spacing(
|
|
28
|
+
"group/calendar bg-background p-3 [--cell-size:--spacing(9)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
|
29
29
|
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
|
30
30
|
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
|
31
31
|
className
|
|
@@ -168,7 +168,7 @@ function CalendarDayButton({
|
|
|
168
168
|
data-range-end={modifiers.range_end}
|
|
169
169
|
data-range-middle={modifiers.range_middle}
|
|
170
170
|
className={cn(
|
|
171
|
-
"group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground flex aspect-square
|
|
171
|
+
"group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground flex aspect-square h-(--cell-size) w-(--cell-size) size-auto min-w-0 flex-col items-center justify-center gap-1 rounded-md p-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
|
172
172
|
defaultClassNames.day,
|
|
173
173
|
className
|
|
174
174
|
)}
|