@alaarab/ogrid-angular 2.1.15 → 2.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.
package/dist/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { flattenColumns, getMultiSelectFilterFields, deriveFilterOptionsFromData, processClientSideData, validateColumns, validateRowIds, computeNextSortState, mergeFilter, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, parseValue, UndoRedoStack, rangesEqual, normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, getCellValue, computeTabNavigation, applyFillValues, computeAggregations, getDataGridStatusBarConfig, computeVisibleRange, computeTotalHeight, getScrollTopForRow, validateVirtualScrollConfig, GRID_BORDER_RADIUS, getStatusBarParts, GRID_CONTEXT_MENU_ITEMS, formatShortcut, injectGlobalStyles, buildHeaderRows, getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildPopoverEditorProps, measureColumnContentWidth, getPaginationViewModel, ROW_NUMBER_COLUMN_WIDTH, reorderColumnArray, findCtrlArrowTarget, measureRange } from '@alaarab/ogrid-core';
1
+ import { flattenColumns, getMultiSelectFilterFields, deriveFilterOptionsFromData, processClientSideData, validateColumns, processClientSideDataAsync, validateRowIds, computeNextSortState, mergeFilter, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, parseValue, UndoRedoStack, rangesEqual, normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, getCellValue, computeTabNavigation, applyFillValues, computeAggregations, getDataGridStatusBarConfig, computeVisibleRange, computeTotalHeight, computeVisibleColumnRange, getScrollTopForRow, validateVirtualScrollConfig, GRID_BORDER_RADIUS, getStatusBarParts, GRID_CONTEXT_MENU_ITEMS, formatShortcut, injectGlobalStyles, partitionColumnsForVirtualization, buildHeaderRows, getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildPopoverEditorProps, measureColumnContentWidth, getPaginationViewModel, ROW_NUMBER_COLUMN_WIDTH, reorderColumnArray, findCtrlArrowTarget, measureRange } from '@alaarab/ogrid-core';
2
2
  export * from '@alaarab/ogrid-core';
3
3
  export { CELL_PADDING, CHECKBOX_COLUMN_WIDTH, DEFAULT_DEBOUNCE_MS, DEFAULT_MIN_COLUMN_WIDTH, GRID_BORDER_RADIUS, PEOPLE_SEARCH_DEBOUNCE_MS, ROW_NUMBER_COLUMN_WIDTH, SIDEBAR_TRANSITION_MS, Z_INDEX, debounce, getCellRenderDescriptor, getHeaderFilterConfig, isInSelectionRange, normalizeSelectionRange, resolveCellDisplayContent, resolveCellStyle, toUserLike } from '@alaarab/ogrid-core';
4
4
  import { Injectable, Input, Component, ChangeDetectionStrategy, ViewEncapsulation, Output, ViewChild, inject, DestroyRef, signal, computed, effect, NgZone, EventEmitter, Injector, EnvironmentInjector, createComponent } from '@angular/core';
