@astryxdesign/core 0.1.9-canary.d2d84f8 → 0.1.9-canary.e00a241

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 (414) hide show
  1. package/dist/Avatar/Avatar.d.ts +6 -1
  2. package/dist/Avatar/Avatar.d.ts.map +1 -1
  3. package/dist/Avatar/Avatar.js +46 -12
  4. package/dist/Avatar/AvatarStatusDot.d.ts +6 -4
  5. package/dist/Avatar/AvatarStatusDot.d.ts.map +1 -1
  6. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  7. package/dist/Breadcrumbs/BreadcrumbItem.js +11 -4
  8. package/dist/Breadcrumbs/index.d.ts +1 -0
  9. package/dist/Breadcrumbs/index.d.ts.map +1 -1
  10. package/dist/Breadcrumbs/index.js +2 -1
  11. package/dist/Calendar/Calendar.d.ts.map +1 -1
  12. package/dist/Calendar/Calendar.js +50 -9
  13. package/dist/Calendar/dayCellUtils.d.ts +4 -4
  14. package/dist/Calendar/dayCellUtils.js +2 -2
  15. package/dist/Calendar/styles.d.ts +30 -31
  16. package/dist/Calendar/styles.d.ts.map +1 -1
  17. package/dist/Calendar/styles.js +30 -31
  18. package/dist/Card/Card.js +1 -1
  19. package/dist/Carousel/Carousel.d.ts.map +1 -1
  20. package/dist/Carousel/Carousel.js +22 -12
  21. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  22. package/dist/Chat/ChatDictationButton.js +3 -1
  23. package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
  24. package/dist/Chat/ChatMessageBubble.js +8 -8
  25. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  26. package/dist/Chat/ChatToolCalls.js +10 -3
  27. package/dist/CheckboxInput/CheckboxInput.js +4 -4
  28. package/dist/Citation/Citation.d.ts +22 -1
  29. package/dist/Citation/Citation.d.ts.map +1 -1
  30. package/dist/Citation/Citation.js +20 -6
  31. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  32. package/dist/CodeBlock/CodeBlock.js +13 -7
  33. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  34. package/dist/Collapsible/Collapsible.js +3 -1
  35. package/dist/CommandPalette/CommandPalette.d.ts +1 -1
  36. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  37. package/dist/CommandPalette/CommandPalette.js +42 -3
  38. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  39. package/dist/ContextMenu/ContextMenu.js +14 -5
  40. package/dist/ContextMenu/index.d.ts +1 -0
  41. package/dist/ContextMenu/index.d.ts.map +1 -1
  42. package/dist/ContextMenu/index.js +3 -1
  43. package/dist/DateInput/DateInput.d.ts +1 -0
  44. package/dist/DateInput/DateInput.d.ts.map +1 -1
  45. package/dist/DateInput/DateInput.js +31 -19
  46. package/dist/DateRangeInput/DateRangeInput.d.ts +1 -0
  47. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  48. package/dist/DateRangeInput/DateRangeInput.js +20 -19
  49. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  50. package/dist/DateTimeInput/DateTimeInput.js +23 -18
  51. package/dist/Divider/Divider.d.ts +2 -2
  52. package/dist/Divider/Divider.d.ts.map +1 -1
  53. package/dist/Divider/Divider.js +13 -0
  54. package/dist/DropdownMenu/DropdownMenu.d.ts +6 -0
  55. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  56. package/dist/DropdownMenu/DropdownMenu.js +22 -5
  57. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  58. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +4 -1
  59. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  60. package/dist/DropdownMenu/DropdownMenuItem.js +4 -1
  61. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
  62. package/dist/DropdownMenu/DropdownMenuRadioItem.js +4 -1
  63. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +104 -0
  64. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -0
  65. package/dist/DropdownMenu/DropdownMenuSubMenu.js +427 -0
  66. package/dist/DropdownMenu/index.d.ts +10 -0
  67. package/dist/DropdownMenu/index.d.ts.map +1 -1
  68. package/dist/DropdownMenu/index.js +13 -0
  69. package/dist/DropdownMenu/menuItemHover.d.ts +23 -0
  70. package/dist/DropdownMenu/menuItemHover.d.ts.map +1 -0
  71. package/dist/DropdownMenu/menuItemHover.js +33 -0
  72. package/dist/DropdownMenu/menuItemRoles.d.ts +8 -0
  73. package/dist/DropdownMenu/menuItemRoles.d.ts.map +1 -1
  74. package/dist/DropdownMenu/menuItemRoles.js +10 -1
  75. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  76. package/dist/DropdownMenu/renderDropdownItems.js +21 -6
  77. package/dist/Field/Field.d.ts +3 -1
  78. package/dist/Field/Field.d.ts.map +1 -1
  79. package/dist/Field/Field.js +4 -1
  80. package/dist/Field/inputStyles.stylex.d.ts +1 -0
  81. package/dist/Field/inputStyles.stylex.d.ts.map +1 -1
  82. package/dist/Field/inputStyles.stylex.js +1 -0
  83. package/dist/FieldStatus/FieldStatus.d.ts +4 -1
  84. package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
  85. package/dist/FieldStatus/FieldStatus.js +4 -2
  86. package/dist/FileInput/FileInput.d.ts +1 -0
  87. package/dist/FileInput/FileInput.d.ts.map +1 -1
  88. package/dist/FileInput/FileInput.js +91 -65
  89. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  90. package/dist/HoverCard/useHoverCard.js +10 -4
  91. package/dist/Icon/Icon.d.ts +15 -1
  92. package/dist/Icon/Icon.d.ts.map +1 -1
  93. package/dist/Icon/Icon.js +19 -3
  94. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  95. package/dist/Lightbox/Lightbox.js +15 -9
  96. package/dist/List/List.d.ts.map +1 -1
  97. package/dist/List/List.js +6 -3
  98. package/dist/Markdown/Markdown.d.ts.map +1 -1
  99. package/dist/Markdown/Markdown.js +7 -0
  100. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  101. package/dist/MultiSelector/MultiSelector.js +65 -40
  102. package/dist/NumberInput/NumberInput.d.ts +1 -0
  103. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  104. package/dist/NumberInput/NumberInput.js +20 -17
  105. package/dist/Outline/Outline.js +2 -2
  106. package/dist/RadioList/RadioList.d.ts +2 -2
  107. package/dist/RadioList/RadioListItem.js +4 -4
  108. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  109. package/dist/Resizable/ResizeHandle.js +49 -19
  110. package/dist/SegmentedControl/SegmentedControlItem.d.ts +1 -1
  111. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  112. package/dist/SegmentedControl/SegmentedControlItem.js +2 -1
  113. package/dist/Selector/Selector.d.ts.map +1 -1
  114. package/dist/Selector/Selector.js +58 -16
  115. package/dist/SideNav/SideNav.d.ts.map +1 -1
  116. package/dist/SideNav/SideNav.js +5 -5
  117. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  118. package/dist/SideNav/SideNavCollapseButton.js +13 -10
  119. package/dist/SideNav/SideNavItem.d.ts +1 -1
  120. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  121. package/dist/SideNav/SideNavItem.js +12 -10
  122. package/dist/Slider/Slider.d.ts.map +1 -1
  123. package/dist/Slider/Slider.js +87 -34
  124. package/dist/Spinner/Spinner.d.ts +3 -2
  125. package/dist/Spinner/Spinner.d.ts.map +1 -1
  126. package/dist/Spinner/Spinner.js +11 -2
  127. package/dist/Switch/Switch.d.ts +8 -1
  128. package/dist/Switch/Switch.d.ts.map +1 -1
  129. package/dist/Switch/Switch.js +160 -91
  130. package/dist/TabList/TabList.d.ts +2 -11
  131. package/dist/TabList/TabList.d.ts.map +1 -1
  132. package/dist/TabList/TabList.js +5 -9
  133. package/dist/TabList/TabListContext.d.ts +0 -5
  134. package/dist/TabList/TabListContext.d.ts.map +1 -1
  135. package/dist/TabList/TabListContext.js +0 -5
  136. package/dist/Table/BaseTable.d.ts.map +1 -1
  137. package/dist/Table/BaseTable.js +30 -4
  138. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  139. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +15 -11
  140. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  141. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +36 -26
  142. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  143. package/dist/Table/plugins/selection/useTableSelection.js +3 -0
  144. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -1
  145. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +14 -6
  146. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
  147. package/dist/Table/plugins/tree/useTableTreeData.js +29 -23
  148. package/dist/Table/types.d.ts +32 -0
  149. package/dist/Table/types.d.ts.map +1 -1
  150. package/dist/TextArea/TextArea.d.ts +1 -0
  151. package/dist/TextArea/TextArea.d.ts.map +1 -1
  152. package/dist/TextArea/TextArea.js +16 -19
  153. package/dist/TextInput/TextInput.d.ts +1 -0
  154. package/dist/TextInput/TextInput.d.ts.map +1 -1
  155. package/dist/TextInput/TextInput.js +18 -17
  156. package/dist/TimeInput/TimeInput.d.ts +1 -0
  157. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  158. package/dist/TimeInput/TimeInput.js +15 -17
  159. package/dist/Timestamp/Timestamp.d.ts +31 -3
  160. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  161. package/dist/Timestamp/Timestamp.js +47 -76
  162. package/dist/Timestamp/formatInstant.d.ts +41 -0
  163. package/dist/Timestamp/formatInstant.d.ts.map +1 -0
  164. package/dist/Timestamp/formatInstant.js +177 -0
  165. package/dist/Timestamp/index.d.ts +1 -0
  166. package/dist/Timestamp/index.d.ts.map +1 -1
  167. package/dist/Timestamp/tooltipEntries.d.ts +56 -0
  168. package/dist/Timestamp/tooltipEntries.d.ts.map +1 -0
  169. package/dist/Timestamp/tooltipEntries.js +163 -0
  170. package/dist/Token/Token.d.ts +30 -3
  171. package/dist/Token/Token.d.ts.map +1 -1
  172. package/dist/Token/Token.js +17 -2
  173. package/dist/Token/index.d.ts +1 -1
  174. package/dist/Token/index.d.ts.map +1 -1
  175. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  176. package/dist/Tokenizer/Tokenizer.js +2 -2
  177. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  178. package/dist/Tooltip/useTooltip.js +17 -1
  179. package/dist/TopNav/TopNavItem.d.ts +3 -1
  180. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  181. package/dist/TopNav/TopNavItem.js +30 -3
  182. package/dist/TreeList/TreeList.d.ts.map +1 -1
  183. package/dist/TreeList/TreeList.js +1 -0
  184. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  185. package/dist/TreeList/TreeListBranches.js +8 -6
  186. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  187. package/dist/TreeList/TreeListItem.js +32 -14
  188. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  189. package/dist/Typeahead/BaseTypeahead.js +19 -10
  190. package/dist/Typeahead/Typeahead.js +1 -1
  191. package/dist/Typeahead/TypeaheadItem.d.ts +1 -1
  192. package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
  193. package/dist/Typeahead/TypeaheadItem.js +20 -10
  194. package/dist/astryx.css +38 -14
  195. package/dist/astryx.umd.js +52 -52
  196. package/dist/astryx.umd.js.map +4 -4
  197. package/dist/hooks/index.d.ts +2 -0
  198. package/dist/hooks/index.d.ts.map +1 -1
  199. package/dist/hooks/index.js +1 -0
  200. package/dist/hooks/isRtlElement.d.ts +18 -0
  201. package/dist/hooks/isRtlElement.d.ts.map +1 -0
  202. package/dist/hooks/isRtlElement.js +27 -0
  203. package/dist/hooks/useAnnounce.d.ts +3 -0
  204. package/dist/hooks/useAnnounce.d.ts.map +1 -1
  205. package/dist/hooks/useAnnounce.js +39 -1
  206. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  207. package/dist/hooks/useFocusTrap.js +52 -11
  208. package/dist/hooks/useGridFocus.d.ts +6 -1
  209. package/dist/hooks/useGridFocus.d.ts.map +1 -1
  210. package/dist/hooks/useGridFocus.js +12 -9
  211. package/dist/hooks/useHotkeys.d.ts +9 -0
  212. package/dist/hooks/useHotkeys.d.ts.map +1 -1
  213. package/dist/hooks/useHotkeys.js +9 -0
  214. package/dist/hooks/useInputStatusIcon.d.ts +38 -0
  215. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -0
  216. package/dist/hooks/useInputStatusIcon.js +158 -0
  217. package/dist/hooks/useListFocus.d.ts +39 -1
  218. package/dist/hooks/useListFocus.d.ts.map +1 -1
  219. package/dist/hooks/useListFocus.js +54 -9
  220. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  221. package/dist/theme/derivedVarRegistry.js +13 -0
  222. package/dist/utils/index.d.ts +1 -0
  223. package/dist/utils/index.d.ts.map +1 -1
  224. package/dist/utils/index.js +2 -1
  225. package/dist/utils/plainDate.d.ts +17 -0
  226. package/dist/utils/plainDate.d.ts.map +1 -1
  227. package/dist/utils/plainDate.js +23 -4
  228. package/dist/utils/rtlStyles.d.ts +24 -0
  229. package/dist/utils/rtlStyles.d.ts.map +1 -0
  230. package/dist/utils/rtlStyles.js +27 -0
  231. package/locales/en.json +76 -0
  232. package/locales/pseudo.json +57 -0
  233. package/package.json +6 -1
  234. package/src/Avatar/Avatar.doc.mjs +18 -4
  235. package/src/Avatar/Avatar.test.tsx +106 -0
  236. package/src/Avatar/Avatar.tsx +62 -10
  237. package/src/Avatar/AvatarStatusDot.doc.mjs +4 -4
  238. package/src/Avatar/AvatarStatusDot.tsx +6 -4
  239. package/src/AvatarGroup/AvatarGroup.test.tsx +16 -1
  240. package/src/Breadcrumbs/BreadcrumbItem.tsx +10 -10
  241. package/src/Breadcrumbs/Breadcrumbs.test.tsx +51 -0
  242. package/src/Breadcrumbs/index.ts +4 -0
  243. package/src/Button/Button.doc.mjs +0 -4
  244. package/src/Calendar/Calendar.test.tsx +170 -20
  245. package/src/Calendar/Calendar.tsx +73 -16
  246. package/src/Calendar/dayCellUtils.test.ts +18 -18
  247. package/src/Calendar/dayCellUtils.ts +2 -2
  248. package/src/Calendar/styles.ts +33 -39
  249. package/src/Card/Card.tsx +1 -1
  250. package/src/Carousel/Carousel.tsx +25 -6
  251. package/src/Chat/ChatComposer.doc.mjs +2 -2
  252. package/src/Chat/ChatComposerInput.doc.mjs +1 -1
  253. package/src/Chat/ChatDictationButton.tsx +7 -1
  254. package/src/Chat/ChatMessageBubble.tsx +15 -10
  255. package/src/Chat/ChatSystemMessage.test.tsx +5 -0
  256. package/src/Chat/ChatToolCalls.tsx +9 -2
  257. package/src/CheckboxInput/CheckboxInput.doc.mjs +6 -0
  258. package/src/CheckboxInput/CheckboxInput.tsx +4 -4
  259. package/src/CheckboxList/CheckboxList.doc.mjs +6 -0
  260. package/src/Citation/Citation.doc.mjs +4 -3
  261. package/src/Citation/Citation.test.tsx +97 -0
  262. package/src/Citation/Citation.tsx +43 -11
  263. package/src/CodeBlock/CodeBlock.tsx +35 -8
  264. package/src/Collapsible/Collapsible.doc.mjs +1 -0
  265. package/src/Collapsible/Collapsible.test.tsx +7 -0
  266. package/src/Collapsible/Collapsible.tsx +16 -6
  267. package/src/CommandPalette/CommandPalette.test.tsx +95 -1
  268. package/src/CommandPalette/CommandPalette.tsx +41 -2
  269. package/src/ContextMenu/ContextMenu.test.tsx +42 -0
  270. package/src/ContextMenu/ContextMenu.tsx +13 -11
  271. package/src/ContextMenu/index.ts +5 -0
  272. package/src/DateInput/DateInput.doc.mjs +15 -5
  273. package/src/DateInput/DateInput.test.tsx +210 -3
  274. package/src/DateInput/DateInput.tsx +41 -29
  275. package/src/DateRangeInput/DateRangeInput.doc.mjs +11 -3
  276. package/src/DateRangeInput/DateRangeInput.test.tsx +120 -3
  277. package/src/DateRangeInput/DateRangeInput.tsx +29 -27
  278. package/src/DateTimeInput/DateTimeInput.doc.mjs +22 -1
  279. package/src/DateTimeInput/DateTimeInput.test.tsx +112 -0
  280. package/src/DateTimeInput/DateTimeInput.tsx +44 -39
  281. package/src/Divider/Divider.test.tsx +32 -8
  282. package/src/Divider/Divider.tsx +14 -1
  283. package/src/DropdownMenu/DropdownMenu.test.tsx +60 -0
  284. package/src/DropdownMenu/DropdownMenu.tsx +31 -12
  285. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +8 -2
  286. package/src/DropdownMenu/DropdownMenuItem.tsx +8 -2
  287. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +8 -2
  288. package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +88 -0
  289. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +635 -0
  290. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +553 -0
  291. package/src/DropdownMenu/index.ts +16 -0
  292. package/src/DropdownMenu/menuItemHover.ts +39 -0
  293. package/src/DropdownMenu/menuItemRoles.ts +9 -0
  294. package/src/DropdownMenu/renderDropdownItems.tsx +29 -15
  295. package/src/Field/Field.tsx +14 -9
  296. package/src/Field/inputStyles.stylex.ts +2 -0
  297. package/src/FieldStatus/FieldStatus.tsx +10 -4
  298. package/src/FileInput/FileInput.doc.mjs +13 -7
  299. package/src/FileInput/FileInput.test.tsx +84 -16
  300. package/src/FileInput/FileInput.tsx +96 -79
  301. package/src/HoverCard/useHoverCard.tsx +15 -3
  302. package/src/Icon/Icon.doc.mjs +13 -0
  303. package/src/Icon/Icon.test.tsx +89 -0
  304. package/src/Icon/Icon.tsx +51 -3
  305. package/src/Lightbox/Lightbox.tsx +11 -3
  306. package/src/List/List.test.tsx +17 -29
  307. package/src/List/List.tsx +6 -1
  308. package/src/Markdown/Markdown.test.tsx +21 -0
  309. package/src/Markdown/Markdown.tsx +4 -0
  310. package/src/MultiSelector/MultiSelector.doc.mjs +11 -0
  311. package/src/MultiSelector/MultiSelector.test.tsx +306 -3
  312. package/src/MultiSelector/MultiSelector.tsx +88 -57
  313. package/src/NumberInput/NumberInput.doc.mjs +11 -5
  314. package/src/NumberInput/NumberInput.test.tsx +48 -0
  315. package/src/NumberInput/NumberInput.tsx +25 -26
  316. package/src/Outline/Outline.test.tsx +12 -12
  317. package/src/Outline/Outline.tsx +2 -2
  318. package/src/RadioList/RadioList.doc.mjs +6 -0
  319. package/src/RadioList/RadioList.tsx +2 -2
  320. package/src/RadioList/RadioListItem.tsx +4 -4
  321. package/src/Resizable/ResizeHandle.test.tsx +94 -0
  322. package/src/Resizable/ResizeHandle.tsx +70 -20
  323. package/src/SegmentedControl/SegmentedControlItem.tsx +2 -0
  324. package/src/Selector/Selector.doc.mjs +8 -0
  325. package/src/Selector/Selector.test.tsx +328 -3
  326. package/src/Selector/Selector.tsx +70 -19
  327. package/src/SideNav/SideNav.tsx +1 -8
  328. package/src/SideNav/SideNavCollapseButton.tsx +9 -7
  329. package/src/SideNav/SideNavItem.tsx +5 -3
  330. package/src/Slider/Slider.doc.mjs +6 -0
  331. package/src/Slider/Slider.test.tsx +240 -12
  332. package/src/Slider/Slider.tsx +96 -23
  333. package/src/Spinner/Spinner.test.tsx +10 -8
  334. package/src/Spinner/Spinner.tsx +15 -5
  335. package/src/Switch/Switch.doc.mjs +142 -42
  336. package/src/Switch/Switch.test.tsx +22 -0
  337. package/src/Switch/Switch.tsx +90 -33
  338. package/src/TabList/TabList.doc.mjs +0 -6
  339. package/src/TabList/TabList.test.tsx +3 -15
  340. package/src/TabList/TabList.tsx +6 -17
  341. package/src/TabList/TabListContext.ts +0 -6
  342. package/src/Table/BaseTable.tsx +34 -2
  343. package/src/Table/Table.doc.mjs +11 -0
  344. package/src/Table/Table.test.tsx +85 -0
  345. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +9 -6
  346. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +24 -17
  347. package/src/Table/plugins/selection/useTableSelection.test.tsx +61 -3
  348. package/src/Table/plugins/selection/useTableSelection.tsx +3 -0
  349. package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +37 -8
  350. package/src/Table/plugins/tree/useTableTreeData.tsx +17 -13
  351. package/src/Table/types.ts +33 -0
  352. package/src/Table/useTableRowStatus.doc.mjs +1 -1
  353. package/src/TextArea/TextArea.doc.mjs +11 -5
  354. package/src/TextArea/TextArea.tsx +17 -26
  355. package/src/TextInput/TextInput.doc.mjs +11 -5
  356. package/src/TextInput/TextInput.test.tsx +130 -3
  357. package/src/TextInput/TextInput.tsx +18 -25
  358. package/src/Thumbnail/Thumbnail.doc.mjs +1 -1
  359. package/src/TimeInput/TimeInput.doc.mjs +11 -5
  360. package/src/TimeInput/TimeInput.tsx +14 -25
  361. package/src/Timestamp/Timestamp.doc.mjs +32 -6
  362. package/src/Timestamp/Timestamp.test.tsx +371 -0
  363. package/src/Timestamp/Timestamp.tsx +96 -93
  364. package/src/Timestamp/formatInstant.ts +218 -0
  365. package/src/Timestamp/index.ts +4 -0
  366. package/src/Timestamp/tooltipEntries.test.ts +552 -0
  367. package/src/Timestamp/tooltipEntries.ts +210 -0
  368. package/src/Token/Token.test.tsx +21 -15
  369. package/src/Token/Token.tsx +31 -14
  370. package/src/Token/index.ts +1 -1
  371. package/src/Tokenizer/Tokenizer.doc.mjs +8 -1
  372. package/src/Tokenizer/Tokenizer.tsx +4 -2
  373. package/src/Tooltip/Tooltip.test.tsx +46 -0
  374. package/src/Tooltip/useTooltip.tsx +20 -0
  375. package/src/TopNav/TopNav.test.tsx +93 -2
  376. package/src/TopNav/TopNavItem.doc.mjs +3 -3
  377. package/src/TopNav/TopNavItem.tsx +35 -6
  378. package/src/TopNav/TopNavMegaMenuFeaturedCard.doc.mjs +1 -1
  379. package/src/TreeList/TreeList.doc.mjs +6 -0
  380. package/src/TreeList/TreeList.test.tsx +50 -10
  381. package/src/TreeList/TreeList.tsx +5 -0
  382. package/src/TreeList/TreeListBranches.tsx +8 -6
  383. package/src/TreeList/TreeListItem.tsx +33 -11
  384. package/src/Typeahead/BaseTypeahead.tsx +29 -14
  385. package/src/Typeahead/Typeahead.doc.mjs +6 -0
  386. package/src/Typeahead/Typeahead.test.tsx +27 -0
  387. package/src/Typeahead/Typeahead.tsx +1 -1
  388. package/src/Typeahead/TypeaheadItem.tsx +2 -1
  389. package/src/__tests__/inputWidthContract.test.tsx +62 -0
  390. package/src/doc-mjs.d.ts +19 -0
  391. package/src/hooks/index.ts +6 -0
  392. package/src/hooks/isRtlElement.ts +27 -0
  393. package/src/hooks/useAnnounce.test.tsx +75 -2
  394. package/src/hooks/useAnnounce.ts +47 -1
  395. package/src/hooks/useFocusTrap.test.tsx +104 -0
  396. package/src/hooks/useFocusTrap.ts +61 -12
  397. package/src/hooks/useGridFocus.doc.mjs +3 -3
  398. package/src/hooks/useGridFocus.test.tsx +41 -0
  399. package/src/hooks/useGridFocus.ts +21 -10
  400. package/src/hooks/useHotkeys.ts +9 -0
  401. package/src/hooks/useInputStatusIcon.test.tsx +162 -0
  402. package/src/hooks/useInputStatusIcon.tsx +224 -0
  403. package/src/hooks/useListFocus.doc.mjs +25 -3
  404. package/src/hooks/useListFocus.test.tsx +153 -1
  405. package/src/hooks/useListFocus.ts +102 -9
  406. package/src/reset.css +0 -26
  407. package/src/reset.test.ts +60 -0
  408. package/src/theme/derivedVarRegistry.test.ts +1 -0
  409. package/src/theme/derivedVarRegistry.ts +6 -0
  410. package/src/theme/generateThemeRules.test.ts +42 -0
  411. package/src/utils/index.ts +2 -0
  412. package/src/utils/plainDate.test.ts +24 -6
  413. package/src/utils/plainDate.ts +18 -4
  414. package/src/utils/rtlStyles.ts +27 -0
