argent-grid 0.2.0 → 0.3.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 (55) hide show
  1. package/AGENTS.md +70 -27
  2. package/e2e/advanced.spec.ts +1 -1
  3. package/e2e/benchmark.spec.ts +7 -7
  4. package/e2e/cell-renderers.spec.ts +152 -0
  5. package/e2e/debug-streaming.spec.ts +31 -0
  6. package/e2e/dnd.spec.ts +73 -0
  7. package/e2e/screenshots.spec.ts +1 -1
  8. package/e2e/visual.spec.ts +30 -9
  9. package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
  10. package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
  11. package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
  12. package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
  13. package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
  14. package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
  15. package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
  16. package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
  17. package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
  18. package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
  19. package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
  20. package/package.json +5 -5
  21. package/plan.md +30 -34
  22. package/src/lib/components/argent-grid.component.css +258 -549
  23. package/src/lib/components/argent-grid.component.html +272 -306
  24. package/src/lib/components/argent-grid.component.ts +585 -135
  25. package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
  26. package/src/lib/components/argent-grid.selection.spec.ts +2 -2
  27. package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
  28. package/src/lib/components/set-filter/set-filter.component.ts +7 -2
  29. package/src/lib/rendering/canvas-renderer.spec.ts +148 -1
  30. package/src/lib/rendering/canvas-renderer.ts +177 -286
  31. package/src/lib/rendering/render/cells.ts +122 -5
  32. package/src/lib/rendering/render/column-utils.ts +27 -5
  33. package/src/lib/rendering/render/hit-test.ts +6 -11
  34. package/src/lib/rendering/render/index.ts +15 -6
  35. package/src/lib/rendering/render/lines.ts +12 -6
  36. package/src/lib/rendering/render/primitives.ts +269 -7
  37. package/src/lib/rendering/render/types.ts +2 -1
  38. package/src/lib/rendering/render/walk.ts +39 -19
  39. package/src/lib/services/grid.service.spec.ts +76 -0
  40. package/src/lib/services/grid.service.ts +451 -114
  41. package/src/lib/themes/theme-quartz.ts +2 -2
  42. package/src/lib/types/ag-grid-types.ts +500 -0
  43. package/src/stories/Advanced.stories.ts +78 -17
  44. package/src/stories/ArgentGrid.stories.ts +50 -26
  45. package/src/stories/Benchmark.stories.ts +17 -15
  46. package/src/stories/CellRenderers.stories.ts +205 -31
  47. package/src/stories/Filtering.stories.ts +56 -16
  48. package/src/stories/Grouping.stories.ts +86 -13
  49. package/src/stories/Streaming.stories.ts +57 -0
  50. package/src/stories/Theming.stories.ts +23 -10
  51. package/src/stories/Tooltips.stories.ts +381 -0
  52. package/src/stories/benchmark-wrapper.component.ts +69 -29
  53. package/src/stories/story-utils.ts +88 -0
  54. package/src/stories/streaming-wrapper.component.ts +441 -0
  55. package/tsconfig.json +1 -0
