@undevy-org/tablewright 0.7.2 → 0.7.4

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.
@@ -6,7 +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
+ import { recordFilterFieldTouch, patchFieldState, resolveFieldApplyChange, formatTextMultiSummary, singleStringDraftValue, formatEnumMultiSummary, enumComboboxSelection, mergeCompoundSubDraft } from "./filter-form-panel-apply.js";
10
+ import { isFilterValueEmpty, getDateRange, getNumberRange, getStringArray, getCompoundValue } from "./filter-values.js";
10
11
  import { hasActiveManagedFilters } from "./managed-filters.js";
11
12
  const DEFAULT_LABELS = {
12
13
  closeFiltersAriaLabel: "Close filters",
@@ -21,7 +22,11 @@ const DEFAULT_LABELS = {
21
22
  compoundSubRangeFromAriaLabel: (subLabel) => `${subLabel} — from`,
22
23
  compoundSubRangeToAriaLabel: (subLabel) => `${subLabel} — to`,
23
24
  compoundSubRangeFromPlaceholder: (subLabel) => `${subLabel} from`,
24
- compoundSubRangeToPlaceholder: (subLabel) => `${subLabel} to`
25
+ compoundSubRangeToPlaceholder: (subLabel) => `${subLabel} to`,
26
+ multiValueFormHint: "Multiple values — edit using the column filter chips or popover above the table.",
27
+ valuesCount: (count) => `${count} values`,
28
+ selectedCount: (count) => `${count} selected`,
29
+ clearFieldForSingleEdit: "Clear to edit one value"
25
30
  };
26
31
  function deriveInitialState(fields, filters) {
27
32
  const byId = new Map(filters.map((f) => [f.columnId, f.value]));
@@ -71,24 +76,38 @@ function FilterFormPanel({
71
76
  return changed ? next : prev;
72
77
  });
73
78
  }, [filters, renderableFields]);
