@astryxdesign/core 0.4.1 → 0.4.2-canary.464a445

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 (182) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/Avatar/Avatar.d.ts +4 -1
  3. package/dist/Avatar/Avatar.d.ts.map +1 -1
  4. package/dist/Avatar/Avatar.js +19 -23
  5. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  6. package/dist/AvatarGroup/AvatarGroupOverflow.js +7 -2
  7. package/dist/Button/Button.d.ts.map +1 -1
  8. package/dist/Button/Button.js +9 -7
  9. package/dist/Chat/ChatMessage.d.ts +7 -0
  10. package/dist/Chat/ChatMessage.d.ts.map +1 -1
  11. package/dist/Chat/ChatMessageBubble.d.ts +13 -1
  12. package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
  13. package/dist/Chat/ChatMessageBubble.js +19 -1
  14. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  15. package/dist/CheckboxInput/CheckboxInput.js +5 -0
  16. package/dist/CommandPalette/CommandPaletteFooter.d.ts.map +1 -1
  17. package/dist/CommandPalette/CommandPaletteFooter.js +5 -3
  18. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  19. package/dist/DateTimeInput/DateTimeInput.js +2 -2
  20. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  21. package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -15
  22. package/dist/HoverCard/HoverCard.d.ts +1 -1
  23. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  24. package/dist/HoverCard/HoverCard.js +5 -13
  25. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  26. package/dist/HoverCard/useHoverCard.js +6 -5
  27. package/dist/InputGroup/groupStyles.d.ts.map +1 -1
  28. package/dist/InputGroup/groupStyles.js +7 -2
  29. package/dist/Layer/layerHost.d.ts +24 -0
  30. package/dist/Layer/layerHost.d.ts.map +1 -0
  31. package/dist/Layer/layerHost.js +79 -0
  32. package/dist/Layer/useLayer.d.ts +14 -6
  33. package/dist/Layer/useLayer.d.ts.map +1 -1
  34. package/dist/Layer/useLayer.js +158 -30
  35. package/dist/NavItem/navItemStyles.stylex.d.ts +17 -5
  36. package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
  37. package/dist/NavItem/navItemStyles.stylex.js +11 -5
  38. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  39. package/dist/RadioList/RadioListItem.js +5 -0
  40. package/dist/SideNav/SideNav.d.ts +7 -9
  41. package/dist/SideNav/SideNav.d.ts.map +1 -1
  42. package/dist/SideNav/SideNav.js +32 -5
  43. package/dist/SideNav/SideNavCollapseButton.d.ts +25 -9
  44. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  45. package/dist/SideNav/SideNavCollapseButton.js +39 -15
  46. package/dist/SideNav/SideNavCollapseContext.d.ts +21 -0
  47. package/dist/SideNav/SideNavCollapseContext.d.ts.map +1 -1
  48. package/dist/SideNav/SideNavCollapseContext.js +16 -2
  49. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  50. package/dist/SideNav/SideNavHeading.js +89 -32
  51. package/dist/SideNav/SideNavItem.d.ts +7 -2
  52. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  53. package/dist/SideNav/SideNavItem.js +119 -75
  54. package/dist/SideNav/SideNavSection.d.ts.map +1 -1
  55. package/dist/SideNav/SideNavSection.js +7 -16
  56. package/dist/SideNav/index.d.ts +1 -1
  57. package/dist/SideNav/index.d.ts.map +1 -1
  58. package/dist/Slider/Slider.d.ts.map +1 -1
  59. package/dist/Slider/Slider.js +56 -15
  60. package/dist/Switch/Switch.d.ts.map +1 -1
  61. package/dist/Switch/Switch.js +5 -0
  62. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  63. package/dist/Thumbnail/Thumbnail.js +5 -0
  64. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  65. package/dist/TopNav/TopNavHeading.js +14 -6
  66. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  67. package/dist/TopNav/TopNavMegaMenu.js +25 -93
  68. package/dist/TopNav/TopNavMegaMenuItem.js +1 -1
  69. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  70. package/dist/TopNav/TopNavMenu.js +10 -5
  71. package/dist/astryx.css +13 -7
  72. package/dist/astryx.umd.js +50 -50
  73. package/dist/astryx.umd.js.map +4 -4
  74. package/dist/hooks/index.d.ts +1 -1
  75. package/dist/hooks/index.d.ts.map +1 -1
  76. package/dist/hooks/index.js +1 -1
  77. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  78. package/dist/hooks/useFocusTrap.js +16 -2
  79. package/dist/hooks/useMenuHover.d.ts +50 -5
  80. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  81. package/dist/hooks/useMenuHover.js +171 -51
  82. package/dist/theme/defineTheme.d.ts +8 -2
  83. package/dist/theme/defineTheme.d.ts.map +1 -1
  84. package/dist/theme/defineTheme.js +8 -1
  85. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  86. package/dist/theme/derivedVarRegistry.js +7 -0
  87. package/dist/theme/expandColorScale.d.ts.map +1 -1
  88. package/dist/theme/expandColorScale.js +1 -0
  89. package/dist/theme/expandMotionScale.d.ts +1 -0
  90. package/dist/theme/expandMotionScale.d.ts.map +1 -1
  91. package/dist/theme/expandMotionScale.js +1 -0
  92. package/dist/theme/expandRadiusScale.d.ts +1 -0
  93. package/dist/theme/expandRadiusScale.d.ts.map +1 -1
  94. package/dist/theme/expandRadiusScale.js +1 -0
  95. package/dist/theme/expandTypeScale.d.ts +1 -0
  96. package/dist/theme/expandTypeScale.d.ts.map +1 -1
  97. package/dist/theme/expandTypeScale.js +1 -0
  98. package/dist/theme/tokens.stylex.d.ts +3 -1
  99. package/dist/theme/tokens.stylex.d.ts.map +1 -1
  100. package/dist/theme/tokens.stylex.js +3 -1
  101. package/locales/en.json +84 -0
  102. package/locales/pseudo.json +63 -0
  103. package/package.json +3 -3
  104. package/src/Avatar/Avatar.doc.mjs +5 -2
  105. package/src/Avatar/Avatar.test.tsx +51 -0
  106. package/src/Avatar/Avatar.tsx +30 -20
  107. package/src/AvatarGroup/AvatarGroup.doc.mjs +1 -1
  108. package/src/AvatarGroup/AvatarGroup.test.tsx +14 -0
  109. package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -2
  110. package/src/Button/Button.tsx +5 -3
  111. package/src/ButtonGroup/ButtonGroup.test.tsx +7 -4
  112. package/src/Card/Card.doc.mjs +2 -0
  113. package/src/Chat/Chat.doc.mjs +4 -1
  114. package/src/Chat/ChatMessage.doc.mjs +3 -3
  115. package/src/Chat/ChatMessage.tsx +7 -0
  116. package/src/Chat/ChatMessageBubble.doc.mjs +7 -0
  117. package/src/Chat/ChatMessageBubble.test.tsx +95 -6
  118. package/src/Chat/ChatMessageBubble.tsx +25 -0
  119. package/src/CheckboxInput/CheckboxInput.tsx +20 -0
  120. package/src/CodeBlock/CodeBlock.doc.mjs +3 -0
  121. package/src/CommandPalette/CommandPaletteFooter.tsx +6 -3
  122. package/src/DateTimeInput/DateTimeInput.tsx +4 -2
  123. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +72 -1
  124. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +24 -20
  125. package/src/HoverCard/HoverCard.doc.mjs +3 -3
  126. package/src/HoverCard/HoverCard.test.tsx +283 -55
  127. package/src/HoverCard/HoverCard.tsx +5 -13
  128. package/src/HoverCard/useHoverCard.tsx +8 -11
  129. package/src/InputGroup/groupStyles.ts +7 -8
  130. package/src/Item/Item.doc.mjs +4 -0
  131. package/src/Layer/layerHost.test.ts +99 -0
  132. package/src/Layer/layerHost.ts +141 -0
  133. package/src/Layer/useLayer.doc.mjs +14 -4
  134. package/src/Layer/useLayer.test.tsx +332 -7
  135. package/src/Layer/useLayer.tsx +235 -36
  136. package/src/MobileNav/MobileNav.doc.mjs +7 -0
  137. package/src/NavItem/navItemStyles.stylex.ts +31 -5
  138. package/src/RadioList/RadioListItem.tsx +20 -0
  139. package/src/SideNav/SideNav.doc.mjs +13 -4
  140. package/src/SideNav/SideNav.test.tsx +858 -2
  141. package/src/SideNav/SideNav.tsx +37 -16
  142. package/src/SideNav/SideNavCollapseButton.doc.mjs +28 -6
  143. package/src/SideNav/SideNavCollapseButton.tsx +67 -15
  144. package/src/SideNav/SideNavCollapseContext.ts +29 -9
  145. package/src/SideNav/SideNavHeading.tsx +53 -25
  146. package/src/SideNav/SideNavItem.doc.mjs +13 -0
  147. package/src/SideNav/SideNavItem.tsx +97 -84
  148. package/src/SideNav/SideNavSection.tsx +6 -20
  149. package/src/SideNav/index.ts +2 -0
  150. package/src/Slider/Slider.test.tsx +69 -33
  151. package/src/Slider/Slider.tsx +65 -13
  152. package/src/Switch/Switch.tsx +20 -0
  153. package/src/TabList/TabList.doc.mjs +3 -0
  154. package/src/Text/Text.doc.mjs +1 -1
  155. package/src/Thumbnail/Thumbnail.doc.mjs +3 -0
  156. package/src/Thumbnail/Thumbnail.tsx +10 -0
  157. package/src/Timestamp/Timestamp.test.tsx +40 -22
  158. package/src/Toolbar/Toolbar.test.tsx +14 -14
  159. package/src/TopNav/TopNav.test.tsx +86 -1
  160. package/src/TopNav/TopNavHeading.tsx +21 -14
  161. package/src/TopNav/TopNavMegaMenu.test.tsx +43 -0
  162. package/src/TopNav/TopNavMegaMenu.tsx +29 -105
  163. package/src/TopNav/TopNavMegaMenuItem.tsx +1 -1
  164. package/src/TopNav/TopNavMenu.test.tsx +41 -0
  165. package/src/TopNav/TopNavMenu.tsx +14 -4
  166. package/src/TreeList/TreeList.doc.mjs +1 -0
  167. package/src/hooks/index.ts +5 -1
  168. package/src/hooks/useFocusTrap.test.tsx +16 -0
  169. package/src/hooks/useFocusTrap.ts +20 -2
  170. package/src/hooks/useMenuHover.test.tsx +364 -0
  171. package/src/hooks/useMenuHover.ts +243 -47
  172. package/src/theme/defineTheme.ts +9 -0
  173. package/src/theme/derivedVarRegistry.test.ts +78 -12
  174. package/src/theme/derivedVarRegistry.ts +4 -0
  175. package/src/theme/expandColorScale.ts +1 -0
  176. package/src/theme/expandMotionScale.ts +1 -0
  177. package/src/theme/expandRadiusScale.ts +1 -0
  178. package/src/theme/expandTypeScale.ts +1 -0
  179. package/src/theme/themingTargets.test.ts +152 -26
  180. package/src/theme/tokens.stylex.ts +3 -1
  181. package/src/theme/tokens.test.ts +12 -0
  182. package/src/theme/useTheme.test.tsx +18 -0
