neogestify-ui-components 2.2.2 → 2.3.0
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/LICENSE +21 -0
- package/README.md +803 -349
- package/dist/components/ElementLibraryBuilder/index.js +299 -120
- package/dist/components/ElementLibraryBuilder/index.js.map +1 -1
- package/dist/components/ElementLibraryBuilder/index.mjs +300 -121
- package/dist/components/ElementLibraryBuilder/index.mjs.map +1 -1
- package/dist/components/VenueMapEditor/index.js.map +1 -1
- package/dist/components/VenueMapEditor/index.mjs.map +1 -1
- package/dist/components/alerts/index.js +108 -51
- package/dist/components/alerts/index.js.map +1 -1
- package/dist/components/alerts/index.mjs +109 -52
- package/dist/components/alerts/index.mjs.map +1 -1
- package/dist/components/html/index.d.mts +101 -22
- package/dist/components/html/index.d.ts +101 -22
- package/dist/components/html/index.js +506 -166
- package/dist/components/html/index.js.map +1 -1
- package/dist/components/html/index.mjs +507 -167
- package/dist/components/html/index.mjs.map +1 -1
- package/dist/components/icons/index.d.mts +5 -1
- package/dist/components/icons/index.d.ts +5 -1
- package/dist/components/icons/index.js +16 -0
- package/dist/components/icons/index.js.map +1 -1
- package/dist/components/icons/index.mjs +13 -1
- package/dist/components/icons/index.mjs.map +1 -1
- package/dist/context/theme/index.js +59 -37
- package/dist/context/theme/index.js.map +1 -1
- package/dist/context/theme/index.mjs +59 -37
- package/dist/context/theme/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +510 -166
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +508 -168
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/html/Button.tsx +84 -38
- package/src/components/html/Form.tsx +33 -13
- package/src/components/html/Input.tsx +110 -31
- package/src/components/html/Loading.tsx +58 -33
- package/src/components/html/Modal.tsx +67 -20
- package/src/components/html/Select.tsx +92 -39
- package/src/components/html/Table.tsx +274 -50
- package/src/components/html/TextArea.tsx +81 -31
- package/src/components/icons/icons.tsx +32 -0
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import { type ReactNode, type CSSProperties } from 'react';
|
|
2
|
+
import { SortAscIcon, SortDescIcon, SortBothIcon } from '../icons/icons';
|
|
3
|
+
|
|
4
|
+
type TableVariant =
|
|
5
|
+
| 'default'
|
|
6
|
+
| 'striped'
|
|
7
|
+
| 'bordered'
|
|
8
|
+
| 'minimal'
|
|
9
|
+
| 'ghost'
|
|
10
|
+
| 'card'
|
|
11
|
+
| 'accent'
|
|
12
|
+
| 'dark'
|
|
13
|
+
| 'custom';
|
|
2
14
|
|
|
3
|
-
type TableVariant = 'default' | 'striped' | 'bordered' | 'minimal' | 'custom';
|
|
4
15
|
type TableSize = 'sm' | 'md' | 'lg';
|
|
5
16
|
|
|
17
|
+
interface SortState {
|
|
18
|
+
key: string;
|
|
19
|
+
direction: 'asc' | 'desc';
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
interface ColumnDef {
|
|
7
23
|
/** Contenido del encabezado */
|
|
8
24
|
header: ReactNode;
|
|
@@ -10,67 +26,105 @@ interface ColumnDef {
|
|
|
10
26
|
className?: string;
|
|
11
27
|
/** Alinear el contenido de esta columna */
|
|
12
28
|
align?: 'left' | 'center' | 'right';
|
|
29
|
+
/** Ancho fijo (px, %, rem…) */
|
|
30
|
+
width?: string | number;
|
|
31
|
+
/** Ancho mínimo */
|
|
32
|
+
minWidth?: string | number;
|
|
33
|
+
/** Fija esta columna a la izquierda durante scroll horizontal */
|
|
34
|
+
sticky?: boolean;
|
|
35
|
+
/** Estilos inline exclusivos para <th> */
|
|
36
|
+
thStyle?: CSSProperties;
|
|
37
|
+
/** Estilos inline exclusivos para <td> */
|
|
38
|
+
tdStyle?: CSSProperties;
|
|
39
|
+
/** Muestra indicador de ordenación. Requiere `key` */
|
|
40
|
+
sortable?: boolean;
|
|
41
|
+
/** Clave usada en sortState y onSort */
|
|
42
|
+
key?: string;
|
|
13
43
|
}
|
|
14
44
|
|
|
15
45
|
interface TableProps {
|
|
16
46
|
/**
|
|
17
|
-
* Definición de columnas
|
|
18
|
-
*
|
|
47
|
+
* Definición de columnas. Acepta strings simples o ColumnDef para
|
|
48
|
+
* configuración avanzada (ancho, sticky, sort, etc.).
|
|
19
49
|
*/
|
|
20
50
|
columns: (ColumnDef | ReactNode)[];
|
|
21
51
|
|
|
22
|
-
/** Filas
|
|
52
|
+
/** Filas del cuerpo. Cada fila es un arreglo de celdas. */
|
|
23
53
|
rows: ReactNode[][];
|
|
24
54
|
|
|
25
|
-
/** Estilo visual
|
|
55
|
+
/** Estilo visual. Default: 'default' */
|
|
26
56
|
variant?: TableVariant;
|
|
27
57
|
|
|
28
|
-
/** Tamaño de padding
|
|
58
|
+
/** Tamaño de padding. Default: 'md' */
|
|
29
59
|
size?: TableSize;
|
|
30
60
|
|
|
31
|
-
/** Clase CSS adicional para el wrapper
|
|
61
|
+
/** Clase CSS adicional para el wrapper <div> */
|
|
32
62
|
className?: string;
|
|
33
63
|
|
|
34
|
-
/** Clase CSS adicional para el
|
|
64
|
+
/** Clase CSS adicional para el <table> */
|
|
35
65
|
tableClassName?: string;
|
|
36
66
|
|
|
37
|
-
/** Clase CSS adicional para cada
|
|
67
|
+
/** Clase CSS adicional para cada <th> */
|
|
38
68
|
thClassName?: string;
|
|
39
69
|
|
|
40
|
-
/** Clase CSS adicional para cada
|
|
70
|
+
/** Clase CSS adicional para cada <td> */
|
|
41
71
|
tdClassName?: string;
|
|
42
72
|
|
|
43
|
-
/** Clase CSS adicional
|
|
73
|
+
/** Clase CSS adicional por fila del body */
|
|
44
74
|
trClassName?: string | ((rowIndex: number) => string);
|
|
45
75
|
|
|
46
|
-
/**
|
|
76
|
+
/** Nodo a mostrar cuando rows está vacío */
|
|
47
77
|
emptyState?: ReactNode;
|
|
48
78
|
|
|
49
79
|
/** Callback al hacer click en una fila */
|
|
50
80
|
onRowClick?: (rowIndex: number) => void;
|
|
51
81
|
|
|
52
|
-
/**
|
|
82
|
+
/** Oculta el thead */
|
|
53
83
|
hideHeader?: boolean;
|
|
54
84
|
|
|
55
|
-
/** Estilos inline para el
|
|
85
|
+
/** Estilos inline para el <table> */
|
|
56
86
|
style?: CSSProperties;
|
|
57
|
-
}
|
|
58
87
|
|
|
59
|
-
|
|
88
|
+
/** Fija el thead al hacer scroll vertical */
|
|
89
|
+
stickyHeader?: boolean;
|
|
60
90
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
91
|
+
/** Caption accesible de la tabla */
|
|
92
|
+
caption?: ReactNode;
|
|
64
93
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
94
|
+
/** Filas del <tfoot> */
|
|
95
|
+
footerRows?: ReactNode[][];
|
|
96
|
+
|
|
97
|
+
/** Muestra esqueleto animado en lugar de rows */
|
|
98
|
+
loading?: boolean;
|
|
99
|
+
|
|
100
|
+
/** Número de filas esqueleto cuando loading=true. Default: 4 */
|
|
101
|
+
loadingRows?: number;
|
|
102
|
+
|
|
103
|
+
/** Estilo inline por fila del body */
|
|
104
|
+
getRowStyle?: (rowIndex: number) => CSSProperties;
|
|
105
|
+
|
|
106
|
+
/** Agrega rounded-lg al wrapper */
|
|
107
|
+
rounded?: boolean;
|
|
108
|
+
|
|
109
|
+
/** Agrega sombra al wrapper */
|
|
110
|
+
shadow?: boolean;
|
|
111
|
+
|
|
112
|
+
/** Desactiva el efecto hover en filas. Default: true */
|
|
113
|
+
hoverable?: boolean;
|
|
114
|
+
|
|
115
|
+
/** Estado actual de ordenación */
|
|
116
|
+
sortState?: SortState | null;
|
|
117
|
+
|
|
118
|
+
/** Callback al hacer click en un th sortable */
|
|
119
|
+
onSort?: (key: string) => void;
|
|
68
120
|
}
|
|
69
121
|
|
|
122
|
+
// ─── Lookup tables ────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
70
124
|
const ALIGN_CLASS: Record<string, string> = {
|
|
71
|
-
left:
|
|
125
|
+
left: 'text-left',
|
|
72
126
|
center: 'text-center',
|
|
73
|
-
right:
|
|
127
|
+
right: 'text-right',
|
|
74
128
|
};
|
|
75
129
|
|
|
76
130
|
const SIZE_TH: Record<TableSize, string> = {
|
|
@@ -90,6 +144,10 @@ const VARIANT_TABLE: Record<TableVariant, string> = {
|
|
|
90
144
|
striped: 'w-full min-w-full table-auto',
|
|
91
145
|
bordered: 'w-full min-w-full table-auto border border-gray-300 dark:border-gray-600',
|
|
92
146
|
minimal: 'w-full min-w-full table-auto',
|
|
147
|
+
ghost: 'w-full min-w-full table-auto',
|
|
148
|
+
card: 'w-full min-w-full table-auto',
|
|
149
|
+
accent: 'w-full min-w-full table-auto',
|
|
150
|
+
dark: 'w-full min-w-full table-auto',
|
|
93
151
|
custom: 'w-full min-w-full table-auto',
|
|
94
152
|
};
|
|
95
153
|
|
|
@@ -98,6 +156,10 @@ const VARIANT_THEAD: Record<TableVariant, string> = {
|
|
|
98
156
|
striped: 'bg-gray-100 dark:bg-gray-700',
|
|
99
157
|
bordered: 'bg-gray-100 dark:bg-gray-700',
|
|
100
158
|
minimal: '',
|
|
159
|
+
ghost: '',
|
|
160
|
+
card: 'bg-gray-50 dark:bg-gray-800/80',
|
|
161
|
+
accent: 'bg-blue-600 dark:bg-blue-700',
|
|
162
|
+
dark: 'bg-gray-800 dark:bg-gray-900',
|
|
101
163
|
custom: '',
|
|
102
164
|
};
|
|
103
165
|
|
|
@@ -106,14 +168,49 @@ const VARIANT_TH: Record<TableVariant, string> = {
|
|
|
106
168
|
striped: 'font-semibold uppercase tracking-wider text-gray-600 dark:text-gray-300',
|
|
107
169
|
bordered: 'font-semibold uppercase tracking-wider text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600',
|
|
108
170
|
minimal: 'font-semibold text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700',
|
|
171
|
+
ghost: 'font-semibold text-gray-600 dark:text-gray-400 border-b-2 border-gray-300 dark:border-gray-600',
|
|
172
|
+
card: 'font-semibold text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-700',
|
|
173
|
+
accent: 'font-semibold uppercase tracking-wider text-white',
|
|
174
|
+
dark: 'font-semibold uppercase tracking-wider text-gray-100',
|
|
109
175
|
custom: '',
|
|
110
176
|
};
|
|
111
177
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
178
|
+
// Base row bg per variant (overridden by stripe for 'striped')
|
|
179
|
+
const VARIANT_TR_BASE: Record<TableVariant, string> = {
|
|
180
|
+
default: 'bg-white dark:bg-gray-800',
|
|
181
|
+
striped: '',
|
|
182
|
+
bordered: 'bg-white dark:bg-gray-800',
|
|
183
|
+
minimal: '',
|
|
184
|
+
ghost: '',
|
|
185
|
+
card: 'bg-white dark:bg-gray-900',
|
|
186
|
+
accent: 'bg-white dark:bg-gray-800',
|
|
187
|
+
dark: 'bg-white dark:bg-gray-800',
|
|
188
|
+
custom: '',
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const VARIANT_TR_HOVER: Record<TableVariant, string> = {
|
|
192
|
+
default: 'hover:bg-gray-50 dark:hover:bg-gray-700/60',
|
|
193
|
+
striped: 'hover:bg-blue-50 dark:hover:bg-blue-900/20',
|
|
194
|
+
bordered: 'hover:bg-gray-50 dark:hover:bg-gray-700/60',
|
|
195
|
+
minimal: 'hover:bg-gray-50 dark:hover:bg-gray-800/60',
|
|
196
|
+
ghost: 'hover:bg-gray-50/70 dark:hover:bg-white/5',
|
|
197
|
+
card: 'hover:bg-gray-50 dark:hover:bg-gray-800/50',
|
|
198
|
+
accent: 'hover:bg-blue-50 dark:hover:bg-blue-900/20',
|
|
199
|
+
dark: 'hover:bg-gray-50 dark:hover:bg-gray-700/60',
|
|
200
|
+
custom: '',
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const VARIANT_TR_STRIPE: Record<TableVariant, (i: number) => string> = {
|
|
204
|
+
default: () => '',
|
|
205
|
+
striped: (i) => i % 2 === 0
|
|
206
|
+
? 'bg-white dark:bg-gray-800'
|
|
207
|
+
: 'bg-gray-50 dark:bg-gray-700/40',
|
|
208
|
+
bordered: () => '',
|
|
209
|
+
minimal: () => '',
|
|
210
|
+
ghost: () => '',
|
|
211
|
+
card: () => '',
|
|
212
|
+
accent: () => '',
|
|
213
|
+
dark: () => '',
|
|
117
214
|
custom: () => '',
|
|
118
215
|
};
|
|
119
216
|
|
|
@@ -122,6 +219,10 @@ const VARIANT_TD: Record<TableVariant, string> = {
|
|
|
122
219
|
striped: 'text-gray-700 dark:text-gray-300',
|
|
123
220
|
bordered: 'text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-700',
|
|
124
221
|
minimal: 'text-gray-700 dark:text-gray-300 border-b border-gray-100 dark:border-gray-800',
|
|
222
|
+
ghost: 'text-gray-700 dark:text-gray-300',
|
|
223
|
+
card: 'text-gray-700 dark:text-gray-300',
|
|
224
|
+
accent: 'text-gray-700 dark:text-gray-300',
|
|
225
|
+
dark: 'text-gray-700 dark:text-gray-300',
|
|
125
226
|
custom: '',
|
|
126
227
|
};
|
|
127
228
|
|
|
@@ -130,9 +231,51 @@ const VARIANT_TBODY_DIVIDER: Record<TableVariant, string> = {
|
|
|
130
231
|
striped: '',
|
|
131
232
|
bordered: '',
|
|
132
233
|
minimal: '',
|
|
234
|
+
ghost: 'divide-y divide-gray-200 dark:divide-gray-700',
|
|
235
|
+
card: 'divide-y divide-gray-100 dark:divide-gray-800',
|
|
236
|
+
accent: 'divide-y divide-gray-200 dark:divide-gray-700',
|
|
237
|
+
dark: 'divide-y divide-gray-200 dark:divide-gray-700',
|
|
133
238
|
custom: '',
|
|
134
239
|
};
|
|
135
240
|
|
|
241
|
+
// ─── Sub-components ───────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
function SortIcon({ direction }: { direction?: 'asc' | 'desc' | null }) {
|
|
244
|
+
if (direction === 'asc') return <SortAscIcon className="inline-block ml-1 w-3 h-3 shrink-0" />;
|
|
245
|
+
if (direction === 'desc') return <SortDescIcon className="inline-block ml-1 w-3 h-3 shrink-0" />;
|
|
246
|
+
return <SortBothIcon className="inline-block ml-1 w-3 h-3 shrink-0 opacity-40" />;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function SkeletonRow({ colCount, size }: { colCount: number; size: TableSize }) {
|
|
250
|
+
return (
|
|
251
|
+
<tr>
|
|
252
|
+
{Array.from({ length: colCount }).map((_, i) => (
|
|
253
|
+
<td key={i} className={SIZE_TD[size]}>
|
|
254
|
+
<div className="h-4 rounded bg-gray-200 dark:bg-gray-700 animate-pulse" />
|
|
255
|
+
</td>
|
|
256
|
+
))}
|
|
257
|
+
</tr>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
function isColumnDef(col: ColumnDef | ReactNode): col is ColumnDef {
|
|
264
|
+
return typeof col === 'object' && col !== null && 'header' in (col as object);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function resolveColumn(col: ColumnDef | ReactNode): ColumnDef {
|
|
268
|
+
if (isColumnDef(col)) return col;
|
|
269
|
+
return { header: col };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function colSizeStyle(col: ColumnDef): CSSProperties {
|
|
273
|
+
const s: CSSProperties = {};
|
|
274
|
+
if (col.width !== undefined) s.width = col.width;
|
|
275
|
+
if (col.minWidth !== undefined) s.minWidth = col.minWidth;
|
|
276
|
+
return s;
|
|
277
|
+
}
|
|
278
|
+
|
|
136
279
|
// ─── Component ───────────────────────────────────────────────────────────────
|
|
137
280
|
|
|
138
281
|
export function Table({
|
|
@@ -149,48 +292,96 @@ export function Table({
|
|
|
149
292
|
onRowClick,
|
|
150
293
|
hideHeader = false,
|
|
151
294
|
style,
|
|
295
|
+
stickyHeader = false,
|
|
296
|
+
caption,
|
|
297
|
+
footerRows,
|
|
298
|
+
loading = false,
|
|
299
|
+
loadingRows = 4,
|
|
300
|
+
getRowStyle,
|
|
301
|
+
rounded = false,
|
|
302
|
+
shadow = false,
|
|
303
|
+
hoverable = true,
|
|
304
|
+
sortState,
|
|
305
|
+
onSort,
|
|
152
306
|
}: TableProps) {
|
|
153
307
|
const cols = columns.map(resolveColumn);
|
|
154
308
|
|
|
155
309
|
const resolvedTrClass = (i: number): string => {
|
|
156
|
-
const
|
|
310
|
+
const stripeCls = VARIANT_TR_STRIPE[variant](i);
|
|
311
|
+
const baseCls = stripeCls || VARIANT_TR_BASE[variant];
|
|
312
|
+
const hoverCls = hoverable ? `${VARIANT_TR_HOVER[variant]} transition-colors` : '';
|
|
157
313
|
const clickCls = onRowClick ? 'cursor-pointer' : '';
|
|
158
314
|
const customCls = typeof trClassName === 'function' ? trClassName(i) : (trClassName ?? '');
|
|
159
|
-
return
|
|
315
|
+
return [baseCls, hoverCls, clickCls, customCls].filter(Boolean).join(' ');
|
|
160
316
|
};
|
|
161
317
|
|
|
318
|
+
const wrapperCls = [
|
|
319
|
+
'overflow-x-auto w-full',
|
|
320
|
+
rounded ? 'rounded-lg overflow-hidden' : '',
|
|
321
|
+
shadow ? 'shadow-md' : '',
|
|
322
|
+
className,
|
|
323
|
+
].filter(Boolean).join(' ');
|
|
324
|
+
|
|
325
|
+
const theadCls = [
|
|
326
|
+
VARIANT_THEAD[variant],
|
|
327
|
+
stickyHeader ? 'sticky top-0 z-20' : '',
|
|
328
|
+
].filter(Boolean).join(' ');
|
|
329
|
+
|
|
330
|
+
const stickyColCls = 'sticky left-0 z-10 bg-inherit';
|
|
331
|
+
|
|
162
332
|
return (
|
|
163
|
-
<div className={
|
|
333
|
+
<div className={wrapperCls}>
|
|
164
334
|
<table
|
|
165
335
|
className={`${VARIANT_TABLE[variant]} ${tableClassName}`.trim()}
|
|
166
336
|
style={style}
|
|
167
337
|
>
|
|
338
|
+
{caption && (
|
|
339
|
+
<caption className="mb-2 text-left text-sm text-gray-500 dark:text-gray-400">
|
|
340
|
+
{caption}
|
|
341
|
+
</caption>
|
|
342
|
+
)}
|
|
343
|
+
|
|
168
344
|
{!hideHeader && (
|
|
169
|
-
<thead className={
|
|
345
|
+
<thead className={theadCls}>
|
|
170
346
|
<tr>
|
|
171
|
-
{cols.map((col, i) =>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
347
|
+
{cols.map((col, i) => {
|
|
348
|
+
const isSortable = col.sortable && col.key;
|
|
349
|
+
const activeSort = sortState?.key === col.key ? sortState!.direction : null;
|
|
350
|
+
return (
|
|
351
|
+
<th
|
|
352
|
+
key={i}
|
|
353
|
+
scope="col"
|
|
354
|
+
className={[
|
|
355
|
+
SIZE_TH[size],
|
|
356
|
+
VARIANT_TH[variant],
|
|
357
|
+
ALIGN_CLASS[col.align ?? 'left'],
|
|
358
|
+
col.className ?? '',
|
|
359
|
+
thClassName,
|
|
360
|
+
col.sticky ? stickyColCls : '',
|
|
361
|
+
isSortable ? 'cursor-pointer select-none' : '',
|
|
362
|
+
].filter(Boolean).join(' ')}
|
|
363
|
+
style={{ ...colSizeStyle(col), ...(col.thStyle ?? {}) }}
|
|
364
|
+
onClick={isSortable ? () => onSort?.(col.key!) : undefined}
|
|
365
|
+
>
|
|
366
|
+
{col.header}
|
|
367
|
+
{isSortable && <SortIcon direction={activeSort} />}
|
|
368
|
+
</th>
|
|
369
|
+
);
|
|
370
|
+
})}
|
|
185
371
|
</tr>
|
|
186
372
|
</thead>
|
|
187
373
|
)}
|
|
374
|
+
|
|
188
375
|
<tbody className={VARIANT_TBODY_DIVIDER[variant]}>
|
|
189
|
-
{
|
|
376
|
+
{loading ? (
|
|
377
|
+
Array.from({ length: loadingRows }).map((_, i) => (
|
|
378
|
+
<SkeletonRow key={i} colCount={cols.length} size={size} />
|
|
379
|
+
))
|
|
380
|
+
) : rows.length === 0 ? (
|
|
190
381
|
<tr>
|
|
191
382
|
<td
|
|
192
383
|
colSpan={cols.length}
|
|
193
|
-
className={`${SIZE_TD[size]} text-center text-gray-400 dark:text-gray-500
|
|
384
|
+
className={`${SIZE_TD[size]} py-8 text-center text-gray-400 dark:text-gray-500`}
|
|
194
385
|
>
|
|
195
386
|
{emptyState ?? 'Sin datos'}
|
|
196
387
|
</td>
|
|
@@ -200,6 +391,7 @@ export function Table({
|
|
|
200
391
|
<tr
|
|
201
392
|
key={rowIndex}
|
|
202
393
|
className={resolvedTrClass(rowIndex)}
|
|
394
|
+
style={getRowStyle?.(rowIndex)}
|
|
203
395
|
onClick={onRowClick ? () => onRowClick(rowIndex) : undefined}
|
|
204
396
|
>
|
|
205
397
|
{row.map((cell, cellIndex) => {
|
|
@@ -213,7 +405,12 @@ export function Table({
|
|
|
213
405
|
ALIGN_CLASS[col?.align ?? 'left'],
|
|
214
406
|
col?.className ?? '',
|
|
215
407
|
tdClassName,
|
|
408
|
+
col?.sticky ? stickyColCls : '',
|
|
216
409
|
].filter(Boolean).join(' ')}
|
|
410
|
+
style={{
|
|
411
|
+
...(col ? colSizeStyle(col) : {}),
|
|
412
|
+
...(col?.tdStyle ?? {}),
|
|
413
|
+
}}
|
|
217
414
|
>
|
|
218
415
|
{cell}
|
|
219
416
|
</td>
|
|
@@ -223,7 +420,34 @@ export function Table({
|
|
|
223
420
|
))
|
|
224
421
|
)}
|
|
225
422
|
</tbody>
|
|
423
|
+
|
|
424
|
+
{footerRows && footerRows.length > 0 && (
|
|
425
|
+
<tfoot className="border-t border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800/50">
|
|
426
|
+
{footerRows.map((row, rowIndex) => (
|
|
427
|
+
<tr key={rowIndex}>
|
|
428
|
+
{row.map((cell, cellIndex) => {
|
|
429
|
+
const col = cols[cellIndex];
|
|
430
|
+
return (
|
|
431
|
+
<td
|
|
432
|
+
key={cellIndex}
|
|
433
|
+
className={[
|
|
434
|
+
SIZE_TD[size],
|
|
435
|
+
'font-medium text-gray-700 dark:text-gray-300',
|
|
436
|
+
ALIGN_CLASS[col?.align ?? 'left'],
|
|
437
|
+
col?.className ?? '',
|
|
438
|
+
tdClassName,
|
|
439
|
+
].filter(Boolean).join(' ')}
|
|
440
|
+
style={col?.tdStyle}
|
|
441
|
+
>
|
|
442
|
+
{cell}
|
|
443
|
+
</td>
|
|
444
|
+
);
|
|
445
|
+
})}
|
|
446
|
+
</tr>
|
|
447
|
+
))}
|
|
448
|
+
</tfoot>
|
|
449
|
+
)}
|
|
226
450
|
</table>
|
|
227
451
|
</div>
|
|
228
452
|
);
|
|
229
|
-
}
|
|
453
|
+
}
|
|
@@ -1,71 +1,121 @@
|
|
|
1
|
-
import { type TextareaHTMLAttributes, type FC, type ReactNode } from 'react';
|
|
1
|
+
import { type TextareaHTMLAttributes, type FC, type ReactNode, useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
type TextAreaVariant = 'default' | 'outline' | 'filled' | 'minimal';
|
|
4
|
+
type TextAreaSize = 'small' | 'medium' | 'large';
|
|
5
|
+
type ResizeOption = 'vertical' | 'horizontal' | 'both' | 'none';
|
|
2
6
|
|
|
3
7
|
interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
4
8
|
label?: string | ReactNode;
|
|
5
9
|
error?: string;
|
|
6
10
|
helperText?: string;
|
|
7
|
-
variant?:
|
|
8
|
-
size?:
|
|
11
|
+
variant?: TextAreaVariant;
|
|
12
|
+
size?: TextAreaSize;
|
|
13
|
+
autoResize?: boolean;
|
|
14
|
+
showCount?: boolean;
|
|
15
|
+
resize?: ResizeOption;
|
|
9
16
|
}
|
|
10
17
|
|
|
18
|
+
const SIZE_CLASSES: Record<TextAreaSize, string> = {
|
|
19
|
+
small: 'px-2 py-1 text-xs',
|
|
20
|
+
medium: 'px-3 py-2 text-sm',
|
|
21
|
+
large: 'px-4 py-3 text-base',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const VARIANT_CLASSES: Record<TextAreaVariant, string> = {
|
|
25
|
+
default: 'border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800',
|
|
26
|
+
outline: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent',
|
|
27
|
+
filled: 'border border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-700',
|
|
28
|
+
minimal: 'border-0 border-b border-gray-300 dark:border-gray-600 bg-transparent rounded-none focus:ring-0',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const RESIZE_CLASSES: Record<ResizeOption, string> = {
|
|
32
|
+
vertical: 'resize-y',
|
|
33
|
+
horizontal: 'resize-x',
|
|
34
|
+
both: 'resize',
|
|
35
|
+
none: 'resize-none',
|
|
36
|
+
};
|
|
37
|
+
|
|
11
38
|
export const TextArea: FC<TextAreaProps> = ({
|
|
12
39
|
label,
|
|
13
40
|
error,
|
|
14
41
|
helperText,
|
|
15
42
|
variant = 'default',
|
|
16
43
|
size = 'medium',
|
|
44
|
+
autoResize = false,
|
|
45
|
+
showCount = false,
|
|
46
|
+
resize = 'vertical',
|
|
17
47
|
className = '',
|
|
18
48
|
id,
|
|
49
|
+
onInput: propsOnInput,
|
|
19
50
|
...props
|
|
20
51
|
}) => {
|
|
21
52
|
const textAreaId = id || `textarea-${Math.random().toString(36).substring(2, 9)}`;
|
|
53
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
22
54
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
55
|
+
const adjustHeight = () => {
|
|
56
|
+
const el = textareaRef.current;
|
|
57
|
+
if (!el) return;
|
|
58
|
+
el.style.height = 'auto';
|
|
59
|
+
el.style.height = `${el.scrollHeight}px`;
|
|
27
60
|
};
|
|
28
61
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (autoResize) adjustHeight();
|
|
64
|
+
}, [autoResize, props.value]);
|
|
65
|
+
|
|
66
|
+
const handleInput = (e: React.FormEvent<HTMLTextAreaElement>) => {
|
|
67
|
+
if (autoResize) adjustHeight();
|
|
68
|
+
propsOnInput?.(e);
|
|
34
69
|
};
|
|
35
70
|
|
|
36
|
-
const
|
|
71
|
+
const baseCls = 'appearance-none relative block w-full placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white rounded-md border focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:border-indigo-500 focus:z-10 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200';
|
|
72
|
+
const errorCls = error ? 'border-red-300 dark:border-red-600 focus:ring-red-500 dark:focus:ring-red-400 focus:border-red-500' : '';
|
|
73
|
+
const resizeCls = autoResize ? 'resize-none overflow-hidden' : RESIZE_CLASSES[resize];
|
|
74
|
+
|
|
75
|
+
const classes = [baseCls, SIZE_CLASSES[size], VARIANT_CLASSES[variant], errorCls, resizeCls, className]
|
|
76
|
+
.filter(Boolean).join(' ');
|
|
37
77
|
|
|
38
|
-
const
|
|
78
|
+
const maxLength = typeof props.maxLength === 'number' ? props.maxLength : undefined;
|
|
79
|
+
const currentLength =
|
|
80
|
+
typeof props.value === 'string' ? props.value.length :
|
|
81
|
+
typeof props.value === 'number' ? String(props.value).length : 0;
|
|
39
82
|
|
|
40
|
-
const
|
|
83
|
+
const overLimit = maxLength !== undefined && currentLength > maxLength;
|
|
41
84
|
|
|
42
85
|
return (
|
|
43
86
|
<div className="space-y-1 w-full">
|
|
44
|
-
{label
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
87
|
+
{(label || showCount) && (
|
|
88
|
+
<div className="flex items-baseline gap-2">
|
|
89
|
+
<div className="flex-1">
|
|
90
|
+
{label && (
|
|
91
|
+
typeof label === 'string' ? (
|
|
92
|
+
<label htmlFor={textAreaId} className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
93
|
+
{label}
|
|
94
|
+
{props.required && <span className="ml-1 text-red-500" aria-hidden="true">*</span>}
|
|
95
|
+
</label>
|
|
96
|
+
) : label
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
{showCount && (
|
|
100
|
+
<span className={`text-xs shrink-0 tabular-nums ${overLimit ? 'text-red-500' : 'text-gray-400 dark:text-gray-500'}`}>
|
|
101
|
+
{maxLength ? `${currentLength} / ${maxLength}` : currentLength}
|
|
102
|
+
</span>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
53
105
|
)}
|
|
54
106
|
<textarea
|
|
107
|
+
ref={textareaRef}
|
|
55
108
|
id={textAreaId}
|
|
56
109
|
className={classes}
|
|
110
|
+
onInput={handleInput}
|
|
57
111
|
{...props}
|
|
58
112
|
/>
|
|
59
113
|
{error && (
|
|
60
|
-
<p className="text-sm text-red-600 dark:text-red-400" role="alert">
|
|
61
|
-
{error}
|
|
62
|
-
</p>
|
|
114
|
+
<p className="text-sm text-red-600 dark:text-red-400" role="alert">{error}</p>
|
|
63
115
|
)}
|
|
64
116
|
{helperText && !error && (
|
|
65
|
-
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
66
|
-
{helperText}
|
|
67
|
-
</p>
|
|
117
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">{helperText}</p>
|
|
68
118
|
)}
|
|
69
119
|
</div>
|
|
70
120
|
);
|
|
71
|
-
};
|
|
121
|
+
};
|
|
@@ -687,4 +687,36 @@ export function IconLayers({ className }: Props) {
|
|
|
687
687
|
<path d="M8.235 1.559a.5.5 0 0 0-.47 0l-7.5 4a.5.5 0 0 0 0 .882L3.188 8 .265 9.559a.5.5 0 0 0 0 .882l7.5 4a.5.5 0 0 0 .47 0l7.5-4a.5.5 0 0 0 0-.882L12.813 8l2.922-1.559a.5.5 0 0 0 0-.882l-7.5-4zm3.515 7.008L14.438 10 8 13.433 1.562 10 4.25 8.567l3.515 1.874a.5.5 0 0 0 .47 0l3.515-1.874zM8 9.433 1.562 6 8 2.567 14.438 6 8 9.433z"/>
|
|
688
688
|
</svg>
|
|
689
689
|
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export function ChevronDownIcon({ className }: Props) {
|
|
693
|
+
return (
|
|
694
|
+
<svg viewBox="0 0 16 16" className={className} fill="currentColor" aria-hidden>
|
|
695
|
+
<path d="M4.516 7.548c.436-.446 1.043-.481 1.576 0L8 9.747l1.908-2.199c.533-.481 1.14-.446 1.576 0 .436.445.408 1.197 0 1.615-.408.418-2.695 2.977-2.695 2.977-.27.282-.64.423-1.01.423s-.74-.14-1.01-.423c0 0-2.287-2.559-2.695-2.977-.408-.418-.436-1.17 0-1.615z" />
|
|
696
|
+
</svg>
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function SortAscIcon({ className }: Props) {
|
|
701
|
+
return (
|
|
702
|
+
<svg viewBox="0 0 12 12" className={className} fill="currentColor" aria-hidden>
|
|
703
|
+
<path d="M6 2.5l4 5H2l4-5z" />
|
|
704
|
+
</svg>
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export function SortDescIcon({ className }: Props) {
|
|
709
|
+
return (
|
|
710
|
+
<svg viewBox="0 0 12 12" className={className} fill="currentColor" aria-hidden>
|
|
711
|
+
<path d="M6 9.5L2 4.5h8L6 9.5z" />
|
|
712
|
+
</svg>
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export function SortBothIcon({ className }: Props) {
|
|
717
|
+
return (
|
|
718
|
+
<svg viewBox="0 0 12 12" className={className} fill="currentColor" aria-hidden>
|
|
719
|
+
<path d="M6 1.5l3 3.5H3L6 1.5zm0 9l-3-3.5h6L6 10.5z" />
|
|
720
|
+
</svg>
|
|
721
|
+
);
|
|
690
722
|
}
|