74
- const setField = useCallback((id2, value) => {
75
- touchedRef.current.add(id2);
76
- setState((prev) => ({ ...prev, [id2]: value }));
77
- }, []);
79
+ const setField = useCallback(
80
+ (id2, value, compoundSubKey) => {
81
+ recordFilterFieldTouch(touchedRef.current, id2, compoundSubKey);
82
+ setState((prev) => patchFieldState(prev, id2, value));
83
+ },
84
+ []
85
+ );
78
86
  const handleApply = useCallback(() => {
79
87
  const activeIds = new Set(filters.map((f) => f.columnId));
88
+ const committedById = new Map(filters.map((f) => [f.columnId, f.value]));
89
+ const touched = touchedRef.current;
80
90
  const changes = [];
81
91
  for (const id2 of renderableFields) {
82
- const value = state[id2];
83
- if (value === void 0 || isFilterValueEmpty(value)) {
84
- if (activeIds.has(id2)) changes.push({ columnId: id2, action: "remove" });
85
- } else {
86
- changes.push({ columnId: id2, action: "set", value });
87
- }
92
+ const config = filterConfigs[id2];
93
+ const committed = committedById.get(id2);
94
+ const draft = state[id2];
95
+ const resolution = resolveFieldApplyChange(
96
+ config,
97
+ id2,
98
+ committed,
99
+ draft,
100
+ touched,
101
+ hiddenSubSet,
102
+ activeIds.has(id2)
103
+ );
104
+ if (resolution.kind === "skip") continue;
105
+ if (resolution.kind === "remove") changes.push({ columnId: id2, action: "remove" });
106
+ else changes.push({ columnId: id2, action: "set", value: resolution.value });
88
107
  }
89
108
  onApply(changes);
90
109
  touchedRef.current.clear();
91
- }, [filters, onApply, renderableFields, state]);
110
+ }, [filterConfigs, filters, hiddenSubSet, onApply, renderableFields, state]);
92
111
  const handleClear = useCallback(() => {
93
112
  setState(deriveInitialState(renderableFields, []));
94
113
  onClear(renderableFields);
@@ -133,7 +152,7 @@ function FilterFormPanel({
133
152
  {
134
153
  config,
135
154
  value: state[fieldId],
136
- onChange: (v) => setField(fieldId, v),
155
+ onChange: (v, compoundSubKey) => setField(fieldId, v, compoundSubKey),
137
156
  fieldId,
138
157
  hiddenSubSet,
139
158
  labels
@@ -161,7 +180,20 @@ function FilterFormPanel({
161
180
  }
162
181
  function Field({ config, value, onChange, fieldId, hiddenSubSet, labels }) {
163
182
  if (config.type === "text") {
164
- const text = Array.isArray(value) ? value[0] ?? "" : "";
183
+ const strings = getStringArray(value);
184
+ if (strings.length > 1) {
185
+ return /* @__PURE__ */ jsx(
186
+ MultiValueStringSummary,
187
+ {
188
+ label: config.label,
189
+ summary: formatTextMultiSummary(strings, labels),
190
+ hint: labels.multiValueFormHint,
191
+ clearLabel: labels.clearFieldForSingleEdit,
192
+ onClearForEdit: () => onChange(void 0)
193
+ }
194
+ );
195
+ }
196
+ const text = singleStringDraftValue(strings);
165
197
  return /* @__PURE__ */ jsxs("label", { className: "flex min-w-0 flex-col gap-1.5", children: [
166
198
  /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-[var(--text-secondary)]", children: config.label }),
167
199
  /* @__PURE__ */ jsx(
@@ -179,7 +211,20 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet, labels }) {
179
211
  ] });
180
212
  }
181
213
  if (config.type === "enum") {
182
- const comboboxValue = Array.isArray(value) && value.length > 0 ? value[0] : null;
214
+ const strings = getStringArray(value);
215
+ if (strings.length > 1) {
216
+ return /* @__PURE__ */ jsx(
217
+ MultiValueStringSummary,
218
+ {
219
+ label: config.label,
220
+ summary: formatEnumMultiSummary(strings, config.options, labels),
221
+ hint: labels.multiValueFormHint,
222
+ clearLabel: labels.clearFieldForSingleEdit,
223
+ onClearForEdit: () => onChange(void 0)
224
+ }
225
+ );
226
+ }
227
+ const comboboxValue = enumComboboxSelection(strings);
183
228
  return /* @__PURE__ */ jsxs("label", { className: "flex min-w-0 flex-col gap-1.5", children: [
184
229
  /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-[var(--text-secondary)]", children: config.label }),
185
230
  /* @__PURE__ */ jsx(
@@ -267,10 +312,7 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet, labels }) {
267
312
  if (config.type === "compound") {
268
313
  const compound = getCompoundValue(value);
269
314
  const updateSub = (subKey, next) => {
270
- const rest = { ...compound };
271
- delete rest[subKey];
272
- const merged = next === void 0 || isFilterValueEmpty(next) ? rest : { ...rest, [subKey]: next };
273
- onChange(Object.keys(merged).length === 0 ? void 0 : merged);
315
+ onChange(mergeCompoundSubDraft(compound, subKey, next), subKey);
274
316
  };
275
317
  const visibleSubs = (config.subFilters ?? []).filter(
276
318
  (sub) => !hiddenSubSet.has(`${fieldId}:${sub.key}`)
@@ -287,7 +329,22 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet, labels }) {
287
329
  /* @__PURE__ */ jsx("div", { className: "grid min-w-0 gap-2 grid-cols-[repeat(auto-fit,minmax(180px,1fr))]", children: visibleSubs.map((sub) => {
288
330
  const subValue = compound[sub.key];
289
331
  if (sub.type === "text") {
290
- const text = Array.isArray(subValue) ? subValue[0] ?? "" : "";
332
+ const strings = getStringArray(subValue);
333
+ if (strings.length > 1) {
334
+ return /* @__PURE__ */ jsx(
335
+ MultiValueStringSummary,
336
+ {
337
+ label: sub.label,
338
+ summary: formatTextMultiSummary(strings, labels),
339
+ hint: labels.multiValueFormHint,
340
+ clearLabel: labels.clearFieldForSingleEdit,
341
+ onClearForEdit: () => updateSub(sub.key, void 0),
342
+ compact: true
343
+ },
344
+ sub.key
345
+ );
346
+ }
347
+ const text = singleStringDraftValue(strings);
291
348
  return /* @__PURE__ */ jsx(
292
349
  Input,
293
350
  {
@@ -304,7 +361,22 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet, labels }) {
304
361
  );
305
362
  }
306
363
  if (sub.type === "enum") {
307
- const comboboxValue = Array.isArray(subValue) && subValue.length > 0 ? subValue[0] : null;
364
+ const strings = getStringArray(subValue);
365
+ if (strings.length > 1) {
366
+ return /* @__PURE__ */ jsx(
367
+ MultiValueStringSummary,
368
+ {
369
+ label: sub.label,
370
+ summary: formatEnumMultiSummary(strings, sub.options, labels),
371
+ hint: labels.multiValueFormHint,
372
+ clearLabel: labels.clearFieldForSingleEdit,
373
+ onClearForEdit: () => updateSub(sub.key, void 0),
374
+ compact: true
375
+ },
376
+ sub.key
377
+ );
378
+ }
379
+ const comboboxValue = enumComboboxSelection(strings);
308
380
  return /* @__PURE__ */ jsx(
309
381
  Combobox,
310
382
  {
@@ -365,6 +437,31 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet, labels }) {
365
437
  }
366
438
  return null;
367
439
  }
440
+ function MultiValueStringSummary({
441
+ label,
442
+ summary,
443
+ hint,
444
+ clearLabel,
445
+ onClearForEdit,
446
+ compact
447
+ }) {
448
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex min-w-0 flex-col gap-1.5", compact && "col-span-full"), children: [
449
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-[var(--text-secondary)]", children: label }),
450
+ /* @__PURE__ */ jsx("p", { className: "text-[12px] text-[var(--text-primary)]", children: summary }),
451
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-snug text-[var(--text-secondary)]", children: hint }),
452
+ /* @__PURE__ */ jsx(
453
+ Button,
454
+ {
455
+ type: "button",
456
+ variant: "secondary",
457
+ size: "sm",
458
+ className: "h-9 self-start",
459
+ onClick: onClearForEdit,
460
+ children: clearLabel
461
+ }
462
+ )
463
+ ] });
464
+ }
368
465
  export {
369
466
  FilterFormPanel
370
467
  };
@@ -0,0 +1,114 @@
1
+ import { isFilterValueEmpty, isMultiStringFilterValue, getCompoundValue } from "./filter-values.js";
2
+ function compoundSubTouchKey(fieldId, subKey) {
3
+ return `${fieldId}:${subKey}`;
4
+ }
5
+ function recordFilterFieldTouch(touched, fieldId, compoundSubKey) {
6
+ touched.add(fieldId);
7
+ if (compoundSubKey) {
8
+ touched.add(compoundSubTouchKey(fieldId, compoundSubKey));
9
+ }
10
+ }
11
+ function singleStringDraftValue(strings) {
12
+ return strings[0] ?? "";
13
+ }
14
+ function enumComboboxSelection(strings) {
15
+ return strings.length > 0 ? strings[0] : null;
16
+ }
17
+ function patchFieldState(prev, fieldId, value) {
18
+ return { ...prev, [fieldId]: value };
19
+ }
20
+ function mergeCompoundSubDraft(compound, subKey, next) {
21
+ const rest = { ...compound };
22
+ delete rest[subKey];
23
+ const merged = next === void 0 || isFilterValueEmpty(next) ? rest : { ...rest, [subKey]: next };
24
+ return Object.keys(merged).length === 0 ? void 0 : merged;
25
+ }
26
+ function formatTextMultiSummary(values, labels) {
27
+ if (values.length === 2) {
28
+ const joined = values.join(", ");
29
+ return joined.length > 24 ? labels.valuesCount(values.length) : joined;
30
+ }
31
+ return labels.valuesCount(values.length);
32
+ }
33
+ function formatEnumMultiSummary(values, options, labels) {
34
+ if (values.length === 2) {
35
+ const parts = values.map((v) => options?.find((o) => o.value === v)?.label ?? v);
36
+ const joined = parts.join(", ");
37
+ return joined.length > 24 ? labels.selectedCount(values.length) : joined;
38
+ }
39
+ return labels.selectedCount(values.length);
40
+ }
41
+ function shouldPreserveMultiSubOnApply(sub, subCommitted, touchKey, touched) {
42
+ if (sub.type !== "text" && sub.type !== "enum") return false;
43
+ return isMultiStringFilterValue(subCommitted) && !touched.has(touchKey);
44
+ }
45
+ function mergeCompoundValueForApply(config, fieldId, committed, draft, touched, hiddenSubSet) {
46
+ const committedCompound = getCompoundValue(committed);
47
+ const draftCompound = getCompoundValue(draft);
48
+ const merged = {};
49
+ for (const sub of config.subFilters ?? []) {
50
+ if (hiddenSubSet.has(compoundSubTouchKey(fieldId, sub.key))) continue;
51
+ const touchKey = compoundSubTouchKey(fieldId, sub.key);
52
+ const subCommitted = committedCompound[sub.key];
53
+ const subDraft = draftCompound[sub.key];
54
+ if (shouldPreserveMultiSubOnApply(sub, subCommitted, touchKey, touched)) {
55
+ if (subCommitted !== void 0 && !isFilterValueEmpty(subCommitted)) {
56
+ merged[sub.key] = subCommitted;
57
+ }
58
+ continue;
59
+ }
60
+ if (subDraft === void 0 || isFilterValueEmpty(subDraft)) continue;
61
+ merged[sub.key] = subDraft;
62
+ }
63
+ return Object.keys(merged).length === 0 ? void 0 : merged;
64
+ }
65
+ function resolveFieldApplyChange(config, fieldId, committed, draft, touched, hiddenSubSet, isActive) {
66
+ switch (config.type) {
67
+ case "text":
68
+ case "enum": {
69
+ if (isMultiStringFilterValue(committed) && !touched.has(fieldId)) {
70
+ return { kind: "skip" };
71
+ }
72
+ if (draft === void 0 || isFilterValueEmpty(draft)) {
73
+ return isActive ? { kind: "remove" } : { kind: "skip" };
74
+ }
75
+ return { kind: "set", value: draft };
76
+ }
77
+ case "date":
78
+ case "number-range": {
79
+ if (draft === void 0 || isFilterValueEmpty(draft)) {
80
+ return isActive ? { kind: "remove" } : { kind: "skip" };
81
+ }
82
+ return { kind: "set", value: draft };
83
+ }
84
+ case "compound": {
85
+ const merged = mergeCompoundValueForApply(
86
+ config,
87
+ fieldId,
88
+ committed,
89
+ draft,
90
+ touched,
91
+ hiddenSubSet
92
+ );
93
+ if (merged === void 0 || isFilterValueEmpty(merged)) {
94
+ return isActive ? { kind: "remove" } : { kind: "skip" };
95
+ }
96
+ return { kind: "set", value: merged };
97
+ }
98
+ default:
99
+ return { kind: "skip" };
100
+ }
101
+ }
102
+ export {
103
+ compoundSubTouchKey,
104
+ enumComboboxSelection,
105
+ formatEnumMultiSummary,
106
+ formatTextMultiSummary,
107
+ mergeCompoundSubDraft,
108
+ mergeCompoundValueForApply,
109
+ patchFieldState,
110
+ recordFilterFieldTouch,
111
+ resolveFieldApplyChange,
112
+ shouldPreserveMultiSubOnApply,
113
+ singleStringDraftValue
114
+ };
@@ -4,6 +4,9 @@ function isObjectValue(value) {
4
4
  function getStringArray(value) {
5
5
  return Array.isArray(value) ? value : [];
6
6
  }
7
+ function isMultiStringFilterValue(value) {
8
+ return getStringArray(value).length > 1;
9
+ }
7
10
  function getDateRange(value) {
8
11
  const range = {};
9
12
  if (!isObjectValue(value)) return range;
@@ -32,5 +35,6 @@ export {
32
35
  getDateRange,
33
36
  getNumberRange,
34
37
  getStringArray,
35
- isFilterValueEmpty
38
+ isFilterValueEmpty,
39
+ isMultiStringFilterValue
36
40
  };
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import { ChevronDown, SunMoon, LogOut } from "lucide-react";
3
- import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "../ui/dropdown-menu.js";
3
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from "../ui/dropdown-menu.js";
4
4
  import { ThemeSwitch } from "../ui/theme-switch.js";
5
5
  import { cn } from "../../lib/utils.js";
6
6
  import { useAppShell } from "./app-shell-context.js";
@@ -57,37 +57,51 @@ function SidebarAccountMenu({
57
57
  sideOffset: 6,
58
58
  className: "min-w-[220px]",
59
59
  children: [
60
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 px-2 py-2", children: [
61
- avatarUrl ? /* @__PURE__ */ jsx(
62
- "img",
60
+ /* @__PURE__ */ jsxs(
61
+ DropdownMenuItem,
62
+ {
63
+ disabled: true,
64
+ className: "gap-2.5 px-2 py-2 opacity-100 focus:bg-transparent data-[disabled]:opacity-100",
65
+ children: [
66
+ avatarUrl ? /* @__PURE__ */ jsx(
67
+ "img",
68
+ {
69
+ src: avatarUrl,
70
+ alt: "",
71
+ className: "h-8 w-8 shrink-0 rounded-full object-cover"
72
+ }
73
+ ) : /* @__PURE__ */ jsx("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--bg-tertiary)] text-[11px] font-semibold text-[var(--text-secondary)]", children: getInitials(name) }),
74
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
75
+ /* @__PURE__ */ jsx("div", { className: "truncate text-[13px] font-medium text-[var(--text-primary)]", children: name }),
76
+ role && /* @__PURE__ */ jsx("div", { className: "truncate text-[12px] text-[var(--text-secondary)]", children: role })
77
+ ] })
78
+ ]
79
+ }
80
+ ),
81
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
82
+ showTheme && /* @__PURE__ */ jsxs(Fragment, { children: [
83
+ /* @__PURE__ */ jsxs(
84
+ DropdownMenuItem,
63
85
  {
64
- src: avatarUrl,
65
- alt: "",
66
- className: "h-8 w-8 shrink-0 rounded-full object-cover"
86
+ className: "flex items-center justify-between px-3 py-2 focus:bg-transparent",
87
+ onSelect: (event) => event.preventDefault(),
88
+ children: [
89
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
90
+ /* @__PURE__ */ jsx(SunMoon, { className: "h-4 w-4" }),
91
+ /* @__PURE__ */ jsx("span", { className: "text-[13px]", children: "Theme" })
92
+ ] }),
93
+ /* @__PURE__ */ jsx(
94
+ ThemeSwitch,
95
+ {
96
+ variant: "icon",
97
+ value: theme,
98
+ onValueChange: onThemeChange
99
+ }
100
+ )
101
+ ]
67
102
  }
68
- ) : /* @__PURE__ */ jsx("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--bg-tertiary)] text-[11px] font-semibold text-[var(--text-secondary)]", children: getInitials(name) }),
69
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
70
- /* @__PURE__ */ jsx("div", { className: "truncate text-[13px] font-medium text-[var(--text-primary)]", children: name }),
71
- role && /* @__PURE__ */ jsx("div", { className: "truncate text-[12px] text-[var(--text-secondary)]", children: role })
72
- ] })
73
- ] }),
74
- /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-[var(--border-subtle)]" }),
75
- showTheme && /* @__PURE__ */ jsxs(Fragment, { children: [
76
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-2", children: [
77
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
78
- /* @__PURE__ */ jsx(SunMoon, { className: "h-4 w-4" }),
79
- /* @__PURE__ */ jsx("span", { className: "text-[13px]", children: "Theme" })
80
- ] }),
81
- /* @__PURE__ */ jsx(
82
- ThemeSwitch,
83
- {
84
- variant: "icon",
85
- value: theme,
86
- onValueChange: onThemeChange
87
- }
88
- )
89
- ] }),
90
- hasItemsBelow && /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-[var(--border-subtle)]" })
103
+ ),
104
+ hasItemsBelow && /* @__PURE__ */ jsx(DropdownMenuSeparator, {})
91
105
  ] }),
92
106
  menuItems && menuItems.map((item) => {
93
107
  const Icon = item.icon;
@@ -107,7 +121,7 @@ function SidebarAccountMenu({
107
121
  item.key
108
122
  );
109
123
  }),
110
- menuItems && menuItems.length > 0 && onSignOut && /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-[var(--border-subtle)]" }),
124
+ menuItems && menuItems.length > 0 && onSignOut && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
111
125
  onSignOut && /* @__PURE__ */ jsxs(
112
126
  DropdownMenuItem,
113
127
  {
@@ -55,12 +55,36 @@ const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checke
55
55
  }
56
56
  ));
57
57
  DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
58
+ const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => /* @__PURE__ */ jsx(
59
+ DropdownMenuPrimitive.Label,
60
+ {
61
+ ref,
62
+ className: cn(
63
+ "px-2 py-1.5 text-[13px] font-semibold text-[var(--text-primary)]",
64
+ inset && "pl-8",
65
+ className
66
+ ),
67
+ ...props
68
+ }
69
+ ));
70
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
71
+ const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
72
+ DropdownMenuPrimitive.Separator,
73
+ {
74
+ ref,
75
+ className: cn("-mx-1 my-1 h-px bg-[var(--border-subtle)]", className),
76
+ ...props
77
+ }
78
+ ));
79
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
58
80
  export {
59
81
  DropdownMenu,
60
82
  DropdownMenuCheckboxItem,
61
83
  DropdownMenuContent,
62
84
  DropdownMenuGroup,
63
85
  DropdownMenuItem,
86
+ DropdownMenuLabel,
64
87
  DropdownMenuPortal,
88
+ DropdownMenuSeparator,
65
89
  DropdownMenuTrigger
66
90
  };
