@astryxdesign/core 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (551) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/dist/AlertDialog/AlertDialog.d.ts +10 -2
  3. package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
  4. package/dist/AlertDialog/AlertDialog.js +22 -3
  5. package/dist/AppShell/AppShell.d.ts +2 -2
  6. package/dist/AppShell/AppShell.d.ts.map +1 -1
  7. package/dist/AppShell/AppShell.js +40 -9
  8. package/dist/AspectRatio/AspectRatio.d.ts +13 -0
  9. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  10. package/dist/AspectRatio/AspectRatio.js +9 -0
  11. package/dist/Avatar/Avatar.d.ts.map +1 -1
  12. package/dist/Avatar/Avatar.js +33 -20
  13. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  14. package/dist/AvatarGroup/AvatarGroupOverflow.js +2 -3
  15. package/dist/Banner/Banner.d.ts.map +1 -1
  16. package/dist/Banner/Banner.js +15 -14
  17. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  18. package/dist/Breadcrumbs/BreadcrumbItem.js +21 -8
  19. package/dist/Breadcrumbs/index.d.ts +2 -1
  20. package/dist/Breadcrumbs/index.d.ts.map +1 -1
  21. package/dist/Breadcrumbs/index.js +1 -0
  22. package/dist/Button/Button.d.ts.map +1 -1
  23. package/dist/Button/Button.js +11 -15
  24. package/dist/Calendar/Calendar.d.ts.map +1 -1
  25. package/dist/Calendar/Calendar.js +2 -1
  26. package/dist/Calendar/styles.d.ts +0 -2
  27. package/dist/Calendar/styles.d.ts.map +1 -1
  28. package/dist/Calendar/styles.js +0 -2
  29. package/dist/Carousel/Carousel.d.ts.map +1 -1
  30. package/dist/Carousel/Carousel.js +8 -12
  31. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  32. package/dist/Chat/ChatDictationButton.js +2 -1
  33. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  34. package/dist/Chat/ChatLayoutScrollButton.js +3 -1
  35. package/dist/Chat/ChatSendButton.d.ts.map +1 -1
  36. package/dist/Chat/ChatSendButton.js +4 -3
  37. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  38. package/dist/Chat/ChatToolCalls.js +17 -17
  39. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  40. package/dist/Chat/useTriggerMenu.js +2 -6
  41. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  42. package/dist/CheckboxInput/CheckboxInput.js +36 -125
  43. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  44. package/dist/ClickableCard/ClickableCard.js +3 -11
  45. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  46. package/dist/CodeBlock/CodeBlock.js +84 -67
  47. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  48. package/dist/Collapsible/Collapsible.js +35 -15
  49. package/dist/CommandPalette/CommandPaletteItem.d.ts +1 -1
  50. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  51. package/dist/CommandPalette/CommandPaletteItem.js +6 -3
  52. package/dist/CommandPalette/CommandPaletteList.js +1 -1
  53. package/dist/ComplexSelector/ComplexSelector.d.ts +1 -1
  54. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  55. package/dist/ComplexSelector/ComplexSelector.js +35 -33
  56. package/dist/ContextMenu/ContextMenu.d.ts +2 -2
  57. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  58. package/dist/ContextMenu/ContextMenu.js +1 -1
  59. package/dist/ContextMenu/index.d.ts +2 -1
  60. package/dist/ContextMenu/index.d.ts.map +1 -1
  61. package/dist/ContextMenu/index.js +1 -0
  62. package/dist/DateInput/DateInput.d.ts +9 -2
  63. package/dist/DateInput/DateInput.d.ts.map +1 -1
  64. package/dist/DateInput/DateInput.js +24 -23
  65. package/dist/DateRangeInput/DateRangeInput.d.ts +9 -2
  66. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  67. package/dist/DateRangeInput/DateRangeInput.js +11 -16
  68. package/dist/DateTimeInput/DateTimeInput.d.ts +9 -1
  69. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  70. package/dist/DateTimeInput/DateTimeInput.js +22 -17
  71. package/dist/Dialog/Dialog.d.ts.map +1 -1
  72. package/dist/Dialog/Dialog.js +4 -4
  73. package/dist/DropdownMenu/DropdownMenu.d.ts +33 -8
  74. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  75. package/dist/DropdownMenu/DropdownMenu.js +62 -20
  76. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +8 -11
  77. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  78. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +32 -31
  79. package/dist/DropdownMenu/DropdownMenuDivider.d.ts +25 -0
  80. package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -0
  81. package/dist/DropdownMenu/DropdownMenuDivider.js +66 -0
  82. package/dist/DropdownMenu/DropdownMenuItem.d.ts +15 -2
  83. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  84. package/dist/DropdownMenu/DropdownMenuItem.js +20 -8
  85. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +5 -5
  86. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
  87. package/dist/DropdownMenu/DropdownMenuRadioItem.js +24 -67
  88. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  89. package/dist/DropdownMenu/DropdownMenuSubMenu.js +2 -8
  90. package/dist/DropdownMenu/index.d.ts +2 -1
  91. package/dist/DropdownMenu/index.d.ts.map +1 -1
  92. package/dist/DropdownMenu/index.js +4 -0
  93. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  94. package/dist/DropdownMenu/renderDropdownItems.js +35 -27
  95. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  96. package/dist/EmptyState/EmptyState.js +9 -5
  97. package/dist/Field/InputClearButton.d.ts +16 -3
  98. package/dist/Field/InputClearButton.d.ts.map +1 -1
  99. package/dist/Field/InputClearButton.js +15 -4
  100. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  101. package/dist/HoverCard/useHoverCard.js +3 -19
  102. package/dist/Indicator/CheckIndicator.d.ts +25 -0
  103. package/dist/Indicator/CheckIndicator.d.ts.map +1 -0
  104. package/dist/Indicator/CheckIndicator.js +155 -0
  105. package/dist/Indicator/CheckboxIndicator.d.ts +22 -0
  106. package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -0
  107. package/dist/Indicator/CheckboxIndicator.js +173 -0
  108. package/dist/Indicator/RadioIndicator.d.ts +23 -0
  109. package/dist/Indicator/RadioIndicator.d.ts.map +1 -0
  110. package/dist/Indicator/RadioIndicator.js +137 -0
  111. package/dist/Indicator/index.d.ts +24 -0
  112. package/dist/Indicator/index.d.ts.map +1 -0
  113. package/dist/Indicator/index.js +26 -0
  114. package/dist/Indicator/indicator.markers.stylex.d.ts +16 -0
  115. package/dist/Indicator/indicator.markers.stylex.d.ts.map +1 -0
  116. package/dist/Indicator/indicator.markers.stylex.js +21 -0
  117. package/dist/Indicator/indicatorRegistry.d.ts +71 -0
  118. package/dist/Indicator/indicatorRegistry.d.ts.map +1 -0
  119. package/dist/Indicator/indicatorRegistry.js +93 -0
  120. package/dist/Indicator/types.d.ts +167 -0
  121. package/dist/Indicator/types.d.ts.map +1 -0
  122. package/dist/Indicator/types.js +1 -0
  123. package/dist/Indicator/useIndicator.d.ts +18 -0
  124. package/dist/Indicator/useIndicator.d.ts.map +1 -0
  125. package/dist/Indicator/useIndicator.js +30 -0
  126. package/dist/Item/Item.d.ts.map +1 -1
  127. package/dist/Item/Item.js +4 -8
  128. package/dist/Layer/useLayer.d.ts +14 -0
  129. package/dist/Layer/useLayer.d.ts.map +1 -1
  130. package/dist/Layer/useLayer.js +25 -2
  131. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  132. package/dist/Lightbox/Lightbox.js +56 -53
  133. package/dist/Link/Link.d.ts.map +1 -1
  134. package/dist/Link/Link.js +4 -5
  135. package/dist/MobileNav/MobileNav.js +8 -8
  136. package/dist/MoreMenu/MoreMenu.d.ts +14 -1
  137. package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
  138. package/dist/MoreMenu/MoreMenu.js +4 -0
  139. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  140. package/dist/MultiSelector/MultiSelector.js +78 -75
  141. package/dist/NumberInput/NumberInput.d.ts +27 -1
  142. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  143. package/dist/NumberInput/NumberInput.js +230 -42
  144. package/dist/Outline/Outline.d.ts.map +1 -1
  145. package/dist/Outline/Outline.js +2 -3
  146. package/dist/Pagination/Pagination.d.ts.map +1 -1
  147. package/dist/Pagination/Pagination.js +61 -63
  148. package/dist/Popover/Popover.d.ts.map +1 -1
  149. package/dist/Popover/Popover.js +6 -8
  150. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  151. package/dist/PowerSearch/PowerSearch.js +1 -1
  152. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  153. package/dist/PowerSearch/PowerSearchEditPopover.js +7 -0
  154. package/dist/ProgressBar/ProgressBar.d.ts +11 -4
  155. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  156. package/dist/ProgressBar/ProgressBar.js +96 -6
  157. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  158. package/dist/RadioList/RadioListItem.js +29 -84
  159. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  160. package/dist/Resizable/ResizeHandle.js +2 -3
  161. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  162. package/dist/SegmentedControl/SegmentedControlItem.js +2 -3
  163. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  164. package/dist/SelectableCard/SelectableCard.js +3 -11
  165. package/dist/Selector/Selector.d.ts +2 -1
  166. package/dist/Selector/Selector.d.ts.map +1 -1
  167. package/dist/Selector/Selector.js +164 -94
  168. package/dist/Selector/hooks.d.ts +18 -6
  169. package/dist/Selector/hooks.d.ts.map +1 -1
  170. package/dist/Selector/hooks.js +57 -40
  171. package/dist/SideNav/SideNav.d.ts.map +1 -1
  172. package/dist/SideNav/SideNav.js +1 -1
  173. package/dist/SideNav/SideNavCollapseButton.d.ts +1 -1
  174. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  175. package/dist/SideNav/SideNavCollapseButton.js +29 -14
  176. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  177. package/dist/SideNav/SideNavHeading.js +56 -20
  178. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  179. package/dist/SideNav/SideNavItem.js +28 -22
  180. package/dist/Stack/stack.stylex.d.ts +3 -3
  181. package/dist/Switch/Switch.js +11 -10
  182. package/dist/TabList/Tab.d.ts.map +1 -1
  183. package/dist/TabList/Tab.js +2 -3
  184. package/dist/TabList/TabMenu.d.ts.map +1 -1
  185. package/dist/TabList/TabMenu.js +41 -19
  186. package/dist/Table/BaseTable.d.ts.map +1 -1
  187. package/dist/Table/BaseTable.js +8 -0
  188. package/dist/Table/index.d.ts +1 -1
  189. package/dist/Table/index.d.ts.map +1 -1
  190. package/dist/Table/index.js +1 -1
  191. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts +1 -1
  192. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  193. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +22 -18
  194. package/dist/Table/plugins/rowExpansion/index.d.ts +1 -1
  195. package/dist/Table/plugins/rowExpansion/index.d.ts.map +1 -1
  196. package/dist/Table/plugins/rowExpansion/index.js +1 -1
  197. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts +41 -76
  198. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  199. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +94 -345
  200. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  201. package/dist/Table/plugins/sortable/useTableSortable.js +23 -4
  202. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
  203. package/dist/Table/plugins/tree/useTableTreeData.js +29 -33
  204. package/dist/Table/tableContextMenu.d.ts.map +1 -1
  205. package/dist/Table/tableContextMenu.js +2 -1
  206. package/dist/Table/types.d.ts +11 -0
  207. package/dist/Table/types.d.ts.map +1 -1
  208. package/dist/TextArea/TextArea.d.ts +10 -1
  209. package/dist/TextArea/TextArea.d.ts.map +1 -1
  210. package/dist/TextArea/TextArea.js +16 -6
  211. package/dist/TextInput/TextInput.d.ts +10 -1
  212. package/dist/TextInput/TextInput.d.ts.map +1 -1
  213. package/dist/TextInput/TextInput.js +13 -19
  214. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  215. package/dist/Thumbnail/Thumbnail.js +32 -26
  216. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  217. package/dist/TimeInput/TimeInput.js +7 -15
  218. package/dist/Timestamp/TimestampHoverCard.d.ts.map +1 -1
  219. package/dist/Timestamp/TimestampHoverCard.js +16 -34
  220. package/dist/Token/Token.d.ts.map +1 -1
  221. package/dist/Token/Token.js +20 -11
  222. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  223. package/dist/Tokenizer/Tokenizer.js +4 -2
  224. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  225. package/dist/Tooltip/useTooltip.js +3 -19
  226. package/dist/TopNav/TopNav.d.ts.map +1 -1
  227. package/dist/TopNav/TopNav.js +6 -5
  228. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  229. package/dist/TopNav/TopNavHeading.js +54 -16
  230. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  231. package/dist/TopNav/TopNavItem.js +3 -9
  232. package/dist/TopNav/TopNavMegaMenu.d.ts +7 -0
  233. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  234. package/dist/TopNav/TopNavMegaMenu.js +146 -60
  235. package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
  236. package/dist/TopNav/TopNavMegaMenuItem.js +22 -3
  237. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  238. package/dist/TopNav/TopNavMenu.js +83 -38
  239. package/dist/TreeList/TreeList.d.ts.map +1 -1
  240. package/dist/TreeList/TreeList.js +13 -0
  241. package/dist/TreeList/TreeListBranches.d.ts +1 -1
  242. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  243. package/dist/TreeList/TreeListBranches.js +14 -4
  244. package/dist/TreeList/TreeListItem.d.ts +11 -1
  245. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  246. package/dist/TreeList/TreeListItem.js +57 -30
  247. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  248. package/dist/Typeahead/BaseTypeahead.js +16 -8
  249. package/dist/astryx.css +74 -85
  250. package/dist/astryx.umd.js +56 -56
  251. package/dist/astryx.umd.js.map +4 -4
  252. package/dist/hooks/containerReveal.stylex.d.ts +61 -0
  253. package/dist/hooks/containerReveal.stylex.d.ts.map +1 -0
  254. package/dist/hooks/containerReveal.stylex.js +63 -0
  255. package/dist/hooks/index.d.ts +3 -0
  256. package/dist/hooks/index.d.ts.map +1 -1
  257. package/dist/hooks/index.js +2 -0
  258. package/dist/hooks/useClipboard.d.ts +59 -0
  259. package/dist/hooks/useClipboard.d.ts.map +1 -0
  260. package/dist/hooks/useClipboard.js +106 -0
  261. package/dist/hooks/useContainerReveal.d.ts +14 -8
  262. package/dist/hooks/useContainerReveal.d.ts.map +1 -1
  263. package/dist/hooks/useContainerReveal.js +28 -69
  264. package/dist/hooks/useIndicatorFocusRing.d.ts +28 -0
  265. package/dist/hooks/useIndicatorFocusRing.d.ts.map +1 -0
  266. package/dist/hooks/useIndicatorFocusRing.js +96 -0
  267. package/dist/hooks/useLongPress.d.ts.map +1 -1
  268. package/dist/hooks/useLongPress.js +12 -2
  269. package/dist/hooks/useScrollLock.d.ts.map +1 -1
  270. package/dist/hooks/useScrollLock.js +33 -18
  271. package/dist/hooks/useStreamingText.d.ts +18 -3
  272. package/dist/hooks/useStreamingText.d.ts.map +1 -1
  273. package/dist/hooks/useStreamingText.js +47 -7
  274. package/dist/hooks/useTreeFocus.d.ts.map +1 -1
  275. package/dist/hooks/useTreeFocus.js +9 -3
  276. package/dist/hooks/useTypeahead.d.ts +3 -2
  277. package/dist/hooks/useTypeahead.d.ts.map +1 -1
  278. package/dist/hooks/useTypeahead.js +19 -7
  279. package/dist/index.d.ts +1 -0
  280. package/dist/index.d.ts.map +1 -1
  281. package/dist/index.js +1 -0
  282. package/dist/theme/defineTheme.d.ts +16 -0
  283. package/dist/theme/defineTheme.d.ts.map +1 -1
  284. package/dist/theme/defineTheme.js +8 -0
  285. package/dist/theme/derivedVarRegistry.d.ts +8 -0
  286. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  287. package/dist/theme/derivedVarRegistry.js +14 -13
  288. package/dist/theme/expandColorScale.d.ts +3 -1
  289. package/dist/theme/expandColorScale.d.ts.map +1 -1
  290. package/dist/theme/expandColorScale.js +17 -7
  291. package/dist/theme/expandTypeScale.d.ts.map +1 -1
  292. package/dist/theme/expandTypeScale.js +17 -0
  293. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  294. package/dist/theme/generateThemeRules.js +11 -0
  295. package/dist/theme/index.d.ts +1 -0
  296. package/dist/theme/index.d.ts.map +1 -1
  297. package/dist/utils/focusOutline.stylex.d.ts +107 -0
  298. package/dist/utils/focusOutline.stylex.d.ts.map +1 -0
  299. package/dist/utils/focusOutline.stylex.js +130 -0
  300. package/dist/utils/focusReturn.d.ts +24 -0
  301. package/dist/utils/focusReturn.d.ts.map +1 -0
  302. package/dist/utils/focusReturn.js +29 -0
  303. package/dist/utils/index.d.ts +1 -0
  304. package/dist/utils/index.d.ts.map +1 -1
  305. package/dist/utils/index.js +1 -0
  306. package/dist/utils/mergeRefs.d.ts.map +1 -1
  307. package/dist/utils/mergeRefs.js +7 -5
  308. package/dist/utils/rtlStyles.d.ts +2 -2
  309. package/dist/utils/rtlStyles.js +2 -2
  310. package/dist/utils/themeProps.d.ts +20 -1
  311. package/dist/utils/themeProps.d.ts.map +1 -1
  312. package/dist/utils/themeProps.js +8 -2
  313. package/locales/en.json +12 -0
  314. package/locales/pseudo.json +9 -0
  315. package/package.json +7 -2
  316. package/src/AlertDialog/AlertDialog.doc.mjs +16 -3
  317. package/src/AlertDialog/AlertDialog.test.tsx +208 -1
  318. package/src/AlertDialog/AlertDialog.tsx +21 -3
  319. package/src/AppShell/AppShell.doc.mjs +11 -0
  320. package/src/AppShell/AppShell.test.tsx +139 -0
  321. package/src/AppShell/AppShell.tsx +32 -22
  322. package/src/AspectRatio/AspectRatio.doc.mjs +27 -3
  323. package/src/AspectRatio/AspectRatio.test.tsx +30 -0
  324. package/src/AspectRatio/AspectRatio.tsx +13 -0
  325. package/src/Avatar/Avatar.doc.mjs +2 -13
  326. package/src/Avatar/Avatar.test.tsx +33 -8
  327. package/src/Avatar/Avatar.tsx +52 -50
  328. package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -9
  329. package/src/Banner/Banner.tsx +9 -6
  330. package/src/Breadcrumbs/BreadcrumbItem.tsx +16 -6
  331. package/src/Breadcrumbs/index.ts +5 -1
  332. package/src/Button/Button.tsx +21 -41
  333. package/src/Calendar/Calendar.tsx +2 -1
  334. package/src/Calendar/styles.ts +0 -8
  335. package/src/Carousel/Carousel.doc.mjs +3 -0
  336. package/src/Carousel/Carousel.tsx +10 -6
  337. package/src/Chat/Chat.doc.mjs +2 -0
  338. package/src/Chat/ChatDictationButton.tsx +7 -1
  339. package/src/Chat/ChatLayoutScrollButton.test.tsx +22 -0
  340. package/src/Chat/ChatLayoutScrollButton.tsx +8 -1
  341. package/src/Chat/ChatSendButton.tsx +2 -3
  342. package/src/Chat/ChatToolCalls.test.tsx +35 -0
  343. package/src/Chat/ChatToolCalls.tsx +36 -16
  344. package/src/Chat/useTriggerMenu.tsx +2 -5
  345. package/src/CheckboxInput/CheckboxInput.doc.mjs +4 -2
  346. package/src/CheckboxInput/CheckboxInput.test.tsx +115 -0
  347. package/src/CheckboxInput/CheckboxInput.tsx +45 -202
  348. package/src/ClickableCard/ClickableCard.tsx +6 -11
  349. package/src/CodeBlock/CodeBlock.doc.mjs +3 -0
  350. package/src/CodeBlock/CodeBlock.test.tsx +91 -0
  351. package/src/CodeBlock/CodeBlock.tsx +57 -80
  352. package/src/Collapsible/Collapsible.tsx +22 -18
  353. package/src/CommandPalette/CommandPalette.test.tsx +29 -0
  354. package/src/CommandPalette/CommandPaletteItem.tsx +7 -4
  355. package/src/CommandPalette/CommandPaletteList.tsx +2 -2
  356. package/src/ComplexSelector/ComplexSelector.doc.mjs +42 -0
  357. package/src/ComplexSelector/ComplexSelector.test.tsx +64 -0
  358. package/src/ComplexSelector/ComplexSelector.tsx +46 -29
  359. package/src/ContextMenu/ContextMenu.doc.mjs +1 -1
  360. package/src/ContextMenu/ContextMenu.test.tsx +41 -0
  361. package/src/ContextMenu/ContextMenu.tsx +3 -3
  362. package/src/ContextMenu/index.ts +5 -1
  363. package/src/DateInput/DateInput.doc.mjs +18 -3
  364. package/src/DateInput/DateInput.test.tsx +93 -11
  365. package/src/DateInput/DateInput.tsx +41 -19
  366. package/src/DateRangeInput/DateRangeInput.doc.mjs +10 -2
  367. package/src/DateRangeInput/DateRangeInput.test.tsx +110 -14
  368. package/src/DateRangeInput/DateRangeInput.tsx +24 -12
  369. package/src/DateTimeInput/DateTimeInput.doc.mjs +16 -2
  370. package/src/DateTimeInput/DateTimeInput.test.tsx +75 -0
  371. package/src/DateTimeInput/DateTimeInput.tsx +35 -8
  372. package/src/Dialog/Dialog.tsx +2 -9
  373. package/src/DropdownMenu/DropdownMenu.doc.mjs +5 -9
  374. package/src/DropdownMenu/DropdownMenu.test.tsx +534 -4
  375. package/src/DropdownMenu/DropdownMenu.tsx +113 -52
  376. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +27 -30
  377. package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +34 -0
  378. package/src/DropdownMenu/DropdownMenuDivider.tsx +81 -0
  379. package/src/DropdownMenu/DropdownMenuItem.doc.mjs +39 -4
  380. package/src/DropdownMenu/DropdownMenuItem.tsx +48 -10
  381. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +30 -76
  382. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +25 -19
  383. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +44 -14
  384. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +1 -5
  385. package/src/DropdownMenu/index.ts +8 -1
  386. package/src/DropdownMenu/renderDropdownItems.tsx +33 -34
  387. package/src/EmptyState/EmptyState.doc.mjs +4 -0
  388. package/src/EmptyState/EmptyState.test.tsx +62 -0
  389. package/src/EmptyState/EmptyState.tsx +16 -6
  390. package/src/Field/Field.doc.mjs +1 -0
  391. package/src/Field/InputClearButton.test.tsx +117 -0
  392. package/src/Field/InputClearButton.tsx +30 -3
  393. package/src/HoverCard/useHoverCard.tsx +2 -23
  394. package/src/Icon/Icon.doc.mjs +2 -1
  395. package/src/Indicator/CheckIndicator.tsx +174 -0
  396. package/src/Indicator/CheckboxIndicator.tsx +257 -0
  397. package/src/Indicator/Indicator.doc.mjs +344 -0
  398. package/src/Indicator/Indicator.test.tsx +393 -0
  399. package/src/Indicator/RadioIndicator.tsx +206 -0
  400. package/src/Indicator/index.ts +45 -0
  401. package/src/Indicator/indicator.markers.stylex.ts +19 -0
  402. package/src/Indicator/indicatorRegistry.test.tsx +106 -0
  403. package/src/Indicator/indicatorRegistry.ts +120 -0
  404. package/src/Indicator/types.ts +186 -0
  405. package/src/Indicator/useIndicator.ts +39 -0
  406. package/src/Item/Item.doc.mjs +1 -1
  407. package/src/Item/Item.tsx +7 -14
  408. package/src/Layer/useLayer.doc.mjs +2 -2
  409. package/src/Layer/useLayer.test.tsx +85 -0
  410. package/src/Layer/useLayer.tsx +39 -1
  411. package/src/Lightbox/Lightbox.doc.mjs +1 -1
  412. package/src/Lightbox/Lightbox.tsx +43 -38
  413. package/src/Link/Link.tsx +4 -11
  414. package/src/MobileNav/MobileNav.tsx +3 -3
  415. package/src/MoreMenu/MoreMenu.doc.mjs +31 -0
  416. package/src/MoreMenu/MoreMenu.test.tsx +68 -1
  417. package/src/MoreMenu/MoreMenu.tsx +19 -0
  418. package/src/MultiSelector/MultiSelector.doc.mjs +9 -1
  419. package/src/MultiSelector/MultiSelector.test.tsx +255 -16
  420. package/src/MultiSelector/MultiSelector.tsx +103 -99
  421. package/src/NumberInput/NumberInput.doc.mjs +265 -50
  422. package/src/NumberInput/NumberInput.test.tsx +726 -21
  423. package/src/NumberInput/NumberInput.tsx +353 -59
  424. package/src/Outline/Outline.tsx +2 -5
  425. package/src/Pagination/Pagination.doc.mjs +1 -1
  426. package/src/Pagination/Pagination.test.tsx +22 -7
  427. package/src/Pagination/Pagination.tsx +38 -35
  428. package/src/Popover/Popover.tsx +6 -7
  429. package/src/PowerSearch/PowerSearch.tsx +1 -1
  430. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +42 -0
  431. package/src/PowerSearch/PowerSearchEditPopover.tsx +7 -0
  432. package/src/ProgressBar/ProgressBar.doc.mjs +27 -5
  433. package/src/ProgressBar/ProgressBar.test.tsx +224 -1
  434. package/src/ProgressBar/ProgressBar.tsx +124 -22
  435. package/src/RadioList/RadioList.doc.mjs +4 -2
  436. package/src/RadioList/RadioListItem.tsx +31 -130
  437. package/src/Resizable/ResizeHandle.tsx +2 -9
  438. package/src/SegmentedControl/SegmentedControlItem.tsx +2 -9
  439. package/src/SelectableCard/SelectableCard.tsx +4 -9
  440. package/src/Selector/Selector.doc.mjs +7 -4
  441. package/src/Selector/Selector.test.tsx +935 -25
  442. package/src/Selector/Selector.tsx +189 -106
  443. package/src/Selector/hooks.ts +77 -50
  444. package/src/SideNav/SideNav.test.tsx +46 -0
  445. package/src/SideNav/SideNav.tsx +1 -0
  446. package/src/SideNav/SideNavCollapseButton.tsx +15 -10
  447. package/src/SideNav/SideNavHeading.tsx +48 -10
  448. package/src/SideNav/SideNavItem.tsx +20 -12
  449. package/src/Stack/Stack.doc.mjs +110 -0
  450. package/src/StatusDot/StatusDot.doc.mjs +12 -3
  451. package/src/Switch/Switch.test.tsx +32 -0
  452. package/src/Switch/Switch.tsx +8 -8
  453. package/src/TabList/Tab.tsx +2 -9
  454. package/src/TabList/TabMenu.tsx +14 -21
  455. package/src/Table/BaseTable.tsx +11 -0
  456. package/src/Table/index.ts +1 -4
  457. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +30 -14
  458. package/src/Table/plugins/rowExpansion/index.ts +1 -4
  459. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +125 -175
  460. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +115 -458
  461. package/src/Table/plugins/sortable/useTableSortable.tsx +2 -6
  462. package/src/Table/plugins/tree/useTableTreeData.tsx +43 -24
  463. package/src/Table/tableContextMenu.test.tsx +27 -0
  464. package/src/Table/tableContextMenu.tsx +1 -0
  465. package/src/Table/types.ts +11 -0
  466. package/src/Table/useTableGroupedRows.doc.mjs +1 -1
  467. package/src/Table/useTableRowExpansion.doc.mjs +43 -66
  468. package/src/TextArea/TextArea.doc.mjs +42 -2
  469. package/src/TextArea/TextArea.test.tsx +219 -0
  470. package/src/TextArea/TextArea.tsx +34 -7
  471. package/src/TextInput/TextInput.doc.mjs +18 -2
  472. package/src/TextInput/TextInput.test.tsx +189 -0
  473. package/src/TextInput/TextInput.tsx +25 -31
  474. package/src/Thumbnail/Thumbnail.tsx +2 -9
  475. package/src/TimeInput/TimeInput.doc.mjs +2 -2
  476. package/src/TimeInput/TimeInput.test.tsx +30 -3
  477. package/src/TimeInput/TimeInput.tsx +9 -26
  478. package/src/Timestamp/Timestamp.doc.mjs +4 -3
  479. package/src/Timestamp/TimestampHoverCard.tsx +13 -35
  480. package/src/Token/Token.tsx +5 -28
  481. package/src/Tokenizer/Tokenizer.doc.mjs +2 -2
  482. package/src/Tokenizer/Tokenizer.test.tsx +45 -3
  483. package/src/Tokenizer/Tokenizer.tsx +10 -2
  484. package/src/Tooltip/useTooltip.tsx +2 -23
  485. package/src/TopNav/TopNav.tsx +1 -3
  486. package/src/TopNav/TopNavHeading.tsx +42 -13
  487. package/src/TopNav/TopNavItem.tsx +3 -21
  488. package/src/TopNav/TopNavMegaMenu.test.tsx +433 -33
  489. package/src/TopNav/TopNavMegaMenu.tsx +118 -53
  490. package/src/TopNav/TopNavMegaMenuItem.tsx +2 -9
  491. package/src/TopNav/TopNavMenu.tsx +34 -37
  492. package/src/TreeList/TreeList.doc.mjs +5 -0
  493. package/src/TreeList/TreeList.test.tsx +206 -7
  494. package/src/TreeList/TreeList.tsx +22 -0
  495. package/src/TreeList/TreeListBranches.tsx +25 -2
  496. package/src/TreeList/TreeListItem.tsx +89 -52
  497. package/src/Typeahead/BaseTypeahead.tsx +19 -6
  498. package/src/Typeahead/Typeahead.doc.mjs +1 -0
  499. package/src/Typeahead/Typeahead.test.tsx +76 -3
  500. package/src/hooks/containerReveal.stylex.ts +102 -0
  501. package/src/hooks/index.ts +4 -0
  502. package/src/hooks/useClipboard.doc.mjs +87 -0
  503. package/src/hooks/useClipboard.test.tsx +151 -0
  504. package/src/hooks/useClipboard.ts +135 -0
  505. package/src/hooks/useContainerReveal.doc.mjs +7 -7
  506. package/src/hooks/useContainerReveal.test.tsx +42 -62
  507. package/src/hooks/useContainerReveal.ts +36 -91
  508. package/src/hooks/useImageMode.test.ts +1 -1
  509. package/src/hooks/useIndicatorFocusRing.tsx +120 -0
  510. package/src/hooks/useLongPress.test.tsx +18 -0
  511. package/src/hooks/useLongPress.ts +12 -2
  512. package/src/hooks/useScrollLock.test.ts +72 -0
  513. package/src/hooks/useScrollLock.ts +51 -19
  514. package/src/hooks/useStreamingText.test.ts +46 -1
  515. package/src/hooks/useStreamingText.ts +49 -7
  516. package/src/hooks/useTreeFocus.test.tsx +50 -0
  517. package/src/hooks/useTreeFocus.ts +14 -3
  518. package/src/hooks/useTypeahead.test.tsx +150 -0
  519. package/src/hooks/useTypeahead.ts +24 -9
  520. package/src/i18n/InternationalizationProvider.doc.mjs +7 -2
  521. package/src/index.ts +1 -0
  522. package/src/theme/defineTheme.ts +24 -0
  523. package/src/theme/derivedVarRegistry.test.ts +21 -1
  524. package/src/theme/derivedVarRegistry.ts +19 -6
  525. package/src/theme/expandColorScale.test.ts +14 -0
  526. package/src/theme/expandColorScale.ts +24 -9
  527. package/src/theme/expandTypeScale.test.ts +43 -0
  528. package/src/theme/expandTypeScale.ts +17 -0
  529. package/src/theme/generateThemeRules.test.ts +61 -20
  530. package/src/theme/generateThemeRules.ts +11 -0
  531. package/src/theme/index.ts +13 -0
  532. package/src/utils/focusOutline.stylex.ts +163 -0
  533. package/src/utils/focusReturn.test.ts +42 -0
  534. package/src/utils/focusReturn.ts +31 -0
  535. package/src/utils/index.ts +1 -0
  536. package/src/utils/mergeRefs.test.ts +48 -0
  537. package/src/utils/mergeRefs.ts +12 -6
  538. package/src/utils/rtlStyles.ts +2 -2
  539. package/src/utils/themeProps.ts +26 -4
  540. package/dist/CheckboxInput/checkbox.markers.stylex.d.ts +0 -9
  541. package/dist/CheckboxInput/checkbox.markers.stylex.d.ts.map +0 -1
  542. package/dist/CheckboxInput/checkbox.markers.stylex.js +0 -14
  543. package/dist/RadioList/radio.markers.stylex.d.ts +0 -7
  544. package/dist/RadioList/radio.markers.stylex.d.ts.map +0 -1
  545. package/dist/RadioList/radio.markers.stylex.js +0 -12
  546. package/dist/hooks/containerReveal.pool.stylex.d.ts +0 -303
  547. package/dist/hooks/containerReveal.pool.stylex.d.ts.map +0 -1
  548. package/dist/hooks/containerReveal.pool.stylex.js +0 -292
  549. package/src/CheckboxInput/checkbox.markers.stylex.ts +0 -12
  550. package/src/RadioList/radio.markers.stylex.ts +0 -10
  551. package/src/hooks/containerReveal.pool.stylex.ts +0 -571
