@bearmenu/ui 0.1.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 (207) hide show
  1. package/dist/chunk-2CUFS5LF.js +80 -0
  2. package/dist/chunk-3GWWZKX7.js +33 -0
  3. package/dist/chunk-3VMDCCCX.js +10 -0
  4. package/dist/chunk-4AVS6CV4.js +89 -0
  5. package/dist/chunk-4JOV7FXW.js +17 -0
  6. package/dist/chunk-4KOMHDZD.js +89 -0
  7. package/dist/chunk-4UO73SKC.js +26 -0
  8. package/dist/chunk-7H54PTVH.js +116 -0
  9. package/dist/chunk-CW24PLDW.js +24 -0
  10. package/dist/chunk-EBAMCZDW.js +112 -0
  11. package/dist/chunk-ERMIA7NP.js +106 -0
  12. package/dist/chunk-ESO2EM3L.js +46 -0
  13. package/dist/chunk-EUHEIWHZ.js +32 -0
  14. package/dist/chunk-FEK5XGZW.js +8 -0
  15. package/dist/chunk-NKHH23YK.js +31 -0
  16. package/dist/chunk-R47NEOSI.js +78 -0
  17. package/dist/chunk-UXAYXRFN.js +69 -0
  18. package/dist/chunk-X6GATUI2.js +19 -0
  19. package/dist/components/accordion.d.ts +9 -0
  20. package/dist/components/accordion.js +53 -0
  21. package/dist/components/alert-dialog.d.ts +23 -0
  22. package/dist/components/alert-dialog.js +98 -0
  23. package/dist/components/alert.d.ts +11 -0
  24. package/dist/components/alert.js +52 -0
  25. package/dist/components/aspect-ratio.d.ts +6 -0
  26. package/dist/components/aspect-ratio.js +2 -0
  27. package/dist/components/avatar.d.ts +8 -0
  28. package/dist/components/avatar.js +47 -0
  29. package/dist/components/badge.d.ts +13 -0
  30. package/dist/components/badge.js +3 -0
  31. package/dist/components/breadcrumb.d.ts +14 -0
  32. package/dist/components/breadcrumb.js +96 -0
  33. package/dist/components/button.d.ts +16 -0
  34. package/dist/components/button.js +4 -0
  35. package/dist/components/calendar.d.ts +11 -0
  36. package/dist/components/calendar.js +56 -0
  37. package/dist/components/card.d.ts +13 -0
  38. package/dist/components/card.js +4 -0
  39. package/dist/components/carousel.d.ts +24 -0
  40. package/dist/components/carousel.js +198 -0
  41. package/dist/components/checkbox.d.ts +6 -0
  42. package/dist/components/checkbox.js +25 -0
  43. package/dist/components/collapsible.d.ts +8 -0
  44. package/dist/components/collapsible.js +26 -0
  45. package/dist/components/command.d.ts +84 -0
  46. package/dist/components/command.js +4 -0
  47. package/dist/components/currency-input.d.ts +17 -0
  48. package/dist/components/currency-input.js +121 -0
  49. package/dist/components/dialog.d.ts +22 -0
  50. package/dist/components/dialog.js +3 -0
  51. package/dist/components/drawer.d.ts +27 -0
  52. package/dist/components/drawer.js +85 -0
  53. package/dist/components/dropdown-menu.d.ts +28 -0
  54. package/dist/components/dropdown-menu.js +211 -0
  55. package/dist/components/empty-state.d.ts +22 -0
  56. package/dist/components/empty-state.js +81 -0
  57. package/dist/components/form.d.ts +28 -0
  58. package/dist/components/form.js +97 -0
  59. package/dist/components/haptic-button.d.ts +12 -0
  60. package/dist/components/haptic-button.js +17 -0
  61. package/dist/components/input-otp.d.ts +14 -0
  62. package/dist/components/input-otp.js +62 -0
  63. package/dist/components/input.d.ts +6 -0
  64. package/dist/components/input.js +3 -0
  65. package/dist/components/label.d.ts +8 -0
  66. package/dist/components/label.js +3 -0
  67. package/dist/components/lazy-markdown.d.ts +11 -0
  68. package/dist/components/lazy-markdown.js +13 -0
  69. package/dist/components/link-button.d.ts +30 -0
  70. package/dist/components/link-button.js +17 -0
  71. package/dist/components/markdown-renderer.d.ts +11 -0
  72. package/dist/components/markdown-renderer.js +10 -0
  73. package/dist/components/multi-select-combobox.d.ts +23 -0
  74. package/dist/components/multi-select-combobox.js +114 -0
  75. package/dist/components/pagination.d.ts +18 -0
  76. package/dist/components/pagination.js +101 -0
  77. package/dist/components/popover.d.ts +8 -0
  78. package/dist/components/popover.js +3 -0
  79. package/dist/components/progress.d.ts +6 -0
  80. package/dist/components/progress.js +30 -0
  81. package/dist/components/radio-group.d.ts +7 -0
  82. package/dist/components/radio-group.js +30 -0
  83. package/dist/components/rating.d.ts +15 -0
  84. package/dist/components/rating.js +120 -0
  85. package/dist/components/scroll-area.d.ts +7 -0
  86. package/dist/components/scroll-area.js +44 -0
  87. package/dist/components/search-bar.d.ts +17 -0
  88. package/dist/components/search-bar.js +92 -0
  89. package/dist/components/searchable-select.d.ts +22 -0
  90. package/dist/components/searchable-select.js +104 -0
  91. package/dist/components/select.d.ts +15 -0
  92. package/dist/components/select.js +129 -0
  93. package/dist/components/separator.d.ts +6 -0
  94. package/dist/components/separator.js +25 -0
  95. package/dist/components/sheet.d.ts +29 -0
  96. package/dist/components/sheet.js +96 -0
  97. package/dist/components/skeleton-card.d.ts +13 -0
  98. package/dist/components/skeleton-card.js +154 -0
  99. package/dist/components/skeleton.d.ts +6 -0
  100. package/dist/components/skeleton.js +3 -0
  101. package/dist/components/slider.d.ts +6 -0
  102. package/dist/components/slider.js +24 -0
  103. package/dist/components/sliding-panels.d.ts +19 -0
  104. package/dist/components/sliding-panels.js +75 -0
  105. package/dist/components/sonner.d.ts +6 -0
  106. package/dist/components/sonner.js +23 -0
  107. package/dist/components/sticky-container.d.ts +44 -0
  108. package/dist/components/sticky-container.js +39 -0
  109. package/dist/components/switch.d.ts +6 -0
  110. package/dist/components/switch.js +31 -0
  111. package/dist/components/table.d.ts +12 -0
  112. package/dist/components/table.js +81 -0
  113. package/dist/components/tabs.d.ts +12 -0
  114. package/dist/components/tabs.js +55 -0
  115. package/dist/components/text.d.ts +31 -0
  116. package/dist/components/text.js +3 -0
  117. package/dist/components/textarea.d.ts +6 -0
  118. package/dist/components/textarea.js +23 -0
  119. package/dist/components/toast.d.ts +18 -0
  120. package/dist/components/toast.js +3 -0
  121. package/dist/components/toaster.d.ts +5 -0
  122. package/dist/components/toaster.js +25 -0
  123. package/dist/components/toggle-group.d.ts +12 -0
  124. package/dist/components/toggle-group.js +72 -0
  125. package/dist/components/toggle.d.ts +13 -0
  126. package/dist/components/toggle.js +3 -0
  127. package/dist/components/tooltip.d.ts +9 -0
  128. package/dist/components/tooltip.js +26 -0
  129. package/dist/components/use-mobile.d.ts +3 -0
  130. package/dist/components/use-mobile.js +19 -0
  131. package/dist/components/use-toast.d.ts +42 -0
  132. package/dist/components/use-toast.js +2 -0
  133. package/dist/hooks/use-haptic.d.ts +22 -0
  134. package/dist/hooks/use-haptic.js +2 -0
  135. package/dist/hooks/use-mobile.d.ts +3 -0
  136. package/dist/hooks/use-mobile.js +19 -0
  137. package/dist/hooks/use-mounted-theme.d.ts +6 -0
  138. package/dist/hooks/use-mounted-theme.js +18 -0
  139. package/dist/hooks/use-sticky.d.ts +18 -0
  140. package/dist/hooks/use-sticky.js +2 -0
  141. package/dist/lib/utils.d.ts +5 -0
  142. package/dist/lib/utils.js +2 -0
  143. package/package.json +203 -0
  144. package/src/components/accordion.tsx +57 -0
  145. package/src/components/alert-dialog.tsx +116 -0
  146. package/src/components/alert.tsx +57 -0
  147. package/src/components/aspect-ratio.tsx +9 -0
  148. package/src/components/avatar.tsx +49 -0
  149. package/src/components/badge.tsx +38 -0
  150. package/src/components/breadcrumb.tsx +102 -0
  151. package/src/components/button.tsx +94 -0
  152. package/src/components/calendar.tsx +60 -0
  153. package/src/components/card.tsx +67 -0
  154. package/src/components/carousel.tsx +230 -0
  155. package/src/components/checkbox.tsx +27 -0
  156. package/src/components/collapsible.tsx +28 -0
  157. package/src/components/command.tsx +148 -0
  158. package/src/components/currency-input.tsx +158 -0
  159. package/src/components/dialog.tsx +103 -0
  160. package/src/components/drawer.tsx +99 -0
  161. package/src/components/dropdown-menu.tsx +228 -0
  162. package/src/components/empty-state.tsx +118 -0
  163. package/src/components/form.tsx +152 -0
  164. package/src/components/haptic-button.tsx +19 -0
  165. package/src/components/input-otp.tsx +70 -0
  166. package/src/components/input.tsx +23 -0
  167. package/src/components/label.tsx +20 -0
  168. package/src/components/lazy-markdown.tsx +20 -0
  169. package/src/components/link-button.tsx +40 -0
  170. package/src/components/markdown-renderer.tsx +20 -0
  171. package/src/components/multi-select-combobox.tsx +148 -0
  172. package/src/components/pagination.tsx +106 -0
  173. package/src/components/popover.tsx +30 -0
  174. package/src/components/progress.tsx +27 -0
  175. package/src/components/radio-group.tsx +37 -0
  176. package/src/components/rating.tsx +136 -0
  177. package/src/components/scroll-area.tsx +45 -0
  178. package/src/components/search-bar.tsx +113 -0
  179. package/src/components/searchable-select.tsx +134 -0
  180. package/src/components/select.tsx +152 -0
  181. package/src/components/separator.tsx +25 -0
  182. package/src/components/sheet.tsx +121 -0
  183. package/src/components/skeleton-card.tsx +282 -0
  184. package/src/components/skeleton.tsx +17 -0
  185. package/src/components/slider.tsx +24 -0
  186. package/src/components/sliding-panels.tsx +99 -0
  187. package/src/components/sonner.tsx +25 -0
  188. package/src/components/sticky-container.tsx +88 -0
  189. package/src/components/switch.tsx +28 -0
  190. package/src/components/table.tsx +90 -0
  191. package/src/components/tabs.tsx +72 -0
  192. package/src/components/text.test.tsx +192 -0
  193. package/src/components/text.tsx +114 -0
  194. package/src/components/textarea.tsx +22 -0
  195. package/src/components/toast.tsx +129 -0
  196. package/src/components/toaster.tsx +31 -0
  197. package/src/components/toggle-group.tsx +69 -0
  198. package/src/components/toggle.tsx +46 -0
  199. package/src/components/tooltip.tsx +29 -0
  200. package/src/components/use-mobile.tsx +19 -0
  201. package/src/components/use-toast.ts +180 -0
  202. package/src/globals.css +409 -0
  203. package/src/hooks/use-haptic.ts +118 -0
  204. package/src/hooks/use-mobile.ts +19 -0
  205. package/src/hooks/use-mounted-theme.ts +20 -0
  206. package/src/hooks/use-sticky.ts +47 -0
  207. package/src/lib/utils.ts +6 -0
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import ReactMarkdown, { type Components } from "react-markdown";
4
+ import remarkGfm from "remark-gfm";
5
+
6
+ type MarkdownRendererProps = {
7
+ content: string;
8
+ className?: string;
9
+ components?: Components;
10
+ };
11
+
12
+ export function MarkdownRenderer({ content, className, components }: MarkdownRendererProps) {
13
+ return (
14
+ <div className={className}>
15
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
16
+ {content}
17
+ </ReactMarkdown>
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,148 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Check, ChevronsUpDown, X } from "lucide-react";
5
+ import { cn } from "../lib/utils";
6
+ import { Button } from "./button";
7
+ import { Badge } from "./badge";
8
+ import {
9
+ Command,
10
+ CommandEmpty,
11
+ CommandGroup,
12
+ CommandInput,
13
+ CommandItem,
14
+ CommandList,
15
+ } from "./command";
16
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
17
+
18
+ export interface MultiSelectOption {
19
+ value: string;
20
+ label: string;
21
+ }
22
+
23
+ interface MultiSelectComboboxProps {
24
+ options: MultiSelectOption[];
25
+ selected: string[];
26
+ onSelectionChange: (values: string[]) => void;
27
+ placeholder?: string;
28
+ searchPlaceholder?: string;
29
+ emptyText?: string;
30
+ className?: string;
31
+ disabled?: boolean;
32
+ loading?: boolean;
33
+ maxDisplay?: number;
34
+ onSearchChange?: (value: string) => void;
35
+ shouldFilter?: boolean;
36
+ }
37
+
38
+ export function MultiSelectCombobox({
39
+ options,
40
+ selected,
41
+ onSelectionChange,
42
+ placeholder = "Select items...",
43
+ searchPlaceholder = "Search...",
44
+ emptyText = "No items found.",
45
+ className,
46
+ disabled = false,
47
+ loading = false,
48
+ maxDisplay = 3,
49
+ onSearchChange,
50
+ shouldFilter = true,
51
+ }: MultiSelectComboboxProps) {
52
+ const [open, setOpen] = React.useState(false);
53
+
54
+ const handleSelect = (value: string) => {
55
+ const newSelected = selected.includes(value)
56
+ ? selected.filter((item) => item !== value)
57
+ : [...selected, value];
58
+ onSelectionChange(newSelected);
59
+ };
60
+
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;
71
+
72
+ 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}
85
+ >
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
+ }}
105
+ >
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)}
132
+ >
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>
147
+ );
148
+ }
@@ -0,0 +1,106 @@
1
+ import * as React from "react";
2
+ import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react";
3
+
4
+ import { cn } from "../lib/utils";
5
+ import { Button, buttonVariants } from "./button";
6
+
7
+ function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
8
+ return (
9
+ <nav
10
+ role="navigation"
11
+ aria-label="pagination"
12
+ data-slot="pagination"
13
+ className={cn("mx-auto flex w-full justify-center", className)}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+
19
+ function PaginationContent({ className, ...props }: React.ComponentProps<"ul">) {
20
+ return (
21
+ <ul
22
+ data-slot="pagination-content"
23
+ className={cn("flex flex-row items-center gap-1", className)}
24
+ {...props}
25
+ />
26
+ );
27
+ }
28
+
29
+ function PaginationItem({ ...props }: React.ComponentProps<"li">) {
30
+ return <li data-slot="pagination-item" {...props} />;
31
+ }
32
+
33
+ type PaginationLinkProps = {
34
+ isActive?: boolean;
35
+ } & Pick<React.ComponentProps<typeof Button>, "size"> &
36
+ React.ComponentProps<"a">;
37
+
38
+ function PaginationLink({ className, isActive, size = "icon", ...props }: PaginationLinkProps) {
39
+ return (
40
+ <a
41
+ aria-current={isActive ? "page" : undefined}
42
+ data-slot="pagination-link"
43
+ data-active={isActive}
44
+ className={cn(
45
+ buttonVariants({
46
+ variant: isActive ? "outline" : "ghost",
47
+ size,
48
+ }),
49
+ className
50
+ )}
51
+ {...props}
52
+ />
53
+ );
54
+ }
55
+
56
+ function PaginationPrevious({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
57
+ return (
58
+ <PaginationLink
59
+ aria-label="Go to previous page"
60
+ size="default"
61
+ className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
62
+ {...props}
63
+ >
64
+ <ChevronLeftIcon />
65
+ <span className="hidden sm:block">Previous</span>
66
+ </PaginationLink>
67
+ );
68
+ }
69
+
70
+ function PaginationNext({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
71
+ return (
72
+ <PaginationLink
73
+ aria-label="Go to next page"
74
+ size="default"
75
+ className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
76
+ {...props}
77
+ >
78
+ <span className="hidden sm:block">Next</span>
79
+ <ChevronRightIcon />
80
+ </PaginationLink>
81
+ );
82
+ }
83
+
84
+ function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span">) {
85
+ return (
86
+ <span
87
+ aria-hidden
88
+ data-slot="pagination-ellipsis"
89
+ className={cn("flex size-9 items-center justify-center", className)}
90
+ {...props}
91
+ >
92
+ <MoreHorizontalIcon className="size-4" />
93
+ <span className="sr-only">More pages</span>
94
+ </span>
95
+ );
96
+ }
97
+
98
+ export {
99
+ Pagination,
100
+ PaginationContent,
101
+ PaginationLink,
102
+ PaginationItem,
103
+ PaginationPrevious,
104
+ PaginationNext,
105
+ PaginationEllipsis,
106
+ };
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
5
+ import { cn } from "../lib/utils";
6
+
7
+ const Popover = PopoverPrimitive.Root;
8
+
9
+ const PopoverTrigger = PopoverPrimitive.Trigger;
10
+
11
+ const PopoverContent = React.forwardRef<
12
+ React.ElementRef<typeof PopoverPrimitive.Content>,
13
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
14
+ >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
15
+ <PopoverPrimitive.Portal>
16
+ <PopoverPrimitive.Content
17
+ ref={ref}
18
+ align={align}
19
+ sideOffset={sideOffset}
20
+ 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",
22
+ className
23
+ )}
24
+ {...props}
25
+ />
26
+ </PopoverPrimitive.Portal>
27
+ ));
28
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
29
+
30
+ export { Popover, PopoverTrigger, PopoverContent };
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as ProgressPrimitive from "@radix-ui/react-progress";
5
+ import { cn } from "../lib/utils";
6
+
7
+ const Progress = React.forwardRef<
8
+ React.ElementRef<typeof ProgressPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
10
+ >(({ className, value, ...props }, ref) => (
11
+ <ProgressPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "relative h-2 w-full overflow-hidden rounded-full glass border border-border/50",
15
+ className
16
+ )}
17
+ {...props}
18
+ >
19
+ <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"
21
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
22
+ />
23
+ </ProgressPrimitive.Root>
24
+ ));
25
+ Progress.displayName = ProgressPrimitive.Root.displayName;
26
+
27
+ export { Progress };
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
5
+ import { Circle } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ const RadioGroup = React.forwardRef<
9
+ React.ElementRef<typeof RadioGroupPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
11
+ >(({ className, ...props }, ref) => {
12
+ return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
13
+ });
14
+ RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
15
+
16
+ const RadioGroupItem = React.forwardRef<
17
+ React.ElementRef<typeof RadioGroupPrimitive.Item>,
18
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
19
+ >(({ className, ...props }, ref) => {
20
+ return (
21
+ <RadioGroupPrimitive.Item
22
+ ref={ref}
23
+ 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",
25
+ className
26
+ )}
27
+ {...props}
28
+ >
29
+ <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" />
31
+ </RadioGroupPrimitive.Indicator>
32
+ </RadioGroupPrimitive.Item>
33
+ );
34
+ });
35
+ RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
36
+
37
+ export { RadioGroup, RadioGroupItem };
@@ -0,0 +1,136 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+ import { Star, StarHalf } from "lucide-react";
6
+
7
+ interface RatingProps {
8
+ value: number; // 1-5
9
+ max?: number; // default 5
10
+ size?: "sm" | "md" | "lg";
11
+ showCount?: boolean;
12
+ reviewCount?: number;
13
+ readonly?: boolean;
14
+ onRatingChange?: (rating: number) => void;
15
+ className?: string;
16
+ }
17
+
18
+ const Rating = React.forwardRef<HTMLDivElement, RatingProps>(
19
+ (
20
+ {
21
+ value,
22
+ max = 5,
23
+ size = "md",
24
+ showCount = true,
25
+ reviewCount,
26
+ readonly = true,
27
+ onRatingChange,
28
+ className,
29
+ ...props
30
+ },
31
+ ref
32
+ ) => {
33
+ const [hoverValue, setHoverValue] = React.useState<number | null>(null);
34
+
35
+ const sizeClasses = {
36
+ sm: "h-3 w-3",
37
+ md: "h-4 w-4",
38
+ lg: "h-5 w-5",
39
+ };
40
+
41
+ const textSizeClasses = {
42
+ sm: "text-xs",
43
+ md: "text-sm",
44
+ lg: "text-base",
45
+ };
46
+
47
+ const handleClick = (rating: number) => {
48
+ if (!readonly && onRatingChange) {
49
+ onRatingChange(rating);
50
+ }
51
+ };
52
+
53
+ const handleMouseEnter = (rating: number) => {
54
+ if (!readonly) {
55
+ setHoverValue(rating);
56
+ }
57
+ };
58
+
59
+ const handleMouseLeave = () => {
60
+ if (!readonly) {
61
+ setHoverValue(null);
62
+ }
63
+ };
64
+
65
+ const displayValue = hoverValue ?? value;
66
+ const fullStars = Math.floor(displayValue);
67
+ const hasHalfStar = displayValue % 1 !== 0;
68
+ const emptyStars = max - fullStars - (hasHalfStar ? 1 : 0);
69
+
70
+ return (
71
+ <div
72
+ ref={ref}
73
+ className={cn("flex items-center gap-1", !readonly && "cursor-pointer", className)}
74
+ {...props}
75
+ >
76
+ {/* Stars */}
77
+ <div className="flex items-center gap-0.5">
78
+ {/* Full stars */}
79
+ {Array.from({ length: fullStars }).map((_, i) => (
80
+ <Star
81
+ key={`full-${i}`}
82
+ className={cn(
83
+ sizeClasses[size],
84
+ "fill-primary text-primary transition-colors",
85
+ !readonly && "hover:fill-primary/80"
86
+ )}
87
+ onClick={() => handleClick(i + 1)}
88
+ onMouseEnter={() => handleMouseEnter(i + 1)}
89
+ onMouseLeave={handleMouseLeave}
90
+ />
91
+ ))}
92
+
93
+ {/* Half star */}
94
+ {hasHalfStar && (
95
+ <StarHalf
96
+ className={cn(
97
+ sizeClasses[size],
98
+ "fill-primary text-primary transition-colors",
99
+ !readonly && "hover:fill-primary/80"
100
+ )}
101
+ onClick={() => handleClick(fullStars + 1)}
102
+ onMouseEnter={() => handleMouseEnter(fullStars + 1)}
103
+ onMouseLeave={handleMouseLeave}
104
+ />
105
+ )}
106
+
107
+ {/* Empty stars */}
108
+ {Array.from({ length: emptyStars }).map((_, i) => (
109
+ <Star
110
+ key={`empty-${i}`}
111
+ className={cn(
112
+ sizeClasses[size],
113
+ "text-muted-foreground/30 transition-colors",
114
+ !readonly && "hover:text-primary/50"
115
+ )}
116
+ onClick={() => handleClick(fullStars + (hasHalfStar ? 1 : 0) + i + 1)}
117
+ onMouseEnter={() => handleMouseEnter(fullStars + (hasHalfStar ? 1 : 0) + i + 1)}
118
+ onMouseLeave={handleMouseLeave}
119
+ />
120
+ ))}
121
+ </div>
122
+
123
+ {/* Review count */}
124
+ {showCount && reviewCount !== undefined && (
125
+ <span className={cn("text-muted-foreground ml-1", textSizeClasses[size])}>
126
+ ({reviewCount})
127
+ </span>
128
+ )}
129
+ </div>
130
+ );
131
+ }
132
+ );
133
+
134
+ Rating.displayName = "Rating";
135
+
136
+ export { Rating };
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
5
+ import { cn } from "../lib/utils";
6
+
7
+ const ScrollArea = React.forwardRef<
8
+ React.ElementRef<typeof ScrollAreaPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
10
+ >(({ className, children, ...props }, ref) => (
11
+ <ScrollAreaPrimitive.Root
12
+ ref={ref}
13
+ className={cn("relative overflow-hidden", className)}
14
+ {...props}
15
+ >
16
+ <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
17
+ {children}
18
+ </ScrollAreaPrimitive.Viewport>
19
+ <ScrollBar />
20
+ <ScrollAreaPrimitive.Corner />
21
+ </ScrollAreaPrimitive.Root>
22
+ ));
23
+ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
24
+
25
+ const ScrollBar = React.forwardRef<
26
+ React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
27
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
28
+ >(({ className, orientation = "vertical", ...props }, ref) => (
29
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
30
+ ref={ref}
31
+ orientation={orientation}
32
+ className={cn(
33
+ "flex touch-none select-none transition-colors",
34
+ orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
35
+ orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
36
+ className
37
+ )}
38
+ {...props}
39
+ >
40
+ <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border/50 backdrop-blur-sm hover:bg-border transition-colors" />
41
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
42
+ ));
43
+ ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
44
+
45
+ export { ScrollArea, ScrollBar };
@@ -0,0 +1,113 @@
1
+ "use client";
2
+
3
+ import type { ChangeEvent } from "react";
4
+ import { Card } from "./card";
5
+ import { Loader2, Search } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ interface SearchBarProps {
9
+ placeholder?: string;
10
+ value?: string;
11
+ onChange?: (value: string) => void;
12
+ isSearching?: boolean;
13
+ className?: string;
14
+ inputClassName?: string;
15
+ autoFocus?: boolean;
16
+ inputAriaLabel?: string;
17
+ size?: "default" | "compact";
18
+ variant?: "primary" | "secondary" | "editorial";
19
+ }
20
+
21
+ export function SearchBar({
22
+ placeholder = "Search...",
23
+ value = "",
24
+ onChange,
25
+ isSearching = false,
26
+ className,
27
+ inputClassName,
28
+ autoFocus = false,
29
+ inputAriaLabel,
30
+ size = "default",
31
+ variant = "editorial",
32
+ }: SearchBarProps) {
33
+ const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
34
+ const newValue = e.target.value;
35
+ onChange?.(newValue);
36
+ };
37
+
38
+ const Wrapper = variant === "editorial" ? "div" : Card;
39
+ const wrapperClassName =
40
+ variant === "editorial"
41
+ ? cn(
42
+ "bg-surface border border-border rounded-[14px] h-12 flex items-center transition-colors focus-within:border-primary",
43
+ className
44
+ )
45
+ : cn(
46
+ "border-2 border-primary-foreground/20 backdrop-blur-xl rounded-lg p-0",
47
+ size === "compact" ? "md:p-1.5" : "md:p-2",
48
+ variant === "primary"
49
+ ? "!bg-primary-foreground/10 !text-primary !border-primary/20"
50
+ : "!bg-primary !text-primary-foreground !border-primary/20",
51
+ className
52
+ );
53
+
54
+ return (
55
+ <Wrapper className={wrapperClassName}>
56
+ <div className="flex items-center relative">
57
+ <div
58
+ className={cn(
59
+ "absolute top-1/2 z-10 -translate-y-1/2",
60
+ size === "compact" ? "left-3.5" : "left-4"
61
+ )}
62
+ >
63
+ <Search
64
+ className={cn(
65
+ variant === "editorial"
66
+ ? "text-muted-foreground"
67
+ : variant === "primary"
68
+ ? "text-primary"
69
+ : "text-primary-foreground",
70
+ size === "compact" ? "h-3.5 w-3.5" : "h-4 w-4"
71
+ )}
72
+ />
73
+ </div>
74
+ <input
75
+ type="text"
76
+ placeholder={placeholder}
77
+ value={value}
78
+ onChange={handleInputChange}
79
+ autoFocus={autoFocus}
80
+ aria-label={inputAriaLabel ?? placeholder}
81
+ className={cn(
82
+ "w-full bg-transparent border-0 focus:outline-none focus-visible:outline-none",
83
+ size === "compact"
84
+ ? "pl-9 pr-3 py-2.5 text-sm placeholder:text-sm rounded-lg"
85
+ : "pl-10 pr-4 py-3 text-[14px] placeholder:text-[14px] rounded-[14px]",
86
+ variant === "editorial"
87
+ ? "text-foreground placeholder:text-muted-foreground"
88
+ : variant === "primary"
89
+ ? "!text-primary placeholder:text-primary/60"
90
+ : "!text-primary-foreground placeholder:text-primary-foreground/60",
91
+ inputClassName
92
+ )}
93
+ />
94
+ {isSearching && (
95
+ <div
96
+ className={cn(
97
+ "absolute top-1/2 -translate-y-1/2",
98
+ size === "compact" ? "right-3.5" : "right-4"
99
+ )}
100
+ >
101
+ <Loader2
102
+ className={cn(
103
+ "animate-spin",
104
+ variant === "editorial" ? "text-muted-foreground" : "text-primary/60",
105
+ size === "compact" ? "h-3.5 w-3.5" : "h-4 w-4"
106
+ )}
107
+ />
108
+ </div>
109
+ )}
110
+ </div>
111
+ </Wrapper>
112
+ );
113
+ }