@astryxdesign/core 0.1.7 → 0.1.8-canary.0c4d8d7

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 (428) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +30 -19
  3. package/dist/AppShell/AppShell.d.ts.map +1 -1
  4. package/dist/AppShell/AppShell.js +6 -2
  5. package/dist/AppShell/AppShellMobileContext.d.ts +8 -0
  6. package/dist/AppShell/AppShellMobileContext.d.ts.map +1 -1
  7. package/dist/Avatar/Avatar.d.ts +10 -5
  8. package/dist/Avatar/Avatar.d.ts.map +1 -1
  9. package/dist/Avatar/Avatar.js +12 -8
  10. package/dist/Avatar/AvatarSizeContext.d.ts +1 -1
  11. package/dist/Avatar/AvatarSizeContext.js +1 -1
  12. package/dist/Avatar/AvatarStatusDot.d.ts +2 -2
  13. package/dist/Avatar/AvatarStatusDot.js +2 -2
  14. package/dist/AvatarGroup/AvatarGroup.d.ts +2 -2
  15. package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
  16. package/dist/AvatarGroup/AvatarGroup.js +2 -2
  17. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
  18. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  19. package/dist/AvatarGroup/AvatarGroupOverflow.js +16 -14
  20. package/dist/Banner/Banner.d.ts +9 -1
  21. package/dist/Banner/Banner.d.ts.map +1 -1
  22. package/dist/Banner/Banner.js +31 -2
  23. package/dist/Button/Button.d.ts +8 -2
  24. package/dist/Button/Button.d.ts.map +1 -1
  25. package/dist/Button/Button.js +31 -2
  26. package/dist/ButtonGroup/ButtonGroup.d.ts +9 -1
  27. package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
  28. package/dist/ButtonGroup/ButtonGroup.js +30 -1
  29. package/dist/Card/Card.d.ts +8 -2
  30. package/dist/Card/Card.d.ts.map +1 -1
  31. package/dist/Card/Card.js +28 -2
  32. package/dist/Carousel/Carousel.d.ts +6 -2
  33. package/dist/Carousel/Carousel.d.ts.map +1 -1
  34. package/dist/Carousel/Carousel.js +55 -5
  35. package/dist/Chat/ChatComposer.d.ts +8 -0
  36. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  37. package/dist/Chat/ChatComposer.js +31 -6
  38. package/dist/Chat/ChatComposerDrawer.d.ts +2 -0
  39. package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
  40. package/dist/Chat/ChatComposerDrawer.js +11 -1
  41. package/dist/Chat/ChatComposerInput.d.ts +20 -3
  42. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  43. package/dist/Chat/ChatComposerInput.js +19 -3
  44. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  45. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  46. package/dist/Chat/ChatMessage.d.ts +1 -1
  47. package/dist/Chat/ChatMessage.d.ts.map +1 -1
  48. package/dist/Chat/ChatMessage.js +1 -1
  49. package/dist/Chat/ChatMessageMetadata.d.ts.map +1 -1
  50. package/dist/Chat/ChatSendButton.d.ts.map +1 -1
  51. package/dist/Chat/ChatToolCalls.d.ts +5 -1
  52. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  53. package/dist/Chat/ChatToolCalls.js +12 -3
  54. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  55. package/dist/Citation/Citation.d.ts.map +1 -1
  56. package/dist/Citation/Citation.js +10 -4
  57. package/dist/ClickableCard/ClickableCard.d.ts +9 -2
  58. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  59. package/dist/ClickableCard/ClickableCard.js +3 -0
  60. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  61. package/dist/CodeBlock/CodeBlock.js +8 -1
  62. package/dist/Collapsible/Collapsible.d.ts +11 -1
  63. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  64. package/dist/Collapsible/Collapsible.js +36 -4
  65. package/dist/CommandPalette/CommandPaletteInput.d.ts +7 -1
  66. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  67. package/dist/CommandPalette/CommandPaletteInput.js +6 -1
  68. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  69. package/dist/ContextMenu/ContextMenu.js +4 -3
  70. package/dist/ContextMenu/index.d.ts +3 -0
  71. package/dist/ContextMenu/index.d.ts.map +1 -1
  72. package/dist/ContextMenu/index.js +7 -1
  73. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  74. package/dist/DateTimeInput/DateTimeInput.js +10 -1
  75. package/dist/Dialog/Dialog.d.ts.map +1 -1
  76. package/dist/Dialog/Dialog.js +2 -1
  77. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  78. package/dist/DropdownMenu/DropdownMenu.js +8 -4
  79. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +88 -0
  80. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -0
  81. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +169 -0
  82. package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -1
  83. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  84. package/dist/DropdownMenu/DropdownMenuContext.js +18 -0
  85. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +68 -0
  86. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -0
  87. package/dist/DropdownMenu/DropdownMenuRadioGroup.js +69 -0
  88. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +69 -0
  89. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -0
  90. package/dist/DropdownMenu/DropdownMenuRadioItem.js +178 -0
  91. package/dist/DropdownMenu/index.d.ts +5 -1
  92. package/dist/DropdownMenu/index.d.ts.map +1 -1
  93. package/dist/DropdownMenu/index.js +10 -1
  94. package/dist/DropdownMenu/menuItemRoles.d.ts +13 -0
  95. package/dist/DropdownMenu/menuItemRoles.d.ts.map +1 -0
  96. package/dist/DropdownMenu/menuItemRoles.js +15 -0
  97. package/dist/FieldStatus/FieldStatus.d.ts +8 -0
  98. package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
  99. package/dist/FieldStatus/FieldStatus.js +22 -4
  100. package/dist/FileInput/FileInput.d.ts.map +1 -1
  101. package/dist/FileInput/FileInput.js +10 -2
  102. package/dist/HoverCard/HoverCard.d.ts +9 -1
  103. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  104. package/dist/HoverCard/HoverCard.js +2 -0
  105. package/dist/HoverCard/useHoverCard.d.ts +8 -0
  106. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  107. package/dist/HoverCard/useHoverCard.js +7 -2
  108. package/dist/Icon/Icon.d.ts +43 -13
  109. package/dist/Icon/Icon.d.ts.map +1 -1
  110. package/dist/Icon/Icon.js +62 -27
  111. package/dist/Layer/useLayer.d.ts +6 -0
  112. package/dist/Layer/useLayer.d.ts.map +1 -1
  113. package/dist/Layer/useLayer.js +2 -0
  114. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  115. package/dist/MobileNav/MobileNav.js +6 -1
  116. package/dist/MobileNav/MobileNavToggle.d.ts.map +1 -1
  117. package/dist/MobileNav/MobileNavToggle.js +4 -0
  118. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  119. package/dist/MultiSelector/MultiSelector.js +39 -12
  120. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  121. package/dist/MultiSelector/hooks.js +16 -0
  122. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  123. package/dist/Outline/Outline.d.ts +68 -3
  124. package/dist/Outline/Outline.d.ts.map +1 -1
  125. package/dist/Outline/Outline.js +128 -26
  126. package/dist/Outline/useScrollSpy.d.ts +27 -7
  127. package/dist/Outline/useScrollSpy.d.ts.map +1 -1
  128. package/dist/Outline/useScrollSpy.js +202 -70
  129. package/dist/OverflowList/OverflowList.d.ts +20 -3
  130. package/dist/OverflowList/OverflowList.d.ts.map +1 -1
  131. package/dist/OverflowList/OverflowList.js +29 -4
  132. package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
  133. package/dist/PowerSearch/usePowerSearchSource.d.ts.map +1 -1
  134. package/dist/SelectableCard/SelectableCard.d.ts +9 -2
  135. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  136. package/dist/SelectableCard/SelectableCard.js +14 -11
  137. package/dist/Selector/Selector.d.ts.map +1 -1
  138. package/dist/Selector/Selector.js +4 -2
  139. package/dist/Selector/hooks.d.ts.map +1 -1
  140. package/dist/Selector/hooks.js +16 -0
  141. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  142. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  143. package/dist/SideNav/SideNavHeading.js +21 -8
  144. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  145. package/dist/Slider/Slider.d.ts.map +1 -1
  146. package/dist/Slider/Slider.js +15 -1
  147. package/dist/TabList/Tab.d.ts.map +1 -1
  148. package/dist/TabList/Tab.js +2 -2
  149. package/dist/TabList/TabList.d.ts.map +1 -1
  150. package/dist/TabList/TabList.js +2 -0
  151. package/dist/TabList/TabMenu.d.ts.map +1 -1
  152. package/dist/TabList/TabMenu.js +1 -1
  153. package/dist/Table/Table.d.ts +1 -1
  154. package/dist/Table/Table.js +1 -1
  155. package/dist/Table/index.d.ts +4 -0
  156. package/dist/Table/index.d.ts.map +1 -1
  157. package/dist/Table/index.js +2 -0
  158. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  159. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  160. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +24 -2
  161. package/dist/Table/plugins/rowStatus/index.d.ts +3 -0
  162. package/dist/Table/plugins/rowStatus/index.d.ts.map +1 -0
  163. package/dist/Table/plugins/rowStatus/index.js +3 -0
  164. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts +60 -0
  165. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -0
  166. package/dist/Table/plugins/rowStatus/useTableRowStatus.js +145 -0
  167. package/dist/Table/plugins/selection/useTableSelection.d.ts +12 -0
  168. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  169. package/dist/Table/plugins/selection/useTableSelection.js +4 -1
  170. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  171. package/dist/Table/plugins/sortable/useTableSortable.js +18 -7
  172. package/dist/Table/plugins/tree/index.d.ts +5 -0
  173. package/dist/Table/plugins/tree/index.d.ts.map +1 -0
  174. package/dist/Table/plugins/tree/index.js +4 -0
  175. package/dist/Table/plugins/tree/useTableTreeData.d.ts +38 -0
  176. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -0
  177. package/dist/Table/plugins/tree/useTableTreeData.js +358 -0
  178. package/dist/Table/plugins/tree/useTableTreeState.d.ts +51 -0
  179. package/dist/Table/plugins/tree/useTableTreeState.d.ts.map +1 -0
  180. package/dist/Table/plugins/tree/useTableTreeState.js +175 -0
  181. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  182. package/dist/Table/useBaseTablePlugins.js +6 -3
  183. package/dist/Thumbnail/Thumbnail.d.ts +8 -1
  184. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  185. package/dist/Thumbnail/Thumbnail.js +46 -8
  186. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  187. package/dist/TimeInput/TimeInput.js +15 -4
  188. package/dist/Tokenizer/Tokenizer.d.ts +1 -1
  189. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  190. package/dist/Tokenizer/Tokenizer.js +16 -4
  191. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  192. package/dist/Toolbar/Toolbar.js +8 -1
  193. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  194. package/dist/TopNav/TopNavHeading.js +18 -8
  195. package/dist/TopNav/TopNavMegaMenu.js +10 -4
  196. package/dist/TopNav/TopNavMenu.d.ts +2 -1
  197. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  198. package/dist/TopNav/TopNavMenu.js +68 -6
  199. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  200. package/dist/TreeList/TreeListItem.js +5 -14
  201. package/dist/astryx.css +51 -16
  202. package/dist/astryx.umd.js +56 -56
  203. package/dist/astryx.umd.js.map +4 -4
  204. package/dist/authoring/doc.d.ts.map +1 -1
  205. package/dist/hooks/computeOverflow.d.ts +47 -0
  206. package/dist/hooks/computeOverflow.d.ts.map +1 -0
  207. package/dist/hooks/computeOverflow.js +213 -0
  208. package/dist/hooks/useOverflow.d.ts +19 -0
  209. package/dist/hooks/useOverflow.d.ts.map +1 -1
  210. package/dist/hooks/useOverflow.js +37 -25
  211. package/dist/theme/Theme.js +1 -1
  212. package/dist/theme/defineTheme.d.ts +1 -1
  213. package/dist/theme/defineTheme.js +1 -1
  214. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  215. package/dist/theme/generateThemeRules.js +51 -0
  216. package/dist/theme/hct.d.ts +0 -1
  217. package/dist/theme/hct.d.ts.map +1 -1
  218. package/dist/theme/hct.js +0 -3
  219. package/dist/utils/types.d.ts +13 -0
  220. package/dist/utils/types.d.ts.map +1 -1
  221. package/docs.mjs +32 -461
  222. package/locales/en.json +212 -172
  223. package/locales/fr-FR.json +10 -0
  224. package/locales/pseudo.json +30 -0
  225. package/package.json +3 -1
  226. package/scripts/postinstall.mjs +93 -0
  227. package/src/AppShell/AppShell.tsx +6 -0
  228. package/src/AppShell/AppShellMobileContext.tsx +8 -0
  229. package/src/AppShell/useAppShellMobile.doc.mjs +8 -0
  230. package/src/Avatar/Avatar.doc.mjs +6 -6
  231. package/src/Avatar/Avatar.tsx +16 -11
  232. package/src/Avatar/AvatarSizeContext.ts +1 -1
  233. package/src/Avatar/AvatarStatusDot.tsx +2 -2
  234. package/src/AvatarGroup/AvatarGroup.doc.mjs +1 -1
  235. package/src/AvatarGroup/AvatarGroup.test.tsx +17 -3
  236. package/src/AvatarGroup/AvatarGroup.tsx +3 -3
  237. package/src/AvatarGroup/AvatarGroupOverflow.doc.mjs +4 -4
  238. package/src/AvatarGroup/AvatarGroupOverflow.tsx +21 -5
  239. package/src/Banner/Banner.doc.mjs +9 -0
  240. package/src/Banner/Banner.test.tsx +30 -0
  241. package/src/Banner/Banner.tsx +47 -3
  242. package/src/Button/Button.doc.mjs +9 -0
  243. package/src/Button/Button.test.tsx +49 -0
  244. package/src/Button/Button.tsx +25 -1
  245. package/src/ButtonGroup/ButtonGroup.doc.mjs +3 -0
  246. package/src/ButtonGroup/ButtonGroup.test.tsx +37 -0
  247. package/src/ButtonGroup/ButtonGroup.tsx +32 -0
  248. package/src/Card/Card.doc.mjs +9 -0
  249. package/src/Card/Card.test.tsx +21 -3
  250. package/src/Card/Card.tsx +35 -2
  251. package/src/Carousel/Carousel.doc.mjs +1 -0
  252. package/src/Carousel/Carousel.test.tsx +149 -1
  253. package/src/Carousel/Carousel.tsx +62 -5
  254. package/src/Chat/Chat.doc.mjs +1 -1
  255. package/src/Chat/ChatComposer.doc.mjs +9 -0
  256. package/src/Chat/ChatComposer.test.tsx +35 -0
  257. package/src/Chat/ChatComposer.tsx +58 -9
  258. package/src/Chat/ChatComposerDrawer.test.tsx +61 -0
  259. package/src/Chat/ChatComposerDrawer.tsx +11 -1
  260. package/src/Chat/ChatComposerInput.doc.mjs +7 -0
  261. package/src/Chat/ChatComposerInput.test.tsx +87 -0
  262. package/src/Chat/ChatComposerInput.tsx +35 -3
  263. package/src/Chat/ChatLayout.tsx +3 -1
  264. package/src/Chat/ChatLayoutScrollButton.tsx +3 -1
  265. package/src/Chat/ChatMessage.tsx +4 -2
  266. package/src/Chat/ChatMessageMetadata.tsx +1 -2
  267. package/src/Chat/ChatSendButton.tsx +5 -1
  268. package/src/Chat/ChatToolCalls.test.tsx +64 -0
  269. package/src/Chat/ChatToolCalls.tsx +13 -1
  270. package/src/Chat/useTriggerMenu.tsx +3 -1
  271. package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
  272. package/src/CheckboxInput/CheckboxInput.test.tsx +32 -1
  273. package/src/CheckboxList/CheckboxList.test.tsx +9 -1
  274. package/src/Citation/Citation.test.tsx +51 -0
  275. package/src/Citation/Citation.tsx +8 -2
  276. package/src/ClickableCard/ClickableCard.doc.mjs +2 -0
  277. package/src/ClickableCard/ClickableCard.test.tsx +34 -0
  278. package/src/ClickableCard/ClickableCard.tsx +11 -1
  279. package/src/CodeBlock/CodeBlock.test.tsx +56 -0
  280. package/src/CodeBlock/CodeBlock.tsx +9 -1
  281. package/src/Collapsible/Collapsible.doc.mjs +7 -0
  282. package/src/Collapsible/Collapsible.test.tsx +80 -0
  283. package/src/Collapsible/Collapsible.tsx +58 -5
  284. package/src/CommandPalette/CommandPaletteInput.doc.mjs +26 -9
  285. package/src/CommandPalette/CommandPaletteInput.test.tsx +34 -0
  286. package/src/CommandPalette/CommandPaletteInput.tsx +11 -0
  287. package/src/ContextMenu/ContextMenu.test.tsx +75 -1
  288. package/src/ContextMenu/ContextMenu.tsx +10 -5
  289. package/src/ContextMenu/index.ts +15 -0
  290. package/src/DateTimeInput/DateTimeInput.test.tsx +35 -0
  291. package/src/DateTimeInput/DateTimeInput.tsx +14 -2
  292. package/src/Dialog/Dialog.tsx +12 -2
  293. package/src/DropdownMenu/DropdownMenu.doc.mjs +23 -0
  294. package/src/DropdownMenu/DropdownMenu.tsx +11 -6
  295. package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +95 -0
  296. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +236 -0
  297. package/src/DropdownMenu/DropdownMenuContext.tsx +29 -1
  298. package/src/DropdownMenu/DropdownMenuItem.doc.mjs +7 -1
  299. package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +80 -0
  300. package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +118 -0
  301. package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +80 -0
  302. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +232 -0
  303. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +163 -0
  304. package/src/DropdownMenu/index.ts +23 -3
  305. package/src/DropdownMenu/menuItemRoles.ts +22 -0
  306. package/src/Field/Field.doc.mjs +1 -1
  307. package/src/Field/Field.test.tsx +57 -12
  308. package/src/Field/FieldLabel.doc.mjs +1 -1
  309. package/src/FieldStatus/FieldStatus.doc.mjs +17 -5
  310. package/src/FieldStatus/FieldStatus.test.tsx +78 -40
  311. package/src/FieldStatus/FieldStatus.tsx +22 -4
  312. package/src/FileInput/FileInput.test.tsx +52 -3
  313. package/src/FileInput/FileInput.tsx +13 -2
  314. package/src/HoverCard/HoverCard.doc.mjs +13 -0
  315. package/src/HoverCard/HoverCard.test.tsx +21 -2
  316. package/src/HoverCard/HoverCard.tsx +11 -0
  317. package/src/HoverCard/useHoverCard.doc.mjs +2 -0
  318. package/src/HoverCard/useHoverCard.tsx +24 -2
  319. package/src/Icon/Icon.doc.mjs +12 -0
  320. package/src/Icon/Icon.test.tsx +142 -0
  321. package/src/Icon/Icon.tsx +98 -30
  322. package/src/IconButton/IconButton.doc.mjs +8 -0
  323. package/src/IconButton/IconButton.test.tsx +15 -18
  324. package/src/Item/Item.doc.mjs +1 -1
  325. package/src/Layer/useLayer.tsx +8 -0
  326. package/src/MobileNav/MobileNav.tsx +12 -1
  327. package/src/MobileNav/MobileNavToggle.test.tsx +116 -0
  328. package/src/MobileNav/MobileNavToggle.tsx +9 -1
  329. package/src/MultiSelector/MultiSelector.doc.mjs +4 -2
  330. package/src/MultiSelector/MultiSelector.test.tsx +162 -1
  331. package/src/MultiSelector/MultiSelector.tsx +55 -12
  332. package/src/MultiSelector/hooks.ts +17 -0
  333. package/src/NumberInput/NumberInput.doc.mjs +2 -2
  334. package/src/NumberInput/NumberInput.test.tsx +11 -7
  335. package/src/NumberInput/NumberInput.tsx +1 -2
  336. package/src/Outline/Outline.doc.mjs +123 -3
  337. package/src/Outline/Outline.test.tsx +735 -0
  338. package/src/Outline/Outline.tsx +183 -33
  339. package/src/Outline/useScrollSpy.ts +248 -72
  340. package/src/OverflowList/OverflowList.doc.mjs +39 -6
  341. package/src/OverflowList/OverflowList.test.tsx +111 -0
  342. package/src/OverflowList/OverflowList.tsx +48 -8
  343. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +67 -0
  344. package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
  345. package/src/PowerSearch/usePowerSearchSource.ts +1 -2
  346. package/src/SelectableCard/SelectableCard.doc.mjs +2 -0
  347. package/src/SelectableCard/SelectableCard.test.tsx +46 -4
  348. package/src/SelectableCard/SelectableCard.tsx +27 -14
  349. package/src/Selector/Selector.test.tsx +69 -0
  350. package/src/Selector/Selector.tsx +5 -1
  351. package/src/Selector/SelectorOption.doc.mjs +1 -1
  352. package/src/Selector/hooks.ts +17 -0
  353. package/src/SideNav/SideNav.test.tsx +119 -3
  354. package/src/SideNav/SideNavCollapseButton.tsx +6 -1
  355. package/src/SideNav/SideNavHeading.tsx +24 -8
  356. package/src/SideNav/SideNavItem.doc.mjs +2 -2
  357. package/src/SideNav/SideNavItem.tsx +5 -1
  358. package/src/Slider/Slider.test.tsx +62 -2
  359. package/src/Slider/Slider.tsx +15 -1
  360. package/src/Switch/Switch.doc.mjs +1 -1
  361. package/src/Switch/Switch.test.tsx +32 -1
  362. package/src/TabList/Tab.tsx +5 -1
  363. package/src/TabList/TabList.doc.mjs +3 -3
  364. package/src/TabList/TabList.test.tsx +66 -0
  365. package/src/TabList/TabList.tsx +8 -0
  366. package/src/TabList/TabMenu.tsx +4 -1
  367. package/src/Table/Table.doc.mjs +2 -0
  368. package/src/Table/Table.tsx +1 -1
  369. package/src/Table/index.ts +12 -0
  370. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +6 -2
  371. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +90 -1
  372. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +37 -5
  373. package/src/Table/plugins/rowStatus/index.ts +7 -0
  374. package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +138 -0
  375. package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +189 -0
  376. package/src/Table/plugins/selection/useTableSelection.test.tsx +16 -0
  377. package/src/Table/plugins/selection/useTableSelection.tsx +18 -1
  378. package/src/Table/plugins/sortable/useTableSortable.test.tsx +95 -9
  379. package/src/Table/plugins/sortable/useTableSortable.tsx +23 -7
  380. package/src/Table/plugins/tree/index.ts +12 -0
  381. package/src/Table/plugins/tree/useTableTreeData-perf.test.tsx +163 -0
  382. package/src/Table/plugins/tree/useTableTreeData.test.tsx +594 -0
  383. package/src/Table/plugins/tree/useTableTreeData.tsx +495 -0
  384. package/src/Table/plugins/tree/useTableTreeState.test.tsx +636 -0
  385. package/src/Table/plugins/tree/useTableTreeState.tsx +253 -0
  386. package/src/Table/useBaseTablePlugins.ts +6 -2
  387. package/src/Table/useTableRowStatus.doc.mjs +30 -0
  388. package/src/Table/useTableSelection.doc.mjs +65 -14
  389. package/src/Table/useTableTreeData.doc.mjs +75 -0
  390. package/src/Table/useTableTreeState.doc.mjs +98 -0
  391. package/src/TextArea/TextArea.doc.mjs +1 -1
  392. package/src/TextArea/TextArea.test.tsx +9 -1
  393. package/src/TextInput/TextInput.doc.mjs +1 -1
  394. package/src/Thumbnail/Thumbnail.doc.mjs +9 -0
  395. package/src/Thumbnail/Thumbnail.test.tsx +38 -8
  396. package/src/Thumbnail/Thumbnail.tsx +28 -1
  397. package/src/TimeInput/TimeInput.test.tsx +76 -1
  398. package/src/TimeInput/TimeInput.tsx +19 -6
  399. package/src/Tokenizer/Tokenizer.doc.mjs +1 -1
  400. package/src/Tokenizer/Tokenizer.test.tsx +137 -2
  401. package/src/Tokenizer/Tokenizer.tsx +16 -4
  402. package/src/Toolbar/Toolbar.tsx +10 -3
  403. package/src/TopNav/TopNav.test.tsx +80 -1
  404. package/src/TopNav/TopNavHeading.tsx +24 -8
  405. package/src/TopNav/TopNavMegaMenu.test.tsx +149 -1
  406. package/src/TopNav/TopNavMegaMenu.tsx +10 -4
  407. package/src/TopNav/TopNavMenu.test.tsx +118 -4
  408. package/src/TopNav/TopNavMenu.tsx +83 -5
  409. package/src/TreeList/TreeList.test.tsx +32 -1
  410. package/src/TreeList/TreeListItem.tsx +19 -10
  411. package/src/authoring/doc.ts +1 -3
  412. package/src/docs-script.test.mjs +18 -13
  413. package/src/hooks/computeOverflow.test.ts +422 -0
  414. package/src/hooks/computeOverflow.ts +290 -0
  415. package/src/hooks/useOverflow.test.ts +96 -4
  416. package/src/hooks/useOverflow.ts +77 -33
  417. package/src/postinstall.test.mjs +71 -0
  418. package/src/theme/Theme.doc.mjs +4 -4
  419. package/src/theme/Theme.tsx +1 -1
  420. package/src/theme/defineTheme.ts +1 -1
  421. package/src/theme/generateThemeRules.test.ts +65 -0
  422. package/src/theme/generateThemeRules.ts +56 -0
  423. package/src/theme/hct.ts +0 -4
  424. package/src/utils/types.ts +14 -0
  425. package/dist/TreeList/treeListItem.markers.stylex.d.ts +0 -16
  426. package/dist/TreeList/treeListItem.markers.stylex.d.ts.map +0 -1
  427. package/dist/TreeList/treeListItem.markers.stylex.js +0 -22
  428. package/src/TreeList/treeListItem.markers.stylex.ts +0 -20