@@ -2,7 +2,7 @@
2
2
 
3
3
  import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
4
4
  import {renderHook, act} from '@testing-library/react';
5
- import {useStreamingText} from './useStreamingText';
5
+ import {snapToGraphemeBoundary, useStreamingText} from './useStreamingText';
6
6
 
7
7
  describe('useStreamingText', () => {
8
8
  let rafCallbacks: ((time: number) => void)[];
@@ -189,4 +189,49 @@ describe('useStreamingText', () => {
189
189
  // (or close to it — the hook drains charsPerTick per tickMs)
190
190
  expect(result.current.length).toBeGreaterThan(targetText.length * 0.5);
191
191
  });
192
+
193
+ it('never reveals a tick boundary landing mid-surrogate-pair or mid-ZWJ-emoji-sequence (#4779)', () => {
194
+ // natural speed advances 10 UTF-16 code units per tick. 9 ASCII chars
195
+ // (indices 0-8) followed by a 4-person ZWJ family emoji (indices 9-19,
196
+ // one grapheme cluster, 11 code units) means the first tick's raw
197
+ // boundary (index 10) lands one code unit into the family emoji's first
198
+ // surrogate pair -- exactly the failure case from the issue.
199
+ const family = '\u{1F468}‍\u{1F469}‍\u{1F467}‍\u{1F466}';
200
+ const targetText = 'a'.repeat(9) + family + 'end';
201
+ const {result} = renderHook(() => useStreamingText(targetText, true));
202
+
203
+ expect(rafCallbacks.length).toBeGreaterThan(0);
204
+ const cb = rafCallbacks.pop()!;
205
+ act(() => cb(performance.now() + 20));
206
+
207
+ // Without the fix this would be 'a'.repeat(9) + a lone high surrogate
208
+ // (renders as U+FFFD). With the fix, the whole not-yet-complete family
209
+ // cluster is held back rather than rendering a broken glyph.
210
+ expect(result.current).toBe('a'.repeat(9));
211
+ });
212
+ });
213
+
214
+ describe('snapToGraphemeBoundary', () => {
215
+ it('does not split a surrogate pair', () => {
216
+ const text = 'a\u{1F389}b'; // 🎉 is a surrogate pair: indices 1 (high) and 2 (low)
217
+ expect(snapToGraphemeBoundary(text, 2)).toBe(1);
218
+ expect(snapToGraphemeBoundary(text, 1)).toBe(1);
219
+ });
220
+
221
+ it('does not split a ZWJ emoji sequence', () => {
222
+ const family = '\u{1F468}‍\u{1F469}‍\u{1F467}‍\u{1F466}';
223
+ const text = 'x' + family + 'y';
224
+ const familyStart = 1;
225
+ const familyEnd = 1 + family.length;
226
+ for (let offset = familyStart + 1; offset < familyEnd; offset++) {
227
+ expect(snapToGraphemeBoundary(text, offset)).toBe(familyStart);
228
+ }
229
+ });
230
+
231
+ it('leaves boundary offsets (start, end, plain-ASCII midpoints) unchanged', () => {
232
+ const text = 'hello';
233
+ expect(snapToGraphemeBoundary(text, 0)).toBe(0);
234
+ expect(snapToGraphemeBoundary(text, 5)).toBe(5);
235
+ expect(snapToGraphemeBoundary(text, 3)).toBe(3);
236
+ });
192
237
  });
