@wallarm-org/design-system 0.82.0 → 0.83.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 (18) hide show
  1. package/dist/components/DropdownMenu/DropdownMenuLabel.js +3 -2
  2. package/dist/components/DropdownMenu/classes.d.ts +1 -0
  3. package/dist/components/DropdownMenu/classes.js +3 -0
  4. package/dist/components/FilterInput/FilterInputField/FilterInputConnectorChip/FilterInputConnectorChip.js +1 -0
  5. package/dist/components/FilterInput/FilterInputMenu/FilterInputFieldMenu/FieldMenuSections.js +94 -72
  6. package/dist/components/FilterInput/FilterInputMenu/FilterInputFieldMenu/FilterInputFieldMenu.js +1 -0
  7. package/dist/components/FilterInput/FilterInputMenu/FilterInputOperatorMenu.js +1 -0
  8. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/FlatValueMenu.js +2 -0
  9. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/NestedValueMenu.js +62 -36
  10. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/NestedValueMenuItem.d.ts +12 -4
  11. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/NestedValueMenuItem.js +49 -15
  12. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/ValueMenuItem.js +2 -2
  13. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/useNestedValueMenuState.d.ts +4 -0
  14. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/useNestedValueMenuState.js +34 -12
  15. package/dist/components/FilterInput/lib/buildValueMenuSections.d.ts +19 -3
  16. package/dist/components/FilterInput/lib/buildValueMenuSections.js +71 -6
  17. package/dist/metadata/components.json +8 -2
  18. package/package.json +1 -1
@@ -2,12 +2,13 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { cn } from "../../utils/cn.js";
3
3
  import { useTestId } from "../../utils/testId.js";
4
4
  import { dropdownMenuLabelVariants } from "./classes.js";
