argent-grid 0.1.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 (122) hide show
  1. package/.github/workflows/ci.yml +69 -0
  2. package/.github/workflows/pages.yml +6 -12
  3. package/.storybook/main.ts +20 -0
  4. package/.storybook/preview.ts +18 -0
  5. package/.storybook/tsconfig.json +24 -0
  6. package/AGENTS.md +70 -27
  7. package/README.md +51 -34
  8. package/angular.json +66 -0
  9. package/biome.json +66 -0
  10. package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
  11. package/docs/AG-GRID-COMPARISON.md +725 -0
  12. package/docs/CELL-RENDERER-GUIDE.md +241 -0
  13. package/docs/CONTEXT-MENU-GUIDE.md +371 -0
  14. package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
  15. package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
  16. package/docs/PERFORMANCE-REVIEW.md +571 -0
  17. package/docs/RESEARCH-STATUS.md +234 -0
  18. package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
  19. package/docs/STORYBOOK-REFACTOR.md +215 -0
  20. package/docs/STORYBOOK-STATUS.md +156 -0
  21. package/docs/TEST-COVERAGE-REPORT.md +276 -0
  22. package/docs/THEME-API-GUIDE.md +445 -0
  23. package/docs/THEME-API-PLAN.md +364 -0
  24. package/e2e/advanced.spec.ts +109 -0
  25. package/e2e/argentgrid.spec.ts +65 -0
  26. package/e2e/benchmark.spec.ts +52 -0
  27. package/e2e/cell-renderers.spec.ts +152 -0
  28. package/e2e/debug-streaming.spec.ts +31 -0
  29. package/e2e/dnd.spec.ts +73 -0
  30. package/e2e/screenshots.spec.ts +52 -0
  31. package/e2e/theming.spec.ts +35 -0
  32. package/e2e/visual.spec.ts +112 -0
  33. package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
  34. package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
  35. package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
  36. package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
  37. package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
  38. package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
  39. package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
  40. package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
  41. package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
  42. package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
  43. package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
  44. package/package.json +21 -7
  45. package/plan.md +56 -28
  46. package/playwright.config.ts +38 -0
  47. package/setup-vitest.ts +10 -13
  48. package/src/lib/argent-grid.module.ts +10 -12
  49. package/src/lib/components/argent-grid.component.css +281 -321
  50. package/src/lib/components/argent-grid.component.html +295 -207
  51. package/src/lib/components/argent-grid.component.spec.ts +120 -160
  52. package/src/lib/components/argent-grid.component.ts +1193 -290
  53. package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
  54. package/src/lib/components/argent-grid.selection.spec.ts +132 -0
  55. package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
  56. package/src/lib/components/set-filter/set-filter.component.ts +307 -0
  57. package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
  58. package/src/lib/directives/click-outside.directive.ts +19 -0
  59. package/src/lib/rendering/canvas-renderer.spec.ts +513 -0
  60. package/src/lib/rendering/canvas-renderer.ts +456 -452
  61. package/src/lib/rendering/live-data-handler.ts +110 -0
  62. package/src/lib/rendering/live-data-optimizations.ts +133 -0
  63. package/src/lib/rendering/render/blit.spec.ts +16 -27
  64. package/src/lib/rendering/render/blit.ts +48 -36
  65. package/src/lib/rendering/render/cells.spec.ts +132 -0
  66. package/src/lib/rendering/render/cells.ts +167 -28
  67. package/src/lib/rendering/render/column-utils.ts +95 -0
  68. package/src/lib/rendering/render/hit-test.ts +50 -0
  69. package/src/lib/rendering/render/index.ts +88 -76
  70. package/src/lib/rendering/render/lines.ts +53 -47
  71. package/src/lib/rendering/render/primitives.ts +423 -0
  72. package/src/lib/rendering/render/theme.spec.ts +8 -12
  73. package/src/lib/rendering/render/theme.ts +7 -10
  74. package/src/lib/rendering/render/types.ts +3 -2
  75. package/src/lib/rendering/render/walk.spec.ts +35 -38
  76. package/src/lib/rendering/render/walk.ts +94 -64
  77. package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
  78. package/src/lib/rendering/utils/damage-tracker.ts +6 -18
  79. package/src/lib/rendering/utils/index.ts +1 -1
  80. package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
  81. package/src/lib/services/grid.service.spec.ts +1241 -201
  82. package/src/lib/services/grid.service.ts +1204 -235
  83. package/src/lib/themes/parts/color-schemes.ts +132 -0
  84. package/src/lib/themes/parts/icon-sets.ts +258 -0
  85. package/src/lib/themes/theme-builder.ts +347 -0
  86. package/src/lib/themes/theme-quartz.ts +72 -0
  87. package/src/lib/themes/types.ts +238 -0
  88. package/src/lib/types/ag-grid-types.ts +573 -14
  89. package/src/public-api.ts +39 -9
  90. package/src/stories/Advanced.stories.ts +249 -0
  91. package/src/stories/ArgentGrid.stories.ts +301 -0
  92. package/src/stories/Benchmark.stories.ts +76 -0
  93. package/src/stories/CellRenderers.stories.ts +395 -0
  94. package/src/stories/Filtering.stories.ts +292 -0
  95. package/src/stories/Grouping.stories.ts +290 -0
  96. package/src/stories/Streaming.stories.ts +57 -0
  97. package/src/stories/Theming.stories.ts +137 -0
  98. package/src/stories/Tooltips.stories.ts +381 -0
  99. package/src/stories/benchmark-wrapper.component.ts +355 -0
  100. package/src/stories/story-utils.ts +88 -0
  101. package/src/stories/streaming-wrapper.component.ts +441 -0
  102. package/tsconfig.json +1 -0
  103. package/tsconfig.storybook.json +10 -0
  104. package/vitest.config.ts +9 -9
  105. package/demo-app/README.md +0 -70
  106. package/demo-app/angular.json +0 -78
  107. package/demo-app/e2e/benchmark.spec.ts +0 -53
  108. package/demo-app/e2e/demo-page.spec.ts +0 -77
  109. package/demo-app/e2e/grid-features.spec.ts +0 -269
  110. package/demo-app/package-lock.json +0 -14023
  111. package/demo-app/package.json +0 -36
  112. package/demo-app/playwright-test-menu.js +0 -19
  113. package/demo-app/playwright.config.ts +0 -23
  114. package/demo-app/src/app/app.component.ts +0 -10
  115. package/demo-app/src/app/app.config.ts +0 -13
  116. package/demo-app/src/app/app.routes.ts +0 -7
  117. package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
  118. package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
  119. package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
  120. package/demo-app/src/index.html +0 -19
  121. package/demo-app/src/main.ts +0 -6
  122. package/demo-app/tsconfig.json +0 -31
