@axzydev/axzy_ui_system 1.3.0 → 1.3.2
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/README.md +6 -2
- package/dist/index.cjs +2342 -1715
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +286 -9
- package/dist/index.d.ts +286 -9
- package/dist/index.js +2164 -1538
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/App.tsx +153 -153
- package/src/components/molecules/calendar/calendar.doc.mdx +2 -1
- package/src/components/molecules/calendar/calendar.tsx +102 -24
- package/src/components/molecules/masked-input/masked-input.doc.mdx +78 -0
- package/src/components/molecules/masked-input/masked-input.props.ts +58 -0
- package/src/components/molecules/masked-input/masked-input.stories.tsx +126 -0
- package/src/components/molecules/masked-input/masked-input.tsx +244 -0
- package/src/components/molecules/table/table.doc.mdx +64 -21
- package/src/components/molecules/table/table.props.ts +81 -2
- package/src/components/molecules/table/table.stories.tsx +155 -0
- package/src/components/molecules/table/table.tsx +177 -36
- package/src/components/organisms/data-table/ITDataTable.doc.mdx +37 -3
- package/src/components/organisms/data-table/ITDataTable.stories.tsx +43 -0
- package/src/components/organisms/data-table/dataTable.props.ts +64 -0
- package/src/components/organisms/data-table/dataTable.tsx +240 -82
- package/src/components/organisms/sidebar/sidebar.doc.mdx +5 -0
- package/src/components/organisms/sidebar/sidebar.props.ts +5 -0
- package/src/components/organisms/sidebar/sidebar.stories.tsx +1 -1
- package/src/components/organisms/sidebar/sidebar.tsx +27 -7
- package/src/components/organisms/topbar/topbar.doc.mdx +3 -0
- package/src/components/organisms/topbar/topbar.props.ts +16 -0
- package/src/components/organisms/topbar/topbar.tsx +133 -98
- package/src/components/templates/layout/layout.doc.mdx +2 -0
- package/src/components/templates/layout/layout.tsx +7 -1
- package/src/components/templates/page/page.doc.mdx +8 -2
- package/src/components/templates/page/page.props.ts +11 -1
- package/src/components/templates/page/page.tsx +11 -1
- package/src/dev.css +58 -1
- package/src/hooks/useElementSize.ts +46 -0
- package/src/hooks/useVirtualRows.ts +123 -0
- package/src/index.ts +6 -2
- package/src/sandbox/LandingShowcase.tsx +465 -0
- package/src/sandbox/NotFoundShowcase.tsx +70 -0
- package/src/sandbox/TopbarSearch.tsx +85 -0
- package/src/sandbox/navigation.ts +148 -0
- package/src/sandbox/useHashRoute.ts +149 -0
- package/src/showcases/DataShowcases.tsx +321 -24
- package/src/showcases/FormShowcases.tsx +178 -0
- package/src/showcases/PageShowcases.tsx +31 -0
- package/src/showcases/StructureShowcases.tsx +42 -4
- package/src/types/table.types.ts +48 -0
- package/src/utils/styles.ts +9 -4
- package/src/utils/table.utils.ts +31 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TableSize, TableVariants } from "@/types/table.types";
|
|
1
|
+
import { TableDensity, TableSize, TableVariants } from "@/types/table.types";
|
|
2
2
|
|
|
3
3
|
/** Data type of a table column, controls default rendering, filter UI, and sort comparison. */
|
|
4
4
|
export type ColumnType = "string" | "date" | "number" | "boolean" | "actions" | "catalog";
|
|
@@ -46,6 +46,22 @@ export interface Column<T = any> {
|
|
|
46
46
|
/** Shows an error state in the filter UI when the catalog failed to load. @default false */
|
|
47
47
|
error?: boolean;
|
|
48
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* Fixed column width. A `number` is treated as pixels (`width: 120` → `120px`);
|
|
51
|
+
* a `string` is passed through as any CSS length (`"20%"`, `"12rem"`).
|
|
52
|
+
* A `<colgroup>` is only rendered when at least one column defines a width.
|
|
53
|
+
*/
|
|
54
|
+
width?: number | string;
|
|
55
|
+
/** Minimum column width in pixels, applied to the header and body cells via inline style. */
|
|
56
|
+
minWidth?: number;
|
|
57
|
+
/** Horizontal alignment of this column's header and body cells. @default "left" */
|
|
58
|
+
align?: "left" | "center" | "right";
|
|
59
|
+
/**
|
|
60
|
+
* Truncates overflowing cell content with an ellipsis and exposes the raw
|
|
61
|
+
* string/number value through the native `title` attribute.
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
truncate?: boolean;
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
export interface ITTableProps<T> {
|
|
@@ -55,7 +71,7 @@ export interface ITTableProps<T> {
|
|
|
55
71
|
containerClassName?: string;
|
|
56
72
|
/** The data array to render in the table body. */
|
|
57
73
|
data: T[];
|
|
58
|
-
/** Visual variant: "default", "striped", "bordered"
|
|
74
|
+
/** Visual variant: "default", "striped", "bordered". */
|
|
59
75
|
variant?: TableVariants;
|
|
60
76
|
/** Additional CSS classes for the root table wrapper. */
|
|
61
77
|
className?: string;
|
|
@@ -75,4 +91,67 @@ export interface ITTableProps<T> {
|
|
|
75
91
|
showVerticalBorder?: boolean;
|
|
76
92
|
/** Custom class for vertical borders (overrides the default subtle gray). */
|
|
77
93
|
verticalBorderClassname?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Callback fired when a row (table view) or card (cards view) is activated,
|
|
96
|
+
* receiving the row item and the originating event. The event is a
|
|
97
|
+
* `MouseEvent` for pointer interaction and a `KeyboardEvent` for Enter/Space
|
|
98
|
+
* activation. Not fired when the interaction starts inside an interactive
|
|
99
|
+
* child (button, link, input, or any `[data-row-click-ignore]` element).
|
|
100
|
+
*/
|
|
101
|
+
onRowClick?: (
|
|
102
|
+
row: T,
|
|
103
|
+
event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>
|
|
104
|
+
) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Table layout algorithm. `"fixed"` respects column `width` values and drops
|
|
107
|
+
* the `min-w-max` / `min-w-[150px]` floors so content no longer forces
|
|
108
|
+
* horizontal scrolling; `"auto"` keeps the content-driven layout.
|
|
109
|
+
* @default "auto"
|
|
110
|
+
*/
|
|
111
|
+
layout?: "auto" | "fixed";
|
|
112
|
+
/**
|
|
113
|
+
* Cell padding and row-height preset. Independent of `size`, which only
|
|
114
|
+
* controls font-size: `"compact"` | `"normal"` | `"comfortable"`.
|
|
115
|
+
* @default "normal"
|
|
116
|
+
*/
|
|
117
|
+
density?: TableDensity;
|
|
118
|
+
/**
|
|
119
|
+
* Container width (in px) below which the table automatically falls back to
|
|
120
|
+
* the cards view. `0` disables the fallback.
|
|
121
|
+
* @default 0
|
|
122
|
+
*/
|
|
123
|
+
autoCardBreakpoint?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Enables row virtualization for the table view: only the visible window of
|
|
126
|
+
* rows is rendered, with spacer rows preserving the full scroll height.
|
|
127
|
+
* The cards view is never virtualized.
|
|
128
|
+
* @default false
|
|
129
|
+
*/
|
|
130
|
+
virtualized?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Max height (px) of the virtualized table's scroll container.
|
|
133
|
+
* Only used when `virtualized` is true.
|
|
134
|
+
* @default 400
|
|
135
|
+
*/
|
|
136
|
+
virtualizedMaxHeight?: number;
|
|
137
|
+
/**
|
|
138
|
+
* Assumed uniform row height (px) used by the virtualizer. Defaults to
|
|
139
|
+
* `getRowHeight(size, density)`, which is size-aware: the `size="md"`
|
|
140
|
+
* baseline is 33 / 45 / 53 (compact / normal / comfortable) and `size`
|
|
141
|
+
* shifts it by the font-size line box (sm 16px / md 20px / lg 28px).
|
|
142
|
+
* Only used when `virtualized` is true.
|
|
143
|
+
*/
|
|
144
|
+
rowHeight?: number;
|
|
145
|
+
/**
|
|
146
|
+
* Number of extra rows rendered above and below the viewport to avoid blank
|
|
147
|
+
* gaps while scrolling. Only used when `virtualized` is true.
|
|
148
|
+
* @default 5
|
|
149
|
+
*/
|
|
150
|
+
overscan?: number;
|
|
151
|
+
/**
|
|
152
|
+
* Makes the header cells sticky while the virtualized body scrolls. Only
|
|
153
|
+
* effective when `virtualized` is true.
|
|
154
|
+
* @default false
|
|
155
|
+
*/
|
|
156
|
+
stickyHeader?: boolean;
|
|
78
157
|
}
|
|
@@ -21,6 +21,14 @@ const meta: Meta<typeof ITTable> = {
|
|
|
21
21
|
control: "select",
|
|
22
22
|
options: ["sm", "md", "lg"],
|
|
23
23
|
},
|
|
24
|
+
density: {
|
|
25
|
+
control: "select",
|
|
26
|
+
options: ["compact", "normal", "comfortable"],
|
|
27
|
+
},
|
|
28
|
+
layout: {
|
|
29
|
+
control: "select",
|
|
30
|
+
options: ["auto", "fixed"],
|
|
31
|
+
},
|
|
24
32
|
},
|
|
25
33
|
};
|
|
26
34
|
|
|
@@ -170,3 +178,150 @@ export const EmptyState: Story = {
|
|
|
170
178
|
title: "No Data Available",
|
|
171
179
|
} as any,
|
|
172
180
|
};
|
|
181
|
+
|
|
182
|
+
// Wrapper for clickable rows/cards
|
|
183
|
+
const RowClickWrapper = (args: any) => {
|
|
184
|
+
const [selected, setSelected] = useState<User | null>(null);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className="space-y-3">
|
|
188
|
+
<p className="text-sm font-medium text-slate-500">
|
|
189
|
+
{selected
|
|
190
|
+
? `Selected: ${selected.name} (id ${selected.id})`
|
|
191
|
+
: "Click a row/card or press Enter/Space while focused"}
|
|
192
|
+
</p>
|
|
193
|
+
<ITTable
|
|
194
|
+
{...args}
|
|
195
|
+
onRowClick={(row: User) => setSelected(row)}
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const WithRowClick: Story = {
|
|
202
|
+
render: (args) => <RowClickWrapper {...args} />,
|
|
203
|
+
args: {
|
|
204
|
+
columns: basicColumns,
|
|
205
|
+
data: mockData.slice(0, 5),
|
|
206
|
+
title: "Clickable Rows",
|
|
207
|
+
} as any,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export const WithRowClickCards: Story = {
|
|
211
|
+
render: (args) => <RowClickWrapper {...args} />,
|
|
212
|
+
args: {
|
|
213
|
+
columns: basicColumns,
|
|
214
|
+
data: mockData.slice(0, 5),
|
|
215
|
+
title: "Clickable Cards",
|
|
216
|
+
defaultView: "cards",
|
|
217
|
+
} as any,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// --- CONFIGURABLE LAYOUT / DENSITY ---
|
|
221
|
+
|
|
222
|
+
const longCellData: User[] = [
|
|
223
|
+
{
|
|
224
|
+
id: 1,
|
|
225
|
+
name: "Extraordinariamente Largo Nombre de Usuario Que No Cabe",
|
|
226
|
+
email: "usuario.con.un.correo.extremadamente.largo@example-enterprise-domain.com",
|
|
227
|
+
roleId: 1,
|
|
228
|
+
balance: 28400,
|
|
229
|
+
isActive: true,
|
|
230
|
+
lastLogin: "2026-07-18T09:30:00",
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: 2,
|
|
234
|
+
name: "Otra Persona Con Un Nombre Bastante Extenso Para Probar",
|
|
235
|
+
email: "otra.persona.con.correo.extenso@example-enterprise-domain.com",
|
|
236
|
+
roleId: 2,
|
|
237
|
+
balance: 15200,
|
|
238
|
+
isActive: false,
|
|
239
|
+
lastLogin: "2026-07-17T14:22:00",
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const fixedColumns: Column<User>[] = [
|
|
244
|
+
{ key: "id", label: "ID", type: "number", width: 64, minWidth: 48, align: "center" },
|
|
245
|
+
{ key: "name", label: "Name", type: "string", width: "35%", truncate: true },
|
|
246
|
+
{ key: "email", label: "Email", type: "string", width: "45%", truncate: true },
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
export const CompactDensity: Story = {
|
|
250
|
+
args: {
|
|
251
|
+
columns: advancedColumns.map(col => ({ ...col, render: undefined })),
|
|
252
|
+
data: mockData,
|
|
253
|
+
title: "Compact Density",
|
|
254
|
+
density: "compact",
|
|
255
|
+
defaultItemsPerPage: 8,
|
|
256
|
+
} as any,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export const FixedLayoutWithWidths: Story = {
|
|
260
|
+
render: (args: any) => (
|
|
261
|
+
<div style={{ width: 480 }} className="rounded-lg border border-dashed border-slate-300">
|
|
262
|
+
<ITTable {...args} />
|
|
263
|
+
</div>
|
|
264
|
+
),
|
|
265
|
+
args: {
|
|
266
|
+
columns: fixedColumns,
|
|
267
|
+
data: longCellData,
|
|
268
|
+
title: "Fixed layout · no horizontal scroll",
|
|
269
|
+
layout: "fixed",
|
|
270
|
+
density: "compact",
|
|
271
|
+
defaultItemsPerPage: 5,
|
|
272
|
+
} as any,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export const AutoCardsNarrowContainer: Story = {
|
|
276
|
+
render: (args: any) => (
|
|
277
|
+
<div style={{ width: 420 }} className="rounded-lg border border-dashed border-slate-300">
|
|
278
|
+
<ITTable {...args} />
|
|
279
|
+
</div>
|
|
280
|
+
),
|
|
281
|
+
args: {
|
|
282
|
+
columns: basicColumns,
|
|
283
|
+
data: mockData.slice(0, 6),
|
|
284
|
+
title: "Auto cards under 640px",
|
|
285
|
+
autoCardBreakpoint: 640,
|
|
286
|
+
defaultItemsPerPage: 6,
|
|
287
|
+
} as any,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// --- VIRTUALIZATION ---
|
|
291
|
+
|
|
292
|
+
const virtualColumns: Column<User>[] = [
|
|
293
|
+
{ key: "id", label: "ID", type: "number", width: 72, minWidth: 56, align: "center" },
|
|
294
|
+
{ key: "name", label: "Name", type: "string", width: "30%", truncate: true },
|
|
295
|
+
{ key: "email", label: "Email", type: "string", width: "40%", truncate: true },
|
|
296
|
+
{
|
|
297
|
+
key: "roleId",
|
|
298
|
+
label: "Role",
|
|
299
|
+
type: "catalog",
|
|
300
|
+
width: "30%",
|
|
301
|
+
truncate: true,
|
|
302
|
+
catalogOptions: { data: mockRoles },
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
export const VirtualizedLargeDataset: Story = {
|
|
307
|
+
render: (args: any) => (
|
|
308
|
+
<div className="space-y-2">
|
|
309
|
+
<p className="text-xs text-slate-500">
|
|
310
|
+
250 rows · only the visible window is mounted (~10–20 <tr> + at most 2 spacer rows)
|
|
311
|
+
</p>
|
|
312
|
+
<ITTable {...args} />
|
|
313
|
+
</div>
|
|
314
|
+
),
|
|
315
|
+
args: {
|
|
316
|
+
columns: virtualColumns,
|
|
317
|
+
data: generateMockData(250),
|
|
318
|
+
title: "Virtualized · 250 rows",
|
|
319
|
+
virtualized: true,
|
|
320
|
+
stickyHeader: true,
|
|
321
|
+
layout: "fixed",
|
|
322
|
+
density: "compact",
|
|
323
|
+
virtualizedMaxHeight: 360,
|
|
324
|
+
defaultItemsPerPage: 250,
|
|
325
|
+
itemsPerPageOptions: [50, 100, 250],
|
|
326
|
+
} as any,
|
|
327
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useTableState } from "@/hooks/useTableState";
|
|
2
|
-
import {
|
|
2
|
+
import { useElementSize } from "@/hooks/useElementSize";
|
|
3
|
+
import { useVirtualRows } from "@/hooks/useVirtualRows";
|
|
4
|
+
import { getRowHeight, sizeStyles, tableAlignClasses, variantStyles } from "@/types/table.types";
|
|
3
5
|
import clsx from "clsx";
|
|
4
6
|
import React, { useState } from "react";
|
|
5
7
|
import {
|
|
@@ -18,6 +20,7 @@ import ITText from "@/components/atoms/text/text";
|
|
|
18
20
|
import {
|
|
19
21
|
tableActionsCell,
|
|
20
22
|
tableBody,
|
|
23
|
+
tableCardClickable,
|
|
21
24
|
tableCell,
|
|
22
25
|
tableCellText,
|
|
23
26
|
tableContainer,
|
|
@@ -25,8 +28,9 @@ import {
|
|
|
25
28
|
tableHeaderCell,
|
|
26
29
|
tableHeaderRow,
|
|
27
30
|
tableRow,
|
|
31
|
+
tableRowClickable,
|
|
28
32
|
} from "@/utils/styles";
|
|
29
|
-
import { formatCurrencyMX, getNestedValue } from "@/utils/table.utils";
|
|
33
|
+
import { formatCurrencyMX, getNestedValue, isInteractiveTarget } from "@/utils/table.utils";
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
36
|
* A feature-rich data table with per-column filtering, sortable columns,
|
|
@@ -57,8 +61,27 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
57
61
|
defaultView = "table",
|
|
58
62
|
showVerticalBorder = true,
|
|
59
63
|
verticalBorderClassname,
|
|
64
|
+
onRowClick,
|
|
65
|
+
layout = "auto",
|
|
66
|
+
density = "normal",
|
|
67
|
+
autoCardBreakpoint = 0,
|
|
68
|
+
virtualized = false,
|
|
69
|
+
virtualizedMaxHeight = 400,
|
|
70
|
+
rowHeight,
|
|
71
|
+
overscan = 5,
|
|
72
|
+
stickyHeader = false,
|
|
60
73
|
}: ITTableProps<T>) {
|
|
61
74
|
const [viewMode, setViewMode] = useState<"table" | "cards">(defaultView);
|
|
75
|
+
const { ref: rootRef, width: containerWidth } = useElementSize<HTMLDivElement>();
|
|
76
|
+
|
|
77
|
+
const isFixed = layout === "fixed";
|
|
78
|
+
const forcedCards =
|
|
79
|
+
autoCardBreakpoint > 0 && containerWidth > 0 && containerWidth < autoCardBreakpoint;
|
|
80
|
+
const effectiveView: "table" | "cards" = forcedCards ? "cards" : viewMode;
|
|
81
|
+
const hasColumnWidths = columns.some((col) => col.width != null);
|
|
82
|
+
const tableLayoutClass = isFixed
|
|
83
|
+
? "w-full text-sm text-left text-secondary-600"
|
|
84
|
+
: "min-w-max w-full text-sm text-left text-secondary-600";
|
|
62
85
|
|
|
63
86
|
const {
|
|
64
87
|
currentPage,
|
|
@@ -159,6 +182,29 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
159
182
|
currentPage * itemsPerPage
|
|
160
183
|
);
|
|
161
184
|
|
|
185
|
+
const isTableVirtual = virtualized && effectiveView === "table";
|
|
186
|
+
const isStickyHeader = stickyHeader && isTableVirtual;
|
|
187
|
+
const resolvedRowHeight = rowHeight ?? getRowHeight(size, density);
|
|
188
|
+
const columnCount = columns.length;
|
|
189
|
+
|
|
190
|
+
const {
|
|
191
|
+
scrollRef,
|
|
192
|
+
startIndex,
|
|
193
|
+
endIndex,
|
|
194
|
+
topSpacerHeight,
|
|
195
|
+
bottomSpacerHeight,
|
|
196
|
+
} = useVirtualRows({
|
|
197
|
+
count: currentData.length,
|
|
198
|
+
rowHeight: resolvedRowHeight,
|
|
199
|
+
overscan,
|
|
200
|
+
enabled: isTableVirtual,
|
|
201
|
+
resetKey: `${currentPage}-${itemsPerPage}`,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const windowData = isTableVirtual
|
|
205
|
+
? currentData.slice(startIndex, endIndex + 1)
|
|
206
|
+
: currentData;
|
|
207
|
+
|
|
162
208
|
const renderFilterInput = (col: Column<T>) => {
|
|
163
209
|
if (!col.filter) return null;
|
|
164
210
|
|
|
@@ -311,8 +357,21 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
311
357
|
);
|
|
312
358
|
};
|
|
313
359
|
|
|
360
|
+
const handleRowClick = (row: T) => (event: React.MouseEvent<HTMLElement>) => {
|
|
361
|
+
if (!onRowClick || isInteractiveTarget(event.target, event.currentTarget)) return;
|
|
362
|
+
onRowClick(row, event);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const handleRowKeyDown = (row: T) => (event: React.KeyboardEvent<HTMLElement>) => {
|
|
366
|
+
if (!onRowClick) return;
|
|
367
|
+
if (event.target !== event.currentTarget) return;
|
|
368
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
onRowClick(row, event);
|
|
371
|
+
};
|
|
372
|
+
|
|
314
373
|
return (
|
|
315
|
-
<div className={clsx("space-y-4 w-full", containerClassName)}>
|
|
374
|
+
<div ref={rootRef} className={clsx("space-y-4 w-full", containerClassName)}>
|
|
316
375
|
<div className={tableContainer} style={{ backgroundColor: 'var(--color-table-rowBg, #ffffff)' }}>
|
|
317
376
|
{/* Header outside overflow */}
|
|
318
377
|
{title && (
|
|
@@ -321,8 +380,9 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
321
380
|
<div className="flex items-center bg-slate-100 dark:bg-slate-800/80 rounded-lg p-0.5 border border-slate-200 dark:border-slate-700/50">
|
|
322
381
|
<button
|
|
323
382
|
onClick={() => setViewMode("table")}
|
|
383
|
+
disabled={forcedCards}
|
|
324
384
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${
|
|
325
|
-
|
|
385
|
+
effectiveView === "table"
|
|
326
386
|
? "bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-sm border border-slate-200/50 dark:border-slate-700"
|
|
327
387
|
: "text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
|
|
328
388
|
}`}
|
|
@@ -333,7 +393,7 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
333
393
|
<button
|
|
334
394
|
onClick={() => setViewMode("cards")}
|
|
335
395
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${
|
|
336
|
-
|
|
396
|
+
effectiveView === "cards"
|
|
337
397
|
? "bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-sm border border-slate-200/50 dark:border-slate-700"
|
|
338
398
|
: "text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
|
|
339
399
|
}`}
|
|
@@ -350,8 +410,9 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
350
410
|
<div className="flex items-center bg-slate-100 dark:bg-slate-800/80 rounded-lg p-0.5 border border-slate-200 dark:border-slate-700/50">
|
|
351
411
|
<button
|
|
352
412
|
onClick={() => setViewMode("table")}
|
|
413
|
+
disabled={forcedCards}
|
|
353
414
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${
|
|
354
|
-
|
|
415
|
+
effectiveView === "table"
|
|
355
416
|
? "bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-sm border border-slate-200/50 dark:border-slate-700"
|
|
356
417
|
: "text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
|
|
357
418
|
}`}
|
|
@@ -362,7 +423,7 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
362
423
|
<button
|
|
363
424
|
onClick={() => setViewMode("cards")}
|
|
364
425
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${
|
|
365
|
-
|
|
426
|
+
effectiveView === "cards"
|
|
366
427
|
? "bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-sm border border-slate-200/50 dark:border-slate-700"
|
|
367
428
|
: "text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
|
|
368
429
|
}`}
|
|
@@ -374,11 +435,18 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
374
435
|
</div>
|
|
375
436
|
)}
|
|
376
437
|
|
|
377
|
-
{
|
|
438
|
+
{effectiveView === "cards" ? (
|
|
378
439
|
<div className="p-4 space-y-3 max-h-[400px] overflow-y-auto">
|
|
379
440
|
{currentData.length > 0 ? (
|
|
380
441
|
currentData.map((row, i) => (
|
|
381
|
-
<div
|
|
442
|
+
<div
|
|
443
|
+
key={i}
|
|
444
|
+
className={clsx(onRowClick && tableCardClickable)}
|
|
445
|
+
role={onRowClick ? "button" : undefined}
|
|
446
|
+
tabIndex={onRowClick ? 0 : undefined}
|
|
447
|
+
onClick={onRowClick ? handleRowClick(row) : undefined}
|
|
448
|
+
onKeyDown={onRowClick ? handleRowKeyDown(row) : undefined}
|
|
449
|
+
>
|
|
382
450
|
{renderCard ? renderCard(row) : renderDefaultCard(row)}
|
|
383
451
|
</div>
|
|
384
452
|
))
|
|
@@ -390,25 +458,62 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
390
458
|
)}
|
|
391
459
|
</div>
|
|
392
460
|
) : (
|
|
393
|
-
<div
|
|
461
|
+
<div
|
|
462
|
+
ref={isTableVirtual ? scrollRef : undefined}
|
|
463
|
+
className={isTableVirtual ? "overflow-x-auto overflow-y-auto" : "overflow-x-auto"}
|
|
464
|
+
style={isTableVirtual ? { maxHeight: virtualizedMaxHeight } : undefined}
|
|
465
|
+
>
|
|
394
466
|
<table
|
|
395
467
|
className={clsx(
|
|
396
|
-
|
|
468
|
+
tableLayoutClass,
|
|
397
469
|
showVerticalBorder && "[&_th]:border-r [&_th:last-child]:border-r-0 [&_td]:border-r [&_td:last-child]:border-r-0",
|
|
398
470
|
showVerticalBorder && (verticalBorderClassname || "[&_th]:border-slate-100 dark:[&_th]:border-slate-700/30 [&_td]:border-slate-100 dark:[&_td]:border-slate-700/30"),
|
|
399
471
|
variantStyles[variant],
|
|
400
472
|
sizeStyles[size]
|
|
401
473
|
)}
|
|
474
|
+
style={isFixed ? { tableLayout: "fixed" } : undefined}
|
|
475
|
+
aria-rowcount={isTableVirtual ? currentData.length + 1 : undefined}
|
|
402
476
|
>
|
|
477
|
+
{hasColumnWidths && (
|
|
478
|
+
<colgroup>
|
|
479
|
+
{columns.map((col) => (
|
|
480
|
+
<col
|
|
481
|
+
key={col.key}
|
|
482
|
+
style={
|
|
483
|
+
col.width != null
|
|
484
|
+
? { width: typeof col.width === "number" ? `${col.width}px` : col.width }
|
|
485
|
+
: undefined
|
|
486
|
+
}
|
|
487
|
+
/>
|
|
488
|
+
))}
|
|
489
|
+
</colgroup>
|
|
490
|
+
)}
|
|
403
491
|
<thead>
|
|
404
|
-
<tr
|
|
492
|
+
<tr
|
|
493
|
+
className={clsx(tableHeaderRow, "dark:text-slate-200")}
|
|
494
|
+
aria-rowindex={isTableVirtual ? 1 : undefined}
|
|
495
|
+
>
|
|
405
496
|
{columns.map((col) => (
|
|
406
497
|
<th
|
|
407
498
|
key={col.key}
|
|
408
499
|
scope="col"
|
|
409
|
-
className={
|
|
500
|
+
className={clsx(
|
|
501
|
+
tableHeaderCell(col.className, density),
|
|
502
|
+
col.align && tableAlignClasses[col.align],
|
|
503
|
+
isStickyHeader && "sticky top-0 z-10 bg-secondary-50"
|
|
504
|
+
)}
|
|
505
|
+
style={
|
|
506
|
+
col.minWidth != null || isStickyHeader
|
|
507
|
+
? {
|
|
508
|
+
...(col.minWidth != null ? { minWidth: `${col.minWidth}px` } : {}),
|
|
509
|
+
...(isStickyHeader
|
|
510
|
+
? { backgroundColor: "var(--color-table-headerBg, #f8fafc)" }
|
|
511
|
+
: {}),
|
|
512
|
+
}
|
|
513
|
+
: undefined
|
|
514
|
+
}
|
|
410
515
|
>
|
|
411
|
-
<div className="flex flex-col gap-3 min-w-[150px]">
|
|
516
|
+
<div className={isFixed ? "flex flex-col gap-3" : "flex flex-col gap-3 min-w-[150px]"}>
|
|
412
517
|
<div className="flex items-center justify-between gap-2">
|
|
413
518
|
<ITText as="span" className="text-slate-900 dark:text-white font-bold">{col.label}</ITText>
|
|
414
519
|
{col.sortable && col.type !== "actions" && (
|
|
@@ -434,29 +539,65 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
434
539
|
</thead>
|
|
435
540
|
<tbody className={clsx(tableBody, "dark:divide-slate-700/30")}>
|
|
436
541
|
{currentData.length > 0 ? (
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
{
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
{renderCellContent(col, row) as React.ReactNode}
|
|
454
|
-
</div>
|
|
542
|
+
<>
|
|
543
|
+
{isTableVirtual && topSpacerHeight > 0 && (
|
|
544
|
+
<tr aria-hidden="true" style={{ height: topSpacerHeight }}>
|
|
545
|
+
<td colSpan={columnCount} style={{ padding: 0, border: 0 }} />
|
|
546
|
+
</tr>
|
|
547
|
+
)}
|
|
548
|
+
{windowData.map((row, windowIndex) => {
|
|
549
|
+
const absoluteIndex = isTableVirtual ? startIndex + windowIndex : windowIndex;
|
|
550
|
+
return (
|
|
551
|
+
<tr
|
|
552
|
+
key={absoluteIndex}
|
|
553
|
+
aria-rowindex={isTableVirtual ? absoluteIndex + 2 : undefined}
|
|
554
|
+
className={clsx(
|
|
555
|
+
tableRow,
|
|
556
|
+
variant === "striped" && absoluteIndex % 2 === 0 && "bg-secondary-50/40 dark:bg-slate-800/20",
|
|
557
|
+
onRowClick && tableRowClickable
|
|
455
558
|
)}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
559
|
+
tabIndex={onRowClick ? 0 : undefined}
|
|
560
|
+
onClick={onRowClick ? handleRowClick(row) : undefined}
|
|
561
|
+
onKeyDown={onRowClick ? handleRowKeyDown(row) : undefined}
|
|
562
|
+
>
|
|
563
|
+
{columns.map((col) => {
|
|
564
|
+
const rawValue = getNestedValue(row, col.key);
|
|
565
|
+
const truncateTitle =
|
|
566
|
+
col.truncate &&
|
|
567
|
+
(typeof rawValue === "string" || typeof rawValue === "number")
|
|
568
|
+
? String(rawValue)
|
|
569
|
+
: undefined;
|
|
570
|
+
return (
|
|
571
|
+
<td
|
|
572
|
+
key={`${absoluteIndex}-${col.key}`}
|
|
573
|
+
className={clsx(
|
|
574
|
+
tableCell(col.className, density),
|
|
575
|
+
col.align && tableAlignClasses[col.align]
|
|
576
|
+
)}
|
|
577
|
+
style={col.minWidth != null ? { minWidth: `${col.minWidth}px` } : undefined}
|
|
578
|
+
title={truncateTitle}
|
|
579
|
+
>
|
|
580
|
+
{col.type === "actions" ? (
|
|
581
|
+
<div className={tableActionsCell}>
|
|
582
|
+
{renderCellContent(col, row) as React.ReactNode}
|
|
583
|
+
</div>
|
|
584
|
+
) : (
|
|
585
|
+
<div className={clsx(tableCellText, col.truncate && "truncate")}>
|
|
586
|
+
{renderCellContent(col, row) as React.ReactNode}
|
|
587
|
+
</div>
|
|
588
|
+
)}
|
|
589
|
+
</td>
|
|
590
|
+
);
|
|
591
|
+
})}
|
|
592
|
+
</tr>
|
|
593
|
+
);
|
|
594
|
+
})}
|
|
595
|
+
{isTableVirtual && bottomSpacerHeight > 0 && (
|
|
596
|
+
<tr aria-hidden="true" style={{ height: bottomSpacerHeight }}>
|
|
597
|
+
<td colSpan={columnCount} style={{ padding: 0, border: 0 }} />
|
|
598
|
+
</tr>
|
|
599
|
+
)}
|
|
600
|
+
</>
|
|
460
601
|
) : (
|
|
461
602
|
<tr>
|
|
462
603
|
<td colSpan={columns.length} className="px-6 py-12 text-center">
|
|
@@ -11,7 +11,7 @@ Advanced server-side data table with automatic pagination, column sorting, and f
|
|
|
11
11
|
|
|
12
12
|
| Prop | Type | Default | Description |
|
|
13
13
|
|------|------|---------|-------------|
|
|
14
|
-
| `columns` | `Column<T>[]` | — | Column definitions with key, label, type, sortable, filter, render, and
|
|
14
|
+
| `columns` | `Column<T>[]` | — | Column definitions with key, label, type, sortable, filter, render, plus layout fields: `width` (`number` px or CSS `string`), `minWidth` (px), `align` (`"left" \| "center" \| "right"`), and `truncate` (ellipsis + native `title`). |
|
|
15
15
|
| `fetchData` | `(params: ITDataTableFetchParams) => Promise<ITDataTableResponse<T>>` | — | Async callback triggered on pagination, filter, or sort change. Must return `{ data, total }`. |
|
|
16
16
|
| `debounceMs` | `number` | `500` | Delay in ms before firing fetch after filter state changes. |
|
|
17
17
|
| `externalFilters` | `Record<string, string \| number \| boolean \| Date>` | `{}` | Filters managed outside the table (e.g., date pickers) merged with internal column filters. |
|
|
@@ -20,11 +20,24 @@ Advanced server-side data table with automatic pagination, column sorting, and f
|
|
|
20
20
|
| `reloadTrigger` | `number \| string \| boolean` | — | Changing this value forces a re-fetch. Use after mutations. |
|
|
21
21
|
| `containerClassName` | `string` | — | CSS classes for the outer wrapper. |
|
|
22
22
|
| `className` | `string` | — | CSS classes applied to the `<table>` element. |
|
|
23
|
-
| `variant` | `"default" \| "striped" \| "bordered" \| "minimal"` | `"default"` | Visual style variant. |
|
|
24
|
-
| `size` | `"sm" \| "md" \| "lg"` | `"md"` |
|
|
23
|
+
| `variant` | `"default" \| "striped" \| "bordered" \| "minimal"` | `"default"` | Visual style variant. `"minimal"` is accepted by the type but has no styles (renders like `"default"`). |
|
|
24
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Font-size preset (does not change padding; use `density` for that). |
|
|
25
25
|
| `itemsPerPageOptions` | `number[]` | `[5, 10, 20]` | Selectable page-size options. |
|
|
26
26
|
| `defaultItemsPerPage` | `number` | `10` | Initial rows per page. |
|
|
27
27
|
| `title` | `string \| ReactNode` | — | Heading displayed above the table. |
|
|
28
|
+
| `renderCard` | `(row: T) => ReactNode` | — | Custom card renderer for the cards view. |
|
|
29
|
+
| `defaultView` | `"table" \| "cards"` | `"table"` | Initial view mode. |
|
|
30
|
+
| `showVerticalBorder` | `boolean` | `true` | Show vertical borders between columns. |
|
|
31
|
+
| `verticalBorderClassname` | `string` | — | Custom class for the vertical borders. |
|
|
32
|
+
| `onRowClick` | `(row: T, event: MouseEvent<HTMLElement> \| KeyboardEvent<HTMLElement>) => void` | — | Row/card activation. `MouseEvent` for pointer, `KeyboardEvent` for Enter/Space. Skipped when the interaction starts inside an interactive child. |
|
|
33
|
+
| `layout` | `"auto" \| "fixed"` | `"auto"` | Table layout algorithm. `"fixed"` honors column `width` values and drops the `min-w-max` / `min-w-[150px]` floors so content no longer forces horizontal scrolling. |
|
|
34
|
+
| `density` | `"compact" \| "normal" \| "comfortable"` | `"normal"` | Cell padding / row-height preset. Independent of `size`, which only controls font-size. |
|
|
35
|
+
| `autoCardBreakpoint` | `number` | `0` | Container width (px) below which the table automatically falls back to the cards view. `0` disables the fallback. |
|
|
36
|
+
| `virtualized` | `boolean` | `false` | Renders only the visible row window with spacer rows preserving scroll height. Table view only; cards view is never virtualized. |
|
|
37
|
+
| `virtualizedMaxHeight` | `number` | `400` | Max height (px) of the virtualized scroll container. Only used when `virtualized` is true. |
|
|
38
|
+
| `rowHeight` | `number` | `getRowHeight(size, density)` | Assumed uniform row height (px). Size-aware: `size="md"` baseline is 33 / 45 / 53 for compact / normal / comfortable; `size` shifts it by the font-size line box (sm 16px / md 20px / lg 28px). |
|
|
39
|
+
| `overscan` | `number` | `5` | Extra rows rendered above and below the viewport to avoid blank gaps. |
|
|
40
|
+
| `stickyHeader` | `boolean` | `false` | Keeps header cells sticky while the virtualized body scrolls. Only effective when `virtualized` is true. |
|
|
28
41
|
|
|
29
42
|
## Usage
|
|
30
43
|
|
|
@@ -42,6 +55,18 @@ import { ITDataTable } from '@axzydev/axzy_ui_system';
|
|
|
42
55
|
fetchData={async (params) => api.fetchItems(params)}
|
|
43
56
|
variant="bordered"
|
|
44
57
|
/>
|
|
58
|
+
|
|
59
|
+
// No horizontal scroll: fixed layout + per-column widths + truncation
|
|
60
|
+
<ITDataTable
|
|
61
|
+
columns={[
|
|
62
|
+
{ key: 'name', label: 'Name', type: 'string', width: '30%', truncate: true },
|
|
63
|
+
{ key: 'email', label: 'Email', type: 'string', width: 220, truncate: true },
|
|
64
|
+
{ key: 'role', label: 'Role', type: 'string', width: 140, align: 'center' },
|
|
65
|
+
]}
|
|
66
|
+
fetchData={async (params) => api.fetchItems(params)}
|
|
67
|
+
layout="fixed"
|
|
68
|
+
density="compact"
|
|
69
|
+
/>
|
|
45
70
|
```
|
|
46
71
|
|
|
47
72
|
## Notes
|
|
@@ -51,6 +76,15 @@ import { ITDataTable } from '@axzydev/axzy_ui_system';
|
|
|
51
76
|
- Debounce prevents excessive API calls while the user types in filter inputs.
|
|
52
77
|
- A loading skeleton overlay covers the table body while fetching, keeping the header visible.
|
|
53
78
|
- Empty state is shown automatically when no results match the current filters.
|
|
79
|
+
- `onRowClick` fires in **both** the table and cards views. The event is a `MouseEvent` for pointer interaction and a `KeyboardEvent` for Enter/Space activation. It is not fired while `isLoading`, and interactions that start inside an interactive child (button, link, input, or `[data-row-click-ignore]`) are ignored.
|
|
80
|
+
- `density` controls padding/row height (`compact` 33px / `normal` 45px / `comfortable` 53px at `size="md"`); `size` changes font-size and shifts the row height by the line box (sm 16px / md 20px / lg 28px).
|
|
81
|
+
- **Anti horizontal scroll recipe:** `layout="fixed"` + a `width` on every column + `truncate` on long text + `density="compact"`. `width` accepts a `number` (px) or any CSS length string (`"20%"`, `"12rem"`). A `<colgroup>` is only emitted when at least one column defines a `width`.
|
|
82
|
+
- With the default `layout="auto"`, `min-w-max` stays active so column widths can still reflow as different cell content enters the window during vertical scroll. Use `layout="fixed"` + column `width` for stable widths.
|
|
83
|
+
- `autoCardBreakpoint` swaps to the cards view when the container (not the viewport) is narrower than the given px value; the Table toggle is disabled while forced, the Cards toggle stays enabled.
|
|
84
|
+
- **Virtualization** applies to the **table view only** — the cards view is never virtualized. It only pays off with a large page size: raise `defaultItemsPerPage` and `itemsPerPageOptions` (defaults are `10` and `[5, 10, 20]`). Rows are assumed to have a uniform `rowHeight`; use `truncate` + `layout="fixed"` for predictable heights.
|
|
85
|
+
- Scroll resets to the top on page or items-per-page change, but **not** on filter change (known limitation). `aria-rowcount` reflects the current page (+1), not the whole dataset, because pagination is independent.
|
|
86
|
+
- `virtualizedMaxHeight`, `rowHeight`, `overscan`, and `stickyHeader` only take effect when `virtualized` is true. `stickyHeader` makes each `<th>` sticky using the `--color-table-headerBg` token (fallback `#f8fafc`).
|
|
87
|
+
- `TableDensity` is exported from the package. `ITSearchTable` does **not** support `layout`, `density`, `autoCardBreakpoint`, `virtualized`, or per-column `width` — do not assume parity.
|
|
54
88
|
|
|
55
89
|
## Stories
|
|
56
90
|
|