@alaarab/ogrid-react-radix 2.0.2 → 2.0.4

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.
@@ -39,7 +39,7 @@
39
39
  min-width: 220px;
40
40
  background: var(--ogrid-bg, #fff);
41
41
  border-radius: 6px;
42
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
42
+ box-shadow: var(--ogrid-shadow, 0 4px 16px rgba(0, 0, 0, 0.12));
43
43
  border: 1px solid var(--ogrid-border, #e0e0e0);
44
44
  display: flex;
45
45
  flex-direction: column;
@@ -108,7 +108,7 @@
108
108
  background: var(--ogrid-bg, #fff);
109
109
  border: 1px solid var(--ogrid-border, #d1d1d1);
110
110
  border-radius: 8px;
111
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.14);
111
+ box-shadow: var(--ogrid-shadow, 0 4px 16px rgba(0, 0, 0, 0.12));
112
112
  overflow: hidden;
113
113
  }
114
114
 
@@ -8,7 +8,7 @@ function UserAvatar({ user, size = 32 }) {
8
8
  width: size,
9
9
  height: size,
10
10
  borderRadius: '50%',
11
- background: '#e0e0e0',
11
+ background: 'var(--ogrid-border, #e0e0e0)',
12
12
  display: 'flex',
13
13
  alignItems: 'center',
14
14
  justifyContent: 'center',
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
3
+ import { COLUMN_HEADER_MENU_ITEMS } from '@alaarab/ogrid-core';
4
+ import styles from './ColumnHeaderMenu.module.css';
5
+ /**
6
+ * Column header dropdown menu for pin/unpin actions.
7
+ * Uses Radix UI DropdownMenu primitives.
8
+ */
9
+ export function ColumnHeaderMenu(props) {
10
+ const { isOpen, onClose, onPinLeft, onPinRight, onUnpin, canPinLeft, canPinRight, canUnpin } = props;
11
+ const handleOpenChange = (open) => {
12
+ if (!open) {
13
+ onClose();
14
+ }
15
+ };
16
+ return (_jsx(DropdownMenu.Root, { open: isOpen, onOpenChange: handleOpenChange, children: _jsx(DropdownMenu.Portal, { children: _jsxs(DropdownMenu.Content, { className: styles.content, sideOffset: 4, align: "start", children: [_jsx(DropdownMenu.Item, { className: styles.item, disabled: !canPinLeft, onSelect: onPinLeft, children: COLUMN_HEADER_MENU_ITEMS[0].label }), _jsx(DropdownMenu.Item, { className: styles.item, disabled: !canPinRight, onSelect: onPinRight, children: COLUMN_HEADER_MENU_ITEMS[1].label }), _jsx(DropdownMenu.Item, { className: styles.item, disabled: !canUnpin, onSelect: onUnpin, children: COLUMN_HEADER_MENU_ITEMS[2].label })] }) }) }));
17
+ }
@@ -0,0 +1,53 @@
1
+ .content {
2
+ min-width: 140px;
3
+ background: white;
4
+ border-radius: 6px;
5
+ padding: 4px;
6
+ box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2);
7
+ z-index: 100;
8
+ animation-duration: 400ms;
9
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
10
+ will-change: transform, opacity;
11
+ }
12
+ @media (prefers-color-scheme: dark) {
13
+ .content {
14
+ background: #1a1a1a;
15
+ box-shadow: 0px 10px 38px -10px rgba(0, 0, 0, 0.5), 0px 10px 20px -15px rgba(0, 0, 0, 0.4);
16
+ }
17
+ }
18
+
19
+ .item {
20
+ font-size: 13px;
21
+ line-height: 1;
22
+ color: #111;
23
+ border-radius: 4px;
24
+ display: flex;
25
+ align-items: center;
26
+ height: 28px;
27
+ padding: 0 8px;
28
+ position: relative;
29
+ user-select: none;
30
+ outline: none;
31
+ cursor: pointer;
32
+ }
33
+ .item[data-disabled] {
34
+ color: #999;
35
+ pointer-events: none;
36
+ cursor: not-allowed;
37
+ }
38
+ .item[data-highlighted]:not([data-disabled]) {
39
+ background-color: #f5f5f5;
40
+ color: #111;
41
+ }
42
+ @media (prefers-color-scheme: dark) {
43
+ .item {
44
+ color: #e0e0e0;
45
+ }
46
+ .item[data-disabled] {
47
+ color: #555;
48
+ }
49
+ .item[data-highlighted]:not([data-disabled]) {
50
+ background-color: #2a2a2a;
51
+ color: #fff;
52
+ }
53
+ }
@@ -0,0 +1 @@
1
+ export { ColumnHeaderMenu } from './ColumnHeaderMenu';
@@ -5,10 +5,11 @@ import { createPortal } from 'react-dom';
5
5
  import * as Popover from '@radix-ui/react-popover';
6
6
  import * as Checkbox from '@radix-ui/react-checkbox';
7
7
  import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
8
+ import { ColumnHeaderMenu } from '../ColumnHeaderMenu';
8
9
  import { InlineCellEditor } from './InlineCellEditor';
9
10
  import { StatusBar } from './StatusBar';
10
11
  import { GridContextMenu } from './GridContextMenu';
11
- import { useDataGridState, useColumnResize, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-react';
12
+ import { useDataGridState, useColumnResize, useColumnReorder, useVirtualScroll, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-react';
12
13
  import styles from './DataGridTable.module.css';
13
14
  // Module-scope stable constants (avoid per-render allocations)
14
15
  const GRID_ROOT_STYLE = { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' };
@@ -18,8 +19,8 @@ const STOP_PROPAGATION = (e) => e.stopPropagation();
18
19
  const PREVENT_DEFAULT = (e) => { e.preventDefault(); };
19
20
  const NOOP = () => { };
20
21
  function GridRowInner(props) {
21
- const { item, rowIndex, rowId, isSelected, visibleCols, columnMeta, renderCellContent, handleSingleRowClick, handleRowCheckboxChange, lastMouseShiftRef, hasCheckboxCol, } = props;
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)))] }));
22
+ const { item, rowIndex, rowId, isSelected, visibleCols, columnMeta, renderCellContent, handleSingleRowClick, handleRowCheckboxChange, lastMouseShiftRef, hasCheckboxCol, hasRowNumbersCol, rowNumberOffset, } = props;
23
+ 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" }) }) }) })), hasRowNumbersCol && (_jsx("td", { className: styles.rowNumberCell, children: _jsx("div", { className: styles.rowNumberCellInner, children: rowNumberOffset + rowIndex + 1 }) })), 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)))] }));
23
24
  }
