@papernote/ui 1.6.0 → 1.7.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.
@@ -0,0 +1,417 @@
1
+ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import {
4
+ FORMULA_DEFINITIONS,
5
+ searchFormulas,
6
+ getFormula,
7
+ FORMULA_CATEGORIES,
8
+ FormulaDefinition,
9
+ FormulaCategory,
10
+ } from '../utils/formulaDefinitions';
11
+
12
+ export interface FormulaAutocompleteProps {
13
+ /** Current input value */
14
+ value: string;
15
+ /** Callback when value changes */
16
+ onChange: (value: string) => void;
17
+ /** Callback when editing is complete */
18
+ onComplete: () => void;
19
+ /** Callback to cancel editing */
20
+ onCancel: () => void;
21
+ /** Position for the dropdown */
22
+ anchorRect: DOMRect | null;
23
+ /** Auto focus the input */
24
+ autoFocus?: boolean;
25
+ /** Custom class name */
26
+ className?: string;
27
+ }
28
+
29
+ interface FormulaHint {
30
+ formula: FormulaDefinition;
31
+ currentParamIndex: number;
32
+ }
33
+
34
+ /**
35
+ * FormulaAutocomplete - Input with formula intellisense
36
+ *
37
+ * Features:
38
+ * - Autocomplete dropdown when typing after '='
39
+ * - Function signature hints while typing parameters
40
+ * - Category-based browsing
41
+ * - Keyboard navigation
42
+ */
43
+ const FormulaAutocomplete: React.FC<FormulaAutocompleteProps> = ({
44
+ value,
45
+ onChange,
46
+ onComplete,
47
+ onCancel,
48
+ anchorRect,
49
+ autoFocus = true,
50
+ className = '',
51
+ }) => {
52
+ const inputRef = useRef<HTMLInputElement>(null);
53
+ const dropdownRef = useRef<HTMLDivElement>(null);
54
+ const [selectedIndex, setSelectedIndex] = useState(0);
55
+ const [showDropdown, setShowDropdown] = useState(false);
56
+ const [showHint, setShowHint] = useState(false);
57
+ const [hint, setHint] = useState<FormulaHint | null>(null);
58
+ const [activeCategory, setActiveCategory] = useState<FormulaCategory | null>(null);
59
+
60
+ // Parse the current formula context
61
+ const formulaContext = useMemo(() => {
62
+ if (!value.startsWith('=')) {
63
+ return { isFormula: false, query: '', inFunction: false, functionName: '', paramIndex: 0 };
64
+ }
65
+
66
+ const formulaText = value.substring(1);
67
+
68
+ // Check if we're typing a function name (before opening paren)
69
+ const functionMatch = formulaText.match(/^([A-Z]+)$/i);
70
+ if (functionMatch) {
71
+ return {
72
+ isFormula: true,
73
+ query: functionMatch[1].toUpperCase(),
74
+ inFunction: false,
75
+ functionName: '',
76
+ paramIndex: 0,
77
+ };
78
+ }
79
+
80
+ // Check if we're inside a function (after opening paren)
81
+ const insideFunctionMatch = formulaText.match(/^([A-Z]+)\((.*)$/i);
82
+ if (insideFunctionMatch) {
83
+ const functionName = insideFunctionMatch[1].toUpperCase();
84
+ const params = insideFunctionMatch[2];
85
+
86
+ // Count commas to determine parameter index (accounting for nested parens)
87
+ let paramIndex = 0;
88
+ let parenDepth = 0;
89
+ for (const char of params) {
90
+ if (char === '(') parenDepth++;
91
+ else if (char === ')') parenDepth--;
92
+ else if (char === ',' && parenDepth === 0) paramIndex++;
93
+ }
94
+
95
+ return {
96
+ isFormula: true,
97
+ query: '',
98
+ inFunction: true,
99
+ functionName,
100
+ paramIndex,
101
+ };
102
+ }
103
+
104
+ // Just '=' or other expression
105
+ return {
106
+ isFormula: true,
107
+ query: formulaText.toUpperCase(),
108
+ inFunction: false,
109
+ functionName: '',
110
+ paramIndex: 0,
111
+ };
112
+ }, [value]);
113
+
114
+ // Get matching formulas for dropdown
115
+ const matchingFormulas = useMemo(() => {
116
+ if (!formulaContext.isFormula || formulaContext.inFunction) return [];
117
+
118
+ if (activeCategory) {
119
+ const categoryFormulas = FORMULA_DEFINITIONS.filter(f => f.category === activeCategory);
120
+ if (formulaContext.query) {
121
+ return categoryFormulas.filter(f => f.name.startsWith(formulaContext.query));
122
+ }
123
+ return categoryFormulas;
124
+ }
125
+
126
+ if (formulaContext.query) {
127
+ return searchFormulas(formulaContext.query);
128
+ }
129
+
130
+ // Show all formulas grouped by first letter when just '='
131
+ return FORMULA_DEFINITIONS.slice(0, 20); // Show first 20 as default
132
+ }, [formulaContext, activeCategory]);
133
+
134
+ // Update hint when inside a function
135
+ useEffect(() => {
136
+ if (formulaContext.inFunction && formulaContext.functionName) {
137
+ const formula = getFormula(formulaContext.functionName);
138
+ if (formula) {
139
+ setHint({
140
+ formula,
141
+ currentParamIndex: formulaContext.paramIndex,
142
+ });
143
+ setShowHint(true);
144
+ setShowDropdown(false);
145
+ } else {
146
+ setShowHint(false);
147
+ setHint(null);
148
+ }
149
+ } else if (formulaContext.isFormula && !formulaContext.inFunction) {
150
+ setShowDropdown(true);
151
+ setShowHint(false);
152
+ setHint(null);
153
+ } else {
154
+ setShowDropdown(false);
155
+ setShowHint(false);
156
+ setHint(null);
157
+ }
158
+ }, [formulaContext]);
159
+
160
+ // Reset selected index when matches change
161
+ useEffect(() => {
162
+ setSelectedIndex(0);
163
+ }, [matchingFormulas.length]);
164
+
165
+ // Auto-focus
166
+ useEffect(() => {
167
+ if (autoFocus && inputRef.current) {
168
+ inputRef.current.focus();
169
+ inputRef.current.select();
170
+ }
171
+ }, [autoFocus]);
172
+
173
+ // Scroll selected item into view
174
+ useEffect(() => {
175
+ if (dropdownRef.current && showDropdown) {
176
+ const selectedItem = dropdownRef.current.querySelector(`[data-index="${selectedIndex}"]`);
177
+ if (selectedItem) {
178
+ selectedItem.scrollIntoView({ block: 'nearest' });
179
+ }
180
+ }
181
+ }, [selectedIndex, showDropdown]);
182
+
183
+ // Handle keyboard navigation
184
+ const handleKeyDown = useCallback(
185
+ (e: React.KeyboardEvent) => {
186
+ if (showDropdown && matchingFormulas.length > 0) {
187
+ switch (e.key) {
188
+ case 'ArrowDown':
189
+ e.preventDefault();
190
+ setSelectedIndex((prev) => Math.min(prev + 1, matchingFormulas.length - 1));
191
+ break;
192
+ case 'ArrowUp':
193
+ e.preventDefault();
194
+ setSelectedIndex((prev) => Math.max(prev - 1, 0));
195
+ break;
196
+ case 'Tab':
197
+ case 'Enter':
198
+ e.preventDefault();
199
+ insertFormula(matchingFormulas[selectedIndex]);
200
+ break;
201
+ case 'Escape':
202
+ e.preventDefault();
203
+ if (showDropdown) {
204
+ setShowDropdown(false);
205
+ } else {
206
+ onCancel();
207
+ }
208
+ break;
209
+ }
210
+ } else {
211
+ if (e.key === 'Enter') {
212
+ e.preventDefault();
213
+ onComplete();
214
+ } else if (e.key === 'Escape') {
215
+ e.preventDefault();
216
+ onCancel();
217
+ }
218
+ }
219
+ },
220
+ [showDropdown, matchingFormulas, selectedIndex, onComplete, onCancel]
221
+ );
222
+
223
+ // Insert selected formula
224
+ const insertFormula = useCallback(
225
+ (formula: FormulaDefinition) => {
226
+ // Replace the current query with the formula name and open paren
227
+ const newValue = `=${formula.name}(`;
228
+ onChange(newValue);
229
+ setShowDropdown(false);
230
+ inputRef.current?.focus();
231
+ },
232
+ [onChange]
233
+ );
234
+
235
+ // Calculate dropdown position
236
+ const dropdownPosition = useMemo(() => {
237
+ if (!anchorRect) return { top: 0, left: 0 };
238
+ return {
239
+ top: anchorRect.bottom + 2,
240
+ left: anchorRect.left,
241
+ };
242
+ }, [anchorRect]);
243
+
244
+ // Prevent blur when clicking inside dropdown
245
+ const handleDropdownMouseDown = useCallback((e: React.MouseEvent) => {
246
+ e.preventDefault(); // Prevents input from losing focus
247
+ }, []);
248
+
249
+ // Render parameter hint
250
+ const renderHint = () => {
251
+ if (!showHint || !hint) return null;
252
+
253
+ return createPortal(
254
+ <div
255
+ className="fixed z-[9999] bg-white border border-stone-200 rounded-lg shadow-lg p-3 max-w-md"
256
+ style={{
257
+ top: dropdownPosition.top,
258
+ left: dropdownPosition.left,
259
+ }}
260
+ onMouseDown={handleDropdownMouseDown}
261
+ >
262
+ {/* Function name and syntax */}
263
+ <div className="font-mono text-sm mb-2">
264
+ <span className="text-primary-600 font-semibold">{hint.formula.name}</span>
265
+ <span className="text-ink-500">(</span>
266
+ {hint.formula.parameters.map((param, idx) => (
267
+ <span key={param.name}>
268
+ {idx > 0 && <span className="text-ink-500">, </span>}
269
+ <span
270
+ className={`${
271
+ idx === hint.currentParamIndex
272
+ ? 'bg-primary-100 text-primary-700 px-1 rounded font-semibold'
273
+ : param.optional
274
+ ? 'text-ink-400'
275
+ : 'text-ink-600'
276
+ }`}
277
+ >
278
+ {param.optional ? `[${param.name}]` : param.name}
279
+ </span>
280
+ </span>
281
+ ))}
282
+ <span className="text-ink-500">)</span>
283
+ </div>
284
+
285
+ {/* Description */}
286
+ <div className="text-xs text-ink-600 mb-2">{hint.formula.description}</div>
287
+
288
+ {/* Current parameter description */}
289
+ {hint.formula.parameters[hint.currentParamIndex] && (
290
+ <div className="text-xs bg-paper-50 p-2 rounded border border-stone-100">
291
+ <span className="font-semibold text-primary-600">
292
+ {hint.formula.parameters[hint.currentParamIndex].name}:
293
+ </span>{' '}
294
+ <span className="text-ink-600">
295
+ {hint.formula.parameters[hint.currentParamIndex].description}
296
+ </span>
297
+ </div>
298
+ )}
299
+ </div>,
300
+ document.body
301
+ );
302
+ };
303
+
304
+ // Render autocomplete dropdown
305
+ const renderDropdown = () => {
306
+ if (!showDropdown || matchingFormulas.length === 0) return null;
307
+
308
+ return createPortal(
309
+ <div
310
+ ref={dropdownRef}
311
+ className="fixed z-[9999] bg-white border border-stone-200 rounded-lg shadow-lg overflow-hidden"
312
+ style={{
313
+ top: dropdownPosition.top,
314
+ left: dropdownPosition.left,
315
+ minWidth: 320,
316
+ maxWidth: 450,
317
+ maxHeight: 300,
318
+ }}
319
+ onMouseDown={handleDropdownMouseDown}
320
+ >
321
+ {/* Category tabs */}
322
+ <div className="flex flex-wrap gap-1 p-2 border-b border-stone-100 bg-paper-50">
323
+ <button
324
+ className={`px-2 py-1 text-xs rounded ${
325
+ activeCategory === null
326
+ ? 'bg-primary-500 text-white'
327
+ : 'bg-white text-ink-600 hover:bg-stone-100'
328
+ }`}
329
+ onClick={() => {
330
+ setActiveCategory(null);
331
+ inputRef.current?.focus();
332
+ }}
333
+ >
334
+ All
335
+ </button>
336
+ {FORMULA_CATEGORIES.map((cat) => (
337
+ <button
338
+ key={cat}
339
+ className={`px-2 py-1 text-xs rounded ${
340
+ activeCategory === cat
341
+ ? 'bg-primary-500 text-white'
342
+ : 'bg-white text-ink-600 hover:bg-stone-100'
343
+ }`}
344
+ onClick={() => {
345
+ setActiveCategory(cat);
346
+ inputRef.current?.focus();
347
+ }}
348
+ >
349
+ {cat}
350
+ </button>
351
+ ))}
352
+ </div>
353
+
354
+ {/* Formula list */}
355
+ <div className="overflow-y-auto" style={{ maxHeight: 220 }}>
356
+ {matchingFormulas.map((formula, index) => (
357
+ <div
358
+ key={formula.name}
359
+ data-index={index}
360
+ className={`px-3 py-2 cursor-pointer border-b border-stone-50 ${
361
+ index === selectedIndex ? 'bg-primary-50' : 'hover:bg-paper-50'
362
+ }`}
363
+ onClick={() => insertFormula(formula)}
364
+ onMouseEnter={() => setSelectedIndex(index)}
365
+ >
366
+ <div className="flex items-center gap-2">
367
+ <span className="font-mono font-semibold text-primary-600 text-sm">
368
+ {formula.name}
369
+ </span>
370
+ <span className="text-xs text-ink-400 bg-stone-100 px-1.5 py-0.5 rounded">
371
+ {formula.category}
372
+ </span>
373
+ </div>
374
+ <div className="text-xs text-ink-500 mt-0.5 truncate">{formula.description}</div>
375
+ <div className="font-mono text-xs text-ink-400 mt-0.5">{formula.syntax}</div>
376
+ </div>
377
+ ))}
378
+ </div>
379
+
380
+ {/* Footer hint */}
381
+ <div className="px-3 py-1.5 bg-paper-50 border-t border-stone-100 text-xs text-ink-400">
382
+ <span className="font-medium">↑↓</span> navigate
383
+ <span className="mx-2">·</span>
384
+ <span className="font-medium">Tab/Enter</span> insert
385
+ <span className="mx-2">·</span>
386
+ <span className="font-medium">Esc</span> close
387
+ </div>
388
+ </div>,
389
+ document.body
390
+ );
391
+ };
392
+
393
+ return (
394
+ <>
395
+ <input
396
+ ref={inputRef}
397
+ type="text"
398
+ value={value}
399
+ onChange={(e) => onChange(e.target.value)}
400
+ onKeyDown={handleKeyDown}
401
+ onBlur={() => {
402
+ // Delay to allow click on dropdown
403
+ setTimeout(() => {
404
+ setShowDropdown(false);
405
+ setShowHint(false);
406
+ }, 150);
407
+ }}
408
+ className={`w-full h-full border-none outline-none bg-transparent font-mono text-sm ${className}`}
409
+ style={{ margin: '-4px', padding: '4px' }}
410
+ />
411
+ {renderDropdown()}
412
+ {renderHint()}
413
+ </>
414
+ );
415
+ };
416
+
417
+ export default FormulaAutocomplete;
@@ -83,6 +83,8 @@ export interface SelectProps {
83
83
  size?: 'sm' | 'md' | 'lg';
84
84
  /** Mobile display mode - 'auto' uses BottomSheet on mobile, 'dropdown' always uses dropdown, 'native' uses native select on mobile */
85
85
  mobileMode?: 'auto' | 'dropdown' | 'native';
86
+ /** Render dropdown via portal (default: true). Set to false when overflow clipping is not an issue */
87
+ usePortal?: boolean;
86
88
  }
87
89
 
88
90
  // Size classes for trigger button
@@ -191,13 +193,16 @@ const Select = forwardRef<SelectHandle, SelectProps>(
191
193
  virtualItemHeight = 42,
192
194
  size = 'md',
193
195
  mobileMode = 'auto',
196
+ usePortal = true,
194
197
  } = props;
195
198
  const [isOpen, setIsOpen] = useState(false);
196
199
  const [searchQuery, setSearchQuery] = useState('');
197
200
  const [scrollTop, setScrollTop] = useState(0);
198
201
  const [activeDescendant] = useState<string | undefined>(undefined);
202
+ const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number; width: number; placement: 'bottom' | 'top' } | null>(null);
199
203
  const selectRef = useRef<HTMLDivElement>(null);
