@smallwebco/tinypivot-react 1.0.48 → 1.0.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -29,10 +29,10 @@ __export(index_exports, {
29
29
  enableDemoMode: () => enableDemoMode,
30
30
  exportPivotToCSV: () => exportPivotToCSV,
31
31
  exportToCSV: () => exportToCSV,
32
- formatCellValue: () => import_tinypivot_core.formatCellValue,
32
+ formatCellValue: () => import_tinypivot_core2.formatCellValue,
33
33
  formatSelectionForClipboard: () => formatSelectionForClipboard,
34
- getAggregationLabel: () => import_tinypivot_core3.getAggregationLabel,
35
- getColumnUniqueValues: () => import_tinypivot_core.getColumnUniqueValues,
34
+ getAggregationLabel: () => import_tinypivot_core5.getAggregationLabel,
35
+ getColumnUniqueValues: () => import_tinypivot_core2.getColumnUniqueValues,
36
36
  setLicenseKey: () => setLicenseKey,
37
37
  useColumnResize: () => useColumnResize,
38
38
  useExcelGrid: () => useExcelGrid,
@@ -44,591 +44,1058 @@ __export(index_exports, {
44
44
  });
45
45
  module.exports = __toCommonJS(index_exports);
46
46
 
47
- // src/components/DataGrid.tsx
48
- var import_react10 = require("react");
49
- var import_react_dom2 = require("react-dom");
50
-
51
- // src/hooks/useExcelGrid.ts
52
- var import_react = require("react");
53
- var import_react_table = require("@tanstack/react-table");
47
+ // src/components/CalculatedFieldModal.tsx
54
48
  var import_tinypivot_core = require("@smallwebco/tinypivot-core");
55
- var multiSelectFilter = (row, columnId, filterValue) => {
56
- if (!filterValue) return true;
57
- if ((0, import_tinypivot_core.isNumericRange)(filterValue)) {
58
- const cellValue = row.getValue(columnId);
59
- if (cellValue === null || cellValue === void 0 || cellValue === "") {
60
- return false;
49
+ var import_react = require("react");
50
+ var import_react_dom = require("react-dom");
51
+ var import_jsx_runtime = require("react/jsx-runtime");
52
+ function CalculatedFieldModal({
53
+ show,
54
+ availableFields,
55
+ existingField,
56
+ onClose,
57
+ onSave
58
+ }) {
59
+ const [name, setName] = (0, import_react.useState)("");
60
+ const [formula, setFormula] = (0, import_react.useState)("");
61
+ const [formatAs, setFormatAs] = (0, import_react.useState)("number");
62
+ const [decimals, setDecimals] = (0, import_react.useState)(2);
63
+ const [error, setError] = (0, import_react.useState)(null);
64
+ (0, import_react.useEffect)(() => {
65
+ if (show) {
66
+ if (existingField) {
67
+ setName(existingField.name);
68
+ setFormula(existingField.formula);
69
+ setFormatAs(existingField.formatAs || "number");
70
+ setDecimals(existingField.decimals ?? 2);
71
+ } else {
72
+ setName("");
73
+ setFormula("");
74
+ setFormatAs("number");
75
+ setDecimals(2);
76
+ }
77
+ setError(null);
61
78
  }
62
- const num = typeof cellValue === "number" ? cellValue : Number.parseFloat(String(cellValue));
63
- if (Number.isNaN(num)) return false;
64
- const { min, max } = filterValue;
65
- if (min !== null && num < min) return false;
66
- if (max !== null && num > max) return false;
67
- return true;
68
- }
69
- if (Array.isArray(filterValue) && filterValue.length > 0) {
70
- const cellValue = row.getValue(columnId);
71
- const cellString = cellValue === null || cellValue === void 0 || cellValue === "" ? "(blank)" : String(cellValue);
72
- return filterValue.includes(cellString);
73
- }
74
- return true;
75
- };
76
- function useExcelGrid(options) {
77
- const { data, enableSorting = true, enableFiltering = true } = options;
78
- const [sorting, setSorting] = (0, import_react.useState)([]);
79
- const [columnFilters, setColumnFilters] = (0, import_react.useState)([]);
80
- const [columnVisibility, setColumnVisibility] = (0, import_react.useState)({});
81
- const [globalFilter, setGlobalFilter] = (0, import_react.useState)("");
82
- const [columnStatsCache, setColumnStatsCache] = (0, import_react.useState)({});
83
- const dataSignature = (0, import_react.useMemo)(
84
- () => `${Date.now()}-${Math.random().toString(36).slice(2)}`,
85
- [data]
86
- );
87
- const columnKeys = (0, import_react.useMemo)(() => {
88
- if (data.length === 0) return [];
89
- return Object.keys(data[0]);
90
- }, [data]);
91
- const getColumnStats = (0, import_react.useCallback)(
92
- (columnKey) => {
93
- const cacheKey = `${columnKey}-${dataSignature}`;
94
- if (!columnStatsCache[cacheKey]) {
95
- const stats = (0, import_tinypivot_core.getColumnUniqueValues)(data, columnKey);
96
- setColumnStatsCache((prev) => ({ ...prev, [cacheKey]: stats }));
97
- return stats;
79
+ }, [show, existingField]);
80
+ const validationError = (0, import_react.useMemo)(() => {
81
+ if (!formula.trim())
82
+ return null;
83
+ return (0, import_tinypivot_core.validateSimpleFormula)(formula, availableFields);
84
+ }, [formula, availableFields]);
85
+ const insertField = (0, import_react.useCallback)((field) => {
86
+ setFormula((prev) => {
87
+ if (prev.trim() && !prev.endsWith(" ")) {
88
+ return `${prev} ${field}`;
98
89
  }
99
- return columnStatsCache[cacheKey];
100
- },
101
- [data, columnStatsCache, dataSignature]
102
- );
103
- const clearStatsCache = (0, import_react.useCallback)(() => {
104
- setColumnStatsCache({});
90
+ return prev + field;
91
+ });
105
92
  }, []);
106
- (0, import_react.useEffect)(() => {
107
- clearStatsCache();
108
- }, [dataSignature, clearStatsCache]);
109
- const columnDefs = (0, import_react.useMemo)(() => {
110
- return columnKeys.map((key) => {
111
- const stats = getColumnStats(key);
112
- return {
113
- id: key,
114
- accessorKey: key,
115
- header: key,
116
- cell: (info) => (0, import_tinypivot_core.formatCellValue)(info.getValue(), stats.type),
117
- filterFn: multiSelectFilter,
118
- meta: {
119
- type: stats.type,
120
- uniqueCount: stats.uniqueValues.length
121
- }
122
- };
93
+ const insertOperator = (0, import_react.useCallback)((op) => {
94
+ setFormula((prev) => {
95
+ if (prev.trim() && !prev.endsWith(" ")) {
96
+ return `${prev} ${op} `;
97
+ }
98
+ return `${prev + op} `;
123
99
  });
124
- }, [columnKeys, getColumnStats]);
125
- const table = (0, import_react_table.useReactTable)({
126
- data,
127
- columns: columnDefs,
128
- state: {
129
- sorting,
130
- columnFilters,
131
- columnVisibility,
132
- globalFilter
133
- },
134
- onSortingChange: setSorting,
135
- onColumnFiltersChange: setColumnFilters,
136
- onColumnVisibilityChange: setColumnVisibility,
137
- onGlobalFilterChange: setGlobalFilter,
138
- getCoreRowModel: (0, import_react_table.getCoreRowModel)(),
139
- getSortedRowModel: enableSorting ? (0, import_react_table.getSortedRowModel)() : void 0,
140
- getFilteredRowModel: enableFiltering ? (0, import_react_table.getFilteredRowModel)() : void 0,
141
- filterFns: {
142
- multiSelect: multiSelectFilter
143
- },
144
- enableSorting,
145
- enableFilters: enableFiltering
146
- });
147
- const filteredRowCount = table.getFilteredRowModel().rows.length;
148
- const totalRowCount = data.length;
149
- const activeFilters = (0, import_react.useMemo)(() => {
150
- return columnFilters.map((f) => {
151
- const filterValue = f.value;
152
- if (filterValue && (0, import_tinypivot_core.isNumericRange)(filterValue)) {
153
- return {
154
- column: f.id,
155
- type: "range",
156
- range: filterValue,
157
- values: []
158
- };
100
+ }, []);
101
+ const handleSave = (0, import_react.useCallback)(() => {
102
+ if (!name.trim()) {
103
+ setError("Name is required");
104
+ return;
105
+ }
106
+ const validationResult = (0, import_tinypivot_core.validateSimpleFormula)(formula, availableFields);
107
+ if (validationResult) {
108
+ setError(validationResult);
109
+ return;
110
+ }
111
+ const field = {
112
+ id: existingField?.id || `calc_${Date.now()}`,
113
+ name: name.trim(),
114
+ formula: formula.trim(),
115
+ formatAs,
116
+ decimals
117
+ };
118
+ onSave(field);
119
+ onClose();
120
+ }, [name, formula, formatAs, decimals, existingField, availableFields, onSave, onClose]);
121
+ const handleOverlayClick = (0, import_react.useCallback)((e) => {
122
+ if (e.target === e.currentTarget) {
123
+ onClose();
124
+ }
125
+ }, [onClose]);
126
+ if (!show)
127
+ return null;
128
+ const modalContent = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vpg-modal-overlay", onClick: handleOverlayClick, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-modal", children: [
129
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-modal-header", children: [
130
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h3", { children: [
131
+ existingField ? "Edit" : "Create",
132
+ " ",
133
+ "Calculated Field"
134
+ ] }),
135
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-modal-close", onClick: onClose, children: "\xD7" })
136
+ ] }),
137
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-modal-body", children: [
138
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-form-group", children: [
139
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-label", children: "Name" }),
140
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
141
+ "input",
142
+ {
143
+ type: "text",
144
+ className: "vpg-input",
145
+ placeholder: "e.g., Profit Margin %",
146
+ value: name,
147
+ onChange: (e) => setName(e.target.value)
148
+ }
149
+ )
150
+ ] }),
151
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-form-group", children: [
152
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-label", children: "Formula" }),
153
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
154
+ "textarea",
155
+ {
156
+ className: "vpg-textarea",
157
+ placeholder: "e.g., revenue / units",
158
+ rows: 2,
159
+ value: formula,
160
+ onChange: (e) => setFormula(e.target.value)
161
+ }
162
+ ),
163
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vpg-formula-hint", children: "Use field names with math operators: + - * / ( )" }),
164
+ validationError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vpg-error", children: validationError })
165
+ ] }),
166
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-form-group", children: [
167
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-label-small", children: "Operators" }),
168
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-button-group", children: [
169
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("+"), children: "+" }),
170
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("-"), children: "\u2212" }),
171
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("*"), children: "\xD7" }),
172
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("/"), children: "\xF7" }),
173
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("("), children: "(" }),
174
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator(")"), children: ")" })
175
+ ] })
176
+ ] }),
177
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-form-group", children: [
178
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-label-small", children: "Insert Field" }),
179
+ availableFields.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vpg-button-group vpg-field-buttons", children: availableFields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
180
+ "button",
181
+ {
182
+ className: "vpg-insert-btn vpg-field-btn",
183
+ onClick: () => insertField(field),
184
+ children: field
185
+ },
186
+ field
187
+ )) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vpg-no-fields", children: "No numeric fields available" })
188
+ ] }),
189
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-form-row", children: [
190
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-form-group vpg-form-group-half", children: [
191
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-label", children: "Format As" }),
192
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
193
+ "select",
194
+ {
195
+ className: "vpg-select",
196
+ value: formatAs,
197
+ onChange: (e) => setFormatAs(e.target.value),
198
+ children: [
199
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "number", children: "Number" }),
200
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "percent", children: "Percentage" }),
201
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "currency", children: "Currency ($)" })
202
+ ]
203
+ }
204
+ )
205
+ ] }),
206
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-form-group vpg-form-group-half", children: [
207
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-label", children: "Decimals" }),
208
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
209
+ "input",
210
+ {
211
+ type: "number",
212
+ className: "vpg-input",
213
+ min: 0,
214
+ max: 6,
215
+ value: decimals,
216
+ onChange: (e) => setDecimals(Number(e.target.value))
217
+ }
218
+ )
219
+ ] })
220
+ ] }),
221
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vpg-error vpg-error-box", children: error })
222
+ ] }),
223
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-modal-footer", children: [
224
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-btn vpg-btn-secondary", onClick: onClose, children: "Cancel" }),
225
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { className: "vpg-btn vpg-btn-primary", onClick: handleSave, children: [
226
+ existingField ? "Update" : "Add",
227
+ " ",
228
+ "Field"
229
+ ] })
230
+ ] })
231
+ ] }) });
232
+ if (typeof document === "undefined")
233
+ return null;
234
+ return (0, import_react_dom.createPortal)(modalContent, document.body);
235
+ }
236
+
237
+ // src/components/ColumnFilter.tsx
238
+ var import_react3 = require("react");
239
+
240
+ // src/components/NumericRangeFilter.tsx
241
+ var import_react2 = require("react");
242
+ var import_jsx_runtime2 = require("react/jsx-runtime");
243
+ function NumericRangeFilter({
244
+ dataMin,
245
+ dataMax,
246
+ currentRange,
247
+ onChange
248
+ }) {
249
+ const [localMin, setLocalMin] = (0, import_react2.useState)(currentRange?.min ?? null);
250
+ const [localMax, setLocalMax] = (0, import_react2.useState)(currentRange?.max ?? null);
251
+ const step = (0, import_react2.useMemo)(() => {
252
+ const range = dataMax - dataMin;
253
+ if (range === 0)
254
+ return 1;
255
+ if (range <= 1)
256
+ return 0.01;
257
+ if (range <= 10)
258
+ return 0.1;
259
+ if (range <= 100)
260
+ return 1;
261
+ if (range <= 1e3)
262
+ return 10;
263
+ return 10 ** (Math.floor(Math.log10(range)) - 2);
264
+ }, [dataMin, dataMax]);
265
+ const formatValue = (0, import_react2.useCallback)((val) => {
266
+ if (val === null)
267
+ return "";
268
+ if (Number.isInteger(val))
269
+ return val.toLocaleString();
270
+ return val.toLocaleString(void 0, { maximumFractionDigits: 2 });
271
+ }, []);
272
+ const isFilterActive = localMin !== null || localMax !== null;
273
+ const minPercent = (0, import_react2.useMemo)(() => {
274
+ if (localMin === null || dataMax === dataMin)
275
+ return 0;
276
+ return (localMin - dataMin) / (dataMax - dataMin) * 100;
277
+ }, [localMin, dataMin, dataMax]);
278
+ const maxPercent = (0, import_react2.useMemo)(() => {
279
+ if (localMax === null || dataMax === dataMin)
280
+ return 100;
281
+ return (localMax - dataMin) / (dataMax - dataMin) * 100;
282
+ }, [localMax, dataMin, dataMax]);
283
+ const handleMinSlider = (0, import_react2.useCallback)((event) => {
284
+ const value = Number.parseFloat(event.target.value);
285
+ setLocalMin(() => {
286
+ if (localMax !== null && value > localMax) {
287
+ return localMax;
159
288
  }
160
- return {
161
- column: f.id,
162
- type: "values",
163
- values: Array.isArray(filterValue) ? filterValue : [],
164
- range: null
165
- };
289
+ return value;
166
290
  });
167
- }, [columnFilters]);
168
- const hasActiveFilter = (0, import_react.useCallback)(
169
- (columnId) => {
170
- const column = table.getColumn(columnId);
171
- if (!column) return false;
172
- const filterValue = column.getFilterValue();
173
- if (!filterValue) return false;
174
- if ((0, import_tinypivot_core.isNumericRange)(filterValue)) {
175
- return filterValue.min !== null || filterValue.max !== null;
291
+ }, [localMax]);
292
+ const handleMaxSlider = (0, import_react2.useCallback)((event) => {
293
+ const value = Number.parseFloat(event.target.value);
294
+ setLocalMax(() => {
295
+ if (localMin !== null && value < localMin) {
296
+ return localMin;
176
297
  }
177
- return Array.isArray(filterValue) && filterValue.length > 0;
178
- },
179
- [table]
180
- );
181
- const setColumnFilter = (0, import_react.useCallback)(
182
- (columnId, values) => {
183
- const column = table.getColumn(columnId);
184
- if (column) {
185
- column.setFilterValue(values.length === 0 ? void 0 : values);
186
- }
187
- },
188
- [table]
189
- );
190
- const setNumericRangeFilter = (0, import_react.useCallback)(
191
- (columnId, range) => {
192
- const column = table.getColumn(columnId);
193
- if (column) {
194
- if (!range || range.min === null && range.max === null) {
195
- column.setFilterValue(void 0);
196
- } else {
197
- column.setFilterValue(range);
298
+ return value;
299
+ });
300
+ }, [localMin]);
301
+ const handleSliderChange = (0, import_react2.useCallback)(() => {
302
+ if (localMin === null && localMax === null) {
303
+ onChange(null);
304
+ } else {
305
+ onChange({ min: localMin, max: localMax });
306
+ }
307
+ }, [localMin, localMax, onChange]);
308
+ const handleMinInput = (0, import_react2.useCallback)((event) => {
309
+ const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
310
+ if (value !== null && !Number.isNaN(value)) {
311
+ setLocalMin(Math.max(dataMin, Math.min(value, localMax ?? dataMax)));
312
+ } else if (value === null) {
313
+ setLocalMin(null);
314
+ }
315
+ }, [dataMin, dataMax, localMax]);
316
+ const handleMaxInput = (0, import_react2.useCallback)((event) => {
317
+ const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
318
+ if (value !== null && !Number.isNaN(value)) {
319
+ setLocalMax(Math.min(dataMax, Math.max(value, localMin ?? dataMin)));
320
+ } else if (value === null) {
321
+ setLocalMax(null);
322
+ }
323
+ }, [dataMin, dataMax, localMin]);
324
+ const handleInputBlur = (0, import_react2.useCallback)(() => {
325
+ if (localMin === null && localMax === null) {
326
+ onChange(null);
327
+ } else {
328
+ onChange({ min: localMin, max: localMax });
329
+ }
330
+ }, [localMin, localMax, onChange]);
331
+ const clearFilter = (0, import_react2.useCallback)(() => {
332
+ setLocalMin(null);
333
+ setLocalMax(null);
334
+ onChange(null);
335
+ }, [onChange]);
336
+ const setFullRange = (0, import_react2.useCallback)(() => {
337
+ setLocalMin(dataMin);
338
+ setLocalMax(dataMax);
339
+ onChange({ min: dataMin, max: dataMax });
340
+ }, [dataMin, dataMax, onChange]);
341
+ (0, import_react2.useEffect)(() => {
342
+ setLocalMin(currentRange?.min ?? null);
343
+ setLocalMax(currentRange?.max ?? null);
344
+ }, [currentRange]);
345
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-range-filter", children: [
346
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-range-info", children: [
347
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vpg-range-label", children: "Data range:" }),
348
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "vpg-range-bounds", children: [
349
+ formatValue(dataMin),
350
+ " ",
351
+ "\u2013",
352
+ formatValue(dataMax)
353
+ ] })
354
+ ] }),
355
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-slider-container", children: [
356
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "vpg-slider-track", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
357
+ "div",
358
+ {
359
+ className: "vpg-slider-fill",
360
+ style: {
361
+ left: `${minPercent}%`,
362
+ right: `${100 - maxPercent}%`
363
+ }
198
364
  }
