@axzydev/axzy_ui_system 1.3.1 → 1.3.3
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 +957 -554
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +191 -9
- package/dist/index.d.ts +191 -9
- package/dist/index.js +908 -505
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/App.tsx +150 -154
- 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 +178 -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 +241 -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/components/theme-provider/themePresets.ts +6 -0
- package/src/components/theme-provider/themeProvider.doc.mdx +1 -0
- package/src/components/theme-provider/themeProvider.props.ts +2 -0
- package/src/components/theme-provider/themeProvider.tsx +13 -2
- package/src/dev.css +58 -1
- package/src/hooks/useElementSize.ts +46 -0
- package/src/hooks/useVirtualRows.ts +123 -0
- package/src/index.ts +2 -0
- package/src/sandbox/LandingShowcase.tsx +654 -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/PageShowcases.tsx +31 -0
- package/src/showcases/StructureShowcases.tsx +42 -4
- package/src/theme/theme.types.ts +2 -0
- package/src/types/table.types.ts +48 -0
- package/src/utils/styles.ts +12 -5
- package/src/utils/table.utils.ts +31 -0
|
@@ -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,14 @@ 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,
|
|
20
|
+
tableCardHover,
|
|
17
21
|
tableCell,
|
|
18
22
|
tableCellText,
|
|
19
23
|
tableContainer,
|
|
@@ -21,6 +25,7 @@ import {
|
|
|
21
25
|
tableHeaderCell,
|
|
22
26
|
tableHeaderRow,
|
|
23
27
|
tableRow,
|
|
28
|
+
tableRowClickable,
|
|
24
29
|
} from "@/utils/styles";
|
|
25
30
|
|
|
26
31
|
const getNestedValue = (obj: unknown, path: string) => {
|
|
@@ -69,12 +74,39 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
69
74
|
defaultView,
|
|
70
75
|
showVerticalBorder = true,
|
|
71
76
|
verticalBorderClassname,
|
|
77
|
+
onRowClick,
|
|
78
|
+
layout = "auto",
|
|
79
|
+
density = "normal",
|
|
80
|
+
autoCardBreakpoint = 0,
|
|
81
|
+
virtualized = false,
|
|
82
|
+
virtualizedMaxHeight = 400,
|
|
83
|
+
rowHeight,
|
|
84
|
+
overscan = 5,
|
|
85
|
+
stickyHeader = false,
|
|
72
86
|
}: ITDataTableProps<T>) {
|
|
73
87
|
const [viewMode, setViewMode] = useState<"table" | "cards">(defaultView || "table");
|
|
88
|
+
const { ref: rootRef, width: containerWidth } = useElementSize<HTMLDivElement>();
|
|
74
89
|
const [data, setData] = useState<T[]>([]);
|
|
75
90
|
const [totalItems, setTotalItems] = useState(0);
|
|
76
91
|
const [isLoading, setIsLoading] = useState(fetchOnMount);
|
|
77
92
|
|
|
93
|
+
const isFixed = layout === "fixed";
|
|
94
|
+
const forcedCards =
|
|
95
|
+
autoCardBreakpoint > 0 && containerWidth > 0 && containerWidth < autoCardBreakpoint;
|
|
96
|
+
const effectiveView: "table" | "cards" = forcedCards ? "cards" : viewMode;
|
|
97
|
+
const hasColumnWidths = columns.some((col) => col.width != null);
|
|
98
|
+
const tableLayoutClass = isFixed
|
|
99
|
+
? "w-full text-sm text-left text-secondary-600 transition-opacity duration-300"
|
|
100
|
+
: "min-w-max w-full text-sm text-left text-secondary-600 transition-opacity duration-300";
|
|
101
|
+
|
|
102
|
+
const isTableVirtual = virtualized && effectiveView === "table";
|
|
103
|
+
const isStickyHeader = stickyHeader && isTableVirtual;
|
|
104
|
+
const resolvedRowHeight = rowHeight ?? getRowHeight(size, density);
|
|
105
|
+
const columnCount = columns.length;
|
|
106
|
+
const scrollOuterClass = isTableVirtual
|
|
107
|
+
? "relative min-h-[200px]"
|
|
108
|
+
: "overflow-x-auto relative min-h-[200px]";
|
|
109
|
+
|
|
78
110
|
const {
|
|
79
111
|
currentPage,
|
|
80
112
|
itemsPerPage,
|
|
@@ -86,6 +118,22 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
86
118
|
handleItemsPerPageChange,
|
|
87
119
|
} = useTableState({ defaultItemsPerPage });
|
|
88
120
|
|
|
121
|
+
const {
|
|
122
|
+
scrollRef,
|
|
123
|
+
startIndex,
|
|
124
|
+
endIndex,
|
|
125
|
+
topSpacerHeight,
|
|
126
|
+
bottomSpacerHeight,
|
|
127
|
+
} = useVirtualRows({
|
|
128
|
+
count: data.length,
|
|
129
|
+
rowHeight: resolvedRowHeight,
|
|
130
|
+
overscan,
|
|
131
|
+
enabled: isTableVirtual,
|
|
132
|
+
resetKey: `${currentPage}-${itemsPerPage}`,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const windowData = isTableVirtual ? data.slice(startIndex, endIndex + 1) : data;
|
|
136
|
+
|
|
89
137
|
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
90
138
|
const hasFetchedRef = useRef(false);
|
|
91
139
|
const fetchDataRef = useRef(fetchData);
|
|
@@ -270,13 +318,181 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
270
318
|
|
|
271
319
|
const segCtrlClass = (mode: "table" | "cards") =>
|
|
272
320
|
`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${
|
|
273
|
-
|
|
321
|
+
effectiveView === mode
|
|
274
322
|
? "bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-sm border border-slate-200/50 dark:border-slate-700"
|
|
275
323
|
: "text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
|
|
276
324
|
}`;
|
|
277
325
|
|
|
326
|
+
const handleRowClick = (row: T) => (event: React.MouseEvent<HTMLElement>) => {
|
|
327
|
+
if (!onRowClick || isLoading || isInteractiveTarget(event.target, event.currentTarget)) return;
|
|
328
|
+
onRowClick(row, event);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const handleRowKeyDown = (row: T) => (event: React.KeyboardEvent<HTMLElement>) => {
|
|
332
|
+
if (!onRowClick || isLoading) return;
|
|
333
|
+
if (event.target !== event.currentTarget) return;
|
|
334
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
335
|
+
event.preventDefault();
|
|
336
|
+
onRowClick(row, event);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const tableEl = (
|
|
340
|
+
<table
|
|
341
|
+
className={clsx(
|
|
342
|
+
tableLayoutClass,
|
|
343
|
+
showVerticalBorder && "[&_th]:border-r [&_th:last-child]:border-r-0 [&_td]:border-r [&_td:last-child]:border-r-0",
|
|
344
|
+
showVerticalBorder && (verticalBorderClassname || "[&_th]:border-slate-100 dark:[&_th]:border-slate-700/30 [&_td]:border-slate-100 dark:[&_td]:border-slate-700/30"),
|
|
345
|
+
isLoading ? "opacity-50" : "opacity-100",
|
|
346
|
+
variantStyles[variant],
|
|
347
|
+
sizeStyles[size],
|
|
348
|
+
className
|
|
349
|
+
)}
|
|
350
|
+
style={isFixed ? { tableLayout: "fixed" } : undefined}
|
|
351
|
+
aria-rowcount={isTableVirtual ? data.length + 1 : undefined}
|
|
352
|
+
>
|
|
353
|
+
{hasColumnWidths && (
|
|
354
|
+
<colgroup>
|
|
355
|
+
{columns.map((col) => (
|
|
356
|
+
<col
|
|
357
|
+
key={col.key}
|
|
358
|
+
style={
|
|
359
|
+
col.width != null
|
|
360
|
+
? { width: typeof col.width === "number" ? `${col.width}px` : col.width }
|
|
361
|
+
: undefined
|
|
362
|
+
}
|
|
363
|
+
/>
|
|
364
|
+
))}
|
|
365
|
+
</colgroup>
|
|
366
|
+
)}
|
|
367
|
+
<thead>
|
|
368
|
+
<tr
|
|
369
|
+
className={clsx(tableHeaderRow, "dark:text-slate-200")}
|
|
370
|
+
aria-rowindex={isTableVirtual ? 1 : undefined}
|
|
371
|
+
>
|
|
372
|
+
{columns.map((col) => (
|
|
373
|
+
<th
|
|
374
|
+
key={col.key}
|
|
375
|
+
scope="col"
|
|
376
|
+
className={clsx(
|
|
377
|
+
tableHeaderCell(col.className, density),
|
|
378
|
+
col.align && tableAlignClasses[col.align],
|
|
379
|
+
isStickyHeader && "sticky top-0 z-10 bg-secondary-50"
|
|
380
|
+
)}
|
|
381
|
+
style={
|
|
382
|
+
col.minWidth != null || isStickyHeader
|
|
383
|
+
? {
|
|
384
|
+
...(col.minWidth != null ? { minWidth: `${col.minWidth}px` } : {}),
|
|
385
|
+
...(isStickyHeader
|
|
386
|
+
? { backgroundColor: "var(--color-table-headerBg, #f8fafc)" }
|
|
387
|
+
: {}),
|
|
388
|
+
}
|
|
389
|
+
: undefined
|
|
390
|
+
}
|
|
391
|
+
>
|
|
392
|
+
<div className={isFixed ? "flex flex-col gap-3" : "flex flex-col gap-3 min-w-[150px]"}>
|
|
393
|
+
<div className="flex items-center justify-between gap-2">
|
|
394
|
+
<ITText as="span" className="text-slate-900 dark:text-white font-bold">{col.label}</ITText>
|
|
395
|
+
{col.sortable && col.type !== "actions" && (
|
|
396
|
+
<button
|
|
397
|
+
onClick={() => handleSort(col.key)}
|
|
398
|
+
disabled={isLoading}
|
|
399
|
+
className={`p-1 rounded-md transition-colors ${
|
|
400
|
+
sortConfig?.key === col.key
|
|
401
|
+
? "bg-secondary-200 text-secondary-900"
|
|
402
|
+
: "hover:bg-secondary-200 text-secondary-400 hover:text-secondary-700"
|
|
403
|
+
} disabled:opacity-50`}
|
|
404
|
+
title={`Ordenar por ${col.label}`}
|
|
405
|
+
>
|
|
406
|
+
<MdOutlineSwapVert className="w-4 h-4" aria-hidden="true" />
|
|
407
|
+
</button>
|
|
408
|
+
)}
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
<div className="w-full">{col.filter ? renderFilterInput(col) : null}</div>
|
|
412
|
+
</div>
|
|
413
|
+
</th>
|
|
414
|
+
))}
|
|
415
|
+
</tr>
|
|
416
|
+
</thead>
|
|
417
|
+
<tbody className={clsx(tableBody, "dark:divide-slate-700/30")}>
|
|
418
|
+
{data.length > 0 ? (
|
|
419
|
+
<>
|
|
420
|
+
{isTableVirtual && topSpacerHeight > 0 && (
|
|
421
|
+
<tr aria-hidden="true" style={{ height: topSpacerHeight }}>
|
|
422
|
+
<td colSpan={columnCount} style={{ padding: 0, border: 0 }} />
|
|
423
|
+
</tr>
|
|
424
|
+
)}
|
|
425
|
+
{windowData.map((row, windowIndex) => {
|
|
426
|
+
const absoluteIndex = isTableVirtual ? startIndex + windowIndex : windowIndex;
|
|
427
|
+
return (
|
|
428
|
+
<tr
|
|
429
|
+
key={absoluteIndex}
|
|
430
|
+
aria-rowindex={isTableVirtual ? absoluteIndex + 2 : undefined}
|
|
431
|
+
className={clsx(
|
|
432
|
+
tableRow,
|
|
433
|
+
variant === "striped" && absoluteIndex % 2 === 0 && "bg-secondary-50/40 dark:bg-slate-800/20",
|
|
434
|
+
onRowClick && !isLoading && tableRowClickable
|
|
435
|
+
)}
|
|
436
|
+
tabIndex={onRowClick && !isLoading ? 0 : undefined}
|
|
437
|
+
onClick={onRowClick && !isLoading ? handleRowClick(row) : undefined}
|
|
438
|
+
onKeyDown={onRowClick && !isLoading ? handleRowKeyDown(row) : undefined}
|
|
439
|
+
>
|
|
440
|
+
{columns.map((col) => {
|
|
441
|
+
const rawValue = getNestedValue(row, col.key);
|
|
442
|
+
const truncateTitle =
|
|
443
|
+
col.truncate &&
|
|
444
|
+
(typeof rawValue === "string" || typeof rawValue === "number")
|
|
445
|
+
? String(rawValue)
|
|
446
|
+
: undefined;
|
|
447
|
+
return (
|
|
448
|
+
<td
|
|
449
|
+
key={`${absoluteIndex}-${col.key}`}
|
|
450
|
+
className={clsx(
|
|
451
|
+
tableCell(col.className, density),
|
|
452
|
+
col.align && tableAlignClasses[col.align]
|
|
453
|
+
)}
|
|
454
|
+
style={col.minWidth != null ? { minWidth: `${col.minWidth}px` } : undefined}
|
|
455
|
+
title={truncateTitle}
|
|
456
|
+
>
|
|
457
|
+
{col.type === "actions" ? (
|
|
458
|
+
<div className={tableActionsCell}>
|
|
459
|
+
{renderCellContent(col, row) as React.ReactNode}
|
|
460
|
+
</div>
|
|
461
|
+
) : (
|
|
462
|
+
<div className={clsx(tableCellText, col.truncate && "truncate")}>
|
|
463
|
+
{renderCellContent(col, row) as React.ReactNode}
|
|
464
|
+
</div>
|
|
465
|
+
)}
|
|
466
|
+
</td>
|
|
467
|
+
);
|
|
468
|
+
})}
|
|
469
|
+
</tr>
|
|
470
|
+
);
|
|
471
|
+
})}
|
|
472
|
+
{isTableVirtual && bottomSpacerHeight > 0 && (
|
|
473
|
+
<tr aria-hidden="true" style={{ height: bottomSpacerHeight }}>
|
|
474
|
+
<td colSpan={columnCount} style={{ padding: 0, border: 0 }} />
|
|
475
|
+
</tr>
|
|
476
|
+
)}
|
|
477
|
+
</>
|
|
478
|
+
) : (
|
|
479
|
+
<tr>
|
|
480
|
+
<td colSpan={columns.length} className="px-6 py-12 text-center">
|
|
481
|
+
{!isLoading && (
|
|
482
|
+
<div className={tableEmptyContent}>
|
|
483
|
+
<ITText as="span" className="text-lg">No se encontraron resultados</ITText>
|
|
484
|
+
<ITText as="span" className="text-sm mt-1">Intenta ajustar los filtros</ITText>
|
|
485
|
+
</div>
|
|
486
|
+
)}
|
|
487
|
+
</td>
|
|
488
|
+
</tr>
|
|
489
|
+
)}
|
|
490
|
+
</tbody>
|
|
491
|
+
</table>
|
|
492
|
+
);
|
|
493
|
+
|
|
278
494
|
return (
|
|
279
|
-
<div className={clsx("space-y-4 w-full relative", containerClassName)}>
|
|
495
|
+
<div ref={rootRef} className={clsx("space-y-4 w-full relative", containerClassName)}>
|
|
280
496
|
<div className={tableContainer} style={{ backgroundColor: 'var(--color-table-rowBg, #ffffff)' }}>
|
|
281
497
|
{title && (
|
|
282
498
|
<div className="px-6 py-5 flex items-center justify-between" style={{ backgroundColor: 'var(--color-table-rowBg, #ffffff)' }}>
|
|
@@ -288,7 +504,7 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
288
504
|
</div>
|
|
289
505
|
)}
|
|
290
506
|
<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")}>
|
|
507
|
+
<button onClick={() => setViewMode("table")} disabled={forcedCards} className={segCtrlClass("table")}>
|
|
292
508
|
<FaTable size={11} />
|
|
293
509
|
Table
|
|
294
510
|
</button>
|
|
@@ -304,7 +520,7 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
304
520
|
{!title && (
|
|
305
521
|
<div className="flex justify-end px-4 pt-3">
|
|
306
522
|
<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")}>
|
|
523
|
+
<button onClick={() => setViewMode("table")} disabled={forcedCards} className={segCtrlClass("table")}>
|
|
308
524
|
<FaTable size={11} />
|
|
309
525
|
Table
|
|
310
526
|
</button>
|
|
@@ -316,7 +532,7 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
316
532
|
</div>
|
|
317
533
|
)}
|
|
318
534
|
|
|
319
|
-
<div className=
|
|
535
|
+
<div className={scrollOuterClass}>
|
|
320
536
|
{isLoading && (
|
|
321
537
|
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/40 backdrop-blur-[2px] transition-all duration-300">
|
|
322
538
|
<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 +546,18 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
330
546
|
</div>
|
|
331
547
|
)}
|
|
332
548
|
|
|
333
|
-
{
|
|
549
|
+
{effectiveView === "cards" ? (
|
|
334
550
|
<div className="p-4 space-y-3 max-h-[400px] overflow-y-auto">
|
|
335
551
|
{data.length > 0 ? (
|
|
336
552
|
data.map((row, i) => (
|
|
337
|
-
<div
|
|
553
|
+
<div
|
|
554
|
+
key={i}
|
|
555
|
+
className={clsx(tableCardHover, onRowClick && !isLoading && tableCardClickable)}
|
|
556
|
+
role={onRowClick && !isLoading ? "button" : undefined}
|
|
557
|
+
tabIndex={onRowClick && !isLoading ? 0 : undefined}
|
|
558
|
+
onClick={onRowClick && !isLoading ? handleRowClick(row) : undefined}
|
|
559
|
+
onKeyDown={onRowClick && !isLoading ? handleRowKeyDown(row) : undefined}
|
|
560
|
+
>
|
|
338
561
|
{renderCard ? renderCard(row) : renderDefaultCard(row)}
|
|
339
562
|
</div>
|
|
340
563
|
))
|
|
@@ -347,80 +570,16 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
347
570
|
)
|
|
348
571
|
)}
|
|
349
572
|
</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
|
-
)}
|
|
573
|
+
) : isTableVirtual ? (
|
|
574
|
+
<div
|
|
575
|
+
ref={scrollRef}
|
|
576
|
+
className="overflow-x-auto overflow-y-auto"
|
|
577
|
+
style={{ maxHeight: virtualizedMaxHeight }}
|
|
361
578
|
>
|
|
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>
|
|
579
|
+
{tableEl}
|
|
580
|
+
</div>
|
|
581
|
+
) : (
|
|
582
|
+
tableEl
|
|
424
583
|
)}
|
|
425
584
|
</div>
|
|
426
585
|
</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
|
};
|
|
@@ -5,7 +5,8 @@ import ITText from "@/components/atoms/text/text";
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Vertical navigation sidebar with submenu expand/collapse, hover tooltips in collapsed mode,
|
|
8
|
-
* and glassmorphism styling.
|
|
8
|
+
* and glassmorphism styling. The collapsed rail expands automatically on pointer hover — there
|
|
9
|
+
* is no toggle button; the hover is the only way to expand and collapse it.
|
|
9
10
|
*
|
|
10
11
|
* @example
|
|
11
12
|
* ```tsx
|
|
@@ -20,8 +21,7 @@ import ITText from "@/components/atoms/text/text";
|
|
|
20
21
|
* ],
|
|
21
22
|
* },
|
|
22
23
|
* ]}
|
|
23
|
-
* isCollapsed
|
|
24
|
-
* onToggleCollapse={() => setCollapsed(!collapsed)}
|
|
24
|
+
* isCollapsed
|
|
25
25
|
* />
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
@@ -33,6 +33,7 @@ export default function ITSidebar({
|
|
|
33
33
|
onItemClick,
|
|
34
34
|
onSubItemClick,
|
|
35
35
|
subitemConnector = 'dot',
|
|
36
|
+
header,
|
|
36
37
|
}: ITSidebarProps) {
|
|
37
38
|
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
|
38
39
|
const [isHovering, setIsHovering] = useState(false);
|
|
@@ -133,12 +134,24 @@ export default function ITSidebar({
|
|
|
133
134
|
backdropFilter: 'blur(12px)',
|
|
134
135
|
}}
|
|
135
136
|
>
|
|
137
|
+
{header && !isSidebarCollapsed && (
|
|
138
|
+
<div className="px-4 pt-4 pb-1 flex-shrink-0">{header}</div>
|
|
139
|
+
)}
|
|
140
|
+
|
|
136
141
|
{/* Navigation Items */}
|
|
137
|
-
<nav className="flex-1 py-4 overflow-y-auto overflow-x-hidden custom-scrollbar px-4">
|
|
142
|
+
<nav aria-label="Componentes de la librería" className="flex-1 py-4 overflow-y-auto overflow-x-hidden custom-scrollbar px-4">
|
|
138
143
|
<ul className="space-y-1">
|
|
139
|
-
{navigationItems.map((item) =>
|
|
144
|
+
{navigationItems.map((item) => {
|
|
145
|
+
const hasSubmenu = !!item.subitems && item.subitems.length > 0;
|
|
146
|
+
const submenuId = `it-sidebar-submenu-${item.id}`;
|
|
147
|
+
return (
|
|
140
148
|
<li key={item.id} className="relative group/navitem">
|
|
141
149
|
<div
|
|
150
|
+
role="button"
|
|
151
|
+
tabIndex={0}
|
|
152
|
+
aria-label={item.label}
|
|
153
|
+
aria-expanded={hasSubmenu ? expandedItems.has(item.id) : undefined}
|
|
154
|
+
aria-controls={hasSubmenu ? submenuId : undefined}
|
|
142
155
|
className={`flex items-center cursor-pointer
|
|
143
156
|
transition-all duration-300 ease-[cubic-bezier(0.2,0,0,1)]
|
|
144
157
|
rounded-xl relative overflow-visible
|
|
@@ -156,6 +169,12 @@ export default function ITSidebar({
|
|
|
156
169
|
if (!item.isActive) e.currentTarget.style.backgroundColor = 'transparent';
|
|
157
170
|
}}
|
|
158
171
|
onClick={() => handleItemClick(item)}
|
|
172
|
+
onKeyDown={(e) => {
|
|
173
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
handleItemClick(item);
|
|
176
|
+
}
|
|
177
|
+
}}
|
|
159
178
|
>
|
|
160
179
|
{item.isActive && !isSidebarCollapsed && (
|
|
161
180
|
<div
|
|
@@ -264,7 +283,7 @@ export default function ITSidebar({
|
|
|
264
283
|
|
|
265
284
|
{/* Submenu - smooth height/opacity when not collapsed */}
|
|
266
285
|
{!isSidebarCollapsed && item.subitems && item.subitems.length > 0 && (
|
|
267
|
-
<div className={`overflow-hidden transition-all duration-400 ease-[cubic-bezier(0.2,0,0,1)] ${expandedItems.has(item.id) ? "max-h-[500px] opacity-100 mt-1" : "max-h-0 opacity-0"}`}>
|
|
286
|
+
<div id={submenuId} role="group" aria-label={item.label} className={`overflow-hidden transition-all duration-400 ease-[cubic-bezier(0.2,0,0,1)] ${expandedItems.has(item.id) ? "max-h-[500px] opacity-100 mt-1" : "max-h-0 opacity-0"}`}>
|
|
268
287
|
<ul
|
|
269
288
|
className="ml-4 flex flex-col gap-0 py-0.5"
|
|
270
289
|
style={{
|
|
@@ -276,6 +295,7 @@ export default function ITSidebar({
|
|
|
276
295
|
{item.subitems.map((subitem) => (
|
|
277
296
|
<li key={subitem.id} className="relative">
|
|
278
297
|
<button
|
|
298
|
+
aria-current={subitem.isActive ? "page" : undefined}
|
|
279
299
|
onClick={() => {
|
|
280
300
|
if (subitem.action) subitem.action();
|
|
281
301
|
if (onSubItemClick) onSubItemClick(subitem);
|
|
@@ -329,7 +349,7 @@ export default function ITSidebar({
|
|
|
329
349
|
</div>
|
|
330
350
|
)}
|
|
331
351
|
</li>
|
|
332
|
-
))}
|
|
352
|
+
);})}
|
|
333
353
|
</ul>
|
|
334
354
|
</nav>
|
|
335
355
|
</aside>
|
|
@@ -13,6 +13,8 @@ Sticky top navigation bar with glassmorphism effect, logo, breadcrumb-style nav
|
|
|
13
13
|
|------|------|---------|-------------|
|
|
14
14
|
| `logo` | `ReactNode` | — | Optional logo element (e.g. an `<img>` or SVG component). |
|
|
15
15
|
| `logoText` | `string` | — | Text displayed next to the logo as the brand/title. |
|
|
16
|
+
| `centerContent` | `ReactNode` | — | Content rendered in the centered region between the left (logo / nav) and right (user menu) areas — e.g. a global search box. Rendered only when provided, and only from the `lg` breakpoint up. |
|
|
17
|
+
| `children` | `ReactNode` | — | Actions rendered in the right area, immediately before the user menu — e.g. a notification bell, a help link, a theme switcher. The component owns the placement; the caller only supplies the controls. The area renders only when this prop or `userMenu` is provided. |
|
|
16
18
|
| `userMenu` | `object` | — | User dropdown configuration with name, email, avatar, and menu items. |
|
|
17
19
|
| `userMenu.userName` | `string` | — | Display name shown in the trigger button and dropdown header. |
|
|
18
20
|
| `userMenu.userEmail` | `string` | — | User email shown in the trigger button and dropdown header. |
|
|
@@ -53,6 +55,7 @@ import { FaChartBar, FaUserCog } from 'react-icons/fa';
|
|
|
53
55
|
|
|
54
56
|
## Notes
|
|
55
57
|
|
|
58
|
+
- The `centerContent` slot renders in a centered `flex-1` (basis 0) region that absorbs all the slack, so the left and right areas stay flush with the row edges. It is `hidden` below `lg`, which leaves the below-`lg` layout exactly as before (logo + user menu).
|
|
56
59
|
- The bar uses a **glassmorphism** backdrop blur effect (`backdrop-blur-xl`), giving it a translucent appearance over scrolling content.
|
|
57
60
|
- Navigation items support **icons** and highlight the active item with the primary color and an underline indicator.
|
|
58
61
|
- The **user menu** dropdown opens on avatar click and closes on outside click (via `useClickOutside` hook).
|