@axzydev/axzy_ui_system 1.3.3 → 1.3.5
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 +1 -1
- package/dist/index.cjs +1195 -946
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +90 -10
- package/dist/index.d.ts +90 -10
- package/dist/index.js +1189 -940
- package/dist/index.js.map +1 -1
- package/package.json +9 -3
- package/src/components/molecules/table/table.doc.mdx +23 -1
- package/src/components/molecules/table/table.props.ts +47 -2
- package/src/components/molecules/table/table.stories.tsx +75 -0
- package/src/components/molecules/table/table.tsx +140 -15
- package/src/components/organisms/data-table/ITDataTable.doc.mdx +25 -3
- package/src/components/organisms/data-table/ITDataTable.stories.tsx +98 -0
- package/src/components/organisms/data-table/dataTable.props.ts +8 -4
- package/src/components/organisms/data-table/dataTable.tsx +109 -8
- package/src/components/organisms/sidebar/sidebar.doc.mdx +39 -1
- package/src/components/organisms/sidebar/sidebar.props.ts +28 -2
- package/src/components/organisms/sidebar/sidebar.stories.tsx +61 -1
- package/src/components/organisms/sidebar/sidebar.tsx +111 -57
- package/src/hooks/useTableState.ts +17 -11
- package/src/index.css +10 -0
- package/src/index.ts +5 -4
- package/src/showcases/DataShowcases.tsx +217 -21
- package/src/showcases/StructureShowcases.tsx +33 -5
- package/src/types/table.types.ts +17 -0
- package/src/utils/table.utils.ts +83 -0
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"packageManager": "pnpm@10.34.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"version": "1.3.
|
|
7
|
+
"version": "1.3.5",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"sideEffects": [
|
|
10
10
|
"*.css"
|
|
@@ -38,7 +38,11 @@
|
|
|
38
38
|
"bundle": "tsup src/index.ts && node scripts/build-css.mjs",
|
|
39
39
|
"watch": "tsup src/index.ts --watch",
|
|
40
40
|
"storybook": "storybook dev -p 6006",
|
|
41
|
-
"build-storybook": "storybook build"
|
|
41
|
+
"build-storybook": "storybook build",
|
|
42
|
+
"ext:catalog": "node vscode-extension/scripts/generate-catalog.mjs",
|
|
43
|
+
"ext:build": "node vscode-extension/esbuild.mjs",
|
|
44
|
+
"ext:typecheck": "tsc --noEmit -p vscode-extension/tsconfig.json",
|
|
45
|
+
"ext:package": "node vscode-extension/esbuild.mjs && cd vscode-extension && npx --yes @vscode/vsce package --no-dependencies"
|
|
42
46
|
},
|
|
43
47
|
"dependencies": {
|
|
44
48
|
"clsx": "^2.1.1",
|
|
@@ -81,6 +85,8 @@
|
|
|
81
85
|
"vitest": "^4.0.18",
|
|
82
86
|
"playwright": "^1.58.0",
|
|
83
87
|
"@vitest/browser-playwright": "^4.0.18",
|
|
84
|
-
"@vitest/coverage-v8": "^4.0.18"
|
|
88
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
89
|
+
"@types/vscode": "1.85.0",
|
|
90
|
+
"esbuild": "^0.25.0"
|
|
85
91
|
}
|
|
86
92
|
}
|
|
@@ -11,7 +11,7 @@ Basic data table with column definitions, sorting, and configurable visual varia
|
|
|
11
11
|
|
|
12
12
|
| Prop | Type | Default | Description |
|
|
13
13
|
|------|------|---------|-------------|
|
|
14
|
-
| `columns` | `Column<T>[]` | — | Column definitions: key, label, type, sortable, filter, render, actions, plus layout fields `width` (`number` px or CSS `string`), `minWidth` (px), `align` (`"left" \| "center" \| "right"`),
|
|
14
|
+
| `columns` | `Column<T>[]` | — | Column definitions: key, label, type, sortable, filter, render, actions, plus layout fields `width` (`number` px or CSS `string`), `minWidth` (px), `align` (`"left" \| "center" \| "right"`), `truncate` (ellipsis + native `title`), and `pinned` (`"left" \| "right"`). |
|
|
15
15
|
| `data` | `T[]` | `[]` | Row data array. |
|
|
16
16
|
| `variant` | `"default" \| "striped" \| "bordered"` | `"default"` | Visual style variant. |
|
|
17
17
|
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Font-size preset (does not change padding). |
|
|
@@ -42,6 +42,8 @@ Basic data table with column definitions, sorting, and configurable visual varia
|
|
|
42
42
|
| `minWidth` | `number` | — | Minimum column width in px, applied to header and body cells via inline style. |
|
|
43
43
|
| `align` | `"left" \| "center" \| "right"` | `"left"` | Horizontal alignment of the column's header and cells. |
|
|
44
44
|
| `truncate` | `boolean` | `false` | Truncates overflowing cell content with an ellipsis and exposes the raw string/number via the native `title` attribute. |
|
|
45
|
+
| `pinned` | `"left" \| "right"` | `undefined` | Freezes the column to the given horizontal edge while the table scrolls sideways. `"left"` keeps it flush to the left edge, `"right"` to the right edge. Omit to leave the column unpinned (identical to the previous behavior). Offsets accumulate from each preceding pinned column's pixel `width` / `minWidth`, so give every pinned column of a side a pixel `width`. The pinned cell gets an opaque background (row background + row hover) and a 1px separator shadow on its frozen edge. One pinned column per side is the supported contract. |
|
|
46
|
+
| `filter` | `true \| "catalog" \| "search" \| "date" \| "date-range"` | `false` | Filter control rendered in the header. `true` = text/number input (or boolean toggle when `type: "boolean"`); `"catalog"` = `ITSelect` fed by `catalogOptions.data`; `"search"` = `ITSearchSelect` typeahead fed by `catalogOptions.data` (optional `catalogOptions.onSearch` for server-side search); `"date"` / `"date-range"` = `ITDatePicker` single/range, bounded by `dateFilterOptions`. |
|
|
45
47
|
|
|
46
48
|
## Usage
|
|
47
49
|
|
|
@@ -67,6 +69,15 @@ const data = [
|
|
|
67
69
|
onRowClick={(row, event) => console.log(row, event.type)}
|
|
68
70
|
/>
|
|
69
71
|
|
|
72
|
+
// Filter modes: ITSearchSelect typeahead + ITDatePicker single / range
|
|
73
|
+
const filterColumns = [
|
|
74
|
+
{ key: 'role', label: 'Role', type: 'string', filter: 'search', catalogOptions: { data: roles } },
|
|
75
|
+
{ key: 'lastLogin', label: 'Last login', type: 'date', filter: 'date', dateFilterOptions: { maxDate: new Date() } },
|
|
76
|
+
{ key: 'hiredAt', label: 'Hired', type: 'date', filter: 'date-range', dateFilterOptions: { maxDate: new Date() } },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
<ITTable columns={filterColumns} data={data} />
|
|
80
|
+
|
|
70
81
|
// No horizontal scroll: fixed layout + per-column widths + truncation
|
|
71
82
|
const fixedColumns = [
|
|
72
83
|
{ key: 'name', label: 'Name', type: 'string', width: '30%', truncate: true },
|
|
@@ -75,17 +86,28 @@ const fixedColumns = [
|
|
|
75
86
|
];
|
|
76
87
|
|
|
77
88
|
<ITTable columns={fixedColumns} data={data} layout="fixed" density="compact" />
|
|
89
|
+
|
|
90
|
+
// Pinned columns: freeze the first column left and the last one right
|
|
91
|
+
const pinnedColumns = [
|
|
92
|
+
{ key: 'id', label: 'ID', type: 'number', width: 72, pinned: 'left' },
|
|
93
|
+
{ key: 'name', label: 'Name', type: 'string', width: 240, truncate: true },
|
|
94
|
+
{ key: 'actions', label: 'Actions', type: 'actions', width: 104, pinned: 'right' },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
<ITTable columns={pinnedColumns} data={data} layout="fixed" density="compact" />
|
|
78
98
|
```
|
|
79
99
|
|
|
80
100
|
## Notes
|
|
81
101
|
|
|
82
102
|
- `columns[].render` provides custom cell rendering via render function
|
|
83
103
|
- `columns[].sortable` enables column header sort toggles
|
|
104
|
+
- `columns[].filter` supports `true` (text/number input, or boolean toggle for `type: "boolean"`), `"catalog"` (`ITSelect`), `"search"` (`ITSearchSelect` typeahead; add `catalogOptions.onSearch` for server-side search), and `"date"` / `"date-range"` (`ITDatePicker`; bound with `dateFilterOptions: { minDate, maxDate }`). Filtering is client-side: `"date"` matches the whole day, `"date-range"` includes both ends (`startOfDay` / `endOfDay`).
|
|
84
105
|
- `onRowClick` fires in **both** the table and cards views. The event is a `MouseEvent` for pointer interaction and a `KeyboardEvent` for Enter/Space activation.
|
|
85
106
|
- Clicks/keys that start inside an interactive child (`button`, `a`, `input`, `select`, `textarea`, `[role="button"]`, `[role="link"]`, `[data-row-click-ignore]`) do **not** trigger `onRowClick`.
|
|
86
107
|
- When `onRowClick` is omitted no `cursor-pointer`, focus ring, `tabIndex`, or `role` is added.
|
|
87
108
|
- `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).
|
|
88
109
|
- **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`.
|
|
110
|
+
- **Pinned columns:** set `columns[].pinned: "left" | "right"` to freeze a column to that edge during horizontal scroll (`position: sticky`; the wrapper needs no `relative`). Omitting `pinned` renders exactly as before. Sticky offsets accumulate from each preceding pinned column's pixel `width` / `minWidth`; a non-px width contributes `minWidth ?? 0`, so give every pinned column of a side a pixel `width`. The pinned cell gets an opaque background (`--color-table-rowBg`, row hover `--color-table-rowHover`) and a 1px separator shadow on its frozen edge. Z-layering: pinned body cell `z-[1]` < sticky header `z-10` < pinned header `z-[15]`; it composes with `stickyHeader` and `virtualized`. One pinned column per side is the supported contract. Known limitation: with `variant="striped"` the opaque pinned background covers that column's stripe tint.
|
|
89
111
|
- 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.
|
|
90
112
|
- `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.
|
|
91
113
|
- **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.
|
|
@@ -3,6 +3,18 @@ import { TableDensity, TableSize, TableVariants } from "@/types/table.types";
|
|
|
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";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Per-column filter mode, set through `Column.filter`.
|
|
8
|
+
*
|
|
9
|
+
* - `true` — text/number input (or boolean toggle when `type: "boolean"`).
|
|
10
|
+
* - `"catalog"` — `ITSelect` dropdown populated from `catalogOptions`.
|
|
11
|
+
* - `"search"` — `ITSearchSelect` typeahead populated from `catalogOptions`
|
|
12
|
+
* (respects `catalogOptions.onSearch` for server-side search).
|
|
13
|
+
* - `"date"` — `ITDatePicker` single-date filter.
|
|
14
|
+
* - `"date-range"` — `ITDatePicker` in range mode.
|
|
15
|
+
*/
|
|
16
|
+
export type ColumnFilterMode = boolean | "catalog" | "search" | "date" | "date-range";
|
|
17
|
+
|
|
6
18
|
/** A single selectable entry for a `"catalog"`-type column's filter/value lookup. */
|
|
7
19
|
export interface CatalogOption {
|
|
8
20
|
/** Unique identifier matched against the row's field value. */
|
|
@@ -23,8 +35,8 @@ export interface Column<T = any> {
|
|
|
23
35
|
currencyMX?: boolean;
|
|
24
36
|
/** Custom action buttons/content rendered for a `"actions"`-type column. Receives the full row object. */
|
|
25
37
|
actions?: (row: T) => React.ReactNode;
|
|
26
|
-
/** Enables per-column filtering. Pass `true` for a text/number/boolean filter matching `type`, or `"
|
|
27
|
-
filter?:
|
|
38
|
+
/** Enables per-column filtering. Pass `true` for a text/number/boolean filter matching `type`, `"catalog"` for an `ITSelect` fed by `catalogOptions`, `"search"` for an `ITSearchSelect` typeahead, or `"date"` / `"date-range"` for an `ITDatePicker`. @default false */
|
|
39
|
+
filter?: ColumnFilterMode;
|
|
28
40
|
/** Column data type. Drives default cell rendering, filter UI, and sort comparison. */
|
|
29
41
|
type: ColumnType;
|
|
30
42
|
/** Whether clicking the header sorts by this column. Ignored for `type: "actions"`. @default false */
|
|
@@ -45,6 +57,22 @@ export interface Column<T = any> {
|
|
|
45
57
|
loading?: boolean;
|
|
46
58
|
/** Shows an error state in the filter UI when the catalog failed to load. @default false */
|
|
47
59
|
error?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Server-side search handler for a `filter: "search"` column. When provided,
|
|
62
|
+
* `ITSearchSelect` delegates filtering to this callback (debounced) instead
|
|
63
|
+
* of filtering `data` locally; the parent is expected to update `data`.
|
|
64
|
+
*/
|
|
65
|
+
onSearch?: (query: string) => void;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Date bounds applied by the `"date"` and `"date-range"` filters, forwarded to
|
|
69
|
+
* the underlying `ITDatePicker`. Ignored by every other filter mode.
|
|
70
|
+
*/
|
|
71
|
+
dateFilterOptions?: {
|
|
72
|
+
/** Earliest selectable date. */
|
|
73
|
+
minDate?: Date;
|
|
74
|
+
/** Latest selectable date. */
|
|
75
|
+
maxDate?: Date;
|
|
48
76
|
};
|
|
49
77
|
/**
|
|
50
78
|
* Fixed column width. A `number` is treated as pixels (`width: 120` → `120px`);
|
|
@@ -62,6 +90,23 @@ export interface Column<T = any> {
|
|
|
62
90
|
* @default false
|
|
63
91
|
*/
|
|
64
92
|
truncate?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Pins this column to a horizontal edge so it stays visible while the table
|
|
95
|
+
* scrolls sideways.
|
|
96
|
+
*
|
|
97
|
+
* - `"left"` — freezes the column flush to the left edge.
|
|
98
|
+
* - `"right"` — freezes the column flush to the right edge.
|
|
99
|
+
*
|
|
100
|
+
* Omit to leave the column unpinned. When more than one column shares a side,
|
|
101
|
+
* sticky offsets accumulate from each preceding pinned column's `width` /
|
|
102
|
+
* `minWidth`; a column without a pixel width resolves to `0px`, so declare a
|
|
103
|
+
* pixel `width` on every pinned column of that side to avoid overlap. The
|
|
104
|
+
* pinned cell renders an opaque background (row background + row hover) and a
|
|
105
|
+
* 1px separator shadow on its frozen edge.
|
|
106
|
+
*
|
|
107
|
+
* @default undefined (column not pinned)
|
|
108
|
+
*/
|
|
109
|
+
pinned?: "left" | "right";
|
|
65
110
|
}
|
|
66
111
|
|
|
67
112
|
export interface ITTableProps<T> {
|
|
@@ -272,6 +272,51 @@ export const FixedLayoutWithWidths: Story = {
|
|
|
272
272
|
} as any,
|
|
273
273
|
};
|
|
274
274
|
|
|
275
|
+
// Pixel widths force the total past the 520px wrapper, so the ID (left) and
|
|
276
|
+
// Actions (right) columns stay frozen while the middle scrolls sideways.
|
|
277
|
+
const pinnedColumns: Column<User>[] = [
|
|
278
|
+
{ key: "id", label: "ID", type: "number", width: 72, minWidth: 56, align: "center", pinned: "left" },
|
|
279
|
+
{ key: "name", label: "Name", type: "string", width: 240, truncate: true },
|
|
280
|
+
{ key: "email", label: "Email", type: "string", width: 280, truncate: true },
|
|
281
|
+
{ key: "balance", label: "Balance", type: "number", width: 160, currencyMX: true },
|
|
282
|
+
{ key: "lastLogin", label: "Last login", type: "string", width: 160, truncate: true },
|
|
283
|
+
{
|
|
284
|
+
key: "isActive",
|
|
285
|
+
label: "Status",
|
|
286
|
+
type: "boolean",
|
|
287
|
+
width: 104,
|
|
288
|
+
align: "center",
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
key: "actions",
|
|
292
|
+
label: "Actions",
|
|
293
|
+
type: "actions",
|
|
294
|
+
width: 104,
|
|
295
|
+
pinned: "right",
|
|
296
|
+
actions: () => (
|
|
297
|
+
<ITButton size="sm" variant="text" color="danger" ariaLabel="Delete">
|
|
298
|
+
<FaTrash />
|
|
299
|
+
</ITButton>
|
|
300
|
+
),
|
|
301
|
+
},
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
export const PinnedColumns: Story = {
|
|
305
|
+
render: (args: any) => (
|
|
306
|
+
<div style={{ width: 520 }} className="rounded-lg border border-dashed border-slate-300">
|
|
307
|
+
<ITTable {...args} />
|
|
308
|
+
</div>
|
|
309
|
+
),
|
|
310
|
+
args: {
|
|
311
|
+
columns: pinnedColumns,
|
|
312
|
+
data: mockData.slice(0, 6),
|
|
313
|
+
title: "Pinned left (ID) & right (Actions)",
|
|
314
|
+
layout: "fixed",
|
|
315
|
+
density: "compact",
|
|
316
|
+
defaultItemsPerPage: 6,
|
|
317
|
+
} as any,
|
|
318
|
+
};
|
|
319
|
+
|
|
275
320
|
export const AutoCardsNarrowContainer: Story = {
|
|
276
321
|
render: (args: any) => (
|
|
277
322
|
<div style={{ width: 420 }} className="rounded-lg border border-dashed border-slate-300">
|
|
@@ -325,3 +370,33 @@ export const VirtualizedLargeDataset: Story = {
|
|
|
325
370
|
itemsPerPageOptions: [50, 100, 250],
|
|
326
371
|
} as any,
|
|
327
372
|
};
|
|
373
|
+
|
|
374
|
+
// --- COLUMN FILTER MODES ---
|
|
375
|
+
|
|
376
|
+
const filterModeColumns: Column<User>[] = [
|
|
377
|
+
{ key: "name", label: "Name", type: "string", filter: true },
|
|
378
|
+
{
|
|
379
|
+
key: "roleId",
|
|
380
|
+
label: "Role (ITSearchSelect)",
|
|
381
|
+
type: "catalog",
|
|
382
|
+
filter: "search",
|
|
383
|
+
catalogOptions: { data: mockRoles },
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
key: "lastLogin",
|
|
387
|
+
label: "Last login (ITDatePicker range)",
|
|
388
|
+
type: "date",
|
|
389
|
+
filter: "date-range",
|
|
390
|
+
dateFilterOptions: { maxDate: new Date() },
|
|
391
|
+
render: (row) => new Date(row.lastLogin).toLocaleDateString("es-MX"),
|
|
392
|
+
},
|
|
393
|
+
];
|
|
394
|
+
|
|
395
|
+
export const WithSearchAndDateFilters: Story = {
|
|
396
|
+
args: {
|
|
397
|
+
columns: filterModeColumns,
|
|
398
|
+
data: generateMockData(25),
|
|
399
|
+
title: "Filtros: ITSearchSelect + ITDatePicker (rango)",
|
|
400
|
+
defaultItemsPerPage: 8,
|
|
401
|
+
} as any,
|
|
402
|
+
};
|
|
@@ -15,6 +15,9 @@ import { MdOutlineSwapVert } from "react-icons/md";
|
|
|
15
15
|
import ITInput from "@/components/atoms/input/input";
|
|
16
16
|
import ITPagination from "@/components/molecules/pagination/pagination";
|
|
17
17
|
import ITSelect from "@/components/molecules/select/select";
|
|
18
|
+
import ITSearchSelect from "@/components/molecules/search-select/search-select";
|
|
19
|
+
import ITDatePicker from "@/components/molecules/date-picker/datePicker";
|
|
20
|
+
import { isSameDay, startOfDay, endOfDay } from "date-fns";
|
|
18
21
|
import { Column, ITTableProps } from "./table.props";
|
|
19
22
|
import ITText from "@/components/atoms/text/text";
|
|
20
23
|
import {
|
|
@@ -31,7 +34,7 @@ import {
|
|
|
31
34
|
tableRow,
|
|
32
35
|
tableRowClickable,
|
|
33
36
|
} from "@/utils/styles";
|
|
34
|
-
import { formatCurrencyMX, getNestedValue, isInteractiveTarget } from "@/utils/table.utils";
|
|
37
|
+
import { formatCurrencyMX, getNestedValue, getPinnedColumnMeta, isInteractiveTarget } from "@/utils/table.utils";
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
40
|
* A feature-rich data table with per-column filtering, sortable columns,
|
|
@@ -146,21 +149,39 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
146
149
|
|
|
147
150
|
const filteredData = sortedData.filter((row) =>
|
|
148
151
|
columns.every((col) => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)
|
|
152
|
+
const rawFilter = filters[col.key];
|
|
153
|
+
if (!col.filter || rawFilter === undefined || rawFilter === "") return true;
|
|
154
|
+
// A date range with both ends empty means "no filter".
|
|
155
|
+
if (Array.isArray(rawFilter) && rawFilter[0] == null && rawFilter[1] == null) {
|
|
154
156
|
return true;
|
|
157
|
+
}
|
|
155
158
|
|
|
156
159
|
const value = getNestedValue(row, col.key);
|
|
157
|
-
|
|
160
|
+
|
|
161
|
+
if (col.filter === "date" || col.filter === "date-range") {
|
|
162
|
+
const rowDate = value instanceof Date ? value : new Date(String(value));
|
|
163
|
+
if (isNaN(rowDate.getTime())) return false;
|
|
164
|
+
if (col.filter === "date") {
|
|
165
|
+
return isSameDay(rowDate, rawFilter as Date);
|
|
166
|
+
}
|
|
167
|
+
const [start, end] = rawFilter as [Date | null, Date | null];
|
|
168
|
+
if (start && rowDate.getTime() < startOfDay(start).getTime()) return false;
|
|
169
|
+
if (end && rowDate.getTime() > endOfDay(end).getTime()) return false;
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (col.filter === "search") {
|
|
174
|
+
// The search-select stores the exact option value (id).
|
|
175
|
+
return String(value) === String(rawFilter);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const filterValue = String(rawFilter).toLowerCase();
|
|
158
179
|
|
|
159
180
|
switch (col.type) {
|
|
160
181
|
case "number":
|
|
161
182
|
return String(value).includes(filterValue);
|
|
162
183
|
case "boolean":
|
|
163
|
-
return value ===
|
|
184
|
+
return value === rawFilter;
|
|
164
185
|
case "catalog": {
|
|
165
186
|
if (!col.catalogOptions) return true;
|
|
166
187
|
const catalogItem = col.catalogOptions.data.find(
|
|
@@ -187,6 +208,7 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
187
208
|
const isStickyHeader = stickyHeader && isTableVirtual;
|
|
188
209
|
const resolvedRowHeight = rowHeight ?? getRowHeight(size, density);
|
|
189
210
|
const columnCount = columns.length;
|
|
211
|
+
const pinnedMeta = React.useMemo(() => getPinnedColumnMeta(columns), [columns]);
|
|
190
212
|
|
|
191
213
|
const {
|
|
192
214
|
scrollRef,
|
|
@@ -283,6 +305,81 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
283
305
|
);
|
|
284
306
|
}
|
|
285
307
|
|
|
308
|
+
if (col.filter === "search" && col.catalogOptions) {
|
|
309
|
+
if (col.catalogOptions.loading) {
|
|
310
|
+
return (
|
|
311
|
+
<FaSpinner
|
|
312
|
+
className="animate-spin"
|
|
313
|
+
aria-label="Cargando opciones"
|
|
314
|
+
title="Cargando opciones"
|
|
315
|
+
/>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (col.catalogOptions.error) {
|
|
320
|
+
return <ITText as="span" className="text-danger-500 text-xs">Error cargando</ITText>;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const searchValue = filters[col.key];
|
|
324
|
+
return (
|
|
325
|
+
<ITSearchSelect
|
|
326
|
+
name={`filter-${col.key}`}
|
|
327
|
+
options={col.catalogOptions.data.map((item) => ({
|
|
328
|
+
value: item.id,
|
|
329
|
+
label: item.name,
|
|
330
|
+
}))}
|
|
331
|
+
value={
|
|
332
|
+
typeof searchValue === "string" || typeof searchValue === "number"
|
|
333
|
+
? searchValue
|
|
334
|
+
: ""
|
|
335
|
+
}
|
|
336
|
+
onChange={(value) => handleFilterChange(col.key, value === "" ? undefined : value)}
|
|
337
|
+
onSearch={col.catalogOptions.onSearch}
|
|
338
|
+
isLoading={col.catalogOptions.loading}
|
|
339
|
+
size="sm"
|
|
340
|
+
placeholder="Buscar..."
|
|
341
|
+
clearable
|
|
342
|
+
className="w-full"
|
|
343
|
+
onBlur={() => { }}
|
|
344
|
+
/>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (col.filter === "date") {
|
|
349
|
+
const dateValue = filters[col.key];
|
|
350
|
+
return (
|
|
351
|
+
<ITDatePicker
|
|
352
|
+
name={`filter-${col.key}`}
|
|
353
|
+
value={dateValue instanceof Date ? dateValue : undefined}
|
|
354
|
+
onChange={(e) => handleFilterChange(col.key, e.target.value as Date)}
|
|
355
|
+
minDate={col.dateFilterOptions?.minDate}
|
|
356
|
+
maxDate={col.dateFilterOptions?.maxDate}
|
|
357
|
+
size="sm"
|
|
358
|
+
placeholder="dd/mm/aaaa"
|
|
359
|
+
className="w-full"
|
|
360
|
+
/>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (col.filter === "date-range") {
|
|
365
|
+
const rangeValue = filters[col.key];
|
|
366
|
+
return (
|
|
367
|
+
<ITDatePicker
|
|
368
|
+
name={`filter-${col.key}`}
|
|
369
|
+
range
|
|
370
|
+
value={Array.isArray(rangeValue) ? rangeValue : undefined}
|
|
371
|
+
onChange={(e) =>
|
|
372
|
+
handleFilterChange(col.key, e.target.value as [Date | null, Date | null])
|
|
373
|
+
}
|
|
374
|
+
minDate={col.dateFilterOptions?.minDate}
|
|
375
|
+
maxDate={col.dateFilterOptions?.maxDate}
|
|
376
|
+
size="sm"
|
|
377
|
+
placeholder="dd/mm/aaaa - dd/mm/aaaa"
|
|
378
|
+
className="w-full"
|
|
379
|
+
/>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
286
383
|
return (
|
|
287
384
|
<ITInput
|
|
288
385
|
name={`filter-${col.key}`}
|
|
@@ -494,22 +591,33 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
494
591
|
className={clsx(tableHeaderRow, "dark:text-slate-200")}
|
|
495
592
|
aria-rowindex={isTableVirtual ? 1 : undefined}
|
|
496
593
|
>
|
|
497
|
-
{columns.map((col) =>
|
|
594
|
+
{columns.map((col) => {
|
|
595
|
+
const pin = pinnedMeta[col.key];
|
|
596
|
+
const isPinned = pin != null;
|
|
597
|
+
return (
|
|
498
598
|
<th
|
|
499
599
|
key={col.key}
|
|
500
600
|
scope="col"
|
|
501
601
|
className={clsx(
|
|
502
602
|
tableHeaderCell(col.className, density),
|
|
503
603
|
col.align && tableAlignClasses[col.align],
|
|
504
|
-
|
|
604
|
+
isPinned && "sticky",
|
|
605
|
+
isStickyHeader && "sticky top-0 bg-secondary-50",
|
|
606
|
+
isPinned ? "z-[15]" : isStickyHeader && "z-10",
|
|
607
|
+
isPinned && (col.pinned === "left" ? "it-table-pinned-left" : "it-table-pinned-right")
|
|
505
608
|
)}
|
|
506
609
|
style={
|
|
507
|
-
col.minWidth != null || isStickyHeader
|
|
610
|
+
col.minWidth != null || isStickyHeader || isPinned
|
|
508
611
|
? {
|
|
509
612
|
...(col.minWidth != null ? { minWidth: `${col.minWidth}px` } : {}),
|
|
510
|
-
...(isStickyHeader
|
|
613
|
+
...(isStickyHeader || isPinned
|
|
511
614
|
? { backgroundColor: "var(--color-table-headerBg, #f8fafc)" }
|
|
512
615
|
: {}),
|
|
616
|
+
...(isPinned
|
|
617
|
+
? pin.side === "left"
|
|
618
|
+
? { left: pin.offset }
|
|
619
|
+
: { right: pin.offset }
|
|
620
|
+
: {}),
|
|
513
621
|
}
|
|
514
622
|
: undefined
|
|
515
623
|
}
|
|
@@ -535,7 +643,8 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
535
643
|
</div>
|
|
536
644
|
</div>
|
|
537
645
|
</th>
|
|
538
|
-
|
|
646
|
+
);
|
|
647
|
+
})}
|
|
539
648
|
</tr>
|
|
540
649
|
</thead>
|
|
541
650
|
<tbody className={clsx(tableBody, "dark:divide-slate-700/30")}>
|
|
@@ -568,14 +677,30 @@ export default function ITTable<T extends Record<string, unknown>>({
|
|
|
568
677
|
(typeof rawValue === "string" || typeof rawValue === "number")
|
|
569
678
|
? String(rawValue)
|
|
570
679
|
: undefined;
|
|
680
|
+
const pin = pinnedMeta[col.key];
|
|
681
|
+
const isPinned = pin != null;
|
|
571
682
|
return (
|
|
572
683
|
<td
|
|
573
684
|
key={`${absoluteIndex}-${col.key}`}
|
|
574
685
|
className={clsx(
|
|
575
686
|
tableCell(col.className, density),
|
|
576
|
-
col.align && tableAlignClasses[col.align]
|
|
687
|
+
col.align && tableAlignClasses[col.align],
|
|
688
|
+
isPinned && "sticky bg-[var(--color-table-rowBg,white)] group-hover:bg-[var(--color-table-rowHover,var(--color-secondary-50))]",
|
|
689
|
+
isPinned && (col.pinned === "left" ? "it-table-pinned-left" : "it-table-pinned-right"),
|
|
690
|
+
isPinned && "z-[1]"
|
|
577
691
|
)}
|
|
578
|
-
style={
|
|
692
|
+
style={
|
|
693
|
+
col.minWidth != null || isPinned
|
|
694
|
+
? {
|
|
695
|
+
...(col.minWidth != null ? { minWidth: `${col.minWidth}px` } : {}),
|
|
696
|
+
...(isPinned
|
|
697
|
+
? pin.side === "left"
|
|
698
|
+
? { left: pin.offset }
|
|
699
|
+
: { right: pin.offset }
|
|
700
|
+
: {}),
|
|
701
|
+
}
|
|
702
|
+
: undefined
|
|
703
|
+
}
|
|
579
704
|
title={truncateTitle}
|
|
580
705
|
>
|
|
581
706
|
{col.type === "actions" ? (
|
|
@@ -11,10 +11,10 @@ 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, plus layout fields: `width` (`number` px or CSS `string`), `minWidth` (px), `align` (`"left" \| "center" \| "right"`),
|
|
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"`), `truncate` (ellipsis + native `title`), and `pinned` (`"left" \| "right"`). |
|
|
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
|
-
| `externalFilters` | `
|
|
17
|
+
| `externalFilters` | `ColumnFilters` | `{}` | Filters managed outside the table (e.g., date pickers) merged with internal column filters. Values may be scalars, a `Date`, or a `[start, end]` tuple. |
|
|
18
18
|
| `loadingIndicator` | `ReactNode` | `<FaSpinner />` | Custom element shown while loading. |
|
|
19
19
|
| `fetchOnMount` | `boolean` | `true` | Whether to fetch data on initial mount. |
|
|
20
20
|
| `reloadTrigger` | `number \| string \| boolean` | — | Changing this value forces a re-fetch. Use after mutations. |
|
|
@@ -56,6 +56,15 @@ import { ITDataTable } from '@axzydev/axzy_ui_system';
|
|
|
56
56
|
variant="bordered"
|
|
57
57
|
/>
|
|
58
58
|
|
|
59
|
+
// Search + date filters (values arrive in params.filters)
|
|
60
|
+
<ITDataTable
|
|
61
|
+
columns={[
|
|
62
|
+
{ key: 'role', label: 'Role', type: 'string', filter: 'search', catalogOptions: { data: roles, onSearch: (q) => api.searchRoles(q) } },
|
|
63
|
+
{ key: 'hiredAt', label: 'Hired', type: 'date', filter: 'date-range', dateFilterOptions: { maxDate: new Date() } },
|
|
64
|
+
]}
|
|
65
|
+
fetchData={async (params) => api.fetchItems(params)}
|
|
66
|
+
/>
|
|
67
|
+
|
|
59
68
|
// No horizontal scroll: fixed layout + per-column widths + truncation
|
|
60
69
|
<ITDataTable
|
|
61
70
|
columns={[
|
|
@@ -67,11 +76,23 @@ import { ITDataTable } from '@axzydev/axzy_ui_system';
|
|
|
67
76
|
layout="fixed"
|
|
68
77
|
density="compact"
|
|
69
78
|
/>
|
|
79
|
+
|
|
80
|
+
// Pinned columns: freeze ID left and the last column right during horizontal scroll
|
|
81
|
+
<ITDataTable
|
|
82
|
+
columns={[
|
|
83
|
+
{ key: 'id', label: 'ID', type: 'number', width: 72, pinned: 'left' },
|
|
84
|
+
{ key: 'name', label: 'Name', type: 'string', width: 240, truncate: true },
|
|
85
|
+
{ key: 'active', label: 'Active', type: 'boolean', width: 120, pinned: 'right' },
|
|
86
|
+
]}
|
|
87
|
+
fetchData={async (params) => api.fetchItems(params)}
|
|
88
|
+
layout="fixed"
|
|
89
|
+
density="compact"
|
|
90
|
+
/>
|
|
70
91
|
```
|
|
71
92
|
|
|
72
93
|
## Notes
|
|
73
94
|
|
|
74
|
-
- Column filters support `"catalog"` (
|
|
95
|
+
- Column filters support `true` (text/number input, or boolean tri-state toggle for `type: "boolean"`), `"catalog"` (`ITSelect`), `"search"` (`ITSearchSelect` typeahead; add `catalogOptions.onSearch` for server-side search), and `"date"` / `"date-range"` (`ITDatePicker`, bounded by `dateFilterOptions`). In `fetchData(params)`, `params.filters[key]` is a `Date` for `"date"`, a `[start, end]` tuple for `"date-range"`, and the scalar/id otherwise.
|
|
75
96
|
- The `externalFilters` prop allows integrating external filter controls like date pickers without building filter UI into the table itself.
|
|
76
97
|
- Debounce prevents excessive API calls while the user types in filter inputs.
|
|
77
98
|
- A loading skeleton overlay covers the table body while fetching, keeping the header visible.
|
|
@@ -79,6 +100,7 @@ import { ITDataTable } from '@axzydev/axzy_ui_system';
|
|
|
79
100
|
- `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
101
|
- `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
102
|
- **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`.
|
|
103
|
+
- **Pinned columns:** set `columns[].pinned: "left" | "right"` to freeze a column to that edge during horizontal scroll (`position: sticky`; the wrapper needs no `relative`). Omitting `pinned` renders exactly as before. Sticky offsets accumulate from each preceding pinned column's pixel `width` / `minWidth`; a non-px width contributes `minWidth ?? 0`, so give every pinned column of a side a pixel `width`. The pinned cell gets an opaque background (`--color-table-rowBg`, row hover `--color-table-rowHover`) and a 1px separator shadow on its frozen edge. Z-layering: pinned body cell `z-[1]` < sticky header `z-10` < pinned header `z-[15]` < the loading overlay `z-20`; it composes with `stickyHeader` and `virtualized`. One pinned column per side is the supported contract. Known limitation: with `variant="striped"` the opaque pinned background covers that column's stripe tint.
|
|
82
104
|
- 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
105
|
- `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
106
|
- **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.
|