199
- }
200
- },
201
- [table]
202
- );
203
- const getNumericRangeFilter = (0, import_react.useCallback)(
204
- (columnId) => {
205
- const column = table.getColumn(columnId);
206
- if (!column) return null;
207
- const filterValue = column.getFilterValue();
208
- if (filterValue && (0, import_tinypivot_core.isNumericRange)(filterValue)) {
209
- return filterValue;
210
- }
211
- return null;
212
- },
213
- [table]
214
- );
215
- const clearAllFilters = (0, import_react.useCallback)(() => {
216
- table.resetColumnFilters();
217
- setGlobalFilter("");
218
- setColumnFilters([]);
219
- }, [table]);
220
- const getColumnFilterValues = (0, import_react.useCallback)(
221
- (columnId) => {
222
- const column = table.getColumn(columnId);
223
- if (!column) return [];
224
- const filterValue = column.getFilterValue();
225
- return Array.isArray(filterValue) ? filterValue : [];
226
- },
227
- [table]
365
+ ) }),
366
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
367
+ "input",
368
+ {
369
+ type: "range",
370
+ className: "vpg-slider vpg-slider-min",
371
+ min: dataMin,
372
+ max: dataMax,
373
+ step,
374
+ value: localMin ?? dataMin,
375
+ onChange: handleMinSlider,
376
+ onMouseUp: handleSliderChange,
377
+ onTouchEnd: handleSliderChange
378
+ }
379
+ ),
380
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
381
+ "input",
382
+ {
383
+ type: "range",
384
+ className: "vpg-slider vpg-slider-max",
385
+ min: dataMin,
386
+ max: dataMax,
387
+ step,
388
+ value: localMax ?? dataMax,
389
+ onChange: handleMaxSlider,
390
+ onMouseUp: handleSliderChange,
391
+ onTouchEnd: handleSliderChange
392
+ }
393
+ )
394
+ ] }),
395
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-range-inputs", children: [
396
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-input-group", children: [
397
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { className: "vpg-input-label", children: "Min" }),
398
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
399
+ "input",
400
+ {
401
+ type: "number",
402
+ className: "vpg-range-input",
403
+ placeholder: formatValue(dataMin),
404
+ value: localMin ?? "",
405
+ step,
406
+ onChange: handleMinInput,
407
+ onBlur: handleInputBlur
408
+ }
409
+ )
410
+ ] }),
411
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vpg-input-separator", children: "to" }),
412
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-input-group", children: [
413
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { className: "vpg-input-label", children: "Max" }),
414
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
415
+ "input",
416
+ {
417
+ type: "number",
418
+ className: "vpg-range-input",
419
+ placeholder: formatValue(dataMax),
420
+ value: localMax ?? "",
421
+ step,
422
+ onChange: handleMaxInput,
423
+ onBlur: handleInputBlur
424
+ }
425
+ )
426
+ ] })
427
+ ] }),
428
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-range-actions", children: [
429
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
430
+ "button",
431
+ {
432
+ className: "vpg-range-btn",
433
+ disabled: !isFilterActive,
434
+ onClick: clearFilter,
435
+ children: [
436
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
437
+ "path",
438
+ {
439
+ strokeLinecap: "round",
440
+ strokeLinejoin: "round",
441
+ strokeWidth: 2,
442
+ d: "M6 18L18 6M6 6l12 12"
443
+ }
444
+ ) }),
445
+ "Clear"
446
+ ]
447
+ }
448
+ ),
449
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { className: "vpg-range-btn", onClick: setFullRange, children: [
450
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
451
+ "path",
452
+ {
453
+ strokeLinecap: "round",
454
+ strokeLinejoin: "round",
455
+ strokeWidth: 2,
456
+ d: "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
457
+ }
458
+ ) }),
459
+ "Full Range"
460
+ ] })
461
+ ] }),
462
+ isFilterActive && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-filter-summary", children: [
463
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
464
+ "path",
465
+ {
466
+ strokeLinecap: "round",
467
+ strokeLinejoin: "round",
468
+ strokeWidth: 2,
469
+ d: "M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
470
+ }
471
+ ) }),
472
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
473
+ "Showing values",
474
+ " ",
475
+ localMin !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("strong", { children: [
476
+ "\u2265",
477
+ formatValue(localMin)
478
+ ] }),
479
+ localMin !== null && localMax !== null && " and ",
480
+ localMax !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("strong", { children: [
481
+ "\u2264",
482
+ formatValue(localMax)
483
+ ] })
484
+ ] })
485
+ ] })
486
+ ] });
487
+ }
488
+
489
+ // src/components/ColumnFilter.tsx
490
+ var import_jsx_runtime3 = require("react/jsx-runtime");
491
+ function ColumnFilter({
492
+ columnName,
493
+ stats,
494
+ selectedValues,
495
+ sortDirection,
496
+ numericRange,
497
+ onFilter,
498
+ onSort,
499
+ onClose,
500
+ onRangeFilter
501
+ }) {
502
+ const [searchQuery, setSearchQuery] = (0, import_react3.useState)("");
503
+ const [localSelected, setLocalSelected] = (0, import_react3.useState)(new Set(selectedValues));
504
+ const dropdownRef = (0, import_react3.useRef)(null);
505
+ const searchInputRef = (0, import_react3.useRef)(null);
506
+ const isNumericColumn = stats.type === "number" && stats.numericMin !== void 0 && stats.numericMax !== void 0;
507
+ const [filterMode, setFilterMode] = (0, import_react3.useState)(numericRange ? "range" : "values");
508
+ const [localRange, setLocalRange] = (0, import_react3.useState)(numericRange ?? null);
509
+ const hasBlankValues = stats.nullCount > 0;
510
+ const filteredValues = (0, import_react3.useMemo)(() => {
511
+ const values = stats.uniqueValues;
512
+ if (!searchQuery)
513
+ return values;
514
+ const query = searchQuery.toLowerCase();
515
+ return values.filter((v) => v.toLowerCase().includes(query));
516
+ }, [stats.uniqueValues, searchQuery]);
517
+ const allValues = (0, import_react3.useMemo)(() => {
518
+ const values = [...filteredValues];
519
+ if (hasBlankValues && (!searchQuery || "(blank)".includes(searchQuery.toLowerCase()))) {
520
+ values.unshift("(blank)");
521
+ }
522
+ return values;
523
+ }, [filteredValues, hasBlankValues, searchQuery]);
524
+ const _isAllSelected = (0, import_react3.useMemo)(
525
+ () => allValues.every((v) => localSelected.has(v)),
526
+ [allValues, localSelected]
228
527
  );
229
- const toggleSort = (0, import_react.useCallback)((columnId) => {
230
- setSorting((prev) => {
231
- const current = prev.find((s) => s.id === columnId);
232
- if (!current) {
233
- return [{ id: columnId, desc: false }];
234
- } else if (!current.desc) {
235
- return [{ id: columnId, desc: true }];
528
+ const toggleValue = (0, import_react3.useCallback)((value) => {
529
+ setLocalSelected((prev) => {
530
+ const next = new Set(prev);
531
+ if (next.has(value)) {
532
+ next.delete(value);
236
533
  } else {
237
- return [];
534
+ next.add(value);
238
535
  }
536
+ return next;
239
537
  });
240
538
  }, []);
241
- const getSortDirection = (0, import_react.useCallback)(
242
- (columnId) => {
243
- const sort = sorting.find((s) => s.id === columnId);
244
- if (!sort) return null;
245
- return sort.desc ? "desc" : "asc";
246
- },
247
- [sorting]
248
- );
249
- return {
250
- // Table instance
251
- table,
252
- // State
253
- sorting,
254
- columnFilters,
255
- columnVisibility,
256
- globalFilter,
257
- columnKeys,
258
- setSorting,
259
- setColumnFilters,
260
- setGlobalFilter,
261
- // Computed
262
- filteredRowCount,
263
- totalRowCount,
264
- activeFilters,
265
- // Methods
266
- getColumnStats,
267
- clearStatsCache,
268
- hasActiveFilter,
269
- setColumnFilter,
270
- getColumnFilterValues,
271
- clearAllFilters,
272
- toggleSort,
273
- getSortDirection,
274
- // Numeric range filters
275
- setNumericRangeFilter,
276
- getNumericRangeFilter
277
- };
278
- }
279
-
280
- // src/hooks/usePivotTable.ts
281
- var import_react3 = require("react");
282
- var import_tinypivot_core3 = require("@smallwebco/tinypivot-core");
283
-
284
- // src/hooks/useLicense.ts
285
- var import_react2 = require("react");
286
- var import_tinypivot_core2 = require("@smallwebco/tinypivot-core");
287
- var globalLicenseInfo = (0, import_tinypivot_core2.getFreeLicenseInfo)();
288
- var globalDemoMode = false;
289
- var listeners = /* @__PURE__ */ new Set();
290
- function notifyListeners() {
291
- listeners.forEach((listener) => listener());
292
- }
293
- async function setLicenseKey(key) {
294
- globalLicenseInfo = await (0, import_tinypivot_core2.validateLicenseKey)(key);
295
- if (!globalLicenseInfo.isValid) {
296
- console.warn("[TinyPivot] Invalid or expired license key. Running in free mode.");
297
- } else if (globalLicenseInfo.type !== "free") {
298
- console.info(`[TinyPivot] Pro license activated (${globalLicenseInfo.type})`);
299
- }
300
- notifyListeners();
301
- }
302
- async function enableDemoMode(secret) {
303
- const demoLicense = await (0, import_tinypivot_core2.getDemoLicenseInfo)(secret);
304
- if (!demoLicense) {
305
- console.warn("[TinyPivot] Demo mode activation failed - invalid secret");
306
- return false;
307
- }
308
- globalDemoMode = true;
309
- globalLicenseInfo = demoLicense;
310
- console.info("[TinyPivot] Demo mode enabled - all Pro features unlocked for evaluation");
311
- notifyListeners();
312
- return true;
313
- }
314
- function configureLicenseSecret(secret) {
315
- (0, import_tinypivot_core2.configureLicenseSecret)(secret);
316
- }
317
- function useLicense() {
318
- const [, forceUpdate] = (0, import_react2.useState)({});
319
- (0, import_react2.useState)(() => {
320
- const update = () => forceUpdate({});
321
- listeners.add(update);
322
- return () => listeners.delete(update);
323
- });
324
- const isDemo = globalDemoMode;
325
- const licenseInfo = globalLicenseInfo;
326
- const isPro = (0, import_react2.useMemo)(
327
- () => globalDemoMode || (0, import_tinypivot_core2.isPro)(licenseInfo),
328
- [licenseInfo]
329
- );
330
- const canUsePivot = (0, import_react2.useMemo)(
331
- () => globalDemoMode || (0, import_tinypivot_core2.canUsePivot)(licenseInfo),
332
- [licenseInfo]
333
- );
334
- const canUseAdvancedAggregations = (0, import_react2.useMemo)(
335
- () => globalDemoMode || licenseInfo.features.advancedAggregations,
336
- [licenseInfo]
337
- );
338
- const canUsePercentageMode = (0, import_react2.useMemo)(
339
- () => globalDemoMode || licenseInfo.features.percentageMode,
340
- [licenseInfo]
341
- );
342
- const showWatermark = (0, import_react2.useMemo)(
343
- () => (0, import_tinypivot_core2.shouldShowWatermark)(licenseInfo, globalDemoMode),
344
- [licenseInfo]
345
- );
346
- const requirePro = (0, import_react2.useCallback)((feature) => {
347
- if (!isPro) {
348
- (0, import_tinypivot_core2.logProRequired)(feature);
349
- return false;
350
- }
351
- return true;
352
- }, [isPro]);
353
- return {
354
- licenseInfo,
355
- isDemo,
356
- isPro,
357
- canUsePivot,
358
- canUseAdvancedAggregations,
359
- canUsePercentageMode,
360
- showWatermark,
361
- requirePro
362
- };
363
- }
364
-
365
- // src/hooks/usePivotTable.ts
366
- function usePivotTable(data) {
367
- const { canUsePivot, requirePro } = useLicense();
368
- const [rowFields, setRowFieldsState] = (0, import_react3.useState)([]);
369
- const [columnFields, setColumnFieldsState] = (0, import_react3.useState)([]);
370
- const [valueFields, setValueFields] = (0, import_react3.useState)([]);
371
- const [showRowTotals, setShowRowTotals] = (0, import_react3.useState)(true);
372
- const [showColumnTotals, setShowColumnTotals] = (0, import_react3.useState)(true);
373
- const [calculatedFields, setCalculatedFields] = (0, import_react3.useState)(() => (0, import_tinypivot_core3.loadCalculatedFields)());
374
- const [currentStorageKey, setCurrentStorageKey] = (0, import_react3.useState)(null);
375
- const availableFields = (0, import_react3.useMemo)(() => {
376
- return (0, import_tinypivot_core3.computeAvailableFields)(data);
377
- }, [data]);
378
- const unassignedFields = (0, import_react3.useMemo)(() => {
379
- return (0, import_tinypivot_core3.getUnassignedFields)(availableFields, rowFields, columnFields, valueFields);
380
- }, [availableFields, rowFields, columnFields, valueFields]);
381
- const isConfigured = (0, import_react3.useMemo)(() => {
382
- return (0, import_tinypivot_core3.isPivotConfigured)({
383
- rowFields,
384
- columnFields,
385
- valueFields,
386
- showRowTotals,
387
- showColumnTotals
388
- });
389
- }, [rowFields, columnFields, valueFields, showRowTotals, showColumnTotals]);
390
- const pivotResult = (0, import_react3.useMemo)(() => {
391
- if (!isConfigured) return null;
392
- if (!canUsePivot) return null;
393
- return (0, import_tinypivot_core3.computePivotResult)(data, {
394
- rowFields,
395
- columnFields,
396
- valueFields,
397
- showRowTotals,
398
- showColumnTotals,
399
- calculatedFields
400
- });
401
- }, [data, isConfigured, canUsePivot, rowFields, columnFields, valueFields, showRowTotals, showColumnTotals, calculatedFields]);
402
- (0, import_react3.useEffect)(() => {
403
- if (data.length === 0) return;
404
- const newKeys = Object.keys(data[0]);
405
- const storageKey = (0, import_tinypivot_core3.generateStorageKey)(newKeys);
406
- if (storageKey !== currentStorageKey) {
407
- setCurrentStorageKey(storageKey);
408
- const savedConfig = (0, import_tinypivot_core3.loadPivotConfig)(storageKey);
409
- if (savedConfig && (0, import_tinypivot_core3.isConfigValidForFields)(savedConfig, newKeys)) {
410
- setRowFieldsState(savedConfig.rowFields);
411
- setColumnFieldsState(savedConfig.columnFields);
412
- setValueFields(savedConfig.valueFields);
413
- setShowRowTotals(savedConfig.showRowTotals);
414
- setShowColumnTotals(savedConfig.showColumnTotals);
415
- if (savedConfig.calculatedFields) {
416
- setCalculatedFields(savedConfig.calculatedFields);
417
- }
418
- } else {
419
- const currentConfig = {
420
- rowFields,
421
- columnFields,
422
- valueFields,
423
- showRowTotals,
424
- showColumnTotals
425
- };
426
- if (!(0, import_tinypivot_core3.isConfigValidForFields)(currentConfig, newKeys)) {
427
- setRowFieldsState([]);
428
- setColumnFieldsState([]);
429
- setValueFields([]);
430
- }
539
+ const selectAll = (0, import_react3.useCallback)(() => {
540
+ setLocalSelected((prev) => {
541
+ const next = new Set(prev);
542
+ for (const value of allValues) {
543
+ next.add(value);
431
544
  }
545
+ return next;
546
+ });
547
+ }, [allValues]);
548
+ const clearAll = (0, import_react3.useCallback)(() => {
549
+ setLocalSelected(/* @__PURE__ */ new Set());
550
+ }, []);
551
+ const applyFilter = (0, import_react3.useCallback)(() => {
552
+ if (localSelected.size === 0) {
553
+ onFilter([]);
554
+ } else {
555
+ onFilter(Array.from(localSelected));
432
556
  }
433
- }, [data]);
557
+ onClose();
558
+ }, [localSelected, onFilter, onClose]);
559
+ const sortAscending = (0, import_react3.useCallback)(() => {
560
+ onSort(sortDirection === "asc" ? null : "asc");
561
+ }, [sortDirection, onSort]);
562
+ const sortDescending = (0, import_react3.useCallback)(() => {
563
+ onSort(sortDirection === "desc" ? null : "desc");
564
+ }, [sortDirection, onSort]);
565
+ const clearFilter = (0, import_react3.useCallback)(() => {
566
+ setLocalSelected(/* @__PURE__ */ new Set());
567
+ onFilter([]);
568
+ onClose();
569
+ }, [onFilter, onClose]);
570
+ const handleRangeChange = (0, import_react3.useCallback)((range) => {
571
+ setLocalRange(range);
572
+ }, []);
573
+ const applyRangeFilter = (0, import_react3.useCallback)(() => {
574
+ onRangeFilter?.(localRange);
575
+ onClose();
576
+ }, [localRange, onRangeFilter, onClose]);
577
+ const clearRangeFilter = (0, import_react3.useCallback)(() => {
578
+ setLocalRange(null);
579
+ onRangeFilter?.(null);
580
+ onClose();
581
+ }, [onRangeFilter, onClose]);
434
582
  (0, import_react3.useEffect)(() => {
435
- if (!currentStorageKey) return;
436
- const config = {
437
- rowFields,
438
- columnFields,
439
- valueFields,
440
- showRowTotals,
441
- showColumnTotals,
442
- calculatedFields
443
- };
444
- (0, import_tinypivot_core3.savePivotConfig)(currentStorageKey, config);
445
- }, [currentStorageKey, rowFields, columnFields, valueFields, showRowTotals, showColumnTotals, calculatedFields]);
446
- const addRowField = (0, import_react3.useCallback)(
447
- (field) => {
448
- if (!rowFields.includes(field)) {
449
- setRowFieldsState((prev) => [...prev, field]);
583
+ const handleClickOutside = (event) => {
584
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
585
+ onClose();
450
586
  }
451
- },
452
- [rowFields]
453
- );
454
- const removeRowField = (0, import_react3.useCallback)((field) => {
455
- setRowFieldsState((prev) => prev.filter((f) => f !== field));
456
- }, []);
457
- const setRowFields = (0, import_react3.useCallback)((fields) => {
458
- setRowFieldsState(fields);
459
- }, []);
460
- const addColumnField = (0, import_react3.useCallback)(
461
- (field) => {
462
- if (!columnFields.includes(field)) {
463
- setColumnFieldsState((prev) => [...prev, field]);
587
+ };
588
+ document.addEventListener("mousedown", handleClickOutside);
589
+ return () => document.removeEventListener("mousedown", handleClickOutside);
590
+ }, [onClose]);
591
+ (0, import_react3.useEffect)(() => {
592
+ const handleKeydown = (event) => {
593
+ if (event.key === "Escape") {
594
+ onClose();
595
+ } else if (event.key === "Enter" && event.ctrlKey) {
596
+ applyFilter();
464
597
  }
465
- },
466
- [columnFields]
467
- );
468
- const removeColumnField = (0, import_react3.useCallback)((field) => {
469
- setColumnFieldsState((prev) => prev.filter((f) => f !== field));
470
- }, []);
471
- const setColumnFields = (0, import_react3.useCallback)((fields) => {
472
- setColumnFieldsState(fields);
598
+ };
599
+ document.addEventListener("keydown", handleKeydown);
600
+ return () => document.removeEventListener("keydown", handleKeydown);
601
+ }, [onClose, applyFilter]);
602
+ (0, import_react3.useEffect)(() => {
603
+ searchInputRef.current?.focus();
473
604
  }, []);
474
- const addValueField = (0, import_react3.useCallback)(
475
- (field, aggregation = "sum") => {
476
- if (aggregation !== "sum" && !requirePro(`${aggregation} aggregation`)) {
477
- return;
478
- }
479
- setValueFields((prev) => {
480
- if (prev.some((v) => v.field === field && v.aggregation === aggregation)) {
481
- return prev;
605
+ (0, import_react3.useEffect)(() => {
606
+ setLocalSelected(new Set(selectedValues));
607
+ }, [selectedValues]);
608
+ (0, import_react3.useEffect)(() => {
609
+ setLocalRange(numericRange ?? null);
610
+ if (numericRange) {
611
+ setFilterMode("range");
612
+ }
613
+ }, [numericRange]);
614
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { ref: dropdownRef, className: "vpg-filter-dropdown", children: [
615
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-filter-header", children: [
616
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "vpg-filter-title", children: columnName }),
617
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "vpg-filter-count", children: [
618
+ stats.uniqueValues.length.toLocaleString(),
619
+ " ",
620
+ "unique"
621
+ ] })
622
+ ] }),
623
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-sort-controls", children: [
624
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
625
+ "button",
626
+ {
627
+ className: `vpg-sort-btn ${sortDirection === "asc" ? "active" : ""}`,
628
+ title: isNumericColumn ? "Sort Low to High" : "Sort A to Z",
629
+ onClick: sortAscending,
630
+ children: [
631
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
632
+ "path",
633
+ {
634
+ strokeLinecap: "round",
635
+ strokeLinejoin: "round",
636
+ strokeWidth: 2,
637
+ d: "M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"
638
+ }
639
+ ) }),
640
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: isNumericColumn ? "1\u21929" : "A\u2192Z" })
641
+ ]
482
642
  }
483
- return [...prev, { field, aggregation }];
484
- });
485
- },
486
- [requirePro]
487
- );
488
- const removeValueField = (0, import_react3.useCallback)((field, aggregation) => {
489
- setValueFields((prev) => {
490
- if (aggregation) {
491
- return prev.filter((v) => !(v.field === field && v.aggregation === aggregation));
492
- }
493
- return prev.filter((v) => v.field !== field);
494
- });
495
- }, []);
496
- const updateValueFieldAggregation = (0, import_react3.useCallback)(
497
- (field, oldAgg, newAgg) => {
498
- setValueFields(
499
- (prev) => prev.map((v) => {
500
- if (v.field === field && v.aggregation === oldAgg) {
501
- return { ...v, aggregation: newAgg };
643
+ ),
644
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
645
+ "button",
646
+ {
647
+ className: `vpg-sort-btn ${sortDirection === "desc" ? "active" : ""}`,
648
+ title: isNumericColumn ? "Sort High to Low" : "Sort Z to A",
649
+ onClick: sortDescending,
650
+ children: [
651
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
652
+ "path",
653
+ {
654
+ strokeLinecap: "round",
655
+ strokeLinejoin: "round",
656
+ strokeWidth: 2,
657
+ d: "M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4"
658
+ }
659
+ ) }),
660
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: isNumericColumn ? "9\u21921" : "Z\u2192A" })
661
+ ]
662
+ }
663
+ )
664
+ ] }),
665
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vpg-divider" }),
666
+ isNumericColumn && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-filter-tabs", children: [
667
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
668
+ "button",
669
+ {
670
+ className: `vpg-tab-btn ${filterMode === "values" ? "active" : ""}`,
671
+ onClick: () => setFilterMode("values"),
672
+ children: [
673
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
674
+ "path",
675
+ {
676
+ strokeLinecap: "round",
677
+ strokeLinejoin: "round",
678
+ strokeWidth: 2,
679
+ d: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
680
+ }
681
+ ) }),
682
+ "Values"
683
+ ]
684
+ }
685
+ ),
686
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
687
+ "button",
688
+ {
689
+ className: `vpg-tab-btn ${filterMode === "range" ? "active" : ""}`,
690
+ onClick: () => setFilterMode("range"),
691
+ children: [
692
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
693
+ "path",
694
+ {
695
+ strokeLinecap: "round",
696
+ strokeLinejoin: "round",
697
+ strokeWidth: 2,
698
+ d: "M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
699
+ }
700
+ ) }),
701
+ "Range"
702
+ ]
703
+ }
704
+ )
705
+ ] }),
706
+ (!isNumericColumn || filterMode === "values") && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
707
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-search-container", children: [
708
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "vpg-search-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
709
+ "path",
710
+ {
711
+ strokeLinecap: "round",
712
+ strokeLinejoin: "round",
713
+ strokeWidth: 2,
714
+ d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
502
715
  }
