banhaten 0.5.0 → 0.6.0

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 (290) hide show
  1. package/CHANGELOG.md +177 -139
  2. package/CONTRIBUTING.md +46 -46
  3. package/README.md +192 -192
  4. package/SECURITY.md +15 -15
  5. package/banhaten.config.example.json +11 -11
  6. package/docs/design-spacing.md +13 -13
  7. package/docs/design-system/README.md +22 -22
  8. package/docs/design-system/adoption-quality-remediation.md +87 -87
  9. package/docs/design-system/agent-usage.md +13 -13
  10. package/docs/design-system/appearance-presets.md +189 -189
  11. package/docs/design-system/appearances/default.md +94 -94
  12. package/docs/design-system/appearances/rounded.md +95 -95
  13. package/docs/design-system/appearances/sharp.md +95 -95
  14. package/docs/design-system/component-docs-pattern.md +81 -81
  15. package/docs/design-system/component-implementation-ownership-audit.md +245 -245
  16. package/docs/design-system/component-quality-audit-2026-08-20.md +225 -225
  17. package/docs/design-system/component-showcase-consistency-report.md +219 -219
  18. package/docs/design-system/component-token-consistency-audit.md +173 -173
  19. package/docs/design-system/components/README.md +91 -90
  20. package/docs/design-system/components/accordion.md +52 -52
  21. package/docs/design-system/components/activity-feed.md +93 -93
  22. package/docs/design-system/components/activity-heatmap.md +56 -56
  23. package/docs/design-system/components/alert-dialog.md +71 -71
  24. package/docs/design-system/components/alert.md +103 -97
  25. package/docs/design-system/components/app-header.md +97 -0
  26. package/docs/design-system/components/aspect-ratio.md +45 -45
  27. package/docs/design-system/components/attribute.md +101 -89
  28. package/docs/design-system/components/autocomplete.md +75 -75
  29. package/docs/design-system/components/avatar.md +53 -53
  30. package/docs/design-system/components/badge.md +55 -55
  31. package/docs/design-system/components/banner.md +86 -86
  32. package/docs/design-system/components/breadcrumbs.md +181 -177
  33. package/docs/design-system/components/button-group.md +97 -97
  34. package/docs/design-system/components/button.md +80 -80
  35. package/docs/design-system/components/card.md +102 -88
  36. package/docs/design-system/components/carousel.md +46 -46
  37. package/docs/design-system/components/catalog-components.md +46 -46
  38. package/docs/design-system/components/checkbox.md +53 -53
  39. package/docs/design-system/components/collapsible.md +63 -63
  40. package/docs/design-system/components/command.md +61 -61
  41. package/docs/design-system/components/context-menu.md +45 -45
  42. package/docs/design-system/components/date-picker.md +80 -80
  43. package/docs/design-system/components/divider.md +102 -102
  44. package/docs/design-system/components/empty-state.md +56 -56
  45. package/docs/design-system/components/field.md +70 -70
  46. package/docs/design-system/components/file-upload.md +186 -186
  47. package/docs/design-system/components/hover-card.md +47 -47
  48. package/docs/design-system/components/icons.md +48 -48
  49. package/docs/design-system/components/input-group.md +57 -57
  50. package/docs/design-system/components/input-otp.md +56 -56
  51. package/docs/design-system/components/input.md +111 -109
  52. package/docs/design-system/components/item.md +58 -58
  53. package/docs/design-system/components/kbd.md +49 -49
  54. package/docs/design-system/components/label.md +49 -49
  55. package/docs/design-system/components/menu.md +94 -73
  56. package/docs/design-system/components/menubar.md +46 -46
  57. package/docs/design-system/components/modal.md +104 -104
  58. package/docs/design-system/components/native-select.md +53 -53
  59. package/docs/design-system/components/navigation-menu.md +49 -49
  60. package/docs/design-system/components/onboarding-step-list-item.md +81 -81
  61. package/docs/design-system/components/page-header.md +127 -93
  62. package/docs/design-system/components/pagination.md +56 -56
  63. package/docs/design-system/components/popover.md +59 -59
  64. package/docs/design-system/components/progress-slider.md +49 -49
  65. package/docs/design-system/components/progress.md +76 -76
  66. package/docs/design-system/components/radio-card.md +50 -50
  67. package/docs/design-system/components/radio-group.md +56 -56
  68. package/docs/design-system/components/resizable.md +43 -43
  69. package/docs/design-system/components/scroll-area.md +50 -50
  70. package/docs/design-system/components/segmented-control.md +54 -54
  71. package/docs/design-system/components/select.md +68 -64
  72. package/docs/design-system/components/sheet.md +66 -66
  73. package/docs/design-system/components/sidebar.md +167 -111
  74. package/docs/design-system/components/skeleton.md +74 -74
  75. package/docs/design-system/components/slideout.md +64 -64
  76. package/docs/design-system/components/slider.md +62 -62
  77. package/docs/design-system/components/social-button.md +49 -49
  78. package/docs/design-system/components/spinner.md +62 -62
  79. package/docs/design-system/components/steps.md +65 -65
  80. package/docs/design-system/components/table.md +499 -422
  81. package/docs/design-system/components/tabs.md +74 -59
  82. package/docs/design-system/components/tag.md +79 -79
  83. package/docs/design-system/components/textarea.md +49 -49
  84. package/docs/design-system/components/timeline.md +82 -82
  85. package/docs/design-system/components/toast.md +57 -57
  86. package/docs/design-system/components/toggle.md +80 -80
  87. package/docs/design-system/components/toolbar.md +89 -89
  88. package/docs/design-system/components/tooltip.md +92 -92
  89. package/docs/design-system/components/typography.md +18 -18
  90. package/docs/design-system/density-and-elevation.md +65 -65
  91. package/docs/design-system/design-system-test-missing-items.md +368 -368
  92. package/docs/design-system/direction-and-localization.md +23 -23
  93. package/docs/design-system/form-recipes.md +73 -73
  94. package/docs/design-system/gap-promotion-policy.md +21 -21
  95. package/docs/design-system/icons.md +74 -74
  96. package/docs/design-system/installation.md +70 -70
  97. package/docs/design-system/launch-comparison-shadcn.md +98 -98
  98. package/docs/design-system/registry-and-cli.md +64 -64
  99. package/docs/design-system/release-policy.md +30 -30
  100. package/docs/design-system/roadmap.md +14 -14
  101. package/docs/design-system/support-matrix.md +23 -23
  102. package/docs/design-system/tabs.md +54 -54
  103. package/docs/design-system/template-gap-audit.md +32 -32
  104. package/docs/design-system/templates.md +59 -59
  105. package/docs/design-system/token-governance.md +72 -72
  106. package/package.json +98 -98
  107. package/registry/component-contract.json +40 -40
  108. package/registry/components/accordion.tsx +155 -155
  109. package/registry/components/alert-dialog.tsx +297 -297
  110. package/registry/components/alert.tsx +309 -293
  111. package/registry/components/app-header.tsx +151 -0
  112. package/registry/components/aspect-ratio.tsx +30 -30
  113. package/registry/components/attribute.tsx +489 -454
  114. package/registry/components/autocomplete.tsx +598 -598
  115. package/registry/components/avatar.tsx +385 -385
  116. package/registry/components/badge.tsx +630 -629
  117. package/registry/components/button-group.tsx +565 -565
  118. package/registry/components/button.tsx +128 -127
  119. package/registry/components/card.tsx +651 -638
  120. package/registry/components/carousel.tsx +296 -296
  121. package/registry/components/chart.tsx +203 -203
  122. package/registry/components/checkbox.tsx +247 -247
  123. package/registry/components/collapsible.tsx +109 -109
  124. package/registry/components/command.tsx +208 -208
  125. package/registry/components/context-menu.tsx +236 -236
  126. package/registry/components/date-picker.tsx +1289 -1289
  127. package/registry/components/expanded/ActivityFeed.tsx +227 -227
  128. package/registry/components/expanded/ActivityHeatmap.tsx +64 -64
  129. package/registry/components/expanded/Banner.tsx +179 -179
  130. package/registry/components/expanded/Breadcrumbs.tsx +135 -131
  131. package/registry/components/expanded/CatalogComponentsShowcase.tsx +205 -205
  132. package/registry/components/expanded/CatalogDivider.tsx +49 -49
  133. package/registry/components/expanded/EmptyState.tsx +155 -155
  134. package/registry/components/expanded/FileUpload.tsx +543 -543
  135. package/registry/components/expanded/OnboardingStepListItem.tsx +60 -60
  136. package/registry/components/expanded/PageHeader.tsx +209 -179
  137. package/registry/components/expanded/Slideout.tsx +108 -108
  138. package/registry/components/expanded/Steps.tsx +402 -402
  139. package/registry/components/expanded/Table.tsx +1109 -1092
  140. package/registry/components/expanded/Timeline.tsx +203 -203
  141. package/registry/components/expanded/activityFeed.css +248 -248
  142. package/registry/components/expanded/activityHeatmap.css +66 -66
  143. package/registry/components/expanded/banner.css +260 -260
  144. package/registry/components/expanded/breadcrumbs.css +157 -140
  145. package/registry/components/expanded/catalogComponentsShowcase.css +87 -87
  146. package/registry/components/expanded/divider.css +75 -75
  147. package/registry/components/expanded/emptyState.css +111 -111
  148. package/registry/components/expanded/fileUpload.css +457 -457
  149. package/registry/components/expanded/onboardingStepListItem.css +135 -135
  150. package/registry/components/expanded/pageHeader.css +284 -248
  151. package/registry/components/expanded/slideout.css +276 -276
  152. package/registry/components/expanded/steps.css +356 -356
  153. package/registry/components/expanded/table.css +655 -606
  154. package/registry/components/expanded/timeline.css +286 -286
  155. package/registry/components/field.tsx +283 -283
  156. package/registry/components/hover-card.tsx +48 -48
  157. package/registry/components/input-content.ts +107 -107
  158. package/registry/components/input-group.tsx +130 -130
  159. package/registry/components/input-otp.tsx +584 -584
  160. package/registry/components/input.tsx +992 -990
  161. package/registry/components/item.tsx +189 -189
  162. package/registry/components/kbd.tsx +246 -246
  163. package/registry/components/label.tsx +78 -78
  164. package/registry/components/menu.tsx +1091 -978
  165. package/registry/components/menubar.tsx +226 -226
  166. package/registry/components/modal.tsx +872 -872
  167. package/registry/components/native-select.tsx +205 -205
  168. package/registry/components/navigation-menu.tsx +171 -171
  169. package/registry/components/pagination.css +36 -36
  170. package/registry/components/pagination.tsx +560 -560
  171. package/registry/components/popover.tsx +338 -338
  172. package/registry/components/progress-slider.tsx +216 -216
  173. package/registry/components/radio-card.tsx +676 -676
  174. package/registry/components/radio-group.tsx +570 -570
  175. package/registry/components/resizable.tsx +108 -108
  176. package/registry/components/scroll-area.tsx +91 -91
  177. package/registry/components/segmented-control.tsx +87 -87
  178. package/registry/components/select-content.tsx +218 -218
  179. package/registry/components/select.tsx +937 -937
  180. package/registry/components/sheet.tsx +305 -305
  181. package/registry/components/sidebar.tsx +1535 -1433
  182. package/registry/components/skeleton.css +54 -54
  183. package/registry/components/skeleton.tsx +482 -482
  184. package/registry/components/slider.tsx +380 -380
  185. package/registry/components/social-button.tsx +387 -387
  186. package/registry/components/spinner.tsx +78 -78
  187. package/registry/components/table-elements.tsx +287 -128
  188. package/registry/components/tabs.css +246 -222
  189. package/registry/components/tabs.tsx +245 -241
  190. package/registry/components/tag.tsx +423 -423
  191. package/registry/components/textarea.tsx +610 -610
  192. package/registry/components/toast.tsx +136 -136
  193. package/registry/components/toggle.tsx +256 -256
  194. package/registry/components/toolbar.tsx +539 -539
  195. package/registry/components/tooltip.tsx +433 -433
  196. package/registry/examples/alert-demo.tsx +34 -34
  197. package/registry/examples/alert-dialog-demo.tsx +42 -42
  198. package/registry/examples/app-header-demo.tsx +40 -0
  199. package/registry/examples/aspect-ratio-demo.tsx +11 -11
  200. package/registry/examples/attribute-demo.tsx +65 -65
  201. package/registry/examples/autocomplete-demo.tsx +109 -109
  202. package/registry/examples/avatar-demo.tsx +132 -132
  203. package/registry/examples/badge-demo.tsx +69 -69
  204. package/registry/examples/button-demo.tsx +86 -86
  205. package/registry/examples/button-group-demo.tsx +63 -63
  206. package/registry/examples/card-demo.tsx +86 -86
  207. package/registry/examples/carousel-demo.tsx +43 -43
  208. package/registry/examples/chart-demo.tsx +18 -18
  209. package/registry/examples/checkbox-demo.tsx +62 -62
  210. package/registry/examples/collapsible-demo.tsx +16 -16
  211. package/registry/examples/collection-filter-bar-demo.tsx +135 -135
  212. package/registry/examples/command-demo.tsx +42 -42
  213. package/registry/examples/command-palette-demo.tsx +235 -235
  214. package/registry/examples/context-menu-demo.tsx +29 -29
  215. package/registry/examples/control-density-contract-demo.tsx +65 -65
  216. package/registry/examples/date-picker-demo.tsx +127 -127
  217. package/registry/examples/divider-demo.tsx +17 -17
  218. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -29
  219. package/registry/examples/expanded/banner-demo.tsx +48 -48
  220. package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -59
  221. package/registry/examples/expanded/empty-state-demo.tsx +39 -39
  222. package/registry/examples/expanded/file-upload-demo.tsx +60 -60
  223. package/registry/examples/expanded/onboarding-step-demo.tsx +11 -11
  224. package/registry/examples/expanded/slideout-demo.tsx +15 -15
  225. package/registry/examples/expanded/steps-demo.tsx +29 -29
  226. package/registry/examples/expanded/tabs-demo.tsx +13 -13
  227. package/registry/examples/expanded/timeline-demo.tsx +18 -18
  228. package/registry/examples/field-demo.tsx +51 -51
  229. package/registry/examples/hover-card-demo.tsx +23 -23
  230. package/registry/examples/input-actions-demo.tsx +36 -36
  231. package/registry/examples/input-demo.tsx +87 -87
  232. package/registry/examples/input-group-demo.tsx +16 -16
  233. package/registry/examples/input-otp-demo.tsx +72 -72
  234. package/registry/examples/item-demo.tsx +46 -46
  235. package/registry/examples/kbd-demo.tsx +12 -12
  236. package/registry/examples/label-demo.tsx +20 -20
  237. package/registry/examples/menu-demo.tsx +122 -122
  238. package/registry/examples/menubar-demo.tsx +34 -34
  239. package/registry/examples/modal-demo.tsx +74 -74
  240. package/registry/examples/native-select-demo.tsx +16 -16
  241. package/registry/examples/navigation-menu-demo.tsx +29 -29
  242. package/registry/examples/pagination-demo.tsx +20 -20
  243. package/registry/examples/popover-demo.tsx +547 -547
  244. package/registry/examples/progress-demo.tsx +37 -37
  245. package/registry/examples/progress-slider-demo.tsx +29 -29
  246. package/registry/examples/radio-card-demo.tsx +51 -51
  247. package/registry/examples/radio-group-demo.tsx +62 -62
  248. package/registry/examples/resizable-demo.tsx +22 -22
  249. package/registry/examples/results-toolbar-demo.tsx +44 -44
  250. package/registry/examples/scroll-area-demo.tsx +15 -15
  251. package/registry/examples/segmented-control-demo.tsx +39 -39
  252. package/registry/examples/select-demo.tsx +87 -87
  253. package/registry/examples/sheet-demo.tsx +47 -47
  254. package/registry/examples/sidebar-demo.tsx +101 -101
  255. package/registry/examples/slider-demo.tsx +31 -31
  256. package/registry/examples/social-button-demo.tsx +61 -61
  257. package/registry/examples/spinner-demo.tsx +59 -59
  258. package/registry/examples/tabs-demo.tsx +13 -13
  259. package/registry/examples/tag-demo.tsx +29 -29
  260. package/registry/examples/textarea-demo.tsx +79 -79
  261. package/registry/examples/toast-demo.tsx +35 -35
  262. package/registry/examples/toggle-demo.tsx +59 -59
  263. package/registry/examples/tooltip-demo.tsx +115 -115
  264. package/registry/guidance/USAGE.md +27 -27
  265. package/registry/hooks/use-direction.ts +207 -207
  266. package/registry/index.json +387 -17
  267. package/registry/styles/globals.css +46 -0
  268. package/registry.json +24 -2
  269. package/schema/config.schema.json +51 -51
  270. package/schema/registry.schema.json +194 -194
  271. package/schema/tokens.schema.json +167 -167
  272. package/src/cli/command-line.js +194 -194
  273. package/src/cli/config-editors.js +9 -9
  274. package/src/cli/doctor.js +399 -399
  275. package/src/cli/file-transaction.js +302 -302
  276. package/src/cli/global-css.js +153 -153
  277. package/src/cli/index.js +1002 -1002
  278. package/src/cli/install-plan.js +592 -592
  279. package/src/cli/install-receipt.js +171 -171
  280. package/src/cli/jsonc.js +203 -203
  281. package/src/cli/package-install-state.js +254 -254
  282. package/src/cli/package-manager.js +238 -238
  283. package/src/cli/package-plan.js +185 -185
  284. package/src/cli/package-version.js +97 -97
  285. package/src/cli/registry-query.js +207 -207
  286. package/src/cli/reporting.js +55 -55
  287. package/src/cli/schema-validation.js +19 -19
  288. package/src/cli/source-scanner.js +686 -686
  289. package/src/cli/vite-config.js +478 -478
  290. package/tokens/banhaten.tokens.json +184 -0
