neogestify-ui-components 2.2.1 → 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 +123 -11
- package/dist/components/html/index.d.ts +123 -11
- package/dist/components/html/index.js +607 -144
- package/dist/components/html/index.js.map +1 -1
- package/dist/components/html/index.mjs +608 -145
- 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 +611 -144
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +609 -146
- 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 +429 -38
- package/src/components/html/TextArea.tsx +81 -31
- package/src/components/icons/icons.tsx +32 -0
|
@@ -1,62 +1,453 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
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';
|
|
14
|
+
|
|
15
|
+
type TableSize = 'sm' | 'md' | 'lg';
|
|
16
|
+
|
|
17
|
+
interface SortState {
|
|
18
|
+
key: string;
|
|
19
|
+
direction: 'asc' | 'desc';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ColumnDef {
|
|
23
|
+
/** Contenido del encabezado */
|
|
24
|
+
header: ReactNode;
|
|
25
|
+
/** Clase CSS adicional para toda la columna (th + td) */
|
|
26
|
+
className?: string;
|
|
27
|
+
/** Alinear el contenido de esta columna */
|
|
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;
|
|
43
|
+
}
|
|
2
44
|
|
|
3
45
|
interface TableProps {
|
|
4
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Definición de columnas. Acepta strings simples o ColumnDef para
|
|
48
|
+
* configuración avanzada (ancho, sticky, sort, etc.).
|
|
49
|
+
*/
|
|
50
|
+
columns: (ColumnDef | ReactNode)[];
|
|
51
|
+
|
|
52
|
+
/** Filas del cuerpo. Cada fila es un arreglo de celdas. */
|
|
5
53
|
rows: ReactNode[][];
|
|
6
|
-
|
|
54
|
+
|
|
55
|
+
/** Estilo visual. Default: 'default' */
|
|
56
|
+
variant?: TableVariant;
|
|
57
|
+
|
|
58
|
+
/** Tamaño de padding. Default: 'md' */
|
|
59
|
+
size?: TableSize;
|
|
60
|
+
|
|
61
|
+
/** Clase CSS adicional para el wrapper <div> */
|
|
7
62
|
className?: string;
|
|
63
|
+
|
|
64
|
+
/** Clase CSS adicional para el <table> */
|
|
65
|
+
tableClassName?: string;
|
|
66
|
+
|
|
67
|
+
/** Clase CSS adicional para cada <th> */
|
|
8
68
|
thClassName?: string;
|
|
69
|
+
|
|
70
|
+
/** Clase CSS adicional para cada <td> */
|
|
9
71
|
tdClassName?: string;
|
|
72
|
+
|
|
73
|
+
/** Clase CSS adicional por fila del body */
|
|
74
|
+
trClassName?: string | ((rowIndex: number) => string);
|
|
75
|
+
|
|
76
|
+
/** Nodo a mostrar cuando rows está vacío */
|
|
77
|
+
emptyState?: ReactNode;
|
|
78
|
+
|
|
79
|
+
/** Callback al hacer click en una fila */
|
|
80
|
+
onRowClick?: (rowIndex: number) => void;
|
|
81
|
+
|
|
82
|
+
/** Oculta el thead */
|
|
83
|
+
hideHeader?: boolean;
|
|
84
|
+
|
|
85
|
+
/** Estilos inline para el <table> */
|
|
86
|
+
style?: CSSProperties;
|
|
87
|
+
|
|
88
|
+
/** Fija el thead al hacer scroll vertical */
|
|
89
|
+
stickyHeader?: boolean;
|
|
90
|
+
|
|
91
|
+
/** Caption accesible de la tabla */
|
|
92
|
+
caption?: ReactNode;
|
|
93
|
+
|
|
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;
|
|
10
120
|
}
|
|
11
121
|
|
|
122
|
+
// ─── Lookup tables ────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
const ALIGN_CLASS: Record<string, string> = {
|
|
125
|
+
left: 'text-left',
|
|
126
|
+
center: 'text-center',
|
|
127
|
+
right: 'text-right',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const SIZE_TH: Record<TableSize, string> = {
|
|
131
|
+
sm: 'px-2 py-1.5 text-xs',
|
|
132
|
+
md: 'px-3 py-2.5 text-xs',
|
|
133
|
+
lg: 'px-4 py-3.5 text-sm',
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const SIZE_TD: Record<TableSize, string> = {
|
|
137
|
+
sm: 'px-2 py-1.5 text-xs',
|
|
138
|
+
md: 'px-3 py-2.5 text-sm',
|
|
139
|
+
lg: 'px-4 py-3.5 text-sm',
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const VARIANT_TABLE: Record<TableVariant, string> = {
|
|
143
|
+
default: 'w-full min-w-full table-auto',
|
|
144
|
+
striped: 'w-full min-w-full table-auto',
|
|
145
|
+
bordered: 'w-full min-w-full table-auto border border-gray-300 dark:border-gray-600',
|
|
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',
|
|
151
|
+
custom: 'w-full min-w-full table-auto',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const VARIANT_THEAD: Record<TableVariant, string> = {
|
|
155
|
+
default: 'bg-gray-100 dark:bg-gray-700',
|
|
156
|
+
striped: 'bg-gray-100 dark:bg-gray-700',
|
|
157
|
+
bordered: 'bg-gray-100 dark:bg-gray-700',
|
|
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',
|
|
163
|
+
custom: '',
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const VARIANT_TH: Record<TableVariant, string> = {
|
|
167
|
+
default: 'font-semibold uppercase tracking-wider text-gray-600 dark:text-gray-300',
|
|
168
|
+
striped: 'font-semibold uppercase tracking-wider text-gray-600 dark:text-gray-300',
|
|
169
|
+
bordered: 'font-semibold uppercase tracking-wider text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600',
|
|
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',
|
|
175
|
+
custom: '',
|
|
176
|
+
};
|
|
177
|
+
|
|
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: () => '',
|
|
214
|
+
custom: () => '',
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const VARIANT_TD: Record<TableVariant, string> = {
|
|
218
|
+
default: 'text-gray-700 dark:text-gray-300',
|
|
219
|
+
striped: 'text-gray-700 dark:text-gray-300',
|
|
220
|
+
bordered: 'text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-700',
|
|
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',
|
|
226
|
+
custom: '',
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const VARIANT_TBODY_DIVIDER: Record<TableVariant, string> = {
|
|
230
|
+
default: 'divide-y divide-gray-200 dark:divide-gray-700',
|
|
231
|
+
striped: '',
|
|
232
|
+
bordered: '',
|
|
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',
|
|
238
|
+
custom: '',
|
|
239
|
+
};
|
|
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
|
+
|
|
279
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
280
|
+
|
|
12
281
|
export function Table({
|
|
13
|
-
|
|
282
|
+
columns,
|
|
14
283
|
rows,
|
|
15
284
|
variant = 'default',
|
|
285
|
+
size = 'md',
|
|
16
286
|
className = '',
|
|
287
|
+
tableClassName = '',
|
|
17
288
|
thClassName = '',
|
|
18
|
-
tdClassName = ''
|
|
289
|
+
tdClassName = '',
|
|
290
|
+
trClassName,
|
|
291
|
+
emptyState,
|
|
292
|
+
onRowClick,
|
|
293
|
+
hideHeader = false,
|
|
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,
|
|
19
306
|
}: TableProps) {
|
|
20
|
-
const
|
|
21
|
-
? 'w-full table-auto border-collapse border border-gray-300 dark:border-gray-600 min-w-full'
|
|
22
|
-
: '';
|
|
307
|
+
const cols = columns.map(resolveColumn);
|
|
23
308
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
309
|
+
const resolvedTrClass = (i: number): string => {
|
|
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` : '';
|
|
313
|
+
const clickCls = onRowClick ? 'cursor-pointer' : '';
|
|
314
|
+
const customCls = typeof trClassName === 'function' ? trClassName(i) : (trClassName ?? '');
|
|
315
|
+
return [baseCls, hoverCls, clickCls, customCls].filter(Boolean).join(' ');
|
|
316
|
+
};
|
|
27
317
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
: ''
|
|
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(' ');
|
|
31
324
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
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';
|
|
35
331
|
|
|
36
332
|
return (
|
|
37
|
-
<div className=
|
|
38
|
-
<table
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
<tr
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
333
|
+
<div className={wrapperCls}>
|
|
334
|
+
<table
|
|
335
|
+
className={`${VARIANT_TABLE[variant]} ${tableClassName}`.trim()}
|
|
336
|
+
style={style}
|
|
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
|
+
|
|
344
|
+
{!hideHeader && (
|
|
345
|
+
<thead className={theadCls}>
|
|
346
|
+
<tr>
|
|
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
|
+
})}
|
|
371
|
+
</tr>
|
|
372
|
+
</thead>
|
|
373
|
+
)}
|
|
374
|
+
|
|
375
|
+
<tbody className={VARIANT_TBODY_DIVIDER[variant]}>
|
|
376
|
+
{loading ? (
|
|
377
|
+
Array.from({ length: loadingRows }).map((_, i) => (
|
|
378
|
+
<SkeletonRow key={i} colCount={cols.length} size={size} />
|
|
379
|
+
))
|
|
380
|
+
) : rows.length === 0 ? (
|
|
381
|
+
<tr>
|
|
382
|
+
<td
|
|
383
|
+
colSpan={cols.length}
|
|
384
|
+
className={`${SIZE_TD[size]} py-8 text-center text-gray-400 dark:text-gray-500`}
|
|
385
|
+
>
|
|
386
|
+
{emptyState ?? 'Sin datos'}
|
|
387
|
+
</td>
|
|
56
388
|
</tr>
|
|
57
|
-
)
|
|
389
|
+
) : (
|
|
390
|
+
rows.map((row, rowIndex) => (
|
|
391
|
+
<tr
|
|
392
|
+
key={rowIndex}
|
|
393
|
+
className={resolvedTrClass(rowIndex)}
|
|
394
|
+
style={getRowStyle?.(rowIndex)}
|
|
395
|
+
onClick={onRowClick ? () => onRowClick(rowIndex) : undefined}
|
|
396
|
+
>
|
|
397
|
+
{row.map((cell, cellIndex) => {
|
|
398
|
+
const col = cols[cellIndex];
|
|
399
|
+
return (
|
|
400
|
+
<td
|
|
401
|
+
key={cellIndex}
|
|
402
|
+
className={[
|
|
403
|
+
SIZE_TD[size],
|
|
404
|
+
VARIANT_TD[variant],
|
|
405
|
+
ALIGN_CLASS[col?.align ?? 'left'],
|
|
406
|
+
col?.className ?? '',
|
|
407
|
+
tdClassName,
|
|
408
|
+
col?.sticky ? stickyColCls : '',
|
|
409
|
+
].filter(Boolean).join(' ')}
|
|
410
|
+
style={{
|
|
411
|
+
...(col ? colSizeStyle(col) : {}),
|
|
412
|
+
...(col?.tdStyle ?? {}),
|
|
413
|
+
}}
|
|
414
|
+
>
|
|
415
|
+
{cell}
|
|
416
|
+
</td>
|
|
417
|
+
);
|
|
418
|
+
})}
|
|
419
|
+
</tr>
|
|
420
|
+
))
|
|
421
|
+
)}
|
|
58
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
|
+
)}
|
|
59
450
|
</table>
|
|
60
451
|
</div>
|
|
61
452
|
);
|
|
62
|
-
}
|
|
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
|
+
};
|