@astryxdesign/core 0.4.0 → 0.4.1-canary.8138bfc

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 (236) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  3. package/dist/AspectRatio/AspectRatio.js +1 -2
  4. package/dist/AspectRatio/index.d.ts.map +1 -1
  5. package/dist/AspectRatio/index.js +1 -2
  6. package/dist/Badge/Badge.d.ts.map +1 -1
  7. package/dist/Badge/Badge.js +1 -2
  8. package/dist/Badge/index.d.ts.map +1 -1
  9. package/dist/Badge/index.js +1 -2
  10. package/dist/Banner/Banner.d.ts +3 -0
  11. package/dist/Banner/Banner.d.ts.map +1 -1
  12. package/dist/Banner/Banner.js +17 -3
  13. package/dist/Blockquote/Blockquote.d.ts.map +1 -1
  14. package/dist/Blockquote/Blockquote.js +1 -2
  15. package/dist/Blockquote/index.d.ts.map +1 -1
  16. package/dist/Blockquote/index.js +1 -2
  17. package/dist/Button/Button.d.ts.map +1 -1
  18. package/dist/Button/Button.js +2 -2
  19. package/dist/Card/Card.d.ts.map +1 -1
  20. package/dist/Card/Card.js +1 -2
  21. package/dist/Card/index.d.ts.map +1 -1
  22. package/dist/Card/index.js +1 -2
  23. package/dist/Center/Center.d.ts.map +1 -1
  24. package/dist/Center/Center.js +1 -2
  25. package/dist/Center/index.d.ts.map +1 -1
  26. package/dist/Center/index.js +1 -2
  27. package/dist/Code/Code.d.ts.map +1 -1
  28. package/dist/Code/Code.js +1 -2
  29. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  30. package/dist/CommandPalette/CommandPalette.js +5 -0
  31. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  32. package/dist/ComplexSelector/ComplexSelector.js +7 -8
  33. package/dist/DateInput/DateInput.d.ts.map +1 -1
  34. package/dist/DateInput/DateInput.js +23 -10
  35. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  36. package/dist/DateRangeInput/DateRangeInput.js +44 -16
  37. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  38. package/dist/DateTimeInput/DateTimeInput.js +32 -13
  39. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +1 -1
  40. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
  41. package/dist/DropdownMenu/DropdownMenuRadioGroup.js +10 -3
  42. package/dist/Field/PanelSearchInput.d.ts +81 -0
  43. package/dist/Field/PanelSearchInput.d.ts.map +1 -0
  44. package/dist/Field/PanelSearchInput.js +151 -0
  45. package/dist/FileInput/FileInput.d.ts.map +1 -1
  46. package/dist/FileInput/FileInput.js +21 -10
  47. package/dist/Grid/Grid.d.ts.map +1 -1
  48. package/dist/Grid/Grid.js +1 -2
  49. package/dist/Grid/index.d.ts.map +1 -1
  50. package/dist/Grid/index.js +1 -2
  51. package/dist/Icon/globalIconRegistry.d.ts +5 -4
  52. package/dist/Icon/globalIconRegistry.d.ts.map +1 -1
  53. package/dist/Icon/globalIconRegistry.js +5 -4
  54. package/dist/Indicator/index.d.ts +1 -1
  55. package/dist/Indicator/index.d.ts.map +1 -1
  56. package/dist/Indicator/types.d.ts +10 -0
  57. package/dist/Indicator/types.d.ts.map +1 -1
  58. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  59. package/dist/Lightbox/Lightbox.js +24 -50
  60. package/dist/MultiSelector/MultiSelector.d.ts +8 -1
  61. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  62. package/dist/MultiSelector/MultiSelector.js +134 -102
  63. package/dist/NavItem/navItemStyles.stylex.d.ts +17 -5
  64. package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
  65. package/dist/NavItem/navItemStyles.stylex.js +11 -5
  66. package/dist/Popover/usePopover.d.ts +15 -0
  67. package/dist/Popover/usePopover.d.ts.map +1 -1
  68. package/dist/Popover/usePopover.js +14 -2
  69. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  70. package/dist/ProgressBar/ProgressBar.js +2 -3
  71. package/dist/Section/Section.d.ts.map +1 -1
  72. package/dist/Section/Section.js +1 -2
  73. package/dist/Section/index.d.ts.map +1 -1
  74. package/dist/Section/index.js +1 -2
  75. package/dist/Selector/Selector.d.ts +10 -0
  76. package/dist/Selector/Selector.d.ts.map +1 -1
  77. package/dist/Selector/Selector.js +135 -95
  78. package/dist/SideNav/SideNav.d.ts +7 -9
  79. package/dist/SideNav/SideNav.d.ts.map +1 -1
  80. package/dist/SideNav/SideNav.js +32 -5
  81. package/dist/SideNav/SideNavCollapseButton.d.ts +25 -9
  82. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  83. package/dist/SideNav/SideNavCollapseButton.js +39 -15
  84. package/dist/SideNav/SideNavCollapseContext.d.ts +21 -0
  85. package/dist/SideNav/SideNavCollapseContext.d.ts.map +1 -1
  86. package/dist/SideNav/SideNavCollapseContext.js +16 -2
  87. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  88. package/dist/SideNav/SideNavHeading.js +70 -24
  89. package/dist/SideNav/SideNavItem.d.ts +7 -2
  90. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  91. package/dist/SideNav/SideNavItem.js +114 -75
  92. package/dist/SideNav/SideNavSection.d.ts.map +1 -1
  93. package/dist/SideNav/SideNavSection.js +7 -16
  94. package/dist/SideNav/index.d.ts +1 -1
  95. package/dist/SideNav/index.d.ts.map +1 -1
  96. package/dist/Skeleton/Skeleton.d.ts.map +1 -1
  97. package/dist/Skeleton/Skeleton.js +1 -2
  98. package/dist/Skeleton/index.d.ts.map +1 -1
  99. package/dist/Skeleton/index.js +1 -2
  100. package/dist/Slider/Slider.d.ts.map +1 -1
  101. package/dist/Slider/Slider.js +2 -6
  102. package/dist/Switch/Switch.d.ts.map +1 -1
  103. package/dist/Switch/Switch.js +3 -3
  104. package/dist/Table/TableRow.d.ts +1 -1
  105. package/dist/Table/TableRow.d.ts.map +1 -1
  106. package/dist/Table/TableRow.js +4 -2
  107. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  108. package/dist/TimeInput/TimeInput.js +7 -2
  109. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  110. package/dist/Timestamp/Timestamp.js +14 -3
  111. package/dist/Timestamp/formatInstant.d.ts +11 -1
  112. package/dist/Timestamp/formatInstant.d.ts.map +1 -1
  113. package/dist/Timestamp/formatInstant.js +3 -1
  114. package/dist/TopNav/TopNavMegaMenu.js +1 -1
  115. package/dist/TopNav/TopNavMegaMenuItem.js +1 -1
  116. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  117. package/dist/TopNav/TopNavMenu.js +2 -2
  118. package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +1 -1
  119. package/dist/VisuallyHidden/VisuallyHidden.js +1 -2
  120. package/dist/VisuallyHidden/index.d.ts.map +1 -1
  121. package/dist/VisuallyHidden/index.js +1 -2
  122. package/dist/astryx.css +20 -17
  123. package/dist/astryx.umd.js +48 -48
  124. package/dist/astryx.umd.js.map +4 -4
  125. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  126. package/dist/hooks/useInputStatusIcon.js +17 -4
  127. package/dist/theme/defineTheme.d.ts +2 -2
  128. package/dist/theme/defineTheme.d.ts.map +1 -1
  129. package/dist/theme/defineTheme.js +2 -1
  130. package/dist/theme/index.d.ts +1 -1
  131. package/dist/theme/index.d.ts.map +1 -1
  132. package/dist/theme/index.js +1 -1
  133. package/dist/theme/tokens.stylex.d.ts +12 -0
  134. package/dist/theme/tokens.stylex.d.ts.map +1 -1
  135. package/dist/theme/tokens.stylex.js +23 -0
  136. package/dist/utils/focusOutline.stylex.d.ts +34 -19
  137. package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
  138. package/dist/utils/focusOutline.stylex.js +46 -33
  139. package/dist/utils/index.d.ts +1 -0
  140. package/dist/utils/index.d.ts.map +1 -1
  141. package/dist/utils/index.js +5 -1
  142. package/dist/utils/interactionModality.d.ts +34 -0
  143. package/dist/utils/interactionModality.d.ts.map +1 -0
  144. package/dist/utils/interactionModality.js +70 -0
  145. package/locales/en.json +44 -0
  146. package/locales/pseudo.json +33 -0
  147. package/package.json +3 -3
  148. package/src/AppShell/AppShell.doc.mjs +3 -0
  149. package/src/AspectRatio/AspectRatio.tsx +0 -2
  150. package/src/AspectRatio/index.ts +0 -2
  151. package/src/Badge/Badge.tsx +0 -2
  152. package/src/Badge/index.ts +0 -2
  153. package/src/Banner/Banner.test.tsx +58 -1
  154. package/src/Banner/Banner.tsx +24 -5
  155. package/src/Blockquote/Blockquote.tsx +0 -2
  156. package/src/Blockquote/index.ts +0 -2
  157. package/src/Button/Button.doc.mjs +2 -2
  158. package/src/Button/Button.tsx +5 -4
  159. package/src/Card/Card.tsx +0 -2
  160. package/src/Card/index.ts +0 -2
  161. package/src/Center/Center.tsx +0 -2
  162. package/src/Center/index.ts +0 -2
  163. package/src/CheckboxInput/CheckboxInput.test.tsx +8 -3
  164. package/src/Code/Code.tsx +0 -2
  165. package/src/CommandPalette/CommandPalette.test.tsx +54 -0
  166. package/src/CommandPalette/CommandPalette.tsx +5 -0
  167. package/src/ComplexSelector/ComplexSelector.test.tsx +15 -6
  168. package/src/ComplexSelector/ComplexSelector.tsx +6 -10
  169. package/src/DateInput/DateInput.test.tsx +19 -0
  170. package/src/DateInput/DateInput.tsx +3 -6
  171. package/src/DateRangeInput/DateRangeInput.tsx +3 -9
  172. package/src/DateTimeInput/DateTimeInput.test.tsx +79 -1
  173. package/src/DateTimeInput/DateTimeInput.tsx +23 -9
  174. package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +2 -1
  175. package/src/Field/PanelSearchInput.tsx +271 -0
  176. package/src/FileInput/FileInput.tsx +21 -8
  177. package/src/Grid/Grid.tsx +0 -2
  178. package/src/Grid/index.ts +0 -2
  179. package/src/Icon/globalIconRegistry.tsx +5 -4
  180. package/src/Indicator/Indicator.doc.mjs +1 -1
  181. package/src/Indicator/index.ts +1 -0
  182. package/src/Indicator/types.ts +11 -0
  183. package/src/Lightbox/Lightbox.tsx +2 -11
  184. package/src/MultiSelector/MultiSelector.doc.mjs +9 -0
  185. package/src/MultiSelector/MultiSelector.test.tsx +135 -5
  186. package/src/MultiSelector/MultiSelector.tsx +170 -127
  187. package/src/NavItem/navItemStyles.stylex.ts +31 -5
  188. package/src/Popover/Popover.doc.mjs +2 -0
  189. package/src/Popover/usePopover.tsx +37 -4
  190. package/src/ProgressBar/ProgressBar.tsx +2 -8
  191. package/src/Section/Section.tsx +0 -2
  192. package/src/Section/index.ts +0 -2
  193. package/src/Selector/Selector.doc.mjs +10 -0
  194. package/src/Selector/Selector.test.tsx +386 -2
  195. package/src/Selector/Selector.tsx +182 -122
  196. package/src/SideNav/SideNav.doc.mjs +13 -4
  197. package/src/SideNav/SideNav.test.tsx +771 -2
  198. package/src/SideNav/SideNav.tsx +37 -16
  199. package/src/SideNav/SideNavCollapseButton.doc.mjs +28 -6
  200. package/src/SideNav/SideNavCollapseButton.tsx +67 -15
  201. package/src/SideNav/SideNavCollapseContext.ts +29 -9
  202. package/src/SideNav/SideNavHeading.tsx +28 -10
  203. package/src/SideNav/SideNavItem.doc.mjs +13 -0
  204. package/src/SideNav/SideNavItem.tsx +92 -84
  205. package/src/SideNav/SideNavSection.tsx +6 -20
  206. package/src/SideNav/index.ts +2 -0
  207. package/src/Skeleton/Skeleton.tsx +0 -2
  208. package/src/Skeleton/index.ts +0 -2
  209. package/src/Slider/Slider.tsx +2 -11
  210. package/src/Switch/Switch.tsx +10 -2
  211. package/src/Table/Table.test.tsx +31 -0
  212. package/src/Table/TableRow.tsx +6 -0
  213. package/src/TimeInput/TimeInput.test.tsx +54 -0
  214. package/src/TimeInput/TimeInput.tsx +17 -2
  215. package/src/Timestamp/Timestamp.test.tsx +91 -14
  216. package/src/Timestamp/Timestamp.tsx +14 -2
  217. package/src/Timestamp/formatInstant.ts +16 -1
  218. package/src/TopNav/TopNavMegaMenu.doc.mjs +5 -0
  219. package/src/TopNav/TopNavMegaMenu.test.tsx +43 -0
  220. package/src/TopNav/TopNavMegaMenu.tsx +4 -1
  221. package/src/TopNav/TopNavMegaMenuItem.tsx +1 -1
  222. package/src/TopNav/TopNavMenu.test.tsx +41 -0
  223. package/src/TopNav/TopNavMenu.tsx +8 -2
  224. package/src/TreeList/TreeList.test.tsx +3 -2
  225. package/src/VisuallyHidden/VisuallyHidden.tsx +0 -2
  226. package/src/VisuallyHidden/index.ts +0 -2
  227. package/src/hooks/useInputStatusIcon.tsx +8 -8
  228. package/src/serverSafeComponents.test.ts +665 -0
  229. package/src/theme/defineTheme.ts +3 -0
  230. package/src/theme/generateThemeRules.test.ts +17 -0
  231. package/src/theme/index.ts +2 -0
  232. package/src/theme/tokens.stylex.ts +18 -0
  233. package/src/theme/tokens.test.ts +37 -0
  234. package/src/utils/focusOutline.stylex.ts +37 -23
  235. package/src/utils/index.ts +4 -0
  236. package/src/utils/interactionModality.ts +74 -0
