@alaarab/ogrid-angular 2.3.0 → 2.4.1

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,7 +1,7 @@
1
- import { FormulaEngine, getCellValue, flattenColumns, getMultiSelectFilterFields, deriveFilterOptionsFromData, processClientSideData, validateColumns, processClientSideDataAsync, validateRowIds, computeNextSortState, mergeFilter, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, parseValue, UndoRedoStack, rangesEqual, normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, applyCellDeletion, getScrollTopForRow, computeTabNavigation, applyFillValues, computeAggregations, getDataGridStatusBarConfig, computeVisibleRange, computeTotalHeight, computeVisibleColumnRange, 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';
1
+ import { handleFormulaBarKeyDown, FormulaEngine, createGridDataAccessor, columnLetterToIndex, flattenColumns, getMultiSelectFilterFields, deriveFilterOptionsFromData, processClientSideData, deriveFormulaBarText, extractFormulaReferences, validateColumns, processClientSideDataAsync, validateRowIds, computeNextSortState, mergeFilter, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, parseValue, UndoRedoStack, rangesEqual, normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, getCellValue, applyCellDeletion, getScrollTopForRow, computeTabNavigation, applyFillValues, computeAggregations, getDataGridStatusBarConfig, computeVisibleRange, computeTotalHeight, computeVisibleColumnRange, validateVirtualScrollConfig, GRID_BORDER_RADIUS, getStatusBarParts, GRID_CONTEXT_MENU_ITEMS, formatShortcut, injectGlobalStyles, partitionColumnsForVirtualization, buildHeaderRows, ROW_NUMBER_COLUMN_ID, ROW_NUMBER_COLUMN_WIDTH, getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildPopoverEditorProps, ROW_NUMBER_COLUMN_MIN_WIDTH, measureColumnContentWidth, getPaginationViewModel, getColumnHeaderMenuItems, reorderColumnArray, findCtrlArrowTarget, measureRange, FORMULA_REF_COLORS } from '@alaarab/ogrid-core';
2
2
  export * from '@alaarab/ogrid-core';
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
- import { Injectable, Input, Component, ChangeDetectionStrategy, ViewEncapsulation, Output, ViewChild, inject, DestroyRef, signal, computed, effect, NgZone, EventEmitter, Injector, EnvironmentInjector, createComponent } from '@angular/core';
3
+ export { CELL_PADDING, CHECKBOX_COLUMN_WIDTH, DEFAULT_DEBOUNCE_MS, DEFAULT_MIN_COLUMN_WIDTH, GRID_BORDER_RADIUS, PEOPLE_SEARCH_DEBOUNCE_MS, ROW_NUMBER_COLUMN_ID, ROW_NUMBER_COLUMN_MIN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, SIDEBAR_TRANSITION_MS, Z_INDEX, debounce, getCellRenderDescriptor, getHeaderFilterConfig, isInSelectionRange, normalizeSelectionRange, resolveCellDisplayContent, resolveCellStyle, toUserLike } from '@alaarab/ogrid-core';
4
+ import { Injectable, Input, Component, ChangeDetectionStrategy, ViewEncapsulation, Output, ViewChild, input, output, viewChild, effect, inject, DestroyRef, signal, computed, NgZone, EventEmitter, Injector, EnvironmentInjector, createComponent } from '@angular/core';
5
5
  import { NgTemplateOutlet } from '@angular/common';
6
6
 
7
7
  var __defProp = Object.defineProperty;
@@ -199,21 +199,9 @@ var FormulaEngineService = class {
199
199
  return this.engine?.getAuditTrail(col, row) ?? null;
200
200
  }
201
201
  // --- Private helpers ---
202
- /**
203
- * Create a data accessor that bridges grid data to formula coordinates.
204
- */
202
+ /** Create a data accessor that bridges grid data to formula coordinates. */
205
203
  createAccessor() {
206
- const items = this.items;
207
- const cols = this.flatColumns;
208
- return {
209
- getCellValue: (col, row) => {
210
- if (row < 0 || row >= items.length) return null;
211
- if (col < 0 || col >= cols.length) return null;
212
- return getCellValue(items[row], cols[col]);
213
- },
214
- getRowCount: () => items.length,
215
- getColumnCount: () => cols.length
216
- };
204
+ return createGridDataAccessor(this.items, this.flatColumns);
217
205
  }
218
206
  };
