@astryxdesign/core 0.3.0 → 0.4.0-canary.3112e99

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 (594) 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 +3 -0
  16. package/dist/Banner/Banner.d.ts.map +1 -1
  17. package/dist/Banner/Banner.js +32 -17
  18. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  19. package/dist/Breadcrumbs/BreadcrumbItem.js +21 -8
  20. package/dist/Breadcrumbs/index.d.ts +2 -1
  21. package/dist/Breadcrumbs/index.d.ts.map +1 -1
  22. package/dist/Breadcrumbs/index.js +1 -0
  23. package/dist/Button/Button.d.ts.map +1 -1
  24. package/dist/Button/Button.js +12 -16
  25. package/dist/Calendar/Calendar.d.ts.map +1 -1
  26. package/dist/Calendar/Calendar.js +2 -1
  27. package/dist/Calendar/styles.d.ts +0 -2
  28. package/dist/Calendar/styles.d.ts.map +1 -1
  29. package/dist/Calendar/styles.js +0 -2
  30. package/dist/Carousel/Carousel.d.ts.map +1 -1
  31. package/dist/Carousel/Carousel.js +8 -12
  32. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  33. package/dist/Chat/ChatDictationButton.js +2 -1
  34. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  35. package/dist/Chat/ChatLayoutScrollButton.js +3 -1
  36. package/dist/Chat/ChatSendButton.d.ts.map +1 -1
  37. package/dist/Chat/ChatSendButton.js +4 -3
  38. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  39. package/dist/Chat/ChatToolCalls.js +17 -17
  40. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  41. package/dist/Chat/useTriggerMenu.js +2 -6
  42. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  43. package/dist/CheckboxInput/CheckboxInput.js +36 -125
  44. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  45. package/dist/ClickableCard/ClickableCard.js +3 -11
  46. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  47. package/dist/CodeBlock/CodeBlock.js +84 -67
  48. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  49. package/dist/Collapsible/Collapsible.js +35 -15
  50. package/dist/CommandPalette/CommandPaletteItem.d.ts +1 -1
  51. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  52. package/dist/CommandPalette/CommandPaletteItem.js +6 -3
  53. package/dist/CommandPalette/CommandPaletteList.js +1 -1
  54. package/dist/ComplexSelector/ComplexSelector.d.ts +1 -1
  55. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  56. package/dist/ComplexSelector/ComplexSelector.js +32 -31
  57. package/dist/ContextMenu/ContextMenu.d.ts +2 -2
  58. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  59. package/dist/ContextMenu/ContextMenu.js +1 -1
  60. package/dist/ContextMenu/index.d.ts +2 -1
  61. package/dist/ContextMenu/index.d.ts.map +1 -1
  62. package/dist/ContextMenu/index.js +1 -0
  63. package/dist/DateInput/DateInput.d.ts +9 -2
  64. package/dist/DateInput/DateInput.d.ts.map +1 -1
  65. package/dist/DateInput/DateInput.js +47 -33
  66. package/dist/DateRangeInput/DateRangeInput.d.ts +9 -2
  67. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  68. package/dist/DateRangeInput/DateRangeInput.js +55 -32
  69. package/dist/DateTimeInput/DateTimeInput.d.ts +9 -1
  70. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  71. package/dist/DateTimeInput/DateTimeInput.js +54 -30
  72. package/dist/Dialog/Dialog.d.ts.map +1 -1
  73. package/dist/Dialog/Dialog.js +4 -4
  74. package/dist/DropdownMenu/DropdownMenu.d.ts +33 -8
  75. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  76. package/dist/DropdownMenu/DropdownMenu.js +62 -20
  77. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +8 -11
  78. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  79. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +32 -31
  80. package/dist/DropdownMenu/DropdownMenuDivider.d.ts +25 -0
  81. package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -0
  82. package/dist/DropdownMenu/DropdownMenuDivider.js +66 -0
  83. package/dist/DropdownMenu/DropdownMenuItem.d.ts +15 -2
  84. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  85. package/dist/DropdownMenu/DropdownMenuItem.js +20 -8
  86. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +1 -1
  87. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
  88. package/dist/DropdownMenu/DropdownMenuRadioGroup.js +10 -3
  89. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +5 -5
  90. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
  91. package/dist/DropdownMenu/DropdownMenuRadioItem.js +24 -67
  92. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  93. package/dist/DropdownMenu/DropdownMenuSubMenu.js +2 -8
  94. package/dist/DropdownMenu/index.d.ts +2 -1
  95. package/dist/DropdownMenu/index.d.ts.map +1 -1
  96. package/dist/DropdownMenu/index.js +4 -0
  97. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  98. package/dist/DropdownMenu/renderDropdownItems.js +35 -27
  99. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  100. package/dist/EmptyState/EmptyState.js +9 -5
  101. package/dist/Field/InputClearButton.d.ts +16 -3
  102. package/dist/Field/InputClearButton.d.ts.map +1 -1
  103. package/dist/Field/InputClearButton.js +15 -4
  104. package/dist/FileInput/FileInput.d.ts.map +1 -1
  105. package/dist/FileInput/FileInput.js +21 -10
  106. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  107. package/dist/HoverCard/useHoverCard.js +3 -19
  108. package/dist/Indicator/CheckIndicator.d.ts +25 -0
  109. package/dist/Indicator/CheckIndicator.d.ts.map +1 -0
  110. package/dist/Indicator/CheckIndicator.js +155 -0
  111. package/dist/Indicator/CheckboxIndicator.d.ts +22 -0
  112. package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -0
  113. package/dist/Indicator/CheckboxIndicator.js +173 -0
  114. package/dist/Indicator/RadioIndicator.d.ts +23 -0
  115. package/dist/Indicator/RadioIndicator.d.ts.map +1 -0
  116. package/dist/Indicator/RadioIndicator.js +137 -0
  117. package/dist/Indicator/index.d.ts +24 -0
  118. package/dist/Indicator/index.d.ts.map +1 -0
  119. package/dist/Indicator/index.js +26 -0
  120. package/dist/Indicator/indicator.markers.stylex.d.ts +16 -0
  121. package/dist/Indicator/indicator.markers.stylex.d.ts.map +1 -0
  122. package/dist/Indicator/indicator.markers.stylex.js +21 -0
  123. package/dist/Indicator/indicatorRegistry.d.ts +71 -0
  124. package/dist/Indicator/indicatorRegistry.d.ts.map +1 -0
  125. package/dist/Indicator/indicatorRegistry.js +93 -0
  126. package/dist/Indicator/types.d.ts +177 -0
  127. package/dist/Indicator/types.d.ts.map +1 -0
  128. package/dist/Indicator/types.js +1 -0
  129. package/dist/Indicator/useIndicator.d.ts +18 -0
  130. package/dist/Indicator/useIndicator.d.ts.map +1 -0
  131. package/dist/Indicator/useIndicator.js +30 -0
  132. package/dist/Item/Item.d.ts.map +1 -1
  133. package/dist/Item/Item.js +4 -8
  134. package/dist/Layer/useLayer.d.ts +14 -0
  135. package/dist/Layer/useLayer.d.ts.map +1 -1
  136. package/dist/Layer/useLayer.js +25 -2
  137. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  138. package/dist/Lightbox/Lightbox.js +80 -103
  139. package/dist/Link/Link.d.ts.map +1 -1
  140. package/dist/Link/Link.js +4 -5
  141. package/dist/MobileNav/MobileNav.js +8 -8
  142. package/dist/MoreMenu/MoreMenu.d.ts +14 -1
  143. package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
  144. package/dist/MoreMenu/MoreMenu.js +4 -0
  145. package/dist/MultiSelector/MultiSelector.d.ts +8 -1
  146. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  147. package/dist/MultiSelector/MultiSelector.js +119 -95
  148. package/dist/NumberInput/NumberInput.d.ts +27 -1
  149. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  150. package/dist/NumberInput/NumberInput.js +230 -42
  151. package/dist/Outline/Outline.d.ts.map +1 -1
  152. package/dist/Outline/Outline.js +2 -3
  153. package/dist/Pagination/Pagination.d.ts.map +1 -1
  154. package/dist/Pagination/Pagination.js +61 -63
  155. package/dist/Popover/Popover.d.ts.map +1 -1
  156. package/dist/Popover/Popover.js +6 -8
  157. package/dist/Popover/usePopover.d.ts +15 -0
  158. package/dist/Popover/usePopover.d.ts.map +1 -1
  159. package/dist/Popover/usePopover.js +14 -2
  160. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  161. package/dist/PowerSearch/PowerSearch.js +1 -1
  162. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  163. package/dist/PowerSearch/PowerSearchEditPopover.js +7 -0
  164. package/dist/ProgressBar/ProgressBar.d.ts +11 -4
  165. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  166. package/dist/ProgressBar/ProgressBar.js +95 -6
  167. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  168. package/dist/RadioList/RadioListItem.js +29 -84
  169. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  170. package/dist/Resizable/ResizeHandle.js +2 -3
  171. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  172. package/dist/SegmentedControl/SegmentedControlItem.js +2 -3
  173. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  174. package/dist/SelectableCard/SelectableCard.js +3 -11
  175. package/dist/Selector/Selector.d.ts +12 -1
  176. package/dist/Selector/Selector.d.ts.map +1 -1
  177. package/dist/Selector/Selector.js +222 -111
  178. package/dist/Selector/hooks.d.ts +18 -6
  179. package/dist/Selector/hooks.d.ts.map +1 -1
  180. package/dist/Selector/hooks.js +57 -40
  181. package/dist/SideNav/SideNav.d.ts.map +1 -1
  182. package/dist/SideNav/SideNav.js +1 -1
  183. package/dist/SideNav/SideNavCollapseButton.d.ts +1 -1
  184. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  185. package/dist/SideNav/SideNavCollapseButton.js +29 -14
  186. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  187. package/dist/SideNav/SideNavHeading.js +56 -20
  188. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  189. package/dist/SideNav/SideNavItem.js +28 -22
  190. package/dist/Slider/Slider.d.ts.map +1 -1
  191. package/dist/Slider/Slider.js +2 -6
  192. package/dist/Stack/stack.stylex.d.ts +3 -3
  193. package/dist/Switch/Switch.d.ts.map +1 -1
  194. package/dist/Switch/Switch.js +14 -13
  195. package/dist/TabList/Tab.d.ts.map +1 -1
  196. package/dist/TabList/Tab.js +2 -3
  197. package/dist/TabList/TabMenu.d.ts.map +1 -1
  198. package/dist/TabList/TabMenu.js +41 -19
  199. package/dist/Table/BaseTable.d.ts.map +1 -1
  200. package/dist/Table/BaseTable.js +8 -0
  201. package/dist/Table/TableRow.d.ts +1 -1
  202. package/dist/Table/TableRow.d.ts.map +1 -1
  203. package/dist/Table/TableRow.js +4 -2
  204. package/dist/Table/index.d.ts +1 -1
  205. package/dist/Table/index.d.ts.map +1 -1
  206. package/dist/Table/index.js +1 -1
  207. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts +1 -1
  208. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  209. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +22 -18
  210. package/dist/Table/plugins/rowExpansion/index.d.ts +1 -1
  211. package/dist/Table/plugins/rowExpansion/index.d.ts.map +1 -1
  212. package/dist/Table/plugins/rowExpansion/index.js +1 -1
  213. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts +41 -76
  214. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  215. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +94 -345
  216. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  217. package/dist/Table/plugins/sortable/useTableSortable.js +23 -4
  218. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
  219. package/dist/Table/plugins/tree/useTableTreeData.js +29 -33
  220. package/dist/Table/tableContextMenu.d.ts.map +1 -1
  221. package/dist/Table/tableContextMenu.js +2 -1
  222. package/dist/Table/types.d.ts +11 -0
  223. package/dist/Table/types.d.ts.map +1 -1
  224. package/dist/TextArea/TextArea.d.ts +10 -1
  225. package/dist/TextArea/TextArea.d.ts.map +1 -1
  226. package/dist/TextArea/TextArea.js +16 -6
  227. package/dist/TextInput/TextInput.d.ts +10 -1
  228. package/dist/TextInput/TextInput.d.ts.map +1 -1
  229. package/dist/TextInput/TextInput.js +13 -19
  230. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  231. package/dist/Thumbnail/Thumbnail.js +32 -26
  232. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  233. package/dist/TimeInput/TimeInput.js +14 -17
  234. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  235. package/dist/Timestamp/Timestamp.js +14 -3
  236. package/dist/Timestamp/TimestampHoverCard.d.ts.map +1 -1
  237. package/dist/Timestamp/TimestampHoverCard.js +16 -34
  238. package/dist/Timestamp/formatInstant.d.ts +11 -1
  239. package/dist/Timestamp/formatInstant.d.ts.map +1 -1
  240. package/dist/Timestamp/formatInstant.js +3 -1
  241. package/dist/Token/Token.d.ts.map +1 -1
  242. package/dist/Token/Token.js +20 -11
  243. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  244. package/dist/Tokenizer/Tokenizer.js +4 -2
  245. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  246. package/dist/Tooltip/useTooltip.js +3 -19
  247. package/dist/TopNav/TopNav.d.ts.map +1 -1
  248. package/dist/TopNav/TopNav.js +6 -5
  249. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  250. package/dist/TopNav/TopNavHeading.js +54 -16
  251. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  252. package/dist/TopNav/TopNavItem.js +3 -9
  253. package/dist/TopNav/TopNavMegaMenu.d.ts +7 -0
  254. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  255. package/dist/TopNav/TopNavMegaMenu.js +146 -60
  256. package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
  257. package/dist/TopNav/TopNavMegaMenuItem.js +22 -3
  258. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  259. package/dist/TopNav/TopNavMenu.js +83 -38
  260. package/dist/TreeList/TreeList.d.ts.map +1 -1
  261. package/dist/TreeList/TreeList.js +13 -0
  262. package/dist/TreeList/TreeListBranches.d.ts +1 -1
  263. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  264. package/dist/TreeList/TreeListBranches.js +14 -4
  265. package/dist/TreeList/TreeListItem.d.ts +11 -1
  266. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  267. package/dist/TreeList/TreeListItem.js +57 -30
  268. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  269. package/dist/Typeahead/BaseTypeahead.js +16 -8
  270. package/dist/astryx.css +84 -96
  271. package/dist/astryx.umd.js +56 -56
  272. package/dist/astryx.umd.js.map +4 -4
  273. package/dist/hooks/containerReveal.stylex.d.ts +61 -0
  274. package/dist/hooks/containerReveal.stylex.d.ts.map +1 -0
  275. package/dist/hooks/containerReveal.stylex.js +63 -0
  276. package/dist/hooks/index.d.ts +3 -0
  277. package/dist/hooks/index.d.ts.map +1 -1
  278. package/dist/hooks/index.js +2 -0
  279. package/dist/hooks/useClipboard.d.ts +59 -0
  280. package/dist/hooks/useClipboard.d.ts.map +1 -0
  281. package/dist/hooks/useClipboard.js +106 -0
  282. package/dist/hooks/useContainerReveal.d.ts +14 -8
  283. package/dist/hooks/useContainerReveal.d.ts.map +1 -1
  284. package/dist/hooks/useContainerReveal.js +28 -69
  285. package/dist/hooks/useIndicatorFocusRing.d.ts +28 -0
  286. package/dist/hooks/useIndicatorFocusRing.d.ts.map +1 -0
  287. package/dist/hooks/useIndicatorFocusRing.js +96 -0
  288. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  289. package/dist/hooks/useInputStatusIcon.js +17 -4
  290. package/dist/hooks/useLongPress.d.ts.map +1 -1
  291. package/dist/hooks/useLongPress.js +12 -2
  292. package/dist/hooks/useScrollLock.d.ts.map +1 -1
  293. package/dist/hooks/useScrollLock.js +33 -18
  294. package/dist/hooks/useStreamingText.d.ts +18 -3
  295. package/dist/hooks/useStreamingText.d.ts.map +1 -1
  296. package/dist/hooks/useStreamingText.js +47 -7
  297. package/dist/hooks/useTreeFocus.d.ts.map +1 -1
  298. package/dist/hooks/useTreeFocus.js +9 -3
  299. package/dist/hooks/useTypeahead.d.ts +3 -2
  300. package/dist/hooks/useTypeahead.d.ts.map +1 -1
  301. package/dist/hooks/useTypeahead.js +19 -7
  302. package/dist/index.d.ts +1 -0
  303. package/dist/index.d.ts.map +1 -1
  304. package/dist/index.js +1 -0
  305. package/dist/theme/defineTheme.d.ts +18 -2
  306. package/dist/theme/defineTheme.d.ts.map +1 -1
  307. package/dist/theme/defineTheme.js +10 -1
  308. package/dist/theme/derivedVarRegistry.d.ts +8 -0
  309. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  310. package/dist/theme/derivedVarRegistry.js +14 -13
  311. package/dist/theme/expandColorScale.d.ts +3 -1
  312. package/dist/theme/expandColorScale.d.ts.map +1 -1
  313. package/dist/theme/expandColorScale.js +17 -7
  314. package/dist/theme/expandTypeScale.d.ts.map +1 -1
  315. package/dist/theme/expandTypeScale.js +17 -0
  316. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  317. package/dist/theme/generateThemeRules.js +11 -0
  318. package/dist/theme/index.d.ts +2 -1
  319. package/dist/theme/index.d.ts.map +1 -1
  320. package/dist/theme/index.js +1 -1
  321. package/dist/theme/tokens.stylex.d.ts +12 -0
  322. package/dist/theme/tokens.stylex.d.ts.map +1 -1
  323. package/dist/theme/tokens.stylex.js +23 -0
  324. package/dist/utils/focusOutline.stylex.d.ts +122 -0
  325. package/dist/utils/focusOutline.stylex.d.ts.map +1 -0
  326. package/dist/utils/focusOutline.stylex.js +143 -0
  327. package/dist/utils/focusReturn.d.ts +24 -0
  328. package/dist/utils/focusReturn.d.ts.map +1 -0
  329. package/dist/utils/focusReturn.js +29 -0
  330. package/dist/utils/index.d.ts +2 -0
  331. package/dist/utils/index.d.ts.map +1 -1
  332. package/dist/utils/index.js +6 -1
  333. package/dist/utils/mergeRefs.d.ts.map +1 -1
  334. package/dist/utils/mergeRefs.js +7 -5
  335. package/dist/utils/rtlStyles.d.ts +2 -2
  336. package/dist/utils/rtlStyles.js +2 -2
  337. package/dist/utils/themeProps.d.ts +20 -1
  338. package/dist/utils/themeProps.d.ts.map +1 -1
  339. package/dist/utils/themeProps.js +8 -2
  340. package/locales/en.json +52 -0
  341. package/locales/pseudo.json +39 -0
  342. package/package.json +8 -3
  343. package/src/AlertDialog/AlertDialog.doc.mjs +16 -3
  344. package/src/AlertDialog/AlertDialog.test.tsx +208 -1
  345. package/src/AlertDialog/AlertDialog.tsx +21 -3
  346. package/src/AppShell/AppShell.doc.mjs +11 -0
  347. package/src/AppShell/AppShell.test.tsx +139 -0
  348. package/src/AppShell/AppShell.tsx +32 -22
  349. package/src/AspectRatio/AspectRatio.doc.mjs +27 -3
  350. package/src/AspectRatio/AspectRatio.test.tsx +30 -0
  351. package/src/AspectRatio/AspectRatio.tsx +13 -0
  352. package/src/Avatar/Avatar.doc.mjs +2 -13
  353. package/src/Avatar/Avatar.test.tsx +33 -8
  354. package/src/Avatar/Avatar.tsx +52 -50
  355. package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -9
  356. package/src/Banner/Banner.test.tsx +58 -1
  357. package/src/Banner/Banner.tsx +33 -11
  358. package/src/Breadcrumbs/BreadcrumbItem.tsx +16 -6
  359. package/src/Breadcrumbs/index.ts +5 -1
  360. package/src/Button/Button.doc.mjs +2 -2
  361. package/src/Button/Button.tsx +22 -41
  362. package/src/Calendar/Calendar.tsx +2 -1
  363. package/src/Calendar/styles.ts +0 -8
  364. package/src/Carousel/Carousel.doc.mjs +3 -0
  365. package/src/Carousel/Carousel.tsx +10 -6
  366. package/src/Chat/Chat.doc.mjs +2 -0
  367. package/src/Chat/ChatDictationButton.tsx +7 -1
  368. package/src/Chat/ChatLayoutScrollButton.test.tsx +22 -0
  369. package/src/Chat/ChatLayoutScrollButton.tsx +8 -1
  370. package/src/Chat/ChatSendButton.tsx +2 -3
  371. package/src/Chat/ChatToolCalls.test.tsx +35 -0
  372. package/src/Chat/ChatToolCalls.tsx +36 -16
  373. package/src/Chat/useTriggerMenu.tsx +2 -5
  374. package/src/CheckboxInput/CheckboxInput.doc.mjs +4 -2
  375. package/src/CheckboxInput/CheckboxInput.test.tsx +120 -0
  376. package/src/CheckboxInput/CheckboxInput.tsx +45 -202
  377. package/src/ClickableCard/ClickableCard.tsx +6 -11
  378. package/src/CodeBlock/CodeBlock.doc.mjs +3 -0
  379. package/src/CodeBlock/CodeBlock.test.tsx +91 -0
  380. package/src/CodeBlock/CodeBlock.tsx +57 -80
  381. package/src/Collapsible/Collapsible.tsx +22 -18
  382. package/src/CommandPalette/CommandPalette.test.tsx +29 -0
  383. package/src/CommandPalette/CommandPaletteItem.tsx +7 -4
  384. package/src/CommandPalette/CommandPaletteList.tsx +2 -2
  385. package/src/ComplexSelector/ComplexSelector.doc.mjs +42 -0
  386. package/src/ComplexSelector/ComplexSelector.test.tsx +73 -0
  387. package/src/ComplexSelector/ComplexSelector.tsx +41 -28
  388. package/src/ContextMenu/ContextMenu.doc.mjs +1 -1
  389. package/src/ContextMenu/ContextMenu.test.tsx +41 -0
  390. package/src/ContextMenu/ContextMenu.tsx +3 -3
  391. package/src/ContextMenu/index.ts +5 -1
  392. package/src/DateInput/DateInput.doc.mjs +18 -3
  393. package/src/DateInput/DateInput.test.tsx +112 -11
  394. package/src/DateInput/DateInput.tsx +44 -25
  395. package/src/DateRangeInput/DateRangeInput.doc.mjs +10 -2
  396. package/src/DateRangeInput/DateRangeInput.test.tsx +110 -14
  397. package/src/DateRangeInput/DateRangeInput.tsx +27 -21
  398. package/src/DateTimeInput/DateTimeInput.doc.mjs +16 -2
  399. package/src/DateTimeInput/DateTimeInput.test.tsx +154 -1
  400. package/src/DateTimeInput/DateTimeInput.tsx +58 -17
  401. package/src/Dialog/Dialog.tsx +2 -9
  402. package/src/DropdownMenu/DropdownMenu.doc.mjs +5 -9
  403. package/src/DropdownMenu/DropdownMenu.test.tsx +534 -4
  404. package/src/DropdownMenu/DropdownMenu.tsx +113 -52
  405. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +27 -30
  406. package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +34 -0
  407. package/src/DropdownMenu/DropdownMenuDivider.tsx +81 -0
  408. package/src/DropdownMenu/DropdownMenuItem.doc.mjs +39 -4
  409. package/src/DropdownMenu/DropdownMenuItem.tsx +48 -10
  410. package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +2 -1
  411. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +30 -76
  412. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +25 -19
  413. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +44 -14
  414. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +1 -5
  415. package/src/DropdownMenu/index.ts +8 -1
  416. package/src/DropdownMenu/renderDropdownItems.tsx +33 -34
  417. package/src/EmptyState/EmptyState.doc.mjs +4 -0
  418. package/src/EmptyState/EmptyState.test.tsx +62 -0
  419. package/src/EmptyState/EmptyState.tsx +16 -6
  420. package/src/Field/Field.doc.mjs +1 -0
  421. package/src/Field/InputClearButton.test.tsx +117 -0
  422. package/src/Field/InputClearButton.tsx +30 -3
  423. package/src/FileInput/FileInput.tsx +21 -8
  424. package/src/HoverCard/useHoverCard.tsx +2 -23
  425. package/src/Icon/Icon.doc.mjs +2 -1
  426. package/src/Indicator/CheckIndicator.tsx +174 -0
  427. package/src/Indicator/CheckboxIndicator.tsx +257 -0
  428. package/src/Indicator/Indicator.doc.mjs +344 -0
  429. package/src/Indicator/Indicator.test.tsx +393 -0
  430. package/src/Indicator/RadioIndicator.tsx +206 -0
  431. package/src/Indicator/index.ts +46 -0
  432. package/src/Indicator/indicator.markers.stylex.ts +19 -0
  433. package/src/Indicator/indicatorRegistry.test.tsx +106 -0
  434. package/src/Indicator/indicatorRegistry.ts +120 -0
  435. package/src/Indicator/types.ts +197 -0
  436. package/src/Indicator/useIndicator.ts +39 -0
  437. package/src/Item/Item.doc.mjs +1 -1
  438. package/src/Item/Item.tsx +7 -14
  439. package/src/Layer/useLayer.doc.mjs +2 -2
  440. package/src/Layer/useLayer.test.tsx +85 -0
  441. package/src/Layer/useLayer.tsx +39 -1
  442. package/src/Lightbox/Lightbox.doc.mjs +1 -1
  443. package/src/Lightbox/Lightbox.tsx +45 -49
  444. package/src/Link/Link.tsx +4 -11
  445. package/src/MobileNav/MobileNav.tsx +3 -3
  446. package/src/MoreMenu/MoreMenu.doc.mjs +31 -0
  447. package/src/MoreMenu/MoreMenu.test.tsx +68 -1
  448. package/src/MoreMenu/MoreMenu.tsx +19 -0
  449. package/src/MultiSelector/MultiSelector.doc.mjs +16 -1
  450. package/src/MultiSelector/MultiSelector.test.tsx +259 -16
  451. package/src/MultiSelector/MultiSelector.tsx +144 -121
  452. package/src/NumberInput/NumberInput.doc.mjs +265 -50
  453. package/src/NumberInput/NumberInput.test.tsx +726 -21
  454. package/src/NumberInput/NumberInput.tsx +353 -59
  455. package/src/Outline/Outline.tsx +2 -5
  456. package/src/Pagination/Pagination.doc.mjs +1 -1
  457. package/src/Pagination/Pagination.test.tsx +22 -7
  458. package/src/Pagination/Pagination.tsx +38 -35
  459. package/src/Popover/Popover.doc.mjs +2 -0
  460. package/src/Popover/Popover.tsx +6 -7
  461. package/src/Popover/usePopover.tsx +37 -4
  462. package/src/PowerSearch/PowerSearch.tsx +1 -1
  463. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +42 -0
  464. package/src/PowerSearch/PowerSearchEditPopover.tsx +7 -0
  465. package/src/ProgressBar/ProgressBar.doc.mjs +27 -5
  466. package/src/ProgressBar/ProgressBar.test.tsx +224 -1
  467. package/src/ProgressBar/ProgressBar.tsx +126 -30
  468. package/src/RadioList/RadioList.doc.mjs +4 -2
  469. package/src/RadioList/RadioListItem.tsx +31 -130
  470. package/src/Resizable/ResizeHandle.tsx +2 -9
  471. package/src/SegmentedControl/SegmentedControlItem.tsx +2 -9
  472. package/src/SelectableCard/SelectableCard.tsx +4 -9
  473. package/src/Selector/Selector.doc.mjs +15 -4
  474. package/src/Selector/Selector.test.tsx +1234 -98
  475. package/src/Selector/Selector.tsx +254 -126
  476. package/src/Selector/hooks.ts +77 -50
  477. package/src/SideNav/SideNav.test.tsx +46 -0
  478. package/src/SideNav/SideNav.tsx +1 -0
  479. package/src/SideNav/SideNavCollapseButton.tsx +15 -10
  480. package/src/SideNav/SideNavHeading.tsx +48 -10
  481. package/src/SideNav/SideNavItem.tsx +20 -12
  482. package/src/Slider/Slider.tsx +2 -11
  483. package/src/Stack/Stack.doc.mjs +110 -0
  484. package/src/StatusDot/StatusDot.doc.mjs +12 -3
  485. package/src/Switch/Switch.test.tsx +32 -0
  486. package/src/Switch/Switch.tsx +18 -10
  487. package/src/TabList/Tab.tsx +2 -9
  488. package/src/TabList/TabMenu.tsx +14 -21
  489. package/src/Table/BaseTable.tsx +11 -0
  490. package/src/Table/Table.test.tsx +31 -0
  491. package/src/Table/TableRow.tsx +6 -0
  492. package/src/Table/index.ts +1 -4
  493. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +30 -14
  494. package/src/Table/plugins/rowExpansion/index.ts +1 -4
  495. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +125 -175
  496. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +115 -458
  497. package/src/Table/plugins/sortable/useTableSortable.tsx +2 -6
  498. package/src/Table/plugins/tree/useTableTreeData.tsx +43 -24
  499. package/src/Table/tableContextMenu.test.tsx +27 -0
  500. package/src/Table/tableContextMenu.tsx +1 -0
  501. package/src/Table/types.ts +11 -0
  502. package/src/Table/useTableGroupedRows.doc.mjs +1 -1
  503. package/src/Table/useTableRowExpansion.doc.mjs +43 -66
  504. package/src/TextArea/TextArea.doc.mjs +42 -2
  505. package/src/TextArea/TextArea.test.tsx +219 -0
  506. package/src/TextArea/TextArea.tsx +34 -7
  507. package/src/TextInput/TextInput.doc.mjs +18 -2
  508. package/src/TextInput/TextInput.test.tsx +189 -0
  509. package/src/TextInput/TextInput.tsx +25 -31
  510. package/src/Thumbnail/Thumbnail.tsx +2 -9
  511. package/src/TimeInput/TimeInput.doc.mjs +2 -2
  512. package/src/TimeInput/TimeInput.test.tsx +84 -3
  513. package/src/TimeInput/TimeInput.tsx +26 -28
  514. package/src/Timestamp/Timestamp.doc.mjs +4 -3
  515. package/src/Timestamp/Timestamp.test.tsx +91 -14
  516. package/src/Timestamp/Timestamp.tsx +14 -2
  517. package/src/Timestamp/TimestampHoverCard.tsx +13 -35
  518. package/src/Timestamp/formatInstant.ts +16 -1
  519. package/src/Token/Token.tsx +5 -28
  520. package/src/Tokenizer/Tokenizer.doc.mjs +2 -2
  521. package/src/Tokenizer/Tokenizer.test.tsx +45 -3
  522. package/src/Tokenizer/Tokenizer.tsx +10 -2
  523. package/src/Tooltip/useTooltip.tsx +2 -23
  524. package/src/TopNav/TopNav.tsx +1 -3
  525. package/src/TopNav/TopNavHeading.tsx +42 -13
  526. package/src/TopNav/TopNavItem.tsx +3 -21
  527. package/src/TopNav/TopNavMegaMenu.doc.mjs +5 -0
  528. package/src/TopNav/TopNavMegaMenu.test.tsx +433 -33
  529. package/src/TopNav/TopNavMegaMenu.tsx +118 -53
  530. package/src/TopNav/TopNavMegaMenuItem.tsx +2 -9
  531. package/src/TopNav/TopNavMenu.tsx +34 -37
  532. package/src/TreeList/TreeList.doc.mjs +5 -0
  533. package/src/TreeList/TreeList.test.tsx +208 -8
  534. package/src/TreeList/TreeList.tsx +22 -0
  535. package/src/TreeList/TreeListBranches.tsx +25 -2
  536. package/src/TreeList/TreeListItem.tsx +89 -52
  537. package/src/Typeahead/BaseTypeahead.tsx +19 -6
  538. package/src/Typeahead/Typeahead.doc.mjs +1 -0
  539. package/src/Typeahead/Typeahead.test.tsx +76 -3
  540. package/src/hooks/containerReveal.stylex.ts +102 -0
  541. package/src/hooks/index.ts +4 -0
  542. package/src/hooks/useClipboard.doc.mjs +87 -0
  543. package/src/hooks/useClipboard.test.tsx +151 -0
  544. package/src/hooks/useClipboard.ts +135 -0
  545. package/src/hooks/useContainerReveal.doc.mjs +7 -7
  546. package/src/hooks/useContainerReveal.test.tsx +42 -62
  547. package/src/hooks/useContainerReveal.ts +36 -91
  548. package/src/hooks/useImageMode.test.ts +1 -1
  549. package/src/hooks/useIndicatorFocusRing.tsx +120 -0
  550. package/src/hooks/useInputStatusIcon.tsx +8 -8
  551. package/src/hooks/useLongPress.test.tsx +18 -0
  552. package/src/hooks/useLongPress.ts +12 -2
  553. package/src/hooks/useScrollLock.test.ts +72 -0
  554. package/src/hooks/useScrollLock.ts +51 -19
  555. package/src/hooks/useStreamingText.test.ts +46 -1
  556. package/src/hooks/useStreamingText.ts +49 -7
  557. package/src/hooks/useTreeFocus.test.tsx +50 -0
  558. package/src/hooks/useTreeFocus.ts +14 -3
  559. package/src/hooks/useTypeahead.test.tsx +150 -0
  560. package/src/hooks/useTypeahead.ts +24 -9
  561. package/src/i18n/InternationalizationProvider.doc.mjs +7 -2
  562. package/src/index.ts +1 -0
  563. package/src/theme/defineTheme.ts +27 -0
  564. package/src/theme/derivedVarRegistry.test.ts +21 -1
  565. package/src/theme/derivedVarRegistry.ts +19 -6
  566. package/src/theme/expandColorScale.test.ts +14 -0
  567. package/src/theme/expandColorScale.ts +24 -9
  568. package/src/theme/expandTypeScale.test.ts +43 -0
  569. package/src/theme/expandTypeScale.ts +17 -0
  570. package/src/theme/generateThemeRules.test.ts +78 -20
  571. package/src/theme/generateThemeRules.ts +11 -0
  572. package/src/theme/index.ts +15 -0
  573. package/src/theme/tokens.stylex.ts +18 -0
  574. package/src/theme/tokens.test.ts +37 -0
  575. package/src/utils/focusOutline.stylex.ts +177 -0
  576. package/src/utils/focusReturn.test.ts +42 -0
  577. package/src/utils/focusReturn.ts +31 -0
  578. package/src/utils/index.ts +5 -0
  579. package/src/utils/mergeRefs.test.ts +48 -0
  580. package/src/utils/mergeRefs.ts +12 -6
  581. package/src/utils/rtlStyles.ts +2 -2
  582. package/src/utils/themeProps.ts +26 -4
  583. package/dist/CheckboxInput/checkbox.markers.stylex.d.ts +0 -9
  584. package/dist/CheckboxInput/checkbox.markers.stylex.d.ts.map +0 -1
  585. package/dist/CheckboxInput/checkbox.markers.stylex.js +0 -14
  586. package/dist/RadioList/radio.markers.stylex.d.ts +0 -7
  587. package/dist/RadioList/radio.markers.stylex.d.ts.map +0 -1
  588. package/dist/RadioList/radio.markers.stylex.js +0 -12
  589. package/dist/hooks/containerReveal.pool.stylex.d.ts +0 -303
  590. package/dist/hooks/containerReveal.pool.stylex.d.ts.map +0 -1
  591. package/dist/hooks/containerReveal.pool.stylex.js +0 -292
  592. package/src/CheckboxInput/checkbox.markers.stylex.ts +0 -12
  593. package/src/RadioList/radio.markers.stylex.ts +0 -10
  594. package/src/hooks/containerReveal.pool.stylex.ts +0 -571
