@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,53 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Skeleton } from "./skeleton";
3
+
4
+ const meta = {
5
+ title: "Components/Skeleton",
6
+ component: Skeleton,
7
+ parameters: {
8
+ layout: "padded",
9
+ },
10
+ tags: ["autodocs"],
11
+ } satisfies Meta<typeof Skeleton>;
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ export const Default: Story = {
17
+ render: () => <Skeleton className="h-4 w-[250px]" />,
18
+ };
19
+
20
+ export const Shapes: Story = {
21
+ render: () => (
22
+ <div className="space-y-4">
23
+ <Skeleton className="h-4 w-[250px]" />
24
+ <Skeleton className="h-4 w-[200px]" />
25
+ <Skeleton className="h-4 w-[150px]" />
26
+ </div>
27
+ ),
28
+ };
29
+
30
+ export const CardSkeleton: Story = {
31
+ render: () => (
32
+ <div className="flex items-center space-x-4 w-[350px]">
33
+ <Skeleton className="h-12 w-12 rounded-full" />
34
+ <div className="space-y-2 flex-1">
35
+ <Skeleton className="h-4 w-full" />
36
+ <Skeleton className="h-4 w-3/4" />
37
+ </div>
38
+ </div>
39
+ ),
40
+ };
41
+
42
+ export const ArticleSkeleton: Story = {
43
+ render: () => (
44
+ <div className="space-y-4 w-[400px]">
45
+ <Skeleton className="h-8 w-3/4" />
46
+ <Skeleton className="h-4 w-full" />
47
+ <Skeleton className="h-4 w-full" />
48
+ <Skeleton className="h-4 w-5/6" />
49
+ <Skeleton className="h-[200px] w-full rounded-md" />
50
+ </div>
51
+ ),
52
+ };
53
+
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import { Skeleton } from "./skeleton";
4
+
5
+ describe("Skeleton", () => {
6
+ it("should render skeleton", () => {
7
+ const { container } = render(<Skeleton />);
8
+ const skeleton = container.querySelector('[data-slot="skeleton"]');
9
+ expect(skeleton).toBeInTheDocument();
10
+ });
11
+
12
+ it("should apply custom className", () => {
13
+ const { container } = render(<Skeleton className="w-20 h-20" />);
14
+ const skeleton = container.querySelector('[data-slot="skeleton"]');
15
+ expect(skeleton).toHaveClass("w-20", "h-20");
16
+ });
17
+
18
+ it("should have animate-pulse class", () => {
19
+ const { container } = render(<Skeleton />);
20
+ const skeleton = container.querySelector('[data-slot="skeleton"]');
21
+ expect(skeleton).toHaveClass("animate-pulse");
22
+ });
23
+ });
24
+
@@ -0,0 +1,13 @@
1
+ import { cn } from "@/lib/utils"
2
+
3
+ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4
+ return (
5
+ <div
6
+ data-slot="skeleton"
7
+ className={cn("bg-accent animate-pulse rounded-md", className)}
8
+ {...props}
9
+ />
10
+ )
11
+ }
12
+
13
+ export { Skeleton }
@@ -0,0 +1,97 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useState } from "react";
3
+ import { Slider } from "./slider";
4
+
5
+ const meta = {
6
+ title: "Components/Slider",
7
+ component: Slider,
8
+ parameters: {
9
+ layout: "padded",
10
+ },
11
+ tags: ["autodocs"],
12
+ argTypes: {
13
+ defaultValue: {
14
+ control: "object",
15
+ description: "The default value of the slider.",
16
+ },
17
+ min: {
18
+ control: "number",
19
+ description: "The minimum value.",
20
+ },
21
+ max: {
22
+ control: "number",
23
+ description: "The maximum value.",
24
+ },
25
+ step: {
26
+ control: "number",
27
+ description: "The step value.",
28
+ },
29
+ },
30
+ } satisfies Meta<typeof Slider>;
31
+
32
+ export default meta;
33
+ type Story = StoryObj<typeof meta>;
34
+
35
+ export const Default: Story = {
36
+ args: {
37
+ defaultValue: [50],
38
+ max: 100,
39
+ },
40
+ };
41
+
42
+ export const WithValue: Story = {
43
+ render: () => {
44
+ const [value, setValue] = useState([50]);
45
+ return (
46
+ <div className="space-y-4 w-[350px]">
47
+ <Slider value={value} onValueChange={setValue} max={100} />
48
+ <div className="text-sm text-muted-foreground">
49
+ Value: {value[0]}
50
+ </div>
51
+ </div>
52
+ );
53
+ },
54
+ };
55
+
56
+ export const Range: Story = {
57
+ render: () => {
58
+ const [value, setValue] = useState([20, 80]);
59
+ return (
60
+ <div className="space-y-4 w-[350px]">
61
+ <Slider value={value} onValueChange={setValue} max={100} />
62
+ <div className="text-sm text-muted-foreground">
63
+ Range: {value[0]} - {value[1]}
64
+ </div>
65
+ </div>
66
+ );
67
+ },
68
+ };
69
+
70
+ export const CustomRange: Story = {
71
+ render: () => {
72
+ const [value, setValue] = useState([25]);
73
+ return (
74
+ <div className="space-y-4 w-[350px]">
75
+ <Slider
76
+ value={value}
77
+ onValueChange={setValue}
78
+ min={0}
79
+ max={1000}
80
+ step={10}
81
+ />
82
+ <div className="text-sm text-muted-foreground">
83
+ Value: {value[0]} (0-1000, step 10)
84
+ </div>
85
+ </div>
86
+ );
87
+ },
88
+ };
89
+
90
+ export const Disabled: Story = {
91
+ render: () => (
92
+ <div className="w-[350px]">
93
+ <Slider defaultValue={[50]} max={100} disabled />
94
+ </div>
95
+ ),
96
+ };
97
+
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import { Slider } from "./slider";
4
+
5
+ describe("Slider", () => {
6
+ it("should render slider", () => {
7
+ const { container } = render(<Slider defaultValue={[50]} />);
8
+ const slider = container.querySelector('[data-slot="slider"]');
9
+ expect(slider).toBeInTheDocument();
10
+ });
11
+
12
+ it("should render slider track", () => {
13
+ const { container } = render(<Slider defaultValue={[50]} />);
14
+ const track = container.querySelector('[data-slot="slider-track"]');
15
+ expect(track).toBeInTheDocument();
16
+ });
17
+
18
+ it("should render slider thumb", () => {
19
+ const { container } = render(<Slider defaultValue={[50]} />);
20
+ const thumb = container.querySelector('[data-slot="slider-thumb"]');
21
+ expect(thumb).toBeInTheDocument();
22
+ });
23
+
24
+ it("should call onValueChange when value changes", async () => {
25
+ const handleChange = vi.fn();
26
+ const { container } = render(
27
+ <Slider value={[50]} onValueChange={handleChange} />
28
+ );
29
+ const thumb = container.querySelector(
30
+ '[data-slot="slider-thumb"]'
31
+ ) as HTMLElement;
32
+ // Note: 실제 슬라이더 드래그는 더 복잡한 상호작용이 필요합니다
33
+ expect(thumb).toBeInTheDocument();
34
+ });
35
+
36
+ it("should support range values", () => {
37
+ const { container } = render(<Slider value={[20, 80]} />);
38
+ const thumbs = container.querySelectorAll('[data-slot="slider-thumb"]');
39
+ expect(thumbs.length).toBe(2);
40
+ });
41
+
42
+ it("should respect min and max values", () => {
43
+ const { container } = render(
44
+ <Slider defaultValue={[25]} min={0} max={100} />
45
+ );
46
+ const slider = container.querySelector('[data-slot="slider"]');
47
+ expect(slider).toBeInTheDocument();
48
+ });
49
+ });
@@ -0,0 +1,63 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SliderPrimitive from "@radix-ui/react-slider"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Slider({
9
+ className,
10
+ defaultValue,
11
+ value,
12
+ min = 0,
13
+ max = 100,
14
+ ...props
15
+ }: React.ComponentProps<typeof SliderPrimitive.Root>) {
16
+ const _values = React.useMemo(
17
+ () =>
18
+ Array.isArray(value)
19
+ ? value
20
+ : Array.isArray(defaultValue)
21
+ ? defaultValue
22
+ : [min, max],
23
+ [value, defaultValue, min, max]
24
+ )
25
+
26
+ return (
27
+ <SliderPrimitive.Root
28
+ data-slot="slider"
29
+ defaultValue={defaultValue}
30
+ value={value}
31
+ min={min}
32
+ max={max}
33
+ className={cn(
34
+ "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
35
+ className
36
+ )}
37
+ {...props}
38
+ >
39
+ <SliderPrimitive.Track
40
+ data-slot="slider-track"
41
+ className={cn(
42
+ "bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
43
+ )}
44
+ >
45
+ <SliderPrimitive.Range
46
+ data-slot="slider-range"
47
+ className={cn(
48
+ "bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
49
+ )}
50
+ />
51
+ </SliderPrimitive.Track>
52
+ {Array.from({ length: _values.length }, (_, index) => (
53
+ <SliderPrimitive.Thumb
54
+ data-slot="slider-thumb"
55
+ key={index}
56
+ className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
57
+ />
58
+ ))}
59
+ </SliderPrimitive.Root>
60
+ )
61
+ }
62
+
63
+ export { Slider }
@@ -0,0 +1,96 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Toaster } from "./sonner";
3
+ import { Button } from "./button";
4
+ import { toast } from "sonner";
5
+
6
+ const meta = {
7
+ title: "Components/Sonner",
8
+ component: Toaster,
9
+ parameters: {
10
+ layout: "padded",
11
+ },
12
+ tags: ["autodocs"],
13
+ } satisfies Meta<typeof Toaster>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const Default: Story = {
19
+ render: () => (
20
+ <>
21
+ <Toaster />
22
+ <div className="flex flex-wrap gap-4">
23
+ <Button
24
+ onClick={() => toast("Event has been created", { description: "Monday, January 3rd at 6:00pm" })}
25
+ >
26
+ Show Toast
27
+ </Button>
28
+ <Button
29
+ variant="outline"
30
+ onClick={() => toast.success("Success!", { description: "Your changes have been saved." })}
31
+ >
32
+ Success
33
+ </Button>
34
+ <Button
35
+ variant="destructive"
36
+ onClick={() => toast.error("Error!", { description: "Something went wrong." })}
37
+ >
38
+ Error
39
+ </Button>
40
+ <Button
41
+ variant="secondary"
42
+ onClick={() => toast.info("Info", { description: "Here's some information." })}
43
+ >
44
+ Info
45
+ </Button>
46
+ <Button
47
+ variant="outline"
48
+ onClick={() => toast.warning("Warning", { description: "Please be careful." })}
49
+ >
50
+ Warning
51
+ </Button>
52
+ </div>
53
+ </>
54
+ ),
55
+ };
56
+
57
+ export const WithActions: Story = {
58
+ render: () => (
59
+ <>
60
+ <Toaster />
61
+ <div className="flex gap-4">
62
+ <Button
63
+ onClick={() =>
64
+ toast("Event has been created", {
65
+ description: "Monday, January 3rd at 6:00pm",
66
+ action: {
67
+ label: "Undo",
68
+ onClick: () => console.log("Undo"),
69
+ },
70
+ })
71
+ }
72
+ >
73
+ Toast with Action
74
+ </Button>
75
+ </div>
76
+ </>
77
+ ),
78
+ };
79
+
80
+ export const LongDuration: Story = {
81
+ render: () => (
82
+ <>
83
+ <Toaster />
84
+ <Button
85
+ onClick={() =>
86
+ toast("This toast will stay for 10 seconds", {
87
+ duration: 10000,
88
+ })
89
+ }
90
+ >
91
+ Long Duration Toast
92
+ </Button>
93
+ </>
94
+ ),
95
+ };
96
+
@@ -0,0 +1,38 @@
1
+ import {
2
+ CircleCheckIcon,
3
+ InfoIcon,
4
+ Loader2Icon,
5
+ OctagonXIcon,
6
+ TriangleAlertIcon,
7
+ } from "lucide-react";
8
+ import { useTheme } from "next-themes";
9
+ import { Toaster as Sonner, type ToasterProps } from "sonner";
10
+
11
+ const Toaster = ({ ...props }: ToasterProps) => {
12
+ const { theme = "system" } = useTheme();
13
+
14
+ return (
15
+ <Sonner
16
+ theme={theme as ToasterProps["theme"]}
17
+ className="toaster group"
18
+ icons={{
19
+ success: <CircleCheckIcon className="size-4" />,
20
+ info: <InfoIcon className="size-4" />,
21
+ warning: <TriangleAlertIcon className="size-4" />,
22
+ error: <OctagonXIcon className="size-4" />,
23
+ loading: <Loader2Icon className="size-4 animate-spin" />,
24
+ }}
25
+ style={
26
+ {
27
+ "--normal-bg": "var(--popover)",
28
+ "--normal-text": "var(--popover-foreground)",
29
+ "--normal-border": "var(--border)",
30
+ "--border-radius": "var(--radius)",
31
+ } as React.CSSProperties
32
+ }
33
+ {...props}
34
+ />
35
+ );
36
+ };
37
+
38
+ export { Toaster };
@@ -0,0 +1,54 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Spinner } from "./spinner";
3
+
4
+ const meta = {
5
+ title: "Components/Spinner",
6
+ component: Spinner,
7
+ parameters: {
8
+ layout: "padded",
9
+ },
10
+ tags: ["autodocs"],
11
+ } satisfies Meta<typeof Spinner>;
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ export const Default: Story = {
17
+ render: () => <Spinner />,
18
+ };
19
+
20
+ export const Sizes: Story = {
21
+ render: () => (
22
+ <div className="flex items-center gap-4">
23
+ <Spinner className="size-4" />
24
+ <Spinner className="size-6" />
25
+ <Spinner className="size-8" />
26
+ <Spinner className="size-12" />
27
+ </div>
28
+ ),
29
+ };
30
+
31
+ export const InButton: Story = {
32
+ render: () => (
33
+ <div className="flex gap-4">
34
+ <button className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-primary-foreground">
35
+ <Spinner className="size-4" />
36
+ Loading...
37
+ </button>
38
+ <button className="inline-flex items-center gap-2 rounded-md border px-4 py-2">
39
+ <Spinner className="size-4" />
40
+ Processing
41
+ </button>
42
+ </div>
43
+ ),
44
+ };
45
+
46
+ export const WithText: Story = {
47
+ render: () => (
48
+ <div className="flex items-center gap-2">
49
+ <Spinner />
50
+ <span className="text-sm text-muted-foreground">Loading data...</span>
51
+ </div>
52
+ ),
53
+ };
54
+
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import { Spinner } from "./spinner";
4
+
5
+ describe("Spinner", () => {
6
+ it("should render spinner", () => {
7
+ const { container } = render(<Spinner />);
8
+ const spinner = container.querySelector('[role="status"]');
9
+ expect(spinner).toBeInTheDocument();
10
+ });
11
+
12
+ it("should have aria-label", () => {
13
+ const { container } = render(<Spinner />);
14
+ const spinner = container.querySelector('[role="status"]');
15
+ expect(spinner).toHaveAttribute("aria-label", "Loading");
16
+ });
17
+
18
+ it("should apply custom className", () => {
19
+ const { container } = render(<Spinner className="size-8" />);
20
+ const spinner = container.querySelector('[role="status"]');
21
+ expect(spinner).toHaveClass("size-8");
22
+ });
23
+
24
+ it("should have animate-spin class", () => {
25
+ const { container } = render(<Spinner />);
26
+ const spinner = container.querySelector('[role="status"]');
27
+ expect(spinner).toHaveClass("animate-spin");
28
+ });
29
+ });
30
+
@@ -0,0 +1,16 @@
1
+ import { Loader2Icon } from "lucide-react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
6
+ return (
7
+ <Loader2Icon
8
+ role="status"
9
+ aria-label="Loading"
10
+ className={cn("size-4 animate-spin", className)}
11
+ {...props}
12
+ />
13
+ )
14
+ }
15
+
16
+ export { Spinner }
@@ -0,0 +1,108 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useState } from "react";
3
+ import { Switch } from "./switch";
4
+ import { Label } from "./label";
5
+
6
+ const meta = {
7
+ title: "Components/Switch",
8
+ component: Switch,
9
+ parameters: {
10
+ layout: "padded",
11
+ },
12
+ tags: ["autodocs"],
13
+ argTypes: {
14
+ checked: {
15
+ control: "boolean",
16
+ description: "Whether the switch is checked.",
17
+ },
18
+ disabled: {
19
+ control: "boolean",
20
+ description: "Whether the switch is disabled.",
21
+ },
22
+ },
23
+ } satisfies Meta<typeof Switch>;
24
+
25
+ export default meta;
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ export const Default: Story = {
29
+ render: () => {
30
+ const [checked, setChecked] = useState(false);
31
+ return (
32
+ <div className="flex items-center space-x-2">
33
+ <Switch id="airplane-mode" checked={checked} onCheckedChange={setChecked} />
34
+ <Label htmlFor="airplane-mode">Airplane Mode</Label>
35
+ </div>
36
+ );
37
+ },
38
+ };
39
+
40
+ export const Checked: Story = {
41
+ render: () => (
42
+ <div className="flex items-center space-x-2">
43
+ <Switch id="checked" defaultChecked />
44
+ <Label htmlFor="checked">Enabled by default</Label>
45
+ </div>
46
+ ),
47
+ };
48
+
49
+ export const Disabled: Story = {
50
+ render: () => (
51
+ <div className="space-y-4">
52
+ <div className="flex items-center space-x-2">
53
+ <Switch id="disabled" disabled />
54
+ <Label htmlFor="disabled">Disabled off</Label>
55
+ </div>
56
+ <div className="flex items-center space-x-2">
57
+ <Switch id="disabled-on" disabled defaultChecked />
58
+ <Label htmlFor="disabled-on">Disabled on</Label>
59
+ </div>
60
+ </div>
61
+ ),
62
+ };
63
+
64
+ export const Multiple: Story = {
65
+ render: () => {
66
+ const [settings, setSettings] = useState({
67
+ notifications: true,
68
+ email: false,
69
+ sms: true,
70
+ });
71
+
72
+ return (
73
+ <div className="space-y-3">
74
+ <div className="flex items-center space-x-2">
75
+ <Switch
76
+ id="notifications"
77
+ checked={settings.notifications}
78
+ onCheckedChange={(checked) =>
79
+ setSettings({ ...settings, notifications: checked as boolean })
80
+ }
81
+ />
82
+ <Label htmlFor="notifications">Notifications</Label>
83
+ </div>
84
+ <div className="flex items-center space-x-2">
85
+ <Switch
86
+ id="email"
87
+ checked={settings.email}
88
+ onCheckedChange={(checked) =>
89
+ setSettings({ ...settings, email: checked as boolean })
90
+ }
91
+ />
92
+ <Label htmlFor="email">Email</Label>
93
+ </div>
94
+ <div className="flex items-center space-x-2">
95
+ <Switch
96
+ id="sms"
97
+ checked={settings.sms}
98
+ onCheckedChange={(checked) =>
99
+ setSettings({ ...settings, sms: checked as boolean })
100
+ }
101
+ />
102
+ <Label htmlFor="sms">SMS</Label>
103
+ </div>
104
+ </div>
105
+ );
106
+ },
107
+ };
108
+