24
25
  const GridRow = React.memo(GridRowInner, areGridRowPropsEqual);
25
26
  function DataGridTableInner(props) {
@@ -27,15 +28,18 @@ function DataGridTableInner(props) {
27
28
  const tableContainerRef = useRef(null);
28
29
  const state = useDataGridState({ props, wrapperRef });
29
30
  const lastMouseShiftRef = useRef(false);
30
- const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels } = state;
31
- const { visibleCols, totalColCount, hasCheckboxCol, colOffset, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides } = layout;
31
+ const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels, pinning } = state;
32
+ const { visibleCols, totalColCount, hasCheckboxCol, hasRowNumbersCol, colOffset, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides } = layout;
32
33
  const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
33
34
  const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
34
35
  const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging } = interaction;
35
36
  const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
36
37
  const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
37
38
  const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
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;
39
+ const { headerMenu } = pinning;
40
+ const { items, columns, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, visibleColumns, columnOrder, onColumnOrderChange, columnReorder, virtualScroll, density = 'normal', pinnedColumns, currentPage = 1, pageSize: propPageSize = 25, } = props;
41
+ // Calculate row number offset for pagination
42
+ const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * propPageSize : 0;
39
43
  // Memoize header rows (recursive tree traversal — avoid recomputing every render)
40
44
  const headerRows = useMemo(() => buildHeaderRows(columns, visibleColumns), [columns, visibleColumns]);
41
45
  const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
@@ -44,6 +48,23 @@ function DataGridTableInner(props) {
44
48
  columnSizingOverrides,
45
49
  setColumnSizingOverrides,
46
50
  });
