@astryxdesign/core 0.7.7-canary.c0929c8 → 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 (472) 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/AppShell/useCloseMobileDrawerOnActivate.d.ts +13 -0
  5. package/dist/AppShell/useCloseMobileDrawerOnActivate.d.ts.map +1 -0
  6. package/dist/AppShell/useCloseMobileDrawerOnActivate.js +45 -0
  7. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  8. package/dist/BottomSheet/BottomSheet.js +79 -9
  9. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  10. package/dist/BottomSheet/BottomSheetPanel.js +23 -4
  11. package/dist/BottomSheet/bottomSheetStack.d.ts.map +1 -1
  12. package/dist/BottomSheet/bottomSheetStack.js +5 -4
  13. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  14. package/dist/BottomSheet/useSheetGestures.js +3 -1
  15. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +2 -1
  16. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  17. package/dist/Breadcrumbs/BreadcrumbItem.js +7 -3
  18. package/dist/Button/Button.d.ts.map +1 -1
  19. package/dist/Button/Button.js +47 -3
  20. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  21. package/dist/CheckboxInput/CheckboxInput.js +13 -2
  22. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  23. package/dist/CheckboxList/CheckboxList.js +7 -1
  24. package/dist/CheckboxList/CheckboxListContext.d.ts +6 -0
  25. package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
  26. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  27. package/dist/CheckboxList/CheckboxListItem.js +10 -2
  28. package/dist/ComplexSelector/ComplexSelector.d.ts +4 -1
  29. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  30. package/dist/ComplexSelector/ComplexSelector.js +14 -4
  31. package/dist/ContextMenu/ContextMenu.js +2 -2
  32. package/dist/DateInput/DateInput.d.ts.map +1 -1
  33. package/dist/DateInput/DateInput.js +5 -2
  34. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  35. package/dist/DateInput/MonthScroller.js +13 -27
  36. package/dist/DateInput/Wheel.d.ts.map +1 -1
  37. package/dist/DateInput/Wheel.js +22 -41
  38. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  39. package/dist/DateTimeInput/DateTimeInput.js +5 -2
  40. package/dist/Dialog/Dialog.d.ts.map +1 -1
  41. package/dist/Dialog/Dialog.js +23 -45
  42. package/dist/Drawer/Drawer.d.ts.map +1 -1
  43. package/dist/Drawer/Drawer.js +15 -11
  44. package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -1
  45. package/dist/Drawer/useDrawerDialogPresence.js +5 -1
  46. package/dist/Field/Field.js +2 -2
  47. package/dist/Field/PanelSearchInput.d.ts +9 -1
  48. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  49. package/dist/Field/PanelSearchInput.js +3 -1
  50. package/dist/HoverCard/HoverCard.d.ts +4 -3
  51. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  52. package/dist/HoverCard/HoverCard.js +27 -2
  53. package/dist/HoverCard/useHoverCard.d.ts +5 -4
  54. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  55. package/dist/HoverCard/useHoverCard.js +17 -31
  56. package/dist/Icon/Icon.d.ts +5 -1
  57. package/dist/Icon/Icon.d.ts.map +1 -1
  58. package/dist/Icon/Icon.js +5 -2
  59. package/dist/InfoTip/InfoTip.d.ts +63 -0
  60. package/dist/InfoTip/InfoTip.d.ts.map +1 -0
  61. package/dist/InfoTip/InfoTip.js +126 -0
  62. package/dist/InfoTip/index.d.ts +10 -0
  63. package/dist/InfoTip/index.d.ts.map +1 -0
  64. package/dist/InfoTip/index.js +12 -0
  65. package/dist/Layer/layerAnimations.stylex.d.ts +7 -8
  66. package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
  67. package/dist/Layer/layerAnimations.stylex.js +25 -20
  68. package/dist/Layer/useBackdropPress.d.ts +27 -0
  69. package/dist/Layer/useBackdropPress.d.ts.map +1 -0
  70. package/dist/Layer/useBackdropPress.js +53 -0
  71. package/dist/Layer/usePressPin.d.ts +20 -10
  72. package/dist/Layer/usePressPin.d.ts.map +1 -1
  73. package/dist/Layer/usePressPin.js +66 -18
  74. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  75. package/dist/Lightbox/Lightbox.js +18 -0
  76. package/dist/MobileNav/MobileNav.js +0 -1
  77. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  78. package/dist/MultiSelector/hooks.js +2 -4
  79. package/dist/Outline/Outline.d.ts +2 -4
  80. package/dist/Outline/Outline.d.ts.map +1 -1
  81. package/dist/Outline/Outline.js +4 -52
  82. package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
  83. package/dist/Outline/useOutlineFromMarkdown.js +1 -0
  84. package/dist/Outline/useScrollSpy.js +3 -3
  85. package/dist/PowerSearch/PowerSearch.d.ts +36 -3
  86. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  87. package/dist/PowerSearch/PowerSearch.js +571 -302
  88. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +100 -0
  89. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +1 -0
  90. package/dist/PowerSearch/PowerSearchBottomSheet.js +1913 -0
  91. package/dist/PowerSearch/PowerSearchEditPopover.d.ts +32 -1
  92. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  93. package/dist/PowerSearch/PowerSearchEditPopover.js +17 -6
  94. package/dist/PowerSearch/PowerSearchValueEditor.d.ts +38 -3
  95. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  96. package/dist/PowerSearch/PowerSearchValueEditor.js +170 -61
  97. package/dist/PowerSearch/formatFilterValue.d.ts +7 -0
  98. package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
  99. package/dist/PowerSearch/formatFilterValue.js +11 -1
  100. package/dist/PowerSearch/formatTokenValue.d.ts +28 -0
  101. package/dist/PowerSearch/formatTokenValue.d.ts.map +1 -0
  102. package/dist/PowerSearch/formatTokenValue.js +160 -0
  103. package/dist/PowerSearch/powerSearchSheetUtils.d.ts +65 -0
  104. package/dist/PowerSearch/powerSearchSheetUtils.d.ts.map +1 -0
  105. package/dist/PowerSearch/powerSearchSheetUtils.js +168 -0
  106. package/dist/RadioList/RadioList.d.ts.map +1 -1
  107. package/dist/RadioList/RadioList.js +8 -2
  108. package/dist/RadioList/RadioListItem.d.ts +1 -1
  109. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  110. package/dist/RadioList/RadioListItem.js +5 -3
  111. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  112. package/dist/Resizable/ResizeHandle.js +118 -260
  113. package/dist/Resizable/resizeGesture.d.ts +91 -0
  114. package/dist/Resizable/resizeGesture.d.ts.map +1 -0
  115. package/dist/Resizable/resizeGesture.js +251 -0
  116. package/dist/SegmentedControl/SegmentedControl.d.ts +3 -1
  117. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  118. package/dist/SegmentedControl/SegmentedControl.js +115 -8
  119. package/dist/SegmentedControl/SegmentedControlContext.d.ts +6 -0
  120. package/dist/SegmentedControl/SegmentedControlContext.d.ts.map +1 -1
  121. package/dist/SegmentedControl/SegmentedControlItem.d.ts +1 -0
  122. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  123. package/dist/SegmentedControl/SegmentedControlItem.js +8 -1
  124. package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts +357 -0
  125. package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts.map +1 -0
  126. package/dist/SegmentedControl/useSegmentedSelectionMotion.js +1349 -0
  127. package/dist/SegmentedControl/useSegmentedTouchTracking.d.ts +70 -0
  128. package/dist/SegmentedControl/useSegmentedTouchTracking.d.ts.map +1 -0
  129. package/dist/SegmentedControl/useSegmentedTouchTracking.js +264 -0
  130. package/dist/Selector/Selector.d.ts.map +1 -1
  131. package/dist/Selector/Selector.js +96 -67
  132. package/dist/Selector/optionShape.d.ts +24 -0
  133. package/dist/Selector/optionShape.d.ts.map +1 -0
  134. package/dist/Selector/optionShape.js +48 -0
  135. package/dist/Selector/selectableOptionEntries.d.ts +20 -0
  136. package/dist/Selector/selectableOptionEntries.d.ts.map +1 -0
  137. package/dist/Selector/selectableOptionEntries.js +38 -0
  138. package/dist/Selector/utils.d.ts +2 -17
  139. package/dist/Selector/utils.d.ts.map +1 -1
  140. package/dist/Selector/utils.js +3 -51
  141. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  142. package/dist/SideNav/SideNavItem.js +3 -9
  143. package/dist/Slider/sliderTouchController.js +2 -2
  144. package/dist/Spinner/Spinner.d.ts +1 -1
  145. package/dist/Spinner/Spinner.d.ts.map +1 -1
  146. package/dist/Spinner/Spinner.js +60 -58
  147. package/dist/Switch/switchTouch.d.ts +0 -7
  148. package/dist/Switch/switchTouch.d.ts.map +1 -1
  149. package/dist/Switch/switchTouch.js +2 -10
  150. package/dist/Switch/useSwitchTouch.d.ts +2 -11
  151. package/dist/Switch/useSwitchTouch.d.ts.map +1 -1
  152. package/dist/Switch/useSwitchTouch.js +35 -93
  153. package/dist/Table/BaseTable.d.ts.map +1 -1
  154. package/dist/Table/BaseTable.js +29 -8
  155. package/dist/TextInput/TextInput.d.ts +2 -2
  156. package/dist/TextInput/TextInput.d.ts.map +1 -1
  157. package/dist/TextInput/TextInput.js +10 -7
  158. package/dist/Toast/Toast.d.ts +11 -1
  159. package/dist/Toast/Toast.d.ts.map +1 -1
  160. package/dist/Toast/Toast.js +30 -15
  161. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  162. package/dist/Toast/ToastViewport.js +94 -34
  163. package/dist/Toast/types.d.ts +9 -3
  164. package/dist/Toast/types.d.ts.map +1 -1
  165. package/dist/Token/Token.d.ts +2 -2
  166. package/dist/Token/Token.d.ts.map +1 -1
  167. package/dist/Token/Token.js +13 -4
  168. package/dist/Token/TokenNamingContext.d.ts +14 -0
  169. package/dist/Token/TokenNamingContext.d.ts.map +1 -0
  170. package/dist/Token/TokenNamingContext.js +23 -0
  171. package/dist/Tokenizer/Tokenizer.d.ts +46 -59
  172. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  173. package/dist/Tokenizer/Tokenizer.js +40 -57
  174. package/dist/Tokenizer/TokenizerBottomSheet.d.ts +25 -28
  175. package/dist/Tokenizer/TokenizerBottomSheet.d.ts.map +1 -1
  176. package/dist/Tokenizer/TokenizerBottomSheet.js +157 -54
  177. package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts +3 -0
  178. package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts.map +1 -0
  179. package/dist/Tokenizer/TokenizerItemRemovableContext.js +19 -0
  180. package/dist/Tooltip/Tooltip.d.ts +25 -30
  181. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  182. package/dist/Tooltip/Tooltip.js +24 -5
  183. package/dist/Tooltip/index.d.ts +1 -1
  184. package/dist/Tooltip/index.d.ts.map +1 -1
  185. package/dist/Tooltip/useTooltip.d.ts +45 -28
  186. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  187. package/dist/Tooltip/useTooltip.js +52 -59
  188. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  189. package/dist/TopNav/TopNavItem.js +4 -7
  190. package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
  191. package/dist/TopNav/TopNavMegaMenuItem.js +4 -7
  192. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  193. package/dist/TopNav/TopNavMenu.js +4 -7
  194. package/dist/Typeahead/TypeaheadItem.d.ts +2 -1
  195. package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
  196. package/dist/animator/createAnimator.d.ts +98 -0
  197. package/dist/animator/createAnimator.d.ts.map +1 -0
  198. package/dist/animator/createAnimator.js +259 -0
  199. package/dist/astryx.css +96 -7
  200. package/dist/gestures/GestureHost.d.ts +27 -1
  201. package/dist/gestures/GestureHost.d.ts.map +1 -1
  202. package/dist/gestures/GestureHost.js +157 -21
  203. package/dist/gestures/PressTracker.d.ts.map +1 -1
  204. package/dist/gestures/PressTracker.js +4 -3
  205. package/dist/hooks/actionGroupTracker.d.ts +7 -4
  206. package/dist/hooks/actionGroupTracker.d.ts.map +1 -1
  207. package/dist/hooks/actionGroupTracker.js +19 -56
  208. package/dist/hooks/index.d.ts +1 -0
  209. package/dist/hooks/index.d.ts.map +1 -1
  210. package/dist/hooks/index.js +1 -0
  211. package/dist/hooks/pressGroupContext.d.ts +6 -0
  212. package/dist/hooks/pressGroupContext.d.ts.map +1 -0
  213. package/dist/hooks/pressGroupContext.js +20 -0
  214. package/dist/hooks/useActionGroupTracking.d.ts +53 -0
  215. package/dist/hooks/useActionGroupTracking.d.ts.map +1 -0
  216. package/dist/hooks/useActionGroupTracking.js +174 -0
  217. package/dist/hooks/useAnimator.d.ts +41 -0
  218. package/dist/hooks/useAnimator.d.ts.map +1 -0
  219. package/dist/hooks/useAnimator.js +70 -0
  220. package/dist/hooks/useAttachment.d.ts +1 -1
  221. package/dist/hooks/useAttachment.js +1 -1
  222. package/dist/hooks/useHighlightedOptionScroll.d.ts +11 -1
  223. package/dist/hooks/useHighlightedOptionScroll.d.ts.map +1 -1
  224. package/dist/hooks/useHighlightedOptionScroll.js +112 -11
  225. package/dist/hooks/useInputStatusIcon.d.ts +1 -1
  226. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  227. package/dist/hooks/useInputStatusIcon.js +2 -2
  228. package/dist/hooks/useLongPress.d.ts +6 -2
  229. package/dist/hooks/useLongPress.d.ts.map +1 -1
  230. package/dist/hooks/useLongPress.js +95 -15
  231. package/dist/hooks/useMediaQuery.d.ts +2 -1
  232. package/dist/hooks/useMediaQuery.d.ts.map +1 -1
  233. package/dist/hooks/useMediaQuery.js +7 -4
  234. package/dist/hooks/useMenuPress.d.ts.map +1 -1
  235. package/dist/hooks/useMenuPress.js +4 -2
  236. package/dist/i18n/generated-english/InfoTip/InfoTip.generated.d.ts +8 -0
  237. package/dist/i18n/generated-english/InfoTip/InfoTip.generated.d.ts.map +1 -0
  238. package/dist/i18n/generated-english/InfoTip/InfoTip.generated.js +12 -0
  239. package/dist/i18n/generated-english/PowerSearch/PowerSearch.generated.d.ts.map +1 -1
  240. package/dist/i18n/generated-english/PowerSearch/PowerSearch.generated.js +1 -0
  241. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts +8 -0
  242. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +1 -0
  243. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +42 -0
  244. package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.d.ts.map +1 -1
  245. package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.js +6 -0
  246. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts +8 -0
  247. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts.map +1 -0
  248. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.js +14 -0
  249. package/dist/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.d.ts.map +1 -1
  250. package/dist/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.js +7 -1
  251. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  252. package/dist/i18n/generated-locales/en.generated.js +33 -1
  253. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  254. package/dist/i18n/generated-locales/pseudo.generated.js +33 -1
  255. package/dist/index.d.ts +2 -0
  256. package/dist/index.d.ts.map +1 -1
  257. package/dist/index.js +5 -0
  258. package/dist/internal/SheetConfirmation.d.ts +11 -0
  259. package/dist/internal/SheetConfirmation.d.ts.map +1 -0
  260. package/dist/internal/SheetConfirmation.js +92 -0
  261. package/dist/internal/indicatorPress.stylex.d.ts +20 -0
  262. package/dist/internal/indicatorPress.stylex.d.ts.map +1 -0
  263. package/dist/internal/indicatorPress.stylex.js +26 -0
  264. package/dist/internal/sheetDraft.d.ts +35 -0
  265. package/dist/internal/sheetDraft.d.ts.map +1 -0
  266. package/dist/internal/sheetDraft.js +65 -0
  267. package/dist/internal/touch-sheet.d.ts +21 -0
  268. package/dist/internal/touch-sheet.d.ts.map +1 -0
  269. package/dist/internal/touch-sheet.js +19 -0
  270. package/dist/internal/useDismissSearchOnTouchDrag.d.ts +24 -0
  271. package/dist/internal/useDismissSearchOnTouchDrag.d.ts.map +1 -0
  272. package/dist/internal/useDismissSearchOnTouchDrag.js +56 -0
  273. package/dist/motion/animator.d.ts +14 -14
  274. package/dist/motion/animator.d.ts.map +1 -1
  275. package/dist/motion/animator.js +41 -18
  276. package/dist/motion/reducedMotion.d.ts +24 -0
  277. package/dist/motion/reducedMotion.d.ts.map +1 -1
  278. package/dist/motion/reducedMotion.js +78 -1
  279. package/dist/motion/roles.d.ts +7 -1
  280. package/dist/motion/roles.d.ts.map +1 -1
  281. package/dist/motion/roles.js +20 -1
  282. package/dist/motion/uikit/alert.d.ts +41 -0
  283. package/dist/motion/uikit/alert.d.ts.map +1 -0
  284. package/dist/motion/uikit/alert.js +43 -0
  285. package/dist/motion/uikit/index.d.ts +6 -5
  286. package/dist/motion/uikit/index.d.ts.map +1 -1
  287. package/dist/motion/uikit/index.js +6 -5
  288. package/dist/motion/uikit/segmented.d.ts +55 -0
  289. package/dist/motion/uikit/segmented.d.ts.map +1 -0
  290. package/dist/motion/uikit/segmented.js +77 -0
  291. package/dist/motion/uikit/springs.d.ts +0 -18
  292. package/dist/motion/uikit/springs.d.ts.map +1 -1
  293. package/dist/motion/uikit/springs.js +5 -21
  294. package/dist/privateAttributes/pressGroupItem.stylex.d.ts +15 -0
  295. package/dist/privateAttributes/pressGroupItem.stylex.d.ts.map +1 -0
  296. package/dist/privateAttributes/pressGroupItem.stylex.js +23 -0
  297. package/dist/privateAttributes/segmentTitle.stylex.d.ts +5 -0
  298. package/dist/privateAttributes/segmentTitle.stylex.d.ts.map +1 -0
  299. package/dist/privateAttributes/segmentTitle.stylex.js +22 -0
  300. package/dist/utils/browserClickWait.d.ts +35 -0
  301. package/dist/utils/browserClickWait.d.ts.map +1 -0
  302. package/dist/utils/browserClickWait.js +123 -0
  303. package/dist/utils/clickGuard.d.ts +14 -1
  304. package/dist/utils/clickGuard.d.ts.map +1 -1
  305. package/dist/utils/clickGuard.js +46 -1
  306. package/dist/utils/dateParser.d.ts.map +1 -1
  307. package/dist/utils/dateParser.js +12 -0
  308. package/dist/utils/focusReturn.d.ts +15 -0
  309. package/dist/utils/focusReturn.d.ts.map +1 -1
  310. package/dist/utils/focusReturn.js +28 -0
  311. package/dist/utils/interactionOverlay.stylex.d.ts +3 -2
  312. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  313. package/dist/utils/interactionOverlay.stylex.js +3 -2
  314. package/dist/utils/pressFeedback.d.ts.map +1 -1
  315. package/dist/utils/pressFeedback.js +19 -6
  316. package/dist/utils/triggerFocus.d.ts +36 -0
  317. package/dist/utils/triggerFocus.d.ts.map +1 -0
  318. package/dist/utils/triggerFocus.js +65 -0
  319. package/locales/en.json +128 -0
  320. package/locales/pseudo.json +96 -0
  321. package/package.json +18 -8
  322. package/src/AlertDialog/AlertDialog.doc.mjs +2 -2
  323. package/src/AlertDialog/AlertDialog.tsx +37 -5
  324. package/src/AppShell/useCloseMobileDrawerOnActivate.ts +52 -0
  325. package/src/BottomSheet/BottomSheet.tsx +95 -10
  326. package/src/BottomSheet/BottomSheetPanel.tsx +40 -5
  327. package/src/BottomSheet/bottomSheetStack.ts +8 -4
  328. package/src/BottomSheet/useSheetGestures.ts +3 -1
  329. package/src/Breadcrumbs/BreadcrumbItem.tsx +13 -3
  330. package/src/Button/Button.tsx +63 -5
  331. package/src/CheckboxInput/CheckboxInput.doc.mjs +18 -0
  332. package/src/CheckboxInput/CheckboxInput.tsx +36 -9
  333. package/src/CheckboxList/CheckboxList.doc.mjs +4 -2
  334. package/src/CheckboxList/CheckboxList.tsx +6 -0
  335. package/src/CheckboxList/CheckboxListContext.tsx +6 -0
  336. package/src/CheckboxList/CheckboxListItem.tsx +11 -2
  337. package/src/ComplexSelector/ComplexSelector.tsx +20 -5
  338. package/src/ContextMenu/ContextMenu.tsx +2 -2
  339. package/src/DateInput/DateInput.tsx +8 -2
  340. package/src/DateInput/MonthScroller.tsx +18 -22
  341. package/src/DateInput/Wheel.tsx +30 -32
  342. package/src/DateTimeInput/DateTimeInput.tsx +8 -2
  343. package/src/Dialog/Dialog.tsx +54 -45
  344. package/src/Drawer/Drawer.tsx +28 -11
  345. package/src/Drawer/useDrawerDialogPresence.ts +5 -1
  346. package/src/Field/Field.tsx +2 -2
  347. package/src/Field/PanelSearchInput.tsx +11 -0
  348. package/src/HoverCard/HoverCard.doc.mjs +4 -2
  349. package/src/HoverCard/HoverCard.tsx +35 -4
  350. package/src/HoverCard/useHoverCard.doc.mjs +178 -36
  351. package/src/HoverCard/useHoverCard.tsx +46 -52
  352. package/src/Icon/Icon.doc.mjs +49 -8
  353. package/src/Icon/Icon.tsx +15 -7
  354. package/src/InfoTip/InfoTip.doc.mjs +192 -0
  355. package/src/InfoTip/InfoTip.tsx +186 -0
  356. package/src/InfoTip/index.ts +14 -0
  357. package/src/Layer/layerAnimations.stylex.ts +24 -16
  358. package/src/Layer/useBackdropPress.ts +81 -0
  359. package/src/Layer/usePressPin.ts +86 -26
  360. package/src/Lightbox/Lightbox.tsx +18 -0
  361. package/src/MobileNav/MobileNav.tsx +1 -1
  362. package/src/MultiSelector/hooks.ts +2 -4
  363. package/src/Outline/Outline.doc.mjs +177 -51
  364. package/src/Outline/Outline.tsx +5 -57
  365. package/src/Outline/useOutlineFromMarkdown.doc.mjs +157 -0
  366. package/src/Outline/useOutlineFromMarkdown.ts +1 -0
  367. package/src/Outline/useScrollSpy.ts +3 -3
  368. package/src/PowerSearch/PowerSearch.doc.mjs +55 -6
  369. package/src/PowerSearch/PowerSearch.tsx +798 -324
  370. package/src/PowerSearch/PowerSearchBottomSheet.tsx +2515 -0
  371. package/src/PowerSearch/PowerSearchEditPopover.tsx +28 -6
  372. package/src/PowerSearch/PowerSearchValueEditor.tsx +203 -38
  373. package/src/PowerSearch/formatFilterValue.ts +18 -1
  374. package/src/PowerSearch/formatTokenValue.ts +215 -0
  375. package/src/PowerSearch/powerSearchSheetUtils.ts +224 -0
  376. package/src/RadioList/RadioList.doc.mjs +4 -2
  377. package/src/RadioList/RadioList.tsx +7 -2
  378. package/src/RadioList/RadioListItem.tsx +5 -3
  379. package/src/Resizable/ResizeHandle.tsx +133 -328
  380. package/src/Resizable/resizeGesture.ts +368 -0
  381. package/src/SegmentedControl/SegmentedControl.doc.mjs +3 -3
  382. package/src/SegmentedControl/SegmentedControl.tsx +179 -5
  383. package/src/SegmentedControl/SegmentedControlContext.ts +6 -0
  384. package/src/SegmentedControl/SegmentedControlItem.tsx +12 -1
  385. package/src/SegmentedControl/useSegmentedSelectionMotion.ts +1640 -0
  386. package/src/SegmentedControl/useSegmentedTouchTracking.ts +308 -0
  387. package/src/Selector/Selector.tsx +136 -89
  388. package/src/Selector/optionShape.ts +64 -0
  389. package/src/Selector/selectableOptionEntries.ts +39 -0
  390. package/src/Selector/utils.ts +9 -68
  391. package/src/SideNav/SideNavItem.tsx +3 -7
  392. package/src/Slider/sliderTouchController.ts +2 -2
  393. package/src/Spinner/Spinner.tsx +77 -63
  394. package/src/Switch/switchTouch.ts +2 -10
  395. package/src/Switch/useSwitchTouch.ts +38 -104
  396. package/src/Table/BaseTable.tsx +46 -12
  397. package/src/TextInput/TextInput.tsx +11 -10
  398. package/src/Toast/Toast.doc.mjs +4 -3
  399. package/src/Toast/Toast.tsx +57 -32
  400. package/src/Toast/ToastViewport.tsx +121 -35
  401. package/src/Toast/types.ts +9 -3
  402. package/src/Token/Token.tsx +15 -4
  403. package/src/Token/TokenNamingContext.ts +28 -0
  404. package/src/Tokenizer/Tokenizer.doc.mjs +16 -0
  405. package/src/Tokenizer/Tokenizer.tsx +89 -85
  406. package/src/Tokenizer/TokenizerBottomSheet.tsx +219 -93
  407. package/src/Tokenizer/TokenizerItemRemovableContext.ts +24 -0
  408. package/src/Tooltip/Tooltip.doc.mjs +123 -54
  409. package/src/Tooltip/Tooltip.tsx +36 -39
  410. package/src/Tooltip/index.ts +0 -1
  411. package/src/Tooltip/useTooltip.doc.mjs +171 -33
  412. package/src/Tooltip/useTooltip.tsx +98 -86
  413. package/src/TopNav/TopNavItem.tsx +6 -5
  414. package/src/TopNav/TopNavMegaMenuItem.tsx +6 -5
  415. package/src/TopNav/TopNavMenu.tsx +6 -5
  416. package/src/Typeahead/TypeaheadItem.doc.mjs +6 -3
  417. package/src/Typeahead/TypeaheadItem.tsx +2 -1
  418. package/src/animator/createAnimator.doc.mjs +153 -0
  419. package/src/animator/createAnimator.ts +324 -0
  420. package/src/gestures/GestureHost.ts +198 -20
  421. package/src/gestures/PressTracker.ts +4 -3
  422. package/src/gestures/README.md +7 -1
  423. package/src/hooks/actionGroupTracker.ts +32 -61
  424. package/src/hooks/index.ts +2 -0
  425. package/src/hooks/pressGroupContext.ts +21 -0
  426. package/src/hooks/useActionGroupTracking.ts +224 -0
  427. package/src/hooks/useAnimator.doc.mjs +109 -0
  428. package/src/hooks/useAnimator.ts +67 -0
  429. package/src/hooks/useAttachment.ts +1 -1
  430. package/src/hooks/useHighlightedOptionScroll.ts +123 -15
  431. package/src/hooks/useInputStatusIcon.tsx +3 -2
  432. package/src/hooks/useLongPress.doc.mjs +2 -2
  433. package/src/hooks/useLongPress.ts +136 -15
  434. package/src/hooks/useMediaQuery.doc.mjs +2 -2
  435. package/src/hooks/useMediaQuery.ts +13 -4
  436. package/src/hooks/useMenuPress.ts +8 -2
  437. package/src/i18n/generated-english/InfoTip/InfoTip.generated.ts +13 -0
  438. package/src/i18n/generated-english/PowerSearch/PowerSearch.generated.ts +1 -0
  439. package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +43 -0
  440. package/src/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.ts +6 -0
  441. package/src/i18n/generated-english/PowerSearch/formatTokenValue.generated.ts +15 -0
  442. package/src/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.ts +7 -1
  443. package/src/i18n/generated-locales/en.generated.ts +43 -0
  444. package/src/i18n/generated-locales/pseudo.generated.ts +44 -0
  445. package/src/index.ts +14 -0
  446. package/src/internal/SheetConfirmation.tsx +101 -0
  447. package/src/internal/indicatorPress.stylex.ts +24 -0
  448. package/src/internal/sheetDraft.ts +80 -0
  449. package/src/internal/touch-sheet.ts +21 -0
  450. package/src/internal/useDismissSearchOnTouchDrag.ts +69 -0
  451. package/src/motion/MEASUREMENTS.md +7 -0
  452. package/src/motion/README.md +9 -5
  453. package/src/motion/animator.ts +61 -29
  454. package/src/motion/reducedMotion.ts +79 -1
  455. package/src/motion/roles.ts +20 -1
  456. package/src/motion/uikit/alert.ts +43 -0
  457. package/src/motion/uikit/index.ts +5 -5
  458. package/src/motion/uikit/segmented.ts +74 -0
  459. package/src/motion/uikit/springs.ts +5 -21
  460. package/src/privateAttributes/pressGroupItem.stylex.ts +23 -0
  461. package/src/privateAttributes/segmentTitle.stylex.ts +22 -0
  462. package/src/utils/browserClickWait.ts +133 -0
  463. package/src/utils/clickGuard.ts +64 -1
  464. package/src/utils/dateParser.ts +16 -0
  465. package/src/utils/focusReturn.ts +30 -0
  466. package/src/utils/interactionOverlay.stylex.ts +3 -2
  467. package/src/utils/pressFeedback.ts +19 -6
  468. package/src/utils/triggerFocus.ts +80 -0
  469. package/dist/internal/tokenizer-presentation.d.ts +0 -21
  470. package/dist/internal/tokenizer-presentation.d.ts.map +0 -1
  471. package/dist/internal/tokenizer-presentation.js +0 -21
  472. package/src/internal/tokenizer-presentation.ts +0 -25
