@fabio.caffarello/react-design-system 1.2.1 → 1.3.1

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 (57) hide show
  1. package/dist/index.cjs +4 -4
  2. package/dist/index.js +696 -246
  3. package/dist/ui/atoms/ErrorMessage/ErrorMessage.d.ts +18 -0
  4. package/dist/ui/atoms/ErrorMessage/ErrorMessage.stories.d.ts +7 -0
  5. package/dist/ui/atoms/ErrorMessage/ErrorMessage.test.d.ts +1 -0
  6. package/dist/ui/atoms/Label/Label.d.ts +20 -0
  7. package/dist/ui/atoms/Label/Label.stories.d.ts +8 -0
  8. package/dist/ui/atoms/Label/Label.test.d.ts +1 -0
  9. package/dist/ui/atoms/NavLink/NavLink.d.ts +20 -0
  10. package/dist/ui/atoms/NavLink/NavLink.stories.d.ts +8 -0
  11. package/dist/ui/atoms/NavLink/NavLink.test.d.ts +1 -0
  12. package/dist/ui/atoms/index.d.ts +3 -0
  13. package/dist/ui/molecules/Breadcrumb/Breadcrumb.d.ts +28 -0
  14. package/dist/ui/molecules/Breadcrumb/Breadcrumb.stories.d.ts +9 -0
  15. package/dist/ui/molecules/Breadcrumb/Breadcrumb.test.d.ts +1 -0
  16. package/dist/ui/molecules/Form/Form.d.ts +24 -0
  17. package/dist/ui/molecules/Form/Form.stories.d.ts +9 -0
  18. package/dist/ui/molecules/Form/Form.test.d.ts +1 -0
  19. package/dist/ui/molecules/Pagination/Pagination.d.ts +28 -0
  20. package/dist/ui/molecules/Pagination/Pagination.stories.d.ts +10 -0
  21. package/dist/ui/molecules/Pagination/Pagination.test.d.ts +1 -0
  22. package/dist/ui/molecules/index.d.ts +4 -0
  23. package/dist/ui/organisms/Modal/Modal.d.ts +25 -0
  24. package/dist/ui/organisms/Modal/Modal.stories.d.ts +9 -0
  25. package/dist/ui/organisms/Modal/Modal.test.d.ts +1 -0
  26. package/dist/ui/organisms/Table/Table.d.ts +35 -0
  27. package/dist/ui/organisms/Table/Table.stories.d.ts +9 -0
  28. package/dist/ui/organisms/Table/Table.test.d.ts +1 -0
  29. package/dist/ui/organisms/index.d.ts +3 -0
  30. package/package.json +1 -1
  31. package/src/ui/atoms/ErrorMessage/ErrorMessage.stories.tsx +81 -0
  32. package/src/ui/atoms/ErrorMessage/ErrorMessage.test.tsx +40 -0
  33. package/src/ui/atoms/ErrorMessage/ErrorMessage.tsx +62 -0
  34. package/src/ui/atoms/Label/Label.stories.tsx +94 -0
  35. package/src/ui/atoms/Label/Label.test.tsx +47 -0
  36. package/src/ui/atoms/Label/Label.tsx +51 -0
  37. package/src/ui/atoms/NavLink/NavLink.stories.tsx +71 -0
  38. package/src/ui/atoms/NavLink/NavLink.test.tsx +44 -0
  39. package/src/ui/atoms/NavLink/NavLink.tsx +63 -0
  40. package/src/ui/atoms/index.ts +6 -0
  41. package/src/ui/molecules/Breadcrumb/Breadcrumb.stories.tsx +75 -0
  42. package/src/ui/molecules/Breadcrumb/Breadcrumb.test.tsx +89 -0
  43. package/src/ui/molecules/Breadcrumb/Breadcrumb.tsx +79 -0
  44. package/src/ui/molecules/Form/Form.stories.tsx +195 -0
  45. package/src/ui/molecules/Form/Form.test.tsx +87 -0
  46. package/src/ui/molecules/Form/Form.tsx +78 -0
  47. package/src/ui/molecules/Pagination/Pagination.stories.tsx +116 -0
  48. package/src/ui/molecules/Pagination/Pagination.test.tsx +112 -0
  49. package/src/ui/molecules/Pagination/Pagination.tsx +170 -0
  50. package/src/ui/molecules/index.ts +7 -0
  51. package/src/ui/organisms/Modal/Modal.stories.tsx +102 -0
  52. package/src/ui/organisms/Modal/Modal.test.tsx +111 -0
  53. package/src/ui/organisms/Modal/Modal.tsx +205 -0
  54. package/src/ui/organisms/Table/Table.stories.tsx +137 -0
  55. package/src/ui/organisms/Table/Table.test.tsx +109 -0
  56. package/src/ui/organisms/Table/Table.tsx +130 -0
  57. package/src/ui/organisms/index.ts +5 -0
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import Label from "./Label";
4
+
5
+ describe("Label", () => {
6
+ it("renders with children", () => {
7
+ render(<Label htmlFor="test">Test Label</Label>);
8
+ expect(screen.getByText("Test Label")).toBeInTheDocument();
9
+ });
10
+
11
+ it("associates with input via htmlFor", () => {
12
+ render(
13
+ <>
14
+ <Label htmlFor="test-input">Test Label</Label>
15
+ <input id="test-input" />
16
+ </>
17
+ );
18
+ const label = screen.getByText("Test Label");
19
+ const input = screen.getByRole("textbox");
20
+ expect(label).toHaveAttribute("for", "test-input");
21
+ expect(input).toHaveAttribute("id", "test-input");
22
+ });
23
+
24
+ it("renders with default variant", () => {
25
+ const { container } = render(<Label htmlFor="test">Label</Label>);
26
+ const label = container.querySelector("label");
27
+ expect(label).toHaveClass("block", "text-sm", "font-medium", "text-gray-700");
28
+ });
29
+
30
+ it("renders with required variant", () => {
31
+ const { container } = render(<Label htmlFor="test" variant="required">Label</Label>);
32
+ const label = container.querySelector("label");
33
+ expect(label).toHaveClass("after:content-['*']", "after:ml-0.5", "after:text-red-500");
34
+ });
35
+
36
+ it("renders with optional variant", () => {
37
+ const { container } = render(<Label htmlFor="test" variant="optional">Label</Label>);
38
+ const label = container.querySelector("label");
39
+ expect(label).toHaveClass("after:content-['(optional)']", "after:ml-1", "after:text-gray-400");
40
+ });
41
+
42
+ it("applies custom className", () => {
43
+ const { container } = render(<Label htmlFor="test" className="custom-class">Label</Label>);
44
+ const label = container.querySelector("label");
45
+ expect(label).toHaveClass("custom-class");
46
+ });
47
+ });
@@ -0,0 +1,51 @@
1
+ import type { LabelHTMLAttributes } from "react";
2
+
3
+ interface Props extends LabelHTMLAttributes<HTMLLabelElement> {
4
+ variant?: "default" | "required" | "optional";
5
+ children: React.ReactNode;
6
+ }
7
+
8
+ /**
9
+ * Label Component
10
+ *
11
+ * A styled label component for form inputs.
12
+ * Follows Atomic Design principles as an Atom component.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <Label htmlFor="email" variant="required">
17
+ * Email Address
18
+ * </Label>
19
+ * ```
20
+ */
21
+ export default function Label({
22
+ variant = "default",
23
+ className = "",
24
+ children,
25
+ ...props
26
+ }: Props) {
27
+ const baseClasses = [
28
+ "block",
29
+ "text-sm",
30
+ "font-medium",
31
+ "text-gray-700",
32
+ ];
33
+
34
+ const variantClasses: Record<NonNullable<Props["variant"]>, string> = {
35
+ default: "",
36
+ required: "after:content-['*'] after:ml-0.5 after:text-red-500",
37
+ optional: "after:content-['(optional)'] after:ml-1 after:text-gray-400 after:font-normal",
38
+ };
39
+
40
+ const classes = [
41
+ ...baseClasses,
42
+ variantClasses[variant],
43
+ className,
44
+ ].filter(Boolean).join(" ");
45
+
46
+ return (
47
+ <label className={classes} {...props}>
48
+ {children}
49
+ </label>
50
+ );
51
+ }
@@ -0,0 +1,71 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import NavLink from "./NavLink";
3
+
4
+ const meta: Meta<typeof NavLink> = {
5
+ title: "UI/Atoms/NavLink",
6
+ component: NavLink,
7
+ parameters: {
8
+ docs: {
9
+ description: {
10
+ component: "A navigation link component with active and disabled states. Used in headers, sidebars, and breadcrumbs.",
11
+ },
12
+ },
13
+ },
14
+ argTypes: {
15
+ variant: {
16
+ control: "select",
17
+ options: ["default", "active", "disabled"],
18
+ description: "Visual variant of the link",
19
+ },
20
+ href: {
21
+ control: "text",
22
+ description: "URL for the link",
23
+ },
24
+ },
25
+ };
26
+
27
+ export const Default: StoryObj<typeof NavLink> = {
28
+ args: {
29
+ children: "Dashboard",
30
+ href: "/dashboard",
31
+ },
32
+ };
33
+
34
+ export const Active: StoryObj<typeof NavLink> = {
35
+ args: {
36
+ children: "Epics",
37
+ href: "/epics",
38
+ variant: "active",
39
+ },
40
+ };
41
+
42
+ export const Disabled: StoryObj<typeof NavLink> = {
43
+ args: {
44
+ children: "Coming Soon",
45
+ variant: "disabled",
46
+ },
47
+ };
48
+
49
+ export const NavigationBar: StoryObj<typeof NavLink> = {
50
+ render: () => (
51
+ <nav className="flex space-x-8">
52
+ <NavLink href="/" variant="active">
53
+ Dashboard
54
+ </NavLink>
55
+ <NavLink href="/epics">
56
+ Epics
57
+ </NavLink>
58
+ <NavLink href="/stories">
59
+ Stories
60
+ </NavLink>
61
+ <NavLink href="/tasks">
62
+ Tasks
63
+ </NavLink>
64
+ <NavLink variant="disabled">
65
+ Coming Soon
66
+ </NavLink>
67
+ </nav>
68
+ ),
69
+ };
70
+
71
+ export default meta;
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import NavLink from "./NavLink";
4
+
5
+ describe("NavLink", () => {
6
+ it("renders as anchor with href", () => {
7
+ render(<NavLink href="/test">Test Link</NavLink>);
8
+ const link = screen.getByText("Test Link");
9
+ expect(link).toBeInTheDocument();
10
+ expect(link.tagName).toBe("A");
11
+ expect(link).toHaveAttribute("href", "/test");
12
+ });
13
+
14
+ it("renders as span when disabled", () => {
15
+ render(<NavLink variant="disabled">Disabled Link</NavLink>);
16
+ const link = screen.getByText("Disabled Link");
17
+ expect(link.tagName).toBe("SPAN");
18
+ expect(link).toHaveAttribute("aria-disabled", "true");
19
+ });
20
+
21
+ it("applies default variant classes", () => {
22
+ const { container } = render(<NavLink href="/test">Link</NavLink>);
23
+ const link = container.querySelector("a");
24
+ expect(link).toHaveClass("border-transparent", "text-gray-500");
25
+ });
26
+
27
+ it("applies active variant classes", () => {
28
+ const { container } = render(<NavLink href="/test" variant="active">Link</NavLink>);
29
+ const link = container.querySelector("a");
30
+ expect(link).toHaveClass("border-indigo-500", "text-gray-900");
31
+ });
32
+
33
+ it("applies disabled variant classes", () => {
34
+ const { container } = render(<NavLink variant="disabled">Link</NavLink>);
35
+ const link = container.querySelector("span");
36
+ expect(link).toHaveClass("text-gray-300", "cursor-not-allowed");
37
+ });
38
+
39
+ it("applies custom className", () => {
40
+ const { container } = render(<NavLink href="/test" className="custom-class">Link</NavLink>);
41
+ const link = container.querySelector("a");
42
+ expect(link).toHaveClass("custom-class");
43
+ });
44
+ });
@@ -0,0 +1,63 @@
1
+ import type { AnchorHTMLAttributes } from "react";
2
+
3
+ interface Props extends AnchorHTMLAttributes<HTMLAnchorElement> {
4
+ variant?: "default" | "active" | "disabled";
5
+ children: React.ReactNode;
6
+ }
7
+
8
+ /**
9
+ * NavLink Component
10
+ *
11
+ * A navigation link component with active and disabled states.
12
+ * Follows Atomic Design principles as an Atom component.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <NavLink href="/dashboard" variant="active">
17
+ * Dashboard
18
+ * </NavLink>
19
+ * ```
20
+ */
21
+ export default function NavLink({
22
+ variant = "default",
23
+ className = "",
24
+ children,
25
+ ...props
26
+ }: Props) {
27
+ const baseClasses = [
28
+ "inline-flex",
29
+ "items-center",
30
+ "px-1",
31
+ "pt-1",
32
+ "border-b-2",
33
+ "text-sm",
34
+ "font-medium",
35
+ "transition-colors",
36
+ ];
37
+
38
+ const variantClasses: Record<NonNullable<Props["variant"]>, string> = {
39
+ default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
40
+ active: "border-indigo-500 text-gray-900",
41
+ disabled: "border-transparent text-gray-300 cursor-not-allowed pointer-events-none",
42
+ };
43
+
44
+ const classes = [
45
+ ...baseClasses,
46
+ variantClasses[variant],
47
+ className,
48
+ ].filter(Boolean).join(" ");
49
+
50
+ if (variant === "disabled") {
51
+ return (
52
+ <span className={classes} aria-disabled="true">
53
+ {children}
54
+ </span>
55
+ );
56
+ }
57
+
58
+ return (
59
+ <a className={classes} {...props}>
60
+ {children}
61
+ </a>
62
+ );
63
+ }
@@ -13,3 +13,9 @@ export { default as Badge } from "./Badge/Badge";
13
13
  export { default as Select } from "./Select/Select";
