@alaarab/ogrid-react-material 2.0.9 → 2.0.12

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">
@@ -26,7 +26,7 @@ export const ColumnChooser = (props) => {
26
26
  ev.stopPropagation();
27
27
  setColumnVisible(columnKey)(ev.target.checked);
28
28
  };
29
- return (_jsxs(Box, { className: className, sx: { display: 'inline-flex' }, children: [_jsxs(Button, { ref: buttonRef, variant: "outlined", size: "small", startIcon: _jsx(ViewColumnIcon, {}), endIcon: isOpen ? _jsx(ExpandLessIcon, {}) : _jsx(ExpandMoreIcon, {}), onClick: handleToggle, "aria-expanded": isOpen, "aria-haspopup": "listbox", sx: {
29
+ return (_jsxs(Box, { className: className, sx: { display: 'inline-flex' }, children: [_jsxs(Button, { ref: buttonRef, variant: "outlined", size: "small", color: "inherit", startIcon: _jsx(ViewColumnIcon, {}), endIcon: isOpen ? _jsx(ExpandLessIcon, {}) : _jsx(ExpandMoreIcon, {}), onClick: handleToggle, "aria-expanded": isOpen, "aria-haspopup": "listbox", sx: {
30
30
  textTransform: 'none',
31
31
  fontWeight: 600,
32
32
  borderColor: isOpen ? 'primary.main' : 'divider',
@@ -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,39 +1,53 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React from 'react';
3
- import Menu from '@mui/material/Menu';
4
- import MenuItem from '@mui/material/MenuItem';
5
- import IconButton from '@mui/material/IconButton';
6
- import MoreVertIcon from '@mui/icons-material/MoreVert';
7
- import { COLUMN_HEADER_MENU_ITEMS } from '@alaarab/ogrid-core';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useMemo, useEffect, useState } from 'react';
3
+ import { Menu, MenuItem, Divider } from '@mui/material';
4
+ import { getColumnHeaderMenuItems } from '@alaarab/ogrid-core';
8
5
  /**
9
- * Column header dropdown menu for pin/unpin actions.
10
- * Uses Material UI Menu component.
6
+ * Column header dropdown menu for pin/sort/autosize actions.
7
+ * Uses Material UI Menu component with anchor position.
11
8
  */
12
9
  export function ColumnHeaderMenu(props) {
13
- const { columnId, isOpen: _isOpen, anchorElement: _anchorElement, onClose, onPinLeft, onPinRight, onUnpin, canPinLeft, canPinRight, canUnpin, } = props;
14
- const [triggerEl, setTriggerEl] = React.useState(null);
15
- const handleTriggerClick = (event) => {
16
- event.stopPropagation();
17
- setTriggerEl(event.currentTarget);
10
+ const { isOpen, anchorElement, onClose, onPinLeft, onPinRight, onUnpin, onSortAsc, onSortDesc, onClearSort, onAutosizeThis, onAutosizeAll, canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable, } = props;
11
+ const [anchorPosition, setAnchorPosition] = useState(undefined);
12
+ useEffect(() => {
13
+ if (isOpen && anchorElement) {
14
+ const rect = anchorElement.getBoundingClientRect();
15
+ setAnchorPosition({
16
+ top: rect.bottom + 4,
17
+ left: rect.left,
18
+ });
19
+ }
20
+ else {
21
+ setAnchorPosition(undefined);
22
+ }
23
+ }, [isOpen, anchorElement]);
24
+ const menuInput = useMemo(() => ({
25
+ canPinLeft,
26
+ canPinRight,
27
+ canUnpin,
28
+ currentSort,
29
+ isSortable,
30
+ isResizable,
31
+ }), [canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable]);
32
+ const items = useMemo(() => getColumnHeaderMenuItems(menuInput), [menuInput]);
33
+ const handlers = {
34
+ pinLeft: onPinLeft,
35
+ pinRight: onPinRight,
36
+ unpin: onUnpin,
37
+ sortAsc: onSortAsc,
38
+ sortDesc: onSortDesc,
39
+ clearSort: onClearSort,
40
+ autosizeThis: onAutosizeThis,
41
+ autosizeAll: onAutosizeAll,
18
42
  };
19
- return (_jsxs(_Fragment, { children: [_jsx(IconButton, { size: "small", onClick: handleTriggerClick, "aria-label": `Column options for ${columnId}`, sx: {
20
- opacity: 0,
21
- transition: 'opacity 0.15s',
22
- padding: '4px',
23
- '.MuiTableCell-root:hover &': {
24
- opacity: 1,
25
- },
26
- '&:focus': {
27
- opacity: 1,
28
- },
29
- }, children: _jsx(MoreVertIcon, { fontSize: "small" }) }), _jsxs(Menu, { anchorEl: triggerEl, open: Boolean(triggerEl), onClose: () => {
30
- setTriggerEl(null);
31
- onClose();
32
- }, anchorOrigin: {
33
- vertical: 'bottom',
34
- horizontal: 'left',
35
- }, transformOrigin: {
36
- vertical: 'top',
37
- horizontal: 'left',
38
- }, children: [_jsx(MenuItem, { disabled: !canPinLeft, onClick: onPinLeft, children: COLUMN_HEADER_MENU_ITEMS[0].label }), _jsx(MenuItem, { disabled: !canPinRight, onClick: onPinRight, children: COLUMN_HEADER_MENU_ITEMS[1].label }), _jsx(MenuItem, { disabled: !canUnpin, onClick: onUnpin, children: COLUMN_HEADER_MENU_ITEMS[2].label })] })] }));
43
+ return (_jsx(Menu, { open: isOpen && !!anchorPosition, onClose: onClose, anchorReference: "anchorPosition", anchorPosition: anchorPosition, slotProps: {
44
+ paper: {
45
+ sx: {
46
+ minWidth: 140,
47
+ },
48
+ },
49
+ }, children: items.map((item, idx) => (_jsxs(React.Fragment, { children: [_jsx(MenuItem, { disabled: item.disabled, onClick: () => {
50
+ handlers[item.id]();
51
+ onClose();
52
+ }, children: item.label }), item.divider && idx < items.length - 1 && _jsx(Divider, {})] }, item.id))) }));
39
53
  }
@@ -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
@@ -18,7 +21,13 @@ const CHECKBOX_CELL_SX = { width: CHECKBOX_COLUMN_WIDTH, minWidth: CHECKBOX_COLU
18
21
  const CHECKBOX_WRAPPER_SX = { display: 'flex', alignItems: 'center', justifyContent: 'center' };
19
22
  const CHECKBOX_PLACEHOLDER_SX = { width: CHECKBOX_COLUMN_WIDTH, minWidth: CHECKBOX_COLUMN_WIDTH, p: 0 };
20
23
  // Header
21
- const STICKY_HEADER_SX = { position: 'sticky', top: 0, zIndex: 8, bgcolor: 'action.hover', '& th': { bgcolor: 'action.hover' } };
24
+ const STICKY_HEADER_SX = {
25
+ /* Removed position: 'sticky', top: 0 - breaks horizontal sticky on pinned columns.
26
+ Instead, apply sticky to individual header cells (HEADER_BASE_SX). */
27
+ zIndex: 8,
28
+ bgcolor: 'action.hover',
29
+ '& th': { bgcolor: 'action.hover' }
30
+ };
22
31
  const HEADER_ROW_SX = { bgcolor: 'action.hover' };
23
32
  const GROUP_HEADER_CELL_SX = { textAlign: 'center', fontWeight: 600, borderBottom: 2, borderColor: 'divider', py: 0.75 };
24
33
  // Density padding helper
@@ -34,6 +43,7 @@ const CELL_CONTENT_BASE_SX = {
34
43
  width: '100%', height: '100%', display: 'flex', alignItems: 'center', minWidth: 0,
35
44
  px: '10px', py: '6px', boxSizing: 'border-box', overflow: 'hidden',
36
45
  textOverflow: 'ellipsis', whiteSpace: 'nowrap', userSelect: 'none', outline: 'none',
46
+ '&:focus-visible': { outline: '2px solid', outlineColor: 'primary.main', outlineOffset: '-2px', zIndex: 3 },
37
47
  };
38
48
  const CELL_CONTENT_NUMERIC_SX = { ...CELL_CONTENT_BASE_SX, justifyContent: 'flex-end', textAlign: 'right' };
39
49
  const CELL_CONTENT_BOOLEAN_SX = { ...CELL_CONTENT_BASE_SX, justifyContent: 'center', textAlign: 'center' };
@@ -41,8 +51,9 @@ const CELL_CONTENT_EDITABLE_SX = { ...CELL_CONTENT_BASE_SX, cursor: 'cell' };
41
51
  const CELL_CONTENT_NUMERIC_EDITABLE_SX = { ...CELL_CONTENT_NUMERIC_SX, cursor: 'cell' };
42
52
  const CELL_CONTENT_BOOLEAN_EDITABLE_SX = { ...CELL_CONTENT_BOOLEAN_SX, cursor: 'cell' };
43
53
  // Cell overlay states (only applied to the few active/selected cells)
44
- const CELL_ACTIVE_SX = { outline: '2px solid var(--ogrid-selection, #217346)', outlineOffset: '-1px', zIndex: 2, position: 'relative', overflow: 'visible' };
45
- 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' } };
46
57
  const CELL_CUT_RANGE_SX = { bgcolor: 'action.hover', opacity: 0.7 };
47
58
  // Pre-computed overlay variant arrays (avoid per-cell array allocation + filter)
48
59
  // Key: `${base}_${overlay}` where overlay is 'active' | 'range' | 'cut'
@@ -98,12 +109,46 @@ const FILL_HANDLE_SX = {
98
109
  };
99
110
  // Cell <td> positioning variants
100
111
  const CELL_TD_BASE_SX = { position: 'relative', p: 0, height: '1px' };
101
- 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' };
102
- 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
+ };
103
128
  // Header cell positioning variants
104
- const HEADER_BASE_SX = { fontWeight: 600, position: 'relative' };
105
- const HEADER_PINNED_LEFT_SX = { ...HEADER_BASE_SX, position: 'sticky', left: 0, top: 0, zIndex: 9, bgcolor: 'action.hover', willChange: 'transform', borderLeft: '2px solid', borderLeftColor: 'primary.main' };
106
- const HEADER_PINNED_RIGHT_SX = { ...HEADER_BASE_SX, position: 'sticky', right: 0, top: 0, zIndex: 9, bgcolor: 'action.hover', willChange: 'transform', borderRight: '2px solid', borderRightColor: 'primary.main' };
129
+ const HEADER_BASE_SX = {
130
+ fontWeight: 600,
131
+ position: 'sticky', /* Enables vertical sticky for all headers */
132
+ top: 0, /* Sticky vertically */
133
+ zIndex: 8, /* Stack above body cells */
134
+ bgcolor: 'action.hover' /* Required for sticky overlap */
135
+ };
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
+ };
107
152
  // Resize handle
108
153
  const RESIZE_HANDLE_SX = {
109
154
  position: 'absolute', top: 0, right: '-3px', bottom: 0, width: '8px',
@@ -120,22 +165,9 @@ const WRAPPER_SCROLL_SX = { display: 'flex', flexDirection: 'column', minHeight:
120
165
  // Table wrapper
121
166
  const TABLE_WRAPPER_SX = { position: 'relative', opacity: 1 };
122
167
  const TABLE_WRAPPER_LOADING_SX = { position: 'relative', opacity: 0.6 };
123
- // Empty state
124
- const EMPTY_STATE_SX = { py: 4, px: 2, textAlign: 'center', borderTop: 1, borderColor: 'divider', bgcolor: 'action.hover' };
125
- // Loading overlay
126
- const LOADING_OVERLAY_SX = {
127
- position: 'absolute', inset: 0, zIndex: 2,
128
- display: 'flex', alignItems: 'center', justifyContent: 'center',
129
- background: 'var(--ogrid-loading-bg, rgba(255,255,255,0.7))',
130
- };
131
- const LOADING_INNER_SX = {
132
- display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1,
133
- p: 2, bgcolor: 'background.paper', border: 1, borderColor: 'divider', borderRadius: 1,
134
- };
135
- // Module-scope event handlers
136
- const STOP_PROPAGATION = (e) => e.stopPropagation();
137
- const PREVENT_DEFAULT = (e) => { e.preventDefault(); };
138
- 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' } };
139
171
  function GridRowInner(props) {
140
172
  const { item, rowIndex, rowId, isSelected, columnLayouts, renderCellContent, handleSingleRowClick, handleRowCheckboxChange, lastMouseShiftRef, hasCheckboxCol, hasRowNumbersCol, rowNumberOffset, } = props;
141
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: {
@@ -150,82 +182,32 @@ function GridRowInner(props) {
150
182
  position: 'sticky',
151
183
  left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
152
184
  zIndex: 3,
153
- }, children: rowNumberOffset + rowIndex + 1 })), columnLayouts.map((cl, colIdx) => (_jsx(TableCell, { sx: cl.tdSx, style: { 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)))] }));
154
186
  }
155
187
  const GridRow = React.memo(GridRowInner, areGridRowPropsEqual);
156
188
  function DataGridTableInner(props) {
157
- const wrapperRef = useRef(null);
158
- const tableContainerRef = useRef(null);
159
- const state = useDataGridState({ props, wrapperRef });
160
- const lastMouseShiftRef = useRef(false);
161
- const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels, pinning } = state;
162
- const { visibleCols, hasCheckboxCol, hasRowNumbersCol, colOffset, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides } = layout;
163
- const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
164
- const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
165
- const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging } = interaction;
166
- const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
167
- const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
168
- const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
169
- 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;
170
- // Calculate row number offset for pagination
171
- const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * propPageSize : 0;
172
- const fitToContent = layoutMode === 'content';
173
- 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;
174
191
  // Density-aware cell padding
175
192
  const densityPadding = useMemo(() => getDensityPadding(density), [density]);
176
193
  const _cellSx = useMemo(() => ({ ...CELL_CONTENT_BASE_SX, ...densityPadding }), [densityPadding]);
177
194
  const headerCellSx = useMemo(() => ({ px: densityPadding.px, py: densityPadding.py }), [densityPadding]);
178
- // Memoize header rows (recursive tree traversal)
179
- const headerRows = useMemo(() => buildHeaderRows(props.columns, props.visibleColumns), [props.columns, props.visibleColumns]);
180
- const { handleResizeStart, getColumnWidth } = useColumnResize({
181
- columnSizingOverrides,
182
- setColumnSizingOverrides,
183
- });
184
- const { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown } = useColumnReorder({
185
- columns: visibleCols,
186
- columnOrder,
187
- onColumnOrderChange,
188
- enabled: columnReorder === true,
189
- pinnedColumns,
190
- wrapperRef,
191
- });
192
- const virtualScrollEnabled = virtualScroll?.enabled === true;
193
- const virtualRowHeight = virtualScroll?.rowHeight ?? 36;
194
- const { visibleRange } = useVirtualScroll({
195
- totalRows: items.length,
196
- rowHeight: virtualRowHeight,
197
- enabled: virtualScrollEnabled,
198
- overscan: virtualScroll?.overscan,
199
- containerRef: wrapperRef,
200
- });
201
195
  // Pre-compute per-column layout (tdSx, widths) so GridRow doesn't recalculate per-cell
202
196
  const columnLayouts = useMemo(() => visibleCols.map((col, colIdx) => {
203
197
  const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
204
- const isPinnedLeft = col.pinned === 'left';
205
- const isPinnedRight = col.pinned === 'right';
198
+ const isPinnedLeft = pinning.pinnedColumns[col.columnId] === 'left';
199
+ const isPinnedRight = pinning.pinnedColumns[col.columnId] === 'right';
206
200
  const columnWidth = getColumnWidth(col);
207
- const tdSx = isPinnedLeft || (isFreezeCol && colIdx === 0) ? CELL_TD_PINNED_LEFT_SX : isPinnedRight ? CELL_TD_PINNED_RIGHT_SX : CELL_TD_BASE_SX;
208
- return { col, tdSx, minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH, width: columnWidth, maxWidth: columnWidth };
209
- }), [visibleCols, freezeCols, getColumnWidth]);
210
- const editCallbacks = useMemo(() => ({ commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit }), [commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit]);
211
- const interactionHandlers = useMemo(() => ({ handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu }), [handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu]);
212
- // Refs for volatile state — lets renderCellContent be stable (same function ref across
213
- // selection changes) so that GridRow's React.memo comparator can skip unaffected rows.
214
- const cellDescriptorInputRef = useLatestRef(cellDescriptorInput);
215
- const pendingEditorValueRef = useLatestRef(pendingEditorValue);
216
- const popoverAnchorElRef = useLatestRef(popoverAnchorEl);
217
- // Stable row-click handler
218
- const selectedRowIdsRef = useLatestRef(selectedRowIds);
219
- const handleSingleRowClick = useCallback((e) => {
220
- if (rowSelection !== 'single')
221
- return;
222
- const rowId = e.currentTarget.dataset.rowId;
223
- if (!rowId)
224
- return;
225
- const ids = selectedRowIdsRef.current;
226
- updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
227
- // eslint-disable-next-line react-hooks/exhaustive-deps -- selectedRowIdsRef is a stable ref
228
- }, [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]);
229
211
  // Wrapper sx (depends on dynamic values — memoize to avoid recreation)
230
212
  const wrapperSx = useMemo(() => ({
231
213
  position: 'relative',
@@ -268,42 +250,82 @@ function DataGridTableInner(props) {
268
250
  },
269
251
  // eslint-disable-next-line react-hooks/exhaustive-deps -- *Ref vars are stable refs from useLatestRef
270
252
  [editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit, getRowId, onCellError]);
271
- 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, { component: "th", scope: "col", rowSpan: headerRows.length > 1 ? 1 : undefined, sx: {
272
- width: ROW_NUMBER_COLUMN_WIDTH,
273
- minWidth: ROW_NUMBER_COLUMN_WIDTH,
274
- maxWidth: ROW_NUMBER_COLUMN_WIDTH,
275
- textAlign: 'center',
276
- fontWeight: 600,
277
- backgroundColor: 'action.hover',
278
- position: 'sticky',
279
- left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
280
- zIndex: 4,
281
- ...headerCellSx,
282
- }, children: "#" })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasRowNumbersCol && (_jsx(TableCell, { rowSpan: headerRows.length - 1, sx: {
283
- width: ROW_NUMBER_COLUMN_WIDTH,
284
- minWidth: ROW_NUMBER_COLUMN_WIDTH,
285
- position: 'sticky',
286
- left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
287
- zIndex: 4,
288
- backgroundColor: 'background.paper',
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, { ...{
254
+ component: "th",
255
+ scope: "col",
256
+ rowSpan: headerRows.length > 1 ? 1 : undefined,
257
+ sx: {
258
+ width: ROW_NUMBER_COLUMN_WIDTH,
259
+ minWidth: ROW_NUMBER_COLUMN_WIDTH,
260
+ maxWidth: ROW_NUMBER_COLUMN_WIDTH,
261
+ textAlign: 'center',
262
+ fontWeight: 600,
263
+ backgroundColor: 'action.hover',
264
+ position: 'sticky',
265
+ left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
266
+ zIndex: 4,
267
+ ...headerCellSx,
268
+ }
269
+ }, children: "#" })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasRowNumbersCol && (_jsx(TableCell, { ...{
270
+ rowSpan: headerRows.length - 1,
271
+ sx: {
272
+ width: ROW_NUMBER_COLUMN_WIDTH,
273
+ minWidth: ROW_NUMBER_COLUMN_WIDTH,
274
+ position: 'sticky',
275
+ left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
276
+ zIndex: 4,
277
+ backgroundColor: 'background.paper',
278
+ }
289
279
  } })), row.map((cell, cellIdx) => {
290
280
  if (cell.isGroup) {
291
- return (_jsx(TableCell, { colSpan: cell.colSpan, component: "th", scope: "colgroup", sx: GROUP_HEADER_CELL_SX, children: cell.label }, cellIdx));
281
+ return (_jsx(TableCell, { ...{
282
+ colSpan: cell.colSpan,
283
+ component: "th",
284
+ scope: "colgroup",
285
+ sx: GROUP_HEADER_CELL_SX
286
+ }, children: cell.label }, cellIdx));
292
287
  }
293
288
  // Leaf cell
294
289
  const col = cell.columnDef;
295
290
  const colIdx = visibleCols.indexOf(col);
296
291
  const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
297
- const isPinnedLeft = col.pinned === 'left';
298
- const isPinnedRight = col.pinned === 'right';
292
+ const isPinnedLeft = pinning.pinnedColumns[col.columnId] === 'left';
293
+ const isPinnedRight = pinning.pinnedColumns[col.columnId] === 'right';
299
294
  const columnWidth = getColumnWidth(col);
300
- const headerSx = isPinnedLeft || (isFreezeCol && colIdx === 0) ? HEADER_PINNED_LEFT_SX : isPinnedRight ? HEADER_PINNED_RIGHT_SX : HEADER_BASE_SX;
301
- return (_jsxs(TableCell, { component: "th", scope: "col", "data-column-id": col.columnId, rowSpan: headerRows.length > 1 ? headerRows.length - rowIdx : undefined, sx: { ...headerSx, ...headerCellSx }, style: {
302
- minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
303
- width: columnWidth,
304
- maxWidth: columnWidth,
305
- ...(columnReorder ? { cursor: isReorderDragging ? 'grabbing' : 'grab' } : undefined),
306
- }, onMouseDown: columnReorder ? (e) => handleHeaderMouseDown(col.columnId, e) : undefined, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx(Box, { component: "button", onClick: (e) => {
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;
302
+ // Determine aria-sort value for sorted columns
303
+ const isSorted = props.sortBy === col.columnId;
304
+ const ariaSort = isSorted
305
+ ? (props.sortDirection === 'asc' ? 'ascending' : 'descending')
306
+ : undefined;
307
+ return (_jsxs(TableCell, { ...{
308
+ component: "th",
309
+ scope: "col",
310
+ 'data-column-id': col.columnId,
311
+ rowSpan: headerRows.length > 1 ? headerRows.length - rowIdx : undefined,
312
+ 'aria-sort': ariaSort,
313
+ sx: {
314
+ ...headerSx,
315
+ ...headerCellSx,
316
+ minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
317
+ width: columnWidth,
318
+ maxWidth: columnWidth,
319
+ ...(columnReorder ? { cursor: isReorderDragging ? 'grabbing' : 'grab' } : {}),
320
+ '&:focus-visible': {
321
+ outline: '2px solid',
322
+ outlineColor: 'primary.main',
323
+ outlineOffset: '-2px',
324
+ zIndex: 11,
325
+ },
326
+ },
327
+ onMouseDown: columnReorder ? (e) => handleHeaderMouseDown(col.columnId, e) : undefined
328
+ }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx(Box, { component: "button", onClick: (e) => {
307
329
  e.stopPropagation();
308
330
  pinning.headerMenu.open(col.columnId, e.currentTarget);
309
331
  }, "aria-label": "Column options", title: "Column options", sx: {
@@ -314,8 +336,8 @@ function DataGridTableInner(props) {
314
336
  fontSize: '16px',
315
337
  lineHeight: 1,
316
338
  color: 'text.secondary',
317
- opacity: 0,
318
- transition: 'opacity 0.15s, background-color 0.15s',
339
+ opacity: 1,
340
+ transition: 'background-color 0.15s',
319
341
  borderRadius: '3px',
320
342
  display: 'flex',
321
343
  alignItems: 'center',
@@ -324,13 +346,14 @@ function DataGridTableInner(props) {
324
346
  height: '20px',
325
347
  '&:hover': {
326
348
  bgcolor: 'action.hover',
327
- opacity: 1,
328
- },
329
- 'th:hover &': {
330
- opacity: 1,
331
349
  },
332
- }, children: "\u22EE" })] }), _jsx(Box, { onMouseDown: (e) => handleResizeStart(e, col), sx: RESIZE_HANDLE_SX })] }, col.columnId));
333
- })] }, 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
334
357
  ? items.slice(visibleRange.startIndex, visibleRange.endIndex + 1).map((item, i) => {
335
358
  const rowIndex = visibleRange.startIndex + i;
336
359
  const rowIdStr = getRowId(item);
@@ -339,17 +362,7 @@ function DataGridTableInner(props) {
339
362
  : items.map((item, rowIndex) => {
340
363
  const rowIdStr = getRowId(item);
341
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));
342
- })), virtualScrollEnabled && visibleRange.offsetBottom > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetBottom }, "aria-hidden": true }))] }))] }), isReorderDragging && dropIndicatorX != null && (_jsx(Box, { sx: {
343
- position: 'absolute',
344
- top: 0,
345
- bottom: 0,
346
- width: 3,
347
- bgcolor: 'var(--ogrid-primary, #217346)',
348
- pointerEvents: 'none',
349
- zIndex: 100,
350
- transition: 'left 0.05s',
351
- left: dropIndicatorX - (wrapperRef.current?.getBoundingClientRect().left ?? 0),
352
- } })), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset }), 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 &&
353
- 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, canPinLeft: pinning.headerMenu.canPinLeft, canPinRight: pinning.headerMenu.canPinRight, canUnpin: pinning.headerMenu.canUnpin })] }), 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 }))] }));
354
367
  }
