banhaten 0.2.0 → 0.4.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 (120) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +5 -5
  3. package/docs/design-system/component-implementation-ownership-audit.md +245 -0
  4. package/docs/design-system/component-quality-audit-2026-08-20.md +225 -0
  5. package/docs/design-system/component-showcase-consistency-report.md +2 -0
  6. package/docs/design-system/components/README.md +1 -0
  7. package/docs/design-system/components/activity-feed.md +2 -0
  8. package/docs/design-system/components/activity-heatmap.md +73 -0
  9. package/docs/design-system/components/alert-dialog.md +1 -1
  10. package/docs/design-system/components/alert.md +3 -3
  11. package/docs/design-system/components/attribute.md +1 -1
  12. package/docs/design-system/components/avatar.md +2 -2
  13. package/docs/design-system/components/badge.md +1 -1
  14. package/docs/design-system/components/banner.md +6 -2
  15. package/docs/design-system/components/breadcrumbs.md +5 -2
  16. package/docs/design-system/components/button-group.md +3 -2
  17. package/docs/design-system/components/button.md +2 -2
  18. package/docs/design-system/components/card.md +11 -3
  19. package/docs/design-system/components/catalog-components.md +3 -0
  20. package/docs/design-system/components/date-picker.md +3 -1
  21. package/docs/design-system/components/file-upload.md +5 -1
  22. package/docs/design-system/components/input.md +8 -6
  23. package/docs/design-system/components/item.md +3 -1
  24. package/docs/design-system/components/kbd.md +11 -8
  25. package/docs/design-system/components/menu.md +2 -2
  26. package/docs/design-system/components/modal.md +1 -1
  27. package/docs/design-system/components/navigation-menu.md +1 -1
  28. package/docs/design-system/components/onboarding-step-list-item.md +3 -2
  29. package/docs/design-system/components/page-header.md +3 -1
  30. package/docs/design-system/components/pagination.md +3 -2
  31. package/docs/design-system/components/radio-card.md +1 -1
  32. package/docs/design-system/components/resizable.md +1 -1
  33. package/docs/design-system/components/scroll-area.md +7 -3
  34. package/docs/design-system/components/select.md +1 -1
  35. package/docs/design-system/components/sidebar.md +60 -18
  36. package/docs/design-system/components/slideout.md +4 -1
  37. package/docs/design-system/components/steps.md +7 -2
  38. package/docs/design-system/components/table.md +25 -7
  39. package/docs/design-system/components/tag.md +3 -3
  40. package/docs/design-system/components/timeline.md +4 -1
  41. package/docs/design-system/components/toggle.md +1 -1
  42. package/docs/design-system/components/toolbar.md +1 -1
  43. package/docs/design-system/components/tooltip.md +1 -1
  44. package/docs/design-system/density-and-elevation.md +23 -2
  45. package/docs/design-system/gap-promotion-report.md +1 -67
  46. package/docs/design-system/icons.md +6 -1
  47. package/docs/design-system/installation.md +1 -1
  48. package/docs/design-system/launch-comparison-shadcn.md +10 -10
  49. package/docs/design-system/token-governance.md +7 -8
  50. package/package.json +3 -2
  51. package/registry/components/alert-dialog.tsx +1 -1
  52. package/registry/components/alert.tsx +17 -13
  53. package/registry/components/attribute.tsx +8 -2
  54. package/registry/components/avatar.tsx +28 -20
  55. package/registry/components/badge.tsx +17 -17
  56. package/registry/components/button-group.tsx +4 -2
  57. package/registry/components/button.tsx +3 -3
  58. package/registry/components/card.tsx +19 -3
  59. package/registry/components/carousel.tsx +14 -0
  60. package/registry/components/command.tsx +37 -3
  61. package/registry/components/date-picker.tsx +83 -35
  62. package/registry/components/expanded/ActivityFeed.tsx +3 -7
  63. package/registry/components/expanded/ActivityHeatmap.tsx +64 -0
  64. package/registry/components/expanded/Banner.tsx +0 -1
  65. package/registry/components/expanded/Breadcrumbs.tsx +3 -0
  66. package/registry/components/expanded/CatalogComponentsShowcase.tsx +3 -4
  67. package/registry/components/expanded/CatalogDivider.tsx +3 -2
  68. package/registry/components/expanded/FileUpload.tsx +7 -5
  69. package/registry/components/expanded/OnboardingStepListItem.tsx +6 -7
  70. package/registry/components/expanded/PageHeader.tsx +35 -44
  71. package/registry/components/expanded/Slideout.tsx +18 -13
  72. package/registry/components/expanded/Steps.tsx +147 -9
  73. package/registry/components/expanded/Table.tsx +50 -18
  74. package/registry/components/expanded/Timeline.tsx +11 -24
  75. package/registry/components/expanded/activityFeed.css +8 -8
  76. package/registry/components/expanded/activityHeatmap.css +66 -0
  77. package/registry/components/expanded/banner.css +30 -15
  78. package/registry/components/expanded/breadcrumbs.css +5 -5
  79. package/registry/components/expanded/fileUpload.css +4 -4
  80. package/registry/components/expanded/onboardingStepListItem.css +16 -7
  81. package/registry/components/expanded/pageHeader.css +14 -11
  82. package/registry/components/expanded/slideout.css +3 -7
  83. package/registry/components/expanded/steps.css +75 -12
  84. package/registry/components/expanded/table.css +63 -69
  85. package/registry/components/expanded/timeline.css +17 -16
  86. package/registry/components/field.tsx +61 -8
  87. package/registry/components/input.tsx +12 -5
  88. package/registry/components/item.tsx +1 -1
  89. package/registry/components/kbd.tsx +109 -15
  90. package/registry/components/menu.tsx +57 -7
  91. package/registry/components/modal.tsx +1 -1
  92. package/registry/components/navigation-menu.tsx +1 -1
  93. package/registry/components/pagination.css +36 -0
  94. package/registry/components/pagination.tsx +20 -13
  95. package/registry/components/progress-slider.tsx +1 -1
  96. package/registry/components/radio-card.tsx +1 -2
  97. package/registry/components/resizable.tsx +36 -2
  98. package/registry/components/scroll-area.tsx +30 -6
  99. package/registry/components/select.tsx +35 -12
  100. package/registry/components/sidebar.tsx +940 -62
  101. package/registry/components/social-button.tsx +12 -5
  102. package/registry/components/table-elements.tsx +128 -0
  103. package/registry/components/tabs.tsx +5 -10
  104. package/registry/components/tag.tsx +2 -2
  105. package/registry/components/toggle.tsx +2 -2
  106. package/registry/components/toolbar.tsx +6 -6
  107. package/registry/components/tooltip.tsx +1 -1
  108. package/registry/examples/attribute-demo.tsx +1 -1
  109. package/registry/examples/avatar-demo.tsx +7 -4
  110. package/registry/examples/command-palette-demo.tsx +0 -1
  111. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -0
  112. package/registry/examples/expanded/breadcrumbs-demo.tsx +3 -3
  113. package/registry/examples/kbd-demo.tsx +2 -1
  114. package/registry/examples/pagination-demo.tsx +5 -5
  115. package/registry/examples/sidebar-demo.tsx +59 -13
  116. package/registry/guidance/USAGE.md +1 -1
  117. package/registry/index.json +621 -60
  118. package/registry/styles/globals.css +211 -151
  119. package/registry.json +44 -6
  120. package/tokens/banhaten.tokens.json +325 -95