503
- return v;
504
- })
505
- );
506
- },
507
- []
508
- );
509
- const clearConfig = (0, import_react3.useCallback)(() => {
510
- setRowFieldsState([]);
511
- setColumnFieldsState([]);
512
- setValueFields([]);
513
- }, []);
514
- const autoSuggestConfig = (0, import_react3.useCallback)(() => {
515
- if (!requirePro("Pivot Table - Auto Suggest")) return;
516
- if (availableFields.length === 0) return;
517
- const categoricalFields = availableFields.filter((f) => !f.isNumeric && f.uniqueCount < 50);
518
- const numericFields = availableFields.filter((f) => f.isNumeric);
519
- if (categoricalFields.length > 0 && numericFields.length > 0) {
520
- setRowFieldsState([categoricalFields[0].field]);
521
- setValueFields([{ field: numericFields[0].field, aggregation: "sum" }]);
522
- }
523
- }, [availableFields, requirePro]);
524
- const addCalculatedField = (0, import_react3.useCallback)((field) => {
525
- setCalculatedFields((prev) => {
526
- const existing = prev.findIndex((f) => f.id === field.id);
527
- let updated;
528
- if (existing >= 0) {
529
- updated = [...prev.slice(0, existing), field, ...prev.slice(existing + 1)];
530
- } else {
531
- updated = [...prev, field];
532
- }
533
- (0, import_tinypivot_core3.saveCalculatedFields)(updated);
534
- return updated;
535
- });
536
- }, []);
537
- const removeCalculatedField = (0, import_react3.useCallback)((id) => {
538
- setCalculatedFields((prev) => {
539
- const updated = prev.filter((f) => f.id !== id);
540
- (0, import_tinypivot_core3.saveCalculatedFields)(updated);
541
- return updated;
542
- });
543
- setValueFields((prev) => prev.filter((v) => v.field !== `calc:${id}`));
544
- }, []);
545
- return {
546
- // State
547
- rowFields,
548
- columnFields,
549
- valueFields,
550
- showRowTotals,
551
- showColumnTotals,
552
- calculatedFields,
553
- // Computed
554
- availableFields,
555
- unassignedFields,
556
- isConfigured,
557
- pivotResult,
558
- // Actions
559
- addRowField,
560
- removeRowField,
561
- addColumnField,
562
- removeColumnField,
563
- addValueField,
564
- removeValueField,
565
- updateValueFieldAggregation,
566
- clearConfig,
567
- setShowRowTotals,
568
- setShowColumnTotals,
569
- autoSuggestConfig,
570
- setRowFields,
571
- setColumnFields,
572
- addCalculatedField,
573
- removeCalculatedField
574
- };
716
+ ) }),
717
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
718
+ "input",
719
+ {
720
+ ref: searchInputRef,
721
+ type: "text",
722
+ value: searchQuery,
723
+ onChange: (e) => setSearchQuery(e.target.value),
724
+ placeholder: "Search values...",
725
+ className: "vpg-search-input"
726
+ }
727
+ ),
728
+ searchQuery && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-clear-search", onClick: () => setSearchQuery(""), children: "\xD7" })
729
+ ] }),
730
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-bulk-actions", children: [
731
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { className: "vpg-bulk-btn", onClick: selectAll, children: [
732
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
733
+ "path",
734
+ {
735
+ strokeLinecap: "round",
736
+ strokeLinejoin: "round",
737
+ strokeWidth: 2,
738
+ d: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
739
+ }
740
+ ) }),
741
+ "Select All"
742
+ ] }),
743
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { className: "vpg-bulk-btn", onClick: clearAll, children: [
744
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
745
+ "path",
746
+ {
747
+ strokeLinecap: "round",
748
+ strokeLinejoin: "round",
749
+ strokeWidth: 2,
750
+ d: "M6 18L18 6M6 6l12 12"
751
+ }
752
+ ) }),
753
+ "Clear All"
754
+ ] })
755
+ ] }),
756
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-values-list", children: [
757
+ allValues.map((value) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
758
+ "label",
759
+ {
760
+ className: `vpg-value-item ${localSelected.has(value) ? "selected" : ""}`,
761
+ children: [
762
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
763
+ "input",
764
+ {
765
+ type: "checkbox",
766
+ checked: localSelected.has(value),
767
+ onChange: () => toggleValue(value),
768
+ className: "vpg-value-checkbox"
769
+ }
770
+ ),
771
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: `vpg-value-text ${value === "(blank)" ? "vpg-blank" : ""}`, children: value })
772
+ ]
773
+ },
774
+ value
775
+ )),
776
+ allValues.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vpg-no-results", children: "No matching values" })
777
+ ] }),
778
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-filter-footer", children: [
779
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-btn-clear", onClick: clearFilter, children: "Clear Filter" }),
780
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-btn-apply", onClick: applyFilter, children: "Apply" })
781
+ ] })
782
+ ] }),
783
+ isNumericColumn && filterMode === "range" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
784
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
785
+ NumericRangeFilter,
786
+ {
787
+ dataMin: stats.numericMin,
788
+ dataMax: stats.numericMax,
789
+ currentRange: localRange,
790
+ onChange: handleRangeChange
791
+ }
792
+ ),
793
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-filter-footer", children: [
794
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-btn-clear", onClick: clearRangeFilter, children: "Clear Filter" }),
795
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-btn-apply", onClick: applyRangeFilter, children: "Apply" })
796
+ ] })
797
+ ] })
798
+ ] });
575
799
  }
576
800
 
577
- // src/hooks/useGridFeatures.ts
801
+ // src/components/DataGrid.tsx
802
+ var import_react10 = require("react");
803
+ var import_react_dom2 = require("react-dom");
804
+
805
+ // src/hooks/useExcelGrid.ts
806
+ var import_tinypivot_core2 = require("@smallwebco/tinypivot-core");
807
+ var import_react_table = require("@tanstack/react-table");
578
808
  var import_react4 = require("react");
