@alaarab/ogrid-vue 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { injectGlobalStyles, Z_INDEX, getStatusBarParts, measureRange, flattenColumns, getMultiSelectFilterFields, deriveFilterOptionsFromData, processClientSideData, processClientSideDataAsync, validateColumns, validateRowIds, computeRowSelectionState, buildCellIndex, UndoRedoStack, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, computeAggregations, getDataGridStatusBarConfig, validateVirtualScrollConfig, computeVisibleRange, computeTotalHeight, computeVisibleColumnRange, partitionColumnsForVirtualization, buildHeaderRows, ROW_NUMBER_COLUMN_WIDTH, getHeaderFilterConfig, getCellRenderDescriptor, buildInlineEditorProps, buildPopoverEditorProps, resolveCellDisplayContent, resolveCellStyle, rangesEqual, normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear, measureColumnContentWidth, getPinStateForColumn, parseValue, applyFillValues, applyCellDeletion, computeTabNavigation, computeArrowNavigation, computeNextSortState, mergeFilter, applyRangeRowSelection, getScrollTopForRow, getCellValue, calculateDropTarget, reorderColumnArray, computeAutoScrollSpeed } from '@alaarab/ogrid-core';
1
+ import { injectGlobalStyles, Z_INDEX, getStatusBarParts, FORMULA_BAR_STYLES, handleFormulaBarKeyDown, measureRange, FORMULA_REF_COLORS, deriveFormulaBarText, extractFormulaReferences, flattenColumns, getMultiSelectFilterFields, deriveFilterOptionsFromData, processClientSideData, processClientSideDataAsync, validateColumns, validateRowIds, computeRowSelectionState, buildCellIndex, UndoRedoStack, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, computeAggregations, getDataGridStatusBarConfig, validateVirtualScrollConfig, computeVisibleRange, computeTotalHeight, computeVisibleColumnRange, partitionColumnsForVirtualization, formatCellReference, buildHeaderRows, indexToColumnLetter, ROW_NUMBER_COLUMN_WIDTH, getHeaderFilterConfig, getCellRenderDescriptor, buildInlineEditorProps, buildPopoverEditorProps, resolveCellDisplayContent, resolveCellStyle, rangesEqual, normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear, measureColumnContentWidth, getPinStateForColumn, parseValue, applyFillValues, processFormulaBarCommit, applyCellDeletion, computeTabNavigation, computeArrowNavigation, computeNextSortState, mergeFilter, columnLetterToIndex, getCellValue, applyRangeRowSelection, getScrollTopForRow, FormulaEngine, calculateDropTarget, reorderColumnArray, createGridDataAccessor, computeAutoScrollSpeed } from '@alaarab/ogrid-core';
2
2
  export * from '@alaarab/ogrid-core';
3
3
  export { buildInlineEditorProps, buildPopoverEditorProps, getCellRenderDescriptor, getHeaderFilterConfig, isInSelectionRange, normalizeSelectionRange, resolveCellDisplayContent, resolveCellStyle, toUserLike } from '@alaarab/ogrid-core';
4
4
  import { defineComponent, ref, computed, onMounted, watch, toValue, onUnmounted, h, shallowRef, triggerRef, nextTick, Teleport, isRef, isReadonly, unref, customRef } from 'vue';
@@ -173,6 +173,211 @@ var StatusBar = defineComponent({
173
173
  };
174
174
  }
175
175
  });
176
+ var FormulaBar = defineComponent({
177
+ name: "FormulaBar",
178
+ props: {
179
+ cellRef: { type: [String, null], default: null },
180
+ formulaText: { type: String, required: true },
181
+ isEditing: { type: Boolean, required: true }
182
+ },
183
+ emits: ["inputChange", "commit", "cancel", "startEditing"],
184
+ setup(props, { emit }) {
185
+ const inputRef = ref(null);
186
+ watch(() => props.isEditing, (editing) => {
187
+ if (editing && inputRef.value) {
188
+ inputRef.value.focus();
189
+ }
190
+ });
191
+ return () => h("div", { style: FORMULA_BAR_STYLES.bar, role: "toolbar", "aria-label": "Formula bar" }, [
192
+ h(
193
+ "div",
194
+ { style: FORMULA_BAR_STYLES.nameBox, "aria-label": "Active cell reference" },
195
+ props.cellRef ?? "\u2014"
196
+ ),
197
+ h("div", { style: FORMULA_BAR_STYLES.fxLabel, "aria-hidden": "true" }, "fx"),
198
+ h("input", {
199
+ ref: inputRef,
200
+ type: "text",
201
+ style: FORMULA_BAR_STYLES.input,
202
+ value: props.formulaText,
203
+ readonly: !props.isEditing,
204
+ onInput: (e) => emit("inputChange", e.target.value),
205
+ onKeydown: (e) => handleFormulaBarKeyDown(e.key, () => e.preventDefault(), () => emit("commit"), () => emit("cancel")),
206
+ onClick: () => {
207
+ if (!props.isEditing) emit("startEditing");
208
+ },
209
+ onDblclick: () => {
210
+ if (!props.isEditing) emit("startEditing");
211
+ },
212
+ "aria-label": "Formula input",
213
+ spellcheck: false,
214
+ autocomplete: "off"
215
+ })
216
+ ]);
217
+ }
218
+ });
219
+ var barStyle = {
220
+ display: "flex",
221
+ alignItems: "center",
222
+ borderTop: "1px solid var(--ogrid-border, #e0e0e0)",
223
+ background: "var(--ogrid-header-bg, #f5f5f5)",
224
+ minHeight: "30px",
225
+ overflowX: "auto",
226
+ overflowY: "hidden",
227
+ gap: "0",
228
+ fontSize: "12px"
229
+ };
230
+ var addBtnStyle = {
231
+ background: "none",
232
+ border: "none",
233
+ cursor: "pointer",
234
+ padding: "4px 10px",
235
+ fontSize: "16px",
236
+ lineHeight: "22px",
237
+ color: "var(--ogrid-fg-secondary, #666)",
238
+ flexShrink: 0
239
+ };
240
+ var tabBaseStyle = {
241
+ background: "none",
242
+ border: "none",
243
+ borderBottom: "2px solid transparent",
244
+ cursor: "pointer",
245
+ padding: "4px 16px",
246
+ fontSize: "12px",
247
+ lineHeight: "22px",
248
+ color: "var(--ogrid-fg, #242424)",
249
+ whiteSpace: "nowrap",
250
+ position: "relative"
251
+ };
252
+ var activeTabStyle = {
253
+ ...tabBaseStyle,
254
+ fontWeight: 600,
255
+ borderBottomColor: "var(--ogrid-primary, #217346)",
256
+ background: "var(--ogrid-bg, #fff)"
257
+ };
258
+ var SheetTabs = defineComponent({
259
+ name: "SheetTabs",
260
+ props: {
261
+ sheets: { type: Array, required: true },
262
+ activeSheet: { type: String, required: true },
263
+ showAddButton: { type: Boolean, default: false }
264
+ },
265
+ emits: ["sheetChange", "sheetAdd"],
266
+ setup(props, { emit }) {
267
+ return () => h("div", { style: barStyle, role: "tablist", "aria-label": "Sheet tabs" }, [
268
+ props.showAddButton ? h("button", {
269
+ type: "button",
270
+ style: addBtnStyle,
271
+ onClick: () => emit("sheetAdd"),
272
+ title: "Add sheet",
273
+ "aria-label": "Add sheet"
274
+ }, "+") : null,
275
+ ...props.sheets.map((sheet) => {
276
+ const isActive = sheet.id === props.activeSheet;
277
+ const base = isActive ? activeTabStyle : tabBaseStyle;
278
+ const style = isActive && sheet.color ? { ...base, borderBottomColor: sheet.color } : base;
279
+ return h("button", {
280
+ key: sheet.id,
281
+ type: "button",
282
+ role: "tab",
283
+ "aria-selected": isActive,
284
+ style,
285
+ onClick: () => emit("sheetChange", sheet.id)
286
+ }, sheet.name);
287
+ })
288
+ ]);
289
+ }
290
+ });
291
+ function measureRef(container, r, colOffset) {
292
+ const startCol = r.col + colOffset;
293
+ const endCol = (r.endCol ?? r.col) + colOffset;
294
+ const endRow = r.endRow ?? r.row;
295
+ const tl = container.querySelector(
296
+ `[data-row-index="${r.row}"][data-col-index="${startCol}"]`
297
+ );
298
+ const br = container.querySelector(
299
+ `[data-row-index="${endRow}"][data-col-index="${endCol}"]`
300
+ );
301
+ if (!tl || !br) return null;
302
+ const cRect = container.getBoundingClientRect();
303
+ const tlRect = tl.getBoundingClientRect();
304
+ const brRect = br.getBoundingClientRect();
305
+ return {
306
+ top: Math.round(tlRect.top - cRect.top),
307
+ left: Math.round(tlRect.left - cRect.left),
308
+ width: Math.round(brRect.right - tlRect.left),
309
+ height: Math.round(brRect.bottom - tlRect.top),
310
+ color: FORMULA_REF_COLORS[r.colorIndex % FORMULA_REF_COLORS.length]
311
+ };
312
+ }
313
+ var FormulaRefOverlay = defineComponent({
314
+ name: "FormulaRefOverlay",
315
+ props: {
316
+ containerEl: { type: Object, default: null },
317
+ references: { type: Array, required: true },
318
+ colOffset: { type: Number, required: true }
319
+ },
320
+ setup(props) {
321
+ const rects = ref([]);
322
+ let rafId = 0;
323
+ function measureAll() {
324
+ const container = props.containerEl;
325
+ const refs = props.references;
326
+ if (!container || refs.length === 0) {
327
+ rects.value = [];
328
+ return;
329
+ }
330
+ const measured = [];
331
+ for (const r of refs) {
332
+ const rect = measureRef(container, r, props.colOffset);
333
+ if (rect) measured.push(rect);
334
+ }
335
+ rects.value = measured;
336
+ }
337
+ watch(
338
+ () => [props.references, props.containerEl, props.colOffset],
339
+ () => {
340
+ cancelAnimationFrame(rafId);
341
+ if (!props.containerEl || props.references.length === 0) {
342
+ rects.value = [];
343
+ return;
344
+ }
345
+ rafId = requestAnimationFrame(measureAll);
346
+ },
347
+ { immediate: true }
348
+ );
349
+ return () => {
350
+ if (rects.value.length === 0) return null;
351
+ return rects.value.map(
352
+ (r, i) => h("svg", {
353
+ key: i,
354
+ style: {
355
+ position: "absolute",
356
+ top: `${r.top}px`,
357
+ left: `${r.left}px`,
358
+ width: `${r.width}px`,
359
+ height: `${r.height}px`,
360
+ pointerEvents: "none",
361
+ zIndex: 3,
362
+ overflow: "visible"
363
+ },
364
+ "aria-hidden": "true"
365
+ }, [
366
+ h("rect", {
367
+ x: "1",
368
+ y: "1",
369
+ width: Math.max(0, r.width - 2),
370
+ height: Math.max(0, r.height - 2),
371
+ fill: "none",
372
+ stroke: r.color,
373
+ "stroke-width": "2",
374
+ style: "shape-rendering: crispEdges"
375
+ })
376
+ ])
377
+ );
378
+ };
379
+ }
380
+ });
176
381
  function useFilterOptions(dataSource, fields) {
177
382
  const filterOptions = ref({});
178
383
  const loadingOptions = ref({});
@@ -210,6 +415,163 @@ function useFilterOptions(dataSource, fields) {
210
415
  }, { immediate: true });
