@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.
Files changed (227) hide show
  1. package/dist/{chunk-ERMIA7NP.js → chunk-7PPCH42H.js} +3 -3
  2. package/dist/{chunk-HLQ2IRE7.js → chunk-BAZFVSSG.js} +3 -3
  3. package/dist/chunk-D5QHEHXJ.js +26 -0
  4. package/dist/{chunk-UISA4CIG.js → chunk-N2KNBPAQ.js} +2 -2
  5. package/dist/{chunk-7H54PTVH.js → chunk-PSR7N36A.js} +38 -10
  6. package/dist/chunk-RIDXFY6M.js +38 -0
  7. package/dist/chunk-UUJVXN2O.js +82 -0
  8. package/dist/chunk-WKMLZDUT.js +122 -0
  9. package/dist/chunk-XK2ZDQVE.js +122 -0
  10. package/dist/{chunk-4KOMHDZD.js → chunk-Y5D5LP4A.js} +18 -3
  11. package/dist/chunk-YJ6OBKZJ.js +32 -0
  12. package/dist/{chunk-ESO2EM3L.js → chunk-ZBBUKXM5.js} +2 -2
  13. package/dist/chunk-ZFNOCP6W.js +106 -0
  14. package/dist/components/accordion.js +11 -5
  15. package/dist/components/alert-dialog.js +5 -3
  16. package/dist/components/alert.d.ts +4 -1
  17. package/dist/components/alert.js +25 -9
  18. package/dist/components/avatar.d.ts +17 -2
  19. package/dist/components/avatar.js +91 -12
  20. package/dist/components/badge.d.ts +1 -1
  21. package/dist/components/badge.js +1 -1
  22. package/dist/components/breadcrumb.js +5 -2
  23. package/dist/components/button-group.d.ts +14 -0
  24. package/dist/components/button-group.js +39 -0
  25. package/dist/components/button.d.ts +3 -1
  26. package/dist/components/button.js +1 -1
  27. package/dist/components/calendar.js +3 -3
  28. package/dist/components/card.d.ts +13 -5
  29. package/dist/components/card.js +2 -2
  30. package/dist/components/carousel.d.ts +20 -3
  31. package/dist/components/carousel.js +72 -12
  32. package/dist/components/checkbox.js +1 -1
  33. package/dist/components/circular-progress.d.ts +16 -0
  34. package/dist/components/circular-progress.js +105 -0
  35. package/dist/components/combobox.d.ts +27 -0
  36. package/dist/components/combobox.js +102 -0
  37. package/dist/components/command.d.ts +16 -3
  38. package/dist/components/command.js +2 -2
  39. package/dist/components/currency-input.d.ts +1 -0
  40. package/dist/components/currency-input.js +73 -52
  41. package/dist/components/date-input.d.ts +10 -0
  42. package/dist/components/date-input.js +40 -0
  43. package/dist/components/description-list.d.ts +15 -0
  44. package/dist/components/description-list.js +67 -0
  45. package/dist/components/dialog.d.ts +13 -2
  46. package/dist/components/dialog.js +1 -1
  47. package/dist/components/drawer.d.ts +5 -2
  48. package/dist/components/drawer.js +47 -9
  49. package/dist/components/dropdown-menu.d.ts +7 -2
  50. package/dist/components/dropdown-menu.js +22 -6
  51. package/dist/components/empty-state.js +2 -2
  52. package/dist/components/field.d.ts +22 -0
  53. package/dist/components/field.js +130 -0
  54. package/dist/components/form.d.ts +2 -1
  55. package/dist/components/form.js +11 -1
  56. package/dist/components/haptic-button.js +3 -3
  57. package/dist/components/input-group.d.ts +18 -0
  58. package/dist/components/input-group.js +92 -0
  59. package/dist/components/input.d.ts +8 -2
  60. package/dist/components/input.js +1 -1
  61. package/dist/components/item.d.ts +23 -0
  62. package/dist/components/item.js +120 -0
  63. package/dist/components/kbd.d.ts +13 -0
  64. package/dist/components/kbd.js +49 -0
  65. package/dist/components/lazy-markdown.d.ts +3 -1
  66. package/dist/components/lazy-markdown.js +7 -7
  67. package/dist/components/link-button.js +1 -1
  68. package/dist/components/multi-select-combobox.js +92 -83
  69. package/dist/components/multi-select-menu.d.ts +0 -4
  70. package/dist/components/multi-select-menu.js +28 -17
  71. package/dist/components/native-select.d.ts +12 -0
  72. package/dist/components/native-select.js +49 -0
  73. package/dist/components/pagination.d.ts +5 -2
  74. package/dist/components/pagination.js +66 -7
  75. package/dist/components/place-about-tab.js +4 -4
  76. package/dist/components/place-card.js +1 -1
  77. package/dist/components/place-details-mobile.js +4 -4
  78. package/dist/components/popover.d.ts +7 -1
  79. package/dist/components/popover.js +1 -1
  80. package/dist/components/progress.d.ts +5 -1
  81. package/dist/components/progress.js +17 -3
  82. package/dist/components/radio-group.js +12 -3
  83. package/dist/components/rating.d.ts +2 -0
  84. package/dist/components/rating.js +88 -70
  85. package/dist/components/scroll-fade.d.ts +15 -0
  86. package/dist/components/scroll-fade.js +116 -0
  87. package/dist/components/search-bar.d.ts +1 -2
  88. package/dist/components/search-bar.js +37 -60
  89. package/dist/components/searchable-select.js +76 -72
  90. package/dist/components/select.d.ts +5 -1
  91. package/dist/components/select.js +30 -14
  92. package/dist/components/selection-bar.d.ts +11 -0
  93. package/dist/components/selection-bar.js +65 -0
  94. package/dist/components/separator.js +3 -2
  95. package/dist/components/sheet.d.ts +1 -1
  96. package/dist/components/sheet.js +1 -1
  97. package/dist/components/skeleton-card.js +1 -1
  98. package/dist/components/skeleton.js +1 -1
  99. package/dist/components/slider.js +21 -4
  100. package/dist/components/sliding-panels.js +9 -6
  101. package/dist/components/spinner.d.ts +17 -0
  102. package/dist/components/spinner.js +50 -0
  103. package/dist/components/stepper.d.ts +18 -0
  104. package/dist/components/stepper.js +199 -0
  105. package/dist/components/switch.js +4 -3
  106. package/dist/components/table.d.ts +23 -5
  107. package/dist/components/table.js +121 -35
  108. package/dist/components/tabs.d.ts +3 -1
  109. package/dist/components/tabs.js +37 -9
  110. package/dist/components/tag-group.d.ts +9 -0
  111. package/dist/components/tag-group.js +26 -0
  112. package/dist/components/tag.d.ts +17 -0
  113. package/dist/components/tag.js +61 -0
  114. package/dist/components/text.d.ts +1 -1
  115. package/dist/components/text.js +1 -1
  116. package/dist/components/textarea.js +2 -1
  117. package/dist/components/toast.js +1 -1
  118. package/dist/components/toaster.js +1 -1
  119. package/dist/components/toggle-group.js +1 -1
  120. package/dist/components/toggle.js +1 -1
  121. package/dist/components/tooltip.d.ts +3 -2
  122. package/dist/components/tooltip.js +16 -2
  123. package/package.json +45 -26
  124. package/src/components/accordion.tsx +11 -6
  125. package/src/components/alert-dialog.tsx +4 -2
  126. package/src/components/alert.tsx +20 -8
  127. package/src/components/avatar.tsx +106 -13
  128. package/src/components/badge.tsx +12 -11
  129. package/src/components/breadcrumb.tsx +5 -2
  130. package/src/components/button-group.stories.tsx +52 -0
  131. package/src/components/button-group.tsx +40 -0
  132. package/src/components/button.test.tsx +174 -0
  133. package/src/components/button.tsx +42 -19
  134. package/src/components/calendar.tsx +2 -2
  135. package/src/components/card.tsx +55 -13
  136. package/src/components/carousel.tsx +85 -13
  137. package/src/components/checkbox.test.tsx +58 -0
  138. package/src/components/checkbox.tsx +8 -3
  139. package/src/components/circular-progress.stories.tsx +39 -0
  140. package/src/components/circular-progress.tsx +102 -0
  141. package/src/components/collapsible.stories.tsx +7 -3
  142. package/src/components/combobox.stories.tsx +82 -0
  143. package/src/components/combobox.test.tsx +118 -0
  144. package/src/components/combobox.tsx +136 -0
  145. package/src/components/command.test.tsx +91 -0
  146. package/src/components/command.tsx +38 -15
  147. package/src/components/currency-input.test.tsx +63 -0
  148. package/src/components/currency-input.tsx +84 -66
  149. package/src/components/date-input.stories.tsx +31 -0
  150. package/src/components/date-input.tsx +43 -0
  151. package/src/components/description-list.stories.tsx +62 -0
  152. package/src/components/description-list.tsx +68 -0
  153. package/src/components/dialog.test.tsx +84 -0
  154. package/src/components/dialog.tsx +64 -24
  155. package/src/components/drawer.tsx +72 -23
  156. package/src/components/dropdown-menu.test.tsx +180 -0
  157. package/src/components/dropdown-menu.tsx +31 -6
  158. package/src/components/field.stories.tsx +36 -0
  159. package/src/components/field.test.tsx +157 -0
  160. package/src/components/field.tsx +146 -0
  161. package/src/components/form.stories.tsx +3 -2
  162. package/src/components/form.test.tsx +95 -0
  163. package/src/components/form.tsx +9 -0
  164. package/src/components/haptic-button.tsx +1 -1
  165. package/src/components/input-group.stories.tsx +61 -0
  166. package/src/components/input-group.tsx +99 -0
  167. package/src/components/input.test.tsx +103 -0
  168. package/src/components/input.tsx +24 -7
  169. package/src/components/item.stories.tsx +52 -0
  170. package/src/components/item.tsx +115 -0
  171. package/src/components/kbd.stories.tsx +33 -0
  172. package/src/components/kbd.tsx +46 -0
  173. package/src/components/lazy-markdown.tsx +10 -7
  174. package/src/components/multi-select-combobox.test.tsx +101 -0
  175. package/src/components/multi-select-combobox.tsx +85 -82
  176. package/src/components/multi-select-menu.tsx +49 -41
  177. package/src/components/native-select.stories.tsx +52 -0
  178. package/src/components/native-select.tsx +45 -0
  179. package/src/components/pagination.tsx +64 -5
  180. package/src/components/popover.tsx +66 -3
  181. package/src/components/progress.tsx +27 -4
  182. package/src/components/radio-group.test.tsx +64 -0
  183. package/src/components/radio-group.tsx +11 -3
  184. package/src/components/rating.test.tsx +112 -0
  185. package/src/components/rating.tsx +97 -82
  186. package/src/components/scroll-fade.stories.tsx +98 -0
  187. package/src/components/scroll-fade.tsx +127 -0
  188. package/src/components/search-bar.stories.tsx +1 -32
  189. package/src/components/search-bar.tsx +22 -59
  190. package/src/components/searchable-select.tsx +69 -73
  191. package/src/components/select.tsx +34 -19
  192. package/src/components/selection-bar.stories.tsx +37 -0
  193. package/src/components/selection-bar.tsx +65 -0
  194. package/src/components/separator.tsx +5 -2
  195. package/src/components/sheet.tsx +1 -1
  196. package/src/components/skeleton.tsx +2 -2
  197. package/src/components/slider.test.tsx +45 -0
  198. package/src/components/slider.tsx +31 -12
  199. package/src/components/sliding-panels.tsx +8 -5
  200. package/src/components/spinner.stories.tsx +35 -0
  201. package/src/components/spinner.tsx +50 -0
  202. package/src/components/stepper.stories.tsx +71 -0
  203. package/src/components/stepper.tsx +220 -0
  204. package/src/components/switch.test.tsx +52 -0
  205. package/src/components/switch.tsx +4 -3
  206. package/src/components/table.tsx +140 -35
  207. package/src/components/tabs.test.tsx +107 -0
  208. package/src/components/tabs.tsx +70 -23
  209. package/src/components/tag-group.stories.tsx +39 -0
  210. package/src/components/tag-group.tsx +28 -0
  211. package/src/components/tag.stories.tsx +50 -0
  212. package/src/components/tag.tsx +63 -0
  213. package/src/components/text.test.tsx +1 -1
  214. package/src/components/text.tsx +19 -3
  215. package/src/components/textarea.test.tsx +48 -0
  216. package/src/components/textarea.tsx +2 -1
  217. package/src/components/toast.tsx +4 -3
  218. package/src/components/toggle.tsx +2 -2
  219. package/src/components/tooltip.tsx +12 -2
  220. package/src/globals.css +126 -90
  221. package/dist/chunk-2BMJRXQX.js +0 -80
  222. package/dist/chunk-4AVS6CV4.js +0 -89
  223. package/dist/chunk-4UO73SKC.js +0 -26
  224. package/dist/chunk-CW24PLDW.js +0 -24
  225. package/dist/chunk-IQASCH2V.js +0 -25
  226. package/dist/chunk-NKHH23YK.js +0 -31
  227. 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
