@opencorestack/opengridx 0.1.6 → 0.1.8

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 (130) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +9 -0
  3. package/dist/index.d.ts +12 -1
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +2505 -2450
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +12 -12
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +177 -0
  10. package/docs/README.md +60 -0
  11. package/docs/components/cell.md +44 -0
  12. package/docs/components/column-group-header.md +57 -0
  13. package/docs/components/column-resize.md +63 -0
  14. package/docs/components/column-visibility.md +61 -0
  15. package/docs/components/datagrid.md +70 -0
  16. package/docs/components/filter-panel.md +26 -0
  17. package/docs/components/header.md +39 -0
  18. package/docs/components/pagination.md +34 -0
  19. package/docs/components/row.md +31 -0
  20. package/docs/components/toolbar.md +40 -0
  21. package/docs/components/tooltip.md +68 -0
  22. package/docs/customization/slots-api.md +227 -0
  23. package/docs/customization/theming.md +293 -0
  24. package/docs/features/aggregation-pivot.md +79 -0
  25. package/docs/features/cell-spanning.md +520 -0
  26. package/docs/features/clipboard.md +76 -0
  27. package/docs/features/custom-pagination.md +143 -0
  28. package/docs/features/data-source.md +72 -0
  29. package/docs/features/editing-reordering.md +99 -0
  30. package/docs/features/export-guide.md +434 -0
  31. package/docs/features/filtering.md +83 -0
  32. package/docs/features/infinite-scroll.md +87 -0
  33. package/docs/features/list-view.md +65 -0
  34. package/docs/features/loading-states.md +68 -0
  35. package/docs/features/master-detail.md +46 -0
  36. package/docs/features/pinning.md +67 -0
  37. package/docs/features/selection.md +70 -0
  38. package/docs/features/sorting-pagination.md +66 -0
  39. package/docs/features/state-persistence.md +67 -0
  40. package/docs/features/tree-data-grouping.md +67 -0
  41. package/docs/features/virtualization.md +188 -0
  42. package/docs/roadmap.md +71 -0
  43. package/lib/components/Cell/Cell.css +119 -0
  44. package/lib/components/Cell/Cell.tsx +264 -0
  45. package/lib/components/Cell/GridEditInputCell.css +62 -0
  46. package/lib/components/Cell/GridEditInputCell.tsx +152 -0
  47. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +67 -0
  48. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +220 -0
  49. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +31 -0
  50. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +85 -0
  51. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +137 -0
  52. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +190 -0
  53. package/lib/components/DataGrid/DataGrid.css +224 -0
  54. package/lib/components/DataGrid/DataGrid.tsx +2269 -0
  55. package/lib/components/DetailPanel/DetailPanel.css +36 -0
  56. package/lib/components/DetailPanel/DetailPanel.tsx +34 -0
  57. package/lib/components/FilterPanel/FilterPanel.css +196 -0
  58. package/lib/components/FilterPanel/FilterPanel.tsx +214 -0
  59. package/lib/components/FilterPanel/index.ts +1 -0
  60. package/lib/components/Header/ColumnMenu.css +68 -0
  61. package/lib/components/Header/ColumnMenu.tsx +249 -0
  62. package/lib/components/Header/Header.css +224 -0
  63. package/lib/components/Header/Header.tsx +516 -0
  64. package/lib/components/ListView/ListView.css +112 -0
  65. package/lib/components/ListView/ListViewRow.tsx +63 -0
  66. package/lib/components/Pagination/Pagination.css +91 -0
  67. package/lib/components/Pagination/Pagination.tsx +151 -0
  68. package/lib/components/QuickFilter/QuickFilter.css +23 -0
  69. package/lib/components/QuickFilter/QuickFilter.tsx +79 -0
  70. package/lib/components/Row/Row.css +126 -0
  71. package/lib/components/Row/Row.tsx +377 -0
  72. package/lib/components/Skeleton/Skeleton.css +55 -0
  73. package/lib/components/Skeleton/Skeleton.tsx +22 -0
  74. package/lib/components/SkeletonRow/SkeletonRow.css +32 -0
  75. package/lib/components/SkeletonRow/SkeletonRow.tsx +89 -0
  76. package/lib/components/SkeletonRow/index.ts +1 -0
  77. package/lib/components/Toolbar/GlobalSearch.css +110 -0
  78. package/lib/components/Toolbar/GlobalSearch.tsx +167 -0
  79. package/lib/components/Toolbar/GridToolbar.css +221 -0
  80. package/lib/components/Toolbar/GridToolbar.tsx +658 -0
  81. package/lib/components/Toolbar/PivotPanel.css +212 -0
  82. package/lib/components/Toolbar/PivotPanel.tsx +239 -0
  83. package/lib/components/Tooltip/Tooltip.css +59 -0
  84. package/lib/components/Tooltip/Tooltip.tsx +106 -0
  85. package/lib/components/ui/Button.css +89 -0
  86. package/lib/components/ui/Button.tsx +39 -0
  87. package/lib/components/ui/Checkbox.css +71 -0
  88. package/lib/components/ui/Checkbox.tsx +66 -0
  89. package/lib/components/ui/DragHandleIcon.css +19 -0
  90. package/lib/components/ui/DragHandleIcon.tsx +18 -0
  91. package/lib/components/ui/ExpandIcon.css +38 -0
  92. package/lib/components/ui/ExpandIcon.tsx +75 -0
  93. package/lib/components/ui/Input.css +59 -0
  94. package/lib/components/ui/Input.tsx +45 -0
  95. package/lib/components/ui/index.ts +15 -0
  96. package/lib/hooks/core/useDataGrid.ts +392 -0
  97. package/lib/hooks/core/useGridApiRef.ts +10 -0
  98. package/lib/hooks/features/useAggregation.ts +143 -0
  99. package/lib/hooks/features/useGridClipboard.ts +133 -0
  100. package/lib/hooks/features/useGridDataSource.ts +190 -0
  101. package/lib/hooks/features/useGridEditing.ts +100 -0
  102. package/lib/hooks/features/useGridSpanning.ts +349 -0
  103. package/lib/hooks/features/usePivot.ts +174 -0
  104. package/lib/hooks/useColumnReorder.ts +114 -0
  105. package/lib/hooks/useRowGrouping.ts +302 -0
  106. package/lib/hooks/useRowReorder.ts +83 -0
  107. package/lib/hooks/useTreeData.ts +323 -0
  108. package/lib/index.ts +106 -0
  109. package/lib/state/index.ts +17 -0
  110. package/lib/state/types.ts +48 -0
  111. package/lib/state/useGridStateStorage.ts +113 -0
  112. package/lib/styles/base/reset.css +21 -0
  113. package/lib/styles/base/variables.css +161 -0
  114. package/lib/styles/opengridx.css +27 -0
  115. package/lib/styles/opengridx.css.d.ts +4 -0
  116. package/lib/theme/DataGridThemeProvider.tsx +276 -0
  117. package/lib/theme/defaultStyles.ts +0 -0
  118. package/lib/theme/index.ts +21 -0
  119. package/lib/theme/presets.ts +148 -0
  120. package/lib/theme/types.ts +155 -0
  121. package/lib/types/index.ts +764 -0
  122. package/lib/utils/export/exportToExcelAdvanced.ts +516 -0
  123. package/lib/utils/export/index.ts +550 -0
  124. package/lib/utils/filtering/index.ts +240 -0
  125. package/lib/utils/pinning/index.ts +282 -0
  126. package/lib/utils/sorting/index.ts +55 -0
  127. package/lib/utils/virtualization/index.ts +106 -0
  128. package/lib/vite-env.d.ts +1 -0
  129. package/llms.txt +2 -1
  130. package/package.json +12 -4
