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,307 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ EventEmitter,
5
+ Input,
6
+ OnInit,
7
+ Output,
8
+ } from '@angular/core';
9
+
10
+ @Component({
11
+ selector: 'argent-set-filter',
12
+ standalone: true,
13
+ imports: [],
14
+ changeDetection: ChangeDetectionStrategy.OnPush,
15
+ template: `
16
+ <div class="set-filter">
17
+ <!-- Search Box -->
18
+ <div class="set-filter-search">
19
+ <input
20
+ type="text"
21
+ [value]="searchText"
22
+ (input)="onSearchInput($event)"
23
+ placeholder="Search..."
24
+ class="set-filter-input"
25
+ />
26
+ </div>
27
+
28
+ <!-- Select All / Clear All -->
29
+ <div class="set-filter-actions">
30
+ <button
31
+ type="button"
32
+ (click)="selectAll()"
33
+ class="set-filter-btn"
34
+ >
35
+ Select All
36
+ </button>
37
+ <button
38
+ type="button"
39
+ (click)="clearAll()"
40
+ class="set-filter-btn"
41
+ >
42
+ Clear All
43
+ </button>
44
+ </div>
45
+
46
+ <!-- Checkbox List -->
47
+ <div class="set-filter-list">
48
+ @for (item of filteredValues; track item.value) {
49
+ <label class="set-filter-item">
50
+ <input
51
+ type="checkbox"
52
+ [checked]="item.selected"
53
+ (change)="onValueToggled(item.value, $event)"
54
+ />
55
+ <span class="set-filter-label">{{ item.displayValue }}</span>
56
+ <span class="set-filter-count">({{ item.count }})</span>
57
+ </label>
58
+ } @empty {
59
+ <div class="set-filter-empty">
60
+ No values found
61
+ </div>
62
+ }
63
+ </div>
64
+
65
+ <!-- Action Buttons -->
66
+ <div class="set-filter-footer">
67
+ <button
68
+ type="button"
69
+ (click)="applyFilter()"
70
+ class="set-filter-apply"
71
+ >
72
+ Apply
73
+ </button>
74
+ <button
75
+ type="button"
76
+ (click)="resetFilter()"
77
+ class="set-filter-reset"
78
+ >
79
+ Reset
80
+ </button>
81
+ </div>
82
+ </div>
83
+ `,
84
+ styles: [
85
+ `
86
+ .set-filter {
87
+ display: flex;
88
+ flex-direction: column;
89
+ width: 250px;
90
+ max-height: 400px;
91
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
92
+ font-size: 13px;
93
+ }
94
+
95
+ .set-filter-search {
96
+ padding: 8px;
97
+ border-bottom: 1px solid #e0e0e0;
98
+ }
99
+
100
+ .set-filter-input {
101
+ width: 100%;
102
+ padding: 6px 10px;
103
+ border: 1px solid #d0d0d0;
104
+ border-radius: 4px;
105
+ font-size: 13px;
106
+ box-sizing: border-box;
107
+ }
108
+
109
+ .set-filter-input:focus {
110
+ outline: none;
111
+ border-color: #4f46e5;
112
+ box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2);
113
+ }
114
+
115
+ .set-filter-actions {
116
+ display: flex;
117
+ gap: 8px;
118
+ padding: 8px;
119
+ border-bottom: 1px solid #e0e0e0;
120
+ }
121
+
122
+ .set-filter-btn {
123
+ flex: 1;
124
+ padding: 4px 8px;
125
+ border: 1px solid #d0d0d0;
126
+ border-radius: 4px;
127
+ background: #fff;
128
+ cursor: pointer;
129
+ font-size: 12px;
130
+ }
131
+
132
+ .set-filter-btn:hover {
133
+ background: #f5f5f5;
134
+ border-color: #4f46e5;
135
+ }
136
+
137
+ .set-filter-list {
138
+ flex: 1;
139
+ overflow-y: auto;
140
+ padding: 8px 0;
141
+ max-height: 250px;
142
+ }
143
+
144
+ .set-filter-item {
145
+ display: flex;
146
+ align-items: center;
147
+ padding: 4px 12px;
148
+ cursor: pointer;
149
+ gap: 8px;
150
+ }
151
+
152
+ .set-filter-item:hover {
153
+ background: #f5f5f5;
154
+ }
155
+
156
+ .set-filter-item input[type="checkbox"] {
157
+ margin: 0;
158
+ cursor: pointer;
159
+ }
160
+
161
+ .set-filter-label {
162
+ flex: 1;
163
+ overflow: hidden;
164
+ text-overflow: ellipsis;
165
+ white-space: nowrap;
166
+ }
167
+
168
+ .set-filter-count {
169
+ color: #999;
170
+ font-size: 11px;
171
+ margin-left: 4px;
172
+ }
173
+
174
+ .set-filter-empty {
175
+ padding: 16px;
176
+ text-align: center;
177
+ color: #999;
178
+ font-style: italic;
179
+ }
180
+
181
+ .set-filter-footer {
182
+ display: flex;
183
+ gap: 8px;
184
+ padding: 8px;
185
+ border-top: 1px solid #e0e0e0;
186
+ }
187
+
188
+ .set-filter-apply,
189
+ .set-filter-reset {
190
+ flex: 1;
191
+ padding: 6px 12px;
192
+ border: none;
193
+ border-radius: 4px;
194
+ cursor: pointer;
195
+ font-size: 13px;
196
+ }
197
+
198
+ .set-filter-apply {
199
+ background: #4f46e5;
200
+ color: #fff;
201
+ }
202
+
203
+ .set-filter-apply:hover {
204
+ background: #4338ca;
205
+ }
206
+
207
+ .set-filter-reset {
208
+ background: #f5f5f5;
209
+ color: #333;
210
+ }
211
+
212
+ .set-filter-reset:hover {
213
+ background: #e5e5e5;
214
+ }
215
+ `,
216
+ ],
217
+ })
218
+ export class SetFilterComponent<T = any> implements OnInit {
219
+ @Input() values: T[] = [];
220
+ @Input() valueFormatter?: (value: T) => string;
221
+ /** When provided, only these values will be pre-checked (restores existing filter state) */
222
+ @Input() initialSelectedValues: T[] | null = null;
223
+ @Output() filterChanged = new EventEmitter<T[]>();
224
+
225
+ searchText = '';
226
+ selectedValues: T[] = [];
227
+ allValues: Array<{ value: T; displayValue: string; count: number; selected: boolean }> = [];
228
+
229
+ get filteredValues() {
230
+ if (!this.searchText) {
231
+ return this.allValues;
232
+ }
233
+ const search = this.searchText.toLowerCase();
234
+ return this.allValues.filter((item) => item.displayValue.toLowerCase().includes(search));
235
+ }
236
+
237
+ ngOnInit(): void {
238
+ this.initializeValues();
239
+ }
240
+
241
+ onSearchInput(event: Event): void {
242
+ this.searchText = (event.target as HTMLInputElement).value;
243
+ }
244
+
245
+ private initializeValues(): void {
246
+ // Count occurrences of each value
247
+ const valueCounts = new Map<T, number>();
248
+ this.values.forEach((value) => {
249
+ valueCounts.set(value, (valueCounts.get(value) || 0) + 1);
250
+ });
251
+
252
+ // Build value list with counts
253
+ // If initialSelectedValues provided, pre-check only those; otherwise check all
254
+ const preSelected = this.initialSelectedValues;
255
+ this.allValues = Array.from(valueCounts.entries()).map(([value, count]) => ({
256
+ value,
257
+ displayValue: this.valueFormatter ? this.valueFormatter(value) : String(value),
258
+ count,
259
+ selected: preSelected ? preSelected.includes(value) : true,
260
+ }));
261
+
262
+ this.selectedValues = this.allValues.filter((v) => v.selected).map((v) => v.value);
263
+ }
264
+
265
+ onSearchChanged(): void {
266
+ // Trigger change detection through filteredValues getter
267
+ }
268
+
269
+ onValueToggled(value: T, event: Event): void {
270
+ const checkbox = event.target as HTMLInputElement;
271
+ const item = this.allValues.find((v) => v.value === value);
272
+ if (item) {
273
+ item.selected = checkbox.checked;
274
+ }
275
+
276
+ this.updateSelectedValues();
277
+ }
278
+
279
+ selectAll(): void {
280
+ this.allValues.forEach((item) => {
281
+ item.selected = true;
282
+ });
283
+ this.updateSelectedValues();
284
+ }
285
+
286
+ clearAll(): void {
287
+ this.allValues.forEach((item) => {
288
+ item.selected = false;
289
+ });
290
+ this.updateSelectedValues();
291
+ }
292
+
293
+ private updateSelectedValues(): void {
294
+ this.selectedValues = this.allValues.filter((item) => item.selected).map((item) => item.value);
295
+ }
296
+
297
+ applyFilter(): void {
298
+ this.filterChanged.emit(this.selectedValues);
299
+ }
300
+
301
+ resetFilter(): void {
302
+ this.searchText = '';
303
+ this.initialSelectedValues = null; // Reset to all-selected state
304
+ this.initializeValues();
305
+ this.filterChanged.emit(this.selectedValues);
306
+ }
307
+ }
@@ -1,24 +1,16 @@
1
- import {
2
- Directive,
3
- Input,
4
- OnInit,
5
- OnDestroy,
6
- ComponentRef,
7
- ViewContainerRef,
8
- ComponentFactoryResolver
9
- } from '@angular/core';
10
- import { GridOptions, ColDef, ColGroupDef } from '../types/ag-grid-types';
1
+ import { ComponentRef, Directive, Input, OnDestroy, OnInit, ViewContainerRef } from '@angular/core';
11
2
  import { ArgentGridComponent } from '../components/argent-grid.component';
