@alaarab/ogrid-core 1.8.2 → 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.
Files changed (74) hide show
  1. package/README.md +28 -31
  2. package/dist/esm/constants.js +11 -0
  3. package/dist/esm/index.js +6 -11
  4. package/dist/esm/types/index.js +2 -1
  5. package/dist/esm/utils/clientSideData.js +25 -12
  6. package/dist/esm/utils/columnUtils.js +6 -0
  7. package/dist/esm/utils/gridRowComparator.js +78 -0
  8. package/dist/esm/utils/index.js +1 -1
  9. package/dist/esm/utils/ogridHelpers.js +2 -1
  10. package/dist/esm/utils/paginationHelpers.js +7 -1
  11. package/dist/types/constants.d.ts +11 -0
  12. package/dist/types/index.d.ts +3 -16
  13. package/dist/types/types/columnTypes.d.ts +5 -9
  14. package/dist/types/types/dataGridTypes.d.ts +12 -133
  15. package/dist/types/types/index.d.ts +3 -3
  16. package/dist/types/utils/gridRowComparator.d.ts +57 -0
  17. package/dist/types/utils/index.d.ts +2 -2
  18. package/package.json +6 -24
  19. package/dist/esm/components/GridContextMenu.js +0 -34
  20. package/dist/esm/components/MarchingAntsOverlay.js +0 -109
  21. package/dist/esm/components/OGridLayout.js +0 -90
  22. package/dist/esm/components/SideBar.js +0 -100
  23. package/dist/esm/components/StatusBar.js +0 -6
  24. package/dist/esm/hooks/index.js +0 -19
  25. package/dist/esm/hooks/useActiveCell.js +0 -46
  26. package/dist/esm/hooks/useCellEditing.js +0 -11
  27. package/dist/esm/hooks/useCellSelection.js +0 -318
  28. package/dist/esm/hooks/useClipboard.js +0 -162
  29. package/dist/esm/hooks/useColumnChooserState.js +0 -62
  30. package/dist/esm/hooks/useColumnHeaderFilterState.js +0 -228
  31. package/dist/esm/hooks/useColumnResize.js +0 -69
  32. package/dist/esm/hooks/useContextMenu.js +0 -17
  33. package/dist/esm/hooks/useDataGridState.js +0 -366
  34. package/dist/esm/hooks/useDebounce.js +0 -35
  35. package/dist/esm/hooks/useFillHandle.js +0 -191
  36. package/dist/esm/hooks/useFilterOptions.js +0 -40
  37. package/dist/esm/hooks/useInlineCellEditorState.js +0 -44
  38. package/dist/esm/hooks/useKeyboardNavigation.js +0 -436
  39. package/dist/esm/hooks/useOGrid.js +0 -414
  40. package/dist/esm/hooks/useRichSelectState.js +0 -53
  41. package/dist/esm/hooks/useRowSelection.js +0 -68
  42. package/dist/esm/hooks/useSideBarState.js +0 -34
  43. package/dist/esm/hooks/useUndoRedo.js +0 -82
  44. package/dist/esm/storybook/index.js +0 -1
  45. package/dist/esm/storybook/mockData.js +0 -73
  46. package/dist/esm/utils/dataGridViewModel.js +0 -233
  47. package/dist/types/components/GridContextMenu.d.ts +0 -18
  48. package/dist/types/components/MarchingAntsOverlay.d.ts +0 -15
  49. package/dist/types/components/OGridLayout.d.ts +0 -37
  50. package/dist/types/components/SideBar.d.ts +0 -30
  51. package/dist/types/components/StatusBar.d.ts +0 -24
  52. package/dist/types/hooks/index.d.ts +0 -37
  53. package/dist/types/hooks/useActiveCell.d.ts +0 -13
  54. package/dist/types/hooks/useCellEditing.d.ts +0 -12
  55. package/dist/types/hooks/useCellSelection.d.ts +0 -17
  56. package/dist/types/hooks/useClipboard.d.ts +0 -25
  57. package/dist/types/hooks/useColumnChooserState.d.ts +0 -27
  58. package/dist/types/hooks/useColumnHeaderFilterState.d.ts +0 -72
  59. package/dist/types/hooks/useColumnResize.d.ts +0 -18
  60. package/dist/types/hooks/useContextMenu.d.ts +0 -15
  61. package/dist/types/hooks/useDataGridState.d.ts +0 -136
  62. package/dist/types/hooks/useDebounce.d.ts +0 -9
  63. package/dist/types/hooks/useFillHandle.d.ts +0 -28
  64. package/dist/types/hooks/useFilterOptions.d.ts +0 -16
  65. package/dist/types/hooks/useInlineCellEditorState.d.ts +0 -24
  66. package/dist/types/hooks/useKeyboardNavigation.d.ts +0 -34
  67. package/dist/types/hooks/useOGrid.d.ts +0 -31
  68. package/dist/types/hooks/useRichSelectState.d.ts +0 -17
  69. package/dist/types/hooks/useRowSelection.d.ts +0 -17
  70. package/dist/types/hooks/useSideBarState.d.ts +0 -15
  71. package/dist/types/hooks/useUndoRedo.d.ts +0 -21
  72. package/dist/types/storybook/index.d.ts +0 -2
  73. package/dist/types/storybook/mockData.d.ts +0 -37
  74. package/dist/types/utils/dataGridViewModel.d.ts +0 -169