@@ -9,9 +9,12 @@
9
9
  * @position Core hook; smooths bursty streamed text into steady character reveal
10
10
  *
11
11
  * Decouples the arrival rate of streamed chunks from the display rate,
12
- * draining characters at a steady pace using requestAnimationFrame.
13
- * Advances on word/syntax boundaries to avoid slicing mid-markdown or
14
- * mid-word, preventing visual glitches when used with markdown renderers.
12
+ * draining characters at a steady pace using requestAnimationFrame. The
13
+ * reveal cadence advances by a fixed number of UTF-16 code units per tick;
14
+ * the rendered slice is then snapped back to the nearest grapheme cluster
15
+ * boundary (see snapToGraphemeBoundary) so a tick landing inside a
16
+ * surrogate pair, flag sequence, or ZWJ emoji doesn't render a lone
17
+ * surrogate or a partial glyph for one frame.
15
18
  *
16
19
  * Animation timing derives from Astryx motion tokens (via useTheme) when
17
20
  * an Theme provider is present. Falls back to sensible defaults when
@@ -68,6 +71,43 @@ const CHARS_PER_TICK = {
68
71
  instant: Infinity,
69
72
  } as const;
70
73
 
74
+ // Reuse a single segmenter when the runtime supports Intl.Segmenter.
75
+ // Granularity is fixed and locale doesn't affect grapheme-cluster
76
+ // boundaries, so one instance is safe to share across calls.
77
+ const graphemeSegmenter =
78
+ typeof Intl.Segmenter === 'function'
79
+ ? new Intl.Segmenter(undefined, {granularity: 'grapheme'})
80
+ : null;
81
+
82
+ function isLowSurrogate(codeUnit: number): boolean {
83
+ return codeUnit >= 0xdc00 && codeUnit <= 0xdfff;
84
+ }
85
+
86
+ /**
87
+ * Snap a UTF-16 code-unit offset down to the start of the grapheme cluster
88
+ * it falls inside, so slicing `text` at the returned offset never splits a
89
+ * surrogate pair, flag sequence, or ZWJ emoji mid-glyph. Offsets at the
90
+ * start/end of the string are returned unchanged (nothing to snap).
91
+ *
92
+ * Falls back to snapping off a lone low surrogate when `Intl.Segmenter` is
93
+ * unavailable — narrower than full grapheme-cluster awareness (a ZWJ
94
+ * sequence or flag pair can still split in that path), but it's what's
95
+ * achievable without a full grapheme-aware dependency, and it's strictly
96
+ * better than the unguarded default of never checking at all.
97
+ */
98
+ export function snapToGraphemeBoundary(text: string, offset: number): number {
99
+ if (offset <= 0 || offset >= text.length) {
100
+ return offset;
101
+ }
102
+ if (graphemeSegmenter) {
103
+ // `containing` only returns undefined for an out-of-range index; offset
104
+ // is already guarded to [1, text.length - 1] above, so this is always
105
+ // defined at runtime — the `??` is to satisfy the type, which is wider.
106
+ return graphemeSegmenter.segment(text).containing(offset)?.index ?? offset;
107
+ }
108
+ return isLowSurrogate(text.charCodeAt(offset)) ? offset - 1 : offset;
109
+ }
110
+
71
111
  /**
72
112
  * Smooths bursty streamed text into a steady character-by-character reveal.
73
113
  *
@@ -76,9 +116,11 @@ const CHARS_PER_TICK = {
76
116
  * reduced motion, the progressive reveal is skipped entirely and the full
77
117
  * `targetText` is returned immediately.
78
118
  *
79
- * The hook advances on word and syntax boundaries, avoiding slices inside
80
- * markdown markers like `**`, backticks, `[]()`, etc. This prevents visual
81
- * glitches when the output is rendered through a markdown parser.
119
+ * The reveal cadence itself advances by a fixed number of UTF-16 code units
120
+ * per tick, not on word or markdown-syntax boundaries but the rendered
121
+ * slice is always snapped back to the nearest grapheme cluster boundary, so
122
+ * a tick landing mid-surrogate-pair or mid-emoji-sequence never renders a
123
+ * broken glyph.
82
124
  *
83
125
  * @example
84
126
  * ```
@@ -191,5 +233,5 @@ export function useStreamingText(
191
233
  return targetText;
192
234
  }
193
235
 
194
- return targetText.slice(0, displayedLen);
236
+ return targetText.slice(0, snapToGraphemeBoundary(targetText, displayedLen));
195
237
  }
@@ -261,4 +261,54 @@ describe('useTreeFocus activation + typeahead', () => {
261
261
  fireEvent.keyDown(tree, {key: 'c'});
262
262
  expect(screen.getByTestId('c')).toHaveFocus();
263
263
  });
264
+
265
+ it('typeahead cycles through same-letter matches on repeated presses', () => {
266
+ const nodes: Node[] = [
267
+ {id: 'apple', label: 'Apple', level: 1},
268
+ {id: 'apricot', label: 'Apricot', level: 1},
269
+ {id: 'avocado', label: 'Avocado', level: 1},
270
+ ];
271
+ render(<Tree collapsed={nodes} />);
272
+ const tree = screen.getByRole('tree');
273
+ screen.getByTestId('apple').focus();
274
+
275
+ fireEvent.keyDown(tree, {key: 'a'});
276
+ expect(screen.getByTestId('apricot')).toHaveFocus();
277
+
278
+ // A repeat must cycle, not extend the query to "aa" (as useTypeahead does).
279
+ fireEvent.keyDown(tree, {key: 'a'});
280
+ expect(screen.getByTestId('avocado')).toHaveFocus();
281
+ });
282
+
283
+ it('typeahead searches from the top when no treeitem has focus', () => {
284
+ const nodes: Node[] = [
285
+ {id: 'apple', label: 'Apple', level: 1},
286
+ {id: 'banana', label: 'Banana', level: 1},
287
+ {id: 'avocado', label: 'Avocado', level: 1},
288
+ ];
289
+ render(<Tree collapsed={nodes} />);
290
+ const tree = screen.getByRole('tree');
291
+
292
+ // No .focus() call: with no current item, the first match must win.
293
+ fireEvent.keyDown(tree, {key: 'a'});
294
+ expect(screen.getByTestId('apple')).toHaveFocus();
295
+ });
296
+
297
+ it('typeahead keeps focus on an item that still matches the refined buffer', () => {
298
+ const nodes: Node[] = [
299
+ {id: 'apple', label: 'Apple', level: 1},
300
+ {id: 'banana', label: 'Banana', level: 1},
301
+ {id: 'apricot', label: 'Apricot', level: 1},
302
+ ];
303
+ render(<Tree collapsed={nodes} />);
304
+ const tree = screen.getByRole('tree');
305
+ screen.getByTestId('apple').focus();
306
+
307
+ fireEvent.keyDown(tree, {key: 'a'});
308
+ expect(screen.getByTestId('apricot')).toHaveFocus();
309
+
310
+ // "ap" refines the search; Apricot still matches, so focus holds.
311
+ fireEvent.keyDown(tree, {key: 'p'});
312
+ expect(screen.getByTestId('apricot')).toHaveFocus();
313
+ });
264
314
  });
@@ -377,14 +377,25 @@ export function useTreeFocus<T extends HTMLElement = HTMLElement>(
377
377
  if (state.timer != null) {
378
378
  clearTimeout(state.timer);
379
379
  }
380
- state.buffer += e.key.toLowerCase();
380
+ // "aa" would match nothing; a repeat means "next match" (useTypeahead).
381
+ const char = e.key.toLowerCase();
382
+ const isRepeatSameChar =
383
+ state.buffer.length > 0 &&
384
+ state.buffer.split('').every(c => c === char);
385
+ state.buffer = isRepeatSameChar ? char : state.buffer + char;
381
386
  state.timer = setTimeout(() => {
382
387
  typeaheadRef.current.buffer = '';
383
388
  }, typeaheadResetMs) as unknown as number;
384
389
 
385
390
  const query = state.buffer;
386
- const start = currentIndex < 0 ? 0 : currentIndex;
387
- const ordered = [...items.slice(start + 1), ...items.slice(0, start + 1)];
391
+ // A longer query is refining, so the current item may still be the match.
392
+ const hasCurrent = currentIndex >= 0;
393
+ const start = hasCurrent ? currentIndex : 0;
394
+ const offset = hasCurrent && query.length === 1 ? 1 : 0;
395
+ const ordered = [
396
+ ...items.slice(start + offset),
397
+ ...items.slice(0, start + offset),
398
+ ];
388
399
  const match = ordered.find(
389
400
  item =>
390
401
  !itemDisabled(item) &&
@@ -106,4 +106,154 @@ describe('useTypeahead', () => {
106
106
  expect(api.onKeyDown(key(' '))).toBe(false);
107
107
  expect(onMatch).not.toHaveBeenCalled();
108
108
  });
109
+
110
+ it('extends a live buffer with a space', () => {
111
+ const onMatch = vi.fn();
112
+ const {result} = renderHook(() =>
113
+ useTypeahead({
114
+ getItemLabels: () => ['Newark', 'New York'],
115
+ onMatch,
116
+ }),
117
+ );
118
+ for (const k of 'new ') {
119
+ result.current.onKeyDown(key(k));
120
+ }
121
+ result.current.onKeyDown(key('y'));
122
+ // "new y" reaches New York; a space that ended the buffer instead would
123
+ // leave "newy", which matches nothing.
124
+ expect(onMatch).toHaveBeenLastCalledWith(1);
125
+ });
126
+
127
+ it('ignores a Space chorded with ctrl or meta even mid-buffer', () => {
128
+ const onMatch = vi.fn();
129
+ const {result} = renderHook(() =>
130
+ useTypeahead({
131
+ getItemLabels: () => ['Melon', 'New York'],
132
+ onMatch,
133
+ }),
134
+ );
135
+ expect(result.current.onKeyDown(key('n'))).toBe(true);
136
+ expect(onMatch).toHaveBeenLastCalledWith(1);
137
+
138
+ // A chord is not typing. Consuming it would append a raw space and leave
139
+ // the buffer as "n ", poisoning every keystroke until the reset window.
140
+ expect(
141
+ result.current.onKeyDown(
142
+ new KeyboardEvent('keydown', {key: ' ', ctrlKey: true}),
143
+ ),
144
+ ).toBe(false);
145
+ expect(
146
+ result.current.onKeyDown(
147
+ new KeyboardEvent('keydown', {key: ' ', metaKey: true}),
148
+ ),
149
+ ).toBe(false);
150
+
151
+ result.current.onKeyDown(key('e'));
152
+ expect(onMatch).toHaveBeenCalledTimes(2);
153
+ expect(onMatch).toHaveBeenLastCalledWith(1);
154
+ });
155
+
156
+ it('starts a fresh single-character search after the current item', () => {
157
+ const {onMatch, api} = setup({current: 0}); // Apple is current
158
+ api.onKeyDown(key('a'));
159
+ // A single-character search begins at current + 1, so pressing the current
160
+ // item's own initial advances instead of re-matching it into a dead key.
161
+ expect(onMatch).toHaveBeenCalledWith(1); // Apricot
162
+ });
163
+
164
+ it('searches from the top when there is no current item', () => {
165
+ // The last item also matches, so this distinguishes "from the top" from
166
+ // wrapping backwards off -1 and hitting the bottom of the list first.
167
+ const onMatch = vi.fn();
168
+ const {result} = renderHook(() =>
169
+ useTypeahead({
170
+ getItemLabels: () => ['Apple', 'Berry', 'Avocado'],
171
+ onMatch,
172
+ getCurrentIndex: () => -1,
173
+ }),
174
+ );
175
+ result.current.onKeyDown(key('a'));
176
+ expect(onMatch).toHaveBeenCalledWith(0); // Apple, not Avocado
177
+ });
178
+
179
+ it('keeps the current item in range once the buffer is multi-character', () => {
180
+ const {onMatch, api} = setup({current: 0});
181
+ api.onKeyDown(key('a')); // single char: advances to Apricot
182
+ expect(onMatch).toHaveBeenLastCalledWith(1);
183
+ api.onKeyDown(key('p')); // "ap" refines, so Apple is a candidate again
184
+ expect(onMatch).toHaveBeenLastCalledWith(0);
185
+ });
186
+
187
+ it('skips unmatchable label slots without shifting the reported index', () => {
188
+ // Callers may pass null/empty entries to keep the index mapping 1:1 with
189
+ // their own items — menus pass el.textContent, which can be null.
190
+ const onMatch = vi.fn();
191
+ const {result} = renderHook(() =>
192
+ useTypeahead({
193
+ getItemLabels: () => [null, '', ' ', undefined, 'Apple'],
194
+ onMatch,
195
+ }),
196
+ );
197
+ result.current.onKeyDown(key('a'));
198
+ expect(onMatch).toHaveBeenCalledWith(4);
199
+ });
200
+
201
+ it('treats any negative or stale current index safely', () => {
202
+ // A sentinel other than -1, and an index left over from a longer list:
203
+ // neither may search backwards or report an index outside the list.
204
+ const fromBelowZero = vi.fn();
205
+ const {result: below} = renderHook(() =>
206
+ useTypeahead({
207
+ getItemLabels: () => LABELS,
208
+ onMatch: fromBelowZero,
209
+ getCurrentIndex: () => -5,
210
+ }),
211
+ );
212
+ below.current.onKeyDown(key('a'));
213
+ expect(fromBelowZero).toHaveBeenCalledWith(0); // Apple, from the top
214
+
215
+ const fromStale = vi.fn();
216
+ const {result: stale} = renderHook(() =>
217
+ useTypeahead({
218
+ getItemLabels: () => LABELS,
219
+ onMatch: fromStale,
220
+ getCurrentIndex: () => 7,
221
+ }),
222
+ );
223
+ stale.current.onKeyDown(key('b'));
224
+ expect(fromStale).toHaveBeenCalledWith(2); // Banana, in range
225
+ });
226
+
227
+ it('wraps onto the only item when the list has one entry', () => {
228
+ // "Advance past the current item" is about cycling; with one item the
229
+ // cycle is itself. The caller decides whether that is a no-op.
230
+ const onMatch = vi.fn();
231
+ const {result} = renderHook(() =>
232
+ useTypeahead({
233
+ getItemLabels: () => ['Apple'],
234
+ onMatch,
235
+ getCurrentIndex: () => 0,
236
+ }),
237
+ );
238
+ expect(result.current.onKeyDown(key('a'))).toBe(true);
239
+ expect(onMatch).toHaveBeenCalledWith(0);
240
+ });
241
+
242
+ it('matches characters composed with Option/Alt', () => {
243
+ const onMatch = vi.fn();
244
+ const {result} = renderHook(() =>
245
+ useTypeahead({
246
+ getItemLabels: () => ['Ångström', 'Berlin'],
247
+ onMatch,
248
+ getCurrentIndex: () => -1,
249
+ }),
250
+ );
251
+ // Option+a on macOS emits a printable 'å' with altKey set. Excluding it
252
+ // makes accented labels untypeable; real chords still carry ctrl/meta.
253
+ const handled = result.current.onKeyDown(
254
+ new KeyboardEvent('keydown', {key: 'å', altKey: true}),
255
+ );
256
+ expect(handled).toBe(true);
257
+ expect(onMatch).toHaveBeenCalledWith(0);
258
+ });
109
259
  });
@@ -32,8 +32,9 @@ export interface UseTypeaheadOptions {
32
32
 
33
33
  /**
34
34
  * The index to start searching from — typically the currently focused item,
35
- * so repeated presses of the same letter cycle through matches. Defaults to
36
- * -1 (search from the top).
35
+ * so repeated presses of the same letter cycle through matches. Any negative
36
+ * value (the default) means nothing is current: the search runs from the top
37
+ * and never skips the first item.
37
38
  */
