next-recomponents 2.0.30 → 2.0.32

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { DataGrid, GridRowModel, GridValidRowModel } from "@mui/x-data-grid";
4
4
  import { Box, Dialog } from "@mui/material";
5
- import React, { useEffect, useMemo, useRef, useState } from "react";
5
+ import React, { useEffect, useMemo, useState } from "react";
6
6
  import useExcel from "../use-excel";
7
7
  import Button from "../button";
8
8
  import regularExpresions from "../regular-expresions";
@@ -26,7 +26,6 @@ type GridDensity = "compact" | "standard" | "comfortable";
26
26
 
27
27
  interface TableProps {
28
28
  data: any;
29
- /** Muestra un input de búsqueda general que filtra filas por palabras clave */
30
29
  searchable?: boolean;
31
30
  flex?: number;
32
31
  editableFields?: string[];
@@ -48,35 +47,9 @@ interface TableProps {
48
47
  className?: string;
49
48
  fontSize?: string;
50
49
  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
50
  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
51
  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
52
  density?: GridDensity;
73
- /**
74
- * Cuando es true, el alto del contenedor se ajusta automáticamente
75
- * al contenido de la tabla, eliminando el scroll vertical.
76
- * Compatible con `wrapText` (filas de alto variable) y con
77
- * cualquier valor de `density`. Cuando está activo, las props
78
- * `height` y `HEIGHT_MAP` se ignoran.
79
- */
80
53
  autoHeight?: boolean;
81
54
  [key: string]: any;
82
55
  }
@@ -114,7 +87,7 @@ function EditIcon() {
114
87
  );
115
88
  }
116
89
 
117
- // ─── KeyValueTable (non-array data) ──────────────────────────────────────────
90
+ // ─── KeyValueTable ────────────────────────────────────────────────────────────
118
91
 
