@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
@@ -6,10 +6,22 @@
6
6
  * @file useScrollOverflow.ts
7
7
  * @input Uses React useRef, useState, useCallback, useEffect
8
8
  * @output Exports useScrollOverflow hook for tracking scroll position edges
9
- * @position Core hook; used by Carousel for fade-edge and button state
9
+ * @position Core hook; used by Carousel and TabList for fade-edge and button
10
+ * state
10
11
  *
11
12
  * Observes a scroll container and reports whether content overflows at the
12
- * start, end, or both edges. Updates on scroll and resize.
13
+ * start, end, or both edges. Updates on every scroll, and on a resize while
14
+ * the container is rendered: a layout read on an element the browser is not
15
+ * rendering (an ancestor with `display: none`, or a `content-visibility:
16
+ * hidden` subtree) either answers zeros or makes the browser lay out that
17
+ * whole hidden subtree for the answer, so a resize observation delivered for
18
+ * a hidden container, and the measurement at registration inside one, are
19
+ * skipped; the hook keeps its last state until the resize observation that
20
+ * follows the reveal (one frame after it paints). A scroll is measured
21
+ * whether or not the container is rendered: a scroll of a hidden container
22
+ * is script's (a selected tab revealed under a hidden panel), its layout is
23
+ * already the scroll's, and a reveal at the same size brings no resize to
24
+ * measure it with.
13
25
  *
14
26
  * SYNC: When modified, update:
15
27
  * - /packages/core/src/hooks/index.ts
@@ -27,6 +39,19 @@ export interface ScrollOverflowState {
27
39
  hasOverflow: boolean;
28
40
  }
29
41
 