@@ -10,11 +10,26 @@
10
10
  *
11
11
  * Drives the active outline item from scroll position. On each scroll
12
12
  * (rAF-throttled) it reads live heading positions and marks the last heading
13
- * whose top has passed its activation line (its own scroll-margin-top, i.e.
14
- * where it lands when navigated to). This is stable — it never compares stale
15
- * cached positions — so the indicator moves monotonically instead of jumping.
16
- * Defaults to the first item at the top and the last item at the bottom so
17
- * short final sections still activate.
13
+ * whose top has passed its activation line. This is stable — it never compares
14
+ * stale cached positions — so the indicator moves monotonically instead of
15
+ * jumping. Defaults to the first item at the top and the last item at the
16
+ * bottom so short final sections still activate.
17
+ *
18
+ * The activation line and the scroll landing are the same number, computed once
19
+ * in `getRestingTop`: scroll-root top + `offset` (a fixed header overlaying the
20
+ * root) + the heading's own `scroll-margin-top`. A heading therefore activates
21
+ * exactly where navigating to it puts it. `offset` and `scroll-margin-top`
22
+ * compose — the header, then the breathing room below it — they do not
23
+ * duplicate each other.
24
+ *
25
+ * It also owns the single navigation path (`scrollTo`), shared by click and
26
+ * keyboard activation: it announces the jump, suppresses scroll-spy for the
27
+ * duration of the programmatic scroll (so the indicator doesn't chase it), and
28
+ * resolves when the scroll settles — via `scrollend` where supported, or a
29
+ * timeout fallback where it is not.
30
+ *
31
+ * The scroll root is the `scrollContainerRef` element when provided, otherwise
32
+ * the nearest scrollable ancestor of the outline, otherwise the viewport.
18
33
  *
19
34
  * SYNC: When modified, update /packages/core/src/Outline/Outline.tsx
20
35
  */
@@ -22,6 +37,14 @@
22
37
  import {useCallback, useEffect, useRef, useState} from 'react';
23
38
  import type {OutlineItem} from './types';
24
39
 
40
+ /**
41
+ * How long to wait for a programmatic smooth scroll to settle when the
42
+ * `scrollend` event never arrives — either because the browser does not
43
+ * support it (Safari at time of writing) or because the target was already in
44
+ * position, so no scroll happened at all and no events were emitted.
45
+ */
46
+ const SCROLL_SETTLE_TIMEOUT_MS = 1200;
47
+
25
48
  /** Keys that scroll the viewport — used to detect a manual scroll intent. */
26
49
  const SCROLL_KEYS = new Set([
27
50
  'ArrowUp',
@@ -58,26 +81,82 @@ function getScrollableAncestor(
58
81
  return null;
59
82
  }
60
83
 
84
+ /** The element's own `scroll-margin-top` in px (0 when unset). */
85
+ function getScrollMarginTop(element: HTMLElement): number {
86
+ return (
87
+ Number.parseFloat(window.getComputedStyle(element).scrollMarginTop) || 0
88
+ );
89
+ }
90
+
91
+ /** The top of the scroll root in viewport coordinates (0 for the viewport). */
92
+ function getRootTop(scrollRoot: HTMLElement | null): number {
93
+ return scrollRoot != null ? scrollRoot.getBoundingClientRect().top : 0;
94
+ }
95
+
96
+ /**
97
+ * Where a heading comes to rest, in viewport coordinates: below the fixed
98
+ * header (`offset`) and below its own `scroll-margin-top` breathing room.
99
+ *
100
+ * This is the single source of truth shared by the activation line and the
101
+ * scroll landing — the two must agree, or a heading activates somewhere other
102
+ * than where navigating to it puts it.
103
+ */
104
+ function getRestingTop(
105
+ target: HTMLElement,
106
+ scrollRoot: HTMLElement | null,
107
+ offset: number,
108
+ ): number {
109
+ return getRootTop(scrollRoot) + offset + getScrollMarginTop(target);
110
+ }
111
+
112
+ /**
113
+ * Bring `target` to rest at the top of the scroll root, below any fixed header.
114
+ *
115
+ * With no `offset`, this is the CSS-native path: `scrollIntoView` already
116
+ * honors the heading's `scroll-margin-top`, and it walks *every* scrollable
117
+ * ancestor, so the browser does it better than we can.
118
+ *
119
+ * An `offset` describes a fixed header overlaying the top of the scroll root —
120
+ * which the browser cannot know about, so `scrollIntoView` would park the
121
+ * heading underneath it, hidden. Compute the landing explicitly instead, from
122
+ * the same {@link getRestingTop} the activation line uses.
123
+ */
124
+ function scrollToTarget(
125
+ target: HTMLElement,
126
+ scrollRoot: HTMLElement | null,
127
+ scrollTarget: HTMLElement | Window,
128
+ offset: number,
129
+ ): void {
130
+ if (offset === 0) {
131
+ target.scrollIntoView({behavior: 'smooth', block: 'start'});
132
+ return;
133
+ }
134
+
135
+ const delta =
136
+ target.getBoundingClientRect().top -
137
+ getRestingTop(target, scrollRoot, offset);
138
+ scrollTarget.scrollBy({top: delta, behavior: 'smooth'});
139
+ }
140
+
61
141
  /**
62
142
  * Resolve the active heading id from current scroll position.
63
143
  *
64
- * A heading is "passed" once its top reaches its activation line — the scroll
65
- * root's top plus the heading's own scroll-margin-top. The active heading is
66
- * the last passed one (headings are in document order). When none have passed
67
- * (scrolled above the first), the first item is active; at the bottom, the
68
- * last item is active.
144
+ * A heading is "passed" once its top reaches its activation line — exactly
145
+ * where navigating to it would land it ({@link getRestingTop}): the scroll
146
+ * root's top, plus `offset` (a fixed header overlaying the scroll root), plus
147
+ * the heading's own scroll-margin-top. The active heading is the last passed
148
+ * one (headings are in document order). When none have passed (scrolled above
149
+ * the first), the first item is active; at the bottom, the last item is active.
69
150
  */
70
151
  function resolveActiveId(
71
152
  items: OutlineItem[],
72
153
  scrollRoot: HTMLElement | null,
154
+ offset: number,
73
155
  ): string | undefined {
74
156
  if (items.length === 0) {
75
157
  return undefined;
76
158
  }
77
159
 
78
- const rootTop =
79
- scrollRoot != null ? scrollRoot.getBoundingClientRect().top : 0;
80
-
81
160
  const atBottom =
82
161
  scrollRoot != null
83
162
  ? scrollRoot.scrollTop + scrollRoot.clientHeight >=
@@ -94,11 +173,10 @@ function resolveActiveId(
94
173
  if (element == null) {
95
174
  continue;
96
175
  }
97
- const top = element.getBoundingClientRect().top;
98
- const marginTop =
99
- Number.parseFloat(window.getComputedStyle(element).scrollMarginTop) || 0;
100
176
 
101
- if (top <= rootTop + marginTop + 1) {
177
+ // 1px of tolerance absorbs sub-pixel rounding after a scroll lands.
178
+ const top = element.getBoundingClientRect().top;
179
+ if (top <= getRestingTop(element, scrollRoot, offset) + 1) {
102
180
  activeId = item.id;
103
181
  } else {
104
182
  break;
@@ -112,6 +190,20 @@ interface UseScrollSpyOptions {
112
190
  items: OutlineItem[];
113
191
  onActiveIdChange?: (id: string) => void;
114
192
  rootRef: React.RefObject<HTMLElement | null>;
193
+ /**
194
+ * Height in px of a fixed header overlaying the top of the scroll root.
195
+ * Shifts both the activation line and the scroll landing by the same amount,
196
+ * on top of each heading's own `scroll-margin-top`.
197
+ */
198
+ offset?: number;
199
+ /** Scroll container to track, instead of the nearest scrollable ancestor. */
200
+ scrollContainerRef?: React.RefObject<HTMLElement | null>;
201
+ /** Whether {@link UseScrollSpyResult.scrollTo} performs the smooth scroll. */
202
+ hasScrollOnClick?: boolean;
203
+ /** Called when a navigation begins, before the scroll starts. */
204
+ onNavigateStart?: (id: string) => void;
205
+ /** Called once per navigation, when the scroll settles or is interrupted. */
206
+ onNavigateEnd?: (id: string) => void;
115
207
  }
116
208
 
117
209
  interface UseScrollSpyResult {
@@ -119,13 +211,19 @@ interface UseScrollSpyResult {
119
211
  /** Set the active id (notifies onActiveIdChange). For controlled consumers. */
120
212
  setActiveId: (id: string) => void;
121
213
  /**
122
- * Handle a click on the outline item with id `id`. Delays moving the
123
- * indicator: scroll-spy is suppressed during the programmatic smooth scroll
124
- * so the indicator doesn't chase it, then the indicator moves once to the
125
- * clicked item when the scroll settles. If the user scrolls manually mid-way,
126
- * scroll-position tracking resumes immediately instead.
214
+ * Navigate to the item with id `id` — the single path shared by click and
215
+ * keyboard activation.
216
+ *
217
+ * Fires `onNavigateStart(id)`, scrolls (unless `hasScrollOnClick` is false),
218
+ * and fires `onNavigateEnd(id)` exactly once when the scroll settles or the
219
+ * user interrupts it. While uncontrolled, scroll-spy is suppressed for the
220
+ * duration so the indicator doesn't chase the scroll through intervening
221
+ * sections; it lands on the target once the scroll settles, or resumes
222
+ * position tracking if the user scrolls away mid-flight.
223
+ *
224
+ * Returns false (and does nothing) when no element with `id` exists.
127
225
  */
128
- lockActiveId: (id: string) => void;
226
+ scrollTo: (id: string) => boolean;
129
227
  }
130
228
 
131
229
  export function useScrollSpy({
@@ -133,18 +231,29 @@ export function useScrollSpy({
133
231
  items,
134
232
  onActiveIdChange,
135
233
  rootRef,
234
+ offset = 0,
235
+ scrollContainerRef,
236
+ hasScrollOnClick = true,
237
+ onNavigateStart,
238
+ onNavigateEnd,
136
239
  }: UseScrollSpyOptions): UseScrollSpyResult {
137
240
  const isControlled = activeId !== undefined;
138
241
  const [uncontrolledActiveId, setUncontrolledActiveId] = useState<
139
242
  string | undefined
140
243
  >(items[0]?.id);
141
244
  const activeIdRef = useRef<string | undefined>(activeId);
142
- // While true, scroll-spy ignores scroll updates because a click is driving a
143
- // programmatic scroll. Released when that scroll settles or the user scrolls.
245
+ // While true, scroll-spy ignores scroll updates because a navigation is
246
+ // driving a programmatic scroll. Released when it settles or the user scrolls.
144
247
  const suppressRef = useRef(false);
145
- const releaseSuppressionRef = useRef<(() => void) | null>(null);
146
- // Latest scroll-position resolver, so the click handler can resume tracking
147
- // when the user scrolls during a programmatic scroll.
248
+ // The navigation currently in flight, if any.
249
+ const navigationRef = useRef<{
250
+ /** End it (without resuming tracking) because a new one is starting. */
251
+ supersede: () => void;
252
+ /** Drop its listeners without firing onNavigateEnd (unmount). */
253
+ teardown: () => void;
254
+ } | null>(null);
255
+ // Latest scroll-position resolver, so a navigation can resume tracking when
256
+ // the user scrolls during the programmatic scroll.
148
257
  const syncRef = useRef<(() => void) | null>(null);
149
258
  // Keep latest items/callback in refs so the scroll listener effect doesn't
150
259
  // re-subscribe on every render (items is a fresh array each render).
@@ -155,12 +264,23 @@ export function useScrollSpy({
155
264
  const itemIds = items.map(item => item.id).join('\n');
156
265
  activeIdRef.current = isControlled ? activeId : uncontrolledActiveId;
157
266
 
267
+ /**
268
+ * The element whose scroll position drives the outline: the explicit
269
+ * container when scoped, else the nearest scrollable ancestor, else null
270
+ * (meaning the viewport).
271
+ */
272
+ const getScrollRoot = useCallback((): HTMLElement | null => {
273
+ return (
274
+ scrollContainerRef?.current ?? getScrollableAncestor(rootRef.current)
275
+ );
276
+ }, [rootRef, scrollContainerRef]);
277
+
158
278
  useEffect(() => {
159
279
  if (isControlled || typeof window === 'undefined') {
160
280
  return;
161
281
  }
162
282
 
163
- const scrollRoot = getScrollableAncestor(rootRef.current);
283
+ const scrollRoot = getScrollRoot();
164
284
  const scrollTarget: HTMLElement | Window = scrollRoot ?? window;
165
285
 
166
286
  let frame = 0;
@@ -169,7 +289,11 @@ export function useScrollSpy({
169
289
  if (suppressRef.current) {
170
290
  return;
171
291
  }
172
- const nextActiveId = resolveActiveId(itemsRef.current, scrollRoot);
292
+ const nextActiveId = resolveActiveId(
293
+ itemsRef.current,
294
+ scrollRoot,
295
+ offset,
296
+ );
173
297
  if (nextActiveId != null && nextActiveId !== activeIdRef.current) {
174
298
  activeIdRef.current = nextActiveId;
175
299
  setUncontrolledActiveId(nextActiveId);
@@ -195,12 +319,12 @@ export function useScrollSpy({
195
319
  cancelAnimationFrame(frame);
196
320
  }
197
321
  };
198
- }, [isControlled, itemIds, rootRef]);
322
+ }, [isControlled, itemIds, offset, getScrollRoot]);
199
323
 
200
- // Tear down any pending suppression listeners when the Outline unmounts.
324
+ // Tear down any in-flight navigation's listeners when the Outline unmounts.
201
325
  useEffect(() => {
202
326
  return () => {
203
- releaseSuppressionRef.current?.();
327
+ navigationRef.current?.teardown();
204
328
  };
205
329
  }, []);
206
330
 
@@ -211,65 +335,117 @@ export function useScrollSpy({
211
335
  onActiveIdChange?.(nextActiveId);
212
336
  };
213
337
 
214
- const lockActiveId = useCallback((clickedId: string) => {
215
- if (typeof window === 'undefined') {
216
- setUncontrolledActiveId(clickedId);
217
- activeIdRef.current = clickedId;
218
- onActiveIdChangeRef.current?.(clickedId);
219
- return;
338
+ // Defined in render (not memoized) so it always closes over the current props
339
+ // — it only ever runs from an event handler, never during render.
340
+ const scrollTo = (id: string): boolean => {
341
+ const target =
342
+ typeof document !== 'undefined' ? document.getElementById(id) : null;
343
+ if (target == null) {
344
+ return false;
345
+ }
346
+
347
+ // A second navigation replaces the first: end the old one (balancing its
348
+ // onNavigateStart) without resuming tracking, since we are about to
349
+ // suppress it again anyway.
350
+ navigationRef.current?.supersede();
351
+
352
+ onNavigateStart?.(id);
353
+
354
+ if (isControlled) {
355
+ // The consumer owns the active state — notify only.
356
+ setActiveId(id);
357
+ } else {
358
+ // Freeze the indicator during the programmatic scroll instead of moving
359
+ // it immediately: it lands on the target once the scroll settles, so it
360
+ // doesn't chase the scroll through intervening sections.
361
+ suppressRef.current = true;
220
362
  }
221
363
 
222
- // Freeze the indicator during the programmatic smooth scroll instead of
223
- // moving it immediately — it lands on the clicked item once the scroll
224
- // settles, so it doesn't chase the scroll through intervening sections.
225
- suppressRef.current = true;
226
- // Replace any in-flight handlers from a previous click.
227
- releaseSuppressionRef.current?.();
364
+ const scrollRoot = getScrollRoot();
365
+ const scrollTarget: HTMLElement | Window = scrollRoot ?? window;
228
366
 
229
367
  let settleTimer = 0;
368
+ let isSettled = false;
369
+
230
370
  const cleanup = () => {
231
- window.removeEventListener('scrollend', onSettle);
232
- window.removeEventListener('wheel', onManual);
233
- window.removeEventListener('touchmove', onManual);
371
+ scrollTarget.removeEventListener('scrollend', onSettle);
372
+ scrollTarget.removeEventListener('wheel', onManual);
373
+ scrollTarget.removeEventListener('touchmove', onManual);
234
374
  window.removeEventListener('keydown', onKeyDown);
235
375
  if (settleTimer !== 0) {
236
376
  clearTimeout(settleTimer);
237
377
  settleTimer = 0;
238
378
  }
239
- releaseSuppressionRef.current = null;
379
+ navigationRef.current = null;
240
380
  };
241
- // Programmatic scroll finished: move the indicator to the clicked item.
242
- const onSettle = () => {
243
- cleanup();
244
- suppressRef.current = false;
245
- setUncontrolledActiveId(clickedId);
246
- activeIdRef.current = clickedId;
247
- onActiveIdChangeRef.current?.(clickedId);
248
- };
249
- // User scrolled mid-flight: hand control back to scroll-position tracking.
250
- const onManual = () => {
381
+
382
+ /**
383
+ * End the navigation exactly once. `didArrive` is false when the user took
384
+ * over with a manual scroll mid-flight. `onNavigateEnd` fires either way,
385
+ * so every `onNavigateStart` is balanced and a consumer's "navigating"
386
+ * state can never leak.
387
+ */
388
+ const finish = (didArrive: boolean, shouldResume = true) => {
389
+ if (isSettled) {
390
+ return;
391
+ }
392
+ isSettled = true;
251
393
  cleanup();
252
- suppressRef.current = false;
253
- syncRef.current?.();
394
+
395
+ if (!isControlled) {
396
+ suppressRef.current = false;
397
+ if (didArrive) {
398
+ setUncontrolledActiveId(id);
399
+ activeIdRef.current = id;
400
+ onActiveIdChangeRef.current?.(id);
401
+ } else if (shouldResume) {
402
+ // Hand control back to scroll-position tracking.
403
+ syncRef.current?.();
404
+ }
405
+ }
406
+
407
+ onNavigateEnd?.(id);
254
408
  };
409
+
410
+ const onSettle = () => finish(true);
411
+ const onManual = () => finish(false);
255
412
  const onKeyDown = (event: KeyboardEvent) => {
256
- if (SCROLL_KEYS.has(event.key)) {
257
- onManual();
413
+ // A key the outline itself consumed (arrow roving focus, Space
414
+ // activation) is prevented, so the browser will not scroll — it is not a
415
+ // manual scroll intent and must not cancel the navigation it just began.
416
+ if (!event.defaultPrevented && SCROLL_KEYS.has(event.key)) {
417
+ finish(false);
258
418
  }
259
419
  };
260
420
 
261
- window.addEventListener('scrollend', onSettle, {once: true});
262
- window.addEventListener('wheel', onManual, {passive: true});
263
- window.addEventListener('touchmove', onManual, {passive: true});
264
- window.addEventListener('keydown', onKeyDown);
265
- // Fallback when scrollend is unsupported or no scroll is needed.
266
- settleTimer = window.setTimeout(onSettle, 1200);
267
- releaseSuppressionRef.current = cleanup;
268
- }, []);
421
+ if (hasScrollOnClick) {
422
+ // Arm settle detection *before* scrolling so an instant jump (a target
423
+ // already in position, or prefers-reduced-motion collapsing the smooth
424
+ // scroll) cannot land before anyone is listening.
425
+ scrollTarget.addEventListener('scrollend', onSettle, {once: true});
426
+ scrollTarget.addEventListener('wheel', onManual, {passive: true});
427
+ scrollTarget.addEventListener('touchmove', onManual, {passive: true});
428
+ window.addEventListener('keydown', onKeyDown);
429
+ settleTimer = window.setTimeout(onSettle, SCROLL_SETTLE_TIMEOUT_MS);
430
+ navigationRef.current = {
431
+ supersede: () => finish(false, false),
432
+ teardown: cleanup,
433
+ };
434
+ scrollToTarget(target, scrollRoot, scrollTarget, offset);
435
+ } else {
436
+ // The consumer owns scrolling — there is no scroll to wait for, so the
437
+ // navigation is already at rest. Resolving immediately (instead of
438
+ // waiting on the settle timeout) keeps onNavigateEnd usable for arrival
439
+ // effects even when hasScrollOnClick is false.
440
+ finish(true);
441
+ }
442
+
443
+ return true;
444
+ };
269
445
 
270
446
  return {
271
447
  activeId: isControlled ? activeId : uncontrolledActiveId,
272
448
  setActiveId,
273
- lockActiveId,
449
+ scrollTo,
274
450
  };
275
451
  }
@@ -61,6 +61,20 @@ export const docs = {
61
61
  description: 'Minimum number of items to always show, even when overflowing.',
62
62
  default: '0',
63
63
  },
64
+ {
65
+ name: 'maxVisibleItems',
66
+ type: 'number',
67
+ description:
68
+ 'Maximum number of items to ever show, even when they all fit. The ceiling partner to minVisibleItems; extra items collapse into the overflow indicator. If less than minVisibleItems, the floor wins.',
69
+ default: 'undefined (no cap)',
70
+ },
71
+ {
72
+ name: 'maxRows',
73
+ type: 'number',
74
+ description:
75
+ 'Wrap items across up to this many rows before collapsing the rest into the overflow indicator. A number, not a boolean. Leave undefined (or set 1) for single-line behavior. Assumes uniform row height.',
76
+ default: 'undefined (single line)',
77
+ },
64
78
  {
65
79
  name: 'collapseFrom',
66
80
  type: "'start' | 'end'",
@@ -91,8 +105,9 @@ export const docs = {
91
105
  'A horizontal list that automatically hides items when they exceed the available width. Use OverflowList for breadcrumbs, toolbars, tag lists, or any row that needs to collapse gracefully at smaller sizes.',
92
106
  bestPractices: [
93
107
  { guidance: true, description: 'Provide a meaningful overflowRenderer: a "+N more" badge, a dropdown, or a count indicator.' },
94
- { guidance: true, description: 'Set minVisibleItems to keep key items visible regardless of container size.' },
95
- { guidance: false, description: 'Use OverflowList for vertical layouts; it only works with horizontal rows.' },
108
+ { guidance: true, description: 'Set minVisibleItems to keep key items visible, and maxVisibleItems to cap the row at a fixed count regardless of available width.' },
109
+ { guidance: true, description: 'Use maxRows to let items wrap onto a bounded number of rows (e.g. a two-row tag cloud) before collapsing the rest into the indicator.' },
110
+ { guidance: false, description: 'Use OverflowList for a vertical stack; horizontal multi-row wrap is supported via maxRows, but items still flow left-to-right, not top-to-bottom.' },
96
111
  ],
97
112
  },
98
113
  };
@@ -127,6 +142,20 @@ export const docsZh = {
127
142
  description: '溢出时始终显示的最小项目数。',
128
143
  default: '0',
129
144
  },
145
+ {
146
+ name: 'maxVisibleItems',
147
+ type: 'number',
148
+ description:
149
+ '始终显示的最大项目数(即使全部都能放下)。它是 minVisibleItems 的上限伙伴;多余项目会折叠进溢出指示器。若小于 minVisibleItems,则以下限为准。',
150
+ default: 'undefined(无上限)',
151
+ },
152
+ {
153
+ name: 'maxRows',
154
+ type: 'number',
155
+ description:
156
+ '在将其余项目折叠进溢出指示器之前,允许项目换行到的最大行数。是数字而非布尔值。留空(或设为 1)表示单行。假定行高一致。',
157
+ default: 'undefined(单行)',
158
+ },
130
159
  {
131
160
  name: 'collapseFrom',
132
161
  type: "'start' | 'end'",
@@ -152,8 +181,9 @@ export const docsZh = {
152
181
  'A horizontal list that automatically hides items when they exceed the available width. Use OverflowList for breadcrumbs, toolbars, tag lists, or any row that needs to collapse gracefully at smaller sizes.',
153
182
  bestPractices: [
154
183
  { guidance: true, description: 'Provide a meaningful overflowRenderer: a "+N more" badge, a dropdown, or a count indicator.' },
155
- { guidance: true, description: 'Set minVisibleItems to keep key items visible regardless of container size.' },
156
- { guidance: false, description: 'Use OverflowList for vertical layouts; it only works with horizontal rows.' },
184
+ { guidance: true, description: 'Set minVisibleItems to keep key items visible, and maxVisibleItems to cap the row at a fixed count regardless of available width.' },
185
+ { guidance: true, description: 'Use maxRows to let items wrap onto a bounded number of rows (e.g. a two-row tag cloud) before collapsing the rest into the indicator.' },
186
+ { guidance: false, description: 'Use OverflowList for a vertical stack; horizontal multi-row wrap is supported via maxRows, but items still flow left-to-right, not top-to-bottom.' },
157
187
  ],
158
188
  },
159
189
  };
@@ -166,8 +196,9 @@ export const docsDense = {
166
196
  'A horizontal list that automatically hides items when they exceed the available width. Use OverflowList for breadcrumbs, toolbars, tag lists, or any row that needs to collapse gracefully at smaller sizes.',
167
197
  bestPractices: [
168
198
  { guidance: true, description: 'Provide a meaningful overflowRenderer: a "+N more" badge, a dropdown, or a count indicator.' },
169
- { guidance: true, description: 'Set minVisibleItems to keep key items visible regardless of container size.' },
170
- { guidance: false, description: 'Use OverflowList for vertical layouts; it only works with horizontal rows.' },
199
+ { guidance: true, description: 'Set minVisibleItems to keep key items visible, and maxVisibleItems to cap the row at a fixed count regardless of available width.' },
200
+ { guidance: true, description: 'Use maxRows to let items wrap onto a bounded number of rows (e.g. a two-row tag cloud) before collapsing the rest into the indicator.' },
201
+ { guidance: false, description: 'Use OverflowList for a vertical stack; horizontal multi-row wrap is supported via maxRows, but items still flow left-to-right, not top-to-bottom.' },
171
202
  ],
172
203
  },
173
204
  propDescriptions: {
@@ -175,6 +206,8 @@ export const docsDense = {
175
206
  overflowRenderer: 'renders overflow indicator, receives hidden items w/ index',
176
207
  gap: 'item gap as spacing step',
177
208
  minVisibleItems: 'min items always shown even when overflowing',
209
+ maxVisibleItems: 'max items ever shown (cap); extra items collapse to indicator; min wins if smaller',
210
+ maxRows: 'wrap items across up to N rows then collapse rest (number, not boolean); undefined = single line',
178
211
  collapseFrom: 'which end to collapse from',
179
212
  behavior: 'observeSelf (default) or observeParent for content-sized containers',
180
213
  xstyle: 'StyleX styles, use stylex.create() values only',
@@ -413,4 +413,115 @@ describe('OverflowList', () => {
413
413
  expect(OverflowList.displayName).toBe('OverflowList');
414
414
  });
415
415
  });
416
+
417
+ describe('maxVisibleItems (cap)', () => {
418
+ it('caps visible items even when they all fit', () => {
419
+ render(
420
+ <OverflowList
421
+ gap={0}
422
+ data-w="1000"
423
+ data-testid="ov"
424
+ maxVisibleItems={2}
425
+ overflowRenderer={indicator('more:')}>
426
+ <button type="button" data-w="40">
427
+ A
428
+ </button>
429
+ <button type="button" data-w="40">
430
+ B
431
+ </button>
432
+ <button type="button" data-w="40">
433
+ C
434
+ </button>
435
+ <button type="button" data-w="40">
436
+ D
437
+ </button>
438
+ </OverflowList>,
439
+ );
440
+ const vis = visibleContainer();
441
+ expect(within(vis).getByText('A')).toBeInTheDocument();
442
+ expect(within(vis).getByText('B')).toBeInTheDocument();
443
+ expect(within(vis).queryByText('C')).not.toBeInTheDocument();
444
+ expect(within(vis).getByText('more:2,3')).toBeInTheDocument();
445
+ });
446
+
447
+ it('min wins over a smaller cap (D1)', () => {
448
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
449
+ render(
450
+ <OverflowList
451
+ gap={0}
452
+ data-w="1000"
453
+ data-testid="ov"
454
+ minVisibleItems={3}
455
+ maxVisibleItems={1}
456
+ overflowRenderer={indicator('more:')}>
457
+ <button type="button" data-w="40">
458
+ A
459
+ </button>
460
+ <button type="button" data-w="40">
461
+ B
462
+ </button>
463
+ <button type="button" data-w="40">
464
+ C
465
+ </button>
466
+ <button type="button" data-w="40">
467
+ D
468
+ </button>
469
+ </OverflowList>,
470
+ );
471
+ const vis = visibleContainer();
472
+ expect(within(vis).getByText('A')).toBeInTheDocument();
473
+ expect(within(vis).getByText('B')).toBeInTheDocument();
474
+ expect(within(vis).getByText('C')).toBeInTheDocument();
475
+ expect(within(vis).queryByText('D')).not.toBeInTheDocument();
476
+ expect(warn).toHaveBeenCalled();
477
+ warn.mockRestore();
478
+ });
479
+ });
480
+
481
+ describe('maxRows (multi-row)', () => {
482
+ it('applies a different container class when maxRows enables wrapping', () => {
483
+ const {rerender} = render(
484
+ <OverflowList gap={0} data-w="1000" data-testid="ov">
485
+ <button type="button" data-w="40">
486
+ A
487
+ </button>
488
+ </OverflowList>,
489
+ );
490
+ const singleLine = visibleContainer().getAttribute('class');
491
+
492
+ rerender(
493
+ <OverflowList gap={0} data-w="1000" data-testid="ov" maxRows={2}>
494
+ <button type="button" data-w="40">
495
+ A
496
+ </button>
497
+ </OverflowList>,
498
+ );
499
+ const multiRow = visibleContainer().getAttribute('class');
500
+ expect(multiRow).not.toEqual(singleLine);
501
+ });
502
+
503
+ it('keeps single-line behavior with maxRows={1}', () => {
504
+ render(
505
+ <OverflowList
506
+ gap={0}
507
+ data-w="100"
508
+ data-testid="ov"
509
+ maxRows={1}
510
+ overflowRenderer={indicator('more:')}>
511
+ <button type="button" data-w="40">
512
+ A
513
+ </button>
514
+ <button type="button" data-w="40">
515
+ B
516
+ </button>
517
+ <button type="button" data-w="40">
518
+ C
519
+ </button>
520
+ </OverflowList>,
521
+ );
522
+ const vis = visibleContainer();
523
+ expect(within(vis).getByText('A')).toBeInTheDocument();
524
+ expect(within(vis).queryByText('B')).not.toBeInTheDocument();
525
+ });
526
+ });
416
527
  });