@astryxdesign/core 0.7.1-canary.ff40b78 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/dist/Button/Button.d.ts +1 -1
  3. package/dist/Button/Button.d.ts.map +1 -1
  4. package/dist/Button/Button.js +5 -2
  5. package/dist/Calendar/Calendar.d.ts.map +1 -1
  6. package/dist/Calendar/Calendar.js +191 -32
  7. package/dist/Chat/ChatComposerInput.d.ts +9 -0
  8. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  9. package/dist/Chat/ChatComposerInput.js +5 -0
  10. package/dist/Chat/ChatMessage.d.ts +11 -0
  11. package/dist/Chat/ChatMessage.d.ts.map +1 -1
  12. package/dist/Chat/ChatMessage.js +18 -9
  13. package/dist/Chat/useTriggerMenu.d.ts +9 -2
  14. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  15. package/dist/Chat/useTriggerMenu.js +222 -50
  16. package/dist/CodeBlock/CodeBlock.d.ts +10 -1
  17. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  18. package/dist/CodeBlock/CodeBlock.js +158 -38
  19. package/dist/CodeBlock/tokenizer.d.ts +39 -0
  20. package/dist/CodeBlock/tokenizer.d.ts.map +1 -1
  21. package/dist/CodeBlock/tokenizer.js +108 -0
  22. package/dist/CommandPalette/CommandPalette.d.ts +9 -5
  23. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  24. package/dist/CommandPalette/CommandPalette.js +38 -20
  25. package/dist/CommandPalette/CommandPaletteEmpty.d.ts +4 -3
  26. package/dist/CommandPalette/CommandPaletteEmpty.d.ts.map +1 -1
  27. package/dist/CommandPalette/CommandPaletteEmpty.js +4 -3
  28. package/dist/CommandPalette/CommandPaletteInput.d.ts +5 -4
  29. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  30. package/dist/CommandPalette/CommandPaletteInput.js +35 -18
  31. package/dist/CommandPalette/CommandPaletteItem.d.ts +11 -6
  32. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  33. package/dist/CommandPalette/CommandPaletteItem.js +8 -9
  34. package/dist/CommandPalette/CommandPaletteList.d.ts +4 -2
  35. package/dist/CommandPalette/CommandPaletteList.d.ts.map +1 -1
  36. package/dist/CommandPalette/CommandPaletteList.js +10 -5
  37. package/dist/ComplexSelector/ComplexSelector.d.ts +8 -5
  38. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  39. package/dist/ComplexSelector/ComplexSelector.js +12 -3
  40. package/dist/ContextMenu/ContextMenu.d.ts +3 -3
  41. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  42. package/dist/ContextMenu/ContextMenu.js +62 -48
  43. package/dist/DateInput/DateInput.js +1 -1
  44. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  45. package/dist/DateInput/MonthScroller.js +12 -4
  46. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  47. package/dist/DateRangeInput/DateRangeInput.js +12 -2
  48. package/dist/Dialog/Dialog.d.ts +1 -1
  49. package/dist/Dialog/Dialog.d.ts.map +1 -1
  50. package/dist/Dialog/Dialog.js +16 -5
  51. package/dist/Dialog/DialogHeader.d.ts +1 -1
  52. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  53. package/dist/Dialog/DialogHeader.js +7 -7
  54. package/dist/Divider/Divider.d.ts +1 -0
  55. package/dist/Divider/Divider.d.ts.map +1 -1
  56. package/dist/Divider/Divider.js +5 -4
  57. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  58. package/dist/DropdownMenu/DropdownMenu.js +161 -65
  59. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +7 -5
  60. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  61. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +12 -5
  62. package/dist/Field/InputClearButton.d.ts +8 -4
  63. package/dist/Field/InputClearButton.d.ts.map +1 -1
  64. package/dist/Field/InputClearButton.js +23 -7
  65. package/dist/FormLayout/FormLayout.d.ts.map +1 -1
  66. package/dist/FormLayout/FormLayout.js +1 -0
  67. package/dist/Grid/Grid.d.ts +13 -4
  68. package/dist/Grid/Grid.d.ts.map +1 -1
  69. package/dist/Grid/Grid.js +7 -1
  70. package/dist/HoverCard/useHoverCard.d.ts +1 -1
  71. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  72. package/dist/HoverCard/useHoverCard.js +67 -49
  73. package/dist/InputGroup/InputGroup.d.ts +2 -4
  74. package/dist/InputGroup/InputGroup.d.ts.map +1 -1
  75. package/dist/InputGroup/InputGroup.js +2 -4
  76. package/dist/InputGroup/groupStyles.d.ts +10 -2
  77. package/dist/InputGroup/groupStyles.d.ts.map +1 -1
  78. package/dist/InputGroup/groupStyles.js +19 -2
  79. package/dist/Layer/useLayerDismissal.d.ts +9 -9
  80. package/dist/Layer/useLayerDismissal.js +9 -9
  81. package/dist/Markdown/Markdown.d.ts.map +1 -1
  82. package/dist/Markdown/Markdown.js +25 -4
  83. package/dist/MobileNav/MobileNav.d.ts +7 -0
  84. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  85. package/dist/MobileNav/MobileNav.js +45 -2
  86. package/dist/MultiSelector/MultiSelector.d.ts +7 -4
  87. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  88. package/dist/MultiSelector/MultiSelector.js +24 -3
  89. package/dist/NumberInput/NumberInput.js +1 -1
  90. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  91. package/dist/PowerSearch/PowerSearchEditPopover.js +6 -0
  92. package/dist/Resizable/ResizeHandle.d.ts +1 -1
  93. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  94. package/dist/Resizable/ResizeHandle.js +83 -15
  95. package/dist/Resizable/useResizable.d.ts +23 -0
  96. package/dist/Resizable/useResizable.d.ts.map +1 -1
  97. package/dist/Resizable/useResizable.js +78 -0
  98. package/dist/Selector/Selector.d.ts +6 -3
  99. package/dist/Selector/Selector.d.ts.map +1 -1
  100. package/dist/Selector/Selector.js +28 -6
  101. package/dist/SideNav/SideNav.d.ts.map +1 -1
  102. package/dist/SideNav/SideNav.js +10 -2
  103. package/dist/Slider/Slider.d.ts.map +1 -1
  104. package/dist/Slider/Slider.js +18 -7
  105. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  106. package/dist/Tokenizer/Tokenizer.js +16 -4
  107. package/dist/Toolbar/Toolbar.d.ts +2 -1
  108. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  109. package/dist/Toolbar/Toolbar.js +3 -1
  110. package/dist/TreeList/TreeList.d.ts.map +1 -1
  111. package/dist/TreeList/TreeList.js +6 -3
  112. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  113. package/dist/TreeList/TreeListItem.js +7 -1
  114. package/dist/astryx.css +22 -0
  115. package/dist/hooks/useScrollOverflow.d.ts.map +1 -1
  116. package/dist/hooks/useScrollOverflow.js +55 -12
  117. package/dist/hooks/useTreeFocus.d.ts +21 -0
  118. package/dist/hooks/useTreeFocus.d.ts.map +1 -1
  119. package/dist/hooks/useTreeFocus.js +40 -2
  120. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  121. package/dist/i18n/generated-locales/en.generated.js +3 -0
  122. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  123. package/dist/i18n/generated-locales/pseudo.generated.js +3 -0
  124. package/dist/reset.a11y.chromium.spec.js +6 -1
  125. package/dist/reset.until-found.a11y.browser.spec.d.ts +2 -0
  126. package/dist/reset.until-found.a11y.browser.spec.d.ts.map +1 -0
  127. package/dist/reset.until-found.a11y.browser.spec.js +71 -0
  128. package/dist/theme/syntax/SyntaxTheme.d.ts.map +1 -1
  129. package/dist/theme/syntax/SyntaxTheme.js +43 -2
  130. package/dist/theme/syntax/defineSyntaxTheme.d.ts +18 -5
  131. package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
  132. package/dist/theme/syntax/defineSyntaxTheme.js +38 -9
  133. package/dist/theme/syntax/index.d.ts +1 -1
  134. package/dist/theme/syntax/index.d.ts.map +1 -1
  135. package/dist/theme/syntax/tokens.d.ts +12 -2
  136. package/dist/theme/syntax/tokens.d.ts.map +1 -1
  137. package/dist/theme/syntax/tokens.js +25 -4
  138. package/dist/utils/plainDate.d.ts +12 -0
  139. package/dist/utils/plainDate.d.ts.map +1 -1
  140. package/dist/utils/plainDate.js +63 -10
  141. package/locales/en.json +13 -1
  142. package/locales/pseudo.json +9 -0
  143. package/package.json +4 -4
  144. package/src/AlertDialog/AlertDialog.doc.mjs +6 -0
  145. package/src/AlertDialog/AlertDialog.spec.md +205 -0
  146. package/src/AlertDialog/AlertDialog.test.tsx +3 -14
  147. package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +115 -0
  148. package/src/AlertDialog/__tests__/AlertDialog.a11y.renders.tsx +52 -0
  149. package/src/AlertDialog/__tests__/AlertDialog.a11y.states.ts +68 -0
  150. package/src/AlertDialog/__tests__/AlertDialog.a11y.test.tsx +144 -0
  151. package/src/Button/Button.test.tsx +37 -0
  152. package/src/Button/Button.tsx +5 -1
  153. package/src/Calendar/Calendar.doc.mjs +234 -60
  154. package/src/Calendar/Calendar.spec.md +209 -0
  155. package/src/Calendar/Calendar.test.tsx +559 -20
  156. package/src/Calendar/Calendar.tsx +258 -41
  157. package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +126 -0
  158. package/src/Calendar/__tests__/Calendar.a11y.known-failures.ts +12 -0
  159. package/src/Calendar/__tests__/Calendar.a11y.renders.tsx +57 -0
  160. package/src/Calendar/__tests__/Calendar.a11y.states.ts +139 -0
  161. package/src/Calendar/__tests__/Calendar.a11y.test.tsx +137 -0
  162. package/src/Carousel/Carousel.test.tsx +54 -71
  163. package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +192 -0
  164. package/src/Carousel/__tests__/Carousel.a11y.known-failures.ts +27 -0
  165. package/src/Carousel/__tests__/Carousel.a11y.renders.tsx +39 -0
  166. package/src/Carousel/__tests__/Carousel.a11y.states.ts +139 -0
  167. package/src/Carousel/__tests__/Carousel.a11y.test.tsx +127 -0
  168. package/src/Chat/ChatComposerInput.doc.mjs +1 -1
  169. package/src/Chat/ChatComposerInput.spec.md +94 -55
  170. package/src/Chat/ChatComposerInput.test.tsx +202 -1
  171. package/src/Chat/ChatComposerInput.tsx +13 -0
  172. package/src/Chat/ChatMessage.doc.mjs +31 -16
  173. package/src/Chat/ChatMessage.spec.md +265 -0
  174. package/src/Chat/ChatMessage.test.tsx +246 -0
  175. package/src/Chat/ChatMessage.tsx +30 -7
  176. package/src/Chat/ChatSystemMessage.spec.md +36 -37
  177. package/src/Chat/ChatSystemMessage.test.tsx +4 -4
  178. package/src/Chat/useTriggerMenu.test.tsx +994 -0
  179. package/src/Chat/useTriggerMenu.tsx +278 -46
  180. package/src/CodeBlock/CodeBlock.doc.mjs +7 -0
  181. package/src/CodeBlock/CodeBlock.spec.md +40 -41
  182. package/src/CodeBlock/CodeBlock.test.tsx +89 -9
  183. package/src/CodeBlock/CodeBlock.tsx +205 -25
  184. package/src/CodeBlock/__tests__/CodeBlockOverflowStop.a11y.chromium.spec.ts +125 -0
  185. package/src/CodeBlock/__tests__/DiffRowPaint.a11y.chromium.spec.ts +93 -0
  186. package/src/CodeBlock/tokenizer.test.ts +112 -0
  187. package/src/CodeBlock/tokenizer.ts +106 -0
  188. package/src/Collapsible/CollapsibleGroup.spec.md +17 -8
  189. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.renders.tsx +45 -0
  190. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.states.ts +139 -0
  191. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.test.tsx +135 -0
  192. package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +128 -0
  193. package/src/CommandPalette/CommandPalette.doc.mjs +126 -23
  194. package/src/CommandPalette/CommandPalette.spec.md +100 -49
  195. package/src/CommandPalette/CommandPalette.test.tsx +241 -2
  196. package/src/CommandPalette/CommandPalette.tsx +47 -21
  197. package/src/CommandPalette/CommandPaletteEmpty.tsx +4 -3
  198. package/src/CommandPalette/CommandPaletteInput.doc.mjs +61 -4
  199. package/src/CommandPalette/CommandPaletteInput.spec.md +215 -0
  200. package/src/CommandPalette/CommandPaletteInput.test.tsx +375 -51
  201. package/src/CommandPalette/CommandPaletteInput.tsx +74 -21
  202. package/src/CommandPalette/CommandPaletteItem.doc.mjs +73 -22
  203. package/src/CommandPalette/CommandPaletteItem.spec.md +223 -0
  204. package/src/CommandPalette/CommandPaletteItem.test.tsx +200 -5
  205. package/src/CommandPalette/CommandPaletteItem.tsx +16 -11
  206. package/src/CommandPalette/CommandPaletteList.doc.mjs +3 -3
  207. package/src/CommandPalette/CommandPaletteList.spec.md +41 -36
  208. package/src/CommandPalette/CommandPaletteList.test.tsx +80 -0
  209. package/src/CommandPalette/CommandPaletteList.tsx +10 -5
  210. package/src/CommandPalette/__tests__/CommandPalette.a11y.chromium.spec.ts +599 -0
  211. package/src/ComplexSelector/ComplexSelector.doc.mjs +10 -8
  212. package/src/ComplexSelector/ComplexSelector.spec.md +14 -9
  213. package/src/ComplexSelector/ComplexSelector.test.tsx +64 -16
  214. package/src/ComplexSelector/ComplexSelector.tsx +37 -8
  215. package/src/ContextMenu/ContextMenu.doc.mjs +6 -0
  216. package/src/ContextMenu/ContextMenu.spec.md +51 -53
  217. package/src/ContextMenu/ContextMenu.test.tsx +132 -1
  218. package/src/ContextMenu/ContextMenu.tsx +78 -48
  219. package/src/DateInput/DateInput.doc.mjs +52 -0
  220. package/src/DateInput/DateInput.spec.md +27 -15
  221. package/src/DateInput/DateInput.test.tsx +38 -0
  222. package/src/DateInput/DateInput.tsx +1 -1
  223. package/src/DateInput/DateInputTouch.test.tsx +22 -0
  224. package/src/DateInput/MonthScroller.tsx +12 -4
  225. package/src/DateRangeInput/DateRangeInput.test.tsx +49 -0
  226. package/src/DateRangeInput/DateRangeInput.tsx +20 -2
  227. package/src/Dialog/Dialog.test.tsx +27 -1
  228. package/src/Dialog/Dialog.tsx +16 -5
  229. package/src/Dialog/DialogHeader.test.tsx +27 -2
  230. package/src/Dialog/DialogHeader.tsx +7 -8
  231. package/src/Dialog/__tests__/Dialog.a11y.chromium.spec.ts +21 -14
  232. package/src/Divider/Divider.doc.mjs +5 -3
  233. package/src/Divider/Divider.spec.md +140 -54
  234. package/src/Divider/Divider.test.tsx +15 -0
  235. package/src/Divider/Divider.tsx +6 -4
  236. package/src/DropdownMenu/DropdownMenu.doc.mjs +2 -2
  237. package/src/DropdownMenu/DropdownMenu.spec.md +121 -45
  238. package/src/DropdownMenu/DropdownMenu.test.tsx +288 -23
  239. package/src/DropdownMenu/DropdownMenu.tsx +212 -81
  240. package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +38 -2
  241. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +32 -18
  242. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +146 -1
  243. package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +136 -0
  244. package/src/DropdownMenu/__tests__/MenuButton.a11y.known-failures.ts +12 -0
  245. package/src/DropdownMenu/__tests__/MenuButton.a11y.renders.tsx +67 -0
  246. package/src/DropdownMenu/__tests__/MenuButton.a11y.states.ts +141 -0
  247. package/src/DropdownMenu/__tests__/MenuButton.a11y.test.tsx +138 -0
  248. package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +40 -5
  249. package/src/Field/InputClearButton.test.tsx +29 -4
  250. package/src/Field/InputClearButton.tsx +26 -8
  251. package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +21 -14
  252. package/src/FormLayout/FormLayout.tsx +1 -0
  253. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +1 -1
  254. package/src/Grid/Grid.doc.mjs +14 -11
  255. package/src/Grid/Grid.spec.md +50 -32
  256. package/src/Grid/Grid.tsx +13 -4
  257. package/src/HoverCard/HoverCard.test.tsx +46 -1
  258. package/src/HoverCard/useHoverCard.tsx +68 -48
  259. package/src/InputGroup/InputGroup.doc.mjs +11 -1
  260. package/src/InputGroup/InputGroup.tsx +2 -4
  261. package/src/InputGroup/groupStyles.ts +40 -2
  262. package/src/Layer/useLayerDismissal.ts +9 -9
  263. package/src/Link/__tests__/Link.a11y.chromium.spec.ts +142 -0
  264. package/src/Link/__tests__/Link.a11y.renders.tsx +97 -0
  265. package/src/Link/__tests__/Link.a11y.states.ts +104 -0
  266. package/src/Link/__tests__/Link.a11y.test.tsx +127 -0
  267. package/src/Markdown/Markdown.doc.mjs +1 -1
  268. package/src/Markdown/Markdown.fr23.perf.test.ts +161 -40
  269. package/src/Markdown/Markdown.spec.md +45 -23
  270. package/src/Markdown/Markdown.test.tsx +38 -0
  271. package/src/Markdown/Markdown.tsx +24 -2
  272. package/src/Markdown/plugins/semanticFence.test.tsx +3 -2
  273. package/src/MobileNav/MobileNav.tsx +50 -2
  274. package/src/MobileNav/MobileNavFocus.test.tsx +553 -0
  275. package/src/MoreMenu/MoreMenu.spec.md +18 -7
  276. package/src/MoreMenu/MoreMenu.test.tsx +38 -0
  277. package/src/MultiSelector/MultiSelector.doc.mjs +8 -8
  278. package/src/MultiSelector/MultiSelector.spec.md +1 -1
  279. package/src/MultiSelector/MultiSelector.test.tsx +188 -21
  280. package/src/MultiSelector/MultiSelector.tsx +50 -11
  281. package/src/NumberInput/NumberInput.test.tsx +24 -0
  282. package/src/NumberInput/NumberInput.tsx +1 -1
  283. package/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts +22 -15
  284. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +78 -0
  285. package/src/PowerSearch/PowerSearchEditPopover.tsx +6 -0
  286. package/src/Resizable/Resizable.spec.md +41 -9
  287. package/src/Resizable/ResizeHandle.test.tsx +565 -2
  288. package/src/Resizable/ResizeHandle.tsx +126 -21
  289. package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +119 -0
  290. package/src/Resizable/__tests__/ResizeHandle.a11y.known-failures.ts +39 -0
  291. package/src/Resizable/__tests__/ResizeHandle.a11y.renders.tsx +80 -0
  292. package/src/Resizable/__tests__/ResizeHandle.a11y.states.ts +197 -0
  293. package/src/Resizable/__tests__/ResizeHandle.a11y.test.tsx +123 -0
  294. package/src/Resizable/useResizable.ts +95 -0
  295. package/src/Selector/Selector.doc.mjs +7 -6
  296. package/src/Selector/Selector.spec.md +37 -24
  297. package/src/Selector/Selector.test.tsx +176 -10
  298. package/src/Selector/Selector.tsx +59 -14
  299. package/src/Selector/__tests__/Combobox.a11y.known-failures.ts +27 -0
  300. package/src/Selector/__tests__/Combobox.a11y.states.ts +145 -0
  301. package/src/Selector/__tests__/Selector.combobox.a11y.chromium.spec.ts +108 -0
  302. package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +126 -0
  303. package/src/SideNav/SideNav.test.tsx +35 -0
  304. package/src/SideNav/SideNav.tsx +16 -2
  305. package/src/Slider/Slider.spec.md +43 -17
  306. package/src/Slider/Slider.test.tsx +134 -0
  307. package/src/Slider/Slider.tsx +29 -6
  308. package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +112 -0
  309. package/src/Slider/__tests__/Slider.a11y.known-failures.ts +12 -0
  310. package/src/Slider/__tests__/Slider.a11y.renders.tsx +42 -0
  311. package/src/Slider/__tests__/Slider.a11y.states.ts +106 -0
  312. package/src/Slider/__tests__/Slider.a11y.test.tsx +105 -0
  313. package/src/Switch/__tests__/Switch.a11y.chromium.spec.ts +25 -18
  314. package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +19 -12
  315. package/src/TextInput/__tests__/TextInput.a11y.chromium.spec.ts +21 -14
  316. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +18 -11
  317. package/src/Tokenizer/Tokenizer.test.tsx +87 -0
  318. package/src/Tokenizer/Tokenizer.tsx +31 -3
  319. package/src/Toolbar/Toolbar.test.tsx +131 -4
  320. package/src/Toolbar/Toolbar.tsx +3 -1
  321. package/src/TreeList/TreeList.spec.md +33 -8
  322. package/src/TreeList/TreeList.test.tsx +72 -0
  323. package/src/TreeList/TreeList.tsx +10 -6
  324. package/src/TreeList/TreeListItem.tsx +5 -0
  325. package/src/TreeList/__tests__/TreeList.a11y.chromium.spec.ts +125 -0
  326. package/src/TreeList/__tests__/TreeList.a11y.renders.tsx +44 -0
  327. package/src/TreeList/__tests__/TreeList.a11y.states.ts +164 -0
  328. package/src/TreeList/__tests__/TreeList.a11y.test.tsx +138 -0
  329. package/src/TreeList/__tests__/TreeListRowNames.a11y.chromium.spec.ts +82 -0
  330. package/src/hooks/useScrollOverflow.test.tsx +222 -0
  331. package/src/hooks/useScrollOverflow.ts +55 -13
  332. package/src/hooks/useTreeFocus.test.tsx +37 -0
  333. package/src/hooks/useTreeFocus.ts +55 -2
  334. package/src/i18n/generated-locales/en.generated.ts +3 -0
  335. package/src/i18n/generated-locales/pseudo.generated.ts +3 -0
  336. package/src/reset.a11y.chromium.spec.ts +9 -1
  337. package/src/reset.css +8 -3
  338. package/src/reset.until-found.a11y.browser.spec.ts +75 -0
  339. package/src/reset.until-found.test.ts +32 -0
  340. package/src/stylexValidation.source-build.test.mjs +42 -0
  341. package/src/theme/syntax/SyntaxTheme.tsx +46 -5
  342. package/src/theme/syntax/defineSyntaxTheme.diff.test.ts +91 -0
  343. package/src/theme/syntax/defineSyntaxTheme.ts +75 -15
  344. package/src/theme/syntax/index.ts +5 -1
  345. package/src/theme/syntax/tokens.ts +32 -2
  346. package/src/theme/syntax/useSyntaxTheme.diff.test.tsx +40 -0
  347. package/src/utils/plainDate.test.ts +147 -0
  348. package/src/utils/plainDate.ts +75 -10
