@astryxdesign/core 0.4.1 → 0.4.2-canary.01592f7

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 (217) 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/Chat/ChatTokenizedText.js +1 -1
  15. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  16. package/dist/CheckboxInput/CheckboxInput.js +5 -0
  17. package/dist/CommandPalette/CommandPaletteFooter.d.ts.map +1 -1
  18. package/dist/CommandPalette/CommandPaletteFooter.js +5 -3
  19. package/dist/ComplexSelector/ComplexSelector.d.ts +38 -4
  20. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  21. package/dist/ComplexSelector/ComplexSelector.js +96 -35
  22. package/dist/ComplexSelector/index.d.ts +2 -2
  23. package/dist/ComplexSelector/index.d.ts.map +1 -1
  24. package/dist/ComplexSelector/index.js +1 -1
  25. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  26. package/dist/DateTimeInput/DateTimeInput.js +2 -2
  27. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  28. package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -15
  29. package/dist/HoverCard/HoverCard.d.ts +1 -1
  30. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  31. package/dist/HoverCard/HoverCard.js +5 -13
  32. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  33. package/dist/HoverCard/useHoverCard.js +6 -5
  34. package/dist/InputGroup/groupStyles.d.ts.map +1 -1
  35. package/dist/InputGroup/groupStyles.js +7 -2
  36. package/dist/Layer/layerHost.d.ts +24 -0
  37. package/dist/Layer/layerHost.d.ts.map +1 -0
  38. package/dist/Layer/layerHost.js +79 -0
  39. package/dist/Layer/useLayer.d.ts +14 -6
  40. package/dist/Layer/useLayer.d.ts.map +1 -1
  41. package/dist/Layer/useLayer.js +158 -30
  42. package/dist/Markdown/parser.d.ts.map +1 -1
  43. package/dist/Markdown/parser.js +55 -12
  44. package/dist/NavItem/navItemStyles.stylex.d.ts +17 -5
  45. package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
  46. package/dist/NavItem/navItemStyles.stylex.js +11 -5
  47. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  48. package/dist/RadioList/RadioListItem.js +5 -0
  49. package/dist/SideNav/SideNav.d.ts +7 -9
  50. package/dist/SideNav/SideNav.d.ts.map +1 -1
  51. package/dist/SideNav/SideNav.js +32 -5
  52. package/dist/SideNav/SideNavCollapseButton.d.ts +25 -9
  53. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  54. package/dist/SideNav/SideNavCollapseButton.js +39 -15
  55. package/dist/SideNav/SideNavCollapseContext.d.ts +21 -0
  56. package/dist/SideNav/SideNavCollapseContext.d.ts.map +1 -1
  57. package/dist/SideNav/SideNavCollapseContext.js +16 -2
  58. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  59. package/dist/SideNav/SideNavHeading.js +89 -32
  60. package/dist/SideNav/SideNavItem.d.ts +7 -2
  61. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  62. package/dist/SideNav/SideNavItem.js +119 -75
  63. package/dist/SideNav/SideNavSection.d.ts.map +1 -1
  64. package/dist/SideNav/SideNavSection.js +7 -16
  65. package/dist/SideNav/index.d.ts +1 -1
  66. package/dist/SideNav/index.d.ts.map +1 -1
  67. package/dist/Slider/Slider.d.ts.map +1 -1
  68. package/dist/Slider/Slider.js +56 -15
  69. package/dist/Switch/Switch.d.ts.map +1 -1
  70. package/dist/Switch/Switch.js +5 -0
  71. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  72. package/dist/Thumbnail/Thumbnail.js +5 -0
  73. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  74. package/dist/TopNav/TopNavHeading.js +14 -6
  75. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  76. package/dist/TopNav/TopNavMegaMenu.js +25 -93
  77. package/dist/TopNav/TopNavMegaMenuItem.js +1 -1
  78. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  79. package/dist/TopNav/TopNavMenu.js +10 -5
  80. package/dist/astryx.css +40 -8
  81. package/dist/astryx.umd.js +50 -50
  82. package/dist/astryx.umd.js.map +4 -4
  83. package/dist/hooks/containerReveal.stylex.d.ts +71 -4
  84. package/dist/hooks/containerReveal.stylex.d.ts.map +1 -1
  85. package/dist/hooks/containerReveal.stylex.js +57 -5
  86. package/dist/hooks/index.d.ts +2 -2
  87. package/dist/hooks/index.d.ts.map +1 -1
  88. package/dist/hooks/index.js +1 -1
  89. package/dist/hooks/useContainerReveal.d.ts +60 -3
  90. package/dist/hooks/useContainerReveal.d.ts.map +1 -1
  91. package/dist/hooks/useContainerReveal.js +27 -5
  92. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  93. package/dist/hooks/useFocusTrap.js +16 -2
  94. package/dist/hooks/useMenuHover.d.ts +50 -5
  95. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  96. package/dist/hooks/useMenuHover.js +171 -51
  97. package/dist/theme/defineTheme.d.ts +16 -5
  98. package/dist/theme/defineTheme.d.ts.map +1 -1
  99. package/dist/theme/defineTheme.js +44 -48
  100. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  101. package/dist/theme/derivedVarRegistry.js +7 -0
  102. package/dist/theme/expandColorScale.d.ts.map +1 -1
  103. package/dist/theme/expandColorScale.js +1 -0
  104. package/dist/theme/expandMotionScale.d.ts +1 -0
  105. package/dist/theme/expandMotionScale.d.ts.map +1 -1
  106. package/dist/theme/expandMotionScale.js +1 -0
  107. package/dist/theme/expandRadiusScale.d.ts +1 -0
  108. package/dist/theme/expandRadiusScale.d.ts.map +1 -1
  109. package/dist/theme/expandRadiusScale.js +1 -0
  110. package/dist/theme/expandTypeScale.d.ts +1 -0
  111. package/dist/theme/expandTypeScale.d.ts.map +1 -1
  112. package/dist/theme/expandTypeScale.js +1 -0
  113. package/dist/theme/mergeComponents.d.ts +20 -0
  114. package/dist/theme/mergeComponents.d.ts.map +1 -0
  115. package/dist/theme/mergeComponents.js +56 -0
  116. package/dist/theme/onMediaTokens.d.ts +6 -1
  117. package/dist/theme/onMediaTokens.d.ts.map +1 -1
  118. package/dist/theme/onMediaTokens.js +11 -3
  119. package/dist/theme/tokens.stylex.d.ts +3 -1
  120. package/dist/theme/tokens.stylex.d.ts.map +1 -1
  121. package/dist/theme/tokens.stylex.js +3 -1
  122. package/locales/en.json +84 -0
  123. package/locales/pseudo.json +63 -0
  124. package/package.json +3 -3
  125. package/src/Avatar/Avatar.doc.mjs +5 -2
  126. package/src/Avatar/Avatar.test.tsx +51 -0
  127. package/src/Avatar/Avatar.tsx +30 -20
  128. package/src/AvatarGroup/AvatarGroup.doc.mjs +1 -1
  129. package/src/AvatarGroup/AvatarGroup.test.tsx +14 -0
  130. package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -2
  131. package/src/Button/Button.tsx +5 -3
  132. package/src/ButtonGroup/ButtonGroup.test.tsx +7 -4
  133. package/src/Card/Card.doc.mjs +2 -0
  134. package/src/Chat/Chat.doc.mjs +4 -1
  135. package/src/Chat/ChatMessage.doc.mjs +3 -3
  136. package/src/Chat/ChatMessage.tsx +7 -0
  137. package/src/Chat/ChatMessageBubble.doc.mjs +7 -0
  138. package/src/Chat/ChatMessageBubble.test.tsx +95 -6
  139. package/src/Chat/ChatMessageBubble.tsx +25 -0
  140. package/src/Chat/ChatTokenizedText.tsx +1 -1
  141. package/src/CheckboxInput/CheckboxInput.tsx +20 -0
  142. package/src/CodeBlock/CodeBlock.doc.mjs +3 -0
  143. package/src/CommandPalette/CommandPaletteFooter.tsx +6 -3
  144. package/src/ComplexSelector/ComplexSelector.doc.mjs +55 -6
  145. package/src/ComplexSelector/ComplexSelector.test.tsx +197 -6
  146. package/src/ComplexSelector/ComplexSelector.tsx +153 -32
  147. package/src/ComplexSelector/index.ts +3 -1
  148. package/src/DateTimeInput/DateTimeInput.tsx +4 -2
  149. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +72 -1
  150. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +24 -20
  151. package/src/HoverCard/HoverCard.doc.mjs +3 -3
  152. package/src/HoverCard/HoverCard.test.tsx +283 -55
  153. package/src/HoverCard/HoverCard.tsx +5 -13
  154. package/src/HoverCard/useHoverCard.tsx +8 -11
  155. package/src/InputGroup/groupStyles.ts +7 -8
  156. package/src/Item/Item.doc.mjs +4 -0
  157. package/src/Layer/layerHost.test.ts +99 -0
  158. package/src/Layer/layerHost.ts +141 -0
  159. package/src/Layer/useLayer.doc.mjs +14 -4
  160. package/src/Layer/useLayer.test.tsx +332 -7
  161. package/src/Layer/useLayer.tsx +235 -36
  162. package/src/Markdown/parser.test.ts +53 -0
  163. package/src/Markdown/parser.ts +53 -12
  164. package/src/MobileNav/MobileNav.doc.mjs +7 -0
  165. package/src/NavItem/navItemStyles.stylex.ts +31 -5
  166. package/src/RadioList/RadioListItem.tsx +20 -0
  167. package/src/SideNav/SideNav.doc.mjs +13 -4
  168. package/src/SideNav/SideNav.test.tsx +858 -2
  169. package/src/SideNav/SideNav.tsx +37 -16
  170. package/src/SideNav/SideNavCollapseButton.doc.mjs +28 -6
  171. package/src/SideNav/SideNavCollapseButton.tsx +67 -15
  172. package/src/SideNav/SideNavCollapseContext.ts +29 -9
  173. package/src/SideNav/SideNavHeading.tsx +53 -25
  174. package/src/SideNav/SideNavItem.doc.mjs +13 -0
  175. package/src/SideNav/SideNavItem.tsx +97 -84
  176. package/src/SideNav/SideNavSection.tsx +6 -20
  177. package/src/SideNav/index.ts +2 -0
  178. package/src/Slider/Slider.test.tsx +69 -33
  179. package/src/Slider/Slider.tsx +65 -13
  180. package/src/Switch/Switch.tsx +20 -0
  181. package/src/TabList/TabList.doc.mjs +3 -0
  182. package/src/Text/Text.doc.mjs +1 -1
  183. package/src/Thumbnail/Thumbnail.doc.mjs +3 -0
  184. package/src/Thumbnail/Thumbnail.tsx +10 -0
  185. package/src/Timestamp/Timestamp.test.tsx +40 -22
  186. package/src/Toolbar/Toolbar.test.tsx +14 -14
  187. package/src/TopNav/TopNav.test.tsx +86 -1
  188. package/src/TopNav/TopNavHeading.tsx +21 -14
  189. package/src/TopNav/TopNavMegaMenu.test.tsx +43 -0
  190. package/src/TopNav/TopNavMegaMenu.tsx +29 -105
  191. package/src/TopNav/TopNavMegaMenuItem.tsx +1 -1
  192. package/src/TopNav/TopNavMenu.test.tsx +41 -0
  193. package/src/TopNav/TopNavMenu.tsx +14 -4
  194. package/src/TreeList/TreeList.doc.mjs +1 -0
  195. package/src/hooks/containerReveal.stylex.ts +140 -9
  196. package/src/hooks/index.ts +6 -1
  197. package/src/hooks/useContainerReveal.doc.mjs +11 -5
  198. package/src/hooks/useContainerReveal.test.tsx +70 -3
  199. package/src/hooks/useContainerReveal.ts +86 -7
  200. package/src/hooks/useFocusTrap.test.tsx +16 -0
  201. package/src/hooks/useFocusTrap.ts +20 -2
  202. package/src/hooks/useMenuHover.test.tsx +364 -0
  203. package/src/hooks/useMenuHover.ts +243 -47
  204. package/src/theme/defineTheme.test.ts +127 -0
  205. package/src/theme/defineTheme.ts +57 -51
  206. package/src/theme/derivedVarRegistry.test.ts +78 -12
  207. package/src/theme/derivedVarRegistry.ts +4 -0
  208. package/src/theme/expandColorScale.ts +1 -0
  209. package/src/theme/expandMotionScale.ts +1 -0
  210. package/src/theme/expandRadiusScale.ts +1 -0
  211. package/src/theme/expandTypeScale.ts +1 -0
  212. package/src/theme/mergeComponents.ts +59 -0
  213. package/src/theme/onMediaTokens.ts +9 -2
  214. package/src/theme/themingTargets.test.ts +152 -26
  215. package/src/theme/tokens.stylex.ts +3 -1
  216. package/src/theme/tokens.test.ts +12 -0
  217. package/src/theme/useTheme.test.tsx +18 -0
