@astryxdesign/core 0.1.8 → 0.1.9-canary.09e9ddd

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 (610) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/dist/AppShell/AppShell.d.ts.map +1 -1
  3. package/dist/AppShell/AppShell.js +6 -2
  4. package/dist/AppShell/AppShellMobileContext.d.ts +8 -0
  5. package/dist/AppShell/AppShellMobileContext.d.ts.map +1 -1
  6. package/dist/Avatar/Avatar.d.ts +53 -3
  7. package/dist/Avatar/Avatar.d.ts.map +1 -1
  8. package/dist/Avatar/Avatar.js +203 -49
  9. package/dist/Avatar/AvatarStatusDot.d.ts +33 -5
  10. package/dist/Avatar/AvatarStatusDot.d.ts.map +1 -1
  11. package/dist/Avatar/AvatarStatusDot.js +144 -19
  12. package/dist/AvatarGroup/AvatarGroup.d.ts +7 -1
  13. package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
  14. package/dist/AvatarGroup/AvatarGroup.js +58 -6
  15. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  16. package/dist/AvatarGroup/AvatarGroupOverflow.js +1 -0
  17. package/dist/Banner/Banner.d.ts +9 -1
  18. package/dist/Banner/Banner.d.ts.map +1 -1
  19. package/dist/Banner/Banner.js +33 -4
  20. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +31 -2
  21. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  22. package/dist/Breadcrumbs/BreadcrumbItem.js +208 -6
  23. package/dist/Breadcrumbs/index.d.ts +5 -0
  24. package/dist/Breadcrumbs/index.d.ts.map +1 -1
  25. package/dist/Breadcrumbs/index.js +9 -1
  26. package/dist/Button/Button.d.ts +8 -2
  27. package/dist/Button/Button.d.ts.map +1 -1
  28. package/dist/Button/Button.js +33 -4
  29. package/dist/ButtonGroup/ButtonGroup.d.ts +9 -1
  30. package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
  31. package/dist/ButtonGroup/ButtonGroup.js +30 -1
  32. package/dist/Calendar/Calendar.d.ts.map +1 -1
  33. package/dist/Calendar/Calendar.js +30 -2
  34. package/dist/Calendar/dayCellUtils.js +1 -1
  35. package/dist/Card/Card.d.ts +8 -2
  36. package/dist/Card/Card.d.ts.map +1 -1
  37. package/dist/Card/Card.js +28 -2
  38. package/dist/Carousel/Carousel.d.ts +4 -1
  39. package/dist/Carousel/Carousel.d.ts.map +1 -1
  40. package/dist/Carousel/Carousel.js +24 -4
  41. package/dist/Chat/ChatComposer.d.ts +8 -0
  42. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  43. package/dist/Chat/ChatComposer.js +46 -7
  44. package/dist/Chat/ChatComposerDrawer.d.ts +2 -0
  45. package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
  46. package/dist/Chat/ChatComposerDrawer.js +11 -1
  47. package/dist/Chat/ChatComposerInput.d.ts +20 -3
  48. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  49. package/dist/Chat/ChatComposerInput.js +36 -4
  50. package/dist/Chat/ChatContext.d.ts +33 -0
  51. package/dist/Chat/ChatContext.d.ts.map +1 -1
  52. package/dist/Chat/ChatContext.js +16 -2
  53. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  54. package/dist/Chat/ChatDictationButton.js +1 -1
  55. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  56. package/dist/Chat/ChatLayout.js +4 -4
  57. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  58. package/dist/Chat/ChatLayoutScrollButton.js +4 -4
  59. package/dist/Chat/ChatMessageMetadata.d.ts.map +1 -1
  60. package/dist/Chat/ChatSendButton.d.ts.map +1 -1
  61. package/dist/Chat/ChatSendButton.js +7 -1
  62. package/dist/Chat/ChatToolCalls.d.ts +5 -1
  63. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  64. package/dist/Chat/ChatToolCalls.js +18 -9
  65. package/dist/Chat/index.d.ts +2 -1
  66. package/dist/Chat/index.d.ts.map +1 -1
  67. package/dist/Chat/index.js +1 -1
  68. package/dist/Chat/useChatStreamScroll.d.ts +2 -1
  69. package/dist/Chat/useChatStreamScroll.d.ts.map +1 -1
  70. package/dist/Chat/useChatStreamScroll.js +22 -8
  71. package/dist/Chat/useSpeechRecognition.d.ts +0 -1
  72. package/dist/Chat/useSpeechRecognition.d.ts.map +1 -1
  73. package/dist/Chat/useSpeechRecognition.js +1 -1
  74. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  75. package/dist/Chat/useTriggerMenu.js +2 -2
  76. package/dist/CheckboxInput/CheckboxInput.js +1 -1
  77. package/dist/CheckboxList/CheckboxListItem.d.ts +20 -1
  78. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  79. package/dist/CheckboxList/CheckboxListItem.js +11 -1
  80. package/dist/Citation/Citation.d.ts +22 -1
  81. package/dist/Citation/Citation.d.ts.map +1 -1
  82. package/dist/Citation/Citation.js +20 -6
  83. package/dist/ClickableCard/ClickableCard.d.ts +9 -2
  84. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  85. package/dist/ClickableCard/ClickableCard.js +5 -2
  86. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  87. package/dist/CodeBlock/CodeBlock.js +9 -2
  88. package/dist/CommandPalette/CommandPalette.d.ts +1 -1
  89. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  90. package/dist/CommandPalette/CommandPalette.js +3 -1
  91. package/dist/CommandPalette/CommandPaletteGroup.d.ts.map +1 -1
  92. package/dist/CommandPalette/CommandPaletteGroup.js +9 -2
  93. package/dist/CommandPalette/CommandPaletteInput.d.ts +7 -1
  94. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  95. package/dist/CommandPalette/CommandPaletteInput.js +6 -1
  96. package/dist/CommandPalette/CommandPaletteItem.js +1 -1
  97. package/dist/DateInput/DateInput.d.ts +55 -2
  98. package/dist/DateInput/DateInput.d.ts.map +1 -1
  99. package/dist/DateInput/DateInput.js +28 -2
  100. package/dist/DateInput/index.d.ts +1 -1
  101. package/dist/DateInput/index.d.ts.map +1 -1
  102. package/dist/DateRangeInput/DateRangeInput.d.ts +9 -2
  103. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  104. package/dist/DateRangeInput/DateRangeInput.js +3 -1
  105. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  106. package/dist/DateTimeInput/DateTimeInput.js +18 -3
  107. package/dist/Dialog/Dialog.d.ts +3 -0
  108. package/dist/Dialog/Dialog.d.ts.map +1 -1
  109. package/dist/Dialog/Dialog.js +57 -6
  110. package/dist/Dialog/DialogContext.d.ts +7 -0
  111. package/dist/Dialog/DialogContext.d.ts.map +1 -1
  112. package/dist/Dialog/DialogContext.js +2 -2
  113. package/dist/Dialog/DialogHeader.d.ts +5 -2
  114. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  115. package/dist/Dialog/DialogHeader.js +6 -1
  116. package/dist/Divider/Divider.js +1 -1
  117. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +1 -2
  118. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  119. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +1 -2
  120. package/dist/DropdownMenu/DropdownMenuItem.js +1 -1
  121. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +1 -2
  122. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
  123. package/dist/DropdownMenu/DropdownMenuRadioGroup.js +1 -2
  124. package/dist/Field/Field.d.ts.map +1 -1
  125. package/dist/Field/Field.js +2 -3
  126. package/dist/Field/FieldLabel.d.ts +1 -1
  127. package/dist/Field/FieldLabel.d.ts.map +1 -1
  128. package/dist/Field/FieldLabel.js +43 -16
  129. package/dist/FieldStatus/FieldStatus.d.ts +15 -0
  130. package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
  131. package/dist/FieldStatus/FieldStatus.js +58 -6
  132. package/dist/FileInput/FileInput.d.ts +9 -2
  133. package/dist/FileInput/FileInput.d.ts.map +1 -1
  134. package/dist/FileInput/FileInput.js +42 -20
  135. package/dist/HoverCard/HoverCard.d.ts +9 -1
  136. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  137. package/dist/HoverCard/HoverCard.js +2 -0
  138. package/dist/HoverCard/useHoverCard.d.ts +8 -0
  139. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  140. package/dist/HoverCard/useHoverCard.js +7 -2
  141. package/dist/Icon/Icon.d.ts +7 -2
  142. package/dist/Icon/Icon.d.ts.map +1 -1
  143. package/dist/Item/Item.d.ts +6 -1
  144. package/dist/Item/Item.d.ts.map +1 -1
  145. package/dist/Item/Item.js +22 -1
  146. package/dist/Layer/useLayer.d.ts +6 -0
  147. package/dist/Layer/useLayer.d.ts.map +1 -1
  148. package/dist/Layer/useLayer.js +2 -0
  149. package/dist/Lightbox/Lightbox.js +4 -4
  150. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  151. package/dist/MobileNav/MobileNav.js +6 -1
  152. package/dist/MobileNav/MobileNavToggle.d.ts.map +1 -1
  153. package/dist/MobileNav/MobileNavToggle.js +4 -0
  154. package/dist/MultiSelector/MultiSelector.d.ts +9 -1
  155. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  156. package/dist/MultiSelector/MultiSelector.js +41 -12
  157. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  158. package/dist/MultiSelector/hooks.js +16 -0
  159. package/dist/NavMenu/NavHeadingMenuItem.js +1 -1
  160. package/dist/NumberInput/NumberInput.d.ts +9 -2
  161. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  162. package/dist/NumberInput/NumberInput.js +8 -1
  163. package/dist/Outline/Outline.d.ts +0 -1
  164. package/dist/Outline/Outline.d.ts.map +1 -1
  165. package/dist/Outline/Outline.js +0 -1
  166. package/dist/OverflowList/OverflowList.d.ts +20 -3
  167. package/dist/OverflowList/OverflowList.d.ts.map +1 -1
  168. package/dist/OverflowList/OverflowList.js +29 -4
  169. package/dist/Pagination/Pagination.d.ts.map +1 -1
  170. package/dist/Pagination/Pagination.js +11 -3
  171. package/dist/Popover/Popover.d.ts.map +1 -1
  172. package/dist/Popover/Popover.js +3 -2
  173. package/dist/Popover/usePopover.d.ts.map +1 -1
  174. package/dist/Popover/usePopover.js +3 -9
  175. package/dist/PowerSearch/PowerSearch.d.ts +9 -2
  176. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  177. package/dist/PowerSearch/PowerSearch.js +2 -0
  178. package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
  179. package/dist/PowerSearch/usePowerSearchSource.d.ts.map +1 -1
  180. package/dist/SelectableCard/SelectableCard.d.ts +9 -2
  181. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  182. package/dist/SelectableCard/SelectableCard.js +16 -13
  183. package/dist/Selector/Selector.d.ts +8 -0
  184. package/dist/Selector/Selector.d.ts.map +1 -1
  185. package/dist/Selector/Selector.js +7 -3
  186. package/dist/Selector/hooks.d.ts.map +1 -1
  187. package/dist/Selector/hooks.js +16 -0
  188. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  189. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  190. package/dist/SideNav/SideNavHeading.js +21 -8
  191. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  192. package/dist/Slider/Slider.d.ts.map +1 -1
  193. package/dist/Slider/Slider.js +19 -2
  194. package/dist/Stack/stack.stylex.d.ts +9 -9
  195. package/dist/TabList/Tab.js +2 -2
  196. package/dist/TabList/TabMenu.js +1 -1
  197. package/dist/Table/BaseTable.d.ts.map +1 -1
  198. package/dist/Table/BaseTable.js +3 -2
  199. package/dist/Table/TableCell.js +3 -3
  200. package/dist/Table/TableHeaderCell.js +3 -3
  201. package/dist/Table/index.d.ts +2 -0
  202. package/dist/Table/index.d.ts.map +1 -1
  203. package/dist/Table/index.js +1 -0
  204. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  205. package/dist/Table/plugins/rowStatus/index.d.ts +3 -0
  206. package/dist/Table/plugins/rowStatus/index.d.ts.map +1 -0
  207. package/dist/Table/plugins/rowStatus/index.js +3 -0
  208. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts +60 -0
  209. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -0
  210. package/dist/Table/plugins/rowStatus/useTableRowStatus.js +145 -0
  211. package/dist/Table/plugins/selection/useTableSelection.d.ts +13 -0
  212. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  213. package/dist/Table/plugins/selection/useTableSelection.js +4 -1
  214. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +1 -1
  215. package/dist/Table/plugins/tree/useTableTreeData.d.ts +16 -0
  216. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
  217. package/dist/Table/plugins/tree/useTableTreeData.js +87 -0
  218. package/dist/Table/plugins/tree/useTableTreeState.d.ts +6 -0
  219. package/dist/Table/plugins/tree/useTableTreeState.d.ts.map +1 -1
  220. package/dist/Table/plugins/tree/useTableTreeState.js +22 -1
  221. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  222. package/dist/Table/useBaseTablePlugins.js +5 -3
  223. package/dist/TextArea/TextArea.d.ts +9 -1
  224. package/dist/TextArea/TextArea.d.ts.map +1 -1
  225. package/dist/TextArea/TextArea.js +2 -0
  226. package/dist/TextInput/TextInput.d.ts +9 -2
  227. package/dist/TextInput/TextInput.d.ts.map +1 -1
  228. package/dist/TextInput/TextInput.js +6 -1
  229. package/dist/Thumbnail/Thumbnail.d.ts +26 -5
  230. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  231. package/dist/Thumbnail/Thumbnail.js +97 -54
  232. package/dist/Thumbnail/thumbnail.markers.stylex.d.ts +9 -0
  233. package/dist/Thumbnail/thumbnail.markers.stylex.d.ts.map +1 -0
  234. package/dist/Thumbnail/thumbnail.markers.stylex.js +14 -0
  235. package/dist/TimeInput/TimeInput.d.ts +9 -2
  236. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  237. package/dist/TimeInput/TimeInput.js +17 -4
  238. package/dist/Timestamp/Timestamp.d.ts +3 -1
  239. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  240. package/dist/Timestamp/Timestamp.js +8 -6
  241. package/dist/Toast/useToast.d.ts.map +1 -1
  242. package/dist/Toast/useToast.js +2 -5
  243. package/dist/ToggleButton/ToggleButtonGroup.d.ts +0 -1
  244. package/dist/ToggleButton/ToggleButtonGroup.d.ts.map +1 -1
  245. package/dist/ToggleButton/ToggleButtonGroup.js +1 -1
  246. package/dist/Tokenizer/Tokenizer.d.ts +10 -3
  247. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  248. package/dist/Tokenizer/Tokenizer.js +18 -4
  249. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  250. package/dist/TopNav/TopNavHeading.js +18 -8
  251. package/dist/TopNav/TopNavMegaMenu.js +10 -4
  252. package/dist/TopNav/TopNavMegaMenuFeaturedCard.d.ts +9 -1
  253. package/dist/TopNav/TopNavMegaMenuFeaturedCard.d.ts.map +1 -1
  254. package/dist/TopNav/TopNavMegaMenuFeaturedCard.js +8 -1
  255. package/dist/TopNav/TopNavMenu.d.ts +2 -1
  256. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  257. package/dist/TopNav/TopNavMenu.js +68 -6
  258. package/dist/TreeList/TreeList.d.ts +12 -3
  259. package/dist/TreeList/TreeList.d.ts.map +1 -1
  260. package/dist/TreeList/TreeList.js +2 -0
  261. package/dist/TreeList/TreeListBranches.d.ts +5 -0
  262. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  263. package/dist/TreeList/TreeListBranches.js +10 -4
  264. package/dist/TreeList/TreeListItem.d.ts +8 -2
  265. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  266. package/dist/TreeList/TreeListItem.js +28 -8
  267. package/dist/TreeList/TreeListTypes.d.ts +25 -1
  268. package/dist/TreeList/TreeListTypes.d.ts.map +1 -1
  269. package/dist/TreeList/index.d.ts +2 -2
  270. package/dist/TreeList/index.d.ts.map +1 -1
  271. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  272. package/dist/Typeahead/BaseTypeahead.js +20 -11
  273. package/dist/Typeahead/Typeahead.d.ts +9 -2
  274. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  275. package/dist/Typeahead/Typeahead.js +2 -0
  276. package/dist/astryx.css +41 -31
  277. package/dist/astryx.umd.js +51 -51
  278. package/dist/astryx.umd.js.map +4 -4
  279. package/dist/authoring/doc.d.ts.map +1 -1
  280. package/dist/docs-types.d.ts +8 -0
  281. package/dist/docs-types.d.ts.map +1 -1
  282. package/dist/hooks/computeOverflow.d.ts +47 -0
  283. package/dist/hooks/computeOverflow.d.ts.map +1 -0
  284. package/dist/hooks/computeOverflow.js +213 -0
  285. package/dist/hooks/index.d.ts +1 -0
  286. package/dist/hooks/index.d.ts.map +1 -1
  287. package/dist/hooks/index.js +2 -1
  288. package/dist/hooks/isRtlElement.d.ts +18 -0
  289. package/dist/hooks/isRtlElement.d.ts.map +1 -0
  290. package/dist/hooks/isRtlElement.js +27 -0
  291. package/dist/hooks/useDevWarning.d.ts +18 -0
  292. package/dist/hooks/useDevWarning.d.ts.map +1 -0
  293. package/dist/hooks/useDevWarning.js +49 -0
  294. package/dist/hooks/useGridFocus.d.ts +6 -1
  295. package/dist/hooks/useGridFocus.d.ts.map +1 -1
  296. package/dist/hooks/useGridFocus.js +12 -9
  297. package/dist/hooks/useListFocus.d.ts +6 -1
  298. package/dist/hooks/useListFocus.d.ts.map +1 -1
  299. package/dist/hooks/useListFocus.js +12 -4
  300. package/dist/hooks/useOverflow.d.ts +19 -0
  301. package/dist/hooks/useOverflow.d.ts.map +1 -1
  302. package/dist/hooks/useOverflow.js +36 -25
  303. package/dist/i18n/InternationalizationContext.d.ts +1 -0
  304. package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
  305. package/dist/i18n/InternationalizationContext.js +3 -0
  306. package/dist/i18n/InternationalizationProvider.d.ts +8 -1
  307. package/dist/i18n/InternationalizationProvider.d.ts.map +1 -1
  308. package/dist/i18n/InternationalizationProvider.js +5 -1
  309. package/dist/i18n/getLocaleDirection.d.ts +18 -0
  310. package/dist/i18n/getLocaleDirection.d.ts.map +1 -0
  311. package/dist/i18n/getLocaleDirection.js +29 -0
  312. package/dist/i18n/index.d.ts +6 -0
  313. package/dist/i18n/index.d.ts.map +1 -1
  314. package/dist/i18n/index.js +7 -1
  315. package/dist/i18n/resolve.d.ts.map +1 -1
  316. package/dist/i18n/resolve.js +7 -18
  317. package/dist/i18n/useDirection.d.ts +2 -0
  318. package/dist/i18n/useDirection.d.ts.map +1 -0
  319. package/dist/i18n/useDirection.js +27 -0
  320. package/dist/theme/Theme.d.ts.map +1 -1
  321. package/dist/theme/Theme.js +2 -7
  322. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  323. package/dist/theme/derivedVarRegistry.js +13 -0
  324. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  325. package/dist/theme/generateThemeRules.js +51 -0
  326. package/dist/theme/hct.d.ts +0 -1
  327. package/dist/theme/hct.d.ts.map +1 -1
  328. package/dist/theme/hct.js +1 -1
  329. package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
  330. package/dist/theme/syntax/defineSyntaxTheme.js +2 -1
  331. package/dist/utils/devWarning.d.ts +42 -0
  332. package/dist/utils/devWarning.d.ts.map +1 -0
  333. package/dist/utils/devWarning.js +88 -0
  334. package/dist/utils/index.d.ts +3 -1
  335. package/dist/utils/index.d.ts.map +1 -1
  336. package/dist/utils/index.js +3 -2
  337. package/dist/utils/plainDate.d.ts +30 -0
  338. package/dist/utils/plainDate.d.ts.map +1 -1
  339. package/dist/utils/plainDate.js +53 -0
  340. package/dist/utils/types.d.ts +13 -0
  341. package/dist/utils/types.d.ts.map +1 -1
  342. package/locales/en.json +32 -0
  343. package/locales/fr-FR.json +14 -0
  344. package/locales/pseudo.json +24 -0
  345. package/package.json +1 -1
  346. package/src/AppShell/AppShell.tsx +6 -0
  347. package/src/AppShell/AppShellMobileContext.tsx +8 -0
  348. package/src/AppShell/useAppShellMobile.doc.mjs +8 -0
  349. package/src/Avatar/Avatar.doc.mjs +60 -3
  350. package/src/Avatar/Avatar.test.tsx +281 -1
  351. package/src/Avatar/Avatar.tsx +324 -64
  352. package/src/Avatar/AvatarStatusDot.doc.mjs +23 -12
  353. package/src/Avatar/AvatarStatusDot.test.tsx +284 -0
  354. package/src/Avatar/AvatarStatusDot.tsx +178 -21
  355. package/src/AvatarGroup/AvatarGroup.doc.mjs +2 -0
  356. package/src/AvatarGroup/AvatarGroup.test.tsx +125 -0
  357. package/src/AvatarGroup/AvatarGroup.tsx +58 -3
  358. package/src/AvatarGroup/AvatarGroupOverflow.tsx +1 -0
  359. package/src/Banner/Banner.doc.mjs +9 -0
  360. package/src/Banner/Banner.test.tsx +30 -0
  361. package/src/Banner/Banner.tsx +53 -9
  362. package/src/Breadcrumbs/BreadcrumbItem.doc.mjs +26 -0
  363. package/src/Breadcrumbs/BreadcrumbItem.tsx +333 -14
  364. package/src/Breadcrumbs/Breadcrumbs.doc.mjs +2 -0
  365. package/src/Breadcrumbs/Breadcrumbs.test.tsx +284 -2
  366. package/src/Breadcrumbs/index.ts +27 -0
  367. package/src/Button/Button.doc.mjs +9 -4
  368. package/src/Button/Button.test.tsx +49 -0
  369. package/src/Button/Button.tsx +27 -3
  370. package/src/ButtonGroup/ButtonGroup.doc.mjs +3 -0
  371. package/src/ButtonGroup/ButtonGroup.test.tsx +37 -0
  372. package/src/ButtonGroup/ButtonGroup.tsx +32 -0
  373. package/src/Calendar/Calendar.doc.mjs +12 -1
  374. package/src/Calendar/Calendar.test.tsx +193 -0
  375. package/src/Calendar/Calendar.tsx +38 -16
  376. package/src/Calendar/dayCellUtils.test.ts +5 -0
  377. package/src/Calendar/dayCellUtils.ts +1 -1
  378. package/src/Card/Card.doc.mjs +9 -0
  379. package/src/Card/Card.test.tsx +21 -3
  380. package/src/Card/Card.tsx +35 -2
  381. package/src/Carousel/Carousel.doc.mjs +1 -0
  382. package/src/Carousel/Carousel.test.tsx +53 -0
  383. package/src/Carousel/Carousel.tsx +34 -4
  384. package/src/Chat/ChatComposer.doc.mjs +12 -3
  385. package/src/Chat/ChatComposer.test.tsx +115 -0
  386. package/src/Chat/ChatComposer.tsx +73 -9
  387. package/src/Chat/ChatComposerDrawer.test.tsx +61 -0
  388. package/src/Chat/ChatComposerDrawer.tsx +11 -1
  389. package/src/Chat/ChatComposerInput.doc.mjs +7 -0
  390. package/src/Chat/ChatComposerInput.test.tsx +103 -0
  391. package/src/Chat/ChatComposerInput.tsx +51 -5
  392. package/src/Chat/ChatContext.tsx +37 -10
  393. package/src/Chat/ChatDictationButton.tsx +4 -1
  394. package/src/Chat/ChatLayout.tsx +7 -5
  395. package/src/Chat/ChatLayoutScrollButton.tsx +7 -2
  396. package/src/Chat/ChatMessageMetadata.tsx +1 -2
  397. package/src/Chat/ChatSendButton.tsx +11 -1
  398. package/src/Chat/ChatToolCalls.test.tsx +64 -0
  399. package/src/Chat/ChatToolCalls.tsx +25 -4
  400. package/src/Chat/index.ts +5 -1
  401. package/src/Chat/useChatStreamScroll.test.tsx +49 -0
  402. package/src/Chat/useChatStreamScroll.ts +27 -10
  403. package/src/Chat/useSpeechRecognition.ts +1 -1
  404. package/src/Chat/useTriggerMenu.tsx +4 -2
  405. package/src/CheckboxInput/CheckboxInput.test.tsx +32 -1
  406. package/src/CheckboxInput/CheckboxInput.tsx +1 -1
  407. package/src/CheckboxList/CheckboxList.test.tsx +81 -3
  408. package/src/CheckboxList/CheckboxListItem.doc.mjs +33 -5
  409. package/src/CheckboxList/CheckboxListItem.tsx +41 -5
  410. package/src/Citation/Citation.doc.mjs +4 -3
  411. package/src/Citation/Citation.test.tsx +97 -0
  412. package/src/Citation/Citation.tsx +43 -11
  413. package/src/ClickableCard/ClickableCard.doc.mjs +2 -0
  414. package/src/ClickableCard/ClickableCard.test.tsx +34 -0
  415. package/src/ClickableCard/ClickableCard.tsx +13 -3
  416. package/src/CodeBlock/CodeBlock.test.tsx +56 -0
  417. package/src/CodeBlock/CodeBlock.tsx +10 -2
  418. package/src/CommandPalette/CommandPalette.doc.mjs +1 -0
  419. package/src/CommandPalette/CommandPalette.tsx +3 -1
  420. package/src/CommandPalette/CommandPaletteGroup.test.tsx +71 -0
  421. package/src/CommandPalette/CommandPaletteGroup.tsx +12 -1
  422. package/src/CommandPalette/CommandPaletteInput.doc.mjs +26 -9
  423. package/src/CommandPalette/CommandPaletteInput.test.tsx +34 -0
  424. package/src/CommandPalette/CommandPaletteInput.tsx +11 -0
  425. package/src/CommandPalette/CommandPaletteItem.tsx +1 -1
  426. package/src/ContextMenu/ContextMenu.test.tsx +193 -0
  427. package/src/DateInput/DateInput.doc.mjs +31 -0
  428. package/src/DateInput/DateInput.test.tsx +131 -0
  429. package/src/DateInput/DateInput.tsx +78 -3
  430. package/src/DateInput/index.ts +1 -0
  431. package/src/DateRangeInput/DateRangeInput.doc.mjs +8 -0
  432. package/src/DateRangeInput/DateRangeInput.test.tsx +23 -0
  433. package/src/DateRangeInput/DateRangeInput.tsx +13 -3
  434. package/src/DateTimeInput/DateTimeInput.doc.mjs +16 -1
  435. package/src/DateTimeInput/DateTimeInput.test.tsx +147 -0
  436. package/src/DateTimeInput/DateTimeInput.tsx +42 -18
  437. package/src/Dialog/Dialog.doc.mjs +2 -2
  438. package/src/Dialog/Dialog.test.tsx +77 -4
  439. package/src/Dialog/Dialog.tsx +89 -5
  440. package/src/Dialog/DialogContext.ts +9 -2
  441. package/src/Dialog/DialogHeader.doc.mjs +2 -2
  442. package/src/Dialog/DialogHeader.tsx +9 -2
  443. package/src/Divider/Divider.tsx +2 -2
  444. package/src/DropdownMenu/DropdownMenu.test.tsx +169 -0
  445. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +1 -2
  446. package/src/DropdownMenu/DropdownMenuItem.tsx +1 -1
  447. package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +1 -2
  448. package/src/Field/Field.test.tsx +57 -12
  449. package/src/Field/Field.tsx +6 -5
  450. package/src/Field/FieldLabel.tsx +9 -1
  451. package/src/FieldStatus/FieldStatus.doc.mjs +17 -5
  452. package/src/FieldStatus/FieldStatus.test.tsx +150 -40
  453. package/src/FieldStatus/FieldStatus.tsx +63 -5
  454. package/src/FileInput/FileInput.doc.mjs +17 -2
  455. package/src/FileInput/FileInput.test.tsx +135 -12
  456. package/src/FileInput/FileInput.tsx +53 -28
  457. package/src/HoverCard/HoverCard.doc.mjs +13 -0
  458. package/src/HoverCard/HoverCard.test.tsx +21 -2
  459. package/src/HoverCard/HoverCard.tsx +11 -0
  460. package/src/HoverCard/useHoverCard.doc.mjs +2 -0
  461. package/src/HoverCard/useHoverCard.tsx +24 -2
  462. package/src/Icon/Icon.doc.mjs +3 -2
  463. package/src/Icon/Icon.tsx +7 -2
  464. package/src/IconButton/IconButton.doc.mjs +8 -0
  465. package/src/IconButton/IconButton.test.tsx +15 -18
  466. package/src/Item/Item.test.tsx +40 -18
  467. package/src/Item/Item.tsx +38 -2
  468. package/src/Layer/useLayer.tsx +8 -0
  469. package/src/Lightbox/Lightbox.tsx +4 -4
  470. package/src/List/List.test.tsx +9 -8
  471. package/src/MobileNav/MobileNav.tsx +12 -1
  472. package/src/MobileNav/MobileNavToggle.test.tsx +116 -0
  473. package/src/MobileNav/MobileNavToggle.tsx +9 -1
  474. package/src/MultiSelector/MultiSelector.doc.mjs +13 -2
  475. package/src/MultiSelector/MultiSelector.test.tsx +195 -3
  476. package/src/MultiSelector/MultiSelector.tsx +65 -12
  477. package/src/MultiSelector/hooks.ts +17 -0
  478. package/src/NavMenu/NavHeadingMenuItem.tsx +1 -1
  479. package/src/NumberInput/NumberInput.doc.mjs +15 -0
  480. package/src/NumberInput/NumberInput.test.tsx +82 -7
  481. package/src/NumberInput/NumberInput.tsx +21 -4
  482. package/src/Outline/Outline.tsx +0 -1
  483. package/src/OverflowList/OverflowList.doc.mjs +39 -6
  484. package/src/OverflowList/OverflowList.test.tsx +111 -0
  485. package/src/OverflowList/OverflowList.tsx +48 -8
  486. package/src/Pagination/Pagination.tsx +11 -3
  487. package/src/Popover/Popover.tsx +7 -4
  488. package/src/Popover/usePopover.tsx +9 -13
  489. package/src/PowerSearch/PowerSearch.doc.mjs +29 -0
  490. package/src/PowerSearch/PowerSearch.test.tsx +43 -0
  491. package/src/PowerSearch/PowerSearch.tsx +10 -1
  492. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +67 -0
  493. package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
  494. package/src/PowerSearch/usePowerSearchSource.ts +1 -2
  495. package/src/SelectableCard/SelectableCard.doc.mjs +2 -0
  496. package/src/SelectableCard/SelectableCard.test.tsx +46 -4
  497. package/src/SelectableCard/SelectableCard.tsx +29 -16
  498. package/src/Selector/Selector.doc.mjs +7 -0
  499. package/src/Selector/Selector.test.tsx +92 -0
  500. package/src/Selector/Selector.tsx +16 -2
  501. package/src/Selector/hooks.ts +17 -0
  502. package/src/SideNav/SideNav.test.tsx +119 -3
  503. package/src/SideNav/SideNavCollapseButton.tsx +6 -1
  504. package/src/SideNav/SideNavHeading.tsx +24 -8
  505. package/src/SideNav/SideNavItem.tsx +5 -1
  506. package/src/Slider/Slider.test.tsx +75 -2
  507. package/src/Slider/Slider.tsx +21 -5
  508. package/src/Switch/Switch.test.tsx +32 -1
  509. package/src/TabList/Tab.tsx +2 -2
  510. package/src/TabList/TabMenu.tsx +2 -2
  511. package/src/Table/BaseTable.tsx +3 -5
  512. package/src/Table/TableCell.tsx +3 -3
  513. package/src/Table/TableHeaderCell.tsx +3 -3
  514. package/src/Table/index.ts +5 -0
  515. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +6 -2
  516. package/src/Table/plugins/rowStatus/index.ts +7 -0
  517. package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +138 -0
  518. package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +189 -0
  519. package/src/Table/plugins/selection/useTableSelection.test.tsx +16 -0
  520. package/src/Table/plugins/selection/useTableSelection.tsx +19 -1
  521. package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +1 -1
  522. package/src/Table/plugins/tree/useTableTreeData.test.tsx +135 -3
  523. package/src/Table/plugins/tree/useTableTreeData.tsx +128 -1
  524. package/src/Table/plugins/tree/useTableTreeState.test.tsx +104 -0
  525. package/src/Table/plugins/tree/useTableTreeState.tsx +46 -2
  526. package/src/Table/useBaseTablePlugins.ts +10 -6
  527. package/src/Table/useTableRowStatus.doc.mjs +30 -0
  528. package/src/Table/useTableSelection.doc.mjs +65 -14
  529. package/src/Table/useTableTreeData.doc.mjs +29 -0
  530. package/src/Table/useTableTreeState.doc.mjs +2 -2
  531. package/src/TextArea/TextArea.doc.mjs +15 -0
  532. package/src/TextArea/TextArea.test.tsx +32 -1
  533. package/src/TextArea/TextArea.tsx +10 -0
  534. package/src/TextInput/TextInput.doc.mjs +15 -0
  535. package/src/TextInput/TextInput.test.tsx +44 -0
  536. package/src/TextInput/TextInput.tsx +13 -1
  537. package/src/Thumbnail/Thumbnail.doc.mjs +19 -10
  538. package/src/Thumbnail/Thumbnail.test.tsx +98 -7
  539. package/src/Thumbnail/Thumbnail.tsx +142 -42
  540. package/src/Thumbnail/thumbnail.markers.stylex.ts +12 -0
  541. package/src/TimeInput/TimeInput.doc.mjs +15 -0
  542. package/src/TimeInput/TimeInput.test.tsx +99 -1
  543. package/src/TimeInput/TimeInput.tsx +29 -6
  544. package/src/Timestamp/Timestamp.doc.mjs +4 -4
  545. package/src/Timestamp/Timestamp.test.tsx +32 -0
  546. package/src/Timestamp/Timestamp.tsx +28 -8
  547. package/src/Toast/useToast.tsx +7 -8
  548. package/src/ToggleButton/ToggleButtonGroup.tsx +1 -1
  549. package/src/Tokenizer/Tokenizer.doc.mjs +30 -1
  550. package/src/Tokenizer/Tokenizer.test.tsx +206 -2
  551. package/src/Tokenizer/Tokenizer.tsx +26 -4
  552. package/src/TopNav/TopNav.test.tsx +125 -1
  553. package/src/TopNav/TopNavHeading.tsx +24 -8
  554. package/src/TopNav/TopNavMegaMenu.test.tsx +149 -1
  555. package/src/TopNav/TopNavMegaMenu.tsx +10 -4
  556. package/src/TopNav/TopNavMegaMenuFeaturedCard.doc.mjs +3 -3
  557. package/src/TopNav/TopNavMegaMenuFeaturedCard.tsx +19 -2
  558. package/src/TopNav/TopNavMenu.test.tsx +118 -4
  559. package/src/TopNav/TopNavMenu.tsx +83 -5
  560. package/src/TreeList/TreeList.doc.mjs +22 -0
  561. package/src/TreeList/TreeList.test.tsx +232 -0
  562. package/src/TreeList/TreeList.tsx +18 -2
  563. package/src/TreeList/TreeListBranches.tsx +16 -2
  564. package/src/TreeList/TreeListItem.tsx +52 -12
  565. package/src/TreeList/TreeListTypes.ts +27 -1
  566. package/src/TreeList/index.ts +2 -2
  567. package/src/Typeahead/BaseTypeahead.tsx +30 -15
  568. package/src/Typeahead/Typeahead.doc.mjs +7 -0
  569. package/src/Typeahead/Typeahead.test.tsx +50 -0
  570. package/src/Typeahead/Typeahead.tsx +10 -0
  571. package/src/authoring/doc.ts +1 -3
  572. package/src/docs-types.ts +5 -0
  573. package/src/hooks/computeOverflow.test.ts +422 -0
  574. package/src/hooks/computeOverflow.ts +290 -0
  575. package/src/hooks/index.ts +2 -0
  576. package/src/hooks/isRtlElement.ts +27 -0
  577. package/src/hooks/useDevWarning.test.tsx +45 -0
  578. package/src/hooks/useDevWarning.ts +54 -0
  579. package/src/hooks/useGridFocus.doc.mjs +3 -3
  580. package/src/hooks/useGridFocus.test.tsx +41 -0
  581. package/src/hooks/useGridFocus.ts +21 -10
  582. package/src/hooks/useListFocus.doc.mjs +3 -3
  583. package/src/hooks/useListFocus.test.tsx +67 -1
  584. package/src/hooks/useListFocus.ts +21 -5
  585. package/src/hooks/useOverflow.test.ts +96 -4
  586. package/src/hooks/useOverflow.ts +74 -33
  587. package/src/i18n/InternationalizationContext.ts +4 -0
  588. package/src/i18n/InternationalizationProvider.doc.mjs +99 -2
  589. package/src/i18n/InternationalizationProvider.tsx +12 -2
  590. package/src/i18n/__tests__/getLocaleDirection.test.ts +40 -0
  591. package/src/i18n/__tests__/useDirection.test.tsx +66 -0
  592. package/src/i18n/getLocaleDirection.ts +29 -0
  593. package/src/i18n/index.ts +6 -0
  594. package/src/i18n/resolve.ts +9 -21
  595. package/src/i18n/useDirection.ts +29 -0
  596. package/src/reset.css +0 -26
  597. package/src/reset.test.ts +60 -0
  598. package/src/theme/Theme.tsx +11 -14
  599. package/src/theme/derivedVarRegistry.test.ts +1 -0
  600. package/src/theme/derivedVarRegistry.ts +6 -0
  601. package/src/theme/generateThemeRules.test.ts +107 -0
  602. package/src/theme/generateThemeRules.ts +56 -0
  603. package/src/theme/hct.ts +1 -1
  604. package/src/theme/syntax/defineSyntaxTheme.ts +5 -6
  605. package/src/utils/devWarning.test.ts +56 -0
  606. package/src/utils/devWarning.ts +102 -0
  607. package/src/utils/index.ts +6 -0
  608. package/src/utils/plainDate.test.ts +22 -0
  609. package/src/utils/plainDate.ts +61 -0
  610. package/src/utils/types.ts +14 -0
