@astryxdesign/core 0.7.2-canary.fe2c655 → 0.7.2

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 (297) hide show
  1. package/CHANGELOG.md +156 -2
  2. package/README.md +4 -4
  3. package/dist/AppShell/AppShell.d.ts.map +1 -1
  4. package/dist/AppShell/AppShell.js +40 -1
  5. package/dist/AspectRatio/AspectRatio.d.ts +6 -8
  6. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  7. package/dist/AspectRatio/AspectRatio.js +44 -3
  8. package/dist/Avatar/Avatar.d.ts.map +1 -1
  9. package/dist/Avatar/Avatar.js +8 -1
  10. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  11. package/dist/AvatarGroup/AvatarGroupOverflow.js +4 -1
  12. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  13. package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
  14. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  15. package/dist/Chat/ChatLayoutScrollButton.js +1 -0
  16. package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
  17. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  18. package/dist/CheckboxInput/CheckboxInput.js +3 -1
  19. package/dist/CheckboxList/CheckboxList.d.ts +1 -1
  20. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  21. package/dist/CheckboxList/CheckboxList.js +6 -2
  22. package/dist/CheckboxList/CheckboxListItem.d.ts +9 -1
  23. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  24. package/dist/CheckboxList/CheckboxListItem.js +4 -2
  25. package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
  26. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  27. package/dist/CommandPalette/CommandPaletteInput.js +37 -5
  28. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  29. package/dist/ContextMenu/ContextMenu.js +5 -2
  30. package/dist/Dialog/Dialog.d.ts.map +1 -1
  31. package/dist/Dialog/Dialog.js +20 -8
  32. package/dist/DropdownMenu/DropdownMenu.d.ts +1 -1
  33. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  34. package/dist/DropdownMenu/DropdownMenu.js +84 -113
  35. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +1 -1
  36. package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -0
  37. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  38. package/dist/DropdownMenu/DropdownMenuContext.js +21 -0
  39. package/dist/DropdownMenu/DropdownMenuDivider.d.ts +5 -3
  40. package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -1
  41. package/dist/DropdownMenu/DropdownMenuDivider.js +7 -3
  42. package/dist/DropdownMenu/DropdownMenuGroup.d.ts +6 -4
  43. package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
  44. package/dist/DropdownMenu/DropdownMenuGroup.js +4 -3
  45. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
  46. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  47. package/dist/DropdownMenu/DropdownMenuSubMenu.js +15 -4
  48. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +24 -0
  49. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -0
  50. package/dist/DropdownMenu/lazyMenuBottomSheet.js +46 -0
  51. package/dist/DropdownMenu/menuRegionStops.d.ts +38 -0
  52. package/dist/DropdownMenu/menuRegionStops.d.ts.map +1 -0
  53. package/dist/DropdownMenu/menuRegionStops.js +131 -0
  54. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  55. package/dist/EmptyState/EmptyState.js +4 -10
  56. package/dist/Field/Field.d.ts +1 -1
  57. package/dist/Field/Field.d.ts.map +1 -1
  58. package/dist/Field/Field.js +8 -4
  59. package/dist/FormLayout/FormLayout.d.ts +5 -4
  60. package/dist/FormLayout/FormLayout.d.ts.map +1 -1
  61. package/dist/FormLayout/FormLayout.js +1 -1
  62. package/dist/FormLayout/FormLayoutContext.d.ts +3 -3
  63. package/dist/FormLayout/FormLayoutContext.js +3 -3
  64. package/dist/Grid/Grid.d.ts +8 -7
  65. package/dist/Grid/Grid.d.ts.map +1 -1
  66. package/dist/Grid/Grid.js +5 -4
  67. package/dist/Grid/GridSpan.d.ts +1 -1
  68. package/dist/Grid/GridSpan.d.ts.map +1 -1
  69. package/dist/Grid/GridSpan.js +1 -0
  70. package/dist/Heading/Heading.d.ts.map +1 -1
  71. package/dist/Heading/Heading.js +4 -2
  72. package/dist/Item/Item.d.ts +6 -2
  73. package/dist/Item/Item.d.ts.map +1 -1
  74. package/dist/Item/Item.js +19 -4
  75. package/dist/List/List.d.ts +46 -19
  76. package/dist/List/List.d.ts.map +1 -1
  77. package/dist/List/List.js +28 -10
  78. package/dist/List/ListContext.d.ts +8 -3
  79. package/dist/List/ListContext.d.ts.map +1 -1
  80. package/dist/List/ListContext.js +7 -2
  81. package/dist/List/ListItem.d.ts +13 -5
  82. package/dist/List/ListItem.d.ts.map +1 -1
  83. package/dist/List/ListItem.js +21 -8
  84. package/dist/List/index.d.ts +1 -1
  85. package/dist/List/index.d.ts.map +1 -1
  86. package/dist/Markdown/Markdown.d.ts.map +1 -1
  87. package/dist/Markdown/Markdown.js +8 -1
  88. package/dist/Markdown/parser.d.ts.map +1 -1
  89. package/dist/Markdown/parser.js +8 -2
  90. package/dist/Markdown/plugins/headingLinks.d.ts.map +1 -1
  91. package/dist/Markdown/plugins/headingLinks.js +9 -1
  92. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  93. package/dist/MetadataList/MetadataList.js +5 -1
  94. package/dist/MetadataList/MetadataListContext.d.ts +1 -1
  95. package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
  96. package/dist/MobileNav/MobileNav.d.ts +9 -0
  97. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  98. package/dist/MobileNav/MobileNav.js +30 -3
  99. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  100. package/dist/PowerSearch/PowerSearch.js +5 -0
  101. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  102. package/dist/PowerSearch/PowerSearchValueEditor.js +45 -11
  103. package/dist/RadioList/RadioList.d.ts +1 -1
  104. package/dist/RadioList/RadioList.d.ts.map +1 -1
  105. package/dist/RadioList/RadioList.js +6 -2
  106. package/dist/RadioList/RadioListItem.d.ts +9 -1
  107. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  108. package/dist/RadioList/RadioListItem.js +2 -0
  109. package/dist/SideNav/SideNavItem.d.ts +8 -1
  110. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  111. package/dist/SideNav/SideNavItem.js +76 -10
  112. package/dist/Text/Text.d.ts.map +1 -1
  113. package/dist/Text/Text.js +4 -2
  114. package/dist/ToggleButton/ToggleButton.d.ts +3 -2
  115. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  116. package/dist/ToggleButton/ToggleButton.js +12 -1
  117. package/dist/Token/Token.js +1 -1
  118. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  119. package/dist/TopNav/TopNavMegaMenu.js +5 -3
  120. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  121. package/dist/TopNav/TopNavMenu.js +5 -3
  122. package/dist/TreeList/TreeList.d.ts.map +1 -1
  123. package/dist/TreeList/TreeList.js +1 -0
  124. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  125. package/dist/TreeList/TreeListBranches.js +2 -2
  126. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  127. package/dist/TreeList/TreeListItem.js +57 -9
  128. package/dist/astryx.css +27 -4
  129. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  130. package/dist/hooks/useMenuHover.js +18 -13
  131. package/dist/reset.a11y.chromium.spec.js +50 -1
  132. package/dist/utils/plainDate.d.ts.map +1 -1
  133. package/dist/utils/plainDate.js +15 -9
  134. package/docs.mjs +4 -4
  135. package/package.json +4 -4
  136. package/scripts/agent-doc-state.mjs +66 -6
  137. package/scripts/postinstall.mjs +44 -3
  138. package/src/AppShell/AppShell.tsx +45 -1
  139. package/src/AspectRatio/AspectRatio.test.tsx +44 -43
  140. package/src/AspectRatio/AspectRatio.tsx +38 -18
  141. package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
  142. package/src/Avatar/Avatar.doc.mjs +5 -3
  143. package/src/Avatar/Avatar.tsx +11 -2
  144. package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
  145. package/src/AvatarGroup/AvatarGroupOverflow.test.tsx +24 -1
  146. package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -0
  147. package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
  148. package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
  149. package/src/Card/Card.doc.mjs +1 -1
  150. package/src/Chat/ChatLayoutScrollButton.test.tsx +13 -0
  151. package/src/Chat/ChatLayoutScrollButton.tsx +4 -2
  152. package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +64 -19
  153. package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +3 -1
  154. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  155. package/src/CheckboxInput/CheckboxInput.tsx +3 -1
  156. package/src/CheckboxList/CheckboxList.fr10.test.tsx +29 -2
  157. package/src/CheckboxList/CheckboxList.test.tsx +22 -0
  158. package/src/CheckboxList/CheckboxList.tsx +4 -1
  159. package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
  160. package/src/CheckboxList/CheckboxListItem.tsx +10 -0
  161. package/src/Code/Code.doc.mjs +1 -0
  162. package/src/CommandPalette/CommandPalette.doc.mjs +2 -0
  163. package/src/CommandPalette/CommandPaletteInput.test.tsx +53 -4
  164. package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
  165. package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
  166. package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +292 -0
  167. package/src/ContextMenu/ContextMenu.doc.mjs +2 -3
  168. package/src/ContextMenu/ContextMenu.test.tsx +12 -2
  169. package/src/ContextMenu/ContextMenu.tsx +4 -1
  170. package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +438 -0
  171. package/src/Dialog/Dialog.test.tsx +46 -0
  172. package/src/Dialog/Dialog.tsx +20 -7
  173. package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
  174. package/src/Drawer/Drawer.doc.mjs +1 -0
  175. package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +57 -0
  176. package/src/DropdownMenu/DropdownMenu.test.tsx +79 -6
  177. package/src/DropdownMenu/DropdownMenu.tsx +88 -144
  178. package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +1 -1
  179. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +1 -1
  180. package/src/DropdownMenu/DropdownMenuContext.tsx +24 -0
  181. package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
  182. package/src/DropdownMenu/DropdownMenuDivider.spec.md +192 -0
  183. package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
  184. package/src/DropdownMenu/DropdownMenuGroup.doc.mjs +169 -10
  185. package/src/DropdownMenu/DropdownMenuGroup.tsx +6 -4
  186. package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +126 -0
  187. package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +1 -1
  188. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +1 -1
  189. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
  190. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +17 -3
  191. package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
  192. package/src/DropdownMenu/__tests__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +334 -0
  193. package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +43 -0
  194. package/src/DropdownMenu/lazyMenuBottomSheet.ts +71 -0
  195. package/src/DropdownMenu/menuRegionStops.ts +167 -0
  196. package/src/EmptyState/EmptyState.doc.mjs +3 -2
  197. package/src/EmptyState/EmptyState.spec.md +189 -0
  198. package/src/EmptyState/EmptyState.test.tsx +74 -3
  199. package/src/EmptyState/EmptyState.tsx +4 -8
  200. package/src/Field/Field.doc.mjs +14 -8
  201. package/src/Field/Field.spec.md +217 -0
  202. package/src/Field/Field.test.tsx +60 -2
  203. package/src/Field/Field.tsx +17 -8
  204. package/src/Field/FieldLabel.doc.mjs +133 -26
  205. package/src/Field/FieldLabel.spec.md +172 -0
  206. package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +296 -0
  207. package/src/FormLayout/FormLayout.doc.mjs +6 -3
  208. package/src/FormLayout/FormLayout.spec.md +192 -0
  209. package/src/FormLayout/FormLayout.test.tsx +15 -0
  210. package/src/FormLayout/FormLayout.tsx +7 -6
  211. package/src/FormLayout/FormLayoutContext.ts +3 -3
  212. package/src/Grid/Grid.doc.mjs +7 -1
  213. package/src/Grid/Grid.spec.md +93 -113
  214. package/src/Grid/Grid.test.tsx +22 -0
  215. package/src/Grid/Grid.tsx +10 -9
  216. package/src/Grid/GridSpan.doc.mjs +3 -3
  217. package/src/Grid/GridSpan.tsx +4 -1
  218. package/src/Heading/Heading.test.tsx +57 -0
  219. package/src/Heading/Heading.tsx +6 -2
  220. package/src/Icon/Icon.doc.mjs +36 -0
  221. package/src/Indicator/Indicator.doc.mjs +3 -3
  222. package/src/Item/Item.doc.mjs +12 -8
  223. package/src/Item/Item.test.tsx +61 -0
  224. package/src/Item/Item.tsx +40 -4
  225. package/src/Layout/LayoutHeader.doc.mjs +5 -0
  226. package/src/List/List.doc.mjs +14 -1
  227. package/src/List/List.spec.md +129 -77
  228. package/src/List/List.test.tsx +163 -94
  229. package/src/List/List.tsx +74 -18
  230. package/src/List/ListContext.tsx +9 -3
  231. package/src/List/ListItem.doc.mjs +16 -0
  232. package/src/List/ListItem.tsx +56 -27
  233. package/src/List/index.ts +1 -1
  234. package/src/Markdown/Markdown.tsx +14 -1
  235. package/src/Markdown/parser.ts +8 -2
  236. package/src/Markdown/plugins/headingLinks.ts +9 -1
  237. package/src/MetadataList/MetadataList.doc.mjs +3 -3
  238. package/src/MetadataList/MetadataList.test.tsx +38 -0
  239. package/src/MetadataList/MetadataList.tsx +8 -1
  240. package/src/MetadataList/MetadataListContext.tsx +1 -1
  241. package/src/MobileNav/MobileNav.tsx +30 -3
  242. package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +173 -15
  243. package/src/MoreMenu/MoreMenu.test.tsx +5 -1
  244. package/src/PowerSearch/PowerSearch.test.tsx +62 -0
  245. package/src/PowerSearch/PowerSearch.tsx +9 -0
  246. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +71 -0
  247. package/src/PowerSearch/PowerSearchValueEditor.tsx +45 -0
  248. package/src/ProgressBar/ProgressBar.doc.mjs +1 -0
  249. package/src/RadioList/RadioList.fr10.test.tsx +45 -2
  250. package/src/RadioList/RadioList.test.tsx +22 -0
  251. package/src/RadioList/RadioList.tsx +4 -1
  252. package/src/RadioList/RadioListItem.doc.mjs +16 -0
  253. package/src/RadioList/RadioListItem.tsx +10 -1
  254. package/src/Resizable/useResizable.doc.mjs +2 -0
  255. package/src/ScrollableArea/ScrollableArea.doc.mjs +1 -1
  256. package/src/SideNav/SideNav.spec.md +227 -0
  257. package/src/SideNav/SideNav.test.tsx +220 -0
  258. package/src/SideNav/SideNavItem.doc.mjs +2 -2
  259. package/src/SideNav/SideNavItem.tsx +99 -17
  260. package/src/Slider/Slider.spec.md +39 -18
  261. package/src/Spinner/Spinner.doc.mjs +1 -1
  262. package/src/TabList/TabList.spec.md +57 -44
  263. package/src/Table/Table.spec.md +6 -1
  264. package/src/Table/plugins/activation/useTableActivation.spec.md +254 -0
  265. package/src/Text/Text.test.tsx +57 -0
  266. package/src/Text/Text.tsx +6 -2
  267. package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
  268. package/src/Timestamp/Timestamp.doc.mjs +1 -1
  269. package/src/ToggleButton/ToggleButton.test.tsx +95 -0
  270. package/src/ToggleButton/ToggleButton.tsx +12 -1
  271. package/src/ToggleButton/ToggleButtonGroup.doc.mjs +4 -0
  272. package/src/Token/Token.tsx +1 -1
  273. package/src/Token/__tests__/TokenRemoveTarget.a11y.chromium.spec.ts +86 -0
  274. package/src/Toolbar/Toolbar.test.tsx +0 -8
  275. package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +163 -0
  276. package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +63 -0
  277. package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +142 -0
  278. package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +126 -0
  279. package/src/TopNav/TopNavMegaMenu.test.tsx +47 -0
  280. package/src/TopNav/TopNavMegaMenu.tsx +9 -3
  281. package/src/TopNav/TopNavMenu.test.tsx +46 -1
  282. package/src/TopNav/TopNavMenu.tsx +13 -3
  283. package/src/TreeList/TreeList.doc.mjs +1 -0
  284. package/src/TreeList/TreeList.test.tsx +327 -6
  285. package/src/TreeList/TreeList.tsx +5 -1
  286. package/src/TreeList/TreeListBranches.tsx +6 -0
  287. package/src/TreeList/TreeListItem.tsx +88 -13
  288. package/src/docPropLiterals.test.ts +36 -10
  289. package/src/hooks/useMenuHover.ts +18 -13
  290. package/src/postinstall.test.mjs +79 -0
  291. package/src/reset.a11y.chromium.spec.ts +52 -1
  292. package/src/reset.css +9 -29
  293. package/src/reset.test.ts +11 -0
  294. package/src/theme/derivedVarRegistry.test.ts +7 -0
  295. package/src/unicodePropertyEscapes.source-build.test.mjs +69 -0
  296. package/src/utils/plainDate.test.ts +37 -1
  297. package/src/utils/plainDate.ts +15 -5
