@fabio.caffarello/react-design-system 1.3.1 → 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.
Files changed (33) hide show
  1. package/dist/index.cjs +4 -4
  2. package/dist/index.js +535 -352
  3. package/dist/ui/atoms/Skeleton/Skeleton.d.ts +20 -0
  4. package/dist/ui/atoms/Skeleton/Skeleton.stories.d.ts +24 -0
  5. package/dist/ui/atoms/Skeleton/Skeleton.test.d.ts +1 -0
  6. package/dist/ui/atoms/Tooltip/Tooltip.d.ts +21 -0
  7. package/dist/ui/atoms/Tooltip/Tooltip.stories.d.ts +23 -0
  8. package/dist/ui/atoms/Tooltip/Tooltip.test.d.ts +1 -0
  9. package/dist/ui/atoms/index.d.ts +4 -0
  10. package/dist/ui/molecules/Dropdown/Dropdown.d.ts +31 -0
  11. package/dist/ui/molecules/Dropdown/Dropdown.stories.d.ts +25 -0
  12. package/dist/ui/molecules/Dropdown/Dropdown.test.d.ts +1 -0
  13. package/dist/ui/molecules/EmptyState/EmptyState.d.ts +26 -0
  14. package/dist/ui/molecules/EmptyState/EmptyState.stories.d.ts +21 -0
  15. package/dist/ui/molecules/EmptyState/EmptyState.test.d.ts +1 -0
  16. package/dist/ui/molecules/index.d.ts +4 -0
  17. package/package.json +8 -2
  18. package/src/ui/atoms/NavLink/NavLink.tsx +2 -0
  19. package/src/ui/atoms/Skeleton/Skeleton.stories.tsx +61 -0
  20. package/src/ui/atoms/Skeleton/Skeleton.test.tsx +40 -0
  21. package/src/ui/atoms/Skeleton/Skeleton.tsx +70 -0
  22. package/src/ui/atoms/Tooltip/Tooltip.stories.tsx +60 -0
  23. package/src/ui/atoms/Tooltip/Tooltip.test.tsx +53 -0
  24. package/src/ui/atoms/Tooltip/Tooltip.tsx +87 -0
  25. package/src/ui/atoms/index.ts +6 -0
  26. package/src/ui/molecules/Breadcrumb/Breadcrumb.tsx +2 -0
  27. package/src/ui/molecules/Dropdown/Dropdown.stories.tsx +58 -0
  28. package/src/ui/molecules/Dropdown/Dropdown.test.tsx +73 -0
  29. package/src/ui/molecules/Dropdown/Dropdown.tsx +125 -0
  30. package/src/ui/molecules/EmptyState/EmptyState.stories.tsx +63 -0
  31. package/src/ui/molecules/EmptyState/EmptyState.test.tsx +55 -0
  32. package/src/ui/molecules/EmptyState/EmptyState.tsx +81 -0
  33. package/src/ui/molecules/index.ts +6 -0
