@fabio.caffarello/react-design-system 1.2.0 → 1.3.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 (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 +9 -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 +76 -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 +168 -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 +203 -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 +128 -0
  57. package/src/ui/organisms/index.ts +5 -0
@@ -0,0 +1,137 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import Table from "./Table";
4
+ import { Badge } from "../../atoms";
5
+
6
+ interface SampleData {
7
+ id: string;
8
+ name: string;
9
+ status: string;
10
+ priority: string;
11
+ createdAt: string;
12
+ }
13
+
14
+ const sampleData: SampleData[] = [
15
+ { id: '1', name: 'Epic 1', status: 'ACTIVE', priority: 'HIGH', createdAt: '2024-01-01' },
16
+ { id: '2', name: 'Epic 2', status: 'DRAFT', priority: 'MEDIUM', createdAt: '2024-01-02' },
17
+ { id: '3', name: 'Epic 3', status: 'COMPLETED', priority: 'LOW', createdAt: '2024-01-03' },
18
+ ];
19
+
20
+ const meta: Meta<typeof Table> = {
21
+ title: "UI/Organisms/Table",
22
+ component: Table,
23
+ parameters: {
24
+ docs: {
25
+ description: {
26
+ component: "A table component with sorting, loading states, and responsive design. Supports custom cell rendering.",
27
+ },
28
+ },
29
+ },
30
+ argTypes: {
31
+ loading: {
32
+ control: "boolean",
33
+ description: "Whether the table is in a loading state",
34
+ },
35
+ },
36
+ };
37
+
38
+ export const Default: StoryObj<typeof Table> = {
39
+ args: {
40
+ columns: [
41
+ { key: 'name', label: 'Name' },
42
+ { key: 'status', label: 'Status' },
43
+ { key: 'priority', label: 'Priority' },
44
+ { key: 'createdAt', label: 'Created At' },
45
+ ],
46
+ data: sampleData,
47
+ },
48
+ };
49
+
50
+ export const WithSorting: StoryObj<typeof Table> = {
51
+ render: () => {
52
+ const [sortColumn, setSortColumn] = useState<string>('');
53
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
54
+ const [data, setData] = useState(sampleData);
55
+
56
+ const handleSort = (columnKey: string, direction: 'asc' | 'desc') => {
57
+ setSortColumn(columnKey);
58
+ setSortDirection(direction);
59
+
60
+ const sorted = [...data].sort((a, b) => {
61
+ const aVal = (a as any)[columnKey];
62
+ const bVal = (b as any)[columnKey];
63
+ const comparison = aVal.localeCompare(bVal);
64
+ return direction === 'asc' ? comparison : -comparison;
65
+ });
66
+
67
+ setData(sorted);
68
+ };
69
+
70
+ return (
71
+ <Table
72
+ columns={[
73
+ { key: 'name', label: 'Name', sortable: true },
74
+ { key: 'status', label: 'Status', sortable: true },
75
+ { key: 'priority', label: 'Priority', sortable: true },
76
+ { key: 'createdAt', label: 'Created At', sortable: true },
77
+ ]}
78
+ data={data}
79
+ onSort={handleSort}
80
+ sortColumn={sortColumn}
81
+ sortDirection={sortDirection}
82
+ />
83
+ );
84
+ },
85
+ };
86
+
87
+ export const WithCustomRendering: StoryObj<typeof Table> = {
88
+ args: {
89
+ columns: [
90
+ { key: 'name', label: 'Name' },
91
+ {
92
+ key: 'status',
93
+ label: 'Status',
94
+ render: (value) => (
95
+ <Badge variant={value === 'ACTIVE' ? 'success' : value === 'COMPLETED' ? 'info' : 'neutral'}>
96
+ {value}
97
+ </Badge>
98
+ ),
99
+ },
100
+ {
101
+ key: 'priority',
102
+ label: 'Priority',
103
+ render: (value) => (
104
+ <Badge variant={value === 'HIGH' ? 'error' : value === 'MEDIUM' ? 'warning' : 'info'}>
105
+ {value}
106
+ </Badge>
107
+ ),
108
+ },
109
+ { key: 'createdAt', label: 'Created At' },
110
+ ],
111
+ data: sampleData,
112
+ },
113
+ };
114
+
115
+ export const Loading: StoryObj<typeof Table> = {
116
+ args: {
117
+ columns: [
118
+ { key: 'name', label: 'Name' },
119
+ { key: 'status', label: 'Status' },
120
+ ],
121
+ data: [],
122
+ loading: true,
123
+ },
124
+ };
125
+
126
+ export const Empty: StoryObj<typeof Table> = {
127
+ args: {
128
+ columns: [
129
+ { key: 'name', label: 'Name' },
130
+ { key: 'status', label: 'Status' },
131
+ ],
132
+ data: [],
133
+ emptyMessage: "No epics found. Create your first epic to get started.",
134
+ },
135
+ };
136
+
137
+ export default meta;
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import Table from "./Table";
4
+
5
+ const sampleData = [
6
+ { id: '1', name: 'Item 1', status: 'ACTIVE' },
7
+ { id: '2', name: 'Item 2', status: 'INACTIVE' },
8
+ ];
9
+
10
+ describe("Table", () => {
11
+ it("renders table with data", () => {
12
+ render(
13
+ <Table
14
+ columns={[
15
+ { key: 'name', label: 'Name' },
16
+ { key: 'status', label: 'Status' },
17
+ ]}
18
+ data={sampleData}
19
+ />
20
+ );
21
+ expect(screen.getByText("Item 1")).toBeInTheDocument();
22
+ expect(screen.getByText("Item 2")).toBeInTheDocument();
23
+ });
24
+
25
+ it("renders column headers", () => {
26
+ render(
27
+ <Table
28
+ columns={[
29
+ { key: 'name', label: 'Name' },
30
+ { key: 'status', label: 'Status' },
31
+ ]}
32
+ data={sampleData}
33
+ />
34
+ );
35
+ expect(screen.getByText("Name")).toBeInTheDocument();
36
+ expect(screen.getByText("Status")).toBeInTheDocument();
37
+ });
38
+
39
+ it("shows loading state", () => {
40
+ render(
41
+ <Table
42
+ columns={[{ key: 'name', label: 'Name' }]}
43
+ data={[]}
44
+ loading={true}
45
+ />
46
+ );
47
+ expect(screen.getByText("Loading...")).toBeInTheDocument();
48
+ });
49
+
50
+ it("shows empty message when no data", () => {
51
+ render(
52
+ <Table
53
+ columns={[{ key: 'name', label: 'Name' }]}
54
+ data={[]}
55
+ emptyMessage="No items found"
56
+ />
57
+ );
58
+ expect(screen.getByText("No items found")).toBeInTheDocument();
59
+ });
60
+
61
+ it("calls onSort when sortable column header is clicked", () => {
62
+ const handleSort = vi.fn();
63
+ render(
64
+ <Table
65
+ columns={[
66
+ { key: 'name', label: 'Name', sortable: true },
67
+ { key: 'status', label: 'Status' },
68
+ ]}
69
+ data={sampleData}
70
+ onSort={handleSort}
71
+ />
72
+ );
73
+ const nameHeader = screen.getByText("Name");
74
+ fireEvent.click(nameHeader);
75
+ expect(handleSort).toHaveBeenCalledWith('name', 'asc');
76
+ });
77
+
78
+ it("uses custom render function", () => {
79
+ render(
80
+ <Table
81
+ columns={[
82
+ {
83
+ key: 'name',
84
+ label: 'Name',
85
+ render: (value) => <strong>{value}</strong>,
86
+ },
87
+ ]}
88
+ data={sampleData}
89
+ />
90
+ );
91
+ const strong = screen.getByText("Item 1");
92
+ expect(strong.tagName).toBe("STRONG");
93
+ });
94
+
95
+ it("shows sort indicator when sorted", () => {
96
+ render(
97
+ <Table
98
+ columns={[
99
+ { key: 'name', label: 'Name', sortable: true },
100
+ ]}
101
+ data={sampleData}
102
+ onSort={() => {}}
103
+ sortColumn="name"
104
+ sortDirection="asc"
105
+ />
106
+ );
107
+ expect(screen.getByText("↑")).toBeInTheDocument();
108
+ });
109
+ });
@@ -0,0 +1,128 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+
3
+ export interface TableColumn<T = any> {
4
+ key: string;
5
+ label: string;
6
+ render?: (value: any, row: T) => ReactNode;
7
+ sortable?: boolean;
8
+ }
9
+
10
+ interface Props<T = any> extends HTMLAttributes<HTMLTableElement> {
11
+ columns: TableColumn<T>[];
12
+ data: T[];
13
+ loading?: boolean;
14
+ onSort?: (columnKey: string, direction: 'asc' | 'desc') => void;
15
+ sortColumn?: string;
16
+ sortDirection?: 'asc' | 'desc';
17
+ emptyMessage?: string;
18
+ }
19
+
20
+ /**
21
+ * Table Component
22
+ *
23
+ * A table component with sorting, loading states, and responsive design.
24
+ * Follows Atomic Design principles as an Organism component.
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <Table
29
+ * columns={[
30
+ * { key: 'name', label: 'Name', sortable: true },
31
+ * { key: 'status', label: 'Status' }
32
+ * ]}
33
+ * data={items}
34
+ * />
35
+ * ```
36
+ */
37
+ export default function Table<T = any>({
38
+ columns,
39
+ data,
40
+ loading = false,
41
+ onSort,
42
+ sortColumn,
43
+ sortDirection,
44
+ emptyMessage = "No data available",
45
+ className = "",
46
+ ...props
47
+ }: Props<T>) {
48
+ const handleSort = (columnKey: string) => {
49
+ if (!onSort || !columns.find(col => col.key === columnKey)?.sortable) {
50
+ return;
51
+ }
52
+
53
+ const newDirection =
54
+ sortColumn === columnKey && sortDirection === 'asc' ? 'desc' : 'asc';
55
+ onSort(columnKey, newDirection);
56
+ };
57
+
58
+ const baseClasses = [
59
+ "min-w-full",
60
+ "divide-y",
61
+ "divide-gray-200",
62
+ ];
63
+
64
+ const classes = [
65
+ ...baseClasses,
66
+ className,
67
+ ].filter(Boolean).join(" ");
68
+
69
+ return (
70
+ <div className="overflow-x-auto">
71
+ <table className={classes} {...props}>
72
+ <thead className="bg-gray-50">
73
+ <tr>
74
+ {columns.map((column) => (
75
+ <th
76
+ key={column.key}
77
+ scope="col"
78
+ className={`px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider ${
79
+ column.sortable && onSort
80
+ ? 'cursor-pointer hover:bg-gray-100 select-none'
81
+ : ''
82
+ }`}
83
+ onClick={() => column.sortable && handleSort(column.key)}
84
+ >
85
+ <div className="flex items-center gap-2">
86
+ <span>{column.label}</span>
87
+ {column.sortable && sortColumn === column.key && (
88
+ <span className="text-gray-400">
89
+ {sortDirection === 'asc' ? '↑' : '↓'}
90
+ </span>
91
+ )}
92
+ </div>
93
+ </th>
94
+ ))}
95
+ </tr>
96
+ </thead>
97
+ <tbody className="bg-white divide-y divide-gray-200">
98
+ {loading ? (
99
+ <tr>
100
+ <td colSpan={columns.length} className="px-6 py-4 text-center text-gray-500">
101
+ Loading...
102
+ </td>
103
+ </tr>
104
+ ) : data.length === 0 ? (
105
+ <tr>
106
+ <td colSpan={columns.length} className="px-6 py-4 text-center text-gray-500">
107
+ {emptyMessage}
108
+ </td>
109
+ </tr>
110
+ ) : (
111
+ data.map((row, rowIndex) => (
112
+ <tr key={rowIndex} className="hover:bg-gray-50">
113
+ {columns.map((column) => {
114
+ const value = (row as any)[column.key];
115
+ return (
116
+ <td key={column.key} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
117
+ {column.render ? column.render(value, row) : value}
118
+ </td>
119
+ );
120
+ })}
121
+ </tr>
122
+ ))
123
+ )}
124
+ </tbody>
125
+ </table>
126
+ </div>
127
+ );
128
+ }
@@ -1 +1,6 @@
1
1
  export { default as LoginBox } from "./LoginBox/LoginBox";
2
+
3
+ export { default as Modal } from "./Modal/Modal";
4
+
5
+ export { default as Table } from "./Table/Table";
6
+ export type { TableColumn } from "./Table/Table";