@alaarab/ogrid 1.2.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.
Files changed (32) hide show
  1. package/README.md +56 -0
  2. package/dist/esm/ColumnChooser/ColumnChooser.js +22 -0
  3. package/dist/esm/ColumnChooser/ColumnChooser.module.css +139 -0
  4. package/dist/esm/ColumnHeaderFilter/ColumnHeaderFilter.js +50 -0
  5. package/dist/esm/ColumnHeaderFilter/ColumnHeaderFilter.module.css +332 -0
  6. package/dist/esm/ColumnHeaderFilter/MultiSelectFilterPopover.js +5 -0
  7. package/dist/esm/ColumnHeaderFilter/PeopleFilterPopover.js +19 -0
  8. package/dist/esm/ColumnHeaderFilter/TextFilterPopover.js +4 -0
  9. package/dist/esm/ColumnHeaderFilter/index.js +1 -0
  10. package/dist/esm/DataGridTable/DataGridTable.js +123 -0
  11. package/dist/esm/DataGridTable/DataGridTable.module.css +421 -0
  12. package/dist/esm/DataGridTable/GridContextMenu.js +26 -0
  13. package/dist/esm/DataGridTable/InlineCellEditor.js +22 -0
  14. package/dist/esm/DataGridTable/StatusBar.js +7 -0
  15. package/dist/esm/OGrid/OGrid.js +16 -0
  16. package/dist/esm/PaginationControls/PaginationControls.js +31 -0
  17. package/dist/esm/PaginationControls/PaginationControls.module.css +112 -0
  18. package/dist/esm/index.js +8 -0
  19. package/dist/types/ColumnChooser/ColumnChooser.d.ts +10 -0
  20. package/dist/types/ColumnHeaderFilter/ColumnHeaderFilter.d.ts +20 -0
  21. package/dist/types/ColumnHeaderFilter/MultiSelectFilterPopover.d.ts +14 -0
  22. package/dist/types/ColumnHeaderFilter/PeopleFilterPopover.d.ts +13 -0
  23. package/dist/types/ColumnHeaderFilter/TextFilterPopover.d.ts +8 -0
  24. package/dist/types/ColumnHeaderFilter/index.d.ts +1 -0
  25. package/dist/types/DataGridTable/DataGridTable.d.ts +7 -0
  26. package/dist/types/DataGridTable/GridContextMenu.d.ts +8 -0
  27. package/dist/types/DataGridTable/InlineCellEditor.d.ts +12 -0
  28. package/dist/types/DataGridTable/StatusBar.d.ts +8 -0
  29. package/dist/types/OGrid/OGrid.d.ts +5 -0
  30. package/dist/types/PaginationControls/PaginationControls.d.ts +11 -0
  31. package/dist/types/index.d.ts +6 -0
  32. package/package.json +56 -0
