@alaarab/ogrid 1.2.2 → 1.3.0

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,50 +64,27 @@ 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
+ return (_jsxs("th", { scope: "col", "data-column-id": col.columnId, className: [
110
88
  isFreezeCol ? styles.freezeCol : '',
111
89
  isFreezeCol && colIdx === 0 ? styles.freezeColFirst : '',
112
90
  isPinnedLeft ? styles.pinnedColLeft : '',
@@ -115,9 +93,33 @@ function DataGridTableInner(props) {
115
93
  minWidth: col.minWidth ?? 80,
116
94
  width: columnWidth,
117
95
  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)] }));
96
+ position: 'relative',
97
+ }, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx("div", { className: styles.resizeHandle, onMouseDown: (e) => handleResizeStart(e, col), "aria-label": `Resize ${col.name}` })] }, col.columnId));
98
+ })] }) }), !showEmptyInGrid && (_jsx("tbody", { children: items.map((item, rowIndex) => {
99
+ const rowIdStr = getRowId(item);
100
+ const isSelected = selectedRowIds.has(rowIdStr);
101
+ return (_jsxs("tr", { className: isSelected ? styles.selectedRow : '', onClick: () => {
102
+ if (rowSelection === 'single') {
103
+ const id = getRowId(item);
104
+ updateSelection(selectedRowIds.has(id) ? new Set() : new Set([id]));
105
+ }
106
+ }, 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) => {
107
+ const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
108
+ const isPinnedLeft = col.pinned === 'left';
109
+ const isPinnedRight = col.pinned === 'right';
110
+ const columnWidth = getColumnWidth(col);
111
+ return (_jsx("td", { className: [
112
+ isFreezeCol ? styles.freezeCol : '',
113
+ isFreezeCol && colIdx === 0 ? styles.freezeColFirst : '',
114
+ isPinnedLeft ? styles.pinnedColLeft : '',
115
+ isPinnedRight ? styles.pinnedColRight : '',
116
+ ].filter(Boolean).join(' '), style: {
117
+ minWidth: col.minWidth ?? 80,
118
+ width: columnWidth,
119
+ maxWidth: columnWidth,
120
+ }, children: renderCellContent(item, col, rowIndex, colIdx) }, col.columnId));
121
+ })] }, rowIdStr));
122
+ }) }))] }), _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 &&
123
+ 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
124
  }
123
125
  export const DataGridTable = React.memo(DataGridTableInner);
@@ -24,6 +24,7 @@
24
24
  }
25
25
 
