@alaarab/ogrid-react-material 2.0.11 → 2.0.13

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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <strong>OGrid for Material UI</strong> — The lightweight React data grid with enterprise features and zero enterprise cost.
2
+ <strong>OGrid for React Material</strong> — The lightweight React data grid with enterprise features and zero enterprise cost.
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -39,7 +39,7 @@ export const ColumnChooser = (props) => {
39
39
  py: 1,
40
40
  borderBottom: 1,
41
41
  borderColor: 'divider',
42
- bgcolor: 'grey.50',
42
+ bgcolor: 'action.hover',
43
43
  }, children: _jsxs(Typography, { variant: "subtitle2", fontWeight: 600, children: ["Select Columns (", visibleCount, " of ", totalCount, ")"] }) }), _jsx(Box, { sx: { maxHeight: 320, overflowY: 'auto', py: 0.5 }, children: columns.map((column) => (_jsx(Box, { sx: { px: 1.5, minHeight: 32, display: 'flex', alignItems: 'center' }, children: _jsx(FormControlLabel, { control: _jsx(Checkbox, { size: "small", checked: visibleColumns.has(column.columnId), onChange: handleCheckboxChange(column.columnId) }), label: _jsx(Typography, { variant: "body2", children: column.name }), sx: { m: 0 } }) }, column.columnId))) }), _jsxs(Box, { sx: {
44
44
  display: 'flex',
45
45
  justifyContent: 'flex-end',
@@ -48,6 +48,6 @@ export const ColumnChooser = (props) => {
48
48
  py: 1,
49
49
  borderTop: 1,
50
50
  borderColor: 'divider',
51
- bgcolor: 'grey.50',
51
+ bgcolor: 'action.hover',
52
52
  }, children: [_jsx(Button, { size: "small", onClick: handleClearAll, sx: { textTransform: 'none' }, children: "Clear All" }), _jsx(Button, { size: "small", variant: "contained", onClick: handleSelectAll, sx: { textTransform: 'none' }, children: "Select All" })] })] })] }));
53
53
  };
@@ -2,46 +2,20 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
3
  import { Popover, Tooltip, IconButton, Box, Typography } from '@mui/material';
4
4
  import { ArrowUpward as ArrowUpwardIcon, ArrowDownward as ArrowDownwardIcon, SwapVert as SwapVertIcon, FilterList as FilterListIcon, } from '@mui/icons-material';
5
- import { useColumnHeaderFilterState } from '@alaarab/ogrid-react';
5
+ import { useColumnHeaderFilterState, getColumnHeaderFilterStateParams, renderFilterContent, } from '@alaarab/ogrid-react';
6
6
  import { TextFilterPopover } from './TextFilterPopover';
7
7
  import { MultiSelectFilterPopover } from './MultiSelectFilterPopover';
8
8
  import { PeopleFilterPopover } from './PeopleFilterPopover';
9
+ const materialRenderers = {
10
+ renderMultiSelect: (p) => (_jsx(MultiSelectFilterPopover, { searchText: p.searchText, onSearchChange: p.onSearchChange, options: p.options, filteredOptions: p.filteredOptions, selected: p.selected, onOptionToggle: p.onOptionToggle, onSelectAll: p.onSelectAll, onClearSelection: p.onClearSelection, onApply: p.onApply, isLoading: p.isLoading })),
11
+ renderText: (p) => (_jsx(TextFilterPopover, { value: p.value, onValueChange: p.onValueChange, onApply: p.onApply, onClear: p.onClear })),
12
+ renderPeople: (p) => (_jsx(PeopleFilterPopover, { selectedUser: p.selectedUser, searchText: p.searchText, onSearchChange: p.onSearchChange, suggestions: p.suggestions, isLoading: p.isLoading, onUserSelect: p.onUserSelect, onClearUser: p.onClearUser, inputRef: p.inputRef })),
13
+ renderDate: (p) => (_jsxs(Box, { sx: { p: 1.5, display: 'flex', flexDirection: 'column', gap: 1 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "caption", sx: { minWidth: 36 }, children: "From:" }), _jsx("input", { type: "date", value: p.tempDateFrom, onChange: (e) => p.setTempDateFrom(e.target.value), style: { flex: 1, padding: '4px 6px' } })] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "caption", sx: { minWidth: 36 }, children: "To:" }), _jsx("input", { type: "date", value: p.tempDateTo, onChange: (e) => p.setTempDateTo(e.target.value), style: { flex: 1, padding: '4px 6px' } })] }), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 0.5 }, children: [_jsx("button", { onClick: p.onClear, disabled: !p.tempDateFrom && !p.tempDateTo, style: { padding: '4px 12px', cursor: 'pointer' }, children: "Clear" }), _jsx("button", { onClick: p.onApply, style: { padding: '4px 12px', cursor: 'pointer' }, children: "Apply" })] })] })),
14
+ };
9
15
  export const ColumnHeaderFilter = React.memo((props) => {
10
- const { columnName, filterType, isSorted = false, isSortedDescending = false, onSort, selectedValues, onFilterChange, options = [], isLoadingOptions = false, textValue = '', onTextChange, selectedUser, onUserChange, peopleSearch, dateValue, onDateChange, } = props;
11
- const state = useColumnHeaderFilterState({
12
- filterType,
13
- isSorted,
14
- isSortedDescending,
15
- onSort,
16
- selectedValues,
17
- onFilterChange,
18
- options,
19
- isLoadingOptions,
20
- textValue,
21
- onTextChange,
22
- selectedUser,
23
- onUserChange,
24
- peopleSearch,
25
- dateValue,
26
- onDateChange,
27
- });
28
- const { headerRef, peopleInputRef, isFilterOpen, setFilterOpen, tempSelected, tempTextValue, setTempTextValue, searchText, setSearchText, filteredOptions, peopleSuggestions, isPeopleLoading, peopleSearchText, setPeopleSearchText, hasActiveFilter, popoverPosition, handlers, } = state;
29
- const safeOptions = options ?? [];
30
- const renderPopoverContent = () => {
31
- if (filterType === 'multiSelect') {
32
- return (_jsx(MultiSelectFilterPopover, { searchText: searchText, onSearchChange: setSearchText, options: safeOptions, filteredOptions: filteredOptions, selected: tempSelected, onOptionToggle: handlers.handleCheckboxChange, onSelectAll: handlers.handleSelectAll, onClearSelection: handlers.handleClearSelection, onApply: handlers.handleApplyMultiSelect, isLoading: isLoadingOptions }));
33
- }
34
- if (filterType === 'text') {
35
- return (_jsx(TextFilterPopover, { value: tempTextValue, onValueChange: setTempTextValue, onApply: handlers.handleTextApply, onClear: handlers.handleTextClear }));
36
- }
37
- if (filterType === 'people') {
38
- return (_jsx(PeopleFilterPopover, { selectedUser: selectedUser, searchText: peopleSearchText, onSearchChange: setPeopleSearchText, suggestions: peopleSuggestions, isLoading: isPeopleLoading, onUserSelect: handlers.handleUserSelect, onClearUser: handlers.handleClearUser, inputRef: peopleInputRef }));
39
- }
40
- if (filterType === 'date') {
41
- return (_jsxs(Box, { sx: { p: 1.5, display: 'flex', flexDirection: 'column', gap: 1 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "caption", sx: { minWidth: 36 }, children: "From:" }), _jsx("input", { type: "date", value: state.tempDateFrom, onChange: (e) => state.setTempDateFrom(e.target.value), style: { flex: 1, padding: '4px 6px' } })] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "caption", sx: { minWidth: 36 }, children: "To:" }), _jsx("input", { type: "date", value: state.tempDateTo, onChange: (e) => state.setTempDateTo(e.target.value), style: { flex: 1, padding: '4px 6px' } })] }), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 0.5 }, children: [_jsx("button", { onClick: handlers.handleDateClear, disabled: !state.tempDateFrom && !state.tempDateTo, style: { padding: '4px 12px', cursor: 'pointer' }, children: "Clear" }), _jsx("button", { onClick: handlers.handleDateApply, style: { padding: '4px 12px', cursor: 'pointer' }, children: "Apply" })] })] }));
42
- }
43
- return null;
44
- };
16
+ const { columnName, filterType, isSorted = false, isSortedDescending = false, onSort, options = [], isLoadingOptions = false, selectedUser, } = props;
17
+ const state = useColumnHeaderFilterState(getColumnHeaderFilterStateParams(props));
18
+ const { headerRef, isFilterOpen, setFilterOpen, hasActiveFilter, popoverPosition, handlers, } = state;
45
19
  return (_jsxs(Box, { ref: headerRef, sx: { display: 'flex', alignItems: 'center', width: '100%', minWidth: 0 }, children: [_jsx(Box, { sx: { flex: 1, minWidth: 0, overflow: 'hidden' }, children: _jsx(Tooltip, { title: columnName, arrow: true, children: _jsx(Typography, { variant: "body2", fontWeight: 600, noWrap: true, "data-header-label": true, sx: { lineHeight: 1.4 }, children: columnName }) }) }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', ml: 0.5, flexShrink: 0 }, children: [onSort && (_jsx(IconButton, { size: "small", onClick: handlers.handleSortClick, "aria-label": `Sort by ${columnName}`, title: isSorted ? (isSortedDescending ? 'Sorted descending' : 'Sorted ascending') : 'Sort', color: isSorted ? 'primary' : 'default', sx: { p: 0.25 }, children: isSorted ? (isSortedDescending ? (_jsx(ArrowDownwardIcon, { sx: { fontSize: 16 } })) : (_jsx(ArrowUpwardIcon, { sx: { fontSize: 16 } }))) : (_jsx(SwapVertIcon, { sx: { fontSize: 16 } })) })), filterType !== 'none' && (_jsxs(IconButton, { size: "small", onClick: handlers.handleFilterIconClick, "aria-label": `Filter ${columnName}`, title: `Filter ${columnName}`, color: hasActiveFilter || isFilterOpen ? 'primary' : 'default', sx: { p: 0.25, position: 'relative' }, children: [_jsx(FilterListIcon, { sx: { fontSize: 16 } }), hasActiveFilter && (_jsx(Box, { sx: {
46
20
  position: 'absolute',
47
21
  top: 2,
@@ -55,6 +29,6 @@ export const ColumnHeaderFilter = React.memo((props) => {
55
29
  sx: { mt: 0.5, overflow: 'visible' },
56
30
  onClick: (e) => e.stopPropagation(),
57
31
  },
58
- }, children: [_jsx(Box, { sx: { borderBottom: 1, borderColor: 'divider', px: 1.5, py: 1 }, children: _jsxs(Typography, { variant: "subtitle2", children: ["Filter: ", columnName] }) }), renderPopoverContent()] })] }));
32
+ }, children: [_jsx(Box, { sx: { borderBottom: 1, borderColor: 'divider', px: 1.5, py: 1 }, children: _jsxs(Typography, { variant: "subtitle2", children: ["Filter: ", columnName] }) }), renderFilterContent(filterType, state, options ?? [], isLoadingOptions, selectedUser, materialRenderers)] })] }));
59
33
  });
