@astryxdesign/core 0.5.2-canary.2da7a39 → 0.5.2-canary.2e51213

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 (375) hide show
  1. package/dist/AppShell/AppShell.d.ts +2 -2
  2. package/dist/AppShell/AppShell.d.ts.map +1 -1
  3. package/dist/AppShell/AppShell.js +10 -7
  4. package/dist/Banner/Banner.d.ts +5 -0
  5. package/dist/Banner/Banner.d.ts.map +1 -1
  6. package/dist/Banner/Banner.js +7 -2
  7. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +3 -2
  8. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  9. package/dist/Breadcrumbs/BreadcrumbItem.js +22 -15
  10. package/dist/Carousel/Carousel.d.ts.map +1 -1
  11. package/dist/Carousel/Carousel.js +2 -2
  12. package/dist/Chat/ChatComposer.d.ts +2 -2
  13. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  14. package/dist/Chat/ChatComposer.js +21 -2
  15. package/dist/Chat/ChatToolCalls.d.ts +1 -1
  16. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  17. package/dist/Chat/ChatToolCalls.js +12 -34
  18. package/dist/Chat/useChatNewMessages.js +3 -3
  19. package/dist/CheckboxList/CheckboxListItem.d.ts +5 -3
  20. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  21. package/dist/DateTimeInput/DateTimeInput.d.ts +47 -13
  22. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  23. package/dist/DateTimeInput/DateTimeInput.js +269 -162
  24. package/dist/DateTimeInput/NativeDateSegment.d.ts +24 -0
  25. package/dist/DateTimeInput/NativeDateSegment.d.ts.map +1 -0
  26. package/dist/DateTimeInput/NativeDateSegment.js +235 -0
  27. package/dist/DateTimeInput/NativeTimeSegment.d.ts +30 -0
  28. package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -0
  29. package/dist/DateTimeInput/NativeTimeSegment.js +248 -0
  30. package/dist/DateTimeInput/index.d.ts +1 -1
  31. package/dist/DateTimeInput/index.d.ts.map +1 -1
  32. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +95 -0
  33. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -0
  34. package/dist/DateTimeInput/nativePickerSegmentStyles.js +103 -0
  35. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  36. package/dist/DropdownMenu/DropdownMenu.js +2 -4
  37. package/dist/Field/Field.d.ts.map +1 -1
  38. package/dist/Field/Field.js +10 -4
  39. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  40. package/dist/Field/PanelSearchInput.js +3 -5
  41. package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
  42. package/dist/FieldStatus/FieldStatus.js +4 -2
  43. package/dist/Layer/useTouchTrigger.d.ts.map +1 -1
  44. package/dist/Layer/useTouchTrigger.js +2 -4
  45. package/dist/Layout/Layout.d.ts +7 -8
  46. package/dist/Layout/Layout.d.ts.map +1 -1
  47. package/dist/Layout/Layout.js +7 -8
  48. package/dist/MultiSelector/MultiSelector.d.ts +11 -1
  49. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  50. package/dist/MultiSelector/MultiSelector.js +89 -45
  51. package/dist/RadioList/RadioListItem.d.ts +30 -5
  52. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  53. package/dist/RadioList/RadioListItem.js +17 -4
  54. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  55. package/dist/Resizable/ResizeHandle.js +102 -44
  56. package/dist/Resizable/index.d.ts +2 -0
  57. package/dist/Resizable/index.d.ts.map +1 -1
  58. package/dist/Resizable/index.js +1 -0
  59. package/dist/Resizable/useResizable.d.ts +89 -8
  60. package/dist/Resizable/useResizable.d.ts.map +1 -1
  61. package/dist/Resizable/useResizable.js +521 -38
  62. package/dist/Resizable/utils.d.ts +35 -0
  63. package/dist/Resizable/utils.d.ts.map +1 -0
  64. package/dist/Resizable/utils.js +27 -0
  65. package/dist/Selector/Selector.d.ts +10 -0
  66. package/dist/Selector/Selector.d.ts.map +1 -1
  67. package/dist/Selector/Selector.js +105 -57
  68. package/dist/Selector/useSelectorPresentation.d.ts +1 -0
  69. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -1
  70. package/dist/Selector/useSelectorPresentation.js +17 -2
  71. package/dist/Slider/Slider.d.ts.map +1 -1
  72. package/dist/Slider/Slider.js +3 -5
  73. package/dist/Spinner/Spinner.d.ts.map +1 -1
  74. package/dist/Spinner/Spinner.js +9 -10
  75. package/dist/Stepper/Step.d.ts +2 -1
  76. package/dist/Stepper/Step.d.ts.map +1 -1
  77. package/dist/Stepper/Step.js +75 -11
  78. package/dist/Stepper/Stepper.d.ts.map +1 -1
  79. package/dist/Stepper/Stepper.js +1 -0
  80. package/dist/TabList/TabList.d.ts.map +1 -1
  81. package/dist/TabList/TabList.js +2 -3
  82. package/dist/Table/index.d.ts +16 -1
  83. package/dist/Table/index.d.ts.map +1 -1
  84. package/dist/Table/index.js +4 -0
  85. package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
  86. package/dist/Table/plugins/columnResize/useTableColumnResize.js +8 -9
  87. package/dist/Table/plugins/rowStatus/index.d.ts +1 -1
  88. package/dist/Table/plugins/rowStatus/index.d.ts.map +1 -1
  89. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts +11 -30
  90. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -1
  91. package/dist/Table/plugins/rowStatus/useTableRowStatus.js +108 -62
  92. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  93. package/dist/Table/plugins/sortable/useTableSortable.js +9 -1
  94. package/dist/Text/useTruncation.d.ts +1 -1
  95. package/dist/Text/useTruncation.d.ts.map +1 -1
  96. package/dist/Text/useTruncation.js +8 -7
  97. package/dist/TimeInput/TimeInput.d.ts +16 -1
  98. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  99. package/dist/TimeInput/TimeInput.js +98 -60
  100. package/dist/TimeInput/index.d.ts +1 -1
  101. package/dist/TimeInput/index.d.ts.map +1 -1
  102. package/dist/Timestamp/Timestamp.d.ts +2 -2
  103. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  104. package/dist/Timestamp/Timestamp.js +4 -145
  105. package/dist/Timestamp/formatRelativeTime.d.ts +20 -0
  106. package/dist/Timestamp/formatRelativeTime.d.ts.map +1 -0
  107. package/dist/Timestamp/formatRelativeTime.js +81 -0
  108. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  109. package/dist/Toast/ToastViewport.js +125 -77
  110. package/dist/Tokenizer/Tokenizer.d.ts +0 -38
  111. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  112. package/dist/Tokenizer/Tokenizer.js +94 -26
  113. package/dist/Tokenizer/useEndLaneReserve.d.ts +31 -0
  114. package/dist/Tokenizer/useEndLaneReserve.d.ts.map +1 -0
  115. package/dist/Tokenizer/useEndLaneReserve.js +136 -0
  116. package/dist/Tooltip/useTooltip.d.ts +1 -1
  117. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  118. package/dist/Tooltip/useTooltip.js +53 -42
  119. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  120. package/dist/Typeahead/BaseTypeahead.js +85 -30
  121. package/dist/Typeahead/Typeahead.d.ts +0 -21
  122. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  123. package/dist/Typeahead/Typeahead.js +108 -67
  124. package/dist/Typeahead/busyIndicatorLane.d.ts +62 -0
  125. package/dist/Typeahead/busyIndicatorLane.d.ts.map +1 -0
  126. package/dist/Typeahead/busyIndicatorLane.js +98 -0
  127. package/dist/astryx.css +25 -7
  128. package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -1
  129. package/dist/hooks/useFocusReturnVisibility.js +3 -5
  130. package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
  131. package/dist/hooks/useIsomorphicLayoutEffect.d.ts.map +1 -1
  132. package/dist/hooks/useOverflow.js +11 -11
  133. package/dist/hooks/useScrollOverflow.d.ts.map +1 -1
  134. package/dist/hooks/useScrollOverflow.js +7 -4
  135. package/dist/theme/defineTheme.d.ts +13 -0
  136. package/dist/theme/defineTheme.d.ts.map +1 -1
  137. package/dist/theme/defineTheme.js +7 -0
  138. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  139. package/dist/theme/generateThemeRules.js +1 -1
  140. package/dist/theme/localTokens.d.ts +18 -0
  141. package/dist/theme/localTokens.d.ts.map +1 -0
  142. package/dist/theme/localTokens.js +162 -0
  143. package/dist/utils/interactionModality.d.ts +6 -28
  144. package/dist/utils/interactionModality.d.ts.map +1 -1
  145. package/dist/utils/interactionModality.js +72 -37
  146. package/dist/utils/sharedResizeObserver.d.ts +17 -9
  147. package/dist/utils/sharedResizeObserver.d.ts.map +1 -1
  148. package/dist/utils/sharedResizeObserver.js +54 -12
  149. package/locales/en.json +8 -0
  150. package/locales/pseudo.json +6 -0
  151. package/package.json +7 -2
  152. package/src/AlertDialog/useImperativeAlertDialog.test.tsx +163 -0
  153. package/src/AppShell/AppShell.doc.mjs +4 -3
  154. package/src/AppShell/AppShell.test.tsx +21 -0
  155. package/src/AppShell/AppShell.tsx +12 -7
  156. package/src/Banner/Banner.doc.mjs +5 -1
  157. package/src/Banner/Banner.tsx +12 -1
  158. package/src/BottomSheet/BottomSheet.doc.mjs +31 -0
  159. package/src/BottomSheet/BottomSheet.spec.md +178 -0
  160. package/src/Breadcrumbs/BreadcrumbItem.tsx +18 -9
  161. package/src/Breadcrumbs/Breadcrumbs.doc.mjs +4 -1
  162. package/src/Breadcrumbs/Breadcrumbs.test.tsx +12 -0
  163. package/src/Button/Button.doc.mjs +56 -0
  164. package/src/ButtonGroup/ButtonGroup.doc.mjs +47 -0
  165. package/src/Calendar/hooks/useCalendarConstraints.test.ts +153 -0
  166. package/src/Calendar/hooks/useCalendarDays.test.ts +265 -0
  167. package/src/Calendar/hooks/useCalendarNavigation.test.ts +252 -0
  168. package/src/Carousel/Carousel.test.tsx +83 -0
  169. package/src/Carousel/Carousel.tsx +8 -2
  170. package/src/Chat/ChatComposer.doc.mjs +7 -4
  171. package/src/Chat/ChatComposer.test.tsx +84 -2
  172. package/src/Chat/ChatComposer.tsx +45 -3
  173. package/src/Chat/ChatSendButton.test.tsx +189 -0
  174. package/src/Chat/ChatToolCalls.doc.mjs +1 -1
  175. package/src/Chat/ChatToolCalls.test.tsx +33 -1
  176. package/src/Chat/ChatToolCalls.tsx +13 -34
  177. package/src/Chat/useChatNewMessages.ts +3 -3
  178. package/src/Chat/useChatPasteAsToken.test.ts +191 -0
  179. package/src/CheckboxList/CheckboxList.doc.mjs +172 -31
  180. package/src/CheckboxList/CheckboxList.spec.md +215 -0
  181. package/src/CheckboxList/CheckboxList.test.tsx +45 -0
  182. package/src/CheckboxList/CheckboxListItem.doc.mjs +11 -9
  183. package/src/CheckboxList/CheckboxListItem.tsx +5 -3
  184. package/src/Collapsible/useCollapsible.test.tsx +236 -0
  185. package/src/CommandPalette/CommandPalette.doc.mjs +72 -0
  186. package/src/CommandPalette/CommandPalette.spec.md +224 -0
  187. package/src/CommandPalette/CommandPaletteEmpty.test.tsx +101 -0
  188. package/src/ComplexSelector/ComplexSelector.doc.mjs +42 -0
  189. package/src/ComplexSelector/ComplexSelector.spec.md +190 -0
  190. package/src/ContextMenu/ContextMenu.doc.mjs +48 -0
  191. package/src/ContextMenu/ContextMenu.spec.md +207 -0
  192. package/src/DateTimeInput/DateTimeInput.doc.mjs +43 -23
  193. package/src/DateTimeInput/DateTimeInput.tsx +388 -181
  194. package/src/DateTimeInput/DateTimeInputTouch.test.tsx +10 -2
  195. package/src/DateTimeInput/NativeDateSegment.tsx +308 -0
  196. package/src/DateTimeInput/NativePickerSegments.test.tsx +626 -0
  197. package/src/DateTimeInput/NativeTimeSegment.tsx +331 -0
  198. package/src/DateTimeInput/index.ts +1 -0
  199. package/src/DateTimeInput/nativePickerSegmentStyles.ts +120 -0
  200. package/src/Divider/Divider.doc.mjs +24 -0
  201. package/src/Divider/Divider.spec.md +157 -0
  202. package/src/DropdownMenu/DropdownMenu.doc.mjs +104 -0
  203. package/src/DropdownMenu/DropdownMenu.spec.md +246 -0
  204. package/src/DropdownMenu/DropdownMenu.tsx +2 -4
  205. package/src/Field/Field.doc.mjs +1 -0
  206. package/src/Field/Field.test.tsx +39 -0
  207. package/src/Field/Field.tsx +18 -1
  208. package/src/Field/InputClearButton.test.tsx +68 -6
  209. package/src/Field/PanelSearchInput.tsx +3 -5
  210. package/src/FieldStatus/FieldStatus.doc.mjs +21 -0
  211. package/src/FieldStatus/FieldStatus.spec.md +213 -0
  212. package/src/FieldStatus/FieldStatus.test.tsx +17 -0
  213. package/src/FieldStatus/FieldStatus.tsx +7 -2
  214. package/src/Grid/Grid.doc.mjs +19 -0
  215. package/src/Grid/Grid.spec.md +162 -0
  216. package/src/Icon/Icon.doc.mjs +196 -48
  217. package/src/Icon/Icon.spec.md +192 -0
  218. package/src/IconButton/IconButton.doc.mjs +38 -0
  219. package/src/Kbd/Kbd.doc.mjs +18 -0
  220. package/src/Kbd/Kbd.spec.md +142 -0
  221. package/src/Layer/useTouchTrigger.ts +2 -4
  222. package/src/Layout/Layout.doc.mjs +57 -17
  223. package/src/Layout/Layout.spec.md +235 -0
  224. package/src/Layout/Layout.test.tsx +1 -1
  225. package/src/Layout/Layout.tsx +7 -8
  226. package/src/Layout/LayoutHeader.doc.mjs +9 -0
  227. package/src/Layout/LayoutPanel.doc.mjs +4 -3
  228. package/src/Lightbox/Lightbox.doc.mjs +149 -33
  229. package/src/Lightbox/Lightbox.spec.md +198 -0
  230. package/src/Lightbox/useLightbox.test.tsx +263 -0
  231. package/src/Markdown/Markdown.doc.mjs +60 -0
  232. package/src/Markdown/Markdown.spec.md +207 -0
  233. package/src/MobileNav/MobileNav.doc.mjs +140 -32
  234. package/src/MobileNav/MobileNav.spec.md +209 -0
  235. package/src/MoreMenu/MoreMenu.doc.mjs +42 -0
  236. package/src/MoreMenu/MoreMenu.spec.md +222 -0
  237. package/src/MultiSelector/MultiSelector.doc.mjs +124 -1
  238. package/src/MultiSelector/MultiSelector.spec.md +293 -0
  239. package/src/MultiSelector/MultiSelector.test.tsx +305 -2
  240. package/src/MultiSelector/MultiSelector.tsx +124 -55
  241. package/src/NavIcon/NavIcon.doc.mjs +17 -0
  242. package/src/NavIcon/NavIcon.spec.md +134 -0
  243. package/src/NavMenu/NavHeadingMenu.spec.md +210 -0
  244. package/src/NavMenu/NavMenu.doc.mjs +37 -0
  245. package/src/Outline/Outline.doc.mjs +35 -0
  246. package/src/Outline/Outline.spec.md +162 -0
  247. package/src/OverflowList/OverflowList.doc.mjs +24 -0
  248. package/src/OverflowList/OverflowList.spec.md +157 -0
  249. package/src/Pagination/Pagination.doc.mjs +74 -0
  250. package/src/Pagination/Pagination.spec.md +226 -0
  251. package/src/Popover/Popover.doc.mjs +264 -65
  252. package/src/Popover/Popover.spec.md +421 -0
  253. package/src/Popover/usePopover.doc.mjs +208 -33
  254. package/src/PowerSearch/PowerSearch.spec.md +291 -0
  255. package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +253 -0
  256. package/src/PowerSearch/PowerSearchToken.test.tsx +228 -0
  257. package/src/ProgressBar/ProgressBar.doc.mjs +198 -38
  258. package/src/ProgressBar/ProgressBar.spec.md +261 -0
  259. package/src/RadioList/RadioList.test.tsx +136 -2
  260. package/src/RadioList/RadioListItem.doc.mjs +30 -11
  261. package/src/RadioList/RadioListItem.tsx +44 -8
  262. package/src/Resizable/Resizable.doc.mjs +130 -24
  263. package/src/Resizable/Resizable.spec.md +246 -0
  264. package/src/Resizable/ResizeHandle.test.tsx +177 -56
  265. package/src/Resizable/ResizeHandle.tsx +128 -45
  266. package/src/Resizable/index.ts +2 -0
  267. package/src/Resizable/shippedDirections.test.ts +462 -0
  268. package/src/Resizable/useResizable.doc.mjs +124 -29
  269. package/src/Resizable/useResizable.test.ts +1277 -3
  270. package/src/Resizable/useResizable.ts +760 -48
  271. package/src/Resizable/utils.test.ts +74 -0
  272. package/src/Resizable/utils.ts +35 -0
  273. package/src/Section/Section.doc.mjs +17 -0
  274. package/src/Section/Section.spec.md +139 -0
  275. package/src/SegmentedControl/SegmentedControl.doc.mjs +83 -0
  276. package/src/SegmentedControl/SegmentedControl.spec.md +152 -0
  277. package/src/Selector/Selector.doc.mjs +133 -97
  278. package/src/Selector/Selector.spec.md +300 -0
  279. package/src/Selector/Selector.test.tsx +318 -1
  280. package/src/Selector/Selector.tsx +127 -60
  281. package/src/Selector/useSelectorPresentation.ts +24 -4
  282. package/src/SideNav/SideNav.test.tsx +3 -3
  283. package/src/SideNav/SideNavCollapseButton.doc.mjs +8 -0
  284. package/src/SideNav/SideNavItem.doc.mjs +1 -1
  285. package/src/Skeleton/Skeleton.doc.mjs +12 -0
  286. package/src/Skeleton/Skeleton.spec.md +127 -0
  287. package/src/Slider/Slider.doc.mjs +184 -30
  288. package/src/Slider/Slider.spec.md +224 -0
  289. package/src/Slider/Slider.tsx +2 -5
  290. package/src/Spinner/Spinner.doc.mjs +5 -5
  291. package/src/Spinner/Spinner.test.tsx +32 -7
  292. package/src/Spinner/Spinner.tsx +46 -14
  293. package/src/Stack/Stack.doc.mjs +22 -0
  294. package/src/Stack/Stack.spec.md +155 -0
  295. package/src/StatusDot/StatusDot.doc.mjs +17 -0
  296. package/src/StatusDot/StatusDot.spec.md +138 -0
  297. package/src/Stepper/Step.doc.mjs +1 -1
  298. package/src/Stepper/Step.tsx +118 -5
  299. package/src/Stepper/Stepper.doc.mjs +51 -1
  300. package/src/Stepper/Stepper.spec.md +245 -0
  301. package/src/Stepper/Stepper.test.tsx +315 -0
  302. package/src/Stepper/Stepper.tsx +8 -0
  303. package/src/Switch/Switch.doc.mjs +40 -0
  304. package/src/Switch/Switch.spec.md +175 -0
  305. package/src/TabList/TabList.tsx +2 -3
  306. package/src/Table/Table.doc.mjs +102 -24
  307. package/src/Table/Table.perf.test.tsx +51 -1
  308. package/src/Table/Table.spec.md +325 -0
  309. package/src/Table/TableHeaderCell.test.tsx +291 -0
  310. package/src/Table/index.ts +19 -4
  311. package/src/Table/plugins/columnResize/useTableColumnResize.tsx +8 -12
  312. package/src/Table/plugins/filtering/useTableFilterState.test.tsx +143 -0
  313. package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +2 -1
  314. package/src/Table/plugins/rowStatus/index.ts +1 -4
  315. package/src/Table/plugins/rowStatus/useTableRowStatus.public.test.ts +92 -0
  316. package/src/Table/plugins/rowStatus/useTableRowStatus.spec.md +354 -0
  317. package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +431 -56
  318. package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +155 -84
  319. package/src/Table/plugins/sortable/useTableSortable.test.tsx +72 -0
  320. package/src/Table/plugins/sortable/useTableSortable.tsx +18 -1
  321. package/src/Table/useTableGroupedRows.doc.mjs +2 -2
  322. package/src/Table/useTableRowStatus.doc.mjs +5 -5
  323. package/src/Text/Text.doc.mjs +190 -43
  324. package/src/Text/Text.spec.md +206 -0
  325. package/src/Text/useTruncation.ts +8 -7
  326. package/src/TextArea/TextArea.doc.mjs +255 -45
  327. package/src/TextArea/TextArea.spec.md +205 -0
  328. package/src/TimeInput/NativeTimeInput.test.tsx +269 -0
  329. package/src/TimeInput/TimeInput.doc.mjs +47 -23
  330. package/src/TimeInput/TimeInput.tsx +109 -50
  331. package/src/TimeInput/index.ts +1 -0
  332. package/src/Timestamp/Timestamp.doc.mjs +2 -2
  333. package/src/Timestamp/Timestamp.test.tsx +75 -3
  334. package/src/Timestamp/Timestamp.tsx +7 -153
  335. package/src/Timestamp/formatRelativeTime.ts +96 -0
  336. package/src/Toast/ToastViewport.test.tsx +194 -0
  337. package/src/Toast/ToastViewport.tsx +136 -58
  338. package/src/ToggleButton/ToggleButton.doc.mjs +56 -0
  339. package/src/Tokenizer/Tokenizer.doc.mjs +2 -0
  340. package/src/Tokenizer/Tokenizer.spec.md +293 -0
  341. package/src/Tokenizer/Tokenizer.test.tsx +335 -1
  342. package/src/Tokenizer/Tokenizer.tsx +129 -46
  343. package/src/Tokenizer/useEndLaneReserve.ts +146 -0
  344. package/src/Toolbar/Toolbar.doc.mjs +19 -0
  345. package/src/Toolbar/Toolbar.spec.md +174 -0
  346. package/src/Tooltip/Tooltip.doc.mjs +17 -0
  347. package/src/Tooltip/Tooltip.spec.md +142 -0
  348. package/src/Tooltip/Tooltip.test.tsx +21 -1
  349. package/src/Tooltip/useTooltip.test.tsx +318 -0
  350. package/src/Tooltip/useTooltip.tsx +60 -39
  351. package/src/TreeList/TreeList.doc.mjs +58 -0
  352. package/src/TreeList/TreeList.spec.md +224 -0
  353. package/src/Typeahead/BaseTypeahead.tsx +96 -25
  354. package/src/Typeahead/Typeahead.doc.mjs +89 -1
  355. package/src/Typeahead/Typeahead.spec.md +257 -0
  356. package/src/Typeahead/Typeahead.test.tsx +502 -1
  357. package/src/Typeahead/Typeahead.tsx +187 -69
  358. package/src/Typeahead/TypeaheadItem.test.tsx +93 -2
  359. package/src/Typeahead/busyIndicatorLane.tsx +109 -0
  360. package/src/__tests__/stylexDeclarations.ts +51 -0
  361. package/src/hooks/useFocusReturnVisibility.ts +3 -5
  362. package/src/hooks/useOverflow.ts +11 -11
  363. package/src/hooks/useScrollLock.test.ts +103 -0
  364. package/src/hooks/useScrollOverflow.ts +7 -4
  365. package/src/i18n/useTranslator.test.tsx +197 -0
  366. package/src/theme/defineTheme.test.ts +227 -0
  367. package/src/theme/defineTheme.ts +30 -0
  368. package/src/theme/derivedVarRegistry.test.ts +6 -0
  369. package/src/theme/generateThemeRules.ts +4 -1
  370. package/src/theme/localTokens.ts +272 -0
  371. package/src/theme/themingTargets.test.ts +126 -28
  372. package/src/utils/interactionModality.test.ts +83 -0
  373. package/src/utils/interactionModality.ts +88 -36
  374. package/src/utils/sharedResizeObserver.test.ts +124 -5
  375. package/src/utils/sharedResizeObserver.ts +60 -14
