@astryxdesign/core 0.7.7-canary.bf14580 → 0.7.7-canary.c19ca12

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 (266) hide show
  1. package/dist/AlertDialog/AlertDialog.d.ts +4 -1
  2. package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
  3. package/dist/AlertDialog/AlertDialog.js +36 -6
  4. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  5. package/dist/BottomSheet/BottomSheet.js +76 -6
  6. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  7. package/dist/BottomSheet/BottomSheetPanel.js +23 -4
  8. package/dist/BottomSheet/bottomSheetStack.d.ts.map +1 -1
  9. package/dist/BottomSheet/bottomSheetStack.js +5 -4
  10. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  11. package/dist/BottomSheet/useSheetGestures.js +3 -1
  12. package/dist/Button/Button.d.ts.map +1 -1
  13. package/dist/Button/Button.js +47 -3
  14. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  15. package/dist/CheckboxInput/CheckboxInput.js +13 -2
  16. package/dist/ComplexSelector/ComplexSelector.d.ts +4 -1
  17. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  18. package/dist/ComplexSelector/ComplexSelector.js +14 -4
  19. package/dist/ContextMenu/ContextMenu.js +2 -2
  20. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  21. package/dist/DateInput/MonthScroller.js +13 -27
  22. package/dist/DateInput/Wheel.d.ts.map +1 -1
  23. package/dist/DateInput/Wheel.js +22 -41
  24. package/dist/Dialog/Dialog.d.ts.map +1 -1
  25. package/dist/Dialog/Dialog.js +7 -31
  26. package/dist/Field/Field.js +2 -2
  27. package/dist/HoverCard/HoverCard.d.ts +4 -3
  28. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  29. package/dist/HoverCard/HoverCard.js +27 -2
  30. package/dist/HoverCard/useHoverCard.d.ts +5 -4
  31. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  32. package/dist/HoverCard/useHoverCard.js +17 -31
  33. package/dist/Icon/Icon.d.ts +5 -1
  34. package/dist/Icon/Icon.d.ts.map +1 -1
  35. package/dist/Icon/Icon.js +5 -2
  36. package/dist/Layer/layerAnimations.stylex.d.ts +7 -8
  37. package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
  38. package/dist/Layer/layerAnimations.stylex.js +25 -20
  39. package/dist/Layer/usePressPin.d.ts +17 -5
  40. package/dist/Layer/usePressPin.d.ts.map +1 -1
  41. package/dist/Layer/usePressPin.js +62 -12
  42. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  43. package/dist/Lightbox/Lightbox.js +18 -0
  44. package/dist/PowerSearch/PowerSearch.d.ts +36 -3
  45. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  46. package/dist/PowerSearch/PowerSearch.js +522 -269
  47. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +100 -0
  48. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +1 -0
  49. package/dist/PowerSearch/PowerSearchBottomSheet.js +1913 -0
  50. package/dist/PowerSearch/PowerSearchEditPopover.d.ts +32 -1
  51. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  52. package/dist/PowerSearch/PowerSearchEditPopover.js +17 -6
  53. package/dist/PowerSearch/PowerSearchValueEditor.d.ts +38 -3
  54. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  55. package/dist/PowerSearch/PowerSearchValueEditor.js +170 -61
  56. package/dist/PowerSearch/formatFilterValue.d.ts +7 -0
  57. package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
  58. package/dist/PowerSearch/formatFilterValue.js +11 -1
  59. package/dist/PowerSearch/formatTokenValue.d.ts +28 -0
  60. package/dist/PowerSearch/formatTokenValue.d.ts.map +1 -0
  61. package/dist/PowerSearch/formatTokenValue.js +160 -0
  62. package/dist/PowerSearch/powerSearchSheetUtils.d.ts +65 -0
  63. package/dist/PowerSearch/powerSearchSheetUtils.d.ts.map +1 -0
  64. package/dist/PowerSearch/powerSearchSheetUtils.js +168 -0
  65. package/dist/RadioList/RadioList.d.ts.map +1 -1
  66. package/dist/RadioList/RadioList.js +3 -2
  67. package/dist/RadioList/RadioListItem.d.ts +1 -1
  68. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  69. package/dist/RadioList/RadioListItem.js +5 -3
  70. package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts.map +1 -1
  71. package/dist/SegmentedControl/useSegmentedSelectionMotion.js +3 -3
  72. package/dist/Slider/sliderTouchController.js +2 -2
  73. package/dist/Spinner/Spinner.d.ts +1 -1
  74. package/dist/Spinner/Spinner.d.ts.map +1 -1
  75. package/dist/Spinner/Spinner.js +60 -58
  76. package/dist/Switch/useSwitchTouch.js +4 -4
  77. package/dist/Table/BaseTable.d.ts.map +1 -1
  78. package/dist/Table/BaseTable.js +29 -8
  79. package/dist/TextInput/TextInput.d.ts +2 -2
  80. package/dist/TextInput/TextInput.d.ts.map +1 -1
  81. package/dist/TextInput/TextInput.js +10 -7
  82. package/dist/Toast/Toast.d.ts +11 -1
  83. package/dist/Toast/Toast.d.ts.map +1 -1
  84. package/dist/Toast/Toast.js +25 -15
  85. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  86. package/dist/Toast/ToastViewport.js +94 -34
  87. package/dist/Toast/types.d.ts +9 -3
  88. package/dist/Toast/types.d.ts.map +1 -1
  89. package/dist/Token/Token.d.ts +2 -2
  90. package/dist/Token/Token.d.ts.map +1 -1
  91. package/dist/Token/Token.js +12 -4
  92. package/dist/Token/TokenNamingContext.d.ts +14 -0
  93. package/dist/Token/TokenNamingContext.d.ts.map +1 -0
  94. package/dist/Token/TokenNamingContext.js +23 -0
  95. package/dist/Tooltip/useTooltip.d.ts +19 -1
  96. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  97. package/dist/Tooltip/useTooltip.js +34 -53
  98. package/dist/Typeahead/TypeaheadItem.d.ts +2 -1
  99. package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
  100. package/dist/animator/createAnimator.d.ts +98 -0
  101. package/dist/animator/createAnimator.d.ts.map +1 -0
  102. package/dist/animator/createAnimator.js +259 -0
  103. package/dist/astryx.css +22 -6
  104. package/dist/gestures/GestureHost.d.ts +27 -1
  105. package/dist/gestures/GestureHost.d.ts.map +1 -1
  106. package/dist/gestures/GestureHost.js +90 -8
  107. package/dist/gestures/PressTracker.d.ts.map +1 -1
  108. package/dist/gestures/PressTracker.js +4 -3
  109. package/dist/hooks/actionGroupTracker.d.ts +4 -1
  110. package/dist/hooks/actionGroupTracker.d.ts.map +1 -1
  111. package/dist/hooks/actionGroupTracker.js +13 -3
  112. package/dist/hooks/index.d.ts +1 -0
  113. package/dist/hooks/index.d.ts.map +1 -1
  114. package/dist/hooks/index.js +1 -0
  115. package/dist/hooks/pressGroupContext.d.ts +6 -0
  116. package/dist/hooks/pressGroupContext.d.ts.map +1 -0
  117. package/dist/hooks/pressGroupContext.js +20 -0
  118. package/dist/hooks/useActionGroupTracking.d.ts +53 -0
  119. package/dist/hooks/useActionGroupTracking.d.ts.map +1 -0
  120. package/dist/hooks/useActionGroupTracking.js +174 -0
  121. package/dist/hooks/useAnimator.d.ts +41 -0
  122. package/dist/hooks/useAnimator.d.ts.map +1 -0
  123. package/dist/hooks/useAnimator.js +70 -0
  124. package/dist/hooks/useAttachment.d.ts +1 -1
  125. package/dist/hooks/useAttachment.js +1 -1
  126. package/dist/hooks/useInputStatusIcon.d.ts +1 -1
  127. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  128. package/dist/hooks/useInputStatusIcon.js +2 -2
  129. package/dist/hooks/useLongPress.d.ts +6 -2
  130. package/dist/hooks/useLongPress.d.ts.map +1 -1
  131. package/dist/hooks/useLongPress.js +95 -15
  132. package/dist/hooks/useMenuPress.d.ts.map +1 -1
  133. package/dist/hooks/useMenuPress.js +4 -2
  134. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts +8 -0
  135. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +1 -0
  136. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +42 -0
  137. package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.d.ts.map +1 -1
  138. package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.js +6 -0
  139. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts +8 -0
  140. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts.map +1 -0
  141. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.js +14 -0
  142. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  143. package/dist/i18n/generated-locales/en.generated.js +30 -0
  144. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  145. package/dist/i18n/generated-locales/pseudo.generated.js +30 -0
  146. package/dist/index.d.ts +1 -0
  147. package/dist/index.d.ts.map +1 -1
  148. package/dist/index.js +4 -0
  149. package/dist/internal/indicatorPress.stylex.d.ts +20 -0
  150. package/dist/internal/indicatorPress.stylex.d.ts.map +1 -0
  151. package/dist/internal/indicatorPress.stylex.js +26 -0
  152. package/dist/motion/animator.d.ts +14 -14
  153. package/dist/motion/animator.d.ts.map +1 -1
  154. package/dist/motion/animator.js +41 -18
  155. package/dist/motion/reducedMotion.d.ts +24 -0
  156. package/dist/motion/reducedMotion.d.ts.map +1 -1
  157. package/dist/motion/reducedMotion.js +78 -1
  158. package/dist/motion/uikit/alert.d.ts +41 -0
  159. package/dist/motion/uikit/alert.d.ts.map +1 -0
  160. package/dist/motion/uikit/alert.js +43 -0
  161. package/dist/privateAttributes/pressGroupItem.stylex.d.ts +15 -0
  162. package/dist/privateAttributes/pressGroupItem.stylex.d.ts.map +1 -0
  163. package/dist/privateAttributes/pressGroupItem.stylex.js +23 -0
  164. package/dist/utils/clickGuard.d.ts +8 -1
  165. package/dist/utils/clickGuard.d.ts.map +1 -1
  166. package/dist/utils/clickGuard.js +24 -1
  167. package/dist/utils/dateParser.d.ts.map +1 -1
  168. package/dist/utils/dateParser.js +12 -0
  169. package/dist/utils/focusReturn.d.ts +15 -0
  170. package/dist/utils/focusReturn.d.ts.map +1 -1
  171. package/dist/utils/focusReturn.js +28 -0
  172. package/dist/utils/interactionOverlay.stylex.d.ts +3 -2
  173. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  174. package/dist/utils/interactionOverlay.stylex.js +3 -2
  175. package/dist/utils/pressFeedback.d.ts.map +1 -1
  176. package/dist/utils/pressFeedback.js +19 -6
  177. package/dist/utils/triggerFocus.d.ts +36 -0
  178. package/dist/utils/triggerFocus.d.ts.map +1 -0
  179. package/dist/utils/triggerFocus.js +65 -0
  180. package/locales/en.json +120 -0
  181. package/locales/pseudo.json +90 -0
  182. package/package.json +9 -4
  183. package/src/AlertDialog/AlertDialog.doc.mjs +2 -2
  184. package/src/AlertDialog/AlertDialog.tsx +37 -5
  185. package/src/BottomSheet/BottomSheet.tsx +85 -5
  186. package/src/BottomSheet/BottomSheetPanel.tsx +40 -5
  187. package/src/BottomSheet/bottomSheetStack.ts +8 -4
  188. package/src/BottomSheet/useSheetGestures.ts +3 -1
  189. package/src/Button/Button.tsx +63 -5
  190. package/src/CheckboxInput/CheckboxInput.doc.mjs +18 -0
  191. package/src/CheckboxInput/CheckboxInput.tsx +36 -9
  192. package/src/ComplexSelector/ComplexSelector.tsx +20 -5
  193. package/src/ContextMenu/ContextMenu.tsx +2 -2
  194. package/src/DateInput/MonthScroller.tsx +18 -22
  195. package/src/DateInput/Wheel.tsx +30 -32
  196. package/src/Dialog/Dialog.tsx +10 -31
  197. package/src/Field/Field.tsx +2 -2
  198. package/src/HoverCard/HoverCard.doc.mjs +4 -2
  199. package/src/HoverCard/HoverCard.tsx +35 -4
  200. package/src/HoverCard/useHoverCard.doc.mjs +178 -36
  201. package/src/HoverCard/useHoverCard.tsx +46 -52
  202. package/src/Icon/Icon.doc.mjs +49 -8
  203. package/src/Icon/Icon.tsx +15 -7
  204. package/src/Layer/layerAnimations.stylex.ts +24 -16
  205. package/src/Layer/usePressPin.ts +81 -15
  206. package/src/Lightbox/Lightbox.tsx +18 -0
  207. package/src/PowerSearch/PowerSearch.doc.mjs +54 -5
  208. package/src/PowerSearch/PowerSearch.tsx +734 -288
  209. package/src/PowerSearch/PowerSearchBottomSheet.tsx +2515 -0
  210. package/src/PowerSearch/PowerSearchEditPopover.tsx +28 -6
  211. package/src/PowerSearch/PowerSearchValueEditor.tsx +203 -38
  212. package/src/PowerSearch/formatFilterValue.ts +18 -1
  213. package/src/PowerSearch/formatTokenValue.ts +215 -0
  214. package/src/PowerSearch/powerSearchSheetUtils.ts +224 -0
  215. package/src/RadioList/RadioList.tsx +3 -2
  216. package/src/RadioList/RadioListItem.tsx +5 -3
  217. package/src/SegmentedControl/useSegmentedSelectionMotion.ts +6 -3
  218. package/src/Slider/sliderTouchController.ts +2 -2
  219. package/src/Spinner/Spinner.tsx +77 -63
  220. package/src/Switch/useSwitchTouch.ts +4 -4
  221. package/src/Table/BaseTable.tsx +46 -12
  222. package/src/TextInput/TextInput.tsx +11 -10
  223. package/src/Toast/Toast.doc.mjs +4 -3
  224. package/src/Toast/Toast.tsx +52 -32
  225. package/src/Toast/ToastViewport.tsx +121 -35
  226. package/src/Toast/types.ts +9 -3
  227. package/src/Token/Token.tsx +13 -4
  228. package/src/Token/TokenNamingContext.ts +28 -0
  229. package/src/Tooltip/useTooltip.doc.mjs +24 -0
  230. package/src/Tooltip/useTooltip.tsx +61 -56
  231. package/src/Typeahead/TypeaheadItem.doc.mjs +6 -3
  232. package/src/Typeahead/TypeaheadItem.tsx +2 -1
  233. package/src/animator/createAnimator.doc.mjs +153 -0
  234. package/src/animator/createAnimator.ts +324 -0
  235. package/src/gestures/GestureHost.ts +105 -10
  236. package/src/gestures/PressTracker.ts +4 -3
  237. package/src/gestures/README.md +7 -1
  238. package/src/hooks/actionGroupTracker.ts +20 -5
  239. package/src/hooks/index.ts +2 -0
  240. package/src/hooks/pressGroupContext.ts +21 -0
  241. package/src/hooks/useActionGroupTracking.ts +224 -0
  242. package/src/hooks/useAnimator.doc.mjs +109 -0
  243. package/src/hooks/useAnimator.ts +67 -0
  244. package/src/hooks/useAttachment.ts +1 -1
  245. package/src/hooks/useInputStatusIcon.tsx +3 -2
  246. package/src/hooks/useLongPress.doc.mjs +2 -2
  247. package/src/hooks/useLongPress.ts +136 -15
  248. package/src/hooks/useMenuPress.ts +8 -2
  249. package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +43 -0
  250. package/src/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.ts +6 -0
  251. package/src/i18n/generated-english/PowerSearch/formatTokenValue.generated.ts +15 -0
  252. package/src/i18n/generated-locales/en.generated.ts +41 -0
  253. package/src/i18n/generated-locales/pseudo.generated.ts +42 -0
  254. package/src/index.ts +13 -0
  255. package/src/internal/indicatorPress.stylex.ts +24 -0
  256. package/src/motion/README.md +8 -5
  257. package/src/motion/animator.ts +61 -29
  258. package/src/motion/reducedMotion.ts +79 -1
  259. package/src/motion/uikit/alert.ts +43 -0
  260. package/src/privateAttributes/pressGroupItem.stylex.ts +23 -0
  261. package/src/utils/clickGuard.ts +37 -1
  262. package/src/utils/dateParser.ts +16 -0
  263. package/src/utils/focusReturn.ts +30 -0
  264. package/src/utils/interactionOverlay.stylex.ts +3 -2
  265. package/src/utils/pressFeedback.ts +19 -6
  266. package/src/utils/triggerFocus.ts +80 -0
