@wheelhouse/ui 0.2.2 → 0.2.4

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 (236) hide show
  1. package/dist/blocks/columns/columns-types.d.ts +40 -0
  2. package/dist/blocks/columns/columns-types.d.ts.map +1 -0
  3. package/dist/blocks/columns/columns-types.js +10 -0
  4. package/dist/blocks/columns/columns-utils.d.ts +13 -0
  5. package/dist/blocks/columns/columns-utils.d.ts.map +1 -0
  6. package/dist/blocks/columns/columns-utils.js +85 -0
  7. package/dist/blocks/columns/columns.d.ts +3 -0
  8. package/dist/blocks/columns/columns.d.ts.map +1 -0
  9. package/dist/blocks/columns/columns.js +79 -0
  10. package/dist/blocks/columns/columns.stories.d.ts +12 -0
  11. package/dist/blocks/columns/columns.stories.d.ts.map +1 -0
  12. package/dist/blocks/columns/columns.stories.js +67 -0
  13. package/dist/blocks/columns/index.d.ts +6 -0
  14. package/dist/blocks/columns/index.d.ts.map +1 -0
  15. package/dist/blocks/columns/index.js +3 -0
  16. package/dist/blocks/date-selector/date-selector-context.d.ts.map +1 -0
  17. package/dist/blocks/date-selector/date-selector-default-i18n.d.ts +10 -0
  18. package/dist/blocks/date-selector/date-selector-default-i18n.d.ts.map +1 -0
  19. package/dist/blocks/date-selector/date-selector-default-i18n.js +29 -0
  20. package/dist/blocks/date-selector/date-selector-i18n-resources.d.ts +11 -0
  21. package/dist/blocks/date-selector/date-selector-i18n-resources.d.ts.map +1 -0
  22. package/dist/blocks/date-selector/date-selector-i18n-resources.js +248 -0
  23. package/dist/blocks/date-selector/date-selector-i18n.shared.d.ts +12 -0
  24. package/dist/blocks/date-selector/date-selector-i18n.shared.d.ts.map +1 -0
  25. package/dist/blocks/date-selector/date-selector-i18n.shared.js +84 -0
  26. package/dist/{components → blocks}/date-selector/date-selector-parts.d.ts +9 -0
  27. package/dist/blocks/date-selector/date-selector-parts.d.ts.map +1 -0
  28. package/dist/{components → blocks}/date-selector/date-selector-parts.js +21 -6
  29. package/dist/{components → blocks}/date-selector/date-selector-types.d.ts +70 -0
  30. package/dist/blocks/date-selector/date-selector-types.d.ts.map +1 -0
  31. package/dist/{components → blocks}/date-selector/date-selector-types.js +22 -0
  32. package/dist/blocks/date-selector/date-selector-value.d.ts +81 -0
  33. package/dist/blocks/date-selector/date-selector-value.d.ts.map +1 -0
  34. package/dist/blocks/date-selector/date-selector-value.js +423 -0
  35. package/dist/{components → blocks}/date-selector/date-selector.d.ts +1 -1
  36. package/dist/blocks/date-selector/date-selector.d.ts.map +1 -0
  37. package/dist/blocks/date-selector/date-selector.js +191 -0
  38. package/dist/{components → blocks}/date-selector/date-selector.stories.d.ts +14 -0
  39. package/dist/blocks/date-selector/date-selector.stories.d.ts.map +1 -0
  40. package/dist/blocks/date-selector/date-selector.stories.js +299 -0
  41. package/dist/blocks/date-selector/index.d.ts +11 -0
  42. package/dist/blocks/date-selector/index.d.ts.map +1 -0
  43. package/dist/blocks/date-selector/index.js +8 -0
  44. package/dist/{components → blocks}/date-selector/use-date-selector.d.ts +4 -3
  45. package/dist/blocks/date-selector/use-date-selector.d.ts.map +1 -0
  46. package/dist/{components → blocks}/date-selector/use-date-selector.js +14 -8
  47. package/dist/blocks/floating-menu-widget/floating-menu-widget.d.ts +26 -0
  48. package/dist/blocks/floating-menu-widget/floating-menu-widget.d.ts.map +1 -0
  49. package/dist/blocks/floating-menu-widget/floating-menu-widget.js +200 -0
  50. package/dist/blocks/floating-menu-widget/floating-menu-widget.stories.d.ts +15 -0
  51. package/dist/blocks/floating-menu-widget/floating-menu-widget.stories.d.ts.map +1 -0
  52. package/dist/blocks/floating-menu-widget/floating-menu-widget.stories.js +22 -0
  53. package/dist/blocks/floating-menu-widget/index.d.ts +3 -0
  54. package/dist/blocks/floating-menu-widget/index.d.ts.map +1 -0
  55. package/dist/blocks/floating-menu-widget/index.js +1 -0
  56. package/dist/blocks/index.d.ts +5 -0
  57. package/dist/blocks/index.d.ts.map +1 -0
  58. package/dist/blocks/index.js +4 -0
  59. package/dist/blocks/navigation/index.d.ts +5 -0
  60. package/dist/blocks/navigation/index.d.ts.map +1 -0
  61. package/dist/blocks/navigation/index.js +2 -0
  62. package/dist/blocks/navigation/navigation-types.d.ts +60 -0
  63. package/dist/blocks/navigation/navigation-types.d.ts.map +1 -0
  64. package/dist/blocks/navigation/navigation-types.js +1 -0
  65. package/dist/blocks/navigation/navigation.d.ts +9 -0
  66. package/dist/blocks/navigation/navigation.d.ts.map +1 -0
  67. package/dist/blocks/navigation/navigation.demo.d.ts +4 -0
  68. package/dist/blocks/navigation/navigation.demo.d.ts.map +1 -0
  69. package/dist/blocks/navigation/navigation.demo.js +46 -0
  70. package/dist/blocks/navigation/navigation.js +98 -0
  71. package/dist/blocks/navigation/navigation.stories.d.ts +14 -0
  72. package/dist/blocks/navigation/navigation.stories.d.ts.map +1 -0
  73. package/dist/blocks/navigation/navigation.stories.js +16 -0
  74. package/dist/components/accordion/accordion.stories.js +1 -1
  75. package/dist/components/alert/alert.stories.js +1 -1
  76. package/dist/components/alert-dialog/alert-dialog.stories.js +1 -1
  77. package/dist/components/aspect-ratio/aspect-ratio.stories.js +1 -1
  78. package/dist/components/avatar/avatar.stories.js +1 -1
  79. package/dist/components/badge/badge.stories.js +1 -1
  80. package/dist/components/breadcrumb/breadcrumb.stories.js +1 -1
  81. package/dist/components/button/button.d.ts +18 -11
  82. package/dist/components/button/button.d.ts.map +1 -1
  83. package/dist/components/button/button.js +27 -14
  84. package/dist/components/button/button.stories.d.ts +11 -0
  85. package/dist/components/button/button.stories.d.ts.map +1 -1
  86. package/dist/components/button/button.stories.js +85 -1
  87. package/dist/components/button-group/button-group.d.ts +10 -4
  88. package/dist/components/button-group/button-group.d.ts.map +1 -1
  89. package/dist/components/button-group/button-group.js +15 -3
  90. package/dist/components/button-group/button-group.stories.js +1 -1
  91. package/dist/components/button-group/index.d.ts +2 -2
  92. package/dist/components/button-group/index.d.ts.map +1 -1
  93. package/dist/components/button-group/index.js +1 -1
  94. package/dist/components/calendar/calendar.stories.js +1 -1
  95. package/dist/components/card/card.stories.js +1 -1
  96. package/dist/components/checkbox/checkbox.stories.js +1 -1
  97. package/dist/components/collapsible/collapsible.stories.js +1 -1
  98. package/dist/components/combobox/combobox.stories.js +1 -1
  99. package/dist/components/command/command.stories.js +1 -1
  100. package/dist/components/context-menu/context-menu.stories.js +1 -1
  101. package/dist/components/dialog/dialog.stories.js +1 -1
  102. package/dist/components/direction/direction.stories.js +1 -1
  103. package/dist/components/drawer/drawer.stories.js +1 -1
  104. package/dist/components/dropdown-menu/dropdown-menu.d.ts +9 -2
  105. package/dist/components/dropdown-menu/dropdown-menu.d.ts.map +1 -1
  106. package/dist/components/dropdown-menu/dropdown-menu.js +4 -1
  107. package/dist/components/dropdown-menu/dropdown-menu.stories.js +1 -1
  108. package/dist/components/dropdown-menu/index.d.ts +2 -2
  109. package/dist/components/dropdown-menu/index.d.ts.map +1 -1
  110. package/dist/components/dropdown-menu/index.js +1 -1
  111. package/dist/components/empty/empty.stories.js +1 -1
  112. package/dist/components/field/field.stories.js +1 -1
  113. package/dist/components/filters/filter-date-metric-value.d.ts +32 -0
  114. package/dist/components/filters/filter-date-metric-value.d.ts.map +1 -0
  115. package/dist/components/filters/filter-date-metric-value.js +406 -0
  116. package/dist/components/filters/filter-fields-listing-demo.d.ts +12 -0
  117. package/dist/components/filters/filter-fields-listing-demo.d.ts.map +1 -0
  118. package/dist/components/filters/filter-fields-listing-demo.js +565 -0
  119. package/dist/components/filters/filters-defaults.d.ts +4 -0
  120. package/dist/components/filters/filters-defaults.d.ts.map +1 -1
  121. package/dist/components/filters/filters-defaults.js +59 -1
  122. package/dist/components/filters/filters-i18n-resources.d.ts +277 -0
  123. package/dist/components/filters/filters-i18n-resources.d.ts.map +1 -0
  124. package/dist/components/filters/filters-i18n-resources.js +276 -0
  125. package/dist/components/filters/filters-i18n.shared.d.ts +16 -0
  126. package/dist/components/filters/filters-i18n.shared.d.ts.map +1 -0
  127. package/dist/components/filters/filters-i18n.shared.js +111 -0
  128. package/dist/components/filters/filters-types.d.ts +40 -1
  129. package/dist/components/filters/filters-types.d.ts.map +1 -1
  130. package/dist/components/filters/filters-utils.d.ts +28 -1
  131. package/dist/components/filters/filters-utils.d.ts.map +1 -1
  132. package/dist/components/filters/filters-utils.js +102 -0
  133. package/dist/components/filters/filters.d.ts +21 -3
  134. package/dist/components/filters/filters.d.ts.map +1 -1
  135. package/dist/components/filters/filters.js +493 -290
  136. package/dist/components/filters/filters.stories.d.ts +107 -2
  137. package/dist/components/filters/filters.stories.d.ts.map +1 -1
  138. package/dist/components/filters/filters.stories.js +224 -30
  139. package/dist/components/filters/index.d.ts +4 -1
  140. package/dist/components/filters/index.d.ts.map +1 -1
  141. package/dist/components/filters/index.js +4 -1
  142. package/dist/components/frame/frame.stories.js +1 -1
  143. package/dist/components/hover-card/hover-card.stories.js +1 -1
  144. package/dist/components/index.d.ts +2 -2
  145. package/dist/components/index.d.ts.map +1 -1
  146. package/dist/components/index.js +2 -2
  147. package/dist/components/input/input.stories.js +1 -1
  148. package/dist/components/input-group/input-group.stories.js +1 -1
  149. package/dist/components/item/item.stories.js +1 -1
  150. package/dist/components/kbd/kbd.stories.js +1 -1
  151. package/dist/components/label/label.stories.js +1 -1
  152. package/dist/components/menubar/menubar.stories.js +1 -1
  153. package/dist/components/native-select/native-select.stories.js +1 -1
  154. package/dist/components/navigation-menu/navigation-menu.stories.js +1 -1
  155. package/dist/components/pagination/pagination.stories.js +1 -1
  156. package/dist/components/popover/index.d.ts +1 -0
  157. package/dist/components/popover/index.d.ts.map +1 -1
  158. package/dist/components/popover/index.js +1 -0
  159. package/dist/components/popover/popover-handle.d.ts +6 -0
  160. package/dist/components/popover/popover-handle.d.ts.map +1 -0
  161. package/dist/components/popover/popover-handle.js +6 -0
  162. package/dist/components/popover/popover.d.ts +41 -7
  163. package/dist/components/popover/popover.d.ts.map +1 -1
  164. package/dist/components/popover/popover.js +50 -3
  165. package/dist/components/popover/popover.stories.js +1 -1
  166. package/dist/components/progress/progress.js +1 -1
  167. package/dist/components/progress/progress.stories.d.ts +11 -2
  168. package/dist/components/progress/progress.stories.d.ts.map +1 -1
  169. package/dist/components/progress/progress.stories.js +78 -5
  170. package/dist/components/radio-group/radio-group.stories.js +1 -1
  171. package/dist/components/resizable/resizable.stories.js +1 -1
  172. package/dist/components/scroll-area/scroll-area.stories.js +1 -1
  173. package/dist/components/select/select.stories.js +1 -1
  174. package/dist/components/separator/separator.stories.js +1 -1
  175. package/dist/components/sheet/sheet.stories.js +1 -1
  176. package/dist/components/sidebar/index.d.ts +2 -0
  177. package/dist/components/sidebar/index.d.ts.map +1 -0
  178. package/dist/components/sidebar/index.js +1 -0
  179. package/dist/components/sidebar/sidebar.d.ts +64 -0
  180. package/dist/components/sidebar/sidebar.d.ts.map +1 -0
  181. package/dist/components/sidebar/sidebar.js +255 -0
  182. package/dist/components/sidebar/sidebar.stories.d.ts +20 -0
  183. package/dist/components/sidebar/sidebar.stories.d.ts.map +1 -0
  184. package/dist/components/sidebar/sidebar.stories.js +184 -0
  185. package/dist/components/skeleton/index.d.ts +3 -0
  186. package/dist/components/skeleton/index.d.ts.map +1 -0
  187. package/dist/components/skeleton/index.js +1 -0
  188. package/dist/components/skeleton/skeleton.d.ts +7 -0
  189. package/dist/components/skeleton/skeleton.d.ts.map +1 -0
  190. package/dist/components/skeleton/skeleton.js +8 -0
  191. package/dist/components/slider/slider.stories.js +1 -1
  192. package/dist/components/sonner/sonner.stories.js +1 -1
  193. package/dist/components/sortable/sortable.stories.js +1 -1
  194. package/dist/components/spinner/spinner.stories.js +1 -1
  195. package/dist/components/status-indicator/status-indicator.stories.js +1 -1
  196. package/dist/components/switch/switch.stories.js +1 -1
  197. package/dist/components/tabs/tabs.stories.js +1 -1
  198. package/dist/components/text/text.stories.js +1 -1
  199. package/dist/components/textarea/textarea.stories.js +1 -1
  200. package/dist/components/toggle/toggle.stories.js +1 -1
  201. package/dist/components/toggle-group/toggle-group.stories.js +1 -1
  202. package/dist/components/tooltip/tooltip.stories.js +1 -1
  203. package/dist/index.d.ts +1 -0
  204. package/dist/index.d.ts.map +1 -1
  205. package/dist/index.js +1 -0
  206. package/dist/tsconfig.tsbuildinfo +1 -1
  207. package/llms.txt +10 -5
  208. package/package.json +8 -4
  209. package/dist/components/date-selector/date-selector-context.d.ts.map +0 -1
  210. package/dist/components/date-selector/date-selector-parts.d.ts.map +0 -1
  211. package/dist/components/date-selector/date-selector-types.d.ts.map +0 -1
  212. package/dist/components/date-selector/date-selector-value.d.ts +0 -47
  213. package/dist/components/date-selector/date-selector-value.d.ts.map +0 -1
  214. package/dist/components/date-selector/date-selector-value.js +0 -183
  215. package/dist/components/date-selector/date-selector.d.ts.map +0 -1
  216. package/dist/components/date-selector/date-selector.js +0 -144
  217. package/dist/components/date-selector/date-selector.stories.d.ts.map +0 -1
  218. package/dist/components/date-selector/date-selector.stories.js +0 -144
  219. package/dist/components/date-selector/index.d.ts +0 -7
  220. package/dist/components/date-selector/index.d.ts.map +0 -1
  221. package/dist/components/date-selector/index.js +0 -5
  222. package/dist/components/date-selector/use-date-selector.d.ts.map +0 -1
  223. package/dist/components/navigation-pattern-1/index.d.ts +0 -3
  224. package/dist/components/navigation-pattern-1/index.d.ts.map +0 -1
  225. package/dist/components/navigation-pattern-1/index.js +0 -1
  226. package/dist/components/navigation-pattern-1/pattern-1.config.d.ts +0 -47
  227. package/dist/components/navigation-pattern-1/pattern-1.config.d.ts.map +0 -1
  228. package/dist/components/navigation-pattern-1/pattern-1.config.js +0 -55
  229. package/dist/components/navigation-pattern-1/pattern-1.d.ts +0 -7
  230. package/dist/components/navigation-pattern-1/pattern-1.d.ts.map +0 -1
  231. package/dist/components/navigation-pattern-1/pattern-1.js +0 -83
  232. package/dist/components/navigation-pattern-1/pattern-1.stories.d.ts +0 -16
  233. package/dist/components/navigation-pattern-1/pattern-1.stories.d.ts.map +0 -1
  234. package/dist/components/navigation-pattern-1/pattern-1.stories.js +0 -20
  235. /package/dist/{components → blocks}/date-selector/date-selector-context.d.ts +0 -0
  236. /package/dist/{components → blocks}/date-selector/date-selector-context.js +0 -0