355
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';
@@ -1,16 +1,22 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
- import { useMemo, useCallback } from 'react';
4
3
  import { IconButton, Button, Select, MenuItem, Box, Typography, } from '@mui/material';
5
4
  import { FirstPage as FirstPageIcon, LastPage as LastPageIcon, ChevronLeft as ChevronLeftIcon, ChevronRight as ChevronRightIcon, } from '@mui/icons-material';
6
- import { getPaginationViewModel } from '@alaarab/ogrid-react';
5
+ import { usePaginationControls } from '@alaarab/ogrid-react';
7
6
  export const PaginationControls = React.memo((props) => {
8
7
  const { currentPage, pageSize, totalCount, onPageChange, onPageSizeChange, pageSizeOptions, entityLabelPlural, className, } = props;
9
- const labelPlural = entityLabelPlural ?? 'items';
10
- const vm = useMemo(() => getPaginationViewModel(currentPage, pageSize, totalCount, pageSizeOptions ? { pageSizeOptions } : undefined), [currentPage, pageSize, totalCount, pageSizeOptions]);
11
- const handlePageSizeChange = useCallback((event) => {
12
- onPageSizeChange(Number(event.target.value));
13
- }, [onPageSizeChange]);
8
+ const { labelPlural, vm, handlePageSizeChange } = usePaginationControls({
9
+ currentPage,
10
+ pageSize,
11
+ totalCount,
12
+ onPageChange,
13
+ onPageSizeChange,
14
+ pageSizeOptions,
15
+ entityLabelPlural,
16
+ });
17
+ const handlePageSizeChangeEvent = (event) => {
18
+ handlePageSizeChange(Number(event.target.value));
19
+ };
14
20
  if (!vm) {
15
21
  return null;
16
22
  }
@@ -25,5 +31,5 @@ export const PaginationControls = React.memo((props) => {
25
31
  width: '100%',
26
32
  minWidth: 0,
27
33
  boxSizing: 'border-box',
28
- }, children: [_jsxs(Typography, { variant: "body2", color: "text.secondary", children: ["Showing ", startItem, " to ", endItem, " of ", totalCount.toLocaleString(), " ", labelPlural] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(IconButton, { size: "small", onClick: () => onPageChange(1), disabled: currentPage === 1, "aria-label": "First page", children: _jsx(FirstPageIcon, { fontSize: "small" }) }), _jsx(IconButton, { size: "small", onClick: () => onPageChange(currentPage - 1), disabled: currentPage === 1, "aria-label": "Previous page", children: _jsx(ChevronLeftIcon, { fontSize: "small" }) }), showStartEllipsis && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outlined", size: "small", onClick: () => onPageChange(1), "aria-label": "Page 1", sx: { minWidth: 32, px: 0.5 }, children: "1" }), _jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mx: 0.5 }, "aria-hidden": true, children: "\u2026" })] })), pageNumbers.map((pageNum) => (_jsx(Button, { variant: currentPage === pageNum ? 'contained' : 'outlined', size: "small", onClick: () => onPageChange(pageNum), "aria-label": `Page ${pageNum}`, "aria-current": currentPage === pageNum ? 'page' : undefined, sx: { minWidth: 32, px: 0.5 }, children: pageNum }, pageNum))), showEndEllipsis && (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mx: 0.5 }, "aria-hidden": true, children: "\u2026" }), _jsx(Button, { variant: "outlined", size: "small", onClick: () => onPageChange(totalPages), "aria-label": `Page ${totalPages}`, sx: { minWidth: 32, px: 0.5 }, children: totalPages })] })), _jsx(IconButton, { size: "small", onClick: () => onPageChange(currentPage + 1), disabled: currentPage >= totalPages, "aria-label": "Next page", children: _jsx(ChevronRightIcon, { fontSize: "small" }) }), _jsx(IconButton, { size: "small", onClick: () => onPageChange(totalPages), disabled: currentPage >= totalPages, "aria-label": "Last page", children: _jsx(LastPageIcon, { fontSize: "small" }) })] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "body2", color: "text.secondary", children: "Rows" }), _jsx(Select, { value: pageSize, onChange: handlePageSizeChange, size: "small", "aria-label": "Rows per page", sx: { minWidth: 70 }, children: vm.pageSizeOptions.map((n) => (_jsx(MenuItem, { value: n, children: n }, n))) })] })] }));
34
+ }, children: [_jsxs(Typography, { variant: "body2", color: "text.secondary", children: ["Showing ", startItem, " to ", endItem, " of ", totalCount.toLocaleString(), " ", labelPlural] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(IconButton, { size: "small", onClick: () => onPageChange(1), disabled: currentPage === 1, "aria-label": "First page", children: _jsx(FirstPageIcon, { fontSize: "small" }) }), _jsx(IconButton, { size: "small", onClick: () => onPageChange(currentPage - 1), disabled: currentPage === 1, "aria-label": "Previous page", children: _jsx(ChevronLeftIcon, { fontSize: "small" }) }), showStartEllipsis && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outlined", size: "small", onClick: () => onPageChange(1), "aria-label": "Page 1", sx: { minWidth: 32, px: 0.5 }, children: "1" }), _jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mx: 0.5 }, "aria-hidden": true, children: "\u2026" })] })), pageNumbers.map((pageNum) => (_jsx(Button, { variant: currentPage === pageNum ? 'contained' : 'outlined', size: "small", onClick: () => onPageChange(pageNum), "aria-label": `Page ${pageNum}`, "aria-current": currentPage === pageNum ? 'page' : undefined, sx: { minWidth: 32, px: 0.5 }, children: pageNum }, pageNum))), showEndEllipsis && (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mx: 0.5 }, "aria-hidden": true, children: "\u2026" }), _jsx(Button, { variant: "outlined", size: "small", onClick: () => onPageChange(totalPages), "aria-label": `Page ${totalPages}`, sx: { minWidth: 32, px: 0.5 }, children: totalPages })] })), _jsx(IconButton, { size: "small", onClick: () => onPageChange(currentPage + 1), disabled: currentPage >= totalPages, "aria-label": "Next page", children: _jsx(ChevronRightIcon, { fontSize: "small" }) }), _jsx(IconButton, { size: "small", onClick: () => onPageChange(totalPages), disabled: currentPage >= totalPages, "aria-label": "Last page", children: _jsx(LastPageIcon, { fontSize: "small" }) })] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "body2", color: "text.secondary", children: "Rows" }), _jsx(Select, { value: pageSize, onChange: handlePageSizeChangeEvent, size: "small", "aria-label": "Rows per page", sx: { minWidth: 70 }, children: vm.pageSizeOptions.map((n) => (_jsx(MenuItem, { value: n, children: n }, n))) })] })] }));
29
35
  });
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>;
@@ -6,12 +6,20 @@ export interface ColumnHeaderMenuProps {
6
6
  onPinLeft: () => void;
7
7
  onPinRight: () => void;
8
8
  onUnpin: () => void;
9
+ onSortAsc: () => void;
10
+ onSortDesc: () => void;
11
+ onClearSort: () => void;
12
+ onAutosizeThis: () => void;
13
+ onAutosizeAll: () => void;
9
14
  canPinLeft: boolean;
10
15
  canPinRight: boolean;
11
16
  canUnpin: boolean;
17
+ currentSort: 'asc' | 'desc' | null;
18
+ isSortable: boolean;
19
+ isResizable: boolean;
12
20
  }
13
21
  /**
14
- * Column header dropdown menu for pin/unpin actions.
15
- * Uses Material UI Menu component.
22
+ * Column header dropdown menu for pin/sort/autosize actions.
23
+ * Uses Material UI Menu component with anchor position.
16
24
  */
17
25
  export declare function ColumnHeaderMenu(props: ColumnHeaderMenuProps): import("react/jsx-runtime").JSX.Element;
@@ -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.9",
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.12",
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.9"
42
+ "@alaarab/ogrid-react": "2.0.12"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@emotion/react": "^11.0.0",
@@ -58,8 +58,8 @@
58
58
  "@testing-library/jest-dom": "^6.9.1",
59
59
  "@testing-library/react": "^16.3.2",
60
60
  "@testing-library/user-event": "^14.6.1",
61
- "@types/react": "^18.3.18",
62
- "@types/react-dom": "^18.3.5",
61
+ "@types/react": "^19.0.0",
62
+ "@types/react-dom": "^19.0.0",
63
63
  "eslint-plugin-storybook": "10.2.8",
64
64
  "react": "^18.3.1",
65
65
  "react-dom": "^18.3.1",
@@ -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';