@@ -0,0 +1,91 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, expect, it, vi} from 'vitest';
4
+ import {
5
+ defineSyntaxTheme,
6
+ REQUIRED_SYNTAX_KEYS,
7
+ syntaxThemeStyle,
8
+ syntaxThemeToCSS,
9
+ } from './defineSyntaxTheme';
10
+ import {syntaxTokenDefaults} from './tokens';
11
+ import {dracula} from './presets';
12
+
13
+ const base = Object.fromEntries(
14
+ REQUIRED_SYNTAX_KEYS.map(key => [key, '#123456']),
15
+ ) as Parameters<typeof defineSyntaxTheme>[0]['tokens'];
16
+
17
+ describe('diff row slots in a syntax theme', () => {
18
+ it('are optional: a theme that sets none keeps the semantic defaults', () => {
19
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
20
+ const theme = defineSyntaxTheme({name: 't', tokens: base});
21
+ expect(warn).not.toHaveBeenCalled();
22
+ expect(theme.tokens.added).toBe(
23
+ syntaxTokenDefaults['--color-syntax-added'],
24
+ );
25
+ expect(theme.tokens.removed).toBe(
26
+ syntaxTokenDefaults['--color-syntax-removed'],
27
+ );
28
+ // A defined theme that sets no header paint gets none: the header row
29
+ // sits on the theme's own background with punctuation text.
30
+ expect(theme.tokens['diff-header']).toBe('transparent');
31
+ // A theme that sets no marker colour keeps its own line-number colour.
32
+ expect(theme.tokens['added-marker']).toBe(
33
+ 'var(--color-syntax-punctuation)',
34
+ );
35
+ expect(theme.tokens['removed-marker']).toBe(
36
+ 'var(--color-syntax-punctuation)',
37
+ );
38
+ warn.mockRestore();
39
+ });
40
+
41
+ it('may be overridden to harmonize with the theme', () => {
42
+ const theme = defineSyntaxTheme({
43
+ name: 't',
44
+ tokens: {...base, added: ['#e6ffed', '#1b3a24'], removed: '#ffeef0'},
45
+ });
46
+ expect(theme.tokens.added).toBe('light-dark(#e6ffed, #1b3a24)');
47
+ expect(theme.tokens.removed).toBe('#ffeef0');
48
+ expect(theme.tokens['diff-header']).toBe('transparent');
49
+ });
50
+
51
+ it('skips unset diff slots in the CSS and style output of a hand-built definition', () => {
52
+ const tokens = {...dracula.tokens};
53
+ delete (tokens as Partial<typeof tokens>).added;
54
+ delete (tokens as Partial<typeof tokens>)['added-marker'];
55
+ const theme = {...dracula, tokens};
56
+ expect(syntaxThemeToCSS(theme)).not.toContain('undefined');
57
+ expect(syntaxThemeToCSS(theme)).not.toContain('--color-syntax-added:');
58
+ expect(syntaxThemeStyle(theme)).not.toHaveProperty('--color-syntax-added');
59
+ expect(syntaxThemeStyle(theme)).toHaveProperty('--color-syntax-removed');
60
+ });
61
+
62
+ it('leaves the shipped presets valid without them: row paints default, markers follow punctuation', () => {
63
+ expect(dracula.tokens.added).toBe(
64
+ syntaxTokenDefaults['--color-syntax-added'],
65
+ );
66
+ expect(dracula.tokens['added-marker']).toBe(
67
+ 'var(--color-syntax-punctuation)',
68
+ );
69
+ expect(dracula.tokens['removed-marker']).toBe(
70
+ 'var(--color-syntax-punctuation)',
71
+ );
72
+ });
73
+
74
+ it('defaults derive from the semantic tokens, never a literal colour', () => {
75
+ expect(syntaxTokenDefaults['--color-syntax-added']).toBe(
76
+ 'color-mix(in srgb, var(--color-success-muted) 25%, transparent)',
77
+ );
78
+ expect(syntaxTokenDefaults['--color-syntax-removed']).toBe(
79
+ 'color-mix(in srgb, var(--color-error-muted) 25%, transparent)',
80
+ );
81
+ expect(syntaxTokenDefaults['--color-syntax-diff-header']).toBe(
82
+ 'var(--color-background-muted)',
83
+ );
84
+ expect(syntaxTokenDefaults['--color-syntax-added-marker']).toBe(
85
+ 'var(--color-text-green)',
86
+ );
87
+ expect(syntaxTokenDefaults['--color-syntax-removed-marker']).toBe(
88
+ 'var(--color-text-red)',
89
+ );
90
+ });
91
+ });
@@ -11,7 +11,11 @@
11
11
  * @see https://github.com/facebook/astryx/issues/1148
