@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,516 @@
1
+ import React from 'react';
2
+ import { Checkbox } from '../ui/Checkbox';
3
+ import { ColumnResizeHandle } from '../ColumnResizeHandle/ColumnResizeHandle';
4
+ import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning, GridAggregationModel, GridColumnGroup, GridColumnGroupingModel } from '../../types';
5
+ import { isColumnPinned, calculatePinnedPositions } from '../../utils/pinning';
6
+ import { ColumnMenu } from './ColumnMenu';
7
+
8
+
9
+ function MenuIcon() {
10
+ return (
11
+ <svg focusable="false" aria-hidden="true" viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
12
+ <path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"></path>
13
+ </svg>
14
+ );
15
+ }
16
+
17
+ function SwapIcon() {
18
+ return (
19
+ <svg focusable="false" aria-hidden="true" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
20
+ <path d="M6.99 11L3 15l3.99 4v-3H14v-2H6.99v-3zM21 9l-3.99-4v3H10v2h7.01v3L21 9z"></path>
21
+ </svg>
22
+ );
23
+ }
24
+
25
+
26
+ // ─── Group header helpers ─────────────────────────────────────────────────────
27
+
28
+ /** Collect all leaf field names from a group node (recursive). */
29
+ function collectGroupFields(group: GridColumnGroup): string[] {
30
+ const out: string[] = [];
31
+ for (const child of group.children) {
32
+ if (typeof child === 'string') out.push(child);
33
+ else out.push(...collectGroupFields(child));
34
+ }
35
+ return out;
36
+ }
37
+
38
+ /** Max nesting depth of the group tree (1 = single level). */
39
+ function groupTreeDepth(groups: GridColumnGroupingModel): number {
40
+ let d = 0;
41
+ function walk(g: GridColumnGroup, depth: number) {
42
+ d = Math.max(d, depth);
43
+ for (const c of g.children) if (typeof c !== 'string') walk(c, depth + 1);
44
+ }
45
+ groups.forEach(g => walk(g, 1));
46
+ return d;
47
+ }
48
+
49
+ interface GroupCell { key: string; label: string; width: number; isGroup: boolean; }
50
+
51
+ /**
52
+ * Builds the flat array of GroupCell descriptors for one row (level).
53
+ * level 0 = top-most group row.
54
+ * allFields is the ORDERED list of all visible columns (including spacers).
55
+ */
56
+ function buildGroupRow(
57
+ groups: GridColumnGroupingModel,
58
+ allFields: { field: string; width: number }[],
59
+ level: number,
60
+ ): GroupCell[] {
61
+ // Map each field → the group that owns it at `level`
62
+ function findAtLevel(g: GridColumnGroup, field: string, cur: number): GridColumnGroup | null {
63
+ if (!collectGroupFields(g).includes(field)) return null;
64
+ if (cur === level) return g;
65
+ for (const child of g.children) {
66
+ if (typeof child !== 'string') {
67
+ const found = findAtLevel(child, field, cur + 1);
68
+ if (found) return found;
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+
74
+ const fieldGroup = new Map<string, GridColumnGroup | null>();
75
+ for (const f of allFields) {
76
+ let found: GridColumnGroup | null = null;
77
+ for (const g of groups) {
78
+ found = findAtLevel(g, f.field, 0);
79
+ if (found) break;
80
+ }
81
+ fieldGroup.set(f.field, found);
82
+ }
83
+
84
+ const cells: GroupCell[] = [];
85
+ let i = 0;
86
+ while (i < allFields.length) {
87
+ const item = allFields[i];
88
+ const group = fieldGroup.get(item.field) ?? null;
89
+ const w = item.width;
90
+
91
+ if (!group) {
92
+ cells.push({ key: `fg-${item.field}-${level}`, label: '', width: w, isGroup: false });
93
+ i++;
94
+ } else {
95
+ let totalW = 0;
96
+ let j = i;
97
+ while (j < allFields.length && fieldGroup.get(allFields[j].field) === group) {
98
+ totalW += allFields[j].width;
99
+ j++;
100
+ }
101
+ cells.push({ key: `${group.groupId}-${level}`, label: group.headerName, width: totalW, isGroup: true });
102
+ i = j;
103
+ }
104
+ }
105
+ return cells;
106
+ }
107
+
108
+ // ─── Props ────────────────────────────────────────────────────────────────────
109
+
110
+ export interface HeaderProps<R extends GridRowModel = GridRowModel> {
111
+ columns: GridColDef<R>[];
112
+ allColumns?: GridColDef<R>[];
113
+ columnGroupingModel?: GridColumnGroupingModel;
114
+ checkboxSelection?: boolean;
115
+ allSelected?: boolean;
116
+ someSelected?: boolean;
117
+ onSelectAll?: (isSelected: boolean) => void;
118
+ onSort?: (field: string, direction: GridSortDirection) => void;
119
+ sortModel?: Array<{ field: string; sort: 'asc' | 'desc' }>;
120
+ onColumnResize?: (field: string, newWidth: number) => void;
121
+ columnWidths?: Record<string, number>;
122
+ pinnedColumns?: GridColumnPinning;
123
+ hasDetailPanel?: boolean;
124
+ pinCheckboxColumn?: boolean;
125
+ pinExpandColumn?: boolean;
126
+ rowReordering?: boolean;
127
+ aggregationModel?: GridAggregationModel;
128
+
129
+ draggedColumn?: string | null;
130
+ dragOverColumn?: string | null;
131
+ onDragStart?: (field: string) => (event: React.DragEvent) => void;
132
+ onDragOver?: (field: string) => (event: React.DragEvent) => void;
133
+ onDragEnd?: () => void;
134
+ onDrop?: (targetField: string) => (event: React.DragEvent) => void;
135
+ focusedCell?: { id: string | number; field: string } | null;
136
+ onHeaderClick?: (field: string) => void;
137
+ onHideColumn?: (field: string) => void;
138
+ onPinColumn?: (field: string, side: 'left' | 'right' | null) => void;
139
+ onManageColumns?: () => void;
140
+ }
141
+
142
+ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps<R>) {
143
+ const {
144
+ columns,
145
+ allColumns,
146
+ columnGroupingModel,
147
+ checkboxSelection = false,
148
+ allSelected = false,
149
+ someSelected = false,
150
+ onSelectAll,
151
+ onSort,
152
+ sortModel = [],
153
+ onColumnResize,
154
+ columnWidths = {},
155
+ pinnedColumns,
156
+ hasDetailPanel = false,
157
+ pinCheckboxColumn = true,
158
+ pinExpandColumn = true,
159
+ rowReordering = false,
160
+ aggregationModel,
161
+ draggedColumn,
162
+ dragOverColumn,
163
+ onDragStart,
164
+ onDragOver,
165
+ onDragEnd,
166
+ onDrop,
167
+ focusedCell,
168
+ onHeaderClick,
169
+ onHideColumn,
170
+ onPinColumn,
171
+ onManageColumns,
172
+ } = props;
173
+
174
+ const cellRefs = React.useRef<Record<string, HTMLElement | null>>({});
175
+ const [menuOpenParams, setMenuOpenParams] = React.useState<{ colDef: GridColDef<R>; anchorEl: HTMLElement } | null>(null);
176
+
177
+ const handleMenuOpen = (colDef: GridColDef<R>) => (event: React.MouseEvent) => {
178
+ event.stopPropagation();
179
+ setMenuOpenParams({ colDef, anchorEl: event.currentTarget as HTMLElement });
180
+ };
181
+
182
+ const handleMenuClose = () => {
183
+ setMenuOpenParams(null);
184
+ };
185
+
186
+ React.useLayoutEffect(() => {
187
+ if (focusedCell?.id === 'HEADER') {
188
+ const element = cellRefs.current[focusedCell.field];
189
+ if (element) {
190
+ element.focus();
191
+ }
192
+ }
193
+ }, [focusedCell]);
194
+
195
+ const handleColumnClick = (colDef: GridColDef<R>) => () => {
196
+ if (colDef.sortable === false || !onSort) return;
197
+
198
+ const currentSort = sortModel.find(item => item.field === colDef.field);
199
+ let newDirection: GridSortDirection = 'asc';
200
+
201
+ if (currentSort) {
202
+ if (currentSort.sort === 'asc') {
203
+ newDirection = 'desc';
204
+ } else {
205
+ newDirection = null;
206
+ }
207
+ }
208
+
209
+ onSort(colDef.field, newDirection);
210
+ };
211
+
212
+ const getSortIcon = (field: string) => {
213
+ const sortItem = sortModel.find(item => item.field === field);
214
+ if (!sortItem) return null;
215
+
216
+ return (
217
+ <span className={`ogx__sort-icon ogx__sort-icon--${sortItem.sort}`} aria-hidden="true" />
218
+ );
219
+ };
220
+
221
+ const pinnedPositions = React.useMemo(() => {
222
+ return calculatePinnedPositions(
223
+ columns,
224
+ columnWidths,
225
+ pinnedColumns,
226
+ checkboxSelection,
227
+ pinCheckboxColumn,
228
+ hasDetailPanel,
229
+ pinExpandColumn,
230
+ rowReordering
231
+ );
232
+ }, [columns, columnWidths, pinnedColumns, checkboxSelection, pinCheckboxColumn, hasDetailPanel, pinExpandColumn, rowReordering]);
233
+
234
+ const groupDepth = columnGroupingModel ? groupTreeDepth(columnGroupingModel) : 0;
235
+
236
+ // All non-spacer fields in order (for group-span math) with their effective physical width
237
+ const allCols = allColumns ?? columns;
238
+ const allLeafFields = allCols
239
+ .filter(c => !(c as any).isSpacer)
240
+ .map(c => ({
241
+ field: c.field,
242
+ width: columnWidths[c.field] ?? c.width ?? 100
243
+ }));
244
+
245
+ // --- CHECKBOX / DETAIL / REORDER prefix width (for group filler) ---
246
+ const CHECKBOX_W = 48;
247
+ const REORDER_W = 48;
248
+ const EXPAND_W = 48;
249
+ const prefixWidth =
250
+ (rowReordering ? REORDER_W : 0) +
251
+ (hasDetailPanel ? EXPAND_W : 0) +
252
+ (checkboxSelection ? CHECKBOX_W : 0);
253
+
254
+ return (
255
+ // Outer sticky wrapper — both group rows AND column header row live here
256
+ // so they ALL stay sticky together and scroll horizontally as one unit.
257
+ <div className="ogx__header-wrap">
258
+ {/* ── Group header rows ── */}
259
+ {groupDepth > 0 && Array.from({ length: groupDepth }, (_, level) => {
260
+ const row = buildGroupRow(
261
+ columnGroupingModel!,
262
+ allLeafFields,
263
+ level,
264
+ );
265
+ return (
266
+ <div key={`ogx-grp-${level}`} className="ogx-col-group-row" role="row">
267
+ {/* Filler for checkbox / reorder / detailPanel prefix */}
268
+ {prefixWidth > 0 && (
269
+ <div
270
+ className="ogx-col-group-cell ogx-col-group-cell--filler"
271
+ style={{ width: prefixWidth, minWidth: prefixWidth, flexShrink: 0 }}
272
+ role="columnheader"
273
+ />
274
+ )}
275
+ {row.map(cell => (
276
+ <div
277
+ key={cell.key}
278
+ role="columnheader"
279
+ className={`ogx-col-group-cell ${cell.isGroup ? 'ogx-col-group-cell--group' : 'ogx-col-group-cell--filler'}`}
280
+ style={{ width: cell.width, minWidth: cell.width, flexShrink: 0 }}
281
+ title={cell.isGroup ? cell.label : undefined}
282
+ >
283
+ {cell.isGroup && (
284
+ <span className="ogx-col-group-cell__label">{cell.label}</span>
285
+ )}
286
+ </div>
287
+ ))}
288
+ </div>
289
+ );
290
+ })}
291
+
292
+ {/* ── Column header row ── */}
293
+ <div className="ogx__header" role="row">
294
+ { }
295
+ {rowReordering && (
296
+ <div
297
+ className="ogx__header-cell ogx__header-cell--pinned ogx__header-cell--pinned-left"
298
+ role="columnheader"
299
+ aria-label="Row reorder handle"
300
+ title="Drag to reorder rows"
301
+ style={{
302
+ minWidth: 48,
303
+ maxWidth: 48,
304
+ position: 'sticky',
305
+ left: 0,
306
+ zIndex: 4
307
+ }}
308
+ />
309
+ )}
310
+
311
+ { }
312
+ {hasDetailPanel && (
313
+ <div
314
+ className={`ogx__header-cell ogx__header-cell--expand ${pinExpandColumn ? 'ogx__header-cell--pinned ogx__header-cell--pinned-left' : ''
315
+ }`}
316
+ role="columnheader"
317
+ aria-label="Expand row details"
318
+ style={{
319
+ minWidth: 48,
320
+ maxWidth: 48,
321
+ position: pinExpandColumn ? 'sticky' : undefined,
322
+ left: pinExpandColumn ? (rowReordering ? 48 : 0) : undefined,
323
+ zIndex: pinExpandColumn ? 5 : undefined
324
+ }}
325
+ >
326
+ { }
327
+ </div>
328
+ )}
329
+
330
+ { }
331
+ {checkboxSelection && (
332
+ <div
333
+ className={`ogx__header-cell ogx__header-cell--checkbox ${pinCheckboxColumn ? 'ogx__header-cell--pinned ogx__header-cell--pinned-left' : ''
334
+ }`}
335
+ role="columnheader"
336
+ style={{
337
+ position: pinCheckboxColumn ? 'sticky' : undefined,
338
+ left: pinCheckboxColumn ? ((rowReordering ? 48 : 0) + (hasDetailPanel && pinExpandColumn ? 48 : 0)) : undefined,
339
+
340
+ }}
341
+ >
342
+ <Checkbox
343
+ id="ogx-select-all"
344
+ name="ogx-select-all"
345
+ checked={allSelected}
346
+ indeterminate={someSelected}
347
+ onChange={(e) => onSelectAll?.(e.target.checked)}
348
+ aria-label={allSelected ? 'Deselect all rows' : 'Select all rows'}
349
+ tabIndex={focusedCell?.id === 'HEADER' && focusedCell.field === '__checkbox_col__' ? 0 : -1}
350
+ onMouseDown={(e) => e.preventDefault()}
351
+ inputRef={(el) => { cellRefs.current['__checkbox_col__'] = el; }}
352
+ />
353
+ </div>
354
+ )}
355
+
356
+ {columns.map((colDef, index) => {
357
+ if ((colDef as any).isSpacer) {
358
+ return (
359
+ <div
360
+ key={colDef.field}
361
+ style={{ width: colDef.width, minWidth: colDef.width, flexShrink: 0 }}
362
+ aria-hidden="true"
363
+ />
364
+ );
365
+ }
366
+ const isSortable = colDef.sortable !== false;
367
+ const isSorted = sortModel.some(item => item.field === colDef.field);
368
+ const isResizable = colDef.resizable !== false;
369
+ const pinnedPosition = isColumnPinned(colDef.field, pinnedColumns);
370
+ const isPinned = pinnedPosition !== null;
371
+ const isDragging = draggedColumn === colDef.field;
372
+ const isDragOver = dragOverColumn === colDef.field;
373
+
374
+ const classNames = [
375
+ 'ogx__header-cell',
376
+ `ogx__header-cell--align-${colDef.headerAlign || colDef.align || 'left'}`,
377
+ isSortable && 'ogx__header-cell--sortable',
378
+ isSorted && 'ogx__header-cell--sorted',
379
+ isPinned && `ogx__header-cell--pinned`,
380
+ isPinned && `ogx__header-cell--pinned-${pinnedPosition}`,
381
+ isDragging && 'ogx__header-cell--dragging',
382
+ isDragOver && 'ogx__header-cell--drag-over',
383
+ (focusedCell?.id === 'HEADER' && focusedCell.field === colDef.field) && 'ogx__header-cell--focused',
384
+ colDef.headerClassName
385
+ ].filter(Boolean).join(' ');
386
+
387
+ const effectiveWidth = columnWidths[colDef.field] ?? colDef.width ?? 100;
388
+
389
+ const style: React.CSSProperties = {
390
+ width: effectiveWidth,
391
+ minWidth: colDef.minWidth,
392
+ maxWidth: colDef.maxWidth,
393
+ flexGrow: colDef.flex ?? 0,
394
+ flexShrink: 0,
395
+ flexBasis: colDef.flex ? 'auto' : 'auto',
396
+ boxSizing: 'border-box',
397
+ position: isPinned ? 'sticky' : 'relative',
398
+ zIndex: colDef.zIndex,
399
+ opacity: isDragging ? 0.5 : 1
400
+ };
401
+
402
+ if (isPinned && pinnedPosition) {
403
+ const offset = pinnedPositions[colDef.field];
404
+ if (pinnedPosition === 'left') {
405
+ style.left = offset;
406
+ } else {
407
+ style.right = offset;
408
+ }
409
+ }
410
+
411
+ const headerContent = colDef.renderHeader
412
+ ? colDef.renderHeader({ field: colDef.field, colDef: colDef as any, colIndex: index })
413
+ : colDef.headerName || colDef.field;
414
+
415
+ const handleHeaderClick = (e: React.MouseEvent) => {
416
+ if ((e.target as HTMLElement).closest('.ogx-column-resize-handle')) {
417
+ e.stopPropagation();
418
+ return;
419
+ }
420
+ onHeaderClick?.(colDef.field);
421
+ if (isSortable) {
422
+ handleColumnClick(colDef)();
423
+ }
424
+ };
425
+
426
+ const handleDragStart = onDragStart ? onDragStart(colDef.field) : undefined;
427
+ const handleDragOver = onDragOver ? onDragOver(colDef.field) : undefined;
428
+ const handleDrop = onDrop ? onDrop(colDef.field) : undefined;
429
+
430
+ return (
431
+ <div
432
+ key={colDef.field}
433
+ className={classNames}
434
+ style={style}
435
+ onClick={handleHeaderClick}
436
+ role="columnheader"
437
+ draggable={!!onDragStart}
438
+ onDragStart={handleDragStart}
439
+ onDragOver={handleDragOver}
440
+ onDragEnd={onDragEnd}
441
+ onDrop={handleDrop}
442
+ aria-sort={
443
+ isSortable
444
+ ? (isSorted
445
+ ? sortModel.find(item => item.field === colDef.field)?.sort === 'asc'
446
+ ? 'ascending'
447
+ : 'descending'
448
+ : 'none')
449
+ : undefined
450
+ }
451
+ aria-colindex={index + 1 + (checkboxSelection ? 1 : 0)}
452
+ data-field={colDef.field}
453
+ tabIndex={-1}
454
+ ref={(el) => { cellRefs.current[colDef.field] = el; }}
455
+ >
456
+ <div className="ogx__header-cell-content">
457
+ {isDragging && (
458
+ <div className="ogx__header-drag-icon">
459
+ <SwapIcon />
460
+ </div>
461
+ )}
462
+ <div className="ogx__header-cell-title-wrapper">
463
+ <span className="ogx__header-cell-title">
464
+ {headerContent}
465
+ </span>
466
+ {aggregationModel && aggregationModel[colDef.field] && (
467
+ <span className="ogx__header-cell-aggregation">
468
+ {aggregationModel[colDef.field]}
469
+ </span>
470
+ )}
471
+ </div>
472
+ {isSortable && getSortIcon(colDef.field)}
473
+ {!colDef.disableColumnMenu && (
474
+ <button
475
+ className="ogx__menu-icon-btn"
476
+ onClick={handleMenuOpen(colDef)}
477
+ aria-label={`Open column menu for ${colDef.headerName || colDef.field}`}
478
+ aria-haspopup="menu"
479
+ tabIndex={-1}
480
+ >
481
+ <MenuIcon />
482
+ </button>
483
+ )}
484
+ </div>
485
+
486
+ { }
487
+ {isResizable && onColumnResize && (
488
+ <ColumnResizeHandle
489
+ field={colDef.field}
490
+ currentWidth={effectiveWidth}
491
+ onResize={onColumnResize}
492
+ minWidth={colDef.minWidth}
493
+ maxWidth={colDef.maxWidth}
494
+ />
495
+ )}
496
+ </div>
497
+ );
498
+ })}
499
+
500
+ {menuOpenParams && (
501
+ <ColumnMenu
502
+ colDef={menuOpenParams.colDef}
503
+ sortModel={sortModel}
504
+ onSort={onSort}
505
+ onHide={onHideColumn}
506
+ onPin={onPinColumn}
507
+ pinnedColumns={pinnedColumns}
508
+ onManageColumns={onManageColumns}
509
+ onClose={handleMenuClose}
510
+ anchorEl={menuOpenParams.anchorEl}
511
+ />
512
+ )}
513
+ </div>{/* end .ogx__header */}
514
+ </div>
515
+ );
516
+ }
@@ -0,0 +1,112 @@
1
+ /* ─── List View Container ─────────────────────────────────────────────────── */
2
+ .ogx-list-view {
3
+ display: flex;
4
+ flex-direction: column;
5
+ width: 100%;
6
+ flex: 1;
7
+ min-height: 0;
8
+ overflow: hidden; /* rows scroll inside .ogx-list-view__rows, not here */
9
+ }
10
+
11
+ /* ─── Scrollable rows area ─────────────────────────────────────────────────── */
12
+ .ogx-list-view__rows {
13
+ flex: 1;
14
+ overflow-y: auto;
15
+ overflow-x: hidden;
16
+ min-height: 0;
17
+ }
18
+
19
+ /* ─── List View Row (the "card") ─────────────────────────────────────────── */
20
+ .ogx-list-view__row {
21
+ display: flex;
22
+ align-items: stretch;
23
+ border-bottom: var(--ogx-border-width-thin) solid var(--ogx-grid-border-color);
24
+ background: var(--ogx-grid-background);
25
+ cursor: pointer;
26
+ transition: background 0.1s;
27
+ min-height: 72px;
28
+ }
29
+
30
+ .ogx-list-view__row:hover {
31
+ background: var(--ogx-color-gray-50, #f8fafc);
32
+ }
33
+
34
+ .ogx-list-view__row--selected {
35
+ background: var(--ogx-selection-bg, rgba(59, 130, 246, 0.08));
36
+ }
37
+
38
+ .ogx-list-view__row--selected:hover {
39
+ background: var(--ogx-selection-hover-bg, rgba(59, 130, 246, 0.13));
40
+ }
41
+
42
+ /* ─── Checkbox column inside list row ────────────────────────────────────── */
43
+ .ogx-list-view__checkbox {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ width: 48px;
48
+ flex-shrink: 0;
49
+ border-right: var(--ogx-border-width-thin) solid var(--ogx-grid-border-color);
50
+ }
51
+
52
+ .ogx-list-view__checkbox input[type="checkbox"] {
53
+ width: 16px;
54
+ height: 16px;
55
+ cursor: pointer;
56
+ accent-color: var(--ogx-color-primary);
57
+ }
58
+
59
+ /* ─── Cell content area ───────────────────────────────────────────────────── */
60
+ .ogx-list-view__cell {
61
+ flex: 1;
62
+ padding: 12px 16px;
63
+ display: flex;
64
+ align-items: center;
65
+ min-width: 0;
66
+ font-family: var(--ogx-font-family);
67
+ font-size: var(--ogx-font-size-sm);
68
+ color: var(--ogx-grid-cell-text);
69
+ }
70
+
71
+ /* ─── Toolbar in list view mode ──────────────────────────────────────────── */
72
+ .ogx-list-view__toolbar {
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: space-between;
76
+ padding: 8px 12px;
77
+ border-bottom: var(--ogx-border-width-thin) solid var(--ogx-grid-border-color);
78
+ background: var(--ogx-grid-header-background);
79
+ font-family: var(--ogx-font-family);
80
+ font-size: var(--ogx-font-size-xs);
81
+ color: var(--ogx-grid-header-text);
82
+ flex-shrink: 0;
83
+ }
84
+
85
+ /* ─── Empty state ────────────────────────────────────────────────────────── */
86
+ .ogx-list-view__empty {
87
+ display: flex;
88
+ flex-direction: column;
89
+ align-items: center;
90
+ justify-content: center;
91
+ padding: 48px 24px;
92
+ gap: 8px;
93
+ font-family: var(--ogx-font-family);
94
+ font-size: var(--ogx-font-size-sm);
95
+ color: var(--ogx-text-secondary, #9ca3af);
96
+ }
97
+
98
+ /* ─── Responsive: auto switch at ≤768px when using ogx--list-auto class ─── */
99
+ @media (max-width: 768px) {
100
+ .ogx--list-auto .ogx__viewport {
101
+ display: none;
102
+ }
103
+ .ogx--list-auto .ogx-list-view {
104
+ display: flex;
105
+ }
106
+ }
107
+
108
+ @media (min-width: 769px) {
109
+ .ogx--list-auto .ogx-list-view {
110
+ display: none;
111
+ }
112
+ }
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import type { GridRowModel, GridRowId, GridListViewColDef, GridRenderCellParams } from '../../types';
3
+
4
+ export interface ListViewRowProps<R extends GridRowModel = GridRowModel> {
5
+ row: R;
6
+ rowIndex: number;
7
+ listViewColumn: GridListViewColDef<R>;
8
+ isSelected?: boolean;
9
+ checkboxSelection?: boolean;
10
+ rowHeight?: number;
11
+ onRowClick?: (row: R) => void;
12
+ onSelectionChange?: (rowId: GridRowId, isSelected: boolean) => void;
13
+ }
14
+
15
+ export function ListViewRow<R extends GridRowModel = GridRowModel>({
16
+ row,
17
+ rowIndex,
18
+ listViewColumn,
19
+ isSelected = false,
20
+ checkboxSelection = false,
21
+ rowHeight,
22
+ onRowClick,
23
+ onSelectionChange,
24
+ }: ListViewRowProps<R>) {
25
+ const params: GridRenderCellParams<R> = {
26
+ row,
27
+ value: undefined,
28
+ field: listViewColumn.field,
29
+ colDef: { field: listViewColumn.field } as any,
30
+ rowIndex,
31
+ colIndex: 0,
32
+ };
33
+
34
+ const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
35
+ e.stopPropagation();
36
+ onSelectionChange?.(row.id, e.target.checked);
37
+ };
38
+
39
+ return (
40
+ <div
41
+ className={`ogx-list-view__row${isSelected ? ' ogx-list-view__row--selected' : ''}`}
42
+ style={rowHeight ? { minHeight: rowHeight } : undefined}
43
+ onClick={() => onRowClick?.(row)}
44
+ role="row"
45
+ aria-rowindex={rowIndex + 2}
46
+ aria-selected={isSelected}
47
+ >
48
+ {checkboxSelection && (
49
+ <div className="ogx-list-view__checkbox" onClick={(e) => e.stopPropagation()}>
50
+ <input
51
+ type="checkbox"
52
+ checked={isSelected}
53
+ onChange={handleCheckboxChange}
54
+ aria-label={`Select row ${row.id}`}
55
+ />
56
+ </div>
57
+ )}
58
+ <div className="ogx-list-view__cell" role="gridcell">
59
+ {listViewColumn.renderCell(params)}
60
+ </div>
61
+ </div>
62
+ );
63
+ }