banhaten 0.1.2 → 0.2.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 (365) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/CONTRIBUTING.md +44 -0
  3. package/README.md +145 -328
  4. package/SECURITY.md +15 -0
  5. package/banhaten.config.example.json +1 -3
  6. package/docs/design-system/README.md +20 -0
  7. package/docs/design-system/adoption-quality-remediation.md +87 -0
  8. package/docs/design-system/agent-usage.md +13 -0
  9. package/docs/design-system/appearance-presets.md +189 -0
  10. package/docs/design-system/appearances/default.md +94 -0
  11. package/docs/design-system/appearances/rounded.md +95 -0
  12. package/docs/design-system/appearances/sharp.md +95 -0
  13. package/docs/design-system/component-docs-pattern.md +81 -0
  14. package/docs/design-system/component-showcase-consistency-report.md +217 -0
  15. package/docs/design-system/component-token-consistency-audit.md +173 -0
  16. package/docs/design-system/components/README.md +75 -0
  17. package/docs/design-system/components/accordion.md +67 -0
  18. package/docs/design-system/components/activity-feed.md +106 -0
  19. package/docs/design-system/components/alert-dialog.md +87 -0
  20. package/docs/design-system/components/alert.md +111 -0
  21. package/docs/design-system/components/aspect-ratio.md +59 -0
  22. package/docs/design-system/components/attribute.md +103 -0
  23. package/docs/design-system/components/autocomplete.md +93 -0
  24. package/docs/design-system/components/avatar.md +70 -0
  25. package/docs/design-system/components/badge.md +72 -0
  26. package/docs/design-system/components/banner.md +99 -0
  27. package/docs/design-system/components/breadcrumbs.md +188 -0
  28. package/docs/design-system/components/button-group.md +117 -0
  29. package/docs/design-system/components/button.md +95 -0
  30. package/docs/design-system/components/card.md +95 -0
  31. package/docs/design-system/components/carousel.md +61 -0
  32. package/docs/design-system/components/catalog-components.md +59 -0
  33. package/docs/design-system/components/chart.md +58 -0
  34. package/docs/design-system/components/checkbox.md +68 -0
  35. package/docs/design-system/components/collapsible.md +63 -0
  36. package/docs/design-system/components/command.md +75 -0
  37. package/docs/design-system/components/context-menu.md +59 -0
  38. package/docs/design-system/components/date-picker.md +97 -0
  39. package/docs/design-system/components/divider.md +117 -0
  40. package/docs/design-system/components/empty-state.md +72 -0
  41. package/docs/design-system/components/field.md +86 -0
  42. package/docs/design-system/components/file-upload.md +199 -0
  43. package/docs/design-system/components/hover-card.md +61 -0
  44. package/docs/design-system/components/icons.md +48 -0
  45. package/docs/design-system/components/input-group.md +73 -0
  46. package/docs/design-system/components/input-otp.md +73 -0
  47. package/docs/design-system/components/input.md +84 -0
  48. package/docs/design-system/components/item.md +72 -0
  49. package/docs/design-system/components/kbd.md +62 -0
  50. package/docs/design-system/components/label.md +64 -0
  51. package/docs/design-system/components/menu.md +76 -0
  52. package/docs/design-system/components/menubar.md +60 -0
  53. package/docs/design-system/components/modal.md +119 -0
  54. package/docs/design-system/components/native-select.md +70 -0
  55. package/docs/design-system/components/navigation-menu.md +63 -0
  56. package/docs/design-system/components/onboarding-step-list-item.md +94 -0
  57. package/docs/design-system/components/page-header.md +98 -0
  58. package/docs/design-system/components/pagination.md +71 -0
  59. package/docs/design-system/components/popover.md +77 -0
  60. package/docs/design-system/components/progress-slider.md +66 -0
  61. package/docs/design-system/components/progress.md +92 -0
  62. package/docs/design-system/components/radio-card.md +65 -0
  63. package/docs/design-system/components/radio-group.md +72 -0
  64. package/docs/design-system/components/resizable.md +57 -0
  65. package/docs/design-system/components/scroll-area.md +60 -0
  66. package/docs/design-system/components/segmented-control.md +71 -0
  67. package/docs/design-system/components/select.md +87 -0
  68. package/docs/design-system/components/sheet.md +82 -0
  69. package/docs/design-system/components/sidebar.md +83 -0
  70. package/docs/design-system/components/skeleton.md +91 -0
  71. package/docs/design-system/components/slideout.md +77 -0
  72. package/docs/design-system/components/slider.md +81 -0
  73. package/docs/design-system/components/social-button.md +65 -0
  74. package/docs/design-system/components/spinner.md +77 -0
  75. package/docs/design-system/components/steps.md +77 -0
  76. package/docs/design-system/components/table.md +411 -0
  77. package/docs/design-system/components/tabs.md +69 -0
  78. package/docs/design-system/components/tag.md +95 -0
  79. package/docs/design-system/components/textarea.md +64 -0
  80. package/docs/design-system/components/timeline.md +95 -0
  81. package/docs/design-system/components/toast.md +73 -0
  82. package/docs/design-system/components/toggle.md +96 -0
  83. package/docs/design-system/components/toolbar.md +105 -0
  84. package/docs/design-system/components/tooltip.md +107 -0
  85. package/docs/design-system/components/typography.md +18 -0
  86. package/docs/design-system/density-and-elevation.md +44 -0
  87. package/docs/design-system/design-system-test-missing-items.md +368 -0
  88. package/docs/design-system/direction-and-localization.md +23 -0
  89. package/docs/design-system/form-recipes.md +73 -0
  90. package/docs/design-system/gap-promotion-policy.md +21 -0
  91. package/docs/design-system/gap-promotion-report.md +166 -0
  92. package/docs/design-system/icons.md +69 -0
  93. package/docs/design-system/installation.md +68 -0
  94. package/docs/design-system/launch-comparison-shadcn.md +98 -0
  95. package/docs/design-system/registry-and-cli.md +64 -0
  96. package/docs/design-system/release-policy.md +30 -0
  97. package/docs/design-system/roadmap.md +14 -0
  98. package/docs/design-system/support-matrix.md +23 -0
  99. package/docs/design-system/tabs.md +53 -0
  100. package/docs/design-system/token-governance.md +60 -0
  101. package/package.json +97 -65
  102. package/registry/component-contract.json +40 -0
  103. package/registry/components/alert-dialog.tsx +297 -0
  104. package/registry/components/alert.tsx +86 -65
  105. package/registry/components/aspect-ratio.tsx +30 -0
  106. package/registry/components/autocomplete.tsx +23 -9
  107. package/registry/components/avatar.tsx +7 -7
  108. package/registry/components/badge.tsx +1 -1
  109. package/registry/components/button-group.tsx +321 -4
  110. package/registry/components/button.tsx +38 -17
  111. package/registry/components/card.tsx +68 -59
  112. package/registry/components/carousel.tsx +282 -0
  113. package/registry/components/chart.tsx +203 -0
  114. package/registry/components/checkbox.tsx +3 -3
  115. package/registry/components/collapsible.tsx +69 -0
  116. package/registry/components/command.tsx +174 -0
  117. package/registry/components/context-menu.tsx +236 -0
  118. package/registry/components/date-picker.tsx +246 -105
  119. package/registry/components/expanded/ActivityFeed.tsx +6 -15
  120. package/registry/components/expanded/Banner.tsx +3 -3
  121. package/registry/components/expanded/Breadcrumbs.tsx +5 -5
  122. package/registry/components/expanded/CatalogComponentsShowcase.tsx +13 -13
  123. package/registry/components/expanded/CatalogDivider.tsx +1 -1
  124. package/registry/components/expanded/EmptyState.tsx +1 -1
  125. package/registry/components/expanded/FileUpload.tsx +9 -2
  126. package/registry/components/expanded/OnboardingStepListItem.tsx +2 -4
  127. package/registry/components/expanded/PageHeader.tsx +14 -14
  128. package/registry/components/expanded/Slideout.tsx +15 -415
  129. package/registry/components/expanded/Steps.tsx +4 -4
  130. package/registry/components/expanded/Table.tsx +116 -69
  131. package/registry/components/expanded/Timeline.tsx +3 -3
  132. package/registry/components/expanded/activityFeed.css +1 -1
  133. package/registry/components/expanded/breadcrumbs.css +139 -139
  134. package/registry/components/expanded/catalogComponentsShowcase.css +83 -83
  135. package/registry/components/expanded/divider.css +1 -1
  136. package/registry/components/expanded/emptyState.css +1 -1
  137. package/registry/components/expanded/fileUpload.css +6 -6
  138. package/registry/components/expanded/slideout.css +9 -685
  139. package/registry/components/expanded/steps.css +274 -274
  140. package/registry/components/expanded/table.css +2 -2
  141. package/registry/components/expanded/timeline.css +264 -264
  142. package/registry/components/field.tsx +230 -0
  143. package/registry/components/hover-card.tsx +48 -0
  144. package/registry/components/input-content.ts +1 -0
  145. package/registry/components/input-group.tsx +130 -0
  146. package/registry/components/input-otp.tsx +13 -3
  147. package/registry/components/input.tsx +70 -45
  148. package/registry/components/item.tsx +189 -0
  149. package/registry/components/kbd.tsx +151 -0
  150. package/registry/components/label.tsx +78 -0
  151. package/registry/components/menu.tsx +34 -246
  152. package/registry/components/menubar.tsx +226 -0
  153. package/registry/components/modal.tsx +216 -143
  154. package/registry/components/native-select.tsx +205 -0
  155. package/registry/components/navigation-menu.tsx +171 -0
  156. package/registry/components/pagination.tsx +80 -58
  157. package/registry/components/popover.tsx +162 -664
  158. package/registry/components/radio-card.tsx +116 -87
  159. package/registry/components/radio-group.tsx +1 -1
  160. package/registry/components/resizable.tsx +74 -0
  161. package/registry/components/scroll-area.tsx +67 -0
  162. package/registry/components/segmented-control.tsx +87 -0
  163. package/registry/components/select-content.tsx +2 -2
  164. package/registry/components/select.tsx +54 -20
  165. package/registry/components/sheet.tsx +305 -0
  166. package/registry/components/sidebar.tsx +352 -0
  167. package/registry/components/skeleton.css +0 -3
  168. package/registry/components/social-button.tsx +116 -30
  169. package/registry/components/spinner.tsx +18 -55
  170. package/registry/components/{expanded/tabs.css → tabs.css} +127 -106
  171. package/registry/components/tabs.tsx +246 -0
  172. package/registry/components/tag.tsx +2 -2
  173. package/registry/components/textarea.tsx +4 -4
  174. package/registry/components/toast.tsx +136 -0
  175. package/registry/components/toggle.tsx +2 -2
  176. package/registry/components/toolbar.tsx +89 -17
  177. package/registry/components/tooltip.tsx +1 -1
  178. package/registry/examples/alert-demo.tsx +25 -5
  179. package/registry/examples/alert-dialog-demo.tsx +42 -0
  180. package/registry/examples/aspect-ratio-demo.tsx +11 -0
  181. package/registry/examples/attribute-demo.tsx +1 -1
  182. package/registry/examples/autocomplete-demo.tsx +3 -3
  183. package/registry/examples/avatar-demo.tsx +7 -7
  184. package/registry/examples/badge-demo.tsx +1 -1
  185. package/registry/examples/button-demo.tsx +6 -6
  186. package/registry/examples/button-group-demo.tsx +23 -2
  187. package/registry/examples/card-demo.tsx +46 -8
  188. package/registry/examples/carousel-demo.tsx +43 -0
  189. package/registry/examples/chart-demo.tsx +33 -0
  190. package/registry/examples/checkbox-demo.tsx +5 -5
  191. package/registry/examples/collapsible-demo.tsx +16 -0
  192. package/registry/examples/collection-filter-bar-demo.tsx +135 -0
  193. package/registry/examples/command-demo.tsx +42 -0
  194. package/registry/examples/{expanded/command-bar-demo.tsx → command-palette-demo.tsx} +5 -15
  195. package/registry/examples/context-menu-demo.tsx +29 -0
  196. package/registry/examples/control-density-contract-demo.tsx +61 -0
  197. package/registry/examples/date-picker-demo.tsx +12 -12
  198. package/registry/examples/divider-demo.tsx +3 -3
  199. package/registry/examples/expanded/banner-demo.tsx +1 -1
  200. package/registry/examples/expanded/breadcrumbs-demo.tsx +1 -1
  201. package/registry/examples/expanded/empty-state-demo.tsx +1 -1
  202. package/registry/examples/expanded/file-upload-demo.tsx +1 -1
  203. package/registry/examples/expanded/onboarding-step-demo.tsx +1 -1
  204. package/registry/examples/expanded/slideout-demo.tsx +2 -2
  205. package/registry/examples/expanded/steps-demo.tsx +1 -1
  206. package/registry/examples/expanded/tabs-demo.tsx +2 -2
  207. package/registry/examples/expanded/timeline-demo.tsx +1 -1
  208. package/registry/examples/field-demo.tsx +51 -0
  209. package/registry/examples/hover-card-demo.tsx +23 -0
  210. package/registry/examples/input-demo.tsx +6 -6
  211. package/registry/examples/input-group-demo.tsx +16 -0
  212. package/registry/examples/input-otp-demo.tsx +4 -4
  213. package/registry/examples/item-demo.tsx +46 -0
  214. package/registry/examples/kbd-demo.tsx +11 -0
  215. package/registry/examples/label-demo.tsx +20 -0
  216. package/registry/examples/menu-demo.tsx +1 -1
  217. package/registry/examples/menubar-demo.tsx +34 -0
  218. package/registry/examples/modal-demo.tsx +67 -9
  219. package/registry/examples/native-select-demo.tsx +16 -0
  220. package/registry/examples/navigation-menu-demo.tsx +29 -0
  221. package/registry/examples/pagination-demo.tsx +6 -3
  222. package/registry/examples/popover-demo.tsx +19 -19
  223. package/registry/examples/progress-demo.tsx +1 -1
  224. package/registry/examples/progress-slider-demo.tsx +1 -1
  225. package/registry/examples/radio-card-demo.tsx +4 -4
  226. package/registry/examples/radio-group-demo.tsx +2 -2
  227. package/registry/examples/resizable-demo.tsx +22 -0
  228. package/registry/examples/results-toolbar-demo.tsx +44 -0
  229. package/registry/examples/scroll-area-demo.tsx +15 -0
  230. package/registry/examples/segmented-control-demo.tsx +39 -0
  231. package/registry/examples/select-demo.tsx +1 -1
  232. package/registry/examples/sheet-demo.tsx +47 -0
  233. package/registry/examples/sidebar-demo.tsx +55 -0
  234. package/registry/examples/slider-demo.tsx +3 -3
  235. package/registry/examples/social-button-demo.tsx +18 -8
  236. package/registry/examples/spinner-demo.tsx +13 -13
  237. package/registry/examples/tabs-demo.tsx +13 -0
  238. package/registry/examples/tag-demo.tsx +1 -1
  239. package/registry/examples/textarea-demo.tsx +4 -4
  240. package/registry/examples/toast-demo.tsx +35 -0
  241. package/registry/examples/toggle-demo.tsx +6 -6
  242. package/registry/examples/tooltip-demo.tsx +4 -4
  243. package/registry/guidance/USAGE.md +27 -0
  244. package/registry/hooks/use-direction.ts +193 -13
  245. package/registry/index.json +6010 -432
  246. package/registry/styles/globals.css +3112 -4690
  247. package/registry.json +1582 -0
  248. package/schema/config.schema.json +51 -0
  249. package/schema/registry.schema.json +190 -0
  250. package/schema/tokens.schema.json +163 -0
  251. package/src/cli/command-line.js +194 -0
  252. package/src/cli/config-editors.js +9 -0
  253. package/src/cli/doctor.js +399 -0
  254. package/src/cli/file-transaction.js +295 -0
  255. package/src/cli/global-css.js +153 -0
  256. package/src/cli/index.js +639 -1391
  257. package/src/cli/install-plan.js +592 -0
  258. package/src/cli/install-receipt.js +171 -0
  259. package/src/cli/jsonc.js +203 -0
  260. package/src/cli/package-install-state.js +254 -0
  261. package/src/cli/package-manager.js +238 -0
  262. package/src/cli/package-plan.js +185 -0
  263. package/src/cli/package-version.js +97 -0
  264. package/src/cli/registry-query.js +207 -0
  265. package/src/cli/reporting.js +55 -0
  266. package/src/cli/schema-validation.js +19 -0
  267. package/src/cli/source-scanner.js +686 -0
  268. package/src/cli/vite-config.js +478 -0
  269. package/tokens/banhaten.tokens.json +4313 -310
  270. package/registry/assets/avatars/avatar-02.jpg +0 -0
  271. package/registry/assets/avatars/avatar-03.jpg +0 -0
  272. package/registry/assets/avatars/avatar-04.jpg +0 -0
  273. package/registry/assets/avatars/avatar-05.jpg +0 -0
  274. package/registry/assets/avatars/avatar-06.jpg +0 -0
  275. package/registry/assets/avatars/avatar-07.jpg +0 -0
  276. package/registry/assets/avatars/avatar-08.jpg +0 -0
  277. package/registry/assets/avatars/avatar-09.jpg +0 -0
  278. package/registry/assets/avatars/avatar-10.jpg +0 -0
  279. package/registry/assets/avatars/avatar-11.jpg +0 -0
  280. package/registry/assets/avatars/avatar-12.jpg +0 -0
  281. package/registry/assets/avatars/avatar-13.jpg +0 -0
  282. package/registry/assets/avatars/avatar-14.jpg +0 -0
  283. package/registry/assets/avatars/avatar-15.jpg +0 -0
  284. package/registry/assets/avatars/avatar-16.jpg +0 -0
  285. package/registry/assets/avatars/avatar-17.jpg +0 -0
  286. package/registry/assets/avatars/avatar-18.jpg +0 -0
  287. package/registry/assets/avatars/avatar-19.jpg +0 -0
  288. package/registry/assets/avatars/avatar-20.jpg +0 -0
  289. package/registry/assets/avatars/avatar-21.jpg +0 -0
  290. package/registry/assets/avatars/avatar-22.jpg +0 -0
  291. package/registry/assets/avatars/avatar-23.jpg +0 -0
  292. package/registry/assets/avatars/avatar-24.jpg +0 -0
  293. package/registry/assets/avatars/avatar-25.jpg +0 -0
  294. package/registry/assets/avatars/avatar-26.jpg +0 -0
  295. package/registry/assets/avatars/avatar-27.jpg +0 -0
  296. package/registry/assets/avatars/avatar-28.jpg +0 -0
  297. package/registry/assets/avatars/avatar-29.jpg +0 -0
  298. package/registry/assets/avatars/avatar-30.jpg +0 -0
  299. package/registry/assets/avatars/avatar-31.jpg +0 -0
  300. package/registry/assets/avatars/avatar-32.jpg +0 -0
  301. package/registry/assets/avatars/avatar-33.jpg +0 -0
  302. package/registry/assets/avatars/avatar-34.jpg +0 -0
  303. package/registry/assets/avatars/avatar-35.jpg +0 -0
  304. package/registry/assets/image-assets.json +0 -744
  305. package/registry/assets/images/art-02.jpg +0 -0
  306. package/registry/assets/images/art-03.jpg +0 -0
  307. package/registry/assets/images/art-04.jpg +0 -0
  308. package/registry/assets/images/art-05.jpg +0 -0
  309. package/registry/assets/images/art-06.jpg +0 -0
  310. package/registry/assets/images/art-07.jpg +0 -0
  311. package/registry/assets/images/art-08.jpg +0 -0
  312. package/registry/assets/images/art-09.jpg +0 -0
  313. package/registry/assets/images/art-10.jpg +0 -0
  314. package/registry/assets/images/art-11.jpg +0 -0
  315. package/registry/assets/images/art-12.jpg +0 -0
  316. package/registry/assets/images/art-13.jpg +0 -0
  317. package/registry/assets/images/art-14.jpg +0 -0
  318. package/registry/assets/images/art-15.jpg +0 -0
  319. package/registry/assets/images/art-16.jpg +0 -0
  320. package/registry/assets/images/art-17.jpg +0 -0
  321. package/registry/assets/images/art-18.jpg +0 -0
  322. package/registry/assets/images/art-19.jpg +0 -0
  323. package/registry/assets/images/art-20.jpg +0 -0
  324. package/registry/assets/images/art-21.jpg +0 -0
  325. package/registry/assets/images/art-22.jpg +0 -0
  326. package/registry/assets/images/art-23.jpg +0 -0
  327. package/registry/assets/images/art-24.jpg +0 -0
  328. package/registry/assets/images/art-25.jpg +0 -0
  329. package/registry/assets/images/art-26.jpg +0 -0
  330. package/registry/assets/images/art-27.jpg +0 -0
  331. package/registry/assets/images/nature-01.jpg +0 -0
  332. package/registry/assets/images/nature-02.jpg +0 -0
  333. package/registry/assets/images/nature-03.jpg +0 -0
  334. package/registry/assets/images/nature-04.jpg +0 -0
  335. package/registry/assets/images/nature-05.jpg +0 -0
  336. package/registry/assets/images/nature-06.jpg +0 -0
  337. package/registry/assets/images/nature-07.jpg +0 -0
  338. package/registry/assets/images/nature-08.jpg +0 -0
  339. package/registry/assets/images/nature-09.jpg +0 -0
  340. package/registry/assets/images/nature-10.jpg +0 -0
  341. package/registry/assets/images/nature-11.jpg +0 -0
  342. package/registry/assets/images/nature-12.jpg +0 -0
  343. package/registry/assets/images/nature-13.jpg +0 -0
  344. package/registry/assets/images/nature-14.jpg +0 -0
  345. package/registry/assets/images/nature-15.jpg +0 -0
  346. package/registry/assets/images/nature-16.jpg +0 -0
  347. package/registry/assets/images/nature-17.jpg +0 -0
  348. package/registry/assets/images/nature-18.jpg +0 -0
  349. package/registry/assets/images/nature-19.jpg +0 -0
  350. package/registry/assets/images/nature-20.jpg +0 -0
  351. package/registry/components/expanded/BannerBoard.tsx +0 -225
  352. package/registry/components/expanded/CatalogTag.tsx +0 -85
  353. package/registry/components/expanded/CommandBar.tsx +0 -386
  354. package/registry/components/expanded/IconExplorer.tsx +0 -612
  355. package/registry/components/expanded/Tabs.tsx +0 -86
  356. package/registry/components/expanded/TimelineShowcase.tsx +0 -158
  357. package/registry/components/expanded/commandBar.css +0 -470
  358. package/registry/components/expanded/iconExplorer.css +0 -764
  359. package/registry/components/expanded/iconPacks.ts +0 -866
  360. package/registry/components/expanded/tag.css +0 -148
  361. package/registry/examples/expanded/icons-demo.tsx +0 -5
  362. package/tokens/Color mode.zip +0 -0
  363. package/tokens/Numbers.zip +0 -0
  364. package/tokens/Radius.zip +0 -0
  365. package/tokens/Theme.zip +0 -0
