@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
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file Outline.tsx
7
- * @input Uses React, StyleX, Astryx link provider integration, Outline hooks/types
7
+ * @input Uses React, StyleX, Astryx link provider integration, Outline hooks/types, useListFocus
8
8
  * @output Exports Outline component and OutlineProps type
9
9
  * @position Core implementation; consumed by index.ts
10
10
  *
@@ -12,6 +12,9 @@
12
12
  * - Sliding indicator track (vertical divider + animated active bar)
13
13
  * - Density variant (default/compact)
14
14
  * - Scroll-spy active state when uncontrolled
15
+ * - Roving-tabindex keyboard navigation (Arrow/Home/End, Enter/Space activate)
16
+ * - onNavigateStart / onNavigateEnd callbacks around the smooth scroll
17
+ * - Scroll scoping (offset, scrollContainerRef, hasScrollOnClick)
15
18
  *
16
19
  * SYNC: When modified, update these files to stay in sync:
17
20
  * - /packages/core/src/Outline/Outline.doc.mjs
@@ -24,6 +27,8 @@ import * as stylex from '@stylexjs/stylex';
24
27
  import "../theme/tokens.stylex.js";
25
28
  import { colorVars, durationVars, easeVars, radiusVars, spacingVars, typeScaleVars, fontWeightVars } from "../theme/tokens.stylex.js";
26
29
  import { useLinkComponent } from "../Link/useLinkComponent.js";
30
+ import { useListFocus } from "../hooks/useListFocus.js";
31
+ import { useIsomorphicLayoutEffect } from "../hooks/useIsomorphicLayoutEffect.js";
27
32
  import { mergeProps, mergeRefs } from "../utils/index.js";
28
33
  import { useScrollSpy } from "./useScrollSpy.js";
29
34
  import { themeProps } from "../utils/themeProps.js";