60
34
  ColumnHeaderFilter.displayName = 'ColumnHeaderFilter';
@@ -1,14 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
- import { useCallback, useRef, useMemo } from 'react';
3
+ import { useCallback, useMemo } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
- import { Box, CircularProgress, Typography, Button, Popover, Checkbox, Table, TableHead, TableBody, TableRow, TableCell, TableContainer, } from '@mui/material';
5
+ import { Box, Popover, Checkbox, Table, TableHead, TableBody, TableRow, TableCell, TableContainer, } from '@mui/material';
6
6
  import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
7
7
  import { ColumnHeaderMenu } from '../ColumnHeaderMenu';
8
8
  import { InlineCellEditor } from './InlineCellEditor';
9
9
  import { StatusBar } from './StatusBar';
10
10
  import { GridContextMenu } from './GridContextMenu';
11
- import { useDataGridState, useColumnResize, useColumnReorder, useVirtualScroll, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, MarchingAntsOverlay, buildHeaderRows, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-react';
11
+ import { EmptyState } from './EmptyState';
12
+ import { LoadingOverlay } from './LoadingOverlay';
13
+ import { DropIndicator } from './DropIndicator';
14
+ import { useDataGridTableOrchestration, getHeaderFilterConfig, getCellRenderDescriptor, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, PREVENT_DEFAULT, NOOP, STOP_PROPAGATION, } from '@alaarab/ogrid-react';
12
15
  // ── Module-scope stable styles (avoid per-render Emotion resolutions) ──
13
16
  const gridRootSx = { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' };
14
17
  // Row
@@ -48,8 +51,9 @@ const CELL_CONTENT_EDITABLE_SX = { ...CELL_CONTENT_BASE_SX, cursor: 'cell' };
48
51
  const CELL_CONTENT_NUMERIC_EDITABLE_SX = { ...CELL_CONTENT_NUMERIC_SX, cursor: 'cell' };
49
52
  const CELL_CONTENT_BOOLEAN_EDITABLE_SX = { ...CELL_CONTENT_BOOLEAN_SX, cursor: 'cell' };
50
53
  // Cell overlay states (only applied to the few active/selected cells)
51
- const CELL_ACTIVE_SX = { outline: '2px solid var(--ogrid-selection, #217346)', outlineOffset: '-1px', zIndex: 2, position: 'relative', overflow: 'visible' };
52
- const CELL_IN_RANGE_SX = { bgcolor: 'var(--ogrid-bg-range, rgba(33, 115, 70, 0.12))' };
54
+ // Active cell: theme-aware bg so dark mode doesn't show white (MUI action.hover adapts to theme)
55
+ const CELL_ACTIVE_SX = { outline: '2px solid var(--ogrid-selection, #217346)', outlineOffset: '-1px', zIndex: 2, position: 'relative', overflow: 'visible', bgcolor: 'action.hover', '&:focus-visible': { outline: '2px solid var(--ogrid-selection, #217346)', outlineOffset: '-1px' } };
56
+ const CELL_IN_RANGE_SX = { bgcolor: 'var(--ogrid-bg-range, rgba(33, 115, 70, 0.12))', '&:focus-visible': { outline: 'none' } };
53
57
  const CELL_CUT_RANGE_SX = { bgcolor: 'action.hover', opacity: 0.7 };
54
58
  // Pre-computed overlay variant arrays (avoid per-cell array allocation + filter)
55
59
  // Key: `${base}_${overlay}` where overlay is 'active' | 'range' | 'cut'
@@ -105,18 +109,46 @@ const FILL_HANDLE_SX = {
105
109
  };
106
110
  // Cell <td> positioning variants
107
111
  const CELL_TD_BASE_SX = { position: 'relative', p: 0, height: '1px' };
108
- const CELL_TD_PINNED_LEFT_SX = { ...CELL_TD_BASE_SX, position: 'sticky', left: 0, zIndex: 6, bgcolor: 'background.paper', willChange: 'transform', borderLeft: '2px solid', borderLeftColor: 'primary.main' };
109
- const CELL_TD_PINNED_RIGHT_SX = { ...CELL_TD_BASE_SX, position: 'sticky', right: 0, zIndex: 6, bgcolor: 'background.paper', willChange: 'transform', borderRight: '2px solid', borderRightColor: 'primary.main' };
112
+ const CELL_TD_PINNED_LEFT_SX = {
113
+ ...CELL_TD_BASE_SX, position: 'sticky', left: 0, zIndex: 6,
114
+ bgcolor: 'background.paper', willChange: 'transform',
115
+ '&::after': {
116
+ content: '""', position: 'absolute', top: '-1px', right: '-4px', bottom: '-1px',
117
+ width: '4px', background: 'linear-gradient(to right, rgba(0,0,0,0.12), transparent)', pointerEvents: 'none',
118
+ },
119
+ };
120
+ const CELL_TD_PINNED_RIGHT_SX = {
121
+ ...CELL_TD_BASE_SX, position: 'sticky', right: 0, zIndex: 6,
122
+ bgcolor: 'background.paper', willChange: 'transform',
123
+ '&::before': {
124
+ content: '""', position: 'absolute', top: '-1px', left: '-4px', bottom: '-1px',
125
+ width: '4px', background: 'linear-gradient(to left, rgba(0,0,0,0.12), transparent)', pointerEvents: 'none',
126
+ },
127
+ };
110
128
  // Header cell positioning variants
111
129
  const HEADER_BASE_SX = {
112
130
  fontWeight: 600,
113
- position: 'sticky', /* Changed from relative - enables vertical sticky for all headers */
131
+ position: 'sticky', /* Enables vertical sticky for all headers */
114
132
  top: 0, /* Sticky vertically */
115
133
  zIndex: 8, /* Stack above body cells */
116
134
  bgcolor: 'action.hover' /* Required for sticky overlap */
117
135
  };
118
- const HEADER_PINNED_LEFT_SX = { ...HEADER_BASE_SX, position: 'sticky', left: 0, top: 0, zIndex: 10 /* Increased from 9 to stack above base header cells (z-index: 8) */, bgcolor: 'action.hover', willChange: 'transform', borderLeft: '2px solid', borderLeftColor: 'primary.main' };
119
- const HEADER_PINNED_RIGHT_SX = { ...HEADER_BASE_SX, position: 'sticky', right: 0, top: 0, zIndex: 10 /* Increased from 9 to stack above base header cells (z-index: 8) */, bgcolor: 'action.hover', willChange: 'transform', borderRight: '2px solid', borderRightColor: 'primary.main' };
136
+ const HEADER_PINNED_LEFT_SX = {
137
+ ...HEADER_BASE_SX, position: 'sticky', left: 0, top: 0,
138
+ zIndex: 10, bgcolor: 'action.hover', willChange: 'transform',
139
+ '&::after': {
140
+ content: '""', position: 'absolute', top: '-1px', right: '-4px', bottom: '-1px',
141
+ width: '4px', background: 'linear-gradient(to right, rgba(0,0,0,0.12), transparent)', pointerEvents: 'none',
142
+ },
143
+ };
144
+ const HEADER_PINNED_RIGHT_SX = {
145
+ ...HEADER_BASE_SX, position: 'sticky', right: 0, top: 0,
146
+ zIndex: 10, bgcolor: 'action.hover', willChange: 'transform',
147
+ '&::before': {
148
+ content: '""', position: 'absolute', top: '-1px', left: '-4px', bottom: '-1px',
149
+ width: '4px', background: 'linear-gradient(to left, rgba(0,0,0,0.12), transparent)', pointerEvents: 'none',
150
+ },
151
+ };
120
152
  // Resize handle
121
153
  const RESIZE_HANDLE_SX = {
122
154
  position: 'absolute', top: 0, right: '-3px', bottom: 0, width: '8px',
@@ -133,22 +165,9 @@ const WRAPPER_SCROLL_SX = { display: 'flex', flexDirection: 'column', minHeight:
133
165
  // Table wrapper
134
166
  const TABLE_WRAPPER_SX = { position: 'relative', opacity: 1 };
135
167
  const TABLE_WRAPPER_LOADING_SX = { position: 'relative', opacity: 0.6 };
136
- // Empty state
137
- const EMPTY_STATE_SX = { py: 4, px: 2, textAlign: 'center', borderTop: 1, borderColor: 'divider', bgcolor: 'action.hover' };
138
- // Loading overlay
139
- const LOADING_OVERLAY_SX = {
140
- position: 'absolute', inset: 0, zIndex: 2,
141
- display: 'flex', alignItems: 'center', justifyContent: 'center',
142
- background: 'var(--ogrid-loading-bg, rgba(255,255,255,0.7))',
143
- };
144
- const LOADING_INNER_SX = {
145
- display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1,
146
- p: 2, bgcolor: 'background.paper', border: 1, borderColor: 'divider', borderRadius: 1,
147
- };
148
- // Module-scope event handlers
149
- const STOP_PROPAGATION = (e) => e.stopPropagation();
150
- const PREVENT_DEFAULT = (e) => { e.preventDefault(); };
151
- const NOOP = () => { };
168
+ // TableBody — remove bottom border from last row so DataGridTable has no outer border
169
+ // (the OGridLayout container provides the border/radius)
170
+ const TABLE_BODY_SX = { '& tr:last-child td': { borderBottom: 'none' } };
152
171
  function GridRowInner(props) {
153
172
  const { item, rowIndex, rowId, isSelected, columnLayouts, renderCellContent, handleSingleRowClick, handleRowCheckboxChange, lastMouseShiftRef, hasCheckboxCol, hasRowNumbersCol, rowNumberOffset, } = props;
154
173
  return (_jsxs(TableRow, { selected: isSelected, "data-row-id": rowId, onClick: handleSingleRowClick, sx: ROW_HOVER_SX, children: [hasCheckboxCol && (_jsx(TableCell, { padding: "checkbox", sx: CHECKBOX_CELL_SX, children: _jsx(Box, { "data-row-index": rowIndex, "data-col-index": 0, onClick: STOP_PROPAGATION, sx: CHECKBOX_WRAPPER_SX, children: _jsx(Checkbox, { checked: isSelected, onChange: (_, checked) => handleRowCheckboxChange(rowId, checked, rowIndex, lastMouseShiftRef.current), size: "small", "aria-label": `Select row ${rowIndex + 1}` }) }) })), hasRowNumbersCol && (_jsx(TableCell, { sx: {
@@ -163,87 +182,37 @@ function GridRowInner(props) {
163
182
  position: 'sticky',
164
183
  left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
165
184
  zIndex: 3,
166
- }, children: rowNumberOffset + rowIndex + 1 })), columnLayouts.map((cl, colIdx) => (_jsx(TableCell, { sx: [cl.tdSx, { minWidth: cl.minWidth, width: cl.width, maxWidth: cl.maxWidth }], children: renderCellContent(item, cl.col, rowIndex, colIdx) }, cl.col.columnId)))] }));
185
+ }, children: rowNumberOffset + rowIndex + 1 })), columnLayouts.map((cl, colIdx) => (_jsx(TableCell, { "data-column-id": cl.col.columnId, sx: [cl.tdSx, { minWidth: cl.minWidth, width: cl.width, maxWidth: cl.maxWidth }], children: renderCellContent(item, cl.col, rowIndex, colIdx) }, cl.col.columnId)))] }));
167
186
  }