@@ -345,7 +345,10 @@ export function generateThemeRules(theme: DefinedTheme): string[] {
345
345
  const val = (key: string): string => tokens[key] || `var(${key})`;
346
346
 
347
347
  // 1. Token block — CSS custom properties on :scope
348
- const tokenEntries = Object.entries(tokens);
348
+ const tokenEntries = [
349
+ ...Object.entries(tokens),
350
+ ...Object.entries(theme.localTokens ?? {}),
351
+ ];
349
352
  if (tokenEntries.length > 0) {
350
353
  const declarations = tokenEntries
351
354
  .map(([prop, value]) => ` ${prop}: ${value};`)
@@ -0,0 +1,272 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import type {ComponentStyleMap, DefinedTheme, TokenValue} from './defineTheme';
4
+ import type {ResolvedOnMedia} from './onMediaTokens';
5
+
6
+ const LOCAL_TOKEN_PREFIX = '--astryx-theme-';
7
+ const THEME_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
8
+ const LOCAL_TOKEN_SUFFIX_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
9
+ const CSS_VAR_PATTERN = /var\(\s*(--[^,\s)]+)/gi;
10
+
11
+ export interface ResolvedLocalTokenContract {
12
+ localTokens: Record<string, string>;
13
+ owners: Record<string, string>;
14
+ lineage: string[];
15
+ }
16
+
17
+ function hasOwn(object: object, key: PropertyKey): boolean {
18
+ return Object.prototype.hasOwnProperty.call(object, key);
19
+ }
20
+
21
+ function isExactLocalTokenName(name: string, owner: string): boolean {
22
+ const prefix = `${LOCAL_TOKEN_PREFIX}${owner}-`;
23
+ return (
24
+ name.startsWith(prefix) &&
25
+ LOCAL_TOKEN_SUFFIX_PATTERN.test(name.slice(prefix.length))
26
+ );
27
+ }
28
+
29
+ function resolveTokenValue(value: TokenValue, path: string): string {
30
+ if (typeof value === 'string') {
31
+ return value;
32
+ }
33
+ if (
34
+ Array.isArray(value) &&
35
+ value.length === 2 &&
36
+ typeof value[0] === 'string' &&
37
+ typeof value[1] === 'string'
38
+ ) {
39
+ return `light-dark(${value[0]}, ${value[1]})`;
40
+ }
41
+ throw new Error(
42
+ `${path} must be a CSS string or a [light, dark] string tuple.`,
43
+ );
44
+ }
45
+
46
+ function collectLocalReferences(value: unknown, refs: Set<string>): void {
47
+ if (typeof value === 'string') {
48
+ CSS_VAR_PATTERN.lastIndex = 0;
49
+ for (
50
+ let match = CSS_VAR_PATTERN.exec(value);
51
+ match;
52
+ match = CSS_VAR_PATTERN.exec(value)
53
+ ) {
54
+ const name = match[1];
55
+ if (name.startsWith(LOCAL_TOKEN_PREFIX)) {
56
+ refs.add(name);
57
+ }
58
+ }
59
+ return;
60
+ }
61
+ if (Array.isArray(value)) {
62
+ for (const item of value) {
63
+ collectLocalReferences(item, refs);
64
+ }
65
+ return;
66
+ }
67
+ if (value && typeof value === 'object') {
68
+ for (const nested of Object.values(value)) {
69
+ collectLocalReferences(nested, refs);
70
+ }
71
+ }
72
+ }
73
+
74
+ function assertNoLocalTokenCycles(localTokens: Record<string, string>): void {
75
+ const dependencies = new Map<string, string[]>();
76
+ for (const [name, value] of Object.entries(localTokens)) {
77
+ const refs = new Set<string>();
78
+ collectLocalReferences(value, refs);
79
+ dependencies.set(
80
+ name,
81
+ [...refs].filter(reference => hasOwn(localTokens, reference)),
82
+ );
83
+ }
84
+
85
+ const visiting = new Set<string>();
86
+ const visited = new Set<string>();
87
+ const visit = (name: string, path: string[]): void => {
88
+ if (visiting.has(name)) {
89
+ const start = path.indexOf(name);
90
+ const cycle = [...path.slice(start), name].join(' -> ');
91
+ throw new Error(`Theme-local token cycle detected: ${cycle}.`);
92
+ }
93
+ if (visited.has(name)) {
94
+ return;
95
+ }
96
+ visiting.add(name);
97
+ for (const dependency of dependencies.get(name) ?? []) {
98
+ visit(dependency, [...path, name]);
99
+ }
100
+ visiting.delete(name);
101
+ visited.add(name);
102
+ };
103
+
104
+ for (const name of Object.keys(localTokens)) {
105
+ visit(name, []);
106
+ }
107
+ }
108
+
109
+ function assertDeclaredReferences(
110
+ localTokens: Record<string, string>,
111
+ components: ComponentStyleMap | undefined,
112
+ onDark: ResolvedOnMedia | undefined,
113
+ onLight: ResolvedOnMedia | undefined,
114
+ ): void {
115
+ const refs = new Set<string>();
116
+ collectLocalReferences(localTokens, refs);
117
+ collectLocalReferences(components, refs);
118
+ collectLocalReferences(onDark?.components, refs);
119
+ collectLocalReferences(onLight?.components, refs);
120
+
121
+ for (const reference of refs) {
122
+ if (!hasOwn(localTokens, reference)) {
123
+ throw new Error(
124
+ `Theme-local token reference "${reference}" has no declaration in the enrolled theme lineage.`,
125
+ );
126
+ }
127
+ }
128
+ }
129
+
130
+ function assertInheritedContract(
131
+ themeName: string,
132
+ base: DefinedTheme,
133
+ ): asserts base is DefinedTheme & {
134
+ localTokens: Record<string, string>;
135
+ __localTokenOwners: Record<string, string>;
136
+ __localTokenLineage: string[];
137
+ } {
138
+ const lineage = base.__localTokenLineage;
139
+ if (
140
+ !Array.isArray(lineage) ||
141
+ lineage.length === 0 ||
142
+ lineage[lineage.length - 1] !== base.name ||
143
+ lineage.some(name => typeof name !== 'string' || name.length === 0)
144
+ ) {
145
+ throw new Error(
146
+ `defineTheme("${themeName}"): the base theme has invalid theme-local token lineage metadata.`,
147
+ );
148
+ }
149
+ if (
150
+ !base.localTokens ||
151
+ typeof base.localTokens !== 'object' ||
152
+ Array.isArray(base.localTokens) ||
153
+ !base.__localTokenOwners ||
154
+ typeof base.__localTokenOwners !== 'object' ||
155
+ Array.isArray(base.__localTokenOwners)
156
+ ) {
157
+ throw new Error(
158
+ `defineTheme("${themeName}"): the base theme has incomplete theme-local token metadata.`,
159
+ );
160
+ }
161
+
162
+ for (const [name, value] of Object.entries(base.localTokens)) {
163
+ const owner = base.__localTokenOwners[name];
164
+ if (
165
+ typeof value !== 'string' ||
166
+ !owner ||
167
+ !lineage.includes(owner) ||
168
+ !isExactLocalTokenName(name, owner)
169
+ ) {
170
+ throw new Error(
171
+ `defineTheme("${themeName}"): inherited local token "${name}" does not match its exact lineage metadata.`,
172
+ );
173
+ }
174
+ }
175
+ for (const name of Object.keys(base.__localTokenOwners)) {
176
+ if (!hasOwn(base.localTokens, name)) {
177
+ throw new Error(
178
+ `defineTheme("${themeName}"): inherited local-token owner metadata names undeclared token "${name}".`,
179
+ );
180
+ }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Resolve and validate the opt-in theme-local token contract.
186
+ *
187
+ * Themes that omit `localTokens` and do not extend an enrolled base bypass this
188
+ * function's reserved-namespace checks so legacy token behavior stays intact.
189
+ */
190
+ export function resolveLocalTokenContract(
191
+ input: {
192
+ name: string;
193
+ localTokens?: Record<string, TokenValue>;
194
+ },
195
+ base: DefinedTheme | undefined,
196
+ tokens: Record<string, string>,
197
+ components: ComponentStyleMap | undefined,
198
+ onDark: ResolvedOnMedia | undefined,
199
+ onLight: ResolvedOnMedia | undefined,
200
+ ): ResolvedLocalTokenContract | undefined {
201
+ const directlyEnrolled = hasOwn(input, 'localTokens');
202
+ const inherited = base?.__localTokenLineage !== undefined;
203
+
204
+ if (!directlyEnrolled && !inherited) {
205
+ return undefined;
206
+ }
207
+
208
+ if (directlyEnrolled && !THEME_NAME_PATTERN.test(input.name)) {
209
+ throw new Error(
210
+ `defineTheme("${input.name}"): themes using localTokens require a stable lower-kebab name.`,
211
+ );
212
+ }
213
+
214
+ if (inherited && base) {
215
+ assertInheritedContract(input.name, base);
216
+ }
217
+
218
+ const localTokens = {...base?.localTokens};
219
+ const owners = {...base?.__localTokenOwners};
220
+ const declarations = input.localTokens;
221
+
222
+ if (
223
+ directlyEnrolled &&
224
+ (declarations === null ||
225
+ declarations === undefined ||
226
+ typeof declarations !== 'object' ||
227
+ Array.isArray(declarations))
228
+ ) {
229
+ throw new Error(
230
+ `defineTheme("${input.name}"): localTokens must be a token map.`,
231
+ );
232
+ }
233
+
234
+ for (const [name, value] of Object.entries(declarations ?? {})) {
235
+ const inheritedOwner = owners[name];
236
+ if (!inheritedOwner) {
237
+ const expectedPrefix = `${LOCAL_TOKEN_PREFIX}${input.name}-`;
238
+ if (!isExactLocalTokenName(name, input.name)) {
239
+ throw new Error(
240
+ `defineTheme("${input.name}"): local token "${name}" must use the exact namespace "${expectedPrefix}" followed by a lowercase kebab-case purpose.`,
241
+ );
242
+ }
243
+ owners[name] = input.name;
244
+ }
245
+ localTokens[name] = resolveTokenValue(
246
+ value,
247
+ `defineTheme("${input.name}").localTokens["${name}"]`,
248
+ );
249
+ }
250
+
251
+ for (const name of Object.keys(localTokens)) {
252
+ if (!owners[name]) {
253
+ throw new Error(
254
+ `defineTheme("${input.name}"): inherited local token "${name}" has no owner metadata.`,
255
+ );
256
+ }
257
+ if (hasOwn(tokens, name)) {
258
+ throw new Error(
259
+ `defineTheme("${input.name}"): token "${name}" cannot be declared in both tokens and localTokens.`,
260
+ );
261
+ }
262
+ }
263
+
264
+ assertDeclaredReferences(localTokens, components, onDark, onLight);
265
+ assertNoLocalTokenCycles(localTokens);
266
+
267
+ return {
268
+ localTokens,
269
+ owners,
270
+ lineage: [...(base?.__localTokenLineage ?? []), input.name],
271
+ };
272
+ }
@@ -3,8 +3,10 @@
3
3
  /* eslint-disable @typescript-eslint/no-require-imports */
4
4
  /**
5
5
  * @file Guards `theming.targets` against the real `themeProps()` call sites (#3741).
6
- * @input Component sources (*.tsx/*.ts) and their `{Name}.doc.mjs` files.
7
- * @output Vitest failures naming each undocumented class / visual prop.
6
+ * @input Core/Lab component sources (*.tsx/*.ts), their `{Name}.doc.mjs`
7
+ * files, and the Lab promotion-candidate manifest.
8
+ * @output Vitest failures naming each undocumented class / visual prop on the
9
+ * stable Core surface or a capability-participating Lab component.
8
10
  * @position Sibling of derivedVarRegistry.test.ts, which already validates the
9
11
  * OTHER fields of the same `theming` block (`vars`, `derived`). `targets` was
10
12
  * the one field with no machine check, so it drifted — twice (#3652, #3680).
@@ -22,15 +24,36 @@
22
24
  * `visualProps` or `states`. Docs may list MORE than the source passes —
23
25
  * components forward props they don't themselves reflect (Timestamp passes
24
26
  * `{format}` but documents `type`/`color`/`format`), and that is intentional.
27
+ *
28
+ * Lab is capability-based: a component participates when it already declares
29
+ * `theming.targets` or enters the existing promotion-candidate manifest. Other
30
+ * Lab components remain free to iterate with runtime hooks that are not yet a
31
+ * documented public theming promise.
25
32
  */
26
33
 
27
34
  import {describe, it, expect} from 'vitest';
28
35
  import {readdirSync, readFileSync} from 'node:fs';
29
- import {join, relative} from 'node:path';
36
+ import {basename, join, relative} from 'node:path';
30
37
  import ts from 'typescript';
31
38
  import {stableClassName} from '../naming';
32
39
 
33
- const SRC_DIR = join(__dirname, '..');
40
+ const CORE_SRC_DIR = join(__dirname, '..');
41
+ const LAB_SRC_DIR = join(__dirname, '../../../lab/src');
42
+ const LAB_PROMOTION_MANIFEST = join(
43
+ __dirname,
44
+ '../../../../internal/lab-readiness/manifest.mjs',
45
+ );
46
+
47
+ interface LabPromotionCandidate {
48
+ sourceDir: string;
49
+ }
50
+
51
+ const {CANDIDATES: labPromotionCandidates} = require(
52
+ LAB_PROMOTION_MANIFEST,
53
+ ) as {CANDIDATES: LabPromotionCandidate[]};
54
+ const LAB_PROMOTION_DIRS = new Set(
55
+ labPromotionCandidates.map(candidate => candidate.sourceDir),
56
+ );
34
57
 
35
58
  // ---------------------------------------------------------------------------
36
59
  // Source scanning: find themeProps() call sites via the TypeScript AST
@@ -314,6 +337,7 @@ type DocBlock = {theming?: {targets?: DocTarget[]}};
314
337
  type ComponentDocModule = {docs?: DocBlock; docsZh?: DocBlock};
315
338
 
316
339
  interface ComponentInfo {
340
+ packageName: 'core' | 'lab';
317
341
  dir: string;
318
342
  sites: ThemeTargetSite[];
319
343
  /** The doc blocks that carry theming.targets, by the key they live under. */
@@ -328,7 +352,10 @@ interface ComponentInfo {
328
352
  * Reads the file as text rather than requiring it: this runs for directories
329
353
  * that have no doc of their own, so most candidates are misses.
330
354
  */
331
- function docFilesDocumenting(classNames: Set<string>): string[] {
355
+ function docFilesDocumenting(
356
+ srcDir: string,
357
+ classNames: Set<string>,
358
+ ): string[] {
332
359
  const matches: string[] = [];
333
360
  const walk = (dir: string): void => {
334
361
  for (const entry of readdirSync(dir, {withFileTypes: true})) {
@@ -346,18 +373,47 @@ function docFilesDocumenting(classNames: Set<string>): string[] {
346
373
  }
347
374
  }
348
375
  };
349
- walk(SRC_DIR);
376
+ walk(srcDir);
350
377
  return matches;
351
378
  }
352
379
 
353
- function discoverComponents(): ComponentInfo[] {
380
+ /**
381
+ * Every directory under `srcDir`, at any depth, relative to `srcDir`.
382
+ *
383
+ * Not just the top level: a component's sources are not always its own direct
384
+ * children — Table's plugins render from `Table/plugins/<name>/`. Scanning only
385
+ * the top level exempted every one of those from this guard, which is the same
386
+ * silent-exemption shape #3741 was filed about.
387
+ *
388
+ * Takes `srcDir` rather than closing over a module constant so it serves both
389
+ * packages, which is what the Lab enrollment above needs.
390
+ */
391
+ function sourceDirs(srcDir: string): string[] {
392
+ const out: string[] = [];
393
+ const walk = (rel: string): void => {
394
+ for (const entry of readdirSync(join(srcDir, rel), {
395
+ withFileTypes: true,
396
+ })) {
397
+ if (entry.isDirectory()) {
398
+ const child = rel === '' ? entry.name : join(rel, entry.name);
399
+ out.push(child);
400
+ walk(child);
401
+ }
402
+ }
403
+ };
404
+ walk('');
405
+ return out;
406
+ }
407
+
408
+ function discoverComponents(
409
+ srcDir: string,
410
+ packageName: ComponentInfo['packageName'],
411
+ requiredDirs: ReadonlySet<string> = new Set(),
412
+ ): ComponentInfo[] {
354
413
  const results: ComponentInfo[] = [];
355
- const dirs = readdirSync(SRC_DIR, {withFileTypes: true})
356
- .filter(d => d.isDirectory())
357
- .map(d => d.name);
358
414
 
359
- for (const dir of dirs) {
360
- const dirPath = join(SRC_DIR, dir);
415
+ for (const dir of sourceDirs(srcDir)) {
416
+ const dirPath = join(srcDir, dir);
361
417
  const dirEntries = readdirSync(dirPath);
362
418
 
363
419
  const sourceFiles = dirEntries.filter(
@@ -387,9 +443,13 @@ function discoverComponents(): ComponentInfo[] {
387
443
  // Both paths match the on-disk listing rather than existsSync: on
388
444
  // case-insensitive filesystems existsSync would match a differently-cased
389
445
  // doc file that CI never checks. (Same guard as derivedVarRegistry.test.ts.)
390
- const docFiles = dirEntries.includes(`${dir}.doc.mjs`)
391
- ? [join(dirPath, `${dir}.doc.mjs`)]
392
- : docFilesDocumenting(new Set(sites.map(s => s.className)));
446
+ // `basename`, not `dir`: a nested source dir is a path
447
+ // (`Table/plugins/foo`), and its own doc file is named for the last
448
+ // segment.
449
+ const ownDoc = `${basename(dir)}.doc.mjs`;
450
+ const docFiles = dirEntries.includes(ownDoc)
451
+ ? [join(dirPath, ownDoc)]
452
+ : docFilesDocumenting(srcDir, new Set(sites.map(s => s.className)));
393
453
  if (docFiles.length === 0) {
394
454
  continue;
395
455
  }
@@ -404,18 +464,23 @@ function discoverComponents(): ComponentInfo[] {
404
464
  }
405
465
  for (const key of ['docs', 'docsZh'] as const) {
406
466
  const targets = mod[key]?.theming?.targets;
407
- // Only blocks that already document a theming surface are held to it —
408
- // a doc with no theming block at all is a separate (documentation) gap.
467
+ // A normal Core or Lab component enrolls by declaring a theming
468
+ // surface. Promotion candidates are also enrolled here so a missing
469
+ // block is a failure rather than a silently skipped readiness gap.
409
470
  if (targets != null) {
410
- docBlocks.push({key, file: relative(SRC_DIR, docFile), targets});
471
+ docBlocks.push({key, file: relative(srcDir, docFile), targets});
411
472
  }
412
473
  }
413
474
  }
414
- if (docBlocks.length === 0) {
475
+ const participates =
476
+ packageName === 'core'
477
+ ? docBlocks.length > 0
478
+ : requiredDirs.has(dir) || docBlocks.length > 0;
479
+ if (!participates) {
415
480
  continue;
416
481
  }
417
482
 
418
- results.push({dir, sites, docBlocks});
483
+ results.push({packageName, dir, sites, docBlocks});
419
484
  }
420
485
  return results;
421
486
  }
@@ -425,24 +490,57 @@ function discoverComponents(): ComponentInfo[] {
425
490
  // ---------------------------------------------------------------------------
426
491
 
427
492
  describe('theming.targets matches the themeProps() call sites', () => {
428
- const components = discoverComponents();
493
+ const components = [
494
+ ...discoverComponents(CORE_SRC_DIR, 'core'),
495
+ ...discoverComponents(LAB_SRC_DIR, 'lab', LAB_PROMOTION_DIRS),
496
+ ];
497
+
498
+ it('finds participating Core and Lab components', () => {
499
+ // A refactor that renames themeProps or drops the Lab package from this
500
+ // inventory must not silently disable either side of the guard.
501
+ expect(components.some(component => component.packageName === 'core')).toBe(
502
+ true,
503
+ );
504
+ expect(components.some(component => component.packageName === 'lab')).toBe(
505
+ true,
506
+ );
507
+ });
429
508
 
430
- it('finds components to check', () => {
431
- // A refactor that renames themeProps must not silently disable this file.
432
- expect(components.length).toBeGreaterThan(0);
509
+ it('enrolls every Lab promotion candidate and explicit theming capability', () => {
510
+ const labDirs = new Set(
511
+ components
512
+ .filter(component => component.packageName === 'lab')
513
+ .map(component => component.dir),
514
+ );
515
+ expect([...LAB_PROMOTION_DIRS].filter(dir => !labDirs.has(dir))).toEqual(
516
+ [],
517
+ );
518
+ expect(LAB_PROMOTION_DIRS.has('CircularProgress')).toBe(false);
519
+ expect(labDirs.has('CircularProgress')).toBe(true);
520
+ expect(labDirs.has('Schedule')).toBe(false);
433
521
  });
434
522
 
435
- for (const {dir, sites, docBlocks} of components) {
523
+ for (const {packageName, dir, sites, docBlocks} of components) {
524
+ const componentLabel = `${packageName}/${dir}`;
436
525
  const renderedClasses = [...new Set(sites.map(s => s.className))].sort();
437
526
 
527
+ it(`${componentLabel}: participating components declare theming metadata`, () => {
528
+ expect(
529
+ docBlocks,
530
+ `${componentLabel} participates in the public theming contract but has ` +
531
+ `no loadable .doc.mjs theming.targets. Lab components participate ` +
532
+ `when they declare theming.targets or enter the promotion manifest.`,
533
+ ).not.toHaveLength(0);
534
+ });
535
+
438
536
  for (const {key, file, targets} of docBlocks) {
439
537
  const documented = new Set(targets.map(t => t.className));
440
538
 
441
- it(`${dir} (${file} ${key}): every rendered class is documented`, () => {
539
+ it(`${componentLabel} (${file} ${key}): every rendered class is documented`, () => {
442
540
  const undocumented = renderedClasses.filter(c => !documented.has(c));
443
541
  expect(
444
542
  undocumented,
445
- `${dir} renders ${undocumented.length} astryx-* class(es) that ` +
543
+ `${componentLabel} renders ${undocumented.length} astryx-* class(es) that ` +
446
544
  `${file} ${key}.theming.targets does not document: ` +
447
545
  `${undocumented.join(', ')}. An undocumented class is an ` +
448
546
  `unthemeable element — theme authors and codegen read targets[] ` +
@@ -450,7 +548,7 @@ describe('theming.targets matches the themeProps() call sites', () => {
450
548
  ).toEqual([]);
451
549
  });
452
550
 
453
- it(`${dir} (${file} ${key}): every visual prop passed to themeProps is documented`, () => {
551
+ it(`${componentLabel} (${file} ${key}): every visual prop passed to themeProps is documented`, () => {
454
552
  const missing: string[] = [];
455
553
  for (const site of sites) {
456
554
  const target = targets.find(t => t.className === site.className);
@@ -0,0 +1,83 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file interactionModality.test.ts
5
+ * @input The interaction-modality utility loaded more than once
6
+ * @output Verifies document-wide listener and modality state continuity
7
+ * @position Unit coverage for interactionModality.ts
8
+ */
9
+
10
+ import {afterEach, describe, expect, it, vi} from 'vitest';
11
+
12
+ const isolatedDocument = document.implementation.createHTMLDocument();
13
+
14
+ async function loadTracker() {
15
+ vi.resetModules();
16
+ return import('./interactionModality');
17
+ }
18
+
19
+ afterEach(() => {
20
+ vi.unstubAllGlobals();
21
+ vi.restoreAllMocks();
22
+ });
23
+
24
+ describe('interactionModality', () => {
25
+ it('does not touch the document when the module loads', async () => {
26
+ vi.stubGlobal('document', isolatedDocument);
27
+ const addEventListener = vi.spyOn(isolatedDocument, 'addEventListener');
28
+
29
+ await loadTracker();
30
+
31
+ expect(addEventListener).not.toHaveBeenCalled();
32
+ });
33
+
34
+ it('shares one listener pair and tracks input between consumers', async () => {
35
+ vi.stubGlobal('document', isolatedDocument);
36
+ const addEventListener = vi.spyOn(isolatedDocument, 'addEventListener');
37
+ const removeEventListener = vi.spyOn(
38
+ isolatedDocument,
39
+ 'removeEventListener',
40
+ );
41
+ const firstModule = await loadTracker();
42
+ const secondModule = await loadTracker();
43
+
44
+ const firstConsumer =
45
+ firstModule.__startInteractionModalityTrackingForTest();
46
+ const secondConsumer =
47
+ secondModule.__startInteractionModalityTrackingForTest();
48
+
49
+ expect(
50
+ addEventListener.mock.calls.filter(([type]) => type === 'pointerdown'),
51
+ ).toHaveLength(1);
52
+ expect(
53
+ addEventListener.mock.calls.filter(([type]) => type === 'keydown'),
54
+ ).toHaveLength(1);
55
+
56
+ isolatedDocument.dispatchEvent(new Event('pointerdown'));
57
+ expect(secondModule.getInteractionModality()).toBe('pointer');
58
+
59
+ firstConsumer();
60
+ secondConsumer();
61
+ isolatedDocument.dispatchEvent(new KeyboardEvent('keydown', {key: 'Tab'}));
62
+ expect(firstModule.getInteractionModality()).toBe('keyboard');
63
+ expect(removeEventListener).not.toHaveBeenCalled();
64
+
65
+ secondModule.__startInteractionModalityTrackingForTest()();
66
+ expect(
67
+ addEventListener.mock.calls.filter(([type]) => type === 'pointerdown'),
68
+ ).toHaveLength(1);
69
+ expect(
70
+ addEventListener.mock.calls.filter(([type]) => type === 'keydown'),
71
+ ).toHaveLength(1);
72
+ });
73
+
74
+ it('uses the keyboard-safe default without a document', async () => {
75
+ vi.stubGlobal('document', undefined);
76
+ const serverModule = await loadTracker();
77
+
78
+ expect(serverModule.getInteractionModality()).toBe('keyboard');
79
+ expect(() =>
80
+ serverModule.__startInteractionModalityTrackingForTest()(),
81
+ ).not.toThrow();
82
+ });
83
+ });