@fabio.caffarello/react-design-system 1.2.1 → 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.
- package/dist/index.cjs +4 -4
- package/dist/index.js +696 -246
- package/dist/ui/atoms/ErrorMessage/ErrorMessage.d.ts +18 -0
- package/dist/ui/atoms/ErrorMessage/ErrorMessage.stories.d.ts +7 -0
- package/dist/ui/atoms/ErrorMessage/ErrorMessage.test.d.ts +1 -0
- package/dist/ui/atoms/Label/Label.d.ts +20 -0
- package/dist/ui/atoms/Label/Label.stories.d.ts +8 -0
- package/dist/ui/atoms/Label/Label.test.d.ts +1 -0
- package/dist/ui/atoms/NavLink/NavLink.d.ts +20 -0
- package/dist/ui/atoms/NavLink/NavLink.stories.d.ts +8 -0
- package/dist/ui/atoms/NavLink/NavLink.test.d.ts +1 -0
- package/dist/ui/atoms/index.d.ts +3 -0
- package/dist/ui/molecules/Breadcrumb/Breadcrumb.d.ts +28 -0
- package/dist/ui/molecules/Breadcrumb/Breadcrumb.stories.d.ts +9 -0
- package/dist/ui/molecules/Breadcrumb/Breadcrumb.test.d.ts +1 -0
- package/dist/ui/molecules/Form/Form.d.ts +24 -0
- package/dist/ui/molecules/Form/Form.stories.d.ts +9 -0
- package/dist/ui/molecules/Form/Form.test.d.ts +1 -0
- package/dist/ui/molecules/Pagination/Pagination.d.ts +28 -0
- package/dist/ui/molecules/Pagination/Pagination.stories.d.ts +10 -0
- package/dist/ui/molecules/Pagination/Pagination.test.d.ts +1 -0
- package/dist/ui/molecules/index.d.ts +4 -0
- package/dist/ui/organisms/Modal/Modal.d.ts +25 -0
- package/dist/ui/organisms/Modal/Modal.stories.d.ts +9 -0
- package/dist/ui/organisms/Modal/Modal.test.d.ts +1 -0
- package/dist/ui/organisms/Table/Table.d.ts +35 -0
- package/dist/ui/organisms/Table/Table.stories.d.ts +9 -0
- package/dist/ui/organisms/Table/Table.test.d.ts +1 -0
- package/dist/ui/organisms/index.d.ts +3 -0
- package/package.json +1 -1
- package/src/ui/atoms/ErrorMessage/ErrorMessage.stories.tsx +81 -0
- package/src/ui/atoms/ErrorMessage/ErrorMessage.test.tsx +40 -0
- package/src/ui/atoms/ErrorMessage/ErrorMessage.tsx +62 -0
- package/src/ui/atoms/Label/Label.stories.tsx +94 -0
- package/src/ui/atoms/Label/Label.test.tsx +47 -0
- package/src/ui/atoms/Label/Label.tsx +51 -0
- package/src/ui/atoms/NavLink/NavLink.stories.tsx +71 -0
- package/src/ui/atoms/NavLink/NavLink.test.tsx +44 -0
- package/src/ui/atoms/NavLink/NavLink.tsx +63 -0
- package/src/ui/atoms/index.ts +6 -0
- package/src/ui/molecules/Breadcrumb/Breadcrumb.stories.tsx +75 -0
- package/src/ui/molecules/Breadcrumb/Breadcrumb.test.tsx +89 -0
- package/src/ui/molecules/Breadcrumb/Breadcrumb.tsx +79 -0
- package/src/ui/molecules/Form/Form.stories.tsx +195 -0
- package/src/ui/molecules/Form/Form.test.tsx +87 -0
- package/src/ui/molecules/Form/Form.tsx +76 -0
- package/src/ui/molecules/Pagination/Pagination.stories.tsx +116 -0
- package/src/ui/molecules/Pagination/Pagination.test.tsx +112 -0
- package/src/ui/molecules/Pagination/Pagination.tsx +168 -0
- package/src/ui/molecules/index.ts +7 -0
- package/src/ui/organisms/Modal/Modal.stories.tsx +102 -0
- package/src/ui/organisms/Modal/Modal.test.tsx +111 -0
- package/src/ui/organisms/Modal/Modal.tsx +203 -0
- package/src/ui/organisms/Table/Table.stories.tsx +137 -0
- package/src/ui/organisms/Table/Table.test.tsx +109 -0
- package/src/ui/organisms/Table/Table.tsx +128 -0
- 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
|
+
}
|