@wallarm-org/design-system 0.26.0 → 0.27.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.
Files changed (59) hide show
  1. package/dist/components/Badge/index.d.ts +1 -0
  2. package/dist/components/FilterInput/FilterInputErrors/parseFilterInputErrors.js +11 -0
  3. package/dist/components/FilterInput/FilterInputMenu/FilterInputMenu.d.ts +2 -1
  4. package/dist/components/FilterInput/FilterInputMenu/FilterInputMenu.js +18 -5
  5. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/FilterInputValueMenu.d.ts +5 -1
  6. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/FilterInputValueMenu.js +13 -84
  7. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/ValueMenuFooter.d.ts +6 -0
  8. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/ValueMenuFooter.js +72 -0
  9. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/ValueMenuItem.js +6 -9
  10. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/useValueMenuDisplayValues.d.ts +31 -0
  11. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/useValueMenuDisplayValues.js +39 -0
  12. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/useValueMenuState.d.ts +6 -1
  13. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/useValueMenuState.js +5 -3
  14. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipEditing.d.ts +1 -0
  15. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipEditing.js +5 -0
  16. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.d.ts +1 -0
  17. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.js +8 -3
  18. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useInputHandlers.js +7 -2
  19. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow.d.ts +2 -0
  20. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow.js +8 -0
  21. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/valueCommitHelpers.js +13 -2
  22. package/dist/components/FilterInput/index.d.ts +1 -1
  23. package/dist/components/FilterInput/index.js +2 -2
  24. package/dist/components/FilterInput/lib/applyAcceptChar.d.ts +7 -0
  25. package/dist/components/FilterInput/lib/applyAcceptChar.js +5 -0
  26. package/dist/components/FilterInput/lib/fields.d.ts +8 -2
  27. package/dist/components/FilterInput/lib/fields.js +2 -2
  28. package/dist/components/FilterInput/lib/index.d.ts +3 -2
  29. package/dist/components/FilterInput/lib/index.js +4 -3
  30. package/dist/components/FilterInput/lib/menuFilterText.d.ts +19 -4
  31. package/dist/components/FilterInput/lib/menuFilterText.js +11 -8
  32. package/dist/components/FilterInput/lib/statusCode/createStatusCodeInputFilter.d.ts +6 -0
  33. package/dist/components/FilterInput/lib/statusCode/createStatusCodeInputFilter.js +2 -0
  34. package/dist/components/FilterInput/lib/statusCode/createStatusCodeNormalizer.d.ts +10 -0
  35. package/dist/components/FilterInput/lib/statusCode/createStatusCodeNormalizer.js +9 -0
  36. package/dist/components/FilterInput/lib/statusCode/createStatusCodeSuggestions.d.ts +16 -0
  37. package/dist/components/FilterInput/lib/statusCode/createStatusCodeSuggestions.js +21 -0
  38. package/dist/components/FilterInput/lib/statusCode/createStatusCodeValidator.d.ts +19 -0
  39. package/dist/components/FilterInput/lib/statusCode/createStatusCodeValidator.js +8 -0
  40. package/dist/components/FilterInput/lib/statusCode/index.d.ts +5 -0
  41. package/dist/components/FilterInput/lib/statusCode/index.js +5 -0
  42. package/dist/components/FilterInput/lib/statusCode/utils/canonicalize.d.ts +20 -0
  43. package/dist/components/FilterInput/lib/statusCode/utils/canonicalize.js +19 -0
  44. package/dist/components/FilterInput/lib/statusCode/utils/constants.d.ts +14 -0
  45. package/dist/components/FilterInput/lib/statusCode/utils/constants.js +17 -0
  46. package/dist/components/FilterInput/lib/statusCode/utils/index.d.ts +5 -0
  47. package/dist/components/FilterInput/lib/statusCode/utils/index.js +5 -0
  48. package/dist/components/FilterInput/lib/statusCode/utils/makeMask.d.ts +7 -0
  49. package/dist/components/FilterInput/lib/statusCode/utils/makeMask.js +14 -0
  50. package/dist/components/FilterInput/lib/statusCode/utils/maskRoots.d.ts +4 -0
  51. package/dist/components/FilterInput/lib/statusCode/utils/maskRoots.js +3 -0
  52. package/dist/components/FilterInput/lib/statusCode/utils/types.d.ts +8 -0
  53. package/dist/components/FilterInput/lib/statusCode/utils/types.js +0 -0
  54. package/dist/components/FilterInput/stories/mockStatusCodes.js +2 -0
  55. package/dist/components/FilterInput/types.d.ts +32 -2
  56. package/dist/metadata/components.json +2 -2
  57. package/package.json +1 -1
  58. package/dist/components/FilterInput/lib/statusCodeSuggestions.d.ts +0 -10
  59. package/dist/components/FilterInput/lib/statusCodeSuggestions.js +0 -45
@@ -1,2 +1,3 @@
1
1
  export { Badge, type BadgeProps } from './Badge';
