@astryxdesign/core 0.6.2 → 0.6.3-canary.0899f21

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 (549) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts +1 -1
  3. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheet.js +3 -1
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts +7 -5
  6. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  7. package/dist/BottomSheet/BottomSheetPanel.js +56 -19
  8. package/dist/Button/Button.d.ts.map +1 -1
  9. package/dist/Button/Button.js +13 -28
  10. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  11. package/dist/Chat/ChatComposer.js +10 -5
  12. package/dist/Chat/ChatComposerDrawer.d.ts +19 -5
  13. package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
  14. package/dist/Chat/ChatComposerDrawer.js +48 -23
  15. package/dist/Chat/ChatComposerInput.d.ts +11 -3
  16. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  17. package/dist/Chat/ChatComposerInput.js +63 -16
  18. package/dist/Chat/ChatDictationButton.d.ts +3 -2
  19. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  20. package/dist/Chat/ChatDictationButton.js +6 -4
  21. package/dist/Chat/ChatLayout.d.ts +19 -0
  22. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  23. package/dist/Chat/ChatLayout.js +19 -0
  24. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  25. package/dist/Chat/ChatLayoutScrollButton.js +44 -34
  26. package/dist/Chat/ChatMessageBubble.d.ts +2 -0
  27. package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
  28. package/dist/Chat/ChatMessageBubble.js +5 -3
  29. package/dist/Chat/ChatMessageList.d.ts +2 -1
  30. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  31. package/dist/Chat/ChatMessageList.js +4 -3
  32. package/dist/Chat/ChatMessageMetadata.d.ts +5 -4
  33. package/dist/Chat/ChatMessageMetadata.d.ts.map +1 -1
  34. package/dist/Chat/ChatMessageMetadata.js +12 -9
  35. package/dist/Chat/ChatSendButton.js +1 -1
  36. package/dist/Chat/index.d.ts +1 -1
  37. package/dist/Chat/index.d.ts.map +1 -1
  38. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  39. package/dist/CheckboxInput/CheckboxInput.js +12 -2
  40. package/dist/Citation/Citation.d.ts +1 -1
  41. package/dist/Citation/Citation.d.ts.map +1 -1
  42. package/dist/Citation/Citation.js +3 -2
  43. package/dist/CodeBlock/highlightRanges.d.ts.map +1 -1
  44. package/dist/CodeBlock/highlightRanges.js +14 -3
  45. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  46. package/dist/Collapsible/Collapsible.js +6 -1
  47. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  48. package/dist/CommandPalette/CommandPaletteInput.js +1 -1
  49. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  50. package/dist/ComplexSelector/ComplexSelector.js +1 -1
  51. package/dist/DateInput/DateInput.d.ts.map +1 -1
  52. package/dist/DateInput/DateInput.js +4 -4
  53. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  54. package/dist/DateInput/NativeDateField.js +16 -16
  55. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  56. package/dist/DateInput/TouchDateField.js +2 -2
  57. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  58. package/dist/DateRangeInput/DateRangeInput.js +4 -4
  59. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  60. package/dist/DateTimeInput/DateTimeInput.js +8 -8
  61. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  62. package/dist/DateTimeInput/TouchDateTimeField.js +4 -4
  63. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +9 -3
  64. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -1
  65. package/dist/DateTimeInput/nativePickerSegmentStyles.js +3 -3
  66. package/dist/Dialog/DialogHeader.d.ts +8 -1
  67. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  68. package/dist/Dialog/DialogHeader.js +13 -4
  69. package/dist/Field/InputClearButton.d.ts +4 -2
  70. package/dist/Field/InputClearButton.d.ts.map +1 -1
  71. package/dist/Field/InputClearButton.js +4 -2
  72. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  73. package/dist/Field/PanelSearchInput.js +1 -1
  74. package/dist/FileInput/FileInput.d.ts.map +1 -1
  75. package/dist/FileInput/FileInput.js +7 -7
  76. package/dist/Icon/Icon.d.ts +6 -31
  77. package/dist/Icon/Icon.d.ts.map +1 -1
  78. package/dist/Icon/Icon.js +8 -71
  79. package/dist/Icon/IconDefaultSizeContext.d.ts +4 -0
  80. package/dist/Icon/IconDefaultSizeContext.d.ts.map +1 -0
  81. package/dist/Icon/IconDefaultSizeContext.js +18 -0
  82. package/dist/Icon/IconSize.stylex.d.ts +65 -0
  83. package/dist/Icon/IconSize.stylex.d.ts.map +1 -0
  84. package/dist/Icon/IconSize.stylex.js +75 -0
  85. package/dist/Item/Item.d.ts +1 -1
  86. package/dist/Item/Item.d.ts.map +1 -1
  87. package/dist/Item/Item.js +4 -3
  88. package/dist/Kbd/Kbd.d.ts +5 -3
  89. package/dist/Kbd/Kbd.d.ts.map +1 -1
  90. package/dist/Kbd/Kbd.js +36 -42
  91. package/dist/Link/Link.d.ts.map +1 -1
  92. package/dist/Link/Link.js +6 -2
  93. package/dist/Link/useLinkComponent.d.ts +5 -15
  94. package/dist/Link/useLinkComponent.d.ts.map +1 -1
  95. package/dist/Link/useLinkComponent.js +34 -51
  96. package/dist/List/List.d.ts +19 -1
  97. package/dist/List/List.d.ts.map +1 -1
  98. package/dist/List/List.js +9 -2
  99. package/dist/List/ListContext.d.ts +1 -0
  100. package/dist/List/ListContext.d.ts.map +1 -1
  101. package/dist/List/ListContext.js +2 -1
  102. package/dist/List/ListItem.d.ts +1 -1
  103. package/dist/List/ListItem.d.ts.map +1 -1
  104. package/dist/List/ListItem.js +9 -2
  105. package/dist/Markdown/Markdown.d.ts +8 -2
  106. package/dist/Markdown/Markdown.d.ts.map +1 -1
  107. package/dist/Markdown/Markdown.fr23.bench.d.ts +2 -0
  108. package/dist/Markdown/Markdown.fr23.bench.d.ts.map +1 -0
  109. package/dist/Markdown/Markdown.fr23.bench.js +150 -0
  110. package/dist/Markdown/Markdown.fr23.sampling.d.ts +2 -0
  111. package/dist/Markdown/Markdown.fr23.sampling.d.ts.map +1 -0
  112. package/dist/Markdown/Markdown.fr23.sampling.js +43 -0
  113. package/dist/Markdown/Markdown.js +262 -109
  114. package/dist/Markdown/ast.d.ts +162 -0
  115. package/dist/Markdown/ast.d.ts.map +1 -0
  116. package/dist/Markdown/ast.js +74 -0
  117. package/dist/Markdown/index.d.ts +5 -3
  118. package/dist/Markdown/index.d.ts.map +1 -1
  119. package/dist/Markdown/index.js +3 -2
  120. package/dist/Markdown/parser/index.d.ts +10 -0
  121. package/dist/Markdown/parser/index.d.ts.map +1 -0
  122. package/dist/Markdown/parser/index.js +10 -0
  123. package/dist/Markdown/parser.d.ts +126 -58
  124. package/dist/Markdown/parser.d.ts.map +1 -1
  125. package/dist/Markdown/parser.js +716 -167
  126. package/dist/Markdown/plugins/frontmatter.d.ts +32 -0
  127. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -0
  128. package/dist/Markdown/plugins/frontmatter.js +132 -0
  129. package/dist/Markdown/plugins/index.d.ts +17 -0
  130. package/dist/Markdown/plugins/index.d.ts.map +1 -0
  131. package/dist/Markdown/plugins/index.js +14 -0
  132. package/dist/Markdown/plugins/protocol.d.ts +191 -0
  133. package/dist/Markdown/plugins/protocol.d.ts.map +1 -0
  134. package/dist/Markdown/plugins/protocol.js +957 -0
  135. package/dist/Markdown/plugins/semanticFence.d.ts +36 -0
  136. package/dist/Markdown/plugins/semanticFence.d.ts.map +1 -0
  137. package/dist/Markdown/plugins/semanticFence.js +213 -0
  138. package/dist/Markdown/plugins/softBreaks.d.ts +3 -0
  139. package/dist/Markdown/plugins/softBreaks.d.ts.map +1 -0
  140. package/dist/Markdown/plugins/softBreaks.js +172 -0
  141. package/dist/Markdown/plugins/sourceDecoration.d.ts +76 -0
  142. package/dist/Markdown/plugins/sourceDecoration.d.ts.map +1 -0
  143. package/dist/Markdown/plugins/sourceDecoration.js +393 -0
  144. package/dist/Markdown/plugins/textTransform.d.ts +30 -0
  145. package/dist/Markdown/plugins/textTransform.d.ts.map +1 -0
  146. package/dist/Markdown/plugins/textTransform.js +374 -0
  147. package/dist/Markdown/remark.d.ts +196 -0
  148. package/dist/Markdown/remark.d.ts.map +1 -0
  149. package/dist/Markdown/remark.js +1062 -0
  150. package/dist/Markdown/url.d.ts +6 -0
  151. package/dist/Markdown/url.d.ts.map +1 -0
  152. package/dist/Markdown/url.js +31 -0
  153. package/dist/Markdown/utils.d.ts +4 -2
  154. package/dist/Markdown/utils.d.ts.map +1 -1
  155. package/dist/Markdown/utils.js +2 -1
  156. package/dist/MetadataList/MetadataList.js +3 -3
  157. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  158. package/dist/NumberInput/NumberInput.js +4 -4
  159. package/dist/Outline/index.d.ts +1 -0
  160. package/dist/Outline/index.d.ts.map +1 -1
  161. package/dist/Outline/parseOutlineFromMarkdown.d.ts +7 -1
  162. package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
  163. package/dist/Outline/parseOutlineFromMarkdown.js +14 -8
  164. package/dist/Outline/useOutlineFromMarkdown.d.ts +3 -1
  165. package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
  166. package/dist/Outline/useOutlineFromMarkdown.js +8 -3
  167. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  168. package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
  169. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  170. package/dist/PowerSearch/PowerSearchValueEditor.js +5 -0
  171. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  172. package/dist/ProgressBar/ProgressBar.js +36 -31
  173. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  174. package/dist/RadioList/RadioListItem.js +13 -1
  175. package/dist/ScrollableArea/ScrollableArea.d.ts +1 -0
  176. package/dist/ScrollableArea/ScrollableArea.d.ts.map +1 -1
  177. package/dist/ScrollableArea/ScrollableArea.js +1 -0
  178. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  179. package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
  180. package/dist/Selector/Selector.d.ts +9 -7
  181. package/dist/Selector/Selector.d.ts.map +1 -1
  182. package/dist/Selector/Selector.js +16 -7
  183. package/dist/SideNav/SideNav.d.ts +2 -1
  184. package/dist/SideNav/SideNav.d.ts.map +1 -1
  185. package/dist/SideNav/SideNav.js +9 -3
  186. package/dist/Slider/Slider.d.ts.map +1 -1
  187. package/dist/Slider/Slider.js +28 -12
  188. package/dist/Spinner/Spinner.d.ts.map +1 -1
  189. package/dist/Spinner/Spinner.js +23 -7
  190. package/dist/Switch/Switch.d.ts.map +1 -1
  191. package/dist/Switch/Switch.js +11 -0
  192. package/dist/TabList/Tab.d.ts +1 -1
  193. package/dist/TabList/Tab.d.ts.map +1 -1
  194. package/dist/TabList/Tab.js +20 -7
  195. package/dist/Table/Table.d.ts +1 -0
  196. package/dist/Table/Table.d.ts.map +1 -1
  197. package/dist/Table/Table.js +29 -13
  198. package/dist/TextArea/TextArea.d.ts.map +1 -1
  199. package/dist/TextArea/TextArea.js +1 -1
  200. package/dist/TextInput/TextInput.d.ts.map +1 -1
  201. package/dist/TextInput/TextInput.js +2 -2
  202. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  203. package/dist/TimeInput/TimeInput.js +4 -4
  204. package/dist/Timer/Timer.d.ts +49 -0
  205. package/dist/Timer/Timer.d.ts.map +1 -0
  206. package/dist/Timer/Timer.js +158 -0
  207. package/dist/Timer/index.d.ts +9 -0
  208. package/dist/Timer/index.d.ts.map +1 -0
  209. package/dist/Timer/index.js +10 -0
  210. package/dist/ToggleButton/ToggleButton.d.ts +9 -6
  211. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  212. package/dist/ToggleButton/ToggleButton.js +15 -27
  213. package/dist/TopNav/TopNavMenu.d.ts +1 -1
  214. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  215. package/dist/TopNav/TopNavMenu.js +3 -2
  216. package/dist/TreeList/TreeList.d.ts.map +1 -1
  217. package/dist/TreeList/TreeList.js +3 -0
  218. package/dist/TreeList/TreeListItem.d.ts +6 -1
  219. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  220. package/dist/TreeList/TreeListItem.js +12 -2
  221. package/dist/TreeList/TreeListTypes.d.ts +8 -1
  222. package/dist/TreeList/TreeListTypes.d.ts.map +1 -1
  223. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  224. package/dist/Typeahead/BaseTypeahead.js +16 -7
  225. package/dist/astryx.css +27 -11
  226. package/dist/hooks/scrollKeyboardDelegation.d.ts +3 -0
  227. package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -0
  228. package/dist/hooks/scrollKeyboardDelegation.js +146 -0
  229. package/dist/hooks/useClickableContainer.d.ts +10 -1
  230. package/dist/hooks/useClickableContainer.d.ts.map +1 -1
  231. package/dist/hooks/useClickableContainer.js +15 -4
  232. package/dist/hooks/useScrollableArea.d.ts +6 -2
  233. package/dist/hooks/useScrollableArea.d.ts.map +1 -1
  234. package/dist/hooks/useScrollableArea.js +17 -5
  235. package/dist/index.d.ts +1 -0
  236. package/dist/index.d.ts.map +1 -1
  237. package/dist/index.js +3 -0
  238. package/dist/theme/declarationBoundary.d.ts +17 -0
  239. package/dist/theme/declarationBoundary.d.ts.map +1 -0
  240. package/dist/theme/declarationBoundary.js +343 -0
  241. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  242. package/dist/theme/derivedVarRegistry.js +4 -0
  243. package/dist/theme/generateThemeRules.d.ts +13 -7
  244. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  245. package/dist/theme/generateThemeRules.js +140 -34
  246. package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
  247. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  248. package/dist/utils/interactionOverlay.stylex.js +9 -0
  249. package/dist/utils/safeUrl.d.ts +11 -0
  250. package/dist/utils/safeUrl.d.ts.map +1 -0
  251. package/dist/utils/safeUrl.js +78 -0
  252. package/locales/af-ZA.json +530 -1
  253. package/locales/ar-SA.json +490 -1
  254. package/locales/ca-ES.json +514 -1
  255. package/locales/cs-CZ.json +498 -1
  256. package/locales/da-DK.json +522 -1
  257. package/locales/de-DE.json +522 -1
  258. package/locales/el-GR.json +498 -1
  259. package/locales/es-ES.json +510 -1
  260. package/locales/fi-FI.json +498 -1
  261. package/locales/fr-FR.json +534 -1
  262. package/locales/he-IL.json +498 -1
  263. package/locales/hu-HU.json +498 -1
  264. package/locales/it-IT.json +510 -1
  265. package/locales/ja-JP.json +490 -1
  266. package/locales/ko-KR.json +498 -1
  267. package/locales/nl-NL.json +534 -1
  268. package/locales/no-NO.json +510 -1
  269. package/locales/pl-PL.json +506 -1
  270. package/locales/pt-BR.json +506 -1
  271. package/locales/pt-PT.json +506 -1
  272. package/locales/ro-RO.json +506 -1
  273. package/locales/ru-RU.json +498 -1
  274. package/locales/sr-SP.json +498 -1
  275. package/locales/sv-SE.json +502 -1
  276. package/locales/tr-TR.json +498 -1
  277. package/locales/uk-UA.json +498 -1
  278. package/locales/vi-VN.json +502 -1
  279. package/locales/zh-CN.json +490 -1
  280. package/locales/zh-TW.json +490 -1
  281. package/package.json +24 -4
  282. package/src/BottomSheet/BottomSheet.doc.mjs +8 -1
  283. package/src/BottomSheet/BottomSheet.spec.md +46 -20
  284. package/src/BottomSheet/BottomSheet.test.tsx +6 -3
  285. package/src/BottomSheet/BottomSheet.tsx +3 -1
  286. package/src/BottomSheet/BottomSheetKeyboard.test.tsx +195 -0
  287. package/src/BottomSheet/BottomSheetPanel.test.tsx +11 -1
  288. package/src/BottomSheet/BottomSheetPanel.tsx +49 -16
  289. package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +344 -0
  290. package/src/Button/Button.doc.mjs +5 -5
  291. package/src/Button/Button.tsx +15 -13
  292. package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
  293. package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
  294. package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
  295. package/src/Button/__tests__/Button.a11y.states.ts +9 -0
  296. package/src/Chat/ChatComposer.doc.mjs +14 -0
  297. package/src/Chat/ChatComposer.spec.md +230 -0
  298. package/src/Chat/ChatComposer.test.tsx +57 -0
  299. package/src/Chat/ChatComposer.tsx +21 -9
  300. package/src/Chat/ChatComposerDrawer.doc.mjs +63 -5
  301. package/src/Chat/ChatComposerDrawer.spec.md +267 -0
  302. package/src/Chat/ChatComposerDrawer.test.tsx +81 -12
  303. package/src/Chat/ChatComposerDrawer.tsx +38 -9
  304. package/src/Chat/ChatComposerInput.doc.mjs +114 -26
  305. package/src/Chat/ChatComposerInput.spec.md +185 -0
  306. package/src/Chat/ChatComposerInput.test.tsx +113 -3
  307. package/src/Chat/ChatComposerInput.tsx +78 -13
  308. package/src/Chat/ChatComposerTokenElement.spec.md +152 -0
  309. package/src/Chat/ChatComposerTokenElement.test.tsx +85 -0
  310. package/src/Chat/ChatDictationButton.doc.mjs +3 -3
  311. package/src/Chat/ChatDictationButton.spec.md +160 -0
  312. package/src/Chat/ChatDictationButton.test.tsx +92 -27
  313. package/src/Chat/ChatDictationButton.tsx +8 -6
  314. package/src/Chat/ChatLayout.doc.mjs +12 -4
  315. package/src/Chat/ChatLayout.spec.md +271 -0
  316. package/src/Chat/ChatLayout.test.tsx +28 -0
  317. package/src/Chat/ChatLayout.tsx +19 -0
  318. package/src/Chat/ChatLayoutScrollButton.spec.md +338 -0
  319. package/src/Chat/ChatLayoutScrollButton.test.tsx +31 -0
  320. package/src/Chat/ChatLayoutScrollButton.tsx +31 -14
  321. package/src/Chat/ChatMessageBubble.doc.mjs +28 -15
  322. package/src/Chat/ChatMessageBubble.spec.md +263 -0
  323. package/src/Chat/ChatMessageBubble.test.tsx +93 -0
  324. package/src/Chat/ChatMessageBubble.tsx +8 -3
  325. package/src/Chat/ChatMessageList.spec.md +222 -0
  326. package/src/Chat/ChatMessageList.test.tsx +64 -2
  327. package/src/Chat/ChatMessageList.tsx +4 -3
  328. package/src/Chat/ChatMessageMetadata.doc.mjs +57 -3
  329. package/src/Chat/ChatMessageMetadata.spec.md +220 -0
  330. package/src/Chat/ChatMessageMetadata.test.tsx +34 -0
  331. package/src/Chat/ChatMessageMetadata.tsx +13 -13
  332. package/src/Chat/ChatSendButton.test.tsx +1 -4
  333. package/src/Chat/ChatSendButton.tsx +1 -1
  334. package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +514 -0
  335. package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +740 -0
  336. package/src/Chat/__tests__/ChatMessageBubble.a11y.chromium.spec.ts +457 -0
  337. package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.test.ts +91 -0
  338. package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.ts +91 -0
  339. package/src/Chat/__tests__/ChatMessageList.a11y.chromium.spec.ts +456 -0
  340. package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +413 -0
  341. package/src/Chat/index.ts +1 -0
  342. package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
  343. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  344. package/src/CheckboxInput/CheckboxInput.tsx +21 -1
  345. package/src/Citation/Citation.doc.mjs +2 -2
  346. package/src/Citation/Citation.test.tsx +17 -0
  347. package/src/Citation/Citation.tsx +4 -2
  348. package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
  349. package/src/ClickableCard/ClickableCard.test.tsx +102 -5
  350. package/src/CodeBlock/highlightRanges.test.ts +143 -1
  351. package/src/CodeBlock/highlightRanges.ts +15 -5
  352. package/src/Collapsible/Collapsible.doc.mjs +11 -0
  353. package/src/Collapsible/Collapsible.test.tsx +21 -0
  354. package/src/Collapsible/Collapsible.tsx +5 -0
  355. package/src/CommandPalette/CommandPaletteInput.tsx +6 -1
  356. package/src/ComplexSelector/ComplexSelector.tsx +6 -1
  357. package/src/DateInput/DateInput.tsx +6 -1
  358. package/src/DateInput/NativeDateField.tsx +24 -8
  359. package/src/DateInput/TouchDateField.tsx +6 -2
  360. package/src/DateRangeInput/DateRangeInput.tsx +6 -1
  361. package/src/DateTimeInput/DateTimeInput.tsx +6 -1
  362. package/src/DateTimeInput/TouchDateTimeField.tsx +6 -1
  363. package/src/DateTimeInput/nativePickerSegmentStyles.ts +24 -8
  364. package/src/Dialog/Dialog.doc.mjs +2 -0
  365. package/src/Dialog/DialogHeader.doc.mjs +22 -0
  366. package/src/Dialog/DialogHeader.test.tsx +167 -3
  367. package/src/Dialog/DialogHeader.tsx +37 -6
  368. package/src/Dialog/modules/DialogHeader.spec.md +101 -42
  369. package/src/DropdownMenu/DropdownMenu.test.tsx +5 -2
  370. package/src/Field/InputClearButton.test.tsx +17 -3
  371. package/src/Field/InputClearButton.tsx +5 -2
  372. package/src/Field/PanelSearchInput.tsx +5 -2
  373. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +1 -1
  374. package/src/FileInput/FileInput.tsx +11 -2
  375. package/src/Icon/Icon.doc.mjs +7 -5
  376. package/src/Icon/Icon.spec.md +81 -40
  377. package/src/Icon/Icon.test.tsx +6 -3
  378. package/src/Icon/Icon.tsx +20 -68
  379. package/src/Icon/IconDefaultSizeContext.ts +23 -0
  380. package/src/Icon/IconSize.stylex.ts +70 -0
  381. package/src/IconButton/IconButton.doc.mjs +3 -3
  382. package/src/IconButton/IconButton.test.tsx +40 -0
  383. package/src/Item/Item.doc.mjs +6 -2
  384. package/src/Item/Item.tsx +9 -3
  385. package/src/Kbd/Kbd.doc.mjs +3 -3
  386. package/src/Kbd/Kbd.test.tsx +57 -1
  387. package/src/Kbd/Kbd.tsx +57 -37
  388. package/src/Layout/LayoutFooter.doc.mjs +17 -0
  389. package/src/Link/Link.doc.mjs +17 -4
  390. package/src/Link/Link.test.tsx +24 -0
  391. package/src/Link/Link.tsx +5 -0
  392. package/src/Link/LinkProvider.doc.mjs +2 -2
  393. package/src/Link/__tests__/Link.navigation.a11y.chromium.spec.ts +261 -0
  394. package/src/Link/useLinkComponent.test.tsx +333 -0
  395. package/src/Link/useLinkComponent.ts +29 -57
  396. package/src/List/List.doc.mjs +6 -0
  397. package/src/List/List.spec.md +200 -0
  398. package/src/List/List.test.tsx +117 -1
  399. package/src/List/List.tsx +22 -2
  400. package/src/List/ListContext.tsx +3 -1
  401. package/src/List/ListItem.doc.mjs +2 -2
  402. package/src/List/ListItem.source-build.test.mjs +41 -0
  403. package/src/List/ListItem.tsx +26 -2
  404. package/src/Markdown/Markdown.doc.mjs +300 -2
  405. package/src/Markdown/Markdown.fr23.bench.ts +174 -0
  406. package/src/Markdown/Markdown.fr23.perf.test.ts +249 -0
  407. package/src/Markdown/Markdown.fr23.sampling.ts +49 -0
  408. package/src/Markdown/Markdown.helpers.perf.test.ts +489 -0
  409. package/src/Markdown/Markdown.public.test.ts +130 -3
  410. package/src/Markdown/Markdown.renderBoundary.test.tsx +72 -0
  411. package/src/Markdown/Markdown.spec.md +203 -45
  412. package/src/Markdown/Markdown.test.tsx +63 -0
  413. package/src/Markdown/Markdown.tsx +414 -135
  414. package/src/Markdown/ast.test.ts +307 -0
  415. package/src/Markdown/ast.ts +337 -0
  416. package/src/Markdown/incremental.test.ts +12 -0
  417. package/src/Markdown/index.ts +37 -1
  418. package/src/Markdown/modules/remark.spec.md +269 -0
  419. package/src/Markdown/modules/softBreaks.spec.md +137 -0
  420. package/src/Markdown/parser/index.ts +64 -0
  421. package/src/Markdown/parser.public.test.ts +103 -0
  422. package/src/Markdown/parser.test.ts +20 -0
  423. package/src/Markdown/parser.ts +1180 -235
  424. package/src/Markdown/plugins/frontmatter.test.tsx +125 -0
  425. package/src/Markdown/plugins/frontmatter.ts +198 -0
  426. package/src/Markdown/plugins/index.ts +54 -0
  427. package/src/Markdown/plugins/protocol.test.tsx +1022 -0
  428. package/src/Markdown/plugins/protocol.ts +1618 -0
  429. package/src/Markdown/plugins/semanticFence.test.tsx +546 -0
  430. package/src/Markdown/plugins/semanticFence.ts +424 -0
  431. package/src/Markdown/plugins/softBreaks.test.tsx +172 -0
  432. package/src/Markdown/plugins/softBreaks.ts +169 -0
  433. package/src/Markdown/plugins/sourceDecoration.test.tsx +799 -0
  434. package/src/Markdown/plugins/sourceDecoration.ts +539 -0
  435. package/src/Markdown/plugins/textTransform.test.ts +467 -0
  436. package/src/Markdown/plugins/textTransform.ts +554 -0
  437. package/src/Markdown/remark.test.tsx +1877 -0
  438. package/src/Markdown/remark.ts +1574 -0
  439. package/src/Markdown/url.ts +36 -0
  440. package/src/Markdown/utils.ts +13 -1
  441. package/src/MetadataList/MetadataList.test.tsx +4 -2
  442. package/src/MetadataList/MetadataList.tsx +3 -3
  443. package/src/MultiSelector/MultiSelector.test.tsx +1 -35
  444. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +128 -0
  445. package/src/NumberInput/NumberInput.tsx +6 -1
  446. package/src/Outline/Outline.doc.mjs +7 -5
  447. package/src/Outline/Outline.spec.md +1 -1
  448. package/src/Outline/index.ts +1 -0
  449. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
  450. package/src/Outline/parseOutlineFromMarkdown.ts +40 -10
  451. package/src/Outline/useOutlineFromMarkdown.ts +15 -3
  452. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  453. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
  454. package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
  455. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +51 -0
  456. package/src/PowerSearch/PowerSearchValueEditor.tsx +4 -0
  457. package/src/ProgressBar/ProgressBar.test.tsx +29 -11
  458. package/src/ProgressBar/ProgressBar.tsx +59 -72
  459. package/src/RadioList/RadioList.doc.mjs +11 -0
  460. package/src/RadioList/RadioList.test.tsx +32 -0
  461. package/src/RadioList/RadioListItem.tsx +25 -1
  462. package/src/ScrollableArea/ScrollableArea.tsx +1 -0
  463. package/src/ScrollableArea/modules/useScrollableArea.spec.md +50 -22
  464. package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
  465. package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
  466. package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
  467. package/src/Selector/Selector.doc.mjs +2 -2
  468. package/src/Selector/Selector.spec.md +51 -49
  469. package/src/Selector/Selector.test.tsx +61 -55
  470. package/src/Selector/Selector.tsx +34 -15
  471. package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +130 -0
  472. package/src/Selector/__tests__/Listbox.a11y.dom.ts +38 -0
  473. package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +63 -0
  474. package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +37 -0
  475. package/src/Selector/__tests__/Listbox.a11y.states.ts +232 -0
  476. package/src/Selector/__tests__/Selector.a11y.chromium.spec.ts +45 -0
  477. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +121 -0
  478. package/src/SideNav/SideNav.doc.mjs +1 -1
  479. package/src/SideNav/SideNav.test.tsx +10 -0
  480. package/src/SideNav/SideNav.tsx +14 -2
  481. package/src/Slider/Slider.doc.mjs +17 -3
  482. package/src/Slider/Slider.test.tsx +221 -0
  483. package/src/Slider/Slider.tsx +40 -9
  484. package/src/Spinner/Spinner.test.tsx +4 -0
  485. package/src/Spinner/Spinner.tsx +31 -18
  486. package/src/Switch/Switch.doc.mjs +11 -0
  487. package/src/Switch/Switch.test.tsx +16 -0
  488. package/src/Switch/Switch.tsx +28 -0
  489. package/src/TabList/Tab.tsx +35 -7
  490. package/src/TabList/TabList.doc.mjs +11 -0
  491. package/src/TabList/TabList.spec.md +204 -0
  492. package/src/TabList/TabList.test.tsx +66 -0
  493. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
  494. package/src/Table/Table.doc.mjs +1 -1
  495. package/src/Table/Table.spec.md +54 -40
  496. package/src/Table/Table.test.tsx +83 -10
  497. package/src/Table/Table.tsx +30 -22
  498. package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +137 -0
  499. package/src/TextArea/TextArea.tsx +6 -1
  500. package/src/TextInput/TextInput.test.tsx +20 -1
  501. package/src/TextInput/TextInput.tsx +6 -1
  502. package/src/TimeInput/TimeInput.tsx +6 -1
  503. package/src/Timer/Timer.doc.mjs +183 -0
  504. package/src/Timer/Timer.spec.md +215 -0
  505. package/src/Timer/Timer.test.tsx +302 -0
  506. package/src/Timer/Timer.tsx +258 -0
  507. package/src/Timer/index.ts +11 -0
  508. package/src/ToggleButton/ToggleButton.doc.mjs +3 -3
  509. package/src/ToggleButton/ToggleButton.test.tsx +299 -4
  510. package/src/ToggleButton/ToggleButton.tsx +26 -33
  511. package/src/ToggleButton/ToggleButtonGroup.doc.mjs +3 -3
  512. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
  513. package/src/Token/Token.doc.mjs +2 -2
  514. package/src/Tokenizer/Tokenizer.spec.md +142 -75
  515. package/src/TopNav/TopNavMenu.doc.mjs +2 -2
  516. package/src/TopNav/TopNavMenu.test.tsx +30 -0
  517. package/src/TopNav/TopNavMenu.tsx +7 -2
  518. package/src/TreeList/TreeList.doc.mjs +3 -3
  519. package/src/TreeList/TreeList.spec.md +123 -75
  520. package/src/TreeList/TreeList.test.tsx +29 -0
  521. package/src/TreeList/TreeList.tsx +3 -0
  522. package/src/TreeList/TreeListItem.tsx +15 -1
  523. package/src/TreeList/TreeListTypes.ts +11 -1
  524. package/src/Typeahead/BaseTypeahead.spec.md +4 -3
  525. package/src/Typeahead/BaseTypeahead.tsx +21 -7
  526. package/src/Typeahead/Typeahead.test.tsx +53 -0
  527. package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
  528. package/src/__tests__/pressState.ts +93 -0
  529. package/src/hooks/scrollKeyboardDelegation.test.ts +155 -0
  530. package/src/hooks/scrollKeyboardDelegation.ts +233 -0
  531. package/src/hooks/useClickableContainer.test.tsx +236 -0
  532. package/src/hooks/useClickableContainer.ts +20 -4
  533. package/src/hooks/useScrollableArea.doc.mjs +15 -3
  534. package/src/hooks/useScrollableArea.test.tsx +59 -1
  535. package/src/hooks/useScrollableArea.ts +34 -10
  536. package/src/index.ts +3 -0
  537. package/src/inputFontFloor.test.ts +106 -0
  538. package/src/tailwind-theme.css +0 -6
  539. package/src/theme/Theme.test.tsx +32 -0
  540. package/src/theme/declarationBoundary.test.ts +126 -0
  541. package/src/theme/declarationBoundary.ts +347 -0
  542. package/src/theme/derivedVarRegistry.test.ts +7 -0
  543. package/src/theme/derivedVarRegistry.ts +1 -0
  544. package/src/theme/generateThemeRules.test.ts +526 -1
  545. package/src/theme/generateThemeRules.ts +228 -50
  546. package/src/theme/tokenValueCompat.test.ts +4 -1
  547. package/src/utils/interactionOverlay.stylex.ts +20 -0
  548. package/src/utils/safeUrl.test.ts +88 -0
  549. package/src/utils/safeUrl.ts +85 -0