@@ -17,6 +17,19 @@
17
17
 
18
18
  import {useEffect} from 'react';
19
19
 
20
+ interface ScrollLockSnapshot {
21
+ scrollX: number;
22
+ scrollY: number;
23
+ overflow: string;
24
+ position: string;
25
+ top: string;
26
+ left: string;
27
+ right: string;
28
+ }
29
+
30
+ let lockCount = 0;
31
+ let originalBodyState: ScrollLockSnapshot | null = null;
32
+
20
33
  /**
21
34
  * Locks body scroll when `isLocked` is true.
22
35
  *
@@ -36,28 +49,47 @@ export function useScrollLock(isLocked: boolean): void {
36
49
  return;
37
50
  }
38
51
 
39
- const scrollX = window.scrollX;
40
- const scrollY = window.scrollY;
41
52
  const {body} = document;
42
- const prevOverflow = body.style.overflow;
43
- const prevPosition = body.style.position;
44
- const prevTop = body.style.top;
45
- const prevLeft = body.style.left;
46
- const prevRight = body.style.right;
47
-
48
- body.style.overflow = 'hidden';
49
- body.style.position = 'fixed';
50
- body.style.top = `-${scrollY}px`;
51
- body.style.left = '0';
52
- body.style.right = '0';
53
+
54
+ if (lockCount === 0) {
55
+ const scrollX = window.scrollX;
56
+ const scrollY = window.scrollY;
57
+
58
+ originalBodyState = {
59
+ scrollX,
60
+ scrollY,
61
+ overflow: body.style.overflow,
62
+ position: body.style.position,
63
+ top: body.style.top,
64
+ left: body.style.left,
65
+ right: body.style.right,
66
+ };
67
+
68
+ body.style.overflow = 'hidden';
69
+ body.style.position = 'fixed';
70
+ body.style.top = `-${scrollY}px`;
71
+ body.style.left = '0';
72
+ body.style.right = '0';
73
+ }
74
+
75
+ lockCount += 1;
53
76
 
54
77
  return () => {
55
- body.style.overflow = prevOverflow;
56
- body.style.position = prevPosition;
57
- body.style.top = prevTop;
58
- body.style.left = prevLeft;
59
- body.style.right = prevRight;
60
- window.scrollTo(scrollX, scrollY);
78
+ lockCount -= 1;
79
+
80
+ if (lockCount !== 0 || originalBodyState == null) {
81
+ return;
82
+ }
83
+
84
+ const state = originalBodyState;
85
+ originalBodyState = null;
86
+
87
+ body.style.overflow = state.overflow;
88
+ body.style.position = state.position;
89
+ body.style.top = state.top;
90
+ body.style.left = state.left;
91
+ body.style.right = state.right;
92
+ window.scrollTo(state.scrollX, state.scrollY);
61
93
  };
62
94
  }, [isLocked]);
63
95
  }
@@ -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,
@@ -41,6 +42,7 @@ import {
41
42
  colorDefaults,
42
43
  spacingDefaults,
43
44
  sizeDefaults,
45
+ focusDefaults,
44
46
  radiusDefaults,
45
47
  shadowDefaults,
46
48
  durationDefaults,
@@ -73,6 +75,7 @@ export type CoreTokenName =
73
75
  | keyof typeof colorDefaults
74
76
  | keyof typeof spacingDefaults
75
77
  | keyof typeof sizeDefaults
78
+ | keyof typeof focusDefaults
76
79
  | keyof typeof radiusDefaults
77
80
  | keyof typeof shadowDefaults
78
81
  | keyof typeof durationDefaults
@@ -280,6 +283,19 @@ export interface DefineThemeInput {
280
283
  components?: ComponentStyleMap;
281
284
  /** Icon registry — maps semantic icon names to React nodes */
