@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,174 @@
|
|
|
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 { Button, buttonVariants } from "./button";
|
|
5
|
+
|
|
6
|
+
// ─── buttonVariants CVA ───────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
describe("buttonVariants", () => {
|
|
9
|
+
it("returns default variant + size classes when called with no args", () => {
|
|
10
|
+
const result = buttonVariants({});
|
|
11
|
+
expect(result).toContain("bg-primary");
|
|
12
|
+
expect(result).toContain("text-primary-foreground");
|
|
13
|
+
expect(result).toContain("h-11");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("maps variant prop to correct classes", () => {
|
|
17
|
+
expect(buttonVariants({ variant: "default" })).toContain("bg-primary");
|
|
18
|
+
expect(buttonVariants({ variant: "destructive" })).toContain("bg-destructive");
|
|
19
|
+
expect(buttonVariants({ variant: "outline" })).toContain("border-border");
|
|
20
|
+
expect(buttonVariants({ variant: "secondary" })).toContain("bg-secondary");
|
|
21
|
+
expect(buttonVariants({ variant: "ghost" })).toContain("hover:bg-accent");
|
|
22
|
+
expect(buttonVariants({ variant: "link" })).toContain("text-primary");
|
|
23
|
+
expect(buttonVariants({ variant: "glass" })).toContain("glass");
|
|
24
|
+
expect(buttonVariants({ variant: "brown" })).toContain("bg-brown");
|
|
25
|
+
expect(buttonVariants({ variant: "antagonist" })).toContain("bg-primary-foreground");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("maps size prop to correct classes", () => {
|
|
29
|
+
expect(buttonVariants({ size: "xs" })).toContain("h-8");
|
|
30
|
+
expect(buttonVariants({ size: "sm" })).toContain("h-9");
|
|
31
|
+
expect(buttonVariants({ size: "default" })).toContain("h-11");
|
|
32
|
+
expect(buttonVariants({ size: "lg" })).toContain("h-12");
|
|
33
|
+
expect(buttonVariants({ size: "icon" })).toContain("rounded-full");
|
|
34
|
+
expect(buttonVariants({ size: "icon-xs" })).toContain("h-8");
|
|
35
|
+
expect(buttonVariants({ size: "icon-sm" })).toContain("h-9");
|
|
36
|
+
expect(buttonVariants({ size: "icon-lg" })).toContain("h-12");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("applies fullWidth class when fullWidth is true", () => {
|
|
40
|
+
expect(buttonVariants({ fullWidth: true })).toContain("w-full");
|
|
41
|
+
expect(buttonVariants({ fullWidth: false })).not.toContain("w-full");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ─── <Button> component ───────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
describe("Button component", () => {
|
|
48
|
+
it("renders as <button> by default", () => {
|
|
49
|
+
render(<Button>click</Button>);
|
|
50
|
+
const el = screen.getByRole("button", { name: "click" });
|
|
51
|
+
expect(el.tagName).toBe("BUTTON");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("renders children correctly", () => {
|
|
55
|
+
render(<Button>label content</Button>);
|
|
56
|
+
expect(screen.getByText("label content")).toBeDefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("forwards ref to the underlying button element", () => {
|
|
60
|
+
const ref = { current: null as HTMLButtonElement | null };
|
|
61
|
+
render(<Button ref={ref}>ref</Button>);
|
|
62
|
+
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("renders as Slot child when asChild is true", () => {
|
|
66
|
+
render(
|
|
67
|
+
<Button asChild>
|
|
68
|
+
<a href="/x">link</a>
|
|
69
|
+
</Button>
|
|
70
|
+
);
|
|
71
|
+
const el = screen.getByRole("link", { name: "link" });
|
|
72
|
+
expect(el.tagName).toBe("A");
|
|
73
|
+
expect(el.getAttribute("href")).toBe("/x");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("applies data-slot=button attribute", () => {
|
|
77
|
+
render(<Button>x</Button>);
|
|
78
|
+
expect(screen.getByRole("button").getAttribute("data-slot")).toBe("button");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Disabled / loading states
|
|
82
|
+
|
|
83
|
+
it("disables the button when disabled prop is set", () => {
|
|
84
|
+
render(<Button disabled>x</Button>);
|
|
85
|
+
expect((screen.getByRole("button") as HTMLButtonElement).disabled).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("disables the button and sets aria-busy when isLoading", () => {
|
|
89
|
+
render(<Button isLoading>x</Button>);
|
|
90
|
+
const el = screen.getByRole("button");
|
|
91
|
+
expect((el as HTMLButtonElement).disabled).toBe(true);
|
|
92
|
+
expect(el.getAttribute("aria-busy")).toBe("true");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("renders spinner when isLoading", () => {
|
|
96
|
+
const { container } = render(<Button isLoading>x</Button>);
|
|
97
|
+
expect(container.querySelector(".animate-spin")).not.toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("does not set aria-busy when not loading", () => {
|
|
101
|
+
render(<Button>x</Button>);
|
|
102
|
+
expect(screen.getByRole("button").getAttribute("aria-busy")).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Click behavior
|
|
106
|
+
|
|
107
|
+
it("calls onClick when clicked", async () => {
|
|
108
|
+
const onClick = vi.fn();
|
|
109
|
+
const user = userEvent.setup();
|
|
110
|
+
render(<Button onClick={onClick}>x</Button>);
|
|
111
|
+
await user.click(screen.getByRole("button"));
|
|
112
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("does not call onClick when disabled", async () => {
|
|
116
|
+
const onClick = vi.fn();
|
|
117
|
+
const user = userEvent.setup();
|
|
118
|
+
render(
|
|
119
|
+
<Button onClick={onClick} disabled>
|
|
120
|
+
x
|
|
121
|
+
</Button>
|
|
122
|
+
);
|
|
123
|
+
await user.click(screen.getByRole("button"));
|
|
124
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("does not call onClick when loading", async () => {
|
|
128
|
+
const onClick = vi.fn();
|
|
129
|
+
const user = userEvent.setup();
|
|
130
|
+
render(
|
|
131
|
+
<Button onClick={onClick} isLoading>
|
|
132
|
+
x
|
|
133
|
+
</Button>
|
|
134
|
+
);
|
|
135
|
+
await user.click(screen.getByRole("button"));
|
|
136
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Class output
|
|
140
|
+
|
|
141
|
+
it("applies variant class from variant prop", () => {
|
|
142
|
+
render(<Button variant="destructive">x</Button>);
|
|
143
|
+
expect(screen.getByRole("button").className).toContain("bg-destructive");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("applies size class from size prop", () => {
|
|
147
|
+
render(<Button size="sm">x</Button>);
|
|
148
|
+
expect(screen.getByRole("button").className).toContain("h-9");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("applies fullWidth class when fullWidth is true", () => {
|
|
152
|
+
render(<Button fullWidth>x</Button>);
|
|
153
|
+
expect(screen.getByRole("button").className).toContain("w-full");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("merges className via twMerge (overrides defaults)", () => {
|
|
157
|
+
render(<Button className="bg-red-500">x</Button>);
|
|
158
|
+
const el = screen.getByRole("button");
|
|
159
|
+
expect(el.className).toContain("bg-red-500");
|
|
160
|
+
// twMerge strips conflicting bg-primary (but not hover:bg-primary/90)
|
|
161
|
+
expect(el.className).not.toMatch(/(^|\s)bg-primary(\s|$)/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("passes through arbitrary HTML attributes", () => {
|
|
165
|
+
render(
|
|
166
|
+
<Button id="my-btn" type="submit">
|
|
167
|
+
x
|
|
168
|
+
</Button>
|
|
169
|
+
);
|
|
170
|
+
const el = screen.getByRole("button") as HTMLButtonElement;
|
|
171
|
+
expect(el.id).toBe("my-btn");
|
|
172
|
+
expect(el.type).toBe("submit");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -1,40 +1,50 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { Slot } from "@radix-ui/react-slot";
|
|
3
3
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
4
5
|
import { cn } from "../lib/utils";
|
|
5
6
|
import { useHaptic } from "../hooks/use-haptic";
|
|
6
7
|
|
|
7
8
|
const buttonVariants = cva(
|
|
8
|
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-
|
|
9
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer [@media(pointer:coarse)]:active:scale-95",
|
|
9
10
|
{
|
|
10
11
|
variants: {
|
|
11
12
|
variant: {
|
|
12
13
|
default:
|
|
13
|
-
"bg-primary
|
|
14
|
+
"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 hover:shadow",
|
|
14
15
|
destructive:
|
|
15
|
-
"bg-destructive text-destructive-foreground shadow-
|
|
16
|
+
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/85 hover:shadow",
|
|
16
17
|
outline:
|
|
17
|
-
"border border-border bg-
|
|
18
|
+
"border border-border bg-background hover:bg-accent hover:text-accent-foreground hover:border-accent",
|
|
18
19
|
secondary:
|
|
19
|
-
"bg-secondary text-secondary-foreground shadow-
|
|
20
|
-
ghost: "hover:bg-
|
|
21
|
-
link: "text-primary underline-offset-4 hover:underline
|
|
22
|
-
glass: "glass glass-hover text-foreground
|
|
20
|
+
"bg-secondary text-secondary-foreground shadow-sm hover:bg-accent hover:text-accent-foreground border border-border hover:border-accent",
|
|
21
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
22
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
23
|
+
glass: "glass glass-hover text-foreground",
|
|
23
24
|
brown:
|
|
24
|
-
"bg-
|
|
25
|
+
"bg-brown text-brown-foreground shadow-sm hover:bg-brown/85 hover:shadow",
|
|
25
26
|
antagonist:
|
|
26
|
-
"bg-primary-foreground text-primary shadow-
|
|
27
|
+
"bg-primary-foreground text-primary shadow-sm hover:bg-accent hover:text-accent-foreground border border-border hover:border-accent",
|
|
27
28
|
},
|
|
28
29
|
size: {
|
|
29
|
-
|
|
30
|
+
xs: "h-8 rounded-lg px-3 text-xs [&_svg]:size-3.5",
|
|
30
31
|
sm: "h-9 rounded-lg px-4",
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
default: "h-11 px-6 py-2",
|
|
33
|
+
lg: "h-12 rounded-2xl px-8",
|
|
34
|
+
icon: "h-11 w-11 rounded-full",
|
|
35
|
+
"icon-xs": "h-8 w-8 rounded-full [&_svg]:size-3.5",
|
|
36
|
+
"icon-sm": "h-9 w-9 rounded-full",
|
|
37
|
+
"icon-lg": "h-12 w-12 rounded-full",
|
|
38
|
+
},
|
|
39
|
+
fullWidth: {
|
|
40
|
+
true: "w-full",
|
|
41
|
+
false: "",
|
|
33
42
|
},
|
|
34
43
|
},
|
|
35
44
|
defaultVariants: {
|
|
36
45
|
variant: "default",
|
|
37
46
|
size: "default",
|
|
47
|
+
fullWidth: false,
|
|
38
48
|
},
|
|
39
49
|
}
|
|
40
50
|
);
|
|
@@ -44,6 +54,7 @@ export interface ButtonProps
|
|
|
44
54
|
asChild?: boolean;
|
|
45
55
|
haptic?: boolean;
|
|
46
56
|
hapticStyle?: "light" | "medium" | "heavy";
|
|
57
|
+
isLoading?: boolean;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
@@ -52,10 +63,14 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
52
63
|
className,
|
|
53
64
|
variant,
|
|
54
65
|
size,
|
|
66
|
+
fullWidth,
|
|
55
67
|
asChild = false,
|
|
56
68
|
haptic = true,
|
|
57
69
|
hapticStyle = "light",
|
|
70
|
+
isLoading = false,
|
|
71
|
+
disabled,
|
|
58
72
|
onClick,
|
|
73
|
+
children,
|
|
59
74
|
...props
|
|
60
75
|
},
|
|
61
76
|
ref
|
|
@@ -64,28 +79,36 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
64
79
|
const { trigger } = useHaptic();
|
|
65
80
|
const [isMounted, setIsMounted] = React.useState(false);
|
|
66
81
|
|
|
67
|
-
// Track mounted state to prevent hydration issues
|
|
68
82
|
React.useEffect(() => {
|
|
69
83
|
setIsMounted(true);
|
|
70
84
|
}, []);
|
|
71
85
|
|
|
72
86
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
73
|
-
// Only trigger haptic feedback after component has mounted to prevent hydration issues
|
|
74
87
|
if (haptic && isMounted) {
|
|
75
88
|
trigger(hapticStyle);
|
|
76
89
|
}
|
|
77
|
-
|
|
78
|
-
// Call the original onClick handler
|
|
79
90
|
onClick?.(event);
|
|
80
91
|
};
|
|
81
92
|
|
|
82
93
|
return (
|
|
83
94
|
<Comp
|
|
84
|
-
|
|
95
|
+
data-slot="button"
|
|
96
|
+
className={cn(buttonVariants({ variant, size, fullWidth, className }))}
|
|
85
97
|
ref={ref}
|
|
86
98
|
onClick={handleClick}
|
|
99
|
+
disabled={disabled || isLoading}
|
|
100
|
+
aria-busy={isLoading || undefined}
|
|
87
101
|
{...props}
|
|
88
|
-
|
|
102
|
+
>
|
|
103
|
+
{isLoading ? (
|
|
104
|
+
<>
|
|
105
|
+
<Loader2 className="animate-spin" aria-hidden="true" />
|
|
106
|
+
{!asChild && children}
|
|
107
|
+
</>
|
|
108
|
+
) : (
|
|
109
|
+
children
|
|
110
|
+
)}
|
|
111
|
+
</Comp>
|
|
89
112
|
);
|
|
90
113
|
}
|
|
91
114
|
);
|
|
@@ -25,7 +25,7 @@ function Calendar({
|
|
|
25
25
|
<DayPicker
|
|
26
26
|
showOutsideDays={showOutsideDays}
|
|
27
27
|
className={cn(
|
|
28
|
-
"group/calendar bg-background p-3 [--cell-size:--spacing(
|
|
28
|
+
"group/calendar bg-background p-3 [--cell-size:--spacing(9)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
|
29
29
|
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
|
30
30
|
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
|
31
31
|
className
|
|
@@ -168,7 +168,7 @@ function CalendarDayButton({
|
|
|
168
168
|
data-range-end={modifiers.range_end}
|
|
169
169
|
data-range-middle={modifiers.range_middle}
|
|
170
170
|
className={cn(
|
|
171
|
-
"group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground flex aspect-square
|
|
171
|
+
"group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground flex aspect-square h-(--cell-size) w-(--cell-size) size-auto min-w-0 flex-col items-center justify-center gap-1 rounded-md p-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
|
172
172
|
defaultClassNames.day,
|
|
173
173
|
className
|
|
174
174
|
)}
|
package/src/components/card.tsx
CHANGED
|
@@ -1,21 +1,42 @@
|
|
|
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
|
import { textVariants } from "./text";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const cardVariants = cva("rounded-3xl text-card-foreground transition-colors", {
|
|
7
|
+
variants: {
|
|
8
|
+
variant: {
|
|
9
|
+
surface: "bg-card border border-border",
|
|
10
|
+
glass: "glass",
|
|
11
|
+
outline: "bg-transparent border border-border",
|
|
12
|
+
muted: "bg-muted border border-transparent",
|
|
13
|
+
},
|
|
14
|
+
size: {
|
|
15
|
+
sm: "[&_[data-slot=card-header]]:p-4 [&_[data-slot=card-content]]:p-4 [&_[data-slot=card-content]]:pt-0 [&_[data-slot=card-footer]]:p-4 [&_[data-slot=card-footer]]:pt-0",
|
|
16
|
+
default:
|
|
17
|
+
"[&_[data-slot=card-header]]:p-6 [&_[data-slot=card-content]]:p-6 [&_[data-slot=card-content]]:pt-0 [&_[data-slot=card-footer]]:p-6 [&_[data-slot=card-footer]]:pt-0",
|
|
18
|
+
lg: "[&_[data-slot=card-header]]:p-8 [&_[data-slot=card-content]]:p-8 [&_[data-slot=card-content]]:pt-0 [&_[data-slot=card-footer]]:p-8 [&_[data-slot=card-footer]]:pt-0",
|
|
19
|
+
},
|
|
20
|
+
interactive: {
|
|
21
|
+
true: "cursor-pointer hover:border-primary/40 hover:-translate-y-0.5 transition-[transform,border-color] duration-200",
|
|
22
|
+
false: "",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
variant: "surface",
|
|
27
|
+
size: "default",
|
|
28
|
+
interactive: false,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
6
31
|
|
|
7
32
|
const Card = React.forwardRef<
|
|
8
33
|
HTMLDivElement,
|
|
9
|
-
React.HTMLAttributes<HTMLDivElement> &
|
|
10
|
-
>(({ className, variant
|
|
34
|
+
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof cardVariants>
|
|
35
|
+
>(({ className, variant, size, interactive, ...props }, ref) => (
|
|
11
36
|
<div
|
|
12
37
|
ref={ref}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
? "glass rounded-[22px] border border-transparent text-card-foreground transition-[transform,border-color] duration-300 [@media(hover:hover)]:hover:-translate-y-0.5 hover:border-primary/40"
|
|
16
|
-
: "bg-surface rounded-[22px] border border-border text-card-foreground transition-all duration-200 hover:border-primary/40 hover:-translate-y-0.5",
|
|
17
|
-
className
|
|
18
|
-
)}
|
|
38
|
+
data-slot="card"
|
|
39
|
+
className={cn(cardVariants({ variant, size, interactive }), className)}
|
|
19
40
|
{...props}
|
|
20
41
|
/>
|
|
21
42
|
));
|
|
@@ -23,7 +44,12 @@ Card.displayName = "Card";
|
|
|
23
44
|
|
|
24
45
|
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
25
46
|
({ className, ...props }, ref) => (
|
|
26
|
-
<div
|
|
47
|
+
<div
|
|
48
|
+
ref={ref}
|
|
49
|
+
data-slot="card-header"
|
|
50
|
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
27
53
|
)
|
|
28
54
|
);
|
|
29
55
|
CardHeader.displayName = "CardHeader";
|
|
@@ -32,6 +58,7 @@ const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivE
|
|
|
32
58
|
({ className, ...props }, ref) => (
|
|
33
59
|
<div
|
|
34
60
|
ref={ref}
|
|
61
|
+
data-slot="card-title"
|
|
35
62
|
className={cn(textVariants({ variant: "h4", wrap: "balance" }), className)}
|
|
36
63
|
{...props}
|
|
37
64
|
/>
|
|
@@ -43,6 +70,7 @@ const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HT
|
|
|
43
70
|
({ className, ...props }, ref) => (
|
|
44
71
|
<div
|
|
45
72
|
ref={ref}
|
|
73
|
+
data-slot="card-description"
|
|
46
74
|
className={cn("text-sm", textVariants({ color: "muted", wrap: "pretty" }), className)}
|
|
47
75
|
{...props}
|
|
48
76
|
/>
|
|
@@ -52,7 +80,7 @@ CardDescription.displayName = "CardDescription";
|
|
|
52
80
|
|
|
53
81
|
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
54
82
|
({ className, ...props }, ref) => (
|
|
55
|
-
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
83
|
+
<div ref={ref} data-slot="card-content" className={cn("p-6 pt-0", className)} {...props} />
|
|
56
84
|
)
|
|
57
85
|
);
|
|
58
86
|
CardContent.displayName = "CardContent";
|
|
@@ -71,9 +99,23 @@ CardAction.displayName = "CardAction";
|
|
|
71
99
|
|
|
72
100
|
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
73
101
|
({ className, ...props }, ref) => (
|
|
74
|
-
<div
|
|
102
|
+
<div
|
|
103
|
+
ref={ref}
|
|
104
|
+
data-slot="card-footer"
|
|
105
|
+
className={cn("flex items-center p-6 pt-0", className)}
|
|
106
|
+
{...props}
|
|
107
|
+
/>
|
|
75
108
|
)
|
|
76
109
|
);
|
|
77
110
|
CardFooter.displayName = "CardFooter";
|
|
78
111
|
|
|
79
|
-
export {
|
|
112
|
+
export {
|
|
113
|
+
Card,
|
|
114
|
+
CardHeader,
|
|
115
|
+
CardFooter,
|
|
116
|
+
CardTitle,
|
|
117
|
+
CardAction,
|
|
118
|
+
CardDescription,
|
|
119
|
+
CardContent,
|
|
120
|
+
cardVariants,
|
|
121
|
+
};
|
|
@@ -16,6 +16,8 @@ type CarouselProps = {
|
|
|
16
16
|
opts?: CarouselOptions;
|
|
17
17
|
plugins?: CarouselPlugin;
|
|
18
18
|
orientation?: "horizontal" | "vertical";
|
|
19
|
+
/** Gap between slides in pixels. Defaults to 16. */
|
|
20
|
+
gap?: number;
|
|
19
21
|
setApi?: (api: CarouselApi) => void;
|
|
20
22
|
};
|
|
21
23
|
|
|
@@ -24,19 +26,21 @@ type CarouselContextProps = {
|
|
|
24
26
|
api: ReturnType<typeof useEmblaCarousel>[1];
|
|
25
27
|
scrollPrev: () => void;
|
|
26
28
|
scrollNext: () => void;
|
|
29
|
+
scrollTo: (index: number) => void;
|
|
27
30
|
canScrollPrev: boolean;
|
|
28
31
|
canScrollNext: boolean;
|
|
32
|
+
selectedIndex: number;
|
|
33
|
+
slideCount: number;
|
|
34
|
+
gap: number;
|
|
29
35
|
} & CarouselProps;
|
|
30
36
|
|
|
31
37
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
|
32
38
|
|
|
33
39
|
function useCarousel() {
|
|
34
40
|
const context = React.useContext(CarouselContext);
|
|
35
|
-
|
|
36
41
|
if (!context) {
|
|
37
42
|
throw new Error("useCarousel must be used within a <Carousel />");
|
|
38
43
|
}
|
|
39
|
-
|
|
40
44
|
return context;
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -45,6 +49,7 @@ function Carousel({
|
|
|
45
49
|
opts,
|
|
46
50
|
setApi,
|
|
47
51
|
plugins,
|
|
52
|
+
gap = 16,
|
|
48
53
|
className,
|
|
49
54
|
children,
|
|
50
55
|
...props
|
|
@@ -58,11 +63,15 @@ function Carousel({
|
|
|
58
63
|
);
|
|
59
64
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
|
60
65
|
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
|
66
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
67
|
+
const [slideCount, setSlideCount] = React.useState(0);
|
|
61
68
|
|
|
62
69
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
|
63
70
|
if (!api) return;
|
|
64
71
|
setCanScrollPrev(api.canScrollPrev());
|
|
65
72
|
setCanScrollNext(api.canScrollNext());
|
|
73
|
+
setSelectedIndex(api.selectedScrollSnap());
|
|
74
|
+
setSlideCount(api.scrollSnapList().length);
|
|
66
75
|
}, []);
|
|
67
76
|
|
|
68
77
|
const scrollPrev = React.useCallback(() => {
|
|
@@ -73,6 +82,13 @@ function Carousel({
|
|
|
73
82
|
api?.scrollNext();
|
|
74
83
|
}, [api]);
|
|
75
84
|
|
|
85
|
+
const scrollTo = React.useCallback(
|
|
86
|
+
(index: number) => {
|
|
87
|
+
api?.scrollTo(index);
|
|
88
|
+
},
|
|
89
|
+
[api]
|
|
90
|
+
);
|
|
91
|
+
|
|
76
92
|
const handleKeyDown = React.useCallback(
|
|
77
93
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
78
94
|
if (event.key === "ArrowLeft") {
|
|
@@ -96,7 +112,6 @@ function Carousel({
|
|
|
96
112
|
onSelect(api);
|
|
97
113
|
api.on("reInit", onSelect);
|
|
98
114
|
api.on("select", onSelect);
|
|
99
|
-
|
|
100
115
|
return () => {
|
|
101
116
|
api?.off("select", onSelect);
|
|
102
117
|
};
|
|
@@ -106,13 +121,17 @@ function Carousel({
|
|
|
106
121
|
<CarouselContext.Provider
|
|
107
122
|
value={{
|
|
108
123
|
carouselRef,
|
|
109
|
-
api
|
|
124
|
+
api,
|
|
110
125
|
opts,
|
|
111
126
|
orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
|
127
|
+
gap,
|
|
112
128
|
scrollPrev,
|
|
113
129
|
scrollNext,
|
|
130
|
+
scrollTo,
|
|
114
131
|
canScrollPrev,
|
|
115
132
|
canScrollNext,
|
|
133
|
+
selectedIndex,
|
|
134
|
+
slideCount,
|
|
116
135
|
}}
|
|
117
136
|
>
|
|
118
137
|
<div
|
|
@@ -130,31 +149,36 @@ function Carousel({
|
|
|
130
149
|
}
|
|
131
150
|
|
|
132
151
|
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
133
|
-
const { carouselRef, orientation } = useCarousel();
|
|
152
|
+
const { carouselRef, orientation, gap } = useCarousel();
|
|
134
153
|
|
|
135
154
|
return (
|
|
136
155
|
<div ref={carouselRef} className="overflow-hidden" data-slot="carousel-content">
|
|
137
156
|
<div
|
|
138
|
-
className={cn("flex", orientation === "horizontal" ? "
|
|
157
|
+
className={cn("flex", orientation === "horizontal" ? "" : "flex-col", className)}
|
|
158
|
+
style={
|
|
159
|
+
orientation === "horizontal"
|
|
160
|
+
? { marginLeft: `-${gap}px` }
|
|
161
|
+
: { marginTop: `-${gap}px` }
|
|
162
|
+
}
|
|
139
163
|
{...props}
|
|
140
164
|
/>
|
|
141
165
|
</div>
|
|
142
166
|
);
|
|
143
167
|
}
|
|
144
168
|
|
|
145
|
-
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
|
146
|
-
const { orientation } = useCarousel();
|
|
169
|
+
function CarouselItem({ className, style, ...props }: React.ComponentProps<"div">) {
|
|
170
|
+
const { orientation, gap } = useCarousel();
|
|
147
171
|
|
|
148
172
|
return (
|
|
149
173
|
<div
|
|
150
174
|
role="group"
|
|
151
175
|
aria-roledescription="slide"
|
|
152
176
|
data-slot="carousel-item"
|
|
153
|
-
className={cn(
|
|
154
|
-
|
|
155
|
-
orientation === "horizontal" ?
|
|
156
|
-
|
|
157
|
-
|
|
177
|
+
className={cn("min-w-0 shrink-0 grow-0 basis-full", className)}
|
|
178
|
+
style={{
|
|
179
|
+
...(orientation === "horizontal" ? { paddingLeft: `${gap}px` } : { paddingTop: `${gap}px` }),
|
|
180
|
+
...style,
|
|
181
|
+
}}
|
|
158
182
|
{...props}
|
|
159
183
|
/>
|
|
160
184
|
);
|
|
@@ -220,6 +244,51 @@ function CarouselNext({
|
|
|
220
244
|
);
|
|
221
245
|
}
|
|
222
246
|
|
|
247
|
+
function CarouselDots({ className, ...props }: React.ComponentProps<"div">) {
|
|
248
|
+
const { slideCount, selectedIndex, scrollTo } = useCarousel();
|
|
249
|
+
|
|
250
|
+
if (slideCount === 0) return null;
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<div
|
|
254
|
+
data-slot="carousel-dots"
|
|
255
|
+
className={cn("flex items-center justify-center gap-1.5", className)}
|
|
256
|
+
{...props}
|
|
257
|
+
>
|
|
258
|
+
{Array.from({ length: slideCount }).map((_, i) => (
|
|
259
|
+
<button
|
|
260
|
+
key={i}
|
|
261
|
+
type="button"
|
|
262
|
+
aria-label={`Go to slide ${i + 1}`}
|
|
263
|
+
aria-current={i === selectedIndex ? "true" : undefined}
|
|
264
|
+
onClick={() => scrollTo(i)}
|
|
265
|
+
className={cn(
|
|
266
|
+
"size-2 rounded-full transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
267
|
+
i === selectedIndex ? "w-6 bg-primary" : "bg-muted-foreground/30 hover:bg-muted-foreground/50"
|
|
268
|
+
)}
|
|
269
|
+
/>
|
|
270
|
+
))}
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function CarouselCounter({ className, ...props }: React.ComponentProps<"div">) {
|
|
276
|
+
const { slideCount, selectedIndex } = useCarousel();
|
|
277
|
+
|
|
278
|
+
if (slideCount === 0) return null;
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div
|
|
282
|
+
data-slot="carousel-counter"
|
|
283
|
+
aria-live="polite"
|
|
284
|
+
className={cn("text-xs tabular-nums text-muted-foreground", className)}
|
|
285
|
+
{...props}
|
|
286
|
+
>
|
|
287
|
+
{selectedIndex + 1} / {slideCount}
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
223
292
|
export {
|
|
224
293
|
type CarouselApi,
|
|
225
294
|
Carousel,
|
|
@@ -227,4 +296,7 @@ export {
|
|
|
227
296
|
CarouselItem,
|
|
228
297
|
CarouselPrevious,
|
|
229
298
|
CarouselNext,
|
|
299
|
+
CarouselDots,
|
|
300
|
+
CarouselCounter,
|
|
301
|
+
useCarousel,
|
|
230
302
|
};
|