42
+ /**
43
+ * Whether the browser is rendering `el`, so a layout read on it answers for a
44
+ * real box. `checkVisibility()` is false under a `display: none` ancestor and
45
+ * inside a `content-visibility: hidden` subtree, and it answers without
46
+ * rendering that subtree. Not `contentVisibilityAuto`: a freshly inserted,
47
+ * on-screen `content-visibility: auto` section counts as skipped until the
48
+ * next rendering update, and a strip mounted into one would paint its first
49
+ * frame unmeasured. A browser without `checkVisibility` measures as before.
50
+ */
51
+ function isRendered(el: HTMLElement): boolean {
52
+ return typeof el.checkVisibility !== 'function' || el.checkVisibility();
53
+ }
54
+
30
55
  /**
31
56
  * Tracks scroll overflow state for a horizontally scrollable container.
32
57
  *
@@ -49,12 +74,7 @@ export function useScrollOverflow() {
49
74
  hasOverflow: false,
50
75
  });
51
76
 
52
- const measure = useCallback(() => {
53
- const el = elRef.current;
54
- if (!el) {
55
- return;
56
- }
57
-
77
+ const measureNow = useCallback((el: HTMLElement) => {
58
78
  const tolerance = 1;
59
79
  const {scrollLeft, scrollWidth, clientWidth} = el;
60
80
  const maxScroll = scrollWidth - clientWidth;
@@ -75,22 +95,44 @@ export function useScrollOverflow() {
75
95
  });
76
96
  }, []);
77
97
 
98
+ // Registration and resize: only a rendered container. A hidden one has no
99
+ // edges to report, and reading its layout would make the browser render
100
+ // the hidden subtree it sits in; the resize observation that follows the
101
+ // reveal measures again.
102
+ const measure = useCallback(() => {
103
+ const el = elRef.current;
104
+ if (el && isRendered(el)) {
105
+ measureNow(el);
106
+ }
107
+ }, [measureNow]);
108
+
109
+ // A scroll: measured hidden or not. Script scrolled the hidden container
110
+ // (its layout is already the scroll's), and a reveal at the same size
111
+ // brings no resize observation, so this is the read that keeps the edges
112
+ // right for it.
113
+ const measureOnScroll = useCallback(() => {
114
+ const el = elRef.current;
115
+ if (el) {
116
+ measureNow(el);
117
+ }
118
+ }, [measureNow]);
119
+
78
120
  const scrollRef = useCallback(
79
121
  (el: HTMLElement | null) => {
80
122
  if (elRef.current) {
81
123
  unobserveRef.current?.();
82
124
  unobserveRef.current = null;
83
- elRef.current.removeEventListener('scroll', measure);
125
+ elRef.current.removeEventListener('scroll', measureOnScroll);
84
126
  }
85
127
 
86
128
  elRef.current = el;
87
129
 
88
130
  if (el) {
89
- el.addEventListener('scroll', measure, {passive: true});
131
+ el.addEventListener('scroll', measureOnScroll, {passive: true});
90
132
  unobserveRef.current = observeResize(el, measure);
91
133
  }
92
134
  },
93
- [measure],
135
+ [measure, measureOnScroll],
94
136
  );
95
137
 
96
138
  useEffect(() => {
@@ -98,10 +140,10 @@ export function useScrollOverflow() {
98
140
  if (elRef.current) {
99
141
  unobserveRef.current?.();
100
142
  unobserveRef.current = null;
101
- elRef.current.removeEventListener('scroll', measure);
143
+ elRef.current.removeEventListener('scroll', measureOnScroll);
102
144
  }
103
145
  };
104
- }, [measure]);
146
+ }, [measureOnScroll]);
105
147
 
106
148
  return {
107
149
  scrollRef,
@@ -262,6 +262,43 @@ describe('useTreeFocus activation + typeahead', () => {
262
262
  expect(screen.getByTestId('c')).toHaveFocus();
263
263
  });
264
264
 
265
+ it('typeahead matches whole text for a direct caller, even when an item carries aria-labelledby and the options smuggle the internal flag', () => {
266
+ function LabelledTree() {
267
+ // An untyped options object trying to reach the internal flag through
268
+ // the public hook: it must have no effect.
269
+ const smuggled = {
270
+ hasRovingTabIndex: true,
271
+ matchLabelledBy: true,
272
+ } as Record<string, unknown>;
273
+ const {treeRef, handleKeyDown, handleFocus} =
274
+ useTreeFocus<HTMLUListElement>(smuggled);
275
+ return (
276
+ <ul
277
+ ref={treeRef}
278
+ role="tree"
279
+ onKeyDown={handleKeyDown}
280
+ onFocus={handleFocus}>
281
+ <li role="treeitem" tabIndex={0} data-testid="first">
282
+ First
283
+ </li>
284
+ <li
285
+ role="treeitem"
286
+ tabIndex={-1}
287
+ aria-labelledby="lbl"
288
+ data-testid="labelled">
289
+ <span>zz</span> <span id="lbl">Budget</span>
290
+ </li>
291
+ </ul>
292
+ );
293
+ }
294
+ render(<LabelledTree />);
295
+ const tree = screen.getByRole('tree');
296
+ screen.getByTestId('first').focus();
297
+ // Released behavior: the text content ("zz Budget") is what matches.
298
+ fireEvent.keyDown(tree, {key: 'z'});
299
+ expect(screen.getByTestId('labelled')).toHaveFocus();
300
+ });
301
+
265
302
  it('typeahead cycles through same-letter matches on repeated presses', () => {
266
303
  const nodes: Node[] = [
267
304
  {id: 'apple', label: 'Apple', level: 1},
@@ -176,6 +176,28 @@ export interface UseTreeFocusReturn<T extends HTMLElement = HTMLElement> {
176
176
  focusLast: () => void;
177
177
  }
178
178
 
179
+ /**
180
+ * The text typeahead matches against. With `matchLabelledBy`, the row's
181
+ * `aria-labelledby` text (the label alone) when it has one; otherwise, and by
182
+ * default, its text content.
183
+ */
184
+ function itemLabelText(item: HTMLElement, matchLabelledBy: boolean): string {
185
+ const labelledBy = matchLabelledBy
186
+ ? item.getAttribute('aria-labelledby')
187
+ : null;
188
+ if (labelledBy != null) {
189
+ const label = labelledBy
190
+ .split(/\s+/)
191
+ .map(id => item.ownerDocument.getElementById(id)?.textContent ?? '')
192
+ .join(' ')
193
+ .trim();
194
+ if (label !== '') {
195
+ return label;
196
+ }
197
+ }
198
+ return (item.textContent ?? '').trim();
199
+ }
200
+
179
201
  /**
180
202
  * Hook for managing roving-tabindex focus + the WAI-ARIA tree keyboard model.
181
203
  *
@@ -208,8 +230,38 @@ export interface UseTreeFocusReturn<T extends HTMLElement = HTMLElement> {
208
230
  * </ul>
209
231
  * ```
210
232
  */