package/dist/es/index.js CHANGED
@@ -2,7 +2,7 @@ import { Badge, badgeVariants } from "./components/ui/badge.js";
2
2
  import { Button, buttonVariants } from "./components/ui/button.js";
3
3
  import { Checkbox } from "./components/ui/checkbox.js";
4
4
  import { Dialog, DialogClose, DialogContent, DialogDescription, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from "./components/ui/dialog.js";
5
- import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuPortal, DropdownMenuTrigger } from "./components/ui/dropdown-menu.js";
5
+ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuTrigger } from "./components/ui/dropdown-menu.js";
6
6
  import { Input } from "./components/ui/input.js";
7
7
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "./components/ui/popover.js";
8
8
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./components/ui/select.js";
@@ -121,7 +121,9 @@ export {
121
121
  DropdownMenuContent,
122
122
  DropdownMenuGroup,
123
123
  DropdownMenuItem,
124
+ DropdownMenuLabel,
124
125
  DropdownMenuPortal,
126
+ DropdownMenuSeparator,
125
127
  DropdownMenuTrigger,
126
128
  DualLineCell,
127
129
  EmptyState,
package/dist/styles.css CHANGED
@@ -469,6 +469,13 @@
469
469
  .z-50 {
470
470
  z-index: 50
471
471
  }
472
+ .col-span-full {
473
+ grid-column: 1 / -1
474
+ }
475
+ .-mx-1 {
476
+ margin-left: -0.25rem;
477
+ margin-right: -0.25rem
478
+ }
472
479
  .mx-3 {
473
480
  margin-left: 0.75rem;
474
481
  margin-right: 0.75rem
@@ -604,6 +611,9 @@
604
611
  .h-full {
605
612
  height: 100%
606
613
  }
614
+ .h-px {
615
+ height: 1px
616
+ }
607
617
  .max-h-\[280px\] {
608
618
  max-height: 280px
609
619
  }
@@ -889,6 +899,9 @@
889
899
  margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
890
900
  margin-bottom: calc(1rem * var(--tw-space-y-reverse))
891
901
  }
902
+ .self-start {
903
+ align-self: flex-start
904
+ }
892
905
  .self-stretch {
893
906
  align-self: stretch
894
907
  }
@@ -1280,6 +1293,9 @@
1280
1293
  .leading-none {
1281
1294
  line-height: 1
1282
1295
  }
1296
+ .leading-snug {
1297
+ line-height: 1.375
1298
+ }
1283
1299
  .leading-tight {
1284
1300
  line-height: 1.25
1285
1301
  }
@@ -1542,6 +1558,9 @@
1542
1558
  .focus\:border-\[var\(--border-focus\)\]:focus {
1543
1559
  border-color: var(--border-focus)
1544
1560
  }
1561
+ .focus\:bg-transparent:focus {
1562
+ background-color: transparent
1563
+ }
1545
1564
  .focus\:outline-none:focus {
1546
1565
  outline: 2px solid transparent;
1547
1566
  outline-offset: 2px
@@ -1661,6 +1680,9 @@
1661
1680
  .data-\[disabled\=true\]\:opacity-50[data-disabled="true"] {
1662
1681
  opacity: 0.5
1663
1682
  }
1683
+ .data-\[disabled\]\:opacity-100[data-disabled] {
1684
+ opacity: 1
1685
+ }
1664
1686
  .data-\[state\=open\]\:opacity-100[data-state="open"] {
1665
1687
  opacity: 1
1666
1688
  }
@@ -235,6 +235,28 @@ const DropdownMenuCheckboxItem = React__namespace.forwardRef(({ className, child
235
235
  }
236
236
  ));
237
237
  DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive__namespace.CheckboxItem.displayName;
238
+ const DropdownMenuLabel = React__namespace.forwardRef(({ className, inset, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
239
+ DropdownMenuPrimitive__namespace.Label,
240
+ {
241
+ ref,
242
+ className: cn(
243
+ "px-2 py-1.5 text-[13px] font-semibold text-[var(--text-primary)]",
244
+ inset && "pl-8",
245
+ className
246
+ ),
247
+ ...props
248
+ }
249
+ ));
250
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive__namespace.Label.displayName;
251
+ const DropdownMenuSeparator = React__namespace.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
252
+ DropdownMenuPrimitive__namespace.Separator,
253
+ {
254
+ ref,
255
+ className: cn("-mx-1 my-1 h-px bg-[var(--border-subtle)]", className),
256
+ ...props
257
+ }
258
+ ));
259
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive__namespace.Separator.displayName;
238
260
  const Input = React__namespace.forwardRef(
239
261
  ({ className, type, error, ...props }, ref) => {
240
262
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -2237,37 +2259,51 @@ function SidebarAccountMenu({
2237
2259
  sideOffset: 6,
2238
2260
  className: "min-w-[220px]",
2239
2261
  children: [
2240
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 px-2 py-2", children: [
2241
- avatarUrl ? /* @__PURE__ */ jsxRuntime.jsx(
2242
- "img",
2262
+ /* @__PURE__ */ jsxRuntime.jsxs(
2263
+ DropdownMenuItem,
2264
+ {
2265
+ disabled: true,
2266
+ className: "gap-2.5 px-2 py-2 opacity-100 focus:bg-transparent data-[disabled]:opacity-100",
2267
+ children: [
2268
+ avatarUrl ? /* @__PURE__ */ jsxRuntime.jsx(
2269
+ "img",
2270
+ {
2271
+ src: avatarUrl,
2272
+ alt: "",
2273
+ className: "h-8 w-8 shrink-0 rounded-full object-cover"
2274
+ }
2275
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--bg-tertiary)] text-[11px] font-semibold text-[var(--text-secondary)]", children: getInitials(name) }),
2276
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
2277
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-[13px] font-medium text-[var(--text-primary)]", children: name }),
2278
+ role && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-[12px] text-[var(--text-secondary)]", children: role })
2279
+ ] })
2280
+ ]
2281
+ }
2282
+ ),
2283
+ /* @__PURE__ */ jsxRuntime.jsx(DropdownMenuSeparator, {}),
2284
+ showTheme && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2285
+ /* @__PURE__ */ jsxRuntime.jsxs(
2286
+ DropdownMenuItem,
2243
2287
  {
2244
- src: avatarUrl,
2245
- alt: "",
2246
- className: "h-8 w-8 shrink-0 rounded-full object-cover"
2288
+ className: "flex items-center justify-between px-3 py-2 focus:bg-transparent",
2289
+ onSelect: (event) => event.preventDefault(),
2290
+ children: [
2291
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
2292
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.SunMoon, { className: "h-4 w-4" }),
2293
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[13px]", children: "Theme" })
2294
+ ] }),
2295
+ /* @__PURE__ */ jsxRuntime.jsx(
2296
+ ThemeSwitch,
2297
+ {
2298
+ variant: "icon",
2299
+ value: theme,
2300
+ onValueChange: onThemeChange
2301
+ }
2302
+ )
2303
+ ]
2247
2304
  }