12
12
  */
13
13
 
14
- import {syntaxTokenDefaults, type SyntaxTokenName} from './tokens';
14
+ import {
15
+ syntaxTokenDefaults,
16
+ DIFF_SYNTAX_TOKEN_NAMES,
17
+ type SyntaxTokenName,
18
+ } from './tokens';
15
19
  import {devWarn} from '../../utils/devWarning';
16
20
 
17
21
  // =============================================================================
@@ -33,7 +37,12 @@ export type SyntaxThemeTokenKey =
33
37
  | 'attribute'
34
38
  | 'property'
35
39
  | 'punctuation'
36
- | 'background';
40
+ | 'background'
41
+ | DiffSyntaxThemeTokenKey;
42
+
43
+ /** The diff row slots a syntax theme MAY set; unset ones keep their defaults. */
44
+ export type DiffSyntaxThemeTokenKey =
45
+ 'added' | 'removed' | 'diff-header' | 'added-marker' | 'removed-marker';
37
46
 
38
47
  /**
39
48
  * Token value — either a single string or a [light, dark] tuple.
@@ -41,14 +50,28 @@ export type SyntaxThemeTokenKey =
41
50
  */
42
51
  export type SyntaxTokenValue = string | [light: string, dark: string];
43
52
 
44
- /** Token map for defineSyntaxTheme input — values can be strings or tuples. */
53
+ /**
54
+ * Token map for defineSyntaxTheme input — values can be strings or tuples.
55
+ * The diff row slots are optional: a theme that sets none keeps the defaults
56
+ * (semantic success/error muted tints and the muted surface).
57
+ */
45
58
  export type SyntaxThemeTokenInput = Record<
