@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
@@ -1,21 +1,42 @@
1
1
  import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
2
3
  import { cn } from "../lib/utils";
3
4
  import { textVariants } from "./text";
4
5
 
5
- type CardVariant = "glass" | "surface";
6
+ const cardVariants = cva("rounded-3xl text-card-foreground transition-colors", {
7
+ variants: {
8
+ variant: {
9
+ surface: "bg-card border border-border",
10
+ glass: "glass",
11
+ outline: "bg-transparent border border-border",
12
+ muted: "bg-muted border border-transparent",
13
+ },
14
+ size: {
15
+ sm: "[&_[data-slot=card-header]]:p-4 [&_[data-slot=card-content]]:p-4 [&_[data-slot=card-content]]:pt-0 [&_[data-slot=card-footer]]:p-4 [&_[data-slot=card-footer]]:pt-0",
16
+ default:
17
+ "[&_[data-slot=card-header]]:p-6 [&_[data-slot=card-content]]:p-6 [&_[data-slot=card-content]]:pt-0 [&_[data-slot=card-footer]]:p-6 [&_[data-slot=card-footer]]:pt-0",
18
+ lg: "[&_[data-slot=card-header]]:p-8 [&_[data-slot=card-content]]:p-8 [&_[data-slot=card-content]]:pt-0 [&_[data-slot=card-footer]]:p-8 [&_[data-slot=card-footer]]:pt-0",
19
+ },
20
+ interactive: {
21
+ true: "cursor-pointer hover:border-primary/40 hover:-translate-y-0.5 transition-[transform,border-color] duration-200",
22
+ false: "",
23
+ },
24
+ },
25
+ defaultVariants: {
26
+ variant: "surface",
27
+ size: "default",
28
+ interactive: false,
29
+ },
30
+ });
6
31
 
7
32
  const Card = React.forwardRef<
8
33
  HTMLDivElement,