@@ -2,8 +2,24 @@
2
2
 
3
3
  import * as React from "react"
4
4
  import { Slot } from "@radix-ui/react-slot"
5
- import { PanelLeftIcon } from "lucide-react"
5
+ import {
6
+ Building2Icon,
7
+ ChevronDownIcon,
8
+ ChevronRightIcon,
9
+ ChevronsUpDownIcon,
10
+ FolderIcon,
11
+ PanelLeftIcon,
12
+ SearchIcon,
13
+ SparklesIcon,
14
+ UserIcon,
15
+ } from "lucide-react"
6
16
 
17
+ import {
18
+ Tooltip,
19
+ TooltipContent,
20
+ TooltipProvider,
21
+ TooltipTrigger,
22
+ } from "@/components/ui/tooltip"
7
23
  import { cn } from "@/lib/utils"
8
24
 
9
25
  type SidebarContextValue = {
@@ -23,21 +39,94 @@ type SidebarProps = React.ComponentProps<"aside"> & {
23
39
  side?: "start" | "end"
24
40
  }
25
41
  type SidebarSectionProps = React.ComponentProps<"div">
26
- type SidebarGroupLabelProps = React.ComponentProps<"div">
42
+ type SidebarHeaderActionProps = React.ComponentProps<"button"> & {
43
+ label?: string
44
+ }
45
+ type SidebarHeaderBrandProps = React.ComponentProps<"div"> & {
46
+ brand?: React.ReactNode
47
+ collapseAction?: React.ReactNode | false
48
+ logo?: React.ReactNode
49
+ onSearchClick?: React.MouseEventHandler<HTMLButtonElement>
50
+ searchAction?: React.ReactNode | false
51
+ searchLabel?: string
52
+ }
53
+ type SidebarWorkspaceVariant = "actions" | "card"
54
+ type SidebarHeaderWorkspaceSwitchProps = React.ComponentProps<"div"> & {
55
+ collapseAction?: React.ReactNode | false
56
+ image?: React.ReactNode
57
+ label?: React.ReactNode
58
+ onSearchClick?: React.MouseEventHandler<HTMLButtonElement>
59
+ onWorkspaceClick?: React.MouseEventHandler<HTMLButtonElement>
60
+ searchAction?: React.ReactNode | false
61
+ searchLabel?: string
62
+ supportText?: React.ReactNode
63
+ variant?: SidebarWorkspaceVariant
64
+ workspaceLabel?: string
65
+ }
66
+ type SidebarFooterUserProps = React.ComponentProps<"button"> & {
67
+ avatar?: React.ReactNode
68
+ name?: React.ReactNode
69
+ supportText?: React.ReactNode
70
+ trailingIcon?: React.ReactNode
71
+ variant?: "default" | "supporting-text"
72
+ }
73
+ type SidebarGroupLabelProps = React.ComponentProps<"div"> & {
74
+ actions?: React.ReactNode
75
+ expandIcon?: React.ReactNode
76
+ expandable?: boolean
77
+ }
78
+ type SidebarGroupActionProps = React.ComponentProps<"button"> & {
79
+ label?: string
80
+ }
27
81
  type SidebarMenuProps = React.ComponentProps<"ul">
28
82
  type SidebarMenuItemProps = React.ComponentProps<"li">
83
+ type SidebarInteractionState = "default" | "hover" | "active" | "focus"
84
+ type SidebarMenuButtonVariant =
85
+ | "default"
86
+ | "avatar"
87
+ | "level2"
88
+ | "level2-line"
29
89
  type SidebarMenuButtonProps = React.ComponentProps<"button"> & {
30
90
  active?: boolean
31
91
  asChild?: boolean
92
+ badge?: React.ReactNode
32
93
  icon?: React.ReactNode
94
+ state?: SidebarInteractionState
95
+ supportText?: React.ReactNode
96
+ tooltip?: React.ReactNode
97
+ trailingIcon?: React.ReactNode
98
+ variant?: SidebarMenuButtonVariant
99
+ }
100
+ type SidebarMenuBadgeProps = React.ComponentProps<"span">
101
+ type SidebarMenuSubProps = React.ComponentProps<"ul">
102
+ type SidebarMenuSubButtonProps = Omit<SidebarMenuButtonProps, "variant"> & {
103
+ showLine?: boolean
104
+ }
105
+ type SidebarMenuTreeItemProps = React.ComponentProps<"button"> & {
106
+ active?: boolean
107
+ asChild?: boolean
108
+ expanded?: boolean
109
+ icon?: React.ReactNode
110
+ level?: 1 | 2 | 3 | 4
111
+ showChevron?: boolean
112
+ state?: SidebarInteractionState
113
+ }
114
+ type SidebarTreeGroupProps = SidebarSectionProps & {
115
+ actions?: React.ReactNode
116
+ expandable?: boolean
117
+ expandIcon?: React.ReactNode
118
+ label?: React.ReactNode
33
119
  }
34
120
  type SidebarToggleProps = React.ComponentProps<"button">
35
121
  type SidebarTriggerProps = SidebarToggleProps
36
122
  type SidebarRailProps = React.ComponentProps<"button">
37
- type SidebarInsetProps = React.ComponentProps<"main">
123
+ type SidebarInsetProps = React.ComponentProps<"div"> & {
124
+ asChild?: boolean
125
+ }
38
126
 
39
127
  const SidebarContext = React.createContext<SidebarContextValue | null>(null)
40
128
 
129
+ /** Owns the controlled or uncontrolled collapsed state for a sidebar composition. */
41
130
  function SidebarProvider({
42
131
  children,
43
132
  className,
@@ -74,36 +163,42 @@ function SidebarProvider({
74
163
 
75
164
  return (
76
165
  <SidebarContext.Provider value={context}>
77
- <div
78
- data-collapsed={resolvedCollapsed ? "true" : undefined}
79
- data-slot="sidebar-provider"
80
- dir={dir}
81
- className={cn("min-w-0", className)}
82
- {...props}
83
- >
84
- {children}
85
- </div>
166
+ <TooltipProvider delayDuration={0}>
167
+ <div
168
+ data-collapsed={resolvedCollapsed ? "true" : "false"}
169
+ data-slot="sidebar-provider"
170
+ dir={dir}
171
+ className={cn("relative min-w-0", className)}
172
+ {...props}
173
+ >
174
+ {children}
175
+ </div>
176
+ </TooltipProvider>
86
177
  </SidebarContext.Provider>
87
178
  )
88
179
  }
89
180
 
181
+ /** The 280px navigation surface that contracts to the Figma 48px icon rail. */
90
182
  const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(function Sidebar(
91
183
  { className, side = "start", ...props },
92
184
  ref
93
185
  ) {
94
- const { collapsed, dir } = useSidebarContext()
186
+ const context = React.useContext(SidebarContext)
187
+ const collapsed = context?.collapsed ?? false
188
+ const dir = context?.dir
95
189
 
96
190
  return (
97
191
  <aside
98
- data-collapsed={collapsed ? "true" : undefined}
192
+ data-collapsed={collapsed ? "true" : "false"}
99
193
  data-side={side}
100
194
  data-slot="sidebar"
101
195
  dir={dir}
102
196
  ref={ref}
103
197
  className={cn(
104
- "group/sidebar flex h-full min-h-[var(--bh-space-19xl-384)] w-[var(--bh-sidebar-width)] max-w-full shrink-0 flex-col overflow-hidden bg-[var(--bh-sidebar-bg)] text-[var(--bh-content-default)] shadow-[var(--shadow-sidebar)] transition-[width,background-color]",
198
+ "group/sidebar flex h-full min-h-[var(--bh-space-19xl-384)] w-[var(--bh-sidebar-width)] max-w-full shrink-0 flex-col overflow-hidden bg-[var(--bh-sidebar-bg)] text-[var(--bh-content-default)] transition-[width,background-color]",
199
+ "border-e border-[var(--bh-sidebar-border)]",
105
200
  "data-[collapsed=true]:w-[var(--bh-sidebar-width-collapsed)] data-[collapsed=true]:bg-[var(--bh-sidebar-bg-collapsed)]",
106
- side === "end" && "ms-auto",
201
+ side === "end" && "ms-auto border-e-0 border-s",
107
202
  className
108
203
  )}
109
204
  {...props}
@@ -111,12 +206,14 @@ const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(function Sidebar(
111
206
  )
112
207
  })
113
208
 
209
+ /** Header slot with the Figma 10px expanded and 4px/8px collapsed insets. */
114
210
  function SidebarHeader({ className, ...props }: SidebarSectionProps) {
115
211
  return (
116
212
  <div
117
213
  data-slot="sidebar-header"
118
214
  className={cn(
119
- "flex min-h-[var(--bh-sidebar-item-height)] shrink-0 items-center gap-[var(--bh-sidebar-item-gap)] px-[var(--bh-sidebar-padding)] py-[var(--bh-space-xl-12)]",
215
+ "flex shrink-0 flex-col items-start gap-[var(--bh-space-lg-10)] overflow-hidden bg-[var(--bh-sidebar-bg)] p-[var(--bh-sidebar-padding)]",
216
+ "group-data-[collapsed=true]/sidebar:items-center group-data-[collapsed=true]/sidebar:px-[var(--bh-space-xs-4)] group-data-[collapsed=true]/sidebar:py-[var(--bh-space-md-8)]",
120
217
  className
121
218
  )}
122
219
  {...props}
@@ -124,12 +221,262 @@ function SidebarHeader({ className, ...props }: SidebarSectionProps) {
124
221
  )
125
222
  }
126
223
 
224
+ /** Compact 28px ghost action used by Figma sidebar headers. */
225
+ const SidebarHeaderAction = React.forwardRef<
226
+ HTMLButtonElement,
227
+ SidebarHeaderActionProps
228
+ >(function SidebarHeaderAction(
229
+ { "aria-label": ariaLabel, children, className, label, type = "button", ...props },
230
+ ref
231
+ ) {
232
+ return (
233
+ <button
234
+ aria-label={ariaLabel ?? label ?? "Sidebar action"}
235
+ data-slot="sidebar-header-action"
236
+ ref={ref}
237
+ type={type}
238
+ className={cn(
239
+ "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]",
240
+ "hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:border-[var(--bh-border-focus)] focus-visible:shadow-[var(--shadow-button-focus)]",
241
+ className
242
+ )}
243
+ {...props}
244
+ >
245
+ <span
246
+ aria-hidden="true"
247
+ data-slot="sidebar-header-action-icon"
248
+ className="inline-flex size-[var(--bh-sidebar-icon-slot-size)] items-center justify-center [&_svg]:size-[var(--bh-sidebar-icon-size)] [&_svg]:shrink-0"
249
+ >
250
+ {children}
251
+ </span>
252
+ </button>
253
+ )
254
+ })
255
+
256
+ /** Brand header with optional search and collapse actions; actions disappear on the icon rail. */
257
+ function SidebarHeaderBrand({
258
+ brand = "Banhaten",
259
+ className,
260
+ collapseAction,
261
+ logo,
262
+ onSearchClick,
263
+ searchAction,
264
+ searchLabel = "Search",
265
+ ...props
266
+ }: SidebarHeaderBrandProps) {
267
+ const { collapsed } = useSidebarContext()
268
+
269
+ return (
270
+ <div
271
+ data-collapsed={collapsed ? "true" : "false"}
272
+ data-slot="sidebar-header-brand"
273
+ className={cn(
274
+ "flex h-[var(--bh-sidebar-brand-height)] w-full items-center justify-between py-[var(--bh-space-xs-4)] ps-[var(--bh-space-sm-6)] pe-[var(--bh-space-xs-4)]",
275
+ collapsed &&
276
+ "size-[var(--bh-sidebar-compact-control-size)] justify-center gap-[var(--bh-space-md-8)] p-0",
277
+ className
278
+ )}
279
+ {...props}
280
+ >
281
+ <span
282
+ data-slot="sidebar-brand-content"
283
+ className="flex min-w-0 items-center gap-[var(--bh-space-md-8)]"
284
+ >
285
+ <span
286
+ data-slot="sidebar-brand-logo"
287
+ 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"
288
+ >
289
+ {logo ?? (
290
+ <span className="inline-flex size-full items-center justify-center bg-[var(--bh-bg-brand-default)] text-[var(--bh-content-on-brand)]">
291
+ <SparklesIcon
292
+ aria-hidden="true"
293
+ className="size-[var(--bh-sidebar-icon-size)]"
294
+ />
295
+ </span>
296
+ )}
297
+ </span>
298
+ {!collapsed ? (
299
+ <span
300
+ data-slot="sidebar-brand-name"
301
+ dir="auto"
302
+ className="truncate 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)]"
303
+ >
304
+ {brand}
305
+ </span>
306
+ ) : null}
307
+ </span>
308
+
309
+ {!collapsed ? (
310
+ <span
311
+ data-slot="sidebar-brand-actions"
312
+ className="flex shrink-0 items-center gap-[var(--bh-space-sm-6)]"
313
+ >
314
+ {searchAction === false
315
+ ? null
316
+ : searchAction ?? (
317
+ <SidebarHeaderAction label={searchLabel} onClick={onSearchClick}>
318
+ <SearchIcon aria-hidden="true" />
319
+ </SidebarHeaderAction>
320
+ )}
321
+ {collapseAction === false ? null : collapseAction ?? <SidebarTrigger />}
322
+ </span>
323
+ ) : null}
324
+ </div>
325
+ )
326
+ }
327
+
328
+ /** Workspace switcher in either the header-actions or bordered card treatment. */
329
+ function SidebarHeaderWorkspaceSwitch({
330
+ className,
331
+ collapseAction,
332
+ image,
333
+ label = "Acme Inc",
334
+ onSearchClick,
335
+ onWorkspaceClick,
336
+ searchAction,
337
+ searchLabel = "Search",
338
+ supportText = "Pro plan",
339
+ variant = "actions",
340
+ workspaceLabel = "Switch workspace",
341
+ ...props
342
+ }: SidebarHeaderWorkspaceSwitchProps) {
343
+ const { collapsed } = useSidebarContext()
344
+ const card = variant === "card"
345
+
346
+ const workspaceButton = (
347
+ <button
348
+ aria-label={workspaceLabel}
349
+ data-slot="sidebar-workspace-trigger"
350
+ onClick={onWorkspaceClick}
351
+ type="button"
352
+ className={cn(
353
+ "group/workspace relative flex min-w-0 items-center text-start outline-none transition-[background-color,border-color,box-shadow]",
354
+ card
355
+ ? "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)]"
356
+ : "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)]",
357
+ collapsed && card &&
358
+ "size-[var(--bh-sidebar-compact-control-size)] justify-center gap-0 p-0",
359
+ collapsed && !card &&
360
+ "size-[var(--bh-sidebar-item-height-collapsed)] max-w-[var(--bh-sidebar-item-height-collapsed)] justify-center gap-0 p-0"
361
+ )}
362
+ >
363
+ <span
364
+ data-slot="sidebar-workspace-image"
365
+ className={cn(
366
+ "inline-flex shrink-0 items-center justify-center overflow-hidden [&>*]:size-full",
367
+ card
368
+ ? "size-[var(--bh-space-6xl-32)] rounded-[var(--bh-radius-md-6)]"
369
+ : "size-[var(--bh-space-5xl-24)] rounded-[var(--bh-radius-lg-8)]",
370
+ collapsed && !card &&
371
+ "size-[var(--bh-sidebar-header-action-size)] rounded-[var(--bh-radius-full)]"
372
+ )}
373
+ >
374
+ {image ?? (
375
+ <Building2Icon
376
+ aria-hidden="true"
377
+ className="size-[var(--bh-sidebar-icon-size)]"
378
+ />
379
+ )}
380
+ </span>
381
+
382
+ {!collapsed ? (
383
+ card ? (
384
+ <span className="flex min-w-0 flex-1 flex-col items-start justify-center">
385
+ <span
386
+ data-slot="sidebar-workspace-label"
387
+ dir="auto"
388
+ 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)]"
389
+ >
390
+ {label}
391
+ </span>
392
+ <span
393
+ data-slot="sidebar-workspace-support"
394
+ dir="auto"
395
+ 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)]"
396
+ >
397
+ {supportText}
398
+ </span>
399
+ </span>
400
+ ) : (
401
+ <span className="flex min-w-0 items-center gap-[var(--bh-space-xxs-2)]">
402
+ <span
403
+ data-slot="sidebar-workspace-label"
404
+ dir="auto"
405
+ 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)]"
406
+ >
407
+ {label}
408
+ </span>
409
+ <ChevronDownIcon
410
+ aria-hidden="true"
411
+ className="size-[var(--bh-space-3xl-16)] shrink-0"
412
+ data-rtl-flip="true"
413
+ />
414
+ </span>
415
+ )
416
+ ) : !card ? (
417
+ <span
418
+ aria-hidden="true"
419
+ data-slot="sidebar-workspace-caret"
420
+ className="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)]"
421
+ >
422
+ <ChevronDownIcon className="size-[var(--bh-space-xl-12)]" />
423
+ </span>
424
+ ) : null}
425
+
426
+ {card && !collapsed ? (
427
+ <ChevronsUpDownIcon
428
+ aria-hidden="true"
429
+ className="size-[var(--bh-space-3xl-16)] shrink-0 text-[var(--bh-content-muted)]"
430
+ />
431
+ ) : null}
432
+ </button>
433
+ )
434
+
435
+ return (
436
+ <div
437
+ data-collapsed={collapsed ? "true" : "false"}
438
+ data-slot="sidebar-header-workspace-switch"
439
+ data-variant={variant}
440
+ className={cn(
441
+ "flex w-full items-center",
442
+ card
443
+ ? "h-[var(--bh-sidebar-workspace-card-height)]"
444
+ : "h-[var(--bh-sidebar-compact-control-size)] justify-between pe-[var(--bh-space-md-8)]",
445
+ collapsed && card && "size-[var(--bh-sidebar-compact-control-size)]",
446
+ collapsed && !card &&
447
+ "size-[var(--bh-sidebar-item-height-collapsed)] justify-center p-0",
448
+ className
449
+ )}
450
+ {...props}
451
+ >
452
+ {workspaceButton}
453
+ {!card && !collapsed ? (
454
+ <span
455
+ data-slot="sidebar-workspace-actions"
456
+ className="flex shrink-0 items-center gap-[var(--bh-space-sm-6)]"
457
+ >
458
+ {searchAction === false
459
+ ? null
460
+ : searchAction ?? (
461
+ <SidebarHeaderAction label={searchLabel} onClick={onSearchClick}>
462
+ <SearchIcon aria-hidden="true" />
463
+ </SidebarHeaderAction>
464
+ )}
465
+ {collapseAction === false ? null : collapseAction ?? <SidebarTrigger />}
466
+ </span>
467
+ ) : null}
468
+ </div>
469
+ )
470
+ }
471
+
472
+ /** Scrollable content region with Figma's expanded and rail insets. */
127
473
  function SidebarBody({ className, ...props }: SidebarSectionProps) {
128
474
  return (
129
475
  <div
130
476
  data-slot="sidebar-body"
131
477
  className={cn(
132
- "min-h-0 flex-1 overflow-auto px-[var(--bh-sidebar-padding)] py-[var(--bh-space-xl-12)]",
478
+ "min-h-0 flex-1 overflow-auto px-[var(--bh-space-xl-12)] py-0",
479
+ "group-data-[collapsed=true]/sidebar:px-0",
133
480
  className
134
481
  )}
135
482
  {...props}
@@ -137,12 +484,14 @@ function SidebarBody({ className, ...props }: SidebarSectionProps) {
137
484
  )
138
485
  }
139
486
 
487
+ /** Bottom-pinned footer with the matching expanded and collapsed padding. */
140
488
  function SidebarFooter({ className, ...props }: SidebarSectionProps) {
141
489
  return (
142
490
  <div
143
491
  data-slot="sidebar-footer"
144
492
  className={cn(
145
- "shrink-0 border-t border-[var(--bh-sidebar-border)] px-[var(--bh-sidebar-padding)] py-[var(--bh-space-xl-12)]",
493
+ "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)]",
494
+ "group-data-[collapsed=true]/sidebar:p-[var(--bh-space-xs-4)]",
146
495
  className
147
496
  )}
148
497
  {...props}
@@ -150,17 +499,132 @@ function SidebarFooter({ className, ...props }: SidebarSectionProps) {
150
499
  )
151
500
  }
152
501
 
502
+ /** User switcher used in the footer, with 48px supporting and 40px compact rows. */
503
+ const SidebarFooterUser = React.forwardRef<
504
+ HTMLButtonElement,
505
+ SidebarFooterUserProps
506
+ >(function SidebarFooterUser(
507
+ {
508
+ "aria-label": ariaLabel,
509
+ avatar,
510
+ className,
511
+ name = "Username",
512
+ supportText = "user@example.com",
513
+ trailingIcon,
514
+ type = "button",
515
+ variant = "supporting-text",
516
+ ...props
517
+ },
518
+ ref
519
+ ) {
520
+ const { collapsed } = useSidebarContext()
521
+ const supporting = variant === "supporting-text"
522
+ const resolvedLabel =
523
+ ariaLabel ?? (typeof name === "string" ? name : "Open user menu")
524
+
525
+ return (
526
+ <button
527
+ aria-label={resolvedLabel}
528
+ data-collapsed={collapsed ? "true" : "false"}
529
+ data-slot="sidebar-footer-user"
530
+ data-variant={variant}
531
+ ref={ref}
532
+ type={type}
533
+ className={cn(
534
+ "flex w-full items-center rounded-[var(--bh-radius-lg-8)] text-start outline-none transition-[background-color,border-color,box-shadow]",
535
+ supporting
536
+ ? "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)]"
537
+ : "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)]",
538
+ "focus-visible:border-[length:var(--bh-border-width-strong)] focus-visible:border-[var(--bh-border-focus)]",
539
+ collapsed &&
540
+ "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)]",
541
+ className
542
+ )}
543
+ {...props}
544
+ >
545
+ <span
546
+ data-slot="sidebar-footer-user-content"
547
+ className={cn(
548
+ "flex min-w-0 flex-1 items-center gap-[var(--bh-space-md-8)] px-[var(--bh-space-xxs-2)]",
549
+ collapsed &&
550
+ "size-[var(--bh-sidebar-compact-control-size)] flex-none justify-center gap-0 p-0"
551
+ )}
552
+ >
553
+ <span
554
+ data-slot="sidebar-footer-user-avatar"
555
+ className={cn(
556
+ "inline-flex shrink-0 items-center justify-center overflow-hidden rounded-[var(--bh-radius-full)] [&>*]:size-full",
557
+ supporting ? "size-[var(--bh-space-6xl-32)]" : "size-[var(--bh-space-5xl-24)]",
558
+ collapsed && "size-[var(--bh-sidebar-compact-control-size)]"
559
+ )}
560
+ >
561
+ {avatar ?? (
562
+ <UserIcon
563
+ aria-hidden="true"
564
+ className="size-[var(--bh-sidebar-icon-size)]"
565
+ />
566
+ )}
567
+ </span>
568
+ {!collapsed ? (
569
+ <span
570
+ data-slot="sidebar-footer-user-text"
571
+ className="flex min-w-0 flex-1 flex-col items-start justify-center"
572
+ >
573
+ <span
574
+ data-slot="sidebar-footer-user-name"
575
+ dir="auto"
576
+ 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)]"
577
+ >
578
+ {name}
579
+ </span>
580
+ {supporting ? (
581
+ <span
582
+ data-slot="sidebar-footer-user-support"
583
+ dir="auto"
584
+ 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)]"
585
+ >
586
+ {supportText}
587
+ </span>
588
+ ) : null}
589
+ </span>
590
+ ) : null}
591
+ </span>
592
+ {!collapsed ? (
593
+ <span
594
+ aria-hidden="true"
595
+ data-slot="sidebar-footer-user-trailing"
596
+ 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)]"
597
+ >
598
+ {trailingIcon ?? <ChevronsUpDownIcon />}
599
+ </span>
600
+ ) : null}
601
+ </button>
602
+ )
603
+ })
604
+
605
+ /** A Figma sidebar group: 12px top padding, 6px bottom padding, and no menu gap. */
153
606
  function SidebarGroup({ className, ...props }: SidebarSectionProps) {
154
607
  return (
155
608
  <div
156
609
  data-slot="sidebar-group"
157
- className={cn("grid gap-[var(--bh-space-md-8)]", className)}
610
+ className={cn(
611
+ "flex w-full flex-col items-start pb-[var(--bh-space-sm-6)] pt-[var(--bh-space-xl-12)]",
612
+ className
613
+ )}
158
614
  {...props}
159
615
  />
160
616
  )
161
617
  }
