@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
@@ -0,0 +1,52 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { NativeSelect } from "./native-select";
3
+
4
+ const meta = {
5
+ title: "Components/NativeSelect",
6
+ component: NativeSelect,
7
+ tags: ["autodocs"],
8
+ } satisfies Meta<typeof NativeSelect>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Default: Story = {
14
+ render: () => (
15
+ <NativeSelect className="w-64" defaultValue="">
16
+ <option value="" disabled>
17
+ Pick a fruit…
18
+ </option>
19
+ <option value="apple">Apple</option>
20
+ <option value="banana">Banana</option>
21
+ <option value="cherry">Cherry</option>
22
+ </NativeSelect>
23
+ ),
24
+ };
25
+
26
+ export const Sizes: Story = {
27
+ render: () => (
28
+ <div className="flex w-64 flex-col gap-3">
29
+ <NativeSelect size="sm" defaultValue="small">
30
+ <option value="small">Small</option>
31
+ </NativeSelect>
32
+ <NativeSelect size="default" defaultValue="default">
33
+ <option value="default">Default</option>
34
+ </NativeSelect>
35
+ <NativeSelect size="lg" defaultValue="large">
36
+ <option value="large">Large</option>
37
+ </NativeSelect>
38
+ </div>
39
+ ),
40
+ };
41
+
42
+ export const Invalid: Story = {
43
+ render: () => (
44
+ <NativeSelect className="w-64" aria-invalid defaultValue="">
45
+ <option value="" disabled>
46
+ Required field…
47
+ </option>
48
+ <option value="a">Option A</option>
49
+ <option value="b">Option B</option>
50
+ </NativeSelect>
51
+ ),
52
+ };
@@ -0,0 +1,45 @@
1
+ import * as React from "react";
2
+ import { ChevronDown } from "lucide-react";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "../lib/utils";
5
+
6
+ const nativeSelectVariants = cva(
7
+ "w-full appearance-none rounded-xl border border-border bg-background pr-9 text-sm text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/40",
8
+ {
9
+ variants: {
10
+ size: {
11
+ sm: "h-9 pl-3",
12
+ default: "h-11 pl-4",
13
+ lg: "h-12 pl-5",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ size: "default",
18
+ },
19
+ }
20
+ );
21
+
22
+ type NativeSelectProps = Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "size"> &
23
+ VariantProps<typeof nativeSelectVariants>;
24
+
25
+ const NativeSelect = React.forwardRef<HTMLSelectElement, NativeSelectProps>(
26
+ ({ className, size, children, ...props }, ref) => (
27
+ <div className="relative inline-flex w-full">
28
+ <select
29
+ ref={ref}
30
+ data-slot="native-select"
31
+ className={cn(nativeSelectVariants({ size }), className)}
32
+ {...props}
33
+ >
34
+ {children}
35
+ </select>
36
+ <ChevronDown
37
+ aria-hidden
38
+ className="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
39
+ />
40
+ </div>
41
+ )
42
+ );
43
+ NativeSelect.displayName = "NativeSelect";
44
+
45
+ export { NativeSelect, nativeSelectVariants };
@@ -1,5 +1,11 @@
1
1
  import * as React from "react";
2
- import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react";
2
+ import {
3
+ ChevronLeftIcon,
4
+ ChevronRightIcon,
5
+ ChevronsLeftIcon,
6
+ ChevronsRightIcon,
7
+ MoreHorizontalIcon,
8
+ } from "lucide-react";
3
9
 
4
10
  import { cn } from "../lib/utils";
5
11
  import { Button, buttonVariants } from "./button";
@@ -32,20 +38,33 @@ function PaginationItem({ ...props }: React.ComponentProps<"li">) {
32
38
 
33
39
  type PaginationLinkProps = {
34
40
  isActive?: boolean;
41
+ disabled?: boolean;
35
42
  } & Pick<React.ComponentProps<typeof Button>, "size"> &
36
43
  React.ComponentProps<"a">;
37
44
 
38
- function PaginationLink({ className, isActive, size = "icon", ...props }: PaginationLinkProps) {
45
+ function PaginationLink({
46
+ className,
47
+ isActive,
48
+ disabled,
49
+ size = "icon",
50
+ onClick,
51
+ ...props
52
+ }: PaginationLinkProps) {
39
53
  return (
40
54
  <a
41
55
  aria-current={isActive ? "page" : undefined}
56
+ aria-disabled={disabled || undefined}
42
57
  data-slot="pagination-link"
43
58
  data-active={isActive}
59
+ tabIndex={disabled ? -1 : undefined}
60
+ onClick={disabled ? (e) => e.preventDefault() : onClick}
44
61
  className={cn(
45
62
  buttonVariants({
46
- variant: isActive ? "outline" : "ghost",
63
+ variant: isActive ? "default" : "ghost",
47
64
  size,
48
65
  }),
66
+ isActive && "pointer-events-none",
67
+ disabled && "pointer-events-none opacity-50",
49
68
  className
50
69
  )}
51
70
  {...props}
@@ -53,7 +72,26 @@ function PaginationLink({ className, isActive, size = "icon", ...props }: Pagina
53
72
  );
54
73
  }
55
74
 
56
- function PaginationPrevious({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
75
+ function PaginationFirst({
76
+ className,
77
+ ...props
78
+ }: React.ComponentProps<typeof PaginationLink>) {
79
+ return (
80
+ <PaginationLink
81
+ aria-label="Go to first page"
82
+ size="icon"
83
+ className={cn(className)}
84
+ {...props}
85
+ >
86
+ <ChevronsLeftIcon />
87
+ </PaginationLink>
88
+ );
89
+ }
90
+
91
+ function PaginationPrevious({
92
+ className,
93
+ ...props
94
+ }: React.ComponentProps<typeof PaginationLink>) {
57
95
  return (
58
96
  <PaginationLink
59
97
  aria-label="Go to previous page"
@@ -67,7 +105,10 @@ function PaginationPrevious({ className, ...props }: React.ComponentProps<typeof
67
105
  );
68
106
  }
69
107
 
70
- function PaginationNext({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
108
+ function PaginationNext({
109
+ className,
110
+ ...props
111
+ }: React.ComponentProps<typeof PaginationLink>) {
71
112
  return (
72
113
  <PaginationLink
73
114
  aria-label="Go to next page"
@@ -81,6 +122,22 @@ function PaginationNext({ className, ...props }: React.ComponentProps<typeof Pag
81
122
  );
82
123
  }
83
124
 
125
+ function PaginationLast({
126
+ className,
127
+ ...props
128
+ }: React.ComponentProps<typeof PaginationLink>) {
129
+ return (
130
+ <PaginationLink
131
+ aria-label="Go to last page"
132
+ size="icon"
133
+ className={cn(className)}
134
+ {...props}
135
+ >
136
+ <ChevronsRightIcon />
137
+ </PaginationLink>
138
+ );
139
+ }
140
+
84
141
  function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span">) {
85
142
  return (
86
143
  <span
@@ -100,7 +157,9 @@ export {
100
157
  PaginationContent,
101
158
  PaginationLink,
102
159
  PaginationItem,
160
+ PaginationFirst,
103
161
  PaginationPrevious,
104
162
  PaginationNext,
163
+ PaginationLast,
105
164
  PaginationEllipsis,
106
165
  };
@@ -5,8 +5,9 @@ import * as PopoverPrimitive from "@radix-ui/react-popover";
5
5
  import { cn } from "../lib/utils";
6
6
 
7
7
  const Popover = PopoverPrimitive.Root;
8
-
9
8
  const PopoverTrigger = PopoverPrimitive.Trigger;
9
+ const PopoverAnchor = PopoverPrimitive.Anchor;
10
+ const PopoverClose = PopoverPrimitive.Close;
10
11
 
11
12
  const PopoverContent = React.forwardRef<
12
13
  React.ElementRef<typeof PopoverPrimitive.Content>,
@@ -15,10 +16,12 @@ const PopoverContent = React.forwardRef<
15
16
  <PopoverPrimitive.Portal>
16
17
  <PopoverPrimitive.Content
17
18
  ref={ref}
19
+ data-slot="popover-content"
18
20
  align={align}
19
21
  sideOffset={sideOffset}
20
22
  className={cn(
21
- "z-50 w-72 rounded-xl bg-cave-depth p-4 text-popover-foreground shadow-2xl outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border border-border/50",
23
+ "z-50 w-72 rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-xl outline-none",
24
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
22
25
  className
23
26
  )}
24
27
  {...props}
@@ -27,4 +30,64 @@ const PopoverContent = React.forwardRef<
27
30
  ));
28
31
  PopoverContent.displayName = PopoverPrimitive.Content.displayName;
29
32
 
30
- export { Popover, PopoverTrigger, PopoverContent };
33
+ const PopoverArrow = React.forwardRef<
34
+ React.ElementRef<typeof PopoverPrimitive.Arrow>,
35
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Arrow>
36
+ >(({ className, ...props }, ref) => (
37
+ <PopoverPrimitive.Arrow
38
+ ref={ref}
39
+ data-slot="popover-arrow"
40
+ className={cn("fill-popover stroke-border", className)}
41
+ {...props}
42
+ />
43
+ ));
44
+ PopoverArrow.displayName = PopoverPrimitive.Arrow.displayName;
45
+
46
+ const PopoverHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
47
+ ({ className, ...props }, ref) => (
48
+ <div
49
+ ref={ref}
50
+ data-slot="popover-header"
51
+ className={cn("flex flex-col space-y-1.5 pb-3", className)}
52
+ {...props}
53
+ />
54
+ )
55
+ );
56
+ PopoverHeader.displayName = "PopoverHeader";
57
+
58
+ const PopoverTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
59
+ ({ className, ...props }, ref) => (
60
+ <h3
61
+ ref={ref}
62
+ data-slot="popover-title"
63
+ className={cn("text-sm font-semibold leading-none", className)}
64
+ {...props}
65
+ />
66
+ )
67
+ );
68
+ PopoverTitle.displayName = "PopoverTitle";
69
+
70
+ const PopoverDescription = React.forwardRef<
71
+ HTMLParagraphElement,
72
+ React.HTMLAttributes<HTMLParagraphElement>
73
+ >(({ className, ...props }, ref) => (
74
+ <p
75
+ ref={ref}
76
+ data-slot="popover-description"
77
+ className={cn("text-sm text-muted-foreground", className)}
78
+ {...props}
79
+ />
80
+ ));
81
+ PopoverDescription.displayName = "PopoverDescription";
82
+
83
+ export {
84
+ Popover,
85
+ PopoverTrigger,
86
+ PopoverContent,
87
+ PopoverAnchor,
88
+ PopoverClose,
89
+ PopoverArrow,
90
+ PopoverHeader,
91
+ PopoverTitle,
92
+ PopoverDescription,
93
+ };
@@ -4,20 +4,43 @@ import * as React from "react";
4
4
  import * as ProgressPrimitive from "@radix-ui/react-progress";
5
5
  import { cn } from "../lib/utils";
6
6
 
7
+ type ProgressTone = "primary" | "success" | "warning" | "destructive";
8
+
9
+ type ProgressProps = React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
10
+ tone?: ProgressTone;
11
+ size?: "sm" | "default" | "lg";
12
+ };
13
+
14
+ const toneClasses: Record<ProgressTone, string> = {
15
+ primary: "bg-primary",
16
+ success: "bg-success",
17
+ warning: "bg-warning",
18
+ destructive: "bg-destructive",
19
+ };
20
+
21
+ const sizeClasses = {
22
+ sm: "h-1",
23
+ default: "h-2",
24
+ lg: "h-3",
25
+ } as const;
26
+
7
27
  const Progress = React.forwardRef<
8
28
  React.ElementRef<typeof ProgressPrimitive.Root>,
9
- React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
10
- >(({ className, value, ...props }, ref) => (
29
+ ProgressProps
30
+ >(({ className, value, tone = "primary", size = "default", ...props }, ref) => (
11
31
  <ProgressPrimitive.Root
12
32
  ref={ref}
33
+ data-slot="progress"
13
34
  className={cn(
14
- "relative h-2 w-full overflow-hidden rounded-full glass border border-border/50",
35
+ "relative w-full overflow-hidden rounded-full bg-muted",
36
+ sizeClasses[size],
15
37
  className
16
38
  )}
17
39
  {...props}
18
40
  >
19
41
  <ProgressPrimitive.Indicator
20
- className="h-full w-full flex-1 bg-gradient-to-r from-primary to-secondary transition-all duration-500 ease-out shadow-lg"
42
+ data-slot="progress-indicator"
43
+ className={cn("h-full w-full flex-1 transition-transform duration-500 ease-out", toneClasses[tone])}
21
44
  style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
22
45
  />
23
46
  </ProgressPrimitive.Root>
@@ -0,0 +1,64 @@
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 { RadioGroup, RadioGroupItem } from "./radio-group";
5
+
6
+ function Setup({
7
+ value,
8
+ onValueChange,
9
+ }: {
10
+ value?: string;
11
+ onValueChange?: (v: string) => void;
12
+ }) {
13
+ return (
14
+ <RadioGroup value={value} onValueChange={onValueChange} aria-label="favorite">
15
+ <RadioGroupItem value="a" aria-label="A" />
16
+ <RadioGroupItem value="b" aria-label="B" />
17
+ <RadioGroupItem value="c" aria-label="C" />
18
+ </RadioGroup>
19
+ );
20
+ }
21
+
22
+ describe("RadioGroup", () => {
23
+ it("renders a radiogroup with items", () => {
24
+ render(<Setup />);
25
+ expect(screen.getByRole("radiogroup", { name: /favorite/i })).toBeDefined();
26
+ expect(screen.getAllByRole("radio")).toHaveLength(3);
27
+ });
28
+
29
+ it("marks the matching item as checked", () => {
30
+ render(<Setup value="b" onValueChange={() => {}} />);
31
+ expect(screen.getByRole("radio", { name: "B" })).toHaveAttribute("data-state", "checked");
32
+ expect(screen.getByRole("radio", { name: "A" })).toHaveAttribute("data-state", "unchecked");
33
+ });
34
+
35
+ it("fires onValueChange when clicking an item", async () => {
36
+ const user = userEvent.setup();
37
+ const onValueChange = vi.fn();
38
+ render(<Setup onValueChange={onValueChange} />);
39
+ await user.click(screen.getByRole("radio", { name: "B" }));
40
+ expect(onValueChange).toHaveBeenCalledWith("b");
41
+ });
42
+
43
+ it("disabled items reject clicks", async () => {
44
+ const user = userEvent.setup();
45
+ const onValueChange = vi.fn();
46
+ render(
47
+ <RadioGroup onValueChange={onValueChange} aria-label="x">
48
+ <RadioGroupItem value="a" aria-label="A" disabled />
49
+ <RadioGroupItem value="b" aria-label="B" />
50
+ </RadioGroup>
51
+ );
52
+ await user.click(screen.getByRole("radio", { name: "A" }));
53
+ expect(onValueChange).not.toHaveBeenCalled();
54
+ });
55
+
56
+ it("respects aria-invalid on items", () => {
57
+ render(
58
+ <RadioGroup aria-label="x">
59
+ <RadioGroupItem value="a" aria-label="A" aria-invalid />
60
+ </RadioGroup>
61
+ );
62
+ expect(screen.getByRole("radio", { name: "A" })).toHaveAttribute("aria-invalid", "true");
63
+ });
64
+ });
@@ -9,7 +9,14 @@ const RadioGroup = React.forwardRef<
9
9
  React.ElementRef<typeof RadioGroupPrimitive.Root>,
10
10
  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
11
11
  >(({ className, ...props }, ref) => {
12
- return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
12
+ return (
13
+ <RadioGroupPrimitive.Root
14
+ data-slot="radio-group"
15
+ className={cn("grid gap-2", className)}
16
+ {...props}
17
+ ref={ref}
18
+ />
19
+ );
13
20
  });
14
21
  RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
15
22
 
@@ -20,14 +27,15 @@ const RadioGroupItem = React.forwardRef<
20
27
  return (
21
28
  <RadioGroupPrimitive.Item
22
29
  ref={ref}
30
+ data-slot="radio-group-item"
23
31
  className={cn(
24
- "aspect-square h-5 w-5 rounded-full border border-border glass text-primary transition-all duration-300 focus: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]:border-primary data-[state=checked]:shadow-lg data-[state=checked]:shadow-primary/20 hover:border-accent/50 hover:shadow-md",
32
+ "aspect-square size-5 rounded-full border border-border bg-background text-primary 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:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary hover:border-primary/50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/40",
25
33
  className
26
34
  )}
27
35
  {...props}
28
36
  >
29
37
  <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
30
- <Circle className="h-2.5 w-2.5 fill-current text-current animate-in zoom-in-50 duration-200" />
38
+ <Circle className="size-2.5 fill-current text-primary animate-in zoom-in-50 duration-150" />
31
39
  </RadioGroupPrimitive.Indicator>
32
40
  </RadioGroupPrimitive.Item>
33
41
  );
@@ -0,0 +1,112 @@
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 { Rating } from "./rating";
5
+
6
+ describe("Rating (readonly)", () => {
7
+ it("renders with role=img and labeled aria description", () => {
8
+ render(<Rating value={3.5} reviewCount={42} />);
9
+ const el = screen.getByRole("img");
10
+ expect(el.getAttribute("aria-label")).toBe("Rating: 3.5 out of 5");
11
+ });
12
+
13
+ it("uses custom label in aria-label", () => {
14
+ render(<Rating value={2} label="Tasty" />);
15
+ expect(screen.getByRole("img").getAttribute("aria-label")).toBe("Tasty: 2 out of 5");
16
+ });
17
+
18
+ it("shows review count when provided and showCount is true", () => {
19
+ render(<Rating value={3} reviewCount={123} />);
20
+ expect(screen.getByText("(123)")).toBeInTheDocument();
21
+ });
22
+
23
+ it("hides count when showCount=false", () => {
24
+ render(<Rating value={3} reviewCount={123} showCount={false} />);
25
+ expect(screen.queryByText("(123)")).toBeNull();
26
+ });
27
+
28
+ it("does not render any radio buttons in readonly mode", () => {
29
+ render(<Rating value={3} />);
30
+ expect(screen.queryByRole("radio")).toBeNull();
31
+ });
32
+ });
33
+
34
+ describe("Rating (interactive)", () => {
35
+ it("renders as radiogroup with `max` radios", () => {
36
+ render(<Rating value={0} readonly={false} onRatingChange={() => {}} />);
37
+ expect(screen.getByRole("radiogroup")).toBeInTheDocument();
38
+ expect(screen.getAllByRole("radio")).toHaveLength(5);
39
+ });
40
+
41
+ it("marks the correct radio as checked", () => {
42
+ render(<Rating value={3} readonly={false} onRatingChange={() => {}} />);
43
+ const radios = screen.getAllByRole("radio");
44
+ expect(radios[2].getAttribute("aria-checked")).toBe("true");
45
+ expect(radios[0].getAttribute("aria-checked")).toBe("false");
46
+ expect(radios[4].getAttribute("aria-checked")).toBe("false");
47
+ });
48
+
49
+ it("fires onRatingChange when a star is clicked", async () => {
50
+ const user = userEvent.setup();
51
+ const onChange = vi.fn();
52
+ render(<Rating value={0} readonly={false} onRatingChange={onChange} />);
53
+ await user.click(screen.getByRole("radio", { name: "4 stars" }));
54
+ expect(onChange).toHaveBeenCalledWith(4);
55
+ });
56
+
57
+ it("ArrowRight increments and ArrowLeft decrements", async () => {
58
+ const user = userEvent.setup();
59
+ const onChange = vi.fn();
60
+ render(<Rating value={3} readonly={false} onRatingChange={onChange} />);
61
+ screen.getAllByRole("radio")[0].focus();
62
+ await user.keyboard("{ArrowRight}");
63
+ expect(onChange).toHaveBeenLastCalledWith(4);
64
+ await user.keyboard("{ArrowLeft}");
65
+ expect(onChange).toHaveBeenLastCalledWith(2);
66
+ });
67
+
68
+ it("ArrowUp increments and ArrowDown decrements", async () => {
69
+ const user = userEvent.setup();
70
+ const onChange = vi.fn();
71
+ render(<Rating value={2} readonly={false} onRatingChange={onChange} />);
72
+ screen.getAllByRole("radio")[0].focus();
73
+ await user.keyboard("{ArrowUp}");
74
+ expect(onChange).toHaveBeenLastCalledWith(3);
75
+ await user.keyboard("{ArrowDown}");
76
+ expect(onChange).toHaveBeenLastCalledWith(1);
77
+ });
78
+
79
+ it("Home jumps to 0 and End jumps to max", async () => {
80
+ const user = userEvent.setup();
81
+ const onChange = vi.fn();
82
+ render(<Rating value={3} readonly={false} onRatingChange={onChange} />);
83
+ screen.getAllByRole("radio")[0].focus();
84
+ await user.keyboard("{Home}");
85
+ expect(onChange).toHaveBeenLastCalledWith(0);
86
+ await user.keyboard("{End}");
87
+ expect(onChange).toHaveBeenLastCalledWith(5);
88
+ });
89
+
90
+ it("clamps at max when ArrowRight pressed at max", async () => {
91
+ const user = userEvent.setup();
92
+ const onChange = vi.fn();
93
+ render(<Rating value={5} readonly={false} onRatingChange={onChange} />);
94
+ screen.getAllByRole("radio")[0].focus();
95
+ await user.keyboard("{ArrowRight}");
96
+ expect(onChange).toHaveBeenLastCalledWith(5);
97
+ });
98
+
99
+ it("clamps at 0 when ArrowLeft pressed at 0", async () => {
100
+ const user = userEvent.setup();
101
+ const onChange = vi.fn();
102
+ render(<Rating value={0} readonly={false} onRatingChange={onChange} />);
103
+ screen.getAllByRole("radio")[0].focus();
104
+ await user.keyboard("{ArrowLeft}");
105
+ expect(onChange).toHaveBeenLastCalledWith(0);
106
+ });
107
+
108
+ it("respects max prop for radio count", () => {
109
+ render(<Rating value={2} max={10} readonly={false} onRatingChange={() => {}} />);
110
+ expect(screen.getAllByRole("radio")).toHaveLength(10);
111
+ });
112
+ });