@smallwebco/tinypivot-react 1.0.49 → 1.0.51

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
  }, []);
@@ -644,920 +1111,484 @@ function usePagination(data, options = {}) {
644
1111
  prevPage,
645
1112
  firstPage,
646
1113
  lastPage,
647
- setPageSize: updatePageSize
648
- };
649
- }
650
- 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)(() => {
654
- if (!searchTerm.trim()) {
655
- return data;
656
- }
657
- const term = caseSensitive ? searchTerm.trim() : searchTerm.trim().toLowerCase();
658
- return data.filter((row) => {
659
- for (const col of columns) {
660
- 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
1114
+ setPageSize: updatePageSize
1115
+ };
1116
+ }
1117
+ function useGlobalSearch(data, columns) {
1118
+ const [searchTerm, setSearchTerm] = (0, import_react5.useState)("");
1119
+ const [caseSensitive, setCaseSensitive] = (0, import_react5.useState)(false);
1120
+ const filteredData = (0, import_react5.useMemo)(() => {
1121
+ if (!searchTerm.trim()) {
1122
+ return data;
1123
+ }
1124
+ const term = caseSensitive ? searchTerm.trim() : searchTerm.trim().toLowerCase();
1125
+ return data.filter((row) => {
1126
+ for (const col of columns) {
1127
+ const value = row[col];
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);
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
+ }
1393
1420
  } else {
1394
- setName("");
1395
- setFormula("");
1396
- setFormatAs("number");
1397
- setDecimals(2);
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);
@@ -2377,14 +2427,18 @@ function PivotSkeleton({
2377
2427
  }
2378
2428
  ),
2379
2429
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-filter-text", children: [
2380
- "Filtered: ",
2430
+ "Filtered:",
2431
+ " ",
2381
2432
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: filterSummary }),
2382
2433
  filteredRowCount !== void 0 && totalRowCount !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-filter-count", children: [
2383
2434
  "(",
2384
2435
  filteredRowCount.toLocaleString(),
2385
- " of ",
2436
+ " ",
2437
+ "of",
2438
+ " ",
2386
2439
  totalRowCount.toLocaleString(),
2387
- " rows)"
2440
+ " ",
2441
+ "rows)"
2388
2442
  ] })
2389
2443
  ] }),
2390
2444
  showFilterTooltip && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-filter-tooltip", children: [
@@ -2396,16 +2450,21 @@ function PivotSkeleton({
2396
2450
  filter.remaining > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-tooltip-more", children: [
2397
2451
  "+",
2398
2452
  filter.remaining,
2399
- " more"
2453
+ " ",
2454
+ "more"
2400
2455
  ] })
2401
2456
  ] }) })
2402
2457
  ] }, filter.column)),
2403
2458
  filteredRowCount !== void 0 && totalRowCount !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-tooltip-summary", children: [
2404
- "Showing ",
2459
+ "Showing",
2460
+ " ",
2405
2461
  filteredRowCount.toLocaleString(),
2406
- " of ",
2462
+ " ",
2463
+ "of",
2464
+ " ",
2407
2465
  totalRowCount.toLocaleString(),
2408
- " rows"
2466
+ " ",
2467
+ "rows"
2409
2468
  ] })
2410
2469
  ] })
2411
2470
  ]
@@ -2414,17 +2473,20 @@ function PivotSkeleton({
2414
2473
  isConfigured && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-config-summary", children: [
2415
2474
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-summary-badge vpg-rows", children: [
2416
2475
  rowFields.length,
2417
- " row",
2476
+ " ",
2477
+ "row",
2418
2478
  rowFields.length !== 1 ? "s" : ""
2419
2479
  ] }),
2420
2480
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-summary-badge vpg-cols", children: [
2421
2481
  columnFields.length,
2422
- " col",
2482
+ " ",
2483
+ "col",
2423
2484
  columnFields.length !== 1 ? "s" : ""
2424
2485
  ] }),
2425
2486
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-summary-badge vpg-vals", children: [
2426
2487
  valueFields.length,
2427
- " val",
2488
+ " ",
2489
+ "val",
2428
2490
  valueFields.length !== 1 ? "s" : ""
2429
2491
  ] })
2430
2492
  ] })
@@ -2595,15 +2657,21 @@ function PivotSkeleton({
2595
2657
  }
2596
2658
  ),
2597
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: [
2598
- "Add a ",
2660
+ "Add a",
2661
+ " ",
2599
2662
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "Values" }),
2600
- " field to see your pivot table"
2663
+ " ",
2664
+ "field to see your pivot table"
2601
2665
  ] }) : rowFields.length === 0 && columnFields.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2602
- "Add ",
2666
+ "Add",
2667
+ " ",
2603
2668
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "Row" }),
2604
- " or ",
2669
+ " ",
2670
+ "or",
2671
+ " ",
2605
2672
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "Column" }),
2606
- " fields to group your data"
2673
+ " ",
2674
+ "fields to group your data"
2607
2675
  ] }) : "Your pivot table will appear here" })
