@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.
Files changed (52) hide show
  1. package/README.md +6 -2
  2. package/dist/index.cjs +2342 -1715
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +1 -1
  5. package/dist/index.d.cts +286 -9
  6. package/dist/index.d.ts +286 -9
  7. package/dist/index.js +2164 -1538
  8. package/dist/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/App.tsx +153 -153
  11. package/src/components/molecules/calendar/calendar.doc.mdx +2 -1
  12. package/src/components/molecules/calendar/calendar.tsx +102 -24
  13. package/src/components/molecules/masked-input/masked-input.doc.mdx +78 -0
  14. package/src/components/molecules/masked-input/masked-input.props.ts +58 -0
  15. package/src/components/molecules/masked-input/masked-input.stories.tsx +126 -0
  16. package/src/components/molecules/masked-input/masked-input.tsx +244 -0
  17. package/src/components/molecules/table/table.doc.mdx +64 -21
  18. package/src/components/molecules/table/table.props.ts +81 -2
  19. package/src/components/molecules/table/table.stories.tsx +155 -0
  20. package/src/components/molecules/table/table.tsx +177 -36
  21. package/src/components/organisms/data-table/ITDataTable.doc.mdx +37 -3
  22. package/src/components/organisms/data-table/ITDataTable.stories.tsx +43 -0
  23. package/src/components/organisms/data-table/dataTable.props.ts +64 -0
  24. package/src/components/organisms/data-table/dataTable.tsx +240 -82
  25. package/src/components/organisms/sidebar/sidebar.doc.mdx +5 -0
  26. package/src/components/organisms/sidebar/sidebar.props.ts +5 -0
  27. package/src/components/organisms/sidebar/sidebar.stories.tsx +1 -1
  28. package/src/components/organisms/sidebar/sidebar.tsx +27 -7
  29. package/src/components/organisms/topbar/topbar.doc.mdx +3 -0
  30. package/src/components/organisms/topbar/topbar.props.ts +16 -0
  31. package/src/components/organisms/topbar/topbar.tsx +133 -98
  32. package/src/components/templates/layout/layout.doc.mdx +2 -0
  33. package/src/components/templates/layout/layout.tsx +7 -1
  34. package/src/components/templates/page/page.doc.mdx +8 -2
  35. package/src/components/templates/page/page.props.ts +11 -1
  36. package/src/components/templates/page/page.tsx +11 -1
  37. package/src/dev.css +58 -1
  38. package/src/hooks/useElementSize.ts +46 -0
  39. package/src/hooks/useVirtualRows.ts +123 -0
  40. package/src/index.ts +6 -2
  41. package/src/sandbox/LandingShowcase.tsx +465 -0
  42. package/src/sandbox/NotFoundShowcase.tsx +70 -0
  43. package/src/sandbox/TopbarSearch.tsx +85 -0
  44. package/src/sandbox/navigation.ts +148 -0
  45. package/src/sandbox/useHashRoute.ts +149 -0
  46. package/src/showcases/DataShowcases.tsx +321 -24
  47. package/src/showcases/FormShowcases.tsx +178 -0
  48. package/src/showcases/PageShowcases.tsx +31 -0
  49. package/src/showcases/StructureShowcases.tsx +42 -4
  50. package/src/types/table.types.ts +48 -0
  51. package/src/utils/styles.ts +9 -4
  52. 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 { sizeStyles, variantStyles } from "@/types/table.types";
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
- viewMode === mode
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="overflow-x-auto relative min-h-[200px]">
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
- {viewMode === "cards" ? (
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 key={i}>
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
- <table
352
- className={clsx(
353
- "min-w-max w-full text-sm text-left text-secondary-600 transition-opacity duration-300",
354
- showVerticalBorder && "[&_th]:border-r [&_th:last-child]:border-r-0 [&_td]:border-r [&_td:last-child]:border-r-0",
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
- <thead>
363
- <tr className={clsx(tableHeaderRow, "dark:text-slate-200")}>
364
- {columns.map((col) => (
365
- <th key={col.key} scope="col" className={tableHeaderCell(col.className)}>
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. Juega con el botón de colapsar para ver las transiciones suaves.</div>
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
  };