@bearmenu/ui 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ERMIA7NP.js → chunk-7PPCH42H.js} +3 -3
- package/dist/{chunk-HLQ2IRE7.js → chunk-BAZFVSSG.js} +3 -3
- package/dist/chunk-D5QHEHXJ.js +26 -0
- package/dist/{chunk-UISA4CIG.js → chunk-N2KNBPAQ.js} +2 -2
- package/dist/{chunk-7H54PTVH.js → chunk-PSR7N36A.js} +38 -10
- package/dist/chunk-RIDXFY6M.js +38 -0
- package/dist/chunk-UUJVXN2O.js +82 -0
- package/dist/chunk-WKMLZDUT.js +122 -0
- package/dist/chunk-XK2ZDQVE.js +122 -0
- package/dist/{chunk-4KOMHDZD.js → chunk-Y5D5LP4A.js} +18 -3
- package/dist/chunk-YJ6OBKZJ.js +32 -0
- package/dist/{chunk-ESO2EM3L.js → chunk-ZBBUKXM5.js} +2 -2
- package/dist/chunk-ZFNOCP6W.js +106 -0
- package/dist/components/accordion.js +11 -5
- package/dist/components/alert-dialog.js +5 -3
- package/dist/components/alert.d.ts +4 -1
- package/dist/components/alert.js +25 -9
- package/dist/components/avatar.d.ts +17 -2
- package/dist/components/avatar.js +91 -12
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/breadcrumb.js +5 -2
- package/dist/components/button-group.d.ts +14 -0
- package/dist/components/button-group.js +39 -0
- package/dist/components/button.d.ts +3 -1
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.js +3 -3
- package/dist/components/card.d.ts +13 -5
- package/dist/components/card.js +2 -2
- package/dist/components/carousel.d.ts +20 -3
- package/dist/components/carousel.js +72 -12
- package/dist/components/checkbox.js +1 -1
- package/dist/components/circular-progress.d.ts +16 -0
- package/dist/components/circular-progress.js +105 -0
- package/dist/components/combobox.d.ts +27 -0
- package/dist/components/combobox.js +102 -0
- package/dist/components/command.d.ts +16 -3
- package/dist/components/command.js +2 -2
- package/dist/components/currency-input.d.ts +1 -0
- package/dist/components/currency-input.js +73 -52
- package/dist/components/date-input.d.ts +10 -0
- package/dist/components/date-input.js +40 -0
- package/dist/components/description-list.d.ts +15 -0
- package/dist/components/description-list.js +67 -0
- package/dist/components/dialog.d.ts +13 -2
- package/dist/components/dialog.js +1 -1
- package/dist/components/drawer.d.ts +5 -2
- package/dist/components/drawer.js +47 -9
- package/dist/components/dropdown-menu.d.ts +7 -2
- package/dist/components/dropdown-menu.js +22 -6
- package/dist/components/empty-state.js +2 -2
- package/dist/components/field.d.ts +22 -0
- package/dist/components/field.js +130 -0
- package/dist/components/form.d.ts +2 -1
- package/dist/components/form.js +11 -1
- package/dist/components/haptic-button.js +3 -3
- package/dist/components/input-group.d.ts +18 -0
- package/dist/components/input-group.js +92 -0
- package/dist/components/input.d.ts +8 -2
- package/dist/components/input.js +1 -1
- package/dist/components/item.d.ts +23 -0
- package/dist/components/item.js +120 -0
- package/dist/components/kbd.d.ts +13 -0
- package/dist/components/kbd.js +49 -0
- package/dist/components/lazy-markdown.d.ts +3 -1
- package/dist/components/lazy-markdown.js +7 -7
- package/dist/components/link-button.js +1 -1
- package/dist/components/multi-select-combobox.js +92 -83
- package/dist/components/multi-select-menu.d.ts +0 -4
- package/dist/components/multi-select-menu.js +28 -17
- package/dist/components/native-select.d.ts +12 -0
- package/dist/components/native-select.js +49 -0
- package/dist/components/pagination.d.ts +5 -2
- package/dist/components/pagination.js +66 -7
- package/dist/components/place-about-tab.js +4 -4
- package/dist/components/place-card.js +1 -1
- package/dist/components/place-details-mobile.js +4 -4
- package/dist/components/popover.d.ts +7 -1
- package/dist/components/popover.js +1 -1
- package/dist/components/progress.d.ts +5 -1
- package/dist/components/progress.js +17 -3
- package/dist/components/radio-group.js +12 -3
- package/dist/components/rating.d.ts +2 -0
- package/dist/components/rating.js +88 -70
- package/dist/components/scroll-fade.d.ts +15 -0
- package/dist/components/scroll-fade.js +116 -0
- package/dist/components/search-bar.d.ts +1 -2
- package/dist/components/search-bar.js +37 -60
- package/dist/components/searchable-select.js +76 -72
- package/dist/components/select.d.ts +5 -1
- package/dist/components/select.js +30 -14
- package/dist/components/selection-bar.d.ts +11 -0
- package/dist/components/selection-bar.js +65 -0
- package/dist/components/separator.js +3 -2
- package/dist/components/sheet.d.ts +1 -1
- package/dist/components/sheet.js +1 -1
- package/dist/components/skeleton-card.js +1 -1
- package/dist/components/skeleton.js +1 -1
- package/dist/components/slider.js +21 -4
- package/dist/components/sliding-panels.js +9 -6
- package/dist/components/spinner.d.ts +17 -0
- package/dist/components/spinner.js +50 -0
- package/dist/components/stepper.d.ts +18 -0
- package/dist/components/stepper.js +199 -0
- package/dist/components/switch.js +4 -3
- package/dist/components/table.d.ts +23 -5
- package/dist/components/table.js +121 -35
- package/dist/components/tabs.d.ts +3 -1
- package/dist/components/tabs.js +37 -9
- package/dist/components/tag-group.d.ts +9 -0
- package/dist/components/tag-group.js +26 -0
- package/dist/components/tag.d.ts +17 -0
- package/dist/components/tag.js +61 -0
- package/dist/components/text.d.ts +1 -1
- package/dist/components/text.js +1 -1
- package/dist/components/textarea.js +2 -1
- package/dist/components/toast.js +1 -1
- package/dist/components/toaster.js +1 -1
- package/dist/components/toggle-group.js +1 -1
- package/dist/components/toggle.js +1 -1
- package/dist/components/tooltip.d.ts +3 -2
- package/dist/components/tooltip.js +16 -2
- package/package.json +45 -26
- package/src/components/accordion.tsx +11 -6
- package/src/components/alert-dialog.tsx +4 -2
- package/src/components/alert.tsx +20 -8
- package/src/components/avatar.tsx +106 -13
- package/src/components/badge.tsx +12 -11
- package/src/components/breadcrumb.tsx +5 -2
- package/src/components/button-group.stories.tsx +52 -0
- package/src/components/button-group.tsx +40 -0
- package/src/components/button.test.tsx +174 -0
- package/src/components/button.tsx +42 -19
- package/src/components/calendar.tsx +2 -2
- package/src/components/card.tsx +55 -13
- package/src/components/carousel.tsx +85 -13
- package/src/components/checkbox.test.tsx +58 -0
- package/src/components/checkbox.tsx +8 -3
- package/src/components/circular-progress.stories.tsx +39 -0
- package/src/components/circular-progress.tsx +102 -0
- package/src/components/collapsible.stories.tsx +7 -3
- package/src/components/combobox.stories.tsx +82 -0
- package/src/components/combobox.test.tsx +118 -0
- package/src/components/combobox.tsx +136 -0
- package/src/components/command.test.tsx +91 -0
- package/src/components/command.tsx +38 -15
- package/src/components/currency-input.test.tsx +63 -0
- package/src/components/currency-input.tsx +84 -66
- package/src/components/date-input.stories.tsx +31 -0
- package/src/components/date-input.tsx +43 -0
- package/src/components/description-list.stories.tsx +62 -0
- package/src/components/description-list.tsx +68 -0
- package/src/components/dialog.test.tsx +84 -0
- package/src/components/dialog.tsx +64 -24
- package/src/components/drawer.tsx +72 -23
- package/src/components/dropdown-menu.test.tsx +180 -0
- package/src/components/dropdown-menu.tsx +31 -6
- package/src/components/field.stories.tsx +36 -0
- package/src/components/field.test.tsx +157 -0
- package/src/components/field.tsx +146 -0
- package/src/components/form.stories.tsx +3 -2
- package/src/components/form.test.tsx +95 -0
- package/src/components/form.tsx +9 -0
- package/src/components/haptic-button.tsx +1 -1
- package/src/components/input-group.stories.tsx +61 -0
- package/src/components/input-group.tsx +99 -0
- package/src/components/input.test.tsx +103 -0
- package/src/components/input.tsx +24 -7
- package/src/components/item.stories.tsx +52 -0
- package/src/components/item.tsx +115 -0
- package/src/components/kbd.stories.tsx +33 -0
- package/src/components/kbd.tsx +46 -0
- package/src/components/lazy-markdown.tsx +10 -7
- package/src/components/multi-select-combobox.test.tsx +101 -0
- package/src/components/multi-select-combobox.tsx +85 -82
- package/src/components/multi-select-menu.tsx +49 -41
- package/src/components/native-select.stories.tsx +52 -0
- package/src/components/native-select.tsx +45 -0
- package/src/components/pagination.tsx +64 -5
- package/src/components/popover.tsx +66 -3
- package/src/components/progress.tsx +27 -4
- package/src/components/radio-group.test.tsx +64 -0
- package/src/components/radio-group.tsx +11 -3
- package/src/components/rating.test.tsx +112 -0
- package/src/components/rating.tsx +97 -82
- package/src/components/scroll-fade.stories.tsx +98 -0
- package/src/components/scroll-fade.tsx +127 -0
- package/src/components/search-bar.stories.tsx +1 -32
- package/src/components/search-bar.tsx +22 -59
- package/src/components/searchable-select.tsx +69 -73
- package/src/components/select.tsx +34 -19
- package/src/components/selection-bar.stories.tsx +37 -0
- package/src/components/selection-bar.tsx +65 -0
- package/src/components/separator.tsx +5 -2
- package/src/components/sheet.tsx +1 -1
- package/src/components/skeleton.tsx +2 -2
- package/src/components/slider.test.tsx +45 -0
- package/src/components/slider.tsx +31 -12
- package/src/components/sliding-panels.tsx +8 -5
- package/src/components/spinner.stories.tsx +35 -0
- package/src/components/spinner.tsx +50 -0
- package/src/components/stepper.stories.tsx +71 -0
- package/src/components/stepper.tsx +220 -0
- package/src/components/switch.test.tsx +52 -0
- package/src/components/switch.tsx +4 -3
- package/src/components/table.tsx +140 -35
- package/src/components/tabs.test.tsx +107 -0
- package/src/components/tabs.tsx +70 -23
- package/src/components/tag-group.stories.tsx +39 -0
- package/src/components/tag-group.tsx +28 -0
- package/src/components/tag.stories.tsx +50 -0
- package/src/components/tag.tsx +63 -0
- package/src/components/text.test.tsx +1 -1
- package/src/components/text.tsx +19 -3
- package/src/components/textarea.test.tsx +48 -0
- package/src/components/textarea.tsx +2 -1
- package/src/components/toast.tsx +4 -3
- package/src/components/toggle.tsx +2 -2
- package/src/components/tooltip.tsx +12 -2
- package/src/globals.css +126 -90
- package/dist/chunk-2BMJRXQX.js +0 -80
- package/dist/chunk-4AVS6CV4.js +0 -89
- package/dist/chunk-4UO73SKC.js +0 -26
- package/dist/chunk-CW24PLDW.js +0 -24
- package/dist/chunk-IQASCH2V.js +0 -25
- package/dist/chunk-NKHH23YK.js +0 -31
- package/dist/chunk-QJ54Z7TC.js +0 -83
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { NativeSelect } from "./native-select";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Components/NativeSelect",
|
|
6
|
+
component: NativeSelect,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
} satisfies Meta<typeof NativeSelect>;
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof meta>;
|
|
12
|
+
|
|
13
|
+
export const Default: Story = {
|
|
14
|
+
render: () => (
|
|
15
|
+
<NativeSelect className="w-64" defaultValue="">
|
|
16
|
+
<option value="" disabled>
|
|
17
|
+
Pick a fruit…
|
|
18
|
+
</option>
|
|
19
|
+
<option value="apple">Apple</option>
|
|
20
|
+
<option value="banana">Banana</option>
|
|
21
|
+
<option value="cherry">Cherry</option>
|
|
22
|
+
</NativeSelect>
|
|
23
|
+
),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Sizes: Story = {
|
|
27
|
+
render: () => (
|
|
28
|
+
<div className="flex w-64 flex-col gap-3">
|
|
29
|
+
<NativeSelect size="sm" defaultValue="small">
|
|
30
|
+
<option value="small">Small</option>
|
|
31
|
+
</NativeSelect>
|
|
32
|
+
<NativeSelect size="default" defaultValue="default">
|
|
33
|
+
<option value="default">Default</option>
|
|
34
|
+
</NativeSelect>
|
|
35
|
+
<NativeSelect size="lg" defaultValue="large">
|
|
36
|
+
<option value="large">Large</option>
|
|
37
|
+
</NativeSelect>
|
|
38
|
+
</div>
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const Invalid: Story = {
|
|
43
|
+
render: () => (
|
|
44
|
+
<NativeSelect className="w-64" aria-invalid defaultValue="">
|
|
45
|
+
<option value="" disabled>
|
|
46
|
+
Required field…
|
|
47
|
+
</option>
|
|
48
|
+
<option value="a">Option A</option>
|
|
49
|
+
<option value="b">Option B</option>
|
|
50
|
+
</NativeSelect>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ChevronDown } from "lucide-react";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
const nativeSelectVariants = cva(
|
|
7
|
+
"w-full appearance-none rounded-xl border border-border bg-background pr-9 text-sm text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/40",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
size: {
|
|
11
|
+
sm: "h-9 pl-3",
|
|
12
|
+
default: "h-11 pl-4",
|
|
13
|
+
lg: "h-12 pl-5",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
size: "default",
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
type NativeSelectProps = Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "size"> &
|
|
23
|
+
VariantProps<typeof nativeSelectVariants>;
|
|
24
|
+
|
|
25
|
+
const NativeSelect = React.forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
26
|
+
({ className, size, children, ...props }, ref) => (
|
|
27
|
+
<div className="relative inline-flex w-full">
|
|
28
|
+
<select
|
|
29
|
+
ref={ref}
|
|
30
|
+
data-slot="native-select"
|
|
31
|
+
className={cn(nativeSelectVariants({ size }), className)}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
{children}
|
|
35
|
+
</select>
|
|
36
|
+
<ChevronDown
|
|
37
|
+
aria-hidden
|
|
38
|
+
className="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
NativeSelect.displayName = "NativeSelect";
|
|
44
|
+
|
|
45
|
+
export { NativeSelect, nativeSelectVariants };
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import {
|
|
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({
|
|
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 ? "
|
|
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
|
|
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({
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
42
|
+
data-slot="progress-indicator"
|
|
43
|
+
className={cn("h-full w-full flex-1 transition-transform duration-500 ease-out", toneClasses[tone])}
|
|
21
44
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
22
45
|
/>
|
|
23
46
|
</ProgressPrimitive.Root>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { RadioGroup, RadioGroupItem } from "./radio-group";
|
|
5
|
+
|
|
6
|
+
function Setup({
|
|
7
|
+
value,
|
|
8
|
+
onValueChange,
|
|
9
|
+
}: {
|
|
10
|
+
value?: string;
|
|
11
|
+
onValueChange?: (v: string) => void;
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<RadioGroup value={value} onValueChange={onValueChange} aria-label="favorite">
|
|
15
|
+
<RadioGroupItem value="a" aria-label="A" />
|
|
16
|
+
<RadioGroupItem value="b" aria-label="B" />
|
|
17
|
+
<RadioGroupItem value="c" aria-label="C" />
|
|
18
|
+
</RadioGroup>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("RadioGroup", () => {
|
|
23
|
+
it("renders a radiogroup with items", () => {
|
|
24
|
+
render(<Setup />);
|
|
25
|
+
expect(screen.getByRole("radiogroup", { name: /favorite/i })).toBeDefined();
|
|
26
|
+
expect(screen.getAllByRole("radio")).toHaveLength(3);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("marks the matching item as checked", () => {
|
|
30
|
+
render(<Setup value="b" onValueChange={() => {}} />);
|
|
31
|
+
expect(screen.getByRole("radio", { name: "B" })).toHaveAttribute("data-state", "checked");
|
|
32
|
+
expect(screen.getByRole("radio", { name: "A" })).toHaveAttribute("data-state", "unchecked");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("fires onValueChange when clicking an item", async () => {
|
|
36
|
+
const user = userEvent.setup();
|
|
37
|
+
const onValueChange = vi.fn();
|
|
38
|
+
render(<Setup onValueChange={onValueChange} />);
|
|
39
|
+
await user.click(screen.getByRole("radio", { name: "B" }));
|
|
40
|
+
expect(onValueChange).toHaveBeenCalledWith("b");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("disabled items reject clicks", async () => {
|
|
44
|
+
const user = userEvent.setup();
|
|
45
|
+
const onValueChange = vi.fn();
|
|
46
|
+
render(
|
|
47
|
+
<RadioGroup onValueChange={onValueChange} aria-label="x">
|
|
48
|
+
<RadioGroupItem value="a" aria-label="A" disabled />
|
|
49
|
+
<RadioGroupItem value="b" aria-label="B" />
|
|
50
|
+
</RadioGroup>
|
|
51
|
+
);
|
|
52
|
+
await user.click(screen.getByRole("radio", { name: "A" }));
|
|
53
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("respects aria-invalid on items", () => {
|
|
57
|
+
render(
|
|
58
|
+
<RadioGroup aria-label="x">
|
|
59
|
+
<RadioGroupItem value="a" aria-label="A" aria-invalid />
|
|
60
|
+
</RadioGroup>
|
|
61
|
+
);
|
|
62
|
+
expect(screen.getByRole("radio", { name: "A" })).toHaveAttribute("aria-invalid", "true");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -9,7 +9,14 @@ const RadioGroup = React.forwardRef<
|
|
|
9
9
|
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
|
10
10
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
|
11
11
|
>(({ className, ...props }, ref) => {
|
|
12
|
-
return
|
|
12
|
+
return (
|
|
13
|
+
<RadioGroupPrimitive.Root
|
|
14
|
+
data-slot="radio-group"
|
|
15
|
+
className={cn("grid gap-2", className)}
|
|
16
|
+
{...props}
|
|
17
|
+
ref={ref}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
13
20
|
});
|
|
14
21
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
|
15
22
|
|
|
@@ -20,14 +27,15 @@ const RadioGroupItem = React.forwardRef<
|
|
|
20
27
|
return (
|
|
21
28
|
<RadioGroupPrimitive.Item
|
|
22
29
|
ref={ref}
|
|
30
|
+
data-slot="radio-group-item"
|
|
23
31
|
className={cn(
|
|
24
|
-
"aspect-square
|
|
32
|
+
"aspect-square size-5 rounded-full border border-border bg-background text-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary hover:border-primary/50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/40",
|
|
25
33
|
className
|
|
26
34
|
)}
|
|
27
35
|
{...props}
|
|
28
36
|
>
|
|
29
37
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
30
|
-
<Circle className="
|
|
38
|
+
<Circle className="size-2.5 fill-current text-primary animate-in zoom-in-50 duration-150" />
|
|
31
39
|
</RadioGroupPrimitive.Indicator>
|
|
32
40
|
</RadioGroupPrimitive.Item>
|
|
33
41
|
);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Rating } from "./rating";
|
|
5
|
+
|
|
6
|
+
describe("Rating (readonly)", () => {
|
|
7
|
+
it("renders with role=img and labeled aria description", () => {
|
|
8
|
+
render(<Rating value={3.5} reviewCount={42} />);
|
|
9
|
+
const el = screen.getByRole("img");
|
|
10
|
+
expect(el.getAttribute("aria-label")).toBe("Rating: 3.5 out of 5");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("uses custom label in aria-label", () => {
|
|
14
|
+
render(<Rating value={2} label="Tasty" />);
|
|
15
|
+
expect(screen.getByRole("img").getAttribute("aria-label")).toBe("Tasty: 2 out of 5");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("shows review count when provided and showCount is true", () => {
|
|
19
|
+
render(<Rating value={3} reviewCount={123} />);
|
|
20
|
+
expect(screen.getByText("(123)")).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("hides count when showCount=false", () => {
|
|
24
|
+
render(<Rating value={3} reviewCount={123} showCount={false} />);
|
|
25
|
+
expect(screen.queryByText("(123)")).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("does not render any radio buttons in readonly mode", () => {
|
|
29
|
+
render(<Rating value={3} />);
|
|
30
|
+
expect(screen.queryByRole("radio")).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("Rating (interactive)", () => {
|
|
35
|
+
it("renders as radiogroup with `max` radios", () => {
|
|
36
|
+
render(<Rating value={0} readonly={false} onRatingChange={() => {}} />);
|
|
37
|
+
expect(screen.getByRole("radiogroup")).toBeInTheDocument();
|
|
38
|
+
expect(screen.getAllByRole("radio")).toHaveLength(5);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("marks the correct radio as checked", () => {
|
|
42
|
+
render(<Rating value={3} readonly={false} onRatingChange={() => {}} />);
|
|
43
|
+
const radios = screen.getAllByRole("radio");
|
|
44
|
+
expect(radios[2].getAttribute("aria-checked")).toBe("true");
|
|
45
|
+
expect(radios[0].getAttribute("aria-checked")).toBe("false");
|
|
46
|
+
expect(radios[4].getAttribute("aria-checked")).toBe("false");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("fires onRatingChange when a star is clicked", async () => {
|
|
50
|
+
const user = userEvent.setup();
|
|
51
|
+
const onChange = vi.fn();
|
|
52
|
+
render(<Rating value={0} readonly={false} onRatingChange={onChange} />);
|
|
53
|
+
await user.click(screen.getByRole("radio", { name: "4 stars" }));
|
|
54
|
+
expect(onChange).toHaveBeenCalledWith(4);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("ArrowRight increments and ArrowLeft decrements", async () => {
|
|
58
|
+
const user = userEvent.setup();
|
|
59
|
+
const onChange = vi.fn();
|
|
60
|
+
render(<Rating value={3} readonly={false} onRatingChange={onChange} />);
|
|
61
|
+
screen.getAllByRole("radio")[0].focus();
|
|
62
|
+
await user.keyboard("{ArrowRight}");
|
|
63
|
+
expect(onChange).toHaveBeenLastCalledWith(4);
|
|
64
|
+
await user.keyboard("{ArrowLeft}");
|
|
65
|
+
expect(onChange).toHaveBeenLastCalledWith(2);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("ArrowUp increments and ArrowDown decrements", async () => {
|
|
69
|
+
const user = userEvent.setup();
|
|
70
|
+
const onChange = vi.fn();
|
|
71
|
+
render(<Rating value={2} readonly={false} onRatingChange={onChange} />);
|
|
72
|
+
screen.getAllByRole("radio")[0].focus();
|
|
73
|
+
await user.keyboard("{ArrowUp}");
|
|
74
|
+
expect(onChange).toHaveBeenLastCalledWith(3);
|
|
75
|
+
await user.keyboard("{ArrowDown}");
|
|
76
|
+
expect(onChange).toHaveBeenLastCalledWith(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("Home jumps to 0 and End jumps to max", async () => {
|
|
80
|
+
const user = userEvent.setup();
|
|
81
|
+
const onChange = vi.fn();
|
|
82
|
+
render(<Rating value={3} readonly={false} onRatingChange={onChange} />);
|
|
83
|
+
screen.getAllByRole("radio")[0].focus();
|
|
84
|
+
await user.keyboard("{Home}");
|
|
85
|
+
expect(onChange).toHaveBeenLastCalledWith(0);
|
|
86
|
+
await user.keyboard("{End}");
|
|
87
|
+
expect(onChange).toHaveBeenLastCalledWith(5);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("clamps at max when ArrowRight pressed at max", async () => {
|
|
91
|
+
const user = userEvent.setup();
|
|
92
|
+
const onChange = vi.fn();
|
|
93
|
+
render(<Rating value={5} readonly={false} onRatingChange={onChange} />);
|
|
94
|
+
screen.getAllByRole("radio")[0].focus();
|
|
95
|
+
await user.keyboard("{ArrowRight}");
|
|
96
|
+
expect(onChange).toHaveBeenLastCalledWith(5);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("clamps at 0 when ArrowLeft pressed at 0", async () => {
|
|
100
|
+
const user = userEvent.setup();
|
|
101
|
+
const onChange = vi.fn();
|
|
102
|
+
render(<Rating value={0} readonly={false} onRatingChange={onChange} />);
|
|
103
|
+
screen.getAllByRole("radio")[0].focus();
|
|
104
|
+
await user.keyboard("{ArrowLeft}");
|
|
105
|
+
expect(onChange).toHaveBeenLastCalledWith(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("respects max prop for radio count", () => {
|
|
109
|
+
render(<Rating value={2} max={10} readonly={false} onRatingChange={() => {}} />);
|
|
110
|
+
expect(screen.getAllByRole("radio")).toHaveLength(10);
|
|
111
|
+
});
|
|
112
|
+
});
|