@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
@@ -14,7 +14,7 @@
14
14
  * - /packages/cli/assets/templates/blocks/components/TreeList/ (showcase blocks)
15
15
  */
16
16
 
17
- import {useId, useMemo, type ReactNode} from 'react';
17
+ import {useId, useMemo, useRef, type ReactNode} from 'react';
18
18
  import * as stylex from '@stylexjs/stylex';
19
19
  import {
20
20
  colorVars,
@@ -27,6 +27,10 @@ import {
27
27
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
28
28
  import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
29
29
  import {usePressFeedback} from '../hooks/usePressFeedback';
30
+ import {
31
+ hasInteractiveAncestor,
32
+ useClickableContainer,
33
+ } from '../hooks/useClickableContainer';
30
34
  import {Icon} from '../Icon';
31
35
  import {mergeProps} from '../utils';
32
36
  import {useLinkComponent} from '../Link/useLinkComponent';
@@ -46,6 +50,13 @@ const styles = stylex.create({
46
50
  padding: 0,
47
51
  position: 'relative',
48
52
  width: '100%',
53
+ // Publish whether THIS treeitem has keyboard focus, for its row's
54
+ // `zIndex` (see `contentWrapper`). Every nested <li> redeclares the
55
+ // default, so a focused ancestor never lifts its descendants' rows.
56
+ '--_tree-focus-z': {
57
+ default: 'auto',
58
+ ':focus-visible': '2',
59
+ },
49
60
  // The treeitem row is the roving-tabindex focus owner; suppress the
50
61
  // native focus ring in favor of the row's :focus-visible outline below.
51
62
  outline: 'none',
@@ -82,6 +93,9 @@ const styles = stylex.create({
82
93
  display: 'flex',
83
94
  alignItems: 'center',
84
95
  gap: spacingVars['--spacing-2'],
96
+ // The row's own inline padding only. The per-level indent lives on
97
+ // `indentedContent` inside it, so a theme or caller padding here composes
98
+ // with the indent instead of replacing it.
85
99
  paddingInline: spacingVars['--spacing-2'],
86
100
  // No `outline: 'none'` here: this element receives the shared focus ring,
87
101
  // which already defaults to none, and the shorthand would erase it.
@@ -89,12 +103,38 @@ const styles = stylex.create({
89
103
  position: 'relative',
90
104
  boxSizing: 'border-box',
91
105
  textAlign: 'start',
92
- // Per-level indent. Declared here (not inline) so it lives in
93
- // `@layer astryx-base` and the theme layer can override it in normal
94
- // cascade order — an inline longhand would outrank every layer. The row
95
- // publishes only the computed distance as `--_tree-indent`; the per-level
96
- // step is the public `--tree-list-indent` lever (see TreeList `root`).
97
- marginInlineStart: 'var(--_tree-indent, 0px)',
106
+ },
107
+ // Guide lines draw above the rows (TreeListBranches) so a row's fill does
108
+ // not break them. Lift the row whose focus ring is showing above the guides
109
+ // too, so no guide cuts through the ring: when focus is inside the row, or
110
+ // when its own treeitem publishes focus (`--_tree-focus-z` on `wrapper`).
111
+ // The ring is the row's outline, so it cannot be lifted on its own: a
112
+ // focused row that is also filled (selected or hovered) covers the guides at
113
+ // that row. That is the accepted trade-off (DD3). Applied only with
114
+ // `focusWithinOrPublished`, the rows that draw the ring: a plain row shows
115
+ // no ring, so its fill stays under the guides.
116
+ focusLift: {
117
+ zIndex: {
118
+ default: 'var(--_tree-focus-z, auto)',
119
+ ':has(:focus-visible)': 2,
120
+ },
121
+ },
122
+ // Per-level indent, drawn INSIDE the row box: the row (`contentWrapper`)
123
+ // stays full width, so a nested row's hover and selected fill spans the
124
+ // list like a root row's, while its content sits exactly where a margin
125
+ // indent would put it. A child of the row rather than the row's own padding,
126
+ // so a theme or caller that sets the row's padding keeps the indent. It
127
+ // inherits the row's flex alignment and gap, so a theme that retunes those
128
+ // on `tree-list-item` still lays the content out. The row publishes only the
129
+ // computed distance as `--_tree-indent`; the per-level step is the public,
130
+ // themeable `--tree-list-indent` lever (see TreeList `root`).
131
+ indentedContent: {
132
+ display: 'flex',
133
+ flex: 1,
134
+ minWidth: 0,
135
+ alignItems: 'inherit',
136
+ gap: 'inherit',
137
+ paddingInlineStart: 'var(--_tree-indent, 0px)',
98
138
  },
99
139
  interactive: {
100
140
  cursor: {
@@ -369,6 +409,7 @@ export function TreeListItem({
369
409
  const labelId = useId();
370
410
  const descriptionId = useId();
371
411
  const LinkComponent = useLinkComponent();
412
+ const linkRef = useRef<HTMLAnchorElement>(null);
372
413
  const isInteractive = onClick != null || href != null;
373
414
 
374
415
  const handleToggle = useMemo(
@@ -388,8 +429,10 @@ export function TreeListItem({
388
429
  if (isDisabled) {
389
430
  return;
390
431
  }
391
- const el = e.target as HTMLElement;
392
- if (el.closest('button, a, input, select, textarea')) {
432
+ // Controls inside the row, native or role-based, keep their own
433
+ // activation (FR7): the same rule link rows get from the shared
434
+ // enlarged-surface hook below.
435
+ if (hasInteractiveAncestor(e.target as Element, e.currentTarget)) {
393
436
  return;
394
437
  }
395
438
  if (onClick != null) {
@@ -402,6 +445,34 @@ export function TreeListItem({
402
445
  return undefined;
403
446
  }, [onClick, hasChildren, onToggle, id, isDisabled]);
404
447
 
448
+ // A link row (an `href` and no other row action) is an enlarged surface for
449
+ // its link: the indent is part of the row, so a click anywhere in it must
450
+ // follow the link, as Enter already does. Built on the shared
451
+ // enlarged-surface hook (family:navigation-destinations), which skips
452
+ // clicks on nested controls, native or role-based, and text selections.
453
+ // - Clicks are delegated: the hook re-dispatches the click on the anchor
454
+ // with its modifier keys, so the link component and the browser handle
455
+ // same-tab, new-tab and new-window activation, and stops the original, so
456
+ // ancestors see one click per press.
457
+ // - Middle-click has no synthetic equivalent, so it takes the hook's href
458
+ // path, which opens the destination in a new tab after the shared
459
+ // navigation rule.
460
+ // The container ref is attached to link rows only, so every other row stays
461
+ // a plain element and both hook instances stay inert on it.
462
+ const forwardsToLink = href != null && handleClick == null;
463
+ const linkRowRef = useRef<HTMLDivElement>(null);
464
+ const {onClick: delegateToLink} = useClickableContainer({
465
+ containerRef: linkRowRef,
466
+ interactiveRef: linkRef,
467
+ disabled: isDisabled || !forwardsToLink,
468
+ });
469
+ const {onMouseUp: openLinkOnMiddleClick} = useClickableContainer({
470
+ containerRef: linkRowRef,
471
+ href,
472
+ target,
473
+ disabled: isDisabled || !forwardsToLink,
474
+ });
475
+
405
476
  // Per-level indent distance. The per-level step is the public, themeable
406
477
  // `--tree-list-indent` lever (default `--spacing-4`, set on the tree-list
407
478
  // root). A leaf adds a fixed chevron-column offset (chevron width + gap) so
@@ -411,8 +482,8 @@ export function TreeListItem({
411
482
  // every row sits flush, like a parent at that level. That offset is tied to
412
483
  // the chevron's own dimensions, not the indent step, so it does not scale
413
484
  // with the lever. Published as the private `--_tree-indent` and consumed by
414
- // `contentWrapper`'s stylesheet `margin-inline-start` (kept out of the inline
415
- // style so the theme layer can override it — see #4308).
485
+ // `indentedContent`'s stylesheet `padding-inline-start`; the step stays
486
+ // themeable through `--tree-list-indent` (see #4308).
416
487
  const reservesChevronColumn = !hasChildren && hasExpandableItems;
417
488
  const indentDistance = reservesChevronColumn
418
489
  ? `calc(${nestedLevel} * var(--tree-list-indent) + ${spacingVars['--spacing-4']} + ${spacingVars['--spacing-2']})`
@@ -521,6 +592,7 @@ export function TreeListItem({
521
592
  )}
522
593
  {href != null ? (
523
594
  <LinkComponent
595
+ ref={linkRef}
524
596
  href={href}
525
597
  target={target}
526
598
  aria-disabled={isDisabled || undefined}
@@ -599,6 +671,7 @@ export function TreeListItem({
599
671
  isInteractive || (hasChildren && onClick == null)
600
672
  ? focusOutlineProps.focusWithinOrPublished(
601
673
  styles.contentWrapper,
674
+ styles.focusLift,
602
675
  densityStyles[density],
603
676
  styles.interactive,
604
677
  interactionOverlayStyles.backgroundImage,
@@ -620,8 +693,10 @@ export function TreeListItem({
620
693
  className,
621
694
  {...indentStyle, ...style},
622
695
  )}
623
- onClick={handleClick}>
624
- {innerContent}
696
+ ref={forwardsToLink ? linkRowRef : undefined}
697
+ onClick={forwardsToLink ? delegateToLink : handleClick}
698
+ onMouseUp={forwardsToLink ? openLinkOnMiddleClick : undefined}>
699
+ <div {...stylex.props(styles.indentedContent)}>{innerContent}</div>
625
700
  </div>
626
701
  </div>
627
702
  {isExpanded && renderedChildren != null && (
@@ -3,7 +3,8 @@
3
3
  /* eslint-disable @typescript-eslint/no-require-imports */
4
4
  /**
5
5
  * @file Guards doc prop types against opaque named unions (#1645).
6
- * @input Every {Name}.doc.mjs plus the TypeScript sources they describe.
6
+ * @input Every {Name}.doc.mjs plus the TypeScript sources they describe,
7
+ * including same-file private aliases used by exported literal unions.
7
8
  * @output Fails when a documented prop type hides its legal literal values.
8
9
  * @position Repo-wide doc-shape guard, sibling of docPropReferences.test.ts.
9
10
  *
@@ -188,18 +189,34 @@ for (const file of findFiles(SRC_DIR, ['.ts', '.tsx'])) {
188
189
  // markers the comment-stripped `src` has already removed.
189
190
  const rawTypeRhs = new Map<string, string>();
190
191
  for (const m of raw.matchAll(
191
- /export type ([A-Z][A-Za-z0-9]*)(?:<[^>]*>)?\s*=\s*([^;]+);/g,
192
+ /\btype ([A-Z][A-Za-z0-9]*)(?:<[^>]*>)?\s*=\s*([^;]+);/g,
192
193
  )) {
193
194
  rawTypeRhs.set(m[1], m[2]);
194
195
  }
195
- for (const m of src.matchAll(
196
- /export type ([A-Z][A-Za-z0-9]*)(?:<[^>]*>)?\s*=\s*([^;]+);/g,
197
- )) {
198
- const [, name, rhsRaw] = m;
199
- const rhs = rhsRaw.replace(/\s+/g, ' ').trim();
200
- if (LITERAL_UNION.test(rhs)) {
196
+ const declarations = [
197
+ ...src.matchAll(
198
+ /\b(export )?type ([A-Z][A-Za-z0-9]*)(?:<[^>]*>)?\s*=\s*([^;]+);/g,
199
+ ),
200
+ ];
201
+ // Resolve same-file literal aliases before registering exported types.
202
+ // Repeat until no new literal unions are found.
203
+ const fileLiterals = new Map<string, string>();
204
+ let previousSize: number;
205
+ do {
206
+ previousSize = fileLiterals.size;
207
+ for (const [, , name, rhsRaw] of declarations) {
208
+ const rhs = rhsRaw
209
+ .replace(/\s+/g, ' ')
210
+ .trim()
211
+ .split('|')
212
+ .map(part => fileLiterals.get(part.trim()) ?? part.trim())
213
+ .join(' | ');
214
+
215
+ if (!LITERAL_UNION.test(rhs)) {
216
+ continue;
217
+ }
201
218
  const deprecated = deprecatedLiteralMembers(rawTypeRhs.get(name) ?? '');
202
- literalUnions.set(
219
+ fileLiterals.set(
203
220
  name,
204
221
  rhs
205
222
  .replace(/^\|\s*/, '')
@@ -209,8 +226,17 @@ for (const file of findFiles(SRC_DIR, ['.ts', '.tsx'])) {
209
226
  .filter(member => !deprecated.has(member))
210
227
  .join(' | '),
211
228
  );
229
+ }
230
+ } while (fileLiterals.size !== previousSize);
231
+ for (const [, exported, name, rhsRaw] of declarations) {
232
+ if (!exported) {
233
+ continue;
234
+ }
235
+ const literals = fileLiterals.get(name);
236
+ if (literals !== undefined) {
237
+ literalUnions.set(name, literals);
212
238
  } else {
213
- namedBodies.set(name, rhs);
239
+ namedBodies.set(name, rhsRaw.replace(/\s+/g, ' ').trim());
214
240
  }
215
241
  }
216
242
  for (const m of src.matchAll(
@@ -230,19 +230,6 @@ export function useMenuHover<T extends HTMLElement = HTMLElement>(
230
230
  const hoverOpenedAtRef = useRef(0);
231
231
  const prevIsOpenRef = useRef(isOpen);
232
232
 
233
- // Catches every close, whatever caused it. A layout effect, not render: it
234
- // must land before the browser hit-tests the vanished panel and fires the
235
- // mouseenter REOPEN_SUPPRESS_MS exists to swallow.
236
- useIsomorphicLayoutEffect(() => {
237
- const wasOpen = prevIsOpenRef.current;
238
- prevIsOpenRef.current = isOpen;
239
- if (wasOpen && !isOpen) {
240
- hoverModeRef.current = false;
241
- hoverOpenedAtRef.current = 0;
242
- closedAtRef.current = Date.now();
243
- }
244
- }, [isOpen]);
245
-
246
233
  const clearTimeouts = useCallback(() => {
247
234
  if (showTimerRef.current) {
248
235
  clearTimeout(showTimerRef.current);
@@ -256,6 +243,24 @@ export function useMenuHover<T extends HTMLElement = HTMLElement>(
256
243
  safeTriangleCleanupRef.current = null;
257
244
  }, []);
258
245
 
246
+ // Catches every close, whatever caused it. A layout effect, not render: it
247
+ // must land before the browser hit-tests the vanished panel and fires the
248
+ // mouseenter REOPEN_SUPPRESS_MS exists to swallow.
249
+ useIsomorphicLayoutEffect(() => {
250
+ const wasOpen = prevIsOpenRef.current;
251
+ prevIsOpenRef.current = isOpen;
252
+ if (wasOpen && !isOpen) {
253
+ // A hover-open scheduled before the close must not fire afterwards and
254
+ // reopen what just closed from elsewhere — e.g. the root menu's light
255
+ // dismiss closing a submenu flyout mid hover-intent (#6893). A fresh
256
+ // mouseenter still opens; only the stale timer is cancelled.
257
+ clearTimeouts();
258
+ hoverModeRef.current = false;
259
+ hoverOpenedAtRef.current = 0;
260
+ closedAtRef.current = Date.now();
261
+ }
262
+ }, [isOpen, clearTimeouts]);
263
+
259
264
  // useListFocus needs an onEscape that is defined in terms of its own listRef;
260
265
  // the indirection avoids the use-before-declare.
261
266
  const escapeHandlerRef = useRef<() => void>(() => {});
@@ -11,17 +11,22 @@
11
11
  */
12
12
 
13
13
  import {describe, it, expect, beforeEach, afterEach} from 'vitest';
14
+ import {spawnSync} from 'node:child_process';
14
15
  import * as fs from 'node:fs';
15
16
  import * as os from 'node:os';
16
17
  import * as path from 'node:path';
18
+ import {fileURLToPath} from 'node:url';
17
19
  // Core's own package-local contract copy — what actually runs at install time.
18
20
  import {
19
21
  isAstryxInitialized,
22
+ isAstryxPromptInReach,
20
23
  shouldNudge,
21
24
  AGENT_DOC_PATHS,
22
25
  INIT_MARKERS,
23
26
  SETUP_NUDGE,
27
+ setupNudge,
24
28
  } from '../scripts/agent-doc-state.mjs';
29
+ import {cliInvocation} from '../scripts/postinstall.mjs';
25
30
  // Dev-only, monorepo-relative: core must not depend on the CLI at runtime (the
26
31
  // CLI peer-depends on core), but the test can read the source and the generator.
27
32
  import * as cliLeaf from '../../cli/foundation/agent-docs/agent-doc-state.mjs';
@@ -92,12 +97,58 @@ describe('core postinstall — one authoritative source, no drift', () => {
92
97
  expect(AGENT_DOC_PATHS).toEqual(cliLeaf.AGENT_DOC_PATHS);
93
98
  expect(INIT_MARKERS).toEqual(cliLeaf.INIT_MARKERS);
94
99
  expect(SETUP_NUDGE).toBe(cliLeaf.SETUP_NUDGE);
100
+ expect(setupNudge('pnpm exec astryx')).toBe(
101
+ cliLeaf.setupNudge('pnpm exec astryx'),
102
+ );
95
103
  });
96
104
 
97
105
  it('agrees with the CLI leaf on a real project', () => {
98
106
  write('AGENTS.md', `# doc\n${MARKER}\nbody`);
99
107
  expect(isAstryxInitialized(tmp)).toBe(cliLeaf.isAstryxInitialized(tmp));
100
108
  });
109
+
110
+ it('agrees with the CLI leaf on a monorepo package', () => {
111
+ fs.mkdirSync(path.join(tmp, '.git'));
112
+ write('AGENTS.md', MARKER);
113
+ write('packages/app/package.json', '{"name":"app"}');
114
+ const app = path.join(tmp, 'packages/app');
115
+ expect(isAstryxPromptInReach(app)).toBe(true);
116
+ expect(isAstryxPromptInReach(app)).toBe(cliLeaf.isAstryxPromptInReach(app));
117
+ });
118
+ });
119
+
120
+ // Install time, end to end: core's real script and its contract copy, installed
121
+ // under a monorepo's node_modules, run the way npm runs them for a package.
122
+ describe('core postinstall — inside a monorepo package', () => {
123
+ const CORE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
124
+ const install = () => {
125
+ const installed = path.join(tmp, 'node_modules/@astryxdesign/core/scripts');
126
+ fs.mkdirSync(installed, {recursive: true});
127
+ for (const file of ['postinstall.mjs', 'agent-doc-state.mjs']) {
128
+ fs.copyFileSync(path.join(CORE, 'scripts', file), path.join(installed, file));
129
+ }
130
+ return spawnSync(process.execPath, [path.join(installed, 'postinstall.mjs')], {
131
+ env: {...process.env, INIT_CWD: path.join(tmp, 'packages/app'), npm_command: 'install'},
132
+ encoding: 'utf8',
133
+ });
134
+ };
135
+ beforeEach(() => {
136
+ fs.mkdirSync(path.join(tmp, '.git'));
137
+ write('packages/app/package.json', '{"name":"app"}');
138
+ });
139
+
140
+ it('stays quiet when init ran at the repository root', () => {
141
+ write('AGENTS.md', MARKER);
142
+ const r = install();
143
+ expect(r.status).toBe(0);
144
+ expect(r.stdout).toBe('');
145
+ });
146
+
147
+ it('still nudges when no folder up to the repository root ran init', () => {
148
+ const r = install();
149
+ expect(r.status).toBe(0);
150
+ expect(r.stdout).toMatch(/finish setup and install the Astryx agent prompt/);
151
+ });
101
152
  });
102
153
 
103
154
  describe('core postinstall — shouldNudge', () => {
@@ -135,3 +186,31 @@ describe('core postinstall — shouldNudge', () => {
135
186
  }
136
187
  });
137
188
  });
189
+
190
+ describe('core postinstall — the nudge names the command the CLI would print', () => {
191
+ it('words the nudge around the stem it is given', () => {
192
+ expect(setupNudge('pnpm exec astryx')).toContain(
193
+ 'run `pnpm exec astryx init` to finish setup',
194
+ );
195
+ expect(SETUP_NUDGE).toContain('run `npx @astryxdesign/cli init`');
196
+ });
197
+
198
+ it("asks the project's installed CLI for its stem", async () => {
199
+ write('package.json', '{"name":"app"}');
200
+ write('pnpm-lock.yaml', '');
201
+ write('node_modules/.bin/astryx', '');
202
+ fs.mkdirSync(path.join(tmp, 'node_modules/@astryxdesign'), {
203
+ recursive: true,
204
+ });
205
+ fs.symlinkSync(
206
+ path.resolve(import.meta.dirname, '../../cli'),
207
+ path.join(tmp, 'node_modules/@astryxdesign/cli'),
208
+ );
209
+ expect(await cliInvocation(tmp)).toBe('pnpm exec astryx');
210
+ });
211
+
212
+ it('has no stem to offer when the CLI is not installed', async () => {
213
+ write('package.json', '{"name":"app"}');
214
+ expect(await cliInvocation(tmp)).toBeUndefined();
215
+ });
216
+ });
@@ -4,7 +4,8 @@
4
4
  * @file reset.a11y.chromium.spec.ts
5
5
  * @input packages/core/src/reset.css, alone on a blank Chromium page
6
6
  * @output Browser evidence that the reset reaches ::before, ::after,
7
- * ::placeholder and the search field's cancel button
7
+ * ::placeholder, the search field's cancel button and the dropdown marker
8
+ * of a field with a list
8
9
  * @position reset.css integration proof. A pseudo-element inside :where() is
9
10
  * invalid, and the forgiving list drops it without an error, so only a
10
11
  * shipping engine shows which of the reset's rules still match.
@@ -13,6 +14,8 @@
13
14
  import * as fs from 'node:fs';
14
15
  import {fileURLToPath} from 'node:url';
15
16
  import {expect, test, type Page} from '@playwright/test';
17
+ // @ts-expect-error -- pngjs ships no declarations; runtime support is pinned.
18
+ import {PNG} from 'pngjs';
16
19
 
17
20
  const RESET_CSS = fs.readFileSync(
18
21
  fileURLToPath(new URL('./reset.css', import.meta.url)),
@@ -154,6 +157,54 @@ test('a search field has no cancel button to clear it', async ({page}) => {
154
157
  await expect(query).toHaveValue('query');
155
158
  });
156
159
 
160
+ test('the dropdown marker of a field with a list sits in its middle', async ({
161
+ page,
162
+ }) => {
163
+ // Chromium shows the marker while the pointer is over the field and draws
164
+ // it as a glyph set on the field's line-height, the 1.5 the reset inherits.
165
+ await page.evaluate(() => {
166
+ const field = document.createElement('input');
167
+ field.id = 'fruit';
168
+ field.setAttribute('list', 'fruits');
169
+ const options = document.createElement('datalist');
170
+ options.id = 'fruits';
171
+ options.append(new Option('Apple'));
172
+ document.body.append(field, options);
173
+ });
174
+ await page.addStyleTag({
175
+ content: `
176
+ #fruit {
177
+ display: block;
178
+ width: 240px;
179
+ padding: 7px 11px;
180
+ border: 1px solid;
181
+ line-height: 1.5;
182
+ }
183
+ `,
184
+ });
185
+ const field = page.locator('#fruit');
186
+ await field.hover();
187
+ const png = PNG.sync.read(await field.screenshot({caret: 'hide'}));
188
+ // The marker is the only ink in the field's last 32px, inside the border.
189
+ const inkRows: number[] = [];
190
+ for (let y = 2; y < png.height - 2; y++) {
191
+ for (let x = png.width - 32; x < png.width - 2; x++) {
192
+ const offset = (y * png.width + x) * 4;
193
+ const luminance =
194
+ (png.data[offset] + png.data[offset + 1] + png.data[offset + 2]) / 3;
195
+ if (luminance < 160) {
196
+ inkRows.push(y);
197
+ break;
198
+ }
199
+ }
200
+ }
201
+ expect(inkRows.length, 'the hovered field shows its marker').toBeGreaterThan(
202
+ 0,
203
+ );
204
+ const markerMiddle = (inkRows[0] + inkRows[inkRows.length - 1] + 1) / 2;
205
+ expect(Math.abs(markerMiddle - png.height / 2)).toBeLessThanOrEqual(1);
206
+ });
207
+
157
208
  test('a later layer and an unlayered rule still override the reset', async ({
158
209
  page,
159
210
  }) => {
package/src/reset.css CHANGED
@@ -203,35 +203,6 @@
203
203
  height: auto;
204
204
  }
205
205
 
206
- /* ===========================================================================
207
- AspectRatio Fit Baseline
208
- =========================================================================== */
209
-
210
- /*
211
- * Child sizing for AspectRatio's `fit` prop — the component marks the
212
- * child's direct parent with `data-astryx-aspect-ratio-override` (carrying
213
- * the fit value), and these direct-child rules size the child, so
214
- * consumers don't hand-write width/height/object-fit on every media child.
215
- * The marker sits on the parent itself, so the rules don't depend on
216
- * AspectRatio's internal element structure or its theming surface. As with
217
- * the [data-astryx-media] baseline below, the rules are zero-specificity
218
- * and any styling the child sets itself still wins. `fit="center"` needs
219
- * no rules here — the component centers the child from its own wrapper.
220
- */
221
- :where([data-astryx-aspect-ratio-override="cover"], [data-astryx-aspect-ratio-override="contain"])
222
- > :where(*) {
223
- width: 100%;
224
- height: 100%;
225
- }
226
-
227
- :where([data-astryx-aspect-ratio-override="cover"]) > :where(img, video) {
228
- object-fit: cover;
229
- }
230
-
231
- :where([data-astryx-aspect-ratio-override="contain"]) > :where(img, video) {
232
- object-fit: contain;
233
- }
234
-
235
206
  /* ===========================================================================
236
207
  Forms
237
208
  =========================================================================== */
@@ -294,6 +265,15 @@
294
265
  color: var(--color-text-secondary, light-dark(#4E606F, #AAAFB5));
295
266
  }
296
267
 
268
+ /*
269
+ * Chromium draws the dropdown marker of a field with a `list` as a glyph
270
+ * set on the line-height the field inherits, so with any line-height above
271
+ * 1 the marker sits below the middle of the field.
272
+ */
273
+ :where([list])::-webkit-calendar-picker-indicator {
274
+ line-height: 1;
275
+ }
276
+
297
277
  /*
298
278
  * Remove the inner padding and cancel buttons in search inputs
299
279
  * for Chrome and Safari.
package/src/reset.test.ts CHANGED
@@ -172,6 +172,17 @@ describe('reset.css pseudo-element rules', () => {
172
172
  ).toBe(true);
173
173
  });
174
174
 
175
+ it('sets the dropdown marker of a field with a list on a single line', () => {
176
+ const marker = ruleBlocks.filter(({selector}) =>
177
+ topLevelSelectors(selector).some(entry =>
178
+ entry.endsWith('::-webkit-calendar-picker-indicator'),
179
+ ),
180
+ );
181
+ expect(marker.map(({body}) => body).join('\n')).toMatch(
182
+ /line-height:\s*1;/,
183
+ );
184
+ });
185
+
175
186
  it('falls back to the --color-text-secondary default for placeholders', () => {
176
187
  // A page that loads only the reset never defines the token. The fallback
177
188
  // repeats the token's own default, so that page shows the placeholder an
@@ -261,6 +261,9 @@ const CROSS_COMPONENT_VARS: Record<string, string[]> = {
261
261
  // The destructive item variant recolors the Item it renders; Item owns,
262
262
  // documents and reads both slots.
263
263
  DropdownMenu: ['--_item-label-color', '--_item-description-color'],
264
+ // List's edge="flush" zeroes the inline inset of the Item each ListItem
265
+ // renders; Item owns, documents and reads it.
266
+ List: ['--_item-inset-inline'],
264
267
  // A layer content root stops an outer Stepper's connector gap; Stepper owns,
265
268
  // documents and reads it.
266
269
  Layer: ['--step-connector-gap'],
@@ -316,6 +319,10 @@ const VARS_WITHOUT_DERIVED_MAPPING = new Set([
316
319
  '--tree-list-indent',
317
320
  '--_tree-indent',
318
321
  '--tree-list-row-gap',
322
+ // Published by each treeitem while it has keyboard focus and read as its
323
+ // row's z-index, so the focused row paints above the guide lines. Focus
324
+ // state, not a value a theme author sets.
325
+ '--_tree-focus-z',
319
326
  // Clipped on the ONE side each on-track connector segment faces the
320
327
  // indicator from — trailing above the node, leading below it, mirrored
321
328
  // again per axis. No standard property maps onto this purpose-based seam:
@@ -0,0 +1,69 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file unicodePropertyEscapes.source-build.test.mjs
5
+ * @input Every Core source module a bundler compiles (tests and stories
6
+ * excluded)
7
+ * @output Fails when a module writes a Unicode property escape (`\p{…}` or
8
+ * `\P{…}`) as a regular expression literal
9
+ * @position Guard for source builds: for targets without property escapes, or
10
+ * with no browserslist, Babel rewrites such a literal, and the copy bundled
11
+ * with Next.js lacks the Unicode data to do it, so the build fails with
12
+ * "Unknown property". Modules build these expressions at runtime instead,
13
+ * as NumberInput/numberParser.ts does.
14
+ */
15
+
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import {fileURLToPath} from 'node:url';
19
+ import {parseSync} from '@babel/core';
20
+ import {describe, expect, it} from 'vitest';
21
+
22
+ const SRC = path.dirname(fileURLToPath(import.meta.url));
23
+ const SKIPPED = /\.(test|stories)\.tsx?$|\.d\.ts$/;
24
+
25
+ function sourceFiles(dir) {
26
+ return fs.readdirSync(dir, {withFileTypes: true}).flatMap(entry => {
27
+ const file = path.join(dir, entry.name);
28
+ if (entry.isDirectory()) {
29
+ return entry.name === '__tests__' ? [] : sourceFiles(file);
30
+ }
31
+ return /\.tsx?$/.test(entry.name) && !SKIPPED.test(entry.name)
32
+ ? [file]
33
+ : [];
34
+ });
35
+ }
36
+
37
+ /** Every regular expression literal in a parsed tree. */
38
+ function regexLiterals(node, found = []) {
39
+ if (node == null || typeof node !== 'object') return found;
40
+ if (node.type === 'RegExpLiteral') found.push(node);
41
+ for (const [key, value] of Object.entries(node)) {
42
+ if (key !== 'loc' && key !== 'extra') regexLiterals(value, found);
43
+ }
44
+ return found;
45
+ }
46
+
47
+ describe('Core regular expressions in source builds', () => {
48
+ it('writes no Unicode property escape as a regex literal', () => {
49
+ const offenders = [];
50
+ for (const file of sourceFiles(SRC)) {
51
+ const source = fs.readFileSync(file, 'utf8');
52
+ if (!/\\[pP]\{/.test(source)) continue;
53
+ const ast = parseSync(source, {
54
+ filename: file,
55
+ babelrc: false,
56
+ configFile: false,
57
+ parserOpts: {plugins: ['typescript', 'jsx']},
58
+ });
59
+ for (const literal of regexLiterals(ast)) {
60
+ if (/\\[pP]\{/.test(literal.pattern)) {
61
+ offenders.push(
62
+ `${path.relative(SRC, file)}:${literal.loc.start.line} /${literal.pattern}/${literal.flags}`,
63
+ );
64
+ }
65
+ }
66
+ }
67
+ expect(offenders).toEqual([]);
68
+ });
69
+ });