@opencorestack/opengridx 0.1.6 → 0.1.8

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 (130) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +9 -0
  3. package/dist/index.d.ts +12 -1
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +2505 -2450
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +12 -12
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +177 -0
  10. package/docs/README.md +60 -0
  11. package/docs/components/cell.md +44 -0
  12. package/docs/components/column-group-header.md +57 -0
  13. package/docs/components/column-resize.md +63 -0
  14. package/docs/components/column-visibility.md +61 -0
  15. package/docs/components/datagrid.md +70 -0
  16. package/docs/components/filter-panel.md +26 -0
  17. package/docs/components/header.md +39 -0
  18. package/docs/components/pagination.md +34 -0
  19. package/docs/components/row.md +31 -0
  20. package/docs/components/toolbar.md +40 -0
  21. package/docs/components/tooltip.md +68 -0
  22. package/docs/customization/slots-api.md +227 -0
  23. package/docs/customization/theming.md +293 -0
  24. package/docs/features/aggregation-pivot.md +79 -0
  25. package/docs/features/cell-spanning.md +520 -0
  26. package/docs/features/clipboard.md +76 -0
  27. package/docs/features/custom-pagination.md +143 -0
  28. package/docs/features/data-source.md +72 -0
  29. package/docs/features/editing-reordering.md +99 -0
  30. package/docs/features/export-guide.md +434 -0
  31. package/docs/features/filtering.md +83 -0
  32. package/docs/features/infinite-scroll.md +87 -0
  33. package/docs/features/list-view.md +65 -0
  34. package/docs/features/loading-states.md +68 -0
  35. package/docs/features/master-detail.md +46 -0
  36. package/docs/features/pinning.md +67 -0
  37. package/docs/features/selection.md +70 -0
  38. package/docs/features/sorting-pagination.md +66 -0
  39. package/docs/features/state-persistence.md +67 -0
  40. package/docs/features/tree-data-grouping.md +67 -0
  41. package/docs/features/virtualization.md +188 -0
  42. package/docs/roadmap.md +71 -0
  43. package/lib/components/Cell/Cell.css +119 -0
  44. package/lib/components/Cell/Cell.tsx +264 -0
  45. package/lib/components/Cell/GridEditInputCell.css +62 -0
  46. package/lib/components/Cell/GridEditInputCell.tsx +152 -0
  47. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +67 -0
  48. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +220 -0
  49. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +31 -0
  50. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +85 -0
  51. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +137 -0
  52. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +190 -0
  53. package/lib/components/DataGrid/DataGrid.css +224 -0
  54. package/lib/components/DataGrid/DataGrid.tsx +2269 -0
  55. package/lib/components/DetailPanel/DetailPanel.css +36 -0
  56. package/lib/components/DetailPanel/DetailPanel.tsx +34 -0
  57. package/lib/components/FilterPanel/FilterPanel.css +196 -0
  58. package/lib/components/FilterPanel/FilterPanel.tsx +214 -0
  59. package/lib/components/FilterPanel/index.ts +1 -0
  60. package/lib/components/Header/ColumnMenu.css +68 -0
  61. package/lib/components/Header/ColumnMenu.tsx +249 -0
  62. package/lib/components/Header/Header.css +224 -0
  63. package/lib/components/Header/Header.tsx +516 -0
  64. package/lib/components/ListView/ListView.css +112 -0
  65. package/lib/components/ListView/ListViewRow.tsx +63 -0
  66. package/lib/components/Pagination/Pagination.css +91 -0
  67. package/lib/components/Pagination/Pagination.tsx +151 -0
  68. package/lib/components/QuickFilter/QuickFilter.css +23 -0
  69. package/lib/components/QuickFilter/QuickFilter.tsx +79 -0
  70. package/lib/components/Row/Row.css +126 -0
  71. package/lib/components/Row/Row.tsx +377 -0
  72. package/lib/components/Skeleton/Skeleton.css +55 -0
  73. package/lib/components/Skeleton/Skeleton.tsx +22 -0
  74. package/lib/components/SkeletonRow/SkeletonRow.css +32 -0
  75. package/lib/components/SkeletonRow/SkeletonRow.tsx +89 -0
  76. package/lib/components/SkeletonRow/index.ts +1 -0
  77. package/lib/components/Toolbar/GlobalSearch.css +110 -0
  78. package/lib/components/Toolbar/GlobalSearch.tsx +167 -0
  79. package/lib/components/Toolbar/GridToolbar.css +221 -0
  80. package/lib/components/Toolbar/GridToolbar.tsx +658 -0
  81. package/lib/components/Toolbar/PivotPanel.css +212 -0
  82. package/lib/components/Toolbar/PivotPanel.tsx +239 -0
  83. package/lib/components/Tooltip/Tooltip.css +59 -0
  84. package/lib/components/Tooltip/Tooltip.tsx +106 -0
  85. package/lib/components/ui/Button.css +89 -0
  86. package/lib/components/ui/Button.tsx +39 -0
  87. package/lib/components/ui/Checkbox.css +71 -0
  88. package/lib/components/ui/Checkbox.tsx +66 -0
  89. package/lib/components/ui/DragHandleIcon.css +19 -0
  90. package/lib/components/ui/DragHandleIcon.tsx +18 -0
  91. package/lib/components/ui/ExpandIcon.css +38 -0
  92. package/lib/components/ui/ExpandIcon.tsx +75 -0
  93. package/lib/components/ui/Input.css +59 -0
  94. package/lib/components/ui/Input.tsx +45 -0
  95. package/lib/components/ui/index.ts +15 -0
  96. package/lib/hooks/core/useDataGrid.ts +392 -0
  97. package/lib/hooks/core/useGridApiRef.ts +10 -0
  98. package/lib/hooks/features/useAggregation.ts +143 -0
  99. package/lib/hooks/features/useGridClipboard.ts +133 -0
  100. package/lib/hooks/features/useGridDataSource.ts +190 -0
  101. package/lib/hooks/features/useGridEditing.ts +100 -0
  102. package/lib/hooks/features/useGridSpanning.ts +349 -0
  103. package/lib/hooks/features/usePivot.ts +174 -0
  104. package/lib/hooks/useColumnReorder.ts +114 -0
  105. package/lib/hooks/useRowGrouping.ts +302 -0
  106. package/lib/hooks/useRowReorder.ts +83 -0
  107. package/lib/hooks/useTreeData.ts +323 -0
  108. package/lib/index.ts +106 -0
  109. package/lib/state/index.ts +17 -0
  110. package/lib/state/types.ts +48 -0
  111. package/lib/state/useGridStateStorage.ts +113 -0
  112. package/lib/styles/base/reset.css +21 -0
  113. package/lib/styles/base/variables.css +161 -0
  114. package/lib/styles/opengridx.css +27 -0
  115. package/lib/styles/opengridx.css.d.ts +4 -0
  116. package/lib/theme/DataGridThemeProvider.tsx +276 -0
  117. package/lib/theme/defaultStyles.ts +0 -0
  118. package/lib/theme/index.ts +21 -0
  119. package/lib/theme/presets.ts +148 -0
  120. package/lib/theme/types.ts +155 -0
  121. package/lib/types/index.ts +764 -0
  122. package/lib/utils/export/exportToExcelAdvanced.ts +516 -0
  123. package/lib/utils/export/index.ts +550 -0
  124. package/lib/utils/filtering/index.ts +240 -0
  125. package/lib/utils/pinning/index.ts +282 -0
  126. package/lib/utils/sorting/index.ts +55 -0
  127. package/lib/utils/virtualization/index.ts +106 -0
  128. package/lib/vite-env.d.ts +1 -0
  129. package/llms.txt +2 -1
  130. package/package.json +12 -4