@@ -1,38 +1,42 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';
3
+ import { createContext, Fragment, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';
4
4
  import { useRender } from '@base-ui/react/use-render';
5
5
  import { cva } from 'class-variance-authority';
6
6
  import { cn } from '../../lib/utils';
7
7
  import { Button } from '../button';
8
8
  import { ButtonGroup, ButtonGroupText } from '../button-group';
9
- import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from '../dropdown-menu';
9
+ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from '../dropdown-menu';
10
10
  import { Input } from '../input';
11
11
  import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText } from '../input-group';
12
12
  import { Kbd } from '../kbd';
13
13
  import { ScrollArea } from '../scroll-area';
14
14
  import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
15
- import { AlertCircle, Check, X } from 'lucide-react';
16
- import { createOperatorsFromI18n, DEFAULT_I18N } from './filters-defaults';
17
- import { createFilter } from './filters-utils';
15
+ import { AlertCircle, Check, ChevronDown, Plus, X } from 'lucide-react';
16
+ import { FilterDateMetricBarPeriodSegments, FilterDateMetricBarValueSegment } from './filter-date-metric-value';
17
+ import { DEFAULT_I18N } from './filters-defaults';
18
+ import { createDefaultDateMetricPayload, createFilter, getOperatorsForField, mergeFilterUpdate } from './filters-utils';
19
+ /** Default for `Filters` `submenuSearchMinOptions`. */
20
+ export const DEFAULT_SUBMENU_SEARCH_MIN_OPTIONS = 10;
21
+ function shouldShowSubmenuSearchField(field, minOptions) {
22
+ if (field.searchable === false)
23
+ return false;
24
+ const threshold = Number.isFinite(minOptions) && minOptions >= 0 ? minOptions : DEFAULT_SUBMENU_SEARCH_MIN_OPTIONS;
25
+ return (field.options?.length ?? 0) >= threshold;
26
+ }
18
27
  const FilterContext = createContext({
19
- variant: 'default',
20
- size: 'default',
28
+ size: 'sm',
21
29
  radius: 'default',
30
+ submenuSearchMinOptions: DEFAULT_SUBMENU_SEARCH_MIN_OPTIONS,
22
31
  i18n: DEFAULT_I18N,
23
32
  className: undefined,
24
- showSearchInput: true,
25
33
  trigger: undefined,
26
34
  allowMultiple: true,
27
35
  });
28
36
  const useFilterContext = () => useContext(FilterContext);
