@fabio.caffarello/react-design-system 1.3.2 → 1.4.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/dist/index.cjs +4 -4
- package/dist/index.js +535 -352
- package/dist/ui/atoms/Skeleton/Skeleton.d.ts +20 -0
- package/dist/ui/atoms/Skeleton/Skeleton.stories.d.ts +24 -0
- package/dist/ui/atoms/Skeleton/Skeleton.test.d.ts +1 -0
- package/dist/ui/atoms/Tooltip/Tooltip.d.ts +21 -0
- package/dist/ui/atoms/Tooltip/Tooltip.stories.d.ts +23 -0
- package/dist/ui/atoms/Tooltip/Tooltip.test.d.ts +1 -0
- package/dist/ui/atoms/index.d.ts +4 -0
- package/dist/ui/molecules/Dropdown/Dropdown.d.ts +31 -0
- package/dist/ui/molecules/Dropdown/Dropdown.stories.d.ts +25 -0
- package/dist/ui/molecules/Dropdown/Dropdown.test.d.ts +1 -0
- package/dist/ui/molecules/EmptyState/EmptyState.d.ts +26 -0
- package/dist/ui/molecules/EmptyState/EmptyState.stories.d.ts +21 -0
- package/dist/ui/molecules/EmptyState/EmptyState.test.d.ts +1 -0
- package/dist/ui/molecules/index.d.ts +4 -0
- package/package.json +1 -1
- package/src/ui/atoms/Skeleton/Skeleton.stories.tsx +61 -0
- package/src/ui/atoms/Skeleton/Skeleton.test.tsx +40 -0
- package/src/ui/atoms/Skeleton/Skeleton.tsx +70 -0
- package/src/ui/atoms/Tooltip/Tooltip.stories.tsx +60 -0
- package/src/ui/atoms/Tooltip/Tooltip.test.tsx +53 -0
- package/src/ui/atoms/Tooltip/Tooltip.tsx +87 -0
- package/src/ui/atoms/index.ts +6 -0
- package/src/ui/molecules/Dropdown/Dropdown.stories.tsx +58 -0
- package/src/ui/molecules/Dropdown/Dropdown.test.tsx +73 -0
- package/src/ui/molecules/Dropdown/Dropdown.tsx +125 -0
- package/src/ui/molecules/EmptyState/EmptyState.stories.tsx +63 -0
- package/src/ui/molecules/EmptyState/EmptyState.test.tsx +55 -0
- package/src/ui/molecules/EmptyState/EmptyState.tsx +81 -0
- package/src/ui/molecules/index.ts +6 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
export interface SkeletonProps extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
variant?: "text" | "card" | "list" | "circle";
|
|
4
|
+
width?: string;
|
|
5
|
+
height?: string;
|
|
6
|
+
lines?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Skeleton Component
|
|
10
|
+
*
|
|
11
|
+
* A skeleton loader component for displaying loading states.
|
|
12
|
+
* Follows Atomic Design principles as an Atom component.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <Skeleton variant="card" />
|
|
17
|
+
* <Skeleton variant="text" lines={3} />
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export default function Skeleton({ variant, width, height, lines, className, ...props }: SkeletonProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { StoryObj } from "@storybook/react";
|
|
2
|
+
import Skeleton from "./Skeleton";
|
|
3
|
+
declare const meta: {
|
|
4
|
+
title: string;
|
|
5
|
+
component: typeof Skeleton;
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: string;
|
|
8
|
+
};
|
|
9
|
+
tags: string[];
|
|
10
|
+
argTypes: {
|
|
11
|
+
variant: {
|
|
12
|
+
control: "select";
|
|
13
|
+
options: string[];
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
export declare const Text: Story;
|
|
20
|
+
export declare const TextMultipleLines: Story;
|
|
21
|
+
export declare const Card: Story;
|
|
22
|
+
export declare const List: Story;
|
|
23
|
+
export declare const Circle: Story;
|
|
24
|
+
export declare const CustomSize: Story;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
export interface TooltipProps extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
content: string;
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
position?: "top" | "bottom" | "left" | "right";
|
|
6
|
+
delay?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Tooltip Component
|
|
10
|
+
*
|
|
11
|
+
* A tooltip component for displaying additional information on hover.
|
|
12
|
+
* Follows Atomic Design principles as an Atom component.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <Tooltip content="This is a tooltip">
|
|
17
|
+
* <Button>Hover me</Button>
|
|
18
|
+
* </Tooltip>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export default function Tooltip({ content, children, position, delay, className, ...props }: TooltipProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { StoryObj } from "@storybook/react";
|
|
2
|
+
import Tooltip from "./Tooltip";
|
|
3
|
+
declare const meta: {
|
|
4
|
+
title: string;
|
|
5
|
+
component: typeof Tooltip;
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: string;
|
|
8
|
+
};
|
|
9
|
+
tags: string[];
|
|
10
|
+
argTypes: {
|
|
11
|
+
position: {
|
|
12
|
+
control: "select";
|
|
13
|
+
options: string[];
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
export declare const Default: Story;
|
|
20
|
+
export declare const Top: Story;
|
|
21
|
+
export declare const Bottom: Story;
|
|
22
|
+
export declare const Left: Story;
|
|
23
|
+
export declare const Right: Story;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/ui/atoms/index.d.ts
CHANGED
|
@@ -9,3 +9,7 @@ export { default as Textarea } from "./Textarea/Textarea";
|
|
|
9
9
|
export { default as Label } from "./Label/Label";
|
|
10
10
|
export { default as ErrorMessage } from "./ErrorMessage/ErrorMessage";
|
|
11
11
|
export { default as NavLink } from "./NavLink/NavLink";
|
|
12
|
+
export { default as Tooltip } from "./Tooltip/Tooltip";
|
|
13
|
+
export type { TooltipProps } from "./Tooltip/Tooltip";
|
|
14
|
+
export { default as Skeleton } from "./Skeleton/Skeleton";
|
|
15
|
+
export type { SkeletonProps } from "./Skeleton/Skeleton";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
export interface DropdownItem {
|
|
3
|
+
label: string;
|
|
4
|
+
onClick: () => void;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
variant?: "default" | "danger";
|
|
7
|
+
}
|
|
8
|
+
export interface DropdownProps extends HTMLAttributes<HTMLDivElement> {
|
|
9
|
+
trigger: ReactNode;
|
|
10
|
+
items: DropdownItem[];
|
|
11
|
+
align?: "left" | "right";
|
|
12
|
+
variant?: "default" | "minimal";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Dropdown Component
|
|
16
|
+
*
|
|
17
|
+
* A dropdown menu component for displaying actions and options.
|
|
18
|
+
* Follows Atomic Design principles as a Molecule component.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <Dropdown
|
|
23
|
+
* trigger={<Button>Actions</Button>}
|
|
24
|
+
* items={[
|
|
25
|
+
* { label: "Edit", onClick: () => handleEdit() },
|
|
26
|
+
* { label: "Delete", onClick: () => handleDelete(), variant: "danger" },
|
|
27
|
+
* ]}
|
|
28
|
+
* />
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export default function Dropdown({ trigger, items, align, variant, className, ...props }: DropdownProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { StoryObj } from "@storybook/react";
|
|
2
|
+
import Dropdown from "./Dropdown";
|
|
3
|
+
declare const meta: {
|
|
4
|
+
title: string;
|
|
5
|
+
component: typeof Dropdown;
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: string;
|
|
8
|
+
};
|
|
9
|
+
tags: string[];
|
|
10
|
+
argTypes: {
|
|
11
|
+
align: {
|
|
12
|
+
control: "select";
|
|
13
|
+
options: string[];
|
|
14
|
+
};
|
|
15
|
+
variant: {
|
|
16
|
+
control: "select";
|
|
17
|
+
options: string[];
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
export declare const Default: Story;
|
|
24
|
+
export declare const WithDisabledItem: Story;
|
|
25
|
+
export declare const AlignedLeft: Story;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
export interface EmptyStateProps extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
title: string;
|
|
4
|
+
message: string;
|
|
5
|
+
actionLabel?: string;
|
|
6
|
+
onAction?: () => void;
|
|
7
|
+
illustration?: ReactNode;
|
|
8
|
+
variant?: "default" | "withAction" | "withIllustration";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* EmptyState Component
|
|
12
|
+
*
|
|
13
|
+
* A component for displaying empty states when there's no content to show.
|
|
14
|
+
* Follows Atomic Design principles as a Molecule component.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <EmptyState
|
|
19
|
+
* title="No epics yet"
|
|
20
|
+
* message="Get started by creating your first epic"
|
|
21
|
+
* actionLabel="Create Epic"
|
|
22
|
+
* onAction={() => router.push('/epics/new')}
|
|
23
|
+
* />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export default function EmptyState({ title, message, actionLabel, onAction, illustration, variant, className, ...props }: EmptyStateProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { StoryObj } from "@storybook/react";
|
|
2
|
+
import EmptyState from "./EmptyState";
|
|
3
|
+
declare const meta: {
|
|
4
|
+
title: string;
|
|
5
|
+
component: typeof EmptyState;
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: string;
|
|
8
|
+
};
|
|
9
|
+
tags: string[];
|
|
10
|
+
argTypes: {
|
|
11
|
+
variant: {
|
|
12
|
+
control: "select";
|
|
13
|
+
options: string[];
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
export declare const Default: Story;
|
|
20
|
+
export declare const WithAction: Story;
|
|
21
|
+
export declare const WithIllustration: Story;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -4,3 +4,7 @@ export { default as Form } from "./Form/Form";
|
|
|
4
4
|
export { default as Breadcrumb } from "./Breadcrumb/Breadcrumb";
|
|
5
5
|
export type { BreadcrumbItem } from "./Breadcrumb/Breadcrumb";
|
|
6
6
|
export { default as Pagination } from "./Pagination/Pagination";
|
|
7
|
+
export { default as EmptyState } from "./EmptyState/EmptyState";
|
|
8
|
+
export type { EmptyStateProps } from "./EmptyState/EmptyState";
|
|
9
|
+
export { default as Dropdown } from "./Dropdown/Dropdown";
|
|
10
|
+
export type { DropdownProps, DropdownItem } from "./Dropdown/Dropdown";
|
package/package.json
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Skeleton from "./Skeleton";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Atoms/Skeleton",
|
|
6
|
+
component: Skeleton,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "centered",
|
|
9
|
+
},
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
argTypes: {
|
|
12
|
+
variant: {
|
|
13
|
+
control: "select",
|
|
14
|
+
options: ["text", "card", "list", "circle"],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
} satisfies Meta<typeof Skeleton>;
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
type Story = StoryObj<typeof meta>;
|
|
21
|
+
|
|
22
|
+
export const Text: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
variant: "text",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const TextMultipleLines: Story = {
|
|
29
|
+
args: {
|
|
30
|
+
variant: "text",
|
|
31
|
+
lines: 3,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const Card: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
variant: "card",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const List: Story = {
|
|
42
|
+
args: {
|
|
43
|
+
variant: "list",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Circle: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
variant: "circle",
|
|
50
|
+
width: "48px",
|
|
51
|
+
height: "48px",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const CustomSize: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
variant: "text",
|
|
58
|
+
width: "200px",
|
|
59
|
+
height: "20px",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
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("renders with default variant", () => {
|
|
7
|
+
const { container } = render(<Skeleton />);
|
|
8
|
+
const skeleton = container.firstChild as HTMLElement;
|
|
9
|
+
expect(skeleton).toHaveClass("h-4");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("renders with card variant", () => {
|
|
13
|
+
const { container } = render(<Skeleton variant="card" />);
|
|
14
|
+
const skeleton = container.firstChild as HTMLElement;
|
|
15
|
+
expect(skeleton).toHaveClass("h-32");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("renders multiple lines for text variant", () => {
|
|
19
|
+
const { container } = render(<Skeleton variant="text" lines={3} />);
|
|
20
|
+
const lines = container.querySelectorAll(".animate-pulse");
|
|
21
|
+
expect(lines.length).toBe(3);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("applies custom width and height", () => {
|
|
25
|
+
const { container } = render(
|
|
26
|
+
<Skeleton width="100px" height="50px" />
|
|
27
|
+
);
|
|
28
|
+
const skeleton = container.firstChild as HTMLElement;
|
|
29
|
+
expect(skeleton.style.width).toBe("100px");
|
|
30
|
+
expect(skeleton.style.height).toBe("50px");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("applies custom className", () => {
|
|
34
|
+
const { container } = render(
|
|
35
|
+
<Skeleton className="custom-class" />
|
|
36
|
+
);
|
|
37
|
+
const skeleton = container.firstChild as HTMLElement;
|
|
38
|
+
expect(skeleton).toHaveClass("custom-class");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
export interface SkeletonProps extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
variant?: "text" | "card" | "list" | "circle";
|
|
5
|
+
width?: string;
|
|
6
|
+
height?: string;
|
|
7
|
+
lines?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Skeleton Component
|
|
12
|
+
*
|
|
13
|
+
* A skeleton loader component for displaying loading states.
|
|
14
|
+
* Follows Atomic Design principles as an Atom component.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <Skeleton variant="card" />
|
|
19
|
+
* <Skeleton variant="text" lines={3} />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export default function Skeleton({
|
|
23
|
+
variant = "text",
|
|
24
|
+
width,
|
|
25
|
+
height,
|
|
26
|
+
lines = 1,
|
|
27
|
+
className = "",
|
|
28
|
+
...props
|
|
29
|
+
}: SkeletonProps) {
|
|
30
|
+
const baseClasses = [
|
|
31
|
+
"animate-pulse",
|
|
32
|
+
"bg-gray-200",
|
|
33
|
+
"rounded",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const variantClasses: Record<NonNullable<SkeletonProps["variant"]>, string> = {
|
|
37
|
+
text: "h-4",
|
|
38
|
+
card: "h-32",
|
|
39
|
+
list: "h-12",
|
|
40
|
+
circle: "rounded-full",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const classes = [
|
|
44
|
+
...baseClasses,
|
|
45
|
+
variantClasses[variant],
|
|
46
|
+
className,
|
|
47
|
+
].filter(Boolean).join(" ");
|
|
48
|
+
|
|
49
|
+
const style: React.CSSProperties = {};
|
|
50
|
+
if (width) style.width = width;
|
|
51
|
+
if (height) style.height = height;
|
|
52
|
+
|
|
53
|
+
if (variant === "text" && lines > 1) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="space-y-2" {...props}>
|
|
56
|
+
{Array.from({ length: lines }).map((_, index) => (
|
|
57
|
+
<div
|
|
58
|
+
key={index}
|
|
59
|
+
className={classes}
|
|
60
|
+
style={index === lines - 1 ? { width: "75%" } : style}
|
|
61
|
+
/>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className={classes} style={style} {...props} />
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Tooltip from "./Tooltip";
|
|
3
|
+
import Button from "../Button/Button";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Atoms/Tooltip",
|
|
7
|
+
component: Tooltip,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "centered",
|
|
10
|
+
},
|
|
11
|
+
tags: ["autodocs"],
|
|
12
|
+
argTypes: {
|
|
13
|
+
position: {
|
|
14
|
+
control: "select",
|
|
15
|
+
options: ["top", "bottom", "left", "right"],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
} satisfies Meta<typeof Tooltip>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof meta>;
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
content: "This is a tooltip",
|
|
26
|
+
children: <Button>Hover me</Button>,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Top: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
content: "Tooltip on top",
|
|
33
|
+
children: <Button>Hover me</Button>,
|
|
34
|
+
position: "top",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const Bottom: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
content: "Tooltip on bottom",
|
|
41
|
+
children: <Button>Hover me</Button>,
|
|
42
|
+
position: "bottom",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Left: Story = {
|
|
47
|
+
args: {
|
|
48
|
+
content: "Tooltip on left",
|
|
49
|
+
children: <Button>Hover me</Button>,
|
|
50
|
+
position: "left",
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Right: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
content: "Tooltip on right",
|
|
57
|
+
children: <Button>Hover me</Button>,
|
|
58
|
+
position: "right",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { fireEvent } from "@testing-library/react";
|
|
4
|
+
import Tooltip from "./Tooltip";
|
|
5
|
+
import Button from "../Button/Button";
|
|
6
|
+
|
|
7
|
+
describe("Tooltip", () => {
|
|
8
|
+
it("renders children", () => {
|
|
9
|
+
render(
|
|
10
|
+
<Tooltip content="Tooltip content">
|
|
11
|
+
<Button>Button</Button>
|
|
12
|
+
</Tooltip>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
expect(screen.getByText("Button")).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("shows tooltip on hover", async () => {
|
|
19
|
+
render(
|
|
20
|
+
<Tooltip content="Tooltip content" delay={0}>
|
|
21
|
+
<Button>Button</Button>
|
|
22
|
+
</Tooltip>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const button = screen.getByText("Button");
|
|
26
|
+
fireEvent.mouseEnter(button);
|
|
27
|
+
|
|
28
|
+
await waitFor(() => {
|
|
29
|
+
expect(screen.getByText("Tooltip content")).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("hides tooltip on mouse leave", async () => {
|
|
34
|
+
render(
|
|
35
|
+
<Tooltip content="Tooltip content" delay={0}>
|
|
36
|
+
<Button>Button</Button>
|
|
37
|
+
</Tooltip>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const button = screen.getByText("Button");
|
|
41
|
+
fireEvent.mouseEnter(button);
|
|
42
|
+
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(screen.getByText("Tooltip content")).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
fireEvent.mouseLeave(button);
|
|
48
|
+
|
|
49
|
+
await waitFor(() => {
|
|
50
|
+
expect(screen.queryByText("Tooltip content")).not.toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
|
|
6
|
+
export interface TooltipProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
content: string;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
position?: "top" | "bottom" | "left" | "right";
|
|
10
|
+
delay?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Tooltip Component
|
|
15
|
+
*
|
|
16
|
+
* A tooltip component for displaying additional information on hover.
|
|
17
|
+
* Follows Atomic Design principles as an Atom component.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <Tooltip content="This is a tooltip">
|
|
22
|
+
* <Button>Hover me</Button>
|
|
23
|
+
* </Tooltip>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export default function Tooltip({
|
|
27
|
+
content,
|
|
28
|
+
children,
|
|
29
|
+
position = "top",
|
|
30
|
+
delay = 200,
|
|
31
|
+
className = "",
|
|
32
|
+
...props
|
|
33
|
+
}: TooltipProps) {
|
|
34
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
35
|
+
const [timeoutId, setTimeoutId] = useState<number | null>(null);
|
|
36
|
+
|
|
37
|
+
const handleMouseEnter = () => {
|
|
38
|
+
const id = setTimeout(() => {
|
|
39
|
+
setIsVisible(true);
|
|
40
|
+
}, delay);
|
|
41
|
+
setTimeoutId(id);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleMouseLeave = () => {
|
|
45
|
+
if (timeoutId) {
|
|
46
|
+
clearTimeout(timeoutId);
|
|
47
|
+
setTimeoutId(null);
|
|
48
|
+
}
|
|
49
|
+
setIsVisible(false);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const positionClasses: Record<NonNullable<TooltipProps["position"]>, string> = {
|
|
53
|
+
top: "bottom-full left-1/2 transform -translate-x-1/2 mb-2",
|
|
54
|
+
bottom: "top-full left-1/2 transform -translate-x-1/2 mt-2",
|
|
55
|
+
left: "right-full top-1/2 transform -translate-y-1/2 mr-2",
|
|
56
|
+
right: "left-full top-1/2 transform -translate-y-1/2 ml-2",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const arrowClasses: Record<NonNullable<TooltipProps["position"]>, string> = {
|
|
60
|
+
top: "top-full left-1/2 transform -translate-x-1/2 border-t-gray-900",
|
|
61
|
+
bottom: "bottom-full left-1/2 transform -translate-x-1/2 border-b-gray-900",
|
|
62
|
+
left: "left-full top-1/2 transform -translate-y-1/2 border-l-gray-900",
|
|
63
|
+
right: "right-full top-1/2 transform -translate-y-1/2 border-r-gray-900",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
className={`relative inline-block ${className}`}
|
|
69
|
+
onMouseEnter={handleMouseEnter}
|
|
70
|
+
onMouseLeave={handleMouseLeave}
|
|
71
|
+
{...props}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
{isVisible && (
|
|
75
|
+
<div
|
|
76
|
+
className={`absolute z-50 px-2 py-1 text-xs text-white bg-gray-900 rounded shadow-lg whitespace-nowrap ${positionClasses[position]}`}
|
|
77
|
+
role="tooltip"
|
|
78
|
+
>
|
|
79
|
+
{content}
|
|
80
|
+
<div
|
|
81
|
+
className={`absolute w-0 h-0 border-4 border-transparent ${arrowClasses[position]}`}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
package/src/ui/atoms/index.ts
CHANGED
|
@@ -19,3 +19,9 @@ export { default as Label } from "./Label/Label";
|
|
|
19
19
|
export { default as ErrorMessage } from "./ErrorMessage/ErrorMessage";
|
|
20
20
|
|
|
21
21
|
export { default as NavLink } from "./NavLink/NavLink";
|
|
22
|
+
|
|
23
|
+
export { default as Tooltip } from "./Tooltip/Tooltip";
|
|
24
|
+
export type { TooltipProps } from "./Tooltip/Tooltip";
|
|
25
|
+
|
|
26
|
+
export { default as Skeleton } from "./Skeleton/Skeleton";
|
|
27
|
+
export type { SkeletonProps } from "./Skeleton/Skeleton";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Dropdown from "./Dropdown";
|
|
3
|
+
import { Button } from "../../atoms";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Molecules/Dropdown",
|
|
7
|
+
component: Dropdown,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "centered",
|
|
10
|
+
},
|
|
11
|
+
tags: ["autodocs"],
|
|
12
|
+
argTypes: {
|
|
13
|
+
align: {
|
|
14
|
+
control: "select",
|
|
15
|
+
options: ["left", "right"],
|
|
16
|
+
},
|
|
17
|
+
variant: {
|
|
18
|
+
control: "select",
|
|
19
|
+
options: ["default", "minimal"],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
} satisfies Meta<typeof Dropdown>;
|
|
23
|
+
|
|
24
|
+
export default meta;
|
|
25
|
+
type Story = StoryObj<typeof meta>;
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
trigger: <Button>Actions</Button>,
|
|
30
|
+
items: [
|
|
31
|
+
{ label: "Edit", onClick: () => console.log("Edit clicked") },
|
|
32
|
+
{ label: "Duplicate", onClick: () => console.log("Duplicate clicked") },
|
|
33
|
+
{ label: "Delete", onClick: () => console.log("Delete clicked"), variant: "danger" },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const WithDisabledItem: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
trigger: <Button>Actions</Button>,
|
|
41
|
+
items: [
|
|
42
|
+
{ label: "Edit", onClick: () => console.log("Edit clicked") },
|
|
43
|
+
{ label: "Archive", onClick: () => console.log("Archive clicked"), disabled: true },
|
|
44
|
+
{ label: "Delete", onClick: () => console.log("Delete clicked"), variant: "danger" },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const AlignedLeft: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
trigger: <Button>Menu</Button>,
|
|
52
|
+
items: [
|
|
53
|
+
{ label: "Option 1", onClick: () => console.log("Option 1") },
|
|
54
|
+
{ label: "Option 2", onClick: () => console.log("Option 2") },
|
|
55
|
+
],
|
|
56
|
+
align: "left",
|
|
57
|
+
},
|
|
58
|
+
};
|