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
@@ -0,0 +1,217 @@
1
+ import { BrowserModule } from '@angular/platform-browser';
2
+ import type { Meta, StoryObj } from '@storybook/angular';
3
+ import { moduleMetadata } from '@storybook/angular';
4
+ import { ArgentGridComponent, ArgentGridModule, themeQuartz } from '../public-api';
5
+
6
+ interface Employee {
7
+ id: number;
8
+ name: string;
9
+ department: string;
10
+ role: string;
11
+ salary: number;
12
+ location: string;
13
+ }
14
+
15
+ const meta: Meta<ArgentGridComponent<Employee>> = {
16
+ title: 'Features/Grouping',
17
+ component: ArgentGridComponent,
18
+ decorators: [
19
+ moduleMetadata({
20
+ imports: [ArgentGridModule, BrowserModule],
21
+ }),
22
+ ],
23
+ parameters: {
24
+ layout: 'fullscreen',
25
+ },
26
+ };
27
+
28
+ export default meta;
29
+ type Story = StoryObj<ArgentGridComponent<Employee>>;
30
+
31
+ function generateStaticData(count: number): Employee[] {
32
+ const departments = ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance'];
33
+ const roles = ['Engineer', 'Manager', 'Director', 'VP', 'Intern'];
34
+ const locations = ['New York', 'San Francisco', 'London', 'Singapore', 'Remote'];
35
+
36
+ return Array.from({ length: count }, (_, i) => ({
37
+ id: i + 1,
38
+ name: `Employee ${i + 1}`,
39
+ department: departments[i % departments.length],
40
+ role: roles[i % roles.length],
41
+ salary: 50000 + i * 1000,
42
+ location: locations[i % locations.length],
43
+ }));
44
+ }
45
+
46
+ export const RowGrouping: Story = {
47
+ args: {
48
+ columnDefs: [
49
+ { field: 'id', headerName: 'ID', width: 80 },
50
+ { field: 'name', headerName: 'Name', width: 200 },
51
+ {
52
+ field: 'department',
53
+ headerName: 'Department 📁',
54
+ width: 180,
55
+ rowGroup: true,
56
+ headerComponentParams: { groupIcon: '📁' },
57
+ },
58
+ { field: 'role', headerName: 'Role', width: 250 },
59
+ { field: 'salary', headerName: 'Salary', width: 120 },
60
+ { field: 'location', headerName: 'Location', width: 150 },
61
+ ],
62
+ rowData: generateStaticData(100),
63
+ height: '500px',
64
+ width: '100%',
65
+ theme: themeQuartz,
66
+ gridOptions: {
67
+ autoGroupColumnDef: {
68
+ headerName: 'Organization 📁',
69
+ width: 250,
70
+ pinned: 'left',
71
+ },
72
+ groupDefaultExpanded: 1, // Expand first level by default
73
+ },
74
+ },
75
+ parameters: {
76
+ docs: {
77
+ description: {
78
+ story:
79
+ '**Row grouping by Department** column. Grouped columns show a 📁 icon. Click the **▶ expand/collapse arrows** in the group rows to show/hide items. **Drag column headers** to the left panel to group by multiple columns.',
80
+ },
81
+ },
82
+ },
83
+ };
84
+
85
+ export const MultiLevelGrouping: Story = {
86
+ args: {
87
+ columnDefs: [
88
+ { field: 'id', headerName: 'ID', width: 80 },
89
+ { field: 'name', headerName: 'Name', width: 200 },
90
+ {
91
+ field: 'department',
92
+ headerName: 'Department 📁',
93
+ width: 150,
94
+ rowGroup: true,
95
+ headerComponentParams: { groupIcon: '📁' },
96
+ },
97
+ {
98
+ field: 'location',
99
+ headerName: 'Location 📁',
100
+ width: 150,
101
+ rowGroup: true,
102
+ headerComponentParams: { groupIcon: '📁' },
103
+ },
104
+ { field: 'role', headerName: 'Role', width: 250 },
105
+ { field: 'salary', headerName: 'Salary', width: 120 },
106
+ ],
107
+ rowData: generateStaticData(100),
108
+ height: '500px',
109
+ width: '100%',
110
+ theme: themeQuartz,
111
+ gridOptions: {
112
+ autoGroupColumnDef: {
113
+ headerName: 'Organization 📁📁',
114
+ width: 300,
115
+ pinned: 'left',
116
+ },
117
+ groupDefaultExpanded: 2, // Expand first 2 levels by default
118
+ },
119
+ },
120
+ parameters: {
121
+ docs: {
122
+ description: {
123
+ story:
124
+ '**Multi-level grouping** by Department AND Location. Each grouped column shows a 📁 icon. Groups are **expanded by default** to show the hierarchy. Click **▶ arrows** to collapse/expand. Notice the nested structure: Department → Location → Employees.',
125
+ },
126
+ },
127
+ },
128
+ };
129
+
130
+ export const GroupingWithAggregation: Story = {
131
+ args: {
132
+ columnDefs: [
133
+ { field: 'id', headerName: 'ID', width: 80 },
134
+ { field: 'name', headerName: 'Name', width: 200 },
135
+ {
136
+ field: 'department',
137
+ headerName: 'Department 📁',
138
+ width: 180,
139
+ rowGroup: true,
140
+ headerComponentParams: { groupIcon: '📁' },
141
+ },
142
+ { field: 'role', headerName: 'Role', width: 250 },
143
+ {
144
+ field: 'salary',
145
+ headerName: 'Salary 💰',
146
+ width: 120,
147
+ aggFunc: 'sum', // Show sum in group rows
148
+ },
149
+ { field: 'location', headerName: 'Location', width: 150 },
150
+ ],
151
+ rowData: generateStaticData(100),
152
+ height: '500px',
153
+ width: '100%',
154
+ theme: themeQuartz,
155
+ gridOptions: {
156
+ autoGroupColumnDef: {
157
+ headerName: 'Organization 📁',
158
+ width: 250,
159
+ pinned: 'left',
160
+ },
161
+ groupDefaultExpanded: 1,
162
+ },
163
+ },
164
+ parameters: {
165
+ docs: {
166
+ description: {
167
+ story:
168
+ '**Grouping with aggregation**. Salary column shows **sum (💰)** for each department group. Look at the group rows to see aggregated values. Supported aggregations: sum, avg, min, max, count.',
169
+ },
170
+ },
171
+ },
172
+ };
173
+
174
+ export const DragAndDropGrouping: Story = {
175
+ args: {
176
+ columnDefs: [
177
+ { field: 'id', headerName: 'ID', width: 80 },
178
+ { field: 'name', headerName: 'Name', width: 200 },
179
+ {
180
+ field: 'department',
181
+ headerName: 'Department 📁 (drag me!)',
182
+ width: 180,
183
+ rowGroup: false, // Not pre-grouped - user can drag
184
+ headerComponentParams: { draggable: true },
185
+ },
186
+ {
187
+ field: 'location',
188
+ headerName: 'Location 📁 (drag me!)',
189
+ width: 150,
190
+ rowGroup: false,
191
+ headerComponentParams: { draggable: true },
192
+ },
193
+ { field: 'role', headerName: 'Role', width: 250 },
194
+ { field: 'salary', headerName: 'Salary', width: 120 },
195
+ ],
196
+ rowData: generateStaticData(100),
197
+ height: '500px',
198
+ width: '100%',
199
+ theme: themeQuartz,
200
+ gridOptions: {
201
+ rowGroupPanelShow: 'always', // Always show group panel
202
+ autoGroupColumnDef: {
203
+ headerName: 'Groups 📁',
204
+ width: 250,
205
+ pinned: 'left',
206
+ },
207
+ },
208
+ },
209
+ parameters: {
210
+ docs: {
211
+ description: {
212
+ story:
213
+ '**Drag-and-drop grouping**. See the **"Drag me!"** labels? **Drag column headers** (Department, Location) to the **"Groups" panel on the left** to create groups dynamically. Drop columns back to ungroup. Multiple columns can be grouped at once.',
214
+ },
215
+ },
216
+ },
217
+ };
@@ -0,0 +1,124 @@
1
+ import { BrowserModule } from '@angular/platform-browser';
2
+ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
3
+ import type { Meta, StoryObj } from '@storybook/angular';
4
+ import { moduleMetadata } from '@storybook/angular';
5
+ import { ArgentGridComponent, ArgentGridModule, colorSchemeDark, themeQuartz } from '../public-api';
6
+
7
+ interface Employee {
8
+ id: number;
9
+ name: string;
10
+ department: string;
11
+ role: string;
12
+ salary: number;
13
+ location: string;
14
+ }
15
+
16
+ const meta: Meta<ArgentGridComponent<Employee>> = {
17
+ title: 'Features/Theming',
18
+ component: ArgentGridComponent,
19
+ decorators: [
20
+ moduleMetadata({
21
+ imports: [ArgentGridModule, BrowserModule, BrowserAnimationsModule],
22
+ }),
23
+ ],
24
+ parameters: {
25
+ layout: 'fullscreen',
26
+ },
27
+ };
28
+
29
+ export default meta;
30
+ type Story = StoryObj<ArgentGridComponent<Employee>>;
31
+
32
+ function generateStaticData(count: number): Employee[] {
33
+ const departments = ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance'];
34
+ const roles = ['Engineer', 'Manager', 'Director', 'VP', 'Intern'];
35
+ const locations = ['New York', 'San Francisco', 'London', 'Singapore', 'Remote'];
36
+
37
+ return Array.from({ length: count }, (_, i) => ({
38
+ id: i + 1,
39
+ name: `Employee ${i + 1}`,
40
+ department: departments[i % departments.length],
41
+ role: roles[i % roles.length],
42
+ salary: 50000 + i * 1000,
43
+ location: locations[i % locations.length],
44
+ }));
45
+ }
46
+
47
+ const columnDefs = [
48
+ { field: 'id', headerName: 'ID', width: 80 },
49
+ { field: 'name', headerName: 'Name', width: 200 },
50
+ { field: 'department', headerName: 'Department', width: 180 },
51
+ { field: 'role', headerName: 'Role', width: 250 },
52
+ { field: 'salary', headerName: 'Salary', width: 120 },
53
+ { field: 'location', headerName: 'Location', width: 150 },
54
+ ];
55
+
56
+ export const LightMode: Story = {
57
+ args: {
58
+ columnDefs,
59
+ rowData: generateStaticData(50),
60
+ height: '400px',
61
+ width: '100%',
62
+ theme: themeQuartz,
63
+ },
64
+ parameters: {
65
+ docs: {
66
+ description: {
67
+ story: 'Default light theme using Quartz theme.',
68
+ },
69
+ },
70
+ },
71
+ };
72
+
73
+ export const DarkMode: Story = {
74
+ args: {
75
+ columnDefs,
76
+ rowData: generateStaticData(50),
77
+ height: '400px',
78
+ width: '100%',
79
+ theme: themeQuartz.withPart(colorSchemeDark),
80
+ },
81
+ parameters: {
82
+ docs: {
83
+ description: {
84
+ story: 'Dark mode using Quartz theme with dark color scheme.',
85
+ },
86
+ },
87
+ },
88
+ };
89
+
90
+ export const CompactMode: Story = {
91
+ args: {
92
+ columnDefs,
93
+ rowData: generateStaticData(50),
94
+ height: '400px',
95
+ width: '100%',
96
+ theme: themeQuartz.withParams({ rowHeight: 32, fontSize: 12, spacing: 4 }),
97
+ },
98
+ parameters: {
99
+ docs: {
100
+ description: {
101
+ story: 'Compact mode with smaller row height and font size.',
102
+ },
103
+ },
104
+ },
105
+ };
106
+
107
+ export const CompactDarkMode: Story = {
108
+ args: {
109
+ columnDefs,
110
+ rowData: generateStaticData(50),
111
+ height: '400px',
112
+ width: '100%',
113
+ theme: themeQuartz
114
+ .withParams({ rowHeight: 32, fontSize: 12, spacing: 4 })
115
+ .withPart(colorSchemeDark),
116
+ },
117
+ parameters: {
118
+ docs: {
119
+ description: {
120
+ story: 'Combined compact and dark mode for dense data display.',
121
+ },
122
+ },
123
+ },
124
+ };
@@ -0,0 +1,315 @@
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
+
5
+ interface Employee {
6
+ id: number;
7
+ name: string;
8
+ department: string;
9
+ role: string;
10
+ salary: number;
11
+ location: string;
12
+ }
13
+
14
+ @Component({
15
+ selector: 'app-benchmark-wrapper',
16
+ standalone: true,
17
+ imports: [CommonModule, ArgentGridModule],
18
+ template: `
19
+ <div class="benchmark-container">
20
+ <div class="controls">
21
+ <button (click)="runBenchmark()" [disabled]="isRunning">
22
+ {{ isRunning ? 'Running...' : 'Run Benchmark' }}
23
+ </button>
24
+ <button (click)="reloadData()">Reload Data</button>
25
+ <span class="row-count">{{ rowCount | number }} rows</span>
26
+ </div>
27
+
28
+ <div class="results" *ngIf="results">
29
+ <div class="result-item">
30
+ <span class="label">Initial Render:</span>
31
+ <span class="value">{{ results.initialRender }}ms</span>
32
+ </div>
33
+ <div class="result-item">
34
+ <span class="label">Selection Update:</span>
35
+ <span class="value">{{ results.selectionUpdateTime }}ms</span>
36
+ </div>
37
+ <div class="result-item">
38
+ <span class="label">Grouping Toggle:</span>
39
+ <span class="value">{{ results.groupingUpdateTime }}ms</span>
40
+ </div>
41
+ <div class="result-item">
42
+ <span class="label">Avg Scroll Frame:</span>
43
+ <span class="value">{{ results.scrollFrameAverage }}ms</span>
44
+ </div>
45
+ <div class="result-item total">
46
+ <span class="label">Total:</span>
47
+ <span class="value">{{ results.totalTime }}ms</span>
48
+ </div>
49
+ </div>
50
+
51
+ <argent-grid
52
+ #grid
53
+ [columnDefs]="columnDefs"
54
+ [rowData]="rowData"
55
+ [height]="height"
56
+ [width]="width"
57
+ [theme]="theme"
58
+ [gridOptions]="gridOptions"
59
+ (gridReady)="onGridReady($event)"
60
+ />
61
+ </div>
62
+ `,
63
+ styles: [
64
+ `
65
+ .benchmark-container {
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 12px;
69
+ }
70
+ .controls {
71
+ display: flex;
72
+ gap: 8px;
73
+ align-items: center;
74
+ }
75
+ .controls button {
76
+ padding: 8px 16px;
77
+ background: #3b82f6;
78
+ color: white;
79
+ border: none;
80
+ border-radius: 4px;
81
+ cursor: pointer;
82
+ font-size: 14px;
83
+ }
84
+ .controls button:disabled {
85
+ background: #9ca3af;
86
+ cursor: not-allowed;
87
+ }
88
+ .controls button:hover:not(:disabled) {
89
+ background: #2563eb;
90
+ }
91
+ .row-count {
92
+ margin-left: auto;
93
+ font-size: 14px;
94
+ color: #6b7280;
95
+ }
96
+ .results {
97
+ display: flex;
98
+ gap: 16px;
99
+ padding: 12px;
100
+ background: #f9fafb;
101
+ border-radius: 4px;
102
+ flex-wrap: wrap;
103
+ }
104
+ .result-item {
105
+ display: flex;
106
+ flex-direction: column;
107
+ gap: 4px;
108
+ }
109
+ .result-item .label {
110
+ font-size: 12px;
111
+ color: #6b7280;
112
+ }
113
+ .result-item .value {
114
+ font-size: 18px;
115
+ font-weight: 600;
116
+ color: #111827;
117
+ }
118
+ .result-item.total .value {
119
+ color: #059669;
120
+ }
121
+ `,
122
+ ],
123
+ })
124
+ export class BenchmarkWrapperComponent implements AfterViewInit, OnDestroy {
125
+ @ViewChild('grid') gridComponent!: ArgentGridComponent;
126
+
127
+ @Input() rowCount = 10000;
128
+
129
+ columnDefs: ColDef<Employee>[] = [
130
+ { field: 'id', headerName: 'ID', width: 80, sortable: true },
131
+ { field: 'name', headerName: 'Name', width: 200, sortable: true },
132
+ { field: 'department', headerName: 'Department', width: 180, sortable: true },
133
+ { field: 'role', headerName: 'Role', width: 250, filter: true },
134
+ { field: 'salary', headerName: 'Salary', width: 120, sortable: true, filter: 'number' },
135
+ { field: 'location', headerName: 'Location', width: 150, filter: true },
136
+ ];
137
+
138
+ rowData: Employee[] = [];
139
+ height = '500px';
140
+ width = '100%';
141
+ theme = themeQuartz;
142
+
143
+ gridOptions = {
144
+ floatingFilter: true,
145
+ enableRangeSelection: true,
146
+ defaultColDef: {
147
+ filter: true,
148
+ sortable: true,
149
+ resizable: true,
150
+ },
151
+ };
152
+
153
+ private gridApi?: GridApi<Employee>;
154
+ private fpsInterval?: number;
155
+
156
+ isRunning = false;
157
+ results: {
158
+ initialRender: number;
159
+ selectionUpdateTime: number;
160
+ groupingUpdateTime: number;
161
+ scrollFrameAverage: number;
162
+ totalTime: number;
163
+ } | null = null;
164
+
165
+ ngAfterViewInit(): void {
166
+ this.reloadData();
167
+ }
168
+
169
+ generateData(count: number): Employee[] {
170
+ const departments = [
171
+ 'Engineering',
172
+ 'Sales',
173
+ 'Marketing',
174
+ 'HR',
175
+ 'Finance',
176
+ 'Operations',
177
+ 'Support',
178
+ ];
179
+ const roles = ['Software Engineer', 'Manager', 'Director', 'VP', 'Intern', 'Analyst', 'Lead'];
180
+ const locations = [
181
+ 'New York',
182
+ 'San Francisco',
183
+ 'London',
184
+ 'Singapore',
185
+ 'Remote',
186
+ 'Berlin',
187
+ 'Tokyo',
188
+ ];
189
+
190
+ return Array.from({ length: count }, (_, i) => ({
191
+ id: i + 1,
192
+ name: `Employee ${i + 1}`,
193
+ department: departments[Math.floor(Math.random() * departments.length)],
194
+ role: roles[Math.floor(Math.random() * roles.length)],
195
+ salary: Math.floor(Math.random() * 150000) + 50000,
196
+ location: locations[Math.floor(Math.random() * locations.length)],
197
+ }));
198
+ }
199
+
200
+ reloadData(): void {
201
+ this.rowData = this.generateData(this.rowCount);
202
+ this.results = null;
203
+ }
204
+
205
+ onGridReady(api: GridApi<Employee>): void {
206
+ this.gridApi = api;
207
+ }
208
+
209
+ runBenchmark(): void {
210
+ if (!this.gridApi || !this.gridComponent || this.isRunning) return;
211
+
212
+ this.isRunning = true;
213
+ this.results = null;
214
+
215
+ const results = {
216
+ initialRender: 0,
217
+ selectionUpdateTime: 0,
218
+ groupingUpdateTime: 0,
219
+ scrollFrameAverage: 0,
220
+ totalTime: 0,
221
+ };
222
+
223
+ const startTime = performance.now();
224
+
225
+ // 1. Initial render time
226
+ results.initialRender = this.gridComponent.getLastFrameTime();
227
+
228
+ // 2. Selection Update Time
229
+ const selStart = performance.now();
230
+ this.gridApi.selectAll();
231
+ setTimeout(() => {
232
+ results.selectionUpdateTime = Number((performance.now() - selStart).toFixed(2));
233
+ this.gridApi?.deselectAll();
234
+
235
+ // 3. Grouping Toggle Time
236
+ const groupStart = performance.now();
237
+ const colDefs = this.gridApi.getColumnDefs() as ColDef<Employee>[];
238
+ const deptCol = colDefs.find((c) => c.field === 'department');
239
+ const wasGrouped = (deptCol as ColDef<Employee>)?.rowGroup;
240
+
241
+ const newColDefs = colDefs.map((col: ColDef<Employee>) => {
242
+ if (col.field === 'department') {
243
+ return { ...col, rowGroup: !wasGrouped };
244
+ }
245
+ return col;
246
+ });
247
+
248
+ this.gridApi.setColumnDefs(newColDefs);
249
+
250
+ setTimeout(() => {
251
+ // Toggle back
252
+ const revertColDefs = newColDefs.map((col: ColDef<Employee>) => {
253
+ if (col.field === 'department') {
254
+ return { ...col, rowGroup: wasGrouped ?? false };
255
+ }
256
+ return col;
257
+ });
258
+ this.gridApi.setColumnDefs(revertColDefs);
259
+
260
+ results.groupingUpdateTime = Number((performance.now() - groupStart).toFixed(2));
261
+
262
+ // 4. Scroll Test
263
+ this.runScrollTest(results, startTime);
264
+ }, 100);
265
+ }, 100);
266
+ }
267
+
268
+ private runScrollTest(results: typeof this.results, startTime: number): void {
269
+ const frameTimes: number[] = [];
270
+ let scrollCount = 0;
271
+ const totalScrollFrames = 30;
272
+ const viewport = this.gridComponent.viewportRef?.nativeElement;
273
+
274
+ if (!viewport) {
275
+ this.finishBenchmark(results, startTime);
276
+ return;
277
+ }
278
+
279
+ const runScroll = () => {
280
+ if (scrollCount < totalScrollFrames) {
281
+ viewport.scrollTop += 100;
282
+ frameTimes.push(this.gridComponent.getLastFrameTime());
283
+ scrollCount++;
284
+ requestAnimationFrame(runScroll);
285
+ } else {
286
+ results.scrollFrameAverage = Number(
287
+ (frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length).toFixed(2)
288
+ );
289
+ this.finishBenchmark(results, startTime);
290
+ }
291
+ };
292
+
293
+ // Reset scroll position first
294
+ viewport.scrollTop = 0;
295
+ setTimeout(() => requestAnimationFrame(runScroll), 100);
296
+ }
297
+
298
+ private finishBenchmark(results: typeof this.results, startTime: number): void {
299
+ results.totalTime = Number((performance.now() - startTime).toFixed(2));
300
+ this.results = results;
301
+ this.isRunning = false;
302
+
303
+ // Reset scroll
304
+ const viewport = this.gridComponent.viewportRef?.nativeElement;
305
+ if (viewport) {
306
+ viewport.scrollTop = 0;
307
+ }
308
+ }
309
+
310
+ ngOnDestroy(): void {
311
+ if (this.fpsInterval) {
312
+ cancelAnimationFrame(this.fpsInterval);
313
+ }
314
+ }
315
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist-storybook",
5
+ "baseUrl": ".",
6
+ "paths": {}
7
+ },
8
+ "include": ["src/**/*.ts", "src/storybook-app/**/*.ts"],
9
+ "exclude": ["node_modules", "dist", "dist-storybook"]
10
+ }
package/vitest.config.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { defineConfig } from 'vitest/config';
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
3
+ import { defineConfig } from 'vitest/config';
4
4
 
