@smallwebco/tinypivot-react 1.0.48 → 1.0.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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);
1378
- } else {
1379
- setName("");
1380
- setFormula("");
1381
- setFormatAs("number");
1382
- setDecimals(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
+ }
1405
+ } else {
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);
@@ -2305,6 +2355,14 @@ function PivotSkeleton({
2305
2355
  [reorderDropTarget]
2306
2356
  );
2307
2357
  const currentFontSize = fontSize;
2358
+ const rowHeaderWidth = 180;
2359
+ const rowHeaderColWidth = useMemo9(() => {
2360
+ const numCols = Math.max(rowFields.length, 1);
2361
+ return Math.max(rowHeaderWidth / numCols, 80);
2362
+ }, [rowFields.length]);
2363
+ const getRowHeaderLeftOffset = useCallback9((fieldIdx) => {
2364
+ return fieldIdx * rowHeaderColWidth;
2365
+ }, [rowHeaderColWidth]);
2308
2366
  return /* @__PURE__ */ jsxs5(
2309
2367
  "div",
2310
2368
  {
@@ -2354,14 +2412,18 @@ function PivotSkeleton({
2354
2412
  }
2355
2413
  ),
2356
2414
  /* @__PURE__ */ jsxs5("span", { className: "vpg-filter-text", children: [
2357
- "Filtered: ",
2415
+ "Filtered:",
2416
+ " ",
2358
2417
  /* @__PURE__ */ jsx5("strong", { children: filterSummary }),
2359
2418
  filteredRowCount !== void 0 && totalRowCount !== void 0 && /* @__PURE__ */ jsxs5("span", { className: "vpg-filter-count", children: [
2360
2419
  "(",
2361
2420
  filteredRowCount.toLocaleString(),
2362
- " of ",
2421
+ " ",
2422
+ "of",
2423
+ " ",
2363
2424
  totalRowCount.toLocaleString(),
2364
- " rows)"
2425
+ " ",
2426
+ "rows)"
2365
2427
  ] })
2366
2428
  ] }),
2367
2429
  showFilterTooltip && /* @__PURE__ */ jsxs5("div", { className: "vpg-filter-tooltip", children: [
@@ -2373,16 +2435,21 @@ function PivotSkeleton({
2373
2435
  filter.remaining > 0 && /* @__PURE__ */ jsxs5("span", { className: "vpg-tooltip-more", children: [
2374
2436
  "+",
2375
2437
  filter.remaining,
2376
- " more"
2438
+ " ",
2439
+ "more"
2377
2440
  ] })
2378
2441
  ] }) })
2379
2442
  ] }, filter.column)),
2380
2443
  filteredRowCount !== void 0 && totalRowCount !== void 0 && /* @__PURE__ */ jsxs5("div", { className: "vpg-tooltip-summary", children: [
2381
- "Showing ",
2444
+ "Showing",
2445
+ " ",
2382
2446
  filteredRowCount.toLocaleString(),
2383
- " of ",
2447
+ " ",
2448
+ "of",
2449
+ " ",
2384
2450
  totalRowCount.toLocaleString(),
2385
- " rows"
2451
+ " ",
2452
+ "rows"
2386
2453
  ] })
2387
2454
  ] })
2388
2455
  ]
