@astryxdesign/core 0.1.4-canary.39ffd21 → 0.1.4-canary.3d64ef2

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 (234) hide show
  1. package/dist/Banner/Banner.d.ts.map +1 -1
  2. package/dist/Banner/Banner.js +8 -1
  3. package/dist/Calendar/Calendar.d.ts.map +1 -1
  4. package/dist/Calendar/Calendar.js +31 -11
  5. package/dist/Calendar/styles.d.ts +2 -2
  6. package/dist/Calendar/styles.d.ts.map +1 -1
  7. package/dist/Calendar/styles.js +2 -2
  8. package/dist/Card/Card.d.ts +4 -2
  9. package/dist/Card/Card.d.ts.map +1 -1
  10. package/dist/Card/Card.js +15 -7
  11. package/dist/Carousel/Carousel.d.ts.map +1 -1
  12. package/dist/Carousel/Carousel.js +1 -0
  13. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  14. package/dist/Chat/ChatToolCalls.js +8 -1
  15. package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
  16. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  17. package/dist/CheckboxInput/CheckboxInput.js +3 -1
  18. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  19. package/dist/ClickableCard/ClickableCard.js +26 -3
  20. package/dist/CodeBlock/CodeBlock.d.ts +10 -1
  21. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  22. package/dist/CodeBlock/CodeBlock.js +23 -5
  23. package/dist/Collapsible/Collapsible.d.ts +9 -3
  24. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  25. package/dist/Collapsible/Collapsible.js +101 -13
  26. package/dist/Collapsible/CollapsibleGroup.d.ts +39 -7
  27. package/dist/Collapsible/CollapsibleGroup.d.ts.map +1 -1
  28. package/dist/Collapsible/CollapsibleGroup.js +70 -9
  29. package/dist/Collapsible/CollapsibleGroupContext.d.ts +31 -0
  30. package/dist/Collapsible/CollapsibleGroupContext.d.ts.map +1 -1
  31. package/dist/Collapsible/CollapsibleGroupContext.js +33 -3
  32. package/dist/Collapsible/index.d.ts +1 -0
  33. package/dist/Collapsible/index.d.ts.map +1 -1
  34. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  35. package/dist/ContextMenu/ContextMenu.js +2 -0
  36. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  37. package/dist/DateTimeInput/DateTimeInput.js +2 -0
  38. package/dist/Grid/Grid.d.ts +4 -3
  39. package/dist/Grid/Grid.d.ts.map +1 -1
  40. package/dist/Grid/Grid.js +26 -9
  41. package/dist/Icon/Icon.d.ts.map +1 -1
  42. package/dist/Icon/Icon.js +5 -1
  43. package/dist/InputGroup/InputGroup.js +2 -2
  44. package/dist/Item/Item.js +1 -1
  45. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  46. package/dist/Lightbox/Lightbox.js +26 -2
  47. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  48. package/dist/NumberInput/NumberInput.js +32 -16
  49. package/dist/Pagination/Pagination.d.ts.map +1 -1
  50. package/dist/Pagination/Pagination.js +87 -36
  51. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  52. package/dist/PowerSearch/PowerSearch.js +39 -21
  53. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  54. package/dist/ProgressBar/ProgressBar.js +2 -1
  55. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  56. package/dist/Resizable/ResizeHandle.js +10 -2
  57. package/dist/SideNav/SideNav.d.ts.map +1 -1
  58. package/dist/SideNav/SideNav.js +6 -2
  59. package/dist/TabList/TabMenu.d.ts.map +1 -1
  60. package/dist/TabList/TabMenu.js +34 -7
  61. package/dist/Table/BaseTable.d.ts.map +1 -1
  62. package/dist/Table/BaseTable.js +14 -3
  63. package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
  64. package/dist/Table/plugins/columnResize/useTableColumnResize.js +5 -2
  65. package/dist/Table/plugins/pagination/paginateData.d.ts.map +1 -1
  66. package/dist/Table/plugins/pagination/paginateData.js +5 -1
  67. package/dist/Table/plugins/selection/useTableSelectionState.d.ts.map +1 -1
  68. package/dist/Table/plugins/selection/useTableSelectionState.js +11 -1
  69. package/dist/Table/plugins/sortable/useTableSortableState.d.ts.map +1 -1
  70. package/dist/Table/plugins/sortable/useTableSortableState.js +8 -4
  71. package/dist/Thumbnail/Thumbnail.d.ts +3 -2
  72. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  73. package/dist/Thumbnail/Thumbnail.js +1 -0
  74. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  75. package/dist/TimeInput/TimeInput.js +6 -1
  76. package/dist/Toast/Toast.d.ts.map +1 -1
  77. package/dist/Toast/Toast.js +11 -3
  78. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  79. package/dist/Toast/ToastViewport.js +12 -0
  80. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  81. package/dist/Typeahead/BaseTypeahead.js +5 -2
  82. package/dist/astryx.css +7 -0
  83. package/dist/astryx.umd.js +48 -48
  84. package/dist/astryx.umd.js.map +4 -4
  85. package/dist/hooks/focusableSelector.d.ts +1 -1
  86. package/dist/hooks/focusableSelector.d.ts.map +1 -1
  87. package/dist/hooks/focusableSelector.js +1 -1
  88. package/dist/hooks/useFocusTrap.d.ts +3 -0
  89. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  90. package/dist/hooks/useFocusTrap.js +38 -1
  91. package/dist/theme/defineTheme.d.ts +1 -1
  92. package/dist/theme/defineTheme.d.ts.map +1 -1
  93. package/dist/theme/expandColorScale.d.ts.map +1 -1
  94. package/dist/theme/expandColorScale.js +10 -3
  95. package/dist/theme/hct.d.ts +0 -15
  96. package/dist/theme/hct.d.ts.map +1 -1
  97. package/dist/theme/hct.js +9 -9
  98. package/dist/theme/tokens.d.ts +10 -2
  99. package/dist/theme/tokens.d.ts.map +1 -1
  100. package/dist/theme/tokens.js +220 -4
  101. package/dist/utils/color.d.ts +42 -0
  102. package/dist/utils/color.d.ts.map +1 -0
  103. package/dist/utils/color.js +150 -0
  104. package/dist/utils/index.d.ts +2 -0
  105. package/dist/utils/index.d.ts.map +1 -1
  106. package/dist/utils/index.js +2 -1
  107. package/dist/utils/timeParser.d.ts.map +1 -1
  108. package/dist/utils/timeParser.js +8 -5
  109. package/package.json +1 -1
  110. package/src/Avatar/Avatar.doc.mjs +11 -0
  111. package/src/Banner/Banner.doc.mjs +8 -0
  112. package/src/Banner/Banner.test.tsx +38 -0
  113. package/src/Banner/Banner.tsx +8 -1
  114. package/src/Calendar/Calendar.doc.mjs +1 -1
  115. package/src/Calendar/Calendar.test.tsx +112 -4
  116. package/src/Calendar/Calendar.tsx +26 -5
  117. package/src/Calendar/styles.ts +12 -2
  118. package/src/Card/Card.tsx +16 -6
  119. package/src/Carousel/Carousel.doc.mjs +12 -12
  120. package/src/Carousel/Carousel.test.tsx +13 -0
  121. package/src/Carousel/Carousel.tsx +1 -0
  122. package/src/Chat/Chat.doc.mjs +1 -1
  123. package/src/Chat/ChatToolCalls.test.tsx +55 -0
  124. package/src/Chat/ChatToolCalls.tsx +10 -12
  125. package/src/CheckboxInput/CheckboxInput.doc.mjs +3 -3
  126. package/src/CheckboxInput/CheckboxInput.test.tsx +71 -4
  127. package/src/CheckboxInput/CheckboxInput.tsx +2 -0
  128. package/src/Citation/Citation.doc.mjs +5 -0
  129. package/src/ClickableCard/ClickableCard.tsx +45 -2
  130. package/src/CodeBlock/CodeBlock.doc.mjs +7 -2
  131. package/src/CodeBlock/CodeBlock.test.tsx +87 -3
  132. package/src/CodeBlock/CodeBlock.tsx +43 -3
  133. package/src/Collapsible/Collapsible.doc.mjs +8 -4
  134. package/src/Collapsible/Collapsible.tsx +86 -10
  135. package/src/Collapsible/CollapsibleGroup.doc.mjs +27 -3
  136. package/src/Collapsible/CollapsibleGroup.test.tsx +288 -0
  137. package/src/Collapsible/CollapsibleGroup.tsx +107 -9
  138. package/src/Collapsible/CollapsibleGroupContext.tsx +42 -2
  139. package/src/Collapsible/index.ts +4 -0
  140. package/src/CommandPalette/CommandPaletteEmpty.doc.mjs +5 -0
  141. package/src/CommandPalette/CommandPaletteGroup.doc.mjs +10 -0
  142. package/src/CommandPalette/CommandPaletteInput.doc.mjs +6 -0
  143. package/src/CommandPalette/CommandPaletteItem.doc.mjs +6 -0
  144. package/src/CommandPalette/CommandPaletteList.doc.mjs +9 -0
  145. package/src/ContextMenu/ContextMenu.doc.mjs +15 -0
  146. package/src/ContextMenu/ContextMenu.test.tsx +15 -0
  147. package/src/ContextMenu/ContextMenu.tsx +2 -0
  148. package/src/ContextMenu/ContextMenuItem.doc.mjs +6 -0
  149. package/src/DateTimeInput/DateTimeInput.test.tsx +70 -0
  150. package/src/DateTimeInput/DateTimeInput.tsx +2 -0
  151. package/src/Dialog/DialogHeader.doc.mjs +45 -0
  152. package/src/DropdownMenu/DropdownMenu.doc.mjs +1 -1
  153. package/src/Field/FieldLabel.test.tsx +2 -2
  154. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -3
  155. package/src/Grid/Grid.test.tsx +43 -18
  156. package/src/Grid/Grid.tsx +26 -9
  157. package/src/Icon/Icon.test.tsx +41 -29
  158. package/src/Icon/Icon.tsx +6 -2
  159. package/src/InputGroup/InputGroup.tsx +2 -2
  160. package/src/Item/Item.tsx +1 -1
  161. package/src/Layout/LayoutContent.doc.mjs +12 -0
  162. package/src/Layout/LayoutPanel.doc.mjs +40 -0
  163. package/src/Lightbox/Lightbox.doc.mjs +5 -0
  164. package/src/Lightbox/Lightbox.test.tsx +123 -2
  165. package/src/Lightbox/Lightbox.tsx +29 -1
  166. package/src/NavMenu/NavMenu.doc.mjs +15 -0
  167. package/src/NumberInput/NumberInput.test.tsx +48 -3
  168. package/src/NumberInput/NumberInput.tsx +32 -15
  169. package/src/Overlay/Overlay.doc.mjs +6 -0
  170. package/src/Pagination/Pagination.test.tsx +143 -0
  171. package/src/Pagination/Pagination.tsx +75 -23
  172. package/src/Popover/Popover.test.tsx +65 -0
  173. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  174. package/src/PowerSearch/PowerSearch.test.tsx +109 -1
  175. package/src/PowerSearch/PowerSearch.tsx +37 -16
  176. package/src/ProgressBar/ProgressBar.test.tsx +10 -0
  177. package/src/ProgressBar/ProgressBar.tsx +2 -1
  178. package/src/Resizable/ResizeHandle.test.tsx +180 -0
  179. package/src/Resizable/ResizeHandle.tsx +10 -2
  180. package/src/SegmentedControl/SegmentedControl.doc.mjs +1 -1
  181. package/src/SelectableCard/SelectableCard.doc.mjs +1 -1
  182. package/src/SideNav/SideNav.doc.mjs +7 -1
  183. package/src/SideNav/SideNav.test.tsx +20 -0
  184. package/src/SideNav/SideNav.tsx +6 -2
  185. package/src/TabList/TabList.test.tsx +185 -1
  186. package/src/TabList/TabMenu.tsx +45 -9
  187. package/src/Table/BaseTable.tsx +13 -2
  188. package/src/Table/Table.test.tsx +51 -0
  189. package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +13 -0
  190. package/src/Table/plugins/columnResize/useTableColumnResize.tsx +3 -1
  191. package/src/Table/plugins/pagination/paginateData.ts +5 -1
  192. package/src/Table/plugins/pagination/useTablePagination.test.tsx +13 -0
  193. package/src/Table/plugins/selection/useTableSelectionState.test.tsx +23 -0
  194. package/src/Table/plugins/selection/useTableSelectionState.tsx +11 -6
  195. package/src/Table/plugins/sortable/useTableSortableState.test.tsx +35 -1
  196. package/src/Table/plugins/sortable/useTableSortableState.tsx +8 -4
  197. package/src/TextArea/TextArea.test.tsx +3 -3
  198. package/src/TextInput/TextInput.test.tsx +3 -3
  199. package/src/Thumbnail/Thumbnail.test.tsx +36 -2
  200. package/src/Thumbnail/Thumbnail.tsx +4 -2
  201. package/src/TimeInput/TimeInput.test.tsx +37 -0
  202. package/src/TimeInput/TimeInput.tsx +11 -1
  203. package/src/Timestamp/Timestamp.doc.mjs +1 -1
  204. package/src/Toast/Toast.tsx +11 -3
  205. package/src/Toast/ToastViewport.test.tsx +64 -0
  206. package/src/Toast/ToastViewport.tsx +12 -0
  207. package/src/ToggleButton/ToggleButton.doc.mjs +1 -0
  208. package/src/Tokenizer/Tokenizer.doc.mjs +2 -2
  209. package/src/TopNav/TopNav.doc.mjs +1 -1
  210. package/src/TreeList/TreeList.doc.mjs +2 -2
  211. package/src/Typeahead/BaseTypeahead.doc.mjs +19 -0
  212. package/src/Typeahead/BaseTypeahead.tsx +5 -2
  213. package/src/Typeahead/Typeahead.doc.mjs +1 -1
  214. package/src/Typeahead/Typeahead.test.tsx +45 -1
  215. package/src/__tests__/TestIcon.tsx +26 -0
  216. package/src/docPropReferences.test.ts +178 -0
  217. package/src/hooks/focusableSelector.ts +1 -1
  218. package/src/hooks/useFocusTrap.doc.mjs +4 -2
  219. package/src/hooks/useFocusTrap.test.tsx +149 -1
  220. package/src/hooks/useFocusTrap.ts +50 -1
  221. package/src/theme/SyntaxTheme.doc.mjs +4 -4
  222. package/src/theme/defineTheme.ts +4 -13
  223. package/src/theme/derivedVarRegistry.test.ts +2 -2
  224. package/src/theme/expandColorScale.test.ts +29 -0
  225. package/src/theme/expandColorScale.ts +11 -6
  226. package/src/theme/hct.ts +9 -17
  227. package/src/theme/tokens.test.ts +182 -9
  228. package/src/theme/tokens.ts +239 -4
  229. package/src/theme/useTheme.test.tsx +20 -0
  230. package/src/utils/color.test.ts +133 -0
  231. package/src/utils/color.ts +148 -0
  232. package/src/utils/index.ts +3 -0
  233. package/src/utils/timeParser.test.ts +13 -0
  234. package/src/utils/timeParser.ts +8 -5