38
39
  getCurrentIndex?: () => number;
39
40
 
@@ -67,9 +68,12 @@ export interface UseTypeaheadReturn {
67
68
  function isPrintableCharacter(e: React.KeyboardEvent | KeyboardEvent): boolean {
68
69
  return (
69
70
  e.key.length === 1 &&
71
+ // Alt alone is not excluded: Option+letter on macOS composes a printable
72
+ // character (Option+a → "å"), and dropping those makes accented labels
73
+ // untypeable. Real chords still carry ctrl or meta — including AltGr,
74
+ // which sets ctrlKey on Windows and Linux.
70
75
  !e.ctrlKey &&
71
76
  !e.metaKey &&
72
- !e.altKey &&
73
77
  // A lone space is used for activation in menus, not typeahead-from-empty.
74
78
  e.key !== ' '
75
79
  );
@@ -133,8 +137,14 @@ export function useTypeahead(options: UseTypeaheadOptions): UseTypeaheadReturn {
133
137
  const onKeyDown = useCallback(
134
138
  (e: React.KeyboardEvent | KeyboardEvent): boolean => {
135
139
  // A bare Space with no active buffer is not typeahead (menus activate on
136
- // Space); once the user is mid-typing, Space extends the query.
137
- const isSpaceMidType = e.key === ' ' && bufferRef.current.length > 0;
140
+ // Space); once the user is mid-typing, Space extends the query. Chorded
141
+ // with ctrl or meta it is neither that is an OS or IME shortcut, and
142
+ // consuming it would append a raw space that poisons the whole buffer.
143
+ const isSpaceMidType =
144
+ e.key === ' ' &&
145
+ !e.ctrlKey &&
146
+ !e.metaKey &&
147
+ bufferRef.current.length > 0;
138
148
  if (!isPrintableCharacter(e) && !isSpaceMidType) {
139
149
  return false;
140
150
  }
@@ -154,11 +164,16 @@ export function useTypeahead(options: UseTypeaheadOptions): UseTypeaheadReturn {
154
164
  bufferRef.current = nextBuffer;
155
165
  scheduleReset();
156
166
 
157
- const start = getCurrentIndex?.() ?? -1;
167
+ const current = getCurrentIndex?.() ?? -1;
158
168
  const count = labels.length;
159
- // When repeating a single char, start the search AFTER the current item
160
- // so we advance; otherwise include the current item.
161
- const offset = isRepeatSameChar ? 1 : 0;
169
+ const hasCurrent = current >= 0;
170
+ const start = hasCurrent ? current : 0;
171
+ // A single-character search starts AFTER the current item, so pressing a
172
+ // letter walks to the next item beginning with it instead of re-matching
173
+ // the one already selected (native <select> and APG getIndexByLetter).
174
+ // Once the buffer is longer it is refining a match, so the current item
175
+ // stays in range. With nothing current there is nothing to advance past.
176
+ const offset = hasCurrent && nextBuffer.length === 1 ? 1 : 0;
162
177
 
163
178
  for (let i = 0; i < count; i++) {
164
179
  const index = (start + offset + i + count) % count;
@@ -40,7 +40,7 @@ export const docs = {
40
40
  {
41
41
  guidance: true,
42
42
  description:
43
- 'Set the `dir` attribute on `<html>` (or a wrapping element) yourself the provider does not set it. Astryx components mirror layout and directional icons from the DOM `dir`, so an RTL locale won\'t visually mirror without it. Use `getLocaleDirection(locale)` to derive the value for both the provider and the DOM.',
43
+ 'Set the `dir` attribute on `<html>` (or a wrapping element) yourself; the provider does not set it. Astryx components mirror layout and directional icons from the DOM `dir`, so an RTL locale won\'t visually mirror without it. Use `getLocaleDirection(locale)` to derive the value for both the provider and the DOM.',
44
44
  },
45
45
  {
46
46
  guidance: false,
@@ -76,7 +76,7 @@ export const docs = {
76
76
  type: "'ltr' | 'rtl'",
77
77
  required: false,
78
78
  description:
79
- 'Explicit text-direction override for the context. When omitted, direction is derived from `locale` via `Intl.Locale.getTextInfo()`. This sets the direction Astryx reads, but it does NOT set the DOM `dir` attribute you must set `dir` on `<html>` (or a wrapping element) yourself, since Astryx components mirror layout and directional icons from the DOM `dir`, not from this prop. Set both to the same value and keep them in sync.',
79
+ 'Explicit text-direction override for the context. When omitted, direction is derived from `locale` via `Intl.Locale.getTextInfo()`. This sets the direction Astryx reads, but it does NOT set the DOM `dir` attribute; you must set `dir` on `<html>` (or a wrapping element) yourself, since Astryx components mirror layout and directional icons from the DOM `dir`, not from this prop. Set both to the same value and keep them in sync.',
80
80
  },
81
81
  {
82
82
  name: 'children',
@@ -163,6 +163,11 @@ export const docsDense = {
163
163
  description:
164
164
  'Use real BCP 47 tags like `fr`, `pt-BR`, or `ar`; regional locales fall back to base language before English.',
165
165
  },
166
+ {
167
+ guidance: true,
168
+ description:
169
+ 'Set `dir` on `<html>` (or a wrapper) yourself; the provider does not. Astryx mirrors layout and directional icons from the DOM `dir`, so an RTL locale will not mirror without it. Use `getLocaleDirection(locale)` for both the provider and the DOM.',
170
+ },
166
171
  {
167
172
  guidance: false,
168
173
  description:
package/src/index.ts CHANGED
@@ -68,6 +68,7 @@ export * from './SelectableCard';
68
68
  export * from './Selector';
69
69
  export * from './MultiSelector';
70
70
  export * from './Icon';
71
+ export * from './Indicator';
71
72
  export * from './InputGroup';
72
73
  export * from './Item';
73
74
  export * from './Text';
@@ -31,6 +31,7 @@
31
31
  */
32
32
 
33
33
  import type {IconRegistry} from '../Icon/globalIconRegistry';
34
+ import type {IndicatorRegistry} from '../Indicator/types';
34
35
  import type {TypographyConfig, FontWeight} from './types';
35
36
  import {
36
37
  resolveOnMedia,
@@ -280,6 +281,19 @@ export interface DefineThemeInput {
280
281
  components?: ComponentStyleMap;
281
282
  /** Icon registry — maps semantic icon names to React nodes */
282
283
  icons?: Partial<IconRegistry>;
284
+ /**
285
+ * Indicator overrides — replaces the components that draw stateful control
286
+ * visuals with the theme's own, by name.
287
+ *
288
+ * Replacement is by indicator name, not per call site, so a single entry
289
+ * reaches every component that draws that indicator: mapping `check` to
290
+ * `RadioIndicator` gives radio visuals to every single-selection mark in the
291
+ * app.
292
+ *
293
+ * Each entry is checked against its indicator's family, so a replacement
294
+ * must accept the states that family passes.
295
+ */
296
+ indicators?: IndicatorRegistry;
283
297
  /**
284
298
  * Default syntax highlighting theme for code components.
285
299
  * Sets --color-syntax-* tokens at the theme root. Can be overridden
@@ -329,6 +343,8 @@ export interface DefinedTheme {
329
343
  components?: ComponentStyleMap;
330
344
  /** Icon registry */
331
345
  icons?: Partial<IconRegistry>;
346
+ /** Indicator overrides for stateful control visuals, keyed by name */
347
+ indicators?: IndicatorRegistry;
332
348
  /** Whether this theme has been pre-compiled by theme build CLI */
333
349
  __built?: true;
334
350
  /**
@@ -622,11 +638,19 @@ export function defineTheme(input: DefineThemeInput): DefinedTheme {
622
638
  ? {...base.icons, ...input.icons}
623
639
  : (input.icons ?? base?.icons);
624
640
 
641
+ // Indicator overrides merge by name, like icons: a child theme replacing one
642
+ // indicator keeps the ones its base replaced.
643
+ const indicators =
644
+ input.indicators && base?.indicators
645
+ ? {...base.indicators, ...input.indicators}
646
+ : (input.indicators ?? base?.indicators);
647
+
625
648
  const theme: DefinedTheme = {
626
649
  name: input.name,
627
650
  tokens,
628
651
  components,
629
652
  icons,
653
+ indicators,
630
654
  __inputTokens: input.tokens,
631
655
  __onDark,
632
656
  __onLight,
@@ -186,7 +186,6 @@ function discoverComponents(): ComponentInfo[] {
186
186
  // ---------------------------------------------------------------------------
187
187
 
188
188
  const DIR_TO_REGISTRY_KEY: Record<string, string> = {
189
- Avatar: 'avatar',
190
189
  Banner: 'banner',
191
190
  Button: 'button',
192
191
  Card: 'card',
@@ -196,8 +195,10 @@ const DIR_TO_REGISTRY_KEY: Record<string, string> = {
196
195
  Field: 'field',
197
196
  HoverCard: 'hovercard',
198
197
  Popover: 'popover',
198
+ ProgressBar: 'progressbar-mark',
199
199
  Section: 'section',
200
200
  SegmentedControl: 'segmented-control',
201
+ TextArea: 'textarea',
201
202
  };
202
203
 
203
204
  /**
@@ -340,4 +341,23 @@ describe('getDerivedVars', () => {
340
341
  it('returns empty for unregistered property', () => {
341
342
  expect(getDerivedVars('card', 'color')).toEqual([]);
342
343
  });
344
+
345
+ it('marks textarea paddingInline as replacing the source property', () => {
346
+ const result = getDerivedVars('textarea', 'paddingInline');
347
+ expect(result).toHaveLength(1);
348
+ expect(result[0].vars).toEqual(['--_textarea-inline-padding']);
349
+ expect(result[0].replaces).toBe(true);
350
+ });
351
+
352
+ it('marks progressbar-mark width and height as replacing the source property', () => {
353
+ for (const [property, varName] of [
354
+ ['width', '--_progressbar-mark-width'],
355
+ ['height', '--_progressbar-mark-height'],
356
+ ]) {
357
+ const result = getDerivedVars('progressbar-mark', property);
358
+ expect(result).toHaveLength(1);
359
+ expect(result[0].vars).toEqual([varName]);
360
+ expect(result[0].replaces).toBe(true);
361
+ }
362
+ });
343
363
  });
@@ -25,6 +25,14 @@ export interface DerivedVarEntry {
25
25
  vars?: string[];
26
26
  /** Named expansion strategy. 'container' expands padding to container tokens. */
27
27
  expand?: 'container';
28
+ /**
29
+ * Emit only the internal `vars`, dropping the source property from the rule.
30
+ * Use when the class-carrying element must NOT receive the standard property
31
+ * itself — the value is consumed by a child through the var instead. Without
32
+ * this, the property is emitted alongside the var (correct when the same
33
+ * element both reads the var and applies the property, e.g. Chat/DropdownMenu).
34
+ */
35
+ replaces?: boolean;
28
36
  }
29
37
 
30
38
  /**
@@ -35,12 +43,6 @@ export interface DerivedVarEntry {
35
43
  * entries share the same property.
36
44
  */
37
45
  export const derivedVarRegistry: Record<string, DerivedVarEntry[]> = {
38
- avatar: [
39
- {property: 'fontSize', vars: ['--_avatar-fallback-font-size']},
40
- {property: 'fontWeight', vars: ['--_avatar-fallback-font-weight']},
41
- {property: 'color', vars: ['--_avatar-fallback-color']},
42
- {property: 'backgroundColor', vars: ['--_avatar-fallback-background']},
43
- ],
44
46
  banner: [{property: 'borderRadius', vars: ['--_banner-radius']}],
45
47
  button: [{property: 'borderRadius', vars: ['--_button-radius']}],
46
48
  card: [
@@ -62,11 +64,22 @@ export const derivedVarRegistry: Record<string, DerivedVarEntry[]> = {
62
64
  field: [{property: 'borderRadius', vars: ['--_field-radius']}],
63
65
  hovercard: [{property: 'borderRadius', vars: ['--_hovercard-radius']}],
64
66
  popover: [{property: 'borderRadius', vars: ['--_popover-radius']}],
67
+ 'progressbar-mark': [
68
+ {property: 'width', vars: ['--_progressbar-mark-width'], replaces: true},
69
+ {property: 'height', vars: ['--_progressbar-mark-height'], replaces: true},
70
+ ],
65
71
  section: [{property: 'padding', expand: 'container'}],
66
72
  'segmented-control': [
67
73
  {property: 'borderRadius', vars: ['--_segmented-control-radius']},
68
74
  {property: 'padding', vars: ['--_segmented-control-padding']},
69
75
  ],
76
+ textarea: [
77
+ {
78
+ property: 'paddingInline',
79
+ vars: ['--_textarea-inline-padding'],
80
+ replaces: true,
81
+ },
82
+ ],
70
83
  };
71
84
 
72
85
  /**
@@ -100,6 +100,20 @@ describe('expandColorScale', () => {
100
100
  standard['--color-text-primary'],
101
101
  );
102
102
  });
103
+
104
+ it('contrast high strengthens both border tokens', () => {
105
+ const standard = expandColorScale({
106
+ accent: '#0064E0',
107
+ contrast: 'standard',
108
+ });
109
+ const high = expandColorScale({accent: '#0064E0', contrast: 'high'});
110
+ // Both the subtle hairline and the emphasized border shift under high
111
+ // contrast, so structural boundaries stay perceivable.
112
+ expect(high['--color-border']).not.toBe(standard['--color-border']);
113
+ expect(high['--color-border-emphasized']).not.toBe(
114
+ standard['--color-border-emphasized'],
115
+ );
116
+ });
103
117
  });
104
118
 
105
119
  describe('expandColorScale — neutral-only themes (#2279)', () => {