@@ -36,6 +36,7 @@ import {mergeProps, mergeRefs} from '../utils';
36
36
  import {
37
37
  SideNavCollapseContext,
38
38
  type SideNavCollapseState,
39
+ type SideNavCollapsibleConfig,
39
40
  type SideNavImperativeCollapseHandle,
40
41
  } from './SideNavCollapseContext';
41
42
  import {SideNavCollapseButton} from './SideNavCollapseButton';
@@ -45,6 +46,7 @@ import {useResizable} from '../Resizable/useResizable';
45
46
  import type {ResizableConfig} from '../Resizable/useResizable';
46
47
  import {ResizeHandle} from '../Resizable/ResizeHandle';
47
48
  import {themeProps} from '../utils/themeProps';
49
+ import {SizeProvider} from '../SizeContext/SizeContext';
48
50
  import {useTranslator} from '../i18n';
49
51
 
50
52
  // =============================================================================
@@ -185,6 +187,13 @@ const styles = stylex.create({
185
187
  },
186
188
  });
187
189
 
190
+ /**
191
+ * Cascaded to the icon rows through `SizeContext` so the built-in collapse
192
+ * button and the consumer's `footerIcons` come out one height. An explicit
193
+ * `size` on a child still wins.
194
+ */
195
+ const FOOTER_ICON_SIZE = 'sm';
196
+
188
197
  // =============================================================================
