@alaarab/ogrid-angular 2.0.19 → 2.0.22

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.
@@ -20,6 +20,8 @@ export class BaseDataGridTableComponent {
20
20
  this.virtualScrollService = new VirtualScrollService();
21
21
  this.lastMouseShift = false;
22
22
  this.columnSizingVersion = signal(0);
23
+ /** Dirty flag — set when column layout changes, cleared after measurement. */
24
+ this.measureDirty = true;
23
25
  /** DOM-measured column widths from the last layout pass.
24
26
  * Used as a minWidth floor to prevent columns from shrinking
25
27
  * when new data loads (e.g. server-side pagination). */
@@ -38,6 +40,10 @@ export class BaseDataGridTableComponent {
38
40
  this.isLoading = computed(() => this.getProps()?.isLoading ?? false);
39
41
  this.loadingMessage = computed(() => 'Loading\u2026');
40
42
  this.layoutModeFit = computed(() => (this.getProps()?.layoutMode ?? 'fill') === 'content');
43
+ this.rowHeightCssVar = computed(() => {
44
+ const rh = this.getProps()?.rowHeight;
45
+ return rh ? `${rh}px` : null;
46
+ });
41
47
  this.ariaLabel = computed(() => this.getProps()?.['aria-label'] ?? 'Data grid');
42
48
  this.ariaLabelledBy = computed(() => this.getProps()?.['aria-labelledby']);
43
49
  this.emptyState = computed(() => this.getProps()?.emptyState);
@@ -152,34 +158,44 @@ export class BaseDataGridTableComponent {
152
158
  };
153
159
  });
154
160
  });
155
- // Compute sticky offsets for pinned columns (cumulative left/right positions)
161
+ // Compute sticky offsets for pinned columns (single pass from both ends)
156
162
  this.pinningOffsets = computed(() => {
157
163
  const layouts = this.columnLayouts();
158
164
  const leftOffsets = {};
159
165
  const rightOffsets = {};
160
- // Left offsets: start after checkbox and row number columns
161
166
  let leftAcc = 0;
162
167
  if (this.hasCheckboxCol())
163
168
  leftAcc += CHECKBOX_COLUMN_WIDTH;
164
169
  if (this.hasRowNumbersCol())
165
170
  leftAcc += ROW_NUMBER_COLUMN_WIDTH;
166
- for (const layout of layouts) {
167
- if (layout.pinnedLeft) {
168
- leftOffsets[layout.col.columnId] = leftAcc;
169
- leftAcc += layout.width + CELL_PADDING;
170
- }
171
- }
172
- // Right offsets: walk from the end
173
171
  let rightAcc = 0;
174
- for (let i = layouts.length - 1; i >= 0; i--) {
175
- const layout = layouts[i];
176
- if (layout.pinnedRight) {
177
- rightOffsets[layout.col.columnId] = rightAcc;
178
- rightAcc += layout.width + CELL_PADDING;
172
+ const len = layouts.length;
173
+ for (let i = 0; i < len; i++) {
174
+ // Left-pinned: walk forward
175
+ const leftLayout = layouts[i];
176
+ if (leftLayout.pinnedLeft) {
177
+ leftOffsets[leftLayout.col.columnId] = leftAcc;
178
+ leftAcc += leftLayout.width + CELL_PADDING;
179
+ }
180
+ // Right-pinned: walk backward
181
+ const ri = len - 1 - i;
182
+ const rightLayout = layouts[ri];
183
+ if (rightLayout.pinnedRight) {
184
+ rightOffsets[rightLayout.col.columnId] = rightAcc;
185
+ rightAcc += rightLayout.width + CELL_PADDING;
179
186
  }
180
187
  }
181
188
  return { leftOffsets, rightOffsets };
182
189
  });
190
+ /** Memoized column menu handlers — avoids recreating objects on every CD cycle */
191
+ this.columnMenuHandlersMap = computed(() => {
192
+ const cols = this.visibleCols();
193
+ const map = new Map();
194
+ for (const col of cols) {
195
+ map.set(col.columnId, this.buildColumnMenuHandlers(col.columnId));
196
+ }
197
+ return map;
198
+ });
183
199
  }
184
200
  /** Lifecycle hook — populate element signals from @ViewChild refs */
185
201
  ngAfterViewInit() {
@@ -191,9 +207,12 @@ export class BaseDataGridTableComponent {
191
207
  this.tableContainerElSignal.set(tableContainer);
192
208
  this.measureColumnWidths();
193
209
  }
194
- /** Lifecycle hook — re-measure column widths after each view update */
210
+ /** Lifecycle hook — re-measure column widths only when layout changed */
195
211
  ngAfterViewChecked() {
196
- this.measureColumnWidths();
212
+ if (this.measureDirty) {
213
+ this.measureDirty = false;
214
+ this.measureColumnWidths();
215
+ }
197
216
  }
198
217
  /** Measure actual th widths from the DOM and update the measuredColumnWidths signal.
199
218
  * Only updates the signal when values actually change, to avoid render loops. */
@@ -255,6 +274,14 @@ export class BaseDataGridTableComponent {
255
274
  this.columnReorderService.enabled.set(p.columnReorder === true);
256
275
  }
257
276
  });
277
+ // Mark measurement dirty when column layout changes
278
+ effect(() => {
279
+ // Track signals that affect column layout
280
+ this.visibleCols();
281
+ this.columnSizingOverrides();
282
+ this.columnSizingVersion();
283
+ this.measureDirty = true;
284
+ });
258
285
  // Wire virtual scroll service inputs