219
207
  FormulaEngineService = __decorateClass([
@@ -291,14 +279,35 @@ var OGridService = class {
291
279
  this.formulaFunctions = signal(void 0);
292
280
  this.namedRanges = signal(void 0);
293
281
  this.sheets = signal(void 0);
282
+ this.sheetDefs = signal(void 0);
283
+ this.activeSheet = signal(void 0);
284
+ this.onSheetChange = signal(void 0);
285
+ this.onSheetAdd = signal(void 0);
294
286
  /** Active cell reference string (e.g. 'A1') updated by DataGridTable when cellReferences is enabled. */
295
287
  this.activeCellRef = signal(null);
296
- /** Stable callback passed to DataGridTable to update activeCellRef. */
288
+ /** Active cell coordinates (0-based col/row). */
289
+ this.activeCellCoords = signal(null);
290
+ /** Stable callback passed to DataGridTable to update activeCellRef + coords. */
297
291
  this.handleActiveCellChange = (ref) => {
298
292
  this.activeCellRef.set(ref);
293
+ if (ref) {
294
+ const m = ref.match(/^([A-Z]+)(\d+)$/);
295
+ if (m) {
296
+ this.activeCellCoords.set({ col: columnLetterToIndex(m[1]), row: parseInt(m[2], 10) - 1 });
297
+ } else {
298
+ this.activeCellCoords.set(null);
299
+ }
300
+ } else {
301
+ this.activeCellCoords.set(null);
302
+ }
299
303
  };
304
+ // --- Formula bar state ---
305
+ this.formulaBarEditing = signal(false);
306
+ this.formulaBarEditText = signal("");
300
307
  // --- Formula engine ---
301
308
  this.formulaService = new FormulaEngineService();
309
+ /** Monotonic counter incremented on formula recalculation — drives cache invalidation. */
310
+ this.formulaVersion = signal(0);
302
311
  // Stable formula method references for dataGridProps (avoid per-recompute arrow functions)
303
312
  this.getFormulaValueFn = (col, row) => this.formulaService.getValue(col, row);
304
313
  this.hasFormulaFn = (col, row) => this.formulaService.hasFormula(col, row);
@@ -460,6 +469,52 @@ var OGridService = class {
460
469
  toggle: (panel) => this.sideBarActivePanel.update((p) => p === panel ? null : panel),
461
470
  close: () => this.sideBarActivePanel.set(null)
462
471
  }));
472
+ // --- Formula bar derived state ---
473
+ /** Display text derived from active cell (formula string or raw value). */
474
+ this.formulaBarDisplayText = computed(() => {
475
+ const coords = this.activeCellCoords();
476
+ if (!coords) return "";
477
+ const getFormula = this.formulaService.enabled() ? (c, r) => this.formulaService.getFormula(c, r) : void 0;
478
+ const items = this.displayItems();
479
+ const cols = this.columns();
480
+ const getRawValue = (c, r) => {
481
+ if (r < 0 || r >= items.length || c < 0 || c >= cols.length) return void 0;
482
+ return getCellValue(items[r], cols[c]);
483
+ };
484
+ return deriveFormulaBarText(coords.col, coords.row, getFormula, getRawValue);
485
+ });
486
+ /** Formula text shown in the bar: edit text when editing, display text otherwise. */
487
+ this.formulaBarText = computed(
488
+ () => this.formulaBarEditing() ? this.formulaBarEditText() : this.formulaBarDisplayText()
489
+ );
490
+ /** References extracted from the current formula text (for highlighting). */
491
+ this.formulaBarReferences = computed(
492
+ () => extractFormulaReferences(this.formulaBarText())
493
+ );
494
+ // Stable formula bar callbacks (avoid new closures per computed)
495
+ this.formulaBarOnInputChangeFn = (text) => {
496
+ this.formulaBarEditText.set(text);
497
+ };
498
+ this.formulaBarOnCommitFn = () => {
499
+ this.commitFormulaBar();
500
+ };
501
+ this.formulaBarOnCancelFn = () => {
502
+ this.cancelFormulaBar();
503
+ };
504
+ this.formulaBarStartEditingFn = () => {
505
+ this.startFormulaBarEditing();
506
+ };
507
+ /** Aggregate formula bar state for template consumption. */
508
+ this.formulaBarState = computed(() => ({
509
+ cellRef: this.activeCellRef(),
510
+ formulaText: this.formulaBarText(),
511
+ isEditing: this.formulaBarEditing(),
512
+ onInputChange: this.formulaBarOnInputChangeFn,
513
+ onCommit: this.formulaBarOnCommitFn,
514
+ onCancel: this.formulaBarOnCancelFn,
515
+ startEditing: this.formulaBarStartEditingFn,
516
+ referencedCells: this.formulaBarReferences()
517
+ }));
463
518
  // --- Pre-computed stable callback references for dataGridProps ---
464
519
  // These avoid recreating arrow functions on every dataGridProps recomputation.
465
520
  this.handleSortFn = (columnKey, direction) => this.handleSort(columnKey, direction);
@@ -499,10 +554,10 @@ var OGridService = class {
499
554
  rowSelection: this.rowSelection(),
500
555
  selectedRows: this.effectiveSelectedRows(),
501
556
  onSelectionChange: this.handleSelectionChangeFn,
502
- showRowNumbers: this.showRowNumbers() || this.cellReferences(),
503
- showColumnLetters: !!this.cellReferences(),
504
- showNameBox: !!this.cellReferences(),
505
- onActiveCellChange: this.cellReferences() ? this.handleActiveCellChange : void 0,
557
+ showRowNumbers: this.showRowNumbers() || this.cellReferences() || this.formulasEnabled(),
558
+ showColumnLetters: !!(this.cellReferences() || this.formulasEnabled()),
559
+ showNameBox: !!(this.cellReferences() && !this.formulasEnabled()),
560
+ onActiveCellChange: this.cellReferences() || this.formulasEnabled() ? this.handleActiveCellChange : void 0,
506
561
  currentPage: this.page(),
507
562
  pageSize: this.pageSize(),
508
563
  statusBar: this.statusBarConfig(),
@@ -527,6 +582,8 @@ var OGridService = class {
527
582
  render: this.emptyState()?.render
528
583
  },
529
584
  formulas: this.formulasEnabled(),
585
+ formulaVersion: this.formulaVersion(),
586
+ formulaReferences: this.formulaBarReferences().length > 0 ? this.formulaBarReferences() : void 0,
530
587
  ...this.formulaService.enabled() ? {
531
588
  getFormulaValue: this.getFormulaValueFn,
532
589
  hasFormula: this.hasFormulaFn,
@@ -694,11 +751,19 @@ var OGridService = class {
694
751
  }
695
752
  });
696
753
  effect(() => {
754
+ this.activeCellCoords();
755
+ this.formulaBarEditing.set(false);
756
+ });
757
+ effect(() => {
758
+ const userRecalcCb = this.onFormulaRecalc();
697
759
  this.formulaService.configure({
698
760
  formulas: this.formulasEnabled(),
699
761
  initialFormulas: this.initialFormulas(),
700
762
  formulaFunctions: this.formulaFunctions(),
701
- onFormulaRecalc: this.onFormulaRecalc(),
763
+ onFormulaRecalc: (result) => {
764
+ this.formulaVersion.update((v) => v + 1);
765
+ userRecalcCb?.(result);
766
+ },
702
767
  namedRanges: this.namedRanges(),
703
768
  sheets: this.sheets()
704
769
  });
@@ -786,6 +851,33 @@ var OGridService = class {
786
851
  });
787
852
  this.onColumnPinned()?.(columnId, pinned);
788
853
  }
854
+ // --- Formula bar methods ---
855
+ startFormulaBarEditing() {
856
+ this.formulaBarEditText.set(this.formulaBarDisplayText());
857
+ this.formulaBarEditing.set(true);
858
+ }
859
+ commitFormulaBar() {
860
+ const coords = this.activeCellCoords();
861
+ if (!coords) return;
862
+ const text = this.formulaBarEditText().trim();
863
+ if (text.startsWith("=")) {
864
+ this.formulaService.setFormula(coords.col, coords.row, text);
865
+ this.formulaVersion.update((v) => v + 1);
866
+ } else {
867
+ this.formulaService.setFormula(coords.col, coords.row, null);
868
+ this.onCellValueChanged()?.({
869
+ rowIndex: coords.row,
870
+ columnId: this.columns()[coords.col]?.columnId ?? "",
871
+ oldValue: void 0,
872
+ newValue: text
873
+ });
874
+ }
875
+ this.formulaBarEditing.set(false);
876
+ }
877
+ cancelFormulaBar() {
878
+ this.formulaBarEditing.set(false);
879
+ this.formulaBarEditText.set("");
880
+ }
789
881
  // --- Configure from props ---
790
882
  configure(props) {
791
883
  this.columnsProp.set(props.columns);
@@ -847,6 +939,10 @@ var OGridService = class {
847
939
  if (props.formulaFunctions !== void 0) this.formulaFunctions.set(props.formulaFunctions);
848
940
  if (props.namedRanges !== void 0) this.namedRanges.set(props.namedRanges);
849
941
  if (props.sheets !== void 0) this.sheets.set(props.sheets);
942
+ if (props.sheetDefs !== void 0) this.sheetDefs.set(props.sheetDefs);
943
+ if (props.activeSheet !== void 0) this.activeSheet.set(props.activeSheet);
944
+ if (props.onSheetChange) this.onSheetChange.set(props.onSheetChange);
945
+ if (props.onSheetAdd) this.onSheetAdd.set(props.onSheetAdd);
850
946
  if (props.entityLabelPlural !== void 0) this.entityLabelPlural.set(props.entityLabelPlural);
851
947
  if (props.className !== void 0) this.className.set(props.className);
852
948
  if (props.layoutMode !== void 0) this.layoutMode.set(props.layoutMode);
@@ -1443,6 +1539,7 @@ var DataGridInteractionHelper = class {
1443
1539
  }
1444
1540
  break;
1445
1541
  case "ArrowDown": {
1542
+ if (editingCell != null) break;
1446
1543
  e.preventDefault();
1447
1544
  const newRow = ctrl ? findCtrlTarget(rowIndex, maxRowIndex, 1, (r) => isEmptyAt(r, Math.max(0, dataColIndex))) : Math.min(rowIndex + 1, maxRowIndex);
1448
1545
  if (shift) {
@@ -1459,6 +1556,7 @@ var DataGridInteractionHelper = class {
1459
1556
  break;
1460
1557
  }
1461
1558
  case "ArrowUp": {
1559
+ if (editingCell != null) break;
1462
1560
  e.preventDefault();
1463
1561
  const newRowUp = ctrl ? findCtrlTarget(rowIndex, 0, -1, (r) => isEmptyAt(r, Math.max(0, dataColIndex))) : Math.max(rowIndex - 1, 0);
1464
1562
  if (shift) {
@@ -1475,6 +1573,7 @@ var DataGridInteractionHelper = class {
1475
1573
  break;
1476
1574
  }
1477
1575
  case "ArrowRight": {
1576
+ if (editingCell != null) break;
1478
1577
  e.preventDefault();
1479
1578
  let newCol;
1480
1579
  if (ctrl && dataColIndex >= 0) {
@@ -1497,6 +1596,7 @@ var DataGridInteractionHelper = class {
1497
1596
  break;
1498
1597
  }
1499
1598
  case "ArrowLeft": {
1599
+ if (editingCell != null) break;
1500
1600
  e.preventDefault();
1501
1601
  let newColLeft;
1502
1602
  if (ctrl && dataColIndex >= 0) {
@@ -2277,7 +2377,10 @@ var DataGridStateService = class {
2277
2377
  getRowId: p?.getRowId ?? ((item) => item["id"]),
2278
2378
  editable: p?.editable,
2279
2379
  onCellValueChanged: this.wrappedOnCellValueChanged(),
2280
- isDragging: cellSel ? this.interactionHelper.isDraggingSig() : false
2380
+ isDragging: cellSel ? this.interactionHelper.isDraggingSig() : false,
2381
+ getFormulaValue: p?.getFormulaValue,
2382
+ hasFormula: p?.hasFormula,
2383
+ formulaVersion: p?.formulaVersion
2281
2384
  },
2282
2385
  statusBarConfig: this.statusBarConfig(),
2283
2386
  showEmptyInGrid: this.showEmptyInGrid(),
@@ -2924,6 +3027,205 @@ SideBarComponent = __decorateClass([
2924
3027
  `
2925
3028
  })
2926
3029
  ], SideBarComponent);
3030
+ var FormulaBarComponent = class {
3031
+ constructor() {
3032
+ /** Active cell reference (e.g. "A1"). */
3033
+ this.cellRef = input(null);
3034
+ /** Text displayed/edited in the formula input. */
3035
+ this.formulaText = input("");
3036
+ /** Whether the input is in editing mode. */
3037
+ this.isEditing = input(false);
3038
+ /** Called when the user changes the input text. */
3039
+ this.inputChange = output();
3040
+ /** Commit the formula bar value. */
3041
+ this.commit = output();
3042
+ /** Cancel editing. */
3043
+ this.cancel = output();
3044
+ /** Start editing the formula bar. */
3045
+ this.startEditing = output();
3046
+ this.inputEl = viewChild("formulaInput");
3047
+ effect(() => {
3048
+ if (this.isEditing()) {
3049
+ const el = this.inputEl()?.nativeElement;
3050
+ if (el) el.focus();
3051
+ }
3052
+ });
3053
+ }
3054
+ onInput(event) {
3055
+ this.inputChange.emit(event.target.value);
3056
+ }
3057
+ onKeyDown(event) {
3058
+ handleFormulaBarKeyDown(
3059
+ event.key,
3060
+ () => event.preventDefault(),
3061
+ () => this.commit.emit(),
3062
+ () => this.cancel.emit()
3063
+ );
3064
+ }
3065
+ onClick() {
3066
+ if (!this.isEditing()) {
3067
+ this.startEditing.emit();
3068
+ }
3069
+ }
3070
+ };
3071
+ FormulaBarComponent = __decorateClass([
3072
+ Component({
3073
+ selector: "ogrid-formula-bar",
3074
+ standalone: true,
3075
+ encapsulation: ViewEncapsulation.None,
3076
+ changeDetection: ChangeDetectionStrategy.OnPush,
3077
+ styles: [`
3078
+ .ogrid-formula-bar {
3079
+ display: flex;
3080
+ align-items: center;
3081
+ border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
3082
+ background: var(--ogrid-bg, #fff);
3083
+ min-height: 28px;
3084
+ font-size: 13px;
3085
+ }
3086
+ .ogrid-formula-bar__name-box {
3087
+ font-family: monospace;
3088
+ font-size: 12px;
3089
+ font-weight: 500;
3090
+ padding: 2px 8px;
3091
+ border-right: 1px solid var(--ogrid-border, #e0e0e0);
3092
+ background: var(--ogrid-bg, #fff);
3093
+ color: var(--ogrid-fg, #242424);
3094
+ min-width: 52px;
3095
+ text-align: center;
3096
+ line-height: 24px;
3097
+ user-select: none;
3098
+ white-space: nowrap;
3099
+ }
3100
+ .ogrid-formula-bar__fx {
3101
+ padding: 2px 8px;
3102
+ font-style: italic;
3103
+ font-weight: 600;
3104
+ color: var(--ogrid-muted-fg, #888);
3105
+ user-select: none;
3106
+ border-right: 1px solid var(--ogrid-border, #e0e0e0);
3107
+ line-height: 24px;
3108
+ font-size: 12px;
3109
+ }
3110
+ .ogrid-formula-bar__input {
3111
+ flex: 1;
3112
+ border: none;
3113
+ outline: none;
3114
+ padding: 2px 8px;
3115
+ font-family: monospace;
3116
+ font-size: 12px;
3117
+ line-height: 24px;
3118
+ background: transparent;
3119
+ color: var(--ogrid-fg, #242424);
3120
+ min-width: 0;
3121
+ }
3122
+ `],
3123
+ template: `
3124
+ <div class="ogrid-formula-bar" role="toolbar" aria-label="Formula bar">
3125
+ <div class="ogrid-formula-bar__name-box" aria-label="Active cell reference">
3126
+ {{ cellRef() ?? '\u2014' }}
3127
+ </div>
3128
+ <div class="ogrid-formula-bar__fx" aria-hidden="true">fx</div>
3129
+ <input
3130
+ #formulaInput
3131
+ type="text"
3132
+ class="ogrid-formula-bar__input"
3133
+ [value]="formulaText()"
3134
+ [readOnly]="!isEditing()"
3135
+ (input)="onInput($event)"
3136
+ (keydown)="onKeyDown($event)"
3137
+ (click)="onClick()"
3138
+ (dblclick)="onClick()"
3139
+ aria-label="Formula input"
3140
+ [attr.spellcheck]="false"
3141
+ autocomplete="off"
3142
+ />
3143
+ </div>
3144
+ `
3145
+ })
3146
+ ], FormulaBarComponent);
3147
+ var SheetTabsComponent = class {
3148
+ constructor() {
3149
+ this.sheets = input.required();
3150
+ this.activeSheet = input.required();
3151
+ this.showAddButton = input(false);
3152
+ this.sheetChange = output();
3153
+ this.sheetAdd = output();
3154
+ }
3155
+ };
3156
+ SheetTabsComponent = __decorateClass([
3157
+ Component({
3158
+ selector: "ogrid-sheet-tabs",
3159
+ standalone: true,
3160
+ encapsulation: ViewEncapsulation.None,
3161
+ changeDetection: ChangeDetectionStrategy.OnPush,
3162
+ styles: [`
3163
+ .ogrid-sheet-tabs {
3164
+ display: flex;
3165
+ align-items: center;
3166
+ border-top: 1px solid var(--ogrid-border, #e0e0e0);
3167
+ background: var(--ogrid-header-bg, #f5f5f5);
3168
+ min-height: 30px;
3169
+ overflow-x: auto;
3170
+ overflow-y: hidden;
3171
+ gap: 0;
3172
+ font-size: 12px;
3173
+ }
3174
+ .ogrid-sheet-tabs__add-btn {
3175
+ background: none;
3176
+ border: none;
3177
+ cursor: pointer;
3178
+ padding: 4px 10px;
3179
+ font-size: 16px;
3180
+ line-height: 22px;
3181
+ color: var(--ogrid-fg-secondary, #666);
3182
+ flex-shrink: 0;
3183
+ }
3184
+ .ogrid-sheet-tabs__tab {
3185
+ background: none;
3186
+ border: none;
3187
+ border-bottom: 2px solid transparent;
3188
+ cursor: pointer;
3189
+ padding: 4px 16px;
3190
+ font-size: 12px;
3191
+ line-height: 22px;
3192
+ color: var(--ogrid-fg, #242424);
3193
+ white-space: nowrap;
3194
+ position: relative;
3195
+ }
3196
+ .ogrid-sheet-tabs__tab--active {
3197
+ font-weight: 600;
3198
+ border-bottom-color: var(--ogrid-primary, #217346);
3199
+ background: var(--ogrid-bg, #fff);
3200
+ }
3201
+ `],
3202
+ template: `
3203
+ <div class="ogrid-sheet-tabs" role="tablist" aria-label="Sheet tabs">
3204
+ @if (showAddButton()) {
3205
+ <button
3206
+ type="button"
3207
+ class="ogrid-sheet-tabs__add-btn"
3208
+ (click)="sheetAdd.emit()"
3209
+ title="Add sheet"
3210
+ aria-label="Add sheet"
3211
+ >+</button>
3212
+ }
3213
+ @for (sheet of sheets(); track sheet.id) {
3214
+ @let isActive = sheet.id === activeSheet();
3215
+ <button
3216
+ type="button"
3217
+ role="tab"
3218
+ class="ogrid-sheet-tabs__tab"
3219
+ [class.ogrid-sheet-tabs__tab--active]="isActive"
3220
+ [attr.aria-selected]="isActive"
3221
+ [style.border-bottom-color]="isActive && sheet.color ? sheet.color : null"
3222
+ (click)="sheetChange.emit(sheet.id)"
3223
+ >{{ sheet.name }}</button>
3224
+ }
3225
+ </div>
3226
+ `
3227
+ })
3228
+ ], SheetTabsComponent);
2927
3229
 
2928
3230
  // src/styles/ogrid-theme-vars.ts
2929
3231
  var OGRID_THEME_VARS_CSS = `
@@ -3042,6 +3344,7 @@ var OGridLayoutComponent = class {
3042
3344
  this.fullScreen = false;
3043
3345
  this.showNameBox = false;
3044
3346
  this.activeCellRef = null;
3347
+ this.formulaBar = null;
3045
3348
  this.isFullScreen = false;
3046
3349
  this.borderRadius = GRID_BORDER_RADIUS;
3047
3350
  this.escListener = null;
@@ -3095,13 +3398,28 @@ __decorateClass([
3095
3398
  __decorateClass([
3096
3399
  Input()
3097
3400
  ], OGridLayoutComponent.prototype, "activeCellRef", 2);
3401
+ __decorateClass([
3402
+ Input()
3403
+ ], OGridLayoutComponent.prototype, "formulaBar", 2);
3404
+ __decorateClass([
3405
+ Input()
3406
+ ], OGridLayoutComponent.prototype, "sheetDefs", 2);
3407
+ __decorateClass([
3408
+ Input()
3409
+ ], OGridLayoutComponent.prototype, "activeSheet", 2);
3410
+ __decorateClass([
3411
+ Input()
3412
+ ], OGridLayoutComponent.prototype, "onSheetChange", 2);
3413
+ __decorateClass([
3414
+ Input()
3415
+ ], OGridLayoutComponent.prototype, "onSheetAdd", 2);
3098
3416
  OGridLayoutComponent = __decorateClass([
3099
3417
  Component({
3100
3418
  selector: "ogrid-layout",
3101
3419
  standalone: true,
3102
3420
  encapsulation: ViewEncapsulation.None,
3103
3421
  changeDetection: ChangeDetectionStrategy.OnPush,
3104
- imports: [SideBarComponent],
3422
+ imports: [SideBarComponent, FormulaBarComponent, SheetTabsComponent],
3105
3423
  styles: [OGRID_THEME_VARS_CSS, `
3106
3424
  :host { display: block; height: 100%; }
3107
3425
  .ogrid-layout-root { display: flex; flex-direction: column; height: 100%; }
@@ -3203,6 +3521,19 @@ OGridLayoutComponent = __decorateClass([
3203
3521
  </div>
3204
3522
  }
3205
3523
 
3524
+ <!-- Formula bar (between toolbar and grid) -->
3525
+ @if (formulaBar) {
3526
+ <ogrid-formula-bar
3527
+ [cellRef]="formulaBar.cellRef"
3528
+ [formulaText]="formulaBar.formulaText"
3529
+ [isEditing]="formulaBar.isEditing"
3530
+ (inputChange)="formulaBar.onInputChange($event)"
3531
+ (commit)="formulaBar.onCommit()"
3532
+ (cancel)="formulaBar.onCancel()"
3533
+ (startEditing)="formulaBar.startEditing()"
3534
+ />
3535
+ }
3536
+
3206
3537
  <!-- Grid area -->
3207
3538
  <div class="ogrid-layout-grid-area">
3208
3539
  @if (sideBar && sideBar.position === 'left') {
@@ -3216,6 +3547,17 @@ OGridLayoutComponent = __decorateClass([
3216
3547
  }
3217
3548
  </div>
3218
3549
 
3550
+ <!-- Sheet tabs (between grid and footer) -->
3551
+ @if (sheetDefs && sheetDefs.length > 0 && activeSheet) {
3552
+ <ogrid-sheet-tabs
3553
+ [sheets]="sheetDefs"
3554
+ [activeSheet]="activeSheet"
3555
+ [showAddButton]="!!onSheetAdd"
3556
+ (sheetChange)="onSheetChange?.($event)"
3557
+ (sheetAdd)="onSheetAdd?.()"
3558
+ />
3559
+ }
3560
+
3219
3561
  <!-- Footer strip (pagination) -->
3220
3562
  @if (hasPagination) {
3221
3563
  <div class="ogrid-layout-footer">
@@ -3579,6 +3921,94 @@ MarchingAntsOverlayComponent = __decorateClass([
3579
3921
  `
3580
3922
  })
3581
3923
  ], MarchingAntsOverlayComponent);
3924
+ function measureRef(container, ref, colOffset) {
3925
+ const startCol = ref.col + colOffset;
3926
+ const endCol = (ref.endCol ?? ref.col) + colOffset;
3927
+ const endRow = ref.endRow ?? ref.row;
3928
+ const tl = container.querySelector(
3929
+ `[data-row-index="${ref.row}"][data-col-index="${startCol}"]`
3930
+ );
3931
+ const br = container.querySelector(
3932
+ `[data-row-index="${endRow}"][data-col-index="${endCol}"]`
3933
+ );
3934
+ if (!tl || !br) return null;
3935
+ const cRect = container.getBoundingClientRect();
3936
+ const tlRect = tl.getBoundingClientRect();
3937
+ const brRect = br.getBoundingClientRect();
3938
+ return {
3939
+ top: Math.round(tlRect.top - cRect.top),
3940
+ left: Math.round(tlRect.left - cRect.left),
3941
+ width: Math.round(brRect.right - tlRect.left),
3942
+ height: Math.round(brRect.bottom - tlRect.top),
3943
+ color: FORMULA_REF_COLORS[ref.colorIndex % FORMULA_REF_COLORS.length]
3944
+ };
3945
+ }
3946
+ var FormulaRefOverlayComponent = class {
3947
+ constructor() {
3948
+ /** The positioned container that wraps the table. */
3949
+ this.containerEl = input(null);
3950
+ /** References to highlight. */
3951
+ this.references = input([]);
3952
+ /** Column offset (1 when checkbox/row-number columns are present). */
3953
+ this.colOffset = input(0);
3954
+ this.rects = signal([]);
3955
+ this.rafId = 0;
3956
+ effect(() => {
3957
+ const refs = this.references();
3958
+ const container = this.containerEl();
3959
+ const colOff = this.colOffset();
3960
+ cancelAnimationFrame(this.rafId);
3961
+ if (!container || refs.length === 0) {
3962
+ this.rects.set([]);
3963
+ return;
3964
+ }
3965
+ this.rafId = requestAnimationFrame(() => {
3966
+ const measured = [];
3967
+ for (const ref of refs) {
3968
+ const r = measureRef(container, ref, colOff);
3969
+ if (r) measured.push(r);
3970
+ }
3971
+ this.rects.set(measured);
3972
+ });
3973
+ });
3974
+ }
3975
+ max0(v) {
3976
+ return Math.max(0, v);
3977
+ }
3978
+ };
3979
+ FormulaRefOverlayComponent = __decorateClass([
3980
+ Component({
3981
+ selector: "ogrid-formula-ref-overlay",
3982
+ standalone: true,
3983
+ encapsulation: ViewEncapsulation.None,
3984
+ changeDetection: ChangeDetectionStrategy.OnPush,
3985
+ template: `
3986
+ @for (r of rects(); track $index) {
3987
+ <svg
3988
+ [style.position]="'absolute'"
3989
+ [style.top.px]="r.top"
3990
+ [style.left.px]="r.left"
3991
+ [style.width.px]="r.width"
3992
+ [style.height.px]="r.height"
3993
+ [style.pointerEvents]="'none'"
3994
+ [style.zIndex]="3"
3995
+ [style.overflow]="'visible'"
3996
+ aria-hidden="true"
3997
+ >
3998
+ <rect
3999
+ x="1" y="1"
4000
+ [attr.width]="max0(r.width - 2)"
4001
+ [attr.height]="max0(r.height - 2)"
4002
+ fill="none"
4003
+ [attr.stroke]="r.color"
4004
+ stroke-width="2"
4005
+ style="shape-rendering: crispEdges"
4006
+ />
4007
+ </svg>
4008
+ }
4009
+ `
4010
+ })
4011
+ ], FormulaRefOverlayComponent);
3582
4012
  var EmptyStateComponent = class {
3583
4013
  constructor() {
3584
4014
  this.message = void 0;
@@ -3881,7 +4311,7 @@ var BaseDataGridTableComponent = class {
3881
4311
  const rightOffsets = {};
3882
4312
  let leftAcc = 0;
3883
4313
  if (this.hasCheckboxCol()) leftAcc += CHECKBOX_COLUMN_WIDTH;
3884
- if (this.hasRowNumbersCol()) leftAcc += ROW_NUMBER_COLUMN_WIDTH;
4314
+ if (this.hasRowNumbersCol()) leftAcc += this.getRowNumberWidth();
3885
4315
  let rightAcc = 0;
3886
4316
  const len = layouts.length;
3887
4317
  for (let i = 0; i < len; i++) {
@@ -3954,6 +4384,12 @@ var BaseDataGridTableComponent = class {
3954
4384
  getGlobalColIndex(col) {
3955
4385
  return this.globalColIndexMap().get(col.columnId) ?? 0;
3956
4386
  }
4387
+ /** Returns the effective row number column width (from overrides or default). */
4388
+ getRowNumberWidth() {
4389
+ const overrides = this.columnSizingOverrides();
4390
+ const override = overrides[ROW_NUMBER_COLUMN_ID];
4391
+ return override ? override.widthPx : ROW_NUMBER_COLUMN_WIDTH;
4392
+ }
3957
4393
  /**
3958
4394
  * Initialize base wiring effects. Must be called from subclass constructor.
3959
4395
  *
@@ -4207,8 +4643,9 @@ var BaseDataGridTableComponent = class {
4207
4643
  this.state().interaction.setSelectionRange?.(null);
4208
4644
  this.getWrapperRef()?.nativeElement.focus({ preventScroll: true });
4209
4645
  const startX = event.clientX;
4210
- const startWidth = this.getColumnWidth(col);
4211
- const minWidth = col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
4646
+ const columnId = col.columnId;
4647
+ const startWidth = columnId === ROW_NUMBER_COLUMN_ID ? this.getRowNumberWidth() : this.getColumnWidth(col);
4648
+ const minWidth = columnId === ROW_NUMBER_COLUMN_ID ? ROW_NUMBER_COLUMN_MIN_WIDTH : col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
4212
4649
  const onMove = (e) => {
4213
4650
  const delta = e.clientX - startX;
4214
4651
  const newWidth = Math.max(minWidth, startWidth + delta);
@@ -4803,7 +5240,11 @@ var BaseInlineCellEditorComponent = class {
4803
5240
  }
4804
5241
  syncFromInputs() {
4805
5242
  const v = this.value;
4806
- this.localValue.set(v != null ? String(v) : "");
5243
+ let strVal = v != null ? String(v) : "";
5244
+ if (this.editorType === "date" && strVal.match(/^\d{4}-\d{2}-\d{2}/)) {
5245
+ strVal = strVal.substring(0, 10);
5246
+ }
5247
+ this.localValue.set(strVal);
4807
5248
  const col = this.column;
4808
5249
  if (col?.cellEditorParams?.values) {
4809
5250
  const vals = col.cellEditorParams.values;
@@ -5014,6 +5455,88 @@ __decorateClass([
5014
5455
  __decorateClass([
5015
5456
  ViewChild("richSelectDropdown")
5016
5457
  ], BaseInlineCellEditorComponent.prototype, "richSelectDropdown", 2);
5458
+ var BaseColumnHeaderMenuComponent = class {
5459
+ constructor() {
5460
+ // Signal-backed inputs so computed() tracks changes reactively
5461
+ this._canPinLeft = signal(true);
5462
+ this._canPinRight = signal(true);
5463
+ this._canUnpin = signal(false);
5464
+ this._currentSort = signal(null);
5465
+ this._isSortable = signal(true);
5466
+ this._isResizable = signal(true);
5467
+ this.handlers = {};
5468
+ this.menuItems = computed(
5469
+ () => getColumnHeaderMenuItems({
5470
+ canPinLeft: this._canPinLeft(),
5471
+ canPinRight: this._canPinRight(),
5472
+ canUnpin: this._canUnpin(),
5473
+ currentSort: this._currentSort(),
5474
+ isSortable: this._isSortable(),
5475
+ isResizable: this._isResizable()
5476
+ })
5477
+ );
5478
+ }
5479
+ set canPinLeft(v) {
5480
+ this._canPinLeft.set(v);
5481
+ }
5482
+ set canPinRight(v) {
5483
+ this._canPinRight.set(v);
5484
+ }
5485
+ set canUnpin(v) {
5486
+ this._canUnpin.set(v);
5487
+ }
5488
+ set currentSort(v) {
5489
+ this._currentSort.set(v);
5490
+ }
5491
+ set isSortable(v) {
5492
+ this._isSortable.set(v);
5493
+ }
5494
+ set isResizable(v) {
5495
+ this._isResizable.set(v);
5496
+ }
5497
+ handleMenuItemClick(itemId) {
5498
+ const h = this.handlers;
5499
+ const actionMap = {
5500
+ pinLeft: h.onPinLeft,
5501
+ pinRight: h.onPinRight,
5502
+ unpin: h.onUnpin,
5503
+ sortAsc: h.onSortAsc,
5504
+ sortDesc: h.onSortDesc,
5505
+ clearSort: h.onClearSort,
5506
+ autosizeThis: h.onAutosizeThis,
5507
+ autosizeAll: h.onAutosizeAll
5508
+ };
5509
+ const action = actionMap[itemId];
5510
+ if (action) {
5511
+ action();
5512
+ h.onClose?.();
5513
+ }
5514
+ }
5515
+ };
5516
+ __decorateClass([
5517
+ Input({ required: true })
5518
+ ], BaseColumnHeaderMenuComponent.prototype, "columnId", 2);
5519
+ __decorateClass([
5520
+ Input()
5521
+ ], BaseColumnHeaderMenuComponent.prototype, "canPinLeft", 1);
5522
+ __decorateClass([
5523
+ Input()
5524
+ ], BaseColumnHeaderMenuComponent.prototype, "canPinRight", 1);
5525
+ __decorateClass([
5526
+ Input()
5527
+ ], BaseColumnHeaderMenuComponent.prototype, "canUnpin", 1);
5528
+ __decorateClass([
5529
+ Input()
5530
+ ], BaseColumnHeaderMenuComponent.prototype, "currentSort", 1);
5531
+ __decorateClass([
5532
+ Input()
5533
+ ], BaseColumnHeaderMenuComponent.prototype, "isSortable", 1);
5534
+ __decorateClass([
5535
+ Input()
5536
+ ], BaseColumnHeaderMenuComponent.prototype, "isResizable", 1);
5537
+ __decorateClass([
5538
+ Input()
5539
+ ], BaseColumnHeaderMenuComponent.prototype, "handlers", 2);
5017
5540
 
5018
5541
  // src/components/inline-cell-editor-template.ts
5019
5542
  var INLINE_CELL_EDITOR_TEMPLATE = `
@@ -5206,4 +5729,4 @@ __decorateClass([
5206
5729
  ViewChild("editorContainer")
5207
5730
  ], BasePopoverCellEditorComponent.prototype, "editorContainerRef", 2);
5208
5731
 
5209
- export { BaseColumnChooserComponent, BaseColumnHeaderFilterComponent, BaseDataGridTableComponent, BaseInlineCellEditorComponent, BaseOGridComponent, BasePaginationControlsComponent, BasePopoverCellEditorComponent, ColumnReorderService, DataGridEditingHelper, DataGridInteractionHelper, DataGridLayoutHelper, DataGridStateService, EmptyStateComponent, FormulaEngineService, GridContextMenuComponent, INLINE_CELL_EDITOR_STYLES, INLINE_CELL_EDITOR_TEMPLATE, MarchingAntsOverlayComponent, OGRID_THEME_VARS_CSS, OGridLayoutComponent, OGridService, POPOVER_CELL_EDITOR_OVERLAY_STYLES, POPOVER_CELL_EDITOR_TEMPLATE, SideBarComponent, StatusBarComponent, VirtualScrollService, createDebouncedCallback, createDebouncedSignal, createLatestCallback };
5732
+ export { BaseColumnChooserComponent, BaseColumnHeaderFilterComponent, BaseColumnHeaderMenuComponent, BaseDataGridTableComponent, BaseInlineCellEditorComponent, BaseOGridComponent, BasePaginationControlsComponent, BasePopoverCellEditorComponent, ColumnReorderService, DataGridEditingHelper, DataGridInteractionHelper, DataGridLayoutHelper, DataGridStateService, EmptyStateComponent, FormulaBarComponent, FormulaEngineService, FormulaRefOverlayComponent, GridContextMenuComponent, INLINE_CELL_EDITOR_STYLES, INLINE_CELL_EDITOR_TEMPLATE, MarchingAntsOverlayComponent, OGRID_THEME_VARS_CSS, OGridLayoutComponent, OGridService, POPOVER_CELL_EDITOR_OVERLAY_STYLES, POPOVER_CELL_EDITOR_TEMPLATE, SheetTabsComponent, SideBarComponent, StatusBarComponent, VirtualScrollService, createDebouncedCallback, createDebouncedSignal, createLatestCallback };
@@ -0,0 +1,30 @@
1
+ import { type IColumnHeaderMenuItem, type ColumnHeaderMenuHandlers } from '@alaarab/ogrid-core';
2
+ /**
3
+ * Abstract base class containing all shared TypeScript logic for ColumnHeaderMenu components.
4
+ * Framework-specific UI packages extend this with their templates and style overrides.
5
+ *
6
+ * Uses signal-backed @Input setters so that computed() tracks input changes reactively
7
+ * (plain @Input properties are not reactive in Angular signals).
8
+ *
9
+ * Subclasses must:
10
+ * 1. Provide a @Component decorator with template and styles
11
+ * 2. Implement their own menu open/close mechanism (mat-menu, p-menu, or native dropdown)
12
+ */
13
+ export declare abstract class BaseColumnHeaderMenuComponent {
14
+ columnId: string;
15
+ private readonly _canPinLeft;
16
+ private readonly _canPinRight;
17
+ private readonly _canUnpin;
18
+ private readonly _currentSort;
19
+ private readonly _isSortable;
20
+ private readonly _isResizable;
21
+ set canPinLeft(v: boolean);
22
+ set canPinRight(v: boolean);
23
+ set canUnpin(v: boolean);
24
+ set currentSort(v: 'asc' | 'desc' | null);
25
+ set isSortable(v: boolean);
26
+ set isResizable(v: boolean);
27
+ handlers: Partial<ColumnHeaderMenuHandlers>;
28
+ readonly menuItems: import("@angular/core").Signal<IColumnHeaderMenuItem[]>;
29
+ handleMenuItemClick(itemId: string): void;
30
+ }
@@ -126,6 +126,9 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
126
126
  editable?: boolean;
127
127
  onCellValueChanged?: ((event: import("@alaarab/ogrid-core").ICellValueChangedEvent<T>) => void) | undefined;
128
128
  isDragging: boolean;
129
+ getFormulaValue?: (col: number, row: number) => unknown;
130
+ hasFormula?: (col: number, row: number) => boolean;
131
+ formulaVersion?: number;
129
132
  }>;
130
133
  readonly pinnedColumnsMap: import("@angular/core").Signal<Record<string, "left" | "right">>;
131
134
  readonly vsEnabled: import("@angular/core").Signal<boolean>;
@@ -174,6 +177,8 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
174
177
  minWidth: number;
175
178
  width: number;
176
179
  }[]>;
180
+ /** Returns the effective row number column width (from overrides or default). */
181
+ getRowNumberWidth(): number;
177
182
  readonly pinningOffsets: import("@angular/core").Signal<{
178
183
  leftOffsets: Record<string, number>;
179
184
  rightOffsets: Record<string, number>;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * FormulaBarComponent -- Standalone Angular formula bar component.
3
+ *
4
+ * Layout: [Name Box] [fx] [Formula Input]
5
+ *
6
+ * Uses --ogrid-* CSS variables for theming.
7
+ * Port of React's FormulaBar component.
8
+ */
9
+ export declare class FormulaBarComponent {
10
+ /** Active cell reference (e.g. "A1"). */
11
+ readonly cellRef: import("@angular/core").InputSignal<string | null>;
12
+ /** Text displayed/edited in the formula input. */
13
+ readonly formulaText: import("@angular/core").InputSignal<string>;
14
+ /** Whether the input is in editing mode. */
15
+ readonly isEditing: import("@angular/core").InputSignal<boolean>;
16
+ /** Called when the user changes the input text. */
17
+ readonly inputChange: import("@angular/core").OutputEmitterRef<string>;
18
+ /** Commit the formula bar value. */
19
+ readonly commit: import("@angular/core").OutputEmitterRef<void>;
20
+ /** Cancel editing. */
21
+ readonly cancel: import("@angular/core").OutputEmitterRef<void>;
22
+ /** Start editing the formula bar. */
23
+ readonly startEditing: import("@angular/core").OutputEmitterRef<void>;
24
+ private readonly inputEl;
25
+ constructor();
26
+ onInput(event: Event): void;
27
+ onKeyDown(event: KeyboardEvent): void;
28
+ onClick(): void;
29
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * FormulaRefOverlayComponent -- Renders colored border overlays on cells
3
+ * referenced by the active formula, like Excel's reference highlighting.
4
+ *
5
+ * Port of React's FormulaRefOverlay component.
6
+ */
7
+ import { type FormulaReference } from '@alaarab/ogrid-core';
8
+ interface RefRect {
9
+ top: number;
10
+ left: number;
11
+ width: number;
12
+ height: number;
13
+ color: string;
14
+ }
15
+ export declare class FormulaRefOverlayComponent {
16
+ /** The positioned container that wraps the table. */
17
+ readonly containerEl: import("@angular/core").InputSignal<HTMLElement | null>;
18
+ /** References to highlight. */
19
+ readonly references: import("@angular/core").InputSignal<FormulaReference[]>;
20
+ /** Column offset (1 when checkbox/row-number columns are present). */
21
+ readonly colOffset: import("@angular/core").InputSignal<number>;
22
+ readonly rects: import("@angular/core").WritableSignal<RefRect[]>;
23
+ private rafId;
24
+ constructor();
25
+ max0(v: number): number;
26
+ }
27
+ export {};
@@ -1,4 +1,6 @@
1
1
  import type { SideBarProps } from './sidebar.component';
2
+ import type { OGridFormulaBarState } from '../services/ogrid.service';
3
+ import type { ISheetDef } from '@alaarab/ogrid-core';
2
4
  export declare class OGridLayoutComponent {
3
5
  className?: string;
4
6
  hasToolbar: boolean;
@@ -8,6 +10,11 @@ export declare class OGridLayoutComponent {
8
10
  fullScreen: boolean;
9
11
  showNameBox: boolean;
10
12
  activeCellRef: string | null;
13
+ formulaBar: OGridFormulaBarState | null;
14
+ sheetDefs: ISheetDef[] | undefined;
15
+ activeSheet: string | undefined;
16
+ onSheetChange: ((sheetId: string) => void) | undefined;
17
+ onSheetAdd: (() => void) | undefined;
11
18
  isFullScreen: boolean;
12
19
  readonly borderRadius = 6;
13
20
  private escListener;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * SheetTabsComponent -- Excel-style sheet tab bar at the bottom of the grid.
3
+ *
4
+ * Layout: [+] [Sheet1] [Sheet2] [Sheet3]
5
+ *
6
+ * Uses --ogrid-* CSS variables for theming.
7
+ * Port of React's SheetTabs component.
8
+ */
9
+ import type { ISheetDef } from '@alaarab/ogrid-core';
10
+ export declare class SheetTabsComponent {
11
+ readonly sheets: import("@angular/core").InputSignal<ISheetDef[]>;
12
+ readonly activeSheet: import("@angular/core").InputSignal<string>;
13
+ readonly showAddButton: import("@angular/core").InputSignal<boolean>;
14
+ readonly sheetChange: import("@angular/core").OutputEmitterRef<string>;
15
+ readonly sheetAdd: import("@angular/core").OutputEmitterRef<void>;
16
+ }
@@ -1,11 +1,11 @@
1
1
  export * from '@alaarab/ogrid-core';
2
- export { CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, PEOPLE_SEARCH_DEBOUNCE_MS, DEFAULT_DEBOUNCE_MS, SIDEBAR_TRANSITION_MS, Z_INDEX, } from '@alaarab/ogrid-core';
2
+ export { CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, ROW_NUMBER_COLUMN_ID, ROW_NUMBER_COLUMN_MIN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, PEOPLE_SEARCH_DEBOUNCE_MS, DEFAULT_DEBOUNCE_MS, SIDEBAR_TRANSITION_MS, Z_INDEX, } from '@alaarab/ogrid-core';
3
3
  export type { IColumnDef, IColumnGroupDef, IColumnDefinition, ICellEditorProps, } from './types';
4
4
  export type { IOGridProps, IOGridClientProps, IOGridServerProps, IOGridDataGridProps, } from './types';
5
5
  export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, ICellValueChangedEvent, CellEditorParams, IValueParserParams, IDateFilterValue, HeaderCell, HeaderRow, RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, } from './types';
6
6
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types';
7
7
  export { OGridService } from './services/ogrid.service';
8
- export type { ColumnChooserPlacement, OGridPagination, OGridColumnChooser, OGridFilters, OGridSideBarState, } from './services/ogrid.service';
8
+ export type { ColumnChooserPlacement, OGridPagination, OGridColumnChooser, OGridFilters, OGridSideBarState, OGridFormulaBarState, } from './services/ogrid.service';
9
9
  export { DataGridStateService } from './services/datagrid-state.service';
10
10
  export type { DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, DataGridPinningState, DataGridStateResult, } from './services/datagrid-state.service';
11
11
  export { DataGridLayoutHelper } from './services/datagrid-layout.service';
@@ -21,6 +21,9 @@ export { GridContextMenuComponent } from './components/grid-context-menu.compone
21
21
  export { SideBarComponent } from './components/sidebar.component';
22
22
  export type { SideBarProps, SideBarFilterColumn } from './components/sidebar.component';
23
23
  export { MarchingAntsOverlayComponent } from './components/marching-ants-overlay.component';
24
+ export { FormulaBarComponent } from './components/formula-bar.component';
25
+ export { SheetTabsComponent } from './components/sheet-tabs.component';
26
+ export { FormulaRefOverlayComponent } from './components/formula-ref-overlay.component';
24
27
  export { EmptyStateComponent } from './components/empty-state.component';
25
28
  export { BaseOGridComponent } from './components/base-ogrid.component';
26
29
  export { BaseDataGridTableComponent } from './components/base-datagrid-table.component';
@@ -30,6 +33,7 @@ export { BaseColumnChooserComponent } from './components/base-column-chooser.com
30
33
  export type { IColumnChooserProps } from './components/base-column-chooser.component';
31
34
  export { BasePaginationControlsComponent } from './components/base-pagination-controls.component';
32
35
  export { BaseInlineCellEditorComponent } from './components/base-inline-cell-editor.component';
36
+ export { BaseColumnHeaderMenuComponent } from './components/base-column-header-menu.component';
33
37
  export { INLINE_CELL_EDITOR_TEMPLATE, INLINE_CELL_EDITOR_STYLES } from './components/inline-cell-editor-template';
34
38
  export { BasePopoverCellEditorComponent, POPOVER_CELL_EDITOR_TEMPLATE, POPOVER_CELL_EDITOR_OVERLAY_STYLES } from './components/base-popover-cell-editor.component';
35
39
  export { OGRID_THEME_VARS_CSS } from './styles/ogrid-theme-vars';
@@ -119,6 +119,9 @@ export interface DataGridViewModelState<T> {
119
119
  editable?: boolean;
120
120
  onCellValueChanged?: (event: ICellValueChangedEvent<T>) => void;
121
121
  isDragging: boolean;
122
+ getFormulaValue?: (col: number, row: number) => unknown;
123
+ hasFormula?: (col: number, row: number) => boolean;
124
+ formulaVersion?: number;
122
125
  };
123
126
  statusBarConfig: IStatusBarProps | null;
124
127
  showEmptyInGrid: boolean;
@@ -126,8 +126,6 @@ export declare class FormulaEngineService<T = unknown> {
126
126
  * Get full audit trail for a cell.
127
127
  */
128
128
  getAuditTrail(col: number, row: number): IAuditTrail | null;
129
- /**
130
- * Create a data accessor that bridges grid data to formula coordinates.
131
- */
129
+ /** Create a data accessor that bridges grid data to formula coordinates. */
132
130
  private createAccessor;
133
131
  }
@@ -1,3 +1,4 @@
1
+ import type { FormulaReference } from '@alaarab/ogrid-core';
1
2
  import type { RowId, IOGridApi, IFilters, FilterValue, IRowSelectionChangeEvent, IStatusBarProps, IColumnDefinition, IDataSource, ISideBarDef, IVirtualScrollConfig, SideBarPanelId, IFormulaFunction, IRecalcResult, IGridDataAccessor } from '../types';
2
3
  import type { IOGridProps, IOGridDataGridProps } from '../types';
3
4
  import type { IColumnDef, IColumnGroupDef, ICellValueChangedEvent } from '../types';
@@ -26,6 +27,17 @@ export interface OGridFilters {
26
27
  hasActiveFilters: boolean;
27
28
  setFilters: (f: IFilters) => void;
28
29
  }
30
+ /** Formula bar state and handlers. */
31
+ export interface OGridFormulaBarState {
32
+ cellRef: string | null;
33
+ formulaText: string;
34
+ isEditing: boolean;
35
+ onInputChange: (text: string) => void;
36
+ onCommit: () => void;
37
+ onCancel: () => void;
38
+ startEditing: () => void;
39
+ referencedCells: FormulaReference[];
40
+ }
29
41
  /** Side bar state. */
30
42
  export interface OGridSideBarState {
31
43
  isEnabled: boolean;
@@ -121,11 +133,24 @@ export declare class OGridService<T> {
121
133
  readonly formulaFunctions: import("@angular/core").WritableSignal<Record<string, IFormulaFunction> | undefined>;
122
134
  readonly namedRanges: import("@angular/core").WritableSignal<Record<string, string> | undefined>;
123
135
  readonly sheets: import("@angular/core").WritableSignal<Record<string, IGridDataAccessor> | undefined>;
136
+ readonly sheetDefs: import("@angular/core").WritableSignal<import("@alaarab/ogrid-core").ISheetDef[] | undefined>;
137
+ readonly activeSheet: import("@angular/core").WritableSignal<string | undefined>;
138
+ readonly onSheetChange: import("@angular/core").WritableSignal<((sheetId: string) => void) | undefined>;
139
+ readonly onSheetAdd: import("@angular/core").WritableSignal<(() => void) | undefined>;
124
140
  /** Active cell reference string (e.g. 'A1') updated by DataGridTable when cellReferences is enabled. */
125
141
  readonly activeCellRef: import("@angular/core").WritableSignal<string | null>;
126
- /** Stable callback passed to DataGridTable to update activeCellRef. */
142
+ /** Active cell coordinates (0-based col/row). */
143
+ readonly activeCellCoords: import("@angular/core").WritableSignal<{
144
+ col: number;
145
+ row: number;
146
+ } | null>;
147
+ /** Stable callback passed to DataGridTable to update activeCellRef + coords. */
127
148
  private readonly handleActiveCellChange;
149
+ private readonly formulaBarEditing;
150
+ private readonly formulaBarEditText;
128
151
  private readonly formulaService;
152
+ /** Monotonic counter incremented on formula recalculation — drives cache invalidation. */
153
+ readonly formulaVersion: import("@angular/core").WritableSignal<number>;
129
154
  private readonly getFormulaValueFn;
130
155
  private readonly hasFormulaFn;
131
156
  private readonly getFormulaFn;
@@ -204,6 +229,18 @@ export declare class OGridService<T> {
204
229
  filterType: "text" | "multiSelect" | "people" | "date";
205
230
  }[]>;
206
231
  readonly sideBarState: import("@angular/core").Signal<OGridSideBarState>;
232
+ /** Display text derived from active cell (formula string or raw value). */
233
+ private readonly formulaBarDisplayText;
234
+ /** Formula text shown in the bar: edit text when editing, display text otherwise. */
235
+ readonly formulaBarText: import("@angular/core").Signal<string>;
236
+ /** References extracted from the current formula text (for highlighting). */
237
+ readonly formulaBarReferences: import("@angular/core").Signal<FormulaReference[]>;
238
+ private readonly formulaBarOnInputChangeFn;
239
+ private readonly formulaBarOnCommitFn;
240
+ private readonly formulaBarOnCancelFn;
241
+ private readonly formulaBarStartEditingFn;
242
+ /** Aggregate formula bar state for template consumption. */
243
+ readonly formulaBarState: import("@angular/core").Signal<OGridFormulaBarState>;
207
244
  private readonly handleSortFn;
208
245
  private readonly handleColumnResizedFn;
209
246
  private readonly handleColumnPinnedFn;
@@ -233,6 +270,9 @@ export declare class OGridService<T> {
233
270
  handleSelectionChange(event: IRowSelectionChangeEvent<T>): void;
234
271
  handleColumnResized(columnId: string, width: number): void;
235
272
  handleColumnPinned(columnId: string, pinned: 'left' | 'right' | null): void;
273
+ private startFormulaBarEditing;
274
+ private commitFormulaBar;
275
+ private cancelFormulaBar;
236
276
  configure(props: IOGridProps<T>): void;
237
277
  /**
238
278
  * Pin a column to the left or right edge.
@@ -1,8 +1,8 @@
1
1
  import type { TemplateRef } from '@angular/core';
2
2
  import type { IColumnDef, IColumnGroupDef, ICellValueChangedEvent } from './columnTypes';
3
- export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, IOGridApi, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail, } from '@alaarab/ogrid-core';
3
+ export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, IOGridApi, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail, ISheetDef, FormulaReference, } from '@alaarab/ogrid-core';
4
4
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from '@alaarab/ogrid-core';
5
- import type { RowId, UserLike, IFilters, FilterValue, RowSelectionMode, IRowSelectionChangeEvent, IStatusBarProps, IDataSource, ISideBarDef, IVirtualScrollConfig, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail } from '@alaarab/ogrid-core';
5
+ import type { RowId, UserLike, IFilters, FilterValue, RowSelectionMode, IRowSelectionChangeEvent, IStatusBarProps, IDataSource, ISideBarDef, IVirtualScrollConfig, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail, ISheetDef, FormulaReference } from '@alaarab/ogrid-core';
6
6
  /** Base props shared by both client-side and server-side OGrid modes. */
7
7
  interface IOGridBaseProps<T> {
8
8
  columns: (IColumnDef<T> | IColumnGroupDef<T>)[];
@@ -90,6 +90,14 @@ interface IOGridBaseProps<T> {
90
90
  namedRanges?: Record<string, string>;
91
91
  /** Sheet accessors for cross-sheet formula references (e.g. { Sheet2: accessor }). */
92
92
  sheets?: Record<string, IGridDataAccessor>;
93
+ /** Sheet definitions for the tab bar at grid bottom. */
94
+ sheetDefs?: ISheetDef[];
95
+ /** Active sheet ID (controlled). */
96
+ activeSheet?: string;
97
+ /** Called when user clicks a sheet tab. */
98
+ onSheetChange?: (sheetId: string) => void;
99
+ /** Called when user clicks the "+" add sheet button. */
100
+ onSheetAdd?: () => void;
93
101
  'aria-label'?: string;
94
102
  'aria-labelledby'?: string;
95
103
  }
@@ -186,4 +194,8 @@ export interface IOGridDataGridProps<T> {
186
194
  getDependents?: (col: number, row: number) => IAuditEntry[];
187
195
  /** Get full audit trail for a cell. */
188
196
  getAuditTrail?: (col: number, row: number) => IAuditTrail | null;
197
+ /** Monotonic counter incremented on each formula recalculation — used for cache invalidation. */
198
+ formulaVersion?: number;
199
+ /** Cell references to highlight (from active formula in formula bar). */
200
+ formulaReferences?: FormulaReference[];
189
201
  }
@@ -1,3 +1,3 @@
1
1
  export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, IValueParserParams, IDateFilterValue, HeaderCell, HeaderRow, } from './columnTypes';
2
- export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridClientProps, IOGridServerProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail, } from './dataGridTypes';
2
+ export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridClientProps, IOGridServerProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail, ISheetDef, FormulaReference, } from './dataGridTypes';
3
3
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './dataGridTypes';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-angular",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
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.3.0"
38
+ "@alaarab/ogrid-core": "2.4.1"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@angular/core": "^21.0.0",