@papernote/ui 1.5.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.
Files changed (37) hide show
  1. package/README.md +3 -3
  2. package/dist/components/ActionBar.d.ts +112 -0
  3. package/dist/components/ActionBar.d.ts.map +1 -0
  4. package/dist/components/DataGrid.d.ts +182 -0
  5. package/dist/components/DataGrid.d.ts.map +1 -0
  6. package/dist/components/FormulaAutocomplete.d.ts +29 -0
  7. package/dist/components/FormulaAutocomplete.d.ts.map +1 -0
  8. package/dist/components/Modal.d.ts +29 -1
  9. package/dist/components/Modal.d.ts.map +1 -1
  10. package/dist/components/PageHeader.d.ts +86 -0
  11. package/dist/components/PageHeader.d.ts.map +1 -0
  12. package/dist/components/Select.d.ts +2 -0
  13. package/dist/components/Select.d.ts.map +1 -1
  14. package/dist/components/index.d.ts +8 -0
  15. package/dist/components/index.d.ts.map +1 -1
  16. package/dist/index.d.ts +419 -3
  17. package/dist/index.esm.js +2533 -350
  18. package/dist/index.esm.js.map +1 -1
  19. package/dist/index.js +2543 -348
  20. package/dist/index.js.map +1 -1
  21. package/dist/styles.css +81 -0
  22. package/dist/utils/formulaDefinitions.d.ts +25 -0
  23. package/dist/utils/formulaDefinitions.d.ts.map +1 -0
  24. package/package.json +1 -1
  25. package/src/components/ActionBar.stories.tsx +246 -0
  26. package/src/components/ActionBar.tsx +242 -0
  27. package/src/components/DataGrid.stories.tsx +356 -0
  28. package/src/components/DataGrid.tsx +1025 -0
  29. package/src/components/FormulaAutocomplete.tsx +417 -0
  30. package/src/components/Modal.stories.tsx +205 -0
  31. package/src/components/Modal.tsx +38 -1
  32. package/src/components/PageHeader.stories.tsx +198 -0
  33. package/src/components/PageHeader.tsx +217 -0
  34. package/src/components/Select.tsx +121 -7
  35. package/src/components/Sidebar.tsx +2 -2
  36. package/src/components/index.ts +36 -0
  37. package/src/utils/formulaDefinitions.ts +1228 -0
package/dist/index.esm.js CHANGED
@@ -1,10 +1,25 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
3
  import React__default, { forwardRef, useState, useEffect, useCallback, useRef, useId, useImperativeHandle, useMemo, Children, isValidElement, cloneElement, Component, createContext as createContext$1, useLayoutEffect, createElement, useContext, useReducer } from 'react';
4
- import { Loader2, X, EyeOff, Eye, AlertTriangle, CheckCircle, AlertCircle, ChevronDown, Search, Check, Minus, Star, Calendar as Calendar$1, ChevronLeft, ChevronRight, Clock, ChevronUp, Plus, TrendingUp, TrendingDown, Info, Trash2, Circle, ChevronsRight, ChevronsLeft, MoreVertical, GripVertical, Upload, Bold, Italic, Underline, List, ListOrdered, Code, Link, Home, FileText, Image, File as File$1, Menu as Menu$1, ArrowDown, User, Settings, LogOut, Moon, Sun, Bell, Edit, Trash, Download, Save, XCircle, Filter, BarChart3, MessageSquare } from 'lucide-react';
4
+ import { Loader2, X, EyeOff, Eye, AlertTriangle, CheckCircle, AlertCircle, ChevronDown, Search, Check, Minus, Star, Calendar as Calendar$1, ChevronLeft, ChevronRight, Clock, ChevronUp, Plus, TrendingUp, TrendingDown, Info, Trash2, Circle, ChevronsRight, ChevronsLeft, MoreVertical, GripVertical, Upload, Bold, Italic, Underline, List, ListOrdered, Code, Link, Home, FileText, Image, File as File$1, Menu as Menu$1, ArrowDown, User, Settings, LogOut, Moon, Sun, Bell, Edit, Trash, Pin, PinOff, Download, Save, ArrowUpDown, Filter, XCircle, BarChart3, MessageSquare } from 'lucide-react';
5
5
  import { createPortal } from 'react-dom';
6
6
  import { Link as Link$1 } from 'react-router-dom';
7
7
 
8
+ function _mergeNamespaces(n, m) {
9
+ m.forEach(function (e) {
10
+ e && typeof e !== 'string' && !Array.isArray(e) && Object.keys(e).forEach(function (k) {
11
+ if (k !== 'default' && !(k in n)) {
12
+ var d = Object.getOwnPropertyDescriptor(e, k);
13
+ Object.defineProperty(n, k, d.get ? d : {
14
+ enumerable: true,
15
+ get: function () { return e[k]; }
16
+ });
17
+ }
18
+ });
19
+ });
20
+ return Object.freeze(n);
21
+ }
22
+
8
23
  /**
9
24
  * Button - Interactive button component with variants, sizes, and loading states
10
25
  *
@@ -745,13 +760,15 @@ const optionSizeClasses = {
745
760
  * ```
746
761
  */
747
762
  const Select = forwardRef((props, ref) => {
748
- const { options = [], groups = [], value, onChange, placeholder = 'Select an option', searchable = false, disabled = false, label, helperText, error, loading = false, clearable = false, creatable = false, onCreateOption, virtualized = false, virtualHeight = '300px', virtualItemHeight = 42, size = 'md', mobileMode = 'auto', } = props;
763
+ const { options = [], groups = [], value, onChange, placeholder = 'Select an option', searchable = false, disabled = false, label, helperText, error, loading = false, clearable = false, creatable = false, onCreateOption, virtualized = false, virtualHeight = '300px', virtualItemHeight = 42, size = 'md', mobileMode = 'auto', usePortal = true, } = props;
749
764
  const [isOpen, setIsOpen] = useState(false);
750
765
  const [searchQuery, setSearchQuery] = useState('');
751
766
  const [scrollTop, setScrollTop] = useState(0);
752
767
  const [activeDescendant] = useState(undefined);
768
+ const [dropdownPosition, setDropdownPosition] = useState(null);
753
769
  const selectRef = useRef(null);
754
770
  const buttonRef = useRef(null);
771
+ const dropdownRef = useRef(null);
755
772
  const searchInputRef = useRef(null);
756
773
  const mobileSearchInputRef = useRef(null);
757
774
  const listRef = useRef(null);
@@ -839,7 +856,11 @@ const Select = forwardRef((props, ref) => {
839
856
  if (useMobileSheet)
840
857
  return; // Mobile sheet handles its own closing
841
858
  const handleClickOutside = (event) => {
842
- if (selectRef.current && !selectRef.current.contains(event.target)) {
859
+ const target = event.target;
860
+ // Check if click is outside both the select trigger and the dropdown portal
861
+ const isOutsideSelect = selectRef.current && !selectRef.current.contains(target);
862
+ const isOutsideDropdown = dropdownRef.current && !dropdownRef.current.contains(target);
863
+ if (isOutsideSelect && isOutsideDropdown) {
843
864
  setIsOpen(false);
844
865
  setSearchQuery('');
845
866
  }
@@ -863,6 +884,46 @@ const Select = forwardRef((props, ref) => {
863
884
  }
864
885
  }
865
886
  }, [isOpen, searchable, useMobileSheet]);
887
+ // Calculate dropdown position with collision detection and scroll/resize handling
888
+ useEffect(() => {
889
+ if (!isOpen || useMobileSheet || !usePortal) {
890
+ setDropdownPosition(null);
891
+ return;
892
+ }
893
+ const updatePosition = () => {
894
+ if (!buttonRef.current)
895
+ return;
896
+ const rect = buttonRef.current.getBoundingClientRect();
897
+ const dropdownHeight = 240; // max-h-60 = 15rem = 240px
898
+ const gap = 2; // Small gap to visually connect to trigger
899
+ const viewportHeight = window.innerHeight;
900
+ // Check if there's enough space below
901
+ const spaceBelow = viewportHeight - rect.bottom;
902
+ const spaceAbove = rect.top;
903
+ const hasSpaceBelow = spaceBelow >= dropdownHeight + gap;
904
+ const hasSpaceAbove = spaceAbove >= dropdownHeight + gap;
905
+ // Prefer bottom placement, flip to top if not enough space below but enough above
906
+ const placement = hasSpaceBelow || !hasSpaceAbove ? 'bottom' : 'top';
907
+ const top = placement === 'bottom'
908
+ ? rect.bottom + gap
909
+ : rect.top - dropdownHeight - gap;
910
+ setDropdownPosition({
911
+ top,
912
+ left: rect.left,
913
+ width: rect.width,
914
+ placement,
915
+ });
916
+ };
917
+ // Initial position calculation
918
+ updatePosition();
919
+ // Listen for scroll events on all scrollable ancestors
920
+ window.addEventListener('scroll', updatePosition, true);
921
+ window.addEventListener('resize', updatePosition);
922
+ return () => {
923
+ window.removeEventListener('scroll', updatePosition, true);
924
+ window.removeEventListener('resize', updatePosition);
925
+ };
926
+ }, [isOpen, useMobileSheet, usePortal]);
866
927
  // Lock body scroll when mobile sheet is open
867
928
  useEffect(() => {
868
929
  if (useMobileSheet && isOpen) {
@@ -939,16 +1000,22 @@ const Select = forwardRef((props, ref) => {
939
1000
  ${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}
940
1001
  `, "aria-labelledby": label ? labelId : undefined, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : (helperText ? helperTextId : undefined), children: [jsx("option", { value: "", disabled: true, children: placeholder }), options.map((opt) => (jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))), groups.map((group) => (jsx("optgroup", { label: group.label, children: group.options.map((opt) => (jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))) }, group.label)))] }), jsx(ChevronDown, { className: "absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5 text-ink-500 pointer-events-none" })] }), error && (jsx("p", { id: errorId, className: "mt-2 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsx("p", { id: helperTextId, className: "mt-2 text-xs text-ink-600", children: helperText }))] }));
941
1002
  }
942
- return (jsxs("div", { className: "w-full", children: [label && (jsx("label", { id: labelId, className: "label", children: label })), jsxs("div", { ref: selectRef, className: "relative", children: [jsxs("button", { ref: buttonRef, type: "button", onClick: () => !disabled && setIsOpen(!isOpen), disabled: disabled, className: `
1003
+ return (jsxs("div", { className: "w-full", children: [label && (jsx("label", { id: labelId, className: "label", children: label })), jsx("div", { ref: selectRef, className: "relative", children: jsxs("button", { ref: buttonRef, type: "button", onClick: () => !disabled && setIsOpen(!isOpen), disabled: disabled, className: `
943
1004
  input w-full flex items-center justify-between px-3
944
1005
  ${sizeClasses$a[effectiveSize]}
945
1006
  ${error ? 'border-error-400 focus:border-error-400 focus:ring-error-400' : ''}
946
1007
  ${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}
947
1008
  `, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? placeholder : undefined, "aria-activedescendant": activeDescendant, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : (helperText ? helperTextId : undefined), "aria-disabled": disabled, children: [jsxs("span", { className: `flex items-center gap-2 ${selectedOption ? 'text-ink-800' : 'text-ink-400'}`, children: [loading && jsx(Loader2, { className: "h-4 w-4 animate-spin text-ink-500" }), !loading && selectedOption?.icon && jsx("span", { children: selectedOption.icon }), selectedOption ? selectedOption.label : placeholder] }), jsxs("div", { className: "flex items-center gap-1", children: [clearable && value && (jsx("button", { type: "button", onClick: (e) => {
948
- e.stopPropagation();
949
- onChange?.('');
950
- setIsOpen(false);
951
- }, className: "text-ink-400 hover:text-ink-600 transition-colors p-0.5", "aria-label": "Clear selection", children: jsx(X, { className: `${effectiveSize === 'lg' ? 'h-5 w-5' : 'h-4 w-4'}` }) })), jsx(ChevronDown, { className: `${effectiveSize === 'lg' ? 'h-5 w-5' : 'h-4 w-4'} text-ink-500 transition-transform ${isOpen ? 'rotate-180' : ''}` })] })] }), isOpen && !useMobileSheet && (jsxs("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", children: [searchable && (jsx("div", { className: "p-2 border-b border-paper-200", children: jsxs("div", { className: "relative", children: [jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-ink-400" }), jsx("input", { ref: searchInputRef, type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search...", 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", role: "searchbox", "aria-label": "Search options", "aria-autocomplete": "list", "aria-controls": listboxId })] }) })), jsx("div", { ref: listRef, id: listboxId, className: "overflow-y-auto", style: { maxHeight: useVirtualScrolling ? virtualHeight : '12rem' }, onScroll: (e) => useVirtualScrolling && setScrollTop(e.currentTarget.scrollTop), role: "listbox", "aria-label": "Available options", "aria-multiselectable": "false", children: renderOptionsContent(false) })] }))] }), isOpen && useMobileSheet && createPortal(jsxs("div", { className: "fixed inset-0 z-50 flex items-end", onClick: (e) => e.target === e.currentTarget && handleClose(), role: "dialog", "aria-modal": "true", "aria-labelledby": label ? `mobile-${labelId}` : undefined, children: [jsx("div", { className: "absolute inset-0 bg-black/50 animate-fade-in" }), jsxs("div", { className: "relative w-full bg-white rounded-t-2xl shadow-2xl animate-slide-up max-h-[85vh] flex flex-col", style: { paddingBottom: 'env(safe-area-inset-bottom)' }, children: [jsx("div", { className: "py-3 cursor-grab", children: jsx("div", { className: "w-12 h-1.5 bg-ink-300 rounded-full mx-auto" }) }), jsxs("div", { className: "px-4 pb-3 border-b border-paper-200 flex items-center justify-between", children: [label && (jsx("h2", { id: `mobile-${labelId}`, className: "text-lg font-semibold text-ink-900", children: label })), !label && (jsx("h2", { className: "text-lg font-semibold text-ink-900", children: placeholder })), jsx("button", { onClick: handleClose, className: "text-ink-400 hover:text-ink-600 transition-colors p-2 -mr-2", "aria-label": "Close", children: jsx(X, { className: "h-5 w-5" }) })] }), searchable && (jsx("div", { className: "p-3 border-b border-paper-200", children: jsxs("div", { className: "relative", children: [jsx(Search, { className: "absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-ink-400" }), jsx("input", { ref: mobileSearchInputRef, type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search...", inputMode: "search", enterKeyHint: "search", className: "w-full pl-12 pr-4 py-3 text-base border border-paper-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-accent-400", role: "searchbox", "aria-label": "Search options" })] }) })), jsx("div", { id: listboxId, className: "overflow-y-auto flex-1", role: "listbox", "aria-label": "Available options", "aria-multiselectable": "false", children: renderOptionsContent(true) })] })] }), document.body), error && (jsx("p", { id: errorId, className: "mt-2 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsx("p", { id: helperTextId, className: "mt-2 text-xs text-ink-600", children: helperText }))] }));
1009
+ e.stopPropagation();
1010
+ onChange?.('');
1011
+ setIsOpen(false);
1012
+ }, className: "text-ink-400 hover:text-ink-600 transition-colors p-0.5", "aria-label": "Clear selection", children: jsx(X, { className: `${effectiveSize === 'lg' ? 'h-5 w-5' : 'h-4 w-4'}` }) })), jsx(ChevronDown, { className: `${effectiveSize === 'lg' ? 'h-5 w-5' : 'h-4 w-4'} text-ink-500 transition-transform ${isOpen ? 'rotate-180' : ''}` })] })] }) }), isOpen && !useMobileSheet && (usePortal ? dropdownPosition : true) && (usePortal ? createPortal(jsxs("div", { ref: dropdownRef, 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 ${dropdownPosition?.placement === 'top' ? 'origin-bottom' : 'origin-top'}`, style: {
1013
+ top: dropdownPosition.top,
1014
+ left: dropdownPosition.left,
1015
+ width: dropdownPosition.width,
1016
+ }, children: [searchable && (jsx("div", { className: "p-2 border-b border-paper-200", children: jsxs("div", { className: "relative", children: [jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-ink-400" }), jsx("input", { ref: searchInputRef, type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search...", 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", role: "searchbox", "aria-label": "Search options", "aria-autocomplete": "list", "aria-controls": listboxId })] }) })), jsx("div", { ref: listRef, id: listboxId, className: "overflow-y-auto", style: { maxHeight: useVirtualScrolling ? virtualHeight : '12rem' }, onScroll: (e) => useVirtualScrolling && setScrollTop(e.currentTarget.scrollTop), role: "listbox", "aria-label": "Available options", "aria-multiselectable": "false", children: renderOptionsContent(false) })] }), document.body) : (
1017
+ // Non-portal dropdown (inline, relative positioning)
1018
+ jsxs("div", { ref: dropdownRef, 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", children: [searchable && (jsx("div", { className: "p-2 border-b border-paper-200", children: jsxs("div", { className: "relative", children: [jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-ink-400" }), jsx("input", { ref: searchInputRef, type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search...", 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", role: "searchbox", "aria-label": "Search options", "aria-autocomplete": "list", "aria-controls": listboxId })] }) })), jsx("div", { ref: listRef, id: listboxId, className: "overflow-y-auto", style: { maxHeight: useVirtualScrolling ? virtualHeight : '12rem' }, onScroll: (e) => useVirtualScrolling && setScrollTop(e.currentTarget.scrollTop), role: "listbox", "aria-label": "Available options", "aria-multiselectable": "false", children: renderOptionsContent(false) })] }))), isOpen && useMobileSheet && createPortal(jsxs("div", { className: "fixed inset-0 z-50 flex items-end", onClick: (e) => e.target === e.currentTarget && handleClose(), role: "dialog", "aria-modal": "true", "aria-labelledby": label ? `mobile-${labelId}` : undefined, children: [jsx("div", { className: "absolute inset-0 bg-black/50 animate-fade-in" }), jsxs("div", { className: "relative w-full bg-white rounded-t-2xl shadow-2xl animate-slide-up max-h-[85vh] flex flex-col", style: { paddingBottom: 'env(safe-area-inset-bottom)' }, children: [jsx("div", { className: "py-3 cursor-grab", children: jsx("div", { className: "w-12 h-1.5 bg-ink-300 rounded-full mx-auto" }) }), jsxs("div", { className: "px-4 pb-3 border-b border-paper-200 flex items-center justify-between", children: [label && (jsx("h2", { id: `mobile-${labelId}`, className: "text-lg font-semibold text-ink-900", children: label })), !label && (jsx("h2", { className: "text-lg font-semibold text-ink-900", children: placeholder })), jsx("button", { onClick: handleClose, className: "text-ink-400 hover:text-ink-600 transition-colors p-2 -mr-2", "aria-label": "Close", children: jsx(X, { className: "h-5 w-5" }) })] }), searchable && (jsx("div", { className: "p-3 border-b border-paper-200", children: jsxs("div", { className: "relative", children: [jsx(Search, { className: "absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-ink-400" }), jsx("input", { ref: mobileSearchInputRef, type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search...", inputMode: "search", enterKeyHint: "search", className: "w-full pl-12 pr-4 py-3 text-base border border-paper-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-accent-400", role: "searchbox", "aria-label": "Search options" })] }) })), jsx("div", { id: listboxId, className: "overflow-y-auto flex-1", role: "listbox", "aria-label": "Available options", "aria-multiselectable": "false", children: renderOptionsContent(true) })] })] }), document.body), error && (jsx("p", { id: errorId, className: "mt-2 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsx("p", { id: helperTextId, className: "mt-2 text-xs text-ink-600", children: helperText }))] }));
952
1019
  });
953
1020
  Select.displayName = 'Select';
954
1021
 
@@ -3164,6 +3231,30 @@ const sizeClasses$8 = {
3164
3231
  * </Modal>
3165
3232
  * ```
3166
3233
  *
3234
+ * @example Scrollable modal for long content
3235
+ * ```tsx
3236
+ * <Modal
3237
+ * isOpen={isOpen}
3238
+ * onClose={handleClose}
3239
+ * title="Terms and Conditions"
3240
+ * scrollable
3241
+ * >
3242
+ * {longContent}
3243
+ * </Modal>
3244
+ * ```
3245
+ *
3246
+ * @example Modal with custom max height
3247
+ * ```tsx
3248
+ * <Modal
3249
+ * isOpen={isOpen}
3250
+ * onClose={handleClose}
3251
+ * title="Document Preview"
3252
+ * maxHeight="70vh"
3253
+ * >
3254
+ * {documentContent}
3255
+ * </Modal>
3256
+ * ```
3257
+ *
3167
3258
  * @example Force modal on mobile
3168
3259
  * ```tsx
3169
3260
  * <Modal
@@ -3189,7 +3280,7 @@ const sizeClasses$8 = {
3189
3280
  * </Modal>
3190
3281
  * ```
3191
3282
  */
3192
- function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton = true, animation = 'scale', mobileMode = 'auto', mobileHeight = 'lg', mobileShowHandle = true, }) {
3283
+ function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton = true, animation = 'scale', scrollable = false, maxHeight, mobileMode = 'auto', mobileHeight = 'lg', mobileShowHandle = true, }) {
3193
3284
  const modalRef = useRef(null);
3194
3285
  const mouseDownOnBackdrop = useRef(false);
3195
3286
  const titleId = useId();
@@ -3255,7 +3346,9 @@ function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton
3255
3346
  return (jsx(BottomSheet, { isOpen: isOpen, onClose: onClose, title: title, height: mobileHeight, showHandle: mobileShowHandle, showCloseButton: showCloseButton, children: children }));
3256
3347
  }
3257
3348
  // Render as standard modal on desktop
3258
- return (jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4 bg-ink-900 bg-opacity-50 backdrop-blur-sm animate-fade-in", onMouseDown: handleBackdropMouseDown, onClick: handleBackdropClick, children: jsxs("div", { ref: modalRef, className: `${sizeClasses$8[size]} w-full bg-white bg-subtle-grain rounded-xl shadow-2xl border border-paper-200 ${getAnimationClass()}`, role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, children: [jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-paper-200", children: [jsx("h3", { id: titleId, className: "text-lg font-medium text-ink-900", children: title }), showCloseButton && (jsx("button", { onClick: onClose, className: "text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Close modal", children: jsx(X, { className: "h-5 w-5" }) }))] }), jsx("div", { className: "px-6 py-4", children: children })] }) }));
3349
+ return (jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4 bg-ink-900 bg-opacity-50 backdrop-blur-sm animate-fade-in", onMouseDown: handleBackdropMouseDown, onClick: handleBackdropClick, children: jsxs("div", { ref: modalRef, className: `${sizeClasses$8[size]} w-full bg-white bg-subtle-grain rounded-xl shadow-2xl border border-paper-200 ${getAnimationClass()}`, role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, children: [jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-paper-200", children: [jsx("h3", { id: titleId, className: "text-lg font-medium text-ink-900", children: title }), showCloseButton && (jsx("button", { onClick: onClose, className: "text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Close modal", children: jsx(X, { className: "h-5 w-5" }) }))] }), jsx("div", { className: `px-6 py-4 ${scrollable || maxHeight ? 'overflow-y-auto' : ''}`, style: {
3350
+ maxHeight: maxHeight || (scrollable ? 'calc(100vh - 200px)' : undefined),
3351
+ }, children: children })] }) }));
3259
3352
  }