26
26
  .tableWidthAnchor {
27
+ position: relative;
27
28
  width: max-content;
28
29
  min-width: max(100%, var(--data-table-total-min-width, 0px));
29
30
  background: var(--ogrid-bg, #fff);
@@ -37,7 +38,6 @@
37
38
  border-spacing: 0;
38
39
  border: 1px solid var(--ogrid-border, #e0e0e0);
39
40
  border-radius: 6px;
40
- overflow: hidden;
41
41
  box-sizing: border-box;
42
42
  table-layout: auto;
43
43
  }
@@ -46,17 +46,30 @@
46
46
  .dataTable td {
47
47
  min-width: 80px;
48
48
  box-sizing: border-box;
49
- padding: 6px 10px;
50
49
  border-right: 1px solid var(--ogrid-border, #e0e0e0);
51
50
  font-size: 13px;
52
51
  vertical-align: middle;
53
52
  }
54
53
 
54
+ .dataTable th {
55
+ padding: 6px 10px;
56
+ }
57
+
58
+ .dataTable td {
59
+ padding: 0;
60
+ }
61
+
55
62
  .dataTable th:last-of-type,
56
63
  .dataTable td:last-of-type {
57
64
  border-right: none;
58
65
  }
59
66
 
67
+ /* When status bar follows, remove bottom border-radius so the frame connects */
68
+ .tableWidthAnchor:has(.statusBar) .dataTable {
69
+ border-bottom-left-radius: 0;
70
+ border-bottom-right-radius: 0;
71
+ }
72
+
60
73
  .dataTable thead {
61
74
  background: var(--ogrid-bg-subtle, #f3f2f1);
62
75
  }
@@ -176,59 +189,51 @@
176
189
  }
177
190
 
178
191
  .dataTable tbody td {
179
- overflow: hidden;
180
- text-overflow: ellipsis;
181
- white-space: nowrap;
192
+ position: relative;
193
+ height: 1px;
182
194
  }
183
195
 
184
196
  .cellContent {
185
197
  width: 100%;
186
- min-height: 100%;
198
+ height: 100%;
187
199
  display: flex;
188
200
  align-items: center;
189
201
  min-width: 0;
202
+ padding: 6px 10px;
203
+ box-sizing: border-box;
204
+ overflow: hidden;
205
+ text-overflow: ellipsis;
206
+ white-space: nowrap;
190
207
  user-select: none;
208
+ outline: none;
191
209
  }
192
210
 
193
211
  .activeCellContent {
194
- outline: 2px solid var(--ogrid-primary, #0066cc);
195
- outline-offset: -2px;
196
- border-radius: 2px;
197
- z-index: 1;
212
+ outline: 2px solid var(--ogrid-selection, #217346);
213
+ outline-offset: -1px;
214
+ z-index: 2;
198
215
  position: relative;
216
+ overflow: visible;
199
217
  }
200
218
 
201
219
  .cellInRange {
202
- background: var(--ogrid-bg-range, #cce4f7) !important;
203
- box-shadow: inset 0 0 0 1px var(--ogrid-primary, #0066cc);
220
+ background: var(--ogrid-bg-range, rgba(33, 115, 70, 0.12)) !important;
204
221
  }
205
222
 
206
223
  .cellCut {
207
- outline: 2px dashed var(--ogrid-primary, #0066cc);
208
- outline-offset: -2px;
209
224
  background: var(--ogrid-bg-hover, rgba(0, 0, 0, 0.04)) !important;
210
- animation: cellCutDash 0.6s linear infinite;
225
+ opacity: 0.7;
211
226
  }
212
227
 
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
228
  .fillHandle {
225
229
  position: absolute;
226
- right: 2px;
227
- bottom: 2px;
228
- width: 8px;
229
- height: 8px;
230
- background: var(--ogrid-primary, #0066cc);
231
- border-radius: 2px;
230
+ right: -3px;
231
+ bottom: -3px;
232
+ width: 7px;
233
+ height: 7px;
234
+ background: var(--ogrid-selection, #217346);
235
+ border: 1px solid #fff;
236
+ border-radius: 1px;
232
237
  cursor: crosshair;
233
238
  pointer-events: auto;
234
239
  z-index: 3;
@@ -251,6 +256,33 @@
251
256
  width: 100%;
252
257
  }
253
258
 
259
+ .rowCheckbox {
260
+ width: 18px;
261
+ height: 18px;
262
+ border: 1.5px solid var(--ogrid-border-strong, #888);
263
+ border-radius: 3px;
264
+ background: var(--ogrid-bg, #fff);
265
+ cursor: pointer;
266
+ display: inline-flex;
267
+ align-items: center;
268
+ justify-content: center;
269
+ flex-shrink: 0;
270
+ padding: 0;
271
+ }
272
+ .rowCheckbox[data-state=checked], .rowCheckbox[data-state=indeterminate] {
273
+ background: var(--ogrid-primary, #0066cc);
274
+ border-color: var(--ogrid-primary, #0066cc);
275
+ }
276
+
277
+ .rowCheckboxIndicator {
278
+ color: var(--ogrid-primary-fg, #fff);
279
+ font-size: 13px;
280
+ line-height: 1;
281
+ display: flex;
282
+ align-items: center;
283
+ justify-content: center;
284
+ }
285
+
254
286
  .statusBar {
255
287
  display: flex;
256
288
  align-items: center;
@@ -300,7 +332,10 @@
300
332
  }
301
333
 
302
334
  .contextMenuItem {
303
- display: block;
335
+ display: flex;
336
+ align-items: center;
337
+ justify-content: space-between;
338
+ gap: 24px;
304
339
  width: 100%;
305
340
  padding: 6px 12px;
306
341
  border: none;
@@ -318,6 +353,15 @@
318
353
  cursor: not-allowed;
319
354
  }
320
355
 
356
+ .contextMenuItemLabel {
357
+ flex: 1;
358
+ }
359
+
360
+ .contextMenuItemShortcut {
361
+ color: var(--ogrid-fg-muted, rgba(0, 0, 0, 0.4));
362
+ font-size: 0.85em;
363
+ }
364
+
321
365
  .contextMenuDivider {
322
366
  height: 1px;
323
367
  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.0",
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",