5
5
  export default defineConfig({
6
6
  plugins: [
@@ -20,7 +20,7 @@ export default defineConfig({
20
20
  }
21
21
  return match;
22
22
  });
23
-
23
+
24
24
  newCode = newCode.replace(/styleUrls:\s*\[\s*['"]([^'"]+)['"]\s*\]/g, (match, p1) => {
25
25
  const stylePath = path.resolve(path.dirname(id), p1);
26
26
  if (fs.existsSync(stylePath)) {
@@ -33,11 +33,11 @@ export default defineConfig({
33
33
  }
34
34
  return match;
35
35
  });
36
-
36
+
37
37
  return { code: newCode, map: null };
38
38
  }
39
- }
40
- }
39
+ },
40
+ },
41
41
  ],
42
42
  test: {
43
43
  globals: true,
@@ -48,8 +48,8 @@ export default defineConfig({
48
48
  provider: 'v8',
49
49
  reporter: ['text', 'json', 'html'],
50
50
  include: ['src/**/*.ts'],
51
- exclude: ['src/**/*.spec.ts', 'src/**/types/**']
51
+ exclude: ['src/**/*.spec.ts', 'src/**/types/**'],
52
52
  },
53
- testTimeout: 10000
54
- }
55
- });
53
+ testTimeout: 10000,
54
+ },
55
+ });