14
14
 
15
15
  export { default as Textarea } from "./Textarea/Textarea";
16
+
17
+ export { default as Label } from "./Label/Label";
18
+
19
+ export { default as ErrorMessage } from "./ErrorMessage/ErrorMessage";
20
+
21
+ export { default as NavLink } from "./NavLink/NavLink";
@@ -0,0 +1,75 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import Breadcrumb from "./Breadcrumb";
3
+
4
+ const meta: Meta<typeof Breadcrumb> = {
5
+ title: "UI/Molecules/Breadcrumb",
6
+ component: Breadcrumb,
7
+ parameters: {
8
+ docs: {
9
+ description: {
10
+ component: "A breadcrumb navigation component for hierarchical navigation. Accessible with proper ARIA labels.",
11
+ },
12
+ },
13
+ },
14
+ argTypes: {
15
+ items: {
16
+ control: "object",
17
+ description: "Array of breadcrumb items",
18
+ },
19
+ separator: {
20
+ control: "text",
21
+ description: "Separator between items",
22
+ },
23
+ },
24
+ };
25
+
26
+ export const Default: StoryObj<typeof Breadcrumb> = {
27
+ args: {
28
+ items: [
29
+ { label: "Home", href: "/" },
30
+ { label: "Epics", href: "/epics" },
31
+ { label: "Epic Details" },
32
+ ],
33
+ },
34
+ };
35
+
36
+ export const TwoLevels: StoryObj<typeof Breadcrumb> = {
37
+ args: {
38
+ items: [
39
+ { label: "Dashboard", href: "/" },
40
+ { label: "Epics" },
41
+ ],
42
+ },
43
+ };
44
+
45
+ export const ThreeLevels: StoryObj<typeof Breadcrumb> = {
46
+ args: {
47
+ items: [
48
+ { label: "Home", href: "/" },
49
+ { label: "Epics", href: "/epics" },
50
+ { label: "User Authentication", href: "/epics/1" },
51
+ { label: "Edit" },
52
+ ],
53
+ },
54
+ };
55
+
56
+ export const CustomSeparator: StoryObj<typeof Breadcrumb> = {
57
+ args: {
58
+ items: [
59
+ { label: "Home", href: "/" },
60
+ { label: "Epics", href: "/epics" },
61
+ { label: "Details" },
62
+ ],
63
+ separator: "›",
64
+ },
65
+ };
66
+
67
+ export const SingleItem: StoryObj<typeof Breadcrumb> = {
68
+ args: {
69
+ items: [
70
+ { label: "Dashboard" },
71
+ ],
72
+ },
73
+ };
74
+
75
+ export default meta;
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import Breadcrumb from "./Breadcrumb";
4
+
5
+ describe("Breadcrumb", () => {
6
+ it("renders breadcrumb items", () => {
7
+ render(
8
+ <Breadcrumb
9
+ items={[
10
+ { label: "Home", href: "/" },
11
+ { label: "Epics" },
12
+ ]}
13
+ />
14
+ );
15
+ expect(screen.getByText("Home")).toBeInTheDocument();
16
+ expect(screen.getByText("Epics")).toBeInTheDocument();
17
+ });
18
+
19
+ it("has aria-label", () => {
20
+ const { container } = render(
21
+ <Breadcrumb items={[{ label: "Home" }]} />
22
+ );
23
+ const nav = container.querySelector("nav");
24
+ expect(nav).toHaveAttribute("aria-label", "Breadcrumb");
25
+ });
26
+
27
+ it("renders last item as current page", () => {
28
+ render(
29
+ <Breadcrumb
30
+ items={[
31
+ { label: "Home", href: "/" },
32
+ { label: "Current" },
33
+ ]}
34
+ />
35
+ );
36
+ const current = screen.getByText("Current");
37
+ expect(current).toHaveAttribute("aria-current", "page");
38
+ });
39
+
40
+ it("renders links for non-last items with href", () => {
41
+ render(
42
+ <Breadcrumb
43
+ items={[
44
+ { label: "Home", href: "/" },
45
+ { label: "Current" },
46
+ ]}
47
+ />
48
+ );
49
+ const link = screen.getByText("Home");
50
+ expect(link.tagName).toBe("A");
51
+ expect(link).toHaveAttribute("href", "/");
52
+ });
53
+
54
+ it("renders separator between items", () => {
55
+ const { container } = render(
56
+ <Breadcrumb
57
+ items={[
58
+ { label: "Home", href: "/" },
59
+ { label: "Epics", href: "/epics" },
60
+ { label: "Current" },
61
+ ]}
62
+ />
63
+ );
64
+ const separators = container.querySelectorAll('[aria-hidden="true"]');
65
+ expect(separators.length).toBe(2);
66
+ });
67
+
68
+ it("uses custom separator", () => {
69
+ const { container } = render(
70
+ <Breadcrumb
71
+ items={[
72
+ { label: "Home", href: "/" },
73
+ { label: "Current" },
74
+ ]}
75
+ separator="›"
76
+ />
77
+ );
78
+ const separator = container.querySelector('[aria-hidden="true"]');
79
+ expect(separator?.textContent).toBe("›");
80
+ });
81
+
82
+ it("applies custom className", () => {
83
+ const { container } = render(
84
+ <Breadcrumb items={[{ label: "Home" }]} className="custom-class" />
85
+ );
86
+ const nav = container.querySelector("nav");
87
+ expect(nav).toHaveClass("custom-class");
88
+ });
89
+ });
@@ -0,0 +1,79 @@
1
+ import type { HTMLAttributes } from "react";
2
+ import NavLink from "../../atoms/NavLink/NavLink";
3
+
4
+ export interface BreadcrumbItem {
5
+ label: string;
6
+ href?: string;
7
+ }
8
+
9
+ interface Props extends HTMLAttributes<HTMLElement> {
10
+ items: BreadcrumbItem[];
11
+ separator?: string;
12
+ }
13
+
14
+ /**
15
+ * Breadcrumb Component
16
+ *
17
+ * A breadcrumb navigation component for hierarchical navigation.
18
+ * Follows Atomic Design principles as a Molecule component.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <Breadcrumb
23
+ * items={[
24
+ * { label: "Home", href: "/" },
25
+ * { label: "Epics", href: "/epics" },
26
+ * { label: "Epic Details" }
27
+ * ]}
28
+ * />
29
+ * ```
30
+ */
31
+ export default function Breadcrumb({
32
+ items,
33
+ separator = "/",
34
+ className = "",
35
+ ...props
36
+ }: Props) {
37
+ const baseClasses = [
38
+ "flex",
39
+ "items-center",
40
+ "space-x-2",
41
+ "text-sm",
42
+ ];
43
+
44
+ const classes = [
45
+ ...baseClasses,
46
+ className,
47
+ ].filter(Boolean).join(" ");
48
+
49
+ return (
50
+ <nav aria-label="Breadcrumb" className={classes} {...(props as any)}>
51
+ <ol className="flex items-center space-x-2">
52
+ {items.map((item, index) => {
53
+ const isLast = index === items.length - 1;
54
+
55
+ return (
56
+ <li key={index} className="flex items-center">
57
+ {index > 0 && (
58
+ <span className="mx-2 text-gray-400" aria-hidden="true">
59
+ {separator}
60
+ </span>
61
+ )}
62
+ {isLast ? (
63
+ <span className="text-gray-900 font-medium" aria-current="page">
64
+ {item.label}
65
+ </span>
66
+ ) : item.href ? (
67
+ <NavLink href={item.href} variant="default">
68
+ {item.label}
69
+ </NavLink>
70
+ ) : (
71
+ <span className="text-gray-500">{item.label}</span>
72
+ )}
73
+ </li>
74
+ );
75
+ })}
76
+ </ol>
77
+ </nav>
78
+ );
79
+ }