banhaten 0.3.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 -73
  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 -244
  16. package/docs/design-system/component-quality-audit-2026-08-20.md +225 -223
  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 +98 -98
  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 +89 -89
  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 +177 -176
  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 -79
  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 +117 -61
  55. package/docs/design-system/components/item.md +59 -57
  56. package/docs/design-system/components/kbd.md +54 -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 +127 -76
  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 -61
  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 -69
  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 -60
  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 -448
  130. package/registry/components/autocomplete.tsx +598 -651
  131. package/registry/components/avatar.tsx +385 -381
  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 -1277
  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 -59
  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 -255
  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 -126
  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 -603
  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 -972
  193. package/registry/components/item.tsx +189 -189
  194. package/registry/components/kbd.tsx +246 -151
  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 -555
  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 -677
  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 -358
  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 -236
  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 -11
  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 -57
  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 +1634 -184
  325. package/registry/styles/globals.css +1462 -1570
  326. package/registry.json +364 -10
  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 +458 -224
@@ -1,358 +1,1433 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { Slot } from "@radix-ui/react-slot"
5
- import { PanelLeftIcon } from "lucide-react"
6
-
7
- import { cn } from "@/lib/utils"
8
-
9
- type SidebarContextValue = {
10
- collapsed: boolean
11
- dir?: "ltr" | "rtl"
12
- setCollapsed: (collapsed: boolean) => void
13
- toggle: () => void
14
- }
15
-
16
- type SidebarProviderProps = React.ComponentProps<"div"> & {
17
- collapsed?: boolean
18
- defaultCollapsed?: boolean
19
- dir?: "ltr" | "rtl"
20
- onCollapsedChange?: (collapsed: boolean) => void
21
- }
22
- type SidebarProps = React.ComponentProps<"aside"> & {
23
- side?: "start" | "end"
24
- }
25
- type SidebarSectionProps = React.ComponentProps<"div">
26
- type SidebarGroupLabelProps = React.ComponentProps<"div">
27
- type SidebarMenuProps = React.ComponentProps<"ul">
28
- type SidebarMenuItemProps = React.ComponentProps<"li">
29
- type SidebarMenuButtonProps = React.ComponentProps<"button"> & {
30
- active?: boolean
31
- asChild?: boolean
32
- icon?: React.ReactNode
33
- }
34
- type SidebarToggleProps = React.ComponentProps<"button">
35
- type SidebarTriggerProps = SidebarToggleProps
36
- type SidebarRailProps = React.ComponentProps<"button">
37
- type SidebarInsetProps = React.ComponentProps<"div"> & {
38
- asChild?: boolean
39
- }
40
-
41
- const SidebarContext = React.createContext<SidebarContextValue | null>(null)
42
-
43
- function SidebarProvider({
44
- children,
45
- className,
46
- collapsed,
47
- defaultCollapsed = false,
48
- dir,
49
- onCollapsedChange,
50
- ...props
51
- }: SidebarProviderProps) {
52
- const [uncontrolledCollapsed, setUncontrolledCollapsed] =
53
- React.useState(defaultCollapsed)
54
- const isControlled = collapsed !== undefined
55
- const resolvedCollapsed = collapsed ?? uncontrolledCollapsed
56
-
57
- const setCollapsed = React.useCallback(
58
- (nextCollapsed: boolean) => {
59
- if (!isControlled) {
60
- setUncontrolledCollapsed(nextCollapsed)
61
- }
62
- onCollapsedChange?.(nextCollapsed)
63
- },
64
- [isControlled, onCollapsedChange]
65
- )
66
-
67
- const context = React.useMemo<SidebarContextValue>(
68
- () => ({
69
- collapsed: resolvedCollapsed,
70
- dir,
71
- setCollapsed,
72
- toggle: () => setCollapsed(!resolvedCollapsed),
73
- }),
74
- [dir, resolvedCollapsed, setCollapsed]
75
- )
76
-
77
- return (
78
- <SidebarContext.Provider value={context}>
79
- <div
80
- data-collapsed={resolvedCollapsed ? "true" : undefined}
81
- data-slot="sidebar-provider"
82
- dir={dir}
83
- className={cn("min-w-0", className)}
84
- {...props}
85
- >
86
- {children}
87
- </div>
88
- </SidebarContext.Provider>
89
- )
90
- }
91
-
92
- const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(function Sidebar(
93
- { className, side = "start", ...props },
94
- ref
95
- ) {
96
- const context = React.useContext(SidebarContext)
97
- const collapsed = context?.collapsed ?? false
98
- const dir = context?.dir
99
-
100
- return (
101
- <aside
102
- data-collapsed={collapsed ? "true" : undefined}
103
- data-side={side}
104
- data-slot="sidebar"
105
- dir={dir}
106
- ref={ref}
107
- className={cn(
108
- "group/sidebar flex h-full min-h-[var(--bh-space-19xl-384)] w-[var(--bh-sidebar-width)] max-w-full shrink-0 flex-col overflow-hidden bg-[var(--bh-sidebar-bg)] text-[var(--bh-content-default)] shadow-[var(--shadow-sidebar)] transition-[width,background-color]",
109
- "data-[collapsed=true]:w-[var(--bh-sidebar-width-collapsed)] data-[collapsed=true]:bg-[var(--bh-sidebar-bg-collapsed)]",
110
- side === "end" && "ms-auto",
111
- className
112
- )}
113
- {...props}
114
- />
115
- )
116
- })
117
-
118
- function SidebarHeader({ className, ...props }: SidebarSectionProps) {
119
- return (
120
- <div
121
- data-slot="sidebar-header"
122
- className={cn(
123
- "flex min-h-[var(--bh-sidebar-item-height)] shrink-0 items-center gap-[var(--bh-sidebar-item-gap)] px-[var(--bh-sidebar-padding)] py-[var(--bh-space-xl-12)]",
124
- className
125
- )}
126
- {...props}
127
- />
128
- )
129
- }
130
-
131
- function SidebarBody({ className, ...props }: SidebarSectionProps) {
132
- return (
133
- <div
134
- data-slot="sidebar-body"
135
- className={cn(
136
- "min-h-0 flex-1 overflow-auto px-[var(--bh-sidebar-padding)] py-[var(--bh-space-xl-12)]",
137
- className
138
- )}
139
- {...props}
140
- />
141
- )
142
- }
143
-
144
- function SidebarFooter({ className, ...props }: SidebarSectionProps) {
145
- return (
146
- <div
147
- data-slot="sidebar-footer"
148
- className={cn(
149
- "shrink-0 border-t border-[var(--bh-sidebar-border)] px-[var(--bh-sidebar-padding)] py-[var(--bh-space-xl-12)]",
150
- className
151
- )}
152
- {...props}
153
- />
154
- )
155
- }
156
-
157
- function SidebarGroup({ className, ...props }: SidebarSectionProps) {
158
- return (
159
- <div
160
- data-slot="sidebar-group"
161
- className={cn("grid gap-[var(--bh-space-md-8)]", className)}
162
- {...props}
163
- />
164
- )
165
- }
166
-
167
- function SidebarGroupLabel({ className, ...props }: SidebarGroupLabelProps) {
168
- const { collapsed } = useSidebarContext()
169
-
170
- if (collapsed) return null
171
-
172
- return (
173
- <div
174
- data-slot="sidebar-group-label"
175
- dir="auto"
176
- className={cn(
177
- "px-[var(--bh-sidebar-item-padding-x)] text-[length:var(--bh-text-body-xs-medium-font-size)] font-[var(--bh-text-body-xs-medium-font-weight)] uppercase leading-[var(--bh-text-body-xs-medium-line-height)] tracking-[var(--bh-text-body-xs-medium-letter-spacing)] text-[var(--bh-content-muted)]",
178
- className
179
- )}
180
- {...props}
181
- />
182
- )
183
- }
184
-
185
- function SidebarMenu({ className, ...props }: SidebarMenuProps) {
186
- return (
187
- <ul
188
- data-slot="sidebar-menu"
189
- className={cn("m-0 grid list-none gap-[var(--bh-space-xs-4)] p-0", className)}
190
- {...props}
191
- />
192
- )
193
- }
194
-
195
- function SidebarMenuItem({ className, ...props }: SidebarMenuItemProps) {
196
- return (
197
- <li
198
- data-slot="sidebar-menu-item"
199
- className={cn("min-w-0", className)}
200
- {...props}
201
- />
202
- )
203
- }
204
-
205
- const SidebarMenuButton = React.forwardRef<
206
- HTMLButtonElement,
207
- SidebarMenuButtonProps
208
- >(function SidebarMenuButton(
209
- { active = false, asChild = false, children, className, icon, ...props },
210
- ref
211
- ) {
212
- const { collapsed } = useSidebarContext()
213
- const Comp = asChild ? Slot : "button"
214
-
215
- return (
216
- <Comp
217
- aria-current={active ? "page" : undefined}
218
- data-active={active ? "true" : undefined}
219
- data-slot="sidebar-menu-button"
220
- ref={ref}
221
- className={cn(
222
- "flex h-[var(--bh-sidebar-item-height)] w-full min-w-0 items-center gap-[var(--bh-sidebar-item-gap)] rounded-[var(--bh-radius-lg-8)] px-[var(--bh-sidebar-item-padding-x)] text-start text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)] text-[var(--bh-sidebar-item-content)] outline-none transition-[background-color,box-shadow,color]",
223
- "hover:bg-[var(--bh-sidebar-item-bg-hover)] focus-visible:shadow-[var(--shadow-button-focus)] data-[active=true]:bg-[var(--bh-sidebar-item-bg-active)] data-[active=true]:text-[var(--bh-sidebar-item-content-active)]",
224
- collapsed && "justify-center px-[var(--bh-space-md-8)]",
225
- className
226
- )}
227
- {...props}
228
- >
229
- {icon ? (
230
- <span
231
- aria-hidden="true"
232
- data-slot="sidebar-menu-icon"
233
- className="inline-flex size-[var(--bh-sidebar-icon-size)] shrink-0 items-center justify-center"
234
- >
235
- {icon}
236
- </span>
237
- ) : null}
238
- {collapsed ? null : (
239
- <span data-slot="sidebar-menu-label" dir="auto" className="min-w-0 flex-1 truncate">
240
- {children}
241
- </span>
242
- )}
243
- </Comp>
244
- )
245
- })
246
-
247
- const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>(
248
- function SidebarToggle({ children, className, onClick, type = "button", ...props }, ref) {
249
- const { collapsed, toggle } = useSidebarContext()
250
-
251
- return (
252
- <button
253
- aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
254
- aria-pressed={collapsed}
255
- data-slot="sidebar-toggle"
256
- ref={ref}
257
- type={type}
258
- className={cn(
259
- "inline-flex size-[var(--bh-sidebar-item-height)] items-center justify-center rounded-[var(--bh-radius-lg-8)] text-[var(--bh-content-subtle)] outline-none transition-[background-color,box-shadow,color] hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:shadow-[var(--shadow-button-focus)]",
260
- className
261
- )}
262
- onClick={(event) => {
263
- onClick?.(event)
264
- if (!event.defaultPrevented) toggle()
265
- }}
266
- {...props}
267
- >
268
- {children ?? (
269
- <PanelLeftIcon
270
- aria-hidden="true"
271
- className="size-[var(--bh-space-5xl-24)] rtl:-scale-x-100"
272
- strokeWidth="var(--bh-icon-stroke-200)"
273
- />
274
- )}
275
- </button>
276
- )
277
- }
278
- )
279
-
280
- const SidebarTrigger = SidebarToggle
281
-
282
- const SidebarRail = React.forwardRef<HTMLButtonElement, SidebarRailProps>(
283
- function SidebarRail({ className, onClick, type = "button", ...props }, ref) {
284
- const { toggle } = useSidebarContext()
285
-
286
- return (
287
- <button
288
- aria-label="Toggle sidebar"
289
- data-slot="sidebar-rail"
290
- ref={ref}
291
- type={type}
292
- className={cn(
293
- "absolute inset-y-0 end-[calc(var(--bh-sidebar-rail-width)*-1)] hidden w-[var(--bh-sidebar-rail-width)] cursor-ew-resize bg-transparent outline-none lg:block",
294
- className
295
- )}
296
- onClick={(event) => {
297
- onClick?.(event)
298
- if (!event.defaultPrevented) toggle()
299
- }}
300
- {...props}
301
- />
302
- )
303
- }
304
- )
305
-
306
- const SidebarInset = React.forwardRef<HTMLDivElement, SidebarInsetProps>(
307
- function SidebarInset({ asChild = false, className, ...props }, ref) {
308
- const Comp = asChild ? Slot : "div"
309
-
310
- return (
311
- <Comp
312
- data-slot="sidebar-inset"
313
- ref={ref}
314
- className={cn("min-w-0 flex-1", className)}
315
- {...props}
316
- />
317
- )
318
- }
319
- )
320
-
321
- function useSidebarContext() {
322
- const context = React.useContext(SidebarContext)
323
- if (!context) {
324
- throw new Error("Sidebar components must be used within SidebarProvider")
325
- }
326
-
327
- return context
328
- }
329
-
330
- export {
331
- Sidebar,
332
- SidebarBody,
333
- SidebarFooter,
334
- SidebarGroup,
335
- SidebarGroupLabel,
336
- SidebarHeader,
337
- SidebarInset,
338
- SidebarMenu,
339
- SidebarMenuButton,
340
- SidebarMenuItem,
341
- SidebarProvider,
342
- SidebarRail,
343
- SidebarTrigger,
344
- SidebarToggle,
345
- }
346
- export type {
347
- SidebarGroupLabelProps,
348
- SidebarInsetProps,
349
- SidebarMenuButtonProps,
350
- SidebarMenuItemProps,
351
- SidebarMenuProps,
352
- SidebarProps,
353
- SidebarProviderProps,
354
- SidebarRailProps,
355
- SidebarTriggerProps,
356
- SidebarSectionProps,
357
- SidebarToggleProps,
358
- }
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Slot } from "@radix-ui/react-slot"
5
+ import {
6
+ Building2Icon,
7
+ ChevronDownIcon,
8
+ ChevronRightIcon,
9
+ ChevronsUpDownIcon,
10
+ FolderIcon,
11
+ MoreHorizontalIcon,
12
+ PanelLeftIcon,
13
+ SearchIcon,
14
+ SparklesIcon,
15
+ UserIcon,
16
+ } from "lucide-react"
17
+
18
+ import {
19
+ Tooltip,
20
+ TooltipContent,
21
+ TooltipProvider,
22
+ TooltipTrigger,
23
+ } from "@/components/ui/tooltip"
24
+ import { cn } from "@/lib/utils"
25
+
26
+ type SidebarContextValue = {
27
+ collapsed: boolean
28
+ dir?: "ltr" | "rtl"
29
+ setCollapsed: (collapsed: boolean) => void
30
+ toggle: () => void
31
+ }
32
+
33
+ type SidebarProviderProps = React.ComponentProps<"div"> & {
34
+ collapsed?: boolean
35
+ defaultCollapsed?: boolean
36
+ dir?: "ltr" | "rtl"
37
+ onCollapsedChange?: (collapsed: boolean) => void
38
+ }
39
+ type SidebarProps = React.ComponentProps<"aside"> & {
40
+ side?: "start" | "end"
41
+ }
42
+ type SidebarSectionProps = React.ComponentProps<"div">
43
+ type SidebarHeaderActionProps = React.ComponentProps<"button"> & {
44
+ label?: string
45
+ }
46
+ type SidebarHeaderBrandProps = React.ComponentProps<"div"> & {
47
+ brand?: React.ReactNode
48
+ collapseAction?: React.ReactNode | false
49
+ logo?: React.ReactNode
50
+ onSearchClick?: React.MouseEventHandler<HTMLButtonElement>
51
+ searchAction?: React.ReactNode | false
52
+ searchLabel?: string
53
+ }
54
+ type SidebarWorkspaceVariant = "actions" | "card"
55
+ type SidebarHeaderWorkspaceSwitchProps = React.ComponentProps<"div"> & {
56
+ collapseAction?: React.ReactNode | false
57
+ image?: React.ReactNode
58
+ label?: React.ReactNode
59
+ onSearchClick?: React.MouseEventHandler<HTMLButtonElement>
60
+ onWorkspaceClick?: React.MouseEventHandler<HTMLButtonElement>
61
+ searchAction?: React.ReactNode | false
62
+ searchLabel?: string
63
+ supportText?: React.ReactNode
64
+ variant?: SidebarWorkspaceVariant
65
+ workspaceLabel?: string
66
+ }
67
+ type SidebarFooterUserProps = React.ComponentProps<"button"> & {
68
+ avatar?: React.ReactNode
69
+ name?: React.ReactNode
70
+ showIdentityPreview?: boolean
71
+ supportText?: React.ReactNode
72
+ trailingIcon?: React.ReactNode
73
+ variant?: "default" | "supporting-text"
74
+ }
75
+ type SidebarGroupLabelProps = React.ComponentProps<"div"> & {
76
+ actions?: React.ReactNode
77
+ expandIcon?: React.ReactNode
78
+ expandable?: boolean
79
+ }
80
+ type SidebarGroupActionProps = React.ComponentProps<"button"> & {
81
+ label?: string
82
+ }
83
+ type SidebarMenuProps = React.ComponentProps<"ul">
84
+ type SidebarMenuItemProps = React.ComponentProps<"li">
85
+ type SidebarInteractionState = "default" | "hover" | "active" | "focus"
86
+ type SidebarMenuButtonVariant =
87
+ | "default"
88
+ | "avatar"
89
+ | "level2"
90
+ | "level2-line"
91
+ type SidebarMenuButtonProps = React.ComponentProps<"button"> & {
92
+ active?: boolean
93
+ asChild?: boolean
94
+ badge?: React.ReactNode
95
+ icon?: React.ReactNode
96
+ state?: SidebarInteractionState
97
+ supportText?: React.ReactNode
98
+ tooltip?: React.ReactNode
99
+ trailingIcon?: React.ReactNode
100
+ variant?: SidebarMenuButtonVariant
101
+ }
102
+ type SidebarMenuBadgeProps = React.ComponentProps<"span">
103
+ type SidebarMenuSubProps = React.ComponentProps<"ul">
104
+ type SidebarMenuSubButtonProps = Omit<SidebarMenuButtonProps, "variant"> & {
105
+ showLine?: boolean
106
+ }
107
+ type SidebarMenuTreeItemProps = React.ComponentProps<"button"> & {
108
+ active?: boolean
109
+ asChild?: boolean
110
+ expanded?: boolean
111
+ icon?: React.ReactNode
112
+ level?: 1 | 2 | 3 | 4
113
+ showChevron?: boolean
114
+ state?: SidebarInteractionState
115
+ }
116
+ type SidebarTreeGroupProps = SidebarSectionProps & {
117
+ actions?: React.ReactNode
118
+ expandable?: boolean
119
+ expandIcon?: React.ReactNode
120
+ label?: React.ReactNode
121
+ }
122
+ type SidebarToggleProps = React.ComponentProps<"button">
123
+ type SidebarTriggerProps = SidebarToggleProps
124
+ type SidebarRailProps = React.ComponentProps<"button">
125
+ type SidebarInsetProps = React.ComponentProps<"div"> & {
126
+ asChild?: boolean
127
+ }
128
+
129
+ const SidebarContext = React.createContext<SidebarContextValue | null>(null)
130
+
131
+ /** Owns the controlled or uncontrolled collapsed state for a sidebar composition. */
132
+ function SidebarProvider({
133
+ children,
134
+ className,
135
+ collapsed,
136
+ defaultCollapsed = false,
137
+ dir,
138
+ onCollapsedChange,
139
+ ...props
140
+ }: SidebarProviderProps) {
141
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] =
142
+ React.useState(defaultCollapsed)
143
+ const isControlled = collapsed !== undefined
144
+ const resolvedCollapsed = collapsed ?? uncontrolledCollapsed
145
+
146
+ const setCollapsed = React.useCallback(
147
+ (nextCollapsed: boolean) => {
148
+ if (!isControlled) {
149
+ setUncontrolledCollapsed(nextCollapsed)
150
+ }
151
+ onCollapsedChange?.(nextCollapsed)
152
+ },
153
+ [isControlled, onCollapsedChange]
154
+ )
155
+
156
+ const context = React.useMemo<SidebarContextValue>(
157
+ () => ({
158
+ collapsed: resolvedCollapsed,
159
+ dir,
160
+ setCollapsed,
161
+ toggle: () => setCollapsed(!resolvedCollapsed),
162
+ }),
163
+ [dir, resolvedCollapsed, setCollapsed]
164
+ )
165
+
166
+ return (
167
+ <SidebarContext.Provider value={context}>
168
+ <TooltipProvider delayDuration={0}>
169
+ <div
170
+ data-collapsed={resolvedCollapsed ? "true" : "false"}
171
+ data-slot="sidebar-provider"
172
+ dir={dir}
173
+ className={cn("relative min-w-0", className)}
174
+ {...props}
175
+ >
176
+ {children}
177
+ </div>
178
+ </TooltipProvider>
179
+ </SidebarContext.Provider>
180
+ )
181
+ }
182
+
183
+ /** The 280px navigation surface with a 900px minimum height that contracts to the Figma 48px icon rail. */
184
+ const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(function Sidebar(
185
+ { className, onTransitionEnd, side = "start", ...props },
186
+ ref
187
+ ) {
188
+ const context = React.useContext(SidebarContext)
189
+ const collapsed = context?.collapsed ?? false
190
+ const dir = context?.dir
191
+ const [overflowVisible, setOverflowVisible] = React.useState(collapsed)
192
+ const previousCollapsed = React.useRef(collapsed)
193
+
194
+ React.useEffect(() => {
195
+ if (previousCollapsed.current === collapsed) return
196
+ previousCollapsed.current = collapsed
197
+
198
+ if (!collapsed) {
199
+ setOverflowVisible(false)
200
+ return
201
+ }
202
+
203
+ const prefersReducedMotion =
204
+ typeof window !== "undefined" &&
205
+ typeof window.matchMedia === "function" &&
206
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches
207
+
208
+ setOverflowVisible(prefersReducedMotion)
209
+ }, [collapsed])
210
+
211
+ return (
212
+ <aside
213
+ data-collapsed={collapsed ? "true" : "false"}
214
+ data-overflow-visible={overflowVisible ? "true" : "false"}
215
+ data-side={side}
216
+ data-slot="sidebar"
217
+ dir={dir}
218
+ ref={ref}
219
+ className={cn(
220
+ "group/sidebar flex h-full min-h-[var(--bh-sidebar-min-height)] w-[var(--bh-sidebar-width)] max-w-full shrink-0 flex-col overflow-hidden bg-[var(--bh-sidebar-bg)] text-[var(--bh-content-default)] transition-[width,background-color] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] will-change-[width] motion-reduce:transition-none",
221
+ "border-e border-[var(--bh-sidebar-border)]",
222
+ "data-[collapsed=true]:w-[var(--bh-sidebar-width-collapsed)] data-[collapsed=true]:bg-[var(--bh-sidebar-bg-collapsed)]",
223
+ collapsed && overflowVisible && "overflow-visible",
224
+ side === "end" && "ms-auto border-e-0 border-s",
225
+ className
226
+ )}
227
+ onTransitionEnd={(event) => {
228
+ onTransitionEnd?.(event)
229
+ if (
230
+ !event.defaultPrevented &&
231
+ event.currentTarget === event.target &&
232
+ event.propertyName === "width"
233
+ ) {
234
+ setOverflowVisible(collapsed)
235
+ }
236
+ }}
237
+ {...props}
238
+ />
239
+ )
240
+ })
241
+
242
+ /** Header slot with the Figma 10px expanded and 4px/8px collapsed insets. */
243
+ function SidebarHeader({ className, ...props }: SidebarSectionProps) {
244
+ return (
245
+ <div
246
+ data-slot="sidebar-header"
247
+ className={cn(
248
+ "flex shrink-0 flex-col items-center gap-[var(--bh-space-lg-10)] overflow-hidden bg-[var(--bh-sidebar-bg)] p-[var(--bh-sidebar-padding)] transition-[padding] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
249
+ "group-data-[collapsed=true]/sidebar:px-[var(--bh-space-xs-4)] group-data-[collapsed=true]/sidebar:py-[var(--bh-space-md-8)]",
250
+ className
251
+ )}
252
+ {...props}
253
+ />
254
+ )
255
+ }
256
+
257
+ /** Compact 28px ghost action used by Figma sidebar headers. */
258
+ const SidebarHeaderAction = React.forwardRef<
259
+ HTMLButtonElement,
260
+ SidebarHeaderActionProps
261
+ >(function SidebarHeaderAction(
262
+ { "aria-label": ariaLabel, children, className, label, type = "button", ...props },
263
+ ref
264
+ ) {
265
+ return (
266
+ <button
267
+ aria-label={ariaLabel ?? label ?? "Sidebar action"}
268
+ data-slot="sidebar-header-action"
269
+ ref={ref}
270
+ type={type}
271
+ className={cn(
272
+ "inline-flex size-[var(--bh-sidebar-header-action-size)] shrink-0 items-center justify-center rounded-[var(--bh-radius-full)] border border-transparent bg-[var(--bh-interactive-ghost-default)] text-[var(--bh-content-default)] outline-none transition-[background-color,border-color,box-shadow,color]",
273
+ "hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:border-[var(--bh-border-focus)] focus-visible:shadow-[var(--elevation-button-focus)]",
274
+ className
275
+ )}
276
+ {...props}
277
+ >
278
+ <span
279
+ aria-hidden="true"
280
+ data-slot="sidebar-header-action-icon"
281
+ className="inline-flex size-[var(--bh-sidebar-icon-slot-size)] items-center justify-center [&_svg]:size-[var(--bh-sidebar-icon-size)] [&_svg]:shrink-0"
282
+ >
283
+ {children}
284
+ </span>
285
+ </button>
286
+ )
287
+ })
288
+
289
+ /** Brand header with optional search and collapse actions; actions disappear on the icon rail. */
290
+ function SidebarHeaderBrand({
291
+ brand = "Banhaten",
292
+ className,
293
+ collapseAction,
294
+ logo,
295
+ onSearchClick,
296
+ searchAction,
297
+ searchLabel = "Search",
298
+ ...props
299
+ }: SidebarHeaderBrandProps) {
300
+ const { collapsed } = useSidebarContext()
301
+ const hasCollapseAction = collapseAction !== false
302
+
303
+ return (
304
+ <div
305
+ data-collapsed={collapsed ? "true" : "false"}
306
+ data-slot="sidebar-header-brand"
307
+ className={cn(
308
+ "relative h-[var(--bh-sidebar-brand-height)] w-full transition-[width,height] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
309
+ collapsed && hasCollapseAction &&
310
+ "h-[calc(var(--bh-sidebar-header-action-size)+var(--bh-space-sm-6)+var(--bh-sidebar-header-action-size))] w-[var(--bh-sidebar-compact-control-size)]",
311
+ collapsed && !hasCollapseAction &&
312
+ "h-[var(--bh-sidebar-header-action-size)] w-[var(--bh-sidebar-compact-control-size)]",
313
+ className
314
+ )}
315
+ {...props}
316
+ >
317
+ <span
318
+ data-slot="sidebar-brand-content"
319
+ className={cn(
320
+ "absolute start-[var(--bh-space-sm-6)] top-[var(--bh-space-xs-4)] flex min-w-0 items-center gap-[var(--bh-space-md-8)] transition-[inset-inline-start,top,gap] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
321
+ collapsed && "start-[var(--bh-space-xs-4)] top-0 gap-0"
322
+ )}
323
+ >
324
+ <span
325
+ data-slot="sidebar-brand-logo"
326
+ className="inline-flex size-[var(--bh-sidebar-header-action-size)] shrink-0 items-center justify-center overflow-hidden rounded-[var(--bh-radius-full)] [&>*]:size-full"
327
+ >
328
+ {logo ?? (
329
+ <span className="inline-flex size-full items-center justify-center bg-[var(--bh-bg-brand-default)] text-[var(--bh-content-on-brand)]">
330
+ <SparklesIcon
331
+ aria-hidden="true"
332
+ className="size-[var(--bh-sidebar-icon-size)]"
333
+ />
334
+ </span>
335
+ )}
336
+ </span>
337
+ <span
338
+ aria-hidden={collapsed || undefined}
339
+ data-slot="sidebar-brand-name"
340
+ dir="auto"
341
+ className={cn(
342
+ "max-w-[calc(var(--bh-sidebar-width)-var(--bh-space-10xl-80))] truncate opacity-[var(--bh-opacity-100)] transition-[max-width,opacity] [transition-duration:0ms,var(--bh-sidebar-motion-duration-content)] [transition-delay:0ms,var(--bh-sidebar-motion-delay-content)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
343
+ "text-[length:var(--bh-text-body-xl-semibold-font-size)] font-[var(--bh-text-body-xl-semibold-font-weight)] leading-[var(--bh-text-body-xl-semibold-line-height)] tracking-[var(--bh-text-body-xl-semibold-letter-spacing)]",
344
+ collapsed &&
345
+ "pointer-events-none max-w-0 opacity-[var(--bh-opacity-0)] [transition-delay:var(--bh-sidebar-motion-duration-content),0ms]"
346
+ )}
347
+ >
348
+ {brand}
349
+ </span>
350
+ </span>
351
+
352
+ {searchAction !== false || hasCollapseAction ? (
353
+ <span
354
+ data-slot="sidebar-brand-actions"
355
+ className={cn(
356
+ "absolute end-[var(--bh-space-xs-4)] top-[calc((var(--bh-sidebar-brand-height)-var(--bh-sidebar-header-action-size))/2)] flex shrink-0 items-center gap-[var(--bh-space-sm-6)] transition-[top,inset-inline-end,gap] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
357
+ collapsed && hasCollapseAction &&
358
+ "end-[var(--bh-space-xs-4)] top-[calc(var(--bh-sidebar-header-action-size)+var(--bh-space-sm-6))] gap-0"
359
+ )}
360
+ >
361
+ {searchAction === false ? null : (
362
+ <span
363
+ aria-hidden={collapsed || undefined}
364
+ data-slot="sidebar-brand-search-action"
365
+ inert={collapsed ? true : undefined}
366
+ className={cn(
367
+ "inline-flex max-w-[var(--bh-sidebar-header-action-size)] overflow-hidden opacity-[var(--bh-opacity-100)] transition-[max-width,opacity] duration-[var(--bh-sidebar-motion-duration-content)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
368
+ collapsed &&
369
+ "pointer-events-none max-w-0 opacity-[var(--bh-opacity-0)]"
370
+ )}
371
+ >
372
+ {searchAction ?? (
373
+ <SidebarHeaderAction label={searchLabel} onClick={onSearchClick}>
374
+ <SearchIcon aria-hidden="true" />
375
+ </SidebarHeaderAction>
376
+ )}
377
+ </span>
378
+ )}
379
+ {hasCollapseAction ? collapseAction ?? <SidebarTrigger /> : null}
380
+ </span>
381
+ ) : null}
382
+ </div>
383
+ )
384
+ }
385
+
386
+ /** Workspace switcher in either the header-actions or bordered card treatment. */
387
+ function SidebarHeaderWorkspaceSwitch({
388
+ className,
389
+ collapseAction,
390
+ image,
391
+ label = "Acme Inc",
392
+ onSearchClick,
393
+ onWorkspaceClick,
394
+ searchAction,
395
+ searchLabel = "Search",
396
+ supportText = "Pro plan",
397
+ variant = "actions",
398
+ workspaceLabel = "Switch workspace",
399
+ ...props
400
+ }: SidebarHeaderWorkspaceSwitchProps) {
401
+ const { collapsed } = useSidebarContext()
402
+ const card = variant === "card"
403
+ const hasCollapseAction = collapseAction !== false
404
+
405
+ const workspaceButton = (
406
+ <button
407
+ aria-label={workspaceLabel}
408
+ data-slot="sidebar-workspace-trigger"
409
+ onClick={onWorkspaceClick}
410
+ type="button"
411
+ className={cn(
412
+ "group/workspace absolute start-0 top-0 flex min-w-0 items-center overflow-hidden text-start outline-none transition-[width,height,gap,padding,border-radius,background-color,border-color,box-shadow] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
413
+ card
414
+ ? "h-[var(--bh-sidebar-workspace-card-height)] w-full gap-[var(--bh-space-md-8)] overflow-hidden rounded-[var(--bh-radius-lg-8)] border border-[var(--bh-border-subtle)] bg-[var(--bh-bg-default)] p-[var(--bh-space-sm-6)] hover:bg-[var(--bh-bg-default-hover)] focus-visible:border-[length:var(--bh-border-width-strong)] focus-visible:border-[var(--bh-border-focus)]"
415
+ : "h-[var(--bh-sidebar-compact-control-size)] w-[var(--bh-sidebar-workspace-trigger-width)] max-w-[var(--bh-sidebar-workspace-trigger-max-width)] gap-[var(--bh-space-md-8)] rounded-[var(--bh-radius-md-6)] p-[var(--bh-space-sm-6)] hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:border-[length:var(--bh-border-width-strong)] focus-visible:border-[var(--bh-border-focus)]",
416
+ collapsed && card &&
417
+ "size-[var(--bh-sidebar-compact-control-size)] gap-0 px-[var(--bh-space-xxs-2)] py-0",
418
+ collapsed && !card &&
419
+ "size-[var(--bh-sidebar-item-height-collapsed)] max-w-[var(--bh-sidebar-item-height-collapsed)] gap-0 px-[var(--bh-space-sm-6)] py-0"
420
+ )}
421
+ >
422
+ <span
423
+ data-slot="sidebar-workspace-image"
424
+ className={cn(
425
+ "inline-flex shrink-0 items-center justify-center overflow-hidden transition-[width,height,border-radius] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none [&>*]:size-full",
426
+ card
427
+ ? "size-[var(--bh-space-6xl-32)] rounded-[var(--bh-radius-md-6)]"
428
+ : "size-[var(--bh-space-5xl-24)] rounded-[var(--bh-radius-lg-8)]",
429
+ collapsed && !card &&
430
+ "size-[var(--bh-sidebar-header-action-size)] rounded-[var(--bh-radius-full)]"
431
+ )}
432
+ >
433
+ {image ?? (
434
+ <Building2Icon
435
+ aria-hidden="true"
436
+ className="size-[var(--bh-sidebar-icon-size)]"
437
+ />
438
+ )}
439
+ </span>
440
+
441
+ {card ? (
442
+ <span
443
+ aria-hidden={collapsed || undefined}
444
+ className={cn(
445
+ "flex min-w-0 max-w-[var(--bh-sidebar-width)] flex-1 flex-col items-start justify-center overflow-hidden opacity-[var(--bh-opacity-100)] transition-[max-width,opacity] [transition-duration:0ms,var(--bh-sidebar-motion-duration-content)] [transition-delay:0ms,var(--bh-sidebar-motion-delay-content)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
446
+ collapsed &&
447
+ "pointer-events-none max-w-0 opacity-[var(--bh-opacity-0)] [transition-delay:var(--bh-sidebar-motion-duration-content),0ms]"
448
+ )}
449
+ >
450
+ <span
451
+ data-slot="sidebar-workspace-label"
452
+ dir="auto"
453
+ className="w-full truncate text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)]"
454
+ >
455
+ {label}
456
+ </span>
457
+ <span
458
+ data-slot="sidebar-workspace-support"
459
+ dir="auto"
460
+ className="w-full truncate 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)]"
461
+ >
462
+ {supportText}
463
+ </span>
464
+ </span>
465
+ ) : (
466
+ <span
467
+ aria-hidden={collapsed || undefined}
468
+ className={cn(
469
+ "flex min-w-0 max-w-[var(--bh-sidebar-width)] items-center gap-[var(--bh-space-xxs-2)] overflow-hidden opacity-[var(--bh-opacity-100)] transition-[max-width,opacity] [transition-duration:0ms,var(--bh-sidebar-motion-duration-content)] [transition-delay:0ms,var(--bh-sidebar-motion-delay-content)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
470
+ collapsed &&
471
+ "pointer-events-none max-w-0 gap-0 opacity-[var(--bh-opacity-0)] [transition-delay:var(--bh-sidebar-motion-duration-content),0ms]"
472
+ )}
473
+ >
474
+ <span
475
+ data-slot="sidebar-workspace-label"
476
+ dir="auto"
477
+ className="truncate text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)]"
478
+ >
479
+ {label}
480
+ </span>
481
+ <ChevronDownIcon
482
+ aria-hidden="true"
483
+ className="size-[var(--bh-space-3xl-16)] shrink-0"
484
+ data-rtl-flip="true"
485
+ />
486
+ </span>
487
+ )}
488
+
489
+ {!card ? (
490
+ <span
491
+ aria-hidden="true"
492
+ data-slot="sidebar-workspace-caret"
493
+ className={cn(
494
+ "pointer-events-none absolute end-0 top-1/2 inline-flex size-[var(--bh-space-3xl-16)] -translate-y-1/2 items-center justify-center rounded-[var(--bh-radius-full)] border border-[var(--bh-border-subtle)] bg-[var(--bh-bg-default)] opacity-[var(--bh-opacity-0)] transition-opacity duration-[var(--bh-sidebar-motion-duration-content)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
495
+ collapsed &&
496
+ "opacity-[var(--bh-opacity-100)] delay-[var(--bh-sidebar-motion-delay-content)]"
497
+ )}
498
+ >
499
+ <ChevronDownIcon className="size-[var(--bh-space-xl-12)]" />
500
+ </span>
501
+ ) : null}
502
+
503
+ {card ? (
504
+ <ChevronsUpDownIcon
505
+ aria-hidden="true"
506
+ className={cn(
507
+ "size-[var(--bh-space-3xl-16)] shrink-0 text-[var(--bh-content-muted)] opacity-[var(--bh-opacity-100)] transition-opacity duration-[var(--bh-sidebar-motion-duration-content)] delay-[var(--bh-sidebar-motion-delay-content)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
508
+ collapsed && "opacity-[var(--bh-opacity-0)] delay-0"
509
+ )}
510
+ />
511
+ ) : null}
512
+ </button>
513
+ )
514
+
515
+ return (
516
+ <div
517
+ data-collapsed={collapsed ? "true" : "false"}
518
+ data-slot="sidebar-header-workspace-switch"
519
+ data-variant={variant}
520
+ className={cn(
521
+ "relative w-full transition-[width,height] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
522
+ card
523
+ ? "h-[var(--bh-sidebar-workspace-card-height)]"
524
+ : "h-[var(--bh-sidebar-compact-control-size)]",
525
+ collapsed && card && hasCollapseAction &&
526
+ "h-[calc(var(--bh-sidebar-compact-control-size)+var(--bh-space-sm-6)+var(--bh-sidebar-header-action-size))] w-[var(--bh-sidebar-compact-control-size)]",
527
+ collapsed && card && !hasCollapseAction &&
528
+ "h-[var(--bh-sidebar-compact-control-size)] w-[var(--bh-sidebar-compact-control-size)]",
529
+ collapsed && !card && hasCollapseAction &&
530
+ "h-[calc(var(--bh-sidebar-item-height-collapsed)+var(--bh-space-sm-6)+var(--bh-sidebar-header-action-size))] w-[var(--bh-sidebar-item-height-collapsed)]",
531
+ collapsed && !card && !hasCollapseAction &&
532
+ "h-[var(--bh-sidebar-item-height-collapsed)] w-[var(--bh-sidebar-item-height-collapsed)]",
533
+ className
534
+ )}
535
+ {...props}
536
+ >
537
+ {workspaceButton}
538
+ {(!card && searchAction !== false) || hasCollapseAction ? (
539
+ <span
540
+ aria-hidden={(card && !collapsed) || undefined}
541
+ data-slot="sidebar-workspace-actions"
542
+ inert={card && !collapsed ? true : undefined}
543
+ className={cn(
544
+ "absolute flex shrink-0 items-center gap-[var(--bh-space-sm-6)] transition-[top,inset-inline-end,gap,opacity] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
545
+ card
546
+ ? "pointer-events-none end-[var(--bh-space-xs-4)] top-[calc((var(--bh-sidebar-workspace-card-height)-var(--bh-sidebar-header-action-size))/2)] opacity-[var(--bh-opacity-0)]"
547
+ : "end-[var(--bh-space-md-8)] top-[calc((var(--bh-sidebar-compact-control-size)-var(--bh-sidebar-header-action-size))/2)] opacity-[var(--bh-opacity-100)]",
548
+ collapsed && card &&
549
+ "pointer-events-auto end-[var(--bh-space-xs-4)] top-[calc(var(--bh-sidebar-compact-control-size)+var(--bh-space-sm-6))] gap-0 opacity-[var(--bh-opacity-100)]",
550
+ collapsed && !card &&
551
+ "end-[var(--bh-space-sm-6)] top-[calc(var(--bh-sidebar-item-height-collapsed)+var(--bh-space-sm-6))] gap-0"
552
+ )}
553
+ >
554
+ {!card && searchAction !== false ? (
555
+ <span
556
+ aria-hidden={collapsed || undefined}
557
+ data-slot="sidebar-workspace-search-action"
558
+ inert={collapsed ? true : undefined}
559
+ className={cn(
560
+ "inline-flex max-w-[var(--bh-sidebar-header-action-size)] overflow-hidden opacity-[var(--bh-opacity-100)] transition-[max-width,opacity] duration-[var(--bh-sidebar-motion-duration-content)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
561
+ collapsed &&
562
+ "pointer-events-none max-w-0 opacity-[var(--bh-opacity-0)]"
563
+ )}
564
+ >
565
+ {searchAction ?? (
566
+ <SidebarHeaderAction label={searchLabel} onClick={onSearchClick}>
567
+ <SearchIcon aria-hidden="true" />
568
+ </SidebarHeaderAction>
569
+ )}
570
+ </span>
571
+ ) : null}
572
+ {hasCollapseAction ? collapseAction ?? <SidebarTrigger /> : null}
573
+ </span>
574
+ ) : null}
575
+ </div>
576
+ )
577
+ }
578
+
579
+ /** Scrollable content region with Figma's expanded and rail insets. */
580
+ function SidebarBody({ className, ...props }: SidebarSectionProps) {
581
+ return (
582
+ <div
583
+ data-slot="sidebar-body"
584
+ className={cn(
585
+ "min-h-0 flex-1 overflow-auto px-[var(--bh-space-xl-12)] py-0 transition-[padding] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
586
+ "group-data-[collapsed=true]/sidebar:px-0",
587
+ className
588
+ )}
589
+ {...props}
590
+ />
591
+ )
592
+ }
593
+
594
+ /** Bottom-pinned footer with the matching expanded and collapsed padding. */
595
+ function SidebarFooter({ className, ...props }: SidebarSectionProps) {
596
+ return (
597
+ <div
598
+ data-slot="sidebar-footer"
599
+ className={cn(
600
+ "mt-auto shrink-0 border-t border-[var(--bh-sidebar-border)] bg-[var(--bh-sidebar-bg)] px-[var(--bh-sidebar-padding)] py-[var(--bh-space-md-8)] transition-[padding] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
601
+ "group-data-[collapsed=true]/sidebar:p-[var(--bh-space-xs-4)]",
602
+ className
603
+ )}
604
+ {...props}
605
+ />
606
+ )
607
+ }
608
+
609
+ /** User switcher used in the footer, with 48px supporting and 40px compact rows. */
610
+ const SidebarFooterUser = React.forwardRef<
611
+ HTMLButtonElement,
612
+ SidebarFooterUserProps
613
+ >(function SidebarFooterUser(
614
+ {
615
+ "aria-label": ariaLabel,
616
+ avatar,
617
+ className,
618
+ name = "Username",
619
+ showIdentityPreview = true,
620
+ supportText = "user@example.com",
621
+ trailingIcon,
622
+ type = "button",
623
+ variant = "supporting-text",
624
+ ...props
625
+ },
626
+ ref
627
+ ) {
628
+ const { collapsed } = useSidebarContext()
629
+ const supporting = variant === "supporting-text"
630
+ const resolvedLabel =
631
+ ariaLabel ?? (typeof name === "string" ? name : "Open user menu")
632
+
633
+ return (
634
+ <button
635
+ aria-label={resolvedLabel}
636
+ data-collapsed={collapsed ? "true" : "false"}
637
+ data-slot="sidebar-footer-user"
638
+ data-variant={variant}
639
+ ref={ref}
640
+ type={type}
641
+ className={cn(
642
+ "group/footer-user relative flex w-full items-center rounded-[var(--bh-radius-lg-8)] text-start outline-none transition-[width,height,gap,padding,border-radius,background-color,border-color,box-shadow] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
643
+ supporting
644
+ ? "h-[var(--bh-space-8xl-48)] gap-[var(--bh-space-xs-4)] p-[var(--bh-space-sm-6)] hover:bg-[var(--bh-interactive-ghost-hover)]"
645
+ : "h-[var(--bh-sidebar-item-height-collapsed)] gap-[var(--bh-space-xs-4)] border border-[var(--bh-border-subtle)] bg-[var(--bh-bg-default)] px-[var(--bh-space-sm-6)] py-[var(--bh-space-md-8)] hover:bg-[var(--bh-bg-default-hover)]",
646
+ "focus-visible:border-[length:var(--bh-border-width-strong)] focus-visible:border-[var(--bh-border-focus)] focus-visible:shadow-[var(--elevation-button-focus)]",
647
+ collapsed &&
648
+ "mx-auto size-[var(--bh-sidebar-compact-control-size)] justify-center gap-0 rounded-[var(--bh-radius-full)] border-0 p-0 hover:bg-[var(--bh-interactive-ghost-hover)]",
649
+ className
650
+ )}
651
+ {...props}
652
+ >
653
+ <span
654
+ data-slot="sidebar-footer-user-content"
655
+ className={cn(
656
+ "flex min-w-0 flex-1 items-center gap-[var(--bh-space-md-8)] overflow-hidden px-[var(--bh-space-xxs-2)] transition-[width,height,gap,padding] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
657
+ collapsed && "gap-0 p-0"
658
+ )}
659
+ >
660
+ <span
661
+ data-slot="sidebar-footer-user-avatar"
662
+ className={cn(
663
+ "inline-flex shrink-0 items-center justify-center overflow-hidden rounded-[var(--bh-radius-full)] transition-[width,height,border-radius] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none [&>*]:size-full",
664
+ supporting ? "size-[var(--bh-space-6xl-32)]" : "size-[var(--bh-space-5xl-24)]",
665
+ collapsed && "size-[var(--bh-sidebar-compact-control-size)]"
666
+ )}
667
+ >
668
+ {avatar ?? (
669
+ <UserIcon
670
+ aria-hidden="true"
671
+ className="size-[var(--bh-sidebar-icon-size)]"
672
+ />
673
+ )}
674
+ </span>
675
+ <span
676
+ aria-hidden={collapsed || undefined}
677
+ data-slot="sidebar-footer-user-text"
678
+ className={cn(
679
+ "flex min-w-0 max-w-[var(--bh-sidebar-width)] flex-1 flex-col items-start justify-center overflow-hidden opacity-[var(--bh-opacity-100)] transition-[max-width,opacity] [transition-duration:0ms,var(--bh-sidebar-motion-duration-content)] [transition-delay:0ms,var(--bh-sidebar-motion-delay-content)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none rtl:items-end rtl:text-right",
680
+ collapsed &&
681
+ "pointer-events-none max-w-0 opacity-[var(--bh-opacity-0)] [transition-delay:var(--bh-sidebar-motion-duration-content),0ms]"
682
+ )}
683
+ >
684
+ <span
685
+ data-slot="sidebar-footer-user-name"
686
+ dir="auto"
687
+ className="w-full truncate text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)]"
688
+ >
689
+ {name}
690
+ </span>
691
+ {supporting ? (
692
+ <span
693
+ data-slot="sidebar-footer-user-support"
694
+ dir="auto"
695
+ className="w-full truncate 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)]"
696
+ >
697
+ {supportText}
698
+ </span>
699
+ ) : null}
700
+ </span>
701
+ </span>
702
+ <span
703
+ aria-hidden="true"
704
+ data-slot="sidebar-footer-user-trailing"
705
+ className={cn(
706
+ "inline-flex size-[var(--bh-space-4xl-20)] max-w-[var(--bh-space-4xl-20)] shrink-0 items-center justify-center overflow-hidden text-[var(--bh-content-muted)] opacity-[var(--bh-opacity-100)] transition-[max-width,opacity] [transition-duration:0ms,var(--bh-sidebar-motion-duration-content)] [transition-delay:0ms,var(--bh-sidebar-motion-delay-content)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none [&_svg]:size-[var(--bh-space-3xl-16)]",
707
+ collapsed &&
708
+ "pointer-events-none max-w-0 opacity-[var(--bh-opacity-0)] [transition-delay:var(--bh-sidebar-motion-duration-content),0ms]"
709
+ )}
710
+ >
711
+ {trailingIcon ?? <ChevronsUpDownIcon />}
712
+ </span>
713
+ {collapsed && showIdentityPreview ? (
714
+ <span
715
+ aria-hidden="true"
716
+ data-slot="sidebar-footer-user-preview"
717
+ className={cn(
718
+ "pointer-events-none invisible absolute bottom-0 start-[calc(100%+var(--bh-sidebar-user-preview-gap))] z-[var(--bh-z-overlay)] flex h-[var(--bh-sidebar-user-preview-height)] w-[var(--bh-sidebar-user-preview-width)] items-center gap-[var(--bh-space-md-8)] rounded-[var(--bh-radius-xl-10)] border border-[var(--bh-border-subtle)] bg-[var(--bh-bg-raised)] px-[var(--bh-space-xl-12)] py-[var(--bh-space-md-8)] opacity-[var(--bh-opacity-0)] shadow-[var(--elevation-component-default)] transition-[opacity,visibility]",
719
+ "group-hover/footer-user:visible group-hover/footer-user:opacity-[var(--bh-opacity-100)] group-focus-visible/footer-user:visible group-focus-visible/footer-user:opacity-[var(--bh-opacity-100)] group-data-[state=open]/footer-user:hidden",
720
+ "group-data-[side=end]/sidebar:start-auto group-data-[side=end]/sidebar:end-[calc(100%+var(--bh-sidebar-user-preview-gap))]"
721
+ )}
722
+ >
723
+ <span className="flex min-w-0 flex-1 flex-col items-start justify-center">
724
+ <span
725
+ dir="auto"
726
+ className="w-full truncate text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)] text-[var(--bh-content-default)]"
727
+ >
728
+ {name}
729
+ </span>
730
+ <span
731
+ dir="auto"
732
+ className="w-full 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)]"
733
+ >
734
+ {supportText}
735
+ </span>
736
+ </span>
737
+ <span className="inline-flex size-[var(--bh-space-4xl-20)] shrink-0 items-center justify-center text-[var(--bh-content-muted)] [&_svg]:size-[var(--bh-space-3xl-16)]">
738
+ {trailingIcon ?? <MoreHorizontalIcon />}
739
+ </span>
740
+ </span>
741
+ ) : null}
742
+ </button>
743
+ )
744
+ })
745
+
746
+ /** A Figma sidebar group: 12px top padding, 6px bottom padding, and no menu gap. */
747
+ function SidebarGroup({ className, ...props }: SidebarSectionProps) {
748
+ return (
749
+ <div
750
+ data-slot="sidebar-group"
751
+ className={cn(
752
+ "flex w-full flex-col items-start pb-[var(--bh-space-sm-6)] pt-[var(--bh-space-xl-12)]",
753
+ className
754
+ )}
755
+ {...props}
756
+ />
757
+ )
758
+ }
759
+
760
+ /** Group label that reveals its disclosure and actions on hover or keyboard focus. */
761
+ function SidebarGroupLabel({
762
+ actions,
763
+ children,
764
+ className,
765
+ expandIcon,
766
+ expandable = false,
767
+ ...props
768
+ }: SidebarGroupLabelProps) {
769
+ const { collapsed } = useSidebarContext()
770
+
771
+ return (
772
+ <div
773
+ aria-hidden={collapsed || undefined}
774
+ data-slot="sidebar-group-label-motion"
775
+ inert={collapsed ? true : undefined}
776
+ className={cn(
777
+ "grid w-full grid-rows-[1fr] opacity-[var(--bh-opacity-100)] transition-[grid-template-rows,opacity] duration-[var(--bh-sidebar-motion-duration-content)] delay-[var(--bh-sidebar-motion-delay-content)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
778
+ collapsed &&
779
+ "pointer-events-none grid-rows-[0fr] opacity-[var(--bh-opacity-0)] delay-0"
780
+ )}
781
+ >
782
+ <div
783
+ data-slot="sidebar-group-label"
784
+ className={cn(
785
+ "group/label flex min-h-0 h-[var(--bh-space-5xl-24)] w-full items-center gap-[var(--bh-space-xs-4)] overflow-hidden rounded-[var(--bh-radius-lg-8)] px-[var(--bh-space-sm-6)] pb-[var(--bh-space-xs-4)]",
786
+ className
787
+ )}
788
+ {...props}
789
+ >
790
+ <span
791
+ data-slot="sidebar-group-label-content"
792
+ className="flex min-w-0 flex-1 items-center gap-[var(--bh-space-xxxs-1)]"
793
+ >
794
+ <span
795
+ data-slot="sidebar-group-label-text"
796
+ dir="auto"
797
+ className="truncate text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)] text-[var(--bh-content-muted)]"
798
+ >
799
+ {children}
800
+ </span>
801
+ {expandable ? (
802
+ <span
803
+ aria-hidden="true"
804
+ data-slot="sidebar-group-label-expand"
805
+ className="inline-flex size-[var(--bh-space-3xl-16)] shrink-0 items-center justify-center opacity-[var(--bh-opacity-0)] transition-opacity group-hover/label:opacity-[var(--bh-opacity-100)] group-focus-within/label:opacity-[var(--bh-opacity-100)] [&_svg]:size-full"
806
+ >
807
+ {expandIcon ?? <ChevronRightIcon data-rtl-flip="true" />}
808
+ </span>
809
+ ) : null}
810
+ </span>
811
+ {actions ? (
812
+ <span
813
+ data-slot="sidebar-group-label-actions"
814
+ className="flex shrink-0 items-center gap-[var(--bh-space-xs-4)] opacity-[var(--bh-opacity-0)] transition-opacity group-hover/label:opacity-[var(--bh-opacity-100)] group-focus-within/label:opacity-[var(--bh-opacity-100)]"
815
+ >
816
+ {actions}
817
+ </span>
818
+ ) : null}
819
+ </div>
820
+ </div>
821
+ )
822
+ }
823
+
824
+ /** 20px group-label icon slot matching the Default, Hover, and Focus variants. */
825
+ const SidebarGroupAction = React.forwardRef<
826
+ HTMLButtonElement,
827
+ SidebarGroupActionProps
828
+ >(function SidebarGroupAction(
829
+ { "aria-label": ariaLabel, children, className, label, type = "button", ...props },
830
+ ref
831
+ ) {
832
+ return (
833
+ <button
834
+ aria-label={ariaLabel ?? label ?? "Group action"}
835
+ data-slot="sidebar-group-action"
836
+ ref={ref}
837
+ type={type}
838
+ className={cn(
839
+ "inline-flex size-[var(--bh-sidebar-icon-slot-size)] items-center justify-center rounded-[var(--bh-control-sm)] border border-transparent bg-[var(--bh-interactive-ghost-default)] text-[var(--bh-content-muted)] outline-none transition-[background-color,border-color,color]",
840
+ "hover:bg-[var(--bh-interactive-ghost-hover)] hover:text-[var(--bh-content-default)] focus-visible:border-[var(--bh-border-focus)]",
841
+ "[&_svg]:size-[var(--bh-sidebar-icon-size)] [&_svg]:shrink-0",
842
+ className
843
+ )}
844
+ {...props}
845
+ >
846
+ {children}
847
+ </button>
848
+ )
849
+ })
850
+
851
+ /** List wrapper for contiguous 32px sidebar menu rows. */
852
+ function SidebarMenu({ className, ...props }: SidebarMenuProps) {
853
+ return (
854
+ <ul
855
+ data-slot="sidebar-menu"
856
+ className={cn("m-0 flex w-full list-none flex-col p-0", className)}
857
+ {...props}
858
+ />
859
+ )
860
+ }
861
+
862
+ /** Semantic list item wrapper. */
863
+ function SidebarMenuItem({ className, ...props }: SidebarMenuItemProps) {
864
+ return (
865
+ <li
866
+ data-slot="sidebar-menu-item"
867
+ className={cn("min-w-0", className)}
868
+ {...props}
869
+ />
870
+ )
871
+ }
872
+
873
+ /** Native 24px neutral count badge for sidebar rows. */
874
+ function SidebarMenuBadge({ children, className, ...props }: SidebarMenuBadgeProps) {
875
+ return (
876
+ <span
877
+ data-slot="sidebar-menu-badge"
878
+ className={cn(
879
+ "inline-flex h-[var(--bh-space-5xl-24)] min-w-[var(--bh-space-5xl-24)] shrink-0 items-center justify-center rounded-[var(--bh-radius-full)] bg-[var(--bh-bg-neutral-subtle)] px-[var(--bh-space-xs-4)] text-center text-[length:var(--bh-text-body-3xs-semibold-font-size)] font-[var(--bh-text-body-3xs-semibold-font-weight)] leading-[var(--bh-text-body-3xs-semibold-line-height)] tracking-[var(--bh-text-body-3xs-semibold-letter-spacing)] text-[var(--bh-content-default)]",
880
+ className
881
+ )}
882
+ {...props}
883
+ >
884
+ {children}
885
+ </span>
886
+ )
887
+ }
888
+
889
+ /** Default, avatar, Level 2, and Level 2 line menu variants from the Figma set. */
890
+ const SidebarMenuButton = React.forwardRef<
891
+ HTMLButtonElement,
892
+ SidebarMenuButtonProps
893
+ >(function SidebarMenuButton(
894
+ {
895
+ "aria-label": ariaLabel,
896
+ active = false,
897
+ asChild = false,
898
+ badge,
899
+ children,
900
+ className,
901
+ icon,
902
+ state = "default",
903
+ supportText,
904
+ tooltip,
905
+ trailingIcon,
906
+ type = "button",
907
+ variant = "default",
908
+ ...props
909
+ },
910
+ ref
911
+ ) {
912
+ const { collapsed, dir } = useSidebarContext()
913
+ const isLevel2 = variant === "level2" || variant === "level2-line"
914
+ const label = getSlottableLabel(children, asChild)
915
+ const resolvedLabel =
916
+ ariaLabel ?? (typeof label === "string" ? label : undefined)
917
+
918
+ const content = (
919
+ <>
920
+ {isLevel2 ? (
921
+ <span
922
+ aria-hidden="true"
923
+ data-slot="sidebar-menu-indent"
924
+ className="relative h-full w-[var(--bh-sidebar-level-2-indent-width)] shrink-0"
925
+ >
926
+ {variant === "level2-line" ? (
927
+ <span
928
+ data-slot="sidebar-menu-guide"
929
+ className={cn(
930
+ "absolute start-[var(--bh-space-md-8)]",
931
+ active
932
+ ? "top-1/2 h-[var(--bh-sidebar-active-guide-height)] -translate-y-1/2 border-e-[length:var(--bh-border-width-strong)] border-[var(--bh-bg-brand-default)]"
933
+ : "inset-y-0 border-e-[length:var(--bh-border-width-default)] border-[var(--bh-border-subtle)]"
934
+ )}
935
+ />
936
+ ) : null}
937
+ </span>
938
+ ) : null}
939
+
940
+ {icon ? (
941
+ <span
942
+ aria-hidden="true"
943
+ data-slot="sidebar-menu-icon"
944
+ className={cn(
945
+ "inline-flex shrink-0 items-center justify-center overflow-hidden [&>*]:size-full [&_svg]:shrink-0",
946
+ variant === "avatar"
947
+ ? "size-[var(--bh-space-4xl-20)] rounded-[var(--bh-radius-md-6)]"
948
+ : "size-[var(--bh-sidebar-icon-slot-size)] [&_svg]:size-[var(--bh-sidebar-icon-size)]",
949
+ collapsed && variant === "avatar" &&
950
+ "size-[var(--bh-space-5xl-24)] rounded-[var(--bh-radius-lg-8)]",
951
+ collapsed && variant === "default" &&
952
+ "[&_svg]:size-[var(--bh-sidebar-icon-size-collapsed)]"
953
+ )}
954
+ >
955
+ {icon}
956
+ </span>
957
+ ) : null}
958
+
959
+ <span
960
+ aria-hidden={collapsed || undefined}
961
+ data-slot="sidebar-menu-label"
962
+ dir="auto"
963
+ className={cn(
964
+ "min-w-0 max-w-[var(--bh-sidebar-width)] flex-1 truncate ps-[var(--bh-space-xs-4)] opacity-[var(--bh-opacity-100)] transition-[max-width,opacity,padding] [transition-duration:0ms,var(--bh-sidebar-motion-duration-content),0ms] [transition-delay:0ms,var(--bh-sidebar-motion-delay-content),0ms] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
965
+ collapsed &&
966
+ "pointer-events-none max-w-0 p-0 opacity-[var(--bh-opacity-0)] [transition-delay:var(--bh-sidebar-motion-duration-content),0ms,var(--bh-sidebar-motion-duration-content)]"
967
+ )}
968
+ >
969
+ {label}
970
+ </span>
971
+
972
+ {supportText ? (
973
+ <span
974
+ aria-hidden={collapsed || undefined}
975
+ data-slot="sidebar-menu-support"
976
+ dir="auto"
977
+ className={cn(
978
+ "max-w-[var(--bh-sidebar-width)] shrink-0 overflow-hidden whitespace-nowrap 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-muted)] opacity-[var(--bh-opacity-100)] transition-[max-width,opacity] [transition-duration:0ms,var(--bh-sidebar-motion-duration-content)] [transition-delay:0ms,var(--bh-sidebar-motion-delay-content)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
979
+ collapsed &&
980
+ "pointer-events-none max-w-0 opacity-[var(--bh-opacity-0)] [transition-delay:var(--bh-sidebar-motion-duration-content),0ms]"
981
+ )}
982
+ >
983
+ {supportText}
984
+ </span>
985
+ ) : null}
986
+
987
+ {badge ? (
988
+ <span
989
+ aria-hidden={collapsed || undefined}
990
+ data-slot="sidebar-menu-badge-slot"
991
+ className={cn(
992
+ "inline-flex max-w-[var(--bh-sidebar-width)] shrink-0 overflow-hidden opacity-[var(--bh-opacity-100)] transition-[max-width,opacity] [transition-duration:0ms,var(--bh-sidebar-motion-duration-content)] [transition-delay:0ms,var(--bh-sidebar-motion-delay-content)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
993
+ collapsed &&
994
+ "pointer-events-none max-w-0 opacity-[var(--bh-opacity-0)] [transition-delay:var(--bh-sidebar-motion-duration-content),0ms]"
995
+ )}
996
+ >
997
+ {badge}
998
+ </span>
999
+ ) : null}
1000
+
1001
+ {trailingIcon ? (
1002
+ <span
1003
+ aria-hidden="true"
1004
+ data-slot="sidebar-menu-trailing"
1005
+ className={cn(
1006
+ "inline-flex size-[var(--bh-sidebar-icon-slot-size)] max-w-[var(--bh-sidebar-icon-slot-size)] shrink-0 items-center justify-center overflow-hidden text-[var(--bh-content-muted)] opacity-[var(--bh-opacity-100)] transition-[max-width,opacity] [transition-duration:0ms,var(--bh-sidebar-motion-duration-content)] [transition-delay:0ms,var(--bh-sidebar-motion-delay-content)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none [&_svg]:size-[var(--bh-sidebar-icon-size)] [&_svg]:shrink-0",
1007
+ collapsed &&
1008
+ "pointer-events-none max-w-0 opacity-[var(--bh-opacity-0)] [transition-delay:var(--bh-sidebar-motion-duration-content),0ms]"
1009
+ )}
1010
+ >
1011
+ {trailingIcon}
1012
+ </span>
1013
+ ) : null}
1014
+ </>
1015
+ )
1016
+
1017
+ const buttonClassName = cn(
1018
+ "group/menu-button relative flex w-full min-w-0 items-center gap-[var(--bh-space-sm-6)] overflow-hidden rounded-[var(--bh-radius-lg-8)] border-[length:var(--bh-border-width-strong)] border-transparent px-[var(--bh-sidebar-item-padding-x)] text-start text-[length:var(--bh-text-body-sm-regular-font-size)] font-[var(--bh-text-body-sm-regular-font-weight)] leading-[var(--bh-text-body-sm-regular-line-height)] tracking-[var(--bh-text-body-sm-regular-letter-spacing)] text-[var(--bh-sidebar-item-content)] opacity-[var(--bh-opacity-100)] outline-none transition-[width,height,gap,padding,margin,opacity,background-color,border-color,color] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
1019
+ "hover:rounded-[var(--bh-radius-md-6)] hover:bg-[var(--bh-sidebar-item-bg-hover)]",
1020
+ "focus-visible:rounded-[var(--bh-radius-md-6)] focus-visible:border-[var(--bh-border-focus)]",
1021
+ "data-[state=hover]:rounded-[var(--bh-radius-md-6)] data-[state=hover]:bg-[var(--bh-sidebar-item-bg-hover)]",
1022
+ "data-[state=focus]:rounded-[var(--bh-radius-md-6)] data-[state=focus]:border-[var(--bh-border-focus)]",
1023
+ "disabled:pointer-events-none disabled:text-[var(--bh-content-disabled)]",
1024
+ variant === "avatar"
1025
+ ? "h-[var(--bh-sidebar-compact-control-size)]"
1026
+ : "h-[var(--bh-sidebar-item-height)]",
1027
+ (active || state === "active") &&
1028
+ "rounded-[var(--bh-radius-md-6)] bg-[var(--bh-sidebar-item-bg-active)] font-[var(--bh-text-body-sm-medium-font-weight)] text-[var(--bh-sidebar-item-content-active)]",
1029
+ collapsed && !isLevel2 &&
1030
+ "ms-[var(--bh-space-xs-4)] size-[var(--bh-sidebar-item-height-collapsed)] gap-0 rounded-[var(--bh-radius-lg-8)] px-[var(--bh-space-md-8)] py-0",
1031
+ collapsed && !isLevel2 && variant === "avatar" &&
1032
+ "px-[var(--bh-space-sm-6)]",
1033
+ collapsed && isLevel2 &&
1034
+ "pointer-events-none h-0 w-full gap-0 border-0 p-0 opacity-[var(--bh-opacity-0)]",
1035
+ className
1036
+ )
1037
+
1038
+ const commonProps = {
1039
+ "aria-current": active ? ("page" as const) : undefined,
1040
+ "aria-hidden": collapsed && isLevel2 ? true : undefined,
1041
+ "aria-label": resolvedLabel,
1042
+ "data-active": active ? "true" : "false",
1043
+ "data-collapsed": collapsed ? "true" : "false",
1044
+ "data-slot": "sidebar-menu-button",
1045
+ "data-state": state,
1046
+ "data-variant": variant,
1047
+ inert: collapsed && isLevel2 ? true : undefined,
1048
+ className: buttonClassName,
1049
+ }
1050
+
1051
+ const button = asChild ? (
1052
+ <Slot {...commonProps} ref={ref} {...props}>
1053
+ {cloneSlottableChild(children, content)}
1054
+ </Slot>
1055
+ ) : (
1056
+ <button {...commonProps} ref={ref} type={type} {...props}>
1057
+ {content}
1058
+ </button>
1059
+ )
1060
+
1061
+ const resolvedTooltip = tooltip ?? resolvedLabel
1062
+ if (!resolvedTooltip || isLevel2) return button
1063
+
1064
+ return (
1065
+ <Tooltip>
1066
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
1067
+ {collapsed ? (
1068
+ <TooltipContent
1069
+ pointerPosition={dir === "rtl" ? "center-right" : "center-left"}
1070
+ showShortcut={false}
1071
+ side={dir === "rtl" ? "left" : "right"}
1072
+ >
1073
+ {resolvedTooltip}
1074
+ </TooltipContent>
1075
+ ) : null}
1076
+ </Tooltip>
1077
+ )
1078
+ })
1079
+
1080
+ /** Nested menu list that contracts before the icon rail settles. */
1081
+ function SidebarMenuSub({ className, ...props }: SidebarMenuSubProps) {
1082
+ const { collapsed } = useSidebarContext()
1083
+
1084
+ return (
1085
+ <div
1086
+ aria-hidden={collapsed || undefined}
1087
+ data-slot="sidebar-menu-sub-motion"
1088
+ inert={collapsed ? true : undefined}
1089
+ className={cn(
1090
+ "grid w-full grid-rows-[1fr] opacity-[var(--bh-opacity-100)] transition-[grid-template-rows,opacity] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
1091
+ collapsed &&
1092
+ "pointer-events-none grid-rows-[0fr] opacity-[var(--bh-opacity-0)]"
1093
+ )}
1094
+ >
1095
+ <ul
1096
+ data-slot="sidebar-menu-sub"
1097
+ className={cn(
1098
+ "m-0 flex min-h-0 w-full list-none flex-col overflow-hidden p-0",
1099
+ className
1100
+ )}
1101
+ {...props}
1102
+ />
1103
+ </div>
1104
+ )
1105
+ }
1106
+
1107
+ /** Level 2 menu row with an optional vertical guide. */
1108
+ const SidebarMenuSubButton = React.forwardRef<
1109
+ HTMLButtonElement,
1110
+ SidebarMenuSubButtonProps
1111
+ >(function SidebarMenuSubButton({ showLine = false, ...props }, ref) {
1112
+ return (
1113
+ <SidebarMenuButton
1114
+ ref={ref}
1115
+ variant={showLine ? "level2-line" : "level2"}
1116
+ {...props}
1117
+ />
1118
+ )
1119
+ })
1120
+
1121
+ const treeLevelClasses: Record<NonNullable<SidebarMenuTreeItemProps["level"]>, string> = {
1122
+ 1: "ps-[var(--bh-space-sm-6)]",
1123
+ 2: "ps-[calc(var(--bh-space-sm-6)+var(--bh-sidebar-tree-indent-step))]",
1124
+ 3: "ps-[calc(var(--bh-space-sm-6)+var(--bh-sidebar-tree-indent-step)+var(--bh-sidebar-tree-indent-step))]",
1125
+ 4: "ps-[calc(var(--bh-space-sm-6)+var(--bh-sidebar-tree-indent-step)+var(--bh-sidebar-tree-indent-step)+var(--bh-sidebar-tree-indent-step))]",
1126
+ }
1127
+
1128
+ /** 28px file-tree row with four Figma indentation levels. */
1129
+ const SidebarMenuTreeItem = React.forwardRef<
1130
+ HTMLButtonElement,
1131
+ SidebarMenuTreeItemProps
1132
+ >(function SidebarMenuTreeItem(
1133
+ {
1134
+ "aria-label": ariaLabel,
1135
+ active = false,
1136
+ asChild = false,
1137
+ children,
1138
+ className,
1139
+ expanded = false,
1140
+ icon,
1141
+ level = 1,
1142
+ showChevron = true,
1143
+ state = "default",
1144
+ type = "button",
1145
+ ...props
1146
+ },
1147
+ ref
1148
+ ) {
1149
+ const { collapsed } = useSidebarContext()
1150
+
1151
+ const label = getSlottableLabel(children, asChild)
1152
+ const resolvedLabel =
1153
+ ariaLabel ?? (typeof label === "string" ? label : undefined)
1154
+ const content = (
1155
+ <>
1156
+ {showChevron ? (
1157
+ <span
1158
+ aria-hidden="true"
1159
+ data-slot="sidebar-tree-chevron"
1160
+ className="inline-flex size-[var(--bh-space-3xl-16)] shrink-0 items-center justify-center [&_svg]:size-full"
1161
+ >
1162
+ {expanded ? (
1163
+ <ChevronDownIcon />
1164
+ ) : (
1165
+ <ChevronRightIcon data-rtl-flip="true" />
1166
+ )}
1167
+ </span>
1168
+ ) : null}
1169
+ <span
1170
+ aria-hidden="true"
1171
+ data-slot="sidebar-tree-icon"
1172
+ className="inline-flex size-[var(--bh-space-3xl-16)] shrink-0 items-center justify-center [&_svg]:size-full"
1173
+ >
1174
+ {icon ?? <FolderIcon />}
1175
+ </span>
1176
+ <span
1177
+ data-slot="sidebar-tree-label"
1178
+ dir="auto"
1179
+ className="min-w-0 flex-1 truncate"
1180
+ >
1181
+ {label}
1182
+ </span>
1183
+ </>
1184
+ )
1185
+
1186
+ const commonProps = {
1187
+ "aria-current": active ? ("page" as const) : undefined,
1188
+ "aria-expanded": showChevron ? expanded : undefined,
1189
+ "aria-hidden": collapsed || undefined,
1190
+ "aria-label": resolvedLabel,
1191
+ "data-active": active ? "true" : "false",
1192
+ "data-collapsed": collapsed ? "true" : "false",
1193
+ "data-level": String(level),
1194
+ "data-slot": "sidebar-menu-tree-item",
1195
+ "data-state": state,
1196
+ inert: collapsed ? true : undefined,
1197
+ className: cn(
1198
+ "flex h-[var(--bh-sidebar-tree-item-height)] w-full min-w-0 items-center gap-[var(--bh-space-sm-6)] overflow-hidden rounded-[var(--bh-radius-lg-8)] border-[length:var(--bh-border-width-strong)] border-transparent pe-[var(--bh-space-sm-6)] text-start 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)] opacity-[var(--bh-opacity-100)] outline-none transition-[height,gap,padding,opacity,background-color,border-color,color] duration-[var(--bh-sidebar-motion-duration-content)] delay-[var(--bh-sidebar-motion-delay-content)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
1199
+ "hover:rounded-[var(--bh-radius-md-6)] hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:rounded-[var(--bh-radius-md-6)] focus-visible:border-[var(--bh-border-focus)]",
1200
+ "data-[state=hover]:rounded-[var(--bh-radius-md-6)] data-[state=hover]:bg-[var(--bh-interactive-ghost-hover)] data-[state=focus]:rounded-[var(--bh-radius-md-6)] data-[state=focus]:border-[var(--bh-border-focus)]",
1201
+ (active || state === "active") &&
1202
+ "rounded-[var(--bh-radius-md-6)] bg-[var(--bh-interactive-ghost-active)] font-[var(--bh-text-body-xs-semibold-font-weight)] text-[var(--bh-content-default)]",
1203
+ treeLevelClasses[level],
1204
+ collapsed &&
1205
+ "pointer-events-none h-0 gap-0 border-0 p-0 opacity-[var(--bh-opacity-0)] delay-0",
1206
+ className
1207
+ ),
1208
+ }
1209
+
1210
+ return asChild ? (
1211
+ <Slot {...commonProps} ref={ref} {...props}>
1212
+ {cloneSlottableChild(children, content)}
1213
+ </Slot>
1214
+ ) : (
1215
+ <button {...commonProps} ref={ref} type={type} {...props}>
1216
+ {content}
1217
+ </button>
1218
+ )
1219
+ })
1220
+
1221
+ /** Labeled tree group used for file-browser and hierarchy sidebars. */
1222
+ function SidebarTreeGroup({
1223
+ actions,
1224
+ children,
1225
+ className,
1226
+ expandable = false,
1227
+ expandIcon,
1228
+ label,
1229
+ ...props
1230
+ }: SidebarTreeGroupProps) {
1231
+ const { collapsed } = useSidebarContext()
1232
+
1233
+ return (
1234
+ <div
1235
+ aria-hidden={collapsed || undefined}
1236
+ data-slot="sidebar-tree-group-motion"
1237
+ inert={collapsed ? true : undefined}
1238
+ className={cn(
1239
+ "grid w-full grid-rows-[1fr] opacity-[var(--bh-opacity-100)] transition-[grid-template-rows,opacity] duration-[var(--bh-sidebar-motion-duration)] ease-[var(--bh-sidebar-motion-easing)] motion-reduce:transition-none",
1240
+ collapsed &&
1241
+ "pointer-events-none grid-rows-[0fr] opacity-[var(--bh-opacity-0)]"
1242
+ )}
1243
+ >
1244
+ <div
1245
+ data-slot="sidebar-tree-group"
1246
+ className={cn(
1247
+ "flex min-h-0 w-full flex-col items-start gap-[var(--bh-space-sm-6)] overflow-hidden pb-[var(--bh-space-sm-6)] pt-[var(--bh-space-xl-12)]",
1248
+ className
1249
+ )}
1250
+ {...props}
1251
+ >
1252
+ {label ? (
1253
+ <SidebarGroupLabel
1254
+ actions={actions}
1255
+ className={cn(
1256
+ expandable &&
1257
+ "[&_[data-slot=sidebar-group-label-expand]]:opacity-[var(--bh-opacity-100)]"
1258
+ )}
1259
+ expandable={expandable}
1260
+ expandIcon={expandIcon ?? (expandable ? <ChevronDownIcon /> : undefined)}
1261
+ >
1262
+ {label}
1263
+ </SidebarGroupLabel>
1264
+ ) : null}
1265
+ <div data-slot="sidebar-tree-menu" className="flex w-full flex-col">
1266
+ {children}
1267
+ </div>
1268
+ </div>
1269
+ </div>
1270
+ )
1271
+ }
1272
+
1273
+ /** 28px collapse control with accessible pressed state. */
1274
+ const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>(
1275
+ function SidebarToggle(
1276
+ { children, className, onClick, type = "button", ...props },
1277
+ ref
1278
+ ) {
1279
+ const { collapsed, toggle } = useSidebarContext()
1280
+
1281
+ return (
1282
+ <button
1283
+ aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
1284
+ aria-pressed={collapsed}
1285
+ data-slot="sidebar-toggle"
1286
+ ref={ref}
1287
+ type={type}
1288
+ className={cn(
1289
+ "inline-flex size-[var(--bh-sidebar-header-action-size)] shrink-0 items-center justify-center rounded-[var(--bh-radius-full)] border border-transparent bg-[var(--bh-interactive-ghost-default)] text-[var(--bh-content-default)] outline-none transition-[background-color,border-color,box-shadow,color]",
1290
+ "hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:border-[var(--bh-border-focus)] focus-visible:shadow-[var(--elevation-button-focus)]",
1291
+ className
1292
+ )}
1293
+ onClick={(event) => {
1294
+ onClick?.(event)
1295
+ if (!event.defaultPrevented) toggle()
1296
+ }}
1297
+ {...props}
1298
+ >
1299
+ <span
1300
+ aria-hidden="true"
1301
+ data-slot="sidebar-toggle-icon"
1302
+ className="inline-flex size-[var(--bh-sidebar-icon-slot-size)] items-center justify-center [&_svg]:size-[var(--bh-sidebar-icon-size)]"
1303
+ >
1304
+ {children ?? (
1305
+ <PanelLeftIcon
1306
+ className="rtl:-scale-x-100"
1307
+ strokeWidth="var(--bh-icon-stroke-200)"
1308
+ />
1309
+ )}
1310
+ </span>
1311
+ </button>
1312
+ )
1313
+ }
1314
+ )
1315
+
1316
+ const SidebarTrigger = SidebarToggle
1317
+
1318
+ /** Edge-hugging pointer target for toggling the sidebar from its border. */
1319
+ const SidebarRail = React.forwardRef<HTMLButtonElement, SidebarRailProps>(
1320
+ function SidebarRail({ className, onClick, type = "button", ...props }, ref) {
1321
+ const { toggle } = useSidebarContext()
1322
+
1323
+ return (
1324
+ <button
1325
+ aria-label="Toggle sidebar"
1326
+ data-slot="sidebar-rail"
1327
+ ref={ref}
1328
+ type={type}
1329
+ className={cn(
1330
+ "absolute inset-y-0 end-[calc(var(--bh-sidebar-rail-width)*-1)] hidden w-[var(--bh-sidebar-rail-width)] cursor-ew-resize bg-transparent outline-none lg:block",
1331
+ className
1332
+ )}
1333
+ onClick={(event) => {
1334
+ onClick?.(event)
1335
+ if (!event.defaultPrevented) toggle()
1336
+ }}
1337
+ {...props}
1338
+ />
1339
+ )
1340
+ }
1341
+ )
1342
+
1343
+ /** Main-content wrapper paired with a sidebar shell. */
1344
+ const SidebarInset = React.forwardRef<HTMLDivElement, SidebarInsetProps>(
1345
+ function SidebarInset({ asChild = false, className, ...props }, ref) {
1346
+ const Comp = asChild ? Slot : "div"
1347
+
1348
+ return (
1349
+ <Comp
1350
+ data-slot="sidebar-inset"
1351
+ ref={ref}
1352
+ className={cn("min-w-0 flex-1", className)}
1353
+ {...props}
1354
+ />
1355
+ )
1356
+ }
1357
+ )
1358
+
1359
+ function getSlottableLabel(children: React.ReactNode, asChild: boolean) {
1360
+ if (!asChild || !React.isValidElement<{ children?: React.ReactNode }>(children)) {
1361
+ return children
1362
+ }
1363
+ return children.props.children
1364
+ }
1365
+
1366
+ function cloneSlottableChild(children: React.ReactNode, content: React.ReactNode) {
1367
+ if (!React.isValidElement<{ children?: React.ReactNode }>(children)) {
1368
+ throw new Error("Sidebar asChild expects one valid React element")
1369
+ }
1370
+ return React.cloneElement(children, undefined, content)
1371
+ }
1372
+
1373
+ function useSidebarContext() {
1374
+ const context = React.useContext(SidebarContext)
1375
+ if (!context) {
1376
+ throw new Error("Sidebar components must be used within SidebarProvider")
1377
+ }
1378
+
1379
+ return context
1380
+ }
1381
+
1382
+ export {
1383
+ Sidebar,
1384
+ SidebarBody,
1385
+ SidebarFooter,
1386
+ SidebarFooterUser,
1387
+ SidebarGroup,
1388
+ SidebarGroupAction,
1389
+ SidebarGroupLabel,
1390
+ SidebarHeader,
1391
+ SidebarHeaderAction,
1392
+ SidebarHeaderBrand,
1393
+ SidebarHeaderWorkspaceSwitch,
1394
+ SidebarInset,
1395
+ SidebarMenu,
1396
+ SidebarMenuBadge,
1397
+ SidebarMenuButton,
1398
+ SidebarMenuItem,
1399
+ SidebarMenuSub,
1400
+ SidebarMenuSubButton,
1401
+ SidebarMenuTreeItem,
1402
+ SidebarProvider,
1403
+ SidebarRail,
1404
+ SidebarToggle,
1405
+ SidebarTreeGroup,
1406
+ SidebarTrigger,
1407
+ }
1408
+ export type {
1409
+ SidebarFooterUserProps,
1410
+ SidebarGroupActionProps,
1411
+ SidebarGroupLabelProps,
1412
+ SidebarHeaderActionProps,
1413
+ SidebarHeaderBrandProps,
1414
+ SidebarHeaderWorkspaceSwitchProps,
1415
+ SidebarInsetProps,
1416
+ SidebarInteractionState,
1417
+ SidebarMenuBadgeProps,
1418
+ SidebarMenuButtonProps,
1419
+ SidebarMenuButtonVariant,
1420
+ SidebarMenuItemProps,
1421
+ SidebarMenuProps,
1422
+ SidebarMenuSubButtonProps,
1423
+ SidebarMenuSubProps,
1424
+ SidebarMenuTreeItemProps,
1425
+ SidebarProps,
1426
+ SidebarProviderProps,
1427
+ SidebarRailProps,
1428
+ SidebarSectionProps,
1429
+ SidebarToggleProps,
1430
+ SidebarTreeGroupProps,
1431
+ SidebarTriggerProps,
1432
+ SidebarWorkspaceVariant,
1433
+ }