168
187
  const GridRow = React.memo(GridRowInner, areGridRowPropsEqual);
169
188
  function DataGridTableInner(props) {
170
- const wrapperRef = useRef(null);
171
- const tableContainerRef = useRef(null);
172
- const state = useDataGridState({ props, wrapperRef });
173
- const lastMouseShiftRef = useRef(false);
174
- const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels, pinning } = state;
175
- const { visibleCols, hasCheckboxCol, hasRowNumbersCol, colOffset, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides } = layout;
176
- const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
177
- const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
178
- const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging } = interaction;
179
- const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
180
- const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
181
- const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
182
- const { items, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, columnOrder, onColumnOrderChange, columnReorder, virtualScroll, density = 'normal', pinnedColumns, currentPage = 1, pageSize: propPageSize = 25, } = props;
183
- // Calculate row number offset for pagination
184
- const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * propPageSize : 0;
185
- const fitToContent = layoutMode === 'content';
186
- const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
189
+ const o = useDataGridTableOrchestration({ props });
190
+ const { wrapperRef, tableContainerRef, lastMouseShiftRef, interaction, pinning, handleResizeStart, getColumnWidth, isReorderDragging, dropIndicatorX, handleHeaderMouseDown, virtualScrollEnabled, visibleRange, items, getRowId, emptyState, freezeRows, freezeCols, suppressHorizontalScroll, isLoading, loadingMessage, ariaLabel, ariaLabelledBy, columnReorder, density, rowNumberOffset, headerRows, allowOverflowX, fitToContent, editCallbacks, interactionHandlers, cellDescriptorInputRef, pendingEditorValueRef, popoverAnchorElRef, handleSingleRowClick, handlePasteVoid, visibleCols, hasCheckboxCol, hasRowNumbersCol, colOffset, minTableWidth, columnSizingOverrides, selectedRowIds, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected, editingCell, setPopoverAnchorEl, cancelPopoverEdit, setActiveCell, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging, menuPosition, closeContextMenu, headerFilterInput, statusBarConfig, showEmptyInGrid, onCellError, } = o;
187
191
  // Density-aware cell padding