@@ -4,19 +4,25 @@
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
19
 
17
20
  import React, {
21
+ lazy,
22
+ Suspense,
18
23
  useCallback,
19
24
  useEffect,
25
+ useId,
20
26
  useImperativeHandle,
21
27
  useMemo,
22
28
  useRef,
@@ -31,36 +37,54 @@ import {
31
37
  type TokenizerOverflowBehavior,
32
38
  } from '../Tokenizer';
33
39
  import {TokenizerClearAllMessageContext} from '../Tokenizer/TokenizerClearAllMessageContext';
40
+ import {TokenizerItemRemovableContext} from '../Tokenizer/TokenizerItemRemovableContext';
34
41
  import {TypeaheadItem} from '../Typeahead/TypeaheadItem';
35
42
  import {Token} from '../Token';
43
+ import {TokenNamingContext} from '../Token/TokenNamingContext';
36
44
  import {Avatar} from '../Avatar';
37
45
  import {layerAnimations} from '../Layer/layerAnimations.stylex';
38
- import {Icon} from '../Icon';
46
+ import {Icon, renderIconSlot} from '../Icon';
39
47
  import type {IconType} from '../Icon';
40
48
  import type {IconName} from '../Icon/globalIconRegistry';
41
49
  import type {InputStatus} from '../Field';
50
+ import {Field} from '../Field/Field';
51
+ import {
52
+ inputStatusBorderStyles,
53
+ inputStatusFocusWithinStyles,
54
+ inputStatusHoverShadowStyles,
55
+ inputWrapperStyles,
56
+ } from '../Field/inputStyles.stylex';
42
57
  import {usePopover} from '../Popover/usePopover';
58
+ import {useTooltip} from '../Tooltip/useTooltip';
43
59
  import {layerViewportInset} from '../Layer/layerViewportInset.stylex';
44
60
  import {useAnnounce} from '../hooks/useAnnounce';
61
+ import {useAdaptivePresentation} from '../hooks/useAdaptivePresentation';
62
+ import {useDevWarning} from '../hooks/useDevWarning';
63
+ import {useIsHydrating} from '../hooks/useIsHydrating';
45
64
  import {
46
65
  spacingVars,
47
66
  colorVars,
67
+ sizeVars,
48
68
  typeScaleVars,
69
+ typographyVars,
49
70
  fontWeightVars,
50
71
  } from '../theme/tokens.stylex';
51
72
  import {useSize} from '../SizeContext/SizeContext';
73
+ import {isFocusDetached} from '../utils/focusReturn';
74
+ import {isRenderable} from '../utils/isRenderable';
52
75
  import {useInternalConfig} from './useInternalConfig';
53
76
  import {usePowerSearchSource} from './usePowerSearchSource';
77
+ import {formatFilterValue} from './formatFilterValue';
78
+ import {NestedEditor, PowerSearchEditPopover} from './PowerSearchEditPopover';
54
79
  import {
55
- formatFilterValue,
56
- formatDateAbsoluteCompact,
57
- } from './formatFilterValue';
58
- import {PowerSearchEditPopover} from './PowerSearchEditPopover';
80
+ buildRelativeDateOptions,
81
+ PowerSearchValueEditor,
82
+ } from './PowerSearchValueEditor';
59
83
  import {resolveOperatorLabel} from './resolveOperatorLabel';
