@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
@@ -31,8 +31,8 @@ test.afterAll(async () => {
31
31
 
32
32
  const STORY = 'core-dropdownmenu--header-and-footer';
33
33
  // The trigger button only: the sub-menu row inside the menu also carries
34
- // aria-haspopup="menu".
35
- const TRIGGER = '#storybook-root button[aria-haspopup="menu"]';
34
+ // aria-haspopup. With a header and footer the trigger opens a dialog.
35
+ const TRIGGER = '#storybook-root button[aria-haspopup="dialog"]';
36
36
 
37
37
  async function openMenu(page: Page) {
38
38
  await page.goto(`${storybook.origin}/iframe.html?id=${STORY}&viewMode=story`);
@@ -76,7 +76,7 @@ test('typing into the header field filters without moving to a row', async ({
76
76
  await expect(page.getByRole('menuitem')).toHaveText(['Design', 'Move to']);
77
77
  });
78
78
 
79
- test('Tab walks header, menu, footer, then closes to the trigger', async ({
79
+ test('Tab walks header, menu, footer, and wraps at both ends', async ({
80
80
  page,
81
81
  }) => {
82
82
  const menu = await openMenu(page);
@@ -97,9 +97,44 @@ test('Tab walks header, menu, footer, then closes to the trigger', async ({
97
97
  await expect(page.getByTestId('created')).toHaveText('1');
98
98
  await expect(menu).toBeVisible();
99
99
 
100
+ // Past the footer's last control, Tab wraps to the header; before the
101
+ // header's first, Shift+Tab wraps to the footer. The dialog stays open.
100
102
  await page.keyboard.press('Tab');
101
- await expect(menu).toBeHidden();
102
- await expect(page.locator(TRIGGER)).toHaveAttribute('aria-expanded', 'false');
103
+ await expect(field).toBeFocused();
104
+ await expect(menu).toBeVisible();
105
+ await page.keyboard.press('Shift+Tab');
106
+ await expect(create).toBeFocused();
107
+ await expect(menu).toBeVisible();
108
+ await expect(page.locator(TRIGGER)).toHaveAttribute('aria-expanded', 'true');
109
+ });
110
+
111
+ test('the popover is a modal dialog around the menu, and the trigger points at it', async ({
112
+ page,
113
+ }) => {
114
+ const menu = await openMenu(page);
115
+ const dialog = page.getByRole('dialog', {name: 'Labels'});
116
+ await expect(dialog).toBeVisible();
117
+ await expect(dialog).toHaveAttribute('aria-modal', 'true');
118
+ await expect(
119
+ dialog.getByRole('menu', {name: 'Labels', exact: true}),
120
+ ).toBeVisible();
121
+ await expect(
122
+ dialog.getByRole('textbox', {name: 'Filter labels'}),
123
+ ).toBeVisible();
124
+ await expect(
125
+ dialog.getByRole('button', {name: 'Create label'}),
126
+ ).toBeVisible();
127
+ const trigger = page.locator(TRIGGER);
128
+ await expect(trigger).toHaveAttribute('aria-haspopup', 'dialog');
129
+ const controlsDialog = await trigger.evaluate(el => {
130
+ const target = document.getElementById(
131
+ el.getAttribute('aria-controls') ?? '',
132
+ );
133
+ const dialogEl = document.querySelector('[role="dialog"]');
134
+ return target != null && dialogEl != null && target.contains(dialogEl);
135
+ });
136
+ expect(controlsDialog).toBe(true);
137
+ await expect(menu).toBeFocused();
103
138
  });
104
139
 
105
140
  test('Shift+Tab from a row goes straight to the header', async ({page}) => {
@@ -25,6 +25,7 @@ import {
25
25
  type InputClearButtonProps,
26
26
  } from './InputClearButton';
27
27
  import {Icon, registerIcons, resetIcons} from '../Icon';
28
+ import {InternationalizationProvider} from '../i18n';
28
29
  import {defineTheme} from '../theme/defineTheme';
29
30
  import {generateThemeCSS} from '../theme/generateThemeRules';
30
31
  import {declaredValue} from '../__tests__/stylexDeclarations';
@@ -95,13 +96,23 @@ describe('InputClearButton public props', () => {
95
96
  });
96
97
 
97
98
  describe('InputClearButton', () => {
98
- it('uses the contextual label for the tooltip and accessible name', async () => {
99
+ it('shows a generic "Clear" tooltip while the accessible name keeps the field label', async () => {
100
+ // A long field label repeated in the tooltip reads as awkward copy
101
+ // ("Clear Assigned engineering partner"); the field's own label already
102
+ // sits beside the glyph. The contextual name stays for assistive tech.
99
103
  const showPopover = vi.spyOn(HTMLElement.prototype, 'showPopover');
100
- render(<InputClearButton label="Clear Search" onClick={() => {}} />);
104
+ render(
105
+ <InputClearButton
106
+ label="Clear Assigned engineering partner"
107
+ onClick={() => {}}
108
+ />,
109
+ );
101
110
 
102
- const button = screen.getByRole('button', {name: 'Clear Search'});
111
+ const button = screen.getByRole('button', {
112
+ name: 'Clear Assigned engineering partner',
113
+ });
103
114
  const tooltip = screen.getByRole('tooltip', {hidden: true});
104
- expect(tooltip).toHaveTextContent(/^Clear Search$/);
115
+ expect(tooltip).toHaveTextContent(/^Clear$/);
105
116
  expect(button.getAttribute('aria-describedby')).toBe(tooltip.id);
106
117
 
107
118
  fireEvent.mouseEnter(button);
@@ -109,6 +120,20 @@ describe('InputClearButton', () => {
109
120
  showPopover.mockRestore();
110
121
  });
111
122
 
123
+ it('localizes the tooltip through the catalog', () => {
124
+ render(
125
+ <InternationalizationProvider
126
+ locale="en"
127
+ overrides={{en: {'@astryx.inputClearButton.tooltip': 'Leeren'}}}>
128
+ <InputClearButton label="Suche leeren" onClick={() => {}} />
129
+ </InternationalizationProvider>,
130
+ );
131
+ expect(screen.getByRole('button', {name: 'Suche leeren'})).toBeTruthy();
132
+ expect(screen.getByRole('tooltip', {hidden: true})).toHaveTextContent(
133
+ /^Leeren$/,
134
+ );
135
+ });
136
+
112
137
  it('renders a real button with the given accessible label', () => {
113
138
  render(<InputClearButton label="Clear" onClick={() => {}} />);
114
139
  const button = screen.getByRole('button', {name: 'Clear'});
@@ -4,9 +4,10 @@
4
4
 
5
5
  /**
6
6
  * @file InputClearButton.tsx
7
- * @input Uses React, Button, and Icon
8
- * @output Exports the public InputClearButton and an internal popup-aware variant,
9
- * both with a contextual tooltip matching their required label.
7
+ * @input Uses React, Button, Icon, and the i18n translator
8
+ * @output Exports the public InputClearButton and an internal popup-aware variant.
9
+ * Both show the localized generic "Clear" tooltip and keep the caller's
10
+ * contextual label as the accessible name.
10
11
  * @position Shared primitive. Every input that renders a clear affordance —
11
12
  * TextInput, NumberInput, TimeInput, DateInput, DateTimeInput,
12
13
  * DateRangeInput, Selector, MultiSelector, Typeahead, Tokenizer, FileInput —
@@ -19,6 +20,7 @@ import type {ReactNode} from 'react';
19
20
  import * as stylex from '@stylexjs/stylex';
20
21
  import {Button} from '../Button';
21
22
  import {Icon} from '../Icon';
23
+ import {useTranslator} from '../i18n';
22
24
  import {themeProps} from '../utils/themeProps';
23
25
 
24
26
  const styles = stylex.create({
@@ -67,7 +69,10 @@ const styles = stylex.create({
67
69
  });
68
70
 
69
71
  export interface InputClearButtonProps {
70
- /** Contextual accessible name for the button, such as "Clear Search". */
72
+ /**
73
+ * Contextual accessible name for the button, such as "Clear Search". The
74
+ * visible tooltip is always the localized generic "Clear".
75
+ */
71
76
  label: string;
72
77
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
73
78
  xstyle?: stylex.StyleXStyles;
@@ -83,10 +88,11 @@ interface InternalInputClearButtonProps extends InputClearButtonProps {
83
88
  type InputClearButtonRenderProps = InputClearButtonProps &
84
89
  Partial<
85
90
  Pick<InternalInputClearButtonProps, 'onPointerDown' | 'onClickCapture'>
86
- >;
91
+ > & {tooltip: string};
87
92
 
88
93
  function renderInputClearButton({
89
94
  label,
95
+ tooltip,
90
96
  onClick,
91
97
  onPointerDown,
92
98
  onClickCapture,
@@ -100,7 +106,11 @@ function renderInputClearButton({
100
106
  variant="ghost"
101
107
  size="sm"
102
108
  label={label}
103
- tooltip={label}
109
+ // The field's label already sits beside the glyph, so the tooltip names
110
+ // only the action; repeating a long label reads as awkward copy. The
111
+ // contextual label stays the accessible name, which still contains
112
+ // the visible word (WCAG 2.5.3, label in name).
113
+ tooltip={tooltip}
104
114
  className={buttonTargetClassName}
105
115
  icon={
106
116
  <Icon
@@ -128,14 +138,22 @@ function renderInputClearButton({
128
138
  }
129
139
 
130
140
  export function InputClearButton(props: InputClearButtonProps): ReactNode {
131
- return renderInputClearButton(props);
141
+ const t = useTranslator();
142
+ return renderInputClearButton({
143
+ ...props,
144
+ tooltip: t('@astryx.inputClearButton.tooltip'),
145
+ });
132
146
  }
133
147
 
134
148
  /** Internal variant used while popup-aware clear behavior is rolled out. */
135
149
  export function InternalInputClearButton(
136
150
  props: InternalInputClearButtonProps,
137
151
  ): ReactNode {
138
- return renderInputClearButton(props);
152
+ const t = useTranslator();
153
+ return renderInputClearButton({
154
+ ...props,
155
+ tooltip: t('@astryx.inputClearButton.tooltip'),
156
+ });
139
157
  }
140
158
 
141
159
  InputClearButton.displayName = 'InputClearButton';
@@ -168,26 +168,18 @@ async function runState(
168
168
  });
169
169
  }
170
170
 
171
- test('every expectation is exercised by at least one Core binding state', async ({
172
- page,
173
- }) => {
174
- test.setTimeout(4 * 60 * 1000);
175
- const cdp = await page.context().newCDPSession(page);
176
- const results: BindingResult[] = [];
177
- for (const state of CORE_STATUS_MESSAGE_BINDING_STATES) {
178
- results.push(await runState(page, cdp, state));
179
- }
180
- expect(neverExercised(results)).toEqual([]);
181
- expect(
182
- unmatchedKnownFailures(CORE_STATUS_MESSAGE_KNOWN_FAILURES, results),
183
- ).toEqual([]);
184
- });
171
+ // Each state's result, recorded by its own test below so the whole-suite check
172
+ // at the end reads it instead of measuring every state a second time. A state
173
+ // whose test did not run in this worker (a filtered run, or a worker restarted
174
+ // after a failure) is measured by the whole-suite check itself.
175
+ const recorded = new Map<string, BindingResult>();
185
176
 
186
177
  for (const state of CORE_STATUS_MESSAGE_BINDING_STATES) {
187
178
  test(`${state.binding} [${state.id}] — ${state.summary}`, async ({page}) => {
188
179
  test.setTimeout(2 * 60 * 1000);
189
180
  const cdp = await page.context().newCDPSession(page);
190
181
  const result = await runState(page, cdp, state);
182
+ recorded.set(state.id, result);
191
183
  const report = summarize(STATUS_MESSAGE_PATTERN, [result]);
192
184
  // eslint-disable-next-line no-console -- the report is this run's artifact
193
185
  console.log(formatReport(report));
@@ -196,3 +188,18 @@ for (const state of CORE_STATUS_MESSAGE_BINDING_STATES) {
196
188
  expect(report.counts.unexpectedPass).toBe(0);
197
189
  });
198
190
  }
191
+
192
+ test('every expectation is exercised by at least one Core binding state', async ({
193
+ page,
194
+ }) => {
195
+ test.setTimeout(4 * 60 * 1000);
196
+ const cdp = await page.context().newCDPSession(page);
197
+ const results: BindingResult[] = [];
198
+ for (const state of CORE_STATUS_MESSAGE_BINDING_STATES) {
199
+ results.push(recorded.get(state.id) ?? (await runState(page, cdp, state)));
200
+ }
201
+ expect(neverExercised(results)).toEqual([]);
202
+ expect(
203
+ unmatchedKnownFailures(CORE_STATUS_MESSAGE_KNOWN_FAILURES, results),
204
+ ).toEqual([]);
205
+ });
@@ -60,6 +60,7 @@ const styles = stylex.create({
60
60
  display: 'flex',
61
61
  flexDirection: 'column',
62
62
  gap: spacingVars['--spacing-4'],
63
+ alignItems: 'stretch',
63
64
  },
64
65
  },
65
66
  });
@@ -17,7 +17,7 @@ exports[`FormLayout > matches snapshot for horizontal direction 1`] = `
17
17
 
18
18
  exports[`FormLayout > matches snapshot for horizontal-labels direction 1`] = `
19
19
  <div
20
- class="astryx-form-layout FormLayout__styles.base xdt5ytf FormLayout__styles.horizontalLabels xrvj5dj x1pmbctz xlaq8a2 x7a106z xedohl4 x1rpgqan x1a1jff"
20
+ class="astryx-form-layout FormLayout__styles.base xdt5ytf FormLayout__styles.horizontalLabels xrvj5dj x1pmbctz xlaq8a2 x7a106z xedohl4 x1rpgqan x1a1jff x1q32nj3"
21
21
  data-direction="horizontal-labels"
22
22
  data-testid="layout"
23
23
  >
@@ -27,10 +27,11 @@ export const docs = {
27
27
  usage: {
28
28
  anatomy,
29
29
  description:
30
- 'A CSS grid layout container for arranging children in rows and columns. Use Grid for card galleries, dashboards, and any multi-column layout. Supports fixed column counts and responsive columns that reflow based on available width.',
30
+ 'A CSS grid layout container for arranging children in rows and columns. Use Grid for card galleries, dashboards, and any multi-column layout. For columns that reflow on small screens, use `columns={{minWidth, max}}`; a numeric `columns` is exactly N columns at every width and never reflows.',
31
31
  bestPractices: [
32
- { guidance: true, description: 'Use responsive columns for layouts that should adapt to screen size: `columns={{minWidth: 280}}`.' },
33
- { guidance: true, description: 'Cap the column count with `max` to prevent rows from getting too wide on large screens.' },
32
+ { guidance: true, description: 'Default to responsive columns for anything that must survive a phone: `columns={{minWidth: 240, max: 4}}` renders at most 4 columns, each at least 240px, and drops to one full-width column when narrow.' },
33
+ { guidance: false, description: 'Use `columns={4}` for a row that must work on small screens; a number never reflows and renders four narrow tracks on a phone. Reserve numbers for rows that must stay fixed, such as a 7-day week.' },
34
+ { guidance: true, description: 'Always pair `minWidth` with `max`: `max` caps the count on wide screens and lets the last column shrink to a narrow container; `{minWidth}` alone overflows a phone narrower than `minWidth`.' },
34
35
  { guidance: true, description: 'Use `repeat: \'fill\'` (the default) for consistent item widths. Use `\'fit\'` when items should stretch to fill leftover space.' },
35
36
  { guidance: false, description: 'Write manual CSS grid; Grid handles spacing and responsive behavior for you.' },
36
37
  { guidance: false, description: 'Use `HStack` with wrapping for grids; use Grid instead.' },
@@ -54,12 +55,12 @@ export const docs = {
54
55
  ],
55
56
  },
56
57
  },
57
- description: 'Grid container with fixed or responsive columns.',
58
+ description: 'Grid container with fixed or responsive columns; `{minWidth, max}` reflows, a number never does.',
58
59
  props: [
59
60
  {
60
61
  name: 'columns',
61
62
  type: "number | {minWidth: number, max?: number, repeat?: 'fill' | 'fit'}",
62
- description: 'Column configuration. Use a number for fixed columns (e.g. `columns={3}`). Use an object for responsive columns: `minWidth` sets the minimum column width in px, `repeat` controls track behavior (`"fill"` preserves empty tracks for consistent widths, `"fit"` collapses empty tracks so items stretch; defaults to `"fill"`), and `max` caps the maximum number of columns.',
63
+ description: 'Column configuration. A number is exactly N equal columns at every width and never reflows (e.g. `columns={3}`); for columns that reflow on small screens, use `{minWidth, max}`. Use an object for responsive columns: `minWidth` sets the minimum column width in px, `repeat` controls track behavior (`"fill"` preserves empty tracks for consistent widths, `"fit"` collapses empty tracks so items stretch; defaults to `"fill"`), and `max` caps the maximum number of columns.',
63
64
  },
64
65
  {
65
66
  name: 'width',
@@ -129,10 +130,11 @@ export const docsZh = {
129
130
  usage: {
130
131
  anatomy,
131
132
  description:
132
- 'A CSS grid layout container for arranging children in rows and columns. Use Grid for card galleries, dashboards, and any multi-column layout. Supports fixed column counts and responsive columns that reflow based on available width.',
133
+ 'A CSS grid layout container for arranging children in rows and columns. Use Grid for card galleries, dashboards, and any multi-column layout. For columns that reflow on small screens, use `columns={{minWidth, max}}`; a numeric `columns` is exactly N columns at every width and never reflows.',
133
134
  bestPractices: [
134
- { guidance: true, description: 'Use responsive columns for layouts that should adapt to screen size: `columns={{minWidth: 280}}`.' },
135
- { guidance: true, description: 'Cap the column count with `max` to prevent rows from getting too wide on large screens.' },
135
+ { guidance: true, description: 'Default to responsive columns for anything that must survive a phone: `columns={{minWidth: 240, max: 4}}` renders at most 4 columns, each at least 240px, and drops to one full-width column when narrow.' },
136
+ { guidance: false, description: 'Use `columns={4}` for a row that must work on small screens; a number never reflows and renders four narrow tracks on a phone. Reserve numbers for rows that must stay fixed, such as a 7-day week.' },
137
+ { guidance: true, description: 'Always pair `minWidth` with `max`: `max` caps the count on wide screens and lets the last column shrink to a narrow container; `{minWidth}` alone overflows a phone narrower than `minWidth`.' },
136
138
  { guidance: true, description: 'Use `repeat: \'fill\'` (the default) for consistent item widths. Use `\'fit\'` when items should stretch to fill leftover space.' },
137
139
  { guidance: false, description: 'Write manual CSS grid; Grid handles spacing and responsive behavior for you.' },
138
140
  { guidance: false, description: 'Use `HStack` with wrapping for grids; use Grid instead.' },
@@ -146,10 +148,11 @@ export const docsDense = {
146
148
  description: 'CSS Grid-based layout w/ responsive column support.',
147
149
  usage: {
148
150
  anatomy,
149
- description: 'A CSS grid layout container for arranging children in rows and columns. Use Grid for card galleries, dashboards, and any multi-column layout. Supports fixed column counts and responsive columns that reflow based on available width.',
151
+ description: 'A CSS grid layout container for arranging children in rows and columns. Use Grid for card galleries, dashboards, and any multi-column layout. For columns that reflow on small screens, use `columns={{minWidth, max}}`; a numeric `columns` is exactly N columns at every width and never reflows.',
150
152
  bestPractices: [
151
- { guidance: true, description: 'Use responsive columns for layouts that should adapt to screen size: columns={{minWidth: 280}}.' },
152
- { guidance: true, description: 'Cap the column count with max to prevent rows from getting too wide on large screens.' },
153
+ { guidance: true, description: 'Default to responsive columns for anything that must survive a phone: columns={{minWidth: 240, max: 4}} = at most 4 columns, each >= 240px, one full-width column when narrow.' },
154
+ { guidance: false, description: 'Use columns={4} for rows that must work on small screens; a number never reflows (4 narrow tracks on a phone). Numbers only for rows that must stay fixed, e.g. a 7-day week.' },
155
+ { guidance: true, description: 'Always pair minWidth with max: max caps the count on wide screens and lets the last column shrink to a narrow container; {minWidth} alone overflows a phone narrower than minWidth.' },
153
156
  { guidance: true, description: 'Use repeat: \'fill\' (the default) for consistent item widths. Use \'fit\' when items should stretch to fill leftover space.' },
154
157
  { guidance: false, description: 'Write manual CSS grid; Grid handles spacing and responsive behavior for you.' },
155
158
  { guidance: false, description: 'Use HStack with wrapping for grids; use Grid instead.' },
@@ -44,36 +44,6 @@ layout, targets, or public API.
44
44
 
45
45
  Consumer migration instructions belong in consumer docs and release notes.
46
46
 
47
- ### DEP-0010 — Deprecate the fixed meaning of numeric `Grid` `columns`
48
-
49
- | Field | Value |
50
- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
- | id | `DEP-0010` |
52
- | cleanup | `CLN-0019` |
53
- | package | `@astryxdesign/core` |
54
- | surface | `Grid` `columns={N}`, which renders exactly N equal tracks (`repeat(N, 1fr)`) |
55
- | old contract | A numeric `columns` value is an explicit equal-width track count at every width |
56
- | replacement | `columns={{count: N, isFixed: true}}` keeps exactly N tracks |
57
- | direct authority | `component:Grid`, with `family:layout-primitives` representative matrix row "Grid / fixed columns". Owners: `cixzhang` (`component:Grid`); `cixzhang`, `imdreamrunner` (`family:layout-primitives`) |
58
- | warning | Consumer docs and the release notes name `DEP-0010` and the fixed shape. No runtime warning (`spec:AST-017` FR29) |
59
- | migration | Mechanical where the fixed count is wanted: rewrite `columns={N}` to `columns={{count: N, isFixed: true}}`. Keeping the number accepts the new reflow |
60
- | codemod | None yet. A preserving codemod is mechanical, but it would opt every caller out of the change it exists to deliver |
61
- | downstream | In-repo fixed rows such as week rows use the fixed shape |
62
- | transition | no prior transition |
63
- | state | `proposed` |
64
- | open decision | This record is draft and lists new responsive behavior as a non-goal; the family matrix pins numeric columns as fixed. Both need an owner ruling |
65
- | target plan | `CLN-0019` in the next minor release |
66
-
67
- ### CLN-0019 — Numeric `columns` means at most N columns
68
-
69
- | Field | Value |
70
- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
71
- | id | `CLN-0019` |
72
- | lifecycle | `DEP-0010` |
73
- | delta | A numeric `columns` value renders up to N columns of at least 12rem and fewer when the grid is narrower; a `GridSpan` in such a grid takes the full row once the grid has fewer columns than it spans |
74
- | rollback | Restore `repeat(N, 1fr)` for numeric `columns` from the final-patch baseline |
75
- | state | `pending` — lands in a declared minor with its `[breaking]` Changeset |
76
-
77
47
  ## Ownership boundary
78
48
 
79
49
  **Owns**
@@ -110,6 +80,43 @@ in the canonical `Grid.doc.mjs` and the subordinate `GridSpan.doc.mjs`;
110
80
  - A Spanning item may span columns, rows, both, or neither while retaining the
111
81
  same target.
112
82
 
83
+ ### Responsive contract
84
+
85
+ The `columns` prop decides how many column tracks Grid lays out and whether
86
+ that count changes with the container's width. Other inputs change the result
87
+ only by changing what `columns` has to fit: `width` and `maxWidth` set the
88
+ container width a reflowing shape measures against; an `xstyle`, `className`,
89
+ or `style` that sets the track template (including under a media query)
90
+ replaces the computed one; and a numeric GridSpan wider than the grid's tracks
91
+ makes the browser add implicit tracks to the whole grid, which later items flow
92
+ into. No other prop changes the count.
93
+
94
+ | `columns` | Behavior | Reflows |
95
+ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
96
+ | `N` (number) | Exactly N tracks that share the row equally at every container width; a child wider than its share widens its track and can push the grid past a narrow container. | never |
97
+ | `{minWidth}` | As many tracks as fit while each stays at least `minWidth` px wide. A container narrower than `minWidth` keeps one `minWidth`-wide track that overflows it. Empty tracks keep their width. | yes |
98
+ | `{minWidth, repeat: 'fit'}` | As above, and empty tracks collapse so the present items stretch across the row. | yes |
99
+ | `{minWidth, max}` | Up to `max` tracks, each at least `minWidth` px and never narrower than an equal share of the row; fewer tracks as the container narrows, down to one track that shrinks to the container rather than overflowing it. `repeat: 'fit'` applies the same way. | yes |
100
+ | omitted, `0`, or negative | One track. | never |
101
+
102
+ - `gap` and `columnGap` count toward the width a reflowing shape has to fit, so
103
+ the full count is reached only when the container has room for the tracks and
104
+ the gaps between them. They never change the count of a numeric `columns`.
105
+ - `height`, `minHeight`, `rowHeight`, `align`, and `justify` size or place the
106
+ container and its items and do not change the column count.
107
+ - GridSpan spans `k` tracks, or the whole row for `'full'`. `'full'` never
108
+ adds tracks. A numeric span wider than the grid's current tracks makes the
109
+ browser add implicit tracks to the grid for the remainder; they persist for
110
+ every row, items placed before and after the span flow into them, and once
111
+ they exist `'full'` covers only the explicit tracks. In a reflowing grid keep
112
+ spans within the smallest count the grid can reach or use `'full'`.
113
+ - For columns that reflow on small screens, use `{minWidth, max}`: `max` is
114
+ what lets the last track shrink to a narrow container, so `{minWidth}` alone
115
+ still overflows a phone narrower than `minWidth`. A numeric `columns` never
116
+ reflows, so a four-up numeric row on a phone renders four narrow tracks, or
117
+ overflows when a child cannot shrink to its share; that is the fixed
118
+ contract, not a defect.
119
+
113
120
  ### Representative states
114
121
 
115
122
  | State | Required invariant | Allowed variation |
@@ -178,8 +185,19 @@ relationship.
178
185
 
179
186
  ## Decision log
180
187
 
181
- None. This draft records current facts and introduces no component-local design,
182
- layout, accessibility, or API decision.
188
+ ### DEC-1 — Numeric `columns` stays fixed; reflow uses the existing object form
189
+
190
+ **Decider:** `cixzhang`, `2026-10-11`
191
+
192
+ `columns={N}` means exactly N equal tracks at every width, and a grid that
193
+ reflows on small screens uses `{minWidth, max?, repeat?}`. Two shapes, one
194
+ responsibility each: the number is the fixed contract, the object form is the
195
+ responsive one, and the record and the docs say so where a caller first meets
196
+ `columns`.
197
+
198
+ Rejected: redefining the number as "at most N" (breaks every released numeric
199
+ grid and needs an opt-out shape); a `{count, isFixed}` shape (duplicates the
200
+ number and the `max` cap).
183
201
 
184
202
  ## Open questions
185
203
 
package/src/Grid/Grid.tsx CHANGED
@@ -31,7 +31,13 @@ export type GridAlignment = 'start' | 'center' | 'end' | 'stretch';
31
31
  /**
32
32
  * Column configuration for Grid.
33
33
  *
34
- * - `number` — fixed equal-width columns (e.g. `columns={3}`)
34
+ * A number means exactly N equal columns at every width; it never reflows, so
35
+ * `columns={4}` on a phone renders four narrow tracks. For columns that reflow
36
+ * on small screens, use the object form: `{minWidth: 240, max: 4}` renders at
37
+ * most four columns, each at least 240px, and fewer when the container is
38
+ * narrower.
39
+ *
40
+ * - `number` — exactly N equal-width columns (e.g. `columns={3}`); never reflows
35
41
  * - `object` — responsive columns based on minimum child width:
36
42
  * - `minWidth` — minimum width (px) for each column track
37
43
  * - `repeat` — `'fill'` (default) preserves empty tracks for consistent widths;
@@ -53,9 +59,12 @@ export interface GridProps extends BaseProps<HTMLDivElement> {
53
59
  /** Ref forwarded to the root element */
54
60
  ref?: React.Ref<HTMLDivElement>;
55
61
  /**
56
- * Column configuration.
57
- * - `number` — fixed equal-width columns
58
- * - `{minWidth, max?, repeat?}` — responsive columns
62
+ * Column configuration. A number is exactly N equal columns and never
63
+ * reflows; for columns that reflow on small screens, use
64
+ * `{minWidth, max}`.
65
+ * - `number` — exactly N equal-width columns at every width
66
+ * - `{minWidth, max?, repeat?}` — responsive columns with a px floor and an
67
+ * optional cap
59
68
  *
60
69
  * @see GridColumns
61
70
  */
@@ -18,13 +18,21 @@ import {
18
18
  beforeEach,
19
19
  afterAll,
20
20
  } from 'vitest';
21
- import {render, screen, fireEvent, waitFor, act} from '@testing-library/react';
21
+ import {
22
+ render,
23
+ renderHook,
24
+ screen,
25
+ fireEvent,
26
+ waitFor,
27
+ act,
28
+ } from '@testing-library/react';
22
29
  import {renderToString} from 'react-dom/server';
23
30
  import {hydrateRoot} from 'react-dom/client';
24
31
  import {StrictMode} from 'react';
25
32
  import {Button} from '../Button/Button';
26
33
  import {Theme, defineTheme} from '../theme';
27
34
  import {HoverCard} from './HoverCard';
35
+ import {useHoverCard} from './useHoverCard';
28
36
  import {__resetInteractionModalityForTest} from '../utils/interactionModality';
29
37
 
30
38
  // Store original matches to restore later
@@ -64,6 +72,43 @@ beforeEach(() => {
64
72
  vi.mocked(HTMLElement.prototype.hidePopover).mockClear();
65
73
  });
66
74
 
75
+ describe('useHoverCard identity', () => {
76
+ it('keeps callback refs stable across unchanged rerenders', () => {
77
+ const {result, rerender} = renderHook(() => useHoverCard());
78
+ const firstRef = result.current.ref;
79
+ const firstInteractionRef = result.current.interactionRef;
80
+
81
+ rerender();
82
+
83
+ expect(result.current.ref).toBe(firstRef);
84
+ expect(result.current.interactionRef).toBe(firstInteractionRef);
85
+ });
86
+
87
+ it('does not rebind trigger listeners on rerender', () => {
88
+ function Harness() {
89
+ const card = useHoverCard();
90
+ return (
91
+ <div>
92
+ <span ref={card.ref}>Trigger</span>
93
+ {card.renderHoverCard(<div>Card</div>)}
94
+ </div>
95
+ );
96
+ }
97
+ const {getByText, rerender} = render(<Harness />);
98
+ const trigger = getByText('Trigger');
99
+ const added: string[] = [];
100
+ const add = trigger.addEventListener.bind(trigger);
101
+ trigger.addEventListener = ((type: string, ...rest: unknown[]) => {
102
+ added.push(type);
103
+ return (add as (...args: unknown[]) => void)(type, ...rest);
104
+ }) as typeof trigger.addEventListener;
105
+
106
+ rerender(<Harness />);
107
+
108
+ expect(added).toEqual([]);
109
+ });
110
+ });
111
+
67
112
  describe('HoverCard', () => {
68
113
  it('renders trigger element', () => {
69
114
  render(