162
618
 
163
- function SidebarGroupLabel({ className, ...props }: SidebarGroupLabelProps) {
619
+ /** Group label that reveals its disclosure and actions on hover or keyboard focus. */
620
+ function SidebarGroupLabel({
621
+ actions,
622
+ children,
623
+ className,
624
+ expandIcon,
625
+ expandable = false,
626
+ ...props
627
+ }: SidebarGroupLabelProps) {
164
628
  const { collapsed } = useSidebarContext()
165
629
 
166
630
  if (collapsed) return null
@@ -168,26 +632,84 @@ function SidebarGroupLabel({ className, ...props }: SidebarGroupLabelProps) {
168
632
  return (
169
633
  <div
170
634
  data-slot="sidebar-group-label"
171
- dir="auto"
172
635
  className={cn(
173
- "px-[var(--bh-sidebar-item-padding-x)] text-[length:var(--bh-text-body-xs-medium-font-size)] font-[var(--bh-text-body-xs-medium-font-weight)] uppercase leading-[var(--bh-text-body-xs-medium-line-height)] tracking-[var(--bh-text-body-xs-medium-letter-spacing)] text-[var(--bh-content-muted)]",
636
+ "group/label flex h-[var(--bh-space-5xl-24)] w-full items-center gap-[var(--bh-space-xs-4)] rounded-[var(--bh-radius-lg-8)] px-[var(--bh-space-sm-6)] pb-[var(--bh-space-xs-4)]",
174
637
  className
175
638
  )}
176
639
  {...props}
177
- />
640
+ >
641
+ <span
642
+ data-slot="sidebar-group-label-content"
643
+ className="flex min-w-0 flex-1 items-center gap-[var(--bh-space-xxxs-1)]"
644
+ >
645
+ <span
646
+ data-slot="sidebar-group-label-text"
647
+ dir="auto"
648
+ 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)]"
649
+ >
650
+ {children}
651
+ </span>
652
+ {expandable ? (
653
+ <span
654
+ aria-hidden="true"
655
+ data-slot="sidebar-group-label-expand"
656
+ 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"
657
+ >
658
+ {expandIcon ?? <ChevronRightIcon data-rtl-flip="true" />}
659
+ </span>
660
+ ) : null}
661
+ </span>
662
+ {actions ? (
663
+ <span
664
+ data-slot="sidebar-group-label-actions"
665
+ 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)]"
666
+ >
667
+ {actions}
668
+ </span>
669
+ ) : null}
670
+ </div>
178
671
  )