@@ -4,44 +4,57 @@
4
4
 
5
5
  /**
6
6
  * @file PowerSearch.tsx
7
- * @input PowerSearchConfig, filters, onChange
7
+ * @input PowerSearchConfig, filters, onChange; Tokenizer, Field parts, Token,
8
+ * useAdaptivePresentation, and PowerSearchBottomSheet (lazy)
8
9
  * @output Structured filter bar with token-based filter management
9
10
  * @position Main component; forwards DOM ref and exposes tokenizer control via
10
- * handleRef
11
+ * handleRef. The experimental `presentation` option swaps the tokenizer
12
+ * field and editor popover for one trigger that opens PowerSearchBottomSheet.
11
13
  *
12
14
  * SYNC: When modified, update:
13
15
  * - /packages/core/src/PowerSearch/index.ts
16
+ * - /packages/core/src/PowerSearch/PowerSearchBottomSheet.tsx (sheet presentation)
14
17
  * - /packages/cli/assets/templates/blocks/components/PowerSearch/ (showcase blocks)
15
18
  */
16
- import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
19
+ import React, { lazy, Suspense, useCallback, useEffect, useId, useImperativeHandle, useMemo, useRef, useState } from 'react';
17
20
  import * as stylex from '@stylexjs/stylex';
18
21
  import { Tokenizer } from "../Tokenizer/index.js";
