@astryxdesign/core 0.4.5 → 0.4.6-canary.cea9ecd

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 (538) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/dist/Avatar/Avatar.d.ts +10 -5
  3. package/dist/Avatar/Avatar.d.ts.map +1 -1
  4. package/dist/Avatar/Avatar.js +139 -30
  5. package/dist/Avatar/AvatarStatusDot.d.ts +2 -1
  6. package/dist/Avatar/AvatarStatusDot.d.ts.map +1 -1
  7. package/dist/Avatar/AvatarStatusDot.js +30 -4
  8. package/dist/Avatar/AvatarStatusLabelContext.d.ts +39 -0
  9. package/dist/Avatar/AvatarStatusLabelContext.d.ts.map +1 -0
  10. package/dist/Avatar/AvatarStatusLabelContext.js +41 -0
  11. package/dist/AvatarGroup/AvatarGroup.d.ts +3 -1
  12. package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
  13. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  14. package/dist/AvatarGroup/AvatarGroupOverflow.js +15 -9
  15. package/dist/Blockquote/Blockquote.d.ts +4 -3
  16. package/dist/Blockquote/Blockquote.d.ts.map +1 -1
  17. package/dist/Blockquote/Blockquote.js +9 -10
  18. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  19. package/dist/BottomSheet/BottomSheetPanel.js +21 -5
  20. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  21. package/dist/Breadcrumbs/BreadcrumbItem.js +47 -34
  22. package/dist/Button/Button.d.ts.map +1 -1
  23. package/dist/Button/Button.js +7 -7
  24. package/dist/Calendar/Calendar.d.ts.map +1 -1
  25. package/dist/Calendar/Calendar.js +21 -12
  26. package/dist/Calendar/hooks/useCalendarNavigation.d.ts.map +1 -1
  27. package/dist/Calendar/hooks/useCalendarNavigation.js +8 -4
  28. package/dist/Calendar/styles.d.ts +2 -2
  29. package/dist/Calendar/styles.d.ts.map +1 -1
  30. package/dist/Calendar/styles.js +5 -5
  31. package/dist/Center/Center.d.ts +23 -1
  32. package/dist/Center/Center.d.ts.map +1 -1
  33. package/dist/Center/Center.js +12 -6
  34. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  35. package/dist/Chat/ChatComposer.js +3 -3
  36. package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
  37. package/dist/Chat/ChatComposerDrawer.js +2 -2
  38. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  39. package/dist/Chat/ChatToolCalls.js +2 -2
  40. package/dist/Chat/useChatDictation.d.ts +1 -1
  41. package/dist/Chat/useChatDictation.d.ts.map +1 -1
  42. package/dist/Chat/useSpeechRecognition.d.ts +1 -1
  43. package/dist/Chat/useSpeechRecognition.d.ts.map +1 -1
  44. package/dist/Chat/useSpeechRecognition.js +4 -3
  45. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  46. package/dist/Chat/useTriggerMenu.js +2 -2
  47. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  48. package/dist/CheckboxInput/CheckboxInput.js +2 -2
  49. package/dist/Citation/Citation.d.ts.map +1 -1
  50. package/dist/Citation/Citation.js +5 -5
  51. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  52. package/dist/ClickableCard/ClickableCard.js +4 -4
  53. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  54. package/dist/CodeBlock/CodeBlock.js +1 -1
  55. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  56. package/dist/Collapsible/Collapsible.js +2 -2
  57. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  58. package/dist/CommandPalette/CommandPaletteItem.js +3 -3
  59. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  60. package/dist/ComplexSelector/ComplexSelector.js +6 -6
  61. package/dist/DateInput/DateInput.d.ts +31 -2
  62. package/dist/DateInput/DateInput.d.ts.map +1 -1
  63. package/dist/DateInput/DateInput.js +90 -22
  64. package/dist/DateInput/MonthScroller.d.ts +35 -0
  65. package/dist/DateInput/MonthScroller.d.ts.map +1 -0
  66. package/dist/DateInput/MonthScroller.js +658 -0
  67. package/dist/DateInput/MonthYearWheels.d.ts +24 -0
  68. package/dist/DateInput/MonthYearWheels.d.ts.map +1 -0
  69. package/dist/DateInput/MonthYearWheels.js +114 -0
  70. package/dist/DateInput/TouchDateField.d.ts +10 -0
  71. package/dist/DateInput/TouchDateField.d.ts.map +1 -0
  72. package/dist/DateInput/TouchDateField.js +810 -0
  73. package/dist/DateInput/Wheel.d.ts +41 -0
  74. package/dist/DateInput/Wheel.d.ts.map +1 -0
  75. package/dist/DateInput/Wheel.js +315 -0
  76. package/dist/DateInput/monthGeometry.d.ts +65 -0
  77. package/dist/DateInput/monthGeometry.d.ts.map +1 -0
  78. package/dist/DateInput/monthGeometry.js +98 -0
  79. package/dist/DateInput/tokens.stylex.d.ts +63 -0
  80. package/dist/DateInput/tokens.stylex.d.ts.map +1 -0
  81. package/dist/DateInput/tokens.stylex.js +74 -0
  82. package/dist/DateInput/useOwnScrollGesture.d.ts +41 -0
  83. package/dist/DateInput/useOwnScrollGesture.d.ts.map +1 -0
  84. package/dist/DateInput/useOwnScrollGesture.js +202 -0
  85. package/dist/DateInput/usePointerDragScroll.d.ts +15 -0
  86. package/dist/DateInput/usePointerDragScroll.d.ts.map +1 -0
  87. package/dist/DateInput/usePointerDragScroll.js +216 -0
  88. package/dist/DateInput/useScrollSettle.d.ts +26 -0
  89. package/dist/DateInput/useScrollSettle.d.ts.map +1 -0
  90. package/dist/DateInput/useScrollSettle.js +149 -0
  91. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  92. package/dist/DateRangeInput/DateRangeInput.js +17 -14
  93. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  94. package/dist/DateTimeInput/DateTimeInput.js +17 -14
  95. package/dist/Dialog/Dialog.d.ts.map +1 -1
  96. package/dist/Dialog/Dialog.js +3 -3
  97. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  98. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +2 -2
  99. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  100. package/dist/DropdownMenu/DropdownMenuItem.js +2 -2
  101. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
  102. package/dist/DropdownMenu/DropdownMenuRadioItem.js +2 -2
  103. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  104. package/dist/DropdownMenu/DropdownMenuSubMenu.js +2 -2
  105. package/dist/Field/FieldLabel.d.ts.map +1 -1
  106. package/dist/Field/FieldLabel.js +4 -4
  107. package/dist/Field/InputClearButton.d.ts.map +1 -1
  108. package/dist/Field/InputClearButton.js +6 -0
  109. package/dist/Field/inputStyles.stylex.d.ts +1 -1
  110. package/dist/Field/inputStyles.stylex.d.ts.map +1 -1
  111. package/dist/Field/inputStyles.stylex.js +5 -5
  112. package/dist/FileInput/FileInput.d.ts.map +1 -1
  113. package/dist/FileInput/FileInput.js +6 -6
  114. package/dist/HoverCard/HoverCard.d.ts +13 -3
  115. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  116. package/dist/HoverCard/HoverCard.js +2 -0
  117. package/dist/HoverCard/index.d.ts +1 -1
  118. package/dist/HoverCard/index.d.ts.map +1 -1
  119. package/dist/HoverCard/useHoverCard.d.ts +17 -2
  120. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  121. package/dist/HoverCard/useHoverCard.js +78 -9
  122. package/dist/InputGroup/InputGroup.js +1 -1
  123. package/dist/Item/Item.d.ts +9 -1
  124. package/dist/Item/Item.d.ts.map +1 -1
  125. package/dist/Item/Item.js +52 -14
  126. package/dist/Layer/index.d.ts +4 -2
  127. package/dist/Layer/index.d.ts.map +1 -1
  128. package/dist/Layer/index.js +2 -0
  129. package/dist/Layer/useLayer.d.ts.map +1 -1
  130. package/dist/Layer/useLayer.js +3 -2
  131. package/dist/Layer/useTouchTrigger.d.ts +113 -0
  132. package/dist/Layer/useTouchTrigger.d.ts.map +1 -0
  133. package/dist/Layer/useTouchTrigger.js +241 -0
  134. package/dist/Layout/container.stylex.d.ts +4 -2
  135. package/dist/Layout/container.stylex.d.ts.map +1 -1
  136. package/dist/Layout/container.stylex.js +19 -11
  137. package/dist/Layout/index.d.ts +1 -0
  138. package/dist/Layout/index.d.ts.map +1 -1
  139. package/dist/Layout/index.js +4 -0
  140. package/dist/Layout/padding.stylex.d.ts +315 -12
  141. package/dist/Layout/padding.stylex.d.ts.map +1 -1
  142. package/dist/Layout/padding.stylex.js +389 -12
  143. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  144. package/dist/Lightbox/Lightbox.js +5 -4
  145. package/dist/Link/Link.d.ts.map +1 -1
  146. package/dist/Link/Link.js +6 -6
  147. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  148. package/dist/MetadataList/MetadataList.js +1 -1
  149. package/dist/MobileNav/MobileNav.d.ts +1 -0
  150. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  151. package/dist/MobileNav/MobileNav.js +24 -6
  152. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  153. package/dist/MultiSelector/MultiSelector.js +8 -8
  154. package/dist/NavItem/navItemStyles.stylex.d.ts +4 -4
  155. package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
  156. package/dist/NavItem/navItemStyles.stylex.js +4 -4
  157. package/dist/NavMenu/NavHeadingMenuItem.d.ts.map +1 -1
  158. package/dist/NavMenu/NavHeadingMenuItem.js +3 -3
  159. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  160. package/dist/NumberInput/NumberInput.js +36 -8
  161. package/dist/Outline/Outline.d.ts.map +1 -1
  162. package/dist/Outline/Outline.js +3 -3
  163. package/dist/Pagination/Pagination.d.ts.map +1 -1
  164. package/dist/Pagination/Pagination.js +2 -2
  165. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  166. package/dist/PowerSearch/PowerSearch.js +11 -12
  167. package/dist/PowerSearch/PowerSearchToken.d.ts.map +1 -1
  168. package/dist/PowerSearch/PowerSearchToken.js +3 -1
  169. package/dist/PowerSearch/formatFilterValue.d.ts +11 -2
  170. package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
  171. package/dist/PowerSearch/formatFilterValue.js +23 -8
  172. package/dist/RadioList/RadioListItem.d.ts +2 -2
  173. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  174. package/dist/RadioList/RadioListItem.js +70 -40
  175. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  176. package/dist/Resizable/ResizeHandle.js +7 -7
  177. package/dist/Section/Section.d.ts +36 -1
  178. package/dist/Section/Section.d.ts.map +1 -1
  179. package/dist/Section/Section.js +12 -2
  180. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  181. package/dist/SegmentedControl/SegmentedControlItem.js +7 -2
  182. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  183. package/dist/SelectableCard/SelectableCard.js +3 -3
  184. package/dist/Selector/Selector.d.ts +26 -1
  185. package/dist/Selector/Selector.d.ts.map +1 -1
  186. package/dist/Selector/Selector.js +93 -20
  187. package/dist/Selector/SelectorOption.d.ts +14 -1
  188. package/dist/Selector/SelectorOption.d.ts.map +1 -1
  189. package/dist/Selector/SelectorOption.js +6 -0
  190. package/dist/Selector/SelectorRowLayoutContext.d.ts +8 -0
  191. package/dist/Selector/SelectorRowLayoutContext.d.ts.map +1 -0
  192. package/dist/Selector/SelectorRowLayoutContext.js +27 -0
  193. package/dist/Selector/types.d.ts +1 -0
  194. package/dist/Selector/types.d.ts.map +1 -1
  195. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  196. package/dist/SideNav/SideNavHeading.js +4 -4
  197. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  198. package/dist/SideNav/SideNavItem.js +3 -3
  199. package/dist/Slider/Slider.d.ts.map +1 -1
  200. package/dist/Slider/Slider.js +7 -7
  201. package/dist/Stack/Stack.d.ts +23 -1
  202. package/dist/Stack/Stack.d.ts.map +1 -1
  203. package/dist/Stack/Stack.js +12 -6
  204. package/dist/Switch/Switch.d.ts.map +1 -1
  205. package/dist/Switch/Switch.js +2 -2
  206. package/dist/TabList/Tab.d.ts.map +1 -1
  207. package/dist/TabList/Tab.js +1 -1
  208. package/dist/TabList/TabMenu.d.ts.map +1 -1
  209. package/dist/TabList/TabMenu.js +3 -3
  210. package/dist/Table/TableRow.js +4 -4
  211. package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
  212. package/dist/Table/plugins/columnResize/useTableColumnResize.js +1 -1
  213. package/dist/Table/plugins/filtering/useTableFiltering.d.ts.map +1 -1
  214. package/dist/Table/plugins/filtering/useTableFiltering.js +2 -2
  215. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  216. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +2 -2
  217. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  218. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +2 -2
  219. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  220. package/dist/Table/plugins/sortable/useTableSortable.js +1 -1
  221. package/dist/Table/plugins/sortable/useTableSortableState.d.ts +5 -3
  222. package/dist/Table/plugins/sortable/useTableSortableState.d.ts.map +1 -1
  223. package/dist/Table/plugins/sortable/useTableSortableState.js +12 -9
  224. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
  225. package/dist/Table/plugins/tree/useTableTreeData.js +3 -3
  226. package/dist/TextArea/TextArea.js +1 -1
  227. package/dist/TextInput/TextInput.js +1 -1
  228. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  229. package/dist/Thumbnail/Thumbnail.js +3 -3
  230. package/dist/TimeInput/TimeInput.js +2 -2
  231. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  232. package/dist/Timestamp/Timestamp.js +6 -4
  233. package/dist/Timestamp/formatInstant.d.ts +3 -2
  234. package/dist/Timestamp/formatInstant.d.ts.map +1 -1
  235. package/dist/Timestamp/formatInstant.js +8 -8
  236. package/dist/Timestamp/tooltipEntries.d.ts +2 -1
  237. package/dist/Timestamp/tooltipEntries.d.ts.map +1 -1
  238. package/dist/Timestamp/tooltipEntries.js +5 -3
  239. package/dist/Toast/Toast.d.ts +4 -3
  240. package/dist/Toast/Toast.d.ts.map +1 -1
  241. package/dist/Toast/Toast.js +10 -7
  242. package/dist/Token/Token.d.ts.map +1 -1
  243. package/dist/Token/Token.js +6 -6
  244. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  245. package/dist/Tokenizer/Tokenizer.js +1 -1
  246. package/dist/Tooltip/Tooltip.d.ts +16 -3
  247. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  248. package/dist/Tooltip/Tooltip.js +2 -0
  249. package/dist/Tooltip/index.d.ts +1 -1
  250. package/dist/Tooltip/index.d.ts.map +1 -1
  251. package/dist/Tooltip/useTooltip.d.ts +21 -2
  252. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  253. package/dist/Tooltip/useTooltip.js +71 -13
  254. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  255. package/dist/TopNav/TopNavHeading.js +4 -4
  256. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  257. package/dist/TopNav/TopNavItem.js +3 -3
  258. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  259. package/dist/TopNav/TopNavMegaMenu.js +2 -2
  260. package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
  261. package/dist/TopNav/TopNavMegaMenuItem.js +2 -2
  262. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  263. package/dist/TopNav/TopNavMenu.js +4 -4
  264. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  265. package/dist/TreeList/TreeListItem.js +7 -7
  266. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  267. package/dist/Typeahead/BaseTypeahead.js +2 -2
  268. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  269. package/dist/Typeahead/Typeahead.js +1 -1
  270. package/dist/astryx.css +124 -62
  271. package/dist/hooks/containerReveal.stylex.d.ts.map +1 -1
  272. package/dist/hooks/containerReveal.stylex.js +18 -18
  273. package/dist/hooks/scrollbarGutter.d.ts +28 -0
  274. package/dist/hooks/scrollbarGutter.d.ts.map +1 -0
  275. package/dist/hooks/scrollbarGutter.js +103 -0
  276. package/dist/hooks/useAutoMediaMode.d.ts +31 -0
  277. package/dist/hooks/useAutoMediaMode.d.ts.map +1 -0
  278. package/dist/hooks/useAutoMediaMode.js +201 -0
  279. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  280. package/dist/hooks/useInputStatusIcon.js +1 -1
  281. package/dist/hooks/useListFocus.d.ts +5 -2
  282. package/dist/hooks/useListFocus.d.ts.map +1 -1
  283. package/dist/hooks/useListFocus.js +12 -6
  284. package/dist/hooks/useScrollLock.d.ts +4 -0
  285. package/dist/hooks/useScrollLock.d.ts.map +1 -1
  286. package/dist/hooks/useScrollLock.js +13 -1
  287. package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
  288. package/dist/i18n/InternationalizationContext.js +2 -0
  289. package/dist/i18n/index.d.ts +6 -0
  290. package/dist/i18n/index.d.ts.map +1 -1
  291. package/dist/i18n/index.js +6 -0
  292. package/dist/i18n/useCollator.d.ts +12 -0
  293. package/dist/i18n/useCollator.d.ts.map +1 -0
  294. package/dist/i18n/useCollator.js +40 -0
  295. package/dist/i18n/useLocale.d.ts +3 -0
  296. package/dist/i18n/useLocale.d.ts.map +1 -0
  297. package/dist/i18n/useLocale.js +31 -0
  298. package/dist/theme/MediaTheme.d.ts +25 -5
  299. package/dist/theme/MediaTheme.d.ts.map +1 -1
  300. package/dist/theme/MediaTheme.js +31 -6
  301. package/dist/theme/defineTheme.d.ts +5 -0
  302. package/dist/theme/defineTheme.d.ts.map +1 -1
  303. package/dist/theme/defineTheme.js +5 -0
  304. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  305. package/dist/theme/derivedVarRegistry.js +7 -0
  306. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  307. package/dist/theme/generateThemeRules.js +66 -5
  308. package/dist/theme/index.d.ts +1 -1
  309. package/dist/theme/index.d.ts.map +1 -1
  310. package/dist/theme/syntax/index.d.ts +6 -0
  311. package/dist/theme/syntax/index.d.ts.map +1 -1
  312. package/dist/theme/syntax/index.js +7 -2
  313. package/dist/utils/dateParser.d.ts +4 -3
  314. package/dist/utils/dateParser.d.ts.map +1 -1
  315. package/dist/utils/dateParser.js +8 -8
  316. package/dist/utils/index.d.ts +1 -1
  317. package/dist/utils/index.d.ts.map +1 -1
  318. package/dist/utils/index.js +1 -1
  319. package/dist/utils/plainDate.d.ts +5 -2
  320. package/dist/utils/plainDate.d.ts.map +1 -1
  321. package/dist/utils/plainDate.js +20 -4
  322. package/locales/en.json +28 -0
  323. package/locales/pseudo.json +21 -0
  324. package/package.json +3 -3
  325. package/src/Avatar/Avatar.doc.mjs +10 -6
  326. package/src/Avatar/Avatar.test.tsx +219 -1
  327. package/src/Avatar/Avatar.tsx +179 -37
  328. package/src/Avatar/AvatarStatusDot.tsx +30 -4
  329. package/src/Avatar/AvatarStatusLabelContext.ts +47 -0
  330. package/src/AvatarGroup/AvatarGroup.doc.mjs +10 -6
  331. package/src/AvatarGroup/AvatarGroup.test.tsx +62 -0
  332. package/src/AvatarGroup/AvatarGroup.tsx +3 -1
  333. package/src/AvatarGroup/AvatarGroupOverflow.tsx +31 -8
  334. package/src/Banner/Banner.doc.mjs +2 -0
  335. package/src/Blockquote/Blockquote.doc.mjs +30 -7
  336. package/src/Blockquote/Blockquote.test.tsx +34 -3
  337. package/src/Blockquote/Blockquote.tsx +11 -9
  338. package/src/BottomSheet/BottomSheet.doc.mjs +3 -3
  339. package/src/BottomSheet/BottomSheet.test.tsx +24 -0
  340. package/src/BottomSheet/BottomSheetPanel.test.tsx +91 -0
  341. package/src/BottomSheet/BottomSheetPanel.tsx +58 -3
  342. package/src/Breadcrumbs/BreadcrumbItem.tsx +36 -23
  343. package/src/Breadcrumbs/Breadcrumbs.doc.mjs +26 -2
  344. package/src/Breadcrumbs/Breadcrumbs.test.tsx +31 -0
  345. package/src/Button/Button.tsx +12 -9
  346. package/src/Calendar/Calendar.tsx +15 -9
  347. package/src/Calendar/hooks/useCalendarNavigation.ts +6 -4
  348. package/src/Calendar/styles.ts +8 -5
  349. package/src/Center/Center.doc.mjs +48 -0
  350. package/src/Center/Center.test.tsx +158 -0
  351. package/src/Center/Center.tsx +50 -9
  352. package/src/Chat/ChatComposer.tsx +11 -5
  353. package/src/Chat/ChatComposerDrawer.tsx +4 -1
  354. package/src/Chat/ChatToolCalls.tsx +13 -4
  355. package/src/Chat/{useChatDictation.test.ts → useChatDictation.test.tsx} +29 -14
  356. package/src/Chat/useChatDictation.ts +1 -1
  357. package/src/Chat/useSpeechRecognition.ts +5 -3
  358. package/src/Chat/useTriggerMenu.tsx +4 -1
  359. package/src/CheckboxInput/CheckboxInput.tsx +5 -2
  360. package/src/Citation/Citation.test.tsx +6 -1
  361. package/src/Citation/Citation.tsx +11 -5
  362. package/src/ClickableCard/ClickableCard.tsx +7 -4
  363. package/src/CodeBlock/CodeBlock.tsx +4 -1
  364. package/src/Collapsible/Collapsible.tsx +5 -2
  365. package/src/CommandPalette/CommandPaletteItem.tsx +6 -3
  366. package/src/ComplexSelector/ComplexSelector.doc.mjs +10 -0
  367. package/src/ComplexSelector/ComplexSelector.tsx +14 -7
  368. package/src/DateInput/DateInput.test.tsx +17 -1
  369. package/src/DateInput/DateInput.tsx +93 -21
  370. package/src/DateInput/DateInputTouch.test.tsx +2582 -0
  371. package/src/DateInput/MonthScroller.tsx +878 -0
  372. package/src/DateInput/MonthYearWheels.tsx +147 -0
  373. package/src/DateInput/TouchDateField.tsx +1223 -0
  374. package/src/DateInput/Wheel.tsx +473 -0
  375. package/src/DateInput/monthGeometry.ts +118 -0
  376. package/src/DateInput/tokens.stylex.ts +99 -0
  377. package/src/DateInput/useOwnScrollGesture.ts +227 -0
  378. package/src/DateInput/usePointerDragScroll.ts +220 -0
  379. package/src/DateInput/useScrollSettle.ts +164 -0
  380. package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
  381. package/src/DateRangeInput/DateRangeInput.tsx +31 -13
  382. package/src/DateTimeInput/DateTimeInput.tsx +25 -9
  383. package/src/Dialog/Dialog.test.tsx +24 -0
  384. package/src/Dialog/Dialog.tsx +3 -0
  385. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +5 -2
  386. package/src/DropdownMenu/DropdownMenuItem.tsx +5 -2
  387. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +5 -2
  388. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +5 -2
  389. package/src/Field/Field.doc.mjs +2 -0
  390. package/src/Field/FieldLabel.tsx +9 -3
  391. package/src/Field/InputClearButton.test.tsx +110 -3
  392. package/src/Field/InputClearButton.tsx +38 -0
  393. package/src/Field/inputStyles.stylex.ts +17 -13
  394. package/src/FileInput/FileInput.tsx +15 -8
  395. package/src/FormLayout/FormLayout.doc.mjs +1 -1
  396. package/src/HoverCard/HoverCard.doc.mjs +18 -0
  397. package/src/HoverCard/HoverCard.test.tsx +189 -0
  398. package/src/HoverCard/HoverCard.tsx +22 -2
  399. package/src/HoverCard/index.ts +1 -0
  400. package/src/HoverCard/useHoverCard.doc.mjs +2 -0
  401. package/src/HoverCard/useHoverCard.tsx +103 -7
  402. package/src/InputGroup/InputGroup.tsx +1 -1
  403. package/src/Item/Item.doc.mjs +1 -0
  404. package/src/Item/Item.test.tsx +50 -0
  405. package/src/Item/Item.tsx +54 -6
  406. package/src/Layer/index.ts +10 -0
  407. package/src/Layer/useLayer.tsx +13 -2
  408. package/src/Layer/useTouchTrigger.test.ts +70 -0
  409. package/src/Layer/useTouchTrigger.ts +366 -0
  410. package/src/Layout/Layout.doc.mjs +18 -0
  411. package/src/Layout/container.stylex.ts +11 -3
  412. package/src/Layout/index.ts +4 -0
  413. package/src/Layout/overlayPaddingReset.test.tsx +222 -0
  414. package/src/Layout/padding.stylex.ts +192 -12
  415. package/src/Lightbox/Lightbox.tsx +11 -3
  416. package/src/Link/Link.tsx +9 -6
  417. package/src/MetadataList/MetadataList.tsx +4 -1
  418. package/src/MobileNav/MobileNav.tsx +61 -5
  419. package/src/MobileNav/MobileNavEntryAnimation.test.tsx +212 -0
  420. package/src/MobileNav/MobileNavScrollbarGutter.test.tsx +109 -0
  421. package/src/MultiSelector/MultiSelector.test.tsx +1 -1
  422. package/src/MultiSelector/MultiSelector.tsx +26 -10
  423. package/src/NavItem/navItemStyles.stylex.ts +7 -4
  424. package/src/NavMenu/NavHeadingMenuItem.tsx +6 -3
  425. package/src/NumberInput/NumberInput.doc.mjs +24 -0
  426. package/src/NumberInput/NumberInput.test.tsx +138 -0
  427. package/src/NumberInput/NumberInput.tsx +47 -8
  428. package/src/Outline/Outline.tsx +5 -2
  429. package/src/Pagination/Pagination.tsx +5 -2
  430. package/src/Popover/Popover.test.tsx +27 -1
  431. package/src/PowerSearch/PowerSearch.tsx +17 -8
  432. package/src/PowerSearch/PowerSearchToken.tsx +3 -0
  433. package/src/PowerSearch/formatFilterValue.test.ts +22 -2
  434. package/src/PowerSearch/formatFilterValue.ts +31 -6
  435. package/src/RadioList/RadioList.doc.mjs +1 -1
  436. package/src/RadioList/RadioList.test.tsx +170 -2
  437. package/src/RadioList/RadioListItem.tsx +88 -53
  438. package/src/Resizable/ResizeHandle.test.tsx +22 -0
  439. package/src/Resizable/ResizeHandle.tsx +25 -7
  440. package/src/Section/Section.doc.mjs +55 -0
  441. package/src/Section/Section.test.tsx +201 -4
  442. package/src/Section/Section.tsx +69 -0
  443. package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
  444. package/src/SegmentedControl/SegmentedControlItem.tsx +15 -3
  445. package/src/SelectableCard/SelectableCard.tsx +6 -3
  446. package/src/Selector/Selector.doc.mjs +16 -2
  447. package/src/Selector/Selector.test.tsx +525 -1
  448. package/src/Selector/Selector.tsx +180 -25
  449. package/src/Selector/SelectorOption.doc.mjs +7 -0
  450. package/src/Selector/SelectorOption.tsx +21 -0
  451. package/src/Selector/SelectorRowLayoutContext.ts +32 -0
  452. package/src/Selector/types.ts +4 -4
  453. package/src/SideNav/SideNavHeading.tsx +14 -5
  454. package/src/SideNav/SideNavItem.tsx +9 -3
  455. package/src/Slider/Slider.tsx +25 -6
  456. package/src/Stack/Stack.doc.mjs +72 -0
  457. package/src/Stack/Stack.test.tsx +156 -0
  458. package/src/Stack/Stack.tsx +50 -8
  459. package/src/Switch/Switch.tsx +5 -2
  460. package/src/TabList/Tab.tsx +4 -1
  461. package/src/TabList/TabMenu.tsx +9 -3
  462. package/src/Table/TableRow.tsx +4 -4
  463. package/src/Table/plugins/columnResize/useTableColumnResize.tsx +6 -2
  464. package/src/Table/plugins/filtering/useTableFiltering.tsx +4 -1
  465. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +10 -3
  466. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +6 -3
  467. package/src/Table/plugins/sortable/useTableSortable.tsx +4 -1
  468. package/src/Table/plugins/sortable/useTableSortableState.test.tsx +34 -0
  469. package/src/Table/plugins/sortable/useTableSortableState.tsx +26 -23
  470. package/src/Table/plugins/tree/useTableTreeData.tsx +10 -4
  471. package/src/TextArea/TextArea.doc.mjs +1 -1
  472. package/src/TextArea/TextArea.tsx +1 -1
  473. package/src/TextInput/TextInput.tsx +1 -1
  474. package/src/Thumbnail/Thumbnail.tsx +9 -3
  475. package/src/TimeInput/TimeInput.tsx +1 -1
  476. package/src/Timestamp/Timestamp.test.tsx +27 -7
  477. package/src/Timestamp/Timestamp.tsx +8 -4
  478. package/src/Timestamp/formatInstant.ts +9 -7
  479. package/src/Timestamp/tooltipEntries.test.ts +11 -6
  480. package/src/Timestamp/tooltipEntries.ts +6 -2
  481. package/src/Toast/Toast.tsx +9 -7
  482. package/src/Token/Token.tsx +14 -5
  483. package/src/Tokenizer/Tokenizer.tsx +4 -1
  484. package/src/Tooltip/Tooltip.doc.mjs +18 -0
  485. package/src/Tooltip/Tooltip.test.tsx +254 -1
  486. package/src/Tooltip/Tooltip.tsx +22 -2
  487. package/src/Tooltip/index.ts +1 -0
  488. package/src/Tooltip/useTooltip.doc.mjs +2 -0
  489. package/src/Tooltip/useTooltip.tsx +99 -21
  490. package/src/TopNav/TopNavHeading.tsx +13 -4
  491. package/src/TopNav/TopNavItem.tsx +6 -3
  492. package/src/TopNav/TopNavMegaMenu.tsx +5 -2
  493. package/src/TopNav/TopNavMegaMenuItem.tsx +5 -2
  494. package/src/TopNav/TopNavMenu.tsx +10 -4
  495. package/src/TreeList/TreeListItem.tsx +22 -7
  496. package/src/Typeahead/BaseTypeahead.tsx +5 -2
  497. package/src/Typeahead/Typeahead.tsx +4 -1
  498. package/src/hooks/containerReveal.stylex.ts +54 -18
  499. package/src/hooks/scrollbarGutter.test.ts +167 -0
  500. package/src/hooks/scrollbarGutter.ts +121 -0
  501. package/src/hooks/useAutoMediaMode.test.ts +109 -0
  502. package/src/hooks/useAutoMediaMode.ts +233 -0
  503. package/src/hooks/useInputStatusIcon.tsx +4 -1
  504. package/src/hooks/useListFocus.doc.mjs +2 -2
  505. package/src/hooks/useListFocus.test.tsx +65 -3
  506. package/src/hooks/useListFocus.ts +15 -7
  507. package/src/hooks/useScrollLock.doc.mjs +4 -4
  508. package/src/hooks/useScrollLock.test.ts +73 -0
  509. package/src/hooks/useScrollLock.ts +14 -0
  510. package/src/i18n/InternationalizationContext.ts +2 -0
  511. package/src/i18n/__tests__/e2e-powersearch.test.tsx +37 -1
  512. package/src/i18n/__tests__/useCollator.test.tsx +115 -0
  513. package/src/i18n/__tests__/useLocale.test.tsx +52 -0
  514. package/src/i18n/index.ts +6 -0
  515. package/src/i18n/useCollator.doc.mjs +84 -0
  516. package/src/i18n/useCollator.ts +44 -0
  517. package/src/i18n/useLocale.doc.mjs +70 -0
  518. package/src/i18n/useLocale.ts +34 -0
  519. package/src/i18n/useTranslator.doc.mjs +1 -1
  520. package/src/reset.css +9 -2
  521. package/src/reset.test.ts +22 -0
  522. package/src/theme/MediaTheme.doc.mjs +23 -4
  523. package/src/theme/MediaTheme.dom.test.tsx +97 -0
  524. package/src/theme/MediaTheme.tsx +42 -7
  525. package/src/theme/defineTheme.test.ts +9 -3
  526. package/src/theme/defineTheme.ts +5 -0
  527. package/src/theme/derivedVarRegistry.test.ts +19 -5
  528. package/src/theme/derivedVarRegistry.ts +4 -0
  529. package/src/theme/generateThemeRules.test.ts +118 -0
  530. package/src/theme/generateThemeRules.ts +83 -5
  531. package/src/theme/index.ts +1 -1
  532. package/src/theme/syntax/index.ts +6 -2
  533. package/src/theme/syntax/serverSafeSyntax.test.ts +68 -0
  534. package/src/utils/dateParser.test.ts +20 -0
  535. package/src/utils/dateParser.ts +14 -7
  536. package/src/utils/index.ts +2 -0
  537. package/src/utils/plainDate.test.ts +6 -0
  538. package/src/utils/plainDate.ts +21 -4
