@astryxdesign/core 0.7.1-canary.ff40b78 → 0.7.1

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 (348) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/dist/Button/Button.d.ts +1 -1
  3. package/dist/Button/Button.d.ts.map +1 -1
  4. package/dist/Button/Button.js +5 -2
  5. package/dist/Calendar/Calendar.d.ts.map +1 -1
  6. package/dist/Calendar/Calendar.js +191 -32
  7. package/dist/Chat/ChatComposerInput.d.ts +9 -0
  8. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  9. package/dist/Chat/ChatComposerInput.js +5 -0
  10. package/dist/Chat/ChatMessage.d.ts +11 -0
  11. package/dist/Chat/ChatMessage.d.ts.map +1 -1
  12. package/dist/Chat/ChatMessage.js +18 -9
  13. package/dist/Chat/useTriggerMenu.d.ts +9 -2
  14. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  15. package/dist/Chat/useTriggerMenu.js +222 -50
  16. package/dist/CodeBlock/CodeBlock.d.ts +10 -1
  17. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  18. package/dist/CodeBlock/CodeBlock.js +158 -38
  19. package/dist/CodeBlock/tokenizer.d.ts +39 -0
  20. package/dist/CodeBlock/tokenizer.d.ts.map +1 -1
  21. package/dist/CodeBlock/tokenizer.js +108 -0
  22. package/dist/CommandPalette/CommandPalette.d.ts +9 -5
  23. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  24. package/dist/CommandPalette/CommandPalette.js +38 -20
  25. package/dist/CommandPalette/CommandPaletteEmpty.d.ts +4 -3
  26. package/dist/CommandPalette/CommandPaletteEmpty.d.ts.map +1 -1
  27. package/dist/CommandPalette/CommandPaletteEmpty.js +4 -3
  28. package/dist/CommandPalette/CommandPaletteInput.d.ts +5 -4
  29. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  30. package/dist/CommandPalette/CommandPaletteInput.js +35 -18
  31. package/dist/CommandPalette/CommandPaletteItem.d.ts +11 -6
  32. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  33. package/dist/CommandPalette/CommandPaletteItem.js +8 -9
  34. package/dist/CommandPalette/CommandPaletteList.d.ts +4 -2
  35. package/dist/CommandPalette/CommandPaletteList.d.ts.map +1 -1
  36. package/dist/CommandPalette/CommandPaletteList.js +10 -5
  37. package/dist/ComplexSelector/ComplexSelector.d.ts +8 -5
  38. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  39. package/dist/ComplexSelector/ComplexSelector.js +12 -3
  40. package/dist/ContextMenu/ContextMenu.d.ts +3 -3
  41. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  42. package/dist/ContextMenu/ContextMenu.js +62 -48
  43. package/dist/DateInput/DateInput.js +1 -1
  44. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  45. package/dist/DateInput/MonthScroller.js +12 -4
  46. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  47. package/dist/DateRangeInput/DateRangeInput.js +12 -2
  48. package/dist/Dialog/Dialog.d.ts +1 -1
  49. package/dist/Dialog/Dialog.d.ts.map +1 -1
  50. package/dist/Dialog/Dialog.js +16 -5
  51. package/dist/Dialog/DialogHeader.d.ts +1 -1
  52. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  53. package/dist/Dialog/DialogHeader.js +7 -7
  54. package/dist/Divider/Divider.d.ts +1 -0
  55. package/dist/Divider/Divider.d.ts.map +1 -1
  56. package/dist/Divider/Divider.js +5 -4
  57. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  58. package/dist/DropdownMenu/DropdownMenu.js +161 -65
  59. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +7 -5
  60. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  61. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +12 -5
  62. package/dist/Field/InputClearButton.d.ts +8 -4
  63. package/dist/Field/InputClearButton.d.ts.map +1 -1
  64. package/dist/Field/InputClearButton.js +23 -7
  65. package/dist/FormLayout/FormLayout.d.ts.map +1 -1
  66. package/dist/FormLayout/FormLayout.js +1 -0
  67. package/dist/Grid/Grid.d.ts +13 -4
  68. package/dist/Grid/Grid.d.ts.map +1 -1
  69. package/dist/Grid/Grid.js +7 -1
  70. package/dist/HoverCard/useHoverCard.d.ts +1 -1
  71. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  72. package/dist/HoverCard/useHoverCard.js +67 -49
  73. package/dist/InputGroup/InputGroup.d.ts +2 -4
  74. package/dist/InputGroup/InputGroup.d.ts.map +1 -1
  75. package/dist/InputGroup/InputGroup.js +2 -4
  76. package/dist/InputGroup/groupStyles.d.ts +10 -2
  77. package/dist/InputGroup/groupStyles.d.ts.map +1 -1
  78. package/dist/InputGroup/groupStyles.js +19 -2
  79. package/dist/Layer/useLayerDismissal.d.ts +9 -9
  80. package/dist/Layer/useLayerDismissal.js +9 -9
  81. package/dist/Markdown/Markdown.d.ts.map +1 -1
  82. package/dist/Markdown/Markdown.js +25 -4
  83. package/dist/MobileNav/MobileNav.d.ts +7 -0
  84. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  85. package/dist/MobileNav/MobileNav.js +45 -2
  86. package/dist/MultiSelector/MultiSelector.d.ts +7 -4
  87. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  88. package/dist/MultiSelector/MultiSelector.js +24 -3
  89. package/dist/NumberInput/NumberInput.js +1 -1
  90. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  91. package/dist/PowerSearch/PowerSearchEditPopover.js +6 -0
  92. package/dist/Resizable/ResizeHandle.d.ts +1 -1
  93. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  94. package/dist/Resizable/ResizeHandle.js +83 -15
  95. package/dist/Resizable/useResizable.d.ts +23 -0
  96. package/dist/Resizable/useResizable.d.ts.map +1 -1
  97. package/dist/Resizable/useResizable.js +78 -0
  98. package/dist/Selector/Selector.d.ts +6 -3
  99. package/dist/Selector/Selector.d.ts.map +1 -1
  100. package/dist/Selector/Selector.js +28 -6
  101. package/dist/SideNav/SideNav.d.ts.map +1 -1
  102. package/dist/SideNav/SideNav.js +10 -2
  103. package/dist/Slider/Slider.d.ts.map +1 -1
  104. package/dist/Slider/Slider.js +18 -7
  105. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  106. package/dist/Tokenizer/Tokenizer.js +16 -4
  107. package/dist/Toolbar/Toolbar.d.ts +2 -1
  108. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  109. package/dist/Toolbar/Toolbar.js +3 -1
  110. package/dist/TreeList/TreeList.d.ts.map +1 -1
  111. package/dist/TreeList/TreeList.js +6 -3
  112. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  113. package/dist/TreeList/TreeListItem.js +7 -1
  114. package/dist/astryx.css +22 -0
  115. package/dist/hooks/useScrollOverflow.d.ts.map +1 -1
  116. package/dist/hooks/useScrollOverflow.js +55 -12
  117. package/dist/hooks/useTreeFocus.d.ts +21 -0
  118. package/dist/hooks/useTreeFocus.d.ts.map +1 -1
  119. package/dist/hooks/useTreeFocus.js +40 -2
  120. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  121. package/dist/i18n/generated-locales/en.generated.js +3 -0
  122. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  123. package/dist/i18n/generated-locales/pseudo.generated.js +3 -0
  124. package/dist/reset.a11y.chromium.spec.js +6 -1
  125. package/dist/reset.until-found.a11y.browser.spec.d.ts +2 -0
  126. package/dist/reset.until-found.a11y.browser.spec.d.ts.map +1 -0
  127. package/dist/reset.until-found.a11y.browser.spec.js +71 -0
  128. package/dist/theme/syntax/SyntaxTheme.d.ts.map +1 -1
  129. package/dist/theme/syntax/SyntaxTheme.js +43 -2
  130. package/dist/theme/syntax/defineSyntaxTheme.d.ts +18 -5
  131. package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
  132. package/dist/theme/syntax/defineSyntaxTheme.js +38 -9
  133. package/dist/theme/syntax/index.d.ts +1 -1
  134. package/dist/theme/syntax/index.d.ts.map +1 -1
  135. package/dist/theme/syntax/tokens.d.ts +12 -2
  136. package/dist/theme/syntax/tokens.d.ts.map +1 -1
  137. package/dist/theme/syntax/tokens.js +25 -4
  138. package/dist/utils/plainDate.d.ts +12 -0
  139. package/dist/utils/plainDate.d.ts.map +1 -1
  140. package/dist/utils/plainDate.js +63 -10
  141. package/locales/en.json +13 -1
  142. package/locales/pseudo.json +9 -0
  143. package/package.json +4 -4
  144. package/src/AlertDialog/AlertDialog.doc.mjs +6 -0
  145. package/src/AlertDialog/AlertDialog.spec.md +205 -0
  146. package/src/AlertDialog/AlertDialog.test.tsx +3 -14
  147. package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +115 -0
  148. package/src/AlertDialog/__tests__/AlertDialog.a11y.renders.tsx +52 -0
  149. package/src/AlertDialog/__tests__/AlertDialog.a11y.states.ts +68 -0
  150. package/src/AlertDialog/__tests__/AlertDialog.a11y.test.tsx +144 -0
  151. package/src/Button/Button.test.tsx +37 -0
  152. package/src/Button/Button.tsx +5 -1
  153. package/src/Calendar/Calendar.doc.mjs +234 -60
  154. package/src/Calendar/Calendar.spec.md +209 -0
  155. package/src/Calendar/Calendar.test.tsx +559 -20
  156. package/src/Calendar/Calendar.tsx +258 -41
  157. package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +126 -0
  158. package/src/Calendar/__tests__/Calendar.a11y.known-failures.ts +12 -0
  159. package/src/Calendar/__tests__/Calendar.a11y.renders.tsx +57 -0
  160. package/src/Calendar/__tests__/Calendar.a11y.states.ts +139 -0
  161. package/src/Calendar/__tests__/Calendar.a11y.test.tsx +137 -0
  162. package/src/Carousel/Carousel.test.tsx +54 -71
  163. package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +192 -0
  164. package/src/Carousel/__tests__/Carousel.a11y.known-failures.ts +27 -0
  165. package/src/Carousel/__tests__/Carousel.a11y.renders.tsx +39 -0
  166. package/src/Carousel/__tests__/Carousel.a11y.states.ts +139 -0
  167. package/src/Carousel/__tests__/Carousel.a11y.test.tsx +127 -0
  168. package/src/Chat/ChatComposerInput.doc.mjs +1 -1
  169. package/src/Chat/ChatComposerInput.spec.md +94 -55
  170. package/src/Chat/ChatComposerInput.test.tsx +202 -1
  171. package/src/Chat/ChatComposerInput.tsx +13 -0
  172. package/src/Chat/ChatMessage.doc.mjs +31 -16
  173. package/src/Chat/ChatMessage.spec.md +265 -0
  174. package/src/Chat/ChatMessage.test.tsx +246 -0
  175. package/src/Chat/ChatMessage.tsx +30 -7
  176. package/src/Chat/ChatSystemMessage.spec.md +36 -37
  177. package/src/Chat/ChatSystemMessage.test.tsx +4 -4
  178. package/src/Chat/useTriggerMenu.test.tsx +994 -0
  179. package/src/Chat/useTriggerMenu.tsx +278 -46
  180. package/src/CodeBlock/CodeBlock.doc.mjs +7 -0
  181. package/src/CodeBlock/CodeBlock.spec.md +40 -41
  182. package/src/CodeBlock/CodeBlock.test.tsx +89 -9
  183. package/src/CodeBlock/CodeBlock.tsx +205 -25
  184. package/src/CodeBlock/__tests__/CodeBlockOverflowStop.a11y.chromium.spec.ts +125 -0
  185. package/src/CodeBlock/__tests__/DiffRowPaint.a11y.chromium.spec.ts +93 -0
  186. package/src/CodeBlock/tokenizer.test.ts +112 -0
  187. package/src/CodeBlock/tokenizer.ts +106 -0
  188. package/src/Collapsible/CollapsibleGroup.spec.md +17 -8
  189. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.renders.tsx +45 -0
  190. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.states.ts +139 -0
  191. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.test.tsx +135 -0
  192. package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +128 -0
  193. package/src/CommandPalette/CommandPalette.doc.mjs +126 -23
  194. package/src/CommandPalette/CommandPalette.spec.md +100 -49
  195. package/src/CommandPalette/CommandPalette.test.tsx +241 -2
  196. package/src/CommandPalette/CommandPalette.tsx +47 -21
  197. package/src/CommandPalette/CommandPaletteEmpty.tsx +4 -3
  198. package/src/CommandPalette/CommandPaletteInput.doc.mjs +61 -4
  199. package/src/CommandPalette/CommandPaletteInput.spec.md +215 -0
  200. package/src/CommandPalette/CommandPaletteInput.test.tsx +375 -51
  201. package/src/CommandPalette/CommandPaletteInput.tsx +74 -21
  202. package/src/CommandPalette/CommandPaletteItem.doc.mjs +73 -22
  203. package/src/CommandPalette/CommandPaletteItem.spec.md +223 -0
  204. package/src/CommandPalette/CommandPaletteItem.test.tsx +200 -5
  205. package/src/CommandPalette/CommandPaletteItem.tsx +16 -11
  206. package/src/CommandPalette/CommandPaletteList.doc.mjs +3 -3
  207. package/src/CommandPalette/CommandPaletteList.spec.md +41 -36
  208. package/src/CommandPalette/CommandPaletteList.test.tsx +80 -0
  209. package/src/CommandPalette/CommandPaletteList.tsx +10 -5
  210. package/src/CommandPalette/__tests__/CommandPalette.a11y.chromium.spec.ts +599 -0
  211. package/src/ComplexSelector/ComplexSelector.doc.mjs +10 -8
  212. package/src/ComplexSelector/ComplexSelector.spec.md +14 -9
  213. package/src/ComplexSelector/ComplexSelector.test.tsx +64 -16
  214. package/src/ComplexSelector/ComplexSelector.tsx +37 -8
  215. package/src/ContextMenu/ContextMenu.doc.mjs +6 -0
  216. package/src/ContextMenu/ContextMenu.spec.md +51 -53
  217. package/src/ContextMenu/ContextMenu.test.tsx +132 -1
  218. package/src/ContextMenu/ContextMenu.tsx +78 -48
  219. package/src/DateInput/DateInput.doc.mjs +52 -0
  220. package/src/DateInput/DateInput.spec.md +27 -15
  221. package/src/DateInput/DateInput.test.tsx +38 -0
  222. package/src/DateInput/DateInput.tsx +1 -1
  223. package/src/DateInput/DateInputTouch.test.tsx +22 -0
  224. package/src/DateInput/MonthScroller.tsx +12 -4
  225. package/src/DateRangeInput/DateRangeInput.test.tsx +49 -0
  226. package/src/DateRangeInput/DateRangeInput.tsx +20 -2
  227. package/src/Dialog/Dialog.test.tsx +27 -1
  228. package/src/Dialog/Dialog.tsx +16 -5
  229. package/src/Dialog/DialogHeader.test.tsx +27 -2
  230. package/src/Dialog/DialogHeader.tsx +7 -8
  231. package/src/Dialog/__tests__/Dialog.a11y.chromium.spec.ts +21 -14
  232. package/src/Divider/Divider.doc.mjs +5 -3
  233. package/src/Divider/Divider.spec.md +140 -54
  234. package/src/Divider/Divider.test.tsx +15 -0
  235. package/src/Divider/Divider.tsx +6 -4
  236. package/src/DropdownMenu/DropdownMenu.doc.mjs +2 -2
  237. package/src/DropdownMenu/DropdownMenu.spec.md +121 -45
  238. package/src/DropdownMenu/DropdownMenu.test.tsx +288 -23
  239. package/src/DropdownMenu/DropdownMenu.tsx +212 -81
  240. package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +38 -2
  241. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +32 -18
  242. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +146 -1
  243. package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +136 -0
  244. package/src/DropdownMenu/__tests__/MenuButton.a11y.known-failures.ts +12 -0
  245. package/src/DropdownMenu/__tests__/MenuButton.a11y.renders.tsx +67 -0
  246. package/src/DropdownMenu/__tests__/MenuButton.a11y.states.ts +141 -0
  247. package/src/DropdownMenu/__tests__/MenuButton.a11y.test.tsx +138 -0
  248. package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +40 -5
  249. package/src/Field/InputClearButton.test.tsx +29 -4
  250. package/src/Field/InputClearButton.tsx +26 -8
  251. package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +21 -14
  252. package/src/FormLayout/FormLayout.tsx +1 -0
  253. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +1 -1
  254. package/src/Grid/Grid.doc.mjs +14 -11
  255. package/src/Grid/Grid.spec.md +50 -32
  256. package/src/Grid/Grid.tsx +13 -4
  257. package/src/HoverCard/HoverCard.test.tsx +46 -1
  258. package/src/HoverCard/useHoverCard.tsx +68 -48
  259. package/src/InputGroup/InputGroup.doc.mjs +11 -1
  260. package/src/InputGroup/InputGroup.tsx +2 -4
  261. package/src/InputGroup/groupStyles.ts +40 -2
  262. package/src/Layer/useLayerDismissal.ts +9 -9
  263. package/src/Link/__tests__/Link.a11y.chromium.spec.ts +142 -0
  264. package/src/Link/__tests__/Link.a11y.renders.tsx +97 -0
  265. package/src/Link/__tests__/Link.a11y.states.ts +104 -0
  266. package/src/Link/__tests__/Link.a11y.test.tsx +127 -0
  267. package/src/Markdown/Markdown.doc.mjs +1 -1
  268. package/src/Markdown/Markdown.fr23.perf.test.ts +161 -40
  269. package/src/Markdown/Markdown.spec.md +45 -23
  270. package/src/Markdown/Markdown.test.tsx +38 -0
  271. package/src/Markdown/Markdown.tsx +24 -2
  272. package/src/Markdown/plugins/semanticFence.test.tsx +3 -2
  273. package/src/MobileNav/MobileNav.tsx +50 -2
  274. package/src/MobileNav/MobileNavFocus.test.tsx +553 -0
  275. package/src/MoreMenu/MoreMenu.spec.md +18 -7
  276. package/src/MoreMenu/MoreMenu.test.tsx +38 -0
  277. package/src/MultiSelector/MultiSelector.doc.mjs +8 -8
  278. package/src/MultiSelector/MultiSelector.spec.md +1 -1
  279. package/src/MultiSelector/MultiSelector.test.tsx +188 -21
  280. package/src/MultiSelector/MultiSelector.tsx +50 -11
  281. package/src/NumberInput/NumberInput.test.tsx +24 -0
  282. package/src/NumberInput/NumberInput.tsx +1 -1
  283. package/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts +22 -15
  284. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +78 -0
  285. package/src/PowerSearch/PowerSearchEditPopover.tsx +6 -0
  286. package/src/Resizable/Resizable.spec.md +41 -9
  287. package/src/Resizable/ResizeHandle.test.tsx +565 -2
  288. package/src/Resizable/ResizeHandle.tsx +126 -21
  289. package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +119 -0
  290. package/src/Resizable/__tests__/ResizeHandle.a11y.known-failures.ts +39 -0
  291. package/src/Resizable/__tests__/ResizeHandle.a11y.renders.tsx +80 -0
  292. package/src/Resizable/__tests__/ResizeHandle.a11y.states.ts +197 -0
  293. package/src/Resizable/__tests__/ResizeHandle.a11y.test.tsx +123 -0
  294. package/src/Resizable/useResizable.ts +95 -0
  295. package/src/Selector/Selector.doc.mjs +7 -6
  296. package/src/Selector/Selector.spec.md +37 -24
  297. package/src/Selector/Selector.test.tsx +176 -10
  298. package/src/Selector/Selector.tsx +59 -14
  299. package/src/Selector/__tests__/Combobox.a11y.known-failures.ts +27 -0
  300. package/src/Selector/__tests__/Combobox.a11y.states.ts +145 -0
  301. package/src/Selector/__tests__/Selector.combobox.a11y.chromium.spec.ts +108 -0
  302. package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +126 -0
  303. package/src/SideNav/SideNav.test.tsx +35 -0
  304. package/src/SideNav/SideNav.tsx +16 -2
  305. package/src/Slider/Slider.spec.md +43 -17
  306. package/src/Slider/Slider.test.tsx +134 -0
  307. package/src/Slider/Slider.tsx +29 -6
  308. package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +112 -0
  309. package/src/Slider/__tests__/Slider.a11y.known-failures.ts +12 -0
  310. package/src/Slider/__tests__/Slider.a11y.renders.tsx +42 -0
  311. package/src/Slider/__tests__/Slider.a11y.states.ts +106 -0
  312. package/src/Slider/__tests__/Slider.a11y.test.tsx +105 -0
  313. package/src/Switch/__tests__/Switch.a11y.chromium.spec.ts +25 -18
  314. package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +19 -12
  315. package/src/TextInput/__tests__/TextInput.a11y.chromium.spec.ts +21 -14
  316. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +18 -11
  317. package/src/Tokenizer/Tokenizer.test.tsx +87 -0
  318. package/src/Tokenizer/Tokenizer.tsx +31 -3
  319. package/src/Toolbar/Toolbar.test.tsx +131 -4
  320. package/src/Toolbar/Toolbar.tsx +3 -1
  321. package/src/TreeList/TreeList.spec.md +33 -8
  322. package/src/TreeList/TreeList.test.tsx +72 -0
  323. package/src/TreeList/TreeList.tsx +10 -6
  324. package/src/TreeList/TreeListItem.tsx +5 -0
  325. package/src/TreeList/__tests__/TreeList.a11y.chromium.spec.ts +125 -0
  326. package/src/TreeList/__tests__/TreeList.a11y.renders.tsx +44 -0
  327. package/src/TreeList/__tests__/TreeList.a11y.states.ts +164 -0
  328. package/src/TreeList/__tests__/TreeList.a11y.test.tsx +138 -0
  329. package/src/TreeList/__tests__/TreeListRowNames.a11y.chromium.spec.ts +82 -0
  330. package/src/hooks/useScrollOverflow.test.tsx +222 -0
  331. package/src/hooks/useScrollOverflow.ts +55 -13
  332. package/src/hooks/useTreeFocus.test.tsx +37 -0
  333. package/src/hooks/useTreeFocus.ts +55 -2
  334. package/src/i18n/generated-locales/en.generated.ts +3 -0
  335. package/src/i18n/generated-locales/pseudo.generated.ts +3 -0
  336. package/src/reset.a11y.chromium.spec.ts +9 -1
  337. package/src/reset.css +8 -3
  338. package/src/reset.until-found.a11y.browser.spec.ts +75 -0
  339. package/src/reset.until-found.test.ts +32 -0
  340. package/src/stylexValidation.source-build.test.mjs +42 -0
  341. package/src/theme/syntax/SyntaxTheme.tsx +46 -5
  342. package/src/theme/syntax/defineSyntaxTheme.diff.test.ts +91 -0
  343. package/src/theme/syntax/defineSyntaxTheme.ts +75 -15
  344. package/src/theme/syntax/index.ts +5 -1
  345. package/src/theme/syntax/tokens.ts +32 -2
  346. package/src/theme/syntax/useSyntaxTheme.diff.test.tsx +40 -0
  347. package/src/utils/plainDate.test.ts +147 -0
  348. package/src/utils/plainDate.ts +75 -10