2608
2676
  ] }) }),
2609
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: [
@@ -2681,9 +2749,11 @@ function PivotSkeleton({
2681
2749
  isConfigured && pivotResult && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-skeleton-footer", children: [
2682
2750
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-footer-info", children: [
2683
2751
  pivotResult.rowHeaders.length,
2684
- " rows \xD7 ",
2752
+ " ",
2753
+ "rows \xD7",
2685
2754
  pivotResult.data[0]?.length || 0,
2686
- " columns"
2755
+ " ",
2756
+ "columns"
2687
2757
  ] }),
2688
2758
  selectionStats && selectionStats.count > 1 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-selection-stats", children: [
2689
2759
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-stat", children: [
@@ -2836,12 +2906,15 @@ function DataGrid({
2836
2906
  removeCalculatedField
2837
2907
  } = usePivotTable(filteredDataForPivot);
2838
2908
  const activeFilterInfo = (0, import_react10.useMemo)(() => {
2839
- if (activeFilters.length === 0) return null;
2909
+ if (activeFilters.length === 0)
2910
+ return null;
2840
2911
  return activeFilters.map((f) => {
2841
2912
  if (f.type === "range" && f.range) {
2842
2913
  const parts = [];
2843
- if (f.range.min !== null) parts.push(`\u2265 ${f.range.min}`);
2844
- 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}`);
2845
2918
  return {
2846
2919
  column: f.column,
2847
2920
  valueCount: 1,
@@ -2866,7 +2939,8 @@ function DataGrid({
2866
2939
  return rows.filter((row) => {
2867
2940
  for (const col of columnKeys) {
2868
2941
  const value = row.original[col];
2869
- if (value === null || value === void 0) continue;
2942
+ if (value === null || value === void 0)
2943
+ continue;
2870
2944
  if (String(value).toLowerCase().includes(term)) {
2871
2945
  return true;
2872
2946
  }
@@ -2876,11 +2950,13 @@ function DataGrid({
2876
2950
  }, [rows, globalSearchTerm, enableSearch, columnKeys]);
2877
2951
  const totalSearchedRows = searchFilteredData.length;
2878
2952
  const totalPages = (0, import_react10.useMemo)(() => {
2879
- if (!enablePagination) return 1;
2953
+ if (!enablePagination)
2954
+ return 1;
2880
2955
  return Math.max(1, Math.ceil(totalSearchedRows / pageSize));
2881
2956
  }, [enablePagination, totalSearchedRows, pageSize]);
2882
2957
  const paginatedRows = (0, import_react10.useMemo)(() => {
2883
- if (!enablePagination) return searchFilteredData;
2958
+ if (!enablePagination)
2959
+ return searchFilteredData;
2884
2960
  const start = (currentPage - 1) * pageSize;
2885
2961
  const end = start + pageSize;
2886
2962
  return searchFilteredData.slice(start, end);
@@ -2889,7 +2965,8 @@ function DataGrid({
2889
2965
  setCurrentPage(1);
2890
2966
  }, [columnFilters, globalSearchTerm]);
2891
2967
  const selectionBounds = (0, import_react10.useMemo)(() => {
2892
- if (!selectionStart || !selectionEnd) return null;
2968
+ if (!selectionStart || !selectionEnd)
2969
+ return null;
2893
2970
  return {
2894
2971
  minRow: Math.min(selectionStart.row, selectionEnd.row),
2895
2972
  maxRow: Math.max(selectionStart.row, selectionEnd.row),
@@ -2898,16 +2975,19 @@ function DataGrid({
2898
2975
  };
2899
2976
  }, [selectionStart, selectionEnd]);
2900
2977
  const selectionStats = (0, import_react10.useMemo)(() => {
2901
- if (!selectionBounds) return null;
2978
+ if (!selectionBounds)
2979
+ return null;
2902
2980
  const { minRow, maxRow, minCol, maxCol } = selectionBounds;
2903
2981
  const values = [];
2904
2982
  let count = 0;
2905
2983
  for (let r = minRow; r <= maxRow; r++) {
2906
2984
  const row = rows[r];
2907
- if (!row) continue;
2985
+ if (!row)
2986
+ continue;
2908
2987
  for (let c = minCol; c <= maxCol; c++) {
2909
2988
  const colId = columnKeys[c];
2910
- if (!colId) continue;
2989
+ if (!colId)
2990
+ continue;
2911
2991
  const value = row.original[colId];
2912
2992
  count++;
2913
2993
  if (value !== null && value !== void 0 && value !== "") {
@@ -2918,19 +2998,23 @@ function DataGrid({
2918
2998
  }
2919
2999
  }
2920
3000
  }
2921
- 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 };
2922
3003
  const sum = values.reduce((a, b) => a + b, 0);
2923
3004
  const avg = sum / values.length;
2924
3005
  return { count, sum, avg, numericCount: values.length };
2925
3006
  }, [selectionBounds, rows, columnKeys]);
2926
3007
  (0, import_react10.useEffect)(() => {
2927
- if (typeof document === "undefined") return;
2928
- if (data.length === 0) return;
3008
+ if (typeof document === "undefined")
3009
+ return;
3010
+ if (data.length === 0)
3011
+ return;
2929
3012
  const widths = {};
2930
3013
  const sampleSize = Math.min(100, data.length);
2931
3014
  const canvas = document.createElement("canvas");
2932
3015
  const ctx = canvas.getContext("2d");
2933
- if (!ctx) return;
3016
+ if (!ctx)
3017
+ return;
2934
3018
  ctx.font = "13px system-ui, -apple-system, sans-serif";
2935
3019
  for (const key of columnKeys) {
2936
3020
  let maxWidth = ctx.measureText(key).width + 56;
@@ -2946,7 +3030,8 @@ function DataGrid({
2946
3030
  }, [data, columnKeys]);
2947
3031
  const startColumnResize = (0, import_react10.useCallback)(
2948
3032
  (columnId, event) => {
2949
- if (!enableColumnResize) return;
3033
+ if (!enableColumnResize)
3034
+ return;
2950
3035
  event.preventDefault();
2951
3036
  event.stopPropagation();
2952
3037
  setResizingColumnId(columnId);
@@ -2956,7 +3041,8 @@ function DataGrid({
2956
3041
  [enableColumnResize, columnWidths]
2957
3042
  );
2958
3043
  (0, import_react10.useEffect)(() => {
2959
- if (!resizingColumnId) return;
3044
+ if (!resizingColumnId)
3045
+ return;
2960
3046
  const handleResizeMove = (event) => {
2961
3047
  const diff = event.clientX - resizeStartX;
2962
3048
  const newWidth = Math.max(MIN_COL_WIDTH, Math.min(MAX_COL_WIDTH, resizeStartWidth + diff));
@@ -2977,7 +3063,8 @@ function DataGrid({
2977
3063
  }, [resizingColumnId, resizeStartX, resizeStartWidth]);
2978
3064
  const startVerticalResize = (0, import_react10.useCallback)(
2979
3065
  (event) => {
2980
- if (!enableVerticalResize) return;
3066
+ if (!enableVerticalResize)
3067
+ return;
2981
3068
  event.preventDefault();
2982
3069
  setIsResizingVertically(true);
2983
3070
  setVerticalResizeStartY(event.clientY);
@@ -2986,7 +3073,8 @@ function DataGrid({
2986
3073
  [enableVerticalResize, gridHeight]
2987
3074
  );
2988
3075
  (0, import_react10.useEffect)(() => {
2989
- if (!isResizingVertically) return;
3076
+ if (!isResizingVertically)
3077
+ return;
2990
3078
  const handleVerticalResizeMove = (event) => {
2991
3079
  const diff = event.clientY - verticalResizeStartY;
2992
3080
  const newHeight = Math.max(minHeight, Math.min(maxHeight, verticalResizeStartHeight + diff));
@@ -3004,7 +3092,8 @@ function DataGrid({
3004
3092
  }, [isResizingVertically, verticalResizeStartY, verticalResizeStartHeight, minHeight, maxHeight]);
3005
3093
  const handleExport = (0, import_react10.useCallback)(() => {
3006
3094
  if (viewMode === "pivot") {
3007
- if (!pivotResult) return;
3095
+ if (!pivotResult)
3096
+ return;
3008
3097
  const pivotFilename = exportFilename.replace(".csv", "-pivot.csv");
3009
3098
  exportPivotToCSV(
3010
3099
  {
@@ -3048,7 +3137,8 @@ function DataGrid({
3048
3137
  onExport
3049
3138
  ]);
3050
3139
  const copySelectionToClipboard = (0, import_react10.useCallback)(() => {
3051
- if (!selectionBounds || !enableClipboard) return;
3140
+ if (!selectionBounds || !enableClipboard)
3141
+ return;
3052
3142
  const text = formatSelectionForClipboard(
3053
3143
  rows.map((r) => r.original),
3054
3144
  columnKeys,
@@ -3205,7 +3295,8 @@ function DataGrid({
3205
3295
  [selectionBounds, selectedCell]
3206
3296
  );
3207
3297
  const formatStatValue = (value) => {
3208
- if (value === null) return "-";
3298
+ if (value === null)
3299
+ return "-";
3209
3300
  if (Math.abs(value) >= 1e3) {
3210
3301
  return value.toLocaleString("en-US", { maximumFractionDigits: 2 });
3211
3302
  }
@@ -3216,12 +3307,15 @@ function DataGrid({
3216
3307
  return !noFormatPatterns.test(columnId);
3217
3308
  };
3218
3309
  const formatCellValueDisplay = (value, columnId) => {
3219
- if (value === null || value === void 0) return "";
3220
- if (value === "") return "";
3310
+ if (value === null || value === void 0)
3311
+ return "";
3312
+ if (value === "")
3313
+ return "";
3221
3314
  const stats = getColumnStats(columnId);
3222
3315
  if (stats.type === "number") {
3223
3316
  const num = typeof value === "number" ? value : Number.parseFloat(String(value));
3224
- if (Number.isNaN(num)) return String(value);
3317
+ if (Number.isNaN(num))
3318
+ return String(value);
3225
3319
  if (shouldFormatNumber(columnId) && Math.abs(num) >= 1e3) {
3226
3320
  return num.toLocaleString("en-US", { maximumFractionDigits: 2 });
3227
3321
  }
@@ -3383,13 +3477,15 @@ function DataGrid({
3383
3477
  ) }),
3384
3478
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
3385
3479
  activeFilterCount,
3386
- " filter",
3480
+ " ",
3481
+ "filter",
3387
3482
  activeFilterCount > 1 ? "s" : ""
3388
3483
  ] })
3389
3484
  ] }),
3390
3485
  globalSearchTerm && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "vpg-search-info", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
3391
3486
  totalSearchedRows,
3392
- " match",
3487
+ " ",
3488
+ "match",
3393
3489
  totalSearchedRows !== 1 ? "es" : ""
3394
3490
  ] }) })
3395
3491
  ] }),
@@ -3410,7 +3506,8 @@ function DataGrid({
3410
3506
  }
3411
3507
  ) }),
3412
3508
  showPivotConfig ? "Hide" : "Show",
3413
- " Config"
3509
+ " ",
3510
+ "Config"
3414
3511
  ]
3415
3512
  }
3416
3513
  ),
@@ -3532,7 +3629,7 @@ function DataGrid({
3532
3629
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "vpg-clear-link", onClick: clearAllFilters, children: "Clear all filters" })
3533
3630
  ] }),
3534
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: [
3535
- /* @__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)(
3536
3633
  "th",
3537
3634
  {
3538
3635
  className: `vpg-header-cell ${hasActiveFilter(colId) ? "vpg-has-filter" : ""} ${getSortDirection(colId) !== null ? "vpg-is-sorted" : ""} ${activeFilterColumn === colId ? "vpg-is-active" : ""}`,
@@ -3661,7 +3758,7 @@ function DataGrid({
3661
3758
  onShowColumnTotalsChange: setPivotShowColumnTotals,
3662
3759
  onClearConfig: clearPivotConfig,
3663
3760
  onAutoSuggest: autoSuggestConfig,
3664
- onDragStart: (field, e) => setDraggingField(field),
3761
+ onDragStart: (field, _e) => setDraggingField(field),
3665
3762
  onDragEnd: () => setDraggingField(null),
3666
3763
  onUpdateAggregation: updateValueFieldAggregation,
3667
3764
  onAddRowField: addRowField,
@@ -3713,11 +3810,13 @@ function DataGrid({
3713
3810
  totalSearchedRows !== totalRowCount && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "vpg-filtered-note", children: [
3714
3811
  "(",
3715
3812
  totalRowCount.toLocaleString(),
3716
- " total)"
3813
+ " ",
3814
+ "total)"
3717
3815
  ] })
3718
3816
  ] }) : filteredRowCount === totalRowCount && totalSearchedRows === totalRowCount ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
3719
3817
  totalRowCount.toLocaleString(),
3720
- " records"
3818
+ " ",
3819
+ "records"
3721
3820
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
3722
3821
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "vpg-filtered-count", children: totalSearchedRows.toLocaleString() }),
3723
3822
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "vpg-separator", children: "of" }),
@@ -3728,7 +3827,8 @@ function DataGrid({
3728
3827
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "vpg-separator", children: "\u2022" }),
3729
3828
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
3730
3829
  totalRowCount.toLocaleString(),
3731
- " source records"
3830
+ " ",
3831
+ "source records"
3732
3832
  ] })
3733
3833
  ] }) }),
3734
3834
  enablePagination && viewMode === "grid" && totalPages > 1 && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "vpg-pagination", children: [
@@ -3767,9 +3867,12 @@ function DataGrid({
3767
3867
  }
3768
3868
  ),
3769
3869
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "vpg-page-info", children: [
3770
- "Page ",
3870
+ "Page",
3871
+ " ",
3771
3872
  currentPage,
3772
- " of ",
3873
+ " ",
3874
+ "of",
3875
+ " ",
3773
3876
  totalPages
3774
3877
  ] }),
3775
3878
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(