51
+ const { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown } = useColumnReorder({
52
+ columns: visibleCols,
53
+ columnOrder,
54
+ onColumnOrderChange,
55
+ enabled: columnReorder === true,
56
+ pinnedColumns,
57
+ wrapperRef,
58
+ });
59
+ const virtualScrollEnabled = virtualScroll?.enabled === true;
60
+ const virtualRowHeight = virtualScroll?.rowHeight ?? 36;
61
+ const { visibleRange } = useVirtualScroll({
62
+ totalRows: items.length,
63
+ rowHeight: virtualRowHeight,
64
+ enabled: virtualScrollEnabled,
65
+ overscan: virtualScroll?.overscan,
66
+ containerRef: wrapperRef,
67
+ });
47
68
  const editCallbacks = useMemo(() => ({ commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit }), [commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit]);
48
69
  const interactionHandlers = useMemo(() => ({ handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu }), [handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu]);
49
70
  // Refs for volatile state — lets renderCellContent be stable (same function ref across
@@ -131,12 +152,12 @@ function DataGridTableInner(props) {
131
152
  },
132
153
  // eslint-disable-next-line react-hooks/exhaustive-deps -- *Ref vars are stable refs from useLatestRef
133
154
  [editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit, getRowId, onCellError]);
