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
@@ -0,0 +1,355 @@
1
+ import { CommonModule } from '@angular/common';
2
+ import { AfterViewInit, Component, Input, OnDestroy, ViewChild } from '@angular/core';
3
+ import { ArgentGridComponent, ArgentGridModule, ColDef, GridApi, themeQuartz } from '../public-api';
4
+ import {
5
+ departmentValueFormatter,
6
+ locationValueFormatter,
7
+ roleValueFormatter,
8
+ STORY_DEPARTMENTS,
9
+ STORY_LOCATIONS,
10
+ STORY_ROLES,
11
+ } from './story-utils';
12
+
13
+ interface Employee {
14
+ id: number;
15
+ name: string;
16
+ department: string;
17
+ role: string;
18
+ salary: number;
19
+ location: string;
20
+ }
21
+
22
+ @Component({
23
+ selector: 'app-benchmark-wrapper',
24
+ standalone: true,
25
+ imports: [CommonModule, ArgentGridModule],
26
+ template: `
27
+ <div class="benchmark-container">
28
+ <div class="controls">
29
+ <button (click)="runBenchmark()" [disabled]="isRunning">
30
+ {{ isRunning ? 'Running...' : 'Run Benchmark' }}
31
+ </button>
32
+ <button (click)="reloadData()">Reload Data</button>
33
+ <span class="row-count">{{ rowCount | number }} rows</span>
34
+ </div>
35
+
36
+ <div class="benchmark-info">
37
+ <strong>What does this benchmark measure?</strong>
38
+ <ul>
39
+ <li><b>Initial Render</b> β€” time to paint the first visible frame of the grid</li>
40
+ <li><b>Selection Update</b> β€” time to select and deselect all {{ rowCount | number }} rows</li>
41
+ <li><b>Grouping Toggle</b> β€” time to enable then revert row grouping on the Department column</li>
42
+ <li><b>Avg Scroll Frame</b> β€” average canvas render time across 30 scroll frames (100 px each)</li>
43
+ <li><b>Total</b> β€” total wall-clock time for the entire benchmark run</li>
44
+ </ul>
45
+ </div>
46
+
47
+ <div class="results" *ngIf="results">
48
+ <div class="result-item" title="Time to paint the first visible frame of the grid">
49
+ <span class="label">Initial Render:</span>
50
+ <span class="value">{{ results.initialRender }}ms</span>
51
+ </div>
52
+ <div class="result-item" title="Time to select and deselect all rows">
53
+ <span class="label">Selection Update:</span>
54
+ <span class="value">{{ results.selectionUpdateTime }}ms</span>
55
+ </div>
56
+ <div class="result-item" title="Time to toggle row grouping on the Department column">
57
+ <span class="label">Grouping Toggle:</span>
58
+ <span class="value">{{ results.groupingUpdateTime }}ms</span>
59
+ </div>
60
+ <div class="result-item" title="Average canvas render time per scroll frame (30 frames, 100px each)">
61
+ <span class="label">Avg Scroll Frame:</span>
62
+ <span class="value">{{ results.scrollFrameAverage }}ms</span>
63
+ </div>
64
+ <div class="result-item total" title="Total wall-clock time for the full benchmark run">
65
+ <span class="label">Total:</span>
66
+ <span class="value">{{ results.totalTime }}ms</span>
67
+ </div>
68
+ </div>
69
+
70
+ <argent-grid
71
+ #grid
72
+ [columnDefs]="columnDefs"
73
+ [rowData]="rowData"
74
+ [height]="height"
75
+ [width]="width"
76
+ [theme]="theme"
77
+ [gridOptions]="gridOptions"
78
+ (gridReady)="onGridReady($event)"
79
+ />
80
+ </div>
81
+ `,
82
+ styles: [
83
+ `
84
+ .benchmark-container {
85
+ display: flex;
86
+ flex-direction: column;
87
+ gap: 12px;
88
+ }
89
+ .controls {
90
+ display: flex;
91
+ gap: 8px;
92
+ align-items: center;
93
+ }
94
+ .controls button {
95
+ padding: 8px 16px;
96
+ background: #3b82f6;
97
+ color: white;
98
+ border: none;
99
+ border-radius: 4px;
100
+ cursor: pointer;
101
+ font-size: 14px;
102
+ }
103
+ .controls button:disabled {
104
+ background: #9ca3af;
105
+ cursor: not-allowed;
106
+ }
107
+ .controls button:hover:not(:disabled) {
108
+ background: #2563eb;
109
+ }
110
+ .row-count {
111
+ margin-left: auto;
112
+ font-size: 14px;
113
+ color: #6b7280;
114
+ }
115
+ .results {
116
+ display: flex;
117
+ gap: 16px;
118
+ padding: 12px;
119
+ background: #f9fafb;
120
+ border-radius: 4px;
121
+ flex-wrap: wrap;
122
+ }
123
+ .result-item {
124
+ display: flex;
125
+ flex-direction: column;
126
+ gap: 4px;
127
+ }
128
+ .result-item .label {
129
+ font-size: 12px;
130
+ color: #6b7280;
131
+ }
132
+ .result-item .value {
133
+ font-size: 18px;
134
+ font-weight: 600;
135
+ color: #111827;
136
+ }
137
+ .result-item.total .value {
138
+ color: #059669;
139
+ }
140
+ .benchmark-info {
141
+ padding: 10px 14px;
142
+ background: #eff6ff;
143
+ border: 1px solid #bfdbfe;
144
+ border-radius: 4px;
145
+ font-size: 13px;
146
+ color: #1e40af;
147
+ }
148
+ .benchmark-info strong {
149
+ display: block;
150
+ margin-bottom: 6px;
151
+ }
152
+ .benchmark-info ul {
153
+ margin: 0;
154
+ padding-left: 18px;
155
+ }
156
+ .benchmark-info li {
157
+ margin-bottom: 3px;
158
+ }
159
+ `,
160
+ ],
161
+ })
162
+ export class BenchmarkWrapperComponent implements AfterViewInit, OnDestroy {
163
+ @ViewChild('grid') gridComponent!: ArgentGridComponent;
164
+
165
+ @Input() rowCount = 100000;
166
+
167
+ columnDefs: ColDef<Employee>[] = [
168
+ { field: 'id', headerName: 'ID', width: 80, sortable: true },
169
+ { field: 'name', headerName: 'Name', width: 200, sortable: true },
170
+ {
171
+ field: 'department',
172
+ headerName: 'Department',
173
+ width: 180,
174
+ sortable: true,
175
+ valueFormatter: departmentValueFormatter,
176
+ },
177
+ {
178
+ field: 'role',
179
+ headerName: 'Role',
180
+ width: 250,
181
+ filter: true,
182
+ valueFormatter: roleValueFormatter,
183
+ },
184
+ { field: 'salary', headerName: 'Salary', width: 120, sortable: true, filter: 'number' },
185
+ {
186
+ field: 'location',
187
+ headerName: 'Location',
188
+ width: 150,
189
+ filter: true,
190
+ valueFormatter: locationValueFormatter,
191
+ },
192
+ ];
193
+
194
+ rowData: Employee[] = [];
195
+ height = 'calc(100vh - 60px)';
196
+ width = '100%';
197
+ theme = themeQuartz;
198
+
199
+ gridOptions = {
200
+ floatingFilter: true,
201
+ enableRangeSelection: true,
202
+ defaultColDef: {
203
+ filter: true,
204
+ sortable: true,
205
+ resizable: true,
206
+ },
207
+ };
208
+
209
+ private gridApi?: GridApi<Employee>;
210
+ private fpsInterval?: number;
211
+
212
+ isRunning = false;
213
+ results: {
214
+ initialRender: number;
215
+ selectionUpdateTime: number;
216
+ groupingUpdateTime: number;
217
+ scrollFrameAverage: number;
218
+ totalTime: number;
219
+ } | null = null;
220
+
221
+ ngAfterViewInit(): void {
222
+ this.reloadData();
223
+ }
224
+
225
+ generateData(count: number): Employee[] {
226
+ const departments = STORY_DEPARTMENTS;
227
+ const roles = STORY_ROLES;
228
+ const locations = STORY_LOCATIONS;
229
+
230
+ return Array.from({ length: count }, (_, i) => ({
231
+ id: i + 1,
232
+ name: `Employee ${i + 1}`,
233
+ department: departments[Math.floor(Math.random() * departments.length)],
234
+ role: roles[Math.floor(Math.random() * roles.length)],
235
+ salary: Math.floor(Math.random() * 150000) + 50000,
236
+ location: locations[Math.floor(Math.random() * locations.length)],
237
+ }));
238
+ }
239
+
240
+ reloadData(): void {
241
+ this.rowData = this.generateData(this.rowCount);
242
+ this.results = null;
243
+ }
244
+
245
+ onGridReady(api: GridApi<Employee>): void {
246
+ this.gridApi = api;
247
+ }
248
+
249
+ runBenchmark(): void {
250
+ if (!this.gridApi || !this.gridComponent || this.isRunning) return;
251
+
252
+ this.isRunning = true;
253
+ this.results = null;
254
+
255
+ const results = {
256
+ initialRender: 0,
257
+ selectionUpdateTime: 0,
258
+ groupingUpdateTime: 0,
259
+ scrollFrameAverage: 0,
260
+ totalTime: 0,
261
+ };
262
+
263
+ const startTime = performance.now();
264
+
265
+ // 1. Initial render time
266
+ results.initialRender = this.gridComponent.getLastFrameTime();
267
+
268
+ // 2. Selection Update Time
269
+ const selStart = performance.now();
270
+ this.gridApi.selectAll();
271
+ setTimeout(() => {
272
+ results.selectionUpdateTime = Number((performance.now() - selStart).toFixed(2));
273
+ this.gridApi?.deselectAll();
274
+
275
+ // 3. Grouping Toggle Time
276
+ const groupStart = performance.now();
277
+ const colDefs = this.gridApi.getColumnDefs() as ColDef<Employee>[];
278
+ const deptCol = colDefs.find((c) => c.field === 'department');
279
+ const wasGrouped = (deptCol as ColDef<Employee>)?.rowGroup;
280
+
281
+ const newColDefs = colDefs.map((col: ColDef<Employee>) => {
282
+ if (col.field === 'department') {
283
+ return { ...col, rowGroup: !wasGrouped };
284
+ }
285
+ return col;
286
+ });
287
+
288
+ this.gridApi.setColumnDefs(newColDefs);
289
+
290
+ setTimeout(() => {
291
+ // Toggle back
292
+ const revertColDefs = newColDefs.map((col: ColDef<Employee>) => {
293
+ if (col.field === 'department') {
294
+ return { ...col, rowGroup: wasGrouped ?? false };
295
+ }
296
+ return col;
297
+ });
298
+ this.gridApi.setColumnDefs(revertColDefs);
299
+
300
+ results.groupingUpdateTime = Number((performance.now() - groupStart).toFixed(2));
301
+
302
+ // 4. Scroll Test
303
+ this.runScrollTest(results, startTime);
304
+ }, 100);
305
+ }, 100);
306
+ }
307
+
308
+ private runScrollTest(results: typeof this.results, startTime: number): void {
309
+ const frameTimes: number[] = [];
310
+ let scrollCount = 0;
311
+ const totalScrollFrames = 30;
312
+ const viewport = this.gridComponent.viewportRef?.nativeElement;
313
+
314
+ if (!viewport) {
315
+ this.finishBenchmark(results, startTime);
316
+ return;
317
+ }
318
+
319
+ const runScroll = () => {
320
+ if (scrollCount < totalScrollFrames) {
321
+ viewport.scrollTop += 100;
322
+ frameTimes.push(this.gridComponent.getLastFrameTime());
323
+ scrollCount++;
324
+ requestAnimationFrame(runScroll);
325
+ } else {
326
+ results.scrollFrameAverage = Number(
327
+ (frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length).toFixed(2)
328
+ );
329
+ this.finishBenchmark(results, startTime);
330
+ }
331
+ };
332
+
333
+ // Reset scroll position first
334
+ viewport.scrollTop = 0;
335
+ setTimeout(() => requestAnimationFrame(runScroll), 100);
336
+ }
337
+
338
+ private finishBenchmark(results: typeof this.results, startTime: number): void {
339
+ results.totalTime = Number((performance.now() - startTime).toFixed(2));
340
+ this.results = results;
341
+ this.isRunning = false;
342
+
343
+ // Reset scroll
344
+ const viewport = this.gridComponent.viewportRef?.nativeElement;
345
+ if (viewport) {
346
+ viewport.scrollTop = 0;
347
+ }
348
+ }
349
+
350
+ ngOnDestroy(): void {
351
+ if (this.fpsInterval) {
352
+ cancelAnimationFrame(this.fpsInterval);
353
+ }
354
+ }
355
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Shared utilities for Storybook stories
3
+ */
4
+
5
+ export const LOCATION_FLAGS: Record<string, string> = {
6
+ 'New York': 'πŸ‡ΊπŸ‡Έ New York',
7
+ 'San Francisco': 'πŸ‡ΊπŸ‡Έ San Francisco',
8
+ London: 'πŸ‡¬πŸ‡§ London',
9
+ Singapore: 'πŸ‡ΈπŸ‡¬ Singapore',
10
+ Remote: '🌐 Remote',
11
+ Berlin: 'πŸ‡©πŸ‡ͺ Berlin',
12
+ Tokyo: 'πŸ‡―πŸ‡΅ Tokyo',
13
+ };
14
+
15
+ export const DEPARTMENT_EMOJIS: Record<string, string> = {
16
+ Engineering: 'βš™οΈ Engineering',
17
+ Sales: 'πŸ’° Sales',
18
+ Marketing: 'πŸ“£ Marketing',
19
+ HR: 'πŸ‘₯ HR',
20
+ Finance: 'πŸ“ˆ Finance',
21
+ Design: '🎨 Design',
22
+ Operations: '🏒 Operations',
23
+ Support: '🎧 Support',
24
+ };
25
+
26
+ export const ROLE_EMOJIS: Record<string, string> = {
27
+ Engineer: 'πŸ’» Engineer',
28
+ 'Software Engineer': 'πŸ’» Software Engineer',
29
+ Manager: 'πŸ‘” Manager',
30
+ Director: '🏒 Director',
31
+ VP: 'πŸ’Ž VP',
32
+ Intern: 'πŸŽ“ Intern',
33
+ Analyst: 'πŸ“Š Analyst',
34
+ Lead: 'πŸ₯‡ Lead',
35
+ };
36
+
37
+ /**
38
+ * Common value formatter for location columns to add flags
39
+ */
40
+ export const locationValueFormatter = (params: any) => {
41
+ return LOCATION_FLAGS[params.value] ?? params.value;
42
+ };
43
+
44
+ /**
45
+ * Common value formatter for department columns to add emojis
46
+ */
47
+ export const departmentValueFormatter = (params: any) => {
48
+ return DEPARTMENT_EMOJIS[params.value] ?? params.value;
49
+ };
50
+
51
+ /**
52
+ * Common value formatter for role columns to add emojis
53
+ */
54
+ export const roleValueFormatter = (params: any) => {
55
+ return ROLE_EMOJIS[params.value] ?? params.value;
56
+ };
57
+
58
+ /**
59
+ * Standard locations for mock data generation
60
+ */
61
+ export const STORY_LOCATIONS = [
62
+ 'New York',
63
+ 'San Francisco',
64
+ 'London',
65
+ 'Singapore',
66
+ 'Remote',
67
+ 'Berlin',
68
+ 'Tokyo',
69
+ ];
70
+
71
+ /**
72
+ * Standard departments for mock data generation
73
+ */
74
+ export const STORY_DEPARTMENTS = [
75
+ 'Engineering',
76
+ 'Sales',
77
+ 'Marketing',
78
+ 'HR',
79
+ 'Finance',
80
+ 'Design',
81
+ 'Operations',
82
+ 'Support',
83
+ ];
84
+
85
+ /**
86
+ * Standard roles for mock data generation
87
+ */
88
+ export const STORY_ROLES = ['Engineer', 'Manager', 'Director', 'VP', 'Intern', 'Analyst', 'Lead'];