@@ -20,15 +20,15 @@ export const docs = {
20
20
  ],
21
21
  anatomy: [
22
22
  {name: 'Scroll container', required: true, description: 'The horizontal overflow area that holds all items.'},
23
- {name: 'Items', required: true, description: 'The children rendered in a row. Each item is animated with a scroll-driven scale effect.'},
23
+ {name: 'Items', required: true, description: 'The children rendered in a row inside the scroll container. With hasSnap, each item snaps to the start edge.'},
24
24
  {name: 'Fade edges', required: false, description: 'Gradient fades on the left and right edges that indicate more content is available. Enabled by default, disable with hasEdgeFade={false}.'},
25
- {name: 'Navigation buttons', required: false, description: 'Prev/next buttons that appear on hover. Enabled by default, disable with hasButtons={false}.'},
25
+ {name: 'Navigation buttons', required: false, description: 'Prev/next buttons that appear when the content can scroll in that direction. Enabled by default, disable with hasButtons={false}.'},
26
26
  ],
27
27
  },
28
28
  props: [
29
29
  {name: 'children', type: 'ReactNode', description: 'Carousel items rendered in a horizontal scroll container.', required: true},
30
30
  {name: 'gap', type: "0 | 0.5 | 1 | 1.5 | 2 | 3 | 4", description: 'Gap between items using the spacing token scale.', default: '1'},
31
- {name: 'hasButtons', type: 'boolean', description: 'Show prev/next navigation buttons on hover (desktop only).', default: 'true'},
31
+ {name: 'hasButtons', type: 'boolean', description: 'Show prev/next navigation buttons when content is scrollable.', default: 'true'},
32
32
  {name: 'hasEdgeFade', type: 'boolean', description: 'Show a gradient edge-fade mask when content overflows, signalling that more items exist off-screen.', default: 'true'},
33
33
  {name: 'hasSnap', type: 'boolean', description: 'Enable scroll-snap so each child snaps to the start edge.', default: 'false'},
34
34
  {name: 'padding', type: '0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10', description: 'Inline padding inside the scroll container, with matching scroll-padding so snap points align to the content edge.'},
@@ -59,20 +59,20 @@ export const docs = {
59
59
  export const docsZh = {
60
60
  usage: {
61
61
  description:
62
- 'Carousel scrolls a row of items horizontally when they overflow the available width. Use it for card grids, image galleries, product lists, or any set of items that should be browsable without taking up the full page.',
62
+ 'Carousel 在项目超出可用宽度时将一行项目水平滚动。适用于卡片网格、图片画廊、产品列表,或任何希望无需占据整页即可浏览的项目集合。',
63
63
  bestPractices: [
64
- {guidance: true, description: 'Enable scroll-snap when each item should land precisely at the start edge, like a gallery or product list.'},
65
- {guidance: true, description: 'Always provide an aria-label that describes what the carousel contains, like "Featured products" or "Team members".'},
66
- {guidance: true, description: 'Use a consistent gap and item width so the carousel looks intentional, not like content overflowing by accident.'},
67
- {guidance: false, description: 'Use a carousel for content every user must see. Not everyone scrolls horizontally, so put critical content above the fold.'},
68
- {guidance: false, description: 'Auto-advance items. Let the user scroll at their own pace.'},
69
- {guidance: false, description: 'Nest carousels. A carousel inside a carousel is confusing and breaks keyboard navigation.'},
64
+ {guidance: true, description: '当每个项目应精确对齐到起始边缘时启用滚动吸附,例如画廊或产品列表。'},
65
+ {guidance: true, description: '始终提供描述轮播内容的 aria-label,例如"精选产品"或"团队成员"。'},
66
+ {guidance: true, description: '使用一致的间距和项目宽度,让轮播看起来是有意为之,而不是内容意外溢出。'},
67
+ {guidance: false, description: '将每位用户都必须看到的内容放入轮播。并非所有人都会水平滚动,关键内容应放在首屏。'},
68
+ {guidance: false, description: '自动轮播项目。让用户按自己的节奏滚动。'},
69
+ {guidance: false, description: '嵌套轮播。轮播中嵌套轮播会造成困惑并破坏键盘导航。'},
70
70
  ],
71
71
  },
72
72
  propDescriptions: {
73
73
  children: '在水平滚动容器中渲染的轮播项目。',
74
74
  gap: '使用间距令牌比例的项目间距。',
75
- hasButtons: '悬停时显示上一个/下一个导航按钮(仅桌面端)。',
75
+ hasButtons: '内容可滚动时显示上一个/下一个导航按钮。',
76
76
  hasEdgeFade: '内容溢出时显示渐变边缘遮罩,提示屏幕外还有更多项目。',
77
77
  hasSnap: '启用滚动吸附,使每个子元素吸附到起始边缘。',
78
78
  padding: '滚动容器内的内联内边距,并设置匹配的 scroll-padding,使吸附点与内容边缘对齐。',
@@ -98,7 +98,7 @@ export const docsDense = {
98
98
  propDescriptions: {
99
99
  children: 'carousel items in horizontal scroll',
100
100
  gap: 'item spacing via spacing token scale',
101
- hasButtons: 'prev/next buttons on hover (desktop)',
101
+ hasButtons: 'prev/next buttons when content is scrollable',
102
102
  hasEdgeFade: 'gradient edge-fade mask on overflow',
103
103
  hasSnap: 'scroll-snap; children snap to start edge',
104
104
  padding: 'inline padding; scroll-padding keeps snap points on content edge',
@@ -38,6 +38,19 @@ describe('Carousel', () => {
38
38
  expect(region).toHaveAttribute('aria-roledescription', 'carousel');
39
39
  });
40
40
 
41
+ it('makes the scroll container keyboard-focusable', () => {
42
+ render(
43
+ <Carousel aria-label="Photos">
44
+ <div>Item</div>
45
+ </Carousel>,
46
+ );
47
+ // The inner scroll container overflows, so it must be reachable by
48
+ // keyboard (axe: scrollable-region-focusable).
49
+ const region = screen.getByRole('region', {name: 'Photos'});
50
+ const scroller = region.firstElementChild;
51
+ expect(scroller).toHaveAttribute('tabindex', '0');
52
+ });
53
+
41
54
  it('applies data-testid', () => {
42
55
  render(
43
56
  <Carousel data-testid="my-carousel">
@@ -340,6 +340,7 @@ export function Carousel({
340
340
  {...htmlProps}>
341
341
  <div
342
342
  ref={composedRef}
343
+ tabIndex={0}
343
344
  {...stylex.props(
344
345
  styles.scroller,
345
346
  gapStyles[gap],
@@ -17,7 +17,7 @@ export const docs = {
17
17
  {className: 'astryx-chat-composer', visualProps: ['density']},
18
18
  {className: 'astryx-chat-composer-input'},
19
19
  {className: 'astryx-chat-composer-drawer', visualProps: ['collapsed']},
20
- {className: 'astryx-chat-message', visualProps: ['sender']},
20
+ {className: 'astryx-chat-message', visualProps: ['sender', 'density']},
21
21
  {
22
22
  className: 'astryx-chat-message-bubble',
23
23
  visualProps: ['sender', 'variant', 'density'],
@@ -97,4 +97,59 @@ describe('ChatToolCalls', () => {
97
97
  );
98
98
  expect(screen.getByText('git status')).toBeInTheDocument();
99
99
  });
100
+
101
+ it('exposes no aria-expanded on a call row without resultDetail', () => {
102
+ const {container} = render(
103
+ <ChatToolCalls calls={[{name: 'bash', status: 'complete'}]} />,
104
+ );
105
+ expect(container.querySelector('[aria-expanded]')).toBeNull();
106
+ });
107
+
108
+ it('wires disclosure semantics on an expandable call row', () => {
109
+ render(
110
+ <ChatToolCalls
111
+ calls={[
112
+ {
113
+ name: 'readFile',
114
+ status: 'complete',
115
+ resultDetail: <div>file contents here</div>,
116
+ },
117
+ ]}
118
+ />,
119
+ );
120
+ const row = screen.getByRole('button');
121
+ expect(row).toHaveAttribute('aria-expanded', 'false');
122
+ // Detail panel is conditionally mounted, so no aria-controls while closed.
123
+ expect(row).not.toHaveAttribute('aria-controls');
124
+ expect(screen.queryByText('file contents here')).not.toBeInTheDocument();
125
+
126
+ fireEvent.click(row);
127
+
128
+ expect(row).toHaveAttribute('aria-expanded', 'true');
129
+ const detailId = row.getAttribute('aria-controls');
130
+ expect(detailId).toBeTruthy();
131
+ const panel = document.getElementById(detailId as string);
132
+ expect(panel).not.toBeNull();
133
+ expect(panel).toHaveTextContent('file contents here');
134
+ });
135
+
136
+ it('points the group header aria-controls at the content region', () => {
137
+ render(
138
+ <ChatToolCalls
139
+ defaultIsExpanded={true}
140
+ calls={[
141
+ {name: 'searchCode', status: 'complete'},
142
+ {name: 'readFile', status: 'complete'},
143
+ ]}
144
+ />,
145
+ );
146
+ const header = screen.getByRole('button');
147
+ expect(header).toHaveAttribute('aria-expanded', 'true');
148
+ const regionId = header.getAttribute('aria-controls');
149
+ expect(regionId).toBeTruthy();
150
+ const region = document.getElementById(regionId as string);
151
+ expect(region).not.toBeNull();
152
+ expect(region).toHaveTextContent('searchCode');
153
+ expect(region).toHaveTextContent('readFile');
154
+ });
100
155
  });
@@ -18,7 +18,7 @@
18
18
  * for builders to wire up.
19
19
  */
20
20
 
21
- import React, {useState, useCallback, type ReactNode} from 'react';
21
+ import React, {useState, useCallback, useId, type ReactNode} from 'react';
22
22
  import type {BaseProps} from '../BaseProps';
23
23
  import * as stylex from '@stylexjs/stylex';
24
24
  import {
@@ -41,11 +41,7 @@ import {themeProps} from '../utils/themeProps';
41
41
  // Types
42
42
  // =============================================================================
43
43
 
44
- export type ChatToolCallStatus =
45
- | 'pending'
46
- | 'running'
47
- | 'complete'
48
- | 'error';
44
+ export type ChatToolCallStatus = 'pending' | 'running' | 'complete' | 'error';
49
45
 
50
46
  export interface ChatToolCallItem {
51
47
  /** Tool/function name. */
@@ -356,6 +352,7 @@ function CallRow({call}: {call: ChatToolCallItem}) {
356
352
  const status = call.status ?? 'complete';
357
353
  const hasDetail = call.resultDetail != null;
358
354
  const [isDetailOpen, setIsDetailOpen] = useState(false);
355
+ const detailId = useId();
359
356
 
360
357
  const toggleDetail = hasDetail
361
358
  ? () => setIsDetailOpen(prev => !prev)
@@ -365,6 +362,8 @@ function CallRow({call}: {call: ChatToolCallItem}) {
365
362
  <div
366
363
  role={hasDetail ? 'button' : undefined}
367
364
  tabIndex={hasDetail ? 0 : undefined}
365
+ aria-expanded={hasDetail ? isDetailOpen : undefined}
366
+ aria-controls={hasDetail && isDetailOpen ? detailId : undefined}
368
367
  onClick={toggleDetail}
369
368
  onKeyDown={
370
369
  hasDetail
@@ -399,11 +398,7 @@ function CallRow({call}: {call: ChatToolCallItem}) {
399
398
  </span>
400
399
  <span {...stylex.props(styles.callName)}>{call.name}</span>
401
400
  {call.node != null && (
402
- <Badge
403
- label={call.node}
404
- variant="neutral"
405
- xstyle={styles.nodePill}
406
- />
401
+ <Badge label={call.node} variant="neutral" xstyle={styles.nodePill} />
407
402
  )}
408
403
  {call.target != null && (
409
404
  <span {...stylex.props(styles.callLabel)}>{call.target}</span>
@@ -448,7 +443,7 @@ function CallRow({call}: {call: ChatToolCallItem}) {
448
443
  <div>
449
444
  {row}
450
445
  {isDetailOpen && (
451
- <div {...stylex.props(styles.callDetailContent)}>
446
+ <div id={detailId} {...stylex.props(styles.callDetailContent)}>
452
447
  {call.resultDetail}
453
448
  </div>
454
449
  )}
@@ -497,6 +492,7 @@ export function ChatToolCalls(props: ChatToolCallsProps) {
497
492
 
498
493
  const autoDefault = defaultIsExpanded ?? false;
499
494
  const [internalExpanded, setInternalExpanded] = useState(autoDefault);
495
+ const contentId = useId();
500
496
  const isControlled = controlledExpanded !== undefined;
501
497
  const isExpanded = isControlled ? controlledExpanded : internalExpanded;
502
498
 
@@ -548,6 +544,7 @@ export function ChatToolCalls(props: ChatToolCallsProps) {
548
544
  role="button"
549
545
  tabIndex={0}
550
546
  aria-expanded={isExpanded}
547
+ aria-controls={contentId}
551
548
  onClick={toggle}
552
549
  onKeyDown={e => {
553
550
  if (e.key === 'Enter' || e.key === ' ') {
@@ -613,6 +610,7 @@ export function ChatToolCalls(props: ChatToolCallsProps) {
613
610
 
614
611
  {/* Expanded: all calls with full metadata */}
615
612
  <div
613
+ id={contentId}
616
614
  {...stylex.props(
617
615
  styles.groupContent,
618
616
  isExpanded && styles.groupContentExpanded,
@@ -125,7 +125,7 @@ export const docs = {
125
125
  theming: {
126
126
  targets: [
127
127
  {className: 'astryx-checkbox-input', visualProps: ['size']},
128
- {className: 'astryx-checkbox'},
128
+ {className: 'astryx-checkbox', visualProps: ['size'], states: ['checked', 'disabled']},
129
129
  ],
130
130
  },
131
131
  usage: {
@@ -178,7 +178,7 @@ export const docsZh = {
178
178
  {name: 'isLoading', type: 'boolean', description: '复选框是否处于加载状态。显示旋转器并阻止交互。', default: 'false'},
179
179
  {name: 'isDisabled', type: 'boolean', description: '复选框是否禁用。', default: 'false'},
180
180
  {name: 'htmlName', type: 'string', description: '底层复选框输入的 HTML name 属性,用于表单提交(勾选时提交 "on")。'},
181
- {name: 'disabledMessage', type: 'string', description: 'Explains why the checkbox is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the checkbox focusable via aria-disabled (toggling stays blocked). Use this instead of wrapping a disabled CheckboxInput in Tooltip — disabled controls swallow the hover events an external Tooltip needs.'},
181
+ {name: 'disabledMessage', type: 'string', description: 'Explains why the checkbox is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the checkbox focusable via aria-disabled (toggling stays blocked). Use this instead of wrapping a disabled CheckboxInput in Tooltip: disabled controls swallow the hover events an external Tooltip needs.'},
182
182
  {name: 'isReadOnly', type: 'boolean', description: '复选框是否为只读。以完整不透明度显示当前状态但阻止交互。与 isDisabled 不同,只读复选框不会变暗。', default: 'false'},
183
183
  {name: 'isOptional', type: 'boolean', description: '字段是否可选。与 isRequired 互斥。', default: 'false'},
184
184
  {name: 'isRequired', type: 'boolean', description: '复选框是否必填。与 isOptional 互斥。', default: 'false'},
@@ -200,7 +200,7 @@ export const docsZh = {
200
200
  'size',
201
201
  ],
202
202
  },
203
- {className: 'astryx-checkbox'},
203
+ {className: 'astryx-checkbox', visualProps: ['size'], states: ['checked', 'disabled']},
204
204
  ],
205
205
  },
206
206
  };
@@ -167,6 +167,59 @@ describe('CheckboxInput', () => {
167
167
  expect(handleChange).not.toHaveBeenCalled();
168
168
  });
169
169
 
170
+ it('forwards data-testid to the input', () => {
171
+ render(
172
+ <CheckboxInput
173
+ label="Accept terms"
174
+ value={false}
175
+ onChange={() => {}}
176
+ data-testid="accept-terms-checkbox"
177
+ />,
178
+ );
179
+ expect(screen.getByTestId('accept-terms-checkbox')).toBe(
180
+ screen.getByRole('checkbox'),
181
+ );
182
+ });
183
+
184
+ it('forwards arbitrary data-* attributes to the input', () => {
185
+ render(
186
+ <CheckboxInput
187
+ label="Accept terms"
188
+ value={false}
189
+ onChange={() => {}}
190
+ data-tracking-id="checkbox-42"
191
+ />,
192
+ );
193
+ expect(screen.getByRole('checkbox')).toHaveAttribute(
194
+ 'data-tracking-id',
195
+ 'checkbox-42',
196
+ );
197
+ });
198
+
199
+ it('does not let rest props override checked, disabled, or type', () => {
200
+ // Not part of CheckboxInputProps; spread (rather than named attributes)
201
+ // to sidestep JSX excess-property checks the same way a real caller's
202
+ // spread rest-props object would arrive untyped at runtime.
203
+ const foreignAttrs: Record<string, unknown> = {
204
+ checked: false,
205
+ disabled: false,
206
+ type: 'text',
207
+ };
208
+ render(
209
+ <CheckboxInput
210
+ {...foreignAttrs}
211
+ label="Accept terms"
212
+ value={true}
213
+ onChange={() => {}}
214
+ isDisabled
215
+ />,
216
+ );
217
+ const checkbox = screen.getByRole('checkbox');
218
+ expect(checkbox).toBeChecked();
219
+ expect(checkbox).toBeDisabled();
220
+ expect(checkbox).toHaveAttribute('type', 'checkbox');
221
+ });
222
+
170
223
  it('forwards ref correctly', () => {
171
224
  const ref = vi.fn();
172
225
  render(
@@ -396,7 +449,12 @@ describe('CheckboxInput', () => {
396
449
  it('submits under htmlName when checked', () => {
397
450
  const {container} = render(
398
451
  <form>
399
- <CheckboxInput label="Terms" htmlName="terms" value={true} onChange={() => {}} />
452
+ <CheckboxInput
453
+ label="Terms"
454
+ htmlName="terms"
455
+ value={true}
456
+ onChange={() => {}}
457
+ />
400
458
  </form>,
401
459
  );
402
460
  const data = new FormData(container.querySelector('form')!);
@@ -416,16 +474,25 @@ describe('CheckboxInput', () => {
416
474
  />
417
475
  </form>,
418
476
  );
419
- expect([...new FormData(container.querySelector('form')!).keys()]).toEqual([]);
477
+ expect([
478
+ ...new FormData(container.querySelector('form')!).keys(),
479
+ ]).toEqual([]);
420
480
  });
421
481
 
422
482
  it('submits nothing when unchecked', () => {
423
483
  const {container} = render(
424
484
  <form>
425
- <CheckboxInput label="Terms" htmlName="terms" value={false} onChange={() => {}} />
485
+ <CheckboxInput
486
+ label="Terms"
487
+ htmlName="terms"
488
+ value={false}
489
+ onChange={() => {}}
490
+ />
426
491
  </form>,
427
492
  );
428
- expect([...new FormData(container.querySelector('form')!).keys()]).toEqual([]);
493
+ expect([
494
+ ...new FormData(container.querySelector('form')!).keys(),
495
+ ]).toEqual([]);
429
496
  });
430
497
  });
431
498
  });
@@ -395,6 +395,7 @@ export function CheckboxInput({
395
395
  className,
396
396
  style,
397
397
  ref,
398
+ ...rest
398
399
  }: CheckboxInputProps) {
399
400
  const id = useId();
400
401
  const descriptionID = useId();
@@ -484,6 +485,7 @@ export function CheckboxInput({
484
485
  )}>
485
486
  <div {...stylex.props(styles.checkboxWrapper, wrapperSizeStyles[size])}>
486
487
  <input
488
+ {...rest}
487
489
  ref={mergeRefs(
488
490
  ref,
489
491
  indeterminateRef,
@@ -88,6 +88,11 @@ export const docs = {
88
88
  default: "'label'",
89
89
  },
90
90
  ],
91
+ theming: {
92
+ targets: [
93
+ {className: 'astryx-citation', visualProps: ['variant']},
94
+ ],
95
+ },
91
96
  };
92
97
 
93
98
  /** @type {import('../docs-types').TranslationDoc} */
@@ -31,7 +31,12 @@
31
31
  import {type ReactNode, type MouseEvent, useRef, type Ref} from 'react';
32
32
  import * as stylex from '@stylexjs/stylex';
33
33
  import type {StyleXStyles} from '@stylexjs/stylex';
34
- import {colorVars, durationVars, easeVars} from '../theme/tokens.stylex';
34
+ import {
35
+ borderVars,
36
+ colorVars,
37
+ durationVars,
38
+ easeVars,
39
+ } from '../theme/tokens.stylex';
35
40
  import type {SizeValue, SpacingStep} from '../utils/types';
36
41
  import {mergeProps, mergeRefs} from '../utils';
37
42
  import {Card} from '../Card/Card';
@@ -66,7 +71,6 @@ const styles = stylex.create({
66
71
  content: '""',
67
72
  position: 'absolute',
68
73
  inset: 0,
69
- borderRadius: 'inherit',
70
74
  pointerEvents: 'none',
71
75
  transitionProperty: 'background-color',
72
76
  transitionDuration: durationVars['--duration-fast'],
@@ -84,6 +88,38 @@ const styles = stylex.create({
84
88
  },
85
89
  },
86
90
  },
91
+ // Borderless variants (everything except `default`): drop the transparent
92
+ // 1px border Card applies. The overlay is inset to the padding box — inside
93
+ // that border — so a transparent border strip stays untinted on hover and
94
+ // reads as a faint ring. With no border, the overlay covers the full box
95
+ // edge-to-edge and the ring disappears.
96
+ borderless: {
97
+ borderWidth: 0,
98
+ },
99
+ // Bordered variant (`default`): draw the 1px border *within* the padding by
100
+ // subtracting the border width from every side. Total inset (border +
101
+ // padding) then equals the borderless variants' padding, so content geometry
102
+ // and outer dimensions stay identical across all variants. The border rests
103
+ // at the subtle token and emphasizes on hover.
104
+ bordered: {
105
+ borderColor: colorVars['--color-border'],
106
+ paddingInlineStart: `calc(var(--container-padding-inline-start) - ${borderVars['--border-width']})`,
107
+ paddingInlineEnd: `calc(var(--container-padding-inline-end) - ${borderVars['--border-width']})`,
108
+ paddingBlockStart: `calc(var(--container-padding-block-start) - ${borderVars['--border-width']})`,
109
+ paddingBlockEnd: `calc(var(--container-padding-block-end) - ${borderVars['--border-width']})`,
110
+ transitionProperty: 'border-color',
111
+ transitionDuration: durationVars['--duration-fast'],
112
+ transitionTimingFunction: easeVars['--ease-standard'],
113
+ },
114
+ // Emphasize the bordered variant's border on hover. Guarded by
115
+ // @media (hover: hover) so touch devices don't get a stuck hover state.
116
+ borderedHoverOnPointer: {
117
+ '@media (hover: hover)': {
118
+ ':hover': {
119
+ borderColor: colorVars['--color-border-emphasized'],
120
+ },
121
+ },
122
+ },
87
123
  disabled: {
88
124
  cursor: 'not-allowed',
89
125
  opacity: 0.5,
@@ -248,6 +284,11 @@ export function ClickableCard({
248
284
 
249
285
  const isLink = href != null;
250
286
 
287
+ // Only the `default` variant has a visible border. Card draws a transparent
288
+ // 1px border on every other variant purely to avoid layout jitter; we drop
289
+ // it here so the hover overlay covers the full box with no untinted ring.
290
+ const hasBorder = variant === 'default';
291
+
251
292
  return (
252
293
  <Card
253
294
  ref={mergeRefs(ref, containerRef)}
@@ -264,8 +305,10 @@ export function ClickableCard({
264
305
  [
265
306
  styles.interactive,
266
307
  styles.focusWithin,
308
+ hasBorder ? styles.bordered : styles.borderless,
267
309
  !isDisabled && styles.overlay,
268
310
  !isDisabled && styles.hoverOnPointer,
311
+ !isDisabled && hasBorder && styles.borderedHoverOnPointer,
269
312
  isDisabled && styles.disabled,
270
313
  xstyleProp,
271
314
  ] as unknown as StyleXStyles
@@ -90,6 +90,11 @@ export const docs = {
90
90
  type: '(code: string, language: string) => Array<{type: string; start: number; end: number}>',
91
91
  description: 'Custom tokenizer override for unsupported languages.',
92
92
  },
93
+ {
94
+ name: 'syntaxTheme',
95
+ type: 'SyntaxThemeDefinition',
96
+ description: 'Per-instance syntax theme override. Shorthand for wrapping the block in <SyntaxTheme theme={...}>. Accepts a preset from @astryxdesign/core/theme/syntax or a theme created with defineSyntaxTheme(). Defaults to the nearest SyntaxTheme ancestor or the theme-level syntax colors.',
97
+ },
93
98
  {
94
99
  name: 'isCollapsible',
95
100
  type: 'boolean',
@@ -135,8 +140,8 @@ export const docs = {
135
140
  },
136
141
  theming: {
137
142
  targets: [
138
- {className: 'astryx-code'},
139
- {className: 'astryx-codeblock', visualProps: ['size', 'language']},
143
+ {className: 'astryx-code', visualProps: ['color']},
144
+ {className: 'astryx-codeblock', visualProps: ['size', 'language', 'container']},
140
145
  ],
141
146
  },
142
147
  usage: {
@@ -3,15 +3,21 @@
3
3
  /**
4
4
  * @file CodeBlock.test.tsx
5
5
  * @input Uses vitest, @testing-library/react, CodeBlock component
6
- * @output Unit tests for CodeBlock (copy button, collapse, scroll region a11y)
6
+ * @output Unit tests for CodeBlock (copy button, collapse, scroll region a11y, syntaxTheme)
7
7
  * @position Testing; validates CodeBlock implementation
8
8
  *
9
9
  * SYNC: When CodeBlock.tsx changes, update tests to match new behavior
10
10
  */
11
11
 
12
- import {describe, it, expect, vi, beforeEach} from 'vitest';
13
- import {render, screen, fireEvent} from '@testing-library/react';
12
+ import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
13
+ import {render, screen, fireEvent, waitFor} from '@testing-library/react';
14
14
  import {CodeBlock} from './CodeBlock';
15
+ import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
16
+ import {dracula} from '../theme/syntax';
17
+
18
+ function politeRegion(): HTMLElement | null {
19
+ return document.querySelector('[data-astryx-live-region="polite"]');
20
+ }
15
21
 
16
22
  // A code sample long enough to exceed the default collapsible threshold (10).
17
23
  const LONG_CODE = Array.from(
@@ -27,6 +33,10 @@ describe('CodeBlock', () => {
27
33
  });
28
34
  });
29
35
 
36
+ afterEach(() => {
37
+ __resetLiveRegionsForTest();
38
+ });
39
+
30
40
  it('renders the code', () => {
31
41
  render(<CodeBlock code="const x = 1;" language="javascript" />);
32
42
  expect(screen.getByText(/const/)).toBeInTheDocument();
@@ -53,6 +63,15 @@ describe('CodeBlock', () => {
53
63
  expect(navigator.clipboard.writeText).toHaveBeenCalledWith('const x = 1;');
54
64
  });
55
65
 
66
+ it('announces "Copied" to a polite live region after copying', async () => {
67
+ render(<CodeBlock code="const x = 1;" language="javascript" />);
68
+ const copyButton = screen.getByRole('button', {name: 'Copy code'});
69
+ fireEvent.click(copyButton);
70
+ await waitFor(() => {
71
+ expect(politeRegion()).toHaveTextContent('Copied');
72
+ });
73
+ });
74
+
56
75
  it('does NOT collapse the block when the copy button is clicked', () => {
57
76
  render(
58
77
  <CodeBlock
@@ -112,4 +131,69 @@ describe('CodeBlock', () => {
112
131
  fireEvent.click(header);
113
132
  expect(header).toHaveAttribute('aria-expanded', 'false');
114
133
  });
134
+
135
+ it('links the collapsible header to its code region via aria-controls', () => {
136
+ render(
137
+ <CodeBlock
138
+ code={LONG_CODE}
139
+ language="javascript"
140
+ title="example"
141
+ isCollapsible
142
+ />,
143
+ );
144
+ const header = screen
145
+ .getAllByRole('button')
146
+ .find(el => el.hasAttribute('aria-expanded'))!;
147
+ const controlsId = header.getAttribute('aria-controls');
148
+ // aria-controls must be present and point at the real code region.
149
+ expect(controlsId).toBeTruthy();
150
+ const region = document.getElementById(controlsId as string);
151
+ expect(region).not.toBeNull();
152
+ // The region contains the scrollable code body (role="group").
153
+ expect(region).toContainElement(screen.getByRole('group'));
154
+ });
155
+
156
+ it('keeps aria-controls resolvable when collapsed (region stays mounted)', () => {
157
+ render(
158
+ <CodeBlock
159
+ code={LONG_CODE}
160
+ language="javascript"
161
+ title="example"
162
+ isCollapsible
163
+ />,
164
+ );
165
+ const header = screen
166
+ .getAllByRole('button')
167
+ .find(el => el.hasAttribute('aria-expanded'))!;
168
+ fireEvent.click(header);
169
+ expect(header).toHaveAttribute('aria-expanded', 'false');
170
+ // The code region uses a CSS grid animation to collapse, so it stays in
171
+ // the DOM — aria-controls stays a valid, resolvable reference (unlike a
172
+ // conditionally-mounted region, which would need a conditional attribute).
173
+ const controlsId = header.getAttribute('aria-controls');
174
+ expect(controlsId).toBeTruthy();
175
+ expect(document.getElementById(controlsId as string)).not.toBeNull();
176
+ });
177
+
178
+ it('applies a per-instance syntax theme via the syntaxTheme prop', () => {
179
+ const {container} = render(
180
+ <CodeBlock
181
+ code="const x = 1;"
182
+ language="javascript"
183
+ syntaxTheme={dracula}
184
+ />,
185
+ );
186
+ const wrapper = container.querySelector('[data-astryx-syntax-theme]');
187
+ expect(wrapper).not.toBeNull();
188
+ expect(wrapper).toHaveAttribute('data-astryx-syntax-theme', 'dracula');
189
+ expect(wrapper!.querySelector('pre')).not.toBeNull();
190
+ });
191
+
192
+ it('renders no syntax theme wrapper when syntaxTheme is not set', () => {
193
+ const {container} = render(
194
+ <CodeBlock code="const x = 1;" language="javascript" />,
195
+ );
196
+ expect(container.querySelector('[data-astryx-syntax-theme]')).toBeNull();
197
+ expect(container.firstElementChild?.tagName).toBe('PRE');
198
+ });
115
199
  });