@@ -201,6 +201,100 @@ describe('ResizeHandle', () => {
201
201
  expect(document.body.style.userSelect).toBe('');
202
202
  });
203
203
 
204
+ // --- RTL pointer-drag direction ---
205
+
206
+ it('drives the region with the raw pointer delta under LTR', () => {
207
+ const resizable: ResizableProps = {
208
+ _size: 200,
209
+ _isCollapsed: false,
210
+ _onResizeStart: vi.fn(),
211
+ _onResizeMove: vi.fn(),
212
+ _onResizeEnd: vi.fn(),
213
+ _minSizePx: 100,
214
+ _maxSizePx: 400,
215
+ _snaps: [],
216
+ _collapsedSize: 40,
217
+ _collapsible: false,
218
+ _isResizableProps: true,
219
+ };
220
+ render(<ResizeHandle resizable={resizable} label="Resize" />);
221
+ const hitArea = screen.getByRole('separator')
222
+ .firstElementChild as HTMLElement;
223
+
224
+ fireEvent.pointerDown(hitArea, {clientX: 0, clientY: 0});
225
+ // Pointer moves +40px to the right → panel grows by +40 under LTR.
226
+ fireEvent.pointerMove(window, {clientX: 40, clientY: 0});
227
+ expect(resizable._onResizeMove).toHaveBeenLastCalledWith(40);
228
+ });
229
+
230
+ it('inverts the pointer delta under RTL so dragging resizes intuitively', () => {
231
+ const resizable: ResizableProps = {
232
+ _size: 200,
233
+ _isCollapsed: false,
234
+ _onResizeStart: vi.fn(),
235
+ _onResizeMove: vi.fn(),
236
+ _onResizeEnd: vi.fn(),
237
+ _minSizePx: 100,
238
+ _maxSizePx: 400,
239
+ _snaps: [],
240
+ _collapsedSize: 40,
241
+ _collapsible: false,
242
+ _isResizableProps: true,
243
+ };
244
+ // Under RTL the start panel sits on the RIGHT, so a pointer move to the
245
+ // right (+clientX) must SHRINK it — the delta is inverted. The handle reads
246
+ // its own computed `direction` via getRTLMultiplier(); jsdom doesn't resolve
247
+ // inherited `direction`, so force it on the separator (the handle element),
248
+ // mirroring the Slider RTL pointer-mapping test precedent.
249
+ render(<ResizeHandle resizable={resizable} label="Resize" />);
250
+ const separator = screen.getByRole('separator');
251
+ const hitArea = separator.firstElementChild as HTMLElement;
252
+
253
+ const realGetComputedStyle = window.getComputedStyle;
254
+ const gcsSpy = vi
255
+ .spyOn(window, 'getComputedStyle')
256
+ .mockImplementation((el: Element, pseudo?: string | null) => {
257
+ if (el === separator) {
258
+ return {direction: 'rtl'} as CSSStyleDeclaration;
259
+ }
260
+ return realGetComputedStyle(el, pseudo ?? undefined);
261
+ });
262
+
263
+ fireEvent.pointerDown(hitArea, {clientX: 0, clientY: 0});
264
+ // Same +40px physical move as LTR, but mirrored → −40 delta under RTL.
265
+ fireEvent.pointerMove(window, {clientX: 40, clientY: 0});
266
+ expect(resizable._onResizeMove).toHaveBeenLastCalledWith(-40);
267
+
268
+ gcsSpy.mockRestore();
269
+ });
270
+
271
+ // --- Hit-area geometry (grab zone tracks the visible pill) ---
272
+
273
+ it('anchors the biased grab zone with the pill offset and a dir-flipped centering shift', () => {
274
+ // For an off-center pill the hit area must reuse the pill's physical offset
275
+ // construction (anchored at insetInlineStart:0) rather than a divider-
276
+ // relative 50% anchor + percentage bias, so the two stay aligned in LTR and
277
+ // RTL. The half-width-difference centering shift (6.5px) is inline, so it
278
+ // flips physical sign under RTL: `- 6.5px` (LTR) vs `+ 6.5px` (RTL). See the
279
+ // Playwright measurement (hitArea.center === pill.center, 0px offset in both
280
+ // directions) for the geometric proof; here we lock the transform shape.
281
+ render(<Harness handleProps={{pillPlacement: 'start'}} />);
282
+ const hitArea = getSeparator().firstElementChild as HTMLElement;
283
+ expect(hitArea.className).toContain('hitAreaOffsetX');
284
+ const style = hitArea.getAttribute('style') ?? '';
285
+ // LTR (default) branch subtracts the centering shift; RTL branch adds it.
286
+ expect(style).toContain('- 6.5px');
287
+ expect(style).toContain('+ 6.5px');
288
+ });
289
+
290
+ it('centers the grab zone on the divider when the pill is centered (no bias)', () => {
291
+ render(<Harness handleProps={{pillPlacement: 'center'}} />);
292
+ const hitArea = getSeparator().firstElementChild as HTMLElement;
293
+ // No physical offset — a plain logical centering class, no biased transform.
294
+ expect(hitArea.className).toContain('hitAreaCenteredX');
295
+ expect(hitArea.className).not.toContain('hitAreaOffsetX');
296
+ });
297
+
204
298
  // --- Prop composition (ordering choice: handler sits after {...props}) ---