@@ -1,978 +1,1091 @@
1
- import * as React from "react"
2
- import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
3
- import { Slot } from "@radix-ui/react-slot"
4
- import { cva, type VariantProps } from "class-variance-authority"
5
-
6
- import { cn } from "@/lib/utils"
7
- import { useResolvedDirection } from "@/hooks/use-direction"
8
-
9
- type MenuWidth = "menu" | "default" | "multiline" | "action" | "auto"
10
- type MenuItemKind = "default" | "multiline" | "action" | "progress" | "button"
11
- type MenuItemState = "default" | "disabled"
12
- type MenuItemIconPosition = "leading" | "trailing"
13
- type MenuItemAvatarSize = "sm" | "lg"
14
- type MenuCollisionPadding = NonNullable<
15
- React.ComponentProps<typeof DropdownMenuPrimitive.Content>["collisionPadding"]
16
- >
17
-
18
- // Default Radix collisionPadding mirrors --bh-space-md-8.
19
- const MENU_COLLISION_PADDING_PX = 8
20
- // Radix sideOffset is numeric; this mirrors --bh-space-xs-4.
21
- const MENU_SUBCONTENT_SIDE_OFFSET_PX = 4
22
-
23
- const menuVariants = cva(
24
- "flex flex-col items-start gap-[var(--bh-menu-gap)] overflow-hidden rounded-[var(--bh-menu-radius)] bg-[var(--bh-menu-bg)] py-[var(--bh-menu-padding-y)] shadow-[var(--elevation-menu)]",
25
- {
26
- variants: {
27
- width: {
28
- menu: "w-[var(--bh-menu-width)]",
29
- default: "w-[var(--bh-menu-item-default-width)]",
30
- multiline: "w-[var(--bh-menu-item-multiline-width)]",
31
- action: "w-[var(--bh-menu-item-action-width)]",
32
- auto: "w-max",
33
- },
34
- },
35
- defaultVariants: {
36
- width: "menu",
37
- },
38
- }
39
- )
40
-
41
- const menuItemVariants = cva(
42
- "group/menu-item relative flex w-full items-center bg-[var(--bh-menu-item-bg)] outline-none transition-colors data-[disabled=true]:pointer-events-none",
43
- {
44
- variants: {
45
- kind: {
46
- default:
47
- "h-[var(--bh-menu-item-default-height)] px-[var(--bh-menu-item-padding-x)]",
48
- multiline:
49
- "min-h-[var(--bh-menu-item-multiline-height)] px-[var(--bh-menu-item-padding-x)]",
50
- action:
51
- "min-h-[var(--bh-menu-item-action-height)] px-[var(--bh-menu-item-padding-x)]",
52
- progress:
53
- "h-[var(--bh-menu-item-progress-height)] px-[var(--bh-menu-item-padding-x)]",
54
- button:
55
- "h-[var(--bh-menu-item-button-height)] px-[var(--bh-menu-item-padding-x)]",
56
- },
57
- },
58
- defaultVariants: {
59
- kind: "default",
60
- },
61
- }
62
- )
63
-
64
- const menuItemContentVariants = cva(
65
- "flex min-w-0 flex-1 transition-colors group-data-[disabled=true]/menu-item:bg-transparent [&_svg]:shrink-0 rtl:[&_svg[data-rtl-flip='true']]:-scale-x-100",
66
- {
67
- variants: {
68
- kind: {
69
- default:
70
- "h-[var(--bh-menu-item-default-height)] items-center gap-[var(--bh-menu-item-gap)] rounded-[var(--bh-menu-item-radius)] p-[var(--bh-menu-item-content-padding)] group-hover/menu-item:bg-[var(--bh-menu-item-hover-bg)]",
71
- multiline:
72
- "min-h-[var(--bh-menu-item-multiline-height)] items-center gap-[var(--bh-menu-item-gap)] rounded-[var(--bh-menu-item-radius)] px-[var(--bh-menu-item-content-padding)] py-[var(--bh-menu-item-content-compact-padding-y)] group-hover/menu-item:bg-[var(--bh-menu-item-hover-bg)]",
73
- action:
74
- "min-h-[var(--bh-menu-item-action-height)] items-center gap-[var(--bh-menu-item-gap)] rounded-[var(--bh-menu-item-radius)] px-[var(--bh-menu-item-content-padding)] py-[var(--bh-menu-item-content-compact-padding-y)] group-hover/menu-item:bg-[var(--bh-menu-item-hover-bg)]",
75
- progress:
76
- "h-[var(--bh-menu-item-progress-height)] flex-col items-start gap-0 px-[var(--bh-menu-item-content-padding)] py-[var(--bh-menu-item-content-compact-padding-y)]",
77
- button:
78
- "h-[var(--bh-menu-item-button-height)] flex-col items-start gap-[var(--bh-menu-item-slot-gap)] px-[var(--bh-menu-item-content-padding)] py-[var(--bh-menu-item-content-compact-padding-y)]",
79
- },
80
- },
81
- defaultVariants: {
82
- kind: "default",
83
- },
84
- }
85
- )
86
-
87
- const menuItemIconVariants = cva(
88
- "flex shrink-0 items-center justify-center text-[var(--bh-content-subtle)] group-data-[disabled=true]/menu-item:text-[var(--bh-content-disabled)]",
89
- {
90
- variants: {
91
- position: {
92
- leading:
93
- "h-[var(--bh-menu-leading-icon-slot-size)] ps-[var(--bh-menu-icon-padding)] [&_svg]:size-[var(--bh-menu-leading-icon-size)]",
94
- trailing:
95
- "size-[var(--bh-menu-trailing-icon-size)] text-[var(--bh-content-muted)] [&_svg]:size-[var(--bh-menu-trailing-icon-size)]",
96
- },
97
- },
98
- defaultVariants: {
99
- position: "leading",
100
- },
101
- }
102
- )
103
-
104
- type MenuProps = React.ComponentProps<typeof DropdownMenuPrimitive.Root>
105
-
106
- type MenuContentProps = React.ComponentProps<typeof DropdownMenuPrimitive.Content> &
107
- VariantProps<typeof menuVariants> & {
108
- asChild?: boolean
109
- }
110
-
111
- type MenuSubContentProps = React.ComponentProps<typeof DropdownMenuPrimitive.SubContent> &
112
- VariantProps<typeof menuVariants> & {
113
- asChild?: boolean
114
- }
115
-
116
- const MenuRoot = DropdownMenuPrimitive.Root
117
- const MenuSub = DropdownMenuPrimitive.Sub
118
- const MenuTrigger = DropdownMenuPrimitive.Trigger
119
- const MenuStaticContext = React.createContext(false)
120
-
121
- function Menu({ dir, ...props }: MenuProps) {
122
- const directionBoundaryRef = React.useRef<HTMLSpanElement | null>(null)
123
- const resolvedDirection = useResolvedDirection(dir, directionBoundaryRef)
124
-
125
- return (
126
- <span className="contents" data-slot="menu-root-boundary" ref={directionBoundaryRef}>
127
- <MenuRoot dir={resolvedDirection} {...props} />
128
- </span>
129
- )
130
- }
131
-
132
- type MenuSurfaceProps = React.ComponentProps<"div"> &
133
- VariantProps<typeof menuVariants> & {
134
- asChild?: boolean
135
- }
136
-
137
- const MenuSurface = React.forwardRef<HTMLDivElement, MenuSurfaceProps>(function MenuSurface({
138
- asChild = false,
139
- children,
140
- className,
141
- role = "menu",
142
- width,
143
- ...props
144
- }, ref) {
145
- const Comp = asChild ? Slot : "div"
146
-
147
- return (
148
- <Comp
149
- data-slot="menu"
150
- data-width={width ?? "menu"}
151
- ref={ref}
152
- role={role}
153
- className={cn(
154
- menuVariants({ width }),
155
- "max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-y-auto",
156
- className
157
- )}
158
- {...props}
159
- >
160
- {children}
161
- </Comp>
162
- )
163
- })
164
-
165
- const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>(function MenuContent({
166
- children,
167
- className,
168
- collisionPadding = MENU_COLLISION_PADDING_PX,
169
- width,
170
- asChild = false,
171
- role = "menu",
172
- ...props
173
- }, ref) {
174
- return (
175
- <DropdownMenuPrimitive.Content
176
- asChild
177
- collisionPadding={collisionPadding}
178
- {...props}
179
- >
180
- <MenuSurface
181
- asChild={asChild}
182
- className={className}
183
- ref={ref}
184
- role={role}
185
- width={width}
186
- >
187
- {children}
188
- </MenuSurface>
189
- </DropdownMenuPrimitive.Content>
190
- )
191
- })
192
-
193
- const MenuSubContent = React.forwardRef<HTMLDivElement, MenuSubContentProps>(function MenuSubContent({
194
- children,
195
- className,
196
- collisionPadding = MENU_COLLISION_PADDING_PX,
197
- sideOffset = MENU_SUBCONTENT_SIDE_OFFSET_PX,
198
- width,
199
- asChild = false,
200
- role = "menu",
201
- ...props
202
- }, ref) {
203
- return (
204
- <DropdownMenuPrimitive.SubContent
205
- asChild
206
- collisionPadding={collisionPadding}
207
- sideOffset={sideOffset}
208
- {...props}
209
- >
210
- <MenuSurface
211
- asChild={asChild}
212
- className={className}
213
- ref={ref}
214
- role={role}
215
- width={width}
216
- >
217
- {children}
218
- </MenuSurface>
219
- </DropdownMenuPrimitive.SubContent>
220
- )
221
- })
222
-
223
- type MenuPreviewProps = React.ComponentProps<"div"> &
224
- VariantProps<typeof menuVariants> & {
225
- asChild?: boolean
226
- }
227
-
228
- function MenuPreview({
229
- children,
230
- className,
231
- role = "menu",
232
- width,
233
- asChild = false,
234
- ...props
235
- }: MenuPreviewProps) {
236
- return (
237
- <MenuStaticContext.Provider value>
238
- <MenuSurface
239
- asChild={asChild}
240
- className={className}
241
- role={role}
242
- width={width}
243
- {...props}
244
- >
245
- {children}
246
- </MenuSurface>
247
- </MenuStaticContext.Provider>
248
- )
249
- }
250
-
251
- const MenuPortal = DropdownMenuPrimitive.Portal
252
-
253
- function getMenuItemTextValue(children: React.ReactNode) {
254
- if (typeof children === "string" || typeof children === "number") {
255
- return String(children)
256
- }
257
-
258
- return undefined
259
- }
260
-
261
- type MenuItemProps =
262
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> &
263
- VariantProps<typeof menuItemVariants>
264
-
265
- type MenuSubTriggerPrimitiveProps = Pick<
266
- React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger>,
267
- "textValue"
268
- >
269
-
270
- type MenuSubTriggerProps = React.ComponentProps<"div"> &
271
- VariantProps<typeof menuItemVariants> &
272
- MenuSubTriggerPrimitiveProps & {
273
- asChild?: boolean
274
- disabled?: boolean
275
- }
276
-
277
- const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuItem({
278
- "aria-checked": ariaChecked,
279
- className,
280
- kind = "default",
281
- disabled = false,
282
- asChild = false,
283
- role,
284
- tabIndex,
285
- children,
286
- onSelect,
287
- textValue,
288
- ...props
289
- }, ref) {
290
- const Comp = asChild ? Slot : "div"
291
- const isStaticMenu = React.useContext(MenuStaticContext)
292
- const selectedKind: MenuItemKind = kind ?? "default"
293
- const isDisabled = disabled
294
- const switchState = findMenuItemSwitchState(children)
295
- const resolvedRole = role ??
296
- (switchState === undefined ? "menuitem" : "menuitemcheckbox")
297
- const resolvedChecked =
298
- resolvedRole === "menuitemcheckbox"
299
- ? (ariaChecked ?? switchState ?? false)
300
- : undefined
301
-
302
- const item = (
303
- <Comp
304
- aria-checked={resolvedChecked}
305
- aria-disabled={isDisabled ? true : undefined}
306
- data-disabled={isDisabled ? "true" : "false"}
307
- data-kind={selectedKind}
308
- data-slot="menu-item"
309
- ref={ref}
310
- role={resolvedRole}
311
- tabIndex={isDisabled ? -1 : (tabIndex ?? 0)}
312
- className={cn(menuItemVariants({ kind: selectedKind, className }))}
313
- {...props}
314
- >
315
- <MenuItemContent kind={selectedKind}>
316
- <MenuItemChildren kind={selectedKind}>{children}</MenuItemChildren>
317
- </MenuItemContent>
318
- </Comp>
319
- )
320
-
321
- if (isStaticMenu) return item
322
-
323
- if (switchState !== undefined) {
324
- return (
325
- <DropdownMenuPrimitive.CheckboxItem
326
- asChild
327
- checked={switchState}
328
- disabled={isDisabled}
329
- onSelect={onSelect}
330
- textValue={textValue ?? getMenuItemTextValue(children)}
331
- >
332
- {item}
333
- </DropdownMenuPrimitive.CheckboxItem>
334
- )
335
- }
336
-
337
- return (
338
- <DropdownMenuPrimitive.Item
339
- asChild
340
- disabled={isDisabled}
341
- onSelect={onSelect}
342
- textValue={textValue ?? getMenuItemTextValue(children)}
343
- >
344
- {item}
345
- </DropdownMenuPrimitive.Item>
346
- )
347
- })
348
-
349
- MenuItem.displayName = "MenuItem"
350
-
351
- function findMenuItemSwitchState(children: React.ReactNode): boolean | undefined {
352
- let state: boolean | undefined
353
-
354
- React.Children.forEach(children, (child) => {
355
- if (state !== undefined || !React.isValidElement(child)) return
356
-
357
- const element = child as React.ReactElement<{
358
- active?: boolean
359
- children?: React.ReactNode
360
- }>
361
- if (element.type === MenuItemSwitch) {
362
- state = Boolean(element.props.active)
363
- return
364
- }
365
-
366
- state = findMenuItemSwitchState(element.props.children)
367
- })
368
-
369
- return state
370
- }
371
-
372
- const MenuSubTrigger = React.forwardRef<HTMLDivElement, MenuSubTriggerProps>(function MenuSubTrigger({
373
- className,
374
- kind = "default",
375
- disabled = false,
376
- asChild = false,
377
- role = "menuitem",
378
- tabIndex,
379
- children,
380
- textValue,
381
- ...props
382
- }, ref) {
383
- const Comp = asChild ? Slot : "div"
384
- const isStaticMenu = React.useContext(MenuStaticContext)
385
- const selectedKind: MenuItemKind = kind ?? "default"
386
- const isDisabled = disabled
387
-
388
- const item = (
389
- <Comp
390
- aria-disabled={isDisabled ? true : undefined}
391
- data-disabled={isDisabled ? "true" : "false"}
392
- data-kind={selectedKind}
393
- data-slot="menu-sub-trigger"
394
- ref={ref}
395
- role={role}
396
- tabIndex={isDisabled ? -1 : (tabIndex ?? 0)}
397
- className={cn(menuItemVariants({ kind: selectedKind, className }))}
398
- {...props}
399
- >
400
- <MenuItemContent kind={selectedKind}>
401
- <MenuItemChildren kind={selectedKind}>{children}</MenuItemChildren>
402
- </MenuItemContent>
403
- </Comp>
404
- )
405
-
406
- if (isStaticMenu) return item
407
-
408
- return (
409
- <DropdownMenuPrimitive.SubTrigger
410
- asChild
411
- disabled={isDisabled}
412
- textValue={textValue ?? getMenuItemTextValue(children)}
413
- >
414
- {item}
415
- </DropdownMenuPrimitive.SubTrigger>
416
- )
417
- })
418
-
419
- MenuSubTrigger.displayName = "MenuSubTrigger"
420
-
421
- type MenuItemContentProps = React.ComponentProps<"div"> &
422
- VariantProps<typeof menuItemContentVariants>
423
-
424
- function MenuItemContent({
425
- className,
426
- kind,
427
- ...props
428
- }: MenuItemContentProps) {
429
- return (
430
- <div
431
- data-slot="menu-item-content"
432
- className={cn(menuItemContentVariants({ kind, className }))}
433
- {...props}
434
- />
435
- )
436
- }
437
-
438
- function MenuItemChildren({
439
- children,
440
- kind,
441
- }: {
442
- children: React.ReactNode
443
- kind: MenuItemKind
444
- }) {
445
- return React.Children.map(children, (child) => {
446
- if (kind === "button" && (typeof child === "string" || typeof child === "number")) {
447
- return <MenuItemAction fullWidth>{child}</MenuItemAction>
448
- }
449
-
450
- if (typeof child === "string" || typeof child === "number") {
451
- return (
452
- <MenuItemText>
453
- <MenuItemTitle>{child}</MenuItemTitle>
454
- </MenuItemText>
455
- )
456
- }
457
-
458
- return child
459
- })
460
- }
461
-
462
- function MenuItemText({
463
- className,
464
- ...props
465
- }: React.ComponentProps<"span">) {
466
- return (
467
- <span
468
- data-slot="menu-item-text"
469
- className={cn(
470
- "flex min-w-0 flex-1 flex-col ps-[var(--bh-space-xs-4)]",
471
- className
472
- )}
473
- {...props}
474
- />
475
- )
476
- }
477
-
478
- function MenuItemTitle({
479
- className,
480
- dir = "auto",
481
- ...props
482
- }: React.ComponentProps<"span">) {
483
- return (
484
- <span
485
- data-slot="menu-item-title"
486
- dir={dir}
487
- className={cn(
488
- "min-w-0 truncate text-start text-[length:var(--bh-text-body-md-regular-font-size)] font-[var(--bh-text-body-md-regular-font-weight)] leading-[var(--bh-text-body-md-regular-line-height)] tracking-[var(--bh-text-body-md-regular-letter-spacing)] text-[var(--bh-content-default)] group-data-[disabled=true]/menu-item:text-[var(--bh-content-disabled)]",
489
- className
490
- )}
491
- {...props}
492
- />
493
- )
494
- }
495
-
496
- function MenuItemDescription({
497
- className,
498
- dir = "auto",
499
- ...props
500
- }: React.ComponentProps<"span">) {
501
- return (
502
- <span
503
- data-slot="menu-item-description"
504
- dir={dir}
505
- className={cn(
506
- "min-w-0 truncate text-start text-[length:var(--bh-text-body-2xs-regular-font-size)] font-[var(--bh-text-body-2xs-regular-font-weight)] leading-[var(--bh-text-body-2xs-regular-line-height)] tracking-[var(--bh-text-body-2xs-regular-letter-spacing)] text-[var(--bh-content-subtle)] group-data-[disabled=true]/menu-item:text-[var(--bh-content-disabled)]",
507
- className
508
- )}
509
- {...props}
510
- />
511
- )
512
- }
513
-
514
- function MenuItemMeta({
515
- className,
516
- dir = "auto",
517
- ...props
518
- }: React.ComponentProps<"span">) {
519
- return (
520
- <span
521
- data-slot="menu-item-meta"
522
- dir={dir}
523
- className={cn(
524
- "shrink-0 whitespace-nowrap text-end text-[length:var(--bh-text-body-2xs-regular-font-size)] font-[var(--bh-text-body-2xs-regular-font-weight)] leading-[var(--bh-text-body-2xs-regular-line-height)] tracking-[var(--bh-text-body-2xs-regular-letter-spacing)] text-[var(--bh-content-subtle)] group-data-[disabled=true]/menu-item:text-[var(--bh-content-disabled)]",
525
- className
526
- )}
527
- {...props}
528
- />
529
- )
530
- }
531
-
532
- type MenuItemIconProps = React.ComponentProps<"span"> &
533
- VariantProps<typeof menuItemIconVariants>
534
-
535
- function MenuItemIcon({
536
- className,
537
- position,
538
- ...props
539
- }: MenuItemIconProps) {
540
- return (
541
- <span
542
- aria-hidden="true"
543
- data-position={position ?? "leading"}
544
- data-slot="menu-item-icon"
545
- className={cn(menuItemIconVariants({ position, className }))}
546
- {...props}
547
- />
548
- )
549
- }
550
-
551
- type MenuItemAvatarProps = React.ComponentProps<"span"> & {
552
- alt?: string
553
- size?: MenuItemAvatarSize
554
- src?: string
555
- }
556
-
557
- function MenuItemAvatar({
558
- alt = "",
559
- children,
560
- className,
561
- size = "sm",
562
- src,
563
- ...props
564
- }: MenuItemAvatarProps) {
565
- return (
566
- <span
567
- data-size={size}
568
- data-slot="menu-item-avatar-wrap"
569
- className={cn(
570
- "flex shrink-0 items-center ps-[var(--bh-menu-icon-padding)] group-data-[disabled=true]/menu-item:opacity-[var(--bh-opacity-60)]",
571
- size === "lg"
572
- ? "h-[var(--bh-space-7xl-40)]"
573
- : "h-[var(--bh-space-5xl-24)]",
574
- className
575
- )}
576
- {...props}
577
- >
578
- <span
579
- data-slot="menu-item-avatar"
580
- className={cn(
581
- "flex shrink-0 items-center justify-center overflow-hidden rounded-[var(--bh-radius-full)] border border-[var(--bh-border-subtle)] bg-[var(--bh-bg-neutral-subtle)] text-[length:var(--bh-text-body-3xs-medium-font-size)] font-[var(--bh-text-body-3xs-medium-font-weight)] leading-[var(--bh-text-body-3xs-medium-line-height)] text-[var(--bh-content-subtle)]",
582
- size === "lg"
583
- ? "size-[var(--bh-space-7xl-40)]"
584
- : "size-[var(--bh-space-5xl-24)]"
585
- )}
586
- >
587
- {src ? (
588
- <img alt={alt} className="size-full object-cover" src={src} />
589
- ) : (
590
- children
591
- )}
592
- </span>
593
- </span>
594
- )
595
- }
596
-
597
- function MenuItemBadge({
598
- className,
599
- dir = "auto",
600
- ...props
601
- }: React.ComponentProps<"span">) {
602
- return (
603
- <span
604
- data-slot="menu-item-badge"
605
- dir={dir}
606
- className={cn(
607
- "inline-flex h-[var(--bh-menu-badge-height)] shrink-0 items-center justify-center rounded-[var(--bh-radius-full)] bg-[var(--bh-bg-neutral-subtle)] px-[var(--bh-menu-badge-padding-x)] text-center text-[length:var(--bh-text-body-3xs-medium-font-size)] font-[var(--bh-text-body-3xs-medium-font-weight)] leading-[var(--bh-text-body-3xs-medium-line-height)] tracking-[var(--bh-text-body-3xs-medium-letter-spacing)] text-[var(--bh-content-default)] group-data-[disabled=true]/menu-item:opacity-[var(--bh-opacity-60)]",
608
- className
609
- )}
610
- {...props}
611
- />
612
- )
613
- }
614
-
615
- type MenuItemSwitchProps = Omit<
616
- React.ComponentProps<"span">,
617
- "aria-checked" | "aria-label" | "aria-labelledby" | "role"
618
- > & {
619
- active?: boolean
620
- disabled?: boolean
621
- }
622
-
623
- function MenuItemSwitch({
624
- active = false,
625
- className,
626
- disabled = false,
627
- ...props
628
- }: MenuItemSwitchProps) {
629
- return (
630
- <span
631
- aria-hidden="true"
632
- data-active={active ? "true" : "false"}
633
- data-disabled={disabled ? "true" : "false"}
634
- data-slot="menu-item-switch"
635
- className={cn(
636
- "flex h-[var(--bh-menu-switch-height)] w-[var(--bh-menu-switch-width)] shrink-0 items-center rounded-[var(--bh-radius-full)] p-[var(--bh-menu-switch-padding)] group-data-[disabled=true]/menu-item:bg-[var(--bh-interactive-switch-disabled)]",
637
- active
638
- ? "justify-end bg-[var(--bh-interactive-switch-active)]"
639
- : "bg-[var(--bh-interactive-switch-default)]",
640
- disabled && "bg-[var(--bh-interactive-switch-disabled)]",
641
- className
642
- )}
643
- {...props}
644
- >
645
- <span
646
- aria-hidden="true"
647
- data-slot="menu-item-switch-handle"
648
- className="size-[var(--bh-menu-switch-handle-size)] rounded-[var(--bh-radius-full)] bg-[var(--bh-interactive-switch-handle)] group-data-[disabled=true]/menu-item:bg-[var(--bh-interactive-switch-handle-disabled)]"
649
- />
650
- </span>
651
- )
652
- }
653
-
654
- type MenuItemActionProps = React.ComponentProps<"button"> & {
655
- asChild?: boolean
656
- fullWidth?: boolean
657
- }
658
-
659
- function MenuItemAction({
660
- className,
661
- asChild = false,
662
- fullWidth = false,
663
- type,
664
- children,
665
- ...props
666
- }: MenuItemActionProps) {
667
- const Comp = asChild ? Slot : "button"
668
-
669
- return (
670
- <Comp
671
- data-slot="menu-item-action"
672
- className={cn(
673
- "inline-flex h-[var(--bh-menu-action-height)] shrink-0 items-center justify-center gap-[var(--bh-menu-action-gap)] rounded-[var(--bh-control-default)] border border-[var(--bh-border-default)] bg-[var(--bh-interactive-outlined-default)] px-[var(--bh-menu-action-padding-x)] py-[var(--bh-space-md-8)] text-[length:var(--bh-text-body-md-medium-font-size)] font-[var(--bh-text-body-md-medium-font-weight)] leading-[var(--bh-text-body-md-medium-line-height)] tracking-[var(--bh-text-body-md-medium-letter-spacing)] text-[var(--bh-content-default)] outline-none transition-[background-color,border-color,box-shadow] hover:bg-[var(--bh-interactive-outlined-hover)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:pointer-events-none disabled:text-[var(--bh-content-disabled)] rtl:[&_svg[data-rtl-flip='true']]:-scale-x-100",
674
- fullWidth && "w-full",
675
- className
676
- )}
677
- {...(!asChild ? { type: type || "button" } : {})}
678
- {...props}
679
- >
680
- {asChild ? children : <MenuItemActionChildren>{children}</MenuItemActionChildren>}
681
- </Comp>
682
- )
683
- }
684
-
685
- function MenuItemActionChildren({ children }: { children: React.ReactNode }) {
686
- return React.Children.map(children, (child) => {
687
- if (typeof child === "string" || typeof child === "number") {
688
- return (
689
- <span data-slot="menu-item-action-label" dir="auto" className="min-w-0 px-[var(--bh-space-xs-4)]">
690
- {child}
691
- </span>
692
- )
693
- }
694
-
695
- return child
696
- })
697
- }
698
-
699
- type MenuProgressProps = React.ComponentProps<"div"> & {
700
- indicator?: React.ReactNode
701
- label?: React.ReactNode
702
- optional?: React.ReactNode
703
- showSpinner?: boolean
704
- value?: number
705
- }
706
-
707
- function MenuProgress({
708
- className,
709
- indicator,
710
- label,
711
- optional,
712
- showSpinner = true,
713
- value = 50,
714
- ...props
715
- }: MenuProgressProps) {
716
- const clampedValue = Math.min(100, Math.max(0, value))
717
- const labelId = React.useId()
718
- const hasLabel = hasRenderableContent(label)
719
-
720
- return (
721
- <div
722
- data-slot="menu-progress"
723
- className={cn(
724
- "flex w-full flex-col gap-[var(--bh-menu-progress-gap)]",
725
- className
726
- )}
727
- {...props}
728
- >
729
- <div
730
- aria-label={hasLabel ? undefined : "Progress"}
731
- aria-labelledby={hasLabel ? labelId : undefined}
732
- aria-valuemax={100}
733
- aria-valuemin={0}
734
- aria-valuenow={clampedValue}
735
- data-slot="menu-progress-track"
736
- role="progressbar"
737
- className="h-[var(--bh-menu-progress-height)] w-full overflow-hidden rounded-[var(--bh-radius-full)] bg-[var(--bh-bg-neutral-soft)]"
738
- >
739
- <span
740
- data-slot="menu-progress-fill"
741
- className="block h-full rounded-[var(--bh-radius-full)] bg-[var(--bh-interactive-indicator-default)]"
742
- style={{ inlineSize: `${clampedValue}%` }}
743
- />
744
- </div>
745
-
746
- {hasRenderableContent(label) ||
747
- hasRenderableContent(optional) ||
748
- hasRenderableContent(indicator) ||
749
- showSpinner ? (
750
- <div
751
- data-slot="menu-progress-row"
752
- className="flex min-w-0 items-center gap-[var(--bh-menu-item-slot-gap)]"
753
- >
754
- {hasRenderableContent(label) || hasRenderableContent(optional) ? (
755
- <span
756
- data-slot="menu-progress-label"
757
- className="flex min-w-0 flex-1 items-center gap-[var(--bh-space-xs-4)]"
758
- >
759
- {hasRenderableContent(label) ? (
760
- <span
761
- id={labelId}
762
- data-slot="menu-progress-label-text"
763
- dir="auto"
764
- className="truncate text-[length:var(--bh-text-body-xs-medium-font-size)] font-[var(--bh-text-body-xs-medium-font-weight)] leading-[var(--bh-text-body-xs-medium-line-height)] tracking-[var(--bh-text-body-xs-medium-letter-spacing)] text-[var(--bh-content-default)]"
765
- >
766
- {label}
767
- </span>
768
- ) : null}
769
- {hasRenderableContent(optional) ? (
770
- <span
771
- data-slot="menu-progress-optional"
772
- dir="auto"
773
- className="truncate text-[length:var(--bh-text-body-xs-regular-font-size)] font-[var(--bh-text-body-xs-regular-font-weight)] leading-[var(--bh-text-body-xs-regular-line-height)] tracking-[var(--bh-text-body-xs-regular-letter-spacing)] text-[var(--bh-content-subtle)]"
774
- >
775
- {optional}
776
- </span>
777
- ) : null}
778
- </span>
779
- ) : null}
780
-
781
- <span
782
- data-slot="menu-progress-indicator"
783
- className="flex shrink-0 items-center gap-[var(--bh-space-xxs-2)] text-[length:var(--bh-text-body-2xs-regular-font-size)] font-[var(--bh-text-body-2xs-regular-font-weight)] leading-[var(--bh-text-body-2xs-regular-line-height)] tracking-[var(--bh-text-body-2xs-regular-letter-spacing)] text-[var(--bh-content-subtle)]"
784
- >
785
- {hasRenderableContent(indicator) ? (
786
- <span dir="auto">{indicator}</span>
787
- ) : null}
788
- {showSpinner ? <MenuProgressSpinner /> : null}
789
- </span>
790
- </div>
791
- ) : null}
792
- </div>
793
- )
794
- }
795
-
796
- function MenuProgressSpinner({ className }: { className?: string }) {
797
- return (
798
- <svg
799
- aria-hidden="true"
800
- data-slot="menu-progress-spinner"
801
- viewBox="0 0 24 24"
802
- className={cn("size-[var(--bh-menu-trailing-icon-size)] animate-spin", className)}
803
- >
804
- <path
805
- d="M12 3a9 9 0 1 1-6.36 2.64"
806
- fill="none"
807
- stroke="currentColor"
808
- strokeLinecap="round"
809
- strokeWidth="var(--bh-icon-stroke-300)"
810
- />
811
- </svg>
812
- )
813
- }
814
-
815
- type MenuLabelProps = React.ComponentProps<"div"> & {
816
- caption?: React.ReactNode
817
- label?: React.ReactNode
818
- }
819
-
820
- function MenuLabel({
821
- caption,
822
- children,
823
- className,
824
- label,
825
- role = "presentation",
826
- ...props
827
- }: MenuLabelProps) {
828
- const isStaticMenu = React.useContext(MenuStaticContext)
829
- const labelContent = (
830
- <div
831
- data-slot="menu-label"
832
- role={role}
833
- className={cn(
834
- "flex h-[var(--bh-menu-item-label-height)] w-full items-center gap-0 bg-[var(--bh-menu-item-bg)] px-[var(--bh-menu-label-padding-x)] py-[var(--bh-space-xs-4)] text-[length:var(--bh-text-body-xs-regular-font-size)] leading-[var(--bh-text-body-xs-regular-line-height)] tracking-[var(--bh-text-body-xs-regular-letter-spacing)]",
835
- className
836
- )}
837
- {...props}
838
- >
839
- {children ?? (
840
- <>
841
- {hasRenderableContent(label) ? (
842
- <span
843
- data-slot="menu-label-primary"
844
- dir="auto"
845
- className="min-w-0 flex-1 truncate text-start font-[var(--bh-text-body-xs-medium-font-weight)] text-[var(--bh-content-default)]"
846
- >
847
- {label}
848
- </span>
849
- ) : null}
850
- {hasRenderableContent(caption) ? (
851
- <span
852
- data-slot="menu-label-caption"
853
- dir="auto"
854
- className="shrink-0 whitespace-nowrap text-end font-[var(--bh-text-body-xs-regular-font-weight)] text-[var(--bh-content-subtle)]"
855
- >
856
- {caption}
857
- </span>
858
- ) : null}
859
- </>
860
- )}
861
- </div>
862
- )
863
-
864
- if (isStaticMenu) return labelContent
865
-
866
- return <DropdownMenuPrimitive.Label asChild>{labelContent}</DropdownMenuPrimitive.Label>
867
- }
868
-
869
- function hasRenderableContent(content: React.ReactNode) {
870
- return (
871
- content !== undefined &&
872
- content !== null &&
873
- content !== false &&
874
- content !== ""
875
- )
876
- }
877
-
878
- function MenuCaption({
879
- className,
880
- dir = "auto",
881
- role = "presentation",
882
- ...props
883
- }: React.ComponentProps<"div">) {
884
- return (
885
- <div
886
- data-slot="menu-caption"
887
- role={role}
888
- className="flex h-[var(--bh-menu-item-caption-height)] w-full items-center px-[var(--bh-menu-item-padding-x)]"
889
- >
890
- <span
891
- dir={dir}
892
- className={cn(
893
- "flex h-full min-w-0 flex-1 items-center justify-center px-[var(--bh-menu-item-content-padding)] py-[var(--bh-menu-item-content-compact-padding-y)] text-center text-[length:var(--bh-text-body-xs-regular-font-size)] font-[var(--bh-text-body-xs-regular-font-weight)] leading-[var(--bh-text-body-xs-regular-line-height)] tracking-[var(--bh-text-body-xs-regular-letter-spacing)] text-[var(--bh-content-subtle)]",
894
- className
895
- )}
896
- {...props}
897
- />
898
- </div>
899
- )
900
- }
901
-
902
- function MenuSeparator({
903
- className,
904
- role = "separator",
905
- ...props
906
- }: React.ComponentProps<"div">) {
907
- const isStaticMenu = React.useContext(MenuStaticContext)
908
- const separator = (
909
- <div
910
- data-slot="menu-separator"
911
- role={role}
912
- className={cn(
913
- "flex h-[var(--bh-menu-item-divider-height)] w-full items-center py-[var(--bh-menu-divider-padding-y)]",
914
- className
915
- )}
916
- {...props}
917
- >
918
- <span className="block w-full border-t border-[var(--bh-border-subtle)]" />
919
- </div>
920
- )
921
-
922
- if (isStaticMenu) return separator
923
-
924
- return <DropdownMenuPrimitive.Separator asChild>{separator}</DropdownMenuPrimitive.Separator>
925
- }
926
-
927
- export {
928
- Menu,
929
- MenuCaption,
930
- MenuContent,
931
- MenuItem,
932
- MenuItemAction,
933
- MenuItemAvatar,
934
- MenuItemBadge,
935
- MenuItemContent,
936
- MenuItemDescription,
937
- MenuItemIcon,
938
- MenuItemMeta,
939
- MenuItemSwitch,
940
- MenuItemText,
941
- MenuItemTitle,
942
- MenuLabel,
943
- MenuProgress,
944
- MenuPreview,
945
- MenuPortal,
946
- MenuRoot,
947
- MenuSeparator,
948
- MenuSub,
949
- MenuSubContent,
950
- MenuSubTrigger,
951
- MenuSurface,
952
- MenuTrigger,
953
- menuItemContentVariants,
954
- menuItemIconVariants,
955
- menuItemVariants,
956
- menuVariants,
957
- }
958
- export type {
959
- MenuItemAvatarSize,
960
- MenuItemAvatarProps,
961
- MenuItemActionProps,
962
- MenuContentProps,
963
- MenuItemContentProps,
964
- MenuItemIconPosition,
965
- MenuItemIconProps,
966
- MenuItemKind,
967
- MenuItemProps,
968
- MenuSubContentProps,
969
- MenuSubTriggerProps,
970
- MenuItemSwitchProps,
971
- MenuItemState,
972
- MenuLabelProps,
973
- MenuProgressProps,
974
- MenuProps,
975
- MenuPreviewProps,
976
- MenuSurfaceProps,
977
- MenuWidth,
978
- }
1
+ import * as React from "react"
2
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
3
+ import { Slot } from "@radix-ui/react-slot"
4
+ import { CheckIcon } from "lucide-react"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import { useResolvedDirection } from "@/hooks/use-direction"
9
+
10
+ type MenuWidth = "menu" | "default" | "multiline" | "action" | "auto"
11
+ type MenuItemKind = "default" | "multiline" | "action" | "progress" | "button"
12
+ type MenuItemState = "default" | "disabled"
13
+ type MenuItemIconPosition = "leading" | "trailing"
14
+ type MenuItemAvatarSize = "sm" | "lg"
15
+ type MenuCollisionPadding = NonNullable<
16
+ React.ComponentProps<typeof DropdownMenuPrimitive.Content>["collisionPadding"]
17
+ >
18
+
19
+ // Default Radix collisionPadding mirrors --bh-space-md-8.
20
+ const MENU_COLLISION_PADDING_PX = 8
21
+ // Radix sideOffset is numeric; this mirrors --bh-space-xs-4.
22
+ const MENU_SUBCONTENT_SIDE_OFFSET_PX = 4
23
+
24
+ const menuVariants = cva(
25
+ "flex flex-col items-start gap-[var(--bh-menu-gap)] overflow-hidden rounded-[var(--bh-menu-radius)] bg-[var(--bh-menu-bg)] py-[var(--bh-menu-padding-y)] shadow-[var(--elevation-menu)]",
26
+ {
27
+ variants: {
28
+ width: {
29
+ menu: "w-[var(--bh-menu-width)]",
30
+ default: "w-[var(--bh-menu-item-default-width)]",
31
+ multiline: "w-[var(--bh-menu-item-multiline-width)]",
32
+ action: "w-[var(--bh-menu-item-action-width)]",
33
+ auto: "w-max",
34
+ },
35
+ },
36
+ defaultVariants: {
37
+ width: "menu",
38
+ },
39
+ }
40
+ )
41
+
42
+ const menuItemVariants = cva(
43
+ "group/menu-item relative flex w-full items-center bg-[var(--bh-menu-item-bg)] outline-none transition-colors data-[disabled=true]:pointer-events-none",
44
+ {
45
+ variants: {
46
+ kind: {
47
+ default:
48
+ "h-[var(--bh-menu-item-default-height)] px-[var(--bh-menu-item-padding-x)]",
49
+ multiline:
50
+ "min-h-[var(--bh-menu-item-multiline-height)] px-[var(--bh-menu-item-padding-x)]",
51
+ action:
52
+ "min-h-[var(--bh-menu-item-action-height)] px-[var(--bh-menu-item-padding-x)]",
53
+ progress:
54
+ "h-[var(--bh-menu-item-progress-height)] px-[var(--bh-menu-item-padding-x)]",
55
+ button:
56
+ "h-[var(--bh-menu-item-button-height)] px-[var(--bh-menu-item-padding-x)]",
57
+ },
58
+ },
59
+ defaultVariants: {
60
+ kind: "default",
61
+ },
62
+ }
63
+ )
64
+
65
+ const menuItemContentVariants = cva(
66
+ "flex min-w-0 flex-1 transition-colors group-data-[disabled=true]/menu-item:bg-transparent [&_svg]:shrink-0 rtl:[&_svg[data-rtl-flip='true']]:-scale-x-100",
67
+ {
68
+ variants: {
69
+ kind: {
70
+ default:
71
+ "h-[var(--bh-menu-item-default-height)] items-center gap-[var(--bh-menu-item-gap)] rounded-[var(--bh-menu-item-radius)] p-[var(--bh-menu-item-content-padding)] group-hover/menu-item:bg-[var(--bh-menu-item-hover-bg)]",
72
+ multiline:
73
+ "min-h-[var(--bh-menu-item-multiline-height)] items-center gap-[var(--bh-menu-item-gap)] rounded-[var(--bh-menu-item-radius)] px-[var(--bh-menu-item-content-padding)] py-[var(--bh-menu-item-content-compact-padding-y)] group-hover/menu-item:bg-[var(--bh-menu-item-hover-bg)]",
74
+ action:
75
+ "min-h-[var(--bh-menu-item-action-height)] items-center gap-[var(--bh-menu-item-gap)] rounded-[var(--bh-menu-item-radius)] px-[var(--bh-menu-item-content-padding)] py-[var(--bh-menu-item-content-compact-padding-y)] group-hover/menu-item:bg-[var(--bh-menu-item-hover-bg)]",
76
+ progress:
77
+ "h-[var(--bh-menu-item-progress-height)] flex-col items-start gap-0 px-[var(--bh-menu-item-content-padding)] py-[var(--bh-menu-item-content-compact-padding-y)]",
78
+ button:
79
+ "h-[var(--bh-menu-item-button-height)] flex-col items-start gap-[var(--bh-menu-item-slot-gap)] px-[var(--bh-menu-item-content-padding)] py-[var(--bh-menu-item-content-compact-padding-y)]",
80
+ },
81
+ },
82
+ defaultVariants: {
83
+ kind: "default",
84
+ },
85
+ }
86
+ )
87
+
88
+ const menuItemIconVariants = cva(
89
+ "flex shrink-0 items-center justify-center text-[var(--bh-content-subtle)] group-data-[disabled=true]/menu-item:text-[var(--bh-content-disabled)]",
90
+ {
91
+ variants: {
92
+ position: {
93
+ leading:
94
+ "h-[var(--bh-menu-leading-icon-slot-size)] ps-[var(--bh-menu-icon-padding)] [&_svg]:size-[var(--bh-menu-leading-icon-size)]",
95
+ trailing:
96
+ "size-[var(--bh-menu-trailing-icon-size)] text-[var(--bh-content-muted)] [&_svg]:size-[var(--bh-menu-trailing-icon-size)]",
97
+ },
98
+ },
99
+ defaultVariants: {
100
+ position: "leading",
101
+ },
102
+ }
103
+ )
104
+
105
+ type MenuProps = React.ComponentProps<typeof DropdownMenuPrimitive.Root>
106
+
107
+ type MenuContentProps = React.ComponentProps<typeof DropdownMenuPrimitive.Content> &
108
+ VariantProps<typeof menuVariants> & {
109
+ asChild?: boolean
110
+ }
111
+
112
+ type MenuSubContentProps = React.ComponentProps<typeof DropdownMenuPrimitive.SubContent> &
113
+ VariantProps<typeof menuVariants> & {
114
+ asChild?: boolean
115
+ }
116
+
117
+ const MenuRoot = DropdownMenuPrimitive.Root
118
+ const MenuSub = DropdownMenuPrimitive.Sub
119
+ const MenuTrigger = DropdownMenuPrimitive.Trigger
120
+ const MenuStaticContext = React.createContext(false)
121
+ // The chosen value of the MenuRadioGroup around an item in a static MenuPreview (Radix tracks it in a live menu).
122
+ const MenuRadioStaticContext = React.createContext<string | undefined>(undefined)
123
+
124
+ function Menu({ dir, ...props }: MenuProps) {
125
+ const directionBoundaryRef = React.useRef<HTMLSpanElement | null>(null)
126
+ const resolvedDirection = useResolvedDirection(dir, directionBoundaryRef)
127
+
128
+ return (
129
+ <span className="contents" data-slot="menu-root-boundary" ref={directionBoundaryRef}>
130
+ <MenuRoot dir={resolvedDirection} {...props} />
131
+ </span>
132
+ )
133
+ }
134
+
135
+ type MenuSurfaceProps = React.ComponentProps<"div"> &
136
+ VariantProps<typeof menuVariants> & {
137
+ asChild?: boolean
138
+ }
139
+
140
+ const MenuSurface = React.forwardRef<HTMLDivElement, MenuSurfaceProps>(function MenuSurface({
141
+ asChild = false,
142
+ children,
143
+ className,
144
+ role = "menu",
145
+ width,
146
+ ...props
147
+ }, ref) {
148
+ const Comp = asChild ? Slot : "div"
149
+
150
+ return (
151
+ <Comp
152
+ data-slot="menu"
153
+ data-width={width ?? "menu"}
154
+ ref={ref}
155
+ role={role}
156
+ className={cn(
157
+ menuVariants({ width }),
158
+ "max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-y-auto",
159
+ className
160
+ )}
161
+ {...props}
162
+ >
163
+ {children}
164
+ </Comp>
165
+ )
166
+ })
167
+
168
+ const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>(function MenuContent({
169
+ children,
170
+ className,
171
+ collisionPadding = MENU_COLLISION_PADDING_PX,
172
+ width,
173
+ asChild = false,
174
+ role = "menu",
175
+ ...props
176
+ }, ref) {
177
+ return (
178
+ <DropdownMenuPrimitive.Content
179
+ asChild
180
+ collisionPadding={collisionPadding}
181
+ {...props}
182
+ >
183
+ <MenuSurface
184
+ asChild={asChild}
185
+ className={className}
186
+ ref={ref}
187
+ role={role}
188
+ width={width}
189
+ >
190
+ {children}
191
+ </MenuSurface>
192
+ </DropdownMenuPrimitive.Content>
193
+ )
194
+ })
195
+
196
+ const MenuSubContent = React.forwardRef<HTMLDivElement, MenuSubContentProps>(function MenuSubContent({
197
+ children,
198
+ className,
199
+ collisionPadding = MENU_COLLISION_PADDING_PX,
200
+ sideOffset = MENU_SUBCONTENT_SIDE_OFFSET_PX,
201
+ width,
202
+ asChild = false,
203
+ role = "menu",
204
+ ...props
205
+ }, ref) {
206
+ return (
207
+ <DropdownMenuPrimitive.SubContent
208
+ asChild
209
+ collisionPadding={collisionPadding}
210
+ sideOffset={sideOffset}
211
+ {...props}
212
+ >
213
+ <MenuSurface
214
+ asChild={asChild}
215
+ className={className}
216
+ ref={ref}
217
+ role={role}
218
+ width={width}
219
+ >
220
+ {children}
221
+ </MenuSurface>
222
+ </DropdownMenuPrimitive.SubContent>
223
+ )
224
+ })
225
+
226
+ type MenuPreviewProps = React.ComponentProps<"div"> &
227
+ VariantProps<typeof menuVariants> & {
228
+ asChild?: boolean
229
+ }
230
+
231
+ function MenuPreview({
232
+ children,
233
+ className,
234
+ role = "menu",
235
+ width,
236
+ asChild = false,
237
+ ...props
238
+ }: MenuPreviewProps) {
239
+ return (
240
+ <MenuStaticContext.Provider value>
241
+ <MenuSurface
242
+ asChild={asChild}
243
+ className={className}
244
+ role={role}
245
+ width={width}
246
+ {...props}
247
+ >
248
+ {children}
249
+ </MenuSurface>
250
+ </MenuStaticContext.Provider>
251
+ )
252
+ }
253
+
254
+ const MenuPortal = DropdownMenuPrimitive.Portal
255
+
256
+ function getMenuItemTextValue(children: React.ReactNode) {
257
+ if (typeof children === "string" || typeof children === "number") {
258
+ return String(children)
259
+ }
260
+
261
+ return undefined
262
+ }
263
+
264
+ type MenuItemProps =
265
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> &
266
+ VariantProps<typeof menuItemVariants>
267
+
268
+ type MenuSubTriggerPrimitiveProps = Pick<
269
+ React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger>,
270
+ "textValue"
271
+ >
272
+
273
+ type MenuSubTriggerProps = React.ComponentProps<"div"> &
274
+ VariantProps<typeof menuItemVariants> &
275
+ MenuSubTriggerPrimitiveProps & {
276
+ asChild?: boolean
277
+ disabled?: boolean
278
+ }
279
+
280
+ const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuItem({
281
+ "aria-checked": ariaChecked,
282
+ className,
283
+ kind = "default",
284
+ disabled = false,
285
+ asChild = false,
286
+ role,
287
+ tabIndex,
288
+ children,
289
+ onSelect,
290
+ textValue,
291
+ ...props
292
+ }, ref) {
293
+ const Comp = asChild ? Slot : "div"
294
+ const isStaticMenu = React.useContext(MenuStaticContext)
295
+ const selectedKind: MenuItemKind = kind ?? "default"
296
+ const isDisabled = disabled
297
+ const switchState = findMenuItemSwitchState(children)
298
+ const resolvedRole = role ??
299
+ (switchState === undefined ? "menuitem" : "menuitemcheckbox")
300
+ const resolvedChecked =
301
+ resolvedRole === "menuitemcheckbox"
302
+ ? (ariaChecked ?? switchState ?? false)
303
+ : undefined
304
+
305
+ const item = (
306
+ <Comp
307
+ aria-checked={resolvedChecked}
308
+ aria-disabled={isDisabled ? true : undefined}
309
+ data-disabled={isDisabled ? "true" : "false"}
310
+ data-kind={selectedKind}
311
+ data-slot="menu-item"
312
+ ref={ref}
313
+ role={resolvedRole}
314
+ tabIndex={isDisabled ? -1 : (tabIndex ?? 0)}
315
+ className={cn(menuItemVariants({ kind: selectedKind, className }))}
316
+ {...props}
317
+ >
318
+ <MenuItemContent kind={selectedKind}>
319
+ <MenuItemChildren kind={selectedKind}>{children}</MenuItemChildren>
320
+ </MenuItemContent>
321
+ </Comp>
322
+ )
323
+
324
+ if (isStaticMenu) return item
325
+
326
+ if (switchState !== undefined) {
327
+ return (
328
+ <DropdownMenuPrimitive.CheckboxItem
329
+ asChild
330
+ checked={switchState}
331
+ disabled={isDisabled}
332
+ onSelect={onSelect}
333
+ textValue={textValue ?? getMenuItemTextValue(children)}
334
+ >
335
+ {item}
336
+ </DropdownMenuPrimitive.CheckboxItem>
337
+ )
338
+ }
339
+
340
+ return (
341
+ <DropdownMenuPrimitive.Item
342
+ asChild
343
+ disabled={isDisabled}
344
+ onSelect={onSelect}
345
+ textValue={textValue ?? getMenuItemTextValue(children)}
346
+ >
347
+ {item}
348
+ </DropdownMenuPrimitive.Item>
349
+ )
350
+ })
351
+
352
+ MenuItem.displayName = "MenuItem"
353
+
354
+ type MenuRadioGroupProps = React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioGroup>
355
+
356
+ // One choice among MenuRadioItems: value is the chosen item's value, onValueChange receives the new one. The group
357
+ // is a full-width column with the menu's item gap, so its items stretch and space as the menu's other items do.
358
+ const menuRadioGroupClassName = "flex w-full flex-col gap-[var(--bh-menu-gap)]"
359
+
360
+ function MenuRadioGroup({ children, className, onValueChange, value, ...props }: MenuRadioGroupProps) {
361
+ const isStaticMenu = React.useContext(MenuStaticContext)
362
+
363
+ if (isStaticMenu) {
364
+ return (
365
+ <MenuRadioStaticContext.Provider value={value}>
366
+ <div data-slot="menu-radio-group" role="group" className={cn(menuRadioGroupClassName, className)} {...props}>
367
+ {children}
368
+ </div>
369
+ </MenuRadioStaticContext.Provider>
370
+ )
371
+ }
372
+
373
+ return (
374
+ <DropdownMenuPrimitive.RadioGroup
375
+ data-slot="menu-radio-group"
376
+ className={cn(menuRadioGroupClassName, className)}
377
+ onValueChange={onValueChange}
378
+ value={value}
379
+ {...props}
380
+ >
381
+ {children}
382
+ </DropdownMenuPrimitive.RadioGroup>
383
+ )
384
+ }
385
+
386
+ type MenuRadioItemProps =
387
+ Omit<React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>, "asChild"> &
388
+ VariantProps<typeof menuItemVariants>
389
+
390
+ // A menu item that is one of a MenuRadioGroup's choices: role menuitemradio with aria-checked, and a trailing check
391
+ // on the chosen one. Radix sets the role and checked state in a live menu; a static MenuPreview sets them here.
392
+ const MenuRadioItem = React.forwardRef<HTMLDivElement, MenuRadioItemProps>(function MenuRadioItem({
393
+ className,
394
+ kind = "default",
395
+ disabled = false,
396
+ children,
397
+ onSelect,
398
+ textValue,
399
+ value,
400
+ ...props
401
+ }, ref) {
402
+ const isStaticMenu = React.useContext(MenuStaticContext)
403
+ const staticValue = React.useContext(MenuRadioStaticContext)
404
+ const selectedKind: MenuItemKind = kind ?? "default"
405
+ const staticChecked = staticValue === value
406
+ const indicator = (
407
+ <MenuItemIcon data-slot="menu-radio-item-indicator" position="trailing">
408
+ <CheckIcon aria-hidden="true" focusable={false} strokeWidth="var(--bh-icon-stroke-200)" />
409
+ </MenuItemIcon>
410
+ )
411
+ // In a live menu, role, aria-checked and data-state come from Radix and must not be overridden.
412
+ const staticSemantics = isStaticMenu
413
+ ? {
414
+ "aria-checked": staticChecked,
415
+ "data-state": staticChecked ? "checked" : "unchecked",
416
+ role: "menuitemradio",
417
+ }
418
+ : {}
419
+
420
+ const item = (
421
+ <div
422
+ {...staticSemantics}
423
+ aria-disabled={disabled ? true : undefined}
424
+ data-disabled={disabled ? "true" : "false"}
425
+ data-kind={selectedKind}
426
+ data-slot="menu-item"
427
+ ref={ref}
428
+ tabIndex={disabled ? -1 : 0}
429
+ className={cn(menuItemVariants({ kind: selectedKind, className }))}
430
+ {...props}
431
+ >
432
+ <MenuItemContent kind={selectedKind}>
433
+ <MenuItemChildren kind={selectedKind}>{children}</MenuItemChildren>
434
+ {isStaticMenu ? (
435
+ staticChecked ? indicator : null
436
+ ) : (
437
+ <DropdownMenuPrimitive.ItemIndicator asChild>{indicator}</DropdownMenuPrimitive.ItemIndicator>
438
+ )}
439
+ </MenuItemContent>
440
+ </div>
441
+ )
442
+
443
+ if (isStaticMenu) return item
444
+
445
+ return (
446
+ <DropdownMenuPrimitive.RadioItem
447
+ asChild
448
+ disabled={disabled}
449
+ onSelect={onSelect}
450
+ textValue={textValue ?? getMenuItemTextValue(children)}
451
+ value={value}
452
+ >
453
+ {item}
454
+ </DropdownMenuPrimitive.RadioItem>
455
+ )
456
+ })
457
+
458
+ MenuRadioItem.displayName = "MenuRadioItem"
459
+
460
+ function findMenuItemSwitchState(children: React.ReactNode): boolean | undefined {
461
+ let state: boolean | undefined
462
+
463
+ React.Children.forEach(children, (child) => {
464
+ if (state !== undefined || !React.isValidElement(child)) return
465
+
466
+ const element = child as React.ReactElement<{
467
+ active?: boolean
468
+ children?: React.ReactNode
469
+ }>
470
+ if (element.type === MenuItemSwitch) {
471
+ state = Boolean(element.props.active)
472
+ return
473
+ }
474
+
475
+ state = findMenuItemSwitchState(element.props.children)
476
+ })
477
+
478
+ return state
479
+ }
480
+
481
+ const MenuSubTrigger = React.forwardRef<HTMLDivElement, MenuSubTriggerProps>(function MenuSubTrigger({
482
+ className,
483
+ kind = "default",
484
+ disabled = false,
485
+ asChild = false,
486
+ role = "menuitem",
487
+ tabIndex,
488
+ children,
489
+ textValue,
490
+ ...props
491
+ }, ref) {
492
+ const Comp = asChild ? Slot : "div"
493
+ const isStaticMenu = React.useContext(MenuStaticContext)
494
+ const selectedKind: MenuItemKind = kind ?? "default"
495
+ const isDisabled = disabled
496
+
497
+ const item = (
498
+ <Comp
499
+ aria-disabled={isDisabled ? true : undefined}
500
+ data-disabled={isDisabled ? "true" : "false"}
501
+ data-kind={selectedKind}
502
+ data-slot="menu-sub-trigger"
503
+ ref={ref}
504
+ role={role}
505
+ tabIndex={isDisabled ? -1 : (tabIndex ?? 0)}
506
+ className={cn(menuItemVariants({ kind: selectedKind, className }))}
507
+ {...props}
508
+ >
509
+ <MenuItemContent kind={selectedKind}>
510
+ <MenuItemChildren kind={selectedKind}>{children}</MenuItemChildren>
511
+ </MenuItemContent>
512
+ </Comp>
513
+ )
514
+
515
+ if (isStaticMenu) return item
516
+
517
+ return (
518
+ <DropdownMenuPrimitive.SubTrigger
519
+ asChild
520
+ disabled={isDisabled}
521
+ textValue={textValue ?? getMenuItemTextValue(children)}
522
+ >
523
+ {item}
524
+ </DropdownMenuPrimitive.SubTrigger>
525
+ )
526
+ })
527
+
528
+ MenuSubTrigger.displayName = "MenuSubTrigger"
529
+
530
+ type MenuItemContentProps = React.ComponentProps<"div"> &
531
+ VariantProps<typeof menuItemContentVariants>
532
+
533
+ function MenuItemContent({
534
+ className,
535
+ kind,
536
+ ...props
537
+ }: MenuItemContentProps) {
538
+ return (
539
+ <div
540
+ data-slot="menu-item-content"
541
+ className={cn(menuItemContentVariants({ kind, className }))}
542
+ {...props}
543
+ />
544
+ )
545
+ }
546
+
547
+ function MenuItemChildren({
548
+ children,
549
+ kind,
550
+ }: {
551
+ children: React.ReactNode
552
+ kind: MenuItemKind
553
+ }) {
554
+ return React.Children.map(children, (child) => {
555
+ if (kind === "button" && (typeof child === "string" || typeof child === "number")) {
556
+ return <MenuItemAction fullWidth>{child}</MenuItemAction>
557
+ }
558
+
559
+ if (typeof child === "string" || typeof child === "number") {
560
+ return (
561
+ <MenuItemText>
562
+ <MenuItemTitle>{child}</MenuItemTitle>
563
+ </MenuItemText>
564
+ )
565
+ }
566
+
567
+ return child
568
+ })
569
+ }
570
+
571
+ function MenuItemText({
572
+ className,
573
+ ...props
574
+ }: React.ComponentProps<"span">) {
575
+ return (
576
+ <span
577
+ data-slot="menu-item-text"
578
+ className={cn(
579
+ "flex min-w-0 flex-1 flex-col ps-[var(--bh-space-xs-4)]",
580
+ className
581
+ )}
582
+ {...props}
583
+ />
584
+ )
585
+ }
586
+
587
+ function MenuItemTitle({
588
+ className,
589
+ dir = "auto",
590
+ ...props
591
+ }: React.ComponentProps<"span">) {
592
+ return (
593
+ <span
594
+ data-slot="menu-item-title"
595
+ dir={dir}
596
+ className={cn(
597
+ "min-w-0 truncate text-start text-[length:var(--bh-text-body-md-regular-font-size)] font-[var(--bh-text-body-md-regular-font-weight)] leading-[var(--bh-text-body-md-regular-line-height)] tracking-[var(--bh-text-body-md-regular-letter-spacing)] text-[var(--bh-content-default)] group-data-[disabled=true]/menu-item:text-[var(--bh-content-disabled)]",
598
+ className
599
+ )}
600
+ {...props}
601
+ />
602
+ )
603
+ }
604
+
605
+ function MenuItemDescription({
606
+ className,
607
+ dir = "auto",
608
+ ...props
609
+ }: React.ComponentProps<"span">) {
610
+ return (
611
+ <span
612
+ data-slot="menu-item-description"
613
+ dir={dir}
614
+ className={cn(
615
+ "min-w-0 truncate text-start text-[length:var(--bh-text-body-2xs-regular-font-size)] font-[var(--bh-text-body-2xs-regular-font-weight)] leading-[var(--bh-text-body-2xs-regular-line-height)] tracking-[var(--bh-text-body-2xs-regular-letter-spacing)] text-[var(--bh-content-subtle)] group-data-[disabled=true]/menu-item:text-[var(--bh-content-disabled)]",
616
+ className
617
+ )}
618
+ {...props}
619
+ />
620
+ )
621
+ }
622
+
623
+ function MenuItemMeta({
624
+ className,
625
+ dir = "auto",
626
+ ...props
627
+ }: React.ComponentProps<"span">) {
628
+ return (
629
+ <span
630
+ data-slot="menu-item-meta"
631
+ dir={dir}
632
+ className={cn(
633
+ "shrink-0 whitespace-nowrap text-end text-[length:var(--bh-text-body-2xs-regular-font-size)] font-[var(--bh-text-body-2xs-regular-font-weight)] leading-[var(--bh-text-body-2xs-regular-line-height)] tracking-[var(--bh-text-body-2xs-regular-letter-spacing)] text-[var(--bh-content-subtle)] group-data-[disabled=true]/menu-item:text-[var(--bh-content-disabled)]",
634
+ className
635
+ )}
636
+ {...props}
637
+ />
638
+ )
639
+ }
640
+
641
+ type MenuItemIconProps = React.ComponentProps<"span"> &
642
+ VariantProps<typeof menuItemIconVariants>
643
+
644
+ function MenuItemIcon({
645
+ className,
646
+ position,
647
+ ...props
648
+ }: MenuItemIconProps) {
649
+ return (
650
+ <span
651
+ aria-hidden="true"
652
+ data-position={position ?? "leading"}
653
+ data-slot="menu-item-icon"
654
+ className={cn(menuItemIconVariants({ position, className }))}
655
+ {...props}
656
+ />
657
+ )
658
+ }
659
+
660
+ type MenuItemAvatarProps = React.ComponentProps<"span"> & {
661
+ alt?: string
662
+ size?: MenuItemAvatarSize
663
+ src?: string
664
+ }
665
+
666
+ function MenuItemAvatar({
667
+ alt = "",
668
+ children,
669
+ className,
670
+ size = "sm",
671
+ src,
672
+ ...props
673
+ }: MenuItemAvatarProps) {
674
+ return (
675
+ <span
676
+ data-size={size}
677
+ data-slot="menu-item-avatar-wrap"
678
+ className={cn(
679
+ "flex shrink-0 items-center ps-[var(--bh-menu-icon-padding)] group-data-[disabled=true]/menu-item:opacity-[var(--bh-opacity-60)]",
680
+ size === "lg"
681
+ ? "h-[var(--bh-space-7xl-40)]"
682
+ : "h-[var(--bh-space-5xl-24)]",
683
+ className
684
+ )}
685
+ {...props}
686
+ >
687
+ <span
688
+ data-slot="menu-item-avatar"
689
+ className={cn(
690
+ "flex shrink-0 items-center justify-center overflow-hidden rounded-[var(--bh-radius-full)] border border-[var(--bh-border-subtle)] bg-[var(--bh-bg-neutral-subtle)] text-[length:var(--bh-text-body-3xs-medium-font-size)] font-[var(--bh-text-body-3xs-medium-font-weight)] leading-[var(--bh-text-body-3xs-medium-line-height)] text-[var(--bh-content-subtle)]",
691
+ size === "lg"
692
+ ? "size-[var(--bh-space-7xl-40)]"
693
+ : "size-[var(--bh-space-5xl-24)]"
694
+ )}
695
+ >
696
+ {src ? (
697
+ <img alt={alt} className="size-full object-cover" src={src} />
698
+ ) : (
699
+ children
700
+ )}
701
+ </span>
702
+ </span>
703
+ )
704
+ }
705
+
706
+ function MenuItemBadge({
707
+ className,
708
+ dir = "auto",
709
+ ...props
710
+ }: React.ComponentProps<"span">) {
711
+ return (
712
+ <span
713
+ data-slot="menu-item-badge"
714
+ dir={dir}
715
+ className={cn(
716
+ "inline-flex h-[var(--bh-menu-badge-height)] shrink-0 items-center justify-center rounded-[var(--bh-radius-full)] bg-[var(--bh-bg-neutral-subtle)] px-[var(--bh-menu-badge-padding-x)] text-center text-[length:var(--bh-text-body-3xs-medium-font-size)] font-[var(--bh-text-body-3xs-medium-font-weight)] leading-[var(--bh-text-body-3xs-medium-line-height)] tracking-[var(--bh-text-body-3xs-medium-letter-spacing)] text-[var(--bh-content-default)] group-data-[disabled=true]/menu-item:opacity-[var(--bh-opacity-60)]",
717
+ className
718
+ )}
719
+ {...props}
720
+ />
721
+ )
722
+ }
723
+
724
+ type MenuItemSwitchProps = Omit<
725
+ React.ComponentProps<"span">,
726
+ "aria-checked" | "aria-label" | "aria-labelledby" | "role"
727
+ > & {
728
+ active?: boolean
729
+ disabled?: boolean
730
+ }
731
+
732
+ function MenuItemSwitch({
733
+ active = false,
734
+ className,
735
+ disabled = false,
736
+ ...props
737
+ }: MenuItemSwitchProps) {
738
+ return (
739
+ <span
740
+ aria-hidden="true"
741
+ data-active={active ? "true" : "false"}
742
+ data-disabled={disabled ? "true" : "false"}
743
+ data-slot="menu-item-switch"
744
+ className={cn(
745
+ "flex h-[var(--bh-menu-switch-height)] w-[var(--bh-menu-switch-width)] shrink-0 items-center rounded-[var(--bh-radius-full)] p-[var(--bh-menu-switch-padding)] group-data-[disabled=true]/menu-item:bg-[var(--bh-interactive-switch-disabled)]",
746
+ active
747
+ ? "justify-end bg-[var(--bh-interactive-switch-active)]"
748
+ : "bg-[var(--bh-interactive-switch-default)]",
749
+ disabled && "bg-[var(--bh-interactive-switch-disabled)]",
750
+ className
751
+ )}
752
+ {...props}
753
+ >
754
+ <span
755
+ aria-hidden="true"
756
+ data-slot="menu-item-switch-handle"
757
+ className="size-[var(--bh-menu-switch-handle-size)] rounded-[var(--bh-radius-full)] bg-[var(--bh-interactive-switch-handle)] group-data-[disabled=true]/menu-item:bg-[var(--bh-interactive-switch-handle-disabled)]"
758
+ />
759
+ </span>
760
+ )
761
+ }
762
+
763
+ type MenuItemActionProps = React.ComponentProps<"button"> & {
764
+ asChild?: boolean
765
+ fullWidth?: boolean
766
+ }
767
+
768
+ function MenuItemAction({
769
+ className,
770
+ asChild = false,
771
+ fullWidth = false,
772
+ type,
773
+ children,
774
+ ...props
775
+ }: MenuItemActionProps) {
776
+ const Comp = asChild ? Slot : "button"
777
+
778
+ return (
779
+ <Comp
780
+ data-slot="menu-item-action"
781
+ className={cn(
782
+ "inline-flex h-[var(--bh-menu-action-height)] shrink-0 items-center justify-center gap-[var(--bh-menu-action-gap)] rounded-[var(--bh-control-default)] border border-[var(--bh-border-default)] bg-[var(--bh-interactive-outlined-default)] px-[var(--bh-menu-action-padding-x)] py-[var(--bh-space-md-8)] text-[length:var(--bh-text-body-md-medium-font-size)] font-[var(--bh-text-body-md-medium-font-weight)] leading-[var(--bh-text-body-md-medium-line-height)] tracking-[var(--bh-text-body-md-medium-letter-spacing)] text-[var(--bh-content-default)] outline-none transition-[background-color,border-color,box-shadow] hover:bg-[var(--bh-interactive-outlined-hover)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:pointer-events-none disabled:text-[var(--bh-content-disabled)] rtl:[&_svg[data-rtl-flip='true']]:-scale-x-100",
783
+ fullWidth && "w-full",
784
+ className
785
+ )}
786
+ {...(!asChild ? { type: type || "button" } : {})}
787
+ {...props}
788
+ >
789
+ {asChild ? children : <MenuItemActionChildren>{children}</MenuItemActionChildren>}
790
+ </Comp>
791
+ )
792
+ }
793
+
794
+ function MenuItemActionChildren({ children }: { children: React.ReactNode }) {
795
+ return React.Children.map(children, (child) => {
796
+ if (typeof child === "string" || typeof child === "number") {
797
+ return (
798
+ <span data-slot="menu-item-action-label" dir="auto" className="min-w-0 px-[var(--bh-space-xs-4)]">
799
+ {child}
800
+ </span>
801
+ )
802
+ }
803
+
804
+ return child
805
+ })
806
+ }
807
+
808
+ type MenuProgressProps = React.ComponentProps<"div"> & {
809
+ indicator?: React.ReactNode
810
+ label?: React.ReactNode
811
+ optional?: React.ReactNode
812
+ showSpinner?: boolean
813
+ value?: number
814
+ }
815
+
816
+ function MenuProgress({
817
+ className,
818
+ indicator,
819
+ label,
820
+ optional,
821
+ showSpinner = true,
822
+ value = 50,
823
+ ...props
824
+ }: MenuProgressProps) {
825
+ const clampedValue = Math.min(100, Math.max(0, value))
826
+ const labelId = React.useId()
827
+ const hasLabel = hasRenderableContent(label)
828
+
829
+ return (
830
+ <div
831
+ data-slot="menu-progress"
832
+ className={cn(
833
+ "flex w-full flex-col gap-[var(--bh-menu-progress-gap)]",
834
+ className
835
+ )}
836
+ {...props}
837
+ >
838
+ <div
839
+ aria-label={hasLabel ? undefined : "Progress"}
840
+ aria-labelledby={hasLabel ? labelId : undefined}
841
+ aria-valuemax={100}
842
+ aria-valuemin={0}
843
+ aria-valuenow={clampedValue}
844
+ data-slot="menu-progress-track"
845
+ role="progressbar"
846
+ className="h-[var(--bh-menu-progress-height)] w-full overflow-hidden rounded-[var(--bh-radius-full)] bg-[var(--bh-bg-neutral-soft)]"
847
+ >
848
+ <span
849
+ data-slot="menu-progress-fill"
850
+ className="block h-full rounded-[var(--bh-radius-full)] bg-[var(--bh-interactive-indicator-default)]"
851
+ style={{ inlineSize: `${clampedValue}%` }}
852
+ />
853
+ </div>
854
+
855
+ {hasRenderableContent(label) ||
856
+ hasRenderableContent(optional) ||
857
+ hasRenderableContent(indicator) ||
858
+ showSpinner ? (
859
+ <div
860
+ data-slot="menu-progress-row"
861
+ className="flex min-w-0 items-center gap-[var(--bh-menu-item-slot-gap)]"
862
+ >
863
+ {hasRenderableContent(label) || hasRenderableContent(optional) ? (
864
+ <span
865
+ data-slot="menu-progress-label"
866
+ className="flex min-w-0 flex-1 items-center gap-[var(--bh-space-xs-4)]"
867
+ >
868
+ {hasRenderableContent(label) ? (
869
+ <span
870
+ id={labelId}
871
+ data-slot="menu-progress-label-text"
872
+ dir="auto"
873
+ className="truncate text-[length:var(--bh-text-body-xs-medium-font-size)] font-[var(--bh-text-body-xs-medium-font-weight)] leading-[var(--bh-text-body-xs-medium-line-height)] tracking-[var(--bh-text-body-xs-medium-letter-spacing)] text-[var(--bh-content-default)]"
874
+ >
875
+ {label}
876
+ </span>
877
+ ) : null}
878
+ {hasRenderableContent(optional) ? (
879
+ <span
880
+ data-slot="menu-progress-optional"
881
+ dir="auto"
882
+ className="truncate text-[length:var(--bh-text-body-xs-regular-font-size)] font-[var(--bh-text-body-xs-regular-font-weight)] leading-[var(--bh-text-body-xs-regular-line-height)] tracking-[var(--bh-text-body-xs-regular-letter-spacing)] text-[var(--bh-content-subtle)]"
883
+ >
884
+ {optional}
885
+ </span>
886
+ ) : null}
887
+ </span>
888
+ ) : null}
889
+
890
+ <span
891
+ data-slot="menu-progress-indicator"
892
+ className="flex shrink-0 items-center gap-[var(--bh-space-xxs-2)] text-[length:var(--bh-text-body-2xs-regular-font-size)] font-[var(--bh-text-body-2xs-regular-font-weight)] leading-[var(--bh-text-body-2xs-regular-line-height)] tracking-[var(--bh-text-body-2xs-regular-letter-spacing)] text-[var(--bh-content-subtle)]"
893
+ >
894
+ {hasRenderableContent(indicator) ? (
895
+ <span dir="auto">{indicator}</span>
896
+ ) : null}
897
+ {showSpinner ? <MenuProgressSpinner /> : null}
898
+ </span>
899
+ </div>
900
+ ) : null}
901
+ </div>
902
+ )
903
+ }
904
+
905
+ function MenuProgressSpinner({ className }: { className?: string }) {
906
+ return (
907
+ <svg
908
+ aria-hidden="true"
909
+ data-slot="menu-progress-spinner"
910
+ viewBox="0 0 24 24"
911
+ className={cn("size-[var(--bh-menu-trailing-icon-size)] animate-spin", className)}
912
+ >
913
+ <path
914
+ d="M12 3a9 9 0 1 1-6.36 2.64"
915
+ fill="none"
916
+ stroke="currentColor"
917
+ strokeLinecap="round"
918
+ strokeWidth="var(--bh-icon-stroke-300)"
919
+ />
920
+ </svg>
921
+ )
922
+ }
923
+
924
+ type MenuLabelProps = React.ComponentProps<"div"> & {
925
+ caption?: React.ReactNode
926
+ label?: React.ReactNode
927
+ }
928
+
929
+ function MenuLabel({
930
+ caption,
931
+ children,
932
+ className,
933
+ label,
934
+ role = "presentation",
935
+ ...props
936
+ }: MenuLabelProps) {
937
+ const isStaticMenu = React.useContext(MenuStaticContext)
938
+ const labelContent = (
939
+ <div
940
+ data-slot="menu-label"
941
+ role={role}
942
+ className={cn(
943
+ "flex h-[var(--bh-menu-item-label-height)] w-full items-center gap-0 bg-[var(--bh-menu-item-bg)] px-[var(--bh-menu-label-padding-x)] py-[var(--bh-space-xs-4)] text-[length:var(--bh-text-body-xs-regular-font-size)] leading-[var(--bh-text-body-xs-regular-line-height)] tracking-[var(--bh-text-body-xs-regular-letter-spacing)]",
944
+ className
945
+ )}
946
+ {...props}
947
+ >
948
+ {children ?? (
949
+ <>
950
+ {hasRenderableContent(label) ? (
951
+ <span
952
+ data-slot="menu-label-primary"
953
+ dir="auto"
954
+ className="min-w-0 flex-1 truncate text-start font-[var(--bh-text-body-xs-medium-font-weight)] text-[var(--bh-content-default)]"
955
+ >
956
+ {label}
957
+ </span>
958
+ ) : null}
959
+ {hasRenderableContent(caption) ? (
960
+ <span
961
+ data-slot="menu-label-caption"
962
+ dir="auto"
963
+ className="shrink-0 whitespace-nowrap text-end font-[var(--bh-text-body-xs-regular-font-weight)] text-[var(--bh-content-subtle)]"
964
+ >
965
+ {caption}
966
+ </span>
967
+ ) : null}
968
+ </>
969
+ )}
970
+ </div>
971
+ )
972
+
973
+ if (isStaticMenu) return labelContent
974
+
975
+ return <DropdownMenuPrimitive.Label asChild>{labelContent}</DropdownMenuPrimitive.Label>
976
+ }
977
+
978
+ function hasRenderableContent(content: React.ReactNode) {
979
+ return (
980
+ content !== undefined &&
981
+ content !== null &&
982
+ content !== false &&
983
+ content !== ""
984
+ )
985
+ }
986
+
987
+ function MenuCaption({
988
+ className,
989
+ dir = "auto",
990
+ role = "presentation",
991
+ ...props
992
+ }: React.ComponentProps<"div">) {
993
+ return (
994
+ <div
995
+ data-slot="menu-caption"
996
+ role={role}
997
+ className="flex h-[var(--bh-menu-item-caption-height)] w-full items-center px-[var(--bh-menu-item-padding-x)]"
998
+ >
999
+ <span
1000
+ dir={dir}
1001
+ className={cn(
1002
+ "flex h-full min-w-0 flex-1 items-center justify-center px-[var(--bh-menu-item-content-padding)] py-[var(--bh-menu-item-content-compact-padding-y)] text-center text-[length:var(--bh-text-body-xs-regular-font-size)] font-[var(--bh-text-body-xs-regular-font-weight)] leading-[var(--bh-text-body-xs-regular-line-height)] tracking-[var(--bh-text-body-xs-regular-letter-spacing)] text-[var(--bh-content-subtle)]",
1003
+ className
1004
+ )}
1005
+ {...props}
1006
+ />
1007
+ </div>
1008
+ )
1009
+ }
1010
+
1011
+ function MenuSeparator({
1012
+ className,
1013
+ role = "separator",
1014
+ ...props
1015
+ }: React.ComponentProps<"div">) {
1016
+ const isStaticMenu = React.useContext(MenuStaticContext)
1017
+ const separator = (
1018
+ <div
1019
+ data-slot="menu-separator"
1020
+ role={role}
1021
+ className={cn(
1022
+ "flex h-[var(--bh-menu-item-divider-height)] w-full items-center py-[var(--bh-menu-divider-padding-y)]",
1023
+ className
1024
+ )}
1025
+ {...props}
1026
+ >
1027
+ <span className="block w-full border-t border-[var(--bh-border-subtle)]" />
1028
+ </div>
1029
+ )
1030
+
1031
+ if (isStaticMenu) return separator
1032
+
1033
+ return <DropdownMenuPrimitive.Separator asChild>{separator}</DropdownMenuPrimitive.Separator>
1034
+ }
1035
+
1036
+ export {
1037
+ Menu,
1038
+ MenuCaption,
1039
+ MenuContent,
1040
+ MenuItem,
1041
+ MenuItemAction,
1042
+ MenuItemAvatar,
1043
+ MenuItemBadge,
1044
+ MenuItemContent,
1045
+ MenuItemDescription,
1046
+ MenuItemIcon,
1047
+ MenuItemMeta,
1048
+ MenuItemSwitch,
1049
+ MenuItemText,
1050
+ MenuItemTitle,
1051
+ MenuLabel,
1052
+ MenuProgress,
1053
+ MenuPreview,
1054
+ MenuPortal,
1055
+ MenuRadioGroup,
1056
+ MenuRadioItem,
1057
+ MenuRoot,
1058
+ MenuSeparator,
1059
+ MenuSub,
1060
+ MenuSubContent,
1061
+ MenuSubTrigger,
1062
+ MenuSurface,
1063
+ MenuTrigger,
1064
+ menuItemContentVariants,
1065
+ menuItemIconVariants,
1066
+ menuItemVariants,
1067
+ menuVariants,
1068
+ }
1069
+ export type {
1070
+ MenuItemAvatarSize,
1071
+ MenuItemAvatarProps,
1072
+ MenuItemActionProps,
1073
+ MenuContentProps,
1074
+ MenuItemContentProps,
1075
+ MenuItemIconPosition,
1076
+ MenuItemIconProps,
1077
+ MenuItemKind,
1078
+ MenuItemProps,
1079
+ MenuSubContentProps,
1080
+ MenuSubTriggerProps,
1081
+ MenuItemSwitchProps,
1082
+ MenuItemState,
1083
+ MenuLabelProps,
1084
+ MenuProgressProps,
1085
+ MenuProps,
1086
+ MenuPreviewProps,
1087
+ MenuRadioGroupProps,
1088
+ MenuRadioItemProps,
1089
+ MenuSurfaceProps,
1090
+ MenuWidth,
1091
+ }