@mzc-fe/design-system 0.0.1-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-push +21 -0
- package/.storybook/main.ts +11 -0
- package/.storybook/preview.tsx +30 -0
- package/.vscode/settings.json +12 -0
- package/.vscode/tailwind.json +105 -0
- package/README.md +136 -0
- package/bitbucket-pipelines.yml +52 -0
- package/components.json +21 -0
- package/eslint.config.js +38 -0
- package/package.json +98 -0
- package/public/vite.svg +1 -0
- package/src/components/accordion.stories.tsx +258 -0
- package/src/components/accordion.test.tsx +390 -0
- package/src/components/accordion.tsx +64 -0
- package/src/components/alert-dialog.stories.tsx +213 -0
- package/src/components/alert-dialog.test.tsx +80 -0
- package/src/components/alert-dialog.tsx +155 -0
- package/src/components/alert.stories.tsx +84 -0
- package/src/components/alert.test.tsx +35 -0
- package/src/components/alert.tsx +66 -0
- package/src/components/aspect-ratio.stories.tsx +97 -0
- package/src/components/aspect-ratio.test.tsx +47 -0
- package/src/components/aspect-ratio.tsx +11 -0
- package/src/components/avatar.stories.tsx +76 -0
- package/src/components/avatar.test.tsx +50 -0
- package/src/components/avatar.tsx +51 -0
- package/src/components/badge.stories.tsx +64 -0
- package/src/components/badge.test.tsx +34 -0
- package/src/components/badge.tsx +46 -0
- package/src/components/breadcrumb.stories.tsx +86 -0
- package/src/components/breadcrumb.test.tsx +74 -0
- package/src/components/breadcrumb.tsx +109 -0
- package/src/components/button-group.stories.tsx +62 -0
- package/src/components/button-group.tsx +83 -0
- package/src/components/button.stories.tsx +118 -0
- package/src/components/button.test.tsx +64 -0
- package/src/components/button.tsx +62 -0
- package/src/components/calendar.stories.tsx +81 -0
- package/src/components/calendar.tsx +220 -0
- package/src/components/card.stories.tsx +110 -0
- package/src/components/card.test.tsx +56 -0
- package/src/components/card.tsx +92 -0
- package/src/components/carousel.stories.tsx +90 -0
- package/src/components/carousel.tsx +239 -0
- package/src/components/chart.tsx +357 -0
- package/src/components/checkbox.stories.tsx +108 -0
- package/src/components/checkbox.test.tsx +67 -0
- package/src/components/checkbox.tsx +32 -0
- package/src/components/collapsible.stories.tsx +106 -0
- package/src/components/collapsible.test.tsx +92 -0
- package/src/components/collapsible.tsx +31 -0
- package/src/components/command.stories.tsx +90 -0
- package/src/components/command.tsx +182 -0
- package/src/components/context-menu.stories.tsx +63 -0
- package/src/components/context-menu.tsx +252 -0
- package/src/components/dialog.stories.tsx +128 -0
- package/src/components/dialog.tsx +141 -0
- package/src/components/drawer.stories.tsx +104 -0
- package/src/components/drawer.tsx +135 -0
- package/src/components/dropdown-menu.stories.tsx +97 -0
- package/src/components/dropdown-menu.tsx +255 -0
- package/src/components/empty.stories.tsx +90 -0
- package/src/components/empty.test.tsx +55 -0
- package/src/components/empty.tsx +104 -0
- package/src/components/field.tsx +246 -0
- package/src/components/form.tsx +168 -0
- package/src/components/hover-card.stories.tsx +66 -0
- package/src/components/hover-card.tsx +44 -0
- package/src/components/input-group.stories.tsx +57 -0
- package/src/components/input-group.test.tsx +40 -0
- package/src/components/input-group.tsx +170 -0
- package/src/components/input-otp.stories.tsx +94 -0
- package/src/components/input-otp.test.tsx +60 -0
- package/src/components/input-otp.tsx +75 -0
- package/src/components/input.stories.tsx +94 -0
- package/src/components/input.test.tsx +53 -0
- package/src/components/input.tsx +21 -0
- package/src/components/item.tsx +193 -0
- package/src/components/kbd.stories.tsx +100 -0
- package/src/components/kbd.test.tsx +28 -0
- package/src/components/kbd.tsx +28 -0
- package/src/components/label.stories.tsx +48 -0
- package/src/components/label.test.tsx +28 -0
- package/src/components/label.tsx +24 -0
- package/src/components/menubar.tsx +274 -0
- package/src/components/navigation-menu.tsx +168 -0
- package/src/components/pagination.stories.tsx +107 -0
- package/src/components/pagination.tsx +127 -0
- package/src/components/popover.stories.tsx +102 -0
- package/src/components/popover.tsx +48 -0
- package/src/components/progress.stories.tsx +76 -0
- package/src/components/progress.test.tsx +36 -0
- package/src/components/progress.tsx +29 -0
- package/src/components/radio-group.stories.tsx +73 -0
- package/src/components/radio-group.test.tsx +74 -0
- package/src/components/radio-group.tsx +45 -0
- package/src/components/resizable.stories.tsx +120 -0
- package/src/components/resizable.tsx +54 -0
- package/src/components/scroll-area.stories.tsx +64 -0
- package/src/components/scroll-area.test.tsx +46 -0
- package/src/components/scroll-area.tsx +58 -0
- package/src/components/select.stories.tsx +111 -0
- package/src/components/select.test.tsx +90 -0
- package/src/components/select.tsx +188 -0
- package/src/components/separator.stories.tsx +76 -0
- package/src/components/separator.test.tsx +24 -0
- package/src/components/separator.tsx +28 -0
- package/src/components/sheet.stories.tsx +122 -0
- package/src/components/sheet.tsx +137 -0
- package/src/components/sidebar.tsx +726 -0
- package/src/components/skeleton.stories.tsx +53 -0
- package/src/components/skeleton.test.tsx +24 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/slider.stories.tsx +97 -0
- package/src/components/slider.test.tsx +49 -0
- package/src/components/slider.tsx +63 -0
- package/src/components/sonner.stories.tsx +96 -0
- package/src/components/sonner.tsx +38 -0
- package/src/components/spinner.stories.tsx +54 -0
- package/src/components/spinner.test.tsx +30 -0
- package/src/components/spinner.tsx +16 -0
- package/src/components/switch.stories.tsx +108 -0
- package/src/components/switch.test.tsx +62 -0
- package/src/components/switch.tsx +31 -0
- package/src/components/table.stories.tsx +139 -0
- package/src/components/table.test.tsx +85 -0
- package/src/components/table.tsx +114 -0
- package/src/components/tabs.stories.tsx +99 -0
- package/src/components/tabs.test.tsx +64 -0
- package/src/components/tabs.tsx +66 -0
- package/src/components/textarea.stories.tsx +89 -0
- package/src/components/textarea.test.tsx +53 -0
- package/src/components/textarea.tsx +18 -0
- package/src/components/toggle-group.stories.tsx +108 -0
- package/src/components/toggle-group.test.tsx +66 -0
- package/src/components/toggle-group.tsx +81 -0
- package/src/components/toggle.stories.tsx +98 -0
- package/src/components/toggle.test.tsx +42 -0
- package/src/components/toggle.tsx +45 -0
- package/src/components/tooltip.stories.tsx +111 -0
- package/src/components/tooltip.tsx +61 -0
- package/src/foundations/README.md +141 -0
- package/src/foundations/ThemeProvider.tsx +77 -0
- package/src/foundations/color.css +232 -0
- package/src/foundations/color.stories.tsx +719 -0
- package/src/foundations/palette.css +249 -0
- package/src/foundations/spacing.css +8 -0
- package/src/foundations/typography.css +143 -0
- package/src/foundations/typography.stories.tsx +17 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.css +176 -0
- package/src/index.ts +336 -0
- package/src/lib/utils.ts +6 -0
- package/src/test/setup.ts +8 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +33 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +25 -0
- package/vite.config.ts +30 -0
- package/vitest.config.ts +25 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { AspectRatio } from "./aspect-ratio";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Components/AspectRatio",
|
|
6
|
+
component: AspectRatio,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "padded",
|
|
9
|
+
},
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
argTypes: {
|
|
12
|
+
ratio: {
|
|
13
|
+
control: "number",
|
|
14
|
+
description: "The aspect ratio (width / height).",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
} satisfies Meta<typeof AspectRatio>;
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
type Story = StoryObj<typeof meta>;
|
|
21
|
+
|
|
22
|
+
export const Default: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
ratio: 16 / 9,
|
|
25
|
+
},
|
|
26
|
+
render: (args) => (
|
|
27
|
+
<div className="w-[450px]">
|
|
28
|
+
<AspectRatio {...args} className="bg-muted rounded-md overflow-hidden">
|
|
29
|
+
<img
|
|
30
|
+
src="http://placecats.com/500/500"
|
|
31
|
+
alt="Photo"
|
|
32
|
+
className="h-full w-full object-cover"
|
|
33
|
+
/>
|
|
34
|
+
</AspectRatio>
|
|
35
|
+
</div>
|
|
36
|
+
),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Ratios: Story = {
|
|
40
|
+
render: () => (
|
|
41
|
+
<div className="space-y-4 w-[450px]">
|
|
42
|
+
<div>
|
|
43
|
+
<p className="mb-2 text-sm font-medium">16:9</p>
|
|
44
|
+
<AspectRatio ratio={16 / 9} className="bg-muted rounded-md">
|
|
45
|
+
<div className="flex h-full w-full items-center justify-center">
|
|
46
|
+
16:9
|
|
47
|
+
</div>
|
|
48
|
+
</AspectRatio>
|
|
49
|
+
</div>
|
|
50
|
+
<div>
|
|
51
|
+
<p className="mb-2 text-sm font-medium">4:3</p>
|
|
52
|
+
<AspectRatio ratio={4 / 3} className="bg-muted rounded-md">
|
|
53
|
+
<div className="flex h-full w-full items-center justify-center">
|
|
54
|
+
4:3
|
|
55
|
+
</div>
|
|
56
|
+
</AspectRatio>
|
|
57
|
+
</div>
|
|
58
|
+
<div>
|
|
59
|
+
<p className="mb-2 text-sm font-medium">1:1</p>
|
|
60
|
+
<AspectRatio ratio={1} className="bg-muted rounded-md">
|
|
61
|
+
<div className="flex h-full w-full items-center justify-center">
|
|
62
|
+
1:1
|
|
63
|
+
</div>
|
|
64
|
+
</AspectRatio>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const WithImage: Story = {
|
|
71
|
+
render: () => (
|
|
72
|
+
<div className="w-[450px]">
|
|
73
|
+
<AspectRatio
|
|
74
|
+
ratio={16 / 9}
|
|
75
|
+
className="bg-muted rounded-md overflow-hidden"
|
|
76
|
+
>
|
|
77
|
+
<img
|
|
78
|
+
src="http://placecats.com/800/450"
|
|
79
|
+
alt="Photo"
|
|
80
|
+
className="h-full w-full object-cover"
|
|
81
|
+
/>
|
|
82
|
+
</AspectRatio>
|
|
83
|
+
</div>
|
|
84
|
+
),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const Square: Story = {
|
|
88
|
+
render: () => (
|
|
89
|
+
<div className="w-[300px]">
|
|
90
|
+
<AspectRatio ratio={1} className="bg-muted rounded-md">
|
|
91
|
+
<div className="flex h-full w-full items-center justify-center">
|
|
92
|
+
Square
|
|
93
|
+
</div>
|
|
94
|
+
</AspectRatio>
|
|
95
|
+
</div>
|
|
96
|
+
),
|
|
97
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import { AspectRatio } from "./aspect-ratio";
|
|
4
|
+
|
|
5
|
+
describe("AspectRatio", () => {
|
|
6
|
+
it("should render aspect ratio container", () => {
|
|
7
|
+
const { container } = render(
|
|
8
|
+
<AspectRatio ratio={16 / 9}>
|
|
9
|
+
<div>Content</div>
|
|
10
|
+
</AspectRatio>
|
|
11
|
+
);
|
|
12
|
+
const aspectRatio = container.querySelector('[data-slot="aspect-ratio"]');
|
|
13
|
+
expect(aspectRatio).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should apply ratio style", () => {
|
|
17
|
+
const { container } = render(
|
|
18
|
+
<AspectRatio ratio={16 / 9}>
|
|
19
|
+
<div>Content</div>
|
|
20
|
+
</AspectRatio>
|
|
21
|
+
);
|
|
22
|
+
const aspectRatio = container.querySelector('[data-slot="aspect-ratio"]') as HTMLElement;
|
|
23
|
+
// Radix UI AspectRatio uses CSS custom properties, check for ratio attribute or style
|
|
24
|
+
expect(aspectRatio).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should render children", () => {
|
|
28
|
+
const { getByText } = render(
|
|
29
|
+
<AspectRatio ratio={1}>
|
|
30
|
+
<div>Aspect ratio content</div>
|
|
31
|
+
</AspectRatio>
|
|
32
|
+
);
|
|
33
|
+
expect(getByText("Aspect ratio content")).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should support different ratios", () => {
|
|
37
|
+
const { container } = render(
|
|
38
|
+
<AspectRatio ratio={4 / 3}>
|
|
39
|
+
<div>Content</div>
|
|
40
|
+
</AspectRatio>
|
|
41
|
+
);
|
|
42
|
+
const aspectRatio = container.querySelector('[data-slot="aspect-ratio"]') as HTMLElement;
|
|
43
|
+
// Radix UI AspectRatio uses CSS custom properties
|
|
44
|
+
expect(aspectRatio).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
|
4
|
+
|
|
5
|
+
function AspectRatio({
|
|
6
|
+
...props
|
|
7
|
+
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
|
8
|
+
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { AspectRatio }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Avatar, AvatarImage, AvatarFallback } from "./avatar";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Components/Avatar",
|
|
6
|
+
component: Avatar,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "padded",
|
|
9
|
+
},
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
} satisfies Meta<typeof Avatar>;
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof meta>;
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<Avatar>
|
|
19
|
+
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
|
20
|
+
<AvatarFallback>CN</AvatarFallback>
|
|
21
|
+
</Avatar>
|
|
22
|
+
),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const WithFallback: Story = {
|
|
26
|
+
render: () => (
|
|
27
|
+
<div className="flex gap-4">
|
|
28
|
+
<Avatar>
|
|
29
|
+
<AvatarImage src="https://invalid-url.png" alt="Invalid" />
|
|
30
|
+
<AvatarFallback>JD</AvatarFallback>
|
|
31
|
+
</Avatar>
|
|
32
|
+
<Avatar>
|
|
33
|
+
<AvatarFallback>AB</AvatarFallback>
|
|
34
|
+
</Avatar>
|
|
35
|
+
</div>
|
|
36
|
+
),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Sizes: Story = {
|
|
40
|
+
render: () => (
|
|
41
|
+
<div className="flex items-center gap-4">
|
|
42
|
+
<Avatar className="size-8">
|
|
43
|
+
<AvatarFallback>SM</AvatarFallback>
|
|
44
|
+
</Avatar>
|
|
45
|
+
<Avatar className="size-12">
|
|
46
|
+
<AvatarFallback>MD</AvatarFallback>
|
|
47
|
+
</Avatar>
|
|
48
|
+
<Avatar className="size-16">
|
|
49
|
+
<AvatarFallback>LG</AvatarFallback>
|
|
50
|
+
</Avatar>
|
|
51
|
+
</div>
|
|
52
|
+
),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const Group: Story = {
|
|
56
|
+
render: () => (
|
|
57
|
+
<div className="flex -space-x-2">
|
|
58
|
+
<Avatar className="border-2 border-background">
|
|
59
|
+
<AvatarImage src="https://github.com/shadcn.png" alt="User 1" />
|
|
60
|
+
<AvatarFallback>U1</AvatarFallback>
|
|
61
|
+
</Avatar>
|
|
62
|
+
<Avatar className="border-2 border-background">
|
|
63
|
+
<AvatarImage src="https://github.com/shadcn.png" alt="User 2" />
|
|
64
|
+
<AvatarFallback>U2</AvatarFallback>
|
|
65
|
+
</Avatar>
|
|
66
|
+
<Avatar className="border-2 border-background">
|
|
67
|
+
<AvatarImage src="https://github.com/shadcn.png" alt="User 3" />
|
|
68
|
+
<AvatarFallback>U3</AvatarFallback>
|
|
69
|
+
</Avatar>
|
|
70
|
+
<Avatar className="border-2 border-background">
|
|
71
|
+
<AvatarFallback>+5</AvatarFallback>
|
|
72
|
+
</Avatar>
|
|
73
|
+
</div>
|
|
74
|
+
),
|
|
75
|
+
};
|
|
76
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import { Avatar, AvatarImage, AvatarFallback } from "./avatar";
|
|
4
|
+
|
|
5
|
+
describe("Avatar", () => {
|
|
6
|
+
it("should render avatar", () => {
|
|
7
|
+
const { container } = render(
|
|
8
|
+
<Avatar>
|
|
9
|
+
<AvatarFallback>JD</AvatarFallback>
|
|
10
|
+
</Avatar>
|
|
11
|
+
);
|
|
12
|
+
const avatar = container.querySelector('[data-slot="avatar"]');
|
|
13
|
+
expect(avatar).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should render avatar image when src is provided", () => {
|
|
17
|
+
const { container } = render(
|
|
18
|
+
<Avatar>
|
|
19
|
+
<AvatarImage src="http://placecats.com/100/100" alt="User" />
|
|
20
|
+
<AvatarFallback>JD</AvatarFallback>
|
|
21
|
+
</Avatar>
|
|
22
|
+
);
|
|
23
|
+
const avatar = container.querySelector('[data-slot="avatar"]');
|
|
24
|
+
expect(avatar).toBeInTheDocument();
|
|
25
|
+
// Radix UI Avatar may not render the image element immediately
|
|
26
|
+
// The image element will be rendered when the image loads
|
|
27
|
+
// We verify that the Avatar component structure is correct
|
|
28
|
+
// The image element may be null initially, but the avatar container exists
|
|
29
|
+
expect(avatar).toBeTruthy();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should render fallback when image fails to load", () => {
|
|
33
|
+
const { getByText } = render(
|
|
34
|
+
<Avatar>
|
|
35
|
+
<AvatarImage src="invalid-url" alt="User" />
|
|
36
|
+
<AvatarFallback>JD</AvatarFallback>
|
|
37
|
+
</Avatar>
|
|
38
|
+
);
|
|
39
|
+
expect(getByText("JD")).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should render fallback when no image is provided", () => {
|
|
43
|
+
const { getByText } = render(
|
|
44
|
+
<Avatar>
|
|
45
|
+
<AvatarFallback>AB</AvatarFallback>
|
|
46
|
+
</Avatar>
|
|
47
|
+
);
|
|
48
|
+
expect(getByText("AB")).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
function Avatar({
|
|
7
|
+
className,
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
|
10
|
+
return (
|
|
11
|
+
<AvatarPrimitive.Root
|
|
12
|
+
data-slot="avatar"
|
|
13
|
+
className={cn(
|
|
14
|
+
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function AvatarImage({
|
|
23
|
+
className,
|
|
24
|
+
...props
|
|
25
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
|
26
|
+
return (
|
|
27
|
+
<AvatarPrimitive.Image
|
|
28
|
+
data-slot="avatar-image"
|
|
29
|
+
className={cn("aspect-square size-full", className)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function AvatarFallback({
|
|
36
|
+
className,
|
|
37
|
+
...props
|
|
38
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
|
39
|
+
return (
|
|
40
|
+
<AvatarPrimitive.Fallback
|
|
41
|
+
data-slot="avatar-fallback"
|
|
42
|
+
className={cn(
|
|
43
|
+
"bg-muted flex size-full items-center justify-center rounded-full",
|
|
44
|
+
className
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { Avatar, AvatarImage, AvatarFallback }
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Badge } from "./badge";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Components/Badge",
|
|
6
|
+
component: Badge,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "padded",
|
|
9
|
+
},
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
argTypes: {
|
|
12
|
+
variant: {
|
|
13
|
+
control: "select",
|
|
14
|
+
options: ["default", "secondary", "destructive", "outline"],
|
|
15
|
+
description: "The visual style variant of the badge.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
} satisfies Meta<typeof Badge>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof meta>;
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
children: "Badge",
|
|
26
|
+
variant: "default",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Variants: Story = {
|
|
31
|
+
render: () => (
|
|
32
|
+
<div className="flex flex-wrap gap-4">
|
|
33
|
+
<Badge variant="default">Default</Badge>
|
|
34
|
+
<Badge variant="secondary">Secondary</Badge>
|
|
35
|
+
<Badge variant="destructive">Destructive</Badge>
|
|
36
|
+
<Badge variant="outline">Outline</Badge>
|
|
37
|
+
</div>
|
|
38
|
+
),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const WithNumbers: Story = {
|
|
42
|
+
render: () => (
|
|
43
|
+
<div className="flex flex-wrap gap-4">
|
|
44
|
+
<Badge>1</Badge>
|
|
45
|
+
<Badge variant="secondary">42</Badge>
|
|
46
|
+
<Badge variant="destructive">999+</Badge>
|
|
47
|
+
<Badge variant="outline">12</Badge>
|
|
48
|
+
</div>
|
|
49
|
+
),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const AsLink: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<div className="flex flex-wrap gap-4">
|
|
55
|
+
<Badge asChild>
|
|
56
|
+
<a href="#">Link Badge</a>
|
|
57
|
+
</Badge>
|
|
58
|
+
<Badge variant="secondary" asChild>
|
|
59
|
+
<a href="#">Secondary Link</a>
|
|
60
|
+
</Badge>
|
|
61
|
+
</div>
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import { Badge } from "./badge";
|
|
4
|
+
|
|
5
|
+
describe("Badge", () => {
|
|
6
|
+
it("should render badge with text", () => {
|
|
7
|
+
const { getByText } = render(<Badge>New</Badge>);
|
|
8
|
+
expect(getByText("New")).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should apply variant classes", () => {
|
|
12
|
+
const { container } = render(<Badge variant="destructive">Error</Badge>);
|
|
13
|
+
const badge = container.querySelector('[data-slot="badge"]');
|
|
14
|
+
expect(badge).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should render with default variant", () => {
|
|
18
|
+
const { container } = render(<Badge>Default</Badge>);
|
|
19
|
+
const badge = container.querySelector('[data-slot="badge"]');
|
|
20
|
+
expect(badge).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should render as link when asChild is true", () => {
|
|
24
|
+
const { container } = render(
|
|
25
|
+
<Badge asChild>
|
|
26
|
+
<a href="/test">Link Badge</a>
|
|
27
|
+
</Badge>
|
|
28
|
+
);
|
|
29
|
+
const link = container.querySelector("a");
|
|
30
|
+
expect(link).toBeInTheDocument();
|
|
31
|
+
expect(link).toHaveTextContent("Link Badge");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
|
14
|
+
secondary:
|
|
15
|
+
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
|
16
|
+
destructive:
|
|
17
|
+
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
18
|
+
outline:
|
|
19
|
+
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: "default",
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
function Badge({
|
|
29
|
+
className,
|
|
30
|
+
variant,
|
|
31
|
+
asChild = false,
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<"span"> &
|
|
34
|
+
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
35
|
+
const Comp = asChild ? Slot : "span"
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Comp
|
|
39
|
+
data-slot="badge"
|
|
40
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { Badge, badgeVariants }
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import {
|
|
3
|
+
Breadcrumb,
|
|
4
|
+
BreadcrumbList,
|
|
5
|
+
BreadcrumbItem,
|
|
6
|
+
BreadcrumbLink,
|
|
7
|
+
BreadcrumbPage,
|
|
8
|
+
BreadcrumbSeparator,
|
|
9
|
+
} from "./breadcrumb";
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: "Components/Breadcrumb",
|
|
13
|
+
component: Breadcrumb,
|
|
14
|
+
parameters: {
|
|
15
|
+
layout: "padded",
|
|
16
|
+
},
|
|
17
|
+
tags: ["autodocs"],
|
|
18
|
+
} satisfies Meta<typeof Breadcrumb>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof meta>;
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {
|
|
24
|
+
render: () => (
|
|
25
|
+
<Breadcrumb>
|
|
26
|
+
<BreadcrumbList>
|
|
27
|
+
<BreadcrumbItem>
|
|
28
|
+
<BreadcrumbLink href="#">Home</BreadcrumbLink>
|
|
29
|
+
</BreadcrumbItem>
|
|
30
|
+
<BreadcrumbSeparator />
|
|
31
|
+
<BreadcrumbItem>
|
|
32
|
+
<BreadcrumbLink href="#">Components</BreadcrumbLink>
|
|
33
|
+
</BreadcrumbItem>
|
|
34
|
+
<BreadcrumbSeparator />
|
|
35
|
+
<BreadcrumbItem>
|
|
36
|
+
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
|
|
37
|
+
</BreadcrumbItem>
|
|
38
|
+
</BreadcrumbList>
|
|
39
|
+
</Breadcrumb>
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const LongPath: Story = {
|
|
44
|
+
render: () => (
|
|
45
|
+
<Breadcrumb>
|
|
46
|
+
<BreadcrumbList>
|
|
47
|
+
<BreadcrumbItem>
|
|
48
|
+
<BreadcrumbLink href="#">Home</BreadcrumbLink>
|
|
49
|
+
</BreadcrumbItem>
|
|
50
|
+
<BreadcrumbSeparator />
|
|
51
|
+
<BreadcrumbItem>
|
|
52
|
+
<BreadcrumbLink href="#">Products</BreadcrumbLink>
|
|
53
|
+
</BreadcrumbItem>
|
|
54
|
+
<BreadcrumbSeparator />
|
|
55
|
+
<BreadcrumbItem>
|
|
56
|
+
<BreadcrumbLink href="#">Electronics</BreadcrumbLink>
|
|
57
|
+
</BreadcrumbItem>
|
|
58
|
+
<BreadcrumbSeparator />
|
|
59
|
+
<BreadcrumbItem>
|
|
60
|
+
<BreadcrumbLink href="#">Computers</BreadcrumbLink>
|
|
61
|
+
</BreadcrumbItem>
|
|
62
|
+
<BreadcrumbSeparator />
|
|
63
|
+
<BreadcrumbItem>
|
|
64
|
+
<BreadcrumbPage>Laptops</BreadcrumbPage>
|
|
65
|
+
</BreadcrumbItem>
|
|
66
|
+
</BreadcrumbList>
|
|
67
|
+
</Breadcrumb>
|
|
68
|
+
),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const Simple: Story = {
|
|
72
|
+
render: () => (
|
|
73
|
+
<Breadcrumb>
|
|
74
|
+
<BreadcrumbList>
|
|
75
|
+
<BreadcrumbItem>
|
|
76
|
+
<BreadcrumbLink href="#">Home</BreadcrumbLink>
|
|
77
|
+
</BreadcrumbItem>
|
|
78
|
+
<BreadcrumbSeparator />
|
|
79
|
+
<BreadcrumbItem>
|
|
80
|
+
<BreadcrumbPage>Current Page</BreadcrumbPage>
|
|
81
|
+
</BreadcrumbItem>
|
|
82
|
+
</BreadcrumbList>
|
|
83
|
+
</Breadcrumb>
|
|
84
|
+
),
|
|
85
|
+
};
|
|
86
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import {
|
|
4
|
+
Breadcrumb,
|
|
5
|
+
BreadcrumbList,
|
|
6
|
+
BreadcrumbItem,
|
|
7
|
+
BreadcrumbLink,
|
|
8
|
+
BreadcrumbPage,
|
|
9
|
+
BreadcrumbSeparator,
|
|
10
|
+
} from "./breadcrumb";
|
|
11
|
+
|
|
12
|
+
describe("Breadcrumb", () => {
|
|
13
|
+
it("should render breadcrumb", () => {
|
|
14
|
+
const { container } = render(
|
|
15
|
+
<Breadcrumb>
|
|
16
|
+
<BreadcrumbList>
|
|
17
|
+
<BreadcrumbItem>
|
|
18
|
+
<BreadcrumbLink href="#">Home</BreadcrumbLink>
|
|
19
|
+
</BreadcrumbItem>
|
|
20
|
+
</BreadcrumbList>
|
|
21
|
+
</Breadcrumb>
|
|
22
|
+
);
|
|
23
|
+
const breadcrumb = container.querySelector('[data-slot="breadcrumb"]');
|
|
24
|
+
expect(breadcrumb).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should render breadcrumb list", () => {
|
|
28
|
+
const { container } = render(
|
|
29
|
+
<Breadcrumb>
|
|
30
|
+
<BreadcrumbList>
|
|
31
|
+
<BreadcrumbItem>
|
|
32
|
+
<BreadcrumbLink href="#">Home</BreadcrumbLink>
|
|
33
|
+
</BreadcrumbItem>
|
|
34
|
+
</BreadcrumbList>
|
|
35
|
+
</Breadcrumb>
|
|
36
|
+
);
|
|
37
|
+
const list = container.querySelector('[data-slot="breadcrumb-list"]');
|
|
38
|
+
expect(list).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should render breadcrumb items", () => {
|
|
42
|
+
const { getByText } = render(
|
|
43
|
+
<Breadcrumb>
|
|
44
|
+
<BreadcrumbList>
|
|
45
|
+
<BreadcrumbItem>
|
|
46
|
+
<BreadcrumbLink href="#">Home</BreadcrumbLink>
|
|
47
|
+
</BreadcrumbItem>
|
|
48
|
+
<BreadcrumbSeparator />
|
|
49
|
+
<BreadcrumbItem>
|
|
50
|
+
<BreadcrumbPage>Current</BreadcrumbPage>
|
|
51
|
+
</BreadcrumbItem>
|
|
52
|
+
</BreadcrumbList>
|
|
53
|
+
</Breadcrumb>
|
|
54
|
+
);
|
|
55
|
+
expect(getByText("Home")).toBeInTheDocument();
|
|
56
|
+
expect(getByText("Current")).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should render breadcrumb separator", () => {
|
|
60
|
+
const { container } = render(
|
|
61
|
+
<Breadcrumb>
|
|
62
|
+
<BreadcrumbList>
|
|
63
|
+
<BreadcrumbItem>
|
|
64
|
+
<BreadcrumbLink href="#">Home</BreadcrumbLink>
|
|
65
|
+
</BreadcrumbItem>
|
|
66
|
+
<BreadcrumbSeparator />
|
|
67
|
+
</BreadcrumbList>
|
|
68
|
+
</Breadcrumb>
|
|
69
|
+
);
|
|
70
|
+
const separator = container.querySelector('[data-slot="breadcrumb-separator"]');
|
|
71
|
+
expect(separator).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|