@@ -1,4 +1,4 @@
1
- import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
1
+ import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
2
2
  import {
3
3
  AfterViewInit,
4
4
  ChangeDetectionStrategy,
@@ -18,6 +18,7 @@ import {
18
18
  import { Subject } from 'rxjs';
19
19
  import { takeUntil } from 'rxjs/operators';
20
20
  import { CanvasRenderer } from '../rendering/canvas-renderer';
21
+ import { isColumnVisible } from '../rendering/render/column-utils';
21
22
  import { GridService } from '../services/grid.service';
22
23
  import { applyThemeCSSVariables, convertThemeToGridTheme } from '../themes/theme-builder';
23
24
  import {
@@ -25,6 +26,7 @@ import {
25
26
  ColDef,
26
27
  ColGroupDef,
27
28
  Column,
29
+ ColumnGroup,
28
30
  DefaultMenuItem,
29
31
  GetContextMenuItemsParams,
30
32
  GridApi,
@@ -49,7 +51,7 @@ export class ArgentGridComponent<TData = any>
49
51
  @Input() theme: any;
50
52
  @Input() height = '500px';
51
53
  @Input() width = '100%';
52
- @Input() rowHeight = 32;
54
+ @Input() rowHeight?: number;
53
55
  @Input() rowSelection: RowSelectionOptions | 'single' | 'multiple' | undefined;
54
56
 
55
57
  @Output() gridReady = new EventEmitter<GridApi<TData>>();
@@ -66,17 +68,31 @@ export class ArgentGridComponent<TData = any>
66
68
  showOverlay = false;
67
69
  private viewportHeight = 500;
68
70
 
71
+ /**
72
+ * Returns the current effective row height, prioritizing grid options, then the input property, and defaulting to 32.
73
+ */
74
+ get effectiveRowHeight(): number {
75
+ return this.gridApi?.getGridOption('rowHeight') || this.rowHeight || 32;
76
+ }
77
+
78
+ /**
79
+ * Returns the current effective header height, prioritizing grid options, then defaulting to effectiveRowHeight.
80
+ */
81
+ get effectiveHeaderHeight(): number {
82
+ return this.gridApi?.getGridOption('headerHeight') || this.effectiveRowHeight;
83
+ }
84
+
69
85
  get totalHeight(): number {
70
86
  if (this.gridApi) return this.gridApi.getTotalHeight();
71
- return (this.rowData?.length || 0) * this.rowHeight;
87
+ return (this.rowData?.length || 0) * (this.rowHeight || 32);
72
88
  }
73
89
 
74
90
  get totalWidth(): number {
75
91
  if (!this.gridApi) return 0;
76
92
  return this.gridApi
77
93
  .getAllColumns()
78
- .filter((col) => col.visible)
79
- .reduce((sum, col) => sum + col.width, 0);
94
+ .filter((col) => isColumnVisible(col))
95
+ .reduce((sum, col) => sum + Math.floor(col.width || 150), 0);
80
96
  }
81
97
 
82
98
  // Selection state
@@ -90,7 +106,7 @@ export class ArgentGridComponent<TData = any>
90
106
  }
91
107
 
92
108
  hasHeaderCheckbox(col: Column): boolean {
93
- return col.colId === 'ag-Grid-SelectionColumn';
109
+ return !!col.headerCheckboxSelection;
94
110
  }
95
111
 
96
112
  trackByColumn(index: number, col: Column | ColDef<TData> | ColGroupDef<TData>): string {
@@ -111,6 +127,7 @@ export class ArgentGridComponent<TData = any>
111
127
  // Resizing state
112
128
  isResizing = false;
113
129
  resizeColumn: Column | null = null;
130
+ resizeItem: Column | ColumnGroup | null = null;
114
131
  private resizeStartX = 0;
115
132
  private resizeStartWidth = 0;
116
133
 
@@ -122,21 +139,35 @@ export class ArgentGridComponent<TData = any>
122
139
  sideBarVisible = false;
123
140
  activeToolPanel: 'columns' | 'filters' | null = null;
124
141
 
142
+ // Row Group Panel state
143
+ rowGroupPanelShow: 'always' | 'onlyWhenGrouping' | 'never' = 'never';
144
+ rowGroupColumns: Column[] = [];
145
+
125
146
  // Context Menu state
126
147
  activeContextMenu = false;
127
148
  contextMenuPosition = { x: 0, y: 0 };
128
149
  contextMenuItems: MenuItemDef[] = [];
129
150
  private contextMenuCell: { rowNode: IRowNode<TData>; column: Column } | null = null;
130
151
 
152
+ // Tooltip state
153
+ tooltipVisible = false;
154
+ tooltipText = '';
155
+ tooltipPosition = { x: 0, y: 0 };
156
+ private _tooltipTimer: any = null;
157
+
131
158
  // Set Filter
132
159
  activeSetFilter = false;
133
160
  setFilterPosition = { x: 0, y: 0 };
134
161
  setFilterValues: any[] = [];
162
+ setFilterSelectedValues: any[] | null = null;
135
163
  setFilterValueFormatter?: (value: any) => string;
136
164
  private activeSetFilterColumn: Column | null = null;
137
165
  private initialColumnDefs: (ColDef<TData> | ColGroupDef<TData>)[] | null = null;
138
166
 
139
- private gridApi!: GridApi<TData>;
167
+ public gridApi!: GridApi<TData>;
168
+ public isColumnVisible = isColumnVisible;
169
+ public Math = Math;
170
+ public scrollbarWidth = 0;
140
171
  private canvasRenderer!: CanvasRenderer;
141
172
  private destroy$ = new Subject<void>();
142
173
  private gridService = new GridService<TData>();
@@ -189,6 +220,16 @@ export class ArgentGridComponent<TData = any>
189
220
  ? convertThemeToGridTheme(changes.theme.currentValue)
190
221
  : undefined;
191
222
  this.canvasRenderer.setTheme(convertedTheme);
223
+
224
+ // Sync rowHeight and headerHeight to GridApi if provided by theme and NOT explicitly overridden by user
225
+ if (this.gridApi) {
226
+ if (convertedTheme?.rowHeight && this.rowHeight === undefined) {
227
+ this.gridApi.setGridOption('rowHeight', convertedTheme.rowHeight);
228
+ }
229
+ if (convertedTheme?.headerHeight) {
230
+ this.gridApi.setGridOption('headerHeight', convertedTheme.headerHeight);
231
+ }
232
+ }
192
233
  }
193
234
  }
194
235
  }
@@ -207,10 +248,9 @@ export class ArgentGridComponent<TData = any>
207
248
  this.canvasRenderer = new CanvasRenderer(
208
249
  this.canvasRef.nativeElement,
209
250
  this.gridApi,
210
- this.rowHeight,
251
+ this.effectiveRowHeight,
211
252
  convertedTheme
212
253
  );
213
-
214
254
  // Wire up cell editing callback
215
255
  this.canvasRenderer.onCellDoubleClick = (rowIndex, colId) => {
216
256
  this.startEditing(rowIndex, colId);
@@ -272,17 +312,34 @@ export class ArgentGridComponent<TData = any>
272
312
  if (this.viewportRef) {
273
313
  const rect = this.viewportRef.nativeElement.getBoundingClientRect();
274
314
  this.viewportHeight = rect.height || 500;
275
- this.canvasRenderer?.setViewportDimensions(rect.width, this.viewportHeight);
315
+ this.canvasRenderer?.setViewportDimensions(
316
+ rect.width,
317
+ this.viewportHeight,
318
+ this.scrollbarWidth
319
+ );
320
+
321
+ const updateScrollbar = () => {
322
+ const viewport = this.viewportRef?.nativeElement;
323
+ if (!viewport) return;
324
+ const newWidth = viewport.offsetWidth - viewport.clientWidth;
325
+ if (this.scrollbarWidth !== newWidth) {
326
+ this.scrollbarWidth = newWidth;
327
+ this._cdr.detectChanges();
328
+ }
329
+ };
276
330
 
277
331
  // Synchronize horizontal scroll with DOM header
278
332
  this.horizontalScrollListener = () => {
333
+ const viewport = this.viewportRef?.nativeElement;
334
+ if (!viewport) return;
335
+
336
+ updateScrollbar();
337
+
279
338
  if (this.headerScrollableRef) {
280
- this.headerScrollableRef.nativeElement.scrollLeft =
281
- this.viewportRef.nativeElement.scrollLeft;
339
+ this.headerScrollableRef.nativeElement.scrollLeft = viewport.scrollLeft;
282
340
  }
283
341
  if (this.headerScrollableFilterRef) {
284
- this.headerScrollableFilterRef.nativeElement.scrollLeft =
285
- this.viewportRef.nativeElement.scrollLeft;
342
+ this.headerScrollableFilterRef.nativeElement.scrollLeft = viewport.scrollLeft;
286
343
  }
287
344
  };
288
345
 
@@ -292,17 +349,35 @@ export class ArgentGridComponent<TData = any>
292
349
 
293
350
  // Add ResizeObserver to handle sidebar toggling and other size changes
294
351
  if (typeof ResizeObserver !== 'undefined') {
352
+ let lastWidth = 0;
353
+ let lastHeight = 0;
295
354
  this.resizeObserver = new ResizeObserver((entries) => {
296
355
  for (const entry of entries) {
297
356
  const { width, height } = entry.contentRect;
357
+ if (Math.abs(width - lastWidth) < 1 && Math.abs(height - lastHeight) < 1) continue;
358
+
359
+ lastWidth = width;
360
+ lastHeight = height;
298
361
  this.viewportHeight = height;
299
- this.canvasRenderer?.setViewportDimensions(width, height);
300
- this.canvasRenderer?.render();
362
+
363
+ // Only update scrollbar if dimensions actually changed
364
+ updateScrollbar();
365
+
366
+ this.canvasRenderer?.setViewportDimensions(width, height, this.scrollbarWidth);
367
+ // setViewportDimensions → updateCanvasSize already schedules a render.
301
368
  this._cdr.detectChanges();
302
369
  }
303
370
  });
304
371
  this.resizeObserver.observe(this.viewportRef.nativeElement);
305
372
  }
373
+
374
+ // Initial calculation
375
+ setTimeout(() => {
376
+ updateScrollbar();
377
+ if (this.canvasRenderer) {
378
+ this.canvasRenderer.render();
379
+ }
380
+ });
306
381
  }
307
382
  }
308
383
 
@@ -321,6 +396,7 @@ export class ArgentGridComponent<TData = any>
321
396
 
322
397
  this.gridApi?.destroy();
323
398
  this.canvasRenderer?.destroy();
399
+ this.onCanvasMouseLeave();
324
400
  }
325
401
 
326
402
  private initializeGrid(): void {
@@ -330,14 +406,35 @@ export class ArgentGridComponent<TData = any>
330
406
  options.rowSelection = this.rowSelection;
331
407
  }