134
- 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: {
155
+ 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 : ''} ${styles[`density-${density}`] || ''}`, 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: {
135
156
  ['--data-table-column-count']: totalColCount,
136
157
  ['--data-table-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'fit-content' : fitToContent ? 'fit-content' : '100%',
137
158
  ['--data-table-min-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'max-content' : fitToContent ? 'max-content' : '100%',
138
159
  ['--data-table-total-min-width']: `${minTableWidth}px`,
139
- }, children: [_jsx("div", { className: styles.tableScrollContent, children: _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: styles.stickyHeader, children: headerRows.map((row, rowIdx) => (_jsxs("tr", { children: [rowIdx === headerRows.length - 1 && hasCheckboxCol && (_jsx("th", { className: styles.selectionHeaderCell, scope: "col", rowSpan: 1, children: _jsx("div", { className: styles.selectionHeaderCellInner, children: _jsx(Checkbox.Root, { className: styles.rowCheckbox, checked: allSelected ? true : someSelected ? 'indeterminate' : false, onCheckedChange: handleSelectAllChecked, "aria-label": "Select all rows", children: _jsx(Checkbox.Indicator, { className: styles.rowCheckboxIndicator, children: someSelected && !allSelected ? '–' : '✓' }) }) }) })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasCheckboxCol && (_jsx("th", { rowSpan: headerRows.length - 1 })), row.map((cell, cellIdx) => {
160
+ }, children: [_jsx("div", { className: styles.tableScrollContent, children: _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: styles.stickyHeader, children: headerRows.map((row, rowIdx) => (_jsxs("tr", { children: [rowIdx === headerRows.length - 1 && hasCheckboxCol && (_jsx("th", { className: styles.selectionHeaderCell, scope: "col", rowSpan: 1, children: _jsx("div", { className: styles.selectionHeaderCellInner, children: _jsx(Checkbox.Root, { className: styles.rowCheckbox, checked: allSelected ? true : someSelected ? 'indeterminate' : false, onCheckedChange: handleSelectAllChecked, "aria-label": "Select all rows", children: _jsx(Checkbox.Indicator, { className: styles.rowCheckboxIndicator, children: someSelected && !allSelected ? '–' : '✓' }) }) }) })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasCheckboxCol && (_jsx("th", { rowSpan: headerRows.length - 1 })), rowIdx === headerRows.length - 1 && hasRowNumbersCol && (_jsx("th", { className: styles.rowNumberHeaderCell, scope: "col", rowSpan: 1, children: _jsx("div", { className: styles.rowNumberHeaderCellInner, children: "#" }) })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasRowNumbersCol && (_jsx("th", { rowSpan: headerRows.length - 1 })), row.map((cell, cellIdx) => {
140
161
  if (cell.isGroup) {
141
162
  return (_jsx("th", { colSpan: cell.colSpan, className: styles.groupHeaderCell, scope: "colgroup", children: cell.label }, cellIdx));
142
163
  }
@@ -145,11 +166,23 @@ function DataGridTableInner(props) {
145
166
  const leafRowSpan = headerRows.length > 1 && rowIdx < headerRows.length - 1
146
167
  ? headerRows.length - rowIdx
147
168
  : undefined;
148
- return (_jsxs("th", { scope: "col", "data-column-id": col.columnId, rowSpan: leafRowSpan, className: columnMeta.hdrClasses[col.columnId] || undefined, style: columnMeta.hdrStyles[col.columnId], children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx("div", { className: styles.resizeHandle, onMouseDown: (e) => handleResizeStart(e, col), "aria-label": `Resize ${col.name}` })] }, col.columnId));
149
- })] }, rowIdx))) }), !showEmptyInGrid && (_jsx("tbody", { children: items.map((item, rowIndex) => {
150
- const rowIdStr = getRowId(item);
151
- 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));
152
- }) }))] }), _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 &&
153
- 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 && (_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 })] }) }))] }));
169
+ return (_jsxs("th", { scope: "col", "data-column-id": col.columnId, rowSpan: leafRowSpan, className: columnMeta.hdrClasses[col.columnId] || undefined, style: {
170
+ ...columnMeta.hdrStyles[col.columnId],
171
+ ...(columnReorder ? { cursor: isReorderDragging ? 'grabbing' : 'grab' } : undefined),
172
+ }, onMouseDown: columnReorder ? (e) => handleHeaderMouseDown(col.columnId, e) : undefined, children: [_jsxs("div", { className: styles.headerCellContent, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx("button", { className: styles.headerMenuTrigger, onClick: (e) => {
173
+ e.stopPropagation();
174
+ headerMenu.open(col.columnId, e.currentTarget);
175
+ }, "aria-label": "Column options", title: "Column options", children: "\u22EE" })] }), _jsx("div", { className: styles.resizeHandle, onMouseDown: (e) => handleResizeStart(e, col), "aria-label": `Resize ${col.name}` })] }, col.columnId));
176
+ })] }, rowIdx))) }), !showEmptyInGrid && (_jsxs("tbody", { children: [virtualScrollEnabled && visibleRange.offsetTop > 0 && (_jsx("tr", { style: { height: visibleRange.offsetTop }, "aria-hidden": true })), (virtualScrollEnabled
177
+ ? items.slice(visibleRange.startIndex, visibleRange.endIndex + 1).map((item, i) => {
178
+ const rowIndex = visibleRange.startIndex + i;
179
+ const rowIdStr = getRowId(item);
180
+ 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, hasRowNumbersCol: hasRowNumbersCol, rowNumberOffset: rowNumberOffset, selectionRange: selectionRange, activeCell: interaction.activeCell, cutRange: cutRange, copyRange: copyRange, isDragging: isDragging, editingRowId: editingCell?.rowId ?? null }, rowIdStr));
181
+ })
182
+ : items.map((item, rowIndex) => {
183
+ const rowIdStr = getRowId(item);
184
+ 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, hasRowNumbersCol: hasRowNumbersCol, rowNumberOffset: rowNumberOffset, selectionRange: selectionRange, activeCell: interaction.activeCell, cutRange: cutRange, copyRange: copyRange, isDragging: isDragging, editingRowId: editingCell?.rowId ?? null }, rowIdStr));
185
+ })), virtualScrollEnabled && visibleRange.offsetBottom > 0 && (_jsx("tr", { style: { height: visibleRange.offsetBottom }, "aria-hidden": true }))] }))] }), isReorderDragging && dropIndicatorX != null && (_jsx("div", { className: styles.dropIndicator, style: { left: dropIndicatorX - (wrapperRef.current?.getBoundingClientRect().left ?? 0) } })), _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 &&
186
+ 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, { isOpen: headerMenu.isOpen, anchorElement: headerMenu.anchorElement, onClose: headerMenu.close, onPinLeft: headerMenu.handlePinLeft, onPinRight: headerMenu.handlePinRight, onUnpin: headerMenu.handleUnpin, canPinLeft: headerMenu.canPinLeft, canPinRight: headerMenu.canPinRight, canUnpin: 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("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 })] }) }))] }));
154
187
  }
155
188
  export const DataGridTable = React.memo(DataGridTableInner);
@@ -4,7 +4,6 @@
4
4
  flex-direction: column;
5
5
  width: 100%;
6
6
  min-width: 0;
7
- min-height: 100%;
8
7
  background: var(--ogrid-bg, #fff);
9
8
  }
10
9
 
@@ -117,6 +116,7 @@
117
116
  z-index: 6;
118
117
  background: var(--ogrid-bg, #ffffff);
119
118
  will-change: transform;
119
+ border-left: 2px solid var(--ogrid-accent, #0078d4);
120
120
  }
121
121
  .dataTable .pinnedColLeft::after {
122
122
  content: "";
@@ -125,7 +125,7 @@
125
125
  right: -4px;
126
126
  bottom: 0;
127
127
  width: 4px;
128
- background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent);
128
+ background: linear-gradient(to right, var(--ogrid-pinned-shadow, rgba(0, 0, 0, 0.12)), transparent);
129
129
  pointer-events: none;
130
130
  }
131
131
 
@@ -139,6 +139,7 @@
139
139
  z-index: 6;
140
140
  background: var(--ogrid-bg, #ffffff);
141
141
  will-change: transform;
142
+ border-right: 2px solid var(--ogrid-accent, #0078d4);
142
143
  }
143
144
  .dataTable .pinnedColRight::before {
144
145
  content: "";
@@ -147,7 +148,7 @@
147
148
  left: -4px;
148
149
  bottom: 0;
149
150
  width: 4px;
150
- background: linear-gradient(to left, rgba(0, 0, 0, 0.08), transparent);
151
+ background: linear-gradient(to left, var(--ogrid-pinned-shadow, rgba(0, 0, 0, 0.12)), transparent);
151
152
  pointer-events: none;
152
153
  }
153
154
 
@@ -164,6 +165,52 @@
164
165
  border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
165
166
  }
166
167
 
168
+ .headerCellContent {
169
+ display: flex;
170
+ align-items: center;
171
+ gap: 4px;
172
+ }
173
+
174
+ .headerMenuTrigger {
175
+ background: transparent;
176
+ border: none;
177
+ cursor: pointer;
178
+ padding: 2px 4px;
179
+ font-size: 16px;
180
+ line-height: 1;
181
+ color: var(--ogrid-fg-secondary, #666);
182
+ opacity: 0;
183
+ transition: opacity 0.15s, background-color 0.15s;
184
+ border-radius: 3px;
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: center;
188
+ min-width: 20px;
189
+ height: 20px;
190
+ }
191
+ .headerMenuTrigger:hover {
192
+ background: var(--ogrid-hover-bg, #f3f2f1);
193
+ opacity: 1;
194
+ }
195
+ .headerMenuTrigger:active {
196
+ background: var(--ogrid-active-bg, #e1dfdd);
197
+ }
198
+ @media (prefers-color-scheme: dark) {
199
+ .headerMenuTrigger {
200
+ color: var(--ogrid-fg-secondary, #aaa);
201
+ }
202
+ .headerMenuTrigger:hover {
203
+ background: var(--ogrid-hover-bg, #2a2a2a);
204
+ }
205
+ .headerMenuTrigger:active {
206
+ background: var(--ogrid-active-bg, #3a3a3a);
207
+ }
208
+ }
209
+
210
+ .dataTable thead th:hover .headerMenuTrigger {
211
+ opacity: 1;
212
+ }
213
+
167
214
  .groupHeaderCell {
168
215
  text-align: center;
169
216
  font-weight: 600;
@@ -172,6 +219,18 @@
172
219
  background: var(--ogrid-header-bg, #f5f5f5);
173
220
  }
174
221
 
222
+ /* Column reorder drop indicator */
223
+ .dropIndicator {
224
+ position: absolute;
225
+ top: 0;
226
+ bottom: 0;
227
+ width: 3px;
228
+ background: var(--ogrid-primary, #217346);
229
+ pointer-events: none;
230
+ z-index: 100;
231
+ transition: left 0.05s;
232
+ }
233
+
175
234
  /* Column resize handle — wide hit area with narrow visual indicator */
176
235
  .resizeHandle {
177
236
  position: absolute;
@@ -254,6 +313,10 @@
254
313
  background: var(--ogrid-bg-range, rgba(33, 115, 70, 0.12)) !important;
255
314
  }
256
315
 
316
+ :global([data-drag-anchor]) {
317
+ background: var(--ogrid-bg, #fff) !important;
318
+ }
319
+
257
320
  .cellCut {
258
321
  background: var(--ogrid-bg-hover, rgba(0, 0, 0, 0.04)) !important;
259
322
  opacity: 0.7;
@@ -290,6 +353,36 @@
290
353
  width: 100%;
291
354
  }
292
355
 
356
+ .dataTable th.rowNumberHeaderCell,
357
+ .dataTable td.rowNumberCell {
358
+ width: 50px;
359
+ min-width: 50px;
360
+ max-width: 50px;
361
+ padding: 4px 8px !important;
362
+ text-align: center;
363
+ background: var(--ogrid-bg-subtle, #f9f9f9);
364
+ font-weight: 600;
365
+ color: var(--ogrid-text-secondary, #666);
366
+ border-right: 1px solid var(--ogrid-border, #e0e0e0);
367
+ position: sticky;
368
+ left: var(--row-number-sticky-left, 0px);
369
+ z-index: 5;
370
+ }
371
+
372
+ .dataTable th.rowNumberHeaderCell {
373
+ background: var(--ogrid-bg-subtle, #f3f2f1);
374
+ z-index: 9;
375
+ }
376
+
377
+ .rowNumberHeaderCellInner,
378
+ .rowNumberCellInner {
379
+ display: flex;
380
+ align-items: center;
381
+ justify-content: center;
382
+ width: 100%;
383
+ font-variant-numeric: tabular-nums;
384
+ }
385
+
293
386
  .rowCheckbox {
294
387
  width: 18px;
295
388
  height: 18px;
@@ -361,7 +454,7 @@
361
454
  background: var(--ogrid-bg, #fff);
362
455
  border: 1px solid var(--ogrid-border, #e0e0e0);
363
456
  border-radius: 6px;
364
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
457
+ box-shadow: var(--ogrid-shadow, 0 4px 16px rgba(0, 0, 0, 0.12));
365
458
  }
366
459
 
367
460
  .contextMenuItem {
@@ -408,7 +501,7 @@
408
501
  display: flex;
409
502
  align-items: center;
410
503
  justify-content: center;
411
- background: rgba(255, 255, 255, 0.7);
504
+ background: var(--ogrid-loading-bg, rgba(255, 255, 255, 0.7));
412
505
  backdrop-filter: blur(1px);
413
506
  pointer-events: all;
414
507
  }
@@ -422,7 +515,7 @@
422
515
  background: var(--ogrid-bg, #fff);
423
516
  border: 1px solid var(--ogrid-border, #c4c4c4);
424
517
  border-radius: 6px;
425
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14);
518
+ box-shadow: var(--ogrid-shadow-sm, 0 2px 4px rgba(0, 0, 0, 0.08));
426
519
  }
427
520
 
428
521
  .loadingOverlayText {
@@ -490,4 +583,18 @@
490
583
  to {
491
584
  transform: rotate(360deg);
492
585
  }
586
+ }
587
+ /* ─── Cell Density ───────────────────────────────────────────── */
588
+ .density-compact .dataTable th {
589
+ padding: 4px 8px;
590
+ }
591
+ .density-compact .cellContent {
592
+ padding: 4px 8px;
593
+ }
594
+
595
+ .density-comfortable .dataTable th {
596
+ padding: 12px 16px;
597
+ }
598
+ .density-comfortable .cellContent {
599
+ padding: 12px 16px;
493
600
  }
@@ -0,0 +1,16 @@
1
+ export interface ColumnHeaderMenuProps {
2
+ isOpen: boolean;
3
+ anchorElement: HTMLElement | null;
4
+ onClose: () => void;
5
+ onPinLeft: () => void;
6
+ onPinRight: () => void;
7
+ onUnpin: () => void;
8
+ canPinLeft: boolean;
9
+ canPinRight: boolean;
10
+ canUnpin: boolean;
11
+ }
12
+ /**
13
+ * Column header dropdown menu for pin/unpin actions.
14
+ * Uses Radix UI DropdownMenu primitives.
15
+ */
16
+ export declare function ColumnHeaderMenu(props: ColumnHeaderMenuProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ export { ColumnHeaderMenu } from './ColumnHeaderMenu';
2
+ export type { ColumnHeaderMenuProps } from './ColumnHeaderMenu';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-react-radix",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
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",
@@ -41,8 +41,9 @@
41
41
  "node": ">=18"
42
42
  },
43
43
  "dependencies": {
44
- "@alaarab/ogrid-react": "2.0.2",
44
+ "@alaarab/ogrid-react": "2.0.4",
45
45
  "@radix-ui/react-checkbox": "^1.1.2",
46
+ "@radix-ui/react-dropdown-menu": "^2.1.4",
46
47
  "@radix-ui/react-popover": "^1.1.2"
47
48
  },
48
49
  "peerDependencies": {