@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
@@ -0,0 +1,514 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file ChatLayoutScrollButton.a11y.chromium.spec.ts
5
+ * @input Uses the ChatLayout scroll-affordance story and a built Storybook
6
+ * @output Keyboard and pixel evidence for the hidden/visible/re-hidden scroll
7
+ * affordance, and for the pill tracking the theme's element-size token
8
+ * @position Real-Chromium evidence for the 2026-09-23 ChatLayout audit. jsdom
9
+ * has no cascade and no sequential focus model, so neither the hidden state's
10
+ * removal from the tab order nor the pill's containment of its own Button is
11
+ * observable in the unit lane.
12
+ *
13
+ * Two claims are proven here, both against the shipped public API:
14
+ *
15
+ * 1. The default affordance is hidden at rest, becomes visible when the reader
16
+ * scrolls away from the newest message, and hides again on activation. It is
17
+ * keyboard reachable exactly while it is visible, because focus landing on
18
+ * something that paints nothing has no visible indicator (WCAG 2.2 SC 2.4.7).
19
+ * 2. The pill's height follows `--size-element-md`, so a theme that retunes the
20
+ * element scale cannot make it clip the Button it wraps. Butter resolves that
21
+ * token to 40px where Neutral resolves it to 32px, so the two shipped themes
22
+ * are the natural before/after — no hand-built arm is needed for the token
23
+ * claim. The regression arm below restores the old literal to show what the
24
+ * fix prevents, and declares its exact delta.
25
+ */
26
+
27
+ import {createHash} from 'node:crypto';
28
+ import * as fs from 'node:fs';
29
+ import * as path from 'node:path';
30
+ import {expect, test, type Locator, type Page} from '@playwright/test';
31
+ import {holdMotionStill} from '@astryxdesign/a11y-spec/chromium';
32
+ import {
33
+ DEFAULT_STORYBOOK_DIR,
34
+ serveStorybook,
35
+ type StaticServer,
36
+ } from '@astryxdesign/a11y-spec/storybook';
37
+
38
+ const OUTPUT = path.resolve('test-results/chat-layout-audit-evidence');
39
+ const STORY_ID = 'core-chatlayout--scroll-affordance-states';
40
+
41
+ /**
42
+ * The pill: the element wrapping the Button, whose visibility and height are
43
+ * the claims below. It also carries the public theming target, so the class is
44
+ * a stable handle for it.
45
+ */
46
+ const PILL = '.astryx-chat-layout-scroll-button';
47
+
48
+ /** One observed tab stop, identified structurally rather than by its text. */
49
+ type FocusStop = {
50
+ tag: string;
51
+ astryxClass: string | null;
52
+ role: string | null;
53
+ tabIndex: string | null;
54
+ accessibleName: string | null;
55
+ isLayoutRoot: boolean;
56
+ };
57
+
58
+ type Shot = {
59
+ file: string;
60
+ sha256: string;
61
+ width: number;
62
+ height: number;
63
+ };
64
+
65
+ type Receipt = {
66
+ /** What this frame is evidence of. */
67
+ state: string;
68
+ storyId: string;
69
+ theme: string;
70
+ colorMode: string;
71
+ direction: string;
72
+ viewport: {width: number; height: number};
73
+ devicePixelRatio: number;
74
+ /** Semantic state that uniquely identifies the intended render. */
75
+ rendered: Record<string, unknown>;
76
+ /** Visible subject geometry, so a zero-box decoy cannot pass as the subject. */
77
+ geometry: {selectorCount: number; width: number; height: number};
78
+ settled: {fontsReady: boolean; pageErrors: number};
79
+ image: Shot;
80
+ };
81
+
82
+ const receipts: Receipt[] = [];
83
+ const pageErrors: string[] = [];
84
+ let storybook: StaticServer;
85
+ let browserVersion = 'unknown';
86
+
87
+ test.beforeAll(async () => {
88
+ // Do NOT wipe: Playwright restarts the worker after a failing test, which
89
+ // re-runs this hook. Wiping here would delete the frames the earlier tests
90
+ // already banked, which is exactly when the evidence matters most.
91
+ fs.mkdirSync(OUTPUT, {recursive: true});
92
+ storybook = await serveStorybook(
93
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
94
+ );
95
+ });
96
+
97
+ test.afterAll(async () => {
98
+ const manifestPath = path.join(OUTPUT, 'manifest.json');
99
+ const previous = fs.existsSync(manifestPath)
100
+ ? (JSON.parse(fs.readFileSync(manifestPath, 'utf8')).frames ?? [])
101
+ : [];
102
+ const merged = [...previous, ...receipts].filter(
103
+ (frame, index, all) =>
104
+ all.findIndex(other => other.state === frame.state) === index,
105
+ );
106
+ fs.writeFileSync(
107
+ manifestPath,
108
+ `${JSON.stringify(
109
+ {
110
+ version: 1,
111
+ component: 'core/ChatLayout',
112
+ headSha:
113
+ process.env.ASTRYX_HEAD_SHA ??
114
+ process.env.GITHUB_SHA ??
115
+ 'local-working-copy',
116
+ checkoutSha: process.env.GITHUB_SHA ?? 'local-working-copy',
117
+ browser: browserVersion,
118
+ frames: merged,
119
+ },
120
+ null,
121
+ 2,
122
+ )}\n`,
123
+ );
124
+ await storybook?.close();
125
+ });
126
+
127
+ async function openStory(
128
+ page: Page,
129
+ {theme = 'neutral', colorMode = 'light'} = {},
130
+ ): Promise<Locator> {
131
+ page.on('pageerror', error => pageErrors.push(String(error)));
132
+ browserVersion = page.context().browser()?.version() ?? 'unknown';
133
+ await page.goto(
134
+ `${storybook.origin}/iframe.html?id=${STORY_ID}&viewMode=story` +
135
+ `&globals=colorMode:${colorMode};astryxTheme:${theme}`,
136
+ );
137
+ const root = page.locator('.astryx-chat-layout');
138
+ await root.waitFor({state: 'visible'});
139
+ await holdMotionStill(page);
140
+ await page.evaluate(async () => document.fonts.ready);
141
+ return root;
142
+ }
143
+
144
+ /** The scroll container settles at the bottom on first fill. */
145
+ async function distanceFromBottom(root: Locator): Promise<number> {
146
+ return root.evaluate(
147
+ element => element.scrollHeight - element.clientHeight - element.scrollTop,
148
+ );
149
+ }
150
+
151
+ /** How far this fixture can scroll at all. The states below need real range. */
152
+ async function scrollRange(root: Locator): Promise<number> {
153
+ return root.evaluate(element => element.scrollHeight - element.clientHeight);
154
+ }
155
+
156
+ /** Scroll to the top, which is the furthest the reader can get from newest. */
157
+ async function scrollToTop(root: Locator): Promise<void> {
158
+ await root.evaluate(element => {
159
+ element.scrollTop = 0;
160
+ element.dispatchEvent(new Event('scroll'));
161
+ });
162
+ }
163
+
164
+ /** Computed visibility of the pill — the state every claim below turns on. */
165
+ async function pillVisibility(page: Page): Promise<string> {
166
+ return page
167
+ .locator(PILL)
168
+ .evaluate(element => getComputedStyle(element).visibility);
169
+ }
170
+
171
+ /**
172
+ * Can the affordance take focus at all? This is the mechanism under test:
173
+ * `visibility: hidden` removes an element from sequential focus navigation AND
174
+ * refuses programmatic focus, which `opacity: 0` does neither of. A CSS locator
175
+ * is used rather than a role query because a hidden button is not in the
176
+ * accessibility tree and a role query would not resolve it.
177
+ */
178
+ async function affordanceAcceptsFocus(page: Page): Promise<boolean> {
179
+ return page
180
+ .locator('.astryx-chat-layout-scroll-button button')
181
+ .evaluate(element => {
182
+ // preventScroll: focusing must not move the scroll position, or the
183
+ // probe would change the very state the next assertion reads.
184
+ (element as HTMLElement).focus({preventScroll: true});
185
+ return document.activeElement === element;
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Tab forward from `from` and report where focus actually lands, by identity.
191
+ *
192
+ * Each stop records the element's tag, its `astryx-*` class, explicit role,
193
+ * explicit tabindex, accessible name, and whether it IS the layout root — not
194
+ * a text-content guess. Several nested elements share the transcript's text,
195
+ * so a label built from `textContent` cannot tell the scrolling layout root
196
+ * from the `role="log"` message list inside it.
197
+ *
198
+ * The sweep MUST stop at the layout boundary. Sequential focus navigation
199
+ * scrolls each stop into view, and this affordance's visibility is a function
200
+ * of scroll position, so a sweep that runs past the last stop wraps around the
201
+ * document, re-enters the layout, and can flip the state it is measuring.
202
+ */
203
+ /** Render a sweep as a readable trail for a failure message. */
204
+ function describeStops(stops: FocusStop[]): string {
205
+ return stops
206
+ .map(stop => {
207
+ const parts = [stop.tag];
208
+ if (stop.astryxClass != null) {
209
+ parts.push(`.${stop.astryxClass}`);
210
+ }
211
+ if (stop.role != null) {
212
+ parts.push(`[role=${stop.role}]`);
213
+ }
214
+ if (stop.isLayoutRoot) {
215
+ parts.push('(layout root)');
216
+ }
217
+ return parts.join('');
218
+ })
219
+ .join(' -> ');
220
+ }
221
+
222
+ async function tabWithinLayout(
223
+ page: Page,
224
+ from: Locator,
225
+ maxPresses: number,
226
+ ): Promise<{reached: boolean; sequence: FocusStop[]}> {
227
+ await from.focus();
228
+ const sequence: FocusStop[] = [];
229
+ for (let index = 0; index < maxPresses; index += 1) {
230
+ await page.keyboard.press('Tab');
231
+ const stop = await page.evaluate(() => {
232
+ const active = document.activeElement as HTMLElement | null;
233
+ const layout = document.querySelector('.astryx-chat-layout');
234
+ if (active == null || active === document.body) {
235
+ return {
236
+ tag: 'body',
237
+ astryxClass: null,
238
+ role: null,
239
+ tabIndex: null,
240
+ accessibleName: null,
241
+ isLayoutRoot: false,
242
+ inside: false,
243
+ onAffordance: false,
244
+ };
245
+ }
246
+ return {
247
+ tag: active.tagName.toLowerCase(),
248
+ astryxClass:
249
+ [...active.classList].find(name => name.startsWith('astryx-')) ??
250
+ null,
251
+ role: active.getAttribute('role'),
252
+ tabIndex: active.getAttribute('tabindex'),
253
+ accessibleName:
254
+ active.getAttribute('aria-label') ??
255
+ active.getAttribute('aria-labelledby') ??
256
+ null,
257
+ isLayoutRoot: active === layout,
258
+ inside: layout != null && layout.contains(active),
259
+ onAffordance:
260
+ active.closest('.astryx-chat-layout-scroll-button') != null,
261
+ };
262
+ });
263
+ const {inside, onAffordance, ...identity} = stop;
264
+ sequence.push(identity);
265
+ if (onAffordance) {
266
+ return {reached: true, sequence};
267
+ }
268
+ if (!inside) {
269
+ break;
270
+ }
271
+ }
272
+ return {reached: false, sequence};
273
+ }
274
+
275
+ /**
276
+ * Read a PNG's real pixel dimensions out of its IHDR header.
277
+ *
278
+ * These frames are ELEMENT screenshots, so their size is the subject's box,
279
+ * not the viewport's. An earlier revision recorded the viewport here and
280
+ * published receipts claiming 1280x720 for images that are 1216x420 — the
281
+ * pixels were genuine, the stated dimensions were not. The viewport stays in
282
+ * its own sensor row, where it belongs.
283
+ */
284
+ function pngShot(file: string, bytes: Buffer): Shot {
285
+ return {
286
+ file,
287
+ sha256: createHash('sha256').update(bytes).digest('hex'),
288
+ width: bytes.readUInt32BE(16),
289
+ height: bytes.readUInt32BE(20),
290
+ };
291
+ }
292
+
293
+ async function capture(
294
+ page: Page,
295
+ state: string,
296
+ rendered: Record<string, unknown>,
297
+ ) {
298
+ const pill = page.locator(PILL);
299
+ const box = await pill.boundingBox();
300
+ if (box == null) {
301
+ throw new Error(`${state}: the pill has no layout box`);
302
+ }
303
+ const bytes = await page.locator('.astryx-chat-layout').screenshot({
304
+ animations: 'disabled',
305
+ });
306
+ const file = `chat-layout-${state}.png`;
307
+ fs.writeFileSync(path.join(OUTPUT, file), bytes);
308
+ const dimensions = await page.evaluate(() => ({
309
+ width: window.innerWidth,
310
+ height: window.innerHeight,
311
+ dpr: window.devicePixelRatio,
312
+ }));
313
+ const receipt: Receipt = {
314
+ state,
315
+ storyId: STORY_ID,
316
+ theme: await page.evaluate(
317
+ () =>
318
+ document
319
+ .querySelector('[data-astryx-theme]')
320
+ ?.getAttribute('data-astryx-theme') ?? 'neutral',
321
+ ),
322
+ colorMode: await page.evaluate(
323
+ () => getComputedStyle(document.documentElement).colorScheme,
324
+ ),
325
+ direction: await page
326
+ .locator('.astryx-chat-layout')
327
+ .evaluate(element => getComputedStyle(element).direction),
328
+ viewport: {width: dimensions.width, height: dimensions.height},
329
+ devicePixelRatio: dimensions.dpr,
330
+ rendered,
331
+ geometry: {
332
+ selectorCount: await page.locator(PILL).count(),
333
+ width: Math.round(box.width),
334
+ height: Math.round(box.height),
335
+ },
336
+ settled: {fontsReady: true, pageErrors: pageErrors.length},
337
+ image: pngShot(file, bytes),
338
+ };
339
+ receipts.push(receipt);
340
+ return receipt;
341
+ }
342
+
343
+ test('the scroll affordance is keyboard reachable exactly while it is visible', async ({
344
+ page,
345
+ }) => {
346
+ const root = await openStory(page);
347
+ const before = page.getByRole('button', {name: 'Before chat', exact: true});
348
+ const pill = page.locator(PILL);
349
+ const SWEEP = 6;
350
+
351
+ // Order matters in every block below: assert the state, probe focus with
352
+ // preventScroll, capture the frame, and only THEN walk Tab. Sequential focus
353
+ // navigation scrolls each stop into view, and this affordance's visibility is
354
+ // a function of scroll position — measured, not assumed: an earlier revision
355
+ // swept first and photographed a "hidden" state that Tab had already scrolled
356
+ // 174px away from the bottom and made visible. The sweep is recorded as
357
+ // evidence of the real tab order; the assertion is the focus probe, because a
358
+ // button that refuses focus() is not in the sequential tab order.
359
+
360
+ // ---- hidden: the resting state, where the defect lived -------------------
361
+ // Precondition: the fixture must actually overflow, or "hidden at rest" and
362
+ // "visible when scrolled up" would both pass without proving anything.
363
+ expect(
364
+ await scrollRange(root),
365
+ 'the fixture must overflow by more than the 100px button threshold',
366
+ ).toBeGreaterThan(150);
367
+ expect(await distanceFromBottom(root)).toBeLessThan(2);
368
+ await expect(pill).toHaveCSS('visibility', 'hidden');
369
+ expect(
370
+ await affordanceAcceptsFocus(page),
371
+ 'a control that paints nothing must refuse focus (WCAG 2.2 SC 2.4.7)',
372
+ ).toBe(false);
373
+ const restFrame = await capture(page, 'rest-hidden', {
374
+ pillVisibility: await pillVisibility(page),
375
+ affordanceAcceptsFocus: false,
376
+ distanceFromBottomPx: await distanceFromBottom(root),
377
+ });
378
+ restFrame.rendered.tabOrderObserved = (
379
+ await tabWithinLayout(page, before, SWEEP)
380
+ ).sequence;
381
+
382
+ // ---- visible: the reader scrolls away from the newest message ------------
383
+ await scrollToTop(root);
384
+ await expect(pill).toHaveCSS('visibility', 'visible');
385
+ expect(await distanceFromBottom(root)).toBeGreaterThan(100);
386
+ expect(
387
+ await affordanceAcceptsFocus(page),
388
+ 'the visible affordance must keep its keyboard access',
389
+ ).toBe(true);
390
+ await expect(
391
+ page.getByRole('button', {name: 'Scroll to bottom'}),
392
+ ).toHaveAccessibleName('Scroll to bottom');
393
+ const visibleFrame = await capture(page, 'scrolled-up-visible', {
394
+ pillVisibility: await pillVisibility(page),
395
+ affordanceAcceptsFocus: true,
396
+ accessibleName: 'Scroll to bottom',
397
+ distanceFromBottomPx: await distanceFromBottom(root),
398
+ });
399
+ const visibleSweep = await tabWithinLayout(page, before, SWEEP);
400
+ visibleFrame.rendered.tabOrderObserved = visibleSweep.sequence;
401
+ visibleFrame.rendered.reachedByTabAfterPresses = visibleSweep.sequence.length;
402
+ expect(
403
+ visibleSweep.reached,
404
+ `Tab never reached the visible affordance: ${describeStops(visibleSweep.sequence)}`,
405
+ ).toBe(true);
406
+
407
+ // ---- re-hidden: activating it returns to the bottom ----------------------
408
+ // The sweep above left focus on the affordance, so Enter activates it. Wait
409
+ // for the scroll spring to SETTLE at the bottom rather than merely for the
410
+ // pill to hide: hiding happens at the 100px threshold, while the animation
411
+ // is still running, and anything that nudges the scroller in that window
412
+ // flips the affordance back.
413
+ await page.keyboard.press('Enter');
414
+ await expect
415
+ .poll(async () => distanceFromBottom(root), {timeout: 5000})
416
+ .toBeLessThan(2);
417
+ await expect(pill).toHaveCSS('visibility', 'hidden');
418
+ expect(
419
+ await affordanceAcceptsFocus(page),
420
+ 'after returning to the bottom the affordance must refuse focus again',
421
+ ).toBe(false);
422
+ const returnedFrame = await capture(page, 'returned-hidden', {
423
+ pillVisibility: await pillVisibility(page),
424
+ affordanceAcceptsFocus: false,
425
+ activatedWith: 'Enter',
426
+ distanceFromBottomPx: await distanceFromBottom(root),
427
+ });
428
+ returnedFrame.rendered.tabOrderObserved = (
429
+ await tabWithinLayout(page, before, SWEEP)
430
+ ).sequence;
431
+
432
+ expect(pageErrors).toEqual([]);
433
+ });
434
+
435
+ test('the pill tracks the theme element-size token instead of a fixed 32px', async ({
436
+ page,
437
+ }) => {
438
+ // Butter resolves --size-element-md to 40px; Neutral resolves it to 32px.
439
+ // Both are shipped themes, so this is a real theme pair, not a mutated arm.
440
+ const sizes: Record<string, {token: string; pill: number; button: number}> =
441
+ {};
442
+
443
+ for (const theme of ['neutral', 'butter']) {
444
+ const root = await openStory(page, {theme});
445
+ await scrollToTop(root);
446
+ const pill = page.locator(PILL);
447
+ await expect(pill).toHaveCSS('visibility', 'visible');
448
+
449
+ const token = await pill.evaluate(element =>
450
+ getComputedStyle(element).getPropertyValue('--size-element-md').trim(),
451
+ );
452
+ const pillHeight = (await pill.boundingBox())?.height ?? 0;
453
+ const buttonHeight =
454
+ (await page.getByRole('button', {name: 'Scroll to bottom'}).boundingBox())
455
+ ?.height ?? 0;
456
+
457
+ sizes[theme] = {
458
+ token,
459
+ pill: Math.round(pillHeight),
460
+ button: Math.round(buttonHeight),
461
+ };
462
+
463
+ // The pill clips its own content (`overflow: hidden`), so it must be at
464
+ // least as tall as the Button it wraps in every theme. Neutral also shrinks
465
+ // its md Button to `calc(--size-element-md - 8px)`, so the two are equal
466
+ // only where a theme leaves the Button at the full token height.
467
+ expect(
468
+ sizes[theme].pill,
469
+ `${theme}: the pill must not be shorter than the Button it clips`,
470
+ ).toBeGreaterThanOrEqual(sizes[theme].button);
471
+ await capture(page, `token-size-${theme}`, {
472
+ theme,
473
+ sizeElementMd: token,
474
+ pillHeightPx: sizes[theme].pill,
475
+ buttonHeightPx: sizes[theme].button,
476
+ clippedPx: Math.max(0, sizes[theme].button - sizes[theme].pill),
477
+ });
478
+ }
479
+
480
+ // The themes must actually disagree, or the pair proves nothing. Butter
481
+ // resolves the token to 40px and leaves its md Button at the full token
482
+ // height; Neutral resolves it to 32px and shrinks the Button by 8px.
483
+ expect(sizes.butter.token).not.toBe(sizes.neutral.token);
484
+ expect(sizes.butter.pill).toBeGreaterThan(sizes.neutral.pill);
485
+ expect(sizes.butter.button).toBeGreaterThan(sizes.neutral.button);
486
+
487
+ // ---- regression arm -----------------------------------------------------
488
+ // ARM: pre-fix literal
489
+ // BASE: this head
490
+ // DELTA: one injected rule restoring the removed literal on the pill —
491
+ // `height: 32px; max-width: 32px` — and nothing else.
492
+ // UNRELATED DELTA: none
493
+ const root = await openStory(page, {theme: 'butter'});
494
+ await scrollToTop(root);
495
+ await page.addStyleTag({
496
+ content: `${PILL} { height: 32px !important; max-width: 32px !important; }`,
497
+ });
498
+ const clippedPill = (await page.locator(PILL).boundingBox())?.height ?? 0;
499
+ const clippedButton =
500
+ (await page.getByRole('button', {name: 'Scroll to bottom'}).boundingBox())
501
+ ?.height ?? 0;
502
+ await capture(page, 'token-size-butter-prefix-literal', {
503
+ theme: 'butter',
504
+ arm: 'pre-fix literal (height: 32px; max-width: 32px)',
505
+ pillHeightPx: Math.round(clippedPill),
506
+ buttonHeightPx: Math.round(clippedButton),
507
+ clippedPx: Math.round(clippedButton - clippedPill),
508
+ });
509
+
510
+ // What the fix prevents: under Butter the old literal left the Button taller
511
+ // than the pill that clips it.
512
+ expect(Math.round(clippedButton)).toBeGreaterThan(Math.round(clippedPill));
513
+ expect(pageErrors).toEqual([]);
514
+ });