@bearmenu/ui 0.5.2 → 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 (223) 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 +9 -6
  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/switch.js +4 -3
  104. package/dist/components/table.d.ts +23 -5
  105. package/dist/components/table.js +121 -35
  106. package/dist/components/tabs.d.ts +3 -1
  107. package/dist/components/tabs.js +37 -9
  108. package/dist/components/tag-group.d.ts +9 -0
  109. package/dist/components/tag-group.js +26 -0
  110. package/dist/components/tag.d.ts +17 -0
  111. package/dist/components/tag.js +61 -0
  112. package/dist/components/text.d.ts +3 -3
  113. package/dist/components/text.js +1 -1
  114. package/dist/components/textarea.js +2 -1
  115. package/dist/components/toast.js +1 -1
  116. package/dist/components/toaster.js +1 -1
  117. package/dist/components/toggle-group.js +1 -1
  118. package/dist/components/toggle.js +1 -1
  119. package/dist/components/tooltip.d.ts +3 -2
  120. package/dist/components/tooltip.js +16 -2
  121. package/package.json +44 -26
  122. package/src/components/accordion.tsx +11 -6
  123. package/src/components/alert-dialog.tsx +4 -2
  124. package/src/components/alert.tsx +20 -8
  125. package/src/components/avatar.tsx +106 -13
  126. package/src/components/badge.tsx +12 -11
  127. package/src/components/breadcrumb.tsx +5 -2
  128. package/src/components/button-group.stories.tsx +52 -0
  129. package/src/components/button-group.tsx +40 -0
  130. package/src/components/button.test.tsx +174 -0
  131. package/src/components/button.tsx +42 -19
  132. package/src/components/calendar.tsx +2 -2
  133. package/src/components/card.tsx +55 -13
  134. package/src/components/carousel.tsx +85 -13
  135. package/src/components/checkbox.test.tsx +58 -0
  136. package/src/components/checkbox.tsx +8 -3
  137. package/src/components/circular-progress.stories.tsx +39 -0
  138. package/src/components/circular-progress.tsx +102 -0
  139. package/src/components/collapsible.stories.tsx +7 -3
  140. package/src/components/combobox.stories.tsx +82 -0
  141. package/src/components/combobox.test.tsx +118 -0
  142. package/src/components/combobox.tsx +136 -0
  143. package/src/components/command.test.tsx +91 -0
  144. package/src/components/command.tsx +38 -15
  145. package/src/components/currency-input.test.tsx +63 -0
  146. package/src/components/currency-input.tsx +84 -66
  147. package/src/components/date-input.stories.tsx +31 -0
  148. package/src/components/date-input.tsx +43 -0
  149. package/src/components/description-list.stories.tsx +62 -0
  150. package/src/components/description-list.tsx +68 -0
  151. package/src/components/dialog.test.tsx +84 -0
  152. package/src/components/dialog.tsx +64 -24
  153. package/src/components/drawer.tsx +72 -23
  154. package/src/components/dropdown-menu.test.tsx +180 -0
  155. package/src/components/dropdown-menu.tsx +31 -6
  156. package/src/components/field.stories.tsx +36 -0
  157. package/src/components/field.test.tsx +157 -0
  158. package/src/components/field.tsx +146 -0
  159. package/src/components/form.stories.tsx +3 -2
  160. package/src/components/form.test.tsx +95 -0
  161. package/src/components/form.tsx +9 -0
  162. package/src/components/haptic-button.tsx +1 -1
  163. package/src/components/input-group.stories.tsx +61 -0
  164. package/src/components/input-group.tsx +99 -0
  165. package/src/components/input.test.tsx +103 -0
  166. package/src/components/input.tsx +24 -7
  167. package/src/components/item.stories.tsx +52 -0
  168. package/src/components/item.tsx +115 -0
  169. package/src/components/kbd.stories.tsx +33 -0
  170. package/src/components/kbd.tsx +46 -0
  171. package/src/components/lazy-markdown.tsx +10 -7
  172. package/src/components/multi-select-combobox.test.tsx +101 -0
  173. package/src/components/multi-select-combobox.tsx +85 -82
  174. package/src/components/multi-select-menu.tsx +49 -41
  175. package/src/components/native-select.stories.tsx +52 -0
  176. package/src/components/native-select.tsx +45 -0
  177. package/src/components/pagination.tsx +64 -5
  178. package/src/components/popover.tsx +66 -3
  179. package/src/components/progress.tsx +27 -4
  180. package/src/components/radio-group.test.tsx +64 -0
  181. package/src/components/radio-group.tsx +11 -3
  182. package/src/components/rating.test.tsx +112 -0
  183. package/src/components/rating.tsx +97 -82
  184. package/src/components/search-bar.stories.tsx +1 -32
  185. package/src/components/search-bar.tsx +22 -59
  186. package/src/components/searchable-select.tsx +69 -73
  187. package/src/components/select.tsx +34 -19
  188. package/src/components/selection-bar.stories.tsx +37 -0
  189. package/src/components/selection-bar.tsx +65 -0
  190. package/src/components/separator.tsx +5 -2
  191. package/src/components/sheet.tsx +1 -1
  192. package/src/components/skeleton.tsx +2 -2
  193. package/src/components/slider.test.tsx +45 -0
  194. package/src/components/slider.tsx +31 -12
  195. package/src/components/sliding-panels.tsx +8 -5
  196. package/src/components/spinner.stories.tsx +35 -0
  197. package/src/components/spinner.tsx +50 -0
  198. package/src/components/stepper.stories.tsx +71 -0
  199. package/src/components/stepper.tsx +220 -0
  200. package/src/components/switch.test.tsx +52 -0
  201. package/src/components/switch.tsx +4 -3
  202. package/src/components/table.tsx +140 -35
  203. package/src/components/tabs.test.tsx +107 -0
  204. package/src/components/tabs.tsx +70 -23
  205. package/src/components/tag-group.stories.tsx +39 -0
  206. package/src/components/tag-group.tsx +28 -0
  207. package/src/components/tag.stories.tsx +50 -0
  208. package/src/components/tag.tsx +63 -0
  209. package/src/components/text.test.tsx +1 -1
  210. package/src/components/text.tsx +19 -3
  211. package/src/components/textarea.test.tsx +48 -0
  212. package/src/components/textarea.tsx +2 -1
  213. package/src/components/toast.tsx +4 -3
  214. package/src/components/toggle.tsx +2 -2
  215. package/src/components/tooltip.tsx +12 -2
  216. package/src/globals.css +126 -90
  217. package/dist/chunk-2BMJRXQX.js +0 -80
  218. package/dist/chunk-4AVS6CV4.js +0 -89
  219. package/dist/chunk-4UO73SKC.js +0 -26
  220. package/dist/chunk-CW24PLDW.js +0 -24
  221. package/dist/chunk-IQASCH2V.js +0 -25
  222. package/dist/chunk-NKHH23YK.js +0 -31
  223. package/dist/chunk-QJ54Z7TC.js +0 -83
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
- import { Check, ChevronsUpDown, X } from "lucide-react";
4
+ import { ChevronsUpDown, X } from "lucide-react";
5
5
  import { cn } from "../lib/utils";
