banhaten 0.2.0 → 0.3.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 (101) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +1 -1
  3. package/docs/design-system/component-implementation-ownership-audit.md +244 -0
  4. package/docs/design-system/component-quality-audit-2026-08-20.md +223 -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 +2 -2
  11. package/docs/design-system/components/avatar.md +1 -1
  12. package/docs/design-system/components/badge.md +1 -1
  13. package/docs/design-system/components/banner.md +5 -1
  14. package/docs/design-system/components/breadcrumbs.md +3 -0
  15. package/docs/design-system/components/button-group.md +1 -0
  16. package/docs/design-system/components/button.md +1 -1
  17. package/docs/design-system/components/card.md +11 -3
  18. package/docs/design-system/components/catalog-components.md +3 -0
  19. package/docs/design-system/components/date-picker.md +1 -1
  20. package/docs/design-system/components/file-upload.md +4 -0
  21. package/docs/design-system/components/modal.md +1 -1
  22. package/docs/design-system/components/navigation-menu.md +1 -1
  23. package/docs/design-system/components/onboarding-step-list-item.md +1 -0
  24. package/docs/design-system/components/page-header.md +3 -1
  25. package/docs/design-system/components/pagination.md +1 -0
  26. package/docs/design-system/components/resizable.md +1 -1
  27. package/docs/design-system/components/scroll-area.md +7 -3
  28. package/docs/design-system/components/sidebar.md +3 -1
  29. package/docs/design-system/components/slideout.md +4 -1
  30. package/docs/design-system/components/steps.md +7 -2
  31. package/docs/design-system/components/table.md +25 -7
  32. package/docs/design-system/components/tag.md +1 -1
  33. package/docs/design-system/components/timeline.md +4 -1
  34. package/docs/design-system/components/toggle.md +1 -1
  35. package/docs/design-system/components/toolbar.md +1 -1
  36. package/docs/design-system/components/tooltip.md +1 -1
  37. package/docs/design-system/density-and-elevation.md +18 -1
  38. package/docs/design-system/gap-promotion-report.md +1 -67
  39. package/docs/design-system/launch-comparison-shadcn.md +10 -10
  40. package/package.json +3 -2
  41. package/registry/components/alert-dialog.tsx +1 -1
  42. package/registry/components/alert.tsx +15 -11
  43. package/registry/components/avatar.tsx +7 -3
  44. package/registry/components/badge.tsx +17 -17
  45. package/registry/components/button-group.tsx +3 -1
  46. package/registry/components/button.tsx +2 -2
  47. package/registry/components/card.tsx +19 -3
  48. package/registry/components/carousel.tsx +14 -0
  49. package/registry/components/command.tsx +37 -3
  50. package/registry/components/date-picker.tsx +67 -31
  51. package/registry/components/expanded/ActivityFeed.tsx +3 -7
  52. package/registry/components/expanded/ActivityHeatmap.tsx +64 -0
  53. package/registry/components/expanded/Banner.tsx +0 -1
  54. package/registry/components/expanded/Breadcrumbs.tsx +3 -0
  55. package/registry/components/expanded/CatalogComponentsShowcase.tsx +3 -4
  56. package/registry/components/expanded/CatalogDivider.tsx +3 -2
  57. package/registry/components/expanded/FileUpload.tsx +7 -5
  58. package/registry/components/expanded/OnboardingStepListItem.tsx +0 -2
  59. package/registry/components/expanded/PageHeader.tsx +35 -44
  60. package/registry/components/expanded/Slideout.tsx +18 -13
  61. package/registry/components/expanded/Steps.tsx +147 -9
  62. package/registry/components/expanded/Table.tsx +50 -18
  63. package/registry/components/expanded/Timeline.tsx +11 -24
  64. package/registry/components/expanded/activityFeed.css +8 -8
  65. package/registry/components/expanded/activityHeatmap.css +66 -0
  66. package/registry/components/expanded/banner.css +21 -11
  67. package/registry/components/expanded/breadcrumbs.css +1 -1
  68. package/registry/components/expanded/fileUpload.css +1 -1
  69. package/registry/components/expanded/onboardingStepListItem.css +2 -2
  70. package/registry/components/expanded/pageHeader.css +14 -11
  71. package/registry/components/expanded/slideout.css +3 -7
  72. package/registry/components/expanded/steps.css +75 -12
  73. package/registry/components/expanded/table.css +58 -67
  74. package/registry/components/expanded/timeline.css +17 -16
  75. package/registry/components/field.tsx +61 -8
  76. package/registry/components/menu.tsx +56 -6
  77. package/registry/components/modal.tsx +1 -1
  78. package/registry/components/navigation-menu.tsx +1 -1
  79. package/registry/components/pagination.css +36 -0
  80. package/registry/components/pagination.tsx +8 -6
  81. package/registry/components/progress-slider.tsx +1 -1
  82. package/registry/components/resizable.tsx +36 -2
  83. package/registry/components/scroll-area.tsx +30 -6
  84. package/registry/components/select.tsx +34 -11
  85. package/registry/components/sidebar.tsx +11 -5
  86. package/registry/components/social-button.tsx +12 -5
  87. package/registry/components/table-elements.tsx +128 -0
  88. package/registry/components/tabs.tsx +5 -10
  89. package/registry/components/tag.tsx +1 -1
  90. package/registry/components/toggle.tsx +2 -2
  91. package/registry/components/toolbar.tsx +5 -5
  92. package/registry/components/tooltip.tsx +1 -1
  93. package/registry/examples/avatar-demo.tsx +7 -4
  94. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -0
  95. package/registry/examples/expanded/breadcrumbs-demo.tsx +3 -3
  96. package/registry/examples/pagination-demo.tsx +5 -5
  97. package/registry/examples/sidebar-demo.tsx +4 -2
  98. package/registry/index.json +392 -33
  99. package/registry/styles/globals.css +68 -42
  100. package/registry.json +40 -3
  101. package/tokens/banhaten.tokens.json +165 -55