179
672
  }
180
673
 
674
+ /** 20px group-label icon slot matching the Default, Hover, and Focus variants. */
675
+ const SidebarGroupAction = React.forwardRef<
676
+ HTMLButtonElement,
677
+ SidebarGroupActionProps
678
+ >(function SidebarGroupAction(
679
+ { "aria-label": ariaLabel, children, className, label, type = "button", ...props },
680
+ ref
681
+ ) {
682
+ return (
683
+ <button
684
+ aria-label={ariaLabel ?? label ?? "Group action"}
685
+ data-slot="sidebar-group-action"
686
+ ref={ref}
687
+ type={type}
688
+ className={cn(
689
+ "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]",
690
+ "hover:bg-[var(--bh-interactive-ghost-hover)] hover:text-[var(--bh-content-default)] focus-visible:border-[var(--bh-border-focus)]",
691
+ "[&_svg]:size-[var(--bh-sidebar-icon-size)] [&_svg]:shrink-0",
692
+ className
693
+ )}
694
+ {...props}
695
+ >
696
+ {children}
697
+ </button>
698
+ )
699
+ })
700
+
701
+ /** List wrapper for contiguous 32px sidebar menu rows. */
181
702
  function SidebarMenu({ className, ...props }: SidebarMenuProps) {
182
703
  return (
183
704
  <ul
184
705
  data-slot="sidebar-menu"
185
- className={cn("m-0 grid list-none gap-[var(--bh-space-xs-4)] p-0", className)}
706
+ className={cn("m-0 flex w-full list-none flex-col p-0", className)}
186
707
  {...props}
187
708
  />
188
709
  )
189
710
  }
