@bearmenu/ui 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) 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 +5 -2
  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 +4 -2
  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 +29 -16
  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/search-bar.d.ts +1 -2
  86. package/dist/components/search-bar.js +37 -60
  87. package/dist/components/searchable-select.js +76 -72
  88. package/dist/components/select.d.ts +5 -1
  89. package/dist/components/select.js +30 -14
  90. package/dist/components/selection-bar.d.ts +11 -0
  91. package/dist/components/selection-bar.js +65 -0
  92. package/dist/components/separator.js +3 -2
  93. package/dist/components/sheet.d.ts +1 -1
  94. package/dist/components/sheet.js +1 -1
  95. package/dist/components/skeleton-card.js +1 -1
  96. package/dist/components/skeleton.js +1 -1
  97. package/dist/components/slider.js +21 -4
  98. package/dist/components/sliding-panels.js +15 -13
  99. package/dist/components/spinner.d.ts +17 -0
  100. package/dist/components/spinner.js +50 -0
  101. package/dist/components/stepper.d.ts +18 -0
  102. package/dist/components/stepper.js +199 -0
  103. package/dist/components/sticky-container.js +1 -0
  104. package/dist/components/switch.js +4 -3
  105. package/dist/components/table.d.ts +23 -5
  106. package/dist/components/table.js +121 -35
  107. package/dist/components/tabs.d.ts +3 -1
  108. package/dist/components/tabs.js +37 -9
  109. package/dist/components/tag-group.d.ts +9 -0
  110. package/dist/components/tag-group.js +26 -0
  111. package/dist/components/tag.d.ts +17 -0
  112. package/dist/components/tag.js +61 -0
  113. package/dist/components/text.d.ts +3 -3
  114. package/dist/components/text.js +1 -1
  115. package/dist/components/textarea.js +2 -1
  116. package/dist/components/toast.js +1 -1
  117. package/dist/components/toaster.js +1 -1
  118. package/dist/components/toggle-group.js +1 -1
  119. package/dist/components/toggle.js +1 -1
  120. package/dist/components/tooltip.d.ts +3 -2
  121. package/dist/components/tooltip.js +16 -2
  122. package/package.json +44 -26
  123. package/src/components/accordion.tsx +11 -6
  124. package/src/components/alert-dialog.tsx +4 -2
  125. package/src/components/alert.tsx +20 -8
  126. package/src/components/avatar.tsx +106 -13
  127. package/src/components/badge.tsx +12 -11
  128. package/src/components/breadcrumb.tsx +5 -2
  129. package/src/components/button-group.stories.tsx +52 -0
  130. package/src/components/button-group.tsx +40 -0
  131. package/src/components/button.test.tsx +174 -0
  132. package/src/components/button.tsx +42 -19
  133. package/src/components/calendar.tsx +2 -2
  134. package/src/components/card.tsx +55 -13
  135. package/src/components/carousel.tsx +85 -13
  136. package/src/components/checkbox.test.tsx +58 -0
  137. package/src/components/checkbox.tsx +8 -3
  138. package/src/components/circular-progress.stories.tsx +39 -0
  139. package/src/components/circular-progress.tsx +102 -0
  140. package/src/components/collapsible.stories.tsx +7 -3
  141. package/src/components/combobox.stories.tsx +82 -0
  142. package/src/components/combobox.test.tsx +118 -0
  143. package/src/components/combobox.tsx +136 -0
  144. package/src/components/command.test.tsx +91 -0
  145. package/src/components/command.tsx +38 -15
  146. package/src/components/currency-input.test.tsx +63 -0
  147. package/src/components/currency-input.tsx +84 -66
  148. package/src/components/date-input.stories.tsx +31 -0
  149. package/src/components/date-input.tsx +43 -0
  150. package/src/components/description-list.stories.tsx +62 -0
  151. package/src/components/description-list.tsx +68 -0
  152. package/src/components/dialog.test.tsx +84 -0
  153. package/src/components/dialog.tsx +64 -24
  154. package/src/components/drawer.tsx +72 -23
  155. package/src/components/dropdown-menu.test.tsx +180 -0
  156. package/src/components/dropdown-menu.tsx +31 -6
  157. package/src/components/field.stories.tsx +36 -0
  158. package/src/components/field.test.tsx +157 -0
  159. package/src/components/field.tsx +146 -0
  160. package/src/components/form.stories.tsx +3 -2
  161. package/src/components/form.test.tsx +95 -0
  162. package/src/components/form.tsx +9 -0
  163. package/src/components/haptic-button.tsx +1 -1
  164. package/src/components/input-group.stories.tsx +61 -0
  165. package/src/components/input-group.tsx +99 -0
  166. package/src/components/input.test.tsx +103 -0
  167. package/src/components/input.tsx +24 -7
  168. package/src/components/item.stories.tsx +52 -0
  169. package/src/components/item.tsx +115 -0
  170. package/src/components/kbd.stories.tsx +33 -0
  171. package/src/components/kbd.tsx +46 -0
  172. package/src/components/lazy-markdown.tsx +10 -7
  173. package/src/components/multi-select-combobox.test.tsx +101 -0
  174. package/src/components/multi-select-combobox.tsx +85 -82
  175. package/src/components/multi-select-menu.tsx +49 -41
  176. package/src/components/native-select.stories.tsx +52 -0
  177. package/src/components/native-select.tsx +45 -0
  178. package/src/components/pagination.tsx +64 -5
  179. package/src/components/popover.tsx +66 -3
  180. package/src/components/progress.tsx +27 -4
  181. package/src/components/radio-group.test.tsx +64 -0
  182. package/src/components/radio-group.tsx +11 -3
  183. package/src/components/rating.test.tsx +112 -0
  184. package/src/components/rating.tsx +97 -82
  185. package/src/components/search-bar.stories.tsx +1 -32
  186. package/src/components/search-bar.tsx +22 -59
  187. package/src/components/searchable-select.tsx +69 -73
  188. package/src/components/select.tsx +34 -19
  189. package/src/components/selection-bar.stories.tsx +37 -0
  190. package/src/components/selection-bar.tsx +65 -0
  191. package/src/components/separator.tsx +5 -2
  192. package/src/components/sheet.tsx +1 -1
  193. package/src/components/skeleton.tsx +2 -2
  194. package/src/components/slider.test.tsx +45 -0
  195. package/src/components/slider.tsx +31 -12
  196. package/src/components/sliding-panels.tsx +14 -12
  197. package/src/components/spinner.stories.tsx +35 -0
  198. package/src/components/spinner.tsx +50 -0
  199. package/src/components/stepper.stories.tsx +71 -0
  200. package/src/components/stepper.tsx +220 -0
  201. package/src/components/sticky-container.tsx +1 -0
  202. package/src/components/switch.test.tsx +52 -0
  203. package/src/components/switch.tsx +4 -3
  204. package/src/components/table.tsx +140 -35
  205. package/src/components/tabs.test.tsx +107 -0
  206. package/src/components/tabs.tsx +70 -23
  207. package/src/components/tag-group.stories.tsx +39 -0
  208. package/src/components/tag-group.tsx +28 -0
  209. package/src/components/tag.stories.tsx +50 -0
  210. package/src/components/tag.tsx +63 -0
  211. package/src/components/text.test.tsx +1 -1
  212. package/src/components/text.tsx +19 -3
  213. package/src/components/textarea.test.tsx +48 -0
  214. package/src/components/textarea.tsx +2 -1
  215. package/src/components/toast.tsx +4 -3
  216. package/src/components/toggle.tsx +2 -2
  217. package/src/components/tooltip.tsx +12 -2
  218. package/src/globals.css +142 -90
  219. package/dist/chunk-2BMJRXQX.js +0 -80
  220. package/dist/chunk-4AVS6CV4.js +0 -89
  221. package/dist/chunk-4UO73SKC.js +0 -26
  222. package/dist/chunk-CW24PLDW.js +0 -24
  223. package/dist/chunk-IQASCH2V.js +0 -25
  224. package/dist/chunk-NKHH23YK.js +0 -31
  225. package/dist/chunk-QJ54Z7TC.js +0 -83