@@ -13,6 +13,7 @@ import {describe, it, expect, vi, beforeEach} from 'vitest';
13
13
  import {render, screen, fireEvent, waitFor} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {TestIcon} from '../__tests__/TestIcon';
16
+ import {InputGroup} from '../InputGroup';
16
17
  import {TextInput} from './TextInput';
17
18
 
18
19
  // Mock showPopover/hidePopover since jsdom does not implement them. Used by the
@@ -229,6 +230,26 @@ describe('TextInput', () => {
229
230
  expect(screen.getByText('Invalid email address')).toBeInTheDocument();
230
231
  });
231
232
 
233
+ it('has no dangling aria-describedby ids inside InputGroup (WCAG 1.3.1)', () => {
234
+ // Inside an InputGroup no Field renders, so the status message element
235
+ // does not exist; aria-describedby must not reference its id.
236
+ render(
237
+ <InputGroup label="Contact">
238
+ <TextInput
239
+ label="Email"
240
+ value=""
241
+ onChange={() => {}}
242
+ status={{type: 'error', message: 'Invalid email address'}}
243
+ />
244
+ </InputGroup>,
245
+ );
246
+ const input = screen.getByRole('textbox');
247
+ const describedBy = input.getAttribute('aria-describedby') ?? '';
248
+ for (const idToken of describedBy.split(/\s+/).filter(Boolean)) {
249
+ expect(document.getElementById(idToken)).not.toBeNull();
250
+ }
251
+ });
252
+
232
253
  it('does not render status message when not provided', () => {
233
254
  render(
234
255
  <TextInput
@@ -659,3 +680,26 @@ describe('TextInput', () => {
659
680
  });
660
681
  });
661
682
  });
