next-recomponents 2.0.29 → 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,28 +47,10 @@ 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;
53
+ autoHeight?: boolean;
73
54
  [key: string]: any;
74
55
  }
75
56
 
@@ -106,7 +87,7 @@ function EditIcon() {
106
87
  );
107
88
  }
108
89
 
109
- // ─── KeyValueTable (non-array data) ──────────────────────────────────────────
90
+ // ─── KeyValueTable ────────────────────────────────────────────────────────────
110
91
 
111
92
  function KeyValueTable({ data }: { data: Record<string, any> }) {
112
93
  return (
@@ -131,7 +112,7 @@ function KeyValueTable({ data }: { data: Record<string, any> }) {
131
112
  );
132
113
  }
133
114
 
134
- // ─── CustomFooter ─────────────────────────────────────────────────────────────
115
+ // ─── Footer ───────────────────────────────────────────────────────────────────
135
116
 
136
117
  const FOOTER_LABELS: Record<FooterAggregation, string> = {
137
118
  sum: "Suma",
@@ -152,6 +133,7 @@ function computeAggregation(
152
133
  return values.length
153
134
  ? values.reduce((acc, v) => acc + v, 0) / values.length
154
135
  : 0;
136
+
155
137
  return 0;
156
138
  }
157
139
 
@@ -163,21 +145,19 @@ function CustomFooter({
163
145
  footer: FooterType;
164
146
  }) {
165
147
  const entries = Object.entries(footer);
148
+
166
149
  if (!entries.length) return null;
167
150
 
168
151
  return (
169
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">
170
153
  {entries.map(([key, type]) => {
171
154
  const value = computeAggregation(rows, key, type);
172
- const formatted = value.toLocaleString("en-US", {
173
- minimumFractionDigits: value % 1 !== 0 ? 2 : 0,
174
- maximumFractionDigits: value % 1 !== 0 ? 2 : 0,
175
- });
155
+
176
156
  return (
177
157
  <span key={key}>
178
158
  {FOOTER_LABELS[type]} de{" "}
179
159
  <span className="text-gray-900">{key}</span>:{" "}
180
- <span className="text-blue-700">{formatted}</span>
160
+ <span className="text-blue-700">{value.toLocaleString()}</span>
181
161
  </span>
182
162
  );
183
163
  })}
@@ -185,31 +165,66 @@ function CustomFooter({
185
165
  );
186
166
  }
187
167
 
188
- // ─── ModalDialog ──────────────────────────────────────────────────────────────
168
+ // ─── Modal ────────────────────────────────────────────────────────────────────
189
169
 
190
170
  interface ModalDialogProps {
191
171
  open: boolean;
192
172
  onClose: () => void;
193
173
  modal: React.ReactNode;
194
174
  selectedRow: GridValidRowModel | undefined;
175
+ onPrev: () => void;
176
+ onNext: () => void;
177
+ hasPrev: boolean;
178
+ hasNext: boolean;
195
179
  }
196
180
 
197
- 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) {
198
191
  return (
199
192
  <Dialog open={open} maxWidth="xl" fullWidth>
200
- <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
+
201
212
  <button
202
213
  onClick={onClose}
203
- 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"
204
215
  >
205
216
  &times;
206
217
  </button>
207
218
  </div>
219
+
208
220
  <div className="mt-4 m-auto p-5">
209
221
  {selectedRow &&
210
222
  React.cloneElement(
211
223
  modal as React.ReactElement,
212
- { row: selectedRow, hide: onClose } as any,
224
+ {
225
+ row: selectedRow,
226
+ hide: onClose,
227
+ } as any,
213
228
  )}
214
229
  </div>
215
230
  </Dialog>
@@ -218,21 +233,7 @@ function ModalDialog({ open, onClose, modal, selectedRow }: ModalDialogProps) {
218
233
 
219
234
  // ─── Toolbar ──────────────────────────────────────────────────────────────────
220
235
 
221
- interface ToolbarProps {
222
- exportName?: string;
223
- onSave?: (data: GridValidRowModel[]) => void;
224
- onSelect?: (data: GridValidRowModel[]) => void;
225
- rows: GridValidRowModel[];
226
- filteredRows: GridValidRowModel[];
227
- }
228
-
229
- function Toolbar({
230
- exportName,
231
- onSave,
232
- onSelect,
233
- rows,
234
- filteredRows,
235
- }: ToolbarProps) {
236
+ function Toolbar({ exportName, onSave, onSelect, rows, filteredRows }: any) {
236
237
  const excel = useExcel();
237
238
 
238
239
  const stripMeta = (r: GridValidRowModel) => {
@@ -243,7 +244,7 @@ function Toolbar({
243
244
  if (!exportName && !onSave && !onSelect) return null;
244
245
 
245
246
  return (
246
- <div className="flex gap-2 p-2 text-xs">
247
+ <div className="flex gap-2 p-2 text-xs">
247
248
  {exportName && (
248
249
  <Button
249
250
  className="bg-green-800 text-white"
@@ -254,6 +255,7 @@ function Toolbar({
254
255
  Exportar
255
256
  </Button>
256
257
  )}
258
+
257
259
  {onSelect ? (
258
260
  <Button
259
261
  disabled={filteredRows.length === 0}
@@ -271,158 +273,7 @@ function Toolbar({
271
273
  );
272
274
  }
273
275
 
274
- // ─── useColumns ───────────────────────────────────────────────────────────────
275
-
276
- function useColumns(
277
- rows: GridValidRowModel[],
278
- currentCoin: string,
279
- options: {
280
- flex: number;
281
- editableFields?: string[];
282
- buttons?: Record<string, any>;
283
- hideColumns: string[];
284
- modal?: React.ReactNode;
285
- wrapText?: boolean;
286
- handleRowUpdate: (row: GridRowModel) => GridRowModel;
287
- onModalOpen: (row: GridValidRowModel) => void;
288
- },
289
- colSize?: Record<string, number>,
290
- ) {
291
- const {
292
- flex,
293
- editableFields,
294
- buttons,
295
- hideColumns,
296
- modal,
297
- wrapText,
298
- handleRowUpdate,
299
- onModalOpen,
300
- } = options;
301
-
302
- return useMemo(() => {
303
- if (!rows.length) return [];
304
-
305
- const cols = Object.keys(rows[0])
306
- .filter((key) => !key.startsWith("_") && !hideColumns.includes(key))
307
- .map((key) => ({
308
- field: key,
309
- headerName: key,
310
- valueFormatter: (value: any) => {
311
- if (value == null || value === "") return "";
312
- const isDate = /(\d{4}-\d{2}-\d{2})(T[\d:,.+-]*(Z)?)?/;
313
- if (`${value}`.match(isDate)) {
314
- return value
315
- .toString()
316
- .split("T")[0]
317
- .split("-")
318
- .reverse()
319
- .join("/");
320
- }
321
-
322
- const splited = `${value}`.split(".");
323
- const hasDecimals =
324
- splited.length == 2 &&
325
- splited.every((v: any) => `${v}`.match(regularExpresions.number));
326
-
327
- if (hasDecimals) {
328
- return [
329
- currentCoin,
330
- (+`${value}`).toLocaleString("en-US", {
331
- minimumFractionDigits: 2,
332
- maximumFractionDigits: 2,
333
- }),
334
- ].join(" ");
335
- }
336
-
337
- const isNumber = typeof value === "number";
338
-
339
- if (isNumber) {
340
- if (isNaN(value)) return value;
341
- return value;
342
- }
343
-
344
- return value;
345
- },
346
- flex: key == "id" ? false : !colSize?.[key],
347
- width: key == "id" ? 80 : (colSize?.[key] ?? undefined),
348
- editable: editableFields?.includes(key) ?? false,
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
- }),
364
- renderCell: buttons?.[key]
365
- ? (params: any) => {
366
- const children =
367
- buttons?.[key]?.type == "input" ? null : params?.row?.[key];
368
-
369
- return React.cloneElement(buttons[key], {
370
- className: `${params?.className ?? ""} m-auto text-xs`,
371
- children,
372
- row: params?.row,
373
- onClick: async (e: TableButtonProps) => {
374
- e.row = params?.row;
375
- if (buttons[key]?.props?.onClick) {
376
- const newVal = await buttons[key].props.onClick(e);
377
-
378
- if (newVal) handleRowUpdate({ ...e.row, newVal });
379
- }
380
- },
381
- });
382
- }
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,
399
- }));
400
-
401
- if (modal) {
402
- cols.unshift({
403
- field: "Modal",
404
- headerName: "Modal",
405
- editable: false,
406
- type: "string",
407
- width: 10,
408
- renderCell: (params: any) =>
409
- (
410
- <Button
411
- className="text-xs"
412
- onClick={() => onModalOpen(params.row)}
413
- icon={<EditIcon />}
414
- >
415
- {params?.row?.["Modal"]}
416
- </Button>
417
- ) as any,
418
- } as any);
419
- }
420
-
421
- return cols;
422
- }, [rows, wrapText]);
423
- }
424
-
425
- // ─── SearchBar ────────────────────────────────────────────────────────────────
276
+ // ─── Search ───────────────────────────────────────────────────────────────────
426
277
 
427
278
  function SearchBar({
428
279
  value,
@@ -433,50 +284,32 @@ function SearchBar({
433
284
  }) {
434
285
  return (
435
286
  <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
287
  <input
450
288
  type="text"
451
289
  value={value}
452
290
  onChange={(e) => onChange(e.target.value)}
453
291
  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"
292
+ className="w-full max-w-xs text-xs border border-gray-300 rounded px-2 py-1"
455
293
  />
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
294
  </div>
466
295
  );
467
296
  }
468
297
 
469
- /** Devuelve true si la fila contiene TODAS las palabras del query */
470
298
  function rowMatchesSearch(row: GridValidRowModel, query: string): boolean {
471
299
  if (!query.trim()) return true;
300
+
472
301
  const words = query.trim().toLowerCase().split(/\s+/);
302
+
473
303
  const rowText = Object.values(row)
474
304
  .filter((v) => v != null && v !== "")
475
305
  .join(" ")
476
306
  .toLowerCase();
307
+
477
308
  return words.every((word) => rowText.includes(word));
478
309
  }
479
310
 
311
+ // ─── Main Table ───────────────────────────────────────────────────────────────
312
+
480
313
  function IHTable({
481
314
  data,
482
315
  flex = 1,
@@ -500,92 +333,205 @@ function IHTable({
500
333
  wrapText = false,
501
334
  density = "standard",
502
335
  searchable = false,
336
+ autoHeight = false,
503
337
  }: TableProps) {
504
- if (modal && onSelect)
505
- throw new Error("Solo se puede usar modal o onSelect por separado");
506
-
507
338
  const [open, setOpen] = useState(false);
339
+
508
340
  const [rows, setRows] = useState<GridValidRowModel[]>(data);
341
+
342
+ const [modalIndex, setModalIndex] = useState<number | null>(null);
343
+
509
344
  const [selectedRows, setSelectedRows] = useState<any>({
510
345
  type: "include",
511
346
  ids: new Set(),
512
347
  });
513
- const [modalRow, setModalRow] = useState<GridValidRowModel | undefined>();
514
- const [searchQuery, setSearchQuery] = useState("");
515
348
 
516
- // Inject Modal button key if modal is provided
517
- const resolvedButtons = modal ? { ...buttons, Modal: "" } : buttons;
349
+ const [searchQuery, setSearchQuery] = useState("");
518
350
 
519
351
  useEffect(() => {
520
352
  setRows(data);
521
353
  }, [data]);
522
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
+
523
375
  const handleModalOpen = (row: GridValidRowModel) => {
524
- setModalRow(row);
376
+ const index = displayRows.findIndex((r) => r.id === row.id);
377
+
378
+ if (index === -1) return;
379
+
380
+ setModalIndex(index);
525
381
  setOpen(true);
526
382
  };
383
+
527
384
  const handleClose = async () => {
528
- const pass = onCloseModal ? await onCloseModal?.(modalRow) : true;
385
+ const pass = onCloseModal ? await onCloseModal(modalRow) : true;
529
386
 
530
387
  if (!pass) return;
388
+
531
389
  setOpen(false);
532
- 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
+ });
533
405
  };
534
406
 
535
407
  const handleRowUpdate = (newRow: GridRowModel) => {
536
408
  if (!newRow.id) throw new Error("Fila sin id");
537
- const updated = { ...newRow, _edited: true } as any;
409
+
410
+ const updated: any = { ...newRow, _edited: true };
411
+
538
412
  setRows((prev) =>
539
413
  prev.map((row) => (row.id === updated.id ? updated : row)),
540
414
  );
415
+
541
416
  return updated;
542
417
  };
543
418
 
544
- const filteredRows = useMemo(() => {
545
- if (selectedRows?.type === "exclude") {
546
- return rows.filter((r) => !Array.from(selectedRows.ids).includes(r.id));
547
- }
548
- if (selectedRows?.type === "include") {
549
- return rows.filter((r) => Array.from(selectedRows.ids).includes(r.id));
550
- }
551
- return [];
552
- }, [selectedRows, rows]);
419
+ const resolvedButtons = modal ? { ...buttons, Modal: "" } : buttons;
553
420
 
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
- );
421
+ const columns = useMemo(() => {
422
+ if (!displayRows.length) return [];
560
423
 
561
- const columns = useColumns(
562
- displayRows,
563
- currentCoin,
564
- {
565
- flex,
566
- editableFields,
567
- buttons: resolvedButtons,
568
- hideColumns,
569
- modal,
570
- wrapText,
571
- handleRowUpdate,
572
- onModalOpen: handleModalOpen,
573
- },
574
- colSize,
575
- );
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]);
576
515
 
577
516
  if (!rows.length) return null;
578
517
 
579
- // When wrapText is active we must use getRowHeight; rowHeight prop is ignored.
580
518
  const rowHeightProps = wrapText
581
519
  ? { getRowHeight: () => "auto" as const }
582
520
  : rowHeight != null
583
521
  ? { rowHeight }
584
522
  : {};
585
523
 
524
+ const containerHeight = autoHeight
525
+ ? undefined
526
+ : (HEIGHT_MAP[displayRows.length] ?? height);
527
+
528
+ const hideFooter =
529
+ autoHeight || displayRows.length <= Object.keys(HEIGHT_MAP).length;
530
+
586
531
  return (
587
532
  <div className="m-2">
588
- {header && <div className="font-bold p-2 ">{header}</div>}
533
+ {header && <div className="font-bold p-2">{header}</div>}
534
+
589
535
  <div className="flex justify-between">
590
536
  <Toolbar
591
537
  exportName={exportName}
@@ -594,6 +540,7 @@ function IHTable({
594
540
  rows={rows}
595
541
  filteredRows={filteredRows}
596
542
  />
543
+
597
544
  {searchable && (
598
545
  <SearchBar value={searchQuery} onChange={setSearchQuery} />
599
546
  )}
@@ -603,9 +550,8 @@ function IHTable({
603
550
  sx={{
604
551
  display: "flex",
605
552
  flexDirection: "column",
606
- height: HEIGHT_MAP[displayRows.length] ?? height,
553
+ height: containerHeight,
607
554
  width,
608
- zIndex: 999999999,
609
555
  }}
610
556
  >
611
557
  {modal && (
@@ -614,6 +560,10 @@ function IHTable({
614
560
  onClose={handleClose}
615
561
  modal={modal}
616
562
  selectedRow={modalRow}
563
+ onPrev={handlePrevRow}
564
+ onNext={handleNextRow}
565
+ hasPrev={(modalIndex ?? 0) > 0}
566
+ hasNext={(modalIndex ?? -1) < displayRows.length - 1}
617
567
  />
618
568
  )}
619
569
 
@@ -622,60 +572,45 @@ function IHTable({
622
572
  rows={displayRows}
623
573
  columns={columns as any}
624
574
  density={density}
575
+ autoHeight={autoHeight}
625
576
  checkboxSelection={Boolean(onSelect)}
626
577
  rowSelectionModel={selectedRows}
627
578
  onRowSelectionModelChange={!modal ? setSelectedRows : undefined}
628
579
  {...rowHeightProps}
629
580
  sx={{
630
581
  fontSize,
582
+
631
583
  "& .MuiDataGrid-cell": {
632
584
  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
- }),
641
585
  },
586
+
642
587
  "& .MuiDataGrid-columnHeader": {
643
588
  fontSize,
644
- ...(wrapText && {
645
- whiteSpace: "normal",
646
- "& .MuiDataGrid-columnHeaderTitle": {
647
- whiteSpace: "normal",
648
- lineHeight: 1.3,
649
- wordBreak: "break-word",
650
- },
651
- }),
652
589
  },
590
+
653
591
  "& .MuiDataGrid-cell--editable": {
654
592
  backgroundColor: "#c6d8f0",
655
593
  fontWeight: 500,
656
594
  },
657
- ...(displayRows.length <= Object.keys(HEIGHT_MAP).length && {
658
- "& .MuiDataGrid-filler": {
659
- display: "none",
660
- },
661
- }),
662
595
  }}
663
596
  editMode="row"
664
597
  processRowUpdate={handleRowUpdate}
665
598
  pageSizeOptions={[5, 10]}
666
- hideFooter={displayRows.length <= Object.keys(HEIGHT_MAP).length}
599
+ hideFooter={hideFooter}
667
600
  />
668
601
  </Box>
602
+
669
603
  <CustomFooter footer={footer} rows={displayRows} />
670
604
  </div>
671
605
  );
672
606
  }
673
607
 
674
- // ─── Public export ────────────────────────────────────────────────────────────
608
+ // ─── Export ───────────────────────────────────────────────────────────────────
675
609
 
676
610
  export default function Table(props: TableProps) {
677
611
  if (Array.isArray(props.data)) {
678
612
  return <IHTable {...props} />;
679
613
  }
614
+
680
615
  return <KeyValueTable data={props.data} />;
681
616
  }