@@ -46,11 +46,9 @@ export function SearchableSelect({
46
46
  onSearchChange,
47
47
  }: SearchableSelectProps) {
48
48
  const [open, setOpen] = React.useState(false);
49
+ const listboxId = React.useId();
49
50
 
50
51
  const selectedOption = options.find((opt) => opt.value === value);
51
-
52
- // Fallback: if we have a value but no matching option, display the value itself
53
- // This handles the case where the selected concept hasn't been fetched yet
54
52
  const displayLabel = selectedOption?.label || (value ? value : undefined);
55
53
 
56
54
  const handleSelect = (selectedValue: string) => {
@@ -58,77 +56,75 @@ export function SearchableSelect({
58
56
  setOpen(false);
59
57
  };
60
58
 
61
- const handleClear = (e: React.MouseEvent | React.KeyboardEvent) => {
62
- e.stopPropagation();
63
- onValueChange(undefined);
64
- };
65
-
66
59
  return (
67
- <Popover open={open} onOpenChange={setOpen}>
68
- <PopoverTrigger asChild>
69
- <Button
70
- variant="outline"
71
- role="combobox"
72
- aria-expanded={open}
73
- className={cn(
74
- "w-full justify-between h-10",
75
- !value && "text-muted-foreground",
76
- className
77
- )}
78
- disabled={disabled}
79
- >
80
- <span className="truncate">{displayLabel || placeholder}</span>
81
- <div className="flex items-center gap-1">
82
- {value && (
83
- <div
84
- role="button"
85
- tabIndex={0}
86
- className="rounded-full p-0.5 hover:bg-accent cursor-pointer"
87
- onClick={handleClear}
88
- onKeyDown={(e) => {
89
- if (e.key === "Enter" || e.key === " ") {
90
- e.preventDefault();
91
- handleClear(e);
92
- }
93
- }}
94
- >
95
- <X className="h-3 w-3" />
96
- </div>
60
+ <div className={cn("relative", className)}>
61
+ <Popover open={open} onOpenChange={setOpen}>
62
+ <PopoverTrigger asChild>
63
+ <Button
64
+ variant="outline"
65
+ role="combobox"
66
+ aria-expanded={open}
67
+ aria-haspopup="listbox"
68
+ aria-controls={open ? listboxId : undefined}
69
+ className={cn(
70
+ "w-full justify-between font-normal",
71
+ value && "pr-16",
72
+ !value && "text-muted-foreground"
97
73
  )}
98
- <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
99
- </div>
100
- </Button>
101
- </PopoverTrigger>
102
- <PopoverContent className="w-full p-0" align="start">
103
- <Command shouldFilter={!onSearchChange}>
104
- <CommandInput placeholder={searchPlaceholder} onValueChange={onSearchChange} />
105
- <CommandList>
106
- <CommandEmpty>{loading ? "Loading..." : emptyText}</CommandEmpty>
107
- <CommandGroup>
108
- {options.map((option) => (
109
- <CommandItem
110
- key={option.value}
111
- value={option.value}
112
- onSelect={() => handleSelect(option.value)}
113
- >
114
- <Check
115
- className={cn(
116
- "mr-2 h-4 w-4",
117
- value === option.value ? "opacity-100" : "opacity-0"
118
- )}
119
- />
120
- <div className="flex flex-col">
121
- <span>{option.label}</span>
122
- {option.description && (
123
- <span className="text-xs text-muted-foreground">{option.description}</span>
124
- )}
125
- </div>
126
- </CommandItem>
127
- ))}
128
- </CommandGroup>
129
- </CommandList>
130
- </Command>
131
- </PopoverContent>
132
- </Popover>
74
+ disabled={disabled}
75
+ >
76
+ <span className="truncate">{displayLabel || placeholder}</span>
77
+ <ChevronsUpDown className="size-4 shrink-0 opacity-50" />
78
+ </Button>
79
+ </PopoverTrigger>
80
+ <PopoverContent
81
+ id={listboxId}
82
+ className="w-[var(--radix-popover-trigger-width)] p-0"
83
+ align="start"
84
+ >
85
+ <Command shouldFilter={!onSearchChange}>
86
+ <CommandInput placeholder={searchPlaceholder} onValueChange={onSearchChange} />
87
+ <CommandList>
88
+ <CommandEmpty>{loading ? "Loading…" : emptyText}</CommandEmpty>
89
+ <CommandGroup>
90
+ {options.map((option) => (
91
+ <CommandItem
92
+ key={option.value}
93
+ value={option.value}
94
+ onSelect={() => handleSelect(option.value)}
95
+ >
96
+ <Check
97
+ className={cn(
98
+ "mr-2 size-4",
99
+ value === option.value ? "opacity-100" : "opacity-0"
100
+ )}
101
+ />
102
+ <div className="flex flex-col">
103
+ <span>{option.label}</span>
104
+ {option.description && (
105
+ <span className="text-xs text-muted-foreground">{option.description}</span>
106
+ )}
107
+ </div>
108
+ </CommandItem>
109
+ ))}
110
+ </CommandGroup>
111
+ </CommandList>
112
+ </Command>
113
+ </PopoverContent>
114
+ </Popover>
115
+ {value && !disabled && (
116
+ <button
117
+ type="button"
118
+ aria-label="Clear selection"
119
+ onClick={(e) => {
120
+ e.stopPropagation();
121
+ onValueChange(undefined);
122
+ }}
123
+ className="absolute right-9 top-1/2 -translate-y-1/2 inline-flex size-6 items-center justify-center rounded-full text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 transition-colors"
124
+ >
125
+ <X className="size-3.5" />
126
+ </button>
127
+ )}
128
+ </div>
133
129
  );
134
130
  }
@@ -2,30 +2,44 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import * as SelectPrimitive from "@radix-ui/react-select";
5
+ import { cva, type VariantProps } from "class-variance-authority";
5
6
  import { Check, ChevronDown, ChevronUp } from "lucide-react";
6
7
  import { cn } from "../lib/utils";
7
8
 
8
9
  const Select = SelectPrimitive.Root;
9
-
10
10
  const SelectGroup = SelectPrimitive.Group;
11
-
12
11
  const SelectValue = SelectPrimitive.Value;
13
12
 
13
+ const selectTriggerVariants = cva(
14
+ "flex w-full items-center justify-between rounded-xl bg-background border border-border text-sm font-medium text-foreground transition-colors focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1 hover:border-foreground/30 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/40",
15
+ {
16
+ variants: {
17
+ size: {
18
+ sm: "h-9 px-3",
19
+ default: "h-11 px-4 py-2",
20
+ lg: "h-12 px-5",
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ size: "default",
25
+ },
26
+ }
27
+ );
28
+
14
29
  const SelectTrigger = React.forwardRef<
15
30
  React.ElementRef<typeof SelectPrimitive.Trigger>,
16
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
17
- >(({ className, children, ...props }, ref) => (
31
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> &
32
+ VariantProps<typeof selectTriggerVariants>
33
+ >(({ className, size, children, ...props }, ref) => (
18
34
  <SelectPrimitive.Trigger
19
35
  ref={ref}
20
- className={cn(
21
- "flex h-11 w-full items-center justify-between rounded-xl bg-surface border border-border px-4 py-2 text-sm font-medium text-muted-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 hover:border-foreground/20 hover:text-foreground",
22
- className
23
- )}
36
+ data-slot="select-trigger"
37
+ className={cn(selectTriggerVariants({ size }), className)}
24
38
  {...props}
25
39
  >
26
40
  {children}
27
41
  <SelectPrimitive.Icon asChild>
28
- <ChevronDown className="h-4 w-4" />
42
+ <ChevronDown className="size-4 opacity-60" />
29
43
  </SelectPrimitive.Icon>
30
44
  </SelectPrimitive.Trigger>
31
45
  ));
@@ -40,7 +54,7 @@ const SelectScrollUpButton = React.forwardRef<
40
54
  className={cn("flex cursor-default items-center justify-center py-1", className)}
41
55
  {...props}
42
56
  >
43
- <ChevronUp className="h-4 w-4" />
57
+ <ChevronUp className="size-4" />
44
58
  </SelectPrimitive.ScrollUpButton>
45
59
  ));
46
60
  SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
@@ -54,7 +68,7 @@ const SelectScrollDownButton = React.forwardRef<
54
68
  className={cn("flex cursor-default items-center justify-center py-1", className)}
55
69
  {...props}
56
70
  >
57
- <ChevronDown className="h-4 w-4" />
71
+ <ChevronDown className="size-4" />
58
72
  </SelectPrimitive.ScrollDownButton>
59
73
  ));
60
74
  SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
@@ -66,8 +80,9 @@ const SelectContent = React.forwardRef<
66
80
  <SelectPrimitive.Portal>
67
81
  <SelectPrimitive.Content
68
82
  ref={ref}
83
+ data-slot="select-content"
69
84
  className={cn(
70
- "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-xl border border-border bg-card text-popover-foreground shadow-lg 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",
85
+ "relative z-50 max-h-96 min-w-32 overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-lg 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",
71
86
  position === "popper" &&
72
87
  "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
73
88
  className
@@ -78,7 +93,7 @@ const SelectContent = React.forwardRef<
78
93
  <SelectScrollUpButton />
79
94
  <SelectPrimitive.Viewport
80
95
  className={cn(
81
- "p-1 flex flex-col gap-1",
96
+ "p-1 flex flex-col gap-0.5",
82
97
  position === "popper" &&
83
98
  "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
84
99
  )}
@@ -97,7 +112,7 @@ const SelectLabel = React.forwardRef<
97
112
  >(({ className, ...props }, ref) => (
98
113
  <SelectPrimitive.Label
99
114
  ref={ref}
100
- className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
115
+ className={cn("py-1.5 pl-8 pr-2 text-xs font-semibold text-muted-foreground", className)}
101
116
  {...props}
102
117
  />
103
118
  ));
@@ -109,18 +124,18 @@ const SelectItem = React.forwardRef<
109
124
  >(({ className, children, ...props }, ref) => (
110
125
  <SelectPrimitive.Item
111
126
  ref={ref}
127
+ data-slot="select-item"
112
128
  className={cn(
113
- "relative flex w-full cursor-default select-none items-center rounded-lg py-2 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[state=checked]:bg-primary/10 data-[state=checked]:text-primary data-[state=checked]:font-medium",
129
+ "relative flex w-full cursor-default select-none items-center rounded-md py-2 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[state=checked]:font-medium",
114
130
  className
115
131
  )}
116
132
  {...props}
117
133
  >
118
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
134
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
119
135
  <SelectPrimitive.ItemIndicator>
120
- <Check className="h-4 w-4" />
136
+ <Check className="size-4" />
121
137
  </SelectPrimitive.ItemIndicator>
122
138
  </span>
123
-
124
139
  <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
125
140
  </SelectPrimitive.Item>
126
141
  ));
@@ -132,7 +147,7 @@ const SelectSeparator = React.forwardRef<
132
147
  >(({ className, ...props }, ref) => (
133
148
  <SelectPrimitive.Separator
134
149
  ref={ref}
135
- className={cn("-mx-1 my-1 h-px bg-muted", className)}
150
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
136
151
  {...props}
137
152
  />
138
153
  ));
@@ -0,0 +1,37 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { SelectionBar } from "./selection-bar";
3
+ import { Button } from "./button";
4
+
5
+ const meta = {
6
+ title: "Components/SelectionBar",
7
+ component: SelectionBar,
8
+ tags: ["autodocs"],
9
+ } satisfies Meta<typeof SelectionBar>;
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof meta>;
13
+
14
+ export const Default: Story = {
15
+ render: () => (
16
+ <div className="w-[36rem]">
17
+ <SelectionBar count={3} onClear={() => {}} itemNoun="row">
18
+ <Button size="sm" variant="outline">
19
+ Archive
20
+ </Button>
21
+ <Button size="sm" variant="destructive">
22
+ Delete
23
+ </Button>
24
+ </SelectionBar>
25
+ </div>
26
+ ),
27
+ };
28
+
29
+ export const SingleSelection: Story = {
30
+ render: () => (
31
+ <div className="w-[36rem]">
32
+ <SelectionBar count={1} onClear={() => {}} itemNoun="file">
33
+ <Button size="sm">Download</Button>
34
+ </SelectionBar>
35
+ </div>
36
+ ),
37
+ };
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { X } from "lucide-react";
5
+ import { cn } from "../lib/utils";
6
+
7
+ type SelectionBarProps = React.HTMLAttributes<HTMLDivElement> & {
8
+ count: number;
9
+ /** Hide bar when count is 0. Defaults to true. */
10
+ hideWhenEmpty?: boolean;
11
+ onClear?: () => void;
12
+ itemNoun?: string;
13
+ };
14
+
15
+ const SelectionBar = React.forwardRef<HTMLDivElement, SelectionBarProps>(
16
+ (
17
+ {
18
+ className,
19
+ count,
20
+ hideWhenEmpty = true,
21
+ onClear,
22
+ itemNoun = "item",
23
+ children,
24
+ ...props
25
+ },
26
+ ref
27
+ ) => {
28
+ if (hideWhenEmpty && count <= 0) return null;
29
+ const noun = count === 1 ? itemNoun : `${itemNoun}s`;
30
+
31
+ return (
32
+ <div
33
+ ref={ref}
34
+ role="region"
35
+ aria-label={`${count} ${noun} selected`}
36
+ data-slot="selection-bar"
37
+ className={cn(
38
+ "flex w-full items-center gap-3 rounded-2xl border border-border bg-card px-4 py-3 shadow-lg",
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ <div className="flex items-center gap-2 text-sm font-medium text-foreground">
44
+ {onClear && (
45
+ <button
46
+ type="button"
47
+ aria-label="Clear selection"
48
+ onClick={onClear}
49
+ className="inline-flex size-6 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
50
+ >
51
+ <X className="size-4" />
52
+ </button>
53
+ )}
54
+ <span aria-live="polite">
55
+ {count} {noun} selected
56
+ </span>
57
+ </div>
58
+ {children && <div className="ml-auto flex items-center gap-2">{children}</div>}
59
+ </div>
60
+ );
61
+ }
62
+ );
63
+ SelectionBar.displayName = "SelectionBar";
64
+
65
+ export { SelectionBar };
@@ -10,11 +10,14 @@ const Separator = React.forwardRef<
10
10
  >(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
11
11
  <SeparatorPrimitive.Root
12
12
  ref={ref}
13
+ data-slot="separator"
13
14
  decorative={decorative}
14
15
  orientation={orientation}
15
16
  className={cn(
16
- "shrink-0 bg-gradient-to-r from-transparent via-border to-transparent backdrop-blur-sm",
17
- orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
17
+ "shrink-0",
18
+ orientation === "horizontal"
19
+ ? "h-px w-full bg-gradient-to-r from-transparent via-border to-transparent"
20
+ : "h-full w-px bg-gradient-to-b from-transparent via-border to-transparent",
18
21
  className
19
22
  )}
20
23
  {...props}
@@ -30,7 +30,7 @@ const SheetOverlay = React.forwardRef<
30
30
  SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
31
31
 
32
32
  const sheetVariants = cva(
33
- "fixed z-[1000] gap-4 glass bg-background p-6 shadow-2xl transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
33
+ "fixed z-[1000] gap-4 bg-background border-border p-6 shadow-2xl transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
34
34
  {
35
35
  variants: {
36
36
  side: {
@@ -6,8 +6,8 @@ function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>)
6
6
  <div
7
7
  data-slot="skeleton"
8
8
  className={cn(
9
- "animate-pulse rounded-xl glass border border-border/30 relative overflow-hidden",
10
- "before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent",
9
+ "relative overflow-hidden rounded-xl bg-muted",
10
+ "before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-foreground/[0.04] before:to-transparent",
11
11
  className
12
12
  )}
13
13
  {...props}
@@ -0,0 +1,45 @@
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 { Slider } from "./slider";
5
+
6
+ describe("Slider", () => {
7
+ it("renders a single thumb for a scalar default value", () => {
8
+ render(<Slider defaultValue={[50]} aria-label="volume" />);
9
+ expect(screen.getAllByRole("slider")).toHaveLength(1);
10
+ });
11
+
12
+ it("renders N thumbs for an N-length value array (range slider)", () => {
13
+ render(<Slider defaultValue={[20, 80]} aria-label="range" />);
14
+ expect(screen.getAllByRole("slider")).toHaveLength(2);
15
+ });
16
+
17
+ it("renders thumbs equal to length of controlled value", () => {
18
+ render(<Slider value={[10, 40, 70]} onValueChange={() => {}} aria-label="three" />);
19
+ expect(screen.getAllByRole("slider")).toHaveLength(3);
20
+ });
21
+
22
+ it("fires onValueChange via ArrowRight", async () => {
23
+ const user = userEvent.setup();
24
+ const onValueChange = vi.fn();
25
+ render(<Slider defaultValue={[50]} onValueChange={onValueChange} aria-label="x" />);
26
+ screen.getByRole("slider").focus();
27
+ await user.keyboard("{ArrowRight}");
28
+ expect(onValueChange).toHaveBeenCalled();
29
+ });
30
+
31
+ it("sets data-disabled when disabled", () => {
32
+ const { container } = render(
33
+ <Slider defaultValue={[50]} disabled aria-label="x" />
34
+ );
35
+ expect(container.querySelector("[data-slot=slider]")).toHaveAttribute("data-disabled");
36
+ });
37
+
38
+ it("respects min/max bounds in aria attributes", () => {
39
+ render(<Slider defaultValue={[5]} min={0} max={10} aria-label="x" />);
40
+ const thumb = screen.getByRole("slider");
41
+ expect(thumb).toHaveAttribute("aria-valuemin", "0");
42
+ expect(thumb).toHaveAttribute("aria-valuemax", "10");
43
+ expect(thumb).toHaveAttribute("aria-valuenow", "5");
44
+ });
45
+ });
@@ -7,18 +7,37 @@ import { cn } from "../lib/utils";
7
7
  const Slider = React.forwardRef<
8
8
  React.ElementRef<typeof SliderPrimitive.Root>,
9
9
  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
10
- >(({ className, ...props }, ref) => (
11
- <SliderPrimitive.Root
12
- ref={ref}
13
- className={cn("relative flex w-full touch-none select-none items-center", className)}
14
- {...props}
15
- >
16
- <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full glass border border-border/50">
17
- <SliderPrimitive.Range className="absolute h-full bg-white shadow-lg" />
18
- </SliderPrimitive.Track>
19
- <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-primary ring-offset-background 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 hover:shadow-lg hover:shadow-primary/20 hover:scale-110" />
20
- </SliderPrimitive.Root>
21
- ));
10
+ >(({ className, value, defaultValue, ...props }, ref) => {
11
+ const thumbCount = React.useMemo(() => {
12
+ if (Array.isArray(value)) return value.length;
13
+ if (Array.isArray(defaultValue)) return defaultValue.length;
14
+ return 1;
15
+ }, [value, defaultValue]);
16
+
17
+ return (
18
+ <SliderPrimitive.Root
19
+ ref={ref}
20
+ data-slot="slider"
21
+ value={value}
22
+ defaultValue={defaultValue}
23
+ className={cn(
24
+ "relative flex w-full touch-none select-none items-center data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
25
+ className
26
+ )}
27
+ {...props}
28
+ >
29
+ <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-muted">
30
+ <SliderPrimitive.Range className="absolute h-full bg-primary" />
31
+ </SliderPrimitive.Track>
32
+ {Array.from({ length: thumbCount }, (_, i) => (
33
+ <SliderPrimitive.Thumb
34
+ key={i}
35
+ className="block size-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:scale-110"
36
+ />
37
+ ))}
38
+ </SliderPrimitive.Root>
39
+ );
40
+ });
22
41
  Slider.displayName = SliderPrimitive.Root.displayName;
23
42
 
24
43
  export { Slider };
@@ -46,20 +46,22 @@ export const SlidingPanels = ({
46
46
  }, [currentIndex]);
47
47
 
48
48
  const handleBack = () => {
49
- if (currentIndex > 0) {
50
- const previousPanel = panels[currentIndex - 1];
51
- if (currentPanelData?.onBack) {
52
- currentPanelData.onBack();
53
- } else {
54
- onPanelChange(previousPanel.id);
55
- }
49
+ const previousPanel = panels[currentIndex - 1];
50
+ if (!previousPanel) return;
51
+ if (currentPanelData?.onBack) {
52
+ currentPanelData.onBack();
53
+ } else {
54
+ onPanelChange(previousPanel.id);
56
55
  }
57
56
  };
58
57
 
59
58
  return (
60
59
  <div
61
- className={cn("relative overflow-hidden transition-[height] duration-300", className)}
62
- style={{ height: containerHeight }}
60
+ className={cn(
61
+ "relative overflow-clip transition-[height] duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]",
62
+ className
63
+ )}
64
+ style={{ height: containerHeight, overflowClipMargin: "8px" }}
63
65
  >
64
66
  {panels.map((panel, index) => {
65
67
  const isActive = panel.id === currentPanel;
@@ -73,10 +75,10 @@ export const SlidingPanels = ({
73
75
  panelRefs.current[index] = el;
74
76
  }}
75
77
  className={cn(
76
- "absolute top-0 left-0 w-full transition-transform duration-300",
78
+ "absolute top-0 left-0 w-full transition-transform duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]",
77
79
  isActive && "translate-x-0",
78
- isLeft && "-translate-x-full",
79
- isRight && "translate-x-full"
80
+ isLeft && "-translate-x-[calc(100%+8px)]",
81
+ isRight && "translate-x-[calc(100%+8px)]"
80
82
  )}
81
83
  aria-hidden={!isActive}
82
84
  >
@@ -0,0 +1,35 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Spinner } from "./spinner";
3
+
4
+ const meta = {
5
+ title: "Components/Spinner",
6
+ component: Spinner,
7
+ tags: ["autodocs"],
8
+ } satisfies Meta<typeof Spinner>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Default: Story = {};
14
+
15
+ export const Sizes: Story = {
16
+ render: () => (
17
+ <div className="flex items-center gap-4">
18
+ <Spinner size="xs" />
19
+ <Spinner size="sm" />
20
+ <Spinner size="default" />
21
+ <Spinner size="lg" />
22
+ <Spinner size="xl" />
23
+ </div>
24
+ ),
25
+ };
26
+
27
+ export const Tones: Story = {
28
+ render: () => (
29
+ <div className="flex items-center gap-4">
30
+ <Spinner tone="default" />
31
+ <Spinner tone="primary" />
32
+ <Spinner tone="destructive" />
33
+ </div>
34
+ ),
35
+ };