@@ -6,17 +6,31 @@ import { cn } from "@/lib/utils"
6
6
 
7
7
  type ButtonGroupSize = "md" | "sm" | "xs"
8
8
  type ButtonGroupMode = "default" | "iconOnly"
9
+ type ButtonGroupItemWidth = "fixed" | "content"
10
+ type ControlDensity = "compact" | "default" | "comfortable"
11
+ type ToggleGroupType = "single" | "multiple"
12
+ type ToggleGroupValue = string | string[]
9
13
 
10
14
  type ButtonGroupContextValue = {
15
+ itemWidth: ButtonGroupItemWidth
11
16
  size: ButtonGroupSize
12
17
  mode: ButtonGroupMode
13
18
  }
14
19
 
15
20
  const buttonGroupContext = React.createContext<ButtonGroupContextValue>({
21
+ itemWidth: "fixed",
16
22
  size: "md",
17
23
  mode: "default",
18
24
  })
19
25
 
26
+ type ToggleGroupContextValue = {
27
+ disabled: boolean
28
+ isPressed: (value: string) => boolean
29
+ toggle: (value: string) => void
30
+ }
31
+
32
+ const toggleGroupContext = React.createContext<ToggleGroupContextValue | null>(null)
33
+
20
34
  const buttonGroupVariants = cva(
21
35
  "inline-flex w-fit items-center overflow-hidden rounded-[var(--bh-button-group-radius)] bg-[var(--bh-button-group-bg)] text-[var(--bh-button-group-content)] shadow-[var(--shadow-button-group)] [--shadow-button-group:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-button-group-border),var(--shadow-component-default)]",
22
36
  {
@@ -46,26 +60,51 @@ const buttonGroupItemVariants = cva(
46
60
  default: "",
47
61
  iconOnly: "",
48
62
  },
63
+ itemWidth: {
64
+ fixed: "",
65
+ content: "",
66
+ },
49
67
  },
50
68
  compoundVariants: [
51
69
  {
52
70
  size: "md",
53
71
  mode: "default",
72
+ itemWidth: "fixed",
54
73
  class:
55
74
  "w-[var(--bh-button-group-md-item-width)] px-[var(--bh-button-group-md-padding-x)] rtl:w-[var(--bh-button-group-md-item-rtl-width)]",
56
75
  },
57
76
  {
58
77
  size: "sm",
59
78
  mode: "default",
79
+ itemWidth: "fixed",
60
80
  class:
61
81
  "w-[var(--bh-button-group-sm-item-width)] px-[var(--bh-button-group-sm-padding-x)] rtl:w-[var(--bh-button-group-sm-item-rtl-width)]",
62
82
  },
63
83
  {
64
84
  size: "xs",
65
85
  mode: "default",
86
+ itemWidth: "fixed",
66
87
  class:
67
88
  "w-[var(--bh-button-group-xs-item-width)] px-[var(--bh-button-group-xs-padding-x)] rtl:w-[var(--bh-button-group-xs-item-rtl-width)]",
68
89
  },
90
+ {
91
+ size: "md",
92
+ mode: "default",
93
+ itemWidth: "content",
94
+ class: "w-auto px-[var(--bh-button-group-md-padding-x)]",
95
+ },
96
+ {
97
+ size: "sm",
98
+ mode: "default",
99
+ itemWidth: "content",
100
+ class: "w-auto px-[var(--bh-button-group-sm-padding-x)]",
101
+ },
102
+ {
103
+ size: "xs",
104
+ mode: "default",
105
+ itemWidth: "content",
106
+ class: "w-auto px-[var(--bh-button-group-xs-padding-x)]",
107
+ },
69
108
  {
70
109
  size: "md",
71
110
  mode: "iconOnly",
@@ -88,35 +127,97 @@ const buttonGroupItemVariants = cva(
88
127
  defaultVariants: {
89
128
  size: "md",
90
129
  mode: "default",
130
+ itemWidth: "fixed",
131
+ },
132
+ }
133
+ )
134
+
135
+ const buttonGroupLabelVariants = cva(
136
+ "relative inline-flex shrink-0 items-center justify-center whitespace-nowrap bg-[var(--bh-button-group-item-bg)] text-[length:var(--bh-text-button-font-size)] font-[var(--bh-text-button-font-weight)] leading-[var(--bh-text-button-line-height)] tracking-[var(--bh-text-button-letter-spacing)] text-[var(--bh-button-group-content)] before:absolute before:inset-y-0 before:start-0 before:w-[var(--bh-border-width-default)] before:bg-[var(--bh-button-group-divider)] before:content-[''] first:before:hidden",
137
+ {
138
+ variants: {
139
+ size: {
140
+ md: "h-[var(--bh-button-group-md-height)]",
141
+ sm: "h-[var(--bh-button-group-sm-height)]",
142
+ xs: "h-[var(--bh-button-group-xs-height)]",
143
+ },
144
+ itemWidth: {
145
+ fixed: "",
146
+ content: "",
147
+ },
148
+ },
149
+ compoundVariants: [
150
+ {
151
+ size: "md",
152
+ itemWidth: "fixed",
153
+ class: "w-[var(--bh-button-group-md-item-width)] px-[var(--bh-button-group-md-padding-x)] rtl:w-[var(--bh-button-group-md-item-rtl-width)]",
154
+ },
155
+ {
156
+ size: "sm",
157
+ itemWidth: "fixed",
158
+ class: "w-[var(--bh-button-group-sm-item-width)] px-[var(--bh-button-group-sm-padding-x)] rtl:w-[var(--bh-button-group-sm-item-rtl-width)]",
159
+ },
160
+ {
161
+ size: "xs",
162
+ itemWidth: "fixed",
163
+ class: "w-[var(--bh-button-group-xs-item-width)] px-[var(--bh-button-group-xs-padding-x)] rtl:w-[var(--bh-button-group-xs-item-rtl-width)]",
164
+ },
165
+ {
166
+ size: "md",
167
+ itemWidth: "content",
168
+ class: "w-auto px-[var(--bh-button-group-md-padding-x)]",
169
+ },
170
+ {
171
+ size: "sm",
172
+ itemWidth: "content",
173
+ class: "w-auto px-[var(--bh-button-group-sm-padding-x)]",
174
+ },
175
+ {
176
+ size: "xs",
177
+ itemWidth: "content",
178
+ class: "w-auto px-[var(--bh-button-group-xs-padding-x)]",
179
+ },
180
+ ],
181
+ defaultVariants: {
182
+ itemWidth: "content",
183
+ size: "sm",
91
184
  },
92
185
  }
93
186
  )
94
187
 
95
188
  type ButtonGroupProps = React.ComponentProps<"div"> &
96
189
  VariantProps<typeof buttonGroupVariants> & {
190
+ density?: ControlDensity
191
+ itemWidth?: ButtonGroupItemWidth
97
192
  mode?: ButtonGroupMode
98
193
  }
99
194
 
100
195
  const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(function ButtonGroup({
101
196
  className,
102
- size = "md",
197
+ density,
198
+ itemWidth = "fixed",
199
+ size,
103
200
  mode = "default",
104
201
  children,
105
202
  role = "group",
106
203
  ...props
107
204
  }, ref) {
205
+ const resolvedSize = resolveButtonGroupSize(density, size)
108
206
  const contextValue = React.useMemo(
109
207
  () => ({
110
- size: (size || "md") as ButtonGroupSize,
208
+ itemWidth,
209
+ size: resolvedSize,
111
210
  mode: mode || "default",
112
211
  }),
113
- [size, mode]
212
+ [itemWidth, mode, resolvedSize]
114
213
  )
115
214
 
116
215
  return (
117
216
  <buttonGroupContext.Provider value={contextValue}>
118
217
  <div
119
218
  data-mode={contextValue.mode}
219
+ data-density={density}
220
+ data-item-width={contextValue.itemWidth}
120
221
  data-size={contextValue.size}
121
222
  data-slot="button-group"
122
223
  ref={ref}
@@ -138,6 +239,7 @@ type ButtonGroupItemProps = React.ComponentProps<"button"> &
138
239
  const ButtonGroupItem = React.forwardRef<HTMLButtonElement, ButtonGroupItemProps>(function ButtonGroupItem({
139
240
  className,
140
241
  size,
242
+ itemWidth,
141
243
  mode,
142
244
  asChild = false,
143
245
  children,
@@ -151,6 +253,7 @@ const ButtonGroupItem = React.forwardRef<HTMLButtonElement, ButtonGroupItemProps
151
253
  const context = React.useContext(buttonGroupContext)
152
254
  const selectedSize = (size || context.size) as ButtonGroupSize
153
255
  const selectedMode = mode || context.mode
256
+ const selectedItemWidth = itemWidth || context.itemWidth
154
257
  const Comp = asChild ? Slot : "button"
155
258
  const accessibleLabel =
156
259
  selectedMode === "iconOnly" ? ariaLabel || getTextAlternative(children) : ariaLabel
@@ -170,6 +273,7 @@ const ButtonGroupItem = React.forwardRef<HTMLButtonElement, ButtonGroupItemProps
170
273
  aria-label={accessibleLabel}
171
274
  aria-pressed={ariaPressed}
172
275
  data-mode={selectedMode}
276
+ data-item-width={selectedItemWidth}
173
277
  data-size={selectedSize}
174
278
  data-slot="button-group-item"
175
279
  ref={ref}
@@ -177,6 +281,7 @@ const ButtonGroupItem = React.forwardRef<HTMLButtonElement, ButtonGroupItemProps
177
281
  buttonGroupItemVariants({
178
282
  size: selectedSize,
179
283
  mode: selectedMode,
284
+ itemWidth: selectedItemWidth,
180
285
  className,
181
286
  })
182
287
  )}
@@ -192,6 +297,39 @@ const ButtonGroupItem = React.forwardRef<HTMLButtonElement, ButtonGroupItemProps
192
297
  )
193
298
  })
194
299
 
300
+ type ButtonGroupLabelProps = React.ComponentProps<"span"> &
301
+ VariantProps<typeof buttonGroupLabelVariants>
302
+
303
+ const ButtonGroupLabel = React.forwardRef<HTMLSpanElement, ButtonGroupLabelProps>(
304
+ function ButtonGroupLabel(
305
+ { "aria-hidden": ariaHidden = true, className, itemWidth, size, ...props },
306
+ ref
307
+ ) {
308
+ const context = React.useContext(buttonGroupContext)
309
+ const selectedSize = (size || context.size) as ButtonGroupSize
310
+ const selectedItemWidth = itemWidth || context.itemWidth
311
+
312
+ return (
313
+ <span
314
+ aria-hidden={ariaHidden}
315
+ className={cn(
316
+ buttonGroupLabelVariants({
317
+ itemWidth: selectedItemWidth,
318
+ size: selectedSize,
319
+ }),
320
+ className
321
+ )}
322
+ data-item-width={selectedItemWidth}
323
+ data-size={selectedSize}
324
+ data-slot="button-group-label"
325
+ dir="auto"
326
+ ref={ref}
327
+ {...props}
328
+ />
329
+ )
330
+ }
331
+ )
332
+
195
333
  function ButtonGroupItemChildren({
196
334
  children,
197
335
  mode,
@@ -237,10 +375,189 @@ function getTextAlternative(children: React.ReactNode) {
237
375
  return text.trim() || undefined
238
376
  }
239
377
 
240
- export { ButtonGroup, ButtonGroupItem, buttonGroupVariants, buttonGroupItemVariants }
378
+ function resolveButtonGroupSize(
379
+ density: ControlDensity | undefined,
380
+ size: ButtonGroupSize | null | undefined
381
+ ): ButtonGroupSize {
382
+ if (density === "compact") return "xs"
383
+ if (density === "default") return "sm"
384
+ if (density === "comfortable") return "md"
385
+ return size ?? "md"
386
+ }
387
+
388
+ type ToggleGroupProps = Omit<ButtonGroupProps, "onChange"> & {
389
+ defaultValue?: ToggleGroupValue
390
+ disabled?: boolean
391
+ onValueChange?: (value: ToggleGroupValue) => void
392
+ type?: ToggleGroupType
393
+ value?: ToggleGroupValue
394
+ }
395
+
396
+ const ToggleGroup = React.forwardRef<HTMLDivElement, ToggleGroupProps>(
397
+ function ToggleGroup(
398
+ {
399
+ children,
400
+ defaultValue,
401
+ disabled = false,
402
+ onKeyDown,
403
+ onValueChange,
404
+ type = "single",
405
+ value,
406
+ ...props
407
+ },
408
+ ref
409
+ ) {
410
+ const controlled = value !== undefined
411
+ const [internalValue, setInternalValue] = React.useState<ToggleGroupValue>(
412
+ defaultValue ?? (type === "multiple" ? [] : "")
413
+ )
414
+ const selectedValue = normalizeToggleGroupValue(
415
+ controlled ? value : internalValue,
416
+ type
417
+ )
418
+
419
+ const updateValue = React.useCallback(
420
+ (nextValue: ToggleGroupValue) => {
421
+ if (!controlled) setInternalValue(nextValue)
422
+ onValueChange?.(nextValue)
423
+ },
424
+ [controlled, onValueChange]
425
+ )
426
+
427
+ const context = React.useMemo<ToggleGroupContextValue>(
428
+ () => ({
429
+ disabled,
430
+ isPressed: (itemValue) =>
431
+ Array.isArray(selectedValue)
432
+ ? selectedValue.includes(itemValue)
433
+ : selectedValue === itemValue,
434
+ toggle: (itemValue) => {
435
+ if (disabled) return
436
+ if (type === "multiple") {
437
+ const values = Array.isArray(selectedValue) ? selectedValue : []
438
+ updateValue(
439
+ values.includes(itemValue)
440
+ ? values.filter((entry) => entry !== itemValue)
441
+ : [...values, itemValue]
442
+ )
443
+ return
444
+ }
445
+ updateValue(selectedValue === itemValue ? "" : itemValue)
446
+ },
447
+ }),
448
+ [disabled, selectedValue, type, updateValue]
449
+ )
450
+
451
+ return (
452
+ <toggleGroupContext.Provider value={context}>
453
+ <ButtonGroup
454
+ aria-disabled={disabled || undefined}
455
+ data-selection-type={type}
456
+ data-slot="toggle-group"
457
+ onKeyDown={(event) => {
458
+ onKeyDown?.(event)
459
+ if (!event.defaultPrevented) moveToggleGroupFocus(event)
460
+ }}
461
+ ref={ref}
462
+ {...props}
463
+ >
464
+ {children}
465
+ </ButtonGroup>
466
+ </toggleGroupContext.Provider>
467
+ )
468
+ }
469
+ )
470
+
471
+ type ToggleGroupItemProps = Omit<ButtonGroupItemProps, "aria-pressed"> & {
472
+ value: string
473
+ }
474
+
475
+ const ToggleGroupItem = React.forwardRef<
476
+ HTMLButtonElement,
477
+ ToggleGroupItemProps
478
+ >(function ToggleGroupItem(
479
+ { disabled, onClick, value, ...props },
480
+ ref
481
+ ) {
482
+ const context = React.useContext(toggleGroupContext)
483
+ if (!context) {
484
+ throw new Error("ToggleGroupItem must be used within ToggleGroup")
485
+ }
486
+
487
+ const pressed = context.isPressed(value)
488
+ const isDisabled = disabled || context.disabled
489
+
490
+ return (
491
+ <ButtonGroupItem
492
+ aria-pressed={pressed}
493
+ data-slot="toggle-group-item"
494
+ data-value={value}
495
+ disabled={isDisabled}
496
+ onClick={(event) => {
497
+ onClick?.(event)
498
+ if (!event.defaultPrevented) context.toggle(value)
499
+ }}
500
+ ref={ref}
501
+ {...props}
502
+ />
503
+ )
504
+ })
505
+
506
+ function normalizeToggleGroupValue(
507
+ value: ToggleGroupValue,
508
+ type: ToggleGroupType
509
+ ): ToggleGroupValue {
510
+ if (type === "multiple") return Array.isArray(value) ? value : value ? [value] : []
511
+ return Array.isArray(value) ? value[0] ?? "" : value
512
+ }
513
+
514
+ function moveToggleGroupFocus(event: React.KeyboardEvent<HTMLDivElement>) {
515
+ const horizontalKeys = ["ArrowLeft", "ArrowRight"]
516
+ const verticalKeys = ["ArrowUp", "ArrowDown"]
517
+ if (![...horizontalKeys, ...verticalKeys, "Home", "End"].includes(event.key)) {
518
+ return
519
+ }
520
+
521
+ const items = [...event.currentTarget.querySelectorAll<HTMLButtonElement>(
522
+ '[data-slot="toggle-group-item"]:not(:disabled)'
523
+ )]
524
+ if (!items.length) return
525
+
526
+ const currentIndex = Math.max(0, items.indexOf(document.activeElement as HTMLButtonElement))
527
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl"
528
+ let nextIndex = currentIndex
529
+
530
+ if (event.key === "Home") nextIndex = 0
531
+ else if (event.key === "End") nextIndex = items.length - 1
532
+ else if (event.key === "ArrowRight") nextIndex += rtl ? -1 : 1
533
+ else if (event.key === "ArrowLeft") nextIndex += rtl ? 1 : -1
534
+ else if (event.key === "ArrowDown") nextIndex += 1
535
+ else if (event.key === "ArrowUp") nextIndex -= 1
536
+
537
+ event.preventDefault()
538
+ items[(nextIndex + items.length) % items.length]?.focus()
539
+ }
540
+
541
+ export {
542
+ ButtonGroup,
543
+ ButtonGroupItem,
544
+ ButtonGroupLabel,
545
+ ToggleGroup,
546
+ ToggleGroupItem,
547
+ buttonGroupVariants,
548
+ buttonGroupItemVariants,
549
+ buttonGroupLabelVariants,
550
+ }
241
551
  export type {
552
+ ButtonGroupLabelProps,
553
+ ButtonGroupItemWidth,
242
554
  ButtonGroupItemProps,
243
555
  ButtonGroupMode,
244
556
  ButtonGroupProps,
245
557
  ButtonGroupSize,
558
+ ControlDensity,
559
+ ToggleGroupItemProps,
560
+ ToggleGroupProps,
561
+ ToggleGroupType,
562
+ ToggleGroupValue,
246
563
  }
@@ -4,38 +4,40 @@ import { cva, type VariantProps } from "class-variance-authority"
4
4
 
5
5
  import { cn } from "@/lib/utils"
6
6
 
7
+ type ControlDensity = "compact" | "default" | "comfortable"
8
+
7
9
  const buttonVariants = cva(
8
- "group/button inline-flex min-w-0 items-center justify-center whitespace-nowrap rounded-[var(--bh-control-default)] text-[length:var(--bh-text-button-font-size)] font-[var(--bh-text-button-font-weight)] leading-[var(--bh-text-button-line-height)] tracking-[var(--bh-text-button-letter-spacing)] outline-none transition-[background-color,border-color,color,box-shadow] focus-visible:shadow-[var(--shadow-button-focus)] disabled:pointer-events-none disabled:shadow-none aria-invalid:border aria-invalid:border-destructive aria-invalid:shadow-[var(--shadow-button-danger-focus)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[var(--bh-button-icon-size)] [&_svg[data-icon='inline-start']]:ms-[var(--bh-button-icon-offset)] [&_svg[data-icon='inline-end']]:me-[var(--bh-button-icon-offset)] rtl:[&_svg[data-rtl-flip='true']]:-scale-x-100",
10
+ "group/button inline-flex min-w-0 items-center justify-center whitespace-nowrap rounded-[var(--bh-control-default)] text-[length:var(--bh-text-button-font-size)] font-[var(--bh-text-button-font-weight)] leading-[var(--bh-text-button-line-height)] tracking-[var(--bh-text-button-letter-spacing)] outline-none transition-[background-color,border-color,color,box-shadow] focus-visible:shadow-[var(--shadow-button-focus)] disabled:pointer-events-none disabled:shadow-none aria-invalid:border-[length:var(--bh-border-width-default)] aria-invalid:border-[var(--bh-border-danger-strong)] aria-invalid:shadow-[var(--shadow-button-danger-focus)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[var(--bh-button-icon-size)] [&_svg[data-icon='inline-start']]:ms-[var(--bh-button-icon-offset)] [&_svg[data-icon='inline-end']]:me-[var(--bh-button-icon-offset)] rtl:[&_svg[data-rtl-flip='true']]:-scale-x-100",
9
11
  {
10
12
  variants: {
11
13
  variant: {
12
14
  default:
13
- "bg-primary text-primary-foreground shadow-[var(--shadow-button-raised)] hover:bg-[var(--primary-hover)] active:bg-[var(--primary-pressed)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
15
+ "bg-[var(--bh-interactive-brand-default)] text-[var(--bh-content-on-brand)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-interactive-brand-hover)] active:bg-[var(--bh-interactive-brand-pressed)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
14
16
  danger:
15
- "bg-destructive text-destructive-foreground shadow-[var(--shadow-button-raised)] hover:bg-[var(--destructive-hover)] active:bg-[var(--destructive-pressed)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-disabled)] disabled:text-[var(--bh-content-disabled)]",
17
+ "bg-[var(--bh-interactive-danger-default)] text-[var(--bh-content-on-color)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-interactive-danger-hover)] active:bg-[var(--bh-interactive-danger-pressed)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-disabled)] disabled:text-[var(--bh-content-disabled)]",
16
18
  destructive:
17
- "bg-destructive text-destructive-foreground shadow-[var(--shadow-button-raised)] hover:bg-[var(--destructive-hover)] active:bg-[var(--destructive-pressed)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-disabled)] disabled:text-[var(--bh-content-disabled)]",
19
+ "bg-[var(--bh-interactive-danger-default)] text-[var(--bh-content-on-color)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-interactive-danger-hover)] active:bg-[var(--bh-interactive-danger-pressed)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-disabled)] disabled:text-[var(--bh-content-disabled)]",
18
20
  success:
19
- "bg-success text-success-foreground shadow-[var(--shadow-button-raised)] hover:bg-[var(--success-hover)] active:bg-[var(--success-pressed)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
21
+ "bg-[var(--bh-bg-success-default)] text-[var(--bh-content-on-color)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-bg-success-bold)] active:bg-[var(--bh-bg-success-strong)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
20
22
  warning:
21
- "bg-warning text-warning-foreground shadow-[var(--shadow-button-raised)] hover:bg-[var(--warning-hover)] active:bg-[var(--warning-pressed)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
23
+ "bg-[var(--bh-bg-warning-default)] text-[var(--bh-content-on-light)] shadow-[var(--shadow-button-raised)] hover:bg-[var(--bh-bg-warning-bold)] active:bg-[var(--bh-bg-warning-strong)] disabled:bg-[var(--bh-interactive-brand-disabled)] disabled:text-[var(--bh-content-disabled)]",
22
24
  secondary:
23
- "bg-secondary text-secondary-foreground shadow-[var(--shadow-button-secondary)] [--bh-button-secondary-border:var(--bh-border-default)] hover:bg-[var(--secondary-hover)] active:bg-[var(--secondary-pressed)] focus-visible:[--bh-button-secondary-focus-border:var(--bh-border-brand-strong)] focus-visible:shadow-[var(--shadow-button-secondary-focus)] disabled:border disabled:border-[var(--bh-border-disabled)] disabled:bg-[var(--bh-interactive-secondary-disabled)] disabled:text-[var(--bh-content-disabled)]",
25
+ "bg-[var(--bh-interactive-secondary-default)] text-[var(--bh-content-default)] shadow-[var(--shadow-button-secondary)] [--bh-button-secondary-border:var(--bh-border-default)] hover:bg-[var(--bh-interactive-secondary-hover)] active:bg-[var(--bh-interactive-secondary-pressed)] focus-visible:[--bh-button-secondary-focus-border:var(--bh-border-brand-strong)] focus-visible:shadow-[var(--shadow-button-secondary-focus)] disabled:border-[length:var(--bh-border-width-default)] disabled:border-[var(--bh-border-disabled)] disabled:bg-[var(--bh-interactive-secondary-disabled)] disabled:text-[var(--bh-content-disabled)]",
24
26
  soft:
25
- "border border-transparent bg-[var(--soft)] text-foreground shadow-[var(--shadow-button-soft)] hover:bg-[var(--soft-hover)] active:bg-[var(--soft-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:bg-[var(--bh-interactive-soft-disabled)] disabled:text-[var(--bh-content-disabled)]",
27
+ "border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-soft-default)] text-[var(--bh-content-default)] shadow-[var(--shadow-button-soft)] hover:bg-[var(--bh-interactive-soft-hover)] active:bg-[var(--bh-interactive-soft-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:bg-[var(--bh-interactive-soft-disabled)] disabled:text-[var(--bh-content-disabled)]",
26
28
  outline:
27
- "border border-[var(--bh-border-default)] bg-[var(--outline)] text-foreground hover:bg-[var(--outline-hover)] active:bg-[var(--outline-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:border-[var(--bh-border-disabled)] disabled:bg-[var(--bh-interactive-outlined-disabled)] disabled:text-[var(--bh-content-disabled)]",
29
+ "border-[length:var(--bh-border-width-default)] border-[var(--bh-border-default)] bg-[var(--bh-interactive-outlined-default)] text-[var(--bh-content-default)] hover:bg-[var(--bh-interactive-outlined-hover)] active:bg-[var(--bh-interactive-outlined-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:border-[var(--bh-border-disabled)] disabled:bg-[var(--bh-interactive-outlined-disabled)] disabled:text-[var(--bh-content-disabled)]",
28
30
  ghost:
29
- "border border-transparent bg-[var(--ghost)] text-foreground hover:bg-[var(--ghost-hover)] active:bg-[var(--ghost-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:bg-[var(--bh-interactive-ghost-disabled)] disabled:text-[var(--bh-content-disabled)]",
31
+ "border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-ghost-default)] text-[var(--bh-content-default)] hover:bg-[var(--bh-interactive-ghost-hover)] active:bg-[var(--bh-interactive-ghost-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:bg-[var(--bh-interactive-ghost-disabled)] disabled:text-[var(--bh-content-disabled)]",
30
32
  "ghost-primary":
31
- "border border-transparent bg-[var(--ghost)] text-[var(--bh-content-brand-default)] hover:bg-[var(--ghost-hover)] active:bg-[var(--ghost-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:bg-[var(--bh-interactive-ghost-disabled)] disabled:text-[var(--bh-content-disabled)]",
33
+ "border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-ghost-default)] text-[var(--bh-content-brand-default)] hover:bg-[var(--bh-interactive-ghost-hover)] active:bg-[var(--bh-interactive-ghost-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:bg-[var(--bh-interactive-ghost-disabled)] disabled:text-[var(--bh-content-disabled)]",
32
34
  white:
33
- "border border-transparent bg-[var(--bh-bg-always-white)] text-[var(--bh-content-on-light)] focus-visible:border-[var(--bh-border-focus)] disabled:opacity-[var(--bh-opacity-30)]",
35
+ "border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-bg-always-white)] text-[var(--bh-content-on-light)] focus-visible:border-[var(--bh-border-focus)] disabled:opacity-[var(--bh-opacity-30)]",
34
36
  "soft-danger":
35
- "border border-transparent bg-[var(--destructive-soft)] text-[var(--destructive-soft-foreground)] hover:bg-[var(--destructive-soft-hover)] active:bg-[var(--destructive-soft-pressed)] focus-visible:border-[var(--destructive)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-soft-disabled)] disabled:text-[var(--bh-content-disabled)]",
37
+ "border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-danger-soft-default)] text-[var(--bh-content-danger-default)] hover:bg-[var(--bh-interactive-danger-soft-hover)] active:bg-[var(--bh-interactive-danger-soft-pressed)] focus-visible:border-[var(--bh-interactive-danger-default)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-soft-disabled)] disabled:text-[var(--bh-content-disabled)]",
36
38
  "soft-destructive":
37
- "border border-transparent bg-[var(--destructive-soft)] text-[var(--destructive-soft-foreground)] hover:bg-[var(--destructive-soft-hover)] active:bg-[var(--destructive-soft-pressed)] focus-visible:border-[var(--destructive)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-soft-disabled)] disabled:text-[var(--bh-content-disabled)]",
38
- link: "bg-[var(--ghost)] text-[var(--bh-content-brand-default)] hover:bg-[var(--ghost-hover)] active:bg-[var(--ghost-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:text-[var(--bh-content-disabled)]",
39
+ "border-[length:var(--bh-border-width-default)] border-transparent bg-[var(--bh-interactive-danger-soft-default)] text-[var(--bh-content-danger-default)] hover:bg-[var(--bh-interactive-danger-soft-hover)] active:bg-[var(--bh-interactive-danger-soft-pressed)] focus-visible:border-[var(--bh-interactive-danger-default)] focus-visible:shadow-[var(--shadow-button-danger-focus)] disabled:bg-[var(--bh-interactive-danger-soft-disabled)] disabled:text-[var(--bh-content-disabled)]",
40
+ link: "bg-[var(--bh-interactive-ghost-default)] text-[var(--bh-content-brand-default)] hover:bg-[var(--bh-interactive-ghost-hover)] active:bg-[var(--bh-interactive-ghost-pressed)] focus-visible:border-[var(--bh-border-brand-strong)] disabled:text-[var(--bh-content-disabled)]",
39
41
  },
40
42
  size: {
41
43
  xs: "h-[var(--bh-button-xs-height)] gap-[var(--bh-button-gap-xs)] px-[var(--bh-button-xs-padding-x)] py-[var(--bh-button-sm-padding-y)] has-[>svg]:px-[var(--bh-button-xs-padding-x)]",
@@ -60,23 +62,30 @@ const buttonVariants = cva(
60
62
  type ButtonProps = React.ComponentProps<"button"> &
61
63
  VariantProps<typeof buttonVariants> & {
62
64
  asChild?: boolean
65
+ density?: ControlDensity
63
66
  }
64
67
 
68
+ type ButtonSize = NonNullable<VariantProps<typeof buttonVariants>["size"]>
69
+
65
70
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button({
66
71
  className,
67
72
  variant,
68
73
  size,
74
+ density,
69
75
  asChild = false,
70
76
  children,
71
77
  ...props
72
78
  }, ref) {
73
79
  const Comp = asChild ? Slot : "button"
80
+ const resolvedSize = resolveButtonSize(density, size)
74
81
 
75
82
  return (
76
83
  <Comp
84
+ data-density={density}
85
+ data-size={resolvedSize}
77
86
  data-slot="button"
78
87
  ref={ref}
79
- className={cn(buttonVariants({ variant, size, className }))}
88
+ className={cn(buttonVariants({ variant, size: resolvedSize, className }))}
80
89
  {...props}
81
90
  >
82
91
  {asChild ? children : <ButtonChildren>{children}</ButtonChildren>}
@@ -98,5 +107,17 @@ function ButtonChildren({ children }: { children: React.ReactNode }) {
98
107
  })
99
108
  }
100
109
 
110
+ function resolveButtonSize(
111
+ density: ControlDensity | undefined,
112
+ size: ButtonSize | null | undefined
113
+ ): ButtonSize | null | undefined {
114
+ if (!density) return size
115
+
116
+ const iconOnly = size?.startsWith("icon") ?? false
117
+ if (density === "compact") return iconOnly ? "icon-xs" : "xs"
118
+ if (density === "default") return iconOnly ? "icon-sm" : "sm"
119
+ return iconOnly ? "icon" : "default"
120
+ }
121
+
101
122
  export { Button, buttonVariants }
102
- export type { ButtonProps }
123
+ export type { ButtonProps, ControlDensity }