19
22
  import { TokenizerClearAllMessageContext } from "../Tokenizer/TokenizerClearAllMessageContext.js";
20
23
  import { TokenizerItemRemovableContext } from "../Tokenizer/TokenizerItemRemovableContext.js";
21
24
  import { TypeaheadItem } from "../Typeahead/TypeaheadItem.js";
22
25
  import { Token } from "../Token/index.js";
26
+ import { TokenNamingContext } from "../Token/TokenNamingContext.js";
23
27
  import { Avatar } from "../Avatar/index.js";
24
28
  import { layerAnimations } from "../Layer/layerAnimations.stylex.js";
25
- import { Icon } from "../Icon/index.js";
29
+ import { Icon, renderIconSlot } from "../Icon/index.js";
30
+ import { Field } from "../Field/Field.js";
31
+ import { inputStatusBorderStyles, inputStatusFocusWithinStyles, inputStatusHoverShadowStyles, inputWrapperStyles } from "../Field/inputStyles.stylex.js";
26
32
  import { usePopover } from "../Popover/usePopover.js";
33
+ import { useTooltip } from "../Tooltip/useTooltip.js";
27
34
  import "../Layer/layerViewportInset.stylex.js";
28
35
  import { layerViewportInset } from "../Layer/layerViewportInset.stylex.js";
29
36
  import { useAnnounce } from "../hooks/useAnnounce.js";
37
+ import { useAdaptivePresentation } from "../hooks/useAdaptivePresentation.js";
38
+ import { useDevWarning } from "../hooks/useDevWarning.js";
39
+ import { useIsHydrating } from "../hooks/useIsHydrating.js";
30
40
  import "../theme/tokens.stylex.js";
31
- import { spacingVars, colorVars, typeScaleVars, fontWeightVars } from "../theme/tokens.stylex.js";
41
+ import { spacingVars, colorVars, sizeVars, typeScaleVars, typographyVars, fontWeightVars } from "../theme/tokens.stylex.js";
32
42
  import { useSize } from "../SizeContext/SizeContext.js";
43
+ import { isFocusDetached } from "../utils/focusReturn.js";
44
+ import { isRenderable } from "../utils/isRenderable.js";
33
45
  import { useInternalConfig } from "./useInternalConfig.js";
34
46
  import { usePowerSearchSource } from "./usePowerSearchSource.js";
35
- import { formatFilterValue, formatDateAbsoluteCompact } from "./formatFilterValue.js";
36
- import { PowerSearchEditPopover } from "./PowerSearchEditPopover.js";
47
+ import { formatFilterValue } from "./formatFilterValue.js";
48
+ import { NestedEditor, PowerSearchEditPopover } from "./PowerSearchEditPopover.js";
49
+ import { buildRelativeDateOptions, PowerSearchValueEditor } from "./PowerSearchValueEditor.js";
37
50
  import { resolveOperatorLabel } from "./resolveOperatorLabel.js";
38
51
  import { themeProps } from "../utils/themeProps.js";
39
- import { truncateCharacters } from "../utils/characters.js";
40
52
  import { useLocale } from "../i18n/useLocale.js";
53
+ import { formatTokenValueText } from "./formatTokenValue.js";
41
54
  import { useMergedRefs } from "../hooks/useMergedRefs.js";
42
55
  import { registerEnglishFallback, useComponentTranslator } from "../i18n/useComponentTranslator.js";
43
56
  import english from "../i18n/generated-english/PowerSearch/PowerSearch.generated.js";
44
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
57
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
45
58
  registerEnglishFallback(english);
46
59
  // =============================================================================
47
60
  // Icon mapping for typeahead entries
@@ -82,219 +95,86 @@ const popoverLayerStyles = {
82
95
  $$css: true
83
96
  }
84
97
  };
