@wallarm-org/design-system 0.70.0 → 0.71.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 (31) hide show
  1. package/dist/components/FilterInput/FilterInputErrors/parseFilterInputErrors.js +7 -5
  2. package/dist/components/FilterInput/FilterInputField/FilterInputChip/ChipSearchInput.js +2 -1
  3. package/dist/components/FilterInput/FilterInputField/FilterInputChip/FilterInputChip.js +12 -9
  4. package/dist/components/FilterInput/FilterInputField/FilterInputChip/Segment.js +2 -2
  5. package/dist/components/FilterInput/FilterInputField/FilterInputChip/classes.d.ts +8 -1
  6. package/dist/components/FilterInput/FilterInputField/FilterInputChip/classes.js +4 -2
  7. package/dist/components/FilterInput/FilterInputField/FilterInputChip/constants.d.ts +3 -0
  8. package/dist/components/FilterInput/FilterInputField/FilterInputChip/constants.js +2 -1
  9. package/dist/components/FilterInput/FilterInputField/FilterInputChip/model/useSizerWidth.d.ts +5 -2
  10. package/dist/components/FilterInput/FilterInputField/FilterInputChip/model/useSizerWidth.js +5 -4
  11. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/FilterInputValueMenu.d.ts +2 -0
  12. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/FilterInputValueMenu.js +2 -4
  13. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/ValueMenuItem.js +14 -6
  14. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/valueOptionSearchText.d.ts +7 -0
  15. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/valueOptionSearchText.js +9 -0
  16. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useBlurCommit.d.ts +8 -1
  17. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useBlurCommit.js +40 -3
  18. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipEditing.js +7 -6
  19. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.d.ts +5 -4
  20. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.js +38 -2
  21. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useResumeBuilding.d.ts +32 -0
  22. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useResumeBuilding.js +75 -0
  23. package/dist/components/FilterInput/hooks/useFilterInputExpression/buildChips.js +17 -5
  24. package/dist/components/FilterInput/hooks/useFilterInputExpression/useFilterInputExpression.js +5 -0
  25. package/dist/components/FilterInput/lib/conditions.d.ts +9 -1
  26. package/dist/components/FilterInput/lib/conditions.js +5 -1
  27. package/dist/components/FilterInput/lib/index.d.ts +1 -1
  28. package/dist/components/FilterInput/lib/index.js +2 -2
  29. package/dist/components/FilterInput/types.d.ts +8 -0
  30. package/dist/metadata/components.json +2 -2
  31. package/package.json +1 -1
@@ -1,11 +1,10 @@
1
1
  import { SEGMENT_VARIANT } from "../FilterInputField/FilterInputChip/index.js";
2
- import { getFieldValues, hasStaticAllowlist, isNoValueOperator, isValidFieldValue } from "../lib/index.js";
2
+ import { getFieldValues, hasStaticAllowlist, incompleteTripletError, isEmptyFilterValue, isNoValueOperator, isValidFieldValue } from "../lib/index.js";
3
3
  const isPairValueMissing = (condition)=>{
4
4
  const pair = condition.pair;
5
5
  if (!pair) return true;
6
6
  if (pair.operator && isNoValueOperator(pair.operator)) return false;
7
- const value = pair.value;
8
- return null == value || '' === value || Array.isArray(value) && 0 === value.length;
7
+ return isEmptyFilterValue(pair.value);
9
8
  };
10
9
  const parseFilterInputErrors = (conditions, fields)=>{
11
10
  const errors = [];
@@ -13,9 +12,12 @@ const parseFilterInputErrors = (conditions, fields)=>{
13
12
  const field = fields.find((f)=>f.name === condition.field);
14
13
  const baseIsNoValue = null != condition.operator && isNoValueOperator(condition.operator);
15
14
  if (field?.pairedField && !baseIsNoValue && isPairValueMissing(condition)) errors.push(`${field.pairedField.label} is required`);
16
- if (!condition.error) continue;
17
15
  const label = field?.label || condition.field;
18
- switch(condition.error){
16
+ if (field && !field.pairedField && !condition.error && incompleteTripletError(condition.operator, condition.value)) {
17
+ errors.push(`${label} is incomplete`);
18
+ continue;
19
+ }
20
+ if (condition.error) switch(condition.error){
19
21
  case SEGMENT_VARIANT.attribute:
20
22
  errors.push(`Unknown field ${condition.field}`);
21
23
  break;
@@ -20,6 +20,7 @@ const ChipSearchInput = ()=>{
20
20
  ref: inputRef,
21
21
  type: "text",
22
22
  role: "combobox",
23
+ size: 1,
23
24
  "aria-expanded": menuOpen,
24
25
  "aria-invalid": error,
25
26
  "aria-label": "Filter value",
@@ -31,7 +32,7 @@ const ChipSearchInput = ()=>{
31
32
  style: {
32
33
  width: `${inputWidth}px`
33
34
  },
34
- className: "h-22 border-none bg-transparent p-0 text-sm shadow-none outline-none ring-0"
35
+ className: "h-22 min-w-0 border-none bg-transparent p-0 text-sm shadow-none outline-none ring-0"
35
36
  }),
36
37
  /*#__PURE__*/ jsx("span", {
37
38
  ref: sizerRef,
@@ -2,7 +2,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useRef } from "react";
3
3
  import { cn } from "../../../../utils/cn.js";
4
4
  import { ChipSearchInput } from "./ChipSearchInput.js";
5
- import { chipVariants } from "./classes.js";
5
+ import { chipVariants, emptyValueHitTarget } from "./classes.js";
6
6
  import { useEditingContext } from "./context/EditingContext.js";
7
7
  import { FilterInputRemoveButton } from "./FilterInputRemoveButton.js";
8
8
  import { PairSeparator } from "./PairSeparator.js";
@@ -10,11 +10,13 @@ import { Segment } from "./Segment.js";
10
10
  import { SEGMENT_VARIANT } from "./segmentVariant.js";
11
11
  const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = false, valueParts, valueSeparator, errorValueIndices, building = false, disabled = false, pair, onRemove, onSegmentClick, onPairSegmentClick, className, ...props })=>{
12
12
  const interactive = !disabled;
13
- const hasError = !!error;
14
13
  const internalRef = useRef(null);
15
14
  const editing = useEditingContext();
16
15
  const isEditingThisChip = null != editing && null != editing.editingSegment && (building ? null == editing.editingChipId : null != chipId && editing.editingChipId === chipId);
17
16
  const activeSegment = isEditingThisChip ? editing.editingSegment : null;
17
+ const effectiveError = isEditingThisChip ? false : error;
18
+ const effectivePairError = isEditingThisChip ? void 0 : pair?.error;
19
+ const hasError = !!effectiveError || !!effectivePairError;
18
20
  const handleSegmentClick = useCallback((segment, e)=>{
19
21
  if (!onSegmentClick) return;
20
22
  if (activeSegment === segment) return;
@@ -62,7 +64,7 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
62
64
  interactive,
63
65
  disabled,
64
66
  building
65
- }), 'max-w-[320px]', className),
67
+ }), pair ? 'max-w-[380px]' : 'max-w-[320px]', className),
66
68
  "data-slot": "filter-input-condition-chip",