188
192
  const densityPadding = useMemo(() => getDensityPadding(density), [density]);
189
193
  const _cellSx = useMemo(() => ({ ...CELL_CONTENT_BASE_SX, ...densityPadding }), [densityPadding]);
190
194
  const headerCellSx = useMemo(() => ({ px: densityPadding.px, py: densityPadding.py }), [densityPadding]);
191
- // Memoize header rows (recursive tree traversal)
192
- const headerRows = useMemo(() => buildHeaderRows(props.columns, props.visibleColumns), [props.columns, props.visibleColumns]);
193
- const { handleResizeStart, getColumnWidth } = useColumnResize({
194
- columnSizingOverrides,
195
- setColumnSizingOverrides,
196
- });
197
- const { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown } = useColumnReorder({
198
- columns: visibleCols,
199
- columnOrder,
200
- onColumnOrderChange,
201
- enabled: columnReorder === true,
202
- pinnedColumns,
203
- wrapperRef,
204
- });
205
- const virtualScrollEnabled = virtualScroll?.enabled === true;
206
- const virtualRowHeight = virtualScroll?.rowHeight ?? 36;
207
- const { visibleRange } = useVirtualScroll({
208
- totalRows: items.length,
209
- rowHeight: virtualRowHeight,
210
- enabled: virtualScrollEnabled,
211
- overscan: virtualScroll?.overscan,
212
- containerRef: wrapperRef,
213
- });
214
195
  // Pre-compute per-column layout (tdSx, widths) so GridRow doesn't recalculate per-cell
215
196
  const columnLayouts = useMemo(() => visibleCols.map((col, colIdx) => {
216
197
  const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
217
- const isPinnedLeft = col.pinned === 'left';
218
- const isPinnedRight = col.pinned === 'right';
198
+ const isPinnedLeft = pinning.pinnedColumns[col.columnId] === 'left';
199
+ const isPinnedRight = pinning.pinnedColumns[col.columnId] === 'right';
219
200
  const columnWidth = getColumnWidth(col);
220
- const tdSx = isPinnedLeft || (isFreezeCol && colIdx === 0) ? CELL_TD_PINNED_LEFT_SX : isPinnedRight ? CELL_TD_PINNED_RIGHT_SX : CELL_TD_BASE_SX;
221
- return { col, tdSx, minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH, width: columnWidth, maxWidth: columnWidth };
222
- }), [visibleCols, freezeCols, getColumnWidth]);
223
- const editCallbacks = useMemo(() => ({ commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit }), [commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit]);
224
- const interactionHandlers = useMemo(() => ({ handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu }), [handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu]);
225
- // Refs for volatile state — lets renderCellContent be stable (same function ref across
226
- // selection changes) so that GridRow's React.memo comparator can skip unaffected rows.
227
- const cellDescriptorInputRef = useLatestRef(cellDescriptorInput);
228
- const pendingEditorValueRef = useLatestRef(pendingEditorValue);
229
- const popoverAnchorElRef = useLatestRef(popoverAnchorEl);
230
- // Stable row-click handler
231
- const selectedRowIdsRef = useLatestRef(selectedRowIds);
232
- const handleSingleRowClick = useCallback((e) => {
233
- if (rowSelection !== 'single')
234
- return;
235
- const rowId = e.currentTarget.dataset.rowId;
236
- if (!rowId)
237
- return;
238
- const ids = selectedRowIdsRef.current;
239
- updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
240
- // eslint-disable-next-line react-hooks/exhaustive-deps -- selectedRowIdsRef is a stable ref
241
- }, [rowSelection, updateSelection]);
201
+ const baseTdSx = isPinnedLeft || (isFreezeCol && colIdx === 0) ? CELL_TD_PINNED_LEFT_SX : isPinnedRight ? CELL_TD_PINNED_RIGHT_SX : CELL_TD_BASE_SX;
202
+ // Override sticky offset for pinned columns (supports multiple pinned columns)
203
+ const tdSx = isPinnedLeft && pinning.leftOffsets[col.columnId] != null
204
+ ? { ...baseTdSx, left: pinning.leftOffsets[col.columnId] }
205
+ : isPinnedRight && pinning.rightOffsets[col.columnId] != null
206
+ ? { ...baseTdSx, right: pinning.rightOffsets[col.columnId] }
207
+ : baseTdSx;
208
+ const hasResizeOverride = !!columnSizingOverrides[col.columnId];
209
+ return { col, tdSx, minWidth: hasResizeOverride ? columnWidth : (col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH), width: columnWidth, maxWidth: columnWidth };
210
+ }), [visibleCols, freezeCols, getColumnWidth, columnSizingOverrides, pinning.pinnedColumns, pinning.leftOffsets, pinning.rightOffsets]);
242
211
  // Wrapper sx (depends on dynamic values — memoize to avoid recreation)