579
- var import_tinypivot_core4 = require("@smallwebco/tinypivot-core");
580
- function exportToCSV(data, columns, options) {
581
- (0, import_tinypivot_core4.exportToCSV)(data, columns, options);
582
- }
583
- function exportPivotToCSV(pivotData, rowFields, columnFields, valueFields, options) {
584
- (0, import_tinypivot_core4.exportPivotToCSV)(pivotData, rowFields, columnFields, valueFields, options);
585
- }
586
- function copyToClipboard(text, onSuccess, onError) {
587
- (0, import_tinypivot_core4.copyToClipboard)(text, onSuccess, onError);
588
- }
589
- function formatSelectionForClipboard(rows, columns, selectionBounds) {
590
- return (0, import_tinypivot_core4.formatSelectionForClipboard)(rows, columns, selectionBounds);
591
- }
592
- function usePagination(data, options = {}) {
593
- const [pageSize, setPageSize] = (0, import_react4.useState)(options.pageSize ?? 50);
594
- const [currentPage, setCurrentPage] = (0, import_react4.useState)(options.currentPage ?? 1);
595
- const totalPages = (0, import_react4.useMemo)(
596
- () => Math.max(1, Math.ceil(data.length / pageSize)),
597
- [data.length, pageSize]
809
+ var multiSelectFilter = (row, columnId, filterValue) => {
810
+ if (!filterValue)
811
+ return true;
812
+ if ((0, import_tinypivot_core2.isNumericRange)(filterValue)) {
813
+ const cellValue = row.getValue(columnId);
814
+ if (cellValue === null || cellValue === void 0 || cellValue === "") {
815
+ return false;
816
+ }
817
+ const num = typeof cellValue === "number" ? cellValue : Number.parseFloat(String(cellValue));
818
+ if (Number.isNaN(num))
819
+ return false;
820
+ const { min, max } = filterValue;
821
+ if (min !== null && num < min)
822
+ return false;
823
+ if (max !== null && num > max)
824
+ return false;
825
+ return true;
826
+ }
827
+ if (Array.isArray(filterValue) && filterValue.length > 0) {
828
+ const cellValue = row.getValue(columnId);
829
+ const cellString = cellValue === null || cellValue === void 0 || cellValue === "" ? "(blank)" : String(cellValue);
830
+ return filterValue.includes(cellString);
831
+ }
832
+ return true;
833
+ };
834
+ function useExcelGrid(options) {
835
+ const { data, enableSorting = true, enableFiltering = true } = options;
836
+ const [sorting, setSorting] = (0, import_react4.useState)([]);
837
+ const [columnFilters, setColumnFilters] = (0, import_react4.useState)([]);
838
+ const [columnVisibility, setColumnVisibility] = (0, import_react4.useState)({});
839
+ const [globalFilter, setGlobalFilter] = (0, import_react4.useState)("");
840
+ const [columnStatsCache, setColumnStatsCache] = (0, import_react4.useState)({});
841
+ const dataSignature = (0, import_react4.useMemo)(
842
+ () => `${Date.now()}-${Math.random().toString(36).slice(2)}`,
843
+ [data]
598
844
  );
599
- const paginatedData = (0, import_react4.useMemo)(() => {
600
- const start = (currentPage - 1) * pageSize;
601
- const end = start + pageSize;
602
- return data.slice(start, end);
603
- }, [data, currentPage, pageSize]);
604
- const startIndex = (0, import_react4.useMemo)(() => (currentPage - 1) * pageSize + 1, [currentPage, pageSize]);
605
- const endIndex = (0, import_react4.useMemo)(
606
- () => Math.min(currentPage * pageSize, data.length),
607
- [currentPage, pageSize, data.length]
845
+ const columnKeys = (0, import_react4.useMemo)(() => {
846
+ if (data.length === 0)
847
+ return [];
848
+ return Object.keys(data[0]);
849
+ }, [data]);
850
+ const getColumnStats = (0, import_react4.useCallback)(
851
+ (columnKey) => {
852
+ const cacheKey = `${columnKey}-${dataSignature}`;
853
+ if (!columnStatsCache[cacheKey]) {
854
+ const stats = (0, import_tinypivot_core2.getColumnUniqueValues)(data, columnKey);
855
+ setColumnStatsCache((prev) => ({ ...prev, [cacheKey]: stats }));
856
+ return stats;
857
+ }
858
+ return columnStatsCache[cacheKey];
859
+ },
860
+ [data, columnStatsCache, dataSignature]
608
861
  );
609
- const goToPage = (0, import_react4.useCallback)(
862
+ const clearStatsCache = (0, import_react4.useCallback)(() => {
863
+ setColumnStatsCache({});
864
+ }, []);
865
+ (0, import_react4.useEffect)(() => {
866
+ clearStatsCache();
867
+ }, [dataSignature, clearStatsCache]);
868
+ const columnDefs = (0, import_react4.useMemo)(() => {
869
+ return columnKeys.map((key) => {
870
+ const stats = getColumnStats(key);
871
+ return {
872
+ id: key,
873
+ accessorKey: key,
874
+ header: key,
875
+ cell: (info) => (0, import_tinypivot_core2.formatCellValue)(info.getValue(), stats.type),
876
+ filterFn: multiSelectFilter,
877
+ meta: {
878
+ type: stats.type,
879
+ uniqueCount: stats.uniqueValues.length
880
+ }
881
+ };
882
+ });
883
+ }, [columnKeys, getColumnStats]);
884
+ const table = (0, import_react_table.useReactTable)({
885
+ data,
886
+ columns: columnDefs,
887
+ state: {
888
+ sorting,
889
+ columnFilters,
890
+ columnVisibility,
891
+ globalFilter
892
+ },
893
+ onSortingChange: setSorting,
894
+ onColumnFiltersChange: setColumnFilters,
895
+ onColumnVisibilityChange: setColumnVisibility,
896
+ onGlobalFilterChange: setGlobalFilter,
897
+ getCoreRowModel: (0, import_react_table.getCoreRowModel)(),
898
+ getSortedRowModel: enableSorting ? (0, import_react_table.getSortedRowModel)() : void 0,
899
+ getFilteredRowModel: enableFiltering ? (0, import_react_table.getFilteredRowModel)() : void 0,
900
+ filterFns: {
901
+ multiSelect: multiSelectFilter
902
+ },
903
+ enableSorting,
904
+ enableFilters: enableFiltering
905
+ });
906
+ const filteredRowCount = table.getFilteredRowModel().rows.length;
907
+ const totalRowCount = data.length;
908
+ const activeFilters = (0, import_react4.useMemo)(() => {
909
+ return columnFilters.map((f) => {
910
+ const filterValue = f.value;
911
+ if (filterValue && (0, import_tinypivot_core2.isNumericRange)(filterValue)) {
912
+ return {
913
+ column: f.id,
914
+ type: "range",
915
+ range: filterValue,
916
+ values: []
917
+ };
918
+ }
919
+ return {
920
+ column: f.id,
921
+ type: "values",
922
+ values: Array.isArray(filterValue) ? filterValue : [],
923
+ range: null
924
+ };
925
+ });
926
+ }, [columnFilters]);
927
+ const hasActiveFilter = (0, import_react4.useCallback)(
928
+ (columnId) => {
929
+ const column = table.getColumn(columnId);
930
+ if (!column)
931
+ return false;
932
+ const filterValue = column.getFilterValue();
933
+ if (!filterValue)
934
+ return false;
935
+ if ((0, import_tinypivot_core2.isNumericRange)(filterValue)) {
936
+ return filterValue.min !== null || filterValue.max !== null;
937
+ }
938
+ return Array.isArray(filterValue) && filterValue.length > 0;
939
+ },
940
+ [table]
941
+ );
942
+ const setColumnFilter = (0, import_react4.useCallback)(
943
+ (columnId, values) => {
944
+ const column = table.getColumn(columnId);
945
+ if (column) {
946
+ column.setFilterValue(values.length === 0 ? void 0 : values);
947
+ }
948
+ },
949
+ [table]
950
+ );
951
+ const setNumericRangeFilter = (0, import_react4.useCallback)(
952
+ (columnId, range) => {
953
+ const column = table.getColumn(columnId);
954
+ if (column) {
955
+ if (!range || range.min === null && range.max === null) {
956
+ column.setFilterValue(void 0);
957
+ } else {
958
+ column.setFilterValue(range);
959
+ }
960
+ }
961
+ },
962
+ [table]
963
+ );
964
+ const getNumericRangeFilter = (0, import_react4.useCallback)(
965
+ (columnId) => {
966
+ const column = table.getColumn(columnId);
967
+ if (!column)
968
+ return null;
969
+ const filterValue = column.getFilterValue();
970
+ if (filterValue && (0, import_tinypivot_core2.isNumericRange)(filterValue)) {
971
+ return filterValue;
972
+ }
973
+ return null;
974
+ },
975
+ [table]
976
+ );
977
+ const clearAllFilters = (0, import_react4.useCallback)(() => {
978
+ table.resetColumnFilters();
979
+ setGlobalFilter("");
980
+ setColumnFilters([]);
981
+ }, [table]);
982
+ const getColumnFilterValues = (0, import_react4.useCallback)(
983
+ (columnId) => {
984
+ const column = table.getColumn(columnId);
985
+ if (!column)
986
+ return [];
987
+ const filterValue = column.getFilterValue();
988
+ return Array.isArray(filterValue) ? filterValue : [];
989
+ },
990
+ [table]
991
+ );
992
+ const toggleSort = (0, import_react4.useCallback)((columnId) => {
993
+ setSorting((prev) => {
994
+ const current = prev.find((s) => s.id === columnId);
995
+ if (!current) {
996
+ return [{ id: columnId, desc: false }];
997
+ } else if (!current.desc) {
998
+ return [{ id: columnId, desc: true }];
999
+ } else {
1000
+ return [];
1001
+ }
1002
+ });
1003
+ }, []);
1004
+ const getSortDirection = (0, import_react4.useCallback)(
1005
+ (columnId) => {
1006
+ const sort = sorting.find((s) => s.id === columnId);
1007
+ if (!sort)
1008
+ return null;
1009
+ return sort.desc ? "desc" : "asc";
1010
+ },
1011
+ [sorting]
1012
+ );
1013
+ return {
1014
+ // Table instance
1015
+ table,
1016
+ // State
1017
+ sorting,
1018
+ columnFilters,
1019
+ columnVisibility,
1020
+ globalFilter,
1021
+ columnKeys,
1022
+ setSorting,
1023
+ setColumnFilters,
1024
+ setGlobalFilter,
1025
+ // Computed
1026
+ filteredRowCount,
1027
+ totalRowCount,
1028
+ activeFilters,
1029
+ // Methods
1030
+ getColumnStats,
1031
+ clearStatsCache,
1032
+ hasActiveFilter,
1033
+ setColumnFilter,
1034
+ getColumnFilterValues,
1035
+ clearAllFilters,
1036
+ toggleSort,
1037
+ getSortDirection,
1038
+ // Numeric range filters
1039
+ setNumericRangeFilter,
1040
+ getNumericRangeFilter
1041
+ };
1042
+ }
1043
+
1044
+ // src/hooks/useGridFeatures.ts
1045
+ var import_tinypivot_core3 = require("@smallwebco/tinypivot-core");
1046
+ var import_react5 = require("react");
1047
+ function exportToCSV(data, columns, options) {
1048
+ (0, import_tinypivot_core3.exportToCSV)(data, columns, options);
1049
+ }
1050
+ function exportPivotToCSV(pivotData, rowFields, columnFields, valueFields, options) {
1051
+ (0, import_tinypivot_core3.exportPivotToCSV)(pivotData, rowFields, columnFields, valueFields, options);
1052
+ }
1053
+ function copyToClipboard(text, onSuccess, onError) {
1054
+ (0, import_tinypivot_core3.copyToClipboard)(text, onSuccess, onError);
1055
+ }
1056
+ function formatSelectionForClipboard(rows, columns, selectionBounds) {
1057
+ return (0, import_tinypivot_core3.formatSelectionForClipboard)(rows, columns, selectionBounds);
1058
+ }
1059
+ function usePagination(data, options = {}) {
1060
+ const [pageSize, setPageSize] = (0, import_react5.useState)(options.pageSize ?? 50);
1061
+ const [currentPage, setCurrentPage] = (0, import_react5.useState)(options.currentPage ?? 1);
1062
+ const totalPages = (0, import_react5.useMemo)(
1063
+ () => Math.max(1, Math.ceil(data.length / pageSize)),
1064
+ [data.length, pageSize]
1065
+ );
1066
+ const paginatedData = (0, import_react5.useMemo)(() => {
1067
+ const start = (currentPage - 1) * pageSize;
1068
+ const end = start + pageSize;
1069
+ return data.slice(start, end);
1070
+ }, [data, currentPage, pageSize]);
1071
+ const startIndex = (0, import_react5.useMemo)(() => (currentPage - 1) * pageSize + 1, [currentPage, pageSize]);
1072
+ const endIndex = (0, import_react5.useMemo)(
1073
+ () => Math.min(currentPage * pageSize, data.length),
1074
+ [currentPage, pageSize, data.length]
1075
+ );
1076
+ const goToPage = (0, import_react5.useCallback)(
610
1077
  (page) => {
611
1078
  setCurrentPage(Math.max(1, Math.min(page, totalPages)));
612
1079
  },
613
1080
  [totalPages]
614
1081
  );
615
- const nextPage = (0, import_react4.useCallback)(() => {
1082
+ const nextPage = (0, import_react5.useCallback)(() => {
616
1083
  if (currentPage < totalPages) {
617
1084
  setCurrentPage((prev) => prev + 1);
618
1085
  }
619
1086
  }, [currentPage, totalPages]);
620
- const prevPage = (0, import_react4.useCallback)(() => {
1087
+ const prevPage = (0, import_react5.useCallback)(() => {
621
1088
  if (currentPage > 1) {
622
1089
  setCurrentPage((prev) => prev - 1);
623
1090
  }
624
1091
  }, [currentPage]);
625
- const firstPage = (0, import_react4.useCallback)(() => {
1092
+ const firstPage = (0, import_react5.useCallback)(() => {
626
1093
  setCurrentPage(1);
627
1094
  }, []);
628
- const lastPage = (0, import_react4.useCallback)(() => {
1095
+ const lastPage = (0, import_react5.useCallback)(() => {
629
1096
  setCurrentPage(totalPages);
630
1097
  }, [totalPages]);
631
- const updatePageSize = (0, import_react4.useCallback)((size) => {
1098
+ const updatePageSize = (0, import_react5.useCallback)((size) => {
632
1099
  setPageSize(size);
633
1100
  setCurrentPage(1);
634
1101
  }, []);
@@ -648,9 +1115,9 @@ function usePagination(data, options = {}) {
648
1115
  };
649
1116
  }
650
1117
  function useGlobalSearch(data, columns) {
651
- const [searchTerm, setSearchTerm] = (0, import_react4.useState)("");
652
- const [caseSensitive, setCaseSensitive] = (0, import_react4.useState)(false);
653
- const filteredData = (0, import_react4.useMemo)(() => {
1118
+ const [searchTerm, setSearchTerm] = (0, import_react5.useState)("");
1119
+ const [caseSensitive, setCaseSensitive] = (0, import_react5.useState)(false);
1120
+ const filteredData = (0, import_react5.useMemo)(() => {
654
1121
  if (!searchTerm.trim()) {
655
1122
  return data;
656
1123
  }
@@ -658,906 +1125,470 @@ function useGlobalSearch(data, columns) {
658
1125
  return data.filter((row) => {
659
1126
  for (const col of columns) {
660
1127
  const value = row[col];
661
- if (value === null || value === void 0) continue;
662
- const strValue = caseSensitive ? String(value) : String(value).toLowerCase();
663
- if (strValue.includes(term)) {
664
- return true;
665
- }
666
- }
667
- return false;
668
- });
669
- }, [data, columns, searchTerm, caseSensitive]);
670
- const clearSearch = (0, import_react4.useCallback)(() => {
671
- setSearchTerm("");
672
- }, []);
673
- return {
674
- searchTerm,
675
- setSearchTerm,
676
- caseSensitive,
677
- setCaseSensitive,
678
- filteredData,
679
- clearSearch
680
- };
681
- }
682
- function useRowSelection(data) {
683
- const [selectedRowIndices, setSelectedRowIndices] = (0, import_react4.useState)(/* @__PURE__ */ new Set());
684
- const selectedRows = (0, import_react4.useMemo)(() => {
685
- return Array.from(selectedRowIndices).sort((a, b) => a - b).map((idx) => data[idx]).filter(Boolean);
686
- }, [data, selectedRowIndices]);
687
- const allSelected = (0, import_react4.useMemo)(() => {
688
- return data.length > 0 && selectedRowIndices.size === data.length;
689
- }, [data.length, selectedRowIndices.size]);
690
- const someSelected = (0, import_react4.useMemo)(() => {
691
- return selectedRowIndices.size > 0 && selectedRowIndices.size < data.length;
692
- }, [data.length, selectedRowIndices.size]);
693
- const toggleRow = (0, import_react4.useCallback)((index) => {
694
- setSelectedRowIndices((prev) => {
695
- const next = new Set(prev);
696
- if (next.has(index)) {
697
- next.delete(index);
698
- } else {
699
- next.add(index);
700
- }
701
- return next;
702
- });
703
- }, []);
704
- const selectRow = (0, import_react4.useCallback)((index) => {
705
- setSelectedRowIndices((prev) => /* @__PURE__ */ new Set([...prev, index]));
706
- }, []);
707
- const deselectRow = (0, import_react4.useCallback)((index) => {
708
- setSelectedRowIndices((prev) => {
709
- const next = new Set(prev);
710
- next.delete(index);
711
- return next;
712
- });
713
- }, []);
714
- const selectAll = (0, import_react4.useCallback)(() => {
715
- setSelectedRowIndices(new Set(data.map((_, idx) => idx)));
716
- }, [data]);
717
- const deselectAll = (0, import_react4.useCallback)(() => {
718
- setSelectedRowIndices(/* @__PURE__ */ new Set());
719
- }, []);
720
- const toggleAll = (0, import_react4.useCallback)(() => {
721
- if (allSelected) {
722
- deselectAll();
723
- } else {
724
- selectAll();
725
- }
726
- }, [allSelected, selectAll, deselectAll]);
727
- const isSelected = (0, import_react4.useCallback)(
728
- (index) => {
729
- return selectedRowIndices.has(index);
730
- },
731
- [selectedRowIndices]
732
- );
733
- const selectRange = (0, import_react4.useCallback)((startIndex, endIndex) => {
734
- const min = Math.min(startIndex, endIndex);
735
- const max = Math.max(startIndex, endIndex);
736
- setSelectedRowIndices((prev) => {
737
- const next = new Set(prev);
738
- for (let i = min; i <= max; i++) {
739
- next.add(i);
740
- }
741
- return next;
742
- });
743
- }, []);
744
- return {
745
- selectedRowIndices,
746
- selectedRows,
747
- allSelected,
748
- someSelected,
749
- toggleRow,
750
- selectRow,
751
- deselectRow,
752
- selectAll,
753
- deselectAll,
754
- toggleAll,
755
- isSelected,
756
- selectRange
757
- };
758
- }
759
- function useColumnResize(initialWidths, minWidth = 60, maxWidth = 600) {
760
- const [columnWidths, setColumnWidths] = (0, import_react4.useState)({ ...initialWidths });
761
- const [isResizing, setIsResizing] = (0, import_react4.useState)(false);
762
- const [resizingColumn, setResizingColumn] = (0, import_react4.useState)(null);
763
- const startResize = (0, import_react4.useCallback)(
764
- (columnId, event) => {
765
- setIsResizing(true);
766
- setResizingColumn(columnId);
767
- const startX = event.clientX;
768
- const startWidth = columnWidths[columnId] || 150;
769
- const handleMouseMove = (e) => {
770
- const diff = e.clientX - startX;
771
- const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + diff));
772
- setColumnWidths((prev) => ({
773
- ...prev,
774
- [columnId]: newWidth
775
- }));
776
- };
777
- const handleMouseUp = () => {
778
- setIsResizing(false);
779
- setResizingColumn(null);
780
- document.removeEventListener("mousemove", handleMouseMove);
781
- document.removeEventListener("mouseup", handleMouseUp);
782
- };
783
- document.addEventListener("mousemove", handleMouseMove);
784
- document.addEventListener("mouseup", handleMouseUp);
785
- },
786
- [columnWidths, minWidth, maxWidth]
787
- );
788
- const resetColumnWidth = (0, import_react4.useCallback)(
789
- (columnId) => {
790
- if (initialWidths[columnId]) {
791
- setColumnWidths((prev) => ({
792
- ...prev,
793
- [columnId]: initialWidths[columnId]
794
- }));
795
- }
796
- },
797
- [initialWidths]
798
- );
799
- const resetAllWidths = (0, import_react4.useCallback)(() => {
800
- setColumnWidths({ ...initialWidths });
801
- }, [initialWidths]);
802
- return {
803
- columnWidths,
804
- setColumnWidths,
805
- isResizing,
806
- resizingColumn,
807
- startResize,
808
- resetColumnWidth,
809
- resetAllWidths
810
- };
811
- }
812
-
813
- // src/components/ColumnFilter.tsx
814
- var import_react6 = require("react");
815
-
816
- // src/components/NumericRangeFilter.tsx
817
- var import_react5 = require("react");
818
- var import_jsx_runtime = require("react/jsx-runtime");
819
- function NumericRangeFilter({
820
- dataMin,
821
- dataMax,
822
- currentRange,
823
- onChange
824
- }) {
825
- const [localMin, setLocalMin] = (0, import_react5.useState)(currentRange?.min ?? null);
826
- const [localMax, setLocalMax] = (0, import_react5.useState)(currentRange?.max ?? null);
827
- const step = (0, import_react5.useMemo)(() => {
828
- const range = dataMax - dataMin;
829
- if (range === 0) return 1;
830
- if (range <= 1) return 0.01;
831
- if (range <= 10) return 0.1;
832
- if (range <= 100) return 1;
833
- if (range <= 1e3) return 10;
834
- return Math.pow(10, Math.floor(Math.log10(range)) - 2);
835
- }, [dataMin, dataMax]);
836
- const formatValue = (0, import_react5.useCallback)((val) => {
837
- if (val === null) return "";
838
- if (Number.isInteger(val)) return val.toLocaleString();
839
- return val.toLocaleString(void 0, { maximumFractionDigits: 2 });
840
- }, []);
841
- const isFilterActive = localMin !== null || localMax !== null;
842
- const minPercent = (0, import_react5.useMemo)(() => {
843
- if (localMin === null || dataMax === dataMin) return 0;
844
- return (localMin - dataMin) / (dataMax - dataMin) * 100;
845
- }, [localMin, dataMin, dataMax]);
846
- const maxPercent = (0, import_react5.useMemo)(() => {
847
- if (localMax === null || dataMax === dataMin) return 100;
848
- return (localMax - dataMin) / (dataMax - dataMin) * 100;
849
- }, [localMax, dataMin, dataMax]);
850
- const handleMinSlider = (0, import_react5.useCallback)((event) => {
851
- const value = Number.parseFloat(event.target.value);
852
- setLocalMin((prev) => {
853
- if (localMax !== null && value > localMax) {
854
- return localMax;
855
- }
856
- return value;
857
- });
858
- }, [localMax]);
859
- const handleMaxSlider = (0, import_react5.useCallback)((event) => {
860
- const value = Number.parseFloat(event.target.value);
861
- setLocalMax((prev) => {
862
- if (localMin !== null && value < localMin) {
863
- return localMin;
864
- }
865
- return value;
866
- });
867
- }, [localMin]);
868
- const handleSliderChange = (0, import_react5.useCallback)(() => {
869
- if (localMin === null && localMax === null) {
870
- onChange(null);
871
- } else {
872
- onChange({ min: localMin, max: localMax });
873
- }
874
- }, [localMin, localMax, onChange]);
875
- const handleMinInput = (0, import_react5.useCallback)((event) => {
876
- const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
877
- if (value !== null && !Number.isNaN(value)) {
878
- setLocalMin(Math.max(dataMin, Math.min(value, localMax ?? dataMax)));
879
- } else if (value === null) {
880
- setLocalMin(null);
881
- }
882
- }, [dataMin, dataMax, localMax]);
883
- const handleMaxInput = (0, import_react5.useCallback)((event) => {
884
- const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
885
- if (value !== null && !Number.isNaN(value)) {
886
- setLocalMax(Math.min(dataMax, Math.max(value, localMin ?? dataMin)));
887
- } else if (value === null) {
888
- setLocalMax(null);
889
- }
890
- }, [dataMin, dataMax, localMin]);
891
- const handleInputBlur = (0, import_react5.useCallback)(() => {
892
- if (localMin === null && localMax === null) {
893
- onChange(null);
894
- } else {
895
- onChange({ min: localMin, max: localMax });
896
- }
897
- }, [localMin, localMax, onChange]);
898
- const clearFilter = (0, import_react5.useCallback)(() => {
899
- setLocalMin(null);
900
- setLocalMax(null);
901
- onChange(null);
902
- }, [onChange]);
903
- const setFullRange = (0, import_react5.useCallback)(() => {
904
- setLocalMin(dataMin);
905
- setLocalMax(dataMax);
906
- onChange({ min: dataMin, max: dataMax });
907
- }, [dataMin, dataMax, onChange]);
908
- (0, import_react5.useEffect)(() => {
909
- setLocalMin(currentRange?.min ?? null);
910
- setLocalMax(currentRange?.max ?? null);
911
- }, [currentRange]);
912
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-range-filter", children: [
913
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-range-info", children: [
914
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "vpg-range-label", children: "Data range:" }),
915
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "vpg-range-bounds", children: [
916
- formatValue(dataMin),
917
- " \u2013 ",
918
- formatValue(dataMax)
919
- ] })
920
- ] }),
921
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-slider-container", children: [
922
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vpg-slider-track", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
923
- "div",
924
- {
925
- className: "vpg-slider-fill",
926
- style: {
927
- left: `${minPercent}%`,
928
- right: `${100 - maxPercent}%`
929
- }
930
- }
931
- ) }),
932
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
933
- "input",
934
- {
935
- type: "range",
936
- className: "vpg-slider vpg-slider-min",
937
- min: dataMin,
938
- max: dataMax,
939
- step,
940
- value: localMin ?? dataMin,
941
- onChange: handleMinSlider,
942
- onMouseUp: handleSliderChange,
943
- onTouchEnd: handleSliderChange
944
- }
945
- ),
946
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
947
- "input",
948
- {
949
- type: "range",
950
- className: "vpg-slider vpg-slider-max",
951
- min: dataMin,
952
- max: dataMax,
953
- step,
954
- value: localMax ?? dataMax,
955
- onChange: handleMaxSlider,
956
- onMouseUp: handleSliderChange,
957
- onTouchEnd: handleSliderChange
958
- }
959
- )
960
- ] }),
961
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-range-inputs", children: [
962
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-input-group", children: [
963
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-input-label", children: "Min" }),
964
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
965
- "input",
966
- {
967
- type: "number",
968
- className: "vpg-range-input",
969
- placeholder: formatValue(dataMin),
970
- value: localMin ?? "",
971
- step,
972
- onChange: handleMinInput,
973
- onBlur: handleInputBlur
974
- }
975
- )
976
- ] }),
977
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "vpg-input-separator", children: "to" }),
978
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-input-group", children: [
979
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-input-label", children: "Max" }),
980
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
981
- "input",
982
- {
983
- type: "number",
984
- className: "vpg-range-input",
985
- placeholder: formatValue(dataMax),
986
- value: localMax ?? "",
987
- step,
988
- onChange: handleMaxInput,
989
- onBlur: handleInputBlur
990
- }
991
- )
992
- ] })
993
- ] }),
994
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-range-actions", children: [
995
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
996
- "button",
997
- {
998
- className: "vpg-range-btn",
999
- disabled: !isFilterActive,
1000
- onClick: clearFilter,
1001
- children: [
1002
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1003
- "path",
1004
- {
1005
- strokeLinecap: "round",
1006
- strokeLinejoin: "round",
1007
- strokeWidth: 2,
1008
- d: "M6 18L18 6M6 6l12 12"
1009
- }
1010
- ) }),
1011
- "Clear"
1012
- ]
1013
- }
1014
- ),
1015
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { className: "vpg-range-btn", onClick: setFullRange, children: [
1016
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1017
- "path",
1018
- {
1019
- strokeLinecap: "round",
1020
- strokeLinejoin: "round",
1021
- strokeWidth: 2,
1022
- d: "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
1023
- }
1024
- ) }),
1025
- "Full Range"
1026
- ] })
1027
- ] }),
1028
- isFilterActive && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-filter-summary", children: [
1029
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1030
- "path",
1031
- {
1032
- strokeLinecap: "round",
1033
- strokeLinejoin: "round",
1034
- strokeWidth: 2,
1035
- d: "M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
1036
- }
1037
- ) }),
1038
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
1039
- "Showing values",
1040
- " ",
1041
- localMin !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
1042
- "\u2265 ",
1043
- formatValue(localMin)
1044
- ] }),
1045
- localMin !== null && localMax !== null && " and ",
1046
- localMax !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
1047
- "\u2264 ",
1048
- formatValue(localMax)
1049
- ] })
1050
- ] })
1051
- ] })
1052
- ] });
1053
- }
1054
-
1055
- // src/components/ColumnFilter.tsx
1056
- var import_jsx_runtime2 = require("react/jsx-runtime");
1057
- function ColumnFilter({
1058
- columnName,
1059
- stats,
1060
- selectedValues,
1061
- sortDirection,
1062
- numericRange,
1063
- onFilter,
1064
- onSort,
1065
- onClose,
1066
- onRangeFilter
1067
- }) {
1068
- const [searchQuery, setSearchQuery] = (0, import_react6.useState)("");
1069
- const [localSelected, setLocalSelected] = (0, import_react6.useState)(new Set(selectedValues));
1070
- const dropdownRef = (0, import_react6.useRef)(null);
1071
- const searchInputRef = (0, import_react6.useRef)(null);
1072
- const isNumericColumn = stats.type === "number" && stats.numericMin !== void 0 && stats.numericMax !== void 0;
1073
- const [filterMode, setFilterMode] = (0, import_react6.useState)(numericRange ? "range" : "values");
1074
- const [localRange, setLocalRange] = (0, import_react6.useState)(numericRange ?? null);
1075
- const hasBlankValues = stats.nullCount > 0;
1076
- const filteredValues = (0, import_react6.useMemo)(() => {
1077
- const values = stats.uniqueValues;
1078
- if (!searchQuery) return values;
1079
- const query = searchQuery.toLowerCase();
1080
- return values.filter((v) => v.toLowerCase().includes(query));
1081
- }, [stats.uniqueValues, searchQuery]);
1082
- const allValues = (0, import_react6.useMemo)(() => {
1083
- const values = [...filteredValues];
1084
- if (hasBlankValues && (!searchQuery || "(blank)".includes(searchQuery.toLowerCase()))) {
1085
- values.unshift("(blank)");
1086
- }
1087
- return values;
1088
- }, [filteredValues, hasBlankValues, searchQuery]);
1089
- const isAllSelected = (0, import_react6.useMemo)(
1090
- () => allValues.every((v) => localSelected.has(v)),
1091
- [allValues, localSelected]
1092
- );
1093
- const toggleValue = (0, import_react6.useCallback)((value) => {
1094
- setLocalSelected((prev) => {
1095
- const next = new Set(prev);
1096
- if (next.has(value)) {
1097
- next.delete(value);
1098
- } else {
1099
- next.add(value);
1100
- }
1101
- return next;
1102
- });
1103
- }, []);
1104
- const selectAll = (0, import_react6.useCallback)(() => {
1105
- setLocalSelected((prev) => {
1106
- const next = new Set(prev);
1107
- for (const value of allValues) {
1108
- next.add(value);
1109
- }
1110
- return next;
1111
- });
1112
- }, [allValues]);
1113
- const clearAll = (0, import_react6.useCallback)(() => {
1114
- setLocalSelected(/* @__PURE__ */ new Set());
1115
- }, []);
1116
- const applyFilter = (0, import_react6.useCallback)(() => {
1117
- if (localSelected.size === 0) {
1118
- onFilter([]);
1119
- } else {
1120
- onFilter(Array.from(localSelected));
1121
- }
1122
- onClose();
1123
- }, [localSelected, onFilter, onClose]);
1124
- const sortAscending = (0, import_react6.useCallback)(() => {
1125
- onSort(sortDirection === "asc" ? null : "asc");
1126
- }, [sortDirection, onSort]);
1127
- const sortDescending = (0, import_react6.useCallback)(() => {
1128
- onSort(sortDirection === "desc" ? null : "desc");
1129
- }, [sortDirection, onSort]);
1130
- const clearFilter = (0, import_react6.useCallback)(() => {
1131
- setLocalSelected(/* @__PURE__ */ new Set());
1132
- onFilter([]);
1133
- onClose();
1134
- }, [onFilter, onClose]);
1135
- const handleRangeChange = (0, import_react6.useCallback)((range) => {
1136
- setLocalRange(range);
1137
- }, []);
1138
- const applyRangeFilter = (0, import_react6.useCallback)(() => {
1139
- onRangeFilter?.(localRange);
1140
- onClose();
1141
- }, [localRange, onRangeFilter, onClose]);
1142
- const clearRangeFilter = (0, import_react6.useCallback)(() => {
1143
- setLocalRange(null);
1144
- onRangeFilter?.(null);
1145
- onClose();
1146
- }, [onRangeFilter, onClose]);
1147
- (0, import_react6.useEffect)(() => {
1148
- const handleClickOutside = (event) => {
1149
- if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
1150
- onClose();
1151
- }
1152
- };
1153
- document.addEventListener("mousedown", handleClickOutside);
1154
- return () => document.removeEventListener("mousedown", handleClickOutside);
1155
- }, [onClose]);
1156
- (0, import_react6.useEffect)(() => {
1157
- const handleKeydown = (event) => {
1158
- if (event.key === "Escape") {
1159
- onClose();
1160
- } else if (event.key === "Enter" && event.ctrlKey) {
1161
- applyFilter();
1162
- }
1163
- };
1164
- document.addEventListener("keydown", handleKeydown);
1165
- return () => document.removeEventListener("keydown", handleKeydown);
1166
- }, [onClose, applyFilter]);
1167
- (0, import_react6.useEffect)(() => {
1168
- searchInputRef.current?.focus();
1169
- }, []);
1170
- (0, import_react6.useEffect)(() => {
1171
- setLocalSelected(new Set(selectedValues));
1172
- }, [selectedValues]);
1173
- (0, import_react6.useEffect)(() => {
1174
- setLocalRange(numericRange ?? null);
1175
- if (numericRange) {
1176
- setFilterMode("range");
1177
- }
1178
- }, [numericRange]);
1179
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ref: dropdownRef, className: "vpg-filter-dropdown", children: [
1180
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-filter-header", children: [
1181
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vpg-filter-title", children: columnName }),
1182
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "vpg-filter-count", children: [
1183
- stats.uniqueValues.length.toLocaleString(),
1184
- " unique"
1185
- ] })
1186
- ] }),
1187
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-sort-controls", children: [
1188
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1189
- "button",
1190
- {
1191
- className: `vpg-sort-btn ${sortDirection === "asc" ? "active" : ""}`,
1192
- title: isNumericColumn ? "Sort Low to High" : "Sort A to Z",
1193
- onClick: sortAscending,
1194
- children: [
1195
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1196
- "path",
1197
- {
1198
- strokeLinecap: "round",
1199
- strokeLinejoin: "round",
1200
- strokeWidth: 2,
1201
- d: "M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"
1202
- }
1203
- ) }),
1204
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: isNumericColumn ? "1\u21929" : "A\u2192Z" })
1205
- ]
1206
- }
1207
- ),
1208
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1209
- "button",
1210
- {
1211
- className: `vpg-sort-btn ${sortDirection === "desc" ? "active" : ""}`,
1212
- title: isNumericColumn ? "Sort High to Low" : "Sort Z to A",
1213
- onClick: sortDescending,
1214
- children: [
1215
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1216
- "path",
1217
- {
1218
- strokeLinecap: "round",
1219
- strokeLinejoin: "round",
1220
- strokeWidth: 2,
1221
- d: "M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4"
1222
- }
1223
- ) }),
1224
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: isNumericColumn ? "9\u21921" : "Z\u2192A" })
1225
- ]
1226
- }
1227
- )
1228
- ] }),
1229
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "vpg-divider" }),
1230
- isNumericColumn && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-filter-tabs", children: [
1231
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1232
- "button",
1233
- {
1234
- className: `vpg-tab-btn ${filterMode === "values" ? "active" : ""}`,
1235
- onClick: () => setFilterMode("values"),
1236
- children: [
1237
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1238
- "path",
1239
- {
1240
- strokeLinecap: "round",
1241
- strokeLinejoin: "round",
1242
- strokeWidth: 2,
1243
- d: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
1244
- }
1245
- ) }),
1246
- "Values"
1247
- ]
1248
- }
1249
- ),
1250
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1251
- "button",
1252
- {
1253
- className: `vpg-tab-btn ${filterMode === "range" ? "active" : ""}`,
1254
- onClick: () => setFilterMode("range"),
1255
- children: [
1256
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1257
- "path",
1258
- {
1259
- strokeLinecap: "round",
1260
- strokeLinejoin: "round",
1261
- strokeWidth: 2,
1262
- d: "M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
1263
- }
1264
- ) }),
1265
- "Range"
1266
- ]
1267
- }
1268
- )
1269
- ] }),
1270
- (!isNumericColumn || filterMode === "values") && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
1271
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-search-container", children: [
1272
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-search-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1273
- "path",
1274
- {
1275
- strokeLinecap: "round",
1276
- strokeLinejoin: "round",
1277
- strokeWidth: 2,
1278
- d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
1279
- }
1280
- ) }),
1281
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1282
- "input",
1283
- {
1284
- ref: searchInputRef,
1285
- type: "text",
1286
- value: searchQuery,
1287
- onChange: (e) => setSearchQuery(e.target.value),
1288
- placeholder: "Search values...",
1289
- className: "vpg-search-input"
1290
- }
1291
- ),
1292
- searchQuery && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "vpg-clear-search", onClick: () => setSearchQuery(""), children: "\xD7" })
1293
- ] }),
1294
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-bulk-actions", children: [
1295
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { className: "vpg-bulk-btn", onClick: selectAll, children: [
1296
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1297
- "path",
1298
- {
1299
- strokeLinecap: "round",
1300
- strokeLinejoin: "round",
1301
- strokeWidth: 2,
1302
- d: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
1303
- }
1304
- ) }),
1305
- "Select All"
1306
- ] }),
1307
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { className: "vpg-bulk-btn", onClick: clearAll, children: [
1308
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1309
- "path",
1310
- {
1311
- strokeLinecap: "round",
1312
- strokeLinejoin: "round",
1313
- strokeWidth: 2,
1314
- d: "M6 18L18 6M6 6l12 12"
1315
- }
1316
- ) }),
1317
- "Clear All"
1318
- ] })
1319
- ] }),
1320
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-values-list", children: [
1321
- allValues.map((value) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1322
- "label",
1323
- {
1324
- className: `vpg-value-item ${localSelected.has(value) ? "selected" : ""}`,
1325
- children: [
1326
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1327
- "input",
1328
- {
1329
- type: "checkbox",
1330
- checked: localSelected.has(value),
1331
- onChange: () => toggleValue(value),
1332
- className: "vpg-value-checkbox"
1333
- }
1334
- ),
1335
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: `vpg-value-text ${value === "(blank)" ? "vpg-blank" : ""}`, children: value })
1336
- ]
1337
- },
1338
- value
1339
- )),
1340
- allValues.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "vpg-no-results", children: "No matching values" })
1341
- ] }),
1342
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-filter-footer", children: [
1343
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "vpg-btn-clear", onClick: clearFilter, children: "Clear Filter" }),
1344
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "vpg-btn-apply", onClick: applyFilter, children: "Apply" })
1345
- ] })
1346
- ] }),
1347
- isNumericColumn && filterMode === "range" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
1348
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1349
- NumericRangeFilter,
1350
- {
1351
- dataMin: stats.numericMin,
1352
- dataMax: stats.numericMax,
1353
- currentRange: localRange,
1354
- onChange: handleRangeChange
1128
+ if (value === null || value === void 0)
1129
+ continue;
1130
+ const strValue = caseSensitive ? String(value) : String(value).toLowerCase();
1131
+ if (strValue.includes(term)) {
1132
+ return true;
1355
1133
  }
1356
- ),
1357
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-filter-footer", children: [
1358
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "vpg-btn-clear", onClick: clearRangeFilter, children: "Clear Filter" }),
1359
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "vpg-btn-apply", onClick: applyRangeFilter, children: "Apply" })
1360
- ] })
1361
- ] })
1362
- ] });
1134
+ }
1135
+ return false;
1136
+ });
1137
+ }, [data, columns, searchTerm, caseSensitive]);
1138
+ const clearSearch = (0, import_react5.useCallback)(() => {
1139
+ setSearchTerm("");
1140
+ }, []);
1141
+ return {
1142
+ searchTerm,
1143
+ setSearchTerm,
1144
+ caseSensitive,
1145
+ setCaseSensitive,
1146
+ filteredData,
1147
+ clearSearch
1148
+ };
1149
+ }
1150
+ function useRowSelection(data) {
1151
+ const [selectedRowIndices, setSelectedRowIndices] = (0, import_react5.useState)(/* @__PURE__ */ new Set());
1152
+ const selectedRows = (0, import_react5.useMemo)(() => {
1153
+ return Array.from(selectedRowIndices).sort((a, b) => a - b).map((idx) => data[idx]).filter(Boolean);
1154
+ }, [data, selectedRowIndices]);
1155
+ const allSelected = (0, import_react5.useMemo)(() => {
1156
+ return data.length > 0 && selectedRowIndices.size === data.length;
1157
+ }, [data.length, selectedRowIndices.size]);
1158
+ const someSelected = (0, import_react5.useMemo)(() => {
1159
+ return selectedRowIndices.size > 0 && selectedRowIndices.size < data.length;
1160
+ }, [data.length, selectedRowIndices.size]);
1161
+ const toggleRow = (0, import_react5.useCallback)((index) => {
1162
+ setSelectedRowIndices((prev) => {
1163
+ const next = new Set(prev);
1164
+ if (next.has(index)) {
1165
+ next.delete(index);
1166
+ } else {
1167
+ next.add(index);
1168
+ }
1169
+ return next;
1170
+ });
1171
+ }, []);
1172
+ const selectRow = (0, import_react5.useCallback)((index) => {
1173
+ setSelectedRowIndices((prev) => /* @__PURE__ */ new Set([...prev, index]));
1174
+ }, []);
1175
+ const deselectRow = (0, import_react5.useCallback)((index) => {
1176
+ setSelectedRowIndices((prev) => {
1177
+ const next = new Set(prev);
1178
+ next.delete(index);
1179
+ return next;
1180
+ });
1181
+ }, []);
1182
+ const selectAll = (0, import_react5.useCallback)(() => {
1183
+ setSelectedRowIndices(new Set(data.map((_, idx) => idx)));
1184
+ }, [data]);
1185
+ const deselectAll = (0, import_react5.useCallback)(() => {
1186
+ setSelectedRowIndices(/* @__PURE__ */ new Set());
1187
+ }, []);
1188
+ const toggleAll = (0, import_react5.useCallback)(() => {
1189
+ if (allSelected) {
1190
+ deselectAll();
1191
+ } else {
1192
+ selectAll();
1193
+ }
1194
+ }, [allSelected, selectAll, deselectAll]);
1195
+ const isSelected = (0, import_react5.useCallback)(
1196
+ (index) => {
1197
+ return selectedRowIndices.has(index);
1198
+ },
1199
+ [selectedRowIndices]
1200
+ );
1201
+ const selectRange = (0, import_react5.useCallback)((startIndex, endIndex) => {
1202
+ const min = Math.min(startIndex, endIndex);
1203
+ const max = Math.max(startIndex, endIndex);
1204
+ setSelectedRowIndices((prev) => {
1205
+ const next = new Set(prev);
1206
+ for (let i = min; i <= max; i++) {
1207
+ next.add(i);
1208
+ }
1209
+ return next;
1210
+ });
1211
+ }, []);
1212
+ return {
1213
+ selectedRowIndices,
1214
+ selectedRows,
1215
+ allSelected,
1216
+ someSelected,
1217
+ toggleRow,
1218
+ selectRow,
1219
+ deselectRow,
1220
+ selectAll,
1221
+ deselectAll,
1222
+ toggleAll,
1223
+ isSelected,
1224
+ selectRange
1225
+ };
1226
+ }
1227
+ function useColumnResize(initialWidths, minWidth = 60, maxWidth = 600) {
1228
+ const [columnWidths, setColumnWidths] = (0, import_react5.useState)({ ...initialWidths });
1229
+ const [isResizing, setIsResizing] = (0, import_react5.useState)(false);
1230
+ const [resizingColumn, setResizingColumn] = (0, import_react5.useState)(null);
1231
+ const startResize = (0, import_react5.useCallback)(
1232
+ (columnId, event) => {
1233
+ setIsResizing(true);
1234
+ setResizingColumn(columnId);
1235
+ const startX = event.clientX;
1236
+ const startWidth = columnWidths[columnId] || 150;
1237
+ const handleMouseMove = (e) => {
1238
+ const diff = e.clientX - startX;
1239
+ const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + diff));
1240
+ setColumnWidths((prev) => ({
1241
+ ...prev,
1242
+ [columnId]: newWidth
1243
+ }));
1244
+ };
1245
+ const handleMouseUp = () => {
1246
+ setIsResizing(false);
1247
+ setResizingColumn(null);
1248
+ document.removeEventListener("mousemove", handleMouseMove);
1249
+ document.removeEventListener("mouseup", handleMouseUp);
1250
+ };
1251
+ document.addEventListener("mousemove", handleMouseMove);
1252
+ document.addEventListener("mouseup", handleMouseUp);
1253
+ },
1254
+ [columnWidths, minWidth, maxWidth]
1255
+ );
1256
+ const resetColumnWidth = (0, import_react5.useCallback)(
1257
+ (columnId) => {
1258
+ if (initialWidths[columnId]) {
1259
+ setColumnWidths((prev) => ({
1260
+ ...prev,
1261
+ [columnId]: initialWidths[columnId]
1262
+ }));
1263
+ }
1264
+ },
1265
+ [initialWidths]
1266
+ );
1267
+ const resetAllWidths = (0, import_react5.useCallback)(() => {
1268
+ setColumnWidths({ ...initialWidths });
1269
+ }, [initialWidths]);
1270
+ return {
1271
+ columnWidths,
1272
+ setColumnWidths,
1273
+ isResizing,
1274
+ resizingColumn,
1275
+ startResize,
1276
+ resetColumnWidth,
1277
+ resetAllWidths
1278
+ };
1363
1279
  }