@@ -0,0 +1,240 @@
1
+
2
+ import type { GridRowModel, GridFilterItem, GridFilterModel, GridFilterGroup } from '../../types';
3
+
4
+ export const FILTER_OPERATORS = {
5
+ contains: (value: any, filterValue: any): boolean => {
6
+ if (value == null || filterValue == null) return false;
7
+ return String(value).toLowerCase().includes(String(filterValue).toLowerCase());
8
+ },
9
+
10
+ equals: (value: any, filterValue: any): boolean => {
11
+ if (value == null && filterValue == null) return true;
12
+ if (value == null || filterValue == null) return false;
13
+ return String(value).toLowerCase() === String(filterValue).toLowerCase();
14
+ },
15
+
16
+ startsWith: (value: any, filterValue: any): boolean => {
17
+ if (value == null || filterValue == null) return false;
18
+ return String(value).toLowerCase().startsWith(String(filterValue).toLowerCase());
19
+ },
20
+
21
+ endsWith: (value: any, filterValue: any): boolean => {
22
+ if (value == null || filterValue == null) return false;
23
+ return String(value).toLowerCase().endsWith(String(filterValue).toLowerCase());
24
+ },
25
+
26
+ isEmpty: (value: any): boolean => {
27
+ return value == null || String(value).trim() === '';
28
+ },
29
+
30
+ isNotEmpty: (value: any): boolean => {
31
+ return value != null && String(value).trim() !== '';
32
+ },
33
+
34
+ '>': (value: any, filterValue: any): boolean => {
35
+ if (value == null || filterValue == null) return false;
36
+ const numValue = Number(value);
37
+ const numFilter = Number(filterValue);
38
+ if (isNaN(numValue) || isNaN(numFilter)) return false;
39
+ return numValue > numFilter;
40
+ },
41
+
42
+ '>=': (value: any, filterValue: any): boolean => {
43
+ if (value == null || filterValue == null) return false;
44
+ const numValue = Number(value);
45
+ const numFilter = Number(filterValue);
46
+ if (isNaN(numValue) || isNaN(numFilter)) return false;
47
+ return numValue >= numFilter;
48
+ },
49
+
50
+ '<': (value: any, filterValue: any): boolean => {
51
+ if (value == null || filterValue == null) return false;
52
+ const numValue = Number(value);
53
+ const numFilter = Number(filterValue);
54
+ if (isNaN(numValue) || isNaN(numFilter)) return false;
55
+ return numValue < numFilter;
56
+ },
57
+
58
+ '<=': (value: any, filterValue: any): boolean => {
59
+ if (value == null || filterValue == null) return false;
60
+ const numValue = Number(value);
61
+ const numFilter = Number(filterValue);
62
+ if (isNaN(numValue) || isNaN(numFilter)) return false;
63
+ return numValue <= numFilter;
64
+ },
65
+
66
+ '!=': (value: any, filterValue: any): boolean => {
67
+ if (filterValue == null || filterValue === '') return true;
68
+ // Numeric comparison if both are numbers, otherwise string
69
+ const numV = Number(value);
70
+ const numF = Number(filterValue);
71
+ if (!isNaN(numV) && !isNaN(numF)) return numV !== numF;
72
+ return String(value).toLowerCase() !== String(filterValue).toLowerCase();
73
+ },
74
+
75
+ isAnyOf: (value: any, filterValue: any[]): boolean => {
76
+ if (value == null || !Array.isArray(filterValue)) return false;
77
+ return filterValue.some(v =>
78
+ String(value).toLowerCase() === String(v).toLowerCase()
79
+ );
80
+ },
81
+
82
+ not: (value: any, filterValue: any): boolean => {
83
+ if (value == null && filterValue == null) return false;
84
+ if (value == null || filterValue == null) return true;
85
+ return String(value).toLowerCase() !== String(filterValue).toLowerCase();
86
+ },
87
+
88
+ // Used by boolean and singleSelect — case-insensitive string comparison
89
+ is: (value: any, filterValue: any): boolean => {
90
+ if (filterValue == null || filterValue === '') return true;
91
+ const rowStr = String(value).toLowerCase();
92
+ const filterStr = String(filterValue).toLowerCase();
93
+ return rowStr === filterStr;
94
+ },
95
+
96
+ '=': (value: any, filterValue: any): boolean => {
97
+ if (value == null || filterValue == null || filterValue === '') return false;
98
+ return Number(value) === Number(filterValue);
99
+ },
100
+ };
101
+
102
+ /**
103
+ * Apply a single filter item to a row
104
+ */
105
+ export function applyFilterItem<R extends GridRowModel>(
106
+ row: R,
107
+ filterItem: GridFilterItem
108
+ ): boolean {
109
+ const { field, operator, value } = filterItem;
110
+
111
+ // Get the value from the row
112
+ const rowValue = row[field];
113
+
114
+ // Get the operator function
115
+ const operatorFn = FILTER_OPERATORS[operator];
116
+ if (!operatorFn) {
117
+ console.warn(`Unknown filter operator: ${operator}`);
118
+ return true;
119
+ }
120
+
121
+ // Apply the operator
122
+ return operatorFn(rowValue, value);
123
+ }
124
+
125
+ /**
126
+ * Apply quick filter (global search across all fields)
127
+ */
128
+ export function applyQuickFilter<R extends GridRowModel>(
129
+ row: R,
130
+ quickFilterValues: string[]
131
+ ): boolean {
132
+ if (!quickFilterValues || quickFilterValues.length === 0) {
133
+ return true;
134
+ }
135
+
136
+ // Check if any quick filter value matches any field in the row
137
+ return quickFilterValues.every(filterValue => {
138
+ const searchTerm = filterValue.toLowerCase();
139
+ return Object.values(row).some(value => {
140
+ if (value == null) return false;
141
+ return String(value).toLowerCase().includes(searchTerm);
142
+ });
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Filter rows based on filter model
148
+ */
149
+ /**
150
+ * Check if a row matches the filter model
151
+ */
152
+ export function isRowMatchingFilter<R extends GridRowModel>(
153
+ row: R,
154
+ filterModel: GridFilterModel | GridFilterGroup // Accept Model or Group (similar structure)
155
+ ): boolean {
156
+ // Normalize input: GridFilterModel matches GridFilterGroup shape largely, but let's be safe
157
+
158
+ const items = (filterModel as any).items || [];
159
+ const logicOperator = (filterModel as any).logicOperator || 'and';
160
+ const quickFilterValues = (filterModel as any).quickFilterValues || [];
161
+
162
+ if (quickFilterValues.length > 0 && !applyQuickFilter(row, quickFilterValues)) {
163
+ return false;
164
+ }
165
+
166
+ if (items.length === 0) {
167
+ return true;
168
+ }
169
+
170
+ const evaluateItem = (item: GridFilterItem | GridFilterGroup): boolean => {
171
+ if ('logicOperator' in item && 'items' in item) {
172
+
173
+ const group = item as GridFilterGroup;
174
+ const groupItems = group.items;
175
+ if (groupItems.length === 0) return true;
176
+
177
+ if (group.logicOperator === 'and') {
178
+ return groupItems.every(evaluateItem);
179
+ } else {
180
+ return groupItems.some(evaluateItem);
181
+ }
182
+ } else {
183
+
184
+ return applyFilterItem(row, item as GridFilterItem);
185
+ }
186
+ };
187
+
188
+ if (logicOperator === 'and') {
189
+
190
+ return items.every(evaluateItem);
191
+ } else {
192
+
193
+ return items.some(evaluateItem);
194
+ }
195
+ }
196
+
197
+ export function filterRows<R extends GridRowModel>(
198
+ rows: R[],
199
+ filterModel: GridFilterModel
200
+ ): R[] {
201
+ if (!filterModel) return rows;
202
+
203
+ const { items = [], quickFilterValues = [] } = filterModel;
204
+
205
+ if (items.length === 0 && (!quickFilterValues || quickFilterValues.length === 0)) {
206
+ return rows;
207
+ }
208
+
209
+ return rows.filter(row => isRowMatchingFilter(row, filterModel));
210
+ }
211
+
212
+ export function getDefaultOperator(type?: string): string {
213
+ switch (type) {
214
+ case 'number':
215
+ return '=';
216
+ case 'date':
217
+ return 'is';
218
+ case 'boolean':
219
+ return 'is';
220
+ case 'singleSelect':
221
+ return 'isAnyOf';
222
+ default:
223
+ return 'contains';
224
+ }
225
+ }
226
+
227
+ export function getOperatorsForType(type?: string): string[] {
228
+ switch (type) {
229
+ case 'number':
230
+ return ['=', '!=', '>', '>=', '<', '<=', 'isEmpty', 'isNotEmpty'];
231
+ case 'date':
232
+ return ['is', 'not', 'after', 'onOrAfter', 'before', 'onOrBefore', 'isEmpty', 'isNotEmpty'];
233
+ case 'boolean':
234
+ return ['is'];
235
+ case 'singleSelect':
236
+ return ['isAnyOf', 'is', 'not'];
237
+ default:
238
+ return ['contains', 'equals', 'startsWith', 'endsWith', 'isEmpty', 'isNotEmpty'];
239
+ }
240
+ }
@@ -0,0 +1,282 @@
1
+
2
+ import type { GridColDef, GridRowModel, GridColumnPinning, GridRowPinning, GridPinnedPosition, GridRowId } from '../../types';
3
+
4
+ export function isColumnPinned(field: string, pinnedColumns?: GridColumnPinning): GridPinnedPosition | null {
5
+ if (!pinnedColumns) return null;
6
+
7
+ if (pinnedColumns.left?.includes(field)) {
8
+ return 'left';
9
+ }
10
+
11
+ if (pinnedColumns.right?.includes(field)) {
12
+ return 'right';
13
+ }
14
+
15
+ return null;
16
+ }
17
+
18
+ export function getPinnedColumnGroups<R extends GridRowModel = GridRowModel>(
19
+ columns: GridColDef<R>[],
20
+ pinnedColumns?: GridColumnPinning
21
+ ): {
22
+ left: GridColDef<R>[];
23
+ center: GridColDef<R>[];
24
+ right: GridColDef<R>[];
25
+ } {
26
+ const left: GridColDef<R>[] = [];
27
+ const center: GridColDef<R>[] = [];
28
+ const right: GridColDef<R>[] = [];
29
+
30
+ if (!pinnedColumns) {
31
+ return { left, center: columns, right };
32
+ }
33
+
34
+ columns.forEach(col => {
35
+ const pinnedPosition = isColumnPinned(col.field, pinnedColumns);
36
+
37
+ if (pinnedPosition === 'left') {
38
+ left.push(col);
39
+ } else if (pinnedPosition === 'right') {
40
+ right.push(col);
41
+ } else {
42
+ center.push(col);
43
+ }
44
+ });
45
+
46
+ if (pinnedColumns.left) {
47
+ left.sort((a, b) => {
48
+ const aIndex = pinnedColumns.left!.indexOf(a.field);
49
+ const bIndex = pinnedColumns.left!.indexOf(b.field);
50
+ return aIndex - bIndex;
51
+ });
52
+ }
53
+
54
+ if (pinnedColumns.right) {
55
+ right.sort((a, b) => {
56
+ const aIndex = pinnedColumns.right!.indexOf(a.field);
57
+ const bIndex = pinnedColumns.right!.indexOf(b.field);
58
+ return aIndex - bIndex;
59
+ });
60
+ }
61
+
62
+ return { left, center, right };
63
+ }
64
+
65
+ export function calculatePinnedPositions<R extends GridRowModel = GridRowModel>(
66
+ columns: GridColDef<R>[],
67
+ columnWidths: Record<string, number>,
68
+ pinnedColumns?: GridColumnPinning,
69
+ checkboxSelection?: boolean,
70
+ pinCheckboxColumn?: boolean,
71
+ hasDetailPanel?: boolean,
72
+ pinExpandColumn?: boolean,
73
+ rowReordering?: boolean
74
+ ): Record<string, number> {
75
+ const positions: Record<string, number> = {};
76
+
77
+ if (!pinnedColumns) return positions;
78
+
79
+ const groups = getPinnedColumnGroups(columns, pinnedColumns);
80
+
81
+ let leftOffset = 0;
82
+
83
+ if (rowReordering) {
84
+ leftOffset += 48;
85
+ }
86
+
87
+ if (hasDetailPanel && pinExpandColumn) {
88
+ leftOffset += 48;
89
+ }
90
+
91
+ if (checkboxSelection && pinCheckboxColumn) {
92
+ leftOffset += 48;
93
+ }
94
+
95
+ groups.left.forEach(col => {
96
+ positions[col.field] = leftOffset;
97
+ const width = columnWidths[col.field] ?? col.width ?? 100;
98
+ leftOffset += width;
99
+ });
100
+
101
+ let rightOffset = 0;
102
+
103
+ [...groups.right].reverse().forEach(col => {
104
+ const width = columnWidths[col.field] ?? col.width ?? 100;
105
+ positions[col.field] = rightOffset;
106
+ rightOffset += width;
107
+ });
108
+
109
+ return positions;
110
+ }
111
+
112
+ export function isColumnPinnable<R extends GridRowModel = GridRowModel>(
113
+ colDef: GridColDef<R>
114
+ ): boolean {
115
+ return colDef.pinnable !== false;
116
+ }
117
+
118
+ export function pinColumn(
119
+ field: string,
120
+ position: GridPinnedPosition,
121
+ currentPinning?: GridColumnPinning
122
+ ): GridColumnPinning {
123
+ const newPinning: GridColumnPinning = {
124
+ left: [...(currentPinning?.left || [])],
125
+ right: [...(currentPinning?.right || [])]
126
+ };
127
+
128
+ newPinning.left = (newPinning.left || []).filter(f => f !== field);
129
+ newPinning.right = (newPinning.right || []).filter(f => f !== field);
130
+
131
+ if (position === 'left') {
132
+ if (!newPinning.left) newPinning.left = [];
133
+ newPinning.left.push(field);
134
+ } else {
135
+ if (!newPinning.right) newPinning.right = [];
136
+ newPinning.right.push(field);
137
+ }
138
+
139
+ return newPinning;
140
+ }
141
+
142
+ export function unpinColumn(
143
+ field: string,
144
+ currentPinning?: GridColumnPinning
145
+ ): GridColumnPinning {
146
+ if (!currentPinning) return {};
147
+
148
+ return {
149
+ left: currentPinning.left?.filter(f => f !== field) || [],
150
+ right: currentPinning.right?.filter(f => f !== field) || []
151
+ };
152
+ }
153
+
154
+ export function getPinnedColumnsWidth(
155
+ columns: GridColDef[],
156
+ columnWidths: Record<string, number>,
157
+ position: GridPinnedPosition,
158
+ pinnedColumns?: GridColumnPinning
159
+ ): number {
160
+ if (!pinnedColumns) return 0;
161
+
162
+ const pinnedFields = position === 'left' ? pinnedColumns.left : pinnedColumns.right;
163
+ if (!pinnedFields) return 0;
164
+
165
+ return pinnedFields.reduce((total, field) => {
166
+ const col = columns.find(c => c.field === field);
167
+ if (!col) return total;
168
+
169
+ const width = columnWidths[field] ?? col.width ?? 100;
170
+ return total + width;
171
+ }, 0);
172
+ }
173
+
174
+ export function isRowPinned(
175
+ rowId: GridRowId,
176
+ pinnedRows?: GridRowPinning
177
+ ): 'top' | 'bottom' | null {
178
+ if (!pinnedRows) return null;
179
+
180
+ if (pinnedRows.top?.includes(rowId)) {
181
+ return 'top';
182
+ }
183
+
184
+ if (pinnedRows.bottom?.includes(rowId)) {
185
+ return 'bottom';
186
+ }
187
+
188
+ return null;
189
+ }
190
+
191
+ export function getPinnedRowGroups<R extends GridRowModel = GridRowModel>(
192
+ rows: R[],
193
+ pinnedRows?: GridRowPinning
194
+ ): {
195
+ top: R[];
196
+ center: R[];
197
+ bottom: R[];
198
+ } {
199
+ const top: R[] = [];
200
+ const center: R[] = [];
201
+ const bottom: R[] = [];
202
+
203
+ if (!pinnedRows) {
204
+ return { top, center: rows, bottom };
205
+ }
206
+
207
+ rows.forEach(row => {
208
+ const pinnedPosition = isRowPinned(row.id, pinnedRows);
209
+
210
+ if (pinnedPosition === 'top') {
211
+ top.push(row);
212
+ } else if (pinnedPosition === 'bottom') {
213
+ bottom.push(row);
214
+ } else {
215
+ center.push(row);
216
+ }
217
+ });
218
+
219
+ if (pinnedRows.top) {
220
+ top.sort((a, b) => {
221
+ const aIndex = pinnedRows.top!.indexOf(a.id);
222
+ const bIndex = pinnedRows.top!.indexOf(b.id);
223
+ return aIndex - bIndex;
224
+ });
225
+ }
226
+
227
+ if (pinnedRows.bottom) {
228
+ bottom.sort((a, b) => {
229
+ const aIndex = pinnedRows.bottom!.indexOf(a.id);
230
+ const bIndex = pinnedRows.bottom!.indexOf(b.id);
231
+ return aIndex - bIndex;
232
+ });
233
+ }
234
+
235
+ return { top, center, bottom };
236
+ }
237
+
238
+ export function pinRow(
239
+ rowId: GridRowId,
240
+ position: 'top' | 'bottom',
241
+ currentPinning?: GridRowPinning
242
+ ): GridRowPinning {
243
+ const newPinning: GridRowPinning = {
244
+ top: [...(currentPinning?.top || [])],
245
+ bottom: [...(currentPinning?.bottom || [])]
246
+ };
247
+
248
+ newPinning.top = (newPinning.top || []).filter(id => id !== rowId);
249
+ newPinning.bottom = (newPinning.bottom || []).filter(id => id !== rowId);
250
+
251
+ if (position === 'top') {
252
+ if (!newPinning.top) newPinning.top = [];
253
+ newPinning.top.push(rowId);
254
+ } else {
255
+ if (!newPinning.bottom) newPinning.bottom = [];
256
+ newPinning.bottom.push(rowId);
257
+ }
258
+
259
+ return newPinning;
260
+ }
261
+
262
+ export function unpinRow(
263
+ rowId: GridRowId,
264
+ currentPinning?: GridRowPinning
265
+ ): GridRowPinning {
266
+ if (!currentPinning) return {};
267
+
268
+ return {
269
+ top: currentPinning.top?.filter(id => id !== rowId) || [],
270
+ bottom: currentPinning.bottom?.filter(id => id !== rowId) || []
271
+ };
272
+ }
273
+
274
+ export function getPinnedRowsCount(
275
+ position: 'top' | 'bottom',
276
+ pinnedRows?: GridRowPinning
277
+ ): number {
278
+ if (!pinnedRows) return 0;
279
+
280
+ const pinnedIds = position === 'top' ? pinnedRows.top : pinnedRows.bottom;
281
+ return pinnedIds?.length || 0;
282
+ }
@@ -0,0 +1,55 @@
1
+
2
+ import type { GridRowModel, GridSortItem } from '../../types';
3
+
4
+ export function compareValues(a: any, b: any, direction: 'asc' | 'desc'): number {
5
+
6
+ if (a == null && b == null) return 0;
7
+ if (a == null) return direction === 'asc' ? 1 : -1;
8
+ if (b == null) return direction === 'asc' ? -1 : 1;
9
+
10
+ if (typeof a === 'number' && typeof b === 'number') {
11
+ return direction === 'asc' ? a - b : b - a;
12
+ }
13
+
14
+ if (a instanceof Date && b instanceof Date) {
15
+ return direction === 'asc'
16
+ ? a.getTime() - b.getTime()
17
+ : b.getTime() - a.getTime();
18
+ }
19
+
20
+ const aStr = String(a).toLowerCase();
21
+ const bStr = String(b).toLowerCase();
22
+
23
+ if (direction === 'asc') {
24
+ return aStr < bStr ? -1 : aStr > bStr ? 1 : 0;
25
+ } else {
26
+ return aStr > bStr ? -1 : aStr < bStr ? 1 : 0;
27
+ }
28
+ }
29
+
30
+ export function sortRows<R extends GridRowModel>(
31
+ rows: R[],
32
+ sortModel: GridSortItem[]
33
+ ): R[] {
34
+ if (sortModel.length === 0) {
35
+ return rows;
36
+ }
37
+
38
+ const sortedRows = [...rows];
39
+
40
+ sortedRows.sort((a, b) => {
41
+ for (const sortItem of sortModel) {
42
+ const aValue = a[sortItem.field];
43
+ const bValue = b[sortItem.field];
44
+
45
+ const comparison = compareValues(aValue, bValue, sortItem.sort);
46
+
47
+ if (comparison !== 0) {
48
+ return comparison;
49
+ }
50
+ }
51
+ return 0;
52
+ });
53
+
54
+ return sortedRows;
55
+ }
@@ -0,0 +1,106 @@
1
+
2
+ import { GridRenderContext } from '../../types';
3
+
4
+ export interface VirtualizationParams {
5
+ scrollTop: number;
6
+ scrollLeft: number;
7
+ viewportHeight: number;
8
+ viewportWidth: number;
9
+ rowHeight: number;
10
+ totalRows: number;
11
+ columnWidths: number[];
12
+ overscan?: number;
13
+ }
14
+
15
+ export interface VirtualizationResult {
16
+ renderContext: GridRenderContext;
17
+ offsetTop: number;
18
+ offsetLeft: number;
19
+ totalHeight: number;
20
+ totalWidth: number;
21
+ }
22
+
23
+ export function calculateVirtualization(params: VirtualizationParams): VirtualizationResult {
24
+ const {
25
+ scrollTop,
26
+ scrollLeft,
27
+ viewportHeight,
28
+ viewportWidth,
29
+ rowHeight,
30
+ totalRows,
31
+ columnWidths,
32
+ overscan = 3
33
+ } = params;
34
+
35
+ const firstVisibleRow = Math.floor(scrollTop / rowHeight);
36
+ const lastVisibleRow = Math.min(
37
+ totalRows - 1,
38
+ Math.ceil((scrollTop + viewportHeight) / rowHeight)
39
+ );
40
+
41
+ const firstRowIndex = Math.max(0, firstVisibleRow - overscan);
42
+ const lastRowIndex = Math.min(totalRows - 1, lastVisibleRow + overscan);
43
+
44
+ let accumulatedWidth = 0;
45
+ let firstColumnIndex = 0;
46
+ let lastColumnIndex = columnWidths.length - 1;
47
+ let offsetLeft = 0;
48
+
49
+ for (let i = 0; i < columnWidths.length; i++) {
50
+ if (accumulatedWidth + columnWidths[i] > scrollLeft) {
51
+ firstColumnIndex = Math.max(0, i - overscan);
52
+ break;
53
+ }
54
+ accumulatedWidth += columnWidths[i];
55
+ }
56
+
57
+ offsetLeft = columnWidths.slice(0, firstColumnIndex).reduce((sum, width) => sum + width, 0);
58
+
59
+ accumulatedWidth = offsetLeft;
60
+ for (let i = firstColumnIndex; i < columnWidths.length; i++) {
61
+ accumulatedWidth += columnWidths[i];
62
+ if (accumulatedWidth >= scrollLeft + viewportWidth) {
63
+ lastColumnIndex = Math.min(columnWidths.length - 1, i + overscan);
64
+ break;
65
+ }
66
+ }
67
+
68
+ const totalHeight = totalRows * rowHeight;
69
+ const totalWidth = columnWidths.reduce((sum, width) => sum + width, 0);
70
+ const offsetTop = firstRowIndex * rowHeight;
71
+
72
+ return {
73
+ renderContext: {
74
+ firstRowIndex,
75
+ lastRowIndex,
76
+ firstColumnIndex,
77
+ lastColumnIndex
78
+ },
79
+ offsetTop,
80
+ offsetLeft,
81
+ totalHeight,
82
+ totalWidth
83
+ };
84
+ }
85
+
86
+ export function getRowPosition(rowIndex: number, rowHeight: number): number {
87
+ return rowIndex * rowHeight;
88
+ }
89
+
90
+ export function getColumnPosition(colIndex: number, columnWidths: number[]): number {
91
+ return columnWidths.slice(0, colIndex).reduce((sum, width) => sum + width, 0);
92
+ }
93
+
94
+ export function isRowVisible(
95
+ rowIndex: number,
96
+ renderContext: GridRenderContext
97
+ ): boolean {
98
+ return rowIndex >= renderContext.firstRowIndex && rowIndex <= renderContext.lastRowIndex;
99
+ }
100
+
101
+ export function isColumnVisible(
102
+ colIndex: number,
103
+ renderContext: GridRenderContext
104
+ ): boolean {
105
+ return colIndex >= renderContext.firstColumnIndex && colIndex <= renderContext.lastColumnIndex;
106
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenGridX — AI Agent Context
2
2
  # Package: @opencorestack/opengridx
3
- # Version: 0.1.6
3
+ # Version: 0.1.7
4
4
  # Docs: https://opencorestack.github.io/OpenGridX/
5
5
  # This file is machine-readable. AI developers: start here.
6
6
 
@@ -234,6 +234,7 @@ export default function App() {
234
234
  | resizable | boolean (default true) | Enable resize handle |
235
235
  | hideable | boolean (default true) | Allow hiding via toolbar |
236
236
  | pinnable | boolean (default true) | Allow pinning via column menu |
237
+ | exportable | boolean (default true) | Include in CSV/Excel/JSON/Print exports |
237
238
  | disableColumnMenu | boolean | Hide the ⋮ column menu button |
238
239
 
239
240
  ---