argent-grid 0.1.0 → 0.2.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 (108) 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 +2 -2
  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/screenshots.spec.ts +52 -0
  28. package/e2e/theming.spec.ts +35 -0
  29. package/e2e/visual.spec.ts +91 -0
  30. package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
  31. package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
  32. package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
  33. package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
  34. package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
  35. package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
  36. package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
  37. package/package.json +20 -6
  38. package/plan.md +50 -18
  39. package/playwright.config.ts +38 -0
  40. package/setup-vitest.ts +10 -13
  41. package/src/lib/argent-grid.module.ts +10 -12
  42. package/src/lib/components/argent-grid.component.css +327 -76
  43. package/src/lib/components/argent-grid.component.html +186 -64
  44. package/src/lib/components/argent-grid.component.spec.ts +120 -160
  45. package/src/lib/components/argent-grid.component.ts +642 -189
  46. package/src/lib/components/argent-grid.selection.spec.ts +132 -0
  47. package/src/lib/components/set-filter/set-filter.component.ts +302 -0
  48. package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
  49. package/src/lib/directives/click-outside.directive.ts +19 -0
  50. package/src/lib/rendering/canvas-renderer.spec.ts +366 -0
  51. package/src/lib/rendering/canvas-renderer.ts +418 -305
  52. package/src/lib/rendering/live-data-handler.ts +110 -0
  53. package/src/lib/rendering/live-data-optimizations.ts +133 -0
  54. package/src/lib/rendering/render/blit.spec.ts +16 -27
  55. package/src/lib/rendering/render/blit.ts +48 -36
  56. package/src/lib/rendering/render/cells.spec.ts +132 -0
  57. package/src/lib/rendering/render/cells.ts +46 -24
  58. package/src/lib/rendering/render/column-utils.ts +73 -0
  59. package/src/lib/rendering/render/hit-test.ts +55 -0
  60. package/src/lib/rendering/render/index.ts +79 -76
  61. package/src/lib/rendering/render/lines.ts +43 -43
  62. package/src/lib/rendering/render/primitives.ts +161 -0
  63. package/src/lib/rendering/render/theme.spec.ts +8 -12
  64. package/src/lib/rendering/render/theme.ts +7 -10
  65. package/src/lib/rendering/render/types.ts +2 -2
  66. package/src/lib/rendering/render/walk.spec.ts +35 -38
  67. package/src/lib/rendering/render/walk.ts +60 -50
  68. package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
  69. package/src/lib/rendering/utils/damage-tracker.ts +6 -18
  70. package/src/lib/rendering/utils/index.ts +1 -1
  71. package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
  72. package/src/lib/services/grid.service.spec.ts +1165 -201
  73. package/src/lib/services/grid.service.ts +819 -187
  74. package/src/lib/themes/parts/color-schemes.ts +132 -0
  75. package/src/lib/themes/parts/icon-sets.ts +258 -0
  76. package/src/lib/themes/theme-builder.ts +347 -0
  77. package/src/lib/themes/theme-quartz.ts +72 -0
  78. package/src/lib/themes/types.ts +238 -0
  79. package/src/lib/types/ag-grid-types.ts +73 -14
  80. package/src/public-api.ts +39 -9
  81. package/src/stories/Advanced.stories.ts +188 -0
  82. package/src/stories/ArgentGrid.stories.ts +277 -0
  83. package/src/stories/Benchmark.stories.ts +74 -0
  84. package/src/stories/CellRenderers.stories.ts +221 -0
  85. package/src/stories/Filtering.stories.ts +252 -0
  86. package/src/stories/Grouping.stories.ts +217 -0
  87. package/src/stories/Theming.stories.ts +124 -0
  88. package/src/stories/benchmark-wrapper.component.ts +315 -0
  89. package/tsconfig.storybook.json +10 -0
  90. package/vitest.config.ts +9 -9
  91. package/demo-app/README.md +0 -70
  92. package/demo-app/angular.json +0 -78
  93. package/demo-app/e2e/benchmark.spec.ts +0 -53
  94. package/demo-app/e2e/demo-page.spec.ts +0 -77
  95. package/demo-app/e2e/grid-features.spec.ts +0 -269
  96. package/demo-app/package-lock.json +0 -14023
  97. package/demo-app/package.json +0 -36
  98. package/demo-app/playwright-test-menu.js +0 -19
  99. package/demo-app/playwright.config.ts +0 -23
  100. package/demo-app/src/app/app.component.ts +0 -10
  101. package/demo-app/src/app/app.config.ts +0 -13
  102. package/demo-app/src/app/app.routes.ts +0 -7
  103. package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
  104. package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
  105. package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
  106. package/demo-app/src/index.html +0 -19
  107. package/demo-app/src/main.ts +0 -6
  108. package/demo-app/tsconfig.json +0 -31