119
92
  function KeyValueTable({ data }: { data: Record<string, any> }) {
120
93
  return (
@@ -139,7 +112,7 @@ function KeyValueTable({ data }: { data: Record<string, any> }) {
139
112
  );
140
113
  }
141
114
 
142
- // ─── CustomFooter ─────────────────────────────────────────────────────────────
115
+ // ─── Footer ───────────────────────────────────────────────────────────────────
143
116
 
144
117
  const FOOTER_LABELS: Record<FooterAggregation, string> = {
145
118
  sum: "Suma",
@@ -160,6 +133,7 @@ function computeAggregation(
160
133
  return values.length
161
134
  ? values.reduce((acc, v) => acc + v, 0) / values.length
162
135
  : 0;
136
+
163
137
  return 0;
164
138
  }
165
139
 
@@ -171,21 +145,19 @@ function CustomFooter({
171
145
  footer: FooterType;
172
146
  }) {
173
147
  const entries = Object.entries(footer);
148
+
174
149
  if (!entries.length) return null;
175
150
 
176
151
  return (
177
152
  <div className="flex justify-end gap-6 px-4 py-2 bg-gray-100 border-t border-gray-300 text-sm font-semibold text-gray-700">
178
153
  {entries.map(([key, type]) => {
179
154
  const value = computeAggregation(rows, key, type);
180
- const formatted = value.toLocaleString("en-US", {
181
- minimumFractionDigits: value % 1 !== 0 ? 2 : 0,
182
- maximumFractionDigits: value % 1 !== 0 ? 2 : 0,
183
- });
155
+
184
156
  return (
185
157
  <span key={key}>
186
158
  {FOOTER_LABELS[type]} de{" "}
187
159
  <span className="text-gray-900">{key}</span>:{" "}
188
- <span className="text-blue-700">{formatted}</span>
160
+ <span className="text-blue-700">{value.toLocaleString()}</span>
189
161
  </span>
190
162
  );
191
163
  })}
@@ -193,31 +165,67 @@ function CustomFooter({
193
165
  );
194
166
  }
195
167
 
196
- // ─── ModalDialog ──────────────────────────────────────────────────────────────
168
+ // ─── Modal ────────────────────────────────────────────────────────────────────
197
169
 
198
170
  interface ModalDialogProps {
199
171
  open: boolean;
200
172
  onClose: () => void;
201
173
  modal: React.ReactNode;
202
174
  selectedRow: GridValidRowModel | undefined;
175
+ onPrev: () => void;
176
+ onNext: () => void;
177
+ hasPrev: boolean;
178
+ hasNext: boolean;
203
179
  }
204
180
 
205
- function ModalDialog({ open, onClose, modal, selectedRow }: ModalDialogProps) {
181
+ function ModalDialog({
182
+ open,
183
+ onClose,
184
+ modal,
185
+ selectedRow,
186
+ onPrev,
187
+ onNext,
188
+ hasPrev,
189
+ hasNext,
190
+ }: ModalDialogProps) {
206
191
  return (
207
192
  <Dialog open={open} maxWidth="xl" fullWidth>
208
- <div className="flex justify-end">
193
+ <div className="flex items-center justify-between p-4 border-b">
194
+ <div className="flex gap-2">
195
+ <button
196
+ disabled={!hasPrev}
197
+ onClick={onPrev}
198
+ className="px-4 py-2 rounded border bg-gray-100 hover:bg-gray-200 disabled:opacity-40"
199
+ >
200
+ ← Anterior
201
+ </button>
202
+
203
+ <button
204
+ disabled={!hasNext}
205
+ onClick={onNext}
206
+ className="px-4 py-2 rounded border bg-gray-100 hover:bg-gray-200 disabled:opacity-40"
207
+ >
208
+ Siguiente →
209
+ </button>
210
+ </div>
211
+
209
212
  <button
210
213
  onClick={onClose}
211
- className=" font-bold p-1 m-5 w-[30px] h-[30px] flex items-center justify-center hover:animate-pulse border shadow rounded bg-red-500 text-white"
214
+ className="font-bold w-[35px] h-[35px] flex items-center justify-center hover:animate-pulse border shadow rounded bg-red-500 text-white"
212
215
  >
213
216
  &times;
214
217
  </button>
215
218
  </div>
219
+
216
220
  <div className="mt-4 m-auto p-5">
217
221
  {selectedRow &&
218
222
  React.cloneElement(
219
223
  modal as React.ReactElement,
220
- { row: selectedRow, hide: onClose } as any,
224
+ {
225
+ key: selectedRow.id,
226
+ row: selectedRow,
227
+ hide: onClose,
228
+ } as any,
221
229
  )}
222
230
  </div>
223
231
  </Dialog>
@@ -226,21 +234,7 @@ function ModalDialog({ open, onClose, modal, selectedRow }: ModalDialogProps) {
226
234
 
227
235
  // ─── Toolbar ──────────────────────────────────────────────────────────────────
228
236
 
229
- interface ToolbarProps {
230
- exportName?: string;
231
- onSave?: (data: GridValidRowModel[]) => void;
232
- onSelect?: (data: GridValidRowModel[]) => void;
233
- rows: GridValidRowModel[];
234
- filteredRows: GridValidRowModel[];
235
- }
236
-
237
- function Toolbar({
238
- exportName,
239
- onSave,
240
- onSelect,
241
- rows,
242
- filteredRows,
243
- }: ToolbarProps) {
237
+ function Toolbar({ exportName, onSave, onSelect, rows, filteredRows }: any) {
244
238
  const excel = useExcel();
245
239
 
246
240
  const stripMeta = (r: GridValidRowModel) => {
@@ -251,7 +245,7 @@ function Toolbar({
251
245
  if (!exportName && !onSave && !onSelect) return null;
252
246
 
253
247
  return (
254
- <div className="flex gap-2 p-2 text-xs">
248
+ <div className="flex gap-2 p-2 text-xs">
255
249
  {exportName && (
256
250
  <Button
257
251
  className="bg-green-800 text-white"
@@ -262,6 +256,7 @@ function Toolbar({
262
256
  Exportar
263
257
  </Button>
264
258
  )}
259
+
265
260
  {onSelect ? (
266
261
  <Button
267
262
  disabled={filteredRows.length === 0}
@@ -279,158 +274,7 @@ function Toolbar({
279
274
  );
280
275
  }
281
276
 
282
- // ─── useColumns ───────────────────────────────────────────────────────────────
283
-
284
- function useColumns(
285
- rows: GridValidRowModel[],
286
- currentCoin: string,
287
- options: {
288
- flex: number;
289
- editableFields?: string[];
290
- buttons?: Record<string, any>;
291
- hideColumns: string[];
292
- modal?: React.ReactNode;
293
- wrapText?: boolean;
294
- handleRowUpdate: (row: GridRowModel) => GridRowModel;
295
- onModalOpen: (row: GridValidRowModel) => void;
296
- },
297
- colSize?: Record<string, number>,
298
- ) {
299
- const {
300
- flex,
301
- editableFields,
302
- buttons,
303
- hideColumns,
304
- modal,
305
- wrapText,
306
- handleRowUpdate,
307
- onModalOpen,
308
- } = options;
309
-
310
- return useMemo(() => {
311
- if (!rows.length) return [];
312
-
313
- const cols = Object.keys(rows[0])
314
- .filter((key) => !key.startsWith("_") && !hideColumns.includes(key))
315
- .map((key) => ({
316
- field: key,
317
- headerName: key,
318
- valueFormatter: (value: any) => {
319
- if (value == null || value === "") return "";
320
- const isDate = /(\d{4}-\d{2}-\d{2})(T[\d:,.+-]*(Z)?)?/;
321
- if (`${value}`.match(isDate)) {
322
- return value
323
- .toString()
324
- .split("T")[0]
325
- .split("-")
326
- .reverse()
327
- .join("/");
328
- }
329
-
330
- const splited = `${value}`.split(".");
331
- const hasDecimals =
332
- splited.length == 2 &&
333
- splited.every((v: any) => `${v}`.match(regularExpresions.number));
334
-
335
- if (hasDecimals) {
336
- return [
337
- currentCoin,
338
- (+`${value}`).toLocaleString("en-US", {
339
- minimumFractionDigits: 2,
340
- maximumFractionDigits: 2,
341
- }),
342
- ].join(" ");
343
- }
344
-
345
- const isNumber = typeof value === "number";
346
-
347
- if (isNumber) {
348
- if (isNaN(value)) return value;
349
- return value;
350
- }
351
-
352
- return value;
353
- },
354
- flex: key == "id" ? false : !colSize?.[key],
355
- width: key == "id" ? 80 : (colSize?.[key] ?? undefined),
356
- editable: editableFields?.includes(key) ?? false,
357
- type: typeof rows[0][key] === "number" ? "number" : "string",
358
- // When wrapText is enabled, allow cells to grow vertically
359
- ...(wrapText && {
360
- renderHeader: (params: any) => (
361
- <span
362
- style={{
363
- whiteSpace: "normal",
364
- lineHeight: 1.3,
365
- wordBreak: "break-word",
366
- }}
367
- >
368
- {params.colDef.headerName}
369
- </span>
370
- ),
371
- }),
372
- renderCell: buttons?.[key]
373
- ? (params: any) => {
374
- const children =
375
- buttons?.[key]?.type == "input" ? null : params?.row?.[key];
376
-
377
- return React.cloneElement(buttons[key], {
378
- className: `${params?.className ?? ""} m-auto text-xs`,
379
- children,
380
- row: params?.row,
381
- onClick: async (e: TableButtonProps) => {
382
- e.row = params?.row;
383
- if (buttons[key]?.props?.onClick) {
384
- const newVal = await buttons[key].props.onClick(e);
385
-
386
- if (newVal) handleRowUpdate({ ...e.row, newVal });
387
- }
388
- },
389
- });
390
- }
391
- : wrapText
392
- ? (params: any) => (
393
- // Plain cell with wrap — no custom button
394
- <span
395
- style={{
396
- whiteSpace: "normal",
397
- wordBreak: "break-word",
398
- lineHeight: 1.4,
399
- padding: "6px 0",
400
- display: "block",
401
- }}
402
- >
403
- {params.formattedValue ?? params.value}
404
- </span>
405
- )
406
- : null,
407
- }));
408
-
409
- if (modal) {
410
- cols.unshift({
411
- field: "Modal",
412
- headerName: "Modal",
413
- editable: false,
414
- type: "string",
415
- width: 10,
416
- renderCell: (params: any) =>
417
- (
418
- <Button
419
- className="text-xs"
420
- onClick={() => onModalOpen(params.row)}
421
- icon={<EditIcon />}
422
- >
423
- {params?.row?.["Modal"]}
424
- </Button>
425
- ) as any,
426
- } as any);
427
- }
428
-
429
- return cols;
430
- }, [rows, wrapText]);
431
- }
432
-
433
- // ─── SearchBar ────────────────────────────────────────────────────────────────
277
+ // ─── Search ───────────────────────────────────────────────────────────────────
434
278
 
435
279
  function SearchBar({
436
280
  value,
@@ -441,50 +285,32 @@ function SearchBar({
441
285
  }) {
442
286
  return (
443
287
  <div className="flex items-center gap-2 px-2 pb-1">
444
- <svg
445
- className="text-gray-400 shrink-0"
446
- width="14"
447
- height="14"
448
- viewBox="0 0 20 20"
449
- fill="currentColor"
450
- >
451
- <path
452
- fillRule="evenodd"
453
- 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"
454
- clipRule="evenodd"
455
- />
456
- </svg>
457
288
  <input
458
289
  type="text"
459
290
  value={value}
460
291
  onChange={(e) => onChange(e.target.value)}
461
292
  placeholder="Buscar…"
462
- 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"
293
+ className="w-full max-w-xs text-xs border border-gray-300 rounded px-2 py-1"
463
294
  />
464
- {value && (
465
- <button
466
- onClick={() => onChange("")}
467
- className="text-gray-400 hover:text-gray-600 text-sm leading-none"
468
- title="Limpiar búsqueda"
469
- >
470
- &times;
471
- </button>
472
- )}
473
295
  </div>
474
296
  );
475
297
  }
476
298
 
477
- /** Devuelve true si la fila contiene TODAS las palabras del query */
478
299
  function rowMatchesSearch(row: GridValidRowModel, query: string): boolean {
479
300
  if (!query.trim()) return true;
301
+
480
302
  const words = query.trim().toLowerCase().split(/\s+/);
303
+
481
304
  const rowText = Object.values(row)
482
305
  .filter((v) => v != null && v !== "")
483
306
  .join(" ")
484
307
  .toLowerCase();
308
+
485
309
  return words.every((word) => rowText.includes(word));
486
310
  }
487
311
 
312
+ // ─── Main Table ───────────────────────────────────────────────────────────────
313
+
488
314
  function IHTable({
489
315
  data,
490
316
  flex = 1,
@@ -510,108 +336,203 @@ function IHTable({
510
336
  searchable = false,
511
337
  autoHeight = false,
512
338
  }: TableProps) {
513
- if (modal && onSelect)
514
- throw new Error("Solo se puede usar modal o onSelect por separado");
515
-
516
339
  const [open, setOpen] = useState(false);
340
+
517
341
  const [rows, setRows] = useState<GridValidRowModel[]>(data);
342
+
343
+ const [modalIndex, setModalIndex] = useState<number | null>(null);
344
+
518
345
  const [selectedRows, setSelectedRows] = useState<any>({
519
346
  type: "include",
520
347
  ids: new Set(),
521
348
  });
522
- const [modalRow, setModalRow] = useState<GridValidRowModel | undefined>();
523
- const [searchQuery, setSearchQuery] = useState("");
524
349
 
525
- // Inject Modal button key if modal is provided
526
- const resolvedButtons = modal ? { ...buttons, Modal: "" } : buttons;
350
+ const [searchQuery, setSearchQuery] = useState("");
527
351
 
528
352
  useEffect(() => {
529
353
  setRows(data);
530
354
  }, [data]);
531
355
 
356
+ const filteredRows = useMemo(() => {
357
+ if (selectedRows?.type === "exclude") {
358
+ return rows.filter((r) => !Array.from(selectedRows.ids).includes(r.id));
359
+ }
360
+
361
+ if (selectedRows?.type === "include") {
362
+ return rows.filter((r) => Array.from(selectedRows.ids).includes(r.id));
363
+ }
364
+
365
+ return [];
366
+ }, [selectedRows, rows]);
367
+
368
+ const displayRows = useMemo(
369
+ () =>
370
+ searchable ? rows.filter((r) => rowMatchesSearch(r, searchQuery)) : rows,
371
+ [rows, searchQuery, searchable],
372
+ );
373
+
374
+ const modalRow = modalIndex != null ? displayRows[modalIndex] : undefined;
375
+
532
376
  const handleModalOpen = (row: GridValidRowModel) => {
533
- setModalRow(row);
377
+ const index = displayRows.findIndex((r) => r.id === row.id);
378
+
379
+ if (index === -1) return;
380
+
381
+ setModalIndex(index);
534
382
  setOpen(true);
535
383
  };
384
+
536
385
  const handleClose = async () => {
537
- const pass = onCloseModal ? await onCloseModal?.(modalRow) : true;
386
+ const pass = onCloseModal ? await onCloseModal(modalRow) : true;
538
387
 
539
388
  if (!pass) return;
389
+
540
390
  setOpen(false);
541
- setModalRow(undefined);
391
+ setModalIndex(null);
392
+ };
393
+
394
+ const handlePrevRow = () => {
395
+ setModalIndex((prev) => {
396
+ if (prev == null) return prev;
397
+ return Math.max(prev - 1, 0);
398
+ });
399
+ };
400
+
401
+ const handleNextRow = () => {
402
+ setModalIndex((prev) => {
403
+ if (prev == null) return prev;
404
+ return Math.min(prev + 1, displayRows.length - 1);
405
+ });
542
406
  };
543
407
 
544
408
  const handleRowUpdate = (newRow: GridRowModel) => {
545
409
  if (!newRow.id) throw new Error("Fila sin id");
546
- const updated = { ...newRow, _edited: true } as any;
410
+
411
+ const updated: any = { ...newRow, _edited: true };
412
+
547
413
  setRows((prev) =>
548
414
  prev.map((row) => (row.id === updated.id ? updated : row)),
549
415
  );
416
+
550
417
  return updated;
551
418
  };
552
419
 
553
- const filteredRows = useMemo(() => {
554
- if (selectedRows?.type === "exclude") {
555
- return rows.filter((r) => !Array.from(selectedRows.ids).includes(r.id));
556
- }
557
- if (selectedRows?.type === "include") {
558
- return rows.filter((r) => Array.from(selectedRows.ids).includes(r.id));
559
- }
560
- return [];
561
- }, [selectedRows, rows]);
420
+ const resolvedButtons = modal ? { ...buttons, Modal: "" } : buttons;
562
421
 
563
- // Rows after applying the search filter (only when searchable is enabled)
564
- const displayRows = useMemo(
565
- () =>
566
- searchable ? rows.filter((r) => rowMatchesSearch(r, searchQuery)) : rows,
567
- [rows, searchQuery, searchable],
568
- );
422
+ const columns = useMemo(() => {
423
+ if (!displayRows.length) return [];
569
424
 
570
- const columns = useColumns(
571
- displayRows,
572
- currentCoin,
573
- {
574
- flex,
575
- editableFields,
576
- buttons: resolvedButtons,
577
- hideColumns,
578
- modal,
579
- wrapText,
580
- handleRowUpdate,
581
- onModalOpen: handleModalOpen,
582
- },
583
- colSize,
584
- );
425
+ const cols = Object.keys(displayRows[0])
426
+ .filter((key) => !key.startsWith("_") && !hideColumns.includes(key))
427
+ .map((key) => ({
428
+ field: key,
429
+ headerName: key,
430
+ flex: key == "id" ? false : !colSize?.[key],
431
+ width: key == "id" ? 80 : (colSize?.[key] ?? undefined),
432
+ editable: editableFields?.includes(key) ?? false,
433
+ type: typeof displayRows[0][key] === "number" ? "number" : "string",
434
+
435
+ valueFormatter: (value: any) => {
436
+ if (value == null || value === "") return "";
437
+
438
+ const isDate = /(\d{4}-\d{2}-\d{2})(T[\d:,.+-]*(Z)?)?/;
439
+
440
+ if (`${value}`.match(isDate)) {
441
+ return value
442
+ .toString()
443
+ .split("T")[0]
444
+ .split("-")
445
+ .reverse()
446
+ .join("/");
447
+ }
448
+
449
+ const splited = `${value}`.split(".");
450
+
451
+ const hasDecimals =
452
+ splited.length == 2 &&
453
+ splited.every((v: any) => `${v}`.match(regularExpresions.number));
454
+
455
+ if (hasDecimals) {
456
+ return [
457
+ currentCoin,
458
+ (+`${value}`).toLocaleString("en-US", {
459
+ minimumFractionDigits: 2,
460
+ maximumFractionDigits: 2,
461
+ }),
462
+ ].join(" ");
463
+ }
464
+
465
+ return value;
466
+ },
467
+
468
+ renderCell: resolvedButtons?.[key]
469
+ ? (params: any) => {
470
+ const children =
471
+ resolvedButtons?.[key]?.type == "input"
472
+ ? null
473
+ : params?.row?.[key];
474
+
475
+ return React.cloneElement(resolvedButtons[key], {
476
+ className: `${params?.className ?? ""} m-auto text-xs`,
477
+ children,
478
+ row: params?.row,
479
+
480
+ onClick: async (e: TableButtonProps) => {
481
+ e.row = params?.row;
482
+
483
+ if (resolvedButtons[key]?.props?.onClick) {
484
+ const newVal = await resolvedButtons[key].props.onClick(e);
485
+
486
+ if (newVal) {
487
+ handleRowUpdate({ ...e.row, newVal });
488
+ }
489
+ }
490
+ },
491
+ });
492
+ }
493
+ : null,
494
+ }));
495
+
496
+ if (modal) {
497
+ cols.unshift({
498
+ field: "Modal",
499
+ headerName: "Modal",
500
+ width: 100,
501
+
502
+ renderCell: (params: any) => (
503
+ <Button
504
+ className="text-xs"
505
+ onClick={() => handleModalOpen(params.row)}
506
+ icon={<EditIcon />}
507
+ >
508
+ Abrir
509
+ </Button>
510
+ ),
511
+ } as any);
512
+ }
513
+
514
+ return cols;
515
+ }, [displayRows]);
585
516
 
586
517
  if (!rows.length) return null;
587
518
 
588
- // ─── Row height props ──────────────────────────────────────────────────────
589
- // Priority: wrapText always wins (variable row height).
590
- // autoHeight delegates height control to MUI; rowHeight prop is still
591
- // respected when neither wrapText nor autoHeight forces auto sizing.
592
519
  const rowHeightProps = wrapText
593
520
  ? { getRowHeight: () => "auto" as const }
594
521
  : rowHeight != null
595
522
  ? { rowHeight }
596
523
  : {};
597
524
 
598
- // ─── Container height ──────────────────────────────────────────────────────
599
- // When autoHeight is active the Box must NOT constrain the height so that
600
- // MUI's own autoHeight can expand the grid to fit all rows without scroll.
601
- // We still honour HEIGHT_MAP for small datasets when autoHeight is off,
602
- // falling back to the explicit `height` prop for larger ones.
603
525
  const containerHeight = autoHeight
604
- ? undefined // let MUI decide
526
+ ? undefined
605
527
  : (HEIGHT_MAP[displayRows.length] ?? height);
606
528
 
607
- // Whether to hide MUI's built-in pagination footer.
608
- // With autoHeight all rows are visible, so pagination is never needed.
609
529
  const hideFooter =
610
530
  autoHeight || displayRows.length <= Object.keys(HEIGHT_MAP).length;
611
531
 
612
532
  return (
613
533
  <div className="m-2">
614
- {header && <div className="font-bold p-2 ">{header}</div>}
534
+ {header && <div className="font-bold p-2">{header}</div>}
535
+
615
536
  <div className="flex justify-between">
616
537
  <Toolbar
617
538
  exportName={exportName}
@@ -620,6 +541,7 @@ function IHTable({
620
541
  rows={rows}
621
542
  filteredRows={filteredRows}
622
543
  />
544
+
623
545
  {searchable && (
624
546
  <SearchBar value={searchQuery} onChange={setSearchQuery} />
625
547
  )}
@@ -629,10 +551,8 @@ function IHTable({
629
551
  sx={{
630
552
  display: "flex",
631
553
  flexDirection: "column",
632
- // undefined height lets the child DataGrid size itself freely
633
554
  height: containerHeight,
634
555
  width,
635
- zIndex: 999999999,
636
556
  }}
637
557
  >
638
558
  {modal && (
@@ -641,6 +561,10 @@ function IHTable({
641
561
  onClose={handleClose}
642
562
  modal={modal}
643
563
  selectedRow={modalRow}
564
+ onPrev={handlePrevRow}
565
+ onNext={handleNextRow}
566
+ hasPrev={(modalIndex ?? 0) > 0}
567
+ hasNext={(modalIndex ?? -1) < displayRows.length - 1}
644
568
  />
645
569
  )}
646
570
 
@@ -649,8 +573,6 @@ function IHTable({
649
573
  rows={displayRows}
650
574
  columns={columns as any}
651
575
  density={density}
652
- // MUI's native autoHeight prop — renders all rows and sizes the
653
- // grid to fit them, regardless of density or wrapText.
654
576
  autoHeight={autoHeight}
655
577
  checkboxSelection={Boolean(onSelect)}
656
578
  rowSelectionModel={selectedRows}
@@ -658,37 +580,19 @@ function IHTable({
658
580
  {...rowHeightProps}
659
581
  sx={{
660
582
  fontSize,
583
+
661
584
  "& .MuiDataGrid-cell": {
662
585
  fontSize,
663
- // Allow cells to wrap text when wrapText is enabled
664
- ...(wrapText && {
665
- alignItems: "flex-start",
666
- paddingTop: "8px",
667
- paddingBottom: "8px",
668
- whiteSpace: "normal",
669
- wordBreak: "break-word",
670
- }),
671
586
  },
587
+
672
588
  "& .MuiDataGrid-columnHeader": {
673
589
  fontSize,
674
- ...(wrapText && {
675
- whiteSpace: "normal",
676
- "& .MuiDataGrid-columnHeaderTitle": {
677
- whiteSpace: "normal",
678
- lineHeight: 1.3,
679
- wordBreak: "break-word",
680
- },
681
- }),
682
590
  },
591
+
683
592
  "& .MuiDataGrid-cell--editable": {
684
593
  backgroundColor: "#c6d8f0",
685
594
  fontWeight: 500,
686
595
  },
687
- ...(displayRows.length <= Object.keys(HEIGHT_MAP).length && {
688
- "& .MuiDataGrid-filler": {
689
- display: "none",
690
- },
691
- }),
692
596
  }}
693
597
  editMode="row"
694
598
  processRowUpdate={handleRowUpdate}
@@ -696,16 +600,18 @@ function IHTable({
696
600
  hideFooter={hideFooter}
697
601
  />
698
602
  </Box>
603
+
699
604
  <CustomFooter footer={footer} rows={displayRows} />
700
605
  </div>
701
606
  );
702
607
  }
703
608
 
704
- // ─── Public export ────────────────────────────────────────────────────────────
609
+ // ─── Export ───────────────────────────────────────────────────────────────────
705
610
 
706
611
  export default function Table(props: TableProps) {
707
612
  if (Array.isArray(props.data)) {
708
613
  return <IHTable {...props} />;
709
614
  }
615
+
710
616
  return <KeyValueTable data={props.data} />;
711
617
  }