@@ -7,8 +7,6 @@ export { getDataGridStatusBarConfig } from './dataGridStatusBar';
7
7
  export { getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, } from './paginationHelpers';
8
8
  export type { PaginationViewModel } from './paginationHelpers';
9
9
  export { GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut } from './gridContextMenuHelpers';
10
- export { getHeaderFilterConfig, getCellRenderDescriptor, isRowInRange, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './dataGridViewModel';
11
- export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, CellInteractionHandlers, } from './dataGridViewModel';
12
10
  export type { CsvColumn } from './exportToCsv';
13
11
  export type { StatusBarPart, StatusBarPartsInput } from './statusBarHelpers';
14
12
  export type { GridContextMenuItem, GridContextMenuHandlerProps } from './gridContextMenuHelpers';
@@ -17,3 +15,5 @@ export type { ParseValueResult } from './valueParsers';
17
15
  export { computeAggregations } from './aggregationUtils';
18
16
  export type { AggregationResult } from './aggregationUtils';
19
17
  export { processClientSideData } from './clientSideData';
18
+ export { areGridRowPropsEqual, isRowInRange } from './gridRowComparator';
19
+ export type { GridRowComparatorProps } from './gridRowComparator';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-core",
3
- "version": "1.8.2",
4
- "description": "OGrid core – framework-agnostic types, hooks, and utilities for OGrid data tables.",
3
+ "version": "2.0.0-beta",
4
+ "description": "OGrid core – framework-agnostic types, algorithms, and utilities for OGrid data grids.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
7
7
  "types": "dist/types/index.d.ts",
@@ -16,28 +16,10 @@
16
16
  "build": "rimraf dist && tsc -p tsconfig.build.json",
17
17
  "test": "jest"
18
18
  },
19
- "keywords": [
20
- "ogrid",
21
- "datatable",
22
- "react",
23
- "typescript",
24
- "grid",
25
- "core"
26
- ],
19
+ "keywords": ["ogrid", "datatable", "typescript", "grid", "core"],
27
20
  "author": "Ala Arab",
28
21
  "license": "MIT",
29
- "files": [
30
- "dist",
31
- "README.md",
32
- "LICENSE"
33
- ],
34
- "engines": {
35
- "node": ">=18"
36
- },
37
- "peerDependencies": {
38
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
39
- },
40
- "publishConfig": {
41
- "access": "public"
42
- }
22
+ "files": ["dist", "README.md", "LICENSE"],
23
+ "engines": { "node": ">=18" },
24
+ "publishConfig": { "access": "public" }
43
25
  }
