@wallarm-org/design-system 0.21.0 → 0.21.1

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 (19) hide show
  1. package/dist/components/DropdownMenu/DropdownMenuContent.js +1 -1
  2. package/dist/components/FilterInput/FilterInput.js +1 -0
  3. package/dist/components/FilterInput/FilterInputField/FilterInputChip/FilterInputChip.js +2 -2
  4. package/dist/components/FilterInput/FilterInputField/FilterInputSearch.js +1 -1
  5. package/dist/components/FilterInput/FilterInputField/InsertionGap/classes.d.ts +1 -1
  6. package/dist/components/FilterInput/FilterInputField/InsertionGap/classes.js +1 -1
  7. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/ValueMenuItem.js +12 -6
  8. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/dom.d.ts +18 -4
  9. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/dom.js +74 -19
  10. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/index.d.ts +1 -1
  11. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/index.js +2 -2
  12. package/dist/components/FilterInput/hooks/useFilterInputSelection/useDragSelection.d.ts +2 -2
  13. package/dist/components/FilterInput/hooks/useFilterInputSelection/useDragSelection.js +17 -13
  14. package/dist/components/FilterInput/hooks/useFilterInputSelection/useFilterInputSelection.d.ts +2 -1
  15. package/dist/components/FilterInput/hooks/useFilterInputSelection/useFilterInputSelection.js +19 -4
  16. package/dist/components/FilterInput/lib/parseExpression/tokenizer.js +14 -0
  17. package/dist/components/FilterInput/lib/serializeExpression.js +3 -5
  18. package/dist/metadata/components.json +2 -2
  19. package/package.json +1 -1
@@ -31,7 +31,7 @@ const DropdownMenuContent = ({ className, children, ref, ...props })=>{
31
31
  children: [
32
32
  /*#__PURE__*/ jsx(ScrollAreaViewport, {
33
33
  children: /*#__PURE__*/ jsx(ScrollAreaContent, {
34
- className: cn('flex flex-col gap-1'),
34
+ className: cn('flex flex-col gap-1 !min-w-0'),
35
35
  children: menuChildren
36
36
  })
37
37
  }),
@@ -38,6 +38,7 @@ const FilterInput = ({ fields = [], value, onChange, placeholder = 'Type to filt
38
38
  conditions,
39
39
  connectors,
40
40
  fields,
41
+ containerRef,
41
42
  chipRegistryRef,
42
43
  inputRef,
43
44
  clearAll,
@@ -44,7 +44,7 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
44
44
  error: hasError,
45
45
  interactive,
46
46
  disabled
47
- }), 'max-w-[600px]', className),
47
+ }), 'max-w-[320px]', className),
48
48
  "data-slot": "filter-input-condition-chip",
49
49
  ...props,
50
50
  children: [
@@ -65,7 +65,7 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
65
65
  }),