@@ -1,366 +0,0 @@
1
- import { Component, OnInit, OnDestroy, AfterViewInit, ViewChild, ChangeDetectorRef } from '@angular/core';
2
- import { CommonModule } from '@angular/common';
3
- import { ArgentGridModule, ArgentGridComponent } from 'argent-grid';
4
- import { GridApi, ColDef, IRowNode } from 'argent-grid';
5
-
6
- interface Employee {
7
- id: number;
8
- name: string;
9
- department: string;
10
- role: string;
11
- salary: number;
12
- salaryTrend: number[];
13
- location: string;
14
- startDate: string;
15
- performance: number;
16
- }
17
-
18
- @Component({
19
- selector: 'app-demo-page',
20
- standalone: true,
21
- imports: [CommonModule, ArgentGridModule],
22
- templateUrl: './demo-page.component.html',
23
- styleUrls: ['./demo-page.component.css'],
24
- })
25
- export class DemoPageComponent implements OnInit, AfterViewInit, OnDestroy {
26
- @ViewChild(ArgentGridComponent) gridComponent!: ArgentGridComponent<Employee>;
27
-
28
- rowData: Employee[] = [];
29
- renderTime = 0;
30
- canvasFrameTime = 0;
31
- fps = 0;
32
- isLoading = false;
33
- rowCount = 100000;
34
- isGrouped = false;
35
- isFloatingFilterShown = true;
36
- isBenchmarking = false;
37
- isMasterDetail = false;
38
- isPivotMode = false;
39
- isSideBarVisible = true;
40
- isFiltered = false;
41
- benchmarkResults: any = null;
42
-
43
- columnDefs: ColDef<Employee>[] = [
44
- { field: 'id', headerName: 'ID', width: 80, sortable: true, filter: 'number' },
45
- { field: 'name', headerName: 'Name', width: 200, sortable: true, filter: 'text' },
46
- { field: 'department', headerName: 'Department', width: 180, sortable: true, filter: 'text', rowGroup: false },
47
- { field: 'role', headerName: 'Role', width: 250, filter: 'text' },
48
- {
49
- field: 'salary',
50
- headerName: 'Salary',
51
- width: 120,
52
- sortable: true,
53
- filter: 'number',
54
- valueFormatter: (params: any) => `$${params.value?.toLocaleString()}`,
55
- },
56
- {
57
- field: 'salaryTrend',
58
- headerName: 'Salary Trend',
59
- width: 150,
60
- sparklineOptions: {
61
- type: 'area',
62
- area: {
63
- fill: 'rgba(74, 222, 128, 0.2)',
64
- stroke: '#4ade80',
65
- strokeWidth: 2
66
- }
67
- }
68
- },
69
- { field: 'location', headerName: 'Location', width: 150, filter: 'text' },
70
- { field: 'startDate', headerName: 'Start Date', width: 130, filter: 'date' },
71
- {
72
- field: 'performance',
73
- headerName: 'Performance',
74
- width: 120,
75
- filter: 'number',
76
- cellRenderer: (params: any) => {
77
- const value = params.value;
78
- const color = value >= 80 ? '#22c55e' : value >= 60 ? '#eab308' : '#ef4444';
79
- return `<span style="color: ${color}; font-weight: bold; padding: 4px 8px; background: ${color}20; border-radius: 4px;">${value}%</span>`;
80
- },
81
- },
82
- ];
83
-
84
- gridOptions: any = {
85
- floatingFilter: true,
86
- enableRangeSelection: true,
87
- sideBar: true,
88
- autoGroupColumnDef: {
89
- headerName: 'Organization',
90
- width: 250,
91
- pinned: 'left'
92
- }
93
- };
94
-
95
- private gridApi?: GridApi<Employee>;
96
- private fpsInterval?: number;
97
- private lastFrameTime = 0;
98
- private fpsUpdateTimer = 0;
99
-
100
- constructor(
101
- private cdr: ChangeDetectorRef
102
- ) {}
103
-
104
- ngOnInit(): void {
105
- this.loadData(100000);
106
- this.startFPSCounter();
107
- }
108
-
109
- ngAfterViewInit(): void {
110
- // Grid is ready after view init
111
- }
112
-
113
- toggleGrouping(): void {
114
- this.isGrouped = !this.isGrouped;
115
- this.columnDefs = this.columnDefs.map(col => {
116
- if (col.field === 'department') {
117
- return { ...col, rowGroup: this.isGrouped };
118
- }
119
- return col;
120
- });
121
-
122
- if (this.gridApi) {
123
- this.gridApi.setColumnDefs(this.columnDefs);
124
- this.gridApi.onFilterChanged(); // Trigger re-processing
125
- }
126
-
127
- this.cdr.detectChanges();
128
- }
129
-
130
- toggleFloatingFilter(): void {
131
- this.isFloatingFilterShown = !this.isFloatingFilterShown;
132
- if (this.gridApi) {
133
- this.gridApi.setGridOption('floatingFilter', this.isFloatingFilterShown);
134
- }
135
- this.cdr.detectChanges();
136
- }
137
-
138
- toggleMasterDetail(): void {
139
- this.isMasterDetail = !this.isMasterDetail;
140
- if (this.gridApi) {
141
- this.gridApi.setGridOption('masterDetail', this.isMasterDetail);
142
- this.gridApi.setGridOption('isRowMaster', (data: any) => data.id % 2 === 0);
143
- this.gridApi.setRowData([...this.rowData]); // Force refresh
144
- }
145
- this.cdr.detectChanges();
146
- }
147
-
148
- togglePivotMode(): void {
149
- this.isPivotMode = !this.isPivotMode;
150
- if (this.gridApi) {
151
- this.columnDefs = this.columnDefs.map(col => {
152
- if (col.field === 'location') {
153
- return { ...col, pivot: this.isPivotMode };
154
- }
155
- if (col.field === 'salary') {
156
- return { ...col, aggFunc: 'sum' };
157
- }
158
- return col;
159
- });
160
- this.gridApi.setColumnDefs(this.columnDefs);
161
- this.gridApi.setPivotMode(this.isPivotMode);
162
- }
163
- this.cdr.detectChanges();
164
- }
165
-
166
- toggleSideBar(): void {
167
- this.isSideBarVisible = !this.isSideBarVisible;
168
- if (this.gridApi) {
169
- this.gridApi.setGridOption('sideBar', this.isSideBarVisible);
170
- }
171
- this.cdr.detectChanges();
172
- }
173
-
174
- toggleFilter(): void {
175
- this.isFiltered = !this.isFiltered;
176
- if (this.gridApi) {
177
- if (this.isFiltered) {
178
- this.gridApi.setFilterModel({
179
- department: { filterType: 'text', type: 'contains', filter: 'Eng' }
180
- });
181
- } else {
182
- this.gridApi.setFilterModel({});
183
- }
184
- }
185
- this.cdr.detectChanges();
186
- }
187
-
188
- startFPSCounter(): void {
189
- const countFPS = () => {
190
- const now = performance.now();
191
- let changed = false;
192
-
193
- if (this.lastFrameTime) {
194
- const delta = now - this.lastFrameTime;
195
-
196
- // Update metrics only every 500ms to reduce change detection cycles
197
- if (now - this.fpsUpdateTimer > 500) {
198
- const newFps = Math.round(1000 / delta);
199
- if (newFps !== this.fps) {
200
- this.fps = newFps;
201
- changed = true;
202
- }
203
-
204
- // Update canvas frame time if available
205
- if (this.gridComponent) {
206
- const newFrameTime = Number(this.gridComponent.getLastFrameTime().toFixed(2));
207
- if (newFrameTime !== this.canvasFrameTime) {
208
- this.canvasFrameTime = newFrameTime;
209
- changed = true;
210
- }
211
- }
212
-
213
- this.fpsUpdateTimer = now;
214
- }
215
- }
216
- this.lastFrameTime = now;
217
-
218
- // In zoneless mode, we manually trigger change detection for these async updates
219
- if (changed) {
220
- this.cdr.detectChanges();
221
- }
222
-
223
- this.fpsInterval = requestAnimationFrame(countFPS);
224
- };
225
- this.fpsInterval = requestAnimationFrame(countFPS);
226
- }
227
-
228
- runBenchmark(): void {
229
- if (!this.gridApi || !this.gridComponent) return;
230
-
231
- this.isBenchmarking = true;
232
- this.benchmarkResults = null;
233
- this.cdr.detectChanges();
234
-
235
- const results = {
236
- initialRender: 0,
237
- scrollFrameAverage: 0,
238
- selectionUpdateTime: 0,
239
- groupingUpdateTime: 0,
240
- totalTime: 0
241
- };
242
-
243
- const startTime = performance.now();
244
-
245
- // 1. Initial render time (cached from last frame)
246
- results.initialRender = this.gridComponent.getLastFrameTime();
247
-
248
- // 2. Selection Update Time
249
- const selStart = performance.now();
250
- this.gridApi.selectAll();
251
- // Wait for render cycle
252
- setTimeout(() => {
253
- results.selectionUpdateTime = Number((performance.now() - selStart).toFixed(2));
254
- this.gridApi?.deselectAll();
255
-
256
- // 3. Grouping Update Time (Toggle grouping twice)
257
- const groupStart = performance.now();
258
- if (!this.isGrouped) {
259
- this.toggleGrouping(); // Turn on
260
- setTimeout(() => {
261
- this.toggleGrouping(); // Turn off
262
- results.groupingUpdateTime = Number((performance.now() - groupStart).toFixed(2));
263
-
264
- startScrolling();
265
- }, 100);
266
- } else {
267
- this.toggleGrouping(); // Turn off
268
- setTimeout(() => {
269
- this.toggleGrouping(); // Turn on
270
- results.groupingUpdateTime = Number((performance.now() - groupStart).toFixed(2));
271
-
272
- startScrolling();
273
- }, 100);
274
- }
275
-
276
- // 4. Scroll performance (programmatic scroll)
277
- const startScrolling = () => {
278
- const frameTimes: number[] = [];
279
- let scrollCount = 0;
280
- const totalScrollFrames = 30;
281
- const viewport = this.gridComponent.viewportRef.nativeElement;
282
-
283
- const runScroll = () => {
284
- if (scrollCount < totalScrollFrames) {
285
- viewport.scrollTop += 100;
286
- frameTimes.push(this.gridComponent.getLastFrameTime());
287
- scrollCount++;
288
- requestAnimationFrame(runScroll);
289
- } else {
290
- // Finished scroll
291
- results.scrollFrameAverage = Number((frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length).toFixed(2));
292
- results.totalTime = Number((performance.now() - startTime).toFixed(2));
293
- this.benchmarkResults = results;
294
- this.isBenchmarking = false;
295
-
296
- // Scroll back up
297
- viewport.scrollTop = 0;
298
- this.cdr.detectChanges();
299
- }
300
- };
301
- requestAnimationFrame(runScroll);
302
- };
303
- }, 100);
304
- }
305
-
306
- loadData(count: number): void {
307
- this.isLoading = true;
308
- const startTime = performance.now();
309
-
310
- const departments = ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance', 'Operations', 'Support', 'Product'];
311
- const roles = [
312
- 'Software Engineer', 'Senior Engineer', 'Staff Engineer', 'Principal Engineer',
313
- 'Engineering Manager', 'Sales Rep', 'Account Executive', 'Marketing Manager',
314
- 'HR Specialist', 'Financial Analyst', 'Operations Manager', 'Support Specialist',
315
- 'Product Manager', 'Senior PM', 'Director',
316
- ];
317
- const locations = ['New York', 'San Francisco', 'London', 'Singapore', 'Tokyo', 'Berlin', 'Remote'];
318
-
319
- const data: Employee[] = [];
320
-
321
- for (let i = 0; i < count; i++) {
322
- const dept = departments[Math.floor(Math.random() * departments.length)];
323
- const role = roles[Math.floor(Math.random() * roles.length)];
324
- const location = locations[Math.floor(Math.random() * locations.length)];
325
-
326
- data.push({
327
- id: i + 1,
328
- name: `Employee ${i + 1}`,
329
- department: dept,
330
- role: `${dept} - ${role}`,
331
- salary: Math.floor(Math.random() * 150000) + 50000,
332
- salaryTrend: Array.from({ length: 10 }, () => Math.floor(Math.random() * 100)),
333
- location,
334
- startDate: new Date(Date.now() - Math.random() * 5 * 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
335
- performance: Math.floor(Math.random() * 40) + 60,
336
- });
337
- }
338
-
339
- setTimeout(() => {
340
- this.rowData = data;
341
- const endTime = performance.now();
342
- this.renderTime = Math.round(endTime - startTime);
343
- this.rowCount = count;
344
- this.isLoading = false;
345
-
346
- this.cdr.detectChanges();
347
- console.log(`Loaded ${count} rows in ${this.renderTime}ms`);
348
- }, 100);
349
- }
350
-
351
- onGridReady(api: GridApi<Employee>): void {
352
- this.gridApi = api;
353
- (window as any).gridApi = api;
354
- console.log('Grid ready:', api);
355
- }
356
-
357
- onRowClicked(event: { data: Employee; node: IRowNode<Employee> }): void {
358
- console.log('Row clicked:', event.data);
359
- }
360
-
361
- ngOnDestroy(): void {
362
- if (this.fpsInterval) {
363
- cancelAnimationFrame(this.fpsInterval);
364
- }
365
- }
366
- }
@@ -1,19 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <title>ArgentGrid - Canvas-Based High-Performance Grid Demo</title>
6
- <base href="/">
7
- <meta name="viewport" content="width=device-width, initial-scale=1">
8
- <meta name="description" content="Live demo of ArgentGrid - Canvas-based Angular grid with 1M+ rows and 60fps scrolling">
9
- <link rel="icon" type="image/x-icon" href="favicon.ico">
10
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
11
- <style>
12
- * { margin: 0; padding: 0; box-sizing: border-box; }
13
- html, body { height: 100%; font-family: 'Inter', sans-serif; background: #0f0f23; }
14
- </style>
15
- </head>
16
- <body>
17
- <app-root></app-root>
18
- </body>
19
- </html>
@@ -1,6 +0,0 @@
1
- import { bootstrapApplication } from '@angular/platform-browser';
2
- import { appConfig } from './app/app.config';
3
- import { AppComponent } from './app/app.component';
4
-
5
- bootstrapApplication(AppComponent, appConfig)
6
- .catch((err) => console.error(err));
@@ -1,31 +0,0 @@
1
- {
2
- "compileOnSave": false,
3
- "compilerOptions": {
4
- "baseUrl": ".",
5
- "outDir": "./dist/out-tsc",
6
- "strict": false,
7
- "noImplicitOverride": true,
8
- "noPropertyAccessFromIndexSignature": true,
9
- "noImplicitReturns": true,
10
- "noFallthroughCasesInSwitch": true,
11
- "skipLibCheck": true,
12
- "esModuleInterop": true,
13
- "sourceMap": true,
14
- "declaration": false,
15
- "experimentalDecorators": true,
16
- "moduleResolution": "bundler",
17
- "importHelpers": true,
18
- "target": "ES2022",
19
- "module": "ES2022",
20
- "lib": ["ES2022", "dom"],
21
- "paths": {}
22
- },
23
- "include": ["src/**/*"],
24
- "exclude": ["src/**/*.spec.ts"],
25
- "angularCompilerOptions": {
26
- "enableI18nLegacyMessageIdFormat": false,
27
- "strictInjectionParameters": true,
28
- "strictInputAccessModifiers": true,
29
- "strictTemplates": true
30
- }
31
- }