@mzc-fe/design-system 0.0.1-rc.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/.husky/pre-push +21 -0
- package/.storybook/main.ts +11 -0
- package/.storybook/preview.tsx +30 -0
- package/.vscode/settings.json +12 -0
- package/.vscode/tailwind.json +105 -0
- package/README.md +136 -0
- package/bitbucket-pipelines.yml +52 -0
- package/components.json +21 -0
- package/eslint.config.js +38 -0
- package/package.json +98 -0
- package/public/vite.svg +1 -0
- package/src/components/accordion.stories.tsx +258 -0
- package/src/components/accordion.test.tsx +390 -0
- package/src/components/accordion.tsx +64 -0
- package/src/components/alert-dialog.stories.tsx +213 -0
- package/src/components/alert-dialog.test.tsx +80 -0
- package/src/components/alert-dialog.tsx +155 -0
- package/src/components/alert.stories.tsx +84 -0
- package/src/components/alert.test.tsx +35 -0
- package/src/components/alert.tsx +66 -0
- package/src/components/aspect-ratio.stories.tsx +97 -0
- package/src/components/aspect-ratio.test.tsx +47 -0
- package/src/components/aspect-ratio.tsx +11 -0
- package/src/components/avatar.stories.tsx +76 -0
- package/src/components/avatar.test.tsx +50 -0
- package/src/components/avatar.tsx +51 -0
- package/src/components/badge.stories.tsx +64 -0
- package/src/components/badge.test.tsx +34 -0
- package/src/components/badge.tsx +46 -0
- package/src/components/breadcrumb.stories.tsx +86 -0
- package/src/components/breadcrumb.test.tsx +74 -0
- package/src/components/breadcrumb.tsx +109 -0
- package/src/components/button-group.stories.tsx +62 -0
- package/src/components/button-group.tsx +83 -0
- package/src/components/button.stories.tsx +118 -0
- package/src/components/button.test.tsx +64 -0
- package/src/components/button.tsx +62 -0
- package/src/components/calendar.stories.tsx +81 -0
- package/src/components/calendar.tsx +220 -0
- package/src/components/card.stories.tsx +110 -0
- package/src/components/card.test.tsx +56 -0
- package/src/components/card.tsx +92 -0
- package/src/components/carousel.stories.tsx +90 -0
- package/src/components/carousel.tsx +239 -0
- package/src/components/chart.tsx +357 -0
- package/src/components/checkbox.stories.tsx +108 -0
- package/src/components/checkbox.test.tsx +67 -0
- package/src/components/checkbox.tsx +32 -0
- package/src/components/collapsible.stories.tsx +106 -0
- package/src/components/collapsible.test.tsx +92 -0
- package/src/components/collapsible.tsx +31 -0
- package/src/components/command.stories.tsx +90 -0
- package/src/components/command.tsx +182 -0
- package/src/components/context-menu.stories.tsx +63 -0
- package/src/components/context-menu.tsx +252 -0
- package/src/components/dialog.stories.tsx +128 -0
- package/src/components/dialog.tsx +141 -0
- package/src/components/drawer.stories.tsx +104 -0
- package/src/components/drawer.tsx +135 -0
- package/src/components/dropdown-menu.stories.tsx +97 -0
- package/src/components/dropdown-menu.tsx +255 -0
- package/src/components/empty.stories.tsx +90 -0
- package/src/components/empty.test.tsx +55 -0
- package/src/components/empty.tsx +104 -0
- package/src/components/field.tsx +246 -0
- package/src/components/form.tsx +168 -0
- package/src/components/hover-card.stories.tsx +66 -0
- package/src/components/hover-card.tsx +44 -0
- package/src/components/input-group.stories.tsx +57 -0
- package/src/components/input-group.test.tsx +40 -0
- package/src/components/input-group.tsx +170 -0
- package/src/components/input-otp.stories.tsx +94 -0
- package/src/components/input-otp.test.tsx +60 -0
- package/src/components/input-otp.tsx +75 -0
- package/src/components/input.stories.tsx +94 -0
- package/src/components/input.test.tsx +53 -0
- package/src/components/input.tsx +21 -0
- package/src/components/item.tsx +193 -0
- package/src/components/kbd.stories.tsx +100 -0
- package/src/components/kbd.test.tsx +28 -0
- package/src/components/kbd.tsx +28 -0
- package/src/components/label.stories.tsx +48 -0
- package/src/components/label.test.tsx +28 -0
- package/src/components/label.tsx +24 -0
- package/src/components/menubar.tsx +274 -0
- package/src/components/navigation-menu.tsx +168 -0
- package/src/components/pagination.stories.tsx +107 -0
- package/src/components/pagination.tsx +127 -0
- package/src/components/popover.stories.tsx +102 -0
- package/src/components/popover.tsx +48 -0
- package/src/components/progress.stories.tsx +76 -0
- package/src/components/progress.test.tsx +36 -0
- package/src/components/progress.tsx +29 -0
- package/src/components/radio-group.stories.tsx +73 -0
- package/src/components/radio-group.test.tsx +74 -0
- package/src/components/radio-group.tsx +45 -0
- package/src/components/resizable.stories.tsx +120 -0
- package/src/components/resizable.tsx +54 -0
- package/src/components/scroll-area.stories.tsx +64 -0
- package/src/components/scroll-area.test.tsx +46 -0
- package/src/components/scroll-area.tsx +58 -0
- package/src/components/select.stories.tsx +111 -0
- package/src/components/select.test.tsx +90 -0
- package/src/components/select.tsx +188 -0
- package/src/components/separator.stories.tsx +76 -0
- package/src/components/separator.test.tsx +24 -0
- package/src/components/separator.tsx +28 -0
- package/src/components/sheet.stories.tsx +122 -0
- package/src/components/sheet.tsx +137 -0
- package/src/components/sidebar.tsx +726 -0
- package/src/components/skeleton.stories.tsx +53 -0
- package/src/components/skeleton.test.tsx +24 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/slider.stories.tsx +97 -0
- package/src/components/slider.test.tsx +49 -0
- package/src/components/slider.tsx +63 -0
- package/src/components/sonner.stories.tsx +96 -0
- package/src/components/sonner.tsx +38 -0
- package/src/components/spinner.stories.tsx +54 -0
- package/src/components/spinner.test.tsx +30 -0
- package/src/components/spinner.tsx +16 -0
- package/src/components/switch.stories.tsx +108 -0
- package/src/components/switch.test.tsx +62 -0
- package/src/components/switch.tsx +31 -0
- package/src/components/table.stories.tsx +139 -0
- package/src/components/table.test.tsx +85 -0
- package/src/components/table.tsx +114 -0
- package/src/components/tabs.stories.tsx +99 -0
- package/src/components/tabs.test.tsx +64 -0
- package/src/components/tabs.tsx +66 -0
- package/src/components/textarea.stories.tsx +89 -0
- package/src/components/textarea.test.tsx +53 -0
- package/src/components/textarea.tsx +18 -0
- package/src/components/toggle-group.stories.tsx +108 -0
- package/src/components/toggle-group.test.tsx +66 -0
- package/src/components/toggle-group.tsx +81 -0
- package/src/components/toggle.stories.tsx +98 -0
- package/src/components/toggle.test.tsx +42 -0
- package/src/components/toggle.tsx +45 -0
- package/src/components/tooltip.stories.tsx +111 -0
- package/src/components/tooltip.tsx +61 -0
- package/src/foundations/README.md +141 -0
- package/src/foundations/ThemeProvider.tsx +77 -0
- package/src/foundations/color.css +232 -0
- package/src/foundations/color.stories.tsx +719 -0
- package/src/foundations/palette.css +249 -0
- package/src/foundations/spacing.css +8 -0
- package/src/foundations/typography.css +143 -0
- package/src/foundations/typography.stories.tsx +17 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.css +176 -0
- package/src/index.ts +336 -0
- package/src/lib/utils.ts +6 -0
- package/src/test/setup.ts +8 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +33 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +25 -0
- package/vite.config.ts +30 -0
- package/vitest.config.ts +25 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Switch } from "./switch";
|
|
5
|
+
|
|
6
|
+
describe("Switch", () => {
|
|
7
|
+
it("should render switch", () => {
|
|
8
|
+
const { container } = render(<Switch />);
|
|
9
|
+
const switchElement = container.querySelector('[data-slot="switch"]');
|
|
10
|
+
expect(switchElement).toBeInTheDocument();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should be checked when checked prop is true", () => {
|
|
14
|
+
const { container } = render(<Switch checked />);
|
|
15
|
+
const switchElement = container.querySelector('[data-slot="switch"]');
|
|
16
|
+
expect(switchElement).toHaveAttribute("data-state", "checked");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should be unchecked by default", () => {
|
|
20
|
+
const { container } = render(<Switch />);
|
|
21
|
+
const switchElement = container.querySelector('[data-slot="switch"]');
|
|
22
|
+
expect(switchElement).toHaveAttribute("data-state", "unchecked");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should call onCheckedChange when clicked", async () => {
|
|
26
|
+
const user = userEvent.setup();
|
|
27
|
+
const handleChange = vi.fn();
|
|
28
|
+
const { container } = render(<Switch onCheckedChange={handleChange} />);
|
|
29
|
+
const switchElement = container.querySelector('[data-slot="switch"]') as HTMLElement;
|
|
30
|
+
await user.click(switchElement);
|
|
31
|
+
expect(handleChange).toHaveBeenCalledWith(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should toggle checked state when clicked", async () => {
|
|
35
|
+
const user = userEvent.setup();
|
|
36
|
+
const handleChange = vi.fn();
|
|
37
|
+
const { container } = render(
|
|
38
|
+
<Switch checked={false} onCheckedChange={handleChange} />
|
|
39
|
+
);
|
|
40
|
+
const switchElement = container.querySelector('[data-slot="switch"]') as HTMLElement;
|
|
41
|
+
await user.click(switchElement);
|
|
42
|
+
expect(handleChange).toHaveBeenCalledWith(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should be disabled when disabled prop is true", () => {
|
|
46
|
+
const { container } = render(<Switch disabled />);
|
|
47
|
+
const switchElement = container.querySelector('[data-slot="switch"]');
|
|
48
|
+
expect(switchElement).toHaveAttribute("data-disabled");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should not call onCheckedChange when disabled", async () => {
|
|
52
|
+
const user = userEvent.setup();
|
|
53
|
+
const handleChange = vi.fn();
|
|
54
|
+
const { container } = render(
|
|
55
|
+
<Switch disabled onCheckedChange={handleChange} />
|
|
56
|
+
);
|
|
57
|
+
const switchElement = container.querySelector('[data-slot="switch"]') as HTMLElement;
|
|
58
|
+
await user.click(switchElement);
|
|
59
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function Switch({
|
|
9
|
+
className,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<SwitchPrimitive.Root
|
|
14
|
+
data-slot="switch"
|
|
15
|
+
className={cn(
|
|
16
|
+
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
<SwitchPrimitive.Thumb
|
|
22
|
+
data-slot="switch-thumb"
|
|
23
|
+
className={cn(
|
|
24
|
+
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
|
25
|
+
)}
|
|
26
|
+
/>
|
|
27
|
+
</SwitchPrimitive.Root>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { Switch }
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import {
|
|
3
|
+
Table,
|
|
4
|
+
TableBody,
|
|
5
|
+
TableCaption,
|
|
6
|
+
TableCell,
|
|
7
|
+
TableFooter,
|
|
8
|
+
TableHead,
|
|
9
|
+
TableHeader,
|
|
10
|
+
TableRow,
|
|
11
|
+
} from "./table";
|
|
12
|
+
|
|
13
|
+
const meta = {
|
|
14
|
+
title: "Components/Table",
|
|
15
|
+
component: Table,
|
|
16
|
+
parameters: {
|
|
17
|
+
layout: "padded",
|
|
18
|
+
},
|
|
19
|
+
tags: ["autodocs"],
|
|
20
|
+
} satisfies Meta<typeof Table>;
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof meta>;
|
|
24
|
+
|
|
25
|
+
export const Default: Story = {
|
|
26
|
+
render: () => (
|
|
27
|
+
<Table>
|
|
28
|
+
<TableCaption>A list of your recent invoices.</TableCaption>
|
|
29
|
+
<TableHeader>
|
|
30
|
+
<TableRow>
|
|
31
|
+
<TableHead className="w-[100px]">Invoice</TableHead>
|
|
32
|
+
<TableHead>Status</TableHead>
|
|
33
|
+
<TableHead>Method</TableHead>
|
|
34
|
+
<TableHead className="text-right">Amount</TableHead>
|
|
35
|
+
</TableRow>
|
|
36
|
+
</TableHeader>
|
|
37
|
+
<TableBody>
|
|
38
|
+
<TableRow>
|
|
39
|
+
<TableCell className="font-medium">INV001</TableCell>
|
|
40
|
+
<TableCell>Paid</TableCell>
|
|
41
|
+
<TableCell>Credit Card</TableCell>
|
|
42
|
+
<TableCell className="text-right">$250.00</TableCell>
|
|
43
|
+
</TableRow>
|
|
44
|
+
<TableRow>
|
|
45
|
+
<TableCell className="font-medium">INV002</TableCell>
|
|
46
|
+
<TableCell>Pending</TableCell>
|
|
47
|
+
<TableCell>PayPal</TableCell>
|
|
48
|
+
<TableCell className="text-right">$150.00</TableCell>
|
|
49
|
+
</TableRow>
|
|
50
|
+
<TableRow>
|
|
51
|
+
<TableCell className="font-medium">INV003</TableCell>
|
|
52
|
+
<TableCell>Unpaid</TableCell>
|
|
53
|
+
<TableCell>Bank Transfer</TableCell>
|
|
54
|
+
<TableCell className="text-right">$350.00</TableCell>
|
|
55
|
+
</TableRow>
|
|
56
|
+
<TableRow>
|
|
57
|
+
<TableCell className="font-medium">INV004</TableCell>
|
|
58
|
+
<TableCell>Paid</TableCell>
|
|
59
|
+
<TableCell>Credit Card</TableCell>
|
|
60
|
+
<TableCell className="text-right">$450.00</TableCell>
|
|
61
|
+
</TableRow>
|
|
62
|
+
</TableBody>
|
|
63
|
+
<TableFooter>
|
|
64
|
+
<TableRow>
|
|
65
|
+
<TableCell colSpan={3}>Total</TableCell>
|
|
66
|
+
<TableCell className="text-right">$1,200.00</TableCell>
|
|
67
|
+
</TableRow>
|
|
68
|
+
</TableFooter>
|
|
69
|
+
</Table>
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const Simple: Story = {
|
|
74
|
+
render: () => (
|
|
75
|
+
<Table>
|
|
76
|
+
<TableHeader>
|
|
77
|
+
<TableRow>
|
|
78
|
+
<TableHead>Name</TableHead>
|
|
79
|
+
<TableHead>Email</TableHead>
|
|
80
|
+
<TableHead>Role</TableHead>
|
|
81
|
+
</TableRow>
|
|
82
|
+
</TableHeader>
|
|
83
|
+
<TableBody>
|
|
84
|
+
<TableRow>
|
|
85
|
+
<TableCell>John Doe</TableCell>
|
|
86
|
+
<TableCell>john@example.com</TableCell>
|
|
87
|
+
<TableCell>Admin</TableCell>
|
|
88
|
+
</TableRow>
|
|
89
|
+
<TableRow>
|
|
90
|
+
<TableCell>Jane Smith</TableCell>
|
|
91
|
+
<TableCell>jane@example.com</TableCell>
|
|
92
|
+
<TableCell>User</TableCell>
|
|
93
|
+
</TableRow>
|
|
94
|
+
<TableRow>
|
|
95
|
+
<TableCell>Bob Johnson</TableCell>
|
|
96
|
+
<TableCell>bob@example.com</TableCell>
|
|
97
|
+
<TableCell>User</TableCell>
|
|
98
|
+
</TableRow>
|
|
99
|
+
</TableBody>
|
|
100
|
+
</Table>
|
|
101
|
+
),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const WithoutFooter: Story = {
|
|
105
|
+
render: () => (
|
|
106
|
+
<Table>
|
|
107
|
+
<TableCaption>Product inventory</TableCaption>
|
|
108
|
+
<TableHeader>
|
|
109
|
+
<TableRow>
|
|
110
|
+
<TableHead>Product</TableHead>
|
|
111
|
+
<TableHead>Category</TableHead>
|
|
112
|
+
<TableHead>Stock</TableHead>
|
|
113
|
+
<TableHead className="text-right">Price</TableHead>
|
|
114
|
+
</TableRow>
|
|
115
|
+
</TableHeader>
|
|
116
|
+
<TableBody>
|
|
117
|
+
<TableRow>
|
|
118
|
+
<TableCell className="font-medium">Laptop</TableCell>
|
|
119
|
+
<TableCell>Electronics</TableCell>
|
|
120
|
+
<TableCell>15</TableCell>
|
|
121
|
+
<TableCell className="text-right">$999.00</TableCell>
|
|
122
|
+
</TableRow>
|
|
123
|
+
<TableRow>
|
|
124
|
+
<TableCell className="font-medium">Mouse</TableCell>
|
|
125
|
+
<TableCell>Accessories</TableCell>
|
|
126
|
+
<TableCell>50</TableCell>
|
|
127
|
+
<TableCell className="text-right">$29.99</TableCell>
|
|
128
|
+
</TableRow>
|
|
129
|
+
<TableRow>
|
|
130
|
+
<TableCell className="font-medium">Keyboard</TableCell>
|
|
131
|
+
<TableCell>Accessories</TableCell>
|
|
132
|
+
<TableCell>30</TableCell>
|
|
133
|
+
<TableCell className="text-right">$79.99</TableCell>
|
|
134
|
+
</TableRow>
|
|
135
|
+
</TableBody>
|
|
136
|
+
</Table>
|
|
137
|
+
),
|
|
138
|
+
};
|
|
139
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import {
|
|
4
|
+
Table,
|
|
5
|
+
TableHeader,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableRow,
|
|
8
|
+
TableHead,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableFooter,
|
|
11
|
+
TableCaption,
|
|
12
|
+
} from "./table";
|
|
13
|
+
|
|
14
|
+
describe("Table", () => {
|
|
15
|
+
it("should render table", () => {
|
|
16
|
+
const { container } = render(
|
|
17
|
+
<Table>
|
|
18
|
+
<TableBody>
|
|
19
|
+
<TableRow>
|
|
20
|
+
<TableCell>Cell</TableCell>
|
|
21
|
+
</TableRow>
|
|
22
|
+
</TableBody>
|
|
23
|
+
</Table>
|
|
24
|
+
);
|
|
25
|
+
const table = container.querySelector('[data-slot="table"]');
|
|
26
|
+
expect(table).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should render table header", () => {
|
|
30
|
+
const { container } = render(
|
|
31
|
+
<Table>
|
|
32
|
+
<TableHeader>
|
|
33
|
+
<TableRow>
|
|
34
|
+
<TableHead>Header</TableHead>
|
|
35
|
+
</TableRow>
|
|
36
|
+
</TableHeader>
|
|
37
|
+
</Table>
|
|
38
|
+
);
|
|
39
|
+
const header = container.querySelector('[data-slot="table-header"]');
|
|
40
|
+
expect(header).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should render table body", () => {
|
|
44
|
+
const { container } = render(
|
|
45
|
+
<Table>
|
|
46
|
+
<TableBody>
|
|
47
|
+
<TableRow>
|
|
48
|
+
<TableCell>Cell</TableCell>
|
|
49
|
+
</TableRow>
|
|
50
|
+
</TableBody>
|
|
51
|
+
</Table>
|
|
52
|
+
);
|
|
53
|
+
const body = container.querySelector('[data-slot="table-body"]');
|
|
54
|
+
expect(body).toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should render table footer", () => {
|
|
58
|
+
const { container } = render(
|
|
59
|
+
<Table>
|
|
60
|
+
<TableFooter>
|
|
61
|
+
<TableRow>
|
|
62
|
+
<TableCell>Footer</TableCell>
|
|
63
|
+
</TableRow>
|
|
64
|
+
</TableFooter>
|
|
65
|
+
</Table>
|
|
66
|
+
);
|
|
67
|
+
const footer = container.querySelector('[data-slot="table-footer"]');
|
|
68
|
+
expect(footer).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should render table caption", () => {
|
|
72
|
+
const { getByText } = render(
|
|
73
|
+
<Table>
|
|
74
|
+
<TableCaption>Table caption</TableCaption>
|
|
75
|
+
<TableBody>
|
|
76
|
+
<TableRow>
|
|
77
|
+
<TableCell>Cell</TableCell>
|
|
78
|
+
</TableRow>
|
|
79
|
+
</TableBody>
|
|
80
|
+
</Table>
|
|
81
|
+
);
|
|
82
|
+
expect(getByText("Table caption")).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="table-container"
|
|
9
|
+
className="relative w-full overflow-x-auto"
|
|
10
|
+
>
|
|
11
|
+
<table
|
|
12
|
+
data-slot="table"
|
|
13
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|
21
|
+
return (
|
|
22
|
+
<thead
|
|
23
|
+
data-slot="table-header"
|
|
24
|
+
className={cn("[&_tr]:border-b", className)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|
31
|
+
return (
|
|
32
|
+
<tbody
|
|
33
|
+
data-slot="table-body"
|
|
34
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|
41
|
+
return (
|
|
42
|
+
<tfoot
|
|
43
|
+
data-slot="table-footer"
|
|
44
|
+
className={cn(
|
|
45
|
+
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
54
|
+
return (
|
|
55
|
+
<tr
|
|
56
|
+
data-slot="table-row"
|
|
57
|
+
className={cn(
|
|
58
|
+
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
67
|
+
return (
|
|
68
|
+
<th
|
|
69
|
+
data-slot="table-head"
|
|
70
|
+
className={cn(
|
|
71
|
+
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
72
|
+
className
|
|
73
|
+
)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
80
|
+
return (
|
|
81
|
+
<td
|
|
82
|
+
data-slot="table-cell"
|
|
83
|
+
className={cn(
|
|
84
|
+
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
85
|
+
className
|
|
86
|
+
)}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function TableCaption({
|
|
93
|
+
className,
|
|
94
|
+
...props
|
|
95
|
+
}: React.ComponentProps<"caption">) {
|
|
96
|
+
return (
|
|
97
|
+
<caption
|
|
98
|
+
data-slot="table-caption"
|
|
99
|
+
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
Table,
|
|
107
|
+
TableHeader,
|
|
108
|
+
TableBody,
|
|
109
|
+
TableFooter,
|
|
110
|
+
TableHead,
|
|
111
|
+
TableRow,
|
|
112
|
+
TableCell,
|
|
113
|
+
TableCaption,
|
|
114
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Components/Tabs",
|
|
6
|
+
component: Tabs,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "padded",
|
|
9
|
+
},
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
} satisfies Meta<typeof Tabs>;
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof meta>;
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<Tabs defaultValue="account" className="w-[400px]">
|
|
19
|
+
<TabsList>
|
|
20
|
+
<TabsTrigger value="account">Account</TabsTrigger>
|
|
21
|
+
<TabsTrigger value="password">Password</TabsTrigger>
|
|
22
|
+
</TabsList>
|
|
23
|
+
<TabsContent value="account">
|
|
24
|
+
Make changes to your account here.
|
|
25
|
+
</TabsContent>
|
|
26
|
+
<TabsContent value="password">Change your password here.</TabsContent>
|
|
27
|
+
</Tabs>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const MultipleTabs: Story = {
|
|
32
|
+
render: () => (
|
|
33
|
+
<Tabs defaultValue="overview" className="w-[500px]">
|
|
34
|
+
<TabsList>
|
|
35
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
36
|
+
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
|
37
|
+
<TabsTrigger value="reports">Reports</TabsTrigger>
|
|
38
|
+
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
|
39
|
+
</TabsList>
|
|
40
|
+
<TabsContent value="overview">
|
|
41
|
+
<div className="space-y-2">
|
|
42
|
+
<h3 className="text-lg font-semibold">Overview</h3>
|
|
43
|
+
<p>View your account overview and recent activity.</p>
|
|
44
|
+
</div>
|
|
45
|
+
</TabsContent>
|
|
46
|
+
<TabsContent value="analytics">
|
|
47
|
+
<div className="space-y-2">
|
|
48
|
+
<h3 className="text-lg font-semibold">Analytics</h3>
|
|
49
|
+
<p>View detailed analytics and metrics.</p>
|
|
50
|
+
</div>
|
|
51
|
+
</TabsContent>
|
|
52
|
+
<TabsContent value="reports">
|
|
53
|
+
<div className="space-y-2">
|
|
54
|
+
<h3 className="text-lg font-semibold">Reports</h3>
|
|
55
|
+
<p>Generate and view reports.</p>
|
|
56
|
+
</div>
|
|
57
|
+
</TabsContent>
|
|
58
|
+
<TabsContent value="notifications">
|
|
59
|
+
<div className="space-y-2">
|
|
60
|
+
<h3 className="text-lg font-semibold">Notifications</h3>
|
|
61
|
+
<p>Manage your notification settings.</p>
|
|
62
|
+
</div>
|
|
63
|
+
</TabsContent>
|
|
64
|
+
</Tabs>
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const LongContent: Story = {
|
|
69
|
+
render: () => (
|
|
70
|
+
<Tabs defaultValue="tab1" className="w-[500px]">
|
|
71
|
+
<TabsList>
|
|
72
|
+
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
|
73
|
+
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
|
|
74
|
+
</TabsList>
|
|
75
|
+
<TabsContent value="tab1" className="space-y-4">
|
|
76
|
+
<h3 className="text-lg font-semibold">Tab 1 Content</h3>
|
|
77
|
+
<p>
|
|
78
|
+
This is a longer content section that demonstrates how tabs handle
|
|
79
|
+
extended content. The content area will expand to accommodate the
|
|
80
|
+
full text.
|
|
81
|
+
</p>
|
|
82
|
+
<p>
|
|
83
|
+
You can include multiple paragraphs, lists, or any other content
|
|
84
|
+
within a tab content area.
|
|
85
|
+
</p>
|
|
86
|
+
<ul className="list-disc list-inside space-y-1 ml-4">
|
|
87
|
+
<li>First item</li>
|
|
88
|
+
<li>Second item</li>
|
|
89
|
+
<li>Third item</li>
|
|
90
|
+
</ul>
|
|
91
|
+
</TabsContent>
|
|
92
|
+
<TabsContent value="tab2">
|
|
93
|
+
<h3 className="text-lg font-semibold">Tab 2 Content</h3>
|
|
94
|
+
<p>This is the content for the second tab.</p>
|
|
95
|
+
</TabsContent>
|
|
96
|
+
</Tabs>
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs";
|
|
5
|
+
|
|
6
|
+
describe("Tabs", () => {
|
|
7
|
+
it("should render tabs", () => {
|
|
8
|
+
const { container } = render(
|
|
9
|
+
<Tabs>
|
|
10
|
+
<TabsList>
|
|
11
|
+
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
|
12
|
+
</TabsList>
|
|
13
|
+
<TabsContent value="tab1">Content 1</TabsContent>
|
|
14
|
+
</Tabs>
|
|
15
|
+
);
|
|
16
|
+
const tabs = container.querySelector('[data-slot="tabs"]');
|
|
17
|
+
expect(tabs).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should render tabs list", () => {
|
|
21
|
+
const { container } = render(
|
|
22
|
+
<Tabs>
|
|
23
|
+
<TabsList>
|
|
24
|
+
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
|
25
|
+
</TabsList>
|
|
26
|
+
<TabsContent value="tab1">Content 1</TabsContent>
|
|
27
|
+
</Tabs>
|
|
28
|
+
);
|
|
29
|
+
const tabsList = container.querySelector('[data-slot="tabs-list"]');
|
|
30
|
+
expect(tabsList).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should display content for active tab", () => {
|
|
34
|
+
const { getByText } = render(
|
|
35
|
+
<Tabs defaultValue="tab1">
|
|
36
|
+
<TabsList>
|
|
37
|
+
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
|
38
|
+
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
|
|
39
|
+
</TabsList>
|
|
40
|
+
<TabsContent value="tab1">Content 1</TabsContent>
|
|
41
|
+
<TabsContent value="tab2">Content 2</TabsContent>
|
|
42
|
+
</Tabs>
|
|
43
|
+
);
|
|
44
|
+
expect(getByText("Content 1")).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should switch tabs when trigger is clicked", async () => {
|
|
48
|
+
const user = userEvent.setup();
|
|
49
|
+
const { getByText, queryByText } = render(
|
|
50
|
+
<Tabs defaultValue="tab1">
|
|
51
|
+
<TabsList>
|
|
52
|
+
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
|
53
|
+
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
|
|
54
|
+
</TabsList>
|
|
55
|
+
<TabsContent value="tab1">Content 1</TabsContent>
|
|
56
|
+
<TabsContent value="tab2">Content 2</TabsContent>
|
|
57
|
+
</Tabs>
|
|
58
|
+
);
|
|
59
|
+
const tab2 = getByText("Tab 2");
|
|
60
|
+
await user.click(tab2);
|
|
61
|
+
expect(getByText("Content 2")).toBeInTheDocument();
|
|
62
|
+
expect(queryByText("Content 1")).not.toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function Tabs({
|
|
9
|
+
className,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<TabsPrimitive.Root
|
|
14
|
+
data-slot="tabs"
|
|
15
|
+
className={cn("flex flex-col gap-2", className)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function TabsList({
|
|
22
|
+
className,
|
|
23
|
+
...props
|
|
24
|
+
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
|
25
|
+
return (
|
|
26
|
+
<TabsPrimitive.List
|
|
27
|
+
data-slot="tabs-list"
|
|
28
|
+
className={cn(
|
|
29
|
+
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function TabsTrigger({
|
|
38
|
+
className,
|
|
39
|
+
...props
|
|
40
|
+
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
41
|
+
return (
|
|
42
|
+
<TabsPrimitive.Trigger
|
|
43
|
+
data-slot="tabs-trigger"
|
|
44
|
+
className={cn(
|
|
45
|
+
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function TabsContent({
|
|
54
|
+
className,
|
|
55
|
+
...props
|
|
56
|
+
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
57
|
+
return (
|
|
58
|
+
<TabsPrimitive.Content
|
|
59
|
+
data-slot="tabs-content"
|
|
60
|
+
className={cn("flex-1 outline-none", className)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent }
|