332
408
 
409
+ // Prioritize explicit rowHeight input if provided
410
+ if (this.rowHeight !== undefined) {
411
+ options.rowHeight = this.rowHeight;
412
+ } else if (this.theme) {
413
+ // If no explicit rowHeight, but theme is provided, use theme's rowHeight
414
+ const convertedTheme = convertThemeToGridTheme(this.theme);
415
+ if (convertedTheme.rowHeight) {
416
+ options.rowHeight = convertedTheme.rowHeight;
417
+ }
418
+ if (convertedTheme.headerHeight && options.headerHeight === undefined) {
419
+ options.headerHeight = convertedTheme.headerHeight;
420
+ }
421
+ }
422
+
333
423
  // Initialize grid API
334
424
  this.gridApi = this.gridService.createApi(this.columnDefs, this.rowData, options);
335
425
 
426
+ // Initial state sync
427
+ this.rowGroupPanelShow = this.gridApi.getGridOption('rowGroupPanelShow') || 'never';
428
+ this.updateRowGroupColumns();
429
+
336
430
  // Listen for grid state changes from API (filters, sorts, options)
337
431
  this.gridService.gridStateChanged$.pipe(takeUntil(this.destroy$)).subscribe((event) => {
338
432
  if (event.type === 'optionChanged' && event.key === 'sideBar') {
339
433
  this.sideBarVisible = !!event.value;
340
434
  }
435
+ if (event.type === 'optionChanged' && event.key === 'rowGroupPanelShow') {
436
+ this.rowGroupPanelShow = event.value || 'never';
437
+ }
341
438
  if (event.type === 'selectionChanged') {
342
439
  this.updateSelectionState();
343
440
 
@@ -346,7 +443,24 @@ export class ArgentGridComponent<TData = any>
346
443
  if (this.canvasRenderer) {
347
444
  this.canvasRenderer.render(); // This calls markAllDirty and schedules render
348
445
  }
446
+ } else if (event.type === 'columnsChanged' || event.type === 'columnGroupExpanded') {
447
+ this.updateRowGroupColumns();
448
+ this.canvasRenderer?.render();
449
+ } else if (event.type === 'transactionApplied') {
450
+ // Efficient rendering: only mark changed rows as dirty instead of full redraw.
451
+ // Callers are responsible for throttling applyTransaction frequency (e.g. via RxJS).
452
+ const changedIndices = (event as any).changedRowIndices as number[] | undefined;
453
+ if (changedIndices && changedIndices.length > 0 && this.canvasRenderer) {
454
+ for (const rowIndex of changedIndices) {
455
+ this.canvasRenderer.invalidateRow(rowIndex);
456
+ }
457
+ } else {
458
+ this.canvasRenderer?.render();
459
+ }
349
460
  } else {
461
+ // All other state changes (sort, filter, rangeSelection, etc.) go through the
462
+ // rAF-coalesced scheduler. Multiple rapid events (e.g. rangeSelectionChanged
463
+ // on every mousemove) collapse into at most one pending frame.
350
464
  this.canvasRenderer?.render();
351
465
  }
352
466
  this._cdr.detectChanges();
@@ -378,7 +492,9 @@ export class ArgentGridComponent<TData = any>
378
492
 
379
493
  if (this.gridApi) {
380
494
  this.gridApi.setRowData(newData || []);
381
- this.canvasRenderer?.render();
495
+ if (this.canvasRenderer) {
496
+ this.canvasRenderer.render();
497
+ }
382
498
  }
383
499
 
384
500
  this.showOverlay = !newData || newData.length === 0;
@@ -406,37 +522,221 @@ export class ArgentGridComponent<TData = any>
406
522
  Object.keys(newOptions).forEach((key) => {
407
523
  this.gridApi.setGridOption(key as any, (newOptions as any)[key]);
408
524
  });
525
+
526
+ if (newOptions.rowGroupPanelShow) {
527
+ this.rowGroupPanelShow = newOptions.rowGroupPanelShow;
528
+ }
529
+
409
530
  this.canvasRenderer?.render();
410
531
  }
411
532
  this._cdr.detectChanges();
412
533
  }
413
534
 