- >(({ className, ...props }, ref) => (
11
- <AvatarPrimitive.Root
12
- ref={ref}
13
- className={cn(
14
- "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full border border-border/50 shadow-lg backdrop-blur-md bg-card/50 transition-all duration-300 hover:shadow-xl hover:shadow-black/20 hover:border-accent/30",
15
- className
16
- )}
17
- {...props}
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
- className={cn("aspect-square h-full w-full object-cover", className)}
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 h-full w-full items-center justify-center rounded-full bg-muted text-muted-foreground text-sm font-medium backdrop-blur-md",
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
- export { Avatar, AvatarImage, AvatarFallback };
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 };
@@ -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-[background-color,color,box-shadow,border-color] duration-300 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 backdrop-blur-md",
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/80 text-secondary-foreground shadow-lg hover:bg-secondary hover:shadow-xl hover:shadow-secondary/20",
12
+ "border-transparent bg-secondary text-secondary-foreground",
14
13
  destructive:
15
- "border-transparent bg-destructive/80 text-destructive-foreground shadow-lg hover:bg-destructive hover:shadow-xl hover:shadow-destructive/20",
16
- outline: "text-foreground glass glass-hover",
17
- overlay: "border-transparent bg-black/50 text-white backdrop-blur-sm shadow-lg",
18
- glass: "glass glass-hover text-foreground border-border/50",
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-green-500/80 text-primary shadow-lg hover:bg-green-500 hover:shadow-xl hover:shadow-green-500/20",
20
+ "border-transparent bg-success text-success-foreground",
21
21
  warning:
22
- "border-transparent bg-yellow-500/80 text-primary shadow-lg hover:bg-yellow-500 hover:shadow-xl hover:shadow-yellow-500/20",
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("hover:text-foreground transition-colors", className)}
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 font-normal", className)}
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-3xl text-sm font-medium transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 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
+ "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 backdrop-blur-sm text-primary-foreground shadow-lg hover:bg-primary/85 border border-primary/20 transition-colors duration-200",
14
+ "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 hover:shadow",
14
15
  destructive:
