@undevy-org/tablewright 0.1.0 → 0.2.0

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.
@@ -2,6 +2,7 @@ import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import { X } from "lucide-react";
3
3
  import { Button } from "../ui/button.js";
4
4
  import { FilterChip } from "./FilterChip.js";
5
+ import { isFilterValueEmpty } from "./filter-values.js";
5
6
  import { ColumnFilterPopover } from "./ColumnFilterPopover.js";
6
7
  import { CompoundFilterPopover } from "./CompoundFilterPopover.js";
7
8
  const DEFAULT_LABELS = {
@@ -9,16 +10,6 @@ const DEFAULT_LABELS = {
9
10
  columns: "Columns",
10
11
  hiddenCount: (n) => `${n} hidden`
11
12
  };
12
- function isFilterValueEmpty(value) {
13
- if (Array.isArray(value)) return value.length === 0;
14
- if (typeof value === "string") return value.length === 0;
15
- if (typeof value === "object" && value !== null) {
16
- const keys = Object.keys(value);
17
- if (keys.length === 0) return true;
18
- return keys.every((k) => isFilterValueEmpty(value[k]));
19
- }
20
- return false;
21
- }
22
13
  function AppliedStateChips({
23
14
  filters,
24
15
  configs,
@@ -5,6 +5,7 @@ import { Checkbox } from "../ui/checkbox.js";
5
5
  import { Popover, PopoverTrigger, PopoverContent } from "../ui/popover.js";
6
6
  import { FilterChip } from "./FilterChip.js";
7
7
  import { FilterPopoverFooter } from "./FilterPopoverFooter.js";
8
+ import { getStringArray, getNumberRange, getDateRange, isFilterValueEmpty } from "./filter-values.js";
8
9
  const DEFAULT_LABELS = {
9
10
  searchPlaceholder: "Search...",
10
11
  addValuePlaceholder: "Add value...",
@@ -26,14 +27,18 @@ function formatChipDate(iso) {
26
27
  return d.toLocaleDateString(void 0, { day: "2-digit", month: "short" });
27
28
  }
28
29
  function isEmptyValue(value, type) {
29
- if (type === "text") return !Array.isArray(value) || value.length === 0;
30
- if (type === "enum") return !Array.isArray(value) || value.length === 0;
31
- if (type === "date")
32
- return typeof value !== "object" || Array.isArray(value) || !value.from && !value.to;
33
- if (type === "number-range")
34
- return typeof value !== "object" || Array.isArray(value) || value.from === void 0 && value.to === void 0;
30
+ if (type === "text" || type === "enum") return isFilterValueEmpty(getStringArray(value));
31
+ if (type === "date") return isFilterValueEmpty(getDateRange(value));
32
+ if (type === "number-range") return isFilterValueEmpty(getNumberRange(value));
35
33
  return true;
36
34
  }
35
+ function dateDraft(value) {
36
+ const { from = "", to = "" } = getDateRange(value);
37
+ return { from, to };
38
+ }
39
+ function numberDraft(bound) {
40
+ return bound !== void 0 ? String(bound) : "";
41
+ }
37
42
  function ColumnFilterPopover({
38
43
  config,
39
44
  filter,
@@ -64,47 +69,36 @@ function ColumnFilterPopover({
64
69
  return () => clearTimeout(id);
65
70
  }, [requestOpen]);
66
71
  const [draftTags, setDraftTags] = useState(
67
- Array.isArray(filter.value) && config.type === "text" ? filter.value : []
72
+ config.type === "text" ? getStringArray(filter.value) : []
68
73
  );
69
74
  const [draftInput, setDraftInput] = useState("");
70
- const [draftEnum, setDraftEnum] = useState(
71
- Array.isArray(filter.value) ? filter.value : []
72
- );
73
- const [draftDate, setDraftDate] = useState(
74
- typeof filter.value === "object" && !Array.isArray(filter.value) ? {
75
- from: String(filter.value.from ?? ""),
76
- to: String(filter.value.to ?? "")
77
- } : { from: "", to: "" }
78
- );
75
+ const [draftEnum, setDraftEnum] = useState(getStringArray(filter.value));
76
+ const [draftDate, setDraftDate] = useState(dateDraft(filter.value));
79
77
  const [draftNumberFrom, setDraftNumberFrom] = useState(
80
- typeof filter.value === "object" && !Array.isArray(filter.value) && typeof filter.value.from === "number" ? String(filter.value.from) : ""
81
- );
82
- const [draftNumberTo, setDraftNumberTo] = useState(
83
- typeof filter.value === "object" && !Array.isArray(filter.value) && typeof filter.value.to === "number" ? String(filter.value.to) : ""
78
+ numberDraft(getNumberRange(filter.value).from)
84
79
  );
80
+ const [draftNumberTo, setDraftNumberTo] = useState(numberDraft(getNumberRange(filter.value).to));
81
+ const resetDrafts = (value) => {
82
+ setDraftTags(config.type === "text" ? getStringArray(value) : []);
83
+ setDraftInput("");
84
+ setDraftEnum(getStringArray(value));
85
+ setDraftDate(dateDraft(value));
86
+ const numVal = getNumberRange(value);
87
+ setDraftNumberFrom(numberDraft(numVal.from));
88
+ setDraftNumberTo(numberDraft(numVal.to));
89
+ };
90
+ const [draftSource, setDraftSource] = useState(filter.value);
91
+ if (!isOpen && draftSource !== filter.value) {
92
+ setDraftSource(filter.value);
93
+ resetDrafts(filter.value);
94
+ }
85
95
  const handleOpenChange = (nextOpen) => {
86
96
  if (!nextOpen) {
87
97
  if (isEmptyValue(filter.value, config.type)) {
88
98
  onRemove();
89
99
  return;
90
100
  }
91
- setDraftTags(Array.isArray(filter.value) && config.type === "text" ? filter.value : []);
92
- setDraftInput("");
93
- setDraftEnum(Array.isArray(filter.value) ? filter.value : []);
94
- setDraftDate(
95
- typeof filter.value === "object" && !Array.isArray(filter.value) ? {
96
- from: String(filter.value.from ?? ""),
97
- to: String(filter.value.to ?? "")
98
- } : { from: "", to: "" }
99
- );
100
- if (typeof filter.value === "object" && !Array.isArray(filter.value)) {
101
- const numVal = filter.value;
102
- setDraftNumberFrom(typeof numVal.from === "number" ? String(numVal.from) : "");
103
- setDraftNumberTo(typeof numVal.to === "number" ? String(numVal.to) : "");
104
- } else {
105
- setDraftNumberFrom("");
106
- setDraftNumberTo("");
107
- }
101
+ resetDrafts(filter.value);
108
102
  }
109
103
  setIsOpen(nextOpen);
110
104
  };
@@ -504,14 +498,14 @@ function ColumnFilterPopover({
504
498
  }
505
499
  return DEFAULT_LABELS.selectedCount(value.length);
506
500
  }
507
- if (config.type === "date" && typeof value === "object" && !Array.isArray(value)) {
508
- const { from, to } = value;
501
+ if (config.type === "date") {
502
+ const { from, to } = getDateRange(value);
509
503
  if (from && to) return `${from} — ${to}`;
510
504
  if (from) return `${DEFAULT_LABELS.chipFrom} ${from}`;
511
505
  if (to) return `${DEFAULT_LABELS.chipTo} ${to}`;
512
506
  }
513
- if (config.type === "number-range" && typeof value === "object" && !Array.isArray(value)) {
514
- const { from, to } = value;
507
+ if (config.type === "number-range") {
508
+ const { from, to } = getNumberRange(value);
515
509
  if (from !== void 0 && to !== void 0) return `${from}–${to}`;
516
510
  if (from !== void 0) return `≥${from}`;
517
511
  if (to !== void 0) return `≤${to}`;
@@ -5,6 +5,7 @@ import { Checkbox } from "../ui/checkbox.js";
5
5
  import { Popover, PopoverTrigger, PopoverContent } from "../ui/popover.js";
6
6
  import { FilterChip } from "./FilterChip.js";
7
7
  import { FilterPopoverFooter } from "./FilterPopoverFooter.js";
8
+ import { getCompoundValue, isFilterValueEmpty, getNumberRange, getStringArray } from "./filter-values.js";
8
9
  const DEFAULT_LABELS = {
9
10
  searchPlaceholder: "Search...",
10
11
  addPlaceholder: "Add...",
@@ -14,25 +15,10 @@ const DEFAULT_LABELS = {
14
15
  statsAllPrefix: "All:",
15
16
  statsFilteredPrefix: "Filt:"
16
17
  };
17
- function isCompoundEmpty(value) {
18
- if (typeof value !== "object" || Array.isArray(value) || value === null) return true;
19
- const obj = value;
20
- return Object.keys(obj).every((k) => isSubValueEmpty(obj[k]));
21
- }
22
- function isSubValueEmpty(value) {
23
- if (value === void 0 || value === null) return true;
24
- if (Array.isArray(value)) return value.length === 0;
25
- if (typeof value === "string") return value.length === 0;
26
- if (typeof value === "object") {
27
- const obj = value;
28
- return obj.from === void 0 && obj.to === void 0;
29
- }
30
- return true;
31
- }
32
18
  function initDraft(sub, compound) {
33
19
  const val = compound[sub.key];
34
20
  if (sub.type === "number-range") {
35
- const range = typeof val === "object" && !Array.isArray(val) ? val : {};
21
+ const range = getNumberRange(val);
36
22
  return {
37
23
  type: "number-range",
38
24
  from: range.from !== void 0 ? String(range.from) : "",
@@ -40,11 +26,11 @@ function initDraft(sub, compound) {
40
26
  };
41
27
  }
42
28
  if (sub.type === "enum") {
43
- return { type: "enum", selected: Array.isArray(val) ? val : [] };
29
+ return { type: "enum", selected: getStringArray(val) };
44
30
  }
45
31
  return {
46
32
  type: "text",
47
- tags: Array.isArray(val) ? val : [],
33
+ tags: getStringArray(val),
48
34
  input: ""
49
35
  };
50
36
  }
@@ -63,9 +49,9 @@ function draftToValue(draft) {
63
49
  return all.length > 0 ? all : void 0;
64
50
  }
65
51
  function formatSubDisplay(sub, val) {
66
- if (isSubValueEmpty(val)) return null;
67
- if (sub.type === "number-range" && typeof val === "object" && !Array.isArray(val)) {
68
- const { from, to } = val;
52
+ if (isFilterValueEmpty(val)) return null;
53
+ if (sub.type === "number-range") {
54
+ const { from, to } = getNumberRange(val);
69
55
  const label = sub.label.replace(/\s+/g, " ").split(" ").pop() ?? sub.label;
70
56
  if (from !== void 0 && to !== void 0) return `${label}: ${from}–${to}`;
71
57
  if (from !== void 0) return `${label}: ≥${from}`;
@@ -99,17 +85,23 @@ function CompoundFilterPopover({
99
85
  onRequestOpenHandled
100
86
  }) {
101
87
  const subFilters = config.subFilters ?? [];
102
- const compound = typeof filter.value === "object" && !Array.isArray(filter.value) ? filter.value : {};
88
+ const compound = getCompoundValue(filter.value);
103
89
  const [isOpen, setIsOpen] = useState(false);
104
90
  const onRequestOpenHandledRef = useRef(onRequestOpenHandled);
105
91
  useEffect(() => {
106
92
  onRequestOpenHandledRef.current = onRequestOpenHandled;
107
93
  });
108
- const [drafts, setDrafts] = useState(() => {
94
+ const initDrafts = () => {
109
95
  const d = {};
110
96
  for (const sub of subFilters) d[sub.key] = initDraft(sub, compound);
111
97
  return d;
112
- });
98
+ };
99
+ const [drafts, setDrafts] = useState(initDrafts);
100
+ const [draftSource, setDraftSource] = useState(filter.value);
101
+ if (!isOpen && draftSource !== filter.value) {
102
+ setDraftSource(filter.value);
103
+ setDrafts(initDrafts());
104
+ }
113
105
  useEffect(() => {
114
106
  if (!initialOpen) return;
115
107
  const id = setTimeout(() => setIsOpen(true), 0);
@@ -125,13 +117,11 @@ function CompoundFilterPopover({
125
117
  }, [requestOpen]);
126
118
  const handleOpenChange = (nextOpen) => {
127
119
  if (!nextOpen) {
128
- if (isCompoundEmpty(filter.value)) {
120
+ if (isFilterValueEmpty(compound)) {
129
121
  onRemove();
130
122
  return;
131
123
  }
132
- const d = {};
133
- for (const sub of subFilters) d[sub.key] = initDraft(sub, compound);
134
- setDrafts(d);
124
+ setDrafts(initDrafts());
135
125
  }
136
126
  setIsOpen(nextOpen);
137
127
  };
@@ -166,7 +156,7 @@ function CompoundFilterPopover({
166
156
  });
167
157
  };
168
158
  const getDisplayValue = () => {
169
- if (isCompoundEmpty(filter.value)) return void 0;
159
+ if (isFilterValueEmpty(compound)) return void 0;
170
160
  const parts = [];
171
161
  for (const sub of subFilters) {
172
162
  const display = formatSubDisplay(sub, compound[sub.key]);
@@ -6,16 +6,8 @@ import { Button } from "../ui/button.js";
6
6
  import { ButtonFooter } from "../ui/button-footer.js";
7
7
  import { Input } from "../ui/input.js";
8
8
  import { Combobox } from "../ui/combobox.js";
9
+ import { isFilterValueEmpty, getDateRange, getNumberRange, getCompoundValue } from "./filter-values.js";
9
10
  import { hasActiveManagedFilters } from "./managed-filters.js";
10
- function isValueEmpty(v) {
11
- if (v === void 0) return true;
12
- if (Array.isArray(v)) return v.length === 0 || v.every((x) => x === "" || x == null);
13
- if (typeof v === "string") return v.length === 0;
14
- if (typeof v === "object" && v !== null) {
15
- return Object.values(v).every((x) => isValueEmpty(x));
16
- }
17
- return false;
18
- }
19
11
  function deriveInitialState(fields, filters) {
20
12
  const byId = new Map(filters.map((f) => [f.columnId, f.value]));
21
13
  const next = {};
@@ -71,7 +63,7 @@ function FilterFormPanel({
71
63
  const changes = [];
72
64
  for (const id2 of renderableFields) {
73
65
  const value = state[id2];
74
- if (isValueEmpty(value)) {
66
+ if (value === void 0 || isFilterValueEmpty(value)) {
75
67
  if (activeIds.has(id2)) changes.push({ columnId: id2, action: "remove" });
76
68
  } else {
77
69
  changes.push({ columnId: id2, action: "set", value });
@@ -85,7 +77,7 @@ function FilterFormPanel({
85
77
  onClear(renderableFields);
86
78
  touchedRef.current.clear();
87
79
  }, [onClear, renderableFields]);
88
- const isClearDisabled = !hasActiveManagedFilters(filters, renderableFields) && renderableFields.every((fieldId) => isValueEmpty(state[fieldId]));
80
+ const isClearDisabled = !hasActiveManagedFilters(filters, renderableFields) && renderableFields.every((fieldId) => isFilterValueEmpty(state[fieldId]));
89
81
  return /* @__PURE__ */ jsxs(
90
82
  "form",
91
83
  {
@@ -186,12 +178,11 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
186
178
  ] });
187
179
  }
188
180
  if (config.type === "date") {
189
- const range = value && !Array.isArray(value) && typeof value === "object" ? value : {};
181
+ const range = getDateRange(value);
190
182
  const update = (key) => (e) => {
191
183
  const v = e.target.value;
192
184
  const next = { ...range, [key]: v || void 0 };
193
- const cleaned = next.from || next.to ? next : void 0;
194
- onChange(cleaned);
185
+ onChange(next.from || next.to ? next : void 0);
195
186
  };
196
187
  return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1.5 [grid-column:span_2]", children: [
197
188
  /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-[var(--text-secondary)]", children: config.label }),
@@ -220,13 +211,12 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
220
211
  ] });
221
212
  }
222
213
  if (config.type === "number-range") {
223
- const range = value && !Array.isArray(value) && typeof value === "object" ? value : {};
214
+ const range = getNumberRange(value);
224
215
  const update = (key) => (e) => {
225
216
  const raw = e.target.value;
226
217
  const parsed = raw === "" ? void 0 : Number(raw);
227
218
  const next = { ...range, [key]: Number.isFinite(parsed) ? parsed : void 0 };
228
- const cleaned = next.from !== void 0 || next.to !== void 0 ? next : void 0;
229
- onChange(cleaned);
219
+ onChange(next.from !== void 0 || next.to !== void 0 ? next : void 0);
230
220
  };
231
221
  return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1.5 [grid-column:span_2]", children: [
232
222
  /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-[var(--text-secondary)]", children: config.label }),
@@ -257,11 +247,11 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
257
247
  ] });
258
248
  }
259
249
  if (config.type === "compound") {
260
- const compound = value && !Array.isArray(value) && typeof value === "object" ? value : {};
261
- const updateSub = (subField, next) => {
250
+ const compound = getCompoundValue(value);
251
+ const updateSub = (subKey, next) => {
262
252
  const rest = { ...compound };
263
- delete rest[subField];
264
- const merged = next === void 0 || isValueEmpty(next) ? rest : { ...rest, [subField]: next };
253
+ delete rest[subKey];
254
+ const merged = next === void 0 || isFilterValueEmpty(next) ? rest : { ...rest, [subKey]: next };
265
255
  onChange(Object.keys(merged).length === 0 ? void 0 : merged);
266
256
  };
267
257
  const visibleSubs = (config.subFilters ?? []).filter(
@@ -277,7 +267,7 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
277
267
  children: [
278
268
  /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-[var(--text-secondary)]", children: config.label }),
279
269
  /* @__PURE__ */ jsx("div", { className: "grid min-w-0 gap-2 grid-cols-[repeat(auto-fit,minmax(180px,1fr))]", children: visibleSubs.map((sub) => {
280
- const subValue = compound[sub.field];
270
+ const subValue = compound[sub.key];
281
271
  if (sub.type === "text") {
282
272
  const text = Array.isArray(subValue) ? subValue[0] ?? "" : "";
283
273
  return /* @__PURE__ */ jsx(
@@ -288,7 +278,7 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
288
278
  value: text,
289
279
  onChange: (e) => {
290
280
  const v = e.target.value;
291
- updateSub(sub.field, v === "" ? void 0 : [v]);
281
+ updateSub(sub.key, v === "" ? void 0 : [v]);
292
282
  },
293
283
  className: "h-9 min-w-0"
294
284
  },
@@ -301,7 +291,7 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
301
291
  Combobox,
302
292
  {
303
293
  value: selected || null,
304
- onChange: (v) => updateSub(sub.field, v ? [v] : void 0),
294
+ onChange: (v) => updateSub(sub.key, v ? [v] : void 0),
305
295
  options: sub.options ?? [],
306
296
  placeholder: sub.label,
307
297
  triggerAriaLabel: sub.label,
@@ -311,13 +301,18 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
311
301
  );
312
302
  }
313
303
  if (sub.type === "number-range") {
314
- const range = subValue && !Array.isArray(subValue) && typeof subValue === "object" ? subValue : {};
304
+ const range = getNumberRange(subValue);
315
305
  const updateRange = (key) => (e) => {
316
306
  const raw = e.target.value;
317
307
  const parsed = raw === "" ? void 0 : Number(raw);
318
- const next = { ...range, [key]: Number.isFinite(parsed) ? parsed : void 0 };
319
- const cleaned = next.from !== void 0 || next.to !== void 0 ? next : void 0;
320
- updateSub(sub.field, cleaned);
308
+ const next = {
309
+ ...range,
310
+ [key]: Number.isFinite(parsed) ? parsed : void 0
311
+ };
312
+ updateSub(
313
+ sub.key,
314
+ next.from !== void 0 || next.to !== void 0 ? next : void 0
315
+ );
321
316
  };
322
317
  return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 gap-2", children: [
323
318
  /* @__PURE__ */ jsx(
@@ -0,0 +1,36 @@
1
+ function isObjectValue(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ function getStringArray(value) {
5
+ return Array.isArray(value) ? value : [];
6
+ }
7
+ function getDateRange(value) {
8
+ const range = {};
9
+ if (!isObjectValue(value)) return range;
10
+ if (typeof value.from === "string") range.from = value.from;
11
+ if (typeof value.to === "string") range.to = value.to;
12
+ return range;
13
+ }
14
+ function getNumberRange(value) {
15
+ const range = {};
16
+ if (!isObjectValue(value)) return range;
17
+ if (typeof value.from === "number") range.from = value.from;
18
+ if (typeof value.to === "number") range.to = value.to;
19
+ return range;
20
+ }
21
+ function getCompoundValue(value) {
22
+ return isObjectValue(value) ? value : {};
23
+ }
24
+ function isFilterValueEmpty(value) {
25
+ if (value === void 0 || value === null) return true;
26
+ if (typeof value === "string") return value.length === 0;
27
+ if (Array.isArray(value)) return value.length === 0;
28
+ return Object.values(value).every((x) => typeof x !== "number" && isFilterValueEmpty(x));
29
+ }
30
+ export {
31
+ getCompoundValue,
32
+ getDateRange,
33
+ getNumberRange,
34
+ getStringArray,
35
+ isFilterValueEmpty
36
+ };
@@ -1,3 +1,4 @@
1
+ import { isFilterValueEmpty } from "./filter-values.js";
1
2
  function applyManagedChanges(orch, changes) {
2
3
  const activeIds = new Set(orch.columnFilters.map((f) => f.columnId));
3
4
  for (const change of changes) {
@@ -14,15 +15,6 @@ function clearManagedFilters(orch, managedColumnIds) {
14
15
  if (activeIds.has(id)) orch.handleRemoveColumnFilter(id);
15
16
  }
16
17
  }
17
- function isFilterValueEmpty(v) {
18
- if (v === void 0) return true;
19
- if (Array.isArray(v)) return v.length === 0 || v.every((x) => x === "" || x == null);
20
- if (typeof v === "string") return v.length === 0;
21
- if (typeof v === "object" && v !== null) {
22
- return Object.values(v).every((x) => isFilterValueEmpty(x));
23
- }
24
- return false;
25
- }
26
18
  function hasActiveManagedFilters(filters, managedFieldIds) {
27
19
  const managed = new Set(managedFieldIds);
28
20
  return filters.some((f) => managed.has(f.columnId) && !isFilterValueEmpty(f.value));
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useMemo, useRef, useCallback } from "react";
1
+ import { useState, useEffect, useMemo, useRef, useCallback, useLayoutEffect } from "react";
2
2
  import { useStore } from "zustand";
3
3
  import { useColumnResize } from "../components/data-table/hooks/use-column-resize.js";
4
4
  import { getVisiblePageNumbers } from "../components/data-table/hooks/use-table-pagination.js";
@@ -6,6 +6,7 @@ import { computeColumnGaps } from "../lib/column-gaps.js";
6
6
  import { computeColumnStats } from "../lib/filter-stats.js";
7
7
  import { createTableOrchestrationStore, defaultValueForFilter } from "./table-orchestration-store.js";
8
8
  const EMPTY_ID_SET = /* @__PURE__ */ new Set();
9
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
9
10
  function useTableOrchestration(config) {
10
11
  const {
11
12
  rows,
@@ -37,14 +38,14 @@ function useTableOrchestration(config) {
37
38
  const requestOpenFilterId = useStore(store, (s) => s.requestOpenFilterId);
38
39
  const currentSort = useStore(store, (s) => s.currentSort);
39
40
  const columnVisibility = useStore(store, (s) => s.columnVisibility);
40
- const page = useStore(store, (s) => s.page);
41
+ const storedPage = useStore(store, (s) => s.page);
41
42
  const rowsPerPage = useStore(store, (s) => s.rowsPerPage);
42
43
  const density = useStore(store, (s) => s.density);
43
44
  const isRefreshing = useStore(store, (s) => s.isRefreshing);
44
- const selectedIds = useStore(store, (s) => s.selectedIds);
45
+ const storedSelectedIds = useStore(store, (s) => s.selectedIds);
45
46
  const activeView = useStore(store, (s) => s.activeView);
46
47
  const columnsMenuOpen = useStore(store, (s) => s.columnsMenuOpen);
47
- const selectedRowId = useStore(store, (s) => s.selectedRowId);
48
+ const storedSelectedRowId = useStore(store, (s) => s.selectedRowId);
48
49
  const hasPriorities = Boolean(columnPriorities && Object.keys(columnPriorities).length > 0);
49
50
  const [isWide, setIsWide] = useState(true);
50
51
  const [isCompact, setIsCompact] = useState(false);
@@ -72,7 +73,7 @@ function useTableOrchestration(config) {
72
73
  }
73
74
  return hidden;
74
75
  }, [isWide, columnPriorities]);
75
- const selectedIdsRef = useRef(selectedIds);
76
+ const selectedIdsRef = useRef(storedSelectedIds);
76
77
  const headerCheckStateRef = useRef(false);
77
78
  const sortState = useMemo(() => {
78
79
  if (!currentSort) return {};
@@ -150,7 +151,7 @@ function useTableOrchestration(config) {
150
151
  if (s) result[`${columnId}:${sub.key}`] = s;
151
152
  }
152
153
  } else {
153
- const s = computeColumnStats(filteredRows, columnId, cfg);
154
+ const s = computeColumnStats(filteredRows, cfg.fieldName ?? columnId, cfg);
154
155
  if (s) result[columnId] = s;
155
156
  }
156
157
  }
@@ -175,7 +176,8 @@ function useTableOrchestration(config) {
175
176
  return result;
176
177
  }, [allColumnFilterStats, filteredColumnFilterStats, filterConfigs]);
177
178
  const totalPages = Math.max(1, Math.ceil(filteredRows.length / rowsPerPage));
178
- const safePage = Math.min(page, totalPages);
179
+ const safePage = Math.min(storedPage, totalPages);
180
+ const page = safePage;
179
181
  const pageNumbers = useMemo(
180
182
  () => getVisiblePageNumbers(safePage, totalPages),
181
183
  [safePage, totalPages]
@@ -184,6 +186,21 @@ function useTableOrchestration(config) {
184
186
  const start = (safePage - 1) * rowsPerPage;
185
187
  return filteredRows.slice(start, start + rowsPerPage);
186
188
  }, [filteredRows, rowsPerPage, safePage]);
189
+ const filteredIdSet = useMemo(
190
+ () => new Set(filteredRows.map(getRowId)),
191
+ [filteredRows, getRowId]
192
+ );
193
+ const selectedRowId = storedSelectedRowId && filteredIdSet.has(storedSelectedRowId) ? storedSelectedRowId : null;
194
+ const selectedIds = useMemo(() => {
195
+ const pruned = new Set([...storedSelectedIds].filter((id) => filteredIdSet.has(id)));
196
+ return pruned.size === storedSelectedIds.size ? storedSelectedIds : pruned;
197
+ }, [storedSelectedIds, filteredIdSet]);
198
+ useIsomorphicLayoutEffect(() => {
199
+ const state = store.getState();
200
+ if (state.page !== safePage) state.setPage(safePage);
201
+ if (state.selectedRowId !== selectedRowId) state.setSelectedRowId(selectedRowId);
202
+ if (state.selectedIds !== selectedIds) state.setSelectedIds(selectedIds);
203
+ }, [store, storedPage, safePage, storedSelectedRowId, selectedRowId, storedSelectedIds, selectedIds]);
187
204
  const pageIds = useMemo(() => new Set(paginatedRows.map(getRowId)), [paginatedRows, getRowId]);
188
205
  const selectedOnPage = useMemo(
189
206
  () => [...selectedIds].filter((id) => pageIds.has(id)).length,
@@ -217,23 +234,6 @@ function useTableOrchestration(config) {
217
234
  const activeCustomFilterChip = activePreset.customFilterChip ?? null;
218
235
  selectedIdsRef.current = selectedIds;
219
236
  headerCheckStateRef.current = headerCheckState;
220
- if (page !== safePage) {
221
- store.getState().setPage(safePage);
222
- }
223
- if (selectedRowId && !filteredRows.some((row) => getRowId(row) === selectedRowId)) {
224
- store.getState().setSelectedRowId(null);
225
- }
226
- const filteredIdSet = useMemo(
227
- () => new Set(filteredRows.map(getRowId)),
228
- [filteredRows, getRowId]
229
- );
230
- const prunedSelectedIds = useMemo(() => {
231
- const pruned = new Set([...selectedIds].filter((id) => filteredIdSet.has(id)));
232
- return pruned.size === selectedIds.size ? selectedIds : pruned;
233
- }, [selectedIds, filteredIdSet]);
234
- if (prunedSelectedIds !== selectedIds) {
235
- store.getState().setSelectedIds(prunedSelectedIds);
236
- }
237
237
  const handleSearchSubmit = useCallback(
238
238
  (e) => {
239
239
  e.preventDefault();
package/dist/es/index.js CHANGED
@@ -61,6 +61,7 @@ import { AppliedStateChips } from "./components/data-table/AppliedStateChips.js"
61
61
  import { FilterFormPanel } from "./components/data-table/FilterFormPanel.js";
62
62
  import { FilterFormToggle, useFilterFormOpen } from "./components/data-table/FilterFormToggle.js";
63
63
  import { applyManagedChanges, clearManagedFilters, hasActiveManagedFilters } from "./components/data-table/managed-filters.js";
64
+ import { getCompoundValue, getDateRange, getNumberRange, getStringArray, isFilterValueEmpty } from "./components/data-table/filter-values.js";
64
65
  import { useTableOrchestration } from "./hooks/useTableOrchestration.js";
65
66
  import { createTableOrchestrationStore, defaultValueForFilter } from "./hooks/table-orchestration-store.js";
66
67
  import { useRowDragOverride } from "./hooks/useRowDragOverride.js";
@@ -183,8 +184,13 @@ export {
183
184
  formatDateParts,
184
185
  formatEnumLabel,
185
186
  formatLiveTimestamp,
187
+ getCompoundValue,
188
+ getDateRange,
189
+ getNumberRange,
190
+ getStringArray,
186
191
  getVisiblePageNumbers,
187
192
  hasActiveManagedFilters,
193
+ isFilterValueEmpty,
188
194
  parsePossibleTimestamp,
189
195
  useAppShell,
190
196
  useColumnResize,
@@ -2703,6 +2703,35 @@ function GroupedColumnToggle({
2703
2703
  }
2704
2704
  );
2705
2705
  }
2706
+ function isObjectValue(value) {
2707
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2708
+ }
2709
+ function getStringArray(value) {
2710
+ return Array.isArray(value) ? value : [];
2711
+ }
2712
+ function getDateRange(value) {
2713
+ const range = {};
2714
+ if (!isObjectValue(value)) return range;
2715
+ if (typeof value.from === "string") range.from = value.from;
2716
+ if (typeof value.to === "string") range.to = value.to;
2717
+ return range;
2718
+ }
2719
+ function getNumberRange(value) {
2720
+ const range = {};
2721
+ if (!isObjectValue(value)) return range;
2722
+ if (typeof value.from === "number") range.from = value.from;
2723
+ if (typeof value.to === "number") range.to = value.to;
2724
+ return range;
2725
+ }
2726
+ function getCompoundValue(value) {
2727
+ return isObjectValue(value) ? value : {};
2728
+ }
2729
+ function isFilterValueEmpty(value) {
2730
+ if (value === void 0 || value === null) return true;
2731
+ if (typeof value === "string") return value.length === 0;
2732
+ if (Array.isArray(value)) return value.length === 0;
2733
+ return Object.values(value).every((x) => typeof x !== "number" && isFilterValueEmpty(x));
2734
+ }
2706
2735
  const DEFAULT_LABELS$2 = {
2707
2736
  searchPlaceholder: "Search...",
2708
2737
  addValuePlaceholder: "Add value...",
@@ -2724,14 +2753,18 @@ function formatChipDate(iso) {
2724
2753
  return d.toLocaleDateString(void 0, { day: "2-digit", month: "short" });
2725
2754
  }
2726
2755
  function isEmptyValue(value, type) {
2727
- if (type === "text") return !Array.isArray(value) || value.length === 0;
2728
- if (type === "enum") return !Array.isArray(value) || value.length === 0;
2729
- if (type === "date")
2730
- return typeof value !== "object" || Array.isArray(value) || !value.from && !value.to;
2731
- if (type === "number-range")
2732
- return typeof value !== "object" || Array.isArray(value) || value.from === void 0 && value.to === void 0;
2756
+ if (type === "text" || type === "enum") return isFilterValueEmpty(getStringArray(value));
2757
+ if (type === "date") return isFilterValueEmpty(getDateRange(value));
2758
+ if (type === "number-range") return isFilterValueEmpty(getNumberRange(value));
2733
2759
  return true;
2734
2760
  }
2761
+ function dateDraft(value) {
2762
+ const { from = "", to = "" } = getDateRange(value);
2763
+ return { from, to };
2764
+ }
2765
+ function numberDraft(bound) {
2766
+ return bound !== void 0 ? String(bound) : "";
2767
+ }
2735
2768
  function ColumnFilterPopover({
2736
2769
  config,
2737
2770
  filter,
@@ -2762,47 +2795,36 @@ function ColumnFilterPopover({
2762
2795
  return () => clearTimeout(id);
2763
2796
  }, [requestOpen]);
2764
2797
  const [draftTags, setDraftTags] = React.useState(
2765
- Array.isArray(filter.value) && config.type === "text" ? filter.value : []
2798
+ config.type === "text" ? getStringArray(filter.value) : []
2766
2799
  );
2767
2800
  const [draftInput, setDraftInput] = React.useState("");
2768
- const [draftEnum, setDraftEnum] = React.useState(
2769
- Array.isArray(filter.value) ? filter.value : []
2770
- );
2771
- const [draftDate, setDraftDate] = React.useState(
2772
- typeof filter.value === "object" && !Array.isArray(filter.value) ? {
2773
- from: String(filter.value.from ?? ""),
2774
- to: String(filter.value.to ?? "")
2775
- } : { from: "", to: "" }
2776
- );
2801
+ const [draftEnum, setDraftEnum] = React.useState(getStringArray(filter.value));
2802
+ const [draftDate, setDraftDate] = React.useState(dateDraft(filter.value));
2777
2803
  const [draftNumberFrom, setDraftNumberFrom] = React.useState(
2778
- typeof filter.value === "object" && !Array.isArray(filter.value) && typeof filter.value.from === "number" ? String(filter.value.from) : ""
2779
- );
2780
- const [draftNumberTo, setDraftNumberTo] = React.useState(
2781
- typeof filter.value === "object" && !Array.isArray(filter.value) && typeof filter.value.to === "number" ? String(filter.value.to) : ""
2804
+ numberDraft(getNumberRange(filter.value).from)
2782
2805
  );
2806
+ const [draftNumberTo, setDraftNumberTo] = React.useState(numberDraft(getNumberRange(filter.value).to));
2807
+ const resetDrafts = (value) => {
2808
+ setDraftTags(config.type === "text" ? getStringArray(value) : []);
2809
+ setDraftInput("");
2810
+ setDraftEnum(getStringArray(value));
2811
+ setDraftDate(dateDraft(value));
2812
+ const numVal = getNumberRange(value);
2813
+ setDraftNumberFrom(numberDraft(numVal.from));
2814
+ setDraftNumberTo(numberDraft(numVal.to));
2815
+ };
2816
+ const [draftSource, setDraftSource] = React.useState(filter.value);
2817
+ if (!isOpen && draftSource !== filter.value) {
2818
+ setDraftSource(filter.value);
2819
+ resetDrafts(filter.value);
2820
+ }
2783
2821
  const handleOpenChange = (nextOpen) => {
2784
2822
  if (!nextOpen) {
2785
2823
  if (isEmptyValue(filter.value, config.type)) {
2786
2824
  onRemove();
2787
2825
  return;
2788
2826
  }
2789
- setDraftTags(Array.isArray(filter.value) && config.type === "text" ? filter.value : []);
2790
- setDraftInput("");
2791
- setDraftEnum(Array.isArray(filter.value) ? filter.value : []);
2792
- setDraftDate(
2793
- typeof filter.value === "object" && !Array.isArray(filter.value) ? {
2794
- from: String(filter.value.from ?? ""),
2795
- to: String(filter.value.to ?? "")
2796
- } : { from: "", to: "" }
2797
- );
2798
- if (typeof filter.value === "object" && !Array.isArray(filter.value)) {
2799
- const numVal = filter.value;
2800
- setDraftNumberFrom(typeof numVal.from === "number" ? String(numVal.from) : "");
2801
- setDraftNumberTo(typeof numVal.to === "number" ? String(numVal.to) : "");
2802
- } else {
2803
- setDraftNumberFrom("");
2804
- setDraftNumberTo("");
2805
- }
2827
+ resetDrafts(filter.value);
2806
2828
  }
2807
2829
  setIsOpen(nextOpen);
2808
2830
  };
@@ -3202,14 +3224,14 @@ function ColumnFilterPopover({
3202
3224
  }
3203
3225
  return DEFAULT_LABELS$2.selectedCount(value.length);
3204
3226
  }
3205
- if (config.type === "date" && typeof value === "object" && !Array.isArray(value)) {
3206
- const { from, to } = value;
3227
+ if (config.type === "date") {
3228
+ const { from, to } = getDateRange(value);
3207
3229
  if (from && to) return `${from} — ${to}`;
3208
3230
  if (from) return `${DEFAULT_LABELS$2.chipFrom} ${from}`;
3209
3231
  if (to) return `${DEFAULT_LABELS$2.chipTo} ${to}`;
3210
3232
  }
3211
- if (config.type === "number-range" && typeof value === "object" && !Array.isArray(value)) {
3212
- const { from, to } = value;
3233
+ if (config.type === "number-range") {
3234
+ const { from, to } = getNumberRange(value);
3213
3235
  if (from !== void 0 && to !== void 0) return `${from}–${to}`;
3214
3236
  if (from !== void 0) return `≥${from}`;
3215
3237
  if (to !== void 0) return `≤${to}`;
@@ -3250,25 +3272,10 @@ const DEFAULT_LABELS$1 = {
3250
3272
  statsAllPrefix: "All:",
3251
3273
  statsFilteredPrefix: "Filt:"
3252
3274
  };
3253
- function isCompoundEmpty(value) {
3254
- if (typeof value !== "object" || Array.isArray(value) || value === null) return true;
3255
- const obj = value;
3256
- return Object.keys(obj).every((k) => isSubValueEmpty(obj[k]));
3257
- }
3258
- function isSubValueEmpty(value) {
3259
- if (value === void 0 || value === null) return true;
3260
- if (Array.isArray(value)) return value.length === 0;
3261
- if (typeof value === "string") return value.length === 0;
3262
- if (typeof value === "object") {
3263
- const obj = value;
3264
- return obj.from === void 0 && obj.to === void 0;
3265
- }
3266
- return true;
3267
- }
3268
3275
  function initDraft(sub, compound) {
3269
3276
  const val = compound[sub.key];
3270
3277
  if (sub.type === "number-range") {
3271
- const range = typeof val === "object" && !Array.isArray(val) ? val : {};
3278
+ const range = getNumberRange(val);
3272
3279
  return {
3273
3280
  type: "number-range",
3274
3281
  from: range.from !== void 0 ? String(range.from) : "",
@@ -3276,11 +3283,11 @@ function initDraft(sub, compound) {
3276
3283
  };
3277
3284
  }
3278
3285
  if (sub.type === "enum") {
3279
- return { type: "enum", selected: Array.isArray(val) ? val : [] };
3286
+ return { type: "enum", selected: getStringArray(val) };
3280
3287
  }
3281
3288
  return {
3282
3289
  type: "text",
3283
- tags: Array.isArray(val) ? val : [],
3290
+ tags: getStringArray(val),
3284
3291
  input: ""
3285
3292
  };
3286
3293
  }
@@ -3299,9 +3306,9 @@ function draftToValue(draft) {
3299
3306
  return all.length > 0 ? all : void 0;
3300
3307
  }
3301
3308
  function formatSubDisplay(sub, val) {
3302
- if (isSubValueEmpty(val)) return null;
3303
- if (sub.type === "number-range" && typeof val === "object" && !Array.isArray(val)) {
3304
- const { from, to } = val;
3309
+ if (isFilterValueEmpty(val)) return null;
3310
+ if (sub.type === "number-range") {
3311
+ const { from, to } = getNumberRange(val);
3305
3312
  const label = sub.label.replace(/\s+/g, " ").split(" ").pop() ?? sub.label;
3306
3313
  if (from !== void 0 && to !== void 0) return `${label}: ${from}–${to}`;
3307
3314
  if (from !== void 0) return `${label}: ≥${from}`;
@@ -3335,17 +3342,23 @@ function CompoundFilterPopover({
3335
3342
  onRequestOpenHandled
3336
3343
  }) {
3337
3344
  const subFilters = config.subFilters ?? [];
3338
- const compound = typeof filter.value === "object" && !Array.isArray(filter.value) ? filter.value : {};
3345
+ const compound = getCompoundValue(filter.value);
3339
3346
  const [isOpen, setIsOpen] = React.useState(false);
3340
3347
  const onRequestOpenHandledRef = React.useRef(onRequestOpenHandled);
3341
3348
  React.useEffect(() => {
3342
3349
  onRequestOpenHandledRef.current = onRequestOpenHandled;
3343
3350
  });
3344
- const [drafts, setDrafts] = React.useState(() => {
3351
+ const initDrafts = () => {
3345
3352
  const d = {};
3346
3353
  for (const sub of subFilters) d[sub.key] = initDraft(sub, compound);
3347
3354
  return d;
3348
- });
3355
+ };
3356
+ const [drafts, setDrafts] = React.useState(initDrafts);
3357
+ const [draftSource, setDraftSource] = React.useState(filter.value);
3358
+ if (!isOpen && draftSource !== filter.value) {
3359
+ setDraftSource(filter.value);
3360
+ setDrafts(initDrafts());
3361
+ }
3349
3362
  React.useEffect(() => {
3350
3363
  if (!initialOpen) return;
3351
3364
  const id = setTimeout(() => setIsOpen(true), 0);
@@ -3361,13 +3374,11 @@ function CompoundFilterPopover({
3361
3374
  }, [requestOpen]);
3362
3375
  const handleOpenChange = (nextOpen) => {
3363
3376
  if (!nextOpen) {
3364
- if (isCompoundEmpty(filter.value)) {
3377
+ if (isFilterValueEmpty(compound)) {
3365
3378
  onRemove();
3366
3379
  return;
3367
3380
  }
3368
- const d = {};
3369
- for (const sub of subFilters) d[sub.key] = initDraft(sub, compound);
3370
- setDrafts(d);
3381
+ setDrafts(initDrafts());
3371
3382
  }
3372
3383
  setIsOpen(nextOpen);
3373
3384
  };
@@ -3402,7 +3413,7 @@ function CompoundFilterPopover({
3402
3413
  });
3403
3414
  };
3404
3415
  const getDisplayValue = () => {
3405
- if (isCompoundEmpty(filter.value)) return void 0;
3416
+ if (isFilterValueEmpty(compound)) return void 0;
3406
3417
  const parts = [];
3407
3418
  for (const sub of subFilters) {
3408
3419
  const display = formatSubDisplay(sub, compound[sub.key]);
@@ -3744,16 +3755,6 @@ const DEFAULT_LABELS = {
3744
3755
  columns: "Columns",
3745
3756
  hiddenCount: (n) => `${n} hidden`
3746
3757
  };
3747
- function isFilterValueEmpty$1(value) {
3748
- if (Array.isArray(value)) return value.length === 0;
3749
- if (typeof value === "string") return value.length === 0;
3750
- if (typeof value === "object" && value !== null) {
3751
- const keys = Object.keys(value);
3752
- if (keys.length === 0) return true;
3753
- return keys.every((k) => isFilterValueEmpty$1(value[k]));
3754
- }
3755
- return false;
3756
- }
3757
3758
  function AppliedStateChips({
3758
3759
  filters,
3759
3760
  configs,
@@ -3799,7 +3800,7 @@ function AppliedStateChips({
3799
3800
  filters.map((filter) => {
3800
3801
  const filterConfig = configs[filter.columnId];
3801
3802
  if (!filterConfig) return null;
3802
- const isEmpty = isFilterValueEmpty$1(filter.value);
3803
+ const isEmpty = isFilterValueEmpty(filter.value);
3803
3804
  if (filterConfig.type === "compound") {
3804
3805
  return /* @__PURE__ */ jsxRuntime.jsx(
3805
3806
  CompoundFilterPopover,
@@ -3859,28 +3860,10 @@ function clearManagedFilters(orch, managedColumnIds) {
3859
3860
  if (activeIds.has(id)) orch.handleRemoveColumnFilter(id);
3860
3861
  }
3861
3862
  }
3862
- function isFilterValueEmpty(v) {
3863
- if (v === void 0) return true;
3864
- if (Array.isArray(v)) return v.length === 0 || v.every((x) => x === "" || x == null);
3865
- if (typeof v === "string") return v.length === 0;
3866
- if (typeof v === "object" && v !== null) {
3867
- return Object.values(v).every((x) => isFilterValueEmpty(x));
3868
- }
3869
- return false;
3870
- }
3871
3863
  function hasActiveManagedFilters(filters, managedFieldIds) {
3872
3864
  const managed = new Set(managedFieldIds);
3873
3865
  return filters.some((f) => managed.has(f.columnId) && !isFilterValueEmpty(f.value));
3874
3866
  }
3875
- function isValueEmpty(v) {
3876
- if (v === void 0) return true;
3877
- if (Array.isArray(v)) return v.length === 0 || v.every((x) => x === "" || x == null);
3878
- if (typeof v === "string") return v.length === 0;
3879
- if (typeof v === "object" && v !== null) {
3880
- return Object.values(v).every((x) => isValueEmpty(x));
3881
- }
3882
- return false;
3883
- }
3884
3867
  function deriveInitialState(fields, filters) {
3885
3868
  const byId = new Map(filters.map((f) => [f.columnId, f.value]));
3886
3869
  const next = {};
@@ -3936,7 +3919,7 @@ function FilterFormPanel({
3936
3919
  const changes = [];
3937
3920
  for (const id2 of renderableFields) {
3938
3921
  const value = state[id2];
3939
- if (isValueEmpty(value)) {
3922
+ if (value === void 0 || isFilterValueEmpty(value)) {
3940
3923
  if (activeIds.has(id2)) changes.push({ columnId: id2, action: "remove" });
3941
3924
  } else {
3942
3925
  changes.push({ columnId: id2, action: "set", value });
@@ -3950,7 +3933,7 @@ function FilterFormPanel({
3950
3933
  onClear(renderableFields);
3951
3934
  touchedRef.current.clear();
3952
3935
  }, [onClear, renderableFields]);
3953
- const isClearDisabled = !hasActiveManagedFilters(filters, renderableFields) && renderableFields.every((fieldId) => isValueEmpty(state[fieldId]));
3936
+ const isClearDisabled = !hasActiveManagedFilters(filters, renderableFields) && renderableFields.every((fieldId) => isFilterValueEmpty(state[fieldId]));
3954
3937
  return /* @__PURE__ */ jsxRuntime.jsxs(
3955
3938
  "form",
3956
3939
  {
@@ -4051,12 +4034,11 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
4051
4034
  ] });
4052
4035
  }
4053
4036
  if (config.type === "date") {
4054
- const range = value && !Array.isArray(value) && typeof value === "object" ? value : {};
4037
+ const range = getDateRange(value);
4055
4038
  const update = (key) => (e) => {
4056
4039
  const v = e.target.value;
4057
4040
  const next = { ...range, [key]: v || void 0 };
4058
- const cleaned = next.from || next.to ? next : void 0;
4059
- onChange(cleaned);
4041
+ onChange(next.from || next.to ? next : void 0);
4060
4042
  };
4061
4043
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col gap-1.5 [grid-column:span_2]", children: [
4062
4044
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] font-medium text-[var(--text-secondary)]", children: config.label }),
@@ -4085,13 +4067,12 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
4085
4067
  ] });
4086
4068
  }
4087
4069
  if (config.type === "number-range") {
4088
- const range = value && !Array.isArray(value) && typeof value === "object" ? value : {};
4070
+ const range = getNumberRange(value);
4089
4071
  const update = (key) => (e) => {
4090
4072
  const raw = e.target.value;
4091
4073
  const parsed = raw === "" ? void 0 : Number(raw);
4092
4074
  const next = { ...range, [key]: Number.isFinite(parsed) ? parsed : void 0 };
4093
- const cleaned = next.from !== void 0 || next.to !== void 0 ? next : void 0;
4094
- onChange(cleaned);
4075
+ onChange(next.from !== void 0 || next.to !== void 0 ? next : void 0);
4095
4076
  };
4096
4077
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col gap-1.5 [grid-column:span_2]", children: [
4097
4078
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] font-medium text-[var(--text-secondary)]", children: config.label }),
@@ -4122,11 +4103,11 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
4122
4103
  ] });
4123
4104
  }
4124
4105
  if (config.type === "compound") {
4125
- const compound = value && !Array.isArray(value) && typeof value === "object" ? value : {};
4126
- const updateSub = (subField, next) => {
4106
+ const compound = getCompoundValue(value);
4107
+ const updateSub = (subKey, next) => {
4127
4108
  const rest = { ...compound };
4128
- delete rest[subField];
4129
- const merged = next === void 0 || isValueEmpty(next) ? rest : { ...rest, [subField]: next };
4109
+ delete rest[subKey];
4110
+ const merged = next === void 0 || isFilterValueEmpty(next) ? rest : { ...rest, [subKey]: next };
4130
4111
  onChange(Object.keys(merged).length === 0 ? void 0 : merged);
4131
4112
  };
4132
4113
  const visibleSubs = (config.subFilters ?? []).filter(
@@ -4142,7 +4123,7 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
4142
4123
  children: [
4143
4124
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] font-medium text-[var(--text-secondary)]", children: config.label }),
4144
4125
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid min-w-0 gap-2 grid-cols-[repeat(auto-fit,minmax(180px,1fr))]", children: visibleSubs.map((sub) => {
4145
- const subValue = compound[sub.field];
4126
+ const subValue = compound[sub.key];
4146
4127
  if (sub.type === "text") {
4147
4128
  const text = Array.isArray(subValue) ? subValue[0] ?? "" : "";
4148
4129
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -4153,7 +4134,7 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
4153
4134
  value: text,
4154
4135
  onChange: (e) => {
4155
4136
  const v = e.target.value;
4156
- updateSub(sub.field, v === "" ? void 0 : [v]);
4137
+ updateSub(sub.key, v === "" ? void 0 : [v]);
4157
4138
  },
4158
4139
  className: "h-9 min-w-0"
4159
4140
  },
@@ -4166,7 +4147,7 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
4166
4147
  Combobox,
4167
4148
  {
4168
4149
  value: selected || null,
4169
- onChange: (v) => updateSub(sub.field, v ? [v] : void 0),
4150
+ onChange: (v) => updateSub(sub.key, v ? [v] : void 0),
4170
4151
  options: sub.options ?? [],
4171
4152
  placeholder: sub.label,
4172
4153
  triggerAriaLabel: sub.label,
@@ -4176,13 +4157,18 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
4176
4157
  );
4177
4158
  }
4178
4159
  if (sub.type === "number-range") {
4179
- const range = subValue && !Array.isArray(subValue) && typeof subValue === "object" ? subValue : {};
4160
+ const range = getNumberRange(subValue);
4180
4161
  const updateRange = (key) => (e) => {
4181
4162
  const raw = e.target.value;
4182
4163
  const parsed = raw === "" ? void 0 : Number(raw);
4183
- const next = { ...range, [key]: Number.isFinite(parsed) ? parsed : void 0 };
4184
- const cleaned = next.from !== void 0 || next.to !== void 0 ? next : void 0;
4185
- updateSub(sub.field, cleaned);
4164
+ const next = {
4165
+ ...range,
4166
+ [key]: Number.isFinite(parsed) ? parsed : void 0
4167
+ };
4168
+ updateSub(
4169
+ sub.key,
4170
+ next.from !== void 0 || next.to !== void 0 ? next : void 0
4171
+ );
4186
4172
  };
4187
4173
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 gap-2", children: [
4188
4174
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4530,6 +4516,7 @@ function createTableOrchestrationStore({
4530
4516
  });
4531
4517
  }
4532
4518
  const EMPTY_ID_SET = /* @__PURE__ */ new Set();
4519
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
4533
4520
  function useTableOrchestration(config) {
4534
4521
  const {
4535
4522
  rows,
@@ -4561,14 +4548,14 @@ function useTableOrchestration(config) {
4561
4548
  const requestOpenFilterId = zustand.useStore(store, (s) => s.requestOpenFilterId);
4562
4549
  const currentSort = zustand.useStore(store, (s) => s.currentSort);
4563
4550
  const columnVisibility = zustand.useStore(store, (s) => s.columnVisibility);
4564
- const page = zustand.useStore(store, (s) => s.page);
4551
+ const storedPage = zustand.useStore(store, (s) => s.page);
4565
4552
  const rowsPerPage = zustand.useStore(store, (s) => s.rowsPerPage);
4566
4553
  const density = zustand.useStore(store, (s) => s.density);
4567
4554
  const isRefreshing = zustand.useStore(store, (s) => s.isRefreshing);
4568
- const selectedIds = zustand.useStore(store, (s) => s.selectedIds);
4555
+ const storedSelectedIds = zustand.useStore(store, (s) => s.selectedIds);
4569
4556
  const activeView = zustand.useStore(store, (s) => s.activeView);
4570
4557
  const columnsMenuOpen = zustand.useStore(store, (s) => s.columnsMenuOpen);
4571
- const selectedRowId = zustand.useStore(store, (s) => s.selectedRowId);
4558
+ const storedSelectedRowId = zustand.useStore(store, (s) => s.selectedRowId);
4572
4559
  const hasPriorities = Boolean(columnPriorities && Object.keys(columnPriorities).length > 0);
4573
4560
  const [isWide, setIsWide] = React.useState(true);
4574
4561
  const [isCompact, setIsCompact] = React.useState(false);
@@ -4596,7 +4583,7 @@ function useTableOrchestration(config) {
4596
4583
  }
4597
4584
  return hidden;
4598
4585
  }, [isWide, columnPriorities]);
4599
- const selectedIdsRef = React.useRef(selectedIds);
4586
+ const selectedIdsRef = React.useRef(storedSelectedIds);
4600
4587
  const headerCheckStateRef = React.useRef(false);
4601
4588
  const sortState = React.useMemo(() => {
4602
4589
  if (!currentSort) return {};
@@ -4674,7 +4661,7 @@ function useTableOrchestration(config) {
4674
4661
  if (s) result[`${columnId}:${sub.key}`] = s;
4675
4662
  }
4676
4663
  } else {
4677
- const s = computeColumnStats(filteredRows, columnId, cfg);
4664
+ const s = computeColumnStats(filteredRows, cfg.fieldName ?? columnId, cfg);
4678
4665
  if (s) result[columnId] = s;
4679
4666
  }
4680
4667
  }
@@ -4699,7 +4686,8 @@ function useTableOrchestration(config) {
4699
4686
  return result;
4700
4687
  }, [allColumnFilterStats, filteredColumnFilterStats, filterConfigs]);
4701
4688
  const totalPages = Math.max(1, Math.ceil(filteredRows.length / rowsPerPage));
4702
- const safePage = Math.min(page, totalPages);
4689
+ const safePage = Math.min(storedPage, totalPages);
4690
+ const page = safePage;
4703
4691
  const pageNumbers = React.useMemo(
4704
4692
  () => getVisiblePageNumbers(safePage, totalPages),
4705
4693
  [safePage, totalPages]
@@ -4708,6 +4696,21 @@ function useTableOrchestration(config) {
4708
4696
  const start = (safePage - 1) * rowsPerPage;
4709
4697
  return filteredRows.slice(start, start + rowsPerPage);
4710
4698
  }, [filteredRows, rowsPerPage, safePage]);
4699
+ const filteredIdSet = React.useMemo(
4700
+ () => new Set(filteredRows.map(getRowId)),
4701
+ [filteredRows, getRowId]
4702
+ );
4703
+ const selectedRowId = storedSelectedRowId && filteredIdSet.has(storedSelectedRowId) ? storedSelectedRowId : null;
4704
+ const selectedIds = React.useMemo(() => {
4705
+ const pruned = new Set([...storedSelectedIds].filter((id) => filteredIdSet.has(id)));
4706
+ return pruned.size === storedSelectedIds.size ? storedSelectedIds : pruned;
4707
+ }, [storedSelectedIds, filteredIdSet]);
4708
+ useIsomorphicLayoutEffect(() => {
4709
+ const state = store.getState();
4710
+ if (state.page !== safePage) state.setPage(safePage);
4711
+ if (state.selectedRowId !== selectedRowId) state.setSelectedRowId(selectedRowId);
4712
+ if (state.selectedIds !== selectedIds) state.setSelectedIds(selectedIds);
4713
+ }, [store, storedPage, safePage, storedSelectedRowId, selectedRowId, storedSelectedIds, selectedIds]);
4711
4714
  const pageIds = React.useMemo(() => new Set(paginatedRows.map(getRowId)), [paginatedRows, getRowId]);
4712
4715
  const selectedOnPage = React.useMemo(
4713
4716
  () => [...selectedIds].filter((id) => pageIds.has(id)).length,
@@ -4741,23 +4744,6 @@ function useTableOrchestration(config) {
4741
4744
  const activeCustomFilterChip = activePreset.customFilterChip ?? null;
4742
4745
  selectedIdsRef.current = selectedIds;
4743
4746
  headerCheckStateRef.current = headerCheckState;
4744
- if (page !== safePage) {
4745
- store.getState().setPage(safePage);
4746
- }
4747
- if (selectedRowId && !filteredRows.some((row) => getRowId(row) === selectedRowId)) {
4748
- store.getState().setSelectedRowId(null);
4749
- }
4750
- const filteredIdSet = React.useMemo(
4751
- () => new Set(filteredRows.map(getRowId)),
4752
- [filteredRows, getRowId]
4753
- );
4754
- const prunedSelectedIds = React.useMemo(() => {
4755
- const pruned = new Set([...selectedIds].filter((id) => filteredIdSet.has(id)));
4756
- return pruned.size === selectedIds.size ? selectedIds : pruned;
4757
- }, [selectedIds, filteredIdSet]);
4758
- if (prunedSelectedIds !== selectedIds) {
4759
- store.getState().setSelectedIds(prunedSelectedIds);
4760
- }
4761
4747
  const handleSearchSubmit = React.useCallback(
4762
4748
  (e) => {
4763
4749
  e.preventDefault();
@@ -5458,8 +5444,13 @@ exports.displayTimeZone = displayTimeZone;
5458
5444
  exports.formatDateParts = formatDateParts;
5459
5445
  exports.formatEnumLabel = formatEnumLabel;
5460
5446
  exports.formatLiveTimestamp = formatLiveTimestamp;
5447
+ exports.getCompoundValue = getCompoundValue;
5448
+ exports.getDateRange = getDateRange;
5449
+ exports.getNumberRange = getNumberRange;
5450
+ exports.getStringArray = getStringArray;
5461
5451
  exports.getVisiblePageNumbers = getVisiblePageNumbers;
5462
5452
  exports.hasActiveManagedFilters = hasActiveManagedFilters;
5453
+ exports.isFilterValueEmpty = isFilterValueEmpty;
5463
5454
  exports.parsePossibleTimestamp = parsePossibleTimestamp;
5464
5455
  exports.useAppShell = useAppShell;
5465
5456
  exports.useColumnResize = useColumnResize;
@@ -1,9 +1,11 @@
1
1
  export type ColumnFilterType = "text" | "enum" | "date" | "number-range" | "compound";
2
2
  export interface CompoundSubFilter {
3
+ /** Sub-filter id: the key of its value inside the compound filter value. */
3
4
  key: string;
4
5
  type: "text" | "number-range" | "enum";
5
6
  label: string;
6
7
  badge?: string;
8
+ /** Row field the sub-filter reads (used for column stats). */
7
9
  field: string;
8
10
  options?: {
9
11
  label: string;
@@ -20,15 +22,19 @@ export interface ColumnFilterConfig {
20
22
  }[];
21
23
  subFilters?: CompoundSubFilter[];
22
24
  }
23
- export type ActiveFilterValue = string | string[] | {
25
+ export type DateRangeValue = {
24
26
  from?: string;
25
27
  to?: string;
26
- } | {
28
+ };
29
+ export type NumberRangeValue = {
27
30
  from?: number;
28
31
  to?: number;
29
- } | {
32
+ };
33
+ /** Compound filter value: sub-filter `key` → that sub-filter's value. */
34
+ export type CompoundFilterValue = {
30
35
  [key: string]: ActiveFilterValue;
31
36
  };
37
+ export type ActiveFilterValue = string | string[] | DateRangeValue | NumberRangeValue | CompoundFilterValue;
32
38
  export interface ActiveFilter {
33
39
  columnId: string;
34
40
  value: ActiveFilterValue;
@@ -0,0 +1,16 @@
1
+ import type { ActiveFilterValue, CompoundFilterValue, DateRangeValue, NumberRangeValue } from "./filter-types.js";
2
+ /** Read a text/enum filter value. Non-array values read as `[]`. */
3
+ export declare function getStringArray(value: ActiveFilterValue | undefined): string[];
4
+ /** Read a date filter value. Non-string bounds are dropped. */
5
+ export declare function getDateRange(value: ActiveFilterValue | undefined): DateRangeValue;
6
+ /** Read a number-range filter value. Non-number bounds are dropped. */
7
+ export declare function getNumberRange(value: ActiveFilterValue | undefined): NumberRangeValue;
8
+ /** Read a compound filter value (sub-filter key → value). Non-objects read as `{}`. */
9
+ export declare function getCompoundValue(value: ActiveFilterValue | undefined): CompoundFilterValue;
10
+ /**
11
+ * True when a filter value constrains nothing: `undefined`, `""`, `[]`, or an
12
+ * object whose every entry is itself empty (so `{}`, `{ from: undefined }` and
13
+ * `{ sub: [] }` are empty; `{ from: 0 }` is not). `[""]` is not empty — `""`
14
+ * can be a real enum option value.
15
+ */
16
+ export declare function isFilterValueEmpty(value: ActiveFilterValue | undefined): boolean;
@@ -70,7 +70,7 @@ export { useScrollActivity } from "./hooks/use-scroll-activity.js";
70
70
  export { cn } from "./lib/utils.js";
71
71
  export { STYLE_SCOPE } from "./lib/style-scope.js";
72
72
  export { PortalContainerContext, usePortalContainer, } from "./context/portal-container-context.js";
73
- export type { ColumnFilterType, CompoundSubFilter, ColumnFilterConfig, ActiveFilterValue, ActiveFilter, FilterColumnStats, FilterStatsSlice, ColumnGroup, ColumnGap, ViewPreset, } from "./components/data-table/filter-types.js";
73
+ export type { ColumnFilterType, CompoundSubFilter, ColumnFilterConfig, ActiveFilterValue, DateRangeValue, NumberRangeValue, CompoundFilterValue, ActiveFilter, FilterColumnStats, FilterStatsSlice, ColumnGroup, ColumnGap, ViewPreset, } from "./components/data-table/filter-types.js";
74
74
  export { GapIndicator, GapCell, GAP_HEADER_ATTR, GAP_CELL_ATTR, } from "./components/data-table/GapIndicator.js";
75
75
  export { TimestampCell } from "./components/data-table/TimestampCell.js";
76
76
  export { ExpiryTimestampCell } from "./components/data-table/ExpiryTimestampCell.js";
@@ -89,6 +89,7 @@ export { FilterFormToggle, useFilterFormOpen } from "./components/data-table/Fil
89
89
  export type { FilterFormToggleProps } from "./components/data-table/FilterFormToggle.js";
90
90
  export { applyManagedChanges, clearManagedFilters, hasActiveManagedFilters, } from "./components/data-table/managed-filters.js";
91
91
  export type { ManagedFilterChange } from "./components/data-table/managed-filters.js";
92
+ export { getCompoundValue, getDateRange, getNumberRange, getStringArray, isFilterValueEmpty, } from "./components/data-table/filter-values.js";
92
93
  export { useTableOrchestration } from "./hooks/useTableOrchestration.js";
93
94
  export type { TableOrchestrationConfig, TableOrchestrationReturn, } from "./hooks/useTableOrchestration.js";
94
95
  export { createTableOrchestrationStore, defaultValueForFilter, } from "./hooks/table-orchestration-store.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@undevy-org/tablewright",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "React data-table and application-shell components: filters, drawers, row controls, sortable and resizable columns, built on Radix UI and TanStack Table.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,7 +16,6 @@
16
16
  "provenance": true
17
17
  },
18
18
  "type": "module",
19
- "packageManager": "pnpm@10.28.2",
20
19
  "sideEffects": [
21
20
  "**/*.css"
22
21
  ],
@@ -40,19 +39,6 @@
40
39
  "files": [
41
40
  "dist"
42
41
  ],
43
- "scripts": {
44
- "build": "node scripts/build.mjs",
45
- "build:js": "vite build --config vite.lib.config.ts",
46
- "build:css": "node scripts/build-css.mjs",
47
- "build:types": "tsc -p tsconfig.build.json",
48
- "check:render": "node scripts/render-check/run.mjs",
49
- "lint": "eslint src .storybook --max-warnings=0",
50
- "typecheck": "tsc --project tsconfig.json --noEmit",
51
- "storybook": "storybook dev -p 6006",
52
- "build-storybook": "storybook build",
53
- "changeset": "changeset",
54
- "release": "changeset publish"
55
- },
56
42
  "peerDependencies": {
57
43
  "@tanstack/react-table": "^8.21.3",
58
44
  "@types/react": "*",
@@ -110,5 +96,18 @@
110
96
  "typescript": "~5.9.3",
111
97
  "typescript-eslint": "^8.48.0",
112
98
  "vite": "^7.3.1"
99
+ },
100
+ "scripts": {
101
+ "build": "node scripts/build.mjs",
102
+ "build:js": "vite build --config vite.lib.config.ts",
103
+ "build:css": "node scripts/build-css.mjs",
104
+ "build:types": "tsc -p tsconfig.build.json",
105
+ "check:render": "node scripts/render-check/run.mjs",
106
+ "lint": "eslint src .storybook --max-warnings=0",
107
+ "typecheck": "tsc --project tsconfig.json --noEmit",
108
+ "storybook": "storybook dev -p 6006",
109
+ "build-storybook": "storybook build",
110
+ "changeset": "changeset",
111
+ "release": "changeset publish"
113
112
  }
114
- }
113
+ }