@@ -0,0 +1,123 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { useCallback, useRef } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import * as Popover from '@radix-ui/react-popover';
6
+ import * as Checkbox from '@radix-ui/react-checkbox';
7
+ import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
8
+ import { InlineCellEditor } from './InlineCellEditor';
9
+ import { StatusBar } from './StatusBar';
10
+ import { GridContextMenu } from './GridContextMenu';
11
+ import { useDataGridState, useColumnResize, getHeaderFilterConfig, getCellRenderDescriptor, } from '@alaarab/ogrid-core';
12
+ import styles from './DataGridTable.module.css';
13
+ function DataGridTableInner(props) {
14
+ const wrapperRef = useRef(null);
15
+ const state = useDataGridState({ props, wrapperRef });
16
+ 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 { items, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
19
+ const allowOverflowX = containerWidth > 0 && minTableWidth > containerWidth;
20
+ const fitToContent = layoutMode === 'content';
21
+ const { handleResizeStart, getColumnWidth } = useColumnResize({
22
+ columnSizingOverrides,
23
+ setColumnSizingOverrides,
24
+ });
25
+ const renderCellContent = useCallback((item, col, rowIndex, colIdx) => {
26
+ const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInput);
27
+ if (descriptor.mode === 'editing-inline') {
28
+ return (_jsx(InlineCellEditor, { value: descriptor.value, item: item, column: col, rowIndex: descriptor.rowIndex, editorType: descriptor.editorType ?? 'text', onCommit: (newValue) => commitCellEdit(item, col.columnId, descriptor.value, newValue, descriptor.rowIndex, descriptor.globalColIndex), onCancel: () => setEditingCell(null) }));
29
+ }
30
+ if (descriptor.mode === 'editing-popover' && typeof col.cellEditor === 'function') {
31
+ const oldValue = descriptor.value;
32
+ const displayValue = pendingEditorValue !== undefined ? pendingEditorValue : oldValue;
33
+ const CustomEditor = col.cellEditor;
34
+ const editorProps = {
35
+ value: displayValue,
36
+ onValueChange: setPendingEditorValue,
37
+ onCommit: () => {
38
+ const newValue = pendingEditorValue !== undefined ? pendingEditorValue : oldValue;
39
+ commitCellEdit(item, col.columnId, oldValue, newValue, descriptor.rowIndex, descriptor.globalColIndex);
40
+ },
41
+ onCancel: cancelPopoverEdit,
42
+ item,
43
+ column: col,
44
+ cellEditorParams: col.cellEditorParams,
45
+ };
46
+ return (_jsxs(Popover.Root, { open: !!popoverAnchorEl, onOpenChange: (open) => { if (!open)
47
+ 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 }) }) })] }));
48
+ }
49
+ let content;
50
+ if (col.renderCell)
51
+ content = col.renderCell(item);
52
+ else {
53
+ const value = descriptor.displayValue;
54
+ if (col.valueFormatter)
55
+ content = col.valueFormatter(value, item);
56
+ else if (value !== null && value !== undefined)
57
+ content = String(value);
58
+ else
59
+ content = null;
60
+ }
61
+ const cellStyle = col.cellStyle ? (typeof col.cellStyle === 'function' ? col.cellStyle(item) : col.cellStyle) : undefined;
62
+ if (cellStyle)
63
+ content = _jsx("span", { style: cellStyle, children: content });
64
+ const cellClassNames = [
65
+ styles.cellContent,
66
+ descriptor.isActive ? styles.activeCellContent : '',
67
+ descriptor.isInRange ? styles.cellInRange : '',
68
+ descriptor.isInCutRange ? styles.cellCut : '',
69
+ ].filter(Boolean).join(' ');
70
+ 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" }))] }));
72
+ }
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 }));
74
+ }, [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: {
76
+ ['--data-table-column-count']: totalColCount,
77
+ ['--data-table-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'fit-content' : fitToContent ? 'fit-content' : '100%',
78
+ ['--data-table-min-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'max-content' : fitToContent ? 'max-content' : '100%',
79
+ ['--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) => {
105
+ const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
106
+ const isPinnedLeft = col.pinned === 'left';
107
+ const isPinnedRight = col.pinned === 'right';
108
+ const columnWidth = getColumnWidth(col);
109
+ return (_jsx("td", { className: [
110
+ isFreezeCol ? styles.freezeCol : '',
111
+ isFreezeCol && colIdx === 0 ? styles.freezeColFirst : '',
112
+ isPinnedLeft ? styles.pinnedColLeft : '',
113
+ isPinnedRight ? styles.pinnedColRight : '',
114
+ ].filter(Boolean).join(' '), style: {
115
+ minWidth: col.minWidth ?? 80,
116
+ width: columnWidth,
117
+ maxWidth: columnWidth,
118
+ }, children: renderCellContent(item, col, rowIndex, colIdx) }, col.columnId));
119
+ })] }, rowIdStr));
120
+ }) }))] }) }) })] }), showEmptyInGrid && emptyState && (_jsx("div", { className: styles.emptyStateInGrid, children: _jsx("div", { children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.emptyStateInGridTitle, children: "No results found" }), _jsx("div", { className: styles.emptyStateInGridMessage, children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx("button", { type: "button", className: styles.emptyStateInGridLink, onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }) }))] }), statusBarConfig && (_jsx(StatusBar, { totalCount: statusBarConfig.totalCount, filteredCount: statusBarConfig.filteredCount, selectedCount: statusBarConfig.selectedCount ?? selectedRowIds.size, selectedCellCount: selectionRange ? (Math.abs(selectionRange.endRow - selectionRange.startRow) + 1) * (Math.abs(selectionRange.endCol - selectionRange.startCol) + 1) : undefined })), contextMenu &&
121
+ createPortal(_jsx(GridContextMenu, { x: contextMenu.x, y: contextMenu.y, hasSelection: hasCellSelection, onCopy: handleCopy, onCut: handleCut, onPaste: () => void handlePaste(), onSelectAll: handleSelectAllCells, onClose: closeContextMenu }), document.body)] }));
122
+ }
123
+ export const DataGridTable = React.memo(DataGridTableInner);
@@ -0,0 +1,421 @@
1
+ .tableScrollContent {
2
+ display: flex;
3
+ flex-direction: column;
4
+ width: 100%;
5
+ min-width: 0;
6
+ background: var(--ogrid-bg, #fff);
7
+ }
8
+
9
+ .tableWrapper {
10
+ overflow-x: hidden;
11
+ overflow-y: visible;
12
+ width: 100%;
13
+ min-width: 0;
14
+ max-width: 100%;
15
+ margin-bottom: 15px;
16
+ border-radius: 6px;
17
+ box-sizing: border-box;
18
+ }
19
+ .tableWrapper[data-overflow-x=true] {
20
+ overflow-x: auto;
21
+ }
22
+ .tableWrapper[data-empty=true] {
23
+ overflow-x: hidden;
24
+ }
25
+
26
+ .tableWidthAnchor {
27
+ width: max-content;
28
+ min-width: max(100%, var(--data-table-total-min-width, 0px));
29
+ background: var(--ogrid-bg, #fff);
30
+ }
31
+
32
+ .dataTable {
33
+ width: var(--data-table-width, fit-content);
34
+ max-width: 100%;
35
+ min-width: var(--data-table-min-width, max-content);
36
+ border-collapse: separate;
37
+ border-spacing: 0;
38
+ border: 1px solid var(--ogrid-border, #e0e0e0);
39
+ border-radius: 6px;
40
+ overflow: hidden;
41
+ box-sizing: border-box;
42
+ table-layout: auto;
43
+ }
44
+
45
+ .dataTable th,
46
+ .dataTable td {
47
+ min-width: 80px;
48
+ box-sizing: border-box;
49
+ padding: 6px 10px;
50
+ border-right: 1px solid var(--ogrid-border, #e0e0e0);
51
+ font-size: 13px;
52
+ vertical-align: middle;
53
+ }
54
+
55
+ .dataTable th:last-of-type,
56
+ .dataTable td:last-of-type {
57
+ border-right: none;
58
+ }
59
+
60
+ .dataTable thead {
61
+ background: var(--ogrid-bg-subtle, #f3f2f1);
62
+ }
63
+
64
+ /* Freeze panes: sticky header when freezeRows >= 1 */
65
+ .dataTable thead.stickyHeader {
66
+ position: sticky;
67
+ top: 0;
68
+ z-index: 2;
69
+ background: var(--ogrid-bg-subtle, #f3f2f1);
70
+ }
71
+
72
+ .dataTable thead.stickyHeader th {
73
+ background: var(--ogrid-bg-subtle, #f3f2f1);
74
+ }
75
+
76
+ /* Freeze panes: sticky first column when freezeCols >= 1 */
77
+ .dataTable .freezeColFirst {
78
+ position: sticky;
79
+ left: 0;
80
+ z-index: 2;
81
+ background: var(--ogrid-bg, #ffffff);
82
+ }
83
+
84
+ .dataTable thead .freezeColFirst {
85
+ background: var(--ogrid-bg-subtle, #f3f2f1);
86
+ }
87
+
88
+ /* Pinned columns: sticky positioning based on column pinned property */
89
+ .pinnedColLeft {
90
+ position: sticky;
91
+ left: 0;
92
+ z-index: 2;
93
+ background: var(--ogrid-bg, #ffffff);
94
+ }
95
+ .pinnedColLeft::after {
96
+ content: "";
97
+ position: absolute;
98
+ top: 0;
99
+ right: -4px;
100
+ bottom: 0;
101
+ width: 4px;
102
+ background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent);
103
+ pointer-events: none;
104
+ }
105
+
106
+ .dataTable thead .pinnedColLeft {
107
+ background: var(--ogrid-bg-subtle, #f3f2f1);
108
+ }
109
+
110
+ .pinnedColRight {
111
+ position: sticky;
112
+ right: 0;
113
+ z-index: 2;
114
+ background: var(--ogrid-bg, #ffffff);
115
+ }
116
+ .pinnedColRight::before {
117
+ content: "";
118
+ position: absolute;
119
+ top: 0;
120
+ left: -4px;
121
+ bottom: 0;
122
+ width: 4px;
123
+ background: linear-gradient(to left, rgba(0, 0, 0, 0.08), transparent);
124
+ pointer-events: none;
125
+ }
126
+
127
+ .dataTable thead .pinnedColRight {
128
+ background: var(--ogrid-bg-subtle, #f3f2f1);
129
+ }
130
+
131
+ .dataTable thead th {
132
+ font-weight: 600;
133
+ font-size: 14px;
134
+ color: var(--ogrid-fg, #242424);
135
+ white-space: nowrap;
136
+ position: relative;
137
+ border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
138
+ }
139
+
140
+ /* Column resize handle */
141
+ .resizeHandle {
142
+ position: absolute;
143
+ top: 0;
144
+ right: 0;
145
+ bottom: 0;
146
+ width: 4px;
147
+ cursor: col-resize;
148
+ user-select: none;
149
+ z-index: 1;
150
+ }
151
+ .resizeHandle:hover {
152
+ background-color: var(--ogrid-accent, #0078d4);
153
+ }
154
+ .resizeHandle:active {
155
+ background-color: var(--ogrid-accent-dark, #005a9e);
156
+ }
157
+
158
+ .dataTable tbody td {
159
+ border-bottom: 1px solid var(--ogrid-border, #e8e8e8);
160
+ }
161
+
162
+ .dataTable tbody tr:last-child td {
163
+ border-bottom: none;
164
+ }
165
+
166
+ .dataTable tbody tr:hover td {
167
+ background: var(--ogrid-bg-hover, #f5f5f5);
168
+ }
169
+
170
+ .dataTable tbody tr.selectedRow td {
171
+ background: var(--ogrid-bg-selected, #e6f0fb);
172
+ }
173
+
174
+ .dataTable tbody tr.selectedRow:hover td {
175
+ background: var(--ogrid-bg-selected-hover, #dae8f8);
176
+ }
177
+
178
+ .dataTable tbody td {
179
+ overflow: hidden;
180
+ text-overflow: ellipsis;
181
+ white-space: nowrap;
182
+ }
183
+
184
+ .cellContent {
185
+ width: 100%;
186
+ min-height: 100%;
187
+ display: flex;
188
+ align-items: center;
189
+ min-width: 0;
190
+ user-select: none;
191
+ }
192
+
193
+ .activeCellContent {
194
+ outline: 2px solid var(--ogrid-primary, #0066cc);
195
+ outline-offset: -2px;
196
+ border-radius: 2px;
197
+ z-index: 1;
198
+ position: relative;
199
+ }
200
+
201
+ .cellInRange {
202
+ background: var(--ogrid-bg-range, #cce4f7) !important;
203
+ box-shadow: inset 0 0 0 1px var(--ogrid-primary, #0066cc);
204
+ }
205
+
206
+ .cellCut {
207
+ outline: 2px dashed var(--ogrid-primary, #0066cc);
208
+ outline-offset: -2px;
209
+ background: var(--ogrid-bg-hover, rgba(0, 0, 0, 0.04)) !important;
210
+ animation: cellCutDash 0.6s linear infinite;
211
+ }
212
+
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
+ .fillHandle {
225
+ position: absolute;
226
+ right: 2px;
227
+ bottom: 2px;
228
+ width: 8px;
229
+ height: 8px;
230
+ background: var(--ogrid-primary, #0066cc);
231
+ border-radius: 2px;
232
+ cursor: crosshair;
233
+ pointer-events: auto;
234
+ z-index: 3;
235
+ }
236
+
237
+ .dataTable th.selectionHeaderCell,
238
+ .dataTable td.selectionCell {
239
+ width: 48px;
240
+ min-width: 48px;
241
+ max-width: 48px;
242
+ padding: 4px !important;
243
+ text-align: center;
244
+ }
245
+
246
+ .selectionHeaderCellInner,
247
+ .selectionCellInner {
248
+ display: flex;
249
+ align-items: center;
250
+ justify-content: center;
251
+ width: 100%;
252
+ }
253
+
254
+ .statusBar {
255
+ display: flex;
256
+ align-items: center;
257
+ gap: 16px;
258
+ width: 100%;
259
+ padding: 6px 12px;
260
+ font-size: 12px;
261
+ color: var(--ogrid-muted, #616161);
262
+ background: var(--ogrid-bg-subtle, #f3f2f1);
263
+ border: 1px solid var(--ogrid-border, #e0e0e0);
264
+ border-top: none;
265
+ border-radius: 0 0 6px 6px;
266
+ min-height: 28px;
267
+ }
268
+
269
+ .statusBarItem {
270
+ display: inline-flex;
271
+ align-items: center;
272
+ gap: 4px;
273
+ }
274
+ .statusBarItem:not(:last-child)::after {
275
+ content: "";
276
+ width: 1px;
277
+ height: 14px;
278
+ background: var(--ogrid-border, #c4c4c4);
279
+ margin-left: 12px;
280
+ }
281
+
282
+ .statusBarLabel {
283
+ color: var(--ogrid-muted, #707070);
284
+ }
285
+
286
+ .statusBarValue {
287
+ color: var(--ogrid-fg, #242424);
288
+ font-weight: 600;
289
+ }
290
+
291
+ .contextMenu {
292
+ position: fixed;
293
+ z-index: 10000;
294
+ min-width: 160px;
295
+ padding: 4px 0;
296
+ background: var(--ogrid-bg, #fff);
297
+ border: 1px solid var(--ogrid-border, #e0e0e0);
298
+ border-radius: 6px;
299
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
300
+ }
301
+
302
+ .contextMenuItem {
303
+ display: block;
304
+ width: 100%;
305
+ padding: 6px 12px;
306
+ border: none;
307
+ background: none;
308
+ font-size: 13px;
309
+ text-align: left;
310
+ cursor: pointer;
311
+ color: var(--ogrid-fg, #242424);
312
+ }
313
+ .contextMenuItem:hover:not(:disabled) {
314
+ background: var(--ogrid-bg-hover, #f5f5f5);
315
+ }
316
+ .contextMenuItem:disabled {
317
+ opacity: 0.5;
318
+ cursor: not-allowed;
319
+ }
320
+
321
+ .contextMenuDivider {
322
+ height: 1px;
323
+ margin: 4px 0;
324
+ background: var(--ogrid-border, #e0e0e0);
325
+ }
326
+
327
+ .loadingOverlayContainer {
328
+ position: relative;
329
+ }
330
+
331
+ .loadingOverlay {
332
+ position: absolute;
333
+ inset: 0;
334
+ z-index: 2;
335
+ display: flex;
336
+ align-items: center;
337
+ justify-content: center;
338
+ background: rgba(255, 255, 255, 0.7);
339
+ backdrop-filter: blur(1px);
340
+ pointer-events: all;
341
+ border-radius: 6px;
342
+ }
343
+
344
+ .loadingOverlayContent {
345
+ display: flex;
346
+ flex-direction: column;
347
+ align-items: center;
348
+ gap: 8px;
349
+ padding: 16px 24px;
350
+ background: var(--ogrid-bg, #fff);
351
+ border: 1px solid var(--ogrid-border, #c4c4c4);
352
+ border-radius: 6px;
353
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14);
354
+ }
355
+
356
+ .loadingOverlayText {
357
+ font-size: 13px;
358
+ font-weight: 500;
359
+ color: var(--ogrid-muted, #616161);
360
+ }
361
+
362
+ .loadingDimmed {
363
+ opacity: 0.6;
364
+ pointer-events: none;
365
+ }
366
+
367
+ .emptyStateInGrid {
368
+ display: flex;
369
+ flex-direction: column;
370
+ align-items: center;
371
+ justify-content: center;
372
+ text-align: center;
373
+ padding: 20px 16px;
374
+ min-height: 88px;
375
+ width: 100%;
376
+ box-sizing: border-box;
377
+ border-top: 1px solid var(--ogrid-border, #e0e0e0);
378
+ background: var(--ogrid-bg-subtle, #fafafa);
379
+ }
380
+
381
+ .emptyStateInGridTitle {
382
+ font-size: 14px;
383
+ font-weight: 600;
384
+ color: var(--ogrid-fg, #242424);
385
+ margin-bottom: 4px;
386
+ }
387
+
388
+ .emptyStateInGridMessage {
389
+ font-size: 13px;
390
+ color: var(--ogrid-muted, #616161);
391
+ line-height: 1.5;
392
+ }
393
+
394
+ .emptyStateInGridLink {
395
+ background: none;
396
+ border: none;
397
+ color: var(--ogrid-primary, #0066cc);
398
+ text-decoration: underline;
399
+ cursor: pointer;
400
+ padding: 0;
401
+ font-size: inherit;
402
+ font-family: inherit;
403
+ }
404
+ .emptyStateInGridLink:hover {
405
+ color: var(--ogrid-primary-hover, #0052a3);
406
+ }
407
+
408
+ .spinner {
409
+ width: 24px;
410
+ height: 24px;
411
+ border: 2px solid var(--ogrid-border, #e0e0e0);
412
+ border-top-color: var(--ogrid-primary, #0066cc);
413
+ border-radius: 50%;
414
+ animation: ogrid-spin 0.8s linear infinite;
415
+ }
416
+
417
+ @keyframes ogrid-spin {
418
+ to {
419
+ transform: rotate(360deg);
420
+ }
421
+ }
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers } from '@alaarab/ogrid-core';
4
+ import styles from './DataGridTable.module.css';
5
+ export function GridContextMenu(props) {
6
+ const { x, y, hasSelection, onClose } = props;
7
+ const ref = React.useRef(null);
8
+ const handlers = React.useMemo(() => getContextMenuHandlers(props), [props]);
9
+ React.useEffect(() => {
10
+ const handleClickOutside = (e) => {
11
+ if (ref.current && !ref.current.contains(e.target))
12
+ onClose();
13
+ };
14
+ const handleKeyDown = (e) => {
15
+ if (e.key === 'Escape')
16
+ onClose();
17
+ };
18
+ document.addEventListener('mousedown', handleClickOutside, true);
19
+ document.addEventListener('keydown', handleKeyDown, true);
20
+ return () => {
21
+ document.removeEventListener('mousedown', handleClickOutside, true);
22
+ document.removeEventListener('keydown', handleKeyDown, true);
23
+ };
24
+ }, [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))) }));
26
+ }
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import * as Checkbox from '@radix-ui/react-checkbox';
4
+ import { useInlineCellEditorState } from '@alaarab/ogrid-core';
5
+ export function InlineCellEditor(props) {
6
+ const { value, column, editorType, onCommit, onCancel } = props;
7
+ const wrapperRef = React.useRef(null);
8
+ const { localValue, setLocalValue, handleKeyDown, handleBlur, commit, cancel } = useInlineCellEditorState({ value, editorType, onCommit, onCancel });
9
+ React.useEffect(() => {
10
+ const input = wrapperRef.current?.querySelector('input');
11
+ input?.focus();
12
+ }, []);
13
+ if (editorType === 'checkbox') {
14
+ const checked = value === true;
15
+ 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" }) }));
16
+ }
17
+ if (editorType === 'select') {
18
+ const values = column.cellEditorParams?.values ?? [];
19
+ return (_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, children: values.map((v) => (_jsx("option", { value: String(v), children: String(v) }, String(v)))) }));
20
+ }
21
+ return (_jsx("div", { ref: wrapperRef, children: _jsx("input", { type: "text", value: localValue, onChange: (e) => setLocalValue(e.target.value), onBlur: handleBlur, onKeyDown: handleKeyDown, style: { minWidth: 60 }, autoFocus: true }) }));
22
+ }
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { getStatusBarParts } from '@alaarab/ogrid-core';
3
+ import styles from './DataGridTable.module.css';
4
+ export function StatusBar(props) {
5
+ const parts = getStatusBarParts(props);
6
+ return (_jsx("div", { className: styles.statusBar, role: "status", "aria-live": "polite", children: parts.map((p) => (_jsxs("span", { className: styles.statusBarItem, children: [_jsx("span", { className: styles.statusBarLabel, children: p.label }), _jsx("span", { className: styles.statusBarValue, children: p.value.toLocaleString() })] }, p.key))) }));
7
+ }
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { forwardRef } from 'react';
4
+ import { DataGridTable } from '../DataGridTable/DataGridTable';
5
+ import { ColumnChooser } from '../ColumnChooser/ColumnChooser';
6
+ import { PaginationControls } from '../PaginationControls/PaginationControls';
7
+ import { useOGrid, OGridLayout, } from '@alaarab/ogrid-core';
8
+ const OGridInner = forwardRef(function OGridInner(props, ref) {
9
+ const { dataGridProps, page, pageSize, displayTotalCount, setPage, setPageSize, columnChooserColumns, visibleColumns, handleVisibilityChange, title, toolbar, className, entityLabelPlural, } = useOGrid(props, ref);
10
+ return (_jsx(OGridLayout, { className: className, gap: 16, title: title, toolbar: toolbar, columnChooser: _jsx(ColumnChooser, { columns: columnChooserColumns, visibleColumns: visibleColumns, onVisibilityChange: handleVisibilityChange }), pagination: _jsx(PaginationControls, { currentPage: page, pageSize: pageSize, totalCount: displayTotalCount, onPageChange: setPage, onPageSizeChange: (size) => {
11
+ setPageSize(size);
12
+ setPage(1);
13
+ }, entityLabelPlural: entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
14
+ });
15
+ OGridInner.displayName = 'OGrid';
16
+ export const OGrid = React.memo(OGridInner);
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { useMemo, useCallback } from 'react';
4
+ import { getPaginationViewModel } from '@alaarab/ogrid-core';
5
+ import styles from './PaginationControls.module.css';
6
+ function ChevronLeft() {
7
+ return _jsx("span", { "aria-hidden": true, children: "\u2039" });
8
+ }
9
+ function ChevronRight() {
10
+ return _jsx("span", { "aria-hidden": true, children: "\u203A" });
11
+ }
12
+ function ChevronDoubleLeft() {
13
+ return _jsx("span", { "aria-hidden": true, children: "\u00AB" });
14
+ }
15
+ function ChevronDoubleRight() {
16
+ return _jsx("span", { "aria-hidden": true, children: "\u00BB" });
17
+ }
18
+ export const PaginationControls = React.memo((props) => {
19
+ const { currentPage, pageSize, totalCount, onPageChange, onPageSizeChange, entityLabelPlural, className } = props;
20
+ const labelPlural = entityLabelPlural ?? 'items';
21
+ const vm = useMemo(() => getPaginationViewModel(currentPage, pageSize, totalCount), [currentPage, pageSize, totalCount]);
22
+ const handlePageSizeChange = useCallback((e) => {
23
+ onPageSizeChange(Number(e.target.value));
24
+ }, [onPageSizeChange]);
25
+ if (!vm) {
26
+ return null;
27
+ }
28
+ const { pageNumbers, showStartEllipsis, showEndEllipsis, totalPages, startItem, endItem } = vm;
29
+ return (_jsxs("div", { className: `${styles.pagination} ${className || ''}`, role: "navigation", "aria-label": "Pagination", children: [_jsxs("div", { className: styles.paginationInfo, children: ["Showing ", startItem, " to ", endItem, " of ", totalCount.toLocaleString(), " ", labelPlural] }), _jsxs("div", { className: styles.paginationControls, children: [_jsx("button", { type: "button", className: styles.navBtn, onClick: () => onPageChange(1), disabled: currentPage === 1, "aria-label": "First page", children: _jsx(ChevronDoubleLeft, {}) }), _jsx("button", { type: "button", className: styles.navBtn, onClick: () => onPageChange(currentPage - 1), disabled: currentPage === 1, "aria-label": "Previous page", children: _jsx(ChevronLeft, {}) }), _jsxs("div", { className: styles.pageNumbers, children: [showStartEllipsis && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: styles.pageBtn, onClick: () => onPageChange(1), "aria-label": "Page 1", children: "1" }), _jsx("span", { className: styles.ellipsis, "aria-hidden": true, children: "\u2026" })] })), pageNumbers.map((pageNum) => (_jsx("button", { type: "button", className: `${styles.pageBtn} ${currentPage === pageNum ? styles.active : ''}`, onClick: () => onPageChange(pageNum), "aria-label": `Page ${pageNum}`, "aria-current": currentPage === pageNum ? 'page' : undefined, children: pageNum }, pageNum))), showEndEllipsis && (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.ellipsis, "aria-hidden": true, children: "\u2026" }), _jsx("button", { type: "button", className: styles.pageBtn, onClick: () => onPageChange(totalPages), "aria-label": `Page ${totalPages}`, children: totalPages })] }))] }), _jsx("button", { type: "button", className: styles.navBtn, onClick: () => onPageChange(currentPage + 1), disabled: currentPage >= totalPages, "aria-label": "Next page", children: _jsx(ChevronRight, {}) }), _jsx("button", { type: "button", className: styles.navBtn, onClick: () => onPageChange(totalPages), disabled: currentPage >= totalPages, "aria-label": "Last page", children: _jsx(ChevronDoubleRight, {}) })] }), _jsxs("div", { className: styles.pageSizeSelector, children: [_jsx("span", { className: styles.pageSizeLabel, children: "Rows" }), _jsx("select", { className: styles.pageSizeSelect, value: String(pageSize), onChange: handlePageSizeChange, "aria-label": "Rows per page", children: vm.pageSizeOptions.map((n) => (_jsx("option", { value: n, children: n }, n))) })] })] }));
30
+ });
31
+ PaginationControls.displayName = 'PaginationControls';