535
+ getHeaderRows(): (Column | ColumnGroup)[][] {
536
+ if (!this.gridApi) return [];
537
+ return this.gridApi.getHeaderRows();
538
+ }
539
+
540
+ getPinnedLeftItems(row: (Column | ColumnGroup)[]): (Column | ColumnGroup)[] {
541
+ return row.filter((item) => item.pinned === 'left' && isColumnVisible(item));
542
+ }
543
+
544
+ getPinnedRightItems(row: (Column | ColumnGroup)[]): (Column | ColumnGroup)[] {
545
+ return row.filter((item) => item.pinned === 'right' && isColumnVisible(item));
546
+ }
547
+
548
+ getNonPinnedItems(row: (Column | ColumnGroup)[]): (Column | ColumnGroup)[] {
549
+ return row.filter((item) => !item.pinned && isColumnVisible(item));
550
+ }
551
+
552
+ getItemWidth(item: Column | ColumnGroup): number {
553
+ if ('children' in item) {
554
+ return item.children.reduce((sum, child) => {
555
+ if (isColumnVisible(child)) {
556
+ return sum + this.getItemWidth(child);
557
+ }
558
+ return sum;
559
+ }, 0);
560
+ }
561
+ return Math.floor(item.width || 150);
562
+ }
563
+
564
+ getItemRowSpan(item: Column | ColumnGroup, rowIndex: number): number {
565
+ if ('children' in item) {
566
+ return 1;
567
+ }
568
+ // Leaf node spans until the bottom
569
+ const totalRows = this.gridApi.getHeaderDepth();
570
+ return totalRows - rowIndex;
571
+ }
572
+
573
+ isColumnGroup(item: Column | ColumnGroup): item is ColumnGroup {
574
+ return 'children' in item;
575
+ }
576
+
577
+ trackByHeaderItem(
578
+ index: number,
579
+ entry: { item: Column | ColumnGroup; rowIndex: number }
580
+ ): string {
581
+ const item = entry.item;
582
+ return 'groupId' in item ? item.groupId : item.colId || index.toString();
583
+ }
584
+
585
+ getItemColSpan(item: Column | ColumnGroup): number {
586
+ if ('children' in item) {
587
+ return item.children.reduce((sum, child) => {
588
+ return sum + this.getItemColSpan(child);
589
+ }, 0);
590
+ }
591
+ return 1;
592
+ }
593
+
594
+ getScrollableHeaderWidth(): number {
595
+ return this.getNonPinnedColumns().reduce((sum, col) => sum + Math.floor(col.width || 150), 0);
596
+ }
597
+
598
+ getGridTemplateColumns(section: 'left' | 'right' | 'none'): string {
599
+ if (!this.gridApi) return '';
600
+ const allCols = this.gridApi.getAllColumns();
601
+ const sectionCols = allCols.filter((c) => {
602
+ if (section === 'left') return c.pinned === 'left';
603
+ if (section === 'right') return c.pinned === 'right';
604
+ return !c.pinned;
605
+ });
606
+
607
+ if (sectionCols.length === 0) return '';
608
+
609
+ const indices = sectionCols.map((c) => c.colIndex || 0);
610
+ const minIndex = Math.min(...indices);
611
+ const maxIndex = Math.max(...indices);
612
+
613
+ const widths = new Array(maxIndex - minIndex + 1).fill('0px');
614
+ sectionCols.forEach((c) => {
615
+ if (isColumnVisible(c)) {
616
+ widths[(c.colIndex || 0) - minIndex] = `${Math.floor(c.width || 150)}px`;
617
+ }
618
+ });
619
+
620
+ return widths.join(' ');
621
+ }
622
+
623
+ getColGridIndex(item: Column | ColumnGroup, section: 'left' | 'right' | 'none'): number {
624
+ if (!this.gridApi) return 1;
625
+ const allCols = this.gridApi.getAllColumns();
626
+ const sectionCols = allCols.filter((c) => {
627
+ if (section === 'left') return c.pinned === 'left';
628
+ if (section === 'right') return c.pinned === 'right';
629
+ return !c.pinned;
630
+ });
631
+ if (sectionCols.length === 0) return 1;
632
+ const minIndex = Math.min(...sectionCols.map((c) => c.colIndex || 0));
633
+
634
+ return (item.colIndex || 0) - minIndex + 1;
635
+ }
636
+
637
+ getScrollableColIndex(item: Column | ColumnGroup): number {
638
+ return this.getColGridIndex(item, 'none');
639
+ }
640
+
641
+ getRightPinnedColIndex(item: Column | ColumnGroup): number {
642
+ return this.getColGridIndex(item, 'right');
643
+ }
644
+
645
+ getLeftPinnedColIndex(item: Column | ColumnGroup): number {
646
+ return this.getColGridIndex(item, 'left');
647
+ }
648
+
649
+ getSectionHeaderItems(
650
+ section: 'left' | 'right' | 'none'
651
+ ): { item: Column | ColumnGroup; rowIndex: number }[] {
652
+ const items: { item: Column | ColumnGroup; rowIndex: number }[] = [];
653
+ const rows = this.getHeaderRows();
654
+ rows.forEach((row, i) => {
655
+ let rowItems: (Column | ColumnGroup)[] = [];
656
+ if (section === 'left') rowItems = this.getPinnedLeftItems(row);
657
+ else if (section === 'right') rowItems = this.getPinnedRightItems(row);
658
+ else rowItems = this.getNonPinnedItems(row);
659
+
660
+ rowItems.forEach((item) => items.push({ item, rowIndex: i }));
661
+ });
662
+ return items;
663
+ }
664
+
665
+ hasExpansionToggle(item: ColumnGroup): boolean {
666
+ return item.children.some(
667
+ (child) => child.columnGroupShow === 'open' || child.columnGroupShow === 'closed'
668
+ );
669
+ }
670
+
671
+ isRowGroupPanelVisible(): boolean {
672
+ const show = this.rowGroupPanelShow;
673
+ if (show === 'always') return true;
674
+ if (show === 'onlyWhenGrouping') {
675
+ return this.rowGroupColumns.length > 0;
676
+ }
677
+ return false;
678
+ }
679
+
680
+ trackByRowGroup(index: number, col: Column): string {
681
+ return col.colId || index.toString();
682
+ }
683
+
684
+ private updateRowGroupColumns(): void {
685
+ if (!this.gridApi) {
686
+ this.rowGroupColumns = [];
687
+ return;
688
+ }
689
+ const groupColIds = this.gridApi.getRowGroupColumns();
690
+ this.rowGroupColumns = this.gridApi
691
+ .getAllColumns()
692
+ .filter((col) => groupColIds.includes(col.colId));
693
+ }
694
+
695
+ getRowGroupColumns(): Column[] {
696
+ return this.rowGroupColumns;
697
+ }
698
+
699
+ onRowGroupDropped(event: CdkDragDrop<any[]>): void {
700
+ const col = event.item.data as Column;
701
+ if (col?.colId && col.colId !== 'ag-Grid-SelectionColumn') {
702
+ this.gridApi.addRowGroupColumn(col.colId);
703
+ }
704
+ }
705
+ removeRowGroup(col: Column): void {
706
+ this.gridApi.removeRowGroupColumn(col.colId);
707
+ }
708
+
709
+ toggleGroup(item: ColumnGroup, event: MouseEvent): void {
710
+ event.stopPropagation();
711
+ this.gridApi.toggleColumnGroup(item.groupId, !item.expanded);
712
+ }
713
+
414
714
  getColumnWidth(col: Column | ColDef<TData> | ColGroupDef<TData>): number {
415
715
  if ('children' in col) {
416
716
  // Column group - sum children widths
417
717
  return col.children.reduce((sum, child) => sum + this.getColumnWidth(child), 0);
418
718
  }
419
- return col.width || 150;
719
+ return Math.floor(col.width || 150);
420
720
  }
421
721
 
422
722
  getLeftPinnedColumns(): Column[] {
423
723
  if (!this.gridApi) return [];
424
724
  return this.gridApi.getAllColumns().filter((col) => {
425
- return col.visible && col.pinned === 'left';
725
+ return isColumnVisible(col) && col.pinned === 'left';
426
726
  });
427
727
  }
428
728
 
429
729
  getRightPinnedColumns(): Column[] {
430
730
  if (!this.gridApi) return [];
431
731
  return this.gridApi.getAllColumns().filter((col) => {
432
- return col.visible && col.pinned === 'right';
732
+ return isColumnVisible(col) && col.pinned === 'right';
433
733
  });
434
734
  }
435
735
 
436
736
  getNonPinnedColumns(): Column[] {
437
737
  if (!this.gridApi) return [];
438
738
  return this.gridApi.getAllColumns().filter((col) => {
439
- return col.visible && !col.pinned;
739
+ return isColumnVisible(col) && !col.pinned;
440
740
  });
441
741
  }
442
742
 
@@ -454,7 +754,7 @@ export class ArgentGridComponent<TData = any>
454
754
 
455
755
  // It's likely a Column object, look up its ColDef
456
756
  const colDef = this.getColumnDefForColumn(col as any);
457
- return colDef ? colDef.sortable !== false : true;
757
+ return colDef && this.isColDef(colDef) ? colDef.sortable !== false : true;
458
758
  }
459
759
 
