@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,212 @@
1
+ .ogx-pivot-panel {
2
+ position: fixed;
3
+ width: 440px;
4
+ max-width: calc(100vw - 24px);
5
+ background: var(--ogx-overlay-background, var(--ogx-grid-background));
6
+ border: var(--ogx-border-width-thin) solid var(--ogx-overlay-border, var(--ogx-grid-border-color));
7
+ border-radius: var(--ogx-border-radius-lg);
8
+ box-shadow: var(--ogx-overlay-shadow, var(--ogx-shadow-lg));
9
+ z-index: 9999;
10
+ overflow: hidden;
11
+ animation: ogx-panel-in 0.15s ease-out;
12
+ font-family: var(--ogx-font-family);
13
+ }
14
+ @keyframes ogx-panel-in {
15
+ from { opacity: 0; transform: translateY(-6px) scale(0.98); }
16
+ to { opacity: 1; transform: translateY(0) scale(1); }
17
+ }
18
+ .ogx-pivot-panel__header {
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: space-between;
22
+ padding: 10px 14px;
23
+ border-bottom: var(--ogx-border-width-thin) solid var(--ogx-overlay-border, var(--ogx-grid-border-color));
24
+ background: var(--ogx-grid-header-background);
25
+ }
26
+ .ogx-pivot-panel__title {
27
+ display: flex;
28
+ align-items: center;
29
+ gap: 6px;
30
+ font-size: var(--ogx-font-size-sm);
31
+ font-weight: 600;
32
+ color: var(--ogx-grid-header-text);
33
+ }
34
+ .ogx-pivot-panel__reset-btn {
35
+ padding: 2px 8px;
36
+ border: var(--ogx-border-width-thin) solid var(--ogx-border-color);
37
+ border-radius: var(--ogx-border-radius-sm);
38
+ background: transparent;
39
+ color: var(--ogx-color-error, #ef4444);
40
+ font-family: var(--ogx-font-family);
41
+ font-size: var(--ogx-font-size-xs);
42
+ font-weight: 500;
43
+ cursor: pointer;
44
+ transition: background 0.15s, border-color 0.15s;
45
+ }
46
+ .ogx-pivot-panel__reset-btn:hover {
47
+ background: var(--ogx-toolbar-btn-danger-bg, rgba(239, 68, 68, 0.08));
48
+ color: var(--ogx-toolbar-btn-danger-text, #ef4444);
49
+ border-color: var(--ogx-color-error, #ef4444);
50
+ }
51
+ .ogx-pivot-panel__body {
52
+ padding: 10px 14px;
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 14px;
56
+ max-height: 300px;
57
+ overflow-y: auto;
58
+ }
59
+ .ogx-pivot-zone {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 6px;
63
+ }
64
+ .ogx-pivot-zone__label {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 6px;
68
+ font-size: 11px;
69
+ font-weight: 700;
70
+ text-transform: uppercase;
71
+ letter-spacing: 0.06em;
72
+ color: var(--ogx-color-gray-400, #94a3b8);
73
+ }
74
+ .ogx-pivot-zone__dot {
75
+ width: 8px;
76
+ height: 8px;
77
+ border-radius: 50%;
78
+ flex-shrink: 0;
79
+ }
80
+ .ogx-pivot-zone__dot--row { background: var(--ogx-color-primary, #3b82f6); }
81
+ .ogx-pivot-zone__dot--col { background: var(--ogx-color-secondary, #8b5cf6); }
82
+ .ogx-pivot-zone__dot--value { background: var(--ogx-color-success, #10b981); }
83
+ .ogx-pivot-chips {
84
+ display: flex;
85
+ flex-wrap: wrap;
86
+ gap: 5px;
87
+ min-height: 30px;
88
+ padding: 5px 8px;
89
+ border: var(--ogx-border-width-thin) solid var(--ogx-overlay-border, var(--ogx-grid-border-color));
90
+ border-radius: var(--ogx-border-radius-md);
91
+ background: var(--ogx-grid-header-background);
92
+ }
93
+ .ogx-pivot-chip {
94
+ display: inline-flex;
95
+ align-items: center;
96
+ gap: 4px;
97
+ padding: 3px 8px 3px 10px;
98
+ border-radius: 999px;
99
+ font-size: 12px;
100
+ font-weight: 500;
101
+ line-height: 1;
102
+ white-space: nowrap;
103
+ }
104
+ .ogx-pivot-chip--row { background: rgba(59,130,246,0.12); color: var(--ogx-color-primary-dark, #2563eb); }
105
+ .ogx-pivot-chip--col { background: rgba(139,92,246,0.12); color: var(--ogx-color-secondary-dark, #7c3aed); }
106
+ .ogx-pivot-chip--value { background: var(--ogx-toolbar-chip-active-bg, rgba(16,185,129,0.12)); color: var(--ogx-toolbar-chip-active-text, #047857); }
107
+ .ogx-pivot-chip__remove {
108
+ display: inline-flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+ width: 14px;
112
+ height: 14px;
113
+ border-radius: 50%;
114
+ border: none;
115
+ background: transparent;
116
+ cursor: pointer;
117
+ opacity: 0.6;
118
+ font-size: 11px;
119
+ font-weight: 700;
120
+ line-height: 1;
121
+ padding: 0;
122
+ color: inherit;
123
+ transition: opacity 0.1s;
124
+ }
125
+ .ogx-pivot-chip__remove:hover { opacity: 1; }
126
+ .ogx-pivot-chip__fn-select {
127
+ border: none;
128
+ background: transparent;
129
+ font-size: 11px;
130
+ font-weight: 700;
131
+ color: inherit;
132
+ cursor: pointer;
133
+ padding: 0 2px;
134
+ margin-left: 2px;
135
+ font-family: var(--ogx-font-family-mono, monospace);
136
+ }
137
+ .ogx-pivot-chips--empty::after {
138
+ content: attr(data-placeholder);
139
+ font-size: 11px;
140
+ color: var(--ogx-color-gray-400, #94a3b8);
141
+ align-self: center;
142
+ pointer-events: none;
143
+ }
144
+ .ogx-pivot-panel__fields-label {
145
+ font-size: 11px;
146
+ font-weight: 700;
147
+ text-transform: uppercase;
148
+ letter-spacing: 0.06em;
149
+ color: var(--ogx-color-gray-400, #94a3b8);
150
+ margin-bottom: 4px;
151
+ }
152
+ .ogx-pivot-fields {
153
+ display: flex;
154
+ flex-wrap: wrap;
155
+ gap: 5px;
156
+ }
157
+ .ogx-pivot-field-btn {
158
+ display: inline-flex;
159
+ align-items: center;
160
+ gap: 4px;
161
+ padding: 4px 10px;
162
+ border: var(--ogx-border-width-thin) solid var(--ogx-overlay-border, var(--ogx-border-color));
163
+ border-radius: 999px;
164
+ background: var(--ogx-grid-header-background);
165
+ color: var(--ogx-grid-row-text);
166
+ font-family: var(--ogx-font-family);
167
+ font-size: 12px;
168
+ font-weight: 500;
169
+ cursor: pointer;
170
+ transition: background 0.12s, border-color 0.12s, color 0.12s;
171
+ white-space: nowrap;
172
+ }
173
+ .ogx-pivot-field-btn:hover {
174
+ background: var(--ogx-color-primary-light, #eff6ff);
175
+ border-color: var(--ogx-color-primary, #3b82f6);
176
+ color: var(--ogx-color-primary, #3b82f6);
177
+ }
178
+ .ogx-pivot-field-btn--numeric::before {
179
+ content: '#';
180
+ font-family: var(--ogx-font-family-mono, monospace);
181
+ font-size: 10px;
182
+ opacity: 0.5;
183
+ }
184
+ .ogx-pivot-field-actions {
185
+ display: flex;
186
+ gap: 3px;
187
+ }
188
+ .ogx-pivot-add-btn {
189
+ display: inline-flex;
190
+ align-items: center;
191
+ padding: 2px 6px;
192
+ border: var(--ogx-border-width-thin) solid var(--ogx-border-color);
193
+ border-radius: var(--ogx-border-radius-sm);
194
+ background: transparent;
195
+ font-size: 10px;
196
+ font-weight: 600;
197
+ cursor: pointer;
198
+ white-space: nowrap;
199
+ transition: background 0.1s, border-color 0.1s, color 0.1s;
200
+ }
201
+ .ogx-pivot-add-btn--row { color: var(--ogx-color-primary, #3b82f6); }
202
+ .ogx-pivot-add-btn--col { color: var(--ogx-color-secondary, #8b5cf6); }
203
+ .ogx-pivot-add-btn--value { color: var(--ogx-toolbar-chip-active-text, #047857); }
204
+ .ogx-pivot-add-btn:hover {
205
+ background: var(--ogx-grid-header-hover-background);
206
+ border-color: currentColor;
207
+ }
208
+ .ogx-pivot-panel__divider {
209
+ border: none;
210
+ border-top: var(--ogx-border-width-thin) solid var(--ogx-overlay-border, var(--ogx-grid-border-color));
211
+ margin: 0;
212
+ }
@@ -0,0 +1,239 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import type { GridColDef } from '../../types';
4
+ import type { GridPivotModel, GridPivotAggFn, GridPivotValueField } from '../../types';
5
+
6
+ const AGG_FNS: GridPivotAggFn[] = ['sum', 'avg', 'count', 'min', 'max'];
7
+
8
+ export function PivotIcon() {
9
+ return (
10
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="none"
11
+ stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
12
+ aria-hidden="true"
13
+ >
14
+ <rect x="3" y="3" width="7" height="7" rx="1" />
15
+ <rect x="14" y="3" width="7" height="7" rx="1" />
16
+ <rect x="3" y="14" width="7" height="7" rx="1" />
17
+ <rect x="14" y="14" width="7" height="7" rx="1" fill="currentColor" opacity="0.25" />
18
+ <path d="M17 14v-4M17 10l-2 2 2 2" />
19
+ </svg>
20
+ );
21
+ }
22
+
23
+ export interface PivotPanelProps {
24
+ anchorRef: React.RefObject<HTMLButtonElement | null>;
25
+ columns: GridColDef[];
26
+ model: GridPivotModel;
27
+ onChange: (model: GridPivotModel) => void;
28
+ onClose: () => void;
29
+ }
30
+
31
+ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: PivotPanelProps) {
32
+ const panelRef = useRef<HTMLDivElement>(null);
33
+ const [pos, setPos] = useState({ top: 0, right: 0 });
34
+
35
+ const computePos = useCallback(() => {
36
+ if (!anchorRef.current) return { top: 0, right: 0 };
37
+ const rect = anchorRef.current.getBoundingClientRect();
38
+ return { top: rect.bottom + 6, right: window.innerWidth - rect.right };
39
+ }, [anchorRef]);
40
+
41
+ useEffect(() => {
42
+ setPos(computePos());
43
+ const update = () => setPos(computePos());
44
+ window.addEventListener('scroll', update, true);
45
+ window.addEventListener('resize', update);
46
+ return () => {
47
+ window.removeEventListener('scroll', update, true);
48
+ window.removeEventListener('resize', update);
49
+ };
50
+ }, [computePos]);
51
+
52
+ useEffect(() => {
53
+ function onClick(e: MouseEvent) {
54
+ if (
55
+ panelRef.current && !panelRef.current.contains(e.target as Node) &&
56
+ anchorRef.current && !anchorRef.current.contains(e.target as Node)
57
+ ) onClose();
58
+ }
59
+ function onKey(e: KeyboardEvent) { if (e.key === 'Escape') onClose(); }
60
+ document.addEventListener('mousedown', onClick);
61
+ document.addEventListener('keydown', onKey);
62
+ return () => {
63
+ document.removeEventListener('mousedown', onClick);
64
+ document.removeEventListener('keydown', onKey);
65
+ };
66
+ }, [anchorRef, onClose]);
67
+
68
+ const inRow = new Set(model.rowFields);
69
+ const inCol = new Set(model.columnFields);
70
+ const inValue = new Set(model.valueFields.map((v) => v.field));
71
+ const isUsed = (f: string) => inRow.has(f) || inCol.has(f) || inValue.has(f);
72
+
73
+ function addRow(field: string) {
74
+ if (inRow.has(field)) return;
75
+ onChange({ ...model, rowFields: [...model.rowFields, field] });
76
+ }
77
+ function addCol(field: string) {
78
+ if (inCol.has(field)) return;
79
+ onChange({ ...model, columnFields: [...model.columnFields, field] });
80
+ }
81
+ function addValue(field: string, aggFn: GridPivotAggFn = 'sum') {
82
+ if (inValue.has(field)) return;
83
+ onChange({ ...model, valueFields: [...model.valueFields, { field, aggFn }] });
84
+ }
85
+ function removeRow(field: string) {
86
+ onChange({ ...model, rowFields: model.rowFields.filter((f) => f !== field) });
87
+ }
88
+ function removeCol(field: string) {
89
+ onChange({ ...model, columnFields: model.columnFields.filter((f) => f !== field) });
90
+ }
91
+ function removeValue(field: string) {
92
+ onChange({ ...model, valueFields: model.valueFields.filter((v) => v.field !== field) });
93
+ }
94
+ function changeValueFn(field: string, aggFn: GridPivotAggFn) {
95
+ onChange({
96
+ ...model,
97
+ valueFields: model.valueFields.map((v) => v.field === field ? { ...v, aggFn } : v),
98
+ });
99
+ }
100
+ function reset() {
101
+ onChange({ rowFields: [], columnFields: [], valueFields: [] });
102
+ }
103
+
104
+ const numericFields = new Set(columns.filter((c) => c.type === 'number' || c.aggregable).map((c) => c.field));
105
+
106
+ const totalActive = model.rowFields.length + model.columnFields.length + model.valueFields.length;
107
+
108
+ const colLabel = (field: string) =>
109
+ columns.find((c) => c.field === field)?.headerName ?? field;
110
+
111
+ const panel = (
112
+ <div
113
+ ref={panelRef}
114
+ className="ogx-pivot-panel"
115
+ role="dialog"
116
+ aria-label="Pivot configuration"
117
+ style={{ top: pos.top, right: pos.right }}
118
+ >
119
+ { }
120
+ <div className="ogx-pivot-panel__header">
121
+ <span className="ogx-pivot-panel__title">
122
+ <PivotIcon />
123
+ Pivot Mode
124
+ </span>
125
+ {totalActive > 0 && (
126
+ <button className="ogx-pivot-panel__reset-btn" onClick={reset} title="Reset pivot">
127
+ Reset
128
+ </button>
129
+ )}
130
+ </div>
131
+
132
+ <div className="ogx-pivot-panel__body">
133
+
134
+ { }
135
+ <div className="ogx-pivot-zone">
136
+ <span className="ogx-pivot-zone__label">
137
+ <span className="ogx-pivot-zone__dot ogx-pivot-zone__dot--row" />
138
+ Row Fields
139
+ </span>
140
+ <div
141
+ className={`ogx-pivot-chips${model.rowFields.length === 0 ? ' ogx-pivot-chips--empty' : ''}`}
142
+ data-placeholder="Add fields here — each becomes a row label"
143
+ >
144
+ {model.rowFields.map((f) => (
145
+ <span key={f} className="ogx-pivot-chip ogx-pivot-chip--row">
146
+ {colLabel(f)}
147
+ <button className="ogx-pivot-chip__remove" onClick={() => removeRow(f)} title={`Remove ${colLabel(f)}`}>×</button>
148
+ </span>
149
+ ))}
150
+ </div>
151
+ </div>
152
+
153
+ { }
154
+ <div className="ogx-pivot-zone">
155
+ <span className="ogx-pivot-zone__label">
156
+ <span className="ogx-pivot-zone__dot ogx-pivot-zone__dot--col" />
157
+ Column Fields
158
+ </span>
159
+ <div
160
+ className={`ogx-pivot-chips${model.columnFields.length === 0 ? ' ogx-pivot-chips--empty' : ''}`}
161
+ data-placeholder="Add fields here — unique values become column headers"
162
+ >
163
+ {model.columnFields.map((f) => (
164
+ <span key={f} className="ogx-pivot-chip ogx-pivot-chip--col">
165
+ {colLabel(f)}
166
+ <button className="ogx-pivot-chip__remove" onClick={() => removeCol(f)} title={`Remove ${colLabel(f)}`}>×</button>
167
+ </span>
168
+ ))}
169
+ </div>
170
+ </div>
171
+
172
+ { }
173
+ <div className="ogx-pivot-zone">
174
+ <span className="ogx-pivot-zone__label">
175
+ <span className="ogx-pivot-zone__dot ogx-pivot-zone__dot--value" />
176
+ Value Fields (aggregated)
177
+ </span>
178
+ <div
179
+ className={`ogx-pivot-chips${model.valueFields.length === 0 ? ' ogx-pivot-chips--empty' : ''}`}
180
+ data-placeholder="Add numeric fields here — values are aggregated"
181
+ >
182
+ {model.valueFields.map((vf: GridPivotValueField) => (
183
+ <span key={vf.field} className="ogx-pivot-chip ogx-pivot-chip--value">
184
+ {colLabel(vf.field)}
185
+ <select
186
+ className="ogx-pivot-chip__fn-select"
187
+ value={vf.aggFn}
188
+ onChange={(e) => changeValueFn(vf.field, e.target.value as GridPivotAggFn)}
189
+ title="Aggregation function"
190
+ >
191
+ {AGG_FNS.map((fn) => (
192
+ <option key={fn} value={fn}>{fn.toUpperCase()}</option>
193
+ ))}
194
+ </select>
195
+ <button className="ogx-pivot-chip__remove" onClick={() => removeValue(vf.field)} title={`Remove ${colLabel(vf.field)}`}>×</button>
196
+ </span>
197
+ ))}
198
+ </div>
199
+ </div>
200
+
201
+ <hr className="ogx-pivot-panel__divider" />
202
+
203
+ { }
204
+ <div>
205
+ <div className="ogx-pivot-panel__fields-label">Available Fields</div>
206
+ <div className="ogx-pivot-fields">
207
+ {columns.map((col) => {
208
+ const isNum = numericFields.has(col.field);
209
+ const used = isUsed(col.field);
210
+ if (used) return null;
211
+ return (
212
+ <div key={col.field} style={{ display: 'flex', alignItems: 'center', gap: 3 }}>
213
+ <span className={`ogx-pivot-field-btn${isNum ? ' ogx-pivot-field-btn--numeric' : ''}`}>
214
+ {col.headerName ?? col.field}
215
+ </span>
216
+ <div className="ogx-pivot-field-actions">
217
+ <button className="ogx-pivot-add-btn ogx-pivot-add-btn--row" onClick={() => addRow(col.field)} title="Add to Row Fields">Row</button>
218
+ <button className="ogx-pivot-add-btn ogx-pivot-add-btn--col" onClick={() => addCol(col.field)} title="Add to Column Fields">Col</button>
219
+ {isNum && (
220
+ <button className="ogx-pivot-add-btn ogx-pivot-add-btn--value" onClick={() => addValue(col.field)} title="Add to Value Fields">Val</button>
221
+ )}
222
+ </div>
223
+ </div>
224
+ );
225
+ })}
226
+ {columns.every((col) => isUsed(col.field)) && (
227
+ <span style={{ fontSize: 12, color: 'var(--ogx-color-gray-400, #94a3b8)' }}>
228
+ All fields are in use.
229
+ </span>
230
+ )}
231
+ </div>
232
+ </div>
233
+
234
+ </div>
235
+ </div>
236
+ );
237
+
238
+ return ReactDOM.createPortal(panel, anchorRef.current?.closest('.ogx-theme-provider') || document.body);
239
+ }
@@ -0,0 +1,59 @@
1
+ .ogx-tooltip {
2
+ pointer-events: none;
3
+ background: var(--ogx-overlay-background, rgba(30, 41, 59, 0.92));
4
+ color: var(--ogx-overlay-text, #fff);
5
+ font-family: var(--ogx-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
6
+ font-size: 10px;
7
+ font-weight: 500;
8
+ line-height: 1.4;
9
+ padding: 3px 6px;
10
+ border-radius: 6px;
11
+ white-space: nowrap;
12
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
13
+ z-index: 99999;
14
+ }
15
+ .ogx-tooltip--top {
16
+ transform: translate(-50%, -100%);
17
+ animation: ogx-tooltip-in-top 0.15s ease-out;
18
+ }
19
+
20
+ .ogx-tooltip--bottom {
21
+ transform: translate(-50%, 0);
22
+ animation: ogx-tooltip-in-bottom 0.15s ease-out;
23
+ }
24
+
25
+ @keyframes ogx-tooltip-in-top {
26
+ from { opacity: 0; transform: translate(-50%, -95%) scale(0.98); }
27
+ to { opacity: 1; transform: translate(-50%, -100%) scale(1); }
28
+ }
29
+
30
+ @keyframes ogx-tooltip-in-bottom {
31
+ from { opacity: 0; transform: translate(-50%, -4px) scale(0.98); }
32
+ to { opacity: 1; transform: translate(-50%, 0) scale(1); }
33
+ }
34
+
35
+ /* Optional: Add a small arrow if needed */
36
+ /*
37
+ .ogx-tooltip::before {
38
+ content: '';
39
+ position: absolute;
40
+ width: 0;
41
+ height: 0;
42
+ border-left: 5px solid transparent;
43
+ border-right: 5px solid transparent;
44
+ }
45
+
46
+ .ogx-tooltip--top::before {
47
+ bottom: -5px;
48
+ left: 50%;
49
+ transform: translateX(-50%);
50
+ border-top: 5px solid var(--ogx-overlay-background, rgba(30, 41, 59, 0.92));
51
+ }
52
+
53
+ .ogx-tooltip--bottom::before {
54
+ top: -5px;
55
+ left: 50%;
56
+ transform: translateX(-50%);
57
+ border-bottom: 5px solid var(--ogx-overlay-background, rgba(30, 41, 59, 0.92));
58
+ }
59
+ */
@@ -0,0 +1,106 @@
1
+ import { useState, useRef, useLayoutEffect, useCallback } from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import './Tooltip.css';
4
+
5
+ interface GridTooltipProps {
6
+ title: React.ReactNode;
7
+ children: React.ReactElement;
8
+ placement?: 'top' | 'bottom' | 'left' | 'right';
9
+ enterDelay?: number;
10
+ leaveDelay?: number;
11
+ }
12
+
13
+ export function GridTooltip({
14
+ title,
15
+ children,
16
+ placement = 'top',
17
+ enterDelay = 200,
18
+ leaveDelay = 0
19
+ }: GridTooltipProps) {
20
+ const [open, setOpen] = useState(false);
21
+ const [coords, setCoords] = useState({ top: 0, left: 0 });
22
+ const timerRef = useRef<NodeJS.Timeout | null>(null);
23
+ const childRef = useRef<HTMLElement | null>(null);
24
+
25
+ const updateCoords = useCallback(() => {
26
+ if (!childRef.current) return;
27
+ const rect = childRef.current.getBoundingClientRect();
28
+
29
+ let t = 0;
30
+ let l = 0;
31
+
32
+ // Note: These calculations are for positioning absolute relative to document.body
33
+ const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
34
+ const scrollY = window.pageYOffset || document.documentElement.scrollTop;
35
+
36
+ if (placement === 'top') {
37
+ t = rect.top + scrollY - 8;
38
+ l = rect.left + scrollX + rect.width / 2;
39
+ } else if (placement === 'bottom') {
40
+ t = rect.bottom + scrollY + 8;
41
+ l = rect.left + scrollX + rect.width / 2;
42
+ }
43
+
44
+ setCoords({ top: t, left: l });
45
+ }, [placement]);
46
+
47
+ useLayoutEffect(() => {
48
+ if (open) {
49
+ updateCoords();
50
+ window.addEventListener('scroll', updateCoords, true);
51
+ window.addEventListener('resize', updateCoords);
52
+ }
53
+ return () => {
54
+ window.removeEventListener('scroll', updateCoords, true);
55
+ window.removeEventListener('resize', updateCoords);
56
+ };
57
+ }, [open, updateCoords]);
58
+
59
+ const handleMouseEnter = () => {
60
+ if (timerRef.current) clearTimeout(timerRef.current);
61
+ timerRef.current = setTimeout(() => {
62
+ setOpen(true);
63
+ }, enterDelay);
64
+ };
65
+
66
+ const handleMouseLeave = () => {
67
+ if (timerRef.current) clearTimeout(timerRef.current);
68
+ timerRef.current = setTimeout(() => {
69
+ setOpen(false);
70
+ }, leaveDelay);
71
+ };
72
+
73
+ const wrapperRef = useCallback((node: HTMLSpanElement | null) => {
74
+ // Use the first real child element for coordinates, not the wrapper span.
75
+ // display:contents would give zero rects; inline-block wrapper gives correct rects
76
+ // but we want coords from the actual child for pixel-perfect placement.
77
+ childRef.current = node ? (node.firstElementChild as HTMLElement) ?? node : null;
78
+ }, []);
79
+
80
+ return (
81
+ <>
82
+ <span
83
+ ref={wrapperRef}
84
+ style={{ display: 'inline-block', lineHeight: 0 }}
85
+ onMouseEnter={handleMouseEnter}
86
+ onMouseLeave={handleMouseLeave}
87
+ >
88
+ {children}
89
+ </span>
90
+ {open && title && ReactDOM.createPortal(
91
+ <div
92
+ className={`ogx-tooltip ogx-tooltip--${placement}`}
93
+ style={{
94
+ top: coords.top,
95
+ left: coords.left,
96
+ position: 'absolute',
97
+ zIndex: 99999
98
+ }}
99
+ >
100
+ {title}
101
+ </div>,
102
+ document.body
103
+ )}
104
+ </>
105
+ );
106
+ }