190
711
 
712
+ /** Semantic list item wrapper. */
191
713
  function SidebarMenuItem({ className, ...props }: SidebarMenuItemProps) {
192
714
  return (
193
715
  <li
@@ -198,50 +720,359 @@ function SidebarMenuItem({ className, ...props }: SidebarMenuItemProps) {
198
720
  )
199
721
  }
200
722
 
723
+ /** Native 24px neutral count badge for sidebar rows. */
724
+ function SidebarMenuBadge({ children, className, ...props }: SidebarMenuBadgeProps) {
725
+ return (
726
+ <span
727
+ data-slot="sidebar-menu-badge"
728
+ className={cn(
729
+ "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)]",
730
+ className
731
+ )}
732
+ {...props}
733
+ >
734
+ {children}
735
+ </span>
736
+ )
737
+ }
738
+
739
+ /** Default, avatar, Level 2, and Level 2 line menu variants from the Figma set. */
201
740
  const SidebarMenuButton = React.forwardRef<
202
741
  HTMLButtonElement,
203
742
  SidebarMenuButtonProps
204
743
  >(function SidebarMenuButton(
205
- { active = false, asChild = false, children, className, icon, ...props },
744
+ {
745
+ "aria-label": ariaLabel,
746
+ active = false,
747
+ asChild = false,
748
+ badge,
749
+ children,
750
+ className,
751
+ icon,
752
+ state = "default",
753
+ supportText,
754
+ tooltip,
755
+ trailingIcon,
756
+ type = "button",
757
+ variant = "default",
758
+ ...props
759
+ },
206
760
  ref
207
761
  ) {
208
- const { collapsed } = useSidebarContext()
209
- const Comp = asChild ? Slot : "button"
762
+ const { collapsed, dir } = useSidebarContext()
763
+ const isLevel2 = variant === "level2" || variant === "level2-line"
764
+ const label = getSlottableLabel(children, asChild)
765
+ const resolvedLabel =
766
+ ariaLabel ?? (typeof label === "string" ? label : undefined)
767
+
768
+ if (collapsed && isLevel2) return null
769
+
770
+ const content = (
771
+ <>
772
+ {isLevel2 ? (
773
+ <span
774
+ aria-hidden="true"
775
+ data-slot="sidebar-menu-indent"
776
+ className="relative h-full w-[var(--bh-sidebar-level-2-indent-width)] shrink-0"
777
+ >
778
+ {variant === "level2-line" ? (
779
+ <span
780
+ data-slot="sidebar-menu-guide"
781
+ className={cn(
782
+ "absolute start-[var(--bh-space-md-8)]",
783
+ active
784
+ ? "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)]"
785
+ : "inset-y-0 border-e-[length:var(--bh-border-width-default)] border-[var(--bh-border-subtle)]"
786
+ )}
787
+ />
788
+ ) : null}
789
+ </span>
790
+ ) : null}
210
791
 
211
- return (
212
- <Comp
213
- aria-current={active ? "page" : undefined}
214
- data-active={active ? "true" : undefined}
215
- data-slot="sidebar-menu-button"
216
- ref={ref}
217
- className={cn(
218
- "flex h-[var(--bh-sidebar-item-height)] w-full min-w-0 items-center gap-[var(--bh-sidebar-item-gap)] rounded-[var(--bh-radius-lg-8)] px-[var(--bh-sidebar-item-padding-x)] text-start text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)] text-[var(--bh-sidebar-item-content)] outline-none transition-[background-color,box-shadow,color]",
219
- "hover:bg-[var(--bh-sidebar-item-bg-hover)] focus-visible:shadow-[var(--shadow-button-focus)] data-[active=true]:bg-[var(--bh-sidebar-item-bg-active)] data-[active=true]:text-[var(--bh-sidebar-item-content-active)]",
220
- collapsed && "justify-center px-[var(--bh-space-md-8)]",
221
- className
222
- )}
223
- {...props}
224
- >
225
792
  {icon ? (
226
793
  <span
227
794
  aria-hidden="true"
228
795
  data-slot="sidebar-menu-icon"
229
- className="inline-flex size-[var(--bh-sidebar-icon-size)] shrink-0 items-center justify-center"
796
+ className={cn(
797
+ "inline-flex shrink-0 items-center justify-center overflow-hidden [&>*]:size-full [&_svg]:shrink-0",
798
+ variant === "avatar"
799
+ ? "size-[var(--bh-space-4xl-20)] rounded-[var(--bh-radius-md-6)]"
800
+ : "size-[var(--bh-sidebar-icon-slot-size)] [&_svg]:size-[var(--bh-sidebar-icon-size)]",
801
+ collapsed && variant === "avatar" &&
802
+ "size-[var(--bh-space-5xl-24)] rounded-[var(--bh-radius-lg-8)]",
803
+ collapsed && variant === "default" &&
804
+ "[&_svg]:size-[var(--bh-sidebar-icon-size-collapsed)]"
805
+ )}
230
806
  >
231
807
  {icon}
232
808
  </span>
233
809
  ) : null}
234
- {collapsed ? null : (
235
- <span data-slot="sidebar-menu-label" dir="auto" className="min-w-0 flex-1 truncate">
236
- {children}
810
+
811
+ {!collapsed ? (
812
+ <span
813
+ data-slot="sidebar-menu-label"
814
+ dir="auto"
815
+ className="min-w-0 flex-1 truncate ps-[var(--bh-space-xs-4)]"
816
+ >
817
+ {label}
237
818
  </span>
238
- )}
239
- </Comp>
819
+ ) : null}
820
+
821
+ {!collapsed && supportText ? (
822
+ <span
823
+ data-slot="sidebar-menu-support"
824
+ dir="auto"
825
+ className="shrink-0 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)]"
826
+ >
827
+ {supportText}
828
+ </span>
829
+ ) : null}
830
+
831
+ {!collapsed && badge ? (
832
+ <span data-slot="sidebar-menu-badge-slot" className="inline-flex shrink-0">
833
+ {badge}
834
+ </span>
835
+ ) : null}
836
+
837
+ {!collapsed && trailingIcon ? (
838
+ <span
839
+ aria-hidden="true"
840
+ data-slot="sidebar-menu-trailing"
841
+ className="inline-flex size-[var(--bh-sidebar-icon-slot-size)] shrink-0 items-center justify-center text-[var(--bh-content-muted)] [&_svg]:size-[var(--bh-sidebar-icon-size)] [&_svg]:shrink-0"
842
+ >
843
+ {trailingIcon}
844
+ </span>
845
+ ) : null}
846
+ </>
847
+ )
848
+
849
+ const buttonClassName = cn(
850
+ "group/menu-button relative flex w-full min-w-0 items-center gap-[var(--bh-space-sm-6)] 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)] outline-none transition-[background-color,border-color,color]",
851
+ "hover:rounded-[var(--bh-radius-md-6)] hover:bg-[var(--bh-sidebar-item-bg-hover)]",
852
+ "focus-visible:rounded-[var(--bh-radius-md-6)] focus-visible:border-[var(--bh-border-focus)]",
853
+ "data-[state=hover]:rounded-[var(--bh-radius-md-6)] data-[state=hover]:bg-[var(--bh-sidebar-item-bg-hover)]",
854
+ "data-[state=focus]:rounded-[var(--bh-radius-md-6)] data-[state=focus]:border-[var(--bh-border-focus)]",
855
+ "disabled:pointer-events-none disabled:text-[var(--bh-content-disabled)]",
856
+ variant === "avatar"
857
+ ? "h-[var(--bh-sidebar-compact-control-size)]"
858
+ : "h-[var(--bh-sidebar-item-height)]",
859
+ (active || state === "active") &&
860
+ "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)]",
861
+ collapsed &&
862
+ "mx-auto size-[var(--bh-sidebar-item-height-collapsed)] justify-center gap-0 rounded-[var(--bh-radius-lg-8)] p-0",
863
+ className
864
+ )
865
+
866
+ const commonProps = {
867
+ "aria-current": active ? ("page" as const) : undefined,
868
+ "aria-label": resolvedLabel,
869
+ "data-active": active ? "true" : "false",
870
+ "data-collapsed": collapsed ? "true" : "false",
871
+ "data-slot": "sidebar-menu-button",
872
+ "data-state": state,
873
+ "data-variant": variant,
874
+ className: buttonClassName,
875
+ }
876
+
877
+ const button = asChild ? (
878
+ <Slot {...commonProps} ref={ref} {...props}>
879
+ {cloneSlottableChild(children, content)}
880
+ </Slot>
881
+ ) : (
882
+ <button {...commonProps} ref={ref} type={type} {...props}>
883
+ {content}
884
+ </button>
885
+ )
886
+
887
+ const resolvedTooltip = tooltip ?? resolvedLabel
888
+ if (!collapsed || !resolvedTooltip) return button
889
+
890
+ return (
891
+ <Tooltip>
892
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
893
+ <TooltipContent
894
+ pointerPosition={dir === "rtl" ? "center-right" : "center-left"}
895
+ showShortcut={false}
896
+ side={dir === "rtl" ? "left" : "right"}
897
+ >
898
+ {resolvedTooltip}
899
+ </TooltipContent>
900
+ </Tooltip>
901
+ )
902
+ })
903
+
904
+ /** Nested menu list that unmounts when the parent sidebar is collapsed. */
905
+ function SidebarMenuSub({ className, ...props }: SidebarMenuSubProps) {
906
+ const { collapsed } = useSidebarContext()
907
+ if (collapsed) return null
908
+
909
+ return (
910
+ <ul
911
+ data-slot="sidebar-menu-sub"
912
+ className={cn("m-0 flex w-full list-none flex-col p-0", className)}
913
+ {...props}
914
+ />
915
+ )
916
+ }
917
+
918
+ /** Level 2 menu row with an optional vertical guide. */
919
+ const SidebarMenuSubButton = React.forwardRef<
920
+ HTMLButtonElement,
921
+ SidebarMenuSubButtonProps
922
+ >(function SidebarMenuSubButton({ showLine = false, ...props }, ref) {
923
+ return (
924
+ <SidebarMenuButton
925
+ ref={ref}
926
+ variant={showLine ? "level2-line" : "level2"}
927
+ {...props}
928
+ />
929
+ )
930
+ })
931
+
932
+ const treeLevelClasses: Record<NonNullable<SidebarMenuTreeItemProps["level"]>, string> = {
933
+ 1: "ps-[var(--bh-space-sm-6)]",
934
+ 2: "ps-[calc(var(--bh-space-sm-6)+var(--bh-sidebar-tree-indent-step))]",
935
+ 3: "ps-[calc(var(--bh-space-sm-6)+var(--bh-sidebar-tree-indent-step)+var(--bh-sidebar-tree-indent-step))]",
936
+ 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))]",
937
+ }
938
+
939
+ /** 28px file-tree row with four Figma indentation levels. */
940
+ const SidebarMenuTreeItem = React.forwardRef<
941
+ HTMLButtonElement,
942
+ SidebarMenuTreeItemProps
943
+ >(function SidebarMenuTreeItem(
944
+ {
945
+ "aria-label": ariaLabel,
946
+ active = false,
947
+ asChild = false,
948
+ children,
949
+ className,
950
+ expanded = false,
951
+ icon,
952
+ level = 1,
953
+ showChevron = true,
954
+ state = "default",
955
+ type = "button",
956
+ ...props
957
+ },
958
+ ref
959
+ ) {
960
+ const { collapsed } = useSidebarContext()
961
+ if (collapsed) return null
962
+
963
+ const label = getSlottableLabel(children, asChild)
964
+ const resolvedLabel =
965
+ ariaLabel ?? (typeof label === "string" ? label : undefined)
966
+ const content = (
967
+ <>
968
+ {showChevron ? (
969
+ <span
970
+ aria-hidden="true"
971
+ data-slot="sidebar-tree-chevron"
972
+ className="inline-flex size-[var(--bh-space-3xl-16)] shrink-0 items-center justify-center [&_svg]:size-full"
973
+ >
974
+ {expanded ? (
975
+ <ChevronDownIcon />
976
+ ) : (
977
+ <ChevronRightIcon data-rtl-flip="true" />
978
+ )}
979
+ </span>
980
+ ) : null}
981
+ <span
982
+ aria-hidden="true"
983
+ data-slot="sidebar-tree-icon"
984
+ className="inline-flex size-[var(--bh-space-3xl-16)] shrink-0 items-center justify-center [&_svg]:size-full"
985
+ >
986
+ {icon ?? <FolderIcon />}
987
+ </span>
988
+ <span
989
+ data-slot="sidebar-tree-label"
990
+ dir="auto"
991
+ className="min-w-0 flex-1 truncate"
992
+ >
993
+ {label}
994
+ </span>
995
+ </>
996
+ )
997
+
998
+ const commonProps = {
999
+ "aria-current": active ? ("page" as const) : undefined,
1000
+ "aria-expanded": showChevron ? expanded : undefined,
1001
+ "aria-label": resolvedLabel,
1002
+ "data-active": active ? "true" : "false",
1003
+ "data-level": String(level),
1004
+ "data-slot": "sidebar-menu-tree-item",
1005
+ "data-state": state,
1006
+ className: cn(
1007
+ "flex h-[var(--bh-sidebar-tree-item-height)] w-full min-w-0 items-center gap-[var(--bh-space-sm-6)] 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)] outline-none transition-[background-color,border-color,color]",
1008
+ "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)]",
1009
+ "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)]",
1010
+ (active || state === "active") &&
1011
+ "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)]",
1012
+ treeLevelClasses[level],
1013
+ className
1014
+ ),
1015
+ }
1016
+
1017
+ return asChild ? (
1018
+ <Slot {...commonProps} ref={ref} {...props}>
1019
+ {cloneSlottableChild(children, content)}
1020
+ </Slot>
1021
+ ) : (
1022
+ <button {...commonProps} ref={ref} type={type} {...props}>
1023
+ {content}
1024
+ </button>
240
1025
  )
241
1026
  })