6
6
  import { Button } from "./button";
7
7
  import { Badge } from "./badge";
8
+ import { Checkbox } from "./checkbox";
8
9
  import {
9
10
  Command,
10
11
  CommandEmpty,
@@ -50,6 +51,7 @@ export function MultiSelectCombobox({
50
51
  shouldFilter = true,
51
52
  }: MultiSelectComboboxProps) {
52
53
  const [open, setOpen] = React.useState(false);
54
+ const listboxId = React.useId();
53
55
 
54
56
  const handleSelect = (value: string) => {
55
57
  const newSelected = selected.includes(value)
@@ -58,91 +60,92 @@ export function MultiSelectCombobox({
58
60
  onSelectionChange(newSelected);
59
61
  };
60
62
 
61
- const handleRemove = (value: string, e: React.MouseEvent | React.KeyboardEvent) => {
62
- e.stopPropagation();
63
- onSelectionChange(selected.filter((item) => item !== value));
64
- };
65
-
66
- const selectedLabels = selected
67
- .map((value) => options.find((opt) => opt.value === value)?.label || value)
68
- .slice(0, maxDisplay);
69
-
70
- const remainingCount = selected.length - maxDisplay;
63
+ const visible = selected.slice(0, maxDisplay);
64
+ const remaining = selected.length - maxDisplay;
71
65
 
72
66
  return (
73
- <Popover open={open} onOpenChange={setOpen}>
74
- <PopoverTrigger asChild>
75
- <Button
76
- variant="outline"
77
- role="combobox"
78
- aria-expanded={open}
79
- className={cn(
80
- "w-full justify-between h-auto min-h-10 py-2",
81
- selected.length === 0 && "text-muted-foreground",
82
- className
83
- )}
84
- disabled={disabled}
67
+ <div className={cn("flex flex-col gap-2", className)}>
68
+ <Popover open={open} onOpenChange={setOpen}>
69
+ <PopoverTrigger asChild>
70
+ <Button
71
+ variant="outline"
72
+ role="combobox"
73
+ aria-expanded={open}
74
+ aria-haspopup="listbox"
75
+ aria-controls={open ? listboxId : undefined}
76
+ className={cn(
77
+ "w-full justify-between font-normal",
78
+ selected.length === 0 && "text-muted-foreground"
79
+ )}
80
+ disabled={disabled}
81
+ >
82
+ <span className="truncate">
83
+ {selected.length === 0
84
+ ? placeholder
85
+ : `${selected.length} selected`}
86
+ </span>
87
+ <ChevronsUpDown className="size-4 shrink-0 opacity-50" />
88
+ </Button>
89
+ </PopoverTrigger>
90
+ <PopoverContent
91
+ id={listboxId}
92
+ className="w-[var(--radix-popover-trigger-width)] p-0"
93
+ align="start"
85
94
  >
86
- <div className="flex flex-wrap gap-1 flex-1 text-left">
87
- {selected.length === 0 ? (
88
- <span>{placeholder}</span>
89
- ) : (
90
- <>
91
- {selectedLabels.map((label, i) => (
92
- <Badge key={selected[i]} variant="secondary" className="text-xs">
93
- {label}
94
- <div
95
- role="button"
96
- tabIndex={0}
97
- className="ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer"
98
- onClick={(e) => handleRemove(selected[i], e)}
99
- onKeyDown={(e) => {
100
- if (e.key === "Enter" || e.key === " ") {
101
- e.preventDefault();
102
- handleRemove(selected[i], e);
103
- }
104
- }}
95
+ <Command shouldFilter={shouldFilter}>
96
+ <CommandInput placeholder={searchPlaceholder} onValueChange={onSearchChange} />
97
+ <CommandList>
98
+ <CommandEmpty>{loading ? "Loading…" : emptyText}</CommandEmpty>
99
+ <CommandGroup>
100
+ {options.map((option) => {
101
+ const isSelected = selected.includes(option.value);
102
+ return (
103
+ <CommandItem
104
+ key={option.value}
105
+ value={option.value}
106
+ onSelect={() => handleSelect(option.value)}
107
+ className="gap-2"
105
108
  >
106
- <X className="h-3 w-3" />
107
- </div>
108
- </Badge>
109
- ))}
110
- {remainingCount > 0 && (
111
- <Badge variant="secondary" className="text-xs">
112
- +{remainingCount} more
113
- </Badge>
114
- )}
115
- </>
116
- )}
117
- </div>
118
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
119
- </Button>
120
- </PopoverTrigger>
121
- <PopoverContent className="w-full p-0" align="start">
122
- <Command shouldFilter={shouldFilter}>
123
- <CommandInput placeholder={searchPlaceholder} onValueChange={onSearchChange} />
124
- <CommandList>
125
- <CommandEmpty>{loading ? "Loading..." : emptyText}</CommandEmpty>
126
- <CommandGroup>
127
- {options.map((option) => (
128
- <CommandItem
129
- key={option.value}
130
- value={option.value}
131
- onSelect={() => handleSelect(option.value)}
109
+ <Checkbox
110
+ checked={isSelected}
111
+ tabIndex={-1}
112
+ aria-hidden
113
+ className="pointer-events-none"
114
+ />
115
+ {option.label}
116
+ </CommandItem>
117
+ );
118
+ })}
119
+ </CommandGroup>
120
+ </CommandList>
121
+ </Command>
122
+ </PopoverContent>
123
+ </Popover>
124
+ {selected.length > 0 && (
125
+ <div className="flex flex-wrap gap-1">
126
+ {visible.map((value) => {
127
+ const label = options.find((opt) => opt.value === value)?.label || value;
128
+ return (
129
+ <Badge key={value} variant="secondary" className="gap-1 pr-1">
130
+ {label}
131
+ <button
132
+ type="button"
133
+ aria-label={`Remove ${label}`}
134
+ onClick={() =>
135
+ onSelectionChange(selected.filter((item) => item !== value))
136
+ }
137
+ className="rounded-full p-0.5 hover:bg-foreground/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors"
132
138
  >
133
- <Check
134
- className={cn(
135
- "mr-2 h-4 w-4",
136
- selected.includes(option.value) ? "opacity-100" : "opacity-0"
137
- )}
138
- />
139
- {option.label}
140
- </CommandItem>
141
- ))}
142
- </CommandGroup>
143
- </CommandList>
144
- </Command>
145
- </PopoverContent>
146
- </Popover>
139
+ <X className="size-3" />
140
+ </button>
141
+ </Badge>
142
+ );
143
+ })}
144
+ {remaining > 0 && (
145
+ <Badge variant="secondary">+{remaining} more</Badge>
146
+ )}
147
+ </div>
148
+ )}
149
+ </div>
147
150
  );
148
151
  }
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
4
5
  import { cn } from "../lib/utils";
5
6
  import { Checkbox } from "./checkbox";
6
- import { Popover, PopoverContent, PopoverTrigger } from "./popover";
7
7
 
8
8
  export interface MultiSelectMenuOption {
9
9
  value: string;
@@ -15,14 +15,10 @@ export interface MultiSelectMenuProps {
15
15
  options: MultiSelectMenuOption[];
16
16
  selected: string[];
17
17
  onSelectedChange: (values: string[]) => void;
18
- /** Trigger element rendered via Radix `asChild` — typically a Button. */
19
18
  trigger: React.ReactNode;
20
- /** Optional heading rendered above the option list. */
21
19
  label?: string;
22
20
  align?: "start" | "center" | "end";
23
- /** Pixel width of the popover content. Defaults to 220. */
24
21
  width?: number;
25
- /** className for the popover content. */
26
22
  contentClassName?: string;
27
23
  emptyText?: string;
28
24
  }
@@ -51,46 +47,58 @@ export function MultiSelectMenu({
51
47
  }
52
48
 
53
49
  return (
54
- <Popover>
55
- <PopoverTrigger asChild>{trigger}</PopoverTrigger>
56
- <PopoverContent
57
- align={align}
58
- className={cn("p-2", contentClassName)}
59
- style={{ width }}
60
- >
61
- {label && (
62
- <div className="text-foreground px-2 py-1.5 text-sm font-semibold">
63
- {label}
64
- </div>
65
- )}
66
- <div role="group" aria-label={label} className="flex flex-col gap-0.5">
50
+ <DropdownMenuPrimitive.Root>
51
+ <DropdownMenuPrimitive.Trigger asChild>{trigger}</DropdownMenuPrimitive.Trigger>
52
+ <DropdownMenuPrimitive.Portal>
53
+ <DropdownMenuPrimitive.Content
54
+ align={align}
55
+ sideOffset={4}
56
+ style={{ width }}
57
+ className={cn(
58
+ "z-50 overflow-hidden rounded-xl border border-border bg-popover p-2 text-popover-foreground shadow-xl",
59
+ "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",
60
+ "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",
61
+ contentClassName
62
+ )}
63
+ >
64
+ {label && (
65
+ <DropdownMenuPrimitive.Label className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
66
+ {label}
67
+ </DropdownMenuPrimitive.Label>
68
+ )}
67
69
  {options.length === 0 ? (
68
- <p className="text-muted-foreground px-2 py-1.5 text-sm">{emptyText}</p>
70
+ <p className="px-2 py-1.5 text-sm text-muted-foreground">{emptyText}</p>
69
71
  ) : (
70
- options.map((option) => {
71
- const checked = selectedSet.has(option.value);
72
- return (
73
- <label
74
- key={option.value}
75
- className={cn(
76
- "flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm select-none",
77
- "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
78
- option.disabled && "pointer-events-none opacity-50",
79
- )}
80
- >
81
- <Checkbox
72
+ <DropdownMenuPrimitive.Group className="flex flex-col gap-0.5">
73
+ {options.map((option) => {
74
+ const checked = selectedSet.has(option.value);
75
+ return (
76
+ <DropdownMenuPrimitive.CheckboxItem
77
+ key={option.value}
82
78
  checked={checked}
79
+ disabled={option.disabled ?? false}
80
+ onSelect={(e) => e.preventDefault()}
83
81
  onCheckedChange={() => handleToggle(option.value)}
84
- disabled={option.disabled}
85
- aria-label={typeof option.label === "string" ? option.label : option.value}
86
- />
87
- <span className="flex-1">{option.label}</span>
88
- </label>
89
- );
90
- })
82
+ className={cn(
83
+ "relative flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none",
84
+ "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
85
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
86
+ )}
87
+ >
88
+ <Checkbox
89
+ checked={checked}
90
+ tabIndex={-1}
91
+ aria-hidden
92
+ className="pointer-events-none"
93
+ />
94
+ <span className="flex-1">{option.label}</span>
95
+ </DropdownMenuPrimitive.CheckboxItem>
96
+ );
97
+ })}
98
+ </DropdownMenuPrimitive.Group>
91
99
  )}
92
- </div>
93
- </PopoverContent>
94
- </Popover>
100
+ </DropdownMenuPrimitive.Content>
101
+ </DropdownMenuPrimitive.Portal>
102
+ </DropdownMenuPrimitive.Root>
95
103
  );
96
104
  }
@@ -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>