3260
3353
  function ModalFooter({ children }) {
3261
3354
  return (jsx("div", { className: "flex items-center justify-end gap-3 px-6 py-4 border-t border-paper-200 bg-paper-50 -mx-6 -mb-4 mt-4 rounded-b-xl", children: children }));
@@ -7880,9 +7973,9 @@ function SidebarNavItem({ item, onNavigate, level = 0, currentPath }) {
7880
7973
  // Auto-detect if this item or any child is active based on currentPath
7881
7974
  const isItemActive = currentPath && item.href ? currentPath === item.href : item.active;
7882
7975
  const isChildActive = hasChildren && currentPath
7883
- ? item.children?.some(child => currentPath === child.href || currentPath?.startsWith(child.href || ''))
7976
+ ? item.children?.some(child => child.href && (currentPath === child.href || currentPath.startsWith(child.href)))
7884
7977
  : false;
7885
- const shouldExpandByDefault = isChildActive || (hasChildren && currentPath?.startsWith(item.href || ''));
7978
+ const shouldExpandByDefault = isChildActive || (hasChildren && item.href && currentPath?.startsWith(item.href));
7886
7979
  const [isExpanded, setIsExpanded] = useState(shouldExpandByDefault);
7887
7980
  const handleClick = () => {
7888
7981
  if (hasChildren) {
@@ -10075,256 +10168,6 @@ mobileView = 'auto', cardConfig, cardGap = 'md', cardClassName, }) {
10075
10168
  return (jsxs(Fragment, { children: [renderPaginationControls(), shouldShowCardView ? cardViewContent : finalContent, contextMenuState.isOpen && contextMenuState.item && (jsx(Menu, { items: convertActionsToMenuItems(contextMenuState.item), position: contextMenuState.position, onClose: () => setContextMenuState({ isOpen: false, position: { x: 0, y: 0 }, item: null }) }))] }));
10076
10169
  }
10077
10170
 
10078
- // Color mapping for action buttons
10079
- const colorClasses = {
10080
- primary: 'bg-accent-500 text-white',
10081
- success: 'bg-success-500 text-white',
10082
- warning: 'bg-warning-500 text-white',
10083
- error: 'bg-error-500 text-white',
10084
- default: 'bg-paper-500 text-white',
10085
- };
10086
- /**
10087
- * SwipeActions - Touch-based swipe actions for list items
10088
- *
10089
- * Wraps any content with swipe-to-reveal actions, commonly used in mobile
10090
- * list items for quick actions like delete, archive, edit, etc.
10091
- *
10092
- * Features:
10093
- * - Left and right swipe actions
10094
- * - Full swipe to trigger primary action
10095
- * - Spring-back animation
10096
- * - Touch and mouse support
10097
- * - Customizable thresholds
10098
- *
10099
- * @example Basic delete action
10100
- * ```tsx
10101
- * <SwipeActions
10102
- * leftActions={[
10103
- * {
10104
- * id: 'delete',
10105
- * label: 'Delete',
10106
- * icon: <Trash className="h-5 w-5" />,
10107
- * color: 'error',
10108
- * onClick: () => handleDelete(item),
10109
- * primary: true,
10110
- * },
10111
- * ]}
10112
- * >
10113
- * <div className="p-4 bg-white">
10114
- * List item content
10115
- * </div>
10116
- * </SwipeActions>
10117
- * ```
10118
- *
10119
- * @example Multiple actions on both sides
10120
- * ```tsx
10121
- * <SwipeActions
10122
- * leftActions={[
10123
- * { id: 'delete', label: 'Delete', icon: <Trash />, color: 'error', onClick: handleDelete },
10124
- * { id: 'archive', label: 'Archive', icon: <Archive />, color: 'warning', onClick: handleArchive },
10125
- * ]}
10126
- * rightActions={[
10127
- * { id: 'edit', label: 'Edit', icon: <Edit />, color: 'primary', onClick: handleEdit },
10128
- * ]}
10129
- * fullSwipe
10130
- * >
10131
- * <ListItem />
10132
- * </SwipeActions>
10133
- * ```
10134
- */
10135
- function SwipeActions({ children, leftActions = [], rightActions = [], threshold = 80, fullSwipeThreshold = 0.5, fullSwipe = false, disabled = false, onSwipeChange, className = '', }) {
10136
- const containerRef = useRef(null);
10137
- const contentRef = useRef(null);
10138
- // Swipe state
10139
- const [translateX, setTranslateX] = useState(0);
10140
- const [isDragging, setIsDragging] = useState(false);
10141
- const [activeDirection, setActiveDirection] = useState(null);
10142
- // Touch/mouse tracking
10143
- const startX = useRef(0);
10144
- const currentX = useRef(0);
10145
- const startTime = useRef(0);
10146
- // Calculate action widths
10147
- const leftActionsWidth = leftActions.length * 72; // 72px per action
10148
- const rightActionsWidth = rightActions.length * 72;
10149
- // Reset position
10150
- const resetPosition = useCallback(() => {
10151
- setTranslateX(0);
10152
- setActiveDirection(null);
10153
- onSwipeChange?.(null);
10154
- }, [onSwipeChange]);
10155
- // Handle touch/mouse start
10156
- const handleStart = useCallback((clientX) => {
10157
- if (disabled)
10158
- return;
10159
- startX.current = clientX;
10160
- currentX.current = clientX;
10161
- startTime.current = Date.now();
10162
- setIsDragging(true);
10163
- }, [disabled]);
10164
- // Handle touch/mouse move
10165
- const handleMove = useCallback((clientX) => {
10166
- if (!isDragging || disabled)
10167
- return;
10168
- const deltaX = clientX - startX.current;
10169
- currentX.current = clientX;
10170
- // Determine direction and apply resistance at boundaries
10171
- let newTranslateX = deltaX;
10172
- // Swiping left (reveals left actions on right side)
10173
- if (deltaX < 0) {
10174
- if (leftActions.length === 0) {
10175
- newTranslateX = deltaX * 0.2; // Heavy resistance if no actions
10176
- }
10177
- else {
10178
- const maxSwipe = fullSwipe
10179
- ? -(containerRef.current?.offsetWidth || 300)
10180
- : -leftActionsWidth;
10181
- newTranslateX = Math.max(maxSwipe, deltaX);
10182
- // Apply resistance past the action buttons
10183
- if (newTranslateX < -leftActionsWidth) {
10184
- const overSwipe = newTranslateX + leftActionsWidth;
10185
- newTranslateX = -leftActionsWidth + overSwipe * 0.3;
10186
- }
10187
- }
10188
- setActiveDirection('left');
10189
- onSwipeChange?.('left');
10190
- }
10191
- // Swiping right (reveals right actions on left side)
10192
- else if (deltaX > 0) {
10193
- if (rightActions.length === 0) {
10194
- newTranslateX = deltaX * 0.2; // Heavy resistance if no actions
10195
- }
10196
- else {
10197
- const maxSwipe = fullSwipe
10198
- ? (containerRef.current?.offsetWidth || 300)
10199
- : rightActionsWidth;
10200
- newTranslateX = Math.min(maxSwipe, deltaX);
10201
- // Apply resistance past the action buttons
10202
- if (newTranslateX > rightActionsWidth) {
10203
- const overSwipe = newTranslateX - rightActionsWidth;
10204
- newTranslateX = rightActionsWidth + overSwipe * 0.3;
10205
- }
10206
- }
10207
- setActiveDirection('right');
10208
- onSwipeChange?.('right');
10209
- }
10210
- setTranslateX(newTranslateX);
10211
- }, [isDragging, disabled, leftActions.length, rightActions.length, leftActionsWidth, rightActionsWidth, fullSwipe, onSwipeChange]);
10212
- // Handle touch/mouse end
10213
- const handleEnd = useCallback(() => {
10214
- if (!isDragging)
10215
- return;
10216
- setIsDragging(false);
10217
- const deltaX = currentX.current - startX.current;
10218
- const velocity = Math.abs(deltaX) / (Date.now() - startTime.current);
10219
- const containerWidth = containerRef.current?.offsetWidth || 300;
10220
- // Check for full swipe trigger
10221
- if (fullSwipe) {
10222
- const swipePercentage = Math.abs(translateX) / containerWidth;
10223
- if (swipePercentage >= fullSwipeThreshold || velocity > 0.5) {
10224
- // Find primary action and trigger it
10225
- if (translateX < 0 && leftActions.length > 0) {
10226
- const primaryAction = leftActions.find(a => a.primary) || leftActions[0];
10227
- primaryAction.onClick();
10228
- resetPosition();
10229
- return;
10230
- }
10231
- else if (translateX > 0 && rightActions.length > 0) {
10232
- const primaryAction = rightActions.find(a => a.primary) || rightActions[0];
10233
- primaryAction.onClick();
10234
- resetPosition();
10235
- return;
10236
- }
10237
- }
10238
- }
10239
- // Snap to open or closed position
10240
- if (Math.abs(translateX) >= threshold || velocity > 0.3) {
10241
- // Snap open
10242
- if (translateX < 0 && leftActions.length > 0) {
10243
- setTranslateX(-leftActionsWidth);
10244
- setActiveDirection('left');
10245
- onSwipeChange?.('left');
10246
- }
10247
- else if (translateX > 0 && rightActions.length > 0) {
10248
- setTranslateX(rightActionsWidth);
10249
- setActiveDirection('right');
10250
- onSwipeChange?.('right');
10251
- }
10252
- else {
10253
- resetPosition();
10254
- }
10255
- }
10256
- else {
10257
- // Snap closed
10258
- resetPosition();
10259
- }
10260
- }, [isDragging, translateX, threshold, fullSwipe, fullSwipeThreshold, leftActions, rightActions, leftActionsWidth, rightActionsWidth, resetPosition, onSwipeChange]);
10261
- // Touch event handlers
10262
- const handleTouchStart = (e) => {
10263
- handleStart(e.touches[0].clientX);
10264
- };
10265
- const handleTouchMove = (e) => {
10266
- handleMove(e.touches[0].clientX);
10267
- };
10268
- const handleTouchEnd = () => {
10269
- handleEnd();
10270
- };
10271
- // Mouse event handlers (for testing/desktop)
10272
- const handleMouseDown = (e) => {
10273
- handleStart(e.clientX);
10274
- };
10275
- const handleMouseMove = (e) => {
10276
- handleMove(e.clientX);
10277
- };
10278
- const handleMouseUp = () => {
10279
- handleEnd();
10280
- };
10281
- // Close on outside click
10282
- useEffect(() => {
10283
- if (activeDirection === null)
10284
- return;
10285
- const handleClickOutside = (e) => {
10286
- if (containerRef.current && !containerRef.current.contains(e.target)) {
10287
- resetPosition();
10288
- }
10289
- };
10290
- document.addEventListener('mousedown', handleClickOutside);
10291
- return () => document.removeEventListener('mousedown', handleClickOutside);
10292
- }, [activeDirection, resetPosition]);
10293
- // Handle mouse leave during drag
10294
- useEffect(() => {
10295
- if (!isDragging)
10296
- return;
10297
- const handleMouseLeave = () => {
10298
- handleEnd();
10299
- };
10300
- document.addEventListener('mouseup', handleMouseLeave);
10301
- return () => document.removeEventListener('mouseup', handleMouseLeave);
10302
- }, [isDragging, handleEnd]);
10303
- // Render action button
10304
- const renderActionButton = (action) => {
10305
- const colorClass = colorClasses[action.color || 'default'];
10306
- return (jsxs("button", { onClick: (e) => {
10307
- e.stopPropagation();
10308
- action.onClick();
10309
- resetPosition();
10310
- }, className: `
10311
- flex flex-col items-center justify-center
10312
- w-18 h-full min-w-[72px]
10313
- ${colorClass}
10314
- transition-transform duration-150
10315
- `, style: {
10316
- transform: isDragging ? 'scale(1)' : 'scale(1)',
10317
- }, children: [action.icon && (jsx("div", { className: "mb-1", children: action.icon })), jsx("span", { className: "text-xs font-medium", children: action.label })] }, action.id));
10318
- };
10319
- return (jsxs("div", { ref: containerRef, className: `relative overflow-hidden ${className}`, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onMouseDown: handleMouseDown, onMouseMove: isDragging ? handleMouseMove : undefined, onMouseUp: handleMouseUp, onMouseLeave: isDragging ? handleEnd : undefined, children: [rightActions.length > 0 && (jsx("div", { className: "absolute left-0 top-0 bottom-0 flex", style: { width: rightActionsWidth }, children: rightActions.map((action) => renderActionButton(action)) })), leftActions.length > 0 && (jsx("div", { className: "absolute right-0 top-0 bottom-0 flex", style: { width: leftActionsWidth }, children: leftActions.map((action) => renderActionButton(action)) })), jsx("div", { ref: contentRef, className: `
10320
- relative bg-white
10321
- ${isDragging ? '' : 'transition-transform duration-200 ease-out'}
10322
- `, style: {
10323
- transform: `translateX(${translateX}px)`,
10324
- touchAction: 'pan-y', // Allow vertical scrolling
10325
- }, children: children })] }));
10326
- }
10327
-
10328
10171
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
10329
10172
 
10330
10173
  function getDefaultExportFromCjs (x) {
@@ -10356,86 +10199,6 @@ function getAugmentedNamespace(n) {
10356
10199
  return a;
10357
10200
  }
10358
10201
 
10359
- var classnames = {exports: {}};
10360
-
10361
- /*!
10362
- Copyright (c) 2018 Jed Watson.
10363
- Licensed under the MIT License (MIT), see
10364
- http://jedwatson.github.io/classnames
10365
- */
10366
-
10367
- (function (module) {
10368
- /* global define */
10369
-
10370
- (function () {
10371
-
10372
- var hasOwn = {}.hasOwnProperty;
10373
-
10374
- function classNames () {
10375
- var classes = '';
10376
-
10377
- for (var i = 0; i < arguments.length; i++) {
10378
- var arg = arguments[i];
10379
- if (arg) {
10380
- classes = appendClass(classes, parseValue(arg));
10381
- }
10382
- }
10383
-
10384
- return classes;
10385
- }
10386
-
10387
- function parseValue (arg) {
10388
- if (typeof arg === 'string' || typeof arg === 'number') {
10389
- return arg;
10390
- }
10391
-
10392
- if (typeof arg !== 'object') {
10393
- return '';
10394
- }
10395
-
10396
- if (Array.isArray(arg)) {
10397
- return classNames.apply(null, arg);
10398
- }
10399
-
10400
- if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {
10401
- return arg.toString();
10402
- }
10403
-
10404
- var classes = '';
10405
-
10406
- for (var key in arg) {
10407
- if (hasOwn.call(arg, key) && arg[key]) {
10408
- classes = appendClass(classes, key);
10409
- }
10410
- }
10411
-
10412
- return classes;
10413
- }
10414
-
10415
- function appendClass (value, newClass) {
10416
- if (!newClass) {
10417
- return value;
10418
- }
10419
-
10420
- if (value) {
10421
- return value + ' ' + newClass;
10422
- }
10423
-
10424
- return value + newClass;
10425
- }
10426
-
10427
- if (module.exports) {
10428
- classNames.default = classNames;
10429
- module.exports = classNames;
10430
- } else {
10431
- window.classNames = classNames;
10432
- }
10433
- }());
10434
- } (classnames));
10435
-
10436
- var classnamesExports = classnames.exports;
10437
- var classNames = /*@__PURE__*/getDefaultExportFromCjs(classnamesExports);
10438
-
10439
10202
  /**
10440
10203
  * Represents unions.
10441
10204
  * (A1, A1:C5, ...)
@@ -32195,7 +31958,7 @@ const Utils$2 = utils$2;
32195
31958
  /**
32196
31959
  * A Excel Formula Parser & Evaluator
32197
31960
  */
32198
- let FormulaParser$2 = class FormulaParser {
31961
+ let FormulaParser$3 = class FormulaParser {
32199
31962
 
32200
31963
  /**
32201
31964
  * @param {{functions: {}, functionsNeedContext: {}, onVariable: function, onCell: function, onRange: function}} [config]
@@ -32526,7 +32289,7 @@ let FormulaParser$2 = class FormulaParser {
32526
32289
  };
32527
32290
 
32528
32291
  var hooks$1 = {
32529
- FormulaParser: FormulaParser$2};
32292
+ FormulaParser: FormulaParser$3};
32530
32293
 
32531
32294
  const FormulaError$2 = requireError();
32532
32295
  const {FormulaHelpers: FormulaHelpers$1, Types, Address} = requireHelpers();
@@ -32982,7 +32745,7 @@ var hooks = {
32982
32745
  DepParser: DepParser$1,
32983
32746
  };
32984
32747
 
32985
- const {FormulaParser: FormulaParser$1} = hooks$1;
32748
+ const {FormulaParser: FormulaParser$2} = hooks$1;
32986
32749
  const {DepParser} = hooks;
32987
32750
  const SSF = ssf$1;
32988
32751
  const FormulaError = requireError();
@@ -32992,16 +32755,2271 @@ const FormulaError = requireError();
32992
32755
  // `\nTotal: ${funs.length}/477, ${funs.length/477*100}% implemented.`);
32993
32756
 
32994
32757
 
32995
- Object.assign(FormulaParser$1, {
32758
+ Object.assign(FormulaParser$2, {
32996
32759
  MAX_ROW: 1048576,
32997
32760
  MAX_COLUMN: 16384,
32998
32761
  SSF,
32999
32762
  DepParser,
33000
32763
  FormulaError, ...requireHelpers()
33001
32764
  });
33002
- var fastFormulaParser = FormulaParser$1;
32765
+ var fastFormulaParser = FormulaParser$2;
32766
+
32767
+ var FormulaParser$1 = /*@__PURE__*/getDefaultExportFromCjs(fastFormulaParser);
32768
+
32769
+ var FormulaParserModule = /*#__PURE__*/_mergeNamespaces({
32770
+ __proto__: null,
32771
+ default: FormulaParser$1
32772
+ }, [fastFormulaParser]);
32773
+
32774
+ /**
32775
+ * Formula definitions for DataGrid intellisense
32776
+ * Based on fast-formula-parser supported functions
32777
+ */
32778
+ const FORMULA_DEFINITIONS = [
32779
+ // ==================== MATH ====================
32780
+ {
32781
+ name: 'SUM',
32782
+ category: 'Math',
32783
+ description: 'Adds all numbers in a range of cells',
32784
+ syntax: 'SUM(number1, [number2], ...)',
32785
+ parameters: [
32786
+ { name: 'number1', description: 'First number or range to add' },
32787
+ { name: 'number2', description: 'Additional numbers or ranges', optional: true },
32788
+ ],
32789
+ example: '=SUM(A1:A10)',
32790
+ },
32791
+ {
32792
+ name: 'AVERAGE',
32793
+ category: 'Math',
32794
+ description: 'Returns the average of the arguments',
32795
+ syntax: 'AVERAGE(number1, [number2], ...)',
32796
+ parameters: [
32797
+ { name: 'number1', description: 'First number or range' },
32798
+ { name: 'number2', description: 'Additional numbers or ranges', optional: true },
32799
+ ],
32800
+ example: '=AVERAGE(B1:B20)',
32801
+ },
32802
+ {
32803
+ name: 'MIN',
32804
+ category: 'Math',
32805
+ description: 'Returns the minimum value in a list of arguments',
32806
+ syntax: 'MIN(number1, [number2], ...)',
32807
+ parameters: [
32808
+ { name: 'number1', description: 'First number or range' },
32809
+ { name: 'number2', description: 'Additional numbers or ranges', optional: true },
32810
+ ],
32811
+ example: '=MIN(A1:A100)',
32812
+ },
32813
+ {
32814
+ name: 'MAX',
32815
+ category: 'Math',
32816
+ description: 'Returns the maximum value in a list of arguments',
32817
+ syntax: 'MAX(number1, [number2], ...)',
32818
+ parameters: [
32819
+ { name: 'number1', description: 'First number or range' },
32820
+ { name: 'number2', description: 'Additional numbers or ranges', optional: true },
32821
+ ],
32822
+ example: '=MAX(A1:A100)',
32823
+ },
32824
+ {
32825
+ name: 'COUNT',
32826
+ category: 'Math',
32827
+ description: 'Counts the number of cells that contain numbers',
32828
+ syntax: 'COUNT(value1, [value2], ...)',
32829
+ parameters: [
32830
+ { name: 'value1', description: 'First value or range' },
32831
+ { name: 'value2', description: 'Additional values or ranges', optional: true },
32832
+ ],
32833
+ example: '=COUNT(A1:A50)',
32834
+ },
32835
+ {
32836
+ name: 'COUNTA',
32837
+ category: 'Math',
32838
+ description: 'Counts the number of non-empty cells',
32839
+ syntax: 'COUNTA(value1, [value2], ...)',
32840
+ parameters: [
32841
+ { name: 'value1', description: 'First value or range' },
32842
+ { name: 'value2', description: 'Additional values or ranges', optional: true },
32843
+ ],
32844
+ example: '=COUNTA(A1:A50)',
32845
+ },
32846
+ {
32847
+ name: 'ROUND',
32848
+ category: 'Math',
32849
+ description: 'Rounds a number to a specified number of digits',
32850
+ syntax: 'ROUND(number, num_digits)',
32851
+ parameters: [
32852
+ { name: 'number', description: 'The number to round' },
32853
+ { name: 'num_digits', description: 'Number of decimal places' },
32854
+ ],
32855
+ example: '=ROUND(3.14159, 2)',
32856
+ },
32857
+ {
32858
+ name: 'ROUNDUP',
32859
+ category: 'Math',
32860
+ description: 'Rounds a number up, away from zero',
32861
+ syntax: 'ROUNDUP(number, num_digits)',
32862
+ parameters: [
32863
+ { name: 'number', description: 'The number to round up' },
32864
+ { name: 'num_digits', description: 'Number of decimal places' },
32865
+ ],
32866
+ example: '=ROUNDUP(3.14159, 2)',
32867
+ },
32868
+ {
32869
+ name: 'ROUNDDOWN',
32870
+ category: 'Math',
32871
+ description: 'Rounds a number down, toward zero',
32872
+ syntax: 'ROUNDDOWN(number, num_digits)',
32873
+ parameters: [
32874
+ { name: 'number', description: 'The number to round down' },
32875
+ { name: 'num_digits', description: 'Number of decimal places' },
32876
+ ],
32877
+ example: '=ROUNDDOWN(3.14159, 2)',
32878
+ },
32879
+ {
32880
+ name: 'ABS',
32881
+ category: 'Math',
32882
+ description: 'Returns the absolute value of a number',
32883
+ syntax: 'ABS(number)',
32884
+ parameters: [{ name: 'number', description: 'The number to get absolute value of' }],
32885
+ example: '=ABS(-5)',
32886
+ },
32887
+ {
32888
+ name: 'SQRT',
32889
+ category: 'Math',
32890
+ description: 'Returns the square root of a number',
32891
+ syntax: 'SQRT(number)',
32892
+ parameters: [{ name: 'number', description: 'The number to get square root of' }],
32893
+ example: '=SQRT(16)',
32894
+ },
32895
+ {
32896
+ name: 'POWER',
32897
+ category: 'Math',
32898
+ description: 'Returns the result of a number raised to a power',
32899
+ syntax: 'POWER(number, power)',
32900
+ parameters: [
32901
+ { name: 'number', description: 'The base number' },
32902
+ { name: 'power', description: 'The exponent' },
32903
+ ],
32904
+ example: '=POWER(2, 8)',
32905
+ },
32906
+ {
32907
+ name: 'MOD',
32908
+ category: 'Math',
32909
+ description: 'Returns the remainder after division',
32910
+ syntax: 'MOD(number, divisor)',
32911
+ parameters: [
32912
+ { name: 'number', description: 'The number to divide' },
32913
+ { name: 'divisor', description: 'The number to divide by' },
32914
+ ],
32915
+ example: '=MOD(10, 3)',
32916
+ },
32917
+ {
32918
+ name: 'INT',
32919
+ category: 'Math',
32920
+ description: 'Rounds a number down to the nearest integer',
32921
+ syntax: 'INT(number)',
32922
+ parameters: [{ name: 'number', description: 'The number to round down' }],
32923
+ example: '=INT(3.7)',
32924
+ },
32925
+ {
32926
+ name: 'CEILING',
32927
+ category: 'Math',
32928
+ description: 'Rounds a number up to the nearest multiple of significance',
32929
+ syntax: 'CEILING(number, significance)',
32930
+ parameters: [
32931
+ { name: 'number', description: 'The number to round' },
32932
+ { name: 'significance', description: 'The multiple to round to' },
32933
+ ],
32934
+ example: '=CEILING(4.3, 1)',
32935
+ },
32936
+ {
32937
+ name: 'FLOOR',
32938
+ category: 'Math',
32939
+ description: 'Rounds a number down to the nearest multiple of significance',
32940
+ syntax: 'FLOOR(number, significance)',
32941
+ parameters: [
32942
+ { name: 'number', description: 'The number to round' },
32943
+ { name: 'significance', description: 'The multiple to round to' },
32944
+ ],
32945
+ example: '=FLOOR(4.7, 1)',
32946
+ },
32947
+ {
32948
+ name: 'PRODUCT',
32949
+ category: 'Math',
32950
+ description: 'Multiplies all the numbers given as arguments',
32951
+ syntax: 'PRODUCT(number1, [number2], ...)',
32952
+ parameters: [
32953
+ { name: 'number1', description: 'First number or range' },
32954
+ { name: 'number2', description: 'Additional numbers or ranges', optional: true },
32955
+ ],
32956
+ example: '=PRODUCT(A1:A5)',
32957
+ },
32958
+ {
32959
+ name: 'SUMPRODUCT',
32960
+ category: 'Math',
32961
+ description: 'Returns the sum of the products of corresponding array components',
32962
+ syntax: 'SUMPRODUCT(array1, [array2], ...)',
32963
+ parameters: [
32964
+ { name: 'array1', description: 'First array' },
32965
+ { name: 'array2', description: 'Additional arrays', optional: true },
32966
+ ],
32967
+ example: '=SUMPRODUCT(A1:A5, B1:B5)',
32968
+ },
32969
+ {
32970
+ name: 'RAND',
32971
+ category: 'Math',
32972
+ description: 'Returns a random number between 0 and 1',
32973
+ syntax: 'RAND()',
32974
+ parameters: [],
32975
+ example: '=RAND()',
32976
+ },
32977
+ {
32978
+ name: 'RANDBETWEEN',
32979
+ category: 'Math',
32980
+ description: 'Returns a random integer between the specified values',
32981
+ syntax: 'RANDBETWEEN(bottom, top)',
32982
+ parameters: [
32983
+ { name: 'bottom', description: 'Minimum value' },
32984
+ { name: 'top', description: 'Maximum value' },
32985
+ ],
32986
+ example: '=RANDBETWEEN(1, 100)',
32987
+ },
32988
+ // ==================== STATISTICAL ====================
32989
+ {
32990
+ name: 'COUNTIF',
32991
+ category: 'Statistical',
32992
+ description: 'Counts cells that meet a single criterion',
32993
+ syntax: 'COUNTIF(range, criteria)',
32994
+ parameters: [
32995
+ { name: 'range', description: 'Range of cells to count' },
32996
+ { name: 'criteria', description: 'Condition to match' },
32997
+ ],
32998
+ example: '=COUNTIF(A1:A10, ">5")',
32999
+ },
33000
+ {
33001
+ name: 'COUNTIFS',
33002
+ category: 'Statistical',
33003
+ description: 'Counts cells that meet multiple criteria',
33004
+ syntax: 'COUNTIFS(range1, criteria1, [range2], [criteria2], ...)',
33005
+ parameters: [
33006
+ { name: 'range1', description: 'First range to evaluate' },
33007
+ { name: 'criteria1', description: 'First condition' },
33008
+ { name: 'range2', description: 'Additional range', optional: true },
33009
+ { name: 'criteria2', description: 'Additional condition', optional: true },
33010
+ ],
33011
+ example: '=COUNTIFS(A:A, ">5", B:B, "<10")',
33012
+ },
33013
+ {
33014
+ name: 'SUMIF',
33015
+ category: 'Statistical',
33016
+ description: 'Sums cells that meet a single criterion',
33017
+ syntax: 'SUMIF(range, criteria, [sum_range])',
33018
+ parameters: [
33019
+ { name: 'range', description: 'Range to evaluate' },
33020
+ { name: 'criteria', description: 'Condition to match' },
33021
+ { name: 'sum_range', description: 'Actual cells to sum', optional: true },
33022
+ ],
33023
+ example: '=SUMIF(A1:A10, ">5", B1:B10)',
33024
+ },
33025
+ {
33026
+ name: 'SUMIFS',
33027
+ category: 'Statistical',
33028
+ description: 'Sums cells that meet multiple criteria',
33029
+ syntax: 'SUMIFS(sum_range, range1, criteria1, [range2], [criteria2], ...)',
33030
+ parameters: [
33031
+ { name: 'sum_range', description: 'Cells to sum' },
33032
+ { name: 'range1', description: 'First range to evaluate' },
33033
+ { name: 'criteria1', description: 'First condition' },
33034
+ { name: 'range2', description: 'Additional range', optional: true },
33035
+ { name: 'criteria2', description: 'Additional condition', optional: true },
33036
+ ],
33037
+ example: '=SUMIFS(C:C, A:A, ">5", B:B, "<10")',
33038
+ },
33039
+ {
33040
+ name: 'AVERAGEIF',
33041
+ category: 'Statistical',
33042
+ description: 'Averages cells that meet a single criterion',
33043
+ syntax: 'AVERAGEIF(range, criteria, [average_range])',
33044
+ parameters: [
33045
+ { name: 'range', description: 'Range to evaluate' },
33046
+ { name: 'criteria', description: 'Condition to match' },
33047
+ { name: 'average_range', description: 'Actual cells to average', optional: true },
33048
+ ],
33049
+ example: '=AVERAGEIF(A1:A10, ">5", B1:B10)',
33050
+ },
33051
+ {
33052
+ name: 'MEDIAN',
33053
+ category: 'Statistical',
33054
+ description: 'Returns the median of the given numbers',
33055
+ syntax: 'MEDIAN(number1, [number2], ...)',
33056
+ parameters: [
33057
+ { name: 'number1', description: 'First number or range' },
33058
+ { name: 'number2', description: 'Additional numbers or ranges', optional: true },
33059
+ ],
33060
+ example: '=MEDIAN(A1:A100)',
33061
+ },
33062
+ {
33063
+ name: 'MODE',
33064
+ category: 'Statistical',
33065
+ description: 'Returns the most frequently occurring value',
33066
+ syntax: 'MODE(number1, [number2], ...)',
33067
+ parameters: [
33068
+ { name: 'number1', description: 'First number or range' },
33069
+ { name: 'number2', description: 'Additional numbers or ranges', optional: true },
33070
+ ],
33071
+ example: '=MODE(A1:A100)',
33072
+ },
33073
+ {
33074
+ name: 'STDEV',
33075
+ category: 'Statistical',
33076
+ description: 'Estimates standard deviation based on a sample',
33077
+ syntax: 'STDEV(number1, [number2], ...)',
33078
+ parameters: [
33079
+ { name: 'number1', description: 'First number or range' },
33080
+ { name: 'number2', description: 'Additional numbers or ranges', optional: true },
33081
+ ],
33082
+ example: '=STDEV(A1:A100)',
33083
+ },
33084
+ {
33085
+ name: 'VAR',
33086
+ category: 'Statistical',
33087
+ description: 'Estimates variance based on a sample',
33088
+ syntax: 'VAR(number1, [number2], ...)',
33089
+ parameters: [
33090
+ { name: 'number1', description: 'First number or range' },
33091
+ { name: 'number2', description: 'Additional numbers or ranges', optional: true },
33092
+ ],
33093
+ example: '=VAR(A1:A100)',
33094
+ },
33095
+ {
33096
+ name: 'LARGE',
33097
+ category: 'Statistical',
33098
+ description: 'Returns the k-th largest value in a data set',
33099
+ syntax: 'LARGE(array, k)',
33100
+ parameters: [
33101
+ { name: 'array', description: 'Range to search' },
33102
+ { name: 'k', description: 'Position from largest' },
33103
+ ],
33104
+ example: '=LARGE(A1:A100, 3)',
33105
+ },
33106
+ {
33107
+ name: 'SMALL',
33108
+ category: 'Statistical',
33109
+ description: 'Returns the k-th smallest value in a data set',
33110
+ syntax: 'SMALL(array, k)',
33111
+ parameters: [
33112
+ { name: 'array', description: 'Range to search' },
33113
+ { name: 'k', description: 'Position from smallest' },
33114
+ ],
33115
+ example: '=SMALL(A1:A100, 3)',
33116
+ },
33117
+ {
33118
+ name: 'RANK',
33119
+ category: 'Statistical',
33120
+ description: 'Returns the rank of a number in a list',
33121
+ syntax: 'RANK(number, ref, [order])',
33122
+ parameters: [
33123
+ { name: 'number', description: 'The number to rank' },
33124
+ { name: 'ref', description: 'Range of numbers' },
33125
+ { name: 'order', description: '0 for descending, non-zero for ascending', optional: true },
33126
+ ],
33127
+ example: '=RANK(A1, A1:A100)',
33128
+ },
33129
+ {
33130
+ name: 'PERCENTILE',
33131
+ category: 'Statistical',
33132
+ description: 'Returns the k-th percentile of values',
33133
+ syntax: 'PERCENTILE(array, k)',
33134
+ parameters: [
33135
+ { name: 'array', description: 'Range of data' },
33136
+ { name: 'k', description: 'Percentile value (0 to 1)' },
33137
+ ],
33138
+ example: '=PERCENTILE(A1:A100, 0.9)',
33139
+ },
33140
+ // ==================== LOOKUP ====================
33141
+ {
33142
+ name: 'VLOOKUP',
33143
+ category: 'Lookup',
33144
+ description: 'Looks for a value in the leftmost column and returns a value in the same row from a specified column',
33145
+ syntax: 'VLOOKUP(lookup_value, table_array, col_index_num, [range_lookup])',
33146
+ parameters: [
33147
+ { name: 'lookup_value', description: 'Value to search for' },
33148
+ { name: 'table_array', description: 'Table range to search' },
33149
+ { name: 'col_index_num', description: 'Column number to return (1-based)' },
33150
+ { name: 'range_lookup', description: 'FALSE for exact match, TRUE for approximate', optional: true },
33151
+ ],
33152
+ example: '=VLOOKUP(A1, B1:D10, 2, FALSE)',
33153
+ },
33154
+ {
33155
+ name: 'HLOOKUP',
33156
+ category: 'Lookup',
33157
+ description: 'Looks for a value in the top row and returns a value in the same column from a specified row',
33158
+ syntax: 'HLOOKUP(lookup_value, table_array, row_index_num, [range_lookup])',
33159
+ parameters: [
33160
+ { name: 'lookup_value', description: 'Value to search for' },
33161
+ { name: 'table_array', description: 'Table range to search' },
33162
+ { name: 'row_index_num', description: 'Row number to return (1-based)' },
33163
+ { name: 'range_lookup', description: 'FALSE for exact match, TRUE for approximate', optional: true },
33164
+ ],
33165
+ example: '=HLOOKUP(A1, A1:Z3, 2, FALSE)',
33166
+ },
33167
+ {
33168
+ name: 'INDEX',
33169
+ category: 'Lookup',
33170
+ description: 'Returns a value from a specific position in a range',
33171
+ syntax: 'INDEX(array, row_num, [col_num])',
33172
+ parameters: [
33173
+ { name: 'array', description: 'Range of cells' },
33174
+ { name: 'row_num', description: 'Row position' },
33175
+ { name: 'col_num', description: 'Column position', optional: true },
33176
+ ],
33177
+ example: '=INDEX(A1:C10, 5, 2)',
33178
+ },
33179
+ {
33180
+ name: 'MATCH',
33181
+ category: 'Lookup',
33182
+ description: 'Returns the relative position of an item in a range',
33183
+ syntax: 'MATCH(lookup_value, lookup_array, [match_type])',
33184
+ parameters: [
33185
+ { name: 'lookup_value', description: 'Value to find' },
33186
+ { name: 'lookup_array', description: 'Range to search' },
33187
+ { name: 'match_type', description: '0 for exact, 1 for less than, -1 for greater than', optional: true },
33188
+ ],
33189
+ example: '=MATCH("Apple", A1:A10, 0)',
33190
+ },
33191
+ {
33192
+ name: 'LOOKUP',
33193
+ category: 'Lookup',
33194
+ description: 'Looks up a value in a vector or array',
33195
+ syntax: 'LOOKUP(lookup_value, lookup_vector, [result_vector])',
33196
+ parameters: [
33197
+ { name: 'lookup_value', description: 'Value to find' },
33198
+ { name: 'lookup_vector', description: 'Range to search' },
33199
+ { name: 'result_vector', description: 'Range to return from', optional: true },
33200
+ ],
33201
+ example: '=LOOKUP(5, A1:A10, B1:B10)',
33202
+ },
33203
+ {
33204
+ name: 'CHOOSE',
33205
+ category: 'Lookup',
33206
+ description: 'Returns a value from a list based on an index',
33207
+ syntax: 'CHOOSE(index_num, value1, [value2], ...)',
33208
+ parameters: [
33209
+ { name: 'index_num', description: 'Which value to return (1-based)' },
33210
+ { name: 'value1', description: 'First value' },
33211
+ { name: 'value2', description: 'Additional values', optional: true },
33212
+ ],
33213
+ example: '=CHOOSE(2, "Red", "Blue", "Green")',
33214
+ },
33215
+ {
33216
+ name: 'ROW',
33217
+ category: 'Lookup',
33218
+ description: 'Returns the row number of a reference',
33219
+ syntax: 'ROW([reference])',
33220
+ parameters: [{ name: 'reference', description: 'Cell reference', optional: true }],
33221
+ example: '=ROW(A5)',
33222
+ },
33223
+ {
33224
+ name: 'COLUMN',
33225
+ category: 'Lookup',
33226
+ description: 'Returns the column number of a reference',
33227
+ syntax: 'COLUMN([reference])',
33228
+ parameters: [{ name: 'reference', description: 'Cell reference', optional: true }],
33229
+ example: '=COLUMN(C1)',
33230
+ },
33231
+ {
33232
+ name: 'ROWS',
33233
+ category: 'Lookup',
33234
+ description: 'Returns the number of rows in a reference',
33235
+ syntax: 'ROWS(array)',
33236
+ parameters: [{ name: 'array', description: 'Range reference' }],
33237
+ example: '=ROWS(A1:A10)',
33238
+ },
33239
+ {
33240
+ name: 'COLUMNS',
33241
+ category: 'Lookup',
33242
+ description: 'Returns the number of columns in a reference',
33243
+ syntax: 'COLUMNS(array)',
33244
+ parameters: [{ name: 'array', description: 'Range reference' }],
33245
+ example: '=COLUMNS(A1:D1)',
33246
+ },
33247
+ {
33248
+ name: 'OFFSET',
33249
+ category: 'Lookup',
33250
+ description: 'Returns a reference offset from a starting point',
33251
+ syntax: 'OFFSET(reference, rows, cols, [height], [width])',
33252
+ parameters: [
33253
+ { name: 'reference', description: 'Starting cell' },
33254
+ { name: 'rows', description: 'Rows to offset' },
33255
+ { name: 'cols', description: 'Columns to offset' },
33256
+ { name: 'height', description: 'Height of result', optional: true },
33257
+ { name: 'width', description: 'Width of result', optional: true },
33258
+ ],
33259
+ example: '=OFFSET(A1, 2, 3)',
33260
+ },
33261
+ {
33262
+ name: 'INDIRECT',
33263
+ category: 'Lookup',
33264
+ description: 'Returns the reference specified by a text string',
33265
+ syntax: 'INDIRECT(ref_text, [a1])',
33266
+ parameters: [
33267
+ { name: 'ref_text', description: 'Reference as text' },
33268
+ { name: 'a1', description: 'TRUE for A1 style, FALSE for R1C1', optional: true },
33269
+ ],
33270
+ example: '=INDIRECT("A" & B1)',
33271
+ },
33272
+ // ==================== TEXT ====================
33273
+ {
33274
+ name: 'CONCATENATE',
33275
+ category: 'Text',
33276
+ description: 'Joins several text strings into one',
33277
+ syntax: 'CONCATENATE(text1, [text2], ...)',
33278
+ parameters: [
33279
+ { name: 'text1', description: 'First text' },
33280
+ { name: 'text2', description: 'Additional text', optional: true },
33281
+ ],
33282
+ example: '=CONCATENATE(A1, " ", B1)',
33283
+ },
33284
+ {
33285
+ name: 'CONCAT',
33286
+ category: 'Text',
33287
+ description: 'Joins text from multiple ranges',
33288
+ syntax: 'CONCAT(text1, [text2], ...)',
33289
+ parameters: [
33290
+ { name: 'text1', description: 'First text or range' },
33291
+ { name: 'text2', description: 'Additional text or ranges', optional: true },
33292
+ ],
33293
+ example: '=CONCAT(A1:A5)',
33294
+ },
33295
+ {
33296
+ name: 'LEFT',
33297
+ category: 'Text',
33298
+ description: 'Returns the leftmost characters from a text string',
33299
+ syntax: 'LEFT(text, [num_chars])',
33300
+ parameters: [
33301
+ { name: 'text', description: 'Text string' },
33302
+ { name: 'num_chars', description: 'Number of characters', optional: true },
33303
+ ],
33304
+ example: '=LEFT(A1, 5)',
33305
+ },
33306
+ {
33307
+ name: 'RIGHT',
33308
+ category: 'Text',
33309
+ description: 'Returns the rightmost characters from a text string',
33310
+ syntax: 'RIGHT(text, [num_chars])',
33311
+ parameters: [
33312
+ { name: 'text', description: 'Text string' },
33313
+ { name: 'num_chars', description: 'Number of characters', optional: true },
33314
+ ],
33315
+ example: '=RIGHT(A1, 5)',
33316
+ },
33317
+ {
33318
+ name: 'MID',
33319
+ category: 'Text',
33320
+ description: 'Returns characters from the middle of a text string',
33321
+ syntax: 'MID(text, start_num, num_chars)',
33322
+ parameters: [
33323
+ { name: 'text', description: 'Text string' },
33324
+ { name: 'start_num', description: 'Starting position' },
33325
+ { name: 'num_chars', description: 'Number of characters' },
33326
+ ],
33327
+ example: '=MID(A1, 3, 5)',
33328
+ },
33329
+ {
33330
+ name: 'LEN',
33331
+ category: 'Text',
33332
+ description: 'Returns the number of characters in a text string',
33333
+ syntax: 'LEN(text)',
33334
+ parameters: [{ name: 'text', description: 'Text string' }],
33335
+ example: '=LEN(A1)',
33336
+ },
33337
+ {
33338
+ name: 'UPPER',
33339
+ category: 'Text',
33340
+ description: 'Converts text to uppercase',
33341
+ syntax: 'UPPER(text)',
33342
+ parameters: [{ name: 'text', description: 'Text to convert' }],
33343
+ example: '=UPPER(A1)',
33344
+ },
33345
+ {
33346
+ name: 'LOWER',
33347
+ category: 'Text',
33348
+ description: 'Converts text to lowercase',
33349
+ syntax: 'LOWER(text)',
33350
+ parameters: [{ name: 'text', description: 'Text to convert' }],
33351
+ example: '=LOWER(A1)',
33352
+ },
33353
+ {
33354
+ name: 'PROPER',
33355
+ category: 'Text',
33356
+ description: 'Capitalizes the first letter of each word',
33357
+ syntax: 'PROPER(text)',
33358
+ parameters: [{ name: 'text', description: 'Text to convert' }],
33359
+ example: '=PROPER(A1)',
33360
+ },
33361
+ {
33362
+ name: 'TRIM',
33363
+ category: 'Text',
33364
+ description: 'Removes extra spaces from text',
33365
+ syntax: 'TRIM(text)',
33366
+ parameters: [{ name: 'text', description: 'Text to trim' }],
33367
+ example: '=TRIM(A1)',
33368
+ },
33369
+ {
33370
+ name: 'CLEAN',
33371
+ category: 'Text',
33372
+ description: 'Removes non-printable characters from text',
33373
+ syntax: 'CLEAN(text)',
33374
+ parameters: [{ name: 'text', description: 'Text to clean' }],
33375
+ example: '=CLEAN(A1)',
33376
+ },
33377
+ {
33378
+ name: 'FIND',
33379
+ category: 'Text',
33380
+ description: 'Finds one text string within another (case-sensitive)',
33381
+ syntax: 'FIND(find_text, within_text, [start_num])',
33382
+ parameters: [
33383
+ { name: 'find_text', description: 'Text to find' },
33384
+ { name: 'within_text', description: 'Text to search in' },
33385
+ { name: 'start_num', description: 'Starting position', optional: true },
33386
+ ],
33387
+ example: '=FIND("@", A1)',
33388
+ },
33389
+ {
33390
+ name: 'SEARCH',
33391
+ category: 'Text',
33392
+ description: 'Finds one text string within another (case-insensitive)',
33393
+ syntax: 'SEARCH(find_text, within_text, [start_num])',
33394
+ parameters: [
33395
+ { name: 'find_text', description: 'Text to find' },
33396
+ { name: 'within_text', description: 'Text to search in' },
33397
+ { name: 'start_num', description: 'Starting position', optional: true },
33398
+ ],
33399
+ example: '=SEARCH("test", A1)',
33400
+ },
33401
+ {
33402
+ name: 'REPLACE',
33403
+ category: 'Text',
33404
+ description: 'Replaces part of a text string with different text',
33405
+ syntax: 'REPLACE(old_text, start_num, num_chars, new_text)',
33406
+ parameters: [
33407
+ { name: 'old_text', description: 'Original text' },
33408
+ { name: 'start_num', description: 'Starting position' },
33409
+ { name: 'num_chars', description: 'Number of characters to replace' },
33410
+ { name: 'new_text', description: 'Replacement text' },
33411
+ ],
33412
+ example: '=REPLACE(A1, 1, 3, "New")',
33413
+ },
33414
+ {
33415
+ name: 'SUBSTITUTE',
33416
+ category: 'Text',
33417
+ description: 'Substitutes new text for old text in a string',
33418
+ syntax: 'SUBSTITUTE(text, old_text, new_text, [instance_num])',
33419
+ parameters: [
33420
+ { name: 'text', description: 'Original text' },
33421
+ { name: 'old_text', description: 'Text to replace' },
33422
+ { name: 'new_text', description: 'Replacement text' },
33423
+ { name: 'instance_num', description: 'Which occurrence to replace', optional: true },
33424
+ ],
33425
+ example: '=SUBSTITUTE(A1, "old", "new")',
33426
+ },
33427
+ {
33428
+ name: 'TEXT',
33429
+ category: 'Text',
33430
+ description: 'Formats a number as text with a specified format',
33431
+ syntax: 'TEXT(value, format_text)',
33432
+ parameters: [
33433
+ { name: 'value', description: 'Value to format' },
33434
+ { name: 'format_text', description: 'Format code' },
33435
+ ],
33436
+ example: '=TEXT(A1, "$#,##0.00")',
33437
+ },
33438
+ {
33439
+ name: 'VALUE',
33440
+ category: 'Text',
33441
+ description: 'Converts a text string that represents a number to a number',
33442
+ syntax: 'VALUE(text)',
33443
+ parameters: [{ name: 'text', description: 'Text to convert' }],
33444
+ example: '=VALUE("123.45")',
33445
+ },
33446
+ {
33447
+ name: 'REPT',
33448
+ category: 'Text',
33449
+ description: 'Repeats text a specified number of times',
33450
+ syntax: 'REPT(text, number_times)',
33451
+ parameters: [
33452
+ { name: 'text', description: 'Text to repeat' },
33453
+ { name: 'number_times', description: 'Number of repetitions' },
33454
+ ],
33455
+ example: '=REPT("*", 5)',
33456
+ },
33457
+ {
33458
+ name: 'EXACT',
33459
+ category: 'Text',
33460
+ description: 'Checks if two text strings are exactly the same',
33461
+ syntax: 'EXACT(text1, text2)',
33462
+ parameters: [
33463
+ { name: 'text1', description: 'First text' },
33464
+ { name: 'text2', description: 'Second text' },
33465
+ ],
33466
+ example: '=EXACT(A1, B1)',
33467
+ },
33468
+ // ==================== LOGICAL ====================
33469
+ {
33470
+ name: 'IF',
33471
+ category: 'Logical',
33472
+ description: 'Returns one value if a condition is true, another if false',
33473
+ syntax: 'IF(logical_test, value_if_true, [value_if_false])',
33474
+ parameters: [
33475
+ { name: 'logical_test', description: 'Condition to test' },
33476
+ { name: 'value_if_true', description: 'Value if condition is true' },
33477
+ { name: 'value_if_false', description: 'Value if condition is false', optional: true },
33478
+ ],
33479
+ example: '=IF(A1>10, "High", "Low")',
33480
+ },
33481
+ {
33482
+ name: 'IFS',
33483
+ category: 'Logical',
33484
+ description: 'Checks multiple conditions and returns the first TRUE result',
33485
+ syntax: 'IFS(condition1, value1, [condition2, value2], ...)',
33486
+ parameters: [
33487
+ { name: 'condition1', description: 'First condition' },
33488
+ { name: 'value1', description: 'Value if first condition is true' },
33489
+ { name: 'condition2', description: 'Second condition', optional: true },
33490
+ { name: 'value2', description: 'Value if second condition is true', optional: true },
33491
+ ],
33492
+ example: '=IFS(A1>90, "A", A1>80, "B", A1>70, "C")',
33493
+ },
33494
+ {
33495
+ name: 'AND',
33496
+ category: 'Logical',
33497
+ description: 'Returns TRUE if all arguments are true',
33498
+ syntax: 'AND(logical1, [logical2], ...)',
33499
+ parameters: [
33500
+ { name: 'logical1', description: 'First condition' },
33501
+ { name: 'logical2', description: 'Additional conditions', optional: true },
33502
+ ],
33503
+ example: '=AND(A1>5, A1<10)',
33504
+ },
33505
+ {
33506
+ name: 'OR',
33507
+ category: 'Logical',
33508
+ description: 'Returns TRUE if any argument is true',
33509
+ syntax: 'OR(logical1, [logical2], ...)',
33510
+ parameters: [
33511
+ { name: 'logical1', description: 'First condition' },
33512
+ { name: 'logical2', description: 'Additional conditions', optional: true },
33513
+ ],
33514
+ example: '=OR(A1="Yes", A1="Y")',
33515
+ },
33516
+ {
33517
+ name: 'NOT',
33518
+ category: 'Logical',
33519
+ description: 'Reverses the logic of its argument',
33520
+ syntax: 'NOT(logical)',
33521
+ parameters: [{ name: 'logical', description: 'Value to reverse' }],
33522
+ example: '=NOT(A1>10)',
33523
+ },
33524
+ {
33525
+ name: 'XOR',
33526
+ category: 'Logical',
33527
+ description: 'Returns TRUE if an odd number of arguments are true',
33528
+ syntax: 'XOR(logical1, [logical2], ...)',
33529
+ parameters: [
33530
+ { name: 'logical1', description: 'First condition' },
33531
+ { name: 'logical2', description: 'Additional conditions', optional: true },
33532
+ ],
33533
+ example: '=XOR(A1>5, B1>5)',
33534
+ },
33535
+ {
33536
+ name: 'IFERROR',
33537
+ category: 'Logical',
33538
+ description: 'Returns a value if an expression results in an error',
33539
+ syntax: 'IFERROR(value, value_if_error)',
33540
+ parameters: [
33541
+ { name: 'value', description: 'Expression to check' },
33542
+ { name: 'value_if_error', description: 'Value to return on error' },
33543
+ ],
33544
+ example: '=IFERROR(A1/B1, 0)',
33545
+ },
33546
+ {
33547
+ name: 'IFNA',
33548
+ category: 'Logical',
33549
+ description: 'Returns a value if an expression results in #N/A',
33550
+ syntax: 'IFNA(value, value_if_na)',
33551
+ parameters: [
33552
+ { name: 'value', description: 'Expression to check' },
33553
+ { name: 'value_if_na', description: 'Value to return if #N/A' },
33554
+ ],
33555
+ example: '=IFNA(VLOOKUP(A1, B:C, 2, FALSE), "Not found")',
33556
+ },
33557
+ {
33558
+ name: 'TRUE',
33559
+ category: 'Logical',
33560
+ description: 'Returns the logical value TRUE',
33561
+ syntax: 'TRUE()',
33562
+ parameters: [],
33563
+ example: '=TRUE()',
33564
+ },
33565
+ {
33566
+ name: 'FALSE',
33567
+ category: 'Logical',
33568
+ description: 'Returns the logical value FALSE',
33569
+ syntax: 'FALSE()',
33570
+ parameters: [],
33571
+ example: '=FALSE()',
33572
+ },
33573
+ {
33574
+ name: 'SWITCH',
33575
+ category: 'Logical',
33576
+ description: 'Evaluates an expression against a list of values',
33577
+ syntax: 'SWITCH(expression, value1, result1, [value2, result2], ..., [default])',
33578
+ parameters: [
33579
+ { name: 'expression', description: 'Value to compare' },
33580
+ { name: 'value1', description: 'First value to match' },
33581
+ { name: 'result1', description: 'Result if first value matches' },
33582
+ { name: 'default', description: 'Default result if no match', optional: true },
33583
+ ],
33584
+ example: '=SWITCH(A1, 1, "One", 2, "Two", "Other")',
33585
+ },
33586
+ // ==================== DATE ====================
33587
+ {
33588
+ name: 'DATE',
33589
+ category: 'Date',
33590
+ description: 'Creates a date from year, month, and day',
33591
+ syntax: 'DATE(year, month, day)',
33592
+ parameters: [
33593
+ { name: 'year', description: 'Year number' },
33594
+ { name: 'month', description: 'Month number (1-12)' },
33595
+ { name: 'day', description: 'Day number' },
33596
+ ],
33597
+ example: '=DATE(2025, 1, 15)',
33598
+ },
33599
+ {
33600
+ name: 'TODAY',
33601
+ category: 'Date',
33602
+ description: 'Returns the current date',
33603
+ syntax: 'TODAY()',
33604
+ parameters: [],
33605
+ example: '=TODAY()',
33606
+ },
33607
+ {
33608
+ name: 'NOW',
33609
+ category: 'Date',
33610
+ description: 'Returns the current date and time',
33611
+ syntax: 'NOW()',
33612
+ parameters: [],
33613
+ example: '=NOW()',
33614
+ },
33615
+ {
33616
+ name: 'YEAR',
33617
+ category: 'Date',
33618
+ description: 'Returns the year of a date',
33619
+ syntax: 'YEAR(serial_number)',
33620
+ parameters: [{ name: 'serial_number', description: 'Date value' }],
33621
+ example: '=YEAR(A1)',
33622
+ },
33623
+ {
33624
+ name: 'MONTH',
33625
+ category: 'Date',
33626
+ description: 'Returns the month of a date (1-12)',
33627
+ syntax: 'MONTH(serial_number)',
33628
+ parameters: [{ name: 'serial_number', description: 'Date value' }],
33629
+ example: '=MONTH(A1)',
33630
+ },
33631
+ {
33632
+ name: 'DAY',
33633
+ category: 'Date',
33634
+ description: 'Returns the day of a date (1-31)',
33635
+ syntax: 'DAY(serial_number)',
33636
+ parameters: [{ name: 'serial_number', description: 'Date value' }],
33637
+ example: '=DAY(A1)',
33638
+ },
33639
+ {
33640
+ name: 'HOUR',
33641
+ category: 'Date',
33642
+ description: 'Returns the hour of a time (0-23)',
33643
+ syntax: 'HOUR(serial_number)',
33644
+ parameters: [{ name: 'serial_number', description: 'Time value' }],
33645
+ example: '=HOUR(A1)',
33646
+ },
33647
+ {
33648
+ name: 'MINUTE',
33649
+ category: 'Date',
33650
+ description: 'Returns the minute of a time (0-59)',
33651
+ syntax: 'MINUTE(serial_number)',
33652
+ parameters: [{ name: 'serial_number', description: 'Time value' }],
33653
+ example: '=MINUTE(A1)',
33654
+ },
33655
+ {
33656
+ name: 'SECOND',
33657
+ category: 'Date',
33658
+ description: 'Returns the second of a time (0-59)',
33659
+ syntax: 'SECOND(serial_number)',
33660
+ parameters: [{ name: 'serial_number', description: 'Time value' }],
33661
+ example: '=SECOND(A1)',
33662
+ },
33663
+ {
33664
+ name: 'WEEKDAY',
33665
+ category: 'Date',
33666
+ description: 'Returns the day of the week (1-7)',
33667
+ syntax: 'WEEKDAY(serial_number, [return_type])',
33668
+ parameters: [
33669
+ { name: 'serial_number', description: 'Date value' },
33670
+ { name: 'return_type', description: 'Number system to use', optional: true },
33671
+ ],
33672
+ example: '=WEEKDAY(A1)',
33673
+ },
33674
+ {
33675
+ name: 'WEEKNUM',
33676
+ category: 'Date',
33677
+ description: 'Returns the week number of the year',
33678
+ syntax: 'WEEKNUM(serial_number, [return_type])',
33679
+ parameters: [
33680
+ { name: 'serial_number', description: 'Date value' },
33681
+ { name: 'return_type', description: 'Week calculation method', optional: true },
33682
+ ],
33683
+ example: '=WEEKNUM(A1)',
33684
+ },
33685
+ {
33686
+ name: 'DATEDIF',
33687
+ category: 'Date',
33688
+ description: 'Calculates the difference between two dates',
33689
+ syntax: 'DATEDIF(start_date, end_date, unit)',
33690
+ parameters: [
33691
+ { name: 'start_date', description: 'Start date' },
33692
+ { name: 'end_date', description: 'End date' },
33693
+ { name: 'unit', description: '"Y", "M", "D", "YM", "YD", or "MD"' },
33694
+ ],
33695
+ example: '=DATEDIF(A1, B1, "D")',
33696
+ },
33697
+ {
33698
+ name: 'EDATE',
33699
+ category: 'Date',
33700
+ description: 'Returns a date a specified number of months before or after',
33701
+ syntax: 'EDATE(start_date, months)',
33702
+ parameters: [
33703
+ { name: 'start_date', description: 'Starting date' },
33704
+ { name: 'months', description: 'Number of months' },
33705
+ ],
33706
+ example: '=EDATE(A1, 3)',
33707
+ },
33708
+ {
33709
+ name: 'EOMONTH',
33710
+ category: 'Date',
33711
+ description: 'Returns the last day of the month, months before or after',
33712
+ syntax: 'EOMONTH(start_date, months)',
33713
+ parameters: [
33714
+ { name: 'start_date', description: 'Starting date' },
33715
+ { name: 'months', description: 'Number of months' },
33716
+ ],
33717
+ example: '=EOMONTH(A1, 0)',
33718
+ },
33719
+ {
33720
+ name: 'NETWORKDAYS',
33721
+ category: 'Date',
33722
+ description: 'Returns the number of working days between two dates',
33723
+ syntax: 'NETWORKDAYS(start_date, end_date, [holidays])',
33724
+ parameters: [
33725
+ { name: 'start_date', description: 'Start date' },
33726
+ { name: 'end_date', description: 'End date' },
33727
+ { name: 'holidays', description: 'Range of holiday dates', optional: true },
33728
+ ],
33729
+ example: '=NETWORKDAYS(A1, B1)',
33730
+ },
33731
+ {
33732
+ name: 'WORKDAY',
33733
+ category: 'Date',
33734
+ description: 'Returns a date a specified number of workdays before or after',
33735
+ syntax: 'WORKDAY(start_date, days, [holidays])',
33736
+ parameters: [
33737
+ { name: 'start_date', description: 'Starting date' },
33738
+ { name: 'days', description: 'Number of workdays' },
33739
+ { name: 'holidays', description: 'Range of holiday dates', optional: true },
33740
+ ],
33741
+ example: '=WORKDAY(A1, 10)',
33742
+ },
33743
+ {
33744
+ name: 'TIME',
33745
+ category: 'Date',
33746
+ description: 'Creates a time from hour, minute, and second',
33747
+ syntax: 'TIME(hour, minute, second)',
33748
+ parameters: [
33749
+ { name: 'hour', description: 'Hour (0-23)' },
33750
+ { name: 'minute', description: 'Minute (0-59)' },
33751
+ { name: 'second', description: 'Second (0-59)' },
33752
+ ],
33753
+ example: '=TIME(14, 30, 0)',
33754
+ },
33755
+ // ==================== INFORMATION ====================
33756
+ {
33757
+ name: 'ISBLANK',
33758
+ category: 'Information',
33759
+ description: 'Returns TRUE if the cell is empty',
33760
+ syntax: 'ISBLANK(value)',
33761
+ parameters: [{ name: 'value', description: 'Cell to check' }],
33762
+ example: '=ISBLANK(A1)',
33763
+ },
33764
+ {
33765
+ name: 'ISNUMBER',
33766
+ category: 'Information',
33767
+ description: 'Returns TRUE if the value is a number',
33768
+ syntax: 'ISNUMBER(value)',
33769
+ parameters: [{ name: 'value', description: 'Value to check' }],
33770
+ example: '=ISNUMBER(A1)',
33771
+ },
33772
+ {
33773
+ name: 'ISTEXT',
33774
+ category: 'Information',
33775
+ description: 'Returns TRUE if the value is text',
33776
+ syntax: 'ISTEXT(value)',
33777
+ parameters: [{ name: 'value', description: 'Value to check' }],
33778
+ example: '=ISTEXT(A1)',
33779
+ },
33780
+ {
33781
+ name: 'ISERROR',
33782
+ category: 'Information',
33783
+ description: 'Returns TRUE if the value is any error',
33784
+ syntax: 'ISERROR(value)',
33785
+ parameters: [{ name: 'value', description: 'Value to check' }],
33786
+ example: '=ISERROR(A1)',
33787
+ },
33788
+ {
33789
+ name: 'ISNA',
33790
+ category: 'Information',
33791
+ description: 'Returns TRUE if the value is #N/A',
33792
+ syntax: 'ISNA(value)',
33793
+ parameters: [{ name: 'value', description: 'Value to check' }],
33794
+ example: '=ISNA(A1)',
33795
+ },
33796
+ {
33797
+ name: 'ISLOGICAL',
33798
+ category: 'Information',
33799
+ description: 'Returns TRUE if the value is a logical value',
33800
+ syntax: 'ISLOGICAL(value)',
33801
+ parameters: [{ name: 'value', description: 'Value to check' }],
33802
+ example: '=ISLOGICAL(A1)',
33803
+ },
33804
+ {
33805
+ name: 'ISEVEN',
33806
+ category: 'Information',
33807
+ description: 'Returns TRUE if the number is even',
33808
+ syntax: 'ISEVEN(number)',
33809
+ parameters: [{ name: 'number', description: 'Number to check' }],
33810
+ example: '=ISEVEN(A1)',
33811
+ },
33812
+ {
33813
+ name: 'ISODD',
33814
+ category: 'Information',
33815
+ description: 'Returns TRUE if the number is odd',
33816
+ syntax: 'ISODD(number)',
33817
+ parameters: [{ name: 'number', description: 'Number to check' }],
33818
+ example: '=ISODD(A1)',
33819
+ },
33820
+ {
33821
+ name: 'TYPE',
33822
+ category: 'Information',
33823
+ description: 'Returns a number indicating the data type',
33824
+ syntax: 'TYPE(value)',
33825
+ parameters: [{ name: 'value', description: 'Value to check' }],
33826
+ example: '=TYPE(A1)',
33827
+ },
33828
+ {
33829
+ name: 'N',
33830
+ category: 'Information',
33831
+ description: 'Returns a value converted to a number',
33832
+ syntax: 'N(value)',
33833
+ parameters: [{ name: 'value', description: 'Value to convert' }],
33834
+ example: '=N(A1)',
33835
+ },
33836
+ {
33837
+ name: 'NA',
33838
+ category: 'Information',
33839
+ description: 'Returns the error value #N/A',
33840
+ syntax: 'NA()',
33841
+ parameters: [],
33842
+ example: '=NA()',
33843
+ },
33844
+ // ==================== FINANCIAL ====================
33845
+ {
33846
+ name: 'PMT',
33847
+ category: 'Financial',
33848
+ description: 'Calculates the payment for a loan',
33849
+ syntax: 'PMT(rate, nper, pv, [fv], [type])',
33850
+ parameters: [
33851
+ { name: 'rate', description: 'Interest rate per period' },
33852
+ { name: 'nper', description: 'Total number of payments' },
33853
+ { name: 'pv', description: 'Present value (loan amount)' },
33854
+ { name: 'fv', description: 'Future value', optional: true },
33855
+ { name: 'type', description: '0 = end of period, 1 = beginning', optional: true },
33856
+ ],
33857
+ example: '=PMT(0.05/12, 60, 10000)',
33858
+ },
33859
+ {
33860
+ name: 'PV',
33861
+ category: 'Financial',
33862
+ description: 'Returns the present value of an investment',
33863
+ syntax: 'PV(rate, nper, pmt, [fv], [type])',
33864
+ parameters: [
33865
+ { name: 'rate', description: 'Interest rate per period' },
33866
+ { name: 'nper', description: 'Total number of periods' },
33867
+ { name: 'pmt', description: 'Payment per period' },
33868
+ { name: 'fv', description: 'Future value', optional: true },
33869
+ { name: 'type', description: '0 = end, 1 = beginning', optional: true },
33870
+ ],
33871
+ example: '=PV(0.05/12, 60, -100)',
33872
+ },
33873
+ {
33874
+ name: 'FV',
33875
+ category: 'Financial',
33876
+ description: 'Returns the future value of an investment',
33877
+ syntax: 'FV(rate, nper, pmt, [pv], [type])',
33878
+ parameters: [
33879
+ { name: 'rate', description: 'Interest rate per period' },
33880
+ { name: 'nper', description: 'Total number of periods' },
33881
+ { name: 'pmt', description: 'Payment per period' },
33882
+ { name: 'pv', description: 'Present value', optional: true },
33883
+ { name: 'type', description: '0 = end, 1 = beginning', optional: true },
33884
+ ],
33885
+ example: '=FV(0.05/12, 60, -100)',
33886
+ },
33887
+ {
33888
+ name: 'NPV',
33889
+ category: 'Financial',
33890
+ description: 'Returns the net present value of an investment',
33891
+ syntax: 'NPV(rate, value1, [value2], ...)',
33892
+ parameters: [
33893
+ { name: 'rate', description: 'Discount rate' },
33894
+ { name: 'value1', description: 'First cash flow' },
33895
+ { name: 'value2', description: 'Additional cash flows', optional: true },
33896
+ ],
33897
+ example: '=NPV(0.1, -10000, 3000, 4200, 6800)',
33898
+ },
33899
+ {
33900
+ name: 'IRR',
33901
+ category: 'Financial',
33902
+ description: 'Returns the internal rate of return',
33903
+ syntax: 'IRR(values, [guess])',
33904
+ parameters: [
33905
+ { name: 'values', description: 'Range of cash flows' },
33906
+ { name: 'guess', description: 'Initial guess for rate', optional: true },
33907
+ ],
33908
+ example: '=IRR(A1:A5)',
33909
+ },
33910
+ {
33911
+ name: 'RATE',
33912
+ category: 'Financial',
33913
+ description: 'Returns the interest rate per period',
33914
+ syntax: 'RATE(nper, pmt, pv, [fv], [type], [guess])',
33915
+ parameters: [
33916
+ { name: 'nper', description: 'Total number of periods' },
33917
+ { name: 'pmt', description: 'Payment per period' },
33918
+ { name: 'pv', description: 'Present value' },
33919
+ { name: 'fv', description: 'Future value', optional: true },
33920
+ { name: 'type', description: '0 = end, 1 = beginning', optional: true },
33921
+ { name: 'guess', description: 'Initial guess', optional: true },
33922
+ ],
33923
+ example: '=RATE(60, -100, 5000)',
33924
+ },
33925
+ {
33926
+ name: 'NPER',
33927
+ category: 'Financial',
33928
+ description: 'Returns the number of periods for an investment',
33929
+ syntax: 'NPER(rate, pmt, pv, [fv], [type])',
33930
+ parameters: [
33931
+ { name: 'rate', description: 'Interest rate per period' },
33932
+ { name: 'pmt', description: 'Payment per period' },
33933
+ { name: 'pv', description: 'Present value' },
33934
+ { name: 'fv', description: 'Future value', optional: true },
33935
+ { name: 'type', description: '0 = end, 1 = beginning', optional: true },
33936
+ ],
33937
+ example: '=NPER(0.05/12, -100, 5000)',
33938
+ },
33939
+ ];
33940
+ // Get all formula names
33941
+ const FORMULA_NAMES = FORMULA_DEFINITIONS.map((f) => f.name);
33942
+ // Get formulas by category
33943
+ const getFormulasByCategory = (category) => FORMULA_DEFINITIONS.filter((f) => f.category === category);
33944
+ // Search formulas by name prefix
33945
+ const searchFormulas = (query) => {
33946
+ const upperQuery = query.toUpperCase();
33947
+ return FORMULA_DEFINITIONS.filter((f) => f.name.startsWith(upperQuery));
33948
+ };
33949
+ // Get formula by exact name
33950
+ const getFormula = (name) => FORMULA_DEFINITIONS.find((f) => f.name === name.toUpperCase());
33951
+ // Get all categories
33952
+ const FORMULA_CATEGORIES = [
33953
+ 'Math',
33954
+ 'Statistical',
33955
+ 'Lookup',
33956
+ 'Text',
33957
+ 'Logical',
33958
+ 'Date',
33959
+ 'Information',
33960
+ 'Financial',
33961
+ ];
33962
+
33963
+ /**
33964
+ * FormulaAutocomplete - Input with formula intellisense
33965
+ *
33966
+ * Features:
33967
+ * - Autocomplete dropdown when typing after '='
33968
+ * - Function signature hints while typing parameters
33969
+ * - Category-based browsing
33970
+ * - Keyboard navigation
33971
+ */
33972
+ const FormulaAutocomplete = ({ value, onChange, onComplete, onCancel, anchorRect, autoFocus = true, className = '', }) => {
33973
+ const inputRef = useRef(null);
33974
+ const dropdownRef = useRef(null);
33975
+ const [selectedIndex, setSelectedIndex] = useState(0);
33976
+ const [showDropdown, setShowDropdown] = useState(false);
33977
+ const [showHint, setShowHint] = useState(false);
33978
+ const [hint, setHint] = useState(null);
33979
+ const [activeCategory, setActiveCategory] = useState(null);
33980
+ // Parse the current formula context
33981
+ const formulaContext = useMemo(() => {
33982
+ if (!value.startsWith('=')) {
33983
+ return { isFormula: false, query: '', inFunction: false, functionName: '', paramIndex: 0 };
33984
+ }
33985
+ const formulaText = value.substring(1);
33986
+ // Check if we're typing a function name (before opening paren)
33987
+ const functionMatch = formulaText.match(/^([A-Z]+)$/i);
33988
+ if (functionMatch) {
33989
+ return {
33990
+ isFormula: true,
33991
+ query: functionMatch[1].toUpperCase(),
33992
+ inFunction: false,
33993
+ functionName: '',
33994
+ paramIndex: 0,
33995
+ };
33996
+ }
33997
+ // Check if we're inside a function (after opening paren)
33998
+ const insideFunctionMatch = formulaText.match(/^([A-Z]+)\((.*)$/i);
33999
+ if (insideFunctionMatch) {
34000
+ const functionName = insideFunctionMatch[1].toUpperCase();
34001
+ const params = insideFunctionMatch[2];
34002
+ // Count commas to determine parameter index (accounting for nested parens)
34003
+ let paramIndex = 0;
34004
+ let parenDepth = 0;
34005
+ for (const char of params) {
34006
+ if (char === '(')
34007
+ parenDepth++;
34008
+ else if (char === ')')
34009
+ parenDepth--;
34010
+ else if (char === ',' && parenDepth === 0)
34011
+ paramIndex++;
34012
+ }
34013
+ return {
34014
+ isFormula: true,
34015
+ query: '',
34016
+ inFunction: true,
34017
+ functionName,
34018
+ paramIndex,
34019
+ };
34020
+ }
34021
+ // Just '=' or other expression
34022
+ return {
34023
+ isFormula: true,
34024
+ query: formulaText.toUpperCase(),
34025
+ inFunction: false,
34026
+ functionName: '',
34027
+ paramIndex: 0,
34028
+ };
34029
+ }, [value]);
34030
+ // Get matching formulas for dropdown
34031
+ const matchingFormulas = useMemo(() => {
34032
+ if (!formulaContext.isFormula || formulaContext.inFunction)
34033
+ return [];
34034
+ if (activeCategory) {
34035
+ const categoryFormulas = FORMULA_DEFINITIONS.filter(f => f.category === activeCategory);
34036
+ if (formulaContext.query) {
34037
+ return categoryFormulas.filter(f => f.name.startsWith(formulaContext.query));
34038
+ }
34039
+ return categoryFormulas;
34040
+ }
34041
+ if (formulaContext.query) {
34042
+ return searchFormulas(formulaContext.query);
34043
+ }
34044
+ // Show all formulas grouped by first letter when just '='
34045
+ return FORMULA_DEFINITIONS.slice(0, 20); // Show first 20 as default
34046
+ }, [formulaContext, activeCategory]);
34047
+ // Update hint when inside a function
34048
+ useEffect(() => {
34049
+ if (formulaContext.inFunction && formulaContext.functionName) {
34050
+ const formula = getFormula(formulaContext.functionName);
34051
+ if (formula) {
34052
+ setHint({
34053
+ formula,
34054
+ currentParamIndex: formulaContext.paramIndex,
34055
+ });
34056
+ setShowHint(true);
34057
+ setShowDropdown(false);
34058
+ }
34059
+ else {
34060
+ setShowHint(false);
34061
+ setHint(null);
34062
+ }
34063
+ }
34064
+ else if (formulaContext.isFormula && !formulaContext.inFunction) {
34065
+ setShowDropdown(true);
34066
+ setShowHint(false);
34067
+ setHint(null);
34068
+ }
34069
+ else {
34070
+ setShowDropdown(false);
34071
+ setShowHint(false);
34072
+ setHint(null);
34073
+ }
34074
+ }, [formulaContext]);
34075
+ // Reset selected index when matches change
34076
+ useEffect(() => {
34077
+ setSelectedIndex(0);
34078
+ }, [matchingFormulas.length]);
34079
+ // Auto-focus
34080
+ useEffect(() => {
34081
+ if (autoFocus && inputRef.current) {
34082
+ inputRef.current.focus();
34083
+ inputRef.current.select();
34084
+ }
34085
+ }, [autoFocus]);
34086
+ // Scroll selected item into view
34087
+ useEffect(() => {
34088
+ if (dropdownRef.current && showDropdown) {
34089
+ const selectedItem = dropdownRef.current.querySelector(`[data-index="${selectedIndex}"]`);
34090
+ if (selectedItem) {
34091
+ selectedItem.scrollIntoView({ block: 'nearest' });
34092
+ }
34093
+ }
34094
+ }, [selectedIndex, showDropdown]);
34095
+ // Handle keyboard navigation
34096
+ const handleKeyDown = useCallback((e) => {
34097
+ if (showDropdown && matchingFormulas.length > 0) {
34098
+ switch (e.key) {
34099
+ case 'ArrowDown':
34100
+ e.preventDefault();
34101
+ setSelectedIndex((prev) => Math.min(prev + 1, matchingFormulas.length - 1));
34102
+ break;
34103
+ case 'ArrowUp':
34104
+ e.preventDefault();
34105
+ setSelectedIndex((prev) => Math.max(prev - 1, 0));
34106
+ break;
34107
+ case 'Tab':
34108
+ case 'Enter':
34109
+ e.preventDefault();
34110
+ insertFormula(matchingFormulas[selectedIndex]);
34111
+ break;
34112
+ case 'Escape':
34113
+ e.preventDefault();
34114
+ if (showDropdown) {
34115
+ setShowDropdown(false);
34116
+ }
34117
+ else {
34118
+ onCancel();
34119
+ }
34120
+ break;
34121
+ }
34122
+ }
34123
+ else {
34124
+ if (e.key === 'Enter') {
34125
+ e.preventDefault();
34126
+ onComplete();
34127
+ }
34128
+ else if (e.key === 'Escape') {
34129
+ e.preventDefault();
34130
+ onCancel();
34131
+ }
34132
+ }
34133
+ }, [showDropdown, matchingFormulas, selectedIndex, onComplete, onCancel]);
34134
+ // Insert selected formula
34135
+ const insertFormula = useCallback((formula) => {
34136
+ // Replace the current query with the formula name and open paren
34137
+ const newValue = `=${formula.name}(`;
34138
+ onChange(newValue);
34139
+ setShowDropdown(false);
34140
+ inputRef.current?.focus();
34141
+ }, [onChange]);
34142
+ // Calculate dropdown position
34143
+ const dropdownPosition = useMemo(() => {
34144
+ if (!anchorRect)
34145
+ return { top: 0, left: 0 };
34146
+ return {
34147
+ top: anchorRect.bottom + 2,
34148
+ left: anchorRect.left,
34149
+ };
34150
+ }, [anchorRect]);
34151
+ // Prevent blur when clicking inside dropdown
34152
+ const handleDropdownMouseDown = useCallback((e) => {
34153
+ e.preventDefault(); // Prevents input from losing focus
34154
+ }, []);
34155
+ // Render parameter hint
34156
+ const renderHint = () => {
34157
+ if (!showHint || !hint)
34158
+ return null;
34159
+ return createPortal(jsxs("div", { className: "fixed z-[9999] bg-white border border-stone-200 rounded-lg shadow-lg p-3 max-w-md", style: {
34160
+ top: dropdownPosition.top,
34161
+ left: dropdownPosition.left,
34162
+ }, onMouseDown: handleDropdownMouseDown, children: [jsxs("div", { className: "font-mono text-sm mb-2", children: [jsx("span", { className: "text-primary-600 font-semibold", children: hint.formula.name }), jsx("span", { className: "text-ink-500", children: "(" }), hint.formula.parameters.map((param, idx) => (jsxs("span", { children: [idx > 0 && jsx("span", { className: "text-ink-500", children: ", " }), jsx("span", { className: `${idx === hint.currentParamIndex
34163
+ ? 'bg-primary-100 text-primary-700 px-1 rounded font-semibold'
34164
+ : param.optional
34165
+ ? 'text-ink-400'
34166
+ : 'text-ink-600'}`, children: param.optional ? `[${param.name}]` : param.name })] }, param.name))), jsx("span", { className: "text-ink-500", children: ")" })] }), jsx("div", { className: "text-xs text-ink-600 mb-2", children: hint.formula.description }), hint.formula.parameters[hint.currentParamIndex] && (jsxs("div", { className: "text-xs bg-paper-50 p-2 rounded border border-stone-100", children: [jsxs("span", { className: "font-semibold text-primary-600", children: [hint.formula.parameters[hint.currentParamIndex].name, ":"] }), ' ', jsx("span", { className: "text-ink-600", children: hint.formula.parameters[hint.currentParamIndex].description })] }))] }), document.body);
34167
+ };
34168
+ // Render autocomplete dropdown
34169
+ const renderDropdown = () => {
34170
+ if (!showDropdown || matchingFormulas.length === 0)
34171
+ return null;
34172
+ return createPortal(jsxs("div", { ref: dropdownRef, className: "fixed z-[9999] bg-white border border-stone-200 rounded-lg shadow-lg overflow-hidden", style: {
34173
+ top: dropdownPosition.top,
34174
+ left: dropdownPosition.left,
34175
+ minWidth: 320,
34176
+ maxWidth: 450,
34177
+ maxHeight: 300,
34178
+ }, onMouseDown: handleDropdownMouseDown, children: [jsxs("div", { className: "flex flex-wrap gap-1 p-2 border-b border-stone-100 bg-paper-50", children: [jsx("button", { className: `px-2 py-1 text-xs rounded ${activeCategory === null
34179
+ ? 'bg-primary-500 text-white'
34180
+ : 'bg-white text-ink-600 hover:bg-stone-100'}`, onClick: () => {
34181
+ setActiveCategory(null);
34182
+ inputRef.current?.focus();
34183
+ }, children: "All" }), FORMULA_CATEGORIES.map((cat) => (jsx("button", { className: `px-2 py-1 text-xs rounded ${activeCategory === cat
34184
+ ? 'bg-primary-500 text-white'
34185
+ : 'bg-white text-ink-600 hover:bg-stone-100'}`, onClick: () => {
34186
+ setActiveCategory(cat);
34187
+ inputRef.current?.focus();
34188
+ }, children: cat }, cat)))] }), jsx("div", { className: "overflow-y-auto", style: { maxHeight: 220 }, children: matchingFormulas.map((formula, index) => (jsxs("div", { "data-index": index, className: `px-3 py-2 cursor-pointer border-b border-stone-50 ${index === selectedIndex ? 'bg-primary-50' : 'hover:bg-paper-50'}`, onClick: () => insertFormula(formula), onMouseEnter: () => setSelectedIndex(index), children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "font-mono font-semibold text-primary-600 text-sm", children: formula.name }), jsx("span", { className: "text-xs text-ink-400 bg-stone-100 px-1.5 py-0.5 rounded", children: formula.category })] }), jsx("div", { className: "text-xs text-ink-500 mt-0.5 truncate", children: formula.description }), jsx("div", { className: "font-mono text-xs text-ink-400 mt-0.5", children: formula.syntax })] }, formula.name))) }), jsxs("div", { className: "px-3 py-1.5 bg-paper-50 border-t border-stone-100 text-xs text-ink-400", children: [jsx("span", { className: "font-medium", children: "\u2191\u2193" }), " navigate", jsx("span", { className: "mx-2", children: "\u00B7" }), jsx("span", { className: "font-medium", children: "Tab/Enter" }), " insert", jsx("span", { className: "mx-2", children: "\u00B7" }), jsx("span", { className: "font-medium", children: "Esc" }), " close"] })] }), document.body);
34189
+ };
34190
+ return (jsxs(Fragment, { children: [jsx("input", { ref: inputRef, type: "text", value: value, onChange: (e) => onChange(e.target.value), onKeyDown: handleKeyDown, onBlur: () => {
34191
+ // Delay to allow click on dropdown
34192
+ setTimeout(() => {
34193
+ setShowDropdown(false);
34194
+ setShowHint(false);
34195
+ }, 150);
34196
+ }, className: `w-full h-full border-none outline-none bg-transparent font-mono text-sm ${className}`, style: { margin: '-4px', padding: '4px' } }), renderDropdown(), renderHint()] }));
34197
+ };
34198
+
34199
+ // Handle both ESM default export and CommonJS module.exports
34200
+ // @ts-ignore
34201
+ const FormulaParser = FormulaParser$1 || FormulaParserModule;
34202
+ /**
34203
+ * Convert column index to Excel-style letter (0 = A, 1 = B, ..., 26 = AA)
34204
+ */
34205
+ const colIndexToLetter = (index) => {
34206
+ let result = '';
34207
+ let num = index;
34208
+ while (num >= 0) {
34209
+ result = String.fromCharCode((num % 26) + 65) + result;
34210
+ num = Math.floor(num / 26) - 1;
34211
+ }
34212
+ return result;
34213
+ };
34214
+ // Note: parseRef is available for future formula reference parsing
34215
+ // const parseRef = (ref: string): { row: number; col: number } | null => { ... }
34216
+ /**
34217
+ * DataGrid - Excel-like data grid component with formulas
34218
+ *
34219
+ * A grid-based spreadsheet component that provides:
34220
+ * - Cell-level editing with formula support (280+ Excel formulas)
34221
+ * - Sorting and filtering
34222
+ * - Frozen rows and columns
34223
+ * - Zebra striping
34224
+ * - CSV export
34225
+ * - Keyboard navigation
34226
+ *
34227
+ * Uses fast-formula-parser (MIT licensed) for formula evaluation.
34228
+ *
34229
+ * @example Basic usage
34230
+ * ```tsx
34231
+ * const columns = [
34232
+ * { key: 'name', header: 'Name' },
34233
+ * { key: 'q1', header: 'Q1', type: 'number' },
34234
+ * { key: 'q2', header: 'Q2', type: 'number' },
34235
+ * { key: 'total', header: 'Total', type: 'number' },
34236
+ * ];
34237
+ *
34238
+ * const data = [
34239
+ * [{ value: 'Widget A' }, { value: 100 }, { value: 150 }, { value: 0, formula: '=SUM(B1:C1)' }],
34240
+ * [{ value: 'Widget B' }, { value: 200 }, { value: 250 }, { value: 0, formula: '=SUM(B2:C2)' }],
34241
+ * ];
34242
+ *
34243
+ * <DataGrid
34244
+ * data={data}
34245
+ * columns={columns}
34246
+ * formulas
34247
+ * zebraStripes
34248
+ * frozenRows={1}
34249
+ * />
34250
+ * ```
34251
+ */
34252
+ const DataGrid = forwardRef(({ data: initialData, columns, onChange, rowHeaders = false, frozenRows: frozenRowsProp = 'none', frozenColumns = 0, showFreezeRowToggle = false, zebraStripes = false, formulas = false, readOnly = false, height = 400, width = '100%', showToolbar = false, title, enableExport = false, exportFileName = 'export.csv', enableSave = false, onSave, toolbarActions, className = '', density = 'normal', }, ref) => {
34253
+ // State
34254
+ const [data, setData] = useState(initialData);
34255
+ const [editingCell, setEditingCell] = useState(null);
34256
+ const [editValue, setEditValue] = useState('');
34257
+ const [sortConfig, setSortConfig] = useState(null);
34258
+ const [filters, setFilters] = useState([]);
34259
+ const [activeFilter, setActiveFilter] = useState(null);
34260
+ const [filterValue, setFilterValue] = useState('');
34261
+ const [isSaving, setIsSaving] = useState(false);
34262
+ const [selectedCell, setSelectedCell] = useState(null);
34263
+ const [frozenRowsState, setFrozenRowsState] = useState(frozenRowsProp);
34264
+ const [editingCellRect, setEditingCellRect] = useState(null);
34265
+ // Update frozen rows when prop changes
34266
+ useEffect(() => {
34267
+ setFrozenRowsState(frozenRowsProp);
34268
+ }, [frozenRowsProp]);
34269
+ const tableRef = useRef(null);
34270
+ const inputRef = useRef(null);
34271
+ // Compute actual number of frozen rows based on mode
34272
+ const frozenRows = useMemo(() => {
34273
+ if (frozenRowsState === 'none')
34274
+ return 0;
34275
+ if (frozenRowsState === 'first')
34276
+ return 1;
34277
+ if (frozenRowsState === 'selected') {
34278
+ // Return selected row + 1 (to include it), or 0 if nothing selected
34279
+ return selectedCell ? selectedCell.row + 1 : 0;
34280
+ }
34281
+ if (typeof frozenRowsState === 'number')
34282
+ return frozenRowsState;
34283
+ return 0;
34284
+ }, [frozenRowsState, selectedCell]);
34285
+ // Check if a specific row is frozen
34286
+ const isRowFrozen = useCallback((rowIndex) => {
34287
+ if (frozenRowsState === 'none')
34288
+ return false;
34289
+ if (frozenRowsState === 'first')
34290
+ return rowIndex === 0;
34291
+ if (frozenRowsState === 'selected') {
34292
+ return selectedCell ? rowIndex === selectedCell.row : false;
34293
+ }
34294
+ if (typeof frozenRowsState === 'number')
34295
+ return rowIndex < frozenRowsState;
34296
+ return false;
34297
+ }, [frozenRowsState, selectedCell]);
34298
+ // Update data when initialData changes
34299
+ useEffect(() => {
34300
+ setData(initialData);
34301
+ }, [initialData]);
34302
+ // Get computed data with formulas evaluated
34303
+ // Uses a cache to handle formula dependencies (formulas referencing other formulas)
34304
+ const computedData = useMemo(() => {
34305
+ if (!formulas)
34306
+ return data;
34307
+ // Cache for computed cell values to handle dependencies
34308
+ const computedCache = new Map();
34309
+ // Recursive function to get cell value, evaluating formulas as needed
34310
+ const getCellValue = (r, c) => {
34311
+ const cacheKey = `${r},${c}`;
34312
+ if (computedCache.has(cacheKey)) {
34313
+ return computedCache.get(cacheKey);
34314
+ }
34315
+ if (r < 0 || r >= data.length || c < 0 || c >= (data[r]?.length || 0)) {
34316
+ return null;
34317
+ }
34318
+ const cell = data[r][c];
34319
+ if (!cell?.formula) {
34320
+ return cell?.value ?? null;
34321
+ }
34322
+ // Mark as computing to detect circular references
34323
+ computedCache.set(cacheKey, '#CIRCULAR');
34324
+ try {
34325
+ const result = parser.parse(cell.formula.substring(1));
34326
+ computedCache.set(cacheKey, result);
34327
+ return result;
34328
+ }
34329
+ catch (error) {
34330
+ computedCache.set(cacheKey, '#ERROR');
34331
+ return '#ERROR';
34332
+ }
34333
+ };
34334
+ // Create parser with callbacks that resolve formula dependencies
34335
+ const parser = new FormulaParser({
34336
+ onCell: ({ row, col }) => {
34337
+ // row and col are 1-indexed in the parser
34338
+ return getCellValue(row - 1, col - 1);
34339
+ },
34340
+ onRange: ({ from, to }) => {
34341
+ const result = [];
34342
+ for (let r = from.row - 1; r <= to.row - 1; r++) {
34343
+ const rowData = [];
34344
+ for (let c = from.col - 1; c <= to.col - 1; c++) {
34345
+ rowData.push(getCellValue(r, c));
34346
+ }
34347
+ result.push(rowData);
34348
+ }
34349
+ return result;
34350
+ },
34351
+ });
34352
+ // Compute all cells
34353
+ return data.map((row, rowIndex) => row.map((cell, colIndex) => {
34354
+ if (cell?.formula) {
34355
+ return {
34356
+ ...cell,
34357
+ value: getCellValue(rowIndex, colIndex),
34358
+ };
34359
+ }
34360
+ return cell;
34361
+ }));
34362
+ }, [formulas, data]);
34363
+ // Apply sorting
34364
+ const sortedData = useMemo(() => {
34365
+ if (!sortConfig)
34366
+ return computedData;
34367
+ const colIndex = columns.findIndex((c) => c.key === sortConfig.key);
34368
+ if (colIndex === -1)
34369
+ return computedData;
34370
+ // Keep frozen rows at top
34371
+ const frozenData = computedData.slice(0, frozenRows);
34372
+ const sortableData = [...computedData.slice(frozenRows)];
34373
+ sortableData.sort((a, b) => {
34374
+ const aVal = a[colIndex]?.value ?? '';
34375
+ const bVal = b[colIndex]?.value ?? '';
34376
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
34377
+ return sortConfig.direction === 'asc' ? aVal - bVal : bVal - aVal;
34378
+ }
34379
+ const aStr = String(aVal).toLowerCase();
34380
+ const bStr = String(bVal).toLowerCase();
34381
+ const cmp = aStr.localeCompare(bStr);
34382
+ return sortConfig.direction === 'asc' ? cmp : -cmp;
34383
+ });
34384
+ return [...frozenData, ...sortableData];
34385
+ }, [computedData, sortConfig, columns, frozenRows]);
34386
+ // Apply filters
34387
+ const filteredData = useMemo(() => {
34388
+ if (filters.length === 0)
34389
+ return sortedData;
34390
+ // Keep frozen rows
34391
+ const frozenData = sortedData.slice(0, frozenRows);
34392
+ const filterableData = sortedData.slice(frozenRows);
34393
+ const filtered = filterableData.filter((row) => {
34394
+ return filters.every((filter) => {
34395
+ const colIndex = columns.findIndex((c) => c.key === filter.key);
34396
+ if (colIndex === -1)
34397
+ return true;
34398
+ const cellValue = String(row[colIndex]?.value ?? '').toLowerCase();
34399
+ return cellValue.includes(filter.value.toLowerCase());
34400
+ });
34401
+ });
34402
+ return [...frozenData, ...filtered];
34403
+ }, [sortedData, filters, columns, frozenRows]);
34404
+ // Handle cell edit start
34405
+ const handleCellDoubleClick = useCallback((rowIndex, colIndex, cellElement) => {
34406
+ if (readOnly)
34407
+ return;
34408
+ const column = columns[colIndex];
34409
+ if (column?.readOnly)
34410
+ return;
34411
+ const cell = data[rowIndex]?.[colIndex];
34412
+ if (cell?.readOnly)
34413
+ return;
34414
+ setEditingCell({ row: rowIndex, col: colIndex });
34415
+ setEditValue(cell?.formula || String(cell?.value ?? ''));
34416
+ // Capture cell position for formula autocomplete dropdown
34417
+ if (cellElement) {
34418
+ setEditingCellRect(cellElement.getBoundingClientRect());
34419
+ }
34420
+ setTimeout(() => inputRef.current?.focus(), 0);
34421
+ }, [readOnly, columns, data]);
34422
+ // Handle cell edit complete
34423
+ const handleEditComplete = useCallback(() => {
34424
+ if (!editingCell)
34425
+ return;
34426
+ const { row, col } = editingCell;
34427
+ const newData = [...data];
34428
+ if (!newData[row])
34429
+ newData[row] = [];
34430
+ const isFormula = editValue.startsWith('=');
34431
+ const numValue = parseFloat(editValue);
34432
+ const value = isFormula ? 0 : !isNaN(numValue) ? numValue : editValue;
34433
+ newData[row][col] = {
34434
+ ...newData[row][col],
34435
+ value,
34436
+ formula: isFormula ? editValue : undefined,
34437
+ };
34438
+ setData(newData);
34439
+ setEditingCell(null);
34440
+ setEditValue('');
34441
+ if (onChange) {
34442
+ onChange(newData, row, col);
34443
+ }
34444
+ }, [editingCell, editValue, data, onChange]);
34445
+ // Handle cell edit cancel
34446
+ const handleEditCancel = useCallback(() => {
34447
+ setEditingCell(null);
34448
+ setEditValue('');
34449
+ }, []);
34450
+ // Handle key down in edit mode
34451
+ const handleEditKeyDown = useCallback((e) => {
34452
+ if (e.key === 'Enter') {
34453
+ e.preventDefault();
34454
+ handleEditComplete();
34455
+ }
34456
+ else if (e.key === 'Escape') {
34457
+ handleEditCancel();
34458
+ }
34459
+ else if (e.key === 'Tab') {
34460
+ e.preventDefault();
34461
+ handleEditComplete();
34462
+ // Move to next cell
34463
+ if (editingCell) {
34464
+ const nextCol = e.shiftKey ? editingCell.col - 1 : editingCell.col + 1;
34465
+ if (nextCol >= 0 && nextCol < columns.length) {
34466
+ handleCellDoubleClick(editingCell.row, nextCol);
34467
+ }
34468
+ }
34469
+ }
34470
+ }, [handleEditComplete, handleEditCancel, editingCell, columns.length, handleCellDoubleClick]);
34471
+ // Handle cell click
34472
+ const handleCellClick = useCallback((rowIndex, colIndex) => {
34473
+ setSelectedCell({ row: rowIndex, col: colIndex });
34474
+ }, []);
34475
+ // Handle keyboard navigation
34476
+ const handleKeyDown = useCallback((e) => {
34477
+ if (editingCell)
34478
+ return;
34479
+ if (!selectedCell)
34480
+ return;
34481
+ const { row, col } = selectedCell;
34482
+ let newRow = row;
34483
+ let newCol = col;
34484
+ switch (e.key) {
34485
+ case 'ArrowUp':
34486
+ newRow = Math.max(0, row - 1);
34487
+ break;
34488
+ case 'ArrowDown':
34489
+ newRow = Math.min(filteredData.length - 1, row + 1);
34490
+ break;
34491
+ case 'ArrowLeft':
34492
+ newCol = Math.max(0, col - 1);
34493
+ break;
34494
+ case 'ArrowRight':
34495
+ newCol = Math.min(columns.length - 1, col + 1);
34496
+ break;
34497
+ case 'Enter':
34498
+ case 'F2':
34499
+ handleCellDoubleClick(row, col);
34500
+ e.preventDefault();
34501
+ return;
34502
+ default:
34503
+ return;
34504
+ }
34505
+ if (newRow !== row || newCol !== col) {
34506
+ setSelectedCell({ row: newRow, col: newCol });
34507
+ e.preventDefault();
34508
+ }
34509
+ }, [editingCell, selectedCell, filteredData.length, columns.length, handleCellDoubleClick]);
34510
+ // Handle sort
34511
+ const handleSort = useCallback((key) => {
34512
+ setSortConfig((prev) => {
34513
+ if (prev?.key === key) {
34514
+ if (prev.direction === 'asc') {
34515
+ return { key, direction: 'desc' };
34516
+ }
34517
+ return null; // Clear sort
34518
+ }
34519
+ return { key, direction: 'asc' };
34520
+ });
34521
+ }, []);
34522
+ // Handle filter
34523
+ const handleFilter = useCallback((key) => {
34524
+ setActiveFilter((prev) => (prev === key ? null : key));
34525
+ const existing = filters.find((f) => f.key === key);
34526
+ setFilterValue(existing?.value || '');
34527
+ }, [filters]);
34528
+ // Apply filter
34529
+ const applyFilter = useCallback(() => {
34530
+ if (!activeFilter)
34531
+ return;
34532
+ setFilters((prev) => {
34533
+ const existing = prev.findIndex((f) => f.key === activeFilter);
34534
+ if (filterValue) {
34535
+ if (existing >= 0) {
34536
+ const newFilters = [...prev];
34537
+ newFilters[existing] = { key: activeFilter, value: filterValue };
34538
+ return newFilters;
34539
+ }
34540
+ return [...prev, { key: activeFilter, value: filterValue }];
34541
+ }
34542
+ else {
34543
+ return prev.filter((f) => f.key !== activeFilter);
34544
+ }
34545
+ });
34546
+ setActiveFilter(null);
34547
+ }, [activeFilter, filterValue]);
34548
+ // Clear filter
34549
+ const clearFilter = useCallback((key) => {
34550
+ setFilters((prev) => prev.filter((f) => f.key !== key));
34551
+ }, []);
34552
+ // Export to CSV
34553
+ const exportToCSV = useCallback(() => {
34554
+ const headers = columns.map((c) => c.header).join(',');
34555
+ const rows = filteredData.map((row) => row
34556
+ .map((cell) => {
34557
+ const val = String(cell?.value ?? '');
34558
+ // Escape quotes and wrap in quotes if contains comma
34559
+ if (val.includes(',') || val.includes('"') || val.includes('\n')) {
34560
+ return `"${val.replace(/"/g, '""')}"`;
34561
+ }
34562
+ return val;
34563
+ })
34564
+ .join(','));
34565
+ const csv = [headers, ...rows].join('\n');
34566
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
34567
+ const url = URL.createObjectURL(blob);
34568
+ const link = document.createElement('a');
34569
+ link.href = url;
34570
+ link.download = exportFileName;
34571
+ link.click();
34572
+ URL.revokeObjectURL(url);
34573
+ addSuccessMessage('Exported to CSV successfully');
34574
+ }, [columns, filteredData, exportFileName]);
34575
+ // Save handler
34576
+ const handleSave = useCallback(async () => {
34577
+ if (!onSave)
34578
+ return;
34579
+ setIsSaving(true);
34580
+ try {
34581
+ await onSave(data);
34582
+ addSuccessMessage('Data saved successfully');
34583
+ }
34584
+ catch (error) {
34585
+ console.error('Save failed:', error);
34586
+ addErrorMessage('Failed to save data');
34587
+ }
34588
+ finally {
34589
+ setIsSaving(false);
34590
+ }
34591
+ }, [onSave, data]);
34592
+ // Toggle freeze first row
34593
+ const toggleFreezeFirstRow = useCallback(() => {
34594
+ setFrozenRowsState((prev) => (prev === 'first' || prev === 1 ? 'none' : 'first'));
34595
+ }, []);
34596
+ // Toggle freeze selected row
34597
+ const toggleFreezeSelectedRow = useCallback(() => {
34598
+ setFrozenRowsState((prev) => (prev === 'selected' ? 'none' : 'selected'));
34599
+ }, []);
34600
+ // Expose imperative handle
34601
+ useImperativeHandle(ref, () => ({
34602
+ getData: () => data,
34603
+ setCell: (rowIndex, colIndex, value) => {
34604
+ const newData = [...data];
34605
+ if (!newData[rowIndex])
34606
+ newData[rowIndex] = [];
34607
+ newData[rowIndex][colIndex] =
34608
+ typeof value === 'object' && value !== null ? value : { value: value };
34609
+ setData(newData);
34610
+ },
34611
+ clearFilters: () => setFilters([]),
34612
+ clearSort: () => setSortConfig(null),
34613
+ exportToCSV,
34614
+ toggleFreezeFirstRow,
34615
+ toggleFreezeSelectedRow,
34616
+ setFrozenRows: setFrozenRowsState,
34617
+ }));
34618
+ // Format cell value for display
34619
+ const formatValue = useCallback((value, column) => {
34620
+ if (value === null || value === undefined)
34621
+ return '';
34622
+ if (typeof value === 'boolean')
34623
+ return value ? 'Yes' : 'No';
34624
+ const numVal = typeof value === 'number' ? value : parseFloat(String(value));
34625
+ if (column.type === 'currency' && !isNaN(numVal)) {
34626
+ const decimals = column.format?.decimals ?? 2;
34627
+ const prefix = column.format?.prefix ?? '$';
34628
+ return `${prefix}${numVal.toFixed(decimals)}`;
34629
+ }
34630
+ if (column.type === 'percent' && !isNaN(numVal)) {
34631
+ const decimals = column.format?.decimals ?? 1;
34632
+ return `${(numVal * 100).toFixed(decimals)}%`;
34633
+ }
34634
+ if (column.type === 'number' && !isNaN(numVal)) {
34635
+ const decimals = column.format?.decimals;
34636
+ if (decimals !== undefined) {
34637
+ return numVal.toFixed(decimals);
34638
+ }
34639
+ }
34640
+ return String(value);
34641
+ }, []);
34642
+ // Density classes
34643
+ const densityClasses = {
34644
+ compact: 'py-1 px-2 text-xs',
34645
+ normal: 'py-2 px-3 text-sm',
34646
+ comfortable: 'py-3 px-4 text-sm',
34647
+ };
34648
+ const cellPadding = densityClasses[density];
34649
+ return (jsxs("div", { className: `data-grid ${className}`, style: { width }, children: [showToolbar && (jsxs(Stack, { direction: "horizontal", spacing: "md", align: "center", className: "mb-3 px-1", children: [title && (jsx("div", { className: "text-lg font-medium text-ink-900 flex-1", children: title })), showFreezeRowToggle && (jsx("div", { className: "relative", children: jsx(Button, { variant: "ghost", size: "sm", icon: frozenRowsState !== 'none' ? (jsx(Pin, { className: "h-4 w-4 text-primary-600" })) : (jsx(PinOff, { className: "h-4 w-4" })), onClick: toggleFreezeFirstRow, title: frozenRowsState === 'first' || frozenRowsState === 1
34650
+ ? 'Unfreeze first row'
34651
+ : 'Freeze first row', children: frozenRowsState === 'first' || frozenRowsState === 1
34652
+ ? 'Unfreeze Row'
34653
+ : 'Freeze Row' }) })), enableExport && (jsx(Button, { variant: "ghost", size: "sm", icon: jsx(Download, { className: "h-4 w-4" }), onClick: exportToCSV, children: "Export" })), enableSave && onSave && (jsx(Button, { variant: "primary", size: "sm", icon: jsx(Save, { className: "h-4 w-4" }), onClick: handleSave, loading: isSaving, children: "Save" })), toolbarActions] })), filters.length > 0 && (jsx(Stack, { direction: "horizontal", spacing: "sm", className: "mb-2 px-1 flex-wrap", children: filters.map((filter) => {
34654
+ const column = columns.find((c) => c.key === filter.key);
34655
+ return (jsxs("div", { className: "inline-flex items-center gap-1 px-2 py-1 bg-primary-100 text-primary-700 rounded text-xs", children: [jsxs("span", { className: "font-medium", children: [column?.header, ":"] }), jsx("span", { children: filter.value }), jsx("button", { onClick: () => clearFilter(filter.key), className: "ml-1 hover:bg-primary-200 rounded p-0.5", children: jsx(X, { className: "h-3 w-3" }) })] }, filter.key));
34656
+ }) })), jsx("div", { ref: tableRef, className: "relative overflow-auto border border-stone-200 rounded-lg bg-white", style: { height }, onKeyDown: handleKeyDown, tabIndex: 0, children: jsxs("table", { className: "border-collapse", style: { tableLayout: 'auto' }, children: [jsx("thead", { className: "sticky top-0 z-20 bg-stone-100", children: jsxs("tr", { children: [rowHeaders && (jsx("th", { className: `${cellPadding} border-b border-r border-stone-200 bg-stone-100 text-left font-semibold text-ink-600 sticky left-0 z-30`, style: { width: 50, minWidth: 50, maxWidth: 50 }, children: "#" })), columns.map((column, colIndex) => {
34657
+ const isFrozen = colIndex < frozenColumns;
34658
+ const leftOffset = rowHeaders ? 50 + columns.slice(0, colIndex).reduce((sum, c) => sum + (c.width || 100), 0) : columns.slice(0, colIndex).reduce((sum, c) => sum + (c.width || 100), 0);
34659
+ return (jsx("th", { className: `${cellPadding} border-b border-r border-stone-200 bg-stone-100 font-semibold text-ink-600 text-${column.align || 'left'} ${isFrozen ? 'sticky z-30' : ''}`, style: {
34660
+ width: column.width,
34661
+ minWidth: column.minWidth || 80,
34662
+ left: isFrozen ? leftOffset : undefined,
34663
+ }, children: jsxs("div", { className: "flex items-center gap-1", children: [jsx("span", { className: "flex-1", children: column.header }), column.sortable && (jsx("button", { onClick: () => handleSort(column.key), className: "p-0.5 hover:bg-stone-200 rounded", children: sortConfig?.key === column.key ? (sortConfig.direction === 'asc' ? (jsx(ChevronUp, { className: "h-4 w-4 text-primary-600" })) : (jsx(ChevronDown, { className: "h-4 w-4 text-primary-600" }))) : (jsx(ArrowUpDown, { className: "h-4 w-4 text-ink-400" })) })), column.filterable && (jsxs("div", { className: "relative", children: [jsx("button", { onClick: () => handleFilter(column.key), className: `p-0.5 hover:bg-stone-200 rounded ${filters.some((f) => f.key === column.key)
34664
+ ? 'text-primary-600'
34665
+ : 'text-ink-400'}`, children: jsx(Filter, { className: "h-4 w-4" }) }), activeFilter === column.key && (jsxs("div", { className: "absolute top-full right-0 mt-1 p-2 bg-white border border-stone-200 rounded-lg shadow-lg z-50 min-w-48", children: [jsx(Input, { size: "sm", placeholder: `Filter ${column.header}...`, value: filterValue, onChange: (e) => setFilterValue(e.target.value), onKeyDown: (e) => {
34666
+ if (e.key === 'Enter')
34667
+ applyFilter();
34668
+ if (e.key === 'Escape')
34669
+ setActiveFilter(null);
34670
+ }, autoFocus: true }), jsxs(Stack, { direction: "horizontal", spacing: "sm", className: "mt-2", children: [jsx(Button, { size: "sm", variant: "ghost", onClick: () => setActiveFilter(null), children: "Cancel" }), jsx(Button, { size: "sm", variant: "primary", onClick: applyFilter, children: "Apply" })] })] }))] }))] }) }, column.key));
34671
+ })] }) }), jsx("tbody", { children: filteredData.map((row, rowIndex) => {
34672
+ const isFrozen = isRowFrozen(rowIndex);
34673
+ const isZebra = zebraStripes && rowIndex % 2 === 1;
34674
+ return (jsxs("tr", { className: `${isZebra ? 'bg-paper-50' : 'bg-white'} ${isFrozen ? 'sticky z-10' : ''} ${isFrozen ? 'shadow-sm' : ''}`, style: {
34675
+ top: isFrozen ? `${40 + rowIndex * 40}px` : undefined,
34676
+ }, children: [rowHeaders && (jsx("td", { className: `${cellPadding} border-b border-r border-stone-200 bg-stone-50 text-ink-500 font-medium sticky left-0 z-10`, style: { width: 50, minWidth: 50, maxWidth: 50 }, children: Array.isArray(rowHeaders) ? rowHeaders[rowIndex] : rowIndex + 1 })), row.map((cell, colIndex) => {
34677
+ const column = columns[colIndex];
34678
+ const isFrozenCol = colIndex < frozenColumns;
34679
+ const isEditing = editingCell?.row === rowIndex && editingCell?.col === colIndex;
34680
+ const isSelected = selectedCell?.row === rowIndex && selectedCell?.col === colIndex;
34681
+ const hasFormula = !!cell?.formula;
34682
+ const leftOffset = rowHeaders
34683
+ ? 50 + columns.slice(0, colIndex).reduce((sum, c) => sum + (c.width || 100), 0)
34684
+ : columns.slice(0, colIndex).reduce((sum, c) => sum + (c.width || 100), 0);
34685
+ return (jsx("td", { className: `${cellPadding} border-b border-r border-stone-200 text-${column?.align || 'left'} ${isFrozenCol ? 'sticky z-10' : ''} ${isZebra && isFrozenCol ? 'bg-paper-50' : isFrozenCol ? 'bg-white' : ''} ${isSelected ? 'ring-2 ring-inset ring-primary-500' : ''} ${hasFormula ? 'bg-blue-50' : ''} ${cell?.className || ''}`, style: {
34686
+ left: isFrozenCol ? leftOffset : undefined,
34687
+ minWidth: column?.minWidth || 80,
34688
+ }, onClick: () => handleCellClick(rowIndex, colIndex), onDoubleClick: (e) => handleCellDoubleClick(rowIndex, colIndex, e.currentTarget), children: isEditing ? (formulas ? (jsx(FormulaAutocomplete, { value: editValue, onChange: setEditValue, onComplete: handleEditComplete, onCancel: handleEditCancel, anchorRect: editingCellRect, autoFocus: true })) : (jsx("input", { ref: inputRef, type: "text", value: editValue, onChange: (e) => setEditValue(e.target.value), onBlur: handleEditComplete, onKeyDown: handleEditKeyDown, className: "w-full h-full border-none outline-none bg-transparent", style: { margin: '-4px', padding: '4px' } }))) : (formatValue(cell?.value, column)) }, colIndex));
34689
+ })] }, rowIndex));
34690
+ }) })] }) }), jsxs("div", { className: "flex items-center justify-between px-2 py-1 text-xs text-ink-500 border-t border-stone-200 bg-stone-50 rounded-b-lg", children: [jsxs("span", { children: [filteredData.length, " row", filteredData.length !== 1 ? 's' : '', filters.length > 0 && ` (filtered)`] }), selectedCell && (jsxs("span", { children: [colIndexToLetter(selectedCell.col), selectedCell.row + 1, data[selectedCell.row]?.[selectedCell.col]?.formula && (jsx("span", { className: "ml-2 text-blue-600", children: data[selectedCell.row][selectedCell.col].formula }))] }))] })] }));
34691
+ });
34692
+ DataGrid.displayName = 'DataGrid';
34693
+
34694
+ // Color mapping for action buttons
34695
+ const colorClasses = {
34696
+ primary: 'bg-accent-500 text-white',
34697
+ success: 'bg-success-500 text-white',
34698
+ warning: 'bg-warning-500 text-white',
34699
+ error: 'bg-error-500 text-white',
34700
+ default: 'bg-paper-500 text-white',
34701
+ };
34702
+ /**
34703
+ * SwipeActions - Touch-based swipe actions for list items
34704
+ *
34705
+ * Wraps any content with swipe-to-reveal actions, commonly used in mobile
34706
+ * list items for quick actions like delete, archive, edit, etc.
34707
+ *
34708
+ * Features:
34709
+ * - Left and right swipe actions
34710
+ * - Full swipe to trigger primary action
34711
+ * - Spring-back animation
34712
+ * - Touch and mouse support
34713
+ * - Customizable thresholds
34714
+ *
34715
+ * @example Basic delete action
34716
+ * ```tsx
34717
+ * <SwipeActions
34718
+ * leftActions={[
34719
+ * {
34720
+ * id: 'delete',
34721
+ * label: 'Delete',
34722
+ * icon: <Trash className="h-5 w-5" />,
34723
+ * color: 'error',
34724
+ * onClick: () => handleDelete(item),
34725
+ * primary: true,
34726
+ * },
34727
+ * ]}
34728
+ * >
34729
+ * <div className="p-4 bg-white">
34730
+ * List item content
34731
+ * </div>
34732
+ * </SwipeActions>
34733
+ * ```
34734
+ *
34735
+ * @example Multiple actions on both sides
34736
+ * ```tsx
34737
+ * <SwipeActions
34738
+ * leftActions={[
34739
+ * { id: 'delete', label: 'Delete', icon: <Trash />, color: 'error', onClick: handleDelete },
34740
+ * { id: 'archive', label: 'Archive', icon: <Archive />, color: 'warning', onClick: handleArchive },
34741
+ * ]}
34742
+ * rightActions={[
34743
+ * { id: 'edit', label: 'Edit', icon: <Edit />, color: 'primary', onClick: handleEdit },
34744
+ * ]}
34745
+ * fullSwipe
34746
+ * >
34747
+ * <ListItem />
34748
+ * </SwipeActions>
34749
+ * ```
34750
+ */
34751
+ function SwipeActions({ children, leftActions = [], rightActions = [], threshold = 80, fullSwipeThreshold = 0.5, fullSwipe = false, disabled = false, onSwipeChange, className = '', }) {
34752
+ const containerRef = useRef(null);
34753
+ const contentRef = useRef(null);
34754
+ // Swipe state
34755
+ const [translateX, setTranslateX] = useState(0);
34756
+ const [isDragging, setIsDragging] = useState(false);
34757
+ const [activeDirection, setActiveDirection] = useState(null);
34758
+ // Touch/mouse tracking
34759
+ const startX = useRef(0);
34760
+ const currentX = useRef(0);
34761
+ const startTime = useRef(0);
34762
+ // Calculate action widths
34763
+ const leftActionsWidth = leftActions.length * 72; // 72px per action
34764
+ const rightActionsWidth = rightActions.length * 72;
34765
+ // Reset position
34766
+ const resetPosition = useCallback(() => {
34767
+ setTranslateX(0);
34768
+ setActiveDirection(null);
34769
+ onSwipeChange?.(null);
34770
+ }, [onSwipeChange]);
34771
+ // Handle touch/mouse start
34772
+ const handleStart = useCallback((clientX) => {
34773
+ if (disabled)
34774
+ return;
34775
+ startX.current = clientX;
34776
+ currentX.current = clientX;
34777
+ startTime.current = Date.now();
34778
+ setIsDragging(true);
34779
+ }, [disabled]);
34780
+ // Handle touch/mouse move
34781
+ const handleMove = useCallback((clientX) => {
34782
+ if (!isDragging || disabled)
34783
+ return;
34784
+ const deltaX = clientX - startX.current;
34785
+ currentX.current = clientX;
34786
+ // Determine direction and apply resistance at boundaries
34787
+ let newTranslateX = deltaX;
34788
+ // Swiping left (reveals left actions on right side)
34789
+ if (deltaX < 0) {
34790
+ if (leftActions.length === 0) {
34791
+ newTranslateX = deltaX * 0.2; // Heavy resistance if no actions
34792
+ }
34793
+ else {
34794
+ const maxSwipe = fullSwipe
34795
+ ? -(containerRef.current?.offsetWidth || 300)
34796
+ : -leftActionsWidth;
34797
+ newTranslateX = Math.max(maxSwipe, deltaX);
34798
+ // Apply resistance past the action buttons
34799
+ if (newTranslateX < -leftActionsWidth) {
34800
+ const overSwipe = newTranslateX + leftActionsWidth;
34801
+ newTranslateX = -leftActionsWidth + overSwipe * 0.3;
34802
+ }
34803
+ }
34804
+ setActiveDirection('left');
34805
+ onSwipeChange?.('left');
34806
+ }
34807
+ // Swiping right (reveals right actions on left side)
34808
+ else if (deltaX > 0) {
34809
+ if (rightActions.length === 0) {
34810
+ newTranslateX = deltaX * 0.2; // Heavy resistance if no actions
34811
+ }
34812
+ else {
34813
+ const maxSwipe = fullSwipe
34814
+ ? (containerRef.current?.offsetWidth || 300)
34815
+ : rightActionsWidth;
34816
+ newTranslateX = Math.min(maxSwipe, deltaX);
34817
+ // Apply resistance past the action buttons
34818
+ if (newTranslateX > rightActionsWidth) {
34819
+ const overSwipe = newTranslateX - rightActionsWidth;
34820
+ newTranslateX = rightActionsWidth + overSwipe * 0.3;
34821
+ }
34822
+ }
34823
+ setActiveDirection('right');
34824
+ onSwipeChange?.('right');
34825
+ }
34826
+ setTranslateX(newTranslateX);
34827
+ }, [isDragging, disabled, leftActions.length, rightActions.length, leftActionsWidth, rightActionsWidth, fullSwipe, onSwipeChange]);
34828
+ // Handle touch/mouse end
34829
+ const handleEnd = useCallback(() => {
34830
+ if (!isDragging)
34831
+ return;
34832
+ setIsDragging(false);
34833
+ const deltaX = currentX.current - startX.current;
34834
+ const velocity = Math.abs(deltaX) / (Date.now() - startTime.current);
34835
+ const containerWidth = containerRef.current?.offsetWidth || 300;
34836
+ // Check for full swipe trigger
34837
+ if (fullSwipe) {
34838
+ const swipePercentage = Math.abs(translateX) / containerWidth;
34839
+ if (swipePercentage >= fullSwipeThreshold || velocity > 0.5) {
34840
+ // Find primary action and trigger it
34841
+ if (translateX < 0 && leftActions.length > 0) {
34842
+ const primaryAction = leftActions.find(a => a.primary) || leftActions[0];
34843
+ primaryAction.onClick();
34844
+ resetPosition();
34845
+ return;
34846
+ }
34847
+ else if (translateX > 0 && rightActions.length > 0) {
34848
+ const primaryAction = rightActions.find(a => a.primary) || rightActions[0];
34849
+ primaryAction.onClick();
34850
+ resetPosition();
34851
+ return;
34852
+ }
34853
+ }
34854
+ }
34855
+ // Snap to open or closed position
34856
+ if (Math.abs(translateX) >= threshold || velocity > 0.3) {
34857
+ // Snap open
34858
+ if (translateX < 0 && leftActions.length > 0) {
34859
+ setTranslateX(-leftActionsWidth);
34860
+ setActiveDirection('left');
34861
+ onSwipeChange?.('left');
34862
+ }
34863
+ else if (translateX > 0 && rightActions.length > 0) {
34864
+ setTranslateX(rightActionsWidth);
34865
+ setActiveDirection('right');
34866
+ onSwipeChange?.('right');
34867
+ }
34868
+ else {
34869
+ resetPosition();
34870
+ }
34871
+ }
34872
+ else {
34873
+ // Snap closed
34874
+ resetPosition();
34875
+ }
34876
+ }, [isDragging, translateX, threshold, fullSwipe, fullSwipeThreshold, leftActions, rightActions, leftActionsWidth, rightActionsWidth, resetPosition, onSwipeChange]);
34877
+ // Touch event handlers
34878
+ const handleTouchStart = (e) => {
34879
+ handleStart(e.touches[0].clientX);
34880
+ };
34881
+ const handleTouchMove = (e) => {
34882
+ handleMove(e.touches[0].clientX);
34883
+ };
34884
+ const handleTouchEnd = () => {
34885
+ handleEnd();
34886
+ };
34887
+ // Mouse event handlers (for testing/desktop)
34888
+ const handleMouseDown = (e) => {
34889
+ handleStart(e.clientX);
34890
+ };
34891
+ const handleMouseMove = (e) => {
34892
+ handleMove(e.clientX);
34893
+ };
34894
+ const handleMouseUp = () => {
34895
+ handleEnd();
34896
+ };
34897
+ // Close on outside click
34898
+ useEffect(() => {
34899
+ if (activeDirection === null)
34900
+ return;
34901
+ const handleClickOutside = (e) => {
34902
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
34903
+ resetPosition();
34904
+ }
34905
+ };
34906
+ document.addEventListener('mousedown', handleClickOutside);
34907
+ return () => document.removeEventListener('mousedown', handleClickOutside);
34908
+ }, [activeDirection, resetPosition]);
34909
+ // Handle mouse leave during drag
34910
+ useEffect(() => {
34911
+ if (!isDragging)
34912
+ return;
34913
+ const handleMouseLeave = () => {
34914
+ handleEnd();
34915
+ };
34916
+ document.addEventListener('mouseup', handleMouseLeave);
34917
+ return () => document.removeEventListener('mouseup', handleMouseLeave);
34918
+ }, [isDragging, handleEnd]);
34919
+ // Render action button
34920
+ const renderActionButton = (action) => {
34921
+ const colorClass = colorClasses[action.color || 'default'];
34922
+ return (jsxs("button", { onClick: (e) => {
34923
+ e.stopPropagation();
34924
+ action.onClick();
34925
+ resetPosition();
34926
+ }, className: `
34927
+ flex flex-col items-center justify-center
34928
+ w-18 h-full min-w-[72px]
34929
+ ${colorClass}
34930
+ transition-transform duration-150
34931
+ `, style: {
34932
+ transform: isDragging ? 'scale(1)' : 'scale(1)',
34933
+ }, children: [action.icon && (jsx("div", { className: "mb-1", children: action.icon })), jsx("span", { className: "text-xs font-medium", children: action.label })] }, action.id));
34934
+ };
34935
+ return (jsxs("div", { ref: containerRef, className: `relative overflow-hidden ${className}`, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onMouseDown: handleMouseDown, onMouseMove: isDragging ? handleMouseMove : undefined, onMouseUp: handleMouseUp, onMouseLeave: isDragging ? handleEnd : undefined, children: [rightActions.length > 0 && (jsx("div", { className: "absolute left-0 top-0 bottom-0 flex", style: { width: rightActionsWidth }, children: rightActions.map((action) => renderActionButton(action)) })), leftActions.length > 0 && (jsx("div", { className: "absolute right-0 top-0 bottom-0 flex", style: { width: leftActionsWidth }, children: leftActions.map((action) => renderActionButton(action)) })), jsx("div", { ref: contentRef, className: `
34936
+ relative bg-white
34937
+ ${isDragging ? '' : 'transition-transform duration-200 ease-out'}
34938
+ `, style: {
34939
+ transform: `translateX(${translateX}px)`,
34940
+ touchAction: 'pan-y', // Allow vertical scrolling
34941
+ }, children: children })] }));
34942
+ }
34943
+
34944
+ var classnames = {exports: {}};
34945
+
34946
+ /*!
34947
+ Copyright (c) 2018 Jed Watson.
34948
+ Licensed under the MIT License (MIT), see
34949
+ http://jedwatson.github.io/classnames
34950
+ */
34951
+
34952
+ (function (module) {
34953
+ /* global define */
34954
+
34955
+ (function () {
34956
+
34957
+ var hasOwn = {}.hasOwnProperty;
34958
+
34959
+ function classNames () {
34960
+ var classes = '';
34961
+
34962
+ for (var i = 0; i < arguments.length; i++) {
34963
+ var arg = arguments[i];
34964
+ if (arg) {
34965
+ classes = appendClass(classes, parseValue(arg));
34966
+ }
34967
+ }
34968
+
34969
+ return classes;
34970
+ }
34971
+
34972
+ function parseValue (arg) {
34973
+ if (typeof arg === 'string' || typeof arg === 'number') {
34974
+ return arg;
34975
+ }
34976
+
34977
+ if (typeof arg !== 'object') {
34978
+ return '';
34979
+ }
34980
+
34981
+ if (Array.isArray(arg)) {
34982
+ return classNames.apply(null, arg);
34983
+ }
34984
+
34985
+ if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {
34986
+ return arg.toString();
34987
+ }
34988
+
34989
+ var classes = '';
34990
+
34991
+ for (var key in arg) {
34992
+ if (hasOwn.call(arg, key) && arg[key]) {
34993
+ classes = appendClass(classes, key);
34994
+ }
34995
+ }
34996
+
34997
+ return classes;
34998
+ }
34999
+
35000
+ function appendClass (value, newClass) {
35001
+ if (!newClass) {
35002
+ return value;
35003
+ }
35004
+
35005
+ if (value) {
35006
+ return value + ' ' + newClass;
35007
+ }
35008
+
35009
+ return value + newClass;
35010
+ }
33003
35011
 
33004
- var FormulaParser = /*@__PURE__*/getDefaultExportFromCjs(fastFormulaParser);
35012
+ if (module.exports) {
35013
+ classNames.default = classNames;
35014
+ module.exports = classNames;
35015
+ } else {
35016
+ window.classNames = classNames;
35017
+ }
35018
+ }());
35019
+ } (classnames));
35020
+
35021
+ var classnamesExports = classnames.exports;
35022
+ var classNames = /*@__PURE__*/getDefaultExportFromCjs(classnamesExports);
33005
35023
 
