@axzydev/axzy_ui_system 1.3.1 → 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 (45) hide show
  1. package/README.md +6 -2
  2. package/dist/index.cjs +930 -544
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +1 -1
  5. package/dist/index.d.cts +187 -9
  6. package/dist/index.d.ts +187 -9
  7. package/dist/index.js +881 -495
  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 +177 -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 +240 -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/dev.css +58 -1
  32. package/src/hooks/useElementSize.ts +46 -0
  33. package/src/hooks/useVirtualRows.ts +123 -0
  34. package/src/index.ts +2 -0
  35. package/src/sandbox/LandingShowcase.tsx +465 -0
  36. package/src/sandbox/NotFoundShowcase.tsx +70 -0
  37. package/src/sandbox/TopbarSearch.tsx +85 -0
  38. package/src/sandbox/navigation.ts +148 -0
  39. package/src/sandbox/useHashRoute.ts +149 -0
  40. package/src/showcases/DataShowcases.tsx +321 -24
  41. package/src/showcases/PageShowcases.tsx +31 -0
  42. package/src/showcases/StructureShowcases.tsx +42 -4
  43. package/src/types/table.types.ts +48 -0
  44. package/src/utils/styles.ts +9 -4
  45. package/src/utils/table.utils.ts +31 -0
@@ -32,6 +32,21 @@ const TABLE_DATA: Record<string, unknown>[] = [
32
32
  { id: 15, name: "Florencia Díaz", email: "florencia@axzy.dev", role: "Admin", status: "active", lastLogin: "2026-07-18T07:15:00", sales: 48900 },
33
33
  ];
34
34
 
