@bearmenu/ui 0.5.2 → 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 +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 +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 +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,107 @@
|
|
|
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 { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs";
|
|
5
|
+
|
|
6
|
+
function Triplet({ defaultValue = "a" }: { defaultValue?: string }) {
|
|
7
|
+
return (
|
|
8
|
+
<Tabs defaultValue={defaultValue}>
|
|
9
|
+
<TabsList>
|
|
10
|
+
<TabsTrigger value="a">A</TabsTrigger>
|
|
11
|
+
<TabsTrigger value="b">B</TabsTrigger>
|
|
12
|
+
<TabsTrigger value="c">C</TabsTrigger>
|
|
13
|
+
</TabsList>
|
|
14
|
+
<TabsContent value="a">Panel A</TabsContent>
|
|
15
|
+
<TabsContent value="b">Panel B</TabsContent>
|
|
16
|
+
<TabsContent value="c">Panel C</TabsContent>
|
|
17
|
+
</Tabs>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("Tabs", () => {
|
|
22
|
+
it("renders tablist with the right number of tabs", () => {
|
|
23
|
+
render(<Triplet />);
|
|
24
|
+
expect(screen.getByRole("tablist")).toBeInTheDocument();
|
|
25
|
+
expect(screen.getAllByRole("tab")).toHaveLength(3);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("first tab is active by default and only its panel is visible", () => {
|
|
29
|
+
render(<Triplet />);
|
|
30
|
+
const tabs = screen.getAllByRole("tab");
|
|
31
|
+
expect(tabs[0].getAttribute("data-state")).toBe("active");
|
|
32
|
+
expect(tabs[1].getAttribute("data-state")).toBe("inactive");
|
|
33
|
+
expect(screen.getByText("Panel A")).toBeInTheDocument();
|
|
34
|
+
expect(screen.queryByText("Panel B")).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("switches panel when a different tab is clicked", async () => {
|
|
38
|
+
const user = userEvent.setup();
|
|
39
|
+
render(<Triplet />);
|
|
40
|
+
await user.click(screen.getByRole("tab", { name: "B" }));
|
|
41
|
+
expect(screen.getByText("Panel B")).toBeInTheDocument();
|
|
42
|
+
expect(screen.queryByText("Panel A")).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("wires aria-controls and aria-labelledby between tab and panel", () => {
|
|
46
|
+
render(<Triplet />);
|
|
47
|
+
const tabA = screen.getByRole("tab", { name: "A" });
|
|
48
|
+
const panelA = screen.getByText("Panel A");
|
|
49
|
+
expect(tabA.getAttribute("aria-controls")).toBe(panelA.id);
|
|
50
|
+
expect(panelA.getAttribute("aria-labelledby")).toBe(tabA.id);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("supports ArrowRight to focus next tab", async () => {
|
|
54
|
+
const user = userEvent.setup();
|
|
55
|
+
render(<Triplet />);
|
|
56
|
+
const tabs = screen.getAllByRole("tab");
|
|
57
|
+
tabs[0].focus();
|
|
58
|
+
await user.keyboard("{ArrowRight}");
|
|
59
|
+
expect(tabs[1]).toHaveFocus();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("supports ArrowLeft to focus previous tab", async () => {
|
|
63
|
+
const user = userEvent.setup();
|
|
64
|
+
render(<Triplet defaultValue="b" />);
|
|
65
|
+
const tabs = screen.getAllByRole("tab");
|
|
66
|
+
tabs[1].focus();
|
|
67
|
+
await user.keyboard("{ArrowLeft}");
|
|
68
|
+
expect(tabs[0]).toHaveFocus();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("fires onValueChange when tab changes", async () => {
|
|
72
|
+
const user = userEvent.setup();
|
|
73
|
+
const onValueChange = vi.fn();
|
|
74
|
+
render(
|
|
75
|
+
<Tabs defaultValue="a" onValueChange={onValueChange}>
|
|
76
|
+
<TabsList>
|
|
77
|
+
<TabsTrigger value="a">A</TabsTrigger>
|
|
78
|
+
<TabsTrigger value="b">B</TabsTrigger>
|
|
79
|
+
</TabsList>
|
|
80
|
+
<TabsContent value="a">A</TabsContent>
|
|
81
|
+
<TabsContent value="b">B</TabsContent>
|
|
82
|
+
</Tabs>
|
|
83
|
+
);
|
|
84
|
+
await user.click(screen.getByRole("tab", { name: "B" }));
|
|
85
|
+
expect(onValueChange).toHaveBeenCalledWith("b");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("supports disabled triggers", async () => {
|
|
89
|
+
const user = userEvent.setup();
|
|
90
|
+
render(
|
|
91
|
+
<Tabs defaultValue="a">
|
|
92
|
+
<TabsList>
|
|
93
|
+
<TabsTrigger value="a">A</TabsTrigger>
|
|
94
|
+
<TabsTrigger value="b" disabled>
|
|
95
|
+
B
|
|
96
|
+
</TabsTrigger>
|
|
97
|
+
</TabsList>
|
|
98
|
+
<TabsContent value="a">Panel A</TabsContent>
|
|
99
|
+
<TabsContent value="b">Panel B</TabsContent>
|
|
100
|
+
</Tabs>
|
|
101
|
+
);
|
|
102
|
+
const tabB = screen.getByRole("tab", { name: "B" });
|
|
103
|
+
expect(tabB.getAttribute("data-disabled")).not.toBeNull();
|
|
104
|
+
await user.click(tabB);
|
|
105
|
+
expect(screen.getByText("Panel A")).toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
});
|
package/src/components/tabs.tsx
CHANGED
|
@@ -5,47 +5,93 @@ import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
|
5
5
|
import { cn } from "../lib/utils";
|
|
6
6
|
|
|
7
7
|
type TabsVariant = "pill" | "underline";
|
|
8
|
+
type TabsOrientation = "horizontal" | "vertical";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
type TabsContextValue = {
|
|
11
|
+
variant: TabsVariant;
|
|
12
|
+
orientation: TabsOrientation;
|
|
13
|
+
};
|
|
10
14
|
|
|
11
|
-
const
|
|
15
|
+
const TabsContext = React.createContext<TabsContextValue>({
|
|
16
|
+
variant: "pill",
|
|
17
|
+
orientation: "horizontal",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const Tabs = React.forwardRef<
|
|
21
|
+
React.ElementRef<typeof TabsPrimitive.Root>,
|
|
22
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
|
|
23
|
+
>(({ className, orientation = "horizontal", ...props }, ref) => (
|
|
24
|
+
<TabsPrimitive.Root
|
|
25
|
+
ref={ref}
|
|
26
|
+
data-slot="tabs"
|
|
27
|
+
orientation={orientation}
|
|
28
|
+
className={cn(
|
|
29
|
+
"flex",
|
|
30
|
+
orientation === "vertical" ? "flex-row gap-4" : "flex-col gap-2",
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
));
|
|
36
|
+
Tabs.displayName = TabsPrimitive.Root.displayName;
|
|
12
37
|
|
|
13
38
|
const TabsList = React.forwardRef<
|
|
14
39
|
React.ElementRef<typeof TabsPrimitive.List>,
|
|
15
40
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {
|
|
16
41
|
variant?: TabsVariant;
|
|
42
|
+
/** Make horizontal lists scrollable when overflowing. Default true for underline, false for pill. */
|
|
43
|
+
scrollable?: boolean;
|
|
17
44
|
}
|
|
18
|
-
>(({ className, variant = "pill", children, ...props }, ref) =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
>(({ className, variant = "pill", scrollable, children, ...props }, ref) => {
|
|
46
|
+
const orientation: TabsOrientation = props["aria-orientation"] === "vertical" ? "vertical" : "horizontal";
|
|
47
|
+
const allowScroll = scrollable ?? orientation === "horizontal";
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<TabsContext.Provider value={{ variant, orientation }}>
|
|
51
|
+
<TabsPrimitive.List
|
|
52
|
+
ref={ref}
|
|
53
|
+
data-slot="tabs-list"
|
|
54
|
+
className={cn(
|
|
55
|
+
variant === "pill"
|
|
56
|
+
? orientation === "vertical"
|
|
57
|
+
? "inline-flex flex-col items-stretch gap-1 rounded-2xl bg-muted p-1 text-muted-foreground"
|
|
58
|
+
: "inline-flex h-11 max-w-full items-center justify-start gap-1 rounded-2xl bg-muted p-1 text-muted-foreground"
|
|
59
|
+
: orientation === "vertical"
|
|
60
|
+
? "inline-flex flex-col items-stretch gap-0 border-r border-border bg-transparent text-muted-foreground"
|
|
61
|
+
: "inline-flex h-auto max-w-full items-center gap-0 border-b border-border bg-transparent text-muted-foreground",
|
|
62
|
+
allowScroll &&
|
|
63
|
+
"overflow-x-auto overscroll-x-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden [&>*]:shrink-0",
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
{...props}
|
|
67
|
+
>
|
|
68
|
+
{children}
|
|
69
|
+
</TabsPrimitive.List>
|
|
70
|
+
</TabsContext.Provider>
|
|
71
|
+
);
|
|
72
|
+
});
|
|
34
73
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
35
74
|
|
|
36
75
|
const TabsTrigger = React.forwardRef<
|
|
37
76
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
38
77
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
39
78
|
>(({ className, ...props }, ref) => {
|
|
40
|
-
const variant = React.useContext(
|
|
79
|
+
const { variant, orientation } = React.useContext(TabsContext);
|
|
41
80
|
|
|
42
81
|
return (
|
|
43
82
|
<TabsPrimitive.Trigger
|
|
44
83
|
ref={ref}
|
|
84
|
+
data-slot="tabs-trigger"
|
|
45
85
|
className={cn(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
86
|
+
"inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
87
|
+
variant === "pill" &&
|
|
88
|
+
"rounded-xl px-4 py-1.5 text-muted-foreground hover:text-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
|
89
|
+
variant === "underline" &&
|
|
90
|
+
orientation === "horizontal" &&
|
|
91
|
+
"relative h-11 rounded-none border-b-2 border-transparent px-4 text-muted-foreground hover:text-foreground data-[state=active]:border-primary data-[state=active]:text-foreground",
|
|
92
|
+
variant === "underline" &&
|
|
93
|
+
orientation === "vertical" &&
|
|
94
|
+
"relative rounded-none border-r-2 border-transparent px-4 py-2 -mr-px text-muted-foreground hover:text-foreground data-[state=active]:border-primary data-[state=active]:text-foreground justify-start",
|
|
49
95
|
className
|
|
50
96
|
)}
|
|
51
97
|
{...props}
|
|
@@ -60,8 +106,9 @@ const TabsContent = React.forwardRef<
|
|
|
60
106
|
>(({ className, ...props }, ref) => (
|
|
61
107
|
<TabsPrimitive.Content
|
|
62
108
|
ref={ref}
|
|
109
|
+
data-slot="tabs-content"
|
|
63
110
|
className={cn(
|
|
64
|
-
"
|
|
111
|
+
"flex-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md",
|
|
65
112
|
className
|
|
66
113
|
)}
|
|
67
114
|
{...props}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { TagGroup } from "./tag-group";
|
|
3
|
+
import { Tag } from "./tag";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Components/TagGroup",
|
|
7
|
+
component: TagGroup,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
} satisfies Meta<typeof TagGroup>;
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof meta>;
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<TagGroup label="Tags">
|
|
17
|
+
<Tag>Design</Tag>
|
|
18
|
+
<Tag>Engineering</Tag>
|
|
19
|
+
<Tag>Product</Tag>
|
|
20
|
+
<Tag>Research</Tag>
|
|
21
|
+
</TagGroup>
|
|
22
|
+
),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const VisibleLabel: Story = {
|
|
26
|
+
render: () => (
|
|
27
|
+
<TagGroup label="Filters" srLabel={false}>
|
|
28
|
+
<Tag variant="primary" onRemove={() => {}}>
|
|
29
|
+
Active
|
|
30
|
+
</Tag>
|
|
31
|
+
<Tag variant="primary" onRemove={() => {}}>
|
|
32
|
+
Last 7 days
|
|
33
|
+
</Tag>
|
|
34
|
+
<Tag variant="primary" onRemove={() => {}}>
|
|
35
|
+
Owner: me
|
|
36
|
+
</Tag>
|
|
37
|
+
</TagGroup>
|
|
38
|
+
),
|
|
39
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
const TagGroup = React.forwardRef<
|
|
5
|
+
HTMLDivElement,
|
|
6
|
+
React.HTMLAttributes<HTMLDivElement> & {
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Visually hide the label, keep accessible. */
|
|
9
|
+
srLabel?: boolean;
|
|
10
|
+
}
|
|
11
|
+
>(({ className, label, srLabel = true, children, ...props }, ref) => (
|
|
12
|
+
<div
|
|
13
|
+
ref={ref}
|
|
14
|
+
role="group"
|
|
15
|
+
aria-label={label}
|
|
16
|
+
data-slot="tag-group"
|
|
17
|
+
className={cn("flex flex-wrap items-center gap-1.5", className)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
{label && !srLabel && (
|
|
21
|
+
<span className="mr-1 text-xs font-medium text-muted-foreground">{label}</span>
|
|
22
|
+
)}
|
|
23
|
+
{children}
|
|
24
|
+
</div>
|
|
25
|
+
));
|
|
26
|
+
TagGroup.displayName = "TagGroup";
|
|
27
|
+
|
|
28
|
+
export { TagGroup };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Tag } from "./tag";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Components/Tag",
|
|
6
|
+
component: Tag,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
} satisfies Meta<typeof Tag>;
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof meta>;
|
|
12
|
+
|
|
13
|
+
export const Variants: Story = {
|
|
14
|
+
render: () => (
|
|
15
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
16
|
+
<Tag>Default</Tag>
|
|
17
|
+
<Tag variant="primary">Primary</Tag>
|
|
18
|
+
<Tag variant="success">Success</Tag>
|
|
19
|
+
<Tag variant="warning">Warning</Tag>
|
|
20
|
+
<Tag variant="destructive">Destructive</Tag>
|
|
21
|
+
<Tag variant="outline">Outline</Tag>
|
|
22
|
+
</div>
|
|
23
|
+
),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Sizes: Story = {
|
|
27
|
+
render: () => (
|
|
28
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
29
|
+
<Tag size="sm">Small</Tag>
|
|
30
|
+
<Tag size="default">Default</Tag>
|
|
31
|
+
<Tag size="lg">Large</Tag>
|
|
32
|
+
</div>
|
|
33
|
+
),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Removable: Story = {
|
|
37
|
+
render: () => (
|
|
38
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
39
|
+
<Tag variant="primary" onRemove={() => {}}>
|
|
40
|
+
React
|
|
41
|
+
</Tag>
|
|
42
|
+
<Tag variant="primary" onRemove={() => {}}>
|
|
43
|
+
TypeScript
|
|
44
|
+
</Tag>
|
|
45
|
+
<Tag variant="primary" onRemove={() => {}}>
|
|
46
|
+
Tailwind
|
|
47
|
+
</Tag>
|
|
48
|
+
</div>
|
|
49
|
+
),
|
|
50
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { X } from "lucide-react";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
const tagVariants = cva(
|
|
9
|
+
"inline-flex items-center gap-1 rounded-full border text-xs font-medium transition-colors",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default: "border-border bg-muted text-foreground",
|
|
14
|
+
primary: "border-primary/30 bg-primary/10 text-primary",
|
|
15
|
+
success: "border-success/30 bg-success/10 text-success",
|
|
16
|
+
warning: "border-warning/30 bg-warning/10 text-warning",
|
|
17
|
+
destructive: "border-destructive/30 bg-destructive/10 text-destructive",
|
|
18
|
+
outline: "border-border bg-transparent text-foreground",
|
|
19
|
+
},
|
|
20
|
+
size: {
|
|
21
|
+
sm: "h-5 px-2 text-[10px]",
|
|
22
|
+
default: "h-6 px-2.5",
|
|
23
|
+
lg: "h-7 px-3 text-sm",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultVariants: {
|
|
27
|
+
variant: "default",
|
|
28
|
+
size: "default",
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
type TagProps = React.HTMLAttributes<HTMLSpanElement> &
|
|
34
|
+
VariantProps<typeof tagVariants> & {
|
|
35
|
+
onRemove?: () => void;
|
|
36
|
+
removeLabel?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
|
|
40
|
+
({ className, variant, size, onRemove, removeLabel = "Remove", children, ...props }, ref) => (
|
|
41
|
+
<span
|
|
42
|
+
ref={ref}
|
|
43
|
+
data-slot="tag"
|
|
44
|
+
className={cn(tagVariants({ variant, size }), className)}
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
{onRemove && (
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
aria-label={removeLabel}
|
|
52
|
+
onClick={onRemove}
|
|
53
|
+
className="-mr-0.5 inline-flex size-3.5 items-center justify-center rounded-full transition-colors hover:bg-foreground/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
54
|
+
>
|
|
55
|
+
<X className="size-3" />
|
|
56
|
+
</button>
|
|
57
|
+
)}
|
|
58
|
+
</span>
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
Tag.displayName = "Tag";
|
|
62
|
+
|
|
63
|
+
export { Tag, tagVariants };
|
|
@@ -26,7 +26,7 @@ describe("textVariants", () => {
|
|
|
26
26
|
expect(textVariants({ variant: "h3" })).toContain("text-xl");
|
|
27
27
|
expect(textVariants({ variant: "h4" })).toContain("text-lg");
|
|
28
28
|
expect(textVariants({ variant: "body-lg" })).toContain("text-base");
|
|
29
|
-
expect(textVariants({ variant: "body" })).toContain("text-
|
|
29
|
+
expect(textVariants({ variant: "body" })).toContain("text-base");
|
|
30
30
|
expect(textVariants({ variant: "body-sm" })).toContain("text-sm");
|
|
31
31
|
expect(textVariants({ variant: "caption" })).toContain("text-xs");
|
|
32
32
|
expect(textVariants({ variant: "label" })).toContain("font-medium");
|
package/src/components/text.tsx
CHANGED
|
@@ -11,9 +11,10 @@ const textVariants = cva("", {
|
|
|
11
11
|
h3: "text-xl font-semibold",
|
|
12
12
|
h4: "text-lg font-semibold leading-none tracking-tight",
|
|
13
13
|
"body-lg": "text-base leading-relaxed",
|
|
14
|
-
body: "text-
|
|
14
|
+
body: "text-base leading-normal",
|
|
15
15
|
"body-sm": "text-sm leading-relaxed",
|
|
16
|
-
|
|
16
|
+
small: "text-sm text-muted-foreground",
|
|
17
|
+
caption: "text-xs leading-normal text-muted-foreground",
|
|
17
18
|
label: "text-sm font-medium",
|
|
18
19
|
overline: "text-xs font-semibold uppercase tracking-wider",
|
|
19
20
|
},
|
|
@@ -89,6 +90,21 @@ type TextOwnProps<T extends TextTag = "p"> = {
|
|
|
89
90
|
type TextProps<T extends TextTag = "p"> = TextOwnProps<T> &
|
|
90
91
|
Omit<React.ComponentPropsWithoutRef<T>, keyof TextOwnProps<T> | "color">;
|
|
91
92
|
|
|
93
|
+
const defaultTagForVariant: Record<NonNullable<VariantProps<typeof textVariants>["variant"]>, TextTag> = {
|
|
94
|
+
display: "h1",
|
|
95
|
+
h1: "h1",
|
|
96
|
+
h2: "h2",
|
|
97
|
+
h3: "h3",
|
|
98
|
+
h4: "h4",
|
|
99
|
+
"body-lg": "p",
|
|
100
|
+
body: "p",
|
|
101
|
+
"body-sm": "p",
|
|
102
|
+
small: "p",
|
|
103
|
+
caption: "span",
|
|
104
|
+
label: "span",
|
|
105
|
+
overline: "span",
|
|
106
|
+
};
|
|
107
|
+
|
|
92
108
|
function Text<T extends TextTag = "p">({
|
|
93
109
|
as,
|
|
94
110
|
variant,
|
|
@@ -100,7 +116,7 @@ function Text<T extends TextTag = "p">({
|
|
|
100
116
|
className,
|
|
101
117
|
...props
|
|
102
118
|
}: TextProps<T>) {
|
|
103
|
-
const Comp = (as ?? "p") as React.ElementType;
|
|
119
|
+
const Comp = (as ?? (variant ? defaultTagForVariant[variant] : "p")) as React.ElementType;
|
|
104
120
|
return (
|
|
105
121
|
<Comp
|
|
106
122
|
className={cn(textVariants({ variant, color, weight, leading, tracking, wrap }), className)}
|
|
@@ -0,0 +1,48 @@
|
|
|
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 { Textarea } from "./textarea";
|
|
5
|
+
|
|
6
|
+
describe("Textarea", () => {
|
|
7
|
+
it("renders a textarea", () => {
|
|
8
|
+
render(<Textarea aria-label="bio" />);
|
|
9
|
+
expect(screen.getByRole("textbox")).toBeDefined();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("uses text-base (16px) so iOS does not zoom on focus", () => {
|
|
13
|
+
render(<Textarea aria-label="bio" />);
|
|
14
|
+
expect(screen.getByRole("textbox").className).toContain("text-base");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("fires onChange when typing", async () => {
|
|
18
|
+
const user = userEvent.setup();
|
|
19
|
+
const onChange = vi.fn();
|
|
20
|
+
render(<Textarea aria-label="bio" onChange={onChange} />);
|
|
21
|
+
await user.type(screen.getByRole("textbox"), "hi");
|
|
22
|
+
expect(onChange).toHaveBeenCalled();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("respects disabled", async () => {
|
|
26
|
+
const user = userEvent.setup();
|
|
27
|
+
const onChange = vi.fn();
|
|
28
|
+
render(<Textarea aria-label="bio" disabled onChange={onChange} />);
|
|
29
|
+
await user.type(screen.getByRole("textbox"), "hi");
|
|
30
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("reflects aria-invalid", () => {
|
|
34
|
+
render(<Textarea aria-label="bio" aria-invalid />);
|
|
35
|
+
expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("merges className", () => {
|
|
39
|
+
render(<Textarea aria-label="bio" className="custom" />);
|
|
40
|
+
expect(screen.getByRole("textbox").className).toContain("custom");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("forwards ref to the textarea element", () => {
|
|
44
|
+
const ref = { current: null as HTMLTextAreaElement | null };
|
|
45
|
+
render(<Textarea aria-label="bio" ref={ref} />);
|
|
46
|
+
expect(ref.current?.tagName).toBe("TEXTAREA");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -7,8 +7,9 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
7
7
|
({ className, ...props }, ref) => {
|
|
8
8
|
return (
|
|
9
9
|
<textarea
|
|
10
|
+
data-slot="textarea"
|
|
10
11
|
className={cn(
|
|
11
|
-
"flex min-h-
|
|
12
|
+
"flex min-h-20 w-full rounded-xl border border-border bg-input/50 px-4 py-3 text-base transition-colors 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 resize-y aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/40",
|
|
12
13
|
className
|
|
13
14
|
)}
|
|
14
15
|
ref={ref}
|
package/src/components/toast.tsx
CHANGED
|
@@ -24,12 +24,13 @@ const ToastViewport = React.forwardRef<
|
|
|
24
24
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
|
25
25
|
|
|
26
26
|
const toastVariants = cva(
|
|
27
|
-
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-xl
|
|
27
|
+
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-xl p-6 pr-8 shadow-2xl transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
|
28
28
|
{
|
|
29
29
|
variants: {
|
|
30
30
|
variant: {
|
|
31
|
-
default: "
|
|
32
|
-
destructive:
|
|
31
|
+
default: "glass text-foreground border border-border/50",
|
|
32
|
+
destructive:
|
|
33
|
+
"destructive border border-destructive/60 bg-destructive text-destructive-foreground",
|
|
33
34
|
},
|
|
34
35
|
},
|
|
35
36
|
defaultVariants: {
|
|
@@ -7,13 +7,13 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
7
7
|
import { cn } from "../lib/utils";
|
|
8
8
|
|
|
9
9
|
const toggleVariants = cva(
|
|
10
|
-
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-
|
|
10
|
+
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-accent/50 hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 transition-colors aria-invalid:ring-2 aria-invalid:ring-destructive/40 whitespace-nowrap",
|
|
11
11
|
{
|
|
12
12
|
variants: {
|
|
13
13
|
variant: {
|
|
14
14
|
default: "bg-transparent",
|
|
15
15
|
outline:
|
|
16
|
-
"border border-
|
|
16
|
+
"border border-border bg-transparent hover:bg-accent/50 hover:text-accent-foreground data-[state=on]:border-primary",
|
|
17
17
|
},
|
|
18
18
|
size: {
|
|
19
19
|
default: "h-9 px-2 min-w-9",
|
|
@@ -4,9 +4,19 @@ import * as React from "react";
|
|
|
4
4
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
5
5
|
import { cn } from "../lib/utils";
|
|
6
6
|
|
|
7
|
-
const TooltipProvider =
|
|
7
|
+
const TooltipProvider = ({
|
|
8
|
+
delayDuration = 200,
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) => (
|
|
11
|
+
<TooltipPrimitive.Provider delayDuration={delayDuration} {...props} />
|
|
12
|
+
);
|
|
8
13
|
|
|
9
|
-
const Tooltip =
|
|
14
|
+
const Tooltip = ({
|
|
15
|
+
delayDuration = 200,
|
|
16
|
+
...props
|
|
17
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Root>) => (
|
|
18
|
+
<TooltipPrimitive.Root delayDuration={delayDuration} {...props} />
|
|
19
|
+
);
|
|
10
20
|
|
|
11
21
|
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
12
22
|
|