5
- const DropdownMenuLabel = ({ className, inset, ...props })=>{
5
+ const DropdownMenuLabel = ({ className, inset, sticky, ...props })=>{
6
6
  const testId = useTestId('label');
7
7
  return /*#__PURE__*/ jsx("div", {
8
8
  "data-testid": testId,
9
9
  className: cn(dropdownMenuLabelVariants({
10
- inset
10
+ inset,
11
+ sticky
11
12
  }), className),
12
13
  ...props
13
14
  });
@@ -6,4 +6,5 @@ export declare const dropdownMenuItemVariants: (props?: ({
6
6
  export declare const dropdownMenuItemIndicatorClassName: string;
7
7
  export declare const dropdownMenuLabelVariants: (props?: ({
8
8
  inset?: boolean | null | undefined;
9
+ sticky?: boolean | null | undefined;
9
10
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -23,6 +23,9 @@ const dropdownMenuLabelVariants = cva('flex justify-between gap-8 px-8 pt-8 pb-2
23
23
  variants: {
24
24
  inset: {
25
25
  true: 'pl-32'
26
+ },
27
+ sticky: {
28
+ true: 'sticky top-0 z-10 bg-bg-surface-2'
26
29
  }
27
30
  }
28
31
  });
@@ -83,6 +83,7 @@ const FilterInputConnectorChip = ({ chipId, variant, onChange, className })=>{
83
83
  ]
84
84
  }),
85
85
  /*#__PURE__*/ jsxs(DropdownMenuFooter, {
86
+ className: "justify-start",
86
87
  children: [
87
88
  /*#__PURE__*/ jsxs("span", {
88
89
  className: "flex items-center gap-4",
@@ -5,40 +5,46 @@ import { CircleSlash } from "../../../../icons/CircleSlash.js";
5
5
  import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuItemIcon, DropdownMenuItemText, DropdownMenuLabel, DropdownMenuSeparator } from "../../../DropdownMenu/index.js";
6
6
  const RecentSection = ({ conditions, fields, onSelect, registerItem })=>/*#__PURE__*/ jsxs(Fragment, {
7
7
  children: [
8
- /*#__PURE__*/ jsx(DropdownMenuLabel, {
9
- children: "Recent"
10
- }),
11
- /*#__PURE__*/ jsx(DropdownMenuGroup, {
12
- children: conditions.map((condition, index)=>{
13
- const fieldMeta = fields.find((f)=>f.name === condition.field);
14
- const attribute = fieldMeta?.label || condition.field;
15
- const operator = String(condition.operator);
16
- const value = String(condition.value);
17
- return /*#__PURE__*/ jsx(DropdownMenuItem, {
18
- value: `recent-${index}`,
19
- ref: registerItem(`recent-${index}`),
20
- onSelect: ()=>{
21
- if (fieldMeta) onSelect(fieldMeta);
22
- },
23
- children: /*#__PURE__*/ jsxs("span", {
24
- className: "flex gap-2 items-center text-sm",
25
- children: [
26
- /*#__PURE__*/ jsx("span", {
27
- className: "text-text-primary",
28
- children: attribute
29
- }),
30
- /*#__PURE__*/ jsx("span", {
31
- className: "text-text-secondary",
32
- children: operator
33
- }),
34
- /*#__PURE__*/ jsx("span", {
35
- className: "font-medium text-text-info",
36
- children: value
8
+ /*#__PURE__*/ jsxs("div", {
9
+ className: "flex flex-col gap-1",
10
+ children: [
11
+ /*#__PURE__*/ jsx(DropdownMenuLabel, {
12
+ sticky: true,
13
+ children: "Recent"
14
+ }),
15
+ /*#__PURE__*/ jsx(DropdownMenuGroup, {
16
+ children: conditions.map((condition, index)=>{
17
+ const fieldMeta = fields.find((f)=>f.name === condition.field);
18
+ const attribute = fieldMeta?.label || condition.field;
19
+ const operator = String(condition.operator);
20
+ const value = String(condition.value);
21
+ return /*#__PURE__*/ jsx(DropdownMenuItem, {
22
+ value: `recent-${index}`,
23
+ ref: registerItem(`recent-${index}`),
24
+ onSelect: ()=>{
25
+ if (fieldMeta) onSelect(fieldMeta);
26
+ },
27
+ children: /*#__PURE__*/ jsxs("span", {
28
+ className: "flex gap-2 items-center text-sm",
29
+ children: [
30
+ /*#__PURE__*/ jsx("span", {
31
+ className: "text-text-primary",
32
+ children: attribute
33
+ }),
34
+ /*#__PURE__*/ jsx("span", {
35
+ className: "text-text-secondary",
36
+ children: operator
37
+ }),
38
+ /*#__PURE__*/ jsx("span", {
39
+ className: "font-medium text-text-info",
40
+ children: value
41
+ })
42
+ ]
37
43
  })
38
- ]
44
+ }, `recent-${index}`);
39
45
  })
40
- }, `recent-${index}`);
41
- })
46
+ })
47
+ ]
42
48
  }),
43
49
  /*#__PURE__*/ jsx(DropdownMenuSeparator, {})
44
50
  ]
@@ -46,32 +52,38 @@ const RecentSection = ({ conditions, fields, onSelect, registerItem })=>/*#__PUR
46
52
  RecentSection.displayName = 'RecentSection';
47
53
  const SuggestionsSection = ({ fields, onSelect, registerItem })=>/*#__PURE__*/ jsxs(Fragment, {
48
54
  children: [
49
- /*#__PURE__*/ jsx(DropdownMenuLabel, {
50
- children: "Suggestions"
51
- }),
52
- /*#__PURE__*/ jsx(DropdownMenuGroup, {
53
- children: fields.map((field, index)=>/*#__PURE__*/ jsx(DropdownMenuItem, {
54
- value: `suggested-${index}`,
55
- ref: registerItem(`suggested-${index}`),
56
- onSelect: ()=>onSelect(field),
57
- children: /*#__PURE__*/ jsxs("span", {
58
- className: "flex gap-2 items-center text-sm",
59
- children: [
60
- /*#__PURE__*/ jsx("span", {
61
- className: "text-text-primary",
62
- children: field.label
63
- }),
64
- /*#__PURE__*/ jsx("span", {
65
- className: "text-text-secondary",
66
- children: "operator"
67
- }),
68
- /*#__PURE__*/ jsx("span", {
69
- className: "font-medium text-text-info",
70
- children: "Value"
55
+ /*#__PURE__*/ jsxs("div", {
56
+ className: "flex flex-col gap-1",
57
+ children: [
58
+ /*#__PURE__*/ jsx(DropdownMenuLabel, {
59
+ sticky: true,
60
+ children: "Suggestions"
61
+ }),
62
+ /*#__PURE__*/ jsx(DropdownMenuGroup, {
63
+ children: fields.map((field, index)=>/*#__PURE__*/ jsx(DropdownMenuItem, {
64
+ value: `suggested-${index}`,
65
+ ref: registerItem(`suggested-${index}`),
66
+ onSelect: ()=>onSelect(field),
67
+ children: /*#__PURE__*/ jsxs("span", {
68
+ className: "flex gap-2 items-center text-sm",
69
+ children: [
70
+ /*#__PURE__*/ jsx("span", {
71
+ className: "text-text-primary",
72
+ children: field.label
73
+ }),
74
+ /*#__PURE__*/ jsx("span", {
75
+ className: "text-text-secondary",
76
+ children: "operator"
77
+ }),
78
+ /*#__PURE__*/ jsx("span", {
79
+ className: "font-medium text-text-info",
80
+ children: "Value"
81
+ })
82
+ ]
71
83
  })
72
- ]
73
- })
74
- }, `suggested-${index}`))
84
+ }, `suggested-${index}`))
85
+ })
86
+ ]
75
87
  }),
76
88
  /*#__PURE__*/ jsx(DropdownMenuSeparator, {})
77
89
  ]
@@ -110,24 +122,34 @@ const OperatorsSection = ({ onSelectAnd, onSelectOr, registerItem })=>/*#__PURE_
110
122
  });
111
123
  OperatorsSection.displayName = 'OperatorsSection';
112
124
  const FieldSections = ({ sections, onSelect, registerItem })=>/*#__PURE__*/ jsx(Fragment, {
113
- children: sections.map((section, index)=>/*#__PURE__*/ jsxs(external_react_Fragment, {
125
+ children: sections.map((section, index)=>{
126
+ const group = /*#__PURE__*/ jsx(DropdownMenuGroup, {
127
+ children: section.fields.map((field)=>/*#__PURE__*/ jsx(DropdownMenuItem, {
128
+ value: `field-${field.name}`,
129
+ ref: registerItem(`field-${field.name}`),
130
+ onSelect: ()=>onSelect(field),
131
+ className: "scroll-mt-32",
132
+ children: /*#__PURE__*/ jsx(DropdownMenuItemText, {
133
+ children: field.label
134
+ })
135
+ }, field.name))
136
+ });
137
+ return /*#__PURE__*/ jsxs(external_react_Fragment, {
114
138
  children: [
115
139
  index > 0 && /*#__PURE__*/ jsx(DropdownMenuSeparator, {}),
116
- section.label && /*#__PURE__*/ jsx(DropdownMenuLabel, {
117
- children: section.label
118
- }),
119
- /*#__PURE__*/ jsx(DropdownMenuGroup, {
120
- children: section.fields.map((field)=>/*#__PURE__*/ jsx(DropdownMenuItem, {
121
- value: `field-${field.name}`,
122
- ref: registerItem(`field-${field.name}`),
123
- onSelect: ()=>onSelect(field),
124
- children: /*#__PURE__*/ jsx(DropdownMenuItemText, {
125
- children: field.label
126
- })
127
- }, field.name))
128
- })
140
+ section.label ? /*#__PURE__*/ jsxs("div", {
141
+ className: "flex flex-col gap-1",
142
+ children: [
143
+ /*#__PURE__*/ jsx(DropdownMenuLabel, {
144
+ sticky: true,
145
+ children: section.label
146
+ }),
147
+ group
148
+ ]
149
+ }) : group
129
150
  ]
130
- }, `${section.label ?? 'ungrouped'}-${index}`))
151
+ }, `${section.label ?? 'ungrouped'}-${index}`);
152
+ })
131
153
  });
132
154
  FieldSections.displayName = 'FieldSections';
133
155
  export { FieldSections, OperatorsSection, RecentSection, SuggestionsSection };
@@ -84,6 +84,7 @@ const FilterInputFieldMenu = ({ fields, filterText = '', onSelect, open = false,
84
84
  registerItem: registerItem
85
85
  }),
86
86
  /*#__PURE__*/ jsxs(DropdownMenuFooter, {
87
+ className: "justify-start",
87
88
  children: [
88
89
  /*#__PURE__*/ jsxs("span", {
89
90
  className: "flex items-center gap-4",
@@ -83,6 +83,7 @@ const FilterInputOperatorMenu = ({ fieldType, operators, onSelect, open = false,
83
83
  ]
84
84
  }, `group-${groupIdx}`)) : /*#__PURE__*/ jsx(MenuEmptyState, {}),
85
85
  /*#__PURE__*/ jsx(DropdownMenuFooter, {
86
+ className: "justify-start",
86
87
  children: /*#__PURE__*/ jsxs("span", {
87
88
  className: "flex items-center gap-4",
88
89
  children: [
@@ -55,6 +55,7 @@ const FlatValueMenu = ({ values, onSelect, onCommit, open = false, onOpenChange,
55
55
  "data-filter-input-menu": "true",
56
56
  children: [
57
57
  displayValues.length > 0 ? /*#__PURE__*/ jsx(DropdownMenuGroup, {
58
+ className: "flex flex-col gap-1",
58
59
  children: displayValues.map((option)=>/*#__PURE__*/ jsx(ValueMenuItem, {
59
60
  option: option,
60
61
  isChecked: selectedValues.some((v)=>String(v) === String(option.value)),
@@ -69,6 +70,7 @@ const FlatValueMenu = ({ values, onSelect, onCommit, open = false, onOpenChange,
69
70
  }, String(option.value)))
70
71
  }) : /*#__PURE__*/ jsx(MenuEmptyState, {}),
71
72
  /*#__PURE__*/ jsx(DropdownMenuFooter, {
73
+ className: "justify-start",
72
74
  children: /*#__PURE__*/ jsx(ValueMenuFooterHints, {
73
75
  multiSelect: multiSelect
74
76
  })
@@ -1,8 +1,7 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { Fragment as external_react_Fragment, useMemo, useRef } from "react";
3
3
  import { cn } from "../../../../utils/cn.js";
4
- import { DropdownMenu, DropdownMenuContent, DropdownMenuFooter, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from "../../../DropdownMenu/index.js";
5
- import { Text } from "../../../Text/index.js";
4
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuFooter, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuSeparator } from "../../../DropdownMenu/index.js";
6
5
  import { buildValueMenuSections } from "../../lib/buildValueMenuSections.js";
7
6
  import { MenuEmptyState } from "../MenuEmptyState.js";
8
7
  import { NestedValueMenuItem } from "./NestedValueMenuItem.js";
@@ -11,9 +10,10 @@ import { ValueMenuFooterHints } from "./ValueMenuFooterHints.js";
11
10
  const NestedValueMenu = ({ values, onSelect, onCommit, open = false, onOpenChange, onEscape, multiSelect = false, initialValues = [], width = 'standard', positioning, onBuildingValueChange, onItemToggle, inputRef, filterText = '', menuRef, blurCommitRef, className })=>{
12
11
  const submenuRef = useRef(null);
13
12
  const rowElsRef = useRef(new Map());
14
- const sections = useMemo(()=>buildValueMenuSections(values, filterText), [
13
+ const sections = useMemo(()=>buildValueMenuSections(values, filterText, multiSelect), [
15
14
  values,
16
- filterText
15
+ filterText,
16
+ multiSelect
17
17
  ]);
18
18
  const state = useNestedValueMenuState({
19
19
  sections,
@@ -43,13 +43,21 @@ const NestedValueMenu = ({ values, onSelect, onCommit, open = false, onOpenChang
43
43
  };
44
44
  const submenuPositioning = useMemo(()=>({
45
45
  placement: 'right-start',
46
- gutter: 8,
46
+ gutter: 4,
47
47
  getAnchorRect: ()=>state.openParentId && rowElsRef.current.get(state.openParentId)?.getBoundingClientRect() || null
48
48
  }), [
49
49
  state.openParentId
50
50
  ]);
51
51
  const openParentRow = state.openParentRow;
52
- const allChecked = openParentRow ? state.allLeavesChecked(openParentRow.option) : false;
52
+ const submenuSelectAllRow = openParentRow && multiSelect ? {
53
+ id: SELECT_ALL_ID,
54
+ option: {
55
+ label: `All ${openParentRow.option.label}`,
56
+ children: openParentRow.option.children ?? []
57
+ },
58
+ isGroup: true,
59
+ isSelectAll: true
60
+ } : null;
53
61
  const hasRows = sections.some((section)=>section.rows.length > 0);
54
62
  return /*#__PURE__*/ jsxs(Fragment, {
55
63
  children: [
@@ -65,31 +73,48 @@ const NestedValueMenu = ({ values, onSelect, onCommit, open = false, onOpenChang
65
73
  className: cn(widthClass, 'max-h-[430px]', className),
66
74
  style: widthStyle,
67
75
  "data-filter-input-menu": "true",
76
+ onMouseMove: state.handleMainMouseMove,
68
77
  children: [
69
- hasRows ? sections.map((section, index)=>/*#__PURE__*/ jsxs(external_react_Fragment, {
78
+ hasRows ? sections.map((section, index)=>{
79
+ const rows = /*#__PURE__*/ jsx(DropdownMenuGroup, {
80
+ className: "flex flex-col gap-1",
81
+ children: section.rows.map((row)=>{
82
+ const isParent = row.isGroup && !row.isSelectAll;
83
+ return /*#__PURE__*/ jsx(NestedValueMenuItem, {
84
+ id: row.id,
85
+ option: row.option,
86
+ isGroup: isParent,
87
+ checkedState: state.getRowCheckState(row),
88
+ isPending: state.topPendingIds.has(row.id),
89
+ multiSelect: multiSelect,
90
+ onSelect: ()=>state.selectRow(row, false),
91
+ onMouseEnter: isParent ? ()=>state.openParent(row.id) : void 0,
92
+ onMouseLeave: isParent ? ()=>state.scheduleClose() : void 0,
93
+ registerRef: setRowRef(row.id),
94
+ expanded: state.openParentId === row.id,
95
+ selectAll: row.isSelectAll,
96
+ highlight: filterText
97
+ }, row.id);
98
+ })
99
+ });
100
+ return /*#__PURE__*/ jsxs(external_react_Fragment, {
70
101
  children: [
71
102
  index > 0 && /*#__PURE__*/ jsx(DropdownMenuSeparator, {}),
72
- section.label && /*#__PURE__*/ jsx(DropdownMenuLabel, {
73
- children: section.label
74
- }),
75
- /*#__PURE__*/ jsx(DropdownMenuGroup, {
76
- children: section.rows.map((row)=>/*#__PURE__*/ jsx(NestedValueMenuItem, {
77
- id: row.id,
78
- option: row.option,
79
- isGroup: row.isGroup,
80
- checkedState: state.getRowCheckState(row),
81
- isPending: state.topPendingIds.has(row.id),
82
- multiSelect: multiSelect,
83
- onSelect: ()=>state.selectRow(row, false),
84
- onMouseEnter: row.isGroup ? ()=>state.openParent(row.id) : void 0,
85
- onMouseLeave: row.isGroup ? ()=>state.scheduleClose() : void 0,
86
- registerRef: setRowRef(row.id),
87
- expanded: state.openParentId === row.id
88
- }, row.id))
89
- })
103
+ section.label ? /*#__PURE__*/ jsxs("div", {
104
+ className: "flex flex-col gap-1",
105
+ children: [
106
+ /*#__PURE__*/ jsx(DropdownMenuLabel, {
107
+ sticky: true,
108
+ children: section.label
109
+ }),
110
+ rows
111
+ ]
112
+ }) : rows
90
113
  ]
91
- }, section.label ?? `section-${index}`)) : /*#__PURE__*/ jsx(MenuEmptyState, {}),
114
+ }, section.label ?? `section-${index}`);
115
+ }) : /*#__PURE__*/ jsx(MenuEmptyState, {}),
92
116
  /*#__PURE__*/ jsx(DropdownMenuFooter, {
117
+ className: "justify-start",
93
118
  children: /*#__PURE__*/ jsx(ValueMenuFooterHints, {
94
119
  multiSelect: multiSelect
95
120
  })
@@ -111,19 +136,20 @@ const NestedValueMenu = ({ values, onSelect, onCommit, open = false, onOpenChang
111
136
  onMouseEnter: state.cancelClose,
112
137
  onMouseLeave: state.scheduleClose,
113
138
  children: /*#__PURE__*/ jsxs(DropdownMenuGroup, {
139
+ className: "flex flex-col gap-1",
114
140
  children: [
115
- /*#__PURE__*/ jsx(DropdownMenuItem, {
116
- value: SELECT_ALL_ID,
117
- ref: state.registerSubmenuItem(SELECT_ALL_ID),
141
+ submenuSelectAllRow && /*#__PURE__*/ jsx(NestedValueMenuItem, {
142
+ id: SELECT_ALL_ID,
143
+ option: submenuSelectAllRow.option,
144
+ isGroup: false,
145
+ checkedState: state.getRowCheckState(submenuSelectAllRow),
146
+ isPending: state.submenuPendingIds.has(SELECT_ALL_ID),
147
+ multiSelect: multiSelect,
118
148
  onSelect: ()=>state.toggleGroup(openParentRow.option),
119
- className: state.submenuPendingIds.has(SELECT_ALL_ID) ? 'bg-states-primary-hover' : void 0,
120
- children: /*#__PURE__*/ jsx(Text, {
121
- size: "sm",
122
- color: "secondary",
123
- children: allChecked ? 'Unselect all' : 'Select all'
124
- })
149
+ registerRef: state.registerSubmenuItem(SELECT_ALL_ID),
150
+ selectAll: true
125
151
  }),
126
- /*#__PURE__*/ jsx(DropdownMenuSeparator, {}),
152
+ submenuSelectAllRow && /*#__PURE__*/ jsx(DropdownMenuSeparator, {}),
127
153
  state.submenuRows.map((row)=>/*#__PURE__*/ jsx(NestedValueMenuItem, {
128
154
  id: row.id,
129
155
  option: row.option,
@@ -15,12 +15,20 @@ interface NestedValueMenuItemProps {
15
15
  registerRef?: (el: HTMLElement | null) => void;
16
16
  /** For group rows: whether its submenu is currently open. */
17
17
  expanded?: boolean;
18
+ /**
19
+ * True for the synthetic "All {group}" bulk-select row: its label renders in a
20
+ * medium weight to read as a section-level control rather than a plain option.
21
+ */
22
+ selectAll?: boolean;
23
+ /** Active search query — its matched substring is emphasized in flat search. */
24
+ highlight?: string;
18
25
  }
19
26
  /**
20
- * A single row in the nested value menu — a committable leaf or a group
21
- * (parent category). Both render a (tri-state, for groups) checkbox; a group's
22
- * submenu opens on hover. No chevron: selection and bulk-selection are driven
23
- * entirely by the checkbox.
27
+ * A single row in the nested value menu — a committable leaf, a group (parent
28
+ * category), or the "All {group}" bulk-select row. Parent categories show a
29
+ * right-side chevron and open a submenu on hover; a checked row is highlighted
30
+ * with a persistent background. Selection is driven by the (tri-state, for
31
+ * groups) checkbox.
24
32
  */
25
33
  export declare const NestedValueMenuItem: FC<NestedValueMenuItemProps>;
26
34
  export {};
@@ -1,15 +1,48 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { ChevronRight } from "../../../../icons/ChevronRight.js";
3
+ import { cn } from "../../../../utils/cn.js";
2
4
  import { Badge } from "../../../Badge/index.js";
3
5
  import { Checkmark } from "../../../Checkmark/index.js";
4
6
  import { DropdownMenuItem } from "../../../DropdownMenu/index.js";
5
7
  import { Text } from "../../../Text/index.js";
6
- const NestedValueMenuItem = ({ id, option, isGroup, checkedState, isPending, multiSelect, onSelect, onMouseEnter, onMouseLeave, registerRef, expanded })=>/*#__PURE__*/ jsxs(DropdownMenuItem, {
8
+ const highlightMatch = (text, query)=>{
9
+ const q = query?.trim().toLowerCase();
10
+ if (!q) return text;
11
+ const lower = text.toLowerCase();
12
+ const parts = [];
13
+ let cursor = 0;
14
+ let key = 0;
15
+ while(cursor < text.length){
16
+ const at = lower.indexOf(q, cursor);
17
+ if (-1 === at) {
18
+ parts.push(/*#__PURE__*/ jsx("span", {
19
+ children: text.slice(cursor)
20
+ }, key++));
21
+ break;
22
+ }
23
+ if (at > cursor) parts.push(/*#__PURE__*/ jsx("span", {
24
+ children: text.slice(cursor, at)
25
+ }, key++));
26
+ parts.push(/*#__PURE__*/ jsx("mark", {
27
+ className: "bg-transparent font-medium text-text-brand",
28
+ children: text.slice(at, at + q.length)
29
+ }, key++));
30
+ cursor = at + q.length;
31
+ }
32
+ return parts;
33
+ };
34
+ const NestedValueMenuItem = ({ id, option, isGroup, checkedState, isPending, multiSelect, onSelect, onMouseEnter, onMouseLeave, registerRef, expanded, selectAll, highlight })=>{
35
+ const isChecked = true === checkedState;
36
+ const checkmark = multiSelect || !isGroup && checkedState ? /*#__PURE__*/ jsx(Checkmark, {
37
+ checkedState: multiSelect ? checkedState : true
38
+ }) : null;
39
+ return /*#__PURE__*/ jsxs(DropdownMenuItem, {
7
40
  value: id,
8
41
  ref: registerRef,
9
42
  onSelect: onSelect,
10
43
  onMouseEnter: onMouseEnter,
11
44
  onMouseLeave: onMouseLeave,
12
- className: isPending ? 'bg-states-primary-hover' : void 0,
45
+ className: cn('scroll-mt-32', isChecked && 'bg-states-primary-active', isPending && 'bg-states-primary-hover'),
13
46
  "aria-checked": multiSelect ? 'indeterminate' === checkedState ? 'mixed' : !!checkedState : void 0,
14
47
  ...isGroup && {
15
48
  'aria-haspopup': 'menu',
@@ -26,29 +59,30 @@ const NestedValueMenuItem = ({ id, option, isGroup, checkedState, isPending, mul
26
59
  children: [
27
60
  /*#__PURE__*/ jsx(Text, {
28
61
  size: "sm",
62
+ weight: selectAll ? 'medium' : 'regular',
29
63
  truncate: true,
30
- children: option.label
64
+ children: highlightMatch(option.label, highlight)
31
65
  }),
32
66
  option.description && /*#__PURE__*/ jsx(Text, {
33
67
  size: "xs",
34
68
  color: "secondary",
35
69
  truncate: true,
36
- children: option.description
70
+ children: highlightMatch(option.description, highlight)
37
71
  })
38
72
  ]
39
73
  }),
40
- multiSelect ? /*#__PURE__*/ jsx("div", {
41
- className: "flex shrink-0 items-start justify-end py-2 ml-auto",
42
- children: /*#__PURE__*/ jsx(Checkmark, {
43
- checkedState: checkedState
44
- })
45
- }) : !isGroup && checkedState ? /*#__PURE__*/ jsx("div", {
46
- className: "flex shrink-0 items-start justify-end py-2 ml-auto",
47
- children: /*#__PURE__*/ jsx(Checkmark, {
48
- checkedState: true
49
- })
50
- }) : null
74
+ /*#__PURE__*/ jsxs("div", {
75
+ className: "flex shrink-0 items-center justify-end gap-8 ml-auto",
76
+ children: [
77
+ isGroup && /*#__PURE__*/ jsx("span", {
78
+ className: "flex text-text-secondary",
79
+ children: /*#__PURE__*/ jsx(ChevronRight, {})
80
+ }),
81
+ checkmark
82
+ ]
83
+ })
51
84
  ]
52
85
  });
86
+ };
53
87
  NestedValueMenuItem.displayName = 'NestedValueMenuItem';
54
88
  export { NestedValueMenuItem };
@@ -32,12 +32,12 @@ const ValueMenuItem = ({ option, isChecked, isPending, multiSelect, onSelect, re
32
32
  ]
33
33
  }),
34
34
  multiSelect ? /*#__PURE__*/ jsx("div", {
35
- className: "flex shrink-0 items-start justify-end py-2 ml-auto",
35
+ className: "flex shrink-0 items-start justify-end ml-auto",
36
36
  children: /*#__PURE__*/ jsx(Checkmark, {
37
37
  checkedState: isChecked
38
38
  })
39
39
  }) : isChecked ? /*#__PURE__*/ jsx("div", {
40
- className: "flex shrink-0 items-start justify-end py-2 ml-auto",
40
+ className: "flex shrink-0 items-start justify-end ml-auto",
41
41
  children: /*#__PURE__*/ jsx(Checkmark, {
42
42
  checkedState: true
43
43
  })
@@ -41,6 +41,10 @@ export declare const useNestedValueMenuState: ({ sections, allValues, open, mult
41
41
  closeParent: () => void;
42
42
  cancelClose: () => void;
43
43
  scheduleClose: () => void;
44
+ handleMainMouseMove: (event: {
45
+ clientX: number;
46
+ clientY: number;
47
+ }) => void;
44
48
  openParentRow: ValueMenuRow | undefined;
45
49
  submenuRows: ValueMenuRow[];
46
50
  selectRow: (row: ValueMenuRow, fromSubmenu: boolean) => void;
@@ -32,7 +32,7 @@ const useNestedValueMenuState = ({ sections, allValues, open, multiSelect, initi
32
32
  ]);
33
33
  const topRowsRef = useRef(topRows);
34
34
  topRowsRef.current = topRows;
35
- const openParentRow = useMemo(()=>topRows.find((row)=>row.id === openParentId && row.isGroup), [
35
+ const openParentRow = useMemo(()=>topRows.find((row)=>row.id === openParentId && row.isGroup && !row.isSelectAll), [
36
36
  topRows,
37
37
  openParentId
38
38
  ]);
@@ -123,7 +123,7 @@ const useNestedValueMenuState = ({ sections, allValues, open, multiSelect, initi
123
123
  closeTimerRef.current = setTimeout(()=>{
124
124
  closeTimerRef.current = null;
125
125
  setOpenParentId(null);
126
- }, 220);
126
+ }, 250);
127
127
  }, [
128
128
  cancelClose
129
129
  ]);
@@ -142,6 +142,25 @@ const useNestedValueMenuState = ({ sections, allValues, open, multiSelect, initi
142
142
  useEffect(()=>()=>cancelClose(), [
143
143
  cancelClose
144
144
  ]);
145
+ const lastPointRef = useRef(null);
146
+ const handleMainMouseMove = useCallback((event)=>{
147
+ const prev = lastPointRef.current;
148
+ const point = {
149
+ x: event.clientX,
150
+ y: event.clientY
151
+ };
152
+ lastPointRef.current = point;
153
+ if (null == openParentId || !prev) return;
154
+ const sub = submenuRef?.current?.getBoundingClientRect();
155
+ if (!sub) return;
156
+ const headingRight = point.x >= prev.x;
157
+ const insideBand = point.y >= sub.top - 12 && point.y <= sub.bottom + 12;
158
+ if (headingRight && insideBand && point.x <= sub.left) cancelClose();
159
+ }, [
160
+ openParentId,
161
+ submenuRef,
162
+ cancelClose
163
+ ]);
145
164
  const toggleGroup = useCallback((option)=>{
146
165
  const leaves = collectLeaves(option.children).map((l)=>l.value);
147
166
  toggleMany(leaves, !allLeavesChecked(option));
@@ -153,7 +172,7 @@ const useNestedValueMenuState = ({ sections, allValues, open, multiSelect, initi
153
172
  ]);
154
173
  const selectRow = useCallback((row, fromSubmenu)=>{
155
174
  if (row.isGroup) {
156
- if (multiSelect) toggleGroup(row.option);
175
+ if (multiSelect || row.isSelectAll) toggleGroup(row.option);
157
176
  else if (!fromSubmenu) openParent(row.id);
158
177
  return;
159
178
  }
@@ -194,7 +213,7 @@ const useNestedValueMenuState = ({ sections, allValues, open, multiSelect, initi
194
213
  ]);
195
214
  const handleTopArrowRight = useCallback(()=>{
196
215
  const row = topRowsRef.current.find((r)=>r.id === topHighlightRef.current);
197
- if (row?.isGroup) openParent(row.id);
216
+ if (row?.isGroup && !row.isSelectAll) openParent(row.id);
198
217
  else if (multiSelect) commitChecked();
199
218
  }, [
200
219
  openParent,
@@ -214,21 +233,23 @@ const useNestedValueMenuState = ({ sections, allValues, open, multiSelect, initi
214
233
  topHighlightRef.current = top.highlightedValue;
215
234
  const submenuNavItems = useMemo(()=>{
216
235
  if (!openParentRow) return [];
217
- return [
236
+ const leafItems = submenuRows.map((row)=>({
237
+ id: row.id,
238
+ label: row.option.label,
239
+ value: row.option.value
240
+ }));
241
+ return multiSelect ? [
218
242
  {
219
243
  id: SELECT_ALL_ID,
220
244
  label: 'Select all',
221
245
  value: SELECT_ALL_ID
222
246
  },
223
- ...submenuRows.map((row)=>({
224
- id: row.id,
225
- label: row.option.label,
226
- value: row.option.value
227
- }))
228
- ];
247
+ ...leafItems
248
+ ] : leafItems;
229
249
  }, [
230
250
  openParentRow,
231
- submenuRows
251
+ submenuRows,
252
+ multiSelect
232
253
  ]);
233
254
  const handleSubmenuSelect = useCallback((item)=>{
234
255
  if (!openParentRow) return;
@@ -272,6 +293,7 @@ const useNestedValueMenuState = ({ sections, allValues, open, multiSelect, initi
272
293
  closeParent,
273
294
  cancelClose,
274
295
  scheduleClose,
296
+ handleMainMouseMove,
275
297
  openParentRow,
276
298
  submenuRows,
277
299
  selectRow,
@@ -9,12 +9,21 @@ export interface ValueMenuRow {
9
9
  id: string;
10
10
  option: FieldValueOption;
11
11
  isGroup: boolean;
12
+ /**
13
+ * True for the synthetic **"All {group}"** bulk-select row rendered at the top
14
+ * of a labeled section. It is a group row for check-state/toggle purposes (its
15
+ * `option.children` are the section's full scope) but is NOT a submenu trigger:
16
+ * it never opens a right-side panel, it only toggles its descendant leaves.
17
+ */
18
+ isSelectAll?: boolean;
12
19
  }
13
20
  /** A render-ready value-menu section. `label` undefined = headerless. */
14
21
  export interface ValueMenuSection {
15
22
  label?: string;
16
23
  rows: ValueMenuRow[];
17
24
  }
25
+ /** Joins ancestry labels for a flat-search row's muted second line. */
26
+ export declare const PATH_SEPARATOR = " \u203A ";
18
27
  /**
19
28
  * Bucket value options into ordered, filtered menu sections.
20
29
  *
@@ -27,7 +36,14 @@ export interface ValueMenuSection {
27
36
  * 3. **Flat** — no grouping: a single headerless section (today's behavior, so
28
37
  * existing fields are visually unchanged).
29
38
  *
30
- * Each section is filtered by `filterText`; sections with no surviving rows are
31
- * dropped. A group row survives if its label OR any descendant leaf matches.
39
+ * A non-empty `filterText` **flattens** a grouped tree into a single flat list
40
+ * of matches at any depth (see {@link buildFlatSearchSections}) hover submenus
41
+ * are unreachable while typing, so deep values are surfaced directly with their
42
+ * ancestry path. With an empty query the grouped structure below is returned.
43
+ *
44
+ * When `multiSelect` is set, every **labeled** section is prefixed with a
45
+ * synthetic "All {label}" bulk-select row (see {@link selectAllRow}) so the user
46
+ * can select/deselect the whole group's scope in one click; single-select menus
47
+ * omit it (bulk selection is meaningless there).
32
48
  */
33
- export declare function buildValueMenuSections(values: FieldValueOption[], filterText: string): ValueMenuSection[];
49
+ export declare function buildValueMenuSections(values: FieldValueOption[], filterText: string, multiSelect?: boolean): ValueMenuSection[];
@@ -15,7 +15,70 @@ const toRows = (options, filterText)=>filterAndSort(options, filterText, rowSear
15
15
  option,
16
16
  isGroup: isValueGroup(option)
17
17
  }));
18
- function buildValueMenuSections(values, filterText) {
18
+ const selectAllRow = (label, scope)=>({
19
+ id: `select-all:${label}`,
20
+ option: {
21
+ label: `All ${label}`,
22
+ children: scope
23
+ },
24
+ isGroup: true,
25
+ isSelectAll: true
26
+ });
27
+ const PATH_SEPARATOR = ' › ';
28
+ const matchesQuery = (value, query)=>null != value && String(value).toLowerCase().includes(query);
29
+ function buildFlatSearchSections(values, query, multiSelect) {
30
+ const shortcuts = [];
31
+ const leaves = [];
32
+ const walk = (options, path)=>{
33
+ for (const option of options)if (isValueGroup(option)) {
34
+ if (multiSelect && matchesQuery(option.label, query)) shortcuts.push({
35
+ id: `select-all:${[
36
+ ...path,
37
+ option.label
38
+ ].join('/')}`,
39
+ option: {
40
+ ...option,
41
+ label: `All ${option.label}`,
42
+ description: path.join(PATH_SEPARATOR) || void 0
43
+ },
44
+ isGroup: true,
45
+ isSelectAll: true
46
+ });
47
+ walk(option.children ?? [], [
48
+ ...path,
49
+ option.label
50
+ ]);
51
+ } else if (matchesQuery(option.label, query) || matchesQuery(option.value, query) || matchesQuery(option.description, query)) {
52
+ const ancestry = path.length > 0 ? path.join(PATH_SEPARATOR) : option.section;
53
+ leaves.push({
54
+ id: `leaf:${String(option.value)}`,
55
+ option: {
56
+ ...option,
57
+ description: ancestry || void 0
58
+ },
59
+ isGroup: false
60
+ });
61
+ }
62
+ };
63
+ walk(values, []);
64
+ const rows = [
65
+ ...shortcuts,
66
+ ...leaves
67
+ ];
68
+ return rows.length > 0 ? [
69
+ {
70
+ rows
71
+ }
72
+ ] : [];
73
+ }
74
+ function buildValueMenuSections(values, filterText, multiSelect = false) {
75
+ const query = filterText.trim().toLowerCase();
76
+ const hasHierarchy = values.some((v)=>isValueGroup(v) || null != v.section);
77
+ if (query.length > 0 && hasHierarchy) return buildFlatSearchSections(values, query, multiSelect);
78
+ const withSelectAll = (label, scope, rows)=>multiSelect ? [
79
+ selectAllRow(label, scope),
80
+ ...rows
81
+ ] : rows;
19
82
  const topGroups = values.filter(isValueGroup);
20
83
  if (topGroups.length > 0) {
21
84
  const sections = [];
@@ -25,10 +88,11 @@ function buildValueMenuSections(values, filterText) {
25
88
  rows: leafRows
26
89
  });
27
90
  for (const group of topGroups){
28
- const rows = toRows(group.children ?? [], filterText);
91
+ const children = group.children ?? [];
92
+ const rows = toRows(children, filterText);
29
93
  if (rows.length > 0) sections.push({
30
94
  label: group.label,
31
- rows
95
+ rows: withSelectAll(group.label, children, rows)
32
96
  });
33
97
  }
34
98
  return sections;
@@ -50,10 +114,11 @@ function buildValueMenuSections(values, filterText) {
50
114
  rows: unsectionedRows
51
115
  });
52
116
  for (const label of order){
53
- const rows = toRows(buckets.get(label), filterText);
117
+ const bucket = buckets.get(label);
118
+ const rows = toRows(bucket, filterText);
54
119
  if (rows.length > 0) sections.push({
55
120
  label,
56
- rows
121
+ rows: withSelectAll(label, bucket, rows)
57
122
  });
58
123
  }
59
124
  return sections;
@@ -65,4 +130,4 @@ function buildValueMenuSections(values, filterText) {
65
130
  }
66
131
  ] : [];
67
132
  }
68
- export { buildValueMenuSections };
133
+ export { PATH_SEPARATOR, buildValueMenuSections };
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.81.0",
3
- "generatedAt": "2026-08-04T09:35:49.157Z",
2
+ "version": "0.82.0",
3
+ "generatedAt": "2026-08-05T07:53:56.679Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
@@ -25561,6 +25561,12 @@
25561
25561
  "options": [
25562
25562
  "true"
25563
25563
  ]
25564
+ },
25565
+ {
25566
+ "name": "sticky",
25567
+ "options": [
25568
+ "true"
25569
+ ]
25564
25570
  }
25565
25571
  ],
25566
25572
  "subComponents": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "0.82.0",
3
+ "version": "0.83.0",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",