242
1027
 
1028
+ /** Labeled tree group used for file-browser and hierarchy sidebars. */
1029
+ function SidebarTreeGroup({
1030
+ actions,
1031
+ children,
1032
+ className,
1033
+ expandable = false,
1034
+ expandIcon,
1035
+ label,
1036
+ ...props
1037
+ }: SidebarTreeGroupProps) {
1038
+ const { collapsed } = useSidebarContext()
1039
+ if (collapsed) return null
1040
+
1041
+ return (
1042
+ <div
1043
+ data-slot="sidebar-tree-group"
1044
+ className={cn(
1045
+ "flex w-full flex-col items-start gap-[var(--bh-space-sm-6)] pb-[var(--bh-space-sm-6)] pt-[var(--bh-space-xl-12)]",
1046
+ className
1047
+ )}
1048
+ {...props}
1049
+ >
1050
+ {label ? (
1051
+ <SidebarGroupLabel
1052
+ actions={actions}
1053
+ className={cn(
1054
+ expandable &&
1055
+ "[&_[data-slot=sidebar-group-label-expand]]:opacity-[var(--bh-opacity-100)]"
1056
+ )}
1057
+ expandable={expandable}
1058
+ expandIcon={expandIcon ?? (expandable ? <ChevronDownIcon /> : undefined)}
1059
+ >
1060
+ {label}
1061
+ </SidebarGroupLabel>
1062
+ ) : null}
1063
+ <div data-slot="sidebar-tree-menu" className="flex w-full flex-col">
1064
+ {children}
1065
+ </div>
1066
+ </div>
1067
+ )
1068
+ }
1069
+
1070
+ /** 28px collapse control with accessible pressed state. */
243
1071
  const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>(
244
- function SidebarToggle({ children, className, onClick, type = "button", ...props }, ref) {
1072
+ function SidebarToggle(
1073
+ { children, className, onClick, type = "button", ...props },
1074
+ ref
1075
+ ) {
245
1076
  const { collapsed, toggle } = useSidebarContext()
246
1077
 
247
1078
  return (
@@ -252,7 +1083,8 @@ const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>(
252
1083
  ref={ref}
253
1084
  type={type}
254
1085
  className={cn(
255
- "inline-flex size-[var(--bh-sidebar-item-height)] items-center justify-center rounded-[var(--bh-radius-lg-8)] text-[var(--bh-content-subtle)] outline-none transition-[background-color,box-shadow,color] hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:shadow-[var(--shadow-button-focus)]",
1086
+ "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]",
1087
+ "hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:border-[var(--bh-border-focus)] focus-visible:shadow-[var(--shadow-button-focus)]",
256
1088
  className
257
1089
  )}
258
1090
  onClick={(event) => {
@@ -261,13 +1093,18 @@ const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>(
261
1093
  }}
262
1094
  {...props}
263
1095
  >
264
- {children ?? (
265
- <PanelLeftIcon
266
- aria-hidden="true"
267
- className="size-[var(--bh-space-5xl-24)] rtl:-scale-x-100"
268
- strokeWidth="var(--bh-icon-stroke-200)"
269
- />
270
- )}
1096
+ <span
1097
+ aria-hidden="true"
1098
+ data-slot="sidebar-toggle-icon"
1099
+ className="inline-flex size-[var(--bh-sidebar-icon-slot-size)] items-center justify-center [&_svg]:size-[var(--bh-sidebar-icon-size)]"
1100
+ >
1101
+ {children ?? (
1102
+ <PanelLeftIcon
1103
+ className="rtl:-scale-x-100"
1104
+ strokeWidth="var(--bh-icon-stroke-200)"
1105
+ />
1106
+ )}
1107
+ </span>
271
1108
  </button>
272
1109
  )
273
1110
  }
@@ -275,6 +1112,7 @@ const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>(
275
1112
 
276
1113
  const SidebarTrigger = SidebarToggle
277
1114
 
1115
+ /** Edge-hugging pointer target for toggling the sidebar from its border. */
278
1116
  const SidebarRail = React.forwardRef<HTMLButtonElement, SidebarRailProps>(
279
1117
  function SidebarRail({ className, onClick, type = "button", ...props }, ref) {
280
1118
  const { toggle } = useSidebarContext()
@@ -299,10 +1137,13 @@ const SidebarRail = React.forwardRef<HTMLButtonElement, SidebarRailProps>(
299
1137
  }
300
1138
  )
301
1139
 
302
- const SidebarInset = React.forwardRef<HTMLElement, SidebarInsetProps>(
303
- function SidebarInset({ className, ...props }, ref) {
1140
+ /** Main-content wrapper paired with a sidebar shell. */
1141
+ const SidebarInset = React.forwardRef<HTMLDivElement, SidebarInsetProps>(
1142
+ function SidebarInset({ asChild = false, className, ...props }, ref) {
1143
+ const Comp = asChild ? Slot : "div"
1144
+
304
1145
  return (
305
- <main
1146
+ <Comp
306
1147
  data-slot="sidebar-inset"
307
1148
  ref={ref}
308
1149
  className={cn("min-w-0 flex-1", className)}
@@ -312,6 +1153,20 @@ const SidebarInset = React.forwardRef<HTMLElement, SidebarInsetProps>(
312
1153
  }
313
1154
  )
314
1155
 
1156
+ function getSlottableLabel(children: React.ReactNode, asChild: boolean) {
1157
+ if (!asChild || !React.isValidElement<{ children?: React.ReactNode }>(children)) {
1158
+ return children
1159
+ }
1160
+ return children.props.children
1161
+ }
1162
+
1163
+ function cloneSlottableChild(children: React.ReactNode, content: React.ReactNode) {
1164
+ if (!React.isValidElement<{ children?: React.ReactNode }>(children)) {
1165
+ throw new Error("Sidebar asChild expects one valid React element")
1166
+ }
1167
+ return React.cloneElement(children, undefined, content)
1168
+ }
1169
+
315
1170
  function useSidebarContext() {
316
1171
  const context = React.useContext(SidebarContext)
317
1172
  if (!context) {
@@ -325,28 +1180,51 @@ export {
325
1180
  Sidebar,
326
1181
  SidebarBody,
327
1182
  SidebarFooter,
1183
+ SidebarFooterUser,
328
1184
  SidebarGroup,
1185
+ SidebarGroupAction,
329
1186
  SidebarGroupLabel,
330
1187
  SidebarHeader,
1188
+ SidebarHeaderAction,
1189
+ SidebarHeaderBrand,
1190
+ SidebarHeaderWorkspaceSwitch,
331
1191
  SidebarInset,
332
1192
  SidebarMenu,
1193
+ SidebarMenuBadge,
333
1194
  SidebarMenuButton,
334
1195
  SidebarMenuItem,
1196
+ SidebarMenuSub,
1197
+ SidebarMenuSubButton,
1198
+ SidebarMenuTreeItem,
335
1199
  SidebarProvider,
336
1200
  SidebarRail,
337
- SidebarTrigger,
338
1201
  SidebarToggle,
1202
+ SidebarTreeGroup,
1203
+ SidebarTrigger,
339
1204
  }
340
1205
  export type {
1206
+ SidebarFooterUserProps,
1207
+ SidebarGroupActionProps,
341
1208
  SidebarGroupLabelProps,
1209
+ SidebarHeaderActionProps,
1210
+ SidebarHeaderBrandProps,
1211
+ SidebarHeaderWorkspaceSwitchProps,
342
1212
  SidebarInsetProps,
1213
+ SidebarInteractionState,
1214
+ SidebarMenuBadgeProps,
343
1215
  SidebarMenuButtonProps,
1216
+ SidebarMenuButtonVariant,
344
1217
  SidebarMenuItemProps,
345
1218
  SidebarMenuProps,
1219
+ SidebarMenuSubButtonProps,
1220
+ SidebarMenuSubProps,
1221
+ SidebarMenuTreeItemProps,
346
1222
  SidebarProps,
347
1223
  SidebarProviderProps,
348
1224
  SidebarRailProps,
349
- SidebarTriggerProps,
350
1225
  SidebarSectionProps,
351
1226
  SidebarToggleProps,
1227
+ SidebarTreeGroupProps,
1228
+ SidebarTriggerProps,
1229
+ SidebarWorkspaceVariant,
352
1230
  }