@@ -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']},
@@ -67,6 +67,7 @@ export const docs = {
67
67
  { guidance: true, description: 'Use the group prop on bubbles (first, middle, last) when a single sender sends multiple consecutive messages; it tightens corner radius to visually connect them.' },
68
68
  { guidance: true, description: 'Use ChatSystemMessage with variant="divider" for date separators and default for inline status notices like joins, leaves, or topic changes.' },
69
69
  { guidance: true, description: 'Put name on the first bubble and metadata on the last bubble in a message so they align with the bubble\'s inline padding.' },
70
+ { guidance: true, description: 'Wrap custom in-message content (cards, attachments, citations) in a ghost bubble so it aligns with the bubble text column; add width="100%" when it should span the full message column instead of the default bubble width cap.' },
70
71
  { guidance: true, description: 'Provide an emptyState prop so new users see a clear prompt to start a conversation instead of a blank screen.' },
71
72
  { guidance: true, description: 'Use the ghost bubble variant for AI-style responses that show rich content like code blocks or markdown without a visible boundary.' },
72
73
  { guidance: false, description: 'Don\'t use ChatSystemMessage for sender content; it has no avatar, alignment, or bubble. Use ChatMessage with a sender role instead.' },
@@ -84,7 +85,7 @@ export const docs = {
84
85
  { name: 'Empty state', required: false, description: 'Centered placeholder shown when no messages exist. Use EmptyState for a consistent look.' },
85
86
  { name: 'Avatar', required: false, description: 'A sender avatar rendered beside the message. Typically Avatar with size="md". Hidden for system messages.' },
86
87
  { name: 'Name', required: false, description: 'Sender name above the message body. Place on the bubble when using bubbles, or on the message wrapper for raw content.' },
87
- { name: 'Content', required: true, description: 'The message body: one or more ChatMessageBubble elements, or any free-form ReactNode like images or tool calls.' },
88
+ { name: 'Content', required: true, description: 'The message body: one or more ChatMessageBubble elements, or any free-form ReactNode like images or tool calls. Wrap non-bubble content in a ghost bubble to align it with the bubble text column.' },
88
89
  { name: 'Metadata', required: false, description: 'Timestamp, delivery status, and footer actions below the message. Place on the last bubble or on the message wrapper.' },
89
90
  ],
90
91
  },