29
- // Container variant for filters wrapper
37
+ // Container spacing for the filters wrapper (chip wrap gap by `size` only).
30
38
  const filtersContainerVariants = cva('flex flex-wrap items-center', {
31
39
  variants: {
32
- variant: {
33
- solid: 'gap-2',
34
- default: '',
35
- },
36
40
  size: {
37
41
  sm: 'gap-1.5',
38
42
  default: 'gap-2.5',
@@ -40,8 +44,7 @@ const filtersContainerVariants = cva('flex flex-wrap items-center', {
40
44
  },
41
45
  },
42
46
  defaultVariants: {
43
- variant: 'default',
44
- size: 'default',
47
+ size: 'sm',
45
48
  },
46
49
  });
47
50
  function FilterInput({ field, onBlur, onKeyDown, className, ...props }) {
@@ -139,6 +142,31 @@ const flattenFields = (fields) => {
139
142
  return [...acc, item];
140
143
  }, []);
141
144
  };
145
+ /** True when the config uses top-level `{ group?, fields }` containers (add-filter menu shows section labels). */
146
+ const hasTopLevelFieldGroups = (config) => {
147
+ return config.some((item) => {
148
+ if (!item || typeof item !== 'object')
149
+ return false;
150
+ return 'fields' in item && Array.isArray(item.fields);
151
+ });
152
+ };
153
+ const parseTopLevelSections = (config) => {
154
+ const sections = [];
155
+ for (const raw of config) {
156
+ const item = raw;
157
+ if (item && typeof item === 'object' && 'fields' in item && Array.isArray(item.fields)) {
158
+ const groupLabel = 'group' in item && item.group ? String(item.group) : undefined;
159
+ sections.push({ groupLabel, fields: item.fields });
160
+ }
161
+ else {
162
+ const f = raw;
163
+ if (f.key && f.type !== 'separator') {
164
+ sections.push({ groupLabel: undefined, fields: [f] });
165
+ }
166
+ }
167
+ }
168
+ return sections;
169
+ };
142
170
  const getFieldsMap = (fields) => {
143
171
  const flatFields = flattenFields(fields);
144
172
  return flatFields.reduce((acc, field) => {
@@ -149,42 +177,34 @@ const getFieldsMap = (fields) => {
149
177
  return acc;
150
178
  }, {});
151
179
  };
152
- // Helper function to get operators for a field
153
- const getOperatorsForField = (field, values, i18n) => {
154
- if (field.operators)
155
- return field.operators;
156
- const operators = createOperatorsFromI18n(i18n);
157
- // Determine field type for operator selection
158
- let fieldType = field.type || 'select';
159
- // If it's a select field but has multiple values, treat as multiselect
160
- if (fieldType === 'select' && values.length > 1) {
161
- fieldType = 'multiselect';
162
- }
163
- // If it's a multiselect field or has multiselect operators, use multiselect operators
164
- if (fieldType === 'multiselect' || field.type === 'multiselect') {
165
- return operators.multiselect;
166
- }
167
- return operators[fieldType] || operators.select;
168
- };
169
180
  function FilterOperatorDropdown({ field, operator, values, onChange }) {
170
181
  const context = useFilterContext();
171
182
  const operators = getOperatorsForField(field, values, context.i18n);
172
- // Find the operator label, with fallback to formatted operator name
173
- const operatorLabel = operators.find((op) => op.value === operator)?.label || context.i18n.helpers.formatOperator(operator);
174
- return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { variant: "outline", size: context.size, className: "text-muted-foreground hover:text-foreground", children: operatorLabel }) }), _jsx(DropdownMenuContent, { align: "start", className: "w-fit min-w-fit", children: operators.map((op) => (_jsxs(DropdownMenuItem, { onClick: () => onChange(op.value), className: cn('flex items-center justify-between data-highlighted:bg-accent data-highlighted:text-accent-foreground'), children: [_jsx("span", { children: op.label }), _jsx(Check, { className: cn('ms-auto text-primary', op.value === operator ? 'opacity-100' : 'opacity-0') })] }, op.value))) })] }));
183
+ const currentOp = operators.find((op) => op.value === operator);
184
+ const operatorLabel = currentOp?.label || context.i18n.helpers.formatOperator(operator);
185
+ const compactTrigger = Boolean(currentOp?.icon);
186
+ const triggerButton = (_jsx(Button, { variant: "outline", size: context.size, className: cn('text-muted-foreground hover:text-foreground', compactTrigger && 'min-w-8 px-2 text-foreground text-muted-foreground [&_svg]:size-3.5 [&_svg]:shrink-0'), "aria-label": compactTrigger ? operatorLabel : undefined, children: compactTrigger ? currentOp?.icon : operatorLabel }));
187
+ return (_jsxs(DropdownMenu, { children: [compactTrigger ? (_jsxs(Tooltip, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(TooltipTrigger, { render: triggerButton }) }), _jsx(TooltipContent, { side: "top", sideOffset: 6, children: operatorLabel })] })) : (_jsx(DropdownMenuTrigger, { render: triggerButton })), _jsx(DropdownMenuContent, { align: "start", className: "w-fit min-w-fit", children: operators.map((op) => (_jsxs(DropdownMenuItem, { onClick: () => onChange(op.value), className: cn('flex items-center justify-between data-highlighted:bg-accent data-highlighted:text-accent-foreground'), children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [op.icon ? _jsx("span", { className: "inline-flex shrink-0 text-muted-foreground [&_svg]:size-3.5", children: op.icon }) : null, _jsx("span", { className: "truncate", children: op.label })] }), _jsx(Check, { className: cn('ms-auto shrink-0 text-primary', op.value === operator ? 'opacity-100' : 'opacity-0') })] }, op.value))) })] }));
175
188
  }
