@axzydev/axzy_ui_system 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/index.cjs +2342 -1715
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +286 -9
- package/dist/index.d.ts +286 -9
- package/dist/index.js +2164 -1538
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/App.tsx +153 -153
- package/src/components/molecules/calendar/calendar.doc.mdx +2 -1
- package/src/components/molecules/calendar/calendar.tsx +102 -24
- package/src/components/molecules/masked-input/masked-input.doc.mdx +78 -0
- package/src/components/molecules/masked-input/masked-input.props.ts +58 -0
- package/src/components/molecules/masked-input/masked-input.stories.tsx +126 -0
- package/src/components/molecules/masked-input/masked-input.tsx +244 -0
- package/src/components/molecules/table/table.doc.mdx +64 -21
- package/src/components/molecules/table/table.props.ts +81 -2
- package/src/components/molecules/table/table.stories.tsx +155 -0
- package/src/components/molecules/table/table.tsx +177 -36
- package/src/components/organisms/data-table/ITDataTable.doc.mdx +37 -3
- package/src/components/organisms/data-table/ITDataTable.stories.tsx +43 -0
- package/src/components/organisms/data-table/dataTable.props.ts +64 -0
- package/src/components/organisms/data-table/dataTable.tsx +240 -82
- package/src/components/organisms/sidebar/sidebar.doc.mdx +5 -0
- package/src/components/organisms/sidebar/sidebar.props.ts +5 -0
- package/src/components/organisms/sidebar/sidebar.stories.tsx +1 -1
- package/src/components/organisms/sidebar/sidebar.tsx +27 -7
- package/src/components/organisms/topbar/topbar.doc.mdx +3 -0
- package/src/components/organisms/topbar/topbar.props.ts +16 -0
- package/src/components/organisms/topbar/topbar.tsx +133 -98
- package/src/components/templates/layout/layout.doc.mdx +2 -0
- package/src/components/templates/layout/layout.tsx +7 -1
- package/src/components/templates/page/page.doc.mdx +8 -2
- package/src/components/templates/page/page.props.ts +11 -1
- package/src/components/templates/page/page.tsx +11 -1
- package/src/dev.css +58 -1
- package/src/hooks/useElementSize.ts +46 -0
- package/src/hooks/useVirtualRows.ts +123 -0
- package/src/index.ts +6 -2
- package/src/sandbox/LandingShowcase.tsx +465 -0
- package/src/sandbox/NotFoundShowcase.tsx +70 -0
- package/src/sandbox/TopbarSearch.tsx +85 -0
- package/src/sandbox/navigation.ts +148 -0
- package/src/sandbox/useHashRoute.ts +149 -0
- package/src/showcases/DataShowcases.tsx +321 -24
- package/src/showcases/FormShowcases.tsx +178 -0
- package/src/showcases/PageShowcases.tsx +31 -0
- package/src/showcases/StructureShowcases.tsx +42 -4
- package/src/types/table.types.ts +48 -0
- package/src/utils/styles.ts +9 -4
- package/src/utils/table.utils.ts +31 -0
|
@@ -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
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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 (< 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={
|
|
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
|
|
429
|
-
<
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
/>
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
ITFormBuilder,
|
|
14
14
|
ITWysiwyg,
|
|
15
15
|
ITField,
|
|
16
|
+
ITMaskedInput,
|
|
16
17
|
ITMultiSelect,
|
|
17
18
|
ITChipInput,
|
|
18
19
|
ITBadget,
|
|
@@ -1913,3 +1914,180 @@ export const ChipInputShowcase = () => {
|
|
|
1913
1914
|
/>
|
|
1914
1915
|
);
|
|
1915
1916
|
};
|
|
1917
|
+
|
|
1918
|
+
// 15. ITMaskedInput Showcase
|
|
1919
|
+
const MaskedDemo = ({
|
|
1920
|
+
title,
|
|
1921
|
+
mask,
|
|
1922
|
+
pattern,
|
|
1923
|
+
hint,
|
|
1924
|
+
initial,
|
|
1925
|
+
}: {
|
|
1926
|
+
title: string;
|
|
1927
|
+
mask: string;
|
|
1928
|
+
pattern?: RegExp;
|
|
1929
|
+
hint?: string;
|
|
1930
|
+
initial?: string;
|
|
1931
|
+
}) => {
|
|
1932
|
+
const [raw, setRaw] = useState<string>(initial ?? "");
|
|
1933
|
+
const [complete, setComplete] = useState<string | null>(null);
|
|
1934
|
+
return (
|
|
1935
|
+
<div>
|
|
1936
|
+
<h4 className="text-sm font-bold text-slate-700 mb-2">{title}</h4>
|
|
1937
|
+
<ITMaskedInput
|
|
1938
|
+
name={title.toLowerCase().replace(/\s+/g, "_")}
|
|
1939
|
+
label={mask}
|
|
1940
|
+
mask={mask}
|
|
1941
|
+
pattern={pattern}
|
|
1942
|
+
value={raw}
|
|
1943
|
+
onChange={(e) => {
|
|
1944
|
+
setRaw(e.target.value);
|
|
1945
|
+
setComplete(null);
|
|
1946
|
+
}}
|
|
1947
|
+
onComplete={(v) => setComplete(v)}
|
|
1948
|
+
/>
|
|
1949
|
+
<p className="mt-1.5 text-xs font-mono text-slate-500">
|
|
1950
|
+
raw: {raw || "(vacío)"}
|
|
1951
|
+
{complete && <span className="text-emerald-600 ml-2">✓ completo</span>}
|
|
1952
|
+
{hint && <span className="text-slate-400 ml-2">· {hint}</span>}
|
|
1953
|
+
</p>
|
|
1954
|
+
</div>
|
|
1955
|
+
);
|
|
1956
|
+
};
|
|
1957
|
+
|
|
1958
|
+
export const MaskedInputShowcase = () => {
|
|
1959
|
+
const [code, setCode] = useState<string>("");
|
|
1960
|
+
const [variant, setVariant] = useState<any>("primary");
|
|
1961
|
+
const [size, setSize] = useState<any>("md");
|
|
1962
|
+
const [disabled, setDisabled] = useState(false);
|
|
1963
|
+
const [withError, setWithError] = useState(false);
|
|
1964
|
+
|
|
1965
|
+
const codeExample = `<ITMaskedInput\n name="code"\n label="Código de activación"\n mask="xxxx-xxxx-xxxx"\n variant="${variant}"\n size="${size}"\n disabled={${disabled}}\n onChange={(e) => setCode(e.target.value)}\n/>`;
|
|
1966
|
+
|
|
1967
|
+
return (
|
|
1968
|
+
<ShowcaseLayout
|
|
1969
|
+
title="ITMaskedInput"
|
|
1970
|
+
description="Input de texto con máscara de formato: teclea solo caracteres útiles y los separadores se insertan solos (xxxx-xxxx-xxxx, (999) 999-9999, etc.)."
|
|
1971
|
+
code={codeExample}
|
|
1972
|
+
demo={
|
|
1973
|
+
<div className="w-full max-w-sm">
|
|
1974
|
+
<ITMaskedInput
|
|
1975
|
+
name="showcase_masked"
|
|
1976
|
+
label="Código de activación"
|
|
1977
|
+
mask="xxxx-xxxx-xxxx"
|
|
1978
|
+
value={code}
|
|
1979
|
+
variant={variant}
|
|
1980
|
+
size={size}
|
|
1981
|
+
disabled={disabled}
|
|
1982
|
+
error={withError}
|
|
1983
|
+
onChange={(e) => setCode(e.target.value)}
|
|
1984
|
+
/>
|
|
1985
|
+
{code && (
|
|
1986
|
+
<p className="mt-2 text-xs text-slate-500 font-mono">
|
|
1987
|
+
Valor actual (raw): {code || "—"}
|
|
1988
|
+
</p>
|
|
1989
|
+
)}
|
|
1990
|
+
</div>
|
|
1991
|
+
}
|
|
1992
|
+
controls={
|
|
1993
|
+
<>
|
|
1994
|
+
<ITSelect
|
|
1995
|
+
name="masked_variant_ctrl"
|
|
1996
|
+
label="Tema de Color"
|
|
1997
|
+
value={variant}
|
|
1998
|
+
onChange={(e: any) => setVariant(e.target.value)}
|
|
1999
|
+
options={[
|
|
2000
|
+
{ label: "Primary", value: "primary" },
|
|
2001
|
+
{ label: "Secondary", value: "secondary" },
|
|
2002
|
+
{ label: "Success", value: "success" },
|
|
2003
|
+
{ label: "Danger", value: "danger" },
|
|
2004
|
+
{ label: "Warning", value: "warning" },
|
|
2005
|
+
{ label: "Info", value: "info" },
|
|
2006
|
+
]}
|
|
2007
|
+
/>
|
|
2008
|
+
<ITSelect
|
|
2009
|
+
name="masked_size_ctrl"
|
|
2010
|
+
label="Tamaño"
|
|
2011
|
+
value={size}
|
|
2012
|
+
onChange={(e: any) => setSize(e.target.value)}
|
|
2013
|
+
options={[
|
|
2014
|
+
{ label: "Small", value: "sm" },
|
|
2015
|
+
{ label: "Medium", value: "md" },
|
|
2016
|
+
{ label: "Large", value: "lg" },
|
|
2017
|
+
]}
|
|
2018
|
+
/>
|
|
2019
|
+
<div className="flex items-center justify-between pt-2">
|
|
2020
|
+
<span className="text-sm font-semibold text-gray-700">Deshabilitado</span>
|
|
2021
|
+
<ITSlideToggle isOn={disabled} onToggle={setDisabled} activeColor="danger" size="sm" />
|
|
2022
|
+
</div>
|
|
2023
|
+
<div className="flex items-center justify-between">
|
|
2024
|
+
<span className="text-sm font-semibold text-gray-700">Error</span>
|
|
2025
|
+
<ITSlideToggle isOn={withError} onToggle={setWithError} activeColor="danger" size="sm" />
|
|
2026
|
+
</div>
|
|
2027
|
+
</>
|
|
2028
|
+
}
|
|
2029
|
+
gallery={
|
|
2030
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
2031
|
+
<MaskedDemo
|
|
2032
|
+
title="Código alfanumérico"
|
|
2033
|
+
mask="xxxx-xxxx-xxxx"
|
|
2034
|
+
hint="Escribe: ABC1-2345"
|
|
2035
|
+
/>
|
|
2036
|
+
<MaskedDemo
|
|
2037
|
+
title="Número de tarjeta"
|
|
2038
|
+
mask="9999-9999-9999-9999"
|
|
2039
|
+
hint="Escribe 16 dígitos"
|
|
2040
|
+
/>
|
|
2041
|
+
<MaskedDemo
|
|
2042
|
+
title="Teléfono"
|
|
2043
|
+
mask="(999) 999-9999"
|
|
2044
|
+
pattern={/^\d{10}$/}
|
|
2045
|
+
hint="Regex /^\\d{10}$/"
|
|
2046
|
+
/>
|
|
2047
|
+
<MaskedDemo
|
|
2048
|
+
title="Código postal"
|
|
2049
|
+
mask="99999"
|
|
2050
|
+
hint="Ejemplo: 44100"
|
|
2051
|
+
/>
|
|
2052
|
+
<MaskedDemo
|
|
2053
|
+
title="CVV / OTP"
|
|
2054
|
+
mask="999"
|
|
2055
|
+
hint="3 dígitos, onComplete"
|
|
2056
|
+
/>
|
|
2057
|
+
<MaskedDemo
|
|
2058
|
+
title="Clave con letras en MAYÚSCULA"
|
|
2059
|
+
mask="A-A-A-999"
|
|
2060
|
+
hint="Token A auto-mayúscula"
|
|
2061
|
+
initial="ABC123"
|
|
2062
|
+
/>
|
|
2063
|
+
</div>
|
|
2064
|
+
}
|
|
2065
|
+
doc={{
|
|
2066
|
+
summary: "Input con máscara: value/onChange entregan solo los caracteres útiles y los separadores se auto-insertan.",
|
|
2067
|
+
description:
|
|
2068
|
+
"Define el patrón con tokens (9=dígito, A=letra mayúscula, a=letra, x=alfanumérico, *=cualquiera) y literales (todo lo demás). Los huecos vacíos se pintan con placeholderChar ('_').",
|
|
2069
|
+
examples: [
|
|
2070
|
+
'<ITMaskedInput name="code" mask="xxxx-xxxx-xxxx" onChange={(e) => setCode(e.target.value)} />',
|
|
2071
|
+
'<ITMaskedInput name="phone" mask="(999) 999-9999" pattern={/^\\d{10}$/} onChange={setPhone} />',
|
|
2072
|
+
'<ITMaskedInput name="card" mask="9999-9999-9999-9999" onComplete={validateCard} />',
|
|
2073
|
+
],
|
|
2074
|
+
props: [
|
|
2075
|
+
{ name: "mask", type: "string", required: true, description: "Patrón de la máscara (tokens + literales)." },
|
|
2076
|
+
{ name: "value", type: "string", description: "Valor limpio (raw): solo caracteres útiles." },
|
|
2077
|
+
{ name: "onChange", type: "(e) => void", description: "e.target.value = raw sin separadores." },
|
|
2078
|
+
{ name: "onComplete", type: "(value) => void", description: "Dispara al llenar toda la máscara." },
|
|
2079
|
+
{ name: "pattern", type: "RegExp", description: "Validación full-match del raw en blur." },
|
|
2080
|
+
{ name: "placeholderChar", type: "string", default: '"_"', description: "Relleno de slots vacíos." },
|
|
2081
|
+
{ name: "variant", type: "ColorsTypes", default: '"primary"', description: "Color de acento." },
|
|
2082
|
+
{ name: "size", type: '"sm" | "md" | "lg"', default: '"md"', description: "Tamaño del input." },
|
|
2083
|
+
{ name: "error", type: "string | boolean", description: "Estado de error." },
|
|
2084
|
+
],
|
|
2085
|
+
notes: [
|
|
2086
|
+
"Los tokens A guardan letras en mayúsculas automáticamente.",
|
|
2087
|
+
"Borrar un separador lo vuelve a insertar; el cursor se conserva en la posición correcta.",
|
|
2088
|
+
"El cursor se preserva también al editar en medio de la máscara.",
|
|
2089
|
+
],
|
|
2090
|
+
}}
|
|
2091
|
+
/>
|
|
2092
|
+
);
|
|
2093
|
+
};
|