@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,64 @@
1
+ import * as React from "react"
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
3
+ import { ChevronDownIcon } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function Accordion({
8
+ ...props
9
+ }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
10
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />
11
+ }
12
+
13
+ function AccordionItem({
14
+ className,
15
+ ...props
16
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
17
+ return (
18
+ <AccordionPrimitive.Item
19
+ data-slot="accordion-item"
20
+ className={cn("border-b last:border-b-0", className)}
21
+ {...props}
22
+ />
23
+ )
24
+ }
25
+
26
+ function AccordionTrigger({
27
+ className,
28
+ children,
29
+ ...props
30
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
31
+ return (
32
+ <AccordionPrimitive.Header className="flex">
33
+ <AccordionPrimitive.Trigger
34
+ data-slot="accordion-trigger"
35
+ className={cn(
36
+ "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
37
+ className
38
+ )}
39
+ {...props}
40
+ >
41
+ {children}
42
+ <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
43
+ </AccordionPrimitive.Trigger>
44
+ </AccordionPrimitive.Header>
45
+ )
46
+ }
47
+
48
+ function AccordionContent({
49
+ className,
50
+ children,
51
+ ...props
52
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
53
+ return (
54
+ <AccordionPrimitive.Content
55
+ data-slot="accordion-content"
56
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
57
+ {...props}
58
+ >
59
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
60
+ </AccordionPrimitive.Content>
61
+ )
62
+ }
63
+
64
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
@@ -0,0 +1,213 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useState } from "react";
3
+ import {
4
+ AlertDialog,
5
+ AlertDialogAction,
6
+ AlertDialogCancel,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle,
12
+ AlertDialogTrigger,
13
+ } from "./alert-dialog";
14
+ import { Button } from "./button";
15
+
16
+ const meta = {
17
+ title: "Components/AlertDialog",
18
+ component: AlertDialog,
19
+ parameters: {
20
+ layout: "padded",
21
+ },
22
+ tags: ["autodocs"],
23
+ argTypes: {
24
+ open: {
25
+ control: "boolean",
26
+ description: "The controlled open state of the dialog.",
27
+ table: {
28
+ type: { summary: "boolean" },
29
+ },
30
+ },
31
+ defaultOpen: {
32
+ control: "boolean",
33
+ description: "The default open state of the dialog.",
34
+ table: {
35
+ type: { summary: "boolean" },
36
+ defaultValue: { summary: "false" },
37
+ },
38
+ },
39
+ },
40
+ } satisfies Meta<typeof AlertDialog>;
41
+
42
+ export default meta;
43
+ type Story = StoryObj<typeof meta>;
44
+
45
+ export const Default: Story = {
46
+ render: () => {
47
+ const [open, setOpen] = useState(false);
48
+ return (
49
+ <AlertDialog open={open} onOpenChange={setOpen}>
50
+ <AlertDialogTrigger asChild>
51
+ <Button>Open Dialog</Button>
52
+ </AlertDialogTrigger>
53
+ <AlertDialogContent>
54
+ <AlertDialogHeader>
55
+ <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
56
+ <AlertDialogDescription>
57
+ This action cannot be undone. This will permanently delete your
58
+ account and remove your data from our servers.
59
+ </AlertDialogDescription>
60
+ </AlertDialogHeader>
61
+ <AlertDialogFooter>
62
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
63
+ <AlertDialogAction>Continue</AlertDialogAction>
64
+ </AlertDialogFooter>
65
+ </AlertDialogContent>
66
+ </AlertDialog>
67
+ );
68
+ },
69
+ };
70
+
71
+ export const Destructive: Story = {
72
+ render: () => {
73
+ const [open, setOpen] = useState(false);
74
+ return (
75
+ <AlertDialog open={open} onOpenChange={setOpen}>
76
+ <AlertDialogTrigger asChild>
77
+ <Button variant="destructive">Delete Account</Button>
78
+ </AlertDialogTrigger>
79
+ <AlertDialogContent>
80
+ <AlertDialogHeader>
81
+ <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
82
+ <AlertDialogDescription>
83
+ This action cannot be undone. This will permanently delete your
84
+ account and remove your data from our servers.
85
+ </AlertDialogDescription>
86
+ </AlertDialogHeader>
87
+ <AlertDialogFooter>
88
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
89
+ <AlertDialogAction className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
90
+ Delete
91
+ </AlertDialogAction>
92
+ </AlertDialogFooter>
93
+ </AlertDialogContent>
94
+ </AlertDialog>
95
+ );
96
+ },
97
+ };
98
+
99
+ export const WithoutCancel: Story = {
100
+ render: () => {
101
+ const [open, setOpen] = useState(false);
102
+ return (
103
+ <AlertDialog open={open} onOpenChange={setOpen}>
104
+ <AlertDialogTrigger asChild>
105
+ <Button>Show Information</Button>
106
+ </AlertDialogTrigger>
107
+ <AlertDialogContent>
108
+ <AlertDialogHeader>
109
+ <AlertDialogTitle>Information</AlertDialogTitle>
110
+ <AlertDialogDescription>
111
+ This is an informational dialog without a cancel button. The user
112
+ must click the action button to close it.
113
+ </AlertDialogDescription>
114
+ </AlertDialogHeader>
115
+ <AlertDialogFooter>
116
+ <AlertDialogAction>Got it</AlertDialogAction>
117
+ </AlertDialogFooter>
118
+ </AlertDialogContent>
119
+ </AlertDialog>
120
+ );
121
+ },
122
+ };
123
+
124
+ export const LongContent: Story = {
125
+ render: () => {
126
+ const [open, setOpen] = useState(false);
127
+ return (
128
+ <AlertDialog open={open} onOpenChange={setOpen}>
129
+ <AlertDialogTrigger asChild>
130
+ <Button>View Terms</Button>
131
+ </AlertDialogTrigger>
132
+ <AlertDialogContent>
133
+ <AlertDialogHeader>
134
+ <AlertDialogTitle>Terms and Conditions</AlertDialogTitle>
135
+ <AlertDialogDescription className="max-h-[60vh] overflow-y-auto">
136
+ <div className="space-y-4">
137
+ <p>
138
+ Please read these terms and conditions carefully before using
139
+ our service. By accessing or using our service, you agree to
140
+ be bound by these terms.
141
+ </p>
142
+ <p>
143
+ <strong>1. Acceptance of Terms</strong>
144
+ </p>
145
+ <p>
146
+ By accessing and using this service, you accept and agree to
147
+ be bound by the terms and provision of this agreement.
148
+ </p>
149
+ <p>
150
+ <strong>2. Use License</strong>
151
+ </p>
152
+ <p>
153
+ Permission is granted to temporarily download one copy of the
154
+ materials on our website for personal, non-commercial
155
+ transitory viewing only.
156
+ </p>
157
+ <p>
158
+ <strong>3. Disclaimer</strong>
159
+ </p>
160
+ <p>
161
+ The materials on our website are provided on an 'as is' basis.
162
+ We make no warranties, expressed or implied, and hereby
163
+ disclaim and negate all other warranties.
164
+ </p>
165
+ <p>
166
+ <strong>4. Limitations</strong>
167
+ </p>
168
+ <p>
169
+ In no event shall we or our suppliers be liable for any
170
+ damages arising out of the use or inability to use the
171
+ materials on our website.
172
+ </p>
173
+ </div>
174
+ </AlertDialogDescription>
175
+ </AlertDialogHeader>
176
+ <AlertDialogFooter>
177
+ <AlertDialogCancel>Decline</AlertDialogCancel>
178
+ <AlertDialogAction>Accept</AlertDialogAction>
179
+ </AlertDialogFooter>
180
+ </AlertDialogContent>
181
+ </AlertDialog>
182
+ );
183
+ },
184
+ };
185
+
186
+ export const CustomActions: Story = {
187
+ render: () => {
188
+ const [open, setOpen] = useState(false);
189
+ return (
190
+ <AlertDialog open={open} onOpenChange={setOpen}>
191
+ <AlertDialogTrigger asChild>
192
+ <Button variant="outline">Custom Actions</Button>
193
+ </AlertDialogTrigger>
194
+ <AlertDialogContent>
195
+ <AlertDialogHeader>
196
+ <AlertDialogTitle>Choose an option</AlertDialogTitle>
197
+ <AlertDialogDescription>
198
+ This dialog demonstrates custom action buttons with different
199
+ styles.
200
+ </AlertDialogDescription>
201
+ </AlertDialogHeader>
202
+ <AlertDialogFooter>
203
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
204
+ <AlertDialogAction className="bg-secondary text-secondary-foreground hover:bg-secondary/80">
205
+ Save Draft
206
+ </AlertDialogAction>
207
+ <AlertDialogAction>Publish</AlertDialogAction>
208
+ </AlertDialogFooter>
209
+ </AlertDialogContent>
210
+ </AlertDialog>
211
+ );
212
+ },
213
+ };
@@ -0,0 +1,80 @@
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 {
5
+ AlertDialog,
6
+ AlertDialogAction,
7
+ AlertDialogCancel,
8
+ AlertDialogContent,
9
+ AlertDialogDescription,
10
+ AlertDialogFooter,
11
+ AlertDialogHeader,
12
+ AlertDialogTitle,
13
+ AlertDialogTrigger,
14
+ } from "./alert-dialog";
15
+
16
+ describe("AlertDialog", () => {
17
+ it("should render alert dialog trigger", () => {
18
+ const { getByText } = render(
19
+ <AlertDialog>
20
+ <AlertDialogTrigger>Open</AlertDialogTrigger>
21
+ <AlertDialogContent>
22
+ <AlertDialogHeader>
23
+ <AlertDialogTitle>Title</AlertDialogTitle>
24
+ <AlertDialogDescription>Description</AlertDialogDescription>
25
+ </AlertDialogHeader>
26
+ <AlertDialogFooter>
27
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
28
+ <AlertDialogAction>Confirm</AlertDialogAction>
29
+ </AlertDialogFooter>
30
+ </AlertDialogContent>
31
+ </AlertDialog>
32
+ );
33
+ expect(getByText("Open")).toBeInTheDocument();
34
+ });
35
+
36
+ it("should open dialog when trigger is clicked", async () => {
37
+ const user = userEvent.setup();
38
+ const { getByText } = render(
39
+ <AlertDialog>
40
+ <AlertDialogTrigger>Open</AlertDialogTrigger>
41
+ <AlertDialogContent>
42
+ <AlertDialogHeader>
43
+ <AlertDialogTitle>Title</AlertDialogTitle>
44
+ <AlertDialogDescription>Description</AlertDialogDescription>
45
+ </AlertDialogHeader>
46
+ <AlertDialogFooter>
47
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
48
+ <AlertDialogAction>Confirm</AlertDialogAction>
49
+ </AlertDialogFooter>
50
+ </AlertDialogContent>
51
+ </AlertDialog>
52
+ );
53
+ const trigger = getByText("Open");
54
+ await user.click(trigger);
55
+ expect(getByText("Title")).toBeInTheDocument();
56
+ expect(getByText("Description")).toBeInTheDocument();
57
+ });
58
+
59
+ it("should call onAction when action button is clicked", async () => {
60
+ const user = userEvent.setup();
61
+ const handleAction = vi.fn();
62
+ const { getByText } = render(
63
+ <AlertDialog>
64
+ <AlertDialogTrigger>Open</AlertDialogTrigger>
65
+ <AlertDialogContent>
66
+ <AlertDialogHeader>
67
+ <AlertDialogTitle>Title</AlertDialogTitle>
68
+ </AlertDialogHeader>
69
+ <AlertDialogFooter>
70
+ <AlertDialogAction onClick={handleAction}>Confirm</AlertDialogAction>
71
+ </AlertDialogFooter>
72
+ </AlertDialogContent>
73
+ </AlertDialog>
74
+ );
75
+ await user.click(getByText("Open"));
76
+ await user.click(getByText("Confirm"));
77
+ expect(handleAction).toHaveBeenCalled();
78
+ });
79
+ });
80
+
@@ -0,0 +1,155 @@
1
+ import * as React from "react";
2
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
3
+
4
+ import { cn } from "@/lib/utils";
5
+ import { buttonVariants } from "@/components/button";
6
+
7
+ function AlertDialog({
8
+ ...props
9
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
10
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
11
+ }
12
+
13
+ function AlertDialogTrigger({
14
+ ...props
15
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
16
+ return (
17
+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
18
+ );
19
+ }
20
+
21
+ function AlertDialogPortal({
22
+ ...props
23
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
24
+ return (
25
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
26
+ );
27
+ }
28
+
29
+ function AlertDialogOverlay({
30
+ className,
31
+ ...props
32
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
33
+ return (
34
+ <AlertDialogPrimitive.Overlay
35
+ data-slot="alert-dialog-overlay"
36
+ className={cn(
37
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
38
+ className
39
+ )}
40
+ {...props}
41
+ />
42
+ );
43
+ }
44
+
45
+ function AlertDialogContent({
46
+ className,
47
+ ...props
48
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
49
+ return (
50
+ <AlertDialogPortal>
51
+ <AlertDialogOverlay />
52
+ <AlertDialogPrimitive.Content
53
+ data-slot="alert-dialog-content"
54
+ className={cn(
55
+ "bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
56
+ className
57
+ )}
58
+ {...props}
59
+ />
60
+ </AlertDialogPortal>
61
+ );
62
+ }
63
+
64
+ function AlertDialogHeader({
65
+ className,
66
+ ...props
67
+ }: React.ComponentProps<"div">) {
68
+ return (
69
+ <div
70
+ data-slot="alert-dialog-header"
71
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
72
+ {...props}
73
+ />
74
+ );
75
+ }
76
+
77
+ function AlertDialogFooter({
78
+ className,
79
+ ...props
80
+ }: React.ComponentProps<"div">) {
81
+ return (
82
+ <div
83
+ data-slot="alert-dialog-footer"
84
+ className={cn(
85
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
86
+ className
87
+ )}
88
+ {...props}
89
+ />
90
+ );
91
+ }
92
+
93
+ function AlertDialogTitle({
94
+ className,
95
+ ...props
96
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
97
+ return (
98
+ <AlertDialogPrimitive.Title
99
+ data-slot="alert-dialog-title"
100
+ className={cn("text-lg font-semibold", className)}
101
+ {...props}
102
+ />
103
+ );
104
+ }
105
+
106
+ function AlertDialogDescription({
107
+ className,
108
+ ...props
109
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
110
+ return (
111
+ <AlertDialogPrimitive.Description
112
+ data-slot="alert-dialog-description"
113
+ className={cn("text-muted-foreground text-sm", className)}
114
+ {...props}
115
+ />
116
+ );
117
+ }
118
+
119
+ function AlertDialogAction({
120
+ className,
121
+ ...props
122
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
123
+ return (
124
+ <AlertDialogPrimitive.Action
125
+ className={cn(buttonVariants(), className)}
126
+ {...props}
127
+ />
128
+ );
129
+ }
130
+
131
+ function AlertDialogCancel({
132
+ className,
133
+ ...props
134
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
135
+ return (
136
+ <AlertDialogPrimitive.Cancel
137
+ className={cn(buttonVariants({ variant: "outline" }), className)}
138
+ {...props}
139
+ />
140
+ );
141
+ }
142
+
143
+ export {
144
+ AlertDialog,
145
+ AlertDialogPortal,
146
+ AlertDialogOverlay,
147
+ AlertDialogTrigger,
148
+ AlertDialogContent,
149
+ AlertDialogHeader,
150
+ AlertDialogFooter,
151
+ AlertDialogTitle,
152
+ AlertDialogDescription,
153
+ AlertDialogAction,
154
+ AlertDialogCancel,
155
+ };
@@ -0,0 +1,84 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Alert, AlertTitle, AlertDescription } from "./alert";
3
+ import { AlertCircleIcon, InfoIcon } from "lucide-react";
4
+
5
+ const meta = {
6
+ title: "Components/Alert",
7
+ component: Alert,
8
+ parameters: {
9
+ layout: "padded",
10
+ },
11
+ tags: ["autodocs"],
12
+ argTypes: {
13
+ variant: {
14
+ control: "select",
15
+ options: ["default", "destructive"],
16
+ description: "The visual style variant of the alert.",
17
+ },
18
+ },
19
+ } satisfies Meta<typeof Alert>;
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ export const Default: Story = {
25
+ args: {
26
+ variant: "default",
27
+ },
28
+ render: (args) => (
29
+ <Alert {...args}>
30
+ <InfoIcon />
31
+ <AlertTitle>Heads up!</AlertTitle>
32
+ <AlertDescription>
33
+ You can add components to your app using the cli.
34
+ </AlertDescription>
35
+ </Alert>
36
+ ),
37
+ };
38
+
39
+ export const Destructive: Story = {
40
+ args: {
41
+ variant: "destructive",
42
+ },
43
+ render: (args) => (
44
+ <Alert {...args}>
45
+ <AlertCircleIcon />
46
+ <AlertTitle>Error</AlertTitle>
47
+ <AlertDescription>
48
+ Your session has expired. Please log in again.
49
+ </AlertDescription>
50
+ </Alert>
51
+ ),
52
+ };
53
+
54
+ export const WithoutIcon: Story = {
55
+ render: () => (
56
+ <Alert>
57
+ <AlertTitle>Notice</AlertTitle>
58
+ <AlertDescription>
59
+ This alert doesn't have an icon, but the layout still works correctly.
60
+ </AlertDescription>
61
+ </Alert>
62
+ ),
63
+ };
64
+
65
+ export const LongContent: Story = {
66
+ render: () => (
67
+ <Alert>
68
+ <InfoIcon />
69
+ <AlertTitle>Important Information</AlertTitle>
70
+ <AlertDescription>
71
+ <p>
72
+ This is a longer alert message that demonstrates how the component
73
+ handles multiple paragraphs and extended content. The alert should
74
+ maintain proper spacing and readability even with longer text.
75
+ </p>
76
+ <p className="mt-2">
77
+ You can include additional information, links, or any other content
78
+ within the alert description.
79
+ </p>
80
+ </AlertDescription>
81
+ </Alert>
82
+ ),
83
+ };
84
+
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import { Alert, AlertTitle, AlertDescription } from "./alert";
4
+
5
+ describe("Alert", () => {
6
+ it("should render alert with role", () => {
7
+ const { container } = render(<Alert>Test alert</Alert>);
8
+ const alert = container.querySelector('[data-slot="alert"]');
9
+ expect(alert).toHaveAttribute("role", "alert");
10
+ });
11
+
12
+ it("should render alert title and description", () => {
13
+ const { getByText } = render(
14
+ <Alert>
15
+ <AlertTitle>Alert Title</AlertTitle>
16
+ <AlertDescription>Alert description</AlertDescription>
17
+ </Alert>
18
+ );
19
+ expect(getByText("Alert Title")).toBeInTheDocument();
20
+ expect(getByText("Alert description")).toBeInTheDocument();
21
+ });
22
+
23
+ it("should apply variant classes", () => {
24
+ const { container } = render(<Alert variant="destructive">Error</Alert>);
25
+ const alert = container.querySelector('[data-slot="alert"]');
26
+ expect(alert).toHaveClass("text-destructive");
27
+ });
28
+
29
+ it("should render with default variant", () => {
30
+ const { container } = render(<Alert>Default alert</Alert>);
31
+ const alert = container.querySelector('[data-slot="alert"]');
32
+ expect(alert).toBeInTheDocument();
33
+ });
34
+ });
35
+
@@ -0,0 +1,66 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function AlertDescription({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="alert-description"
57
+ className={cn(
58
+ "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ export { Alert, AlertTitle, AlertDescription }