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