next-recomponents 2.0.30 → 2.0.31

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,66 @@ 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
+ row: selectedRow,
226
+ hide: onClose,
227
+ } as any,
221
228
  )}
222
229
  </div>
223
230
  </Dialog>
@@ -226,21 +233,7 @@ function ModalDialog({ open, onClose, modal, selectedRow }: ModalDialogProps) {
226
233
 
227
234
  // ─── Toolbar ──────────────────────────────────────────────────────────────────
228
235
 
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) {
236
+ function Toolbar({ exportName, onSave, onSelect, rows, filteredRows }: any) {
244
237
  const excel = useExcel();
245
238
 
246
239
  const stripMeta = (r: GridValidRowModel) => {
@@ -251,7 +244,7 @@ function Toolbar({
251
244
  if (!exportName && !onSave && !onSelect) return null;
252
245
 
253
246
  return (
254
- <div className="flex gap-2 p-2 text-xs">
247
+ <div className="flex gap-2 p-2 text-xs">
255
248
  {exportName && (
256
249
  <Button
257
250
  className="bg-green-800 text-white"
@@ -262,6 +255,7 @@ function Toolbar({
262
255
  Exportar
263
256
  </Button>
264
257
  )}
258
+
265
259
  {onSelect ? (
266
260
  <Button
267
261
  disabled={filteredRows.length === 0}
@@ -279,158 +273,7 @@ function Toolbar({
279
273
  );
280
274
  }
281
275
 
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 ────────────────────────────────────────────────────────────────
276
+ // ─── Search ───────────────────────────────────────────────────────────────────
434
277
 
435
278
  function SearchBar({
436
279
  value,
@@ -441,50 +284,32 @@ function SearchBar({
441
284
  }) {
442
285
  return (
443
286
  <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
287
  <input
458
288
  type="text"
459
289
  value={value}
460
290
  onChange={(e) => onChange(e.target.value)}
461
291
  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"
292
+ className="w-full max-w-xs text-xs border border-gray-300 rounded px-2 py-1"
463
293
  />
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
294
  </div>
474
295
  );
475
296
  }
476
297
 
477
- /** Devuelve true si la fila contiene TODAS las palabras del query */
478
298
  function rowMatchesSearch(row: GridValidRowModel, query: string): boolean {
479
299
  if (!query.trim()) return true;
300
+
480
301
  const words = query.trim().toLowerCase().split(/\s+/);
302
+
481
303
  const rowText = Object.values(row)
482
304
  .filter((v) => v != null && v !== "")
483
305
  .join(" ")
484
306
  .toLowerCase();
307
+
485
308
  return words.every((word) => rowText.includes(word));
486
309
  }
487
310
 
311
+ // ─── Main Table ───────────────────────────────────────────────────────────────
312
+
488
313
  function IHTable({
489
314
  data,
490
315
  flex = 1,
@@ -510,108 +335,203 @@ function IHTable({
510
335
  searchable = false,
511
336
  autoHeight = false,
512
337
  }: TableProps) {
513
- if (modal && onSelect)
514
- throw new Error("Solo se puede usar modal o onSelect por separado");
515
-
516
338
  const [open, setOpen] = useState(false);
339
+
517
340
  const [rows, setRows] = useState<GridValidRowModel[]>(data);
341
+
342
+ const [modalIndex, setModalIndex] = useState<number | null>(null);
343
+
518
344
  const [selectedRows, setSelectedRows] = useState<any>({
519
345
  type: "include",
520
346
  ids: new Set(),
521
347
  });
522
- const [modalRow, setModalRow] = useState<GridValidRowModel | undefined>();
523
- const [searchQuery, setSearchQuery] = useState("");
524
348
 
525
- // Inject Modal button key if modal is provided
526
- const resolvedButtons = modal ? { ...buttons, Modal: "" } : buttons;
349
+ const [searchQuery, setSearchQuery] = useState("");
527
350
 
528
351
  useEffect(() => {
529
352
  setRows(data);
530
353
  }, [data]);
531
354
 
355
+ const filteredRows = useMemo(() => {
356
+ if (selectedRows?.type === "exclude") {
357
+ return rows.filter((r) => !Array.from(selectedRows.ids).includes(r.id));
358
+ }
359
+
360
+ if (selectedRows?.type === "include") {
361
+ return rows.filter((r) => Array.from(selectedRows.ids).includes(r.id));
362
+ }
363
+
364
+ return [];
365
+ }, [selectedRows, rows]);
366
+
367
+ const displayRows = useMemo(
368
+ () =>
369
+ searchable ? rows.filter((r) => rowMatchesSearch(r, searchQuery)) : rows,
370
+ [rows, searchQuery, searchable],
371
+ );
372
+
373
+ const modalRow = modalIndex != null ? displayRows[modalIndex] : undefined;
374
+
532
375
  const handleModalOpen = (row: GridValidRowModel) => {
533
- setModalRow(row);
376
+ const index = displayRows.findIndex((r) => r.id === row.id);
377
+
378
+ if (index === -1) return;
379
+
380
+ setModalIndex(index);
534
381
  setOpen(true);
535
382
  };
383
+
536
384
  const handleClose = async () => {
537
- const pass = onCloseModal ? await onCloseModal?.(modalRow) : true;
385
+ const pass = onCloseModal ? await onCloseModal(modalRow) : true;
538
386
 
539
387
  if (!pass) return;
388
+
540
389
  setOpen(false);
541
- setModalRow(undefined);
390
+ setModalIndex(null);
391
+ };
392
+
393
+ const handlePrevRow = () => {
394
+ setModalIndex((prev) => {
395
+ if (prev == null) return prev;
396
+ return Math.max(prev - 1, 0);
397
+ });
398
+ };
399
+
400
+ const handleNextRow = () => {
401
+ setModalIndex((prev) => {
402
+ if (prev == null) return prev;
403
+ return Math.min(prev + 1, displayRows.length - 1);
404
+ });
542
405
  };
543
406
 
544
407
  const handleRowUpdate = (newRow: GridRowModel) => {
545
408
  if (!newRow.id) throw new Error("Fila sin id");
546
- const updated = { ...newRow, _edited: true } as any;
409
+
410
+ const updated: any = { ...newRow, _edited: true };
411
+
547
412
  setRows((prev) =>
548
413
  prev.map((row) => (row.id === updated.id ? updated : row)),
549
414
  );
415
+
550
416
  return updated;
551
417
  };
552
418
 
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]);
419
+ const resolvedButtons = modal ? { ...buttons, Modal: "" } : buttons;
562
420
 
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
- );
421
+ const columns = useMemo(() => {
422
+ if (!displayRows.length) return [];
569
423
 
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
- );
424
+ const cols = Object.keys(displayRows[0])
425
+ .filter((key) => !key.startsWith("_") && !hideColumns.includes(key))
426
+ .map((key) => ({
427
+ field: key,
428
+ headerName: key,
429
+ flex: key == "id" ? false : !colSize?.[key],
430
+ width: key == "id" ? 80 : (colSize?.[key] ?? undefined),
431
+ editable: editableFields?.includes(key) ?? false,
432
+ type: typeof displayRows[0][key] === "number" ? "number" : "string",
433
+
434
+ valueFormatter: (value: any) => {
435
+ if (value == null || value === "") return "";
436
+
437
+ const isDate = /(\d{4}-\d{2}-\d{2})(T[\d:,.+-]*(Z)?)?/;
438
+
439
+ if (`${value}`.match(isDate)) {
440
+ return value
441
+ .toString()
442
+ .split("T")[0]
443
+ .split("-")
444
+ .reverse()
445
+ .join("/");
446
+ }
447
+
448
+ const splited = `${value}`.split(".");
449
+
450
+ const hasDecimals =
451
+ splited.length == 2 &&
452
+ splited.every((v: any) => `${v}`.match(regularExpresions.number));
453
+
454
+ if (hasDecimals) {
455
+ return [
456
+ currentCoin,
457
+ (+`${value}`).toLocaleString("en-US", {
458
+ minimumFractionDigits: 2,
459
+ maximumFractionDigits: 2,
460
+ }),
461
+ ].join(" ");
462
+ }
463
+
464
+ return value;
465
+ },
466
+
467
+ renderCell: resolvedButtons?.[key]
468
+ ? (params: any) => {
469
+ const children =
470
+ resolvedButtons?.[key]?.type == "input"
471
+ ? null
472
+ : params?.row?.[key];
473
+
474
+ return React.cloneElement(resolvedButtons[key], {
475
+ className: `${params?.className ?? ""} m-auto text-xs`,
476
+ children,
477
+ row: params?.row,
478
+
479
+ onClick: async (e: TableButtonProps) => {
480
+ e.row = params?.row;
481
+
482
+ if (resolvedButtons[key]?.props?.onClick) {
483
+ const newVal = await resolvedButtons[key].props.onClick(e);
484
+
485
+ if (newVal) {
486
+ handleRowUpdate({ ...e.row, newVal });
487
+ }
488
+ }
489
+ },
490
+ });
491
+ }
492
+ : null,
493
+ }));
494
+
495
+ if (modal) {
496
+ cols.unshift({
497
+ field: "Modal",
498
+ headerName: "Modal",
499
+ width: 100,
500
+
501
+ renderCell: (params: any) => (
502
+ <Button
503
+ className="text-xs"
504
+ onClick={() => handleModalOpen(params.row)}
505
+ icon={<EditIcon />}
506
+ >
507
+ Abrir
508
+ </Button>
509
+ ),
510
+ } as any);
511
+ }
512
+
513
+ return cols;
514
+ }, [displayRows]);
585
515
 
586
516
  if (!rows.length) return null;
587
517
 
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
518
  const rowHeightProps = wrapText
593
519
  ? { getRowHeight: () => "auto" as const }
594
520
  : rowHeight != null
595
521
  ? { rowHeight }
596
522
  : {};
597
523
 
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
524
  const containerHeight = autoHeight
604
- ? undefined // let MUI decide
525
+ ? undefined
605
526
  : (HEIGHT_MAP[displayRows.length] ?? height);
606
527
 
607
- // Whether to hide MUI's built-in pagination footer.
608
- // With autoHeight all rows are visible, so pagination is never needed.
609
528
  const hideFooter =
610
529
  autoHeight || displayRows.length <= Object.keys(HEIGHT_MAP).length;
611
530
 
612
531
  return (
613
532
  <div className="m-2">
614
- {header && <div className="font-bold p-2 ">{header}</div>}
533
+ {header && <div className="font-bold p-2">{header}</div>}
534
+
615
535
  <div className="flex justify-between">
616
536
  <Toolbar
617
537
  exportName={exportName}
@@ -620,6 +540,7 @@ function IHTable({
620
540
  rows={rows}
621
541
  filteredRows={filteredRows}
622
542
  />
543
+
623
544
  {searchable && (
624
545
  <SearchBar value={searchQuery} onChange={setSearchQuery} />
625
546
  )}
@@ -629,10 +550,8 @@ function IHTable({
629
550
  sx={{
630
551
  display: "flex",
631
552
  flexDirection: "column",
632
- // undefined height lets the child DataGrid size itself freely
633
553
  height: containerHeight,
634
554
  width,
635
- zIndex: 999999999,
636
555
  }}
637
556
  >
638
557
  {modal && (
@@ -641,6 +560,10 @@ function IHTable({
641
560
  onClose={handleClose}
642
561
  modal={modal}
643
562
  selectedRow={modalRow}
563
+ onPrev={handlePrevRow}
564
+ onNext={handleNextRow}
565
+ hasPrev={(modalIndex ?? 0) > 0}
566
+ hasNext={(modalIndex ?? -1) < displayRows.length - 1}
644
567
  />
645
568
  )}
646
569
 
@@ -649,8 +572,6 @@ function IHTable({
649
572
  rows={displayRows}
650
573
  columns={columns as any}
651
574
  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
575
  autoHeight={autoHeight}
655
576
  checkboxSelection={Boolean(onSelect)}
656
577
  rowSelectionModel={selectedRows}
@@ -658,37 +579,19 @@ function IHTable({
658
579
  {...rowHeightProps}
659
580
  sx={{
660
581
  fontSize,
582
+
661
583
  "& .MuiDataGrid-cell": {
662
584
  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
585
  },
586
+
672
587
  "& .MuiDataGrid-columnHeader": {
673
588
  fontSize,
674
- ...(wrapText && {
675
- whiteSpace: "normal",
676
- "& .MuiDataGrid-columnHeaderTitle": {
677
- whiteSpace: "normal",
678
- lineHeight: 1.3,
679
- wordBreak: "break-word",
680
- },
681
- }),
682
589
  },
590
+
683
591
  "& .MuiDataGrid-cell--editable": {
684
592
  backgroundColor: "#c6d8f0",
685
593
  fontWeight: 500,
686
594
  },
687
- ...(displayRows.length <= Object.keys(HEIGHT_MAP).length && {
688
- "& .MuiDataGrid-filler": {
689
- display: "none",
690
- },
691
- }),
692
595
  }}
693
596
  editMode="row"
694
597
  processRowUpdate={handleRowUpdate}
@@ -696,16 +599,18 @@ function IHTable({
696
599
  hideFooter={hideFooter}
697
600
  />
698
601
  </Box>
602
+
699
603
  <CustomFooter footer={footer} rows={displayRows} />
700
604
  </div>
701
605
  );
702
606
  }
703
607
 
704
- // ─── Public export ────────────────────────────────────────────────────────────
608
+ // ─── Export ───────────────────────────────────────────────────────────────────
705
609
 
706
610
  export default function Table(props: TableProps) {
707
611
  if (Array.isArray(props.data)) {
708
612
  return <IHTable {...props} />;
709
613
  }
614
+
710
615
  return <KeyValueTable data={props.data} />;
711
616
  }