@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.js CHANGED
@@ -1,1296 +1,701 @@
1
- // src/components/DataGrid.tsx
2
- import { useState as useState10, useMemo as useMemo10, useCallback as useCallback10, useEffect as useEffect7, useRef as useRef2 } from "react";
3
- import { createPortal as createPortal2 } from "react-dom";
4
-
5
- // src/hooks/useExcelGrid.ts
6
- import { useState, useMemo, useCallback, useEffect } from "react";
7
- import {
8
- useReactTable,
9
- getCoreRowModel,
10
- getFilteredRowModel,
11
- getSortedRowModel
12
- } from "@tanstack/react-table";
13
- import { getColumnUniqueValues, formatCellValue, isNumericRange } from "@smallwebco/tinypivot-core";
14
- var multiSelectFilter = (row, columnId, filterValue) => {
15
- if (!filterValue) return true;
16
- if (isNumericRange(filterValue)) {
17
- const cellValue = row.getValue(columnId);
18
- if (cellValue === null || cellValue === void 0 || cellValue === "") {
19
- return false;
20
- }
21
- const num = typeof cellValue === "number" ? cellValue : Number.parseFloat(String(cellValue));
22
- if (Number.isNaN(num)) return false;
23
- const { min, max } = filterValue;
24
- if (min !== null && num < min) return false;
25
- if (max !== null && num > max) return false;
26
- return true;
27
- }
28
- if (Array.isArray(filterValue) && filterValue.length > 0) {
29
- const cellValue = row.getValue(columnId);
30
- const cellString = cellValue === null || cellValue === void 0 || cellValue === "" ? "(blank)" : String(cellValue);
31
- return filterValue.includes(cellString);
32
- }
33
- return true;
34
- };
35
- function useExcelGrid(options) {
36
- const { data, enableSorting = true, enableFiltering = true } = options;
37
- const [sorting, setSorting] = useState([]);
38
- const [columnFilters, setColumnFilters] = useState([]);
39
- const [columnVisibility, setColumnVisibility] = useState({});
40
- const [globalFilter, setGlobalFilter] = useState("");
41
- const [columnStatsCache, setColumnStatsCache] = useState({});
42
- const dataSignature = useMemo(
43
- () => `${Date.now()}-${Math.random().toString(36).slice(2)}`,
44
- [data]
45
- );
46
- const columnKeys = useMemo(() => {
47
- if (data.length === 0) return [];
48
- return Object.keys(data[0]);
49
- }, [data]);
50
- const getColumnStats = useCallback(
51
- (columnKey) => {
52
- const cacheKey = `${columnKey}-${dataSignature}`;
53
- if (!columnStatsCache[cacheKey]) {
54
- const stats = getColumnUniqueValues(data, columnKey);
55
- setColumnStatsCache((prev) => ({ ...prev, [cacheKey]: stats }));
56
- return stats;
57
- }
58
- return columnStatsCache[cacheKey];
59
- },
60
- [data, columnStatsCache, dataSignature]
61
- );
62
- const clearStatsCache = useCallback(() => {
63
- setColumnStatsCache({});
64
- }, []);
1
+ // src/components/CalculatedFieldModal.tsx
2
+ import { validateSimpleFormula } from "@smallwebco/tinypivot-core";
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ function CalculatedFieldModal({
7
+ show,
8
+ availableFields,
9
+ existingField,
10
+ onClose,
11
+ onSave
12
+ }) {
13
+ const [name, setName] = useState("");
14
+ const [formula, setFormula] = useState("");
15
+ const [formatAs, setFormatAs] = useState("number");
16
+ const [decimals, setDecimals] = useState(2);
17
+ const [error, setError] = useState(null);
65
18
  useEffect(() => {
66
- clearStatsCache();
67
- }, [dataSignature, clearStatsCache]);
68
- const columnDefs = useMemo(() => {
69
- return columnKeys.map((key) => {
70
- const stats = getColumnStats(key);
71
- return {
72
- id: key,
73
- accessorKey: key,
74
- header: key,
75
- cell: (info) => formatCellValue(info.getValue(), stats.type),
76
- filterFn: multiSelectFilter,
77
- meta: {
78
- type: stats.type,
79
- uniqueCount: stats.uniqueValues.length
80
- }
81
- };
82
- });
83
- }, [columnKeys, getColumnStats]);
84
- const table = useReactTable({
85
- data,
86
- columns: columnDefs,
87
- state: {
88
- sorting,
89
- columnFilters,
90
- columnVisibility,
91
- globalFilter
92
- },
93
- onSortingChange: setSorting,
94
- onColumnFiltersChange: setColumnFilters,
95
- onColumnVisibilityChange: setColumnVisibility,
96
- onGlobalFilterChange: setGlobalFilter,
97
- getCoreRowModel: getCoreRowModel(),
98
- getSortedRowModel: enableSorting ? getSortedRowModel() : void 0,
99
- getFilteredRowModel: enableFiltering ? getFilteredRowModel() : void 0,
100
- filterFns: {
101
- multiSelect: multiSelectFilter
102
- },
103
- enableSorting,
104
- enableFilters: enableFiltering
105
- });
106
- const filteredRowCount = table.getFilteredRowModel().rows.length;
107
- const totalRowCount = data.length;
108
- const activeFilters = useMemo(() => {
109
- return columnFilters.map((f) => {
110
- const filterValue = f.value;
111
- if (filterValue && isNumericRange(filterValue)) {
112
- return {
113
- column: f.id,
114
- type: "range",
115
- range: filterValue,
116
- values: []
117
- };
118
- }
119
- return {
120
- column: f.id,
121
- type: "values",
122
- values: Array.isArray(filterValue) ? filterValue : [],
123
- range: null
124
- };
125
- });
126
- }, [columnFilters]);
127
- const hasActiveFilter = useCallback(
128
- (columnId) => {
129
- const column = table.getColumn(columnId);
130
- if (!column) return false;
131
- const filterValue = column.getFilterValue();
132
- if (!filterValue) return false;
133
- if (isNumericRange(filterValue)) {
134
- return filterValue.min !== null || filterValue.max !== null;
135
- }
136
- return Array.isArray(filterValue) && filterValue.length > 0;
137
- },
138
- [table]
139
- );
140
- const setColumnFilter = useCallback(
141
- (columnId, values) => {
142
- const column = table.getColumn(columnId);
143
- if (column) {
144
- column.setFilterValue(values.length === 0 ? void 0 : values);
145
- }
146
- },
147
- [table]
148
- );
149
- const setNumericRangeFilter = useCallback(
150
- (columnId, range) => {
151
- const column = table.getColumn(columnId);
152
- if (column) {
153
- if (!range || range.min === null && range.max === null) {
154
- column.setFilterValue(void 0);
155
- } else {
156
- column.setFilterValue(range);
157
- }
158
- }
159
- },
160
- [table]
161
- );
162
- const getNumericRangeFilter = useCallback(
163
- (columnId) => {
164
- const column = table.getColumn(columnId);
165
- if (!column) return null;
166
- const filterValue = column.getFilterValue();
167
- if (filterValue && isNumericRange(filterValue)) {
168
- return filterValue;
19
+ if (show) {
20
+ if (existingField) {
21
+ setName(existingField.name);
22
+ setFormula(existingField.formula);
23
+ setFormatAs(existingField.formatAs || "number");
24
+ setDecimals(existingField.decimals ?? 2);
25
+ } else {
26
+ setName("");
27
+ setFormula("");
28
+ setFormatAs("number");
29
+ setDecimals(2);
169
30
  }
31
+ setError(null);
32
+ }
33
+ }, [show, existingField]);
34
+ const validationError = useMemo(() => {
35
+ if (!formula.trim())
170
36
  return null;
171
- },
172
- [table]
173
- );
174
- const clearAllFilters = useCallback(() => {
175
- table.resetColumnFilters();
176
- setGlobalFilter("");
177
- setColumnFilters([]);
178
- }, [table]);
179
- const getColumnFilterValues = useCallback(
180
- (columnId) => {
181
- const column = table.getColumn(columnId);
182
- if (!column) return [];
183
- const filterValue = column.getFilterValue();
184
- return Array.isArray(filterValue) ? filterValue : [];
185
- },
186
- [table]
187
- );
188
- const toggleSort = useCallback((columnId) => {
189
- setSorting((prev) => {
190
- const current = prev.find((s) => s.id === columnId);
191
- if (!current) {
192
- return [{ id: columnId, desc: false }];
193
- } else if (!current.desc) {
194
- return [{ id: columnId, desc: true }];
195
- } else {
196
- return [];
37
+ return validateSimpleFormula(formula, availableFields);
38
+ }, [formula, availableFields]);
39
+ const insertField = useCallback((field) => {
40
+ setFormula((prev) => {
41
+ if (prev.trim() && !prev.endsWith(" ")) {
42
+ return `${prev} ${field}`;
197
43
  }
44
+ return prev + field;
198
45
  });
199
46
  }, []);
200
- const getSortDirection = useCallback(
201
- (columnId) => {
202
- const sort = sorting.find((s) => s.id === columnId);
203
- if (!sort) return null;
204
- return sort.desc ? "desc" : "asc";
205
- },
206
- [sorting]
207
- );
208
- return {
209
- // Table instance
210
- table,
211
- // State
212
- sorting,
213
- columnFilters,
214
- columnVisibility,
215
- globalFilter,
216
- columnKeys,
217
- setSorting,
218
- setColumnFilters,
219
- setGlobalFilter,
220
- // Computed
221
- filteredRowCount,
222
- totalRowCount,
223
- activeFilters,
224
- // Methods
225
- getColumnStats,
226
- clearStatsCache,
227
- hasActiveFilter,
228
- setColumnFilter,
229
- getColumnFilterValues,
230
- clearAllFilters,
231
- toggleSort,
232
- getSortDirection,
233
- // Numeric range filters
234
- setNumericRangeFilter,
235
- getNumericRangeFilter
236
- };
237
- }
238
-
239
- // src/hooks/usePivotTable.ts
240
- import { useState as useState3, useMemo as useMemo3, useEffect as useEffect2, useCallback as useCallback3 } from "react";
241
- import {
242
- computeAvailableFields,
243
- getUnassignedFields,
244
- isPivotConfigured,
245
- computePivotResult,
246
- generateStorageKey,
247
- savePivotConfig,
248
- loadPivotConfig,
249
- isConfigValidForFields,
250
- getAggregationLabel,
251
- loadCalculatedFields,
252
- saveCalculatedFields
253
- } from "@smallwebco/tinypivot-core";
254
-
255
- // src/hooks/useLicense.ts
256
- import { useState as useState2, useCallback as useCallback2, useMemo as useMemo2 } from "react";
257
- import {
258
- validateLicenseKey,
259
- configureLicenseSecret as coreConfigureLicenseSecret,
260
- getDemoLicenseInfo,
261
- getFreeLicenseInfo,
262
- canUsePivot as coreCanUsePivot,
263
- isPro as coreIsPro,
264
- shouldShowWatermark as coreShouldShowWatermark,
265
- logProRequired
266
- } from "@smallwebco/tinypivot-core";
267
- var globalLicenseInfo = getFreeLicenseInfo();
268
- var globalDemoMode = false;
269
- var listeners = /* @__PURE__ */ new Set();
270
- function notifyListeners() {
271
- listeners.forEach((listener) => listener());
272
- }
273
- async function setLicenseKey(key) {
274
- globalLicenseInfo = await validateLicenseKey(key);
275
- if (!globalLicenseInfo.isValid) {
276
- console.warn("[TinyPivot] Invalid or expired license key. Running in free mode.");
277
- } else if (globalLicenseInfo.type !== "free") {
278
- console.info(`[TinyPivot] Pro license activated (${globalLicenseInfo.type})`);
279
- }
280
- notifyListeners();
281
- }
282
- async function enableDemoMode(secret) {
283
- const demoLicense = await getDemoLicenseInfo(secret);
284
- if (!demoLicense) {
285
- console.warn("[TinyPivot] Demo mode activation failed - invalid secret");
286
- return false;
287
- }
288
- globalDemoMode = true;
289
- globalLicenseInfo = demoLicense;
290
- console.info("[TinyPivot] Demo mode enabled - all Pro features unlocked for evaluation");
291
- notifyListeners();
292
- return true;
293
- }
294
- function configureLicenseSecret(secret) {
295
- coreConfigureLicenseSecret(secret);
296
- }
297
- function useLicense() {
298
- const [, forceUpdate] = useState2({});
299
- useState2(() => {
300
- const update = () => forceUpdate({});
301
- listeners.add(update);
302
- return () => listeners.delete(update);
303
- });
304
- const isDemo = globalDemoMode;
305
- const licenseInfo = globalLicenseInfo;
306
- const isPro = useMemo2(
307
- () => globalDemoMode || coreIsPro(licenseInfo),
308
- [licenseInfo]
309
- );
310
- const canUsePivot = useMemo2(
311
- () => globalDemoMode || coreCanUsePivot(licenseInfo),
312
- [licenseInfo]
313
- );
314
- const canUseAdvancedAggregations = useMemo2(
315
- () => globalDemoMode || licenseInfo.features.advancedAggregations,
316
- [licenseInfo]
317
- );
318
- const canUsePercentageMode = useMemo2(
319
- () => globalDemoMode || licenseInfo.features.percentageMode,
320
- [licenseInfo]
321
- );
322
- const showWatermark = useMemo2(
323
- () => coreShouldShowWatermark(licenseInfo, globalDemoMode),
324
- [licenseInfo]
325
- );
326
- const requirePro = useCallback2((feature) => {
327
- if (!isPro) {
328
- logProRequired(feature);
329
- return false;
47
+ const insertOperator = useCallback((op) => {
48
+ setFormula((prev) => {
49
+ if (prev.trim() && !prev.endsWith(" ")) {
50
+ return `${prev} ${op} `;
51
+ }
52
+ return `${prev + op} `;
53
+ });
54
+ }, []);
55
+ const handleSave = useCallback(() => {
56
+ if (!name.trim()) {
57
+ setError("Name is required");
58
+ return;
330
59
  }
331
- return true;
332
- }, [isPro]);
333
- return {
334
- licenseInfo,
335
- isDemo,
336
- isPro,
337
- canUsePivot,
338
- canUseAdvancedAggregations,
339
- canUsePercentageMode,
340
- showWatermark,
341
- requirePro
342
- };
60
+ const validationResult = validateSimpleFormula(formula, availableFields);
61
+ if (validationResult) {
62
+ setError(validationResult);
63
+ return;
64
+ }
65
+ const field = {
66
+ id: existingField?.id || `calc_${Date.now()}`,
67
+ name: name.trim(),
68
+ formula: formula.trim(),
69
+ formatAs,
70
+ decimals
71
+ };
72
+ onSave(field);
73
+ onClose();
74
+ }, [name, formula, formatAs, decimals, existingField, availableFields, onSave, onClose]);
75
+ const handleOverlayClick = useCallback((e) => {
76
+ if (e.target === e.currentTarget) {
77
+ onClose();
78
+ }
79
+ }, [onClose]);
80
+ if (!show)
81
+ return null;
82
+ const modalContent = /* @__PURE__ */ jsx("div", { className: "vpg-modal-overlay", onClick: handleOverlayClick, children: /* @__PURE__ */ jsxs("div", { className: "vpg-modal", children: [
83
+ /* @__PURE__ */ jsxs("div", { className: "vpg-modal-header", children: [
84
+ /* @__PURE__ */ jsxs("h3", { children: [
85
+ existingField ? "Edit" : "Create",
86
+ " ",
87
+ "Calculated Field"
88
+ ] }),
89
+ /* @__PURE__ */ jsx("button", { className: "vpg-modal-close", onClick: onClose, children: "\xD7" })
90
+ ] }),
91
+ /* @__PURE__ */ jsxs("div", { className: "vpg-modal-body", children: [
92
+ /* @__PURE__ */ jsxs("div", { className: "vpg-form-group", children: [
93
+ /* @__PURE__ */ jsx("label", { className: "vpg-label", children: "Name" }),
94
+ /* @__PURE__ */ jsx(
95
+ "input",
96
+ {
97
+ type: "text",
98
+ className: "vpg-input",
99
+ placeholder: "e.g., Profit Margin %",
100
+ value: name,
101
+ onChange: (e) => setName(e.target.value)
102
+ }
103
+ )
104
+ ] }),
105
+ /* @__PURE__ */ jsxs("div", { className: "vpg-form-group", children: [
106
+ /* @__PURE__ */ jsx("label", { className: "vpg-label", children: "Formula" }),
107
+ /* @__PURE__ */ jsx(
108
+ "textarea",
109
+ {
110
+ className: "vpg-textarea",
111
+ placeholder: "e.g., revenue / units",
112
+ rows: 2,
113
+ value: formula,
114
+ onChange: (e) => setFormula(e.target.value)
115
+ }
116
+ ),
117
+ /* @__PURE__ */ jsx("div", { className: "vpg-formula-hint", children: "Use field names with math operators: + - * / ( )" }),
118
+ validationError && /* @__PURE__ */ jsx("div", { className: "vpg-error", children: validationError })
119
+ ] }),
120
+ /* @__PURE__ */ jsxs("div", { className: "vpg-form-group", children: [
121
+ /* @__PURE__ */ jsx("label", { className: "vpg-label-small", children: "Operators" }),
122
+ /* @__PURE__ */ jsxs("div", { className: "vpg-button-group", children: [
123
+ /* @__PURE__ */ jsx("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("+"), children: "+" }),
124
+ /* @__PURE__ */ jsx("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("-"), children: "\u2212" }),
125
+ /* @__PURE__ */ jsx("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("*"), children: "\xD7" }),
126
+ /* @__PURE__ */ jsx("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("/"), children: "\xF7" }),
127
+ /* @__PURE__ */ jsx("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("("), children: "(" }),
128
+ /* @__PURE__ */ jsx("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator(")"), children: ")" })
129
+ ] })
130
+ ] }),
131
+ /* @__PURE__ */ jsxs("div", { className: "vpg-form-group", children: [
132
+ /* @__PURE__ */ jsx("label", { className: "vpg-label-small", children: "Insert Field" }),
133
+ availableFields.length > 0 ? /* @__PURE__ */ jsx("div", { className: "vpg-button-group vpg-field-buttons", children: availableFields.map((field) => /* @__PURE__ */ jsx(
134
+ "button",
135
+ {
136
+ className: "vpg-insert-btn vpg-field-btn",
137
+ onClick: () => insertField(field),
138
+ children: field
139
+ },
140
+ field
141
+ )) }) : /* @__PURE__ */ jsx("div", { className: "vpg-no-fields", children: "No numeric fields available" })
142
+ ] }),
143
+ /* @__PURE__ */ jsxs("div", { className: "vpg-form-row", children: [
144
+ /* @__PURE__ */ jsxs("div", { className: "vpg-form-group vpg-form-group-half", children: [
145
+ /* @__PURE__ */ jsx("label", { className: "vpg-label", children: "Format As" }),
146
+ /* @__PURE__ */ jsxs(
147
+ "select",
148
+ {
149
+ className: "vpg-select",
150
+ value: formatAs,
151
+ onChange: (e) => setFormatAs(e.target.value),
152
+ children: [
153
+ /* @__PURE__ */ jsx("option", { value: "number", children: "Number" }),
154
+ /* @__PURE__ */ jsx("option", { value: "percent", children: "Percentage" }),
155
+ /* @__PURE__ */ jsx("option", { value: "currency", children: "Currency ($)" })
156
+ ]
157
+ }
158
+ )
159
+ ] }),
160
+ /* @__PURE__ */ jsxs("div", { className: "vpg-form-group vpg-form-group-half", children: [
161
+ /* @__PURE__ */ jsx("label", { className: "vpg-label", children: "Decimals" }),
162
+ /* @__PURE__ */ jsx(
163
+ "input",
164
+ {
165
+ type: "number",
166
+ className: "vpg-input",
167
+ min: 0,
168
+ max: 6,
169
+ value: decimals,
170
+ onChange: (e) => setDecimals(Number(e.target.value))
171
+ }
172
+ )
173
+ ] })
174
+ ] }),
175
+ error && /* @__PURE__ */ jsx("div", { className: "vpg-error vpg-error-box", children: error })
176
+ ] }),
177
+ /* @__PURE__ */ jsxs("div", { className: "vpg-modal-footer", children: [
178
+ /* @__PURE__ */ jsx("button", { className: "vpg-btn vpg-btn-secondary", onClick: onClose, children: "Cancel" }),
179
+ /* @__PURE__ */ jsxs("button", { className: "vpg-btn vpg-btn-primary", onClick: handleSave, children: [
180
+ existingField ? "Update" : "Add",
181
+ " ",
182
+ "Field"
183
+ ] })
184
+ ] })
185
+ ] }) });
186
+ if (typeof document === "undefined")
187
+ return null;
188
+ return createPortal(modalContent, document.body);
343
189
  }
344
190
 
345
- // src/hooks/usePivotTable.ts
346
- function usePivotTable(data) {
347
- const { canUsePivot, requirePro } = useLicense();
348
- const [rowFields, setRowFieldsState] = useState3([]);
349
- const [columnFields, setColumnFieldsState] = useState3([]);
350
- const [valueFields, setValueFields] = useState3([]);
351
- const [showRowTotals, setShowRowTotals] = useState3(true);
352
- const [showColumnTotals, setShowColumnTotals] = useState3(true);
353
- const [calculatedFields, setCalculatedFields] = useState3(() => loadCalculatedFields());
354
- const [currentStorageKey, setCurrentStorageKey] = useState3(null);
355
- const availableFields = useMemo3(() => {
356
- return computeAvailableFields(data);
357
- }, [data]);
358
- const unassignedFields = useMemo3(() => {
359
- return getUnassignedFields(availableFields, rowFields, columnFields, valueFields);
360
- }, [availableFields, rowFields, columnFields, valueFields]);
361
- const isConfigured = useMemo3(() => {
362
- return isPivotConfigured({
363
- rowFields,
364
- columnFields,
365
- valueFields,
366
- showRowTotals,
367
- showColumnTotals
368
- });
369
- }, [rowFields, columnFields, valueFields, showRowTotals, showColumnTotals]);
370
- const pivotResult = useMemo3(() => {
371
- if (!isConfigured) return null;
372
- if (!canUsePivot) return null;
373
- return computePivotResult(data, {
374
- rowFields,
375
- columnFields,
376
- valueFields,
377
- showRowTotals,
378
- showColumnTotals,
379
- calculatedFields
191
+ // src/components/ColumnFilter.tsx
192
+ import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo3, useRef, useState as useState3 } from "react";
193
+
194
+ // src/components/NumericRangeFilter.tsx
195
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
196
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
197
+ function NumericRangeFilter({
198
+ dataMin,
199
+ dataMax,
200
+ currentRange,
201
+ onChange
202
+ }) {
203
+ const [localMin, setLocalMin] = useState2(currentRange?.min ?? null);
204
+ const [localMax, setLocalMax] = useState2(currentRange?.max ?? null);
205
+ const step = useMemo2(() => {
206
+ const range = dataMax - dataMin;
207
+ if (range === 0)
208
+ return 1;
209
+ if (range <= 1)
210
+ return 0.01;
211
+ if (range <= 10)
212
+ return 0.1;
213
+ if (range <= 100)
214
+ return 1;
215
+ if (range <= 1e3)
216
+ return 10;
217
+ return 10 ** (Math.floor(Math.log10(range)) - 2);
218
+ }, [dataMin, dataMax]);
219
+ const formatValue = useCallback2((val) => {
220
+ if (val === null)
221
+ return "";
222
+ if (Number.isInteger(val))
223
+ return val.toLocaleString();
224
+ return val.toLocaleString(void 0, { maximumFractionDigits: 2 });
225
+ }, []);
226
+ const isFilterActive = localMin !== null || localMax !== null;
227
+ const minPercent = useMemo2(() => {
228
+ if (localMin === null || dataMax === dataMin)
229
+ return 0;
230
+ return (localMin - dataMin) / (dataMax - dataMin) * 100;
231
+ }, [localMin, dataMin, dataMax]);
232
+ const maxPercent = useMemo2(() => {
233
+ if (localMax === null || dataMax === dataMin)
234
+ return 100;
235
+ return (localMax - dataMin) / (dataMax - dataMin) * 100;
236
+ }, [localMax, dataMin, dataMax]);
237
+ const handleMinSlider = useCallback2((event) => {
238
+ const value = Number.parseFloat(event.target.value);
239
+ setLocalMin(() => {
240
+ if (localMax !== null && value > localMax) {
241
+ return localMax;
242
+ }
243
+ return value;
380
244
  });
381
- }, [data, isConfigured, canUsePivot, rowFields, columnFields, valueFields, showRowTotals, showColumnTotals, calculatedFields]);
382
- useEffect2(() => {
383
- if (data.length === 0) return;
384
- const newKeys = Object.keys(data[0]);
385
- const storageKey = generateStorageKey(newKeys);
386
- if (storageKey !== currentStorageKey) {
387
- setCurrentStorageKey(storageKey);
388
- const savedConfig = loadPivotConfig(storageKey);
389
- if (savedConfig && isConfigValidForFields(savedConfig, newKeys)) {
390
- setRowFieldsState(savedConfig.rowFields);
391
- setColumnFieldsState(savedConfig.columnFields);
392
- setValueFields(savedConfig.valueFields);
393
- setShowRowTotals(savedConfig.showRowTotals);
394
- setShowColumnTotals(savedConfig.showColumnTotals);
395
- if (savedConfig.calculatedFields) {
396
- setCalculatedFields(savedConfig.calculatedFields);
397
- }
398
- } else {
399
- const currentConfig = {
400
- rowFields,
401
- columnFields,
402
- valueFields,
403
- showRowTotals,
404
- showColumnTotals
405
- };
406
- if (!isConfigValidForFields(currentConfig, newKeys)) {
407
- setRowFieldsState([]);
408
- setColumnFieldsState([]);
409
- setValueFields([]);
410
- }
245
+ }, [localMax]);
246
+ const handleMaxSlider = useCallback2((event) => {
247
+ const value = Number.parseFloat(event.target.value);
248
+ setLocalMax(() => {
249
+ if (localMin !== null && value < localMin) {
250
+ return localMin;
411
251
  }
252
+ return value;
253
+ });
254
+ }, [localMin]);
255
+ const handleSliderChange = useCallback2(() => {
256
+ if (localMin === null && localMax === null) {
257
+ onChange(null);
258
+ } else {
259
+ onChange({ min: localMin, max: localMax });
412
260
  }
413
- }, [data]);
414
- useEffect2(() => {
415
- if (!currentStorageKey) return;
416
- const config = {
417
- rowFields,
418
- columnFields,
419
- valueFields,
420
- showRowTotals,
421
- showColumnTotals,
422
- calculatedFields
423
- };
424
- savePivotConfig(currentStorageKey, config);
425
- }, [currentStorageKey, rowFields, columnFields, valueFields, showRowTotals, showColumnTotals, calculatedFields]);
426
- const addRowField = useCallback3(
427
- (field) => {
428
- if (!rowFields.includes(field)) {
429
- setRowFieldsState((prev) => [...prev, field]);
430
- }
431
- },
432
- [rowFields]
433
- );
434
- const removeRowField = useCallback3((field) => {
435
- setRowFieldsState((prev) => prev.filter((f) => f !== field));
436
- }, []);
437
- const setRowFields = useCallback3((fields) => {
438
- setRowFieldsState(fields);
439
- }, []);
440
- const addColumnField = useCallback3(
441
- (field) => {
442
- if (!columnFields.includes(field)) {
443
- setColumnFieldsState((prev) => [...prev, field]);
444
- }
445
- },
446
- [columnFields]
447
- );
448
- const removeColumnField = useCallback3((field) => {
449
- setColumnFieldsState((prev) => prev.filter((f) => f !== field));
450
- }, []);
451
- const setColumnFields = useCallback3((fields) => {
452
- setColumnFieldsState(fields);
453
- }, []);
454
- const addValueField = useCallback3(
455
- (field, aggregation = "sum") => {
456
- if (aggregation !== "sum" && !requirePro(`${aggregation} aggregation`)) {
457
- return;
458
- }
459
- setValueFields((prev) => {
460
- if (prev.some((v) => v.field === field && v.aggregation === aggregation)) {
461
- return prev;
261
+ }, [localMin, localMax, onChange]);
262
+ const handleMinInput = useCallback2((event) => {
263
+ const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
264
+ if (value !== null && !Number.isNaN(value)) {
265
+ setLocalMin(Math.max(dataMin, Math.min(value, localMax ?? dataMax)));
266
+ } else if (value === null) {
267
+ setLocalMin(null);
268
+ }
269
+ }, [dataMin, dataMax, localMax]);
270
+ const handleMaxInput = useCallback2((event) => {
271
+ const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
272
+ if (value !== null && !Number.isNaN(value)) {
273
+ setLocalMax(Math.min(dataMax, Math.max(value, localMin ?? dataMin)));
274
+ } else if (value === null) {
275
+ setLocalMax(null);
276
+ }
277
+ }, [dataMin, dataMax, localMin]);
278
+ const handleInputBlur = useCallback2(() => {
279
+ if (localMin === null && localMax === null) {
280
+ onChange(null);
281
+ } else {
282
+ onChange({ min: localMin, max: localMax });
283
+ }
284
+ }, [localMin, localMax, onChange]);
285
+ const clearFilter = useCallback2(() => {
286
+ setLocalMin(null);
287
+ setLocalMax(null);
288
+ onChange(null);
289
+ }, [onChange]);
290
+ const setFullRange = useCallback2(() => {
291
+ setLocalMin(dataMin);
292
+ setLocalMax(dataMax);
293
+ onChange({ min: dataMin, max: dataMax });
294
+ }, [dataMin, dataMax, onChange]);
295
+ useEffect2(() => {
296
+ setLocalMin(currentRange?.min ?? null);
297
+ setLocalMax(currentRange?.max ?? null);
298
+ }, [currentRange]);
299
+ return /* @__PURE__ */ jsxs2("div", { className: "vpg-range-filter", children: [
300
+ /* @__PURE__ */ jsxs2("div", { className: "vpg-range-info", children: [
301
+ /* @__PURE__ */ jsx2("span", { className: "vpg-range-label", children: "Data range:" }),
302
+ /* @__PURE__ */ jsxs2("span", { className: "vpg-range-bounds", children: [
303
+ formatValue(dataMin),
304
+ " ",
305
+ "\u2013",
306
+ formatValue(dataMax)
307
+ ] })
308
+ ] }),
309
+ /* @__PURE__ */ jsxs2("div", { className: "vpg-slider-container", children: [
310
+ /* @__PURE__ */ jsx2("div", { className: "vpg-slider-track", children: /* @__PURE__ */ jsx2(
311
+ "div",
312
+ {
313
+ className: "vpg-slider-fill",
314
+ style: {
315
+ left: `${minPercent}%`,
316
+ right: `${100 - maxPercent}%`
317
+ }
462
318
  }
463
- return [...prev, { field, aggregation }];
464
- });
465
- },
466
- [requirePro]
467
- );
468
- const removeValueField = useCallback3((field, aggregation) => {
469
- setValueFields((prev) => {
470
- if (aggregation) {
471
- return prev.filter((v) => !(v.field === field && v.aggregation === aggregation));
472
- }
473
- return prev.filter((v) => v.field !== field);
474
- });
475
- }, []);
476
- const updateValueFieldAggregation = useCallback3(
477
- (field, oldAgg, newAgg) => {
478
- setValueFields(
479
- (prev) => prev.map((v) => {
480
- if (v.field === field && v.aggregation === oldAgg) {
481
- return { ...v, aggregation: newAgg };
319
+ ) }),
320
+ /* @__PURE__ */ jsx2(
321
+ "input",
322
+ {
323
+ type: "range",
324
+ className: "vpg-slider vpg-slider-min",
325
+ min: dataMin,
326
+ max: dataMax,
327
+ step,
328
+ value: localMin ?? dataMin,
329
+ onChange: handleMinSlider,
330
+ onMouseUp: handleSliderChange,
331
+ onTouchEnd: handleSliderChange
332
+ }
333
+ ),
334
+ /* @__PURE__ */ jsx2(
335
+ "input",
336
+ {
337
+ type: "range",
338
+ className: "vpg-slider vpg-slider-max",
339
+ min: dataMin,
340
+ max: dataMax,
341
+ step,
342
+ value: localMax ?? dataMax,
343
+ onChange: handleMaxSlider,
344
+ onMouseUp: handleSliderChange,
345
+ onTouchEnd: handleSliderChange
346
+ }
347
+ )
348
+ ] }),
349
+ /* @__PURE__ */ jsxs2("div", { className: "vpg-range-inputs", children: [
350
+ /* @__PURE__ */ jsxs2("div", { className: "vpg-input-group", children: [
351
+ /* @__PURE__ */ jsx2("label", { className: "vpg-input-label", children: "Min" }),
352
+ /* @__PURE__ */ jsx2(
353
+ "input",
354
+ {
355
+ type: "number",
356
+ className: "vpg-range-input",
357
+ placeholder: formatValue(dataMin),
358
+ value: localMin ?? "",
359
+ step,
360
+ onChange: handleMinInput,
361
+ onBlur: handleInputBlur
482
362
  }
483
- return v;
484
- })
485
- );
486
- },
487
- []
488
- );
489
- const clearConfig = useCallback3(() => {
490
- setRowFieldsState([]);
491
- setColumnFieldsState([]);
492
- setValueFields([]);
493
- }, []);
494
- const autoSuggestConfig = useCallback3(() => {
495
- if (!requirePro("Pivot Table - Auto Suggest")) return;
496
- if (availableFields.length === 0) return;
497
- const categoricalFields = availableFields.filter((f) => !f.isNumeric && f.uniqueCount < 50);
498
- const numericFields = availableFields.filter((f) => f.isNumeric);
499
- if (categoricalFields.length > 0 && numericFields.length > 0) {
500
- setRowFieldsState([categoricalFields[0].field]);
501
- setValueFields([{ field: numericFields[0].field, aggregation: "sum" }]);
363
+ )
364
+ ] }),
365
+ /* @__PURE__ */ jsx2("span", { className: "vpg-input-separator", children: "to" }),
366
+ /* @__PURE__ */ jsxs2("div", { className: "vpg-input-group", children: [
367
+ /* @__PURE__ */ jsx2("label", { className: "vpg-input-label", children: "Max" }),
368
+ /* @__PURE__ */ jsx2(
369
+ "input",
370
+ {
371
+ type: "number",
372
+ className: "vpg-range-input",
373
+ placeholder: formatValue(dataMax),
374
+ value: localMax ?? "",
375
+ step,
376
+ onChange: handleMaxInput,
377
+ onBlur: handleInputBlur
378
+ }
379
+ )
380
+ ] })
381
+ ] }),
382
+ /* @__PURE__ */ jsxs2("div", { className: "vpg-range-actions", children: [
383
+ /* @__PURE__ */ jsxs2(
384
+ "button",
385
+ {
386
+ className: "vpg-range-btn",
387
+ disabled: !isFilterActive,
388
+ onClick: clearFilter,
389
+ children: [
390
+ /* @__PURE__ */ jsx2("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
391
+ "path",
392
+ {
393
+ strokeLinecap: "round",
394
+ strokeLinejoin: "round",
395
+ strokeWidth: 2,
396
+ d: "M6 18L18 6M6 6l12 12"
397
+ }
398
+ ) }),
399
+ "Clear"
400
+ ]
401
+ }
402
+ ),
403
+ /* @__PURE__ */ jsxs2("button", { className: "vpg-range-btn", onClick: setFullRange, children: [
404
+ /* @__PURE__ */ jsx2("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
405
+ "path",
406
+ {
407
+ strokeLinecap: "round",
408
+ strokeLinejoin: "round",
409
+ strokeWidth: 2,
410
+ d: "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
411
+ }
412
+ ) }),
413
+ "Full Range"
414
+ ] })
415
+ ] }),
416
+ isFilterActive && /* @__PURE__ */ jsxs2("div", { className: "vpg-filter-summary", children: [
417
+ /* @__PURE__ */ jsx2("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
418
+ "path",
419
+ {
420
+ strokeLinecap: "round",
421
+ strokeLinejoin: "round",
422
+ strokeWidth: 2,
423
+ 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"
424
+ }
425
+ ) }),
426
+ /* @__PURE__ */ jsxs2("span", { children: [
427
+ "Showing values",
428
+ " ",
429
+ localMin !== null && /* @__PURE__ */ jsxs2("strong", { children: [
430
+ "\u2265",
431
+ formatValue(localMin)
432
+ ] }),
433
+ localMin !== null && localMax !== null && " and ",
434
+ localMax !== null && /* @__PURE__ */ jsxs2("strong", { children: [
435
+ "\u2264",
436
+ formatValue(localMax)
437
+ ] })
438
+ ] })
439
+ ] })
440
+ ] });
441
+ }
442
+
443
+ // src/components/ColumnFilter.tsx
444
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
445
+ function ColumnFilter({
446
+ columnName,
447
+ stats,
448
+ selectedValues,
449
+ sortDirection,
450
+ numericRange,
451
+ onFilter,
452
+ onSort,
453
+ onClose,
454
+ onRangeFilter
455
+ }) {
456
+ const [searchQuery, setSearchQuery] = useState3("");
457
+ const [localSelected, setLocalSelected] = useState3(new Set(selectedValues));
458
+ const dropdownRef = useRef(null);
459
+ const searchInputRef = useRef(null);
460
+ const isNumericColumn = stats.type === "number" && stats.numericMin !== void 0 && stats.numericMax !== void 0;
461
+ const [filterMode, setFilterMode] = useState3(numericRange ? "range" : "values");
462
+ const [localRange, setLocalRange] = useState3(numericRange ?? null);
463
+ const hasBlankValues = stats.nullCount > 0;
464
+ const filteredValues = useMemo3(() => {
465
+ const values = stats.uniqueValues;
466
+ if (!searchQuery)
467
+ return values;
468
+ const query = searchQuery.toLowerCase();
469
+ return values.filter((v) => v.toLowerCase().includes(query));
470
+ }, [stats.uniqueValues, searchQuery]);
471
+ const allValues = useMemo3(() => {
472
+ const values = [...filteredValues];
473
+ if (hasBlankValues && (!searchQuery || "(blank)".includes(searchQuery.toLowerCase()))) {
474
+ values.unshift("(blank)");
502
475
  }
503
- }, [availableFields, requirePro]);
504
- const addCalculatedField = useCallback3((field) => {
505
- setCalculatedFields((prev) => {
506
- const existing = prev.findIndex((f) => f.id === field.id);
507
- let updated;
508
- if (existing >= 0) {
509
- updated = [...prev.slice(0, existing), field, ...prev.slice(existing + 1)];
476
+ return values;
477
+ }, [filteredValues, hasBlankValues, searchQuery]);
478
+ const _isAllSelected = useMemo3(
479
+ () => allValues.every((v) => localSelected.has(v)),
480
+ [allValues, localSelected]
481
+ );
482
+ const toggleValue = useCallback3((value) => {
483
+ setLocalSelected((prev) => {
484
+ const next = new Set(prev);
485
+ if (next.has(value)) {
486
+ next.delete(value);
510
487
  } else {
511
- updated = [...prev, field];
488
+ next.add(value);
512
489
  }
513
- saveCalculatedFields(updated);
514
- return updated;
515
- });
516
- }, []);
517
- const removeCalculatedField = useCallback3((id) => {
518
- setCalculatedFields((prev) => {
519
- const updated = prev.filter((f) => f.id !== id);
520
- saveCalculatedFields(updated);
521
- return updated;
490
+ return next;
522
491
  });
523
- setValueFields((prev) => prev.filter((v) => v.field !== `calc:${id}`));
524
492
  }, []);
525
- return {
526
- // State
527
- rowFields,
528
- columnFields,
529
- valueFields,
530
- showRowTotals,
531
- showColumnTotals,
532
- calculatedFields,
533
- // Computed
534
- availableFields,
535
- unassignedFields,
536
- isConfigured,
537
- pivotResult,
538
- // Actions
539
- addRowField,
540
- removeRowField,
541
- addColumnField,
542
- removeColumnField,
543
- addValueField,
544
- removeValueField,
545
- updateValueFieldAggregation,
546
- clearConfig,
547
- setShowRowTotals,
548
- setShowColumnTotals,
549
- autoSuggestConfig,
550
- setRowFields,
551
- setColumnFields,
552
- addCalculatedField,
553
- removeCalculatedField
554
- };
555
- }
556
-
557
- // src/hooks/useGridFeatures.ts
558
- import { useState as useState4, useMemo as useMemo4, useCallback as useCallback4 } from "react";
559
- import {
560
- exportToCSV as coreExportToCSV,
561
- exportPivotToCSV as coreExportPivotToCSV,
562
- copyToClipboard as coreCopyToClipboard,
563
- formatSelectionForClipboard as coreFormatSelection
564
- } from "@smallwebco/tinypivot-core";
565
- function exportToCSV(data, columns, options) {
566
- coreExportToCSV(data, columns, options);
567
- }
568
- function exportPivotToCSV(pivotData, rowFields, columnFields, valueFields, options) {
569
- coreExportPivotToCSV(pivotData, rowFields, columnFields, valueFields, options);
570
- }
571
- function copyToClipboard(text, onSuccess, onError) {
572
- coreCopyToClipboard(text, onSuccess, onError);
573
- }
574
- function formatSelectionForClipboard(rows, columns, selectionBounds) {
575
- return coreFormatSelection(rows, columns, selectionBounds);
576
- }
577
- function usePagination(data, options = {}) {
578
- const [pageSize, setPageSize] = useState4(options.pageSize ?? 50);
579
- const [currentPage, setCurrentPage] = useState4(options.currentPage ?? 1);
580
- const totalPages = useMemo4(
581
- () => Math.max(1, Math.ceil(data.length / pageSize)),
582
- [data.length, pageSize]
583
- );
584
- const paginatedData = useMemo4(() => {
585
- const start = (currentPage - 1) * pageSize;
586
- const end = start + pageSize;
587
- return data.slice(start, end);
588
- }, [data, currentPage, pageSize]);
589
- const startIndex = useMemo4(() => (currentPage - 1) * pageSize + 1, [currentPage, pageSize]);
590
- const endIndex = useMemo4(
591
- () => Math.min(currentPage * pageSize, data.length),
592
- [currentPage, pageSize, data.length]
593
- );
594
- const goToPage = useCallback4(
595
- (page) => {
596
- setCurrentPage(Math.max(1, Math.min(page, totalPages)));
597
- },
598
- [totalPages]
599
- );
600
- const nextPage = useCallback4(() => {
601
- if (currentPage < totalPages) {
602
- setCurrentPage((prev) => prev + 1);
603
- }
604
- }, [currentPage, totalPages]);
605
- const prevPage = useCallback4(() => {
606
- if (currentPage > 1) {
607
- setCurrentPage((prev) => prev - 1);
608
- }
609
- }, [currentPage]);
610
- const firstPage = useCallback4(() => {
611
- setCurrentPage(1);
612
- }, []);
613
- const lastPage = useCallback4(() => {
614
- setCurrentPage(totalPages);
615
- }, [totalPages]);
616
- const updatePageSize = useCallback4((size) => {
617
- setPageSize(size);
618
- setCurrentPage(1);
619
- }, []);
620
- return {
621
- pageSize,
622
- currentPage,
623
- totalPages,
624
- paginatedData,
625
- startIndex,
626
- endIndex,
627
- goToPage,
628
- nextPage,
629
- prevPage,
630
- firstPage,
631
- lastPage,
632
- setPageSize: updatePageSize
633
- };
634
- }
635
- function useGlobalSearch(data, columns) {
636
- const [searchTerm, setSearchTerm] = useState4("");
637
- const [caseSensitive, setCaseSensitive] = useState4(false);
638
- const filteredData = useMemo4(() => {
639
- if (!searchTerm.trim()) {
640
- return data;
641
- }
642
- const term = caseSensitive ? searchTerm.trim() : searchTerm.trim().toLowerCase();
643
- return data.filter((row) => {
644
- for (const col of columns) {
645
- const value = row[col];
646
- if (value === null || value === void 0) continue;
647
- const strValue = caseSensitive ? String(value) : String(value).toLowerCase();
648
- if (strValue.includes(term)) {
649
- return true;
650
- }
651
- }
652
- return false;
653
- });
654
- }, [data, columns, searchTerm, caseSensitive]);
655
- const clearSearch = useCallback4(() => {
656
- setSearchTerm("");
657
- }, []);
658
- return {
659
- searchTerm,
660
- setSearchTerm,
661
- caseSensitive,
662
- setCaseSensitive,
663
- filteredData,
664
- clearSearch
665
- };
666
- }
667
- function useRowSelection(data) {
668
- const [selectedRowIndices, setSelectedRowIndices] = useState4(/* @__PURE__ */ new Set());
669
- const selectedRows = useMemo4(() => {
670
- return Array.from(selectedRowIndices).sort((a, b) => a - b).map((idx) => data[idx]).filter(Boolean);
671
- }, [data, selectedRowIndices]);
672
- const allSelected = useMemo4(() => {
673
- return data.length > 0 && selectedRowIndices.size === data.length;
674
- }, [data.length, selectedRowIndices.size]);
675
- const someSelected = useMemo4(() => {
676
- return selectedRowIndices.size > 0 && selectedRowIndices.size < data.length;
677
- }, [data.length, selectedRowIndices.size]);
678
- const toggleRow = useCallback4((index) => {
679
- setSelectedRowIndices((prev) => {
493
+ const selectAll = useCallback3(() => {
494
+ setLocalSelected((prev) => {
680
495
  const next = new Set(prev);
681
- if (next.has(index)) {
682
- next.delete(index);
683
- } else {
684
- next.add(index);
496
+ for (const value of allValues) {
497
+ next.add(value);
685
498
  }
686
499
  return next;
687
500
  });
501
+ }, [allValues]);
502
+ const clearAll = useCallback3(() => {
503
+ setLocalSelected(/* @__PURE__ */ new Set());
688
504
  }, []);
689
- const selectRow = useCallback4((index) => {
690
- setSelectedRowIndices((prev) => /* @__PURE__ */ new Set([...prev, index]));
691
- }, []);
692
- const deselectRow = useCallback4((index) => {
693
- setSelectedRowIndices((prev) => {
694
- const next = new Set(prev);
695
- next.delete(index);
696
- return next;
697
- });
698
- }, []);
699
- const selectAll = useCallback4(() => {
700
- setSelectedRowIndices(new Set(data.map((_, idx) => idx)));
701
- }, [data]);
702
- const deselectAll = useCallback4(() => {
703
- setSelectedRowIndices(/* @__PURE__ */ new Set());
704
- }, []);
705
- const toggleAll = useCallback4(() => {
706
- if (allSelected) {
707
- deselectAll();
505
+ const applyFilter = useCallback3(() => {
506
+ if (localSelected.size === 0) {
507
+ onFilter([]);
708
508
  } else {
709
- selectAll();
509
+ onFilter(Array.from(localSelected));
710
510
  }
711
- }, [allSelected, selectAll, deselectAll]);
712
- const isSelected = useCallback4(
713
- (index) => {
714
- return selectedRowIndices.has(index);
715
- },
716
- [selectedRowIndices]
717
- );
718
- const selectRange = useCallback4((startIndex, endIndex) => {
719
- const min = Math.min(startIndex, endIndex);
720
- const max = Math.max(startIndex, endIndex);
721
- setSelectedRowIndices((prev) => {
722
- const next = new Set(prev);
723
- for (let i = min; i <= max; i++) {
724
- next.add(i);
725
- }
726
- return next;
727
- });
511
+ onClose();
512
+ }, [localSelected, onFilter, onClose]);
513
+ const sortAscending = useCallback3(() => {
514
+ onSort(sortDirection === "asc" ? null : "asc");
515
+ }, [sortDirection, onSort]);
516
+ const sortDescending = useCallback3(() => {
517
+ onSort(sortDirection === "desc" ? null : "desc");
518
+ }, [sortDirection, onSort]);
519
+ const clearFilter = useCallback3(() => {
520
+ setLocalSelected(/* @__PURE__ */ new Set());
521
+ onFilter([]);
522
+ onClose();
523
+ }, [onFilter, onClose]);
524
+ const handleRangeChange = useCallback3((range) => {
525
+ setLocalRange(range);
728
526
  }, []);
729
- return {
730
- selectedRowIndices,
731
- selectedRows,
732
- allSelected,
733
- someSelected,
734
- toggleRow,
735
- selectRow,
736
- deselectRow,
737
- selectAll,
738
- deselectAll,
739
- toggleAll,
740
- isSelected,
741
- selectRange
742
- };
743
- }
744
- function useColumnResize(initialWidths, minWidth = 60, maxWidth = 600) {
745
- const [columnWidths, setColumnWidths] = useState4({ ...initialWidths });
746
- const [isResizing, setIsResizing] = useState4(false);
747
- const [resizingColumn, setResizingColumn] = useState4(null);
748
- const startResize = useCallback4(
749
- (columnId, event) => {
750
- setIsResizing(true);
751
- setResizingColumn(columnId);
752
- const startX = event.clientX;
753
- const startWidth = columnWidths[columnId] || 150;
754
- const handleMouseMove = (e) => {
755
- const diff = e.clientX - startX;
756
- const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + diff));
757
- setColumnWidths((prev) => ({
758
- ...prev,
759
- [columnId]: newWidth
760
- }));
761
- };
762
- const handleMouseUp = () => {
763
- setIsResizing(false);
764
- setResizingColumn(null);
765
- document.removeEventListener("mousemove", handleMouseMove);
766
- document.removeEventListener("mouseup", handleMouseUp);
767
- };
768
- document.addEventListener("mousemove", handleMouseMove);
769
- document.addEventListener("mouseup", handleMouseUp);
770
- },
771
- [columnWidths, minWidth, maxWidth]
772
- );
773
- const resetColumnWidth = useCallback4(
774
- (columnId) => {
775
- if (initialWidths[columnId]) {
776
- setColumnWidths((prev) => ({
777
- ...prev,
778
- [columnId]: initialWidths[columnId]
779
- }));
527
+ const applyRangeFilter = useCallback3(() => {
528
+ onRangeFilter?.(localRange);
529
+ onClose();
530
+ }, [localRange, onRangeFilter, onClose]);
531
+ const clearRangeFilter = useCallback3(() => {
532
+ setLocalRange(null);
533
+ onRangeFilter?.(null);
534
+ onClose();
535
+ }, [onRangeFilter, onClose]);
536
+ useEffect3(() => {
537
+ const handleClickOutside = (event) => {
538
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
539
+ onClose();
780
540
  }
781
- },
782
- [initialWidths]
783
- );
784
- const resetAllWidths = useCallback4(() => {
785
- setColumnWidths({ ...initialWidths });
786
- }, [initialWidths]);
787
- return {
788
- columnWidths,
789
- setColumnWidths,
790
- isResizing,
791
- resizingColumn,
792
- startResize,
793
- resetColumnWidth,
794
- resetAllWidths
795
- };
796
- }
797
-
798
- // src/components/ColumnFilter.tsx
799
- import { useState as useState6, useEffect as useEffect4, useRef, useCallback as useCallback6, useMemo as useMemo6 } from "react";
800
-
801
- // src/components/NumericRangeFilter.tsx
802
- import { useState as useState5, useCallback as useCallback5, useMemo as useMemo5, useEffect as useEffect3 } from "react";
803
- import { jsx, jsxs } from "react/jsx-runtime";
804
- function NumericRangeFilter({
805
- dataMin,
806
- dataMax,
807
- currentRange,
808
- onChange
809
- }) {
810
- const [localMin, setLocalMin] = useState5(currentRange?.min ?? null);
811
- const [localMax, setLocalMax] = useState5(currentRange?.max ?? null);
812
- const step = useMemo5(() => {
813
- const range = dataMax - dataMin;
814
- if (range === 0) return 1;
815
- if (range <= 1) return 0.01;
816
- if (range <= 10) return 0.1;
817
- if (range <= 100) return 1;
818
- if (range <= 1e3) return 10;
819
- return Math.pow(10, Math.floor(Math.log10(range)) - 2);
820
- }, [dataMin, dataMax]);
821
- const formatValue = useCallback5((val) => {
822
- if (val === null) return "";
823
- if (Number.isInteger(val)) return val.toLocaleString();
824
- return val.toLocaleString(void 0, { maximumFractionDigits: 2 });
825
- }, []);
826
- const isFilterActive = localMin !== null || localMax !== null;
827
- const minPercent = useMemo5(() => {
828
- if (localMin === null || dataMax === dataMin) return 0;
829
- return (localMin - dataMin) / (dataMax - dataMin) * 100;
830
- }, [localMin, dataMin, dataMax]);
831
- const maxPercent = useMemo5(() => {
832
- if (localMax === null || dataMax === dataMin) return 100;
833
- return (localMax - dataMin) / (dataMax - dataMin) * 100;
834
- }, [localMax, dataMin, dataMax]);
835
- const handleMinSlider = useCallback5((event) => {
836
- const value = Number.parseFloat(event.target.value);
837
- setLocalMin((prev) => {
838
- if (localMax !== null && value > localMax) {
839
- return localMax;
840
- }
841
- return value;
842
- });
843
- }, [localMax]);
844
- const handleMaxSlider = useCallback5((event) => {
845
- const value = Number.parseFloat(event.target.value);
846
- setLocalMax((prev) => {
847
- if (localMin !== null && value < localMin) {
848
- return localMin;
541
+ };
542
+ document.addEventListener("mousedown", handleClickOutside);
543
+ return () => document.removeEventListener("mousedown", handleClickOutside);
544
+ }, [onClose]);
545
+ useEffect3(() => {
546
+ const handleKeydown = (event) => {
547
+ if (event.key === "Escape") {
548
+ onClose();
549
+ } else if (event.key === "Enter" && event.ctrlKey) {
550
+ applyFilter();
849
551
  }
850
- return value;
851
- });
852
- }, [localMin]);
853
- const handleSliderChange = useCallback5(() => {
854
- if (localMin === null && localMax === null) {
855
- onChange(null);
856
- } else {
857
- onChange({ min: localMin, max: localMax });
858
- }
859
- }, [localMin, localMax, onChange]);
860
- const handleMinInput = useCallback5((event) => {
861
- const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
862
- if (value !== null && !Number.isNaN(value)) {
863
- setLocalMin(Math.max(dataMin, Math.min(value, localMax ?? dataMax)));
864
- } else if (value === null) {
865
- setLocalMin(null);
866
- }
867
- }, [dataMin, dataMax, localMax]);
868
- const handleMaxInput = useCallback5((event) => {
869
- const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
870
- if (value !== null && !Number.isNaN(value)) {
871
- setLocalMax(Math.min(dataMax, Math.max(value, localMin ?? dataMin)));
872
- } else if (value === null) {
873
- setLocalMax(null);
874
- }
875
- }, [dataMin, dataMax, localMin]);
876
- const handleInputBlur = useCallback5(() => {
877
- if (localMin === null && localMax === null) {
878
- onChange(null);
879
- } else {
880
- onChange({ min: localMin, max: localMax });
881
- }
882
- }, [localMin, localMax, onChange]);
883
- const clearFilter = useCallback5(() => {
884
- setLocalMin(null);
885
- setLocalMax(null);
886
- onChange(null);
887
- }, [onChange]);
888
- const setFullRange = useCallback5(() => {
889
- setLocalMin(dataMin);
890
- setLocalMax(dataMax);
891
- onChange({ min: dataMin, max: dataMax });
892
- }, [dataMin, dataMax, onChange]);
552
+ };
553
+ document.addEventListener("keydown", handleKeydown);
554
+ return () => document.removeEventListener("keydown", handleKeydown);
555
+ }, [onClose, applyFilter]);
893
556
  useEffect3(() => {
894
- setLocalMin(currentRange?.min ?? null);
895
- setLocalMax(currentRange?.max ?? null);
896
- }, [currentRange]);
897
- return /* @__PURE__ */ jsxs("div", { className: "vpg-range-filter", children: [
898
- /* @__PURE__ */ jsxs("div", { className: "vpg-range-info", children: [
899
- /* @__PURE__ */ jsx("span", { className: "vpg-range-label", children: "Data range:" }),
900
- /* @__PURE__ */ jsxs("span", { className: "vpg-range-bounds", children: [
901
- formatValue(dataMin),
902
- " \u2013 ",
903
- formatValue(dataMax)
557
+ searchInputRef.current?.focus();
558
+ }, []);
559
+ useEffect3(() => {
560
+ setLocalSelected(new Set(selectedValues));
561
+ }, [selectedValues]);
562
+ useEffect3(() => {
563
+ setLocalRange(numericRange ?? null);
564
+ if (numericRange) {
565
+ setFilterMode("range");
566
+ }
567
+ }, [numericRange]);
568
+ return /* @__PURE__ */ jsxs3("div", { ref: dropdownRef, className: "vpg-filter-dropdown", children: [
569
+ /* @__PURE__ */ jsxs3("div", { className: "vpg-filter-header", children: [
570
+ /* @__PURE__ */ jsx3("span", { className: "vpg-filter-title", children: columnName }),
571
+ /* @__PURE__ */ jsxs3("span", { className: "vpg-filter-count", children: [
572
+ stats.uniqueValues.length.toLocaleString(),
573
+ " ",
574
+ "unique"
904
575
  ] })
905
576
  ] }),
906
- /* @__PURE__ */ jsxs("div", { className: "vpg-slider-container", children: [
907
- /* @__PURE__ */ jsx("div", { className: "vpg-slider-track", children: /* @__PURE__ */ jsx(
908
- "div",
909
- {
910
- className: "vpg-slider-fill",
911
- style: {
912
- left: `${minPercent}%`,
913
- right: `${100 - maxPercent}%`
914
- }
915
- }
916
- ) }),
917
- /* @__PURE__ */ jsx(
918
- "input",
577
+ /* @__PURE__ */ jsxs3("div", { className: "vpg-sort-controls", children: [
578
+ /* @__PURE__ */ jsxs3(
579
+ "button",
919
580
  {
920
- type: "range",
921
- className: "vpg-slider vpg-slider-min",
922
- min: dataMin,
923
- max: dataMax,
924
- step,
925
- value: localMin ?? dataMin,
926
- onChange: handleMinSlider,
927
- onMouseUp: handleSliderChange,
928
- onTouchEnd: handleSliderChange
581
+ className: `vpg-sort-btn ${sortDirection === "asc" ? "active" : ""}`,
582
+ title: isNumericColumn ? "Sort Low to High" : "Sort A to Z",
583
+ onClick: sortAscending,
584
+ children: [
585
+ /* @__PURE__ */ jsx3("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
586
+ "path",
587
+ {
588
+ strokeLinecap: "round",
589
+ strokeLinejoin: "round",
590
+ strokeWidth: 2,
591
+ d: "M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"
592
+ }
593
+ ) }),
594
+ /* @__PURE__ */ jsx3("span", { children: isNumericColumn ? "1\u21929" : "A\u2192Z" })
595
+ ]
929
596
  }
930
597
  ),
931
- /* @__PURE__ */ jsx(
932
- "input",
598
+ /* @__PURE__ */ jsxs3(
599
+ "button",
933
600
  {
934
- type: "range",
935
- className: "vpg-slider vpg-slider-max",
936
- min: dataMin,
937
- max: dataMax,
938
- step,
939
- value: localMax ?? dataMax,
940
- onChange: handleMaxSlider,
941
- onMouseUp: handleSliderChange,
942
- onTouchEnd: handleSliderChange
601
+ className: `vpg-sort-btn ${sortDirection === "desc" ? "active" : ""}`,
602
+ title: isNumericColumn ? "Sort High to Low" : "Sort Z to A",
603
+ onClick: sortDescending,
604
+ children: [
605
+ /* @__PURE__ */ jsx3("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
606
+ "path",
607
+ {
608
+ strokeLinecap: "round",
609
+ strokeLinejoin: "round",
610
+ strokeWidth: 2,
611
+ d: "M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4"
612
+ }
613
+ ) }),
614
+ /* @__PURE__ */ jsx3("span", { children: isNumericColumn ? "9\u21921" : "Z\u2192A" })
615
+ ]
943
616
  }
944
617
  )
945
618
  ] }),
946
- /* @__PURE__ */ jsxs("div", { className: "vpg-range-inputs", children: [
947
- /* @__PURE__ */ jsxs("div", { className: "vpg-input-group", children: [
948
- /* @__PURE__ */ jsx("label", { className: "vpg-input-label", children: "Min" }),
949
- /* @__PURE__ */ jsx(
950
- "input",
951
- {
952
- type: "number",
953
- className: "vpg-range-input",
954
- placeholder: formatValue(dataMin),
955
- value: localMin ?? "",
956
- step,
957
- onChange: handleMinInput,
958
- onBlur: handleInputBlur
959
- }
960
- )
961
- ] }),
962
- /* @__PURE__ */ jsx("span", { className: "vpg-input-separator", children: "to" }),
963
- /* @__PURE__ */ jsxs("div", { className: "vpg-input-group", children: [
964
- /* @__PURE__ */ jsx("label", { className: "vpg-input-label", children: "Max" }),
965
- /* @__PURE__ */ jsx(
966
- "input",
967
- {
968
- type: "number",
969
- className: "vpg-range-input",
970
- placeholder: formatValue(dataMax),
971
- value: localMax ?? "",
972
- step,
973
- onChange: handleMaxInput,
974
- onBlur: handleInputBlur
975
- }
976
- )
977
- ] })
978
- ] }),
979
- /* @__PURE__ */ jsxs("div", { className: "vpg-range-actions", children: [
980
- /* @__PURE__ */ jsxs(
619
+ /* @__PURE__ */ jsx3("div", { className: "vpg-divider" }),
620
+ isNumericColumn && /* @__PURE__ */ jsxs3("div", { className: "vpg-filter-tabs", children: [
621
+ /* @__PURE__ */ jsxs3(
981
622
  "button",
982
623
  {
983
- className: "vpg-range-btn",
984
- disabled: !isFilterActive,
985
- onClick: clearFilter,
624
+ className: `vpg-tab-btn ${filterMode === "values" ? "active" : ""}`,
625
+ onClick: () => setFilterMode("values"),
986
626
  children: [
987
- /* @__PURE__ */ jsx("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx(
627
+ /* @__PURE__ */ jsx3("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
988
628
  "path",
989
629
  {
990
630
  strokeLinecap: "round",
991
631
  strokeLinejoin: "round",
992
632
  strokeWidth: 2,
993
- d: "M6 18L18 6M6 6l12 12"
633
+ 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"
994
634
  }
995
635
  ) }),
996
- "Clear"
636
+ "Values"
997
637
  ]
998
638
  }
999
639
  ),
1000
- /* @__PURE__ */ jsxs("button", { className: "vpg-range-btn", onClick: setFullRange, children: [
1001
- /* @__PURE__ */ jsx("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx(
640
+ /* @__PURE__ */ jsxs3(
641
+ "button",
642
+ {
643
+ className: `vpg-tab-btn ${filterMode === "range" ? "active" : ""}`,
644
+ onClick: () => setFilterMode("range"),
645
+ children: [
646
+ /* @__PURE__ */ jsx3("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
647
+ "path",
648
+ {
649
+ strokeLinecap: "round",
650
+ strokeLinejoin: "round",
651
+ strokeWidth: 2,
652
+ 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"
653
+ }
654
+ ) }),
655
+ "Range"
656
+ ]
657
+ }
658
+ )
659
+ ] }),
660
+ (!isNumericColumn || filterMode === "values") && /* @__PURE__ */ jsxs3(Fragment, { children: [
661
+ /* @__PURE__ */ jsxs3("div", { className: "vpg-search-container", children: [
662
+ /* @__PURE__ */ jsx3("svg", { className: "vpg-search-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
1002
663
  "path",
1003
664
  {
1004
665
  strokeLinecap: "round",
1005
666
  strokeLinejoin: "round",
1006
667
  strokeWidth: 2,
1007
- d: "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
668
+ d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
1008
669
  }
1009
670
  ) }),
1010
- "Full Range"
1011
- ] })
1012
- ] }),
1013
- isFilterActive && /* @__PURE__ */ jsxs("div", { className: "vpg-filter-summary", children: [
1014
- /* @__PURE__ */ jsx("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx(
1015
- "path",
1016
- {
1017
- strokeLinecap: "round",
1018
- strokeLinejoin: "round",
1019
- strokeWidth: 2,
1020
- 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"
1021
- }
1022
- ) }),
1023
- /* @__PURE__ */ jsxs("span", { children: [
1024
- "Showing values",
1025
- " ",
1026
- localMin !== null && /* @__PURE__ */ jsxs("strong", { children: [
1027
- "\u2265 ",
1028
- formatValue(localMin)
671
+ /* @__PURE__ */ jsx3(
672
+ "input",
673
+ {
674
+ ref: searchInputRef,
675
+ type: "text",
676
+ value: searchQuery,
677
+ onChange: (e) => setSearchQuery(e.target.value),
678
+ placeholder: "Search values...",
679
+ className: "vpg-search-input"
680
+ }
681
+ ),
682
+ searchQuery && /* @__PURE__ */ jsx3("button", { className: "vpg-clear-search", onClick: () => setSearchQuery(""), children: "\xD7" })
683
+ ] }),
684
+ /* @__PURE__ */ jsxs3("div", { className: "vpg-bulk-actions", children: [
685
+ /* @__PURE__ */ jsxs3("button", { className: "vpg-bulk-btn", onClick: selectAll, children: [
686
+ /* @__PURE__ */ jsx3("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
687
+ "path",
688
+ {
689
+ strokeLinecap: "round",
690
+ strokeLinejoin: "round",
691
+ strokeWidth: 2,
692
+ d: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
693
+ }
694
+ ) }),
695
+ "Select All"
1029
696
  ] }),
1030
- localMin !== null && localMax !== null && " and ",
1031
- localMax !== null && /* @__PURE__ */ jsxs("strong", { children: [
1032
- "\u2264 ",
1033
- formatValue(localMax)
1034
- ] })
1035
- ] })
1036
- ] })
1037
- ] });
1038
- }
1039
-
1040
- // src/components/ColumnFilter.tsx
1041
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1042
- function ColumnFilter({
1043
- columnName,
1044
- stats,
1045
- selectedValues,
1046
- sortDirection,
1047
- numericRange,
1048
- onFilter,
1049
- onSort,
1050
- onClose,
1051
- onRangeFilter
1052
- }) {
1053
- const [searchQuery, setSearchQuery] = useState6("");
1054
- const [localSelected, setLocalSelected] = useState6(new Set(selectedValues));
1055
- const dropdownRef = useRef(null);
1056
- const searchInputRef = useRef(null);
1057
- const isNumericColumn = stats.type === "number" && stats.numericMin !== void 0 && stats.numericMax !== void 0;
1058
- const [filterMode, setFilterMode] = useState6(numericRange ? "range" : "values");
1059
- const [localRange, setLocalRange] = useState6(numericRange ?? null);
1060
- const hasBlankValues = stats.nullCount > 0;
1061
- const filteredValues = useMemo6(() => {
1062
- const values = stats.uniqueValues;
1063
- if (!searchQuery) return values;
1064
- const query = searchQuery.toLowerCase();
1065
- return values.filter((v) => v.toLowerCase().includes(query));
1066
- }, [stats.uniqueValues, searchQuery]);
1067
- const allValues = useMemo6(() => {
1068
- const values = [...filteredValues];
1069
- if (hasBlankValues && (!searchQuery || "(blank)".includes(searchQuery.toLowerCase()))) {
1070
- values.unshift("(blank)");
1071
- }
1072
- return values;
1073
- }, [filteredValues, hasBlankValues, searchQuery]);
1074
- const isAllSelected = useMemo6(
1075
- () => allValues.every((v) => localSelected.has(v)),
1076
- [allValues, localSelected]
1077
- );
1078
- const toggleValue = useCallback6((value) => {
1079
- setLocalSelected((prev) => {
1080
- const next = new Set(prev);
1081
- if (next.has(value)) {
1082
- next.delete(value);
1083
- } else {
1084
- next.add(value);
1085
- }
1086
- return next;
1087
- });
1088
- }, []);
1089
- const selectAll = useCallback6(() => {
1090
- setLocalSelected((prev) => {
1091
- const next = new Set(prev);
1092
- for (const value of allValues) {
1093
- next.add(value);
1094
- }
1095
- return next;
1096
- });
1097
- }, [allValues]);
1098
- const clearAll = useCallback6(() => {
1099
- setLocalSelected(/* @__PURE__ */ new Set());
1100
- }, []);
1101
- const applyFilter = useCallback6(() => {
1102
- if (localSelected.size === 0) {
1103
- onFilter([]);
1104
- } else {
1105
- onFilter(Array.from(localSelected));
1106
- }
1107
- onClose();
1108
- }, [localSelected, onFilter, onClose]);
1109
- const sortAscending = useCallback6(() => {
1110
- onSort(sortDirection === "asc" ? null : "asc");
1111
- }, [sortDirection, onSort]);
1112
- const sortDescending = useCallback6(() => {
1113
- onSort(sortDirection === "desc" ? null : "desc");
1114
- }, [sortDirection, onSort]);
1115
- const clearFilter = useCallback6(() => {
1116
- setLocalSelected(/* @__PURE__ */ new Set());
1117
- onFilter([]);
1118
- onClose();
1119
- }, [onFilter, onClose]);
1120
- const handleRangeChange = useCallback6((range) => {
1121
- setLocalRange(range);
1122
- }, []);
1123
- const applyRangeFilter = useCallback6(() => {
1124
- onRangeFilter?.(localRange);
1125
- onClose();
1126
- }, [localRange, onRangeFilter, onClose]);
1127
- const clearRangeFilter = useCallback6(() => {
1128
- setLocalRange(null);
1129
- onRangeFilter?.(null);
1130
- onClose();
1131
- }, [onRangeFilter, onClose]);
1132
- useEffect4(() => {
1133
- const handleClickOutside = (event) => {
1134
- if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
1135
- onClose();
1136
- }
1137
- };
1138
- document.addEventListener("mousedown", handleClickOutside);
1139
- return () => document.removeEventListener("mousedown", handleClickOutside);
1140
- }, [onClose]);
1141
- useEffect4(() => {
1142
- const handleKeydown = (event) => {
1143
- if (event.key === "Escape") {
1144
- onClose();
1145
- } else if (event.key === "Enter" && event.ctrlKey) {
1146
- applyFilter();
1147
- }
1148
- };
1149
- document.addEventListener("keydown", handleKeydown);
1150
- return () => document.removeEventListener("keydown", handleKeydown);
1151
- }, [onClose, applyFilter]);
1152
- useEffect4(() => {
1153
- searchInputRef.current?.focus();
1154
- }, []);
1155
- useEffect4(() => {
1156
- setLocalSelected(new Set(selectedValues));
1157
- }, [selectedValues]);
1158
- useEffect4(() => {
1159
- setLocalRange(numericRange ?? null);
1160
- if (numericRange) {
1161
- setFilterMode("range");
1162
- }
1163
- }, [numericRange]);
1164
- return /* @__PURE__ */ jsxs2("div", { ref: dropdownRef, className: "vpg-filter-dropdown", children: [
1165
- /* @__PURE__ */ jsxs2("div", { className: "vpg-filter-header", children: [
1166
- /* @__PURE__ */ jsx2("span", { className: "vpg-filter-title", children: columnName }),
1167
- /* @__PURE__ */ jsxs2("span", { className: "vpg-filter-count", children: [
1168
- stats.uniqueValues.length.toLocaleString(),
1169
- " unique"
1170
- ] })
1171
- ] }),
1172
- /* @__PURE__ */ jsxs2("div", { className: "vpg-sort-controls", children: [
1173
- /* @__PURE__ */ jsxs2(
1174
- "button",
1175
- {
1176
- className: `vpg-sort-btn ${sortDirection === "asc" ? "active" : ""}`,
1177
- title: isNumericColumn ? "Sort Low to High" : "Sort A to Z",
1178
- onClick: sortAscending,
1179
- children: [
1180
- /* @__PURE__ */ jsx2("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
1181
- "path",
1182
- {
1183
- strokeLinecap: "round",
1184
- strokeLinejoin: "round",
1185
- strokeWidth: 2,
1186
- d: "M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"
1187
- }
1188
- ) }),
1189
- /* @__PURE__ */ jsx2("span", { children: isNumericColumn ? "1\u21929" : "A\u2192Z" })
1190
- ]
1191
- }
1192
- ),
1193
- /* @__PURE__ */ jsxs2(
1194
- "button",
1195
- {
1196
- className: `vpg-sort-btn ${sortDirection === "desc" ? "active" : ""}`,
1197
- title: isNumericColumn ? "Sort High to Low" : "Sort Z to A",
1198
- onClick: sortDescending,
1199
- children: [
1200
- /* @__PURE__ */ jsx2("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
1201
- "path",
1202
- {
1203
- strokeLinecap: "round",
1204
- strokeLinejoin: "round",
1205
- strokeWidth: 2,
1206
- d: "M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4"
1207
- }
1208
- ) }),
1209
- /* @__PURE__ */ jsx2("span", { children: isNumericColumn ? "9\u21921" : "Z\u2192A" })
1210
- ]
1211
- }
1212
- )
1213
- ] }),
1214
- /* @__PURE__ */ jsx2("div", { className: "vpg-divider" }),
1215
- isNumericColumn && /* @__PURE__ */ jsxs2("div", { className: "vpg-filter-tabs", children: [
1216
- /* @__PURE__ */ jsxs2(
1217
- "button",
1218
- {
1219
- className: `vpg-tab-btn ${filterMode === "values" ? "active" : ""}`,
1220
- onClick: () => setFilterMode("values"),
1221
- children: [
1222
- /* @__PURE__ */ jsx2("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
1223
- "path",
1224
- {
1225
- strokeLinecap: "round",
1226
- strokeLinejoin: "round",
1227
- strokeWidth: 2,
1228
- 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"
1229
- }
1230
- ) }),
1231
- "Values"
1232
- ]
1233
- }
1234
- ),
1235
- /* @__PURE__ */ jsxs2(
1236
- "button",
1237
- {
1238
- className: `vpg-tab-btn ${filterMode === "range" ? "active" : ""}`,
1239
- onClick: () => setFilterMode("range"),
1240
- children: [
1241
- /* @__PURE__ */ jsx2("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
1242
- "path",
1243
- {
1244
- strokeLinecap: "round",
1245
- strokeLinejoin: "round",
1246
- strokeWidth: 2,
1247
- 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"
1248
- }
1249
- ) }),
1250
- "Range"
1251
- ]
1252
- }
1253
- )
1254
- ] }),
1255
- (!isNumericColumn || filterMode === "values") && /* @__PURE__ */ jsxs2(Fragment, { children: [
1256
- /* @__PURE__ */ jsxs2("div", { className: "vpg-search-container", children: [
1257
- /* @__PURE__ */ jsx2("svg", { className: "vpg-search-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
1258
- "path",
1259
- {
1260
- strokeLinecap: "round",
1261
- strokeLinejoin: "round",
1262
- strokeWidth: 2,
1263
- d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
1264
- }
1265
- ) }),
1266
- /* @__PURE__ */ jsx2(
1267
- "input",
1268
- {
1269
- ref: searchInputRef,
1270
- type: "text",
1271
- value: searchQuery,
1272
- onChange: (e) => setSearchQuery(e.target.value),
1273
- placeholder: "Search values...",
1274
- className: "vpg-search-input"
1275
- }
1276
- ),
1277
- searchQuery && /* @__PURE__ */ jsx2("button", { className: "vpg-clear-search", onClick: () => setSearchQuery(""), children: "\xD7" })
1278
- ] }),
1279
- /* @__PURE__ */ jsxs2("div", { className: "vpg-bulk-actions", children: [
1280
- /* @__PURE__ */ jsxs2("button", { className: "vpg-bulk-btn", onClick: selectAll, children: [
1281
- /* @__PURE__ */ jsx2("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
1282
- "path",
1283
- {
1284
- strokeLinecap: "round",
1285
- strokeLinejoin: "round",
1286
- strokeWidth: 2,
1287
- d: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
1288
- }
1289
- ) }),
1290
- "Select All"
1291
- ] }),
1292
- /* @__PURE__ */ jsxs2("button", { className: "vpg-bulk-btn", onClick: clearAll, children: [
1293
- /* @__PURE__ */ jsx2("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
697
+ /* @__PURE__ */ jsxs3("button", { className: "vpg-bulk-btn", onClick: clearAll, children: [
698
+ /* @__PURE__ */ jsx3("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
1294
699
  "path",
1295
700
  {
1296
701
  strokeLinecap: "round",
@@ -1302,13 +707,13 @@ function ColumnFilter({
1302
707
  "Clear All"
1303
708
  ] })
1304
709
  ] }),
1305
- /* @__PURE__ */ jsxs2("div", { className: "vpg-values-list", children: [
1306
- allValues.map((value) => /* @__PURE__ */ jsxs2(
710
+ /* @__PURE__ */ jsxs3("div", { className: "vpg-values-list", children: [
711
+ allValues.map((value) => /* @__PURE__ */ jsxs3(
1307
712
  "label",
1308
713
  {
1309
714
  className: `vpg-value-item ${localSelected.has(value) ? "selected" : ""}`,
1310
715
  children: [
1311
- /* @__PURE__ */ jsx2(
716
+ /* @__PURE__ */ jsx3(
1312
717
  "input",
1313
718
  {
1314
719
  type: "checkbox",
@@ -1317,20 +722,20 @@ function ColumnFilter({
1317
722
  className: "vpg-value-checkbox"
1318
723
  }
1319
724
  ),
1320
- /* @__PURE__ */ jsx2("span", { className: `vpg-value-text ${value === "(blank)" ? "vpg-blank" : ""}`, children: value })
725
+ /* @__PURE__ */ jsx3("span", { className: `vpg-value-text ${value === "(blank)" ? "vpg-blank" : ""}`, children: value })
1321
726
  ]
1322
727
  },
1323
728
  value
1324
729
  )),
1325
- allValues.length === 0 && /* @__PURE__ */ jsx2("div", { className: "vpg-no-results", children: "No matching values" })
730
+ allValues.length === 0 && /* @__PURE__ */ jsx3("div", { className: "vpg-no-results", children: "No matching values" })
1326
731
  ] }),
1327
- /* @__PURE__ */ jsxs2("div", { className: "vpg-filter-footer", children: [
1328
- /* @__PURE__ */ jsx2("button", { className: "vpg-btn-clear", onClick: clearFilter, children: "Clear Filter" }),
1329
- /* @__PURE__ */ jsx2("button", { className: "vpg-btn-apply", onClick: applyFilter, children: "Apply" })
732
+ /* @__PURE__ */ jsxs3("div", { className: "vpg-filter-footer", children: [
733
+ /* @__PURE__ */ jsx3("button", { className: "vpg-btn-clear", onClick: clearFilter, children: "Clear Filter" }),
734
+ /* @__PURE__ */ jsx3("button", { className: "vpg-btn-apply", onClick: applyFilter, children: "Apply" })
1330
735
  ] })
1331
736
  ] }),
1332
- isNumericColumn && filterMode === "range" && /* @__PURE__ */ jsxs2(Fragment, { children: [
1333
- /* @__PURE__ */ jsx2(
737
+ isNumericColumn && filterMode === "range" && /* @__PURE__ */ jsxs3(Fragment, { children: [
738
+ /* @__PURE__ */ jsx3(
1334
739
  NumericRangeFilter,
1335
740
  {
1336
741
  dataMin: stats.numericMin,
@@ -1339,210 +744,836 @@ function ColumnFilter({
1339
744
  onChange: handleRangeChange
1340
745
  }
1341
746
  ),
1342
- /* @__PURE__ */ jsxs2("div", { className: "vpg-filter-footer", children: [
1343
- /* @__PURE__ */ jsx2("button", { className: "vpg-btn-clear", onClick: clearRangeFilter, children: "Clear Filter" }),
1344
- /* @__PURE__ */ jsx2("button", { className: "vpg-btn-apply", onClick: applyRangeFilter, children: "Apply" })
747
+ /* @__PURE__ */ jsxs3("div", { className: "vpg-filter-footer", children: [
748
+ /* @__PURE__ */ jsx3("button", { className: "vpg-btn-clear", onClick: clearRangeFilter, children: "Clear Filter" }),
749
+ /* @__PURE__ */ jsx3("button", { className: "vpg-btn-apply", onClick: applyRangeFilter, children: "Apply" })
1345
750
  ] })
1346
751
  ] })
1347
752
  ] });
1348
753
  }
1349
754
 
1350
- // src/components/PivotConfig.tsx
1351
- import { useState as useState8, useMemo as useMemo8, useCallback as useCallback8 } from "react";
1352
- import { AGGREGATION_OPTIONS, getAggregationSymbol } from "@smallwebco/tinypivot-core";
755
+ // src/components/DataGrid.tsx
756
+ import { useCallback as useCallback10, useEffect as useEffect7, useMemo as useMemo10, useRef as useRef2, useState as useState10 } from "react";
757
+ import { createPortal as createPortal2 } from "react-dom";
1353
758
 
1354
- // src/components/CalculatedFieldModal.tsx
1355
- import { useState as useState7, useEffect as useEffect5, useMemo as useMemo7, useCallback as useCallback7 } from "react";
1356
- import { createPortal } from "react-dom";
1357
- import { validateSimpleFormula } from "@smallwebco/tinypivot-core";
1358
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1359
- function CalculatedFieldModal({
1360
- show,
1361
- availableFields,
1362
- existingField,
1363
- onClose,
1364
- onSave
1365
- }) {
1366
- const [name, setName] = useState7("");
1367
- const [formula, setFormula] = useState7("");
1368
- const [formatAs, setFormatAs] = useState7("number");
1369
- const [decimals, setDecimals] = useState7(2);
1370
- const [error, setError] = useState7(null);
1371
- useEffect5(() => {
1372
- if (show) {
1373
- if (existingField) {
1374
- setName(existingField.name);
1375
- setFormula(existingField.formula);
1376
- setFormatAs(existingField.formatAs || "number");
1377
- setDecimals(existingField.decimals ?? 2);
759
+ // src/hooks/useExcelGrid.ts
760
+ import { formatCellValue, getColumnUniqueValues, isNumericRange } from "@smallwebco/tinypivot-core";
761
+ import {
762
+ getCoreRowModel,
763
+ getFilteredRowModel,
764
+ getSortedRowModel,
765
+ useReactTable
766
+ } from "@tanstack/react-table";
767
+ import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo4, useState as useState4 } from "react";
768
+ var multiSelectFilter = (row, columnId, filterValue) => {
769
+ if (!filterValue)
770
+ return true;
771
+ if (isNumericRange(filterValue)) {
772
+ const cellValue = row.getValue(columnId);
773
+ if (cellValue === null || cellValue === void 0 || cellValue === "") {
774
+ return false;
775
+ }
776
+ const num = typeof cellValue === "number" ? cellValue : Number.parseFloat(String(cellValue));
777
+ if (Number.isNaN(num))
778
+ return false;
779
+ const { min, max } = filterValue;
780
+ if (min !== null && num < min)
781
+ return false;
782
+ if (max !== null && num > max)
783
+ return false;
784
+ return true;
785
+ }
786
+ if (Array.isArray(filterValue) && filterValue.length > 0) {
787
+ const cellValue = row.getValue(columnId);
788
+ const cellString = cellValue === null || cellValue === void 0 || cellValue === "" ? "(blank)" : String(cellValue);
789
+ return filterValue.includes(cellString);
790
+ }
791
+ return true;
792
+ };
793
+ function useExcelGrid(options) {
794
+ const { data, enableSorting = true, enableFiltering = true } = options;
795
+ const [sorting, setSorting] = useState4([]);
796
+ const [columnFilters, setColumnFilters] = useState4([]);
797
+ const [columnVisibility, setColumnVisibility] = useState4({});
798
+ const [globalFilter, setGlobalFilter] = useState4("");
799
+ const [columnStatsCache, setColumnStatsCache] = useState4({});
800
+ const dataSignature = useMemo4(
801
+ () => `${Date.now()}-${Math.random().toString(36).slice(2)}`,
802
+ [data]
803
+ );
804
+ const columnKeys = useMemo4(() => {
805
+ if (data.length === 0)
806
+ return [];
807
+ return Object.keys(data[0]);
808
+ }, [data]);
809
+ const getColumnStats = useCallback4(
810
+ (columnKey) => {
811
+ const cacheKey = `${columnKey}-${dataSignature}`;
812
+ if (!columnStatsCache[cacheKey]) {
813
+ const stats = getColumnUniqueValues(data, columnKey);
814
+ setColumnStatsCache((prev) => ({ ...prev, [cacheKey]: stats }));
815
+ return stats;
816
+ }
817
+ return columnStatsCache[cacheKey];
818
+ },
819
+ [data, columnStatsCache, dataSignature]
820
+ );
821
+ const clearStatsCache = useCallback4(() => {
822
+ setColumnStatsCache({});
823
+ }, []);
824
+ useEffect4(() => {
825
+ clearStatsCache();
826
+ }, [dataSignature, clearStatsCache]);
827
+ const columnDefs = useMemo4(() => {
828
+ return columnKeys.map((key) => {
829
+ const stats = getColumnStats(key);
830
+ return {
831
+ id: key,
832
+ accessorKey: key,
833
+ header: key,
834
+ cell: (info) => formatCellValue(info.getValue(), stats.type),
835
+ filterFn: multiSelectFilter,
836
+ meta: {
837
+ type: stats.type,
838
+ uniqueCount: stats.uniqueValues.length
839
+ }
840
+ };
841
+ });
842
+ }, [columnKeys, getColumnStats]);
843
+ const table = useReactTable({
844
+ data,
845
+ columns: columnDefs,
846
+ state: {
847
+ sorting,
848
+ columnFilters,
849
+ columnVisibility,
850
+ globalFilter
851
+ },
852
+ onSortingChange: setSorting,
853
+ onColumnFiltersChange: setColumnFilters,
854
+ onColumnVisibilityChange: setColumnVisibility,
855
+ onGlobalFilterChange: setGlobalFilter,
856
+ getCoreRowModel: getCoreRowModel(),
857
+ getSortedRowModel: enableSorting ? getSortedRowModel() : void 0,
858
+ getFilteredRowModel: enableFiltering ? getFilteredRowModel() : void 0,
859
+ filterFns: {
860
+ multiSelect: multiSelectFilter
861
+ },
862
+ enableSorting,
863
+ enableFilters: enableFiltering
864
+ });
865
+ const filteredRowCount = table.getFilteredRowModel().rows.length;
866
+ const totalRowCount = data.length;
867
+ const activeFilters = useMemo4(() => {
868
+ return columnFilters.map((f) => {
869
+ const filterValue = f.value;
870
+ if (filterValue && isNumericRange(filterValue)) {
871
+ return {
872
+ column: f.id,
873
+ type: "range",
874
+ range: filterValue,
875
+ values: []
876
+ };
877
+ }
878
+ return {
879
+ column: f.id,
880
+ type: "values",
881
+ values: Array.isArray(filterValue) ? filterValue : [],
882
+ range: null
883
+ };
884
+ });
885
+ }, [columnFilters]);
886
+ const hasActiveFilter = useCallback4(
887
+ (columnId) => {
888
+ const column = table.getColumn(columnId);
889
+ if (!column)
890
+ return false;
891
+ const filterValue = column.getFilterValue();
892
+ if (!filterValue)
893
+ return false;
894
+ if (isNumericRange(filterValue)) {
895
+ return filterValue.min !== null || filterValue.max !== null;
896
+ }
897
+ return Array.isArray(filterValue) && filterValue.length > 0;
898
+ },
899
+ [table]
900
+ );
901
+ const setColumnFilter = useCallback4(
902
+ (columnId, values) => {
903
+ const column = table.getColumn(columnId);
904
+ if (column) {
905
+ column.setFilterValue(values.length === 0 ? void 0 : values);
906
+ }
907
+ },
908
+ [table]
909
+ );
910
+ const setNumericRangeFilter = useCallback4(
911
+ (columnId, range) => {
912
+ const column = table.getColumn(columnId);
913
+ if (column) {
914
+ if (!range || range.min === null && range.max === null) {
915
+ column.setFilterValue(void 0);
916
+ } else {
917
+ column.setFilterValue(range);
918
+ }
919
+ }
920
+ },
921
+ [table]
922
+ );
923
+ const getNumericRangeFilter = useCallback4(
924
+ (columnId) => {
925
+ const column = table.getColumn(columnId);
926
+ if (!column)
927
+ return null;
928
+ const filterValue = column.getFilterValue();
929
+ if (filterValue && isNumericRange(filterValue)) {
930
+ return filterValue;
931
+ }
932
+ return null;
933
+ },
934
+ [table]
935
+ );
936
+ const clearAllFilters = useCallback4(() => {
937
+ table.resetColumnFilters();
938
+ setGlobalFilter("");
939
+ setColumnFilters([]);
940
+ }, [table]);
941
+ const getColumnFilterValues = useCallback4(
942
+ (columnId) => {
943
+ const column = table.getColumn(columnId);
944
+ if (!column)
945
+ return [];
946
+ const filterValue = column.getFilterValue();
947
+ return Array.isArray(filterValue) ? filterValue : [];
948
+ },
949
+ [table]
950
+ );
951
+ const toggleSort = useCallback4((columnId) => {
952
+ setSorting((prev) => {
953
+ const current = prev.find((s) => s.id === columnId);
954
+ if (!current) {
955
+ return [{ id: columnId, desc: false }];
956
+ } else if (!current.desc) {
957
+ return [{ id: columnId, desc: true }];
958
+ } else {
959
+ return [];
960
+ }
961
+ });
962
+ }, []);
963
+ const getSortDirection = useCallback4(
964
+ (columnId) => {
965
+ const sort = sorting.find((s) => s.id === columnId);
966
+ if (!sort)
967
+ return null;
968
+ return sort.desc ? "desc" : "asc";
969
+ },
970
+ [sorting]
971
+ );
972
+ return {
973
+ // Table instance
974
+ table,
975
+ // State
976
+ sorting,
977
+ columnFilters,
978
+ columnVisibility,
979
+ globalFilter,
980
+ columnKeys,
981
+ setSorting,
982
+ setColumnFilters,
983
+ setGlobalFilter,
984
+ // Computed
985
+ filteredRowCount,
986
+ totalRowCount,
987
+ activeFilters,
988
+ // Methods
989
+ getColumnStats,
990
+ clearStatsCache,
991
+ hasActiveFilter,
992
+ setColumnFilter,
993
+ getColumnFilterValues,
994
+ clearAllFilters,
995
+ toggleSort,
996
+ getSortDirection,
997
+ // Numeric range filters
998
+ setNumericRangeFilter,
999
+ getNumericRangeFilter
1000
+ };
1001
+ }
1002
+
1003
+ // src/hooks/useGridFeatures.ts
1004
+ import {
1005
+ copyToClipboard as coreCopyToClipboard,
1006
+ exportPivotToCSV as coreExportPivotToCSV,
1007
+ exportToCSV as coreExportToCSV,
1008
+ formatSelectionForClipboard as coreFormatSelection
1009
+ } from "@smallwebco/tinypivot-core";
1010
+ import { useCallback as useCallback5, useMemo as useMemo5, useState as useState5 } from "react";
1011
+ function exportToCSV(data, columns, options) {
1012
+ coreExportToCSV(data, columns, options);
1013
+ }
1014
+ function exportPivotToCSV(pivotData, rowFields, columnFields, valueFields, options) {
1015
+ coreExportPivotToCSV(pivotData, rowFields, columnFields, valueFields, options);
1016
+ }
1017
+ function copyToClipboard(text, onSuccess, onError) {
1018
+ coreCopyToClipboard(text, onSuccess, onError);
1019
+ }
1020
+ function formatSelectionForClipboard(rows, columns, selectionBounds) {
1021
+ return coreFormatSelection(rows, columns, selectionBounds);
1022
+ }
1023
+ function usePagination(data, options = {}) {
1024
+ const [pageSize, setPageSize] = useState5(options.pageSize ?? 50);
1025
+ const [currentPage, setCurrentPage] = useState5(options.currentPage ?? 1);
1026
+ const totalPages = useMemo5(
1027
+ () => Math.max(1, Math.ceil(data.length / pageSize)),
1028
+ [data.length, pageSize]
1029
+ );
1030
+ const paginatedData = useMemo5(() => {
1031
+ const start = (currentPage - 1) * pageSize;
1032
+ const end = start + pageSize;
1033
+ return data.slice(start, end);
1034
+ }, [data, currentPage, pageSize]);
1035
+ const startIndex = useMemo5(() => (currentPage - 1) * pageSize + 1, [currentPage, pageSize]);
1036
+ const endIndex = useMemo5(
1037
+ () => Math.min(currentPage * pageSize, data.length),
1038
+ [currentPage, pageSize, data.length]
1039
+ );
1040
+ const goToPage = useCallback5(
1041
+ (page) => {
1042
+ setCurrentPage(Math.max(1, Math.min(page, totalPages)));
1043
+ },
1044
+ [totalPages]
1045
+ );
1046
+ const nextPage = useCallback5(() => {
1047
+ if (currentPage < totalPages) {
1048
+ setCurrentPage((prev) => prev + 1);
1049
+ }
1050
+ }, [currentPage, totalPages]);
1051
+ const prevPage = useCallback5(() => {
1052
+ if (currentPage > 1) {
1053
+ setCurrentPage((prev) => prev - 1);
1054
+ }
1055
+ }, [currentPage]);
1056
+ const firstPage = useCallback5(() => {
1057
+ setCurrentPage(1);
1058
+ }, []);
1059
+ const lastPage = useCallback5(() => {
1060
+ setCurrentPage(totalPages);
1061
+ }, [totalPages]);
1062
+ const updatePageSize = useCallback5((size) => {
1063
+ setPageSize(size);
1064
+ setCurrentPage(1);
1065
+ }, []);
1066
+ return {
1067
+ pageSize,
1068
+ currentPage,
1069
+ totalPages,
1070
+ paginatedData,
1071
+ startIndex,
1072
+ endIndex,
1073
+ goToPage,
1074
+ nextPage,
1075
+ prevPage,
1076
+ firstPage,
1077
+ lastPage,
1078
+ setPageSize: updatePageSize
1079
+ };
1080
+ }
1081
+ function useGlobalSearch(data, columns) {
1082
+ const [searchTerm, setSearchTerm] = useState5("");
1083
+ const [caseSensitive, setCaseSensitive] = useState5(false);
1084
+ const filteredData = useMemo5(() => {
1085
+ if (!searchTerm.trim()) {
1086
+ return data;
1087
+ }
1088
+ const term = caseSensitive ? searchTerm.trim() : searchTerm.trim().toLowerCase();
1089
+ return data.filter((row) => {
1090
+ for (const col of columns) {
1091
+ const value = row[col];
1092
+ if (value === null || value === void 0)
1093
+ continue;
1094
+ const strValue = caseSensitive ? String(value) : String(value).toLowerCase();
1095
+ if (strValue.includes(term)) {
1096
+ return true;
1097
+ }
1098
+ }
1099
+ return false;
1100
+ });
1101
+ }, [data, columns, searchTerm, caseSensitive]);
1102
+ const clearSearch = useCallback5(() => {
1103
+ setSearchTerm("");
1104
+ }, []);
1105
+ return {
1106
+ searchTerm,
1107
+ setSearchTerm,
1108
+ caseSensitive,
1109
+ setCaseSensitive,
1110
+ filteredData,
1111
+ clearSearch
1112
+ };
1113
+ }
1114
+ function useRowSelection(data) {
1115
+ const [selectedRowIndices, setSelectedRowIndices] = useState5(/* @__PURE__ */ new Set());
1116
+ const selectedRows = useMemo5(() => {
1117
+ return Array.from(selectedRowIndices).sort((a, b) => a - b).map((idx) => data[idx]).filter(Boolean);
1118
+ }, [data, selectedRowIndices]);
1119
+ const allSelected = useMemo5(() => {
1120
+ return data.length > 0 && selectedRowIndices.size === data.length;
1121
+ }, [data.length, selectedRowIndices.size]);
1122
+ const someSelected = useMemo5(() => {
1123
+ return selectedRowIndices.size > 0 && selectedRowIndices.size < data.length;
1124
+ }, [data.length, selectedRowIndices.size]);
1125
+ const toggleRow = useCallback5((index) => {
1126
+ setSelectedRowIndices((prev) => {
1127
+ const next = new Set(prev);
1128
+ if (next.has(index)) {
1129
+ next.delete(index);
1130
+ } else {
1131
+ next.add(index);
1132
+ }
1133
+ return next;
1134
+ });
1135
+ }, []);
1136
+ const selectRow = useCallback5((index) => {
1137
+ setSelectedRowIndices((prev) => /* @__PURE__ */ new Set([...prev, index]));
1138
+ }, []);
1139
+ const deselectRow = useCallback5((index) => {
1140
+ setSelectedRowIndices((prev) => {
1141
+ const next = new Set(prev);
1142
+ next.delete(index);
1143
+ return next;
1144
+ });
1145
+ }, []);
1146
+ const selectAll = useCallback5(() => {
1147
+ setSelectedRowIndices(new Set(data.map((_, idx) => idx)));
1148
+ }, [data]);
1149
+ const deselectAll = useCallback5(() => {
1150
+ setSelectedRowIndices(/* @__PURE__ */ new Set());
1151
+ }, []);
1152
+ const toggleAll = useCallback5(() => {
1153
+ if (allSelected) {
1154
+ deselectAll();
1155
+ } else {
1156
+ selectAll();
1157
+ }
1158
+ }, [allSelected, selectAll, deselectAll]);
1159
+ const isSelected = useCallback5(
1160
+ (index) => {
1161
+ return selectedRowIndices.has(index);
1162
+ },
1163
+ [selectedRowIndices]
1164
+ );
1165
+ const selectRange = useCallback5((startIndex, endIndex) => {
1166
+ const min = Math.min(startIndex, endIndex);
1167
+ const max = Math.max(startIndex, endIndex);
1168
+ setSelectedRowIndices((prev) => {
1169
+ const next = new Set(prev);
1170
+ for (let i = min; i <= max; i++) {
1171
+ next.add(i);
1172
+ }
1173
+ return next;
1174
+ });
1175
+ }, []);
1176
+ return {
1177
+ selectedRowIndices,
1178
+ selectedRows,
1179
+ allSelected,
1180
+ someSelected,
1181
+ toggleRow,
1182
+ selectRow,
1183
+ deselectRow,
1184
+ selectAll,
1185
+ deselectAll,
1186
+ toggleAll,
1187
+ isSelected,
1188
+ selectRange
1189
+ };
1190
+ }
1191
+ function useColumnResize(initialWidths, minWidth = 60, maxWidth = 600) {
1192
+ const [columnWidths, setColumnWidths] = useState5({ ...initialWidths });
1193
+ const [isResizing, setIsResizing] = useState5(false);
1194
+ const [resizingColumn, setResizingColumn] = useState5(null);
1195
+ const startResize = useCallback5(
1196
+ (columnId, event) => {
1197
+ setIsResizing(true);
1198
+ setResizingColumn(columnId);
1199
+ const startX = event.clientX;
1200
+ const startWidth = columnWidths[columnId] || 150;
1201
+ const handleMouseMove = (e) => {
1202
+ const diff = e.clientX - startX;
1203
+ const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + diff));
1204
+ setColumnWidths((prev) => ({
1205
+ ...prev,
1206
+ [columnId]: newWidth
1207
+ }));
1208
+ };
1209
+ const handleMouseUp = () => {
1210
+ setIsResizing(false);
1211
+ setResizingColumn(null);
1212
+ document.removeEventListener("mousemove", handleMouseMove);
1213
+ document.removeEventListener("mouseup", handleMouseUp);
1214
+ };
1215
+ document.addEventListener("mousemove", handleMouseMove);
1216
+ document.addEventListener("mouseup", handleMouseUp);
1217
+ },
1218
+ [columnWidths, minWidth, maxWidth]
1219
+ );
1220
+ const resetColumnWidth = useCallback5(
1221
+ (columnId) => {
1222
+ if (initialWidths[columnId]) {
1223
+ setColumnWidths((prev) => ({
1224
+ ...prev,
1225
+ [columnId]: initialWidths[columnId]
1226
+ }));
1227
+ }
1228
+ },
1229
+ [initialWidths]
1230
+ );
1231
+ const resetAllWidths = useCallback5(() => {
1232
+ setColumnWidths({ ...initialWidths });
1233
+ }, [initialWidths]);
1234
+ return {
1235
+ columnWidths,
1236
+ setColumnWidths,
1237
+ isResizing,
1238
+ resizingColumn,
1239
+ startResize,
1240
+ resetColumnWidth,
1241
+ resetAllWidths
1242
+ };
1243
+ }
1244
+
1245
+ // src/hooks/useLicense.ts
1246
+ import {
1247
+ canUsePivot as coreCanUsePivot,
1248
+ configureLicenseSecret as coreConfigureLicenseSecret,
1249
+ isPro as coreIsPro,
1250
+ shouldShowWatermark as coreShouldShowWatermark,
1251
+ getDemoLicenseInfo,
1252
+ getFreeLicenseInfo,
1253
+ logProRequired,
1254
+ validateLicenseKey
1255
+ } from "@smallwebco/tinypivot-core";
1256
+ import { useCallback as useCallback6, useMemo as useMemo6, useState as useState6 } from "react";
1257
+ var globalLicenseInfo = getFreeLicenseInfo();
1258
+ var globalDemoMode = false;
1259
+ var listeners = /* @__PURE__ */ new Set();
1260
+ function notifyListeners() {
1261
+ listeners.forEach((listener) => listener());
1262
+ }
1263
+ async function setLicenseKey(key) {
1264
+ globalLicenseInfo = await validateLicenseKey(key);
1265
+ if (!globalLicenseInfo.isValid) {
1266
+ console.warn("[TinyPivot] Invalid or expired license key. Running in free mode.");
1267
+ } else if (globalLicenseInfo.type !== "free") {
1268
+ console.info(`[TinyPivot] Pro license activated (${globalLicenseInfo.type})`);
1269
+ }
1270
+ notifyListeners();
1271
+ }
1272
+ async function enableDemoMode(secret) {
1273
+ const demoLicense = await getDemoLicenseInfo(secret);
1274
+ if (!demoLicense) {
1275
+ console.warn("[TinyPivot] Demo mode activation failed - invalid secret");
1276
+ return false;
1277
+ }
1278
+ globalDemoMode = true;
1279
+ globalLicenseInfo = demoLicense;
1280
+ console.info("[TinyPivot] Demo mode enabled - all Pro features unlocked for evaluation");
1281
+ notifyListeners();
1282
+ return true;
1283
+ }
1284
+ function configureLicenseSecret(secret) {
1285
+ coreConfigureLicenseSecret(secret);
1286
+ }
1287
+ function useLicense() {
1288
+ const [, forceUpdate] = useState6({});
1289
+ useState6(() => {
1290
+ const update = () => forceUpdate({});
1291
+ listeners.add(update);
1292
+ return () => listeners.delete(update);
1293
+ });
1294
+ const isDemo = globalDemoMode;
1295
+ const licenseInfo = globalLicenseInfo;
1296
+ const isPro = useMemo6(
1297
+ () => globalDemoMode || coreIsPro(licenseInfo),
1298
+ [licenseInfo]
1299
+ );
1300
+ const canUsePivot = useMemo6(
1301
+ () => globalDemoMode || coreCanUsePivot(licenseInfo),
1302
+ [licenseInfo]
1303
+ );
1304
+ const canUseAdvancedAggregations = useMemo6(
1305
+ () => globalDemoMode || licenseInfo.features.advancedAggregations,
1306
+ [licenseInfo]
1307
+ );
1308
+ const canUsePercentageMode = useMemo6(
1309
+ () => globalDemoMode || licenseInfo.features.percentageMode,
1310
+ [licenseInfo]
1311
+ );
1312
+ const showWatermark = useMemo6(
1313
+ () => coreShouldShowWatermark(licenseInfo, globalDemoMode),
1314
+ [licenseInfo]
1315
+ );
1316
+ const requirePro = useCallback6((feature) => {
1317
+ if (!isPro) {
1318
+ logProRequired(feature);
1319
+ return false;
1320
+ }
1321
+ return true;
1322
+ }, [isPro]);
1323
+ return {
1324
+ licenseInfo,
1325
+ isDemo,
1326
+ isPro,
1327
+ canUsePivot,
1328
+ canUseAdvancedAggregations,
1329
+ canUsePercentageMode,
1330
+ showWatermark,
1331
+ requirePro
1332
+ };
1333
+ }
1334
+
1335
+ // src/hooks/usePivotTable.ts
1336
+ import {
1337
+ computeAvailableFields,
1338
+ computePivotResult,
1339
+ generateStorageKey,
1340
+ getAggregationLabel,
1341
+ getUnassignedFields,
1342
+ isConfigValidForFields,
1343
+ isPivotConfigured,
1344
+ loadCalculatedFields,
1345
+ loadPivotConfig,
1346
+ saveCalculatedFields,
1347
+ savePivotConfig
1348
+ } from "@smallwebco/tinypivot-core";
1349
+ import { useCallback as useCallback7, useEffect as useEffect5, useMemo as useMemo7, useState as useState7 } from "react";
1350
+ function usePivotTable(data) {
1351
+ const { canUsePivot, requirePro } = useLicense();
1352
+ const [rowFields, setRowFieldsState] = useState7([]);
1353
+ const [columnFields, setColumnFieldsState] = useState7([]);
1354
+ const [valueFields, setValueFields] = useState7([]);
1355
+ const [showRowTotals, setShowRowTotals] = useState7(true);
1356
+ const [showColumnTotals, setShowColumnTotals] = useState7(true);
1357
+ const [calculatedFields, setCalculatedFields] = useState7(() => loadCalculatedFields());
1358
+ const [currentStorageKey, setCurrentStorageKey] = useState7(null);
1359
+ const availableFields = useMemo7(() => {
1360
+ return computeAvailableFields(data);
1361
+ }, [data]);
1362
+ const unassignedFields = useMemo7(() => {
1363
+ return getUnassignedFields(availableFields, rowFields, columnFields, valueFields);
1364
+ }, [availableFields, rowFields, columnFields, valueFields]);
1365
+ const isConfigured = useMemo7(() => {
1366
+ return isPivotConfigured({
1367
+ rowFields,
1368
+ columnFields,
1369
+ valueFields,
1370
+ showRowTotals,
1371
+ showColumnTotals
1372
+ });
1373
+ }, [rowFields, columnFields, valueFields, showRowTotals, showColumnTotals]);
1374
+ const pivotResult = useMemo7(() => {
1375
+ if (!isConfigured)
1376
+ return null;
1377
+ if (!canUsePivot)
1378
+ return null;
1379
+ return computePivotResult(data, {
1380
+ rowFields,
1381
+ columnFields,
1382
+ valueFields,
1383
+ showRowTotals,
1384
+ showColumnTotals,
1385
+ calculatedFields
1386
+ });
1387
+ }, [data, isConfigured, canUsePivot, rowFields, columnFields, valueFields, showRowTotals, showColumnTotals, calculatedFields]);
1388
+ useEffect5(() => {
1389
+ if (data.length === 0)
1390
+ return;
1391
+ const newKeys = Object.keys(data[0]);
1392
+ const storageKey = generateStorageKey(newKeys);
1393
+ if (storageKey !== currentStorageKey) {
1394
+ setCurrentStorageKey(storageKey);
1395
+ const savedConfig = loadPivotConfig(storageKey);
1396
+ if (savedConfig && isConfigValidForFields(savedConfig, newKeys)) {
1397
+ setRowFieldsState(savedConfig.rowFields);
1398
+ setColumnFieldsState(savedConfig.columnFields);
1399
+ setValueFields(savedConfig.valueFields);
1400
+ setShowRowTotals(savedConfig.showRowTotals);
1401
+ setShowColumnTotals(savedConfig.showColumnTotals);
1402
+ if (savedConfig.calculatedFields) {
1403
+ setCalculatedFields(savedConfig.calculatedFields);
1404
+ }
1378
1405
  } else {
1379
- setName("");
1380
- setFormula("");
1381
- setFormatAs("number");
1382
- setDecimals(2);
1406
+ const currentConfig = {
1407
+ rowFields,
1408
+ columnFields,
1409
+ valueFields,
1410
+ showRowTotals,
1411
+ showColumnTotals
1412
+ };
1413
+ if (!isConfigValidForFields(currentConfig, newKeys)) {
1414
+ setRowFieldsState([]);
1415
+ setColumnFieldsState([]);
1416
+ setValueFields([]);
1417
+ }
1383
1418
  }
1384
- setError(null);
1385
1419
  }
1386
- }, [show, existingField]);
1387
- const validationError = useMemo7(() => {
1388
- if (!formula.trim()) return null;
1389
- return validateSimpleFormula(formula, availableFields);
1390
- }, [formula, availableFields]);
1391
- const insertField = useCallback7((field) => {
1392
- setFormula((prev) => {
1393
- if (prev.trim() && !prev.endsWith(" ")) {
1394
- return prev + " " + field;
1420
+ }, [data]);
1421
+ useEffect5(() => {
1422
+ if (!currentStorageKey)
1423
+ return;
1424
+ const config = {
1425
+ rowFields,
1426
+ columnFields,
1427
+ valueFields,
1428
+ showRowTotals,
1429
+ showColumnTotals,
1430
+ calculatedFields
1431
+ };
1432
+ savePivotConfig(currentStorageKey, config);
1433
+ }, [currentStorageKey, rowFields, columnFields, valueFields, showRowTotals, showColumnTotals, calculatedFields]);
1434
+ const addRowField = useCallback7(
1435
+ (field) => {
1436
+ if (!rowFields.includes(field)) {
1437
+ setRowFieldsState((prev) => [...prev, field]);
1395
1438
  }
1396
- return prev + field;
1397
- });
1439
+ },
1440
+ [rowFields]
1441
+ );
1442
+ const removeRowField = useCallback7((field) => {
1443
+ setRowFieldsState((prev) => prev.filter((f) => f !== field));
1398
1444
  }, []);
1399
- const insertOperator = useCallback7((op) => {
1400
- setFormula((prev) => {
1401
- if (prev.trim() && !prev.endsWith(" ")) {
1402
- return prev + " " + op + " ";
1445
+ const setRowFields = useCallback7((fields) => {
1446
+ setRowFieldsState(fields);
1447
+ }, []);
1448
+ const addColumnField = useCallback7(
1449
+ (field) => {
1450
+ if (!columnFields.includes(field)) {
1451
+ setColumnFieldsState((prev) => [...prev, field]);
1452
+ }
1453
+ },
1454
+ [columnFields]
1455
+ );
1456
+ const removeColumnField = useCallback7((field) => {
1457
+ setColumnFieldsState((prev) => prev.filter((f) => f !== field));
1458
+ }, []);
1459
+ const setColumnFields = useCallback7((fields) => {
1460
+ setColumnFieldsState(fields);
1461
+ }, []);
1462
+ const addValueField = useCallback7(
1463
+ (field, aggregation = "sum") => {
1464
+ if (aggregation !== "sum" && !requirePro(`${aggregation} aggregation`)) {
1465
+ return;
1466
+ }
1467
+ setValueFields((prev) => {
1468
+ if (prev.some((v) => v.field === field && v.aggregation === aggregation)) {
1469
+ return prev;
1470
+ }
1471
+ return [...prev, { field, aggregation }];
1472
+ });
1473
+ },
1474
+ [requirePro]
1475
+ );
1476
+ const removeValueField = useCallback7((field, aggregation) => {
1477
+ setValueFields((prev) => {
1478
+ if (aggregation) {
1479
+ return prev.filter((v) => !(v.field === field && v.aggregation === aggregation));
1403
1480
  }
1404
- return prev + op + " ";
1481
+ return prev.filter((v) => v.field !== field);
1405
1482
  });
1406
1483
  }, []);
1407
- const handleSave = useCallback7(() => {
1408
- if (!name.trim()) {
1409
- setError("Name is required");
1484
+ const updateValueFieldAggregation = useCallback7(
1485
+ (field, oldAgg, newAgg) => {
1486
+ setValueFields(
1487
+ (prev) => prev.map((v) => {
1488
+ if (v.field === field && v.aggregation === oldAgg) {
1489
+ return { ...v, aggregation: newAgg };
1490
+ }
1491
+ return v;
1492
+ })
1493
+ );
1494
+ },
1495
+ []
1496
+ );
1497
+ const clearConfig = useCallback7(() => {
1498
+ setRowFieldsState([]);
1499
+ setColumnFieldsState([]);
1500
+ setValueFields([]);
1501
+ }, []);
1502
+ const autoSuggestConfig = useCallback7(() => {
1503
+ if (!requirePro("Pivot Table - Auto Suggest"))
1410
1504
  return;
1411
- }
1412
- const validationResult = validateSimpleFormula(formula, availableFields);
1413
- if (validationResult) {
1414
- setError(validationResult);
1505
+ if (availableFields.length === 0)
1415
1506
  return;
1507
+ const categoricalFields = availableFields.filter((f) => !f.isNumeric && f.uniqueCount < 50);
1508
+ const numericFields = availableFields.filter((f) => f.isNumeric);
1509
+ if (categoricalFields.length > 0 && numericFields.length > 0) {
1510
+ setRowFieldsState([categoricalFields[0].field]);
1511
+ setValueFields([{ field: numericFields[0].field, aggregation: "sum" }]);
1416
1512
  }
1417
- const field = {
1418
- id: existingField?.id || `calc_${Date.now()}`,
1419
- name: name.trim(),
1420
- formula: formula.trim(),
1421
- formatAs,
1422
- decimals
1423
- };
1424
- onSave(field);
1425
- onClose();
1426
- }, [name, formula, formatAs, decimals, existingField, availableFields, onSave, onClose]);
1427
- const handleOverlayClick = useCallback7((e) => {
1428
- if (e.target === e.currentTarget) {
1429
- onClose();
1430
- }
1431
- }, [onClose]);
1432
- if (!show) return null;
1433
- const modalContent = /* @__PURE__ */ jsx3("div", { className: "vpg-modal-overlay", onClick: handleOverlayClick, children: /* @__PURE__ */ jsxs3("div", { className: "vpg-modal", children: [
1434
- /* @__PURE__ */ jsxs3("div", { className: "vpg-modal-header", children: [
1435
- /* @__PURE__ */ jsxs3("h3", { children: [
1436
- existingField ? "Edit" : "Create",
1437
- " Calculated Field"
1438
- ] }),
1439
- /* @__PURE__ */ jsx3("button", { className: "vpg-modal-close", onClick: onClose, children: "\xD7" })
1440
- ] }),
1441
- /* @__PURE__ */ jsxs3("div", { className: "vpg-modal-body", children: [
1442
- /* @__PURE__ */ jsxs3("div", { className: "vpg-form-group", children: [
1443
- /* @__PURE__ */ jsx3("label", { className: "vpg-label", children: "Name" }),
1444
- /* @__PURE__ */ jsx3(
1445
- "input",
1446
- {
1447
- type: "text",
1448
- className: "vpg-input",
1449
- placeholder: "e.g., Profit Margin %",
1450
- value: name,
1451
- onChange: (e) => setName(e.target.value)
1452
- }
1453
- )
1454
- ] }),
1455
- /* @__PURE__ */ jsxs3("div", { className: "vpg-form-group", children: [
1456
- /* @__PURE__ */ jsx3("label", { className: "vpg-label", children: "Formula" }),
1457
- /* @__PURE__ */ jsx3(
1458
- "textarea",
1459
- {
1460
- className: "vpg-textarea",
1461
- placeholder: "e.g., revenue / units",
1462
- rows: 2,
1463
- value: formula,
1464
- onChange: (e) => setFormula(e.target.value)
1465
- }
1466
- ),
1467
- /* @__PURE__ */ jsx3("div", { className: "vpg-formula-hint", children: "Use field names with math operators: + - * / ( )" }),
1468
- validationError && /* @__PURE__ */ jsx3("div", { className: "vpg-error", children: validationError })
1469
- ] }),
1470
- /* @__PURE__ */ jsxs3("div", { className: "vpg-form-group", children: [
1471
- /* @__PURE__ */ jsx3("label", { className: "vpg-label-small", children: "Operators" }),
1472
- /* @__PURE__ */ jsxs3("div", { className: "vpg-button-group", children: [
1473
- /* @__PURE__ */ jsx3("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("+"), children: "+" }),
1474
- /* @__PURE__ */ jsx3("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("-"), children: "\u2212" }),
1475
- /* @__PURE__ */ jsx3("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("*"), children: "\xD7" }),
1476
- /* @__PURE__ */ jsx3("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("/"), children: "\xF7" }),
1477
- /* @__PURE__ */ jsx3("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("("), children: "(" }),
1478
- /* @__PURE__ */ jsx3("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator(")"), children: ")" })
1479
- ] })
1480
- ] }),
1481
- /* @__PURE__ */ jsxs3("div", { className: "vpg-form-group", children: [
1482
- /* @__PURE__ */ jsx3("label", { className: "vpg-label-small", children: "Insert Field" }),
1483
- availableFields.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "vpg-button-group vpg-field-buttons", children: availableFields.map((field) => /* @__PURE__ */ jsx3(
1484
- "button",
1485
- {
1486
- className: "vpg-insert-btn vpg-field-btn",
1487
- onClick: () => insertField(field),
1488
- children: field
1489
- },
1490
- field
1491
- )) }) : /* @__PURE__ */ jsx3("div", { className: "vpg-no-fields", children: "No numeric fields available" })
1492
- ] }),
1493
- /* @__PURE__ */ jsxs3("div", { className: "vpg-form-row", children: [
1494
- /* @__PURE__ */ jsxs3("div", { className: "vpg-form-group vpg-form-group-half", children: [
1495
- /* @__PURE__ */ jsx3("label", { className: "vpg-label", children: "Format As" }),
1496
- /* @__PURE__ */ jsxs3(
1497
- "select",
1498
- {
1499
- className: "vpg-select",
1500
- value: formatAs,
1501
- onChange: (e) => setFormatAs(e.target.value),
1502
- children: [
1503
- /* @__PURE__ */ jsx3("option", { value: "number", children: "Number" }),
1504
- /* @__PURE__ */ jsx3("option", { value: "percent", children: "Percentage" }),
1505
- /* @__PURE__ */ jsx3("option", { value: "currency", children: "Currency ($)" })
1506
- ]
1507
- }
1508
- )
1509
- ] }),
1510
- /* @__PURE__ */ jsxs3("div", { className: "vpg-form-group vpg-form-group-half", children: [
1511
- /* @__PURE__ */ jsx3("label", { className: "vpg-label", children: "Decimals" }),
1512
- /* @__PURE__ */ jsx3(
1513
- "input",
1514
- {
1515
- type: "number",
1516
- className: "vpg-input",
1517
- min: 0,
1518
- max: 6,
1519
- value: decimals,
1520
- onChange: (e) => setDecimals(Number(e.target.value))
1521
- }
1522
- )
1523
- ] })
1524
- ] }),
1525
- error && /* @__PURE__ */ jsx3("div", { className: "vpg-error vpg-error-box", children: error })
1526
- ] }),
1527
- /* @__PURE__ */ jsxs3("div", { className: "vpg-modal-footer", children: [
1528
- /* @__PURE__ */ jsx3("button", { className: "vpg-btn vpg-btn-secondary", onClick: onClose, children: "Cancel" }),
1529
- /* @__PURE__ */ jsxs3("button", { className: "vpg-btn vpg-btn-primary", onClick: handleSave, children: [
1530
- existingField ? "Update" : "Add",
1531
- " Field"
1532
- ] })
1533
- ] })
1534
- ] }) });
1535
- if (typeof document === "undefined") return null;
1536
- return createPortal(modalContent, document.body);
1513
+ }, [availableFields, requirePro]);
1514
+ const addCalculatedField = useCallback7((field) => {
1515
+ setCalculatedFields((prev) => {
1516
+ const existing = prev.findIndex((f) => f.id === field.id);
1517
+ let updated;
1518
+ if (existing >= 0) {
1519
+ updated = [...prev.slice(0, existing), field, ...prev.slice(existing + 1)];
1520
+ } else {
1521
+ updated = [...prev, field];
1522
+ }
1523
+ saveCalculatedFields(updated);
1524
+ return updated;
1525
+ });
1526
+ }, []);
1527
+ const removeCalculatedField = useCallback7((id) => {
1528
+ setCalculatedFields((prev) => {
1529
+ const updated = prev.filter((f) => f.id !== id);
1530
+ saveCalculatedFields(updated);
1531
+ return updated;
1532
+ });
1533
+ setValueFields((prev) => prev.filter((v) => v.field !== `calc:${id}`));
1534
+ }, []);
1535
+ return {
1536
+ // State
1537
+ rowFields,
1538
+ columnFields,
1539
+ valueFields,
1540
+ showRowTotals,
1541
+ showColumnTotals,
1542
+ calculatedFields,
1543
+ // Computed
1544
+ availableFields,
1545
+ unassignedFields,
1546
+ isConfigured,
1547
+ pivotResult,
1548
+ // Actions
1549
+ addRowField,
1550
+ removeRowField,
1551
+ addColumnField,
1552
+ removeColumnField,
1553
+ addValueField,
1554
+ removeValueField,
1555
+ updateValueFieldAggregation,
1556
+ clearConfig,
1557
+ setShowRowTotals,
1558
+ setShowColumnTotals,
1559
+ autoSuggestConfig,
1560
+ setRowFields,
1561
+ setColumnFields,
1562
+ addCalculatedField,
1563
+ removeCalculatedField
1564
+ };
1537
1565
  }
1538
1566
 
1539
1567
  // src/components/PivotConfig.tsx
1568
+ import { AGGREGATION_OPTIONS, getAggregationSymbol } from "@smallwebco/tinypivot-core";
1569
+ import { useCallback as useCallback8, useMemo as useMemo8, useState as useState8 } from "react";
1540
1570
  import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1541
1571
  function aggregationRequiresPro(agg) {
1542
1572
  return agg !== "sum";
1543
1573
  }
1544
1574
  function getFieldIcon(type, isCalculated) {
1545
- if (isCalculated) return "\u0192";
1575
+ if (isCalculated)
1576
+ return "\u0192";
1546
1577
  switch (type) {
1547
1578
  case "number":
1548
1579
  return "#";
@@ -1564,7 +1595,7 @@ function PivotConfig({
1564
1595
  onShowRowTotalsChange,
1565
1596
  onShowColumnTotalsChange,
1566
1597
  onClearConfig,
1567
- onAutoSuggest,
1598
+ onAutoSuggest: _onAutoSuggest,
1568
1599
  onDragStart,
1569
1600
  onDragEnd,
1570
1601
  onUpdateAggregation,
@@ -1584,12 +1615,10 @@ function PivotConfig({
1584
1615
  const isAggregationAvailable = useCallback8((agg) => {
1585
1616
  return !aggregationRequiresPro(agg) || canUseAdvancedAggregations;
1586
1617
  }, [canUseAdvancedAggregations]);
1587
- const numericFieldNames = useMemo8(
1588
- () => availableFields.filter((f) => f.isNumeric).map((f) => f.field),
1589
- [availableFields]
1590
- );
1618
+ const numericFieldNames = useMemo8(() => availableFields.filter((f) => f.isNumeric).map((f) => f.field), [availableFields]);
1591
1619
  const calculatedFieldsAsStats = useMemo8(() => {
1592
- if (!calculatedFields) return [];
1620
+ if (!calculatedFields)
1621
+ return [];
1593
1622
  return calculatedFields.map((calc) => ({
1594
1623
  field: `calc:${calc.id}`,
1595
1624
  type: "number",
@@ -1624,7 +1653,8 @@ function PivotConfig({
1624
1653
  );
1625
1654
  }, [allAvailableFields, rowFields, columnFields, valueFields]);
1626
1655
  const filteredUnassignedFields = useMemo8(() => {
1627
- if (!fieldSearch.trim()) return unassignedFields;
1656
+ if (!fieldSearch.trim())
1657
+ return unassignedFields;
1628
1658
  const search = fieldSearch.toLowerCase().trim();
1629
1659
  return unassignedFields.filter((f) => {
1630
1660
  const fieldName = f.field.toLowerCase();
@@ -1829,7 +1859,8 @@ function PivotConfig({
1829
1859
  ] }),
1830
1860
  /* @__PURE__ */ jsxs4("div", { className: "vpg-unassigned-section", children: [
1831
1861
  /* @__PURE__ */ jsx4("div", { className: "vpg-section-header", children: /* @__PURE__ */ jsxs4("div", { className: "vpg-section-label", children: [
1832
- "Available ",
1862
+ "Available",
1863
+ " ",
1833
1864
  /* @__PURE__ */ jsx4("span", { className: "vpg-count", children: unassignedFields.length })
1834
1865
  ] }) }),
1835
1866
  /* @__PURE__ */ jsxs4("div", { className: "vpg-field-search", children: [
@@ -1883,7 +1914,8 @@ function PivotConfig({
1883
1914
  onClick: (e) => {
1884
1915
  e.stopPropagation();
1885
1916
  const calcField = calculatedFields?.find((c) => c.id === field.calcId);
1886
- if (calcField) openCalcModal(calcField);
1917
+ if (calcField)
1918
+ openCalcModal(calcField);
1887
1919
  },
1888
1920
  children: "\u270E"
1889
1921
  }
@@ -1946,8 +1978,8 @@ function PivotConfig({
1946
1978
  }
1947
1979
 
1948
1980
  // src/components/PivotSkeleton.tsx
1949
- import { useState as useState9, useMemo as useMemo9, useCallback as useCallback9, useEffect as useEffect6 } from "react";
1950
1981
  import { getAggregationLabel as getAggregationLabel2, getAggregationSymbol as getAggregationSymbol2 } from "@smallwebco/tinypivot-core";
1982
+ import { useCallback as useCallback9, useEffect as useEffect6, useMemo as useMemo9, useState as useState9 } from "react";
1951
1983
  import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1952
1984
  function PivotSkeleton({
1953
1985
  rowFields,
@@ -2002,7 +2034,8 @@ function PivotSkeleton({
2002
2034
  const [showCopyToast, setShowCopyToast] = useState9(false);
2003
2035
  const [copyToastMessage, setCopyToastMessage] = useState9("");
2004
2036
  const selectionBounds = useMemo9(() => {
2005
- if (!selectionStart || !selectionEnd) return null;
2037
+ if (!selectionStart || !selectionEnd)
2038
+ return null;
2006
2039
  return {
2007
2040
  minRow: Math.min(selectionStart.row, selectionEnd.row),
2008
2041
  maxRow: Math.max(selectionStart.row, selectionEnd.row),
@@ -2048,7 +2081,8 @@ function PivotSkeleton({
2048
2081
  return () => document.removeEventListener("mouseup", handleMouseUp);
2049
2082
  }, []);
2050
2083
  const sortedRowIndices = useMemo9(() => {
2051
- if (!pivotResult) return [];
2084
+ if (!pivotResult)
2085
+ return [];
2052
2086
  const indices = pivotResult.rowHeaders.map((_, i) => i);
2053
2087
  const headers = pivotResult.rowHeaders;
2054
2088
  const data = pivotResult.data;
@@ -2062,9 +2096,12 @@ function PivotSkeleton({
2062
2096
  const colIdx = sortTarget;
2063
2097
  const aVal = data[a]?.[colIdx]?.value ?? null;
2064
2098
  const bVal = data[b]?.[colIdx]?.value ?? null;
2065
- if (aVal === null && bVal === null) cmp = 0;
2066
- else if (aVal === null) cmp = 1;
2067
- else if (bVal === null) cmp = -1;
2099
+ if (aVal === null && bVal === null)
2100
+ cmp = 0;
2101
+ else if (aVal === null)
2102
+ cmp = 1;
2103
+ else if (bVal === null)
2104
+ cmp = -1;
2068
2105
  else cmp = aVal - bVal;
2069
2106
  }
2070
2107
  return sortDirection === "asc" ? cmp : -cmp;
@@ -2072,12 +2109,14 @@ function PivotSkeleton({
2072
2109
  return indices;
2073
2110
  }, [pivotResult, sortTarget, sortDirection]);
2074
2111
  const copySelectionToClipboard = useCallback9(() => {
2075
- if (!selectionBounds || !pivotResult) return;
2112
+ if (!selectionBounds || !pivotResult)
2113
+ return;
2076
2114
  const { minRow, maxRow, minCol, maxCol } = selectionBounds;
2077
2115
  const lines = [];
2078
2116
  for (let r = minRow; r <= maxRow; r++) {
2079
2117
  const sortedIdx = sortedRowIndices[r];
2080
- if (sortedIdx === void 0) continue;
2118
+ if (sortedIdx === void 0)
2119
+ continue;
2081
2120
  const rowValues = [];
2082
2121
  for (let c = minCol; c <= maxCol; c++) {
2083
2122
  const cell = pivotResult.data[sortedIdx]?.[c];
@@ -2097,7 +2136,8 @@ function PivotSkeleton({
2097
2136
  }, [selectionBounds, pivotResult, sortedRowIndices]);
2098
2137
  useEffect6(() => {
2099
2138
  const handleKeydown = (event) => {
2100
- if (!selectionBounds) return;
2139
+ if (!selectionBounds)
2140
+ return;
2101
2141
  if ((event.ctrlKey || event.metaKey) && event.key === "c") {
2102
2142
  event.preventDefault();
2103
2143
  copySelectionToClipboard();
@@ -2113,13 +2153,15 @@ function PivotSkeleton({
2113
2153
  return () => document.removeEventListener("keydown", handleKeydown);
2114
2154
  }, [selectionBounds, copySelectionToClipboard]);
2115
2155
  const selectionStats = useMemo9(() => {
2116
- if (!selectionBounds || !pivotResult) return null;
2156
+ if (!selectionBounds || !pivotResult)
2157
+ return null;
2117
2158
  const { minRow, maxRow, minCol, maxCol } = selectionBounds;
2118
2159
  const values = [];
2119
2160
  let count = 0;
2120
2161
  for (let r = minRow; r <= maxRow; r++) {
2121
2162
  const sortedIdx = sortedRowIndices[r];
2122
- if (sortedIdx === void 0) continue;
2163
+ if (sortedIdx === void 0)
2164
+ continue;
2123
2165
  for (let c = minCol; c <= maxCol; c++) {
2124
2166
  const cell = pivotResult.data[sortedIdx]?.[c];
2125
2167
  count++;
@@ -2128,7 +2170,8 @@ function PivotSkeleton({
2128
2170
  }
2129
2171
  }
2130
2172
  }
2131
- if (count <= 1) return null;
2173
+ if (count <= 1)
2174
+ return null;
2132
2175
  const sum = values.reduce((a, b) => a + b, 0);
2133
2176
  const avg = values.length > 0 ? sum / values.length : 0;
2134
2177
  return {
@@ -2139,8 +2182,10 @@ function PivotSkeleton({
2139
2182
  };
2140
2183
  }, [selectionBounds, pivotResult, sortedRowIndices]);
2141
2184
  const formatStatValue = useCallback9((val) => {
2142
- if (Math.abs(val) >= 1e6) return `${(val / 1e6).toFixed(2)}M`;
2143
- if (Math.abs(val) >= 1e3) return `${(val / 1e3).toFixed(2)}K`;
2185
+ if (Math.abs(val) >= 1e6)
2186
+ return `${(val / 1e6).toFixed(2)}M`;
2187
+ if (Math.abs(val) >= 1e3)
2188
+ return `${(val / 1e3).toFixed(2)}K`;
2144
2189
  return val.toFixed(2);
2145
2190
  }, []);
2146
2191
  const columnHeaderCells = useMemo9(() => {
@@ -2172,12 +2217,14 @@ function PivotSkeleton({
2172
2217
  }, [pivotResult, valueFields]);
2173
2218
  const hasActiveFilters = activeFilters && activeFilters.length > 0;
2174
2219
  const filterSummary = useMemo9(() => {
2175
- if (!activeFilters || activeFilters.length === 0) return "";
2220
+ if (!activeFilters || activeFilters.length === 0)
2221
+ return "";
2176
2222
  return activeFilters.map((f) => f.column).join(", ");
2177
2223
  }, [activeFilters]);
2178
2224
  const [showFilterTooltip, setShowFilterTooltip] = useState9(false);
2179
2225
  const filterTooltipDetails = useMemo9(() => {
2180
- if (!activeFilters || activeFilters.length === 0) return [];
2226
+ if (!activeFilters || activeFilters.length === 0)
2227
+ return [];
2181
2228
  return activeFilters.map((f) => {
2182
2229
  if (f.isRange && f.displayText) {
2183
2230
  return {
@@ -2219,10 +2266,13 @@ function PivotSkeleton({
2219
2266
  setDragOverArea(null);
2220
2267
  return;
2221
2268
  }
2222
- if (rowFields.includes(field)) onRemoveRowField(field);
2223
- if (columnFields.includes(field)) onRemoveColumnField(field);
2269
+ if (rowFields.includes(field))
2270
+ onRemoveRowField(field);
2271
+ if (columnFields.includes(field))
2272
+ onRemoveColumnField(field);
2224
2273
  const existingValue = valueFields.find((v) => v.field === field);
2225
- if (existingValue) onRemoveValueField(field, existingValue.aggregation);
2274
+ if (existingValue)
2275
+ onRemoveValueField(field, existingValue.aggregation);
2226
2276
  switch (area) {
2227
2277
  case "row":
2228
2278
  onAddRowField(field);
@@ -2362,14 +2412,18 @@ function PivotSkeleton({
2362
2412
  }
2363
2413
  ),
2364
2414
  /* @__PURE__ */ jsxs5("span", { className: "vpg-filter-text", children: [
2365
- "Filtered: ",
2415
+ "Filtered:",
2416
+ " ",
2366
2417
  /* @__PURE__ */ jsx5("strong", { children: filterSummary }),
2367
2418
  filteredRowCount !== void 0 && totalRowCount !== void 0 && /* @__PURE__ */ jsxs5("span", { className: "vpg-filter-count", children: [
2368
2419
  "(",
2369
2420
  filteredRowCount.toLocaleString(),
2370
- " of ",
2421
+ " ",
2422
+ "of",
2423
+ " ",
2371
2424
  totalRowCount.toLocaleString(),
2372
- " rows)"
2425
+ " ",
2426
+ "rows)"
2373
2427
  ] })
2374
2428
  ] }),
2375
2429
  showFilterTooltip && /* @__PURE__ */ jsxs5("div", { className: "vpg-filter-tooltip", children: [
@@ -2381,16 +2435,21 @@ function PivotSkeleton({
2381
2435
  filter.remaining > 0 && /* @__PURE__ */ jsxs5("span", { className: "vpg-tooltip-more", children: [
2382
2436
  "+",
2383
2437
  filter.remaining,
2384
- " more"
2438
+ " ",
2439
+ "more"
2385
2440
  ] })
2386
2441
  ] }) })
2387
2442
  ] }, filter.column)),
2388
2443
  filteredRowCount !== void 0 && totalRowCount !== void 0 && /* @__PURE__ */ jsxs5("div", { className: "vpg-tooltip-summary", children: [
2389
- "Showing ",
2444
+ "Showing",
2445
+ " ",
2390
2446
  filteredRowCount.toLocaleString(),
2391
- " of ",
2447
+ " ",
2448
+ "of",
2449
+ " ",
2392
2450
  totalRowCount.toLocaleString(),
2393
- " rows"
2451
+ " ",
2452
+ "rows"
2394
2453
  ] })
2395
2454
  ] })
2396
2455
  ]
@@ -2399,17 +2458,20 @@ function PivotSkeleton({
2399
2458
  isConfigured && /* @__PURE__ */ jsxs5("div", { className: "vpg-config-summary", children: [
2400
2459
  /* @__PURE__ */ jsxs5("span", { className: "vpg-summary-badge vpg-rows", children: [
2401
2460
  rowFields.length,
2402
- " row",
2461
+ " ",
2462
+ "row",
2403
2463
  rowFields.length !== 1 ? "s" : ""
2404
2464
  ] }),
2405
2465
  /* @__PURE__ */ jsxs5("span", { className: "vpg-summary-badge vpg-cols", children: [
2406
2466
  columnFields.length,
2407
- " col",
2467
+ " ",
2468
+ "col",
2408
2469
  columnFields.length !== 1 ? "s" : ""
2409
2470
  ] }),
2410
2471
  /* @__PURE__ */ jsxs5("span", { className: "vpg-summary-badge vpg-vals", children: [
2411
2472
  valueFields.length,
2412
- " val",
2473
+ " ",
2474
+ "val",
2413
2475
  valueFields.length !== 1 ? "s" : ""
2414
2476
  ] })
2415
2477
  ] })
@@ -2580,15 +2642,21 @@ function PivotSkeleton({
2580
2642
  }
2581
2643
  ),
2582
2644
  /* @__PURE__ */ jsx5("span", { className: "vpg-placeholder-text", children: valueFields.length === 0 ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
2583
- "Add a ",
2645
+ "Add a",
2646
+ " ",
2584
2647
  /* @__PURE__ */ jsx5("strong", { children: "Values" }),
2585
- " field to see your pivot table"
2648
+ " ",
2649
+ "field to see your pivot table"
2586
2650
  ] }) : rowFields.length === 0 && columnFields.length === 0 ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
2587
- "Add ",
2651
+ "Add",
2652
+ " ",
2588
2653
  /* @__PURE__ */ jsx5("strong", { children: "Row" }),
2589
- " or ",
2654
+ " ",
2655
+ "or",
2656
+ " ",
2590
2657
  /* @__PURE__ */ jsx5("strong", { children: "Column" }),
2591
- " fields to group your data"
2658
+ " ",
2659
+ "fields to group your data"
2592
2660
  ] }) : "Your pivot table will appear here" })
2593
2661
  ] }) }),
2594
2662
  isConfigured && pivotResult && /* @__PURE__ */ jsx5("div", { className: "vpg-table-container", children: /* @__PURE__ */ jsxs5("table", { className: "vpg-pivot-table", children: [
@@ -2666,9 +2734,11 @@ function PivotSkeleton({
2666
2734
  isConfigured && pivotResult && /* @__PURE__ */ jsxs5("div", { className: "vpg-skeleton-footer", children: [
2667
2735
  /* @__PURE__ */ jsxs5("span", { className: "vpg-footer-info", children: [
2668
2736
  pivotResult.rowHeaders.length,
2669
- " rows \xD7 ",
2737
+ " ",
2738
+ "rows \xD7",
2670
2739
  pivotResult.data[0]?.length || 0,
2671
- " columns"
2740
+ " ",
2741
+ "columns"
2672
2742
  ] }),
2673
2743
  selectionStats && selectionStats.count > 1 && /* @__PURE__ */ jsxs5("div", { className: "vpg-selection-stats", children: [
2674
2744
  /* @__PURE__ */ jsxs5("span", { className: "vpg-stat", children: [
@@ -2821,12 +2891,15 @@ function DataGrid({
2821
2891
  removeCalculatedField
2822
2892
  } = usePivotTable(filteredDataForPivot);
2823
2893
  const activeFilterInfo = useMemo10(() => {
2824
- if (activeFilters.length === 0) return null;
2894
+ if (activeFilters.length === 0)
2895
+ return null;
2825
2896
  return activeFilters.map((f) => {
2826
2897
  if (f.type === "range" && f.range) {
2827
2898
  const parts = [];
2828
- if (f.range.min !== null) parts.push(`\u2265 ${f.range.min}`);
2829
- if (f.range.max !== null) parts.push(`\u2264 ${f.range.max}`);
2899
+ if (f.range.min !== null)
2900
+ parts.push(`\u2265 ${f.range.min}`);
2901
+ if (f.range.max !== null)
2902
+ parts.push(`\u2264 ${f.range.max}`);
2830
2903
  return {
2831
2904
  column: f.column,
2832
2905
  valueCount: 1,
@@ -2851,7 +2924,8 @@ function DataGrid({
2851
2924
  return rows.filter((row) => {
2852
2925
  for (const col of columnKeys) {
2853
2926
  const value = row.original[col];
2854
- if (value === null || value === void 0) continue;
2927
+ if (value === null || value === void 0)
2928
+ continue;
2855
2929
  if (String(value).toLowerCase().includes(term)) {
2856
2930
  return true;
2857
2931
  }
@@ -2861,11 +2935,13 @@ function DataGrid({
2861
2935
  }, [rows, globalSearchTerm, enableSearch, columnKeys]);
2862
2936
  const totalSearchedRows = searchFilteredData.length;
2863
2937
  const totalPages = useMemo10(() => {
2864
- if (!enablePagination) return 1;
2938
+ if (!enablePagination)
2939
+ return 1;
2865
2940
  return Math.max(1, Math.ceil(totalSearchedRows / pageSize));
2866
2941
  }, [enablePagination, totalSearchedRows, pageSize]);
2867
2942
  const paginatedRows = useMemo10(() => {
2868
- if (!enablePagination) return searchFilteredData;
2943
+ if (!enablePagination)
2944
+ return searchFilteredData;
2869
2945
  const start = (currentPage - 1) * pageSize;
2870
2946
  const end = start + pageSize;
2871
2947
  return searchFilteredData.slice(start, end);
@@ -2874,7 +2950,8 @@ function DataGrid({
2874
2950
  setCurrentPage(1);
2875
2951
  }, [columnFilters, globalSearchTerm]);
2876
2952
  const selectionBounds = useMemo10(() => {
2877
- if (!selectionStart || !selectionEnd) return null;
2953
+ if (!selectionStart || !selectionEnd)
2954
+ return null;
2878
2955
  return {
2879
2956
  minRow: Math.min(selectionStart.row, selectionEnd.row),
2880
2957
  maxRow: Math.max(selectionStart.row, selectionEnd.row),
@@ -2883,16 +2960,19 @@ function DataGrid({
2883
2960
  };
2884
2961
  }, [selectionStart, selectionEnd]);
2885
2962
  const selectionStats = useMemo10(() => {
2886
- if (!selectionBounds) return null;
2963
+ if (!selectionBounds)
2964
+ return null;
2887
2965
  const { minRow, maxRow, minCol, maxCol } = selectionBounds;
2888
2966
  const values = [];
2889
2967
  let count = 0;
2890
2968
  for (let r = minRow; r <= maxRow; r++) {
2891
2969
  const row = rows[r];
2892
- if (!row) continue;
2970
+ if (!row)
2971
+ continue;
2893
2972
  for (let c = minCol; c <= maxCol; c++) {
2894
2973
  const colId = columnKeys[c];
2895
- if (!colId) continue;
2974
+ if (!colId)
2975
+ continue;
2896
2976
  const value = row.original[colId];
2897
2977
  count++;
2898
2978
  if (value !== null && value !== void 0 && value !== "") {
@@ -2903,19 +2983,23 @@ function DataGrid({
2903
2983
  }
2904
2984
  }
2905
2985
  }
2906
- if (values.length === 0) return { count, sum: null, avg: null, numericCount: 0 };
2986
+ if (values.length === 0)
2987
+ return { count, sum: null, avg: null, numericCount: 0 };
2907
2988
  const sum = values.reduce((a, b) => a + b, 0);
2908
2989
  const avg = sum / values.length;
2909
2990
  return { count, sum, avg, numericCount: values.length };
2910
2991
  }, [selectionBounds, rows, columnKeys]);
2911
2992
  useEffect7(() => {
2912
- if (typeof document === "undefined") return;
2913
- if (data.length === 0) return;
2993
+ if (typeof document === "undefined")
2994
+ return;
2995
+ if (data.length === 0)
2996
+ return;
2914
2997
  const widths = {};
2915
2998
  const sampleSize = Math.min(100, data.length);
2916
2999
  const canvas = document.createElement("canvas");
2917
3000
  const ctx = canvas.getContext("2d");
2918
- if (!ctx) return;
3001
+ if (!ctx)
3002
+ return;
2919
3003
  ctx.font = "13px system-ui, -apple-system, sans-serif";
2920
3004
  for (const key of columnKeys) {
2921
3005
  let maxWidth = ctx.measureText(key).width + 56;
@@ -2931,7 +3015,8 @@ function DataGrid({
2931
3015
  }, [data, columnKeys]);
2932
3016
  const startColumnResize = useCallback10(
2933
3017
  (columnId, event) => {
2934
- if (!enableColumnResize) return;
3018
+ if (!enableColumnResize)
3019
+ return;
2935
3020
  event.preventDefault();
2936
3021
  event.stopPropagation();
2937
3022
  setResizingColumnId(columnId);
@@ -2941,7 +3026,8 @@ function DataGrid({
2941
3026
  [enableColumnResize, columnWidths]
2942
3027
  );
2943
3028
  useEffect7(() => {
2944
- if (!resizingColumnId) return;
3029
+ if (!resizingColumnId)
3030
+ return;
2945
3031
  const handleResizeMove = (event) => {
2946
3032
  const diff = event.clientX - resizeStartX;
2947
3033
  const newWidth = Math.max(MIN_COL_WIDTH, Math.min(MAX_COL_WIDTH, resizeStartWidth + diff));
@@ -2962,7 +3048,8 @@ function DataGrid({
2962
3048
  }, [resizingColumnId, resizeStartX, resizeStartWidth]);
2963
3049
  const startVerticalResize = useCallback10(
2964
3050
  (event) => {
2965
- if (!enableVerticalResize) return;
3051
+ if (!enableVerticalResize)
3052
+ return;
2966
3053
  event.preventDefault();
2967
3054
  setIsResizingVertically(true);
2968
3055
  setVerticalResizeStartY(event.clientY);
@@ -2971,7 +3058,8 @@ function DataGrid({
2971
3058
  [enableVerticalResize, gridHeight]
2972
3059
  );
2973
3060
  useEffect7(() => {
2974
- if (!isResizingVertically) return;
3061
+ if (!isResizingVertically)
3062
+ return;
2975
3063
  const handleVerticalResizeMove = (event) => {
2976
3064
  const diff = event.clientY - verticalResizeStartY;
2977
3065
  const newHeight = Math.max(minHeight, Math.min(maxHeight, verticalResizeStartHeight + diff));
@@ -2989,7 +3077,8 @@ function DataGrid({
2989
3077
  }, [isResizingVertically, verticalResizeStartY, verticalResizeStartHeight, minHeight, maxHeight]);
2990
3078
  const handleExport = useCallback10(() => {
2991
3079
  if (viewMode === "pivot") {
2992
- if (!pivotResult) return;
3080
+ if (!pivotResult)
3081
+ return;
2993
3082
  const pivotFilename = exportFilename.replace(".csv", "-pivot.csv");
2994
3083
  exportPivotToCSV(
2995
3084
  {
@@ -3033,7 +3122,8 @@ function DataGrid({
3033
3122
  onExport
3034
3123
  ]);
3035
3124
  const copySelectionToClipboard = useCallback10(() => {
3036
- if (!selectionBounds || !enableClipboard) return;
3125
+ if (!selectionBounds || !enableClipboard)
3126
+ return;
3037
3127
  const text = formatSelectionForClipboard(
3038
3128
  rows.map((r) => r.original),
3039
3129
  columnKeys,
@@ -3190,7 +3280,8 @@ function DataGrid({
3190
3280
  [selectionBounds, selectedCell]
3191
3281
  );
3192
3282
  const formatStatValue = (value) => {
3193
- if (value === null) return "-";
3283
+ if (value === null)
3284
+ return "-";
3194
3285
  if (Math.abs(value) >= 1e3) {
3195
3286
  return value.toLocaleString("en-US", { maximumFractionDigits: 2 });
3196
3287
  }
@@ -3201,12 +3292,15 @@ function DataGrid({
3201
3292
  return !noFormatPatterns.test(columnId);
3202
3293
  };
3203
3294
  const formatCellValueDisplay = (value, columnId) => {
3204
- if (value === null || value === void 0) return "";
3205
- if (value === "") return "";
3295
+ if (value === null || value === void 0)
3296
+ return "";
3297
+ if (value === "")
3298
+ return "";
3206
3299
  const stats = getColumnStats(columnId);
3207
3300
  if (stats.type === "number") {
3208
3301
  const num = typeof value === "number" ? value : Number.parseFloat(String(value));
3209
- if (Number.isNaN(num)) return String(value);
3302
+ if (Number.isNaN(num))
3303
+ return String(value);
3210
3304
  if (shouldFormatNumber(columnId) && Math.abs(num) >= 1e3) {
3211
3305
  return num.toLocaleString("en-US", { maximumFractionDigits: 2 });
3212
3306
  }
@@ -3368,13 +3462,15 @@ function DataGrid({
3368
3462
  ) }),
3369
3463
  /* @__PURE__ */ jsxs6("span", { children: [
3370
3464
  activeFilterCount,
3371
- " filter",
3465
+ " ",
3466
+ "filter",
3372
3467
  activeFilterCount > 1 ? "s" : ""
3373
3468
  ] })
3374
3469
  ] }),
3375
3470
  globalSearchTerm && /* @__PURE__ */ jsx6("div", { className: "vpg-search-info", children: /* @__PURE__ */ jsxs6("span", { children: [
3376
3471
  totalSearchedRows,
3377
- " match",
3472
+ " ",
3473
+ "match",
3378
3474
  totalSearchedRows !== 1 ? "es" : ""
3379
3475
  ] }) })
3380
3476
  ] }),
@@ -3395,7 +3491,8 @@ function DataGrid({
3395
3491
  }
3396
3492
  ) }),
3397
3493
  showPivotConfig ? "Hide" : "Show",
3398
- " Config"
3494
+ " ",
3495
+ "Config"
3399
3496
  ]
3400
3497
  }
3401
3498
  ),
@@ -3517,7 +3614,7 @@ function DataGrid({
3517
3614
  /* @__PURE__ */ jsx6("button", { className: "vpg-clear-link", onClick: clearAllFilters, children: "Clear all filters" })
3518
3615
  ] }),
3519
3616
  !loading && filteredRowCount > 0 && /* @__PURE__ */ jsx6("div", { className: "vpg-table-wrapper", children: /* @__PURE__ */ jsxs6("table", { className: "vpg-table", style: { minWidth: `${totalTableWidth}px` }, children: [
3520
- /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsx6("tr", { children: columnKeys.map((colId, colIndex) => /* @__PURE__ */ jsxs6(
3617
+ /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsx6("tr", { children: columnKeys.map((colId) => /* @__PURE__ */ jsxs6(
3521
3618
  "th",
3522
3619
  {
3523
3620
  className: `vpg-header-cell ${hasActiveFilter(colId) ? "vpg-has-filter" : ""} ${getSortDirection(colId) !== null ? "vpg-is-sorted" : ""} ${activeFilterColumn === colId ? "vpg-is-active" : ""}`,
@@ -3646,7 +3743,7 @@ function DataGrid({
3646
3743
  onShowColumnTotalsChange: setPivotShowColumnTotals,
3647
3744
  onClearConfig: clearPivotConfig,
3648
3745
  onAutoSuggest: autoSuggestConfig,
3649
- onDragStart: (field, e) => setDraggingField(field),
3746
+ onDragStart: (field, _e) => setDraggingField(field),
3650
3747
  onDragEnd: () => setDraggingField(null),
3651
3748
  onUpdateAggregation: updateValueFieldAggregation,
3652
3749
  onAddRowField: addRowField,
@@ -3698,11 +3795,13 @@ function DataGrid({
3698
3795
  totalSearchedRows !== totalRowCount && /* @__PURE__ */ jsxs6("span", { className: "vpg-filtered-note", children: [
3699
3796
  "(",
3700
3797
  totalRowCount.toLocaleString(),
3701
- " total)"
3798
+ " ",
3799
+ "total)"
3702
3800
  ] })
3703
3801
  ] }) : filteredRowCount === totalRowCount && totalSearchedRows === totalRowCount ? /* @__PURE__ */ jsxs6("span", { children: [
3704
3802
  totalRowCount.toLocaleString(),
3705
- " records"
3803
+ " ",
3804
+ "records"
3706
3805
  ] }) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
3707
3806
  /* @__PURE__ */ jsx6("span", { className: "vpg-filtered-count", children: totalSearchedRows.toLocaleString() }),
3708
3807
  /* @__PURE__ */ jsx6("span", { className: "vpg-separator", children: "of" }),
@@ -3713,7 +3812,8 @@ function DataGrid({
3713
3812
  /* @__PURE__ */ jsx6("span", { className: "vpg-separator", children: "\u2022" }),
3714
3813
  /* @__PURE__ */ jsxs6("span", { children: [
3715
3814
  totalRowCount.toLocaleString(),
3716
- " source records"
3815
+ " ",
3816
+ "source records"
3717
3817
  ] })
3718
3818
  ] }) }),
3719
3819
  enablePagination && viewMode === "grid" && totalPages > 1 && /* @__PURE__ */ jsxs6("div", { className: "vpg-pagination", children: [
@@ -3752,9 +3852,12 @@ function DataGrid({
3752
3852
  }
3753
3853
  ),
3754
3854
  /* @__PURE__ */ jsxs6("span", { className: "vpg-page-info", children: [
3755
- "Page ",
3855
+ "Page",
3856
+ " ",
3756
3857
  currentPage,
3757
- " of ",
3858
+ " ",
3859
+ "of",
3860
+ " ",
3758
3861
  totalPages
3759
3862
  ] }),
3760
3863
  /* @__PURE__ */ jsx6(