next-recomponents 2.0.27 → 2.0.29

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.
@@ -1,14 +1,12 @@
1
- import { DetailedHTMLProps, InputHTMLAttributes } from "react";
1
+ import { React } from "next/dist/server/route-modules/app-page/vendored/rsc/entrypoints";
2
+ import { InputHTMLAttributes, useState } from "react";
2
3
 
3
- interface InputProps
4
- extends DetailedHTMLProps<
5
- InputHTMLAttributes<HTMLInputElement>,
6
- HTMLInputElement
7
- > {
4
+ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
8
5
  label: React.ReactNode;
9
6
  regex?: RegExp;
10
7
  invalidMessage?: React.ReactNode;
11
8
  }
9
+
12
10
  export default function Input({
13
11
  label,
14
12
  className,
@@ -18,26 +16,82 @@ export default function Input({
18
16
  }: InputProps) {
19
17
  const value = `${props?.value || ""}`;
20
18
  const isValid = !regex ? true : regex.test(value);
19
+ const isPassword = props.type === "password";
20
+ const [showPassword, setShowPassword] = useState(false);
21
21
 
22
22
  return (
23
23
  <div className="w-full">
24
24
  <label className="flex flex-col gap-1">
25
- <div className="font-bold ">
25
+ <div className="font-bold">
26
26
  {label} {props?.required && <span className="text-red-500">*</span>}
27
27
  </div>
28
- <div>
28
+ <div className="relative">
29
29
  <input
30
30
  {...props}
31
+ type={isPassword && showPassword ? "text" : props.type}
31
32
  className={[
32
33
  "p-2 w-full rounded border shadow",
33
- value != "" && !isValid && "bg-red-200 text-black",
34
- value != "" && isValid && "bg-green-200 text-black",
34
+ isPassword && "pr-10",
35
+ value !== "" && !isValid && "bg-red-200 text-black",
36
+ value !== "" && isValid && "bg-green-200 text-black",
35
37
  className,
36
- ].join(" ")}
38
+ ]
39
+ .filter(Boolean)
40
+ .join(" ")}
37
41
  />
42
+ {isPassword && (
43
+ <button
44
+ type="button"
45
+ onClick={() => setShowPassword((prev) => !prev)}
46
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-800 focus:outline-none"
47
+ aria-label={
48
+ showPassword ? "Ocultar contraseña" : "Mostrar contraseña"
49
+ }
50
+ >
51
+ {showPassword ? (
52
+ // Ojo cerrado (ocultar)
53
+ <svg
54
+ xmlns="http://www.w3.org/2000/svg"
55
+ className="h-5 w-5"
56
+ fill="none"
57
+ viewBox="0 0 24 24"
58
+ stroke="currentColor"
59
+ >
60
+ <path
61
+ strokeLinecap="round"
62
+ strokeLinejoin="round"
63
+ strokeWidth={2}
64
+ d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
65
+ />
66
+ </svg>
67
+ ) : (
68
+ // Ojo abierto (mostrar)
69
+ <svg
70
+ xmlns="http://www.w3.org/2000/svg"
71
+ className="h-5 w-5"
72
+ fill="none"
73
+ viewBox="0 0 24 24"
74
+ stroke="currentColor"
75
+ >
76
+ <path
77
+ strokeLinecap="round"
78
+ strokeLinejoin="round"
79
+ strokeWidth={2}
80
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
81
+ />
82
+ <path
83
+ strokeLinecap="round"
84
+ strokeLinejoin="round"
85
+ strokeWidth={2}
86
+ d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
87
+ />
88
+ </svg>
89
+ )}
90
+ </button>
91
+ )}
38
92
  </div>
39
93
  </label>
40
- {!isValid && value != "" && (
94
+ {!isValid && value !== "" && (
41
95
  <div className="text-red-800 invalid">{invalidMessage}</div>
42
96
  )}
43
97
  </div>
@@ -22,8 +22,12 @@ interface FooterType {
22
22
  [key: string]: FooterAggregation;
23
23
  }
24
24
 
25
+ type GridDensity = "compact" | "standard" | "comfortable";
26
+
25
27
  interface TableProps {
26
28
  data: any;
29
+ /** Muestra un input de búsqueda general que filtra filas por palabras clave */
30
+ searchable?: boolean;
27
31
  flex?: number;
28
32
  editableFields?: string[];
29
33
  onSelect?: (data: GridValidRowModel[]) => void;
@@ -44,6 +48,28 @@ interface TableProps {
44
48
  className?: string;
45
49
  fontSize?: string;
46
50
  colSize?: Record<string, number>;
51
+ /**
52
+ * Alto fijo de fila en px.
53
+ * Si se omite usa el default de MUI según `density`
54
+ * (compact ≈ 36, standard ≈ 52, comfortable ≈ 68).
55
+ * Se ignora automáticamente cuando `wrapText` es true.
56
+ */
57
+ rowHeight?: number;
58
+ /**
59
+ * Cuando es true el texto largo hace wrap en lugar de truncarse,
60
+ * y el alto de cada fila crece para mostrar todo el contenido.
61
+ * Internamente activa `getRowHeight={() => "auto"}` en el DataGrid.
62
+ */
63
+ wrapText?: boolean;
64
+ /**
65
+ * Densidad visual de la tabla.
66
+ * Afecta el padding de celdas y encabezados independientemente
67
+ * del tamaño de fuente.
68
+ * - "compact" → padding mínimo, tabla muy densa
69
+ * - "standard" → (default) comportamiento por defecto de MUI
70
+ * - "comfortable" → padding generoso, fácil de leer
71
+ */
72
+ density?: GridDensity;
47
73
  [key: string]: any;
48
74
  }
49
75
 
@@ -256,6 +282,7 @@ function useColumns(
256
282
  buttons?: Record<string, any>;
257
283
  hideColumns: string[];
258
284
  modal?: React.ReactNode;
285
+ wrapText?: boolean;
259
286
  handleRowUpdate: (row: GridRowModel) => GridRowModel;
260
287
  onModalOpen: (row: GridValidRowModel) => void;
261
288
  },
@@ -267,9 +294,11 @@ function useColumns(
267
294
  buttons,
268
295
  hideColumns,
269
296
  modal,
297
+ wrapText,
270
298
  handleRowUpdate,
271
299
  onModalOpen,
272
300
  } = options;
301
+
273
302
  return useMemo(() => {
274
303
  if (!rows.length) return [];
275
304
 
@@ -309,7 +338,6 @@ function useColumns(
309
338
 
310
339
  if (isNumber) {
311
340
  if (isNaN(value)) return value;
312
-
313
341
  return value;
314
342
  }
315
343
 
@@ -319,6 +347,20 @@ function useColumns(
319
347
  width: key == "id" ? 80 : (colSize?.[key] ?? undefined),
320
348
  editable: editableFields?.includes(key) ?? false,
321
349
  type: typeof rows[0][key] === "number" ? "number" : "string",
350
+ // When wrapText is enabled, allow cells to grow vertically
351
+ ...(wrapText && {
352
+ renderHeader: (params: any) => (
353
+ <span
354
+ style={{
355
+ whiteSpace: "normal",
356
+ lineHeight: 1.3,
357
+ wordBreak: "break-word",
358
+ }}
359
+ >
360
+ {params.colDef.headerName}
361
+ </span>
362
+ ),
363
+ }),
322
364
  renderCell: buttons?.[key]
323
365
  ? (params: any) => {
324
366
  const children =
@@ -338,14 +380,28 @@ function useColumns(
338
380
  },
339
381
  });
340
382
  }
341
- : null,
383
+ : wrapText
384
+ ? (params: any) => (
385
+ // Plain cell with wrap — no custom button
386
+ <span
387
+ style={{
388
+ whiteSpace: "normal",
389
+ wordBreak: "break-word",
390
+ lineHeight: 1.4,
391
+ padding: "6px 0",
392
+ display: "block",
393
+ }}
394
+ >
395
+ {params.formattedValue ?? params.value}
396
+ </span>
397
+ )
398
+ : null,
342
399
  }));
343
400
 
344
401
  if (modal) {
345
402
  cols.unshift({
346
403
  field: "Modal",
347
404
  headerName: "Modal",
348
- // flex,
349
405
  editable: false,
350
406
  type: "string",
351
407
  width: 10,
@@ -363,10 +419,63 @@ function useColumns(
363
419
  }
364
420
 
365
421
  return cols;
366
- }, [rows]);
422
+ }, [rows, wrapText]);
367
423
  }
368
424
 
369
- // ─── IHTable (array data) ─────────────────────────────────────────────────────
425
+ // ─── SearchBar ────────────────────────────────────────────────────────────────
426
+
427
+ function SearchBar({
428
+ value,
429
+ onChange,
430
+ }: {
431
+ value: string;
432
+ onChange: (v: string) => void;
433
+ }) {
434
+ return (
435
+ <div className="flex items-center gap-2 px-2 pb-1">
436
+ <svg
437
+ className="text-gray-400 shrink-0"
438
+ width="14"
439
+ height="14"
440
+ viewBox="0 0 20 20"
441
+ fill="currentColor"
442
+ >
443
+ <path
444
+ fillRule="evenodd"
445
+ d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
446
+ clipRule="evenodd"
447
+ />
448
+ </svg>
449
+ <input
450
+ type="text"
451
+ value={value}
452
+ onChange={(e) => onChange(e.target.value)}
453
+ placeholder="Buscar…"
454
+ className="w-full max-w-xs text-xs border border-gray-300 rounded px-2 py-1 outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-200 transition"
455
+ />
456
+ {value && (
457
+ <button
458
+ onClick={() => onChange("")}
459
+ className="text-gray-400 hover:text-gray-600 text-sm leading-none"
460
+ title="Limpiar búsqueda"
461
+ >
462
+ &times;
463
+ </button>
464
+ )}
465
+ </div>
466
+ );
467
+ }
468
+
469
+ /** Devuelve true si la fila contiene TODAS las palabras del query */
470
+ function rowMatchesSearch(row: GridValidRowModel, query: string): boolean {
471
+ if (!query.trim()) return true;
472
+ const words = query.trim().toLowerCase().split(/\s+/);
473
+ const rowText = Object.values(row)
474
+ .filter((v) => v != null && v !== "")
475
+ .join(" ")
476
+ .toLowerCase();
477
+ return words.every((word) => rowText.includes(word));
478
+ }
370
479
 
371
480
  function IHTable({
372
481
  data,
@@ -387,6 +496,10 @@ function IHTable({
387
496
  fontSize = "1rem",
388
497
  className,
389
498
  colSize,
499
+ rowHeight,
500
+ wrapText = false,
501
+ density = "standard",
502
+ searchable = false,
390
503
  }: TableProps) {
391
504
  if (modal && onSelect)
392
505
  throw new Error("Solo se puede usar modal o onSelect por separado");
@@ -398,6 +511,7 @@ function IHTable({
398
511
  ids: new Set(),
399
512
  });
400
513
  const [modalRow, setModalRow] = useState<GridValidRowModel | undefined>();
514
+ const [searchQuery, setSearchQuery] = useState("");
401
515
 
402
516
  // Inject Modal button key if modal is provided
403
517
  const resolvedButtons = modal ? { ...buttons, Modal: "" } : buttons;
@@ -437,8 +551,15 @@ function IHTable({
437
551
  return [];
438
552
  }, [selectedRows, rows]);
439
553
 
554
+ // Rows after applying the search filter (only when searchable is enabled)
555
+ const displayRows = useMemo(
556
+ () =>
557
+ searchable ? rows.filter((r) => rowMatchesSearch(r, searchQuery)) : rows,
558
+ [rows, searchQuery, searchable],
559
+ );
560
+
440
561
  const columns = useColumns(
441
- rows,
562
+ displayRows,
442
563
  currentCoin,
443
564
  {
444
565
  flex,
@@ -446,6 +567,7 @@ function IHTable({
446
567
  buttons: resolvedButtons,
447
568
  hideColumns,
448
569
  modal,
570
+ wrapText,
449
571
  handleRowUpdate,
450
572
  onModalOpen: handleModalOpen,
451
573
  },
@@ -454,22 +576,34 @@ function IHTable({
454
576
 
455
577
  if (!rows.length) return null;
456
578
 
579
+ // When wrapText is active we must use getRowHeight; rowHeight prop is ignored.
580
+ const rowHeightProps = wrapText
581
+ ? { getRowHeight: () => "auto" as const }
582
+ : rowHeight != null
583
+ ? { rowHeight }
584
+ : {};
585
+
457
586
  return (
458
587
  <div className="m-2">
459
588
  {header && <div className="font-bold p-2 ">{header}</div>}
460
- <Toolbar
461
- exportName={exportName}
462
- onSave={onSave}
463
- onSelect={onSelect}
464
- rows={rows}
465
- filteredRows={filteredRows}
466
- />
589
+ <div className="flex justify-between">
590
+ <Toolbar
591
+ exportName={exportName}
592
+ onSave={onSave}
593
+ onSelect={onSelect}
594
+ rows={rows}
595
+ filteredRows={filteredRows}
596
+ />
597
+ {searchable && (
598
+ <SearchBar value={searchQuery} onChange={setSearchQuery} />
599
+ )}
600
+ </div>
467
601
 
468
602
  <Box
469
603
  sx={{
470
604
  display: "flex",
471
605
  flexDirection: "column",
472
- height: HEIGHT_MAP[rows.length] ?? height,
606
+ height: HEIGHT_MAP[displayRows.length] ?? height,
473
607
  width,
474
608
  zIndex: 999999999,
475
609
  }}
@@ -485,24 +619,42 @@ function IHTable({
485
619
 
486
620
  <DataGrid
487
621
  className={className}
488
- rows={rows}
622
+ rows={displayRows}
489
623
  columns={columns as any}
624
+ density={density}
490
625
  checkboxSelection={Boolean(onSelect)}
491
626
  rowSelectionModel={selectedRows}
492
627
  onRowSelectionModelChange={!modal ? setSelectedRows : undefined}
628
+ {...rowHeightProps}
493
629
  sx={{
494
- fontSize, // equivalente a text-xs
630
+ fontSize,
495
631
  "& .MuiDataGrid-cell": {
496
632
  fontSize,
633
+ // Allow cells to wrap text when wrapText is enabled
634
+ ...(wrapText && {
635
+ alignItems: "flex-start",
636
+ paddingTop: "8px",
637
+ paddingBottom: "8px",
638
+ whiteSpace: "normal",
639
+ wordBreak: "break-word",
640
+ }),
497
641
  },
498
642
  "& .MuiDataGrid-columnHeader": {
499
643
  fontSize,
644
+ ...(wrapText && {
645
+ whiteSpace: "normal",
646
+ "& .MuiDataGrid-columnHeaderTitle": {
647
+ whiteSpace: "normal",
648
+ lineHeight: 1.3,
649
+ wordBreak: "break-word",
650
+ },
651
+ }),
500
652
  },
501
653
  "& .MuiDataGrid-cell--editable": {
502
654
  backgroundColor: "#c6d8f0",
503
655
  fontWeight: 500,
504
656
  },
505
- ...(rows.length <= Object.keys(HEIGHT_MAP).length && {
657
+ ...(displayRows.length <= Object.keys(HEIGHT_MAP).length && {
506
658
  "& .MuiDataGrid-filler": {
507
659
  display: "none",
508
660
  },
@@ -511,10 +663,10 @@ function IHTable({
511
663
  editMode="row"
512
664
  processRowUpdate={handleRowUpdate}
513
665
  pageSizeOptions={[5, 10]}
514
- hideFooter={rows.length <= Object.keys(HEIGHT_MAP).length}
666
+ hideFooter={displayRows.length <= Object.keys(HEIGHT_MAP).length}
515
667
  />
516
668
  </Box>
517
- <CustomFooter footer={footer} rows={rows} />
669
+ <CustomFooter footer={footer} rows={displayRows} />
518
670
  </div>
519
671
  );
520
672
  }