@wallarm-org/design-system 0.78.1 → 0.79.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 (33) hide show
  1. package/dist/components/FilterInput/FilterInputErrors/parseFilterInputErrors.js +2 -2
  2. package/dist/components/FilterInput/FilterInputField/ChipsWithGaps.js +1 -1
  3. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/FilterInputValueMenu.d.ts +21 -3
  4. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/FilterInputValueMenu.js +10 -75
  5. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/FlatValueMenu.d.ts +8 -0
  6. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/FlatValueMenu.js +81 -0
  7. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/NestedValueMenu.d.ts +10 -0
  8. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/NestedValueMenu.js +145 -0
  9. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/NestedValueMenuItem.d.ts +26 -0
  10. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/NestedValueMenuItem.js +54 -0
  11. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/ValueMenuFooterHints.d.ts +12 -0
  12. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/ValueMenuFooterHints.js +69 -0
  13. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/useNestedValueMenuState.d.ts +65 -0
  14. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/useNestedValueMenuState.js +293 -0
  15. package/dist/components/FilterInput/FilterInputMenu/hooks/useKeyboardNav.d.ts +3 -1
  16. package/dist/components/FilterInput/FilterInputMenu/hooks/useKeyboardNav.js +20 -1
  17. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/lib/deriveAutocompleteValues.js +3 -3
  18. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/lib/valueResolution.js +5 -5
  19. package/dist/components/FilterInput/hooks/useFilterInputExpression/buildChips.js +2 -2
  20. package/dist/components/FilterInput/lib/buildValueMenuSections.d.ts +33 -0
  21. package/dist/components/FilterInput/lib/buildValueMenuSections.js +68 -0
  22. package/dist/components/FilterInput/lib/collapseValuesToLabels.d.ts +32 -0
  23. package/dist/components/FilterInput/lib/collapseValuesToLabels.js +55 -0
  24. package/dist/components/FilterInput/lib/fields.d.ts +19 -7
  25. package/dist/components/FilterInput/lib/fields.js +18 -4
  26. package/dist/components/FilterInput/lib/index.d.ts +3 -1
  27. package/dist/components/FilterInput/lib/index.js +3 -1
  28. package/dist/components/FilterInput/lib/validation.js +2 -2
  29. package/dist/components/FilterInput/types.d.ts +31 -2
  30. package/dist/metadata/components.json +2 -2
  31. package/package.json +1 -1
  32. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/ValueMenuFooter.d.ts +0 -6
  33. package/dist/components/FilterInput/FilterInputMenu/FilterInputValueMenu/ValueMenuFooter.js +0 -72
@@ -1,5 +1,5 @@
1
1
  import { SEGMENT_VARIANT } from "../FilterInputField/FilterInputChip/index.js";
2
- import { getFieldValues, hasStaticAllowlist, incompleteTripletError, isEmptyFilterValue, isNoValueOperator, isValidFieldValue } from "../lib/index.js";
2
+ import { collectLeaves, 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;
@@ -34,7 +34,7 @@ const parseFilterInputErrors = (conditions, fields)=>{
34
34
  }
35
35
  }
