@alaarab/ogrid 1.8.2 → 1.9.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.
package/README.md CHANGED
@@ -1,6 +1,40 @@
1
- # @alaarab/ogrid
1
+ <p align="center">
2
+ <strong>OGrid</strong> — The lightweight React data grid with enterprise features and zero enterprise cost.
3
+ </p>
2
4
 
3
- [OGrid](https://github.com/alaarab/ogrid) data table with [Radix UI](https://www.radix-ui.com/) primitives and no Fluent/Material dependency. Sort, filter (text, multi-select, people), paginate, show/hide columns, spreadsheet-style selection (cell range, copy/paste, context menu), row selection, status bar, and CSV export. Use an in-memory array or plug in your own API.
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/@alaarab/ogrid"><img src="https://img.shields.io/npm/v/@alaarab/ogrid?color=%23217346&label=npm" alt="npm version" /></a>
7
+ <a href="https://github.com/alaarab/ogrid/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License" /></a>
8
+ <img src="https://img.shields.io/badge/React-17%20%7C%2018%20%7C%2019-blue" alt="React 17, 18, 19" />
9
+ <img src="https://img.shields.io/badge/TypeScript-strict-blue" alt="TypeScript strict" />
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://alaarab.github.io/ogrid/">Documentation</a> · <a href="https://alaarab.github.io/ogrid/docs/getting-started/overview">Getting Started</a> · <a href="https://alaarab.github.io/ogrid/docs/api/ogrid-props">API Reference</a>
14
+ </p>
15
+
16
+ ---
17
+
18
+ This is the **default OGrid package** built with Radix UI primitives — lightweight, no Fluent/Material dependency. Also available for [Fluent UI](https://www.npmjs.com/package/@alaarab/ogrid-fluent) and [Material UI](https://www.npmjs.com/package/@alaarab/ogrid-material). Same API, just swap the import.
19
+
20
+ ## Why OGrid?
21
+
22
+ | | OGrid | AG Grid Community | AG Grid Enterprise |
23
+ |---|---|---|---|
24
+ | Spreadsheet selection | Built-in | - | $999/dev/year |
25
+ | Clipboard (copy/paste) | Built-in | - | $999/dev/year |
26
+ | Fill handle | Built-in | - | $999/dev/year |
27
+ | Undo/redo | Built-in | - | $999/dev/year |
28
+ | Context menu | Built-in | - | $999/dev/year |
29
+ | Status bar | Built-in | - | $999/dev/year |
30
+ | Side bar | Built-in | - | $999/dev/year |
31
+ | Cell editing | Built-in | Built-in | Built-in |
32
+ | Sorting & filtering | Built-in | Built-in | Built-in |
33
+ | **License** | **MIT (free)** | MIT | Commercial |
34
+
35
+ ## Features
36
+
37
+ Sorting · Filtering (text, multi-select, date range, people picker) · Pagination · Cell editing (inline, select, checkbox, rich select, date, custom popover) · Spreadsheet selection · Clipboard · Fill handle · Undo/redo · Row selection · Column groups · Column pinning · Column resize · Column chooser · Side bar · Context menu · Status bar with aggregations · CSV export · Grid API · Server-side data · Column state persistence · Keyboard navigation (Excel-style Ctrl+Arrow) · Built-in column types (text, numeric, date, boolean) · React 17/18/19 · TypeScript strict
4
38
 
5
39
  ## Install
6
40
 
@@ -8,49 +42,37 @@
8
42
  npm install @alaarab/ogrid
9
43
  ```
10
44
 
11
- ### Peer Dependencies
12
-
13
- ```
14
- react ^17.0.0 || ^18.0.0 || ^19.0.0
15
- react-dom ^17.0.0 || ^18.0.0 || ^19.0.0
16
- ```
17
-
18
- Radix UI primitives (`@radix-ui/react-checkbox`, `@radix-ui/react-popover`) are bundled as regular dependencies.
45
+ Radix UI primitives are bundled as regular dependencies — only `react` and `react-dom` are peer deps.
19
46
 
20
47
  ## Quick Start
21
48
 
22
49
  ```tsx
23
50
  import { OGrid, type IColumnDef } from '@alaarab/ogrid';
24
51
 
25
- const columns: IColumnDef<Product>[] = [
26
- { columnId: 'name', name: 'Name', sortable: true, filterable: { type: 'text' }, renderCell: (item) => <span>{item.name}</span> },
27
- { columnId: 'category', name: 'Category', sortable: true, filterable: { type: 'multiSelect', filterField: 'category' }, renderCell: (item) => <span>{item.category}</span> },
52
+ const columns: IColumnDef<Employee>[] = [
53
+ { columnId: 'name', name: 'Name', sortable: true, editable: true },
54
+ { columnId: 'department', name: 'Department',
55
+ filterable: { type: 'multiSelect' },
56
+ cellEditor: 'richSelect', cellEditorParams: { values: ['Engineering', 'Sales', 'Marketing'] } },
57
+ { columnId: 'salary', name: 'Salary', type: 'numeric', editable: true,
58
+ valueFormatter: (v) => `$${Number(v).toLocaleString()}` },
28
59
  ];
29
60
 
30
- <OGrid<Product>
31
- data={products}
61
+ <OGrid
32
62
  columns={columns}
33
- getRowId={(r) => r.id}
34
- entityLabelPlural="products"
63
+ data={employees}
64
+ getRowId={(e) => e.id}
65
+ editable
66
+ cellSelection
67
+ statusBar
68
+ sideBar
35
69
  />
36
70
  ```
37
71
 
38
- ## Components
39
-
40
- - **`OGrid<T>`** -- Full table with column chooser, filters, and pagination (Radix/native implementation)
41
- - **`DataGridTable<T>`** -- Lower-level grid for custom state management
42
- - **`ColumnChooser`** -- Column visibility dropdown
43
- - **`PaginationControls`** -- Pagination UI
44
- - **`ColumnHeaderFilter`** -- Column header with sort/filter (used internally)
72
+ ## Documentation
45
73
 
46
- All core types, hooks, and utilities are re-exported from `@alaarab/ogrid-core`.
47
-
48
- ## Storybook
49
-
50
- ```bash
51
- npm run storybook
52
- ```
74
+ Full docs, API reference, and interactive examples at **[alaarab.github.io/ogrid](https://alaarab.github.io/ogrid/)**.
53
75
 
54
76
  ## License
55
77
 
56
- MIT
78
+ MIT — Free forever. No enterprise tiers. No feature paywalls.
@@ -8,70 +8,19 @@ 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, isRowInRange, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from '@alaarab/ogrid-core';
11
+ import { useDataGridState, useColumnResize, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-core';
12
12
  import styles from './DataGridTable.module.css';
13
13
  // Module-scope stable constants (avoid per-render allocations)
14
14
  const GRID_ROOT_STYLE = { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' };
15
15
  const CURSOR_CELL_STYLE = { cursor: 'cell' };
16
+ const POPOVER_ANCHOR_STYLE = { minHeight: '100%', minWidth: 40 };
16
17
  const STOP_PROPAGATION = (e) => e.stopPropagation();
17
18
  const PREVENT_DEFAULT = (e) => { e.preventDefault(); };
19
+ const NOOP = () => { };
18
20
  function GridRowInner(props) {
19
21
  const { item, rowIndex, rowId, isSelected, visibleCols, columnMeta, renderCellContent, handleSingleRowClick, handleRowCheckboxChange, lastMouseShiftRef, hasCheckboxCol, } = props;
20
22
  return (_jsxs("tr", { className: isSelected ? styles.selectedRow : '', "data-row-id": rowId, onClick: handleSingleRowClick, children: [hasCheckboxCol && (_jsx("td", { className: styles.selectionCell, children: _jsx("div", { className: styles.selectionCellInner, "data-row-index": rowIndex, "data-col-index": 0, onClick: STOP_PROPAGATION, children: _jsx(Checkbox.Root, { className: styles.rowCheckbox, checked: isSelected, onCheckedChange: (c) => handleRowCheckboxChange(rowId, !!c, rowIndex, lastMouseShiftRef.current), "aria-label": `Select row ${rowIndex + 1}`, children: _jsx(Checkbox.Indicator, { className: styles.rowCheckboxIndicator, children: "\u2713" }) }) }) })), visibleCols.map((col, colIdx) => (_jsx("td", { className: columnMeta.cellClasses[col.columnId] || undefined, style: columnMeta.cellStyles[col.columnId], children: renderCellContent(item, col, rowIndex, colIdx) }, col.columnId)))] }));
21
23
  }
22
- function areGridRowPropsEqual(prev, next) {
23
- // Data / structure changes — always re-render
24
- if (prev.item !== next.item)
25
- return false;
26
- if (prev.isSelected !== next.isSelected)
27
- return false;
28
- if (prev.visibleCols !== next.visibleCols)
29
- return false;
30
- if (prev.columnMeta !== next.columnMeta)
31
- return false;
32
- if (prev.hasCheckboxCol !== next.hasCheckboxCol)
33
- return false;
34
- const ri = prev.rowIndex;
35
- // Editing cell in this row?
36
- if (prev.editingRowId !== next.editingRowId) {
37
- if (prev.editingRowId === prev.rowId || next.editingRowId === next.rowId)
38
- return false;
39
- }
40
- // Active cell in this row?
41
- const prevActive = prev.activeCell?.rowIndex === ri;
42
- const nextActive = next.activeCell?.rowIndex === ri;
43
- if (prevActive !== nextActive)
44
- return false;
45
- if (prevActive && nextActive && prev.activeCell.columnIndex !== next.activeCell.columnIndex)
46
- return false;
47
- // Selection range touches this row?
48
- const prevInSel = isRowInRange(prev.selectionRange, ri);
49
- const nextInSel = isRowInRange(next.selectionRange, ri);
50
- if (prevInSel !== nextInSel)
51
- return false;
52
- if (prevInSel && nextInSel) {
53
- if (prev.selectionRange.startCol !== next.selectionRange.startCol ||
54
- prev.selectionRange.endCol !== next.selectionRange.endCol)
55
- return false;
56
- }
57
- // Fill handle (selection end row) + isDragging
58
- const prevIsEnd = prev.selectionRange?.endRow === ri;
59
- const nextIsEnd = next.selectionRange?.endRow === ri;
60
- if (prevIsEnd !== nextIsEnd)
61
- return false;
62
- if ((prevIsEnd || nextIsEnd) && prev.isDragging !== next.isDragging)
63
- return false;
64
- // Cut/copy ranges touch this row?
65
- if (prev.cutRange !== next.cutRange) {
66
- if (isRowInRange(prev.cutRange, ri) || isRowInRange(next.cutRange, ri))
67
- return false;
68
- }
69
- if (prev.copyRange !== next.copyRange) {
70
- if (isRowInRange(prev.copyRange, ri) || isRowInRange(next.copyRange, ri))
71
- return false;
72
- }
73
- return true;
74
- }
75
24
  const GridRow = React.memo(GridRowInner, areGridRowPropsEqual);
76
25
  function DataGridTableInner(props) {
77
26
  const wrapperRef = useRef(null);
@@ -83,8 +32,9 @@ function DataGridTableInner(props) {
83
32
  const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
84
33
  const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
85
34
  const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging } = interaction;
35
+ const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
86
36
  const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
87
- const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid } = viewModels;
37
+ const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
88
38
  const { items, columns, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, visibleColumns, } = props;
89
39
  // Memoize header rows (recursive tree traversal — avoid recomputing every render)
90
40
  const headerRows = useMemo(() => buildHeaderRows(columns, visibleColumns), [columns, visibleColumns]);
@@ -98,12 +48,9 @@ function DataGridTableInner(props) {
98
48
  const interactionHandlers = useMemo(() => ({ handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu }), [handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu]);
99
49
  // Refs for volatile state — lets renderCellContent be stable (same function ref across
100
50
  // selection changes) so that GridRow's React.memo comparator can skip unaffected rows.
101
- const cellDescriptorInputRef = useRef(cellDescriptorInput);
102
- cellDescriptorInputRef.current = cellDescriptorInput;
103
- const pendingEditorValueRef = useRef(pendingEditorValue);
104
- pendingEditorValueRef.current = pendingEditorValue;
105
- const popoverAnchorElRef = useRef(popoverAnchorEl);
106
- popoverAnchorElRef.current = popoverAnchorEl;
51
+ const cellDescriptorInputRef = useLatestRef(cellDescriptorInput);
52
+ const pendingEditorValueRef = useLatestRef(pendingEditorValue);
53
+ const popoverAnchorElRef = useLatestRef(popoverAnchorEl);
107
54
  // Pre-compute column styles and classNames (avoids per-cell object creation in the row loop)
108
55
  const columnMeta = useMemo(() => {
109
56
  const cellStyles = {};
@@ -118,13 +65,13 @@ function DataGridTableInner(props) {
118
65
  const isPinnedLeft = col.pinned === 'left';
119
66
  const isPinnedRight = col.pinned === 'right';
120
67
  cellStyles[col.columnId] = {
121
- minWidth: col.minWidth ?? 80,
68
+ minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
122
69
  width: hasExplicitWidth ? columnWidth : undefined,
123
70
  maxWidth: hasExplicitWidth ? columnWidth : undefined,
124
71
  textAlign: col.type === 'numeric' ? 'right' : col.type === 'boolean' ? 'center' : undefined,
125
72
  };
126
73
  hdrStyles[col.columnId] = {
127
- minWidth: col.minWidth ?? 80,
74
+ minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
128
75
  width: hasExplicitWidth ? columnWidth : undefined,
129
76
  maxWidth: hasExplicitWidth ? columnWidth : undefined,
130
77
  };
@@ -144,8 +91,7 @@ function DataGridTableInner(props) {
144
91
  return { cellStyles, cellClasses, hdrStyles, hdrClasses };
145
92
  }, [visibleCols, getColumnWidth, columnSizingOverrides, freezeCols]);
146
93
  // Stable row-click handler (avoids creating a new arrow function per row)
147
- const selectedRowIdsRef = useRef(selectedRowIds);
148
- selectedRowIdsRef.current = selectedRowIds;
94
+ const selectedRowIdsRef = useLatestRef(selectedRowIds);
149
95
  const handleSingleRowClick = useCallback((e) => {
150
96
  if (rowSelection !== 'single')
151
97
  return;
@@ -161,28 +107,27 @@ function DataGridTableInner(props) {
161
107
  // GridRow's React.memo comparator can skip rows whose selection state hasn't changed.
162
108
  const renderCellContent = useCallback((item, col, rowIndex, colIdx) => {
163
109
  const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInputRef.current);
110
+ const rowId = getRowId(item);
111
+ let content;
164
112
  if (descriptor.mode === 'editing-inline') {
165
- return _jsx(InlineCellEditor, { ...buildInlineEditorProps(item, col, descriptor, editCallbacks) });
113
+ content = _jsx(InlineCellEditor, { ...buildInlineEditorProps(item, col, descriptor, editCallbacks) });
166
114
  }
167
- if (descriptor.mode === 'editing-popover' && typeof col.cellEditor === 'function') {
115
+ else if (descriptor.mode === 'editing-popover' && typeof col.cellEditor === 'function') {
168
116
  const editorProps = buildPopoverEditorProps(item, col, descriptor, pendingEditorValueRef.current, editCallbacks);
169
117
  const CustomEditor = col.cellEditor;
170
- return (_jsxs(Popover.Root, { open: !!popoverAnchorElRef.current, onOpenChange: (open) => { if (!open)
171
- cancelPopoverEdit(); }, children: [_jsx(Popover.Anchor, { asChild: true, children: _jsx("div", { ref: (el) => el && setPopoverAnchorEl(el), style: { minHeight: '100%', minWidth: 40 }, "aria-hidden": true }) }), _jsx(Popover.Portal, { children: _jsx(Popover.Content, { sideOffset: 4, onOpenAutoFocus: (e) => e.preventDefault(), children: _jsx(CustomEditor, { ...editorProps }) }) })] }));
118
+ content = (_jsxs(Popover.Root, { open: !!popoverAnchorElRef.current, onOpenChange: (open) => { if (!open)
119
+ cancelPopoverEdit(); }, children: [_jsx(Popover.Anchor, { asChild: true, children: _jsx("div", { ref: (el) => el && setPopoverAnchorEl(el), style: POPOVER_ANCHOR_STYLE, "aria-hidden": true }) }), _jsx(Popover.Portal, { children: _jsx(Popover.Content, { sideOffset: 4, onOpenAutoFocus: (e) => e.preventDefault(), children: _jsx(CustomEditor, { ...editorProps }) }) })] }));
120
+ }
121
+ else {
122
+ const displayContent = resolveCellDisplayContent(col, item, descriptor.displayValue);
123
+ const cellStyle = resolveCellStyle(col, item);
124
+ const styledContent = cellStyle ? _jsx("span", { style: cellStyle, children: displayContent }) : displayContent;
125
+ const cellClassNames = `${styles.cellContent}${descriptor.isActive && !descriptor.isInRange ? ` ${styles.activeCellContent}` : ''}${descriptor.isInRange ? ` ${styles.cellInRange}` : ''}${descriptor.isInCutRange ? ` ${styles.cellCut}` : ''}${descriptor.isInCopyRange ? ` ${styles.cellCopied}` : ''}`;
126
+ const interactionProps = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
127
+ content = (_jsxs("div", { className: cellClassNames, ...interactionProps, style: descriptor.canEditAny ? CURSOR_CELL_STYLE : undefined, children: [styledContent, descriptor.canEditAny && descriptor.isSelectionEndCell && (_jsx("div", { className: styles.fillHandle, onMouseDown: handleFillHandleMouseDown, "aria-label": "Fill handle" }))] }));
172
128
  }
173
- const content = resolveCellDisplayContent(col, item, descriptor.displayValue);
174
- const cellStyle = resolveCellStyle(col, item);
175
- const styledContent = cellStyle ? _jsx("span", { style: cellStyle, children: content }) : content;
176
- const cellClassNames = [
177
- styles.cellContent,
178
- descriptor.isActive && !descriptor.isInRange ? styles.activeCellContent : '',
179
- descriptor.isInRange ? styles.cellInRange : '',
180
- descriptor.isInCutRange ? styles.cellCut : '',
181
- descriptor.isInCopyRange ? styles.cellCopied : '',
182
- ].filter(Boolean).join(' ');
183
- const interactionProps = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
184
- return (_jsxs("div", { className: cellClassNames, ...interactionProps, style: descriptor.canEditAny ? CURSOR_CELL_STYLE : undefined, children: [styledContent, descriptor.canEditAny && descriptor.isSelectionEndCell && (_jsx("div", { className: styles.fillHandle, onMouseDown: handleFillHandleMouseDown, "aria-label": "Fill handle" }))] }));
185
- }, [editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit]);
129
+ return (_jsx(CellErrorBoundary, { onError: onCellError, children: content }, `${rowId}-${col.columnId}`));
130
+ }, [editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit, getRowId, onCellError]);
186
131
  return (_jsxs("div", { style: GRID_ROOT_STYLE, children: [_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: PREVENT_DEFAULT, onKeyDown: handleGridKeyDown, style: {
187
132
  ['--data-table-column-count']: totalColCount,
188
133
  ['--data-table-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'fit-content' : fitToContent ? 'fit-content' : '100%',
@@ -202,6 +147,6 @@ function DataGridTableInner(props) {
202
147
  const rowIdStr = getRowId(item);
203
148
  return (_jsx(GridRow, { item: item, rowIndex: rowIndex, rowId: rowIdStr, isSelected: selectedRowIds.has(rowIdStr), visibleCols: visibleCols, columnMeta: columnMeta, renderCellContent: renderCellContent, handleSingleRowClick: handleSingleRowClick, handleRowCheckboxChange: handleRowCheckboxChange, lastMouseShiftRef: lastMouseShiftRef, hasCheckboxCol: hasCheckboxCol, selectionRange: selectionRange, activeCell: interaction.activeCell, cutRange: cutRange, copyRange: copyRange, isDragging: isDragging, editingRowId: editingCell?.rowId ?? null }, rowIdStr));
204
149
  }) }))] }), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset }), 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.') })] })) }) }))] }) }) }), menuPosition &&
205
- createPortal(_jsx(GridContextMenu, { x: menuPosition.x, y: menuPosition.y, hasSelection: hasCellSelection, canUndo: canUndo, canRedo: canRedo, onUndo: onUndo ?? (() => { }), onRedo: onRedo ?? (() => { }), onCopy: handleCopy, onCut: handleCut, onPaste: () => void handlePaste(), onSelectAll: handleSelectAllCells, onClose: closeContextMenu }), document.body)] }), 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 && 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 })] }) }))] }));
150
+ 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)] }), 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 && 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 })] }) }))] }));
206
151
  }
207
152
  export const DataGridTable = React.memo(DataGridTableInner);
@@ -266,7 +266,7 @@
266
266
  width: 7px;
267
267
  height: 7px;
268
268
  background: var(--ogrid-selection, #217346);
269
- border: 1px solid #fff;
269
+ border: 1px solid var(--ogrid-bg, #fff);
270
270
  border-radius: 1px;
271
271
  cursor: crosshair;
272
272
  pointer-events: auto;
@@ -1,85 +1,10 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import * as React from 'react';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
3
2
  import * as Checkbox from '@radix-ui/react-checkbox';
4
- import { useInlineCellEditorState, useRichSelectState } from '@alaarab/ogrid-core';
5
- // Match .cellContent layout so column width doesn't shift during editing
6
- const editorWrapperStyle = {
7
- width: '100%',
8
- height: '100%',
9
- display: 'flex',
10
- alignItems: 'center',
11
- padding: '6px 10px',
12
- boxSizing: 'border-box',
13
- overflow: 'hidden',
14
- minWidth: 0,
15
- };
16
- const editorInputStyle = {
17
- width: '100%',
18
- padding: 0,
19
- border: 'none',
20
- background: 'transparent',
21
- color: 'inherit',
22
- font: 'inherit',
23
- fontSize: '13px',
24
- outline: 'none',
25
- minWidth: 0,
26
- };
3
+ import { BaseInlineCellEditor, selectEditorStyle } from '@alaarab/ogrid-core';
4
+ const selectWrapperStyle = { width: '100%', height: '100%', display: 'flex', alignItems: 'center', padding: '6px 10px', boxSizing: 'border-box', overflow: 'hidden', minWidth: 0 };
27
5
  export function InlineCellEditor(props) {
28
- const { value, column, editorType, onCommit, onCancel } = props;
29
- const wrapperRef = React.useRef(null);
30
- const { localValue, setLocalValue, handleKeyDown, handleBlur, commit, cancel } = useInlineCellEditorState({ value, editorType, onCommit, onCancel });
31
- const richSelectValues = column.cellEditorParams?.values ?? [];
32
- const richSelectFormatValue = column.cellEditorParams?.formatValue;
33
- const richSelect = useRichSelectState({
34
- values: richSelectValues,
35
- formatValue: richSelectFormatValue,
36
- initialValue: value,
37
- onCommit,
38
- onCancel,
39
- });
40
- React.useEffect(() => {
41
- const input = wrapperRef.current?.querySelector('input');
42
- input?.focus();
43
- }, []);
44
- if (editorType === 'richSelect') {
45
- return (_jsxs("div", { ref: wrapperRef, style: { ...editorWrapperStyle, position: 'relative' }, children: [_jsx("input", { type: "text", value: richSelect.searchText, onChange: (e) => richSelect.setSearchText(e.target.value), onKeyDown: richSelect.handleKeyDown, placeholder: "Search...", autoFocus: true, style: editorInputStyle }), _jsxs("div", { style: {
46
- position: 'absolute',
47
- top: '100%',
48
- left: 0,
49
- right: 0,
50
- maxHeight: 200,
51
- overflowY: 'auto',
52
- background: 'var(--ogrid-bg, #fff)',
53
- border: '1px solid var(--ogrid-border, #ccc)',
54
- zIndex: 10,
55
- boxShadow: '0 4px 16px rgba(0,0,0,0.2)',
56
- }, role: "listbox", children: [richSelect.filteredValues.map((v, i) => (_jsx("div", { role: "option", "aria-selected": i === richSelect.highlightedIndex, onClick: () => richSelect.selectValue(v), style: {
57
- padding: '6px 8px',
58
- cursor: 'pointer',
59
- color: 'var(--ogrid-fg, #242424)',
60
- background: i === richSelect.highlightedIndex ? 'var(--ogrid-bg-hover, #e8f0fe)' : undefined,
61
- }, children: richSelect.getDisplayText(v) }, String(v)))), richSelect.filteredValues.length === 0 && (_jsx("div", { style: { padding: '6px 8px', color: 'var(--ogrid-muted, #999)' }, children: "No matches" }))] })] }));
62
- }
63
- if (editorType === 'checkbox') {
64
- const checked = value === true;
65
- return (_jsx(Checkbox.Root, { checked: checked, onCheckedChange: (c) => commit(c === true), onKeyDown: (e) => e.key === 'Escape' && (e.preventDefault(), cancel()), children: _jsx(Checkbox.Indicator, { children: "\u2713" }) }));
66
- }
67
- if (editorType === 'select') {
68
- const values = column.cellEditorParams?.values ?? [];
69
- return (_jsx("div", { style: editorWrapperStyle, children: _jsx("select", { value: value !== null && value !== undefined ? String(value) : '', onChange: (e) => commit(e.target.value), onKeyDown: (e) => e.key === 'Escape' && (e.preventDefault(), cancel()), autoFocus: true, style: {
70
- width: '100%',
71
- padding: 0,
72
- border: 'none',
73
- background: 'transparent',
74
- color: 'inherit',
75
- font: 'inherit',
76
- fontSize: '13px',
77
- cursor: 'pointer',
78
- outline: 'none',
79
- }, children: values.map((v) => (_jsx("option", { value: String(v), children: String(v) }, String(v)))) }) }));
80
- }
81
- if (editorType === 'date') {
82
- return (_jsx("div", { ref: wrapperRef, style: editorWrapperStyle, children: _jsx("input", { type: "date", value: localValue, onChange: (e) => setLocalValue(e.target.value), onBlur: handleBlur, onKeyDown: handleKeyDown, style: editorInputStyle, autoFocus: true }) }));
83
- }
84
- return (_jsx("div", { ref: wrapperRef, style: editorWrapperStyle, children: _jsx("input", { type: "text", value: localValue, onChange: (e) => setLocalValue(e.target.value), onBlur: handleBlur, onKeyDown: handleKeyDown, style: editorInputStyle, autoFocus: true }) }));
6
+ return (_jsx(BaseInlineCellEditor, { ...props, renderCheckbox: (checked, onCommit, onCancel) => (_jsx(Checkbox.Root, { checked: checked, onCheckedChange: (c) => onCommit(c === true), onKeyDown: (e) => e.key === 'Escape' && (e.preventDefault(), onCancel()), children: _jsx(Checkbox.Indicator, { children: "\u2713" }) })), renderSelect: (value, values, onCommit, onCancel) => {
7
+ const { column } = props;
8
+ return (_jsx("div", { style: selectWrapperStyle, children: _jsx("select", { value: value !== null && value !== undefined ? String(value) : '', onChange: (e) => onCommit(e.target.value), onKeyDown: (e) => e.key === 'Escape' && (e.preventDefault(), onCancel()), autoFocus: true, style: selectEditorStyle, children: values.map((v) => (_jsx("option", { value: String(v), children: String(v) }, String(v)))) }) }));
9
+ } }));
85
10
  }
@@ -6,11 +6,11 @@ import { ColumnChooser } from '../ColumnChooser/ColumnChooser';
6
6
  import { PaginationControls } from '../PaginationControls/PaginationControls';
7
7
  import { useOGrid, OGridLayout, } from '@alaarab/ogrid-core';
8
8
  const OGridInner = forwardRef(function OGridInner(props, ref) {
9
- const { dataGridProps, page, pageSize, displayTotalCount, setPage, setPageSize, columnChooserColumns, visibleColumns, handleVisibilityChange, columnChooserPlacement, toolbar, toolbarBelow, className, entityLabelPlural, pageSizeOptions, sideBarProps, } = useOGrid(props, ref);
10
- return (_jsx(OGridLayout, { className: className, sideBar: sideBarProps, toolbar: toolbar, toolbarBelow: toolbarBelow, toolbarEnd: columnChooserPlacement === 'toolbar' ? (_jsx(ColumnChooser, { columns: columnChooserColumns, visibleColumns: visibleColumns, onVisibilityChange: handleVisibilityChange })) : undefined, pagination: _jsx(PaginationControls, { currentPage: page, pageSize: pageSize, totalCount: displayTotalCount, onPageChange: setPage, onPageSizeChange: (size) => {
11
- setPageSize(size);
12
- setPage(1);
13
- }, pageSizeOptions: pageSizeOptions, entityLabelPlural: entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
9
+ const { dataGridProps, pagination, columnChooser, layout } = useOGrid(props, ref);
10
+ return (_jsx(OGridLayout, { 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) => {
11
+ pagination.setPageSize(size);
12
+ pagination.setPage(1);
13
+ }, pageSizeOptions: pagination.pageSizeOptions, entityLabelPlural: pagination.entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
14
14
  });
15
15
  OGridInner.displayName = 'OGrid';
16
16
  export const OGrid = React.memo(OGridInner);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid",
3
- "version": "1.8.2",
3
+ "version": "1.9.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",