@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
@@ -35,6 +35,12 @@ export const docs = {
35
35
  description: 'StyleX styles for layout customization.',
36
36
  },
37
37
  ],
38
+ playground: {
39
+ defaults: {
40
+ label: 'Edit',
41
+ description: 'Modify this item',
42
+ },
43
+ },
38
44
  };
39
45
 
40
46
  export const docsZh = {
@@ -214,6 +214,21 @@ describe('DateTimeInput', () => {
214
214
  expect(screen.getByRole('combobox')).not.toHaveAttribute('aria-busy');
215
215
  });
216
216
 
217
+ it('sets aria-busy on the time input when isLoading is true', () => {
218
+ render(<DateTimeInput label="Meeting" isLoading onChange={() => {}} />);
219
+ expect(screen.getByLabelText('Meeting time')).toHaveAttribute(
220
+ 'aria-busy',
221
+ 'true',
222
+ );
223
+ });
224
+
225
+ it('does not set aria-busy on the time input when not loading', () => {
226
+ render(<DateTimeInput label="Meeting" onChange={() => {}} />);
227
+ expect(screen.getByLabelText('Meeting time')).not.toHaveAttribute(
228
+ 'aria-busy',
229
+ );
230
+ });
231
+
217
232
  it('renders status icon for error status', () => {
218
233
  render(
219
234
  <DateTimeInput
@@ -270,6 +285,61 @@ describe('DateTimeInput', () => {
270
285
  expect(found).toBe(true);
271
286
  });
272
287
 
288
+ it('links the description to the time input via aria-describedby', () => {
289
+ render(
290
+ <DateTimeInput
291
+ label="Meeting"
292
+ description="Pick the meeting datetime"
293
+ onChange={() => {}}
294
+ />,
295
+ );
296
+ // The description covers both halves of the field, so the time input must
297
+ // carry it too — a screen-reader user tabbing into the time half should
298
+ // not lose the field's description.
299
+ const timeInput = screen.getByLabelText('Meeting time');
300
+ const describedBy = timeInput.getAttribute('aria-describedby')!;
301
+ const ids = describedBy.split(' ');
302
+ const found = ids.some(id =>
303
+ document
304
+ .getElementById(id)
305
+ ?.textContent?.includes('Pick the meeting datetime'),
306
+ );
307
+ expect(found).toBe(true);
308
+ });
309
+
310
+ it('links the status message to the time input via aria-describedby', () => {
311
+ render(
312
+ <DateTimeInput
313
+ label="Meeting"
314
+ onChange={() => {}}
315
+ status={{type: 'error', message: 'Invalid datetime'}}
316
+ />,
317
+ );
318
+ const timeInput = screen.getByLabelText('Meeting time');
319
+ const describedBy = timeInput.getAttribute('aria-describedby')!;
320
+ const ids = describedBy.split(' ');
321
+ const found = ids.some(id =>
322
+ document.getElementById(id)?.textContent?.includes('Invalid datetime'),
323
+ );
324
+ expect(found).toBe(true);
325
+ });
326
+
327
+ it('links the disabled reason to the time input via aria-describedby', () => {
328
+ HTMLElement.prototype.showPopover = vi.fn();
329
+ HTMLElement.prototype.hidePopover = vi.fn();
330
+ render(
331
+ <DateTimeInput
332
+ label="When"
333
+ onChange={() => {}}
334
+ isDisabled
335
+ disabledMessage="You need the Editor role"
336
+ />,
337
+ );
338
+ const timeInput = screen.getByLabelText('When time');
339
+ const tooltip = screen.getByRole('tooltip', {hidden: true});
340
+ expect(timeInput.getAttribute('aria-describedby')).toContain(tooltip.id);
341
+ });
342
+
273
343
  it('handles Escape keydown on date input without error', () => {
274
344
  render(<DateTimeInput label="Meeting" onChange={() => {}} />);
275
345
  const input = screen.getByRole('combobox');
@@ -987,10 +987,12 @@ export function DateTimeInput({
987
987
  aria-disabled={showsDisabledMessage ? 'true' : undefined}
988
988
  readOnly={showsDisabledMessage || undefined}
989
989
  aria-label={timeLabel ?? `${label} time`}
990
+ aria-describedby={ariaDescribedBy}
990
991
  aria-required={isRequired === true ? 'true' : undefined}
991
992
  aria-invalid={
992
993
  status?.type === 'error' || !isTimeInputValid ? 'true' : undefined
993
994
  }
995
+ aria-busy={isBusy || undefined}
994
996
  {...stylex.props(
995
997
  styles.input,
996
998
  isEffectivelyDisabled && styles.inputDisabled,
@@ -65,6 +65,51 @@ export const docs = {
65
65
  default: 'true',
66
66
  },
67
67
  ],
68
+ playground: {
69
+ defaults: {
70
+ title: 'Delete file?',
71
+ subtitle: 'This action cannot be undone.',
72
+ hasDivider: true,
73
+ },
74
+ },
75
+ examples: [
76
+ {
77
+ label: 'Basic',
78
+ code: `
79
+ import {DialogHeader} from '@astryxdesign/core/Dialog';
80
+
81
+ <DialogHeader title="Delete file?" subtitle="This action cannot be undone." />;
82
+ `,
83
+ },
84
+ {
85
+ label: 'With close button',
86
+ code: `
87
+ import {useState} from 'react';
88
+ import {DialogHeader} from '@astryxdesign/core/Dialog';
89
+
90
+ function Header() {
91
+ const [, setIsOpen] = useState(true);
92
+
93
+ // Passing onOpenChange renders a close button that calls it with false.
94
+ return <DialogHeader title="Settings" onOpenChange={setIsOpen} />;
95
+ }
96
+ `,
97
+ },
98
+ {
99
+ label: 'With start and end content',
100
+ code: `
101
+ import {DialogHeader} from '@astryxdesign/core/Dialog';
102
+ import {Icon} from '@astryxdesign/core/Icon';
103
+ import {Badge} from '@astryxdesign/core/Badge';
104
+
105
+ <DialogHeader
106
+ title="Notifications"
107
+ startContent={<Icon icon="chevronLeft" size="sm" />}
108
+ endContent={<Badge label="3" />}
109
+ />;
110
+ `,
111
+ },
112
+ ],
68
113
  };
69
114
 
70
115
  export const docsZh = {
@@ -11,7 +11,7 @@ export const docs = {
11
11
  theming: {
12
12
  targets: [
13
13
  {className: 'astryx-dropdown-menu'},
14
- {className: 'astryx-dropdown-menu-item'},
14
+ {className: 'astryx-dropdown-menu-item', visualProps: ['size']},
15
15
  ],
16
16
  vars: [
17
17
  {name: '--_dropdown-menu-radius', description: 'Border radius of the menu popup', default: 'var(--radius-element)', private: true},
@@ -11,7 +11,7 @@
11
11
 
12
12
  import {describe, it, expect, vi} from 'vitest';
13
13
  import {render, screen} from '@testing-library/react';
14
- import {StarIcon} from '@heroicons/react/24/outline';
14
+ import {TestIcon} from '../__tests__/TestIcon';
15
15
  import {FieldLabel} from './FieldLabel';
16
16
 
17
17
  describe('FieldLabel', () => {
@@ -49,7 +49,7 @@ describe('FieldLabel', () => {
49
49
  <FieldLabel
50
50
  label="Starred"
51
51
  inputID="starred-input"
52
- labelIcon={StarIcon}
52
+ labelIcon={TestIcon}
53
53
  />,
54
54
  );
55
55
  const svg = document.querySelector('svg');
@@ -4,7 +4,6 @@ exports[`FormLayout > matches snapshot for horizontal direction 1`] = `
4
4
  <div
5
5
  class="astryx-form-layout horizontal FormLayout__styles.base xdt5ytf x18g69wz FormLayout__styles.horizontal xrvj5dj x1mt1orb xu6a5m6"
6
6
  data-direction="horizontal"
7
- data-style-src="@astryxdesign/core:src/FormLayout/FormLayout.tsx:40; @astryxdesign/core:src/FormLayout/FormLayout.tsx:45"
8
7
  data-testid="layout"
9
8
  >
10
9
  <input
@@ -20,7 +19,6 @@ exports[`FormLayout > matches snapshot for horizontal-labels direction 1`] = `
20
19
  <div
21
20
  class="astryx-form-layout horizontal-labels FormLayout__styles.base xdt5ytf FormLayout__styles.horizontalLabels xrvj5dj x1pmbctz xlaq8a2 x7a106z xedohl4 x1rpgqan x1a1jff"
22
21
  data-direction="horizontal-labels"
23
- data-style-src="@astryxdesign/core:src/FormLayout/FormLayout.tsx:40; @astryxdesign/core:src/FormLayout/FormLayout.tsx:50"
24
22
  data-testid="layout"
25
23
  >
26
24
  <label>
@@ -36,7 +34,6 @@ exports[`FormLayout > matches snapshot for vertical direction 1`] = `
36
34
  <div
37
35
  class="astryx-form-layout vertical FormLayout__styles.base x78zum5 xdt5ytf x18g69wz"
38
36
  data-direction="vertical"
39
- data-style-src="@astryxdesign/core:src/FormLayout/FormLayout.tsx:40"
40
37
  data-testid="layout"
41
38
  >
42
39
  <input
@@ -65,7 +65,7 @@ describe('Grid', () => {
65
65
  expect(templateColumns(grid)).toBe('repeat(auto-fill, minmax(250px, 1fr))');
66
66
  });
67
67
 
68
- it('renders with columns object max (capped via track-max)', () => {
68
+ it('renders with columns object max (count capped, tracks still fill)', () => {
69
69
  render(
70
70
  <Grid columns={{minWidth: 250, max: 3}} gap={4} data-testid="grid">
71
71
  <div>Item 1</div>
@@ -74,13 +74,37 @@ describe('Grid', () => {
74
74
  </Grid>,
75
75
  );
76
76
  const grid = screen.getByTestId('grid');
77
- // Track max caps at (100% - 2 * gap) / 3 — no maxWidth on container
77
+ // Cap lives on the track MIN (min(100%, max(minWidth, perColumn))); the
78
+ // track MAX stays 1fr so present columns fill the row (a lone column on
79
+ // mobile stretches to 100% instead of leaving dead space).
78
80
  expect(templateColumns(grid)).toBe(
79
- 'repeat(auto-fill, minmax(250px, calc((100% - 2 * var(--spacing-4)) / 3)))',
81
+ 'repeat(auto-fill, minmax(min(100%, max(250px, calc((100% - 2 * var(--spacing-4)) / 3))), 1fr))',
80
82
  );
81
83
  expect(grid.style.maxWidth).toBe('');
82
84
  });
83
85
 
86
+ it('keeps track max at 1fr with a max cap so a lone column can fill (#3391)', () => {
87
+ // Regression: previously the cap was applied to the track MAX
88
+ // (minmax(minWidth, 100%/max)), so when fewer than `max` columns fit —
89
+ // e.g. a single column on mobile — the lone column was pinned to ~100%/max
90
+ // and left dead space on the right. The cap now lives on the track MIN and
91
+ // the track MAX stays 1fr, so present columns always stretch to fill.
92
+ render(
93
+ <Grid columns={{minWidth: 360, max: 2}} gap={4} data-testid="grid">
94
+ <div>Left</div>
95
+ <div>Right</div>
96
+ </Grid>,
97
+ );
98
+ const grid = screen.getByTestId('grid');
99
+ const template = templateColumns(grid);
100
+ // Track max must be 1fr (fills), not a fraction-of-container cap.
101
+ expect(template).toMatch(/, 1fr\)\)$/);
102
+ expect(template).not.toMatch(/, calc\([^)]*\/ 2\)\)\)$/);
103
+ expect(template).toBe(
104
+ 'repeat(auto-fill, minmax(min(100%, max(360px, calc((100% - 1 * var(--spacing-4)) / 2))), 1fr))',
105
+ );
106
+ });
107
+
84
108
  it('renders with columns object max using columnGap', () => {
85
109
  render(
86
110
  <Grid columns={{minWidth: 200, max: 4}} columnGap={6} data-testid="grid">
@@ -89,9 +113,9 @@ describe('Grid', () => {
89
113
  </Grid>,
90
114
  );
91
115
  const grid = screen.getByTestId('grid');
92
- // columnGap takes precedence for track-max calculation
116
+ // columnGap takes precedence in the perColumn floor calculation
93
117
  expect(templateColumns(grid)).toBe(
94
- 'repeat(auto-fill, minmax(200px, calc((100% - 3 * var(--spacing-6)) / 4)))',
118
+ 'repeat(auto-fill, minmax(min(100%, max(200px, calc((100% - 3 * var(--spacing-6)) / 4))), 1fr))',
95
119
  );
96
120
  expect(grid.style.maxWidth).toBe('');
97
121
  });
@@ -165,7 +189,7 @@ describe('Grid', () => {
165
189
  expect(templateColumns(grid)).toBe('1fr');
166
190
  });
167
191
 
168
- it('uses auto-fill without track-max cap when no max specified', () => {
192
+ it('uses auto-fill with a plain 1fr track when no max specified', () => {
169
193
  render(
170
194
  <Grid columns={{minWidth: 200}} data-testid="grid">
171
195
  <div>Item</div>
@@ -220,7 +244,7 @@ describe('Grid', () => {
220
244
 
221
245
  // --- P2: columns object + columnGap interaction (hardening #719) ---
222
246
 
223
- it('uses columnGap var in track-max when both columnGap and gap are set', () => {
247
+ it('uses columnGap var in the count-cap floor when both columnGap and gap are set', () => {
224
248
  render(
225
249
  <Grid
226
250
  columns={{minWidth: 200, max: 3}}
@@ -231,14 +255,14 @@ describe('Grid', () => {
231
255
  </Grid>,
232
256
  );
233
257
  const grid = screen.getByTestId('grid');
234
- // columnGap takes precedence over gap in track-max
258
+ // columnGap takes precedence over gap in the perColumn floor
235
259
  expect(templateColumns(grid)).toBe(
236
- 'repeat(auto-fill, minmax(200px, calc((100% - 2 * var(--spacing-6)) / 3)))',
260
+ 'repeat(auto-fill, minmax(min(100%, max(200px, calc((100% - 2 * var(--spacing-6)) / 3))), 1fr))',
237
261
  );
238
262
  expect(grid.style.maxWidth).toBe('');
239
263
  });
240
264
 
241
- it('uses gap var in track-max when columnGap is not set', () => {
265
+ it('uses gap var in the count-cap floor when columnGap is not set', () => {
242
266
  render(
243
267
  <Grid columns={{minWidth: 150, max: 2}} gap={3} data-testid="grid">
244
268
  <div>Item</div>
@@ -246,12 +270,12 @@ describe('Grid', () => {
246
270
  );
247
271
  const grid = screen.getByTestId('grid');
248
272
  expect(templateColumns(grid)).toBe(
249
- 'repeat(auto-fill, minmax(150px, calc((100% - 1 * var(--spacing-3)) / 2)))',
273
+ 'repeat(auto-fill, minmax(min(100%, max(150px, calc((100% - 1 * var(--spacing-3)) / 2))), 1fr))',
250
274
  );
251
275
  expect(grid.style.maxWidth).toBe('');
252
276
  });
253
277
 
254
- it('uses simple fraction in track-max when no gap is set', () => {
278
+ it('uses simple fraction in the count-cap floor when no gap is set', () => {
255
279
  render(
256
280
  <Grid columns={{minWidth: 100, max: 3}} data-testid="grid">
257
281
  <div>Item</div>
@@ -259,7 +283,7 @@ describe('Grid', () => {
259
283
  );
260
284
  const grid = screen.getByTestId('grid');
261
285
  expect(templateColumns(grid)).toBe(
262
- 'repeat(auto-fill, minmax(100px, calc(100% / 3)))',
286
+ 'repeat(auto-fill, minmax(min(100%, max(100px, calc(100% / 3))), 1fr))',
263
287
  );
264
288
  expect(grid.style.maxWidth).toBe('');
265
289
  });
@@ -332,7 +356,7 @@ describe('Grid', () => {
332
356
  expect(templateColumns(grid)).toBe('repeat(auto-fill, minmax(280px, 1fr))');
333
357
  });
334
358
 
335
- it('renders with columns={{minWidth, max}} capping via track-max', () => {
359
+ it('renders with columns={{minWidth, max}} capping the count while filling', () => {
336
360
  render(
337
361
  <Grid columns={{minWidth: 280, max: 3}} gap={4} data-testid="grid">
338
362
  <div>Item 1</div>
@@ -340,14 +364,15 @@ describe('Grid', () => {
340
364
  </Grid>,
341
365
  );
342
366
  const grid = screen.getByTestId('grid');
343
- // Track-max limits columns — grid stays full width
367
+ // Count is capped via the track MIN; track MAX stays 1fr so present
368
+ // columns fill the row (grid stays full width).
344
369
  expect(templateColumns(grid)).toBe(
345
- 'repeat(auto-fill, minmax(280px, calc((100% - 2 * var(--spacing-4)) / 3)))',
370
+ 'repeat(auto-fill, minmax(min(100%, max(280px, calc((100% - 2 * var(--spacing-4)) / 3))), 1fr))',
346
371
  );
347
372
  expect(grid.style.maxWidth).toBe('');
348
373
  });
349
374
 
350
- it('renders with columns={{minWidth, max, repeat: "fit"}} using auto-fit + track-max', () => {
375
+ it('renders with columns={{minWidth, max, repeat: "fit"}} using auto-fit + count cap', () => {
351
376
  render(
352
377
  <Grid
353
378
  columns={{minWidth: 280, max: 3, repeat: 'fit'}}
@@ -359,7 +384,7 @@ describe('Grid', () => {
359
384
  );
360
385
  const grid = screen.getByTestId('grid');
361
386
  expect(templateColumns(grid)).toBe(
362
- 'repeat(auto-fit, minmax(280px, calc((100% - 2 * var(--spacing-4)) / 3)))',
387
+ 'repeat(auto-fit, minmax(min(100%, max(280px, calc((100% - 2 * var(--spacing-4)) / 3))), 1fr))',
363
388
  );
364
389
  expect(grid.style.maxWidth).toBe('');
365
390
  });
package/src/Grid/Grid.tsx CHANGED
@@ -38,9 +38,10 @@ export type GridAlignment = 'start' | 'center' | 'end' | 'stretch';
38
38
  * - `minWidth` — minimum width (px) for each column track
39
39
  * - `repeat` — `'fill'` (default) preserves empty tracks for consistent widths;
40
40
  * `'fit'` collapses empty tracks so items stretch to fill
41
- * - `max` — caps the maximum number of columns by limiting track max size.
42
- * The grid stretches to 100% of its parent; `max` only limits how many
43
- * columns appear.
41
+ * - `max` — caps the maximum number of columns. The grid stretches to 100%
42
+ * of its parent and the columns that are present always fill the row, so a
43
+ * layout collapsing to a single column on mobile stretches to full width
44
+ * (no dead space on the right).
44
45
  */
45
46
  export type GridColumns =
46
47
  | number
@@ -321,9 +322,20 @@ const spacingVarNames: Record<SpacingStep, string> = {
321
322
  };
322
323
 
323
324
  /**
324
- * Build a grid-template-columns value that caps columns at `max` by
325
- * limiting each track's max size to `(100% - (max-1) * gap) / max`.
326
- * The grid stays 100% width; the track-max prevents more than `max` columns.
325
+ * Build a grid-template-columns value that caps the column *count* at `max`
326
+ * while still letting the columns that are actually present stretch to fill
327
+ * the row.
328
+ *
329
+ * The cap lives on the track's **min** size, not its max: each track is at
330
+ * least `perColumn = (100% - (max-1) * gap) / max`, so more than `max` columns
331
+ * can never fit. The track **max** stays `1fr`, so whenever fewer than `max`
332
+ * columns fit (most importantly a lone column on mobile) they still grow to
333
+ * fill the whole row — no dead space on the right.
334
+ *
335
+ * The track min is `max(minWidth, perColumn)` so an explicit `minWidth` is
336
+ * still honored, wrapped in `min(100%, …)` so a single column on a viewport
337
+ * narrower than `minWidth`/`perColumn` shrinks to the container instead of
338
+ * overflowing it.
327
339
  */
328
340
  function buildCappedTemplate(
329
341
  minWidth: number,
@@ -339,12 +351,17 @@ function buildCappedTemplate(
339
351
  ? spacingVarNames[gap]
340
352
  : null;
341
353
 
342
- // trackMax = (100% - (maxCols - 1) * gap) / maxCols
343
- const trackMax = gapVar
354
+ // perColumn = (100% - (maxCols - 1) * gap) / maxCols — the width a track
355
+ // would have if exactly `maxCols` columns were present. Used as a floor so
356
+ // the grid never fits more than `maxCols` columns.
357
+ const perColumn = gapVar
344
358
  ? `calc((100% - ${maxCols - 1} * var(${gapVar})) / ${maxCols})`
345
359
  : `calc(100% / ${maxCols})`;
346
360
 
347
- return `repeat(${repeatMode}, minmax(${minWidth}px, ${trackMax}))`;
361
+ // Track min caps the count; track max stays 1fr so present columns fill.
362
+ const trackMin = `min(100%, max(${minWidth}px, ${perColumn}))`;
363
+
364
+ return `repeat(${repeatMode}, minmax(${trackMin}, 1fr))`;
348
365
  }
349
366
 
350
367
  /**
@@ -11,48 +11,48 @@
11
11
 
12
12
  import {describe, it, expect, vi} from 'vitest';
13
13
  import {render, screen} from '@testing-library/react';
14
- import {HomeIcon} from '@heroicons/react/24/outline';
14
+ import {TestIcon} from '../__tests__/TestIcon';
15
15
  import {Icon} from './Icon';
16
16
 
17
17
  describe('Icon', () => {
18
18
  it('renders the icon component', () => {
19
- render(<Icon icon={HomeIcon} data-testid="icon" />);
19
+ render(<Icon icon={TestIcon} data-testid="icon" />);
20
20
  expect(screen.getByTestId('icon')).toBeInTheDocument();
21
21
  });
22
22
 
23
23
  it('renders as an SVG element', () => {
24
- render(<Icon icon={HomeIcon} data-testid="icon" />);
24
+ render(<Icon icon={TestIcon} data-testid="icon" />);
25
25
  const icon = screen.getByTestId('icon');
26
26
  expect(icon.tagName.toLowerCase()).toBe('svg');
27
27
  });
28
28
 
29
29
  it('applies aria-hidden by default', () => {
30
- render(<Icon icon={HomeIcon} data-testid="icon" />);
30
+ render(<Icon icon={TestIcon} data-testid="icon" />);
31
31
  expect(screen.getByTestId('icon')).toHaveAttribute('aria-hidden', 'true');
32
32
  });
33
33
 
34
34
  it('renders with different color variants', () => {
35
35
  const {rerender} = render(
36
- <Icon icon={HomeIcon} color="primary" data-testid="icon" />,
36
+ <Icon icon={TestIcon} color="primary" data-testid="icon" />,
37
37
  );
38
38
  expect(screen.getByTestId('icon')).toBeInTheDocument();
39
39
 
40
- rerender(<Icon icon={HomeIcon} color="secondary" data-testid="icon" />);
40
+ rerender(<Icon icon={TestIcon} color="secondary" data-testid="icon" />);
41
41
  expect(screen.getByTestId('icon')).toBeInTheDocument();
42
42
 
43
- rerender(<Icon icon={HomeIcon} color="accent" data-testid="icon" />);
43
+ rerender(<Icon icon={TestIcon} color="accent" data-testid="icon" />);
44
44
  expect(screen.getByTestId('icon')).toBeInTheDocument();
45
45
 
46
- rerender(<Icon icon={HomeIcon} color="success" data-testid="icon" />);
46
+ rerender(<Icon icon={TestIcon} color="success" data-testid="icon" />);
47
47
  expect(screen.getByTestId('icon')).toBeInTheDocument();
48
48
 
49
- rerender(<Icon icon={HomeIcon} color="error" data-testid="icon" />);
49
+ rerender(<Icon icon={TestIcon} color="error" data-testid="icon" />);
50
50
  expect(screen.getByTestId('icon')).toBeInTheDocument();
51
51
 
52
- rerender(<Icon icon={HomeIcon} color="warning" data-testid="icon" />);
52
+ rerender(<Icon icon={TestIcon} color="warning" data-testid="icon" />);
53
53
  expect(screen.getByTestId('icon')).toBeInTheDocument();
54
54
 
55
- rerender(<Icon icon={HomeIcon} color="inherit" data-testid="icon" />);
55
+ rerender(<Icon icon={TestIcon} color="inherit" data-testid="icon" />);
56
56
  expect(screen.getByTestId('icon')).toBeInTheDocument();
57
57
  });
58
58
 
@@ -70,50 +70,41 @@ describe('Icon', () => {
70
70
  'purple',
71
71
  ] as const;
72
72
  const {rerender} = render(
73
- <Icon
74
- icon={HomeIcon}
75
- color={nonSemanticColors[0]}
76
- data-testid="icon"
77
- />,
73
+ <Icon icon={TestIcon} color={nonSemanticColors[0]} data-testid="icon" />,
78
74
  );
79
75
  expect(screen.getByTestId('icon')).toBeInTheDocument();
80
76
 
81
77
  for (const c of nonSemanticColors.slice(1)) {
82
- rerender(<Icon icon={HomeIcon} color={c} data-testid="icon" />);
78
+ rerender(<Icon icon={TestIcon} color={c} data-testid="icon" />);
83
79
  expect(screen.getByTestId('icon')).toBeInTheDocument();
84
80
  }
85
81
  });
86
82
 
87
83
  it('renders with different size variants', () => {
88
84
  const {rerender} = render(
89
- <Icon icon={HomeIcon} size="xsm" data-testid="icon" />,
85
+ <Icon icon={TestIcon} size="xsm" data-testid="icon" />,
90
86
  );
91
87
  expect(screen.getByTestId('icon')).toBeInTheDocument();
92
88
 
93
- rerender(<Icon icon={HomeIcon} size="sm" data-testid="icon" />);
89
+ rerender(<Icon icon={TestIcon} size="sm" data-testid="icon" />);
94
90
  expect(screen.getByTestId('icon')).toBeInTheDocument();
95
91
 
96
- rerender(<Icon icon={HomeIcon} size="md" data-testid="icon" />);
92
+ rerender(<Icon icon={TestIcon} size="md" data-testid="icon" />);
97
93
  expect(screen.getByTestId('icon')).toBeInTheDocument();
98
94
 
99
- rerender(<Icon icon={HomeIcon} size="lg" data-testid="icon" />);
95
+ rerender(<Icon icon={TestIcon} size="lg" data-testid="icon" />);
100
96
  expect(screen.getByTestId('icon')).toBeInTheDocument();
101
97
  });
102
98
 
103
99
  it('forwards ref correctly', () => {
104
100
  const ref = vi.fn();
105
- render(<Icon icon={HomeIcon} ref={ref} />);
101
+ render(<Icon icon={TestIcon} ref={ref} />);
106
102
  expect(ref).toHaveBeenCalledWith(expect.any(SVGSVGElement));
107
103
  });
108
104
 
109
105
  it('passes additional SVG props', () => {
110
106
  render(
111
- <Icon
112
- icon={HomeIcon}
113
- data-testid="icon"
114
- role="img"
115
- aria-label="Home"
116
- />,
107
+ <Icon icon={TestIcon} data-testid="icon" role="img" aria-label="Home" />,
117
108
  );
118
109
  const icon = screen.getByTestId('icon');
119
110
  expect(icon).toHaveAttribute('role', 'img');
@@ -121,8 +112,29 @@ describe('Icon', () => {
121
112
  });
122
113
 
123
114
  it('uses default color and size when not specified', () => {
124
- render(<Icon icon={HomeIcon} data-testid="icon" />);
115
+ render(<Icon icon={TestIcon} data-testid="icon" />);
125
116
  // The component should render without errors with defaults
126
117
  expect(screen.getByTestId('icon')).toBeInTheDocument();
127
118
  });
119
+
120
+ it('applies aria-hidden by default in string (registry) mode', () => {
121
+ render(<Icon icon="check" data-testid="icon" />);
122
+ expect(screen.getByTestId('icon')).toHaveAttribute('aria-hidden', 'true');
123
+ });
124
+
125
+ it('lets a string-mode icon be made meaningful by overriding aria-hidden', () => {
126
+ render(
127
+ <Icon
128
+ icon="check"
129
+ data-testid="icon"
130
+ role="img"
131
+ aria-label="Done"
132
+ aria-hidden={false}
133
+ />,
134
+ );
135
+ const icon = screen.getByTestId('icon');
136
+ expect(icon).toHaveAttribute('role', 'img');
137
+ expect(icon).toHaveAttribute('aria-label', 'Done');
138
+ expect(icon).toHaveAttribute('aria-hidden', 'false');
139
+ });
128
140
  });
package/src/Icon/Icon.tsx CHANGED
@@ -281,12 +281,16 @@ function IconFromRegistry({
281
281
 
282
282
  return (
283
283
  <span
284
+ // Decorative by default, but placed BEFORE the prop spread so consumers
285
+ // can override it (e.g. `aria-hidden={false}` + `role="img"` +
286
+ // `aria-label`) to expose a meaningful standalone icon. This matches
287
+ // component-mode Icon, which already sets aria-hidden before its spread.
288
+ aria-hidden="true"
284
289
  {...(spanProps as React.HTMLAttributes<HTMLSpanElement>)}
285
290
  {...mergeProps(
286
291
  themeProps('icon', {size, color}),
287
292
  stylex.props(styles.span, colorStyles[color], spanSizeStyles[size]),
288
- )}
289
- aria-hidden="true">
293
+ )}>
290
294
  {resolvedIcon}
291
295
  </span>
292
296
  );
@@ -201,11 +201,11 @@ export function InputGroup({
201
201
  labelTooltip={labelTooltip}>
202
202
  <div
203
203
  ref={ref}
204
+ data-testid={testId}
205
+ {...rest}
204
206
  role="group"
205
207
  aria-labelledby={labelID}
206
208
  aria-describedby={describedByIDs}
207
- data-testid={testId}
208
- {...rest}
209
209
  {...mergeProps(
210
210
  themeProps('input-group', {
211
211
  size,
package/src/Item/Item.tsx CHANGED
@@ -470,9 +470,9 @@ export function Item({
470
470
  return (
471
471
  <Component
472
472
  ref={ref as React.Ref<never>}
473
+ {...restProps}
473
474
  aria-selected={isSelected || undefined}
474
475
  aria-disabled={isDisabled || undefined}
475
- {...restProps}
476
476
  {...mergeProps(
477
477
  themeProps('item', {density, align}),
478
478
  stylex.props(
@@ -14,6 +14,12 @@ export const docs = {
14
14
  type: 'ReactNode',
15
15
  description: 'Content.',
16
16
  },
17
+ {
18
+ name: 'padding',
19
+ type: '0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10',
20
+ description:
21
+ 'Internal padding using the spacing scale. Overrides the default padding from the layout container.',
22
+ },
17
23
  {
18
24
  name: 'isScrollable',
19
25
  type: 'boolean',
@@ -44,6 +50,11 @@ export const docsZh = {
44
50
  type: 'ReactNode',
45
51
  description: '内容。',
46
52
  },
53
+ {
54
+ name: 'padding',
55
+ type: '0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10',
56
+ description: '使用间距比例的内边距。覆盖布局容器的默认内边距。',
57
+ },
47
58
  {
48
59
  name: 'isScrollable',
49
60
  type: 'boolean',
@@ -70,6 +81,7 @@ export const docsDense = {
70
81
  description: 'Scrollable main content area.',
71
82
  propDescriptions: {
72
83
  children: 'Content.',
84
+ padding: 'Internal padding on spacing scale. Overrides layout container default.',
73
85
  isScrollable: 'Enable scrollable overflow.',
74
86
  label: 'Accessible label for landmark element.',
75
87
  role: 'ARIA landmark role.',