683
+
684
+
685
+ describe('TextInput statusVariant forwarding', () => {
686
+ it('defaults to attached (status renders with data-variant="attached")', () => {
687
+ const {container} = render(
688
+ <TextInput label="Email" value="" onChange={() => {}} status={{type: 'error', message: 'Invalid email'}} />,
689
+ );
690
+ expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
691
+ 'data-variant',
692
+ 'attached',
693
+ );
694
+ });
695
+
696
+ it('forwards statusVariant="detached" to the underlying Field status', () => {
697
+ const {container} = render(
698
+ <TextInput label="Email" value="" onChange={() => {}} status={{type: 'error', message: 'Invalid email'}} statusVariant="detached" />,
699
+ );
700
+ expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
701
+ 'data-variant',
702
+ 'detached',
703
+ );
704
+ });
705
+ });
@@ -44,6 +44,7 @@ import {
44
44
  inputStatusBorderStyles,
45
45
  inputStatusHoverShadowStyles,
46
46
  inputStatusFocusWithinStyles,
47
+ type FieldStatusVariant,
47
48
  } from '../Field';
48
49
  import {Icon, renderIconSlot, type IconType} from '../Icon';
49
50
  import {Spinner} from '../Spinner';
@@ -198,6 +199,13 @@ export interface TextInputProps extends Omit<
198
199
  * If message is provided, displays a floating message box below the input.
