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