2
2
  export { badgeVariants } from './classes';
3
+ export type { BadgeColor, BadgeType } from './types';
@@ -11,6 +11,17 @@ const parseFilterInputErrors = (conditions, fields)=>{
11
11
  errors.push(`Unknown field ${condition.field}`);
12
12
  break;
13
13
  case 'value':
14
+ if (field?.validate) {
15
+ const values = Array.isArray(condition.value) ? condition.value : [
16
+ condition.value
17
+ ];
18
+ const invalidValues = values.filter((v)=>null != v && field.validate(v));
19
+ if (invalidValues.length > 0) {
20
+ const formatted = invalidValues.map((v)=>String(v)).join(', ');
21
+ errors.push(`Invalid value for ${label}: ${formatted}`);
22
+ break;
23
+ }
24
+ }
14
25
  if (field && hasStaticAllowlist(field) && Array.isArray(condition.value)) {
15
26
  const fv = getFieldValues(field);
16
27
  if (fv.length > 0) {
@@ -1,4 +1,4 @@
1
- import type { FC, RefObject } from 'react';
1
+ import { type FC, type RefObject } from 'react';
2
2
  import type { ChipSegment } from '../FilterInputField/FilterInputChip';
3
3
  import type { FieldMetadata, FilterOperator, MenuState } from '../types';
4
4
  export interface FilterInputAutocompleteState {
@@ -20,6 +20,7 @@ export interface FilterInputAutocompleteState {
20
20
  handleMenuClose: () => void;
21
21
  handleMenuDiscard: () => void;
22
22
  handleBuildingValueChange: (preview: string | undefined) => void;
23
+ handleMultiSelectToggle: () => void;
23
24
  segmentFilterText: string;
24
25
  segmentMenuFilterText: string;
25
26
  editingSegment: ChipSegment | null;
@@ -1,16 +1,28 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { getFieldValues, getValueFilterText, isBetweenOperator, isMultiSelectOperator } from "../lib/index.js";
2
+ import { useMemo } from "react";
3
+ import { getCurrentValueTokenText, getFieldValues, getValueFilterText, isBetweenOperator, isMultiSelectOperator } from "../lib/index.js";
3
4
  import { FilterInputDateValueMenu } from "./FilterInputDateValueMenu/index.js";
4
5
  import { FilterInputFieldMenu } from "./FilterInputFieldMenu/index.js";
5
6
  import { FilterInputOperatorMenu } from "./FilterInputOperatorMenu.js";
6
7
  import { FilterInputValueMenu } from "./FilterInputValueMenu/index.js";
7
8
  const FilterInputMenu = ({ fields, autocomplete })=>{
8
- const { inputText, menuState, selectedField, selectedOperator, menuPositioning, editingMultiValues, editingSingleValue, editingDateRange, inputRef, menuRef, handleFieldSelect, handleOperatorSelect, handleValueSelect, handleMultiCommit, handleRangeSelect, handleMenuClose, handleMenuDiscard, handleBuildingValueChange, segmentMenuFilterText, editingSegment, blurCommitRef } = autocomplete;
9
+ const { inputText, menuState, selectedField, selectedOperator, menuPositioning, editingMultiValues, editingSingleValue, editingDateRange, inputRef, menuRef, handleFieldSelect, handleOperatorSelect, handleValueSelect, handleMultiCommit, handleRangeSelect, handleMenuClose, handleMenuDiscard, handleBuildingValueChange, handleMultiSelectToggle, segmentMenuFilterText, editingSegment, blurCommitRef } = autocomplete;
9
10
  const fieldFilterText = 'attribute' === editingSegment ? segmentMenuFilterText : inputText;
10
11
  const operatorFilterText = 'operator' === editingSegment ? segmentMenuFilterText : inputText;
11
- const valueInputText = 'value' === editingSegment ? segmentMenuFilterText : inputText;
12
- const selectedFieldValues = selectedField ? getFieldValues(selectedField, valueInputText) : [];
13
- const valueFilterText = getValueFilterText(editingSegment, inputText, segmentMenuFilterText, selectedOperator, selectedFieldValues);
12
+ const currentTokenText = getCurrentValueTokenText(editingSegment, inputText, segmentMenuFilterText, selectedOperator);
13
+ const selectedContext = useMemo(()=>({
14
+ selectedValues: [
15
+ ...editingMultiValues,
16
+ ...null != editingSingleValue ? [
17
+ editingSingleValue
18
+ ] : []
19
+ ]
20
+ }), [
21
+ editingMultiValues,
22
+ editingSingleValue
23
+ ]);
24
+ const selectedFieldValues = selectedField ? getFieldValues(selectedField, currentTokenText, selectedContext) : [];
25
+ const valueFilterText = getValueFilterText(currentTokenText, selectedOperator, selectedFieldValues);
14
26
  const showOperatorMenu = !!selectedField;
15
27
  const showValueMenu = !!selectedField && !!selectedOperator;
16
28
  const isDateField = selectedField?.type === 'date';
@@ -64,6 +76,7 @@ const FilterInputMenu = ({ fields, autocomplete })=>{
64
76
  highlightValue: editingSingleValue,
65
77
  positioning: menuPositioning,
66
78
  onBuildingValueChange: handleBuildingValueChange,
79
+ onItemToggle: handleMultiSelectToggle,
67
80
  inputRef: inputRef,
68
81
  menuRef: menuRef,
69
82
  filterText: valueFilterText,
@@ -1,9 +1,10 @@
1
1
  import { type FC, type RefObject } from 'react';
2
+ import type { BadgeColor } from '../../../Badge';
2
3
  export interface ValueOption {
3
4
  value: string | number | boolean;
4
5
  label: string;
5
6
  badge?: {
6
- color: string;
7
+ color: BadgeColor;
7
8
  text: string;
8
9
  };
9
10
  hasSubmenu?: boolean;
@@ -22,6 +23,9 @@ export interface FilterInputValueMenuProps {
22
23
  width?: 'standard' | 'compact' | number;
23
24
  positioning?: Record<string, unknown>;
24
25
  onBuildingValueChange?: (preview: string | undefined) => void;
26
+ /** Fires on explicit multi-select toggle (click or keyboard) — use to react
27
+ * only to user-initiated toggles, not to initialization. */
28
+ onItemToggle?: () => void;
25
29
  /** Ref to the query bar input — ArrowUp on first item returns focus here */
26
30
  inputRef?: RefObject<HTMLInputElement | null>;
27
31
  /** Text to filter values by label */
@@ -1,14 +1,14 @@
1
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { cn } from "../../../../utils/cn.js";
4
- import { DropdownMenu, DropdownMenuContent, DropdownMenuFooter, DropdownMenuGroup } from "../../../DropdownMenu/index.js";
5
- import { Kbd } from "../../../Kbd/Kbd.js";
6
- import { KbdGroup } from "../../../Kbd/KbdGroup.js";
4
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup } from "../../../DropdownMenu/index.js";
7
5
  import { filterAndSort } from "../../lib/index.js";
8
6
  import { MenuEmptyState } from "../MenuEmptyState.js";
7
+ import { useValueMenuDisplayValues } from "./useValueMenuDisplayValues.js";
9
8
  import { useValueMenuState } from "./useValueMenuState.js";
9
+ import { ValueMenuFooter } from "./ValueMenuFooter.js";
10
10
  import { ValueMenuItem } from "./ValueMenuItem.js";
11
- const FilterInputValueMenu = ({ values, onSelect, onCommit, open = false, onOpenChange, onEscape, multiSelect = false, initialValues = [], highlightValue, width = 'standard', positioning, onBuildingValueChange, inputRef, filterText = '', menuRef, blurCommitRef, className })=>{
11
+ const FilterInputValueMenu = ({ values, onSelect, onCommit, open = false, onOpenChange, onEscape, multiSelect = false, initialValues = [], highlightValue, width = 'standard', positioning, onBuildingValueChange, onItemToggle, inputRef, filterText = '', menuRef, blurCommitRef, className })=>{
12
12
  const filteredValues = useMemo(()=>filterAndSort(values, filterText, (v)=>[
13
13
  v.label,
14
14
  String(v.value)
@@ -27,26 +27,18 @@ const FilterInputValueMenu = ({ values, onSelect, onCommit, open = false, onOpen
27
27
  onEscape,
28
28
  onOpenChange,
29
29
  onBuildingValueChange,
30
+ onItemToggle,
30
31
  inputRef,
31
32
  menuRef,
32
33
  blurCommitRef
33
34
  });
34
- const displayValues = useMemo(()=>{
35
- if (!multiSelect) return filteredValues;
36
- const checkedSet = new Set(checkedValues.map(String));
37
- if (0 === checkedSet.size) return filteredValues;
38
- const checkedItems = values.filter((v)=>checkedSet.has(String(v.value)));
39
- const uncheckedFiltered = filteredValues.filter((v)=>!checkedSet.has(String(v.value)));
40
- return [
41
- ...checkedItems,
42
- ...uncheckedFiltered
43
- ];
44
- }, [
45
- filteredValues,
35
+ const displayValues = useValueMenuDisplayValues({
46
36
  values,
37
+ filteredValues,
47
38
  multiSelect,
48
- checkedValues
49
- ]);
39
+ checkedValues,
40
+ highlightValue
41
+ });
50
42
  const widthClass = 'compact' === width ? 'w-[172px]' : 'w-[300px]';
51
43
  const widthStyle = 'number' == typeof width ? {
52
44
  width: `${width}px`
@@ -76,71 +68,8 @@ const FilterInputValueMenu = ({ values, onSelect, onCommit, open = false, onOpen
76
68
  })
77
69
  }, String(option.value)))
78
70
  }) : /*#__PURE__*/ jsx(MenuEmptyState, {}),
79
- /*#__PURE__*/ jsx(DropdownMenuFooter, {
80
- children: multiSelect ? /*#__PURE__*/ jsxs(Fragment, {
81
- children: [
82
- /*#__PURE__*/ jsxs("span", {
83
- className: "flex items-center gap-4",
84
- children: [
85
- /*#__PURE__*/ jsx(KbdGroup, {
86
- children: /*#__PURE__*/ jsx(Kbd, {
87
- children: "↵"
88
- })
89
- }),
90
- "to select"
91
- ]
92
- }),
93
- /*#__PURE__*/ jsxs("span", {
94
- className: "flex items-center gap-4",
95
- children: [
96
- /*#__PURE__*/ jsxs(KbdGroup, {
97
- children: [
98
- /*#__PURE__*/ jsx(Kbd, {
99
- children: "⌘"
100
- }),
101
- /*#__PURE__*/ jsx(Kbd, {
102
- children: "↑"
103
- }),
104
- /*#__PURE__*/ jsx(Kbd, {
105
- children: "↓"
106
- })
107
- ]
108
- }),
109
- "to multi-select"
110
- ]
111
- })
112
- ]
113
- }) : /*#__PURE__*/ jsxs(Fragment, {
114
- children: [
115
- /*#__PURE__*/ jsxs("span", {
116
- className: "flex items-center gap-4",
117
- children: [
118
- /*#__PURE__*/ jsxs(KbdGroup, {
119
- children: [
120
- /*#__PURE__*/ jsx(Kbd, {
121
- children: "↑"
122
- }),
123
- /*#__PURE__*/ jsx(Kbd, {
124
- children: "↓"
125
- })
126
- ]
127
- }),
128
- "to navigate"
129
- ]
130
- }),
131
- /*#__PURE__*/ jsxs("span", {
132
- className: "flex items-center gap-4",
133
- children: [
134
- /*#__PURE__*/ jsx(KbdGroup, {
135
- children: /*#__PURE__*/ jsx(Kbd, {
136
- children: "↵"
137
- })
138
- }),
139
- "to select"
140
- ]
141
- })
142
- ]
143
- })
71
+ /*#__PURE__*/ jsx(ValueMenuFooter, {
72
+ multiSelect: multiSelect
144
73
  })
145
74
  ]
146
75
  })
@@ -0,0 +1,6 @@
1
+ import type { FC } from 'react';
2
+ interface ValueMenuFooterProps {
3
+ multiSelect: boolean;
4
+ }
5
+ export declare const ValueMenuFooter: FC<ValueMenuFooterProps>;
6
+ export {};
@@ -0,0 +1,72 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { DropdownMenuFooter } from "../../../DropdownMenu/index.js";
3
+ import { Kbd } from "../../../Kbd/Kbd.js";
4
+ import { KbdGroup } from "../../../Kbd/KbdGroup.js";
5
+ const ValueMenuFooter = ({ multiSelect })=>/*#__PURE__*/ jsx(DropdownMenuFooter, {
6
+ children: multiSelect ? /*#__PURE__*/ jsxs(Fragment, {
7
+ children: [
8
+ /*#__PURE__*/ jsxs("span", {
9
+ className: "flex items-center gap-4",
10
+ children: [
11
+ /*#__PURE__*/ jsx(KbdGroup, {
12
+ children: /*#__PURE__*/ jsx(Kbd, {
13
+ children: "↵"
14
+ })
15
+ }),
16
+ "to select"
17
+ ]
18
+ }),
19
+ /*#__PURE__*/ jsxs("span", {
20
+ className: "flex items-center gap-4",
21
+ children: [
22
+ /*#__PURE__*/ jsxs(KbdGroup, {
23
+ children: [
24
+ /*#__PURE__*/ jsx(Kbd, {
25
+ children: "⌘"
26
+ }),
27
+ /*#__PURE__*/ jsx(Kbd, {
28
+ children: "↑"
29
+ }),
30
+ /*#__PURE__*/ jsx(Kbd, {
31
+ children: "↓"
32
+ })
33
+ ]
34
+ }),
35
+ "to multi-select"
36
+ ]
37
+ })
38
+ ]
39
+ }) : /*#__PURE__*/ jsxs(Fragment, {
40
+ children: [
41
+ /*#__PURE__*/ jsxs("span", {
42
+ className: "flex items-center gap-4",
43
+ children: [
44
+ /*#__PURE__*/ jsxs(KbdGroup, {
45
+ children: [
46
+ /*#__PURE__*/ jsx(Kbd, {
47
+ children: "↑"
48
+ }),
49
+ /*#__PURE__*/ jsx(Kbd, {
50
+ children: "↓"
51
+ })
52
+ ]
53
+ }),
54
+ "to navigate"
55
+ ]
56
+ }),
57
+ /*#__PURE__*/ jsxs("span", {
58
+ className: "flex items-center gap-4",
59
+ children: [
60
+ /*#__PURE__*/ jsx(KbdGroup, {
61
+ children: /*#__PURE__*/ jsx(Kbd, {
62
+ children: "↵"
63
+ })
64
+ }),
65
+ "to select"
66
+ ]
67
+ })
68
+ ]
69
+ })
70
+ });
71
+ ValueMenuFooter.displayName = 'ValueMenuFooter';
72
+ export { ValueMenuFooter };
@@ -1,5 +1,6 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { ChevronRight } from "../../../../icons/ChevronRight.js";
3
+ import { Badge } from "../../../Badge/index.js";
3
4
  import { Checkmark } from "../../../Checkmark/index.js";
4
5
  import { DropdownMenuItem } from "../../../DropdownMenu/index.js";
5
6
  import { Text } from "../../../Text/index.js";
@@ -8,15 +9,11 @@ const ValueMenuItem = ({ option, isChecked, isPending, multiSelect, onSelect })=
8
9
  onSelect: onSelect,
9
10
  className: isPending ? 'bg-states-primary-hover' : void 0,
10
11
  children: [
11
- option.badge ? /*#__PURE__*/ jsx("div", {
12
- className: "flex items-center gap-4 px-6 py-2 rounded-4 text-xs font-medium max-w-[320px] min-h-[20px] overflow-clip",
13
- style: {
14
- backgroundColor: option.badge.color
15
- },
16
- children: /*#__PURE__*/ jsx("span", {
17
- className: "min-w-0 truncate leading-4",
18
- children: option.badge.text
19
- })
12
+ option.badge ? /*#__PURE__*/ jsx(Badge, {
13
+ color: option.badge.color,
14
+ type: "secondary",
15
+ variant: "default",
16
+ children: option.badge.text
20
17
  }) : /*#__PURE__*/ jsx("div", {
21
18
  className: "min-w-0",
22
19
  children: /*#__PURE__*/ jsx(Text, {
@@ -0,0 +1,31 @@
1
+ import type { ValueOption } from './FilterInputValueMenu';
2
+ type ConditionValue = string | number | boolean;
3
+ interface UseValueMenuDisplayValuesOptions {
4
+ /** Raw option list coming from the parent (e.g. `getSuggestions` output). */
5
+ values: ValueOption[];
6
+ /** Filtered / sorted view of `values` used for normal rendering. */
7
+ filteredValues: ValueOption[];
8
+ multiSelect: boolean;
9
+ /** Currently checked values when `multiSelect` is true. */
10
+ checkedValues: ConditionValue[];
11
+ /** Currently highlighted value when `multiSelect` is false. */
12
+ highlightValue?: ConditionValue;
13
+ }
14
+ /**
15
+ * Compose the final dropdown list shown to the user.
16
+ *
17
+ * The parent helper (`values`) is free to change shape between renders — for
18
+ * instance a dynamic `getSuggestions` may narrow its result after a selection
19
+ * is made. This hook keeps the user's currently-selected entries pinned at
20
+ * the top of the list with a stable presentation:
21
+ *
22
+ * 1. Every option the menu has ever rendered is remembered in a `Map` keyed
23
+ * by value. When a selected entry is no longer in the current `values`,
24
+ * the remembered option (with its original label/badge) is used.
25
+ * 2. If nothing has been seen for that value either, a plain-text option is
26
+ * fabricated so the user can still see and toggle it.
27
+ * 3. Unchecked items come from `filteredValues` (already filter-sorted) so
28
+ * the search query still applies to them.
29
+ */
30
+ export declare const useValueMenuDisplayValues: ({ values, filteredValues, multiSelect, checkedValues, highlightValue, }: UseValueMenuDisplayValuesOptions) => ValueOption[];
31
+ export {};
@@ -0,0 +1,39 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+ const useValueMenuDisplayValues = ({ values, filteredValues, multiSelect, checkedValues, highlightValue })=>{
3
+ const optionMemoryRef = useRef(new Map());
4
+ useEffect(()=>{
5
+ for (const opt of values)optionMemoryRef.current.set(String(opt.value), opt);
6
+ }, [
7
+ values
8
+ ]);
9
+ return useMemo(()=>{
10
+ const selectedList = multiSelect ? checkedValues : null != highlightValue ? [
11
+ highlightValue
12
+ ] : [];
13
+ if (0 === selectedList.length) return filteredValues;
14
+ const selectedSet = new Set(selectedList.map(String));
15
+ const selectedItems = selectedList.map((v)=>{
16
+ const key = String(v);
17
+ const match = values.find((opt)=>String(opt.value) === key);
18
+ if (match) return match;
19
+ const remembered = optionMemoryRef.current.get(key);
20
+ if (remembered) return remembered;
21
+ return {
22
+ value: v,
23
+ label: key
24
+ };
25
+ });
26
+ const restFiltered = filteredValues.filter((v)=>!selectedSet.has(String(v.value)));
27
+ return [
28
+ ...selectedItems,
29
+ ...restFiltered
30
+ ];
31
+ }, [
32
+ filteredValues,
33
+ values,
34
+ multiSelect,
35
+ checkedValues,
36
+ highlightValue
37
+ ]);
38
+ };
39
+ export { useValueMenuDisplayValues };
@@ -13,12 +13,17 @@ interface UseValueMenuStateOptions {
13
13
  onEscape?: () => void;
14
14
  onOpenChange?: (open: boolean) => void;
15
15
  onBuildingValueChange?: (preview: string | undefined) => void;
16
+ /** Fires whenever the user explicitly toggles an item in multi-select mode
17
+ * (click or keyboard). Distinct from onBuildingValueChange, which also fires
18
+ * on mount with the initial preview — use this when you need to react only
19
+ * to actual user-initiated toggles. */
20
+ onItemToggle?: () => void;
16
21
  inputRef?: RefObject<HTMLInputElement | null>;
17
22
  menuRef?: RefObject<HTMLDivElement | null>;
18
23
  /** Ref to register blur commit function — called by blur handler before state reset. Returns true if committed. */
19
24
  blurCommitRef?: RefObject<(() => boolean) | null>;
20
25
  }
21
- export declare const useValueMenuState: ({ values, open, multiSelect, initialValues, highlightValue, onSelect, onCommit, onEscape, onOpenChange, onBuildingValueChange, inputRef, menuRef, blurCommitRef, }: UseValueMenuStateOptions) => {
26
+ export declare const useValueMenuState: ({ values, open, multiSelect, initialValues, highlightValue, onSelect, onCommit, onEscape, onOpenChange, onBuildingValueChange, onItemToggle, inputRef, menuRef, blurCommitRef, }: UseValueMenuStateOptions) => {
22
27
  checkedValues: ConditionValue[];
23
28
  selectedValues: ConditionValue[];
24
29
  highlightedValue: string;
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { useKeyboardNav } from "../hooks/useKeyboardNav.js";
3
- const useValueMenuState = ({ values, open, multiSelect, initialValues, highlightValue, onSelect, onCommit, onEscape, onOpenChange, onBuildingValueChange, inputRef, menuRef, blurCommitRef })=>{
3
+ const useValueMenuState = ({ values, open, multiSelect, initialValues, highlightValue, onSelect, onCommit, onEscape, onOpenChange, onBuildingValueChange, onItemToggle, inputRef, menuRef, blurCommitRef })=>{
4
4
  const [checkedValues, setCheckedValues] = useState(initialValues);
5
5
  const checkedValuesRef = useRef(checkedValues);
6
6
  checkedValuesRef.current = checkedValues;
@@ -59,8 +59,10 @@ const useValueMenuState = ({ values, open, multiSelect, initialValues, highlight
59
59
  values
60
60
  ]);
61
61
  const handleItemSelect = (item)=>{
62
- if (multiSelect) toggleValue(item.value);
63
- else onSelect(item.value);
62
+ if (multiSelect) {
63
+ toggleValue(item.value);
64
+ onItemToggle?.();
65
+ } else onSelect(item.value);
64
66
  };
65
67
  const handleClose = ()=>{
66
68
  if (multiSelect) commitChecked();
@@ -24,6 +24,7 @@ export declare const useChipEditing: ({ conditions, chips, fields, containerRef,
24
24
  segmentFilterText: string;
25
25
  segmentMenuFilterText: string;
26
26
  setSegmentFilterText: (text: string) => void;
27
+ resetSegmentTyping: () => void;
27
28
  handleChipClick: (chipId: string, segment: ChipSegment, anchorRect: DOMRect) => void;
28
29
  clearEditing: () => void;
29
30
  cancelSegmentEdit: () => void;
@@ -82,6 +82,9 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
82
82
  setSegmentFilterText(text);
83
83
  setUserHasTyped(true);
84
84
  }, []);
85
+ const resetSegmentTyping = useCallback(()=>{
86
+ setUserHasTyped(false);
87
+ }, []);
85
88
  const segmentDisplayText = segmentFilterText;
86
89
  const segmentMenuFilterText = userHasTyped ? segmentFilterText : '';
87
90
  return useMemo(()=>({
@@ -91,6 +94,7 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
91
94
  segmentFilterText: segmentDisplayText,
92
95
  segmentMenuFilterText,
93
96
  setSegmentFilterText: handleSegmentFilterChange,
97
+ resetSegmentTyping,
94
98
  handleChipClick,
95
99
  clearEditing,
96
100
  cancelSegmentEdit
@@ -100,6 +104,7 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
100
104
  segmentDisplayText,
101
105
  segmentMenuFilterText,
102
106
  handleSegmentFilterChange,
107
+ resetSegmentTyping,
103
108
  handleChipClick,
104
109
  clearEditing,
105
110
  cancelSegmentEdit
@@ -44,6 +44,7 @@ export declare const useFilterInputAutocomplete: ({ fields, conditions, chips, u
44
44
  handleValueSelect: (val: string | number | boolean) => void;
45
45
  handleMultiCommit: (values: Array<string | number | boolean>) => void;
46
46
  handleBuildingValueChange: (preview: string | undefined) => void;
47
+ handleMultiSelectToggle: () => void;
47
48
  handleMenuClose: () => void;
48
49
  handleMenuDiscard: (continueBuilding?: boolean) => void;
49
50
  handleChipClick: (chipId: string, segment: import("../../FilterInputField").ChipSegment, anchorRect: DOMRect) => void;
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useRef, useState } from "react";
2
2
  import { flushSync } from "react-dom";
3
3
  import { useDateRange } from "../../FilterInputMenu/FilterInputDateValueMenu/hooks.js";
4
- import { chipIdToConditionIndex } from "../../lib/index.js";
4
+ import { applyAcceptChar, chipIdToConditionIndex } from "../../lib/index.js";
5
5
  import { deriveAutocompleteValues } from "./deriveAutocompleteValues.js";
6
6
  import { useChipEditing } from "./useChipEditing.js";
7
7
  import { useFocusManagement } from "./useFocusManagement.js";
@@ -75,7 +75,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
75
75
  const inputTextRef = useRef(inputText);
76
76
  inputTextRef.current = inputText;
77
77
  const commitBuildingOnBlurRef = useRef(()=>false);
78
- const { handleMenuClose, handleFieldSelect, handleOperatorSelect, handleValueSelect, handleMultiCommit, handleBuildingValueChange, handleRangeSelect, handleCustomValueCommit, handleCustomAttributeCommit } = useMenuFlow({
78
+ const { handleMenuClose, handleFieldSelect, handleOperatorSelect, handleValueSelect, handleMultiCommit, handleBuildingValueChange, handleMultiSelectToggle, handleRangeSelect, handleCustomValueCommit, handleCustomAttributeCommit } = useMenuFlow({
79
79
  editing,
80
80
  selectedField,
81
81
  selectedOperator,
@@ -209,6 +209,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
209
209
  handleValueSelect,
210
210
  handleMultiCommit,
211
211
  handleBuildingValueChange,
212
+ handleMultiSelectToggle,
212
213
  handleMenuClose,
213
214
  handleMenuDiscard: resetState,
214
215
  handleChipClick: editing.handleChipClick,
@@ -227,7 +228,11 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
227
228
  editingSegment: editing.editingSegment,
228
229
  segmentFilterText: editing.segmentFilterText,
229
230
  segmentMenuFilterText: editing.segmentMenuFilterText,
230
- handleSegmentFilterChange: editing.setSegmentFilterText,
231
+ handleSegmentFilterChange: (text)=>{
232
+ const accept = selectedField?.acceptChar;
233
+ const next = 'value' === editing.editingSegment && accept ? applyAcceptChar(text, accept) : text;
234
+ editing.setSegmentFilterText(next);
235
+ },
231
236
  cancelSegmentEdit: editing.cancelSegmentEdit,
232
237
  handleCustomValueCommit,
233
238
  handleCustomAttributeCommit,
@@ -1,13 +1,18 @@
1
1
  import { useCallback, useRef } from "react";
2
- import { OPERATOR_SYMBOLS, getOperatorFromLabel, hasFieldValues } from "../../lib/index.js";
2
+ import { OPERATOR_SYMBOLS, applyAcceptChar, getOperatorFromLabel, hasFieldValues } from "../../lib/index.js";
3
3
  const useInputHandlers = ({ inputText, menuState, selectedField, isFocused, fields, inputRef, conditionsRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuOffset, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit })=>{
4
4
  const menuRef = useRef(null);
5
5
  const handleInputChange = useCallback((e)=>{
6
- const text = e.target.value;
6
+ let text = e.target.value;
7
+ if ('value' === menuState && selectedField?.acceptChar) {
8
+ text = applyAcceptChar(text, selectedField.acceptChar);
9
+ if (text !== e.target.value) e.target.value = text;
10
+ }
7
11
  setInputText(text);
8
12
  if (text && !selectedField) setMenuState('field');
9
13
  else if (!text && !selectedField) setMenuState(isFocused && 0 === conditionsLengthRef.current ? 'field' : 'closed');
10
14
  }, [
15
+ menuState,
11
16
  selectedField,
12
17
  isFocused,
13
18
  setInputText,
@@ -7,6 +7,7 @@ interface MenuFlowDeps {
7
7
  editingSegment: string | null;
8
8
  setEditingSegment: (segment: ChipSegment | null) => void;
9
9
  setSegmentFilterText: (text: string) => void;
10
+ resetSegmentTyping: () => void;
10
11
  };
11
12
  selectedField: FieldMetadata | null;
12
13
  selectedOperator: FilterOperator | null;
@@ -34,6 +35,7 @@ export declare const useMenuFlow: ({ editing, selectedField, selectedOperator, f
34
35
  handleValueSelect: (val: string | number | boolean) => void;
35
36
  handleMultiCommit: (values: Array<string | number | boolean>) => void;
36
37
  handleBuildingValueChange: (preview: string | undefined) => void;
38
+ handleMultiSelectToggle: () => void;
37
39
  handleRangeSelect: (from: string, to: string) => void;
38
40
  handleCustomValueCommit: (customText: string) => void;
39
41
  handleCustomAttributeCommit: (customText: string) => void;
@@ -109,6 +109,13 @@ const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRe
109
109
  }, [
110
110
  setBuildingMultiValue
111
111
  ]);
112
+ const handleMultiSelectToggle = useCallback(()=>{
113
+ if ('value' === editing.editingSegment) editing.resetSegmentTyping();
114
+ else setInputText('');
115
+ }, [
116
+ editing,
117
+ setInputText
118
+ ]);
112
119
  const handleRangeSelect = useCallback((from, to)=>{
113
120
  if (!selectedField || !selectedOperator) return;
114
121
  const isEditing = !!editing.editingChipId;
@@ -184,6 +191,7 @@ const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRe
184
191
  handleValueSelect,
185
192
  handleMultiCommit,
186
193
  handleBuildingValueChange,
194
+ handleMultiSelectToggle,
187
195
  handleRangeSelect,
188
196
  handleCustomValueCommit,
189
197
  handleCustomAttributeCommit
@@ -3,6 +3,10 @@ import { chipIdToConditionIndex, getFieldValues, hasStaticAllowlist, isDatePrese
3
3
  const findMatchingFieldValue = (fieldValues, text)=>fieldValues.find((v)=>v.label.toLowerCase() === text.toLowerCase() || String(v.value).toLowerCase() === text.toLowerCase());
4
4
  const isValidFieldValue = (fieldValues, v)=>fieldValues.some((opt)=>opt.value === v || String(opt.value).toLowerCase() === String(v).toLowerCase());
5
5
  const getInvalidValueIndices = (field, values)=>{
6
+ if (field.validate) return values.reduce((acc, v, idx)=>{
7
+ if (field.validate(v)) acc.push(idx);
8
+ return acc;
9
+ }, []);
6
10
  if (!hasStaticAllowlist(field)) return [];
7
11
  const fv = getFieldValues(field);
8
12
  if (0 === fv.length) return [];
@@ -25,14 +29,21 @@ const resolveFieldValue = (field, text)=>{
25
29
  const resolveSingleValue = (field, trimmed)=>{
26
30
  const fv = getFieldValues(field);
27
31
  const match = findMatchingFieldValue(fv, trimmed);
32
+ const raw = match ? match.value : trimmed;
33
+ const resolved = field.normalize ? field.normalize(raw) : raw;
34
+ if (field.validate) return {
35
+ resolved,
36
+ error: field.validate(resolved) ? true : void 0
37
+ };
28
38
  return {
29
- resolved: match ? match.value : trimmed,
39
+ resolved,
30
40
  error: hasStaticAllowlist(field) && fv.length > 0 && !match ? true : void 0
31
41
  };
32
42
  };
33
43
  const resolveMultiValues = (field, trimmed)=>{
34
44
  const parts = trimmed.split(',').map((s)=>s.trim()).filter(Boolean);
35
- const resolved = parts.map((part)=>resolveFieldValue(field, part));
45
+ const raw = parts.map((part)=>resolveFieldValue(field, part));
46
+ const resolved = field.normalize ? raw.map((v)=>field.normalize(v)) : raw;
36
47
  const error = getInvalidValueIndices(field, resolved).length > 0 ? true : void 0;
37
48
  return {
38
49
  resolved,
@@ -1,5 +1,5 @@
1
1
  export { FilterInput, type FilterInputProps } from './FilterInput';
2
2
  export { FilterInputChip, type FilterInputChipProps } from './FilterInputField';
3
3
  export { FilterInputFieldMenu, type FilterInputFieldMenuProps, FilterInputOperatorMenu, type FilterInputOperatorMenuProps, FilterInputValueMenu, type FilterInputValueMenuProps, type ValueOption, } from './FilterInputMenu';
4
- export { createStatusCodeSuggestions, type FilterParseError, isFilterParseError, parseExpression, type StatusCodeSuggestionsOptions, serializeExpression, } from './lib';
4
+ export { createStatusCodeInputFilter, createStatusCodeNormalizer, createStatusCodeSuggestions, createStatusCodeValidator, type FilterParseError, isFilterParseError, parseExpression, type StatusCodeSuggestionsOptions, serializeExpression, } from './lib';
5
5
  export type { Condition, ExprNode, FieldMetadata, FieldType, FieldValueOption, FilterInputChipData, FilterInputChipVariant, FilterOperator, Group, } from './types';