@@ -135,8 +140,15 @@ function getIndentStyle(level) {
135
140
  * track that animates to the active item.
136
141
  *
137
142
  * When `activeId` is omitted, it tracks scroll position and marks the last
138
- * heading whose top has passed its activation line (its scroll-margin-top)
139
- * active — defaulting to the first item at the top and the last at the bottom.
143
+ * heading whose top has passed its activation line — which is exactly where
144
+ * navigating to that heading lands it: `offset` (a fixed header overlaying the
145
+ * scroll root) plus the heading's own `scroll-margin-top`. It defaults to the
146
+ * first item at the top and the last at the bottom.
147
+ *
148
+ * Keyboard: the list is a single tab stop (roving tabindex), seated on the
149
+ * active heading. Arrow keys move between headings, Home/End jump to the ends,
150
+ * and Enter/Space activate — so a long table of contents costs one Tab press,
151
+ * not one per heading.
140
152
  *
141
153
  * @example
142
154
  * ```
@@ -148,6 +160,20 @@ function getIndentStyle(level) {
148
160
  * ]}
149
161
  * />
150
162
  * ```
163
+ *
164
+ * Scoped to a custom scroll container, under a fixed header:
165
+ *
166
+ * @example
167
+ * ```
168
+ * const scrollContainerRef = useRef<HTMLDivElement>(null);
169
+ * <div ref={scrollContainerRef} style={{overflowY: 'auto'}}>...</div>
170
+ * <Outline
171
+ * items={items}
172
+ * scrollContainerRef={scrollContainerRef}
173
+ * offset={64}
174
+ * onNavigateEnd={id => flashHeading(id)}
175
+ * />
176
+ * ```
151
177
  */
152
178
  export function Outline({
153
179
  items,
@@ -155,6 +181,11 @@ export function Outline({
155
181
  onActiveIdChange,
156
182
  label: labelFromProps,
157
183
  density = 'default',
184
+ onNavigateStart,
185
+ onNavigateEnd,
186
+ offset = 0,
187
+ scrollContainerRef,
188
+ hasScrollOnClick = true,
158
189
  xstyle,
159
190
  className,
160
191
  style,
@@ -166,42 +197,109 @@ export function Outline({
166
197
  const label = labelFromProps ?? t('@astryx.outline.label');
167
198
  const rootRef = useRef(null);
168
199
  const LinkComponent = useLinkComponent();
169
- const isControlled = activeId !== undefined;
170
200
  const {
171
201
  activeId: resolvedActiveId,
172
- setActiveId,
173
- lockActiveId
202
+ scrollTo
174
203
  } = useScrollSpy({
175
204
  activeId,
176
205
  items,
177
206
  onActiveIdChange,
178
- rootRef
207
+ rootRef,
208
+ offset,
209
+ scrollContainerRef,
210
+ hasScrollOnClick,
211
+ onNavigateStart,
212
+ onNavigateEnd
213
+ });
214
+
215
+ // Roving tabindex over the links: the whole outline is one tab stop, and
216
+ // arrows move between headings. The hook owns `tabindex` on the items.
217
+ const {
218
+ listRef,
219
+ handleKeyDown: handleListKeyDown,
220
+ handleFocus
221
+ } = useListFocus({
222
+ itemSelector: 'a[href]',
223
+ orientation: 'vertical',
224
+ hasRovingTabIndex: true
225
+ });
226
+
227
+ // Seat the tab stop on the *active* heading. WAI-ARIA puts the single tab
228
+ // stop on the current item, so tabbing into a table of contents while
229
+ // reading section 7 lands on section 7 — not back at section 1, which is
230
+ // where useListFocus would otherwise leave it (it promotes the first item on
231
+ // mount and then keeps whichever item already holds the stop).
232
+ //
233
+ // Only while focus is outside the list: once the reader has arrowed to an
234
+ // item, the stop is theirs and scroll-spy must not yank it away. Sets
235
+ // `tabindex` only — it never steals focus. Registered after useListFocus so
236
+ // it runs after the hook's own layout effect and wins; on later commits the
237
+ // hook then finds this item already holding the stop and keeps it.
238
+ useIsomorphicLayoutEffect(() => {
239
+ const list = listRef.current;
240
+ if (list == null || list.contains(document.activeElement)) {
241
+ return;
242
+ }
243
+ const links = Array.from(list.querySelectorAll('a[href]'));
244
+ const active = links.find(link => link.getAttribute('aria-current') === 'true');
245
+ if (active == null) {
246
+ return;
247
+ }
248
+ for (const link of links) {
249
+ const tabIndex = link === active ? '0' : '-1';
250
+ if (link.getAttribute('tabindex') !== tabIndex) {
251
+ link.setAttribute('tabindex', tabIndex);
252
+ }
253
+ }
179
254
  });
255
+
256
+ /**
257
+ * The single navigation path, shared by click and keyboard activation:
258
+ * push the hash, then hand off to the scroll-spy's `scrollTo`, which fires
259
+ * the navigate callbacks and moves the indicator when the scroll settles.
260
+ */
261
+ const navigate = id => {
262
+ if (scrollTo(id)) {
263
+ window.history.pushState(null, '', `#${id}`);
264
+ }
265
+ };
266
+
267
+ /** Whether an event carries a modifier chord we must leave to the browser. */
268
+ const hasModifier = event => event.metaKey || event.altKey || event.ctrlKey || event.shiftKey;
180
269
  const handleClick = id => event => {
181
- const target = document.getElementById(id);
270
+ // Let the browser handle modified clicks (open in new tab, etc.) without
271
+ // touching the active state.
272
+ if (event.defaultPrevented || hasModifier(event)) {
273
+ return;
274
+ }
182
275
 
183
- // Let the browser handle modified clicks (open in new tab, etc.) and
184
- // missing targets without touching the active state.
185
- if (target == null || event.defaultPrevented || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
276
+ // With no target in the DOM — lazily-rendered or virtualized content —
277
+ // there is nothing to scroll to and nothing to make active. Leave the
278
+ // anchor to the browser's native fragment navigation (which still
279
+ // updates the URL) rather than swallowing the click into a dead link.
280
+ if (document.getElementById(id) == null) {
186
281
  return;
187
282
  }
188
- event.preventDefault();
189
- window.history.pushState(null, '', `#${id}`);
190
283
 
191
- // Move the indicator to the clicked item in a single step. Controlled
192
- // consumers own the active state (notify only); uncontrolled mode pins
193
- // the active id and suppresses scroll-spy until the next manual scroll,
194
- // so the click is honored — even for short/last sections — and the
195
- // indicator doesn't chase the smooth scroll through other sections.
196
- if (isControlled) {
197
- setActiveId(id);
198
- } else {
199
- lockActiveId(id);
284
+ // Suppress the anchor's default jump: we own the scroll (or, with
285
+ // `hasScrollOnClick={false}`, deliberately leave it to the consumer).
286
+ event.preventDefault();
287
+ navigate(id);
288
+ };
289
+ const handleKeyDown = id => event => {
290
+ // Enter is a link's native activation and already produces a click.
291
+ // Space is not, so wire it up here to match the button-like affordance.
292
+ if (event.key !== ' ' && event.key !== 'Spacebar') {
293
+ return;
200
294
  }
201
- target.scrollIntoView({
202
- behavior: 'smooth',
203
- block: 'start'
204
- });
295
+ if (event.defaultPrevented || hasModifier(event)) {
296
+ return;
297
+ }
298
+
299
+ // preventDefault does double duty: it stops the page from scrolling, and
300
+ // it tells useScrollSpy this Space is an activation, not a manual scroll.
301
+ event.preventDefault();
302
+ navigate(id);
205
303
  };
206
304
  return /*#__PURE__*/_jsxs("nav", {
207
305
  ...props,
@@ -215,7 +313,10 @@ export function Outline({
215
313
  ...{
216
314
  className: "x78zum5 xdt5ytf x1lsbc85 x1ghz6dp x1717udv xe8uvvx x98rzlu xeuugli"
217
315
  },
316
+ ref: listRef,
218
317
  role: "list",
318
+ onKeyDown: handleListKeyDown,
319
+ onFocus: handleFocus,
219
320
  children: items.map(item => {
220
321
  const isActive = item.id === resolvedActiveId;
221
322
  return /*#__PURE__*/_jsx("li", {
@@ -227,6 +328,7 @@ export function Outline({
227
328
  href: `#${item.id}`,
228
329
  "aria-current": isActive ? 'true' : undefined,
229
330
  onClick: handleClick(item.id),
331
+ onKeyDown: handleKeyDown(item.id),
230
332
  ...mergeProps(themeProps('outline-item', {
231
333
  active: isActive ? 'active' : null,
232
334
  level: item.level
@@ -4,20 +4,40 @@ interface UseScrollSpyOptions {
4
4
  items: OutlineItem[];
5
5
  onActiveIdChange?: (id: string) => void;
6
6
  rootRef: React.RefObject<HTMLElement | null>;
7
+ /**
8
+ * Height in px of a fixed header overlaying the top of the scroll root.
9
+ * Shifts both the activation line and the scroll landing by the same amount,
10
+ * on top of each heading's own `scroll-margin-top`.
11
+ */
12
+ offset?: number;
13
+ /** Scroll container to track, instead of the nearest scrollable ancestor. */
14
+ scrollContainerRef?: React.RefObject<HTMLElement | null>;
15
+ /** Whether {@link UseScrollSpyResult.scrollTo} performs the smooth scroll. */
16
+ hasScrollOnClick?: boolean;
17
+ /** Called when a navigation begins, before the scroll starts. */
18
+ onNavigateStart?: (id: string) => void;
19
+ /** Called once per navigation, when the scroll settles or is interrupted. */
20
+ onNavigateEnd?: (id: string) => void;
7
21
  }
8
22
  interface UseScrollSpyResult {
9
23
  activeId: string | undefined;
10
24
  /** Set the active id (notifies onActiveIdChange). For controlled consumers. */
11
25
  setActiveId: (id: string) => void;
12
26
  /**
13
- * Handle a click on the outline item with id `id`. Delays moving the
14
- * indicator: scroll-spy is suppressed during the programmatic smooth scroll
15
- * so the indicator doesn't chase it, then the indicator moves once to the
16
- * clicked item when the scroll settles. If the user scrolls manually mid-way,
17
- * scroll-position tracking resumes immediately instead.
27
+ * Navigate to the item with id `id` — the single path shared by click and
28
+ * keyboard activation.
29
+ *
30
+ * Fires `onNavigateStart(id)`, scrolls (unless `hasScrollOnClick` is false),
31
+ * and fires `onNavigateEnd(id)` exactly once when the scroll settles or the
32
+ * user interrupts it. While uncontrolled, scroll-spy is suppressed for the
33
+ * duration so the indicator doesn't chase the scroll through intervening
34
+ * sections; it lands on the target once the scroll settles, or resumes
35
+ * position tracking if the user scrolls away mid-flight.
36
+ *
37
+ * Returns false (and does nothing) when no element with `id` exists.
18
38
  */
19
- lockActiveId: (id: string) => void;
39
+ scrollTo: (id: string) => boolean;
20
40
  }
21
- export declare function useScrollSpy({ activeId, items, onActiveIdChange, rootRef, }: UseScrollSpyOptions): UseScrollSpyResult;
41
+ export declare function useScrollSpy({ activeId, items, onActiveIdChange, rootRef, offset, scrollContainerRef, hasScrollOnClick, onNavigateStart, onNavigateEnd, }: UseScrollSpyOptions): UseScrollSpyResult;
22
42
  export {};
23
43
  //# sourceMappingURL=useScrollSpy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useScrollSpy.d.ts","sourceRoot":"","sources":["../../src/Outline/useScrollSpy.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,SAAS,CAAC;AAuFzC,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CAC9C;AAED,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,+EAA+E;IAC/E,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;;;;OAMG;IACH,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,KAAK,EACL,gBAAgB,EAChB,OAAO,GACR,EAAE,mBAAmB,GAAG,kBAAkB,CA2I1C"}
1
+ {"version":3,"file":"useScrollSpy.d.ts","sourceRoot":"","sources":["../../src/Outline/useScrollSpy.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,SAAS,CAAC;AAsJzC,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC7C;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACzD,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,iEAAiE;IACjE,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,6EAA6E;IAC7E,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,+EAA+E;IAC/E,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;;;;;;;;;;OAYG;IACH,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;CACnC;AAED,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,KAAK,EACL,gBAAgB,EAChB,OAAO,EACP,MAAU,EACV,kBAAkB,EAClB,gBAAuB,EACvB,eAAe,EACf,aAAa,GACd,EAAE,mBAAmB,GAAG,kBAAkB,CAoN1C"}
@@ -10,15 +10,38 @@
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
  */
21
36
  import { useCallback, useEffect, useRef, useState } from 'react';
37
+ /**
38
+ * How long to wait for a programmatic smooth scroll to settle when the
39
+ * `scrollend` event never arrives — either because the browser does not
40
+ * support it (Safari at time of writing) or because the target was already in
41
+ * position, so no scroll happened at all and no events were emitted.
42
+ */
43
+ const SCROLL_SETTLE_TIMEOUT_MS = 1200;
44
+
22
45
  /** Keys that scroll the viewport — used to detect a manual scroll intent. */
23
46
  const SCROLL_KEYS = new Set(['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' ', 'Spacebar']);
24
47
  function getScrollableAncestor(element) {
@@ -35,20 +58,69 @@ function getScrollableAncestor(element) {
35
58
  return null;
36
59
  }
37
60
 
61
+ /** The element's own `scroll-margin-top` in px (0 when unset). */
62
+ function getScrollMarginTop(element) {
63
+ return Number.parseFloat(window.getComputedStyle(element).scrollMarginTop) || 0;
64
+ }
65
+
66
+ /** The top of the scroll root in viewport coordinates (0 for the viewport). */
67
+ function getRootTop(scrollRoot) {
68
+ return scrollRoot != null ? scrollRoot.getBoundingClientRect().top : 0;
69
+ }
70
+
71
+ /**
72
+ * Where a heading comes to rest, in viewport coordinates: below the fixed
73
+ * header (`offset`) and below its own `scroll-margin-top` breathing room.
74
+ *
75
+ * This is the single source of truth shared by the activation line and the
76
+ * scroll landing — the two must agree, or a heading activates somewhere other
77
+ * than where navigating to it puts it.
78
+ */
79
+ function getRestingTop(target, scrollRoot, offset) {
80
+ return getRootTop(scrollRoot) + offset + getScrollMarginTop(target);
81
+ }
82
+
83
+ /**
84
+ * Bring `target` to rest at the top of the scroll root, below any fixed header.
85
+ *
86
+ * With no `offset`, this is the CSS-native path: `scrollIntoView` already
87
+ * honors the heading's `scroll-margin-top`, and it walks *every* scrollable
88
+ * ancestor, so the browser does it better than we can.
89
+ *
90
+ * An `offset` describes a fixed header overlaying the top of the scroll root —
91
+ * which the browser cannot know about, so `scrollIntoView` would park the
92
+ * heading underneath it, hidden. Compute the landing explicitly instead, from
93
+ * the same {@link getRestingTop} the activation line uses.
94
+ */
95
+ function scrollToTarget(target, scrollRoot, scrollTarget, offset) {
96
+ if (offset === 0) {
97
+ target.scrollIntoView({
98
+ behavior: 'smooth',
99
+ block: 'start'
100
+ });
101
+ return;
102
+ }
103
+ const delta = target.getBoundingClientRect().top - getRestingTop(target, scrollRoot, offset);
104
+ scrollTarget.scrollBy({
105
+ top: delta,
106
+ behavior: 'smooth'
107
+ });
108
+ }
109
+
38
110
  /**
39
111
  * Resolve the active heading id from current scroll position.
40
112
  *
41
- * A heading is "passed" once its top reaches its activation line — the scroll
42
- * root's top plus the heading's own scroll-margin-top. The active heading is
43
- * the last passed one (headings are in document order). When none have passed
44
- * (scrolled above the first), the first item is active; at the bottom, the
45
- * last item is active.
113
+ * A heading is "passed" once its top reaches its activation line — exactly
114
+ * where navigating to it would land it ({@link getRestingTop}): the scroll
115
+ * root's top, plus `offset` (a fixed header overlaying the scroll root), plus
116
+ * the heading's own scroll-margin-top. The active heading is the last passed
117
+ * one (headings are in document order). When none have passed (scrolled above
118
+ * the first), the first item is active; at the bottom, the last item is active.
46
119
  */
47
- function resolveActiveId(items, scrollRoot) {
120
+ function resolveActiveId(items, scrollRoot, offset) {
48
121
  if (items.length === 0) {
49
122
  return undefined;
50
123
  }
51
- const rootTop = scrollRoot != null ? scrollRoot.getBoundingClientRect().top : 0;
52
124
  const atBottom = scrollRoot != null ? scrollRoot.scrollTop + scrollRoot.clientHeight >= scrollRoot.scrollHeight - 2 : window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2;
53
125
  if (atBottom) {
54
126
  return items[items.length - 1].id;
@@ -59,9 +131,10 @@ function resolveActiveId(items, scrollRoot) {
59
131
  if (element == null) {
60
132
  continue;
61
133
  }
134
+
135
+ // 1px of tolerance absorbs sub-pixel rounding after a scroll lands.
62
136
  const top = element.getBoundingClientRect().top;
63
- const marginTop = Number.parseFloat(window.getComputedStyle(element).scrollMarginTop) || 0;
64
- if (top <= rootTop + marginTop + 1) {
137
+ if (top <= getRestingTop(element, scrollRoot, offset) + 1) {
65
138
  activeId = item.id;
66
139
  } else {
67
140
  break;
@@ -73,17 +146,23 @@ export function useScrollSpy({
73
146
  activeId,
74
147
  items,
75
148
  onActiveIdChange,
76
- rootRef
149
+ rootRef,
150
+ offset = 0,
151
+ scrollContainerRef,
152
+ hasScrollOnClick = true,
153
+ onNavigateStart,
154
+ onNavigateEnd
77
155
  }) {
78
156
  const isControlled = activeId !== undefined;
79
157
  const [uncontrolledActiveId, setUncontrolledActiveId] = useState(items[0]?.id);
80
158
  const activeIdRef = useRef(activeId);
81
- // While true, scroll-spy ignores scroll updates because a click is driving a
82
- // programmatic scroll. Released when that scroll settles or the user scrolls.
159
+ // While true, scroll-spy ignores scroll updates because a navigation is
160
+ // driving a programmatic scroll. Released when it settles or the user scrolls.
83
161
  const suppressRef = useRef(false);
84
- const releaseSuppressionRef = useRef(null);
85
- // Latest scroll-position resolver, so the click handler can resume tracking
86
- // when the user scrolls during a programmatic scroll.
162
+ // The navigation currently in flight, if any.
163
+ const navigationRef = useRef(null);
164
+ // Latest scroll-position resolver, so a navigation can resume tracking when
165
+ // the user scrolls during the programmatic scroll.
87
166
  const syncRef = useRef(null);
88
167
  // Keep latest items/callback in refs so the scroll listener effect doesn't
89
168
  // re-subscribe on every render (items is a fresh array each render).
@@ -93,11 +172,20 @@ export function useScrollSpy({
93
172
  onActiveIdChangeRef.current = onActiveIdChange;
94
173
  const itemIds = items.map(item => item.id).join('\n');
95
174
  activeIdRef.current = isControlled ? activeId : uncontrolledActiveId;
175
+
176
+ /**
177
+ * The element whose scroll position drives the outline: the explicit
178
+ * container when scoped, else the nearest scrollable ancestor, else null
179
+ * (meaning the viewport).
180
+ */
181
+ const getScrollRoot = useCallback(() => {
182
+ return scrollContainerRef?.current ?? getScrollableAncestor(rootRef.current);
183
+ }, [rootRef, scrollContainerRef]);
96
184
  useEffect(() => {
97
185
  if (isControlled || typeof window === 'undefined') {
98
186
  return;
99
187
  }
100
- const scrollRoot = getScrollableAncestor(rootRef.current);
188
+ const scrollRoot = getScrollRoot();
101
189
  const scrollTarget = scrollRoot ?? window;
102
190
  let frame = 0;
103
191
  const update = () => {
@@ -105,7 +193,7 @@ export function useScrollSpy({
105
193
  if (suppressRef.current) {
106
194
  return;
107
195
  }
108
- const nextActiveId = resolveActiveId(itemsRef.current, scrollRoot);
196
+ const nextActiveId = resolveActiveId(itemsRef.current, scrollRoot, offset);
109
197
  if (nextActiveId != null && nextActiveId !== activeIdRef.current) {
110
198
  activeIdRef.current = nextActiveId;
111
199
  setUncontrolledActiveId(nextActiveId);
@@ -133,12 +221,12 @@ export function useScrollSpy({
133
221
  cancelAnimationFrame(frame);
134
222
  }
135
223
  };
136
- }, [isControlled, itemIds, rootRef]);
224
+ }, [isControlled, itemIds, offset, getScrollRoot]);
137
225
 
138
- // Tear down any pending suppression listeners when the Outline unmounts.
226
+ // Tear down any in-flight navigation's listeners when the Outline unmounts.
139
227
  useEffect(() => {
140
228
  return () => {
141
- releaseSuppressionRef.current?.();
229
+ navigationRef.current?.teardown();
142
230
  };
143
231
  }, []);
144
232
  const setActiveId = nextActiveId => {
@@ -147,68 +235,112 @@ export function useScrollSpy({
147
235
  }
148
236
  onActiveIdChange?.(nextActiveId);
149
237
  };
150
- const lockActiveId = useCallback(clickedId => {
151
- if (typeof window === 'undefined') {
152
- setUncontrolledActiveId(clickedId);
153
- activeIdRef.current = clickedId;
154
- onActiveIdChangeRef.current?.(clickedId);
155
- return;
238
+
239
+ // Defined in render (not memoized) so it always closes over the current props
240
+ // — it only ever runs from an event handler, never during render.
241
+ const scrollTo = id => {
242
+ const target = typeof document !== 'undefined' ? document.getElementById(id) : null;
243
+ if (target == null) {
244
+ return false;
156
245
  }
157
246
 
158
- // Freeze the indicator during the programmatic smooth scroll instead of
159
- // moving it immediately — it lands on the clicked item once the scroll
160
- // settles, so it doesn't chase the scroll through intervening sections.
161
- suppressRef.current = true;
162
- // Replace any in-flight handlers from a previous click.
163
- releaseSuppressionRef.current?.();
247
+ // A second navigation replaces the first: end the old one (balancing its
248
+ // onNavigateStart) without resuming tracking, since we are about to
249
+ // suppress it again anyway.
250
+ navigationRef.current?.supersede();
251
+ onNavigateStart?.(id);
252
+ if (isControlled) {
253
+ // The consumer owns the active state — notify only.
254
+ setActiveId(id);
255
+ } else {
256
+ // Freeze the indicator during the programmatic scroll instead of moving
257
+ // it immediately: it lands on the target once the scroll settles, so it
258
+ // doesn't chase the scroll through intervening sections.
259
+ suppressRef.current = true;
260
+ }
261
+ const scrollRoot = getScrollRoot();
262
+ const scrollTarget = scrollRoot ?? window;
164
263
  let settleTimer = 0;
264
+ let isSettled = false;
165
265
  const cleanup = () => {
166
- window.removeEventListener('scrollend', onSettle);
167
- window.removeEventListener('wheel', onManual);
168
- window.removeEventListener('touchmove', onManual);
266
+ scrollTarget.removeEventListener('scrollend', onSettle);
267
+ scrollTarget.removeEventListener('wheel', onManual);
268
+ scrollTarget.removeEventListener('touchmove', onManual);
169
269
  window.removeEventListener('keydown', onKeyDown);
170
270
  if (settleTimer !== 0) {
171
271
  clearTimeout(settleTimer);
172
272
  settleTimer = 0;
173
273
  }
174
- releaseSuppressionRef.current = null;
175
- };
176
- // Programmatic scroll finished: move the indicator to the clicked item.
177
- const onSettle = () => {
178
- cleanup();
179
- suppressRef.current = false;
180
- setUncontrolledActiveId(clickedId);
181
- activeIdRef.current = clickedId;
182
- onActiveIdChangeRef.current?.(clickedId);
274
+ navigationRef.current = null;
183
275
  };
184
- // User scrolled mid-flight: hand control back to scroll-position tracking.
185
- const onManual = () => {
276
+
277
+ /**
278
+ * End the navigation exactly once. `didArrive` is false when the user took
279
+ * over with a manual scroll mid-flight. `onNavigateEnd` fires either way,
280
+ * so every `onNavigateStart` is balanced and a consumer's "navigating"
281
+ * state can never leak.
282
+ */
283
+ const finish = (didArrive, shouldResume = true) => {
284
+ if (isSettled) {
285
+ return;
286
+ }
287
+ isSettled = true;
186
288
  cleanup();
187
- suppressRef.current = false;
188
- syncRef.current?.();
289
+ if (!isControlled) {
290
+ suppressRef.current = false;
291
+ if (didArrive) {
292
+ setUncontrolledActiveId(id);
293
+ activeIdRef.current = id;
294
+ onActiveIdChangeRef.current?.(id);
295
+ } else if (shouldResume) {
296
+ // Hand control back to scroll-position tracking.
297
+ syncRef.current?.();
298
+ }
299
+ }
300
+ onNavigateEnd?.(id);
189
301
  };
302
+ const onSettle = () => finish(true);
303
+ const onManual = () => finish(false);
190
304
  const onKeyDown = event => {
191
- if (SCROLL_KEYS.has(event.key)) {
192
- onManual();
305
+ // A key the outline itself consumed (arrow roving focus, Space
306
+ // activation) is prevented, so the browser will not scroll — it is not a
307
+ // manual scroll intent and must not cancel the navigation it just began.
308
+ if (!event.defaultPrevented && SCROLL_KEYS.has(event.key)) {
309
+ finish(false);
193
310
  }
194
311
  };
195
- window.addEventListener('scrollend', onSettle, {
196
- once: true
197
- });
198
- window.addEventListener('wheel', onManual, {
199
- passive: true
200
- });
201
- window.addEventListener('touchmove', onManual, {
202
- passive: true
203
- });
204
- window.addEventListener('keydown', onKeyDown);
205
- // Fallback when scrollend is unsupported or no scroll is needed.
206
- settleTimer = window.setTimeout(onSettle, 1200);
207
- releaseSuppressionRef.current = cleanup;
208
- }, []);
312
+ if (hasScrollOnClick) {
313
+ // Arm settle detection *before* scrolling so an instant jump (a target
314
+ // already in position, or prefers-reduced-motion collapsing the smooth
315
+ // scroll) cannot land before anyone is listening.
316
+ scrollTarget.addEventListener('scrollend', onSettle, {
317
+ once: true
318
+ });
319
+ scrollTarget.addEventListener('wheel', onManual, {
320
+ passive: true
321
+ });
322
+ scrollTarget.addEventListener('touchmove', onManual, {
323
+ passive: true
324
+ });
325
+ window.addEventListener('keydown', onKeyDown);
326
+ settleTimer = window.setTimeout(onSettle, SCROLL_SETTLE_TIMEOUT_MS);
327
+ navigationRef.current = {
328
+ supersede: () => finish(false, false),
329
+ teardown: cleanup
330
+ };
331
+ scrollToTarget(target, scrollRoot, scrollTarget, offset);
332
+ } else {
333
+ // The consumer owns scrolling — there is no scroll to wait for, so the
334
+ // navigation is already at rest. Resolving immediately (instead of
335
+ // waiting on the settle timeout) keeps onNavigateEnd usable for arrival
336
+ // effects even when hasScrollOnClick is false.
337
+ finish(true);
338
+ }
339
+ return true;
340
+ };
209
341
  return {
210
342
  activeId: isControlled ? activeId : uncontrolledActiveId,
211
343
  setActiveId,
212
- lockActiveId
344
+ scrollTo
213
345
  };
214
346
  }