199
200
  */
200
201
  status?: InputStatus;
202
+ /**
203
+ * How the status message is placed relative to the input.
204
+ * - 'attached': message overlaps directly below the input (bordered treatment)
205
+ * - 'detached': message floats below as a separate element with spacing
206
+ * @default 'attached'
207
+ */
208
+ statusVariant?: FieldStatusVariant;
201
209
  /**
202
210
  * The size of the input.
203
211
  * - 'sm': Compact size (18px height)
@@ -280,6 +288,7 @@ export function TextInput({
280
288
  disabledMessage,
281
289
  startIcon,
282
290
  status,
291
+ statusVariant = 'attached',
283
292
  size: sizeProp,
284
293
  onChange,
285
294
  changeAction,
@@ -347,7 +356,9 @@ export function TextInput({
347
356
  inputLabelID,
348
357
  [
349
358
  description ? descriptionID : null,
350
- status?.message ? statusMessageID : null,
359
+ // The status message element is rendered by Field, which is skipped
360
+ // inside an InputGroup — only reference it when it actually exists.
361
+ !inputGroup && status?.message ? statusMessageID : null,
351
362
  showsDisabledMessage ? disabledMessageTooltip.describedBy : null,
352
363
  ],
353
364
  inputGroup,
@@ -495,6 +506,7 @@ export function TextInput({
495
506
  }
496
507
  : undefined
497
508
  }
509
+ statusVariant={statusVariant}
498
510
  labelTooltip={labelTooltip}
499
511
  width={width}>
500
512
  {inputWrapper}
@@ -16,7 +16,7 @@ export const docs = {
16
16
  {
17
17
  name: 'alt',
18
18
  type: 'string',
19
- description: 'Alt text for the image.',
19
+ description: 'Alt text for the image. When omitted, the image is explicitly decorative (alt="", role="presentation", aria-hidden) and hidden from screen readers; a dev warning fires if neither alt nor label is set.',
20
20
  },
21
21
  {
22
22
  name: 'label',
@@ -31,7 +31,7 @@ export const docs = {
31
31
  {
32
32
  name: 'onClick',
33
33
  type: '(e: React.MouseEvent) => void',
34
- description: 'Click handler. Adds button semantics and hover shadow.',
34
+ description: 'Click handler. Adds button semantics and a hover overlay.',
35
35
  },
36
36
  {
37
37
  name: 'isLoading',
@@ -45,6 +45,13 @@ export const docs = {
45
45
  description: 'Whether the thumbnail is disabled.',
46
46
  default: 'false',
47
47
  },
48
+ {
49
+ name: 'showRemoveOn',
50
+ type: "'always' | 'hover'",
51
+ description:
52
+ 'When the remove button is visible. `hover` (the default) reveals it on hover and on keyboard focus, and keeps it visible on any touch-capable device; `always` shows it at rest. Only applies when `onRemove` is set.',
53
+ default: "'hover'",
54
+ },
48
55
  {
49
56
  name: 'xstyle',
50
57
  type: 'StyleXStyles',
@@ -77,14 +84,14 @@ export const docs = {
77
84
  bestPractices: [
78
85
  {guidance: true, description: 'Always provide a label prop with the file name so the thumbnail and its remove button are accessible to screen readers and show a tooltip on hover.'},
79
86
  {guidance: true, description: 'Use isLoading without a src to show a skeleton during initial upload, and isLoading with a src to show a spinner overlay once a preview URL is available.'},
80
- {guidance: true, description: 'Pair onClick with a lightbox or detail view so users can inspect the full image; the thumbnail adds button semantics and a hover shadow automatically.'},
87
+ {guidance: true, description: 'Pair onClick with a lightbox or detail view so users can inspect the full image; the thumbnail adds button semantics and a hover overlay automatically.'},
81
88
  {guidance: false, description: "Don't use Thumbnail for non-image file types like PDFs or spreadsheets; use a file attachment component with an appropriate icon instead."},
82
89
  {guidance: false, description: "Don't omit alt text when a src is provided; screen readers need a description of the image content, not just the file name from label."},
83
90
  ],
84
91
  anatomy: [
85
92
  {name: 'Image', required: false, description: 'The preview image, displayed as a square with cover fit.'},
86
93
  {name: 'Placeholder', required: false, description: 'An image silhouette icon shown when no src is provided.'},
87
- {name: 'Remove button', required: false, description: 'An overlaid close button in the top-right corner. Appears when onRemove is set. Uses APCA luminance detection to stay visible on any image.'},
94
+ {name: 'Remove button', required: false, description: 'An overlaid close button in the top-right corner. Appears when onRemove is set. Sits on a fixed --color-overlay scrim with an --color-on-dark icon to stay visible on any image.'},
88
95
  {name: 'Upload overlay', required: false, description: 'A semi-transparent overlay with a spinner, shown when isLoading is true and a src preview is available.'},
89
96
  {name: 'Skeleton', required: false, description: 'A shimmer animation shown when isLoading is true and no src is set.'},
90
97
  ],
@@ -95,12 +102,13 @@ export const docs = {
95
102
  export const docsZh = {
96
103
  propDescriptions: {
97
104
  src: '\u56FE\u7247\u6E90 URL\u3002',
98
- alt: '\u56FE\u7247\u7684\u66FF\u4EE3\u6587\u672C\u3002',
105
+ alt: '\u56FE\u7247\u7684\u66FF\u4EE3\u6587\u672C\u3002\u7701\u7565\u65F6\u56FE\u7247\u4F1A\u88AB\u663E\u5F0F\u6807\u8BB0\u4E3A\u88C5\u9970\u6027\uFF08alt=""\u3001role="presentation"\u3001aria-hidden\uFF09\u5E76\u5BF9\u5C4F\u5E55\u9605\u8BFB\u5668\u9690\u85CF\uFF1B\u82E5 alt \u548C label \u5747\u672A\u8BBE\u7F6E\uFF0C\u5F00\u53D1\u73AF\u5883\u4F1A\u53D1\u51FA\u8B66\u544A\u3002',
99
106
  label: '\u65E0\u969C\u788D\u6807\u7B7E\uFF08\u5982\u6587\u4EF6\u540D\uFF09\u3002\u60AC\u505C\u65F6\u4EE5\u63D0\u793A\u5DE5\u5177\u663E\u793A\u3002',
100
107
  onRemove: '\u8986\u76D6\u5C42\u79FB\u9664\u6309\u94AE\u7684\u56DE\u8C03\u3002',
101
- onClick: '\u70B9\u51FB\u5904\u7406\u5668\u3002\u6DFB\u52A0\u6309\u94AE\u8BED\u4E49\u548C\u60AC\u505C\u9634\u5F71\u3002',
108
+ onClick: '\u70B9\u51FB\u5904\u7406\u5668\u3002\u6DFB\u52A0\u6309\u94AE\u8BED\u4E49\u548C\u60AC\u505C\u8986\u76D6\u5C42\u3002',
102
109
  isLoading: '\u663E\u793A\u9AA8\u67B6\u5C4F\uFF08\u65E0 src\uFF09\u6216\u4E0A\u4F20\u8986\u76D6\u5C42\uFF08\u6709 src\uFF09\u3002',
103
110
  isDisabled: '\u662F\u5426\u7981\u7528\u7F29\u7565\u56FE\u3002',
111
+ showRemoveOn: '\u4F55\u65F6\u663E\u793A\u79FB\u9664\u6309\u94AE\uFF1Ahover\uFF08\u9ED8\u8BA4\uFF0C\u60AC\u505C\u65F6\u4EE5\u53CA\u952E\u76D8\u805A\u7126\u65F6\u663E\u793A\uFF0C\u89E6\u6478\u8BBE\u5907\u4E0A\u4FDD\u6301\u53EF\u89C1\uFF09| always\uFF08\u59CB\u7EC8\u663E\u793A\uFF09\u3002\u4EC5\u5728\u8BBE\u7F6E onRemove \u65F6\u751F\u6548\u3002',
104
112
  xstyle: '\u7528\u4E8E\u5E03\u5C40\u81EA\u5B9A\u4E49\u7684 StyleX \u6837\u5F0F\u3002\u5FC5\u987B\u662F stylex.create() \u7684\u503C\uFF0C\u800C\u975E\u5185\u8054\u6837\u5F0F\u5BF9\u8C61\u3002',
105
113
  className: '\u6839\u5143\u7D20\u7684 CSS \u7C7B\u540D\u3002\u5EFA\u8BAE\u4F7F\u7528 xstyle\u3002',
106
114
  style: '\u6839\u5143\u7D20\u7684\u5185\u8054\u6837\u5F0F\u3002\u5EFA\u8BAE\u4F7F\u7528 xstyle\u3002',
@@ -112,7 +120,7 @@ export const docsZh = {
112
120
  bestPractices: [
113
121
  {guidance: true, description: '\u59CB\u7EC8\u63D0\u4F9B label \u5C5E\u6027\u5E76\u586B\u5199\u6587\u4EF6\u540D\uFF0C\u4EE5\u4FBF\u5C4F\u5E55\u9605\u8BFB\u5668\u80FD\u591F\u8BC6\u522B\u7F29\u7565\u56FE\u53CA\u5176\u79FB\u9664\u6309\u94AE\uFF0C\u5E76\u5728\u60AC\u505C\u65F6\u663E\u793A\u63D0\u793A\u3002'},
114
122
  {guidance: true, description: '\u4E0A\u4F20\u521D\u671F\u4F7F\u7528\u4E0D\u5E26 src \u7684 isLoading \u663E\u793A\u9AA8\u67B6\u5C4F\uFF1B\u83B7\u5F97\u9884\u89C8 URL \u540E\u4F7F\u7528\u5E26 src \u7684 isLoading \u663E\u793A\u65CB\u8F6C\u52A0\u8F7D\u8986\u76D6\u5C42\u3002'},
115
- {guidance: true, description: '\u5C06 onClick \u4E0E\u706F\u7BB1\u6216\u8BE6\u60C5\u89C6\u56FE\u914D\u5BF9\uFF0C\u4EE5\u4FBF\u7528\u6237\u67E5\u770B\u5B8C\u6574\u56FE\u7247\u2014\u2014\u7EC4\u4EF6\u4F1A\u81EA\u52A8\u6DFB\u52A0\u6309\u94AE\u8BED\u4E49\u548C\u60AC\u505C\u9634\u5F71\u3002'},
123
+ {guidance: true, description: '\u5C06 onClick \u4E0E\u706F\u7BB1\u6216\u8BE6\u60C5\u89C6\u56FE\u914D\u5BF9\uFF0C\u4EE5\u4FBF\u7528\u6237\u67E5\u770B\u5B8C\u6574\u56FE\u7247\u2014\u2014\u7EC4\u4EF6\u4F1A\u81EA\u52A8\u6DFB\u52A0\u6309\u94AE\u8BED\u4E49\u548C\u60AC\u505C\u8986\u76D6\u5C42\u3002'},
116
124
  {guidance: false, description: '\u5BF9 PDF \u6216\u7535\u5B50\u8868\u683C\u7B49\u975E\u56FE\u7247\u6587\u4EF6\u7C7B\u578B\u4F7F\u7528 Thumbnail\u2014\u2014\u5E94\u4F7F\u7528\u5E26\u6709\u76F8\u5E94\u56FE\u6807\u7684\u6587\u4EF6\u9644\u4EF6\u7EC4\u4EF6\u3002'},
117
125
  {guidance: false, description: '\u63D0\u4F9B src \u65F6\u7701\u7565 alt \u6587\u672C\u2014\u2014\u5C4F\u5E55\u9605\u8BFB\u5668\u9700\u8981\u56FE\u7247\u5185\u5BB9\u7684\u63CF\u8FF0\uFF0C\u800C\u4E0D\u4EC5\u662F label \u4E2D\u7684\u6587\u4EF6\u540D\u3002'},
118
126
  ],
@@ -128,19 +136,20 @@ export const docsDense = {
128
136
  bestPractices: [
129
137
  {guidance: true, description: 'Always set label (file name) for a11y; powers screen reader announce + hover tooltip.'},
130
138
  {guidance: true, description: 'isLoading w/o src \u2192 skeleton; isLoading w/ src \u2192 spinner overlay. Two distinct loading states.'},
131
- {guidance: true, description: 'onClick adds button semantics + hover shadow; pair with lightbox for full preview.'},
139
+ {guidance: true, description: 'onClick adds button semantics + hover overlay; pair with lightbox for full preview.'},
132
140
  {guidance: false, description: "Don't use for non-image files (PDF, xlsx); use file attachment component with icon instead."},
133
141
  {guidance: false, description: "Don't omit alt when src present; screen readers need image content description, not just label."},
134
142
  ],
135
143
  },
136
144
  propDescriptions: {
137
145
  src: 'Image source URL.',
138
- alt: 'Alt text for image.',
146
+ alt: 'Alt text for image. Omitted → explicitly decorative (alt="" + role="presentation" + aria-hidden); dev warn when src set with no alt and no label.',
139
147
  label: 'Accessible label (file name). Tooltip on hover, aria-label.',
140
148
  onRemove: '(e) => void. Overlaid remove button callback.',
141
- onClick: '(e) => void. Adds button semantics + hover shadow.',
149
+ onClick: '(e) => void. Adds button semantics + hover overlay.',
142
150
  isLoading: 'Skeleton (no src) or upload overlay (with src). Default: false.',
143
151
  isDisabled: 'Disabled state. Default: false.',
152
+ showRemoveOn: "When remove button shows: hover (default — reveal on hover/focus, stays visible on touch) | always. Only when onRemove set.",
144
153
  xstyle: 'stylex.create() for layout.',
145
154
  className: 'CSS class. Prefer xstyle.',
146
155
  style: 'Inline styles. Prefer xstyle.',
@@ -21,16 +21,19 @@ describe('Thumbnail', () => {
21
21
  });
22
22
 
23
23
  it('shows skeleton when isLoading with no src', () => {
24
- const {container} = render(
25
- <Thumbnail isLoading data-testid="thumb" />,
26
- );
24
+ const {container} = render(<Thumbnail isLoading data-testid="thumb" />);
27
25
  expect(container.querySelector('.astryx-skeleton')).toBeInTheDocument();
28
26
  expect(screen.queryByRole('img')).toBeNull();
29
27
  });
30
28
 
31
29
  it('shows image with upload overlay when isLoading with src', () => {
32
30
  render(
33
- <Thumbnail src="/local.jpg" alt="Uploading" isLoading data-testid="thumb" />,
31
+ <Thumbnail
32
+ src="/local.jpg"
33
+ alt="Uploading"
34
+ isLoading
35
+ data-testid="thumb"
36
+ />,
34
37
  );
35
38
  const img = screen.getByRole('img');
36
39
  expect(img).toHaveAttribute('src', '/local.jpg');
@@ -75,6 +78,40 @@ describe('Thumbnail', () => {
75
78
  ).toBeInTheDocument();
76
79
  });
77
80
 
81
+ it('marks the image as explicitly decorative when no alt is provided', () => {
82
+ render(
83
+ <Thumbnail src="/photo.jpg" label="photo.png" data-testid="thumb" />,
84
+ );
85
+ const img = screen.getByTestId('thumb').querySelector('img');
86
+ expect(img).toHaveAttribute('alt', '');
87
+ expect(img).toHaveAttribute('role', 'presentation');
88
+ expect(img).toHaveAttribute('aria-hidden', 'true');
89
+ // The image must not be exposed to assistive technology as a nameless img.
90
+ expect(screen.queryByRole('img')).toBeNull();
91
+ });
92
+
93
+ it('exposes the image with normal img semantics when alt is provided', () => {
94
+ render(<Thumbnail src="/photo.jpg" alt="Vacation photo" />);
95
+ const img = screen.getByRole('img', {name: 'Vacation photo'});
96
+ expect(img).not.toHaveAttribute('role');
97
+ expect(img).not.toHaveAttribute('aria-hidden');
98
+ });
99
+
100
+ it('warns once when src is set with no alt and no label', () => {
101
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
102
+ render(<Thumbnail src="/photo.jpg" />);
103
+ expect(warnSpy).toHaveBeenCalledTimes(1);
104
+ expect(warnSpy.mock.calls[0][0]).toContain('Thumbnail');
105
+ warnSpy.mockRestore();
106
+ });
107
+
108
+ it('does not warn when the thumbnail is named via label or alt', () => {
109
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
110
+ render(<Thumbnail src="/a.jpg" label="a.png" />);
111
+ render(<Thumbnail src="/b.jpg" alt="Photo b" />);
112
+ expect(warnSpy).not.toHaveBeenCalled();
113
+ warnSpy.mockRestore();
114
+ });
78
115
 
79
116
  it('label is shown via tooltip, not as inline text', () => {
80
117
  render(<Thumbnail label="photo.png" data-testid="thumb" />);
@@ -117,9 +154,7 @@ describe('Thumbnail', () => {
117
154
 
118
155
  it('is not interactive when isLoading', () => {
119
156
  const onClick = vi.fn();
120
- render(
121
- <Thumbnail src="/img.jpg" alt="Test" onClick={onClick} isLoading />,
122
- );
157
+ render(<Thumbnail src="/img.jpg" alt="Test" onClick={onClick} isLoading />);
123
158
  expect(screen.queryByRole('button')).toBeNull();
124
159
  });
125
160
 
@@ -128,4 +163,60 @@ describe('Thumbnail', () => {
128
163
  render(<Thumbnail ref={ref} data-testid="thumb" />);
129
164
  expect(ref).toHaveBeenCalled();
130
165
  });
166
+
167
+ describe('showRemoveOn', () => {
168
+ // The reveal style lives on the slot <div> that wraps the remove button
169
+ // (ancestor-marker styles can't ride on a child component's xstyle prop),
170
+ // so assert on the button's parent element.
171
+ const removeSlotClass = (
172
+ showRemoveOn?: 'always' | 'hover',
173
+ ): {className: string; unmount: () => void} => {
174
+ const view = render(
175
+ <Thumbnail
176
+ label="file.png"
177
+ onRemove={vi.fn()}
178
+ showRemoveOn={showRemoveOn}
179
+ />,
180
+ );
181
+ const slot = screen.getByRole('button', {
182
+ name: 'Remove file.png',
183
+ }).parentElement!;
184
+ return {className: slot.className, unmount: view.unmount};
185
+ };
186
+
187
+ it('renders the remove button in the DOM even when showRemoveOn="hover"', () => {
188
+ // Hover reveal is CSS-only (opacity) — the button must stay mounted so
189
+ // it remains reachable by keyboard and assistive tech.
190
+ render(
191
+ <Thumbnail label="file.png" onRemove={vi.fn()} showRemoveOn="hover" />,
192
+ );
193
+ expect(
194
+ screen.getByRole('button', {name: 'Remove file.png'}),
195
+ ).toBeInTheDocument();
196
+ });
197
+
198
+ it('applies a distinct slot class when showRemoveOn="hover" vs "always"', () => {
199
+ const always = removeSlotClass('always');
200
+ always.unmount();
201
+ const hover = removeSlotClass('hover');
202
+ expect(hover.className).not.toBe(always.className);
203
+ });
204
+
205
+ it('defaults to "hover" (same slot class as an explicit hover)', () => {
206
+ const def = removeSlotClass(undefined);
207
+ def.unmount();
208
+ const hover = removeSlotClass('hover');
209
+ expect(def.className).toBe(hover.className);
210
+ });
211
+
212
+ it('still fires onRemove when revealed on hover', async () => {
213
+ const user = userEvent.setup();
214
+ const onRemove = vi.fn();
215
+ render(
216
+ <Thumbnail label="file.png" onRemove={onRemove} showRemoveOn="hover" />,
217
+ );
218
+ await user.click(screen.getByRole('button', {name: 'Remove file.png'}));
219
+ expect(onRemove).toHaveBeenCalledOnce();
220
+ });
221
+ });
131
222
  });
@@ -4,14 +4,18 @@
4
4
 
5
5
  /**
6
6
  * @file Thumbnail.tsx
7
- * @input Uses React, stylex, Button, Skeleton, Spinner, MediaTheme, useImageMode
7
+ * @input Uses React, stylex, Button, Skeleton, Spinner
8
8
  * @output Exports Thumbnail component, ThumbnailProps
9
9
  * @position Core implementation; consumed by index.ts
10
10
  *
11
11
  * Square preview card for image attachments. Shows a skeleton shimmer while
12
12
  * the image loads, the image on success, or a placeholder on failure.
13
- * Uses useImageMode (APCA) to detect image luminance so the overlaid
14
- * remove button always has sufficient contrast.
13
+ * The overlaid remove button sits on a fixed --color-overlay scrim with an
14
+ * --color-on-dark icon so it always has sufficient contrast against the image.
15
+ *
16
+ * Images without `alt` are explicitly decorative (alt="" +
17
+ * role="presentation" + aria-hidden, matching Avatar). A dev-time warning
18
+ * fires once when `src` is set with no `alt` and no other name source.
15
19
  *
16
20
  * SYNC: When modified, update these files to stay in sync:
17
21
  * - /packages/core/src/Thumbnail/Thumbnail.doc.mjs
@@ -26,7 +30,6 @@ import {
26
30
  colorVars,
27
31
  radiusVars,
28
32
  spacingVars,
29
- shadowVars,
30
33
  durationVars,
31
34
  easeVars,
32
35
  } from '../theme/tokens.stylex';
@@ -35,15 +38,12 @@ import {Icon} from '../Icon';
35
38
  import {Skeleton} from '../Skeleton';
36
39
  import {Spinner} from '../Spinner';
37
40
  import {Tooltip} from '../Tooltip/Tooltip';
38
- import {MediaTheme} from '../theme/MediaTheme';
39
- import {useImageMode} from '../hooks/useImageMode';
41
+ import {useDevWarning} from '../hooks/useDevWarning';
40
42
  import type {BaseProps} from '../BaseProps';
41
43
  import {mergeProps} from '../utils';
42
44
  import {themeProps} from '../utils/themeProps';
43
45
  import {useTranslator} from '../i18n';
44
-
45
- /** Sample the region behind the remove button (20px button, 4px inset, in 64px container). */
46
- const BUTTON_REGION = {x: 0.5, y: 0.06, width: 0.44, height: 0.44};
46
+ import {thumbnailScope} from './thumbnail.markers.stylex';
47
47
 