33006
35024
  var scheduler = {exports: {}};
33007
35025
 
@@ -34965,7 +36983,7 @@ function extractFormula(value) {
34965
36983
  return value.slice(1);
34966
36984
  }
34967
36985
  function createFormulaParser(data, config) {
34968
- return new FormulaParser(__assign(__assign({}, config), { onCell: function (ref) {
36986
+ return new FormulaParser$1(__assign(__assign({}, config), { onCell: function (ref) {
34969
36987
  var point = {
34970
36988
  row: ref.row - 1,
34971
36989
  column: ref.col - 1,
@@ -53810,6 +55828,171 @@ function PageLayout({ title, description, children, className = '', headerConten
53810
55828
  return (jsxs(Page, { padding: "none", maxWidth: maxWidth, fixed: fixed, children: [headerContent, jsxs("div", { className: `${paddingClasses} ${maxWidthClasses[maxWidth]} mx-auto ${className}`, children: [jsxs("div", { className: "mb-8", children: [jsx("h1", { className: "text-3xl font-bold text-ink-900 mb-2", children: title }), description && (jsx("p", { className: "text-ink-600", children: description }))] }), children] })] }));
53811
55829
  }
53812
55830
 
55831
+ /**
55832
+ * PageHeader - Standard page header with title, breadcrumbs, and actions
55833
+ *
55834
+ * A consistent header component for pages that provides:
55835
+ * - Page title and optional subtitle
55836
+ * - Breadcrumb navigation
55837
+ * - Action buttons (Create, Export, etc.)
55838
+ * - Optional back button
55839
+ * - Sticky positioning option
55840
+ *
55841
+ * @example Basic usage
55842
+ * ```tsx
55843
+ * <PageHeader
55844
+ * title="Products"
55845
+ * subtitle="Manage your product catalog"
55846
+ * breadcrumbs={[{ label: 'Inventory' }, { label: 'Products' }]}
55847
+ * actions={[
55848
+ * { id: 'export', label: 'Export', icon: <Download />, onClick: handleExport, variant: 'ghost' },
55849
+ * { id: 'add', label: 'Add Product', icon: <Plus />, onClick: handleAdd, variant: 'primary' },
55850
+ * ]}
55851
+ * />
55852
+ * ```
55853
+ *
55854
+ * @example With back button
55855
+ * ```tsx
55856
+ * <PageHeader
55857
+ * title="Edit Product"
55858
+ * backButton={{ label: 'Back to Products', onClick: () => navigate('/products') }}
55859
+ * />
55860
+ * ```
55861
+ *
55862
+ * @example With custom right content
55863
+ * ```tsx
55864
+ * <PageHeader
55865
+ * title="Dashboard"
55866
+ * rightContent={<DateRangePicker value={range} onChange={setRange} />}
55867
+ * />
55868
+ * ```
55869
+ */
55870
+ function PageHeader({ title, subtitle, breadcrumbs, showHomeBreadcrumb = true, actions, rightContent, belowTitle, className = '', sticky = false, backButton, }) {
55871
+ const variantStyles = {
55872
+ primary: 'bg-accent-500 text-white border-accent-500 hover:bg-accent-600 hover:shadow-sm',
55873
+ secondary: 'bg-white text-ink-700 border-paper-300 hover:bg-paper-50 hover:border-paper-400 shadow-xs hover:shadow-sm',
55874
+ ghost: 'bg-transparent text-ink-600 border-transparent hover:text-ink-800 hover:bg-paper-100',
55875
+ danger: 'bg-error-500 text-white border-error-500 hover:bg-error-600 hover:shadow-sm',
55876
+ outline: 'bg-transparent text-ink-700 border-paper-300 hover:bg-paper-50 hover:border-ink-400',
55877
+ };
55878
+ return (jsx("div", { className: `
55879
+ bg-white border-b border-paper-200
55880
+ ${sticky ? 'sticky top-0 z-40' : ''}
55881
+ ${className}
55882
+ `, children: jsxs("div", { className: "px-6 py-4", children: [breadcrumbs && breadcrumbs.length > 0 && (jsx("div", { className: "mb-3", children: jsx(Breadcrumbs, { items: breadcrumbs, showHome: showHomeBreadcrumb }) })), backButton && (jsx("div", { className: "mb-3", children: jsxs("button", { onClick: backButton.onClick, className: "inline-flex items-center gap-1.5 text-sm text-ink-500 hover:text-ink-700 transition-colors", children: [jsx("svg", { className: "h-4 w-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }), jsx("span", { children: backButton.label || 'Back' })] }) })), jsxs("div", { className: "flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4", children: [jsxs("div", { className: "min-w-0 flex-1", children: [jsx("h1", { className: "text-2xl font-bold text-ink-900 truncate", children: title }), subtitle && (jsx("p", { className: "mt-1 text-sm text-ink-500", children: subtitle }))] }), (actions || rightContent) && (jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [rightContent, actions && actions.map((action) => (jsxs("button", { onClick: action.onClick, disabled: action.disabled || action.loading, className: `
55883
+ inline-flex items-center justify-center gap-2
55884
+ px-4 py-2 text-sm font-medium rounded-lg border
55885
+ transition-all duration-200
55886
+ focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
55887
+ disabled:opacity-40 disabled:cursor-not-allowed
55888
+ ${variantStyles[action.variant || 'secondary']}
55889
+ ${action.hideOnMobile ? 'hidden sm:inline-flex' : ''}
55890
+ `, children: [action.loading ? (jsxs("svg", { className: "h-4 w-4 animate-spin", fill: "none", viewBox: "0 0 24 24", children: [jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] })) : action.icon ? (jsx("span", { className: "h-4 w-4", children: action.icon })) : null, jsx("span", { children: action.label })] }, action.id)))] }))] }), belowTitle && (jsx("div", { className: "mt-4", children: belowTitle }))] }) }));
55891
+ }
55892
+
55893
+ /**
55894
+ * ActionBar - Flexible toolbar for page-level and contextual actions
55895
+ *
55896
+ * A versatile action container that can be used for:
55897
+ * - Bulk actions when rows are selected
55898
+ * - Page-level actions and controls
55899
+ * - Form action buttons (Save/Cancel)
55900
+ * - Contextual toolbars
55901
+ *
55902
+ * @example Basic bulk actions bar
55903
+ * ```tsx
55904
+ * <ActionBar
55905
+ * visible={selectedIds.length > 0}
55906
+ * leftContent={<Text weight="medium">{selectedIds.length} selected</Text>}
55907
+ * actions={[
55908
+ * { id: 'export', label: 'Export', icon: <Download />, onClick: handleExport },
55909
+ * { id: 'delete', label: 'Delete', icon: <Trash />, onClick: handleDelete, variant: 'danger' },
55910
+ * ]}
55911
+ * onDismiss={() => setSelectedIds([])}
55912
+ * showDismiss
55913
+ * />
55914
+ * ```
55915
+ *
55916
+ * @example Sticky bottom form actions
55917
+ * ```tsx
55918
+ * <ActionBar
55919
+ * position="bottom"
55920
+ * sticky
55921
+ * rightContent={
55922
+ * <>
55923
+ * <Button variant="ghost" onClick={handleCancel}>Cancel</Button>
55924
+ * <Button variant="primary" onClick={handleSave} loading={isSaving}>Save Changes</Button>
55925
+ * </>
55926
+ * }
55927
+ * />
55928
+ * ```
55929
+ *
55930
+ * @example Info bar with center content
55931
+ * ```tsx
55932
+ * <ActionBar
55933
+ * variant="info"
55934
+ * centerContent={
55935
+ * <Text size="sm">Showing results for "search term" - 42 items found</Text>
55936
+ * }
55937
+ * onDismiss={clearSearch}
55938
+ * showDismiss
55939
+ * />
55940
+ * ```
55941
+ */
55942
+ function ActionBar({ leftContent, centerContent, rightContent, actions, position = 'top', sticky = false, visible = true, onDismiss, showDismiss = false, className = '', variant = 'default', compact = false, }) {
55943
+ if (!visible) {
55944
+ return null;
55945
+ }
55946
+ const variantStyles = {
55947
+ default: 'bg-white border-paper-200',
55948
+ primary: 'bg-accent-50 border-accent-200',
55949
+ warning: 'bg-warning-50 border-warning-200',
55950
+ info: 'bg-blue-50 border-blue-200',
55951
+ };
55952
+ const buttonVariantStyles = {
55953
+ primary: 'bg-accent-500 text-white border-accent-500 hover:bg-accent-600 hover:shadow-sm',
55954
+ secondary: 'bg-white text-ink-700 border-paper-300 hover:bg-paper-50 hover:border-paper-400 shadow-xs hover:shadow-sm',
55955
+ ghost: 'bg-transparent text-ink-600 border-transparent hover:text-ink-800 hover:bg-paper-100',
55956
+ danger: 'bg-error-500 text-white border-error-500 hover:bg-error-600 hover:shadow-sm',
55957
+ outline: 'bg-transparent text-ink-700 border-paper-300 hover:bg-paper-50 hover:border-ink-400',
55958
+ };
55959
+ const positionStyles = {
55960
+ top: sticky ? 'sticky top-0 z-40 border-b' : 'border-b',
55961
+ bottom: sticky ? 'sticky bottom-0 z-40 border-t' : 'border-t',
55962
+ };
55963
+ return (jsx("div", { className: `
55964
+ ${variantStyles[variant]}
55965
+ ${positionStyles[position]}
55966
+ ${compact ? 'px-4 py-2' : 'px-6 py-3'}
55967
+ ${className}
55968
+ `, role: "toolbar", "aria-label": "Action bar", children: jsxs("div", { className: "flex items-center justify-between gap-4", children: [jsxs("div", { className: "flex items-center gap-3 min-w-0 flex-shrink-0", children: [showDismiss && onDismiss && (jsx("button", { onClick: onDismiss, className: "\n flex items-center justify-center\n w-8 h-8 rounded-full\n text-ink-400 hover:text-ink-600\n hover:bg-paper-100\n transition-colors\n ", "aria-label": "Dismiss", children: jsx(X, { className: "h-4 w-4" }) })), leftContent] }), centerContent && (jsx("div", { className: "flex-1 flex items-center justify-center min-w-0", children: centerContent })), jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [rightContent, actions && actions.map((action) => (jsxs("button", { onClick: action.onClick, disabled: action.disabled || action.loading, className: `
55969
+ inline-flex items-center justify-center gap-2
55970
+ px-3 py-1.5 text-sm font-medium rounded-lg border
55971
+ transition-all duration-200
55972
+ focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
55973
+ disabled:opacity-40 disabled:cursor-not-allowed
55974
+ ${buttonVariantStyles[action.variant || 'secondary']}
55975
+ `, children: [action.loading ? (jsxs("svg", { className: "h-4 w-4 animate-spin", fill: "none", viewBox: "0 0 24 24", children: [jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] })) : action.icon ? (jsx("span", { className: "h-4 w-4", children: action.icon })) : null, jsx("span", { children: action.label })] }, action.id)))] })] }) }));
55976
+ }
55977
+ /**
55978
+ * ActionBar.Left - Semantic wrapper for left content
55979
+ */
55980
+ function ActionBarLeft({ children }) {
55981
+ return jsx(Fragment, { children: children });
55982
+ }
55983
+ /**
55984
+ * ActionBar.Center - Semantic wrapper for center content
55985
+ */
55986
+ function ActionBarCenter({ children }) {
55987
+ return jsx(Fragment, { children: children });
55988
+ }
55989
+ /**
55990
+ * ActionBar.Right - Semantic wrapper for right content
55991
+ */
55992
+ function ActionBarRight({ children }) {
55993
+ return jsx(Fragment, { children: children });
55994
+ }
55995
+
53813
55996
  const sizeClasses = {
53814
55997
  sm: 'max-w-sm',
53815
55998
  md: 'max-w-md',
@@ -54601,5 +56784,5 @@ function Responsive({ mobile, tablet, desktop, }) {
54601
56784
  return jsx(Fragment, { children: mobile || tablet || desktop });
54602
56785
  }
54603
56786
 
54604
- export { Accordion, ActionButton, AdminModal, Alert, AlertDialog, AppLayout, Autocomplete, Avatar, BREAKPOINTS, Badge, BottomNavigation, BottomNavigationSpacer, BottomSheet, Box, Breadcrumbs, Button, ButtonGroup, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CardView, Carousel, Checkbox, CheckboxList, Chip, ChipGroup, Collapsible, ColorPicker, Combobox, ComingSoon, CommandPalette, ConfirmDialog, ContextMenu, ControlBar, CurrencyDisplay, CurrencyInput, Dashboard, DashboardContent, DashboardHeader, DataTable, DataTableCardView, DateDisplay, DatePicker, DateRangePicker, DateTimePicker, DesktopOnly, Drawer, DrawerFooter, DropZone, Dropdown, DropdownTrigger, EmptyState, ErrorBoundary, ExpandablePanel, ExpandablePanelContainer, ExpandablePanelSpacer, ExpandableRowButton, ExpandableToolbar, ExpandedRowEditForm, ExportButton, FieldArray, FileUpload, FilterBar, FilterControls, FilterStatusBanner, FloatingActionButton, Form, FormContext, FormControl, FormWizard, Grid, GridItem, Hide, HoverCard, InfiniteScroll, Input, KanbanBoard, Layout, Loading, LoadingOverlay, Logo, MarkdownEditor, MaskedInput, Menu, MenuDivider, MobileHeader, MobileHeaderSpacer, MobileLayout, MobileOnly, MobileProvider, Modal, ModalFooter, MultiSelect, NotificationBar, NotificationIndicator, NumberInput, Page, PageLayout, PageNavigation, Pagination, PasswordInput, Popover, Progress, PullToRefresh, QueryTransparency, RadioGroup, Rating, Responsive, RichTextEditor, SearchBar, SearchableList, Select, Separator, Show, Sidebar, SidebarGroup, Skeleton, SkeletonCard$1 as SkeletonCard, SkeletonTable, Slider, Spreadsheet, SpreadsheetReport, Stack, StatCard, StatItem, StatsCardGrid, StatsGrid, StatusBadge, StatusBar, StepIndicator, Stepper, SwipeActions, Switch, Tabs, Text, Textarea, ThemeToggle, TimePicker, Timeline, Toast, ToastContainer, Tooltip, Transfer, TreeView, TwoColumnContent, UserProfileButton, addErrorMessage, addInfoMessage, addSuccessMessage, addWarningMessage, calculateColumnWidth, createActionsSection, createFiltersSection, createMultiSheetExcel, createPageControlsSection, createQueryDetailsSection, exportDataTableToExcel, exportToExcel, formatStatisticValue, formatStatistics, loadColumnOrder, loadColumnWidths, reorderArray, saveColumnOrder, saveColumnWidths, statusManager, useBreakpoint, useBreakpointValue, useColumnReorder, useColumnResize, useCommandPalette, useConfirmDialog, useFABScroll, useFormContext, useIsDesktop, useIsMobile, useIsTablet, useIsTouchDevice, useMediaQuery, useMobileContext, useOrientation, usePrefersMobile, usePullToRefresh, useResponsiveCallback, useSafeAreaInsets, useViewportSize, withMobileContext };
56787
+ export { Accordion, ActionBar, ActionBarCenter, ActionBarLeft, ActionBarRight, ActionButton, AdminModal, Alert, AlertDialog, AppLayout, Autocomplete, Avatar, BREAKPOINTS, Badge, BottomNavigation, BottomNavigationSpacer, BottomSheet, Box, Breadcrumbs, Button, ButtonGroup, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CardView, Carousel, Checkbox, CheckboxList, Chip, ChipGroup, Collapsible, ColorPicker, Combobox, ComingSoon, CommandPalette, ConfirmDialog, ContextMenu, ControlBar, CurrencyDisplay, CurrencyInput, Dashboard, DashboardContent, DashboardHeader, DataGrid, DataTable, DataTableCardView, DateDisplay, DatePicker, DateRangePicker, DateTimePicker, DesktopOnly, Drawer, DrawerFooter, DropZone, Dropdown, DropdownTrigger, EmptyState, ErrorBoundary, ExpandablePanel, ExpandablePanelContainer, ExpandablePanelSpacer, ExpandableRowButton, ExpandableToolbar, ExpandedRowEditForm, ExportButton, FORMULA_CATEGORIES, FORMULA_DEFINITIONS, FORMULA_NAMES, FieldArray, FileUpload, FilterBar, FilterControls, FilterStatusBanner, FloatingActionButton, Form, FormContext, FormControl, FormWizard, Grid, GridItem, Hide, HoverCard, InfiniteScroll, Input, KanbanBoard, Layout, Loading, LoadingOverlay, Logo, MarkdownEditor, MaskedInput, Menu, MenuDivider, MobileHeader, MobileHeaderSpacer, MobileLayout, MobileOnly, MobileProvider, Modal, ModalFooter, MultiSelect, NotificationBar, NotificationIndicator, NumberInput, Page, PageHeader, PageLayout, PageNavigation, Pagination, PasswordInput, Popover, Progress, PullToRefresh, QueryTransparency, RadioGroup, Rating, Responsive, RichTextEditor, SearchBar, SearchableList, Select, Separator, Show, Sidebar, SidebarGroup, Skeleton, SkeletonCard$1 as SkeletonCard, SkeletonTable, Slider, Spreadsheet, SpreadsheetReport, Stack, StatCard, StatItem, StatsCardGrid, StatsGrid, StatusBadge, StatusBar, StepIndicator, Stepper, SwipeActions, Switch, Tabs, Text, Textarea, ThemeToggle, TimePicker, Timeline, Toast, ToastContainer, Tooltip, Transfer, TreeView, TwoColumnContent, UserProfileButton, addErrorMessage, addInfoMessage, addSuccessMessage, addWarningMessage, calculateColumnWidth, createActionsSection, createFiltersSection, createMultiSheetExcel, createPageControlsSection, createQueryDetailsSection, exportDataTableToExcel, exportToExcel, formatStatisticValue, formatStatistics, getFormula, getFormulasByCategory, loadColumnOrder, loadColumnWidths, reorderArray, saveColumnOrder, saveColumnWidths, searchFormulas, statusManager, useBreakpoint, useBreakpointValue, useColumnReorder, useColumnResize, useCommandPalette, useConfirmDialog, useFABScroll, useFormContext, useIsDesktop, useIsMobile, useIsTablet, useIsTouchDevice, useMediaQuery, useMobileContext, useOrientation, usePrefersMobile, usePullToRefresh, useResponsiveCallback, useSafeAreaInsets, useViewportSize, withMobileContext };
54605
56788
  //# sourceMappingURL=index.esm.js.map