200
204
  const buttonRef = useRef<HTMLButtonElement>(null);
205
+ const dropdownRef = useRef<HTMLDivElement>(null);
201
206
  const searchInputRef = useRef<HTMLInputElement>(null);
202
207
  const mobileSearchInputRef = useRef<HTMLInputElement>(null);
203
208
  const listRef = useRef<HTMLDivElement>(null);
@@ -313,9 +318,14 @@ const Select = forwardRef<SelectHandle, SelectProps>(
313
318
  // Handle click outside (desktop dropdown only)
314
319
  useEffect(() => {
315
320
  if (useMobileSheet) return; // Mobile sheet handles its own closing
316
-
321
+
317
322
  const handleClickOutside = (event: MouseEvent) => {
318
- if (selectRef.current && !selectRef.current.contains(event.target as Node)) {
323
+ const target = event.target as Node;
324
+ // Check if click is outside both the select trigger and the dropdown portal
325
+ const isOutsideSelect = selectRef.current && !selectRef.current.contains(target);
326
+ const isOutsideDropdown = dropdownRef.current && !dropdownRef.current.contains(target);
327
+
328
+ if (isOutsideSelect && isOutsideDropdown) {
319
329
  setIsOpen(false);
320
330
  setSearchQuery('');
321
331
  }
@@ -342,6 +352,55 @@ const Select = forwardRef<SelectHandle, SelectProps>(
342
352
  }
343
353
  }, [isOpen, searchable, useMobileSheet]);
344
354
 
355
+ // Calculate dropdown position with collision detection and scroll/resize handling
356
+ useEffect(() => {
357
+ if (!isOpen || useMobileSheet || !usePortal) {
358
+ setDropdownPosition(null);
359
+ return;
360
+ }
361
+
362
+ const updatePosition = () => {
363
+ if (!buttonRef.current) return;
364
+
365
+ const rect = buttonRef.current.getBoundingClientRect();
366
+ const dropdownHeight = 240; // max-h-60 = 15rem = 240px
367
+ const gap = 2; // Small gap to visually connect to trigger
368
+ const viewportHeight = window.innerHeight;
369
+
370
+ // Check if there's enough space below
371
+ const spaceBelow = viewportHeight - rect.bottom;
372
+ const spaceAbove = rect.top;
373
+ const hasSpaceBelow = spaceBelow >= dropdownHeight + gap;
374
+ const hasSpaceAbove = spaceAbove >= dropdownHeight + gap;
375
+
376
+ // Prefer bottom placement, flip to top if not enough space below but enough above
377
+ const placement: 'bottom' | 'top' = hasSpaceBelow || !hasSpaceAbove ? 'bottom' : 'top';
378
+
379
+ const top = placement === 'bottom'
380
+ ? rect.bottom + gap
381
+ : rect.top - dropdownHeight - gap;
382
+
383
+ setDropdownPosition({
384
+ top,
385
+ left: rect.left,
386
+ width: rect.width,
387
+ placement,
388
+ });
389
+ };
390
+
391
+ // Initial position calculation
392
+ updatePosition();
393
+
394
+ // Listen for scroll events on all scrollable ancestors
395
+ window.addEventListener('scroll', updatePosition, true);
396
+ window.addEventListener('resize', updatePosition);
397
+
398
+ return () => {
399
+ window.removeEventListener('scroll', updatePosition, true);
400
+ window.removeEventListener('resize', updatePosition);
401
+ };
402
+ }, [isOpen, useMobileSheet, usePortal]);
403
+
345
404
  // Lock body scroll when mobile sheet is open
346
405
  useEffect(() => {
347
406
  if (useMobileSheet && isOpen) {
@@ -617,9 +676,64 @@ const Select = forwardRef<SelectHandle, SelectProps>(
617
676
  </div>
618
677
  </button>
619
678
 
620
- {/* Desktop Dropdown */}
621
- {isOpen && !useMobileSheet && (
622
- <div className="absolute z-50 w-full mt-2 bg-white bg-subtle-grain rounded-lg shadow-lg border border-paper-200 max-h-60 overflow-hidden animate-fade-in">
679
+ </div>
680
+
681
+ {/* Desktop Dropdown - rendered via portal to avoid overflow clipping */}
682
+ {isOpen && !useMobileSheet && (usePortal ? dropdownPosition : true) && (
683
+ usePortal ? createPortal(
684
+ <div
685
+ ref={dropdownRef}
686
+ className={`fixed z-[9999] bg-white bg-subtle-grain rounded-lg shadow-lg border border-paper-200 max-h-60 overflow-hidden animate-fade-in ${
687
+ dropdownPosition?.placement === 'top' ? 'origin-bottom' : 'origin-top'
688
+ }`}
689
+ style={{
690
+ top: dropdownPosition!.top,
691
+ left: dropdownPosition!.left,
692
+ width: dropdownPosition!.width,
693
+ }}
694
+ >
695
+ {/* Search Input */}
696
+ {searchable && (
697
+ <div className="p-2 border-b border-paper-200">
698
+ <div className="relative">
699
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-ink-400" />
700
+ <input
701
+ ref={searchInputRef}
702
+ type="text"
703
+ value={searchQuery}
704
+ onChange={(e) => setSearchQuery(e.target.value)}
705
+ placeholder="Search..."
706
+ className="w-full pl-9 pr-3 py-2 text-sm border border-paper-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-accent-400"
707
+ role="searchbox"
708
+ aria-label="Search options"
709
+ aria-autocomplete="list"
710
+ aria-controls={listboxId}
711
+ />
712
+ </div>
713
+ </div>
714
+ )}
715
+
716
+ {/* Options List */}
717
+ <div
718
+ ref={listRef}
719
+ id={listboxId}
720
+ className="overflow-y-auto"
721
+ style={{ maxHeight: useVirtualScrolling ? virtualHeight : '12rem' }}
722
+ onScroll={(e) => useVirtualScrolling && setScrollTop(e.currentTarget.scrollTop)}
723
+ role="listbox"
724
+ aria-label="Available options"
725
+ aria-multiselectable="false"
726
+ >
727
+ {renderOptionsContent(false)}
728
+ </div>
729
+ </div>,
730
+ document.body
731
+ ) : (
732
+ // Non-portal dropdown (inline, relative positioning)
733
+ <div
734
+ ref={dropdownRef}
735
+ className="absolute z-50 mt-1 w-full bg-white bg-subtle-grain rounded-lg shadow-lg border border-paper-200 max-h-60 overflow-hidden animate-fade-in"
736
+ >
623
737
  {/* Search Input */}
624
738
  {searchable && (
625
739
  <div className="p-2 border-b border-paper-200">
@@ -655,8 +769,8 @@ const Select = forwardRef<SelectHandle, SelectProps>(
655
769
  {renderOptionsContent(false)}
656
770
  </div>
657
771
  </div>
658
- )}
659
- </div>
772
+ )
773
+ )}
660
774
 
661
775
  {/* Mobile Bottom Sheet */}
662
776
  {isOpen && useMobileSheet && createPortal(
@@ -315,6 +315,17 @@ export type {
315
315
  export { default as DataTableCardView } from './DataTableCardView';
316
316
  export type { CardViewConfig, DataTableCardViewProps } from './DataTableCardView';
317
317
 
318
+ // DataGrid (Excel-like grid with formulas)
319
+ export { default as DataGrid } from './DataGrid';
320
+ export type {
321
+ DataGridProps,
322
+ DataGridHandle,
323
+ DataGridColumn,
324
+ DataGridCell,
325
+ CellValue,
326
+ FrozenRowMode,
327
+ } from './DataGrid';
328
+
318
329
  export { default as SwipeActions } from './SwipeActions';
319
330
  export type { SwipeActionsProps, SwipeAction } from './SwipeActions';
320
331
 
@@ -322,6 +333,10 @@ export type { SwipeActionsProps, SwipeAction } from './SwipeActions';
322
333
  export { Spreadsheet, SpreadsheetReport } from './Spreadsheet';
323
334
  export type { SpreadsheetProps, SpreadsheetCell, Matrix, CellBase } from './Spreadsheet';
324
335
 
336
+ // ExcelTable has been moved to a separate package: @papernote/excel-table
337
+ // This is due to Handsontable's commercial licensing requirements
338
+ // See: https://github.com/kwhittenberger/papernote-ui/tree/main/packages/excel-table
339
+
325
340
  export { default as ExpandedRowEditForm } from './ExpandedRowEditForm';
326
341
  export type {
327
342
  ExpandedRowEditFormProps,
@@ -448,6 +463,21 @@ export type { ColumnResize, ColumnOrder } from '../utils/tableEnhancements';
448
463
  export { exportToExcel, exportDataTableToExcel, createMultiSheetExcel } from '../utils/excelExport';
449
464
  export type { ExcelColumn, ExportToExcelOptions, DataTableExportOptions, MultiSheetExcelOptions } from '../utils/excelExport';
450
465
 
466
+ // Formula Definitions (for DataGrid intellisense)
467
+ export {
468
+ FORMULA_DEFINITIONS,
469
+ FORMULA_NAMES,
470
+ FORMULA_CATEGORIES,
471
+ getFormulasByCategory,
472
+ searchFormulas,
473
+ getFormula,
474
+ } from '../utils/formulaDefinitions';
475
+ export type {
476
+ FormulaDefinition,
477
+ FormulaParameter,
478
+ FormulaCategory,
479
+ } from '../utils/formulaDefinitions';
480
+
451
481
  // Hooks
452
482
  export { useColumnResize, useColumnReorder } from '../hooks/useTableEnhancements';
453
483
  export type { UseColumnResizeOptions, UseColumnReorderOptions } from '../hooks/useTableEnhancements';