@@ -0,0 +1,658 @@
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import type { GridColDef, GridAggregationModel, GridPivotModel, GridFilterModel } from '../../types';
4
+ import { PivotPanel, PivotIcon } from './PivotPanel';
5
+ import { GlobalSearch } from './GlobalSearch';
6
+ import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibilityPanel';
7
+ import { FilterPanel } from '../FilterPanel/FilterPanel';
8
+ import { GridTooltip } from '../Tooltip/Tooltip';
9
+
10
+ export interface GridToolbarProps {
11
+ columns?: GridColDef[];
12
+ /** Original user-defined columns (before pivot transformation).
13
+ * When provided, the PivotPanel shows these instead of `columns`
14
+ * to prevent pivot-generated synthetic columns from appearing in
15
+ * 'Available Fields' and causing infinite nested aggregations. */
16
+ baseColumns?: GridColDef[];
17
+ aggregationModel?: GridAggregationModel;
18
+ onAggregationModelChange?: (model: GridAggregationModel) => void;
19
+ pivotModel?: GridPivotModel;
20
+ onPivotModelChange?: (model: GridPivotModel) => void;
21
+ filterModel?: GridFilterModel;
22
+ onFilterModelChange?: (model: GridFilterModel) => void;
23
+ columnVisibilityModel?: Record<string, boolean>;
24
+ onColumnVisibilityModelChange?: (model: Record<string, boolean>) => void;
25
+ onColumnReorder?: (fromField: string, toField: string) => void;
26
+ forceColumnsOpen?: boolean;
27
+ onColumnsPanelClose?: () => void;
28
+ children?: React.ReactNode;
29
+ rightContent?: React.ReactNode;
30
+ style?: React.CSSProperties;
31
+ }
32
+
33
+ const AGGREGATION_FUNCTIONS = ['none', 'sum', 'avg', 'count', 'min', 'max'] as const;
34
+
35
+ function SigmaIcon() {
36
+ return (
37
+ <svg
38
+ width="18" height="18" viewBox="0 0 24 24"
39
+ fill="none" stroke="currentColor" strokeWidth="2"
40
+ strokeLinecap="round" strokeLinejoin="round"
41
+ aria-hidden="true"
42
+ >
43
+ <path d="M18 4H6l6 8-6 8h12" />
44
+ </svg>
45
+ );
46
+ }
47
+
48
+ function ViewColumnIcon() {
49
+ return (
50
+ <svg focusable="false" aria-hidden="true" viewBox="0 0 24 24" width="18" height="18" fill="none">
51
+ <rect x="2" y="4" width="20" height="16" rx="2" stroke="currentColor" strokeWidth="1.8" />
52
+ <rect x="8.5" y="4" width="7" height="16" fill="currentColor" opacity="0.25" />
53
+ <line x1="8.5" y1="4" x2="8.5" y2="20" stroke="currentColor" strokeWidth="1.8" />
54
+ <line x1="15.5" y1="4" x2="15.5" y2="20" stroke="currentColor" strokeWidth="1.8" />
55
+ <line x1="2" y1="9" x2="22" y2="9" stroke="currentColor" strokeWidth="1.2" />
56
+ <line x1="2" y1="14" x2="22" y2="14" stroke="currentColor" strokeWidth="1.2" />
57
+ </svg>
58
+ );
59
+ }
60
+
61
+ function FilterIcon() {
62
+ return (
63
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
64
+ <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
65
+ </svg>
66
+ );
67
+ }
68
+
69
+ interface PanelPosition { top: number; right: number; }
70
+
71
+ function AggregationPanel({
72
+ anchorRef,
73
+ aggregationModel,
74
+ aggregableColumns,
75
+ onFunctionChange,
76
+ onClearAll,
77
+ onClose,
78
+ activeCount,
79
+ }: {
80
+ anchorRef: React.RefObject<HTMLButtonElement | null>;
81
+ aggregationModel: GridAggregationModel;
82
+ aggregableColumns: GridColDef[];
83
+ onFunctionChange: (field: string, fn: string) => void;
84
+ onClearAll: () => void;
85
+ onClose: () => void;
86
+ activeCount: number;
87
+ }) {
88
+ const panelRef = useRef<HTMLDivElement>(null);
89
+ const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
90
+
91
+ const computePos = useCallback((): PanelPosition => {
92
+ if (!anchorRef.current) return { top: 0, right: 0 };
93
+ const rect = anchorRef.current.getBoundingClientRect();
94
+ return {
95
+ top: rect.bottom + 6,
96
+ right: window.innerWidth - rect.right,
97
+ };
98
+ }, [anchorRef]);
99
+
100
+ useEffect(() => {
101
+ setPos(computePos());
102
+ const update = () => setPos(computePos());
103
+ window.addEventListener('scroll', update, true);
104
+ window.addEventListener('resize', update);
105
+ return () => {
106
+ window.removeEventListener('scroll', update, true);
107
+ window.removeEventListener('resize', update);
108
+ };
109
+ }, [computePos]);
110
+
111
+ useEffect(() => {
112
+ function handleClick(e: MouseEvent) {
113
+ if (
114
+ panelRef.current && !panelRef.current.contains(e.target as Node) &&
115
+ anchorRef.current && !anchorRef.current.contains(e.target as Node)
116
+ ) {
117
+ onClose();
118
+ }
119
+ }
120
+ document.addEventListener('mousedown', handleClick);
121
+ return () => document.removeEventListener('mousedown', handleClick);
122
+ }, [anchorRef, onClose]);
123
+
124
+ useEffect(() => {
125
+ function onKey(e: KeyboardEvent) { if (e.key === 'Escape') onClose(); }
126
+ document.addEventListener('keydown', onKey);
127
+ return () => document.removeEventListener('keydown', onKey);
128
+ }, [onClose]);
129
+
130
+ const panel = (
131
+ <div
132
+ ref={panelRef}
133
+ className="ogx-toolbar__panel"
134
+ role="dialog"
135
+ aria-label="Summaries configuration"
136
+ style={{ top: pos.top, right: pos.right, width: 400, maxWidth: 'calc(100vw - 24px)' }}
137
+ >
138
+ <div className="ogx-toolbar__panel-header">
139
+ <span className="ogx-toolbar__panel-title">
140
+ <SigmaIcon />
141
+ Summaries
142
+ </span>
143
+ {activeCount > 0 && (
144
+ <button
145
+ className="ogx-toolbar__clear-btn"
146
+ onClick={onClearAll}
147
+ title="Clear all summaries"
148
+ >
149
+ Clear all
150
+ </button>
151
+ )}
152
+ </div>
153
+
154
+ <div className="ogx-toolbar__panel-body">
155
+ {aggregableColumns.length === 0 ? (
156
+ <p className="ogx-toolbar__empty">
157
+ No aggregable columns.<br />
158
+ Set <code>aggregable: true</code> on a column to enable.
159
+ </p>
160
+ ) : (
161
+ aggregableColumns.map((col) => {
162
+ const currentFn = aggregationModel[col.field] || 'none';
163
+ return (
164
+ <div key={col.field} className="ogx-toolbar__agg-row">
165
+ <span className="ogx-toolbar__agg-label" title={col.field}>
166
+ {col.headerName || col.field}
167
+ </span>
168
+ <div className="ogx-toolbar__agg-pills">
169
+ {AGGREGATION_FUNCTIONS.map((fn) => (
170
+ <button
171
+ key={fn}
172
+ className={`ogx-toolbar__pill${currentFn === fn ? ' ogx-toolbar__pill--active' : ''}`}
173
+ onClick={() => onFunctionChange(col.field, fn)}
174
+ aria-pressed={currentFn === fn}
175
+ >
176
+ {fn}
177
+ </button>
178
+ ))}
179
+ </div>
180
+ </div>
181
+ );
182
+ })
183
+ )}
184
+ </div>
185
+ </div>
186
+ );
187
+
188
+ return ReactDOM.createPortal(panel, anchorRef.current?.closest('.ogx-theme-provider') || document.body);
189
+ }
190
+
191
+ function ColumnsPanelWrapper({
192
+ anchorRef,
193
+ columns,
194
+ visibleColumns,
195
+ onVisibilityChange,
196
+ onShowAll,
197
+ onHideAll,
198
+ onColumnReorder,
199
+ onClose
200
+ }: {
201
+ anchorRef: React.RefObject<HTMLButtonElement | null>;
202
+ columns: GridColDef[];
203
+ visibleColumns: Set<string>;
204
+ onVisibilityChange: (field: string, isVisible: boolean) => void;
205
+ onShowAll: () => void;
206
+ onHideAll: () => void;
207
+ onColumnReorder?: (fromField: string, toField: string) => void;
208
+ onClose: () => void;
209
+ }) {
210
+ const panelRef = useRef<HTMLDivElement>(null);
211
+ const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
212
+
213
+ const computePos = useCallback((): PanelPosition => {
214
+ if (!anchorRef.current) return { top: 0, right: 0 };
215
+ const rect = anchorRef.current.getBoundingClientRect();
216
+ return {
217
+ top: rect.bottom + 6,
218
+ right: window.innerWidth - rect.right,
219
+ };
220
+ }, [anchorRef]);
221
+
222
+ useEffect(() => {
223
+ setPos(computePos());
224
+ const update = () => setPos(computePos());
225
+ window.addEventListener('scroll', update, true);
226
+ window.addEventListener('resize', update);
227
+ return () => {
228
+ window.removeEventListener('scroll', update, true);
229
+ window.removeEventListener('resize', update);
230
+ };
231
+ }, [computePos]);
232
+
233
+ useEffect(() => {
234
+ function handleClick(e: MouseEvent) {
235
+ if (
236
+ panelRef.current && !panelRef.current.contains(e.target as Node) &&
237
+ anchorRef.current && !anchorRef.current.contains(e.target as Node)
238
+ ) {
239
+ onClose();
240
+ }
241
+ }
242
+ document.addEventListener('mousedown', handleClick);
243
+ return () => document.removeEventListener('mousedown', handleClick);
244
+ }, [anchorRef, onClose]);
245
+
246
+ const panel = (
247
+ <div
248
+ ref={panelRef}
249
+ className="ogx-toolbar__panel"
250
+ role="dialog"
251
+ aria-label="Column visibility"
252
+ style={{ top: pos.top, right: pos.right, width: 280, padding: 0 }}
253
+ >
254
+ <ColumnVisibilityPanel
255
+ columns={columns}
256
+ visibleColumns={visibleColumns}
257
+ onVisibilityChange={onVisibilityChange}
258
+ onShowAll={onShowAll}
259
+ onHideAll={onHideAll}
260
+ onColumnReorder={onColumnReorder}
261
+ />
262
+ </div>
263
+ );
264
+
265
+ return ReactDOM.createPortal(panel, anchorRef.current?.closest('.ogx-theme-provider') || document.body);
266
+ }
267
+
268
+ function FilterPanelWrapper({
269
+ anchorRef,
270
+ columns,
271
+ filterModel,
272
+ onFilterModelChange,
273
+ onClose
274
+ }: {
275
+ anchorRef: React.RefObject<HTMLButtonElement | null>;
276
+ columns: GridColDef[];
277
+ filterModel: GridFilterModel;
278
+ onFilterModelChange: (model: GridFilterModel) => void;
279
+ onClose: () => void;
280
+ }) {
281
+ const panelRef = useRef<HTMLDivElement>(null);
282
+ const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
283
+ // Stable ref for onClose — prevents the mousedown effect from
284
+ // tearing down and re-registering on every filterModel update,
285
+ // which could fire the close callback during the re-register window.
286
+ const onCloseRef = useRef(onClose);
287
+ onCloseRef.current = onClose;
288
+
289
+ const computePos = useCallback((): PanelPosition => {
290
+ if (!anchorRef.current) return { top: 0, right: 0 };
291
+ const rect = anchorRef.current.getBoundingClientRect();
292
+ return {
293
+ top: rect.bottom + 6,
294
+ right: window.innerWidth - rect.right,
295
+ };
296
+ }, [anchorRef]);
297
+
298
+ useEffect(() => {
299
+ setPos(computePos());
300
+ const update = () => setPos(computePos());
301
+ window.addEventListener('scroll', update, true);
302
+ window.addEventListener('resize', update);
303
+ return () => {
304
+ window.removeEventListener('scroll', update, true);
305
+ window.removeEventListener('resize', update);
306
+ };
307
+ }, [computePos]);
308
+
309
+ // Only Escape key closes the filter panel automatically.
310
+ // Click-outside is intentionally removed: users need to type in filter
311
+ // inputs without the panel closing. There is an explicit Close button.
312
+ useEffect(() => {
313
+ function handleKey(e: KeyboardEvent) {
314
+ if (e.key === 'Escape') onCloseRef.current();
315
+ }
316
+ document.addEventListener('keydown', handleKey);
317
+ return () => document.removeEventListener('keydown', handleKey);
318
+ // Empty deps — listener is registered once and uses the stable ref
319
+ }, []);
320
+
321
+ const activeFilterCount = filterModel.items?.length ?? 0;
322
+
323
+ const handleClearAll = () => {
324
+ onFilterModelChange({ ...filterModel, items: [], logicOperator: 'and' });
325
+ };
326
+
327
+ const panel = (
328
+ <div
329
+ ref={panelRef}
330
+ className="ogx-toolbar__panel ogx-toolbar__panel--filter"
331
+ role="dialog"
332
+ aria-label="Advanced filters"
333
+ style={{ top: pos.top, right: pos.right, width: 540, maxWidth: 'calc(100vw - 24px)' }}
334
+ >
335
+ <div className="ogx-toolbar__panel-header">
336
+ <span className="ogx-toolbar__panel-title">
337
+ <FilterIcon />
338
+ Filters
339
+ {activeFilterCount > 0 && (
340
+ <span className="ogx-toolbar__panel-count">{activeFilterCount}</span>
341
+ )}
342
+ </span>
343
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
344
+ {activeFilterCount > 0 && (
345
+ <button
346
+ className="ogx-toolbar__clear-btn"
347
+ onClick={handleClearAll}
348
+ title="Clear all filters"
349
+ >
350
+ Clear all
351
+ </button>
352
+ )}
353
+ <button
354
+ className="ogx-toolbar__close-btn"
355
+ onClick={() => onCloseRef.current()}
356
+ title="Close filters"
357
+ aria-label="Close filters panel"
358
+ >
359
+ Close
360
+ </button>
361
+ </div>
362
+ </div>
363
+
364
+ <div className="ogx-toolbar__panel-body ogx-toolbar__panel-body--filter">
365
+ <FilterPanel
366
+ columns={columns}
367
+ filterModel={filterModel}
368
+ onFilterModelChange={onFilterModelChange}
369
+ />
370
+ </div>
371
+ </div>
372
+ );
373
+
374
+ return ReactDOM.createPortal(panel, anchorRef.current?.closest('.ogx-theme-provider') || document.body);
375
+ }
376
+
377
+ const EMPTY_PIVOT: GridPivotModel = { rowFields: [], columnFields: [], valueFields: [] };
378
+
379
+ export function GridToolbar({
380
+ columns = [],
381
+ baseColumns,
382
+ aggregationModel = {},
383
+ onAggregationModelChange,
384
+ pivotModel,
385
+ onPivotModelChange,
386
+ filterModel,
387
+ onFilterModelChange,
388
+ columnVisibilityModel = {},
389
+ onColumnVisibilityModelChange,
390
+ onColumnReorder,
391
+ forceColumnsOpen,
392
+ onColumnsPanelClose,
393
+ children,
394
+ rightContent,
395
+ style,
396
+ }: GridToolbarProps) {
397
+ const [aggOpen, setAggOpen] = useState(false);
398
+ const [pivotOpen, setPivotOpen] = useState(false);
399
+ const [colsOpen, setColsOpen] = useState(false);
400
+ const [filterOpen, setFilterOpen] = useState(false);
401
+
402
+ useEffect(() => {
403
+ if (forceColumnsOpen) {
404
+ setColsOpen(true);
405
+ setAggOpen(false);
406
+ setPivotOpen(false);
407
+ setFilterOpen(false);
408
+ }
409
+ }, [forceColumnsOpen]);
410
+
411
+ const aggButtonRef = useRef<HTMLButtonElement>(null);
412
+ const pivotButtonRef = useRef<HTMLButtonElement>(null);
413
+ const colsButtonRef = useRef<HTMLButtonElement>(null);
414
+ const filterButtonRef = useRef<HTMLButtonElement>(null);
415
+
416
+ const currentPivotModel = pivotModel ?? EMPTY_PIVOT;
417
+ const pivotActive = currentPivotModel.rowFields.length > 0
418
+ || currentPivotModel.columnFields.length > 0
419
+ || currentPivotModel.valueFields.length > 0;
420
+
421
+ const aggregableColumns = columns.filter(
422
+ (c) => c.aggregable !== false && (c.type === 'number' || c.aggregable === true)
423
+ );
424
+
425
+ const activeCount = Object.keys(aggregationModel).filter(
426
+ (f) => aggregationModel[f] && aggregationModel[f] !== 'none'
427
+ ).length;
428
+
429
+ const handleFunctionChange = useCallback(
430
+ (field: string, fn: string) => {
431
+ if (!onAggregationModelChange) return;
432
+ const next = { ...aggregationModel };
433
+ if (fn === 'none') {
434
+ delete next[field];
435
+ } else {
436
+ next[field] = fn;
437
+ }
438
+ onAggregationModelChange(next);
439
+ },
440
+ [aggregationModel, onAggregationModelChange]
441
+ );
442
+
443
+ const clearAllAgg = useCallback(() => {
444
+ onAggregationModelChange?.({});
445
+ }, [onAggregationModelChange]);
446
+
447
+ const handleSearchChange = useCallback((value: string) => {
448
+ if (!onFilterModelChange) return;
449
+
450
+ const values = value ? [value] : [];
451
+ const nextModel = {
452
+ ...(filterModel || {}),
453
+ quickFilterValues: values,
454
+ items: filterModel?.items || []
455
+ };
456
+ onFilterModelChange(nextModel);
457
+ }, [filterModel, onFilterModelChange]);
458
+
459
+ const searchValue = filterModel?.quickFilterValues?.[0] || '';
460
+ const activeFilterCount = (filterModel?.items?.length || 0);
461
+
462
+ // Column Visibility Logic
463
+ const visibleColumns = new Set(
464
+ columns
465
+ .filter(col => columnVisibilityModel[col.field] !== false) // default true
466
+ .map(col => col.field)
467
+ );
468
+
469
+ const handleVisibilityChange = useCallback((field: string, isVisible: boolean) => {
470
+ if (!onColumnVisibilityModelChange) return;
471
+ const next = { ...columnVisibilityModel, [field]: isVisible };
472
+ onColumnVisibilityModelChange(next);
473
+ }, [columnVisibilityModel, onColumnVisibilityModelChange]);
474
+
475
+ // Batch show/hide — build one full model so no stale-closure overwrites
476
+ const handleShowAllColumns = useCallback(() => {
477
+ if (!onColumnVisibilityModelChange) return;
478
+ const next = { ...columnVisibilityModel };
479
+ columns.forEach(col => {
480
+ if (col.hideable !== false) next[col.field] = true;
481
+ });
482
+ onColumnVisibilityModelChange(next);
483
+ }, [columns, columnVisibilityModel, onColumnVisibilityModelChange]);
484
+
485
+ const handleHideAllColumns = useCallback(() => {
486
+ if (!onColumnVisibilityModelChange) return;
487
+ const next = { ...columnVisibilityModel };
488
+ columns.forEach(col => {
489
+ if (col.hideable !== false) next[col.field] = false;
490
+ });
491
+ onColumnVisibilityModelChange(next);
492
+ }, [columns, columnVisibilityModel, onColumnVisibilityModelChange]);
493
+
494
+ return (
495
+ <div className="ogx-toolbar" role="toolbar" aria-label="Grid toolbar" style={style}>
496
+ {/* Left slot — custom content */}
497
+ {children && <div className="ogx-toolbar__left">{children}</div>}
498
+
499
+ {/* Spacer */}
500
+ <div className="ogx-toolbar__spacer" />
501
+
502
+ {/* Right: built-in buttons */}
503
+ <div className="ogx-toolbar__actions">
504
+ {onFilterModelChange && (
505
+ <div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
506
+ <GlobalSearch
507
+ value={searchValue}
508
+ onChange={handleSearchChange}
509
+ placeholder="Search..."
510
+ />
511
+ </div>
512
+ )}
513
+
514
+ {/* Columns Button */}
515
+ {onColumnVisibilityModelChange && (
516
+ <div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
517
+ <GridTooltip title="Columns">
518
+ <button
519
+ ref={colsButtonRef}
520
+ className={`ogx-toolbar__icon-btn${colsOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
521
+ aria-label="Manage columns"
522
+ onClick={() => {
523
+ setColsOpen(!colsOpen);
524
+ setAggOpen(false);
525
+ setPivotOpen(false);
526
+ setFilterOpen(false);
527
+ }}
528
+ >
529
+ <ViewColumnIcon />
530
+ </button>
531
+ </GridTooltip>
532
+ {colsOpen && (
533
+ <ColumnsPanelWrapper
534
+ anchorRef={colsButtonRef}
535
+ columns={columns}
536
+ visibleColumns={visibleColumns}
537
+ onVisibilityChange={handleVisibilityChange}
538
+ onShowAll={handleShowAllColumns}
539
+ onHideAll={handleHideAllColumns}
540
+ onColumnReorder={onColumnReorder}
541
+ onClose={() => { setColsOpen(false); onColumnsPanelClose?.(); }}
542
+ />
543
+ )}
544
+ </div>
545
+ )}
546
+
547
+ {/* Filters Button */}
548
+ {onFilterModelChange && (
549
+ <div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
550
+ <GridTooltip title="Filters">
551
+ <button
552
+ ref={filterButtonRef}
553
+ className={`ogx-toolbar__icon-btn${filterOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
554
+ aria-label="Advanced filters"
555
+ onClick={() => {
556
+ setFilterOpen(!filterOpen);
557
+ setAggOpen(false);
558
+ setPivotOpen(false);
559
+ setColsOpen(false);
560
+ }}
561
+ >
562
+ <FilterIcon />
563
+ {activeFilterCount > 0 && (
564
+ <span className="ogx-toolbar__dot" aria-label="Filters active" />
565
+ )}
566
+ </button>
567
+ </GridTooltip>
568
+ {filterOpen && (
569
+ <FilterPanelWrapper
570
+ anchorRef={filterButtonRef}
571
+ columns={columns}
572
+ filterModel={filterModel || { items: [] }}
573
+ onFilterModelChange={onFilterModelChange}
574
+ onClose={() => setFilterOpen(false)}
575
+ />
576
+ )}
577
+ </div>
578
+ )}
579
+
580
+ {/* Pivot button */}
581
+ {onPivotModelChange && (
582
+ <div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
583
+ <GridTooltip title="Pivot">
584
+ <button
585
+ ref={pivotButtonRef}
586
+ id="ogx-pivot-btn"
587
+ className={`ogx-toolbar__icon-btn${pivotOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
588
+ aria-label="Configure pivot"
589
+ aria-expanded={pivotOpen}
590
+ aria-controls="ogx-pivot-panel"
591
+ onClick={() => {
592
+ setPivotOpen(!pivotOpen);
593
+ setAggOpen(false);
594
+ setColsOpen(false);
595
+ setFilterOpen(false);
596
+ }}
597
+ >
598
+ <PivotIcon />
599
+ {pivotActive && (
600
+ <span className="ogx-toolbar__dot" aria-label="Pivot active" />
601
+ )}
602
+ </button>
603
+ </GridTooltip>
604
+
605
+ {pivotOpen && (
606
+ <PivotPanel
607
+ anchorRef={pivotButtonRef}
608
+ columns={baseColumns ?? columns}
609
+ model={currentPivotModel}
610
+ onChange={(m) => { onPivotModelChange?.(m); }}
611
+ onClose={() => setPivotOpen(false)}
612
+ />
613
+ )}
614
+ </div>
615
+ )}
616
+
617
+ {/* Summaries button */}
618
+ <div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
619
+ <GridTooltip title="Summaries">
620
+ <button
621
+ ref={aggButtonRef}
622
+ id="ogx-aggregation-btn"
623
+ className={`ogx-toolbar__icon-btn${aggOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
624
+ aria-label="Configure summaries"
625
+ aria-expanded={aggOpen}
626
+ aria-controls="ogx-aggregation-panel"
627
+ onClick={() => {
628
+ setAggOpen(!aggOpen);
629
+ setPivotOpen(false);
630
+ setColsOpen(false);
631
+ setFilterOpen(false);
632
+ }}
633
+ >
634
+ <SigmaIcon />
635
+ {activeCount > 0 && (
636
+ <span className="ogx-toolbar__dot" aria-label={`${activeCount} active summaries`} />
637
+ )}
638
+ </button>
639
+ </GridTooltip>
640
+
641
+ {aggOpen && (
642
+ <AggregationPanel
643
+ anchorRef={aggButtonRef}
644
+ aggregationModel={aggregationModel}
645
+ aggregableColumns={aggregableColumns}
646
+ onFunctionChange={handleFunctionChange}
647
+ onClearAll={clearAllAgg}
648
+ onClose={() => setAggOpen(false)}
649
+ activeCount={activeCount}
650
+ />
651
+ )}
652
+ </div>
653
+
654
+ {rightContent}
655
+ </div>
656
+ </div>
657
+ );
658
+ }