@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.
Files changed (160) hide show
  1. package/.husky/pre-push +21 -0
  2. package/.storybook/main.ts +11 -0
  3. package/.storybook/preview.tsx +30 -0
  4. package/.vscode/settings.json +12 -0
  5. package/.vscode/tailwind.json +105 -0
  6. package/README.md +136 -0
  7. package/bitbucket-pipelines.yml +52 -0
  8. package/components.json +21 -0
  9. package/eslint.config.js +38 -0
  10. package/package.json +98 -0
  11. package/public/vite.svg +1 -0
  12. package/src/components/accordion.stories.tsx +258 -0
  13. package/src/components/accordion.test.tsx +390 -0
  14. package/src/components/accordion.tsx +64 -0
  15. package/src/components/alert-dialog.stories.tsx +213 -0
  16. package/src/components/alert-dialog.test.tsx +80 -0
  17. package/src/components/alert-dialog.tsx +155 -0
  18. package/src/components/alert.stories.tsx +84 -0
  19. package/src/components/alert.test.tsx +35 -0
  20. package/src/components/alert.tsx +66 -0
  21. package/src/components/aspect-ratio.stories.tsx +97 -0
  22. package/src/components/aspect-ratio.test.tsx +47 -0
  23. package/src/components/aspect-ratio.tsx +11 -0
  24. package/src/components/avatar.stories.tsx +76 -0
  25. package/src/components/avatar.test.tsx +50 -0
  26. package/src/components/avatar.tsx +51 -0
  27. package/src/components/badge.stories.tsx +64 -0
  28. package/src/components/badge.test.tsx +34 -0
  29. package/src/components/badge.tsx +46 -0
  30. package/src/components/breadcrumb.stories.tsx +86 -0
  31. package/src/components/breadcrumb.test.tsx +74 -0
  32. package/src/components/breadcrumb.tsx +109 -0
  33. package/src/components/button-group.stories.tsx +62 -0
  34. package/src/components/button-group.tsx +83 -0
  35. package/src/components/button.stories.tsx +118 -0
  36. package/src/components/button.test.tsx +64 -0
  37. package/src/components/button.tsx +62 -0
  38. package/src/components/calendar.stories.tsx +81 -0
  39. package/src/components/calendar.tsx +220 -0
  40. package/src/components/card.stories.tsx +110 -0
  41. package/src/components/card.test.tsx +56 -0
  42. package/src/components/card.tsx +92 -0
  43. package/src/components/carousel.stories.tsx +90 -0
  44. package/src/components/carousel.tsx +239 -0
  45. package/src/components/chart.tsx +357 -0
  46. package/src/components/checkbox.stories.tsx +108 -0
  47. package/src/components/checkbox.test.tsx +67 -0
  48. package/src/components/checkbox.tsx +32 -0
  49. package/src/components/collapsible.stories.tsx +106 -0
  50. package/src/components/collapsible.test.tsx +92 -0
  51. package/src/components/collapsible.tsx +31 -0
  52. package/src/components/command.stories.tsx +90 -0
  53. package/src/components/command.tsx +182 -0
  54. package/src/components/context-menu.stories.tsx +63 -0
  55. package/src/components/context-menu.tsx +252 -0
  56. package/src/components/dialog.stories.tsx +128 -0
  57. package/src/components/dialog.tsx +141 -0
  58. package/src/components/drawer.stories.tsx +104 -0
  59. package/src/components/drawer.tsx +135 -0
  60. package/src/components/dropdown-menu.stories.tsx +97 -0
  61. package/src/components/dropdown-menu.tsx +255 -0
  62. package/src/components/empty.stories.tsx +90 -0
  63. package/src/components/empty.test.tsx +55 -0
  64. package/src/components/empty.tsx +104 -0
  65. package/src/components/field.tsx +246 -0
  66. package/src/components/form.tsx +168 -0
  67. package/src/components/hover-card.stories.tsx +66 -0
  68. package/src/components/hover-card.tsx +44 -0
  69. package/src/components/input-group.stories.tsx +57 -0
  70. package/src/components/input-group.test.tsx +40 -0
  71. package/src/components/input-group.tsx +170 -0
  72. package/src/components/input-otp.stories.tsx +94 -0
  73. package/src/components/input-otp.test.tsx +60 -0
  74. package/src/components/input-otp.tsx +75 -0
  75. package/src/components/input.stories.tsx +94 -0
  76. package/src/components/input.test.tsx +53 -0
  77. package/src/components/input.tsx +21 -0
  78. package/src/components/item.tsx +193 -0
  79. package/src/components/kbd.stories.tsx +100 -0
  80. package/src/components/kbd.test.tsx +28 -0
  81. package/src/components/kbd.tsx +28 -0
  82. package/src/components/label.stories.tsx +48 -0
  83. package/src/components/label.test.tsx +28 -0
  84. package/src/components/label.tsx +24 -0
  85. package/src/components/menubar.tsx +274 -0
  86. package/src/components/navigation-menu.tsx +168 -0
  87. package/src/components/pagination.stories.tsx +107 -0
  88. package/src/components/pagination.tsx +127 -0
  89. package/src/components/popover.stories.tsx +102 -0
  90. package/src/components/popover.tsx +48 -0
  91. package/src/components/progress.stories.tsx +76 -0
  92. package/src/components/progress.test.tsx +36 -0
  93. package/src/components/progress.tsx +29 -0
  94. package/src/components/radio-group.stories.tsx +73 -0
  95. package/src/components/radio-group.test.tsx +74 -0
  96. package/src/components/radio-group.tsx +45 -0
  97. package/src/components/resizable.stories.tsx +120 -0
  98. package/src/components/resizable.tsx +54 -0
  99. package/src/components/scroll-area.stories.tsx +64 -0
  100. package/src/components/scroll-area.test.tsx +46 -0
  101. package/src/components/scroll-area.tsx +58 -0
  102. package/src/components/select.stories.tsx +111 -0
  103. package/src/components/select.test.tsx +90 -0
  104. package/src/components/select.tsx +188 -0
  105. package/src/components/separator.stories.tsx +76 -0
  106. package/src/components/separator.test.tsx +24 -0
  107. package/src/components/separator.tsx +28 -0
  108. package/src/components/sheet.stories.tsx +122 -0
  109. package/src/components/sheet.tsx +137 -0
  110. package/src/components/sidebar.tsx +726 -0
  111. package/src/components/skeleton.stories.tsx +53 -0
  112. package/src/components/skeleton.test.tsx +24 -0
  113. package/src/components/skeleton.tsx +13 -0
  114. package/src/components/slider.stories.tsx +97 -0
  115. package/src/components/slider.test.tsx +49 -0
  116. package/src/components/slider.tsx +63 -0
  117. package/src/components/sonner.stories.tsx +96 -0
  118. package/src/components/sonner.tsx +38 -0
  119. package/src/components/spinner.stories.tsx +54 -0
  120. package/src/components/spinner.test.tsx +30 -0
  121. package/src/components/spinner.tsx +16 -0
  122. package/src/components/switch.stories.tsx +108 -0
  123. package/src/components/switch.test.tsx +62 -0
  124. package/src/components/switch.tsx +31 -0
  125. package/src/components/table.stories.tsx +139 -0
  126. package/src/components/table.test.tsx +85 -0
  127. package/src/components/table.tsx +114 -0
  128. package/src/components/tabs.stories.tsx +99 -0
  129. package/src/components/tabs.test.tsx +64 -0
  130. package/src/components/tabs.tsx +66 -0
  131. package/src/components/textarea.stories.tsx +89 -0
  132. package/src/components/textarea.test.tsx +53 -0
  133. package/src/components/textarea.tsx +18 -0
  134. package/src/components/toggle-group.stories.tsx +108 -0
  135. package/src/components/toggle-group.test.tsx +66 -0
  136. package/src/components/toggle-group.tsx +81 -0
  137. package/src/components/toggle.stories.tsx +98 -0
  138. package/src/components/toggle.test.tsx +42 -0
  139. package/src/components/toggle.tsx +45 -0
  140. package/src/components/tooltip.stories.tsx +111 -0
  141. package/src/components/tooltip.tsx +61 -0
  142. package/src/foundations/README.md +141 -0
  143. package/src/foundations/ThemeProvider.tsx +77 -0
  144. package/src/foundations/color.css +232 -0
  145. package/src/foundations/color.stories.tsx +719 -0
  146. package/src/foundations/palette.css +249 -0
  147. package/src/foundations/spacing.css +8 -0
  148. package/src/foundations/typography.css +143 -0
  149. package/src/foundations/typography.stories.tsx +17 -0
  150. package/src/hooks/use-mobile.ts +19 -0
  151. package/src/index.css +176 -0
  152. package/src/index.ts +336 -0
  153. package/src/lib/utils.ts +6 -0
  154. package/src/test/setup.ts +8 -0
  155. package/src/vite-env.d.ts +1 -0
  156. package/tsconfig.app.json +33 -0
  157. package/tsconfig.json +13 -0
  158. package/tsconfig.node.json +25 -0
  159. package/vite.config.ts +30 -0
  160. 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 }