@astryxdesign/core 0.4.0 → 0.4.1-canary.1ce0b4c

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 (271) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  3. package/dist/AspectRatio/AspectRatio.js +1 -2
  4. package/dist/AspectRatio/index.d.ts.map +1 -1
  5. package/dist/AspectRatio/index.js +1 -2
  6. package/dist/Badge/Badge.d.ts.map +1 -1
  7. package/dist/Badge/Badge.js +1 -2
  8. package/dist/Badge/index.d.ts.map +1 -1
  9. package/dist/Badge/index.js +1 -2
  10. package/dist/Banner/Banner.d.ts +3 -0
  11. package/dist/Banner/Banner.d.ts.map +1 -1
  12. package/dist/Banner/Banner.js +17 -3
  13. package/dist/Blockquote/Blockquote.d.ts.map +1 -1
  14. package/dist/Blockquote/Blockquote.js +1 -2
  15. package/dist/Blockquote/index.d.ts.map +1 -1
  16. package/dist/Blockquote/index.js +1 -2
  17. package/dist/Button/Button.d.ts.map +1 -1
  18. package/dist/Button/Button.js +11 -9
  19. package/dist/Card/Card.d.ts.map +1 -1
  20. package/dist/Card/Card.js +1 -2
  21. package/dist/Card/index.d.ts.map +1 -1
  22. package/dist/Card/index.js +1 -2
  23. package/dist/Center/Center.d.ts.map +1 -1
  24. package/dist/Center/Center.js +1 -2
  25. package/dist/Center/index.d.ts.map +1 -1
  26. package/dist/Center/index.js +1 -2
  27. package/dist/Code/Code.d.ts.map +1 -1
  28. package/dist/Code/Code.js +1 -2
  29. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  30. package/dist/CommandPalette/CommandPalette.js +5 -0
  31. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  32. package/dist/ComplexSelector/ComplexSelector.js +7 -8
  33. package/dist/DateInput/DateInput.d.ts.map +1 -1
  34. package/dist/DateInput/DateInput.js +23 -10
  35. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  36. package/dist/DateRangeInput/DateRangeInput.js +44 -16
  37. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  38. package/dist/DateTimeInput/DateTimeInput.js +32 -13
  39. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +1 -1
  40. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
  41. package/dist/DropdownMenu/DropdownMenuRadioGroup.js +10 -3
  42. package/dist/Field/PanelSearchInput.d.ts +81 -0
  43. package/dist/Field/PanelSearchInput.d.ts.map +1 -0
  44. package/dist/Field/PanelSearchInput.js +151 -0
  45. package/dist/FileInput/FileInput.d.ts.map +1 -1
  46. package/dist/FileInput/FileInput.js +21 -10
  47. package/dist/Grid/Grid.d.ts.map +1 -1
  48. package/dist/Grid/Grid.js +1 -2
  49. package/dist/Grid/index.d.ts.map +1 -1
  50. package/dist/Grid/index.js +1 -2
  51. package/dist/HoverCard/HoverCard.d.ts +1 -1
  52. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  53. package/dist/HoverCard/HoverCard.js +5 -13
  54. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  55. package/dist/HoverCard/useHoverCard.js +6 -5
  56. package/dist/Icon/globalIconRegistry.d.ts +5 -4
  57. package/dist/Icon/globalIconRegistry.d.ts.map +1 -1
  58. package/dist/Icon/globalIconRegistry.js +5 -4
  59. package/dist/Indicator/index.d.ts +1 -1
  60. package/dist/Indicator/index.d.ts.map +1 -1
  61. package/dist/Indicator/types.d.ts +10 -0
  62. package/dist/Indicator/types.d.ts.map +1 -1
  63. package/dist/InputGroup/groupStyles.d.ts.map +1 -1
  64. package/dist/InputGroup/groupStyles.js +7 -2
  65. package/dist/Layer/layerHost.d.ts +24 -0
  66. package/dist/Layer/layerHost.d.ts.map +1 -0
  67. package/dist/Layer/layerHost.js +79 -0
  68. package/dist/Layer/useLayer.d.ts +14 -6
  69. package/dist/Layer/useLayer.d.ts.map +1 -1
  70. package/dist/Layer/useLayer.js +155 -29
  71. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  72. package/dist/Lightbox/Lightbox.js +24 -50
  73. package/dist/MultiSelector/MultiSelector.d.ts +8 -1
  74. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  75. package/dist/MultiSelector/MultiSelector.js +134 -102
  76. package/dist/NavItem/navItemStyles.stylex.d.ts +17 -5
  77. package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
  78. package/dist/NavItem/navItemStyles.stylex.js +11 -5
  79. package/dist/Popover/usePopover.d.ts +15 -0
  80. package/dist/Popover/usePopover.d.ts.map +1 -1
  81. package/dist/Popover/usePopover.js +14 -2
  82. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  83. package/dist/ProgressBar/ProgressBar.js +2 -3
  84. package/dist/Section/Section.d.ts.map +1 -1
  85. package/dist/Section/Section.js +1 -2
  86. package/dist/Section/index.d.ts.map +1 -1
  87. package/dist/Section/index.js +1 -2
  88. package/dist/Selector/Selector.d.ts +10 -0
  89. package/dist/Selector/Selector.d.ts.map +1 -1
  90. package/dist/Selector/Selector.js +135 -95
  91. package/dist/SideNav/SideNav.d.ts +7 -9
  92. package/dist/SideNav/SideNav.d.ts.map +1 -1
  93. package/dist/SideNav/SideNav.js +32 -5
  94. package/dist/SideNav/SideNavCollapseButton.d.ts +25 -9
  95. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  96. package/dist/SideNav/SideNavCollapseButton.js +39 -15
  97. package/dist/SideNav/SideNavCollapseContext.d.ts +21 -0
  98. package/dist/SideNav/SideNavCollapseContext.d.ts.map +1 -1
  99. package/dist/SideNav/SideNavCollapseContext.js +16 -2
  100. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  101. package/dist/SideNav/SideNavHeading.js +70 -24
  102. package/dist/SideNav/SideNavItem.d.ts +7 -2
  103. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  104. package/dist/SideNav/SideNavItem.js +114 -75
  105. package/dist/SideNav/SideNavSection.d.ts.map +1 -1
  106. package/dist/SideNav/SideNavSection.js +7 -16
  107. package/dist/SideNav/index.d.ts +1 -1
  108. package/dist/SideNav/index.d.ts.map +1 -1
  109. package/dist/Skeleton/Skeleton.d.ts.map +1 -1
  110. package/dist/Skeleton/Skeleton.js +1 -2
  111. package/dist/Skeleton/index.d.ts.map +1 -1
  112. package/dist/Skeleton/index.js +1 -2
  113. package/dist/Slider/Slider.d.ts.map +1 -1
  114. package/dist/Slider/Slider.js +2 -6
  115. package/dist/Switch/Switch.d.ts.map +1 -1
  116. package/dist/Switch/Switch.js +3 -3
  117. package/dist/Table/TableRow.d.ts +1 -1
  118. package/dist/Table/TableRow.d.ts.map +1 -1
  119. package/dist/Table/TableRow.js +4 -2
  120. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  121. package/dist/TimeInput/TimeInput.js +7 -2
  122. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  123. package/dist/Timestamp/Timestamp.js +14 -3
  124. package/dist/Timestamp/formatInstant.d.ts +11 -1
  125. package/dist/Timestamp/formatInstant.d.ts.map +1 -1
  126. package/dist/Timestamp/formatInstant.js +3 -1
  127. package/dist/TopNav/TopNavMegaMenu.js +1 -1
  128. package/dist/TopNav/TopNavMegaMenuItem.js +1 -1
  129. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  130. package/dist/TopNav/TopNavMenu.js +2 -2
  131. package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +1 -1
  132. package/dist/VisuallyHidden/VisuallyHidden.js +1 -2
  133. package/dist/VisuallyHidden/index.d.ts.map +1 -1
  134. package/dist/VisuallyHidden/index.js +1 -2
  135. package/dist/astryx.css +23 -24
  136. package/dist/astryx.umd.js +48 -48
  137. package/dist/astryx.umd.js.map +4 -4
  138. package/dist/hooks/index.d.ts +1 -1
  139. package/dist/hooks/index.d.ts.map +1 -1
  140. package/dist/hooks/index.js +1 -1
  141. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  142. package/dist/hooks/useFocusTrap.js +16 -2
  143. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  144. package/dist/hooks/useInputStatusIcon.js +17 -4
  145. package/dist/theme/defineTheme.d.ts +2 -2
  146. package/dist/theme/defineTheme.d.ts.map +1 -1
  147. package/dist/theme/defineTheme.js +2 -1
  148. package/dist/theme/index.d.ts +1 -1
  149. package/dist/theme/index.d.ts.map +1 -1
  150. package/dist/theme/index.js +1 -1
  151. package/dist/theme/tokens.stylex.d.ts +12 -0
  152. package/dist/theme/tokens.stylex.d.ts.map +1 -1
  153. package/dist/theme/tokens.stylex.js +23 -0
  154. package/dist/utils/focusOutline.stylex.d.ts +34 -19
  155. package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
  156. package/dist/utils/focusOutline.stylex.js +46 -33
  157. package/dist/utils/index.d.ts +1 -0
  158. package/dist/utils/index.d.ts.map +1 -1
  159. package/dist/utils/index.js +5 -1
  160. package/dist/utils/interactionModality.d.ts +34 -0
  161. package/dist/utils/interactionModality.d.ts.map +1 -0
  162. package/dist/utils/interactionModality.js +70 -0
  163. package/locales/en.json +44 -0
  164. package/locales/pseudo.json +33 -0
  165. package/package.json +3 -3
  166. package/src/AppShell/AppShell.doc.mjs +3 -0
  167. package/src/AspectRatio/AspectRatio.tsx +0 -2
  168. package/src/AspectRatio/index.ts +0 -2
  169. package/src/Badge/Badge.tsx +0 -2
  170. package/src/Badge/index.ts +0 -2
  171. package/src/Banner/Banner.test.tsx +58 -1
  172. package/src/Banner/Banner.tsx +24 -5
  173. package/src/Blockquote/Blockquote.tsx +0 -2
  174. package/src/Blockquote/index.ts +0 -2
  175. package/src/Button/Button.doc.mjs +2 -2
  176. package/src/Button/Button.tsx +10 -7
  177. package/src/ButtonGroup/ButtonGroup.test.tsx +7 -4
  178. package/src/Card/Card.tsx +0 -2
  179. package/src/Card/index.ts +0 -2
  180. package/src/Center/Center.tsx +0 -2
  181. package/src/Center/index.ts +0 -2
  182. package/src/CheckboxInput/CheckboxInput.test.tsx +8 -3
  183. package/src/Code/Code.tsx +0 -2
  184. package/src/CommandPalette/CommandPalette.test.tsx +54 -0
  185. package/src/CommandPalette/CommandPalette.tsx +5 -0
  186. package/src/ComplexSelector/ComplexSelector.test.tsx +15 -6
  187. package/src/ComplexSelector/ComplexSelector.tsx +6 -10
  188. package/src/DateInput/DateInput.test.tsx +19 -0
  189. package/src/DateInput/DateInput.tsx +3 -6
  190. package/src/DateRangeInput/DateRangeInput.tsx +3 -9
  191. package/src/DateTimeInput/DateTimeInput.test.tsx +79 -1
  192. package/src/DateTimeInput/DateTimeInput.tsx +23 -9
  193. package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +2 -1
  194. package/src/Field/PanelSearchInput.tsx +271 -0
  195. package/src/FileInput/FileInput.tsx +21 -8
  196. package/src/Grid/Grid.tsx +0 -2
  197. package/src/Grid/index.ts +0 -2
  198. package/src/HoverCard/HoverCard.doc.mjs +3 -3
  199. package/src/HoverCard/HoverCard.test.tsx +283 -55
  200. package/src/HoverCard/HoverCard.tsx +5 -13
  201. package/src/HoverCard/useHoverCard.tsx +8 -11
  202. package/src/Icon/globalIconRegistry.tsx +5 -4
  203. package/src/Indicator/Indicator.doc.mjs +1 -1
  204. package/src/Indicator/index.ts +1 -0
  205. package/src/Indicator/types.ts +11 -0
  206. package/src/InputGroup/groupStyles.ts +7 -8
  207. package/src/Layer/layerHost.test.ts +99 -0
  208. package/src/Layer/layerHost.ts +141 -0
  209. package/src/Layer/useLayer.doc.mjs +14 -4
  210. package/src/Layer/useLayer.test.tsx +318 -7
  211. package/src/Layer/useLayer.tsx +229 -35
  212. package/src/Lightbox/Lightbox.tsx +2 -11
  213. package/src/MultiSelector/MultiSelector.doc.mjs +9 -0
  214. package/src/MultiSelector/MultiSelector.test.tsx +135 -5
  215. package/src/MultiSelector/MultiSelector.tsx +170 -127
  216. package/src/NavItem/navItemStyles.stylex.ts +31 -5
  217. package/src/Popover/Popover.doc.mjs +2 -0
  218. package/src/Popover/usePopover.tsx +37 -4
  219. package/src/ProgressBar/ProgressBar.tsx +2 -8
  220. package/src/Section/Section.tsx +0 -2
  221. package/src/Section/index.ts +0 -2
  222. package/src/Selector/Selector.doc.mjs +10 -0
  223. package/src/Selector/Selector.test.tsx +386 -2
  224. package/src/Selector/Selector.tsx +182 -122
  225. package/src/SideNav/SideNav.doc.mjs +13 -4
  226. package/src/SideNav/SideNav.test.tsx +771 -2
  227. package/src/SideNav/SideNav.tsx +37 -16
  228. package/src/SideNav/SideNavCollapseButton.doc.mjs +28 -6
  229. package/src/SideNav/SideNavCollapseButton.tsx +67 -15
  230. package/src/SideNav/SideNavCollapseContext.ts +29 -9
  231. package/src/SideNav/SideNavHeading.tsx +28 -10
  232. package/src/SideNav/SideNavItem.doc.mjs +13 -0
  233. package/src/SideNav/SideNavItem.tsx +92 -84
  234. package/src/SideNav/SideNavSection.tsx +6 -20
  235. package/src/SideNav/index.ts +2 -0
  236. package/src/Skeleton/Skeleton.tsx +0 -2
  237. package/src/Skeleton/index.ts +0 -2
  238. package/src/Slider/Slider.tsx +2 -11
  239. package/src/Switch/Switch.tsx +10 -2
  240. package/src/Table/Table.test.tsx +31 -0
  241. package/src/Table/TableRow.tsx +6 -0
  242. package/src/Text/Text.doc.mjs +1 -1
  243. package/src/TimeInput/TimeInput.test.tsx +54 -0
  244. package/src/TimeInput/TimeInput.tsx +17 -2
  245. package/src/Timestamp/Timestamp.test.tsx +125 -30
  246. package/src/Timestamp/Timestamp.tsx +14 -2
  247. package/src/Timestamp/formatInstant.ts +16 -1
  248. package/src/Toolbar/Toolbar.test.tsx +14 -14
  249. package/src/TopNav/TopNavMegaMenu.doc.mjs +5 -0
  250. package/src/TopNav/TopNavMegaMenu.test.tsx +43 -0
  251. package/src/TopNav/TopNavMegaMenu.tsx +4 -1
  252. package/src/TopNav/TopNavMegaMenuItem.tsx +1 -1
  253. package/src/TopNav/TopNavMenu.test.tsx +41 -0
  254. package/src/TopNav/TopNavMenu.tsx +8 -2
  255. package/src/TreeList/TreeList.test.tsx +3 -2
  256. package/src/VisuallyHidden/VisuallyHidden.tsx +0 -2
  257. package/src/VisuallyHidden/index.ts +0 -2
  258. package/src/hooks/index.ts +5 -1
  259. package/src/hooks/useFocusTrap.test.tsx +16 -0
  260. package/src/hooks/useFocusTrap.ts +20 -2
  261. package/src/hooks/useInputStatusIcon.tsx +8 -8
  262. package/src/serverSafeComponents.test.ts +665 -0
  263. package/src/theme/defineTheme.ts +3 -0
  264. package/src/theme/generateThemeRules.test.ts +17 -0
  265. package/src/theme/index.ts +2 -0
  266. package/src/theme/themingTargets.test.ts +152 -26
  267. package/src/theme/tokens.stylex.ts +18 -0
  268. package/src/theme/tokens.test.ts +37 -0
  269. package/src/utils/focusOutline.stylex.ts +37 -23
  270. package/src/utils/index.ts +4 -0
  271. package/src/utils/interactionModality.ts +74 -0
