@lamenna/lxp-react 0.0.1 → 0.1.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/components/Button/Button.js +2 -2
- package/dist/components/Button/__tests__/button.test.d.ts +1 -1
- package/dist/components/Button/__tests__/button.test.js +46 -43
- package/dist/components/Table/Table.d.ts +10 -0
- package/dist/components/Table/Table.js +253 -0
- package/dist/components/Table/__tests__/table.test.d.ts +1 -0
- package/dist/components/Table/__tests__/table.test.js +44 -0
- package/dist/components/ThemeProvider/ThemeProvider.d.ts +24 -0
- package/dist/components/ThemeProvider/ThemeProvider.js +22 -0
- package/dist/components/ThemeProvider/__tests__/theme-provider.test.d.ts +1 -0
- package/dist/components/ThemeProvider/__tests__/theme-provider.test.js +20 -0
- package/dist/components/ThemeSwitcher/ThemeSwitcher.d.ts +6 -0
- package/dist/components/ThemeSwitcher/ThemeSwitcher.js +18 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/package.json +8 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { buttonConfig
|
|
2
|
+
import { buttonConfig } from "@lamenna/lxp-shared-components";
|
|
3
3
|
export const Button = ({ children, variant = "primary", size = "md", disabled = false, className, id, ariaLabel, testID, onClick, }) => {
|
|
4
4
|
const variantConfig = buttonConfig.variants[variant];
|
|
5
5
|
const sizeConfig = buttonConfig.sizes[size];
|
|
@@ -10,5 +10,5 @@ export const Button = ({ children, variant = "primary", size = "md", disabled =
|
|
|
10
10
|
opacity: disabled ? 0.6 : 1,
|
|
11
11
|
cursor: disabled ? "not-allowed" : "pointer",
|
|
12
12
|
};
|
|
13
|
-
return (React.createElement("button", { style: styles, onClick: onClick, disabled: disabled, className: className, id: id, "aria-label": ariaLabel, "data-testid": testID }, children));
|
|
13
|
+
return (React.createElement("button", { style: styles, type: "button", onClick: onClick, disabled: disabled, className: className, id: id, "aria-label": ariaLabel, "data-testid": testID }, children));
|
|
14
14
|
};
|
|
@@ -4,91 +4,94 @@
|
|
|
4
4
|
* This test suite validates the React implementation of the Button component,
|
|
5
5
|
* ensuring it properly renders and responds to user interactions.
|
|
6
6
|
*/
|
|
7
|
-
import
|
|
8
|
-
import React from
|
|
9
|
-
import { render, screen, fireEvent } from
|
|
10
|
-
import { Button } from
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
import "@testing-library/jest-dom";
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
10
|
+
import { Button } from "../Button";
|
|
11
|
+
import { buttonConfig } from "@lamenna/lxp-shared-components";
|
|
12
|
+
describe("Button Component (React)", () => {
|
|
13
|
+
describe("Rendering", () => {
|
|
14
|
+
it("should render a button element", () => {
|
|
14
15
|
render(React.createElement(Button, null, "Click me"));
|
|
15
|
-
const button = screen.getByRole(
|
|
16
|
+
const button = screen.getByRole("button");
|
|
16
17
|
expect(button).toBeInTheDocument();
|
|
17
18
|
});
|
|
18
|
-
it(
|
|
19
|
+
it("should display button text", () => {
|
|
19
20
|
render(React.createElement(Button, null, "Test Button"));
|
|
20
|
-
expect(screen.getByText(
|
|
21
|
+
expect(screen.getByText("Test Button")).toBeInTheDocument();
|
|
21
22
|
});
|
|
22
|
-
it(
|
|
23
|
+
it("should apply primary variant styles by default", () => {
|
|
23
24
|
render(React.createElement(Button, null, "Primary"));
|
|
24
|
-
const button = screen.getByRole(
|
|
25
|
-
expect(button).
|
|
25
|
+
const button = screen.getByRole("button");
|
|
26
|
+
expect(button).toHaveStyle(`background-color: ${buttonConfig.variants.primary.backgroundColor}`);
|
|
27
|
+
expect(button).toHaveStyle(`color: ${buttonConfig.variants.primary.color}`);
|
|
26
28
|
});
|
|
27
|
-
it(
|
|
29
|
+
it("should apply secondary variant styles when specified", () => {
|
|
28
30
|
render(React.createElement(Button, { variant: "secondary" }, "Secondary"));
|
|
29
|
-
const button = screen.getByRole(
|
|
30
|
-
expect(button).
|
|
31
|
+
const button = screen.getByRole("button");
|
|
32
|
+
expect(button).toHaveStyle(`background-color: ${buttonConfig.variants.secondary.backgroundColor}`);
|
|
33
|
+
expect(button).toHaveStyle(`color: ${buttonConfig.variants.secondary.color}`);
|
|
31
34
|
});
|
|
32
|
-
it(
|
|
35
|
+
it("should apply size styles correctly", () => {
|
|
33
36
|
const { rerender } = render(React.createElement(Button, { size: "sm" }, "Small"));
|
|
34
|
-
expect(screen.getByRole(
|
|
37
|
+
expect(screen.getByRole("button")).toHaveStyle(`height: ${buttonConfig.sizes.sm.height}`);
|
|
35
38
|
rerender(React.createElement(Button, { size: "md" }, "Medium"));
|
|
36
|
-
expect(screen.getByRole(
|
|
39
|
+
expect(screen.getByRole("button")).toHaveStyle(`height: ${buttonConfig.sizes.md.height}`);
|
|
37
40
|
rerender(React.createElement(Button, { size: "lg" }, "Large"));
|
|
38
|
-
expect(screen.getByRole(
|
|
41
|
+
expect(screen.getByRole("button")).toHaveStyle(`height: ${buttonConfig.sizes.lg.height}`);
|
|
39
42
|
});
|
|
40
43
|
});
|
|
41
|
-
describe(
|
|
42
|
-
it(
|
|
44
|
+
describe("Interactions", () => {
|
|
45
|
+
it("should handle click events", () => {
|
|
43
46
|
const onClick = jest.fn();
|
|
44
47
|
render(React.createElement(Button, { onClick: onClick }, "Click me"));
|
|
45
|
-
const button = screen.getByRole(
|
|
48
|
+
const button = screen.getByRole("button");
|
|
46
49
|
fireEvent.click(button);
|
|
47
50
|
expect(onClick).toHaveBeenCalledTimes(1);
|
|
48
51
|
});
|
|
49
|
-
it(
|
|
52
|
+
it("should not trigger onClick when disabled", () => {
|
|
50
53
|
const onClick = jest.fn();
|
|
51
54
|
render(React.createElement(Button, { onClick: onClick, disabled: true }, "Disabled"));
|
|
52
|
-
const button = screen.getByRole(
|
|
55
|
+
const button = screen.getByRole("button");
|
|
53
56
|
fireEvent.click(button);
|
|
54
57
|
expect(onClick).not.toHaveBeenCalled();
|
|
55
58
|
});
|
|
56
|
-
it(
|
|
59
|
+
it("should have disabled attribute when disabled", () => {
|
|
57
60
|
render(React.createElement(Button, { disabled: true }, "Disabled"));
|
|
58
|
-
const button = screen.getByRole(
|
|
61
|
+
const button = screen.getByRole("button");
|
|
59
62
|
expect(button).toBeDisabled();
|
|
60
63
|
});
|
|
61
64
|
});
|
|
62
|
-
describe(
|
|
63
|
-
it(
|
|
65
|
+
describe("Accessibility", () => {
|
|
66
|
+
it("should support aria-label", () => {
|
|
64
67
|
render(React.createElement(Button, { ariaLabel: "Close dialog" }, "\u00D7"));
|
|
65
|
-
const button = screen.getByLabelText(
|
|
68
|
+
const button = screen.getByLabelText("Close dialog");
|
|
66
69
|
expect(button).toBeInTheDocument();
|
|
67
70
|
});
|
|
68
|
-
it(
|
|
71
|
+
it("should be keyboard accessible", () => {
|
|
69
72
|
const onClick = jest.fn();
|
|
70
73
|
render(React.createElement(Button, { onClick: onClick }, "Press me"));
|
|
71
|
-
const button = screen.getByRole(
|
|
72
|
-
fireEvent.keyDown(button, { key:
|
|
74
|
+
const button = screen.getByRole("button");
|
|
75
|
+
fireEvent.keyDown(button, { key: "Enter", code: "Enter" });
|
|
73
76
|
// Button should handle Enter key (default behavior)
|
|
74
77
|
expect(button).toBeInTheDocument();
|
|
75
78
|
});
|
|
76
|
-
it(
|
|
79
|
+
it("should have proper role attribute", () => {
|
|
77
80
|
render(React.createElement(Button, null, "Action"));
|
|
78
|
-
const button = screen.getByRole(
|
|
79
|
-
expect(button).toHaveAttribute(
|
|
81
|
+
const button = screen.getByRole("button");
|
|
82
|
+
expect(button).toHaveAttribute("type", "button");
|
|
80
83
|
});
|
|
81
84
|
});
|
|
82
|
-
describe(
|
|
83
|
-
it(
|
|
85
|
+
describe("CSS Classes", () => {
|
|
86
|
+
it("should apply custom className", () => {
|
|
84
87
|
render(React.createElement(Button, { className: "custom-class" }, "Custom"));
|
|
85
|
-
const button = screen.getByRole(
|
|
86
|
-
expect(button).toHaveClass(
|
|
88
|
+
const button = screen.getByRole("button");
|
|
89
|
+
expect(button).toHaveClass("custom-class");
|
|
87
90
|
});
|
|
88
|
-
it(
|
|
91
|
+
it("should apply id when provided", () => {
|
|
89
92
|
render(React.createElement(Button, { id: "test-button" }, "ID Button"));
|
|
90
|
-
const button = screen.getByRole(
|
|
91
|
-
expect(button).toHaveAttribute(
|
|
93
|
+
const button = screen.getByRole("button");
|
|
94
|
+
expect(button).toHaveAttribute("id", "test-button");
|
|
92
95
|
});
|
|
93
96
|
});
|
|
94
97
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type BaseTableProps, type TableAction } from "@lamenna/lxp-shared-components";
|
|
3
|
+
export interface TableProps extends BaseTableProps {
|
|
4
|
+
/**
|
|
5
|
+
* Optional custom renderer for the actions column. When omitted, a
|
|
6
|
+
* simple default button layout is used.
|
|
7
|
+
*/
|
|
8
|
+
renderActions?: (row: any, actions: TableAction[]) => React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export declare const Table: React.FC<TableProps>;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import { tableConfig, tableThemeConfig, } from "@lamenna/lxp-shared-components";
|
|
3
|
+
const getCellContent = (column, row) => {
|
|
4
|
+
const rawValue = row[column.key];
|
|
5
|
+
if (column.render) {
|
|
6
|
+
return column.render(rawValue, row);
|
|
7
|
+
}
|
|
8
|
+
if (rawValue && typeof rawValue === "object") {
|
|
9
|
+
const { line1, line2, text, name } = rawValue;
|
|
10
|
+
const primary = line1 ?? text ?? name;
|
|
11
|
+
const secondary = line2;
|
|
12
|
+
if (primary || secondary) {
|
|
13
|
+
return (React.createElement("div", null,
|
|
14
|
+
primary && React.createElement("div", null, primary),
|
|
15
|
+
secondary && (React.createElement("div", { style: { fontSize: "0.75rem", color: "#6B7280" } }, secondary))));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return rawValue ?? "";
|
|
19
|
+
};
|
|
20
|
+
export const Table = ({ columns, data, actions = [], renderActions, selectable = false, showPagination = true, showSearch = true, loading = false, theme = "tera", title = tableConfig.defaultTitle, serverSide = false, totalCount, currentPage, pageSize, pageSizeOptions, emptyState, onSelectionChange, onPageChange, onPageSizeChange, onSearch, enableRowClick = false, onRowClick, }) => {
|
|
21
|
+
const themeStyles = tableThemeConfig[theme];
|
|
22
|
+
const [localPage, setLocalPage] = useState(currentPage ?? 1);
|
|
23
|
+
const [localPageSize, setLocalPageSize] = useState(pageSize ?? tableConfig.defaultPageSize);
|
|
24
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
25
|
+
const [selectedRows, setSelectedRows] = useState([]);
|
|
26
|
+
const effectivePage = currentPage ?? localPage;
|
|
27
|
+
const effectivePageSize = pageSize ?? localPageSize;
|
|
28
|
+
const effectivePageSizeOptions = pageSizeOptions ?? tableConfig.defaultPageSizeOptions;
|
|
29
|
+
const filteredData = useMemo(() => {
|
|
30
|
+
if (!showSearch || !searchTerm.trim())
|
|
31
|
+
return data;
|
|
32
|
+
const term = searchTerm.toLowerCase();
|
|
33
|
+
return data.filter((row) => Object.values(row).some((value) => String(typeof value === "object" && value !== null
|
|
34
|
+
? value.line1 ?? value.line2 ?? ""
|
|
35
|
+
: value ?? "")
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.includes(term)));
|
|
38
|
+
}, [data, searchTerm, showSearch]);
|
|
39
|
+
const pagedData = useMemo(() => {
|
|
40
|
+
if (!showPagination || serverSide)
|
|
41
|
+
return filteredData;
|
|
42
|
+
const start = (effectivePage - 1) * effectivePageSize;
|
|
43
|
+
return filteredData.slice(start, start + effectivePageSize);
|
|
44
|
+
}, [
|
|
45
|
+
filteredData,
|
|
46
|
+
showPagination,
|
|
47
|
+
serverSide,
|
|
48
|
+
effectivePage,
|
|
49
|
+
effectivePageSize,
|
|
50
|
+
]);
|
|
51
|
+
const totalItems = serverSide
|
|
52
|
+
? totalCount ?? filteredData.length
|
|
53
|
+
: filteredData.length;
|
|
54
|
+
const totalPages = showPagination
|
|
55
|
+
? Math.max(1, Math.ceil(totalItems / effectivePageSize))
|
|
56
|
+
: 1;
|
|
57
|
+
const handleToggleRow = (index) => {
|
|
58
|
+
const nextSelection = selectedRows.includes(index)
|
|
59
|
+
? selectedRows.filter((i) => i !== index)
|
|
60
|
+
: [...selectedRows, index];
|
|
61
|
+
setSelectedRows(nextSelection);
|
|
62
|
+
onSelectionChange?.(nextSelection);
|
|
63
|
+
};
|
|
64
|
+
const handleSearchChange = (value) => {
|
|
65
|
+
setSearchTerm(value);
|
|
66
|
+
if (onSearch) {
|
|
67
|
+
onSearch({
|
|
68
|
+
page: effectivePage,
|
|
69
|
+
pageSize: effectivePageSize,
|
|
70
|
+
searchTerm: value,
|
|
71
|
+
filters: {},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const handlePageChangeInternal = (nextPage) => {
|
|
76
|
+
if (nextPage < 1 || nextPage > totalPages)
|
|
77
|
+
return;
|
|
78
|
+
if (currentPage == null) {
|
|
79
|
+
setLocalPage(nextPage);
|
|
80
|
+
}
|
|
81
|
+
onPageChange?.({
|
|
82
|
+
page: nextPage,
|
|
83
|
+
pageSize: effectivePageSize,
|
|
84
|
+
searchTerm,
|
|
85
|
+
filters: {},
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
const handlePageSizeChangeInternal = (nextSize) => {
|
|
89
|
+
if (pageSize == null) {
|
|
90
|
+
setLocalPageSize(nextSize);
|
|
91
|
+
setLocalPage(1);
|
|
92
|
+
}
|
|
93
|
+
onPageSizeChange?.({
|
|
94
|
+
page: 1,
|
|
95
|
+
pageSize: nextSize,
|
|
96
|
+
searchTerm,
|
|
97
|
+
filters: {},
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
const defaultRenderActions = (row, rowActions) => {
|
|
101
|
+
if (!rowActions || rowActions.length === 0)
|
|
102
|
+
return null;
|
|
103
|
+
return (React.createElement("div", { style: { display: "flex", gap: "0.5rem" } }, rowActions.map((action, index) => {
|
|
104
|
+
const isDisabled = action.disabled ? action.disabled(row) : false;
|
|
105
|
+
let background = "#E5E7EB";
|
|
106
|
+
let color = "#374151";
|
|
107
|
+
if (action.variant === "primary") {
|
|
108
|
+
background = "#DBEAFE";
|
|
109
|
+
color = "#1D4ED8";
|
|
110
|
+
}
|
|
111
|
+
else if (action.variant === "secondary") {
|
|
112
|
+
background = "#EDE9FE";
|
|
113
|
+
color = "#5B21B6";
|
|
114
|
+
}
|
|
115
|
+
else if (action.variant === "danger") {
|
|
116
|
+
background = "#FEE2E2";
|
|
117
|
+
color = "#B91C1C";
|
|
118
|
+
}
|
|
119
|
+
return (React.createElement("button", { key: `action-${index}`, type: "button", onClick: () => !isDisabled && action.action(row), disabled: isDisabled, style: {
|
|
120
|
+
padding: "4px 12px",
|
|
121
|
+
borderRadius: "9999px",
|
|
122
|
+
border: "none",
|
|
123
|
+
background,
|
|
124
|
+
color,
|
|
125
|
+
fontSize: "0.75rem",
|
|
126
|
+
cursor: isDisabled ? "not-allowed" : "pointer",
|
|
127
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
128
|
+
} }, action.label));
|
|
129
|
+
})));
|
|
130
|
+
};
|
|
131
|
+
const renderBody = () => {
|
|
132
|
+
if (loading) {
|
|
133
|
+
return (React.createElement("tr", null,
|
|
134
|
+
React.createElement("td", { colSpan: columns.length + (selectable ? 1 : 0) + (actions.length ? 1 : 0), style: { padding: "1.5rem", textAlign: "center" } }, tableConfig.loadingMessage)));
|
|
135
|
+
}
|
|
136
|
+
if (!pagedData.length) {
|
|
137
|
+
const message = emptyState?.message ??
|
|
138
|
+
(emptyState?.showDefault ?? true
|
|
139
|
+
? tableConfig.emptyMessage
|
|
140
|
+
: undefined);
|
|
141
|
+
return (React.createElement("tr", null,
|
|
142
|
+
React.createElement("td", { colSpan: columns.length + (selectable ? 1 : 0) + (actions.length ? 1 : 0), style: {
|
|
143
|
+
padding: "1.5rem",
|
|
144
|
+
textAlign: "center",
|
|
145
|
+
color: themeStyles.emptyStateTextColor,
|
|
146
|
+
} }, message)));
|
|
147
|
+
}
|
|
148
|
+
return pagedData.map((row, rowIndex) => {
|
|
149
|
+
const globalIndex = serverSide
|
|
150
|
+
? rowIndex
|
|
151
|
+
: (effectivePage - 1) * effectivePageSize + rowIndex;
|
|
152
|
+
const isSelected = selectedRows.includes(globalIndex);
|
|
153
|
+
return (React.createElement("tr", { key: globalIndex, onClick: enableRowClick
|
|
154
|
+
? () => {
|
|
155
|
+
onRowClick?.(row, globalIndex);
|
|
156
|
+
}
|
|
157
|
+
: undefined, style: {
|
|
158
|
+
backgroundColor: "#FFFFFF",
|
|
159
|
+
borderBottom: `1px solid ${themeStyles.borderColor}`,
|
|
160
|
+
cursor: enableRowClick ? "pointer" : "default",
|
|
161
|
+
minHeight: tableConfig.rowMinHeight,
|
|
162
|
+
} },
|
|
163
|
+
selectable && (React.createElement("td", { style: {
|
|
164
|
+
width: tableConfig.selectionColumnWidth,
|
|
165
|
+
padding: "0.75rem",
|
|
166
|
+
} },
|
|
167
|
+
React.createElement("input", { type: "checkbox", checked: isSelected, onChange: () => handleToggleRow(globalIndex) }))),
|
|
168
|
+
columns.map((column) => (React.createElement("td", { key: column.key, style: {
|
|
169
|
+
padding: "0.75rem 1rem",
|
|
170
|
+
fontSize: "0.875rem",
|
|
171
|
+
textAlign: column.align ?? "left",
|
|
172
|
+
} }, getCellContent(column, row)))),
|
|
173
|
+
actions.length > 0 && (React.createElement("td", { style: { padding: "0.75rem 1rem", textAlign: "right" } }, (renderActions ?? defaultRenderActions)(row, actions)))));
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
return (React.createElement("div", { style: {
|
|
177
|
+
border: `1px solid ${themeStyles.borderColor}`,
|
|
178
|
+
borderRadius: "0.5rem",
|
|
179
|
+
backgroundColor: "#FFFFFF",
|
|
180
|
+
fontFamily: themeStyles.fontFamily,
|
|
181
|
+
} },
|
|
182
|
+
React.createElement("div", { style: {
|
|
183
|
+
display: "flex",
|
|
184
|
+
justifyContent: "space-between",
|
|
185
|
+
alignItems: "center",
|
|
186
|
+
padding: "1rem 1.25rem",
|
|
187
|
+
borderBottom: `1px solid ${themeStyles.borderColor}`,
|
|
188
|
+
} },
|
|
189
|
+
React.createElement("div", null,
|
|
190
|
+
React.createElement("h3", { style: {
|
|
191
|
+
margin: 0,
|
|
192
|
+
fontSize: "1rem",
|
|
193
|
+
fontWeight: tableConfig.headerFontWeight,
|
|
194
|
+
} }, title),
|
|
195
|
+
totalItems > 0 && (React.createElement("span", { style: { fontSize: "0.875rem", color: "#6B7280" } },
|
|
196
|
+
"(",
|
|
197
|
+
totalItems,
|
|
198
|
+
")"))),
|
|
199
|
+
showSearch && (React.createElement("input", { type: "search", placeholder: "Search...", value: searchTerm, onChange: (e) => handleSearchChange(e.target.value), style: {
|
|
200
|
+
padding: "0.5rem 0.75rem",
|
|
201
|
+
borderRadius: "0.375rem",
|
|
202
|
+
border: "1px solid #D1D5DB",
|
|
203
|
+
fontSize: "0.875rem",
|
|
204
|
+
minWidth: "200px",
|
|
205
|
+
} }))),
|
|
206
|
+
React.createElement("div", { style: { overflowX: "auto" } },
|
|
207
|
+
React.createElement("table", { style: { width: "100%", borderCollapse: "collapse" } },
|
|
208
|
+
React.createElement("thead", null,
|
|
209
|
+
React.createElement("tr", { style: {
|
|
210
|
+
backgroundColor: themeStyles.headerBackground,
|
|
211
|
+
borderBottom: `1px solid ${themeStyles.borderColor}`,
|
|
212
|
+
} },
|
|
213
|
+
selectable && (React.createElement("th", { style: {
|
|
214
|
+
width: tableConfig.selectionColumnWidth,
|
|
215
|
+
padding: "0.75rem",
|
|
216
|
+
} })),
|
|
217
|
+
columns.map((column) => (React.createElement("th", { key: column.key, style: {
|
|
218
|
+
padding: "0.75rem 1rem",
|
|
219
|
+
fontSize: "0.75rem",
|
|
220
|
+
textAlign: column.align ?? "left",
|
|
221
|
+
textTransform: "uppercase",
|
|
222
|
+
letterSpacing: "0.05em",
|
|
223
|
+
color: themeStyles.headerTextColor,
|
|
224
|
+
} }, column.label))),
|
|
225
|
+
actions.length > 0 && (React.createElement("th", { style: {
|
|
226
|
+
padding: "0.75rem 1rem",
|
|
227
|
+
textAlign: "right",
|
|
228
|
+
} }, "Actions")))),
|
|
229
|
+
React.createElement("tbody", null, renderBody()))),
|
|
230
|
+
showPagination && totalPages > 1 && (React.createElement("div", { style: {
|
|
231
|
+
display: "flex",
|
|
232
|
+
justifyContent: "space-between",
|
|
233
|
+
alignItems: "center",
|
|
234
|
+
padding: "0.75rem 1.25rem",
|
|
235
|
+
borderTop: `1px solid ${themeStyles.borderColor}`,
|
|
236
|
+
fontSize: "0.875rem",
|
|
237
|
+
} },
|
|
238
|
+
React.createElement("div", null,
|
|
239
|
+
"Page ",
|
|
240
|
+
effectivePage,
|
|
241
|
+
" of ",
|
|
242
|
+
totalPages),
|
|
243
|
+
React.createElement("div", { style: {
|
|
244
|
+
display: "flex",
|
|
245
|
+
alignItems: "center",
|
|
246
|
+
gap: "0.75rem",
|
|
247
|
+
} },
|
|
248
|
+
React.createElement("label", null,
|
|
249
|
+
React.createElement("span", { style: { marginRight: "0.25rem" } }, "Rows per page"),
|
|
250
|
+
React.createElement("select", { value: effectivePageSize, onChange: (e) => handlePageSizeChangeInternal(Number(e.target.value)) }, effectivePageSizeOptions.map((size) => (React.createElement("option", { key: size, value: size }, size))))),
|
|
251
|
+
React.createElement("button", { type: "button", onClick: () => handlePageChangeInternal(effectivePage - 1), disabled: effectivePage === 1 }, "Previous"),
|
|
252
|
+
React.createElement("button", { type: "button", onClick: () => handlePageChangeInternal(effectivePage + 1), disabled: effectivePage === totalPages }, "Next"))))));
|
|
253
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
4
|
+
import { Table } from "../Table";
|
|
5
|
+
describe("Table Component (React)", () => {
|
|
6
|
+
const columns = [
|
|
7
|
+
{ key: "name", label: "Name" },
|
|
8
|
+
{ key: "email", label: "Email" },
|
|
9
|
+
];
|
|
10
|
+
const data = [
|
|
11
|
+
{ id: 1, name: "John Doe", email: "john@example.com" },
|
|
12
|
+
{ id: 2, name: "Jane Smith", email: "jane@example.com" },
|
|
13
|
+
];
|
|
14
|
+
it("renders header and rows", () => {
|
|
15
|
+
render(React.createElement(Table, { columns: columns, data: data, title: "Users", showPagination: false }));
|
|
16
|
+
expect(screen.getByText("Users")).toBeInTheDocument();
|
|
17
|
+
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
|
18
|
+
expect(screen.getByText("jane@example.com")).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
it("applies theme data attribute and font family", () => {
|
|
21
|
+
const { container } = render(React.createElement(Table, { columns: columns, data: data, theme: "cynk", showPagination: false }));
|
|
22
|
+
const root = container.firstChild;
|
|
23
|
+
expect(root.style.fontFamily).toContain("Inter");
|
|
24
|
+
});
|
|
25
|
+
it("handles row click when enabled", () => {
|
|
26
|
+
const handleRowClick = jest.fn();
|
|
27
|
+
render(React.createElement(Table, { columns: columns, data: data, enableRowClick: true, onRowClick: handleRowClick, showPagination: false }));
|
|
28
|
+
const row = screen.getByText("John Doe").closest("tr");
|
|
29
|
+
expect(row).not.toBeNull();
|
|
30
|
+
if (row) {
|
|
31
|
+
fireEvent.click(row);
|
|
32
|
+
}
|
|
33
|
+
expect(handleRowClick).toHaveBeenCalledTimes(1);
|
|
34
|
+
expect(handleRowClick).toHaveBeenCalledWith(expect.objectContaining({ name: "John Doe" }), 0);
|
|
35
|
+
});
|
|
36
|
+
it("renders actions and calls action handler", () => {
|
|
37
|
+
const actionFn = jest.fn();
|
|
38
|
+
render(React.createElement(Table, { columns: columns, data: data, actions: [{ label: "View", action: actionFn, variant: "primary" }], showPagination: false }));
|
|
39
|
+
const button = screen.getAllByText("View")[0];
|
|
40
|
+
fireEvent.click(button);
|
|
41
|
+
expect(actionFn).toHaveBeenCalledTimes(1);
|
|
42
|
+
expect(actionFn).toHaveBeenCalledWith(expect.objectContaining({ name: "John Doe" }));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import { type CompanyTheme } from "@lamenna/lxp-shared-components";
|
|
3
|
+
interface ThemeContextValue {
|
|
4
|
+
theme: CompanyTheme;
|
|
5
|
+
setTheme: (theme: CompanyTheme) => void;
|
|
6
|
+
toggleTheme: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare const useTheme: () => ThemeContextValue;
|
|
9
|
+
export interface ThemeProviderProps {
|
|
10
|
+
/**
|
|
11
|
+
* Controlled theme value. When provided, the component becomes controlled
|
|
12
|
+
* and `setTheme` / `toggleTheme` will update internal state but the final
|
|
13
|
+
* theme is driven by this prop.
|
|
14
|
+
*/
|
|
15
|
+
theme?: CompanyTheme;
|
|
16
|
+
/**
|
|
17
|
+
* Default theme to use when `theme` is not provided (uncontrolled mode).
|
|
18
|
+
*/
|
|
19
|
+
defaultTheme?: CompanyTheme;
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare const ThemeProvider: React.FC<ThemeProviderProps>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo, useState, } from "react";
|
|
2
|
+
import { themeConfig, defaultTheme, } from "@lamenna/lxp-shared-components";
|
|
3
|
+
const ThemeContext = createContext(undefined);
|
|
4
|
+
export const useTheme = () => {
|
|
5
|
+
const ctx = useContext(ThemeContext);
|
|
6
|
+
if (!ctx) {
|
|
7
|
+
throw new Error("useTheme must be used within a ThemeProvider");
|
|
8
|
+
}
|
|
9
|
+
return ctx;
|
|
10
|
+
};
|
|
11
|
+
export const ThemeProvider = ({ theme: controlledTheme, defaultTheme: defaultThemeProp = defaultTheme, children, className, }) => {
|
|
12
|
+
const [uncontrolledTheme, setUncontrolledTheme] = useState(controlledTheme ?? defaultThemeProp);
|
|
13
|
+
const currentTheme = controlledTheme ?? uncontrolledTheme;
|
|
14
|
+
const value = useMemo(() => ({
|
|
15
|
+
theme: currentTheme,
|
|
16
|
+
setTheme: (next) => setUncontrolledTheme(next),
|
|
17
|
+
toggleTheme: () => setUncontrolledTheme((prev) => (prev === "tera" ? "cynk" : "tera")),
|
|
18
|
+
}), [currentTheme]);
|
|
19
|
+
const themeStyles = themeConfig[currentTheme];
|
|
20
|
+
return (React.createElement(ThemeContext.Provider, { value: value },
|
|
21
|
+
React.createElement("div", { className: className, "data-theme": currentTheme, style: { fontFamily: themeStyles.fontFamily } }, children)));
|
|
22
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import { ThemeProvider, useTheme } from "../ThemeProvider";
|
|
5
|
+
const Consumer = () => {
|
|
6
|
+
const { theme } = useTheme();
|
|
7
|
+
return React.createElement("div", { "data-testid": "theme-consumer" }, theme);
|
|
8
|
+
};
|
|
9
|
+
describe("ThemeProvider (React)", () => {
|
|
10
|
+
it("provides default theme when none is specified", () => {
|
|
11
|
+
render(React.createElement(ThemeProvider, null,
|
|
12
|
+
React.createElement(Consumer, null)));
|
|
13
|
+
expect(screen.getByTestId("theme-consumer")).toHaveTextContent("tera");
|
|
14
|
+
});
|
|
15
|
+
it("respects initial theme prop", () => {
|
|
16
|
+
render(React.createElement(ThemeProvider, { defaultTheme: "cynk" },
|
|
17
|
+
React.createElement(Consumer, null)));
|
|
18
|
+
expect(screen.getByTestId("theme-consumer")).toHaveTextContent("cynk");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useTheme } from "../ThemeProvider/ThemeProvider";
|
|
3
|
+
export const ThemeSwitcher = ({ className = "", showLabels = true, }) => {
|
|
4
|
+
const { theme, toggleTheme } = useTheme();
|
|
5
|
+
const nextThemeLabel = theme === "tera" ? "Cynk" : "Tera";
|
|
6
|
+
return (React.createElement("button", { type: "button", onClick: toggleTheme, className: className, title: `Switch to ${nextThemeLabel} theme`, style: {
|
|
7
|
+
padding: "8px 16px",
|
|
8
|
+
borderRadius: "6px",
|
|
9
|
+
border: "1px solid #E5E7EB",
|
|
10
|
+
background: "#FFFFFF",
|
|
11
|
+
color: "#111827",
|
|
12
|
+
cursor: "pointer",
|
|
13
|
+
fontSize: "0.875rem",
|
|
14
|
+
display: "inline-flex",
|
|
15
|
+
alignItems: "center",
|
|
16
|
+
gap: "0.5rem",
|
|
17
|
+
} }, showLabels ? (React.createElement("span", null, theme === "tera" ? "Tera theme" : "Cynk theme")) : (React.createElement("span", null, theme === "tera" ? "T" : "C"))));
|
|
18
|
+
};
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lamenna/lxp-react",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "React components for LXP",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,20 +11,20 @@
|
|
|
11
11
|
"access": "public"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"react": "^
|
|
14
|
+
"react": "^19.0.0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@lamenna/lxp-core": "0.0
|
|
18
|
-
"@lamenna/lxp-tokens": "0.0
|
|
19
|
-
"@lamenna/lxp-shared-components": "0.0
|
|
17
|
+
"@lamenna/lxp-core": "0.1.0",
|
|
18
|
+
"@lamenna/lxp-tokens": "0.1.0",
|
|
19
|
+
"@lamenna/lxp-shared-components": "0.1.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@testing-library/jest-dom": "^6.1.5",
|
|
23
23
|
"@testing-library/react": "^14.1.2",
|
|
24
24
|
"@types/jest": "^29.5.11",
|
|
25
|
-
"@types/react": "^
|
|
26
|
-
"react": "^
|
|
27
|
-
"typescript": "^5.
|
|
25
|
+
"@types/react": "^19.0.0",
|
|
26
|
+
"react": "^19.2.3",
|
|
27
|
+
"typescript": "^5.9.3"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "tsc",
|