282
285
  icons?: Partial<IconRegistry>;
286
+ /**
287
+ * Indicator overrides — replaces the components that draw stateful control
288
+ * visuals with the theme's own, by name.
289
+ *
290
+ * Replacement is by indicator name, not per call site, so a single entry
291
+ * reaches every component that draws that indicator: mapping `check` to
292
+ * `RadioIndicator` gives radio visuals to every single-selection mark in the
293
+ * app.
294
+ *
295
+ * Each entry is checked against its indicator's family, so a replacement
296
+ * must accept the states that family passes.
297
+ */
298
+ indicators?: IndicatorRegistry;
283
299
  /**
284
300
  * Default syntax highlighting theme for code components.
285
301
  * Sets --color-syntax-* tokens at the theme root. Can be overridden
@@ -329,6 +345,8 @@ export interface DefinedTheme {
329
345
  components?: ComponentStyleMap;
330
346
  /** Icon registry */
331
347
  icons?: Partial<IconRegistry>;
348
+ /** Indicator overrides for stateful control visuals, keyed by name */
349
+ indicators?: IndicatorRegistry;
332
350
  /** Whether this theme has been pre-compiled by theme build CLI */
333
351
  __built?: true;
334
352
  /**
@@ -361,6 +379,7 @@ export const tokenDefaults: Record<string, string> = {
361
379
  ...colorDefaults,
362
380
  ...spacingDefaults,
363
381
  ...sizeDefaults,
382
+ ...focusDefaults,
364
383
  ...radiusDefaults,
365
384
  ...shadowDefaults,
366
385
  ...durationDefaults,
@@ -622,11 +641,19 @@ export function defineTheme(input: DefineThemeInput): DefinedTheme {
622
641
  ? {...base.icons, ...input.icons}
623
642
  : (input.icons ?? base?.icons);
624
643
 
644
+ // Indicator overrides merge by name, like icons: a child theme replacing one
645
+ // indicator keeps the ones its base replaced.
646
+ const indicators =
647
+ input.indicators && base?.indicators
648
+ ? {...base.indicators, ...input.indicators}
649
+ : (input.indicators ?? base?.indicators);
650
+
625
651
  const theme: DefinedTheme = {
626
652
  name: input.name,
627
653
  tokens,
628
654
  components,
629
655
  icons,
656
+ indicators,
630
657
  __inputTokens: input.tokens,
631
658
  __onDark,
632
659
  __onLight,