211
416
  return { filterOptions, loadingOptions };
212
417
  }
418
+ function useLatestRef(source) {
419
+ let value = unref(source);
420
+ return customRef((track, trigger) => ({
421
+ get() {
422
+ if (isRef(source)) {
423
+ value = source.value;
424
+ }
425
+ return value;
426
+ },
427
+ set(newValue) {
428
+ value = newValue;
429
+ trigger();
430
+ }
431
+ }));
432
+ }
433
+
434
+ // src/composables/useFormulaEngine.ts
435
+ function useFormulaEngine(params) {
436
+ const {
437
+ formulas,
438
+ items,
439
+ flatColumns,
440
+ initialFormulas,
441
+ onFormulaRecalc,
442
+ formulaFunctions,
443
+ namedRanges,
444
+ sheets
445
+ } = params;
446
+ const itemsRef = useLatestRef(items);
447
+ const flatColumnsRef = useLatestRef(flatColumns);
448
+ const onFormulaRecalcRef = useLatestRef(onFormulaRecalc);
449
+ const engineRef = shallowRef(null);
450
+ let initialLoaded = false;
451
+ const enabled = computed(() => formulas?.value ?? false);
452
+ function createAccessor() {
453
+ return createGridDataAccessor(itemsRef.value, flatColumnsRef.value);
454
+ }
455
+ watch(
456
+ enabled,
457
+ (isEnabled) => {
458
+ if (isEnabled && !engineRef.value) {
459
+ engineRef.value = new FormulaEngine({
460
+ customFunctions: formulaFunctions,
461
+ namedRanges
462
+ });
463
+ if (sheets) {
464
+ for (const [name, accessor] of Object.entries(sheets)) {
465
+ engineRef.value.registerSheet(name, accessor);
466
+ }
467
+ }
468
+ if (initialFormulas && !initialLoaded) {
469
+ initialLoaded = true;
470
+ const accessor = createAccessor();
471
+ const result = engineRef.value.loadFormulas(initialFormulas, accessor);
472
+ if (result.updatedCells.length > 0) {
473
+ onFormulaRecalcRef.value?.(result);
474
+ }
475
+ }
476
+ } else if (!isEnabled && engineRef.value) {
477
+ engineRef.value = null;
478
+ }
479
+ },
480
+ { immediate: true }
481
+ );
482
+ function getFormulaValue(col, row) {
483
+ return engineRef.value?.getValue(col, row);
484
+ }
485
+ function hasFormula(col, row) {
486
+ return engineRef.value?.hasFormula(col, row) ?? false;
487
+ }
488
+ function getFormula(col, row) {
489
+ return engineRef.value?.getFormula(col, row);
490
+ }
491
+ function setFormula(col, row, formula) {
492
+ if (!engineRef.value) return;
493
+ const accessor = createAccessor();
494
+ const result = engineRef.value.setFormula(col, row, formula, accessor);
495
+ if (result.updatedCells.length > 0) {
496
+ onFormulaRecalcRef.value?.(result);
497
+ }
498
+ }
499
+ function onCellChanged(col, row) {
500
+ if (!engineRef.value) return;
501
+ const accessor = createAccessor();
502
+ const result = engineRef.value.onCellChanged(col, row, accessor);
503
+ if (result.updatedCells.length > 0) {
504
+ onFormulaRecalcRef.value?.(result);
505
+ }
506
+ }
507
+ function getPrecedents(col, row) {
508
+ return engineRef.value?.getPrecedents(col, row) ?? [];
509
+ }
510
+ function getDependents(col, row) {
511
+ return engineRef.value?.getDependents(col, row) ?? [];
512
+ }
513
+ function getAuditTrail(col, row) {
514
+ return engineRef.value?.getAuditTrail(col, row) ?? null;
515
+ }
516
+ return {
517
+ enabled,
518
+ getFormulaValue,
519
+ hasFormula,
520
+ getFormula,
521
+ setFormula,
522
+ onCellChanged,
523
+ getPrecedents,
524
+ getDependents,
525
+ getAuditTrail
526
+ };
527
+ }
528
+ function useFormulaBar(params) {
529
+ const { activeCol, activeRow, activeCellRef, getFormula, getRawValue, setFormula, onCellValueChanged } = params;
530
+ const isEditing = ref(false);
531
+ const editText = ref("");
532
+ const isFormulaBarEditing = ref(false);
533
+ const displayText = computed(
534
+ () => deriveFormulaBarText(activeCol.value, activeRow.value, getFormula, getRawValue)
535
+ );
536
+ watch([activeCol, activeRow], () => {
537
+ isEditing.value = false;
538
+ isFormulaBarEditing.value = false;
539
+ });
540
+ const startEditing = () => {
541
+ editText.value = displayText.value;
542
+ isEditing.value = true;
543
+ isFormulaBarEditing.value = true;
544
+ };
545
+ const onInputChange = (text) => {
546
+ editText.value = text;
547
+ };
548
+ const onCommit = () => {
549
+ const col = activeCol.value;
550
+ const row = activeRow.value;
551
+ if (col == null || row == null || !setFormula) return;
552
+ processFormulaBarCommit(editText.value, col, row, setFormula, onCellValueChanged);
553
+ isEditing.value = false;
554
+ isFormulaBarEditing.value = false;
555
+ };
556
+ const onCancel = () => {
557
+ isEditing.value = false;
558
+ isFormulaBarEditing.value = false;
559
+ editText.value = "";
560
+ };
561
+ const formulaText = computed(() => isEditing.value ? editText.value : displayText.value);
562
+ const referencedCells = computed(() => extractFormulaReferences(formulaText.value));
563
+ return {
564
+ cellRef: activeCellRef,
565
+ formulaText,
566
+ isEditing,
567
+ onInputChange,
568
+ onCommit,
569
+ onCancel,
570
+ startEditing,
571
+ referencedCells,
572
+ isFormulaBarEditing
573
+ };
574
+ }
213
575
  var DEFAULT_PANELS = ["columns", "filters"];