@@ -0,0 +1,1223 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file TouchDateField.tsx
7
+ * @input Uses React, Field, BottomSheet, Button, Icon, Calendar hooks, MonthScroller, MonthYearWheels
8
+ * @output Exports TouchDateField — the touch surface behind DateInput
9
+ * @position Internal component; consumed by DateInput.tsx
10
+ *
11
+ * The touch half of `DateInput`, holding `DateInput`'s whole prop
12
+ * contract so the two are interchangeable. Everything field-shaped —
13
+ * `Field` wrapper, status treatment, optimistic `changeAction`, the
14
+ * disabled-reason tooltip, `InputGroup` membership — behaves exactly as it
15
+ * does on the desktop control; only the picker differs.
16
+ *
17
+ * ## The closed field is deliberately the same control
18
+ *
19
+ * It is a real `<input>`, not a button: same element, same `role="combobox"`,
20
+ * same border, same clear button, so `ref` (typed `Ref<HTMLInputElement>` by
21
+ * `DateInputProps`) is honestly a reference to an input, the label's `for`
22
+ * names it natively, and the switch between surfaces moves nothing on screen.
23
+ *
24
+ * It just cannot be typed into: `readOnly` blocks entry, and `inputMode="none"`
25
+ * stops the virtual keyboard from opening over the sheet. Text entry is the
26
+ * one part of the desktop control that has no place here — the keyboard it
27
+ * summons would cover the picker it is meant to fill in.
28
+ *
29
+ * ## Three ideas in the picker
30
+ *
31
+ * 1. One month per screen. Every pane is exactly the height of the scrollport
32
+ * and snaps to its start, so the picker is a fixed height and there is no
33
+ * resting position showing half of two months. See MonthScroller.
34
+ * 2. Swiping is the month control, and the arrows are the backup. A flick
35
+ * reaches a neighbouring month in the direction you already think of it;
36
+ * the pair of arrows in the header's trailing corner is there for a
37
+ * deliberate single step, and for anyone not swiping at all.
38
+ * 3. The title is the escape hatch. Tap it and the same box becomes a month
39
+ * wheel and a year wheel — a flick each to reach 2019 instead of forty.
40
+ *
41
+ * SYNC: When modified, update these files to stay in sync:
42
+ * - /packages/core/src/DateInput/DateInput.tsx
43
+ * - /packages/core/src/DateInput/DateInput.doc.mjs
44
+ * - /packages/core/src/DateInput/DateInputTouch.test.tsx
45
+ */
46
+
47
+ import {
48
+ useCallback,
49
+ useEffect,
50
+ useId,
51
+ useMemo,
52
+ useOptimistic,
53
+ useRef,
54
+ useState,
55
+ useTransition,
56
+ } from 'react';
57
+ import * as stylex from '@stylexjs/stylex';
58
+ import {BottomSheet} from '../BottomSheet';
59
+ import {Button} from '../Button';
60
+ import {useCalendarConstraints} from '../Calendar';
61
+ import type {DateInputProps} from './DateInput';
62
+ import {
63
+ Field,
64
+ InputClearButton,
65
+ inputWrapperStyles,
66
+ inputStatusBorderStyles,
67
+ inputStatusHoverShadowStyles,
68
+ inputStatusFocusWithinStyles,
69
+ } from '../Field';
70
+ import {useInputStatusIcon} from '../hooks';
71
+ import {useResolvedRequired} from '../hooks/useResolvedRequired';
72
+ import {Icon} from '../Icon';
73
+ import {IconButton} from '../IconButton';
74
+ import {useTranslator} from '../i18n';
75
+ import {useInputGroup} from '../InputGroup';
76
+ import {groupStyles} from '../InputGroup/groupStyles';
77
+ import {stableClassName} from '../naming';
78
+ import {useSize} from '../SizeContext';
79
+ import {Spinner} from '../Spinner';
80
+ import {
81
+ colorVars,
82
+ spacingVars,
83
+ radiusVars,
84
+ sizeVars,
85
+ borderVars,
86
+ fontWeightVars,
87
+ typeScaleVars,
88
+ typographyVars,
89
+ durationVars,
90
+ easeVars,
91
+ } from '../theme/tokens.stylex';
92
+ import {useTooltip} from '../Tooltip';
93
+ import {VisuallyHidden} from '../VisuallyHidden';
94
+ import {
95
+ focusOutlineStyles,
96
+ getInputARIA,
97
+ isImeKeyEvent,
98
+ mergeProps,
99
+ mergeRefs,
100
+ rtlStyles,
101
+ themeProps,
102
+ formatSharedDate,
103
+ plainDateFromISO,
104
+ plainDateToday,
105
+ plainDateFormat,
106
+ DATE_FORMAT_MONTH_YEAR,
107
+ DATE_FORMAT_WEEKDAY_ONLY,
108
+ type ISODateString,
109
+ } from '../utils';
110
+ import {normalizeDayOfWeek} from '../utils/dateTypes';
111
+ import {MonthScroller, type MonthScrollerHandle} from './MonthScroller';
112
+ import {MonthYearWheels} from './MonthYearWheels';
113
+ import {
114
+ DEFAULT_MONTH_REACH,
115
+ clampIndex,
116
+ fromMonthIndex,
117
+ monthIndexOf,
118
+ } from './monthGeometry';
119
+ import {dateInputTouchSizes, dateInputTouchGeometry} from './tokens.stylex';
120
+
121
+ /**
122
+ * The comfortable minimum tap target on both iOS and Android. Applied as a
123
+ * FLOOR under the size prop rather than replacing it: `size` still means what
124
+ * it means, it just cannot produce a control a thumb misses.
125
+ */
126
+ const TOUCH_TARGET = dateInputTouchSizes.daySize;
127
+
128
+ /**
129
+ * The whole surface swap, in one leg.
130
+ *
131
+ * It used to be two: the outgoing surface faded out, then the incoming one
132
+ * faded in, 110ms each. The sequencing existed for one reason — the wheels'
133
+ * panel was transparent, so overlapping the two put the wheels' translucent
134
+ * selection band over the calendar grid and tinted a band-shaped strip of it,
135
+ * which read as "the grey area animates differently from the content".
136
+ *
137
+ * Giving the wheels an opaque background removes the reason. Nothing shows
138
+ * through them, so they can simply fade in ON TOP of a calendar that does not
139
+ * move at all — no empty middle, no outgoing animation, and one duration
140
+ * instead of a wait plus a fade.
141
+ *
142
+ * `--duration-fast` rather than a literal now that this is a whole duration
143
+ * and not half of one.
144
+ */
145
+ const SWAP_DURATION = durationVars['--duration-fast'];
146
+
147
+ const sizeStyles = stylex.create({
148
+ sm: {
149
+ height: sizeVars['--size-element-sm'],
150
+ minWidth: 180,
151
+ minBlockSize: {default: null, '@media (pointer: coarse)': TOUCH_TARGET},
152
+ },
153
+ md: {
154
+ height: sizeVars['--size-element-md'],
155
+ minWidth: 180,
156
+ minBlockSize: {default: null, '@media (pointer: coarse)': TOUCH_TARGET},
157
+ },
158
+ lg: {
159
+ height: sizeVars['--size-element-lg'],
160
+ minWidth: 180,
161
+ minBlockSize: {default: null, '@media (pointer: coarse)': TOUCH_TARGET},
162
+ },
163
+ });
164
+
165
+ const styles = stylex.create({
166
+ // ---- the closed field ----
167
+ wrapper: {
168
+ gap: spacingVars['--spacing-2'],
169
+ },
170
+ iconButton: {
171
+ display: 'flex',
172
+ alignItems: 'center',
173
+ justifyContent: 'center',
174
+ padding: 0,
175
+ margin: 0,
176
+ borderWidth: 0,
177
+ borderStyle: 'none',
178
+ backgroundColor: 'transparent',
179
+ cursor: {
180
+ default: 'pointer',
181
+ ':is(:disabled,[aria-disabled="true"])': 'default',
182
+ },
183
+ borderRadius: radiusVars['--radius-element'],
184
+ },
185
+ iconButtonDisabled: {
186
+ cursor: 'default',
187
+ },
188
+ input: {
189
+ display: 'block',
190
+ flex: 1,
191
+ minWidth: 0,
192
+ borderWidth: 0,
193
+ borderStyle: 'none',
194
+ padding: 0,
195
+ fontFamily: typographyVars['--font-family-body'],
196
+ // Below 16px iOS zooms the page on focus. The field is focusable even
197
+ // though it is not typable, so it needs the same floor DateInput has.
198
+ fontSize: {
199
+ default: typeScaleVars['--text-body-size'],
200
+ '@media (pointer: coarse)': `max(1rem, ${typeScaleVars['--text-body-size']})`,
201
+ },
202
+ lineHeight: typeScaleVars['--text-body-leading'],
203
+ color: colorVars['--color-text-primary'],
204
+ backgroundColor: 'transparent',
205
+ outline: 'none',
206
+ // It opens a picker; it does not take text. The caret would say otherwise.
207
+ caretColor: 'transparent',
208
+ cursor: {
209
+ default: 'pointer',
210
+ ':is(:disabled,[aria-disabled="true"])': 'default',
211
+ },
212
+ userSelect: 'none',
213
+ '::placeholder': {
214
+ color: colorVars['--color-text-secondary'],
215
+ },
216
+ },
217
+ inputDisabled: {
218
+ cursor: 'default',
219
+ },
220
+
221
+ // ---- the picker surface ----
222
+ surface: {
223
+ display: 'flex',
224
+ flexDirection: 'column',
225
+ inlineSize: '100%',
226
+ },
227
+ header: {
228
+ display: 'flex',
229
+ alignItems: 'center',
230
+ justifyContent: 'space-between',
231
+ gap: spacingVars['--spacing-2'],
232
+ blockSize: sizeVars['--size-element-lg'],
233
+ // No inline padding of its own: the content box owns the inset, and any
234
+ // extra here would push the arrows off the line the day grid sits on.
235
+ },
236
+ /**
237
+ * Both arrows together, at the trailing corner. `IconButton` draws each
238
+ * one — a hand-rolled button had the glyph off-centre, and matching
239
+ * Button's optical centring by hand is exactly the sort of thing a shared
240
+ * component is for.
241
+ */
242
+ monthArrows: {
243
+ display: 'flex',
244
+ alignItems: 'center',
245
+ gap: spacingVars['--spacing-0-5'],
246
+ // The pair is the trailing item; the title takes the space before it.
247
+ marginInlineStart: 'auto',
248
+ // The plate starts below the header, so these two are the one part of the
249
+ // calendar the layer above cannot cover. They fade on its timing instead,
250
+ // both directions, so the change reads as one motion rather than chrome
251
+ // blinking out a beat before the grid is covered.
252
+ transitionProperty: 'opacity, visibility',
253
+ transitionDuration: SWAP_DURATION,
254
+ transitionTimingFunction: 'linear',
255
+ '@media (prefers-reduced-motion: reduce)': {
256
+ transitionDuration: '0.01s',
257
+ },
258
+ },
259
+ /**
260
+ * Hidden while the wheels are up: they step the calendar, and the calendar
261
+ * is not on screen. Hidden rather than unmounted because they are the
262
+ * tallest thing in the header — dropping them would shorten it by 8px and
263
+ * shift the whole sheet just as the panels cross-fade.
264
+ *
265
+ * `visibility: hidden` also takes them out of the tab order and the
266
+ * accessibility tree, so there is nothing to reach that cannot be seen.
267
+ */
268
+ /**
269
+ * An arrow with nowhere to go. Hidden, not unmounted, and not merely
270
+ * disabled — it keeps its 44px so the remaining arrow does not slide
271
+ * sideways as the range's edge is reached, and `visibility: hidden` takes
272
+ * it out of the tab order and the accessibility tree so nothing invisible
273
+ * is reachable.
274
+ */
275
+ monthArrowUnavailable: {
276
+ visibility: 'hidden',
277
+ },
278
+ monthArrowsHidden: {
279
+ visibility: 'hidden',
280
+ opacity: 0,
281
+ pointerEvents: 'none',
282
+ },
283
+ /**
284
+ * `Button`'s own sizes top out at 36px, which is fine for a mouse and short
285
+ * of the 44px every other target in this sheet honours. Floor it on a
286
+ * coarse pointer, the same way the field and the day cells do.
287
+ */
288
+ monthArrow: {
289
+ minBlockSize: {default: null, '@media (pointer: coarse)': TOUCH_TARGET},
290
+ minInlineSize: {default: null, '@media (pointer: coarse)': TOUCH_TARGET},
291
+ },
292
+ /**
293
+ * The wrapper the RTL mirror rides on has to be a flex box. A bare inline
294
+ * span puts the glyph on the text baseline, which lifts it a few px off the
295
+ * button's optical centre — the whole reason these are `IconButton`s now.
296
+ * Core's Calendar carries the identical `navIcon` rule.
297
+ */
298
+ monthArrowIcon: {
299
+ display: 'inline-flex',
300
+ },
301
+ /**
302
+ * The month and year, and the toggle into the wheels. Leading, so it reads
303
+ * first and sits on the same line as the day grid below it.
304
+ */
305
+ title: {
306
+ display: 'flex',
307
+ alignItems: 'center',
308
+ gap: spacingVars['--spacing-1'],
309
+ blockSize: '100%',
310
+ paddingInline: spacingVars['--spacing-2'],
311
+ // Pulls the text back onto the grid's line: the button's own padding
312
+ // would otherwise inset the label past it.
313
+ marginInlineStart: `calc(-1 * ${spacingVars['--spacing-2']})`,
314
+ borderWidth: 0,
315
+ borderStyle: 'none',
316
+ borderRadius: radiusVars['--radius-element'],
317
+ backgroundColor: {
318
+ default: 'transparent',
319
+ '@media (hover: hover)': {
320
+ default: 'transparent',
321
+ ':hover:where(:not(:disabled,[aria-disabled="true"]))':
322
+ colorVars['--color-overlay-hover'],
323
+ },
324
+ ':active': colorVars['--color-overlay-pressed'],
325
+ },
326
+ color: colorVars['--color-text-primary'],
327
+ fontSize: typeScaleVars['--text-large-size'],
328
+ fontWeight: fontWeightVars['--font-weight-semibold'],
329
+ cursor: {
330
+ default: 'pointer',
331
+ ':is(:disabled,[aria-disabled="true"])': 'default',
332
+ },
333
+ whiteSpace: 'nowrap',
334
+ },
335
+ titleChevron: {
336
+ display: 'inline-flex',
337
+ // The one part of the swap that keeps `--ease-standard`, because it is
338
+ // the one part that travels: a rotation has a distance to cover, and
339
+ // fast-out-slow-in is what that curve is for. Same duration as the
340
+ // surface it announces, so the two land together.
341
+ transitionProperty: 'transform',
342
+ transitionDuration: SWAP_DURATION,
343
+ transitionTimingFunction: easeVars['--ease-standard'],
344
+ '@media (prefers-reduced-motion: reduce)': {
345
+ transitionDuration: '0.01s',
346
+ },
347
+ },
348
+ titleChevronOpen: {
349
+ transform: 'rotate(180deg)',
350
+ },
351
+ weekdays: {
352
+ display: 'grid',
353
+ gridTemplateColumns: 'repeat(7, 1fr)',
354
+ blockSize: sizeVars['--size-element-sm'],
355
+ alignItems: 'center',
356
+ // Same as the arrows: above the plate's reach, so it fades on the layer's
357
+ // timing rather than clearing on its own.
358
+ transitionProperty: 'opacity, visibility',
359
+ transitionDuration: SWAP_DURATION,
360
+ transitionTimingFunction: 'linear',
361
+ '@media (prefers-reduced-motion: reduce)': {
362
+ transitionDuration: '0.01s',
363
+ },
364
+ },
365
+ weekdaysHidden: {
366
+ // Hidden, not unmounted: the row still owes the surface its height, or
367
+ // opening the wheels would make the picker shorter.
368
+ visibility: 'hidden',
369
+ opacity: 0,
370
+ },
371
+ weekday: {
372
+ textAlign: 'center',
373
+ fontSize: typeScaleVars['--text-supporting-size'],
374
+ fontWeight: fontWeightVars['--font-weight-normal'],
375
+ color: colorVars['--color-text-secondary'],
376
+ },
377
+ body: {
378
+ display: 'grid',
379
+ blockSize: dateInputTouchGeometry.paneBlockSize,
380
+ position: 'relative',
381
+ },
382
+ /**
383
+ * The two panels share one grid cell, and the wheels are the one on top.
384
+ */
385
+ panel: {
386
+ gridArea: '1 / 1',
387
+ minWidth: 0,
388
+ },
389
+ /**
390
+ * The layer underneath — the calendar, and the calendar's footer actions.
391
+ *
392
+ * It has no opacity and never fades. `visibility` is the only thing that
393
+ * moves, and giving it the layer's own duration is what times it: CSS
394
+ * interpolates `visibility` discretely, holding `visible` for the whole
395
+ * transition whenever either end is `visible` and taking the final value
396
+ * only at the finish. So it disappears exactly when the cover completes,
397
+ * and on the way back it is there from the first frame, revealed as the
398
+ * layer above fades off it. Nothing about it is ever seen changing.
399
+ *
400
+ * It is not merely decorative to hide it: `inert` keeps it off the tab
401
+ * order, but a layer that is only COVERED is still `visible` to a screen
402
+ * reader, so the two footer actions would both be announced.
403
+ *
404
+ * `visibility` (not `display`) also keeps the month scroller laid out while
405
+ * the wheels are up, so its scroll offset survives the round trip and the
406
+ * wheels can steer it before it is shown again.
407
+ */
408
+ panelBeneath: {
409
+ transitionProperty: 'visibility',
410
+ transitionDuration: SWAP_DURATION,
411
+ '@media (prefers-reduced-motion: reduce)': {
412
+ transitionDuration: '0.01s',
413
+ },
414
+ },
415
+ panelBeneathHidden: {
416
+ visibility: 'hidden',
417
+ pointerEvents: 'none',
418
+ },
419
+ /**
420
+ * The month and year, as one layer that fades in and out on top.
421
+ *
422
+ * The calendar underneath does not fade with it — this layer is opaque, so
423
+ * covering it is enough, and animating it too would be animating the date
424
+ * picker rather than the thing arriving over it.
425
+ *
426
+ * Three properties carry the whole design:
427
+ *
428
+ * `backgroundColor` is what makes the fade uniform. Without it the wheels
429
+ * are a translucent selection band and some text, each compositing against
430
+ * a live calendar grid on its own terms — so the band area faded unlike the
431
+ * rest and read as "the grey area animates differently from the content".
432
+ * With the plate inside the fading group, the group renders opaque first
433
+ * and the fade applies to the finished image, so every pixel of it crosses
434
+ * at the same rate.
435
+ *
436
+ * `isolation` is what makes the plate actually cover. Backgrounds and text
437
+ * paint in separate phases, so without a stacking context a later sibling's
438
+ * background lands UNDER an earlier sibling's text — the plate went in
439
+ * opaque and the calendar's day numbers showed straight through it. Easy to
440
+ * lose, because any opacity below 1 makes a stacking context anyway: the
441
+ * cover only breaks at the two ends of the fade, where opacity is exactly
442
+ * 1, which is to say whenever anyone is actually looking.
443
+ *
444
+ * `visibility` rides along with `opacity`, on the same rule as the layer
445
+ * beneath: it stays `visible` for the whole transition, so fading out is
446
+ * seen rather than cut on the first frame, and the layer still leaves the
447
+ * a11y tree and the hit testing at the end of it.
448
+ *
449
+ * Easing is `linear`, not `--ease-standard`. That token is
450
+ * `cubic-bezier(0.24, 1, 0.4, 1)`, which is right for something travelling
451
+ * a distance and wrong for a fade: measured, it put the opacity at 50% in
452
+ * 91ms and 95% in 241ms of a 410ms transition, so the fade was over long
453
+ * before the duration was, and lengthening the duration bought an
454
+ * imperceptible tail rather than a slower fade. A fade has no distance to
455
+ * cover, so its progress should be its progress.
456
+ */
457
+ panelOverlay: {
458
+ backgroundColor: colorVars['--color-background-surface'],
459
+ isolation: 'isolate',
460
+ transitionProperty: 'opacity, visibility',
461
+ transitionDuration: SWAP_DURATION,
462
+ transitionTimingFunction: 'linear',
463
+ '@media (prefers-reduced-motion: reduce)': {
464
+ transitionDuration: '0.01s',
465
+ },
466
+ },
467
+ panelOverlayHidden: {
468
+ visibility: 'hidden',
469
+ opacity: 0,
470
+ pointerEvents: 'none',
471
+ },
472
+ footer: {
473
+ paddingBlockStart: spacingVars['--spacing-2'],
474
+ // Same reason as the header: the content box owns the inline inset, so
475
+ // Done's edge lines up with the grid's rather than sitting 4px inside it.
476
+ // One grid cell, so the two actions stack and the row is as tall as one
477
+ // button whichever is showing.
478
+ display: 'grid',
479
+ gridTemplateColumns: '1fr',
480
+ },
481
+ /**
482
+ * One footer action. Both occupy the same cell, and the wheels' one is a
483
+ * layer over the calendar's pair exactly as the panels above are. The row
484
+ * never changes height either way.
485
+ */
486
+ footerAction: {
487
+ gridArea: '1 / 1',
488
+ // Side by side and equal: the calendar's cell holds Reset and Save, the
489
+ // wheels' holds one button. `1fr` each rather than content width, so
490
+ // neither label's length decides how the row is divided.
491
+ display: 'flex',
492
+ gap: spacingVars['--spacing-2'],
493
+ },
494
+ sheetBody: {
495
+ // One inset on every edge. The block-start is the exception and has to
496
+ // be: the sheet's grab handle floats out of flow, costing no layout
497
+ // height of its own, so the content wrapper owes it the 24px it occupies
498
+ // — which reads as the same inset, because the handle sits in it.
499
+ paddingInline: spacingVars['--spacing-4'],
500
+ paddingBlockStart: spacingVars['--spacing-6'],
501
+ paddingBlockEnd: spacingVars['--spacing-4'],
502
+ },
503
+ divider: {
504
+ blockSize: borderVars['--border-width'],
505
+ backgroundColor: colorVars['--color-border'],
506
+ marginBlockStart: spacingVars['--spacing-1'],
507
+ },
508
+ });
509
+
510
+ /**
511
+ * The touch surface. Takes `DateInput`'s props verbatim; see
512
+ * {@link DateInput} for when it is chosen over the desktop control.
513
+ */
514
+ export function TouchDateField({
515
+ label,
516
+ isLabelHidden = false,
517
+ description,
518
+ isOptional = false,
519
+ isRequired = false,
520
+ isDisabled = false,
521
+ disabledMessage,
522
+ value,
523
+ onChange,
524
+ changeAction,
525
+ isLoading = false,
526
+ min,
527
+ max,
528
+ dateConstraints,
529
+ placeholder: placeholderFromProps,
530
+ size: sizeProp,
531
+ status,
532
+ statusVariant = 'attached',
533
+ labelTooltip,
534
+ hasClear = false,
535
+ // Desktop-only: the scroller is a single continuously paged column, so a
536
+ // second month would be the month already one flick away. Accepted (the
537
+ // prop types are shared) and ignored.
538
+ numberOfMonths: _numberOfMonths,
539
+ weekStartsOn: weekStartsOnProp = 0,
540
+ format = 'date_long',
541
+ width,
542
+ xstyle,
543
+ className,
544
+ style,
545
+ ref,
546
+ ...rest
547
+ }: DateInputProps) {
548
+ const t = useTranslator();
549
+ const isEffectivelyRequired = useResolvedRequired({isRequired, isOptional});
550
+ const placeholder =
551
+ placeholderFromProps ?? t('@astryx.dateInput.placeholder');
552
+ const size = useSize(sizeProp, 'md');
553
+ const weekStartsOn = normalizeDayOfWeek(weekStartsOnProp);
554
+
555
+ const id = useId();
556
+ const inputLabelID = useId();
557
+ const descriptionID = useId();
558
+ const statusMessageID = useId();
559
+ const inputRef = useRef<HTMLInputElement | null>(null);
560
+ const inputGroup = useInputGroup();
561
+
562
+ const [, startTransition] = useTransition();
563
+ const [optimisticValue, setOptimisticValue] = useOptimistic(value);
564
+ const isBusy = isLoading || optimisticValue !== value;
565
+ const isEffectivelyDisabled = isDisabled || isBusy;
566
+
567
+ // Disabled-reason tooltip, same contract as DateInput: a disabled control
568
+ // swallows pointer events, so the listeners attach to the wrapper and the
569
+ // input stays focusable via aria-disabled rather than the disabled
570
+ // attribute. Only the persistent disabled state surfaces a reason, never
571
+ // the transient busy one.
572
+ const showsDisabledMessage = isDisabled && !!disabledMessage;
573
+ const disabledMessageTooltip = useTooltip({
574
+ placement: 'above',
575
+ focusTrigger: 'always',
576
+ isEnabled: showsDisabledMessage,
577
+ });
578
+
579
+ const {isDateDisabled} = useCalendarConstraints({min, max, dateConstraints});
580
+
581
+ const {statusIcon, describedBy: statusTooltipDescribedBy} =
582
+ useInputStatusIcon({
583
+ status,
584
+ statusVariant,
585
+ isInGroup: !!inputGroup,
586
+ });
587
+
588
+ const {ariaLabelledBy, ariaDescribedBy} = getInputARIA(
589
+ inputLabelID,
590
+ [
591
+ description ? descriptionID : null,
592
+ statusVariant !== 'tooltip' && status?.message ? statusMessageID : null,
593
+ statusTooltipDescribedBy,
594
+ showsDisabledMessage ? disabledMessageTooltip.describedBy : null,
595
+ ],
596
+ inputGroup,
597
+ );
598
+
599
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
600
+ const [isWheelOpen, setIsWheelOpen] = useState(false);
601
+ const scrollerHandleRef = useRef<MonthScrollerHandle | null>(null);
602
+
603
+ const today = useMemo(() => plainDateToday(), []);
604
+ const selectedDate = useMemo(
605
+ () =>
606
+ optimisticValue != null && /^\d{4}-\d{2}-\d{2}$/.test(optimisticValue)
607
+ ? plainDateFromISO(optimisticValue)
608
+ : null,
609
+ [optimisticValue],
610
+ );
611
+
612
+ // The reachable range. Explicit bounds win; otherwise the scroller reaches a
613
+ // century in each direction from wherever it opened. Anchored once, in a
614
+ // state initializer: recomputing it as the selection moves would shift every
615
+ // pane's scroll offset under the user mid-gesture.
616
+ const [anchorMonthIndex] = useState(() =>
617
+ monthIndexOf(
618
+ value != null && /^\d{4}-\d{2}-\d{2}$/.test(value)
619
+ ? plainDateFromISO(value)
620
+ : plainDateToday(),
621
+ ),
622
+ );
623
+ const minMonthIndex =
624
+ min != null
625
+ ? monthIndexOf(plainDateFromISO(min))
626
+ : anchorMonthIndex - DEFAULT_MONTH_REACH;
627
+ const maxMonthIndex =
628
+ max != null
629
+ ? monthIndexOf(plainDateFromISO(max))
630
+ : anchorMonthIndex + DEFAULT_MONTH_REACH;
631
+
632
+ const [monthIndex, setMonthIndex] = useState(() =>
633
+ clampIndex(anchorMonthIndex, minMonthIndex, maxMonthIndex),
634
+ );
635
+
636
+ const {year, month} = fromMonthIndex(monthIndex);
637
+
638
+ /**
639
+ * Three-letter weekday names — "Sun", not Calendar's "Su".
640
+ *
641
+ * The sheet is full width and the columns are ~51px, so there is room for
642
+ * the form people actually read, and a picker operated by thumb should not
643
+ * make anyone decode "Tu" against "Th".
644
+ *
645
+ * This is CLDR's `abbreviated` width, which `Intl` produces natively — no
646
+ * truncation, so the 28 non-English locales stay correct rather than being
647
+ * sliced to three characters. (Verified against the CLDR tables for all 30
648
+ * locales in the catalog.) Calendar's 2-letter row is CLDR's *short* width,
649
+ * which `Intl` cannot express, which is exactly why that surface needs a
650
+ * generated table and this one does not.
651
+ *
652
+ * Built here rather than taken from `useCalendarDays`, which supplies the
653
+ * short form for Calendar's own header. The rotation matches: day 4 of
654
+ * January 1970 was a Sunday, so offsetting from it by `weekStartsOn` walks
655
+ * the week in the same order the panes lay out their columns.
656
+ */
657
+ const dayNames = useMemo(
658
+ () =>
659
+ Array.from({length: 7}, (_, offset) =>
660
+ plainDateFormat(
661
+ {year: 1970, month: 1, day: 4 + ((weekStartsOn + offset) % 7)},
662
+ DATE_FORMAT_WEEKDAY_ONLY,
663
+ ),
664
+ ),
665
+ [weekStartsOn],
666
+ );
667
+ const monthYearLabel = plainDateFormat(
668
+ {year, month, day: 1},
669
+ DATE_FORMAT_MONTH_YEAR,
670
+ );
671
+
672
+ // Formats the committed value only. A function format is called with the ISO
673
+ // value; a named one reuses Timestamp's shared date mapping, so the same
674
+ // literal renders the same shape here and on the desktop control.
675
+ const displayValue =
676
+ optimisticValue != null && /^\d{4}-\d{2}-\d{2}$/.test(optimisticValue)
677
+ ? typeof format === 'function'
678
+ ? format(optimisticValue)
679
+ : formatSharedDate(plainDateFromISO(optimisticValue), format)
680
+ : '';
681
+
682
+ const fireChange = useCallback(
683
+ (newValue: ISODateString | undefined) => {
684
+ if (isBusy) {
685
+ return;
686
+ }
687
+ onChange?.(newValue);
688
+ if (changeAction) {
689
+ startTransition(async () => {
690
+ setOptimisticValue(newValue);
691
+ await changeAction(newValue);
692
+ });
693
+ }
694
+ },
695
+ [isBusy, onChange, changeAction, startTransition, setOptimisticValue],
696
+ );
697
+
698
+ const openSheet = useCallback(() => {
699
+ if (!isEffectivelyDisabled) {
700
+ // Always onto the calendar, whatever was showing last time. The wheels
701
+ // are a detour taken to reach a far month, not a mode to be left in:
702
+ // reopening into them would answer a question the user has not asked
703
+ // yet, and hide the dates they came back for behind another tap.
704
+ setIsWheelOpen(false);
705
+ setIsSheetOpen(true);
706
+ }
707
+ }, [isEffectivelyDisabled]);
708
+
709
+ const handleClear = useCallback(() => {
710
+ fireChange(undefined);
711
+ inputRef.current?.focus();
712
+ }, [fireChange]);
713
+
714
+ /**
715
+ * Put the picker back to how it opens: no date, current month.
716
+ *
717
+ * Two things, because clearing a date and then being left staring at the
718
+ * month of the date you just cleared is a half-finished action — the
719
+ * calendar should look the way it does before anything is chosen.
720
+ *
721
+ * "If possible" is load-bearing: a range can exclude the current month
722
+ * entirely (a booking window starting next quarter), and there is no
723
+ * honest place to go in that case. `clampIndex` would silently land on the
724
+ * nearest edge, which is a different month presented as if it were today's,
725
+ * so the move is skipped instead and the calendar stays where it is. The
726
+ * value is still cleared either way — that half never depends on the range.
727
+ */
728
+ const handleResetInSheet = useCallback(() => {
729
+ fireChange(undefined);
730
+ const currentMonth = monthIndexOf(today);
731
+ if (currentMonth < minMonthIndex || currentMonth > maxMonthIndex) {
732
+ return;
733
+ }
734
+ if (currentMonth === monthIndex) {
735
+ return;
736
+ }
737
+ setMonthIndex(currentMonth);
738
+ scrollerHandleRef.current?.scrollToMonth(currentMonth, 'smooth');
739
+ }, [fireChange, today, monthIndex, minMonthIndex, maxMonthIndex]);
740
+
741
+ // Selection commits on the tap and leaves the sheet up, so a mistake can be
742
+ // corrected in place and a nearby date reconsidered without reopening.
743
+ // Dismissal is the footer's Done (and the handle, the scrim, Escape) — none
744
+ // of which commit anything, because this already has.
745
+ const handleSelect = useCallback(
746
+ (next: ISODateString) => {
747
+ fireChange(next);
748
+ },
749
+ [fireChange],
750
+ );
751
+
752
+ // Whether there is anywhere to step. An arrow with nowhere to go is hidden
753
+ // rather than disabled: a disabled control still says "this is a thing you
754
+ // could do", and at the end of a range it is not — the range is the whole
755
+ // truth, and there is no state the user can reach where it becomes
756
+ // available. A greyed chevron sitting there permanently reads as broken.
757
+ const canStepBack = monthIndex > minMonthIndex;
758
+ const canStepForward = monthIndex < maxMonthIndex;
759
+
760
+ // One month either way, clamped to the reachable range. Goes through the
761
+ // same scrollToMonth the swipe settles on, so the arrows and the gesture
762
+ // cannot disagree about where a month rests.
763
+ const stepMonth = useCallback(
764
+ (delta: number) => {
765
+ const target = clampIndex(
766
+ monthIndex + delta,
767
+ minMonthIndex,
768
+ maxMonthIndex,
769
+ );
770
+ if (target === monthIndex) {
771
+ return;
772
+ }
773
+ setMonthIndex(target);
774
+ scrollerHandleRef.current?.scrollToMonth(target, 'smooth');
775
+ },
776
+ [monthIndex, minMonthIndex, maxMonthIndex],
777
+ );
778
+
779
+ // A wheel commit steers the scroller immediately, even though it is behind
780
+ // the wheels: it keeps its layout box while hidden, so by the time the
781
+ // wheels close it is already resting on the new month.
782
+ const handleWheelChange = useCallback((next: number) => {
783
+ setMonthIndex(next);
784
+ scrollerHandleRef.current?.scrollToMonth(next, 'auto');
785
+ }, []);
786
+
787
+ /**
788
+ * The calendar reports the month it has scrolled to — but only while it is
789
+ * the surface being scrolled.
790
+ *
791
+ * While the wheels are up the wheels are the source of truth, and the
792
+ * calendar is being STEERED by them: a wheel commit calls `scrollToMonth`
793
+ * above, the calendar scrolls, and it would report that month straight back
794
+ * here. That closes a cycle — wheel commits, calendar echoes, the echo moves
795
+ * the wheel's selected row, the wheel is repositioned onto it, and the
796
+ * resulting scroll reads as another commit.
797
+ *
798
+ * Whether that cycle converges depends on how precisely a browser reports
799
+ * "scrolling stopped", which is not something to rely on. With `scrollend`
800
+ * (Chrome) it settles at once; on iOS below Safari 26 there is no
801
+ * `scrollend` and momentum runs on for a second or more after the finger
802
+ * lifts, so each lap committed the next month along and the value climbed
803
+ * on its own. Ignoring the echo removes the cycle instead of damping it.
804
+ */
805
+ const handleVisibleMonthChange = useCallback(
806
+ (next: number) => {
807
+ if (isWheelOpen) {
808
+ return;
809
+ }
810
+ setMonthIndex(next);
811
+ },
812
+ [isWheelOpen],
813
+ );
814
+
815
+ /**
816
+ * Put the calendar back where it belongs when the wheels close.
817
+ *
818
+ * The wheels steer it while it is hidden, and a hidden scroller is not a
819
+ * reliable place to leave a scroll position: `visibility: hidden` keeps the
820
+ * layout box, but iOS re-snaps the scroller when it becomes visible again,
821
+ * and it does not necessarily re-snap to the pane we put it on. That fires
822
+ * a scroll at the exact moment reports start being trusted again — which is
823
+ * why the month drifted on the way back to the dates.
824
+ *
825
+ * Re-asserting is cheap when nothing moved (the scroller is already there,
826
+ * so nothing scrolls) and exactly right when something did. `scrollToMonth`
827
+ * marks the target as steered, so this correction does not report itself
828
+ * back either.
829
+ */
830
+ // Read through a ref so this effect depends on the surface change alone.
831
+ // Listing `monthIndex` would re-run it on every month the user swipes to,
832
+ // yanking the scroller back mid-gesture.
833
+ const monthIndexRef = useRef(monthIndex);
834
+ monthIndexRef.current = monthIndex;
835
+ useEffect(() => {
836
+ if (isWheelOpen) {
837
+ return;
838
+ }
839
+ scrollerHandleRef.current?.scrollToMonth(monthIndexRef.current, 'auto');
840
+ }, [isWheelOpen]);
841
+
842
+ // APG combobox keys. The field takes no text, so every printable key is
843
+ // free — but only the documented openers are wired, so a stray keystroke
844
+ // does not pop a sheet.
845
+ const handleInputKeyDown = useCallback(
846
+ (event: React.KeyboardEvent) => {
847
+ // Same guard core's pointer surface carries. This field is readOnly and
848
+ // takes no composition of its own, but an IME sitting over it still
849
+ // sends its committing Enter here first — and opening a date sheet on
850
+ // the keystroke that finishes a Korean syllable is the same wrong
851
+ // answer. See utils/ime.ts.
852
+ if (isImeKeyEvent(event.nativeEvent)) {
853
+ return;
854
+ }
855
+ if (
856
+ event.key === 'ArrowDown' ||
857
+ event.key === 'Enter' ||
858
+ event.key === ' ' ||
859
+ event.key === 'Spacebar'
860
+ ) {
861
+ event.preventDefault();
862
+ openSheet();
863
+ }
864
+ },
865
+ [openSheet],
866
+ );
867
+
868
+ const surface = (
869
+ <div {...stylex.props(styles.surface)}>
870
+ <div {...stylex.props(styles.header)}>
871
+ <button
872
+ type="button"
873
+ onClick={() => setIsWheelOpen(open => !open)}
874
+ aria-expanded={isWheelOpen}
875
+ // One string, not a template: the comma and the word order are
876
+ // the translator's to choose, not English's.
877
+ aria-label={t('@astryx.dateInput.chooseMonthYear', {
878
+ monthYear: monthYearLabel,
879
+ })}
880
+ // A `data-` hook rather than a theme target. `themeProps` would
881
+ // publish `astryx-date-input-touch-title` as a themeable selector,
882
+ // and this is internal structure of the sheet — the field and its
883
+ // toggle icon are the documented targets, and nothing has asked to
884
+ // restyle the header button. Adding a target later is additive;
885
+ // withdrawing one is not.
886
+ data-title="month-year"
887
+ {...stylex.props(styles.title, focusOutlineStyles.focusVisible)}>
888
+ <span>{monthYearLabel}</span>
889
+ <Icon
890
+ icon="chevronDown"
891
+ size="sm"
892
+ color="secondary"
893
+ // The transform rides on the Icon itself, not a wrapper span, so
894
+ // the element a theme can target is the element that moves.
895
+ xstyle={[
896
+ styles.titleChevron,
897
+ isWheelOpen && styles.titleChevronOpen,
898
+ ]}
899
+ />
900
+ </button>
901
+ {/* Both arrows at the trailing corner, as a pair — and only while the
902
+ calendar is the surface they step. `IconButton` gives them
903
+ Button's optical centring, focus ring, disabled treatment and hit
904
+ area; the hand-rolled version had the glyph off-centre.
905
+
906
+ Mirrored under RTL by the shared helper: "previous" is the earlier
907
+ month, which sits on the right when the inline axis runs that way,
908
+ and the panes mirror with it. */}
909
+ <span
910
+ data-arrows="months"
911
+ // `inert` as well as the hidden styling: the fade keeps them
912
+ // `visible` until it finishes, and a control that is on its way out
913
+ // should not answer a click or a Tab in the meantime.
914
+ inert={isWheelOpen ? true : undefined}
915
+ {...stylex.props(
916
+ styles.monthArrows,
917
+ isWheelOpen && styles.monthArrowsHidden,
918
+ )}>
919
+ <IconButton
920
+ variant="ghost"
921
+ size="sm"
922
+ xstyle={[
923
+ styles.monthArrow,
924
+ !canStepBack && styles.monthArrowUnavailable,
925
+ ]}
926
+ isDisabled={!canStepBack}
927
+ onClick={() => stepMonth(-1)}
928
+ label={t('@astryx.calendar.previousMonth')}
929
+ icon={
930
+ <span {...stylex.props(styles.monthArrowIcon, rtlStyles.mirror)}>
931
+ <Icon icon="chevronLeft" size="sm" color="inherit" />
932
+ </span>
933
+ }
934
+ />
935
+ <IconButton
936
+ variant="ghost"
937
+ size="sm"
938
+ xstyle={[
939
+ styles.monthArrow,
940
+ !canStepForward && styles.monthArrowUnavailable,
941
+ ]}
942
+ isDisabled={!canStepForward}
943
+ onClick={() => stepMonth(1)}
944
+ label={t('@astryx.calendar.nextMonth')}
945
+ icon={
946
+ <span {...stylex.props(styles.monthArrowIcon, rtlStyles.mirror)}>
947
+ <Icon icon="chevronRight" size="sm" color="inherit" />
948
+ </span>
949
+ }
950
+ />
951
+ </span>
952
+ </div>
953
+
954
+ {/* Decorative: each day carries its weekday in its accessible name, so
955
+ this row is not a header row for assistive technology — and it must
956
+ live outside the scroller, or it would scroll away with the month. */}
957
+ <div
958
+ aria-hidden="true"
959
+ {...stylex.props(
960
+ styles.weekdays,
961
+ isWheelOpen && styles.weekdaysHidden,
962
+ )}>
963
+ {dayNames.map(name => (
964
+ <div key={name} {...stylex.props(styles.weekday)}>
965
+ {name}
966
+ </div>
967
+ ))}
968
+ </div>
969
+
970
+ <div {...stylex.props(styles.body)}>
971
+ <div
972
+ data-panel="calendar"
973
+ // `inert` as well as the hidden styling: the panel keeps its layout
974
+ // box (so the scroller holds its position and the wheels can steer
975
+ // it), which means without this it would still be tabbable behind
976
+ // the layer on top.
977
+ inert={isWheelOpen ? true : undefined}
978
+ {...stylex.props(
979
+ styles.panel,
980
+ styles.panelBeneath,
981
+ isWheelOpen && styles.panelBeneathHidden,
982
+ )}>
983
+ <MonthScroller
984
+ key={`${minMonthIndex}:${maxMonthIndex}`}
985
+ handleRef={scrollerHandleRef}
986
+ minMonthIndex={minMonthIndex}
987
+ maxMonthIndex={maxMonthIndex}
988
+ initialMonthIndex={monthIndex}
989
+ onVisibleMonthChange={handleVisibleMonthChange}
990
+ selectedDate={selectedDate}
991
+ today={today}
992
+ isDateDisabled={isDateDisabled}
993
+ weekStartsOn={weekStartsOn}
994
+ onSelect={handleSelect}
995
+ />
996
+ </div>
997
+ <div
998
+ data-panel="wheels"
999
+ inert={isWheelOpen ? undefined : true}
1000
+ {...stylex.props(
1001
+ styles.panel,
1002
+ styles.panelOverlay,
1003
+ !isWheelOpen && styles.panelOverlayHidden,
1004
+ )}>
1005
+ <MonthYearWheels
1006
+ monthIndex={monthIndex}
1007
+ minMonthIndex={minMonthIndex}
1008
+ maxMonthIndex={maxMonthIndex}
1009
+ onChange={handleWheelChange}
1010
+ monthLabel={t('@astryx.dateInput.monthWheel')}
1011
+ yearLabel={t('@astryx.dateInput.yearWheel')}
1012
+ isActive={isWheelOpen}
1013
+ />
1014
+ </div>
1015
+ </div>
1016
+
1017
+ {/* Done does NOT commit: a tap on a day has already fired onChange by
1018
+ the time it is reachable. It is a close button, exactly equivalent
1019
+ to the grab handle, the scrim and Escape — which is why it is safe
1020
+ for those to remain, and why there is no Cancel to pair it with.
1021
+
1022
+ It sits alone. A "Today" button was here and only moved the calendar
1023
+ to the current month WITHOUT selecting it, which read as broken: the
1024
+ one thing a button called Today should obviously do is the thing it
1025
+ did not. Navigating and selecting are two different intents and it
1026
+ conflated them, so it is gone until it can be one or the other on
1027
+ purpose. Reaching today by scrolling still works. */}
1028
+ {/* No `inert` here: the two cells below take turns, so the footer as a
1029
+ whole is always live. It carried one while the footer was hidden
1030
+ entirely on the wheels, and leaving it behind made the wheels' own
1031
+ Done button unreachable — an inert ancestor disables everything
1032
+ inside it. */}
1033
+ <div {...stylex.props(styles.footer)}>
1034
+ {/* Both actions share one grid cell, the same way the panels above
1035
+ them do, and follow the same base/overlay rule — so the footer
1036
+ never changes height and the two never read as superimposed. Each
1037
+ belongs to a surface: Save finishes on the calendar, Done finishes
1038
+ the detour through the wheels. */}
1039
+ <div
1040
+ inert={isWheelOpen ? true : undefined}
1041
+ {...stylex.props(
1042
+ styles.footerAction,
1043
+ styles.panelBeneath,
1044
+ isWheelOpen && styles.panelBeneathHidden,
1045
+ )}>
1046
+ <Button
1047
+ variant="secondary"
1048
+ size="md"
1049
+ width="100%"
1050
+ label={t('@astryx.dateInput.resetPicking')}
1051
+ onClick={handleResetInSheet}
1052
+ />
1053
+ <Button
1054
+ variant="primary"
1055
+ // md, not sm: it is the action a thumb reaches for, so it gets
1056
+ // the comfortable size rather than the compact one the header's
1057
+ // ghost buttons use.
1058
+ size="md"
1059
+ width="100%"
1060
+ label={t('@astryx.dateInput.savePicking')}
1061
+ onClick={() => setIsSheetOpen(false)}
1062
+ />
1063
+ </div>
1064
+ <div
1065
+ inert={isWheelOpen ? undefined : true}
1066
+ {...stylex.props(
1067
+ styles.footerAction,
1068
+ styles.panelOverlay,
1069
+ !isWheelOpen && styles.panelOverlayHidden,
1070
+ )}>
1071
+ <Button
1072
+ // secondary, not primary: this one does not finish the task, it
1073
+ // finishes a step. Giving both surfaces a primary button would
1074
+ // say the wheels are somewhere you can complete from.
1075
+ variant="secondary"
1076
+ size="md"
1077
+ width="100%"
1078
+ label={t('@astryx.dateInput.doneChoosingMonth')}
1079
+ onClick={() => setIsWheelOpen(false)}
1080
+ />
1081
+ </div>
1082
+ </div>
1083
+ </div>
1084
+ );
1085
+
1086
+ const inputWrapper = (
1087
+ <div
1088
+ ref={el => {
1089
+ // Anchor + hover/focus listeners for the disabled-message tooltip.
1090
+ // Gated internally by isEnabled, so attaching unconditionally is safe.
1091
+ disabledMessageTooltip.ref(el);
1092
+ }}
1093
+ {...rest}
1094
+ {...mergeProps(
1095
+ themeProps('date-input', {
1096
+ size,
1097
+ status: status?.type ?? null,
1098
+ disabled: isDisabled ? 'disabled' : null,
1099
+ }),
1100
+ stylex.props(
1101
+ inputWrapperStyles.base,
1102
+ sizeStyles[size],
1103
+ styles.wrapper,
1104
+ isEffectivelyDisabled && inputWrapperStyles.disabled,
1105
+ status && inputStatusBorderStyles[status.type],
1106
+ status &&
1107
+ !isEffectivelyDisabled &&
1108
+ inputStatusHoverShadowStyles[status.type],
1109
+ status && inputStatusFocusWithinStyles[status.type],
1110
+ inputGroup && groupStyles.inGroup,
1111
+ xstyle,
1112
+ ),
1113
+ className,
1114
+ style,
1115
+ )}>
1116
+ {inputGroup && <VisuallyHidden id={inputLabelID}>{label}</VisuallyHidden>}
1117
+ <button
1118
+ type="button"
1119
+ onClick={openSheet}
1120
+ disabled={isEffectivelyDisabled}
1121
+ aria-label={t('@astryx.dateInput.openCalendar')}
1122
+ tabIndex={-1}
1123
+ {...stylex.props(
1124
+ focusOutlineStyles.focusVisible,
1125
+ styles.iconButton,
1126
+ isEffectivelyDisabled && styles.iconButtonDisabled,
1127
+ )}>
1128
+ <Icon
1129
+ icon="calendar"
1130
+ size="sm"
1131
+ color="secondary"
1132
+ {...themeProps('date-input-toggle-icon', {
1133
+ state: isSheetOpen ? 'expanded' : 'collapsed',
1134
+ })}
1135
+ />
1136
+ </button>
1137
+ <input
1138
+ ref={mergeRefs(ref, inputRef)}
1139
+ id={id}
1140
+ type="text"
1141
+ role="combobox"
1142
+ value={displayValue}
1143
+ // No typing on this surface: the picker is the input method, and the
1144
+ // virtual keyboard would cover the sheet it is meant to fill in.
1145
+ // readOnly blocks entry; inputMode="none" is what actually keeps the
1146
+ // keyboard down when the field takes focus.
1147
+ readOnly
1148
+ inputMode="none"
1149
+ onChange={() => {}}
1150
+ onClick={openSheet}
1151
+ onKeyDown={handleInputKeyDown}
1152
+ placeholder={placeholder}
1153
+ disabled={isEffectivelyDisabled && !showsDisabledMessage}
1154
+ aria-disabled={showsDisabledMessage ? 'true' : undefined}
1155
+ aria-labelledby={ariaLabelledBy}
1156
+ aria-describedby={ariaDescribedBy}
1157
+ aria-required={isEffectivelyRequired ? 'true' : undefined}
1158
+ aria-invalid={status?.type === 'error' ? 'true' : undefined}
1159
+ aria-busy={isBusy || undefined}
1160
+ aria-expanded={isSheetOpen}
1161
+ aria-haspopup="dialog"
1162
+ aria-autocomplete="none"
1163
+ autoComplete="off"
1164
+ {...stylex.props(
1165
+ styles.input,
1166
+ isEffectivelyDisabled && styles.inputDisabled,
1167
+ )}
1168
+ />
1169
+ {hasClear && value !== undefined && !isEffectivelyDisabled && (
1170
+ <InputClearButton
1171
+ label={t('@astryx.dateInput.clear', {label})}
1172
+ onClick={handleClear}
1173
+ iconClassName={stableClassName('date-input-clear-icon')}
1174
+ />
1175
+ )}
1176
+ {isBusy && <Spinner size="sm" />}
1177
+ {statusIcon}
1178
+ <BottomSheet
1179
+ isOpen={isSheetOpen}
1180
+ onOpenChange={setIsSheetOpen}
1181
+ label={t('@astryx.dateInput.dialogLabel')}
1182
+ // The picker is a fixed height by construction, so the sheet should be
1183
+ // exactly as tall as it is rather than claiming a viewport budget.
1184
+ height="hug">
1185
+ <div {...stylex.props(styles.sheetBody)}>{surface}</div>
1186
+ </BottomSheet>
1187
+ {showsDisabledMessage &&
1188
+ disabledMessageTooltip.renderTooltip(disabledMessage)}
1189
+ </div>
1190
+ );
1191
+
1192
+ if (inputGroup) {
1193
+ return inputWrapper;
1194
+ }
1195
+
1196
+ return (
1197
+ <Field
1198
+ label={label}
1199
+ isLabelHidden={isLabelHidden}
1200
+ description={description}
1201
+ inputID={id}
1202
+ descriptionID={description ? descriptionID : undefined}
1203
+ isOptional={isOptional}
1204
+ isRequired={isRequired}
1205
+ isDisabled={isDisabled}
1206
+ status={
1207
+ status
1208
+ ? {
1209
+ type: status.type,
1210
+ message: status.message,
1211
+ messageID: status.message ? statusMessageID : undefined,
1212
+ }
1213
+ : undefined
1214
+ }
1215
+ statusVariant={statusVariant}
1216
+ labelTooltip={labelTooltip}
1217
+ width={width}>
1218
+ {inputWrapper}
1219
+ </Field>
1220
+ );
1221
+ }
1222
+
1223
+ TouchDateField.displayName = 'TouchDateField';