@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.
- package/dist/chunk-2CUFS5LF.js +80 -0
- package/dist/chunk-3GWWZKX7.js +33 -0
- package/dist/chunk-3VMDCCCX.js +10 -0
- package/dist/chunk-4AVS6CV4.js +89 -0
- package/dist/chunk-4JOV7FXW.js +17 -0
- package/dist/chunk-4KOMHDZD.js +89 -0
- package/dist/chunk-4UO73SKC.js +26 -0
- package/dist/chunk-7H54PTVH.js +116 -0
- package/dist/chunk-CW24PLDW.js +24 -0
- package/dist/chunk-EBAMCZDW.js +112 -0
- package/dist/chunk-ERMIA7NP.js +106 -0
- package/dist/chunk-ESO2EM3L.js +46 -0
- package/dist/chunk-EUHEIWHZ.js +32 -0
- package/dist/chunk-FEK5XGZW.js +8 -0
- package/dist/chunk-NKHH23YK.js +31 -0
- package/dist/chunk-R47NEOSI.js +78 -0
- package/dist/chunk-UXAYXRFN.js +69 -0
- package/dist/chunk-X6GATUI2.js +19 -0
- package/dist/components/accordion.d.ts +9 -0
- package/dist/components/accordion.js +53 -0
- package/dist/components/alert-dialog.d.ts +23 -0
- package/dist/components/alert-dialog.js +98 -0
- package/dist/components/alert.d.ts +11 -0
- package/dist/components/alert.js +52 -0
- package/dist/components/aspect-ratio.d.ts +6 -0
- package/dist/components/aspect-ratio.js +2 -0
- package/dist/components/avatar.d.ts +8 -0
- package/dist/components/avatar.js +47 -0
- package/dist/components/badge.d.ts +13 -0
- package/dist/components/badge.js +3 -0
- package/dist/components/breadcrumb.d.ts +14 -0
- package/dist/components/breadcrumb.js +96 -0
- package/dist/components/button.d.ts +16 -0
- package/dist/components/button.js +4 -0
- package/dist/components/calendar.d.ts +11 -0
- package/dist/components/calendar.js +56 -0
- package/dist/components/card.d.ts +13 -0
- package/dist/components/card.js +4 -0
- package/dist/components/carousel.d.ts +24 -0
- package/dist/components/carousel.js +198 -0
- package/dist/components/checkbox.d.ts +6 -0
- package/dist/components/checkbox.js +25 -0
- package/dist/components/collapsible.d.ts +8 -0
- package/dist/components/collapsible.js +26 -0
- package/dist/components/command.d.ts +84 -0
- package/dist/components/command.js +4 -0
- package/dist/components/currency-input.d.ts +17 -0
- package/dist/components/currency-input.js +121 -0
- package/dist/components/dialog.d.ts +22 -0
- package/dist/components/dialog.js +3 -0
- package/dist/components/drawer.d.ts +27 -0
- package/dist/components/drawer.js +85 -0
- package/dist/components/dropdown-menu.d.ts +28 -0
- package/dist/components/dropdown-menu.js +211 -0
- package/dist/components/empty-state.d.ts +22 -0
- package/dist/components/empty-state.js +81 -0
- package/dist/components/form.d.ts +28 -0
- package/dist/components/form.js +97 -0
- package/dist/components/haptic-button.d.ts +12 -0
- package/dist/components/haptic-button.js +17 -0
- package/dist/components/input-otp.d.ts +14 -0
- package/dist/components/input-otp.js +62 -0
- package/dist/components/input.d.ts +6 -0
- package/dist/components/input.js +3 -0
- package/dist/components/label.d.ts +8 -0
- package/dist/components/label.js +3 -0
- package/dist/components/lazy-markdown.d.ts +11 -0
- package/dist/components/lazy-markdown.js +13 -0
- package/dist/components/link-button.d.ts +30 -0
- package/dist/components/link-button.js +17 -0
- package/dist/components/markdown-renderer.d.ts +11 -0
- package/dist/components/markdown-renderer.js +10 -0
- package/dist/components/multi-select-combobox.d.ts +23 -0
- package/dist/components/multi-select-combobox.js +114 -0
- package/dist/components/pagination.d.ts +18 -0
- package/dist/components/pagination.js +101 -0
- package/dist/components/popover.d.ts +8 -0
- package/dist/components/popover.js +3 -0
- package/dist/components/progress.d.ts +6 -0
- package/dist/components/progress.js +30 -0
- package/dist/components/radio-group.d.ts +7 -0
- package/dist/components/radio-group.js +30 -0
- package/dist/components/rating.d.ts +15 -0
- package/dist/components/rating.js +120 -0
- package/dist/components/scroll-area.d.ts +7 -0
- package/dist/components/scroll-area.js +44 -0
- package/dist/components/search-bar.d.ts +17 -0
- package/dist/components/search-bar.js +92 -0
- package/dist/components/searchable-select.d.ts +22 -0
- package/dist/components/searchable-select.js +104 -0
- package/dist/components/select.d.ts +15 -0
- package/dist/components/select.js +129 -0
- package/dist/components/separator.d.ts +6 -0
- package/dist/components/separator.js +25 -0
- package/dist/components/sheet.d.ts +29 -0
- package/dist/components/sheet.js +96 -0
- package/dist/components/skeleton-card.d.ts +13 -0
- package/dist/components/skeleton-card.js +154 -0
- package/dist/components/skeleton.d.ts +6 -0
- package/dist/components/skeleton.js +3 -0
- package/dist/components/slider.d.ts +6 -0
- package/dist/components/slider.js +24 -0
- package/dist/components/sliding-panels.d.ts +19 -0
- package/dist/components/sliding-panels.js +75 -0
- package/dist/components/sonner.d.ts +6 -0
- package/dist/components/sonner.js +23 -0
- package/dist/components/sticky-container.d.ts +44 -0
- package/dist/components/sticky-container.js +39 -0
- package/dist/components/switch.d.ts +6 -0
- package/dist/components/switch.js +31 -0
- package/dist/components/table.d.ts +12 -0
- package/dist/components/table.js +81 -0
- package/dist/components/tabs.d.ts +12 -0
- package/dist/components/tabs.js +55 -0
- package/dist/components/text.d.ts +31 -0
- package/dist/components/text.js +3 -0
- package/dist/components/textarea.d.ts +6 -0
- package/dist/components/textarea.js +23 -0
- package/dist/components/toast.d.ts +18 -0
- package/dist/components/toast.js +3 -0
- package/dist/components/toaster.d.ts +5 -0
- package/dist/components/toaster.js +25 -0
- package/dist/components/toggle-group.d.ts +12 -0
- package/dist/components/toggle-group.js +72 -0
- package/dist/components/toggle.d.ts +13 -0
- package/dist/components/toggle.js +3 -0
- package/dist/components/tooltip.d.ts +9 -0
- package/dist/components/tooltip.js +26 -0
- package/dist/components/use-mobile.d.ts +3 -0
- package/dist/components/use-mobile.js +19 -0
- package/dist/components/use-toast.d.ts +42 -0
- package/dist/components/use-toast.js +2 -0
- package/dist/hooks/use-haptic.d.ts +22 -0
- package/dist/hooks/use-haptic.js +2 -0
- package/dist/hooks/use-mobile.d.ts +3 -0
- package/dist/hooks/use-mobile.js +19 -0
- package/dist/hooks/use-mounted-theme.d.ts +6 -0
- package/dist/hooks/use-mounted-theme.js +18 -0
- package/dist/hooks/use-sticky.d.ts +18 -0
- package/dist/hooks/use-sticky.js +2 -0
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +2 -0
- package/package.json +203 -0
- package/src/components/accordion.tsx +57 -0
- package/src/components/alert-dialog.tsx +116 -0
- package/src/components/alert.tsx +57 -0
- package/src/components/aspect-ratio.tsx +9 -0
- package/src/components/avatar.tsx +49 -0
- package/src/components/badge.tsx +38 -0
- package/src/components/breadcrumb.tsx +102 -0
- package/src/components/button.tsx +94 -0
- package/src/components/calendar.tsx +60 -0
- package/src/components/card.tsx +67 -0
- package/src/components/carousel.tsx +230 -0
- package/src/components/checkbox.tsx +27 -0
- package/src/components/collapsible.tsx +28 -0
- package/src/components/command.tsx +148 -0
- package/src/components/currency-input.tsx +158 -0
- package/src/components/dialog.tsx +103 -0
- package/src/components/drawer.tsx +99 -0
- package/src/components/dropdown-menu.tsx +228 -0
- package/src/components/empty-state.tsx +118 -0
- package/src/components/form.tsx +152 -0
- package/src/components/haptic-button.tsx +19 -0
- package/src/components/input-otp.tsx +70 -0
- package/src/components/input.tsx +23 -0
- package/src/components/label.tsx +20 -0
- package/src/components/lazy-markdown.tsx +20 -0
- package/src/components/link-button.tsx +40 -0
- package/src/components/markdown-renderer.tsx +20 -0
- package/src/components/multi-select-combobox.tsx +148 -0
- package/src/components/pagination.tsx +106 -0
- package/src/components/popover.tsx +30 -0
- package/src/components/progress.tsx +27 -0
- package/src/components/radio-group.tsx +37 -0
- package/src/components/rating.tsx +136 -0
- package/src/components/scroll-area.tsx +45 -0
- package/src/components/search-bar.tsx +113 -0
- package/src/components/searchable-select.tsx +134 -0
- package/src/components/select.tsx +152 -0
- package/src/components/separator.tsx +25 -0
- package/src/components/sheet.tsx +121 -0
- package/src/components/skeleton-card.tsx +282 -0
- package/src/components/skeleton.tsx +17 -0
- package/src/components/slider.tsx +24 -0
- package/src/components/sliding-panels.tsx +99 -0
- package/src/components/sonner.tsx +25 -0
- package/src/components/sticky-container.tsx +88 -0
- package/src/components/switch.tsx +28 -0
- package/src/components/table.tsx +90 -0
- package/src/components/tabs.tsx +72 -0
- package/src/components/text.test.tsx +192 -0
- package/src/components/text.tsx +114 -0
- package/src/components/textarea.tsx +22 -0
- package/src/components/toast.tsx +129 -0
- package/src/components/toaster.tsx +31 -0
- package/src/components/toggle-group.tsx +69 -0
- package/src/components/toggle.tsx +46 -0
- package/src/components/tooltip.tsx +29 -0
- package/src/components/use-mobile.tsx +19 -0
- package/src/components/use-toast.ts +180 -0
- package/src/globals.css +409 -0
- package/src/hooks/use-haptic.ts +118 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-mounted-theme.ts +20 -0
- package/src/hooks/use-sticky.ts +47 -0
- 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
|
+
}
|