@@ -18,7 +18,7 @@ export const DEFAULT_THEME: GridTheme = {
18
18
  bgCell: '#ffffff',
19
19
  bgCellEven: '#f8f9fa',
20
20
  bgHeader: '#f8f9fa',
21
- bgSelection: '#e3f2fd',
21
+ bgSelection: '#d1e9ff', // Clearer blue for selection
22
22
  bgHover: '#f0f2f5',
23
23
  bgGroupRow: '#f5f5f5',
24
24
 
@@ -61,7 +61,7 @@ export const DARK_THEME: PartialTheme = {
61
61
 
62
62
  textCell: '#cccccc',
63
63
  textHeader: '#ffffff',
64
-
64
+
65
65
  borderColor: '#3c3c3c',
66
66
  headerBorderColor: '#3c3c3c',
67
67
  gridLineColor: '#3c3c3c',
@@ -78,13 +78,13 @@ export function mergeTheme(base: GridTheme, ...overrides: PartialTheme[]): GridT
78
78
  if (overrides.length === 0) return base;
79
79
 
80
80
  let result = { ...base };
81
-
81
+
82
82
  for (const override of overrides) {
83
83
  if (override) {
84
84
  result = { ...result, ...override };
85
85
  }
86
86
  }
87
-
87
+
88
88
  return result;
89
89
  }
90
90
 
@@ -113,7 +113,7 @@ export function getRowTheme(
113
113
  if (isSelected) {
114
114
  return { bgCell: baseTheme.bgSelection };
115
115
  }
116
-
116
+
117
117
  if (isHovered) {
118
118
  return { bgCell: baseTheme.bgHover };
119
119
  }
@@ -192,10 +192,7 @@ export function getThemePreset(name: string): PartialTheme {
192
192
  /**
193
193
  * Create a complete theme from a preset name and optional overrides
194
194
  */
195
- export function createTheme(
196
- presetName: string = 'default',
197
- overrides?: PartialTheme
198
- ): GridTheme {
195
+ export function createTheme(presetName: string = 'default', overrides?: PartialTheme): GridTheme {
199
196
  const preset = getThemePreset(presetName);
200
197
  return mergeTheme(DEFAULT_THEME, preset, overrides || {});
201
- }
198
+ }
@@ -4,7 +4,7 @@
4
4
  * Shared type definitions used across the rendering modules.
5
5
  */
6
6
 
7
- import { Column, IRowNode, ColDef, GridApi } from '../../types/ag-grid-types';
7
+ import { ColDef, Column, GridApi, IRowNode } from '../../types/ag-grid-types';
8
8
 
9
9
  // ============================================================================
10
10
  // CORE RENDERING TYPES
@@ -116,6 +116,7 @@ export interface CellDrawContext<TData = any> {
116
116
  isSelected: boolean;
117
117
  isHovered: boolean;
118
118
  isEvenRow: boolean;
119
+ api: GridApi<TData>;
119
120
  }
120
121
 
121
122
  /**
@@ -276,4 +277,4 @@ export interface GridMouseEvent {
276
277
  hitTest: HitTestResult;
277
278
  canvasX: number;
278
279
  canvasY: number;
279
- }
280
+ }
@@ -4,22 +4,21 @@
4
4
  * Tests for walkColumns, walkRows, walkCells, and related utilities.
5
5
  */
6
6
 
7
+ import { Column } from '../../types/ag-grid-types';
7
8
  import {
8
- walkColumns,
9
- getPositionedColumns,
10
- getPinnedWidths,
11
- walkRows,
12
- getVisibleRowRange,
13
- getRowY,
14
- walkCells,
9
+ calculateVisibleRange,
15
10
  getColumnAtX,
16
11
  getColumnIndex,
17
- getTotalColumnWidth,
12
+ getPinnedWidths,
13
+ getPositionedColumns,
18
14
  getRowAtY,
15
+ getRowY,
16
+ getTotalColumnWidth,
17
+ getVisibleRowRange,
19
18
  isRowVisible,
20
- calculateVisibleRange,
19
+ walkColumns,
20
+ walkRows,
21
21
  } from './walk';
22
- import { Column } from '../../types/ag-grid-types';
23
22
 
24
23
  // Helper to create mock columns
25
24
  function createMockColumn(overrides: Partial<Column> = {}): Column {
@@ -66,11 +65,11 @@ describe('Walker Functions', () => {
66
65
  describe('getVisibleRowRange', () => {
67
66
  it('should calculate visible row range with buffer', () => {
68
67
  const result = getVisibleRowRange(
69
- 100, // scrollTop
70
- 400, // viewportHeight
71
- 32, // rowHeight
68
+ 100, // scrollTop
69
+ 400, // viewportHeight
70
+ 32, // rowHeight
72
71
  1000, // totalRowCount
73
- 5 // buffer
72
+ 5 // buffer
74
73
  );
75
74
 
76
75
  // Starting row: floor(100/32) = 3, minus buffer = -2 -> clamped to 0
@@ -82,11 +81,11 @@ describe('Walker Functions', () => {
82
81
 
83
82
  it('should clamp start row to 0', () => {
84
83
  const result = getVisibleRowRange(
85
- 0, // scrollTop
86
- 400, // viewportHeight
87
- 32, // rowHeight
84
+ 0, // scrollTop
85
+ 400, // viewportHeight
86
+ 32, // rowHeight
88
87
  1000, // totalRowCount
89
- 5 // buffer
88
+ 5 // buffer
90
89
  );
91
90
 
92
91
  expect(result.startRow).toBe(0);
@@ -94,11 +93,11 @@ describe('Walker Functions', () => {
94
93
 
95
94
  it('should clamp end row to total row count', () => {
96
95
  const result = getVisibleRowRange(
97
- 0, // scrollTop
98
- 400, // viewportHeight
99
- 32, // rowHeight
100
- 10, // totalRowCount (small)
101
- 5 // buffer
96
+ 0, // scrollTop
97
+ 400, // viewportHeight
98
+ 32, // rowHeight
99
+ 10, // totalRowCount (small)
100
+ 5 // buffer
102
101
  );
103
102
 
104
103
  expect(result.endRow).toBeLessThanOrEqual(10);
@@ -116,7 +115,7 @@ describe('Walker Functions', () => {
116
115
  const visited: string[] = [];
117
116
  const xPositions: number[] = [];
118
117
 
119
- walkColumns(columns, 0, 400, 50, 75, (col, x, width, isPinned, pinSide) => {
118
+ walkColumns(columns, 0, 400, 50, 75, (col, x, _width, _isPinned, _pinSide) => {
120
119
  visited.push(col.colId);
121
120
  xPositions.push(x);
122
121
  });
@@ -141,7 +140,7 @@ describe('Walker Functions', () => {
141
140
 
142
141
  // Scroll so center1 and part of center2 are hidden
143
142
  const visited: string[] = [];
144
- walkColumns(columns, 150, 400, 50, 75, (col, x, width, isPinned) => {
143
+ walkColumns(columns, 150, 400, 50, 75, (col, _x, _width, _isPinned) => {
145
144
  visited.push(col.colId);
146
145
  });
147
146
 
@@ -156,7 +155,7 @@ describe('Walker Functions', () => {
156
155
  const visited: { row: number; y: number }[] = [];
157
156
  const getRowNode = (index: number) => ({ data: { id: index } }) as any;
158
157
 
159
- walkRows(0, 5, 0, 32, getRowNode, (rowIndex, y, height, rowNode) => {
158
+ walkRows(0, 5, 0, 32, getRowNode, (rowIndex, y, _height, _rowNode) => {
160
159
  visited.push({ row: rowIndex, y });
161
160
  });
162
161
 
@@ -170,7 +169,7 @@ describe('Walker Functions', () => {
170
169
  const visited: { row: number; y: number }[] = [];
171
170
  const getRowNode = (index: number) => ({ data: { id: index } }) as any;
172
171
 
173
- walkRows(10, 15, 320, 32, getRowNode, (rowIndex, y, height, rowNode) => {
172
+ walkRows(10, 15, 320, 32, getRowNode, (rowIndex, y, _height, _rowNode) => {
174
173
  visited.push({ row: rowIndex, y });
175
174
  });
176
175
 
@@ -255,9 +254,7 @@ describe('Walker Functions', () => {
255
254
  });
256
255
 
257
256
  it('should return null for position outside columns', () => {
258
- const columns: Column[] = [
259
- createMockColumn({ colId: 'col1', width: 100 }),
260
- ];
257
+ const columns: Column[] = [createMockColumn({ colId: 'col1', width: 100 })];
261
258
 
262
259
  const result = getColumnAtX(columns, 200, 0, 400);
263
260
  expect(result.column).toBeNull();
@@ -311,13 +308,13 @@ describe('Walker Functions', () => {
311
308
 
312
309
  const range = calculateVisibleRange(
313
310
  columns,
314
- 0, // scrollTop
315
- 0, // scrollLeft
316
- 400, // viewportWidth
317
- 400, // viewportHeight
318
- 32, // rowHeight
319
- 100, // totalRowCount
320
- 5 // buffer
311
+ 0, // scrollTop
312
+ 0, // scrollLeft
313
+ 400, // viewportWidth
314
+ 400, // viewportHeight
315
+ 32, // rowHeight
316
+ 100, // totalRowCount
317
+ 5 // buffer
321
318
  );
322
319
 
323
320
  expect(range.startRow).toBe(0);
@@ -357,4 +354,4 @@ describe('Walker Functions', () => {
357
354
  expect(getColumnIndex(columns, 'notfound')).toBe(-1);
358
355
  });
359
356
  });
360
- });
357
+ });
@@ -5,13 +5,13 @@
5
5
  * Based on Glide Data Grid's walker architecture.
6
6
  */
7
7
 
8
- import { Column, IRowNode, GridApi } from '../../types/ag-grid-types';
9
- import {
10
- ColumnWalkCallback,
11
- RowWalkCallback,
8
+ import { Column, GridApi, IRowNode } from '../../types/ag-grid-types';
9
+ import {
12
10
  CellWalkCallback,
11
+ ColumnWalkCallback,
13
12
  PositionedColumn,
14
- VisibleRange
13
+ RowWalkCallback,
14
+ VisibleRange,
15
15
  } from './types';
16
16
 
17
17
  // ============================================================================
@@ -25,47 +25,52 @@ import {
25
25
  export function walkColumns(
26
26
  columns: Column[],
27
27
  scrollX: number,
28
- viewportWidth: number,
28
+ viewportWidth: number, // Total width of the container
29
29
  leftPinnedWidth: number,
30
30
  rightPinnedWidth: number,
31
- callback: ColumnWalkCallback
31
+ callback: ColumnWalkCallback,
32
+ availableWidth?: number // Width excluding vertical scrollbar
32
33
  ): void {
33
- const leftPinned = columns.filter(c => c.pinned === 'left');
34
- const rightPinned = columns.filter(c => c.pinned === 'right');
35
- const centerColumns = columns.filter(c => !c.pinned);
34
+ const leftPinned = columns.filter((c) => c.pinned === 'left');
35
+ const rightPinned = columns.filter((c) => c.pinned === 'right');
36
+ const centerColumns = columns.filter((c) => !c.pinned);
37
+
38
+ const effectiveWidth = availableWidth ?? viewportWidth;
36
39
 
37
40
  // 1. Left pinned columns (no scroll offset)
38
41
  let x = 0;
39
42
  for (const col of leftPinned) {
40
- callback(col, x, col.width, true, 'left');
41
- x += col.width;
43
+ const width = Math.floor(col.width);
44
+ callback(col, x, width, true, 'left');
45
+ x += width;
42
46
  }
43
47
 
44
48
  // 2. Center columns (with scroll offset and clipping)
45
- const centerStartX = leftPinnedWidth;
46
- const centerEndX = viewportWidth - rightPinnedWidth;
47
- const centerWidth = centerEndX - centerStartX;
49
+ const centerStartX = Math.floor(leftPinnedWidth);
50
+ const centerEndX = Math.floor(effectiveWidth - rightPinnedWidth);
48
51
 
49
- x = leftPinnedWidth - scrollX;
52
+ x = centerStartX - scrollX;
50
53
  for (const col of centerColumns) {
54
+ const width = Math.floor(col.width);
51
55
  // Skip columns completely outside viewport
52
- if (x + col.width < centerStartX) {
53
- x += col.width;
56
+ if (x + width < centerStartX) {
57
+ x += width;
54
58
  continue;
55
59
  }
56
60
  if (x > centerEndX) {
57
61
  break; // Rest of columns are off-screen
58
62
  }
59
63
 
60
- callback(col, x, col.width, false);
61
- x += col.width;
64
+ callback(col, x, width, false);
65
+ x += width;
62
66
  }
63
67
 
64
68
  // 3. Right pinned columns (no scroll offset)
65
- x = viewportWidth - rightPinnedWidth;
69
+ x = centerEndX;
66
70
  for (const col of rightPinned) {
67
- callback(col, x, col.width, true, 'right');
68
- x += col.width;
71
+ const width = Math.floor(col.width);
72
+ callback(col, x, width, true, 'right');
73
+ x += width;
69
74
  }
70
75
  }
71
76
 
@@ -77,14 +82,21 @@ export function getPositionedColumns(
77
82
  scrollX: number,
78
83
  viewportWidth: number,
79
84
  leftPinnedWidth: number,
80
- rightPinnedWidth: number
85
+ rightPinnedWidth: number,
86
+ availableWidth?: number
81
87
  ): PositionedColumn[] {
82
88
  const result: PositionedColumn[] = [];
83
89
 
84
- walkColumns(columns, scrollX, viewportWidth, leftPinnedWidth, rightPinnedWidth,
90
+ walkColumns(
91
+ columns,
92
+ scrollX,
93
+ viewportWidth,
94
+ leftPinnedWidth,
95
+ rightPinnedWidth,
85
96
  (column, x, width, isPinned, pinSide) => {
86
97
  result.push({ column, x, width, isPinned, pinSide });
87
- }
98
+ },
99
+ availableWidth
88
100
  );
89
101
 
90
102
  return result;
@@ -95,12 +107,12 @@ export function getPositionedColumns(
95
107
  */
96
108
  export function getPinnedWidths(columns: Column[]): { left: number; right: number } {
97
109
  const left = columns
98
- .filter(c => c.pinned === 'left')
99
- .reduce((sum, c) => sum + c.width, 0);
100
-
110
+ .filter((c) => c.pinned === 'left')
111
+ .reduce((sum, c) => sum + Math.floor(c.width), 0);
112
+
101
113
  const right = columns
102
- .filter(c => c.pinned === 'right')
103
- .reduce((sum, c) => sum + c.width, 0);
114
+ .filter((c) => c.pinned === 'right')
115
+ .reduce((sum, c) => sum + Math.floor(c.width), 0);
104
116
 
105
117
  return { left, right };
106
118
  }
@@ -124,7 +136,7 @@ export function walkRows(
124
136
  for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) {
125
137
  const y = api ? api.getRowY(rowIndex) - scrollTop : rowIndex * rowHeight - scrollTop;
126
138
  const rowNode = getRowNode(rowIndex);
127
- const height = (api && rowNode) ? (rowNode.rowHeight || rowHeight) : rowHeight;
139
+ const height = api && rowNode ? rowNode.rowHeight || rowHeight : rowHeight;
128
140
  callback(rowIndex, y, height, rowNode);
129
141
  }
130
142
  }
@@ -141,20 +153,20 @@ export function getVisibleRowRange(
141
153
  api?: GridApi
142
154
  ): { startRow: number; endRow: number } {
143
155
  if (api) {
144
- const startRow = Math.max(0, api.getRowAtY(scrollTop) - buffer);
156
+ const startRow = Math.max(0, Math.min(totalRowCount - 1, api.getRowAtY(scrollTop)) - buffer);
145
157
  const endRow = Math.min(
146
158
  totalRowCount,
147
- api.getRowAtY(scrollTop + viewportHeight) + buffer + 1
159
+ api.getRowAtY(Math.max(0, scrollTop + viewportHeight)) + buffer + 1
148
160
  );
149
- return { startRow, endRow };
161
+ return {
162
+ startRow: Math.max(0, startRow),
163
+ endRow: Math.max(0, Math.min(totalRowCount, endRow)),
164
+ };
150
165
  }
151
166
 
152
167
  const startRow = Math.max(0, Math.floor(scrollTop / rowHeight) - buffer);
153
168
  const visibleRowCount = Math.ceil(viewportHeight / rowHeight);
154
- const endRow = Math.min(
155
- totalRowCount,
156
- startRow + visibleRowCount + buffer * 2
157
- );
169
+ const endRow = Math.min(totalRowCount, startRow + visibleRowCount + buffer * 2);
158
170
 
159
171
  return { startRow, endRow };
160
172
  }
@@ -180,7 +192,7 @@ export function walkCells(
180
192
  scrollX: number,
181
193
  scrollTop: number,
182
194
  viewportWidth: number,
183
- viewportHeight: number,
195
+ _viewportHeight: number,
184
196
  rowHeight: number,
185
197
  getRowNode: (index: number) => IRowNode | null,
186
198
  callback: CellWalkCallback
@@ -189,8 +201,13 @@ export function walkCells(
189
201
 
190
202
  // Walk columns for each row
191
203
  walkRows(startRow, endRow, scrollTop, rowHeight, getRowNode, (rowIndex, y, height, rowNode) => {
192
- walkColumns(columns, scrollX, viewportWidth, leftPinnedWidth, rightPinnedWidth,
193
- (column, x, width, isPinned) => {
204
+ walkColumns(
205
+ columns,
206
+ scrollX,
207
+ viewportWidth,
208
+ leftPinnedWidth,
209
+ rightPinnedWidth,
210
+ (column, x, width, _isPinned) => {
194
211
  callback(column, rowIndex, x, y, width, height, rowNode);
195
212
  }
196
213
  );
@@ -208,13 +225,15 @@ export function getColumnAtX(
208
225
  columns: Column[],
209
226
  x: number,
210
227
  scrollX: number,
211
- viewportWidth: number
228
+ viewportWidth: number,
229
+ availableWidth?: number
212
230
  ): { column: Column | null; index: number; localX: number } {
213
231
  const { left: leftPinnedWidth, right: rightPinnedWidth } = getPinnedWidths(columns);
232
+ const effectiveWidth = availableWidth ?? viewportWidth;
214
233
 
215
- const leftPinned = columns.filter(c => c.pinned === 'left');
216
- const rightPinned = columns.filter(c => c.pinned === 'right');
217
- const centerColumns = columns.filter(c => !c.pinned);
234
+ const leftPinned = columns.filter((c) => c.pinned === 'left');
235
+ const rightPinned = columns.filter((c) => c.pinned === 'right');
236
+ const centerColumns = columns.filter((c) => !c.pinned);
218
237
 
219
238
  // Check left pinned
220
239
  if (x < leftPinnedWidth) {
@@ -229,8 +248,9 @@ export function getColumnAtX(
229
248
  }
230
249
 
231
250
  // Check right pinned
232
- if (x > viewportWidth - rightPinnedWidth) {
233
- let colX = viewportWidth - rightPinnedWidth;
251
+ const centerEndX = Math.floor(effectiveWidth - rightPinnedWidth);
252
+ if (x > centerEndX) {
253
+ let colX = centerEndX;
234
254
  for (let i = 0; i < rightPinned.length; i++) {
235
255
  const col = rightPinned[i];
236
256
  if (x < colX + col.width) {
@@ -258,18 +278,18 @@ export function getColumnAtX(
258
278
  * Get column index in visible columns array
259
279
  */
260
280
  export function getColumnIndex(columns: Column[], colId: string): number {
261
- return columns.findIndex(c => c.colId === colId);
281
+ return columns.findIndex((c) => c.colId === colId);
262
282
  }
263
283
 
264
284
  /**
265
285
  * Calculate total width of columns
266
286
  */
267
287
  export function getTotalColumnWidth(columns: Column[]): number {
268
- return columns.reduce((sum, col) => sum + col.width, 0);
288
+ return columns.reduce((sum, col) => sum + Math.floor(col.width), 0);
269
289
  }
270
290
 
271
291
  // ============================================================================
272
- // ROW UTILITIES
292
+ // ROW UTILITIES
273
293
  // ============================================================================
274
294
 
275
295
  /**
@@ -291,7 +311,7 @@ export function isRowVisible(
291
311
  const y = rowIndex * rowHeight;
292
312
  const rowBottom = y + rowHeight;
293
313
  const viewportBottom = scrollTop + viewportHeight;
294
-
314
+
295
315
  return y < viewportBottom && rowBottom > scrollTop;
296
316
  }
297
317
 
@@ -310,19 +330,26 @@ export function calculateVisibleRange(
310
330
  viewportHeight: number,
311
331
  rowHeight: number,
312
332
  totalRowCount: number,
313
- rowBuffer: number = 5
333
+ rowBuffer: number = 5,
334
+ availableWidth?: number
314
335
  ): VisibleRange {
315
336
  const { startRow, endRow } = getVisibleRowRange(
316
- scrollTop, viewportHeight, rowHeight, totalRowCount, rowBuffer
337
+ scrollTop,
338
+ viewportHeight,
339
+ rowHeight,
340
+ totalRowCount,
341
+ rowBuffer
317
342
  );
318
343
 
344
+ const effectiveWidth = availableWidth ?? viewportWidth;
345
+
319
346
  // For columns, we just track indices
320
- const centerColumns = columns.filter(c => !c.pinned);
321
- const leftPinned = columns.filter(c => c.pinned === 'left');
322
- const rightPinned = columns.filter(c => c.pinned === 'right');
347
+ const centerColumns = columns.filter((c) => !c.pinned);
348
+ const leftPinned = columns.filter((c) => c.pinned === 'left');
349
+ const rightPinned = columns.filter((c) => c.pinned === 'right');
323
350
 
324
- const leftPinnedWidth = leftPinned.reduce((sum, c) => sum + c.width, 0);
325
- const rightPinnedWidth = rightPinned.reduce((sum, c) => sum + c.width, 0);
351
+ const leftPinnedWidth = leftPinned.reduce((sum, c) => sum + Math.floor(c.width), 0);
352
+ const rightPinnedWidth = rightPinned.reduce((sum, c) => sum + Math.floor(c.width), 0);
326
353
 
327
354
  // Find first and last visible center column
328
355
  let startColumnIndex = leftPinned.length;
@@ -331,21 +358,24 @@ export function calculateVisibleRange(
331
358
  let x = leftPinnedWidth - scrollLeft;
332
359
  for (let i = 0; i < centerColumns.length; i++) {
333
360
  const col = centerColumns[i];
334
- if (x + col.width > leftPinnedWidth) {
361
+ const width = Math.floor(col.width);
362
+ if (x + width > leftPinnedWidth) {
335
363
  startColumnIndex = leftPinned.length + i;
336
364
  break;
337
365
  }
338
- x += col.width;
366
+ x += width;
339
367
  }
340
368
 
341
369
  x = leftPinnedWidth - scrollLeft;
370
+ const centerEndX = Math.floor(effectiveWidth - rightPinnedWidth);
342
371
  for (let i = 0; i < centerColumns.length; i++) {
343
372
  const col = centerColumns[i];
344
- if (x > viewportWidth - rightPinnedWidth) {
373
+ const width = Math.floor(col.width);
374
+ if (x > centerEndX) {
345
375
  endColumnIndex = leftPinned.length + i;
346
376
  break;
347
377
  }
348
- x += col.width;
378
+ x += width;
349
379
  }
350
380
 
351
381
  // Add right pinned columns
@@ -357,4 +387,4 @@ export function calculateVisibleRange(
357
387
  startColumnIndex,
358
388
  endColumnIndex,
359
389
  };
360
- }
390
+ }
@@ -4,12 +4,8 @@
4
4
  * Tests for DamageTracker class and related utilities.
5
5
  */
6
6
 
7
- import {
8
- DamageTracker,
9
- getDirtyBounds,
10
- mergeRectangles,
11
- } from './damage-tracker';
12
7
  import { DirtyRegions, Rectangle } from '../render/types';
8
+ import { DamageTracker, getDirtyBounds, mergeRectangles } from './damage-tracker';
13
9
 
14
10
  describe('DamageTracker', () => {
15
11
  let tracker: DamageTracker;
@@ -221,7 +217,12 @@ describe('DamageTracker', () => {
221
217
  [3, 4],
222
218
  ]);
223
219
  const cells = tracker.getDirtyCells();
224
- expect(cells).toEqual(expect.arrayContaining([[1, 2], [3, 4]]));
220
+ expect(cells).toEqual(
221
+ expect.arrayContaining([
222
+ [1, 2],
223
+ [3, 4],
224
+ ])
225
+ );
225
226
  });
226
227
  });
227
228
 
@@ -441,4 +442,4 @@ describe('mergeRectangles', () => {
441
442
  it('should handle empty array', () => {
442
443
  expect(mergeRectangles([])).toEqual([]);
443
444
  });
444
- });
445
+ });
@@ -86,10 +86,7 @@ export class DamageTracker {
86
86
  /**
87
87
  * Mark dirty based on selection change
88
88
  */
89
- markSelectionChanged(
90
- oldSelection: Set<number>,
91
- newSelection: Set<number>
92
- ): void {
89
+ markSelectionChanged(oldSelection: Set<number>, newSelection: Set<number>): void {
93
90
  // Mark rows that changed selection state
94
91
  for (const row of oldSelection) {
95
92
  if (!newSelection.has(row)) {
@@ -186,7 +183,7 @@ export class DamageTracker {
186
183
  * Get dirty cells
187
184
  */
188
185
  getDirtyCells(): Array<[number, number]> {
189
- return Array.from(this.damagedCells).map(key => {
186
+ return Array.from(this.damagedCells).map((key) => {
190
187
  const [col, row] = key.split(',').map(Number);
191
188
  return [col, row];
192
189
  });
@@ -201,16 +198,8 @@ export class DamageTracker {
201
198
  * - If too many individual cells, promote to row/column
202
199
  * - If too many rows/columns, promote to full redraw
203
200
  */
204
- optimize(thresholds: {
205
- maxCells?: number;
206
- maxRows?: number;
207
- maxColumns?: number;
208
- } = {}): void {
209
- const {
210
- maxCells = 50,
211
- maxRows = 100,
212
- maxColumns = 20,
213
- } = thresholds;
201
+ optimize(thresholds: { maxCells?: number; maxRows?: number; maxColumns?: number } = {}): void {
202
+ const { maxCells = 50, maxRows = 100, maxColumns = 20 } = thresholds;
214
203
 
215
204
  // If too many cells, check if we should promote to rows
216
205
  if (this.damagedCells.size > maxCells) {
@@ -361,8 +350,7 @@ export function getDirtyBounds(
361
350
 
362
351
  const width = columnWidths[col] || 0;
363
352
 
364
- if (x + width >= 0 && x <= viewportWidth &&
365
- y + rowHeight >= 0 && y <= viewportHeight) {
353
+ if (x + width >= 0 && x <= viewportWidth && y + rowHeight >= 0 && y <= viewportHeight) {
366
354
  rects.push({
367
355
  x: Math.max(0, x),
368
356
  y: Math.max(0, y),
@@ -420,4 +408,4 @@ export function mergeRectangles(rects: Rectangle[]): Rectangle[] {
420
408
  }
421
409
 
422
410
  return merged;
423
- }
411
+ }
@@ -4,4 +4,4 @@
4
4
  * Exports all utility modules.
5
5
  */
6
6
 
7
- export * from './damage-tracker';
7
+ export * from './damage-tracker';