@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,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
const itemVariants = cva(
|
|
9
|
+
"flex w-full items-center gap-3 rounded-xl 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",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default: "bg-transparent",
|
|
14
|
+
muted: "bg-muted",
|
|
15
|
+
outline: "border border-border bg-transparent",
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
sm: "px-3 py-2",
|
|
19
|
+
default: "px-4 py-3",
|
|
20
|
+
lg: "px-5 py-4",
|
|
21
|
+
},
|
|
22
|
+
interactive: {
|
|
23
|
+
true: "cursor-pointer hover:bg-accent hover:text-accent-foreground active:scale-[0.99]",
|
|
24
|
+
false: "",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: {
|
|
28
|
+
variant: "default",
|
|
29
|
+
size: "default",
|
|
30
|
+
interactive: false,
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
type ItemProps = React.HTMLAttributes<HTMLDivElement> &
|
|
36
|
+
VariantProps<typeof itemVariants> & {
|
|
37
|
+
asChild?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const Item = React.forwardRef<HTMLDivElement, ItemProps>(
|
|
41
|
+
({ className, variant, size, interactive, asChild, ...props }, ref) => {
|
|
42
|
+
const Comp = asChild ? Slot : "div";
|
|
43
|
+
return (
|
|
44
|
+
<Comp
|
|
45
|
+
ref={ref}
|
|
46
|
+
data-slot="item"
|
|
47
|
+
className={cn(itemVariants({ variant, size, interactive }), className)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
Item.displayName = "Item";
|
|
54
|
+
|
|
55
|
+
const ItemMedia = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
56
|
+
({ className, ...props }, ref) => (
|
|
57
|
+
<div
|
|
58
|
+
ref={ref}
|
|
59
|
+
data-slot="item-media"
|
|
60
|
+
className={cn("inline-flex shrink-0 items-center justify-center text-muted-foreground", className)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
ItemMedia.displayName = "ItemMedia";
|
|
66
|
+
|
|
67
|
+
const ItemContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
68
|
+
({ className, ...props }, ref) => (
|
|
69
|
+
<div
|
|
70
|
+
ref={ref}
|
|
71
|
+
data-slot="item-content"
|
|
72
|
+
className={cn("flex min-w-0 flex-1 flex-col gap-0.5", className)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
ItemContent.displayName = "ItemContent";
|
|
78
|
+
|
|
79
|
+
const ItemTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
80
|
+
({ className, ...props }, ref) => (
|
|
81
|
+
<div
|
|
82
|
+
ref={ref}
|
|
83
|
+
data-slot="item-title"
|
|
84
|
+
className={cn("truncate text-sm font-medium text-foreground", className)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
ItemTitle.displayName = "ItemTitle";
|
|
90
|
+
|
|
91
|
+
const ItemDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
92
|
+
({ className, ...props }, ref) => (
|
|
93
|
+
<div
|
|
94
|
+
ref={ref}
|
|
95
|
+
data-slot="item-description"
|
|
96
|
+
className={cn("truncate text-xs text-muted-foreground", className)}
|
|
97
|
+
{...props}
|
|
98
|
+
/>
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
ItemDescription.displayName = "ItemDescription";
|
|
102
|
+
|
|
103
|
+
const ItemActions = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
104
|
+
({ className, ...props }, ref) => (
|
|
105
|
+
<div
|
|
106
|
+
ref={ref}
|
|
107
|
+
data-slot="item-actions"
|
|
108
|
+
className={cn("ml-auto inline-flex shrink-0 items-center gap-1", className)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
);
|
|
113
|
+
ItemActions.displayName = "ItemActions";
|
|
114
|
+
|
|
115
|
+
export { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, itemVariants };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Kbd, KbdGroup } from "./kbd";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Components/Kbd",
|
|
6
|
+
component: Kbd,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
} satisfies Meta<typeof Kbd>;
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof meta>;
|
|
12
|
+
|
|
13
|
+
export const Default: Story = { args: { children: "⌘" } };
|
|
14
|
+
|
|
15
|
+
export const Sizes: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<div className="flex items-center gap-2">
|
|
18
|
+
<Kbd size="sm">⌘</Kbd>
|
|
19
|
+
<Kbd size="default">⌘</Kbd>
|
|
20
|
+
<Kbd size="lg">⌘</Kbd>
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const Combination: Story = {
|
|
26
|
+
render: () => (
|
|
27
|
+
<KbdGroup>
|
|
28
|
+
<Kbd>⌘</Kbd>
|
|
29
|
+
<span className="text-muted-foreground">+</span>
|
|
30
|
+
<Kbd>K</Kbd>
|
|
31
|
+
</KbdGroup>
|
|
32
|
+
),
|
|
33
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
const kbdVariants = cva(
|
|
6
|
+
"inline-flex select-none items-center justify-center gap-1 rounded-md border border-border bg-muted font-mono font-medium text-muted-foreground shadow-[0_1px_0_0_var(--border)]",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
size: {
|
|
10
|
+
sm: "h-5 min-w-5 px-1 text-[10px]",
|
|
11
|
+
default: "h-6 min-w-6 px-1.5 text-xs",
|
|
12
|
+
lg: "h-7 min-w-7 px-2 text-sm",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
size: "default",
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const Kbd = React.forwardRef<
|
|
22
|
+
HTMLElement,
|
|
23
|
+
React.HTMLAttributes<HTMLElement> & VariantProps<typeof kbdVariants>
|
|
24
|
+
>(({ className, size, ...props }, ref) => (
|
|
25
|
+
<kbd
|
|
26
|
+
ref={ref}
|
|
27
|
+
data-slot="kbd"
|
|
28
|
+
className={cn(kbdVariants({ size }), className)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
));
|
|
32
|
+
Kbd.displayName = "Kbd";
|
|
33
|
+
|
|
34
|
+
const KbdGroup = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
|
|
35
|
+
({ className, ...props }, ref) => (
|
|
36
|
+
<span
|
|
37
|
+
ref={ref}
|
|
38
|
+
data-slot="kbd-group"
|
|
39
|
+
className={cn("inline-flex items-center gap-1", className)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
KbdGroup.displayName = "KbdGroup";
|
|
45
|
+
|
|
46
|
+
export { Kbd, KbdGroup, kbdVariants };
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import * as React from "react";
|
|
4
4
|
import type { Components } from "react-markdown";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
() => import("./markdown-renderer").then((m) => m.MarkdownRenderer),
|
|
9
|
-
{ ssr: false }
|
|
6
|
+
const MarkdownRendererLazy = React.lazy(() =>
|
|
7
|
+
import("./markdown-renderer").then((m) => ({ default: m.MarkdownRenderer }))
|
|
10
8
|
);
|
|
11
9
|
|
|
12
10
|
type LazyMarkdownProps = {
|
|
13
11
|
content: string;
|
|
14
12
|
className?: string;
|
|
15
13
|
components?: Components;
|
|
14
|
+
fallback?: React.ReactNode;
|
|
16
15
|
};
|
|
17
16
|
|
|
18
|
-
export function LazyMarkdown(props: LazyMarkdownProps) {
|
|
19
|
-
return
|
|
17
|
+
export function LazyMarkdown({ fallback = null, ...props }: LazyMarkdownProps) {
|
|
18
|
+
return (
|
|
19
|
+
<React.Suspense fallback={fallback}>
|
|
20
|
+
<MarkdownRendererLazy {...props} />
|
|
21
|
+
</React.Suspense>
|
|
22
|
+
);
|
|
20
23
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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 * as React from "react";
|
|
5
|
+
import { MultiSelectCombobox, type MultiSelectOption } from "./multi-select-combobox";
|
|
6
|
+
|
|
7
|
+
const options: MultiSelectOption[] = [
|
|
8
|
+
{ value: "apple", label: "Apple" },
|
|
9
|
+
{ value: "banana", label: "Banana" },
|
|
10
|
+
{ value: "cherry", label: "Cherry" },
|
|
11
|
+
{ value: "date", label: "Date" },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function Wrap({
|
|
15
|
+
initial = [],
|
|
16
|
+
maxDisplay,
|
|
17
|
+
onChange,
|
|
18
|
+
}: {
|
|
19
|
+
initial?: string[];
|
|
20
|
+
maxDisplay?: number;
|
|
21
|
+
onChange?: (v: string[]) => void;
|
|
22
|
+
}) {
|
|
23
|
+
const [selected, setSelected] = React.useState<string[]>(initial);
|
|
24
|
+
return (
|
|
25
|
+
<MultiSelectCombobox
|
|
26
|
+
options={options}
|
|
27
|
+
selected={selected}
|
|
28
|
+
maxDisplay={maxDisplay}
|
|
29
|
+
onSelectionChange={(v) => {
|
|
30
|
+
setSelected(v);
|
|
31
|
+
onChange?.(v);
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("MultiSelectCombobox", () => {
|
|
38
|
+
it("renders placeholder when nothing is selected", () => {
|
|
39
|
+
render(<Wrap />);
|
|
40
|
+
expect(screen.getByRole("combobox")).toHaveTextContent("Select items...");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("renders count when items are selected", () => {
|
|
44
|
+
render(<Wrap initial={["apple", "banana"]} />);
|
|
45
|
+
expect(screen.getByRole("combobox")).toHaveTextContent("2 selected");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("opens on trigger click and exposes aria-expanded=true", async () => {
|
|
49
|
+
const user = userEvent.setup();
|
|
50
|
+
render(<Wrap />);
|
|
51
|
+
const trigger = screen.getByRole("combobox");
|
|
52
|
+
await user.click(trigger);
|
|
53
|
+
expect(trigger.getAttribute("aria-expanded")).toBe("true");
|
|
54
|
+
expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("adds a value when an option is selected", async () => {
|
|
58
|
+
const user = userEvent.setup();
|
|
59
|
+
const onChange = vi.fn();
|
|
60
|
+
render(<Wrap onChange={onChange} />);
|
|
61
|
+
await user.click(screen.getByRole("combobox"));
|
|
62
|
+
await user.click(screen.getByRole("option", { name: /apple/i }));
|
|
63
|
+
expect(onChange).toHaveBeenCalledWith(["apple"]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("removes a value when an already-selected option is clicked", async () => {
|
|
67
|
+
const user = userEvent.setup();
|
|
68
|
+
const onChange = vi.fn();
|
|
69
|
+
render(<Wrap initial={["apple", "banana"]} onChange={onChange} />);
|
|
70
|
+
await user.click(screen.getByRole("combobox"));
|
|
71
|
+
await user.click(screen.getByRole("option", { name: /banana/i }));
|
|
72
|
+
expect(onChange).toHaveBeenCalledWith(["apple"]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("renders one removal button per visible selection", () => {
|
|
76
|
+
render(<Wrap initial={["apple", "banana"]} />);
|
|
77
|
+
expect(screen.getByRole("button", { name: "Remove Apple" })).toBeInTheDocument();
|
|
78
|
+
expect(screen.getByRole("button", { name: "Remove Banana" })).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("removes a value when its badge X is clicked", async () => {
|
|
82
|
+
const user = userEvent.setup();
|
|
83
|
+
const onChange = vi.fn();
|
|
84
|
+
render(<Wrap initial={["apple", "banana"]} onChange={onChange} />);
|
|
85
|
+
await user.click(screen.getByRole("button", { name: "Remove Apple" }));
|
|
86
|
+
expect(onChange).toHaveBeenCalledWith(["banana"]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("respects maxDisplay and shows a +N more badge", () => {
|
|
90
|
+
render(<Wrap initial={["apple", "banana", "cherry", "date"]} maxDisplay={2} />);
|
|
91
|
+
expect(screen.getByRole("button", { name: "Remove Apple" })).toBeInTheDocument();
|
|
92
|
+
expect(screen.getByRole("button", { name: "Remove Banana" })).toBeInTheDocument();
|
|
93
|
+
expect(screen.queryByRole("button", { name: "Remove Cherry" })).toBeNull();
|
|
94
|
+
expect(screen.getByText("+2 more")).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("does not render the +N more badge when all fit", () => {
|
|
98
|
+
render(<Wrap initial={["apple"]} maxDisplay={3} />);
|
|
99
|
+
expect(screen.queryByText(/\+\d+ more/)).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { ChevronsUpDown, X } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/utils";
|
|
6
6
|
import { Button } from "./button";
|
|
7
7
|
import { Badge } from "./badge";
|
|
8
|
+
import { Checkbox } from "./checkbox";
|
|
8
9
|
import {
|
|
9
10
|
Command,
|
|
10
11
|
CommandEmpty,
|
|
@@ -50,6 +51,7 @@ export function MultiSelectCombobox({
|
|
|
50
51
|
shouldFilter = true,
|
|
51
52
|
}: MultiSelectComboboxProps) {
|
|
52
53
|
const [open, setOpen] = React.useState(false);
|
|
54
|
+
const listboxId = React.useId();
|
|
53
55
|
|
|
54
56
|
const handleSelect = (value: string) => {
|
|
55
57
|
const newSelected = selected.includes(value)
|
|
@@ -58,91 +60,92 @@ export function MultiSelectCombobox({
|
|
|
58
60
|
onSelectionChange(newSelected);
|
|
59
61
|
};
|
|
60
62
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
onSelectionChange(selected.filter((item) => item !== value));
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const selectedLabels = selected
|
|
67
|
-
.map((value) => options.find((opt) => opt.value === value)?.label || value)
|
|
68
|
-
.slice(0, maxDisplay);
|
|
69
|
-
|
|
70
|
-
const remainingCount = selected.length - maxDisplay;
|
|
63
|
+
const visible = selected.slice(0, maxDisplay);
|
|
64
|
+
const remaining = selected.length - maxDisplay;
|
|
71
65
|
|
|
72
66
|
return (
|
|
73
|
-
<
|
|
74
|
-
<
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
className
|
|
83
|
-
|
|
84
|
-
|
|
67
|
+
<div className={cn("flex flex-col gap-2", className)}>
|
|
68
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
69
|
+
<PopoverTrigger asChild>
|
|
70
|
+
<Button
|
|
71
|
+
variant="outline"
|
|
72
|
+
role="combobox"
|
|
73
|
+
aria-expanded={open}
|
|
74
|
+
aria-haspopup="listbox"
|
|
75
|
+
aria-controls={open ? listboxId : undefined}
|
|
76
|
+
className={cn(
|
|
77
|
+
"w-full justify-between font-normal",
|
|
78
|
+
selected.length === 0 && "text-muted-foreground"
|
|
79
|
+
)}
|
|
80
|
+
disabled={disabled}
|
|
81
|
+
>
|
|
82
|
+
<span className="truncate">
|
|
83
|
+
{selected.length === 0
|
|
84
|
+
? placeholder
|
|
85
|
+
: `${selected.length} selected`}
|
|
86
|
+
</span>
|
|
87
|
+
<ChevronsUpDown className="size-4 shrink-0 opacity-50" />
|
|
88
|
+
</Button>
|
|
89
|
+
</PopoverTrigger>
|
|
90
|
+
<PopoverContent
|
|
91
|
+
id={listboxId}
|
|
92
|
+
className="w-[var(--radix-popover-trigger-width)] p-0"
|
|
93
|
+
align="start"
|
|
85
94
|
>
|
|
86
|
-
<
|
|
87
|
-
{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
onKeyDown={(e) => {
|
|
100
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
101
|
-
e.preventDefault();
|
|
102
|
-
handleRemove(selected[i], e);
|
|
103
|
-
}
|
|
104
|
-
}}
|
|
95
|
+
<Command shouldFilter={shouldFilter}>
|
|
96
|
+
<CommandInput placeholder={searchPlaceholder} onValueChange={onSearchChange} />
|
|
97
|
+
<CommandList>
|
|
98
|
+
<CommandEmpty>{loading ? "Loading…" : emptyText}</CommandEmpty>
|
|
99
|
+
<CommandGroup>
|
|
100
|
+
{options.map((option) => {
|
|
101
|
+
const isSelected = selected.includes(option.value);
|
|
102
|
+
return (
|
|
103
|
+
<CommandItem
|
|
104
|
+
key={option.value}
|
|
105
|
+
value={option.value}
|
|
106
|
+
onSelect={() => handleSelect(option.value)}
|
|
107
|
+
className="gap-2"
|
|
105
108
|
>
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
</
|
|
120
|
-
</
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
109
|
+
<Checkbox
|
|
110
|
+
checked={isSelected}
|
|
111
|
+
tabIndex={-1}
|
|
112
|
+
aria-hidden
|
|
113
|
+
className="pointer-events-none"
|
|
114
|
+
/>
|
|
115
|
+
{option.label}
|
|
116
|
+
</CommandItem>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
</CommandGroup>
|
|
120
|
+
</CommandList>
|
|
121
|
+
</Command>
|
|
122
|
+
</PopoverContent>
|
|
123
|
+
</Popover>
|
|
124
|
+
{selected.length > 0 && (
|
|
125
|
+
<div className="flex flex-wrap gap-1">
|
|
126
|
+
{visible.map((value) => {
|
|
127
|
+
const label = options.find((opt) => opt.value === value)?.label || value;
|
|
128
|
+
return (
|
|
129
|
+
<Badge key={value} variant="secondary" className="gap-1 pr-1">
|
|
130
|
+
{label}
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
aria-label={`Remove ${label}`}
|
|
134
|
+
onClick={() =>
|
|
135
|
+
onSelectionChange(selected.filter((item) => item !== value))
|
|
136
|
+
}
|
|
137
|
+
className="rounded-full p-0.5 hover:bg-foreground/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors"
|
|
132
138
|
>
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
</Command>
|
|
145
|
-
</PopoverContent>
|
|
146
|
-
</Popover>
|
|
139
|
+
<X className="size-3" />
|
|
140
|
+
</button>
|
|
141
|
+
</Badge>
|
|
142
|
+
);
|
|
143
|
+
})}
|
|
144
|
+
{remaining > 0 && (
|
|
145
|
+
<Badge variant="secondary">+{remaining} more</Badge>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
147
150
|
);
|
|
148
151
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
4
5
|
import { cn } from "../lib/utils";
|
|
5
6
|
import { Checkbox } from "./checkbox";
|
|
6
|
-
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
7
7
|
|
|
8
8
|
export interface MultiSelectMenuOption {
|
|
9
9
|
value: string;
|
|
@@ -15,14 +15,10 @@ export interface MultiSelectMenuProps {
|
|
|
15
15
|
options: MultiSelectMenuOption[];
|
|
16
16
|
selected: string[];
|
|
17
17
|
onSelectedChange: (values: string[]) => void;
|
|
18
|
-
/** Trigger element rendered via Radix `asChild` — typically a Button. */
|
|
19
18
|
trigger: React.ReactNode;
|
|
20
|
-
/** Optional heading rendered above the option list. */
|
|
21
19
|
label?: string;
|
|
22
20
|
align?: "start" | "center" | "end";
|
|
23
|
-
/** Pixel width of the popover content. Defaults to 220. */
|
|
24
21
|
width?: number;
|
|
25
|
-
/** className for the popover content. */
|
|
26
22
|
contentClassName?: string;
|
|
27
23
|
emptyText?: string;
|
|
28
24
|
}
|
|
@@ -51,46 +47,58 @@ export function MultiSelectMenu({
|
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
return (
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
50
|
+
<DropdownMenuPrimitive.Root>
|
|
51
|
+
<DropdownMenuPrimitive.Trigger asChild>{trigger}</DropdownMenuPrimitive.Trigger>
|
|
52
|
+
<DropdownMenuPrimitive.Portal>
|
|
53
|
+
<DropdownMenuPrimitive.Content
|
|
54
|
+
align={align}
|
|
55
|
+
sideOffset={4}
|
|
56
|
+
style={{ width }}
|
|
57
|
+
className={cn(
|
|
58
|
+
"z-50 overflow-hidden rounded-xl border border-border bg-popover p-2 text-popover-foreground shadow-xl",
|
|
59
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
60
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
61
|
+
contentClassName
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
{label && (
|
|
65
|
+
<DropdownMenuPrimitive.Label className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
|
66
|
+
{label}
|
|
67
|
+
</DropdownMenuPrimitive.Label>
|
|
68
|
+
)}
|
|
67
69
|
{options.length === 0 ? (
|
|
68
|
-
<p className="
|
|
70
|
+
<p className="px-2 py-1.5 text-sm text-muted-foreground">{emptyText}</p>
|
|
69
71
|
) : (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm select-none",
|
|
77
|
-
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
78
|
-
option.disabled && "pointer-events-none opacity-50",
|
|
79
|
-
)}
|
|
80
|
-
>
|
|
81
|
-
<Checkbox
|
|
72
|
+
<DropdownMenuPrimitive.Group className="flex flex-col gap-0.5">
|
|
73
|
+
{options.map((option) => {
|
|
74
|
+
const checked = selectedSet.has(option.value);
|
|
75
|
+
return (
|
|
76
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
77
|
+
key={option.value}
|
|
82
78
|
checked={checked}
|
|
79
|
+
disabled={option.disabled ?? false}
|
|
80
|
+
onSelect={(e) => e.preventDefault()}
|
|
83
81
|
onCheckedChange={() => handleToggle(option.value)}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
className={cn(
|
|
83
|
+
"relative flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none",
|
|
84
|
+
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
|
|
85
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
|
86
|
+
)}
|
|
87
|
+
>
|
|
88
|
+
<Checkbox
|
|
89
|
+
checked={checked}
|
|
90
|
+
tabIndex={-1}
|
|
91
|
+
aria-hidden
|
|
92
|
+
className="pointer-events-none"
|
|
93
|
+
/>
|
|
94
|
+
<span className="flex-1">{option.label}</span>
|
|
95
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</DropdownMenuPrimitive.Group>
|
|
91
99
|
)}
|
|
92
|
-
</
|
|
93
|
-
</
|
|
94
|
-
</
|
|
100
|
+
</DropdownMenuPrimitive.Content>
|
|
101
|
+
</DropdownMenuPrimitive.Portal>
|
|
102
|
+
</DropdownMenuPrimitive.Root>
|
|
95
103
|
);
|
|
96
104
|
}
|