@alaarab/ogrid-react 2.0.0-beta
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 +55 -0
- package/dist/esm/components/BaseInlineCellEditor.js +112 -0
- package/dist/esm/components/CellErrorBoundary.js +43 -0
- package/dist/esm/components/EmptyState.js +19 -0
- package/dist/esm/components/GridContextMenu.js +35 -0
- package/dist/esm/components/MarchingAntsOverlay.js +110 -0
- package/dist/esm/components/OGridLayout.js +91 -0
- package/dist/esm/components/SideBar.js +122 -0
- package/dist/esm/components/StatusBar.js +6 -0
- package/dist/esm/hooks/index.js +25 -0
- package/dist/esm/hooks/useActiveCell.js +62 -0
- package/dist/esm/hooks/useCellEditing.js +15 -0
- package/dist/esm/hooks/useCellSelection.js +327 -0
- package/dist/esm/hooks/useClipboard.js +161 -0
- package/dist/esm/hooks/useColumnChooserState.js +62 -0
- package/dist/esm/hooks/useColumnHeaderFilterState.js +180 -0
- package/dist/esm/hooks/useColumnResize.js +92 -0
- package/dist/esm/hooks/useContextMenu.js +21 -0
- package/dist/esm/hooks/useDataGridState.js +313 -0
- package/dist/esm/hooks/useDateFilterState.js +34 -0
- package/dist/esm/hooks/useDebounce.js +35 -0
- package/dist/esm/hooks/useFillHandle.js +195 -0
- package/dist/esm/hooks/useFilterOptions.js +40 -0
- package/dist/esm/hooks/useInlineCellEditorState.js +44 -0
- package/dist/esm/hooks/useKeyboardNavigation.js +419 -0
- package/dist/esm/hooks/useLatestRef.js +11 -0
- package/dist/esm/hooks/useMultiSelectFilterState.js +59 -0
- package/dist/esm/hooks/useOGrid.js +465 -0
- package/dist/esm/hooks/usePeopleFilterState.js +68 -0
- package/dist/esm/hooks/useRichSelectState.js +58 -0
- package/dist/esm/hooks/useRowSelection.js +80 -0
- package/dist/esm/hooks/useSideBarState.js +39 -0
- package/dist/esm/hooks/useTableLayout.js +77 -0
- package/dist/esm/hooks/useTextFilterState.js +25 -0
- package/dist/esm/hooks/useUndoRedo.js +83 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/storybook/index.js +1 -0
- package/dist/esm/storybook/mockData.js +73 -0
- package/dist/esm/types/columnTypes.js +1 -0
- package/dist/esm/types/dataGridTypes.js +1 -0
- package/dist/esm/types/index.js +1 -0
- package/dist/esm/utils/dataGridViewModel.js +220 -0
- package/dist/esm/utils/gridRowComparator.js +2 -0
- package/dist/esm/utils/index.js +5 -0
- package/dist/types/components/BaseInlineCellEditor.d.ts +33 -0
- package/dist/types/components/CellErrorBoundary.d.ts +25 -0
- package/dist/types/components/EmptyState.d.ts +26 -0
- package/dist/types/components/GridContextMenu.d.ts +18 -0
- package/dist/types/components/MarchingAntsOverlay.d.ts +15 -0
- package/dist/types/components/OGridLayout.d.ts +37 -0
- package/dist/types/components/SideBar.d.ts +30 -0
- package/dist/types/components/StatusBar.d.ts +24 -0
- package/dist/types/hooks/index.d.ts +48 -0
- package/dist/types/hooks/useActiveCell.d.ts +13 -0
- package/dist/types/hooks/useCellEditing.d.ts +16 -0
- package/dist/types/hooks/useCellSelection.d.ts +22 -0
- package/dist/types/hooks/useClipboard.d.ts +30 -0
- package/dist/types/hooks/useColumnChooserState.d.ts +27 -0
- package/dist/types/hooks/useColumnHeaderFilterState.d.ts +73 -0
- package/dist/types/hooks/useColumnResize.d.ts +23 -0
- package/dist/types/hooks/useContextMenu.d.ts +19 -0
- package/dist/types/hooks/useDataGridState.d.ts +137 -0
- package/dist/types/hooks/useDateFilterState.d.ts +19 -0
- package/dist/types/hooks/useDebounce.d.ts +9 -0
- package/dist/types/hooks/useFillHandle.d.ts +33 -0
- package/dist/types/hooks/useFilterOptions.d.ts +16 -0
- package/dist/types/hooks/useInlineCellEditorState.d.ts +24 -0
- package/dist/types/hooks/useKeyboardNavigation.d.ts +47 -0
- package/dist/types/hooks/useLatestRef.d.ts +6 -0
- package/dist/types/hooks/useMultiSelectFilterState.d.ts +24 -0
- package/dist/types/hooks/useOGrid.d.ts +52 -0
- package/dist/types/hooks/usePeopleFilterState.d.ts +25 -0
- package/dist/types/hooks/useRichSelectState.d.ts +22 -0
- package/dist/types/hooks/useRowSelection.d.ts +22 -0
- package/dist/types/hooks/useSideBarState.d.ts +20 -0
- package/dist/types/hooks/useTableLayout.d.ts +27 -0
- package/dist/types/hooks/useTextFilterState.d.ts +16 -0
- package/dist/types/hooks/useUndoRedo.d.ts +23 -0
- package/dist/types/index.d.ts +23 -0
- package/dist/types/storybook/index.d.ts +2 -0
- package/dist/types/storybook/mockData.d.ts +37 -0
- package/dist/types/types/columnTypes.d.ts +25 -0
- package/dist/types/types/dataGridTypes.d.ts +152 -0
- package/dist/types/types/index.d.ts +3 -0
- package/dist/types/utils/dataGridViewModel.d.ts +161 -0
- package/dist/types/utils/gridRowComparator.d.ts +2 -0
- package/dist/types/utils/index.d.ts +6 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<strong>OGrid React</strong> — React hooks, headless components, and utilities for OGrid data grids.
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@alaarab/ogrid-react"><img src="https://img.shields.io/npm/v/@alaarab/ogrid-react?color=%23217346&label=npm" alt="npm version" /></a>
|
|
7
|
+
<a href="https://github.com/alaarab/ogrid/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License" /></a>
|
|
8
|
+
<img src="https://img.shields.io/badge/React-17%20%7C%2018%20%7C%2019-blue" alt="React 17, 18, 19" />
|
|
9
|
+
<img src="https://img.shields.io/badge/TypeScript-strict-blue" alt="TypeScript strict" />
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://alaarab.github.io/ogrid/">Documentation</a> · <a href="https://alaarab.github.io/ogrid/docs/getting-started/overview">Getting Started</a> · <a href="https://alaarab.github.io/ogrid/docs/api/ogrid-props">API Reference</a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
React hooks and headless components for [OGrid](https://github.com/alaarab/ogrid) data grids. You typically don't need to install this directly — the UI packages ([`@alaarab/ogrid-react-radix`](https://www.npmjs.com/package/@alaarab/ogrid-react-radix), [`@alaarab/ogrid-react-fluent`](https://www.npmjs.com/package/@alaarab/ogrid-react-fluent), [`@alaarab/ogrid-react-material`](https://www.npmjs.com/package/@alaarab/ogrid-react-material)) re-export everything from this package.
|
|
19
|
+
|
|
20
|
+
## What's Inside
|
|
21
|
+
|
|
22
|
+
### Hooks
|
|
23
|
+
|
|
24
|
+
- `useOGrid` — Orchestrator: pagination, sorting, filtering, visibility, editing, row selection, status bar, grid API
|
|
25
|
+
- `useDataGridState` — All DataGridTable state: layout, selection, editing, interaction, context menu, view models
|
|
26
|
+
- `useColumnHeaderFilterState` — Filter popover (open, temp values, apply/clear, people search debounce)
|
|
27
|
+
- `useColumnChooserState` — Column visibility dropdown
|
|
28
|
+
- `useInlineCellEditorState` — Inline cell editor
|
|
29
|
+
- `useRichSelectState` — Searchable rich select dropdown
|
|
30
|
+
- `useSideBarState` — Side bar panel management
|
|
31
|
+
- `useActiveCell`, `useCellSelection`, `useCellEditing`, `useRowSelection`, `useKeyboardNavigation`, `useClipboard`, `useFillHandle`, `useUndoRedo`, `useContextMenu`, `useColumnResize`, `useFilterOptions`, `useDebounce`
|
|
32
|
+
|
|
33
|
+
### Headless Components
|
|
34
|
+
|
|
35
|
+
`OGridLayout` · `StatusBar` · `GridContextMenu` · `SideBar` · `MarchingAntsOverlay`
|
|
36
|
+
|
|
37
|
+
### Utilities
|
|
38
|
+
|
|
39
|
+
`processClientSideData` · `exportToCsv` · `getCellValue` · `flattenColumns` · `buildHeaderRows` · `getPaginationViewModel` · `getHeaderFilterConfig` · `getCellRenderDescriptor` · `computeAggregations` · `formatShortcut` · `GRID_CONTEXT_MENU_ITEMS`
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install @alaarab/ogrid-react
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Peer dep: `react ^17 || ^18 || ^19`.
|
|
48
|
+
|
|
49
|
+
## Documentation
|
|
50
|
+
|
|
51
|
+
Full docs at **[alaarab.github.io/ogrid](https://alaarab.github.io/ogrid/)**.
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT — Free forever.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { useInlineCellEditorState, useRichSelectState } from '../hooks';
|
|
4
|
+
// ── Shared editor style constants (used across all 3 UI packages) ──
|
|
5
|
+
export const editorWrapperStyle = {
|
|
6
|
+
width: '100%',
|
|
7
|
+
height: '100%',
|
|
8
|
+
display: 'flex',
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
padding: '6px 10px',
|
|
11
|
+
boxSizing: 'border-box',
|
|
12
|
+
overflow: 'hidden',
|
|
13
|
+
minWidth: 0,
|
|
14
|
+
};
|
|
15
|
+
export const editorInputStyle = {
|
|
16
|
+
width: '100%',
|
|
17
|
+
padding: 0,
|
|
18
|
+
border: 'none',
|
|
19
|
+
background: 'transparent',
|
|
20
|
+
color: 'inherit',
|
|
21
|
+
font: 'inherit',
|
|
22
|
+
fontSize: '13px',
|
|
23
|
+
outline: 'none',
|
|
24
|
+
minWidth: 0,
|
|
25
|
+
};
|
|
26
|
+
export const richSelectWrapperStyle = {
|
|
27
|
+
...editorWrapperStyle,
|
|
28
|
+
position: 'relative',
|
|
29
|
+
};
|
|
30
|
+
export const richSelectDropdownStyle = {
|
|
31
|
+
position: 'absolute',
|
|
32
|
+
top: '100%',
|
|
33
|
+
left: 0,
|
|
34
|
+
right: 0,
|
|
35
|
+
maxHeight: 200,
|
|
36
|
+
overflowY: 'auto',
|
|
37
|
+
background: 'var(--ogrid-bg, #fff)',
|
|
38
|
+
border: '1px solid var(--ogrid-border, #ccc)',
|
|
39
|
+
zIndex: 10,
|
|
40
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.2)',
|
|
41
|
+
};
|
|
42
|
+
export const richSelectOptionStyle = {
|
|
43
|
+
padding: '6px 8px',
|
|
44
|
+
cursor: 'pointer',
|
|
45
|
+
color: 'var(--ogrid-fg, #242424)',
|
|
46
|
+
};
|
|
47
|
+
export const richSelectOptionHighlightedStyle = {
|
|
48
|
+
...richSelectOptionStyle,
|
|
49
|
+
background: 'var(--ogrid-bg-hover, #e8f0fe)',
|
|
50
|
+
};
|
|
51
|
+
export const richSelectNoMatchesStyle = {
|
|
52
|
+
padding: '6px 8px',
|
|
53
|
+
color: 'var(--ogrid-muted, #999)',
|
|
54
|
+
};
|
|
55
|
+
export const selectEditorStyle = {
|
|
56
|
+
width: '100%',
|
|
57
|
+
padding: 0,
|
|
58
|
+
border: 'none',
|
|
59
|
+
background: 'transparent',
|
|
60
|
+
color: 'inherit',
|
|
61
|
+
font: 'inherit',
|
|
62
|
+
fontSize: '13px',
|
|
63
|
+
cursor: 'pointer',
|
|
64
|
+
outline: 'none',
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Base inline cell editor with shared logic for all editor types except checkbox and select
|
|
68
|
+
* (which are framework-specific). Used by all 3 UI packages to avoid duplication.
|
|
69
|
+
*
|
|
70
|
+
* Usage:
|
|
71
|
+
* - Radix: Pass Radix Checkbox/native select via render props
|
|
72
|
+
* - Fluent: Pass Fluent Checkbox/Select via render props
|
|
73
|
+
* - Material: Pass MUI Checkbox/Select via render props
|
|
74
|
+
*/
|
|
75
|
+
export function BaseInlineCellEditor(props) {
|
|
76
|
+
const { value, column, editorType, onCommit, onCancel, renderCheckbox, renderSelect } = props;
|
|
77
|
+
const wrapperRef = React.useRef(null);
|
|
78
|
+
const { localValue, setLocalValue, handleKeyDown, handleBlur, commit, cancel } = useInlineCellEditorState({ value, editorType, onCommit, onCancel });
|
|
79
|
+
const richSelectValues = column.cellEditorParams?.values ?? [];
|
|
80
|
+
const richSelectFormatValue = column.cellEditorParams?.formatValue;
|
|
81
|
+
const richSelect = useRichSelectState({
|
|
82
|
+
values: richSelectValues,
|
|
83
|
+
formatValue: richSelectFormatValue,
|
|
84
|
+
initialValue: value,
|
|
85
|
+
onCommit,
|
|
86
|
+
onCancel,
|
|
87
|
+
});
|
|
88
|
+
React.useEffect(() => {
|
|
89
|
+
const input = wrapperRef.current?.querySelector('input');
|
|
90
|
+
input?.focus();
|
|
91
|
+
}, []);
|
|
92
|
+
// Rich select (shared across all frameworks)
|
|
93
|
+
if (editorType === 'richSelect') {
|
|
94
|
+
return (_jsxs("div", { ref: wrapperRef, style: richSelectWrapperStyle, children: [_jsx("input", { type: "text", value: richSelect.searchText, onChange: (e) => richSelect.setSearchText(e.target.value), onKeyDown: richSelect.handleKeyDown, placeholder: "Search...", autoFocus: true, style: editorInputStyle }), _jsxs("div", { style: richSelectDropdownStyle, role: "listbox", children: [richSelect.filteredValues.map((v, i) => (_jsx("div", { role: "option", "aria-selected": i === richSelect.highlightedIndex, onClick: () => richSelect.selectValue(v), style: i === richSelect.highlightedIndex ? richSelectOptionHighlightedStyle : richSelectOptionStyle, children: richSelect.getDisplayText(v) }, String(v)))), richSelect.filteredValues.length === 0 && (_jsx("div", { style: richSelectNoMatchesStyle, children: "No matches" }))] })] }));
|
|
95
|
+
}
|
|
96
|
+
// Checkbox (framework-specific)
|
|
97
|
+
if (editorType === 'checkbox') {
|
|
98
|
+
const checked = value === true;
|
|
99
|
+
return _jsx(_Fragment, { children: renderCheckbox(checked, (val) => commit(val), cancel) });
|
|
100
|
+
}
|
|
101
|
+
// Select (framework-specific)
|
|
102
|
+
if (editorType === 'select') {
|
|
103
|
+
const values = column.cellEditorParams?.values ?? [];
|
|
104
|
+
return _jsx(_Fragment, { children: renderSelect(value, values, commit, cancel) });
|
|
105
|
+
}
|
|
106
|
+
// Date editor (shared across all frameworks)
|
|
107
|
+
if (editorType === 'date') {
|
|
108
|
+
return (_jsx("div", { ref: wrapperRef, style: editorWrapperStyle, children: _jsx("input", { type: "date", value: localValue, onChange: (e) => setLocalValue(e.target.value), onBlur: handleBlur, onKeyDown: handleKeyDown, style: editorInputStyle, autoFocus: true }) }));
|
|
109
|
+
}
|
|
110
|
+
// Text editor (default, shared across all frameworks)
|
|
111
|
+
return (_jsx("div", { ref: wrapperRef, style: editorWrapperStyle, children: _jsx("input", { type: "text", value: localValue, onChange: (e) => setLocalValue(e.target.value), onBlur: handleBlur, onKeyDown: handleKeyDown, style: editorInputStyle, autoFocus: true }) }));
|
|
112
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
const DEFAULT_FALLBACK_STYLE = {
|
|
4
|
+
color: '#d32f2f',
|
|
5
|
+
fontSize: '0.75rem',
|
|
6
|
+
padding: '2px 4px',
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Error boundary for cell renderers and custom editors.
|
|
10
|
+
* Prevents a runtime error in a cell from crashing the entire grid.
|
|
11
|
+
*/
|
|
12
|
+
export class CellErrorBoundary extends React.Component {
|
|
13
|
+
constructor(props) {
|
|
14
|
+
super(props);
|
|
15
|
+
this.state = { hasError: false };
|
|
16
|
+
}
|
|
17
|
+
static getDerivedStateFromError() {
|
|
18
|
+
return { hasError: true };
|
|
19
|
+
}
|
|
20
|
+
componentDidCatch(error, errorInfo) {
|
|
21
|
+
if (this.props.onError) {
|
|
22
|
+
this.props.onError(error, errorInfo);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
componentDidUpdate(prevProps) {
|
|
26
|
+
// Reset error state when children change (e.g., navigating to a different cell)
|
|
27
|
+
if (prevProps.children !== this.props.children && this.state.hasError) {
|
|
28
|
+
this.setState({ hasError: false });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
resetErrorBoundary() {
|
|
32
|
+
this.setState({ hasError: false });
|
|
33
|
+
}
|
|
34
|
+
render() {
|
|
35
|
+
if (this.state.hasError) {
|
|
36
|
+
if (this.props.fallback !== undefined) {
|
|
37
|
+
return this.props.fallback;
|
|
38
|
+
}
|
|
39
|
+
return _jsx("span", { style: DEFAULT_FALLBACK_STYLE, children: "\u26A0 Error" });
|
|
40
|
+
}
|
|
41
|
+
return this.props.children;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const clearButtonStyle = { background: 'none', border: 'none', color: 'inherit', textDecoration: 'underline', cursor: 'pointer', padding: 0, font: 'inherit' };
|
|
3
|
+
/**
|
|
4
|
+
* Headless empty state component with default rendering logic.
|
|
5
|
+
* Framework-specific wrappers provide styling.
|
|
6
|
+
*
|
|
7
|
+
* Default behavior:
|
|
8
|
+
* - Shows "No results found" title
|
|
9
|
+
* - If hasActiveFilters=true: shows "clear all filters" link
|
|
10
|
+
* - If message provided: shows custom message
|
|
11
|
+
* - If render provided: uses custom renderer
|
|
12
|
+
*/
|
|
13
|
+
export function EmptyState(props) {
|
|
14
|
+
const { message, hasActiveFilters, onClearAll, render } = props;
|
|
15
|
+
if (render) {
|
|
16
|
+
return _jsx(_Fragment, { children: render() });
|
|
17
|
+
}
|
|
18
|
+
return (_jsx(_Fragment, { children: message != null ? (message) : hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx("button", { type: "button", onClick: onClearAll, style: clearButtonStyle, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut } from '../utils';
|
|
4
|
+
const menuPositionStyle = (x, y) => ({ left: x, top: y });
|
|
5
|
+
export function GridContextMenu(props) {
|
|
6
|
+
const { x, y, hasSelection, canUndo, canRedo, onClose, onCopy, onCut, onPaste, onSelectAll, onUndo, onRedo, classNames } = props;
|
|
7
|
+
const ref = React.useRef(null);
|
|
8
|
+
const handlers = React.useMemo(() => getContextMenuHandlers({ onCopy, onCut, onPaste, onSelectAll, onUndo, onRedo, onClose }), [onCopy, onCut, onPaste, onSelectAll, onUndo, onRedo, onClose]);
|
|
9
|
+
const isDisabled = React.useCallback((item) => {
|
|
10
|
+
if (item.disabledWhenNoSelection && !hasSelection)
|
|
11
|
+
return true;
|
|
12
|
+
if (item.id === 'undo' && !canUndo)
|
|
13
|
+
return true;
|
|
14
|
+
if (item.id === 'redo' && !canRedo)
|
|
15
|
+
return true;
|
|
16
|
+
return false;
|
|
17
|
+
}, [hasSelection, canUndo, canRedo]);
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
const handleClickOutside = (e) => {
|
|
20
|
+
if (ref.current && !ref.current.contains(e.target))
|
|
21
|
+
onClose();
|
|
22
|
+
};
|
|
23
|
+
const handleKeyDown = (e) => {
|
|
24
|
+
if (e.key === 'Escape')
|
|
25
|
+
onClose();
|
|
26
|
+
};
|
|
27
|
+
document.addEventListener('mousedown', handleClickOutside, true);
|
|
28
|
+
document.addEventListener('keydown', handleKeyDown, true);
|
|
29
|
+
return () => {
|
|
30
|
+
document.removeEventListener('mousedown', handleClickOutside, true);
|
|
31
|
+
document.removeEventListener('keydown', handleKeyDown, true);
|
|
32
|
+
};
|
|
33
|
+
}, [onClose]);
|
|
34
|
+
return (_jsx("div", { ref: ref, className: classNames?.contextMenu, role: "menu", style: menuPositionStyle(x, y), "aria-label": "Grid context menu", children: GRID_CONTEXT_MENU_ITEMS.map((item) => (_jsxs(React.Fragment, { children: [item.dividerBefore && _jsx("div", { className: classNames?.contextMenuDivider }), _jsxs("button", { type: "button", className: classNames?.contextMenuItem, onClick: handlers[item.id], disabled: isDisabled(item), children: [_jsx("span", { className: classNames?.contextMenuItemLabel, children: item.label }), item.shortcut && (_jsx("span", { className: classNames?.contextMenuItemShortcut, children: formatShortcut(item.shortcut) }))] })] }, item.id))) }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* MarchingAntsOverlay — Renders range overlays on top of the grid:
|
|
4
|
+
*
|
|
5
|
+
* 1. **Selection range**: solid green border around the current selection
|
|
6
|
+
* 2. **Copy/Cut range**: animated dashed border (marching ants) like Excel
|
|
7
|
+
*
|
|
8
|
+
* Uses SVG rects positioned via cell data-attribute measurements.
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
11
|
+
const MARCHING_ANTS_ANIMATION = { animation: 'ogrid-marching-ants 0.5s linear infinite' };
|
|
12
|
+
// Inject the @keyframes rule once into <head> (deduplicates across multiple OGrid instances / module copies)
|
|
13
|
+
function ensureKeyframes() {
|
|
14
|
+
if (typeof document === 'undefined')
|
|
15
|
+
return;
|
|
16
|
+
if (document.getElementById('ogrid-marching-ants-keyframes'))
|
|
17
|
+
return;
|
|
18
|
+
const style = document.createElement('style');
|
|
19
|
+
style.id = 'ogrid-marching-ants-keyframes';
|
|
20
|
+
style.textContent =
|
|
21
|
+
'@keyframes ogrid-marching-ants{to{stroke-dashoffset:-8}}';
|
|
22
|
+
document.head.appendChild(style);
|
|
23
|
+
}
|
|
24
|
+
/** Measure the bounding rect of a range within a container. */
|
|
25
|
+
function measureRange(container, range, colOffset) {
|
|
26
|
+
const startGlobalCol = range.startCol + colOffset;
|
|
27
|
+
const endGlobalCol = range.endCol + colOffset;
|
|
28
|
+
const topLeft = container.querySelector(`[data-row-index="${range.startRow}"][data-col-index="${startGlobalCol}"]`);
|
|
29
|
+
const bottomRight = container.querySelector(`[data-row-index="${range.endRow}"][data-col-index="${endGlobalCol}"]`);
|
|
30
|
+
if (!topLeft || !bottomRight)
|
|
31
|
+
return null;
|
|
32
|
+
const cRect = container.getBoundingClientRect();
|
|
33
|
+
const tlRect = topLeft.getBoundingClientRect();
|
|
34
|
+
const brRect = bottomRight.getBoundingClientRect();
|
|
35
|
+
return {
|
|
36
|
+
top: tlRect.top - cRect.top,
|
|
37
|
+
left: tlRect.left - cRect.left,
|
|
38
|
+
width: brRect.right - tlRect.left,
|
|
39
|
+
height: brRect.bottom - tlRect.top,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function MarchingAntsOverlay({ containerRef, selectionRange, copyRange, cutRange, colOffset, }) {
|
|
43
|
+
const [selRect, setSelRect] = useState(null);
|
|
44
|
+
const [clipRect, setClipRect] = useState(null);
|
|
45
|
+
const rafRef = useRef(0);
|
|
46
|
+
const clipRange = copyRange ?? cutRange;
|
|
47
|
+
const measureAll = useCallback(() => {
|
|
48
|
+
const container = containerRef.current;
|
|
49
|
+
if (!container) {
|
|
50
|
+
setSelRect(null);
|
|
51
|
+
setClipRect(null);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
setSelRect(selectionRange ? measureRange(container, selectionRange, colOffset) : null);
|
|
55
|
+
setClipRect(clipRange ? measureRange(container, clipRange, colOffset) : null);
|
|
56
|
+
}, [selectionRange, clipRange, containerRef, colOffset]);
|
|
57
|
+
// Inject keyframes on mount
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
ensureKeyframes();
|
|
60
|
+
}, []);
|
|
61
|
+
// Measure when any range changes; re-measure on resize
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!selectionRange && !clipRange) {
|
|
64
|
+
setSelRect(null);
|
|
65
|
+
setClipRect(null);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Delay one frame so cells are rendered
|
|
69
|
+
rafRef.current = requestAnimationFrame(measureAll);
|
|
70
|
+
const container = containerRef.current;
|
|
71
|
+
let ro;
|
|
72
|
+
if (container) {
|
|
73
|
+
ro = new ResizeObserver(measureAll);
|
|
74
|
+
ro.observe(container);
|
|
75
|
+
}
|
|
76
|
+
return () => {
|
|
77
|
+
cancelAnimationFrame(rafRef.current);
|
|
78
|
+
ro?.disconnect();
|
|
79
|
+
};
|
|
80
|
+
}, [selectionRange, clipRange, measureAll, containerRef]);
|
|
81
|
+
if (!selRect && !clipRect)
|
|
82
|
+
return null;
|
|
83
|
+
// When clipboard range matches the selection range, hide the solid selection border
|
|
84
|
+
// so the marching ants animation is clearly visible (not obscured by solid stroke underneath).
|
|
85
|
+
const clipRangeMatchesSel = selectionRange != null &&
|
|
86
|
+
clipRange != null &&
|
|
87
|
+
selectionRange.startRow === clipRange.startRow &&
|
|
88
|
+
selectionRange.startCol === clipRange.startCol &&
|
|
89
|
+
selectionRange.endRow === clipRange.endRow &&
|
|
90
|
+
selectionRange.endCol === clipRange.endCol;
|
|
91
|
+
return (_jsxs(_Fragment, { children: [selRect && !clipRangeMatchesSel && (_jsx("svg", { style: {
|
|
92
|
+
position: 'absolute',
|
|
93
|
+
top: selRect.top,
|
|
94
|
+
left: selRect.left,
|
|
95
|
+
width: selRect.width,
|
|
96
|
+
height: selRect.height,
|
|
97
|
+
pointerEvents: 'none',
|
|
98
|
+
zIndex: 4,
|
|
99
|
+
overflow: 'visible',
|
|
100
|
+
}, "aria-hidden": "true", children: _jsx("rect", { x: "1", y: "1", width: Math.max(0, selRect.width - 2), height: Math.max(0, selRect.height - 2), fill: "none", stroke: "var(--ogrid-selection, #217346)", strokeWidth: "2" }) })), clipRect && (_jsx("svg", { style: {
|
|
101
|
+
position: 'absolute',
|
|
102
|
+
top: clipRect.top,
|
|
103
|
+
left: clipRect.left,
|
|
104
|
+
width: clipRect.width,
|
|
105
|
+
height: clipRect.height,
|
|
106
|
+
pointerEvents: 'none',
|
|
107
|
+
zIndex: 5,
|
|
108
|
+
overflow: 'visible',
|
|
109
|
+
}, "aria-hidden": "true", children: _jsx("rect", { x: "1", y: "1", width: Math.max(0, clipRect.width - 2), height: Math.max(0, clipRect.height - 2), fill: "none", stroke: "var(--ogrid-selection, #217346)", strokeWidth: "2", strokeDasharray: "4 4", style: MARCHING_ANTS_ANIMATION }) }))] }));
|
|
110
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { SideBar } from './SideBar';
|
|
3
|
+
import { GRID_BORDER_RADIUS } from '@alaarab/ogrid-core';
|
|
4
|
+
// Stable style objects (avoid re-creating on every render)
|
|
5
|
+
const borderedContainerStyle = {
|
|
6
|
+
border: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
7
|
+
borderRadius: GRID_BORDER_RADIUS,
|
|
8
|
+
overflow: 'hidden',
|
|
9
|
+
display: 'flex',
|
|
10
|
+
flexDirection: 'column',
|
|
11
|
+
flex: 1,
|
|
12
|
+
minHeight: 0,
|
|
13
|
+
background: 'var(--ogrid-bg, #fff)',
|
|
14
|
+
};
|
|
15
|
+
const toolbarStripBase = {
|
|
16
|
+
display: 'flex',
|
|
17
|
+
justifyContent: 'space-between',
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
padding: '6px 12px',
|
|
20
|
+
background: 'var(--ogrid-header-bg, #f5f5f5)',
|
|
21
|
+
gap: 8,
|
|
22
|
+
flexWrap: 'wrap',
|
|
23
|
+
minHeight: 0,
|
|
24
|
+
};
|
|
25
|
+
/** Toolbar strip with border-bottom (when it's the only toolbar row). */
|
|
26
|
+
const toolbarStripStyle = {
|
|
27
|
+
...toolbarStripBase,
|
|
28
|
+
borderBottom: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
29
|
+
};
|
|
30
|
+
/** Toolbar strip without border-bottom (when toolbarBelow follows — it owns the border). */
|
|
31
|
+
const toolbarStripNoBorderStyle = toolbarStripBase;
|
|
32
|
+
const toolbarSectionStyle = {
|
|
33
|
+
display: 'flex',
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
gap: 8,
|
|
36
|
+
};
|
|
37
|
+
/** Secondary toolbar row (e.g. active filter chips). Matches toolbar strip styling. */
|
|
38
|
+
const toolbarBelowStyle = {
|
|
39
|
+
borderBottom: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
40
|
+
padding: '6px 12px',
|
|
41
|
+
background: 'var(--ogrid-header-bg, #f5f5f5)',
|
|
42
|
+
};
|
|
43
|
+
const footerStripStyle = {
|
|
44
|
+
borderTop: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
45
|
+
background: 'var(--ogrid-header-bg, #f5f5f5)',
|
|
46
|
+
padding: '6px 12px',
|
|
47
|
+
};
|
|
48
|
+
const gridAreaFlexStyle = {
|
|
49
|
+
width: '100%',
|
|
50
|
+
minWidth: 0,
|
|
51
|
+
minHeight: 0,
|
|
52
|
+
flex: 1,
|
|
53
|
+
display: 'flex',
|
|
54
|
+
};
|
|
55
|
+
const gridAreaSoloStyle = {
|
|
56
|
+
width: '100%',
|
|
57
|
+
minWidth: 0,
|
|
58
|
+
minHeight: 0,
|
|
59
|
+
flex: 1,
|
|
60
|
+
display: 'flex',
|
|
61
|
+
flexDirection: 'column',
|
|
62
|
+
};
|
|
63
|
+
const gridChildStyle = {
|
|
64
|
+
flex: 1,
|
|
65
|
+
minWidth: 0,
|
|
66
|
+
minHeight: 0,
|
|
67
|
+
display: 'flex',
|
|
68
|
+
flexDirection: 'column',
|
|
69
|
+
};
|
|
70
|
+
const rootStyle = {
|
|
71
|
+
display: 'flex',
|
|
72
|
+
flexDirection: 'column',
|
|
73
|
+
height: '100%',
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Renders OGrid layout as a unified bordered container:
|
|
77
|
+
* ┌────────────────────────────────────┐
|
|
78
|
+
* │ [toolbar strip] │
|
|
79
|
+
* ├────────────────────────────────────┤
|
|
80
|
+
* │ [sidebar]? [grid] │
|
|
81
|
+
* ├────────────────────────────────────┤
|
|
82
|
+
* │ [footer strip / pagination] │
|
|
83
|
+
* └────────────────────────────────────┘
|
|
84
|
+
*/
|
|
85
|
+
export function OGridLayout(props) {
|
|
86
|
+
const { containerComponent: Container = 'div', containerProps = {}, className, toolbar, toolbarEnd, toolbarBelow, children, pagination, sideBar, } = props;
|
|
87
|
+
const hasSideBar = sideBar != null;
|
|
88
|
+
const sideBarPosition = sideBar?.position ?? 'right';
|
|
89
|
+
const hasToolbar = toolbar != null || toolbarEnd != null;
|
|
90
|
+
return (_jsx(Container, { className: className, style: rootStyle, ...containerProps, children: _jsxs("div", { style: borderedContainerStyle, children: [hasToolbar && (_jsxs("div", { style: toolbarBelow ? toolbarStripNoBorderStyle : toolbarStripStyle, children: [_jsx("div", { style: toolbarSectionStyle, children: toolbar }), _jsx("div", { style: toolbarSectionStyle, children: toolbarEnd })] })), toolbarBelow && (_jsx("div", { style: toolbarBelowStyle, children: toolbarBelow })), hasSideBar ? (_jsxs("div", { style: gridAreaFlexStyle, children: [sideBarPosition === 'left' && _jsx(SideBar, { ...sideBar }), _jsx("div", { style: gridChildStyle, children: children }), sideBarPosition !== 'left' && _jsx(SideBar, { ...sideBar })] })) : (_jsx("div", { style: gridAreaSoloStyle, children: children })), pagination && (_jsx("div", { style: footerStripStyle, children: pagination }))] }) }));
|
|
91
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
const PANEL_WIDTH = 240;
|
|
3
|
+
const TAB_WIDTH = 36;
|
|
4
|
+
const PANEL_LABELS = {
|
|
5
|
+
columns: 'Columns',
|
|
6
|
+
filters: 'Filters',
|
|
7
|
+
};
|
|
8
|
+
// --- Stable style objects (avoid re-creating on every render) ---
|
|
9
|
+
const tabStripBaseStyle = {
|
|
10
|
+
display: 'flex',
|
|
11
|
+
flexDirection: 'column',
|
|
12
|
+
width: TAB_WIDTH,
|
|
13
|
+
background: 'var(--ogrid-header-bg, #f5f5f5)',
|
|
14
|
+
};
|
|
15
|
+
const tabStripBorderLeft = { ...tabStripBaseStyle, borderLeft: '1px solid var(--ogrid-border, #e0e0e0)' };
|
|
16
|
+
const tabStripBorderRight = { ...tabStripBaseStyle, borderRight: '1px solid var(--ogrid-border, #e0e0e0)' };
|
|
17
|
+
const tabButtonBase = {
|
|
18
|
+
width: TAB_WIDTH,
|
|
19
|
+
height: TAB_WIDTH,
|
|
20
|
+
border: 'none',
|
|
21
|
+
cursor: 'pointer',
|
|
22
|
+
color: 'var(--ogrid-fg, #242424)',
|
|
23
|
+
fontSize: 14,
|
|
24
|
+
display: 'flex',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
justifyContent: 'center',
|
|
27
|
+
};
|
|
28
|
+
const tabButtonActive = { ...tabButtonBase, background: 'var(--ogrid-bg, #fff)', fontWeight: 'bold' };
|
|
29
|
+
const tabButtonInactive = { ...tabButtonBase, background: 'transparent', fontWeight: 'normal' };
|
|
30
|
+
const panelContainerBase = {
|
|
31
|
+
width: PANEL_WIDTH,
|
|
32
|
+
display: 'flex',
|
|
33
|
+
flexDirection: 'column',
|
|
34
|
+
overflow: 'hidden',
|
|
35
|
+
background: 'var(--ogrid-bg, #fff)',
|
|
36
|
+
color: 'var(--ogrid-fg, #242424)',
|
|
37
|
+
};
|
|
38
|
+
const panelContainerBorderLeft = { ...panelContainerBase, borderLeft: '1px solid var(--ogrid-border, #e0e0e0)' };
|
|
39
|
+
const panelContainerBorderRight = { ...panelContainerBase, borderRight: '1px solid var(--ogrid-border, #e0e0e0)' };
|
|
40
|
+
const panelHeaderStyle = {
|
|
41
|
+
display: 'flex',
|
|
42
|
+
justifyContent: 'space-between',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
padding: '8px 12px',
|
|
45
|
+
borderBottom: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
46
|
+
fontWeight: 600,
|
|
47
|
+
};
|
|
48
|
+
const closeButtonStyle = { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 16, color: 'var(--ogrid-fg, #242424)' };
|
|
49
|
+
const panelBodyStyle = { flex: 1, overflowY: 'auto', padding: '8px 12px' };
|
|
50
|
+
const sideBarRootStyle = { display: 'flex', flexDirection: 'row', flexShrink: 0 };
|
|
51
|
+
const buttonRowStyle = { display: 'flex', gap: 8, marginBottom: 8 };
|
|
52
|
+
const actionButtonStyle = { flex: 1, cursor: 'pointer', background: 'var(--ogrid-bg-subtle, #f3f2f1)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4, padding: '4px 8px' };
|
|
53
|
+
const checkboxLabelStyle = { display: 'flex', alignItems: 'center', gap: 6, padding: '2px 0', cursor: 'pointer' };
|
|
54
|
+
const noFilterStyle = { color: 'var(--ogrid-muted, #999)', fontStyle: 'italic' };
|
|
55
|
+
const filterGroupStyle = { marginBottom: 12 };
|
|
56
|
+
const filterLabelStyle = { fontWeight: 500, marginBottom: 4, fontSize: 13 };
|
|
57
|
+
const textInputStyle = { width: '100%', boxSizing: 'border-box', padding: '4px 6px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4 };
|
|
58
|
+
const dateContainerStyle = { display: 'flex', flexDirection: 'column', gap: 4 };
|
|
59
|
+
const dateLabelStyle = { display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 };
|
|
60
|
+
const dateInputStyle = { flex: 1, padding: '2px 4px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4 };
|
|
61
|
+
const multiSelectContainerStyle = { maxHeight: 120, overflowY: 'auto' };
|
|
62
|
+
const multiSelectLabelStyle = { display: 'flex', alignItems: 'center', gap: 4, padding: '1px 0', cursor: 'pointer', fontSize: 13 };
|
|
63
|
+
export function SideBar(props) {
|
|
64
|
+
const { activePanel, onPanelChange, panels, position, columns, visibleColumns, onVisibilityChange, onSetVisibleColumns, filterableColumns, filters, onFilterChange, filterOptions, } = props;
|
|
65
|
+
const isOpen = activePanel !== null;
|
|
66
|
+
const handleTabClick = (panel) => {
|
|
67
|
+
onPanelChange(activePanel === panel ? null : panel);
|
|
68
|
+
};
|
|
69
|
+
const tabStripStyle = position === 'right' ? tabStripBorderLeft : tabStripBorderRight;
|
|
70
|
+
const panelContainerStyle = position === 'right' ? panelContainerBorderLeft : panelContainerBorderRight;
|
|
71
|
+
const tabStrip = (_jsx("div", { style: tabStripStyle, role: "tablist", "aria-label": "Side bar tabs", children: panels.map((panel) => (_jsx("button", { role: "tab", "aria-selected": activePanel === panel, "aria-label": PANEL_LABELS[panel], onClick: () => handleTabClick(panel), title: PANEL_LABELS[panel], style: activePanel === panel ? tabButtonActive : tabButtonInactive, children: panel === 'columns' ? '\u2261' : '\u2A65' }, panel))) }));
|
|
72
|
+
const panelContent = isOpen ? (_jsxs("div", { role: "tabpanel", "aria-label": PANEL_LABELS[activePanel], style: panelContainerStyle, children: [_jsxs("div", { style: panelHeaderStyle, children: [_jsx("span", { children: PANEL_LABELS[activePanel] }), _jsx("button", { onClick: () => onPanelChange(null), style: closeButtonStyle, "aria-label": "Close panel", children: "\u00D7" })] }), _jsxs("div", { style: panelBodyStyle, children: [activePanel === 'columns' && (_jsx(ColumnsPanel, { columns: columns, visibleColumns: visibleColumns, onVisibilityChange: onVisibilityChange, onSetVisibleColumns: onSetVisibleColumns })), activePanel === 'filters' && (_jsx(FiltersPanel, { filterableColumns: filterableColumns, filters: filters, onFilterChange: onFilterChange, filterOptions: filterOptions }))] })] })) : null;
|
|
73
|
+
return (_jsxs("div", { style: sideBarRootStyle, role: "complementary", "aria-label": "Side bar", children: [position === 'left' && tabStrip, position === 'left' && panelContent, position === 'right' && panelContent, position === 'right' && tabStrip] }));
|
|
74
|
+
}
|
|
75
|
+
// --- Internal sub-components ---
|
|
76
|
+
function ColumnsPanel(props) {
|
|
77
|
+
const { columns, visibleColumns, onVisibilityChange, onSetVisibleColumns } = props;
|
|
78
|
+
const allVisible = columns.every((c) => visibleColumns.has(c.columnId));
|
|
79
|
+
const handleSelectAll = () => {
|
|
80
|
+
const next = new Set(visibleColumns);
|
|
81
|
+
columns.forEach((c) => next.add(c.columnId));
|
|
82
|
+
onSetVisibleColumns(next);
|
|
83
|
+
};
|
|
84
|
+
const handleClearAll = () => {
|
|
85
|
+
const next = new Set();
|
|
86
|
+
columns.forEach((c) => {
|
|
87
|
+
if (c.required && visibleColumns.has(c.columnId))
|
|
88
|
+
next.add(c.columnId);
|
|
89
|
+
});
|
|
90
|
+
onSetVisibleColumns(next);
|
|
91
|
+
};
|
|
92
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: buttonRowStyle, children: [_jsx("button", { onClick: handleSelectAll, disabled: allVisible, style: actionButtonStyle, children: "Select All" }), _jsx("button", { onClick: handleClearAll, style: actionButtonStyle, children: "Clear All" })] }), columns.map((col) => (_jsxs("label", { style: checkboxLabelStyle, children: [_jsx("input", { type: "checkbox", checked: visibleColumns.has(col.columnId), onChange: (e) => onVisibilityChange(col.columnId, e.target.checked), disabled: col.required }), _jsx("span", { children: col.name })] }, col.columnId)))] }));
|
|
93
|
+
}
|
|
94
|
+
function FiltersPanel(props) {
|
|
95
|
+
const { filterableColumns, filters, onFilterChange, filterOptions } = props;
|
|
96
|
+
if (filterableColumns.length === 0) {
|
|
97
|
+
return _jsx("div", { style: noFilterStyle, children: "No filterable columns" });
|
|
98
|
+
}
|
|
99
|
+
return (_jsx(_Fragment, { children: filterableColumns.map((col) => {
|
|
100
|
+
const filterKey = col.filterField;
|
|
101
|
+
return (_jsxs("div", { style: filterGroupStyle, children: [_jsx("div", { style: filterLabelStyle, children: col.name }), col.filterType === 'text' && (_jsx("input", { type: "text", value: filters[filterKey]?.type === 'text' ? filters[filterKey].value : '', onChange: (e) => onFilterChange(filterKey, e.target.value ? { type: 'text', value: e.target.value } : undefined), placeholder: `Filter ${col.name}...`, "aria-label": `Filter ${col.name}`, style: textInputStyle })), col.filterType === 'date' && (_jsxs("div", { style: dateContainerStyle, children: [_jsxs("label", { style: dateLabelStyle, children: ["From:", _jsx("input", { type: "date", value: filters[filterKey]?.type === 'date' ? (filters[filterKey].value.from ?? '') : '', onChange: (e) => {
|
|
102
|
+
const from = e.target.value || undefined;
|
|
103
|
+
const existingValue = filters[filterKey]?.type === 'date' ? filters[filterKey].value : {};
|
|
104
|
+
const to = existingValue.to;
|
|
105
|
+
onFilterChange(filterKey, from || to ? { type: 'date', value: { from, to } } : undefined);
|
|
106
|
+
}, "aria-label": `${col.name} from date`, style: dateInputStyle })] }), _jsxs("label", { style: dateLabelStyle, children: ["To:", _jsx("input", { type: "date", value: filters[filterKey]?.type === 'date' ? (filters[filterKey].value.to ?? '') : '', onChange: (e) => {
|
|
107
|
+
const to = e.target.value || undefined;
|
|
108
|
+
const existingValue = filters[filterKey]?.type === 'date' ? filters[filterKey].value : {};
|
|
109
|
+
const from = existingValue.from;
|
|
110
|
+
onFilterChange(filterKey, from || to ? { type: 'date', value: { from, to } } : undefined);
|
|
111
|
+
}, "aria-label": `${col.name} to date`, style: dateInputStyle })] })] })), col.filterType === 'multiSelect' && (_jsx("div", { style: multiSelectContainerStyle, role: "group", "aria-label": `${col.name} options`, children: (filterOptions[filterKey] ?? []).map((opt) => {
|
|
112
|
+
const selected = filters[filterKey]?.type === 'multiSelect' ? filters[filterKey].value.includes(opt) : false;
|
|
113
|
+
return (_jsxs("label", { style: multiSelectLabelStyle, children: [_jsx("input", { type: "checkbox", checked: selected, onChange: (e) => {
|
|
114
|
+
const current = filters[filterKey]?.type === 'multiSelect' ? filters[filterKey].value : [];
|
|
115
|
+
const next = e.target.checked
|
|
116
|
+
? [...current, opt]
|
|
117
|
+
: current.filter((v) => v !== opt);
|
|
118
|
+
onFilterChange(filterKey, next.length > 0 ? { type: 'multiSelect', value: next } : undefined);
|
|
119
|
+
} }), _jsx("span", { children: opt })] }, opt));
|
|
120
|
+
}) }))] }, col.columnId));
|
|
121
|
+
}) }));
|
|
122
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getStatusBarParts } from '../utils';
|
|
3
|
+
export function StatusBar({ classNames, ...rest }) {
|
|
4
|
+
const parts = getStatusBarParts(rest);
|
|
5
|
+
return (_jsx("div", { className: classNames?.statusBar, role: "status", "aria-live": "polite", children: parts.map((p) => (_jsxs("span", { className: classNames?.statusBarItem, children: [_jsx("span", { className: classNames?.statusBarLabel, children: p.label }), _jsx("span", { className: classNames?.statusBarValue, children: p.value.toLocaleString() })] }, p.key))) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export { useFilterOptions } from './useFilterOptions';
|
|
2
|
+
export { useOGrid } from './useOGrid';
|
|
3
|
+
export { useActiveCell } from './useActiveCell';
|
|
4
|
+
export { useCellEditing } from './useCellEditing';
|
|
5
|
+
export { useContextMenu } from './useContextMenu';
|
|
6
|
+
export { useCellSelection } from './useCellSelection';
|
|
7
|
+
export { useClipboard } from './useClipboard';
|
|
8
|
+
export { useRowSelection } from './useRowSelection';
|
|
9
|
+
export { useKeyboardNavigation } from './useKeyboardNavigation';
|
|
10
|
+
export { useUndoRedo } from './useUndoRedo';
|
|
11
|
+
export { useDebounce } from './useDebounce';
|
|
12
|
+
export { useFillHandle } from './useFillHandle';
|
|
13
|
+
export { useDataGridState } from './useDataGridState';
|
|
14
|
+
export { useColumnHeaderFilterState } from './useColumnHeaderFilterState';
|
|
15
|
+
export { useTextFilterState } from './useTextFilterState';
|
|
16
|
+
export { useMultiSelectFilterState } from './useMultiSelectFilterState';
|
|
17
|
+
export { usePeopleFilterState } from './usePeopleFilterState';
|
|
18
|
+
export { useDateFilterState } from './useDateFilterState';
|
|
19
|
+
export { useColumnChooserState } from './useColumnChooserState';
|
|
20
|
+
export { useInlineCellEditorState } from './useInlineCellEditorState';
|
|
21
|
+
export { useColumnResize } from './useColumnResize';
|
|
22
|
+
export { useRichSelectState } from './useRichSelectState';
|
|
23
|
+
export { useSideBarState } from './useSideBarState';
|
|
24
|
+
export { useTableLayout } from './useTableLayout';
|
|
25
|
+
export { useLatestRef } from './useLatestRef';
|