46
- SyntaxThemeTokenKey,
59
+ Exclude<SyntaxThemeTokenKey, DiffSyntaxThemeTokenKey>,
47
60
  SyntaxTokenValue
48
- >;
61
+ > &
62
+ Partial<Record<DiffSyntaxThemeTokenKey, SyntaxTokenValue>>;
49
63
 
50
- /** Resolved token map — all values are CSS strings (tuples resolved to light-dark()). */
51
- export type SyntaxThemeTokenMap = Record<SyntaxThemeTokenKey, string>;
64
+ /**
65
+ * Resolved token map — all values are CSS strings (tuples resolved to
66
+ * light-dark()). The diff row slots are optional here as in the input, so a
67
+ * map written before they existed still type-checks; an unset slot renders
68
+ * its default.
69
+ */
70
+ export type SyntaxThemeTokenMap = Record<
71
+ Exclude<SyntaxThemeTokenKey, DiffSyntaxThemeTokenKey>,
72
+ string
73
+ > &
74
+ Partial<Record<DiffSyntaxThemeTokenKey, string>>;
52
75
 
53
76
  /** Input to defineSyntaxTheme. */
54
77
  export interface SyntaxThemeInput {
@@ -81,6 +104,22 @@ export const ALL_SYNTAX_KEYS: SyntaxThemeTokenKey[] = Object.keys(
81
104
  syntaxTokenDefaults,
82
105
  ).map(k => k.replace(CSS_PREFIX, '') as SyntaxThemeTokenKey);
83
106
 
107
+ const DIFF_KEYS = new Set<SyntaxThemeTokenKey>(
108
+ DIFF_SYNTAX_TOKEN_NAMES.map(
109
+ name => name.replace(CSS_PREFIX, '') as SyntaxThemeTokenKey,
110
+ ),
111
+ );
112
+
113
+ /** The marker slots: unset in a theme, they follow that theme's punctuation. */
114
+ const MARKER_KEYS = new Set<SyntaxThemeTokenKey>([
115
+ 'added-marker',
116
+ 'removed-marker',
117
+ ]);
118
+
119
+ /** The keys every syntax theme must set: everything but the diff rows. */
120
+ export const REQUIRED_SYNTAX_KEYS: SyntaxThemeTokenKey[] =
121
+ ALL_SYNTAX_KEYS.filter(key => !DIFF_KEYS.has(key));
122
+
84
123
  // =============================================================================
85
124
  // Helpers
86
125
  // =============================================================================
@@ -138,19 +177,37 @@ export function resolveSyntaxTokenForMode(
138
177
  export function defineSyntaxTheme(
139
178
  input: SyntaxThemeInput,
140
179
  ): SyntaxThemeDefinition {
141
- const missing = ALL_SYNTAX_KEYS.filter(key => !(key in input.tokens));
180
+ const missing = REQUIRED_SYNTAX_KEYS.filter(key => !(key in input.tokens));
142
181
  if (missing.length > 0) {
143
182
  devWarn(
144
183
  'defineSyntaxTheme',
145
184
  `"${input.name}": missing tokens: ${missing.join(', ')}. ` +
146
- `All 14 syntax tokens are required.`,
185
+ `All ${REQUIRED_SYNTAX_KEYS.length} syntax colour tokens are required; ` +
186
+ `the diff row slots (added, removed, diff-header, added-marker, removed-marker) are optional.`,
147
187
  );
148
188
  }
149
189
 
150
- // Resolve tuples to light-dark() CSS strings
190
+ // Resolve tuples to light-dark() CSS strings. An unset diff row paint keeps
191
+ // its default so every existing theme renders a readable diff unchanged; an
192
+ // unset marker slot takes this theme's own line-number colour (its
193
+ // punctuation token) rather than the page theme's green and red, and an
194
+ // unset header paint is none at all (the header row sits on this theme's
195
+ // own background with punctuation text), so number, glyph, and header
196
+ // text stay legible on this theme's background in every page mode — the
197
+ // glyph's shape carries the meaning where the colour does not.
151
198
  const resolved: Partial<SyntaxThemeTokenMap> = {};
152
199
  for (const key of ALL_SYNTAX_KEYS) {
153
- resolved[key] = resolveTokenValue(input.tokens[key]);
200
+ const value = (
201
+ input.tokens as Partial<Record<SyntaxThemeTokenKey, SyntaxTokenValue>>
202
+ )[key];
203
+ resolved[key] =
204
+ value !== undefined
205
+ ? resolveTokenValue(value)
206
+ : MARKER_KEYS.has(key)
207
+ ? 'var(--color-syntax-punctuation)'
208
+ : key === 'diff-header'
209
+ ? 'transparent'
210
+ : syntaxTokenDefaults[toCSSProperty(key)];
154
211
  }
155
212
 
156
213
  return {
@@ -170,14 +227,17 @@ export function syntaxThemeStyle(
170
227
  ): Record<string, string> {
171
228
  const vars: Record<string, string> = {};
172
229
  for (const key of ALL_SYNTAX_KEYS) {
173
- vars[toCSSProperty(key)] = theme.tokens[key];
230
+ // An unset diff slot is left to its default rather than written empty.
231
+ const value = theme.tokens[key];
232
+ if (value !== undefined) vars[toCSSProperty(key)] = value;
174
233
  }
175
234
  return vars;
176
235
  }
177
236
 
178
237
  /** Convert a syntax theme to CSS declarations (no selector wrapper). */
179
238
  export function syntaxThemeToCSS(theme: SyntaxThemeDefinition): string {
180
- return ALL_SYNTAX_KEYS.map(
181
- key => toCSSProperty(key) + ': ' + theme.tokens[key] + ';',
182
- ).join('\n ');
239
+ // An unset diff slot is left to its default rather than written `undefined`.
240
+ return ALL_SYNTAX_KEYS.filter(key => theme.tokens[key] !== undefined)
241
+ .map(key => toCSSProperty(key) + ': ' + theme.tokens[key] + ';')
242
+ .join('\n ');
183
243
  }
@@ -14,7 +14,11 @@
14
14
  export {syntaxTokenDefaults} from './tokens';
15
15
  export type {SyntaxTokenName} from './tokens';
16
16
 
17
- export {defineSyntaxTheme, syntaxThemeStyle, syntaxThemeToCSS} from './defineSyntaxTheme';
17
+ export {
18
+ defineSyntaxTheme,
19
+ syntaxThemeStyle,
20
+ syntaxThemeToCSS,
21
+ } from './defineSyntaxTheme';
18
22
  export type {
19
23
  SyntaxThemeDefinition,
20
24
  SyntaxThemeInput,
@@ -13,8 +13,11 @@
13
13
  * colors automatically adapt to any theme's color system. Themes can also
14
14
  * set an explicit syntax theme via defineTheme({ syntax: dracula }).
15
15
  *
16
- * 14-token architecture validated against 11 community code themes.
17
- * All themes map cleanly to these 14 slots.
16
+ * 14 token colours and the code background, validated against 11 community
17
+ * code themes — all of them map cleanly to those slots — plus five diff row
18
+ * slots (added, removed, header paints; added and removed markers) that default
19
+ * to semantic tokens, so a syntax theme that does not set them keeps a
20
+ * readable diff.
18
21
  *
19
22
  * @see https://github.com/facebook/astryx/issues/1148
20
23
  */
@@ -51,6 +54,33 @@ export const syntaxTokenDefaults = {
51
54
  '--color-syntax-punctuation': 'var(--color-text-secondary)',
52
55
  // background -> muted surface
53
56
  '--color-syntax-background': 'var(--color-background-muted)',
57
+ // Diff rows (CodeBlock isDiff): whole-row tints behind unchanged syntax
58
+ // colours. Added/removed derive from the semantic success/error muted
59
+ // tokens at 25% strength: at full strength the secondary-grey syntax
60
+ // tokens (operator, punctuation, comment) fall under 4.5:1 on the tints
61
+ // in light; at 25% every default token clears it on both schemes with
62
+ // margin while the tint still reads against the plain row. Header rows
63
+ // (@@, and ---/+++ before the first hunk) sit on the muted surface.
64
+ '--color-syntax-added':
65
+ 'color-mix(in srgb, var(--color-success-muted) 25%, transparent)',
66
+ '--color-syntax-removed':
67
+ 'color-mix(in srgb, var(--color-error-muted) 25%, transparent)',
68
+ '--color-syntax-diff-header': 'var(--color-background-muted)',
69
+ // The margin of a changed row — its line number and +/- glyph — in the
70
+ // default theme's tinted-text tokens, made for tinted surfaces; a defined
71
+ // syntax theme that leaves them unset follows its own punctuation colour
72
+ // (see defineSyntaxTheme). Code text keeps the syntax theme's colours.
73
+ '--color-syntax-added-marker': 'var(--color-text-green)',
74
+ '--color-syntax-removed-marker': 'var(--color-text-red)',
54
75
  } as const;
55
76
 
56
77
  export type SyntaxTokenName = keyof typeof syntaxTokenDefaults;
78
+
79
+ /** The five diff row slots: optional in a syntax theme, defaulted when unset. */
80
+ export const DIFF_SYNTAX_TOKEN_NAMES = [
81
+ '--color-syntax-added',
82
+ '--color-syntax-removed',
83
+ '--color-syntax-diff-header',
84
+ '--color-syntax-added-marker',
85
+ '--color-syntax-removed-marker',
86
+ ] as const satisfies ReadonlyArray<SyntaxTokenName>;
@@ -0,0 +1,40 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file useSyntaxTheme.diff.test.tsx
5
+ * @input A SyntaxTheme provider with a preset that sets no diff slots
6
+ * @output The hook hands a canvas consumer paintable colours for every slot
7
+ * @position Diff slot resolution for the imperative (canvas) path
8
+ */
9
+
10
+ import {render} from '@testing-library/react';
11
+ import {describe, expect, it} from 'vitest';
12
+ import {SyntaxTheme, useSyntaxTheme} from './SyntaxTheme';
13
+ import {dracula} from './presets';
14
+ import {parseColor} from '../../utils/color';
15
+
16
+ function Probe({onTokens}: {onTokens: (t: Record<string, string>) => void}) {
17
+ const syntax = useSyntaxTheme();
18
+ if (syntax) onTokens(syntax.tokens);
19
+ return null;
20
+ }
21
+
22
+ describe('useSyntaxTheme diff slots', () => {
23
+ it("resolves unset marker slots to the theme's own punctuation colour", () => {
24
+ let tokens: Record<string, string> = {};
25
+ render(
26
+ <SyntaxTheme theme={dracula}>
27
+ <Probe onTokens={t => (tokens = t)} />
28
+ </SyntaxTheme>,
29
+ );
30
+ expect(tokens.punctuation).toMatch(/^#|^rgb/);
31
+ expect(tokens['added-marker']).toBe(tokens.punctuation);
32
+ expect(tokens['removed-marker']).toBe(tokens.punctuation);
33
+ // The row paints derive from the page theme's resolved muted tokens at
34
+ // the default strength, as rgba() a canvas can paint.
35
+ expect(tokens.added).toMatch(/^rgba\(\d+, \d+, \d+, 0\.\d+\)$/);
36
+ expect(tokens.removed).toMatch(/^rgba\(\d+, \d+, \d+, 0\.\d+\)$/);
37
+ expect(parseColor(tokens.added)?.a).toBeCloseTo(0.2 * 0.25, 3);
38
+ expect(tokens['diff-header']).toBe('transparent');
39
+ });
40
+ });
@@ -30,6 +30,7 @@ import {
30
30
  formatSharedDate,
31
31
  DATE_FORMAT_WITH_WEEKDAY,
32
32
  DATE_FORMAT_MONTH_YEAR,
33
+ plainDateWeekFieldName,
33
34
  } from './plainDate';
34
35
  import type {ISODateString} from './dateTypes';
35
36
 
@@ -149,6 +150,14 @@ describe('plainDateToDate / plainDateFromDate', () => {
149
150
  expect(d.getDate()).toBe(15);
150
151
  expect(plainDateFromDate(d)).toEqual(pd);
151
152
  });
153
+
154
+ it('keeps years 1-99 instead of mapping them to 1900-1999', () => {
155
+ const pd: PlainDate = {year: 1, month: 1, day: 15};
156
+ const d = plainDateToDate(pd);
157
+ expect(d.getFullYear()).toBe(1);
158
+ expect(d.getDay()).toBe(1); // 0001-01-15 is a Monday
159
+ expect(plainDateFromDate(d)).toEqual(pd);
160
+ });
152
161
  });
153
162
 
154
163
  describe('plainDateToday', () => {
@@ -361,6 +370,15 @@ describe('plainDateDiffDays', () => {
361
370
  ),
362
371
  ).toBe(2);
363
372
  });
373
+
374
+ it('counts across the year 99 to 100 boundary', () => {
375
+ expect(
376
+ plainDateDiffDays(
377
+ {year: 99, month: 12, day: 31},
378
+ {year: 100, month: 1, day: 1},
379
+ ),
380
+ ).toBe(1);
381
+ });
364
382
  });
365
383
 
366
384
  describe('plainDateToInstant / plainDateFromInstant', () => {
@@ -393,6 +411,119 @@ describe('plainDateToInstant / plainDateFromInstant', () => {
393
411
  ),
394
412
  ).toBe(Date.UTC(2026, 4, 13, 16, 30));
395
413
  });
414
+
415
+ it.each([
416
+ // Both spring forward from a negative offset at local midnight.
417
+ [
418
+ 'America/Santiago',
419
+ {year: 2026, month: 9, day: 6},
420
+ Date.UTC(2026, 8, 6, 4),
421
+ ],
422
+ ['America/Havana', {year: 2026, month: 3, day: 8}, Date.UTC(2026, 2, 8, 5)],
423
+ // A positive offset already resolved forward; it must not move.
424
+ ['Asia/Beirut', {year: 2026, month: 3, day: 29}, Date.UTC(2026, 2, 28, 22)],
425
+ ] as const)(
426
+ 'starts a day whose midnight daylight saving skips at its first instant (%s)',
427
+ (timezoneID, date, firstInstant) => {
428
+ const instant = plainDateToInstant(date, timezoneID);
429
+ expect(instant).toBe(firstInstant);
430
+ expect(plainDateFromInstant(instant, timezoneID)).toEqual(date);
431
+ expect(plainDateFromInstant(instant - 1, timezoneID)).not.toEqual(date);
432
+ },
433
+ );
434
+
435
+ it('resolves any wall time inside a daylight-saving gap forward', () => {
436
+ // 2:30 AM does not happen on March 8, 2026 in Los Angeles: 3:30 AM PDT.
437
+ expect(
438
+ plainDateToInstant(
439
+ {year: 2026, month: 3, day: 8},
440
+ 'America/Los_Angeles',
441
+ 2,
442
+ 30,
443
+ ),
444
+ ).toBe(Date.UTC(2026, 2, 8, 10, 30));
445
+ });
446
+
447
+ it('keeps the earlier instant of a wall time that happens twice', () => {
448
+ // Havana falls back at 1:00 AM on November 1, 2026: midnight once, then
449
+ // 12:00–1:00 AM twice; the first 12:30 AM is still daylight time.
450
+ expect(
451
+ plainDateToInstant(
452
+ {year: 2026, month: 11, day: 1},
453
+ 'America/Havana',
454
+ 0,
455
+ 30,
456
+ ),
457
+ ).toBe(Date.UTC(2026, 10, 1, 4, 30));
458
+ });
459
+
460
+ it.each([
461
+ // Berlin falls back at 3:00 AM CEST on October 25, 2026: 2:00–3:00 AM
462
+ // happens twice, first at +2, then at +1.
463
+ [
464
+ 'Europe/Berlin',
465
+ {year: 2026, month: 10, day: 25},
466
+ 2,
467
+ 30,
468
+ Date.UTC(2026, 9, 25, 0, 30),
469
+ ],
470
+ // Sydney falls back at 3:00 AM AEDT on April 5, 2026: first at +11.
471
+ [
472
+ 'Australia/Sydney',
473
+ {year: 2026, month: 4, day: 5},
474
+ 2,
475
+ 30,
476
+ Date.UTC(2026, 3, 4, 15, 30),
477
+ ],
478
+ ] as const)(
479
+ 'keeps the earlier instant of a repeated wall time in a positive-offset zone (%s)',
480
+ (timezoneID, date, hour, minute, earlier) => {
481
+ expect(plainDateToInstant(date, timezoneID, hour, minute)).toBe(earlier);
482
+ },
483
+ );
484
+
485
+ it('starts a date the zone skips at the first instant after the gap', () => {
486
+ // Samoa moved across the date line after December 29, 2011: December 30
487
+ // never happened in Pacific/Apia, so it resolves to 00:00 on December 31
488
+ // (+14), the next date.
489
+ const instant = plainDateToInstant(
490
+ {year: 2011, month: 12, day: 30},
491
+ 'Pacific/Apia',
492
+ );
493
+ expect(instant).toBe(Date.UTC(2011, 11, 30, 10));
494
+ expect(plainDateFromInstant(instant, 'Pacific/Apia')).toEqual({
495
+ year: 2011,
496
+ month: 12,
497
+ day: 31,
498
+ });
499
+ });
500
+
501
+ it('moves a wall time in a 30-minute gap forward by 30 minutes', () => {
502
+ // Lord Howe Island jumps from 2:00 to 2:30 AM on October 4, 2026, from
503
+ // +10:30 to +11: 2:15 AM does not happen and reads as 2:45 AM.
504
+ expect(
505
+ plainDateToInstant(
506
+ {year: 2026, month: 10, day: 4},
507
+ 'Australia/Lord_Howe',
508
+ 2,
509
+ 15,
510
+ ),
511
+ ).toBe(Date.UTC(2026, 9, 3, 15, 45));
512
+ });
513
+
514
+ it('keeps years 1-99 instead of mapping them to 1900-1999', () => {
515
+ const date: PlainDate = {year: 1, month: 1, day: 15};
516
+ // 0001-01-01T00:00:00Z is -62135596800000
517
+ expect(plainDateToInstant(date, 'UTC', 9, 30)).toBe(
518
+ -62135596800000 + (14 * 24 + 9) * 3600000 + 30 * 60000,
519
+ );
520
+ expect(
521
+ plainDateFromInstant(
522
+ plainDateToInstant(date, 'America/Los_Angeles'),
523
+ 'America/Los_Angeles',
524
+ ),
525
+ ).toEqual(date);
526
+ });
396
527
  });