@@ -75,6 +75,7 @@ var OGridService = class {
75
75
  this.virtualScroll = signal(void 0);
76
76
  this.ariaLabel = signal(void 0);
77
77
  this.ariaLabelledBy = signal(void 0);
78
+ this.workerSort = signal(false);
78
79
  // --- Internal state signals ---
79
80
  this.internalData = signal([]);
80
81
  this.internalLoading = signal(false);
@@ -94,6 +95,9 @@ var OGridService = class {
94
95
  this.filterAbortController = null;
95
96
  this.refreshCounter = signal(0);
96
97
  this.firstDataRendered = signal(false);
98
+ // Worker sort async state
99
+ this.asyncClientItems = signal(null);
100
+ this.workerSortAbortId = 0;
97
101
  // Side bar state
98
102
  this.sideBarActivePanel = signal(null);
99
103
  // Filter options state
@@ -134,8 +138,9 @@ var OGridService = class {
134
138
  if (this.hasServerFilterOptions()) return this.serverFilterOptions();
135
139
  return deriveFilterOptionsFromData(this.displayData(), this.columns());
136
140
  });
141
+ /** Sync path: used when workerSort is off. */
137
142
  this.clientItemsAndTotal = computed(() => {
138
- if (!this.isClientSide()) return null;
143
+ if (!this.isClientSide() || this.workerSort()) return null;
139
144
  const rows = processClientSideData(
140
145
  this.displayData(),
141
146
  this.columns(),
@@ -148,12 +153,18 @@ var OGridService = class {
148
153
  const paged = rows.slice(start, start + this.pageSize());
149
154
  return { items: paged, totalCount: total };
150
155
  });
156
+ /** Resolved client items — sync or async depending on workerSort. */
157
+ this.resolvedClientItems = computed(() => {
158
+ const syncResult = this.clientItemsAndTotal();
159
+ if (syncResult) return syncResult;
160
+ return this.asyncClientItems();
161
+ });
151
162
  this.displayItems = computed(() => {
152
- const cit = this.clientItemsAndTotal();
163
+ const cit = this.resolvedClientItems();
153
164
  return this.isClientSide() && cit ? cit.items : this.serverItems();
154
165
  });
155
166
  this.displayTotalCount = computed(() => {
156
- const cit = this.clientItemsAndTotal();
167
+ const cit = this.resolvedClientItems();
157
168
  return this.isClientSide() && cit ? cit.totalCount : this.serverTotalCount();
158
169
  });
159
170
  this.hasActiveFilters = computed(() => {
@@ -323,6 +334,34 @@ var OGridService = class {
323
334
  validateColumns(cols);
324
335
  }
325
336
  });
337
+ effect((onCleanup) => {
338
+ if (!this.isClientSide() || !this.workerSort()) return;
339
+ const data = this.displayData();
340
+ const cols = this.columns();
341
+ const filters = this.filters();
342
+ const sortField = this.sort().field;
343
+ const sortDir = this.sort().direction;
344
+ const page = this.page();
345
+ const ps = this.pageSize();
346
+ const abortId = ++this.workerSortAbortId;
347
+ processClientSideDataAsync(data, cols, filters, sortField, sortDir).then((rows) => {
348
+ if (abortId !== this.workerSortAbortId) return;
349
+ const total = rows.length;
350
+ const start = (page - 1) * ps;
351
+ const paged = rows.slice(start, start + ps);
352
+ this.asyncClientItems.set({ items: paged, totalCount: total });
353
+ }).catch(() => {
354
+ if (abortId !== this.workerSortAbortId) return;
355
+ const rows = processClientSideData(data, cols, filters, sortField, sortDir);
356
+ const total = rows.length;
357
+ const start = (page - 1) * ps;
358
+ const paged = rows.slice(start, start + ps);
359
+ this.asyncClientItems.set({ items: paged, totalCount: total });
360
+ });
361
+ onCleanup(() => {
362
+ this.workerSortAbortId++;
363
+ });
364
+ });
326
365
  effect((onCleanup) => {
327
366
  const ds = this.dataSource();
328
367
  if (!this.isServerSide() || !ds) {
@@ -486,6 +525,13 @@ var OGridService = class {
486
525
  // --- Configure from props ---
487
526
  configure(props) {
488
527
  this.columnsProp.set(props.columns);
528
+ if (Object.keys(this.pinnedOverrides()).length === 0) {
529
+ const initial = {};
530
+ for (const col of flattenColumns(props.columns)) {
531
+ if (col.pinned) initial[col.columnId] = col.pinned;
532
+ }
533
+ if (Object.keys(initial).length > 0) this.pinnedOverrides.set(initial);
534
+ }
489
535
  this.getRowId.set(props.getRowId);
490
536
  if ("data" in props && props.data !== void 0) this.data.set(props.data);
491
537
  if ("dataSource" in props && props.dataSource !== void 0) this.dataSource.set(props.dataSource);
@@ -528,6 +574,7 @@ var OGridService = class {
528
574
  if (props.columnChooser !== void 0) this.columnChooserProp.set(props.columnChooser);
529
575
  if (props.columnReorder !== void 0) this.columnReorder.set(props.columnReorder);
530
576
  if (props.virtualScroll !== void 0) this.virtualScroll.set(props.virtualScroll);
577
+ if (props.workerSort !== void 0) this.workerSort.set(props.workerSort);
531
578
  if (props.entityLabelPlural !== void 0) this.entityLabelPlural.set(props.entityLabelPlural);
532
579
  if (props.className !== void 0) this.className.set(props.className);
533
580
  if (props.layoutMode !== void 0) this.layoutMode.set(props.layoutMode);
@@ -2229,6 +2276,10 @@ var VirtualScrollService = class {
2229
2276
  this.containerHeight = signal(0);
2230
2277
  // --- Internal state ---
2231
2278
  this.scrollTop = signal(0);
2279
+ this.scrollLeft = signal(0);
2280
+ // --- Column virtualization inputs ---
2281
+ this.columnWidths = signal([]);
2282
+ this.containerWidth = signal(0);
2232
2283
  // Scrollable container reference for programmatic scrolling
2233
2284
  this.containerEl = null;
2234
2285
  // --- Derived computed signals ---
@@ -2258,6 +2309,21 @@ var VirtualScrollService = class {
2258
2309
  });
2259
2310
  /** Total scrollable height in pixels. */
2260
2311
  this.totalHeight = computed(() => computeTotalHeight(this.totalRows(), this.rowHeight()));
2312
+ // --- Column virtualization ---
2313
+ this.columnsEnabled = computed(() => this.config().columns === true);
2314
+ this.columnOverscan = computed(() => this.config().columnOverscan ?? 2);
2315
+ /** The visible column range with spacer widths, or null when column virtualization is off. */
2316
+ this.columnRange = computed(() => {
2317
+ if (!this.columnsEnabled()) return null;
2318
+ const widths = this.columnWidths();
2319
+ if (widths.length === 0) return null;
2320
+ return computeVisibleColumnRange(
2321
+ this.scrollLeft(),
2322
+ widths,
2323
+ this.containerWidth(),
2324
+ this.columnOverscan()
2325
+ );
2326
+ });
2261
2327
  this.destroyRef.onDestroy(() => {
2262
2328
  this.containerEl = null;
2263
2329
  });
@@ -2271,10 +2337,12 @@ var VirtualScrollService = class {
2271
2337
  }
2272
2338
  /**
2273
2339
  * Call this from the container's scroll event handler.
2340
+ * Tracks both vertical and horizontal scroll positions.
2274
2341
  */
2275
2342
  onScroll(event) {
2276
2343
  const target = event.target;
2277
2344
  this.scrollTop.set(target.scrollTop);
2345
+ this.scrollLeft.set(target.scrollLeft);
2278
2346
  }
2279
2347
  /**
2280
2348
  * Scroll to a specific row index.
@@ -3404,6 +3472,53 @@ var BaseDataGridTableComponent = class {
3404
3472
  if (!this.vsEnabled()) return 0;
3405
3473
  return this.vsVisibleRange().startIndex;
3406
3474
  });
3475
+ // Column virtualization
3476
+ this.vsColumnsEnabled = computed(() => this.virtualScrollService.columnsEnabled());
3477
+ this.vsColumnRange = computed(() => this.virtualScrollService.columnRange());
3478
+ /** Partitioned columns for column virtualization: pinned-left, virtualized-unpinned, pinned-right, with spacer widths. */
3479
+ this.vsColumnPartition = computed(() => {
3480
+ if (!this.vsColumnsEnabled()) return null;
3481
+ const cols = this.visibleCols();
3482
+ const range = this.vsColumnRange();
3483
+ const props = this.getProps();
3484
+ const pinnedCols = props?.pinnedColumns;
3485
+ return partitionColumnsForVirtualization(cols, range, pinnedCols);
3486
+ });
3487
+ /** Column layouts filtered by column virtualization range (or all if column virt is off). */
3488
+ this.vsColumnLayouts = computed(() => {
3489
+ const allLayouts = this.columnLayouts();
3490
+ const partition = this.vsColumnPartition();
3491
+ if (!partition) return allLayouts;
3492
+ const visibleIds = /* @__PURE__ */ new Set();
3493
+ for (const col of partition.pinnedLeft) visibleIds.add(col.columnId);
3494
+ for (const col of partition.virtualizedUnpinned) visibleIds.add(col.columnId);
3495
+ for (const col of partition.pinnedRight) visibleIds.add(col.columnId);
3496
+ return allLayouts.filter((layout) => visibleIds.has(layout.col.columnId));
3497
+ });
3498
+ /** Visible columns filtered by column virtualization range (or all if column virt is off). */
3499
+ this.vsVisibleCols = computed(() => {
3500
+ const allCols = this.visibleCols();
3501
+ const partition = this.vsColumnPartition();
3502
+ if (!partition) return allCols;
3503
+ const visibleIds = /* @__PURE__ */ new Set();
3504
+ for (const col of partition.pinnedLeft) visibleIds.add(col.columnId);
3505
+ for (const col of partition.virtualizedUnpinned) visibleIds.add(col.columnId);
3506
+ for (const col of partition.pinnedRight) visibleIds.add(col.columnId);
3507
+ return allCols.filter((col) => visibleIds.has(col.columnId));
3508
+ });
3509
+ /** Left spacer width for column virtualization (in pixels). */
3510
+ this.vsLeftSpacerWidth = computed(() => this.vsColumnPartition()?.leftSpacerWidth ?? 0);
3511
+ /** Right spacer width for column virtualization (in pixels). */
3512
+ this.vsRightSpacerWidth = computed(() => this.vsColumnPartition()?.rightSpacerWidth ?? 0);
3513
+ /** Map from columnId to its global index in visibleCols. Used to maintain correct colIdx for cell descriptors during column virtualization. */
3514
+ this.globalColIndexMap = computed(() => {
3515
+ const cols = this.visibleCols();
3516
+ const map = /* @__PURE__ */ new Map();
3517
+ for (let i = 0; i < cols.length; i++) {
3518
+ map.set(cols[i].columnId, i);
3519
+ }
3520
+ return map;
3521
+ });
3407
3522
  // Popover editing
3408
3523
  this.popoverAnchorEl = computed(() => this.editingState().popoverAnchorEl);
3409
3524
  this.pendingEditorValueForPopover = computed(() => this.editingState().pendingEditorValue);
@@ -3527,6 +3642,10 @@ var BaseDataGridTableComponent = class {
3527
3642
  this.measuredColumnWidths.set(measured);
3528
3643
  }
3529
3644
  }
3645
+ /** Get global column index for a column (correct even during column virtualization). */
3646
+ getGlobalColIndex(col) {
3647
+ return this.globalColIndexMap().get(col.columnId) ?? 0;
3648
+ }
3530
3649
  /**
3531
3650
  * Initialize base wiring effects. Must be called from subclass constructor.
3532
3651
  *
@@ -3577,16 +3696,26 @@ var BaseDataGridTableComponent = class {
3577
3696
  this.virtualScrollService.updateConfig({
3578
3697
  enabled: p.virtualScroll.enabled,
3579
3698
  rowHeight: p.virtualScroll.rowHeight,
3580
- overscan: p.virtualScroll.overscan
3699
+ overscan: p.virtualScroll.overscan,
3700
+ columns: p.virtualScroll.columns,
3701
+ columnOverscan: p.virtualScroll.columnOverscan
3581
3702
  });
3582
3703
  }
3583
3704
  }
3584
3705
  });
3706
+ effect(() => {
3707
+ const layouts = this.columnLayouts();
3708
+ const props = this.getProps();
3709
+ const pinnedCols = props?.pinnedColumns ?? {};
3710
+ const unpinnedWidths = layouts.filter((l) => !l.pinnedLeft && !l.pinnedRight && !pinnedCols[l.col.columnId]).map((l) => l.width);
3711
+ this.virtualScrollService.columnWidths.set(unpinnedWidths);
3712
+ });
3585
3713
  effect(() => {
3586
3714
  const el = this.wrapperElSignal();
3587
3715
  if (el) {
3588
3716
  this.virtualScrollService.setContainer(el);
3589
3717
  this.virtualScrollService.containerHeight.set(el.clientHeight);
3718
+ this.virtualScrollService.containerWidth.set(el.clientWidth);
3590
3719
  }
3591
3720
  });
3592
3721
  }
@@ -134,6 +134,34 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
134
134
  readonly vsBottomSpacerHeight: import("@angular/core").Signal<number>;
135
135
  readonly vsVisibleItems: import("@angular/core").Signal<T[]>;
136
136
  readonly vsStartIndex: import("@angular/core").Signal<number>;
137
+ readonly vsColumnsEnabled: import("@angular/core").Signal<boolean>;
138
+ readonly vsColumnRange: import("@angular/core").Signal<import("@alaarab/ogrid-core").IVisibleColumnRange | null>;
139
+ /** Partitioned columns for column virtualization: pinned-left, virtualized-unpinned, pinned-right, with spacer widths. */
140
+ readonly vsColumnPartition: import("@angular/core").Signal<{
141
+ pinnedLeft: import("@alaarab/ogrid-core").IColumnDef<T>[];
142
+ virtualizedUnpinned: import("@alaarab/ogrid-core").IColumnDef<T>[];
143
+ pinnedRight: import("@alaarab/ogrid-core").IColumnDef<T>[];
144
+ leftSpacerWidth: number;
145
+ rightSpacerWidth: number;
146
+ } | null>;
147
+ /** Column layouts filtered by column virtualization range (or all if column virt is off). */
148
+ readonly vsColumnLayouts: import("@angular/core").Signal<{
149
+ col: IColumnDef<T>;
150
+ pinnedLeft: boolean;
151
+ pinnedRight: boolean;
152
+ minWidth: number;
153
+ width: number;
154
+ }[]>;
155
+ /** Visible columns filtered by column virtualization range (or all if column virt is off). */
156
+ readonly vsVisibleCols: import("@angular/core").Signal<IColumnDef<T>[]>;
157
+ /** Left spacer width for column virtualization (in pixels). */
158
+ readonly vsLeftSpacerWidth: import("@angular/core").Signal<number>;
159
+ /** Right spacer width for column virtualization (in pixels). */
160
+ readonly vsRightSpacerWidth: import("@angular/core").Signal<number>;
161
+ /** Map from columnId to its global index in visibleCols. Used to maintain correct colIdx for cell descriptors during column virtualization. */
162
+ readonly globalColIndexMap: import("@angular/core").Signal<Map<string, number>>;
163
+ /** Get global column index for a column (correct even during column virtualization). */
164
+ getGlobalColIndex(col: IColumnDef<T>): number;
137
165
  readonly popoverAnchorEl: import("@angular/core").Signal<HTMLElement | null>;
138
166
  readonly pendingEditorValueForPopover: import("@angular/core").Signal<unknown>;
139
167
  readonly allowOverflowX: import("@angular/core").Signal<boolean>;
@@ -108,6 +108,7 @@ export declare class OGridService<T> {
108
108
  readonly virtualScroll: import("@angular/core").WritableSignal<IVirtualScrollConfig | undefined>;
109
109
  readonly ariaLabel: import("@angular/core").WritableSignal<string | undefined>;
110
110
  readonly ariaLabelledBy: import("@angular/core").WritableSignal<string | undefined>;
111
+ readonly workerSort: import("@angular/core").WritableSignal<boolean>;
111
112
  private readonly internalData;
112
113
  private readonly internalLoading;
113
114
  private readonly internalPage;
@@ -125,6 +126,8 @@ export declare class OGridService<T> {
125
126
  private filterAbortController;
126
127
  private readonly refreshCounter;
127
128
  private readonly firstDataRendered;
129
+ private readonly asyncClientItems;
130
+ private workerSortAbortId;
128
131
  private readonly sideBarActivePanel;
129
132
  private readonly serverFilterOptions;
130
133
  private readonly loadingFilterOptions;
@@ -147,10 +150,16 @@ export declare class OGridService<T> {
147
150
  readonly multiSelectFilterFields: import("@angular/core").Signal<string[]>;
148
151
  readonly hasServerFilterOptions: import("@angular/core").Signal<boolean>;
149
152
  readonly clientFilterOptions: import("@angular/core").Signal<Record<string, string[]>>;
153
+ /** Sync path: used when workerSort is off. */
150
154
  readonly clientItemsAndTotal: import("@angular/core").Signal<{
151
155
  items: T[];
152
156
  totalCount: number;
153
157
  } | null>;
158
+ /** Resolved client items — sync or async depending on workerSort. */
159
+ readonly resolvedClientItems: import("@angular/core").Signal<{
160
+ items: T[];
161
+ totalCount: number;
162
+ } | null>;
154
163
  readonly displayItems: import("@angular/core").Signal<T[]>;
155
164
  readonly displayTotalCount: import("@angular/core").Signal<number>;
156
165
  readonly hasActiveFilters: import("@angular/core").Signal<boolean>;
@@ -1,4 +1,4 @@
1
- import type { IVisibleRange, IVirtualScrollConfig } from '@alaarab/ogrid-core';
1
+ import type { IVisibleRange, IVisibleColumnRange, IVirtualScrollConfig } from '@alaarab/ogrid-core';
2
2
  /**
3
3
  * Manages virtual scrolling state using Angular signals.
4
4
  * Port of React's useVirtualScroll hook.
@@ -13,6 +13,9 @@ export declare class VirtualScrollService {
13
13
  readonly config: import("@angular/core").WritableSignal<IVirtualScrollConfig>;
14
14
  readonly containerHeight: import("@angular/core").WritableSignal<number>;
15
15
  readonly scrollTop: import("@angular/core").WritableSignal<number>;
16
+ readonly scrollLeft: import("@angular/core").WritableSignal<number>;
17
+ readonly columnWidths: import("@angular/core").WritableSignal<number[]>;
18
+ readonly containerWidth: import("@angular/core").WritableSignal<number>;
16
19
  private containerEl;
17
20
  readonly rowHeight: import("@angular/core").Signal<number>;
18
21
  readonly overscan: import("@angular/core").Signal<number>;
@@ -24,6 +27,10 @@ export declare class VirtualScrollService {
24
27
  readonly visibleRange: import("@angular/core").Signal<IVisibleRange>;
25
28
  /** Total scrollable height in pixels. */
26
29
  readonly totalHeight: import("@angular/core").Signal<number>;
30
+ readonly columnsEnabled: import("@angular/core").Signal<boolean>;
31
+ readonly columnOverscan: import("@angular/core").Signal<number>;
32
+ /** The visible column range with spacer widths, or null when column virtualization is off. */
33
+ readonly columnRange: import("@angular/core").Signal<IVisibleColumnRange | null>;
27
34
  constructor();
28
35
  /**
29
36
  * Set the scrollable container element.
@@ -32,6 +39,7 @@ export declare class VirtualScrollService {
32
39
  setContainer(el: HTMLElement | null): void;
33
40
  /**
34
41
  * Call this from the container's scroll event handler.
42
+ * Tracks both vertical and horizontal scroll positions.
35
43
  */
36
44
  onScroll(event: Event): void;
37
45
  /**
@@ -63,6 +63,8 @@ interface IOGridBaseProps<T> {
63
63
  sideBar?: boolean | ISideBarDef;
64
64
  columnReorder?: boolean;
65
65
  virtualScroll?: IVirtualScrollConfig;
66
+ /** Offload sort/filter to a Web Worker for large datasets. Falls back to sync when sort column has a custom compare. */
67
+ workerSort?: boolean;
66
68
  /** Fixed row height in pixels. Overrides default row height (36px). */
67
69
  rowHeight?: number;
68
70
  pageSizeOptions?: number[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-angular",
3
- "version": "2.1.15",
3
+ "version": "2.2.0",
4
4
  "description": "OGrid Angular – Angular services, signals, and headless components for OGrid data grids.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -35,7 +35,7 @@
35
35
  "node": ">=18"
36
36
  },
37
37
  "dependencies": {
38
- "@alaarab/ogrid-core": "2.1.15"
38
+ "@alaarab/ogrid-core": "2.2.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@angular/core": "^21.0.0",