@bearmenu/ui 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +5 -2
- 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 +4 -2
- 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 +29 -16
- 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/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 +15 -13
- 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/sticky-container.js +1 -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 +3 -3
- 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 +44 -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/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 +14 -12
- 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/sticky-container.tsx +1 -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 +142 -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,103 @@
|
|
|
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 { Input, inputVariants } from "./input";
|
|
5
|
+
|
|
6
|
+
// ─── inputVariants CVA ────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
describe("inputVariants", () => {
|
|
9
|
+
it("returns default size classes when called with no args", () => {
|
|
10
|
+
const result = inputVariants({});
|
|
11
|
+
expect(result).toContain("h-11");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("maps size prop to correct classes", () => {
|
|
15
|
+
expect(inputVariants({ size: "sm" })).toContain("h-9");
|
|
16
|
+
expect(inputVariants({ size: "default" })).toContain("h-11");
|
|
17
|
+
expect(inputVariants({ size: "lg" })).toContain("h-12");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("includes aria-invalid styles in base classes", () => {
|
|
21
|
+
const result = inputVariants({});
|
|
22
|
+
expect(result).toContain("aria-invalid:border-destructive");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// ─── <Input> component ────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
describe("Input component", () => {
|
|
29
|
+
it("renders as <input> by default", () => {
|
|
30
|
+
render(<Input placeholder="enter" />);
|
|
31
|
+
const el = screen.getByPlaceholderText("enter");
|
|
32
|
+
expect(el.tagName).toBe("INPUT");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("applies data-slot=input attribute", () => {
|
|
36
|
+
render(<Input data-testid="i" />);
|
|
37
|
+
expect(screen.getByTestId("i").getAttribute("data-slot")).toBe("input");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("forwards ref to the underlying input element", () => {
|
|
41
|
+
const ref = { current: null as HTMLInputElement | null };
|
|
42
|
+
render(<Input ref={ref} />);
|
|
43
|
+
expect(ref.current).toBeInstanceOf(HTMLInputElement);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("respects type prop", () => {
|
|
47
|
+
render(<Input type="email" data-testid="i" />);
|
|
48
|
+
expect((screen.getByTestId("i") as HTMLInputElement).type).toBe("email");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("accepts user input", async () => {
|
|
52
|
+
const user = userEvent.setup();
|
|
53
|
+
render(<Input data-testid="i" />);
|
|
54
|
+
const el = screen.getByTestId("i") as HTMLInputElement;
|
|
55
|
+
await user.type(el, "hello");
|
|
56
|
+
expect(el.value).toBe("hello");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("calls onChange handler", async () => {
|
|
60
|
+
const onChange = vi.fn();
|
|
61
|
+
const user = userEvent.setup();
|
|
62
|
+
render(<Input data-testid="i" onChange={onChange} />);
|
|
63
|
+
await user.type(screen.getByTestId("i"), "a");
|
|
64
|
+
expect(onChange).toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("respects disabled prop", async () => {
|
|
68
|
+
const onChange = vi.fn();
|
|
69
|
+
const user = userEvent.setup();
|
|
70
|
+
render(<Input data-testid="i" disabled onChange={onChange} />);
|
|
71
|
+
const el = screen.getByTestId("i") as HTMLInputElement;
|
|
72
|
+
expect(el.disabled).toBe(true);
|
|
73
|
+
await user.type(el, "x");
|
|
74
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("wires aria-invalid for invalid state", () => {
|
|
78
|
+
render(<Input data-testid="i" aria-invalid="true" />);
|
|
79
|
+
expect(screen.getByTestId("i").getAttribute("aria-invalid")).toBe("true");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("respects controlled value", () => {
|
|
83
|
+
render(<Input data-testid="i" value="hello" onChange={() => {}} />);
|
|
84
|
+
expect((screen.getByTestId("i") as HTMLInputElement).value).toBe("hello");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("applies size class from size prop", () => {
|
|
88
|
+
render(<Input data-testid="i" size="sm" />);
|
|
89
|
+
expect(screen.getByTestId("i").className).toContain("h-9");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("merges className via twMerge", () => {
|
|
93
|
+
render(<Input data-testid="i" className="text-red-500" />);
|
|
94
|
+
expect(screen.getByTestId("i").className).toContain("text-red-500");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("passes through name and id", () => {
|
|
98
|
+
render(<Input id="email" name="email" data-testid="i" />);
|
|
99
|
+
const el = screen.getByTestId("i");
|
|
100
|
+
expect(el.id).toBe("email");
|
|
101
|
+
expect(el.getAttribute("name")).toBe("email");
|
|
102
|
+
});
|
|
103
|
+
});
|
package/src/components/input.tsx
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
3
|
import { cn } from "../lib/utils";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
const inputVariants = cva(
|
|
6
|
+
"flex w-full rounded-xl border border-border bg-input/50 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:bg-input disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/40",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
size: {
|
|
10
|
+
sm: "h-9 px-3 text-base md:text-sm",
|
|
11
|
+
default: "h-11 px-4 py-2",
|
|
12
|
+
lg: "h-12 px-5 text-base",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
size: "default",
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export interface InputProps
|
|
22
|
+
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
|
|
23
|
+
VariantProps<typeof inputVariants> {}
|
|
5
24
|
|
|
6
25
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
7
|
-
({ className, type, ...props }, ref) => {
|
|
26
|
+
({ className, type, size, ...props }, ref) => {
|
|
8
27
|
return (
|
|
9
28
|
<input
|
|
10
29
|
type={type}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
className
|
|
14
|
-
)}
|
|
30
|
+
data-slot="input"
|
|
31
|
+
className={cn(inputVariants({ size }), className)}
|
|
15
32
|
ref={ref}
|
|
16
33
|
{...props}
|
|
17
34
|
/>
|
|
@@ -20,4 +37,4 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
20
37
|
);
|
|
21
38
|
Input.displayName = "Input";
|
|
22
39
|
|
|
23
|
-
export { Input };
|
|
40
|
+
export { Input, inputVariants };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Mail, ChevronRight } from "lucide-react";
|
|
3
|
+
import { Item, ItemActions, ItemContent, ItemDescription, ItemMedia, ItemTitle } from "./item";
|
|
4
|
+
import { Avatar, AvatarFallback } from "./avatar";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/Item",
|
|
8
|
+
component: Item,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
} satisfies Meta<typeof Item>;
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof meta>;
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<Item interactive className="w-80">
|
|
18
|
+
<ItemMedia>
|
|
19
|
+
<Mail className="size-5" />
|
|
20
|
+
</ItemMedia>
|
|
21
|
+
<ItemContent>
|
|
22
|
+
<ItemTitle>Inbox</ItemTitle>
|
|
23
|
+
<ItemDescription>3 new messages</ItemDescription>
|
|
24
|
+
</ItemContent>
|
|
25
|
+
<ItemActions>
|
|
26
|
+
<ChevronRight className="size-4 text-muted-foreground" />
|
|
27
|
+
</ItemActions>
|
|
28
|
+
</Item>
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const Variants: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<div className="flex w-80 flex-col gap-2">
|
|
35
|
+
<Item variant="default" interactive>
|
|
36
|
+
<ItemMedia>
|
|
37
|
+
<Avatar size="sm"><AvatarFallback>AS</AvatarFallback></Avatar>
|
|
38
|
+
</ItemMedia>
|
|
39
|
+
<ItemContent>
|
|
40
|
+
<ItemTitle>Alice Smith</ItemTitle>
|
|
41
|
+
<ItemDescription>Online</ItemDescription>
|
|
42
|
+
</ItemContent>
|
|
43
|
+
</Item>
|
|
44
|
+
<Item variant="muted">
|
|
45
|
+
<ItemContent><ItemTitle>Muted background</ItemTitle></ItemContent>
|
|
46
|
+
</Item>
|
|
47
|
+
<Item variant="outline">
|
|
48
|
+
<ItemContent><ItemTitle>Outline border</ItemTitle></ItemContent>
|
|
49
|
+
</Item>
|
|
50
|
+
</div>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
@@ -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
|
+
});
|