@@ -2391,17 +2458,20 @@ function PivotSkeleton({
2391
2458
  isConfigured && /* @__PURE__ */ jsxs5("div", { className: "vpg-config-summary", children: [
2392
2459
  /* @__PURE__ */ jsxs5("span", { className: "vpg-summary-badge vpg-rows", children: [
2393
2460
  rowFields.length,
2394
- " row",
2461
+ " ",
2462
+ "row",
2395
2463
  rowFields.length !== 1 ? "s" : ""
2396
2464
  ] }),
2397
2465
  /* @__PURE__ */ jsxs5("span", { className: "vpg-summary-badge vpg-cols", children: [
2398
2466
  columnFields.length,
2399
- " col",
2467
+ " ",
2468
+ "col",
2400
2469
  columnFields.length !== 1 ? "s" : ""
2401
2470
  ] }),
2402
2471
  /* @__PURE__ */ jsxs5("span", { className: "vpg-summary-badge vpg-vals", children: [
2403
2472
  valueFields.length,
2404
- " val",
2473
+ " ",
2474
+ "val",
2405
2475
  valueFields.length !== 1 ? "s" : ""
2406
2476
  ] })
2407
2477
  ] })
@@ -2572,31 +2642,39 @@ function PivotSkeleton({
2572
2642
  }
2573
2643
  ),
2574
2644
  /* @__PURE__ */ jsx5("span", { className: "vpg-placeholder-text", children: valueFields.length === 0 ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
2575
- "Add a ",
2645
+ "Add a",
2646
+ " ",
2576
2647
  /* @__PURE__ */ jsx5("strong", { children: "Values" }),
2577
- " field to see your pivot table"
2648
+ " ",
2649
+ "field to see your pivot table"
2578
2650
  ] }) : rowFields.length === 0 && columnFields.length === 0 ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
2579
- "Add ",
2651
+ "Add",
2652
+ " ",
2580
2653
  /* @__PURE__ */ jsx5("strong", { children: "Row" }),
2581
- " or ",
2654
+ " ",
2655
+ "or",
2656
+ " ",
2582
2657
  /* @__PURE__ */ jsx5("strong", { children: "Column" }),
2583
- " fields to group your data"
2658
+ " ",
2659
+ "fields to group your data"
2584
2660
  ] }) : "Your pivot table will appear here" })
2585
2661
  ] }) }),