@@ -0,0 +1,70 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file interactionModality.ts
5
+ * @input Global pointerdown/keydown events
6
+ * @output `getInteractionModality()` — how the user last interacted
7
+ * @position Internal utility; used where `:focus-visible` alone is too broad
8
+ *
9
+ * `:focus-visible` is the right selector for a focus ring and should stay the
10
+ * condition that draws one. It is not, on its own, "focused by keyboard":
11
+ * per CSS Selectors 4 a pointer-focused element that supports text entry
12
+ * matches it too, deliberately, so that a clicked text field still shows where
13
+ * typing will go. Measured in Chromium — a bare `<input>` clicked with a mouse
14
+ * matches `:focus-visible`.
15
+ *
16
+ * A component that wants a ring for keyboard focus ONLY therefore needs one bit
17
+ * `:focus-visible` cannot give it: which device moved focus. This records that,
18
+ * to be used as a gate ALONGSIDE `:focus-visible`, never as a replacement for
19
+ * it — the browser's heuristic still decides everything else, including
20
+ * `focus({focusVisible: true})`.
21
+ *
22
+ * Listeners are registered once for the document, in the capture phase so they
23
+ * run before the focus they explain, and are never removed: two passive
24
+ * listeners for the whole app, versus a pair per mounted input.
25
+ */
26
+
27
+ // Default to keyboard: with no interaction yet, focus arrived programmatically
28
+ // or from the browser restoring it, and showing a ring is the safe error.
29
+ let modality = 'keyboard';
30
+ let isListening = false;
31
+ function onPointerDown() {
32
+ modality = 'pointer';
33
+ }
34
+ function onKeyDown(event) {
35
+ // Modifier-only presses are not navigation — holding Shift before a click
36
+ // must not turn that click into "keyboard".
37
+ if (event.metaKey || event.altKey || event.ctrlKey) {
38
+ return;
39
+ }
40
+ modality = 'keyboard';
41
+ }
42
+
43
+ /**
44
+ * Start tracking, once per document. Safe to call repeatedly and on the server
45
+ * (where it does nothing).
46
+ */
47
+ export function trackInteractionModality() {
48
+ if (isListening || typeof document === 'undefined') {
49
+ return;
50
+ }
51
+ isListening = true;
52
+ document.addEventListener('pointerdown', onPointerDown, {
53
+ capture: true,
54
+ passive: true
55
+ });
56
+ document.addEventListener('keydown', onKeyDown, {
57
+ capture: true,
58
+ passive: true
59
+ });
60
+ }
61
+
62
+ /** How the user last interacted with the page. */
63
+ export function getInteractionModality() {
64
+ return modality;
65
+ }
66
+
67
+ /** Test-only: restore the initial state between cases. */
68
+ export function __resetInteractionModalityForTest() {
69
+ modality = 'keyboard';
70
+ }
package/locales/en.json CHANGED
@@ -639,6 +639,10 @@
639
639
  "defaultMessage": "Clear {label}",