@@ -10,10 +10,14 @@ type FieldOrientation = "vertical" | "horizontal" | "responsive"
10
10
 
11
11
  type FieldContextValue = {
12
12
  controlId: string
13
- descriptionId: string
13
+ defaultDescriptionId: string
14
+ defaultErrorId: string
15
+ descriptionIds: readonly string[]
14
16
  disabled?: boolean
15
- errorId: string
17
+ errorIds: readonly string[]
16
18
  invalid?: boolean
19
+ registerDescription: (id: string) => () => void
20
+ registerError: (id: string) => () => void
17
21
  }
18
22
 
19
23
  type FieldProps = React.ComponentProps<"div"> & {
@@ -48,15 +52,38 @@ function Field({
48
52
  }: FieldProps) {
49
53
  const generatedId = React.useId()
50
54
  const resolvedControlId = controlId ?? `${generatedId}-control`
55
+ const [descriptionIds, setDescriptionIds] = React.useState<string[]>([])
56
+ const [errorIds, setErrorIds] = React.useState<string[]>([])
57
+ const registerDescription = React.useCallback(
58
+ (id: string) => registerFieldReference(setDescriptionIds, id),
59
+ []
60
+ )
61
+ const registerError = React.useCallback(
62
+ (id: string) => registerFieldReference(setErrorIds, id),
63
+ []
64
+ )
51
65
  const context = React.useMemo<FieldContextValue>(
52
66
  () => ({
53
67
  controlId: resolvedControlId,
54
- descriptionId: `${generatedId}-description`,
68
+ defaultDescriptionId: `${generatedId}-description`,
69
+ defaultErrorId: `${generatedId}-error`,
70
+ descriptionIds,
55
71
  disabled,
56
- errorId: `${generatedId}-error`,
72
+ errorIds,
57
73
  invalid,
74
+ registerDescription,
75
+ registerError,
58
76
  }),
59
- [disabled, generatedId, invalid, resolvedControlId]
77
+ [
78
+ descriptionIds,
79
+ disabled,
80
+ errorIds,
81
+ generatedId,
82
+ invalid,
83
+ registerDescription,
84
+ registerError,
85
+ resolvedControlId,
86
+ ]
60
87
  )
61
88
 
62
89
  return (
@@ -86,7 +113,10 @@ function FieldControl({ className, ...props }: FieldControlProps) {
86
113
  const context = useFieldContext("FieldControl")
87
114
  const describedBy =
88
115
  props["aria-describedby"] ??
89
- [context.descriptionId, context.invalid ? context.errorId : undefined]
116
+ [
117
+ ...context.descriptionIds,
118
+ ...(context.invalid ? context.errorIds : []),
119
+ ]
90
120
  .filter(Boolean)
91
121
  .join(" ")
92
122
 
@@ -117,12 +147,18 @@ function FieldLabel({ htmlFor, ...props }: FieldLabelProps) {
117
147
 
118
148
  function FieldDescription({ className, id, ...props }: FieldDescriptionProps) {
119
149
  const context = useFieldContext("FieldDescription")
150
+ const resolvedId = id ?? context.defaultDescriptionId
151
+
152
+ React.useEffect(
153
+ () => context.registerDescription(resolvedId),
154
+ [context.registerDescription, resolvedId]
155
+ )
120
156
 
121
157
  return (
122
158
  <p
123
159
  data-slot="field-description"
124
160
  dir="auto"
125
- id={id ?? context.descriptionId}
161
+ id={resolvedId}
126
162
  className={cn(
127
163
  "m-0 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)]",
128
164
  className
@@ -134,12 +170,18 @@ function FieldDescription({ className, id, ...props }: FieldDescriptionProps) {
134
170
 
135
171
  function FieldError({ className, id, role = "alert", ...props }: FieldErrorProps) {
136
172
  const context = useFieldContext("FieldError")
173
+ const resolvedId = id ?? context.defaultErrorId
174
+
175
+ React.useEffect(
176
+ () => context.registerError(resolvedId),
177
+ [context.registerError, resolvedId]
178
+ )
137
179
 
138
180
  return (
139
181
  <p
140
182
  data-slot="field-error"
141
183
  dir="auto"
142
- id={id ?? context.errorId}
184
+ id={resolvedId}
143
185
  role={role}
144
186
  className={cn(
145
187
  "m-0 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-danger-default)]",
@@ -150,6 +192,17 @@ function FieldError({ className, id, role = "alert", ...props }: FieldErrorProps
150
192
  )
151
193
  }
152
194
 
195
+ function registerFieldReference(
196
+ setIds: React.Dispatch<React.SetStateAction<string[]>>,
197
+ id: string
198
+ ) {
199
+ setIds((current) => (current.includes(id) ? current : [...current, id]))
200
+
201
+ return () => {
202
+ setIds((current) => current.filter((candidate) => candidate !== id))
203
+ }
204
+ }
205
+
153
206
  function FieldGroup({ className, ...props }: FieldGroupProps) {
154
207
  return (
155
208
  <div
@@ -283,11 +283,12 @@ type MenuSubTriggerProps = React.ComponentProps<"div"> &
283
283
  }
284
284
 
285
285
  const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuItem({
286
+ "aria-checked": ariaChecked,
286
287
  className,
287
288
  kind = "default",
288
289
  disabled = false,
289
290
  asChild = false,
290
- role = "menuitem",
291
+ role,
291
292
  tabIndex,
292
293
  children,
293
294
  onSelect,
@@ -298,15 +299,23 @@ const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuIt
298
299
  const isStaticMenu = React.useContext(MenuStaticContext)
299
300
  const selectedKind: MenuItemKind = kind ?? "default"
300
301
  const isDisabled = disabled
302
+ const switchState = findMenuItemSwitchState(children)
303
+ const resolvedRole = role ??
304
+ (switchState === undefined ? "menuitem" : "menuitemcheckbox")
305
+ const resolvedChecked =
306
+ resolvedRole === "menuitemcheckbox"
307
+ ? (ariaChecked ?? switchState ?? false)
308
+ : undefined
301
309
 
302
310
  const item = (
303
311
  <Comp
312
+ aria-checked={resolvedChecked}
304
313
  aria-disabled={isDisabled ? true : undefined}
305
314
  data-disabled={isDisabled ? "true" : "false"}
306
315
  data-kind={selectedKind}
307
316
  data-slot="menu-item"
308
317
  ref={ref}
309
- role={role}
318
+ role={resolvedRole}
310
319
  tabIndex={isDisabled ? -1 : (tabIndex ?? 0)}
311
320
  className={cn(menuItemVariants({ kind: selectedKind, className }))}
312
321
  {...props}
@@ -319,6 +328,20 @@ const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuIt
319
328
 
320
329
  if (isStaticMenu) return item
321
330
 
331
+ if (switchState !== undefined) {
332
+ return (
333
+ <DropdownMenuPrimitive.CheckboxItem
334
+ asChild
335
+ checked={switchState}
336
+ disabled={isDisabled}
337
+ onSelect={onSelect}
338
+ textValue={textValue ?? getMenuItemTextValue(children)}
339
+ >
340
+ {item}
341
+ </DropdownMenuPrimitive.CheckboxItem>
342
+ )
343
+ }
344
+
322
345
  return (
323
346
  <DropdownMenuPrimitive.Item
324
347
  asChild
@@ -333,6 +356,27 @@ const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuIt
333
356
 
334
357
  MenuItem.displayName = "MenuItem"
335
358
 
359
+ function findMenuItemSwitchState(children: React.ReactNode): boolean | undefined {
360
+ let state: boolean | undefined
361
+
362
+ React.Children.forEach(children, (child) => {
363
+ if (state !== undefined || !React.isValidElement(child)) return
364
+
365
+ const element = child as React.ReactElement<{
366
+ active?: boolean
367
+ children?: React.ReactNode
368
+ }>
369
+ if (element.type === MenuItemSwitch) {
370
+ state = Boolean(element.props.active)
371
+ return
372
+ }
373
+
374
+ state = findMenuItemSwitchState(element.props.children)
375
+ })
376
+
377
+ return state
378
+ }
379
+
336
380
  const MenuSubTrigger = React.forwardRef<HTMLDivElement, MenuSubTriggerProps>(function MenuSubTrigger({
337
381
  className,
338
382
  kind = "default",
@@ -576,7 +620,10 @@ function MenuItemBadge({
576
620
  )
577
621
  }
578
622
 
579
- type MenuItemSwitchProps = Omit<React.ComponentProps<"span">, "role"> & {
623
+ type MenuItemSwitchProps = Omit<
624
+ React.ComponentProps<"span">,
625
+ "aria-checked" | "aria-label" | "aria-labelledby" | "role"
626
+ > & {
580
627
  active?: boolean
581
628
  disabled?: boolean
582
629
  }
@@ -589,12 +636,10 @@ function MenuItemSwitch({
589
636
  }: MenuItemSwitchProps) {
590
637
  return (
591
638
  <span
592
- aria-checked={active}
593
- aria-disabled={disabled || undefined}
639
+ aria-hidden="true"
594
640
  data-active={active ? "true" : "false"}
595
641
  data-disabled={disabled ? "true" : "false"}
596
642
  data-slot="menu-item-switch"
597
- role="switch"
598
643
  className={cn(
599
644
  "flex h-[var(--bh-menu-switch-height)] w-[var(--bh-menu-switch-width)] shrink-0 items-center rounded-[var(--bh-radius-full)] p-[var(--bh-menu-switch-padding)] group-data-[disabled=true]/menu-item:bg-[var(--bh-interactive-switch-disabled)]",
600
645
  active
@@ -677,6 +722,8 @@ function MenuProgress({
677
722
  ...props
678
723
  }: MenuProgressProps) {
679
724
  const clampedValue = Math.min(100, Math.max(0, value))
725
+ const labelId = React.useId()
726
+ const hasLabel = hasRenderableContent(label)
680
727
 
681
728
  return (
682
729
  <div
@@ -688,6 +735,8 @@ function MenuProgress({
688
735
  {...props}
689
736
  >
690
737
  <div
738
+ aria-label={hasLabel ? undefined : "Progress"}
739
+ aria-labelledby={hasLabel ? labelId : undefined}
691
740
  aria-valuemax={100}
692
741
  aria-valuemin={0}
693
742
  aria-valuenow={clampedValue}
@@ -717,6 +766,7 @@ function MenuProgress({
717
766
  >
718
767
  {hasRenderableContent(label) ? (
719
768
  <span
769
+ id={labelId}
720
770
  data-slot="menu-progress-label-text"
721
771
  dir="auto"
722
772
  className="truncate text-[length:var(--bh-text-body-xs-medium-font-size)] font-[var(--bh-text-body-xs-medium-font-weight)] leading-[var(--bh-text-body-xs-medium-line-height)] tracking-[var(--bh-text-body-xs-medium-letter-spacing)] text-[var(--bh-content-default)]"
@@ -39,7 +39,7 @@ type ConfirmModalMessages = {
39
39
 
40
40
  const modalSurfaceBaseClassName = [
41
41
  "flex max-h-[calc(100vh-(var(--bh-space-8xl-48)*2))] w-[var(--bh-modal-width)] max-w-[calc(100vw-(var(--bh-space-4xl-20)*2))] flex-col overflow-hidden rounded-[var(--bh-radius-2xl-12)] bg-[var(--bh-bg-raised)] text-[var(--bh-content-default)] shadow-[var(--shadow-modal)] outline-none",
42
- "[--bh-modal-border:var(--bh-border-subtle)] [--shadow-modal:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-modal-border),var(--shadow-component-default)]",
42
+ "[--bh-modal-border:var(--bh-border-subtle)] [--shadow-modal:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-modal-border),var(--shadow-lg)]",
43
43
  ]
44
44
 
45
45
  const modalSizeVariants = {
@@ -145,7 +145,7 @@ const NavigationMenuIndicator = React.forwardRef<
145
145
  className={cn("top-full z-[var(--bh-z-popover)] flex h-[var(--bh-space-lg-10)] items-end justify-center overflow-hidden", className)}
146
146
  {...props}
147
147
  >
148
- <div className="relative top-[var(--bh-space-xs-4)] size-[var(--bh-space-lg-10)] rotate-45 rounded-tl-[var(--bh-radius-sm-4)] bg-[var(--bh-bg-raised)] shadow-[var(--shadow-popover)]" />
148
+ <div className="relative top-[var(--bh-space-xs-4)] size-[var(--bh-space-lg-10)] rotate-45 rounded-tl-[var(--bh-radius-sm-4)] bg-[var(--bh-bg-raised)] shadow-[inset_0_0_0_var(--bh-border-width-default)_var(--bh-border-subtle)]" />
149
149
  </NavigationMenuPrimitive.Indicator>
150
150
  )
151
151
  })
@@ -0,0 +1,36 @@
1
+ .ds-pagination {
2
+ container-type: inline-size;
3
+ }
4
+
5
+ .ds-pagination__controls {
6
+ overflow-x: auto;
7
+ overscroll-behavior-inline: contain;
8
+ scrollbar-width: thin;
9
+ }
10
+
11
+ /* Approved exception: container-query breakpoints stay literal because CSS vars are not reliable in query conditions. */
12
+ @container (max-width: 28rem) {
13
+ .ds-pagination [data-slot="pagination-caption"] {
14
+ width: 100%;
15
+ flex: 1 0 100%;
16
+ }
17
+
18
+ .ds-pagination[data-type="numeric"] .ds-pagination__controls {
19
+ width: 100%;
20
+ justify-content: center;
21
+ }
22
+
23
+ .ds-pagination[data-type="numeric"] .ds-pagination__pages > :nth-child(n + 4):not(:last-child) {
24
+ display: none;
25
+ }
26
+
27
+ .ds-pagination[data-type="simple"] .ds-pagination__controls {
28
+ margin-inline-start: auto;
29
+ }
30
+
31
+ .ds-pagination[data-type="summary"] [data-slot="pagination-summary"] {
32
+ order: -1;
33
+ width: 100%;
34
+ text-align: center;
35
+ }
36
+ }
@@ -5,6 +5,8 @@ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
5
5
  import { cn } from "@/lib/utils"
6
6
  import { useResolvedDirection } from "@/hooks/use-direction"
7
7
 
8
+ import "./pagination.css"
9
+
8
10
  type PaginationType = "numeric" | "simple" | "summary"
9
11
  type PaginationVariant = "soft" | "ghost"
10
12
  type PaginationPage =
@@ -65,7 +67,7 @@ const arabicPaginationMessages: PaginationMessages = {
65
67
 
66
68
  const paginationVariants = cva(
67
69
  [
68
- "flex max-w-full items-center",
70
+ "ds-pagination flex w-full max-w-full min-w-0 flex-wrap items-center",
69
71
  "text-[length:var(--bh-pagination-font-size)]",
70
72
  "leading-[var(--bh-pagination-line-height)]",
71
73
  "tracking-[var(--bh-pagination-letter-spacing)]",
@@ -74,11 +76,11 @@ const paginationVariants = cva(
74
76
  variants: {
75
77
  type: {
76
78
  numeric:
77
- "w-[var(--bh-pagination-numeric-width)] justify-between gap-[var(--bh-pagination-root-gap)]",
79
+ "max-w-[var(--bh-pagination-numeric-width)] justify-between gap-[var(--bh-pagination-root-gap)]",
78
80
  simple:
79
- "w-[var(--bh-pagination-simple-width)] justify-between gap-[var(--bh-pagination-root-gap)]",
81
+ "max-w-[var(--bh-pagination-simple-width)] justify-between gap-[var(--bh-pagination-root-gap)]",
80
82
  summary:
81
- "w-[var(--bh-pagination-simple-width)] justify-between gap-[var(--bh-pagination-root-gap)]",
83
+ "max-w-[var(--bh-pagination-simple-width)] justify-between gap-[var(--bh-pagination-root-gap)]",
82
84
  },
83
85
  showCaption: {
84
86
  true: "",
@@ -321,7 +323,7 @@ const Pagination = React.forwardRef<HTMLElement, PaginationProps>(function Pagin
321
323
  />
322
324
  <div
323
325
  data-slot="pagination-pages"
324
- className="flex shrink-0 items-center gap-[var(--bh-pagination-page-gap)]"
326
+ className="ds-pagination__pages flex min-w-0 shrink-0 items-center gap-[var(--bh-pagination-page-gap)]"
325
327
  >
326
328
  {pages.map((item, index) => (
327
329
  <PaginationPageItem
@@ -381,7 +383,7 @@ function PaginationControls({
381
383
  <div
382
384
  data-slot="pagination-controls"
383
385
  className={cn(
384
- "flex shrink-0 items-center",
386
+ "ds-pagination__controls flex max-w-full min-w-0 shrink-0 items-center",
385
387
  type === "numeric"
386
388
  ? "gap-[var(--bh-pagination-control-gap)]"
387
389
  : "gap-[var(--bh-pagination-action-gap)]"
@@ -12,7 +12,7 @@ type ProgressSliderSize = "default" | "lg"
12
12
  const progressSliderVariants = cva(
13
13
  [
14
14
  "group/progress-slider relative flex touch-none select-none items-center",
15
- "w-[var(--bh-progress-slider-width-current)] max-w-full",
15
+ "w-full max-w-[var(--bh-progress-slider-width-current)] min-w-0",
16
16
  "data-[disabled]:pointer-events-none data-[disabled]:opacity-[var(--bh-opacity-60)]",
17
17
  "[--bh-progress-slider-width-current:var(--bh-progress-slider-width-md)]",
18
18
  "[--bh-progress-slider-height-current:var(--bh-progress-slider-height-md)]",
@@ -17,7 +17,9 @@ import { cn } from "@/lib/utils"
17
17
  type ResizablePanelGroupProps = Omit<GroupProps, "orientation"> & {
18
18
  direction?: Orientation
19
19
  }
20
- type ResizablePanelProps = PanelProps
20
+ type ResizablePanelProps = PanelProps & {
21
+ contentTabIndex?: number
22
+ }
21
23
  type ResizableHandleProps = SeparatorProps & {
22
24
  withHandle?: boolean
23
25
  }
@@ -41,7 +43,39 @@ function ResizablePanelGroup({
41
43
  )
42
44
  }
43
45
 
44
- const ResizablePanel = Panel
46
+ function ResizablePanel({
47
+ className,
48
+ contentTabIndex = 0,
49
+ elementRef,
50
+ ...props
51
+ }: ResizablePanelProps) {
52
+ const setElementRef = React.useCallback(
53
+ (node: HTMLDivElement | null) => {
54
+ const content = node?.firstElementChild
55
+ if (content instanceof HTMLElement) {
56
+ content.tabIndex = contentTabIndex
57
+ content.setAttribute("data-slot", "resizable-panel-content")
58
+ }
59
+
60
+ if (typeof elementRef === "function") elementRef(node)
61
+ else if (elementRef) {
62
+ ;(elementRef as React.MutableRefObject<HTMLDivElement | null>).current = node
63
+ }
64
+ },
65
+ [contentTabIndex, elementRef]
66
+ )
67
+
68
+ return (
69
+ <Panel
70
+ className={cn(
71
+ "outline-none focus-visible:shadow-[var(--shadow-component-focus-ring)]",
72
+ className
73
+ )}
74
+ elementRef={setElementRef}
75
+ {...props}
76
+ />
77
+ )
78
+ }
45
79
 
46
80
  function ResizableHandle({
47
81
  className,
@@ -5,9 +5,15 @@ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5
5
 
6
6
  import { cn } from "@/lib/utils"
7
7
 
8
+ type ScrollAreaOrientation = "vertical" | "horizontal" | "both"
8
9
  type ScrollAreaProps = React.ComponentPropsWithoutRef<
9
10
  typeof ScrollAreaPrimitive.Root
10
- >
11
+ > & {
12
+ orientation?: ScrollAreaOrientation
13
+ viewportProps?: React.ComponentPropsWithoutRef<
14
+ typeof ScrollAreaPrimitive.Viewport
15
+ >
16
+ }
11
17
  type ScrollBarProps = React.ComponentPropsWithoutRef<
12
18
  typeof ScrollAreaPrimitive.ScrollAreaScrollbar
13
19
  >
@@ -15,9 +21,24 @@ type ScrollBarProps = React.ComponentPropsWithoutRef<
15
21
  const ScrollArea = React.forwardRef<
16
22
  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
17
23
  ScrollAreaProps
18
- >(function ScrollArea({ children, className, ...props }, ref) {
24
+ >(function ScrollArea(
25
+ {
26
+ children,
27
+ className,
28
+ orientation = "vertical",
29
+ viewportProps,
30
+ ...props
31
+ },
32
+ ref
33
+ ) {
34
+ const hasVerticalScrollbar =
35
+ orientation === "vertical" || orientation === "both"
36
+ const hasHorizontalScrollbar =
37
+ orientation === "horizontal" || orientation === "both"
38
+
19
39
  return (
20
40
  <ScrollAreaPrimitive.Root
41
+ data-orientation={orientation}
21
42
  data-slot="scroll-area"
22
43
  ref={ref}
23
44
  className={cn("relative overflow-hidden", className)}
@@ -25,12 +46,15 @@ const ScrollArea = React.forwardRef<
25
46
  >
26
47
  <ScrollAreaPrimitive.Viewport
27
48
  data-slot="scroll-area-viewport"
28
- className="size-full rounded-[inherit]"
49
+ {...viewportProps}
50
+ tabIndex={viewportProps?.tabIndex ?? 0}
51
+ className={cn("size-full rounded-[inherit] outline-none focus-visible:shadow-[var(--shadow-component-focus-ring)]", viewportProps?.className)}
29
52
  >
30
53
  {children}
31
54
  </ScrollAreaPrimitive.Viewport>
32
- <ScrollBar />
33
- <ScrollAreaPrimitive.Corner />
55
+ {hasVerticalScrollbar ? <ScrollBar orientation="vertical" /> : null}
56
+ {hasHorizontalScrollbar ? <ScrollBar orientation="horizontal" /> : null}
57
+ {orientation === "both" ? <ScrollAreaPrimitive.Corner /> : null}
34
58
  </ScrollAreaPrimitive.Root>
35
59
  )
36
60
  })
@@ -64,4 +88,4 @@ const ScrollBar = React.forwardRef<
64
88
  })
65
89
 
66
90
  export { ScrollArea, ScrollBar }
67
- export type { ScrollAreaProps, ScrollBarProps }
91
+ export type { ScrollAreaOrientation, ScrollAreaProps, ScrollBarProps }
@@ -224,6 +224,9 @@ const Select = React.forwardRef<HTMLDivElement, SelectProps>(function Select({
224
224
  const shouldRenderMenu = React.Children.count(children) > 0
225
225
  const selectId = triggerId || generatedId
226
226
  const helperId = `${selectId}-helper`
227
+ const triggerAccessibleLabel = shouldRenderLabel
228
+ ? undefined
229
+ : getAccessibleText(selectedValue) ?? getAccessibleText(selectedPlaceholder)
227
230
  const rootOpenProps =
228
231
  open === undefined ? { onOpenChange } : { open, onOpenChange }
229
232
 
@@ -261,8 +264,8 @@ const Select = React.forwardRef<HTMLDivElement, SelectProps>(function Select({
261
264
  <div data-slot="select-control" className="relative w-full">
262
265
  <SelectPrimitive.Trigger asChild>
263
266
  <SelectTrigger
267
+ aria-label={triggerAccessibleLabel}
264
268
  aria-describedby={shouldRenderHelperText ? helperId : undefined}
265
- aria-expanded={open || undefined}
266
269
  aria-haspopup="listbox"
267
270
  aria-invalid={isError || undefined}
268
271
  expandIcon={expandIcon}
@@ -436,6 +439,12 @@ function hasRenderableContent(content: React.ReactNode) {
436
439
  )
437
440
  }
438
441
 
442
+ function getAccessibleText(content: React.ReactNode) {
443
+ return typeof content === "string" || typeof content === "number"
444
+ ? String(content)
445
+ : undefined
446
+ }
447
+
439
448
  type SelectHelperTextProps = React.ComponentProps<"p"> & {
440
449
  hasIcon?: boolean
441
450
  type?: "default" | "error"
@@ -592,7 +601,14 @@ SelectTrigger.displayName = "SelectTrigger"
592
601
  type SelectMenuProps = React.ComponentProps<"div">
593
602
 
594
603
  const SelectMenu = React.forwardRef<HTMLDivElement, SelectMenuProps>(function SelectMenu(
595
- { children, className, onKeyDown, role, ...props },
604
+ {
605
+ "aria-label": ariaLabel = "Options",
606
+ children,
607
+ className,
608
+ onKeyDown,
609
+ role,
610
+ ...props
611
+ },
596
612
  ref
597
613
  ) {
598
614
  const handleKeyDown = React.useCallback(
@@ -638,6 +654,7 @@ const SelectMenu = React.forwardRef<HTMLDivElement, SelectMenuProps>(function Se
638
654
 
639
655
  return (
640
656
  <div
657
+ aria-label={ariaLabel}
641
658
  data-slot="select-menu"
642
659
  onKeyDown={handleKeyDown}
643
660
  ref={ref}
@@ -645,7 +662,11 @@ const SelectMenu = React.forwardRef<HTMLDivElement, SelectMenuProps>(function Se
645
662
  className={cn(selectMenu(), className)}
646
663
  {...props}
647
664
  >
648
- <div data-slot="select-menu-list" className={selectMenuList()}>
665
+ <div
666
+ data-slot="select-menu-list"
667
+ role="presentation"
668
+ className={selectMenuList()}
669
+ >
649
670
  {children}
650
671
  </div>
651
672
  </div>
@@ -689,6 +710,8 @@ type SelectMenuItemProps = SelectMenuItemBaseProps &
689
710
 
690
711
  const SelectMenuItem = React.forwardRef<HTMLButtonElement, SelectMenuItemProps>(function SelectMenuItem({
691
712
  addonText,
713
+ "aria-checked": ariaChecked,
714
+ "aria-selected": ariaSelected,
692
715
  children,
693
716
  className,
694
717
  disabled,
@@ -706,8 +729,12 @@ const SelectMenuItem = React.forwardRef<HTMLButtonElement, SelectMenuItemProps>(
706
729
  const generatedValue = React.useId()
707
730
  const isDisabled = disabled
708
731
  const selectedWithEndCheck = selected && selectionType === "default"
709
- const resolvedRole =
710
- role || (selectionType === "checkbox" ? "menuitemcheckbox" : "option")
732
+ const resolvedRole = role || "option"
733
+ const usesCheckedState =
734
+ resolvedRole === "checkbox" ||
735
+ resolvedRole === "menuitemcheckbox" ||
736
+ resolvedRole === "radio" ||
737
+ resolvedRole === "menuitemradio"
711
738
  const selectedMedia =
712
739
  media === false ? null : media ?? getDefaultSelectItemMedia(itemType)
713
740
  const content = label ?? children
@@ -723,13 +750,9 @@ const SelectMenuItem = React.forwardRef<HTMLButtonElement, SelectMenuItemProps>(
723
750
 
724
751
  const item = (
725
752
  <button
726
- aria-checked={
727
- selectionType === "checkbox" ? Boolean(selected) : undefined
728
- }
753
+ aria-checked={usesCheckedState ? (ariaChecked ?? Boolean(selected)) : undefined}
729
754
  aria-disabled={isDisabled || undefined}
730
- aria-selected={
731
- selectionType === "default" ? Boolean(selected) : undefined
732
- }
755
+ aria-selected={resolvedRole === "option" ? (ariaSelected ?? Boolean(selected)) : undefined}
733
756
  data-disabled={isDisabled ? "true" : undefined}
734
757
  data-item-type={itemType}
735
758
  data-selected={selected ? "true" : "false"}
@@ -34,7 +34,9 @@ type SidebarMenuButtonProps = React.ComponentProps<"button"> & {
34
34
  type SidebarToggleProps = React.ComponentProps<"button">
35
35
  type SidebarTriggerProps = SidebarToggleProps
36
36
  type SidebarRailProps = React.ComponentProps<"button">
37
- type SidebarInsetProps = React.ComponentProps<"main">
37
+ type SidebarInsetProps = React.ComponentProps<"div"> & {
38
+ asChild?: boolean
39
+ }
38
40
 
39
41
  const SidebarContext = React.createContext<SidebarContextValue | null>(null)
40
42
 
@@ -91,7 +93,9 @@ const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(function Sidebar(
91
93
  { className, side = "start", ...props },
92
94
  ref
93
95
  ) {
94
- const { collapsed, dir } = useSidebarContext()
96
+ const context = React.useContext(SidebarContext)
97
+ const collapsed = context?.collapsed ?? false
98
+ const dir = context?.dir
95
99
 
96
100
  return (
97
101
  <aside
@@ -299,10 +303,12 @@ const SidebarRail = React.forwardRef<HTMLButtonElement, SidebarRailProps>(
299
303
  }
300
304
  )
301
305
 
302
- const SidebarInset = React.forwardRef<HTMLElement, SidebarInsetProps>(
303
- function SidebarInset({ className, ...props }, ref) {
306
+ const SidebarInset = React.forwardRef<HTMLDivElement, SidebarInsetProps>(
307
+ function SidebarInset({ asChild = false, className, ...props }, ref) {
308
+ const Comp = asChild ? Slot : "div"
309
+
304
310
  return (
305
- <main
311
+ <Comp
306
312
  data-slot="sidebar-inset"
307
313
  ref={ref}
308
314
  className={cn("min-w-0 flex-1", className)}