1364
1280
 
1365
- // src/components/PivotConfig.tsx
1366
- var import_react8 = require("react");
1367
- var import_tinypivot_core6 = require("@smallwebco/tinypivot-core");
1281
+ // src/hooks/useLicense.ts
1282
+ var import_tinypivot_core4 = require("@smallwebco/tinypivot-core");
1283
+ var import_react6 = require("react");
1284
+ var globalLicenseInfo = (0, import_tinypivot_core4.getFreeLicenseInfo)();
1285
+ var globalDemoMode = false;
1286
+ var listeners = /* @__PURE__ */ new Set();
1287
+ function notifyListeners() {
1288
+ listeners.forEach((listener) => listener());
1289
+ }
1290
+ async function setLicenseKey(key) {
1291
+ globalLicenseInfo = await (0, import_tinypivot_core4.validateLicenseKey)(key);
1292
+ if (!globalLicenseInfo.isValid) {
1293
+ console.warn("[TinyPivot] Invalid or expired license key. Running in free mode.");
1294
+ } else if (globalLicenseInfo.type !== "free") {
1295
+ console.info(`[TinyPivot] Pro license activated (${globalLicenseInfo.type})`);
1296
+ }
1297
+ notifyListeners();
1298
+ }
1299
+ async function enableDemoMode(secret) {
1300
+ const demoLicense = await (0, import_tinypivot_core4.getDemoLicenseInfo)(secret);
1301
+ if (!demoLicense) {
1302
+ console.warn("[TinyPivot] Demo mode activation failed - invalid secret");
1303
+ return false;
1304
+ }
1305
+ globalDemoMode = true;
1306
+ globalLicenseInfo = demoLicense;
1307
+ console.info("[TinyPivot] Demo mode enabled - all Pro features unlocked for evaluation");
1308
+ notifyListeners();
1309
+ return true;
1310
+ }
1311
+ function configureLicenseSecret(secret) {
1312
+ (0, import_tinypivot_core4.configureLicenseSecret)(secret);
1313
+ }
1314
+ function useLicense() {
1315
+ const [, forceUpdate] = (0, import_react6.useState)({});
1316
+ (0, import_react6.useState)(() => {
1317
+ const update = () => forceUpdate({});
1318
+ listeners.add(update);
1319
+ return () => listeners.delete(update);
1320
+ });
1321
+ const isDemo = globalDemoMode;
1322
+ const licenseInfo = globalLicenseInfo;
1323
+ const isPro = (0, import_react6.useMemo)(
1324
+ () => globalDemoMode || (0, import_tinypivot_core4.isPro)(licenseInfo),
1325
+ [licenseInfo]
1326
+ );
1327
+ const canUsePivot = (0, import_react6.useMemo)(
1328
+ () => globalDemoMode || (0, import_tinypivot_core4.canUsePivot)(licenseInfo),
1329
+ [licenseInfo]
1330
+ );
1331
+ const canUseAdvancedAggregations = (0, import_react6.useMemo)(
1332
+ () => globalDemoMode || licenseInfo.features.advancedAggregations,
1333
+ [licenseInfo]
1334
+ );
1335
+ const canUsePercentageMode = (0, import_react6.useMemo)(
1336
+ () => globalDemoMode || licenseInfo.features.percentageMode,
1337
+ [licenseInfo]
1338
+ );
1339
+ const showWatermark = (0, import_react6.useMemo)(
1340
+ () => (0, import_tinypivot_core4.shouldShowWatermark)(licenseInfo, globalDemoMode),
1341
+ [licenseInfo]
1342
+ );
1343
+ const requirePro = (0, import_react6.useCallback)((feature) => {
1344
+ if (!isPro) {
1345
+ (0, import_tinypivot_core4.logProRequired)(feature);
1346
+ return false;
1347
+ }
1348
+ return true;
1349
+ }, [isPro]);
1350
+ return {
1351
+ licenseInfo,
1352
+ isDemo,
1353
+ isPro,
1354
+ canUsePivot,
1355
+ canUseAdvancedAggregations,
1356
+ canUsePercentageMode,
1357
+ showWatermark,
1358
+ requirePro
1359
+ };
1360
+ }
1368
1361
 