243
212
  const wrapperSx = useMemo(() => ({
244
213
  position: 'relative',
245
214
  flex: 1,
246
- minHeight: 0,
215
+ minHeight: isLoading && items.length === 0 ? 200 : 0,
247
216
  width: fitToContent ? 'fit-content' : '100%',
248
217
  maxWidth: '100%',
249
218
  overflowX: suppressHorizontalScroll ? 'hidden' : allowOverflowX ? 'auto' : 'hidden',
@@ -252,7 +221,7 @@ function DataGridTableInner(props) {
252
221
  willChange: 'scroll-position',
253
222
  '& [data-drag-range]': { bgcolor: 'rgba(33, 115, 70, 0.12) !important' },
254
223
  '& [data-drag-anchor]': { bgcolor: 'background.paper !important' },
255
- }), [fitToContent, suppressHorizontalScroll, allowOverflowX]);
224
+ }), [fitToContent, suppressHorizontalScroll, allowOverflowX, isLoading, items.length]);
256
225
  const renderCellContent = useCallback((item, col, rowIndex, colIdx) => {
257
226
  const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInputRef.current);
258
227
  const rowId = getRowId(item);
@@ -281,7 +250,7 @@ function DataGridTableInner(props) {
281
250
  },
282
251
  // eslint-disable-next-line react-hooks/exhaustive-deps -- *Ref vars are stable refs from useLatestRef
283
252
  [editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit, getRowId, onCellError]);