@@ -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
+ };
@@ -0,0 +1,73 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import Dropdown from "./Dropdown";
4
+ import { Button } from "../../atoms";
5
+
6
+ describe("Dropdown", () => {
7
+ it("renders trigger", () => {
8
+ render(
9
+ <Dropdown
10
+ trigger={<Button>Actions</Button>}
11
+ items={[]}
12
+ />
13
+ );
14
+
15
+ expect(screen.getByText("Actions")).toBeInTheDocument();
16
+ });
17
+
18
+ it("opens dropdown when trigger is clicked", () => {
19
+ render(
20
+ <Dropdown
21
+ trigger={<Button>Actions</Button>}
22
+ items={[
23
+ { label: "Edit", onClick: vi.fn() },
24
+ ]}
25
+ />
26
+ );
27
+
28
+ const trigger = screen.getByText("Actions");
29
+ fireEvent.click(trigger);
30
+
31
+ expect(screen.getByText("Edit")).toBeInTheDocument();
32
+ });
33
+
34
+ it("calls onClick when item is clicked", () => {
35
+ const handleClick = vi.fn();
36
+ render(
37
+ <Dropdown
38
+ trigger={<Button>Actions</Button>}
39
+ items={[
40
+ { label: "Edit", onClick: handleClick },
41
+ ]}
42
+ />
43
+ );
44
+
45
+ const trigger = screen.getByText("Actions");
46
+ fireEvent.click(trigger);
47
+
48
+ const item = screen.getByText("Edit");
49
+ fireEvent.click(item);
50
+
51
+ expect(handleClick).toHaveBeenCalledTimes(1);
52
+ });
53
+
54
+ it("closes dropdown when clicking outside", () => {
55
+ render(
56
+ <Dropdown
57
+ trigger={<Button>Actions</Button>}
58
+ items={[
59
+ { label: "Edit", onClick: vi.fn() },
60
+ ]}
61
+ />
62
+ );
63
+
64
+ const trigger = screen.getByText("Actions");
65
+ fireEvent.click(trigger);
66
+
67
+ expect(screen.getByText("Edit")).toBeInTheDocument();
68
+
69
+ fireEvent.mouseDown(document.body);
70
+
71
+ expect(screen.queryByText("Edit")).not.toBeInTheDocument();
72
+ });
73
+ });
@@ -0,0 +1,125 @@
1
+ 'use client';
2
+
3
+ import type { HTMLAttributes, ReactNode } from "react";
4
+ import { useState, useRef, useEffect } from "react";
5
+
6
+ export interface DropdownItem {
7
+ label: string;
8
+ onClick: () => void;
9
+ disabled?: boolean;
10
+ variant?: "default" | "danger";
11
+ }
12
+
13
+ export interface DropdownProps extends HTMLAttributes<HTMLDivElement> {
14
+ trigger: ReactNode;
15
+ items: DropdownItem[];
16
+ align?: "left" | "right";
17
+ variant?: "default" | "minimal";
18
+ }
19
+
20
+ /**
21
+ * Dropdown Component
22
+ *
23
+ * A dropdown menu component for displaying actions and options.
24
+ * Follows Atomic Design principles as a Molecule component.
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <Dropdown
29
+ * trigger={<Button>Actions</Button>}
30
+ * items={[
31
+ * { label: "Edit", onClick: () => handleEdit() },
32
+ * { label: "Delete", onClick: () => handleDelete(), variant: "danger" },
33
+ * ]}
34
+ * />
35
+ * ```
36
+ */
37
+ export default function Dropdown({
38
+ trigger,
39
+ items,
40
+ align = "right",
41
+ variant = "default",
42
+ className = "",
43
+ ...props
44
+ }: DropdownProps) {
45
+ const [isOpen, setIsOpen] = useState(false);
46
+ const dropdownRef = useRef<HTMLDivElement>(null);
47
+
48
+ useEffect(() => {
49
+ const handleClickOutside = (event: MouseEvent) => {
50
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
51
+ setIsOpen(false);
52
+ }
53
+ };
54
+
55
+ if (isOpen) {
56
+ document.addEventListener("mousedown", handleClickOutside);
57
+ }
58
+
59
+ return () => {
60
+ document.removeEventListener("mousedown", handleClickOutside);
61
+ };
62
+ }, [isOpen]);
63
+
64
+ const handleItemClick = (item: DropdownItem) => {
65
+ if (!item.disabled) {
66
+ item.onClick();
67
+ setIsOpen(false);
68
+ }
69
+ };
70
+
71
+ const alignClasses = align === "right" ? "right-0" : "left-0";
72
+
73
+ return (
74
+ <div className={`relative inline-block ${className}`} ref={dropdownRef} {...props}>
75
+ <div onClick={() => setIsOpen(!isOpen)}>
76
+ {trigger}
77
+ </div>
78
+
79
+ {isOpen && (
80
+ <>
81
+ <div
82
+ className="fixed inset-0 z-10"
83
+ onClick={() => setIsOpen(false)}
84
+ />
85
+ <div
86
+ className={`absolute z-20 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 ${alignClasses}`}
87
+ role="menu"
88
+ aria-orientation="vertical"
89
+ >
90
+ <div className="py-1" role="none">
91
+ {items.map((item, index) => {
92
+ const itemClasses = [
93
+ "block",
94
+ "px-4",
95
+ "py-2",
96
+ "text-sm",
97
+ "w-full",
98
+ "text-left",
99
+ item.disabled
100
+ ? "text-gray-400 cursor-not-allowed"
101
+ : item.variant === "danger"
102
+ ? "text-red-700 hover:bg-red-50"
103
+ : "text-gray-700 hover:bg-gray-100",
104
+ ].filter(Boolean).join(" ");
105
+
106
+ return (
107
+ <button
108
+ key={index}
109
+ type="button"
110
+ className={itemClasses}
111
+ onClick={() => handleItemClick(item)}
112
+ disabled={item.disabled}
113
+ role="menuitem"
114
+ >
115
+ {item.label}
116
+ </button>
117
+ );
118
+ })}
119
+ </div>
120
+ </div>
121
+ </>
122
+ )}
123
+ </div>
124
+ );
125
+ }
@@ -0,0 +1,63 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import EmptyState from "./EmptyState";
3
+
4
+ const meta = {
5
+ title: "Molecules/EmptyState",
6
+ component: EmptyState,
7
+ parameters: {
8
+ layout: "centered",
9
+ },
10
+ tags: ["autodocs"],
11
+ argTypes: {
12
+ variant: {
13
+ control: "select",
14
+ options: ["default", "withAction", "withIllustration"],
15
+ },
16
+ },
17
+ } satisfies Meta<typeof EmptyState>;
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ export const Default: Story = {
23
+ args: {
24
+ title: "No items found",
25
+ message: "There are no items to display at this time.",
26
+ },
27
+ };
28
+
29
+ export const WithAction: Story = {
30
+ args: {
31
+ title: "No epics yet",
32
+ message: "Get started by creating your first epic to organize your work.",
33
+ actionLabel: "Create Epic",
34
+ onAction: () => console.log("Action clicked"),
35
+ variant: "withAction",
36
+ },
37
+ };
38
+
39
+ export const WithIllustration: Story = {
40
+ args: {
41
+ title: "No stories found",
42
+ message: "This epic doesn't have any stories yet. Add a story to get started.",
43
+ actionLabel: "Add Story",
44
+ onAction: () => console.log("Action clicked"),
45
+ variant: "withIllustration",
46
+ illustration: (
47
+ <svg
48
+ className="mx-auto h-12 w-12 text-gray-400"
49
+ fill="none"
50
+ viewBox="0 0 24 24"
51
+ stroke="currentColor"
52
+ aria-hidden="true"
53
+ >
54
+ <path
55
+ strokeLinecap="round"
56
+ strokeLinejoin="round"
57
+ strokeWidth={2}
58
+ d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
59
+ />
60
+ </svg>
61
+ ),
62
+ },
63
+ };
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import EmptyState from "./EmptyState";
4
+
5
+ describe("EmptyState", () => {
6
+ it("renders title and message", () => {
7
+ render(<EmptyState title="No items" message="No items found" />);
8
+
9
+ expect(screen.getByText("No items")).toBeInTheDocument();
10
+ expect(screen.getByText("No items found")).toBeInTheDocument();
11
+ });
12
+
13
+ it("renders action button when actionLabel and onAction are provided", () => {
14
+ const handleAction = vi.fn();
15
+ render(
16
+ <EmptyState
17
+ title="No items"
18
+ message="No items found"
19
+ actionLabel="Create Item"
20
+ onAction={handleAction}
21
+ />
22
+ );
23
+
24
+ const button = screen.getByText("Create Item");
25
+ expect(button).toBeInTheDocument();
26
+
27
+ button.click();
28
+ expect(handleAction).toHaveBeenCalledTimes(1);
29
+ });
30
+
31
+ it("renders illustration when provided", () => {
32
+ const illustration = <div data-testid="illustration">Illustration</div>;
33
+ render(
34
+ <EmptyState
35
+ title="No items"
36
+ message="No items found"
37
+ illustration={illustration}
38
+ />
39
+ );
40
+
41
+ expect(screen.getByTestId("illustration")).toBeInTheDocument();
42
+ });
43
+
44
+ it("applies custom className", () => {
45
+ const { container } = render(
46
+ <EmptyState
47
+ title="No items"
48
+ message="No items found"
49
+ className="custom-class"
50
+ />
51
+ );
52
+
53
+ expect(container.firstChild).toHaveClass("custom-class");
54
+ });
55
+ });
@@ -0,0 +1,81 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import { Button } from "../../atoms";
3
+ import { Text } from "../../atoms";
4
+
5
+ export interface EmptyStateProps extends HTMLAttributes<HTMLDivElement> {
6
+ title: string;
7
+ message: string;
8
+ actionLabel?: string;
9
+ onAction?: () => void;
10
+ illustration?: ReactNode;
11
+ variant?: "default" | "withAction" | "withIllustration";
12
+ }
13
+
14
+ /**
15
+ * EmptyState Component
16
+ *
17
+ * A component for displaying empty states when there's no content to show.
18
+ * Follows Atomic Design principles as a Molecule component.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <EmptyState
23
+ * title="No epics yet"
24
+ * message="Get started by creating your first epic"
25
+ * actionLabel="Create Epic"
26
+ * onAction={() => router.push('/epics/new')}
27
+ * />
28
+ * ```
29
+ */
30
+ export default function EmptyState({
31
+ title,
32
+ message,
33
+ actionLabel,
34
+ onAction,
35
+ illustration,
36
+ variant = "default",
37
+ className = "",
38
+ ...props
39
+ }: EmptyStateProps) {
40
+ const baseClasses = [
41
+ "flex",
42
+ "flex-col",
43
+ "items-center",
44
+ "justify-center",
45
+ "text-center",
46
+ "py-12",
47
+ "px-4",
48
+ ];
49
+
50
+ const classes = [
51
+ ...baseClasses,
52
+ className,
53
+ ].filter(Boolean).join(" ");
54
+
55
+ const showAction = variant === "withAction" || (actionLabel && onAction);
56
+ const showIllustration = variant === "withIllustration" || illustration;
57
+
58
+ return (
59
+ <div className={classes} {...props}>
60
+ {showIllustration && illustration && (
61
+ <div className="mb-4">
62
+ {illustration}
63
+ </div>
64
+ )}
65
+
66
+ <Text as="h3" className="text-lg font-semibold text-gray-900 mb-2">
67
+ {title}
68
+ </Text>
69
+
70
+ <Text as="p" className="text-sm text-gray-500 mb-6 max-w-sm">
71
+ {message}
72
+ </Text>
73
+
74
+ {showAction && actionLabel && onAction && (
75
+ <Button variant="regular" onClick={onAction}>
76
+ {actionLabel}
77
+ </Button>
78
+ )}
79
+ </div>
80
+ );
81
+ }
@@ -8,3 +8,9 @@ export { default as Breadcrumb } from "./Breadcrumb/Breadcrumb";
8
8
  export type { BreadcrumbItem } from "./Breadcrumb/Breadcrumb";
9
9
 
10
10
  export { default as Pagination } from "./Pagination/Pagination";
11
+
12
+ export { default as EmptyState } from "./EmptyState/EmptyState";
13
+ export type { EmptyStateProps } from "./EmptyState/EmptyState";
14
+
15
+ export { default as Dropdown } from "./Dropdown/Dropdown";
16
+ export type { DropdownProps, DropdownItem } from "./Dropdown/Dropdown";