@@ -100,6 +101,7 @@ export const docsZh = {
100
101
  { guidance: true, description: '当同一发送者发送多条连续消息时,使用气泡的 group 属性(first、middle、last)来视觉上连接相关消息。' },
101
102
  { guidance: true, description: '使用 ChatSystemMessage 的 variant="divider" 作为日期分隔线,使用 default 作为内联状态通知。' },
102
103
  { guidance: true, description: '将 name 放在第一个气泡上,metadata 放在最后一个气泡上,使其与气泡的内边距对齐。' },
104
+ { guidance: true, description: '将自定义消息内容(卡片、附件、引用)包裹在 ghost 变体的气泡中,使其与气泡文本列对齐;需要占满消息列宽度时设置 width="100%"。' },
103
105
  { guidance: true, description: '提供 emptyState 属性,让新用户看到开始对话的提示,而不是空白屏幕。' },
104
106
  { guidance: true, description: '使用 ghost 气泡变体显示代码块或 Markdown 等富内容,无需可见边界。' },
105
107
  { guidance: false, description: '不要将 ChatSystemMessage 用于发送者内容,它没有头像、对齐或气泡。请使用带有发送者角色的 ChatMessage。' },
@@ -131,6 +133,7 @@ export const docsDense = {
131
133
  { guidance: true, description: 'Group prop on bubbles (first/middle/last) for consecutive same-sender messages; tightens corner radius.' },
132
134
  { guidance: true, description: 'SystemMessage: divider variant for date breaks, default for status notices.' },
133
135
  { guidance: true, description: 'Name on first bubble, metadata on last; aligns with bubble inline padding.' },
136
+ { guidance: true, description: 'Ghost bubble wraps custom content (cards/attachments); width="100%" spans full message column (default cap max(80%,280px)).' },
134
137
  { guidance: true, description: 'Provide emptyState so new users see a prompt, not a blank screen.' },
135
138
  { guidance: true, description: 'Ghost bubble variant for rich AI content like code/markdown without visible boundary.' },
136
139
  { guidance: false, description: 'SystemMessage for sender content: no avatar/alignment/bubble. Use ChatMessage instead.' },
@@ -17,7 +17,7 @@ export const docs = {
17
17
  {
18
18
  name: 'children',
19
19
  type: 'ReactNode',
20
- description: 'Free-form content: bubbles, asset lists, tool calls, images.',
20
+ description: 'Free-form content: bubbles, asset lists, tool calls, images. Custom (non-bubble) children render flush with the message edge; wrap them in a ghost ChatMessageBubble to align them with the bubble text column.',
21
21
  required: true,
22
22
  },
23
23
  {
@@ -67,7 +67,7 @@ export const docsZh = {
67
67
  description: '发送者上下文包装器,根据发送者角色处理头像、名称、元数据和对齐方式。',
68
68
  propDescriptions: {
69
69
  sender: '消息发送者,控制对齐和布局。',
70
- children: '自由内容:气泡、资源列表、工具调用、图片。',
70
+ children: '自由内容:气泡、资源列表、工具调用、图片。非气泡的自定义子内容默认与消息边缘齐平,可用 ghost 变体的 ChatMessageBubble 包裹使其与气泡文本列对齐。',
71
71
  avatar: '消息旁边渲染的头像元素。通常是 Avatar。',
72
72
  name: '消息正文上方渲染的发送者名称。当第一个子元素不是气泡时使用。',
73
73
  metadata: '消息正文下方渲染的元数据。当最后一个子元素不是气泡时使用。',
@@ -81,7 +81,7 @@ export const docsDense = {
81
81
  description: 'sender context wrapper; handles avatar+name+metadata+alignment by sender role',
82
82
  propDescriptions: {
83
83
  sender: 'who sent; controls alignment+layout',
84
- children: 'free-form: bubbles, assets, tool calls, images',
84
+ children: 'free-form: bubbles, assets, tool calls, images; wrap non-bubble content in a ghost bubble to align with bubble text column',
85
85
  avatar: 'avatar element beside msg; typically Avatar',
86
86
  name: 'sender name above body; use when first child is raw (not bubble)',
87
87
  displayName: 'sender name above body; use when first child is raw (not bubble)',
@@ -41,6 +41,13 @@ import {useTranslator} from '../i18n';
41
41
  export interface ChatMessageProps extends BaseProps<HTMLElement> {
42
42
  ref?: React.Ref<HTMLElement>;
43
43
  sender: ChatMessageSender;
44
+ /**
45
+ * Message body — bubbles, tool calls, images, or any free-form content.
46
+ * Custom (non-bubble) children render flush with the message edge; wrap
47
+ * them in a ghost bubble (`<ChatMessageBubble variant="ghost">`) to align
48
+ * them with the bubble's text column, and add `width="100%"` when they
49
+ * should span the full message column.
50
+ */
44
51
  children: ReactNode;
45
52
  avatar?: ReactNode;
46
53
  /**
@@ -45,6 +45,11 @@ export const docs = {
45
45
  type: "'first' | 'middle' | 'last'",
46
46
  description: 'Position within a multi-bubble group. Controls corner radius reduction on the sender side. Leave unset for standalone bubbles (full radius).',
47
47
  },
48
+ {
49
+ name: 'width',
50
+ type: 'SizeValue',
51
+ description: 'Width of the bubble (number = pixels, string = used as-is). When set, replaces the default max(80%, 280px) width cap. Combine with variant="ghost" to let custom content (an artifact card, attachments) span the full message column.',
52
+ },
48
53
  ],
49
54
  };
50
55
 
@@ -59,6 +64,7 @@ export const docsZh = {
59
64
  name: '气泡上方渲染的发送者名称,与气泡文本内边距对齐。用于消息中的第一个气泡。',
60
65
  metadata: '气泡下方渲染的元数据内容,与气泡文本内边距对齐。用于消息中的最后一个气泡。',
61
66
  group: '多气泡组中的位置。控制发送者侧的圆角缩减。',
67
+ width: '气泡宽度(数字 = 像素,字符串 = 按原样使用)。设置后替代默认的 max(80%, 280px) 宽度上限;与 ghost 变体组合可让自定义内容占满消息列宽度。',
62
68
  },
63
69
  };
64
70
 
@@ -74,5 +80,6 @@ export const docsDense = {
74
80
  displayName: 'sender name above bubble, aligned w/ bubble padding',
75
81
  metadata: 'metadata below bubble, aligned w/ bubble padding',
76
82
  group: 'position in multi-bubble group; controls corner radius reduction',
83
+ width: 'bubble width (number=px, string=as-is); replaces default max(80%,280px) cap; ghost+width="100%" spans full message column',
77
84
  },
78
85
  };
@@ -7,6 +7,16 @@ import {ChatMessageBubble} from './ChatMessageBubble';
7
7
  import {ChatMessageList} from './ChatMessageList';
8
8
  import {ChatMessageMetadata} from './ChatMessageMetadata';
9
9
 
10
+ /**
11
+ * Classes shared between two elements. StyleX emits one atomic class per
12
+ * style declaration, so two elements share a class exactly when they share
13
+ * a declaration — e.g. the same density paddingInline.
14
+ */
15
+ function sharedClasses(a: Element, b: Element): string[] {
16
+ const bClasses = new Set(Array.from(b.classList));
17
+ return Array.from(a.classList).filter(c => bClasses.has(c));
18
+ }
19
+
10
20
  describe('ChatMessageBubble', () => {
11
21
  it('renders children', () => {
12
22
  render(
@@ -29,9 +39,7 @@ describe('ChatMessageBubble', () => {
29
39
 
30
40
  it('defaults to assistant when no context', () => {
31
41
  render(
32
- <ChatMessageBubble data-testid="bubble">
33
- Standalone
34
- </ChatMessageBubble>,
42
+ <ChatMessageBubble data-testid="bubble">Standalone</ChatMessageBubble>,
35
43
  );
36
44
  const el = screen.getByTestId('bubble');
37
45
  expect(el.className).toContain('assistant');
@@ -41,9 +49,7 @@ describe('ChatMessageBubble', () => {
41
49
  render(
42
50
  <ChatMessageList density="compact">
43
51
  <ChatMessage sender="assistant">
44
- <ChatMessageBubble data-testid="bubble">
45
- Compact
46
- </ChatMessageBubble>
52
+ <ChatMessageBubble data-testid="bubble">Compact</ChatMessageBubble>
47
53
  </ChatMessage>
48
54
  </ChatMessageList>,
49
55
  );
@@ -59,6 +65,89 @@ describe('ChatMessageBubble', () => {
59
65
  );
60
66
  expect(screen.getByTestId('my-bubble')).toBeTruthy();
61
67
  });
68
+
69
+ it('ghost variant aligns custom content with the bubble text column (#2574)', () => {
70
+ // Repro from the issue: a raw child renders flush with the message
71
+ // edge — it carries none of the inset the bubble's name slot gets.
72
+ const {container} = render(
73
+ <ChatMessage sender="assistant">
74
+ <ChatMessageBubble name="Navi">Hello</ChatMessageBubble>
75
+ <div data-testid="raw">Artifact card</div>
76
+ <ChatMessageBubble variant="ghost" data-testid="ghost">
77
+ Artifact card
78
+ </ChatMessageBubble>
79
+ </ChatMessage>,
80
+ );
81
+ const nameSlot = container.querySelector('[data-chat-name]')!;
82
+ expect(nameSlot).toBeTruthy();
83
+
84
+ // Unwrapped custom content: no shared inset — the misalignment case.
85
+ expect(sharedClasses(screen.getByTestId('raw'), nameSlot)).toEqual([]);
86
+
87
+ // Ghost-wrapped content: shares the bubble slot's paddingInline
88
+ // declaration, so its text column matches the filled bubble exactly.
89
+ expect(
90
+ sharedClasses(screen.getByTestId('ghost'), nameSlot).length,
91
+ ).toBeGreaterThan(0);
92
+ });
93
+
94
+ it('ghost inset tracks message density', () => {
95
+ const renderAtDensity = (density: 'balanced' | 'spacious') => {
96
+ const {container} = render(
97
+ <ChatMessage sender="assistant" density={density}>
98
+ <ChatMessageBubble name="Navi">Hello</ChatMessageBubble>
99
+ <ChatMessageBubble variant="ghost" data-testid={`ghost-${density}`}>
100
+ Card
101
+ </ChatMessageBubble>
102
+ </ChatMessage>,
103
+ );
104
+ return sharedClasses(
105
+ screen.getByTestId(`ghost-${density}`),
106
+ container.querySelector('[data-chat-name]')!,
107
+ );
108
+ };
109
+
110
+ const balancedInset = renderAtDensity('balanced');
111
+ const spaciousInset = renderAtDensity('spacious');
112
+
113
+ // Both densities align with their own bubble's slot padding...
114
+ expect(balancedInset.length).toBeGreaterThan(0);
115
+ expect(spaciousInset.length).toBeGreaterThan(0);
116
+ // ...and spacious uses a wider inset than balanced.
117
+ expect(spaciousInset).not.toEqual(balancedInset);
118
+ });
119
+
120
+ it('width prop replaces the default width cap', () => {
121
+ render(
122
+ <ChatMessage sender="assistant">
123
+ <ChatMessageBubble data-testid="capped">Text</ChatMessageBubble>
124
+ <ChatMessageBubble variant="ghost" width="100%" data-testid="full">
125
+ Card
126
+ </ChatMessageBubble>
127
+ </ChatMessage>,
128
+ );
129
+ const capped = screen.getByTestId('capped');
130
+ const full = screen.getByTestId('full');
131
+
132
+ // Default bubbles keep the cap; a width bubble replaces it with none.
133
+ expect(getComputedStyle(capped).maxWidth).toMatch(/^max\(80%,\s*280px\)$/);
134
+ expect(getComputedStyle(full).maxWidth).toBe('none');
135
+ // The dynamic width value is set on the element (string passes through).
136
+ expect(full.getAttribute('style')).toContain('100%');
137
+ });
138
+
139
+ it('numeric width is treated as pixels', () => {
140
+ render(
141
+ <ChatMessage sender="assistant">
142
+ <ChatMessageBubble width={420} data-testid="fixed">
143
+ Artifact
144
+ </ChatMessageBubble>
145
+ </ChatMessage>,
146
+ );
147
+ expect(screen.getByTestId('fixed').getAttribute('style')).toContain(
148
+ '420px',
149
+ );
150
+ });
62
151
  });
63
152
 
64
153
  describe('ChatMessageMetadata', () => {
@@ -15,6 +15,9 @@
15
15
  * - If you use bubbles on one side (e.g. assistant), use them consistently
16
16
  * for all messages on that side. Use `ghost` variant for content that
17
17
  * needs alignment without a visual boundary.
18
+ * - For custom content (cards, attachments, citations) that should span
19
+ * the full message column instead of the default width cap, combine
20
+ * `ghost` with `width="100%"` (#2574).
18
21
  * - Put `name` on the first bubble in a message, `metadata` on the last.
19
22
  * - For unbubbled messages, use ChatMessage's `name` and `metadata`
20
23
  * props instead.
@@ -36,6 +39,7 @@ import {
36
39
  } from '../theme/tokens.stylex';
37
40
  import {useChatMessageContext} from './ChatContext';
38
41
  import {mergeProps} from '../utils';
42
+ import type {SizeValue} from '../utils/types';
39
43
  import type {BaseProps} from '../BaseProps';
40
44
  import {themeProps} from '../utils/themeProps';
41
45
 
@@ -83,6 +87,15 @@ export interface ChatMessageBubbleProps extends BaseProps<HTMLDivElement> {
83
87
  * Leave unset for standalone bubbles (full radius).
84
88
  */
85
89
  group?: 'first' | 'middle' | 'last';
90
+
91
+ /**
92
+ * Width of the bubble.
93
+ * Numbers are treated as pixels, strings are used as-is (e.g. "100%").
94
+ * When set, replaces the default `max(80%, 280px)` width cap; leave unset
95
+ * to keep the cap. Combine with `variant="ghost"` to let custom content
96
+ * (an artifact card, attachments) span the full message column.
97
+ */
98
+ width?: SizeValue;
86
99
  }
87
100
 
88
101
  // =============================================================================
@@ -189,6 +202,16 @@ const styles = stylex.create({
189
202
  },
190
203
  });
191
204
 
205
+ // Dynamic styles for sizing props
206
+ const dynamicStyles = stylex.create({
207
+ sizing: (width: SizeValue) => ({
208
+ width,
209
+ // An explicit width replaces the default cap — a full-column or
210
+ // fixed-width bubble shouldn't also be clamped by max(80%, 280px).
211
+ maxWidth: 'none',
212
+ }),
213
+ });
214
+
192
215
  // =============================================================================
193
216
  // Component
194
217
  // =============================================================================
@@ -216,6 +239,7 @@ export function ChatMessageBubble({
216
239
  name,
217
240
  metadata,
218
241
  group,
242
+ width,
219
243
  xstyle,
220
244
  className,
221
245
  style: styleProp,
@@ -292,6 +316,7 @@ export function ChatMessageBubble({
292
316
  paddingStyle,
293
317
  variant === 'ghost' && styles.paddingBlockNone,
294
318
  groupStyle,
319
+ width != null && dynamicStyles.sizing(width),
295
320
  xstyle,
296
321
  ),
297
322
  className,
@@ -74,7 +74,7 @@ function isCustomToken(
74
74
  }
75
75
 
76
76
  function escapeRegExp(str: string): string {
77
- return str.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&');
77
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
78
78
  }
79
79
 
80
80
  // =============================================================================
@@ -81,6 +81,26 @@ const styles = stylex.create({
81
81
  opacity: 0,
82
82
  cursor: 'pointer',
83
83
  zIndex: 1,
84
+ minInlineSize: {
85
+ default: null,
86
+ '@media (pointer: coarse)': '24px',
87
+ },
88
+ minBlockSize: {
89
+ default: null,
90
+ '@media (pointer: coarse)': '24px',
91
+ },
92
+ insetBlockStart: {
93
+ default: null,
94
+ '@media (pointer: coarse)': '50%',
95
+ },
96
+ insetInlineStart: {
97
+ default: null,
98
+ '@media (pointer: coarse)': '50%',
99
+ },
100
+ transform: {
101
+ default: null,
102
+ '@media (pointer: coarse)': 'translate(-50%, -50%)',
103
+ },
84
104
  },
85
105
  inputDisabled: {
86
106
  cursor: 'not-allowed',
@@ -146,6 +146,9 @@ export const docs = {
146
146
  {className: 'astryx-codeblock-title', visualProps: ['size', 'language']},
147
147
  {className: 'astryx-codeblock-copy-button'},
148
148
  ],
149
+ vars: [
150
+ {name: '--_codeblock-gutter-width', description: 'Width of the line-number gutter, computed from the digit count of the last line so the code column starts at a stable offset.', default: '2ch', private: true},
151
+ ],
149
152
  },
150
153
  usage: {
151
154
  description: 'CodeBlock renders syntax-highlighted code with line numbers, a copy button, and optional collapsible sections. Use CodeBlock for multi-line snippets like source files, terminal commands, and configuration examples. Use Code for inline references to function names, variables, or CLI flags within body text.',
@@ -24,6 +24,7 @@ import {
24
24
  } from '../theme/tokens.stylex';
25
25
  import {Kbd} from '../Kbd';
26
26
  import {themeProps} from '../utils/themeProps';
27
+ import {useTranslator} from '../i18n';
27
28
 
28
29
  const styles = stylex.create({
29
30
  footer: {
@@ -85,6 +86,8 @@ export function CommandPaletteFooter({
85
86
  style,
86
87
  ...props
87
88
  }: CommandPaletteFooterProps) {
89
+ const t = useTranslator();
90
+
88
91
  return (
89
92
  <div
90
93
  ref={ref}
@@ -100,15 +103,15 @@ export function CommandPaletteFooter({
100
103
  <span {...stylex.props(styles.hint)}>
101
104
  <Kbd keys="up" />
102
105
  <Kbd keys="down" />
103
- Navigate
106
+ {t('@astryx.commandPalette.footer.navigate')}
104
107
  </span>
105
108
  <span {...stylex.props(styles.hint)}>
106
109
  <Kbd keys="enter" />
107
- Select
110
+ {t('@astryx.commandPalette.footer.select')}
108
111
  </span>
109
112
  <span {...stylex.props(styles.hint)}>
110
113
  <Kbd keys="escape" />
111
- Close
114
+ {t('@astryx.commandPalette.footer.close')}
112
115
  </span>
113
116
  </>
114
117
  )}
@@ -1,5 +1,14 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ /**
4
+ * @file ComplexSelector.doc.mjs
5
+ * @input ComplexSelector public API and composition contract
6
+ * @output Exports full and dense component documentation
7
+ * @position Core documentation consumed by CLI and Storybook autodocs
8
+ *
9
+ * SYNC: When modified, update ComplexSelector.tsx, tests, and stories.
10
+ */
11
+
3
12
  /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
4
13
 
5
14
  export const docs = {
@@ -19,7 +28,10 @@ export const docs = {
19
28
  ],
20
29
  theming: {
21
30
  targets: [
22
- {className: 'astryx-complex-selector', visualProps: ['size', 'status']},
31
+ {
32
+ className: 'astryx-complex-selector',
33
+ visualProps: ['variant', 'size', 'status'],
34
+ },
23
35
  {
24
36
  className: 'astryx-complex-selector-indicator-icon',
25
37
  states: ['state'],
@@ -32,7 +44,7 @@ export const docs = {
32
44
  name: 'ComplexSelector',
33
45
  displayName: 'Complex Selector',
34
46
  description:
35
- 'A field and dialog-popover shell for custom selector content.',
47
+ 'An input or toolbar trigger and dialog-popover shell for custom selector content.',
36
48
  props: [
37
49
  {
38
50
  name: 'label',
@@ -93,9 +105,21 @@ export const docs = {
93
105
  {
94
106
  name: 'size',
95
107
  type: "'sm' | 'md' | 'lg'",
96
- description: 'Trigger and field size.',
108
+ description: 'Exact trigger height: sm 28px, md 32px, or lg 36px.',
97
109
  default: "'md'",
98
110
  },
111
+ {
112
+ name: 'variant',
113
+ type: "'input' | 'ghost'",
114
+ description:
115
+ 'Visual trigger style. Input is the bordered form treatment; ghost matches toolbar buttons.',
116
+ default: "'input'",
117
+ },
118
+ {
119
+ name: 'startIcon',
120
+ type: 'ReactNode | IconType',
121
+ description: 'Icon displayed at the start of the trigger.',
122
+ },
99
123
  {
100
124
  name: 'width',
101
125
  type: 'SizeValue',
@@ -107,6 +131,18 @@ export const docs = {
107
131
  description: 'Popup placement.',
108
132
  default: "'below'",
109
133
  },
134
+ {
135
+ name: 'alignment',
136
+ type: "'start' | 'center' | 'end'",
137
+ description: 'Popup alignment along the placement axis.',
138
+ default: "'start'",
139
+ },
140
+ {
141
+ name: 'handleRef',
142
+ type: 'React.Ref<ComplexSelectorHandle>',
143
+ description:
144
+ 'Imperative handle for programmatic control. Exposes open(), close(), toggle(), and isOpen().',
145
+ },
110
146
  {
111
147
  name: 'contentXstyle',
112
148
  type: 'StyleXStyles',
@@ -119,6 +155,16 @@ export const docs = {
119
155
  description:
120
156
  'Use ComplexSelector when a selection needs richer custom content than a Selector option row. It is intentionally one component: ComplexSelector owns the field, trigger, popover, focus restore, and changeAction flow, while the content render prop owns the selector-specific accessible structure.',
121
157
  bestPractices: [
158
+ {
159
+ guidance: true,
160
+ description:
161
+ 'Use variant="ghost" with a startIcon when the selector is triggered from a toolbar. Use alignment="end" when a wide surface should align its end edge to the trigger.',
162
+ },
163
+ {
164
+ guidance: true,
165
+ description:
166
+ 'For staged editors, keep draft state in the composed content and call the provided onChange helper only from Apply. Cancel or dismiss without committing.',
167
+ },
122
168
  {
123
169
  guidance: true,
124
170
  description:
@@ -164,7 +210,7 @@ export const docsDense = {
164
210
  group: 'Selector',
165
211
  category: 'Data Input',
166
212
  description:
167
- 'Field+dialog-popover shell for rich custom selectors. Content gets value/onChange/close/state; content owns semantics. Use focus hooks and evaluate custom content against WCAG 2.2.',
213
+ 'Input/ghost trigger + dialog-popover shell for rich custom selectors. Content gets value/onChange/close/state; content owns semantics. Use focus hooks and evaluate custom content against WCAG 2.2.',
168
214
  usage: {
169
215
  description:
170
216
  'Use when a selection needs richer custom content than a Selector row. One component: it owns field, trigger, popover, focus restore, and changeAction; the render prop owns the selector-specific accessible structure.',
@@ -211,10 +257,13 @@ export const docsDense = {
211
257
  value: 'Controlled value.',
212
258
  onChange: 'Commit value.',
213
259
  changeAction: 'Async action after onChange; drives optimistic value/busy.',
214
- children:
215
- 'Render custom dialog content from (value,onChange,close,state).',
260
+ children: 'Render custom dialog content from (value,onChange,close,state).',
216
261
  triggerLabel: 'Closed trigger label/content.',
262
+ variant: 'input for forms; ghost for toolbar triggers.',
263
+ startIcon: 'Leading trigger icon.',
217
264
  placement: 'Popup placement.',
265
+ alignment: 'Popup alignment.',
266
+ handleRef: 'Imperative open/close/toggle handle.',
218
267
  accessibility:
219
268
  'Custom content must provide its own accessible structure. Use focus hooks and evaluate against WCAG 2.2.',
220
269
  },