1369
- // src/components/CalculatedFieldModal.tsx
1370
- var import_react7 = require("react");
1371
- var import_react_dom = require("react-dom");
1362
+ // src/hooks/usePivotTable.ts
1372
1363
  var import_tinypivot_core5 = require("@smallwebco/tinypivot-core");
1373
- var import_jsx_runtime3 = require("react/jsx-runtime");
1374
- function CalculatedFieldModal({
1375
- show,
1376
- availableFields,
1377
- existingField,
1378
- onClose,
1379
- onSave
1380
- }) {
1381
- const [name, setName] = (0, import_react7.useState)("");
1382
- const [formula, setFormula] = (0, import_react7.useState)("");
1383
- const [formatAs, setFormatAs] = (0, import_react7.useState)("number");
1384
- const [decimals, setDecimals] = (0, import_react7.useState)(2);
1385
- const [error, setError] = (0, import_react7.useState)(null);
1364
+ var import_react7 = require("react");
1365
+ function usePivotTable(data) {
1366
+ const { canUsePivot, requirePro } = useLicense();
1367
+ const [rowFields, setRowFieldsState] = (0, import_react7.useState)([]);
1368
+ const [columnFields, setColumnFieldsState] = (0, import_react7.useState)([]);
1369
+ const [valueFields, setValueFields] = (0, import_react7.useState)([]);
1370
+ const [showRowTotals, setShowRowTotals] = (0, import_react7.useState)(true);
1371
+ const [showColumnTotals, setShowColumnTotals] = (0, import_react7.useState)(true);
1372
+ const [calculatedFields, setCalculatedFields] = (0, import_react7.useState)(() => (0, import_tinypivot_core5.loadCalculatedFields)());
1373
+ const [currentStorageKey, setCurrentStorageKey] = (0, import_react7.useState)(null);
1374
+ const availableFields = (0, import_react7.useMemo)(() => {
1375
+ return (0, import_tinypivot_core5.computeAvailableFields)(data);
1376
+ }, [data]);
1377
+ const unassignedFields = (0, import_react7.useMemo)(() => {
1378
+ return (0, import_tinypivot_core5.getUnassignedFields)(availableFields, rowFields, columnFields, valueFields);
1379
+ }, [availableFields, rowFields, columnFields, valueFields]);
1380
+ const isConfigured = (0, import_react7.useMemo)(() => {
1381
+ return (0, import_tinypivot_core5.isPivotConfigured)({
1382
+ rowFields,
1383
+ columnFields,
1384
+ valueFields,
1385
+ showRowTotals,
1386
+ showColumnTotals
1387
+ });
1388
+ }, [rowFields, columnFields, valueFields, showRowTotals, showColumnTotals]);
1389
+ const pivotResult = (0, import_react7.useMemo)(() => {
1390
+ if (!isConfigured)
1391
+ return null;
1392
+ if (!canUsePivot)
1393
+ return null;
1394
+ return (0, import_tinypivot_core5.computePivotResult)(data, {
1395
+ rowFields,
1396
+ columnFields,
1397
+ valueFields,
1398
+ showRowTotals,
1399
+ showColumnTotals,
1400
+ calculatedFields
1401
+ });
1402
+ }, [data, isConfigured, canUsePivot, rowFields, columnFields, valueFields, showRowTotals, showColumnTotals, calculatedFields]);
1386
1403
  (0, import_react7.useEffect)(() => {
1387
- if (show) {
1388
- if (existingField) {
1389
- setName(existingField.name);
1390
- setFormula(existingField.formula);
1391
- setFormatAs(existingField.formatAs || "number");
1392
- setDecimals(existingField.decimals ?? 2);
1393
- } else {
1394
- setName("");
1395
- setFormula("");
1396
- setFormatAs("number");
1397
- setDecimals(2);
1404
+ if (data.length === 0)
1405
+ return;
1406
+ const newKeys = Object.keys(data[0]);
1407
+ const storageKey = (0, import_tinypivot_core5.generateStorageKey)(newKeys);
1408
+ if (storageKey !== currentStorageKey) {
1409
+ setCurrentStorageKey(storageKey);
1410
+ const savedConfig = (0, import_tinypivot_core5.loadPivotConfig)(storageKey);
1411
+ if (savedConfig && (0, import_tinypivot_core5.isConfigValidForFields)(savedConfig, newKeys)) {
1412
+ setRowFieldsState(savedConfig.rowFields);
1413
+ setColumnFieldsState(savedConfig.columnFields);
1414
+ setValueFields(savedConfig.valueFields);
1415
+ setShowRowTotals(savedConfig.showRowTotals);
1416
+ setShowColumnTotals(savedConfig.showColumnTotals);
1417
+ if (savedConfig.calculatedFields) {
1418
+ setCalculatedFields(savedConfig.calculatedFields);
1419
+ }
1420
+ } else {
1421
+ const currentConfig = {
1422
+ rowFields,
1423
+ columnFields,
1424
+ valueFields,
1425
+ showRowTotals,
1426
+ showColumnTotals
1427
+ };
1428
+ if (!(0, import_tinypivot_core5.isConfigValidForFields)(currentConfig, newKeys)) {
1429
+ setRowFieldsState([]);
1430
+ setColumnFieldsState([]);
1431
+ setValueFields([]);
1432
+ }
1398
1433
  }
1399
- setError(null);
1400
1434
  }
1401
- }, [show, existingField]);
1402
- const validationError = (0, import_react7.useMemo)(() => {
1403
- if (!formula.trim()) return null;
1404
- return (0, import_tinypivot_core5.validateSimpleFormula)(formula, availableFields);
1405
- }, [formula, availableFields]);
1406
- const insertField = (0, import_react7.useCallback)((field) => {
1407
- setFormula((prev) => {
1408
- if (prev.trim() && !prev.endsWith(" ")) {
1409
- return prev + " " + field;
1435
+ }, [data]);
1436
+ (0, import_react7.useEffect)(() => {
1437
+ if (!currentStorageKey)
1438
+ return;
1439
+ const config = {
1440
+ rowFields,
1441
+ columnFields,
1442
+ valueFields,
1443
+ showRowTotals,
1444
+ showColumnTotals,
1445
+ calculatedFields
1446
+ };
1447
+ (0, import_tinypivot_core5.savePivotConfig)(currentStorageKey, config);
1448
+ }, [currentStorageKey, rowFields, columnFields, valueFields, showRowTotals, showColumnTotals, calculatedFields]);
1449
+ const addRowField = (0, import_react7.useCallback)(
1450
+ (field) => {
1451
+ if (!rowFields.includes(field)) {
1452
+ setRowFieldsState((prev) => [...prev, field]);
1410
1453
  }
1411
- return prev + field;
1412
- });
1454
+ },
1455
+ [rowFields]
1456
+ );
1457
+ const removeRowField = (0, import_react7.useCallback)((field) => {
1458
+ setRowFieldsState((prev) => prev.filter((f) => f !== field));
1413
1459
  }, []);
1414
- const insertOperator = (0, import_react7.useCallback)((op) => {
1415
- setFormula((prev) => {
1416
- if (prev.trim() && !prev.endsWith(" ")) {
1417
- return prev + " " + op + " ";
1460
+ const setRowFields = (0, import_react7.useCallback)((fields) => {
1461
+ setRowFieldsState(fields);
1462
+ }, []);
1463
+ const addColumnField = (0, import_react7.useCallback)(
1464
+ (field) => {
1465
+ if (!columnFields.includes(field)) {
1466
+ setColumnFieldsState((prev) => [...prev, field]);
1467
+ }
1468
+ },
1469
+ [columnFields]
1470
+ );
1471
+ const removeColumnField = (0, import_react7.useCallback)((field) => {
1472
+ setColumnFieldsState((prev) => prev.filter((f) => f !== field));
1473
+ }, []);
1474
+ const setColumnFields = (0, import_react7.useCallback)((fields) => {
1475
+ setColumnFieldsState(fields);
1476
+ }, []);
1477
+ const addValueField = (0, import_react7.useCallback)(
1478
+ (field, aggregation = "sum") => {
1479
+ if (aggregation !== "sum" && !requirePro(`${aggregation} aggregation`)) {
1480
+ return;
1481
+ }
1482
+ setValueFields((prev) => {
1483
+ if (prev.some((v) => v.field === field && v.aggregation === aggregation)) {
1484
+ return prev;
1485
+ }
1486
+ return [...prev, { field, aggregation }];
1487
+ });
1488
+ },
1489
+ [requirePro]
1490
+ );
1491
+ const removeValueField = (0, import_react7.useCallback)((field, aggregation) => {
1492
+ setValueFields((prev) => {
1493
+ if (aggregation) {
1494
+ return prev.filter((v) => !(v.field === field && v.aggregation === aggregation));
1418
1495
  }
1419
- return prev + op + " ";
1496
+ return prev.filter((v) => v.field !== field);
1420
1497
  });
1421
1498
  }, []);
1422
- const handleSave = (0, import_react7.useCallback)(() => {
1423
- if (!name.trim()) {
1424
- setError("Name is required");
1499
+ const updateValueFieldAggregation = (0, import_react7.useCallback)(
1500
+ (field, oldAgg, newAgg) => {
1501
+ setValueFields(
1502
+ (prev) => prev.map((v) => {
1503
+ if (v.field === field && v.aggregation === oldAgg) {
1504
+ return { ...v, aggregation: newAgg };
1505
+ }
1506
+ return v;
1507
+ })
1508
+ );
1509
+ },
1510
+ []
1511
+ );
1512
+ const clearConfig = (0, import_react7.useCallback)(() => {
1513
+ setRowFieldsState([]);
1514
+ setColumnFieldsState([]);
1515
+ setValueFields([]);
1516
+ }, []);
1517
+ const autoSuggestConfig = (0, import_react7.useCallback)(() => {
1518
+ if (!requirePro("Pivot Table - Auto Suggest"))
1425
1519
  return;
1426
- }
1427
- const validationResult = (0, import_tinypivot_core5.validateSimpleFormula)(formula, availableFields);
1428
- if (validationResult) {
1429
- setError(validationResult);
1520
+ if (availableFields.length === 0)
1430
1521
  return;
1522
+ const categoricalFields = availableFields.filter((f) => !f.isNumeric && f.uniqueCount < 50);
1523
+ const numericFields = availableFields.filter((f) => f.isNumeric);
1524
+ if (categoricalFields.length > 0 && numericFields.length > 0) {
1525
+ setRowFieldsState([categoricalFields[0].field]);
1526
+ setValueFields([{ field: numericFields[0].field, aggregation: "sum" }]);
1431
1527
  }
1432
- const field = {
1433
- id: existingField?.id || `calc_${Date.now()}`,
1434
- name: name.trim(),
1435
- formula: formula.trim(),
1436
- formatAs,
1437
- decimals
1438
- };
1439
- onSave(field);
1440
- onClose();
1441
- }, [name, formula, formatAs, decimals, existingField, availableFields, onSave, onClose]);
1442
- const handleOverlayClick = (0, import_react7.useCallback)((e) => {
1443
- if (e.target === e.currentTarget) {
1444
- onClose();
1445
- }
1446
- }, [onClose]);
1447
- if (!show) return null;
1448
- const modalContent = /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vpg-modal-overlay", onClick: handleOverlayClick, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-modal", children: [
1449
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-modal-header", children: [
1450
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("h3", { children: [
1451
- existingField ? "Edit" : "Create",
1452
- " Calculated Field"
1453
- ] }),
1454
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-modal-close", onClick: onClose, children: "\xD7" })
1455
- ] }),
1456
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-modal-body", children: [
1457
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-form-group", children: [
1458
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "vpg-label", children: "Name" }),
1459
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1460
- "input",
1461
- {
1462
- type: "text",
1463
- className: "vpg-input",
1464
- placeholder: "e.g., Profit Margin %",
1465
- value: name,
1466
- onChange: (e) => setName(e.target.value)
1467
- }
1468
- )
1469
- ] }),
1470
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-form-group", children: [
1471
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "vpg-label", children: "Formula" }),
1472
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1473
- "textarea",
1474
- {
1475
- className: "vpg-textarea",
1476
- placeholder: "e.g., revenue / units",
1477
- rows: 2,
1478
- value: formula,
1479
- onChange: (e) => setFormula(e.target.value)
1480
- }
1481
- ),
1482
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vpg-formula-hint", children: "Use field names with math operators: + - * / ( )" }),
1483
- validationError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vpg-error", children: validationError })
1484
- ] }),
1485
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-form-group", children: [
1486
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "vpg-label-small", children: "Operators" }),
1487
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-button-group", children: [
1488
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("+"), children: "+" }),
1489
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("-"), children: "\u2212" }),
1490
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("*"), children: "\xD7" }),
1491
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("/"), children: "\xF7" }),
1492
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("("), children: "(" }),
1493
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator(")"), children: ")" })
1494
- ] })
1495
- ] }),
1496
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-form-group", children: [
1497
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "vpg-label-small", children: "Insert Field" }),
1498
- availableFields.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vpg-button-group vpg-field-buttons", children: availableFields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1499
- "button",
1500
- {
1501
- className: "vpg-insert-btn vpg-field-btn",
1502
- onClick: () => insertField(field),
1503
- children: field
1504
- },
1505
- field
1506
- )) }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vpg-no-fields", children: "No numeric fields available" })
1507
- ] }),
1508
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-form-row", children: [
1509
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-form-group vpg-form-group-half", children: [
1510
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "vpg-label", children: "Format As" }),
1511
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1512
- "select",
1513
- {
1514
- className: "vpg-select",
1515
- value: formatAs,
1516
- onChange: (e) => setFormatAs(e.target.value),
1517
- children: [
1518
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "number", children: "Number" }),
1519
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "percent", children: "Percentage" }),
1520
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "currency", children: "Currency ($)" })
1521
- ]
1522
- }
1523
- )
1524
- ] }),
1525
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-form-group vpg-form-group-half", children: [
1526
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "vpg-label", children: "Decimals" }),
1527
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1528
- "input",
1529
- {
1530
- type: "number",
1531
- className: "vpg-input",
1532
- min: 0,
1533
- max: 6,
1534
- value: decimals,
1535
- onChange: (e) => setDecimals(Number(e.target.value))
1536
- }
1537
- )
1538
- ] })
1539
- ] }),
1540
- error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vpg-error vpg-error-box", children: error })
1541
- ] }),
1542
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-modal-footer", children: [
1543
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-btn vpg-btn-secondary", onClick: onClose, children: "Cancel" }),
1544
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { className: "vpg-btn vpg-btn-primary", onClick: handleSave, children: [
1545
- existingField ? "Update" : "Add",
1546
- " Field"
1547
- ] })
1548
- ] })
1549
- ] }) });
1550
- if (typeof document === "undefined") return null;
1551
- return (0, import_react_dom.createPortal)(modalContent, document.body);
1528
+ }, [availableFields, requirePro]);
1529
+ const addCalculatedField = (0, import_react7.useCallback)((field) => {
1530
+ setCalculatedFields((prev) => {
1531
+ const existing = prev.findIndex((f) => f.id === field.id);
1532
+ let updated;
1533
+ if (existing >= 0) {
1534
+ updated = [...prev.slice(0, existing), field, ...prev.slice(existing + 1)];
1535
+ } else {
1536
+ updated = [...prev, field];
1537
+ }
1538
+ (0, import_tinypivot_core5.saveCalculatedFields)(updated);
1539
+ return updated;
1540
+ });
1541
+ }, []);
1542
+ const removeCalculatedField = (0, import_react7.useCallback)((id) => {
1543
+ setCalculatedFields((prev) => {
1544
+ const updated = prev.filter((f) => f.id !== id);
1545
+ (0, import_tinypivot_core5.saveCalculatedFields)(updated);
1546
+ return updated;
1547
+ });
1548
+ setValueFields((prev) => prev.filter((v) => v.field !== `calc:${id}`));
1549
+ }, []);
1550
+ return {
1551
+ // State
1552
+ rowFields,
1553
+ columnFields,
1554
+ valueFields,
1555
+ showRowTotals,
1556
+ showColumnTotals,
1557
+ calculatedFields,
1558
+ // Computed
1559
+ availableFields,
1560
+ unassignedFields,
1561
+ isConfigured,
1562
+ pivotResult,
1563
+ // Actions
1564
+ addRowField,
1565
+ removeRowField,
1566
+ addColumnField,
1567
+ removeColumnField,
1568
+ addValueField,
1569
+ removeValueField,
1570
+ updateValueFieldAggregation,
1571
+ clearConfig,
1572
+ setShowRowTotals,
1573
+ setShowColumnTotals,
1574
+ autoSuggestConfig,
1575
+ setRowFields,
1576
+ setColumnFields,
1577
+ addCalculatedField,
1578
+ removeCalculatedField
1579
+ };
1552
1580
  }
