@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,102 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
3
+ import { Button } from "./button";
4
+
5
+ const meta = {
6
+ title: "Components/Popover",
7
+ component: Popover,
8
+ parameters: {
9
+ layout: "padded",
10
+ },
11
+ tags: ["autodocs"],
12
+ } satisfies Meta<typeof Popover>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ render: () => (
19
+ <Popover>
20
+ <PopoverTrigger asChild>
21
+ <Button variant="outline">Open popover</Button>
22
+ </PopoverTrigger>
23
+ <PopoverContent>
24
+ <div className="space-y-2">
25
+ <h4 className="font-medium leading-none">Dimensions</h4>
26
+ <p className="text-sm text-muted-foreground">
27
+ Set the dimensions for the layer.
28
+ </p>
29
+ </div>
30
+ </PopoverContent>
31
+ </Popover>
32
+ ),
33
+ };
34
+
35
+ export const WithForm: Story = {
36
+ render: () => (
37
+ <Popover>
38
+ <PopoverTrigger asChild>
39
+ <Button variant="outline">Open form</Button>
40
+ </PopoverTrigger>
41
+ <PopoverContent className="w-80">
42
+ <div className="space-y-4">
43
+ <div className="space-y-2">
44
+ <h4 className="font-medium leading-none">Dimensions</h4>
45
+ <p className="text-sm text-muted-foreground">
46
+ Set the dimensions for the layer.
47
+ </p>
48
+ </div>
49
+ <div className="grid gap-2">
50
+ <div className="grid grid-cols-3 items-center gap-4">
51
+ <label htmlFor="width" className="text-sm">
52
+ Width
53
+ </label>
54
+ <input
55
+ id="width"
56
+ defaultValue="100%"
57
+ className="col-span-2 h-8 rounded-md border px-2 text-sm"
58
+ />
59
+ </div>
60
+ <div className="grid grid-cols-3 items-center gap-4">
61
+ <label htmlFor="height" className="text-sm">
62
+ Height
63
+ </label>
64
+ <input
65
+ id="height"
66
+ defaultValue="25px"
67
+ className="col-span-2 h-8 rounded-md border px-2 text-sm"
68
+ />
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </PopoverContent>
73
+ </Popover>
74
+ ),
75
+ };
76
+
77
+ export const WithList: Story = {
78
+ render: () => (
79
+ <Popover>
80
+ <PopoverTrigger asChild>
81
+ <Button variant="outline">View options</Button>
82
+ </PopoverTrigger>
83
+ <PopoverContent className="w-56">
84
+ <div className="space-y-2">
85
+ <h4 className="font-medium">Options</h4>
86
+ <ul className="space-y-1 text-sm">
87
+ <li className="cursor-pointer hover:bg-accent p-2 rounded">
88
+ Option 1
89
+ </li>
90
+ <li className="cursor-pointer hover:bg-accent p-2 rounded">
91
+ Option 2
92
+ </li>
93
+ <li className="cursor-pointer hover:bg-accent p-2 rounded">
94
+ Option 3
95
+ </li>
96
+ </ul>
97
+ </div>
98
+ </PopoverContent>
99
+ </Popover>
100
+ ),
101
+ };
102
+
@@ -0,0 +1,48 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Popover({
9
+ ...props
10
+ }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
11
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />
12
+ }
13
+
14
+ function PopoverTrigger({
15
+ ...props
16
+ }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
17
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
18
+ }
19
+
20
+ function PopoverContent({
21
+ className,
22
+ align = "center",
23
+ sideOffset = 4,
24
+ ...props
25
+ }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
26
+ return (
27
+ <PopoverPrimitive.Portal>
28
+ <PopoverPrimitive.Content
29
+ data-slot="popover-content"
30
+ align={align}
31
+ sideOffset={sideOffset}
32
+ className={cn(
33
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
34
+ className
35
+ )}
36
+ {...props}
37
+ />
38
+ </PopoverPrimitive.Portal>
39
+ )
40
+ }
41
+
42
+ function PopoverAnchor({
43
+ ...props
44
+ }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
45
+ return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
46
+ }
47
+
48
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
@@ -0,0 +1,76 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Progress } from "./progress";
3
+
4
+ const meta = {
5
+ title: "Components/Progress",
6
+ component: Progress,
7
+ parameters: {
8
+ layout: "padded",
9
+ },
10
+ tags: ["autodocs"],
11
+ argTypes: {
12
+ value: {
13
+ control: { type: "range", min: 0, max: 100, step: 1 },
14
+ description: "The progress value (0-100).",
15
+ },
16
+ },
17
+ } satisfies Meta<typeof Progress>;
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ export const Default: Story = {
23
+ args: {
24
+ value: 33,
25
+ },
26
+ };
27
+
28
+ export const Values: Story = {
29
+ render: () => (
30
+ <div className="space-y-4 w-[350px]">
31
+ <div className="space-y-2">
32
+ <div className="flex justify-between text-sm">
33
+ <span>0%</span>
34
+ </div>
35
+ <Progress value={0} />
36
+ </div>
37
+ <div className="space-y-2">
38
+ <div className="flex justify-between text-sm">
39
+ <span>25%</span>
40
+ </div>
41
+ <Progress value={25} />
42
+ </div>
43
+ <div className="space-y-2">
44
+ <div className="flex justify-between text-sm">
45
+ <span>50%</span>
46
+ </div>
47
+ <Progress value={50} />
48
+ </div>
49
+ <div className="space-y-2">
50
+ <div className="flex justify-between text-sm">
51
+ <span>75%</span>
52
+ </div>
53
+ <Progress value={75} />
54
+ </div>
55
+ <div className="space-y-2">
56
+ <div className="flex justify-between text-sm">
57
+ <span>100%</span>
58
+ </div>
59
+ <Progress value={100} />
60
+ </div>
61
+ </div>
62
+ ),
63
+ };
64
+
65
+ export const WithLabel: Story = {
66
+ render: () => (
67
+ <div className="space-y-2 w-[350px]">
68
+ <div className="flex justify-between text-sm">
69
+ <span>Uploading...</span>
70
+ <span>45%</span>
71
+ </div>
72
+ <Progress value={45} />
73
+ </div>
74
+ ),
75
+ };
76
+
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import { Progress } from "./progress";
4
+
5
+ describe("Progress", () => {
6
+ it("should render progress", () => {
7
+ const { container } = render(<Progress value={50} />);
8
+ const progress = container.querySelector('[data-slot="progress"]');
9
+ expect(progress).toBeInTheDocument();
10
+ });
11
+
12
+ it("should render progress indicator", () => {
13
+ const { container } = render(<Progress value={50} />);
14
+ const indicator = container.querySelector('[data-slot="progress-indicator"]');
15
+ expect(indicator).toBeInTheDocument();
16
+ });
17
+
18
+ it("should apply value correctly", () => {
19
+ const { container } = render(<Progress value={75} />);
20
+ const indicator = container.querySelector('[data-slot="progress-indicator"]') as HTMLElement;
21
+ expect(indicator.style.transform).toContain("25%");
22
+ });
23
+
24
+ it("should handle 0 value", () => {
25
+ const { container } = render(<Progress value={0} />);
26
+ const indicator = container.querySelector('[data-slot="progress-indicator"]') as HTMLElement;
27
+ expect(indicator.style.transform).toContain("100%");
28
+ });
29
+
30
+ it("should handle 100 value", () => {
31
+ const { container } = render(<Progress value={100} />);
32
+ const indicator = container.querySelector('[data-slot="progress-indicator"]') as HTMLElement;
33
+ expect(indicator.style.transform).toContain("0%");
34
+ });
35
+ });
36
+
@@ -0,0 +1,29 @@
1
+ import * as React from "react"
2
+ import * as ProgressPrimitive from "@radix-ui/react-progress"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ function Progress({
7
+ className,
8
+ value,
9
+ ...props
10
+ }: React.ComponentProps<typeof ProgressPrimitive.Root>) {
11
+ return (
12
+ <ProgressPrimitive.Root
13
+ data-slot="progress"
14
+ className={cn(
15
+ "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
16
+ className
17
+ )}
18
+ {...props}
19
+ >
20
+ <ProgressPrimitive.Indicator
21
+ data-slot="progress-indicator"
22
+ className="bg-primary h-full w-full flex-1 transition-all"
23
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
24
+ />
25
+ </ProgressPrimitive.Root>
26
+ )
27
+ }
28
+
29
+ export { Progress }
@@ -0,0 +1,73 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { RadioGroup, RadioGroupItem } from "./radio-group";
3
+ import { Label } from "./label";
4
+
5
+ const meta = {
6
+ title: "Components/RadioGroup",
7
+ component: RadioGroup,
8
+ parameters: {
9
+ layout: "padded",
10
+ },
11
+ tags: ["autodocs"],
12
+ } satisfies Meta<typeof RadioGroup>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ render: () => (
19
+ <RadioGroup defaultValue="option-one">
20
+ <div className="flex items-center space-x-2">
21
+ <RadioGroupItem value="option-one" id="option-one" />
22
+ <Label htmlFor="option-one">Option One</Label>
23
+ </div>
24
+ <div className="flex items-center space-x-2">
25
+ <RadioGroupItem value="option-two" id="option-two" />
26
+ <Label htmlFor="option-two">Option Two</Label>
27
+ </div>
28
+ <div className="flex items-center space-x-2">
29
+ <RadioGroupItem value="option-three" id="option-three" />
30
+ <Label htmlFor="option-three">Option Three</Label>
31
+ </div>
32
+ </RadioGroup>
33
+ ),
34
+ };
35
+
36
+ export const Horizontal: Story = {
37
+ render: () => (
38
+ <RadioGroup defaultValue="option-one" className="flex gap-6">
39
+ <div className="flex items-center space-x-2">
40
+ <RadioGroupItem value="option-one" id="h-option-one" />
41
+ <Label htmlFor="h-option-one">Option One</Label>
42
+ </div>
43
+ <div className="flex items-center space-x-2">
44
+ <RadioGroupItem value="option-two" id="h-option-two" />
45
+ <Label htmlFor="h-option-two">Option Two</Label>
46
+ </div>
47
+ <div className="flex items-center space-x-2">
48
+ <RadioGroupItem value="option-three" id="h-option-three" />
49
+ <Label htmlFor="h-option-three">Option Three</Label>
50
+ </div>
51
+ </RadioGroup>
52
+ ),
53
+ };
54
+
55
+ export const Disabled: Story = {
56
+ render: () => (
57
+ <RadioGroup defaultValue="option-one">
58
+ <div className="flex items-center space-x-2">
59
+ <RadioGroupItem value="option-one" id="d-option-one" />
60
+ <Label htmlFor="d-option-one">Option One</Label>
61
+ </div>
62
+ <div className="flex items-center space-x-2">
63
+ <RadioGroupItem value="option-two" id="d-option-two" disabled />
64
+ <Label htmlFor="d-option-two">Option Two (Disabled)</Label>
65
+ </div>
66
+ <div className="flex items-center space-x-2">
67
+ <RadioGroupItem value="option-three" id="d-option-three" />
68
+ <Label htmlFor="d-option-three">Option Three</Label>
69
+ </div>
70
+ </RadioGroup>
71
+ ),
72
+ };
73
+
@@ -0,0 +1,74 @@
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 { RadioGroup, RadioGroupItem } from "./radio-group";
5
+
6
+ describe("RadioGroup", () => {
7
+ it("should render radio group with items", () => {
8
+ const { container } = render(
9
+ <RadioGroup>
10
+ <RadioGroupItem value="option1" id="option1" />
11
+ <RadioGroupItem value="option2" id="option2" />
12
+ </RadioGroup>
13
+ );
14
+ const radioGroup = container.querySelector('[data-slot="radio-group"]');
15
+ expect(radioGroup).toBeInTheDocument();
16
+ });
17
+
18
+ it("should have default value when defaultValue is provided", () => {
19
+ const { container } = render(
20
+ <RadioGroup defaultValue="option1">
21
+ <RadioGroupItem value="option1" id="option1" />
22
+ <RadioGroupItem value="option2" id="option2" />
23
+ </RadioGroup>
24
+ );
25
+ const option1 = container.querySelector('[value="option1"]') as HTMLInputElement;
26
+ // Radix UI radio group uses data attributes for state
27
+ expect(option1).toBeInTheDocument();
28
+ });
29
+
30
+ it("should call onValueChange when item is clicked", async () => {
31
+ const user = userEvent.setup();
32
+ const handleChange = vi.fn();
33
+ const { container } = render(
34
+ <RadioGroup onValueChange={handleChange}>
35
+ <RadioGroupItem value="option1" id="option1" />
36
+ <RadioGroupItem value="option2" id="option2" />
37
+ </RadioGroup>
38
+ );
39
+ const option2 = container.querySelector('[value="option2"]') as HTMLElement;
40
+ await user.click(option2);
41
+ expect(handleChange).toHaveBeenCalledWith("option2");
42
+ });
43
+
44
+ it("should only allow one item to be selected at a time", async () => {
45
+ const user = userEvent.setup();
46
+ const handleChange = vi.fn();
47
+ const { container } = render(
48
+ <RadioGroup onValueChange={handleChange}>
49
+ <RadioGroupItem value="option1" id="option1" />
50
+ <RadioGroupItem value="option2" id="option2" />
51
+ </RadioGroup>
52
+ );
53
+ const option1 = container.querySelector('[value="option1"]') as HTMLElement;
54
+ const option2 = container.querySelector('[value="option2"]') as HTMLElement;
55
+
56
+ await user.click(option1);
57
+ expect(handleChange).toHaveBeenCalledWith("option1");
58
+
59
+ await user.click(option2);
60
+ expect(handleChange).toHaveBeenCalledWith("option2");
61
+ });
62
+
63
+ it("should disable items when disabled prop is true", () => {
64
+ const { container } = render(
65
+ <RadioGroup disabled>
66
+ <RadioGroupItem value="option1" id="option1" />
67
+ <RadioGroupItem value="option2" id="option2" />
68
+ </RadioGroup>
69
+ );
70
+ const option1 = container.querySelector('[value="option1"]') as HTMLInputElement;
71
+ expect(option1).toBeDisabled();
72
+ });
73
+ });
74
+
@@ -0,0 +1,45 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5
+ import { CircleIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function RadioGroup({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
13
+ return (
14
+ <RadioGroupPrimitive.Root
15
+ data-slot="radio-group"
16
+ className={cn("grid gap-3", className)}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ function RadioGroupItem({
23
+ className,
24
+ ...props
25
+ }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
26
+ return (
27
+ <RadioGroupPrimitive.Item
28
+ data-slot="radio-group-item"
29
+ className={cn(
30
+ "border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
31
+ className
32
+ )}
33
+ {...props}
34
+ >
35
+ <RadioGroupPrimitive.Indicator
36
+ data-slot="radio-group-indicator"
37
+ className="relative flex items-center justify-center"
38
+ >
39
+ <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
40
+ </RadioGroupPrimitive.Indicator>
41
+ </RadioGroupPrimitive.Item>
42
+ )
43
+ }
44
+
45
+ export { RadioGroup, RadioGroupItem }
@@ -0,0 +1,120 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import {
3
+ ResizableHandle,
4
+ ResizablePanel,
5
+ ResizablePanelGroup,
6
+ } from "./resizable";
7
+
8
+ const meta = {
9
+ title: "Components/Resizable",
10
+ component: ResizablePanelGroup,
11
+ parameters: {
12
+ layout: "padded",
13
+ },
14
+ tags: ["autodocs"],
15
+ } satisfies Meta<typeof ResizablePanelGroup>;
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ export const Default: Story = {
21
+ args: {
22
+ direction: "horizontal",
23
+ },
24
+ render: () => (
25
+ <ResizablePanelGroup
26
+ direction="horizontal"
27
+ className="max-w-md rounded-lg border"
28
+ >
29
+ <ResizablePanel defaultSize={50}>
30
+ <div className="flex h-[200px] items-center justify-center p-6">
31
+ <span className="font-semibold">One</span>
32
+ </div>
33
+ </ResizablePanel>
34
+ <ResizableHandle />
35
+ <ResizablePanel defaultSize={50}>
36
+ <div className="flex h-[200px] items-center justify-center p-6">
37
+ <span className="font-semibold">Two</span>
38
+ </div>
39
+ </ResizablePanel>
40
+ </ResizablePanelGroup>
41
+ ),
42
+ };
43
+
44
+ export const Vertical: Story = {
45
+ args: {
46
+ direction: "vertical",
47
+ },
48
+ render: () => (
49
+ <ResizablePanelGroup
50
+ direction="vertical"
51
+ className="min-h-[200px] max-w-md rounded-lg border"
52
+ >
53
+ <ResizablePanel defaultSize={50}>
54
+ <div className="flex h-full items-center justify-center p-6">
55
+ <span className="font-semibold">One</span>
56
+ </div>
57
+ </ResizablePanel>
58
+ <ResizableHandle />
59
+ <ResizablePanel defaultSize={50}>
60
+ <div className="flex h-full items-center justify-center p-6">
61
+ <span className="font-semibold">Two</span>
62
+ </div>
63
+ </ResizablePanel>
64
+ </ResizablePanelGroup>
65
+ ),
66
+ };
67
+
68
+ export const ThreePanels: Story = {
69
+ args: {
70
+ direction: "horizontal",
71
+ },
72
+ render: () => (
73
+ <ResizablePanelGroup
74
+ direction="horizontal"
75
+ className="max-w-md rounded-lg border"
76
+ >
77
+ <ResizablePanel defaultSize={25}>
78
+ <div className="flex h-[200px] items-center justify-center p-6">
79
+ <span className="font-semibold">One</span>
80
+ </div>
81
+ </ResizablePanel>
82
+ <ResizableHandle />
83
+ <ResizablePanel defaultSize={50}>
84
+ <div className="flex h-[200px] items-center justify-center p-6">
85
+ <span className="font-semibold">Two</span>
86
+ </div>
87
+ </ResizablePanel>
88
+ <ResizableHandle />
89
+ <ResizablePanel defaultSize={25}>
90
+ <div className="flex h-[200px] items-center justify-center p-6">
91
+ <span className="font-semibold">Three</span>
92
+ </div>
93
+ </ResizablePanel>
94
+ </ResizablePanelGroup>
95
+ ),
96
+ };
97
+
98
+ export const WithHandle: Story = {
99
+ args: {
100
+ direction: "horizontal",
101
+ },
102
+ render: () => (
103
+ <ResizablePanelGroup
104
+ direction="horizontal"
105
+ className="max-w-md rounded-lg border"
106
+ >
107
+ <ResizablePanel defaultSize={50}>
108
+ <div className="flex h-[200px] items-center justify-center p-6">
109
+ <span className="font-semibold">One</span>
110
+ </div>
111
+ </ResizablePanel>
112
+ <ResizableHandle withHandle />
113
+ <ResizablePanel defaultSize={50}>
114
+ <div className="flex h-[200px] items-center justify-center p-6">
115
+ <span className="font-semibold">Two</span>
116
+ </div>
117
+ </ResizablePanel>
118
+ </ResizablePanelGroup>
119
+ ),
120
+ };
@@ -0,0 +1,54 @@
1
+ import * as React from "react"
2
+ import { GripVerticalIcon } from "lucide-react"
3
+ import * as ResizablePrimitive from "react-resizable-panels"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function ResizablePanelGroup({
8
+ className,
9
+ ...props
10
+ }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
11
+ return (
12
+ <ResizablePrimitive.PanelGroup
13
+ data-slot="resizable-panel-group"
14
+ className={cn(
15
+ "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function ResizablePanel({
24
+ ...props
25
+ }: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
26
+ return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
27
+ }
28
+
29
+ function ResizableHandle({
30
+ withHandle,
31
+ className,
32
+ ...props
33
+ }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
34
+ withHandle?: boolean
35
+ }) {
36
+ return (
37
+ <ResizablePrimitive.PanelResizeHandle
38
+ data-slot="resizable-handle"
39
+ className={cn(
40
+ "bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
41
+ className
42
+ )}
43
+ {...props}
44
+ >
45
+ {withHandle && (
46
+ <div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
47
+ <GripVerticalIcon className="size-2.5" />
48
+ </div>
49
+ )}
50
+ </ResizablePrimitive.PanelResizeHandle>
51
+ )
52
+ }
53
+
54
+ export { ResizablePanelGroup, ResizablePanel, ResizableHandle }