189
198
  // Types
190
199
  // =============================================================================
@@ -197,6 +206,9 @@ export interface SideNavProps extends BaseProps<HTMLElement> {
197
206
  * Imperative collapse handle for SideNavCollapseButton instances rendered
198
207
  * outside this SideNav. This intentionally stays separate from `ref`, which
199
208
  * continues to expose the root HTMLElement.
209
+ *
210
+ * @deprecated Hand the same controlled `collapsible` config to SideNav and
211
+ * to the outside button instead.
200
212
  */
201
213
  handleRef?: React.Ref<SideNavImperativeCollapseHandle>;
202
214
 
@@ -270,21 +282,14 @@ export interface SideNavProps extends BaseProps<HTMLElement> {
270
282
  * - `true` — enables collapse with default toggle button and uncontrolled state
271
283
  * - Object — enables collapse with advanced configuration:
272
284
  * - `defaultIsCollapsed` — start collapsed (uncontrolled)
273
- * - `isCollapsed` + `onCollapsedChange` — controlled mode
285
+ * - `isCollapsed` + `onCollapsedChange` — controlled mode. Pass the same
286
+ * object to a `SideNavCollapseButton` rendered outside this SideNav
274
287
  * - `hasButton` — render built-in collapse button (default: true)
275
288
  * - `buttonLabel` — accessibility label for the collapse button
276
289
  *
277
290
  * @default false
278
291
  */
279
- collapsible?:
280
- | boolean
281
- | {
282
- defaultIsCollapsed?: boolean;
283
- isCollapsed?: boolean;
284
- onCollapsedChange?: (isCollapsed: boolean) => void;
285
- hasButton?: boolean;
286
- buttonLabel?: string;
287
- };
292
+ collapsible?: boolean | SideNavCollapsibleConfig;
288
293
  }
289
294
 
290
295
  // =============================================================================
@@ -375,6 +380,8 @@ export function SideNav({
375
380
  const toggle = useCallback(() => {
376
381
  const next = !collapsed;
377
382
 
383
+ // Deprecated `handleRef` path only: an out-of-tree button reads this
384
+ // snapshot while rendering, which can happen before SideNav re-renders.
378
385
  collapseStateRef.current = {
379
386
  ...collapseStateRef.current,
380
387
  isCollapsed: next,
@@ -429,7 +436,9 @@ export function SideNav({
429
436
  style,
430
437
  )}>
431
438
  {header}
432
- <div {...stylex.props(styles.topbarIcons)}>{footerIcons}</div>
439
+ <div {...stylex.props(styles.topbarIcons)}>
440
+ <SizeProvider value={FOOTER_ICON_SIZE}>{footerIcons}</SizeProvider>
441
+ </div>
433
442
  </div>
434
443
  );
435
444
  }
@@ -441,7 +450,13 @@ export function SideNav({
441
450
 
442
451
  if (renderMode === 'drawer') {
443
452
  return (
444
- <MobileNav header={header} data-testid={testId}>
453
+ <MobileNav
454
+ header={header}
455
+ data-testid={testId}
456
+ xstyle={xstyle}
457
+ className={className}
458
+ style={style}
459
+ {...props}>
445
460
  {topContent}
446
461
  {children}
447
462
  {hasDrawerFooter && (
@@ -449,7 +464,9 @@ export function SideNav({
449
464
  {footer}
450
465
  {footerIcons && (
451
466
  <div {...stylex.props(styles.drawerFooterIcons)}>
452
- {footerIcons}
467
+ <SizeProvider value={FOOTER_ICON_SIZE}>
468
+ {footerIcons}
469
+ </SizeProvider>
453
470
  </div>
454
471
  )}
455
472
  </div>
@@ -472,7 +489,9 @@ export function SideNav({
472
489
  {footer}
473
490
  {footerIcons && (
474
491
  <div {...stylex.props(styles.drawerFooterIcons)}>
475
- {footerIcons}
492
+ <SizeProvider value={FOOTER_ICON_SIZE}>
493
+ {footerIcons}
494
+ </SizeProvider>
476
495
  </div>
477
496
  )}
478
497
  </div>
@@ -544,8 +563,10 @@ export function SideNav({
544
563
  styles.footerRow,
545
564
  collapsed && styles.footerRowCollapsed,
546
565
  )}>
547
- {showCollapseButton && <SideNavCollapseButton />}
548
- {footerIcons}
566
+ <SizeProvider value={FOOTER_ICON_SIZE}>
567
+ {showCollapseButton && <SideNavCollapseButton />}
568
+ {footerIcons}
569
+ </SizeProvider>
549
570
  </div>
550
571
  </div>
551
572
  )}
@@ -7,18 +7,28 @@ export const docs = {
7
7
  subComponentOf: 'SideNav',
8
8
  displayName: 'Side Nav Collapse Button',
9
9
  isHiddenFromOverview: true,
10
- description: 'Toggle button for sidenav collapse. Place inside SideNav (reads context automatically) or outside (pass handleRef). Renders as an icon-only ghost button by default.',
10
+ description: 'Toggle button for sidenav collapse. Place inside SideNav (reads context automatically) or outside it (hand the same controlled collapsible config to both). Renders as an icon-only ghost button by default.',
11
11
  props: [
12
+ {
13
+ name: 'collapsible',
14
+ type: '{isCollapsed: boolean, onCollapsedChange: (isCollapsed: boolean) => void}',
15
+ description: 'The same controlled collapsible config passed to SideNav. Only needed when the button is rendered outside the sidenav, where collapse context cannot reach it.',
16
+ },
12
17
  {
13
18
  name: 'handleRef',
14
19
  type: 'RefObject<SideNavImperativeCollapseHandle | null>',
15
- description: 'Imperative collapse handle from SideNav. Only needed when the button is rendered outside the sidenav.',
20
+ description: 'Deprecated. Imperative collapse handle from SideNav; pass collapsible instead.',
16
21
  },
17
22
  {
18
23
  name: 'label',
19
24
  type: 'string',
20
25
  description: 'Custom button label. When provided, renders as a text button with chevron. When omitted, renders icon-only.',
21
26
  },
27
+ {
28
+ name: 'size',
29
+ type: "'sm' | 'md' | 'lg'",
30
+ description: "Button size. Defaults to the size its container cascades — 'sm' inside a SideNav footer — and to 'md' with no container. Set it when the button sits outside a sized container and has to match its neighbours.",
31
+ },
22
32
  {
23
33
  name: 'children',
24
34
  type: 'ReactNode',
@@ -31,18 +41,28 @@ export const docsZh = {
31
41
  name: 'SideNavCollapseButton',
32
42
  isHiddenFromOverview: true,
33
43
  displayName: 'Side Nav Collapse Button',
34
- description: '侧边栏折叠切换按钮。放置在 SideNav 内部(自动读取上下文)或外部(传入 handleRef)。默认渲染为仅图标的 ghost 按钮。',
44
+ description: '侧边栏折叠切换按钮。放置在 SideNav 内部(自动读取上下文)或外部(向两者传入同一个受控 collapsible 配置)。默认渲染为仅图标的 ghost 按钮。',
35
45
  props: [
46
+ {
47
+ name: 'collapsible',
48
+ type: '{isCollapsed: boolean, onCollapsedChange: (isCollapsed: boolean) => void}',
49
+ description: '与 SideNav 相同的受控 collapsible 配置。仅在按钮渲染在侧边栏外部、无法读取折叠上下文时需要。',
50
+ },
36
51
  {
37
52
  name: 'handleRef',
38
53
  type: 'RefObject<SideNavImperativeCollapseHandle | null>',
39
- description: '来自 SideNav 的命令式折叠句柄。仅在按钮渲染在侧边栏外部时需要。',
54
+ description: '已废弃。来自 SideNav 的命令式折叠句柄;请改用 collapsible。',
40
55
  },
41
56
  {
42
57
  name: 'label',
43
58
  type: 'string',
44
59
  description: '自定义按钮标签。提供时渲染为带箭头的文本按钮。省略时渲染为仅图标按钮。',
45
60
  },
61
+ {
62
+ name: 'size',
63
+ type: "'sm' | 'md' | 'lg'",
64
+ description: "按钮尺寸。默认使用容器级联的尺寸(SideNav 页脚内为 'sm'),无容器时为 'md'。当按钮位于已设定尺寸的容器之外且需要与相邻元素保持一致时显式设置。",
65
+ },
46
66
  {
47
67
  name: 'children',
48
68
  type: 'ReactNode',
@@ -55,10 +75,12 @@ export const docsDense = {
55
75
  name: 'SideNavCollapseButton',
56
76
  isHiddenFromOverview: true,
57
77
  displayName: 'Side Nav Collapse Button',
58
- description: 'Toggle button for sidenav collapse. Place inside SideNav (reads context) or outside (pass handleRef). Icon-only ghost button by default.',
78
+ description: 'Toggle button for sidenav collapse. Place inside SideNav (reads context) or outside (pass the same controlled collapsible config both get). Icon-only ghost button by default.',
59
79
  propDescriptions: {
60
- handleRef: 'Imperative collapse handle from SideNav. Only needed when button rendered outside sidenav.',
80
+ collapsible: 'Controlled collapsible config, same object SideNav gets. Only needed when button rendered outside sidenav.',
81
+ handleRef: 'Deprecated — pass collapsible instead.',
61
82
  label: 'Custom label. Text button w/ chevron when provided, icon-only when omitted.',
83
+ size: "Button size. Inherits from the container ('sm' in a SideNav footer), else 'md'.",
62
84
  children: 'Custom content. Overrides default chevron icon + label.',
63
85
  },
64
86
  };
@@ -9,7 +9,7 @@
9
9
  * @position Composable toggle button for sidenav collapse
10
10
  *
11
11
  * Place inside SideNav (reads context automatically) or outside
12
- * (pass handleRef to connect). Customizable via label/children.
12
+ * (pass the same controlled `collapsible` config both get).
13
13
  *
14
14
  * SYNC: When modified, update:
15
15
  * - /packages/core/src/SideNav/SideNav.doc.mjs
@@ -17,7 +17,7 @@
17
17
  * - /packages/cli/assets/templates/blocks/components/SideNav/ (showcase blocks)
18
18
  */
19
19
 
20
- import React, {type ReactNode} from 'react';
20
+ import React, {useCallback, type ReactNode} from 'react';
21
21
  import * as stylex from '@stylexjs/stylex';
22
22
  import {durationVars, easeVars} from '../theme/tokens.stylex';
23
23
  import {Icon} from '../Icon';
@@ -27,21 +27,34 @@ import {composeEventHandlers, rtlStyles} from '../utils';
27
27
  import {
28
28
  useSideNavCollapse,
29
29
  type SideNavCollapseState,
30
+ type SideNavControlledCollapsible,
30
31
  type SideNavImperativeCollapseHandle,
31
32
  } from './SideNavCollapseContext';
32
33
  import {useAppShellMobile} from '../AppShell/AppShellMobileContext';
33
34
  import {useTranslator} from '../i18n';
35
+ import type {ElementSize} from '../SizeContext/SizeContext';
34
36
 
35
37
  // =============================================================================
36
38
  // Styles
37
39
  // =============================================================================
38
40
 
39
41
  const styles = stylex.create({
42
+ // A flex container, so the glyph is a flex item. Left to blockify as a flex
43
+ // item of Button's icon slot, this span gets a line box and seats the
44
+ // chevron on its text baseline — 2.42px above the button's centre.
45
+ chevronMirror: {
46
+ display: 'flex',
47
+ alignItems: 'center',
48
+ justifyContent: 'center',
49
+ },
40
50
  chevron: {
41
51
  display: 'inline-flex',
42
52
  alignItems: 'center',
43
53
  transitionProperty: 'transform',
44
- transitionDuration: durationVars['--duration-fast'],
54
+ transitionDuration: {
55
+ default: durationVars['--duration-fast'],
56
+ '@media (prefers-reduced-motion: reduce)': '0s',
57
+ },
45
58
  transitionTimingFunction: easeVars['--ease-standard'],
46
59
  },
47
60
  chevronCollapsed: {
@@ -56,8 +69,16 @@ const styles = stylex.create({
56
69
  export interface SideNavCollapseButtonProps extends BaseProps<HTMLButtonElement> {
57
70
  ref?: React.Ref<HTMLButtonElement>;
58
71
  /**
59
- * Imperative handle from SideNav. Only needed when the button is rendered
60
- * outside the sidenav, where collapse context is unavailable.
72
+ * The same controlled `collapsible` config given to SideNav
73
+ * (`{isCollapsed, onCollapsedChange}`). Needed only when the button is
74
+ * rendered outside the sidenav, where collapse context cannot reach it.
75
+ */
76
+ collapsible?: SideNavControlledCollapsible;
77
+
78
+ /**
79
+ * Imperative handle from SideNav.
80
+ *
81
+ * @deprecated Pass `collapsible` instead.
61
82
  */
62
83
  handleRef?: React.RefObject<SideNavImperativeCollapseHandle | null>;
63
84
 
@@ -67,6 +88,13 @@ export interface SideNavCollapseButtonProps extends BaseProps<HTMLButtonElement>
67
88
  */
68
89
  label?: string;
69
90
 
91
+ /**
92
+ * Button size. Defaults to the size its container cascades — `sm` in a
93
+ * SideNav footer — or `md` outside one. Set it for placements with no row
94
+ * to inherit from, e.g. a button placed in a `TopNav`.
95
+ */
96
+ size?: ElementSize;
97
+
70
98
  /**
71
99
  * Custom button content. Overrides the default chevron icon and label.
72
100
  */
@@ -82,7 +110,8 @@ export interface SideNavCollapseButtonProps extends BaseProps<HTMLButtonElement>
82
110
  *
83
111
  * Place anywhere inside SideNav (header, topContent, footer, footerIcons)
84
112
  * and it reads collapse state from context automatically. For placement
85
- * outside the sidenav (e.g. in TopNav or content area), pass handleRef.
113
+ * outside the sidenav (e.g. in TopNav or content area), hold the state and
114
+ * hand the same `collapsible` config to both.
86
115
  *
87
116
  * @example
88
117
  * ```
@@ -93,22 +122,27 @@ export interface SideNavCollapseButtonProps extends BaseProps<HTMLButtonElement>
93
122
  *
94
123
  * @example
95
124
  * ```
96
- * const ref = useRef(null);
97
- * <TopNav endContent={<SideNavCollapseButton handleRef={ref} />} />
98
- * <SideNav handleRef={ref} collapsible>...</SideNav>
125
+ * const [isCollapsed, setIsCollapsed] = useState(false);
126
+ * const collapsible = {isCollapsed, onCollapsedChange: setIsCollapsed};
127
+ * <TopNav endContent={<SideNavCollapseButton collapsible={collapsible} />} />
128
+ * <SideNav collapsible={{...collapsible, hasButton: false}}>...</SideNav>
99
129
  * ```
100
130
  */
101
131
  export function SideNavCollapseButton({
102
132
  ref,
133
+ collapsible,
103
134
  handleRef,
104
135
  label,
136
+ size,
105
137
  children,
106
138
  onClick: onClickProp,
107
139
  ...props
108
140
  }: SideNavCollapseButtonProps) {
109
141
  const t = useTranslator();
110
- const {isCollapsed, toggle, isCollapsible} =
111
- useSideNavCollapseState(handleRef);
142
+ const {isCollapsed, toggle, isCollapsible} = useSideNavCollapseState(
143
+ collapsible,
144
+ handleRef,
145
+ );
112
146
  const {isMobile} = useAppShellMobile();
113
147
 
114
148
  // Hide when not collapsible, or when in mobile mode (sidenav is in
@@ -127,6 +161,7 @@ export function SideNavCollapseButton({
127
161
  : t('@astryx.sideNavCollapseButton.collapseSidebar'))
128
162
  }
129
163
  variant="ghost"
164
+ size={size}
130
165
  {...props}
131
166
  onClick={composeEventHandlers(onClickProp, toggle)}
132
167
  icon={
@@ -135,7 +170,7 @@ export function SideNavCollapseButton({
135
170
  // into) the state rotation: both are `transform`, so on a single
136
171
  // element one would overwrite the other and the chevron would stop
137
172
  // mirroring under RTL. See utils/rtlStyles.ts.
138
- <span {...stylex.props(rtlStyles.mirror)}>
173
+ <span {...stylex.props(styles.chevronMirror, rtlStyles.mirror)}>
139
174
  {/* `sm` (1rem) matches what this glyph already renders at: Button's
140
175
  icon slot pins its wrapper to 16px, and the registry SVG is
141
176
  1em, so the chevron is 16px today. */}
@@ -156,11 +191,30 @@ export function SideNavCollapseButton({
156
191
  SideNavCollapseButton.displayName = 'SideNavCollapseButton';
157
192
 
158
193
  function useSideNavCollapseState(
194
+ collapsible: SideNavControlledCollapsible | undefined,
159
195
  handleRef:
160
196
  React.RefObject<SideNavImperativeCollapseHandle | null> | undefined,
161
197
  ): SideNavCollapseState {
162
198
  const contextCollapseState = useSideNavCollapse();
163
199
 
200
+ const {isCollapsed, onCollapsedChange} = collapsible ?? {};
201
+ const toggleCollapsible = useCallback(
202
+ () => onCollapsedChange?.(!isCollapsed),
203
+ [isCollapsed, onCollapsedChange],
204
+ );
205
+
206
+ const toggleHandle = useCallback(() => {
207
+ handleRef?.current?.getCollapseState()?.toggle();
208
+ }, [handleRef]);
209
+
210
+ if (collapsible != null) {
211
+ return {
212
+ isCollapsed: isCollapsed ?? false,
213
+ toggle: toggleCollapsible,
214
+ isCollapsible: true,
215
+ };
216
+ }
217
+
164
218
  if (handleRef == null) {
165
219
  return contextCollapseState;
166
220
  }
@@ -169,9 +223,7 @@ function useSideNavCollapseState(
169
223
 
170
224
  return {
171
225
  isCollapsed: externalCollapseState?.isCollapsed ?? false,
172
- toggle: () => {
173
- handleRef.current?.getCollapseState()?.toggle();
174
- },
226
+ toggle: toggleHandle,
175
227
  isCollapsible: externalCollapseState?.isCollapsible ?? true,
176
228
  };
177
229
  }
@@ -10,8 +10,8 @@
10
10
  *
11
11
  * Provides collapse state to SideNavCollapseButton and other
12
12
  * sidenav children. Set by SideNav when isCollapsible is true.
13
- * Also provides a small imperative handle for ref-based collapse buttons
14
- * rendered outside the SideNav tree.
13
+ * A button rendered outside the SideNav tree is out of context's reach and
14
+ * takes the controlled `collapsible` config as a prop instead.
15
15
  */
16
16
 
17
17
  import {createContext, use} from 'react';
@@ -25,18 +25,38 @@ export interface SideNavCollapseState {
25
25
  isCollapsible: boolean;
26
26
  }
27
27
 
28
+ /** Object form of SideNav's `collapsible` prop. */
29
+ export interface SideNavCollapsibleConfig {
30
+ defaultIsCollapsed?: boolean;
31
+ isCollapsed?: boolean;
32
+ onCollapsedChange?: (isCollapsed: boolean) => void;
33
+ hasButton?: boolean;
34
+ buttonLabel?: string;
35
+ }
36
+
37
+ /**
38
+ * The controlled form: the consumer holds the state, so it can be handed to
39
+ * both SideNav and a SideNavCollapseButton rendered outside it.
40
+ */
41
+ export interface SideNavControlledCollapsible extends SideNavCollapsibleConfig {
42
+ isCollapsed: boolean;
43
+ onCollapsedChange: (isCollapsed: boolean) => void;
44
+ }
28
45
 
46
+ /**
47
+ * @deprecated Pass the same controlled `collapsible` config to SideNav and to
48
+ * the out-of-tree SideNavCollapseButton instead. The state then reaches the
49
+ * button through props rather than through a ref.
50
+ */
29
51
  export interface SideNavImperativeCollapseHandle {
30
52
  getCollapseState: () => SideNavCollapseState | null;
31
53
  }
32
54
 
33
- export const SideNavCollapseContext = createContext<SideNavCollapseState>(
34
- {
35
- isCollapsed: false,
36
- toggle: () => {},
37
- isCollapsible: false,
38
- },
39
- );
55
+ export const SideNavCollapseContext = createContext<SideNavCollapseState>({
56
+ isCollapsed: false,
57
+ toggle: () => {},
58
+ isCollapsible: false,
59
+ });
40
60
  SideNavCollapseContext.displayName = 'SideNavCollapseContext';
41
61
 
42
62
  /**
@@ -36,6 +36,7 @@ import {Link} from '../Link';
36
36
  import {Icon} from '../Icon';
37
37
  import {Tooltip} from '../Tooltip';
38
38
  import {navItemStyles} from '../NavItem/navItemStyles.stylex';
39
+ import {focusOutlineProps} from '../utils/focusOutline.stylex';
39
40
  import {useSideNavCollapse} from './SideNavCollapseContext';
40
41
  import {useLinkComponent} from '../Link/useLinkComponent';
41
42
  import type {LinkComponentType} from '../Link/types';
@@ -415,7 +416,11 @@ export function SideNavHeading({
415
416
  data-testid={testId}
416
417
  {...mergeProps(
417
418
  themeProps('side-nav-heading'),
418
- stylex.props(navItemStyles.item, styles.rootCollapsed, xstyle),
419
+ focusOutlineProps.focusVisible(
420
+ navItemStyles.item,
421
+ styles.rootCollapsed,
422
+ xstyle,
423
+ ),
419
424
  className,
420
425
  style,
421
426
  )}>
@@ -434,7 +439,7 @@ export function SideNavHeading({
434
439
  {...triggerProps}
435
440
  {...mergeProps(
436
441
  themeProps('side-nav-heading'),
437
- stylex.props(
442
+ focusOutlineProps.focusVisible(
438
443
  navItemStyles.item,
439
444
  styles.rootCollapsed,
440
445
  styles.menuTrigger,
@@ -452,7 +457,7 @@ export function SideNavHeading({
452
457
  {...contentProps}>
453
458
  <button
454
459
  type="button"
455
- {...stylex.props(styles.popoverHeading)}
460
+ {...focusOutlineProps.focusVisible(styles.popoverHeading)}
456
461
  onClick={triggerProps.onClick}>
457
462
  {icon && <span {...stylex.props(styles.icon)}>{icon}</span>}
458
463
  <span {...stylex.props(styles.textContainer)}>
@@ -549,7 +554,10 @@ export function SideNavHeading({
549
554
  {hasAnyHref && headingHref && menu ? (
550
555
  <LinkComponent
551
556
  href={headingHref}
552
- {...stylex.props(styles.heading, styles.headingLink)}>
557
+ {...focusOutlineProps.focusVisible(
558
+ styles.heading,
559
+ styles.headingLink,
560
+ )}>
553
561
  {heading}
554
562
  </LinkComponent>
555
563
  ) : (
@@ -586,7 +594,7 @@ export function SideNavHeading({
586
594
  const popoverHeadingContent = (
587
595
  <button
588
596
  type="button"
589
- {...stylex.props(styles.popoverHeading)}
597
+ {...focusOutlineProps.focusVisible(styles.popoverHeading)}
590
598
  onClick={triggerProps.onClick}>
591
599
  {icon && <span {...stylex.props(styles.icon)}>{icon}</span>}
592
600
  {renderTextContent(
@@ -609,7 +617,11 @@ export function SideNavHeading({
609
617
  data-testid={testId}
610
618
  {...mergeProps(
611
619
  themeProps('side-nav-heading'),
612
- stylex.props(styles.root, styles.menuTrigger, xstyle),
620
+ focusOutlineProps.focusVisible(
621
+ styles.root,
622
+ styles.menuTrigger,
623
+ xstyle,
624
+ ),
613
625
  className,
614
626
  style,
615
627
  )}
@@ -646,7 +658,10 @@ export function SideNavHeading({
646
658
  triggerProps.onClick();
647
659
  }}
648
660
  {...popover.triggerProps}
649
- {...stylex.props(styles.chevron, styles.interactive)}>
661
+ {...focusOutlineProps.focusVisible(
662
+ styles.chevron,
663
+ styles.interactive,
664
+ )}>
650
665
  <Icon
651
666
  icon="chevronDown"
652
667
  size="sm"
@@ -701,7 +716,7 @@ export function SideNavHeading({
701
716
  <LinkComponent
702
717
  href={headingHref}
703
718
  aria-label={heading}
704
- {...stylex.props(styles.icon)}>
719
+ {...focusOutlineProps.focusVisible(styles.icon)}>
705
720
  {icon}
706
721
  </LinkComponent>
707
722
  ) : (
@@ -717,7 +732,10 @@ export function SideNavHeading({
717
732
  triggerProps.onClick();
718
733
  }}
719
734
  {...popover.triggerProps}
720
- {...stylex.props(styles.chevron, styles.interactive)}>
735
+ {...focusOutlineProps.focusVisible(
736
+ styles.chevron,
737
+ styles.interactive,
738
+ )}>
721
739
  <Icon
722
740
  icon="chevronDown"
723
741
  size="sm"
@@ -770,7 +788,7 @@ export function SideNavHeading({
770
788
  <LinkComponent
771
789
  href={headingHref}
772
790
  aria-label={heading}
773
- {...stylex.props(styles.icon)}>
791
+ {...focusOutlineProps.focusVisible(styles.icon)}>
774
792
  {icon}
775
793
  </LinkComponent>
776
794
  ) : (
@@ -91,6 +91,12 @@ export const docs = {
91
91
  description: 'Enables collapse behavior for items with children. Pass true for uncontrolled (starts expanded), or an object for controlled mode.',
92
92
  default: 'false',
93
93
  },
94
+ {
95
+ name: 'size',
96
+ type: "'sm' | 'md' | 'lg'",
97
+ description: 'Size variant for the nav item row.',
98
+ default: "'md'",
99
+ },
94
100
  ],
95
101
  };
96
102
 
@@ -159,6 +165,12 @@ export const docsZh = {
159
165
  description: '启用带子项的折叠行为。传 true 为非受控模式(默认展开),或传对象用于受控模式。',
160
166
  default: 'false',
161
167
  },
168
+ {
169
+ name: 'size',
170
+ type: "'sm' | 'md' | 'lg'",
171
+ description: '导航项的尺寸变体。',
172
+ default: "'md'",
173
+ },
162
174
  ],
163
175
  };
164
176
 
@@ -179,5 +191,6 @@ export const docsDense = {
179
191
  endContent: 'Right-side content such as badges or counts.',
180
192
  children: 'Sub-items for nesting.',
181
193
  collapsible: 'Enables collapse for items w/ children. true=uncontrolled, object=controlled mode.',
194
+ size: "Row size variant: 'sm' | 'md' | 'lg'.",
182
195
  },
183
196
  };