@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
|
@@ -142,6 +142,39 @@ export const Default: Story = {
|
|
|
142
142
|
},
|
|
143
143
|
};
|
|
144
144
|
|
|
145
|
+
// Column widths keep the fixed layout from forcing horizontal scroll.
|
|
146
|
+
const fixedColumns = columns.map((col, i) => ({
|
|
147
|
+
...col,
|
|
148
|
+
width: [64, '28%', '34%', '22%', '16%'][i],
|
|
149
|
+
truncate: col.type === 'string' || col.type === 'catalog',
|
|
150
|
+
})) as any;
|
|
151
|
+
|
|
152
|
+
export const FixedLayoutCompact: Story = {
|
|
153
|
+
args: {
|
|
154
|
+
title: 'Compact fixed layout',
|
|
155
|
+
columns: fixedColumns,
|
|
156
|
+
fetchData: mockFetchItems,
|
|
157
|
+
defaultItemsPerPage: 10,
|
|
158
|
+
layout: 'fixed',
|
|
159
|
+
density: 'compact',
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const VirtualizedLargeDataset: Story = {
|
|
164
|
+
args: {
|
|
165
|
+
title: 'Virtualized · 100 rows',
|
|
166
|
+
columns: fixedColumns,
|
|
167
|
+
fetchData: mockFetchItems,
|
|
168
|
+
defaultItemsPerPage: 100,
|
|
169
|
+
itemsPerPageOptions: [10, 50, 100],
|
|
170
|
+
virtualized: true,
|
|
171
|
+
stickyHeader: true,
|
|
172
|
+
layout: 'fixed',
|
|
173
|
+
density: 'compact',
|
|
174
|
+
virtualizedMaxHeight: 360,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
145
178
|
export const RemoteWithExternalFilters: Story = {
|
|
146
179
|
render: () => {
|
|
147
180
|
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
|
|
@@ -200,6 +233,16 @@ export const RemoteWithExternalFilters: Story = {
|
|
|
200
233
|
}
|
|
201
234
|
};
|
|
202
235
|
|
|
236
|
+
export const WithRowClick: Story = {
|
|
237
|
+
args: {
|
|
238
|
+
title: 'Filas clickeables',
|
|
239
|
+
columns,
|
|
240
|
+
fetchData: mockFetchItems,
|
|
241
|
+
defaultItemsPerPage: 10,
|
|
242
|
+
onRowClick: (row: any) => console.log('Row clicked:', row),
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
203
246
|
export const SkeletonLoading: Story = {
|
|
204
247
|
args: {
|
|
205
248
|
title: 'Cargando Datos...',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from "react";
|
|
2
2
|
import { Column } from "@/components/molecules/table/table.props";
|
|
3
|
+
import { TableDensity } from "@/types/table.types";
|
|
3
4
|
|
|
4
5
|
/** Parameters passed to `fetchData` every time pagination, filters, or sorting change. */
|
|
5
6
|
export interface ITDataTableFetchParams {
|
|
@@ -91,4 +92,67 @@ export interface ITDataTableProps<T extends Record<string, unknown>> {
|
|
|
91
92
|
showVerticalBorder?: boolean;
|
|
92
93
|
/** Custom class for vertical borders (overrides the default subtle gray). */
|
|
93
94
|
verticalBorderClassname?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Callback fired when a row (table view) or card (cards view) is activated,
|
|
97
|
+
* receiving the row item and the originating event. The event is a
|
|
98
|
+
* `MouseEvent` for pointer interaction and a `KeyboardEvent` for Enter/Space
|
|
99
|
+
* activation. Not fired when the interaction starts inside an interactive
|
|
100
|
+
* child (button, link, input, or any `[data-row-click-ignore]` element).
|
|
101
|
+
*/
|
|
102
|
+
onRowClick?: (
|
|
103
|
+
row: T,
|
|
104
|
+
event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>
|
|
105
|
+
) => void;
|
|
106
|
+
/**
|
|
107
|
+
* Table layout algorithm. `"fixed"` respects column `width` values and drops
|
|
108
|
+
* the `min-w-max` / `min-w-[150px]` floors so content no longer forces
|
|
109
|
+
* horizontal scrolling; `"auto"` keeps the content-driven layout.
|
|
110
|
+
* @default "auto"
|
|
111
|
+
*/
|
|
112
|
+
layout?: "auto" | "fixed";
|
|
113
|
+
/**
|
|
114
|
+
* Cell padding and row-height preset. Independent of `size`, which only
|
|
115
|
+
* controls font-size: `"compact"` | `"normal"` | `"comfortable"`.
|
|
116
|
+
* @default "normal"
|
|
117
|
+
*/
|
|
118
|
+
density?: TableDensity;
|
|
119
|
+
/**
|
|
120
|
+
* Container width (in px) below which the table automatically falls back to
|
|
121
|
+
* the cards view. `0` disables the fallback.
|
|
122
|
+
* @default 0
|
|
123
|
+
*/
|
|
124
|
+
autoCardBreakpoint?: number;
|
|
125
|
+
/**
|
|
126
|
+
* Enables row virtualization for the table view: only the visible window of
|
|
127
|
+
* rows is rendered, with spacer rows preserving the full scroll height.
|
|
128
|
+
* The cards view is never virtualized.
|
|
129
|
+
* @default false
|
|
130
|
+
*/
|
|
131
|
+
virtualized?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Max height (px) of the virtualized table's scroll container.
|
|
134
|
+
* Only used when `virtualized` is true.
|
|
135
|
+
* @default 400
|
|
136
|
+
*/
|
|
137
|
+
virtualizedMaxHeight?: number;
|
|
138
|
+
/**
|
|
139
|
+
* Assumed uniform row height (px) used by the virtualizer. Defaults to
|
|
140
|
+
* `getRowHeight(size, density)`, which is size-aware: the `size="md"`
|
|
141
|
+
* baseline is 33 / 45 / 53 (compact / normal / comfortable) and `size`
|
|
142
|
+
* shifts it by the font-size line box (sm 16px / md 20px / lg 28px).
|
|
143
|
+
* Only used when `virtualized` is true.
|
|
144
|
+
*/
|
|
145
|
+
rowHeight?: number;
|
|
146
|
+
/**
|
|
147
|
+
* Number of extra rows rendered above and below the viewport to avoid blank
|
|
148
|
+
* gaps while scrolling. Only used when `virtualized` is true.
|
|
149
|
+
* @default 5
|
|
150
|
+
*/
|
|
151
|
+
overscan?: number;
|
|
152
|
+
/**
|
|
153
|
+
* Makes the header cells sticky while the virtualized body scrolls. Only
|
|
154
|
+
* effective when `virtualized` is true.
|
|
155
|
+
* @default false
|
|
156
|
+
*/
|
|
157
|
+
stickyHeader?: boolean;
|
|
94
158
|
}
|
|
@@ -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, useEffect, useCallback, useRef } from "react";
|
|
5
7
|
import { FaCheck, FaSpinner, FaTimes, FaTable, FaThLarge } from "react-icons/fa";
|
|
@@ -8,12 +10,13 @@ import ITInput from "@/components/atoms/input/input";
|
|
|
8
10
|
import ITPagination from "@/components/molecules/pagination/pagination";
|
|
9
11
|
import ITSelect from "@/components/molecules/select/select";
|
|
10
12
|
import { Column } from "@/components/molecules/table/table.props";
|
|
11
|
-
import { formatCurrencyMX } from "@/utils/table.utils";
|
|
13
|
+
import { formatCurrencyMX, isInteractiveTarget } from "@/utils/table.utils";
|
|
12
14
|
import { ITDataTableProps } from "./dataTable.props";
|
|
13
15
|
import ITText from "@/components/atoms/text/text";
|
|
14
16
|
import {
|
|
15
17
|
tableActionsCell,
|
|
16
18
|
tableBody,
|
|
19
|
+
tableCardClickable,
|
|
17
20
|
tableCell,
|
|
18
21
|
tableCellText,
|
|
19
22
|
tableContainer,
|
|
@@ -21,6 +24,7 @@ import {
|
|
|
21
24
|
tableHeaderCell,
|
|
22
25
|
tableHeaderRow,
|
|
23
26
|
tableRow,
|
|
27
|
+
tableRowClickable,
|
|
24
28
|
} from "@/utils/styles";
|
|
25
29
|
|
|
26
30
|
const getNestedValue = (obj: unknown, path: string) => {
|
|
@@ -69,12 +73,39 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
69
73
|
defaultView,
|
|
70
74
|
showVerticalBorder = true,
|
|
71
75
|
verticalBorderClassname,
|
|
76
|
+
onRowClick,
|
|
77
|
+
layout = "auto",
|
|
78
|
+
density = "normal",
|
|
79
|
+
autoCardBreakpoint = 0,
|
|
80
|
+
virtualized = false,
|
|
81
|
+
virtualizedMaxHeight = 400,
|
|
82
|
+
rowHeight,
|
|
83
|
+
overscan = 5,
|
|
84
|
+
stickyHeader = false,
|
|
72
85
|
}: ITDataTableProps<T>) {
|
|
73
86
|
const [viewMode, setViewMode] = useState<"table" | "cards">(defaultView || "table");
|
|
87
|
+
const { ref: rootRef, width: containerWidth } = useElementSize<HTMLDivElement>();
|
|
74
88
|
const [data, setData] = useState<T[]>([]);
|
|
75
89
|
const [totalItems, setTotalItems] = useState(0);
|
|
76
90
|
const [isLoading, setIsLoading] = useState(fetchOnMount);
|
|
77
91
|
|
|
92
|
+
const isFixed = layout === "fixed";
|
|
93
|
+
const forcedCards =
|
|
94
|
+
autoCardBreakpoint > 0 && containerWidth > 0 && containerWidth < autoCardBreakpoint;
|
|
95
|
+
const effectiveView: "table" | "cards" = forcedCards ? "cards" : viewMode;
|
|
96
|
+
const hasColumnWidths = columns.some((col) => col.width != null);
|
|
97
|
+
const tableLayoutClass = isFixed
|
|
98
|
+
? "w-full text-sm text-left text-secondary-600 transition-opacity duration-300"
|
|
99
|
+
: "min-w-max w-full text-sm text-left text-secondary-600 transition-opacity duration-300";
|
|
100
|
+
|
|
101
|
+
const isTableVirtual = virtualized && effectiveView === "table";
|
|
102
|
+
const isStickyHeader = stickyHeader && isTableVirtual;
|
|
103
|
+
const resolvedRowHeight = rowHeight ?? getRowHeight(size, density);
|
|
104
|
+
const columnCount = columns.length;
|
|
105
|
+
const scrollOuterClass = isTableVirtual
|
|
106
|
+
? "relative min-h-[200px]"
|
|
107
|
+
: "overflow-x-auto relative min-h-[200px]";
|
|
108
|
+
|
|
78
109
|
const {
|
|
79
110
|
currentPage,
|
|
80
111
|
itemsPerPage,
|
|
@@ -86,6 +117,22 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
86
117
|
handleItemsPerPageChange,
|
|
87
118
|
} = useTableState({ defaultItemsPerPage });
|
|
88
119
|
|
|
120
|
+
const {
|
|
121
|
+
scrollRef,
|
|
122
|
+
startIndex,
|
|
123
|
+
endIndex,
|
|
124
|
+
topSpacerHeight,
|
|
125
|
+
bottomSpacerHeight,
|
|
126
|
+
} = useVirtualRows({
|
|
127
|
+
count: data.length,
|
|
128
|
+
rowHeight: resolvedRowHeight,
|
|
129
|
+
overscan,
|
|
130
|
+
enabled: isTableVirtual,
|
|
131
|
+
resetKey: `${currentPage}-${itemsPerPage}`,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const windowData = isTableVirtual ? data.slice(startIndex, endIndex + 1) : data;
|
|
135
|
+
|
|
89
136
|
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
90
137
|
const hasFetchedRef = useRef(false);
|
|
91
138
|
const fetchDataRef = useRef(fetchData);
|
|
@@ -270,13 +317,181 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
270
317
|
|
|
271
318
|
const segCtrlClass = (mode: "table" | "cards") =>
|
|
272
319
|
`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${
|
|
273
|
-
|
|
320
|
+
effectiveView === mode
|
|
274
321
|
? "bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-sm border border-slate-200/50 dark:border-slate-700"
|
|
275
322
|
: "text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
|
|
276
323
|
}`;
|
|
277
324
|
|
|
325
|
+
const handleRowClick = (row: T) => (event: React.MouseEvent<HTMLElement>) => {
|
|
326
|
+
if (!onRowClick || isLoading || isInteractiveTarget(event.target, event.currentTarget)) return;
|
|
327
|
+
onRowClick(row, event);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const handleRowKeyDown = (row: T) => (event: React.KeyboardEvent<HTMLElement>) => {
|
|
331
|
+
if (!onRowClick || isLoading) return;
|
|
332
|
+
if (event.target !== event.currentTarget) return;
|
|
333
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
334
|
+
event.preventDefault();
|
|
335
|
+
onRowClick(row, event);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const tableEl = (
|
|
339
|
+
<table
|
|
340
|
+
className={clsx(
|
|
341
|
+
tableLayoutClass,
|
|
342
|
+
showVerticalBorder && "[&_th]:border-r [&_th:last-child]:border-r-0 [&_td]:border-r [&_td:last-child]:border-r-0",
|
|
343
|
+
showVerticalBorder && (verticalBorderClassname || "[&_th]:border-slate-100 dark:[&_th]:border-slate-700/30 [&_td]:border-slate-100 dark:[&_td]:border-slate-700/30"),
|
|
344
|
+
isLoading ? "opacity-50" : "opacity-100",
|
|
345
|
+
variantStyles[variant],
|
|
346
|
+
sizeStyles[size],
|
|
347
|
+
className
|
|
348
|
+
)}
|
|
349
|
+
style={isFixed ? { tableLayout: "fixed" } : undefined}
|
|
350
|
+
aria-rowcount={isTableVirtual ? data.length + 1 : undefined}
|
|
351
|
+
>
|
|
352
|
+
{hasColumnWidths && (
|
|
353
|
+
<colgroup>
|
|
354
|
+
{columns.map((col) => (
|
|
355
|
+
<col
|
|
356
|
+
key={col.key}
|
|
357
|
+
style={
|
|
358
|
+
col.width != null
|
|
359
|
+
? { width: typeof col.width === "number" ? `${col.width}px` : col.width }
|
|
360
|
+
: undefined
|
|
361
|
+
}
|
|
362
|
+
/>
|
|
363
|
+
))}
|
|
364
|
+
</colgroup>
|
|
365
|
+
)}
|
|
366
|
+
<thead>
|
|
367
|
+
<tr
|
|
368
|
+
className={clsx(tableHeaderRow, "dark:text-slate-200")}
|
|
369
|
+
aria-rowindex={isTableVirtual ? 1 : undefined}
|
|
370
|
+
>
|
|
371
|
+
{columns.map((col) => (
|
|
372
|
+
<th
|
|
373
|
+
key={col.key}
|
|
374
|
+
scope="col"
|
|
375
|
+
className={clsx(
|
|
376
|
+
tableHeaderCell(col.className, density),
|
|
377
|
+
col.align && tableAlignClasses[col.align],
|
|
378
|
+
isStickyHeader && "sticky top-0 z-10 bg-secondary-50"
|
|
379
|
+
)}
|
|
380
|
+
style={
|
|
381
|
+
col.minWidth != null || isStickyHeader
|
|
382
|
+
? {
|
|
383
|
+
...(col.minWidth != null ? { minWidth: `${col.minWidth}px` } : {}),
|
|
384
|
+
...(isStickyHeader
|
|
385
|
+
? { backgroundColor: "var(--color-table-headerBg, #f8fafc)" }
|
|
386
|
+
: {}),
|
|
387
|
+
}
|
|
388
|
+
: undefined
|
|
389
|
+
}
|
|
390
|
+
>
|
|
391
|
+
<div className={isFixed ? "flex flex-col gap-3" : "flex flex-col gap-3 min-w-[150px]"}>
|
|
392
|
+
<div className="flex items-center justify-between gap-2">
|
|
393
|
+
<ITText as="span" className="text-slate-900 dark:text-white font-bold">{col.label}</ITText>
|
|
394
|
+
{col.sortable && col.type !== "actions" && (
|
|
395
|
+
<button
|
|
396
|
+
onClick={() => handleSort(col.key)}
|
|
397
|
+
disabled={isLoading}
|
|
398
|
+
className={`p-1 rounded-md transition-colors ${
|
|
399
|
+
sortConfig?.key === col.key
|
|
400
|
+
? "bg-secondary-200 text-secondary-900"
|
|
401
|
+
: "hover:bg-secondary-200 text-secondary-400 hover:text-secondary-700"
|
|
402
|
+
} disabled:opacity-50`}
|
|
403
|
+
title={`Ordenar por ${col.label}`}
|
|
404
|
+
>
|
|
405
|
+
<MdOutlineSwapVert className="w-4 h-4" aria-hidden="true" />
|
|
406
|
+
</button>
|
|
407
|
+
)}
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<div className="w-full">{col.filter ? renderFilterInput(col) : null}</div>
|
|
411
|
+
</div>
|
|
412
|
+
</th>
|
|
413
|
+
))}
|
|
414
|
+
</tr>
|
|
415
|
+
</thead>
|
|
416
|
+
<tbody className={clsx(tableBody, "dark:divide-slate-700/30")}>
|
|
417
|
+
{data.length > 0 ? (
|
|
418
|
+
<>
|
|
419
|
+
{isTableVirtual && topSpacerHeight > 0 && (
|
|
420
|
+
<tr aria-hidden="true" style={{ height: topSpacerHeight }}>
|
|
421
|
+
<td colSpan={columnCount} style={{ padding: 0, border: 0 }} />
|
|
422
|
+
</tr>
|
|
423
|
+
)}
|
|
424
|
+
{windowData.map((row, windowIndex) => {
|
|
425
|
+
const absoluteIndex = isTableVirtual ? startIndex + windowIndex : windowIndex;
|
|
426
|
+
return (
|
|
427
|
+
<tr
|
|
428
|
+
key={absoluteIndex}
|
|
429
|
+
aria-rowindex={isTableVirtual ? absoluteIndex + 2 : undefined}
|
|
430
|
+
className={clsx(
|
|
431
|
+
tableRow,
|
|
432
|
+
variant === "striped" && absoluteIndex % 2 === 0 && "bg-secondary-50/40 dark:bg-slate-800/20",
|
|
433
|
+
onRowClick && !isLoading && tableRowClickable
|
|
434
|
+
)}
|
|
435
|
+
tabIndex={onRowClick && !isLoading ? 0 : undefined}
|
|
436
|
+
onClick={onRowClick && !isLoading ? handleRowClick(row) : undefined}
|
|
437
|
+
onKeyDown={onRowClick && !isLoading ? handleRowKeyDown(row) : undefined}
|
|
438
|
+
>
|
|
439
|
+
{columns.map((col) => {
|
|
440
|
+
const rawValue = getNestedValue(row, col.key);
|
|
441
|
+
const truncateTitle =
|
|
442
|
+
col.truncate &&
|
|
443
|
+
(typeof rawValue === "string" || typeof rawValue === "number")
|
|
444
|
+
? String(rawValue)
|
|
445
|
+
: undefined;
|
|
446
|
+
return (
|
|
447
|
+
<td
|
|
448
|
+
key={`${absoluteIndex}-${col.key}`}
|
|
449
|
+
className={clsx(
|
|
450
|
+
tableCell(col.className, density),
|
|
451
|
+
col.align && tableAlignClasses[col.align]
|
|
452
|
+
)}
|
|
453
|
+
style={col.minWidth != null ? { minWidth: `${col.minWidth}px` } : undefined}
|
|
454
|
+
title={truncateTitle}
|
|
455
|
+
>
|
|
456
|
+
{col.type === "actions" ? (
|
|
457
|
+
<div className={tableActionsCell}>
|
|
458
|
+
{renderCellContent(col, row) as React.ReactNode}
|
|
459
|
+
</div>
|
|
460
|
+
) : (
|
|
461
|
+
<div className={clsx(tableCellText, col.truncate && "truncate")}>
|
|
462
|
+
{renderCellContent(col, row) as React.ReactNode}
|
|
463
|
+
</div>
|
|
464
|
+
)}
|
|
465
|
+
</td>
|
|
466
|
+
);
|
|
467
|
+
})}
|
|
468
|
+
</tr>
|
|
469
|
+
);
|
|
470
|
+
})}
|
|
471
|
+
{isTableVirtual && bottomSpacerHeight > 0 && (
|
|
472
|
+
<tr aria-hidden="true" style={{ height: bottomSpacerHeight }}>
|
|
473
|
+
<td colSpan={columnCount} style={{ padding: 0, border: 0 }} />
|
|
474
|
+
</tr>
|
|
475
|
+
)}
|
|
476
|
+
</>
|
|
477
|
+
) : (
|
|
478
|
+
<tr>
|
|
479
|
+
<td colSpan={columns.length} className="px-6 py-12 text-center">
|
|
480
|
+
{!isLoading && (
|
|
481
|
+
<div className={tableEmptyContent}>
|
|
482
|
+
<ITText as="span" className="text-lg">No se encontraron resultados</ITText>
|
|
483
|
+
<ITText as="span" className="text-sm mt-1">Intenta ajustar los filtros</ITText>
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
</td>
|
|
487
|
+
</tr>
|
|
488
|
+
)}
|
|
489
|
+
</tbody>
|
|
490
|
+
</table>
|
|
491
|
+
);
|
|
492
|
+
|
|
278
493
|
return (
|
|
279
|
-
<div className={clsx("space-y-4 w-full relative", containerClassName)}>
|
|
494
|
+
<div ref={rootRef} className={clsx("space-y-4 w-full relative", containerClassName)}>
|
|
280
495
|
<div className={tableContainer} style={{ backgroundColor: 'var(--color-table-rowBg, #ffffff)' }}>
|
|
281
496
|
{title && (
|
|
282
497
|
<div className="px-6 py-5 flex items-center justify-between" style={{ backgroundColor: 'var(--color-table-rowBg, #ffffff)' }}>
|
|
@@ -288,7 +503,7 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
288
503
|
</div>
|
|
289
504
|
)}
|
|
290
505
|
<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">
|
|
291
|
-
<button onClick={() => setViewMode("table")} className={segCtrlClass("table")}>
|
|
506
|
+
<button onClick={() => setViewMode("table")} disabled={forcedCards} className={segCtrlClass("table")}>
|
|
292
507
|
<FaTable size={11} />
|
|
293
508
|
Table
|
|
294
509
|
</button>
|
|
@@ -304,7 +519,7 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
304
519
|
{!title && (
|
|
305
520
|
<div className="flex justify-end px-4 pt-3">
|
|
306
521
|
<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">
|
|
307
|
-
<button onClick={() => setViewMode("table")} className={segCtrlClass("table")}>
|
|
522
|
+
<button onClick={() => setViewMode("table")} disabled={forcedCards} className={segCtrlClass("table")}>
|
|
308
523
|
<FaTable size={11} />
|
|
309
524
|
Table
|
|
310
525
|
</button>
|
|
@@ -316,7 +531,7 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
316
531
|
</div>
|
|
317
532
|
)}
|
|
318
533
|
|
|
319
|
-
<div className=
|
|
534
|
+
<div className={scrollOuterClass}>
|
|
320
535
|
{isLoading && (
|
|
321
536
|
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/40 backdrop-blur-[2px] transition-all duration-300">
|
|
322
537
|
<div className="flex flex-col items-center gap-3 p-6 rounded-2xl shadow-xl border border-secondary-100 animate-in fade-in zoom-in duration-300" style={{ backgroundColor: 'var(--color-table-rowBg, #ffffff)' }}>
|
|
@@ -330,11 +545,18 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
330
545
|
</div>
|
|
331
546
|
)}
|
|
332
547
|
|
|
333
|
-
{
|
|
548
|
+
{effectiveView === "cards" ? (
|
|
334
549
|
<div className="p-4 space-y-3 max-h-[400px] overflow-y-auto">
|
|
335
550
|
{data.length > 0 ? (
|
|
336
551
|
data.map((row, i) => (
|
|
337
|
-
<div
|
|
552
|
+
<div
|
|
553
|
+
key={i}
|
|
554
|
+
className={clsx(onRowClick && !isLoading && tableCardClickable)}
|
|
555
|
+
role={onRowClick && !isLoading ? "button" : undefined}
|
|
556
|
+
tabIndex={onRowClick && !isLoading ? 0 : undefined}
|
|
557
|
+
onClick={onRowClick && !isLoading ? handleRowClick(row) : undefined}
|
|
558
|
+
onKeyDown={onRowClick && !isLoading ? handleRowKeyDown(row) : undefined}
|
|
559
|
+
>
|
|
338
560
|
{renderCard ? renderCard(row) : renderDefaultCard(row)}
|
|
339
561
|
</div>
|
|
340
562
|
))
|
|
@@ -347,80 +569,16 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
347
569
|
)
|
|
348
570
|
)}
|
|
349
571
|
</div>
|
|
350
|
-
) : (
|
|
351
|
-
<
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
showVerticalBorder && (verticalBorderClassname || "[&_th]:border-slate-100 dark:[&_th]:border-slate-700/30 [&_td]:border-slate-100 dark:[&_td]:border-slate-700/30"),
|
|
356
|
-
isLoading ? "opacity-50" : "opacity-100",
|
|
357
|
-
variantStyles[variant],
|
|
358
|
-
sizeStyles[size],
|
|
359
|
-
className
|
|
360
|
-
)}
|
|
572
|
+
) : isTableVirtual ? (
|
|
573
|
+
<div
|
|
574
|
+
ref={scrollRef}
|
|
575
|
+
className="overflow-x-auto overflow-y-auto"
|
|
576
|
+
style={{ maxHeight: virtualizedMaxHeight }}
|
|
361
577
|
>
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
<div className="flex flex-col gap-3 min-w-[150px]">
|
|
367
|
-
<div className="flex items-center justify-between gap-2">
|
|
368
|
-
<ITText as="span" className="text-slate-900 dark:text-white font-bold">{col.label}</ITText>
|
|
369
|
-
{col.sortable && col.type !== "actions" && (
|
|
370
|
-
<button
|
|
371
|
-
onClick={() => handleSort(col.key)}
|
|
372
|
-
disabled={isLoading}
|
|
373
|
-
className={`p-1 rounded-md transition-colors ${
|
|
374
|
-
sortConfig?.key === col.key
|
|
375
|
-
? "bg-secondary-200 text-secondary-900"
|
|
376
|
-
: "hover:bg-secondary-200 text-secondary-400 hover:text-secondary-700"
|
|
377
|
-
} disabled:opacity-50`}
|
|
378
|
-
title={`Ordenar por ${col.label}`}
|
|
379
|
-
>
|
|
380
|
-
<MdOutlineSwapVert className="w-4 h-4" aria-hidden="true" />
|
|
381
|
-
</button>
|
|
382
|
-
)}
|
|
383
|
-
</div>
|
|
384
|
-
|
|
385
|
-
<div className="w-full">{col.filter ? renderFilterInput(col) : null}</div>
|
|
386
|
-
</div>
|
|
387
|
-
</th>
|
|
388
|
-
))}
|
|
389
|
-
</tr>
|
|
390
|
-
</thead>
|
|
391
|
-
<tbody className={clsx(tableBody, "dark:divide-slate-700/30")}>
|
|
392
|
-
{data.length > 0 ? (
|
|
393
|
-
data.map((row, rowIndex) => (
|
|
394
|
-
<tr key={rowIndex} className={clsx(tableRow, variant === "striped" && "odd:bg-secondary-50/40 dark:odd:bg-slate-800/20")}>
|
|
395
|
-
{columns.map((col) => (
|
|
396
|
-
<td key={`${rowIndex}-${col.key}`} className={tableCell(col.className)}>
|
|
397
|
-
{col.type === "actions" ? (
|
|
398
|
-
<div className={tableActionsCell}>
|
|
399
|
-
{renderCellContent(col, row) as React.ReactNode}
|
|
400
|
-
</div>
|
|
401
|
-
) : (
|
|
402
|
-
<div className={tableCellText}>
|
|
403
|
-
{renderCellContent(col, row) as React.ReactNode}
|
|
404
|
-
</div>
|
|
405
|
-
)}
|
|
406
|
-
</td>
|
|
407
|
-
))}
|
|
408
|
-
</tr>
|
|
409
|
-
))
|
|
410
|
-
) : (
|
|
411
|
-
<tr>
|
|
412
|
-
<td colSpan={columns.length} className="px-6 py-12 text-center">
|
|
413
|
-
{!isLoading && (
|
|
414
|
-
<div className={tableEmptyContent}>
|
|
415
|
-
<ITText as="span" className="text-lg">No se encontraron resultados</ITText>
|
|
416
|
-
<ITText as="span" className="text-sm mt-1">Intenta ajustar los filtros</ITText>
|
|
417
|
-
</div>
|
|
418
|
-
)}
|
|
419
|
-
</td>
|
|
420
|
-
</tr>
|
|
421
|
-
)}
|
|
422
|
-
</tbody>
|
|
423
|
-
</table>
|
|
578
|
+
{tableEl}
|
|
579
|
+
</div>
|
|
580
|
+
) : (
|
|
581
|
+
tableEl
|
|
424
582
|
)}
|
|
425
583
|
</div>
|
|
426
584
|
</div>
|
|
@@ -19,6 +19,7 @@ Vertical navigation sidebar with collapsible submenus, tooltips when collapsed,
|
|
|
19
19
|
| `onSubItemClick` | `(subitem: ITNavigationSubItem) => void` | — | Callback when a sub-navigation item is clicked. |
|
|
20
20
|
| `subitemConnector` | `"dot" \| "\|" \| "none"` | `"dot"` | Visual connector style for sub-items. |
|
|
21
21
|
| `className` | `string` | — | Additional CSS classes on the sidebar `<aside>`. |
|
|
22
|
+
| `header` | `ReactNode` | — | Optional content rendered above the navigation list (e.g. a wordmark). Hidden while the sidebar is collapsed, and independent of the collapse toggle button, which is always visible in both states. |
|
|
22
23
|
|
|
23
24
|
### ITNavigationItem
|
|
24
25
|
|
|
@@ -71,6 +72,9 @@ const items = [
|
|
|
71
72
|
|
|
72
73
|
## Notes
|
|
73
74
|
|
|
75
|
+
- The button lives above the scrollable `<nav>` (never inside it), so it stays in the tab order and visible in both states; it is centered in the 88px rail when collapsed and right-aligned when expanded.
|
|
76
|
+
- The toggle is a real `<button type="button">` with `aria-expanded`, `aria-controls` pointing at the `<nav>` it controls, and a state-dependent `aria-label`/`title` ("Expandir/Colapsar barra lateral"). Its chevron rotates 180° when the sidebar is expanded, so one icon signals both directions.
|
|
77
|
+
- There is no collapse toggle button: the collapsed rail expands and collapses automatically on pointer hover.
|
|
74
78
|
- Collapsed mode shrinks to icon-only; tooltips show labels on hover with glassmorphism style.
|
|
75
79
|
- Submenus expand/collapse with smooth transitions when a parent item is clicked.
|
|
76
80
|
- Active item highlighting uses a colored left-border accent and subtle background tint.
|
|
@@ -78,6 +82,7 @@ const items = [
|
|
|
78
82
|
- `subitemConnector` styles the visual link between parent and sub-items: `dot` (default), `|` (vertical bar), or `none`.
|
|
79
83
|
- `visibleOnMobile` forces sidebar visibility on smaller screens for responsive layouts.
|
|
80
84
|
- Themed via CSS variables for consistent colors, shadows, and glassmorphism effects across light and dark modes.
|
|
85
|
+
- Accessible by default: the `<nav>` is labelled and carries the `id` referenced by the toggle's `aria-controls`, the toggle exposes `aria-expanded` plus a visible `focus-visible` ring, group toggles expose `aria-expanded`/`aria-controls` and respond to Enter/Space, submenus are labelled `role="group"`, and the active sub-item carries `aria-current="page"`.
|
|
81
86
|
|
|
82
87
|
## Stories
|
|
83
88
|
|
|
@@ -46,5 +46,10 @@ export interface ITSidebarProps {
|
|
|
46
46
|
subitemConnector?: 'dot' | '|' | 'none';
|
|
47
47
|
/** Additional CSS classes on the sidebar `<aside>`. */
|
|
48
48
|
className?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Optional content rendered above the navigation list (e.g. a wordmark).
|
|
51
|
+
* Hidden while the sidebar is collapsed, so it only shows on hover.
|
|
52
|
+
*/
|
|
53
|
+
header?: React.ReactNode;
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -71,7 +71,7 @@ export const Default: Story = {
|
|
|
71
71
|
render: (args) => (
|
|
72
72
|
<div className="h-screen bg-gray-50 flex">
|
|
73
73
|
<ITSidebar {...args} />
|
|
74
|
-
<div className="flex-1 p-8 text-zinc-500 font-medium">Contenido principal simulado.
|
|
74
|
+
<div className="flex-1 p-8 text-zinc-500 font-medium">Contenido principal simulado. Pasa el cursor por los íconos para ver el efecto de glassmorphism en los tooltips flotantes.</div>
|
|
75
75
|
</div>
|
|
76
76
|
),
|
|
77
77
|
};
|