@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.
Files changed (50) hide show
  1. package/README.md +6 -2
  2. package/dist/index.cjs +957 -554
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +1 -1
  5. package/dist/index.d.cts +191 -9
  6. package/dist/index.d.ts +191 -9
  7. package/dist/index.js +908 -505
  8. package/dist/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/App.tsx +150 -154
  11. package/src/components/molecules/table/table.doc.mdx +64 -21
  12. package/src/components/molecules/table/table.props.ts +81 -2
  13. package/src/components/molecules/table/table.stories.tsx +155 -0
  14. package/src/components/molecules/table/table.tsx +178 -36
  15. package/src/components/organisms/data-table/ITDataTable.doc.mdx +37 -3
  16. package/src/components/organisms/data-table/ITDataTable.stories.tsx +43 -0
  17. package/src/components/organisms/data-table/dataTable.props.ts +64 -0
  18. package/src/components/organisms/data-table/dataTable.tsx +241 -82
  19. package/src/components/organisms/sidebar/sidebar.doc.mdx +5 -0
  20. package/src/components/organisms/sidebar/sidebar.props.ts +5 -0
  21. package/src/components/organisms/sidebar/sidebar.stories.tsx +1 -1
  22. package/src/components/organisms/sidebar/sidebar.tsx +27 -7
  23. package/src/components/organisms/topbar/topbar.doc.mdx +3 -0
  24. package/src/components/organisms/topbar/topbar.props.ts +16 -0
  25. package/src/components/organisms/topbar/topbar.tsx +133 -98
  26. package/src/components/templates/layout/layout.doc.mdx +2 -0
  27. package/src/components/templates/layout/layout.tsx +7 -1
  28. package/src/components/templates/page/page.doc.mdx +8 -2
  29. package/src/components/templates/page/page.props.ts +11 -1
  30. package/src/components/templates/page/page.tsx +11 -1
  31. package/src/components/theme-provider/themePresets.ts +6 -0
  32. package/src/components/theme-provider/themeProvider.doc.mdx +1 -0
  33. package/src/components/theme-provider/themeProvider.props.ts +2 -0
  34. package/src/components/theme-provider/themeProvider.tsx +13 -2
  35. package/src/dev.css +58 -1
  36. package/src/hooks/useElementSize.ts +46 -0
  37. package/src/hooks/useVirtualRows.ts +123 -0
  38. package/src/index.ts +2 -0
  39. package/src/sandbox/LandingShowcase.tsx +654 -0
  40. package/src/sandbox/NotFoundShowcase.tsx +70 -0
  41. package/src/sandbox/TopbarSearch.tsx +85 -0
  42. package/src/sandbox/navigation.ts +148 -0
  43. package/src/sandbox/useHashRoute.ts +149 -0
  44. package/src/showcases/DataShowcases.tsx +321 -24
  45. package/src/showcases/PageShowcases.tsx +31 -0
  46. package/src/showcases/StructureShowcases.tsx +42 -4
  47. package/src/theme/theme.types.ts +2 -0
  48. package/src/types/table.types.ts +48 -0
  49. package/src/utils/styles.ts +12 -5
  50. 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 { 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,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
- viewMode === mode
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="overflow-x-auto relative min-h-[200px]">
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
- {viewMode === "cards" ? (
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 key={i}>
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
- <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
- )}
573
+ ) : isTableVirtual ? (
574
+ <div
575
+ ref={scrollRef}
576
+ className="overflow-x-auto overflow-y-auto"
577
+ style={{ maxHeight: virtualizedMaxHeight }}
361
578
  >
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>
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. 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
  };
@@ -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={collapsed}
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).