3
+ import { ColDef, ColGroupDef, GridOptions } from '../types/ag-grid-types';
12
4
 
13
5
  /**
14
6
  * AgGridCompatibilityDirective - Drop-in replacement for AG Grid
15
- *
7
+ *
16
8
  * This directive allows users to migrate from AG Grid to ArgentGrid
17
9
  * by simply changing their import statement. It maps AG Grid's
18
10
  * [columnDefs] and [rowData] inputs to ArgentGrid's internal format.
19
11
  */
20
12
  @Directive({
21
- selector: '[argentGrid], ag-grid-angular'
13
+ selector: '[argentGrid], ag-grid-angular',
22
14
  })
23
15
  export class AgGridCompatibilityDirective<TData = any> implements OnInit, OnDestroy {
24
16
  @Input('columnDefs') set columnDefs(value: (ColDef<TData> | ColGroupDef<TData>)[] | null) {
@@ -29,7 +21,7 @@ export class AgGridCompatibilityDirective<TData = any> implements OnInit, OnDest
29
21
  return this._columnDefs;
30
22
  }
31
23
  private _columnDefs: (ColDef<TData> | ColGroupDef<TData>)[] | null = null;
32
-
24
+
33
25
  @Input('rowData') set rowData(value: TData[] | null) {
34
26
  this._rowData = value;
35
27
  this.updateGrid();
@@ -38,7 +30,7 @@ export class AgGridCompatibilityDirective<TData = any> implements OnInit, OnDest
38
30
  return this._rowData;
39
31
  }
40
32
  private _rowData: TData[] | null = null;
41
-
33
+
42
34
  @Input('gridOptions') set gridOptions(value: GridOptions<TData>) {
43
35
  this._gridOptions = value;
44
36
  this.updateGrid();
@@ -47,42 +39,40 @@ export class AgGridCompatibilityDirective<TData = any> implements OnInit, OnDest
47
39
  return this._gridOptions;
48
40
  }
49
41
  private _gridOptions: GridOptions<TData> | null = null;
50
-
42
+
51
43
  private gridComponentRef: ComponentRef<ArgentGridComponent<TData>> | null = null;
52
-
53
- constructor(
54
- private viewContainerRef: ViewContainerRef
55
- ) {}
56
-
44
+
45
+ constructor(private viewContainerRef: ViewContainerRef) {}
46
+
57
47
  ngOnInit(): void {
58
48
  this.createGridComponent();
59
49
  }
60
-
50
+
61
51
  ngOnDestroy(): void {
62
52
  if (this.gridComponentRef) {
63
53
  this.gridComponentRef.destroy();
64
54
  }
65
55
  }
66
-
56
+
67
57
  private createGridComponent(): void {
68
58
  // Create ArgentGrid component
69
59
  this.gridComponentRef = this.viewContainerRef.createComponent(ArgentGridComponent as any);
70
-
60
+
71
61
  this.updateGrid();
72
62
  }
73
-
63
+
74
64
  private updateGrid(): void {
75
65
  if (!this.gridComponentRef) {
76
66
  return;
77
67
  }
78
-
68
+
79
69
  // Map AG Grid inputs to ArgentGrid component
80
70
  this.gridComponentRef.instance.columnDefs = this.columnDefs;
81
71
  this.gridComponentRef.instance.rowData = this.rowData;
82
72
  this.gridComponentRef.instance.gridOptions = this.gridOptions || undefined;
83
73
  this.gridComponentRef.instance.refresh();
84
74
  }
85
-
75
+
86
76
  /**
87
77
  * Get the underlying GridApi for programmatic access
88
78
  */
@@ -0,0 +1,19 @@
1
+ import { Directive, ElementRef, EventEmitter, HostListener, Output } from '@angular/core';
2
+
3
+ @Directive({
4
+ selector: '[clickOutside]',
5
+ standalone: true,
6
+ })
7
+ export class ClickOutsideDirective {
8
+ @Output() clickOutside = new EventEmitter<void>();
9
+
10
+ constructor(private elementRef: ElementRef) {}
11
+
12
+ @HostListener('document:click', ['$event.target'])
13
+ onClick(target: HTMLElement): void {
14
+ const clickedInside = this.elementRef.nativeElement.contains(target);
15
+ if (!clickedInside) {
16
+ this.clickOutside.emit();
17
+ }
18
+ }
19
+ }