1553
1581
 
1554
1582
  // src/components/PivotConfig.tsx
1583
+ var import_tinypivot_core6 = require("@smallwebco/tinypivot-core");
1584
+ var import_react8 = require("react");
1555
1585
  var import_jsx_runtime4 = require("react/jsx-runtime");
1556
1586
  function aggregationRequiresPro(agg) {
1557
1587
  return agg !== "sum";
1558
1588
  }
1559
1589
  function getFieldIcon(type, isCalculated) {
1560
- if (isCalculated) return "\u0192";
1590
+ if (isCalculated)
1591
+ return "\u0192";
1561
1592
  switch (type) {
1562
1593
  case "number":
1563
1594
  return "#";
@@ -1579,7 +1610,7 @@ function PivotConfig({
1579
1610
  onShowRowTotalsChange,
1580
1611
  onShowColumnTotalsChange,
1581
1612
  onClearConfig,
1582
- onAutoSuggest,
1613
+ onAutoSuggest: _onAutoSuggest,
1583
1614
  onDragStart,
1584
1615
  onDragEnd,
1585
1616
  onUpdateAggregation,
@@ -1599,12 +1630,10 @@ function PivotConfig({
1599
1630
  const isAggregationAvailable = (0, import_react8.useCallback)((agg) => {
1600
1631
  return !aggregationRequiresPro(agg) || canUseAdvancedAggregations;
1601
1632
  }, [canUseAdvancedAggregations]);
1602
- const numericFieldNames = (0, import_react8.useMemo)(
1603
- () => availableFields.filter((f) => f.isNumeric).map((f) => f.field),
1604
- [availableFields]
1605
- );
1633
+ const numericFieldNames = (0, import_react8.useMemo)(() => availableFields.filter((f) => f.isNumeric).map((f) => f.field), [availableFields]);
1606
1634
  const calculatedFieldsAsStats = (0, import_react8.useMemo)(() => {
1607
- if (!calculatedFields) return [];
1635
+ if (!calculatedFields)
1636
+ return [];
1608
1637
  return calculatedFields.map((calc) => ({
1609
1638
  field: `calc:${calc.id}`,
1610
1639
  type: "number",
@@ -1639,7 +1668,8 @@ function PivotConfig({
1639
1668
  );
1640
1669
  }, [allAvailableFields, rowFields, columnFields, valueFields]);
1641
1670
  const filteredUnassignedFields = (0, import_react8.useMemo)(() => {
1642
- if (!fieldSearch.trim()) return unassignedFields;
1671
+ if (!fieldSearch.trim())
1672
+ return unassignedFields;
1643
1673
  const search = fieldSearch.toLowerCase().trim();
1644
1674
  return unassignedFields.filter((f) => {
1645
1675
  const fieldName = f.field.toLowerCase();
@@ -1844,7 +1874,8 @@ function PivotConfig({
1844
1874
  ] }),
1845
1875
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "vpg-unassigned-section", children: [
1846
1876
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "vpg-section-header", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "vpg-section-label", children: [
1847
- "Available ",
1877
+ "Available",
1878
+ " ",
1848
1879
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "vpg-count", children: unassignedFields.length })
1849
1880
  ] }) }),
1850
1881
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "vpg-field-search", children: [
@@ -1898,7 +1929,8 @@ function PivotConfig({
1898
1929
  onClick: (e) => {
1899
1930
  e.stopPropagation();
1900
1931
  const calcField = calculatedFields?.find((c) => c.id === field.calcId);
1901
- if (calcField) openCalcModal(calcField);
1932
+ if (calcField)
1933
+ openCalcModal(calcField);
1902
1934
  },
1903
1935
  children: "\u270E"
1904
1936
  }
@@ -1961,8 +1993,8 @@ function PivotConfig({
1961
1993
  }
1962
1994
 
1963
1995
  // src/components/PivotSkeleton.tsx
1964
- var import_react9 = require("react");
1965
1996
  var import_tinypivot_core7 = require("@smallwebco/tinypivot-core");
1997
+ var import_react9 = require("react");
1966
1998
  var import_jsx_runtime5 = require("react/jsx-runtime");
1967
1999
  function PivotSkeleton({
1968
2000
  rowFields,
@@ -2017,7 +2049,8 @@ function PivotSkeleton({
2017
2049
  const [showCopyToast, setShowCopyToast] = (0, import_react9.useState)(false);
2018
2050
  const [copyToastMessage, setCopyToastMessage] = (0, import_react9.useState)("");
2019
2051
  const selectionBounds = (0, import_react9.useMemo)(() => {
2020
- if (!selectionStart || !selectionEnd) return null;
2052
+ if (!selectionStart || !selectionEnd)
2053
+ return null;
2021
2054
  return {
2022
2055
  minRow: Math.min(selectionStart.row, selectionEnd.row),
2023
2056
  maxRow: Math.max(selectionStart.row, selectionEnd.row),
@@ -2063,7 +2096,8 @@ function PivotSkeleton({
2063
2096
  return () => document.removeEventListener("mouseup", handleMouseUp);
2064
2097
  }, []);
2065
2098
  const sortedRowIndices = (0, import_react9.useMemo)(() => {
2066
- if (!pivotResult) return [];
2099
+ if (!pivotResult)
2100
+ return [];
2067
2101
  const indices = pivotResult.rowHeaders.map((_, i) => i);
2068
2102
  const headers = pivotResult.rowHeaders;
2069
2103
  const data = pivotResult.data;
@@ -2077,9 +2111,12 @@ function PivotSkeleton({
2077
2111
  const colIdx = sortTarget;
2078
2112
  const aVal = data[a]?.[colIdx]?.value ?? null;
2079
2113
  const bVal = data[b]?.[colIdx]?.value ?? null;
2080
- if (aVal === null && bVal === null) cmp = 0;
2081
- else if (aVal === null) cmp = 1;
2082
- else if (bVal === null) cmp = -1;
2114
+ if (aVal === null && bVal === null)
2115
+ cmp = 0;
2116
+ else if (aVal === null)
2117
+ cmp = 1;
2118
+ else if (bVal === null)
2119
+ cmp = -1;
2083
2120
  else cmp = aVal - bVal;
2084
2121
  }
2085
2122
  return sortDirection === "asc" ? cmp : -cmp;
@@ -2087,12 +2124,14 @@ function PivotSkeleton({
2087
2124
  return indices;
2088
2125
  }, [pivotResult, sortTarget, sortDirection]);
2089
2126
  const copySelectionToClipboard = (0, import_react9.useCallback)(() => {
2090
- if (!selectionBounds || !pivotResult) return;
2127
+ if (!selectionBounds || !pivotResult)
2128
+ return;
2091
2129
  const { minRow, maxRow, minCol, maxCol } = selectionBounds;
2092
2130
  const lines = [];
2093
2131
  for (let r = minRow; r <= maxRow; r++) {
2094
2132
  const sortedIdx = sortedRowIndices[r];
2095
- if (sortedIdx === void 0) continue;
2133
+ if (sortedIdx === void 0)
2134
+ continue;
2096
2135
  const rowValues = [];
2097
2136
  for (let c = minCol; c <= maxCol; c++) {
2098
2137
  const cell = pivotResult.data[sortedIdx]?.[c];
@@ -2112,7 +2151,8 @@ function PivotSkeleton({
2112
2151
  }, [selectionBounds, pivotResult, sortedRowIndices]);
2113
2152
  (0, import_react9.useEffect)(() => {
2114
2153
  const handleKeydown = (event) => {
2115
- if (!selectionBounds) return;
2154
+ if (!selectionBounds)
2155
+ return;
2116
2156
  if ((event.ctrlKey || event.metaKey) && event.key === "c") {
2117
2157
  event.preventDefault();
2118
2158
  copySelectionToClipboard();
@@ -2128,13 +2168,15 @@ function PivotSkeleton({
2128
2168
  return () => document.removeEventListener("keydown", handleKeydown);
2129
2169
  }, [selectionBounds, copySelectionToClipboard]);
2130
2170
  const selectionStats = (0, import_react9.useMemo)(() => {
2131
- if (!selectionBounds || !pivotResult) return null;
2171
+ if (!selectionBounds || !pivotResult)
2172
+ return null;
2132
2173
  const { minRow, maxRow, minCol, maxCol } = selectionBounds;
2133
2174
  const values = [];
2134
2175
  let count = 0;
2135
2176
  for (let r = minRow; r <= maxRow; r++) {
2136
2177
  const sortedIdx = sortedRowIndices[r];
2137
- if (sortedIdx === void 0) continue;
2178
+ if (sortedIdx === void 0)
2179
+ continue;
2138
2180
  for (let c = minCol; c <= maxCol; c++) {
2139
2181
  const cell = pivotResult.data[sortedIdx]?.[c];
2140
2182
  count++;
@@ -2143,7 +2185,8 @@ function PivotSkeleton({
2143
2185
  }
2144
2186
  }
2145
2187
  }
2146
- if (count <= 1) return null;
2188
+ if (count <= 1)
2189
+ return null;
2147
2190
  const sum = values.reduce((a, b) => a + b, 0);
2148
2191
  const avg = values.length > 0 ? sum / values.length : 0;
2149
2192
  return {
@@ -2154,8 +2197,10 @@ function PivotSkeleton({
2154
2197
  };
2155
2198
  }, [selectionBounds, pivotResult, sortedRowIndices]);
2156
2199
  const formatStatValue = (0, import_react9.useCallback)((val) => {
2157
- if (Math.abs(val) >= 1e6) return `${(val / 1e6).toFixed(2)}M`;
2158
- if (Math.abs(val) >= 1e3) return `${(val / 1e3).toFixed(2)}K`;
2200
+ if (Math.abs(val) >= 1e6)
2201
+ return `${(val / 1e6).toFixed(2)}M`;
2202
+ if (Math.abs(val) >= 1e3)
2203
+ return `${(val / 1e3).toFixed(2)}K`;
2159
2204
  return val.toFixed(2);
2160
2205
  }, []);
2161
2206
  const columnHeaderCells = (0, import_react9.useMemo)(() => {
@@ -2187,12 +2232,14 @@ function PivotSkeleton({
2187
2232
  }, [pivotResult, valueFields]);
2188
2233
  const hasActiveFilters = activeFilters && activeFilters.length > 0;
2189
2234
  const filterSummary = (0, import_react9.useMemo)(() => {
2190
- if (!activeFilters || activeFilters.length === 0) return "";
2235
+ if (!activeFilters || activeFilters.length === 0)
2236
+ return "";
2191
2237
  return activeFilters.map((f) => f.column).join(", ");
2192
2238
  }, [activeFilters]);
2193
2239
  const [showFilterTooltip, setShowFilterTooltip] = (0, import_react9.useState)(false);
2194
2240
  const filterTooltipDetails = (0, import_react9.useMemo)(() => {
2195
- if (!activeFilters || activeFilters.length === 0) return [];
2241
+ if (!activeFilters || activeFilters.length === 0)
2242
+ return [];
2196
2243
  return activeFilters.map((f) => {
2197
2244
  if (f.isRange && f.displayText) {
2198
2245
  return {
@@ -2234,10 +2281,13 @@ function PivotSkeleton({
2234
2281
  setDragOverArea(null);
2235
2282
  return;
2236
2283
  }
2237
- if (rowFields.includes(field)) onRemoveRowField(field);
2238
- if (columnFields.includes(field)) onRemoveColumnField(field);
2284
+ if (rowFields.includes(field))
2285
+ onRemoveRowField(field);
2286
+ if (columnFields.includes(field))
2287
+ onRemoveColumnField(field);
2239
2288
  const existingValue = valueFields.find((v) => v.field === field);
2240
- if (existingValue) onRemoveValueField(field, existingValue.aggregation);
2289
+ if (existingValue)
2290
+ onRemoveValueField(field, existingValue.aggregation);
2241
2291
  switch (area) {
2242
2292
  case "row":
2243
2293
  onAddRowField(field);
@@ -2320,6 +2370,14 @@ function PivotSkeleton({
2320
2370
  [reorderDropTarget]
2321
2371
  );
2322
2372
  const currentFontSize = fontSize;
2373
+ const rowHeaderWidth = 180;
2374
+ const rowHeaderColWidth = (0, import_react9.useMemo)(() => {
2375
+ const numCols = Math.max(rowFields.length, 1);
2376
+ return Math.max(rowHeaderWidth / numCols, 80);
2377
+ }, [rowFields.length]);
2378
+ const getRowHeaderLeftOffset = (0, import_react9.useCallback)((fieldIdx) => {
2379
+ return fieldIdx * rowHeaderColWidth;
2380
+ }, [rowHeaderColWidth]);
2323
2381
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2324
2382
  "div",
2325
2383
  {
@@ -2369,14 +2427,18 @@ function PivotSkeleton({
2369
2427
  }
2370
2428
  ),
2371
2429
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-filter-text", children: [
2372
- "Filtered: ",
2430
+ "Filtered:",
2431
+ " ",
2373
2432
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: filterSummary }),
2374
2433
  filteredRowCount !== void 0 && totalRowCount !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-filter-count", children: [
2375
2434
  "(",
2376
2435
  filteredRowCount.toLocaleString(),
2377
- " of ",
2436
+ " ",
2437
+ "of",
2438
+ " ",
2378
2439
  totalRowCount.toLocaleString(),
2379
- " rows)"
2440
+ " ",
2441
+ "rows)"
2380
2442
  ] })
2381
2443
  ] }),
2382
2444
  showFilterTooltip && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-filter-tooltip", children: [
@@ -2388,16 +2450,21 @@ function PivotSkeleton({
2388
2450
  filter.remaining > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-tooltip-more", children: [
2389
2451
  "+",
2390
2452
  filter.remaining,
2391
- " more"
2453
+ " ",
2454
+ "more"
2392
2455
  ] })
2393
2456
  ] }) })
2394
2457
  ] }, filter.column)),
2395
2458
  filteredRowCount !== void 0 && totalRowCount !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-tooltip-summary", children: [
2396
- "Showing ",
2459
+ "Showing",
2460
+ " ",
2397
2461
  filteredRowCount.toLocaleString(),
2398
- " of ",
2462
+ " ",
2463
+ "of",
2464
+ " ",
2399
2465
  totalRowCount.toLocaleString(),
2400
- " rows"
2466
+ " ",
2467
+ "rows"
2401
2468
  ] })
2402
2469
  ] })
2403
2470
  ]
@@ -2406,17 +2473,20 @@ function PivotSkeleton({
2406
2473
  isConfigured && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-config-summary", children: [
2407
2474
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-summary-badge vpg-rows", children: [
2408
2475
  rowFields.length,
2409
- " row",
2476
+ " ",
2477
+ "row",
2410
2478
  rowFields.length !== 1 ? "s" : ""
2411
2479
  ] }),
2412
2480
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-summary-badge vpg-cols", children: [
2413
2481
  columnFields.length,
2414
- " col",
2482
+ " ",
2483
+ "col",
2415
2484
  columnFields.length !== 1 ? "s" : ""
2416
2485
  ] }),
2417
2486
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-summary-badge vpg-vals", children: [
2418
2487
  valueFields.length,
2419
- " val",
2488
+ " ",
2489
+ "val",
2420
2490
  valueFields.length !== 1 ? "s" : ""
2421
2491
  ] })
2422
2492
  ] })
@@ -2587,31 +2657,39 @@ function PivotSkeleton({
2587
2657
  }
2588
2658
  ),
2589
2659
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "vpg-placeholder-text", children: valueFields.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2590
- "Add a ",
2660
+ "Add a",
2661
+ " ",
2591
2662
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "Values" }),
2592
- " field to see your pivot table"
2663
+ " ",
2664
+ "field to see your pivot table"
2593
2665
  ] }) : rowFields.length === 0 && columnFields.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2594
- "Add ",
2666
+ "Add",
2667
+ " ",
2595
2668
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "Row" }),
2596
- " or ",
2669
+ " ",
2670
+ "or",
2671
+ " ",
2597
2672
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "Column" }),
2598
- " fields to group your data"
2673
+ " ",
2674
+ "fields to group your data"
2599
2675
  ] }) : "Your pivot table will appear here" })