205
299
 
206
300
  it('runs a consumer onKeyDown alongside keyboard resizing', () => {
@@ -56,14 +56,27 @@ function resolveEffectiveSide(
56
56
  }
57
57
 
58
58
  /**
59
- * Hit area bias percentage. When the pill is off-center, the grab zone
60
- * shifts ~2:1 toward the pill so users can reach the visible grip easily.
59
+ * Hit-area inline bias, expressed as a fraction of the pill's per-side offset.
60
+ *
61
+ * The visible pill is placed by `pillOffsetX`/`pillOffsetY`: anchored at the
62
+ * divider's inline-start edge and pushed one grip-width-plus-gap toward the
63
+ * panel side via a PHYSICAL translate (dir −1 = start, +1 = end). Because that
64
+ * offset is physical, the pill sits on the same physical side of the divider in
65
+ * both LTR and RTL. The grab zone must sit OVER that pill, so it has to use the
66
+ * exact same anchor + physical-offset construction as the pill (just wider) —
67
+ * anchoring the hit area at the divider centre (`insetInlineStart: 50%`) and
68
+ * biasing with a percentage translate mixes a logical anchor (which flips under
69
+ * RTL) with a physical translate (which doesn't), leaving the grab zone stranded
70
+ * to one side under RTL. Returning the pill's `dir` here lets the hit area reuse
71
+ * `pillOffsetX`/`pillOffsetY` so the two elements are positioned identically.
61
72
  */
62
- function hitAreaBias(effectiveSide: 'start' | 'end' | 'center'): string {
73
+ function hitAreaBiasDir(
74
+ effectiveSide: 'start' | 'end' | 'center',
75
+ ): number | null {
63
76
  if (effectiveSide === 'center') {
64
- return '50%';
77
+ return null;
65
78
  }
66
- return effectiveSide === 'start' ? '66.67%' : '33.33%';
79
+ return effectiveSide === 'start' ? -1 : 1;
67
80
  }
68
81
 
69
82
  const styles = stylex.create({
@@ -102,8 +115,8 @@ const styles = stylex.create({
102
115
  },
103
116
  overlayVertical: {
104
117
  insetBlockEnd: 0,
105
- left: 0,
106
- right: 0,
118
+ insetInlineStart: 0,
119
+ insetInlineEnd: 0,
107
120
  height: 'var(--resize-handle-hit-area, 16px)',
108
121
  },
109
122
  horizontal: {
@@ -145,16 +158,24 @@ const styles = stylex.create({
145
158
  width: spacingVars['--spacing-4'],
146
159
  top: 0,
147
160
  bottom: 0,
148
- left: '50%',
149
161
  cursor: 'col-resize',
150
162
  },
151
163
  hitAreaVertical: {
152
164
  height: spacingVars['--spacing-4'],
153
- left: 0,
154
- right: 0,
155
- top: '50%',
165
+ insetInlineStart: 0,
166
+ insetInlineEnd: 0,
156
167
  cursor: 'row-resize',
157
168
  },
169
+ // Centered grab zone (pillPlacement 'center' / no bias): sit the hit area on
170
+ // the divider itself. Anchored logically so it centers in both directions.
171
+ hitAreaCenteredX: {
172
+ insetInlineStart: '50%',
173
+ transform: 'translateX(-50%)',
174
+ },
175
+ hitAreaCenteredY: {
176
+ insetBlockStart: '50%',
177
+ transform: 'translateY(-50%)',
178
+ },
158
179
 
159
180
  // Pill base — themes target .astryx-resize-handle-pill for size/shape.
160
181
  pill: {
@@ -167,7 +188,7 @@ const styles = stylex.create({
167
188
  transitionDuration: durationVars['--duration-fast'],
168
189
  transitionTimingFunction: easeVars['--ease-standard'],
169
190
  top: '50%',
170
- left: '50%',
191
+ insetInlineStart: '50%',
171
192
  transform: 'translate(-50%, -50%)',
172
193
  },
173
194
  pillHorizontal: {
@@ -193,14 +214,32 @@ const styles = stylex.create({
193
214
  // Dynamic styles — avoids inline style overrides.
194
215
  // Each axis gets its own function since StyleX requires static structure.
195
216
  const dynamicStyles = stylex.create({
196
- hitAreaBiasX: (pct: string) => ({
197
- transform: `translateX(-${pct})`,
217
+ // Hit-area inline offset — mirrors the pill's `pillOffsetX`/`pillOffsetY`
218
+ // construction so the grab zone sits centred on the visible pill in BOTH
219
+ // directions. Both anchor at the divider's inline-start edge
220
+ // (`insetInlineStart: 0`) and travel toward the panel side with the SAME
221
+ // physical translate `dir * (gripWidth + gap)`, so they share the pill's
222
+ // near edge. The hit area is wider, so it also shifts by half the width
223
+ // difference `(16px − 3px) / 2 = 6.5px` to align the two CENTRES. That
224
+ // centring shift is along the inline axis (a box grows from its inline-start
225
+ // edge toward inline-end), so it must flip physical sign under RTL — unlike
226
+ // the `dir * (...)` travel, which is physical and identical in both
227
+ // directions because the pill's own offset is physical. Mixing the previous
228
+ // divider-relative `50%` anchor with a percentage translate stranded the grab
229
+ // zone to one side under RTL; this construction keeps it on the pill.
230
+ hitAreaOffsetX: (dir: number) => ({
231
+ insetInlineStart: 0,
232
+ transform: {
233
+ default: `translate(calc(${dir} * (3px + ${spacingVars['--spacing-1']}) - 6.5px), -50%)`,
234
+ ':is([dir="rtl"] *)': `translate(calc(${dir} * (3px + ${spacingVars['--spacing-1']}) + 6.5px), -50%)`,
235
+ },
198
236
  }),
199
- hitAreaBiasY: (pct: string) => ({
200
- transform: `translateY(-${pct})`,
237
+ hitAreaOffsetY: (dir: number) => ({
238
+ insetBlockStart: 0,
239
+ transform: `translate(-50%, calc(${dir} * (3px + ${spacingVars['--spacing-1']}) - 6.5px))`,
201
240
  }),
202
241
  pillOffsetX: (dir: number) => ({
203
- left: 0,
242
+ insetInlineStart: 0,
204
243
  transform: `translate(calc(${dir} * (100% + ${spacingVars['--spacing-1']})), -50%)`,
205
244
  }),
206
245
  // Vertical offset: no rotation — use explicit landscape dimensions.
@@ -333,6 +372,9 @@ export function ResizeHandle({
333
372
  isReversed,
334
373
  resizable?._isCollapsed ?? false,
335
374
  );
375
+ // Physical offset direction shared by the pill and the grab zone (null =
376
+ // centered / no bias). Keeps the two positioned identically in LTR and RTL.
377
+ const hitBiasDir = hitAreaBiasDir(effectiveSide);
336
378
 
337
379
  const getRTLMultiplier = useCallback((): number => {
338
380
  const el = handleRef.current;
@@ -538,9 +580,17 @@ export function ResizeHandle({
538
580
  {...stylex.props(
539
581
  styles.hitArea,
540
582
  isHorizontal ? styles.hitAreaHorizontal : styles.hitAreaVertical,
541
- isHorizontal
542
- ? dynamicStyles.hitAreaBiasX(hitAreaBias(effectiveSide))
543
- : dynamicStyles.hitAreaBiasY(hitAreaBias(effectiveSide)),
583
+ // Bias the grab zone to sit over the visible pill. When the pill is
584
+ // centered (no bias) just center the hit area on the divider; when
585
+ // it's offset to a panel side, reuse the pill's physical-offset
586
+ // construction so the two stay aligned in LTR and RTL alike.
587
+ hitBiasDir == null
588
+ ? isHorizontal
589
+ ? styles.hitAreaCenteredX
590
+ : styles.hitAreaCenteredY
591
+ : isHorizontal
592
+ ? dynamicStyles.hitAreaOffsetX(hitBiasDir)
593
+ : dynamicStyles.hitAreaOffsetY(hitBiasDir),
544
594
  isDisabled && styles.disabled,
545
595
  )}
546
596
  onPointerDown={handlePointerDown}
@@ -169,6 +169,7 @@ export function SegmentedControlItem({
169
169
  icon,
170
170
  isDisabled = false,
171
171
  onClick: onClickProp,
172
+ xstyle,
172
173
  ...rest
173
174
  }: SegmentedControlItemProps) {
174
175
  const ctx = useSegmentedControlContext();
@@ -228,6 +229,7 @@ export function SegmentedControlItem({
228
229
  isSelected && styles.selected,
229
230
  !isSelected && !isItemDisabled && styles.hover,
230
231
  isItemDisabled && styles.disabled,
232
+ xstyle,
231
233
  ),
232
234
  )}>
233
235
  {iconElement}
@@ -23,6 +23,8 @@ export const docs = {
23
23
  targets: [
24
24
  {className: 'astryx-selector', visualProps: ['size', 'status']},
25
25
  {className: 'astryx-selector-option'},
26
+ {className: 'astryx-selector-clear-icon'},
27
+ {className: 'astryx-selector-indicator-icon', states: ['state']},
26
28
  ],
27
29
  },
28
30
  description: 'Dropdown selector for choosing from a list of options.',
@@ -141,6 +143,12 @@ export const docs = {
141
143
  description:
142
144
  'Custom render function for each selectable option in the dropdown. Use this instead of JSX children; dividers and sections are rendered by the selector.',
143
145
  },
146
+ {
147
+ name: 'width',
148
+ type: 'SizeValue',
149
+ description:
150
+ 'Width of the field (number = pixels, string used as-is, e.g. "100%"). Sizes the whole field (label, control, and status) so they stay aligned.',
151
+ },
144
152
  {
145
153
  name: 'xstyle',
146
154
  type: 'StyleXStyles',
@@ -20,8 +20,11 @@ import {
20
20
  import userEvent from '@testing-library/user-event';
21
21
  import {Selector} from './Selector';
22
22
  import {SelectorOption} from './SelectorOption';
23
+ import {Icon} from '../Icon';
23
24
  import {InputGroup, InputGroupText} from '../InputGroup';
24
25
  import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
26
+ import {defineTheme} from '../theme/defineTheme';
27
+ import {generateThemeCSSFlat} from '../theme/generateThemeRules';
25
28
 
26
29
  function politeRegion(): HTMLElement | null {
27
30
  return document.querySelector('[data-astryx-live-region="polite"]');
@@ -746,6 +749,119 @@ describe('Selector', () => {
746
749
  expect(politeRegion()?.textContent ?? '').toBe('');
747
750
  });
748
751
  });
752
+
753
+ describe('grouped search', () => {
754
+ const GROUPED = [
755
+ {
756
+ type: 'section' as const,
757
+ title: 'Citrus',
758
+ options: [
759
+ {value: 'orange', label: 'Orange'},
760
+ {value: 'lemon', label: 'Lemon'},
761
+ ],
762
+ },
763
+ {
764
+ type: 'section' as const,
765
+ title: 'Berries',
766
+ options: [
767
+ {value: 'strawberry', label: 'Strawberry'},
768
+ {value: 'blueberry', label: 'Blueberry'},
769
+ ],
770
+ },
771
+ ];
772
+
773
+ it('keeps the group header above matching items while searching', async () => {
774
+ const user = userEvent.setup();
775
+ render(
776
+ <Selector
777
+ label="Fruit"
778
+ options={GROUPED}
779
+ onChange={() => {}}
780
+ hasSearch
781
+ />,
782
+ );
783
+ await user.click(screen.getByRole('button', {name: 'Fruit'}));
784
+ // "orange" only matches within the Citrus group.
785
+ await user.type(screen.getByRole('combobox', h), 'orange');
786
+
787
+ expect(
788
+ screen.getByRole('group', {name: 'Citrus', ...h}),
789
+ ).toBeInTheDocument();
790
+ const options = screen.getAllByRole('option', h);
791
+ expect(options).toHaveLength(1);
792
+ expect(options[0]).toHaveTextContent('Orange');
793
+ });
794
+
795
+ it('hides a group whose items have no match', async () => {
796
+ const user = userEvent.setup();
797
+ render(
798
+ <Selector
799
+ label="Fruit"
800
+ options={GROUPED}
801
+ onChange={() => {}}
802
+ hasSearch
803
+ />,
804
+ );
805
+ await user.click(screen.getByRole('button', {name: 'Fruit'}));
806
+ // "berry" only matches items inside the Berries group.
807
+ await user.type(screen.getByRole('combobox', h), 'berry');
808
+
809
+ expect(
810
+ screen.getByRole('group', {name: 'Berries', ...h}),
811
+ ).toBeInTheDocument();
812
+ expect(
813
+ screen.queryByRole('group', {name: 'Citrus', ...h}),
814
+ ).not.toBeInTheDocument();
815
+ expect(screen.getAllByRole('option', h)).toHaveLength(2);
816
+ });
817
+
818
+ it('lands keyboard focus on the correct option after filtering', async () => {
819
+ const user = userEvent.setup();
820
+ render(
821
+ <Selector
822
+ label="Fruit"
823
+ options={GROUPED}
824
+ onChange={() => {}}
825
+ hasSearch
826
+ />,
827
+ );
828
+ await user.click(screen.getByRole('button', {name: 'Fruit'}));
829
+ const search = screen.getByRole('combobox', h);
830
+ // "berry" leaves Strawberry, Blueberry (in that document order).
831
+ await user.type(search, 'berry');
832
+ const options = screen.getAllByRole('option', h);
833
+ expect(options.map(o => o.textContent)).toEqual([
834
+ 'Strawberry',
835
+ 'Blueberry',
836
+ ]);
837
+ await user.keyboard('{ArrowDown}');
838
+ expect(search).toHaveAttribute('aria-activedescendant', options[0].id);
839
+ await user.keyboard('{ArrowDown}');
840
+ expect(search).toHaveAttribute('aria-activedescendant', options[1].id);
841
+ });
842
+
843
+ it('shows the empty state when no group has a match', async () => {
844
+ const user = userEvent.setup();
845
+ render(
846
+ <Selector
847
+ label="Fruit"
848
+ options={GROUPED}
849
+ onChange={() => {}}
850
+ hasSearch
851
+ />,
852
+ );
853
+ await user.click(screen.getByRole('button', {name: 'Fruit'}));
854
+ await user.type(screen.getByRole('combobox', h), 'zzz');
855
+ expect(screen.queryAllByRole('option', h)).toHaveLength(0);
856
+ expect(
857
+ screen.queryByRole('group', {name: 'Citrus', ...h}),
858
+ ).not.toBeInTheDocument();
859
+ const listbox = screen.getByRole('listbox', h);
860
+ expect(
861
+ within(listbox).getByText('No results found'),
862
+ ).toBeInTheDocument();
863
+ });
864
+ });
749
865
  });
750
866
 
751
867
  describe('keyboard accessibility', () => {
@@ -1074,11 +1190,14 @@ describe('Selector', () => {
1074
1190
  });
1075
1191
  });
1076
1192
 
1077
-
1078
1193
  describe('Selector statusVariant forwarding', () => {
1079
1194
  it('defaults to attached (status renders with data-variant="attached")', () => {
1080
1195
  const {container} = render(
1081
- <Selector label="Fruit" options={['Apple', 'Banana']} status={{type: 'error', message: 'Required'}} />,
1196
+ <Selector
1197
+ label="Fruit"
1198
+ options={['Apple', 'Banana']}
1199
+ status={{type: 'error', message: 'Required'}}
1200
+ />,
1082
1201
  );
1083
1202
  expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
1084
1203
  'data-variant',
@@ -1088,7 +1207,12 @@ describe('Selector statusVariant forwarding', () => {
1088
1207
 
1089
1208
  it('forwards statusVariant="detached" to the underlying Field status', () => {
1090
1209
  const {container} = render(
1091
- <Selector label="Fruit" options={['Apple', 'Banana']} status={{type: 'error', message: 'Required'}} statusVariant="detached" />,
1210
+ <Selector
1211
+ label="Fruit"
1212
+ options={['Apple', 'Banana']}
1213
+ status={{type: 'error', message: 'Required'}}
1214
+ statusVariant="detached"
1215
+ />,
1092
1216
  );
1093
1217
  expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
1094
1218
  'data-variant',
@@ -1096,3 +1220,204 @@ describe('Selector statusVariant forwarding', () => {
1096
1220
  );
1097
1221
  });
1098
1222
  });
1223
+
1224
+ describe('Selector clear icon theme target', () => {
1225
+ // Resolve the clear glyph span (the astryx-icon element inside the clear
1226
+ // button), independent of the theme target class.
1227
+ const getClearIcon = (): HTMLElement => {
1228
+ const button = screen.getByRole('button', {name: 'Clear Fruit'});
1229
+ const icon = button.querySelector('.astryx-icon');
1230
+ if (icon == null) {
1231
+ throw new Error('clear icon not found');
1232
+ }
1233
+ return icon as HTMLElement;
1234
+ };
1235
+
1236
+ it('renders the astryx-selector-clear-icon target on the clear glyph', () => {
1237
+ render(
1238
+ <Selector
1239
+ label="Fruit"
1240
+ options={OPTIONS}
1241
+ value="Banana"
1242
+ onChange={() => {}}
1243
+ hasClear
1244
+ />,
1245
+ );
1246
+ // The stable theme target lands on the icon element itself (not the
1247
+ // button), so a theme can restyle just this glyph (color, size, hover)
1248
+ // via `defineTheme` — a button-level target could not reach the icon's
1249
+ // own color/size.
1250
+ const icon = getClearIcon();
1251
+ expect(icon).toHaveClass('astryx-selector-clear-icon');
1252
+ expect(icon).toHaveClass('astryx-icon');
1253
+ });
1254
+
1255
+ it('keeps the clear button functional alongside the target', () => {
1256
+ const onChange = vi.fn();
1257
+ render(
1258
+ <Selector
1259
+ label="Fruit"
1260
+ options={OPTIONS}
1261
+ value="Banana"
1262
+ onChange={onChange}
1263
+ hasClear
1264
+ />,
1265
+ );
1266
+ const clear = screen.getByRole('button', {name: 'Clear Fruit'});
1267
+ expect(clear.tagName).toBe('BUTTON');
1268
+ fireEvent.click(clear);
1269
+ expect(onChange).toHaveBeenCalledWith(null);
1270
+ });
1271
+
1272
+ it('renders the default icon (secondary color, sm size) byte-identically', () => {
1273
+ // Pixel-identical default guard: the clear glyph must carry the exact same
1274
+ // StyleX color/size classes as a standalone secondary/sm icon. The added
1275
+ // target class is purely additive — it changes nothing until a theme
1276
+ // targets it.
1277
+ render(
1278
+ <Selector
1279
+ label="Fruit"
1280
+ options={OPTIONS}
1281
+ value="Banana"
1282
+ onChange={() => {}}
1283
+ hasClear
1284
+ />,
1285
+ );
1286
+ const icon = getClearIcon();
1287
+
1288
+ const {container: refContainer} = render(
1289
+ <Icon icon="close" size="sm" color="secondary" />,
1290
+ );
1291
+ const refIcon = refContainer.querySelector('.astryx-icon') as HTMLElement;
1292
+
1293
+ const styleClasses = (el: HTMLElement) =>
1294
+ el.className
1295
+ .split(' ')
1296
+ .filter(c => c !== 'astryx-selector-clear-icon')
1297
+ .sort();
1298
+
1299
+ expect(styleClasses(icon)).toEqual(styleClasses(refIcon));
1300
+ });
1301
+
1302
+ it('exposes selector-clear-icon so a theme reaches the icon color, size, and hover', () => {
1303
+ // jsdom cannot resolve the @layer cascade, so the DOM-class assertion above
1304
+ // (target lands on the icon element) plus this generation assertion (the
1305
+ // theme emits same-element icon rules in @layer astryx-theme) together
1306
+ // prove the seam: a same-element theme rule wins over the icon's own
1307
+ // base-layer color/size.
1308
+ const theme = defineTheme({
1309
+ name: 'selector-clear-icon-test',
1310
+ components: {
1311
+ 'selector-clear-icon': {
1312
+ base: {
1313
+ width: '12px',
1314
+ height: '12px',
1315
+ fontSize: '12px',
1316
+ color: 'var(--color-icon-secondary)',
1317
+ ':hover': {color: 'var(--color-icon-primary)'},
1318
+ },
1319
+ },
1320
+ },
1321
+ });
1322
+ const css = generateThemeCSSFlat(theme);
1323
+ expect(css).toContain('.astryx-selector-clear-icon {');
1324
+ expect(css).toContain('width: 12px');
1325
+ expect(css).toContain('height: 12px');
1326
+ expect(css).toContain('.astryx-selector-clear-icon:hover {');
1327
+ expect(css).toContain('color: var(--color-icon-primary)');
1328
+ });
1329
+ });
1330
+
1331
+ describe('Selector indicator (chevron) icon theme target', () => {
1332
+ const getIndicatorIcon = (container: HTMLElement): HTMLElement => {
1333
+ // The chevron is the only glyph carrying the indicator target class.
1334
+ const icon = container.querySelector('.astryx-selector-indicator-icon');
1335
+ if (icon == null) {
1336
+ throw new Error('indicator icon not found');
1337
+ }
1338
+ return icon as HTMLElement;
1339
+ };
1340
+
1341
+ it('renders the astryx-selector-indicator-icon target on the chevron glyph', () => {
1342
+ const {container} = render(
1343
+ <Selector label="Fruit" options={OPTIONS} onChange={() => {}} />,
1344
+ );
1345
+ // The stable theme target lands on the icon element itself (not the trigger
1346
+ // button), so a theme can restyle just this glyph (color, size, hover) —
1347
+ // and each open/closed state — via `defineTheme`. A button-level target
1348
+ // could not reach the icon's own color/size.
1349
+ const icon = getIndicatorIcon(container);
1350
+ expect(icon).toHaveClass('astryx-selector-indicator-icon');
1351
+ expect(icon).toHaveClass('astryx-icon');
1352
+ // Open/closed state is reflected so a theme can target each state alone.
1353
+ expect(icon).toHaveAttribute('data-state', 'collapsed');
1354
+ });
1355
+
1356
+ it('reflects the expanded state on the chevron when the popover is open', async () => {
1357
+ const user = userEvent.setup();
1358
+ const {container} = render(
1359
+ <Selector label="Fruit" options={OPTIONS} onChange={() => {}} />,
1360
+ );
1361
+ await user.click(screen.getByRole('combobox'));
1362
+ await waitFor(() => {
1363
+ expect(getIndicatorIcon(container)).toHaveAttribute(
1364
+ 'data-state',
1365
+ 'expanded',
1366
+ );
1367
+ });
1368
+ });
1369
+
1370
+ it('renders the default icon (inherit color, sm size) byte-identically', () => {
1371
+ // Pixel-identical default guard: the chevron glyph must carry the exact
1372
+ // same StyleX color/size classes as a standalone inherit/sm icon. The added
1373
+ // target class + data-state are purely additive — they change nothing until
1374
+ // a theme targets them.
1375
+ const {container} = render(
1376
+ <Selector label="Fruit" options={OPTIONS} onChange={() => {}} />,
1377
+ );
1378
+ const icon = getIndicatorIcon(container);
1379
+
1380
+ const {container: refContainer} = render(
1381
+ <Icon icon="chevronDown" size="sm" color="inherit" />,
1382
+ );
1383
+ const refIcon = refContainer.querySelector('.astryx-icon') as HTMLElement;
1384
+
1385
+ // Exclude the additive theme-target classes (the stable target + its
1386
+ // reflected state class) so only the StyleX color/size classes remain.
1387
+ const themeTargetClasses = new Set([
1388
+ 'astryx-selector-indicator-icon',
1389
+ 'collapsed',
1390
+ 'expanded',
1391
+ ]);
1392
+ const styleClasses = (el: HTMLElement) =>
1393
+ el.className
1394
+ .split(' ')
1395
+ .filter(c => !themeTargetClasses.has(c))
1396
+ .sort();
1397
+
1398
+ expect(styleClasses(icon)).toEqual(styleClasses(refIcon));
1399
+ });
1400
+
1401
+ it('exposes selector-indicator-icon so a theme reaches the icon size and per-state color', () => {
1402
+ // jsdom cannot resolve the @layer cascade, so the DOM-class assertions
1403
+ // above (target lands on the icon element) plus this generation assertion
1404
+ // (the theme emits same-element icon rules in @layer astryx-theme) together
1405
+ // prove the seam: a same-element theme rule wins over the icon's own
1406
+ // base-layer color/size.
1407
+ const theme = defineTheme({
1408
+ name: 'selector-indicator-icon-test',
1409
+ components: {
1410
+ 'selector-indicator-icon': {
1411
+ base: {width: '14px', height: '14px', fontSize: '14px'},
1412
+ 'state:expanded': {color: 'var(--color-icon-primary)'},
1413
+ },
1414
+ },
1415
+ });
1416
+ const css = generateThemeCSSFlat(theme);
1417
+ expect(css).toContain('.astryx-selector-indicator-icon {');
1418
+ expect(css).toContain('width: 14px');
1419
+ expect(css).toContain('height: 14px');
1420
+ expect(css).toContain('.astryx-selector-indicator-icon.expanded');
1421
+ expect(css).toContain('color: var(--color-icon-primary)');
1422
+ });
1423
+ });