259
286
  effect(() => {
260
287
  const p = this.getProps();
@@ -314,6 +341,24 @@ export class BaseDataGridTableComponent {
314
341
  getFilterConfig(col) {
315
342
  return getHeaderFilterConfig(col, this.headerFilterInput());
316
343
  }
344
+ /** Build column menu handler object for a single column */
345
+ buildColumnMenuHandlers(columnId) {
346
+ return {
347
+ onPinLeft: () => this.onPinColumn(columnId, 'left'),
348
+ onPinRight: () => this.onPinColumn(columnId, 'right'),
349
+ onUnpin: () => this.onUnpinColumn(columnId),
350
+ onSortAsc: () => this.onSortAsc(columnId),
351
+ onSortDesc: () => this.onSortDesc(columnId),
352
+ onClearSort: () => this.onClearSort(columnId),
353
+ onAutosizeThis: () => this.onAutosizeColumn(columnId),
354
+ onAutosizeAll: () => this.onAutosizeAllColumns(),
355
+ onClose: () => { }
356
+ };
357
+ }
358
+ /** Get memoized handlers for a column */
359
+ getColumnMenuHandlersMemoized(columnId) {
360
+ return this.columnMenuHandlersMap().get(columnId) ?? this.buildColumnMenuHandlers(columnId);
361
+ }
317
362
  getCellDescriptor(item, col, rowIndex, colIdx) {
318
363
  return getCellRenderDescriptor(item, col, rowIndex, colIdx, this.cellDescriptorInput());
319
364
  }
@@ -330,6 +375,48 @@ export class BaseDataGridTableComponent {
330
375
  cancelPopoverEdit: () => this.cancelPopoverEdit(),
331
376
  });
332
377
  }
378
+ /** Check if a specific cell is the active cell (PrimeNG inline template helper). */
379
+ isActiveCell(rowIndex, colIdx) {
380
+ const ac = this.activeCell();
381
+ if (!ac)
382
+ return false;
383
+ return ac.rowIndex === rowIndex && ac.columnIndex === colIdx + this.colOffset();
384
+ }
385
+ /** Check if a cell is within the current selection range (PrimeNG inline template helper). */
386
+ isInSelectionRange(rowIndex, colIdx) {
387
+ const range = this.selectionRange();
388
+ if (!range)
389
+ return false;
390
+ const minR = Math.min(range.startRow, range.endRow);
391
+ const maxR = Math.max(range.startRow, range.endRow);
392
+ const minC = Math.min(range.startCol, range.endCol);
393
+ const maxC = Math.max(range.startCol, range.endCol);
394
+ return rowIndex >= minR && rowIndex <= maxR && colIdx >= minC && colIdx <= maxC;
395
+ }
396
+ /** Check if a cell is the selection end cell for fill handle display. */
397
+ isSelectionEndCell(rowIndex, colIdx) {
398
+ const range = this.selectionRange();
399
+ if (!range || this.isDragging() || this.copyRange() || this.cutRange())
400
+ return false;
401
+ return rowIndex === range.endRow && colIdx === range.endCol;
402
+ }
403
+ /** Get cell background color based on selection state. */
404
+ getCellBackground(rowIndex, colIdx) {
405
+ if (this.isInSelectionRange(rowIndex, colIdx))
406
+ return 'var(--ogrid-range-bg, rgba(33, 115, 70, 0.08))';
407
+ return null;
408
+ }
409
+ /** Resolve editor type from column definition. */
410
+ getEditorType(col, _item) {
411
+ if (col.cellEditor === 'text' || col.cellEditor === 'select' || col.cellEditor === 'checkbox' || col.cellEditor === 'date' || col.cellEditor === 'richSelect') {
412
+ return col.cellEditor;
413
+ }
414
+ if (col.type === 'date')
415
+ return 'date';
416
+ if (col.type === 'boolean')
417
+ return 'checkbox';
418
+ return 'text';
419
+ }
333
420
  getSelectValues(col) {
334
421
  const params = col.cellEditorParams;
335
422
  if (params && typeof params === 'object' && 'values' in params) {
@@ -524,7 +611,7 @@ export class BaseDataGridTableComponent {
524
611
  ...this.columnSizingOverrides(),
525
612
  [columnId]: { widthPx: width },
526
613
  });
527
- this.state().layout.onColumnResized?.(columnId, width);
614
+ (this.state().layout.onAutosizeColumn ?? this.state().layout.onColumnResized)?.(columnId, width);
528
615
  }
529
616
  onAutosizeAllColumns() {
530
617
  const tableEl = this.tableContainerEl() ?? undefined;
@@ -532,7 +619,7 @@ export class BaseDataGridTableComponent {
532
619
  for (const col of this.visibleCols()) {
533
620
  const width = measureColumnContentWidth(col.columnId, col.minWidth, tableEl);
534
621
  overrides[col.columnId] = { widthPx: width };
535
- this.state().layout.onColumnResized?.(col.columnId, width);
622
+ (this.state().layout.onAutosizeColumn ?? this.state().layout.onColumnResized)?.(col.columnId, width);
536
623
  }
537
624
  this.state().layout.setColumnSizingOverrides({
538
625
  ...this.columnSizingOverrides(),
@@ -0,0 +1,115 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Input, ViewChild, Injector, EnvironmentInjector, inject, signal, effect, createComponent } from '@angular/core';
8
+ /**
9
+ * Shared popover cell editor template used by all Angular UI packages.
10
+ */
11
+ export const POPOVER_CELL_EDITOR_TEMPLATE = `
12
+ <div #anchorEl
13
+ class="ogrid-popover-anchor"
14
+ [attr.data-row-index]="rowIndex"
15
+ [attr.data-col-index]="globalColIndex"
16
+ >
17
+ {{ displayValue }}
18
+ </div>
19
+ @if (showEditor()) {
20
+ <div class="ogrid-popover-editor-overlay" (click)="handleOverlayClick()">
21
+ <div class="ogrid-popover-editor-content" #editorContainer></div>
22
+ </div>
23
+ }
24
+ `;
25
+ /**
26
+ * Shared overlay + content styles for popover cell editors.
27
+ * Subclasses provide their own .ogrid-popover-anchor styles.
28
+ */
29
+ export const POPOVER_CELL_EDITOR_OVERLAY_STYLES = `
30
+ :host { display: contents; }
31
+ .ogrid-popover-editor-overlay {
32
+ position: fixed; inset: 0; z-index: 1000;
33
+ background: rgba(0,0,0,0.3);
34
+ display: flex; align-items: center; justify-content: center;
35
+ }
36
+ .ogrid-popover-editor-content {
37
+ background: var(--ogrid-bg, #ffffff); border-radius: 4px; padding: 16px;
38
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
39
+ max-width: 90vw; max-height: 90vh; overflow: auto;
40
+ color: var(--ogrid-fg, rgba(0, 0, 0, 0.87));
41
+ }
42
+ `;
43
+ /**
44
+ * Abstract base class for Angular popover cell editors.
45
+ * Contains all shared inputs, ViewChild refs, effects, and overlay click handling.
46
+ *
47
+ * Subclasses only need a @Component decorator with selector, template, and
48
+ * framework-specific .ogrid-popover-anchor CSS styles.
49
+ */
50
+ export class BasePopoverCellEditorComponent {
51
+ constructor() {
52
+ this.injector = inject(Injector);
53
+ this.envInjector = inject(EnvironmentInjector);
54
+ this.showEditor = signal(false);
55
+ // Show editor after anchor is rendered
56
+ effect(() => {
57
+ const anchor = this.anchorRef;
58
+ if (anchor) {
59
+ setTimeout(() => this.showEditor.set(true), 0);
60
+ }
61
+ });
62
+ // Render custom editor component when container is available
63
+ effect(() => {
64
+ const container = this.editorContainerRef;
65
+ const props = this.editorProps;
66
+ const col = this.column;
67
+ if (!container || !this.showEditor() || typeof col.cellEditor !== 'function')
68
+ return;
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ const EditorComponent = col.cellEditor; // ComponentType
71
+ const componentRef = createComponent(EditorComponent, {
72
+ environmentInjector: this.envInjector,
73
+ elementInjector: this.injector,
74
+ });
75
+ // Pass props to component instance
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ Object.assign(componentRef.instance, props);
78
+ componentRef.changeDetectorRef.detectChanges();
79
+ // Append to DOM
80
+ container.nativeElement.appendChild(componentRef.location.nativeElement);
81
+ // Cleanup on destroy
82
+ return () => componentRef.destroy();
83
+ });
84
+ }
85
+ handleOverlayClick() {
86
+ this.onCancel();
87
+ }
88
+ }
89
+ __decorate([
90
+ Input({ required: true })
91
+ ], BasePopoverCellEditorComponent.prototype, "item", void 0);
92
+ __decorate([
93
+ Input({ required: true })
94
+ ], BasePopoverCellEditorComponent.prototype, "column", void 0);
95
+ __decorate([
96
+ Input({ required: true })
97
+ ], BasePopoverCellEditorComponent.prototype, "rowIndex", void 0);
98
+ __decorate([
99
+ Input({ required: true })
100
+ ], BasePopoverCellEditorComponent.prototype, "globalColIndex", void 0);
101
+ __decorate([
102
+ Input({ required: true })
103
+ ], BasePopoverCellEditorComponent.prototype, "displayValue", void 0);
104
+ __decorate([
105
+ Input({ required: true })
106
+ ], BasePopoverCellEditorComponent.prototype, "editorProps", void 0);
107
+ __decorate([
108
+ Input({ required: true })
109
+ ], BasePopoverCellEditorComponent.prototype, "onCancel", void 0);
110
+ __decorate([
111
+ ViewChild('anchorEl')
112
+ ], BasePopoverCellEditorComponent.prototype, "anchorRef", void 0);
113
+ __decorate([
114
+ ViewChild('editorContainer')
115
+ ], BasePopoverCellEditorComponent.prototype, "editorContainerRef", void 0);
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { Component, Input, Output, EventEmitter } from '@angular/core';
7
+ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
8
8
  import { CommonModule } from '@angular/common';
9
9
  let EmptyStateComponent = class EmptyStateComponent {
10
10
  constructor() {
@@ -30,6 +30,7 @@ EmptyStateComponent = __decorate([
30
30
  Component({
31
31
  selector: 'ogrid-empty-state',
32
32
  standalone: true,
33
+ changeDetection: ChangeDetectionStrategy.OnPush,
33
34
  imports: [CommonModule],
34
35
  styles: [`
35
36
  .ogrid-empty-state-clear-btn {
@@ -4,8 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { Component, Input, Output, EventEmitter, ViewChild, DestroyRef, inject } from '@angular/core';
8
- import { CommonModule } from '@angular/common';
7
+ import { Component, Input, Output, EventEmitter, ViewChild, DestroyRef, inject, ChangeDetectionStrategy } from '@angular/core';
9
8
  import { GRID_CONTEXT_MENU_ITEMS, formatShortcut } from '@alaarab/ogrid-core';
10
9
  let GridContextMenuComponent = class GridContextMenuComponent {
11
10
  constructor() {
@@ -119,7 +118,7 @@ GridContextMenuComponent = __decorate([
119
118
  Component({
120
119
  selector: 'ogrid-context-menu',
121
120
  standalone: true,
122
- imports: [CommonModule],
121
+ changeDetection: ChangeDetectionStrategy.OnPush,
123
122
  template: `
124
123
  <div
125
124
  #menuRef
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Shared inline cell editor template used by all Angular UI packages.
3
+ * The template is identical across Material, PrimeNG, and Radix implementations.
4
+ */
5
+ export const INLINE_CELL_EDITOR_TEMPLATE = `
6
+ @switch (editorType) {
7
+ @case ('text') {
8
+ <input
9
+ #inputEl
10
+ type="text"
11
+ [value]="localValue()"
12
+ (input)="localValue.set($any($event.target).value)"
13
+ (keydown)="onTextKeyDown($event)"
14
+ (blur)="onTextBlur()"
15
+ [style]="getInputStyle()"
16
+ />
17
+ }
18
+ @case ('richSelect') {
19
+ <div #richSelectWrapper
20
+ style="width:100%;height:100%;display:flex;align-items:center;padding:6px 10px;box-sizing:border-box;min-width:0;position:relative">
21
+ <input
22
+ #richSelectInput
23
+ type="text"
24
+ [value]="searchText()"
25
+ (input)="onRichSelectSearch($any($event.target).value)"
26
+ (keydown)="onRichSelectKeyDown($event)"
27
+ placeholder="Search..."
28
+ style="width:100%;padding:0;border:none;background:transparent;color:inherit;font:inherit;font-size:13px;outline:none;min-width:0"
29
+ />
30
+ <div #richSelectDropdown role="listbox"
31
+ style="position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2)">
32
+ @for (opt of filteredOptions(); track opt; let i = $index) {
33
+ <div role="option"
34
+ [attr.aria-selected]="i === highlightedIndex()"
35
+ (click)="commitValue(opt)"
36
+ [style]="i === highlightedIndex() ? 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424);background:var(--ogrid-bg-hover, #e8f0fe)' : 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424)'">
37
+ {{ getDisplayText(opt) }}
38
+ </div>
39
+ }
40
+ @if (filteredOptions().length === 0) {
41
+ <div style="padding:6px 8px;color:var(--ogrid-muted, #999)">No matches</div>
42
+ }
43
+ </div>
44
+ </div>
45
+ }
46
+ @case ('select') {
47
+ <div #selectWrapper tabindex="0"
48
+ style="width:100%;height:100%;display:flex;align-items:center;padding:6px 10px;box-sizing:border-box;min-width:0;position:relative"
49
+ (keydown)="onCustomSelectKeyDown($event)">
50
+ <div style="display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font-size:13px;color:inherit">
51
+ <span>{{ getDisplayText(value) }}</span>
52
+ <span style="margin-left:4px;font-size:10px;opacity:0.5">&#9662;</span>
53
+ </div>
54
+ <div #selectDropdown role="listbox"
55
+ style="position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2)">
56
+ @for (opt of selectOptions(); track opt; let i = $index) {
57
+ <div role="option"
58
+ [attr.aria-selected]="i === highlightedIndex()"
59
+ (click)="commitValue(opt)"
60
+ [style]="i === highlightedIndex() ? 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424);background:var(--ogrid-bg-hover, #e8f0fe)' : 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424)'">
61
+ {{ getDisplayText(opt) }}
62
+ </div>
63
+ }
64
+ </div>
65
+ </div>
66
+ }
67
+ @case ('checkbox') {
68
+ <div style="display:flex;align-items:center;justify-content:center;width:100%;height:100%">
69
+ <input
70
+ type="checkbox"
71
+ [checked]="!!localValue()"
72
+ (change)="commitValue($any($event.target).checked)"
73
+ (keydown)="onCheckboxKeyDown($event)"
74
+ />
75
+ </div>
76
+ }
77
+ @case ('date') {
78
+ <input
79
+ #inputEl
80
+ type="date"
81
+ [value]="localValue()"
82
+ (change)="commitValue($any($event.target).value)"
83
+ (keydown)="onTextKeyDown($event)"
84
+ (blur)="onTextBlur()"
85
+ [style]="getInputStyle()"
86
+ />
87
+ }
88
+ @default {
89
+ <input
90
+ #inputEl
91
+ type="text"
92
+ [value]="localValue()"
93
+ (input)="localValue.set($any($event.target).value)"
94
+ (keydown)="onTextKeyDown($event)"
95
+ (blur)="onTextBlur()"
96
+ [style]="getInputStyle()"
97
+ />
98
+ }
99
+ }
100
+ `;
101
+ export const INLINE_CELL_EDITOR_STYLES = `
102
+ :host {
103
+ display: block;
104
+ width: 100%;
105
+ height: 100%;
106
+ }
107
+ `;
@@ -4,8 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { Component, Input, signal, DestroyRef, inject } from '@angular/core';
8
- import { CommonModule } from '@angular/common';
7
+ import { Component, Input, signal, DestroyRef, inject, ChangeDetectionStrategy } from '@angular/core';
9
8
  import { measureRange, injectGlobalStyles } from '@alaarab/ogrid-core';
10
9
  let MarchingAntsOverlayComponent = class MarchingAntsOverlayComponent {
11
10
  constructor() {
@@ -112,7 +111,7 @@ MarchingAntsOverlayComponent = __decorate([
112
111
  Component({
113
112
  selector: 'ogrid-marching-ants-overlay',
114
113
  standalone: true,
115
- imports: [CommonModule],
114
+ changeDetection: ChangeDetectionStrategy.OnPush,
116
115
  styles: [`
117
116
  .ogrid-marching-ants-svg { position: absolute; pointer-events: none; overflow: visible; }
118
117
  .ogrid-marching-ants-svg--selection { z-index: 4; }
@@ -4,8 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { Component, Input, ViewEncapsulation } from '@angular/core';
8
- import { CommonModule } from '@angular/common';
7
+ import { Component, Input, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core';
9
8
  import { SideBarComponent } from './sidebar.component';
10
9
  import { GRID_BORDER_RADIUS } from '@alaarab/ogrid-core';
11
10
  let OGridLayoutComponent = class OGridLayoutComponent {
@@ -37,7 +36,8 @@ OGridLayoutComponent = __decorate([
37
36
  selector: 'ogrid-layout',
38
37
  standalone: true,
39
38
  encapsulation: ViewEncapsulation.None,
40
- imports: [CommonModule, SideBarComponent],
39
+ changeDetection: ChangeDetectionStrategy.OnPush,
40
+ imports: [SideBarComponent],
41
41
  styles: [`
42
42
  /* ─── OGrid Theme Variables ─── */
43
43
  :root {
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { Component, Input } from '@angular/core';
7
+ import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
8
8
  import { CommonModule } from '@angular/common';
9
9
  const PANEL_WIDTH = 240;
10
10
  const TAB_WIDTH = 36;
@@ -100,6 +100,7 @@ SideBarComponent = __decorate([
100
100
  Component({
101
101
  selector: 'ogrid-sidebar',
102
102
  standalone: true,
103
+ changeDetection: ChangeDetectionStrategy.OnPush,
103
104
  imports: [CommonModule],
104
105
  styles: [`
105
106
  .ogrid-sidebar-root { display: flex; flex-direction: row; flex-shrink: 0; }
@@ -4,8 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { Component, Input } from '@angular/core';
8
- import { CommonModule } from '@angular/common';
7
+ import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
9
8
  import { getStatusBarParts } from '@alaarab/ogrid-core';
10
9
  let StatusBarComponent = class StatusBarComponent {
11
10
  constructor() {
@@ -52,7 +51,7 @@ StatusBarComponent = __decorate([
52
51
  Component({
53
52
  selector: 'ogrid-status-bar',
54
53
  standalone: true,
55
- imports: [CommonModule],
54
+ changeDetection: ChangeDetectionStrategy.OnPush,
56
55
  template: `
57
56
  <div [class]="classNames?.statusBar ?? ''" role="status" aria-live="polite">
58
57
  @for (part of getParts(); track part.key) {
package/dist/esm/index.js CHANGED
@@ -20,4 +20,8 @@ export { BaseColumnHeaderFilterComponent } from './components/base-column-header
20
20
  export { BaseColumnChooserComponent } from './components/base-column-chooser.component';
21
21
  export { BasePaginationControlsComponent } from './components/base-pagination-controls.component';
22
22
  export { BaseInlineCellEditorComponent } from './components/base-inline-cell-editor.component';
23
+ export { INLINE_CELL_EDITOR_TEMPLATE, INLINE_CELL_EDITOR_STYLES } from './components/inline-cell-editor-template';
24
+ export { BasePopoverCellEditorComponent, POPOVER_CELL_EDITOR_TEMPLATE, POPOVER_CELL_EDITOR_OVERLAY_STYLES } from './components/base-popover-cell-editor.component';
25
+ // Shared styles
26
+ export { OGRID_THEME_VARS_CSS } from './styles/ogrid-theme-vars';
23
27
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, createDebouncedSignal, createDebouncedCallback, debounce, createLatestRef, createLatestCallback, } from './utils';
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { Injectable, signal, computed, effect, DestroyRef, inject } from '@angular/core';
7
+ import { Injectable, signal, computed, effect, DestroyRef, inject, NgZone } from '@angular/core';
8
8
  import { flattenColumns, getDataGridStatusBarConfig, parseValue, computeAggregations, getCellValue, normalizeSelectionRange, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, UndoRedoStack, findCtrlArrowTarget, computeTabNavigation, formatSelectionAsTsv, parseTsvClipboard, rangesEqual, } from '@alaarab/ogrid-core';
9
9
  /**
10
10
  * Single orchestration service for DataGridTable. Takes grid props,
@@ -15,6 +15,7 @@ import { flattenColumns, getDataGridStatusBarConfig, parseValue, computeAggregat
15
15
  let DataGridStateService = class DataGridStateService {
16
16
  constructor() {
17
17
  this.destroyRef = inject(DestroyRef);
18
+ this.ngZone = inject(NgZone);
18
19
  // --- Input signals ---
19
20
  this.props = signal(null);
20
21
  this.wrapperEl = signal(null);
@@ -39,6 +40,9 @@ let DataGridStateService = class DataGridStateService {
39
40
  this.redoLengthSig = signal(0);
40
41
  // Fill handle state
41
42
  this.fillDragStart = null;
43
+ this.fillRafId = 0;
44
+ this.fillMoveHandler = null;
45
+ this.fillUpHandler = null;
42
46
  // Row selection
43
47
  this.lastClickedRow = -1;
44
48
  // Drag selection refs
@@ -61,10 +65,12 @@ let DataGridStateService = class DataGridStateService {
61
65
  const p = this.props();
62
66
  return p ? p.cellSelection !== false : true;
63
67
  });
64
- // Undo/redo wrapped callback
68
+ // Narrow signal extractors — prevent full props() dependency in effects/computed
69
+ this.originalOnCellValueChanged = computed(() => this.props()?.onCellValueChanged);
70
+ this.initialColumnWidthsSig = computed(() => this.props()?.initialColumnWidths);
71
+ // Undo/redo wrapped callback — only recomputes when the actual callback reference changes
65
72
  this.wrappedOnCellValueChanged = computed(() => {
66
- const p = this.props();
67
- const original = p?.onCellValueChanged;
73
+ const original = this.originalOnCellValueChanged();
68
74
  if (!original)
69
75
  return undefined;
70
76
  return (event) => {
@@ -202,29 +208,33 @@ let DataGridStateService = class DataGridStateService {
202
208
  return p.items.length === 0 && !!p.emptyState && !p.isLoading;
203
209
  });
204
210
  // Setup window event listeners for cell selection drag
205
- // Using effect with cleanup return to ensure proper removal on destroy
211
+ // Run outside NgZone to avoid 60Hz change detection during drag
206
212
  effect((onCleanup) => {
207
213
  const onMove = (e) => this.onWindowMouseMove(e);
208
214
  const onUp = () => this.onWindowMouseUp();
209
- window.addEventListener('mousemove', onMove, true);
210
- window.addEventListener('mouseup', onUp, true);
215
+ this.ngZone.runOutsideAngular(() => {
216
+ window.addEventListener('mousemove', onMove, true);
217
+ window.addEventListener('mouseup', onUp, true);
218
+ });
211
219
  onCleanup(() => {
212
220
  window.removeEventListener('mousemove', onMove, true);
213
221
  window.removeEventListener('mouseup', onUp, true);
214
222
  });
215
223
  });
216
224
  // Initialize column sizing overrides from initial widths
225
+ // Only track initialColumnWidths, not all props
217
226
  effect(() => {
218
- const p = this.props();
219
- if (p?.initialColumnWidths) {
227
+ const widths = this.initialColumnWidthsSig();
228
+ if (widths) {
220
229
  const result = {};
221
- for (const [id, width] of Object.entries(p.initialColumnWidths)) {
230
+ for (const [id, width] of Object.entries(widths)) {
222
231
  result[id] = { widthPx: width };
223
232
  }
224
233
  this.columnSizingOverridesSig.set(result);
225
234
  }
226
235
  });
227
236
  // Container width measurement via ResizeObserver
237
+ // Run outside NgZone — signal.set() inside still triggers Angular reactivity
228
238
  effect(() => {
229
239
  const el = this.wrapperEl();
230
240
  if (this.resizeObserver) {
@@ -240,16 +250,22 @@ let DataGridStateService = class DataGridStateService {
240
250
  (parseFloat(cs.borderRightWidth || '0') || 0);
241
251
  this.containerWidthSig.set(Math.max(0, rect.width - borderX));
242
252
  };
243
- this.resizeObserver = new ResizeObserver(measure);
244
- this.resizeObserver.observe(el);
253
+ this.ngZone.runOutsideAngular(() => {
254
+ this.resizeObserver = new ResizeObserver(measure);
255
+ this.resizeObserver.observe(el);
256
+ });
245
257
  measure();
246
258
  });
247
- // Cleanup on destroy — null out refs to prevent accidental reuse after teardown
259
+ // Cleanup on destroy — cancel pending work and release references
248
260
  this.destroyRef.onDestroy(() => {
249
261
  if (this.rafId) {
250
262
  cancelAnimationFrame(this.rafId);
251
263
  this.rafId = 0;
252
264
  }
265
+ if (this.fillRafId) {
266
+ cancelAnimationFrame(this.fillRafId);
267
+ this.fillRafId = 0;
268
+ }
253
269
  if (this.autoScrollInterval) {
254
270
  clearInterval(this.autoScrollInterval);
255
271
  this.autoScrollInterval = null;
@@ -258,6 +274,17 @@ let DataGridStateService = class DataGridStateService {
258
274
  this.resizeObserver.disconnect();
259
275
  this.resizeObserver = null;
260
276
  }
277
+ // Remove fill-handle window listeners if active
278
+ if (this.fillMoveHandler) {
279
+ window.removeEventListener('mousemove', this.fillMoveHandler, true);
280
+ this.fillMoveHandler = null;
281
+ }
282
+ if (this.fillUpHandler) {
283
+ window.removeEventListener('mouseup', this.fillUpHandler, true);
284
+ this.fillUpHandler = null;
285
+ }
286
+ // Clear undo/redo stack to release closure references
287
+ this.undoRedoStack.clear();
261
288
  });
262
289
  // Clean up column sizing overrides for removed columns
263
290
  effect(() => {
@@ -956,6 +983,7 @@ let DataGridStateService = class DataGridStateService {
956
983
  columnSizingOverrides: this.columnSizingOverridesSig(),
957
984
  setColumnSizingOverrides: (overrides) => this.columnSizingOverridesSig.set(overrides),
958
985
  onColumnResized: p?.onColumnResized,
986
+ onAutosizeColumn: p?.onAutosizeColumn,
959
987
  };
960
988
  const rowSelection = {
961
989
  selectedRowIds: this.selectedRowIds(),
@@ -1191,7 +1219,6 @@ let DataGridStateService = class DataGridStateService {
1191
1219
  const fillStart = this.fillDragStart;
1192
1220
  let fillDragEnd = { endRow: fillStart.startRow, endCol: fillStart.startCol };
1193
1221
  let liveFillRange = null;
1194
- let fillRafId = 0;
1195
1222
  let lastFillMousePos = null;
1196
1223
  const resolveRange = (cx, cy) => {
1197
1224
  const target = document.elementFromPoint(cx, cy);
@@ -1211,10 +1238,10 @@ let DataGridStateService = class DataGridStateService {
1211
1238
  };
1212
1239
  const onMove = (e) => {
1213
1240
  lastFillMousePos = { cx: e.clientX, cy: e.clientY };
1214
- if (fillRafId)
1215
- cancelAnimationFrame(fillRafId);
1216
- fillRafId = requestAnimationFrame(() => {
1217
- fillRafId = 0;
1241
+ if (this.fillRafId)
1242
+ cancelAnimationFrame(this.fillRafId);
1243
+ this.fillRafId = requestAnimationFrame(() => {
1244
+ this.fillRafId = 0;
1218
1245
  if (!lastFillMousePos)
1219
1246
  return;
1220
1247
  const newRange = resolveRange(lastFillMousePos.cx, lastFillMousePos.cy);
@@ -1233,9 +1260,11 @@ let DataGridStateService = class DataGridStateService {
1233
1260
  const onUp = () => {
1234
1261
  window.removeEventListener('mousemove', onMove, true);
1235
1262
  window.removeEventListener('mouseup', onUp, true);
1236
- if (fillRafId) {
1237
- cancelAnimationFrame(fillRafId);
1238
- fillRafId = 0;
1263
+ this.fillMoveHandler = null;
1264
+ this.fillUpHandler = null;
1265
+ if (this.fillRafId) {
1266
+ cancelAnimationFrame(this.fillRafId);
1267
+ this.fillRafId = 0;
1239
1268
  }
1240
1269
  if (lastFillMousePos) {
1241
1270
  const flushed = resolveRange(lastFillMousePos.cx, lastFillMousePos.cy);
@@ -1281,12 +1310,14 @@ let DataGridStateService = class DataGridStateService {
1281
1310
  this.endBatch();
1282
1311
  }
1283
1312
  this.fillDragStart = null;
1284
- // Remove event listeners after mouseup completes
1285
- window.removeEventListener('mousemove', onMove, true);
1286
- window.removeEventListener('mouseup', onUp, true);
1287
1313
  };
1288
- window.addEventListener('mousemove', onMove, true);
1289
- window.addEventListener('mouseup', onUp, true);
1314
+ // Track handlers for cleanup on destroy
1315
+ this.fillMoveHandler = onMove;
1316
+ this.fillUpHandler = onUp;
1317
+ this.ngZone.runOutsideAngular(() => {
1318
+ window.addEventListener('mousemove', onMove, true);
1319
+ window.addEventListener('mouseup', onUp, true);
1320
+ });
1290
1321
  }
1291
1322
  };
1292
1323
  DataGridStateService = __decorate([
@@ -37,6 +37,7 @@ let OGridService = class OGridService {
37
37
  this.columnOrder = signal(undefined);
38
38
  this.onColumnOrderChange = signal(undefined);
39
39
  this.onColumnResized = signal(undefined);
40
+ this.onAutosizeColumn = signal(undefined);
40
41
  this.onColumnPinned = signal(undefined);
41
42
  this.defaultPageSize = signal(DEFAULT_PAGE_SIZE);
42
43
  this.defaultSortBy = signal(undefined);
@@ -51,6 +52,7 @@ let OGridService = class OGridService {
51
52
  this.editable = signal(undefined);
52
53
  this.cellSelection = signal(undefined);
53
54
  this.density = signal('normal');
55
+ this.rowHeight = signal(undefined);
54
56
  this.onCellValueChanged = signal(undefined);
55
57
  this.onUndo = signal(undefined);
56
58
  this.onRedo = signal(undefined);
@@ -207,6 +209,14 @@ let OGridService = class OGridService {
207
209
  toggle: (panel) => this.sideBarActivePanel.update((p) => p === panel ? null : panel),
208
210
  close: () => this.sideBarActivePanel.set(null),
209
211
  }));
212
+ // --- Pre-computed stable callback references for dataGridProps ---
213
+ // These avoid recreating arrow functions on every dataGridProps recomputation.
214
+ this.handleSortFn = (columnKey, direction) => this.handleSort(columnKey, direction);
215
+ this.handleColumnResizedFn = (columnId, width) => this.handleColumnResized(columnId, width);
216
+ this.handleColumnPinnedFn = (columnId, pinned) => this.handleColumnPinned(columnId, pinned);
217
+ this.handleSelectionChangeFn = (event) => this.handleSelectionChange(event);
218
+ this.handleFilterChangeFn = (key, value) => this.handleFilterChange(key, value);
219
+ this.clearAllFiltersFn = () => this.setFilters({});
210
220
  // --- Data grid props computed ---
211
221
  this.dataGridProps = computed(() => ({
212
222
  items: this.displayItems(),
@@ -214,17 +224,19 @@ let OGridService = class OGridService {
214
224
  getRowId: this.getRowId(),
215
225
  sortBy: this.sort().field,
216
226
  sortDirection: this.sort().direction,
217
- onColumnSort: (columnKey, direction) => this.handleSort(columnKey, direction),
227
+ onColumnSort: this.handleSortFn,
218
228
  visibleColumns: this.visibleColumns(),
219
229
  columnOrder: this.columnOrder(),
220
230
  onColumnOrderChange: this.onColumnOrderChange(),
221
- onColumnResized: (columnId, width) => this.handleColumnResized(columnId, width),
222
- onColumnPinned: (columnId, pinned) => this.handleColumnPinned(columnId, pinned),
231
+ onColumnResized: this.handleColumnResizedFn,
232
+ onAutosizeColumn: this.onAutosizeColumn(),
233
+ onColumnPinned: this.handleColumnPinnedFn,
223
234
  pinnedColumns: this.pinnedOverrides(),
224
235
  initialColumnWidths: this.columnWidthOverrides(),
225
236
  editable: this.editable(),
226
237
  cellSelection: this.cellSelection(),
227
238
  density: this.density(),
239
+ rowHeight: this.rowHeight(),
228
240
  onCellValueChanged: this.onCellValueChanged(),
229
241
  onUndo: this.onUndo(),
230
242
  onRedo: this.onRedo(),
@@ -232,11 +244,11 @@ let OGridService = class OGridService {
232
244
  canRedo: this.canRedo(),
233
245
  rowSelection: this.rowSelection(),
234
246
  selectedRows: this.effectiveSelectedRows(),
235
- onSelectionChange: (event) => this.handleSelectionChange(event),
247
+ onSelectionChange: this.handleSelectionChangeFn,
236
248
  statusBar: this.statusBarConfig(),
237
249
  isLoading: this.isLoadingResolved(),
238
250
  filters: this.filters(),
239
- onFilterChange: (key, value) => this.handleFilterChange(key, value),
251
+ onFilterChange: this.handleFilterChangeFn,
240
252
  filterOptions: this.clientFilterOptions(),
241
253
  loadingFilterOptions: this.dataSource()?.fetchFilterOptions ? this.loadingFilterOptions() : EMPTY_LOADING_OPTIONS,
242
254
  peopleSearch: this.dataSource()?.searchPeople?.bind(this.dataSource()),
@@ -249,7 +261,7 @@ let OGridService = class OGridService {
249
261
  'aria-labelledby': this.ariaLabelledBy(),
250
262
  emptyState: {
251
263
  hasActiveFilters: this.hasActiveFilters(),
252
- onClearAll: () => this.setFilters({}),
264
+ onClearAll: this.clearAllFiltersFn,
253
265
  message: this.emptyState()?.message,
254
266
  render: this.emptyState()?.render,
255
267
  },
@@ -369,6 +381,24 @@ let OGridService = class OGridService {
369
381
  this.sideBarActivePanel.set(parsed.defaultPanel);
370
382
  }
371
383
  });
384
+ // Cleanup on destroy — reset callback signals to prevent closure retention
385
+ this.destroyRef.onDestroy(() => {
386
+ this.onPageChange.set(undefined);
387
+ this.onPageSizeChange.set(undefined);
388
+ this.onSortChange.set(undefined);
389
+ this.onFiltersChange.set(undefined);
390
+ this.onVisibleColumnsChange.set(undefined);
391
+ this.onColumnOrderChange.set(undefined);
392
+ this.onColumnResized.set(undefined);
393
+ this.onAutosizeColumn.set(undefined);
394
+ this.onColumnPinned.set(undefined);
395
+ this.onCellValueChanged.set(undefined);
396
+ this.onSelectionChange.set(undefined);
397
+ this.onFirstDataRendered.set(undefined);
398
+ this.onError.set(undefined);
399
+ this.onUndo.set(undefined);
400
+ this.onRedo.set(undefined);
401
+ });
372
402
  }
373
403
  // --- Setters ---
374
404
  setPage(p) {
@@ -469,6 +499,8 @@ let OGridService = class OGridService {
469
499
  this.onColumnOrderChange.set(props.onColumnOrderChange);
470
500
  if (props.onColumnResized)
471
501
  this.onColumnResized.set(props.onColumnResized);
502
+ if (props.onAutosizeColumn)
503
+ this.onAutosizeColumn.set(props.onAutosizeColumn);
472
504
  if (props.onColumnPinned)
473
505
  this.onColumnPinned.set(props.onColumnPinned);
474
506
  if (props.defaultPageSize !== undefined)
@@ -483,6 +515,8 @@ let OGridService = class OGridService {
483
515
  this.cellSelection.set(props.cellSelection);
484
516
  if (props.density !== undefined)
485
517
  this.density.set(props.density);
518
+ if (props.rowHeight !== undefined)
519
+ this.rowHeight.set(props.rowHeight);
486
520
  if (props.onCellValueChanged)
487
521
  this.onCellValueChanged.set(props.onCellValueChanged);
488
522
  if (props.onUndo)
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shared OGrid CSS theme variables (light + dark mode).
3
+ * Used by all Angular UI packages (Material, PrimeNG, Radix) to avoid duplication.
4
+ */
5
+ export const OGRID_THEME_VARS_CSS = `
6
+ /* ─── OGrid Theme Variables ─── */
7
+ :root {
8
+ --ogrid-bg: #ffffff;
9
+ --ogrid-fg: rgba(0, 0, 0, 0.87);
10
+ --ogrid-fg-secondary: rgba(0, 0, 0, 0.6);
11
+ --ogrid-fg-muted: rgba(0, 0, 0, 0.5);
12
+ --ogrid-border: rgba(0, 0, 0, 0.12);
13
+ --ogrid-header-bg: rgba(0, 0, 0, 0.04);
14
+ --ogrid-hover-bg: rgba(0, 0, 0, 0.04);
15
+ --ogrid-selected-row-bg: #e6f0fb;
16
+ --ogrid-active-cell-bg: rgba(0, 0, 0, 0.02);
17
+ --ogrid-range-bg: rgba(33, 115, 70, 0.12);
18
+ --ogrid-accent: #0078d4;
19
+ --ogrid-selection-color: #217346;
20
+ --ogrid-loading-overlay: rgba(255, 255, 255, 0.7);
21
+ }
22
+ @media (prefers-color-scheme: dark) {
23
+ :root:not([data-theme="light"]) {
24
+ --ogrid-bg: #1e1e1e;
25
+ --ogrid-fg: rgba(255, 255, 255, 0.87);
26
+ --ogrid-fg-secondary: rgba(255, 255, 255, 0.6);
27
+ --ogrid-fg-muted: rgba(255, 255, 255, 0.5);
28
+ --ogrid-border: rgba(255, 255, 255, 0.12);
29
+ --ogrid-header-bg: rgba(255, 255, 255, 0.06);
30
+ --ogrid-hover-bg: rgba(255, 255, 255, 0.08);
31
+ --ogrid-selected-row-bg: #1a3a5c;
32
+ --ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);
33
+ --ogrid-range-bg: rgba(46, 160, 67, 0.15);
34
+ --ogrid-accent: #4da6ff;
35
+ --ogrid-selection-color: #2ea043;
36
+ --ogrid-loading-overlay: rgba(0, 0, 0, 0.7);
37
+ }
38
+ }
39
+ [data-theme="dark"] {
40
+ --ogrid-bg: #1e1e1e;
41
+ --ogrid-fg: rgba(255, 255, 255, 0.87);
42
+ --ogrid-fg-secondary: rgba(255, 255, 255, 0.6);
43
+ --ogrid-fg-muted: rgba(255, 255, 255, 0.5);
44
+ --ogrid-border: rgba(255, 255, 255, 0.12);
45
+ --ogrid-header-bg: rgba(255, 255, 255, 0.06);
46
+ --ogrid-hover-bg: rgba(255, 255, 255, 0.08);
47
+ --ogrid-selected-row-bg: #1a3a5c;
48
+ --ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);
49
+ --ogrid-range-bg: rgba(46, 160, 67, 0.15);
50
+ --ogrid-accent: #4da6ff;
51
+ --ogrid-selection-color: #2ea043;
52
+ --ogrid-loading-overlay: rgba(0, 0, 0, 0.7);
53
+ }`;
@@ -19,6 +19,8 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
19
19
  readonly virtualScrollService: VirtualScrollService;
20
20
  protected lastMouseShift: boolean;
21
21
  readonly columnSizingVersion: import("@angular/core").WritableSignal<number>;
22
+ /** Dirty flag — set when column layout changes, cleared after measurement. */
23
+ private measureDirty;
22
24
  /** DOM-measured column widths from the last layout pass.
23
25
  * Used as a minWidth floor to prevent columns from shrinking
24
26
  * when new data loads (e.g. server-side pagination). */
@@ -33,7 +35,7 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
33
35
  protected abstract getTableContainerRef(): ElementRef<HTMLElement> | undefined;
34
36
  /** Lifecycle hook — populate element signals from @ViewChild refs */
35
37
  ngAfterViewInit(): void;
36
- /** Lifecycle hook — re-measure column widths after each view update */
38
+ /** Lifecycle hook — re-measure column widths only when layout changed */
37
39
  ngAfterViewChecked(): void;
38
40
  /** Measure actual th widths from the DOM and update the measuredColumnWidths signal.
39
41
  * Only updates the signal when values actually change, to avoid render loops. */
@@ -46,6 +48,7 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
46
48
  readonly isLoading: import("@angular/core").Signal<boolean>;
47
49
  readonly loadingMessage: import("@angular/core").Signal<string>;
48
50
  readonly layoutModeFit: import("@angular/core").Signal<boolean>;
51
+ readonly rowHeightCssVar: import("@angular/core").Signal<string | null>;
49
52
  readonly ariaLabel: import("@angular/core").Signal<string>;
50
53
  readonly ariaLabelledBy: import("@angular/core").Signal<string | undefined>;
51
54
  readonly emptyState: import("@angular/core").Signal<{
@@ -169,10 +172,46 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
169
172
  visibleColIndex(col: IColumnDef<T>): number;
170
173
  getColumnWidth(col: IColumnDef<T>): number;
171
174
  getFilterConfig(col: IColumnDef<T>): HeaderFilterConfig;
175
+ /** Memoized column menu handlers — avoids recreating objects on every CD cycle */
176
+ protected readonly columnMenuHandlersMap: import("@angular/core").Signal<Map<string, {
177
+ onPinLeft: () => void;
178
+ onPinRight: () => void;
179
+ onUnpin: () => void;
180
+ onSortAsc: () => void;
181
+ onSortDesc: () => void;
182
+ onClearSort: () => void;
183
+ onAutosizeThis: () => void;
184
+ onAutosizeAll: () => void;
185
+ onClose: () => void;
186
+ }>>;
187
+ /** Build column menu handler object for a single column */
188
+ private buildColumnMenuHandlers;
189
+ /** Get memoized handlers for a column */
190
+ getColumnMenuHandlersMemoized(columnId: string): {
191
+ onPinLeft: () => void;
192
+ onPinRight: () => void;
193
+ onUnpin: () => void;
194
+ onSortAsc: () => void;
195
+ onSortDesc: () => void;
196
+ onClearSort: () => void;
197
+ onAutosizeThis: () => void;
198
+ onAutosizeAll: () => void;
199
+ onClose: () => void;
200
+ };
172
201
  getCellDescriptor(item: T, col: IColumnDef<T>, rowIndex: number, colIdx: number): CellRenderDescriptor;
173
202
  resolveCellContent(col: IColumnDef<T>, item: T, displayValue: unknown): unknown;
174
203
  resolveCellStyleFn(col: IColumnDef<T>, item: T): Record<string, string> | undefined;
175
204
  buildPopoverEditorProps(item: T, col: IColumnDef<T>, descriptor: CellRenderDescriptor): unknown;
205
+ /** Check if a specific cell is the active cell (PrimeNG inline template helper). */
206
+ isActiveCell(rowIndex: number, colIdx: number): boolean;
207
+ /** Check if a cell is within the current selection range (PrimeNG inline template helper). */
208
+ isInSelectionRange(rowIndex: number, colIdx: number): boolean;
209
+ /** Check if a cell is the selection end cell for fill handle display. */
210
+ isSelectionEndCell(rowIndex: number, colIdx: number): boolean;
211
+ /** Get cell background color based on selection state. */
212
+ getCellBackground(rowIndex: number, colIdx: number): string | null;
213
+ /** Resolve editor type from column definition. */
214
+ getEditorType(col: IColumnDef<T>, _item: T): 'text' | 'select' | 'checkbox' | 'date' | 'richSelect';
176
215
  getSelectValues(col: IColumnDef<T>): string[];
177
216
  formatDateForInput(value: unknown): string;
178
217
  getPinnedLeftOffset(columnId: string): number | null;
@@ -0,0 +1,33 @@
1
+ import type { IColumnDef, ICellEditorProps } from '../types';
2
+ /**
3
+ * Shared popover cell editor template used by all Angular UI packages.
4
+ */
5
+ export declare const POPOVER_CELL_EDITOR_TEMPLATE = "\n <div #anchorEl\n class=\"ogrid-popover-anchor\"\n [attr.data-row-index]=\"rowIndex\"\n [attr.data-col-index]=\"globalColIndex\"\n >\n {{ displayValue }}\n </div>\n @if (showEditor()) {\n <div class=\"ogrid-popover-editor-overlay\" (click)=\"handleOverlayClick()\">\n <div class=\"ogrid-popover-editor-content\" #editorContainer></div>\n </div>\n }\n";
6
+ /**
7
+ * Shared overlay + content styles for popover cell editors.
8
+ * Subclasses provide their own .ogrid-popover-anchor styles.
9
+ */
10
+ export declare const POPOVER_CELL_EDITOR_OVERLAY_STYLES = "\n :host { display: contents; }\n .ogrid-popover-editor-overlay {\n position: fixed; inset: 0; z-index: 1000;\n background: rgba(0,0,0,0.3);\n display: flex; align-items: center; justify-content: center;\n }\n .ogrid-popover-editor-content {\n background: var(--ogrid-bg, #ffffff); border-radius: 4px; padding: 16px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n max-width: 90vw; max-height: 90vh; overflow: auto;\n color: var(--ogrid-fg, rgba(0, 0, 0, 0.87));\n }\n";
11
+ /**
12
+ * Abstract base class for Angular popover cell editors.
13
+ * Contains all shared inputs, ViewChild refs, effects, and overlay click handling.
14
+ *
15
+ * Subclasses only need a @Component decorator with selector, template, and
16
+ * framework-specific .ogrid-popover-anchor CSS styles.
17
+ */
18
+ export declare abstract class BasePopoverCellEditorComponent<T = unknown> {
19
+ item: T;
20
+ column: IColumnDef<T>;
21
+ rowIndex: number;
22
+ globalColIndex: number;
23
+ displayValue: unknown;
24
+ editorProps: ICellEditorProps<T>;
25
+ onCancel: () => void;
26
+ private anchorRef?;
27
+ private editorContainerRef?;
28
+ private readonly injector;
29
+ private readonly envInjector;
30
+ protected readonly showEditor: import("@angular/core").WritableSignal<boolean>;
31
+ constructor();
32
+ protected handleOverlayClick(): void;
33
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared inline cell editor template used by all Angular UI packages.
3
+ * The template is identical across Material, PrimeNG, and Radix implementations.
4
+ */
5
+ export declare const INLINE_CELL_EDITOR_TEMPLATE = "\n @switch (editorType) {\n @case ('text') {\n <input\n #inputEl\n type=\"text\"\n [value]=\"localValue()\"\n (input)=\"localValue.set($any($event.target).value)\"\n (keydown)=\"onTextKeyDown($event)\"\n (blur)=\"onTextBlur()\"\n [style]=\"getInputStyle()\"\n />\n }\n @case ('richSelect') {\n <div #richSelectWrapper\n style=\"width:100%;height:100%;display:flex;align-items:center;padding:6px 10px;box-sizing:border-box;min-width:0;position:relative\">\n <input\n #richSelectInput\n type=\"text\"\n [value]=\"searchText()\"\n (input)=\"onRichSelectSearch($any($event.target).value)\"\n (keydown)=\"onRichSelectKeyDown($event)\"\n placeholder=\"Search...\"\n style=\"width:100%;padding:0;border:none;background:transparent;color:inherit;font:inherit;font-size:13px;outline:none;min-width:0\"\n />\n <div #richSelectDropdown role=\"listbox\"\n style=\"position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2)\">\n @for (opt of filteredOptions(); track opt; let i = $index) {\n <div role=\"option\"\n [attr.aria-selected]=\"i === highlightedIndex()\"\n (click)=\"commitValue(opt)\"\n [style]=\"i === highlightedIndex() ? 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424);background:var(--ogrid-bg-hover, #e8f0fe)' : 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424)'\">\n {{ getDisplayText(opt) }}\n </div>\n }\n @if (filteredOptions().length === 0) {\n <div style=\"padding:6px 8px;color:var(--ogrid-muted, #999)\">No matches</div>\n }\n </div>\n </div>\n }\n @case ('select') {\n <div #selectWrapper tabindex=\"0\"\n style=\"width:100%;height:100%;display:flex;align-items:center;padding:6px 10px;box-sizing:border-box;min-width:0;position:relative\"\n (keydown)=\"onCustomSelectKeyDown($event)\">\n <div style=\"display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font-size:13px;color:inherit\">\n <span>{{ getDisplayText(value) }}</span>\n <span style=\"margin-left:4px;font-size:10px;opacity:0.5\">&#9662;</span>\n </div>\n <div #selectDropdown role=\"listbox\"\n style=\"position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2)\">\n @for (opt of selectOptions(); track opt; let i = $index) {\n <div role=\"option\"\n [attr.aria-selected]=\"i === highlightedIndex()\"\n (click)=\"commitValue(opt)\"\n [style]=\"i === highlightedIndex() ? 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424);background:var(--ogrid-bg-hover, #e8f0fe)' : 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424)'\">\n {{ getDisplayText(opt) }}\n </div>\n }\n </div>\n </div>\n }\n @case ('checkbox') {\n <div style=\"display:flex;align-items:center;justify-content:center;width:100%;height:100%\">\n <input\n type=\"checkbox\"\n [checked]=\"!!localValue()\"\n (change)=\"commitValue($any($event.target).checked)\"\n (keydown)=\"onCheckboxKeyDown($event)\"\n />\n </div>\n }\n @case ('date') {\n <input\n #inputEl\n type=\"date\"\n [value]=\"localValue()\"\n (change)=\"commitValue($any($event.target).value)\"\n (keydown)=\"onTextKeyDown($event)\"\n (blur)=\"onTextBlur()\"\n [style]=\"getInputStyle()\"\n />\n }\n @default {\n <input\n #inputEl\n type=\"text\"\n [value]=\"localValue()\"\n (input)=\"localValue.set($any($event.target).value)\"\n (keydown)=\"onTextKeyDown($event)\"\n (blur)=\"onTextBlur()\"\n [style]=\"getInputStyle()\"\n />\n }\n }\n";
6
+ export declare const INLINE_CELL_EDITOR_STYLES = "\n :host {\n display: block;\n width: 100%;\n height: 100%;\n }\n";
@@ -24,5 +24,8 @@ export { BaseColumnChooserComponent } from './components/base-column-chooser.com
24
24
  export type { IColumnChooserProps } from './components/base-column-chooser.component';
25
25
  export { BasePaginationControlsComponent } from './components/base-pagination-controls.component';
26
26
  export { BaseInlineCellEditorComponent } from './components/base-inline-cell-editor.component';
27
+ export { INLINE_CELL_EDITOR_TEMPLATE, INLINE_CELL_EDITOR_STYLES } from './components/inline-cell-editor-template';
28
+ export { BasePopoverCellEditorComponent, POPOVER_CELL_EDITOR_TEMPLATE, POPOVER_CELL_EDITOR_OVERLAY_STYLES } from './components/base-popover-cell-editor.component';
29
+ export { OGRID_THEME_VARS_CSS } from './styles/ogrid-theme-vars';
27
30
  export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, } from './utils';
28
31
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, createDebouncedSignal, createDebouncedCallback, debounce, createLatestRef, createLatestCallback, } from './utils';
@@ -21,6 +21,8 @@ export interface DataGridLayoutState<T> {
21
21
  widthPx: number;
22
22
  }>) => void;
23
23
  onColumnResized?: (columnId: string, width: number) => void;
24
+ /** Called when user requests autosize for a single column (with measured width). */
25
+ onAutosizeColumn?: (columnId: string, width: number) => void;
24
26
  }
25
27
  export interface DataGridRowSelectionState {
26
28
  selectedRowIds: Set<RowId>;
@@ -113,7 +115,7 @@ export interface DataGridViewModelState<T> {
113
115
  };
114
116
  statusBarConfig: IStatusBarProps | null;
115
117
  showEmptyInGrid: boolean;
116
- onCellError?: (error: Error) => void;
118
+ onCellError?: (error: Error, info: unknown) => void;
117
119
  }
118
120
  /** Column pinning state and column header menu. */
119
121
  export interface DataGridPinningState {
@@ -152,6 +154,7 @@ export interface DataGridStateResult<T> {
152
154
  */
153
155
  export declare class DataGridStateService<T> {
154
156
  private destroyRef;
157
+ private ngZone;
155
158
  readonly props: import("@angular/core").WritableSignal<IOGridDataGridProps<T> | null>;
156
159
  readonly wrapperEl: import("@angular/core").WritableSignal<HTMLElement | null>;
157
160
  private readonly editingCellSig;
@@ -171,6 +174,9 @@ export declare class DataGridStateService<T> {
171
174
  private readonly undoLengthSig;
172
175
  private readonly redoLengthSig;
173
176
  private fillDragStart;
177
+ private fillRafId;
178
+ private fillMoveHandler;
179
+ private fillUpHandler;
174
180
  private lastClickedRow;
175
181
  private dragStartPos;
176
182
  private dragMoved;
@@ -185,6 +191,8 @@ export declare class DataGridStateService<T> {
185
191
  private readonly headerMenuAnchorElementSig;
186
192
  private readonly propsResolved;
187
193
  readonly cellSelection: import("@angular/core").Signal<boolean>;
194
+ private readonly originalOnCellValueChanged;
195
+ private readonly initialColumnWidthsSig;
188
196
  private readonly wrappedOnCellValueChanged;
189
197
  readonly flatColumnsRaw: import("@angular/core").Signal<IColumnDef<T>[]>;
190
198
  readonly flatColumns: import("@angular/core").Signal<IColumnDef<T>[]>;
@@ -68,6 +68,7 @@ export declare class OGridService<T> {
68
68
  readonly columnOrder: import("@angular/core").WritableSignal<string[] | undefined>;
69
69
  readonly onColumnOrderChange: import("@angular/core").WritableSignal<((order: string[]) => void) | undefined>;
70
70
  readonly onColumnResized: import("@angular/core").WritableSignal<((columnId: string, width: number) => void) | undefined>;
71
+ readonly onAutosizeColumn: import("@angular/core").WritableSignal<((columnId: string, width: number) => void) | undefined>;
71
72
  readonly onColumnPinned: import("@angular/core").WritableSignal<((columnId: string, pinned: "left" | "right" | null) => void) | undefined>;
72
73
  readonly defaultPageSize: import("@angular/core").WritableSignal<number>;
73
74
  readonly defaultSortBy: import("@angular/core").WritableSignal<string | undefined>;
@@ -85,6 +86,7 @@ export declare class OGridService<T> {
85
86
  readonly editable: import("@angular/core").WritableSignal<boolean | undefined>;
86
87
  readonly cellSelection: import("@angular/core").WritableSignal<boolean | undefined>;
87
88
  readonly density: import("@angular/core").WritableSignal<"compact" | "normal" | "comfortable">;
89
+ readonly rowHeight: import("@angular/core").WritableSignal<number | undefined>;
88
90
  readonly onCellValueChanged: import("@angular/core").WritableSignal<((event: ICellValueChangedEvent<T>) => void) | undefined>;
89
91
  readonly onUndo: import("@angular/core").WritableSignal<(() => void) | undefined>;
90
92
  readonly onRedo: import("@angular/core").WritableSignal<(() => void) | undefined>;
@@ -164,6 +166,12 @@ export declare class OGridService<T> {
164
166
  filterType: "text" | "multiSelect" | "people" | "date";
165
167
  }[]>;
166
168
  readonly sideBarState: import("@angular/core").Signal<OGridSideBarState>;
169
+ private readonly handleSortFn;
170
+ private readonly handleColumnResizedFn;
171
+ private readonly handleColumnPinnedFn;
172
+ private readonly handleSelectionChangeFn;
173
+ private readonly handleFilterChangeFn;
174
+ private readonly clearAllFiltersFn;
167
175
  readonly dataGridProps: import("@angular/core").Signal<IOGridDataGridProps<T>>;
168
176
  readonly pagination: import("@angular/core").Signal<OGridPagination>;
169
177
  readonly columnChooser: import("@angular/core").Signal<OGridColumnChooser>;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Shared OGrid CSS theme variables (light + dark mode).
3
+ * Used by all Angular UI packages (Material, PrimeNG, Radix) to avoid duplication.
4
+ */
5
+ export declare const OGRID_THEME_VARS_CSS = "\n/* \u2500\u2500\u2500 OGrid Theme Variables \u2500\u2500\u2500 */\n:root {\n --ogrid-bg: #ffffff;\n --ogrid-fg: rgba(0, 0, 0, 0.87);\n --ogrid-fg-secondary: rgba(0, 0, 0, 0.6);\n --ogrid-fg-muted: rgba(0, 0, 0, 0.5);\n --ogrid-border: rgba(0, 0, 0, 0.12);\n --ogrid-header-bg: rgba(0, 0, 0, 0.04);\n --ogrid-hover-bg: rgba(0, 0, 0, 0.04);\n --ogrid-selected-row-bg: #e6f0fb;\n --ogrid-active-cell-bg: rgba(0, 0, 0, 0.02);\n --ogrid-range-bg: rgba(33, 115, 70, 0.12);\n --ogrid-accent: #0078d4;\n --ogrid-selection-color: #217346;\n --ogrid-loading-overlay: rgba(255, 255, 255, 0.7);\n}\n@media (prefers-color-scheme: dark) {\n :root:not([data-theme=\"light\"]) {\n --ogrid-bg: #1e1e1e;\n --ogrid-fg: rgba(255, 255, 255, 0.87);\n --ogrid-fg-secondary: rgba(255, 255, 255, 0.6);\n --ogrid-fg-muted: rgba(255, 255, 255, 0.5);\n --ogrid-border: rgba(255, 255, 255, 0.12);\n --ogrid-header-bg: rgba(255, 255, 255, 0.06);\n --ogrid-hover-bg: rgba(255, 255, 255, 0.08);\n --ogrid-selected-row-bg: #1a3a5c;\n --ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);\n --ogrid-range-bg: rgba(46, 160, 67, 0.15);\n --ogrid-accent: #4da6ff;\n --ogrid-selection-color: #2ea043;\n --ogrid-loading-overlay: rgba(0, 0, 0, 0.7);\n }\n}\n[data-theme=\"dark\"] {\n --ogrid-bg: #1e1e1e;\n --ogrid-fg: rgba(255, 255, 255, 0.87);\n --ogrid-fg-secondary: rgba(255, 255, 255, 0.6);\n --ogrid-fg-muted: rgba(255, 255, 255, 0.5);\n --ogrid-border: rgba(255, 255, 255, 0.12);\n --ogrid-header-bg: rgba(255, 255, 255, 0.06);\n --ogrid-hover-bg: rgba(255, 255, 255, 0.08);\n --ogrid-selected-row-bg: #1a3a5c;\n --ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);\n --ogrid-range-bg: rgba(46, 160, 67, 0.15);\n --ogrid-accent: #4da6ff;\n --ogrid-selection-color: #2ea043;\n --ogrid-loading-overlay: rgba(0, 0, 0, 0.7);\n}";
@@ -27,6 +27,8 @@ interface IOGridBaseProps<T> {
27
27
  columnOrder?: string[];
28
28
  onColumnOrderChange?: (order: string[]) => void;
29
29
  onColumnResized?: (columnId: string, width: number) => void;
30
+ /** Called when user requests autosize for a single column (with measured width). */
31
+ onAutosizeColumn?: (columnId: string, width: number) => void;
30
32
  onColumnPinned?: (columnId: string, pinned: 'left' | 'right' | null) => void;
31
33
  editable?: boolean;
32
34
  cellSelection?: boolean;
@@ -57,10 +59,13 @@ interface IOGridBaseProps<T> {
57
59
  sideBar?: boolean | ISideBarDef;
58
60
  columnReorder?: boolean;
59
61
  virtualScroll?: IVirtualScrollConfig;
62
+ /** Fixed row height in pixels. Overrides default row height (36px). */
63
+ rowHeight?: number;
60
64
  pageSizeOptions?: number[];
61
65
  onFirstDataRendered?: () => void;
62
66
  onError?: (error: unknown) => void;
63
- onCellError?: (error: Error) => void;
67
+ onCellError?: (error: Error, info: unknown) => void;
68
+ showRowNumbers?: boolean;
64
69
  'aria-label'?: string;
65
70
  'aria-labelledby'?: string;
66
71
  }
@@ -89,6 +94,8 @@ export interface IOGridDataGridProps<T> {
89
94
  columnOrder?: string[];
90
95
  onColumnOrderChange?: (order: string[]) => void;
91
96
  onColumnResized?: (columnId: string, width: number) => void;
97
+ /** Called when user requests autosize for a single column (with measured width). */
98
+ onAutosizeColumn?: (columnId: string, width: number) => void;
92
99
  onColumnPinned?: (columnId: string, pinned: 'left' | 'right' | null) => void;
93
100
  pinnedColumns?: Record<string, 'left' | 'right'>;
94
101
  initialColumnWidths?: Record<string, number>;
@@ -119,13 +126,15 @@ export interface IOGridDataGridProps<T> {
119
126
  getUserByEmail?: (email: string) => Promise<UserLike | undefined>;
120
127
  columnReorder?: boolean;
121
128
  virtualScroll?: IVirtualScrollConfig;
129
+ /** Fixed row height in pixels. Overrides default row height (36px). */
130
+ rowHeight?: number;
122
131
  emptyState?: {
123
132
  onClearAll: () => void;
124
133
  hasActiveFilters: boolean;
125
134
  message?: string;
126
135
  render?: TemplateRef<unknown>;
127
136
  };
128
- onCellError?: (error: Error) => void;
137
+ onCellError?: (error: Error, info: unknown) => void;
129
138
  'aria-label'?: string;
130
139
  'aria-labelledby'?: string;
131
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-angular",
3
- "version": "2.0.19",
3
+ "version": "2.0.22",
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.0.19"
38
+ "@alaarab/ogrid-core": "2.0.22"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@angular/core": "^21.0.0",