67
69
  ...building && {
68
70
  'data-building': ''
@@ -72,7 +74,7 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
72
74
  /*#__PURE__*/ jsx(Segment, {
73
75
  variant: SEGMENT_VARIANT.attribute,
74
76
  className: "shrink-0",
75
- error: true === error || error === SEGMENT_VARIANT.attribute,
77
+ error: true === effectiveError || effectiveError === SEGMENT_VARIANT.attribute,
76
78
  onClick: interactive ? (e)=>handleSegmentClick(SEGMENT_VARIANT.attribute, e) : void 0,
77
79
  onMouseDown: interactive && building ? handleSegmentMouseDown : void 0,
78
80
  ...segmentEditProps(SEGMENT_VARIANT.attribute),
@@ -86,10 +88,10 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
86
88
  ...segmentEditProps(SEGMENT_VARIANT.operator),
87
89
  children: operator ?? ''
88
90
  }),
89
- (value || baseActiveSegment === SEGMENT_VARIANT.value) && /*#__PURE__*/ jsx(Segment, {
91
+ (value || baseActiveSegment === SEGMENT_VARIANT.value || !pair && effectiveError === SEGMENT_VARIANT.value) && /*#__PURE__*/ jsx(Segment, {
90
92
  variant: SEGMENT_VARIANT.value,
91
- className: "min-w-0",
92
- error: baseActiveSegment !== SEGMENT_VARIANT.value && (true === error || error === SEGMENT_VARIANT.value),
93
+ className: value || pair ? pair ? 'max-w-[90px] shrink-0' : 'min-w-0' : emptyValueHitTarget,
94
+ error: baseActiveSegment !== SEGMENT_VARIANT.value && (true === effectiveError || effectiveError === SEGMENT_VARIANT.value),
93
95
  valueParts: valueParts,
94
96
  valueSeparator: valueSeparator,
95
97
  errorValueIndices: errorValueIndices,
@@ -104,6 +106,7 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
104
106
  /*#__PURE__*/ jsx(Segment, {
105
107
  variant: SEGMENT_VARIANT.attribute,
106
108
  className: "shrink-0",
109
+ onClick: interactive && pair.error ? (e)=>handlePairSegmentClick(pair.operator ? SEGMENT_VARIANT.value : SEGMENT_VARIANT.operator, e) : void 0,
107
110
  children: pair.attribute
108
111
  }),
109
112
  (pair.operator || pairActiveSegment === SEGMENT_VARIANT.operator) && /*#__PURE__*/ jsx(Segment, {
@@ -115,8 +118,8 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
115
118
  }),
116
119
  (null != pair.value || pairActiveSegment === SEGMENT_VARIANT.value) && /*#__PURE__*/ jsx(Segment, {
117
120
  variant: SEGMENT_VARIANT.value,
118
- className: "min-w-0",
119
- error: pairActiveSegment !== SEGMENT_VARIANT.value && (true === pair.error || pair.error === SEGMENT_VARIANT.value),
121
+ className: pair.value ? 'min-w-0' : emptyValueHitTarget,
122
+ error: pairActiveSegment !== SEGMENT_VARIANT.value && (true === effectivePairError || effectivePairError === SEGMENT_VARIANT.value),
120
123
  onClick: interactive ? (e)=>handlePairSegmentClick('value', e) : void 0,
121
124
  ...segmentEditProps(SEGMENT_VARIANT.value, 1),
122
125
  children: pair.value ?? ''
@@ -2,7 +2,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useContext, useEffect, useRef } from "react";
3
3
  import { cn } from "../../../../utils/cn.js";
4
4
  import { FilterInputContext } from "../../FilterInputContext/FilterInputContext.js";
5
- import { segmentContainer, segmentTextVariants } from "./classes.js";
5
+ import { editingValueMinWidth, segmentContainer, segmentTextVariants } from "./classes.js";
6
6
  import { CHAR_WIDTH_PX } from "./constants.js";
7
7
  import { MultiValueSegment } from "./MultiValueSegment.js";
8
8
  import { useSizerWidth } from "./model/useSizerWidth.js";
@@ -78,7 +78,7 @@ const Segment = ({ variant, children, className, error, editing, editText, onEdi
78
78
  className: cn(segmentTextVariants({
79
79
  variant,
80
80
  error
81
- }), 'bg-transparent outline-none p-0 m-0'),
81
+ }), 'bg-transparent outline-none p-0 m-0', variant === SEGMENT_VARIANT.value && editingValueMinWidth),
82
82
  style: {
83
83
  width: `${inputWidth}px`
84
84
  }
@@ -7,12 +7,19 @@ export declare const chipVariants: (props?: ({
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  /** Segment container */
9
9
  export declare const segmentContainer = "flex flex-col justify-center overflow-hidden leading-none";
10
+ /** Hit target for an empty required value: a small full-height box so the value
11
+ * is clickable and the trailing × can't sit on top of it (AS-1192). */
12
+ export declare const emptyValueHitTarget = "min-w-[4px] self-stretch";
13
+ /** Min width for the value edit input so an empty freeform value stays clickable. */
14
+ export declare const editingValueMinWidth = "min-w-[3.5rem]";
10
15
  /** Segment text styles by variant */
11
16
  export declare const segmentTextVariants: (props?: ({
12
17
  variant?: "value" | "operator" | "attribute" | null | undefined;
13
18
  error?: boolean | null | undefined;
14
19
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
15
- /** Remove button styles — hidden by default, shown on chip hover or button focus */
20
+ /** Remove button styles — hidden by default, shown on chip hover or button focus.
21
+ * `pointer-events-none` while invisible stops it swallowing clicks meant for the
22
+ * chip/input and silently deleting the chip (AS-1179). */
16
23
  export declare const removeButtonVariants: (props?: ({
17
24
  error?: boolean | null | undefined;
18
25
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -35,6 +35,8 @@ const chipVariants = cva(`h-22 group/chip relative flex items-center justify-cen
35
35
  }
36
36
  });
37
37
  const segmentContainer = 'flex flex-col justify-center overflow-hidden leading-none';
38
+ const emptyValueHitTarget = 'min-w-[4px] self-stretch';
39
+ const editingValueMinWidth = 'min-w-[3.5rem]';
38
40
  const segmentTextVariants = cva('truncate text-sm', {
39
41
  variants: {
40
42
  variant: {
@@ -68,7 +70,7 @@ const segmentTextVariants = cva('truncate text-sm', {
68
70
  error: false
69
71
  }
70
72
  });
71
- const removeButtonVariants = cva(`absolute -right-[13px] top-[-1px] bottom-[-1px] flex items-center justify-center p-0 cursor-pointer w-[18px] border border-solid border-l-0 rounded-r-8 opacity-0 group-hover/chip:opacity-100 focus:opacity-100 transition-opacity ${hiddenWhenSelected}`, {
73
+ const removeButtonVariants = cva(`absolute -right-[13px] top-[-1px] bottom-[-1px] flex items-center justify-center p-0 cursor-pointer w-[18px] border border-solid border-l-0 rounded-r-8 opacity-0 pointer-events-none group-hover/chip:opacity-100 group-hover/chip:pointer-events-auto focus:opacity-100 focus:pointer-events-auto transition-opacity ${hiddenWhenSelected}`, {
72
74
  variants: {
73
75
  error: {
74
76
  true: 'border-border-danger bg-bg-light-danger text-text-danger',
@@ -79,4 +81,4 @@ const removeButtonVariants = cva(`absolute -right-[13px] top-[-1px] bottom-[-1px
79
81
  error: false
80
82
  }
81
83
  });
82
- export { chipVariants, removeButtonVariants, segmentContainer, segmentTextVariants };
84
+ export { chipVariants, editingValueMinWidth, emptyValueHitTarget, removeButtonVariants, segmentContainer, segmentTextVariants };
@@ -1,5 +1,8 @@
1
1
  /** Minimum input width in px */
2
2
  export declare const MIN_INPUT_WIDTH = 4;
3
+ /** Maximum input width in px — caps the content-sized input so a long value
4
+ * can't grow it past the chip; beyond this it scrolls content natively. */
5
+ export declare const MAX_INPUT_WIDTH = 280;
3
6
  /** Extra pixels added to measured width to prevent text clipping */
4
7
  export declare const WIDTH_OFFSET = 1;
5
8
  /** Approximate width of a single character in px (text-sm fallback) */
@@ -1,4 +1,5 @@
1
1
  const MIN_INPUT_WIDTH = 4;
2
+ const MAX_INPUT_WIDTH = 280;
2
3
  const WIDTH_OFFSET = 1;
3
4
  const CHAR_WIDTH_PX = 8;
4
- export { CHAR_WIDTH_PX, MIN_INPUT_WIDTH, WIDTH_OFFSET };
5
+ export { CHAR_WIDTH_PX, MAX_INPUT_WIDTH, MIN_INPUT_WIDTH, WIDTH_OFFSET };
@@ -6,11 +6,14 @@ interface UseSizerWidthOptions {
6
6
  text: string;
7
7
  /** Minimum width floor (defaults to MIN_INPUT_WIDTH) */
8
8
  minWidth?: number;
9
+ /** Maximum width ceiling (defaults to MAX_INPUT_WIDTH) */
10
+ maxWidth?: number;
9
11
  }
10
12
  /**
11
13
  * Measures input width from a hidden sizer span.
12
14
  * Falls back to character count * CHAR_WIDTH_PX when the DOM element is unavailable.
13
- * Returns the measured width + WIDTH_OFFSET to prevent text clipping.
15
+ * Returns the measured width + WIDTH_OFFSET to prevent text clipping, clamped to
16
+ * [minWidth, maxWidth] so a long value can't widen the input past the chip.
14
17
  */
15
- export declare const useSizerWidth: ({ sizerRef, text, minWidth, }: UseSizerWidthOptions) => number;
18
+ export declare const useSizerWidth: ({ sizerRef, text, minWidth, maxWidth, }: UseSizerWidthOptions) => number;
16
19
  export {};
@@ -1,13 +1,14 @@
1
1
  import { useEffect, useState } from "react";
2
- import { CHAR_WIDTH_PX, MIN_INPUT_WIDTH, WIDTH_OFFSET } from "../constants.js";
3
- const useSizerWidth = ({ sizerRef, text, minWidth = MIN_INPUT_WIDTH })=>{
2
+ import { CHAR_WIDTH_PX, MAX_INPUT_WIDTH, MIN_INPUT_WIDTH, WIDTH_OFFSET } from "../constants.js";
3
+ const useSizerWidth = ({ sizerRef, text, minWidth = MIN_INPUT_WIDTH, maxWidth = MAX_INPUT_WIDTH })=>{
4
4
  const [width, setWidth] = useState(minWidth);
5
5
  useEffect(()=>{
6
6
  const sizerWidth = sizerRef.current?.getBoundingClientRect().width ?? text.length * CHAR_WIDTH_PX;
7
- setWidth(Math.max(minWidth, sizerWidth));
7
+ setWidth(Math.min(maxWidth, Math.max(minWidth, sizerWidth)));
8
8
  }, [
9
9
  text,
10
- minWidth
10
+ minWidth,
11
+ maxWidth
11
12
  ]);
12
13
  return width + WIDTH_OFFSET;
13
14
  };
@@ -7,6 +7,8 @@ export interface ValueOption {
7
7
  color: BadgeColor;
8
8
  text: string;
9
9
  };
10
+ /** Muted secondary line rendered beneath the bold `label`. Display-only. */
11
+ description?: string;
10
12
  hasSubmenu?: boolean;
11
13
  }
12
14
  type ConditionValue = string | number | boolean;
@@ -8,11 +8,9 @@ import { useValueMenuDisplayValues } from "./useValueMenuDisplayValues.js";
8
8
  import { useValueMenuState } from "./useValueMenuState.js";
9
9
  import { ValueMenuFooter } from "./ValueMenuFooter.js";
10
10
  import { ValueMenuItem } from "./ValueMenuItem.js";
11
+ import { valueOptionSearchText } from "./valueOptionSearchText.js";
11
12
  const FilterInputValueMenu = ({ values, onSelect, onCommit, open = false, onOpenChange, onEscape, multiSelect = false, initialValues = [], highlightValue, width = 'standard', positioning, onBuildingValueChange, onItemToggle, inputRef, filterText = '', menuRef, blurCommitRef, className })=>{
12
- const filteredValues = useMemo(()=>filterAndSort(values, filterText, (v)=>[
13
- v.label,
14
- String(v.value)
15
- ]), [
13
+ const filteredValues = useMemo(()=>filterAndSort(values, filterText, valueOptionSearchText), [
16
14
  values,
17
15
  filterText
18
16
  ]);
@@ -15,13 +15,21 @@ const ValueMenuItem = ({ option, isChecked, isPending, multiSelect, onSelect, re
15
15
  type: "secondary",
16
16
  variant: "default",
17
17
  children: option.badge.text
18
- }) : /*#__PURE__*/ jsx("div", {
18
+ }) : /*#__PURE__*/ jsxs("div", {
19
19
  className: "min-w-0",
20
- children: /*#__PURE__*/ jsx(Text, {
21
- size: "sm",
22
- truncate: true,
23
- children: option.label
24
- })
20
+ children: [
21
+ /*#__PURE__*/ jsx(Text, {
22
+ size: "sm",
23
+ truncate: true,
24
+ children: option.label
25
+ }),
26
+ option.description && /*#__PURE__*/ jsx(Text, {
27
+ size: "xs",
28
+ color: "secondary",
29
+ truncate: true,
30
+ children: option.description
31
+ })
32
+ ]
25
33
  }),
26
34
  multiSelect ? /*#__PURE__*/ jsx("div", {
27
35
  className: "flex shrink-0 items-start justify-end py-2 ml-auto",
@@ -0,0 +1,7 @@
1
+ import type { ValueOption } from './FilterInputValueMenu';
2
+ /**
3
+ * Searchable strings for a value option: its label, its stringified value, and
4
+ * its optional `description` (so a path fragment finds the row). Fed to
5
+ * `filterAndSort`. The description entry is omitted when absent.
6
+ */
7
+ export declare const valueOptionSearchText: (option: Pick<ValueOption, "value" | "label" | "description">) => string[];
@@ -0,0 +1,9 @@
1
+ const valueOptionSearchText = (option)=>{
2
+ const text = [
3
+ option.label,
4
+ String(option.value)
5
+ ];
6
+ if (option.description) text.push(option.description);
7
+ return text;
8
+ };
9
+ export { valueOptionSearchText };
@@ -1,5 +1,6 @@
1
1
  import type { RefObject } from 'react';
2
2
  import type { FieldMetadata, FilterOperator, UpsertCondition } from '../../types';
3
+ import type { BuildingBase } from './useAutocompleteState';
3
4
  interface UseBlurCommitDeps {
4
5
  selectedField: FieldMetadata | null;
5
6
  selectedOperator: FilterOperator | null;
@@ -9,6 +10,12 @@ interface UseBlurCommitDeps {
9
10
  handleCustomValueCommit: (text: string) => void;
10
11
  upsertCondition: UpsertCondition;
11
12
  resetState: () => void;
13
+ /** Which triplet is being built: 0 = base, 1 = paired second. */
14
+ buildingSide: 0 | 1;
15
+ setBuildingSide: (side: 0 | 1) => void;
16
+ /** Base triplet stashed while building a paired chip's second value. */
17
+ buildingBase: BuildingBase | null;
18
+ setBuildingBase: (base: BuildingBase | null) => void;
12
19
  /** Indirection ref breaking the useMenuFlow ↔ useBlurCommit cycle. */
13
20
  commitBuildingOnBlurRef: RefObject<() => boolean>;
14
21
  /** Same indirection for force-commit (area-click → incomplete becomes error). */
@@ -20,7 +27,7 @@ interface UseBlurCommitDeps {
20
27
  * editable. Re-entry-guarded: refs cleared synchronously so concurrent callers
21
28
  * (multiple onOpenChange + blur in one tick) don't create duplicate chips.
22
29
  */
23
- export declare const useBlurCommit: ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, commitBuildingOnBlurRef, commitBuildingForceRef, }: UseBlurCommitDeps) => {
30
+ export declare const useBlurCommit: ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, buildingSide, setBuildingSide, buildingBase, setBuildingBase, commitBuildingOnBlurRef, commitBuildingForceRef, }: UseBlurCommitDeps) => {
24
31
  commitBuildingOnBlur: () => boolean;
25
32
  hasIncompleteBuilding: () => boolean;
26
33
  };
@@ -1,16 +1,34 @@
1
1
  import { useCallback, useLayoutEffect, useRef } from "react";
2
2
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
3
3
  import { isBuildingComplete, isNoValueOperator } from "../../lib/index.js";
4
- const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, commitBuildingOnBlurRef, commitBuildingForceRef })=>{
4
+ const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, buildingSide, setBuildingSide, buildingBase, setBuildingBase, commitBuildingOnBlurRef, commitBuildingForceRef })=>{
5
5
  const selectedFieldRef = useRef(selectedField);
6
6
  const selectedOperatorRef = useRef(selectedOperator);
7
7
  const inputTextRef = useRef(inputText);
8
+ const buildingSideRef = useRef(buildingSide);
9
+ const buildingBaseRef = useRef(buildingBase);
8
10
  useLayoutEffect(()=>{
9
11
  selectedFieldRef.current = selectedField;
10
12
  selectedOperatorRef.current = selectedOperator;
11
13
  inputTextRef.current = inputText;
14
+ buildingSideRef.current = buildingSide;
15
+ buildingBaseRef.current = buildingBase;
12
16
  });
13
17
  const committingRef = useRef(false);
18
+ const commitPairedBaseWithErroredValue = useCallback((base, field, operator)=>{
19
+ upsertCondition(base.field, base.operator, base.value, null, effectiveInsertIndexRef.current);
20
+ const pairError = operator ? SEGMENT_VARIANT.value : true;
21
+ upsertCondition(field, operator ?? void 0, null, void 0, void 0, pairError, void 0, 1);
22
+ setBuildingBase(null);
23
+ setBuildingSide(0);
24
+ resetState();
25
+ }, [
26
+ upsertCondition,
27
+ resetState,
28
+ effectiveInsertIndexRef,
29
+ setBuildingBase,
30
+ setBuildingSide
31
+ ]);
14
32
  const commitBuildingOnBlur = useCallback(()=>{
15
33
  if (committingRef.current) return false;
16
34
  const field = selectedFieldRef.current;
@@ -19,6 +37,19 @@ const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChip
19
37
  if (!field) return false;
20
38
  if (editingChipId) return false;
21
39
  const hasTypedValue = !!operator && !isNoValueOperator(operator) && text.length > 0;
40
+ if (!hasTypedValue && 1 === buildingSideRef.current && buildingBaseRef.current) {
41
+ committingRef.current = true;
42
+ try {
43
+ const base = buildingBaseRef.current;
44
+ selectedFieldRef.current = null;
45
+ selectedOperatorRef.current = null;
46
+ inputTextRef.current = '';
47
+ commitPairedBaseWithErroredValue(base, field, operator);
48
+ return true;
49
+ } finally{
50
+ committingRef.current = false;
51
+ }
52
+ }
22
53
  if (!isBuildingComplete(field, operator, null) && !hasTypedValue) return false;
23
54
  committingRef.current = true;
24
55
  try {
@@ -40,7 +71,8 @@ const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChip
40
71
  handleCustomValueCommit,
41
72
  upsertCondition,
42
73
  resetState,
43
- effectiveInsertIndexRef
74
+ effectiveInsertIndexRef,
75
+ commitPairedBaseWithErroredValue
44
76
  ]);
45
77
  useLayoutEffect(()=>{
46
78
  commitBuildingOnBlurRef.current = commitBuildingOnBlur;
@@ -68,6 +100,10 @@ const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChip
68
100
  handleCustomValueCommit(text);
69
101
  return true;
70
102
  }
103
+ if (1 === buildingSideRef.current && buildingBaseRef.current) {
104
+ commitPairedBaseWithErroredValue(buildingBaseRef.current, field, operator);
105
+ return true;
106
+ }
71
107
  if (isBuildingComplete(field, operator, null)) {
72
108
  upsertCondition(field, operator, null, void 0, effectiveInsertIndexRef.current);
73
109
  resetState();
@@ -85,7 +121,8 @@ const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChip
85
121
  handleCustomValueCommit,
86
122
  upsertCondition,
87
123
  resetState,
88
- effectiveInsertIndexRef
124
+ effectiveInsertIndexRef,
125
+ commitPairedBaseWithErroredValue
89
126
  ]);
90
127
  useLayoutEffect(()=>{
91
128
  commitBuildingForceRef.current = commitBuildingForce;
@@ -64,17 +64,18 @@ const useChipEditing = ({ conditions, chips, fields, setMenuAnchor, setSelectedF
64
64
  const field = condition && fieldsRef.current.find((f)=>f.name === condition.field);
65
65
  const pairedField = field ? field.pairedField : void 0;
66
66
  const chip = chipsRef.current.find((c)=>c.id === chipId);
67
- if (!condition?.pair || !pairedField || !chip?.pair) return;
67
+ if (!condition || !pairedField || !chip?.pair) return;
68
+ const rawOperator = getOperatorFromLabel(chip.pair.operator || '', pairedField.type) ?? condition.pair?.operator ?? null;
69
+ const targetSegment = segment !== SEGMENT_VARIANT.value || rawOperator ? segment : SEGMENT_VARIANT.operator;
68
70
  setMenuAnchor(anchorEl);
69
71
  setEditingChipId(chipId);
70
72
  setEditingSide(1);
71
73
  setSelectedField(pairedField);
72
- const rawOperator = getOperatorFromLabel(chip.pair.operator || '', pairedField.type) ?? condition.pair.operator ?? null;
73
- setSelectedOperator(segment === SEGMENT_VARIANT.value ? rawOperator : null);
74
- setEditingSegment(segment);
75
- setSegmentFilterText(segment === SEGMENT_VARIANT.operator ? chip.pair.operator ?? '' : chip.pair.value ?? '');
74
+ setSelectedOperator(targetSegment === SEGMENT_VARIANT.value ? rawOperator : null);
75
+ setEditingSegment(targetSegment);
76
+ setSegmentFilterText(targetSegment === SEGMENT_VARIANT.operator ? chip.pair.operator ?? '' : chip.pair.value ?? '');
76
77
  setUserHasTyped(false);
77
- setMenuState(SEGMENT_TO_MENU[segment]);
78
+ setMenuState(SEGMENT_TO_MENU[targetSegment]);
78
79
  }, [
79
80
  setMenuAnchor,
80
81
  setSelectedField,
@@ -1,4 +1,5 @@
1
1
  import type { RefObject } from 'react';
2
+ import { type ChipSegment } from '../../FilterInputField/FilterInputChip';
2
3
  import type { Condition, FieldMetadata, FilterInputChipData, UpsertCondition } from '../../types';
3
4
  interface UseFilterInputAutocompleteOptions {
4
5
  fields: FieldMetadata[];
@@ -49,10 +50,10 @@ export declare const useFilterInputAutocomplete: ({ fields, conditions, chips, u
49
50
  handleMenuDiscard: () => void;
50
51
  /** Hard reset for paste/clipboard flows — scraps in-progress building. */
51
52
  resetAutocompleteState: (continueBuilding?: boolean) => void;
52
- handleChipClick: (chipId: string, segment: import("../../FilterInputField").ChipSegment, anchorEl: HTMLElement) => void;
53
- handlePairChipClick: (chipId: string, segment: import("../../FilterInputField").ChipSegment, anchorEl: HTMLElement) => void;
54
- handleBuildingChipClick: (segment: import("../../FilterInputField").ChipSegment, anchorEl: HTMLElement) => void;
55
- switchEditSegment: (targetSegment: import("../../FilterInputField").ChipSegment) => boolean;
53
+ handleChipClick: (chipId: string, segment: ChipSegment, anchorEl: HTMLElement) => void;
54
+ handlePairChipClick: (chipId: string, segment: ChipSegment, anchorEl: HTMLElement) => void;
55
+ handleBuildingChipClick: (segment: ChipSegment, anchorEl: HTMLElement) => void;
56
+ switchEditSegment: (targetSegment: ChipSegment) => boolean;
56
57
  removeEditingChip: () => void;
57
58
  handleConnectorChange: (connectorId: string, value: "and" | "or") => void;
58
59
  handleChipRemove: (chipId: string) => void;
@@ -1,3 +1,4 @@
1
+ import { useCallback } from "react";
1
2
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
2
3
  import { useDateRange } from "../../FilterInputMenu/FilterInputDateValueMenu/hooks.js";
3
4
  import { deriveAutocompleteValues } from "./lib/index.js";
@@ -11,6 +12,7 @@ import { useInputHandlers } from "./useInputHandlers.js";
11
12
  import { useMenuFlow } from "./useMenuFlow/index.js";
12
13
  import { useMenuPositioning } from "./useMenuPositioning.js";
13
14
  import { useResetState } from "./useResetState.js";
15
+ import { useResumeBuilding } from "./useResumeBuilding.js";
14
16
  import { useSegmentEditFlow } from "./useSegmentEditFlow.js";
15
17
  const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition, removeCondition, removeConditionAtIndex, clearAll, setConnectorValue, containerRef, buildingChipRef, inputRef })=>{
16
18
  const state = useAutocompleteState({
@@ -123,6 +125,10 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
123
125
  handleCustomValueCommit,
124
126
  upsertCondition,
125
127
  resetState,
128
+ buildingSide,
129
+ setBuildingSide,
130
+ buildingBase,
131
+ setBuildingBase,
126
132
  commitBuildingOnBlurRef,
127
133
  commitBuildingForceRef
128
134
  });
@@ -170,6 +176,36 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
170
176
  setMenuState,
171
177
  setMenuAnchor
172
178
  });
179
+ const tryResumeBuilding = useResumeBuilding({
180
+ conditions,
181
+ fields,
182
+ removeConditionAtIndex,
183
+ setInsertIndex,
184
+ setBuildingBase,
185
+ setBuildingSide,
186
+ setSelectedField,
187
+ setSelectedOperator,
188
+ setBuildingMultiValue,
189
+ setInputText,
190
+ setMenuState,
191
+ resetMenuAnchor,
192
+ clearEditing: editing.clearEditing,
193
+ inputRef
194
+ });
195
+ const handleChipClick = useCallback((chipId, segment, anchorEl)=>{
196
+ if (tryResumeBuilding(chipId, segment, 0)) return;
197
+ editing.handleChipClick(chipId, segment, anchorEl);
198
+ }, [
199
+ tryResumeBuilding,
200
+ editing.handleChipClick
201
+ ]);
202
+ const handlePairChipClick = useCallback((chipId, segment, anchorEl)=>{
203
+ if (tryResumeBuilding(chipId, segment, 1)) return;
204
+ editing.handlePairChipClick(chipId, segment, anchorEl);
205
+ }, [
206
+ tryResumeBuilding,
207
+ editing.handlePairChipClick
208
+ ]);
173
209
  const { isBuilding, buildingChipData, editingMultiValues, editingSingleValue, editingDateRange } = deriveAutocompleteValues({
174
210
  editingChipId: editing.editingChipId,
175
211
  selectedField,
@@ -204,8 +240,8 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
204
240
  handleMenuClose,
205
241
  handleMenuDiscard,
206
242
  resetAutocompleteState: resetState,
207
- handleChipClick: editing.handleChipClick,
208
- handlePairChipClick: editing.handlePairChipClick,
243
+ handleChipClick,
244
+ handlePairChipClick,
209
245
  handleBuildingChipClick,
210
246
  switchEditSegment,
211
247
  removeEditingChip,
@@ -0,0 +1,32 @@
1
+ import type { RefObject } from 'react';
2
+ import { type ChipSegment } from '../../FilterInputField/FilterInputChip';
3
+ import type { Condition, FieldMetadata, FilterOperator, MenuState } from '../../types';
4
+ import type { BuildingBase } from './useAutocompleteState';
5
+ interface UseResumeBuildingDeps {
6
+ conditions: Condition[];
7
+ fields: FieldMetadata[];
8
+ removeConditionAtIndex: (index: number) => void;
9
+ setInsertIndex: (index: number | null) => void;
10
+ setBuildingBase: (base: BuildingBase | null) => void;
11
+ setBuildingSide: (side: 0 | 1) => void;
12
+ setSelectedField: (field: FieldMetadata | null) => void;
13
+ setSelectedOperator: (op: FilterOperator | null) => void;
14
+ setBuildingMultiValue: (val: string | undefined) => void;
15
+ setInputText: (text: string) => void;
16
+ setMenuState: (state: MenuState) => void;
17
+ resetMenuAnchor: () => void;
18
+ /** Exit any inline-edit so the resumed flow is a clean building chip. */
19
+ clearEditing: () => void;
20
+ inputRef: RefObject<HTMLInputElement | null>;
21
+ }
22
+ /**
23
+ * Resume building a committed-but-incomplete chip instead of inline-editing it,
24
+ * reopening the menu at the first missing step so the building cascade — incl.
25
+ * the paired second triplet — carries on. Makes "press anywhere → red chip →
26
+ * click to continue" work for paired fields (AS-1179).
27
+ *
28
+ * Returns `true` when it took over (caller must skip inline-edit), `false` when
29
+ * the chip is complete (edit it normally).
30
+ */
31
+ export declare const useResumeBuilding: ({ conditions, fields, removeConditionAtIndex, setInsertIndex, setBuildingBase, setBuildingSide, setSelectedField, setSelectedOperator, setBuildingMultiValue, setInputText, setMenuState, resetMenuAnchor, clearEditing, inputRef, }: UseResumeBuildingDeps) => (chipId: string, segment?: ChipSegment, side?: 0 | 1) => boolean;
32
+ export {};
@@ -0,0 +1,75 @@
1
+ import { useCallback } from "react";
2
+ import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
3
+ import { chipIdToConditionIndex, isEmptyFilterValue, isNoValueOperator } from "../../lib/index.js";
4
+ const useResumeBuilding = ({ conditions, fields, removeConditionAtIndex, setInsertIndex, setBuildingBase, setBuildingSide, setSelectedField, setSelectedOperator, setBuildingMultiValue, setInputText, setMenuState, resetMenuAnchor, clearEditing, inputRef })=>useCallback((chipId, segment, side = 0)=>{
5
+ const idx = chipIdToConditionIndex(chipId);
6
+ if (null === idx) return false;
7
+ const condition = conditions[idx];
8
+ if (!condition || condition.disabled) return false;
9
+ const field = fields.find((f)=>f.name === condition.field);
10
+ if (!field) return false;
11
+ const baseOperatorMissing = !condition.operator;
12
+ const baseOperatorTakesValue = null != condition.operator && !isNoValueOperator(condition.operator);
13
+ const baseValueMissing = baseOperatorTakesValue && isEmptyFilterValue(condition.value);
14
+ const startBuilding = (params)=>{
15
+ clearEditing();
16
+ removeConditionAtIndex(idx);
17
+ setInsertIndex(idx);
18
+ setBuildingBase(params.base);
19
+ setBuildingSide(params.side);
20
+ setSelectedField(params.selectedField);
21
+ setSelectedOperator(params.selectedOperator);
22
+ setBuildingMultiValue(void 0);
23
+ setInputText('');
24
+ resetMenuAnchor();
25
+ setMenuState(params.menuState);
26
+ requestAnimationFrame(()=>inputRef.current?.focus());
27
+ };
28
+ if (baseOperatorMissing) return false;
29
+ if (baseValueMissing) {
30
+ if (!field.pairedField && segment !== SEGMENT_VARIANT.value) return false;
31
+ startBuilding({
32
+ base: null,
33
+ side: 0,
34
+ selectedField: field,
35
+ selectedOperator: condition.operator ?? null,
36
+ menuState: 'value'
37
+ });
38
+ return true;
39
+ }
40
+ if (1 === side && segment === SEGMENT_VARIANT.value && field.pairedField && !isNoValueOperator(condition.operator)) {
41
+ const pairOperator = condition.pair?.operator;
42
+ const pairValueMissing = null != pairOperator && !isNoValueOperator(pairOperator) && isEmptyFilterValue(condition.pair?.value);
43
+ if (pairValueMissing) {
44
+ startBuilding({
45
+ base: {
46
+ field,
47
+ operator: condition.operator,
48
+ value: condition.value
49
+ },
50
+ side: 1,
51
+ selectedField: field.pairedField,
52
+ selectedOperator: pairOperator,
53
+ menuState: 'value'
54
+ });
55
+ return true;
56
+ }
57
+ }
58
+ return false;
59
+ }, [
60
+ conditions,
61
+ fields,
62
+ removeConditionAtIndex,
63
+ setInsertIndex,
64
+ setBuildingBase,
65
+ setBuildingSide,
66
+ setSelectedField,
67
+ setSelectedOperator,
68
+ setBuildingMultiValue,
69
+ setInputText,
70
+ setMenuState,
71
+ resetMenuAnchor,
72
+ clearEditing,
73
+ inputRef
74
+ ]);
75
+ export { useResumeBuilding };
@@ -1,5 +1,5 @@
1
1
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
2
- import { NO_VALUE_PLACEHOLDER, canBorrowCrossFieldLabel, findOptionByValue, findValueLabelInFields, getDateDisplayLabel, getInvalidValueIndices, getOperatorLabel, isNoValueOperator } from "../../lib/index.js";
2
+ import { NO_VALUE_PLACEHOLDER, canBorrowCrossFieldLabel, findOptionByValue, findValueLabelInFields, getDateDisplayLabel, getInvalidValueIndices, getOperatorLabel, incompleteTripletError, isEmptyFilterValue, isNoValueOperator } from "../../lib/index.js";
3
3
  const INVALID_DATE = 'Invalid Date';
4
4
  const DATE_RANGE_SEPARATOR = ' – ';
5
5
  const MULTI_VALUE_SEPARATOR = ', ';
@@ -84,23 +84,35 @@ const buildMultiValueChip = (baseChip, condition, field, fields, chipError)=>{
84
84
  };
85
85
  };
86
86
  const buildPairChip = (condition, field, fields)=>{
87
- if (!condition.pair || !field?.pairedField) return;
87
+ if (!field?.pairedField) return;
88
+ if (null != condition.operator && isNoValueOperator(condition.operator)) return;
88
89
  const pf = field.pairedField;
90
+ if (!condition.pair) {
91
+ const baseComplete = null != condition.operator && !isEmptyFilterValue(condition.value);
92
+ if (!baseComplete) return;
93
+ return {
94
+ attribute: pf.label || pf.name,
95
+ value: '',
96
+ error: SEGMENT_VARIANT.value
97
+ };
98
+ }
89
99
  const { operator, value, error } = condition.pair;
90
100
  const displayValue = operator && isNoValueOperator(operator) ? NO_VALUE_PLACEHOLDER : resolveValueLabel(value, pf, fields) ?? String(value ?? '');
101
+ const valueRequiredButMissing = !(operator && isNoValueOperator(operator)) && isEmptyFilterValue(value);
102
+ const pairError = error || (valueRequiredButMissing ? SEGMENT_VARIANT.value : void 0);
91
103
  return {
92
104
  attribute: pf.label || pf.name,
93
105
  operator: operator ? getOperatorLabel(operator, pf.type || DEFAULT_FIELD_TYPE) : void 0,
94
106
  value: displayValue,
95
- ...error && {
96
- error
107
+ ...pairError && {
108
+ error: pairError
97
109
  }
98
110
  };
99
111
  };
100
112
  const makeConditionChipBase = (i, conditions, fields, error)=>{
101
113
  const condition = conditions[i];
102
114
  if (!condition) return makeEmptyChip(i, error);
103
- const chipError = condition.error || (error ? true : void 0);
115
+ const chipError = condition.error || (error ? true : void 0) || incompleteTripletError(condition.operator, condition.value);
104
116
  const field = fields.find((f)=>f.name === condition.field);
105
117
  const baseChip = buildBaseChip(i, condition, field);
106
118
  if (condition.operator && isNoValueOperator(condition.operator)) return {
@@ -155,6 +155,11 @@ const useFilterInputExpression = ({ fields, value, onChange, error, externalErro
155
155
  return;
156
156
  }
157
157
  const condition = buildCondition(field, operator, val, error, dateOrigin);
158
+ if (editingChipId) {
159
+ const idx = chipIdToConditionIndex(editingChipId);
160
+ const prevCondition = null !== idx ? prev.conditions[idx] : void 0;
161
+ if (prevCondition?.pair && prevCondition.field === condition.field) condition.pair = prevCondition.pair;
162
+ }
158
163
  const newConditions = applyCondition(prev.conditions, condition, editingChipId, atIndex);
159
164
  const newConnectors = addConnectorIfNeeded(prev.connectors, newConditions.length, editingChipId, atIndex, prev.conditions.length);
160
165
  applyState({
@@ -1,6 +1,14 @@
1
- import type { FilterInputChipData } from '../types';
1
+ import type { ChipErrorSegment, Condition, FilterInputChipData, FilterOperator } from '../types';
2
2
  /** Map a chip ID (e.g. "chip-2") back to condition index */
3
3
  export declare const chipIdToConditionIndex: (chipId: string) => number | null;
4
+ /** A filter value is empty when null/undefined, '', or an empty array. */
5
+ export declare const isEmptyFilterValue: (value: Condition["value"] | undefined) => boolean;
6
+ /**
7
+ * Error segment for an incomplete triplet, or `undefined` when complete:
8
+ * missing operator → whole triplet (`true`); value-bearing operator with an
9
+ * empty value → the value segment; no-value operator ("is set") → complete.
10
+ */
11
+ export declare const incompleteTripletError: (operator: FilterOperator | null | undefined, value: Condition["value"] | undefined) => ChipErrorSegment | undefined;
4
12
  /**
5
13
  * Convert a condition-level insert index to a position in the chips array.
6
14
  * When `afterConnector` is true, split AFTER the connector preceding the target chip.
@@ -1,8 +1,12 @@
1
+ import { SEGMENT_VARIANT } from "../FilterInputField/FilterInputChip/segmentVariant.js";
1
2
  import { CHIP_ID_PATTERN } from "./constants.js";
3
+ import { isNoValueOperator } from "./operators.js";
2
4
  const chipIdToConditionIndex = (chipId)=>{
3
5
  const match = chipId.match(CHIP_ID_PATTERN);
4
6
  return match ? Number(match[1]) : null;
5
7
  };
8
+ const isEmptyFilterValue = (value)=>null == value || '' === value || Array.isArray(value) && 0 === value.length;
9
+ const incompleteTripletError = (operator, value)=>operator ? isNoValueOperator(operator) ? void 0 : isEmptyFilterValue(value) ? SEGMENT_VARIANT.value : void 0 : true;
6
10
  const findChipSplitIndex = (chips, conditionInsertIndex, afterConnector = false)=>{
7
11
  let conditionCount = 0;
8
12
  for(let i = 0; i < chips.length; i++)if ('chip' === chips[i].variant) {
@@ -15,4 +19,4 @@ const findChipSplitIndex = (chips, conditionInsertIndex, afterConnector = false)
15
19
  }
16
20
  return chips.length;
17
21
  };
18
- export { chipIdToConditionIndex, findChipSplitIndex };
22
+ export { chipIdToConditionIndex, findChipSplitIndex, incompleteTripletError, isEmptyFilterValue };
@@ -3,7 +3,7 @@ export { DATE_PRESETS, formatDateForChip, getDateDisplayLabel, isDatePreset, } f
3
3
  export { applyAcceptChar } from './applyAcceptChar';
4
4
  export { applyFieldValueTransforms } from './applyFieldValueTransforms';
5
5
  export { applyKnownFieldHelpers, getKnownFieldSerializer } from './applyKnownFieldHelpers';
6
- export { chipIdToConditionIndex, findChipSplitIndex } from './conditions';
6
+ export { chipIdToConditionIndex, findChipSplitIndex, incompleteTripletError, isEmptyFilterValue, } from './conditions';
7
7
  export { CONNECTOR_ID_PATTERN, MENU_BASE_GUTTER, MENU_CHIP_GUTTER_OFFSET, NO_VALUE_OPERATORS, OPERATOR_LABELS, OPERATOR_LABELS_BY_TYPE, OPERATOR_SYMBOLS, OPERATORS_BY_TYPE, QUERY_BAR_SELECTOR, VARIANT_LABELS, } from './constants';
8
8
  export { COUNTRY_OPTIONS } from './country';
9
9
  export { type AnchorBounds, buildAnchoredRect, isMenuRelated, toAnchorBounds } from './dom';
@@ -2,7 +2,7 @@ import { DATE_PRESETS, formatDateForChip, getDateDisplayLabel, isDatePreset } fr
2
2
  import { applyAcceptChar } from "./applyAcceptChar.js";
3
3
  import { applyFieldValueTransforms } from "./applyFieldValueTransforms.js";
4
4
  import { applyKnownFieldHelpers, getKnownFieldSerializer } from "./applyKnownFieldHelpers.js";
5
- import { chipIdToConditionIndex, findChipSplitIndex } from "./conditions.js";
5
+ import { chipIdToConditionIndex, findChipSplitIndex, incompleteTripletError, isEmptyFilterValue } from "./conditions.js";
6
6
  import { CONNECTOR_ID_PATTERN, MENU_BASE_GUTTER, MENU_CHIP_GUTTER_OFFSET, NO_VALUE_OPERATORS, OPERATORS_BY_TYPE, OPERATOR_LABELS, OPERATOR_LABELS_BY_TYPE, OPERATOR_SYMBOLS, QUERY_BAR_SELECTOR, VARIANT_LABELS } from "./constants.js";
7
7
  import { COUNTRY_OPTIONS } from "./country/index.js";
8
8
  import { buildAnchoredRect, isMenuRelated, toAnchorBounds } from "./dom.js";
@@ -15,4 +15,4 @@ import { SEGMENT_TO_MENU } from "./segmentMenu.js";
15
15
  import { serializeExpression } from "./serializeExpression.js";
16
16
  import { createStatusCodeInputFilter, createStatusCodeNormalizer, createStatusCodeSerializer, createStatusCodeSuggestions, createStatusCodeValidator } from "./statusCode/index.js";
17
17
  import { canBorrowCrossFieldLabel, findMatchingFieldValue, getInvalidValueIndices, isValidFieldValue, validateValueForField } from "./validation.js";
18
- export { CONNECTOR_ID_PATTERN, COUNTRY_OPTIONS, DATE_PRESETS, MENU_BASE_GUTTER, MENU_CHIP_GUTTER_OFFSET, NO_VALUE_OPERATORS, NO_VALUE_PLACEHOLDER, OPERATORS_BY_TYPE, OPERATOR_LABELS, OPERATOR_LABELS_BY_TYPE, OPERATOR_SYMBOLS, QUERY_BAR_SELECTOR, SEGMENT_TO_MENU, VARIANT_LABELS, applyAcceptChar, applyFieldValueTransforms, applyKnownFieldHelpers, buildAnchoredRect, canBorrowCrossFieldLabel, chipIdToConditionIndex, createStatusCodeInputFilter, createStatusCodeNormalizer, createStatusCodeSerializer, createStatusCodeSuggestions, createStatusCodeValidator, filterAndSort, findChipSplitIndex, findMatchingFieldValue, findOptionByValue, findValueLabelInFields, formatDateForChip, getCurrentValueTokenText, getDateDisplayLabel, getFieldOperators, getFieldValues, getInvalidValueIndices, getKnownFieldSerializer, getOperatorFromLabel, getOperatorLabel, getValueFilterText, hasFieldValues, hasStaticAllowlist, isBetweenOperator, isBuildingComplete, isDatePreset, isFilterParseError, isMenuRelated, isMultiSelectOperator, isNoValueOperator, isOperatorAllowedForField, isValidFieldValue, isValueShapeCompatible, nextBuildingMenu, parseExpression, serializeExpression, toAnchorBounds, validateValueForField };
18
+ export { CONNECTOR_ID_PATTERN, COUNTRY_OPTIONS, DATE_PRESETS, MENU_BASE_GUTTER, MENU_CHIP_GUTTER_OFFSET, NO_VALUE_OPERATORS, NO_VALUE_PLACEHOLDER, OPERATORS_BY_TYPE, OPERATOR_LABELS, OPERATOR_LABELS_BY_TYPE, OPERATOR_SYMBOLS, QUERY_BAR_SELECTOR, SEGMENT_TO_MENU, VARIANT_LABELS, applyAcceptChar, applyFieldValueTransforms, applyKnownFieldHelpers, buildAnchoredRect, canBorrowCrossFieldLabel, chipIdToConditionIndex, createStatusCodeInputFilter, createStatusCodeNormalizer, createStatusCodeSerializer, createStatusCodeSuggestions, createStatusCodeValidator, filterAndSort, findChipSplitIndex, findMatchingFieldValue, findOptionByValue, findValueLabelInFields, formatDateForChip, getCurrentValueTokenText, getDateDisplayLabel, getFieldOperators, getFieldValues, getInvalidValueIndices, getKnownFieldSerializer, getOperatorFromLabel, getOperatorLabel, getValueFilterText, hasFieldValues, hasStaticAllowlist, incompleteTripletError, isBetweenOperator, isBuildingComplete, isDatePreset, isEmptyFilterValue, isFilterParseError, isMenuRelated, isMultiSelectOperator, isNoValueOperator, isOperatorAllowedForField, isValidFieldValue, isValueShapeCompatible, nextBuildingMenu, parseExpression, serializeExpression, toAnchorBounds, validateValueForField };
@@ -59,6 +59,14 @@ export interface FieldValueOption {
59
59
  color: BadgeColor;
60
60
  text: string;
61
61
  };
62
+ /**
63
+ * Optional secondary text rendered as a muted line beneath the bold `label`
64
+ * in the value dropdown (e.g. a backend path that explains or disambiguates
65
+ * the option). Display-only — never committed as a filter value. Because the
66
+ * bold `label` need not be unique, this line is what distinguishes rows that
67
+ * share a label (two `request-id` rows with different paths).
68
+ */
69
+ description?: string;
62
70
  }
63
71
  /**
64
72
  * Field Metadata Interface
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.69.0",
3
- "generatedAt": "2026-07-02T05:34:16.725Z",
2
+ "version": "0.70.0",
3
+ "generatedAt": "2026-07-02T11:59:11.867Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "0.70.0",
3
+ "version": "0.71.0",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",