397
528
 
398
529
  describe('plainDateIsBefore / plainDateIsAfter', () => {
@@ -579,6 +710,11 @@ describe('plainDateGetWeekNumber', () => {
579
710
  it('returns week 1 for Jan 4 (always in ISO week 1)', () => {
580
711
  expect(plainDateGetWeekNumber({year: 2026, month: 1, day: 4})).toBe(1);
581
712
  });
713
+
714
+ it('keeps years 1-99 instead of mapping them to 1900-1999', () => {
715
+ // 0004-01-01 is a Thursday (ISO week 1); 1904-01-01 was a Friday.
716
+ expect(plainDateGetWeekNumber({year: 4, month: 1, day: 1})).toBe(1);
717
+ });
582
718
  });
583
719
 
584
720
  describe('plainDateFormat', () => {
@@ -663,3 +799,14 @@ describe('formatSharedDate', () => {
663
799
  );
664
800
  });
665
801
  });
802
+
803
+ describe('plainDateWeekFieldName', () => {
804
+ it("returns the locale's CLDR name for the week field", () => {
805
+ expect(plainDateWeekFieldName('en-US')).toBe('week');
806
+ expect(plainDateWeekFieldName('de-DE')).toBe('Woche');
807
+ });
808
+
809
+ it('never returns an empty name', () => {
810
+ expect(plainDateWeekFieldName('zxx').length).toBeGreaterThan(0);
811
+ });
812
+ });
@@ -45,7 +45,11 @@ export function plainDateToISO(pd: PlainDate): ISODateString {
45
45
  }
46
46
 
47
47
  export function plainDateToDate(pd: PlainDate): Date {
48
- return new Date(pd.year, pd.month - 1, pd.day);
48
+ // Unlike the Date constructor, setFullYear keeps years 0-99 as given
49
+ // instead of reading them as 1900-1999.
50
+ const d = new Date(0, 0, 1);
51
+ d.setFullYear(pd.year, pd.month - 1, pd.day);
52
+ return d;
49
53
  }
50
54
 
51
55
  export function plainDateFromDate(d: Date): PlainDate {
@@ -89,6 +93,24 @@ export function plainDateAddDays(pd: PlainDate, n: number): PlainDate {
89
93
  return plainDateFromDate(d);
90
94
  }
91
95
 
96
+ /**
97
+ * `Date.UTC` with years 0-99 kept as given; `Date.UTC` itself reads them as
98
+ * 1900-1999.
99
+ */
100
+ function utcTime(
101
+ year: number,
102
+ monthIndex: number,
103
+ day: number,
104
+ hour = 0,
105
+ minute = 0,
106
+ second = 0,
107
+ ): number {
108
+ const d = new Date(0);
109
+ d.setUTCFullYear(year, monthIndex, day);
110
+ d.setUTCHours(hour, minute, second);
111
+ return d.getTime();
112
+ }
113
+
92
114
  /**
93
115
  * Whole calendar days from `a` to `b`, ignoring time of day. Positive when
94
116
  * `b` is after `a`, negative when before. Uses UTC midnight so DST shifts
@@ -96,8 +118,8 @@ export function plainDateAddDays(pd: PlainDate, n: number): PlainDate {
96
118
  */
97
119
  export function plainDateDiffDays(a: PlainDate, b: PlainDate): number {
98
120
  const msPerDay = 24 * 60 * 60 * 1000;
99
- const aUTC = Date.UTC(a.year, a.month - 1, a.day);
100
- const bUTC = Date.UTC(b.year, b.month - 1, b.day);
121
+ const aUTC = utcTime(a.year, a.month - 1, a.day);
122
+ const bUTC = utcTime(b.year, b.month - 1, b.day);
101
123
  return Math.round((bUTC - aUTC) / msPerDay);
102
124
  }
103
125
 
@@ -152,7 +174,7 @@ export function getTimeZoneParts(
152
174
  function getTimezoneOffsetMS(timezoneID: string, instant: number): number {
153
175
  const parts = getTimeZoneParts(instant, timezoneID);
154
176
  return (
155
- Date.UTC(
177
+ utcTime(
156
178
  parts.year,
157
179
  parts.month - 1,
158
180
  parts.day,
@@ -163,17 +185,35 @@ function getTimezoneOffsetMS(timezoneID: string, instant: number): number {
163
185
  );
164
186
  }
165
187
 
188
+ const DAY_MS = 24 * 60 * 60 * 1000;
189
+
166
190
  export function plainDateToInstant(
167
191
  date: PlainDate,
168
192
  timezoneID: string,
169
193
  hour = 0,
170
194
  minute = 0,
171
195
  ): number {
172
- const utcGuess = Date.UTC(date.year, date.month - 1, date.day, hour, minute);
173
- const firstOffset = getTimezoneOffsetMS(timezoneID, utcGuess);
174
- const firstInstant = utcGuess - firstOffset;
175
- const secondOffset = getTimezoneOffsetMS(timezoneID, firstInstant);
176
- return utcGuess - secondOffset;
196
+ // The wall time read as if it were UTC; subtracting an offset gives a
197
+ // candidate instant.
198
+ const wallTime = utcTime(date.year, date.month - 1, date.day, hour, minute);
199
+ // The offsets in effect a day either side cover any transition that can make
200
+ // this wall time skipped or repeated.
201
+ const offsetBefore = getTimezoneOffsetMS(timezoneID, wallTime - DAY_MS);
202
+ const offsetAfter = getTimezoneOffsetMS(timezoneID, wallTime + DAY_MS);
203
+ // AST-067 (Temporal's "compatible" disambiguation): a wall time that happens
204
+ // twice resolves to its earlier occurrence, and one that never happens is
205
+ // read in the offset before the transition, which moves it forward by the
206
+ // length of the gap.
207
+ const candidates = [offsetBefore, offsetAfter]
208
+ .map(offset => wallTime - offset)
209
+ .filter(
210
+ (instant, index) =>
211
+ getTimezoneOffsetMS(timezoneID, instant) ===
212
+ (index === 0 ? offsetBefore : offsetAfter),
213
+ );
214
+ return candidates.length > 0
215
+ ? Math.min(...candidates)
216
+ : wallTime - offsetBefore;
177
217
  }
178
218
 
179
219
  export function plainDateFromInstant(
@@ -245,7 +285,7 @@ export function plainDateGetWeekNumber(pd: PlainDate): number {
245
285
  const d = plainDateToDate(pd);
246
286
  const dayNum = d.getDay() || 7;
247
287
  d.setDate(d.getDate() + 4 - dayNum);
248
- const yearStart = new Date(d.getFullYear(), 0, 1);
288
+ const yearStart = plainDateToDate({year: d.getFullYear(), month: 1, day: 1});
249
289
  return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
250
290
  }
251
291
 
@@ -330,6 +370,31 @@ export function plainDateFormat(
330
370
  }).format(plainDateToDate(pd));
331
371
  }
332
372
 
373
+ /**
374
+ * The locale's name for the "week of year" date field ("week", "Woche", "週"),
375
+ * from CLDR through `Intl.DisplayNames`. Calendar names its week-number
376
+ * column header with it, so no catalog string is needed for a word the
377
+ * platform already carries in every locale.
378
+ *
379
+ * `Intl.DisplayNames` with `type: 'dateTimeField'` is inside the Baseline − 2
380
+ * support tier (Chrome 106, Safari 15.4, Firefox 110). Should a host lack it
381
+ * or return nothing, the field's English CLDR name stands in rather than an
382
+ * empty header.
383
+ */
384
+ export function plainDateWeekFieldName(locale: Locale = 'en'): string {
385
+ try {
386
+ const name = new Intl.DisplayNames(locale, {type: 'dateTimeField'}).of(
387
+ 'weekOfYear',
388
+ );
389
+ if (name != null && name !== '') {
390
+ return name;
391
+ }
392
+ } catch {
393
+ // Fall through to the English CLDR value.
394
+ }
395
+ return 'week';
396
+ }
397
+
333
398
  // =============================================================================
334
399
  // Shared date-only format vocabulary
335
400
  // =============================================================================