2248
- ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--bg-tertiary)] text-[11px] font-semibold text-[var(--text-secondary)]", children: getInitials(name) }),
2249
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
2250
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-[13px] font-medium text-[var(--text-primary)]", children: name }),
2251
- role && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-[12px] text-[var(--text-secondary)]", children: role })
2252
- ] })
2253
- ] }),
2254
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "my-1 border-t border-[var(--border-subtle)]" }),
2255
- showTheme && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2256
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-3 py-2", children: [
2257
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
2258
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.SunMoon, { className: "h-4 w-4" }),
2259
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[13px]", children: "Theme" })
2260
- ] }),
2261
- /* @__PURE__ */ jsxRuntime.jsx(
2262
- ThemeSwitch,
2263
- {
2264
- variant: "icon",
2265
- value: theme,
2266
- onValueChange: onThemeChange
2267
- }
2268
- )
2269
- ] }),
2270
- hasItemsBelow && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "my-1 border-t border-[var(--border-subtle)]" })
2305
+ ),
2306
+ hasItemsBelow && /* @__PURE__ */ jsxRuntime.jsx(DropdownMenuSeparator, {})
2271
2307
  ] }),
2272
2308
  menuItems && menuItems.map((item) => {
2273
2309
  const Icon = item.icon;
@@ -2287,7 +2323,7 @@ function SidebarAccountMenu({
2287
2323
  item.key
2288
2324
  );
2289
2325
  }),
2290
- menuItems && menuItems.length > 0 && onSignOut && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "my-1 border-t border-[var(--border-subtle)]" }),
2326
+ menuItems && menuItems.length > 0 && onSignOut && /* @__PURE__ */ jsxRuntime.jsx(DropdownMenuSeparator, {}),
2291
2327
  onSignOut && /* @__PURE__ */ jsxRuntime.jsxs(
2292
2328
  DropdownMenuItem,
2293
2329
  {
@@ -2825,6 +2861,9 @@ function isObjectValue(value) {
2825
2861
  function getStringArray(value) {
2826
2862
  return Array.isArray(value) ? value : [];
2827
2863
  }
2864
+ function isMultiStringFilterValue(value) {
2865
+ return getStringArray(value).length > 1;
2866
+ }
2828
2867
  function getDateRange(value) {
2829
2868
  const range = {};
2830
2869
  if (!isObjectValue(value)) return range;
@@ -3998,6 +4037,106 @@ function AppliedStateChips({
3998
4037
  )
3999
4038
  ] });
4000
4039
  }
4040
+ function compoundSubTouchKey(fieldId, subKey) {
4041
+ return `${fieldId}:${subKey}`;
4042
+ }
4043
+ function recordFilterFieldTouch(touched, fieldId, compoundSubKey) {
4044
+ touched.add(fieldId);
4045
+ if (compoundSubKey) {
4046
+ touched.add(compoundSubTouchKey(fieldId, compoundSubKey));
4047
+ }
4048
+ }
4049
+ function singleStringDraftValue(strings) {
4050
+ return strings[0] ?? "";
4051
+ }
4052
+ function enumComboboxSelection(strings) {
4053
+ return strings.length > 0 ? strings[0] : null;
4054
+ }
4055
+ function patchFieldState(prev, fieldId, value) {
4056
+ return { ...prev, [fieldId]: value };
4057
+ }
4058
+ function mergeCompoundSubDraft(compound, subKey, next) {
4059
+ const rest = { ...compound };
4060
+ delete rest[subKey];
4061
+ const merged = next === void 0 || isFilterValueEmpty(next) ? rest : { ...rest, [subKey]: next };
4062
+ return Object.keys(merged).length === 0 ? void 0 : merged;
4063
+ }
4064
+ function formatTextMultiSummary(values, labels) {
4065
+ if (values.length === 2) {
4066
+ const joined = values.join(", ");
4067
+ return joined.length > 24 ? labels.valuesCount(values.length) : joined;
4068
+ }
4069
+ return labels.valuesCount(values.length);
4070
+ }
4071
+ function formatEnumMultiSummary(values, options, labels) {
4072
+ if (values.length === 2) {
4073
+ const parts = values.map((v) => options?.find((o) => o.value === v)?.label ?? v);
4074
+ const joined = parts.join(", ");
4075
+ return joined.length > 24 ? labels.selectedCount(values.length) : joined;
4076
+ }
4077
+ return labels.selectedCount(values.length);
4078
+ }
4079
+ function shouldPreserveMultiSubOnApply(sub, subCommitted, touchKey, touched) {
4080
+ if (sub.type !== "text" && sub.type !== "enum") return false;
4081
+ return isMultiStringFilterValue(subCommitted) && !touched.has(touchKey);
4082
+ }
4083
+ function mergeCompoundValueForApply(config, fieldId, committed, draft, touched, hiddenSubSet) {
4084
+ const committedCompound = getCompoundValue(committed);
4085
+ const draftCompound = getCompoundValue(draft);
4086
+ const merged = {};
4087
+ for (const sub of config.subFilters ?? []) {
4088
+ if (hiddenSubSet.has(compoundSubTouchKey(fieldId, sub.key))) continue;
4089
+ const touchKey = compoundSubTouchKey(fieldId, sub.key);
4090
+ const subCommitted = committedCompound[sub.key];
4091
+ const subDraft = draftCompound[sub.key];
4092
+ if (shouldPreserveMultiSubOnApply(sub, subCommitted, touchKey, touched)) {
4093
+ if (subCommitted !== void 0 && !isFilterValueEmpty(subCommitted)) {
4094
+ merged[sub.key] = subCommitted;
4095
+ }
4096
+ continue;
4097
+ }
4098
+ if (subDraft === void 0 || isFilterValueEmpty(subDraft)) continue;
4099
+ merged[sub.key] = subDraft;
4100
+ }
4101
+ return Object.keys(merged).length === 0 ? void 0 : merged;
4102
+ }
4103
+ function resolveFieldApplyChange(config, fieldId, committed, draft, touched, hiddenSubSet, isActive) {
4104
+ switch (config.type) {
4105
+ case "text":
4106
+ case "enum": {
4107
+ if (isMultiStringFilterValue(committed) && !touched.has(fieldId)) {
4108
+ return { kind: "skip" };
4109
+ }
4110
+ if (draft === void 0 || isFilterValueEmpty(draft)) {
4111
+ return isActive ? { kind: "remove" } : { kind: "skip" };
4112
+ }
4113
+ return { kind: "set", value: draft };
4114
+ }
4115
+ case "date":
4116
+ case "number-range": {
4117
+ if (draft === void 0 || isFilterValueEmpty(draft)) {
4118
+ return isActive ? { kind: "remove" } : { kind: "skip" };
4119
+ }
4120
+ return { kind: "set", value: draft };
4121
+ }
4122
+ case "compound": {
4123
+ const merged = mergeCompoundValueForApply(
4124
+ config,
4125
+ fieldId,
4126
+ committed,
4127
+ draft,
4128
+ touched,
4129
+ hiddenSubSet
4130
+ );
4131
+ if (merged === void 0 || isFilterValueEmpty(merged)) {
4132
+ return isActive ? { kind: "remove" } : { kind: "skip" };
4133
+ }
4134
+ return { kind: "set", value: merged };
4135
+ }
4136
+ default:
4137
+ return { kind: "skip" };
4138
+ }
4139
+ }
4001
4140
  function applyManagedChanges(orch, changes) {
4002
4141
  const activeIds = new Set(orch.columnFilters.map((f) => f.columnId));
4003
4142
  for (const change of changes) {
@@ -4031,7 +4170,11 @@ const DEFAULT_LABELS = {
4031
4170
  compoundSubRangeFromAriaLabel: (subLabel) => `${subLabel} — from`,
4032
4171
  compoundSubRangeToAriaLabel: (subLabel) => `${subLabel} — to`,
4033
4172
  compoundSubRangeFromPlaceholder: (subLabel) => `${subLabel} from`,
4034
- compoundSubRangeToPlaceholder: (subLabel) => `${subLabel} to`
4173
+ compoundSubRangeToPlaceholder: (subLabel) => `${subLabel} to`,
4174
+ multiValueFormHint: "Multiple values — edit using the column filter chips or popover above the table.",
4175
+ valuesCount: (count) => `${count} values`,
4176
+ selectedCount: (count) => `${count} selected`,
4177
+ clearFieldForSingleEdit: "Clear to edit one value"
4035
4178
  };
4036
4179
  function deriveInitialState(fields, filters) {
4037
4180
  const byId = new Map(filters.map((f) => [f.columnId, f.value]));
@@ -4081,24 +4224,38 @@ function FilterFormPanel({
4081
4224
  return changed ? next : prev;
4082
4225
  });
4083
4226
  }, [filters, renderableFields]);
4084
- const setField = React.useCallback((id2, value) => {
4085
- touchedRef.current.add(id2);
4086
- setState((prev) => ({ ...prev, [id2]: value }));
4087
- }, []);
4227
+ const setField = React.useCallback(
4228
+ (id2, value, compoundSubKey) => {
4229
+ recordFilterFieldTouch(touchedRef.current, id2, compoundSubKey);
4230
+ setState((prev) => patchFieldState(prev, id2, value));
4231
+ },
4232
+ []
4233
+ );
4088
4234
  const handleApply = React.useCallback(() => {
4089
4235
  const activeIds = new Set(filters.map((f) => f.columnId));
4236
+ const committedById = new Map(filters.map((f) => [f.columnId, f.value]));
4237
+ const touched = touchedRef.current;
4090
4238
  const changes = [];
4091
4239
  for (const id2 of renderableFields) {
4092
- const value = state[id2];
4093
- if (value === void 0 || isFilterValueEmpty(value)) {
4094
- if (activeIds.has(id2)) changes.push({ columnId: id2, action: "remove" });
4095
- } else {
4096
- changes.push({ columnId: id2, action: "set", value });
4097
- }
4240
+ const config = filterConfigs[id2];
4241
+ const committed = committedById.get(id2);
4242
+ const draft = state[id2];
4243
+ const resolution = resolveFieldApplyChange(
4244
+ config,
4245
+ id2,
4246
+ committed,
4247
+ draft,
4248
+ touched,
4249
+ hiddenSubSet,
4250
+ activeIds.has(id2)
4251
+ );
4252
+ if (resolution.kind === "skip") continue;
4253
+ if (resolution.kind === "remove") changes.push({ columnId: id2, action: "remove" });
4254
+ else changes.push({ columnId: id2, action: "set", value: resolution.value });
4098
4255
  }
4099
4256
  onApply(changes);
4100
4257
  touchedRef.current.clear();
4101
- }, [filters, onApply, renderableFields, state]);
4258
+ }, [filterConfigs, filters, hiddenSubSet, onApply, renderableFields, state]);
4102
4259
  const handleClear = React.useCallback(() => {
4103
4260
  setState(deriveInitialState(renderableFields, []));
4104
4261
  onClear(renderableFields);
@@ -4143,7 +4300,7 @@ function FilterFormPanel({
4143
4300
  {
4144
4301
  config,
4145
4302
  value: state[fieldId],
4146
- onChange: (v) => setField(fieldId, v),
4303
+ onChange: (v, compoundSubKey) => setField(fieldId, v, compoundSubKey),
4147
4304
  fieldId,
4148
4305
  hiddenSubSet,
4149
4306
  labels
@@ -4171,7 +4328,20 @@ function FilterFormPanel({
4171
4328
  }
4172
4329
  function Field({ config, value, onChange, fieldId, hiddenSubSet, labels }) {
4173
4330
  if (config.type === "text") {
4174
- const text = Array.isArray(value) ? value[0] ?? "" : "";
4331
+ const strings = getStringArray(value);
4332
+ if (strings.length > 1) {
4333
+ return /* @__PURE__ */ jsxRuntime.jsx(
4334
+ MultiValueStringSummary,
4335
+ {
4336
+ label: config.label,
4337
+ summary: formatTextMultiSummary(strings, labels),
4338
+ hint: labels.multiValueFormHint,
4339
+ clearLabel: labels.clearFieldForSingleEdit,
4340
+ onClearForEdit: () => onChange(void 0)
4341
+ }
4342
+ );
4343
+ }
4344
+ const text = singleStringDraftValue(strings);
4175
4345
  return /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex min-w-0 flex-col gap-1.5", children: [
4176
4346
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] font-medium text-[var(--text-secondary)]", children: config.label }),
4177
4347
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4189,7 +4359,20 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet, labels }) {
4189
4359
  ] });
4190
4360
  }
4191
4361
  if (config.type === "enum") {
4192
- const comboboxValue = Array.isArray(value) && value.length > 0 ? value[0] : null;
4362
+ const strings = getStringArray(value);
4363
+ if (strings.length > 1) {
4364
+ return /* @__PURE__ */ jsxRuntime.jsx(
4365
+ MultiValueStringSummary,
4366
+ {
4367
+ label: config.label,
4368
+ summary: formatEnumMultiSummary(strings, config.options, labels),
4369
+ hint: labels.multiValueFormHint,
4370
+ clearLabel: labels.clearFieldForSingleEdit,
4371
+ onClearForEdit: () => onChange(void 0)
4372
+ }
4373
+ );
4374
+ }
4375
+ const comboboxValue = enumComboboxSelection(strings);
4193
4376
  return /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex min-w-0 flex-col gap-1.5", children: [
4194
4377
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] font-medium text-[var(--text-secondary)]", children: config.label }),
4195
4378
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4277,10 +4460,7 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet, labels }) {
4277
4460
  if (config.type === "compound") {
4278
4461
  const compound = getCompoundValue(value);
4279
4462
  const updateSub = (subKey, next) => {
4280
- const rest = { ...compound };
4281
- delete rest[subKey];
4282
- const merged = next === void 0 || isFilterValueEmpty(next) ? rest : { ...rest, [subKey]: next };
4283
- onChange(Object.keys(merged).length === 0 ? void 0 : merged);
4463
+ onChange(mergeCompoundSubDraft(compound, subKey, next), subKey);
4284
4464
  };
4285
4465
  const visibleSubs = (config.subFilters ?? []).filter(
4286
4466
  (sub) => !hiddenSubSet.has(`${fieldId}:${sub.key}`)
@@ -4297,7 +4477,22 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet, labels }) {
4297
4477
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid min-w-0 gap-2 grid-cols-[repeat(auto-fit,minmax(180px,1fr))]", children: visibleSubs.map((sub) => {
4298
4478
  const subValue = compound[sub.key];
4299
4479
  if (sub.type === "text") {
4300
- const text = Array.isArray(subValue) ? subValue[0] ?? "" : "";
4480
+ const strings = getStringArray(subValue);
4481
+ if (strings.length > 1) {
4482
+ return /* @__PURE__ */ jsxRuntime.jsx(
4483
+ MultiValueStringSummary,
4484
+ {
4485
+ label: sub.label,
4486
+ summary: formatTextMultiSummary(strings, labels),
4487
+ hint: labels.multiValueFormHint,
4488
+ clearLabel: labels.clearFieldForSingleEdit,
4489
+ onClearForEdit: () => updateSub(sub.key, void 0),
4490
+ compact: true
4491
+ },
4492
+ sub.key
4493
+ );
4494
+ }
4495
+ const text = singleStringDraftValue(strings);
4301
4496
  return /* @__PURE__ */ jsxRuntime.jsx(
4302
4497
  Input,
4303
4498
  {
@@ -4314,7 +4509,22 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet, labels }) {
4314
4509
  );
4315
4510
  }
4316
4511
  if (sub.type === "enum") {
4317
- const comboboxValue = Array.isArray(subValue) && subValue.length > 0 ? subValue[0] : null;
4512
+ const strings = getStringArray(subValue);
4513
+ if (strings.length > 1) {
4514
+ return /* @__PURE__ */ jsxRuntime.jsx(
4515
+ MultiValueStringSummary,
4516
+ {
4517
+ label: sub.label,
4518
+ summary: formatEnumMultiSummary(strings, sub.options, labels),
4519
+ hint: labels.multiValueFormHint,
4520
+ clearLabel: labels.clearFieldForSingleEdit,
4521
+ onClearForEdit: () => updateSub(sub.key, void 0),
4522
+ compact: true
4523
+ },
4524
+ sub.key
4525
+ );
4526
+ }
4527
+ const comboboxValue = enumComboboxSelection(strings);
4318
4528
  return /* @__PURE__ */ jsxRuntime.jsx(
4319
4529
  Combobox,
4320
4530
  {
@@ -4375,6 +4585,31 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet, labels }) {
4375
4585
  }
4376
4586
  return null;
4377
4587
  }
4588
+ function MultiValueStringSummary({
4589
+ label,
4590
+ summary,
4591
+ hint,
4592
+ clearLabel,
4593
+ onClearForEdit,
4594
+ compact
4595
+ }) {
4596
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex min-w-0 flex-col gap-1.5", compact && "col-span-full"), children: [
4597
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] font-medium text-[var(--text-secondary)]", children: label }),
4598
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[12px] text-[var(--text-primary)]", children: summary }),
4599
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] leading-snug text-[var(--text-secondary)]", children: hint }),
4600
+ /* @__PURE__ */ jsxRuntime.jsx(
4601
+ Button,
4602
+ {
4603
+ type: "button",
4604
+ variant: "secondary",
4605
+ size: "sm",
4606
+ className: "h-9 self-start",
4607
+ onClick: onClearForEdit,
4608
+ children: clearLabel
4609
+ }
4610
+ )
4611
+ ] });
4612
+ }
4378
4613
  const STORAGE_PREFIX = "tablewright.filters.open.";