85
- function truncateString(value, limit) {
86
- // Same semantics as before — strings within limit + 3 pass through, longer
87
- // ones cut to limit + '...' — but counted in characters, not code units.
88
- return truncateCharacters(value, limit + 3, '...');
89
- }
90
- function getEnumLabel(values, value) {
91
- return values.find(v => v.value === value)?.label ?? value;
98
+ // The sheet presentation's trigger field: the same control box as the
99
+ // tokenizer field, with display-only tokens and one button as its only tab
100
+ // stop.
101
+ const sheetTriggerSizeStyles = {
102
+ sm: {
103
+ kAzted: "x1gdqauq",
104
+ $$css: true
105
+ },
106
+ md: {
107
+ kAzted: "x1uogy3y",
108
+ $$css: true
109
+ },
110
+ lg: {
111
+ kAzted: "x1xf4nls",
112
+ $$css: true
113
+ }
114
+ };
115
+ const sheetTriggerStyles = {
116
+ wrapper: {
117
+ kVAEAm: "x1n2onr6",
118
+ kwnvtZ: "xozqiw3",
119
+ kOIVth: "xzye2dw",
120
+ kZKoxP: "xt7dq6l",
121
+ kkrTdU: "x1ypdohk xvivl9a",
122
+ $$css: true
123
+ },
124
+ wrapperWithTokens: {
125
+ k8WAf4: "x1x7cmac",
126
+ kg3NbH: "xx2oswg",
127
+ $$css: true
128
+ }
129
+ };
130
+
131
+ // The sheet flow (three standalone sheets, the sheet editors) only
132
+ // renders for the bottom-sheet presentation. Loading it on demand keeps it out
133
+ // of pages whose PowerSearch stays a tokenizer field.
134
+ // One import promise, shared by the preload and the lazy element: the sheet
135
+ // presentation loads the flow's chunk as soon as it is active, so the first
136
+ // press does not wait for the network, while nothing of the flow is rendered
137
+ // until a press.
138
+ const loadPowerSearchBottomSheet = async () => import("./PowerSearchBottomSheet.js");
139
+ const LazyPowerSearchBottomSheet = /*#__PURE__*/lazy(async () => loadPowerSearchBottomSheet().then(module => ({
140
+ default: module.PowerSearchBottomSheet
141
+ })));
142
+ // The value step reuses the popover presentation's editors, which this
143
+ // module already loads; see PowerSearchSheetEditors.
144
+ const SHEET_EDITORS = {
145
+ NestedEditor,
146
+ PowerSearchValueEditor,
147
+ buildRelativeDateOptions
148
+ };
149
+
150
+ /**
151
+ * Hosts the lazily loaded sheet flow. Renders nothing on the server or in the
152
+ * render that hydrates server HTML: a lazy component that has not loaded yet
153
+ * would leave server markup incomplete and abort hydration. The closed flow
154
+ * has no visible markup, so it first renders right after hydration.
155
+ */
156
+ function PowerSearchSheetHost(props) {
157
+ const isHydrating = useIsHydrating();
158
+ if (isHydrating) {
159
+ return null;
160
+ }
161
+ return /*#__PURE__*/_jsx(Suspense, {
162
+ fallback: null,
163
+ children: /*#__PURE__*/_jsx(LazyPowerSearchBottomSheet, {
164
+ ...props,
165
+ editors: SHEET_EDITORS
166
+ })
167
+ });
92
168
  }
93
169
  function PowerSearchTokenValue({
94
- operatorValue,
95
- filterValue,
96
- maxLength,
97
- locale
170
+ text
98
171
  }) {
99
- switch (filterValue.type) {
100
- case 'empty':
101
- return null;
102
- case 'string':
103
- return /*#__PURE__*/_jsx("span", {
104
- ...{
105
- className: "x1lvx875"
106
- },
107
- children: truncateString(filterValue.value, maxLength)
108
- });
109
- case 'integer':
110
- case 'float':
111
- return /*#__PURE__*/_jsx("span", {
112
- ...{
113
- className: "x1lvx875"
114
- },
115
- children: filterValue.value
116
- });
117
- case 'enum':
118
- if (operatorValue.type === 'enum') {
119
- return /*#__PURE__*/_jsx("span", {
120
- ...{
121
- className: "x1lvx875"
122
- },
123
- children: truncateString(getEnumLabel(operatorValue.values, filterValue.value), maxLength)
124
- });
125
- }
126
- return /*#__PURE__*/_jsx("span", {
127
- ...{
128
- className: "x1lvx875"
129
- },
130
- children: truncateString(filterValue.value, maxLength)
131
- });
132
- case 'string_list':
133
- {
134
- const items = filterValue.value;
135
- if (items.length === 0) {
136
- return null;
137
- }
138
- if (items.length === 1) {
139
- return /*#__PURE__*/_jsx("span", {
140
- ...{
141
- className: "x1lvx875"
142
- },
143
- children: truncateString(items[0], maxLength)
144
- });
145
- }
146
- const totalLength = items.reduce((sum, s) => sum + s.length, 0);
147
- if (totalLength > maxLength) {
148
- return /*#__PURE__*/_jsxs("span", {
149
- ...{
150
- className: "x1lvx875"
151
- },
152
- children: [items.length, " items"]
153
- });
154
- }
155
- return /*#__PURE__*/_jsx("span", {
156
- ...{
157
- className: "x1lvx875"
158
- },
159
- children: items.join(', ')
160
- });
161
- }
162
- case 'enum_list':
163
- {
164
- const items = filterValue.value;
165
- if (items.length === 0) {
166
- return null;
167
- }
168
- if (operatorValue.type === 'enum_list') {
169
- const labels = items.map(v => getEnumLabel(operatorValue.values, v));
170
- if (labels.length === 1) {
171
- return /*#__PURE__*/_jsx("span", {
172
- ...{
173
- className: "x1lvx875"
174
- },
175
- children: truncateString(labels[0], maxLength)
176
- });
177
- }
178
- const totalLength = labels.reduce((sum, s) => sum + s.length, 0);
179
- if (totalLength > maxLength) {
180
- return /*#__PURE__*/_jsxs("span", {
181
- ...{
182
- className: "x1lvx875"
183
- },
184
- children: [labels.length, " items"]
185
- });
186
- }
187
- return /*#__PURE__*/_jsx("span", {
188
- ...{
189
- className: "x1lvx875"
190
- },
191
- children: labels.join(', ')
192
- });
193
- }
194
- if (items.length === 1) {
195
- return /*#__PURE__*/_jsx("span", {
196
- ...{
197
- className: "x1lvx875"
198
- },
199
- children: truncateString(items[0], maxLength)
200
- });
201
- }
202
- return /*#__PURE__*/_jsxs("span", {
203
- ...{
204
- className: "x1lvx875"
205
- },
206
- children: [items.length, " items"]
207
- });
208
- }
209
- case 'entity_list':
210
- {
211
- const entities = filterValue.value;
212
- if (entities.length === 0) {
213
- return null;
214
- }
215
- if (entities.length === 1) {
216
- return /*#__PURE__*/_jsx("span", {
217
- ...{
218
- className: "x1lvx875"
219
- },
220
- children: truncateString(entities[0].label, maxLength)
221
- });
222
- }
223
- const totalLength = entities.reduce((sum, e) => sum + e.label.length, 0);
224
- if (totalLength > maxLength) {
225
- return /*#__PURE__*/_jsxs("span", {
226
- ...{
227
- className: "x1lvx875"
228
- },
229
- children: [entities.length, " items"]
230
- });
231
- }
232
- return /*#__PURE__*/_jsx("span", {
233
- ...{
234
- className: "x1lvx875"
235
- },
236
- children: entities.map(e => e.label).join(', ')
237
- });
238
- }
239
- case 'time':
240
- return /*#__PURE__*/_jsx("span", {
241
- ...{
242
- className: "x1lvx875"
243
- },
244
- children: filterValue.value
245
- });
246
- case 'date_absolute':
247
- {
248
- const formatted = formatDateAbsoluteCompact(filterValue.unixSeconds, locale);
249
- return /*#__PURE__*/_jsx("span", {
250
- ...{
251
- className: "x1lvx875"
252
- },
253
- children: truncateString(formatted, maxLength)
254
- });
255
- }
256
- case 'date_relative':
257
- return /*#__PURE__*/_jsx("span", {
258
- ...{
259
- className: "x1lvx875"
260
- },
261
- children: truncateString(filterValue.value, maxLength)
262
- });
263
- case 'date_range':
264
- return /*#__PURE__*/_jsx("span", {
265
- ...{
266
- className: "x1lvx875"
267
- },
268
- children: "date range"
269
- });
270
- case 'custom':
271
- if (operatorValue.type === 'custom') {
272
- return /*#__PURE__*/_jsx("span", {
273
- ...{
274
- className: "x1lvx875"
275
- },
276
- children: truncateString(operatorValue.getString(filterValue.value), maxLength)
277
- });
278
- }
279
- return /*#__PURE__*/_jsx("span", {
280
- ...{
281
- className: "x1lvx875"
282
- },
283
- children: filterValue.value
284
- });
285
- case 'nested':
286
- {
287
- const count = filterValue.value.length;
288
- return /*#__PURE__*/_jsx("span", {
289
- ...{
290
- className: "x1lvx875"
291
- },
292
- children: count === 1 ? '1 filter' : `${count} filters`
293
- });
294
- }
295
- default:
296
- return null;
297
- }
172
+ return /*#__PURE__*/_jsx("span", {
173
+ ...{
174
+ className: "x1lvx875"
175
+ },
176
+ children: text
177
+ });
298
178
  }
299
179
 
300
180
  // =============================================================================
@@ -309,6 +189,14 @@ function PowerSearchTokenValue({
309
189
  // Component
310
190
  // =============================================================================
311
191
 
192
+ /**
193
+ * Whether a press on a token with this operator commits the filter instead of
194
+ * opening its editor: an `empty` operator with no `empty` Editor override.
195
+ */
196
+ function commitsWithoutEditor(operator, componentOverrides) {
197
+ return operator?.value.type === 'empty' && componentOverrides?.empty?.Editor == null;
198
+ }
199
+
312
200
  /**
313
201
  * Structured filter bar where each token represents a filter (field + operator + value).
314
202
  *
@@ -408,7 +296,8 @@ export function PowerSearch({
408
296
  xstyle,
409
297
  className,
410
298
  style,
411
- components: componentOverrides
299
+ components: componentOverrides,
300
+ experimental
412
301
  }) {
413
302
  const size = useSize(sizeProp, 'md');
414
303
  const config = useInternalConfig(configProp);
@@ -419,12 +308,129 @@ export function PowerSearch({
419
308
  const placeholder = placeholderFromProps ?? t('@astryx.powersearch.placeholder');
420
309
  const popoverSaveButtonLabel = popoverSaveButtonLabelFromProps ?? t('@astryx.powersearch.editor.apply');
421
310
  const tokenizerRef = useRef(null);
311
+
312
+ // Experimental presentation. `adaptive`
313
+ // resolves through the policy the selection controls share; an open flow
314
+ // keeps its presentation until it closes, so a resolution change mid-task
315
+ // cannot pull the surface out from under the person.
316
+ const presentation = experimental?.presentation ?? 'popover';
317
+ const resolvedPresentation = useAdaptivePresentation(presentation);
318
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
319
+ // Latched for the flow's lifetime: the flow stays presented after it closes
320
+ // until the sheets have run their exit and handed focus back to the trigger.
321
+ // Without the latch, `adaptive` resolving to `popover` mid-session (a phone
322
+ // turned to landscape) would unmount the sheets and the trigger they return
323
+ // focus to in the same render, dropping focus to the page body.
324
+ const [isSheetPresented, setIsSheetPresented] = useState(false);
325
+ const isSheetPresentation = isSheetOpen || isSheetPresented || resolvedPresentation === 'bottom-sheet';
326
+ const triggerRef = useRef(null);
327
+ // `hasAutoFocus` applies only to the control the field first mounts. A
328
+ // presentation swap remounts the control (combobox input or trigger), and
329
+ // React's `autoFocus` on the new one would pull focus from wherever on the
330
+ // page it was; the roots' callback refs hand over the focus the field held
331
+ // instead. A field that never swaps keeps `autoFocus` on every render, so a
332
+ // Dialog that keeps it mounted still finds its `data-autofocus` on open.
333
+ const [autoFocusState, setAutoFocusState] = useState({
334
+ presentation: isSheetPresentation,
335
+ isFirstControl: true
336
+ });
337
+ if (autoFocusState.presentation !== isSheetPresentation) {
338
+ setAutoFocusState({
339
+ presentation: isSheetPresentation,
340
+ isFirstControl: false
341
+ });
342
+ }
343
+ const autoFocusOnMount = hasAutoFocus && autoFocusState.isFirstControl && autoFocusState.presentation === isSheetPresentation;
344
+ // A focus the swap moves continues the same visit to the field: the
345
+ // control that held focus is gone (no blur fires for a removed element), so
346
+ // the caller is told of neither a blur nor a new focus.
347
+ const isSwapFocusRef = useRef(false);
348
+ const focusAfterSwap = useCallback(control => {
349
+ if (control == null) {
350
+ return;
351
+ }
352
+ isSwapFocusRef.current = true;
353
+ try {
354
+ control.focus();
355
+ } finally {
356
+ isSwapFocusRef.current = false;
357
+ }
358
+ }, []);
359
+ const handleFieldFocus = useCallback(event => {
360
+ if (!isSwapFocusRef.current) {
361
+ onFocus?.(event);
362
+ }
363
+ }, [onFocus]);
364
+ const handleFieldBlur = useCallback(event => {
365
+ onBlur?.(event);
366
+ }, [onBlur]);
367
+ // While the flow is open, the sheet announces its own content from inside
368
+ // its dialog; this component announces only with the flow closed, and
369
+ // `useAnnounce` routes those messages by where the field is — into a Dialog
370
+ // or modal Drawer that holds it, or the shared channel when no modal does.
371
+ // A result count that changed during the flow's exit is spoken once the
372
+ // flow has fully closed and focus is back on the trigger: the closing
373
+ // dialog stays modal through its exit, so neither its region nor the
374
+ // page-body channel is heard before.
375
+ const afterCloseResultCountRef = useRef(null);
376
+ const triggerId = useId();
377
+ const triggerLabelId = useId();
378
+ const triggerContentId = useId();
379
+ const statusMessageId = useId();
380
+ // A field that becomes disabled cannot keep its flow open, and must not
381
+ // reopen it when re-enabled. Adjusted in the
382
+ // render that sees the change, not an effect, so no frame shows both.
383
+ const [wasDisabled, setWasDisabled] = useState(isDisabled);
384
+ if (isDisabled !== wasDisabled) {
385
+ setWasDisabled(isDisabled);
386
+ if (isDisabled && isSheetOpen) {
387
+ setIsSheetOpen(false);
388
+ }
389
+ }
390
+ useDevWarning('PowerSearch', '`menuWidth` has no surface in the bottom-sheet presentation: the sheet flow has no typeahead popover to size. Scope it to the popover presentation or leave it out.', isSheetPresentation && menuWidth != null);
391
+ useDevWarning('PowerSearch', '`tokenOverflowBehavior` has no surface in the bottom-sheet presentation: the trigger wraps its display-only tokens. Scope it to the popover presentation or leave it out.', isSheetPresentation && tokenOverflowBehavior != null);
422
392
  const [popoverState, setPopoverStateRaw] = useState({
423
393
  type: 'idle'
424
394
  });
425
395
 
396
+ // A default filter token's visible text: its label (field and operator)
397
+ // and its value. The token's remove button and editor dialog are named by
398
+ // the same text.
399
+ const tokenText = useCallback(filter => {
400
+ const field = config.getField(filter.field);
401
+ const operator = config.getOperator(filter.field, filter.operator);
402
+ const fieldLabel = field?.label ?? '';
403
+ const operatorLabel = operator ? resolveOperatorLabel(operator, t) : '';
404
+ const label = `${fieldLabel}: ${operatorLabel}`.trim();
405
+ const value = operator ? formatTokenValueText(operator.value, filter.value, Math.max(maxTokenLength - fieldLabel.length - operatorLabel.length, 10), locale) : null;
406
+ return {
407
+ label,
408
+ value,
409
+ full: value != null ? `${label} ${value}` : label
410
+ };
411
+ }, [config, maxTokenLength, t, locale]);
412
+ // The built-in token being edited. A token from a `components` override
413
+ // names itself, so its editor keeps the plain popover.
414
+ const editedFilter = popoverState.type === 'editing' ? filters[popoverState.filterIndex] : undefined;
415
+ const editedOperator = editedFilter != null ? config.getOperator(editedFilter.field, editedFilter.operator) : undefined;
416
+ const editedBuiltInFilter = editedOperator == null || componentOverrides?.[editedOperator.value.type]?.Token == null ? editedFilter : undefined;
417
+ // Whether the editor popover is open, for the hand-off below, which runs
418
+ // in the commit that swaps presentations.
419
+ const isPopoverOpenRef = useRef(false);
420
+ // When `adaptive` flips to the sheet while the editor popover is open (a
421
+ // phone rotated), the editing ends with the popover: its state returns to
422
+ // idle (through the raw setter, since the tokenizer input is gone), in the
423
+ // render that sees the swap, so a later switch back does not reopen a stale
424
+ // editor.
425
+ if (isSheetPresentation && popoverState.type !== 'idle') {
426
+ setPopoverStateRaw({
427
+ type: 'idle'
428
+ });
429
+ }
430
+
426
431
  // Layer for positioning the edit popover anchored to the tokenizer
427
432
  const handleLayerHide = useCallback(() => {
433
+ isPopoverOpenRef.current = false;
428
434
  setPopoverStateRaw({
429
435
  type: 'idle'
430
436
  });
@@ -434,13 +440,56 @@ export function PowerSearch({
434
440
  hasLightDismiss: true,
435
441
  hasCloseButton: false,
436
442
  hasAutoFocus: false,
437
- // The popup's own listbox/menu content is the exposed semantics; focus
438
- // stays on the tokenizer input, so a modal dialog wrapper is incorrect.
439
- role: 'none'
443
+ // Adding: the popup's own listbox/menu content is the exposed semantics,
444
+ // and focus stays on the tokenizer input, so a dialog wrapper is
445
+ // incorrect. Editing a token: focus moves into the editor's controls, so
446
+ // the editor is a non-modal dialog named by the token's visible text.
447
+ role: editedBuiltInFilter != null ? 'dialog' : 'none',
448
+ isModal: false,
449
+ dialogLabel: editedBuiltInFilter != null ? tokenText(editedBuiltInFilter).full : undefined
440
450
  });
441
451
 
452
+ // A presentation swap removes the focused control, so the other
453
+ // presentation's primary control (the combobox input or the trigger) takes
454
+ // focus and focus never falls to the page body. Each presentation keys its
455
+ // root, so a swap mounts a new root. The detaching root notes that it held
456
+ // focus, which also covers an input focused by server-rendered `autofocus`
457
+ // before hydration. The editor popover is portaled outside the root, so an
458
+ // open editor counts as held focus. The attaching root gives focus to its
459
+ // primary control when focus has fallen to the page body; the sheet root
460
+ // also hides a popover layer left open.
461
+ const isFocusPendingHandOffRef = useRef(false);
462
+ const hidePopover = popover.hide;
463
+ const handOffFocusRef = useCallback(node => {
464
+ if (node == null) {
465
+ return;
466
+ }
467
+ const trigger = triggerRef.current;
468
+ const isSheetRoot = trigger != null && node.contains(trigger);
469
+ // The sheet presentation preloads the flow's chunk, so the first press
470
+ // opens without waiting for the network; nothing renders.
471
+ if (isSheetRoot) {
472
+ void loadPowerSearchBottomSheet();
473
+ }
474
+ if (isSheetRoot && isPopoverOpenRef.current) {
475
+ isPopoverOpenRef.current = false;
476
+ hidePopover();
477
+ }
478
+ const takesFocus = isFocusPendingHandOffRef.current;
479
+ isFocusPendingHandOffRef.current = false;
480
+ if (takesFocus && isFocusDetached(node.ownerDocument)) {
481
+ focusAfterSwap(isSheetRoot ? trigger : tokenizerRef.current);
482
+ }
483
+ return () => {
484
+ if (node.contains(node.ownerDocument.activeElement) || isPopoverOpenRef.current) {
485
+ isFocusPendingHandOffRef.current = true;
486
+ }
487
+ };
488
+ }, [focusAfterSwap, hidePopover]);
489
+
442
490
  // Wrapper that manages layer visibility and tokenizer focus alongside state
443
491
  const setPopoverState = useCallback(state => {
492
+ isPopoverOpenRef.current = state.type !== 'idle';
444
493
  setPopoverStateRaw(state);
445
494
  if (state.type !== 'idle') {
446
495
  // Schedule after the current frame so React can render the popover
@@ -455,13 +504,22 @@ export function PowerSearch({
455
504
  }
456
505
  }, [popover]);
457
506
 
458
- // Expose imperative handle
507
+ // Expose imperative handle; in the sheet presentation the trigger is the
508
+ // field's focusable control.
459
509
  useImperativeHandle(handleRef, () => ({
460
510
  focusTypeahead() {
461
- tokenizerRef.current?.focus();
511
+ if (isSheetPresentation) {
512
+ triggerRef.current?.focus();
513
+ } else {
514
+ tokenizerRef.current?.focus();
515
+ }
462
516
  },
463
517
  blurTypeahead() {
464
- tokenizerRef.current?.blur();
518
+ if (isSheetPresentation) {
519
+ triggerRef.current?.blur();
520
+ } else {
521
+ tokenizerRef.current?.blur();
522
+ }
465
523
  }
466
524
  }));
467
525
 
@@ -567,7 +625,7 @@ export function PowerSearch({
567
625
  // edit: commit it as that editor's save would, the way adding one does,
568
626
  // instead of opening it. A consumer's `empty` Editor override still opens.
569
627
  const operator = config.getOperator(filter.field, filter.operator);
570
- if (operator?.value.type === 'empty' && componentOverrides?.empty?.Editor == null) {
628
+ if (commitsWithoutEditor(operator, componentOverrides)) {
571
629
  const newFilters = [...filters];
572
630
  newFilters[index] = {
573
631
  field: filter.field,
@@ -622,15 +680,17 @@ export function PowerSearch({
622
680
  });
623
681
  }, [setPopoverState]);
624
682
 
625
- // Custom token renderer
626
- // Custom token renderer
627
- const renderToken = useCallback((item, onRemove) => {
683
+ // Token renderer for both presentations. Given `onRemove`, the token is
684
+ // the tokenizer field's: click edits, × removes. Without it (the sheet
685
+ // presentation's trigger) the token is display-only; the field as a whole
686
+ // opens the flow.
687
+ const renderFilterToken = useCallback((item, onRemove) => {
628
688
  const auxData = item.auxiliaryData;
629
689
  const filterIndex = auxData?.filterIndex ?? -1;
630
690
  const filter = filterIndex >= 0 ? filters[filterIndex] : undefined;
631
691
  const field = auxData ? config.getField(auxData.fieldKey) : undefined;
632
692
  const operator = auxData?.operatorKey ? config.getOperator(auxData.fieldKey, auxData.operatorKey) : undefined;
633
- const canInteract = !isReadOnly && !isDisabled && !filter?.isReadOnly;
693
+ const canInteract = onRemove != null && !isReadOnly && !isDisabled && !filter?.isReadOnly;
634
694
  const handleClick = canInteract ? () => handleTokenClick(filterIndex) : undefined;
635
695
  const handleRemove = canInteract ? onRemove : undefined;
636
696
 
@@ -652,13 +712,14 @@ export function PowerSearch({
652
712
  // Default token rendering
653
713
  const fieldLabel = field?.label ?? '';
654
714
  const operatorLabel = operator ? resolveOperatorLabel(operator, t) : '';
655
- const tokenLabel = `${fieldLabel}: ${operatorLabel}`.trim();
656
- const adjustedMaxLength = Math.max(maxTokenLength - fieldLabel.length - operatorLabel.length, 10);
657
- const valueContent = operator && filter ? /*#__PURE__*/_jsx(PowerSearchTokenValue, {
658
- operatorValue: operator.value,
659
- filterValue: filter.value,
660
- maxLength: adjustedMaxLength,
661
- locale: locale
715
+ const text = filter != null ? tokenText(filter) : {
716
+ label: `${fieldLabel}: ${operatorLabel}`.trim(),
717
+ value: null
718
+ };
719
+ const tokenLabel = text.label;
720
+ const fullText = text.value != null ? `${text.label} ${text.value}` : text.label;
721
+ const valueContent = text.value != null ? /*#__PURE__*/_jsx(PowerSearchTokenValue, {
722
+ text: text.value
662
723
  }) : undefined;
663
724
 
664
725
  // Show entity photo as token icon for single-entity filters
@@ -671,24 +732,39 @@ export function PowerSearch({
671
732
  size: 16
672
733
  });
673
734
  }
674
- return /*#__PURE__*/_jsx(Token, {
675
- label: tokenLabel,
676
- size: size,
677
- icon: tokenIcon,
678
- endContent: valueContent,
679
- onMouseDown: handleClick ? e => {
680
- // Keep a token press from entering Tokenizer's input-focus
681
- // redirect. Click still edits/removes; Tab entry is unchanged.
682
- e.preventDefault();
683
- } : undefined,
684
- onClick: handleClick ? e => {
685
- e.stopPropagation();
686
- handleClick();
687
- } : undefined,
688
- onRemove: handleRemove,
689
- isDisabled: isDisabled
735
+
736
+ // A token that opens its editor says that it opens a dialog, and the
737
+ // naming context names it by all its visible text.
738
+ const editorProps = handleClick != null && !commitsWithoutEditor(operator, componentOverrides) ? {
739
+ 'aria-haspopup': 'dialog',
740
+ 'aria-expanded': popoverState.type === 'editing' && popoverState.filterIndex === filterIndex
741
+ } : null;
742
+ return /*#__PURE__*/_jsx(TokenNamingContext, {
743
+ value: {
744
+ actionName: fullText,
745
+ removeName: fullText
746
+ },
747
+ children: /*#__PURE__*/_jsx(Token, {
748
+ label: tokenLabel,
749
+ size: size,
750
+ icon: tokenIcon,
751
+ ...editorProps,
752
+ endContent: valueContent,
753
+ onMouseDown: handleClick ? e => {
754
+ // Keep a token press from entering Tokenizer's input-focus
755
+ // redirect. Click still edits/removes; Tab entry is unchanged.
756
+ e.preventDefault();
757
+ } : undefined,
758
+ onClick: handleClick ? e => {
759
+ e.stopPropagation();
760
+ handleClick();
761
+ } : undefined,
762
+ onRemove: handleRemove,
763
+ isDisabled: isDisabled
764
+ })
690
765
  });
691
- }, [filters, config, configProp, maxTokenLength, locale, size, isReadOnly, isDisabled, handleTokenClick, componentOverrides, t]);
766
+ }, [filters, config, configProp, maxTokenLength, size, isReadOnly, isDisabled, handleTokenClick, componentOverrides, t, popoverState, tokenText]);
767
+ const renderToken = useCallback((item, onRemove) => renderFilterToken(item, onRemove), [renderFilterToken]);
692
768
 
693
769
  // Custom typeahead item renderer — adds field icon and description
694
770
  const renderItem = useCallback(item => {
@@ -776,28 +852,38 @@ export function PowerSearch({
776
852
  return resultCount;
777
853
  }, [resultCount, t]);
778
854
 
779
- // Announce result-count changes to screen readers through a polite live
780
- // region, mirroring the way Typeahead announces its dropdown result count.
781
- // The count is otherwise only shown visually and stays silent to assistive
782
- // tech. Only a change after mount is spoken: the guard compares against the
783
- // last announced text rather than a mounted flag, so StrictMode's effect
784
- // replay (which resets a flag and would speak the count present on mount
785
- // unprompted) announces nothing until the count changes.
855
+ // This Effect synchronizes a polite live region with the result count,
856
+ // mirroring the way Typeahead announces its dropdown result count: the count
857
+ // is otherwise only shown visually and stays silent to assistive tech. The
858
+ // region follows where the field is: the modal that holds the field (a
859
+ // Dialog, a modal Drawer), else the shared channel. While a sheet of the flow is open the sheet itself announces the
860
+ // count from inside its dialog, and this Effect stays quiet. Only a
861
+ // change after mount is spoken — the count present on mount is not
862
+ // announced unprompted — and the guard compares against the last announced
863
+ // text rather than a mounted flag, so StrictMode's effect replay announces
864
+ // nothing either. A change that lands while the flow is closing waits for
865
+ // the trigger focus, since the dialog is still modal through its exit. The
866
+ // announcement acquires no resource, so there is no cleanup.
786
867
  const announce = useAnnounce();
787
868
  // PowerSearch words its Clear all as its record does; the field announces
788
869
  // these words instead of Tokenizer's (component:Tokenizer AR7).
789
870
  const hasReadOnlyFilter = filters.some(filter => filter.isReadOnly);
790
871
  const clearAllMessage = useMemo(() => hasReadOnlyFilter ? t('@astryx.powersearch.editableFiltersRemoved') : t('@astryx.powersearch.allFiltersRemoved'), [hasReadOnlyFilter, t]);
791
872
  const announcedResultCountRef = useRef(resultCountText);
873
+ const isSheetExiting = isSheetPresented && !isSheetOpen;
792
874
  useEffect(() => {
793
875
  if (resultCountText === announcedResultCountRef.current) {
794
876
  return;
795
877
  }
796
878
  announcedResultCountRef.current = resultCountText;
797
879
  if (resultCountText != null) {
798
- announce(resultCountText);
880
+ if (isSheetExiting) {
881
+ afterCloseResultCountRef.current = resultCountText;
882
+ } else if (!isSheetOpen) {
883
+ announce(resultCountText);
884
+ }
799
885
  }
800
- }, [resultCountText, announce]);
886
+ }, [resultCountText, announce, isSheetExiting, isSheetOpen]);
801
887
 
802
888
  // Build combined endContent from resultCount + endContent props
803
889
  const combinedEndContent = useMemo(() => {
@@ -816,9 +902,176 @@ export function PowerSearch({
816
902
  }
817
903
  return resultCountNode || endContent || undefined;
818
904
  }, [resultCountText, endContent]);
905
+
906
+ // ------------------------------------------------------- sheet trigger --
907
+
908
+ // Disabled-reason tooltip for the sheet trigger. Disabled controls swallow
909
+ // pointer events, so the listeners attach to the field's control box; the
910
+ // trigger stays perceivable through aria-disabled.
911
+ const showsDisabledMessage = isDisabled && !!disabledMessage;
912
+ const disabledMessageTooltip = useTooltip({
913
+ placement: 'above',
914
+ focusTrigger: 'always',
915
+ isEnabled: isSheetPresentation && showsDisabledMessage
916
+ });
917
+ const rootRef = useMergedRefs(ref, popover.triggerRef, handOffFocusRef);
918
+ const openSheet = useCallback(() => {
919
+ if (isDisabled) {
920
+ return;
921
+ }
922
+ // The flow restores focus to the element that was active when it opened;
923
+ // make sure that is the trigger even for a tap on a token.
924
+ triggerRef.current?.focus({
925
+ preventScroll: true
926
+ });
927
+ setIsSheetPresented(true);
928
+ setIsSheetOpen(true);
929
+ }, [isDisabled]);
930
+
931
+ // The field's end lane — the result count and `endContent` — sits outside
932
+ // the trigger button: a caller's action button there keeps its own tab stop
933
+ // and taps, and an interactive control must not nest in a component
934
+ // button, so a tap on it must not open the flow.
935
+ const endLaneRef = useRef(null);
936
+ const handleTriggerWrapperClick = useCallback(event => {
937
+ // Tokens are display-only; a tap on them or the empty space around them
938
+ // opens the flow. The trigger button handles its own click, and the end
939
+ // lane handles its own.
940
+ if (event.target instanceof Node && (triggerRef.current?.contains(event.target) || endLaneRef.current?.contains(event.target))) {
941
+ return;
942
+ }
943
+ openSheet();
944
+ }, [openSheet]);
945
+ const handleTriggerFocusWithin = useCallback(event => {
946
+ // The flow focuses the trigger only after its exit completes; that focus
947
+ // is the signal that the retained sheets can leave. If the resolution
948
+ // moved to the popover meanwhile, the tokenizer input takes over the
949
+ // focus once it mounts (the hand-off above).
950
+ if (!isSheetOpen && isSheetPresented) {
951
+ setIsSheetPresented(false);
952
+ // The dialogs have closed: the page-body channel is exposed again.
953
+ const resultCount = afterCloseResultCountRef.current;
954
+ afterCloseResultCountRef.current = null;
955
+ if (resultCount != null) {
956
+ announce(resultCount);
957
+ }
958
+ }
959
+ if (!(event.relatedTarget instanceof Node) || !event.currentTarget.contains(event.relatedTarget)) {
960
+ handleFieldFocus(event);
961
+ }
962
+ }, [announce, handleFieldFocus, isSheetOpen, isSheetPresented]);
963
+ const handleTriggerBlurWithin = useCallback(event => {
964
+ if (!(event.relatedTarget instanceof Node) || !event.currentTarget.contains(event.relatedTarget)) {
965
+ handleFieldBlur(event);
966
+ }
967
+ }, [handleFieldBlur]);
968
+ if (isSheetPresentation) {
969
+ const triggerDescribedBy = [status?.message ? statusMessageId : null, showsDisabledMessage ? disabledMessageTooltip.describedBy : null].filter(Boolean).join(' ') || undefined;
970
+ return /*#__PURE__*/_jsxs("div", {
971
+ ref: rootRef,
972
+ ...themeProps('power-search'),
973
+ children: [/*#__PURE__*/_jsxs(Field, {
974
+ label: label,
975
+ isLabelHidden: isLabelHidden,
976
+ inputID: triggerId,
977
+ labelID: triggerLabelId,
978
+ isDisabled: isDisabled,
979
+ status: status ? {
980
+ type: status.type,
981
+ message: status.message,
982
+ messageID: status.message ? statusMessageId : undefined
983
+ } : undefined,
984
+ statusVariant: statusVariant,
985
+ xstyle: xstyle,
986
+ className: className,
987
+ style: style,
988
+ children: [/*#__PURE__*/_jsxs("div", {
989
+ ref: disabledMessageTooltip.ref,
990
+ onClick: handleTriggerWrapperClick,
991
+ onFocus: handleTriggerFocusWithin,
992
+ onBlur: handleTriggerBlurWithin,
993
+ "aria-disabled": isDisabled ? true : undefined,
994
+ ...stylex.props(inputWrapperStyles.base, sheetTriggerStyles.wrapper, filters.length > 0 && sheetTriggerStyles.wrapperWithTokens, sheetTriggerSizeStyles[size], isDisabled && inputWrapperStyles.disabled, status && inputStatusBorderStyles[status.type], status && !isDisabled && inputStatusHoverShadowStyles[status.type], status && inputStatusFocusWithinStyles[status.type]),
995
+ children: [/*#__PURE__*/_jsxs("div", {
996
+ id: triggerContentId,
997
+ ...{
998
+ className: "x1n2onr6 xeuugli x1iyjqo2 x78zum5 x6s0dn4 x1a02dak x1lfs0n9 xajebq8"
999
+ },
1000
+ children: [isRenderable(startIcon) && /*#__PURE__*/_jsx("span", {
1001
+ ...{
1002
+ 0: {},
1003
+ 1: {
1004
+ className: "x1xrm450"
1005
+ }
1006
+ }[!!(filters.length > 0) << 0],
1007
+ children: renderIconSlot(startIcon, {
1008
+ size: 'sm',
1009
+ color: 'secondary'
1010
+ })
1011
+ }), tokenizerValue.map(item => /*#__PURE__*/_jsx(React.Fragment, {
1012
+ children: renderFilterToken(item, undefined)
1013
+ }, item.id)), /*#__PURE__*/_jsx("button", {
1014
+ type: "button",
1015
+ id: triggerId,
1016
+ ref: triggerRef
1017
+ // The field's primary control: `data-testid` lands here, as it
1018
+ // lands on the combobox input in the popover presentation.
1019
+ ,
1020
+ "data-testid": testId,
1021
+ autoFocus: autoFocusOnMount,
1022
+ onClick: openSheet,
1023
+ "aria-disabled": isDisabled ? true : undefined,
1024
+ "aria-haspopup": "dialog",
1025
+ "aria-expanded": isSheetOpen,
1026
+ "aria-labelledby": `${triggerLabelId} ${triggerContentId}`,
1027
+ "aria-describedby": triggerDescribedBy,
1028
+ ...{
1029
+ 0: {
1030
+ className: "xjyslct xfoc8du x1fns5xo xkh2ocl x78zum5 x6s0dn4 xt970qd x7a5moj x1ghz6dp xc342km xng3xce xjbqb8w xv1l7n4 x9ynric xjm74w1 xw6l6zx x1yc453h xuxw1ft x1ypdohk xvivl9a x1a2a7pz"
1031
+ },
1032
+ 1: {
1033
+ className: "xjyslct xfoc8du xkh2ocl x78zum5 x6s0dn4 xt970qd x7a5moj x1ghz6dp xc342km xng3xce xjbqb8w xv1l7n4 x9ynric xjm74w1 xw6l6zx x1yc453h xuxw1ft x1ypdohk xvivl9a x1a2a7pz x10l6tqk x10a8y8t xeuugli x1717udv"
1034
+ }
1035
+ }[!!(filters.length > 0) << 0],
1036
+ children: filters.length === 0 ? placeholder : null
1037
+ })]
1038
+ }), (endContent || isRenderable(resultCountText)) && /*#__PURE__*/_jsxs("div", {
1039
+ ref: endLaneRef,
1040
+ ...{
1041
+ className: "x78zum5 x6s0dn4 x1txdalj x2lah0s x1nryj5t"
1042
+ },
1043
+ children: [isRenderable(resultCountText) && /*#__PURE__*/_jsx("span", {
1044
+ ...{
1045
+ className: "x141an7d x1ltkj2j xv1l7n4 xuxw1ft"
1046
+ },
1047
+ children: resultCountText
1048
+ }), endContent]
1049
+ })]
1050
+ }), showsDisabledMessage && disabledMessageTooltip.renderTooltip(disabledMessage)]
1051
+ }), (isSheetOpen || isSheetPresented) && /*#__PURE__*/_jsx(PowerSearchSheetHost, {
1052
+ isOpen: isSheetOpen,
1053
+ onOpenChange: setIsSheetOpen,
1054
+ finalFocusRef: triggerRef,
1055
+ resultCountText: resultCountText,
1056
+ config: config,
1057
+ configProp: configProp,
1058
+ filters: filters,
1059
+ onChange: onChange,
1060
+ searchSource: searchSource,
1061
+ placeholder: placeholder,
1062
+ isReadOnly: isReadOnly,
1063
+ isDisabled: isDisabled,
1064
+ hasClear: hasClear,
1065
+ maxOperatorMenuItems: maxOperatorMenuItems,
1066
+ maxSearchResults: maxSearchResults,
1067
+ saveButtonLabel: popoverSaveButtonLabelFromProps,
1068
+ components: componentOverrides
1069
+ })]
1070
+ }, "sheet");
1071
+ }
819
1072
  return /*#__PURE__*/_jsxs(_Fragment, {
820
1073
  children: [/*#__PURE__*/_jsx("div", {
821
- ref: useMergedRefs(ref, popover.triggerRef),
1074
+ ref: rootRef,
822
1075
  ...themeProps('power-search'),
823
1076
  children: /*#__PURE__*/_jsx(TokenizerClearAllMessageContext, {
824
1077
  value: clearAllMessage,
@@ -836,7 +1089,7 @@ export function PowerSearch({
836
1089
  maxMenuItems: MAX_BROWSE_MENU_ITEMS,
837
1090
  menuWidth: menuWidth,
838
1091
  placeholder: filters.length === 0 ? placeholder : '',
839
- hasAutoFocus: hasAutoFocus,
1092
+ hasAutoFocus: autoFocusOnMount,
840
1093
  hasClear: hasClear && !isReadOnly,
841
1094
  startIcon: startIcon,
842
1095
  endContent: combinedEndContent,
@@ -848,8 +1101,8 @@ export function PowerSearch({
848
1101
  debounceMs: 0,
849
1102
  status: status,
850
1103
  statusVariant: statusVariant,
851
- onFocus: onFocus,
852
- onBlur: onBlur,
1104
+ onFocus: handleFieldFocus,
1105
+ onBlur: handleFieldBlur,
853
1106
  containerXstyle: xstyle,
854
1107
  containerClassName: className,
855
1108
  containerStyle: style,
@@ -857,7 +1110,7 @@ export function PowerSearch({
857
1110
  })
858
1111
  })
859
1112
  })
860
- }), popover.render(popoverContent, {
1113
+ }, "popover"), popover.render(popoverContent, {
861
1114
  placement: 'below',
862
1115
  alignment: 'start',
863
1116
  offset: spacingVars['--spacing-1'],