@@ -42,6 +42,7 @@
42
42
  * SYNC: When modified, update:
43
43
  * - /packages/core/src/theme/expandTypeScale.test.ts
44
44
  * - /packages/core/src/theme/defineTheme.ts
45
+ * - /packages/cli/assets/theme.template.ts (the annotated field reference)
45
46
  */
46
47
  /** Font weight value — either a CSS string or a var() reference. */
47
48
  export type FontWeightValue = string;
@@ -1 +1 @@
1
- {"version":3,"file":"expandTypeScale.d.ts","sourceRoot":"","sources":["../../src/theme/expandTypeScale.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAMH,oEAAoE;AACpE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AAErC;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAC1C,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,eAAe,CAAC,CAC/C,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,OAAO,CACvC,MAAM,CACF,MAAM,GACN,OAAO,GACP,OAAO,GACP,MAAM,GACN,YAAY,GACZ,WAAW,GACX,WAAW,GACX,WAAW,GACX,CAAC,MAAM,GAAG,EAAE,CAAC,EACf,eAAe,CAChB,CACF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,OAAO,CAAC,EAAE;QACR,yEAAyE;QACzE,OAAO,CAAC,EAAE,sBAAsB,CAAC;QACjC,oEAAoE;QACpE,IAAI,CAAC,EAAE,mBAAmB,CAAC;KAC5B,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAwIrD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CA+CxE;AAiBD;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,eAAe,GACvB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAmDxD"}
1
+ {"version":3,"file":"expandTypeScale.d.ts","sourceRoot":"","sources":["../../src/theme/expandTypeScale.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAMH,oEAAoE;AACpE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AAErC;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAC1C,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,eAAe,CAAC,CAC/C,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,OAAO,CACvC,MAAM,CACF,MAAM,GACN,OAAO,GACP,OAAO,GACP,MAAM,GACN,YAAY,GACZ,WAAW,GACX,WAAW,GACX,WAAW,GACX,CAAC,MAAM,GAAG,EAAE,CAAC,EACf,eAAe,CAChB,CACF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,OAAO,CAAC,EAAE;QACR,yEAAyE;QACzE,OAAO,CAAC,EAAE,sBAAsB,CAAC;QACjC,oEAAoE;QACpE,IAAI,CAAC,EAAE,mBAAmB,CAAC;KAC5B,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAwIrD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CA+CxE;AAiBD;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,eAAe,GACvB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAmDxD"}
@@ -44,6 +44,7 @@
44
44
  * SYNC: When modified, update:
45
45
  * - /packages/core/src/theme/expandTypeScale.test.ts
46
46
  * - /packages/core/src/theme/defineTheme.ts
47
+ * - /packages/cli/assets/theme.template.ts (the annotated field reference)
47
48
  */
48
49
 
49
50
  // =============================================================================
@@ -6,7 +6,9 @@
6
6
  * - *Vars: CSS custom properties that themes can override via createTheme
7
7
  *
8
8
  * SYNC: When modified, run `node scripts/generate-token-docs.mjs` to update docs.
9
- * CI checks for drift via --check flag.
9
+ * CI checks for drift via --check flag. Adding or removing a token FAMILY (a
10
+ * whole `*Defaults` group) also means updating:
11
+ * - /packages/cli/assets/theme.template.ts (its inventory of what exists)
10
12
  *
11
13
  * Domain tokens (syntax highlighting, data visualization) live separately in
12
14
  * /packages/core/src/theme/domainTokens/ — they're tree-shaken from core components.
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.stylex.d.ts","sourceRoot":"","sources":["../../src/theme/tokens.stylex.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAM3C,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyHhB,CAAC;AAEX,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAAmC,CAAC;AAM1D,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;CAgBlB,CAAC;AAEX,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;WAAqC,CAAC;AAM9D,eAAO,MAAM,YAAY;;;;CAIf,CAAC;AAEX,eAAO,MAAM,QAAQ;;;;WAAkC,CAAC;AAMxD,eAAO,MAAM,cAAc;;CAEjB,CAAC;AAEX,eAAO,MAAM,UAAU;;WAAoC,CAAC;AAE5D,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,cAAc,CAAC;AAWxD,eAAO,MAAM,aAAa;;;;;CAKhB,CAAC;AAEX,eAAO,MAAM,SAAS;;;;;WAAmC,CAAC;AAM1D,eAAO,MAAM,cAAc;;;;;;;;CAWjB,CAAC;AAEX,eAAO,MAAM,UAAU;;;;;;;;WAAoC,CAAC;AAQ5D,eAAO,MAAM,cAAc;;;;;;;;;CAejB,CAAC;AAEX,eAAO,MAAM,UAAU;;;;;;;;;WAAoC,CAAC;AAU5D,eAAO,MAAM,gBAAgB;;;;;;;;;;CAUnB,CAAC;AAEX,eAAO,MAAM,YAAY;;;;;;;;;;WAAsC,CAAC;AAEhE,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,gBAAgB,CAAC;AAM5D,eAAO,MAAM,YAAY;;CAEf,CAAC;AAEX,eAAO,MAAM,QAAQ;;WAAkC,CAAC;AAExD,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,YAAY,CAAC;AAKpD,eAAO,MAAM,kBAAkB;;;;CAMrB,CAAC;AAEX,eAAO,MAAM,cAAc;;;;WAAwC,CAAC;AAMpE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;CAcnB,CAAC;AAEX,eAAO,MAAM,YAAY;;;;;;;;;;;;;WAAsC,CAAC;AAMhE,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AAEX,eAAO,MAAM,cAAc;;;;;WAAwC,CAAC;AAMpE,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,aAAa,CAAC;AACtD,MAAM,MAAM,cAAc,GAAG,MAAM,OAAO,eAAe,CAAC;AAC1D,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,YAAY,CAAC;AACpD,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,cAAc,CAAC;AACxD,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,cAAc,CAAC;AACxD,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,kBAAkB,CAAC;AAChE,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,gBAAgB,CAAC;AAC5D,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,kBAAkB,CAAC;AAehE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0DpB,CAAC;AAEX,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAAuC,CAAC;AAElE,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,iBAAiB,CAAC"}
1
+ {"version":3,"file":"tokens.stylex.d.ts","sourceRoot":"","sources":["../../src/theme/tokens.stylex.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAM3C,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyHhB,CAAC;AAEX,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAAmC,CAAC;AAM1D,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;CAgBlB,CAAC;AAEX,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;WAAqC,CAAC;AAM9D,eAAO,MAAM,YAAY;;;;CAIf,CAAC;AAEX,eAAO,MAAM,QAAQ;;;;WAAkC,CAAC;AAMxD,eAAO,MAAM,cAAc;;CAEjB,CAAC;AAEX,eAAO,MAAM,UAAU;;WAAoC,CAAC;AAE5D,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,cAAc,CAAC;AAWxD,eAAO,MAAM,aAAa;;;;;CAKhB,CAAC;AAEX,eAAO,MAAM,SAAS;;;;;WAAmC,CAAC;AAM1D,eAAO,MAAM,cAAc;;;;;;;;CAWjB,CAAC;AAEX,eAAO,MAAM,UAAU;;;;;;;;WAAoC,CAAC;AAQ5D,eAAO,MAAM,cAAc;;;;;;;;;CAejB,CAAC;AAEX,eAAO,MAAM,UAAU;;;;;;;;;WAAoC,CAAC;AAU5D,eAAO,MAAM,gBAAgB;;;;;;;;;;CAUnB,CAAC;AAEX,eAAO,MAAM,YAAY;;;;;;;;;;WAAsC,CAAC;AAEhE,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,gBAAgB,CAAC;AAM5D,eAAO,MAAM,YAAY;;CAEf,CAAC;AAEX,eAAO,MAAM,QAAQ;;WAAkC,CAAC;AAExD,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,YAAY,CAAC;AAKpD,eAAO,MAAM,kBAAkB;;;;CAMrB,CAAC;AAEX,eAAO,MAAM,cAAc;;;;WAAwC,CAAC;AAMpE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;CAcnB,CAAC;AAEX,eAAO,MAAM,YAAY;;;;;;;;;;;;;WAAsC,CAAC;AAMhE,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AAEX,eAAO,MAAM,cAAc;;;;;WAAwC,CAAC;AAMpE,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,aAAa,CAAC;AACtD,MAAM,MAAM,cAAc,GAAG,MAAM,OAAO,eAAe,CAAC;AAC1D,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,YAAY,CAAC;AACpD,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,cAAc,CAAC;AACxD,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,cAAc,CAAC;AACxD,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,kBAAkB,CAAC;AAChE,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,gBAAgB,CAAC;AAC5D,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,kBAAkB,CAAC;AAehE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0DpB,CAAC;AAEX,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAAuC,CAAC;AAElE,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,iBAAiB,CAAC"}
@@ -8,7 +8,9 @@
8
8
  * - *Vars: CSS custom properties that themes can override via createTheme
9
9
  *
10
10
  * SYNC: When modified, run `node scripts/generate-token-docs.mjs` to update docs.
11
- * CI checks for drift via --check flag.
11
+ * CI checks for drift via --check flag. Adding or removing a token FAMILY (a
12
+ * whole `*Defaults` group) also means updating:
13
+ * - /packages/cli/assets/theme.template.ts (its inventory of what exists)
12
14
  *
13
15
  * Domain tokens (syntax highlighting, data visualization) live separately in
14
16
  * /packages/core/src/theme/domainTokens/ — they're tree-shaken from core components.
package/locales/en.json CHANGED
@@ -419,6 +419,66 @@
419
419
  "defaultMessage": "Next",
420
420
  "description": "Screen-reader-only label on the right-arrow button in a Lightbox (navigates to next media item). Pairs with `lightbox.previous`."
421
421
  },
422
+ "@astryx.listInput.emptyTitle": {
423
+ "defaultMessage": "No {itemName}s yet",
424
+ "description": "EmptyState title shown inside a lab ListInput when its collection has no records. `{itemName}` is the consumer's singular noun for one record (e.g. \"guest\"); the source appends a literal \"s\" to pluralize it, which only works for regular English plurals. If your language cannot pluralize an interpolated noun this way, rephrase around `{itemName}` instead (e.g. \"No {itemName} added yet\")."
425
+ },
426
+ "@astryx.listInput.emptyDescription": {
427
+ "defaultMessage": "Add a {itemName} to get started.",
428
+ "description": "EmptyState supporting text shown inside a lab ListInput when its collection has no records. `{itemName}` is the consumer's singular noun for one record (e.g. \"guest\")."
429
+ },
430
+ "@astryx.listInput.addItem": {
431
+ "defaultMessage": "Add {itemName}",
432
+ "description": "Accessible label on the button that appends a new record to a lab ListInput. `{itemName}` is the consumer's singular noun for one record (e.g. \"guest\")."
433
+ },
434
+ "@astryx.listInput.removeItem": {
435
+ "defaultMessage": "Remove {itemName} {position, number}",
436
+ "description": "Accessible label and tooltip on the button that deletes one record from a lab ListInput. `{itemName}` is the consumer's singular noun for one record; `{position}` is its 1-based row number (e.g. \"Remove guest 2\")."
437
+ },
438
+ "@astryx.listInput.reorderItem": {
439
+ "defaultMessage": "Reorder {itemName} {position, number}",
440
+ "description": "Accessible label on the drag-handle button that reorders one record in a lab ListInput. `{itemName}` is the consumer's singular noun for one record; `{position}` is its 1-based row number (e.g. \"Reorder guest 2\")."
441
+ },
442
+ "@astryx.listInput.fieldLabelWithPosition": {
443
+ "defaultMessage": "{header}, {itemName} {position, number} of {total, number}",
444
+ "description": "Accessible name for a field inside a lab ListInput row after the first row, disambiguating repeated column labels. `{header}` is the column's own label (e.g. \"Name\"); `{itemName}` is the consumer's singular noun for one record; `{position}`/`{total}` are the row's 1-based index and the total row count (e.g. \"Name, guest 2 of 3\")."
445
+ },
446
+ "@astryx.listInput.reorderInstructions": {
447
+ "defaultMessage": "Use Arrow Up or Arrow Down to move this item one position. Press Space or Enter to pick it up for extended keyboard reordering.",
448
+ "description": "Visually-hidden instructions describing how to use a lab ListInput row's keyboard reorder handle, referenced via aria-describedby from every reorder button."
449
+ },
450
+ "@astryx.listInput.announceAdded": {
451
+ "defaultMessage": "Added {itemName} {position, number}.",
452
+ "description": "Screen-reader-only live announcement after a new record is appended to a lab ListInput. `{itemName}` is the consumer's singular noun for one record; `{position}` is the new record's 1-based row number."
453
+ },
454
+ "@astryx.listInput.announceRemoved": {
455
+ "defaultMessage": "Removed {itemName} {position, number}.",
456
+ "description": "Screen-reader-only live announcement after a record is deleted from a lab ListInput. `{itemName}` is the consumer's singular noun for one record; `{position}` is the removed record's former 1-based row number."
457
+ },
458
+ "@astryx.listInput.announceGrabbed": {
459
+ "defaultMessage": "{itemName} {position, number} grabbed. Use arrow keys to move, Space or Enter to drop, and Escape to cancel.",
460
+ "description": "Screen-reader-only live announcement when a lab ListInput record's keyboard reorder handle enters extended \"lift\" mode. `{itemName}` is the consumer's singular noun for one record; `{position}` is its 1-based row number."
461
+ },
462
+ "@astryx.listInput.announceMovedToPosition": {
463
+ "defaultMessage": "{itemName} moved to position {position, number} of {total, number}.",
464
+ "description": "Screen-reader-only live announcement each time a lab ListInput record's reorder position changes (arrow-key step, keyboard lift-mode preview, or pointer drag). `{itemName}` is the consumer's singular noun for one record; `{position}`/`{total}` are the record's new 1-based position and the total row count."
465
+ },
466
+ "@astryx.listInput.announceReorderCancelled": {
467
+ "defaultMessage": "Reordering cancelled.",
468
+ "description": "Screen-reader-only live announcement when a lab ListInput reorder in progress is cancelled (Escape key, blur, or the collection becoming disabled/loading mid-drag)."
469
+ },
470
+ "@astryx.listInput.announceReturnedToPosition": {
471
+ "defaultMessage": "{itemName} returned to position {position, number}.",
472
+ "description": "Screen-reader-only live announcement when a lab ListInput reorder is committed without the record's position actually changing. `{itemName}` is the consumer's singular noun for one record; `{position}` is its unchanged 1-based row number."
473
+ },
474
+ "@astryx.listInput.announceDropped": {
475
+ "defaultMessage": "{itemName} dropped at position {position, number} of {total, number}.",
476
+ "description": "Screen-reader-only live announcement when a lab ListInput reorder is committed with the record's position actually changing. `{itemName}` is the consumer's singular noun for one record; `{position}`/`{total}` are its new 1-based position and the total row count."
477
+ },
478
+ "@astryx.listInput.announceAlreadyAtBoundary": {
479
+ "defaultMessage": "This {itemName} is already {boundary, select, first {first} last {last} other {}}.",
480
+ "description": "Screen-reader-only live announcement when an arrow-key reorder attempt has no effect because the record is already at that end of the list. `{itemName}` is the consumer's singular noun for one record; `{boundary}` is always exactly \"first\" or \"last\"."
481
+ },
422
482
  "@astryx.markdown.taskList": {
423
483
  "defaultMessage": "Task list",
424
484
  "description": "Screen-reader-only accessible name for a GitHub-flavored Markdown task list (rendered from `- [ ] item` / `- [x] done` syntax)."
@@ -643,6 +703,14 @@
643
703
  "defaultMessage": "Invalid date",
644
704
  "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
705
  },
706
+ "@astryx.dateTimeInput.timeHint12h": {
707
+ "defaultMessage": "e.g., 2:30 PM",
708
+ "description": "Placeholder hint shown in the DateTimeInput time field when focused and empty with 12-hour format. Gives an example of expected input format."
709
+ },
710
+ "@astryx.dateTimeInput.timeHint24h": {
711
+ "defaultMessage": "e.g., 14:30",
712
+ "description": "Placeholder hint shown in the DateTimeInput time field when focused and empty with 24-hour format. Gives an example of expected input format."
713
+ },
646
714
  "@astryx.dateTimeInput.timeSuffix": {
647
715
  "defaultMessage": "{label} time",
648
716
  "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}\")."
@@ -935,6 +1003,10 @@
935
1003
  "defaultMessage": "Collapse {label}",
936
1004
  "description": "Screen-reader-only label on the same chevron when the group is expanded. Example: `Collapse Settings`. Pairs with `sideNavItem.expand`."
937
1005
  },
1006
+ "@astryx.sideNavItem.submenuLabel": {
1007
+ "defaultMessage": "{label} submenu",
1008
+ "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`."
1009
+ },
938
1010
  "@astryx.tableFiltering.filterByColumn": {
939
1011
  "defaultMessage": "Filter {header}",
940
1012
  "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."
@@ -1050,5 +1122,17 @@
1050
1122
  "@astryx.token.remove": {
1051
1123
  "defaultMessage": "Remove {label}",
1052
1124
  "description": "Screen-reader-only label on the \"×\" on an individual Token/chip. Example: `Remove John Smith`, `Remove Active`. Imperative verb."
1125
+ },
1126
+ "@astryx.commandPalette.footer.navigate": {
1127
+ "defaultMessage": "Navigate",
1128
+ "description": "Keyboard hint in the CommandPalette footer, paired with up/down arrow key icons. Tells users arrow keys move between items."
1129
+ },
1130
+ "@astryx.commandPalette.footer.select": {
1131
+ "defaultMessage": "Select",
1132
+ "description": "Keyboard hint in the CommandPalette footer, paired with the Enter key icon. Tells users Enter activates the highlighted item."
1133
+ },
1134
+ "@astryx.commandPalette.footer.close": {
1135
+ "defaultMessage": "Close",
1136
+ "description": "Keyboard hint in the CommandPalette footer, paired with the Escape key icon. Tells users Escape dismisses the palette."
1053
1137
  }
1054
1138
  }
@@ -314,6 +314,51 @@
314
314
  "@astryx.lightbox.next": {
315
315
  "defaultMessage": "⟦Ñéẋţ⟧"
316
316
  },
317
+ "@astryx.listInput.emptyTitle": {
318
+ "defaultMessage": "⟦Ñó {itemName}š ýéţ⟧"
319
+ },
320
+ "@astryx.listInput.emptyDescription": {
321
+ "defaultMessage": "⟦Àðð à {itemName} ţó ĝéţ šţàřţéð.⟧"
322
+ },
323
+ "@astryx.listInput.addItem": {
324
+ "defaultMessage": "⟦Àðð {itemName}⟧"
325
+ },
326
+ "@astryx.listInput.removeItem": {
327
+ "defaultMessage": "⟦Řéɱóṽé {itemName} {position, number}⟧"
328
+ },
329
+ "@astryx.listInput.reorderItem": {
330
+ "defaultMessage": "⟦Řéóřðéř {itemName} {position, number}⟧"
331
+ },
332
+ "@astryx.listInput.fieldLabelWithPosition": {
333
+ "defaultMessage": "⟦{header}, {itemName} {position, number} óƒ {total, number}⟧"
334
+ },
335
+ "@astryx.listInput.reorderInstructions": {
336
+ "defaultMessage": "⟦Úšé Àřřóŵ Úþ óř Àřřóŵ Ðóŵñ ţó ɱóṽé ţĥíš íţéɱ óñé þóšíţíóñ. Þřéšš Šþàçé óř Éñţéř ţó þíçķ íţ úþ ƒóř éẋţéñðéð ķéýƀóàřð řéóřðéříñĝ.⟧"
337
+ },
338
+ "@astryx.listInput.announceAdded": {
339
+ "defaultMessage": "⟦Àððéð {itemName} {position, number}.⟧"
340
+ },
341
+ "@astryx.listInput.announceRemoved": {
342
+ "defaultMessage": "⟦Řéɱóṽéð {itemName} {position, number}.⟧"
343
+ },
344
+ "@astryx.listInput.announceGrabbed": {
345
+ "defaultMessage": "⟦{itemName} {position, number} ĝřàƀƀéð. Úšé àřřóŵ ķéýš ţó ɱóṽé, Šþàçé óř Éñţéř ţó ðřóþ, àñð Éšçàþé ţó çàñçéł.⟧"
346
+ },
347
+ "@astryx.listInput.announceMovedToPosition": {
348
+ "defaultMessage": "⟦{itemName} ɱóṽéð ţó þóšíţíóñ {position, number} óƒ {total, number}.⟧"
349
+ },
350
+ "@astryx.listInput.announceReorderCancelled": {
351
+ "defaultMessage": "⟦Řéóřðéříñĝ çàñçéłłéð.⟧"
352
+ },
353
+ "@astryx.listInput.announceReturnedToPosition": {
354
+ "defaultMessage": "⟦{itemName} řéţúřñéð ţó þóšíţíóñ {position, number}.⟧"
355
+ },
356
+ "@astryx.listInput.announceDropped": {
357
+ "defaultMessage": "⟦{itemName} ðřóþþéð àţ þóšíţíóñ {position, number} óƒ {total, number}.⟧"
358
+ },
359
+ "@astryx.listInput.announceAlreadyAtBoundary": {
360
+ "defaultMessage": "⟦Ţĥíš {itemName} íš àłřéàðý {boundary, select, first {first} last {last} other {}}.⟧"
361
+ },
317
362
  "@astryx.markdown.taskList": {
318
363
  "defaultMessage": "⟦Ţàšķ łíšţ⟧"
319
364
  },
@@ -482,6 +527,12 @@
482
527
  "@astryx.dateInput.invalidDate": {
483
528
  "defaultMessage": "⟦Íñṽàłíð ðàţé⟧"
484
529
  },
530
+ "@astryx.dateTimeInput.timeHint12h": {
531
+ "defaultMessage": "⟦é.ĝ., 2:30 ÞṀ⟧"
532
+ },
533
+ "@astryx.dateTimeInput.timeHint24h": {
534
+ "defaultMessage": "⟦é.ĝ., 14:30⟧"
535
+ },
485
536
  "@astryx.dateTimeInput.timeSuffix": {
486
537
  "defaultMessage": "⟦{label} ţíɱé⟧"
487
538
  },
@@ -701,6 +752,9 @@
701
752
  "@astryx.sideNavItem.collapse": {
702
753
  "defaultMessage": "⟦Çółłàþšé {label}⟧"
703
754
  },
755
+ "@astryx.sideNavItem.submenuLabel": {
756
+ "defaultMessage": "⟦{label} šúƀɱéñú⟧"
757
+ },
704
758
  "@astryx.tableFiltering.filterByColumn": {
705
759
  "defaultMessage": "⟦Ƒíłţéř {header}⟧"
706
760
  },
@@ -787,5 +841,14 @@
787
841
  },
788
842
  "@astryx.token.remove": {
789
843
  "defaultMessage": "⟦Řéɱóṽé {label}⟧"
844
+ },
845
+ "@astryx.commandPalette.footer.navigate": {
846
+ "defaultMessage": "⟦Ñàṽíĝàţé⟧"
847
+ },
848
+ "@astryx.commandPalette.footer.select": {
849
+ "defaultMessage": "⟦Šéłéçţ⟧"
850
+ },
851
+ "@astryx.commandPalette.footer.close": {
852
+ "defaultMessage": "⟦Çłóšé⟧"
790
853
  }
791
854
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.4.1",
3
+ "version": "0.4.2-canary.464a445",
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.2-canary.464a445",
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.1"
666
+ "rimraf": "^6.0.1"
667
667
  },