4379
4614
  function readPersisted(persistKey, fallback) {
4380
4615
  if (typeof window === "undefined") return fallback;
@@ -5640,7 +5875,9 @@ exports.DropdownMenuCheckboxItem = DropdownMenuCheckboxItem;
5640
5875
  exports.DropdownMenuContent = DropdownMenuContent;
5641
5876
  exports.DropdownMenuGroup = DropdownMenuGroup;
5642
5877
  exports.DropdownMenuItem = DropdownMenuItem;
5878
+ exports.DropdownMenuLabel = DropdownMenuLabel;
5643
5879
  exports.DropdownMenuPortal = DropdownMenuPortal;
5880
+ exports.DropdownMenuSeparator = DropdownMenuSeparator;
5644
5881
  exports.DropdownMenuTrigger = DropdownMenuTrigger;
5645
5882
  exports.DualLineCell = DualLineCell;
5646
5883
  exports.EmptyState = EmptyState;
@@ -317,6 +317,13 @@
317
317
  .z-50 {
318
318
  z-index: 50
319
319
  }
320
+ .col-span-full {
321
+ grid-column: 1 / -1
322
+ }
323
+ .-mx-1 {
324
+ margin-left: -0.25rem;
325
+ margin-right: -0.25rem
326
+ }
320
327
  .mx-3 {
321
328
  margin-left: 0.75rem;
322
329
  margin-right: 0.75rem
@@ -452,6 +459,9 @@
452
459
  .h-full {
453
460
  height: 100%
454
461
  }
462
+ .h-px {
463
+ height: 1px
464
+ }
455
465
  .max-h-\[280px\] {
456
466
  max-height: 280px
457
467
  }
@@ -737,6 +747,9 @@
737
747
  margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
738
748
  margin-bottom: calc(1rem * var(--tw-space-y-reverse))
739
749
  }
750
+ .self-start {
751
+ align-self: flex-start
752
+ }
740
753
  .self-stretch {
741
754
  align-self: stretch
742
755
  }
@@ -1128,6 +1141,9 @@
1128
1141
  .leading-none {
1129
1142
  line-height: 1
1130
1143
  }
1144
+ .leading-snug {
1145
+ line-height: 1.375
1146
+ }
1131
1147
  .leading-tight {
1132
1148
  line-height: 1.25
1133
1149
  }
@@ -1390,6 +1406,9 @@
1390
1406
  .focus\:border-\[var\(--border-focus\)\]:focus {
1391
1407
  border-color: var(--border-focus)
1392
1408
  }
1409
+ .focus\:bg-transparent:focus {
1410
+ background-color: transparent
1411
+ }
1393
1412
  .focus\:outline-none:focus {
1394
1413
  outline: 2px solid transparent;
1395
1414
  outline-offset: 2px
@@ -1509,6 +1528,9 @@
1509
1528
  .data-\[disabled\=true\]\:opacity-50[data-disabled="true"] {
1510
1529
  opacity: 0.5
1511
1530
  }
1531
+ .data-\[disabled\]\:opacity-100[data-disabled] {
1532
+ opacity: 1
1533
+ }
1512
1534
  .data-\[state\=open\]\:opacity-100[data-state="open"] {
1513
1535
  opacity: 1
1514
1536
  }
@@ -14,6 +14,12 @@ export interface FilterFormPanelLabels {
14
14
  compoundSubRangeToAriaLabel: (subLabel: string) => string;
15
15
  compoundSubRangeFromPlaceholder: (subLabel: string) => string;
16
16
  compoundSubRangeToPlaceholder: (subLabel: string) => string;
17
+ /** Shown under read-only multi-value text fields in the form panel. */
18
+ multiValueFormHint: string;
19
+ valuesCount: (count: number) => string;
20
+ selectedCount: (count: number) => string;
21
+ /** Clears a multi-value field so the user can enter a single value. */
22
+ clearFieldForSingleEdit: string;
17
23
  }
18
24
  export interface FilterFormPanelProps {
19
25
  /** All known filter configs (same shape used by GroupedFilterAdd). */
@@ -0,0 +1,27 @@
1
+ import type { ActiveFilterValue, ColumnFilterConfig, CompoundFilterValue, CompoundSubFilter } from "./filter-types.js";
2
+ export interface MultiValueSummaryLabels {
3
+ valuesCount: (count: number) => string;
4
+ selectedCount: (count: number) => string;
5
+ }
6
+ export declare function compoundSubTouchKey(fieldId: string, subKey: string): string;
7
+ export declare function recordFilterFieldTouch(touched: Set<string>, fieldId: string, compoundSubKey?: string): void;
8
+ export declare function singleStringDraftValue(strings: string[]): string;
9
+ export declare function enumComboboxSelection(strings: string[]): string | null;
10
+ export declare function patchFieldState<T extends Record<string, ActiveFilterValue | undefined>>(prev: T, fieldId: string, value: ActiveFilterValue | undefined): T;
11
+ export declare function mergeCompoundSubDraft(compound: CompoundFilterValue, subKey: string, next: ActiveFilterValue | undefined): ActiveFilterValue | undefined;
12
+ export declare function formatTextMultiSummary(values: string[], labels: MultiValueSummaryLabels): string;
13
+ export declare function formatEnumMultiSummary(values: string[], options: {
14
+ label: string;
15
+ value: string;
16
+ }[] | undefined, labels: MultiValueSummaryLabels): string;
17
+ export declare function shouldPreserveMultiSubOnApply(sub: CompoundSubFilter, subCommitted: ActiveFilterValue | undefined, touchKey: string, touched: Set<string>): boolean;
18
+ export declare function mergeCompoundValueForApply(config: ColumnFilterConfig, fieldId: string, committed: ActiveFilterValue | undefined, draft: ActiveFilterValue | undefined, touched: Set<string>, hiddenSubSet: Set<string>): ActiveFilterValue | undefined;
19
+ export type ApplyResolution = {
20
+ kind: "skip";
21
+ } | {
22
+ kind: "remove";
23
+ } | {
24
+ kind: "set";
25
+ value: ActiveFilterValue;
26
+ };
27
+ export declare function resolveFieldApplyChange(config: ColumnFilterConfig, fieldId: string, committed: ActiveFilterValue | undefined, draft: ActiveFilterValue | undefined, touched: Set<string>, hiddenSubSet: Set<string>, isActive: boolean): ApplyResolution;
@@ -34,6 +34,14 @@ export type NumberRangeValue = {
34
34
  export type CompoundFilterValue = {
35
35
  [key: string]: ActiveFilterValue;
36
36
  };
37
+ /**
38
+ * Stored filter payload for one column. Text and enum filters are usually `string[]`
39
+ * (one element for a single selection; multiple for popover/chip multi-select).
40
+ *
41
+ * `FilterFormPanel` edits at most one string per text/enum field (and per compound
42
+ * sub-filter of those types). Multi-value text/enum filters are edited via column
43
+ * filter chips and `ColumnFilterPopover`, not the form panel.
44
+ */
37
45
  export type ActiveFilterValue = string | string[] | DateRangeValue | NumberRangeValue | CompoundFilterValue;
38
46
  export interface ActiveFilter {
39
47
  columnId: string;
@@ -1,6 +1,8 @@
1
1
  import type { ActiveFilterValue, CompoundFilterValue, DateRangeValue, NumberRangeValue } from "./filter-types.js";
2
2
  /** Read a text/enum filter value. Non-array values read as `[]`. */
3
3
  export declare function getStringArray(value: ActiveFilterValue | undefined): string[];
4
+ /** True when a text/enum value selects more than one string (popover/chip multi-select). */
5
+ export declare function isMultiStringFilterValue(value: ActiveFilterValue | undefined): boolean;
4
6
  /** Read a date filter value. Non-string bounds are dropped. */
5
7
  export declare function getDateRange(value: ActiveFilterValue | undefined): DateRangeValue;
6
8
  /** Read a number-range filter value. Non-number bounds are dropped. */
@@ -7,4 +7,8 @@ declare const DropdownMenuPortal: React.FC<DropdownMenuPrimitive.DropdownMenuPor
7
7
  declare const DropdownMenuContent: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
8
8
  declare const DropdownMenuItem: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
9
9
  declare const DropdownMenuCheckboxItem: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuCheckboxItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
10
- export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuPortal, DropdownMenuTrigger, };
10
+ declare const DropdownMenuLabel: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuLabelProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
11
+ inset?: boolean;
12
+ } & React.RefAttributes<HTMLDivElement>>;
13
+ declare const DropdownMenuSeparator: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuSeparatorProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
14
+ export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuTrigger, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@undevy-org/tablewright",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
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": {