@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,740 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts
5
+ * @input Uses the ChatLayout scroll-button states story and a built Storybook
6
+ * @output Rendered evidence that the documented theming target reaches the
7
+ * element painting the pill, plus the audited state frames and their receipts
8
+ * @position Real-Chromium evidence for the 2026-09-23 ChatLayoutScrollButton
9
+ * audit. Frames land under the component-audit evidence directory that CI
10
+ * already uploads, in a `scroll-button/` subdirectory so this manifest never
11
+ * merges with the sibling ChatLayout manifest.
12
+ *
13
+ * The claim under test is `architecture:component-theming-surface` INV4:
14
+ * "A target belongs on a stable visible element that paints theme-controlled
15
+ * output—not an event wrapper, speculative internal structure, or a node
16
+ * created only for layout plumbing."
17
+ *
18
+ * This is not observable in the unit lane and not observable to the probe-reach
19
+ * gate either. jsdom resolves no cascade, so it cannot say which element paints.
20
+ * The gate's reach check asks whether a target's override lands on the element
21
+ * carrying the class, and it does — that element simply is not the one a reader
22
+ * sees. Only a real engine can be asked the question that matters: does styling
23
+ * the documented target change the pill?
24
+ *
25
+ * The pill is identified structurally, as the button's parent element, so every
26
+ * measurement below is independent of where the class currently sits.
27
+ */
28
+
29
+ import {createHash} from 'node:crypto';
30
+ import * as fs from 'node:fs';
31
+ import * as path from 'node:path';
32
+ import {expect, test, type Locator, type Page} from '@playwright/test';
33
+ import {holdMotionStill} from '@astryxdesign/a11y-spec/chromium';
34
+ import {
35
+ DEFAULT_STORYBOOK_DIR,
36
+ serveStorybook,
37
+ type StaticServer,
38
+ } from '@astryxdesign/a11y-spec/storybook';
39
+
40
+ const OUTPUT = path.resolve(
41
+ 'test-results/chat-layout-audit-evidence/scroll-button',
42
+ );
43
+ const STORY_ID = 'core-chatlayout--scroll-button-states';
44
+
45
+ /** The rendered configurations the story stages, in capture order. */
46
+ const STATES = [
47
+ 'hidden',
48
+ 'visible-collapsed',
49
+ 'visible-labelled',
50
+ 'visible-long-label',
51
+ ] as const;
52
+
53
+ type State = (typeof STATES)[number];
54
+
55
+ /** How an interaction frame was driven. `rest` is the undriven baseline. */
56
+ type Interaction = 'rest' | 'hover' | 'focus-visible' | 'pressed';
57
+
58
+ /** The two configurations whose interaction states the audit grades. */
59
+ const INTERACTION_STATES = ['visible-collapsed', 'visible-labelled'] as const;
60
+
61
+ type Shot = {
62
+ file: string;
63
+ sha256: string;
64
+ /** Decoded from the PNG's own IHDR — the image's pixels, not the viewport. */
65
+ width: number;
66
+ height: number;
67
+ };
68
+
69
+ type Receipt = {
70
+ state: string;
71
+ storyId: string;
72
+ theme: string;
73
+ colorMode: string;
74
+ direction: string;
75
+ /** The window, recorded in its own row so it can never be read as the image. */
76
+ viewport: {width: number; height: number};
77
+ devicePixelRatio: number;
78
+ rendered: Record<string, unknown>;
79
+ geometry: {selectorCount: number; width: number; height: number};
80
+ settled: {fontsReady: boolean; pageErrors: number};
81
+ image: Shot;
82
+ };
83
+
84
+ const receipts: Receipt[] = [];
85
+ const pageErrors: string[] = [];
86
+ let storybook: StaticServer;
87
+ let browserVersion = 'unknown';
88
+
89
+ test.beforeAll(async () => {
90
+ // Do NOT wipe: Playwright restarts the worker after a failing test and
91
+ // re-runs this hook, which would delete the frames already banked — exactly
92
+ // when a failing audit needs them most.
93
+ fs.mkdirSync(OUTPUT, {recursive: true});
94
+ storybook = await serveStorybook(
95
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
96
+ );
97
+ });
98
+
99
+ test.afterAll(async () => {
100
+ const manifestPath = path.join(OUTPUT, 'manifest.json');
101
+ const previous = fs.existsSync(manifestPath)
102
+ ? (JSON.parse(fs.readFileSync(manifestPath, 'utf8')).frames ?? [])
103
+ : [];
104
+ const merged = [...previous, ...receipts].filter(
105
+ (frame, index, all) =>
106
+ all.findIndex(other => other.state === frame.state) === index,
107
+ );
108
+ fs.writeFileSync(
109
+ manifestPath,
110
+ `${JSON.stringify(
111
+ {
112
+ version: 1,
113
+ component: 'core/ChatLayoutScrollButton',
114
+ headSha:
115
+ process.env.ASTRYX_HEAD_SHA ??
116
+ process.env.GITHUB_SHA ??
117
+ 'local-working-copy',
118
+ checkoutSha: process.env.GITHUB_SHA ?? 'local-working-copy',
119
+ browser: browserVersion,
120
+ frames: merged,
121
+ },
122
+ null,
123
+ 2,
124
+ )}\n`,
125
+ );
126
+ await storybook?.close();
127
+ });
128
+
129
+ async function openStory(
130
+ page: Page,
131
+ {theme = 'neutral', colorMode = 'light'} = {},
132
+ ): Promise<void> {
133
+ page.on('pageerror', error => pageErrors.push(String(error)));
134
+ browserVersion = page.context().browser()?.version() ?? 'unknown';
135
+ await page.goto(
136
+ `${storybook.origin}/iframe.html?id=${STORY_ID}&viewMode=story` +
137
+ `&globals=colorMode:${colorMode};astryxTheme:${theme}`,
138
+ );
139
+ await page.locator(`[data-scroll-button-state="hidden"]`).waitFor();
140
+ await holdMotionStill(page);
141
+ await page.evaluate(async () => document.fonts.ready);
142
+ }
143
+
144
+ function instance(page: Page, state: State): Locator {
145
+ return page.locator(`[data-scroll-button-state="${state}"]`);
146
+ }
147
+
148
+ /**
149
+ * Read one staged instance: which element carries the public target, whether
150
+ * that element is the one painting the pill, and what each of them paints.
151
+ *
152
+ * The pill is `button.parentElement` — the element that clips the Button and
153
+ * carries the surface a reader sees. Naming it structurally rather than by
154
+ * class is the whole point: the question is *where the class is*, so the
155
+ * measurement cannot be allowed to depend on the answer.
156
+ */
157
+ async function readInstance(page: Page, state: State) {
158
+ return instance(page, state).evaluate(host => {
159
+ const button = host.querySelector('button');
160
+ const pill = button?.parentElement ?? null;
161
+ const themed = host.querySelector('.astryx-chat-layout-scroll-button');
162
+ const root = host.firstElementChild;
163
+
164
+ const describe = (element: Element | null) => {
165
+ if (element == null) {
166
+ return null;
167
+ }
168
+ const styles = getComputedStyle(element);
169
+ const box = element.getBoundingClientRect();
170
+ return {
171
+ backgroundColor: styles.backgroundColor,
172
+ boxShadow: styles.boxShadow,
173
+ borderRadius: styles.borderTopLeftRadius,
174
+ visibility: styles.visibility,
175
+ width: Math.round(box.width),
176
+ height: Math.round(box.height),
177
+ };
178
+ };
179
+
180
+ return {
181
+ targetCount: host.querySelectorAll('.astryx-chat-layout-scroll-button')
182
+ .length,
183
+ /** Is the public target on the element that paints the pill? */
184
+ targetIsPill: themed != null && themed === pill,
185
+ /** Or is it on the component's outermost layout node? */
186
+ targetIsRoot: themed != null && themed === root,
187
+ themed: describe(themed),
188
+ pill: describe(pill),
189
+ root: describe(root),
190
+ button: (() => {
191
+ if (button == null) {
192
+ return null;
193
+ }
194
+ const box = button.getBoundingClientRect();
195
+ const label = button.querySelector('span, p, div');
196
+ return {
197
+ width: Math.round(box.width),
198
+ height: Math.round(box.height),
199
+ accessibleName: button.getAttribute('aria-label'),
200
+ textOverflowing:
201
+ label != null ? label.scrollWidth > label.clientWidth + 1 : false,
202
+ };
203
+ })(),
204
+ };
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Where the focus ring would be drawn, and whether anything clips it away.
210
+ *
211
+ * `outline` is painted OUTSIDE the element's border box, starting at
212
+ * `outline-offset` and extending `outline-width` further. If an ancestor with
213
+ * `overflow: hidden` has no room between the button's border box and its own
214
+ * padding box, every pixel of that ring is clipped and the computed style is a
215
+ * promise the page never keeps.
216
+ */
217
+ async function readFocusRingRoom(page: Page, state: State) {
218
+ return instance(page, state)
219
+ .locator('button')
220
+ .evaluate(element => {
221
+ const styles = getComputedStyle(element);
222
+ const offset = Number.parseFloat(styles.outlineOffset) || 0;
223
+ const width = Number.parseFloat(styles.outlineWidth) || 0;
224
+
225
+ let clipper: Element | null = element.parentElement;
226
+ while (clipper != null) {
227
+ const overflow = getComputedStyle(clipper).overflow;
228
+ if (overflow !== 'visible') {
229
+ break;
230
+ }
231
+ clipper = clipper.parentElement;
232
+ }
233
+ if (clipper == null) {
234
+ return {outreach: offset + width, clipped: false, roomPx: null};
235
+ }
236
+
237
+ const button = element.getBoundingClientRect();
238
+ const clip = clipper.getBoundingClientRect();
239
+ // Smallest gap on any side between the button's box and the clip box.
240
+ const room = Math.min(
241
+ button.top - clip.top,
242
+ button.left - clip.left,
243
+ clip.right - button.right,
244
+ clip.bottom - button.bottom,
245
+ );
246
+ return {
247
+ outreach: offset + width,
248
+ roomPx: Math.round(room),
249
+ clipperOverflow: getComputedStyle(clipper).overflow,
250
+ clipperClass:
251
+ [...clipper.classList].find(name => name.startsWith('astryx-')) ??
252
+ null,
253
+ clipped: room < offset + width,
254
+ };
255
+ });
256
+ }
257
+
258
+ /** `rgba(r, g, b, 0)` and `transparent` both mean "this element paints no fill". */
259
+ function isTransparent(color: string): boolean {
260
+ return color === 'transparent' || /,\s*0\)$/.test(color);
261
+ }
262
+
263
+ /**
264
+ * What the Button is painting right now: the four properties its interaction
265
+ * states actually move. Recorded per frame so a reader can confirm from the
266
+ * receipt that the state engaged, rather than trusting the file name.
267
+ *
268
+ * Read from the `<button>` rather than the pill: the pill owns the surface and
269
+ * the theming target, while hover, focus, and press are Button's own treatment.
270
+ */
271
+ async function readButtonPaint(page: Page, state: State) {
272
+ return instance(page, state)
273
+ .locator('button')
274
+ .evaluate(element => {
275
+ const styles = getComputedStyle(element);
276
+ return {
277
+ backgroundColor: styles.backgroundColor,
278
+ backgroundImage: styles.backgroundImage,
279
+ transform: styles.transform,
280
+ outlineWidth: styles.outlineWidth,
281
+ outlineStyle: styles.outlineStyle,
282
+ isFocused: document.activeElement === element,
283
+ matchesFocusVisible: element.matches(':focus-visible'),
284
+ matchesHover: element.matches(':hover'),
285
+ matchesActive: element.matches(':active'),
286
+ };
287
+ });
288
+ }
289
+
290
+ /**
291
+ * Tab from the top of the document until focus lands inside `state`.
292
+ *
293
+ * Keyboard is the only modality that yields `:focus-visible` — a synthetic
294
+ * `focus()` call does not, and a click sets pointer modality and suppresses
295
+ * it. The page must therefore be freshly loaded with no pointer press behind
296
+ * it, which is why the caller reloads before asking for a focus frame.
297
+ */
298
+ async function tabTo(
299
+ page: Page,
300
+ state: State,
301
+ maxPresses = 8,
302
+ ): Promise<boolean> {
303
+ for (let index = 0; index < maxPresses; index += 1) {
304
+ await page.keyboard.press('Tab');
305
+ const landed = await page.evaluate(selector => {
306
+ const active = document.activeElement;
307
+ return active != null && active.closest(selector) != null;
308
+ }, `[data-scroll-button-state="${state}"]`);
309
+ if (landed) {
310
+ return true;
311
+ }
312
+ }
313
+ return false;
314
+ }
315
+
316
+ /**
317
+ * Read a PNG's real pixel dimensions out of its IHDR header.
318
+ *
319
+ * These are ELEMENT screenshots, so their size is the subject's box, not the
320
+ * window's. The viewport keeps its own receipt row; publishing it as the image
321
+ * size would be a false claim about the bytes on disk.
322
+ */
323
+ function pngShot(file: string, bytes: Buffer): Shot {
324
+ return {
325
+ file,
326
+ sha256: createHash('sha256').update(bytes).digest('hex'),
327
+ width: bytes.readUInt32BE(16),
328
+ height: bytes.readUInt32BE(20),
329
+ };
330
+ }
331
+
332
+ async function capture(
333
+ page: Page,
334
+ state: State,
335
+ frameName: string,
336
+ rendered: Record<string, unknown>,
337
+ {theme = 'neutral', colorMode = 'light'} = {},
338
+ ): Promise<Receipt> {
339
+ const host = instance(page, state);
340
+ const box = await host.boundingBox();
341
+ if (box == null) {
342
+ throw new Error(`${frameName}: the staged instance has no layout box`);
343
+ }
344
+ const bytes = await host.screenshot({animations: 'disabled'});
345
+ const file = `chat-layout-scroll-button__${frameName}.png`;
346
+ fs.writeFileSync(path.join(OUTPUT, file), bytes);
347
+
348
+ const window = await page.evaluate(() => ({
349
+ width: globalThis.innerWidth,
350
+ height: globalThis.innerHeight,
351
+ dpr: globalThis.devicePixelRatio,
352
+ }));
353
+
354
+ const receipt: Receipt = {
355
+ state: frameName,
356
+ storyId: STORY_ID,
357
+ theme: await page.evaluate(
358
+ () =>
359
+ document
360
+ .querySelector('[data-astryx-theme]')
361
+ ?.getAttribute('data-astryx-theme') ?? 'neutral',
362
+ ),
363
+ colorMode: await page.evaluate(
364
+ () => getComputedStyle(document.documentElement).colorScheme,
365
+ ),
366
+ direction: await host.evaluate(
367
+ element => getComputedStyle(element).direction,
368
+ ),
369
+ viewport: {width: window.width, height: window.height},
370
+ devicePixelRatio: window.dpr,
371
+ rendered: {
372
+ requestedTheme: theme,
373
+ requestedColorMode: colorMode,
374
+ ...rendered,
375
+ },
376
+ geometry: {
377
+ selectorCount: await host.count(),
378
+ width: Math.round(box.width),
379
+ height: Math.round(box.height),
380
+ },
381
+ settled: {fontsReady: true, pageErrors: pageErrors.length},
382
+ image: pngShot(file, bytes),
383
+ };
384
+ receipts.push(receipt);
385
+ return receipt;
386
+ }
387
+
388
+ test('the documented theming target sits on the element that paints the pill', async ({
389
+ page,
390
+ }) => {
391
+ await openStory(page);
392
+ const state: State = 'visible-collapsed';
393
+ const reading = await readInstance(page, state);
394
+
395
+ // The frame is banked BEFORE the assertions, so a failing run still publishes
396
+ // the picture of what it found.
397
+ await capture(page, state, 'target-placement-neutral', {
398
+ targetIsPill: reading.targetIsPill,
399
+ targetIsRoot: reading.targetIsRoot,
400
+ themed: reading.themed,
401
+ pill: reading.pill,
402
+ });
403
+
404
+ expect(reading.targetCount, 'exactly one element carries the target').toBe(1);
405
+
406
+ // INV4, read directly: the target is on the painting element, and that
407
+ // element is the pill rather than the full-width row that centres it.
408
+ expect(
409
+ reading.targetIsPill,
410
+ 'architecture:component-theming-surface INV4 — the public target must sit ' +
411
+ 'on the element that paints the pill, not on the layout node that centres it',
412
+ ).toBe(true);
413
+
414
+ expect(
415
+ isTransparent(reading.themed?.backgroundColor ?? 'transparent'),
416
+ 'the themed element must paint a fill a theme can replace',
417
+ ).toBe(false);
418
+ expect(
419
+ reading.themed?.boxShadow,
420
+ 'the themed element must own the pill elevation',
421
+ ).not.toBe('none');
422
+
423
+ // A target on the centring row is as wide as the dock; a target on the pill
424
+ // is as wide as the pill. The numbers separate the two without naming either.
425
+ expect(reading.themed?.width).toBe(reading.pill?.width);
426
+ expect(reading.themed?.height).toBe(reading.pill?.height);
427
+
428
+ expect(pageErrors).toEqual([]);
429
+ });
430
+
431
+ test('a theme that styles the documented target repaints the pill', async ({
432
+ page,
433
+ }) => {
434
+ const state: State = 'visible-collapsed';
435
+
436
+ // The probe theme exists precisely to answer this: it styles every declared
437
+ // target with a loud generated colour, so "did the override reach the pixels
438
+ // a reader sees" is an equality test rather than a judgement.
439
+ await openStory(page, {theme: 'neutral'});
440
+ const neutral = await readInstance(page, state);
441
+ await capture(page, state, 'theme-reach-neutral', {
442
+ pillBackground: neutral.pill?.backgroundColor,
443
+ rootBackground: neutral.root?.backgroundColor,
444
+ });
445
+
446
+ await openStory(page, {theme: 'probe'});
447
+ const probe = await readInstance(page, state);
448
+ await capture(
449
+ page,
450
+ state,
451
+ 'theme-reach-probe',
452
+ {
453
+ pillBackground: probe.pill?.backgroundColor,
454
+ rootBackground: probe.root?.backgroundColor,
455
+ targetIsPill: probe.targetIsPill,
456
+ },
457
+ {theme: 'probe'},
458
+ );
459
+
460
+ expect(
461
+ probe.pill?.backgroundColor,
462
+ 'the probe theme must actually change the surface the reader sees; an ' +
463
+ 'unchanged pill means the override landed somewhere else',
464
+ ).not.toBe(neutral.pill?.backgroundColor);
465
+
466
+ // ---- regression arm -----------------------------------------------------
467
+ // ARM: pre-fix target placement
468
+ // BASE: this head, probe theme, same page
469
+ // DELTA: the target class is moved from the pill back up to the component's
470
+ // outer layout node — one `classList` move, and nothing else.
471
+ // UNRELATED DELTA: none
472
+ const armMoved = await instance(page, state).evaluate(host => {
473
+ const root = host.firstElementChild;
474
+ const themed = host.querySelector('.astryx-chat-layout-scroll-button');
475
+ if (root == null || themed == null || themed === root) {
476
+ return false;
477
+ }
478
+ themed.classList.remove('astryx-chat-layout-scroll-button');
479
+ root.classList.add('astryx-chat-layout-scroll-button');
480
+ return true;
481
+ });
482
+ expect(
483
+ armMoved,
484
+ 'the arm must actually relocate the target, or it proves nothing',
485
+ ).toBe(true);
486
+
487
+ const arm = await readInstance(page, state);
488
+ await capture(
489
+ page,
490
+ state,
491
+ 'theme-reach-probe-prefix-placement',
492
+ {
493
+ arm: 'pre-fix placement (target on the outer layout node)',
494
+ pillBackground: arm.pill?.backgroundColor,
495
+ rootBackground: arm.root?.backgroundColor,
496
+ rootWidth: arm.root?.width,
497
+ pillWidth: arm.pill?.width,
498
+ },
499
+ {theme: 'probe'},
500
+ );
501
+
502
+ // What the fix prevents: the theme colour lands on the invisible centring
503
+ // row — painting a band the width of the dock — while the pill a reader
504
+ // actually sees keeps the colour the theme asked to replace.
505
+ expect(arm.pill?.backgroundColor).toBe(neutral.pill?.backgroundColor);
506
+ expect(arm.root?.backgroundColor).not.toBe(neutral.root?.backgroundColor);
507
+ expect(arm.root?.width).toBeGreaterThan(arm.pill?.width ?? 0);
508
+
509
+ expect(pageErrors).toEqual([]);
510
+ });
511
+
512
+ test('every audited state renders in light and dark, and the target meets WCAG 2.5.8', async ({
513
+ page,
514
+ }) => {
515
+ for (const colorMode of ['light', 'dark'] as const) {
516
+ await openStory(page, {colorMode});
517
+ for (const state of STATES) {
518
+ const reading = await readInstance(page, state);
519
+ await capture(
520
+ page,
521
+ state,
522
+ `${state}__${colorMode}`,
523
+ {
524
+ pillVisibility: reading.pill?.visibility,
525
+ pillWidth: reading.pill?.width,
526
+ pillHeight: reading.pill?.height,
527
+ buttonWidth: reading.button?.width,
528
+ buttonHeight: reading.button?.height,
529
+ accessibleName: reading.button?.accessibleName,
530
+ labelTextOverflowing: reading.button?.textOverflowing,
531
+ },
532
+ {colorMode},
533
+ );
534
+
535
+ if (state === 'visible-collapsed') {
536
+ // WCAG 2.2 SC 2.5.8 (AA): a 24x24 CSS-px square fits inside the
537
+ // target. Measured rather than derived from the size token, because
538
+ // the token is a theme dial and the criterion is not.
539
+ expect(
540
+ reading.button?.width ?? 0,
541
+ `${colorMode}: the collapsed target must fit a 24px square`,
542
+ ).toBeGreaterThanOrEqual(24);
543
+ expect(
544
+ reading.button?.height ?? 0,
545
+ `${colorMode}: the collapsed target must fit a 24px square`,
546
+ ).toBeGreaterThanOrEqual(24);
547
+ }
548
+ }
549
+ }
550
+
551
+ expect(pageErrors).toEqual([]);
552
+ });
553
+
554
+ /**
555
+ * The interaction states, driven for real and photographed while they are
556
+ * engaged.
557
+ *
558
+ * Twelve frames: the two visible configurations x hover, keyboard
559
+ * focus-visible, and a held press x light and dark. Every frame asserts that
560
+ * the state actually engaged — `:hover`, `:focus-visible`, or `:active`
561
+ * matched — and compares the captured bytes against a rest frame taken from
562
+ * the same page moments earlier.
563
+ *
564
+ * The byte comparison is the point. An earlier revision proved hover and press
565
+ * from computed style and accepted a non-zero `outline-width` as proof of a
566
+ * visible focus ring. That was a false positive: this component's focus ring
567
+ * is painted entirely outside the button's border box, and the pill that wraps
568
+ * it clips with `overflow: hidden`, so all four focused frames came back
569
+ * byte-identical to rest while every computed reading said the ring was there.
570
+ * Computed style says what was declared; only the frame says what was painted.
571
+ *
572
+ * Order within a color mode is deliberate. Hover comes first; the pointer then
573
+ * moves away before the focus pass, or the hovered element would still paint
574
+ * hover underneath the focus ring. Press comes last, because a pointer press
575
+ * sets pointer modality and suppresses `:focus-visible` for the rest of the
576
+ * page's life — so the page is reloaded before the focus pass to guarantee the
577
+ * keyboard modality the ring depends on.
578
+ */
579
+ test('the interaction states are driven and photographed while engaged', async ({
580
+ page,
581
+ }) => {
582
+ for (const colorMode of ['light', 'dark'] as const) {
583
+ /** Rest, for the same page and color mode, as the comparison baseline. */
584
+ await openStory(page, {colorMode});
585
+ // Park the pointer FIRST. A reload does not move the physical mouse, so
586
+ // without this the second color mode reads its "rest" through whichever
587
+ // button the previous pass left the cursor over — and that reading is a
588
+ // hover reading, which then makes hover look like it paints nothing. The
589
+ // engagement assertion below caught exactly that, so it is load-bearing.
590
+ await page.mouse.move(0, 0);
591
+ const rest: Record<
592
+ string,
593
+ Awaited<ReturnType<typeof readButtonPaint>>
594
+ > = {};
595
+ for (const state of INTERACTION_STATES) {
596
+ rest[state] = await readButtonPaint(page, state);
597
+ expect(
598
+ rest[state].matchesHover,
599
+ `${state}/${colorMode}: the rest baseline must not itself be hovered`,
600
+ ).toBe(false);
601
+ }
602
+
603
+ // ---- hover ------------------------------------------------------------
604
+ for (const state of INTERACTION_STATES) {
605
+ const restBytes = await instance(page, state).screenshot({
606
+ animations: 'disabled',
607
+ });
608
+ const restSha = createHash('sha256').update(restBytes).digest('hex');
609
+
610
+ await instance(page, state).locator('button').hover();
611
+ const paint = await readButtonPaint(page, state);
612
+ const interaction: Interaction = 'hover';
613
+ const receipt = await capture(
614
+ page,
615
+ state,
616
+ `${state}__${interaction}__${colorMode}`,
617
+ {interaction, paint, restPaint: rest[state]},
618
+ {colorMode},
619
+ );
620
+ receipt.rendered.restSha256 = restSha;
621
+ receipt.rendered.changedPixels = receipt.image.sha256 !== restSha;
622
+
623
+ expect(
624
+ paint.matchesHover,
625
+ `${state}/${colorMode}: the pointer must actually be over the button`,
626
+ ).toBe(true);
627
+ // Pixels, not computed style. That distinction is the whole lesson of
628
+ // the focus finding below.
629
+ expect(
630
+ receipt.image.sha256 !== restSha,
631
+ `${state}/${colorMode}: the hovered frame must differ from rest`,
632
+ ).toBe(true);
633
+ }
634
+
635
+ // ---- keyboard focus-visible -------------------------------------------
636
+ // Reload rather than move the mouse: this pass must run with no pointer
637
+ // interaction behind it at all.
638
+ await openStory(page, {colorMode});
639
+ await page.mouse.move(0, 0);
640
+ for (const state of INTERACTION_STATES) {
641
+ // The rest bytes for THIS page, taken before focus is driven. Held in
642
+ // memory: it is the comparison subject, not a frame worth publishing.
643
+ const restBytes = await instance(page, state).screenshot({
644
+ animations: 'disabled',
645
+ });
646
+ const restSha = createHash('sha256').update(restBytes).digest('hex');
647
+
648
+ const reached = await tabTo(page, state);
649
+ expect(reached, `${state}/${colorMode}: Tab must reach the button`).toBe(
650
+ true,
651
+ );
652
+ const paint = await readButtonPaint(page, state);
653
+ const room = await readFocusRingRoom(page, state);
654
+ const interaction: Interaction = 'focus-visible';
655
+ const receipt = await capture(
656
+ page,
657
+ state,
658
+ `${state}__${interaction}__${colorMode}`,
659
+ {interaction, paint, restPaint: rest[state], focusRing: room},
660
+ {colorMode},
661
+ );
662
+
663
+ // Keyboard reachability is real and must not regress.
664
+ expect(
665
+ paint.isFocused && paint.matchesFocusVisible,
666
+ `${state}/${colorMode}: the button must hold keyboard-visible focus`,
667
+ ).toBe(true);
668
+
669
+ // What the ring actually does to the pixels. An earlier revision of this
670
+ // spec asserted `outlineStyle !== 'none' && outlineWidth !== '0px'` and
671
+ // called that proof of a visible ring. It is not: a computed outline is
672
+ // a declaration, and this one is painted entirely outside the button's
673
+ // border box, where an ancestor clips it away. The frame is the only
674
+ // thing that can answer the question, so the frame is what is compared.
675
+ const ringChangedPixels = receipt.image.sha256 !== restSha;
676
+ receipt.rendered.restSha256 = restSha;
677
+ receipt.rendered.ringChangedPixels = ringChangedPixels;
678
+
679
+ if (!ringChangedPixels) {
680
+ // Deliberately NOT an assertion, in either direction. Asserting a pass
681
+ // would be the false positive again; asserting the failure would turn
682
+ // the repair into a red check and enshrine the defect as the baseline.
683
+ // It is recorded in the receipt, annotated on the run, and reported as
684
+ // an open BLOCK in the audit.
685
+ const note =
686
+ `KNOWN FAILURE — ${state}/${colorMode}: the focused frame is ` +
687
+ `byte-identical to rest, so the keyboard focus indicator paints ` +
688
+ `nothing (WCAG 2.2 SC 2.4.7; spec:AST-020/FR1). The ring reaches ` +
689
+ `${room.outreach}px beyond the button's border box and the ` +
690
+ `clipping ancestor leaves ${room.roomPx}px of room ` +
691
+ `(overflow: ${room.clipperOverflow}). Open BLOCK, not accepted.`;
692
+ console.warn(note);
693
+ test
694
+ .info()
695
+ .annotations.push({type: 'known-failure', description: note});
696
+ }
697
+ }
698
+
699
+ // ---- held press -------------------------------------------------------
700
+ // Captured with the button still down. Releasing first would photograph
701
+ // the state the press returns to, not the press.
702
+ for (const state of INTERACTION_STATES) {
703
+ await openStory(page, {colorMode});
704
+ const button = instance(page, state).locator('button');
705
+ const box = await button.boundingBox();
706
+ if (box == null) {
707
+ throw new Error(`${state}: the button has no layout box`);
708
+ }
709
+ const restBytes = await instance(page, state).screenshot({
710
+ animations: 'disabled',
711
+ });
712
+ const restSha = createHash('sha256').update(restBytes).digest('hex');
713
+ await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
714
+ await page.mouse.down();
715
+ const paint = await readButtonPaint(page, state);
716
+ const interaction: Interaction = 'pressed';
717
+ const receipt = await capture(
718
+ page,
719
+ state,
720
+ `${state}__${interaction}__${colorMode}`,
721
+ {interaction, paint, restPaint: rest[state]},
722
+ {colorMode},
723
+ );
724
+ await page.mouse.up();
725
+ receipt.rendered.restSha256 = restSha;
726
+ receipt.rendered.changedPixels = receipt.image.sha256 !== restSha;
727
+
728
+ expect(
729
+ paint.matchesActive,
730
+ `${state}/${colorMode}: the button must still be held down`,
731
+ ).toBe(true);
732
+ expect(
733
+ receipt.image.sha256 !== restSha,
734
+ `${state}/${colorMode}: the pressed frame must differ from rest`,
735
+ ).toBe(true);
736
+ }
737
+ }
738
+
739
+ expect(pageErrors).toEqual([]);
740
+ });