668
668
  "dependencies": {
669
669
  "intl-messageformat": "^11.2.9"
@@ -33,6 +33,9 @@ export const docs = {
33
33
  {className: 'astryx-avatar-status-dot', visualProps: ['variant']},
34
34
  {className: 'astryx-avatar-status-dot-glyph', visualProps: ['shape']},
35
35
  ],
36
+ vars: [
37
+ {name: '--_avatar-group-overlap', description: 'Negative inline offset applied to every avatar after the first when avatars are stacked in an AvatarGroup. Set from the group size; a more negative value tightens the stack.', default: 'set at runtime from the group avatar size (px)', private: true},
38
+ ],
36
39
  },
37
40
  description: 'Displays a user avatar with image, initials fallback, and optional status indicator.',
38
41
  props: [
@@ -59,7 +62,7 @@ export const docs = {
59
62
  {
60
63
  name: 'size',
61
64
  type: "'xsm' | 'sm' | 'md' | 'lg' | 'xl' | number",
62
- description: "Avatar size. Use a named size ('xsm' 20px, 'sm' 24px, 'md' 36px, 'lg' 48px, 'xl' 128px) or a numeric pixel value. Avatar shares Icon's abbreviated scale, but its tiers are larger because avatars align with media rather than glyphs.",
65
+ description: "Avatar size. Use a named size ('xsm' 20px, 'sm' 24px, 'md' 36px, 'lg' 48px, 'xl' 128px) or a numeric pixel value. Avatar shares Icon's abbreviated scale, but its tiers are larger because avatars align with media rather than glyphs. Inside an AvatarGroup the group's size wins and this prop is ignored.",
63
66
  default: "'md'",
64
67
  },
65
68
  {
@@ -151,7 +154,7 @@ export const docsDense = {
151
154
  fallbackSrc: 'fallback image when primary fails',
152
155
  name: 'user name for initials and alt text',
153
156
  alt: 'alt text; falls back to name',
154
- size: "avatar size. Named ('xsm' 20px, 'sm' 24px, 'md' 36px, 'lg' 48px, 'xl' 128px) or numeric px.",
157
+ size: "avatar size. Named ('xsm' 20px, 'sm' 24px, 'md' 36px, 'lg' 48px, 'xl' 128px) or numeric px. An AvatarGroup's size overrides it.",
155
158
  status:
156
159
  'corner content for status indicators; its `label` is composed into the avatar accessible name ("Jane Doe, Online")',
157
160
  tooltip:
@@ -65,6 +65,27 @@ describe('Avatar', () => {
65
65
  expect(icon?.querySelector('svg')).not.toBeNull();
66
66
  });
67
67
 
68
+ it('puts the avatar box on the element that carries the theme target', () => {
69
+ // T7: `.astryx-avatar` documents a `size` visual prop, so the width and
70
+ // height that prop selects on must live on the targeted element — a theme
71
+ // rule that resizes the target has to resize the whole avatar, not leave a
72
+ // fixed-size circle inside a grown box.
73
+ render(<Avatar name="Ada Lovelace" size="lg" data-testid="a" />);
74
+ const root = screen.getByTestId('a');
75
+ expect(root.className).toContain('astryx-avatar');
76
+ const rootStyle = root.getAttribute('style') ?? '';
77
+ expect(rootStyle).toContain('48px');
78
+
79
+ const content = root.firstElementChild as HTMLElement;
80
+ expect(content.getAttribute('style') ?? '').not.toContain('48px');
81
+ });
82
+
83
+ it('keeps the box on the root for an interactive avatar too', () => {
84
+ render(<Avatar name="Ada" size="lg" href="/ada" />);
85
+ const link = screen.getByRole('link', {name: 'Ada'});
86
+ expect(link.getAttribute('style') ?? '').toContain('48px');
87
+ });
88
+
68
89
  it('does not split an emoji surrogate pair when generating initials', () => {
69
90
  render(<Avatar name="😀 Ada" data-testid="avatar" />);
70
91
  expect(screen.getByTestId('avatar')).toHaveTextContent('😀A');
@@ -184,6 +205,36 @@ describe('Avatar', () => {
184
205
  });
185
206
  });
186
207
 
208
+ describe('a whitespace-only name carries no identity', () => {
209
+ it('falls through to the default icon instead of an empty plate', () => {
210
+ render(<Avatar name=" " data-testid="a" />);
211
+ const el = screen.getByTestId('a');
212
+ expect(el.querySelector('svg')).not.toBeNull();
213
+ expect(el).toHaveTextContent('');
214
+ });
215
+
216
+ it('is decorative rather than a role="img" with a blank name', () => {
217
+ render(<Avatar name=" " data-testid="a" />);
218
+ const el = screen.getByTestId('a');
219
+ expect(el).toHaveAttribute('aria-hidden', 'true');
220
+ expect(el).not.toHaveAttribute('aria-label');
221
+ });
222
+
223
+ it('keeps a meaningful alt as the accessible name and still shows the icon', () => {
224
+ render(<Avatar name=" " alt="Profile photo" data-testid="a" />);
225
+ const el = screen.getByRole('img', {name: 'Profile photo'});
226
+ expect(el).toBe(screen.getByTestId('a'));
227
+ expect(el.querySelector('svg')).not.toBeNull();
228
+ });
229
+
230
+ it('treats a whitespace-only alt the same way', () => {
231
+ render(<Avatar alt=" " name="Ada Lovelace" data-testid="a" />);
232
+ expect(
233
+ screen.getByRole('img', {name: 'Ada Lovelace'}),
234
+ ).toBeInTheDocument();
235
+ });
236
+ });
237
+
187
238
  it('retries a new fallbackSrc after a previous fallbackSrc failed to load', () => {
188
239
  const {rerender} = render(
189
240
  <Avatar name="Ada" fallbackSrc="https://example.com/broken.jpg" />,
@@ -33,6 +33,7 @@ import {mergeProps, mergeRefs} from '../utils';
33
33
  import {themeProps} from '../utils/themeProps';
34
34
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
35
35
  import {useTooltip} from '../Tooltip/useTooltip';
36
+ import {useDevWarning} from '../hooks/useDevWarning';
36
37
  import {useLinkComponent} from '../Link/useLinkComponent';
37
38
  import type {LinkComponentType} from '../Link/types';
38
39
  import {useTranslator} from '../i18n';
@@ -112,16 +113,17 @@ const styles = stylex.create({
112
113
  position: 'relative',
113
114
  display: 'inline-flex',
114
115
  flexShrink: 0,
115
- // The wrapper is not clipped (so the status dot can overflow), so it must be
116
- // rounded itself: a theme can set a background on the `.astryx-avatar`
117
- // wrapper, and an unrounded wrapper would show that fill as square corners
118
- // behind the circular content.
116
+ // The wrapper carries the avatar's box as well as its radius, so a theme
117
+ // rule on the `.astryx-avatar` target reaches both: the size the `size`
118
+ // visual prop selects on is set here, and the content below fills it.
119
119
  borderRadius: radiusVars['--radius-full'],
120
120
  },
121
121
  content: {
122
122
  display: 'flex',
123
123
  alignItems: 'center',
124
124
  justifyContent: 'center',
125
+ width: '100%',
126
+ height: '100%',
125
127
  borderRadius: radiusVars['--radius-full'],
126
128
  overflow: 'hidden',
127
129
  userSelect: 'none',
@@ -251,6 +253,9 @@ export interface AvatarProps extends BaseProps<HTMLDivElement> {
251
253
  /**
252
254
  * The size of the avatar. A named size (`xsm` 20px, `sm` 24px, `md` 36px,
253
255
  * `lg` 48px, `xl` 128px) or a specific pixel value.
256
+ *
257
+ * Inside an `AvatarGroup` the group's `size` wins: a group sizes its members
258
+ * uniformly, so this prop is ignored there.
254
259
  * @default 'md'
255
260
  */
256
261
  size?: AvatarSize;
@@ -397,7 +402,7 @@ function DefaultIcon({size}: {size: number}) {
397
402
  * ```
398
403
  * <Avatar src="/user.jpg" name="John Doe" />
399
404
  * <Avatar name="Jane Smith" size="xl" />
400
- * <Avatar src="/user.jpg" status={<OnlineIndicator />} />
405
+ * <Avatar src="/user.jpg" status={<AvatarStatusDot variant="success" label="Online" />} />
401
406
  * <Avatar name="jsmith" tooltip="Jane Smith, Staff Engineer" />
402
407
  * <Avatar name="Jane" tooltip={false} />
403
408
  * <Avatar src="/user.jpg" name="John Doe" href="/users/john" />
@@ -434,8 +439,13 @@ export function Avatar({
434
439
  const showImage = src && erroredSrc !== src;
435
440
  const showFallbackImage =
436
441
  !showImage && fallbackSrc && erroredFallbackSrc !== fallbackSrc;
437
- const showInitials = !showImage && !showFallbackImage && name;
438
- const showIcon = !showImage && !showFallbackImage && !name;
442
+ // A whitespace-only string carries no identity. Without this it produces no
443
+ // initials (getInitials trims to nothing) and no default icon (a space is
444
+ // truthy), leaving an empty plate behind a blank accessible name.
445
+ const meaningfulName = name?.trim() ? name : undefined;
446
+ const meaningfulAlt = alt?.trim() ? alt : undefined;
447
+ const showInitials = !showImage && !showFallbackImage && meaningfulName;
448
+ const showIcon = !showImage && !showFallbackImage && !meaningfulName;
439
449
 
440
450
  // A meaningful accessible name comes from `alt`/`name`, composed with the
441
451
  // status element's `label` when one is present ("Jane Doe, Online") — the
@@ -446,7 +456,7 @@ export function Avatar({
446
456
  // it as `presentation`/`aria-hidden` rather than announcing a meaningless
447
457
  // generic "Avatar" (obs-9).
448
458
  const t = useTranslator();
449
- const nameLabel = alt || name;
459
+ const nameLabel = meaningfulAlt || meaningfulName;
450
460
  const statusLabel = getStatusLabel(status);
451
461
  const accessibleName =
452
462
  nameLabel && statusLabel
@@ -471,7 +481,7 @@ export function Avatar({
471
481
  ? undefined
472
482
  : typeof tooltip === 'string'
473
483
  ? tooltip
474
- : name;
484
+ : meaningfulName;
475
485
  const trimmedTooltip = tooltipContent?.trim();
476
486
  const showTooltip = trimmedTooltip != null && trimmedTooltip !== '';
477
487
  // Whether the tooltip text is a consumer-authored override (a custom string)
@@ -513,20 +523,19 @@ export function Avatar({
513
523
  const LinkComponent = useLinkComponent(as);
514
524
 
515
525
  // An interactive control with no accessible name is an unacceptable control
516
- // name. Warn in the same client-safe way sibling components do (Field,
517
- // Timestamp, Popover) — a plain `console.warn`, never gated on `process.env`
518
- // (which is not available on the client in this codebase).
519
- if (isInteractive && !accessibleName) {
520
- console.warn(
521
- 'Avatar: an interactive avatar (with `href` or `onClick`) needs a ' +
522
- 'meaningful accessible name. Pass `alt` or `name`.',
523
- );
524
- }
526
+ // name. `useDevWarning` is the shared guardrail: it warns once per mount
527
+ // rather than on every render, and compiles out of production builds.
528
+ useDevWarning(
529
+ 'Avatar',
530
+ 'an interactive avatar (with `href` or `onClick`) needs a meaningful ' +
531
+ 'accessible name. Pass `alt` or `name`.',
532
+ isInteractive && !accessibleName,
533
+ );
525
534
 
526
535
  // The inner visuals are identical across the static and interactive variants.
527
536
  const visualContent = (
528
537
  <>
529
- <div {...stylex.props(styles.content, dynamicStyles.size(numericSize))}>
538
+ <div {...stylex.props(styles.content)}>
530
539
  {showImage && (
531
540
  <img
532
541
  src={src}
@@ -552,7 +561,7 @@ export function Avatar({
552
561
  dynamicStyles.fontSize(numericSize),
553
562
  ),
554
563
  )}>
555
- {getInitials(name)}
564
+ {getInitials(meaningfulName)}
556
565
  </div>
557
566
  )}
558
567
  {showIcon && (
@@ -585,6 +594,7 @@ export function Avatar({
585
594
  themeProps('avatar', {size: resolvedSize}),
586
595
  focusOutlineProps.focusVisible(
587
596
  styles.wrapper,
597
+ dynamicStyles.size(numericSize),
588
598
  isInteractive && styles.interactive,
589
599
  avatarGroup && groupStyles.ring,
590
600
  avatarGroup && groupStyles.overlap,
@@ -25,7 +25,7 @@ export const docs = {
25
25
  theming: {
26
26
  targets: [
27
27
  {className: 'astryx-avatar-group', visualProps: ['size']},
28
- {className: 'astryx-avatar-group-overflow'},
28
+ {className: 'astryx-avatar-group-overflow', visualProps: ['size']},
29
29
  ],
30
30
  },
31
31
  description: 'Stacked avatar display with overlapping layout and optional overflow indicator. Children are Avatar elements.',
@@ -104,6 +104,20 @@ describe('AvatarGroupOverflow', () => {
104
104
  expect(overflow).toHaveTextContent('+5');
105
105
  });
106
106
 
107
+ it('applies the group size class to the overflow chip', () => {
108
+ render(
109
+ <AvatarGroup size="lg">
110
+ <Avatar name="Alice" />
111
+ <AvatarGroupOverflow count={5} />
112
+ </AvatarGroup>,
113
+ );
114
+
115
+ const overflow = screen.getByLabelText('5 more');
116
+ expect(overflow.className).toContain('astryx-avatar-group-overflow');
117
+ expect(overflow.className).toContain('lg');
118
+ expect(overflow).toHaveAttribute('data-size', 'lg');
119
+ });
120
+
107
121
  it('renders as button when onClick is provided', () => {
108
122
  render(
109
123
  <AvatarGroup>
@@ -146,6 +146,7 @@ export function AvatarGroupOverflow({
146
146
  }: AvatarGroupOverflowProps): ReactNode {
147
147
  const t = useTranslator();
148
148
  const group = useAvatarGroup();
149
+ const size = group?.size ?? 'md';
149
150
  const numericSize = group?.numericSize ?? 36;
150
151
  const overlap = group?.overlap ?? 0;
151
152
 
@@ -162,7 +163,7 @@ export function AvatarGroupOverflow({
162
163
  aria-label={label}
163
164
  data-avatar-item=""
164
165
  {...mergeProps(
165
- themeProps('avatar-group-overflow'),
166
+ themeProps('avatar-group-overflow', {size}),
166
167
  focusOutlineProps.focusVisible(
167
168
  styles.base,
168
169
  styles.button,
@@ -186,7 +187,7 @@ export function AvatarGroupOverflow({
186
187
  {...rest}
187
188
  aria-label={label}
188
189
  {...mergeProps(
189
- themeProps('avatar-group-overflow'),
190
+ themeProps('avatar-group-overflow', {size}),
190
191
  stylex.props(
191
192
  styles.base,
192
193
  styles.overlap,
@@ -438,8 +438,10 @@ const loadingStyles = stylex.create({
438
438
  *
439
439
  * Layers always carry the native `popover` attribute (useLayer.tsx), and a
440
440
  * popover is never an in-flow member — it is `display: none` until shown, then
441
- * promoted to the top layer. So "last member" is: no following element sibling
442
- * 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.
443
445
  *
444
446
  * Reading it the other way round — marking the *buttons* and testing for a
445
447
  * marked sibling — is the trap: it silently reclassifies anything it doesn't
@@ -455,7 +457,7 @@ const loadingStyles = stylex.create({
455
457
  * The leading edge still uses `:first-child` — a member's button always precedes
456
458
  * its own layer, so the first button is genuinely `:first-child`.
457
459
  */
458
- const IS_LAST_ITEM = ':not(:has(~ *:not([popover])))';
460
+ const IS_LAST_ITEM = ':not(:has(~ *:not([popover]):not(template)))';
459
461
 
460
462
  const groupStyles = stylex.create({
461
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
  );
@@ -95,6 +95,8 @@ export const docs = {
95
95
  ],
96
96
  vars: [
97
97
  {name: '--_card-radius', description: 'Border radius of the card', default: 'var(--radius-container)', private: true},
98
+ {name: '--_card-elevation', description: 'Resting shadow of the card, set from the elevation prop. Composed into the card box-shadow list alongside --_card-ring rather than written as boxShadow directly, so a ring and an elevation can coexist.', default: '0 0 transparent', private: true},
99
+ {name: '--_card-ring', description: 'Inset ring drawn in the card box-shadow list. SelectableCard sets it to show selection without taking over the shadow.', default: '0 0 transparent', private: true},
98
100
  ],
99
101
  derived: [
100
102
  {property: 'borderRadius', vars: ['--_card-radius']},