2600
2676
  ] }) }),
2601
2677
  isConfigured && pivotResult && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "vpg-table-container", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("table", { className: "vpg-pivot-table", children: [
2602
2678
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("thead", { children: columnHeaderCells.map((headerRow, levelIdx) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("tr", { className: "vpg-column-header-row", children: [
2603
- levelIdx === 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2679
+ levelIdx === 0 && (rowFields.length > 0 ? rowFields : ["Rows"]).map((field, fieldIdx) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2604
2680
  "th",
2605
2681
  {
2606
2682
  className: "vpg-row-header-label",
2607
2683
  rowSpan: columnHeaderCells.length,
2684
+ style: { width: `${rowHeaderColWidth}px`, minWidth: "80px", left: `${getRowHeaderLeftOffset(fieldIdx)}px` },
2608
2685
  onClick: () => toggleSort("row"),
2609
2686
  children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-header-content", children: [
2610
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: rowFields.join(" / ") || "Rows" }),
2611
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: `vpg-sort-indicator ${sortTarget === "row" ? "active" : ""}`, children: sortTarget === "row" ? sortDirection === "asc" ? "\u2191" : "\u2193" : "\u21C5" })
2687
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: field }),
2688
+ (fieldIdx === rowFields.length - 1 || rowFields.length === 0) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: `vpg-sort-indicator ${sortTarget === "row" ? "active" : ""}`, children: sortTarget === "row" ? sortDirection === "asc" ? "\u2191" : "\u2193" : "\u21C5" })
2612
2689
  ] })
2613
- }
2614
- ),
2690
+ },
2691
+ `row-header-${fieldIdx}`
2692
+ )),
2615
2693
  headerRow.map((cell, idx) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2616
2694
  "th",
2617
2695
  {
@@ -2629,7 +2707,15 @@ function PivotSkeleton({
2629
2707
  ] }, `header-${levelIdx}`)) }),
2630
2708
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("tbody", { children: [
2631
2709
  sortedRowIndices.map((sortedIdx) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("tr", { className: "vpg-data-row", children: [
2632
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { className: "vpg-row-header-cell", children: pivotResult.rowHeaders[sortedIdx].map((val, idx) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "vpg-row-value", children: val }, idx)) }),
2710
+ pivotResult.rowHeaders[sortedIdx].map((val, idx) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2711
+ "th",
2712
+ {
2713
+ className: "vpg-row-header-cell",
2714
+ style: { width: `${rowHeaderColWidth}px`, minWidth: "80px", left: `${getRowHeaderLeftOffset(idx)}px` },
2715
+ children: val
2716
+ },
2717
+ `row-${sortedIdx}-${idx}`
2718
+ )),
2633
2719
  pivotResult.data[sortedIdx].map((cell, colIdx) => {
2634
2720
  const displayRowIdx = sortedRowIndices.indexOf(sortedIdx);
2635
2721
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
@@ -2646,7 +2732,15 @@ function PivotSkeleton({
2646
2732
  pivotResult.rowTotals[sortedIdx] && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { className: "vpg-data-cell vpg-total-cell", children: pivotResult.rowTotals[sortedIdx].formattedValue })
2647
2733
  ] }, sortedIdx)),
2648
2734
  pivotResult.columnTotals.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("tr", { className: "vpg-totals-row", children: [
2649
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { className: "vpg-row-header-cell vpg-total-label", children: "Total" }),
2735
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2736
+ "th",
2737
+ {
2738
+ className: "vpg-row-header-cell vpg-total-label",
2739
+ colSpan: Math.max(rowFields.length, 1),
2740
+ style: { width: `${rowHeaderWidth}px` },
2741
+ children: "Total"
2742
+ }
2743
+ ),
2650
2744
  pivotResult.columnTotals.map((cell, colIdx) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { className: "vpg-data-cell vpg-total-cell", children: cell.formattedValue }, colIdx)),
2651
2745
  pivotResult.rowTotals.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { className: "vpg-data-cell vpg-grand-total-cell", children: pivotResult.grandTotal.formattedValue })
2652
2746
  ] })
@@ -2655,9 +2749,11 @@ function PivotSkeleton({
2655
2749
  isConfigured && pivotResult && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-skeleton-footer", children: [
2656
2750
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-footer-info", children: [
2657
2751
  pivotResult.rowHeaders.length,
2658
- " rows \xD7 ",
2752
+ " ",
2753
+ "rows \xD7",
2659
2754
  pivotResult.data[0]?.length || 0,
2660
- " columns"
2755
+ " ",
2756
+ "columns"
2661
2757
  ] }),
2662
2758
  selectionStats && selectionStats.count > 1 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-selection-stats", children: [
2663
2759
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-stat", children: [
@@ -2810,12 +2906,15 @@ function DataGrid({
2810
2906
  removeCalculatedField
2811
2907
  } = usePivotTable(filteredDataForPivot);
2812
2908
  const activeFilterInfo = (0, import_react10.useMemo)(() => {
2813
- if (activeFilters.length === 0) return null;
2909
+ if (activeFilters.length === 0)
2910
+ return null;
2814
2911
  return activeFilters.map((f) => {
2815
2912
  if (f.type === "range" && f.range) {
2816
2913
  const parts = [];
2817
- if (f.range.min !== null) parts.push(`\u2265 ${f.range.min}`);
2818
- if (f.range.max !== null) parts.push(`\u2264 ${f.range.max}`);
2914
+ if (f.range.min !== null)
2915
+ parts.push(`\u2265 ${f.range.min}`);
2916
+ if (f.range.max !== null)
2917
+ parts.push(`\u2264 ${f.range.max}`);
2819
2918
  return {
2820
2919
  column: f.column,
2821
2920
  valueCount: 1,
@@ -2840,7 +2939,8 @@ function DataGrid({
2840
2939
  return rows.filter((row) => {
2841
2940
  for (const col of columnKeys) {
2842
2941
  const value = row.original[col];
2843
- if (value === null || value === void 0) continue;
2942
+ if (value === null || value === void 0)
2943
+ continue;
2844
2944
  if (String(value).toLowerCase().includes(term)) {
2845
2945
  return true;
2846
2946
  }
@@ -2850,11 +2950,13 @@ function DataGrid({
2850
2950
  }, [rows, globalSearchTerm, enableSearch, columnKeys]);
2851
2951
  const totalSearchedRows = searchFilteredData.length;
2852
2952
  const totalPages = (0, import_react10.useMemo)(() => {
2853
- if (!enablePagination) return 1;
2953
+ if (!enablePagination)
2954
+ return 1;
2854
2955
  return Math.max(1, Math.ceil(totalSearchedRows / pageSize));
2855
2956
  }, [enablePagination, totalSearchedRows, pageSize]);
2856
2957
  const paginatedRows = (0, import_react10.useMemo)(() => {
2857
- if (!enablePagination) return searchFilteredData;
2958
+ if (!enablePagination)
2959
+ return searchFilteredData;
2858
2960
  const start = (currentPage - 1) * pageSize;
2859
2961
  const end = start + pageSize;
2860
2962
  return searchFilteredData.slice(start, end);
@@ -2863,7 +2965,8 @@ function DataGrid({
2863
2965
  setCurrentPage(1);
2864
2966
  }, [columnFilters, globalSearchTerm]);
2865
2967
  const selectionBounds = (0, import_react10.useMemo)(() => {
2866
- if (!selectionStart || !selectionEnd) return null;
2968
+ if (!selectionStart || !selectionEnd)
2969
+ return null;
2867
2970
  return {
2868
2971
  minRow: Math.min(selectionStart.row, selectionEnd.row),
2869
2972
  maxRow: Math.max(selectionStart.row, selectionEnd.row),
@@ -2872,16 +2975,19 @@ function DataGrid({
2872
2975
  };
2873
2976
  }, [selectionStart, selectionEnd]);
2874
2977
  const selectionStats = (0, import_react10.useMemo)(() => {
2875
- if (!selectionBounds) return null;
2978
+ if (!selectionBounds)
2979
+ return null;
2876
2980
  const { minRow, maxRow, minCol, maxCol } = selectionBounds;
2877
2981
  const values = [];
2878
2982
  let count = 0;
2879
2983
  for (let r = minRow; r <= maxRow; r++) {
2880
2984
  const row = rows[r];
2881
- if (!row) continue;
2985
+ if (!row)
2986
+ continue;
2882
2987
  for (let c = minCol; c <= maxCol; c++) {
2883
2988
  const colId = columnKeys[c];
2884
- if (!colId) continue;
2989
+ if (!colId)
2990
+ continue;
2885
2991
  const value = row.original[colId];
2886
2992
  count++;
2887
2993
  if (value !== null && value !== void 0 && value !== "") {
@@ -2892,19 +2998,23 @@ function DataGrid({
2892
2998
  }
2893
2999
  }
2894
3000
  }
2895
- if (values.length === 0) return { count, sum: null, avg: null, numericCount: 0 };
3001
+ if (values.length === 0)
3002
+ return { count, sum: null, avg: null, numericCount: 0 };
2896
3003
  const sum = values.reduce((a, b) => a + b, 0);
2897
3004
  const avg = sum / values.length;
2898
3005
  return { count, sum, avg, numericCount: values.length };
2899
3006
  }, [selectionBounds, rows, columnKeys]);
2900
3007
  (0, import_react10.useEffect)(() => {
2901
- if (typeof document === "undefined") return;
2902
- if (data.length === 0) return;
3008
+ if (typeof document === "undefined")
3009
+ return;
3010
+ if (data.length === 0)
3011
+ return;
2903
3012
  const widths = {};
2904
3013
  const sampleSize = Math.min(100, data.length);
2905
3014
  const canvas = document.createElement("canvas");
2906
3015
  const ctx = canvas.getContext("2d");
2907
- if (!ctx) return;
3016
+ if (!ctx)
3017
+ return;
2908
3018
  ctx.font = "13px system-ui, -apple-system, sans-serif";
2909
3019
  for (const key of columnKeys) {
2910
3020
  let maxWidth = ctx.measureText(key).width + 56;
@@ -2920,7 +3030,8 @@ function DataGrid({
2920
3030
  }, [data, columnKeys]);
2921
3031
  const startColumnResize = (0, import_react10.useCallback)(
2922
3032
  (columnId, event) => {
2923
- if (!enableColumnResize) return;
3033
+ if (!enableColumnResize)
3034
+ return;
2924
3035
  event.preventDefault();
2925
3036
  event.stopPropagation();
2926
3037
  setResizingColumnId(columnId);
@@ -2930,7 +3041,8 @@ function DataGrid({
2930
3041
  [enableColumnResize, columnWidths]
2931
3042
  );
2932
3043
  (0, import_react10.useEffect)(() => {
2933
- if (!resizingColumnId) return;
3044
+ if (!resizingColumnId)
3045
+ return;
2934
3046
  const handleResizeMove = (event) => {
2935
3047
  const diff = event.clientX - resizeStartX;
2936
3048
  const newWidth = Math.max(MIN_COL_WIDTH, Math.min(MAX_COL_WIDTH, resizeStartWidth + diff));
@@ -2951,7 +3063,8 @@ function DataGrid({
2951
3063
  }, [resizingColumnId, resizeStartX, resizeStartWidth]);
2952
3064
  const startVerticalResize = (0, import_react10.useCallback)(
2953
3065
  (event) => {
2954
- if (!enableVerticalResize) return;
3066
+ if (!enableVerticalResize)
3067
+ return;
2955
3068
  event.preventDefault();
2956
3069
  setIsResizingVertically(true);
2957
3070
  setVerticalResizeStartY(event.clientY);
@@ -2960,7 +3073,8 @@ function DataGrid({
2960
3073
  [enableVerticalResize, gridHeight]
2961
3074
  );
2962
3075
  (0, import_react10.useEffect)(() => {
2963
- if (!isResizingVertically) return;
3076
+ if (!isResizingVertically)
3077
+ return;
2964
3078
  const handleVerticalResizeMove = (event) => {
2965
3079
  const diff = event.clientY - verticalResizeStartY;
2966
3080
  const newHeight = Math.max(minHeight, Math.min(maxHeight, verticalResizeStartHeight + diff));
@@ -2978,7 +3092,8 @@ function DataGrid({
2978
3092
  }, [isResizingVertically, verticalResizeStartY, verticalResizeStartHeight, minHeight, maxHeight]);
2979
3093
  const handleExport = (0, import_react10.useCallback)(() => {
2980
3094
  if (viewMode === "pivot") {
2981
- if (!pivotResult) return;
3095
+ if (!pivotResult)
3096
+ return;
2982
3097
  const pivotFilename = exportFilename.replace(".csv", "-pivot.csv");
2983
3098
  exportPivotToCSV(
2984
3099
  {
@@ -3022,7 +3137,8 @@ function DataGrid({
3022
3137
  onExport
3023
3138
  ]);
3024
3139
  const copySelectionToClipboard = (0, import_react10.useCallback)(() => {
3025
- if (!selectionBounds || !enableClipboard) return;
3140
+ if (!selectionBounds || !enableClipboard)
3141
+ return;
3026
3142
  const text = formatSelectionForClipboard(
3027
3143
  rows.map((r) => r.original),
3028
3144
  columnKeys,
@@ -3179,7 +3295,8 @@ function DataGrid({
3179
3295
  [selectionBounds, selectedCell]
3180
3296
  );
3181
3297
  const formatStatValue = (value) => {
3182
- if (value === null) return "-";
3298
+ if (value === null)
3299
+ return "-";
3183
3300
  if (Math.abs(value) >= 1e3) {
3184
3301
  return value.toLocaleString("en-US", { maximumFractionDigits: 2 });
3185
3302
  }
@@ -3190,12 +3307,15 @@ function DataGrid({
3190
3307
  return !noFormatPatterns.test(columnId);
3191
3308
  };
3192
3309
  const formatCellValueDisplay = (value, columnId) => {
3193
- if (value === null || value === void 0) return "";
3194
- if (value === "") return "";
3310
+ if (value === null || value === void 0)
3311
+ return "";
3312
+ if (value === "")
3313
+ return "";
3195
3314
  const stats = getColumnStats(columnId);
3196
3315
  if (stats.type === "number") {
3197
3316
  const num = typeof value === "number" ? value : Number.parseFloat(String(value));
3198
- if (Number.isNaN(num)) return String(value);
3317
+ if (Number.isNaN(num))
3318
+ return String(value);
3199
3319
  if (shouldFormatNumber(columnId) && Math.abs(num) >= 1e3) {
3200
3320
  return num.toLocaleString("en-US", { maximumFractionDigits: 2 });
3201
3321
  }
@@ -3357,13 +3477,15 @@ function DataGrid({
3357
3477
  ) }),
3358
3478
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
3359
3479
  activeFilterCount,
3360
- " filter",
3480
+ " ",
3481
+ "filter",
3361
3482
  activeFilterCount > 1 ? "s" : ""
3362
3483
  ] })
3363
3484
  ] }),
3364
3485
  globalSearchTerm && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "vpg-search-info", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
3365
3486
  totalSearchedRows,
3366
- " match",
3487
+ " ",
3488
+ "match",
3367
3489
  totalSearchedRows !== 1 ? "es" : ""
3368
3490
  ] }) })
3369
3491
  ] }),
@@ -3384,7 +3506,8 @@ function DataGrid({
3384
3506
  }
3385
3507
  ) }),
3386
3508
  showPivotConfig ? "Hide" : "Show",
3387
- " Config"
3509
+ " ",
3510
+ "Config"
3388
3511
  ]
3389
3512
  }
3390
3513
  ),
@@ -3506,7 +3629,7 @@ function DataGrid({
3506
3629
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "vpg-clear-link", onClick: clearAllFilters, children: "Clear all filters" })
3507
3630
  ] }),
3508
3631
  !loading && filteredRowCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "vpg-table-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("table", { className: "vpg-table", style: { minWidth: `${totalTableWidth}px` }, children: [
3509
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("tr", { children: columnKeys.map((colId, colIndex) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3632
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("tr", { children: columnKeys.map((colId) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3510
3633
  "th",
3511
3634
  {
3512
3635
  className: `vpg-header-cell ${hasActiveFilter(colId) ? "vpg-has-filter" : ""} ${getSortDirection(colId) !== null ? "vpg-is-sorted" : ""} ${activeFilterColumn === colId ? "vpg-is-active" : ""}`,
@@ -3635,7 +3758,7 @@ function DataGrid({
3635
3758
  onShowColumnTotalsChange: setPivotShowColumnTotals,
3636
3759
  onClearConfig: clearPivotConfig,
3637
3760
  onAutoSuggest: autoSuggestConfig,
3638
- onDragStart: (field, e) => setDraggingField(field),
3761
+ onDragStart: (field, _e) => setDraggingField(field),
3639
3762
  onDragEnd: () => setDraggingField(null),
3640
3763
  onUpdateAggregation: updateValueFieldAggregation,
3641
3764
  onAddRowField: addRowField,
@@ -3687,11 +3810,13 @@ function DataGrid({
3687
3810
  totalSearchedRows !== totalRowCount && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "vpg-filtered-note", children: [
3688
3811
  "(",
3689
3812
  totalRowCount.toLocaleString(),
3690
- " total)"
3813
+ " ",
3814
+ "total)"
3691
3815
  ] })
3692
3816
  ] }) : filteredRowCount === totalRowCount && totalSearchedRows === totalRowCount ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
3693
3817
  totalRowCount.toLocaleString(),
3694
- " records"
3818
+ " ",
3819
+ "records"
3695
3820
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
3696
3821
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "vpg-filtered-count", children: totalSearchedRows.toLocaleString() }),
3697
3822
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "vpg-separator", children: "of" }),
@@ -3702,7 +3827,8 @@ function DataGrid({
3702
3827
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "vpg-separator", children: "\u2022" }),
3703
3828
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
3704
3829
  totalRowCount.toLocaleString(),
3705
- " source records"
3830
+ " ",
3831
+ "source records"
3706
3832
  ] })
3707
3833
  ] }) }),
3708
3834
  enablePagination && viewMode === "grid" && totalPages > 1 && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "vpg-pagination", children: [
@@ -3741,9 +3867,12 @@ function DataGrid({
3741
3867
  }
3742
3868
  ),
3743
3869
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "vpg-page-info", children: [
3744
- "Page ",
3870
+ "Page",
3871
+ " ",
3745
3872
  currentPage,
3746
- " of ",
3873
+ " ",
3874
+ "of",
3875
+ " ",
3747
3876
  totalPages
3748
3877
  ] }),
3749
3878
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(