214
576
  function useSideBarState(params) {
215
577
  const { config } = params;
@@ -494,6 +856,22 @@ function useOGrid(props) {
494
856
  const displayTotalCount = computed(
495
857
  () => isClientSide.value && resolvedClientItems.value ? resolvedClientItems.value.totalCount : serverTotalCount.value
496
858
  );
859
+ const formulasRef = computed(() => !!props.value.formulas);
860
+ const formulaVersionRef = ref(0);
861
+ const wrappedOnFormulaRecalc = (result) => {
862
+ formulaVersionRef.value += 1;
863
+ props.value.onFormulaRecalc?.(result);
864
+ };
865
+ const formulaEngine = useFormulaEngine({
866
+ formulas: formulasRef,
867
+ items: displayItems,
868
+ flatColumns: columns,
869
+ initialFormulas: props.value.initialFormulas,
870
+ onFormulaRecalc: wrappedOnFormulaRecalc,
871
+ formulaFunctions: props.value.formulaFunctions,
872
+ namedRanges: props.value.namedRanges,
873
+ sheets: props.value.sheets
874
+ });
497
875
  let firstDataRendered = false;
498
876
  let rowIdsValidated = false;
499
877
  watch(displayItems, (items) => {
@@ -581,6 +959,37 @@ function useOGrid(props) {
581
959
  });
582
960
  const clearAllFilters = () => setFilters({});
583
961
  const isLoadingResolved = computed(() => isServerSide.value && loading.value || displayLoading.value);
962
+ const activeCellRef = ref(null);
963
+ const activeCellCoords = ref(null);
964
+ const onActiveCellChange = (cellRef) => {
965
+ activeCellRef.value = cellRef;
966
+ if (cellRef) {
967
+ const m = cellRef.match(/^([A-Z]+)(\d+)$/);
968
+ if (m) {
969
+ activeCellCoords.value = { col: columnLetterToIndex(m[1]), row: parseInt(m[2], 10) - 1 };
970
+ } else {
971
+ activeCellCoords.value = null;
972
+ }
973
+ } else {
974
+ activeCellCoords.value = null;
975
+ }
976
+ };
977
+ const formulaBarActiveCol = computed(() => activeCellCoords.value?.col ?? null);
978
+ const formulaBarActiveRow = computed(() => activeCellCoords.value?.row ?? null);
979
+ const getRawValue = (col, row) => {
980
+ const items = displayItems.value;
981
+ const cols = columns.value;
982
+ if (row < 0 || row >= items.length || col < 0 || col >= cols.length) return void 0;
983
+ return getCellValue(items[row], cols[col]);
984
+ };
985
+ const formulaBarState = useFormulaBar({
986
+ activeCol: formulaBarActiveCol,
987
+ activeRow: formulaBarActiveRow,
988
+ activeCellRef,
989
+ getFormula: formulaEngine.enabled.value ? formulaEngine.getFormula : void 0,
990
+ getRawValue,
991
+ setFormula: formulaEngine.enabled.value ? formulaEngine.setFormula : void 0
992
+ });
584
993
  const dataGridProps = computed(() => {
585
994
  const p = props.value;
586
995
  const ds = dataProps.value.dataSource;
@@ -608,7 +1017,11 @@ function useOGrid(props) {
608
1017
  rowSelection: p.rowSelection ?? "none",
609
1018
  selectedRows: effectiveSelectedRows.value,
610
1019
  onSelectionChange: handleSelectionChange,
611
- showRowNumbers: p.showRowNumbers,
1020
+ showRowNumbers: p.showRowNumbers || p.cellReferences || p.formulas,
1021
+ showColumnLetters: !!(p.cellReferences || p.formulas),
1022
+ showNameBox: !!(p.cellReferences && !p.formulas),
1023
+ // formula bar includes name box
1024
+ onActiveCellChange: p.cellReferences || p.formulas ? onActiveCellChange : void 0,
612
1025
  currentPage: page.value,
613
1026
  pageSize: pageSize.value,
614
1027
  statusBar: statusBarConfig.value,
@@ -633,7 +1046,20 @@ function useOGrid(props) {
633
1046
  onClearAll: clearAllFilters,
634
1047
  message: p.emptyState?.message,
635
1048
  render: p.emptyState?.render
636
- }
1049
+ },
1050
+ formulas: p.formulas,
1051
+ formulaVersion: formulaVersionRef.value,
1052
+ ...formulaEngine.enabled.value ? {
1053
+ getFormulaValue: formulaEngine.getFormulaValue,
1054
+ hasFormula: formulaEngine.hasFormula,
1055
+ getFormula: formulaEngine.getFormula,
1056
+ setFormula: formulaEngine.setFormula,
1057
+ onFormulaCellChanged: formulaEngine.onCellChanged,
1058
+ getPrecedents: formulaEngine.getPrecedents,
1059
+ getDependents: formulaEngine.getDependents,
1060
+ getAuditTrail: formulaEngine.getAuditTrail
1061
+ } : {},
1062
+ formulaReferences: formulaBarState.referencedCells.value.length > 0 ? formulaBarState.referencedCells.value : void 0
637
1063
  };
638
1064
  });
639
1065
  const pagination = computed(() => ({
@@ -651,14 +1077,59 @@ function useOGrid(props) {
651
1077
  onVisibilityChange: handleVisibilityChange,
652
1078
  placement: columnChooserPlacement.value
653
1079
  }));
654
- const layout = computed(() => ({
655
- toolbar: props.value.toolbar,
656
- toolbarBelow: props.value.toolbarBelow,
657
- className: props.value.className,
658
- emptyState: props.value.emptyState,
659
- sideBarProps: sideBarProps.value,
660
- fullScreen: props.value.fullScreen
661
- }));
1080
+ const layout = computed(() => {
1081
+ const p = props.value;
1082
+ const formulas = !!p.formulas;
1083
+ const showNameBox = !!p.cellReferences && !formulas;
1084
+ let resolvedToolbar = p.toolbar;
1085
+ if (showNameBox) {
1086
+ const nameBoxEl = h("div", {
1087
+ style: {
1088
+ display: "inline-flex",
1089
+ alignItems: "center",
1090
+ padding: "0 8px",
1091
+ fontFamily: "'Consolas', 'Courier New', monospace",
1092
+ fontSize: "12px",
1093
+ border: "1px solid rgba(0,0,0,0.12)",
1094
+ borderRadius: "3px",
1095
+ height: "24px",
1096
+ marginRight: "8px",
1097
+ background: "#fff",
1098
+ minWidth: "40px",
1099
+ color: "rgba(0,0,0,0.6)"
1100
+ },
1101
+ "aria-label": "Active cell reference"
1102
+ }, activeCellRef.value ?? "\u2014");
1103
+ resolvedToolbar = [nameBoxEl, resolvedToolbar];
1104
+ }
1105
+ const formulaBarEl = formulas ? h(FormulaBar, {
1106
+ cellRef: formulaBarState.cellRef.value,
1107
+ formulaText: formulaBarState.formulaText.value,
1108
+ isEditing: formulaBarState.isEditing.value,
1109
+ onInputChange: formulaBarState.onInputChange,
1110
+ onCommit: formulaBarState.onCommit,
1111
+ onCancel: formulaBarState.onCancel,
1112
+ onStartEditing: formulaBarState.startEditing
1113
+ }) : void 0;
1114
+ const sheetTabsEl = p.sheetDefs && p.sheetDefs.length > 0 && p.activeSheet && p.onSheetChange ? h(SheetTabs, {
1115
+ sheets: p.sheetDefs,
1116
+ activeSheet: p.activeSheet,
1117
+ showAddButton: !!p.onSheetAdd,
1118
+ onSheetChange: p.onSheetChange,
1119
+ onSheetAdd: p.onSheetAdd ?? (() => {
1120
+ })
1121
+ }) : void 0;
1122
+ return {
1123
+ toolbar: resolvedToolbar,
1124
+ toolbarBelow: p.toolbarBelow,
1125
+ className: p.className,
1126
+ emptyState: p.emptyState,
1127
+ sideBarProps: sideBarProps.value,
1128
+ fullScreen: p.fullScreen,
1129
+ formulaBar: formulaBarEl,
1130
+ sheetTabs: sheetTabsEl
1131
+ };
1132
+ });
662
1133
  const filtersResult = computed(() => ({
663
1134
  hasActiveFilters: hasActiveFilters.value,
664
1135
  setFilters
@@ -880,23 +1351,6 @@ function useActiveCell(wrapperRef, editingCell) {
880
1351
  });
881
1352
  return { activeCell, setActiveCell };
882
1353
  }
883
- function useLatestRef(source) {
884
- let value = unref(source);
885
- return customRef((track, trigger) => ({
886
- get() {
887
- if (isRef(source)) {
888
- value = source.value;
889
- }
890
- return value;
891
- },
892
- set(newValue) {
893
- value = newValue;
894
- trigger();
895
- }
896
- }));
897
- }
898
-
899
- // src/composables/useCellSelection.ts
900
1354
  var DRAG_ATTR = "data-drag-range";
901
1355
  var DRAG_ANCHOR_ATTR = "data-drag-anchor";
902
1356
  var AUTO_SCROLL_EDGE = 40;
@@ -1313,7 +1767,7 @@ function useKeyboardNavigation(params) {
1313
1767
  const maxColIndex = visibleColumnCount - 1 + colOffset;
1314
1768
  if (items.length === 0) return;
1315
1769
  if (activeCell === null) {
1316
- if (["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "Tab", "Enter", "Home", "End"].includes(e.key)) {
1770
+ if (["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "Tab", "Enter", "Home", "End", "PageDown", "PageUp"].includes(e.key)) {
1317
1771
  setActiveCell({ rowIndex: 0, columnIndex: colOffset });
1318
1772
  e.preventDefault();
1319
1773
  }
@@ -1413,6 +1867,36 @@ function useKeyboardNavigation(params) {
1413
1867
  setActiveCell({ rowIndex: newRowEnd, columnIndex: maxColIndex });
1414
1868
  break;
1415
1869
  }
1870
+ case "PageDown":
1871
+ case "PageUp": {
1872
+ e.preventDefault();
1873
+ const wrapperEl = wrapperRef.value;
1874
+ let pageSize = 10;
1875
+ if (wrapperEl) {
1876
+ const row = wrapperEl.querySelector("tbody tr");
1877
+ if (row && row.offsetHeight > 0) pageSize = Math.max(1, Math.floor(wrapperEl.clientHeight / row.offsetHeight));
1878
+ }
1879
+ const pgDirection = e.key === "PageDown" ? 1 : -1;
1880
+ const newRowPage = Math.max(0, Math.min(rowIndex + pgDirection * pageSize, maxRowIndex));
1881
+ if (shift) {
1882
+ setSelectionRange({
1883
+ startRow: selectionRange?.startRow ?? rowIndex,
1884
+ startCol: selectionRange?.startCol ?? dataColIndex,
1885
+ endRow: newRowPage,
1886
+ endCol: selectionRange?.endCol ?? dataColIndex
1887
+ });
1888
+ } else {
1889
+ setSelectionRange({
1890
+ startRow: newRowPage,
1891
+ startCol: dataColIndex,
1892
+ endRow: newRowPage,
1893
+ endCol: dataColIndex
1894
+ });
1895
+ }
1896
+ setActiveCell({ rowIndex: newRowPage, columnIndex });
1897
+ scrollToRow?.(newRowPage, "center");
1898
+ break;
1899
+ }
1416
1900
  case "Enter":
1417
1901
  case "F2": {
1418
1902
  e.preventDefault();
@@ -2263,7 +2747,10 @@ function useDataGridState(params) {
2263
2747
  getRowId,
2264
2748
  editable: editableProp.value,
2265
2749
  onCellValueChanged: onCellValueChanged.value,
2266
- isDragging: cellSelection.value ? isDragging.value : false
2750
+ isDragging: cellSelection.value ? isDragging.value : false,
2751
+ getFormulaValue: props.value.getFormulaValue,
2752
+ hasFormula: props.value.hasFormula,
2753
+ formulaVersion: props.value.formulaVersion
2267
2754
  }));
2268
2755
  const popoverAnchorEl = ref(null);
2269
2756
  const setPopoverAnchorEl = (el) => {
@@ -2952,9 +3439,14 @@ function useColumnChooserState(params) {
2952
3439
  }
2953
3440
  function useInlineCellEditorState(params) {
2954
3441
  const { value, editorType, onCommit, onCancel } = params;
2955
- const localValue = ref(
2956
- value !== null && value !== void 0 ? String(value) : ""
2957
- );
3442
+ const localValue = ref((() => {
3443
+ if (value === null || value === void 0) return "";
3444
+ if (editorType === "date") {
3445
+ const str = String(value);
3446
+ return str.match(/^\d{4}-\d{2}-\d{2}/) ? str.substring(0, 10) : str;
3447
+ }
3448
+ return String(value);
3449
+ })());
2958
3450
  const setLocalValue = (v) => {
2959
3451
  localValue.value = v;
2960
3452
  };
@@ -2970,14 +3462,14 @@ function useInlineCellEditorState(params) {
2970
3462
  e.stopPropagation();
2971
3463
  cancel();
2972
3464
  }
2973
- if (e.key === "Enter" && editorType === "text") {
3465
+ if (e.key === "Enter" && (editorType === "text" || editorType === "date")) {
2974
3466
  e.preventDefault();
2975
3467
  e.stopPropagation();
2976
3468
  commit(localValue.value);
2977
3469
  }
2978
3470
  };
2979
3471
  const handleBlur = () => {
2980
- if (editorType === "text") {
3472
+ if (editorType === "text" || editorType === "date") {
2981
3473
  commit(localValue.value);
2982
3474
  }
2983
3475
  };
@@ -3336,6 +3828,7 @@ function getCellInteractionProps(descriptor, columnId, handlers) {
3336
3828
  const base = {
3337
3829
  "data-row-index": descriptor.rowIndex,
3338
3830
  "data-col-index": descriptor.globalColIndex,
3831
+ ...descriptor.isActive ? { "data-active-cell": "true" } : {},
3339
3832
  ...descriptor.isInRange ? { "data-in-range": "true" } : {},
3340
3833
  tabindex: descriptor.isActive ? 0 : -1,
3341
3834
  onMousedown: (e) => handlers.handleCellMouseDown(e, descriptor.rowIndex, descriptor.globalColIndex),
@@ -3371,6 +3864,24 @@ function createDataGridTable(ui) {
3371
3864
  columnPartition,
3372
3865
  globalColIndexMap
3373
3866
  } = useDataGridTableSetup({ props: propsRef });
3867
+ const rowNumberOffset = computed(() => {
3868
+ const p = propsRef.value;
3869
+ const hasRowNumbers = p.showRowNumbers || p.showColumnLetters;
3870
+ return hasRowNumbers ? ((p.currentPage ?? 1) - 1) * (p.pageSize ?? 25) : 0;
3871
+ });
3872
+ watch(
3873
+ [() => state.interaction.value.activeCell, rowNumberOffset],
3874
+ ([ac, offset]) => {
3875
+ const cb = propsRef.value.onActiveCellChange;
3876
+ if (!cb) return;
3877
+ if (ac) {
3878
+ cb(formatCellReference(ac.columnIndex - state.layout.value.colOffset, offset + ac.rowIndex + 1));
3879
+ } else {
3880
+ cb(null);
3881
+ }
3882
+ },
3883
+ { immediate: true }
3884
+ );
3374
3885
  const onWrapperMousedown = (e) => {
3375
3886
  lastMouseShift.value = e.shiftKey;
3376
3887
  };
@@ -3446,7 +3957,7 @@ function createDataGridTable(ui) {
3446
3957
  } = layout;
3447
3958
  const currentPage = p.currentPage ?? 1;
3448
3959
  const pageSize = p.pageSize ?? 25;
3449
- const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * pageSize : 0;
3960
+ const rowNumberOffset2 = hasRowNumbersCol ? (currentPage - 1) * pageSize : 0;
3450
3961
  const { selectedRowIds, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
3451
3962
  const { editingCell: _editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
3452
3963
  const {
@@ -3536,13 +4047,14 @@ function createDataGridTable(ui) {
3536
4047
  ]);
3537
4048
  }
3538
4049
  const content = resolveCellDisplayContent(col, item, descriptor.displayValue);
3539
- const cellStyle = resolveCellStyle(col, item);
4050
+ const cellStyle = resolveCellStyle(col, item, descriptor.displayValue);
3540
4051
  const interactionProps2 = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
3541
4052
  const cellClasses = ["ogrid-cell-content"];
3542
4053
  if (col.type === "numeric") cellClasses.push("ogrid-cell-content--numeric");
3543
4054
  else if (col.type === "boolean") cellClasses.push("ogrid-cell-content--boolean");
3544
4055
  if (descriptor.canEditAny) cellClasses.push("ogrid-cell-content--editable");
3545
4056
  if (descriptor.isActive) cellClasses.push("ogrid-cell-content--active");
4057
+ if (descriptor.isActive && descriptor.isInRange) cellClasses.push("ogrid-cell-content--active-in-range");
3546
4058
  if (descriptor.isInRange && !descriptor.isActive) cellClasses.push("ogrid-cell-in-range");
3547
4059
  if (descriptor.isInCutRange) cellClasses.push("ogrid-cell-cut");
3548
4060
  const styledContent = cellStyle ? h("span", { style: cellStyle }, content) : content;
@@ -3644,10 +4156,23 @@ function createDataGridTable(ui) {
3644
4156
  ...virtualScrollEnabled.value ? { "data-virtual-scroll": "" } : {}
3645
4157
  }, [
3646
4158
  // Header
3647
- h(
3648
- "thead",
3649
- { class: stickyHeader ? "ogrid-thead ogrid-sticky-header" : "ogrid-thead" },
3650
- headerRows.map(
4159
+ h("thead", { class: stickyHeader ? "ogrid-thead ogrid-sticky-header" : "ogrid-thead" }, [
4160
+ // Column letter row (A, B, C...) for cell references
4161
+ ...p.showColumnLetters ? [
4162
+ h("tr", { class: "ogrid-column-letter-row" }, [
4163
+ ...hasCheckboxCol ? [h("th", { class: "ogrid-column-letter-cell" })] : [],
4164
+ ...hasRowNumbersCol ? [h("th", { class: "ogrid-column-letter-cell" })] : [],
4165
+ ...visibleCols.map((col, colIdx) => {
4166
+ const { classes: hdrCls, style: hdrSty } = getHeaderClassAndStyle(col);
4167
+ return h("th", {
4168
+ key: col.columnId,
4169
+ class: `ogrid-column-letter-cell ${hdrCls}`,
4170
+ style: hdrSty
4171
+ }, indexToColumnLetter(colIdx));
4172
+ })
4173
+ ])
4174
+ ] : [],
4175
+ ...headerRows.map(
3651
4176
  (row, rowIdx) => h("tr", { key: rowIdx, class: "ogrid-header-row" }, [
3652
4177
  // Checkbox header cell
3653
4178
  ...rowIdx === headerRows.length - 1 && hasCheckboxCol ? [
@@ -3757,7 +4282,7 @@ function createDataGridTable(ui) {
3757
4282
  })
3758
4283
  ])
3759
4284
  )
3760
- ),
4285
+ ]),
3761
4286
  // Body
3762
4287
  ...!showEmptyInGrid ? [
3763
4288
  h("tbody", {}, (() => {
@@ -3822,7 +4347,7 @@ function createDataGridTable(ui) {
3822
4347
  left: hasCheckboxCol ? `${CHECKBOX_COLUMN_WIDTH}px` : "0",
3823
4348
  zIndex: 2
3824
4349
  }
3825
- }, String(rowNumberOffset + rowIndex + 1))
4350
+ }, String(rowNumberOffset2 + rowIndex + 1))
3826
4351
  ] : [],
3827
4352
  // Left spacer for column virtualization
3828
4353
  ...leftSpacerWidth > 0 ? [
@@ -3893,6 +4418,14 @@ function createDataGridTable(ui) {
3893
4418
  columnSizingOverrides: layout.columnSizingOverrides,
3894
4419
  columnOrder: p.columnOrder
3895
4420
  }),
4421
+ // Formula reference overlay
4422
+ ...p.formulaReferences && p.formulaReferences.length > 0 ? [
4423
+ h(FormulaRefOverlay, {
4424
+ containerEl: tableContainerRef.value,
4425
+ references: p.formulaReferences,
4426
+ colOffset: _colOffset
4427
+ })
4428
+ ] : [],
3896
4429
  // Column header menu
3897
4430
  h(ui.ColumnHeaderMenu, {
3898
4431
  isOpen: headerMenu.isOpen,
@@ -3978,6 +4511,7 @@ function createInlineCellEditor(options) {
3978
4511
  dropdown.style.maxHeight = `${maxH}px`;
3979
4512
  dropdown.style.zIndex = "9999";
3980
4513
  dropdown.style.right = "auto";
4514
+ dropdown.style.textAlign = "left";
3981
4515
  if (flipUp) {
3982
4516
  dropdown.style.top = "auto";
3983
4517
  dropdown.style.bottom = `${window.innerHeight - rect.top}px`;
@@ -4083,7 +4617,7 @@ function createInlineCellEditor(options) {
4083
4617
  selectDropdownRef.value = el;
4084
4618
  },
4085
4619
  role: "listbox",
4086
- style: { position: "absolute", top: "100%", left: "0", right: "0", maxHeight: "200px", overflowY: "auto", background: "var(--ogrid-bg, #fff)", border: "1px solid var(--ogrid-border, rgba(0,0,0,0.12))", zIndex: "10", boxShadow: "0 4px 16px rgba(0,0,0,0.2)" }
4620
+ style: { position: "absolute", top: "100%", left: "0", right: "0", maxHeight: "200px", overflowY: "auto", background: "var(--ogrid-bg, #fff)", border: "1px solid var(--ogrid-border, rgba(0,0,0,0.12))", zIndex: "10", boxShadow: "0 4px 16px rgba(0,0,0,0.2)", textAlign: "left" }
4087
4621
  }, values.map(
4088
4622
  (v, i) => h("div", {
4089
4623
  key: String(v),
@@ -4493,8 +5027,12 @@ function createOGrid(ui) {
4493
5027
  style: { padding: "8px 12px", borderBottom: "1px solid var(--ogrid-border, rgba(0,0,0,0.12))" }
4494
5028
  }, [layout.value.toolbarBelow])
4495
5029
  ] : [],
5030
+ // Formula bar (between toolbar and grid)
5031
+ ...layout.value.formulaBar ? [layout.value.formulaBar] : [],
4496
5032
  // Main content area (sidebar + grid)
4497
5033
  h("div", { style: { display: "flex", flex: "1", minHeight: "0" } }, mainAreaChildren),
5034
+ // Sheet tabs (between grid and footer)
5035
+ ...layout.value.sheetTabs ? [layout.value.sheetTabs] : [],
4498
5036
  // Footer strip (pagination)
4499
5037
  h("div", {
4500
5038
  style: {
@@ -4511,4 +5049,4 @@ function createOGrid(ui) {
4511
5049
  });
4512
5050
  }
4513
5051
 
4514
- export { MarchingAntsOverlay, StatusBar, createDataGridTable, createInlineCellEditor, createOGrid, getCellInteractionProps, useActiveCell, useCellEditing, useCellSelection, useClipboard, useColumnChooserState, useColumnHeaderFilterState, useColumnHeaderMenuState, useColumnPinning, useColumnReorder, useColumnResize, useContextMenu, useDataGridState, useDataGridTableSetup, useDateFilterState, useDebounce, useDebouncedCallback, useFillHandle, useFilterOptions, useInlineCellEditorState, useKeyboardNavigation, useMultiSelectFilterState, useOGrid, usePeopleFilterState, useRichSelectState, useRowSelection, useSideBarState, useTableLayout, useTextFilterState, useUndoRedo, useVirtualScroll };
5052
+ export { FormulaBar, FormulaRefOverlay, MarchingAntsOverlay, SheetTabs, StatusBar, createDataGridTable, createInlineCellEditor, createOGrid, getCellInteractionProps, useActiveCell, useCellEditing, useCellSelection, useClipboard, useColumnChooserState, useColumnHeaderFilterState, useColumnHeaderMenuState, useColumnPinning, useColumnReorder, useColumnResize, useContextMenu, useDataGridState, useDataGridTableSetup, useDateFilterState, useDebounce, useDebouncedCallback, useFillHandle, useFilterOptions, useFormulaBar, useInlineCellEditorState, useKeyboardNavigation, useMultiSelectFilterState, useOGrid, usePeopleFilterState, useRichSelectState, useRowSelection, useSideBarState, useTableLayout, useTextFilterState, useUndoRedo, useVirtualScroll };
@@ -140,6 +140,24 @@
140
140
  color: var(--ogrid-fg);
141
141
  }
142
142
 
143
+ /* === Column letter row (cell references) === */
144
+
145
+ .ogrid-column-letter-row {
146
+ background: var(--ogrid-column-letter-bg, var(--ogrid-header-bg));
147
+ }
148
+
149
+ .ogrid-column-letter-cell {
150
+ text-align: center;
151
+ font-size: 11px;
152
+ font-weight: 500;
153
+ color: var(--ogrid-fg-muted, rgba(0, 0, 0, 0.5));
154
+ padding: 2px 4px;
155
+ background: var(--ogrid-column-letter-bg, var(--ogrid-header-bg));
156
+ border-bottom: 1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12));
157
+ user-select: none;
158
+ font-variant-numeric: tabular-nums;
159
+ }
160
+
143
161
  /* === Checkbox column === */
144
162
 
145
163
  .ogrid-checkbox-header,
@@ -254,6 +272,14 @@
254
272
  overflow: visible;
255
273
  }
256
274
 
275
+ /* Active cell inside a selection range: suppress outline (Excel behavior).
276
+ The range overlay border is sufficient; the active cell is distinguished
277
+ by its white background vs. the dimmed range cells. */
278
+ .ogrid-cell-content--active-in-range {
279
+ outline: none;
280
+ background: var(--ogrid-bg, #fff);
281
+ }
282
+
257
283
  /* === Editing cell wrapper === */
258
284
 
259
285
  .ogrid-editing-cell {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * FormulaBar — Headless Excel-style formula bar component.
3
+ *
4
+ * Layout: [Name Box] [fx] [Formula Input]
5
+ *
6
+ * Uses --ogrid-* CSS variables for theming.
7
+ */
8
+ import { type PropType } from 'vue';
9
+ export interface FormulaBarProps {
10
+ /** Active cell reference (e.g. "A1"). */
11
+ cellRef: string | null;
12
+ /** Text displayed/edited in the formula input. */
13
+ formulaText: string;
14
+ /** Whether the input is in editing mode. */
15
+ isEditing: boolean;
16
+ }
17
+ export declare const FormulaBar: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
18
+ cellRef: {
19
+ type: PropType<string | null>;
20
+ default: null;
21
+ };
22
+ formulaText: {
23
+ type: StringConstructor;
24
+ required: true;
25
+ };
26
+ isEditing: {
27
+ type: BooleanConstructor;
28
+ required: true;
29
+ };
30
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
31
+ [key: string]: any;
32
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("cancel" | "inputChange" | "commit" | "startEditing")[], "cancel" | "inputChange" | "commit" | "startEditing", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
33
+ cellRef: {
34
+ type: PropType<string | null>;
35
+ default: null;
36
+ };
37
+ formulaText: {
38
+ type: StringConstructor;
39
+ required: true;
40
+ };
41
+ isEditing: {
42
+ type: BooleanConstructor;
43
+ required: true;
44
+ };
45
+ }>> & Readonly<{
46
+ onCancel?: ((...args: any[]) => any) | undefined;
47
+ onInputChange?: ((...args: any[]) => any) | undefined;
48
+ onCommit?: ((...args: any[]) => any) | undefined;
49
+ onStartEditing?: ((...args: any[]) => any) | undefined;
50
+ }>, {
51
+ cellRef: string | null;
52
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * FormulaRefOverlay — Renders colored border overlays on cells referenced by
3
+ * the active formula, like Excel's reference highlighting.
4
+ *
5
+ * Port of React's FormulaRefOverlay component.
6
+ */
7
+ import { type PropType } from 'vue';
8
+ import { type FormulaReference } from '@alaarab/ogrid-core';
9
+ export declare const FormulaRefOverlay: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
10
+ containerEl: {
11
+ type: PropType<HTMLElement | null>;
12
+ default: null;
13
+ };
14
+ references: {
15
+ type: PropType<FormulaReference[]>;
16
+ required: true;
17
+ };
18
+ colOffset: {
19
+ type: NumberConstructor;
20
+ required: true;
21
+ };
22
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
23
+ [key: string]: any;
24
+ }>[] | null, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
25
+ containerEl: {
26
+ type: PropType<HTMLElement | null>;
27
+ default: null;
28
+ };
29
+ references: {
30
+ type: PropType<FormulaReference[]>;
31
+ required: true;
32
+ };
33
+ colOffset: {
34
+ type: NumberConstructor;
35
+ required: true;
36
+ };
37
+ }>> & Readonly<{}>, {
38
+ containerEl: HTMLElement | null;
39
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * SheetTabs — Excel-style sheet tab bar at the bottom of the grid.
3
+ *
4
+ * Layout: [+] [Sheet1] [Sheet2] [Sheet3]
5
+ */
6
+ import { type PropType } from 'vue';
7
+ import type { ISheetDef } from '@alaarab/ogrid-core';
8
+ export interface SheetTabsProps {
9
+ sheets: ISheetDef[];
10
+ activeSheet: string;
11
+ }
12
+ export declare const SheetTabs: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
13
+ sheets: {
14
+ type: PropType<ISheetDef[]>;
15
+ required: true;
16
+ };
17
+ activeSheet: {
18
+ type: StringConstructor;
19
+ required: true;
20
+ };
21
+ showAddButton: {
22
+ type: BooleanConstructor;
23
+ default: boolean;
24
+ };
25
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
26
+ [key: string]: any;
27
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("sheetChange" | "sheetAdd")[], "sheetChange" | "sheetAdd", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
28
+ sheets: {
29
+ type: PropType<ISheetDef[]>;
30
+ required: true;
31
+ };
32
+ activeSheet: {
33
+ type: StringConstructor;
34
+ required: true;
35
+ };
36
+ showAddButton: {
37
+ type: BooleanConstructor;
38
+ default: boolean;
39
+ };
40
+ }>> & Readonly<{
41
+ onSheetChange?: ((...args: any[]) => any) | undefined;
42
+ onSheetAdd?: ((...args: any[]) => any) | undefined;
43
+ }>, {
44
+ showAddButton: boolean;
45
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -30,6 +30,10 @@ export { useLatestRef } from './useLatestRef';
30
30
  export type { MaybeShallowRef } from './useLatestRef';
31
31
  export { useTableLayout } from './useTableLayout';
32
32
  export type { UseTableLayoutParams, UseTableLayoutResult } from './useTableLayout';
33
+ export { useFormulaEngine } from './useFormulaEngine';
34
+ export type { UseFormulaEngineParams, UseFormulaEngineResult } from './useFormulaEngine';
35
+ export { useFormulaBar } from './useFormulaBar';
36
+ export type { UseFormulaBarParams, UseFormulaBarResult } from './useFormulaBar';
33
37
  export { useColumnHeaderFilterState } from './useColumnHeaderFilterState';
34
38
  export type { UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, } from './useColumnHeaderFilterState';
35
39
  export { useTextFilterState } from './useTextFilterState';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * useFormulaBar — Vue composable for formula bar state.
3
+ *
4
+ * Manages the formula bar text, editing mode, and reference extraction.
5
+ */
6
+ import { type Ref } from 'vue';
7
+ import { type FormulaReference } from '@alaarab/ogrid-core';
8
+ export interface UseFormulaBarParams {
9
+ /** Active cell column index (0-based). */
10
+ activeCol: Ref<number | null>;
11
+ /** Active cell row index (0-based). */
12
+ activeRow: Ref<number | null>;
13
+ /** Active cell reference string (e.g. "A1"). */
14
+ activeCellRef: Ref<string | null>;
15
+ /** Get formula string for a cell. */
16
+ getFormula?: (col: number, row: number) => string | undefined;
17
+ /** Get raw display value for a cell. */
18
+ getRawValue?: (col: number, row: number) => unknown;
19
+ /** Set formula for a cell. */
20
+ setFormula?: (col: number, row: number, formula: string | null) => void;
21
+ /** Commit a non-formula value change. */
22
+ onCellValueChanged?: (col: number, row: number, value: unknown) => void;
23
+ }
24
+ export interface UseFormulaBarResult {
25
+ /** Cell reference string (e.g. "A1"). */
26
+ cellRef: Ref<string | null>;
27
+ /** Text shown in the formula bar input. */
28
+ formulaText: Ref<string>;
29
+ /** Whether the formula bar input is being edited. */
30
+ isEditing: Ref<boolean>;
31
+ /** Update the formula bar input text. */
32
+ onInputChange: (text: string) => void;
33
+ /** Commit the current edit. */
34
+ onCommit: () => void;
35
+ /** Cancel the current edit. */
36
+ onCancel: () => void;
37
+ /** Start editing the formula bar. */
38
+ startEditing: () => void;
39
+ /** References extracted from the current formula text (for highlighting). */
40
+ referencedCells: Ref<FormulaReference[]>;
41
+ /** Whether the formula bar is actively being edited (for click-to-insert-ref guards). */
42
+ isFormulaBarEditing: Ref<boolean>;
43
+ }
44
+ export declare function useFormulaBar(params: UseFormulaBarParams): UseFormulaBarResult;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * useFormulaEngine — Vue composable for integrating the formula engine with the grid.
3
+ *
4
+ * Lazily creates a FormulaEngine instance when `formulas` ref is true.
5
+ * Provides accessor bridge between grid data and formula coordinates.
6
+ * Tree-shakeable: if `formulas` is false, no formula code is loaded.
7
+ */
8
+ import { type Ref } from 'vue';
9
+ import { type IGridDataAccessor, type IFormulaFunction, type IRecalcResult, type IColumnDef, type IAuditEntry, type IAuditTrail } from '@alaarab/ogrid-core';
10
+ export interface UseFormulaEngineParams<T> {
11
+ /** Enable formula support. */
12
+ formulas?: Ref<boolean>;
13
+ /** Grid data items. */
14
+ items: Ref<T[]>;
15
+ /** Flat leaf columns (for mapping column index <-> columnId). */
16
+ flatColumns: Ref<IColumnDef<T>[]>;
17
+ /** Initial formulas to load. */
18
+ initialFormulas?: Array<{
19
+ col: number;
20
+ row: number;
21
+ formula: string;
22
+ }>;
23
+ /** Called when recalculation produces cascading updates. */
24
+ onFormulaRecalc?: (result: IRecalcResult) => void;
25
+ /** Custom formula functions. */
26
+ formulaFunctions?: Record<string, IFormulaFunction>;
27
+ /** Named ranges: name → cell/range reference string. */
28
+ namedRanges?: Record<string, string>;
29
+ /** Sheet accessors for cross-sheet references. */
30
+ sheets?: Record<string, IGridDataAccessor>;
31
+ }
32
+ export interface UseFormulaEngineResult {
33
+ /** Whether formula support is active. */
34
+ enabled: Ref<boolean>;
35
+ /** Get the formula engine's computed value for a cell coordinate. */
36
+ getFormulaValue: (col: number, row: number) => unknown;
37
+ /** Check if a cell has a formula. */
38
+ hasFormula: (col: number, row: number) => boolean;
39
+ /** Get the formula string for a cell. */
40
+ getFormula: (col: number, row: number) => string | undefined;
41
+ /** Set or clear a formula for a cell. Triggers recalculation. */
42
+ setFormula: (col: number, row: number, formula: string | null) => void;
43
+ /** Notify the engine that a non-formula cell value changed. Triggers dependent recalc. */
44
+ onCellChanged: (col: number, row: number) => void;
45
+ /** Get all cells that a cell depends on (deep, transitive). */
46
+ getPrecedents: (col: number, row: number) => IAuditEntry[];
47
+ /** Get all cells that depend on a cell (deep, transitive). */
48
+ getDependents: (col: number, row: number) => IAuditEntry[];
49
+ /** Get full audit trail for a cell. */
50
+ getAuditTrail: (col: number, row: number) => IAuditTrail | null;
51
+ }
52
+ export declare function useFormulaEngine<T>(params: UseFormulaEngineParams<T>): UseFormulaEngineResult;
@@ -31,6 +31,10 @@ export interface UseOGridLayout {
31
31
  };
32
32
  sideBarProps: SideBarProps | null;
33
33
  fullScreen?: boolean;
34
+ /** Formula bar element (rendered between toolbar and grid). */
35
+ formulaBar?: unknown;
36
+ /** Sheet tabs element (rendered between grid and footer). */
37
+ sheetTabs?: unknown;
34
38
  }
35
39
  /** Filter state. */
36
40
  export interface UseOGridFilters {
@@ -4,8 +4,11 @@ export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnGroupDef,
4
4
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types';
5
5
  export { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
6
6
  export { StatusBar, type StatusBarProps } from './components/StatusBar';
7
- export { useOGrid, useDataGridState, useActiveCell, useCellEditing, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useFillHandle, useUndoRedo, useContextMenu, useColumnResize, useColumnReorder, useVirtualScroll, useFilterOptions, useDebounce, useDebouncedCallback, useTableLayout, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useRichSelectState, useSideBarState, useColumnPinning, useColumnHeaderMenuState, useDataGridTableSetup, } from './composables';
8
- export type { UseOGridResult, UseOGridPagination, UseOGridColumnChooser, UseOGridLayout, UseOGridFilters, ColumnChooserPlacement, UseDataGridStateParams, UseDataGridStateResult, DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, DataGridPinningState, UseActiveCellResult, EditingCell, UseCellEditingParams, UseCellEditingResult, UseCellSelectionParams, UseCellSelectionResult, UseClipboardParams, UseClipboardResult, UseRowSelectionParams, UseRowSelectionResult, UseKeyboardNavigationParams, UseKeyboardNavigationResult, UseFillHandleParams, UseFillHandleResult, UseUndoRedoParams, UseUndoRedoResult, ContextMenuPosition, UseContextMenuResult, UseColumnResizeParams, UseColumnResizeResult, UseColumnReorderParams, UseColumnReorderResult, UseVirtualScrollParams, UseVirtualScrollResult, UseFilterOptionsResult, UseTableLayoutParams, UseTableLayoutResult, UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, UseTextFilterStateParams, UseTextFilterStateResult, UseMultiSelectFilterStateParams, UseMultiSelectFilterStateResult, UsePeopleFilterStateParams, UsePeopleFilterStateResult, UseDateFilterStateParams, UseDateFilterStateResult, UseColumnChooserStateParams, UseColumnChooserStateResult, InlineCellEditorType, UseInlineCellEditorStateParams, UseInlineCellEditorStateResult, UseRichSelectStateParams, UseRichSelectStateResult, UseSideBarStateParams, UseSideBarStateResult, DebouncedFn, UseColumnPinningParams, UseColumnPinningResult, UseColumnHeaderMenuStateParams, UseColumnHeaderMenuStateResult, UseDataGridTableSetupParams, UseDataGridTableSetupResult, MaybeShallowRef, } from './composables';
7
+ export { FormulaBar, type FormulaBarProps } from './components/FormulaBar';
8
+ export { SheetTabs, type SheetTabsProps } from './components/SheetTabs';
9
+ export { FormulaRefOverlay } from './components/FormulaRefOverlay';
10
+ export { useOGrid, useDataGridState, useActiveCell, useCellEditing, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useFillHandle, useUndoRedo, useContextMenu, useColumnResize, useColumnReorder, useVirtualScroll, useFilterOptions, useDebounce, useDebouncedCallback, useTableLayout, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useRichSelectState, useSideBarState, useColumnPinning, useColumnHeaderMenuState, useDataGridTableSetup, useFormulaBar, } from './composables';
11
+ export type { UseOGridResult, UseOGridPagination, UseOGridColumnChooser, UseOGridLayout, UseOGridFilters, ColumnChooserPlacement, UseDataGridStateParams, UseDataGridStateResult, DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, DataGridPinningState, UseActiveCellResult, EditingCell, UseCellEditingParams, UseCellEditingResult, UseCellSelectionParams, UseCellSelectionResult, UseClipboardParams, UseClipboardResult, UseRowSelectionParams, UseRowSelectionResult, UseKeyboardNavigationParams, UseKeyboardNavigationResult, UseFillHandleParams, UseFillHandleResult, UseUndoRedoParams, UseUndoRedoResult, ContextMenuPosition, UseContextMenuResult, UseColumnResizeParams, UseColumnResizeResult, UseColumnReorderParams, UseColumnReorderResult, UseVirtualScrollParams, UseVirtualScrollResult, UseFilterOptionsResult, UseTableLayoutParams, UseTableLayoutResult, UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, UseTextFilterStateParams, UseTextFilterStateResult, UseMultiSelectFilterStateParams, UseMultiSelectFilterStateResult, UsePeopleFilterStateParams, UsePeopleFilterStateResult, UseDateFilterStateParams, UseDateFilterStateResult, UseColumnChooserStateParams, UseColumnChooserStateResult, InlineCellEditorType, UseInlineCellEditorStateParams, UseInlineCellEditorStateResult, UseRichSelectStateParams, UseRichSelectStateResult, UseSideBarStateParams, UseSideBarStateResult, DebouncedFn, UseColumnPinningParams, UseColumnPinningResult, UseColumnHeaderMenuStateParams, UseColumnHeaderMenuStateResult, UseDataGridTableSetupParams, UseDataGridTableSetupResult, MaybeShallowRef, UseFormulaBarParams, UseFormulaBarResult, } from './composables';
9
12
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './utils';
10
13
  export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, CellInteractionHandlers, CellInteractionProps, } from './utils';
11
14
  export { createDataGridTable, type IDataGridTableUIBindings } from './components/createDataGridTable';
@@ -1,7 +1,7 @@
1
1
  import type { IColumnDef, IColumnGroupDef, ICellValueChangedEvent } from './columnTypes';
2
- export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IOGridApi, IVirtualScrollConfig, } from '@alaarab/ogrid-core';
2
+ export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IOGridApi, IVirtualScrollConfig, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail, ISheetDef, FormulaReference, } from '@alaarab/ogrid-core';
3
3
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from '@alaarab/ogrid-core';
4
- import type { RowId, UserLike, IFilters, FilterValue, RowSelectionMode, IRowSelectionChangeEvent, IStatusBarProps, IDataSource, ISideBarDef, IVirtualScrollConfig } from '@alaarab/ogrid-core';
4
+ import type { RowId, UserLike, IFilters, FilterValue, RowSelectionMode, IRowSelectionChangeEvent, IStatusBarProps, IDataSource, ISideBarDef, IVirtualScrollConfig, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail, ISheetDef, FormulaReference } from '@alaarab/ogrid-core';
5
5
  /** Base props shared by both client-side and server-side OGrid modes. */
6
6
  interface IOGridBaseProps<T> {
7
7
  columns: (IColumnDef<T> | IColumnGroupDef<T>)[];
@@ -42,6 +42,8 @@ interface IOGridBaseProps<T> {
42
42
  onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
43
43
  /** Show Excel-style row numbers column at the start of the grid (1, 2, 3...). Default: false. */
44
44
  showRowNumbers?: boolean;
45
+ /** Enable Excel-style cell references: column letter headers, row numbers, and name box. Implies showRowNumbers. */
46
+ cellReferences?: boolean;
45
47
  statusBar?: boolean | IStatusBarProps;
46
48
  defaultPageSize?: number;
47
49
  defaultSortBy?: string;
@@ -87,6 +89,30 @@ interface IOGridBaseProps<T> {
87
89
  rowHeight?: number;
88
90
  /** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
89
91
  density?: 'compact' | 'normal' | 'comfortable';
92
+ /** Enable Excel-like formula support. When true, cells starting with '=' are treated as formulas. Default: false. */
93
+ formulas?: boolean;
94
+ /** Initial formulas to load when the formula engine initializes. */
95
+ initialFormulas?: Array<{
96
+ col: number;
97
+ row: number;
98
+ formula: string;
99
+ }>;
100
+ /** Called when formula recalculation produces updated cell values (e.g. cascade from an edited cell). */
101
+ onFormulaRecalc?: (result: IRecalcResult) => void;
102
+ /** Custom formula functions to register with the formula engine (e.g. { MYFUNC: { minArgs: 1, maxArgs: 1, evaluate: ... } }). */
103
+ formulaFunctions?: Record<string, IFormulaFunction>;
104
+ /** Named ranges for the formula engine: name → cell/range ref string (e.g. { Revenue: 'A1:A10' }). */
105
+ namedRanges?: Record<string, string>;
106
+ /** Sheet accessors for cross-sheet formula references (e.g. { Sheet2: accessor }). */
107
+ sheets?: Record<string, IGridDataAccessor>;
108
+ /** Sheet definitions for the tab bar at grid bottom. */
109
+ sheetDefs?: ISheetDef[];
110
+ /** Active sheet ID (controlled). */
111
+ activeSheet?: string;
112
+ /** Called when user clicks a sheet tab. */
113
+ onSheetChange?: (sheetId: string) => void;
114
+ /** Called when user clicks the "+" add sheet button. */
115
+ onSheetAdd?: () => void;
90
116
  'aria-label'?: string;
91
117
  'aria-labelledby'?: string;
92
118
  }
@@ -144,6 +170,10 @@ export interface IOGridDataGridProps<T> {
144
170
  selectedRows?: Set<RowId>;
145
171
  onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
146
172
  showRowNumbers?: boolean;
173
+ showColumnLetters?: boolean;
174
+ showNameBox?: boolean;
175
+ /** Callback when the active cell changes. Used by the name box to display the current cell reference. */
176
+ onActiveCellChange?: (ref: string | null) => void;
147
177
  currentPage?: number;
148
178
  pageSize?: number;
149
179
  statusBar?: IStatusBarProps;
@@ -175,4 +205,26 @@ export interface IOGridDataGridProps<T> {
175
205
  'aria-labelledby'?: string;
176
206
  /** Custom keydown handler. Called before grid's built-in handling. Call event.preventDefault() to suppress grid default. */
177
207
  onKeyDown?: (event: KeyboardEvent) => void;
208
+ /** Enable formula support. When true, cell values starting with '=' are treated as formulas. */
209
+ formulas?: boolean;
210
+ /** Get the formula engine's computed value for a cell, or undefined if no formula. */
211
+ getFormulaValue?: (col: number, row: number) => unknown;
212
+ /** Check if a cell has a formula. */
213
+ hasFormula?: (col: number, row: number) => boolean;
214
+ /** Get the formula string for a cell. */
215
+ getFormula?: (col: number, row: number) => string | undefined;
216
+ /** Set a formula for a cell (called from edit commit when value starts with '='). */
217
+ setFormula?: (col: number, row: number, formula: string | null) => void;
218
+ /** Notify the formula engine that a non-formula cell changed. */
219
+ onFormulaCellChanged?: (col: number, row: number) => void;
220
+ /** Get all cells that a cell depends on (deep, transitive). */
221
+ getPrecedents?: (col: number, row: number) => IAuditEntry[];
222
+ /** Get all cells that depend on a cell (deep, transitive). */
223
+ getDependents?: (col: number, row: number) => IAuditEntry[];
224
+ /** Get full audit trail for a cell. */
225
+ getAuditTrail?: (col: number, row: number) => IAuditTrail | null;
226
+ /** Monotonic counter incremented on each formula recalculation — used for cache invalidation. */
227
+ formulaVersion?: number;
228
+ /** Cell references to highlight (from active formula in formula bar). */
229
+ formulaReferences?: FormulaReference[];
178
230
  }
@@ -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, } 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, ISheetDef, FormulaReference, } from './dataGridTypes';
3
3
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './dataGridTypes';
@@ -26,6 +26,7 @@ export interface CellInteractionHandlers {
26
26
  export interface CellInteractionProps {
27
27
  'data-row-index': number;
28
28
  'data-col-index': number;
29
+ 'data-active-cell'?: 'true';
29
30
  'data-in-range'?: 'true';
30
31
  tabindex: number;
31
32
  role?: 'button';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-vue",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "OGrid Vue – Vue 3 composables, headless components, and utilities for OGrid data grids.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -36,7 +36,7 @@
36
36
  "node": ">=18"
37
37
  },
38
38
  "dependencies": {
39
- "@alaarab/ogrid-core": "2.2.0"
39
+ "@alaarab/ogrid-core": "2.4.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "vue": "^3.3.0"