@opencorestack/opengridx 0.1.6 → 0.1.7

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