9
- React.HTMLAttributes<HTMLDivElement> & { variant?: CardVariant }
10
- >(({ className, variant = "glass", ...props }, ref) => (
34
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof cardVariants>
35
+ >(({ className, variant, size, interactive, ...props }, ref) => (
11
36
  <div
12
37
  ref={ref}
13
- className={cn(
14
- variant === "glass"
15
- ? "glass rounded-[22px] border border-transparent text-card-foreground transition-[transform,border-color] duration-300 [@media(hover:hover)]:hover:-translate-y-0.5 hover:border-primary/40"
16
- : "bg-surface rounded-[22px] border border-border text-card-foreground transition-all duration-200 hover:border-primary/40 hover:-translate-y-0.5",
17
- className
18
- )}
38
+ data-slot="card"
39
+ className={cn(cardVariants({ variant, size, interactive }), className)}
19
40
  {...props}
20
41
  />
21
42
  ));
@@ -23,7 +44,12 @@ Card.displayName = "Card";
23
44
 
24
45
  const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
25
46
  ({ className, ...props }, ref) => (
26
- <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
47
+ <div
48
+ ref={ref}
49
+ data-slot="card-header"
50
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
51
+ {...props}
52
+ />
27
53
  )
28
54
  );
29
55
  CardHeader.displayName = "CardHeader";
@@ -32,6 +58,7 @@ const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivE
32
58
  ({ className, ...props }, ref) => (
33
59
  <div
34
60
  ref={ref}
61
+ data-slot="card-title"
35
62
  className={cn(textVariants({ variant: "h4", wrap: "balance" }), className)}
36
63
  {...props}
37
64
  />
@@ -43,6 +70,7 @@ const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HT
43
70
  ({ className, ...props }, ref) => (
44
71
  <div
45
72
  ref={ref}
73
+ data-slot="card-description"
46
74
  className={cn("text-sm", textVariants({ color: "muted", wrap: "pretty" }), className)}
47
75
  {...props}
48
76
  />
@@ -52,7 +80,7 @@ CardDescription.displayName = "CardDescription";
52
80
 
53
81
  const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
54
82
  ({ className, ...props }, ref) => (
55
- <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
83
+ <div ref={ref} data-slot="card-content" className={cn("p-6 pt-0", className)} {...props} />
56
84
  )
57
85
  );
58
86
  CardContent.displayName = "CardContent";
@@ -71,9 +99,23 @@ CardAction.displayName = "CardAction";
71
99
 
72
100
  const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
73
101
  ({ className, ...props }, ref) => (
74
- <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
102
+ <div
103
+ ref={ref}
104
+ data-slot="card-footer"
105
+ className={cn("flex items-center p-6 pt-0", className)}
106
+ {...props}
107
+ />
75
108
  )
76
109
  );
77
110
  CardFooter.displayName = "CardFooter";
78
111
 
79
- export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
112
+ export {
113
+ Card,
114
+ CardHeader,
115
+ CardFooter,
116
+ CardTitle,
117
+ CardAction,
118
+ CardDescription,
119
+ CardContent,
120
+ cardVariants,
121
+ };
@@ -16,6 +16,8 @@ type CarouselProps = {
16
16
  opts?: CarouselOptions;
17
17
  plugins?: CarouselPlugin;
18
18
  orientation?: "horizontal" | "vertical";
19
+ /** Gap between slides in pixels. Defaults to 16. */
20
+ gap?: number;
19
21
  setApi?: (api: CarouselApi) => void;
20
22
  };
21
23
 
@@ -24,19 +26,21 @@ type CarouselContextProps = {
24
26
  api: ReturnType<typeof useEmblaCarousel>[1];
25
27
  scrollPrev: () => void;
26
28
  scrollNext: () => void;
29
+ scrollTo: (index: number) => void;
27
30
  canScrollPrev: boolean;
28
31
  canScrollNext: boolean;
32
+ selectedIndex: number;
33
+ slideCount: number;
34
+ gap: number;
29
35
  } & CarouselProps;
30
36
 
31
37
  const CarouselContext = React.createContext<CarouselContextProps | null>(null);
32
38
 
33
39
  function useCarousel() {
34
40
  const context = React.useContext(CarouselContext);
35
-
36
41
  if (!context) {
37
42
  throw new Error("useCarousel must be used within a <Carousel />");
38
43
  }
39
-
40
44
  return context;
41
45
  }
42
46
 
@@ -45,6 +49,7 @@ function Carousel({
45
49
  opts,
46
50
  setApi,
47
51
  plugins,
52
+ gap = 16,
48
53
  className,
49
54
  children,
50
55
  ...props
@@ -58,11 +63,15 @@ function Carousel({
58
63
  );
59
64
  const [canScrollPrev, setCanScrollPrev] = React.useState(false);
60
65
  const [canScrollNext, setCanScrollNext] = React.useState(false);
66
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
67
+ const [slideCount, setSlideCount] = React.useState(0);
61
68
 
62
69
  const onSelect = React.useCallback((api: CarouselApi) => {
63
70
  if (!api) return;
64
71
  setCanScrollPrev(api.canScrollPrev());
65
72
  setCanScrollNext(api.canScrollNext());
73
+ setSelectedIndex(api.selectedScrollSnap());
74
+ setSlideCount(api.scrollSnapList().length);
66
75
  }, []);
67
76
 
68
77
  const scrollPrev = React.useCallback(() => {
@@ -73,6 +82,13 @@ function Carousel({
73
82
  api?.scrollNext();
74
83
  }, [api]);
75
84
 
85
+ const scrollTo = React.useCallback(
86
+ (index: number) => {
87
+ api?.scrollTo(index);
88
+ },
89
+ [api]
90
+ );
91
+
76
92
  const handleKeyDown = React.useCallback(
77
93
  (event: React.KeyboardEvent<HTMLDivElement>) => {
78
94
  if (event.key === "ArrowLeft") {
@@ -96,7 +112,6 @@ function Carousel({
96
112
  onSelect(api);
97
113
  api.on("reInit", onSelect);
98
114
  api.on("select", onSelect);
99
-
100
115
  return () => {
101
116
  api?.off("select", onSelect);
102
117
  };
@@ -106,13 +121,17 @@ function Carousel({
106
121
  <CarouselContext.Provider
107
122
  value={{
108
123
  carouselRef,
109
- api: api,
124
+ api,
110
125
  opts,
111
126
  orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
127
+ gap,
112
128
  scrollPrev,
113
129
  scrollNext,
130
+ scrollTo,
114
131
  canScrollPrev,
115
132
  canScrollNext,
133
+ selectedIndex,
134
+ slideCount,
116
135
  }}
117
136
  >
118
137
  <div
@@ -130,31 +149,36 @@ function Carousel({
130
149
  }
131
150
 
132
151
  function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
133
- const { carouselRef, orientation } = useCarousel();
152
+ const { carouselRef, orientation, gap } = useCarousel();
134
153
 
135
154
  return (
136
155
  <div ref={carouselRef} className="overflow-hidden" data-slot="carousel-content">
137
156
  <div
138
- className={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className)}
157
+ className={cn("flex", orientation === "horizontal" ? "" : "flex-col", className)}
158
+ style={
159
+ orientation === "horizontal"
160
+ ? { marginLeft: `-${gap}px` }
161
+ : { marginTop: `-${gap}px` }
162
+ }
139
163
  {...props}
140
164
  />
141
165
  </div>
142
166
  );
143
167
  }
144
168
 
145
- function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
146
- const { orientation } = useCarousel();
169
+ function CarouselItem({ className, style, ...props }: React.ComponentProps<"div">) {
170
+ const { orientation, gap } = useCarousel();
147
171
 
148
172
  return (
149
173
  <div
150
174
  role="group"
151
175
  aria-roledescription="slide"
152
176
  data-slot="carousel-item"
153
- className={cn(
154
- "min-w-0 shrink-0 grow-0 basis-full",
155
- orientation === "horizontal" ? "pl-4" : "pt-4",
156
- className
157
- )}
177
+ className={cn("min-w-0 shrink-0 grow-0 basis-full", className)}
178
+ style={{
179
+ ...(orientation === "horizontal" ? { paddingLeft: `${gap}px` } : { paddingTop: `${gap}px` }),
180
+ ...style,
181
+ }}
158
182
  {...props}
159
183
  />
160
184
  );
@@ -220,6 +244,51 @@ function CarouselNext({
220
244
  );
221
245
  }
222
246
 
247
+ function CarouselDots({ className, ...props }: React.ComponentProps<"div">) {
248
+ const { slideCount, selectedIndex, scrollTo } = useCarousel();
249
+
250
+ if (slideCount === 0) return null;
251
+
252
+ return (
253
+ <div
254
+ data-slot="carousel-dots"
255
+ className={cn("flex items-center justify-center gap-1.5", className)}
256
+ {...props}
257
+ >
258
+ {Array.from({ length: slideCount }).map((_, i) => (
259
+ <button
260
+ key={i}
261
+ type="button"
262
+ aria-label={`Go to slide ${i + 1}`}
263
+ aria-current={i === selectedIndex ? "true" : undefined}
264
+ onClick={() => scrollTo(i)}
265
+ className={cn(
266
+ "size-2 rounded-full transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
267
+ i === selectedIndex ? "w-6 bg-primary" : "bg-muted-foreground/30 hover:bg-muted-foreground/50"
268
+ )}
269
+ />
270
+ ))}
271
+ </div>
272
+ );
273
+ }
274
+
275
+ function CarouselCounter({ className, ...props }: React.ComponentProps<"div">) {
276
+ const { slideCount, selectedIndex } = useCarousel();
277
+
278
+ if (slideCount === 0) return null;
279
+
280
+ return (
281
+ <div
282
+ data-slot="carousel-counter"
283
+ aria-live="polite"
284
+ className={cn("text-xs tabular-nums text-muted-foreground", className)}
285
+ {...props}
286
+ >
287
+ {selectedIndex + 1} / {slideCount}
288
+ </div>
289
+ );
290
+ }
291
+
223
292
  export {
224
293
  type CarouselApi,
225
294
  Carousel,
@@ -227,4 +296,7 @@ export {
227
296
  CarouselItem,
228
297
  CarouselPrevious,
229
298
  CarouselNext,
299
+ CarouselDots,
300
+ CarouselCounter,
301
+ useCarousel,
230
302
  };
@@ -0,0 +1,58 @@
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 { Checkbox } from "./checkbox";
5
+
6
+ describe("Checkbox", () => {
7
+ it("renders an unchecked checkbox by default", () => {
8
+ render(<Checkbox aria-label="Accept" />);
9
+ const cb = screen.getByRole("checkbox", { name: /accept/i });
10
+ expect(cb).toHaveAttribute("data-state", "unchecked");
11
+ });
12
+
13
+ it("renders checked when `checked` is true", () => {
14
+ render(<Checkbox aria-label="Accept" checked onCheckedChange={() => {}} />);
15
+ expect(screen.getByRole("checkbox")).toHaveAttribute("data-state", "checked");
16
+ });
17
+
18
+ it("renders indeterminate state", () => {
19
+ render(<Checkbox aria-label="x" checked="indeterminate" onCheckedChange={() => {}} />);
20
+ expect(screen.getByRole("checkbox")).toHaveAttribute("data-state", "indeterminate");
21
+ });
22
+
23
+ it("calls onCheckedChange when clicked", async () => {
24
+ const user = userEvent.setup();
25
+ const onCheckedChange = vi.fn();
26
+ render(<Checkbox aria-label="x" onCheckedChange={onCheckedChange} />);
27
+ await user.click(screen.getByRole("checkbox"));
28
+ expect(onCheckedChange).toHaveBeenCalledWith(true);
29
+ });
30
+
31
+ it("calls onCheckedChange via Space key", async () => {
32
+ const user = userEvent.setup();
33
+ const onCheckedChange = vi.fn();
34
+ render(<Checkbox aria-label="x" onCheckedChange={onCheckedChange} />);
35
+ const cb = screen.getByRole("checkbox");
36
+ cb.focus();
37
+ await user.keyboard(" ");
38
+ expect(onCheckedChange).toHaveBeenCalledWith(true);
39
+ });
40
+
41
+ it("does not fire when disabled", async () => {
42
+ const user = userEvent.setup();
43
+ const onCheckedChange = vi.fn();
44
+ render(<Checkbox aria-label="x" disabled onCheckedChange={onCheckedChange} />);
45
+ await user.click(screen.getByRole("checkbox"));
46
+ expect(onCheckedChange).not.toHaveBeenCalled();
47
+ });
48
+
49
+ it("forwards the aria-invalid attribute", () => {
50
+ render(<Checkbox aria-label="x" aria-invalid />);
51
+ expect(screen.getByRole("checkbox")).toHaveAttribute("aria-invalid", "true");
52
+ });
53
+
54
+ it("merges className", () => {
55
+ render(<Checkbox aria-label="x" className="custom-class" />);
56
+ expect(screen.getByRole("checkbox").className).toContain("custom-class");
57
+ });
58
+ });
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5
- import { Check } from "lucide-react";
5
+ import { Check, Minus } from "lucide-react";
6
6
  import { cn } from "../lib/utils";
7
7
 
8
8
  const Checkbox = React.forwardRef<
@@ -11,14 +11,19 @@ const Checkbox = React.forwardRef<
11
11
  >(({ className, ...props }, ref) => (
12
12
  <CheckboxPrimitive.Root
13
13
  ref={ref}
14
+ data-slot="checkbox"
14
15
  className={cn(
15
- "peer h-5 w-5 shrink-0 rounded-md border border-border bg-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary data-[state=checked]:shadow-lg data-[state=checked]:shadow-primary/20 data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:shadow-lg data-[state=indeterminate]:shadow-primary/20 hover:border-primary/40 hover:shadow-md",
16
+ "peer size-5 shrink-0 rounded-md border border-border bg-background transition-all duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground data-[state=indeterminate]:border-primary hover:border-primary/50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/40",
16
17
  className
17
18
  )}
18
19
  {...props}
19
20
  >
20
21
  <CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
21
- <Check className="h-3.5 w-3.5 animate-in zoom-in-50 duration-200" />
22
+ {props.checked === "indeterminate" ? (
23
+ <Minus className="size-3.5 animate-in fade-in-0 zoom-in-50 duration-200 ease-out" strokeWidth={3} />
24
+ ) : (
25
+ <Check className="size-3.5 animate-in fade-in-0 zoom-in-50 duration-200 ease-out" strokeWidth={3} />
26
+ )}
22
27
  </CheckboxPrimitive.Indicator>
23
28
  </CheckboxPrimitive.Root>
24
29
  ));
@@ -0,0 +1,39 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { CircularProgress } from "./circular-progress";
3
+
4
+ const meta = {
5
+ title: "Components/CircularProgress",
6
+ component: CircularProgress,
7
+ tags: ["autodocs"],
8
+ } satisfies Meta<typeof CircularProgress>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Default: Story = { args: { value: 60, showLabel: true } };
14
+
15
+ export const Sizes: Story = {
16
+ render: () => (
17
+ <div className="flex items-center gap-4">
18
+ <CircularProgress value={75} size={24} />
19
+ <CircularProgress value={75} size={40} />
20
+ <CircularProgress value={75} size={56} showLabel />
21
+ <CircularProgress value={75} size={80} showLabel strokeWidth={6} />
22
+ </div>
23
+ ),
24
+ };
25
+
26
+ export const Tones: Story = {
27
+ render: () => (
28
+ <div className="flex items-center gap-4">
29
+ <CircularProgress value={60} tone="primary" showLabel />
30
+ <CircularProgress value={60} tone="success" showLabel />
31
+ <CircularProgress value={60} tone="warning" showLabel />
32
+ <CircularProgress value={60} tone="destructive" showLabel />
33
+ </div>
34
+ ),
35
+ };
36
+
37
+ export const Indeterminate: Story = {
38
+ args: { indeterminate: true, size: 40 },
39
+ };
@@ -0,0 +1,102 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+
6
+ type CircularProgressProps = React.HTMLAttributes<HTMLDivElement> & {
7
+ value?: number;
8
+ max?: number;
9
+ size?: number;
10
+ strokeWidth?: number;
11
+ tone?: "primary" | "success" | "warning" | "destructive";
12
+ showLabel?: boolean;
13
+ /** Indeterminate spinner mode. */
14
+ indeterminate?: boolean;
15
+ /** Accessible label. Defaults to "Progress". */
16
+ label?: string;
17
+ };
18
+
19
+ const toneClasses = {
20
+ primary: "stroke-primary",
21
+ success: "stroke-success",
22
+ warning: "stroke-warning",
23
+ destructive: "stroke-destructive",
24
+ };
25
+
26
+ const CircularProgress = React.forwardRef<HTMLDivElement, CircularProgressProps>(
27
+ (
28
+ {
29
+ className,
30
+ value = 0,
31
+ max = 100,
32
+ size = 40,
33
+ strokeWidth = 4,
34
+ tone = "primary",
35
+ showLabel = false,
36
+ indeterminate = false,
37
+ label = "Progress",
38
+ ...props
39
+ },
40
+ ref
41
+ ) => {
42
+ const radius = (size - strokeWidth) / 2;
43
+ const circumference = 2 * Math.PI * radius;
44
+ const clampedValue = Math.min(Math.max(value, 0), max);
45
+ const percent = (clampedValue / max) * 100;
46
+ const dashOffset = circumference - (circumference * percent) / 100;
47
+
48
+ return (
49
+ <div
50
+ ref={ref}
51
+ data-slot="circular-progress"
52
+ role="progressbar"
53
+ aria-valuemin={0}
54
+ aria-valuemax={max}
55
+ aria-valuenow={indeterminate ? undefined : clampedValue}
56
+ aria-label={label}
57
+ className={cn("relative inline-flex shrink-0 items-center justify-center", className)}
58
+ style={{ width: size, height: size }}
59
+ {...props}
60
+ >
61
+ <svg
62
+ width={size}
63
+ height={size}
64
+ viewBox={`0 0 ${size} ${size}`}
65
+ className={cn(indeterminate && "animate-spin")}
66
+ >
67
+ <circle
68
+ cx={size / 2}
69
+ cy={size / 2}
70
+ r={radius}
71
+ strokeWidth={strokeWidth}
72
+ fill="none"
73
+ className="stroke-muted"
74
+ />
75
+ <circle
76
+ cx={size / 2}
77
+ cy={size / 2}
78
+ r={radius}
79
+ strokeWidth={strokeWidth}
80
+ fill="none"
81
+ strokeLinecap="round"
82
+ className={cn("transition-[stroke-dashoffset] duration-300 ease-out", toneClasses[tone])}
83
+ style={{
84
+ strokeDasharray: circumference,
85
+ strokeDashoffset: indeterminate ? circumference * 0.75 : dashOffset,
86
+ transform: `rotate(-90deg)`,
87
+ transformOrigin: `${size / 2}px ${size / 2}px`,
88
+ }}
89
+ />
90
+ </svg>
91
+ {showLabel && !indeterminate && (
92
+ <span className="absolute text-xs font-medium tabular-nums text-foreground">
93
+ {Math.round(percent)}%
94
+ </span>
95
+ )}
96
+ </div>
97
+ );
98
+ }
99
+ );
100
+ CircularProgress.displayName = "CircularProgress";
101
+
102
+ export { CircularProgress };
@@ -1,7 +1,7 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./collapsible";
3
3
  import { Button } from "./button";
4
- import { ChevronsUpDown } from "lucide-react";
4
+ import { ChevronDown } from "lucide-react";
5
5
 
6
6
  const meta = {
7
7
  title: "Components/Collapsible",
@@ -18,8 +18,12 @@ export const Default: Story = {
18
18
  <div className="flex items-center justify-between space-x-4 px-4">
19
19
  <h4 className="text-sm font-semibold">@peduarte starred 3 repositories</h4>
20
20
  <CollapsibleTrigger asChild>
21
- <Button variant="ghost" size="sm" className="w-9 p-0">
22
- <ChevronsUpDown className="h-4 w-4" />
21
+ <Button
22
+ variant="ghost"
23
+ size="sm"
24
+ className="w-9 p-0 [&[data-state=open]>svg]:rotate-180"
25
+ >
26
+ <ChevronDown className="h-4 w-4 transition-transform duration-200" />
23
27
  <span className="sr-only">Toggle</span>
24
28
  </Button>
25
29
  </CollapsibleTrigger>
@@ -0,0 +1,82 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { Combobox, type ComboboxOption } from "./combobox";
4
+
5
+ const meta = {
6
+ title: "Components/Combobox",
7
+ component: Combobox,
8
+ tags: ["autodocs"],
9
+ } satisfies Meta<typeof Combobox>;
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof meta>;
13
+
14
+ const frameworks: ComboboxOption[] = [
15
+ { value: "next", label: "Next.js", description: "React framework" },
16
+ { value: "remix", label: "Remix", description: "Full-stack web framework" },
17
+ { value: "astro", label: "Astro", description: "Content-driven web framework" },
18
+ { value: "vite", label: "Vite", description: "Frontend tooling" },
19
+ { value: "nuxt", label: "Nuxt", description: "Vue framework" },
20
+ ];
21
+
22
+ export const Default: Story = {
23
+ render: () => {
24
+ const [value, setValue] = useState<string | undefined>("next");
25
+ return (
26
+ <div className="w-72">
27
+ <Combobox
28
+ options={frameworks}
29
+ value={value}
30
+ onValueChange={setValue}
31
+ placeholder="Pick a framework"
32
+ />
33
+ </div>
34
+ );
35
+ },
36
+ };
37
+
38
+ export const Empty: Story = {
39
+ render: () => {
40
+ const [value, setValue] = useState<string | undefined>(undefined);
41
+ return (
42
+ <div className="w-72">
43
+ <Combobox
44
+ options={frameworks}
45
+ value={value}
46
+ onValueChange={setValue}
47
+ placeholder="No selection"
48
+ />
49
+ </div>
50
+ );
51
+ },
52
+ };
53
+
54
+ export const Disabled: Story = {
55
+ render: () => (
56
+ <div className="w-72">
57
+ <Combobox
58
+ options={frameworks}
59
+ value="next"
60
+ onValueChange={() => {}}
61
+ disabled
62
+ />
63
+ </div>
64
+ ),
65
+ };
66
+
67
+ export const Loading: Story = {
68
+ render: () => {
69
+ const [value, setValue] = useState<string | undefined>(undefined);
70
+ return (
71
+ <div className="w-72">
72
+ <Combobox
73
+ options={[]}
74
+ value={value}
75
+ onValueChange={setValue}
76
+ loading
77
+ placeholder="Pick something…"
78
+ />
79
+ </div>
80
+ );
81
+ },
82
+ };