640
640
  "description": "Aria label for the clear-value button in DateInput / DateRangeInput / DateTimeInput. `{label}` is the field's own label so screen readers announce e.g. 'Clear Start date'."
641
641
  },
642
+ "@astryx.dateInput.invalidDate": {
643
+ "defaultMessage": "Invalid date",
644
+ "description": "Screen-reader-only live-region alert in DateInput / DateTimeInput when the typed date cannot be parsed. The field silently reverts on blur, so this is the only rejection feedback a screen-reader user gets."
645
+ },
642
646
  "@astryx.dateTimeInput.timeSuffix": {
643
647
  "defaultMessage": "{label} time",
644
648
  "description": "Screen-reader-only accessible name for the time-of-day slot in a DateTimeInput. Example: `Start date time`; reorder freely (e.g. \"time of {label}\")."
@@ -711,6 +715,10 @@
711
715
  "defaultMessage": "Select a time",
712
716
  "description": "\"Time\" = clock time (HH:MM), not duration or era. Grey placeholder inside an empty TimeInput field; imperative verb."
713
717
  },
718
+ "@astryx.timeInput.invalidTime": {
719
+ "defaultMessage": "Invalid time",
720
+ "description": "Screen-reader-only live-region alert in TimeInput / DateTimeInput when the typed time cannot be parsed. \"Time\" = clock time (HH:MM). The field silently reverts on blur, so this is the only rejection feedback a screen-reader user gets."
721
+ },
714
722
  "@astryx.topNav.heading.dialogLabel": {
715
723
  "defaultMessage": "Navigation menu",
716
724
  "description": "Screen-reader-only accessible name for the dropdown dialog opened from a TopNavHeading's overflow menu. Kept separate from the SideNav sibling."
@@ -807,6 +815,38 @@
807
815
  "defaultMessage": "Clear {label}",
808
816
  "description": "Screen-reader-only label on the X that removes the selected file from a FileInput. Example: `Clear Attachment`, `Clear Résumé`."
809
817
  },