460
760
  getHeaderName(col: Column | ColDef<TData> | ColGroupDef<TData>): string {
@@ -509,7 +809,47 @@ export class ArgentGridComponent<TData = any>
509
809
  if ((col as any).colId === 'ag-Grid-SelectionColumn') return false;
510
810
  if ('children' in col) return false;
511
811
  const colDef = this.getColumnDefForColumn(col as any);
512
- return colDef ? colDef.suppressHeaderMenuButton !== true : true;
812
+ return colDef && this.isColDef(colDef) ? colDef.suppressHeaderMenuButton !== true : true;
813
+ }
814
+
815
+ hasHeaderFilterButton(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
816
+ if ((col as any).colId === 'ag-Grid-SelectionColumn') return false;
817
+ if ('children' in col) return false;
818
+ const colDef = this.getColumnDefForColumn(col as any);
819
+ if (!colDef || !this.isColDef(colDef)) return false;
820
+ if (!colDef.filter || colDef.suppressHeaderFilterButton === true) return false;
821
+ // Don't show when floating filters are active — they already provide quick access
822
+ if (colDef.floatingFilter) return false;
823
+ return true;
824
+ }
825
+
826
+ isColumnFiltered(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
827
+ if (!this.gridApi) return false;
828
+ const column = col as Column;
829
+ const field = column.field || column.colId;
830
+ if (!field) return false;
831
+ const filterModel = this.gridApi.getFilterModel();
832
+ return !!(filterModel as any)[field];
833
+ }
834
+
835
+ onHeaderFilterClick(event: MouseEvent, col: Column | ColDef<TData> | ColGroupDef<TData>): void {
836
+ event.stopPropagation();
837
+ const column = col as Column;
838
+ const target = event.target as HTMLElement;
839
+ // Use closest ancestor that has a bounding rect useful for positioning
840
+ const iconEl = (target.closest('.argent-grid-header-filter-icon') as HTMLElement) ?? target;
841
+ const rect = iconEl.getBoundingClientRect();
842
+ const containerRect = this._elementRef.nativeElement.getBoundingClientRect();
843
+ const position = {
844
+ x: rect.left - containerRect.left,
845
+ y: rect.bottom - containerRect.top + 4,
846
+ };
847
+ const colDef = this.getColumnDefForColumn(column);
848
+ if (colDef && this.isColDef(colDef) && colDef.filter === 'set') {
849
+ this.openSetFilter(null, column, position);
850
+ } else {
851
+ this.openFilterPopup(null, column, position);
852
+ }
513
853
  }
514
854
 
515
855
  onHeaderMenuClick(event: MouseEvent, col: Column | ColDef<TData> | ColGroupDef<TData>): void {
@@ -523,21 +863,29 @@ export class ArgentGridComponent<TData = any>
523
863
  this.activeHeaderMenu = col;
524
864
  this.headerMenuItems = this.getHeaderMenuItems(col as Column);
525
865
 
526
- // Position menu below the icon using fixed (viewport) coordinates
866
+ // Position menu below the icon using coordinates relative to the grid container
527
867
  const target = event.target as HTMLElement;
528
868
  const rect = target.getBoundingClientRect();
869
+ const containerRect = this._elementRef.nativeElement.getBoundingClientRect();
870
+
871
+ // Align left edge of menu with left edge of icon
872
+ let x = rect.left - containerRect.left;
873
+ let y = rect.bottom - containerRect.top + 4;
529
874
 
530
- let x = rect.right - 200; // Align right, assuming menu width ~200px
531
- let y = rect.bottom + 4;
875
+ // Prevent menu from going off-container bounds
876
+ const containerWidth = this._elementRef.nativeElement.offsetWidth;
877
+ const containerHeight = this._elementRef.nativeElement.offsetHeight;
532
878
 
533
- // Prevent menu from going off-screen
879
+ // Assuming menu width is up to 200px for boundary checks
880
+ if (x + 200 > containerWidth) {
881
+ x = rect.right - containerRect.left - 200; // Flip to right-aligned if it overflows
882
+ }
534
883
  if (x < 0) x = 0;
535
- if (x + 200 > window.innerWidth) x = window.innerWidth - 200;
536
884
 
537
885
  // Check if menu would overflow bottom
538
886
  const estimatedHeight = this.headerMenuItems.length * 30 + 20;
539
- if (y + estimatedHeight > window.innerHeight) {
540
- y = Math.max(0, rect.top - estimatedHeight);
887
+ if (y + estimatedHeight > containerHeight) {
888
+ y = Math.max(0, rect.top - containerRect.top - estimatedHeight);
541
889
  }
542
890
 
543
891
  this.headerMenuPosition = { x, y };
@@ -569,7 +917,7 @@ export class ArgentGridComponent<TData = any>
569
917
 
570
918
  // 2. Filter items
571
919
  const colDef = this.getColumnDefForColumn(col);
572
- if (colDef && colDef.filter !== false) {
920
+ if (colDef && this.isColDef(colDef) && colDef.filter !== false) {
573
921
  const filterType = colDef.filter || 'text';
574
922
 
575
923
  if (filterType === 'set') {
@@ -621,6 +969,15 @@ export class ArgentGridComponent<TData = any>
621
969
  action: () => this.hideColumnMenu(),
622
970
  });
623
971
 
972
+ items.push({ name: '', action: () => {}, separator: true });
973
+
974
+ // 5. Columns panel (open sidebar)
975
+ items.push({
976
+ name: 'Columns',
977
+ icon: '☰',
978
+ action: () => this.openColumnsPanel(),
979
+ });
980
+
624
981
  return items;
625
982
  }
626
983
 
@@ -689,8 +1046,6 @@ export class ArgentGridComponent<TData = any>
689
1046
  'copyWithHeaders',
690
1047
  'separator',
691
1048
  'export',
692
- 'separator',
693
- 'resetColumns',
694
1049
  ]);
695
1050
  }
696
1051
 
@@ -698,13 +1053,17 @@ export class ArgentGridComponent<TData = any>
698
1053
 
699
1054
  this.activeContextMenu = true;
700
1055
 
701
- // Position menu at mouse coordinates (fixed/viewport)
702
- let x = event.clientX;
703
- let y = event.clientY;
1056
+ // Position menu at mouse coordinates relative to container
1057
+ const containerRect = this._elementRef.nativeElement.getBoundingClientRect();
1058
+ let x = event.clientX - containerRect.left;
1059
+ let y = event.clientY - containerRect.top;
704
1060
 
705
- // Prevent menu from going off-screen
706
- if (x + 200 > window.innerWidth) x = window.innerWidth - 200;
707
- if (y + 200 > window.innerHeight) y = window.innerHeight - 200;
1061
+ // Prevent menu from going off-container bounds
1062
+ const containerWidth = this._elementRef.nativeElement.offsetWidth;
1063
+ const containerHeight = this._elementRef.nativeElement.offsetHeight;
1064
+
1065
+ if (x + 200 > containerWidth) x = containerWidth - 200;
1066
+ if (y + 200 > containerHeight) y = containerHeight - 200;
708
1067
 
709
1068
  this.contextMenuPosition = { x, y };
710
1069
 
@@ -760,6 +1119,87 @@ export class ArgentGridComponent<TData = any>
760
1119
  }
761
1120
  }
762
1121
 