176
189
  function SelectOptionsPopover({ field, values, onChange, onClose, inline = false }) {
177
190
  const [open, setOpen] = useState(false);
178
191
  const [searchInput, setSearchInput] = useState('');
179
192
  const [highlightedIndex, setHighlightedIndex] = useState(-1);
180
193
  const inputRef = useRef(null);
194
+ const listboxRef = useRef(null);
181
195
  const context = useFilterContext();
182
196
  const baseId = useId();
197
+ const showSubmenuSearch = shouldShowSubmenuSearchField(field, context.submenuSearchMinOptions);
183
198
  useEffect(() => {
184
- if (open) {
199
+ if (!open)
200
+ return;
201
+ if (showSubmenuSearch) {
185
202
  inputRef.current?.focus();
186
203
  }
187
- }, [open]);
204
+ else {
205
+ listboxRef.current?.focus();
206
+ }
207
+ }, [open, showSubmenuSearch]);
188
208
  useEffect(() => {
189
209
  if (highlightedIndex >= 0 && open) {
190
210
  const element = document.getElementById(`${baseId}-item-${highlightedIndex}`);
@@ -192,65 +212,70 @@ function SelectOptionsPopover({ field, values, onChange, onClose, inline = false
192
212
  }
193
213
  }, [highlightedIndex, open, baseId]);
194
214
  const isMultiSelect = field.type === 'multiselect' || values.length > 1;
195
- const effectiveValues = (field.value !== undefined ? field.value : values) || [];
196
- const selectedOptions = field.options?.filter((opt) => effectiveValues.includes(opt.value)) || [];
215
+ const effectiveValues = useMemo(() => (field.value !== undefined ? field.value : values) || [], [field.value, values]);
216
+ const selectedOptions = useMemo(() => field.options?.filter((opt) => effectiveValues.includes(opt.value)) || [], [field.options, effectiveValues]);
197
217
  const triggerPreviewIcons = selectedOptions.slice(0, 3).filter((option) => option.icon != null);
198
- const unselectedOptions = field.options?.filter((opt) => !effectiveValues.includes(opt.value)) || [];
218
+ const unselectedOptions = useMemo(() => field.options?.filter((opt) => !effectiveValues.includes(opt.value)) || [], [field.options, effectiveValues]);
199
219
  // Filter options based on search input
200
220
  const filteredSelectedOptions = selectedOptions; // Keep all selected visible
201
- const filteredUnselectedOptions = unselectedOptions.filter((opt) => opt.label.toLowerCase().includes(searchInput.toLowerCase()));
202
- const allFilteredOptions = [...filteredSelectedOptions, ...filteredUnselectedOptions];
203
- const handleClose = () => {
221
+ const filteredUnselectedOptions = useMemo(() => unselectedOptions.filter((opt) => opt.label.toLowerCase().includes(searchInput.toLowerCase())), [unselectedOptions, searchInput]);
222
+ const allFilteredOptions = useMemo(() => [...filteredSelectedOptions, ...filteredUnselectedOptions], [filteredSelectedOptions, filteredUnselectedOptions]);
223
+ const handleClose = useCallback(() => {
204
224
  setHighlightedIndex(-1);
205
225
  setOpen(false);
206
226
  onClose?.();
207
- };
208
- const renderMenuContent = () => (_jsxs(_Fragment, { children: [field.searchable !== false && (_jsxs(_Fragment, { children: [_jsx(Input, { ref: inputRef, role: "combobox", "aria-autocomplete": "list", "aria-expanded": true, "aria-haspopup": "listbox", "aria-controls": `${baseId}-listbox`, "aria-activedescendant": highlightedIndex >= 0 ? `${baseId}-item-${highlightedIndex}` : undefined, placeholder: context.i18n.placeholders.searchField(field.label || ''), className: cn('h-8 rounded-none border-0 border-input bg-transparent! px-2 text-sm shadow-none', 'focus-visible:border-border focus-visible:ring-0 focus-visible:ring-offset-0', open && 'placeholder:text-foreground'), value: searchInput, onChange: (e) => {
227
+ }, [onClose]);
228
+ const onSelectOptionsListKeyDown = useCallback((e) => {
229
+ if (e.key === 'ArrowDown') {
230
+ e.preventDefault();
231
+ if (allFilteredOptions.length > 0) {
232
+ setHighlightedIndex((prev) => (prev < allFilteredOptions.length - 1 ? prev + 1 : 0));
233
+ }
234
+ }
235
+ else if (e.key === 'ArrowUp') {
236
+ e.preventDefault();
237
+ if (allFilteredOptions.length > 0) {
238
+ setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : allFilteredOptions.length - 1));
239
+ }
240
+ }
241
+ else if (e.key === 'ArrowLeft') {
242
+ e.preventDefault();
243
+ setHighlightedIndex(-1);
244
+ setOpen(false);
245
+ }
246
+ else if (e.key === 'Enter' && highlightedIndex >= 0) {
247
+ e.preventDefault();
248
+ const option = allFilteredOptions[highlightedIndex];
249
+ if (option) {
250
+ const isSelected = effectiveValues.includes(option.value);
251
+ const next = isSelected
252
+ ? effectiveValues.filter((v) => v !== option.value)
253
+ : isMultiSelect
254
+ ? [...effectiveValues, option.value]
255
+ : [option.value];
256
+ if (!isSelected && isMultiSelect && field.maxSelections && next.length > field.maxSelections) {
257
+ return;
258
+ }
259
+ if (field.onValueChange) {
260
+ field.onValueChange(next);
261
+ }
262
+ else {
263
+ onChange(next);
264
+ }
265
+ if (!isMultiSelect)
266
+ handleClose();
267
+ }
268
+ }
269
+ e.stopPropagation();
270
+ }, [allFilteredOptions, highlightedIndex, effectiveValues, isMultiSelect, field, onChange, handleClose]);
271
+ const renderMenuContent = () => (_jsxs(_Fragment, { children: [showSubmenuSearch && (_jsxs(_Fragment, { children: [_jsx(Input, { ref: inputRef, role: "combobox", "aria-autocomplete": "list", "aria-expanded": true, "aria-haspopup": "listbox", "aria-controls": `${baseId}-listbox`, "aria-activedescendant": highlightedIndex >= 0 ? `${baseId}-item-${highlightedIndex}` : undefined, placeholder: context.i18n.placeholders.searchField(field.label || ''), className: cn('h-8 rounded-none border-0 border-input bg-transparent! px-2 text-sm shadow-none', 'focus-visible:border-border focus-visible:ring-0 focus-visible:ring-offset-0', open && 'placeholder:text-foreground'), value: searchInput, onChange: (e) => {
209
272
  setSearchInput(e.target.value);
210
273
  setHighlightedIndex(-1);
211
- }, onBlur: () => open && inputRef.current?.focus(), onClick: (e) => e.stopPropagation(), onKeyDown: (e) => {
212
- if (e.key === 'ArrowDown') {
213
- e.preventDefault();
214
- if (allFilteredOptions.length > 0) {
215
- setHighlightedIndex((prev) => (prev < allFilteredOptions.length - 1 ? prev + 1 : 0));
216
- }
217
- }
218
- else if (e.key === 'ArrowUp') {
219
- e.preventDefault();
220
- if (allFilteredOptions.length > 0) {
221
- setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : allFilteredOptions.length - 1));
222
- }
223
- }
224
- else if (e.key === 'ArrowLeft') {
225
- e.preventDefault();
226
- setHighlightedIndex(-1);
227
- setOpen(false);
228
- }
229
- else if (e.key === 'Enter' && highlightedIndex >= 0) {
230
- e.preventDefault();
231
- const option = allFilteredOptions[highlightedIndex];
232
- if (option) {
233
- const isSelected = effectiveValues.includes(option.value);
234
- const next = isSelected
235
- ? effectiveValues.filter((v) => v !== option.value)
236
- : isMultiSelect
237
- ? [...effectiveValues, option.value]
238
- : [option.value];
239
- if (!isSelected && isMultiSelect && field.maxSelections && next.length > field.maxSelections) {
240
- return;
241
- }
242
- if (field.onValueChange) {
243
- field.onValueChange(next);
244
- }
245
- else {
246
- onChange(next);
247
- }
248
- if (!isMultiSelect)
249
- handleClose();
250
- }
251
- }
252
- e.stopPropagation();
253
- } }), _jsx(DropdownMenuSeparator, {})] })), _jsx("div", { className: "relative flex max-h-full", children: _jsx("div", { className: "flex max-h-[min(var(--available-height),24rem)] w-full scroll-pt-2 scroll-pb-2 flex-col overscroll-contain", role: "listbox", id: `${baseId}-listbox`, children: _jsxs(ScrollArea, { className: "size-full min-h-0 **:data-[slot=scroll-area-scrollbar]:m-0 [&_[data-slot=scroll-area-viewport]]:h-full [&_[data-slot=scroll-area-viewport]]:overscroll-contain", children: [allFilteredOptions.length === 0 && _jsx("div", { className: "py-2 text-center text-sm text-muted-foreground", children: context.i18n.noResultsFound }), filteredSelectedOptions.length > 0 && (_jsx(DropdownMenuGroup, { className: "px-1", children: filteredSelectedOptions.map((option, index) => {
274
+ }, onBlur: () => open && showSubmenuSearch && inputRef.current?.focus(), onClick: (e) => e.stopPropagation(), onKeyDown: onSelectOptionsListKeyDown }), _jsx(DropdownMenuSeparator, {})] })), _jsx("div", { className: "relative flex max-h-full", children: _jsx("div", { ref: listboxRef, className: cn('flex max-h-[min(var(--available-height),24rem)] w-full scroll-pt-2 scroll-pb-2 flex-col overscroll-contain', !showSubmenuSearch && 'outline-hidden'), role: "listbox", id: `${baseId}-listbox`, tabIndex: !showSubmenuSearch ? 0 : -1, onKeyDown: (e) => {
275
+ if (!showSubmenuSearch) {
276
+ onSelectOptionsListKeyDown(e);
277
+ }
278
+ }, children: _jsxs(ScrollArea, { className: "size-full min-h-0 **:data-[slot=scroll-area-scrollbar]:m-0 [&_[data-slot=scroll-area-viewport]]:h-full [&_[data-slot=scroll-area-viewport]]:overscroll-contain", children: [allFilteredOptions.length === 0 && _jsx("div", { className: "py-2 text-center text-sm text-muted-foreground", children: context.i18n.noResultsFound }), filteredSelectedOptions.length > 0 && (_jsx(DropdownMenuGroup, { className: "px-1", children: filteredSelectedOptions.map((option, index) => {
254
279
  const isHighlighted = highlightedIndex === index;
255
280
  const itemId = `${baseId}-item-${index}`;
256
281
  return (_jsxs(DropdownMenuCheckboxItem, { id: itemId, role: "option", "aria-selected": isHighlighted, "data-highlighted": isHighlighted || undefined, onMouseEnter: () => setHighlightedIndex(index), checked: true, className: cn('data-highlighted:bg-accent data-highlighted:text-accent-foreground', option.className), onSelect: (e) => {
@@ -305,11 +330,12 @@ function SelectOptionsPopover({ field, values, onChange, onClose, inline = false
305
330
  : context.i18n.select] })) }) }) }), _jsx(DropdownMenuContent, { align: "start", className: cn('w-[200px] px-0', field.className), children: renderMenuContent() })] }));
306
331
  }
307
332
  function FilterValueSelector({ field, values, onChange, operator, autoFocus }) {
333
+ const context = useFilterContext();
308
334
  if (operator === 'empty' || operator === 'not_empty') {
309
335
  return null;
310
336
  }
311
337
  if (field.customRenderer) {
312
- return (_jsx(ButtonGroupText, { className: "bg-background text-start whitespace-nowrap outline-hidden hover:bg-accent aria-expanded:bg-accent dark:bg-input/30", children: field.customRenderer({ field, values, onChange, operator }) }));
338
+ return (_jsx(ButtonGroupText, { size: context.size, className: "bg-background text-start whitespace-nowrap outline-hidden hover:bg-accent aria-expanded:bg-accent dark:bg-input/30", children: field.customRenderer({ field, values, onChange, operator }) }));
313
339
  }
314
340
  if (field.type === 'text') {
315
341
  return (_jsx(FilterInput, { type: "text", value: values[0] || '', onChange: (e) => onChange([e.target.value]), placeholder: field.placeholder, pattern: field.pattern, field: field, className: cn('w-36', field.className), autoFocus: autoFocus }));
@@ -319,32 +345,30 @@ function FilterValueSelector({ field, values, onChange, operator, autoFocus }) {
319
345
  }
320
346
  return _jsx(SelectOptionsPopover, { field: field, values: values, onChange: onChange });
321
347
  }
322
- export const FiltersContent = ({ filters, fields, onChange }) => {
348
+ function FilterBarRow({ filter, field, i18n, size, radius, lastAddedFilterId, onUpdateFilter, onRemove, filterLabelClassName, }) {
349
+ return (_jsxs(ButtonGroup, { className: cn(radius === 'full' &&
350
+ '*:data-[slot]:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:!rounded-r-full [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0'), children: [_jsxs(ButtonGroupText, { size: size, className: cn(filterLabelClassName, radius === 'full' && '!rounded-l-full !rounded-r-none'), children: [field.icon && field.icon, field.label] }), field.type === 'date_metric' ? (_jsx(FilterDateMetricBarPeriodSegments, { field: field, values: filter.values, onChange: (values) => onUpdateFilter({ values }), i18n: i18n, size: size, autoFocus: filter.id === lastAddedFilterId })) : null, _jsx(FilterOperatorDropdown, { field: field, operator: filter.operator, values: filter.values, onChange: (operator) => onUpdateFilter({ operator }) }), field.type === 'date_metric' ? (_jsx(FilterDateMetricBarValueSegment, { field: field, values: filter.values, onChange: (values) => onUpdateFilter({ values }), operator: filter.operator, i18n: i18n, size: size, onOperatorChange: (operator) => onUpdateFilter({ operator }) }, `${filter.id}-${filter.operator}`)) : (_jsx(FilterValueSelector, { field: field, values: filter.values, onChange: (values) => onUpdateFilter({ values }), operator: filter.operator, autoFocus: filter.id === lastAddedFilterId })), _jsx(FilterRemoveButton, { onClick: onRemove, className: cn(radius === 'full' && 'shrink-0 !rounded-l-none !rounded-r-full') })] }));
351
+ }
352
+ export const FiltersContent = ({ filters, fields, onChange, lastAddedFilterId = null }) => {
323
353
  const context = useFilterContext();
324
354
  const fieldsMap = useMemo(() => getFieldsMap(fields), [fields]);
325
355
  const updateFilter = useCallback((filterId, updates) => {
326
356
  onChange(filters.map((filter) => {
327
- if (filter.id === filterId) {
328
- const updatedFilter = { ...filter, ...updates };
329
- if (updates.operator === 'empty' || updates.operator === 'not_empty') {
330
- updatedFilter.values = [];
331
- }
332
- return updatedFilter;
333
- }
334
- return filter;
357
+ if (filter.id !== filterId)
358
+ return filter;
359
+ return mergeFilterUpdate(filter, updates, fieldsMap[filter.field]);
335
360
  }));
336
- }, [filters, onChange]);
361
+ }, [filters, onChange, fieldsMap]);
337
362
  const removeFilter = useCallback((filterId) => {
338
363
  onChange(filters.filter((filter) => filter.id !== filterId));
339
364
  }, [filters, onChange]);
340
365
  return (_jsx("div", { className: cn(filtersContainerVariants({
341
- variant: context.variant,
342
366
  size: context.size,
343
367
  }), context.className), children: filters.map((filter) => {
344
368
  const field = fieldsMap[filter.field];
345
369
  if (!field)
346
370
  return null;
347
- return (_jsxs(ButtonGroup, { children: [_jsxs(ButtonGroupText, { children: [field.icon && field.icon, field.label] }), _jsx(FilterOperatorDropdown, { field: field, operator: filter.operator, values: filter.values, onChange: (operator) => updateFilter(filter.id, { operator }) }), _jsx(FilterValueSelector, { field: field, values: filter.values, onChange: (values) => updateFilter(filter.id, { values }), operator: filter.operator, autoFocus: false }), _jsx(FilterRemoveButton, { onClick: () => removeFilter(filter.id) })] }, filter.id));
371
+ return (_jsx(FilterBarRow, { filter: filter, field: field, i18n: context.i18n, size: context.size, radius: context.radius, lastAddedFilterId: lastAddedFilterId, onUpdateFilter: (updates) => updateFilter(filter.id, updates), onRemove: () => removeFilter(filter.id) }, filter.id));
348
372
  }) }));
349
373
  };
350
374
  function FilterSubmenuContent({ field, currentValues, isMultiSelect, onToggle, i18n, isActive, onActive, onBack, onClose, }) {
@@ -352,9 +376,11 @@ function FilterSubmenuContent({ field, currentValues, isMultiSelect, onToggle, i
352
376
  const [highlightedIndex, setHighlightedIndex] = useState(-1);
353
377
  const inputRef = useRef(null);
354
378
  const baseId = useId();
379
+ const filterCtx = useFilterContext();
380
+ const showSubmenuSearch = shouldShowSubmenuSearchField(field, filterCtx.submenuSearchMinOptions);
355
381
  useEffect(() => {
356
382
  if (isActive) {
357
- if (field.searchable !== false) {
383
+ if (showSubmenuSearch) {
358
384
  inputRef.current?.focus();
359
385
  }
360
386
  else {
@@ -362,7 +388,7 @@ function FilterSubmenuContent({ field, currentValues, isMultiSelect, onToggle, i
362
388
  listbox?.focus();
363
389
  }
364
390
  }
365
- }, [isActive, field.searchable, baseId]);
391
+ }, [isActive, showSubmenuSearch, baseId]);
366
392
  const filteredOptions = useMemo(() => {
367
393
  return (field.options?.filter((option) => {
368
394
  const isSelected = currentValues.includes(option.value);
@@ -388,77 +414,48 @@ function FilterSubmenuContent({ field, currentValues, isMultiSelect, onToggle, i
388
414
  element?.scrollIntoView({ block: 'nearest' });
389
415
  }
390
416
  }, [resolvedHighlight, isActive, baseId]);
391
- return (_jsxs("div", { className: "flex flex-col", onMouseEnter: onActive, children: [field.searchable !== false && (_jsxs(_Fragment, { children: [_jsx(Input, { ref: inputRef, role: "combobox", "aria-autocomplete": "list", "aria-expanded": true, "aria-haspopup": "listbox", "aria-controls": `${baseId}-listbox`, "aria-activedescendant": resolvedHighlight >= 0 ? `${baseId}-item-${resolvedHighlight}` : undefined, placeholder: i18n.placeholders.searchField(field.label || ''), className: cn('h-8 rounded-none border-0 bg-transparent! px-2 text-sm shadow-none', 'focus-visible:border-border focus-visible:ring-0 focus-visible:ring-offset-0', isActive && 'placeholder:text-foreground'), value: searchInput, onBlur: () => isActive && inputRef.current?.focus(), onChange: (e) => {
417
+ const onFilterSubmenuListKeyDown = useCallback((e) => {
418
+ if (e.key === 'ArrowDown') {
419
+ e.preventDefault();
420
+ if (filteredOptions.length > 0) {
421
+ setHighlightedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : 0));
422
+ }
423
+ }
424
+ else if (e.key === 'ArrowUp') {
425
+ e.preventDefault();
426
+ if (filteredOptions.length > 0) {
427
+ setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : filteredOptions.length - 1));
428
+ }
429
+ }
430
+ else if (e.key === 'ArrowLeft') {
431
+ e.preventDefault();
432
+ onBack?.();
433
+ }
434
+ else if (e.key === 'Enter' && resolvedHighlight >= 0) {
435
+ e.preventDefault();
436
+ const option = filteredOptions[resolvedHighlight];
437
+ if (option) {
438
+ onToggle(option.value, currentValues.includes(option.value));
439
+ if (!isMultiSelect) {
440
+ onBack?.();
441
+ }
442
+ }
443
+ }
444
+ else if (e.key === 'Escape') {
445
+ e.preventDefault();
446
+ onClose?.();
447
+ }
448
+ e.stopPropagation();
449
+ }, [filteredOptions, resolvedHighlight, currentValues, isMultiSelect, onToggle, onBack, onClose]);
450
+ return (_jsxs("div", { className: "flex flex-col", onMouseEnter: onActive, children: [showSubmenuSearch && (_jsxs(_Fragment, { children: [_jsx(Input, { ref: inputRef, role: "combobox", "aria-autocomplete": "list", "aria-expanded": true, "aria-haspopup": "listbox", "aria-controls": `${baseId}-listbox`, "aria-activedescendant": resolvedHighlight >= 0 ? `${baseId}-item-${resolvedHighlight}` : undefined, placeholder: i18n.placeholders.searchField(field.label || ''), className: cn('h-8 rounded-none border-0 bg-transparent! px-2 text-sm shadow-none', 'focus-visible:border-border focus-visible:ring-0 focus-visible:ring-offset-0', isActive && 'placeholder:text-foreground'), value: searchInput, onBlur: () => isActive && showSubmenuSearch && inputRef.current?.focus(), onChange: (e) => {
392
451
  setSearchInput(e.target.value);
393
452
  setHighlightedIndex(-1);
394
453
  }, onFocus: () => onActive?.(), onMouseEnter: (e) => {
395
454
  onActive?.();
396
455
  e.stopPropagation();
397
- }, onClick: (e) => e.stopPropagation(), onKeyDown: (e) => {
398
- if (e.key === 'ArrowDown') {
399
- e.preventDefault();
400
- if (filteredOptions.length > 0) {
401
- setHighlightedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : 0));
402
- }
403
- }
404
- else if (e.key === 'ArrowUp') {
405
- e.preventDefault();
406
- if (filteredOptions.length > 0) {
407
- setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : filteredOptions.length - 1));
408
- }
409
- }
410
- else if (e.key === 'ArrowLeft') {
411
- e.preventDefault();
412
- onBack?.();
413
- }
414
- else if (e.key === 'Enter' && resolvedHighlight >= 0) {
415
- e.preventDefault();
416
- const option = filteredOptions[resolvedHighlight];
417
- if (option) {
418
- onToggle(option.value, currentValues.includes(option.value));
419
- if (!isMultiSelect) {
420
- onBack?.();
421
- }
422
- }
423
- }
424
- else if (e.key === 'Escape') {
425
- e.preventDefault();
426
- onClose?.();
427
- }
428
- e.stopPropagation();
429
- } }), _jsx(DropdownMenuSeparator, {})] })), _jsx("div", { className: "relative flex max-h-full", children: _jsx("div", { className: "flex max-h-[min(var(--available-height),24rem)] w-full scroll-pt-2 scroll-pb-2 flex-col overscroll-contain outline-hidden", role: "listbox", id: `${baseId}-listbox`, tabIndex: field.searchable === false ? 0 : -1, onKeyDown: (e) => {
430
- if (field.searchable === false) {
431
- if (e.key === 'ArrowDown') {
432
- e.preventDefault();
433
- if (filteredOptions.length > 0) {
434
- setHighlightedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : 0));
435
- }
436
- }
437
- else if (e.key === 'ArrowUp') {
438
- e.preventDefault();
439
- if (filteredOptions.length > 0) {
440
- setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : filteredOptions.length - 1));
441
- }
442
- }
443
- else if (e.key === 'ArrowLeft') {
444
- e.preventDefault();
445
- onBack?.();
446
- }
447
- else if (e.key === 'Enter' && resolvedHighlight >= 0) {
448
- e.preventDefault();
449
- const option = filteredOptions[resolvedHighlight];
450
- if (option) {
451
- onToggle(option.value, currentValues.includes(option.value));
452
- if (!isMultiSelect) {
453
- onBack?.();
454
- }
455
- }
456
- }
457
- else if (e.key === 'Escape') {
458
- e.preventDefault();
459
- onClose?.();
460
- }
461
- e.stopPropagation();
456
+ }, onClick: (e) => e.stopPropagation(), onKeyDown: onFilterSubmenuListKeyDown }), _jsx(DropdownMenuSeparator, {})] })), _jsx("div", { className: "relative flex max-h-full", children: _jsx("div", { className: "flex max-h-[min(var(--available-height),24rem)] w-full scroll-pt-2 scroll-pb-2 flex-col overscroll-contain outline-hidden", role: "listbox", id: `${baseId}-listbox`, tabIndex: !showSubmenuSearch ? 0 : -1, onKeyDown: (e) => {
457
+ if (!showSubmenuSearch) {
458
+ onFilterSubmenuListKeyDown(e);
462
459
  }
463
460
  }, children: _jsx(ScrollArea, { className: "size-full min-h-0 **:data-[slot=scroll-area-scrollbar]:m-0 [&_[data-slot=scroll-area-viewport]]:h-full [&_[data-slot=scroll-area-viewport]]:overscroll-contain", children: filteredOptions.length === 0 ? (_jsx("div", { className: "py-2 text-center text-sm text-muted-foreground", children: i18n.noResultsFound })) : (_jsx(DropdownMenuGroup, { children: filteredOptions.map((option, index) => {
464
461
  const isSelected = currentValues.includes(option.value);
@@ -470,14 +467,16 @@ function FilterSubmenuContent({ field, currentValues, isMultiSelect, onToggle, i
470
467
  }, onCheckedChange: () => onToggle(option.value, isSelected), children: [option.icon && option.icon, _jsx("span", { className: "truncate", children: option.label })] }, String(option.value)));
471
468
  }) })) }) }) })] }));
472
469
  }
473
- export function Filters({ filters, fields, onChange, className, variant = 'default', size = 'default', radius = 'default', i18n, showSearchInput = true, trigger, allowMultiple = true, menuPopupClassName, enableShortcut = false, shortcutKey = 'f', shortcutLabel = 'F', }) {
470
+ export function Filters({ filters, fields, onChange, className, size = 'sm', radius = 'default', i18n, showSearchInput = true, submenuSearchMinOptions = DEFAULT_SUBMENU_SEARCH_MIN_OPTIONS, trigger, allowMultiple = true, menuPopupClassName, collapsibleAddFilterGroups = false, defaultAddFilterGroupsCollapsed = false, nestedAddFilterGroups = false, collapseAddButton = false, enableShortcut = false, shortcutKey = 'f', shortcutLabel = 'F', }) {
474
471
  const [addFilterOpen, setAddFilterOpen] = useState(false);
475
472
  const [menuSearchInput, setMenuSearchInput] = useState('');
476
473
  const [activeMenu, setActiveMenu] = useState('root');
477
474
  const [openSubMenu, setOpenSubMenu] = useState(null);
475
+ const [openAddFilterSectionIndex, setOpenAddFilterSectionIndex] = useState(null);
478
476
  const [highlightedIndex, setHighlightedIndex] = useState(-1);
479
477
  const [lastAddedFilterId, setLastAddedFilterId] = useState(null);
480
478
  const rootInputRef = useRef(null);
479
+ const prevAddFilterOpenRef = useRef(false);
481
480
  const rootId = useId();
482
481
  useEffect(() => {
483
482
  if (!enableShortcut)
@@ -501,6 +500,7 @@ export function Filters({ filters, fields, onChange, className, variant = 'defau
501
500
  // Track which filter instance is being built in the current Add Filter menu session
502
501
  // Maps fieldKey -> unique filterId created during this open session
503
502
  const [sessionFilterIds, setSessionFilterIds] = useState({});
503
+ const [collapsedAddFilterGroupIndices, setCollapsedAddFilterGroupIndices] = useState(() => new Set());
504
504
  useEffect(() => {
505
505
  if (lastAddedFilterId) {
506
506
  const timer = setTimeout(() => {
@@ -509,34 +509,30 @@ export function Filters({ filters, fields, onChange, className, variant = 'defau
509
509
  return () => clearTimeout(timer);
510
510
  }
511
511
  }, [lastAddedFilterId]);
512
- const mergedI18n = {
512
+ const mergedI18n = useMemo(() => ({
513
513
  ...DEFAULT_I18N,
514
514
  ...i18n,
515
515
  operators: { ...DEFAULT_I18N.operators, ...i18n?.operators },
516
516
  placeholders: { ...DEFAULT_I18N.placeholders, ...i18n?.placeholders },
517
517
  validation: { ...DEFAULT_I18N.validation, ...i18n?.validation },
518
- };
518
+ helpers: { ...DEFAULT_I18N.helpers, ...i18n?.helpers },
519
+ }), [i18n]);
519
520
  const fieldsMap = useMemo(() => getFieldsMap(fields), [fields]);
520
521
  const updateFilter = useCallback((filterId, updates) => {
521
522
  onChange(filters.map((filter) => {
522
- if (filter.id === filterId) {
523
- const updatedFilter = { ...filter, ...updates };
524
- if (updates.operator === 'empty' || updates.operator === 'not_empty') {
525
- updatedFilter.values = [];
526
- }
527
- return updatedFilter;
528
- }
529
- return filter;
523
+ if (filter.id !== filterId)
524
+ return filter;
525
+ return mergeFilterUpdate(filter, updates, fieldsMap[filter.field]);
530
526
  }));
531
- }, [filters, onChange]);
527
+ }, [filters, onChange, fieldsMap]);
532
528
  const removeFilter = useCallback((filterId) => {
533
529
  onChange(filters.filter((filter) => filter.id !== filterId));
534
530
  }, [filters, onChange]);
535
531
  const addFilter = useCallback((fieldKey) => {
536
532
  const field = fieldsMap[fieldKey];
537
533
  if (field && field.key) {
538
- const defaultOperator = field.defaultOperator || (field.type === 'multiselect' ? 'is_any_of' : 'is');
539
- const defaultValues = field.type === 'text' ? [''] : [];
534
+ const defaultOperator = field.defaultOperator || (field.type === 'multiselect' ? 'is_any_of' : field.type === 'date_metric' ? 'value_greater_than' : 'is');
535
+ const defaultValues = field.type === 'text' ? [''] : field.type === 'date_metric' ? [createDefaultDateMetricPayload()] : [];
540
536
  const newFilter = createFilter(fieldKey, defaultOperator, defaultValues);
541
537
  setLastAddedFilterId(newFilter.id);
542
538
  onChange([...filters, newFilter]);
@@ -544,6 +540,18 @@ export function Filters({ filters, fields, onChange, className, variant = 'defau
544
540
  setMenuSearchInput('');
545
541
  }
546
542
  }, [fieldsMap, filters, onChange]);
543
+ const toggleAddFilterGroupCollapsed = useCallback((sectionIndex) => {
544
+ setCollapsedAddFilterGroupIndices((prev) => {
545
+ const next = new Set(prev);
546
+ if (next.has(sectionIndex)) {
547
+ next.delete(sectionIndex);
548
+ }
549
+ else {
550
+ next.add(sectionIndex);
551
+ }
552
+ return next;
553
+ });
554
+ }, []);
547
555
  const selectableFields = useMemo(() => {
548
556
  const flatFields = flattenFields(fields);
549
557
  return flatFields.filter((field) => {
@@ -554,96 +562,322 @@ export function Filters({ filters, fields, onChange, className, variant = 'defau
554
562
  return !filters.some((filter) => filter.field === field.key);
555
563
  });
556
564
  }, [fields, filters, allowMultiple]);
557
- const filteredFields = useMemo(() => {
558
- return selectableFields.filter((f) => !menuSearchInput || f.label?.toLowerCase().includes(menuSearchInput.toLowerCase()));
559
- }, [selectableFields, menuSearchInput]);
565
+ const useGroupedAddMenu = useMemo(() => hasTopLevelFieldGroups(fields), [fields]);
566
+ const { filteredFields, filteredSections } = useMemo(() => {
567
+ const matchesSearch = (f, groupLabel) => {
568
+ if (!menuSearchInput)
569
+ return true;
570
+ const ql = menuSearchInput.toLowerCase();
571
+ if (f.label?.toLowerCase().includes(ql))
572
+ return true;
573
+ if (groupLabel?.toLowerCase().includes(ql))
574
+ return true;
575
+ return false;
576
+ };
577
+ const isSelectableLeaf = (f) => {
578
+ if (!f.key || f.type === 'separator')
579
+ return false;
580
+ if (allowMultiple)
581
+ return true;
582
+ return !filters.some((filter) => filter.field === f.key);
583
+ };
584
+ if (!useGroupedAddMenu) {
585
+ return {
586
+ filteredFields: selectableFields.filter((f) => matchesSearch(f, undefined)),
587
+ filteredSections: undefined,
588
+ };
589
+ }
590
+ const sections = parseTopLevelSections(fields)
591
+ .map((sec) => ({
592
+ groupLabel: sec.groupLabel,
593
+ fields: sec.fields.filter(isSelectableLeaf).filter((f) => matchesSearch(f, sec.groupLabel)),
594
+ }))
595
+ .filter((sec) => sec.fields.length > 0);
596
+ return {
597
+ filteredFields: sections.flatMap((s) => s.fields),
598
+ filteredSections: sections,
599
+ };
600
+ }, [fields, filters, allowMultiple, menuSearchInput, useGroupedAddMenu, selectableFields]);
601
+ const nestedRootRows = useMemo(() => {
602
+ if (!useGroupedAddMenu || !nestedAddFilterGroups) {
603
+ return null;
604
+ }
605
+ if (!filteredSections?.length) {
606
+ return [];
607
+ }
608
+ return filteredSections.map((section, sectionIndex) => {
609
+ const needsSectionSubmenu = Boolean(section.groupLabel) || section.fields.length > 1;
610
+ if (needsSectionSubmenu) {
611
+ return {
612
+ rowType: 'section',
613
+ sectionIndex,
614
+ label: section.groupLabel ?? mergedI18n.addFilterUngroupedSection,
615
+ fields: section.fields,
616
+ };
617
+ }
618
+ return { rowType: 'inlineField', sectionIndex, field: section.fields[0] };
619
+ });
620
+ }, [useGroupedAddMenu, nestedAddFilterGroups, filteredSections, mergedI18n.addFilterUngroupedSection]);
621
+ const flatAddMenuKeyboardFields = useMemo(() => {
622
+ if (nestedAddFilterGroups && useGroupedAddMenu) {
623
+ return filteredFields;
624
+ }
625
+ if (!useGroupedAddMenu || !collapsibleAddFilterGroups || !filteredSections?.length) {
626
+ return filteredFields;
627
+ }
628
+ return filteredSections.flatMap((sec, si) => (collapsedAddFilterGroupIndices.has(si) ? [] : sec.fields));
629
+ }, [nestedAddFilterGroups, useGroupedAddMenu, collapsibleAddFilterGroups, filteredSections, collapsedAddFilterGroupIndices, filteredFields]);
630
+ const rootMenuLength = nestedRootRows !== null ? nestedRootRows.length : flatAddMenuKeyboardFields.length;
631
+ useEffect(() => {
632
+ const justOpened = addFilterOpen && !prevAddFilterOpenRef.current;
633
+ prevAddFilterOpenRef.current = addFilterOpen;
634
+ if (!justOpened) {
635
+ return;
636
+ }
637
+ if (nestedAddFilterGroups && useGroupedAddMenu) {
638
+ setCollapsedAddFilterGroupIndices(new Set());
639
+ return;
640
+ }
641
+ if (collapsibleAddFilterGroups && defaultAddFilterGroupsCollapsed && filteredSections?.length) {
642
+ setCollapsedAddFilterGroupIndices(new Set(filteredSections.map((_, i) => i)));
643
+ }
644
+ else {
645
+ setCollapsedAddFilterGroupIndices(new Set());
646
+ }
647
+ }, [addFilterOpen, collapsibleAddFilterGroups, defaultAddFilterGroupsCollapsed, filteredSections, nestedAddFilterGroups, useGroupedAddMenu]);
560
648
  const rootResolvedHighlight = useMemo(() => {
561
- if (!addFilterOpen || filteredFields.length === 0) {
649
+ if (!addFilterOpen || rootMenuLength === 0) {
562
650
  return -1;
563
651
  }
564
652
  if (highlightedIndex < 0) {
565
653
  return 0;
566
654
  }
567
- return Math.min(highlightedIndex, filteredFields.length - 1);
568
- }, [addFilterOpen, filteredFields.length, highlightedIndex]);
655
+ return Math.min(highlightedIndex, rootMenuLength - 1);
656
+ }, [addFilterOpen, rootMenuLength, highlightedIndex]);
569
657
  useEffect(() => {
570
658
  if (rootResolvedHighlight >= 0 && addFilterOpen) {
571
659
  const element = document.getElementById(`${rootId}-item-${rootResolvedHighlight}`);
572
660
  element?.scrollIntoView({ block: 'nearest' });
573
661
  }
574
662
  }, [rootResolvedHighlight, addFilterOpen, rootId]);
663
+ const addMenuCollapsed = Boolean(collapseAddButton && filters.length > 0);
575
664
  const triggerButton = useRender({
576
- render: trigger,
665
+ render: (addMenuCollapsed ? (_jsx(Button, { variant: "outline", size: size === 'sm' ? 'icon-sm' : size === 'lg' ? 'icon-lg' : 'icon', "aria-label": mergedI18n.addFilterTitle, children: _jsx(Plus, { className: "size-4 shrink-0", "aria-hidden": true }) })) : (trigger)),
577
666
  defaultTagName: 'button',
578
667
  });
579
- return (_jsx(FilterContext.Provider, { value: {
580
- variant,
581
- size,
582
- radius,
583
- i18n: mergedI18n,
584
- className,
585
- trigger,
586
- allowMultiple,
587
- }, children: _jsxs("div", { className: cn(filtersContainerVariants({ variant, size }), className), children: [selectableFields.length > 0 && (_jsxs(DropdownMenu, { open: addFilterOpen, onOpenChange: (open) => {
668
+ const filterContextValue = useMemo(() => ({
669
+ size,
670
+ radius,
671
+ submenuSearchMinOptions,
672
+ i18n: mergedI18n,
673
+ className,
674
+ trigger,
675
+ allowMultiple,
676
+ }), [size, radius, submenuSearchMinOptions, mergedI18n, className, trigger, allowMultiple]);
677
+ const renderAddFilterMenuFieldRow = (field, index, rowOpts) => {
678
+ const rootListHi = rowOpts?.rootListHighlightIndex;
679
+ const isHighlighted = rootListHi === null ? false : rootResolvedHighlight === (rootListHi === undefined ? index : rootListHi);
680
+ const itemId = rowOpts?.itemId ?? `${rootId}-item-${index}`;
681
+ const hasSubMenu = (field.type === 'select' || field.type === 'multiselect') && field.options?.length;
682
+ const syncRootListHighlight = () => {
683
+ if (rowOpts?.rootListHighlightIndex === null)
684
+ return;
685
+ const hi = rowOpts?.rootListHighlightIndex ?? index;
686
+ setHighlightedIndex(hi);
687
+ setActiveMenu('root');
688
+ };
689
+ if (hasSubMenu) {
690
+ const isMultiSelect = field.type === 'multiselect';
691
+ const fieldKey = field.key;
692
+ const sessionFilterId = sessionFilterIds[fieldKey];
693
+ const sessionFilter = sessionFilterId ? filters.find((f) => f.id === sessionFilterId) : null;
694
+ const currentValues = sessionFilter?.values || [];
695
+ return (_jsxs(DropdownMenuSub, { open: openSubMenu === fieldKey, onOpenChange: (open) => {
696
+ if (open) {
697
+ setOpenSubMenu(fieldKey);
698
+ }
699
+ else {
700
+ if (openSubMenu === fieldKey) {
701
+ setOpenSubMenu(null);
702
+ setActiveMenu('root');
703
+ }
704
+ }
705
+ }, children: [_jsxs(DropdownMenuSubTrigger, { id: itemId, role: "option", "aria-selected": isHighlighted, "data-highlighted": isHighlighted || undefined, onMouseEnter: syncRootListHighlight, className: "data-highlighted:bg-accent data-highlighted:text-accent-foreground data-popup-open:bg-accent data-popup-open:text-accent-foreground", children: [field.icon, _jsx("span", { children: field.label })] }), _jsx(DropdownMenuSubContent, { className: "w-[200px]", side: "right", children: _jsx(FilterSubmenuContent, { field: field, currentValues: currentValues, isMultiSelect: isMultiSelect, i18n: mergedI18n, isActive: activeMenu === fieldKey, onActive: () => {
706
+ if (shouldShowSubmenuSearchField(field, filterContextValue.submenuSearchMinOptions)) {
707
+ setActiveMenu(fieldKey);
708
+ }
709
+ }, onBack: () => {
710
+ setOpenSubMenu(null);
711
+ setActiveMenu('root');
712
+ }, onClose: () => setAddFilterOpen(false), onToggle: (value, isSelected) => {
713
+ if (isMultiSelect) {
714
+ const nextValues = isSelected ? currentValues.filter((v) => v !== value) : [...currentValues, value];
715
+ if (sessionFilter) {
716
+ if (nextValues.length === 0) {
717
+ onChange(filters.filter((f) => f.id !== sessionFilter.id));
718
+ setSessionFilterIds((prev) => ({
719
+ ...prev,
720
+ [fieldKey]: '',
721
+ }));
722
+ }
723
+ else {
724
+ onChange(filters.map((f) => (f.id === sessionFilter.id ? { ...f, values: nextValues } : f)));
725
+ }
726
+ }
727
+ else {
728
+ const newFilter = createFilter(fieldKey, field.defaultOperator || 'is_any_of', nextValues);
729
+ onChange([...filters, newFilter]);
730
+ setSessionFilterIds((prev) => ({
731
+ ...prev,
732
+ [fieldKey]: newFilter.id,
733
+ }));
734
+ }
735
+ }
736
+ else {
737
+ const newFilter = createFilter(fieldKey, field.defaultOperator || 'is', [value]);
738
+ setLastAddedFilterId(newFilter.id);
739
+ onChange([...filters, newFilter]);
740
+ setAddFilterOpen(false);
741
+ }
742
+ } }) })] }, fieldKey));
743
+ }
744
+ return (_jsxs(DropdownMenuItem, { id: itemId, role: "option", "aria-selected": isHighlighted, "data-highlighted": isHighlighted || undefined, onMouseEnter: syncRootListHighlight, onClick: () => field.key && addFilter(field.key), className: "data-highlighted:bg-accent data-highlighted:text-accent-foreground", children: [field.icon, _jsx("span", { children: field.label })] }, field.key));
745
+ };
746
+ return (_jsx(FilterContext.Provider, { value: filterContextValue, children: _jsxs("div", { className: cn(filtersContainerVariants({ size }), className), children: [selectableFields.length > 0 && (_jsxs(DropdownMenu, { open: addFilterOpen, onOpenChange: (open) => {
588
747
  setAddFilterOpen(open);
589
748
  if (!open) {
590
749
  setMenuSearchInput('');
591
750
  setSessionFilterIds({});
592
751
  setOpenSubMenu(null);
752
+ setOpenAddFilterSectionIndex(null);
593
753
  setHighlightedIndex(-1);
594
754
  }
595
755
  else {
596
756
  setActiveMenu('root');
597
757
  setHighlightedIndex(-1);
598
758
  }
599
- }, children: [_jsx(DropdownMenuTrigger, { render: triggerButton }), _jsxs(DropdownMenuContent, { className: cn('w-[220px]', menuPopupClassName), align: "start", children: [showSearchInput && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "relative", children: [_jsx(Input, { ref: rootInputRef, role: "combobox", "aria-controls": `${rootId}-listbox`, "aria-activedescendant": rootResolvedHighlight >= 0 ? `${rootId}-item-${rootResolvedHighlight}` : undefined, placeholder: mergedI18n.searchFields, className: cn('h-8 rounded-none border-0 bg-transparent! px-2 text-sm shadow-none', 'focus-visible:border-border focus-visible:ring-0 focus-visible:ring-offset-0', activeMenu === 'root' && 'placeholder:text-foreground'), value: menuSearchInput, onFocus: () => setActiveMenu('root'), onMouseEnter: () => setActiveMenu('root'), onBlur: () => activeMenu === 'root' && rootInputRef.current?.focus(), onChange: (e) => {
759
+ }, children: [_jsx(DropdownMenuTrigger, { render: triggerButton }), _jsxs(DropdownMenuContent, { className: cn('w-[220px]', menuPopupClassName), align: "start", children: [showSearchInput && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "relative", children: [_jsx(Input, { ref: rootInputRef, role: "combobox", "aria-controls": `${rootId}-listbox`, "aria-activedescendant": rootResolvedHighlight >= 0 ? `${rootId}-item-${rootResolvedHighlight}` : undefined, placeholder: mergedI18n.searchFields, className: cn('h-8 rounded-none border-0 bg-transparent! px-2 text-sm shadow-none', 'focus-visible:border-border focus-visible:ring-0 focus-visible:ring-offset-0', activeMenu === 'root' && 'placeholder:text-foreground'), value: menuSearchInput, onFocus: () => setActiveMenu('root'), onMouseEnter: () => setActiveMenu('root'), onBlur: (e) => {
760
+ const rt = e.relatedTarget;
761
+ const menuSurface = e.currentTarget.closest('[data-slot="dropdown-menu-content"]');
762
+ const focusStaysInsideMenu = Boolean(rt) &&
763
+ (Boolean(menuSurface?.contains(rt)) ||
764
+ Boolean(rt?.closest?.('[data-slot="dropdown-menu-sub-content"]')));
765
+ if (activeMenu === 'root' && !focusStaysInsideMenu) {
766
+ rootInputRef.current?.focus();
767
+ }
768
+ }, onChange: (e) => {
600
769
  setMenuSearchInput(e.target.value);
601
770
  setHighlightedIndex(-1);
602
771
  }, onClick: (e) => e.stopPropagation(), onKeyDown: (e) => {
603
772
  if (e.key === 'ArrowDown') {
604
773
  e.preventDefault();
605
- if (filteredFields.length > 0) {
606
- setHighlightedIndex((prev) => (prev < filteredFields.length - 1 ? prev + 1 : 0));
774
+ if (rootMenuLength > 0) {
775
+ setHighlightedIndex((prev) => (prev < rootMenuLength - 1 ? prev + 1 : 0));
607
776
  }
608
777
  }
609
778
  else if (e.key === 'ArrowUp') {
610
779
  e.preventDefault();
611
- if (filteredFields.length > 0) {
612
- setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : filteredFields.length - 1));
780
+ if (rootMenuLength > 0) {
781
+ setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : rootMenuLength - 1));
613
782
  }
614
783
  }
615
784
  else if ((e.key === 'ArrowRight' || e.key === 'ArrowLeft') && rootResolvedHighlight >= 0) {
616
- const field = filteredFields[rootResolvedHighlight];
617
- const hasSubMenu = field && (field.type === 'select' || field.type === 'multiselect') && field.options?.length;
618
- if (e.key === 'ArrowRight' && hasSubMenu) {
619
- e.preventDefault();
620
- setOpenSubMenu(field.key || null);
621
- setActiveMenu(field.key || 'root');
785
+ if (nestedRootRows) {
786
+ const row = nestedRootRows[rootResolvedHighlight];
787
+ if (e.key === 'ArrowRight' && row) {
788
+ e.preventDefault();
789
+ if (row.rowType === 'section') {
790
+ setOpenAddFilterSectionIndex(row.sectionIndex);
791
+ setOpenSubMenu(null);
792
+ setActiveMenu('root');
793
+ }
794
+ else {
795
+ const field = row.field;
796
+ const hasSubMenu = (field.type === 'select' || field.type === 'multiselect') && field.options?.length;
797
+ if (hasSubMenu && field.key) {
798
+ setOpenSubMenu(field.key);
799
+ setActiveMenu(field.key);
800
+ }
801
+ }
802
+ }
803
+ else if (e.key === 'ArrowLeft') {
804
+ e.preventDefault();
805
+ if (openSubMenu) {
806
+ setOpenSubMenu(null);
807
+ setActiveMenu('root');
808
+ }
809
+ else if (openAddFilterSectionIndex !== null) {
810
+ setOpenAddFilterSectionIndex(null);
811
+ setActiveMenu('root');
812
+ }
813
+ }
622
814
  }
623
- else if (e.key === 'ArrowLeft') {
624
- e.preventDefault();
625
- if (openSubMenu) {
626
- setOpenSubMenu(null);
627
- setActiveMenu('root');
815
+ else {
816
+ const field = flatAddMenuKeyboardFields[rootResolvedHighlight];
817
+ const hasSubMenu = field && (field.type === 'select' || field.type === 'multiselect') && field.options?.length;
818
+ if (e.key === 'ArrowRight' && hasSubMenu) {
819
+ e.preventDefault();
820
+ setOpenSubMenu(field.key || null);
821
+ setActiveMenu(field.key || 'root');
822
+ }
823
+ else if (e.key === 'ArrowLeft') {
824
+ e.preventDefault();
825
+ if (openSubMenu) {
826
+ setOpenSubMenu(null);
827
+ setActiveMenu('root');
828
+ }
628
829
  }
629
830
  }
630
831
  }
631
832
  else if (e.key === 'Enter' && rootResolvedHighlight >= 0) {
632
833
  e.preventDefault();
633
- const field = filteredFields[rootResolvedHighlight];
634
- if (field.key) {
635
- const hasSubMenu = (field.type === 'select' || field.type === 'multiselect') && field.options?.length;
636
- if (!hasSubMenu) {
637
- addFilter(field.key);
834
+ if (nestedRootRows) {
835
+ const row = nestedRootRows[rootResolvedHighlight];
836
+ if (row?.rowType === 'section') {
837
+ if (openAddFilterSectionIndex === row.sectionIndex) {
838
+ setOpenAddFilterSectionIndex(null);
839
+ setOpenSubMenu(null);
840
+ setActiveMenu('root');
841
+ }
842
+ else {
843
+ setOpenAddFilterSectionIndex(row.sectionIndex);
844
+ setOpenSubMenu(null);
845
+ setActiveMenu('root');
846
+ }
638
847
  }
639
- else {
640
- if (openSubMenu === field.key) {
848
+ else if (row?.field.key) {
849
+ const fieldKey = row.field.key;
850
+ const field = row.field;
851
+ const hasSubMenu = (field.type === 'select' || field.type === 'multiselect') && field.options?.length;
852
+ if (!hasSubMenu) {
853
+ addFilter(fieldKey);
854
+ }
855
+ else if (openSubMenu === fieldKey) {
641
856
  setOpenSubMenu(null);
642
857
  setActiveMenu('root');
643
858
  }
644
859
  else {
645
- setOpenSubMenu(field.key);
646
- setActiveMenu(field.key);
860
+ setOpenSubMenu(fieldKey);
861
+ setActiveMenu(fieldKey);
862
+ }
863
+ }
864
+ }
865
+ else {
866
+ const field = flatAddMenuKeyboardFields[rootResolvedHighlight];
867
+ if (field.key) {
868
+ const hasSubMenu = (field.type === 'select' || field.type === 'multiselect') && field.options?.length;
869
+ if (!hasSubMenu) {
870
+ addFilter(field.key);
871
+ }
872
+ else {
873
+ if (openSubMenu === field.key) {
874
+ setOpenSubMenu(null);
875
+ setActiveMenu('root');
876
+ }
877
+ else {
878
+ setOpenSubMenu(field.key);
879
+ setActiveMenu(field.key);
880
+ }
647
881
  }
648
882
  }
649
883
  }
@@ -656,78 +890,47 @@ export function Filters({ filters, fields, onChange, className, variant = 'defau
656
890
  if (filteredFields.length === 0) {
657
891
  return _jsx("div", { className: "py-2 text-center text-sm text-muted-foreground", children: mergedI18n.noFieldsFound });
658
892
  }
659
- return filteredFields.map((field, index) => {
660
- const isHighlighted = rootResolvedHighlight === index;
661
- const itemId = `${rootId}-item-${index}`;
662
- const hasSubMenu = (field.type === 'select' || field.type === 'multiselect') && field.options?.length;
663
- if (hasSubMenu) {
664
- const isMultiSelect = field.type === 'multiselect';
665
- const fieldKey = field.key;
666
- const sessionFilterId = sessionFilterIds[fieldKey];
667
- const sessionFilter = sessionFilterId ? filters.find((f) => f.id === sessionFilterId) : null;
668
- const currentValues = sessionFilter?.values || [];
669
- return (_jsxs(DropdownMenuSub, { open: openSubMenu === fieldKey, onOpenChange: (open) => {
670
- if (open) {
671
- setOpenSubMenu(fieldKey);
672
- }
673
- else {
674
- if (openSubMenu === fieldKey) {
675
- setOpenSubMenu(null);
676
- setActiveMenu('root');
677
- }
678
- }
679
- }, children: [_jsxs(DropdownMenuSubTrigger, { id: itemId, role: "option", "aria-selected": isHighlighted, "data-highlighted": isHighlighted || undefined, onMouseEnter: () => {
680
- setHighlightedIndex(index);
681
- setActiveMenu('root');
682
- }, className: "data-highlighted:bg-accent data-highlighted:text-accent-foreground data-popup-open:bg-accent data-popup-open:text-accent-foreground", children: [field.icon, _jsx("span", { children: field.label })] }), _jsx(DropdownMenuSubContent, { className: "w-[200px]", side: "right", children: _jsx(FilterSubmenuContent, { field: field, currentValues: currentValues, isMultiSelect: isMultiSelect, i18n: mergedI18n, isActive: activeMenu === fieldKey, onActive: () => {
683
- if (field.searchable !== false) {
684
- setActiveMenu(fieldKey);
685
- }
686
- }, onBack: () => {
893
+ if (useGroupedAddMenu && filteredSections?.length) {
894
+ if (nestedRootRows !== null) {
895
+ return nestedRootRows.map((row, ri) => (_jsx(Fragment, { children: row.rowType === 'inlineField' ? (_jsx(DropdownMenuGroup, { children: renderAddFilterMenuFieldRow(row.field, ri, {
896
+ itemId: `${rootId}-item-${ri}`,
897
+ rootListHighlightIndex: ri,
898
+ }) })) : (_jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuSub, { open: openAddFilterSectionIndex === row.sectionIndex, onOpenChange: (open) => {
899
+ if (open) {
900
+ setOpenAddFilterSectionIndex(row.sectionIndex);
901
+ setOpenSubMenu(null);
902
+ setActiveMenu('root');
903
+ }
904
+ else {
905
+ setOpenAddFilterSectionIndex((prev) => prev === row.sectionIndex ? null : prev);
687
906
  setOpenSubMenu(null);
688
907
  setActiveMenu('root');
689
- }, onClose: () => setAddFilterOpen(false), onToggle: (value, isSelected) => {
690
- if (isMultiSelect) {
691
- const nextValues = isSelected
692
- ? currentValues.filter((v) => v !== value)
693
- : [...currentValues, value];
694
- if (sessionFilter) {
695
- if (nextValues.length === 0) {
696
- onChange(filters.filter((f) => f.id !== sessionFilter.id));
697
- setSessionFilterIds((prev) => ({
698
- ...prev,
699
- [fieldKey]: '',
700
- }));
701
- }
702
- else {
703
- onChange(filters.map((f) => f.id === sessionFilter.id ? { ...f, values: nextValues } : f));
704
- }
705
- }
706
- else {
707
- const newFilter = createFilter(fieldKey, field.defaultOperator || 'is_any_of', nextValues);
708
- onChange([...filters, newFilter]);
709
- setSessionFilterIds((prev) => ({
710
- ...prev,
711
- [fieldKey]: newFilter.id,
712
- }));
713
- }
714
- }
715
- else {
716
- const newFilter = createFilter(fieldKey, field.defaultOperator || 'is', [
717
- value,
718
- ]);
719
- setLastAddedFilterId(newFilter.id);
720
- onChange([...filters, newFilter]);
721
- setAddFilterOpen(false);
722
- }
723
- } }) })] }, fieldKey));
908
+ }
909
+ }, children: [_jsx(DropdownMenuSubTrigger, { id: `${rootId}-item-${ri}`, role: "option", "aria-selected": rootResolvedHighlight === ri, "data-highlighted": rootResolvedHighlight === ri || undefined, onMouseEnter: () => {
910
+ setHighlightedIndex(ri);
911
+ setActiveMenu('root');
912
+ }, className: "data-highlighted:bg-accent data-highlighted:text-accent-foreground data-popup-open:bg-accent data-popup-open:text-accent-foreground", children: _jsx("span", { className: "min-w-0 flex-1 truncate", children: row.label }) }), _jsx(DropdownMenuSubContent, { className: cn('w-[220px]', menuPopupClassName), side: "right", align: "start", children: _jsx(ScrollArea, { className: "max-h-[min(var(--available-height),22rem)] **:data-[slot=scroll-area-scrollbar]:m-0", children: _jsx(DropdownMenuGroup, { children: row.fields.map((field, fi) => renderAddFilterMenuFieldRow(field, fi, {
913
+ itemId: `${rootId}-s${row.sectionIndex}-i${fi}`,
914
+ rootListHighlightIndex: null,
915
+ })) }) }) })] }) })) }, `add-filter-root-${row.rowType}-${row.sectionIndex}`)));
724
916
  }
725
- return (_jsxs(DropdownMenuItem, { id: itemId, role: "option", "aria-selected": isHighlighted, "data-highlighted": isHighlighted || undefined, onMouseEnter: () => setHighlightedIndex(index), onClick: () => field.key && addFilter(field.key), className: "data-highlighted:bg-accent data-highlighted:text-accent-foreground", children: [field.icon, _jsx("span", { children: field.label })] }, field.key));
726
- });
917
+ let running = 0;
918
+ return filteredSections.map((section, si) => {
919
+ const showCollapsibleHeader = collapsibleAddFilterGroups && (!!section.groupLabel || section.fields.length > 1);
920
+ const isCollapsed = collapsibleAddFilterGroups && showCollapsibleHeader && collapsedAddFilterGroupIndices.has(si);
921
+ return (_jsxs(Fragment, { children: [_jsxs(DropdownMenuGroup, { children: [collapsibleAddFilterGroups && showCollapsibleHeader ? (_jsxs("button", { type: "button", "aria-expanded": !isCollapsed, className: cn('flex w-full items-center gap-1 rounded-md px-1.5 py-1.5 text-left text-xs font-medium text-muted-foreground outline-none select-none', 'hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring'), onClick: (e) => {
922
+ e.preventDefault();
923
+ e.stopPropagation();
924
+ toggleAddFilterGroupCollapsed(si);
925
+ }, children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: section.groupLabel ?? mergedI18n.addFilterUngroupedSection }), _jsx(ChevronDown, { className: cn('size-4 shrink-0 opacity-70 transition-transform', isCollapsed && '-rotate-90'), "aria-hidden": true })] })) : section.groupLabel ? (_jsx(DropdownMenuLabel, { children: section.groupLabel })) : null, (!collapsibleAddFilterGroups || !showCollapsibleHeader || !isCollapsed) &&
926
+ section.fields.map((field) => renderAddFilterMenuFieldRow(field, running++))] }), collapsibleAddFilterGroups && filteredSections.length > 1 && si < filteredSections.length - 1 ? (_jsx(DropdownMenuSeparator, {})) : null] }, `add-filter-section-${si}`));
927
+ });
928
+ }
929
+ return flatAddMenuKeyboardFields.map((field, index) => renderAddFilterMenuFieldRow(field, index));
727
930
  })() }) }) })] })] })), filters.map((filter) => {
728
931
  const field = fieldsMap[filter.field];
729
932
  if (!field)
730
933
  return null;
731
- return (_jsxs(ButtonGroup, { children: [_jsxs(ButtonGroupText, { className: "bg-background dark:bg-input/30", children: [field.icon && field.icon, field.label] }), _jsx(FilterOperatorDropdown, { field: field, operator: filter.operator, values: filter.values, onChange: (operator) => updateFilter(filter.id, { operator }) }), _jsx(FilterValueSelector, { field: field, values: filter.values, operator: filter.operator, onChange: (values) => updateFilter(filter.id, { values }), autoFocus: filter.id === lastAddedFilterId }), _jsx(FilterRemoveButton, { onClick: () => removeFilter(filter.id) })] }, filter.id));
934
+ return (_jsx(FilterBarRow, { filter: filter, field: field, i18n: mergedI18n, size: size, radius: radius, lastAddedFilterId: lastAddedFilterId, onUpdateFilter: (updates) => updateFilter(filter.id, updates), onRemove: () => removeFilter(filter.id), filterLabelClassName: "bg-background dark:bg-input/30" }, filter.id));
732
935
  })] }) }));
733
936
  }