banhaten 0.5.0 → 0.6.0

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