@alaarab/ogrid 1.2.2 → 1.3.1

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.
@@ -8,13 +8,14 @@ import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
8
8
  import { InlineCellEditor } from './InlineCellEditor';
9
9
  import { StatusBar } from './StatusBar';
10
10
  import { GridContextMenu } from './GridContextMenu';
11
- import { useDataGridState, useColumnResize, getHeaderFilterConfig, getCellRenderDescriptor, } from '@alaarab/ogrid-core';
11
+ import { useDataGridState, useColumnResize, getHeaderFilterConfig, getCellRenderDescriptor, MarchingAntsOverlay, } from '@alaarab/ogrid-core';
12
12
  import styles from './DataGridTable.module.css';
13
13
  function DataGridTableInner(props) {
14
14
  const wrapperRef = useRef(null);
15
+ const tableContainerRef = useRef(null);
15
16
  const state = useDataGridState({ props, wrapperRef });
16
17
  const lastMouseShiftRef = useRef(false);
17
- const { visibleCols, totalColCount, hasCheckboxCol, selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected, setEditingCell, pendingEditorValue, setPendingEditorValue, setActiveCell, handleCellMouseDown, handleSelectAllCells, contextMenu, setContextMenu, handleCellContextMenu, closeContextMenu, handleCopy, handleCut, handlePaste, handleGridKeyDown, handleFillHandleMouseDown, containerWidth, minTableWidth, columnSizingOverrides, setColumnSizingOverrides, statusBarConfig, showEmptyInGrid, hasCellSelection, selectionRange, headerFilterInput, cellDescriptorInput, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl, } = state;
18
+ const { visibleCols, totalColCount, hasCheckboxCol, selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected, setEditingCell, pendingEditorValue, setPendingEditorValue, setActiveCell, handleCellMouseDown, handleSelectAllCells, contextMenu, setContextMenu, handleCellContextMenu, closeContextMenu, canUndo, canRedo, onUndo, onRedo, handleCopy, handleCut, handlePaste, handleGridKeyDown, handleFillHandleMouseDown, containerWidth, minTableWidth, columnSizingOverrides, setColumnSizingOverrides, statusBarConfig, showEmptyInGrid, hasCellSelection, selectionRange, copyRange, cutRange, colOffset, headerFilterInput, cellDescriptorInput, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl, } = state;
18
19
  const { items, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
19
20
  const allowOverflowX = containerWidth > 0 && minTableWidth > containerWidth;
20
21
  const fitToContent = layoutMode === 'content';
@@ -63,61 +64,64 @@ function DataGridTableInner(props) {
63
64
  content = _jsx("span", { style: cellStyle, children: content });
64
65
  const cellClassNames = [
65
66
  styles.cellContent,
66
- descriptor.isActive ? styles.activeCellContent : '',
67
+ descriptor.isActive && !descriptor.isInRange ? styles.activeCellContent : '',
67
68
  descriptor.isInRange ? styles.cellInRange : '',
68
69
  descriptor.isInCutRange ? styles.cellCut : '',
70
+ descriptor.isInCopyRange ? styles.cellCopied : '',
69
71
  ].filter(Boolean).join(' ');
70
72
  if (descriptor.canEditAny) {
71
- return (_jsxs("div", { className: cellClassNames, "data-row-index": descriptor.rowIndex, "data-col-index": descriptor.globalColIndex, "data-in-range": descriptor.isInRange ? 'true' : undefined, role: "button", tabIndex: descriptor.isActive ? 0 : -1, onMouseDown: (e) => handleCellMouseDown(e, descriptor.rowIndex, descriptor.globalColIndex), onClick: () => setActiveCell({ rowIndex: descriptor.rowIndex, columnIndex: descriptor.globalColIndex }), onDoubleClick: () => setEditingCell({ rowId: descriptor.rowId, columnId: col.columnId }), onContextMenu: handleCellContextMenu, style: { minHeight: '100%', cursor: 'cell', outline: 'none', position: 'relative', userSelect: 'none' }, children: [content, descriptor.isSelectionEndCell && (_jsx("div", { className: styles.fillHandle, onMouseDown: handleFillHandleMouseDown, "aria-label": "Fill handle" }))] }));
73
+ return (_jsxs("div", { className: cellClassNames, "data-row-index": descriptor.rowIndex, "data-col-index": descriptor.globalColIndex, "data-in-range": descriptor.isInRange ? 'true' : undefined, role: "button", tabIndex: descriptor.isActive ? 0 : -1, onMouseDown: (e) => handleCellMouseDown(e, descriptor.rowIndex, descriptor.globalColIndex), onClick: () => setActiveCell({ rowIndex: descriptor.rowIndex, columnIndex: descriptor.globalColIndex }), onDoubleClick: () => setEditingCell({ rowId: descriptor.rowId, columnId: col.columnId }), onContextMenu: handleCellContextMenu, style: { cursor: 'cell' }, children: [content, descriptor.isSelectionEndCell && (_jsx("div", { className: styles.fillHandle, onMouseDown: handleFillHandleMouseDown, "aria-label": "Fill handle" }))] }));
72
74
  }
73
- return (_jsx("div", { className: cellClassNames, "data-row-index": descriptor.rowIndex, "data-col-index": descriptor.globalColIndex, "data-in-range": descriptor.isInRange ? 'true' : undefined, tabIndex: descriptor.isActive ? 0 : -1, onMouseDown: (e) => handleCellMouseDown(e, descriptor.rowIndex, descriptor.globalColIndex), onClick: () => setActiveCell({ rowIndex: descriptor.rowIndex, columnIndex: descriptor.globalColIndex }), onContextMenu: handleCellContextMenu, style: { outline: 'none', userSelect: 'none' }, children: content }));
75
+ return (_jsx("div", { className: cellClassNames, "data-row-index": descriptor.rowIndex, "data-col-index": descriptor.globalColIndex, "data-in-range": descriptor.isInRange ? 'true' : undefined, tabIndex: descriptor.isActive ? 0 : -1, onMouseDown: (e) => handleCellMouseDown(e, descriptor.rowIndex, descriptor.globalColIndex), onClick: () => setActiveCell({ rowIndex: descriptor.rowIndex, columnIndex: descriptor.globalColIndex }), onContextMenu: handleCellContextMenu, children: content }));
74
76
  }, [cellDescriptorInput, pendingEditorValue, popoverAnchorEl, handleCellMouseDown, handleCellContextMenu, handleFillHandleMouseDown, setActiveCell, setEditingCell, setPendingEditorValue, setPopoverAnchorEl, commitCellEdit, cancelPopoverEdit]);
75
- return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, onMouseDown: (e) => { lastMouseShiftRef.current = e.shiftKey; }, className: `${styles.tableWrapper} ${rowSelection !== 'none' ? styles.selectableGrid : ''}`, role: "region", "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'), "aria-labelledby": ariaLabelledBy, "data-empty": showEmptyInGrid ? 'true' : undefined, "data-column-count": totalColCount, "data-freeze-rows": freezeRows != null && freezeRows >= 1 ? freezeRows : undefined, "data-freeze-cols": freezeCols != null && freezeCols >= 1 ? freezeCols : undefined, "data-overflow-x": allowOverflowX ? 'true' : 'false', "data-container-width": containerWidth, "data-min-table-width": Math.round(minTableWidth), "data-has-selection": rowSelection !== 'none' ? 'true' : undefined, onContextMenu: (e) => { e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY }); }, onKeyDown: handleGridKeyDown, style: {
77
+ return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, onMouseDown: (e) => { lastMouseShiftRef.current = e.shiftKey; }, className: `${styles.tableWrapper} ${rowSelection !== 'none' ? styles.selectableGrid : ''}`, role: "region", "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'), "aria-labelledby": ariaLabelledBy, "data-empty": showEmptyInGrid ? 'true' : undefined, "data-column-count": totalColCount, "data-freeze-rows": freezeRows != null && freezeRows >= 1 ? freezeRows : undefined, "data-freeze-cols": freezeCols != null && freezeCols >= 1 ? freezeCols : undefined, "data-overflow-x": allowOverflowX ? 'true' : 'false', "data-container-width": containerWidth, "data-min-table-width": Math.round(minTableWidth), "data-has-selection": rowSelection !== 'none' ? 'true' : undefined, onContextMenu: (e) => { e.preventDefault(); }, onKeyDown: handleGridKeyDown, style: {
76
78
  ['--data-table-column-count']: totalColCount,
77
79
  ['--data-table-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'fit-content' : fitToContent ? 'fit-content' : '100%',
78
80
  ['--data-table-min-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'max-content' : fitToContent ? 'max-content' : '100%',
79
81
  ['--data-table-total-min-width']: `${minTableWidth}px`,
80
- }, children: [_jsxs("div", { className: styles.tableScrollContent, children: [_jsxs("div", { className: isLoading && items.length > 0 ? styles.loadingOverlayContainer : undefined, children: [isLoading && items.length > 0 && (_jsx("div", { className: styles.loadingOverlay, "aria-live": "polite", children: _jsxs("div", { className: styles.loadingOverlayContent, children: [_jsx("div", { className: styles.spinner }), _jsx("span", { className: styles.loadingOverlayText, children: loadingMessage })] }) })), _jsx("div", { className: isLoading && items.length > 0 ? styles.loadingDimmed : undefined, children: _jsx("div", { className: styles.tableWidthAnchor, children: _jsxs("table", { className: styles.dataTable, children: [_jsx("thead", { className: freezeRows != null && freezeRows >= 1 ? styles.stickyHeader : undefined, children: _jsxs("tr", { children: [hasCheckboxCol && (_jsx("th", { className: styles.selectionHeaderCell, scope: "col", children: _jsx("div", { className: styles.selectionHeaderCellInner, children: _jsx(Checkbox.Root, { checked: allSelected ? true : someSelected ? 'indeterminate' : false, onCheckedChange: (c) => handleSelectAll(!!c), "aria-label": "Select all rows", children: _jsx(Checkbox.Indicator, { children: "\u2713" }) }) }) })), visibleCols.map((col, colIdx) => {
81
- const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
82
- const isPinnedLeft = col.pinned === 'left';
83
- const isPinnedRight = col.pinned === 'right';
84
- const columnWidth = getColumnWidth(col);
85
- return (_jsxs("th", { scope: "col", "data-column-id": col.columnId, className: [
86
- isFreezeCol ? styles.freezeCol : '',
87
- isFreezeCol && colIdx === 0 ? styles.freezeColFirst : '',
88
- isPinnedLeft ? styles.pinnedColLeft : '',
89
- isPinnedRight ? styles.pinnedColRight : '',
90
- ].filter(Boolean).join(' '), style: {
91
- minWidth: col.minWidth ?? 80,
92
- width: columnWidth,
93
- maxWidth: columnWidth,
94
- position: 'relative',
95
- }, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx("div", { className: styles.resizeHandle, onMouseDown: (e) => handleResizeStart(e, col), "aria-label": `Resize ${col.name}` })] }, col.columnId));
96
- })] }) }), !showEmptyInGrid && (_jsx("tbody", { children: items.map((item, rowIndex) => {
97
- const rowIdStr = getRowId(item);
98
- const isSelected = selectedRowIds.has(rowIdStr);
99
- return (_jsxs("tr", { className: isSelected ? styles.selectedRow : '', onClick: () => {
100
- if (rowSelection === 'single') {
101
- const id = getRowId(item);
102
- updateSelection(selectedRowIds.has(id) ? new Set() : new Set([id]));
103
- }
104
- }, children: [hasCheckboxCol && (_jsx("td", { className: styles.selectionCell, children: _jsx("div", { className: styles.selectionCellInner, "data-row-index": rowIndex, "data-col-index": 0, onClick: (e) => e.stopPropagation(), children: _jsx("input", { type: "checkbox", checked: selectedRowIds.has(rowIdStr), onChange: (e) => handleRowCheckboxChange(rowIdStr, e.target.checked, rowIndex, lastMouseShiftRef.current), "aria-label": `Select row ${rowIndex + 1}` }) }) })), visibleCols.map((col, colIdx) => {
82
+ }, children: [_jsxs("div", { className: styles.tableScrollContent, children: [_jsxs("div", { className: isLoading && items.length > 0 ? styles.loadingOverlayContainer : undefined, children: [isLoading && items.length > 0 && (_jsx("div", { className: styles.loadingOverlay, "aria-live": "polite", children: _jsxs("div", { className: styles.loadingOverlayContent, children: [_jsx("div", { className: styles.spinner }), _jsx("span", { className: styles.loadingOverlayText, children: loadingMessage })] }) })), _jsx("div", { className: isLoading && items.length > 0 ? styles.loadingDimmed : undefined, children: _jsxs("div", { className: styles.tableWidthAnchor, ref: tableContainerRef, children: [_jsxs("table", { className: styles.dataTable, children: [_jsx("thead", { className: freezeRows != null && freezeRows >= 1 ? styles.stickyHeader : undefined, children: _jsxs("tr", { children: [hasCheckboxCol && (_jsx("th", { className: styles.selectionHeaderCell, scope: "col", children: _jsx("div", { className: styles.selectionHeaderCellInner, children: _jsx(Checkbox.Root, { className: styles.rowCheckbox, checked: allSelected ? true : someSelected ? 'indeterminate' : false, onCheckedChange: (c) => handleSelectAll(!!c), "aria-label": "Select all rows", children: _jsx(Checkbox.Indicator, { className: styles.rowCheckboxIndicator, children: someSelected && !allSelected ? '–' : '✓' }) }) }) })), visibleCols.map((col, colIdx) => {
105
83
  const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
106
84
  const isPinnedLeft = col.pinned === 'left';
107
85
  const isPinnedRight = col.pinned === 'right';
108
86
  const columnWidth = getColumnWidth(col);
109
- return (_jsx("td", { className: [
87
+ const hasExplicitWidth = !!(columnSizingOverrides[col.columnId] || col.idealWidth != null || col.defaultWidth != null);
88
+ return (_jsxs("th", { scope: "col", "data-column-id": col.columnId, className: [
110
89
  isFreezeCol ? styles.freezeCol : '',
111
90
  isFreezeCol && colIdx === 0 ? styles.freezeColFirst : '',
112
91
  isPinnedLeft ? styles.pinnedColLeft : '',
113
92
  isPinnedRight ? styles.pinnedColRight : '',
114
93
  ].filter(Boolean).join(' '), style: {
115
94
  minWidth: col.minWidth ?? 80,
116
- width: columnWidth,
117
- maxWidth: columnWidth,
118
- }, children: renderCellContent(item, col, rowIndex, colIdx) }, col.columnId));
119
- })] }, rowIdStr));
120
- }) }))] }) }) })] }), showEmptyInGrid && emptyState && (_jsx("div", { className: styles.emptyStateInGrid, children: _jsx("div", { children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.emptyStateInGridTitle, children: "No results found" }), _jsx("div", { className: styles.emptyStateInGridMessage, children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx("button", { type: "button", className: styles.emptyStateInGridLink, onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }) }))] }), 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 })), contextMenu &&
121
- createPortal(_jsx(GridContextMenu, { x: contextMenu.x, y: contextMenu.y, hasSelection: hasCellSelection, onCopy: handleCopy, onCut: handleCut, onPaste: () => void handlePaste(), onSelectAll: handleSelectAllCells, onClose: closeContextMenu }), document.body)] }));
95
+ width: hasExplicitWidth ? columnWidth : undefined,
96
+ maxWidth: hasExplicitWidth ? columnWidth : undefined,
97
+ position: 'relative',
98
+ }, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx("div", { className: styles.resizeHandle, onMouseDown: (e) => handleResizeStart(e, col), "aria-label": `Resize ${col.name}` })] }, col.columnId));
99
+ })] }) }), !showEmptyInGrid && (_jsx("tbody", { children: items.map((item, rowIndex) => {
100
+ const rowIdStr = getRowId(item);
101
+ const isSelected = selectedRowIds.has(rowIdStr);
102
+ return (_jsxs("tr", { className: isSelected ? styles.selectedRow : '', onClick: () => {
103
+ if (rowSelection === 'single') {
104
+ const id = getRowId(item);
105
+ updateSelection(selectedRowIds.has(id) ? new Set() : new Set([id]));
106
+ }
107
+ }, children: [hasCheckboxCol && (_jsx("td", { className: styles.selectionCell, children: _jsx("div", { className: styles.selectionCellInner, "data-row-index": rowIndex, "data-col-index": 0, onClick: (e) => e.stopPropagation(), children: _jsx(Checkbox.Root, { className: styles.rowCheckbox, checked: selectedRowIds.has(rowIdStr), onCheckedChange: (c) => handleRowCheckboxChange(rowIdStr, !!c, rowIndex, lastMouseShiftRef.current), "aria-label": `Select row ${rowIndex + 1}`, children: _jsx(Checkbox.Indicator, { className: styles.rowCheckboxIndicator, children: "\u2713" }) }) }) })), visibleCols.map((col, colIdx) => {
108
+ const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
109
+ const isPinnedLeft = col.pinned === 'left';
110
+ const isPinnedRight = col.pinned === 'right';
111
+ const columnWidth = getColumnWidth(col);
112
+ const hasExplicitWidth = !!(columnSizingOverrides[col.columnId] || col.idealWidth != null || col.defaultWidth != null);
113
+ return (_jsx("td", { className: [
114
+ isFreezeCol ? styles.freezeCol : '',
115
+ isFreezeCol && colIdx === 0 ? styles.freezeColFirst : '',
116
+ isPinnedLeft ? styles.pinnedColLeft : '',
117
+ isPinnedRight ? styles.pinnedColRight : '',
118
+ ].filter(Boolean).join(' '), style: {
119
+ minWidth: col.minWidth ?? 80,
120
+ width: hasExplicitWidth ? columnWidth : undefined,
121
+ maxWidth: hasExplicitWidth ? columnWidth : undefined,
122
+ }, children: renderCellContent(item, col, rowIndex, colIdx) }, col.columnId));
123
+ })] }, rowIdStr));
124
+ }) }))] }), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset }), 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 }))] }) })] }), showEmptyInGrid && emptyState && (_jsx("div", { className: styles.emptyStateInGrid, children: _jsx("div", { children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.emptyStateInGridTitle, children: "No results found" }), _jsx("div", { className: styles.emptyStateInGridMessage, children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx("button", { type: "button", className: styles.emptyStateInGridLink, onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }) }))] }), contextMenu &&
125
+ createPortal(_jsx(GridContextMenu, { x: contextMenu.x, y: contextMenu.y, hasSelection: hasCellSelection, canUndo: canUndo, canRedo: canRedo, onUndo: onUndo ?? (() => { }), onRedo: onRedo ?? (() => { }), onCopy: handleCopy, onCut: handleCut, onPaste: () => void handlePaste(), onSelectAll: handleSelectAllCells, onClose: closeContextMenu }), document.body)] }));
122
126
  }