@@ -3,14 +3,14 @@
3
3
  /**
4
4
  * @file List.test.tsx
5
5
  * @input Uses vitest, @testing-library/react, List, ListItem
6
- * @output Unit tests for List and ListItem, including bounded inline edge compensation
6
+ * @output Unit tests for List and ListItem, including the row edge treatment
7
7
  * @position Testing; validates List.tsx and ListItem.tsx implementation
8
8
  *
9
9
  * SYNC: When modified, update this header
10
10
  */
11
11
 
12
12
  import {describe, it, expect, vi} from 'vitest';
13
- import {render, screen, waitFor} from '@testing-library/react';
13
+ import {cleanup, render, screen, waitFor} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {rulesDeclaredFor} from '../__tests__/pressState';
16
16
  import {List} from './List';
@@ -18,6 +18,25 @@ import {ListItem} from './ListItem';
18
18
  import {ListMarkerScope} from './ListContext';
19
19
 
20
20
  describe('List', () => {
21
+ it('forwards ListItem align to its Item row unchanged', () => {
22
+ render(
23
+ <List>
24
+ <ListItem label="Default" data-testid="default" />
25
+ <ListItem label="Start" align="start" data-testid="start" />
26
+ <ListItem label="First" align="first-line" data-testid="first" />
27
+ </List>,
28
+ );
29
+ expect(screen.getByTestId('default')).toHaveAttribute(
30
+ 'data-align',
31
+ 'center',
32
+ );
33
+ expect(screen.getByTestId('start')).toHaveAttribute('data-align', 'start');
34
+ expect(screen.getByTestId('first')).toHaveAttribute(
35
+ 'data-align',
36
+ 'first-line',
37
+ );
38
+ });
39
+
21
40
  // ===========================================================================
22
41
  // Basic rendering
23
42
  // ===========================================================================
@@ -258,121 +277,171 @@ describe('List', () => {
258
277
  });
259
278
 
260
279
  // ===========================================================================
261
- // Inline edge compensation
280
+ // Edge treatment
262
281
  // ===========================================================================
263
282
 
264
- it('does not apply inline edge compensation styles by default', () => {
283
+ /** Every injected rule that names one of the element's classes. */
284
+ function declarationsFor(el: Element): string {
285
+ return rulesDeclaredFor(el).join('\n').replace(/\s+/g, '');
286
+ }
287
+
288
+ /** The declarations of the row inset variable that reach the row. */
289
+ function insetDeclarations(item: Element): string[] {
290
+ return rulesDeclaredFor(item).filter(rule =>
291
+ rule.includes('--_item-inset-inline:'),
292
+ );
293
+ }
294
+
295
+ const ZERO_INSET = /--_item-inset-inline:\s*0(px)?\s*;/;
296
+ const LIST_REACH = [
297
+ 'margin-inline-start:calc(-1*var(--container-padding-inline-start,0px))',
298
+ 'margin-inline-end:calc(-1*var(--container-padding-inline-end,0px))',
299
+ 'width:calc(100%+var(--container-padding-inline-start,0px)+var(--container-padding-inline-end,0px))',
300
+ ];
301
+ const OUTSET_ROW = [
302
+ 'margin-inline-start:max(var(--container-padding-inline-start,0px)-var(--_item-inset-inline),0px)',
303
+ 'margin-inline-end:max(var(--container-padding-inline-end,0px)-var(--_item-inset-inline),0px)',
304
+ ];
305
+ const BLEED_ROW = [
306
+ 'padding-inline-start:max(var(--container-padding-inline-start,0px),var(--_item-inset-inline))',
307
+ 'padding-inline-end:max(var(--container-padding-inline-end,0px),var(--_item-inset-inline))',
308
+ ];
309
+
310
+ function renderEdge(props: Partial<React.ComponentProps<typeof List>>) {
265
311
  const {container} = render(
266
- <List>
267
- <ListItem label="Item" />
312
+ <List header={<span>Items</span>} {...props}>
313
+ <ListItem label="Static" />
314
+ <ListItem label="Button" onClick={() => {}} />
315
+ <ListItem label="Link" href="/docs" />
268
316
  </List>,
269
317
  );
270
- const item = container.querySelector('li')!;
271
- expect(item.className).not.toContain('inlineEdgeCompensation');
272
- });
318
+ const ul = container.querySelector('ul')!;
319
+ return {
320
+ ul,
321
+ header: ul.parentElement!,
322
+ items: [...ul.querySelectorAll('li')],
323
+ };
324
+ }
273
325
 
274
- it('applies the clamped cancelling margin to each item when edgeCompensation is inline', () => {
275
- // The margin lives on the row element itself — the only place that can
276
- // read --_item-inset-inline, which Item sets on the same element (custom
277
- // properties cascade downward, so the <ul> cannot read it).
278
- const {container} = render(
279
- <List edgeCompensation="inline">
280
- <ListItem label="Item 1" />
281
- <ListItem label="Item 2" />
282
- </List>,
283
- );
284
- const items = container.querySelectorAll('li');
285
- expect(items).toHaveLength(2);
326
+ it('keeps the row padding and leaves the list and rows in place by default', () => {
327
+ const {ul, items} = renderEdge({});
286
328
  for (const item of items) {
287
- expect(item.className).toContain('inlineEdgeCompensation');
329
+ expect(item.className).not.toMatch(/edge(Flush|Outset|Bleed)/);
330
+ expect(insetDeclarations(item)).toHaveLength(1);
331
+ expect(insetDeclarations(item)[0]).not.toMatch(ZERO_INSET);
288
332
  }
289
- expect(container.querySelector('ul')!.className).not.toContain(
290
- 'inlineEdgeCompensation',
291
- );
333
+ expect(ul.className).not.toContain('reachesContainerPadding');
334
+ expect(edgeContext(ul)).toBe('padded');
292
335
  });
293
336
 
294
- it('uses the same var-derived cancel for every density', () => {
295
- // The cancelling margin reads --_item-inset-inline instead of hardcoding
296
- // per-density values, so all densities (and theme paddingInline
297
- // overrides on `item`) share one style.
337
+ /** The edge each row resolved, read from the row's own edge class. */
338
+ function edgeContext(ul: Element): string {
339
+ const edges = [...ul.querySelectorAll('li')].map(li => {
340
+ const match = /edge(Flush|Outset|Bleed)/.exec(li.className);
341
+ return match ? match[1].toLowerCase() : 'padded';
342
+ });
343
+ expect(new Set(edges).size).toBe(1);
344
+ return edges[0];
345
+ }
346
+
347
+ it('applies one treatment to every row, static or interactive, and never to the header', () => {
348
+ for (const edge of ['flush', 'outset', 'bleed'] as const) {
349
+ const {ul, header, items} = renderEdge({edge});
350
+ expect(items).toHaveLength(3);
351
+ expect(edgeContext(ul)).toBe(edge);
352
+ expect(header.className).not.toMatch(/edge|reachesContainerPadding/);
353
+ cleanup();
354
+ }
355
+ });
356
+
357
+ it('flush: zeroes the shared inset at every density and leaves the list in place', () => {
298
358
  for (const density of ['compact', 'balanced', 'spacious'] as const) {
299
- const {container, unmount} = render(
300
- <List edgeCompensation="inline" density={density}>
301
- <ListItem label="Item" />
302
- </List>,
303
- );
304
- expect(container.querySelector('li')!.className).toContain(
305
- 'inlineEdgeCompensation',
306
- );
307
- unmount();
359
+ const {ul, items} = renderEdge({edge: 'flush', density});
360
+ expect(ul.className).not.toContain('reachesContainerPadding');
361
+ for (const item of items) {
362
+ expect(item).toHaveAttribute('data-density', density);
363
+ const insets = insetDeclarations(item);
364
+ expect(insets).toHaveLength(1);
365
+ expect(insets[0]).toMatch(ZERO_INSET);
366
+ }
367
+ cleanup();
308
368
  }
309
369
  });
310
370
 
311
- it('does not pull the header when edgeCompensation is inline', () => {
312
- // The negative margin lives on the row elements, so the header keeps its
313
- // position and the row text aligns up to it.
314
- const {container} = render(
315
- <List edgeCompensation="inline" header={<span>Items</span>}>
316
- <ListItem label="Item" />
317
- </List>,
318
- );
319
- expect(container.querySelector('li')!.className).toContain(
320
- 'inlineEdgeCompensation',
321
- );
322
- const ul = container.querySelector('ul')!;
323
- expect(ul.className).not.toContain('inlineEdgeCompensation');
324
- expect(ul.parentElement?.className).not.toContain('inlineEdgeCompensation');
371
+ it('outset: the list reaches the container padding and each row steps back in by what its inset does not fill, per edge', () => {
372
+ // Each edge reads its own container padding var: one var on both edges
373
+ // over-reaches the other edge under asymmetric padding (16px start /
374
+ // 4px end), so a selected row would paint past the outer border.
375
+ const {ul, items} = renderEdge({edge: 'outset'});
376
+ const listCss = declarationsFor(ul);
377
+ for (const declaration of LIST_REACH) {
378
+ expect(listCss).toContain(declaration);
379
+ }
380
+ for (const item of items) {
381
+ const css = declarationsFor(item);
382
+ for (const declaration of OUTSET_ROW) {
383
+ expect(css).toContain(declaration);
384
+ }
385
+ expect(css).not.toContain(
386
+ 'margin-inline-end:max(var(--container-padding-inline-start',
387
+ );
388
+ expect(insetDeclarations(item)[0]).not.toMatch(ZERO_INSET);
389
+ }
325
390
  });
326
391
 
327
- it('clamps each inline edge against its own container padding var', () => {
328
- // Regression for asymmetric container padding (16px start / 4px end):
329
- // clamping BOTH margins against --container-padding-inline-start
330
- // over-cancels the end edge by the difference, and the selected row
331
- // paints past the outer border (RTL mirrors the spill). Each edge must
332
- // clamp against its matching var; logical properties keep RTL correct.
333
- // jsdom does no real layout, so assert on the injected StyleX CSS
334
- // (runtimeInjection is on in the test config).
335
- const {container} = render(
336
- <List edgeCompensation="inline">
337
- <ListItem label="Item" />
338
- </List>,
339
- );
340
- expect(container.querySelector('li')!.className).toContain(
341
- 'inlineEdgeCompensation',
342
- );
343
- let out = '';
344
- for (const sheet of Array.from(document.styleSheets)) {
345
- try {
346
- for (const rule of Array.from(sheet.cssRules)) {
347
- out += rule.cssText + '\n';
348
- }
349
- } catch {
350
- // ignore cross-origin sheets
392
+ it('bleed: the list reaches the container padding and rows fill it with text on the padding line, per edge', () => {
393
+ const {ul, items} = renderEdge({edge: 'bleed'});
394
+ const listCss = declarationsFor(ul);
395
+ for (const declaration of LIST_REACH) {
396
+ expect(listCss).toContain(declaration);
397
+ }
398
+ for (const item of items) {
399
+ const css = declarationsFor(item);
400
+ for (const declaration of BLEED_ROW) {
401
+ expect(css).toContain(declaration);
351
402
  }
403
+ expect(css).toContain('border-radius:0');
404
+ expect(css).not.toMatch(/margin-inline-(start|end):max/);
352
405
  }
353
- out += Array.from(document.querySelectorAll('style'))
354
- .map(s => s.textContent || '')
355
- .join('\n');
356
- const css = out.replace(/\s+/g, '');
357
- expect(css).toContain(
358
- 'margin-inline-start:calc(-1*min(var(--_item-inset-inline),var(--container-padding-inline-start,0px)))',
359
- );
360
- expect(css).toContain(
361
- 'margin-inline-end:calc(-1*min(var(--_item-inset-inline),var(--container-padding-inline-end,0px)))',
362
- );
363
- // The end edge must not clamp against the start var (the over-cancel).
364
- expect(css).not.toContain(
365
- 'margin-inline-end:calc(-1*min(var(--_item-inset-inline),var(--container-padding-inline-start,0px)))',
366
- );
367
406
  });
368
407
 
369
- it('does not apply the cancelling margin to a ListItem outside a List', () => {
408
+ it('keeps the inline clip on the list for every treatment', () => {
409
+ for (const edge of ['padded', 'flush', 'outset', 'bleed'] as const) {
410
+ const {ul} = renderEdge({edge});
411
+ expect(declarationsFor(ul)).toContain('overflow-x:clip');
412
+ cleanup();
413
+ }
414
+ });
415
+
416
+ it('treats the deprecated edgeCompensation="inline" as edge="outset" without a runtime warning', () => {
417
+ // Plain use is reported statically by the @deprecated type; rendering
418
+ // never warns merely because the old name is used (spec:AST-017 FR29a).
419
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
420
+ const {ul} = renderEdge({edgeCompensation: 'inline'});
421
+ expect(edgeContext(ul)).toBe('outset');
422
+ expect(ul.className).toContain('reachesContainerPadding');
423
+ expect(warn).not.toHaveBeenCalled();
424
+ warn.mockRestore();
425
+ });
426
+
427
+ it('lets edge win over the deprecated edgeCompensation and warns about the conflict', () => {
428
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
429
+ const {ul} = renderEdge({edge: 'flush', edgeCompensation: 'inline'});
430
+ expect(edgeContext(ul)).toBe('flush');
431
+ expect(ul.className).not.toContain('reachesContainerPadding');
432
+ expect(warn).toHaveBeenCalledTimes(1);
433
+ const message = String(warn.mock.calls[0][0]);
434
+ expect(message).toContain('`edge` wins');
435
+ expect(message).toContain('astryx upgrade');
436
+ warn.mockRestore();
437
+ });
438
+
439
+ it('keeps the inset of a ListItem outside a List', () => {
370
440
  const {container} = render(<ListItem label="Standalone" />);
371
- expect(container.querySelector('li')!.className).not.toContain(
372
- 'inlineEdgeCompensation',
441
+ expect(container.querySelector('li')!.className).not.toMatch(
442
+ /edge(Flush|Outset|Bleed)/,
373
443
  );
374
444
  });
375
-
376
445
  // ===========================================================================
377
446
  // Dividers
378
447
  // ===========================================================================
package/src/List/List.tsx CHANGED
@@ -4,9 +4,10 @@
4
4
 
5
5
  /**
6
6
  * @file List.tsx
7
- * @input Uses React, ReactNode, StyleXStyles, theme tokens, ListContext
8
- * with optional inline edge compensation
9
- * @output Exports List component, ListProps, ListDensity, ListStyle types
7
+ * @input Uses React, ReactNode, StyleXStyles, theme tokens, useDevWarning,
8
+ * and ListContext with the row edge treatment
9
+ * @output Exports List component, ListProps, ListDensity, ListEdge, ListStyle
10
+ * types
10
11
  * @position Core implementation; consumed by index.ts, tested by List.test.tsx
11
12
  *
12
13
  * SYNC: When modified, update these files to stay in sync:
@@ -24,13 +25,16 @@ import type {BaseProps} from '../BaseProps';
24
25
  import {
25
26
  ListContext,
26
27
  type ListDensity,
28
+ type ListEdge,
27
29
  type ListMarkerStyle,
28
30
  } from './ListContext';
29
31
  import {mergeProps} from '../utils';
30
32
  import {themeProps} from '../utils/themeProps';
33
+ import {useDevWarning} from '../hooks/useDevWarning';
31
34
 
32
35
  export {
33
36
  type ListDensity,
37
+ type ListEdge,
34
38
  type ListMarkerStyle as ListStyle,
35
39
  } from './ListContext';
36
40
 
@@ -60,16 +64,39 @@ export interface ListProps extends BaseProps<
60
64
  hasDividers?: boolean;
61
65
 
62
66
  /**
63
- * Compensates for each item's built-in inline inset, up to the container
64
- * padding available on each edge. Use "inline" to bring row content toward
65
- * sibling content such as a section heading. Content aligns when the
66
- * container padding is at least the item inset; smaller padding leaves
67
- * some inset uncompensated.
68
- * The cancelling margin reads the same variable the items derive their
69
- * inline padding from, so it tracks density and theme padding overrides
70
- * automatically. Hover and selection backgrounds still extend past the
71
- * text by the inset.
72
- * Omit to leave item positions unchanged.
67
+ * Where each row's text, dividers, and hover and selection backgrounds sit,
68
+ * relative to the list's edges and the container around it.
69
+ * - 'padded': text sits inside each row's padding, so it starts a little in
70
+ * from a heading above the list and backgrounds have room around it
71
+ * - 'flush': rows drop their side padding, so text starts on the heading's
72
+ * line and dividers and backgrounds stop at the row's content, with no
73
+ * room beyond it on either side. Works anywhere. Suits static rows
74
+ * - 'outset': text sits on the heading's line, and dividers and backgrounds
75
+ * reach a little past it into the container's padding. Suits interactive
76
+ * rows in a container
77
+ * - 'bleed': text sits on the heading's line, and dividers and backgrounds
78
+ * reach the container's edges with square corners, like Table rows in a
79
+ * Card
80
+ *
81
+ * A row's first content is what lines up with the heading: its marker,
82
+ * icon, or avatar when it has one, otherwise its label. 'outset' and
83
+ * 'bleed' reach into the padding of the nearest container that shares its
84
+ * padding: a Card, Section, Dialog, or BottomSheet, or a transparent
85
+ * container such as a Layout region with explicit padding or a padded
86
+ * ScrollableArea. Place the List directly in that container's padding:
87
+ * padding that Stack, Center, or your own styles add in between is not
88
+ * counted. Drawers and the automatic padding of LayoutContent and
89
+ * LayoutPanel aren't supported yet. With no such container, including in a Popover, they look
90
+ * like 'padded'. In a container with less padding than the rows' own, text
91
+ * starts a little after the heading's line. A theme's inline padding on `item` replaces the row
92
+ * padding of every value, including the zero padding of 'flush'.
93
+ * @default 'padded'
94
+ */
95
+ edge?: ListEdge;
96
+
97
+ /**
98
+ * @deprecated Use `edge="outset"`, which this sets. Still works; `edge`
99
+ * wins when both are set. `astryx upgrade` rewrites existing usage.
73
100
  */
74
101
  edgeCompensation?: 'inline';
75
102
 
@@ -121,6 +148,18 @@ const styles = stylex.create({
121
148
  // vertical scroller. (StyleX emits the physical longhand of this axis.)
122
149
  overflowInline: 'clip',
123
150
  },
151
+ // 'outset' and 'bleed' rows reach into the container's padding. The list
152
+ // reaches the container's padding edge on each inline edge first, so its
153
+ // inline clip (above) moves out with the rows instead of cutting them; each
154
+ // row then places itself inside it. Container padding vars are inherited,
155
+ // so the list can read them, while the rows' own inset is scoped to the
156
+ // rows. Missing or reset container padding resolves to zero.
157
+ reachesContainerPadding: {
158
+ marginInlineStart: 'calc(-1 * var(--container-padding-inline-start, 0px))',
159
+ marginInlineEnd: 'calc(-1 * var(--container-padding-inline-end, 0px))',
160
+ width:
161
+ 'calc(100% + var(--container-padding-inline-start, 0px) + var(--container-padding-inline-end, 0px))',
162
+ },
124
163
  withDividers: {
125
164
  gap: 0,
126
165
  },
@@ -148,9 +187,10 @@ const dynamicStyles = stylex.create({
148
187
  * Renders semantic `<ul>` or `<ol>` elements with configurable density,
149
188
  * dividers, marker styles, and an optional header.
150
189
  *
151
- * Set `edgeCompensation="inline"` to compensate for the items' inline inset
152
- * up to the container padding available on each edge, for alignment with
153
- * sibling content such as a section heading.
190
+ * Set `edge` to choose where row text, dividers, and backgrounds sit: inside
191
+ * each row's padding (`padded`, the default), at the list's edges (`flush`),
192
+ * on a heading's line with backgrounds reaching into the container's padding
193
+ * (`outset`), or reaching the container's edges like Table rows (`bleed`).
154
194
  *
155
195
  * @example
156
196
  * ```
@@ -162,12 +202,16 @@ const dynamicStyles = stylex.create({
162
202
  * <ListItem label="First step" />
163
203
  * <ListItem label="Second step" />
164
204
  * </List>
205
+ * <List edge="flush" hasDividers>
206
+ * <ListItem label="Solstice Mug" description="Ceramic, 12 oz" />
207
+ * </List>
165
208
  * ```
166
209
  */
167
210
  export function List({
168
211
  children,
169
212
  density = 'balanced',
170
213
  hasDividers = false,
214
+ edge: edgeProp,
171
215
  edgeCompensation,
172
216
  header,
173
217
  listStyle = 'none',
@@ -182,10 +226,21 @@ export function List({
182
226
  const headerId = useId();
183
227
  const isOrdered = listStyle === 'decimal';
184
228
  const Tag = isOrdered ? 'ol' : 'ul';
229
+ // Plain use of the deprecated prop is reported by its @deprecated type;
230
+ // development warns only when the two names conflict (spec:AST-017 FR29).
231
+ useDevWarning(
232
+ 'List',
233
+ '`edge` and the deprecated `edgeCompensation` are both set; `edge` wins. ' +
234
+ 'Remove `edgeCompensation`. `astryx upgrade` rewrites existing usage.',
235
+ edgeProp != null && edgeCompensation != null,
236
+ );
237
+ const edge: ListEdge =
238
+ edgeProp ?? (edgeCompensation === 'inline' ? 'outset' : 'padded');
239
+ const reachesContainerPadding = edge === 'outset' || edge === 'bleed';
185
240
 
186
241
  const contextValue = useMemo(
187
- () => ({density, hasDividers, listStyle, edgeCompensation}),
188
- [density, hasDividers, listStyle, edgeCompensation],
242
+ () => ({density, hasDividers, edge, listStyle}),
243
+ [density, hasDividers, edge, listStyle],
189
244
  );
190
245
 
191
246
  const listElement = (
@@ -211,6 +266,7 @@ export function List({
211
266
  stylex.props(
212
267
  styles.list,
213
268
  hasDividers && styles.withDividers,
269
+ reachesContainerPadding && styles.reachesContainerPadding,
214
270
  listStyle !== 'none' &&
215
271
  (start != null && start !== 1
216
272
  ? dynamicStyles.counterStart(start - 1)
@@ -5,8 +5,8 @@
5
5
  /**
6
6
  * @file ListContext.tsx
7
7
  * @input Layer-scoped React context
8
- * @output Exports ListContext for sharing density, dividers, marker style,
9
- * and optional inline edge compensation between List and ListItem, and
8
+ * @output Exports ListContext for sharing density, dividers, the row edge
9
+ * treatment, and marker style between List and ListItem, and
10
10
  * ListMarkerScope, which sets the marker for the items inside it, or a
11
11
  * task item's checkbox in its place
12
12
  * @position Internal context; consumed by List.tsx and ListItem.tsx
@@ -18,6 +18,12 @@ import {createLayerScopedContext as createContext} from '../Layer/layerScopedCon
18
18
  export type ListDensity = 'compact' | 'balanced' | 'spacious';
19
19
  export type ListMarkerStyle = 'none' | 'disc' | 'decimal' | 'circle';
20
20
 
21
+ /**
22
+ * Where each row's content and its lines and backgrounds sit relative to the
23
+ * list's edges and the padding of the container around the list.
24
+ */
25
+ export type ListEdge = 'padded' | 'flush' | 'outset' | 'bleed';
26
+
21
27
  /**
22
28
  * Every marker ListItem draws: the public styles, and the square, letter,
23
29
  * and roman markers Markdown's nested lists use (spec:AST-061 DEC-6).
@@ -35,8 +41,8 @@ export interface ListTaskMarker {
35
41
  export interface ListContextValue {
36
42
  density: ListDensity;
37
43
  hasDividers: boolean;
44
+ edge: ListEdge;
38
45
  listStyle: ListMarkerStyle;
39
- edgeCompensation?: 'inline';
40
46
  /**
41
47
  * The marker items draw instead of the one `listStyle` names, set by
42
48
  * ListMarkerScope; internal. Ignored when `listStyle` is `'none'`.
@@ -88,6 +88,13 @@ export const docs = {
88
88
  description:
89
89
  'Link relationship tokens. noopener noreferrer are merged automatically for target="_blank".',
90
90
  },
91
+ {
92
+ name: 'align',
93
+ type: "'center' | 'start' | 'first-line'",
94
+ description:
95
+ 'Vertical alignment of the start and end content against the label, passed through to Item unchanged. `first-line` keeps a small mark (a status dot, an icon) level with the first line of a wrapped label. See Item.align.',
96
+ default: "'center'",
97
+ },
91
98
  {
92
99
  name: 'isDisabled',
93
100
  type: 'boolean',
@@ -173,6 +180,13 @@ export const docsZh = {
173
180
  description:
174
181
  '链接关系标记。target="_blank" 会自动合并 noopener noreferrer。',
175
182
  },
183
+ {
184
+ name: 'align',
185
+ type: "'center' | 'start' | 'first-line'",
186
+ description:
187
+ '起始和结束内容相对于标签的垂直对齐方式,原样传递给 Item。`first-line` 让小标记(状态点、图标)与换行标签的第一行保持水平。参见 Item.align。',
188
+ default: "'center'",
189
+ },
176
190
  {
177
191
  name: 'isDisabled',
178
192
  type: 'boolean',
@@ -207,6 +221,8 @@ export const docsDense = {
207
221
  description:
208
222
  'List item w/ label, description, start/end content slots, interactive patterns.',
209
223
  propDescriptions: {
224
+ align:
225
+ "Start/end content alignment, passed to Item: 'center' (default), 'start', or 'first-line' (centered on the label's first line).",
210
226
  label:
211
227
  'Primary content. String auto-truncates; ReactNode renders as given.',
212
228
  description: 'Secondary text below label.',