284
- return (_jsxs(Box, { sx: gridRootSx, children: [_jsxs(Box, { ref: wrapperRef, tabIndex: 0, role: "region", "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'), "aria-labelledby": ariaLabelledBy, onMouseDown: (e) => { lastMouseShiftRef.current = e.shiftKey; }, onKeyDown: handleGridKeyDown, onContextMenu: PREVENT_DEFAULT, "data-overflow-x": allowOverflowX ? 'true' : 'false', "data-density": density, sx: wrapperSx, children: [_jsx(Box, { sx: WRAPPER_SCROLL_SX, children: _jsx(TableContainer, { sx: { minWidth: allowOverflowX ? minTableWidth : undefined }, children: _jsxs(Box, { ref: tableContainerRef, sx: isLoading && items.length > 0 ? TABLE_WRAPPER_LOADING_SX : TABLE_WRAPPER_SX, children: [_jsxs(Table, { size: "small", sx: { overflow: 'hidden', minWidth: minTableWidth }, "data-freeze-rows": freezeRows != null && freezeRows >= 1 ? freezeRows : undefined, "data-freeze-cols": freezeCols != null && freezeCols >= 1 ? freezeCols : undefined, children: [_jsx(TableHead, { sx: STICKY_HEADER_SX, children: headerRows.map((row, rowIdx) => (_jsxs(TableRow, { sx: HEADER_ROW_SX, children: [rowIdx === headerRows.length - 1 && hasCheckboxCol && (_jsx(TableCell, { ...{ padding: "checkbox", rowSpan: headerRows.length > 1 ? 1 : undefined, sx: CHECKBOX_CELL_SX }, children: _jsx(Checkbox, { checked: allSelected, indeterminate: someSelected, onChange: (_, c) => handleSelectAll(!!c), size: "small", "aria-label": "Select all rows" }) })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasCheckboxCol && (_jsx(TableCell, { ...{ rowSpan: headerRows.length - 1, sx: CHECKBOX_PLACEHOLDER_SX } })), rowIdx === headerRows.length - 1 && hasRowNumbersCol && (_jsx(TableCell, { ...{
253
+ return (_jsxs(Box, { sx: gridRootSx, children: [_jsxs(Box, { ref: wrapperRef, tabIndex: 0, role: "region", "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'), "aria-labelledby": ariaLabelledBy, onMouseDown: (e) => { lastMouseShiftRef.current = e.shiftKey; }, onKeyDown: handleGridKeyDown, onContextMenu: PREVENT_DEFAULT, "data-overflow-x": allowOverflowX ? 'true' : 'false', "data-density": density, sx: wrapperSx, children: [_jsx(Box, { sx: WRAPPER_SCROLL_SX, children: _jsx(TableContainer, { sx: { minWidth: allowOverflowX ? minTableWidth : undefined }, children: _jsxs(Box, { ref: tableContainerRef, sx: isLoading && items.length > 0 ? TABLE_WRAPPER_LOADING_SX : TABLE_WRAPPER_SX, children: [_jsxs(Table, { size: "small", sx: { minWidth: minTableWidth, borderCollapse: 'separate', borderSpacing: 0 }, "data-freeze-rows": freezeRows != null && freezeRows >= 1 ? freezeRows : undefined, "data-freeze-cols": freezeCols != null && freezeCols >= 1 ? freezeCols : undefined, children: [_jsx(TableHead, { sx: STICKY_HEADER_SX, children: headerRows.map((row, rowIdx) => (_jsxs(TableRow, { sx: HEADER_ROW_SX, children: [rowIdx === headerRows.length - 1 && hasCheckboxCol && (_jsx(TableCell, { ...{ padding: "checkbox", rowSpan: headerRows.length > 1 ? 1 : undefined, sx: CHECKBOX_CELL_SX }, children: _jsx(Checkbox, { checked: allSelected, indeterminate: someSelected, onChange: (_, c) => handleSelectAll(!!c), size: "small", "aria-label": "Select all rows" }) })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasCheckboxCol && (_jsx(TableCell, { ...{ rowSpan: headerRows.length - 1, sx: CHECKBOX_PLACEHOLDER_SX } })), rowIdx === headerRows.length - 1 && hasRowNumbersCol && (_jsx(TableCell, { ...{
285
254
  component: "th",
286
255
  scope: "col",
287
256
  rowSpan: headerRows.length > 1 ? 1 : undefined,
@@ -320,10 +289,16 @@ function DataGridTableInner(props) {
320
289
  const col = cell.columnDef;
321
290
  const colIdx = visibleCols.indexOf(col);
322
291
  const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
323
- const isPinnedLeft = col.pinned === 'left';
324
- const isPinnedRight = col.pinned === 'right';
292
+ const isPinnedLeft = pinning.pinnedColumns[col.columnId] === 'left';
293
+ const isPinnedRight = pinning.pinnedColumns[col.columnId] === 'right';
325
294
  const columnWidth = getColumnWidth(col);
326
- const headerSx = isPinnedLeft || (isFreezeCol && colIdx === 0) ? HEADER_PINNED_LEFT_SX : isPinnedRight ? HEADER_PINNED_RIGHT_SX : HEADER_BASE_SX;
295
+ const baseHeaderSx = isPinnedLeft || (isFreezeCol && colIdx === 0) ? HEADER_PINNED_LEFT_SX : isPinnedRight ? HEADER_PINNED_RIGHT_SX : HEADER_BASE_SX;
296
+ // Override sticky offset for pinned columns (supports multiple pinned columns)
297
+ const headerSx = isPinnedLeft && pinning.leftOffsets[col.columnId] != null
298
+ ? { ...baseHeaderSx, left: pinning.leftOffsets[col.columnId] }
299
+ : isPinnedRight && pinning.rightOffsets[col.columnId] != null
300
+ ? { ...baseHeaderSx, right: pinning.rightOffsets[col.columnId] }
301
+ : baseHeaderSx;
327
302
  // Determine aria-sort value for sorted columns
328
303
  const isSorted = props.sortBy === col.columnId;
329
304
  const ariaSort = isSorted
@@ -372,8 +347,13 @@ function DataGridTableInner(props) {
372
347
  '&:hover': {
373
348
  bgcolor: 'action.hover',
374
349
  },
375
- }, children: "\u22EE" })] }), _jsx(Box, { onMouseDown: (e) => handleResizeStart(e, col), sx: RESIZE_HANDLE_SX })] }, col.columnId));
376
- })] }, rowIdx))) }), !showEmptyInGrid && (_jsxs(TableBody, { children: [virtualScrollEnabled && visibleRange.offsetTop > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetTop }, "aria-hidden": true })), (virtualScrollEnabled
350
+ }, children: "\u22EE" })] }), _jsx(Box, { onMouseDown: (e) => {
351
+ setActiveCell(null);
352
+ interaction.setSelectionRange(null);
353
+ wrapperRef.current?.focus({ preventScroll: true });
354
+ handleResizeStart(e, col);
355
+ }, sx: RESIZE_HANDLE_SX })] }, col.columnId));
356
+ })] }, rowIdx))) }), !showEmptyInGrid && (_jsxs(TableBody, { sx: TABLE_BODY_SX, children: [virtualScrollEnabled && visibleRange.offsetTop > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetTop }, "aria-hidden": true })), (virtualScrollEnabled
377
357
  ? items.slice(visibleRange.startIndex, visibleRange.endIndex + 1).map((item, i) => {
378
358
  const rowIndex = visibleRange.startIndex + i;
379
359
  const rowIdStr = getRowId(item);
@@ -382,17 +362,7 @@ function DataGridTableInner(props) {
382
362
  : items.map((item, rowIndex) => {
383
363
  const rowIdStr = getRowId(item);
384
364
  return (_jsx(GridRow, { item: item, rowIndex: rowIndex, rowId: rowIdStr, isSelected: selectedRowIds.has(rowIdStr), columnLayouts: columnLayouts, renderCellContent: renderCellContent, handleSingleRowClick: handleSingleRowClick, handleRowCheckboxChange: handleRowCheckboxChange, lastMouseShiftRef: lastMouseShiftRef, hasCheckboxCol: hasCheckboxCol, hasRowNumbersCol: hasRowNumbersCol, rowNumberOffset: rowNumberOffset, selectionRange: selectionRange, activeCell: interaction.activeCell, cutRange: cutRange, copyRange: copyRange, isDragging: isDragging, editingRowId: editingCell?.rowId ?? null }, rowIdStr));
385
- })), virtualScrollEnabled && visibleRange.offsetBottom > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetBottom }, "aria-hidden": true }))] }))] }), isReorderDragging && dropIndicatorX != null && (_jsx(Box, { sx: {
386
- position: 'absolute',
387
- top: 0,
388
- bottom: 0,
389
- width: 3,
390
- bgcolor: 'var(--ogrid-primary, #217346)',
391
- pointerEvents: 'none',
392
- zIndex: 100,
393
- transition: 'left 0.05s',
394
- left: dropIndicatorX - (wrapperRef.current?.getBoundingClientRect().left ?? 0),
395
- } })), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset, items: items, visibleColumns: props.visibleColumns, columnSizingOverrides: columnSizingOverrides, columnOrder: props.columnOrder }), showEmptyInGrid && emptyState && (_jsx(Box, { sx: EMPTY_STATE_SX, children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: "No results found" }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx(Button, { variant: "text", size: "small", onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }))] }) }) }), menuPosition &&
396
- createPortal(_jsx(GridContextMenu, { x: menuPosition.x, y: menuPosition.y, hasSelection: hasCellSelection, canUndo: canUndo, canRedo: canRedo, onUndo: onUndo ?? NOOP, onRedo: onRedo ?? NOOP, onCopy: handleCopy, onCut: handleCut, onPaste: handlePasteVoid, onSelectAll: handleSelectAllCells, onClose: closeContextMenu }), document.body), _jsx(ColumnHeaderMenu, { columnId: pinning.headerMenu.openForColumn || '', isOpen: pinning.headerMenu.isOpen, anchorElement: pinning.headerMenu.anchorElement, onClose: pinning.headerMenu.close, onPinLeft: pinning.headerMenu.handlePinLeft, onPinRight: pinning.headerMenu.handlePinRight, onUnpin: pinning.headerMenu.handleUnpin, onSortAsc: pinning.headerMenu.handleSortAsc, onSortDesc: pinning.headerMenu.handleSortDesc, onClearSort: pinning.headerMenu.handleClearSort, onAutosizeThis: pinning.headerMenu.handleAutosizeThis, onAutosizeAll: pinning.headerMenu.handleAutosizeAll, canPinLeft: pinning.headerMenu.canPinLeft, canPinRight: pinning.headerMenu.canPinRight, canUnpin: pinning.headerMenu.canUnpin, currentSort: pinning.headerMenu.currentSort, isSortable: pinning.headerMenu.isSortable, isResizable: pinning.headerMenu.isResizable })] }), statusBarConfig && (_jsx(StatusBar, { totalCount: statusBarConfig.totalCount, filteredCount: statusBarConfig.filteredCount, selectedCount: statusBarConfig.selectedCount ?? selectedRowIds.size, selectedCellCount: selectionRange ? (Math.abs(selectionRange.endRow - selectionRange.startRow) + 1) * (Math.abs(selectionRange.endCol - selectionRange.startCol) + 1) : undefined, aggregation: statusBarConfig.aggregation, suppressRowCount: statusBarConfig.suppressRowCount })), isLoading && (_jsx(Box, { sx: LOADING_OVERLAY_SX, children: _jsxs(Box, { sx: LOADING_INNER_SX, children: [_jsx(CircularProgress, { size: 24 }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: loadingMessage })] }) }))] }));
365
+ })), virtualScrollEnabled && visibleRange.offsetBottom > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetBottom }, "aria-hidden": true }))] }))] }), isReorderDragging && dropIndicatorX != null && (_jsx(DropIndicator, { dropIndicatorX: dropIndicatorX, wrapperLeft: wrapperRef.current?.getBoundingClientRect().left ?? 0 })), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset, items: items, visibleColumns: props.visibleColumns, columnSizingOverrides: columnSizingOverrides, columnOrder: props.columnOrder }), showEmptyInGrid && emptyState && (_jsx(EmptyState, { emptyState: emptyState }))] }) }) }), menuPosition &&
366
+ createPortal(_jsx(GridContextMenu, { x: menuPosition.x, y: menuPosition.y, hasSelection: hasCellSelection, canUndo: canUndo, canRedo: canRedo, onUndo: onUndo ?? NOOP, onRedo: onRedo ?? NOOP, onCopy: handleCopy, onCut: handleCut, onPaste: handlePasteVoid, onSelectAll: o.interaction.handleSelectAllCells, onClose: closeContextMenu }), document.body), _jsx(ColumnHeaderMenu, { columnId: pinning.headerMenu.openForColumn || '', isOpen: pinning.headerMenu.isOpen, anchorElement: pinning.headerMenu.anchorElement, onClose: pinning.headerMenu.close, onPinLeft: pinning.headerMenu.handlePinLeft, onPinRight: pinning.headerMenu.handlePinRight, onUnpin: pinning.headerMenu.handleUnpin, onSortAsc: pinning.headerMenu.handleSortAsc, onSortDesc: pinning.headerMenu.handleSortDesc, onClearSort: pinning.headerMenu.handleClearSort, onAutosizeThis: pinning.headerMenu.handleAutosizeThis, onAutosizeAll: pinning.headerMenu.handleAutosizeAll, canPinLeft: pinning.headerMenu.canPinLeft, canPinRight: pinning.headerMenu.canPinRight, canUnpin: pinning.headerMenu.canUnpin, currentSort: pinning.headerMenu.currentSort, isSortable: pinning.headerMenu.isSortable, isResizable: pinning.headerMenu.isResizable })] }), statusBarConfig && (_jsx(StatusBar, { totalCount: statusBarConfig.totalCount, filteredCount: statusBarConfig.filteredCount, selectedCount: statusBarConfig.selectedCount ?? selectedRowIds.size, selectedCellCount: selectionRange ? (Math.abs(selectionRange.endRow - selectionRange.startRow) + 1) * (Math.abs(selectionRange.endCol - selectionRange.startCol) + 1) : undefined, aggregation: statusBarConfig.aggregation, suppressRowCount: statusBarConfig.suppressRowCount })), isLoading && (_jsx(LoadingOverlay, { message: loadingMessage }))] }));
397
367
  }