1122
+ // ============================================================================
1123
+ // TOOLTIP
1124
+ // ============================================================================
1125
+
1126
+ onCanvasMouseMove(event: MouseEvent): void {
1127
+ // Cancel any pending show and hide the current tooltip on every move
1128
+ if (this._tooltipTimer) {
1129
+ clearTimeout(this._tooltipTimer);
1130
+ this._tooltipTimer = null;
1131
+ }
1132
+ this.tooltipVisible = false;
1133
+
1134
+ if (!this.canvasRenderer) return;
1135
+
1136
+ const hit = this.canvasRenderer.getHitTestResult(event);
1137
+ const { rowIndex, columnIndex } = hit;
1138
+ if (rowIndex < 0 || columnIndex < 0) return;
1139
+
1140
+ const columns = this.canvasRenderer.getAllColumns();
1141
+ const column = columns[columnIndex];
1142
+ if (!column) return;
1143
+
1144
+ const text = this.computeTooltipText(rowIndex, column);
1145
+ if (!text) return;
1146
+
1147
+ this._tooltipTimer = setTimeout(() => {
1148
+ const containerRect = this._elementRef.nativeElement.getBoundingClientRect();
1149
+ let tx = event.clientX - containerRect.left + 14;
1150
+ let ty = event.clientY - containerRect.top + 14;
1151
+ // Keep within container bounds
1152
+ const cw = this._elementRef.nativeElement.offsetWidth;
1153
+ const ch = this._elementRef.nativeElement.offsetHeight;
1154
+ if (tx + 220 > cw) tx = Math.max(0, tx - 234);
1155
+ if (ty + 56 > ch) ty = Math.max(0, ty - 70);
1156
+ this.tooltipText = text;
1157
+ this.tooltipPosition = { x: tx, y: ty };
1158
+ this.tooltipVisible = true;
1159
+ this._cdr.detectChanges();
1160
+ }, 500);
1161
+ }
1162
+
1163
+ onCanvasMouseLeave(): void {
1164
+ if (this._tooltipTimer) {
1165
+ clearTimeout(this._tooltipTimer);
1166
+ this._tooltipTimer = null;
1167
+ }
1168
+ if (this.tooltipVisible) {
1169
+ this.tooltipVisible = false;
1170
+ this._cdr.detectChanges();
1171
+ }
1172
+ }
1173
+
1174
+ private computeTooltipText(rowIndex: number, column: Column): string | null {
1175
+ const colDef = this.getColumnDefForColumn(column) as ColDef<TData> | null;
1176
+ if (!colDef || !this.isColDef(colDef)) return null;
1177
+
1178
+ const rowNode = this.gridApi?.getDisplayedRowAtIndex(rowIndex);
1179
+ if (!rowNode?.data) return null;
1180
+
1181
+ // tooltipValueGetter takes priority (AG Grid parity)
1182
+ if (typeof colDef.tooltipValueGetter === 'function') {
1183
+ const val = colDef.field ? (rowNode.data as any)[colDef.field as string] : undefined;
1184
+ return (
1185
+ colDef.tooltipValueGetter({
1186
+ value: val,
1187
+ data: rowNode.data as TData,
1188
+ node: rowNode,
1189
+ column,
1190
+ }) ?? null
1191
+ );
1192
+ }
1193
+
1194
+ // tooltipField — show the value of that field
1195
+ if (colDef.tooltipField) {
1196
+ const val = (rowNode.data as any)[colDef.tooltipField as string];
1197
+ return val != null ? String(val) : null;
1198
+ }
1199
+
1200
+ return null;
1201
+ }
1202
+
763
1203
  closeContextMenu(): void {
764
1204
  this.activeContextMenu = false;
765
1205
  this.contextMenuCell = null;
@@ -789,6 +1229,14 @@ export class ArgentGridComponent<TData = any>
789
1229
  if (!field || !this.gridApi) return;
790
1230
 
791
1231
  this.setFilterValues = this.gridService.getUniqueValues(field as string);
1232
+
1233
+ // Restore previously selected values from the current filter model
1234
+ const existingFilter = this.gridApi.getFilterModel()[field as string] as any;
1235
+ this.setFilterSelectedValues =
1236
+ existingFilter?.filterType === 'set' && Array.isArray(existingFilter.values)
1237
+ ? existingFilter.values
1238
+ : null;
1239
+
792
1240
  const colDef = 'field' in col ? (col as ColDef<TData>) : null;
793
1241
  this.setFilterValueFormatter = colDef?.valueFormatter
794
1242
  ? (colDef.valueFormatter as any)
@@ -798,9 +1246,10 @@ export class ArgentGridComponent<TData = any>
798
1246
  this.setFilterPosition = position;
799
1247
  } else if (event) {
800
1248
  const rect = (event.target as HTMLElement).getBoundingClientRect();
1249
+ const containerRect = this._elementRef.nativeElement.getBoundingClientRect();
801
1250
  this.setFilterPosition = {
802
- x: rect.left,
803
- y: rect.bottom + 5,
1251
+ x: rect.left - containerRect.left,
1252
+ y: rect.bottom - containerRect.top + 5,
804
1253
  };
805
1254
  }
806
1255
 
@@ -908,7 +1357,8 @@ export class ArgentGridComponent<TData = any>
908
1357
  ): void {
909
1358
  this.activeFilterPopupColumn = col;
910
1359
  const colDef = this.getColumnDefForColumn(col);
911
- this.activeFilterPopupType = colDef?.filter === 'number' ? 'number' : 'text';
1360
+ this.activeFilterPopupType =
1361
+ colDef && this.isColDef(colDef) && colDef.filter === 'number' ? 'number' : 'text';
912
1362
 
913
1363
  // Initialize operator and values from current model or default
914
1364
  const model = this.gridApi?.getFilterModel()[col.colId] as any;
@@ -921,7 +1371,11 @@ export class ArgentGridComponent<TData = any>
921
1371
  this.filterPopupPosition = position;
922
1372
  } else if (event) {
923
1373
  const rect = (event.target as HTMLElement).getBoundingClientRect();
924
- this.filterPopupPosition = { x: rect.left, y: rect.bottom + 5 };
1374
+ const containerRect = this._elementRef.nativeElement.getBoundingClientRect();
1375
+ this.filterPopupPosition = {
1376
+ x: rect.left - containerRect.left,
1377
+ y: rect.bottom - containerRect.top + 5,
1378
+ };
925
1379
  }
926
1380
 