@@ -55,7 +55,7 @@ export const docs = {
55
55
  name: 'href',
56
56
  type: 'string',
57
57
  description:
58
- 'Link URL. When provided, the token renders as an <a> element.',
58
+ 'Link URL. When provided, the token renders as an <a> element. The destination follows the shared navigation rule described on the Link `href` prop.',
59
59
  },
60
60
  {
61
61
  name: 'description',
@@ -241,7 +241,7 @@ export const docsDense = {
241
241
  isDisabled: 'Reduces opacity, blocks interactions.',
242
242
  onRemove: 'Fired on remove button click. Renders X button when provided.',
243
243
  onClick: 'Click handler. Renders <span> w/ invisible <button> inside for a11y.',
244
- href: 'Link URL. Renders as <a> element.',
244
+ href: 'Link URL. Renders as <a> element. Follows the shared navigation rule (see Link href).',
245
245
  description: 'A11y description via aria-description on root.',
246
246
  endContent: 'Content after label, before remove button.',
247
247
  isLabelHidden: 'Visually hides label w/ screen-reader-only clip; stays accessible.',
@@ -7,7 +7,7 @@ authority: current
7
7
  archive_reason: null
8
8
  superseded_by: null
9
9
  approved_by: cixzhang
10
- approved_at: 2026-08-31
10
+ approved_at: 2026-09-19
11
11
  owners: [cixzhang]
12
12
  review_triggers: [public-api, behavior, layout, accessibility]
13
13
  verified_by:
@@ -32,6 +32,16 @@ system_specs: [spec:AST-002/DEC-1]
32
32
 
33
33
  # Tokenizer component contract
34
34
 
35
+ | Area | Contract |
36
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37
+ | Public contract | None. The existing `hasEntriesOnFocus` mode gains a defined post-selection lifecycle; no prop, type, export, or syntax changes. |
38
+ | Behavior | A committed empty-query bootstrap choice keeps remaining eligible loaded choices open, filters committed IDs, and advances the active option; terminal states close safely. |
39
+ | End-user impact | People selecting several loaded values can continue without reopening the menu, while disabled, loading, exhausted, and dismissed states remain non-interactive. |
40
+ | Builder impact | None. Existing opt-in callers need no migration or refetch, and typed-query selection remains unchanged. |
41
+ | Compatibility/readiness | The default `hasEntriesOnFocus={false}` path and direct single-select BaseTypeahead remain unchanged. This decision is current and owner-approved; implementation and exact-head interaction evidence remain pending in #6360. |
42
+ | Review checks | Reject implementations that filter by label instead of ID, reorder eligible choices, lose focus or a valid active descendant, refetch unnecessarily, or let disabled, exhausted, dismissed, or stale-loading states remain interactive or reopen. |
43
+ | Governing rules | This canonical `component:Tokenizer` record: [FR13–FR15](#behavioral-and-layout-contract), [AR5](#accessibility-contract), and [DEC-2](#dec-2--focus-bootstrap-supports-consecutive-committed-selections). |
44
+
35
45
  ## Intent
36
46
 
37
47
  Tokenizer lets a person build and edit a set of searchable values as removable
@@ -39,20 +49,26 @@ tokens around one stable combobox input. It owns the field surface, token/input
39
49
  layout, suggestion-menu relationship, and the package-internal identity needed to
40
50
  open a related surface from the control where the task began.
41
51
 
42
- This record preserves shipped Tokenizer facts and defines one package-internal
43
- opening-control descriptor for Core peers. The descriptor lets Layer prepare
44
- placement without exposing DOM refs, pointer coordinates, or width policy to
45
- consumers. The dependent [PowerSearch draft](https://github.com/facebook/astryx/pull/5804)
52
+ This record preserves shipped Tokenizer facts and defines two component-owned
53
+ contracts: one package-internal opening-control descriptor for Core peers, and the
54
+ post-selection lifecycle of an empty-query bootstrap menu. The descriptor lets
55
+ Layer prepare placement without exposing DOM refs, pointer coordinates, or width
56
+ policy to consumers. The dependent [PowerSearch draft](https://github.com/facebook/astryx/pull/5804)
46
57
  remains evidence for the need, not authority for this record.
47
58
 
48
59
  ## Compatibility and migration
49
60
 
50
- - Released default preserved: `yes`; this pull request changes documentation only.
51
- - Compatibility class: additive contract with no runtime, DOM, styling, target,
52
- declaration, or public API change.
53
- - Controlled/uncontrolled behavior: unchanged.
54
- - Migration decision: none. A later implementation must preserve the public
55
- Tokenizer API and its existing suggestion-menu sizing behavior.
61
+ - The public API and the default `hasEntriesOnFocus={false}` path are unchanged.
62
+ - Compatibility class: intentional behavior correction for the existing opt-in
63
+ `hasEntriesOnFocus` mode. A committed selection from its current empty-query
64
+ bootstrap cohort keeps the remaining eligible loaded choices available instead
65
+ of closing the menu after every choice.
66
+ - Controlled/uncontrolled ownership is unchanged. Retention follows committed
67
+ controlled values; a proposed value that the caller has not accepted remains
68
+ available.
69
+ - Migration decision: none. Existing callers that omit `hasEntriesOnFocus`, typed
70
+ search selection, explicit dismissal, disabled state, maximum-entry state, and
71
+ source-loading behavior retain their prior boundaries.
56
72
 
57
73
  Consumer migration instructions belong in consumer docs and release notes.
58
74
 
@@ -62,6 +78,9 @@ Consumer migration instructions belong in consumer docs and release notes.
62
78
 
63
79
  - The token collection, stable native search input, and painted outer field/control
64
80
  surface that contains them.
81
+ - The lifecycle of Tokenizer's own focus-bootstrapped suggestion menu after a
82
+ committed selection, including selected-value exclusion, next-option choice,
83
+ focus continuity, and terminal close states.
65
84
  - The active opening control for a task initiated within Tokenizer: the inner input
66
85
  for add/search work or the actual activated token/control for edit work.
67
86
  - The identity, usability, lifetime, and safe fallback semantics of those elements
@@ -72,6 +91,10 @@ Consumer migration instructions belong in consumer docs and release notes.
72
91
 
73
92
  **Does not own / non-goals**
74
93
 
94
+ - General BaseTypeahead selection policy. Direct single-select BaseTypeahead and
95
+ Typeahead keep their select-clear-close-refocus lifecycle.
96
+ - A new rule for typed-query selection or a requirement to refetch suggestions
97
+ after each Tokenizer selection.
75
98
  - General anchor positioning, top-layer hosting, viewport collision, generated
76
99
  anchor names, or prepared-placement lifecycle — owned by
77
100
  `architecture:layer-runtime`.
@@ -86,8 +109,9 @@ Consumer migration instructions belong in consumer docs and release notes.
86
109
 
87
110
  ## Public concepts
88
111
 
89
- No public concept is introduced. Existing consumer props and usage remain in
90
- `Tokenizer.doc.mjs`.
112
+ No public API concept is introduced. Existing consumer props and usage remain in
113
+ `Tokenizer.doc.mjs`. The existing `hasEntriesOnFocus` mode now includes the
114
+ component-owned post-selection behavior in FR13–FR15.
91
115
 
92
116
  The descriptor is package-internal. It must be absent from exported
93
117
  `TokenizerProps`, public barrels, generated declarations, consumer docs, CLI
@@ -100,20 +124,23 @@ Each requirement identifies its basis. This current record defines the approved
100
124
  package-internal contract while implementation and focused verification remain
101
125
  pending.
102
126
 
103
- | ID | Invariant | Basis | Status |
104
- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------- |
105
- | FR1 | Tokenizer MUST keep one native combobox input mounted across token addition, removal, clear, ordinary wrapping, and maximum-entry states. Reaching `maxEntries` may make the input zero-width, transparent, and out of flow, but MUST NOT replace its identity. | Current source and tests | Verified current behavior |
106
- | FR2 | Tokenizer's suggestion menu MUST remain associated with the outer field/control surface. Existing `menuWidth` behavior remains an exact preferred pixel width plus the outer field's minimum width; it is not a maximum. | Current BaseTypeahead/Tokenizer source and docs | Verified current behavior; preserve |
107
- | FR3 | `unfocusedInline` MAY replace direct token layout with OverflowList while blurred. `unfocusedLayer` MAY keep the real wrapper/input mounted in a context layer while an ordinary-flow placeholder represents the collapsed field. | Current source, tests, and stories | Verified current behavior |
108
- | FR4 | Tokenizer MUST provide Core peers one package-internal descriptor with stable identities for its outer field/control surface and active opening control. Layer consumes the descriptor; public consumers cannot observe or configure it. | Approved package-internal contract; #5804 evidence; `spec:AST-002/DEC-1` | Current contract; implementation pending |
109
- | FR5 | Add/search opens MUST identify the stable inner input as the opening control before the result row disappears. Selection, query clearing, result removal, refocus, or later input collapse MUST NOT substitute the transient result row or a pointer coordinate. | Current selection order plus approved descriptor contract | Current contract; implementation pending |
110
- | FR6 | Edit opens MUST capture the actual activated token/control before state removes or replaces it. The Tokenizer-owned capture path MUST include custom `renderToken` output when activation originates from an identifiable descendant control; it MUST NOT require a new public ref contract. | Current owned wrapper plus approved capture contract | Current contract; implementation pending |
111
- | FR7 | A non-null element is usable only while connected, rendered, and geometrically meaningful for the open being prepared. A disconnected, stale, hidden-layer, zero-width, or otherwise unusable opening control MUST fall back safely to the outer field at logical start before show. | Current max/overflow states plus approved safety rule | Current contract; implementation pending |
112
- | FR8 | The descriptor MUST remain valid across focus movement, token add/remove/clear, wrapping, and `unfocusedLayer` reparenting. It MUST clear on unmount, perform no DOM work during SSR, and never retain an element beyond the open preparation that uses it. | Approved identity/lifetime contract | Current contract; implementation pending |
113
- | FR9 | A consuming surface MUST resolve its preferred width before choosing horizontal placement. A surface at least as wide as the outer field MUST use outer-field logical-start alignment. Only a surface narrower than the outer field may use opening-control geometry. | Approved conditional-placement contract | Current contract; implementation pending |
114
- | FR10 | For a narrower surface, Layer MUST compare the opening control with the outer field's physical start/end geometry, choose the nearer physical edge, map that result to logical start/end, and use logical start on a tie. Viewport collision runs last. | Approved Layer-consumer contract | Current contract; implementation pending |
115
- | FR11 | Opening placement MUST be modality-neutral and latched for one open lifetime. Pointer, keyboard, touch, and programmatic activation of the same control MUST produce the same preferred edge. Pointer movement, later focus movement, resize, or target collapse MUST NOT recompute it. | `architecture:interaction-modality` plus approved stability contract | Current contract; implementation pending |
116
- | FR12 | Placement state MUST commit while the surface is hidden, and Layer MUST show it on the next animation frame only after anchor, alignment, and width styles exist. A stale or invalid descriptor MUST never cause a visible wrong-position or first-frame flash. | Approved no-flash contract | Current contract; implementation pending |
127
+ | ID | Invariant | Basis | Status |
128
+ | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------- |
129
+ | FR1 | Tokenizer MUST keep one native combobox input mounted across token addition, removal, clear, ordinary wrapping, and maximum-entry states. Reaching `maxEntries` may make the input zero-width, transparent, and out of flow, but MUST NOT replace its identity. | Current source and tests | Verified current behavior |
130
+ | FR2 | Tokenizer's suggestion menu MUST remain associated with the outer field/control surface. Existing `menuWidth` behavior remains an exact preferred pixel width plus the outer field's minimum width; it is not a maximum. | Current BaseTypeahead/Tokenizer source and docs | Verified current behavior; preserve |
131
+ | FR3 | `unfocusedInline` MAY replace direct token layout with OverflowList while blurred. `unfocusedLayer` MAY keep the real wrapper/input mounted in a context layer while an ordinary-flow placeholder represents the collapsed field. | Current source, tests, and stories | Verified current behavior |
132
+ | FR4 | Tokenizer MUST provide Core peers one package-internal descriptor with stable identities for its outer field/control surface and active opening control. Layer consumes the descriptor; public consumers cannot observe or configure it. | Approved package-internal contract; #5804 evidence; `spec:AST-002/DEC-1` | Current contract; implementation pending |
133
+ | FR5 | Add/search opens MUST identify the stable inner input as the opening control before the result row disappears. Selection, query clearing, result removal, refocus, or later input collapse MUST NOT substitute the transient result row or a pointer coordinate. | Current selection order plus approved descriptor contract | Current contract; implementation pending |
134
+ | FR6 | Edit opens MUST capture the actual activated token/control before state removes or replaces it. The Tokenizer-owned capture path MUST include custom `renderToken` output when activation originates from an identifiable descendant control; it MUST NOT require a new public ref contract. | Current owned wrapper plus approved capture contract | Current contract; implementation pending |
135
+ | FR7 | A non-null element is usable only while connected, rendered, and geometrically meaningful for the open being prepared. A disconnected, stale, hidden-layer, zero-width, or otherwise unusable opening control MUST fall back safely to the outer field at logical start before show. | Current max/overflow states plus approved safety rule | Current contract; implementation pending |
136
+ | FR8 | The descriptor MUST remain valid across focus movement, token add/remove/clear, wrapping, and `unfocusedLayer` reparenting. It MUST clear on unmount, perform no DOM work during SSR, and never retain an element beyond the open preparation that uses it. | Approved identity/lifetime contract | Current contract; implementation pending |
137
+ | FR9 | A consuming surface MUST resolve its preferred width before choosing horizontal placement. A surface at least as wide as the outer field MUST use outer-field logical-start alignment. Only a surface narrower than the outer field may use opening-control geometry. | Approved conditional-placement contract | Current contract; implementation pending |
138
+ | FR10 | For a narrower surface, Layer MUST compare the opening control with the outer field's physical start/end geometry, choose the nearer physical edge, map that result to logical start/end, and use logical start on a tie. Viewport collision runs last. | Approved Layer-consumer contract | Current contract; implementation pending |
139
+ | FR11 | Opening placement MUST be modality-neutral and latched for one open lifetime. Pointer, keyboard, touch, and programmatic activation of the same control MUST produce the same preferred edge. Pointer movement, later focus movement, resize, or target collapse MUST NOT recompute it. | `architecture:interaction-modality` plus approved stability contract | Current contract; implementation pending |
140
+ | FR12 | Placement state MUST commit while the surface is hidden, and Layer MUST show it on the next animation frame only after anchor, alignment, and width styles exist. A stale or invalid descriptor MUST never cause a visible wrong-position or first-frame flash. | Approved no-flash contract | Current contract; implementation pending |
141
+ | FR13 | Tokenizer MUST exclude every choice whose `SearchableItem.id` is already present in its committed `value` from displayed bootstrap and search cohorts, including results that settle after the controlled value changes. A caller-rejected proposed value remains eligible because it was not committed. Distinct IDs remain eligible even when their labels match. | Approved Tokenizer multi-selection contract | Current contract; implementation and verification pending |
142
+ | FR14 | With `hasEntriesOnFocus` enabled, committing a choice from the current settled empty-query bootstrap cohort MUST keep the input focused and the popup open when another eligible loaded choice remains. The active option advances to the next remaining choice in displayed order; when the committed choice was last, the nearest preceding choice becomes active. `aria-activedescendant` MUST reference that rendered option. | Approved rapid multi-select lifecycle | Current contract; implementation and verification pending |
143
+ | FR15 | The retained popup MUST close and clear its active descendant when no eligible loaded choice remains, `maxEntries` is reached, the control becomes disabled, focus leaves, or the person dismisses it. Disabled Tokenizer remains non-interactive. Pending or stale source work MUST NOT reopen a terminal popup or expose stale choices; loading has no selectable placeholder and its busy state clears when work is cancelled or settles. | Approved terminal-state safety boundary | Current contract; implementation and verification pending |
117
144
 
118
145
  ### Allowed variation
119
146
 
@@ -132,21 +159,30 @@ pending.
132
159
  - **AV5 — Responsive movement.** CSS may follow outer-field movement, container
133
160
  resize, writing direction, and collision while open. JavaScript does not
134
161
  remeasure or select a new preferred edge until the next open.
162
+ - **AV6 — Internal retention mechanism.** Tokenizer and BaseTypeahead may retain,
163
+ project, or otherwise coordinate the settled cohort through a package-private
164
+ mechanism. That mechanism must preserve FR13–FR15, must not become public API,
165
+ and must not change direct single-select BaseTypeahead behavior.
135
166
 
136
167
  ### Representative states
137
168
 
138
- | State | Required invariant | Allowed variation |
139
- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
140
- | Empty field | The mounted inner input is the opening control; a full-width menu stays aligned to the outer field. | Placeholder and bootstrap results may vary. |
141
- | One or many tokens | The same input remains after tokens and moves with wrapping; full-width placement does not follow that movement. | Token labels, wrapping rows, and custom content may vary. |
142
- | At `maxEntries` | The input identity remains mounted but its zero-width/hidden box is unusable as geometry; opening falls back to the outer field. | Search remains unavailable under current behavior. |
143
- | Token removal or clear | Input identity survives and current refocus behavior remains. | Token count and input position change. |
144
- | `unfocusedInline` | Blurred OverflowList and focused direct-token layouts resolve the same live input identity; a collapsed input is not used as geometry. | Visible token count may vary with width. |
145
- | `unfocusedLayer`, focused | The live wrapper/input may be reparented into the context layer without descriptor churn. | Corrective portal location may vary. |
146
- | `unfocusedLayer`, unfocused | The hidden-layer input is unusable for a new open; the ordinary-flow outer-field fallback is used. | Placeholder token count may vary. |
147
- | Default or custom token edit | The actual activated token/control is captured before edit state changes; if no usable descendant control exists, input then outer-field fallback applies. | Custom token markup may vary. |
148
- | LTR or RTL | Physical proximity selects an edge only for a narrower surface; logical start wins ties and full-width placement. | Collision may move the final rendered surface. |
149
- | SSR, hydration, or unmount | No server geometry read occurs; first client open prepares normally; unmount clears identities and pending show work. | Hydration timing may vary. |
169
+ | State | Required invariant | Allowed variation |
170
+ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
171
+ | Empty field | The mounted inner input is the opening control; a full-width menu stays aligned to the outer field. | Placeholder and bootstrap results may vary. |
172
+ | Focus bootstrap, choices remain | A committed selection is removed from the displayed cohort; the input and popup remain active on the next valid remaining choice. | Internal cohort storage and projection may vary. |
173
+ | Focus bootstrap, selected choice was last | The nearest preceding remaining choice becomes active without changing the displayed order. | Option identity representation may vary. |
174
+ | Focus bootstrap, no choices or max reached | The popup closes and no active descendant remains; pending work cannot reopen it. | Source cancellation mechanism may vary. |
175
+ | Source pending or replaced | Busy semantics identify pending work; stale work cannot add choices or reopen a terminal popup. | A settled current cohort may be retained internally. |
176
+ | Disabled | No choice can be committed; an open popup closes and clears its active descendant while disabled-reason focusability remains available where configured. | Native-disabled or focusable-disabled representation follows current Tokenizer behavior. |
177
+ | One or many tokens | The same input remains after tokens and moves with wrapping; full-width placement does not follow that movement. | Token labels, wrapping rows, and custom content may vary. |
178
+ | At `maxEntries` | The input identity remains mounted but its zero-width/hidden box is unusable as geometry; opening falls back to the outer field. | Search remains unavailable under current behavior. |
179
+ | Token removal or clear | Input identity survives and current refocus behavior remains. | Token count and input position change. |
180
+ | `unfocusedInline` | Blurred OverflowList and focused direct-token layouts resolve the same live input identity; a collapsed input is not used as geometry. | Visible token count may vary with width. |
181
+ | `unfocusedLayer`, focused | The live wrapper/input may be reparented into the context layer without descriptor churn. | Corrective portal location may vary. |
182
+ | `unfocusedLayer`, unfocused | The hidden-layer input is unusable for a new open; the ordinary-flow outer-field fallback is used. | Placeholder token count may vary. |
183
+ | Default or custom token edit | The actual activated token/control is captured before edit state changes; if no usable descendant control exists, input then outer-field fallback applies. | Custom token markup may vary. |
184
+ | LTR or RTL | Physical proximity selects an edge only for a narrower surface; logical start wins ties and full-width placement. | Collision may move the final rendered surface. |
185
+ | SSR, hydration, or unmount | No server geometry read occurs; first client open prepares normally; unmount clears identities and pending show work. | Hydration timing may vary. |
150
186
 
151
187
  ### Transformation and precedence order
152
188
 
@@ -195,17 +231,22 @@ pending.
195
231
  - **AR4 — No visible placement error.** A person MUST NOT see a surface first
196
232
  paint at an outer-start or stale position and then jump to its resolved edge.
197
233
  Invalid identity falls back before show rather than flashing or showing wrong.
234
+ - **AR5 — Retained combobox semantics.** While a focus-bootstrapped menu remains
235
+ open after selection, DOM focus MUST stay on the combobox, `aria-expanded` MUST
236
+ remain true, and `aria-activedescendant` MUST name the rendered next valid
237
+ option. Closing for dismissal, disabled, maximum, or exhaustion MUST remove the
238
+ active descendant. Selected and disabled options are never interactive.
198
239
 
199
240
  ## Design relationships
200
241
 
201
- | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
202
- | --------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------- | -------------- | ------------------ |
203
- | Outer field/control surface | Contains tokens, input, and end controls; remains the full-width placement and safe fallback anchor. | Current Tokenizer source | Prominent | FR1–FR4, FR7, FR9 |
204
- | Token chips | Present selected values and removal; an activated token/control may become the per-open opening control. | Token component or custom renderer | Prominent | FR6, FR8, AR2–AR3 |
205
- | Search input | Remains one mounted combobox node and is the add/search opening control while geometrically usable. | Current Tokenizer/BaseTypeahead source | Prominent | FR1, FR5, FR7–FR8 |
206
- | Suggestion menu | Retains current outer-field anchoring and exact preferred `menuWidth` plus outer minimum. | Current BaseTypeahead source | Prominent | FR2, FR9 |
207
- | Overflow placeholder/layer | Preserves a usable outer-field fallback while live content may reparent or collapse. | Current Tokenizer/Layer composition | Supporting | FR3, FR7–FR8 |
208
- | Dependent narrower surface | Uses the descriptor only after its owning component resolves a width narrower than the field. | Consuming component plus Layer | Prominent | FR9–FR12 |
242
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
243
+ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | -------------- | ------------------- |
244
+ | Outer field/control surface | Contains tokens, input, and end controls; remains the full-width placement and safe fallback anchor. | Current Tokenizer source | Prominent | FR1–FR4, FR7, FR9 |
245
+ | Token chips | Present selected values and removal; an activated token/control may become the per-open opening control. | Token component or custom renderer | Prominent | FR6, FR8, AR2–AR3 |
246
+ | Search input | Remains one mounted combobox node and is the add/search opening control while geometrically usable. | Current Tokenizer/BaseTypeahead source | Prominent | FR1, FR5, FR7–FR8 |
247
+ | Suggestion menu | Retains current outer-field anchoring and exact preferred `menuWidth`; focus bootstrap keeps only eligible choices and a valid active option. | Current BaseTypeahead source | Prominent | FR2, FR13–FR15, AR5 |
248
+ | Overflow placeholder/layer | Preserves a usable outer-field fallback while live content may reparent or collapse. | Current Tokenizer/Layer composition | Supporting | FR3, FR7–FR8 |
249
+ | Dependent narrower surface | Uses the descriptor only after its owning component resolves a width narrower than the field. | Consuming component plus Layer | Prominent | FR9–FR12 |
209
250
 
210
251
  This contract adds no target, state, or visual styling contract.
211
252
 
@@ -231,34 +272,40 @@ This contract adds no target, state, or visual styling contract.
231
272
  descriptor and owns its own editor width policy. It is dependent draft evidence,
232
273
  not authority for Tokenizer.
233
274
 
234
- ### Required implementation and visual evidence
235
-
236
- This spec-only pull request intentionally changes no source, public docs, stories,
237
- or Changeset. When the descriptor and Layer consumer ship, that implementation
238
- must update the owning source/tests/docs and add screenshotable open-state stories:
239
-
240
- 1. Tokenizer's full-width suggestion menu open with an empty field and with tokens;
241
- both remain aligned to the outer field.
242
- 2. The menu open with wrapped tokens and through `unfocusedLayer` reparenting;
243
- descriptor identity and safe fallback remain stable.
244
- 3. The menu open in RTL and a constrained viewport; logical alignment and final
245
- collision remain correct.
246
-
247
- The visual harness must capture the surface already open, including the first
248
- paint. Instrumented verification must prove no first-frame flash and exactly one
249
- outer-plus-opening geometry read batch per open. PowerSearch's separate evidence
250
- owns its 400–720 editor states; those dimensions never become Tokenizer policy.
275
+ ### Required implementation and browser evidence
276
+
277
+ The post-selection lifecycle must update the owning source, focused tests,
278
+ consumer docs, and Changeset together. Exact-head browser evidence must exercise:
279
+
280
+ 1. Pointer and keyboard selection from a settled focus-bootstrap cohort, with the
281
+ committed value removed, the popup still open, input focus preserved, and the
282
+ next remaining option active.
283
+ 2. The fallback when the committed option was last, plus terminal closure for no
284
+ remaining choice and `maxEntries`.
285
+ 3. Escape, Tab or outside focus departure, transition to native-disabled and
286
+ focusable-disabled states, and pending or stale source settlement without a
287
+ later reopen.
288
+ 4. The unchanged path when `hasEntriesOnFocus` is off and direct single-select
289
+ BaseTypeahead selection still closes.
290
+
291
+ The separate opening-control descriptor remains pending. When that descriptor and
292
+ its Layer consumer ship, they must add screenshotable full/equal/narrow-width,
293
+ wrapped-token, overflow-layer, LTR/RTL, collision, stale-fallback, and first-paint
294
+ evidence. Instrumented verification must prove one geometry-read batch per open.
295
+ PowerSearch's separate evidence owns its editor dimensions; those dimensions never
296
+ become Tokenizer policy.
251
297
 
252
298
  ## Verification map
253
299
 
254
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
255
- | ---------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------- |
256
- | FR1–FR3 | Existing Tokenizer/BaseTypeahead unit tests plus source inspection | empty, tokens, remove, clear, max, wrapping, both overflow modes | Input remounts, suggestion menu changes anchor/width semantics, or overflow ownership is misstated. | `audit:Tokenizer/current` |
257
- | FR4–FR8 | Focused descriptor identity/capture tests when implemented | add, default/custom token edit, max, hidden layer, reparent, stale node, unmount, SSR | Descriptor leaks publicly, captures a transient result/pointer, misses custom controls, or uses unusable geometry. | `audit:Tokenizer/opening-anchor` |
258
- | FR9–FR12 | Layer unit tests and real-Chromium geometry checks when implemented | full/equal/narrow surface, start/end/tie, LTR/RTL, collision, stale fallback | Full-width follows inner input, tie is physical, collision runs early, or stale identity flashes. | `audit:Tokenizer/placement` |
259
- | PR1–PR5 | Instrumented reads, pending-frame cleanup tests, and first-frame Chromium capture | open, close before frame, reopen opposite side, resize while open | Render-time/repeated reads, observer/pointer tracking, uncancelled frame, or first-frame jump occurs. | `audit:Tokenizer/performance` |
260
- | AR1–AR4 | Existing interaction suites plus focused pointer/keyboard/browser checks | add/edit, focus/refocus, overflow, disabled, dismissal | Modality changes placement, semantics/focus regress, or visible placement is wrong before fallback. | `audit:Tokenizer/accessibility` |
261
- | Record structure | `scripts/check-knowledge.mjs` | Template-v3 current record and current-only frontmatter links | Invalid metadata, authority, or relationship references fail repository validation. | `audit:Tokenizer/knowledge` |
300
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
301
+ | ---------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
302
+ | FR1–FR3 | Existing Tokenizer/BaseTypeahead unit tests plus source inspection | empty, tokens, remove, clear, max, wrapping, both overflow modes | Input remounts, suggestion menu changes anchor/width semantics, or overflow ownership is misstated. | `audit:Tokenizer/current` |
303
+ | FR4–FR8 | Focused descriptor identity/capture tests when implemented | add, default/custom token edit, max, hidden layer, reparent, stale node, unmount, SSR | Descriptor leaks publicly, captures a transient result/pointer, misses custom controls, or uses unusable geometry. | `audit:Tokenizer/opening-anchor` |
304
+ | FR9–FR12 | Layer unit tests and real-Chromium geometry checks when implemented | full/equal/narrow surface, start/end/tie, LTR/RTL, collision, stale fallback | Full-width follows inner input, tie is physical, collision runs early, or stale identity flashes. | `audit:Tokenizer/placement` |
305
+ | FR13–FR15, AR5 | Focused Tokenizer/BaseTypeahead tests and exact-head Chromium interaction evidence | pointer/keyboard commit, first/middle/last choice, rejected/deferred control, loading, disabled, max, exhaustion, Escape, Tab, outside focus | Selected values remain available, order changes, focus leaves the combobox, active descendant names no option, stale work reopens, or a terminal state stays interactive. | `audit:Tokenizer/multi-select` |
306
+ | PR1–PR5 | Instrumented reads, pending-frame cleanup tests, and first-frame Chromium capture | open, close before frame, reopen opposite side, resize while open | Render-time/repeated reads, observer/pointer tracking, uncancelled frame, or first-frame jump occurs. | `audit:Tokenizer/performance` |
307
+ | AR1–AR5 | Existing interaction suites plus focused pointer/keyboard/browser checks | add/edit, focus/refocus, overflow, disabled, dismissal, retained bootstrap selection | Modality changes placement, semantics/focus regress, active descendant becomes invalid, or visible placement is wrong before fallback. | `audit:Tokenizer/accessibility` |
308
+ | Record structure | `scripts/check-knowledge.mjs` | Template-v3 current record and current-only frontmatter links | Invalid metadata, authority, or relationship references fail repository validation. | `audit:Tokenizer/knowledge` |
262
309
 
263
310
  ## Decision log
264
311
 
@@ -281,13 +328,33 @@ remains unchanged. Implementation must provide real-Chromium open-state evidence
281
328
  for full-width and narrower placement, LTR and RTL, constrained collision, stale
282
329
  fallback, and the first paint without a visible jump.
283
330
 
331
+ ### DEC-2 — Focus bootstrap supports consecutive committed selections
332
+
333
+ **Reference:** `component:Tokenizer/DEC-2`
334
+ **Decider:** `cixzhang`, `2026-09-19`
335
+
336
+ When `hasEntriesOnFocus` is enabled, committing a choice from the current settled
337
+ empty-query bootstrap cohort keeps the popup open with the remaining eligible
338
+ loaded choices. Committed values are excluded by `SearchableItem.id`; distinct
339
+ items with the same label remain eligible. The option after the committed choice
340
+ in displayed order becomes active; when there is no following choice, the nearest preceding choice becomes active. Input focus and valid combobox/listbox
341
+ ARIA relationships remain intact.
342
+
343
+ The popup closes and clears its active descendant when no eligible loaded choice
344
+ remains, `maxEntries` is reached, the control becomes disabled, focus leaves, or
345
+ the person dismisses it. Pending or stale source work cannot reopen a terminal
346
+ popup or make stale choices interactive. This decision adds no public API, does
347
+ not require a refetch, and does not change typed-query selection or direct
348
+ single-select BaseTypeahead behavior.
349
+
284
350
  ## Open questions
285
351
 
286
- None. Implementation and focused verification remain pending.
352
+ None. The opening-control descriptor implementation remains pending; the
353
+ post-selection lifecycle has no unresolved decision.
287
354
 
288
355
  ## Content boundary
289
356
 
290
357
  This file does not duplicate consumer prop tables, usage examples, search
291
358
  algorithms, implementation code, current audit results, PowerSearch editor sizing,
292
- or shared Layer mechanics. Public docs, stories, and release notes change only
293
- when implementation ships.
359
+ or shared Layer mechanics. Consumer docs and release notes project only the
360
+ approved component behavior.
@@ -27,7 +27,7 @@ export const docs = {
27
27
  {
28
28
  name: 'items',
29
29
  type: 'TopNavMenuItemData[]',
30
- description: 'Menu items to display in the hover popover.',
30
+ description: 'Menu items to display in the hover popover. Item href values follow the shared navigation rule described on Link; rejected destinations remain visible without navigation.',
31
31
  required: true,
32
32
  },
33
33
  {
@@ -83,7 +83,7 @@ export const docsDense = {
83
83
  description: 'Nav item w/ hover-triggered popover menu containing rich items w/ icon, title, optional description.',
84
84
  propDescriptions: {
85
85
  label: 'Trigger button visible label.',
86
- items: 'Menu items in hover popover.',
86
+ items: 'Menu items in hover popover. href follows the Link navigation rule; rejected destinations do not navigate.',
87
87
  delay: 'Show delay ms on hover.',
88
88
  hideDelay: 'Hide delay ms after mouse leaves.',
89
89
  },
@@ -31,6 +31,36 @@ const mockItems = [
31
31
  ];
32
32
 
33
33
  describe('TopNavMenu', () => {
34
+ it.each([
35
+ 'javascript:alert(1)',
36
+ 'vbscript:MsgBox(1)',
37
+ 'data:text/html,<b>x</b>',
38
+ ])('does not expose rejected menu destination %s', href => {
39
+ const onClick = vi.fn();
40
+ render(
41
+ <TopNavMenu
42
+ label="Menu"
43
+ items={[{title: 'Destination', href, onClick}]}
44
+ />,
45
+ );
46
+ const item = screen.getByRole('menuitem', {hidden: true});
47
+ expect(item).not.toHaveAttribute('href');
48
+ fireEvent.click(item);
49
+ expect(onClick).toHaveBeenCalledOnce();
50
+ });
51
+
52
+ it('preserves an accepted native menu destination', () => {
53
+ render(
54
+ <TopNavMenu
55
+ label="Menu"
56
+ items={[{title: 'Destination', href: '/docs'}]}
57
+ />,
58
+ );
59
+ expect(screen.getByRole('menuitem', {hidden: true})).toHaveAttribute(
60
+ 'href',
61
+ '/docs',
62
+ );
63
+ });
34
64
  it('renders the trigger button with label', () => {
35
65
  render(<TopNavMenu label="Products" items={mockItems} />);
36
66
  expect(screen.getByRole('button', {name: 'Products'})).toBeInTheDocument();
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * @file TopNavMenu.tsx
7
7
  * @input Uses React, StyleX, usePopover, useMenuHover, useListFocus,
8
- * useTypeahead, TopNavItem tokens
8
+ * useTypeahead, TopNavItem tokens, and the shared navigation policy
9
9
  * @output Exports TopNavMenu component and related types
10
10
  * @position Navigation item with hover-triggered overflow menu for TopNav
11
11
  *
@@ -37,6 +37,7 @@ import {useTopNavSlot} from './TopNavContext';
37
37
  import {useTopNavRenderMode} from './TopNavRenderContext';
38
38
  import {useAppShellMobile} from '../AppShell/AppShellMobileContext';
39
39
  import {useLinkComponent} from '../Link/useLinkComponent';
40
+ import {isSafeUrl} from '../utils/safeUrl';
40
41
  import {themeProps} from '../utils/themeProps';
41
42
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
42
43
  import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
@@ -575,7 +576,11 @@ export function TopNavMenu({
575
576
  // Single tab stop: useListFocus owns the roving tabindex and
576
577
  // promotes exactly one item to 0.
577
578
  tabIndex={-1}
578
- href={item.href}
579
+ href={
580
+ item.href != null && isSafeUrl(item.href)
581
+ ? item.href
582
+ : undefined
583
+ }
579
584
  onClick={item.onClick}
580
585
  {...focusOutlineProps.focusVisible(styles.menuItem)}>
581
586
  <div {...stylex.props(styles.menuItemIcon)}>{item.icon}</div>
@@ -101,7 +101,7 @@ export const docs = {
101
101
  name: 'items',
102
102
  type: 'TreeListItemData[]',
103
103
  description:
104
- 'Recursive tree item data. Each item has id, label, optional children array, and optional isExpanded boolean for initial state.',
104
+ 'Recursive tree item data with id, label, optional children and isExpanded, plus optional xstyle, className, and style applied to that item row.',
105
105
  required: true,
106
106
  },
107
107
  {
@@ -244,7 +244,7 @@ export const docsDense = {
244
244
  ],
245
245
  },
246
246
  propDescriptions: {
247
- items: 'Recursive tree item data w/ id, label, optional children + isExpanded.',
247
+ items: 'Recursive tree item data w/ id, label, optional children + isExpanded, plus optional xstyle/className/style on its row.',
248
248
  density: 'Spacing density for items.',
249
249
  variant: 'Guide-line treatment: lineGuides shows connectors, noGuides hides them (indent kept). Orthogonal to density.',
250
250
  header: 'Header content, linked to tree via aria-labelledby.',
@@ -256,7 +256,7 @@ export const docsDense = {
256
256
  displayName: 'Tree List',
257
257
  description: 'Tree list container. Accepts items data + rendering config. Expansion managed internally.',
258
258
  propDescriptions: {
259
- items: 'Recursive tree item data w/ id, label, optional children + isExpanded.',
259
+ items: 'Recursive tree item data w/ id, label, optional children + isExpanded, plus optional xstyle/className/style on its row.',
260
260
  density: 'Spacing density for items.',
261
261
  variant: 'Guide-line treatment: lineGuides shows connectors, noGuides hides them (indent kept). Orthogonal to density.',
262
262
  header: 'Header content, linked to tree via aria-labelledby.',