398
368
  export const DataGridTable = React.memo(DataGridTableInner);
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box } from '@mui/material';
3
+ export function DropIndicator({ dropIndicatorX, wrapperLeft }) {
4
+ return (_jsx(Box, { sx: {
5
+ position: 'absolute',
6
+ top: 0,
7
+ bottom: 0,
8
+ width: 3,
9
+ bgcolor: 'var(--ogrid-primary, #217346)',
10
+ pointerEvents: 'none',
11
+ zIndex: 100,
12
+ transition: 'left 0.05s',
13
+ left: dropIndicatorX - wrapperLeft,
14
+ } }));
15
+ }
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Typography, Button } from '@mui/material';
3
+ const EMPTY_STATE_SX = { py: 4, px: 2, textAlign: 'center', borderTop: 1, borderColor: 'divider', bgcolor: 'action.hover' };
4
+ export function EmptyState({ emptyState }) {
5
+ return (_jsx(Box, { sx: EMPTY_STATE_SX, children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: "No results found" }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx(Button, { variant: "text", size: "small", onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }));
6
+ }
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, CircularProgress, Typography } from '@mui/material';
3
+ const LOADING_OVERLAY_SX = {
4
+ position: 'absolute', inset: 0, zIndex: 2,
5
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
6
+ background: 'var(--ogrid-loading-bg, rgba(255,255,255,0.7))',
7
+ };
8
+ const LOADING_INNER_SX = {
9
+ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1,
10
+ p: 2, bgcolor: 'background.paper', border: 1, borderColor: 'divider', borderRadius: 1,
11
+ };
12
+ export function LoadingOverlay({ message }) {
13
+ return (_jsx(Box, { sx: LOADING_OVERLAY_SX, children: _jsxs(Box, { sx: LOADING_INNER_SX, children: [_jsx(CircularProgress, { size: 24 }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: message })] }) }));
14
+ }
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { forwardRef } from 'react';
4
+ import { Box, useTheme } from '@mui/material';
5
+ import { DataGridTable } from '../DataGridTable/DataGridTable';
6
+ import { ColumnChooser } from '../ColumnChooser/ColumnChooser';
7
+ import { PaginationControls } from '../PaginationControls/PaginationControls';
8
+ import { useOGrid, OGridLayout, } from '@alaarab/ogrid-react';
9
+ const OGridInner = forwardRef(function OGridInner(props, ref) {
10
+ const { dataGridProps, pagination, columnChooser, layout } = useOGrid(props, ref);
11
+ const theme = useTheme();
12
+ // Set --ogrid-* CSS variables so the shared OGridLayout adapts to MUI theme (both modes)
13
+ const containerSx = {
14
+ display: 'flex', flexDirection: 'column', gap: 1,
15
+ '--ogrid-bg': theme.palette.background.default,
16
+ '--ogrid-border': theme.palette.divider,
17
+ '--ogrid-header-bg': theme.palette.action.hover,
18
+ '--ogrid-fg': theme.palette.text.primary,
19
+ '--ogrid-fg-secondary': theme.palette.text.secondary,
20
+ '--ogrid-fg-muted': theme.palette.text.disabled,
21
+ '--ogrid-hover-bg': theme.palette.action.hover,
22
+ };
23
+ return (_jsx(OGridLayout, { containerComponent: Box, containerProps: { sx: containerSx }, className: layout.className, sideBar: layout.sideBarProps, toolbar: layout.toolbar, toolbarBelow: layout.toolbarBelow, toolbarEnd: columnChooser.placement === 'toolbar' ? (_jsx(ColumnChooser, { columns: columnChooser.columns, visibleColumns: columnChooser.visibleColumns, onVisibilityChange: columnChooser.onVisibilityChange })) : undefined, pagination: _jsx(PaginationControls, { currentPage: pagination.page, pageSize: pagination.pageSize, totalCount: pagination.displayTotalCount, onPageChange: pagination.setPage, onPageSizeChange: (size) => {
24
+ pagination.setPageSize(size);
25
+ pagination.setPage(1);
26
+ }, pageSizeOptions: pagination.pageSizeOptions, entityLabelPlural: pagination.entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
27
+ });
28
+ OGridInner.displayName = 'OGrid';
29
+ export const OGrid = React.memo(OGridInner);
@@ -0,0 +1 @@
1
+ export { OGrid } from './OGrid';
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Components
2
- export { OGrid, MaterialDataTable } from './MaterialDataTable';
2
+ export { OGrid } from './OGrid';
3
3
  export { DataGridTable } from './DataGridTable/DataGridTable';
4
4
  export { ColumnChooser } from './ColumnChooser/ColumnChooser';
5
5
  export { ColumnHeaderFilter } from './ColumnHeaderFilter/ColumnHeaderFilter';
@@ -1,10 +1,4 @@
1
1
  import * as React from 'react';
2
- import type { IColumnDefinition } from '@alaarab/ogrid-react';
3
- export type { IColumnDefinition };
4
- export interface IColumnChooserProps {
5
- columns: IColumnDefinition[];
6
- visibleColumns: Set<string>;
7
- onVisibilityChange: (columnKey: string, visible: boolean) => void;
8
- className?: string;
9
- }
2
+ import type { IColumnChooserProps } from '@alaarab/ogrid-react';
3
+ export type { IColumnChooserProps };
10
4
  export declare const ColumnChooser: React.FC<IColumnChooserProps>;
@@ -1,22 +1,4 @@
1
1
  import * as React from 'react';
2
- import type { UserLike, ColumnFilterType, IDateFilterValue } from '@alaarab/ogrid-react';
3
- export interface IColumnHeaderFilterProps {
4
- columnKey: string;
5
- columnName: string;
6
- filterType: ColumnFilterType;
7
- isSorted?: boolean;
8
- isSortedDescending?: boolean;
9
- onSort?: () => void;
10
- selectedValues?: string[];
11
- onFilterChange?: (values: string[]) => void;
12
- options?: string[];
13
- isLoadingOptions?: boolean;
14
- textValue?: string;
15
- onTextChange?: (value: string) => void;
16
- selectedUser?: UserLike;
17
- onUserChange?: (user: UserLike | undefined) => void;
18
- peopleSearch?: (query: string) => Promise<UserLike[]>;
19
- dateValue?: IDateFilterValue;
20
- onDateChange?: (value: IDateFilterValue | undefined) => void;
21
- }
2
+ import type { IColumnHeaderFilterProps } from '@alaarab/ogrid-react';
3
+ export type { IColumnHeaderFilterProps };
22
4
  export declare const ColumnHeaderFilter: React.FC<IColumnHeaderFilterProps>;
@@ -0,0 +1,7 @@
1
+ import * as React from 'react';
2
+ interface DropIndicatorProps {
3
+ dropIndicatorX: number;
4
+ wrapperLeft: number;
5
+ }
6
+ export declare function DropIndicator({ dropIndicatorX, wrapperLeft }: DropIndicatorProps): React.ReactElement;
7
+ export {};
@@ -0,0 +1,11 @@
1
+ import * as React from 'react';
2
+ interface EmptyStateProps {
3
+ emptyState: {
4
+ render?: () => React.ReactNode;
5
+ message?: React.ReactNode;
6
+ hasActiveFilters?: boolean;
7
+ onClearAll?: () => void;
8
+ };
9
+ }
10
+ export declare function EmptyState({ emptyState }: EmptyStateProps): React.ReactElement;
11
+ export {};
@@ -0,0 +1,6 @@
1
+ import * as React from 'react';
2
+ interface LoadingOverlayProps {
3
+ message: string;
4
+ }
5
+ export declare function LoadingOverlay({ message }: LoadingOverlayProps): React.ReactElement;
6
+ export {};
@@ -3,5 +3,3 @@ import { type IOGridProps, type IOGridApi } from '@alaarab/ogrid-react';
3
3
  export type { IOGridProps } from '@alaarab/ogrid-react';
4
4
  declare const OGridInner: React.ForwardRefExoticComponent<IOGridProps<unknown> & React.RefAttributes<IOGridApi<unknown>>>;
5
5
  export declare const OGrid: typeof OGridInner;
6
- /** @deprecated Use `OGrid` instead. Backward-compat alias. */
7
- export declare const MaterialDataTable: React.ForwardRefExoticComponent<IOGridProps<unknown> & React.RefAttributes<IOGridApi<unknown>>>;
@@ -0,0 +1 @@
1
+ export { OGrid, type IOGridProps } from './OGrid';
@@ -1,12 +1,4 @@
1
1
  import * as React from 'react';
2
- export interface IPaginationControlsProps {
3
- currentPage: number;
4
- pageSize: number;
5
- totalCount: number;
6
- onPageChange: (page: number) => void;
7
- onPageSizeChange: (pageSize: number) => void;
8
- pageSizeOptions?: number[];
9
- entityLabelPlural?: string;
10
- className?: string;
11
- }
2
+ import type { IPaginationControlsProps } from '@alaarab/ogrid-react';
3
+ export type { IPaginationControlsProps };
12
4
  export declare const PaginationControls: React.FC<IPaginationControlsProps>;
@@ -1,4 +1,4 @@
1
- export { OGrid, MaterialDataTable, type IOGridProps } from './MaterialDataTable';
1
+ export { OGrid, type IOGridProps } from './OGrid';
2
2
  export { DataGridTable } from './DataGridTable/DataGridTable';
3
3
  export { ColumnChooser, type IColumnChooserProps } from './ColumnChooser/ColumnChooser';
4
4
  export { ColumnHeaderFilter, type IColumnHeaderFilterProps } from './ColumnHeaderFilter/ColumnHeaderFilter';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-react-material",
3
- "version": "2.0.11",
4
- "description": "OGrid Material UI implementation – MUI Table–based data grid with sorting, filtering, pagination, column chooser, spreadsheet selection, and CSV export.",
3
+ "version": "2.0.13",
4
+ "description": "OGrid React Material implementation – MUI Table–based data grid with sorting, filtering, pagination, column chooser, spreadsheet selection, and CSV export.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
7
7
  "types": "dist/types/index.d.ts",
@@ -39,7 +39,7 @@
39
39
  "node": ">=18"
40
40
  },
41
41
  "dependencies": {
42
- "@alaarab/ogrid-react": "2.0.11"
42
+ "@alaarab/ogrid-react": "2.0.13"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@emotion/react": "^11.0.0",
@@ -1,19 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import * as React from 'react';
3
- import { forwardRef } from 'react';
4
- import { Box } from '@mui/material';
5
- import { DataGridTable } from '../DataGridTable/DataGridTable';
6
- import { ColumnChooser } from '../ColumnChooser/ColumnChooser';
7
- import { PaginationControls } from '../PaginationControls/PaginationControls';
8
- import { useOGrid, OGridLayout, } from '@alaarab/ogrid-react';
9
- const OGridInner = forwardRef(function OGridInner(props, ref) {
10
- const { dataGridProps, pagination, columnChooser, layout } = useOGrid(props, ref);
11
- return (_jsx(OGridLayout, { containerComponent: Box, containerProps: { sx: { display: 'flex', flexDirection: 'column', gap: 1 } }, className: layout.className, sideBar: layout.sideBarProps, toolbar: layout.toolbar, toolbarBelow: layout.toolbarBelow, toolbarEnd: columnChooser.placement === 'toolbar' ? (_jsx(ColumnChooser, { columns: columnChooser.columns, visibleColumns: columnChooser.visibleColumns, onVisibilityChange: columnChooser.onVisibilityChange })) : undefined, pagination: _jsx(PaginationControls, { currentPage: pagination.page, pageSize: pagination.pageSize, totalCount: pagination.displayTotalCount, onPageChange: pagination.setPage, onPageSizeChange: (size) => {
12
- pagination.setPageSize(size);
13
- pagination.setPage(1);
14
- }, pageSizeOptions: pagination.pageSizeOptions, entityLabelPlural: pagination.entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
15
- });
16
- OGridInner.displayName = 'OGrid';
17
- export const OGrid = React.memo(OGridInner);
18
- /** @deprecated Use `OGrid` instead. Backward-compat alias. */
19
- export const MaterialDataTable = OGrid;
@@ -1 +0,0 @@
1
- export { OGrid, MaterialDataTable } from './MaterialDataTable';
@@ -1 +0,0 @@
1
- export { OGrid, MaterialDataTable, type IOGridProps } from './MaterialDataTable';