36
36
  if (field && hasStaticAllowlist(field) && Array.isArray(condition.value)) {
37
- const fv = getFieldValues(field);
37
+ const fv = collectLeaves(getFieldValues(field));
38
38
  if (fv.length > 0) {
39
39
  const invalidValues = condition.value.filter((v)=>!isValidFieldValue(fv, v));
40
40
  if (invalidValues.length > 0) {
@@ -18,7 +18,7 @@ const ChipsWithGaps = ({ chips, hideLeadingGap, hideTrailingGap, onChipClick, on
18
18
  const isConnector = 'and' === chip.variant || 'or' === chip.variant;
19
19
  if (isCondition) elements.push(/*#__PURE__*/ jsx("div", {
20
20
  ref: chipRef(chip.id),
21
- className: "shrink-0 cursor-pointer hover:z-10",
21
+ className: "shrink-0 cursor-pointer hover:z-30",
22
22
  children: /*#__PURE__*/ jsx(FilterInputChip, {
23
23
  chipId: chip.id,
24
24
  attribute: chip.attribute ?? '',
@@ -1,7 +1,11 @@
1
- import { type FC, type RefObject } from 'react';
1
+ import type { FC, RefObject } from 'react';
2
2
  import type { BadgeColor } from '../../../Badge';
3
3
  export interface ValueOption {
4
- value: string | number | boolean;
4
+ /**
5
+ * Committable value — present on **leaf** options only. Group options (those
6
+ * with `children`) omit it; they are presentational and never committed.
7
+ */
8
+ value?: string | number | boolean;
5
9
  label: string;
6
10
  badge?: {
7
11
  color: BadgeColor;
@@ -9,12 +13,20 @@ export interface ValueOption {
9
13
  };
10
14
  /** Muted secondary line rendered beneath the bold `label`. Display-only. */
11
15
  description?: string;
16
+ /**
17
+ * Nested sub-values. When present this option is a group: a section header at
18
+ * the top level, or a submenu trigger when nested. Only leaves are committed.
19
+ */
20
+ children?: ValueOption[];
21
+ /** Optional section-header label to bucket top-level options under a heading. */
22
+ section?: string;
23
+ /** @deprecated superseded by `children`; kept for back-compat. */
12
24
  hasSubmenu?: boolean;
13
25
  }
14
26
  type ConditionValue = string | number | boolean;
15
27
  export interface FilterInputValueMenuProps {
16
28
  values: ValueOption[];
17
- onSelect: (value: ValueOption['value']) => void;
29
+ onSelect: (value: ConditionValue) => void;
18
30
  onCommit?: (values: ConditionValue[]) => void;
19
31
  open?: boolean;
20
32
  onOpenChange?: (open: boolean) => void;
@@ -37,5 +49,11 @@ export interface FilterInputValueMenuProps {
37
49
  blurCommitRef?: RefObject<(() => boolean) | null>;
38
50
  className?: string;
39
51
  }
52
+ /**
53
+ * Value dropdown (third autocomplete step). Dispatches to the nested menu
54
+ * (sections + submenu) when the field's values contain groups or `section`
55
+ * tags, otherwise to the flat single-list menu. Owns no hooks itself so each
56
+ * variant's hooks mount/unmount cleanly.
57
+ */
40
58
  export declare const FilterInputValueMenu: FC<FilterInputValueMenuProps>;
41
59
  export {};
@@ -1,78 +1,13 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useMemo } from "react";
3
- import { cn } from "../../../../utils/cn.js";
4
- import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup } from "../../../DropdownMenu/index.js";
5
- import { filterAndSort } from "../../lib/index.js";
6
- import { MenuEmptyState } from "../MenuEmptyState.js";
7
- import { useValueMenuDisplayValues } from "./useValueMenuDisplayValues.js";
8
- import { useValueMenuState } from "./useValueMenuState.js";
9
- import { ValueMenuFooter } from "./ValueMenuFooter.js";
10
- import { ValueMenuItem } from "./ValueMenuItem.js";
11
- import { valueOptionSearchText } from "./valueOptionSearchText.js";
12
- const FilterInputValueMenu = ({ values, onSelect, onCommit, open = false, onOpenChange, onEscape, multiSelect = false, initialValues = [], highlightValue, width = 'standard', positioning, onBuildingValueChange, onItemToggle, inputRef, filterText = '', menuRef, blurCommitRef, className })=>{
13
- const filteredValues = useMemo(()=>filterAndSort(values, filterText, valueOptionSearchText), [
14
- values,
15
- filterText
16
- ]);
17
- const { selectedValues, checkedValues, highlightedValue, onHighlightChange, pendingIds, registerItem, handleItemSelect } = useValueMenuState({
18
- values: filteredValues,
19
- open,
20
- multiSelect,
21
- initialValues,
22
- highlightValue,
23
- onSelect,
24
- onCommit,
25
- onEscape,
26
- onOpenChange,
27
- onBuildingValueChange,
28
- onItemToggle,
29
- inputRef,
30
- menuRef,
31
- blurCommitRef
32
- });
33
- const displayValues = useValueMenuDisplayValues({
34
- values,
35
- filteredValues,
36
- multiSelect,
37
- checkedValues,
38
- highlightValue
39
- });
40
- const widthClass = 'compact' === width ? 'w-[172px]' : 'w-[300px]';
41
- const widthStyle = 'number' == typeof width ? {
42
- width: `${width}px`
43
- } : void 0;
44
- return /*#__PURE__*/ jsx(DropdownMenu, {
45
- open: open,
46
- onOpenChange: onOpenChange,
47
- closeOnSelect: false,
48
- positioning: positioning,
49
- highlightedValue: highlightedValue,
50
- onHighlightChange: onHighlightChange,
51
- children: /*#__PURE__*/ jsxs(DropdownMenuContent, {
52
- ref: menuRef,
53
- className: cn(widthClass, 'max-h-[430px]', className),
54
- style: widthStyle,
55
- "data-filter-input-menu": "true",
56
- children: [
57
- displayValues.length > 0 ? /*#__PURE__*/ jsx(DropdownMenuGroup, {
58
- children: displayValues.map((option)=>/*#__PURE__*/ jsx(ValueMenuItem, {
59
- option: option,
60
- isChecked: selectedValues.some((v)=>String(v) === String(option.value)),
61
- isPending: pendingIds.has(String(option.value)),
62
- multiSelect: multiSelect,
63
- registerItem: registerItem,
64
- onSelect: ()=>handleItemSelect({
65
- id: String(option.value),
66
- label: option.label,
67
- value: option.value
68
- })
69
- }, String(option.value)))
70
- }) : /*#__PURE__*/ jsx(MenuEmptyState, {}),
71
- /*#__PURE__*/ jsx(ValueMenuFooter, {
72
- multiSelect: multiSelect
73
- })
74
- ]
75
- })
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { isValueGroup } from "../../lib/index.js";
3
+ import { FlatValueMenu } from "./FlatValueMenu.js";
4
+ import { NestedValueMenu } from "./NestedValueMenu.js";
5
+ const FilterInputValueMenu = (props)=>{
6
+ const isNested = props.values.some((v)=>isValueGroup(v) || null != v.section);
7
+ return isNested ? /*#__PURE__*/ jsx(NestedValueMenu, {
8
+ ...props
9
+ }) : /*#__PURE__*/ jsx(FlatValueMenu, {
10
+ ...props
76
11
  });
77
12
  };
78
13
  FilterInputValueMenu.displayName = 'FilterInputValueMenu';
@@ -0,0 +1,8 @@
1
+ import { type FC } from 'react';
2
+ import type { FilterInputValueMenuProps } from './FilterInputValueMenu';
3
+ /**
4
+ * Flat (non-nested) value menu — the original single-list implementation.
5
+ * Rendered by `FilterInputValueMenu` for fields whose values have no groups or
6
+ * sections. Behavior is unchanged from before nesting was introduced.
7
+ */
8
+ export declare const FlatValueMenu: FC<FilterInputValueMenuProps>;
@@ -0,0 +1,81 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { cn } from "../../../../utils/cn.js";
4
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuFooter, DropdownMenuGroup } from "../../../DropdownMenu/index.js";
5
+ import { filterAndSort } from "../../lib/index.js";
6
+ import { MenuEmptyState } from "../MenuEmptyState.js";
7
+ import { useValueMenuDisplayValues } from "./useValueMenuDisplayValues.js";
8
+ import { useValueMenuState } from "./useValueMenuState.js";
9
+ import { ValueMenuFooterHints } from "./ValueMenuFooterHints.js";
10
+ import { ValueMenuItem } from "./ValueMenuItem.js";
11
+ import { valueOptionSearchText } from "./valueOptionSearchText.js";
12
+ const FlatValueMenu = ({ values, onSelect, onCommit, open = false, onOpenChange, onEscape, multiSelect = false, initialValues = [], highlightValue, width = 'standard', positioning, onBuildingValueChange, onItemToggle, inputRef, filterText = '', menuRef, blurCommitRef, className })=>{
13
+ const filteredValues = useMemo(()=>filterAndSort(values, filterText, valueOptionSearchText), [
14
+ values,
15
+ filterText
16
+ ]);
17
+ const { selectedValues, checkedValues, highlightedValue, onHighlightChange, pendingIds, registerItem, handleItemSelect } = useValueMenuState({
18
+ values: filteredValues,
19
+ open,
20
+ multiSelect,
21
+ initialValues,
22
+ highlightValue,
23
+ onSelect,
24
+ onCommit,
25
+ onEscape,
26
+ onOpenChange,
27
+ onBuildingValueChange,
28
+ onItemToggle,
29
+ inputRef,
30
+ menuRef,
31
+ blurCommitRef
32
+ });
33
+ const displayValues = useValueMenuDisplayValues({
34
+ values,
35
+ filteredValues,
36
+ multiSelect,
37
+ checkedValues,
38
+ highlightValue
39
+ });
40
+ const widthClass = 'compact' === width ? 'w-[172px]' : 'w-[300px]';
41
+ const widthStyle = 'number' == typeof width ? {
42
+ width: `${width}px`
43
+ } : void 0;
44
+ return /*#__PURE__*/ jsx(DropdownMenu, {
45
+ open: open,
46
+ onOpenChange: onOpenChange,
47
+ closeOnSelect: false,
48
+ positioning: positioning,
49
+ highlightedValue: highlightedValue,
50
+ onHighlightChange: onHighlightChange,
51
+ children: /*#__PURE__*/ jsxs(DropdownMenuContent, {
52
+ ref: menuRef,
53
+ className: cn(widthClass, 'max-h-[430px]', className),
54
+ style: widthStyle,
55
+ "data-filter-input-menu": "true",
56
+ children: [
57
+ displayValues.length > 0 ? /*#__PURE__*/ jsx(DropdownMenuGroup, {
58
+ children: displayValues.map((option)=>/*#__PURE__*/ jsx(ValueMenuItem, {
59
+ option: option,
60
+ isChecked: selectedValues.some((v)=>String(v) === String(option.value)),
61
+ isPending: pendingIds.has(String(option.value)),
62
+ multiSelect: multiSelect,
63
+ registerItem: registerItem,
64
+ onSelect: ()=>handleItemSelect({
65
+ id: String(option.value),
66
+ label: option.label,
67
+ value: option.value
68
+ })
69
+ }, String(option.value)))
70
+ }) : /*#__PURE__*/ jsx(MenuEmptyState, {}),
71
+ /*#__PURE__*/ jsx(DropdownMenuFooter, {
72
+ children: /*#__PURE__*/ jsx(ValueMenuFooterHints, {
73
+ multiSelect: multiSelect
74
+ })
75
+ })
76
+ ]
77
+ })
78
+ });
79
+ };
80
+ FlatValueMenu.displayName = 'FlatValueMenu';
81
+ export { FlatValueMenu };
@@ -0,0 +1,10 @@
1
+ import { type FC } from 'react';
2
+ import type { FilterInputValueMenuProps } from './FilterInputValueMenu';
3
+ /**
4
+ * Value menu with nesting: top-level options are grouped under section headers
5
+ * and a parent category opens a right-side submenu of its leaf sub-values
6
+ * (multi-select). Only committable leaves reach the expression — sections and
7
+ * parent categories are presentational. Rendered by `FilterInputValueMenu` when
8
+ * the field's values contain groups/sections; the flat menu handles the rest.
9
+ */
10
+ export declare const NestedValueMenu: FC<FilterInputValueMenuProps>;
@@ -0,0 +1,145 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment as external_react_Fragment, useMemo, useRef } from "react";
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";
6
+ import { buildValueMenuSections } from "../../lib/buildValueMenuSections.js";
7
+ import { MenuEmptyState } from "../MenuEmptyState.js";
8
+ import { NestedValueMenuItem } from "./NestedValueMenuItem.js";
9
+ import { SELECT_ALL_ID, useNestedValueMenuState } from "./useNestedValueMenuState.js";
10
+ import { ValueMenuFooterHints } from "./ValueMenuFooterHints.js";
11
+ const NestedValueMenu = ({ values, onSelect, onCommit, open = false, onOpenChange, onEscape, multiSelect = false, initialValues = [], width = 'standard', positioning, onBuildingValueChange, onItemToggle, inputRef, filterText = '', menuRef, blurCommitRef, className })=>{
12
+ const submenuRef = useRef(null);
13
+ const rowElsRef = useRef(new Map());
14
+ const sections = useMemo(()=>buildValueMenuSections(values, filterText), [
15
+ values,
16
+ filterText
17
+ ]);
18
+ const state = useNestedValueMenuState({
19
+ sections,
20
+ allValues: values,
21
+ open,
22
+ multiSelect,
23
+ initialValues,
24
+ onSelect,
25
+ onCommit,
26
+ onEscape,
27
+ onOpenChange,
28
+ onBuildingValueChange,
29
+ onItemToggle,
30
+ inputRef,
31
+ menuRef,
32
+ submenuRef,
33
+ blurCommitRef
34
+ });
35
+ const widthClass = 'compact' === width ? 'w-[172px]' : 'w-[300px]';
36
+ const widthStyle = 'number' == typeof width ? {
37
+ width: `${width}px`
38
+ } : void 0;
39
+ const setRowRef = (id)=>(el)=>{
40
+ state.registerTopItem(id)(el);
41
+ if (el) rowElsRef.current.set(id, el);
42
+ else rowElsRef.current.delete(id);
43
+ };
44
+ const submenuPositioning = useMemo(()=>({
45
+ placement: 'right-start',
46
+ gutter: 8,
47
+ getAnchorRect: ()=>state.openParentId && rowElsRef.current.get(state.openParentId)?.getBoundingClientRect() || null
48
+ }), [
49
+ state.openParentId
50
+ ]);
51
+ const openParentRow = state.openParentRow;
52
+ const allChecked = openParentRow ? state.allLeavesChecked(openParentRow.option) : false;
53
+ const hasRows = sections.some((section)=>section.rows.length > 0);
54
+ return /*#__PURE__*/ jsxs(Fragment, {
55
+ children: [
56
+ /*#__PURE__*/ jsx(DropdownMenu, {
57
+ open: open,
58
+ onOpenChange: onOpenChange,
59
+ closeOnSelect: false,
60
+ positioning: positioning,
61
+ highlightedValue: state.topHighlightedValue,
62
+ onHighlightChange: state.onTopHighlightChange,
63
+ children: /*#__PURE__*/ jsxs(DropdownMenuContent, {
64
+ ref: menuRef,
65
+ className: cn(widthClass, 'max-h-[430px]', className),
66
+ style: widthStyle,
67
+ "data-filter-input-menu": "true",
68
+ children: [
69
+ hasRows ? sections.map((section, index)=>/*#__PURE__*/ jsxs(external_react_Fragment, {
70
+ children: [
71
+ 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
+ })
90
+ ]
91
+ }, section.label ?? `section-${index}`)) : /*#__PURE__*/ jsx(MenuEmptyState, {}),
92
+ /*#__PURE__*/ jsx(DropdownMenuFooter, {
93
+ children: /*#__PURE__*/ jsx(ValueMenuFooterHints, {
94
+ multiSelect: multiSelect
95
+ })
96
+ })
97
+ ]
98
+ })
99
+ }),
100
+ openParentRow && /*#__PURE__*/ jsx(DropdownMenu, {
101
+ open: null != state.openParentId,
102
+ closeOnSelect: false,
103
+ positioning: submenuPositioning,
104
+ highlightedValue: state.submenuHighlightedValue,
105
+ onHighlightChange: state.onSubmenuHighlightChange,
106
+ children: /*#__PURE__*/ jsx(DropdownMenuContent, {
107
+ ref: submenuRef,
108
+ className: cn(widthClass, 'max-h-[430px]'),
109
+ style: widthStyle,
110
+ "data-filter-input-menu": "true",
111
+ onMouseEnter: state.cancelClose,
112
+ onMouseLeave: state.scheduleClose,
113
+ children: /*#__PURE__*/ jsxs(DropdownMenuGroup, {
114
+ children: [
115
+ /*#__PURE__*/ jsx(DropdownMenuItem, {
116
+ value: SELECT_ALL_ID,
117
+ ref: state.registerSubmenuItem(SELECT_ALL_ID),
118
+ 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
+ })
125
+ }),
126
+ /*#__PURE__*/ jsx(DropdownMenuSeparator, {}),
127
+ state.submenuRows.map((row)=>/*#__PURE__*/ jsx(NestedValueMenuItem, {
128
+ id: row.id,
129
+ option: row.option,
130
+ isGroup: row.isGroup,
131
+ checkedState: state.getRowCheckState(row),
132
+ isPending: state.submenuPendingIds.has(row.id),
133
+ multiSelect: multiSelect,
134
+ onSelect: ()=>state.selectRow(row, true),
135
+ registerRef: state.registerSubmenuItem(row.id)
136
+ }, row.id))
137
+ ]
138
+ })
139
+ })
140
+ })
141
+ ]
142
+ });
143
+ };
144
+ NestedValueMenu.displayName = 'NestedValueMenu';
145
+ export { NestedValueMenu };
@@ -0,0 +1,26 @@
1
+ import type { FC } from 'react';
2
+ import type { CheckboxCheckedState } from '@ark-ui/react/checkbox';
3
+ import type { ValueOption } from './FilterInputValueMenu';
4
+ interface NestedValueMenuItemProps {
5
+ id: string;
6
+ option: ValueOption;
7
+ /** Whether this row is a group (parent category → opens a submenu on hover). */
8
+ isGroup: boolean;
9
+ checkedState: CheckboxCheckedState;
10
+ isPending: boolean;
11
+ multiSelect: boolean;
12
+ onSelect: () => void;
13
+ onMouseEnter?: () => void;
14
+ onMouseLeave?: () => void;
15
+ registerRef?: (el: HTMLElement | null) => void;
16
+ /** For group rows: whether its submenu is currently open. */
17
+ expanded?: boolean;
18
+ }
19
+ /**
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.
24
+ */
25
+ export declare const NestedValueMenuItem: FC<NestedValueMenuItemProps>;
26
+ export {};
@@ -0,0 +1,54 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Badge } from "../../../Badge/index.js";
3
+ import { Checkmark } from "../../../Checkmark/index.js";
4
+ import { DropdownMenuItem } from "../../../DropdownMenu/index.js";
5
+ import { Text } from "../../../Text/index.js";
6
+ const NestedValueMenuItem = ({ id, option, isGroup, checkedState, isPending, multiSelect, onSelect, onMouseEnter, onMouseLeave, registerRef, expanded })=>/*#__PURE__*/ jsxs(DropdownMenuItem, {
7
+ value: id,
8
+ ref: registerRef,
9
+ onSelect: onSelect,
10
+ onMouseEnter: onMouseEnter,
11
+ onMouseLeave: onMouseLeave,
12
+ className: isPending ? 'bg-states-primary-hover' : void 0,
13
+ "aria-checked": multiSelect ? 'indeterminate' === checkedState ? 'mixed' : !!checkedState : void 0,
14
+ ...isGroup && {
15
+ 'aria-haspopup': 'menu',
16
+ 'aria-expanded': !!expanded
17
+ },
18
+ children: [
19
+ option.badge ? /*#__PURE__*/ jsx(Badge, {
20
+ color: option.badge.color,
21
+ type: "secondary",
22
+ variant: "default",
23
+ children: option.badge.text
24
+ }) : /*#__PURE__*/ jsxs("div", {
25
+ className: "min-w-0",
26
+ children: [
27
+ /*#__PURE__*/ jsx(Text, {
28
+ size: "sm",
29
+ truncate: true,
30
+ children: option.label
31
+ }),
32
+ option.description && /*#__PURE__*/ jsx(Text, {
33
+ size: "xs",
34
+ color: "secondary",
35
+ truncate: true,
36
+ children: option.description
37
+ })
38
+ ]
39
+ }),
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
51
+ ]
52
+ });
53
+ NestedValueMenuItem.displayName = 'NestedValueMenuItem';
54
+ export { NestedValueMenuItem };
@@ -0,0 +1,12 @@
1
+ import type { FC } from 'react';
2
+ interface ValueMenuFooterHintsProps {
3
+ multiSelect: boolean;
4
+ }
5
+ /**
6
+ * Keyboard-hint content for the value menu footer. Rendered *inside* a
7
+ * `DropdownMenuFooter` at the call site (not a footer itself) so the footer
8
+ * element stays a direct child of `DropdownMenuContent` and is pinned outside
9
+ * the scrolling list instead of scrolling away with the options.
10
+ */
11
+ export declare const ValueMenuFooterHints: FC<ValueMenuFooterHintsProps>;
12
+ export {};
@@ -0,0 +1,69 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { Kbd } from "../../../Kbd/Kbd.js";
3
+ import { KbdGroup } from "../../../Kbd/KbdGroup.js";
4
+ const ValueMenuFooterHints = ({ multiSelect })=>multiSelect ? /*#__PURE__*/ jsxs(Fragment, {
5
+ children: [
6
+ /*#__PURE__*/ jsxs("span", {
7
+ className: "flex items-center gap-4",
8
+ children: [
9
+ /*#__PURE__*/ jsx(KbdGroup, {
10
+ children: /*#__PURE__*/ jsx(Kbd, {
11
+ children: "↵"
12
+ })
13
+ }),
14
+ "to select"
15
+ ]
16
+ }),
17
+ /*#__PURE__*/ jsxs("span", {
18
+ className: "flex items-center gap-4",
19
+ children: [
20
+ /*#__PURE__*/ jsxs(KbdGroup, {
21
+ children: [
22
+ /*#__PURE__*/ jsx(Kbd, {
23
+ children: "⌘"
24
+ }),
25
+ /*#__PURE__*/ jsx(Kbd, {
26
+ children: "↑"
27
+ }),
28
+ /*#__PURE__*/ jsx(Kbd, {
29
+ children: "↓"
30
+ })
31
+ ]
32
+ }),
33
+ "to multi-select"
34
+ ]
35
+ })
36
+ ]
37
+ }) : /*#__PURE__*/ jsxs(Fragment, {
38
+ children: [
39
+ /*#__PURE__*/ jsxs("span", {
40
+ className: "flex items-center gap-4",
41
+ children: [
42
+ /*#__PURE__*/ jsxs(KbdGroup, {
43
+ children: [
44
+ /*#__PURE__*/ jsx(Kbd, {
45
+ children: "↑"
46
+ }),
47
+ /*#__PURE__*/ jsx(Kbd, {
48
+ children: "↓"
49
+ })
50
+ ]
51
+ }),
52
+ "to navigate"
53
+ ]
54
+ }),
55
+ /*#__PURE__*/ jsxs("span", {
56
+ className: "flex items-center gap-4",
57
+ children: [
58
+ /*#__PURE__*/ jsx(KbdGroup, {
59
+ children: /*#__PURE__*/ jsx(Kbd, {
60
+ children: "↵"
61
+ })
62
+ }),
63
+ "to select"
64
+ ]
65
+ })
66
+ ]
67
+ });
68
+ ValueMenuFooterHints.displayName = 'ValueMenuFooterHints';
69
+ export { ValueMenuFooterHints };
@@ -0,0 +1,65 @@
1
+ import type { RefObject } from 'react';
2
+ import type { CheckboxCheckedState } from '@ark-ui/react/checkbox';
3
+ import type { ValueMenuRow, ValueMenuSection } from '../../lib/buildValueMenuSections';
4
+ import type { FieldValueOption } from '../../types';
5
+ type ConditionValue = string | number | boolean;
6
+ /** Sentinel id for the submenu's "Select all / Unselect all" toggle row. */
7
+ export declare const SELECT_ALL_ID = "__filter_value_select_all__";
8
+ interface UseNestedValueMenuStateOptions {
9
+ /** Filtered, ordered top-level sections. */
10
+ sections: ValueMenuSection[];
11
+ /** Full (unfiltered) value tree — used for leaf-label resolution / preview. */
12
+ allValues: FieldValueOption[];
13
+ open: boolean;
14
+ multiSelect: boolean;
15
+ initialValues: ConditionValue[];
16
+ onSelect: (value: ConditionValue) => void;
17
+ onCommit?: (values: ConditionValue[]) => void;
18
+ onEscape?: () => void;
19
+ onOpenChange?: (open: boolean) => void;
20
+ onBuildingValueChange?: (preview: string | undefined) => void;
21
+ onItemToggle?: () => void;
22
+ inputRef?: RefObject<HTMLInputElement | null>;
23
+ menuRef?: RefObject<HTMLDivElement | null>;
24
+ submenuRef?: RefObject<HTMLDivElement | null>;
25
+ blurCommitRef?: RefObject<(() => boolean) | null>;
26
+ }
27
+ /**
28
+ * Selection + two-level keyboard navigation for the **nested** value menu
29
+ * (sections + a right-side submenu of leaf sub-values). Selection state is a
30
+ * single flat array of committed **leaf** values — group rows are never stored;
31
+ * toggling one is a bulk shortcut for its descendant leaves.
32
+ *
33
+ * Only one level listens for keys at a time: the two `useKeyboardNav` instances
34
+ * are gated on `open` so the top-level listener detaches while the submenu is
35
+ * open and vice-versa (no double navigation).
36
+ */
37
+ export declare const useNestedValueMenuState: ({ sections, allValues, open, multiSelect, initialValues, onSelect, onCommit, onEscape, onOpenChange, onBuildingValueChange, onItemToggle, inputRef, menuRef, submenuRef, blurCommitRef, }: UseNestedValueMenuStateOptions) => {
38
+ checkedValues: ConditionValue[];
39
+ openParentId: string | null;
40
+ openParent: (id: string) => void;
41
+ closeParent: () => void;
42
+ cancelClose: () => void;
43
+ scheduleClose: () => void;
44
+ openParentRow: ValueMenuRow | undefined;
45
+ submenuRows: ValueMenuRow[];
46
+ selectRow: (row: ValueMenuRow, fromSubmenu: boolean) => void;
47
+ toggleGroup: (option: FieldValueOption) => void;
48
+ isLeafChecked: (value: ConditionValue) => boolean;
49
+ getRowCheckState: (row: ValueMenuRow) => CheckboxCheckedState;
50
+ allLeavesChecked: (option: FieldValueOption) => boolean;
51
+ commitChecked: () => boolean;
52
+ topHighlightedValue: string;
53
+ onTopHighlightChange: (details: {
54
+ highlightedValue: string | null;
55
+ }) => void;
56
+ topPendingIds: Set<string>;
57
+ registerTopItem: (id: string) => (el: HTMLElement | null) => void;
58
+ submenuHighlightedValue: string;
59
+ onSubmenuHighlightChange: (details: {
60
+ highlightedValue: string | null;
61
+ }) => void;
62
+ submenuPendingIds: Set<string>;
63
+ registerSubmenuItem: (id: string) => (el: HTMLElement | null) => void;
64
+ };
65
+ export {};