233
+
211
234
  export function useTreeFocus<T extends HTMLElement = HTMLElement>(
212
235
  options: UseTreeFocusOptions = {},
236
+ ): UseTreeFocusReturn<T> {
237
+ // The flag is pinned off here so an untyped options object cannot smuggle
238
+ // it through the public hook.
239
+ return useTreeFocusInternal<T>({...options, matchLabelledBy: false});
240
+ }
241
+
242
+ /**
243
+ * Options the package's own components can pass beyond the public hook.
244
+ *
245
+ * Not exported from the package: TreeList imports this module directly, so
246
+ * `UseTreeFocusOptions` stays exactly as released.
247
+ */
248
+ export type UseTreeFocusInternalOptions = UseTreeFocusOptions & {
249
+ /**
250
+ * Match typeahead against the item's `aria-labelledby` text (its label
251
+ * alone) instead of its whole text content, so start content, a chevron
252
+ * toggle or a description never takes part. TreeList passes this; the
253
+ * released text match stays the default for direct callers.
254
+ * @default false
255
+ */
256
+ matchLabelledBy?: boolean;
257
+ };
258
+
259
+ /**
260
+ * The implementation behind {@link useTreeFocus}. Not exported from the
261
+ * package; the public hook calls it without the internal options.
262
+ */
263
+ export function useTreeFocusInternal<T extends HTMLElement = HTMLElement>(
264
+ options: UseTreeFocusInternalOptions = {},
213
265
  ): UseTreeFocusReturn<T> {
214
266
  const {
215
267
  itemSelector = '[role="treeitem"]',
@@ -222,6 +274,7 @@ export function useTreeFocus<T extends HTMLElement = HTMLElement>(
222
274
  onActivate,
223
275
  typeahead = true,
224
276
  typeaheadResetMs = DEFAULT_TYPEAHEAD_RESET_MS,
277
+ matchLabelledBy = false,
225
278
  onActiveChange,
226
279
  hasRovingTabIndex = false,
227
280
  } = options;
@@ -399,14 +452,14 @@ export function useTreeFocus<T extends HTMLElement = HTMLElement>(
399
452
  const match = ordered.find(
400
453
  item =>
401
454
  !itemDisabled(item) &&
402
- (item.textContent ?? '').trim().toLowerCase().startsWith(query),
455
+ itemLabelText(item, matchLabelledBy).toLowerCase().startsWith(query),
403
456
  );
404
457
  if (match != null) {
405
458
  e.preventDefault();
406
459
  focusItem(match);
407
460
  }
408
461
  },
409
- [typeaheadResetMs, itemDisabled, focusItem],
462
+ [typeaheadResetMs, itemDisabled, focusItem, matchLabelledBy],
410
463
  );
411
464
 
412
465
  const handleKeyDown = useCallback(
@@ -246,6 +246,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
246
246
  '@astryx.chat.speechRecognition.noSpeechDetected': 'No speech was detected.',
247
247
  '@astryx.commandPalette.label': 'Command palette',
248
248
  '@astryx.commandPalette.input.placeholder': 'Search…',
249
+ '@astryx.commandPalette.input.label': 'Search commands',
249
250
  '@astryx.commandPalette.list.label': 'Commands',
250
251
  '@astryx.contextMenu.label': 'Context menu',
251
252
  '@astryx.dateInput.placeholder': 'Select a date',
@@ -353,6 +354,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
353
354
  '@astryx.numberInput.clearLabel': 'Clear {label}',
354
355
  '@astryx.numberInput.decrementLabel': 'Decrement {label}',
355
356
  '@astryx.numberInput.incrementLabel': 'Increment {label}',
357
+ '@astryx.numberInput.invalidNumber': 'Invalid number',
356
358
  '@astryx.selector.clearLabel': 'Clear {label}',
357
359
  '@astryx.sideNavCollapseButton.expandSidebar': 'Expand sidebar',
358
360
  '@astryx.sideNavCollapseButton.collapseSidebar': 'Collapse sidebar',
@@ -386,6 +388,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
386
388
  '{count, number} {count, plural, one {character} other {characters}} remaining',
387
389
  '@astryx.textArea.charactersOverLimit':
388
390
  '{count, number} {count, plural, one {character} other {characters}} over the limit',
391
+ '@astryx.inputClearButton.tooltip': 'Clear',
389
392
  '@astryx.textInput.clearLabel': 'Clear {label}',
390
393
  '@astryx.thumbnail.remove': 'Remove {accessibleName}',
391
394
  '@astryx.thumbnail.open': 'Open {accessibleName}',
@@ -253,6 +253,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
253
253
  '⟦Ñó šþééçĥ ŵàš ðéţéçţéð.⟧',
254
254
  '@astryx.commandPalette.label': '⟦Çóɱɱàñð þàłéţţé⟧',
255
255
  '@astryx.commandPalette.input.placeholder': '⟦Šéàřçĥ…⟧',
256
+ '@astryx.commandPalette.input.label': '⟦Šéàřçĥ çóɱɱàñðš⟧',
256
257
  '@astryx.commandPalette.list.label': '⟦Çóɱɱàñðš⟧',
257
258
  '@astryx.contextMenu.label': '⟦Çóñţéẋţ ɱéñú⟧',
258
259
  '@astryx.dateInput.placeholder': '⟦Šéłéçţ à ðàţé⟧',
@@ -360,6 +361,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
360
361
  '@astryx.numberInput.clearLabel': '⟦Çłéàř {label}⟧',
361
362
  '@astryx.numberInput.decrementLabel': '⟦Ðéçřéɱéñţ {label}⟧',
362
363
  '@astryx.numberInput.incrementLabel': '⟦Íñçřéɱéñţ {label}⟧',
364
+ '@astryx.numberInput.invalidNumber': '⟦Íñṽàłíð ñúɱƀéř⟧',
363
365
  '@astryx.selector.clearLabel': '⟦Çłéàř {label}⟧',
364
366
  '@astryx.sideNavCollapseButton.expandSidebar': '⟦Éẋþàñð šíðéƀàř⟧',
365
367
  '@astryx.sideNavCollapseButton.collapseSidebar': '⟦Çółłàþšé šíðéƀàř⟧',
@@ -393,6 +395,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
393
395
  '⟦{count, number} {count, plural, one {character} other {characters}} řéɱàíñíñĝ⟧',
394
396
  '@astryx.textArea.charactersOverLimit':
395
397
  '⟦{count, number} {count, plural, one {character} other {characters}} óṽéř ţĥé łíɱíţ⟧',
398
+ '@astryx.inputClearButton.tooltip': '⟦Çłéàř⟧',
396
399
  '@astryx.textInput.clearLabel': '⟦Çłéàř {label}⟧',
397
400
  '@astryx.thumbnail.remove': '⟦Řéɱóṽé {accessibleName}⟧',
398
401
  '@astryx.thumbnail.open': '⟦Óþéñ {accessibleName}⟧',
@@ -72,7 +72,15 @@ test('Chromium keeps every selector the reset writes', async ({page}) => {
72
72
  // A forgiving list drops an invalid entry instead of the whole rule, so a
73
73
  // dropped pseudo-element shows up as a shorter serialized selector.
74
74
  const parsed = await page.evaluate(() => {
75
- const layer = document.styleSheets[0].cssRules[0] as CSSLayerBlockRule;
75
+ // The sheet opens with the `@layer` order statement; the rules live in
76
+ // the `reset` layer block after it.
77
+ const layer = [...document.styleSheets[0].cssRules].find(
78
+ (rule): rule is CSSLayerBlockRule =>
79
+ rule instanceof CSSLayerBlockRule && rule.name === 'reset',
80
+ );
81
+ if (layer == null) {
82
+ throw new Error('reset.css has no @layer reset block');
83
+ }
76
84
  return [...layer.cssRules].map(rule => (rule as CSSStyleRule).selectorText);
77
85
  });
78
86
  expect(parsed.map(normalize)).toEqual(
package/src/reset.css CHANGED
@@ -25,7 +25,8 @@
25
25
  * import '@astryxdesign/core/astryx.css'; // @layer astryx-base — component styles
26
26
  * import '@astryxdesign/theme-neutral/theme.css'; // @layer astryx-theme — theme overrides
27
27
  *
28
- * Layer order: reset → astryx-base → astryx-theme
28
+ * Layer order: reset → astryx-base → astryx-theme. This file registers that
29
+ * order itself, like every Astryx stylesheet, so it holds in any load order.
29
30
  * Consumer styles (unlayered) always override all Astryx layers.
30
31
  * The reset has the lowest priority and can be overridden by any other layer.
31
32
  *
@@ -36,6 +37,8 @@
36
37
  * - modern-normalize
37
38
  */
38
39
 
40
+ @layer reset, astryx-base, astryx-theme;
41
+
39
42
  @layer reset {
40
43
 
41
44
  /* ===========================================================================
@@ -354,9 +357,11 @@
354
357
  }
355
358
 
356
359
  /*
357
- * Ensure hidden elements are actually hidden.
360
+ * Ensure hidden elements are actually hidden. `hidden="until-found"` keeps
361
+ * the browser's own rendering, `content-visibility: hidden`, so find in page
362
+ * and text fragment links can still find its content and reveal it.
358
363
  */
359
- :where([hidden]) {
364
+ :where([hidden]:not([hidden='until-found' i])) {
360
365
  display: none;
361
366
  }
362
367
 
@@ -0,0 +1,75 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file reset.until-found.a11y.browser.spec.ts
5
+ * @input packages/core/src/reset.css, alone on a blank page
6
+ * @output Browser evidence that `hidden="until-found"` content stays findable
7
+ * @position reset.css integration proof. The browser hides until-found content
8
+ * with `content-visibility: hidden`, which find in page and text fragments
9
+ * can search and reveal; `display: none` takes the content out of both, and
10
+ * only a shipping engine shows which one applies.
11
+ */
12
+
13
+ import * as fs from 'node:fs';
14
+ import {fileURLToPath} from 'node:url';
15
+ import {expect, test} from '@playwright/test';
16
+
17
+ const RESET_CSS = fs.readFileSync(
18
+ fileURLToPath(new URL('./reset.css', import.meta.url)),
19
+ 'utf8',
20
+ );
21
+
22
+ // Served at a real URL so a text fragment navigation can run against it.
23
+ const ORIGIN = 'http://reset.test';
24
+ const PAGE = `<!doctype html>
25
+ <style>${RESET_CSS}</style>
26
+ <div style="height: 3000px">Above the fold</div>
27
+ <div id="answer" hidden="until-found">The hidden answer</div>
28
+ <div id="closed" hidden>Plain hidden text</div>
29
+ <script>
30
+ document.getElementById('answer').addEventListener('beforematch', () => {
31
+ document.body.dataset.beforematch = 'fired';
32
+ });
33
+ </script>`;
34
+
35
+ test.beforeEach(async ({page}) => {
36
+ await page.route(`${ORIGIN}/**`, async route =>
37
+ route.fulfill({contentType: 'text/html', body: PAGE}),
38
+ );
39
+ });
40
+
41
+ test('until-found content keeps the browser rendering', async ({page}) => {
42
+ await page.goto(`${ORIGIN}/`);
43
+ const styles = await page.evaluate(() =>
44
+ ['answer', 'closed'].map(id => {
45
+ const element = document.getElementById(id);
46
+ if (!element) {
47
+ throw new Error(`missing #${id}`);
48
+ }
49
+ const style = getComputedStyle(element);
50
+ return {
51
+ display: style.display,
52
+ contentVisibility: style.contentVisibility,
53
+ };
54
+ }),
55
+ );
56
+ expect(styles).toEqual([
57
+ {display: 'block', contentVisibility: 'hidden'},
58
+ {display: 'none', contentVisibility: 'visible'},
59
+ ]);
60
+ });
61
+
62
+ test('a text fragment finds until-found content and reveals it', async ({
63
+ page,
64
+ }) => {
65
+ await page.goto(`${ORIGIN}/#:~:text=hidden%20answer`);
66
+ // The browser removes the attribute and fires beforematch when it reveals.
67
+ await expect(page.locator('#answer')).not.toHaveAttribute('hidden');
68
+ await expect(page.locator('#answer')).toBeInViewport();
69
+ await expect(page.locator('body')).toHaveAttribute(
70
+ 'data-beforematch',
71
+ 'fired',
72
+ );
73
+ // Plain hidden content stays out of reach.
74
+ await expect(page.locator('#closed')).toBeHidden();
75
+ });
@@ -0,0 +1,32 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * Static guard for the reset's `[hidden]` rule and `hidden="until-found"`.
5
+ *
6
+ * The browser hides until-found content with `content-visibility: hidden`,
7
+ * which find in page and text fragments can search and reveal. A
8
+ * `display: none` that also matches it takes the content out of both.
9
+ * reset.until-found.a11y.browser.spec.ts measures the result in a browser.
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+
15
+ import {describe, expect, it} from 'vitest';
16
+
17
+ describe('reset.css and hidden="until-found"', () => {
18
+ it('sets display: none only on hidden elements that are not until-found', () => {
19
+ const stripped = fs
20
+ .readFileSync(path.resolve(__dirname, './reset.css'), 'utf-8')
21
+ .replace(/\/\*[\s\S]*?\*\//g, '');
22
+ const hiddenRules = [...stripped.matchAll(/([^{}]+)\{([^{}]*)\}/g)]
23
+ .map(([, selector, body]) => ({selector: selector.trim(), body}))
24
+ .filter(({selector}) => selector.includes('[hidden'));
25
+ expect(hiddenRules.length).toBeGreaterThan(0);
26
+ for (const {selector, body} of hiddenRules) {
27
+ if (/display:\s*none/.test(body)) {
28
+ expect(selector).toContain(":not([hidden='until-found' i])");
29
+ }
30
+ }
31
+ });
32
+ });
@@ -0,0 +1,42 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file stylexValidation.source-build.test.mjs
5
+ * @input Uses Babel and the core StyleX build config
6
+ * @output Verifies unsupported StyleX declarations fail the core build
7
+ * @position Regression test for the core build's propertyValidationMode
8
+ */
9
+
10
+ import path from 'node:path';
11
+ import {fileURLToPath} from 'node:url';
12
+ import {transformAsync} from '@babel/core';
13
+ import {describe, expect, it} from 'vitest';
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const CORE_ROOT = path.resolve(__dirname, '..');
17
+
18
+ function compile(declaration) {
19
+ const source = `import * as stylex from '@stylexjs/stylex';
20
+ export const styles = stylex.create({root: {${declaration}}});`;
21
+ return transformAsync(source, {
22
+ babelrc: false,
23
+ configFile: path.join(CORE_ROOT, 'babel.config.json'),
24
+ filename: path.join(__dirname, 'unsupported-declaration.tsx'),
25
+ });
26
+ }
27
+
28
+ describe('core StyleX property validation', () => {
29
+ // In StyleX's default `silent` mode each of these compiles successfully and
30
+ // the declaration never reaches the shipped CSS, so a green build proves
31
+ // nothing. The core build must fail instead.
32
+ it.each([
33
+ ['border', "border: 'none'", /border is not supported/],
34
+ ['background', "background: 'none'", /background is not supported/],
35
+ ['all', "all: 'unset'", /all is not supported/],
36
+ ])(
37
+ 'rejects the %s shorthand instead of dropping it',
38
+ async (_name, declaration, message) => {
39
+ await expect(compile(declaration)).rejects.toThrow(message);
40
+ },
41
+ );
42
+ });
@@ -20,6 +20,8 @@ import {
20
20
  type SyntaxThemeTokenKey,
21
21
  } from './defineSyntaxTheme';
22
22
  import {useMediaQuery} from '../../hooks/useMediaQuery';
23
+ import {useTheme} from '../useTheme';
24
+ import {formatColor, parseColor} from '../../utils/color';
23
25
 
24
26
  // =============================================================================
25
27
  // Context
@@ -59,6 +61,22 @@ export interface UseSyntaxThemeReturn {
59
61
  // Hook
60
62
  // =============================================================================
61
63
 
64
+ /** The strength of the default diff row paints: the muted tokens at 25%. */
65
+ const DIFF_TINT_STRENGTH = 0.25;
66
+
67
+ /**
68
+ * The colour `color-mix(in srgb, <colour> <strength>, transparent)` paints, as
69
+ * a value a canvas can use. Returns undefined for a colour the shared parser
70
+ * does not read (the CSS default then stands).
71
+ */
72
+ function mixWithTransparent(
73
+ colour: string,
74
+ strength: number,
75
+ ): string | undefined {
76
+ const parsed = parseColor(colour);
77
+ return parsed ? formatColor({...parsed, a: parsed.a * strength}) : undefined;
78
+ }
79
+
62
80
  /**
63
81
  * Access the current syntax theme's token values, resolved for the active
64
82
  * color mode. Returns null if no SyntaxTheme provider is present.
@@ -74,6 +92,7 @@ export interface UseSyntaxThemeReturn {
74
92
  */
75
93
  export function useSyntaxTheme(): UseSyntaxThemeReturn | null {
76
94
  const ctx = use(SyntaxThemeContext);
95
+ const page = useTheme();
77
96
  const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
78
97
  const effectiveMode: 'light' | 'dark' = prefersDark ? 'dark' : 'light';
79
98
 
@@ -83,13 +102,35 @@ export function useSyntaxTheme(): UseSyntaxThemeReturn | null {
83
102
  }
84
103
  const resolved: Partial<Record<SyntaxThemeTokenKey, string>> = {};
85
104
  for (const key of ALL_SYNTAX_KEYS) {
86
- resolved[key] = resolveSyntaxTokenForMode(
87
- ctx.theme.__inputTokens[key],
88
- effectiveMode,
89
- );
105
+ const input = ctx.theme.__inputTokens[key];
106
+ if (input !== undefined) {
107
+ resolved[key] = resolveSyntaxTokenForMode(input, effectiveMode);
108
+ }
109
+ }
110
+ // A diff row slot the theme did not set follows its CSS default; a canvas
111
+ // consumer needs a colour it can paint, so the marker slots take the
112
+ // theme's own resolved punctuation colour and the row paints derive from
113
+ // the enclosing page theme's resolved tokens (the same mix the CSS
114
+ // default makes), never from a probe outside the nearest Theme.
115
+ for (const key of ALL_SYNTAX_KEYS) {
116
+ if (resolved[key] !== undefined) {
117
+ continue;
118
+ }
119
+ const fallback = ctx.theme.tokens[key] ?? '';
120
+ if (key === 'added-marker' || key === 'removed-marker') {
121
+ resolved[key] = resolved.punctuation ?? fallback;
122
+ } else if (key === 'added' || key === 'removed') {
123
+ const base = page.token(
124
+ key === 'added' ? '--color-success-muted' : '--color-error-muted',
125
+ );
126
+ resolved[key] =
127
+ mixWithTransparent(base, DIFF_TINT_STRENGTH) ?? fallback;
128
+ } else {
129
+ resolved[key] = fallback;
130
+ }
90
131
  }
91
132
  return resolved as Record<SyntaxThemeTokenKey, string>;
92
- }, [ctx, effectiveMode]);
133
+ }, [ctx, effectiveMode, page]);
93
134
 
94
135
  if (!ctx || !tokens) {
95
136
  return null;