15
- "bg-destructive text-destructive-foreground shadow-lg hover:bg-destructive/90 hover:shadow-destructive/20 backdrop-blur-sm border border-destructive/20",
16
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/85 hover:shadow",
16
17
  outline:
17
- "border border-border bg-sidebar backdrop-blur-md hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:border-sidebar-accent-foreground/20 hover:shadow-lg",
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-lg hover:bg-secondary/90 hover:shadow-secondary/20 backdrop-blur-sm border border-secondary/20",
20
- ghost: "hover:bg-foreground/10 hover:text-foreground transition-colors duration-200",
21
- link: "text-primary underline-offset-4 hover:underline hover:text-primary/80",
22
- glass: "glass glass-hover text-foreground hover:text-accent-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-[#482E1F] text-white shadow-lg hover:bg-[#352317] hover:shadow-[#482E1F]/20 backdrop-blur-sm border border-[#482E1F]/30 transition-all duration-300",
25
+ "bg-brown text-brown-foreground shadow-sm hover:bg-brown/85 hover:shadow",
25
26
  antagonist:
26
- "bg-primary-foreground text-primary shadow-lg hover:bg-primary-foreground/90 hover:shadow-primary-foreground/20 backdrop-blur-sm border border-primary-foreground/20 transition-all duration-300",
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
- default: "h-11 px-6 py-2",
30
+ xs: "h-8 rounded-lg px-3 text-xs [&_svg]:size-3.5",
30
31
  sm: "h-9 rounded-lg px-4",
31
- lg: "h-12 rounded-xl px-8",
32
- icon: "h-11 w-11",
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
- className={cn(buttonVariants({ variant, size, className }))}
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(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
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 size-auto w-full min-w-(--cell-size) flex-col gap-1 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",
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
  )}