35
+ const VIRTUAL_USERS = Array.from({ length: 240 }, (_, i) => ({
36
+ id: i + 1,
37
+ name: `User ${String(i + 1).padStart(3, "0")}`,
38
+ email: `user${i + 1}@axzy.dev`,
39
+ role: (["Admin", "Editor", "Viewer"] as const)[i % 3],
40
+ active: i % 2 === 0,
41
+ }));
42
+ const virtualUserColumns: any = [
43
+ { key: "id", label: "ID", type: "number", width: 64 },
44
+ { key: "name", label: "Name", type: "string", width: "26%", truncate: true },
45
+ { key: "email", label: "Email", type: "string", width: "28%", truncate: true },
46
+ { key: "role", label: "Role", type: "string", width: "16%" },
47
+ { key: "active", label: "Active", type: "boolean", width: "12%" },
48
+ ];
49
+
35
50
  const nameColumn = {
36
51
  key: "name",
37
52
  label: "Name",
@@ -67,6 +82,7 @@ export const TableShowcase = () => {
67
82
  const [variant, setVariant] = useState<any>("default");
68
83
  const [size, setSize] = useState<any>("sm");
69
84
  const [useCustomCard, setUseCustomCard] = useState(true);
85
+ const [selectedRow, setSelectedRow] = useState<Record<string, unknown> | null>(null);
70
86
 
71
87
  const customCard = (row: Record<string, unknown>) => {
72
88
  const initials = (row.name as string).split(" ").map((w: string) => w[0]).join("");
@@ -157,6 +173,31 @@ export const TableShowcase = () => {
157
173
  }
158
174
  ];
159
175
 
176
+ const fixedColumns: any = columns.map((col: any, i: number) => ({
177
+ ...col,
178
+ width: [64, "18%", "18%", "14%", "20%", "10%"][i],
179
+ truncate: col.key === "email" || col.key === "name",
180
+ }));
181
+
182
+ // Single-line columns for virtualization: no avatar, no actions, no custom
183
+ // render, so every row is a deterministic one-line box.
184
+ const virtualColumns: any = [
185
+ { key: "id", label: "ID", type: "number", width: 64 },
186
+ { key: "name", label: "Name", type: "string", width: "28%", truncate: true },
187
+ { key: "email", label: "Email", type: "string", width: "28%", truncate: true },
188
+ { key: "role", label: "Role", type: "string", width: "16%" },
189
+ { key: "sales", label: "Sales", type: "number", width: "18%" },
190
+ ];
191
+
192
+ const virtualData = useMemo(
193
+ () =>
194
+ Array.from({ length: 300 }, (_, i) => ({
195
+ ...TABLE_DATA[i % TABLE_DATA.length],
196
+ id: i + 1,
197
+ })),
198
+ []
199
+ );
200
+
160
201
  const code = `<ITTable
161
202
  columns={columns}
162
203
  data={data}
@@ -173,16 +214,24 @@ export const TableShowcase = () => {
173
214
  description="Tabla con vista responsive Table / Cards, paginación, filtros y template de card personalizable."
174
215
  code={code}
175
216
  demo={
176
- <div className="w-full bg-white dark:bg-slate-900 rounded-xl overflow-hidden shadow-sm">
177
- <ITTable
178
- columns={columns}
179
- data={TABLE_DATA}
180
- title="Team members"
181
- variant={variant}
182
- size={size}
183
- defaultItemsPerPage={5}
184
- renderCard={useCustomCard ? customCard : undefined}
185
- />
217
+ <div className="w-full space-y-2">
218
+ <div className="text-xs font-medium text-slate-500 dark:text-slate-400 px-1">
219
+ {selectedRow
220
+ ? `Selected: ${selectedRow.name as string}`
221
+ : "Click a row/card or press Enter/Space to select"}
222
+ </div>
223
+ <div className="w-full bg-white dark:bg-slate-900 rounded-xl overflow-hidden shadow-sm">
224
+ <ITTable
225
+ columns={columns}
226
+ data={TABLE_DATA}
227
+ title="Team members"
228
+ variant={variant}
229
+ size={size}
230
+ defaultItemsPerPage={5}
231
+ renderCard={useCustomCard ? customCard : undefined}
232
+ onRowClick={(row) => setSelectedRow(row)}
233
+ />
234
+ </div>
186
235
  </div>
187
236
  }
188
237
  controls={
@@ -195,8 +244,7 @@ export const TableShowcase = () => {
195
244
  options={[
196
245
  { label: "Default", value: "default" },
197
246
  { label: "Striped", value: "striped" },
198
- { label: "Bordered", value: "bordered" },
199
- { label: "Clean", value: "clean" }
247
+ { label: "Bordered", value: "bordered" }
200
248
  ]}
201
249
  />
202
250
  <ITSelect
@@ -221,6 +269,48 @@ export const TableShowcase = () => {
221
269
  )}
222
270
  </>
223
271
  }
272
+ doc={{
273
+ summary: "Tabla de datos con vistas Table/Cards, paginación, filtros, ordenamiento y activación de fila.",
274
+ examples: [
275
+ '<ITTable columns={columns} data={data} title="Users" />',
276
+ '<ITTable columns={columns} data={data} defaultView="cards" onRowClick={(row) => open(row)} />',
277
+ '<ITTable columns={fixedColumns} data={data} layout="fixed" density="compact" />',
278
+ '<ITTable columns={cols} data={rows} virtualized stickyHeader layout="fixed" density="compact" defaultItemsPerPage={300} itemsPerPageOptions={[50, 100, 300]} />',
279
+ ],
280
+ props: [
281
+ { name: "columns", type: "Column<T>[]", description: "Definiciones de columna: key, label, type, sortable, filter, render, actions." },
282
+ { name: "data", type: "T[]", default: "[]", description: "Arreglo de filas a renderizar." },
283
+ { name: "containerClassName", type: "string", description: "Clases del contenedor externo." },
284
+ { name: "className", type: "string", description: "Declarado por compatibilidad, pero ITTable no lo aplica; usar containerClassName." },
285
+ { name: "variant", type: '"default" | "striped" | "bordered"', default: '"default"', description: "Variante visual." },
286
+ { name: "size", type: '"sm" | "md" | "lg"', default: '"md"', description: "Tamaño de fuente; no cambia padding (usar density)." },
287
+ { name: "itemsPerPageOptions", type: "number[]", default: "[5, 10, 20]", description: "Opciones del selector de filas por página." },
288
+ { name: "defaultItemsPerPage", type: "number", default: "10", description: "Filas por página iniciales." },
289
+ { name: "title", type: "string", description: "Título opcional sobre la tabla." },
290
+ { name: "renderCard", type: "(row: T) => ReactNode", description: "Renderer custom de card para la vista cards." },
291
+ { name: "defaultView", type: '"table" | "cards"', default: '"table"', description: "Vista inicial." },
292
+ { name: "showVerticalBorder", type: "boolean", default: "true", description: "Bordes verticales entre columnas." },
293
+ { name: "verticalBorderClassname", type: "string", description: "Clases custom de los bordes verticales." },
294
+ { name: "onRowClick", type: "(row: T, event: MouseEvent<HTMLElement> | KeyboardEvent<HTMLElement>) => void", description: "Activa fila/card. MouseEvent en puntero, KeyboardEvent en Enter/Space. Se ignora si la interacción inicia en un hijo interactivo." },
295
+ { name: "layout", type: '"auto" | "fixed"', default: '"auto"', description: "fixed respeta width de columna y elimina min-w, evitando scroll horizontal." },
296
+ { name: "density", type: '"compact" | "normal" | "comfortable"', default: '"normal"', description: "Padding / alto de fila (independiente de size, que solo cambia font-size)." },
297
+ { name: "autoCardBreakpoint", type: "number", default: "0", description: "Ancho de contenedor (px) bajo el cual cae a la vista cards. 0 desactiva." },
298
+ { name: "virtualized", type: "boolean", default: "false", description: "Vista tabla: renderiza solo la ventana visible con spacer rows. Cards nunca virtualiza." },
299
+ { name: "virtualizedMaxHeight", type: "number", default: "400", description: "Alto máx (px) del contenedor con scroll virtual." },
300
+ { name: "rowHeight", type: "number", default: "getRowHeight(size, density)", description: "Alto de fila asumido (px). Size-aware: baseline size=\"md\" = 33/45/53 según densidad; size lo desplaza por el line box (sm 16 / md 20 / lg 28 px)." },
301
+ { name: "overscan", type: "number", default: "5", description: "Filas extra renderizadas arriba/abajo del viewport." },
302
+ { name: "stickyHeader", type: "boolean", default: "false", description: "Header sticky mientras scrollea el cuerpo virtualizado." },
303
+ ],
304
+ notes: [
305
+ "onRowClick funciona en ambas vistas (tabla y cards).",
306
+ "Cuando se omite no se agrega cursor-pointer, foco, tabIndex ni role.",
307
+ "Column acepta width (number=px | string CSS), minWidth (px), align (left/center/right) y truncate (ellipsis + title nativo).",
308
+ "Anti scroll horizontal: layout=\"fixed\" + width por columna + truncate + density.",
309
+ "Virtualización: solo vista tabla; rinde con page size grande; filas de alto uniforme; el scroll no se reinicia al cambiar filtros.",
310
+ "Virtualización exige alto de fila uniforme. El default es size-aware (getRowHeight(size, density)): con size=\"md\" el baseline es compact 33 / normal 45 / comfortable 53 (padding de celda + line box text-sm de 20px + 1px del divide-y); size lo desplaza por el line box (sm 16 / md 20 / lg 28 px, es decir −4 / 0 / +8 px). Una celda de dos líneas (avatar + texto) mide ~50px real, distinto de ese alto asumido, así que los spacers se desalinean: usar celdas de una sola línea o pasar un rowHeight explícito.",
311
+ "ITSearchTable no soporta layout/density/autoCardBreakpoint/virtualized/width — no asumir paridad.",
312
+ ],
313
+ }}
224
314
  gallery={
225
315
  <div className="space-y-6">
226
316
  <div>
@@ -269,6 +359,7 @@ export const TableShowcase = () => {
269
359
  size="sm"
270
360
  defaultItemsPerPage={6}
271
361
  defaultView="cards"
362
+ onRowClick={(row) => setSelectedRow(row)}
272
363
  />
273
364
  </div>
274
365
  </div>
@@ -304,6 +395,51 @@ export const TableShowcase = () => {
304
395
  />
305
396
  </div>
306
397
  </div>
398
+
399
+ <div>
400
+ <h4 className="text-sm font-bold text-slate-700 dark:text-slate-200 mb-3">Compact fixed layout — no horizontal scroll</h4>
401
+ <div className="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden" style={{ maxWidth: 560 }}>
402
+ <ITTable
403
+ columns={fixedColumns}
404
+ data={TABLE_DATA}
405
+ title="Fixed layout"
406
+ layout="fixed"
407
+ density="compact"
408
+ defaultItemsPerPage={5}
409
+ />
410
+ </div>
411
+ </div>
412
+
413
+ <div>
414
+ <h4 className="text-sm font-bold text-slate-700 dark:text-slate-200 mb-3">Auto cards fallback (&lt; 640px container)</h4>
415
+ <div className="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden" style={{ maxWidth: 420 }}>
416
+ <ITTable
417
+ columns={columns}
418
+ data={TABLE_DATA.slice(0, 6)}
419
+ title="Narrow container"
420
+ autoCardBreakpoint={640}
421
+ defaultItemsPerPage={6}
422
+ />
423
+ </div>
424
+ </div>
425
+
426
+ <div>
427
+ <h4 className="text-sm font-bold text-slate-700 dark:text-slate-200 mb-3">Virtualized table — 300 rows (only visible window mounted)</h4>
428
+ <div className="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden">
429
+ <ITTable
430
+ columns={virtualColumns}
431
+ data={virtualData}
432
+ title="Virtualized · 300 rows"
433
+ virtualized
434
+ stickyHeader
435
+ layout="fixed"
436
+ density="compact"
437
+ virtualizedMaxHeight={360}
438
+ defaultItemsPerPage={300}
439
+ itemsPerPageOptions={[50, 100, 300]}
440
+ />
441
+ </div>
442
+ </div>
307
443
  </div>
308
444
  }
309
445
  />
@@ -317,6 +453,13 @@ export const DataTableShowcase = () => {
317
453
  const [reloadKey, setReloadKey] = useState(0);
318
454
  const [showVBorder, setShowVBorder] = useState(true);
319
455
  const [borderStyle, setBorderStyle] = useState<"default" | "blue" | "rose" | "none">("default");
456
+ const [selectedRow, setSelectedRow] = useState<any>(null);
457
+ const [dtSize, setDtSize] = useState<"sm" | "md" | "lg">("md");
458
+ const [dtDensity, setDtDensity] = useState<"compact" | "normal" | "comfortable">("normal");
459
+ const [dtLayout, setDtLayout] = useState<"auto" | "fixed">("auto");
460
+ const [dtAutoCardBreakpoint, setDtAutoCardBreakpoint] = useState(0);
461
+ const [dtVirtualized, setDtVirtualized] = useState(true);
462
+ const [dtStickyHeader, setDtStickyHeader] = useState(false);
320
463
 
321
464
  const userColumns = useMemo(() => [
322
465
  { key: "id", label: "ID", type: "number" as const },
@@ -403,6 +546,12 @@ export const DataTableShowcase = () => {
403
546
  };
404
547
  }, [apiState, MOCK_USERS]);
405
548
 
549
+ const virtualFetchData = useCallback(async (params: ITDataTableFetchParams): Promise<ITDataTableResponse<any>> => {
550
+ await new Promise((r) => setTimeout(r, 150));
551
+ const start = (params.page - 1) * params.limit;
552
+ return { data: VIRTUAL_USERS.slice(start, start + params.limit), total: VIRTUAL_USERS.length };
553
+ }, []);
554
+
406
555
  const handleStateChange = (s: typeof apiState) => {
407
556
  setApiState(s);
408
557
  setReloadKey(k => k + 1);
@@ -412,7 +561,10 @@ export const DataTableShowcase = () => {
412
561
  columns={columns}
413
562
  fetchData={api.fetchUsers}
414
563
  title="Users"
415
- defaultItemsPerPage={5}
564
+ defaultItemsPerPage={4}
565
+ size="${dtSize}"
566
+ layout="${dtLayout}"
567
+ density="${dtDensity}"${dtAutoCardBreakpoint > 0 ? `\n autoCardBreakpoint={${dtAutoCardBreakpoint}}` : ""}
416
568
  showVerticalBorder={${showVBorder}}${borderStyle !== "default" ? `\n verticalBorderClassname="${borderStyle === "blue" ? "[&_th]:border-blue-200 [&_td]:border-blue-100" : "[&_th]:border-rose-200 [&_td]:border-rose-100"}"` : ""}
417
569
  renderCard={(row) => <CustomCard row={row} />}
418
570
  />`;
@@ -425,16 +577,27 @@ export const DataTableShowcase = () => {
425
577
  description="Tabla auto-suficiente con carga dinámica, filtros, paginación y vista responsive Cards/Table."
426
578
  code={code}
427
579
  demo={
428
- <div className="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden shadow-md">
429
- <ITDataTable
430
- key={reloadKey}
431
- columns={userColumns}
432
- fetchData={fetchData}
433
- title={apiState === "error" ? "Connection error" : apiState === "empty" ? "No data" : "Users"}
434
- defaultItemsPerPage={4}
435
- showVerticalBorder={showVBorder}
436
- verticalBorderClassname={borderClass}
437
- renderCard={useCustomCard ? (row: any) => {
580
+ <div className="w-full space-y-2">
581
+ <div className="text-xs font-medium text-slate-500 dark:text-slate-400 px-1">
582
+ {selectedRow
583
+ ? `Selected: ${selectedRow.name as string} (id ${selectedRow.id as number})`
584
+ : "Click a row/card or press Enter/Space to select"}
585
+ </div>
586
+ <div className="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden shadow-md">
587
+ <ITDataTable
588
+ key={reloadKey}
589
+ columns={userColumns}
590
+ fetchData={fetchData}
591
+ title={apiState === "error" ? "Connection error" : apiState === "empty" ? "No data" : "Users"}
592
+ defaultItemsPerPage={4}
593
+ size={dtSize}
594
+ layout={dtLayout}
595
+ density={dtDensity}
596
+ autoCardBreakpoint={dtAutoCardBreakpoint}
597
+ showVerticalBorder={showVBorder}
598
+ verticalBorderClassname={borderClass}
599
+ onRowClick={(row) => setSelectedRow(row)}
600
+ renderCard={useCustomCard ? (row: any) => {
438
601
  const initials = (row.name as string).split(" ").map((w: string) => w[0]).join("");
439
602
  return (
440
603
  <div className="group bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-3 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-slate-300 dark:hover:border-slate-600">
@@ -458,7 +621,8 @@ export const DataTableShowcase = () => {
458
621
  </div>
459
622
  );
460
623
  } : undefined}
461
- />
624
+ />
625
+ </div>
462
626
  </div>
463
627
  }
464
628
  controls={
@@ -511,8 +675,120 @@ export const DataTableShowcase = () => {
511
675
  ))}
512
676
  </div>
513
677
  </div>
678
+ <div className="space-y-3">
679
+ <p className="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-2">Table capabilities</p>
680
+ <ITSelect
681
+ name="dt_size"
682
+ label="Size"
683
+ value={dtSize}
684
+ onChange={(e: any) => setDtSize(e.target.value as "sm" | "md" | "lg")}
685
+ options={[
686
+ { label: "Small", value: "sm" },
687
+ { label: "Medium", value: "md" },
688
+ { label: "Large", value: "lg" },
689
+ ]}
690
+ />
691
+ <ITSelect
692
+ name="dt_density"
693
+ label="Density"
694
+ value={dtDensity}
695
+ onChange={(e: any) => setDtDensity(e.target.value as "compact" | "normal" | "comfortable")}
696
+ options={[
697
+ { label: "Compact", value: "compact" },
698
+ { label: "Normal", value: "normal" },
699
+ { label: "Comfortable", value: "comfortable" },
700
+ ]}
701
+ />
702
+ <ITSelect
703
+ name="dt_layout"
704
+ label="Layout"
705
+ value={dtLayout}
706
+ onChange={(e: any) => setDtLayout(e.target.value as "auto" | "fixed")}
707
+ options={[
708
+ { label: "Auto", value: "auto" },
709
+ { label: "Fixed", value: "fixed" },
710
+ ]}
711
+ />
712
+ <ITSelect
713
+ name="dt_autocards"
714
+ label="Auto cards below (px)"
715
+ value={String(dtAutoCardBreakpoint)}
716
+ onChange={(e: any) => setDtAutoCardBreakpoint(Number(e.target.value))}
717
+ options={[
718
+ { label: "Off", value: "0" },
719
+ { label: "640px", value: "640" },
720
+ { label: "900px", value: "900" },
721
+ { label: "1200px", value: "1200" },
722
+ ]}
723
+ />
724
+ <div className="flex items-center justify-between pt-2">
725
+ <span className="text-sm font-semibold text-gray-700 dark:text-slate-300">Virtualized (gallery)</span>
726
+ <ITSlideToggle isOn={dtVirtualized} onToggle={setDtVirtualized} size="sm" />
727
+ </div>
728
+ <div className="flex items-center justify-between">
729
+ <span className="text-sm font-semibold text-gray-700 dark:text-slate-300">Sticky header</span>
730
+ <ITSlideToggle isOn={dtStickyHeader} onToggle={setDtStickyHeader} size="sm" disabled={!dtVirtualized} />
731
+ </div>
732
+ {dtVirtualized && (
733
+ <div className="p-2 bg-slate-100 dark:bg-slate-800 rounded text-xs text-slate-500">
734
+ size, density, layout y auto-cards aplican al demo principal; virtualized y sticky header solo al bloque "Server-side virtualization" de la galería. Ese bloque no recibe autoCardBreakpoint: las cards nunca se virtualizan y el fallback de auto-cards desactivaría la virtualización en silencio.
735
+ </div>
736
+ )}
737
+ </div>
514
738
  </div>
515
739
  }
740
+ doc={{
741
+ summary: "Tabla server-side con fetch, filtros, ordenamiento, paginación y activación de fila/card.",
742
+ examples: [
743
+ '<ITDataTable columns={columns} fetchData={api.fetchUsers} />',
744
+ '<ITDataTable columns={columns} fetchData={api.fetchUsers} onRowClick={(row) => open(row)} />',
745
+ '<ITDataTable columns={fixedColumns} fetchData={api.fetchUsers} layout="fixed" density="compact" />',
746
+ '<ITDataTable columns={cols} fetchData={api.page} virtualized stickyHeader defaultItemsPerPage={200} itemsPerPageOptions={[50, 100, 200]} debounceMs={0} />',
747
+ '<ITDataTable columns={cols} fetchData={api.page} layout="fixed" density="compact" autoCardBreakpoint={640} />',
748
+ ],
749
+ props: [
750
+ { name: "columns", type: "Column<T>[]", description: "Definiciones de columna." },
751
+ { name: "fetchData", type: "(params: ITDataTableFetchParams) => Promise<ITDataTableResponse<T>>", required: true, description: "Callback asíncrono disparado al cambiar página, filtros u orden. Retorna { data, total }." },
752
+ { name: "debounceMs", type: "number", default: "500", description: "Espera antes de refetch tras cambiar filtros." },
753
+ { name: "externalFilters", type: "Record<string, string | number | boolean | Date>", default: "{}", description: "Filtros externos fusionados con los internos." },
754
+ { name: "loadingIndicator", type: "ReactNode", description: "Elemento custom mostrado mientras carga." },
755
+ { name: "fetchOnMount", type: "boolean", default: "true", description: "Dispara fetch al montar." },
756
+ { name: "reloadTrigger", type: "number | string | boolean", description: "Cambiar su valor fuerza un refetch." },
757
+ { name: "containerClassName", type: "string", description: "Clases del contenedor externo." },
758
+ { name: "className", type: "string", description: "Clases aplicadas al elemento <table>." },
759
+ { name: "variant", type: '"default" | "striped" | "bordered" | "minimal"', default: '"default"', description: "Variante visual. minimal no tiene estilos (igual que default)." },
760
+ { name: "size", type: '"sm" | "md" | "lg"', default: '"md"', description: "Tamaño de fuente; no cambia padding (usar density)." },
761
+ { name: "itemsPerPageOptions", type: "number[]", default: "[5, 10, 20]", description: "Opciones del selector de filas por página." },
762
+ { name: "defaultItemsPerPage", type: "number", default: "10", description: "Filas por página iniciales." },
763
+ { name: "title", type: "string | ReactNode", description: "Título sobre la tabla." },
764
+ { name: "renderCard", type: "(row: T) => ReactNode", description: "Renderer custom de card." },
765
+ { name: "defaultView", type: '"table" | "cards"', default: '"table"', description: "Vista inicial." },
766
+ { name: "showVerticalBorder", type: "boolean", default: "true", description: "Bordes verticales entre columnas." },
767
+ { name: "verticalBorderClassname", type: "string", description: "Clases custom de los bordes verticales." },
768
+ { name: "onRowClick", type: "(row: T, event: MouseEvent<HTMLElement> | KeyboardEvent<HTMLElement>) => void", description: "Activa fila/card. MouseEvent en puntero, KeyboardEvent en Enter/Space. No se dispara mientras carga y se ignora en hijos interactivos." },
769
+ { name: "layout", type: '"auto" | "fixed"', default: '"auto"', description: "fixed respeta width de columna y elimina min-w, evitando scroll horizontal." },
770
+ { name: "density", type: '"compact" | "normal" | "comfortable"', default: '"normal"', description: "Padding / alto de fila (independiente de size, que solo cambia font-size)." },
771
+ { name: "autoCardBreakpoint", type: "number", default: "0", description: "Ancho de contenedor (px) bajo el cual cae a la vista cards. 0 desactiva." },
772
+ { name: "virtualized", type: "boolean", default: "false", description: "Vista tabla: renderiza solo la ventana visible con spacer rows. Cards nunca virtualiza." },
773
+ { name: "virtualizedMaxHeight", type: "number", default: "400", description: "Alto máx (px) del contenedor con scroll virtual." },
774
+ { name: "rowHeight", type: "number", default: "getRowHeight(size, density)", description: "Alto de fila asumido (px). Size-aware: baseline size=\"md\" = 33/45/53 según densidad; size lo desplaza por el line box (sm 16 / md 20 / lg 28 px)." },
775
+ { name: "overscan", type: "number", default: "5", description: "Filas extra renderizadas arriba/abajo del viewport." },
776
+ { name: "stickyHeader", type: "boolean", default: "false", description: "Header sticky mientras scrollea el cuerpo virtualizado." },
777
+ ],
778
+ notes: [
779
+ "onRowClick funciona en ambas vistas (tabla y cards).",
780
+ "Interacciones que inician en botones, links o inputs no disparan onRowClick.",
781
+ "Column acepta width (number=px | string CSS), minWidth (px), align (left/center/right) y truncate (ellipsis + title nativo).",
782
+ "Anti scroll horizontal: layout=\"fixed\" + width por columna + truncate + density.",
783
+ "Virtualización: solo vista tabla; rinde con page size grande; filas de alto uniforme; el scroll no se reinicia al cambiar filtros.",
784
+ "La virtualización server-side hace ventana sobre la PÁGINA ACTUAL (data.length), no sobre total: usar un defaultItemsPerPage grande (p. ej. 200) con itemsPerPageOptions que lo incluya, o el efecto es invisible.",
785
+ "Virtualización exige alto de fila uniforme. El default es size-aware (getRowHeight(size, density)): con size=\"md\" el baseline es compact 33 / normal 45 / comfortable 53 (padding de celda + line box text-sm de 20px + 1px del divide-y); size lo desplaza por el line box (sm 16 / md 20 / lg 28 px, es decir −4 / 0 / +8 px). Una celda de dos líneas (avatar + texto) mide ~50px real y desalinea los spacers: usar celdas de una sola línea o pasar un rowHeight explícito.",
786
+ "autoCardBreakpoint fuerza la vista cards bajo el ancho dado, y la virtualización se desactiva en silencio al pasar a cards: no combinar virtualized + autoCardBreakpoint.",
787
+ "El toggle interno Table/Cards también desactiva la virtualización; las cards nunca se virtualizan.",
788
+ "debounceMs={0} evita el retardo de refetch en demos.",
789
+ "variant=\"minimal\" se acepta por tipo pero no tiene estilos (igual que default). ITSearchTable no soporta layout/density/autoCardBreakpoint/virtualized/width.",
790
+ ],
791
+ }}
516
792
  gallery={
517
793
  <div className="space-y-6">
518
794
  <div>
@@ -568,6 +844,27 @@ export const DataTableShowcase = () => {
568
844
  />
569
845
  </div>
570
846
  </div>
847
+
848
+ <div>
849
+ <h4 className="text-sm font-bold text-slate-700 dark:text-slate-200 mb-3">Server-side virtualization — 240 rows {dtVirtualized ? "(page 200)" : "(page 10)"}</h4>
850
+ <div className="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden">
851
+ <ITDataTable
852
+ key={dtVirtualized ? "dt-virt-on" : "dt-virt-off"}
853
+ columns={virtualUserColumns}
854
+ fetchData={virtualFetchData}
855
+ title="Virtualized users"
856
+ defaultItemsPerPage={dtVirtualized ? 200 : 10}
857
+ itemsPerPageOptions={dtVirtualized ? [50, 100, 200] : [5, 10, 20]}
858
+ size={dtSize}
859
+ virtualized={dtVirtualized}
860
+ virtualizedMaxHeight={360}
861
+ stickyHeader={dtStickyHeader}
862
+ layout={dtLayout}
863
+ density={dtDensity}
864
+ debounceMs={0}
865
+ />
866
+ </div>
867
+ </div>
571
868
  </div>
572
869
  }
573
870
  />
@@ -397,6 +397,15 @@ export const PageShowcase = () => {
397
397
  const [screen, setScreen] = useState<"list" | "detail">("list");
398
398
  const [selectedUser, setSelectedUser] = useState<Record<string, unknown> | null>(null);
399
399
  const [showIcon, setShowIcon] = useState(true);
400
+ const [hPadding, setHPadding] = useState<"default" | "none" | "tight" | "wide">("default");
401
+
402
+ // `undefined` lets ITPage fall back to its own default ("px-4 sm:px-6 lg:px-8").
403
+ const hPaddingValue: string | undefined = {
404
+ default: undefined,
405
+ none: "px-0",
406
+ tight: "px-2 sm:px-3",
407
+ wide: "px-8 sm:px-12 lg:px-16",
408
+ }[hPadding];
400
409
 
401
410
  const isNormal = state === "normal";
402
411
  const isDetail = screen === "detail";
@@ -551,6 +560,7 @@ export const PageShowcase = () => {
551
560
  error={state === "error" ? "Could not load user details. Please try again." : null}
552
561
  errorTitle="Details error"
553
562
  onRetry={() => setState("normal")}
563
+ horizontalPadding={hPaddingValue}
554
564
  empty={state === "empty"}
555
565
  emptyTitle="No user data"
556
566
  emptyDescription="No information available for this user."
@@ -601,6 +611,7 @@ export const PageShowcase = () => {
601
611
  error={state === "error" ? "Could not load team members. Check your connection and try again." : null}
602
612
  errorTitle="Connection error"
603
613
  onRetry={() => setState("normal")}
614
+ horizontalPadding={hPaddingValue}
604
615
  empty={state === "empty"}
605
616
  emptyTitle="No team members yet"
606
617
  emptyDescription="Invite your first team member to get started."
@@ -640,6 +651,21 @@ export const PageShowcase = () => {
640
651
  <ITSlideToggle isOn={showIcon} onToggle={setShowIcon} size="sm" />
641
652
  </ITFlex>
642
653
 
654
+ <div>
655
+ <p className="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-2">Horizontal padding</p>
656
+ <ITFlex gap={2} wrap="wrap">
657
+ {(["default", "none", "tight", "wide"] as const).map((p) => (
658
+ <ITButton
659
+ key={p}
660
+ label={p === "default" ? "Default" : p === "none" ? "px-0" : p === "tight" ? "px-2" : "px-8"}
661
+ variant={hPadding === p ? "filled" : "outlined"}
662
+ size="sm"
663
+ onClick={() => setHPadding(p)}
664
+ />
665
+ ))}
666
+ </ITFlex>
667
+ </div>
668
+
643
669
  {isNormal && (
644
670
  <div>
645
671
  <p className="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-2">Screen</p>
@@ -674,6 +700,7 @@ export const PageShowcase = () => {
674
700
  <h4 className="text-sm font-bold text-slate-700 dark:text-slate-200 mb-3">Users — table with actions</h4>
675
701
  <div className="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden">
676
702
  <ITPage
703
+ horizontalPadding={hPaddingValue}
677
704
  title="Users"
678
705
  description="1,245 registered · 38 active now"
679
706
  icon={<FaUsers size={20} />}
@@ -701,6 +728,7 @@ export const PageShowcase = () => {
701
728
  <h4 className="text-sm font-bold text-slate-700 dark:text-slate-200 mb-3">Product detail</h4>
702
729
  <div className="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden">
703
730
  <ITPage
731
+ horizontalPadding={hPaddingValue}
704
732
  title="MacBook Pro 16'' M3 Max"
705
733
  description="SKU: MBP-16-M3MAX-2024 · Stock: 24"
706
734
  icon={<FaBoxOpen size={20} />}
@@ -748,6 +776,7 @@ export const PageShowcase = () => {
748
776
  <h4 className="text-sm font-bold text-slate-700 dark:text-slate-200 mb-3">Edit profile</h4>
749
777
  <div className="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden">
750
778
  <ITPage
779
+ horizontalPadding={hPaddingValue}
751
780
  title="Edit profile"
752
781
  description="Update your personal information and contact details"
753
782
  icon={<FaEdit size={20} />}
@@ -798,6 +827,7 @@ export const PageShowcase = () => {
798
827
  <h4 className="text-sm font-bold text-slate-700 dark:text-slate-200 mb-3">Orders — status table</h4>
799
828
  <div className="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden">
800
829
  <ITPage
830
+ horizontalPadding={hPaddingValue}
801
831
  title="Orders"
802
832
  description="89 pending · 1,247 completed this month"
803
833
  icon={<FaShoppingCart size={20} />}
@@ -850,6 +880,7 @@ export const PageShowcase = () => {
850
880
  <h4 className="text-sm font-bold text-slate-700 dark:text-slate-200 mb-3">Settings</h4>
851
881
  <div className="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden">
852
882
  <ITPage
883
+ horizontalPadding={hPaddingValue}
853
884
  title="Settings"
854
885
  description="Manage your account and workspace preferences"
855
886
  icon={<FaCog size={20} />}
@@ -901,13 +901,20 @@ export const TextShowcase = () => {
901
901
 
902
902
  // 3. ITLayout & ITNavbar Showcase
903
903
  export const LayoutShowcase = () => {
904
- const [collapsed, setCollapsed] = useState(false);
904
+ const [collapsed, setCollapsed] = useState(true);
905
905
  const [activeId, setActiveId] = useState("dashboard");
906
906
  const [navActiveId, setNavActiveId] = useState<string | null>("1");
907
907
 
908
908
  const topBar = {
909
909
  logo: <div className="w-9 h-9 rounded-lg bg-primary-500 flex items-center justify-center text-white font-bold">A</div>,
910
910
  logoText: "AXZY Console",
911
+ centerContent: (
912
+ <input
913
+ type="search"
914
+ placeholder="Buscar componente…"
915
+ className="w-full rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-3 py-1.5 text-xs text-slate-700 dark:text-slate-200 outline-none focus:ring-2 focus:ring-primary-500/40"
916
+ />
917
+ ),
911
918
  navItems: [
912
919
  { id: "home", label: "Inicio", icon: <FaHome />, action: () => setActiveId("home") },
913
920
  { id: "docs", label: "Documentos", icon: <FaFileAlt />, action: () => setActiveId("docs") },
@@ -942,19 +949,29 @@ export const LayoutShowcase = () => {
942
949
  { id: "settings", label: "Configuración", icon: <FaCog />, isActive: activeId === "settings", action: () => setActiveId("settings") },
943
950
  ],
944
951
  isCollapsed: collapsed,
945
- onToggleCollapse: () => setCollapsed(v => !v),
952
+ header: (
953
+ <ITText
954
+ className="text-xs font-bold tracking-wide"
955
+ style={{ color: "var(--it-sidebar-label-color, var(--color-secondary-700))" }}
956
+ >
957
+ AXZY v1.3
958
+ </ITText>
959
+ ),
946
960
  };
947
961
 
948
962
  const code = `<ITLayout
949
963
  topBar={{
950
964
  logo: <Logo />,
951
965
  logoText: "AXZY Console",
966
+ // Slot central opcional (visible desde lg) — ideal para un buscador global
967
+ centerContent: <SearchBox value={term} onChange={setTerm} />,
952
968
  userMenu: { userName, userEmail, menuItems }
953
969
  }}
954
970
  sidebar={{
955
971
  navigationItems: [...],
956
972
  isCollapsed: ${collapsed},
957
- onToggleCollapse: () => setCollapsed(v => !v)
973
+ // Slot opcional sobre la navegación (oculto al colapsar)
974
+ header: <span>AXZY v1.3</span>
958
975
  }}
959
976
  >
960
977
  {/* Tu contenido */}
@@ -1025,7 +1042,7 @@ export const LayoutShowcase = () => {
1025
1042
  <ITSlideToggle isOn={collapsed} onToggle={setCollapsed} size="sm" />
1026
1043
  </div>
1027
1044
  <div className="text-xs text-slate-500">
1028
- En móvil (&lt;lg) el sidebar se abre como drawer con un fondo oscuro. Usa el botón ☰ del topbar.
1045
+ En móvil (&lt;lg) el sidebar se abre como drawer con un fondo oscuro. Usa el botón ☰ del topbar. El botón ⟩ del propio sidebar también colapsa/expande.
1029
1046
  </div>
1030
1047
  </ITStack>
1031
1048
  }
@@ -1088,6 +1105,27 @@ export const LayoutShowcase = () => {
1088
1105
  </div>
1089
1106
  </ITStack>
1090
1107
  }
1108
+ doc={{
1109
+ summary: "Chasis estructural del portal: topbar + sidebar colapsable + área de contenido.",
1110
+ examples: [
1111
+ '<ITLayout topBar={{ centerContent: <SearchBox /> }} sidebar={{ navigationItems }}>',
1112
+ " <Dashboard />",
1113
+ "</ITLayout>",
1114
+ ],
1115
+ props: [
1116
+ { name: "topBar", type: "ITTopBarProps", required: true, description: "Configuración de la barra superior." },
1117
+ { name: "sidebar", type: "ITSidebarProps", required: true, description: "Configuración del sidebar (navigationItems, isCollapsed, header…)." },
1118
+ { name: "children", type: "React.ReactNode", required: true, description: "Contenido principal." },
1119
+ { name: "className", type: "string", description: "Clases del wrapper externo." },
1120
+ { name: "contentClassName", type: "string", description: "Clases del contenedor de contenido." },
1121
+ { name: "sidebar.header", type: "React.ReactNode", description: "Slot opcional sobre la navegación (ej. wordmark o filtro). Oculto al colapsar." },
1122
+ ],
1123
+ notes: [
1124
+ "El sidebar hereda aria-current/aria-expanded y acepta un slot header opcional.",
1125
+ "Con `sidebar.isCollapsed` omitido el contenido reserva 88px (hover expande sobre el contenido). Si lo controlas a `false`, reserva 280px y no tapa el contenido.",
1126
+ "El riel colapsado se expande automáticamente al pasar el cursor por encima; no hay botón de colapso.",
1127
+ ],
1128
+ }}
1091
1129
  />
1092
1130
  );
1093
1131
  };