48
48
  export interface ThumbnailProps extends BaseProps<HTMLDivElement> {
49
49
  /** Ref forwarded to the root element */
@@ -54,7 +54,14 @@ export interface ThumbnailProps extends BaseProps<HTMLDivElement> {
54
54
  */
55
55
  src?: string;
56
56
  /**
57
- * Alt text for the image. Required for accessibility when `src` is provided.
57
+ * Alt text for the image.
58
+ *
59
+ * When omitted, the image is explicitly decorative (`alt=""` +
60
+ * `role="presentation"` + `aria-hidden`, matching Avatar) and hidden from
61
+ * assistive technology. Provide `alt` whenever the image conveys content;
62
+ * without it, screen reader users only hear the `label` (file name), or a
63
+ * generic "thumbnail" if `label` is also missing — which triggers a
64
+ * dev-time warning.
58
65
  */
59
66
  alt?: string;
60
67
  /**
@@ -90,6 +97,17 @@ export interface ThumbnailProps extends BaseProps<HTMLDivElement> {
90
97
  * @default false
91
98
  */
92
99
  isDisabled?: boolean;
100
+ /**
101
+ * When the remove button is visible.
102
+ * - `'hover'` — the button is revealed on hover, and on keyboard focus so
103
+ * it stays reachable. On any touch-capable device it stays visible.
104
+ * This is the default.
105
+ * - `'always'` — the button is always shown.
106
+ *
107
+ * Only has an effect when `onRemove` is set.
108
+ * @default 'hover'
109
+ */
110
+ showRemoveOn?: 'always' | 'hover';
93
111
  /**
94
112
  * Test ID for testing frameworks.
95
113
  */
@@ -140,22 +158,6 @@ const styles = stylex.create({
140
158
  },
141
159
  interactive: {
142
160
  cursor: 'pointer',
143
- transitionProperty: 'opacity, box-shadow',
144
- transitionDuration: durationVars['--duration-fast'],
145
- transitionTimingFunction: easeVars['--ease-standard'],
146
- boxShadow: {
147
- default: 'none',
148
- ':hover': {
149
- '@media (hover: hover)': shadowVars['--shadow-med'],
150
- },
151
- },
152
- opacity: {
153
- default: 1,
154
- ':hover': {
155
- '@media (hover: hover)': 0.85,
156
- },
157
- ':active': 0.75,
158
- },
159
161
  outline: {
160
162
  default: null,
161
163
  ':has(:focus-visible)': `2px solid ${colorVars['--color-accent']}`,
@@ -165,6 +167,33 @@ const styles = stylex.create({
165
167
  ':has(:focus-visible)': '2px',
166
168
  },
167
169
  },
170
+ // Hover/pressed overlay — the exact same treatment as ClickableCard and
171
+ // SelectableCard. A transparent `::after` tints on hover/press instead of
172
+ // shifting shadow or opacity. Guarded by @media (hover: hover) so touch
173
+ // devices don't show a stuck hover state; active/pressed works everywhere.
174
+ overlay: {
175
+ '::after': {
176
+ content: '""',
177
+ position: 'absolute',
178
+ inset: 0,
179
+ borderRadius: 'inherit',
180
+ pointerEvents: 'none',
181
+ transitionProperty: 'background-color',
182
+ transitionDuration: durationVars['--duration-fast'],
183
+ transitionTimingFunction: easeVars['--ease-standard'],
184
+ backgroundColor: 'transparent',
185
+ },
186
+ ':active::after': {
187
+ backgroundColor: colorVars['--color-overlay-pressed'],
188
+ },
189
+ },
190
+ hoverOnPointer: {
191
+ '@media (hover: hover)': {
192
+ ':hover::after': {
193
+ backgroundColor: colorVars['--color-overlay-hover'],
194
+ },
195
+ },
196
+ },
168
197
  interactiveButton: {
169
198
  all: 'unset',
170
199
  cursor: 'pointer',
@@ -175,14 +204,22 @@ const styles = stylex.create({
175
204
  overflow: 'hidden',
176
205
  },
177
206
 
178
- removeButtonOverrides: {
207
+ removeSlot: {
179
208
  position: 'absolute',
180
209
  top: spacingVars['--spacing-1'],
181
- right: spacingVars['--spacing-1'],
210
+ insetInlineEnd: spacingVars['--spacing-1'],
182
211
  zIndex: 1,
212
+ lineHeight: 0,
213
+ },
214
+ removeButtonOverrides: {
183
215
  '--_button-radius': `calc(${radiusVars['--radius-element']} - ${spacingVars['--spacing-1']})`,
184
216
  height: 20,
185
217
  minWidth: 20,
218
+ // Fixed colors instead of luminance-adapting theme: a translucent scrim
219
+ // (--color-overlay) plus an --color-on-dark icon reads on any image
220
+ // without sampling pixel brightness.
221
+ backgroundColor: colorVars['--color-overlay'],
222
+ color: colorVars['--color-on-dark'],
186
223
  },
187
224
  disabled: {
188
225
  opacity: 0.5,
@@ -199,6 +236,28 @@ const styles = stylex.create({
199
236
  zIndex: 1,
200
237
  lineHeight: 0,
201
238
  },
239
+ // showRemoveOn="hover": the remove button is hidden at rest and revealed
240
+ // when the thumbnail is hovered or when focus enters it (keyboard). Only
241
+ // opacity is animated — the button stays mounted and focusable so tabbing
242
+ // to it triggers the :focus-within reveal. Any touch-capable device (incl.
243
+ // hybrid laptops that also report hover) keeps the button visible so the
244
+ // remove path is never gated behind an unavailable hover.
245
+ removeOnHover: {
246
+ opacity: {
247
+ default: 0,
248
+ [stylex.when.ancestor(':hover', thumbnailScope)]: {
249
+ '@media (hover: hover)': 1,
250
+ },
251
+ [stylex.when.ancestor(':focus-within', thumbnailScope)]: 1,
252
+ '@media (any-pointer: coarse)': 1,
253
+ },
254
+ transitionProperty: 'opacity',
255
+ transitionDuration: {
256
+ default: durationVars['--duration-fast'],
257
+ '@media (prefers-reduced-motion: reduce)': '0s',
258
+ },
259
+ transitionTimingFunction: easeVars['--ease-standard'],
260
+ },
202
261
  });
203
262
 
204
263
  // =============================================================================
@@ -227,15 +286,18 @@ function ImagePlaceholder() {
227
286
  *
228
287
  * Shows a skeleton shimmer while the image loads, the image on success, or
229
288
  * a placeholder icon on failure / when no src is provided. An overlaid
230
- * remove button appears when `onRemove` is set.
289
+ * remove button appears when `onRemove` is set — revealed on hover or
290
+ * keyboard focus by default (`showRemoveOn`), or always shown.
231
291
  *
232
- * Uses `useImageMode` (APCA) to detect image luminance and `MediaTheme`
233
- * to ensure the remove button always has sufficient contrast against the image.
292
+ * The remove button uses a fixed `--color-overlay` scrim with an
293
+ * `--color-on-dark` icon so it stays legible on any image without sampling
294
+ * the image's luminance.
234
295
  *
235
296
  * @example
236
297
  * ```
237
298
  * <Thumbnail src="/photo.jpg" alt="Vacation photo" onRemove={() => {}} />
238
299
  * <Thumbnail src="/preview.png" alt="Preview" onClick={() => {}} label="preview.png" />
300
+ * <Thumbnail src="/logo.png" alt="Logo" onRemove={() => {}} showRemoveOn="always" />
239
301
  * ```
240
302
  */
241
303
  export function Thumbnail({
@@ -246,6 +308,7 @@ export function Thumbnail({
246
308
  onClick,
247
309
  isLoading = false,
248
310
  isDisabled = false,
311
+ showRemoveOn = 'hover',
249
312
  xstyle,
250
313
  className,
251
314
  style,
@@ -254,7 +317,6 @@ export function Thumbnail({
254
317
  ...props
255
318
  }: ThumbnailProps) {
256
319
  const t = useTranslator();
257
- const imageMode = useImageMode(src, {region: BUTTON_REGION, fallback: null});
258
320
 
259
321
  const hasSrc = src != null;
260
322
  const showSkeleton = isLoading && !hasSrc;
@@ -262,13 +324,47 @@ export function Thumbnail({
262
324
  const showUploadOverlay = isLoading && hasSrc;
263
325
  const showPlaceholder = !isLoading && !hasSrc;
264
326
  const isInteractive = onClick != null && !isDisabled && !isLoading;
327
+ const hasRemove = onRemove != null && !isDisabled;
328
+ const isHoverReveal = hasRemove && showRemoveOn === 'hover';
265
329
  const accessibleName =
266
- label && alt ? `${label} — ${alt}` : (label ?? alt ?? 'thumbnail');
330
+ label && alt
331
+ ? `${label} — ${alt}`
332
+ : (label ?? alt ?? t('@astryx.thumbnail.fallbackName'));
333
+
334
+ // Without `alt`, the image is explicitly decorative rather than silently
335
+ // empty-alt, matching Avatar's handling of unnamed images.
336
+ const isImageDecorative = !alt;
337
+
338
+ // Dev-time guardrail: `src` with no `alt` hides the image from assistive
339
+ // technology. That is fine when the thumbnail is otherwise named — `label`
340
+ // (or a consumer-provided aria name) becomes the group's accessible name —
341
+ // but with no name source at all the group falls back to a generic
342
+ // "thumbnail".
343
+ const hasNameSource =
344
+ alt != null ||
345
+ label != null ||
346
+ props['aria-label'] != null ||
347
+ props['aria-labelledby'] != null;
348
+ useDevWarning(
349
+ 'Thumbnail',
350
+ '`src` is set without `alt` or `label`. The image is ' +
351
+ 'treated as decorative and hidden from assistive technology, and ' +
352
+ 'the thumbnail falls back to a generic "thumbnail" name. Pass ' +
353
+ '`alt` to describe the image content, or `label` (file name) to ' +
354
+ 'name the thumbnail.',
355
+ hasSrc && !hasNameSource,
356
+ );
267
357
 
268
358
  const imageContent = (
269
359
  <>
270
360
  {showImage && (
271
- <img src={src} alt={alt ?? ''} {...stylex.props(styles.image)} />
361
+ <img
362
+ src={src}
363
+ alt={alt ?? ''}
364
+ role={isImageDecorative ? 'presentation' : undefined}
365
+ aria-hidden={isImageDecorative || undefined}
366
+ {...stylex.props(styles.image)}
367
+ />
272
368
  )}
273
369
  {showSkeleton && <Skeleton radius={2} />}
274
370
  {showPlaceholder && (
@@ -279,8 +375,12 @@ export function Thumbnail({
279
375
  </>
280
376
  );
281
377
 
282
- const removeButtonEl =
283
- onRemove != null && !isDisabled ? (
378
+ const removeButtonEl = hasRemove ? (
379
+ <div
380
+ {...stylex.props(
381
+ styles.removeSlot,
382
+ isHoverReveal && styles.removeOnHover,
383
+ )}>
284
384
  <Button
285
385
  icon={<Icon icon="close" size="xsm" />}
286
386
  label={t('@astryx.thumbnail.remove', {accessibleName})}
@@ -293,7 +393,8 @@ export function Thumbnail({
293
393
  }}
294
394
  xstyle={styles.removeButtonOverrides}
295
395
  />
296
- ) : null;
396
+ </div>
397
+ ) : null;
297
398
 
298
399
  const thumbnail = (
299
400
  <div
@@ -311,7 +412,10 @@ export function Thumbnail({
311
412
  <div
312
413
  {...stylex.props(
313
414
  styles.imageContainer,
415
+ isHoverReveal && thumbnailScope,
314
416
  isInteractive && styles.interactive,
417
+ isInteractive && styles.overlay,
418
+ isInteractive && styles.hoverOnPointer,
315
419
  )}>
316
420
  {isInteractive ? (
317
421
  <button
@@ -330,11 +434,7 @@ export function Thumbnail({
330
434
  <Spinner size="sm" shade="onMedia" />
331
435
  </div>
332
436
  )}
333
- {removeButtonEl != null && imageMode != null ? (
334
- <MediaTheme mode={imageMode}>{removeButtonEl}</MediaTheme>
335
- ) : (
336
- removeButtonEl
337
- )}
437
+ {removeButtonEl}
338
438
  </div>
339
439
  </div>
340
440
  );