818
+ "@astryx.fileInput.dropHint": {
819
+ "defaultMessage": "Drop files here",
820
+ "description": "Text shown in the dropzone area when a user is dragging files over the FileInput."
821
+ },
822
+ "@astryx.fileInput.errorInvalidType": {
823
+ "defaultMessage": "\"{fileName}\" is not an accepted file type",
824
+ "description": "Validation error shown when a selected file does not match the accepted types."
825
+ },
826
+ "@astryx.fileInput.errorMaxFiles": {
827
+ "defaultMessage": "Maximum {maxFiles} files allowed",
828
+ "description": "Validation error shown when the number of selected files exceeds the maximum."
829
+ },
830
+ "@astryx.fileInput.errorMaxSize": {
831
+ "defaultMessage": "\"{fileName}\" exceeds {maxSize} limit",
832
+ "description": "Validation error shown when a selected file exceeds the maximum size. {maxSize} is a formatted size string like \"2.0 MB\"."
833
+ },
834
+ "@astryx.fileInput.fileSelected": {
835
+ "defaultMessage": "1 file selected: {fileName}",
836
+ "description": "Live-region announcement when a single file is successfully selected."
837
+ },
838
+ "@astryx.fileInput.filesSelected": {
839
+ "defaultMessage": "{count} files selected",
840
+ "description": "Live-region announcement when multiple files are successfully selected."
841
+ },
842
+ "@astryx.fileInput.placeholder": {
843
+ "defaultMessage": "Choose file",
844
+ "description": "Default placeholder text shown when no file is selected in a single-file FileInput."
845
+ },
846
+ "@astryx.fileInput.placeholderMultiple": {
847
+ "defaultMessage": "Choose files",
848
+ "description": "Default placeholder text shown when no files are selected in a multi-file FileInput."
849
+ },
810
850
  "@astryx.fileInput.required": {
811
851
  "defaultMessage": "Required",
812
852
  "description": "Screen-reader-only description on the FileInput trigger indicating the field must be filled in. Mirrors the visible `Required` indicator next to the field label; conveyed via description because aria-required is not supported on role=\"button\"."
@@ -895,6 +935,10 @@
895
935
  "defaultMessage": "Collapse {label}",
896
936
  "description": "Screen-reader-only label on the same chevron when the group is expanded. Example: `Collapse Settings`. Pairs with `sideNavItem.expand`."
897
937
  },