@@ -81,17 +81,19 @@ describe('CodeBlock', () => {
81
81
  expect(root).toHaveStyle({opacity: '0.75'});
82
82
  });
83
83
 
84
- it('makes the scroll container keyboard-focusable', () => {
84
+ it('names the code viewport and gives it a tab stop only while it overflows', () => {
85
+ // jsdom has no layout, so the viewport fits: a fitting viewport is not a
86
+ // tab stop (AST-025 FR12); the overflowing states are browser evidence.
85
87
  render(<CodeBlock code="const x = 1;" language="javascript" />);
86
88
  const region = screen.getByRole('group');
87
- expect(region).toHaveAttribute('tabindex', '0');
89
+ expect(region).not.toHaveAttribute('tabindex');
88
90
  expect(region).toHaveAttribute('aria-label', 'javascript');
91
+ expect(region).toHaveAttribute('data-scroll-axis', 'both');
89
92
  });
90
93
 
91
94
  it('labels the scroll region "Code" when no language label is shown', () => {
92
95
  render(<CodeBlock code="hello" hasLanguageLabel={false} />);
93
96
  const region = screen.getByRole('group');
94
- expect(region).toHaveAttribute('tabindex', '0');
95
97
  expect(region).toHaveAttribute('aria-label', 'Code');
96
98
  });
97
99
 
@@ -119,6 +121,84 @@ describe('CodeBlock', () => {
119
121
  expect(screen.getByRole('group')).toHaveStyle({maxHeight: '0px'});
120
122
  });
121
123
 
124
+ describe('diff layer', () => {
125
+ const diff =
126
+ '@@ -1,2 +1,2 @@\n- const a = 1;\n+ const a = 2;\n const b = 3;';
127
+
128
+ it('marks added, removed and header rows and leaves context unmarked', () => {
129
+ const {container} = render(
130
+ <CodeBlock code={diff} language="typescript" isDiff />,
131
+ );
132
+ const rows = container.querySelectorAll('[data-line]');
133
+ expect(rows).toHaveLength(4);
134
+ expect(rows[0].getAttribute('data-diff')).toBe('header');
135
+ expect(rows[1].getAttribute('data-diff')).toBe('removed');
136
+ expect(rows[2].getAttribute('data-diff')).toBe('added');
137
+ expect(rows[3].getAttribute('data-diff')).toBeNull();
138
+ });
139
+
140
+ it('puts the glyph in its own marker column and keeps base-language tokens past it', () => {
141
+ const {container} = render(
142
+ <CodeBlock code={diff} language="typescript" isDiff />,
143
+ );
144
+ const rows = container.querySelectorAll('[data-line]');
145
+ const marker = (row: Element) =>
146
+ row.querySelector('[data-diff-marker]')?.textContent;
147
+ expect(marker(rows[1])).toBe('-');
148
+ expect(marker(rows[2])).toBe('+');
149
+ // The glyph is the source's own text, so assistive technology reads it.
150
+ expect(rows[1].querySelector('[data-diff-marker]')).not.toHaveAttribute(
151
+ 'aria-hidden',
152
+ );
153
+ expect(marker(rows[3])).toBe(' ');
154
+ // The code column starts after the glyph on every line: the removed and
155
+ // context lines' content keeps its own two-space indentation.
156
+ const content = (row: Element) =>
157
+ row.querySelectorAll('span')[1]?.textContent;
158
+ expect(content(rows[1])).toBe(' const a = 1;');
159
+ expect(content(rows[3])).toBe(' const b = 3;');
160
+ // `const` is a keyword on changed and context lines alike.
161
+ expect(rows[1].querySelector('.astryx-token-keyword')).not.toBeNull();
162
+ expect(rows[3].querySelector('.astryx-token-keyword')).not.toBeNull();
163
+ });
164
+
165
+ it('keeps the header and names the viewport diff / tsx diff', () => {
166
+ const {rerender} = render(<CodeBlock code={diff} isDiff />);
167
+ expect(screen.getByRole('group', {name: 'diff'})).toBeInTheDocument();
168
+ expect(screen.getByText('diff')).toBeInTheDocument();
169
+ rerender(<CodeBlock code={diff} language="tsx" isDiff />);
170
+ expect(screen.getByRole('group', {name: 'tsx diff'})).toBeInTheDocument();
171
+ expect(screen.getByText('tsx diff')).toBeInTheDocument();
172
+ });
173
+
174
+ it('copies the exact source, glyphs included', () => {
175
+ render(<CodeBlock code={diff} language="typescript" isDiff />);
176
+ fireEvent.click(screen.getByRole('button', {name: 'Copy code'}));
177
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(diff);
178
+ });
179
+
180
+ it('composes with highlightLines and does nothing without isDiff', () => {
181
+ const {container, rerender} = render(
182
+ <CodeBlock
183
+ code={diff}
184
+ language="typescript"
185
+ isDiff
186
+ highlightLines={[2]}
187
+ />,
188
+ );
189
+ expect(
190
+ container
191
+ .querySelector('[data-diff="removed"]')
192
+ ?.getAttribute('data-line'),
193
+ ).toBe('2');
194
+ rerender(<CodeBlock code={diff} language="typescript" />);
195
+ expect(container.querySelector('[data-diff]')).toBeNull();
196
+ expect(
197
+ container.querySelector('[data-line] [aria-hidden="true"]'),
198
+ ).toBeNull();
199
+ });
200
+ });
201
+
122
202
  it('copies code when the copy button is clicked', () => {
123
203
  render(<CodeBlock code="const x = 1;" language="javascript" />);
124
204
  const copyButton = screen.getByRole('button', {name: 'Copy code'});
@@ -371,9 +451,9 @@ describe('CodeBlock', () => {
371
451
 
372
452
  fireEvent.click(header);
373
453
  expect(header).toHaveAttribute('aria-expanded', 'false');
374
- // Collapsed: the wrapper is inert, so the keyboard-focusable scroll
375
- // container (tabIndex=0) inside it drops out of the tab order and the
376
- // accessibility tree instead of remaining an invisible tab stop.
454
+ // Collapsed: the wrapper is inert, so the scroll container inside it
455
+ // drops out of the tab order and the accessibility tree instead of
456
+ // remaining an invisible tab stop while it overflows.
377
457
  expect(region).toHaveAttribute('inert');
378
458
  const scrollContainer = screen.getByRole('group');
379
459
  expect(scrollContainer.closest('[inert]')).toBe(region);
@@ -399,12 +479,12 @@ describe('CodeBlock', () => {
399
479
  expect(region).toHaveAttribute('inert');
400
480
  fireEvent.click(header);
401
481
  expect(header).toHaveAttribute('aria-expanded', 'true');
402
- // Expanded again: inert is removed and the scroll container is a
403
- // keyboard-focusable group once more.
482
+ // Expanded again: inert is removed and the scroll container is a named
483
+ // group once more (its tab stop follows overflow, see the browser spec).
404
484
  expect(region).not.toHaveAttribute('inert');
405
485
  const scrollContainer = screen.getByRole('group');
406
486
  expect(scrollContainer.closest('[inert]')).toBeNull();
407
- expect(scrollContainer).toHaveAttribute('tabindex', '0');
487
+ expect(scrollContainer).toHaveAttribute('data-scroll-axis', 'both');
408
488
  });
409
489
 
410
490
  it('applies a per-instance syntax theme via the syntaxTheme prop', () => {
@@ -34,11 +34,14 @@ import {
34
34
  } from '../theme/tokens.stylex';
35
35
  import {mergeProps} from '../utils';
36
36
  import {useClipboard} from '../hooks/useClipboard';
37
+ import {useScrollableArea} from '../hooks/useScrollableArea';
37
38
  import {Icon} from '../Icon';
38
39
  import {IconButton} from '../IconButton';
40
+ import type {DiffLineKind} from './tokenizer';
39
41
  import {
40
42
  tokenize,
41
43
  tokenizeAsync,
44
+ splitDiffLines,
42
45
  flatTokensToLines,
43
46
  SYNC_TOKENIZE_THRESHOLD,
44
47
  } from './tokenizer';
@@ -117,6 +120,13 @@ const styles = stylex.create({
117
120
  backgroundColor: 'var(--color-syntax-background)',
118
121
  overflow: 'hidden',
119
122
  },
123
+ // The viewport is a flex item of the column root. Its implied min-height
124
+ // would be its content while it fits (the shared capability keeps it
125
+ // `overflow: clip` then), so a root sized from outside could never make it
126
+ // shrink and scroll; 0 lets the root's bound reach it.
127
+ viewport: {
128
+ minHeight: 0,
129
+ },
120
130
  headerRow: {
121
131
  display: 'flex',
122
132
  alignItems: 'center',
@@ -160,10 +170,6 @@ const styles = stylex.create({
160
170
  margin: 0,
161
171
  lineHeight: typeScaleVars['--text-supporting-leading'],
162
172
  },
163
- scrollContainer: {
164
- overflowX: 'auto',
165
- overflowY: 'auto',
166
- },
167
173
  codeWrapper: {
168
174
  display: 'flex',
169
175
  minWidth: 'fit-content',
@@ -228,6 +234,10 @@ const styles = stylex.create({
228
234
  code: {
229
235
  display: 'block',
230
236
  flex: 1,
237
+ // The block grows to its longest line, so a row's paint (diff tint,
238
+ // highlightLines) spans the full scroll width instead of stopping at the
239
+ // viewport edge when the reader scrolls a long line.
240
+ minWidth: 'max-content',
231
241
  paddingBlock: spacingVars['--spacing-3'],
232
242
  paddingInline: spacingVars['--spacing-4'],
233
243
  margin: 0,
@@ -239,6 +249,8 @@ const styles = stylex.create({
239
249
  overflowWrap: 'normal',
240
250
  },
241
251
  codeWrapped: {
252
+ // Wrapped lines never overflow, and max-content would stop them wrapping.
253
+ minWidth: 'auto',
242
254
  whiteSpace: 'pre-wrap',
243
255
  wordBreak: 'break-all',
244
256
  overflowWrap: 'break-word',
@@ -298,6 +310,75 @@ const styles = stylex.create({
298
310
  marginInline: `calc(-1 * ${spacingVars['--spacing-4']})`,
299
311
  paddingInline: spacingVars['--spacing-4'],
300
312
  },
313
+ // Diff change rows: the whole row is tinted so added and removed lines read
314
+ // at a glance; the text keeps its ordinary colour, which stays readable on
315
+ // the tint where the semantic text colours would not, and the leading +/-
316
+ // glyph stays in the text as the cue that needs no colour. A caller's
317
+ // highlightLines row is declared after these and keeps its background.
318
+ // A changed row's margin reads the change: the tint runs under the gutter
319
+ // cell too (the row spans the full width), and the line number and the
320
+ // glyph take the row's marker slot. The code — tokens and plain runs alike
321
+ // — keeps the syntax theme's colours, so an identifier looks the same on a
322
+ // changed row as on a context row.
323
+ lineAdded: {
324
+ backgroundColor: 'var(--color-syntax-added)',
325
+ marginInline: `calc(-1 * ${spacingVars['--spacing-4']})`,
326
+ paddingInline: spacingVars['--spacing-4'],
327
+ '::before': {color: 'var(--color-syntax-added-marker)'},
328
+ },
329
+ lineRemoved: {
330
+ backgroundColor: 'var(--color-syntax-removed)',
331
+ marginInline: `calc(-1 * ${spacingVars['--spacing-4']})`,
332
+ paddingInline: spacingVars['--spacing-4'],
333
+ '::before': {color: 'var(--color-syntax-removed-marker)'},
334
+ },
335
+ // Hunk and file headers stay visible but quiet: a muted row with secondary
336
+ // text, never change paint.
337
+ lineDiffHeader: {
338
+ backgroundColor: 'var(--color-syntax-diff-header)',
339
+ // The syntax theme's own secondary colour: text-secondary in the default
340
+ // theme, the theme's punctuation colour in a defined one.
341
+ color: 'var(--color-syntax-punctuation)',
342
+ marginInline: `calc(-1 * ${spacingVars['--spacing-4']})`,
343
+ paddingInline: spacingVars['--spacing-4'],
344
+ },
345
+ // The glyph column: the +, - or space of every line sits in its own track
346
+ // before the code, so the code column starts at the same inline offset on
347
+ // every line and the content's indentation reads exactly as written. The
348
+ // glyph is laid out as text but not selectable; the copy button still
349
+ // copies the original source with its glyphs.
350
+ lineDiff: {
351
+ display: 'grid',
352
+ gridTemplateColumns: '1ch 1fr',
353
+ columnGap: spacingVars['--spacing-2'],
354
+ },
355
+ // With line numbers the gutter's gap (which the full-height divider sits
356
+ // in) has to stay between the number and the marker, so the grid has no
357
+ // gap of its own and the marker carries both gaps as margins.
358
+ lineDiffNumbered: {
359
+ // `auto`, not `1ch`: the marker's gap margins have to widen its track.
360
+ gridTemplateColumns: 'var(--_codeblock-gutter-width) auto 1fr',
361
+ columnGap: 0,
362
+ },
363
+ // The glyph is the source's own text: read by assistive technology, kept
364
+ // out of a drag selection (copy returns the exact source anyway).
365
+ diffMarker: {
366
+ color: colorVars['--color-text-secondary'],
367
+ userSelect: 'none',
368
+ whiteSpace: 'pre',
369
+ },
370
+ diffMarkerNumbered: {
371
+ marginInlineStart: `calc(${spacingVars['--spacing-3']} + ${borderVars['--border-width']} + ${spacingVars['--spacing-4']})`,
372
+ marginInlineEnd: spacingVars['--spacing-2'],
373
+ },
374
+ // On a tinted row the glyph is the cue a reader without colour depends on,
375
+ // so it takes the row's marker slot like the line number does.
376
+ diffMarkerAdded: {
377
+ color: 'var(--color-syntax-added-marker)',
378
+ },
379
+ diffMarkerRemoved: {
380
+ color: 'var(--color-syntax-removed-marker)',
381
+ },
301
382
  sizeSm: {
302
383
  fontSize: typeScaleVars['--text-supporting-size'],
303
384
  },
@@ -332,12 +413,14 @@ const CodeChunk = React.memo(function CodeChunk({
332
413
  lines,
333
414
  startIndex,
334
415
  highlightSet,
416
+ diffKinds,
335
417
  renderLineContent,
336
418
  lineNumbers,
337
419
  }: {
338
420
  lines: string[];
339
421
  startIndex: number;
340
422
  highlightSet: Set<number> | null;
423
+ diffKinds: ReadonlyArray<DiffLineKind> | null;
341
424
  renderLineContent: (line: string, lineIndex: number) => React.ReactNode;
342
425
  lineNumbers: boolean;
343
426
  }) {
@@ -345,13 +428,22 @@ const CodeChunk = React.memo(function CodeChunk({
345
428
  <>
346
429
  {lines.map((line, j) => {
347
430
  const i = startIndex + j;
431
+ const diffKind = diffKinds?.[i];
348
432
  return (
349
433
  <div
350
434
  key={i}
351
435
  data-line={i + 1}
436
+ data-diff={diffKind === 'context' ? undefined : diffKind}
352
437
  {...stylex.props(
353
438
  styles.line,
354
439
  lineNumbers && styles.lineNumbered,
440
+ diffKinds != null && styles.lineDiff,
441
+ diffKinds != null && lineNumbers && styles.lineDiffNumbered,
442
+ diffKind === 'added' && styles.lineAdded,
443
+ diffKind === 'removed' && styles.lineRemoved,
444
+ diffKind === 'header' && styles.lineDiffHeader,
445
+ // Last on purpose: a highlight replaces a diff tint rather than
446
+ // stacking on it, so tokens keep their single-paint contrast.
355
447
  (highlightSet?.has(i + 1) ?? false) && styles.lineHighlighted,
356
448
  )}>
357
449
  {renderLineContent(line, i)}
@@ -365,6 +457,7 @@ const CodeChunk = React.memo(function CodeChunk({
365
457
  function renderLines(
366
458
  lines: string[],
367
459
  highlightSet: Set<number> | null,
460
+ diffKinds: ReadonlyArray<DiffLineKind> | null,
368
461
  renderLineContent: (line: string, lineIndex: number) => React.ReactNode,
369
462
  lineNumbers: boolean,
370
463
  chunkSize: number = LINE_CHUNK_SIZE,
@@ -377,6 +470,7 @@ function renderLines(
377
470
  lines={lines}
378
471
  startIndex={0}
379
472
  highlightSet={highlightSet}
473
+ diffKinds={diffKinds}
380
474
  renderLineContent={renderLineContent}
381
475
  lineNumbers={lineNumbers}
382
476
  />
@@ -399,6 +493,7 @@ function renderLines(
399
493
  lines={chunkLines}
400
494
  startIndex={start}
401
495
  highlightSet={highlightSet}
496
+ diffKinds={diffKinds}
402
497
  renderLineContent={renderLineContent}
403
498
  lineNumbers={lineNumbers}
404
499
  />
@@ -420,6 +515,15 @@ export interface CodeBlockProps extends BaseProps<HTMLPreElement> {
420
515
  hasLanguageLabel?: boolean;
421
516
  hasLineNumbers?: boolean;
422
517
  highlightLines?: number[];
518
+ /**
519
+ * Render the code as a unified diff over `language`: lines starting with
520
+ * `+` are added, `-` removed, `@@` is a hunk header (a `--- `/`+++ ` pair
521
+ * directly before one is a file header), anything else is context; the
522
+ * label reads `diff` or `<language> diff`. The glyph sits in its own column and the rest of every line
523
+ * keeps the language's syntax highlighting. Copy still returns the exact
524
+ * source, glyphs included.
525
+ */
526
+ isDiff?: boolean;
423
527
  hasCopyButton?: boolean;
424
528
  onCopy?: () => void;
425
529
  isWrapped?: boolean;
@@ -594,6 +698,8 @@ function SpanCodeContent({
594
698
  lines,
595
699
  tokenLines,
596
700
  highlightSet,
701
+ diffKinds,
702
+ diff,
597
703
  isWrapped,
598
704
  sizeStyle,
599
705
  hasLineNumbers,
@@ -602,6 +708,8 @@ function SpanCodeContent({
602
708
  lines: string[];
603
709
  tokenLines: TokenLine[];
604
710
  highlightSet: Set<number> | null;
711
+ diffKinds: ReadonlyArray<DiffLineKind> | null;
712
+ diff: ReturnType<typeof splitDiffLines> | null;
605
713
  isWrapped: boolean;
606
714
  sizeStyle: stylex.StyleXStyles;
607
715
  hasLineNumbers: boolean;
@@ -614,6 +722,31 @@ function SpanCodeContent({
614
722
  const renderLineContent = useCallback(
615
723
  (line: string, lineIndex: number): React.ReactNode => {
616
724
  const tokens = tokenLines[lineIndex] ?? [];
725
+ if (diff) {
726
+ // Header rows show their raw text; every other row shows its glyph in
727
+ // the marker column and the base-language tokens of the rest.
728
+ const kind = diff.kinds[lineIndex];
729
+ const content = kind === 'header' ? line : diff.content[lineIndex];
730
+ return (
731
+ <>
732
+ <span
733
+ data-diff-marker=""
734
+ {...stylex.props(
735
+ styles.diffMarker,
736
+ hasLineNumbers && styles.diffMarkerNumbered,
737
+ kind === 'added' && styles.diffMarkerAdded,
738
+ kind === 'removed' && styles.diffMarkerRemoved,
739
+ )}>
740
+ {diff.glyphs[lineIndex] || ' '}
741
+ </span>
742
+ <span {...stylex.props(styles.lineContent)}>
743
+ {kind === 'header'
744
+ ? content || '\u200b'
745
+ : buildSpanLine(content, tokens)}
746
+ </span>
747
+ </>
748
+ );
749
+ }
617
750
  // Wrap tokens in a single element so they occupy one grid cell when line
618
751
  // numbers are on (see `lineNumbered`); an inline span is a no-op when off.
619
752
  return (
@@ -622,7 +755,7 @@ function SpanCodeContent({
622
755
  </span>
623
756
  );
624
757
  },
625
- [tokenLines],
758
+ [tokenLines, diff, hasLineNumbers],
626
759
  );
627
760
 
628
761
  return (
@@ -634,7 +767,13 @@ function SpanCodeContent({
634
767
  hasLineNumbers && styles.codeNumbered,
635
768
  hasLineNumbers && dynamicStyles.gutterWidth(maxDigits),
636
769
  )}>
637
- {renderLines(lines, highlightSet, renderLineContent, hasLineNumbers)}
770
+ {renderLines(
771
+ lines,
772
+ highlightSet,
773
+ diffKinds,
774
+ renderLineContent,
775
+ hasLineNumbers,
776
+ )}
638
777
  </code>
639
778
  );
640
779
  }
@@ -647,6 +786,7 @@ function RangeCodeContent({
647
786
  lines,
648
787
  tokenLines,
649
788
  highlightSet,
789
+ diffKinds,
650
790
  isWrapped,
651
791
  sizeStyle,
652
792
  hasLineNumbers,
@@ -655,6 +795,7 @@ function RangeCodeContent({
655
795
  lines: string[];
656
796
  tokenLines: TokenLine[];
657
797
  highlightSet: Set<number> | null;
798
+ diffKinds: ReadonlyArray<DiffLineKind> | null;
658
799
  isWrapped: boolean;
659
800
  sizeStyle: stylex.StyleXStyles;
660
801
  hasLineNumbers: boolean;
@@ -694,7 +835,13 @@ function RangeCodeContent({
694
835
  hasLineNumbers && styles.codeNumbered,
695
836
  hasLineNumbers && dynamicStyles.gutterWidth(maxDigits),
696
837
  )}>
697
- {renderLines(lines, highlightSet, renderLineContent, hasLineNumbers)}
838
+ {renderLines(
839
+ lines,
840
+ highlightSet,
841
+ diffKinds,
842
+ renderLineContent,
843
+ hasLineNumbers,
844
+ )}
698
845
  </code>
699
846
  );
700
847
  }
@@ -719,6 +866,7 @@ export function CodeBlock({
719
866
  hasLanguageLabel = true,
720
867
  hasLineNumbers = false,
721
868
  highlightLines,
869
+ isDiff = false,
722
870
  hasCopyButton = true,
723
871
  onCopy,
724
872
  isWrapped = false,
@@ -746,6 +894,7 @@ export function CodeBlock({
746
894
  });
747
895
 
748
896
  const useSpans =
897
+ isDiff ||
749
898
  highlightMode === 'spans' ||
750
899
  (highlightMode === 'auto' && !hasHighlightAPI()) ||
751
900
  (highlightMode === 'auto' && isSafari());
@@ -758,8 +907,19 @@ export function CodeBlock({
758
907
  return l;
759
908
  }, [code]);
760
909
 
761
- const tokenLines = useTokenLines(code, language, customTokenizer);
762
-
910
+ // In diff mode the base language sees the code with the glyph column set
911
+ // aside, so a `tsx` diff still highlights as `tsx` on every line; headers
912
+ // contribute an empty line and are rendered as their raw text.
913
+ const diff = useMemo(
914
+ () => (isDiff ? splitDiffLines(lines) : null),
915
+ [isDiff, lines],
916
+ );
917
+ const tokenLines = useTokenLines(
918
+ diff ? diff.content.join('\n') : code,
919
+ language,
920
+ customTokenizer,
921
+ );
922
+ const diffKinds = diff?.kinds ?? null;
763
923
  const highlightSet = useMemo(
764
924
  () => (highlightLines ? new Set(highlightLines) : null),
765
925
  [highlightLines],
@@ -775,8 +935,18 @@ export function CodeBlock({
775
935
  const sizeStyle = size === 'sm' ? styles.sizeSm : styles.sizeMd;
776
936
  // Digits in the largest line number — sizes the gutter column width.
777
937
  const maxLineDigits = String(lines.length).length;
778
- const languageLabel =
779
- hasLanguageLabel && language !== 'plaintext' ? language : null;
938
+ // With the diff layer on, the label and viewport name say so: `diff` over
939
+ // plaintext, `tsx diff` over a language — so a diff keeps its header,
940
+ // collapse control, and a name that still matches a `diff` fence.
941
+ const languageLabel = !hasLanguageLabel
942
+ ? null
943
+ : isDiff
944
+ ? language === 'plaintext'
945
+ ? 'diff'
946
+ : `${language} diff`
947
+ : language !== 'plaintext'
948
+ ? language
949
+ : null;
780
950
  const showHeader = title != null || languageLabel != null;
781
951
 
782
952
  // A collapsed body must never outlive the visible header that controls it.
@@ -800,7 +970,19 @@ export function CodeBlock({
800
970
  // this is always a resolvable reference — aria-controls can be unconditional.
801
971
  const regionId = useId();
802
972
 
803
- const scrollContainerRef = useRef<HTMLDivElement>(null);
973
+ // The code viewport enters the Tab order only while its content actually
974
+ // overflows (AST-025 FR12–FR13): the shared scroll capability measures
975
+ // both axes, keeps focus where it is when overflow goes away, and owns the
976
+ // overflow declarations. role="group" (not "region") so several code
977
+ // blocks on a page don't create duplicate same-named landmarks.
978
+ const {getViewportProps, getContentProps} = useScrollableArea({
979
+ axis: 'both',
980
+ keyboardAccess: {
981
+ owner: 'viewport',
982
+ role: 'group',
983
+ label: languageLabel ?? t('@astryx.codeBlock.code'),
984
+ },
985
+ });
804
986
  const scrollStyle: CSSProperties | undefined =
805
987
  maxHeight != null
806
988
  ? {
@@ -895,27 +1077,24 @@ export function CodeBlock({
895
1077
 
896
1078
  const codeBody = (
897
1079
  <div
898
- ref={scrollContainerRef}
899
- // The scroll container is keyboard-focusable so keyboard users can
900
- // scroll long or wide code that overflows the viewport. Uses
901
- // role="group" (not "region") so multiple code blocks on a page don't
902
- // create duplicate same-named landmarks (axe: landmark-unique).
903
- tabIndex={0}
904
- role="group"
905
- aria-label={languageLabel ?? t('@astryx.codeBlock.code')}
906
- {...mergeProps(stylex.props(styles.scrollContainer), {
1080
+ {...getViewportProps<HTMLDivElement>({
1081
+ ...stylex.props(styles.viewport),
907
1082
  style: scrollStyle,
908
1083
  })}>
909
1084
  <div
910
- {...stylex.props(
911
- styles.codeWrapper,
912
- showHeader && !hasLineNumbers && styles.codeWrapperCompact,
1085
+ {...getContentProps<HTMLDivElement>(
1086
+ stylex.props(
1087
+ styles.codeWrapper,
1088
+ showHeader && !hasLineNumbers && styles.codeWrapperCompact,
1089
+ ),
913
1090
  )}>
914
1091
  {useSpans ? (
915
1092
  <SpanCodeContent
916
1093
  lines={lines}
917
1094
  tokenLines={tokenLines}
918
1095
  highlightSet={highlightSet}
1096
+ diffKinds={diffKinds}
1097
+ diff={diff}
919
1098
  isWrapped={isWrapped}
920
1099
  sizeStyle={sizeStyle}
921
1100
  hasLineNumbers={hasLineNumbers}
@@ -926,6 +1105,7 @@ export function CodeBlock({
926
1105
  lines={lines}
927
1106
  tokenLines={tokenLines}
928
1107
  highlightSet={highlightSet}
1108
+ diffKinds={diffKinds}
929
1109
  isWrapped={isWrapped}
930
1110
  sizeStyle={sizeStyle}
931
1111
  hasLineNumbers={hasLineNumbers}
@@ -958,7 +1138,7 @@ export function CodeBlock({
958
1138
  // While collapsed, the region is only hidden visually (0fr grid
959
1139
  // row); inert also removes it from the tab order and accessibility
960
1140
  // tree so keyboard users cannot Tab into the invisible scroll
961
- // container (tabIndex=0). aria-controls pointing at an inert
1141
+ // container (a tab stop while it overflows). aria-controls pointing at an inert
962
1142
  // element remains a valid, resolvable reference.
963
1143
  inert={isCollapsed ? true : undefined}
964
1144
  {...stylex.props(