2586
2662
  isConfigured && pivotResult && /* @__PURE__ */ jsx5("div", { className: "vpg-table-container", children: /* @__PURE__ */ jsxs5("table", { className: "vpg-pivot-table", children: [
2587
2663
  /* @__PURE__ */ jsx5("thead", { children: columnHeaderCells.map((headerRow, levelIdx) => /* @__PURE__ */ jsxs5("tr", { className: "vpg-column-header-row", children: [
2588
- levelIdx === 0 && /* @__PURE__ */ jsx5(
2664
+ levelIdx === 0 && (rowFields.length > 0 ? rowFields : ["Rows"]).map((field, fieldIdx) => /* @__PURE__ */ jsx5(
2589
2665
  "th",
2590
2666
  {
2591
2667
  className: "vpg-row-header-label",
2592
2668
  rowSpan: columnHeaderCells.length,
2669
+ style: { width: `${rowHeaderColWidth}px`, minWidth: "80px", left: `${getRowHeaderLeftOffset(fieldIdx)}px` },
2593
2670
  onClick: () => toggleSort("row"),
2594
2671
  children: /* @__PURE__ */ jsxs5("div", { className: "vpg-header-content", children: [
2595
- /* @__PURE__ */ jsx5("span", { children: rowFields.join(" / ") || "Rows" }),
2596
- /* @__PURE__ */ jsx5("span", { className: `vpg-sort-indicator ${sortTarget === "row" ? "active" : ""}`, children: sortTarget === "row" ? sortDirection === "asc" ? "\u2191" : "\u2193" : "\u21C5" })
2672
+ /* @__PURE__ */ jsx5("span", { children: field }),
2673
+ (fieldIdx === rowFields.length - 1 || rowFields.length === 0) && /* @__PURE__ */ jsx5("span", { className: `vpg-sort-indicator ${sortTarget === "row" ? "active" : ""}`, children: sortTarget === "row" ? sortDirection === "asc" ? "\u2191" : "\u2193" : "\u21C5" })
2597
2674
  ] })
2598
- }
2599
- ),
2675
+ },
2676
+ `row-header-${fieldIdx}`
2677
+ )),
2600
2678
  headerRow.map((cell, idx) => /* @__PURE__ */ jsx5(
2601
2679
  "th",
2602
2680
  {
@@ -2614,7 +2692,15 @@ function PivotSkeleton({
2614
2692
  ] }, `header-${levelIdx}`)) }),
2615
2693
  /* @__PURE__ */ jsxs5("tbody", { children: [
2616
2694
  sortedRowIndices.map((sortedIdx) => /* @__PURE__ */ jsxs5("tr", { className: "vpg-data-row", children: [
2617
- /* @__PURE__ */ jsx5("th", { className: "vpg-row-header-cell", children: pivotResult.rowHeaders[sortedIdx].map((val, idx) => /* @__PURE__ */ jsx5("span", { className: "vpg-row-value", children: val }, idx)) }),
2695
+ pivotResult.rowHeaders[sortedIdx].map((val, idx) => /* @__PURE__ */ jsx5(
2696
+ "th",
2697
+ {
2698
+ className: "vpg-row-header-cell",
2699
+ style: { width: `${rowHeaderColWidth}px`, minWidth: "80px", left: `${getRowHeaderLeftOffset(idx)}px` },
2700
+ children: val
2701
+ },
2702
+ `row-${sortedIdx}-${idx}`
2703
+ )),
2618
2704
  pivotResult.data[sortedIdx].map((cell, colIdx) => {
2619
2705
  const displayRowIdx = sortedRowIndices.indexOf(sortedIdx);
2620
2706
  return /* @__PURE__ */ jsx5(
@@ -2631,7 +2717,15 @@ function PivotSkeleton({
2631
2717
  pivotResult.rowTotals[sortedIdx] && /* @__PURE__ */ jsx5("td", { className: "vpg-data-cell vpg-total-cell", children: pivotResult.rowTotals[sortedIdx].formattedValue })
2632
2718
  ] }, sortedIdx)),
2633
2719
  pivotResult.columnTotals.length > 0 && /* @__PURE__ */ jsxs5("tr", { className: "vpg-totals-row", children: [
2634
- /* @__PURE__ */ jsx5("th", { className: "vpg-row-header-cell vpg-total-label", children: "Total" }),
2720
+ /* @__PURE__ */ jsx5(
2721
+ "th",
2722
+ {
2723
+ className: "vpg-row-header-cell vpg-total-label",
2724
+ colSpan: Math.max(rowFields.length, 1),
2725
+ style: { width: `${rowHeaderWidth}px` },
2726
+ children: "Total"
2727
+ }
2728
+ ),
2635
2729
  pivotResult.columnTotals.map((cell, colIdx) => /* @__PURE__ */ jsx5("td", { className: "vpg-data-cell vpg-total-cell", children: cell.formattedValue }, colIdx)),
2636
2730
  pivotResult.rowTotals.length > 0 && /* @__PURE__ */ jsx5("td", { className: "vpg-data-cell vpg-grand-total-cell", children: pivotResult.grandTotal.formattedValue })
2637
2731
  ] })
@@ -2640,9 +2734,11 @@ function PivotSkeleton({
2640
2734
  isConfigured && pivotResult && /* @__PURE__ */ jsxs5("div", { className: "vpg-skeleton-footer", children: [
2641
2735
  /* @__PURE__ */ jsxs5("span", { className: "vpg-footer-info", children: [
2642
2736
  pivotResult.rowHeaders.length,
2643
- " rows \xD7 ",
2737
+ " ",
2738
+ "rows \xD7",
2644
2739
  pivotResult.data[0]?.length || 0,
2645
- " columns"
2740
+ " ",
2741
+ "columns"
2646
2742
  ] }),
2647
2743
  selectionStats && selectionStats.count > 1 && /* @__PURE__ */ jsxs5("div", { className: "vpg-selection-stats", children: [
2648
2744
  /* @__PURE__ */ jsxs5("span", { className: "vpg-stat", children: [
@@ -2795,12 +2891,15 @@ function DataGrid({
2795
2891
  removeCalculatedField
2796
2892
  } = usePivotTable(filteredDataForPivot);
2797
2893
  const activeFilterInfo = useMemo10(() => {
2798
- if (activeFilters.length === 0) return null;
2894
+ if (activeFilters.length === 0)
2895
+ return null;
2799
2896
  return activeFilters.map((f) => {
2800
2897
  if (f.type === "range" && f.range) {
2801
2898
  const parts = [];
2802
- if (f.range.min !== null) parts.push(`\u2265 ${f.range.min}`);
2803
- 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}`);
2804
2903
  return {
2805
2904
  column: f.column,
2806
2905
  valueCount: 1,
@@ -2825,7 +2924,8 @@ function DataGrid({
2825
2924
  return rows.filter((row) => {
2826
2925
  for (const col of columnKeys) {
2827
2926
  const value = row.original[col];
2828
- if (value === null || value === void 0) continue;
2927
+ if (value === null || value === void 0)
2928
+ continue;
2829
2929
  if (String(value).toLowerCase().includes(term)) {
2830
2930
  return true;
2831
2931
  }
@@ -2835,11 +2935,13 @@ function DataGrid({
2835
2935
  }, [rows, globalSearchTerm, enableSearch, columnKeys]);
2836
2936
  const totalSearchedRows = searchFilteredData.length;
2837
2937
  const totalPages = useMemo10(() => {
2838
- if (!enablePagination) return 1;
2938
+ if (!enablePagination)
2939
+ return 1;
2839
2940
  return Math.max(1, Math.ceil(totalSearchedRows / pageSize));
2840
2941
  }, [enablePagination, totalSearchedRows, pageSize]);
2841
2942
  const paginatedRows = useMemo10(() => {
2842
- if (!enablePagination) return searchFilteredData;
2943
+ if (!enablePagination)
2944
+ return searchFilteredData;
2843
2945
  const start = (currentPage - 1) * pageSize;
2844
2946
  const end = start + pageSize;
2845
2947
  return searchFilteredData.slice(start, end);
@@ -2848,7 +2950,8 @@ function DataGrid({
2848
2950
  setCurrentPage(1);
2849
2951
  }, [columnFilters, globalSearchTerm]);
2850
2952
  const selectionBounds = useMemo10(() => {
2851
- if (!selectionStart || !selectionEnd) return null;
2953
+ if (!selectionStart || !selectionEnd)
2954
+ return null;
2852
2955
  return {
2853
2956
  minRow: Math.min(selectionStart.row, selectionEnd.row),
2854
2957
  maxRow: Math.max(selectionStart.row, selectionEnd.row),
@@ -2857,16 +2960,19 @@ function DataGrid({
2857
2960
  };
2858
2961
  }, [selectionStart, selectionEnd]);
2859
2962
  const selectionStats = useMemo10(() => {
2860
- if (!selectionBounds) return null;
2963
+ if (!selectionBounds)
2964
+ return null;
2861
2965
  const { minRow, maxRow, minCol, maxCol } = selectionBounds;
2862
2966
  const values = [];
2863
2967
  let count = 0;
2864
2968
  for (let r = minRow; r <= maxRow; r++) {
2865
2969
  const row = rows[r];
2866
- if (!row) continue;
2970
+ if (!row)
2971
+ continue;
2867
2972
  for (let c = minCol; c <= maxCol; c++) {
2868
2973
  const colId = columnKeys[c];
2869
- if (!colId) continue;
2974
+ if (!colId)
2975
+ continue;
2870
2976
  const value = row.original[colId];
2871
2977
  count++;
2872
2978
  if (value !== null && value !== void 0 && value !== "") {
@@ -2877,19 +2983,23 @@ function DataGrid({
2877
2983
  }
2878
2984
  }
2879
2985
  }
2880
- 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 };
2881
2988
  const sum = values.reduce((a, b) => a + b, 0);
2882
2989
  const avg = sum / values.length;
2883
2990
  return { count, sum, avg, numericCount: values.length };
2884
2991
  }, [selectionBounds, rows, columnKeys]);
2885
2992
  useEffect7(() => {
2886
- if (typeof document === "undefined") return;
2887
- if (data.length === 0) return;
2993
+ if (typeof document === "undefined")
2994
+ return;
2995
+ if (data.length === 0)
2996
+ return;
2888
2997
  const widths = {};
2889
2998
  const sampleSize = Math.min(100, data.length);
2890
2999
  const canvas = document.createElement("canvas");
2891
3000
  const ctx = canvas.getContext("2d");
2892
- if (!ctx) return;
3001
+ if (!ctx)
3002
+ return;
2893
3003
  ctx.font = "13px system-ui, -apple-system, sans-serif";
2894
3004
  for (const key of columnKeys) {
2895
3005
  let maxWidth = ctx.measureText(key).width + 56;
@@ -2905,7 +3015,8 @@ function DataGrid({
2905
3015
  }, [data, columnKeys]);
2906
3016
  const startColumnResize = useCallback10(
2907
3017
  (columnId, event) => {
2908
- if (!enableColumnResize) return;
3018
+ if (!enableColumnResize)
3019
+ return;
2909
3020
  event.preventDefault();
2910
3021
  event.stopPropagation();
2911
3022
  setResizingColumnId(columnId);
@@ -2915,7 +3026,8 @@ function DataGrid({
2915
3026
  [enableColumnResize, columnWidths]
2916
3027
  );
2917
3028
  useEffect7(() => {
2918
- if (!resizingColumnId) return;
3029
+ if (!resizingColumnId)
3030
+ return;
2919
3031
  const handleResizeMove = (event) => {
2920
3032
  const diff = event.clientX - resizeStartX;
2921
3033
  const newWidth = Math.max(MIN_COL_WIDTH, Math.min(MAX_COL_WIDTH, resizeStartWidth + diff));
@@ -2936,7 +3048,8 @@ function DataGrid({
2936
3048
  }, [resizingColumnId, resizeStartX, resizeStartWidth]);
2937
3049
  const startVerticalResize = useCallback10(
2938
3050
  (event) => {
2939
- if (!enableVerticalResize) return;
3051
+ if (!enableVerticalResize)
3052
+ return;
2940
3053
  event.preventDefault();
2941
3054
  setIsResizingVertically(true);
2942
3055
  setVerticalResizeStartY(event.clientY);
@@ -2945,7 +3058,8 @@ function DataGrid({
2945
3058
  [enableVerticalResize, gridHeight]
2946
3059
  );
2947
3060
  useEffect7(() => {
2948
- if (!isResizingVertically) return;
3061
+ if (!isResizingVertically)
3062
+ return;
2949
3063
  const handleVerticalResizeMove = (event) => {
2950
3064
  const diff = event.clientY - verticalResizeStartY;
2951
3065
  const newHeight = Math.max(minHeight, Math.min(maxHeight, verticalResizeStartHeight + diff));
@@ -2963,7 +3077,8 @@ function DataGrid({
2963
3077
  }, [isResizingVertically, verticalResizeStartY, verticalResizeStartHeight, minHeight, maxHeight]);
2964
3078
  const handleExport = useCallback10(() => {
2965
3079
  if (viewMode === "pivot") {
2966
- if (!pivotResult) return;
3080
+ if (!pivotResult)
3081
+ return;
2967
3082
  const pivotFilename = exportFilename.replace(".csv", "-pivot.csv");
2968
3083
  exportPivotToCSV(
2969
3084
  {
@@ -3007,7 +3122,8 @@ function DataGrid({
3007
3122
  onExport
3008
3123
  ]);
3009
3124
  const copySelectionToClipboard = useCallback10(() => {
3010
- if (!selectionBounds || !enableClipboard) return;
3125
+ if (!selectionBounds || !enableClipboard)
3126
+ return;
3011
3127
  const text = formatSelectionForClipboard(
3012
3128
  rows.map((r) => r.original),
3013
3129
  columnKeys,
@@ -3164,7 +3280,8 @@ function DataGrid({
3164
3280
  [selectionBounds, selectedCell]
3165
3281
  );
3166
3282
  const formatStatValue = (value) => {
3167
- if (value === null) return "-";
3283
+ if (value === null)
3284
+ return "-";
3168
3285
  if (Math.abs(value) >= 1e3) {
3169
3286
  return value.toLocaleString("en-US", { maximumFractionDigits: 2 });
3170
3287
  }
@@ -3175,12 +3292,15 @@ function DataGrid({
3175
3292
  return !noFormatPatterns.test(columnId);
3176
3293
  };
3177
3294
  const formatCellValueDisplay = (value, columnId) => {
3178
- if (value === null || value === void 0) return "";
3179
- if (value === "") return "";
3295
+ if (value === null || value === void 0)
3296
+ return "";
3297
+ if (value === "")
3298
+ return "";
3180
3299
  const stats = getColumnStats(columnId);
3181
3300
  if (stats.type === "number") {
3182
3301
  const num = typeof value === "number" ? value : Number.parseFloat(String(value));
3183
- if (Number.isNaN(num)) return String(value);
3302
+ if (Number.isNaN(num))
3303
+ return String(value);
3184
3304
  if (shouldFormatNumber(columnId) && Math.abs(num) >= 1e3) {
3185
3305
  return num.toLocaleString("en-US", { maximumFractionDigits: 2 });
3186
3306
  }
@@ -3342,13 +3462,15 @@ function DataGrid({
3342
3462
  ) }),
3343
3463
  /* @__PURE__ */ jsxs6("span", { children: [
3344
3464
  activeFilterCount,
3345
- " filter",
3465
+ " ",
3466
+ "filter",
3346
3467
  activeFilterCount > 1 ? "s" : ""
3347
3468
  ] })
3348
3469
  ] }),
3349
3470
  globalSearchTerm && /* @__PURE__ */ jsx6("div", { className: "vpg-search-info", children: /* @__PURE__ */ jsxs6("span", { children: [
3350
3471
  totalSearchedRows,
3351
- " match",
3472
+ " ",
3473
+ "match",
3352
3474
  totalSearchedRows !== 1 ? "es" : ""
3353
3475
  ] }) })
3354
3476
  ] }),
@@ -3369,7 +3491,8 @@ function DataGrid({
3369
3491
  }
3370
3492
  ) }),
3371
3493
  showPivotConfig ? "Hide" : "Show",
3372
- " Config"
3494
+ " ",
3495
+ "Config"
3373
3496
  ]
3374
3497
  }
3375
3498
  ),
@@ -3491,7 +3614,7 @@ function DataGrid({
3491
3614
  /* @__PURE__ */ jsx6("button", { className: "vpg-clear-link", onClick: clearAllFilters, children: "Clear all filters" })
3492
3615
  ] }),
3493
3616
  !loading && filteredRowCount > 0 && /* @__PURE__ */ jsx6("div", { className: "vpg-table-wrapper", children: /* @__PURE__ */ jsxs6("table", { className: "vpg-table", style: { minWidth: `${totalTableWidth}px` }, children: [
3494
- /* @__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(
3495
3618
  "th",
3496
3619
  {
3497
3620
  className: `vpg-header-cell ${hasActiveFilter(colId) ? "vpg-has-filter" : ""} ${getSortDirection(colId) !== null ? "vpg-is-sorted" : ""} ${activeFilterColumn === colId ? "vpg-is-active" : ""}`,
@@ -3620,7 +3743,7 @@ function DataGrid({
3620
3743
  onShowColumnTotalsChange: setPivotShowColumnTotals,
3621
3744
  onClearConfig: clearPivotConfig,
3622
3745
  onAutoSuggest: autoSuggestConfig,
3623
- onDragStart: (field, e) => setDraggingField(field),
3746
+ onDragStart: (field, _e) => setDraggingField(field),
3624
3747
  onDragEnd: () => setDraggingField(null),
3625
3748
  onUpdateAggregation: updateValueFieldAggregation,
3626
3749
  onAddRowField: addRowField,
@@ -3672,11 +3795,13 @@ function DataGrid({
3672
3795
  totalSearchedRows !== totalRowCount && /* @__PURE__ */ jsxs6("span", { className: "vpg-filtered-note", children: [
3673
3796
  "(",
3674
3797
  totalRowCount.toLocaleString(),
3675
- " total)"
3798
+ " ",
3799
+ "total)"
3676
3800
  ] })
3677
3801
  ] }) : filteredRowCount === totalRowCount && totalSearchedRows === totalRowCount ? /* @__PURE__ */ jsxs6("span", { children: [
3678
3802
  totalRowCount.toLocaleString(),
3679
- " records"
3803
+ " ",
3804
+ "records"
3680
3805
  ] }) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
3681
3806
  /* @__PURE__ */ jsx6("span", { className: "vpg-filtered-count", children: totalSearchedRows.toLocaleString() }),
3682
3807
  /* @__PURE__ */ jsx6("span", { className: "vpg-separator", children: "of" }),
@@ -3687,7 +3812,8 @@ function DataGrid({
3687
3812
  /* @__PURE__ */ jsx6("span", { className: "vpg-separator", children: "\u2022" }),
3688
3813
  /* @__PURE__ */ jsxs6("span", { children: [
3689
3814
  totalRowCount.toLocaleString(),
3690
- " source records"
3815
+ " ",
3816
+ "source records"
3691
3817
  ] })
3692
3818
  ] }) }),
3693
3819
  enablePagination && viewMode === "grid" && totalPages > 1 && /* @__PURE__ */ jsxs6("div", { className: "vpg-pagination", children: [
@@ -3726,9 +3852,12 @@ function DataGrid({
3726
3852
  }
3727
3853
  ),
3728
3854
  /* @__PURE__ */ jsxs6("span", { className: "vpg-page-info", children: [
3729
- "Page ",
3855
+ "Page",
3856
+ " ",
3730
3857
  currentPage,
3731
- " of ",
3858
+ " ",
3859
+ "of",
3860
+ " ",
3732
3861
  totalPages
3733
3862
  ] }),
3734
3863
  /* @__PURE__ */ jsx6(