@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,195 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import Form from "./Form";
3
+ import { Input, Label, Button, Textarea, Select, ErrorMessage } from "../../atoms";
4
+
5
+ const meta: Meta<typeof Form> = {
6
+ title: "UI/Molecules/Form",
7
+ component: Form,
8
+ parameters: {
9
+ docs: {
10
+ description: {
11
+ component: "A wrapper component for forms with validation states, error/success messages, and layout.",
12
+ },
13
+ },
14
+ },
15
+ argTypes: {
16
+ loading: {
17
+ control: "boolean",
18
+ description: "Whether the form is in a loading state",
19
+ },
20
+ error: {
21
+ control: "text",
22
+ description: "Global error message to display",
23
+ },
24
+ success: {
25
+ control: "text",
26
+ description: "Success message to display",
27
+ },
28
+ },
29
+ };
30
+
31
+ export const Default: StoryObj<typeof Form> = {
32
+ args: {
33
+ onSubmit: (e) => {
34
+ e.preventDefault();
35
+ alert("Form submitted!");
36
+ },
37
+ },
38
+ render: (args) => (
39
+ <Form {...args} className="max-w-md">
40
+ <div className="space-y-2">
41
+ <Label htmlFor="name" variant="required">
42
+ Name
43
+ </Label>
44
+ <Input id="name" name="name" placeholder="Enter your name" required />
45
+ </div>
46
+ <div className="space-y-2">
47
+ <Label htmlFor="email" variant="required">
48
+ Email
49
+ </Label>
50
+ <Input id="email" name="email" type="email" placeholder="Enter your email" required />
51
+ </div>
52
+ <Button type="submit" variant="regular">
53
+ Submit
54
+ </Button>
55
+ </Form>
56
+ ),
57
+ };
58
+
59
+ export const WithError: StoryObj<typeof Form> = {
60
+ args: {
61
+ error: "Please fix the errors below and try again.",
62
+ onSubmit: (e) => {
63
+ e.preventDefault();
64
+ },
65
+ },
66
+ render: (args) => (
67
+ <Form {...args} className="max-w-md">
68
+ <div className="space-y-2">
69
+ <Label htmlFor="email" variant="required">
70
+ Email
71
+ </Label>
72
+ <Input
73
+ id="email"
74
+ name="email"
75
+ type="email"
76
+ aria-invalid="true"
77
+ aria-describedby="email-error"
78
+ />
79
+ <ErrorMessage message="Please enter a valid email address" id="email-error" />
80
+ </div>
81
+ <Button type="submit" variant="regular">
82
+ Submit
83
+ </Button>
84
+ </Form>
85
+ ),
86
+ };
87
+
88
+ export const WithSuccess: StoryObj<typeof Form> = {
89
+ args: {
90
+ success: "Form submitted successfully!",
91
+ onSubmit: (e) => {
92
+ e.preventDefault();
93
+ },
94
+ },
95
+ render: (args) => (
96
+ <Form {...args} className="max-w-md">
97
+ <div className="space-y-2">
98
+ <Label htmlFor="name" variant="required">
99
+ Name
100
+ </Label>
101
+ <Input id="name" name="name" />
102
+ </div>
103
+ <Button type="submit" variant="regular">
104
+ Submit
105
+ </Button>
106
+ </Form>
107
+ ),
108
+ };
109
+
110
+ export const Loading: StoryObj<typeof Form> = {
111
+ args: {
112
+ loading: true,
113
+ onSubmit: (e) => {
114
+ e.preventDefault();
115
+ },
116
+ },
117
+ render: (args) => (
118
+ <Form {...args} className="max-w-md">
119
+ <div className="space-y-2">
120
+ <Label htmlFor="name" variant="required">
121
+ Name
122
+ </Label>
123
+ <Input id="name" name="name" disabled />
124
+ </div>
125
+ <Button type="submit" variant="regular" disabled>
126
+ Submitting...
127
+ </Button>
128
+ </Form>
129
+ ),
130
+ };
131
+
132
+ export const CompleteForm: StoryObj<typeof Form> = {
133
+ args: {
134
+ onSubmit: (e) => {
135
+ e.preventDefault();
136
+ alert("Form submitted!");
137
+ },
138
+ },
139
+ render: (args) => (
140
+ <Form {...args} className="max-w-md space-y-4">
141
+ <div className="space-y-2">
142
+ <Label htmlFor="title" variant="required">
143
+ Title
144
+ </Label>
145
+ <Input id="title" name="title" placeholder="Enter title" required />
146
+ </div>
147
+ <div className="space-y-2">
148
+ <Label htmlFor="description" variant="required">
149
+ Description
150
+ </Label>
151
+ <Textarea id="description" name="description" rows={4} placeholder="Enter description" required />
152
+ </div>
153
+ <div className="space-y-2">
154
+ <Label htmlFor="status" variant="required">
155
+ Status
156
+ </Label>
157
+ <Select
158
+ id="status"
159
+ name="status"
160
+ options={[
161
+ { value: "DRAFT", label: "Draft" },
162
+ { value: "ACTIVE", label: "Active" },
163
+ { value: "COMPLETED", label: "Completed" },
164
+ ]}
165
+ placeholder="Select status"
166
+ />
167
+ </div>
168
+ <div className="space-y-2">
169
+ <Label htmlFor="priority" variant="optional">
170
+ Priority
171
+ </Label>
172
+ <Select
173
+ id="priority"
174
+ name="priority"
175
+ options={[
176
+ { value: "LOW", label: "Low" },
177
+ { value: "MEDIUM", label: "Medium" },
178
+ { value: "HIGH", label: "High" },
179
+ ]}
180
+ placeholder="Select priority"
181
+ />
182
+ </div>
183
+ <div className="flex gap-2">
184
+ <Button type="submit" variant="regular">
185
+ Submit
186
+ </Button>
187
+ <Button type="button" variant="secondary">
188
+ Cancel
189
+ </Button>
190
+ </div>
191
+ </Form>
192
+ ),
193
+ };
194
+
195
+ export default meta;
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import Form from "./Form";
4
+ import { Input, Button } from "../../atoms";
5
+
6
+ describe("Form", () => {
7
+ it("renders form with children", () => {
8
+ const { container } = render(
9
+ <Form>
10
+ <Input name="test" />
11
+ </Form>
12
+ );
13
+ const form = container.querySelector("form");
14
+ expect(form).toBeInTheDocument();
15
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
16
+ });
17
+
18
+ it("calls onSubmit when form is submitted", () => {
19
+ const handleSubmit = vi.fn((e) => {
20
+ e.preventDefault();
21
+ });
22
+ const { container } = render(
23
+ <Form onSubmit={handleSubmit}>
24
+ <Button type="submit">Submit</Button>
25
+ </Form>
26
+ );
27
+ const form = container.querySelector("form");
28
+ if (form) {
29
+ fireEvent.submit(form);
30
+ }
31
+ expect(handleSubmit).toHaveBeenCalledTimes(1);
32
+ });
33
+
34
+ it("displays error message when error prop is provided", () => {
35
+ render(
36
+ <Form error="Something went wrong">
37
+ <Input name="test" />
38
+ </Form>
39
+ );
40
+ expect(screen.getByText("Something went wrong")).toBeInTheDocument();
41
+ expect(screen.getByText("Something went wrong")).toHaveAttribute("role", "alert");
42
+ });
43
+
44
+ it("displays success message when success prop is provided", () => {
45
+ render(
46
+ <Form success="Form submitted successfully!">
47
+ <Input name="test" />
48
+ </Form>
49
+ );
50
+ expect(screen.getByText("Form submitted successfully!")).toBeInTheDocument();
51
+ expect(screen.getByText("Form submitted successfully!")).toHaveAttribute("role", "alert");
52
+ });
53
+
54
+ it("does not call onSubmit when loading", () => {
55
+ const handleSubmit = vi.fn();
56
+ const { container } = render(
57
+ <Form onSubmit={handleSubmit} loading={true}>
58
+ <Button type="submit">Submit</Button>
59
+ </Form>
60
+ );
61
+ const form = container.querySelector("form");
62
+ if (form) {
63
+ fireEvent.submit(form);
64
+ }
65
+ expect(handleSubmit).not.toHaveBeenCalled();
66
+ });
67
+
68
+ it("applies custom className", () => {
69
+ const { container } = render(
70
+ <Form className="custom-class">
71
+ <Input name="test" />
72
+ </Form>
73
+ );
74
+ const form = container.querySelector("form");
75
+ expect(form).toHaveClass("custom-class");
76
+ });
77
+
78
+ it("has noValidate attribute", () => {
79
+ const { container } = render(
80
+ <Form>
81
+ <Input name="test" />
82
+ </Form>
83
+ );
84
+ const form = container.querySelector("form");
85
+ expect(form).toHaveAttribute("noValidate");
86
+ });
87
+ });
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+
3
+ import type { FormHTMLAttributes, ReactNode } from "react";
4
+
5
+ interface Props extends FormHTMLAttributes<HTMLFormElement> {
6
+ children: ReactNode;
7
+ onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void;
8
+ loading?: boolean;
9
+ error?: string | null;
10
+ success?: string | null;
11
+ }
12
+
13
+ /**
14
+ * Form Component
15
+ *
16
+ * A wrapper component for forms with validation states and layout.
17
+ * Follows Atomic Design principles as a Molecule component.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <Form onSubmit={handleSubmit} loading={isSubmitting}>
22
+ * <Input name="email" />
23
+ * <Button type="submit">Submit</Button>
24
+ * </Form>
25
+ * ```
26
+ */
27
+ export default function Form({
28
+ children,
29
+ onSubmit,
30
+ loading = false,
31
+ error = null,
32
+ success = null,
33
+ className = "",
34
+ ...props
35
+ }: Props) {
36
+ const baseClasses = [
37
+ "space-y-4",
38
+ ];
39
+
40
+ const classes = [
41
+ ...baseClasses,
42
+ className,
43
+ ].filter(Boolean).join(" ");
44
+
45
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
46
+ e.preventDefault();
47
+ if (onSubmit && !loading) {
48
+ onSubmit(e);
49
+ }
50
+ };
51
+
52
+ return (
53
+ <form
54
+ className={classes}
55
+ onSubmit={handleSubmit}
56
+ noValidate
57
+ {...props}
58
+ >
59
+ {children}
60
+ {error && (
61
+ <div
62
+ role="alert"
63
+ className="p-3 text-sm text-red-800 bg-red-50 border border-red-200 rounded"
64
+ >
65
+ {error}
66
+ </div>
67
+ )}
68
+ {success && (
69
+ <div
70
+ role="alert"
71
+ className="p-3 text-sm text-green-800 bg-green-50 border border-green-200 rounded"
72
+ >
73
+ {success}
74
+ </div>
75
+ )}
76
+ </form>
77
+ );
78
+ }
@@ -0,0 +1,116 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import Pagination from "./Pagination";
4
+
5
+ const meta: Meta<typeof Pagination> = {
6
+ title: "UI/Molecules/Pagination",
7
+ component: Pagination,
8
+ parameters: {
9
+ docs: {
10
+ description: {
11
+ component: "A pagination component for navigating through pages of data. Supports page info and ellipsis for large page counts.",
12
+ },
13
+ },
14
+ },
15
+ argTypes: {
16
+ currentPage: {
17
+ control: "number",
18
+ description: "Current page number (1-based)",
19
+ },
20
+ totalPages: {
21
+ control: "number",
22
+ description: "Total number of pages",
23
+ },
24
+ showPageInfo: {
25
+ control: "boolean",
26
+ description: "Whether to show page information",
27
+ },
28
+ },
29
+ };
30
+
31
+ export const Default: StoryObj<typeof Pagination> = {
32
+ render: () => {
33
+ const [page, setPage] = useState(1);
34
+ return (
35
+ <Pagination
36
+ currentPage={page}
37
+ totalPages={10}
38
+ onPageChange={setPage}
39
+ totalItems={100}
40
+ itemsPerPage={10}
41
+ />
42
+ );
43
+ },
44
+ };
45
+
46
+ export const FirstPage: StoryObj<typeof Pagination> = {
47
+ render: () => {
48
+ const [page, setPage] = useState(1);
49
+ return (
50
+ <Pagination
51
+ currentPage={page}
52
+ totalPages={10}
53
+ onPageChange={setPage}
54
+ />
55
+ );
56
+ },
57
+ };
58
+
59
+ export const MiddlePage: StoryObj<typeof Pagination> = {
60
+ render: () => {
61
+ const [page, setPage] = useState(5);
62
+ return (
63
+ <Pagination
64
+ currentPage={page}
65
+ totalPages={10}
66
+ onPageChange={setPage}
67
+ totalItems={100}
68
+ itemsPerPage={10}
69
+ />
70
+ );
71
+ },
72
+ };
73
+
74
+ export const LastPage: StoryObj<typeof Pagination> = {
75
+ render: () => {
76
+ const [page, setPage] = useState(10);
77
+ return (
78
+ <Pagination
79
+ currentPage={page}
80
+ totalPages={10}
81
+ onPageChange={setPage}
82
+ totalItems={100}
83
+ itemsPerPage={10}
84
+ />
85
+ );
86
+ },
87
+ };
88
+
89
+ export const FewPages: StoryObj<typeof Pagination> = {
90
+ render: () => {
91
+ const [page, setPage] = useState(2);
92
+ return (
93
+ <Pagination
94
+ currentPage={page}
95
+ totalPages={3}
96
+ onPageChange={setPage}
97
+ />
98
+ );
99
+ },
100
+ };
101
+
102
+ export const WithoutPageInfo: StoryObj<typeof Pagination> = {
103
+ render: () => {
104
+ const [page, setPage] = useState(1);
105
+ return (
106
+ <Pagination
107
+ currentPage={page}
108
+ totalPages={10}
109
+ onPageChange={setPage}
110
+ showPageInfo={false}
111
+ />
112
+ );
113
+ },
114
+ };
115
+
116
+ export default meta;
@@ -0,0 +1,112 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import Pagination from "./Pagination";
4
+
5
+ describe("Pagination", () => {
6
+ it("renders pagination controls", () => {
7
+ const handlePageChange = vi.fn();
8
+ render(
9
+ <Pagination
10
+ currentPage={1}
11
+ totalPages={5}
12
+ onPageChange={handlePageChange}
13
+ />
14
+ );
15
+ expect(screen.getByText("Previous")).toBeInTheDocument();
16
+ expect(screen.getByText("Next")).toBeInTheDocument();
17
+ });
18
+
19
+ it("disables Previous button on first page", () => {
20
+ const handlePageChange = vi.fn();
21
+ render(
22
+ <Pagination
23
+ currentPage={1}
24
+ totalPages={5}
25
+ onPageChange={handlePageChange}
26
+ />
27
+ );
28
+ const prevButton = screen.getByText("Previous");
29
+ expect(prevButton).toBeDisabled();
30
+ });
31
+
32
+ it("disables Next button on last page", () => {
33
+ const handlePageChange = vi.fn();
34
+ render(
35
+ <Pagination
36
+ currentPage={5}
37
+ totalPages={5}
38
+ onPageChange={handlePageChange}
39
+ />
40
+ );
41
+ const nextButton = screen.getByText("Next");
42
+ expect(nextButton).toBeDisabled();
43
+ });
44
+
45
+ it("calls onPageChange when Next is clicked", () => {
46
+ const handlePageChange = vi.fn();
47
+ render(
48
+ <Pagination
49
+ currentPage={1}
50
+ totalPages={5}
51
+ onPageChange={handlePageChange}
52
+ />
53
+ );
54
+ const nextButton = screen.getByText("Next");
55
+ fireEvent.click(nextButton);
56
+ expect(handlePageChange).toHaveBeenCalledWith(2);
57
+ });
58
+
59
+ it("calls onPageChange when Previous is clicked", () => {
60
+ const handlePageChange = vi.fn();
61
+ render(
62
+ <Pagination
63
+ currentPage={2}
64
+ totalPages={5}
65
+ onPageChange={handlePageChange}
66
+ />
67
+ );
68
+ const prevButton = screen.getByText("Previous");
69
+ fireEvent.click(prevButton);
70
+ expect(handlePageChange).toHaveBeenCalledWith(1);
71
+ });
72
+
73
+ it("calls onPageChange when page number is clicked", () => {
74
+ const handlePageChange = vi.fn();
75
+ render(
76
+ <Pagination
77
+ currentPage={1}
78
+ totalPages={5}
79
+ onPageChange={handlePageChange}
80
+ />
81
+ );
82
+ const page3 = screen.getByLabelText("Go to page 3");
83
+ fireEvent.click(page3);
84
+ expect(handlePageChange).toHaveBeenCalledWith(3);
85
+ });
86
+
87
+ it("shows page info when provided", () => {
88
+ render(
89
+ <Pagination
90
+ currentPage={2}
91
+ totalPages={10}
92
+ onPageChange={() => {}}
93
+ totalItems={100}
94
+ itemsPerPage={10}
95
+ />
96
+ );
97
+ expect(screen.getByText(/Showing 11 to 20 of 100 results/)).toBeInTheDocument();
98
+ });
99
+
100
+ it("highlights current page", () => {
101
+ render(
102
+ <Pagination
103
+ currentPage={3}
104
+ totalPages={5}
105
+ onPageChange={() => {}}
106
+ />
107
+ );
108
+ const currentPageButton = screen.getByLabelText("Go to page 3");
109
+ expect(currentPageButton).toHaveAttribute("aria-current", "page");
110
+ expect(currentPageButton).toHaveClass("bg-indigo-600", "text-white");
111
+ });
112
+ });