@@ -1,34 +0,0 @@
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/gridContextMenuHelpers';
4
- export function GridContextMenu(props) {
5
- const { x, y, hasSelection, canUndo, canRedo, onClose, classNames } = props;
6
- const ref = React.useRef(null);
7
- const handlers = React.useMemo(() => getContextMenuHandlers(props), [props]);
8
- const isDisabled = React.useCallback((item) => {
9
- if (item.disabledWhenNoSelection && !hasSelection)
10
- return true;
11
- if (item.id === 'undo' && !canUndo)
12
- return true;
13
- if (item.id === 'redo' && !canRedo)
14
- return true;
15
- return false;
16
- }, [hasSelection, canUndo, canRedo]);
17
- React.useEffect(() => {
18
- const handleClickOutside = (e) => {
19
- if (ref.current && !ref.current.contains(e.target))
20
- onClose();
21
- };
22
- const handleKeyDown = (e) => {
23
- if (e.key === 'Escape')
24
- onClose();
25
- };
26
- document.addEventListener('mousedown', handleClickOutside, true);
27
- document.addEventListener('keydown', handleKeyDown, true);
28
- return () => {
29
- document.removeEventListener('mousedown', handleClickOutside, true);
30
- document.removeEventListener('keydown', handleKeyDown, true);
31
- };
32
- }, [onClose]);
33
- return (_jsx("div", { ref: ref, className: classNames?.contextMenu, role: "menu", style: { left: x, top: 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))) }));
34
- }
@@ -1,109 +0,0 @@
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
- // Inject the @keyframes rule once into <head>
12
- let styleInjected = false;
13
- function ensureKeyframes() {
14
- if (styleInjected || typeof document === 'undefined')
15
- return;
16
- const style = document.createElement('style');
17
- style.id = 'ogrid-marching-ants-keyframes';
18
- style.textContent =
19
- '@keyframes ogrid-marching-ants{to{stroke-dashoffset:-8}}';
20
- document.head.appendChild(style);
21
- styleInjected = true;
22
- }
23
- /** Measure the bounding rect of a range within a container. */
24
- function measureRange(container, range, colOffset) {
25
- const startGlobalCol = range.startCol + colOffset;
26
- const endGlobalCol = range.endCol + colOffset;
27
- const topLeft = container.querySelector(`[data-row-index="${range.startRow}"][data-col-index="${startGlobalCol}"]`);
28
- const bottomRight = container.querySelector(`[data-row-index="${range.endRow}"][data-col-index="${endGlobalCol}"]`);
29
- if (!topLeft || !bottomRight)
30
- return null;
31
- const cRect = container.getBoundingClientRect();
32
- const tlRect = topLeft.getBoundingClientRect();
33
- const brRect = bottomRight.getBoundingClientRect();
34
- return {
35
- top: tlRect.top - cRect.top,
36
- left: tlRect.left - cRect.left,
37
- width: brRect.right - tlRect.left,
38
- height: brRect.bottom - tlRect.top,
39
- };
40
- }
41
- export function MarchingAntsOverlay({ containerRef, selectionRange, copyRange, cutRange, colOffset, }) {
42
- const [selRect, setSelRect] = useState(null);
43
- const [clipRect, setClipRect] = useState(null);
44
- const rafRef = useRef(0);
45
- const clipRange = copyRange ?? cutRange;
46
- const measureAll = useCallback(() => {
47
- const container = containerRef.current;
48
- if (!container) {
49
- setSelRect(null);
50
- setClipRect(null);
51
- return;
52
- }
53
- setSelRect(selectionRange ? measureRange(container, selectionRange, colOffset) : null);
54
- setClipRect(clipRange ? measureRange(container, clipRange, colOffset) : null);
55
- }, [selectionRange, clipRange, containerRef, colOffset]);
56
- // Inject keyframes on mount
57
- useEffect(() => {
58
- ensureKeyframes();
59
- }, []);
60
- // Measure when any range changes; re-measure on resize
61
- useEffect(() => {
62
- if (!selectionRange && !clipRange) {
63
- setSelRect(null);
64
- setClipRect(null);
65
- return;
66
- }
67
- // Delay one frame so cells are rendered
68
- rafRef.current = requestAnimationFrame(measureAll);
69
- const container = containerRef.current;
70
- let ro;
71
- if (container) {
72
- ro = new ResizeObserver(measureAll);
73
- ro.observe(container);
74
- }
75
- return () => {
76
- cancelAnimationFrame(rafRef.current);
77
- ro?.disconnect();
78
- };
79
- }, [selectionRange, clipRange, measureAll, containerRef]);
80
- if (!selRect && !clipRect)
81
- return null;
82
- // When clipboard range matches the selection range, hide the solid selection border
83
- // so the marching ants animation is clearly visible (not obscured by solid stroke underneath).
84
- const clipRangeMatchesSel = selectionRange != null &&
85
- clipRange != null &&
86
- selectionRange.startRow === clipRange.startRow &&
87
- selectionRange.startCol === clipRange.startCol &&
88
- selectionRange.endRow === clipRange.endRow &&
89
- selectionRange.endCol === clipRange.endCol;
90
- return (_jsxs(_Fragment, { children: [selRect && !clipRangeMatchesSel && (_jsx("svg", { style: {
91
- position: 'absolute',
92
- top: selRect.top,
93
- left: selRect.left,
94
- width: selRect.width,
95
- height: selRect.height,
96
- pointerEvents: 'none',
97
- zIndex: 4,
98
- overflow: 'visible',
99
- }, "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: {
100
- position: 'absolute',
101
- top: clipRect.top,
102
- left: clipRect.left,
103
- width: clipRect.width,
104
- height: clipRect.height,
105
- pointerEvents: 'none',
106
- zIndex: 5,
107
- overflow: 'visible',
108
- }, "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: { animation: 'ogrid-marching-ants 0.5s linear infinite' } }) }))] }));
109
- }
@@ -1,90 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { SideBar } from './SideBar';
3
- // Stable style objects (avoid re-creating on every render)
4
- const borderedContainerStyle = {
5
- border: '1px solid var(--ogrid-border, #e0e0e0)',
6
- borderRadius: 6,
7
- overflow: 'hidden',
8
- display: 'flex',
9
- flexDirection: 'column',
10
- flex: 1,
11
- minHeight: 0,
12
- background: 'var(--ogrid-bg, #fff)',
13
- };
14
- const toolbarStripBase = {
15
- display: 'flex',
16
- justifyContent: 'space-between',
17
- alignItems: 'center',
18
- padding: '6px 12px',
19
- background: 'var(--ogrid-header-bg, #f5f5f5)',
20
- gap: 8,
21
- flexWrap: 'wrap',
22
- minHeight: 0,
23
- };
24
- /** Toolbar strip with border-bottom (when it's the only toolbar row). */
25
- const toolbarStripStyle = {
26
- ...toolbarStripBase,
27
- borderBottom: '1px solid var(--ogrid-border, #e0e0e0)',
28
- };
29
- /** Toolbar strip without border-bottom (when toolbarBelow follows — it owns the border). */
30
- const toolbarStripNoBorderStyle = toolbarStripBase;
31
- const toolbarSectionStyle = {
32
- display: 'flex',
33
- alignItems: 'center',
34
- gap: 8,
35
- };
36
- /** Secondary toolbar row (e.g. active filter chips). Matches toolbar strip styling. */
37
- const toolbarBelowStyle = {
38
- borderBottom: '1px solid var(--ogrid-border, #e0e0e0)',
39
- padding: '6px 12px',
40
- background: 'var(--ogrid-header-bg, #f5f5f5)',
41
- };
42
- const footerStripStyle = {
43
- borderTop: '1px solid var(--ogrid-border, #e0e0e0)',
44
- background: 'var(--ogrid-header-bg, #f5f5f5)',
45
- padding: '6px 12px',
46
- };
47
- const gridAreaFlexStyle = {
48
- width: '100%',
49
- minWidth: 0,
50
- minHeight: 0,
51
- flex: 1,
52
- display: 'flex',
53
- };
54
- const gridAreaSoloStyle = {
55
- width: '100%',
56
- minWidth: 0,
57
- minHeight: 0,
58
- flex: 1,
59
- display: 'flex',
60
- flexDirection: 'column',
61
- };
62
- const gridChildStyle = {
63
- flex: 1,
64
- minWidth: 0,
65
- minHeight: 0,
66
- display: 'flex',
67
- flexDirection: 'column',
68
- };
69
- /**
70
- * Renders OGrid layout as a unified bordered container:
71
- * ┌────────────────────────────────────┐
72
- * │ [toolbar strip] │
73
- * ├────────────────────────────────────┤
74
- * │ [sidebar]? [grid] │
75
- * ├────────────────────────────────────┤
76
- * │ [footer strip / pagination] │
77
- * └────────────────────────────────────┘
78
- */
79
- export function OGridLayout(props) {
80
- const { containerComponent: Container = 'div', containerProps = {}, className, toolbar, toolbarEnd, toolbarBelow, children, pagination, sideBar, } = props;
81
- const hasSideBar = sideBar != null;
82
- const sideBarPosition = sideBar?.position ?? 'right';
83
- const hasToolbar = toolbar != null || toolbarEnd != null;
84
- const rootStyle = {
85
- display: 'flex',
86
- flexDirection: 'column',
87
- height: '100%',
88
- };
89
- 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 }))] }) }));
90
- }
@@ -1,100 +0,0 @@
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
- export function SideBar(props) {
9
- const { activePanel, onPanelChange, panels, position, columns, visibleColumns, onVisibilityChange, onSetVisibleColumns, filterableColumns, filters, onFilterChange, filterOptions, } = props;
10
- const isOpen = activePanel !== null;
11
- const handleTabClick = (panel) => {
12
- onPanelChange(activePanel === panel ? null : panel);
13
- };
14
- const tabStrip = (_jsx("div", { style: {
15
- display: 'flex',
16
- flexDirection: 'column',
17
- width: TAB_WIDTH,
18
- borderLeft: position === 'right' ? '1px solid var(--ogrid-border, #e0e0e0)' : undefined,
19
- borderRight: position === 'left' ? '1px solid var(--ogrid-border, #e0e0e0)' : undefined,
20
- background: 'var(--ogrid-header-bg, #f5f5f5)',
21
- }, 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: {
22
- width: TAB_WIDTH,
23
- height: TAB_WIDTH,
24
- border: 'none',
25
- cursor: 'pointer',
26
- background: activePanel === panel ? 'var(--ogrid-bg, #fff)' : 'transparent',
27
- color: 'var(--ogrid-fg, #242424)',
28
- fontWeight: activePanel === panel ? 'bold' : 'normal',
29
- fontSize: 14,
30
- display: 'flex',
31
- alignItems: 'center',
32
- justifyContent: 'center',
33
- }, children: panel === 'columns' ? '\u2261' : '\u2A65' }, panel))) }));
34
- const panelContent = isOpen ? (_jsxs("div", { role: "tabpanel", "aria-label": PANEL_LABELS[activePanel], style: {
35
- width: PANEL_WIDTH,
36
- display: 'flex',
37
- flexDirection: 'column',
38
- borderLeft: position === 'right' ? '1px solid var(--ogrid-border, #e0e0e0)' : undefined,
39
- borderRight: position === 'left' ? '1px solid var(--ogrid-border, #e0e0e0)' : undefined,
40
- overflow: 'hidden',
41
- background: 'var(--ogrid-bg, #fff)',
42
- color: 'var(--ogrid-fg, #242424)',
43
- }, children: [_jsxs("div", { style: {
44
- display: 'flex',
45
- justifyContent: 'space-between',
46
- alignItems: 'center',
47
- padding: '8px 12px',
48
- borderBottom: '1px solid var(--ogrid-border, #e0e0e0)',
49
- fontWeight: 600,
50
- }, children: [_jsx("span", { children: PANEL_LABELS[activePanel] }), _jsx("button", { onClick: () => onPanelChange(null), style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 16, color: 'var(--ogrid-fg, #242424)' }, "aria-label": "Close panel", children: "\u00D7" })] }), _jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '8px 12px' }, 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;
51
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'row', flexShrink: 0 }, role: "complementary", "aria-label": "Side bar", children: [position === 'left' && tabStrip, position === 'left' && panelContent, position === 'right' && panelContent, position === 'right' && tabStrip] }));
52
- }
53
- // --- Internal sub-components ---
54
- function ColumnsPanel(props) {
55
- const { columns, visibleColumns, onVisibilityChange, onSetVisibleColumns } = props;
56
- const allVisible = columns.every((c) => visibleColumns.has(c.columnId));
57
- const handleSelectAll = () => {
58
- const next = new Set(visibleColumns);
59
- columns.forEach((c) => next.add(c.columnId));
60
- onSetVisibleColumns(next);
61
- };
62
- const handleClearAll = () => {
63
- const next = new Set();
64
- columns.forEach((c) => {
65
- if (c.required && visibleColumns.has(c.columnId))
66
- next.add(c.columnId);
67
- });
68
- onSetVisibleColumns(next);
69
- };
70
- return (_jsxs(_Fragment, { children: [_jsxs("div", { style: { display: 'flex', gap: 8, marginBottom: 8 }, children: [_jsx("button", { onClick: handleSelectAll, disabled: allVisible, style: { 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' }, children: "Select All" }), _jsx("button", { onClick: handleClearAll, style: { 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' }, children: "Clear All" })] }), columns.map((col) => (_jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6, padding: '2px 0', cursor: 'pointer' }, 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)))] }));
71
- }
72
- function FiltersPanel(props) {
73
- const { filterableColumns, filters, onFilterChange, filterOptions } = props;
74
- if (filterableColumns.length === 0) {
75
- return _jsx("div", { style: { color: 'var(--ogrid-muted, #999)', fontStyle: 'italic' }, children: "No filterable columns" });
76
- }
77
- return (_jsx(_Fragment, { children: filterableColumns.map((col) => {
78
- const filterKey = col.filterField;
79
- return (_jsxs("div", { style: { marginBottom: 12 }, children: [_jsx("div", { style: { fontWeight: 500, marginBottom: 4, fontSize: 13 }, 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: { 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 } })), col.filterType === 'date' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }, children: ["From:", _jsx("input", { type: "date", value: filters[filterKey]?.type === 'date' ? (filters[filterKey].value.from ?? '') : '', onChange: (e) => {
80
- const from = e.target.value || undefined;
81
- const existingValue = filters[filterKey]?.type === 'date' ? filters[filterKey].value : {};
82
- const to = existingValue.to;
83
- onFilterChange(filterKey, from || to ? { type: 'date', value: { from, to } } : undefined);
84
- }, "aria-label": `${col.name} from date`, style: { flex: 1, padding: '2px 4px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4 } })] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }, children: ["To:", _jsx("input", { type: "date", value: filters[filterKey]?.type === 'date' ? (filters[filterKey].value.to ?? '') : '', onChange: (e) => {
85
- const to = e.target.value || undefined;
86
- const existingValue = filters[filterKey]?.type === 'date' ? filters[filterKey].value : {};
87
- const from = existingValue.from;
88
- onFilterChange(filterKey, from || to ? { type: 'date', value: { from, to } } : undefined);
89
- }, "aria-label": `${col.name} to date`, style: { flex: 1, padding: '2px 4px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4 } })] })] })), col.filterType === 'multiSelect' && (_jsx("div", { style: { maxHeight: 120, overflowY: 'auto' }, role: "group", "aria-label": `${col.name} options`, children: (filterOptions[filterKey] ?? []).map((opt) => {
90
- const selected = filters[filterKey]?.type === 'multiSelect' ? filters[filterKey].value.includes(opt) : false;
91
- return (_jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, padding: '1px 0', cursor: 'pointer', fontSize: 13 }, children: [_jsx("input", { type: "checkbox", checked: selected, onChange: (e) => {
92
- const current = filters[filterKey]?.type === 'multiSelect' ? filters[filterKey].value : [];
93
- const next = e.target.checked
94
- ? [...current, opt]
95
- : current.filter((v) => v !== opt);
96
- onFilterChange(filterKey, next.length > 0 ? { type: 'multiSelect', value: next } : undefined);
97
- } }), _jsx("span", { children: opt })] }, opt));
98
- }) }))] }, col.columnId));
99
- }) }));
100
- }
@@ -1,6 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { getStatusBarParts } from '../utils/statusBarHelpers';
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
- }
@@ -1,19 +0,0 @@
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 { useColumnChooserState } from './useColumnChooserState';
16
- export { useInlineCellEditorState } from './useInlineCellEditorState';
17
- export { useColumnResize } from './useColumnResize';
18
- export { useRichSelectState } from './useRichSelectState';
19
- export { useSideBarState } from './useSideBarState';
@@ -1,46 +0,0 @@
1
- import { useState, useLayoutEffect, useCallback, useRef } from 'react';
2
- /**
3
- * Tracks the active cell for keyboard navigation.
4
- * When wrapperRef and editingCell are provided, scrolls the active cell into view when it changes (and not editing).
5
- */
6
- export function useActiveCell(wrapperRef, editingCell) {
7
- const [activeCell, _setActiveCell] = useState(null);
8
- const activeCellRef = useRef(activeCell);
9
- activeCellRef.current = activeCell;
10
- // Deduplicating setter — skips state update (and all downstream effects) when
11
- // the cell coordinates haven't actually changed. This prevents re-renders when
12
- // rapidly clicking the same cell.
13
- const setActiveCell = useCallback((cell) => {
14
- const prev = activeCellRef.current;
15
- if (prev === cell)
16
- return;
17
- if (prev && cell && prev.rowIndex === cell.rowIndex && prev.columnIndex === cell.columnIndex)
18
- return;
19
- _setActiveCell(cell);
20
- }, []);
21
- // useLayoutEffect ensures focus moves synchronously before the browser can
22
- // reset focus to body (fixes left/right arrow navigation losing focus)
23
- useLayoutEffect(() => {
24
- if (activeCell == null ||
25
- wrapperRef?.current == null ||
26
- editingCell != null)
27
- return;
28
- const { rowIndex, columnIndex } = activeCell;
29
- const selector = `[data-row-index="${rowIndex}"][data-col-index="${columnIndex}"]`;
30
- const cell = wrapperRef.current.querySelector(selector);
31
- if (cell) {
32
- if (typeof cell.scrollIntoView === 'function') {
33
- // Account for sticky <thead> so scrollIntoView doesn't leave
34
- // the cell hidden behind the header.
35
- const thead = wrapperRef.current.querySelector('thead');
36
- const headerHeight = thead ? thead.getBoundingClientRect().height : 0;
37
- cell.style.scrollMarginTop = `${headerHeight}px`;
38
- cell.scrollIntoView({ block: 'nearest', inline: 'nearest' });
39
- }
40
- if (document.activeElement !== cell && typeof cell.focus === 'function') {
41
- cell.focus();
42
- }
43
- }
44
- }, [activeCell, editingCell, wrapperRef]);
45
- return { activeCell, setActiveCell };
46
- }
@@ -1,11 +0,0 @@
1
- import { useState } from 'react';
2
- export function useCellEditing() {
3
- const [editingCell, setEditingCell] = useState(null);
4
- const [pendingEditorValue, setPendingEditorValue] = useState(undefined);
5
- return {
6
- editingCell,
7
- setEditingCell,
8
- pendingEditorValue,
9
- setPendingEditorValue,
10
- };
11
- }