66
66
  (value || 'value' === activeSegment) && /*#__PURE__*/ jsx(Segment, {
67
67
  variant: "value",
68
- className: "shrink-0",
68
+ className: "min-w-0",
69
69
  error: 'value' !== activeSegment && (true === error || 'value' === error),
70
70
  valueParts: valueParts,
71
71
  valueSeparator: valueSeparator,
@@ -2,7 +2,7 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { useFilterInputContext } from "../FilterInputContext/index.js";
3
3
  import { filterInputInputVariants } from "./classes.js";
4
4
  const CHAR_WIDTH_PX = 10;
5
- const MIN_INPUT_WIDTH = 20;
5
+ const MIN_INPUT_WIDTH = 4;
6
6
  const FilterInputSearch = ({ hasContent })=>{
7
7
  const { inputText, inputRef, placeholder, error, menuOpen, onInputChange, onInputKeyDown, onInputClick } = useFilterInputContext();
8
8
  return /*#__PURE__*/ jsx("input", {
@@ -1,4 +1,4 @@
1
1
  /** Insertion gap button */
2
- export declare const insertionGapButton = "group relative z-20 flex h-24 w-8 shrink-0 cursor-text items-center justify-center";
2
+ export declare const insertionGapButton = "group relative z-20 flex h-24 w-4 shrink-0 cursor-text items-center justify-center";
3
3
  /** Insertion gap divider indicator */
4
4
  export declare const insertionGapDivider = "h-16 w-1 rounded-full bg-transparent transition-colors group-hover:bg-border-primary/50";
@@ -1,3 +1,3 @@
1
- const insertionGapButton = 'group relative z-20 flex h-24 w-8 shrink-0 cursor-text items-center justify-center';
1
+ const insertionGapButton = 'group relative z-20 flex h-24 w-4 shrink-0 cursor-text items-center justify-center';
2
2
  const insertionGapDivider = 'h-16 w-1 rounded-full bg-transparent transition-colors group-hover:bg-border-primary/50';
3
3
  export { insertionGapButton, insertionGapDivider };
@@ -1,7 +1,8 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { ChevronRight } from "../../../../icons/ChevronRight.js";
3
3
  import { Checkmark } from "../../../Checkmark/index.js";
4
- import { DropdownMenuItem, DropdownMenuItemText } from "../../../DropdownMenu/index.js";
4
+ import { DropdownMenuItem } from "../../../DropdownMenu/index.js";
5
+ import { Text } from "../../../Text/index.js";
5
6
  const ValueMenuItem = ({ option, isChecked, isPending, multiSelect, onSelect })=>/*#__PURE__*/ jsxs(DropdownMenuItem, {
6
7
  value: String(option.value),
7
8
  onSelect: onSelect,
@@ -17,20 +18,25 @@ const ValueMenuItem = ({ option, isChecked, isPending, multiSelect, onSelect })=
17
18
  className: "size-6 rounded-full bg-current"
18
19
  }),
19
20
  /*#__PURE__*/ jsx("span", {
20
- className: "leading-4 text-ellipsis",
21
+ className: "min-w-0 truncate leading-4",
21
22
  children: option.badge.text
22
23
  })
23
24
  ]
24
- }) : /*#__PURE__*/ jsx(DropdownMenuItemText, {
25
- children: option.label
25
+ }) : /*#__PURE__*/ jsx("div", {
26
+ className: "min-w-0",
27
+ children: /*#__PURE__*/ jsx(Text, {
28
+ size: "sm",
29
+ truncate: true,
30
+ children: option.label
31
+ })
26
32
  }),
27
33
  multiSelect ? /*#__PURE__*/ jsx("div", {
28
- className: "flex items-start justify-end py-2 ml-auto",
34
+ className: "flex shrink-0 items-start justify-end py-2 ml-auto",
29
35
  children: /*#__PURE__*/ jsx(Checkmark, {
30
36
  checkedState: isChecked
31
37
  })
32
38
  }) : isChecked ? /*#__PURE__*/ jsx("div", {
33
- className: "flex items-start justify-end py-2 ml-auto",
39
+ className: "flex shrink-0 items-start justify-end py-2 ml-auto",
34
40
  children: /*#__PURE__*/ jsx(Checkmark, {
35
41
  checkedState: true
36
42
  })
@@ -2,7 +2,21 @@ export declare const clearDragAttributes: (chips: Map<string, HTMLElement>) => v
2
2
  export declare const hasDragSelection: (chips: Map<string, HTMLElement>) => boolean;
3
3
  /** Get condition indices of drag-selected chips (chip-0, chip-2 → [0, 2]) */
4
4
  export declare const getSelectedConditionIndices: (chips: Map<string, HTMLElement>) => number[];
5
- /** Mark chips as drag-selected based on horizontal range. Returns true if any chip was selected. */
6
- export declare const updateDragSelection: (registry: Map<string, HTMLElement>, startX: number, currentX: number) => boolean;
7
- /** Check if every condition chip in the registry is drag-selected */
8
- export declare const areAllConditionsDragged: (registry: Map<string, HTMLElement>) => boolean;
5
+ interface VisualEntry {
6
+ id: string;
7
+ el: HTMLElement;
8
+ rect: DOMRect;
9
+ /** Index in the full sorted array (preserved through condition filtering) */
10
+ allIdx: number;
11
+ }
12
+ /** Snapshot registry entries with cached rects, sorted by visual position (top→bottom, left→right) */
13
+ export declare const getVisualEntries: (registry: Map<string, HTMLElement>) => VisualEntry[];
14
+ /** Resolve the condition chip ID at the mousedown point (called once per drag). */
15
+ export declare const resolveAnchorChipId: (registry: Map<string, HTMLElement>, target: HTMLElement, x: number, y: number) => string | null;
16
+ /**
17
+ * Mark chips as drag-selected from a fixed anchor chip to the current cursor position.
18
+ * Both anchor and end are resolved in the same visually-sorted array so indices always match.
19
+ * Connectors between selected condition chips are included automatically.
20
+ */
21
+ export declare const updateDragSelection: (all: VisualEntry[], anchorChipId: string, currentX: number, currentY: number) => boolean;
22
+ export {};
@@ -1,11 +1,5 @@
1
1
  import { CHIP_ID_PREFIX } from "./constants.js";
2
2
  const DRAG_ATTR = 'data-drag-selected';
3
- const isChipInRange = (chip, x1, x2)=>{
4
- const rect = chip.getBoundingClientRect();
5
- const minX = Math.min(x1, x2);
6
- const maxX = Math.max(x1, x2);
7
- return rect.left <= maxX && rect.right >= minX;
8
- };
9
3
  const clearDragAttributes = (chips)=>{
10
4
  [
11
5
  ...chips.values()
@@ -17,18 +11,79 @@ const hasDragSelection = (chips)=>[
17
11
  const getSelectedConditionIndices = (chips)=>[
18
12
  ...chips.entries()
19
13
  ].filter(([id, el])=>id.startsWith(CHIP_ID_PREFIX) && el.hasAttribute(DRAG_ATTR)).map(([id])=>Number(id.slice(CHIP_ID_PREFIX.length))).filter((index)=>!Number.isNaN(index)).sort((a, b)=>a - b);
20
- const updateDragSelection = (registry, startX, currentX)=>[
21
- ...registry.values()
22
- ].reduce((found, chip)=>{
23
- const inRange = isChipInRange(chip, startX, currentX);
24
- if (inRange) chip.setAttribute(DRAG_ATTR, '');
25
- else chip.removeAttribute(DRAG_ATTR);
26
- return found || inRange;
27
- }, false);
28
- const areAllConditionsDragged = (registry)=>{
29
- const conditions = [
14
+ const getVisualEntries = (registry)=>[
15
+ ...registry.entries()
16
+ ].map(([id, el])=>({
17
+ id,
18
+ el,
19
+ rect: el.getBoundingClientRect(),
20
+ allIdx: 0
21
+ })).sort((a, b)=>{
22
+ const rowDiff = a.rect.top - b.rect.top;
23
+ if (Math.abs(rowDiff) > Math.min(a.rect.height, b.rect.height) / 2) return rowDiff;
24
+ return a.rect.left - b.rect.left;
25
+ }).map((entry, i)=>({
26
+ ...entry,
27
+ allIdx: i
28
+ }));
29
+ const findClosestByDistance = (entries, x, y)=>entries.reduce((best, entry, i)=>{
30
+ const r = entry.rect;
31
+ const dx = Math.max(r.left - x, 0, x - r.right);
32
+ const dy = Math.max(r.top - y, 0, y - r.bottom);
33
+ const dist = dx * dx + dy * dy;
34
+ return dist < best.dist ? {
35
+ idx: i,
36
+ dist
37
+ } : best;
38
+ }, {
39
+ idx: -1,
40
+ dist: 1 / 0
41
+ }).idx;
42
+ const findByReadingOrder = (entries, x, y)=>{
43
+ if (0 === entries.length) return -1;
44
+ const exactIdx = entries.findIndex((e)=>x >= e.rect.left && x <= e.rect.right && y >= e.rect.top && y <= e.rect.bottom);
45
+ if (-1 !== exactIdx) return exactIdx;
46
+ const chipHeight = entries[0].rect.height;
47
+ const lastBefore = entries.reduce((best, entry, i)=>{
48
+ const cy = (entry.rect.top + entry.rect.bottom) / 2;
49
+ const cx = (entry.rect.left + entry.rect.right) / 2;
50
+ const rowAbove = cy + chipHeight / 2 < y;
51
+ const sameRow = !rowAbove && Math.abs(cy - y) <= chipHeight;
52
+ if (rowAbove || sameRow && cx <= x) return i;
53
+ return best;
54
+ }, -1);
55
+ return lastBefore >= 0 ? lastBefore : findClosestByDistance(entries, x, y);
56
+ };
57
+ const resolveAnchorChipId = (registry, target, x, y)=>{
58
+ const hit = [
30
59
  ...registry.entries()
31
- ].filter(([id])=>id.startsWith(CHIP_ID_PREFIX));
32
- return conditions.length > 0 && conditions.every(([, el])=>el.hasAttribute(DRAG_ATTR));
60
+ ].find(([id, el])=>id.startsWith(CHIP_ID_PREFIX) && el.contains(target));
61
+ if (hit) return hit[0];
62
+ const conditions = getVisualEntries(registry).filter((e)=>e.id.startsWith(CHIP_ID_PREFIX));
63
+ if (0 === conditions.length) return null;
64
+ const idx = findClosestByDistance(conditions, x, y);
65
+ return idx >= 0 ? conditions[idx].id : null;
66
+ };
67
+ const updateDragSelection = (all, anchorChipId, currentX, currentY)=>{
68
+ if (0 === all.length) return false;
69
+ const conditions = all.filter((e)=>e.id.startsWith(CHIP_ID_PREFIX));
70
+ if (0 === conditions.length) return false;
71
+ const anchorIdx = conditions.findIndex((e)=>e.id === anchorChipId);
72
+ if (-1 === anchorIdx) return false;
73
+ const endIdx = findByReadingOrder(conditions, currentX, currentY);
74
+ if (-1 === endIdx) return false;
75
+ const minIdx = Math.min(anchorIdx, endIdx);
76
+ const maxIdx = Math.max(anchorIdx, endIdx);
77
+ const firstAll = conditions[minIdx].allIdx;
78
+ const lastAll = conditions[maxIdx].allIdx;
79
+ let found = false;
80
+ all.forEach((entry, i)=>{
81
+ const inRange = i >= firstAll && i <= lastAll;
82
+ if (inRange) {
83
+ entry.el.setAttribute(DRAG_ATTR, '');
84
+ found = true;
85
+ } else entry.el.removeAttribute(DRAG_ATTR);
86
+ });
87
+ return found;
33
88
  };
34
- export { areAllConditionsDragged, clearDragAttributes, getSelectedConditionIndices, hasDragSelection, updateDragSelection };
89
+ export { clearDragAttributes, getSelectedConditionIndices, getVisualEntries, hasDragSelection, resolveAnchorChipId, updateDragSelection };
@@ -1,3 +1,3 @@
1
1
  export { CHIP_ID_PREFIX, DRAG_THRESHOLD, PASTE_ERROR_TIMEOUT } from './constants';
2
- export { areAllConditionsDragged, clearDragAttributes, getSelectedConditionIndices, hasDragSelection, updateDragSelection, } from './dom';
2
+ export { clearDragAttributes, getSelectedConditionIndices, getVisualEntries, hasDragSelection, resolveAnchorChipId, updateDragSelection, } from './dom';
3
3
  export { serializeSelectedOrAll } from './serialize';
@@ -1,4 +1,4 @@
1
1
  import { CHIP_ID_PREFIX, DRAG_THRESHOLD, PASTE_ERROR_TIMEOUT } from "./constants.js";
2
- import { areAllConditionsDragged, clearDragAttributes, getSelectedConditionIndices, hasDragSelection, updateDragSelection } from "./dom.js";
2
+ import { clearDragAttributes, getSelectedConditionIndices, getVisualEntries, hasDragSelection, resolveAnchorChipId, updateDragSelection } from "./dom.js";
3
3
  import { serializeSelectedOrAll } from "./serialize.js";
4
- export { CHIP_ID_PREFIX, DRAG_THRESHOLD, PASTE_ERROR_TIMEOUT, areAllConditionsDragged, clearDragAttributes, getSelectedConditionIndices, hasDragSelection, serializeSelectedOrAll, updateDragSelection };
4
+ export { CHIP_ID_PREFIX, DRAG_THRESHOLD, PASTE_ERROR_TIMEOUT, clearDragAttributes, getSelectedConditionIndices, getVisualEntries, hasDragSelection, resolveAnchorChipId, serializeSelectedOrAll, updateDragSelection };
@@ -2,9 +2,9 @@ import type { MouseEvent, RefObject } from 'react';
2
2
  interface UseDragSelectionOptions {
3
3
  chipRegistryRef: RefObject<Map<string, HTMLElement>>;
4
4
  inputRef: RefObject<HTMLInputElement | null>;
5
- onSelectAll: () => void;
5
+ closeMenu: () => void;
6
6
  }
7
- export declare const useDragSelection: ({ chipRegistryRef, inputRef, onSelectAll, }: UseDragSelectionOptions) => {
7
+ export declare const useDragSelection: ({ chipRegistryRef, inputRef, closeMenu, }: UseDragSelectionOptions) => {
8
8
  handleMouseDown: (e: MouseEvent<HTMLDivElement>) => void;
9
9
  };
10
10
  export {};
@@ -1,33 +1,37 @@
1
1
  import { useCallback, useRef } from "react";
2
- import { DRAG_THRESHOLD, areAllConditionsDragged, clearDragAttributes, updateDragSelection } from "./lib/index.js";
3
- const useDragSelection = ({ chipRegistryRef, inputRef, onSelectAll })=>{
2
+ import { DRAG_THRESHOLD, getVisualEntries, resolveAnchorChipId, updateDragSelection } from "./lib/index.js";
3
+ const DRAG_IGNORE_SELECTOR = 'input, [role="combobox"]';
4
+ const useDragSelection = ({ chipRegistryRef, inputRef, closeMenu })=>{
4
5
  const isDraggingRef = useRef(false);
5
6
  const dragStartXRef = useRef(0);
7
+ const dragStartYRef = useRef(0);
8
+ const anchorChipIdRef = useRef(null);
9
+ const visualEntriesRef = useRef([]);
6
10
  const handleMouseDown = useCallback((e)=>{
7
11
  if (0 !== e.button) return;
8
12
  const target = e.target;
9
- if (target.closest('input, button, [role="combobox"]')) return;
13
+ if (target.closest(DRAG_IGNORE_SELECTOR)) return;
10
14
  dragStartXRef.current = e.clientX;
15
+ dragStartYRef.current = e.clientY;
11
16
  isDraggingRef.current = false;
17
+ anchorChipIdRef.current = resolveAnchorChipId(chipRegistryRef.current, target, e.clientX, e.clientY);
18
+ visualEntriesRef.current = getVisualEntries(chipRegistryRef.current);
12
19
  const handleMouseMove = (moveEvent)=>{
13
- if (Math.abs(moveEvent.clientX - dragStartXRef.current) < DRAG_THRESHOLD) return;
20
+ const dx = moveEvent.clientX - dragStartXRef.current;
21
+ const dy = moveEvent.clientY - dragStartYRef.current;
22
+ if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
14
23
  if (!isDraggingRef.current) {
15
24
  isDraggingRef.current = true;
16
25
  document.body.style.userSelect = 'none';
26
+ closeMenu();
27
+ inputRef.current?.blur();
17
28
  }
18
- const hasSelected = updateDragSelection(chipRegistryRef.current, dragStartXRef.current, moveEvent.clientX);
19
- if (hasSelected) inputRef.current?.blur();
29
+ if (anchorChipIdRef.current) updateDragSelection(visualEntriesRef.current, anchorChipIdRef.current, moveEvent.clientX, moveEvent.clientY);
20
30
  };
21
31
  const handleMouseUp = ()=>{
22
32
  document.removeEventListener('mousemove', handleMouseMove);
23
33
  document.removeEventListener('mouseup', handleMouseUp);
24
34
  document.body.style.userSelect = '';
25
- if (!isDraggingRef.current) return;
26
- const registry = chipRegistryRef.current;
27
- if (areAllConditionsDragged(registry)) {
28
- clearDragAttributes(registry);
29
- onSelectAll();
30
- }
31
35
  isDraggingRef.current = false;
32
36
  };
33
37
  document.addEventListener('mousemove', handleMouseMove);
@@ -35,7 +39,7 @@ const useDragSelection = ({ chipRegistryRef, inputRef, onSelectAll })=>{
35
39
  }, [
36
40
  chipRegistryRef,
37
41
  inputRef,
38
- onSelectAll
42
+ closeMenu
39
43
  ]);
40
44
  return {
41
45
  handleMouseDown
@@ -4,6 +4,7 @@ interface UseFilterInputSelectionOptions {
4
4
  conditions: Condition[];
5
5
  connectors: Array<'and' | 'or'>;
6
6
  fields: FieldMetadata[];
7
+ containerRef: RefObject<HTMLDivElement | null>;
7
8
  chipRegistryRef: RefObject<Map<string, HTMLElement>>;
8
9
  inputRef: RefObject<HTMLInputElement | null>;
9
10
  clearAll: () => void;
@@ -11,7 +12,7 @@ interface UseFilterInputSelectionOptions {
11
12
  closeMenu: () => void;
12
13
  onChange?: (expression: ExprNode | null) => void;
13
14
  }
14
- export declare const useFilterInputSelection: ({ conditions, connectors, fields, chipRegistryRef, inputRef, clearAll, setInputText, closeMenu, onChange, }: UseFilterInputSelectionOptions) => {
15
+ export declare const useFilterInputSelection: ({ conditions, connectors, fields, containerRef, chipRegistryRef, inputRef, clearAll, setInputText, closeMenu, onChange, }: UseFilterInputSelectionOptions) => {
15
16
  allSelected: boolean;
16
17
  pasteError: string | null;
17
18
  clearSelection: () => void;
@@ -1,9 +1,9 @@
1
- import { useCallback, useState } from "react";
2
- import { clearDragAttributes } from "./lib/index.js";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { clearDragAttributes, hasDragSelection } from "./lib/index.js";
3
3
  import { useDragSelection } from "./useDragSelection.js";
4
4
  import { useSelectionClipboard } from "./useSelectionClipboard.js";
5
5
  import { useSelectionKeyboard } from "./useSelectionKeyboard.js";
6
- const useFilterInputSelection = ({ conditions, connectors, fields, chipRegistryRef, inputRef, clearAll, setInputText, closeMenu, onChange })=>{
6
+ const useFilterInputSelection = ({ conditions, connectors, fields, containerRef, chipRegistryRef, inputRef, clearAll, setInputText, closeMenu, onChange })=>{
7
7
  const [allSelected, setAllSelected] = useState(false);
8
8
  const [pasteError, setPasteError] = useState(null);
9
9
  const onSelectAll = useCallback(()=>setAllSelected(true), []);
@@ -14,10 +14,25 @@ const useFilterInputSelection = ({ conditions, connectors, fields, chipRegistryR
14
14
  chipRegistryRef
15
15
  ]);
16
16
  const dismissPasteError = useCallback(()=>setPasteError(null), []);
17
+ const allSelectedRef = useRef(false);
18
+ allSelectedRef.current = allSelected;
19
+ useEffect(()=>{
20
+ const handleClickOutside = (e)=>{
21
+ if (!containerRef.current?.contains(e.target)) {
22
+ if (allSelectedRef.current || hasDragSelection(chipRegistryRef.current)) clearSelection();
23
+ }
24
+ };
25
+ document.addEventListener('mousedown', handleClickOutside);
26
+ return ()=>document.removeEventListener('mousedown', handleClickOutside);
27
+ }, [
28
+ containerRef,
29
+ chipRegistryRef,
30
+ clearSelection
31
+ ]);
17
32
  const { handleMouseDown } = useDragSelection({
18
33
  chipRegistryRef,
19
34
  inputRef,
20
- onSelectAll
35
+ closeMenu
21
36
  });
22
37
  const { handleKeyDown } = useSelectionKeyboard({
23
38
  allSelected,
@@ -67,6 +67,20 @@ const tokenize = (input)=>{
67
67
  push('OPERATOR', ch, i);
68
68
  continue;
69
69
  }
70
+ if ('"' === ch || "'" === ch) {
71
+ const quote = ch;
72
+ const start = i;
73
+ i++;
74
+ while(i < input.length && input[i] !== quote)i++;
75
+ if (i >= input.length) throw FilterParseError(`Unterminated string at position ${start}`);
76
+ tokens.push({
77
+ type: 'IDENT',
78
+ value: input.slice(start + 1, i),
79
+ pos: start
80
+ });
81
+ i++;
82
+ continue;
83
+ }
70
84
  if (isIdentChar(ch)) {
71
85
  const start = i;
72
86
  while(i < input.length && isIdentChar(input[i]))i++;
@@ -1,10 +1,8 @@
1
+ const quoteValue = (v)=>`"${v}"`;
1
2
  const serializeValue = (condition)=>{
2
3
  if ('is_null' === condition.operator || 'is_not_null' === condition.operator) return '';
3
- if (Array.isArray(condition.value)) {
4
- const parts = condition.value.map(String);
5
- return `[${parts.join(', ')}]`;
6
- }
7
- return String(condition.value ?? '');
4
+ if (Array.isArray(condition.value)) return `[${condition.value.map(quoteValue).join(', ')}]`;
5
+ return quoteValue(condition.value ?? '');
8
6
  };
9
7
  const serializeCondition = (condition)=>{
10
8
  const field = condition.field;
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.20.0",
3
- "generatedAt": "2026-04-07T08:00:47.141Z",
2
+ "version": "0.21.0",
3
+ "generatedAt": "2026-04-08T11:20:13.053Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Alert",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",