927
1381
  setTimeout(() => {
@@ -993,9 +1447,16 @@ export class ArgentGridComponent<TData = any>
993
1447
  this._cdr.detectChanges();
994
1448
  }
995
1449
 
1450
+ openColumnsPanel(): void {
1451
+ this.sideBarVisible = true;
1452
+ this.activeToolPanel = 'columns';
1453
+ this.closeHeaderMenu();
1454
+ this._cdr.detectChanges();
1455
+ }
1456
+
996
1457
  toggleColumnVisibility(col: Column): void {
997
1458
  const colDef = this.getColumnDefForColumn(col);
998
- if (colDef) {
1459
+ if (colDef && this.isColDef(colDef)) {
999
1460
  colDef.hide = col.visible; // Toggle
1000
1461
  this.initializeGrid(); // Re-initialize to handle visibility changes correctly
1001
1462
  this.canvasRenderer?.render();
@@ -1096,7 +1557,7 @@ export class ArgentGridComponent<TData = any>
1096
1557
 
1097
1558
  // Update original ColDef to ensure persistence
1098
1559
  const colDef = this.getColumnDefForColumn(col);
1099
- if (colDef) {
1560
+ if (colDef && this.isColDef(colDef)) {
1100
1561
  colDef.sort = direction;
1101
1562
  }
1102
1563
 
@@ -1113,7 +1574,7 @@ export class ArgentGridComponent<TData = any>
1113
1574
 
1114
1575
  // Update the original column definition
1115
1576
  const colDef = this.getColumnDefForColumn(col);
1116
- if (colDef) {
1577
+ if (colDef && this.isColDef(colDef)) {
1117
1578
  colDef.hide = true;
1118
1579
  }
1119
1580
 
@@ -1132,7 +1593,7 @@ export class ArgentGridComponent<TData = any>
1132
1593
 
1133
1594
  // Update the original column definition
1134
1595
  const colDef = this.getColumnDefForColumn(col);
1135
- if (colDef) {
1596
+ if (colDef && this.isColDef(colDef)) {
1136
1597
  colDef.pinned = pin as any;
1137
1598
  }
1138
1599
 
@@ -1143,76 +1604,43 @@ export class ArgentGridComponent<TData = any>
1143
1604
  this.closeHeaderMenu();
1144
1605
  }
1145
1606
 
1146
- onColumnDropped(event: CdkDragDrop<any[]>, pinType: 'left' | 'right' | 'none'): void {
1147
- if (!this.columnDefs) return;
1148
-
1149
- // Get current groups (using internal Columns)
1150
- const left = [...this.getLeftPinnedColumns()];
1151
- const center = [...this.getNonPinnedColumns()];
1152
- const right = [...this.getRightPinnedColumns()];
1607
+ onColumnDropped(event: CdkDragDrop<any>, pinned: 'left' | 'right' | 'none'): void {
1608
+ const col = event.item.data as Column;
1609
+ if (!col) return;
1153
1610
 
1154
- const containerMap: { [key: string]: any[] } = {
1155
- 'left-pinned': left,
1156
- scrollable: center,
1157
- 'right-pinned': right,
1158
- };
1159
-
1160
- const previousContainerData = containerMap[event.previousContainer.id];
1161
- const currentContainerData = containerMap[event.container.id];
1611
+ const targetPinned = pinned === 'none' ? false : pinned;
1162
1612
 
1163
- if (event.previousContainer === event.container) {
1164
- moveItemInArray(currentContainerData, event.previousIndex, event.currentIndex);
1165
- } else {
1166
- transferArrayItem(
1167
- previousContainerData,
1168
- currentContainerData,
1169
- event.previousIndex,
1170
- event.currentIndex
1171
- );
1172
-
1173
- // Update pinned state of the moved column in its original definition
1174
- const movedCol = currentContainerData[event.currentIndex] as Column;
1175
- const colDef = this.getColumnDefForColumn(movedCol);
1176
- if (colDef) {
1177
- colDef.pinned = pinType === 'none' ? null : (pinType as any);
1178
- }
1613
+ if (col.pinned !== targetPinned) {
1614
+ this.gridApi.setColumnPinned(col, targetPinned);
1179
1615
  }
1180
1616
 
1181
- // Map internal Columns back to their original ColDefs in the new order
1182
- const orderedVisibleColDefs: (ColDef<TData> | ColGroupDef<TData>)[] = [];
1183
- [...left, ...center, ...right].forEach((col) => {
1184
- const def = this.getColumnDefForColumn(col);
1185
- if (def) orderedVisibleColDefs.push(def);
1186
- });
1187
-
1188
- // Reconstruct full columnDefs array, maintaining hidden columns
1189
- const hidden = this.columnDefs.filter((c) => {
1190
- if ('children' in c) return false;
1191
- return (c as ColDef).hide;
1192
- });
1617
+ this.gridApi.moveColumn(col, event.currentIndex);
1193
1618
 
1194
- const newDefs = [...orderedVisibleColDefs, ...hidden];
1195
-
1196
- this.onColumnDefsChanged(newDefs);
1619
+ this.canvasRenderer?.render();
1620
+ this._cdr.detectChanges();
1197
1621
  }
1198
1622
 
1199
1623
  // --- Column Resizing Logic ---
1200
1624
 
1201
- isResizable(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
1202
- if ((col as any).colId === 'ag-Grid-SelectionColumn') return true;
1203
- if ('children' in col) return false;
1204
- const colDef = this.getColumnDefForColumn(col as any);
1205
- return colDef ? colDef.resizable !== false : true;
1625
+ isResizable(item: Column | ColumnGroup | ColDef<TData> | ColGroupDef<TData>): boolean {
1626
+ if ('children' in item) {
1627
+ return (item as any).children.some((child: any) => this.isResizable(child));
1628
+ }
1629
+ const colId = (item as any).colId || (item as any).field?.toString();
1630
+ if (colId === 'ag-Grid-SelectionColumn') return true;
1631
+
1632
+ const colDef = this.getColumnDefForColumn(item as any);
1633
+ return colDef && this.isColDef(colDef) ? colDef.resizable !== false : true;
1206
1634
  }
1207
1635
 
1208
- onResizeMouseDown(event: MouseEvent, col: Column): void {
1636
+ onResizeMouseDown(event: MouseEvent, item: Column | ColumnGroup): void {
1209
1637
  event.stopPropagation();
1210
1638
  event.preventDefault();
1211
1639
 
1212
1640
  this.isResizing = true;
1213
- this.resizeColumn = col;
1641
+ this.resizeItem = item;
1214
1642
  this.resizeStartX = event.clientX;
1215
- this.resizeStartWidth = col.width;
1643
+ this.resizeStartWidth = this.getItemWidth(item);
1216
1644
 
1217
1645
  const mouseMoveHandler = (e: MouseEvent) => this.onResizeMouseMove(e);
1218
1646
  const mouseUpHandler = () => {
@@ -1226,19 +1654,12 @@ export class ArgentGridComponent<TData = any>
1226
1654
  }
1227
1655
 
1228
1656
  private onResizeMouseMove(event: MouseEvent): void {
1229
- if (!this.isResizing || !this.resizeColumn) return;
1657
+ if (!this.isResizing || !this.resizeItem) return;
1230
1658
 
1231
1659
  const deltaX = event.clientX - this.resizeStartX;
1232
1660
  const newWidth = Math.max(20, this.resizeStartWidth + deltaX);
1233
1661
 
1234
- // Update internal column width
1235
- this.resizeColumn.width = newWidth;
1236
-
1237
- // Update original ColDef
1238
- const colDef = this.getColumnDefForColumn(this.resizeColumn);
1239
- if (colDef) {
1240
- colDef.width = newWidth;
1241
- }
1662
+ this.applyResize(this.resizeItem!, newWidth);
1242
1663
 
1243
1664
  // Force re-render
1244
1665
  this.canvasRenderer?.render();
@@ -1247,7 +1668,29 @@ export class ArgentGridComponent<TData = any>
1247
1668
 
1248
1669
  private onResizeMouseUp(): void {
1249
1670
  this.isResizing = false;
1250
- this.resizeColumn = null;
1671
+ this.resizeItem = null;
1672
+ }
1673
+
1674
+ private applyResize(item: Column | ColumnGroup, newWidth: number): void {
1675
+ if ('children' in item) {
1676
+ const currentWidth = this.getItemWidth(item);
1677
+ if (currentWidth === 0) return;
1678
+
1679
+ const ratio = newWidth / currentWidth;
1680
+ item.children.forEach((child) => {
1681
+ if (isColumnVisible(child)) {
1682
+ const childWidth = this.getItemWidth(child);
1683
+ this.applyResize(child, childWidth * ratio);
1684
+ }
1685
+ });
1686
+ } else {
1687
+ const finalWidth = Math.floor(newWidth);
1688
+ (item as Column).width = finalWidth;
1689
+ const colDef = this.getColumnDefForColumn(item as Column);
1690
+ if (colDef && this.isColDef(colDef)) {
1691
+ colDef.width = finalWidth;
1692
+ }
1693
+ }
1251
1694
  }
1252
1695
 
1253
1696
  // --- Floating Filter Logic ---
@@ -1297,7 +1740,7 @@ export class ArgentGridComponent<TData = any>
1297
1740
  private filterTimeout: any;
1298
1741
  onFloatingFilterInput(event: Event, col: Column | ColDef<TData> | ColGroupDef<TData>): void {
1299
1742
  const colDef = this.getColumnDefForColumn(col as any);
1300
- if (!colDef || 'children' in colDef) return;
1743
+ if (!colDef || !this.isColDef(colDef)) return;
1301
1744
 
1302
1745
  const input = event.target as HTMLInputElement;
1303
1746
  const value = input.value;
@@ -1354,7 +1797,7 @@ export class ArgentGridComponent<TData = any>
1354
1797
  input: HTMLInputElement
1355
1798
  ): void {
1356
1799
  const colDef = this.getColumnDefForColumn(col as any);
1357
- if (!colDef || 'children' in colDef) return;
1800
+ if (!colDef || !this.isColDef(colDef)) return;
1358
1801
 
1359
1802
  input.value = '';
1360
1803
  const colId = (col as any).colId || (col as any).field?.toString() || '';
@@ -1390,7 +1833,7 @@ export class ArgentGridComponent<TData = any>
1390
1833
 
1391
1834
  // Check if cell is editable
1392
1835
  const colDef = this.getColumnDefForColumn(column);
1393
- if (colDef && colDef.editable === false) return;
1836
+ if (colDef && this.isColDef(colDef) && colDef.editable === false) return;
1394
1837
 
1395
1838
  // If already editing another cell, stop it first
1396
1839
  if (this.isEditing) {
@@ -1404,18 +1847,18 @@ export class ArgentGridComponent<TData = any>
1404
1847
  this.editingValue = value !== null && value !== undefined ? String(value) : '';
1405
1848
 
1406
1849
  // Calculate editor position based on row and column
1407
- const columns = this.gridApi.getAllColumns().filter((c) => c.visible);
1850
+ const columns = this.gridApi.getAllColumns().filter((c) => isColumnVisible(c));
1408
1851
  let x = 0;
1409
1852
  for (const col of columns) {
1410
1853
  if (col.colId === colId) break;
1411
- x += col.width;
1854
+ x += Math.floor(col.width || 150);
1412
1855
  }
1413
1856
 
1414
1857
  this.editorPosition = {
1415
1858
  x: x - this.canvasRenderer.currentScrollLeft,
1416
- y: rowIndex * this.rowHeight - this.canvasRenderer.currentScrollTop,
1417
- width: column.width,
1418
- height: this.rowHeight,
1859
+ y: rowIndex * this.effectiveRowHeight - this.canvasRenderer.currentScrollTop,
1860
+ width: Math.floor(column.width),
1861
+ height: this.effectiveRowHeight,
1419
1862
  };
1420
1863
 
1421
1864
  this.isEditing = true;
@@ -1558,30 +2001,37 @@ export class ArgentGridComponent<TData = any>
1558
2001
  }
1559
2002
  }
1560
2003
  private getColumnDefForColumn(
1561
- column: Column | ColDef<TData> | ColGroupDef<TData>
1562
- ): ColDef<TData> | null {
2004
+ column: Column | ColumnGroup | ColDef<TData> | ColGroupDef<TData>
2005
+ ): ColDef<TData> | ColGroupDef<TData> | null {
1563
2006
  if (!this.columnDefs) return null;
1564
2007
 
1565
- const colId = (column as any).colId || (column as any).field?.toString();
2008
+ const colId =
2009
+ (column as any).colId || (column as any).field?.toString() || (column as any).groupId;
1566
2010
  if (!colId) return null;
1567
2011
 
1568
2012
  const defaultColDef = this.gridOptions?.defaultColDef || {};
1569
2013
 
1570
- for (const def of this.columnDefs) {
1571
- if ('children' in def) {
1572
- const found = def.children.find((c) => {
1573
- const cDef = c as ColDef;
1574
- return cDef.colId === colId || cDef.field?.toString() === colId;
1575
- });
1576
- if (found) return { ...defaultColDef, ...(found as ColDef<TData>) } as ColDef<TData>;
1577
- } else {
1578
- const cDef = def as ColDef;
1579
- if (cDef.colId === colId || cDef.field?.toString() === colId) {
1580
- return { ...defaultColDef, ...cDef } as ColDef<TData>;
2014
+ const findDef = (
2015
+ defs: (ColDef<TData> | ColGroupDef<TData>)[]
2016
+ ): ColDef<TData> | ColGroupDef<TData> | null => {
2017
+ for (const def of defs) {
2018
+ const defId = (def as any).colId || (def as any).field?.toString() || (def as any).groupId;
2019
+ if (defId === colId) {
2020
+ return 'children' in def ? def : ({ ...defaultColDef, ...def } as ColDef<TData>);
2021
+ }
2022
+ if ('children' in def) {
2023
+ const found = findDef(def.children);
2024
+ if (found) return found;
1581
2025
  }
1582
2026
  }
1583
- }
1584
- return null;
2027
+ return null;
2028
+ };
2029
+
2030
+ return findDef(this.columnDefs);
2031
+ }
2032
+
2033
+ private isColDef(def: any): def is ColDef<TData> {
2034
+ return def && !('children' in def);
1585
2035
  }
1586
2036
 
1587
2037
  onRowClick(rowIndex: number, event: MouseEvent): void {