123
127
  export const DataGridTable = React.memo(DataGridTableInner);
@@ -1,3 +1,4 @@
1
+ @charset "UTF-8";
1
2
  .tableScrollContent {
2
3
  display: flex;
3
4
  flex-direction: column;
@@ -12,7 +13,6 @@
12
13
  width: 100%;
13
14
  min-width: 0;
14
15
  max-width: 100%;
15
- margin-bottom: 15px;
16
16
  border-radius: 6px;
17
17
  box-sizing: border-box;
18
18
  }
@@ -24,8 +24,10 @@
24
24
  }
25
25
 
26
26
  .tableWidthAnchor {
27
+ position: relative;
27
28
  width: max-content;
28
- min-width: max(100%, var(--data-table-total-min-width, 0px));
29
+ /* No min-width: 100% — anchor sizes to grid content so status bar aligns with the table border.
30
+ .tableScrollContent provides the full-width background so no gap is visible. */
29
31
  background: var(--ogrid-bg, #fff);
30
32
  }
31
33
 
@@ -37,7 +39,6 @@
37
39
  border-spacing: 0;
38
40
  border: 1px solid var(--ogrid-border, #e0e0e0);
39
41
  border-radius: 6px;
40
- overflow: hidden;
41
42
  box-sizing: border-box;
42
43
  table-layout: auto;
43
44
  }
@@ -46,17 +47,30 @@
46
47
  .dataTable td {
47
48
  min-width: 80px;
48
49
  box-sizing: border-box;
49
- padding: 6px 10px;
50
50
  border-right: 1px solid var(--ogrid-border, #e0e0e0);
51
51
  font-size: 13px;
52
52
  vertical-align: middle;
53
53
  }
54
54
 
55
+ .dataTable th {
56
+ padding: 6px 10px;
57
+ }
58
+
59
+ .dataTable td {
60
+ padding: 0;
61
+ }
62
+
55
63
  .dataTable th:last-of-type,
56
64
  .dataTable td:last-of-type {
57
65
  border-right: none;
58
66
  }
59
67
 
68
+ /* When status bar follows, remove bottom border-radius so the frame connects */
69
+ .tableWidthAnchor:has(.statusBar) .dataTable {
70
+ border-bottom-left-radius: 0;
71
+ border-bottom-right-radius: 0;
72
+ }
73
+
60
74
  .dataTable thead {
61
75
  background: var(--ogrid-bg-subtle, #f3f2f1);
62
76
  }
@@ -176,59 +190,51 @@
176
190
  }
177
191
 
178
192
  .dataTable tbody td {
179
- overflow: hidden;
180
- text-overflow: ellipsis;
181
- white-space: nowrap;
193
+ position: relative;
194
+ height: 1px;
182
195
  }
183
196
 
184
197
  .cellContent {
185
198
  width: 100%;
186
- min-height: 100%;
199
+ height: 100%;
187
200
  display: flex;
188
201
  align-items: center;
189
202
  min-width: 0;
203
+ padding: 6px 10px;
204
+ box-sizing: border-box;
205
+ overflow: hidden;
206
+ text-overflow: ellipsis;
207
+ white-space: nowrap;
190
208
  user-select: none;
209
+ outline: none;
191
210
  }
192
211
 
193
212
  .activeCellContent {
194
- outline: 2px solid var(--ogrid-primary, #0066cc);
195
- outline-offset: -2px;
196
- border-radius: 2px;
197
- z-index: 1;
213
+ outline: 2px solid var(--ogrid-selection, #217346);
214
+ outline-offset: -1px;
215
+ z-index: 2;
198
216
  position: relative;
217
+ overflow: visible;
199
218
  }
200
219
 
201
220
  .cellInRange {
202
- background: var(--ogrid-bg-range, #cce4f7) !important;
203
- box-shadow: inset 0 0 0 1px var(--ogrid-primary, #0066cc);
221
+ background: var(--ogrid-bg-range, rgba(33, 115, 70, 0.12)) !important;
204
222
  }
205
223
 
206
224
  .cellCut {
207
- outline: 2px dashed var(--ogrid-primary, #0066cc);
208
- outline-offset: -2px;
209
225
  background: var(--ogrid-bg-hover, rgba(0, 0, 0, 0.04)) !important;
210
- animation: cellCutDash 0.6s linear infinite;
226
+ opacity: 0.7;
211
227
  }
212
228
 
213
- @keyframes cellCutDash {
214
- 0% {
215
- outline-offset: -2px;
216
- }
217
- 50% {
218
- outline-offset: -3px;
219
- }
220
- 100% {
221
- outline-offset: -2px;
222
- }
223
- }
224
229
  .fillHandle {
225
230
  position: absolute;
226
- right: 2px;
227
- bottom: 2px;
228
- width: 8px;
229
- height: 8px;
230
- background: var(--ogrid-primary, #0066cc);
231
- border-radius: 2px;
231
+ right: -3px;
232
+ bottom: -3px;
233
+ width: 7px;
234
+ height: 7px;
235
+ background: var(--ogrid-selection, #217346);
236
+ border: 1px solid #fff;
237
+ border-radius: 1px;
232
238
  cursor: crosshair;
233
239
  pointer-events: auto;
234
240
  z-index: 3;
@@ -251,12 +257,40 @@
251
257
  width: 100%;
252
258
  }
253
259
 
260
+ .rowCheckbox {
261
+ width: 18px;
262
+ height: 18px;
263
+ border: 1.5px solid var(--ogrid-border-strong, #888);
264
+ border-radius: 3px;
265
+ background: var(--ogrid-bg, #fff);
266
+ cursor: pointer;
267
+ display: inline-flex;
268
+ align-items: center;
269
+ justify-content: center;
270
+ flex-shrink: 0;
271
+ padding: 0;
272
+ }
273
+ .rowCheckbox[data-state=checked], .rowCheckbox[data-state=indeterminate] {
274
+ background: var(--ogrid-primary, #0066cc);
275
+ border-color: var(--ogrid-primary, #0066cc);
276
+ }
277
+
278
+ .rowCheckboxIndicator {
279
+ color: var(--ogrid-primary-fg, #fff);
280
+ font-size: 13px;
281
+ line-height: 1;
282
+ display: flex;
283
+ align-items: center;
284
+ justify-content: center;
285
+ }
286
+
254
287
  .statusBar {
255
288
  display: flex;
256
289
  align-items: center;
257
290
  gap: 16px;
258
291
  width: 100%;
259
292
  padding: 6px 12px;
293
+ box-sizing: border-box;
260
294
  font-size: 12px;
261
295
  color: var(--ogrid-muted, #616161);
262
296
  background: var(--ogrid-bg-subtle, #f3f2f1);
@@ -300,7 +334,10 @@
300
334
  }
301
335
 
302
336
  .contextMenuItem {
303
- display: block;
337
+ display: flex;
338
+ align-items: center;
339
+ justify-content: space-between;
340
+ gap: 24px;
304
341
  width: 100%;
305
342
  padding: 6px 12px;
306
343
  border: none;
@@ -318,6 +355,15 @@
318
355
  cursor: not-allowed;
319
356
  }
320
357
 
358
+ .contextMenuItemLabel {
359
+ flex: 1;
360
+ }
361
+
362
+ .contextMenuItemShortcut {
363
+ color: var(--ogrid-fg-muted, rgba(0, 0, 0, 0.4));
364
+ font-size: 0.85em;
365
+ }
366
+
321
367
  .contextMenuDivider {
322
368
  height: 1px;
323
369
  margin: 4px 0;
@@ -1,11 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
- import { GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers } from '@alaarab/ogrid-core';
3
+ import { GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut } from '@alaarab/ogrid-core';
4
4
  import styles from './DataGridTable.module.css';
5
5
  export function GridContextMenu(props) {
6
- const { x, y, hasSelection, onClose } = props;
6
+ const { x, y, hasSelection, canUndo, canRedo, onClose } = props;
7
7
  const ref = React.useRef(null);
8
8
  const handlers = React.useMemo(() => getContextMenuHandlers(props), [props]);
9
+ const isDisabled = React.useCallback((item) => {
10
+ if (item.disabledWhenNoSelection && !hasSelection)
11
+ return true;
12
+ if (item.id === 'undo' && !canUndo)
13
+ return true;
14
+ if (item.id === 'redo' && !canRedo)
15
+ return true;
16
+ return false;
17
+ }, [hasSelection, canUndo, canRedo]);
9
18
  React.useEffect(() => {
10
19
  const handleClickOutside = (e) => {
11
20
  if (ref.current && !ref.current.contains(e.target))
@@ -22,5 +31,5 @@ export function GridContextMenu(props) {
22
31
  document.removeEventListener('keydown', handleKeyDown, true);
23
32
  };
24
33
  }, [onClose]);
25
- return (_jsx("div", { ref: ref, className: styles.contextMenu, role: "menu", style: { left: x, top: y }, "aria-label": "Grid context menu", children: GRID_CONTEXT_MENU_ITEMS.map((item) => (_jsxs(React.Fragment, { children: [item.id === 'selectAll' && _jsx("div", { className: styles.contextMenuDivider }), _jsx("button", { type: "button", className: styles.contextMenuItem, onClick: handlers[item.id], disabled: item.disabledWhenNoSelection ? !hasSelection : false, children: item.label })] }, item.id))) }));
34
+ return (_jsx("div", { ref: ref, className: styles.contextMenu, role: "menu", style: { left: x, top: y }, "aria-label": "Grid context menu", children: GRID_CONTEXT_MENU_ITEMS.map((item) => (_jsxs(React.Fragment, { children: [item.dividerBefore && _jsx("div", { className: styles.contextMenuDivider }), _jsxs("button", { type: "button", className: styles.contextMenuItem, onClick: handlers[item.id], disabled: isDisabled(item), children: [_jsx("span", { className: styles.contextMenuItemLabel, children: item.label }), item.shortcut && (_jsx("span", { className: styles.contextMenuItemShortcut, children: formatShortcut(item.shortcut) }))] })] }, item.id))) }));
26
35
  }
@@ -4,5 +4,7 @@ export interface GridContextMenuProps extends GridContextMenuHandlerProps {
4
4
  x: number;
5
5
  y: number;
6
6
  hasSelection: boolean;
7
+ canUndo: boolean;
8
+ canRedo: boolean;
7
9
  }
8
10
  export declare function GridContextMenu(props: GridContextMenuProps): React.ReactElement;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "OGrid default (Radix) – Data grid with sorting, filtering, pagination, column chooser, and CSV export. Packed with Radix UI; no Fluent or Material required.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",