60
84
  import {themeProps} from '../utils/themeProps';
61
- import {truncateCharacters} from '../utils/characters';
62
85
  import {useLocale} from '../i18n/useLocale';
63
86
  import type {Locale} from '../i18n/types';
87
+ import type {SearchableItem} from '../Typeahead/types';
64
88
  import type {
65
89
  PowerSearchConfig,
66
90
  PowerSearchFilter,
@@ -71,9 +95,14 @@ import type {
71
95
  PowerSearchHandle,
72
96
  FilterValue,
73
97
  OperatorValue,
74
- EnumItem,
75
98
  PowerSearchComponents,
99
+ PowerSearchOperator,
76
100
  } from './types';
101
+ import type {
102
+ PowerSearchBottomSheetProps,
103
+ PowerSearchSheetEditors,
104
+ } from './PowerSearchBottomSheet';
105
+ import {formatTokenValueText} from './formatTokenValue';
77
106
 
78
107
  import {useMergedRefs} from '../hooks/useMergedRefs';
79
108
  import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement';
@@ -146,217 +175,131 @@ const resultCountStyles = stylex.create({
146
175
  },
147
176
  });
148
177
 
149
- function truncateString(value: string, limit: number): string {
150
- // Same semantics as before — strings within limit + 3 pass through, longer
151
- // ones cut to limit + '...' — but counted in characters, not code units.
152
- return truncateCharacters(value, limit + 3, '...');
153
- }
154
-
155
- function getEnumLabel(values: ReadonlyArray<EnumItem>, value: string): string {
156
- return values.find(v => v.value === value)?.label ?? value;
157
- }
158
-
159
- function PowerSearchTokenValue({
160
- operatorValue,
161
- filterValue,
162
- maxLength,
163
- locale,
164
- }: {
165
- operatorValue: OperatorValue;
166
- filterValue: FilterValue;
167
- maxLength: number;
168
- locale: Locale;
169
- }) {
170
- switch (filterValue.type) {
171
- case 'empty':
172
- return null;
173
-
174
- case 'string':
175
- return (
176
- <span {...stylex.props(tokenValueStyles.value)}>
177
- {truncateString(filterValue.value, maxLength)}
178
- </span>
179
- );
180
-
181
- case 'integer':
182
- case 'float':
183
- return (
184
- <span {...stylex.props(tokenValueStyles.value)}>
185
- {filterValue.value}
186
- </span>
187
- );
188
-
189
- case 'enum':
190
- if (operatorValue.type === 'enum') {
191
- return (
192
- <span {...stylex.props(tokenValueStyles.value)}>
193
- {truncateString(
194
- getEnumLabel(operatorValue.values, filterValue.value),
195
- maxLength,
196
- )}
197
- </span>
198
- );
199
- }
200
- return (
201
- <span {...stylex.props(tokenValueStyles.value)}>
202
- {truncateString(filterValue.value, maxLength)}
203
- </span>
204
- );
205
-
206
- case 'string_list': {
207
- const items = filterValue.value;
208
- if (items.length === 0) {
209
- return null;
210
- }
211
- if (items.length === 1) {
212
- return (
213
- <span {...stylex.props(tokenValueStyles.value)}>
214
- {truncateString(items[0], maxLength)}
215
- </span>
216
- );
217
- }
218
- const totalLength = items.reduce((sum, s) => sum + s.length, 0);
219
- if (totalLength > maxLength) {
220
- return (
221
- <span {...stylex.props(tokenValueStyles.value)}>
222
- {items.length} items
223
- </span>
224
- );
225
- }
226
- return (
227
- <span {...stylex.props(tokenValueStyles.value)}>
228
- {items.join(', ')}
229
- </span>
230
- );
231
- }
232
-
233
- case 'enum_list': {
234
- const items = filterValue.value;
235
- if (items.length === 0) {
236
- return null;
237
- }
238
- if (operatorValue.type === 'enum_list') {
239
- const labels = items.map(v => getEnumLabel(operatorValue.values, v));
240
- if (labels.length === 1) {
241
- return (
242
- <span {...stylex.props(tokenValueStyles.value)}>
243
- {truncateString(labels[0], maxLength)}
244
- </span>
245
- );
246
- }
247
- const totalLength = labels.reduce((sum, s) => sum + s.length, 0);
248
- if (totalLength > maxLength) {
249
- return (
250
- <span {...stylex.props(tokenValueStyles.value)}>
251
- {labels.length} items
252
- </span>
253
- );
254
- }
255
- return (
256
- <span {...stylex.props(tokenValueStyles.value)}>
257
- {labels.join(', ')}
258
- </span>
259
- );
260
- }
261
- if (items.length === 1) {
262
- return (
263
- <span {...stylex.props(tokenValueStyles.value)}>
264
- {truncateString(items[0], maxLength)}
265
- </span>
266
- );
267
- }
268
- return (
269
- <span {...stylex.props(tokenValueStyles.value)}>
270
- {items.length} items
271
- </span>
272
- );
273
- }
274
-
275
- case 'entity_list': {
276
- const entities = filterValue.value;
277
- if (entities.length === 0) {
278
- return null;
279
- }
280
- if (entities.length === 1) {
281
- return (
282
- <span {...stylex.props(tokenValueStyles.value)}>
283
- {truncateString(entities[0].label, maxLength)}
284
- </span>
285
- );
286
- }
287
- const totalLength = entities.reduce((sum, e) => sum + e.label.length, 0);
288
- if (totalLength > maxLength) {
289
- return (
290
- <span {...stylex.props(tokenValueStyles.value)}>
291
- {entities.length} items
292
- </span>
293
- );
294
- }
295
- return (
296
- <span {...stylex.props(tokenValueStyles.value)}>
297
- {entities.map(e => e.label).join(', ')}
298
- </span>
299
- );
300
- }
301
-
302
- case 'time':
303
- return (
304
- <span {...stylex.props(tokenValueStyles.value)}>
305
- {filterValue.value}
306
- </span>
307
- );
308
-
309
- case 'date_absolute': {
310
- const formatted = formatDateAbsoluteCompact(
311
- filterValue.unixSeconds,
312
- locale,
313
- );
314
- return (
315
- <span {...stylex.props(tokenValueStyles.value)}>
316
- {truncateString(formatted, maxLength)}
317
- </span>
318
- );
319
- }
320
-
321
- case 'date_relative':
322
- return (
323
- <span {...stylex.props(tokenValueStyles.value)}>
324
- {truncateString(filterValue.value, maxLength)}
325
- </span>
326
- );
327
-
328
- case 'date_range':
329
- return <span {...stylex.props(tokenValueStyles.value)}>date range</span>;
178
+ // The sheet presentation's trigger field: the same control box as the
179
+ // tokenizer field, with display-only tokens and one button as its only tab
180
+ // stop.
181
+ const sheetTriggerSizeStyles = stylex.create({
182
+ sm: {minHeight: sizeVars['--size-element-sm']},
183
+ md: {minHeight: sizeVars['--size-element-md']},
184
+ lg: {minHeight: sizeVars['--size-element-lg']},
185
+ });
330
186
 
331
- case 'custom':
332
- if (operatorValue.type === 'custom') {
333
- return (
334
- <span {...stylex.props(tokenValueStyles.value)}>
335
- {truncateString(
336
- operatorValue.getString(filterValue.value),
337
- maxLength,
338
- )}
339
- </span>
340
- );
341
- }
342
- return (
343
- <span {...stylex.props(tokenValueStyles.value)}>
344
- {filterValue.value}
345
- </span>
346
- );
187
+ const sheetTriggerStyles = stylex.create({
188
+ wrapper: {
189
+ position: 'relative',
190
+ flexWrap: 'nowrap',
191
+ gap: spacingVars['--spacing-1'],
192
+ height: 'auto',
193
+ cursor: {
194
+ default: 'pointer',
195
+ ':is([aria-disabled="true"])': 'default',
196
+ },
197
+ },
198
+ wrapperWithTokens: {
199
+ paddingBlock: `calc(${spacingVars['--spacing-1']} - 1px)`,
200
+ paddingInline: `calc(${spacingVars['--spacing-1']} - 1px)`,
201
+ },
202
+ contentSection: {
203
+ position: 'relative',
204
+ minWidth: 0,
205
+ flexGrow: 1,
206
+ display: 'flex',
207
+ alignItems: 'center',
208
+ flexWrap: 'wrap',
209
+ columnGap: spacingVars['--spacing-1'],
210
+ rowGap: `calc(${spacingVars['--spacing-1']} - 1px)`,
211
+ },
212
+ startIconWithTokens: {
213
+ marginInlineStart: `calc(${spacingVars['--spacing-2']} - ${spacingVars['--spacing-1']} + 1px)`,
214
+ },
215
+ // The only tab stop in the field. It stays visually borderless while the
216
+ // wrapper delegates taps on tokens and empty space to it.
217
+ trigger: {
218
+ appearance: 'none',
219
+ flex: '1 1 40px',
220
+ minWidth: '40px',
221
+ alignSelf: 'stretch',
222
+ display: 'flex',
223
+ alignItems: 'center',
224
+ paddingBlock: 0,
225
+ paddingInline: spacingVars['--spacing-1'],
226
+ margin: 0,
227
+ borderWidth: 0,
228
+ borderStyle: 'none',
229
+ backgroundColor: 'transparent',
230
+ color: colorVars['--color-text-secondary'],
231
+ fontFamily: typographyVars['--font-family-body'],
232
+ fontSize: typeScaleVars['--text-body-size'],
233
+ lineHeight: typeScaleVars['--text-body-leading'],
234
+ textAlign: 'start',
235
+ whiteSpace: 'nowrap',
236
+ cursor: {
237
+ default: 'pointer',
238
+ ':is([aria-disabled="true"])': 'default',
239
+ },
240
+ outline: 'none',
241
+ },
242
+ // Once a token is present the empty trigger covers the token lane instead
243
+ // of reserving 40px or wrapping onto an otherwise blank row.
244
+ triggerWithTokens: {
245
+ position: 'absolute',
246
+ inset: 0,
247
+ minWidth: 0,
248
+ padding: 0,
249
+ },
250
+ endSection: {
251
+ display: 'flex',
252
+ alignItems: 'center',
253
+ gap: spacingVars['--spacing-2'],
254
+ flexShrink: 0,
255
+ paddingInlineEnd: spacingVars['--spacing-1'],
256
+ },
257
+ });
347
258
 
348
- case 'nested': {
349
- const count = filterValue.value.length;
350
- return (
351
- <span {...stylex.props(tokenValueStyles.value)}>
352
- {count === 1 ? '1 filter' : `${count} filters`}
353
- </span>
354
- );
355
- }
259
+ // The sheet flow (three standalone sheets, the sheet editors) only
260
+ // renders for the bottom-sheet presentation. Loading it on demand keeps it out
261
+ // of pages whose PowerSearch stays a tokenizer field.
262
+ // One import promise, shared by the preload and the lazy element: the sheet
263
+ // presentation loads the flow's chunk as soon as it is active, so the first
264
+ // press does not wait for the network, while nothing of the flow is rendered
265
+ // until a press.
266
+ const loadPowerSearchBottomSheet = async () =>
267
+ import('./PowerSearchBottomSheet');
268
+ const LazyPowerSearchBottomSheet = lazy(async () =>
269
+ loadPowerSearchBottomSheet().then(module => ({
270
+ default: module.PowerSearchBottomSheet,
271
+ })),
272
+ );
273
+ // The value step reuses the popover presentation's editors, which this
274
+ // module already loads; see PowerSearchSheetEditors.
275
+ const SHEET_EDITORS: PowerSearchSheetEditors = {
276
+ NestedEditor,
277
+ PowerSearchValueEditor,
278
+ buildRelativeDateOptions,
279
+ };
356
280
 
357
- default:
358
- return null;
281
+ /**
282
+ * Hosts the lazily loaded sheet flow. Renders nothing on the server or in the
283
+ * render that hydrates server HTML: a lazy component that has not loaded yet
284
+ * would leave server markup incomplete and abort hydration. The closed flow
285
+ * has no visible markup, so it first renders right after hydration.
286
+ */
287
+ function PowerSearchSheetHost(
288
+ props: Omit<PowerSearchBottomSheetProps, 'editors'>,
289
+ ) {
290
+ const isHydrating = useIsHydrating();
291
+ if (isHydrating) {
292
+ return null;
359
293
  }
294
+ return (
295
+ <Suspense fallback={null}>
296
+ <LazyPowerSearchBottomSheet {...props} editors={SHEET_EDITORS} />
297
+ </Suspense>
298
+ );
299
+ }
300
+
301
+ function PowerSearchTokenValue({text}: {text: string}) {
302
+ return <span {...stylex.props(tokenValueStyles.value)}>{text}</span>;
360
303
  }
361
304
 
362
305
  // =============================================================================
@@ -467,6 +410,37 @@ export interface PowerSearchProps extends Omit<
467
410
  * Keys are operator value types (e.g. 'string', 'enum', 'date_absolute').
468
411
  */
469
412
  components?: PowerSearchComponents;
413
+
414
+ /**
415
+ * Experimental options, outside the stable compatibility promise: anything
416
+ * in this object may change or be removed in a patch release. Omit it for
417
+ * the stable behavior.
418
+ * @experimental
419
+ */
420
+ experimental?: {
421
+ /**
422
+ * How the field presents filter editing.
423
+ * - `'popover'`: the tokenizer field with its typeahead and the editor
424
+ * popover — the stable behavior.
425
+ * - `'bottom-sheet'`: the whole field is one trigger showing display-only
426
+ * filter tokens; it opens a stack of sheets. Filters lists the filters
427
+ * as rows that edit or remove one, with Add filter, Done, and Clear
428
+ * all, which asks first and keeps read-only filters. Add filter opens
429
+ * above it with the fields (or the content search with inline
430
+ * suggestions), and a value step opens above that, stages operator →
431
+ * value, and confirms from its footer. Dismissing a step reveals the
432
+ * one beneath. Suited to compact touch screens, where the editor
433
+ * popover competes with the software keyboard.
434
+ * - `'adaptive'`: the bottom sheet on compact coarse-pointer screens,
435
+ * otherwise the popover — the same resolution Selector uses.
436
+ *
437
+ * In the sheet presentation `menuWidth` and `tokenOverflowBehavior` have
438
+ * no surface to act on and warn in development.
439
+ * @experimental
440
+ * @default 'popover'
441
+ */
442
+ presentation?: 'popover' | 'bottom-sheet' | 'adaptive';
443
+ };
470
444
  }
471
445
 
472
446
  // =============================================================================
@@ -489,6 +463,20 @@ type PopoverState =
489
463
  // Component
490
464
  // =============================================================================
491
465
 
466
+ /**
467
+ * Whether a press on a token with this operator commits the filter instead of
468
+ * opening its editor: an `empty` operator with no `empty` Editor override.
469
+ */
470
+ function commitsWithoutEditor(
471
+ operator: PowerSearchOperator | undefined,
472
+ componentOverrides: PowerSearchComponents | undefined,
473
+ ): boolean {
474
+ return (
475
+ operator?.value.type === 'empty' &&
476
+ componentOverrides?.empty?.Editor == null
477
+ );
478
+ }
479
+
492
480
  /**
493
481
  * Structured filter bar where each token represents a filter (field + operator + value).
494
482
  *
@@ -589,6 +577,7 @@ export function PowerSearch({
589
577
  className,
590
578
  style,
591
579
  components: componentOverrides,
580
+ experimental,
592
581
  }: PowerSearchProps) {
593
582
  const size = useSize(sizeProp, 'md');
594
583
  const config = useInternalConfig(configProp);
@@ -602,12 +591,164 @@ export function PowerSearch({
602
591
  popoverSaveButtonLabelFromProps ?? t('@astryx.powersearch.editor.apply');
603
592
  const tokenizerRef = useRef<TokenizerHandle>(null);
604
593
 
594
+ // Experimental presentation. `adaptive`
595
+ // resolves through the policy the selection controls share; an open flow
596
+ // keeps its presentation until it closes, so a resolution change mid-task
597
+ // cannot pull the surface out from under the person.
598
+ const presentation = experimental?.presentation ?? 'popover';
599
+ const resolvedPresentation = useAdaptivePresentation(presentation);
600
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
601
+ // Latched for the flow's lifetime: the flow stays presented after it closes
602
+ // until the sheets have run their exit and handed focus back to the trigger.
603
+ // Without the latch, `adaptive` resolving to `popover` mid-session (a phone
604
+ // turned to landscape) would unmount the sheets and the trigger they return
605
+ // focus to in the same render, dropping focus to the page body.
606
+ const [isSheetPresented, setIsSheetPresented] = useState(false);
607
+ const isSheetPresentation =
608
+ isSheetOpen || isSheetPresented || resolvedPresentation === 'bottom-sheet';
609
+ const triggerRef = useRef<HTMLButtonElement>(null);
610
+ // `hasAutoFocus` applies only to the control the field first mounts. A
611
+ // presentation swap remounts the control (combobox input or trigger), and
612
+ // React's `autoFocus` on the new one would pull focus from wherever on the
613
+ // page it was; the roots' callback refs hand over the focus the field held
614
+ // instead. A field that never swaps keeps `autoFocus` on every render, so a
615
+ // Dialog that keeps it mounted still finds its `data-autofocus` on open.
616
+ const [autoFocusState, setAutoFocusState] = useState({
617
+ presentation: isSheetPresentation,
618
+ isFirstControl: true,
619
+ });
620
+ if (autoFocusState.presentation !== isSheetPresentation) {
621
+ setAutoFocusState({
622
+ presentation: isSheetPresentation,
623
+ isFirstControl: false,
624
+ });
625
+ }
626
+ const autoFocusOnMount =
627
+ hasAutoFocus &&
628
+ autoFocusState.isFirstControl &&
629
+ autoFocusState.presentation === isSheetPresentation;
630
+ // A focus the swap moves continues the same visit to the field: the
631
+ // control that held focus is gone (no blur fires for a removed element), so
632
+ // the caller is told of neither a blur nor a new focus.
633
+ const isSwapFocusRef = useRef(false);
634
+ const focusAfterSwap = useCallback((control: {focus(): void} | null) => {
635
+ if (control == null) {
636
+ return;
637
+ }
638
+ isSwapFocusRef.current = true;
639
+ try {
640
+ control.focus();
641
+ } finally {
642
+ isSwapFocusRef.current = false;
643
+ }
644
+ }, []);
645
+ const handleFieldFocus = useCallback(
646
+ (event: React.FocusEvent) => {
647
+ if (!isSwapFocusRef.current) {
648
+ onFocus?.(event);
649
+ }
650
+ },
651
+ [onFocus],
652
+ );
653
+ const handleFieldBlur = useCallback(
654
+ (event: React.FocusEvent) => {
655
+ onBlur?.(event);
656
+ },
657
+ [onBlur],
658
+ );
659
+ // While the flow is open, the sheet announces its own content from inside
660
+ // its dialog; this component announces only with the flow closed, and
661
+ // `useAnnounce` routes those messages by where the field is — into a Dialog
662
+ // or modal Drawer that holds it, or the shared channel when no modal does.
663
+ // A result count that changed during the flow's exit is spoken once the
664
+ // flow has fully closed and focus is back on the trigger: the closing
665
+ // dialog stays modal through its exit, so neither its region nor the
666
+ // page-body channel is heard before.
667
+ const afterCloseResultCountRef = useRef<string | null>(null);
668
+ const triggerId = useId();
669
+ const triggerLabelId = useId();
670
+ const triggerContentId = useId();
671
+ const statusMessageId = useId();
672
+ // A field that becomes disabled cannot keep its flow open, and must not
673
+ // reopen it when re-enabled. Adjusted in the
674
+ // render that sees the change, not an effect, so no frame shows both.
675
+ const [wasDisabled, setWasDisabled] = useState(isDisabled);
676
+ if (isDisabled !== wasDisabled) {
677
+ setWasDisabled(isDisabled);
678
+ if (isDisabled && isSheetOpen) {
679
+ setIsSheetOpen(false);
680
+ }
681
+ }
682
+ useDevWarning(
683
+ 'PowerSearch',
684
+ '`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.',
685
+ isSheetPresentation && menuWidth != null,
686
+ );
687
+ useDevWarning(
688
+ 'PowerSearch',
689
+ '`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.',
690
+ isSheetPresentation && tokenOverflowBehavior != null,
691
+ );
692
+
605
693
  const [popoverState, setPopoverStateRaw] = useState<PopoverState>({
606
694
  type: 'idle',
607
695
  });
608
696
 
697
+ // A default filter token's visible text: its label (field and operator)
698
+ // and its value. The token's remove button and editor dialog are named by
699
+ // the same text.
700
+ const tokenText = useCallback(
701
+ (filter: PowerSearchFilter) => {
702
+ const field = config.getField(filter.field);
703
+ const operator = config.getOperator(filter.field, filter.operator);
704
+ const fieldLabel = field?.label ?? '';
705
+ const operatorLabel = operator ? resolveOperatorLabel(operator, t) : '';
706
+ const label = `${fieldLabel}: ${operatorLabel}`.trim();
707
+ const value = operator
708
+ ? formatTokenValueText(
709
+ operator.value,
710
+ filter.value,
711
+ Math.max(
712
+ maxTokenLength - fieldLabel.length - operatorLabel.length,
713
+ 10,
714
+ ),
715
+ locale,
716
+ )
717
+ : null;
718
+ return {label, value, full: value != null ? `${label} ${value}` : label};
719
+ },
720
+ [config, maxTokenLength, t, locale],
721
+ );
722
+ // The built-in token being edited. A token from a `components` override
723
+ // names itself, so its editor keeps the plain popover.
724
+ const editedFilter =
725
+ popoverState.type === 'editing'
726
+ ? filters[popoverState.filterIndex]
727
+ : undefined;
728
+ const editedOperator =
729
+ editedFilter != null
730
+ ? config.getOperator(editedFilter.field, editedFilter.operator)
731
+ : undefined;
732
+ const editedBuiltInFilter =
733
+ editedOperator == null ||
734
+ componentOverrides?.[editedOperator.value.type]?.Token == null
735
+ ? editedFilter
736
+ : undefined;
737
+ // Whether the editor popover is open, for the hand-off below, which runs
738
+ // in the commit that swaps presentations.
739
+ const isPopoverOpenRef = useRef(false);
740
+ // When `adaptive` flips to the sheet while the editor popover is open (a
741
+ // phone rotated), the editing ends with the popover: its state returns to
742
+ // idle (through the raw setter, since the tokenizer input is gone), in the
743
+ // render that sees the swap, so a later switch back does not reopen a stale
744
+ // editor.
745
+ if (isSheetPresentation && popoverState.type !== 'idle') {
746
+ setPopoverStateRaw({type: 'idle'});
747
+ }
748
+
609
749
  // Layer for positioning the edit popover anchored to the tokenizer
610
750
  const handleLayerHide = useCallback(() => {
751
+ isPopoverOpenRef.current = false;
611
752
  setPopoverStateRaw({type: 'idle'});
612
753
  }, []);
613
754
 
@@ -616,14 +757,66 @@ export function PowerSearch({
616
757
  hasLightDismiss: true,
617
758
  hasCloseButton: false,
618
759
  hasAutoFocus: false,
619
- // The popup's own listbox/menu content is the exposed semantics; focus
620
- // stays on the tokenizer input, so a modal dialog wrapper is incorrect.
621
- role: 'none',
760
+ // Adding: the popup's own listbox/menu content is the exposed semantics,
761
+ // and focus stays on the tokenizer input, so a dialog wrapper is
762
+ // incorrect. Editing a token: focus moves into the editor's controls, so
763
+ // the editor is a non-modal dialog named by the token's visible text.
764
+ role: editedBuiltInFilter != null ? 'dialog' : 'none',
765
+ isModal: false,
766
+ dialogLabel:
767
+ editedBuiltInFilter != null
768
+ ? tokenText(editedBuiltInFilter).full
769
+ : undefined,
622
770
  });
623
771
 
772
+ // A presentation swap removes the focused control, so the other
773
+ // presentation's primary control (the combobox input or the trigger) takes
774
+ // focus and focus never falls to the page body. Each presentation keys its
775
+ // root, so a swap mounts a new root. The detaching root notes that it held
776
+ // focus, which also covers an input focused by server-rendered `autofocus`
777
+ // before hydration. The editor popover is portaled outside the root, so an
778
+ // open editor counts as held focus. The attaching root gives focus to its
779
+ // primary control when focus has fallen to the page body; the sheet root
780
+ // also hides a popover layer left open.
781
+ const isFocusPendingHandOffRef = useRef(false);
782
+ const hidePopover = popover.hide;
783
+ const handOffFocusRef = useCallback(
784
+ (node: HTMLDivElement | null) => {
785
+ if (node == null) {
786
+ return;
787
+ }
788
+ const trigger = triggerRef.current;
789
+ const isSheetRoot = trigger != null && node.contains(trigger);
790
+ // The sheet presentation preloads the flow's chunk, so the first press
791
+ // opens without waiting for the network; nothing renders.
792
+ if (isSheetRoot) {
793
+ void loadPowerSearchBottomSheet();
794
+ }
795
+ if (isSheetRoot && isPopoverOpenRef.current) {
796
+ isPopoverOpenRef.current = false;
797
+ hidePopover();
798
+ }
799
+ const takesFocus = isFocusPendingHandOffRef.current;
800
+ isFocusPendingHandOffRef.current = false;
801
+ if (takesFocus && isFocusDetached(node.ownerDocument)) {
802
+ focusAfterSwap(isSheetRoot ? trigger : tokenizerRef.current);
803
+ }
804
+ return () => {
805
+ if (
806
+ node.contains(node.ownerDocument.activeElement) ||
807
+ isPopoverOpenRef.current
808
+ ) {
809
+ isFocusPendingHandOffRef.current = true;
810
+ }
811
+ };
812
+ },
813
+ [focusAfterSwap, hidePopover],
814
+ );
815
+
624
816
  // Wrapper that manages layer visibility and tokenizer focus alongside state
625
817
  const setPopoverState = useCallback(
626
818
  (state: PopoverState) => {
819
+ isPopoverOpenRef.current = state.type !== 'idle';
627
820
  setPopoverStateRaw(state);
628
821
  if (state.type !== 'idle') {
629
822
  // Schedule after the current frame so React can render the popover
@@ -640,13 +833,22 @@ export function PowerSearch({
640
833
  [popover],
641
834
  );
642
835
 
643
- // Expose imperative handle
836
+ // Expose imperative handle; in the sheet presentation the trigger is the
837
+ // field's focusable control.
644
838
  useImperativeHandle(handleRef, () => ({
645
839
  focusTypeahead() {
646
- tokenizerRef.current?.focus();
840
+ if (isSheetPresentation) {
841
+ triggerRef.current?.focus();
842
+ } else {
843
+ tokenizerRef.current?.focus();
844
+ }
647
845
  },
648
846
  blurTypeahead() {
649
- tokenizerRef.current?.blur();
847
+ if (isSheetPresentation) {
848
+ triggerRef.current?.blur();
849
+ } else {
850
+ tokenizerRef.current?.blur();
851
+ }
650
852
  },
651
853
  }));
652
854
 
@@ -738,19 +940,39 @@ export function PowerSearch({
738
940
  value: undefined,
739
941
  },
740
942
  });
741
- } else if (change.type === 'remove' && change.item) {
742
- const auxData = change.item.auxiliaryData as PowerSearchAuxData;
743
- if (auxData?.filterIndex != null) {
744
- const newFilters = filters.filter((_, i) =>
745
- _items.some(item => item.auxiliaryData?.filterIndex === i),
943
+ } else if (change.type === 'remove') {
944
+ const keptIndices = new Set(
945
+ _items.map(item => item.auxiliaryData?.filterIndex),
946
+ );
947
+ const firstRemovedIndex = filters.findIndex(
948
+ (_, i) => !keptIndices.has(i),
949
+ );
950
+ if (firstRemovedIndex >= 0) {
951
+ onChange(
952
+ filters.filter((_, i) => keptIndices.has(i)),
953
+ 'remove',
954
+ firstRemovedIndex,
746
955
  );
747
- onChange(newFilters, 'remove', auxData.filterIndex);
748
956
  }
749
957
  }
750
958
  },
751
959
  [config, filters, onChange, setPopoverState],
752
960
  );
753
961
 
962
+ // Tokenizer's Backspace and Clear all do not go through renderToken. This
963
+ // predicate locks each read-only filter, and every token of a read-only
964
+ // field.
965
+ const isItemRemovable = useCallback(
966
+ (item: SearchableItem) => {
967
+ if (isReadOnly) {
968
+ return false;
969
+ }
970
+ const filterIndex = (item as PowerSearchItem).auxiliaryData?.filterIndex;
971
+ return filterIndex == null || !filters[filterIndex]?.isReadOnly;
972
+ },
973
+ [filters, isReadOnly],
974
+ );
975
+
754
976
  // Handle clicking a token to edit
755
977
  const handleTokenClick = useCallback(
756
978
  (index: number) => {
@@ -767,10 +989,7 @@ export function PowerSearch({
767
989
  // edit: commit it as that editor's save would, the way adding one does,
768
990
  // instead of opening it. A consumer's `empty` Editor override still opens.
769
991
  const operator = config.getOperator(filter.field, filter.operator);
770
- if (
771
- operator?.value.type === 'empty' &&
772
- componentOverrides?.empty?.Editor == null
773
- ) {
992
+ if (commitsWithoutEditor(operator, componentOverrides)) {
774
993
  const newFilters = [...filters];
775
994
  newFilters[index] = {
776
995
  field: filter.field,
@@ -833,10 +1052,12 @@ export function PowerSearch({
833
1052
  setPopoverState({type: 'idle'});
834
1053
  }, [setPopoverState]);
835
1054
 
836
- // Custom token renderer
837
- // Custom token renderer
838
- const renderToken = useCallback(
839
- (item: PowerSearchItem, onRemove: () => void) => {
1055
+ // Token renderer for both presentations. Given `onRemove`, the token is
1056
+ // the tokenizer field's: click edits, × removes. Without it (the sheet
1057
+ // presentation's trigger) the token is display-only; the field as a whole
1058
+ // opens the flow.
1059
+ const renderFilterToken = useCallback(
1060
+ (item: PowerSearchItem, onRemove: (() => void) | undefined) => {
840
1061
  const auxData = item.auxiliaryData;
841
1062
  const filterIndex = auxData?.filterIndex ?? -1;
842
1063
  const filter = filterIndex >= 0 ? filters[filterIndex] : undefined;
@@ -846,7 +1067,8 @@ export function PowerSearch({
846
1067
  ? config.getOperator(auxData.fieldKey, auxData.operatorKey)
847
1068
  : undefined;
848
1069
 
849
- const canInteract = !isReadOnly && !isDisabled && !filter?.isReadOnly;
1070
+ const canInteract =
1071
+ onRemove != null && !isReadOnly && !isDisabled && !filter?.isReadOnly;
850
1072
  const handleClick = canInteract
851
1073
  ? () => handleTokenClick(filterIndex)
852
1074
  : undefined;
@@ -875,20 +1097,16 @@ export function PowerSearch({
875
1097
  // Default token rendering
876
1098
  const fieldLabel = field?.label ?? '';
877
1099
  const operatorLabel = operator ? resolveOperatorLabel(operator, t) : '';
878
- const tokenLabel = `${fieldLabel}: ${operatorLabel}`.trim();
879
- const adjustedMaxLength = Math.max(
880
- maxTokenLength - fieldLabel.length - operatorLabel.length,
881
- 10,
882
- );
883
-
1100
+ const text =
1101
+ filter != null
1102
+ ? tokenText(filter)
1103
+ : {label: `${fieldLabel}: ${operatorLabel}`.trim(), value: null};
1104
+ const tokenLabel = text.label;
1105
+ const fullText =
1106
+ text.value != null ? `${text.label} ${text.value}` : text.label;
884
1107
  const valueContent =
885
- operator && filter ? (
886
- <PowerSearchTokenValue
887
- operatorValue={operator.value}
888
- filterValue={filter.value}
889
- maxLength={adjustedMaxLength}
890
- locale={locale}
891
- />
1108
+ text.value != null ? (
1109
+ <PowerSearchTokenValue text={text.value} />
892
1110
  ) : undefined;
893
1111
 
894
1112
  // Show entity photo as token icon for single-entity filters
@@ -902,32 +1120,49 @@ export function PowerSearch({
902
1120
  tokenIcon = <Avatar src={entity.photo} name={entity.label} size={16} />;
903
1121
  }
904
1122
 
1123
+ // A token that opens its editor says that it opens a dialog, and the
1124
+ // naming context names it by all its visible text.
1125
+ const editorProps =
1126
+ handleClick != null &&
1127
+ !commitsWithoutEditor(operator, componentOverrides)
1128
+ ? {
1129
+ 'aria-haspopup': 'dialog' as const,
1130
+ 'aria-expanded':
1131
+ popoverState.type === 'editing' &&
1132
+ popoverState.filterIndex === filterIndex,
1133
+ }
1134
+ : null;
1135
+
905
1136
  return (
906
- <Token
907
- label={tokenLabel}
908
- size={size}
909
- icon={tokenIcon}
910
- endContent={valueContent}
911
- onMouseDown={
912
- handleClick
913
- ? e => {
914
- // Keep a token press from entering Tokenizer's input-focus
915
- // redirect. Click still edits/removes; Tab entry is unchanged.
916
- e.preventDefault();
917
- }
918
- : undefined
919
- }
920
- onClick={
921
- handleClick
922
- ? (e: React.MouseEvent) => {
923
- e.stopPropagation();
924
- handleClick();
925
- }
926
- : undefined
927
- }
928
- onRemove={handleRemove}
929
- isDisabled={isDisabled}
930
- />
1137
+ <TokenNamingContext
1138
+ value={{actionName: fullText, removeName: fullText}}>
1139
+ <Token
1140
+ label={tokenLabel}
1141
+ size={size}
1142
+ icon={tokenIcon}
1143
+ {...editorProps}
1144
+ endContent={valueContent}
1145
+ onMouseDown={
1146
+ handleClick
1147
+ ? e => {
1148
+ // Keep a token press from entering Tokenizer's input-focus
1149
+ // redirect. Click still edits/removes; Tab entry is unchanged.
1150
+ e.preventDefault();
1151
+ }
1152
+ : undefined
1153
+ }
1154
+ onClick={
1155
+ handleClick
1156
+ ? (e: React.MouseEvent) => {
1157
+ e.stopPropagation();
1158
+ handleClick();
1159
+ }
1160
+ : undefined
1161
+ }
1162
+ onRemove={handleRemove}
1163
+ isDisabled={isDisabled}
1164
+ />
1165
+ </TokenNamingContext>
931
1166
  );
932
1167
  },
933
1168
  [
@@ -935,16 +1170,23 @@ export function PowerSearch({
935
1170
  config,
936
1171
  configProp,
937
1172
  maxTokenLength,
938
- locale,
939
1173
  size,
940
1174
  isReadOnly,
941
1175
  isDisabled,
942
1176
  handleTokenClick,
943
1177
  componentOverrides,
944
1178
  t,
1179
+ popoverState,
1180
+ tokenText,
945
1181
  ],
946
1182
  );
947
1183
 
1184
+ const renderToken = useCallback(
1185
+ (item: PowerSearchItem, onRemove: () => void) =>
1186
+ renderFilterToken(item, onRemove),
1187
+ [renderFilterToken],
1188
+ );
1189
+
948
1190
  // Custom typeahead item renderer — adds field icon and description
949
1191
  const renderItem = useCallback(
950
1192
  (item: PowerSearchItem) => {
@@ -1062,30 +1304,44 @@ export function PowerSearch({
1062
1304
  return resultCount;
1063
1305
  }, [resultCount, t]);
1064
1306
 
1065
- // Announce result-count changes to screen readers through a polite live
1066
- // region, mirroring the way Typeahead announces its dropdown result count.
1067
- // The count is otherwise only shown visually and stays silent to assistive
1068
- // tech. Only a change after mount is spoken: the guard compares against the
1069
- // last announced text rather than a mounted flag, so StrictMode's effect
1070
- // replay (which resets a flag and would speak the count present on mount
1071
- // unprompted) announces nothing until the count changes.
1307
+ // This Effect synchronizes a polite live region with the result count,
1308
+ // mirroring the way Typeahead announces its dropdown result count: the count
1309
+ // is otherwise only shown visually and stays silent to assistive tech. The
1310
+ // region follows where the field is: the modal that holds the field (a
1311
+ // Dialog, a modal Drawer), else the shared channel. While a sheet of the flow is open the sheet itself announces the
1312
+ // count from inside its dialog, and this Effect stays quiet. Only a
1313
+ // change after mount is spoken — the count present on mount is not
1314
+ // announced unprompted — and the guard compares against the last announced
1315
+ // text rather than a mounted flag, so StrictMode's effect replay announces
1316
+ // nothing either. A change that lands while the flow is closing waits for
1317
+ // the trigger focus, since the dialog is still modal through its exit. The
1318
+ // announcement acquires no resource, so there is no cleanup.
1072
1319
  const announce = useAnnounce();
1073
1320
  // PowerSearch words its Clear all as its record does; the field announces
1074
1321
  // these words instead of Tokenizer's (component:Tokenizer AR7).
1322
+ const hasReadOnlyFilter = filters.some(filter => filter.isReadOnly);
1075
1323
  const clearAllMessage = useMemo(
1076
- () => t('@astryx.powersearch.allFiltersRemoved'),
1077
- [t],
1324
+ () =>
1325
+ hasReadOnlyFilter
1326
+ ? t('@astryx.powersearch.editableFiltersRemoved')
1327
+ : t('@astryx.powersearch.allFiltersRemoved'),
1328
+ [hasReadOnlyFilter, t],
1078
1329
  );
1079
1330
  const announcedResultCountRef = useRef(resultCountText);
1331
+ const isSheetExiting = isSheetPresented && !isSheetOpen;
1080
1332
  useEffect(() => {
1081
1333
  if (resultCountText === announcedResultCountRef.current) {
1082
1334
  return;
1083
1335
  }
1084
1336
  announcedResultCountRef.current = resultCountText;
1085
1337
  if (resultCountText != null) {
1086
- announce(resultCountText);
1338
+ if (isSheetExiting) {
1339
+ afterCloseResultCountRef.current = resultCountText;
1340
+ } else if (!isSheetOpen) {
1341
+ announce(resultCountText);
1342
+ }
1087
1343
  }
1088
- }, [resultCountText, announce]);
1344
+ }, [resultCountText, announce, isSheetExiting, isSheetOpen]);
1089
1345
 
1090
1346
  // Build combined endContent from resultCount + endContent props
1091
1347
  const combinedEndContent = useMemo((): React.ReactNode => {
@@ -1106,48 +1362,266 @@ export function PowerSearch({
1106
1362
  return resultCountNode || endContent || undefined;
1107
1363
  }, [resultCountText, endContent]);
1108
1364
 
1365
+ // ------------------------------------------------------- sheet trigger --
1366
+
1367
+ // Disabled-reason tooltip for the sheet trigger. Disabled controls swallow
1368
+ // pointer events, so the listeners attach to the field's control box; the
1369
+ // trigger stays perceivable through aria-disabled.
1370
+ const showsDisabledMessage = isDisabled && !!disabledMessage;
1371
+ const disabledMessageTooltip = useTooltip({
1372
+ placement: 'above',
1373
+ focusTrigger: 'always',
1374
+ isEnabled: isSheetPresentation && showsDisabledMessage,
1375
+ });
1376
+ const rootRef = useMergedRefs(
1377
+ ref,
1378
+ popover.triggerRef as React.Ref<HTMLDivElement>,
1379
+ handOffFocusRef,
1380
+ );
1381
+
1382
+ const openSheet = useCallback(() => {
1383
+ if (isDisabled) {
1384
+ return;
1385
+ }
1386
+ // The flow restores focus to the element that was active when it opened;
1387
+ // make sure that is the trigger even for a tap on a token.
1388
+ triggerRef.current?.focus({preventScroll: true});
1389
+ setIsSheetPresented(true);
1390
+ setIsSheetOpen(true);
1391
+ }, [isDisabled]);
1392
+
1393
+ // The field's end lane — the result count and `endContent` — sits outside
1394
+ // the trigger button: a caller's action button there keeps its own tab stop
1395
+ // and taps, and an interactive control must not nest in a component
1396
+ // button, so a tap on it must not open the flow.
1397
+ const endLaneRef = useRef<HTMLDivElement>(null);
1398
+ const handleTriggerWrapperClick = useCallback(
1399
+ (event: React.MouseEvent<HTMLDivElement>) => {
1400
+ // Tokens are display-only; a tap on them or the empty space around them
1401
+ // opens the flow. The trigger button handles its own click, and the end
1402
+ // lane handles its own.
1403
+ if (
1404
+ event.target instanceof Node &&
1405
+ (triggerRef.current?.contains(event.target) ||
1406
+ endLaneRef.current?.contains(event.target))
1407
+ ) {
1408
+ return;
1409
+ }
1410
+ openSheet();
1411
+ },
1412
+ [openSheet],
1413
+ );
1414
+
1415
+ const handleTriggerFocusWithin = useCallback(
1416
+ (event: React.FocusEvent<HTMLDivElement>) => {
1417
+ // The flow focuses the trigger only after its exit completes; that focus
1418
+ // is the signal that the retained sheets can leave. If the resolution
1419
+ // moved to the popover meanwhile, the tokenizer input takes over the
1420
+ // focus once it mounts (the hand-off above).
1421
+ if (!isSheetOpen && isSheetPresented) {
1422
+ setIsSheetPresented(false);
1423
+ // The dialogs have closed: the page-body channel is exposed again.
1424
+ const resultCount = afterCloseResultCountRef.current;
1425
+ afterCloseResultCountRef.current = null;
1426
+ if (resultCount != null) {
1427
+ announce(resultCount);
1428
+ }
1429
+ }
1430
+ if (
1431
+ !(event.relatedTarget instanceof Node) ||
1432
+ !event.currentTarget.contains(event.relatedTarget)
1433
+ ) {
1434
+ handleFieldFocus(event);
1435
+ }
1436
+ },
1437
+ [announce, handleFieldFocus, isSheetOpen, isSheetPresented],
1438
+ );
1439
+ const handleTriggerBlurWithin = useCallback(
1440
+ (event: React.FocusEvent<HTMLDivElement>) => {
1441
+ if (
1442
+ !(event.relatedTarget instanceof Node) ||
1443
+ !event.currentTarget.contains(event.relatedTarget)
1444
+ ) {
1445
+ handleFieldBlur(event);
1446
+ }
1447
+ },
1448
+ [handleFieldBlur],
1449
+ );
1450
+
1451
+ if (isSheetPresentation) {
1452
+ const triggerDescribedBy =
1453
+ [
1454
+ status?.message ? statusMessageId : null,
1455
+ showsDisabledMessage ? disabledMessageTooltip.describedBy : null,
1456
+ ]
1457
+ .filter(Boolean)
1458
+ .join(' ') || undefined;
1459
+
1460
+ return (
1461
+ <div key="sheet" ref={rootRef} {...themeProps('power-search')}>
1462
+ <Field
1463
+ label={label}
1464
+ isLabelHidden={isLabelHidden}
1465
+ inputID={triggerId}
1466
+ labelID={triggerLabelId}
1467
+ isDisabled={isDisabled}
1468
+ status={
1469
+ status
1470
+ ? {
1471
+ type: status.type,
1472
+ message: status.message,
1473
+ messageID: status.message ? statusMessageId : undefined,
1474
+ }
1475
+ : undefined
1476
+ }
1477
+ statusVariant={statusVariant}
1478
+ xstyle={xstyle}
1479
+ className={className}
1480
+ style={style}>
1481
+ <div
1482
+ ref={disabledMessageTooltip.ref}
1483
+ onClick={handleTriggerWrapperClick}
1484
+ onFocus={handleTriggerFocusWithin}
1485
+ onBlur={handleTriggerBlurWithin}
1486
+ aria-disabled={isDisabled ? true : undefined}
1487
+ {...stylex.props(
1488
+ inputWrapperStyles.base,
1489
+ sheetTriggerStyles.wrapper,
1490
+ filters.length > 0 && sheetTriggerStyles.wrapperWithTokens,
1491
+ sheetTriggerSizeStyles[size],
1492
+ isDisabled && inputWrapperStyles.disabled,
1493
+ status && inputStatusBorderStyles[status.type],
1494
+ status &&
1495
+ !isDisabled &&
1496
+ inputStatusHoverShadowStyles[status.type],
1497
+ status && inputStatusFocusWithinStyles[status.type],
1498
+ )}>
1499
+ {/* The trigger's name is the label plus everything visible here:
1500
+ the placeholder, or the tokens, so speech input can say what
1501
+ it sees and assistive technology hears the selected filters
1502
+ (WCAG 2.5.3). */}
1503
+ <div
1504
+ id={triggerContentId}
1505
+ {...stylex.props(sheetTriggerStyles.contentSection)}>
1506
+ {isRenderable(startIcon as React.ReactNode) && (
1507
+ <span
1508
+ {...stylex.props(
1509
+ filters.length > 0 &&
1510
+ sheetTriggerStyles.startIconWithTokens,
1511
+ )}>
1512
+ {renderIconSlot(startIcon, {size: 'sm', color: 'secondary'})}
1513
+ </span>
1514
+ )}
1515
+ {tokenizerValue.map(item => (
1516
+ <React.Fragment key={item.id}>
1517
+ {renderFilterToken(item, undefined)}
1518
+ </React.Fragment>
1519
+ ))}
1520
+ <button
1521
+ type="button"
1522
+ id={triggerId}
1523
+ ref={triggerRef}
1524
+ // The field's primary control: `data-testid` lands here, as it
1525
+ // lands on the combobox input in the popover presentation.
1526
+ data-testid={testId}
1527
+ autoFocus={autoFocusOnMount}
1528
+ onClick={openSheet}
1529
+ aria-disabled={isDisabled ? true : undefined}
1530
+ aria-haspopup="dialog"
1531
+ aria-expanded={isSheetOpen}
1532
+ aria-labelledby={`${triggerLabelId} ${triggerContentId}`}
1533
+ aria-describedby={triggerDescribedBy}
1534
+ {...stylex.props(
1535
+ sheetTriggerStyles.trigger,
1536
+ filters.length > 0 && sheetTriggerStyles.triggerWithTokens,
1537
+ )}>
1538
+ {filters.length === 0 ? placeholder : null}
1539
+ </button>
1540
+ </div>
1541
+ {(endContent || isRenderable(resultCountText)) && (
1542
+ <div
1543
+ ref={endLaneRef}
1544
+ {...stylex.props(sheetTriggerStyles.endSection)}>
1545
+ {isRenderable(resultCountText) && (
1546
+ <span {...stylex.props(resultCountStyles.text)}>
1547
+ {resultCountText}
1548
+ </span>
1549
+ )}
1550
+ {endContent}
1551
+ </div>
1552
+ )}
1553
+ </div>
1554
+ {showsDisabledMessage &&
1555
+ disabledMessageTooltip.renderTooltip(disabledMessage)}
1556
+ </Field>
1557
+ {/* Mounted from the press until the flow's exit has completed and
1558
+ focus is back on the trigger (`isSheetPresented`): at rest the
1559
+ field renders its trigger alone — no closed dialogs, rows, search
1560
+ field or editors in the DOM. */}
1561
+ {(isSheetOpen || isSheetPresented) && (
1562
+ <PowerSearchSheetHost
1563
+ isOpen={isSheetOpen}
1564
+ onOpenChange={setIsSheetOpen}
1565
+ finalFocusRef={triggerRef}
1566
+ resultCountText={resultCountText}
1567
+ config={config}
1568
+ configProp={configProp}
1569
+ filters={filters}
1570
+ onChange={onChange}
1571
+ searchSource={searchSource}
1572
+ placeholder={placeholder}
1573
+ isReadOnly={isReadOnly}
1574
+ isDisabled={isDisabled}
1575
+ hasClear={hasClear}
1576
+ maxOperatorMenuItems={maxOperatorMenuItems}
1577
+ maxSearchResults={maxSearchResults}
1578
+ saveButtonLabel={popoverSaveButtonLabelFromProps}
1579
+ components={componentOverrides}
1580
+ />
1581
+ )}
1582
+ </div>
1583
+ );
1584
+ }
1585
+
1109
1586
  return (
1110
1587
  <>
1111
- <div
1112
- ref={useMergedRefs(
1113
- ref,
1114
- popover.triggerRef as React.Ref<HTMLDivElement>,
1115
- )}
1116
- {...themeProps('power-search')}>
1588
+ <div key="popover" ref={rootRef} {...themeProps('power-search')}>
1117
1589
  {/* The field's Clear all announces PowerSearch's own words
1118
1590
  (component:PowerSearch; component:Tokenizer AR7). */}
1119
1591
  <TokenizerClearAllMessageContext value={clearAllMessage}>
1120
- <Tokenizer
1121
- handleRef={tokenizerRef}
1122
- label={label}
1123
- isLabelHidden={isLabelHidden}
1124
- searchSource={searchSource}
1125
- value={tokenizerValue}
1126
- onChange={handleTokenizerChange}
1127
- renderToken={renderToken}
1128
- renderItem={renderItem}
1129
- maxMenuItems={MAX_BROWSE_MENU_ITEMS}
1130
- menuWidth={menuWidth}
1131
- placeholder={filters.length === 0 ? placeholder : ''}
1132
- hasAutoFocus={hasAutoFocus}
1133
- hasClear={hasClear && !isReadOnly}
1134
- startIcon={startIcon}
1135
- endContent={combinedEndContent}
1136
- isDisabled={isDisabled}
1137
- disabledMessage={disabledMessage}
1138
- size={size}
1139
- tokenOverflowBehavior={tokenOverflowBehavior}
1140
- hasEntriesOnFocus
1141
- debounceMs={0}
1142
- status={status}
1143
- statusVariant={statusVariant}
1144
- onFocus={onFocus}
1145
- onBlur={onBlur}
1146
- containerXstyle={xstyle}
1147
- containerClassName={className}
1148
- containerStyle={style}
1149
- data-testid={testId}
1150
- />
1592
+ <TokenizerItemRemovableContext value={isItemRemovable}>
1593
+ <Tokenizer
1594
+ handleRef={tokenizerRef}
1595
+ label={label}
1596
+ isLabelHidden={isLabelHidden}
1597
+ searchSource={searchSource}
1598
+ value={tokenizerValue}
1599
+ onChange={handleTokenizerChange}
1600
+ renderToken={renderToken}
1601
+ renderItem={renderItem}
1602
+ maxMenuItems={MAX_BROWSE_MENU_ITEMS}
1603
+ menuWidth={menuWidth}
1604
+ placeholder={filters.length === 0 ? placeholder : ''}
1605
+ hasAutoFocus={autoFocusOnMount}
1606
+ hasClear={hasClear && !isReadOnly}
1607
+ startIcon={startIcon}
1608
+ endContent={combinedEndContent}
1609
+ isDisabled={isDisabled}
1610
+ disabledMessage={disabledMessage}
1611
+ size={size}
1612
+ tokenOverflowBehavior={tokenOverflowBehavior}
1613
+ hasEntriesOnFocus
1614
+ debounceMs={0}
1615
+ status={status}
1616
+ statusVariant={statusVariant}
1617
+ onFocus={handleFieldFocus}
1618
+ onBlur={handleFieldBlur}
1619
+ containerXstyle={xstyle}
1620
+ containerClassName={className}
1621
+ containerStyle={style}
1622
+ data-testid={testId}
1623
+ />
1624
+ </TokenizerItemRemovableContext>
1151
1625
  </TokenizerClearAllMessageContext>
1152
1626
  </div>
1153
1627
  {popover.render(popoverContent, {