938
+ "@astryx.sideNavItem.submenuLabel": {
939
+ "defaultMessage": "{label} submenu",
940
+ "description": "Accessible name for the flyout dialog that opens from a collapsed (icon-only) SideNav item to show its sub-items. {label} is the parent item's label. Example: `Settings submenu`."
941
+ },
898
942
  "@astryx.tableFiltering.filterByColumn": {
899
943
  "defaultMessage": "Filter {header}",
900
944
  "description": "Triple-use string on per-column filter controls: visible label, placeholder, AND aria label. `{header}` is the column header — example: `Filter Name`. Keep short."
@@ -479,6 +479,9 @@
479
479
  "@astryx.dateInput.clear": {
480
480
  "defaultMessage": "⟦Çłéàř {label}⟧"
481
481
  },
482
+ "@astryx.dateInput.invalidDate": {
483
+ "defaultMessage": "⟦Íñṽàłíð ðàţé⟧"
484
+ },
482
485
  "@astryx.dateTimeInput.timeSuffix": {
483
486
  "defaultMessage": "⟦{label} ţíɱé⟧"
484
487
  },
@@ -533,6 +536,9 @@
533
536
  "@astryx.timeInput.placeholder": {
534
537
  "defaultMessage": "⟦Šéłéçţ à ţíɱé⟧"
535
538
  },
539
+ "@astryx.timeInput.invalidTime": {
540
+ "defaultMessage": "⟦Íñṽàłíð ţíɱé⟧"
541
+ },
536
542
  "@astryx.topNav.heading.dialogLabel": {
537
543
  "defaultMessage": "⟦Ñàṽíĝàţíóñ ɱéñú⟧"
538
544
  },
@@ -605,6 +611,30 @@
605
611
  "@astryx.fileInput.clearLabel": {
606
612
  "defaultMessage": "⟦Çłéàř {label}⟧"
607
613
  },
614
+ "@astryx.fileInput.dropHint": {
615
+ "defaultMessage": "⟦Ðřóþ ƒíłéš ĥéřé⟧"
616
+ },
617
+ "@astryx.fileInput.errorInvalidType": {
618
+ "defaultMessage": "⟦\"{fileName}\" íš ñóţ àñ àççéþţéð ƒíłé ţýþé⟧"
619
+ },
620
+ "@astryx.fileInput.errorMaxFiles": {
621
+ "defaultMessage": "⟦Ṁàẋíɱúɱ {maxFiles} ƒíłéš àłłóŵéð⟧"
622
+ },
623
+ "@astryx.fileInput.errorMaxSize": {
624
+ "defaultMessage": "⟦\"{fileName}\" éẋçééðš {maxSize} łíɱíţ⟧"
625
+ },
626
+ "@astryx.fileInput.fileSelected": {
627
+ "defaultMessage": "⟦1 ƒíłé šéłéçţéð: {fileName}⟧"
628
+ },
629
+ "@astryx.fileInput.filesSelected": {
630
+ "defaultMessage": "⟦{count} ƒíłéš šéłéçţéð⟧"
631
+ },
632
+ "@astryx.fileInput.placeholder": {
633
+ "defaultMessage": "⟦Çĥóóšé ƒíłé⟧"
634
+ },
635
+ "@astryx.fileInput.placeholderMultiple": {
636
+ "defaultMessage": "⟦Çĥóóšé ƒíłéš⟧"
637
+ },
608
638
  "@astryx.fileInput.required": {
609
639
  "defaultMessage": "⟦Řéɋúířéð⟧"
610
640
  },
@@ -671,6 +701,9 @@
671
701
  "@astryx.sideNavItem.collapse": {
672
702
  "defaultMessage": "⟦Çółłàþšé {label}⟧"
673
703
  },
704
+ "@astryx.sideNavItem.submenuLabel": {
705
+ "defaultMessage": "⟦{label} šúƀɱéñú⟧"
706
+ },
674
707
  "@astryx.tableFiltering.filterByColumn": {
675
708
  "defaultMessage": "⟦Ƒíłţéř {header}⟧"
676
709
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.4.0",
3
+ "version": "0.4.1-canary.1ce0b4c",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -654,6 +654,7 @@
654
654
  "react-dom": ">=19.0.0"
655
655
  },
656
656
  "devDependencies": {
657
+ "@astryxdesign/cli": "0.4.1-canary.1ce0b4c",
657
658
  "@babel/cli": "^8.0.4",
658
659
  "@babel/core": "^7.29.7",
659
660
  "@babel/preset-react": "^8.0.1",
@@ -662,8 +663,7 @@
662
663
  "@testing-library/dom": "^10.0.0",
663
664
  "@testing-library/jest-dom": "^6.6.0",
664
665
  "@testing-library/react": "^16.3.2",
665
- "rimraf": "^6.0.1",
666
- "@astryxdesign/cli": "0.4.0"
666
+ "rimraf": "^6.0.1"
667
667
  },
668
668
  "dependencies": {
669
669
  "intl-messageformat": "^11.2.9"
@@ -203,8 +203,11 @@ export const docsDense = {
203
203
  bestPractices: [
204
204
  {guidance: true, description: 'Choose the right height: use "fill" for dashboards with internal scrolling and "auto" for pages that grow with content.'},
205
205
  {guidance: true, description: 'Set `contentPadding` based on content type: 4 for forms and settings, 0 for tables and dashboards.'},
206
+ {guidance: true, description: 'Give every nav slot an accessible name: TopNav and SideNav are separate navigation landmarks, so pass `label` to each.'},
207
+ {guidance: true, description: 'Start the page heading inside `children`: AppShell owns the skip link, banner, and main landmarks but renders no heading, so the first content heading is the page h1.'},
206
208
  {guidance: false, description: "Nest one AppShell inside another; it's the outermost layout frame."},
207
209
  {guidance: false, description: 'Use for sub-page layouts; use Layout for content areas within AppShell.'},
210
+ {guidance: false, description: 'Add your own skip link or <main>; AppShell renders both, and a second main landmark makes the first ambiguous.'},
208
211
  ],
209
212
  },
210
213
  propDescriptions: {
@@ -1,7 +1,5 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- 'use client';
4
-
5
3
  /**
6
4
  * @file AspectRatio.tsx
7
5
  * @input Uses React, stylex
@@ -1,7 +1,5 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- 'use client';
4
-
5
3
  /**
6
4
  * @file index.ts
7
5
  * @input Imports from AspectRatio.tsx
@@ -1,7 +1,5 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- 'use client';
4
-
5
3
  /**
6
4
  * @file Badge.tsx
7
5
  * @input Uses React, HTMLAttributes
@@ -1,7 +1,5 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- 'use client';
4
-
5
3
  /**
6
4
  * @file index.ts
7
5
  * @input Imports Badge component and types from Badge.tsx
@@ -3,7 +3,8 @@
3
3
  /**
4
4
  * @file Banner.test.tsx
5
5
  * @input Uses vitest, @testing-library/react, Banner component
6
- * @output Unit tests for Banner component behavior
6
+ * @output Unit tests for Banner component behavior, including the
7
+ * 'banner-icon' theme target riding on the status icon glyph (#4166)
7
8
  * @position Testing; validates Banner.tsx implementation
8
9
  *
9
10
  * SYNC: When modified, update this header
@@ -337,6 +338,62 @@ describe('Banner', () => {
337
338
  expect(ref.current).toBeInstanceOf(HTMLDivElement);
338
339
  });
339
340
 
341
+ // =========================================================================
342
+ // Status icon color theming (#4166)
343
+ // =========================================================================
344
+
345
+ it("carries the 'banner-icon' theme target on the default status icon glyph", () => {
346
+ // Theme overrides for 'banner-icon' + 'status:X' compile to
347
+ // '.astryx-banner-icon.<status>' (parseStyleKey). The target must sit on
348
+ // the <Icon> span itself so those same-element rules in
349
+ // @layer astryx-theme beat the Icon's own color variant.
350
+ const statuses = ['info', 'warning', 'error', 'success'] as const;
351
+ for (const status of statuses) {
352
+ const {container, unmount} = render(
353
+ <Banner status={status} title={`${status} banner`} />,
354
+ );
355
+ const glyph = container.querySelector(
356
+ `.astryx-icon.astryx-banner-icon.${status}`,
357
+ );
358
+ expect(glyph).not.toBeNull();
359
+ expect(glyph).toHaveAttribute('data-status', status);
360
+ // Exactly one element carries the target — the layout wrapper no
361
+ // longer does.
362
+ expect(container.querySelectorAll('.astryx-banner-icon')).toHaveLength(1);
363
+ unmount();
364
+ }
365
+ });
366
+
367
+ it('keeps the color variant on the theme-target element (regression pin for #4166)', () => {
368
+ // Pre-fix, '.astryx-banner-icon.info' matched the layout wrapper while
369
+ // the color variant (data-color="accent") sat on an inner span that a
370
+ // theme override could never reach. Target and paint now share one
371
+ // element.
372
+ const {container} = render(<Banner status="info" title="Info" />);
373
+ const target = container.querySelector('.astryx-banner-icon.info');
374
+ expect(target).toHaveAttribute('data-color', 'accent');
375
+ });
376
+
377
+ it("keeps the 'banner-icon' target on the wrapper for a custom icon node", () => {
378
+ // Core never injects props into consumer elements, so with a custom
379
+ // `icon` the target stays on the (layout-only) wrapper and overrides
380
+ // reach the node via inheritance. The node itself is untouched.
381
+ const {container} = render(
382
+ <Banner
383
+ status="info"
384
+ title="Custom icon"
385
+ icon={<span data-testid="custom-glyph">i</span>}
386
+ />,
387
+ );
388
+ const targets = container.querySelectorAll('.astryx-banner-icon');
389
+ expect(targets).toHaveLength(1);
390
+ expect(targets[0]?.tagName).toBe('DIV');
391
+ expect(targets[0]).toHaveAttribute('aria-hidden', 'true');
392
+ const custom = container.querySelector('[data-testid="custom-glyph"]');
393
+ expect(custom).not.toBeNull();
394
+ expect(custom?.className).toBe('');
395
+ });
396
+
340
397
  // =========================================================================
341
398
  // Icon registry integration
342
399
  // =========================================================================
@@ -12,6 +12,9 @@
12
12
  * - Root container: layout-only wrapper (flex column), no visual styling, no theme target
13
13
  * - Header area (themeProps 'banner'): colored status background with icon, title, description, actions, dismiss
14
14
  * - Content area (themeProps 'banner-content'): collapsible card background for additional content (children)
15
+ * - Status icon (themeProps 'banner-icon'): the target rides on the default
16
+ * <Icon> itself — the element that paints — so 'status:X' overrides reach
17
+ * the glyph (#4166); for a custom `icon` node it stays on the layout wrapper
15
18
  * - No left border accent — color is expressed through the full header background
16
19
  * - Each visual area owns its own border-radius (no overflow:clip on the container)
17
20
  * - When children are provided, a collapse/expand toggle button appears in the end area
@@ -445,16 +448,32 @@ export function Banner({
445
448
  : styles.headerCardStandalone),
446
449
  ),
447
450
  )}>
451
+ {/* The 'banner-icon' target rides on the element that paints: the
452
+ default <Icon> below, or this wrapper when a custom `icon` node
453
+ is passed (core never injects props into consumer elements, so
454
+ overrides reach it via inheritance). The wrapper itself stays
455
+ layout-only. */}
448
456
  <div
449
- {...mergeProps(
450
- themeProps('banner-icon', {status}),
451
- stylex.props(styles.iconWrapper),
452
- )}
457
+ {...(icon != null
458
+ ? mergeProps(
459
+ themeProps('banner-icon', {status}),
460
+ stylex.props(styles.iconWrapper),
461
+ )
462
+ : stylex.props(styles.iconWrapper))}
453
463
  aria-hidden="true">
454
464
  {icon != null ? (
455
465
  icon
456
466
  ) : (
457
- <Icon icon={defaultIconName} size="md" color={iconColor} />
467
+ // Applied to the status <Icon> itself rather than the wrapper, so
468
+ // the element that paints the glyph is the element a theme
469
+ // targets — a 'banner-icon' 'status:X' color override beats the
470
+ // Icon's own variant from @layer astryx-theme (#4166).
471
+ <Icon
472
+ icon={defaultIconName}
473
+ size="md"
474
+ color={iconColor}
475
+ {...themeProps('banner-icon', {status})}
476
+ />
458
477
  )}
459
478
  </div>
460
479
  <div {...stylex.props(styles.headerContent)}>
@@ -1,7 +1,5 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- 'use client';
4
-
5
3
  /**
6
4
  * @file Blockquote.tsx
7
5
  * @input Uses React, stylex, spacing and color tokens
@@ -1,7 +1,5 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- 'use client';
4
-
5
3
  /**
6
4
  * @file index.ts
7
5
  * @input Imports from Blockquote.tsx
@@ -163,7 +163,7 @@ export const docs = {
163
163
  ],
164
164
  vars: [
165
165
  {name: '--_button-radius', description: 'Border radius', default: 'var(--radius-element)', private: true},
166
- {name: '--button-focus-offset', description: 'Focus ring outline offset', default: '3px'},
166
+ {name: '--button-focus-offset', description: 'Focus ring outline offset', default: 'var(--focus-outline-offset)'},
167
167
  {name: '--button-icon-only-aspect', description: 'Aspect ratio for icon-only buttons', default: '1 / 1'},
168
168
  ],
169
169
  derived: [
@@ -245,7 +245,7 @@ export const docsZh = {
245
245
  ],
246
246
  vars: [
247
247
  {name: '--_button-radius', description: '圆角半径', default: 'var(--radius-element)', private: true},
248
- {name: '--button-focus-offset', description: '焦点环轮廓偏移', default: '3px'},
248
+ {name: '--button-focus-offset', description: '焦点环轮廓偏移', default: 'var(--focus-outline-offset)'},
249
249
  {name: '--button-icon-only-aspect', description: '纯图标按钮的宽高比', default: '1 / 1'},
250
250
  ],
251
251
  derived: [
@@ -34,6 +34,7 @@ import {
34
34
  fontWeightVars,
35
35
  typeScaleVars,
36
36
  shadowVars,
37
+ focusVars,
37
38
  } from '../theme/tokens.stylex';
38
39
  import {Spinner} from '../Spinner';
39
40
  import {VisuallyHidden} from '../VisuallyHidden';
@@ -57,10 +58,10 @@ import type {ButtonVariantMap} from './index';
57
58
  const styles = stylex.create({
58
59
  base: {
59
60
  // Kept as a public themeable var (documented in Button.doc.mjs) even though
60
- // its default now matches the shared outline: removing it would break any
61
- // theme setting it, for no gain. It overrides the shared offset, so a theme
62
- // can still tune the ring distance on buttons specifically.
63
- '--button-focus-offset': '3px',
61
+ // it now defaults to the shared token: removing it would break any theme
62
+ // setting it, for no gain. It overrides the shared offset, so a theme can
63
+ // still tune the ring distance on buttons specifically.
64
+ '--button-focus-offset': focusVars['--focus-outline-offset'],
64
65
  outlineOffset: {
65
66
  default: '0',
66
67
  ':focus-visible': 'var(--button-focus-offset)',
@@ -437,8 +438,10 @@ const loadingStyles = stylex.create({
437
438
  *
438
439
  * Layers always carry the native `popover` attribute (useLayer.tsx), and a
439
440
  * popover is never an in-flow member — it is `display: none` until shown, then
440
- * promoted to the top layer. So "last member" is: no following element sibling
441
- * that isn't a popover.
441
+ * promoted to the top layer. Context layers also retain an inert `<template>`
442
+ * marker so they can re-resolve their JSX position. Neither element is a group
443
+ * member, so "last member" is: no following element sibling besides those two
444
+ * pieces of layer infrastructure.
442
445
  *
443
446
  * Reading it the other way round — marking the *buttons* and testing for a
444
447
  * marked sibling — is the trap: it silently reclassifies anything it doesn't
@@ -454,7 +457,7 @@ const loadingStyles = stylex.create({
454
457
  * The leading edge still uses `:first-child` — a member's button always precedes
455
458
  * its own layer, so the first button is genuinely `:first-child`.
456
459
  */
457
- const IS_LAST_ITEM = ':not(:has(~ *:not([popover])))';
460
+ const IS_LAST_ITEM = ':not(:has(~ *:not([popover]):not(template)))';
458
461
 
459
462
  const groupStyles = stylex.create({
460
463
  horizontal: {
@@ -202,9 +202,9 @@ describe('ButtonGroup', () => {
202
202
  // Trailing radius (issue #2508)
203
203
  //
204
204
  // The trailing end cap cannot be keyed off `:last-child`: members render an
205
- // invisible layer AFTER their button (tooltip'd Button, DropdownMenu), and
206
- // useLayer renders it inline rather than portaling it, so the layer steals the
207
- // slot. See IS_LAST_ITEM in Button.tsx.
205
+ // invisible layer infrastructure AFTER their button (tooltip'd Button,
206
+ // DropdownMenu), and useLayer renders a marker plus the layer inline when the
207
+ // host is safe, so they steal the slot. See IS_LAST_ITEM in Button.tsx.
208
208
  //
209
209
  // HOW THESE TESTS CATCH THE BUG
210
210
  // jsdom applies no StyleX CSS, so a DOM-only test cannot prove which rule
@@ -219,7 +219,9 @@ describe('ButtonGroup', () => {
219
219
  describe('trailing radius (#2508)', () => {
220
220
  /** The group members, in DOM order (excludes invisible layer siblings). */
221
221
  const items = (group: HTMLElement): Element[] =>
222
- Array.from(group.querySelectorAll(':scope > *:not([popover])'));
222
+ Array.from(
223
+ group.querySelectorAll(':scope > *:not([popover]):not(template)'),
224
+ );
223
225
 
224
226
  // -- Compiled CSS, read from the source -----------------------------------
225
227
 
@@ -375,6 +377,7 @@ describe('ButtonGroup', () => {
375
377
  // .stylex.ts file it compiles to a mangled selector like `[x13pbwiz]`
376
378
  // that matches nothing in the DOM.
377
379
  expect(selector).toContain('[popover]');
380
+ expect(selector).toContain('template');
378
381
  }
379
382
  },
380
383
  );
package/src/Card/Card.tsx CHANGED
@@ -1,7 +1,5 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- 'use client';
4
-
5
3
  /**
6
4
  * @file Card.tsx
7
5
  * @input Uses container utility, StyleX
package/src/Card/index.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- 'use client';
4
-
5
3
  /**
6
4
  * @file index.ts
7
5
  * @input Imports Card component
@@ -1,7 +1,5 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- 'use client';
4
-
5
3
  /**
6
4
  * @file Center.tsx
7
5
  * @input Uses React, StyleX for centering styles, Layout padding.stylex for spacing-scale padding
@@ -1,7 +1,5 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- 'use client';
4
-
5
3
  /**
6
4
  * @file index.ts
7
5
  * @input Imports Center component
@@ -17,6 +17,7 @@ import {Theme} from '../theme/Theme';
17
17
  import {defineTheme} from '../theme/defineTheme';
18
18
  import {getForcedColorsRules} from '../__tests__/forcedColors';
19
19
  import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
20
+ import {FOCUS_OUTLINE_PARTS} from '../utils/focusOutline.stylex';
20
21
 
21
22
  afterEach(() => {
22
23
  __resetLiveRegionsForTest();
@@ -686,7 +687,9 @@ describe('focus ring ownership (WCAG 2.4.7)', () => {
686
687
  <CheckboxInput label="Accept" value={false} onChange={() => {}} />,
687
688
  );
688
689
  focusInput(container);
689
- expect(indicatorOf(container).style.outlineStyle).toBe('solid');
690
+ expect(indicatorOf(container).style.outlineStyle).toBe(
691
+ FOCUS_OUTLINE_PARTS.outlineStyle,
692
+ );
690
693
  });
691
694
 
692
695
  it('paints it on a replacement that forwards nothing', () => {
@@ -701,7 +704,7 @@ describe('focus ring ownership (WCAG 2.4.7)', () => {
701
704
  expect(replaced.className).toBe('');
702
705
  // ...and it still gets a ring, because the owner drew it.
703
706
  focusInput(container);
704
- expect(replaced.style.outlineStyle).toBe('solid');
707
+ expect(replaced.style.outlineStyle).toBe(FOCUS_OUTLINE_PARTS.outlineStyle);
705
708
  });
706
709
 
707
710
  it('clears the ring on blur', () => {
@@ -709,7 +712,9 @@ describe('focus ring ownership (WCAG 2.4.7)', () => {
709
712
  <CheckboxInput label="Accept" value={false} onChange={() => {}} />,
710
713
  );
711
714
  const input = focusInput(container);
712
- expect(indicatorOf(container).style.outlineStyle).toBe('solid');
715
+ expect(indicatorOf(container).style.outlineStyle).toBe(
716
+ FOCUS_OUTLINE_PARTS.outlineStyle,
717
+ );
713
718
  fireEvent.blur(input);
714
719
  expect(indicatorOf(container).style.outlineStyle).toBe('');
715
720
  });
package/src/Code/Code.tsx CHANGED
@@ -1,7 +1,5 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- 'use client';
4
-
5
3
  /**
6
4
  * @file Code.tsx
7
5
  * @input Uses React, StyleX, theme tokens